将ShaderToy中的着色器移至Three.js
更多示例代码:github 地址
更多示例效果:demo 地址
基本的转换规则:
- 把 ShaderToy 特定变量作为 uniform 变量添加到自己的着色器中,如 iGlobalTime、iResolution 等
- 把 mainImage(out vec4 z, in vec2 w)重命名为 main()
- 把输出变量重命名为 gl_FragColor,而输入变量即为顶点着色器里的输出
- 利用 three.js 内置的 uv 变量,重写 shadertoy 里对于坐标的计算
遵照这种基本模式,可以在 three 中转换 shadertoy 中的着色器代码。
我首先在 shadertoy 上选了一个看起来很简单的效果,上升气泡。
大概是这个样子的。
下面是它的 shader 代码:
短小精悍,非常适合做最初的测试。
然后就是去尝试对它进行改造了。
首先,我们知道 shadertoy 中提供的只是片元着色器,所以我需要自己首先补充一个顶点着色器,代码如下:
precision mediump float;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
很简单,只需要做一件事,转换目标的坐标系。
第二步是抄代码,把 shadertoy 里的代码直接复制下来,粘贴到我们的片元着色器中。
第三步,改造。我们不需要知道它是如何完成动画和几何体构建的,只需要按照模式,改掉不兼容的地方即可。
3.1 我们看到代码里有两个以 “i” 开头的变量,iGlobalTime 和 iResolution ,这两个在 shader 中对应的是 uniform 变量,需要我们在 js 代码里传进来。我暂时不必知道这两个变量将被赋予什么样的值,先在最顶部手动去定义这两个变量即可。
3.2 mainImage() => main();fragColor => gl_FragColor。
3.3 修改 uv 坐标:shadertoy 中没有内置 uv 变量,所以它需要进行一次计算,而我们在 three.js 里可以跳过这一步,直接把第 6 行改为,’ -1.0 + 2.0 * vUv ‘。
到这一步,fragment shader 改造结束:
precision mediump float;
uniform float iGlobalTime;
uniform vec2 iResolution;
varying vec2 vUv;
void main() {
vec2 uv = -1.0 + 2.0 * vUv;
uv.x *= iResolution.x / iResolution.y;
vec3 color = vec3(0.8 + 0.2 * uv.y);
for( int i = 0; i < 40; i++ ){
// bubble seeds
float pha = sin(float(i) * 546.13 + 1.0) * 0.5 + 0.5;
float siz = pow( sin(float(i) * 651.74 + 5.0) * 0.5 + 0.5, 4.0 );
float pox = sin(float(i) * 321.55 + 4.1) * iResolution.x / iResolution.y;
// buble size, position and color
float rad = 0.1 + 0.5 * siz;
vec2 pos = vec2( pox, -1.0 - rad + (2.0 + 2.0 * rad) * mod(pha + 0.1 * iGlobalTime * (0.2 + 0.8 * siz), 1.0));
float dis = length( uv - pos );
vec3 col = mix(vec3(0.94, 0.3, 0.0), vec3(0.1, 0.4, 0.8), 0.5 + 0.5 * sin(float(i) * 1.2 + 1.9));
// col += 8.0 * smoothstep( rad * 0.95, rad, dis );
// render
float f = length(uv-pos)/rad;
f = sqrt(clamp(1.0 - f * f, 0.0, 1.0));
color -= col.zyx * (1.0 - smoothstep( rad * 0.95, rad, dis )) * f;
}
color *= sqrt(1.5 - 0.5 * length(uv));
gl_FragColor = vec4(color, 1.0);
}
这时还没结束,我们现在依然不知道 iGlobalTime 和 iResolution 是什么。所以我们还是需要简单的看一下它的代码,不难发现,iGlobalTime 是一个时间流,代表的是帧率,而 iResolution 代表的则是坐标,我们可以理解为是这个 shader 容器的长和宽。
了解了它们是什么,我们自然就能够知道如何给它们赋值:
// ...
// 初始值
var uniforms = {
iGlobalTime: {
type: 'f',
value: 1.0
},
iResolution: {
type: 'v2',
value: new THREE.Vector2()
}
}
uniforms.iResolution.value.x = 1 // window.innerWidth;
uniforms.iResolution.value.y = 1 // window.innerHeight;
// 加到shader材质中
var material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: document.getElementById('general').textContent,
fragmentShader: document.getElementById('frag1').textContent
})
// ...
mesh.startTime = Date.now() // 在网格中保存一个初始时间
mesh.uniforms = uniforms // 在网格中保存uniform变量
// ...
// 逐帧更新时间
function render() {
var time = (Date.now() - mesh.startTime) / 1000
mesh.uniforms.iGlobalTime.value = time
// ...
}
效果:
完美改造。
更复杂的 shader
当然,这只是一个简单例子,实际在 three.js 中,很多时候我们是需要把着色器的效果作用于场景,而不是单个的网格内部,这时上述套路很明显就不再适合了。
如何在场景中随意发挥着色器的威力,把 shadertoy 或者自己写的着色器融合进后处理通道中,这也是我目前正在研究和解决的问题,在解决之后会继续更新这篇文章。