今天我们将要和大家分享一些 WebGL 实验,在这个实验中我们将创建一个非常逼真的雨滴效果,并把它放到不同的场景中去。在这篇文章中,我们将给出制作这种效果所用到的一些一般性技术和技巧的概览。
请注意:文中制作的效果还处于试验阶段,可能无法在所有浏览器中都看到预期的效果。***使用 Chrome。
入门
如果我们想制作一个基于现实世界的效果,那么首先我们需要剖析一下它看起来究竟是什么样子的,这样制作出的效果才能显得真实。如果你去找一些水滴落在窗户上的图片来看(当然,你肯定已经在生活中观察过他们了),你会发现由于折射,雨滴似乎会把它后面的图像上下颠倒。
图片来源:Wikipedia, GGB reflection in raindrop
同时你还会看到相互之间距离很近的雨滴会合并成一个——而且如果超过了一定的尺寸,它就会向下滑落,并且留下一道小小的痕迹。
为了模拟这种行为,我们必须绘制大量的雨滴,在每一帧上都更新它们的折射效果,并且要在一个合适的帧率下做这些事情,为此我们需要极好的性能———所以,为了能够使用显卡的硬件加速,我们将使用 WebGL。
WebGL
WebGL 是一个绘制 2D 和 3D 图形的 JavaScript 接口,并且允许使用 GPU 以获得更好的性能。它基于 OpenGL ES,着色器由一门叫做 GLSL 的语言写成,而不是 JS。
总之,如果你仅仅做过网页开发,那么它看起来是很难使用的——这不仅仅是一门新的语言,而且还是一个全新的概念——但是一旦你掌握了一些核心的概念,它就会变得容易不少。
在这篇文章中我们将仅给出一些基本的使用示例,更多深入的解析请参阅 WebGl Fundamentals 。
首先我们需要一个 canvas 标签。WebGL 是在 canvas 上绘制的,它是一个绘制环境,类似于我们用 getContext('2d') 获取到的绘制环境。
- <canvas id="container" width="800" height="600"></canvas>
- var canvas = document.getElementById("container");
- var gl = canvas.getContext("webgl");
接下来我们需要一段程序,它由顶点着色器和片段着色器(译注:『片段着色器』又称『像素着色器』)构成。着色器就是一些函数:顶点着色器在每个顶点执行一次,而片段着色器在每个像素上都被调用一次。它们的任务分别是返回坐标和颜色。这是我们的 WebGL 应用的核心。
首先来创建我们的着色器。这是一个顶点着色器,我们不会对顶点做任何修改,所以简单地让数据穿过它就好了:
- <script id="vert-shader" type="x-shader/x-vertex">
- // gets the current position
- attribute vec4 a_position;
- void main() {
- // returns the position
- gl_Position = a_position;
- }
- </script>
这个是片段着色器。这个着色器将会根据坐标来设置每个像素点的颜色。
- <script id="frag-shader" type="x-shader/x-fragment">
- precision mediump float;
- void main() {
- // current coordinates
- vec4 coord = gl_FragCoord;
- // sets the color
- gl_FragColor = vec4(coord.x/800.0,coord.y/600.0, 0.0, 1.0);
- }
- </script>
现在我们把着色器连接到 WebGL 环境中去:
- function createShader(gl,source,type){
- var shader = gl.createShader(type);
- source = document.getElementById(source).text;
- gl.shaderSource(shader, source);
- gl.compileShader(shader);
- return shader;
- }
- var vertexShader = createShader(gl, 'vert-shader', gl.VERTEX_SHADER);
- var fragShader = createShader(gl, 'frag-shader', gl.FRAGMENT_SHADER);
- var program = gl.createProgram();
- gl.attachShader(program, vertexShader);
- gl.attachShader(program, fragShader);
- gl.linkProgram(program);
- gl.useProgram(program);
接下来我们创建一个对象然后在它上面绘制我们的着色器。这里我们来画个矩形——确切地说,画两个矩形。
- // create rectangle
- var buffer = gl.createBuffer();
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
- gl.bufferData(
- gl.ARRAY_BUFFER,
- new Float32Array([
- -1.0, -1.0,
- 1.0, -1.0,
- -1.0, 1.0,
- -1.0, 1.0,
- 1.0, -1.0,
- 1.0, 1.0]),
- gl.STATIC_DRAW);
- // vertex data
- var positionLocation = gl.getAttribLocation(program, "a_position");
- gl.enableVertexAttribArray(positionLocation);
- gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
***,绘制整个图像:
- gl.drawArrays(gl.TRIANGLES, 0, 6);
结果如下:
之后你可以尽情玩弄这些着色器以便搞明白它是怎么工作的。你可以在 ShaderToy 上找到很多很棒的着色器的例子。
雨滴
现在让我们来看看如何制作雨滴的效果。首先我们来看一下单个的雨滴是什么样的:
现在这里发生了很多事情。
Alpha 通道变成了这样是因为我们使用了类似于文章 Creative Gooey Effects 中提到的一个技术来让雨滴粘连到一起。
颜色变成这样也是有原因的:我们使用了类似 法线贴图 的技术来实现折射效果。我们将利用雨滴的颜色来获取我们透过雨滴看到的贴图的坐标。这是没有遮罩时它的样子:
在这张图片中,我们将通过绿色通道的数据来获取 X 坐标,通过红色通道的数据来获取 Y 坐标。
现在我们可以写我们的着色器了,并且可以同时使用贴图数据和雨滴的位置来翻转并扭曲雨滴后方的贴图了。
下雨过程
在创建雨滴之后,我们就可以开始对下雨进行模拟了。
让雨点之间相互作用是很难快速计算的——随着新的雨点的到来,运算量将会呈指数级增长——所以我们必须做一点优化。
在这个示例中,我把大雨点和小雨点分开了。小雨点绘制在一个单独的 canvas 上,并且没有没追踪。这样我就可以绘制上千个小雨滴而且不会让速度有任何减慢。缺点是它们都是静态的,而且由于我们每帧都在创建新雨滴,他们将会累积起来。为了修复这个问题,我们将会使用大一点的雨滴。
由于大雨滴是会移动的,于是我们可以利用它们来清除它们下方的小雨滴。擦除操作在 canvas 中比较麻烦:实际上我们还是要画一些东西出来,但是要使用 globalCompositeOperation='destination-out。因此,每当一个大的雨滴移动,我们就会在小雨滴的 canvas 上绘制一个圆,并使用复合操作来清除这些雨滴,使效果更加逼真。
***,我们把所有这些绘制在一个大的 canvas 上,然后把它作为我们的 WebGL 着色器的贴图。
为了把它做得更轻便一点,我们要利用背景会失焦这一事实,因此我们用了一个小尺寸的贴图,然后把它放大。在 WebGL 中,贴图的尺寸会直接影响到性能。我们需要用另外一个没有失焦的贴图来制作雨滴。模糊是一个代价很高的操作,实时的模糊处理应该尽量避免掉——但是由于雨滴很小,我们可以把贴图也变得很小。
总结
为了制作像雨滴这样的逼真的效果,我们需要考虑很多复杂的细节。先将效果从现实世界中分离出来是重建任何一个效果的关键所在,一旦知道了它在现实世界中是如何工作的,我们就可以把它的行为映射到虚拟世界。有了 WebGL,我们可以获得很高的性能(我们可以使用显卡的硬件加速)因此对于这类效果,它是一个很不错的选择。
希望各位喜欢这个实验并且受到启发!
示例(http://tympanus.net/Development/RainEffect/)
源码(http://tympanus.net/Development/RainEffect/RainEffect.zip)