将shadertoy上的WebGL移植到桌面OpenGL中

shadertoy上有许多着色器的炫酷示例,充分展现了着色器和可编程管线的强大之处。网站的名字起的很有意思,真的感觉上面的大牛写着色器就像是在玩玩具,随心所欲,天马行空。我什么时候可以达到写着色器犹如玩玩具的水平啊!

上面的示例都是基于WebGL写的,而且都是只用了fragment shader,这让我感觉很诧异,只用fragment shader就可以写出各种3D效果,甚至是真实感渲染!

更新:其实是使用了ray tracing(光线跟踪)算法,该算法就是基于片段上的每一个pixel来进行渲染的,它以每一个pixel为出发点,通过跟踪这个点发出的光线(其实是反过来的)来计算这个点最终的颜色,优点是渲染效果很好,缺点是渲染速度很慢

目前我还不会WebGL,所以并不能理解其中的奥秘,只能硬生生的将这些fragment shader直接移植到桌面OpenGL的fragment shader中,通过uniform传递一些全局参数。vertex shader只起到传递作用。调用程序中我先建一个从(0, 0)-(window_width, window_height)映射到(-1, -1)-(1, 1)的数组,然后送入vertex buffer中,让fragment shader对每一个点做一次运算。运行了几个示例,都是很卡,毕竟是逐点运算,不过WebGL怎么就做到运行地那么流畅呢?我很是好奇啊,等以后学了WebGL再说。

移植的代码参考了shadertoy-render,这是一个用python写的移植程序并可输出视频文件。


下面上代码:

调用程序

1
2
3
4
5
6
7
8
9
10
GLfloat g_vertex_buffer_data[window_width][window_height][2];
for(int i = 0; i < window_width; i++)
for(int j = 0; j < window_height; j++) {
g_vertex_buffer_data[i][j][0] = 2.0f / window_width * i - 1;
g_vertex_buffer_data[i][j][1] = 2.0f / window_height * j - 1;
}
GLuint vertexbuffer;
glGenBuffers(1, &vertexbuffer);
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW);

渲染前先做从pixel到vertex的坐标映射。pixel是经过光栅化的一个像素,相对于窗口平面来说,而vertex是光栅化之前的一个顶点,相对于3D空间来说,当然也可以是2D空间。

更新:这个繁琐的映射过程其实是不需要的,只需要画出画出铺满整个窗口的矩形即可,代码如下:

1
2
3
4
5
6
7
8
9
10
11
GLfloat g_vertex_buffer_data[12] = {
-1,-1,
-1, 1,
1, 1,
-1,-1,
1, 1,
1,-1};
GLuint vertexbuffer;
glGenBuffers(1, &vertexbuffer);
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW);

详见另一篇博文《对片段着色器的一点理解》


1
2
glUniform1f(glGetUniformLocation(programID, "iGlobalTime"), glfwGetTime());
glUniform3f(glGetUniformLocation(programID, "iResolution"), window_width, window_height, 0);

渲染过程中需要传递两个全局参数,为了简单,目前只用到了窗口大小和时间两个参数。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
glUseProgram(programID);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
glVertexAttribPointer(
0, // attribute 0
2, // size
GL_FLOAT, // type
GL_FALSE, // normalized?
0, // stride
(void*)0 // array buffer offset
);
glUniform1f(glGetUniformLocation(programID, "iGlobalTime"), glfwGetTime());
glUniform3f(glGetUniformLocation(programID, "iResolution"), window_width, window_height, 0);
glDrawArrays(GL_POINTS, 0, sizeof(g_vertex_buffer_data) / 8);
glDisableVertexAttribArray(0);

准备工作做好就可以渲染了。


vertex shader

1
2
3
4
5
6
7
8
#version 330 core

layout(location = 0) in vec2 position;

void main()
{
gl_Position = vec4(position, 0.0, 1.0);
}

简单的传递顶点。


fragment shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#version 330 core

uniform vec3 iResolution; // viewport resolution (in pixels)
uniform float iGlobalTime; // shader playback time (in seconds)
uniform vec4 iMouse; // mouse pixel coords
uniform vec4 iDate; // (year, month, day, time in seconds)
uniform float iSampleRate; // sound sample rate (i.e., 44100)
uniform sampler2D iChannel0; // input channel. XX = 2D/Cube
uniform sampler2D iChannel1; // input channel. XX = 2D/Cube
uniform sampler2D iChannel2; // input channel. XX = 2D/Cube
uniform sampler2D iChannel3; // input channel. XX = 2D/Cube
uniform vec3 iChannelResolution[4]; // channel resolution (in pixels)
uniform float iChannelTime[4]; // channel playback time (in sec)

//insert WebGL code here

void main()
{
mainImage(gl_FragColor, gl_FragCoord.xy);
}

前面的几个uniform是WebGL中会用到的全局参数,目前只用到了窗口大小和时间。
WebGL的主函数为mainIgame(),所以在main()中调用一下就好了,函数的第一个参数是一个传出参数,是经过计算后的当前片段的颜色,第二个参数是一个传入参数,是当前片段的坐标,范围为0-window_width和0-window_height。