Shadertoy 的坐标转换

945 阅读1分钟
原文链接: zhuanlan.zhihu.com

两段代码

最近在看 Shadertoy 网站,发现很多 shader 都有这行代码。

vec2 uv = (2.0 * fragCoord.xy - iResolution.xy) / min(iResolution.y, iResolution.x);

这行代码一时看不明白。慢慢细看,实际上是跟如下代码等价的。

vec2 uv = fragCoord.xy / iResolution.xy
uv = 2.0 * uv - 1.0;
if (iResolution.x > iResolution.y) {
    uv.x *= iResolution.x / iResolution.y;
} else {
    uv.y *= iResolution.y / iResolution.x;
}

在 shadertoy 中,iResolution 表示画布像素高宽。

  • fragCoord.xy / iResolution.xy 会将坐标转换到 [0, 1] 之间。
  • uv = 2.0 * uv - 1.0 将坐标转换到 [-1, 1] 之间,中央为原点(0,0)。
  • 随后的判断,保持短边为 [-1, 1],长的那条边坐标相应放大。

等价解释

上述两段代码等价,一下子比较难看出来,分两种情况分析。当横屏时,iResolution.x > iResolution.y。第一段代码相应为

vec2 uv = (2.0 * fragCoord.xy - iResolution.xy) / iResolution.y;
=>
uv.x = (2.0 * fragCoord.x - iResolution.x) / iResolution.y
uv.y = (2.0 * fragCoord.y - iResolution.y) / iResolution.y
=>
uv.x = ((2.0 * fragCoord.x - iResolution.x) / iResolution.x) * (iResolution.x / iResolution.y)
uv.y = ((2.0 * fragCoord.y - iResolution.y) / iResolution.y) * 1.0;
=> 
uv.x = (2.0 * fragCoord.x / iResolution.x - 1.0) * (iResolution.x / iResolution.y)
uv.y = (2.0 * fragCoord.y / iResolution.y - 1.0) * 1.0;

从而就跟这几行代码等价了。

vec2 uv = fragCoord.xy / iResolution.xy
uv = 2.0 * uv - 1.0;
uv.x *= iResolution.x / iResolution.y;

同理也可以分析竖屏时的情况。shadertoy 都是横屏,因而有时会省略 iResolution.x > iResolution.y 这个判断。也很经常写成

vec2 uv = 2.0 * (fragCoord.xy / iResolution.xy) - 1.0;
uv.x *= iResolution.x / iResolution.y;

但这样将效果移植到竖屏的手机上,坐标是不对的。

例子

以下这个例子,在画布中央画了一个红色的圆。为简单,画出的圆是有锯齿。

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = (2.0 * fragCoord.xy - iResolution.xy) / min(iResolution.y, iResolution.x);
    float len = length(uv);
    
    if (len <= 0.5) {
        fragColor = vec4(1.0, 0.0, 0.0, 1.0);
    } else {
        fragColor = vec4(0.0, 0.0, 0.0, 1.0);
    }
}

当横屏时(宽大于高),会显示成。

横屏

当竖屏时(宽小于高),会显示成。

竖屏

想画出没有锯齿的圆,可以这样写

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
   vec2 uv = (2.0 * fragCoord.xy - iResolution.xy) / min(iResolution.y, iResolution.x);
   float d = length(uv) - 0.5;
   float t = clamp(d * min(iResolution.y,iResolution.x) * 0.25, 0.0, 1.0);
   
   vec4 bg = vec4(0.0, 0.0, 0.0, 1.0);
   vec4 circle = vec4(1.0, 0.0, 0.0, 1.0 - t);
   
   fragColor = mix(bg, circle, circle.a);
}

实际上参考了这里。A Simple Circle