ShaderToy在WebGL的GLSL中是如何被集成并运行的?

Number of views 152

我最近一直在Shadertoy上闲逛,努力学习更多关于OpenGL和GLSL的知识。根据我目前的理解,OpenGL用户首先必须准备所有要使用的几何体,并配置OpenGL的后端渲染数据(允许的灯光数量,纹理存储等)。完成后,用户必须在OpenGL程序编译之前提供至少一个顶点着色器程序和一个片段着色器程序。然而,当我在Shadertoy上查看代码示例时,我只看到一个着色器程序,并且使用的大多数几何形状似乎都直接写入GLSL代码中。这是怎么做到的呢?我的猜测是顶点着色器已经预先准备好了,而可编辑/样本着色器只是一个片元着色器。但这并不能解释一些更复杂的几何例子……谁能解释一下Shadertoy里的复杂几何体是怎么显示出来的?

1 Answers

哈哈,这个问题真是问对地方了,刚好这个平台集成了ShaderToy。
首先要明白,ShaderToy 是一个编写片元着色器的一个小的IDE。

那么什么是片元着色器?

如果你渲染一个全屏四边形,意味着这四个点分别放置在视口的四个角落,那么这个四边形的所有像素的信息的处理阶段就被称为片元着色器(DirectX称为像素着色器),因为可以说现在每个片元正好对应屏幕上的一个像素。所以片元着色器是一种针对全屏四边形的像素着色器。

因此,属性总是相同的,顶点着色器也是如此:

positions = [ [-1,1], [1,1], [-1,-1], [1,-1] ]
uv = [ [0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0] ]

这个四边形通常以 TRIANGLE_STRIP 的形式进行图元装配从而渲染。有些人会使用片元着色器的内置变量 gl_FragCoord,然后将其除以屏幕的尺寸,即宽高,得出的结果就是uv(0~1)的取值范围。
简单举一下顶点着色器与片元着色器的例子:
顶点着色器:

attribute vec2 aPos;
attribute vec2 aUV;
varying vec2 vUV;

void main() {
    gl_Position = vec4(aPos, 0.0, 1.0);
    vUV = aUV;
}

片元着色器:

uniform vec2 uScreenResolution;
varying vec2 vUV;

void main() {
    // vUV 等于 gl_FragCoord/uScreenResolution
    // 这里可以做一些片元着色器其它相关的算法
    gl_FragColor = vec3(someColor);
}

而ShaderToy的做法会默认为用户提供一些 uniform,如 iResolution(即 uScreenResolution)、iGlobalTime、iMouse 等,你可以在你的片元着色器中直接对它们进行拿来即用。

对于直接在片元着色器(即像素着色器)中渲染几何体,开发者通常使用一种称为RayMarching(光线行进)的技术。会比较复杂,简单讲:你可以通过一些数学公式来表示你的几何体(通常为SDF),然后在片元着色器中,当你想检查某个像素是否是你几何体的一部分时,你使用那个公式来获取信息。
简单用这个平台运行下ShaderToy,使用方式可以看下参考下平台使用指南:

float opSmoothUnion( float d1, float d2, float k )
{
    float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 );
    return mix( d2, d1, h ) - k*h*(1.0-h);
}

float sdSphere( vec3 p, float s )
{
  return length(p)-s;
} 

float map(vec3 p)
{
	float d = 2.0;
	for (int i = 0; i < 16; i++) {
		float fi = float(i);
		float time = iTime * (fract(fi * 412.531 + 0.513) - 0.5) * 2.0;
		d = opSmoothUnion(
            sdSphere(p + sin(time + fi * vec3(52.5126, 64.62744, 632.25)) * vec3(2.0, 2.0, 0.8), mix(0.5, 1.0, fract(fi * 412.531 + 0.5124))),
			d,
			0.4
		);
	}
	return d;
}

vec3 calcNormal( in vec3 p )
{
    const float h = 1e-5; // or some other value
    const vec2 k = vec2(1,-1);
    return normalize( k.xyy*map( p + k.xyy*h ) + 
                      k.yyx*map( p + k.yyx*h ) + 
                      k.yxy*map( p + k.yxy*h ) + 
                      k.xxx*map( p + k.xxx*h ) );
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord/iResolution.xy;
    
    // screen size is 6m x 6m
	vec3 rayOri = vec3((uv - 0.5) * vec2(iResolution.x/iResolution.y, 1.0) * 6.0, 3.0);
	vec3 rayDir = vec3(0.0, 0.0, -1.0);
	
	float depth = 0.0;
	vec3 p;
	
	for(int i = 0; i < 64; i++) {
		p = rayOri + rayDir * depth;
		float dist = map(p);
        depth += dist;
		if (dist < 1e-6) {
			break;
		}
	}
	
    depth = min(6.0, depth);
	vec3 n = calcNormal(p);
    float b = max(0.0, dot(n, vec3(0.577)));
    vec3 col = (0.5 + 0.5 * cos((b + iTime * 3.0) + uv.xyx * 2.0 + vec3(0,2,4))) * (0.85 + b * 0.35);
    col *= exp( -depth * 0.15 );
	
    // maximum thickness is 2m in alpha channel
    fragColor = vec4(col, 1.0 - (depth - 0.5) / 2.0);
}