熟悉three.js的同学都知道,three.js为了方便开发封装了一些比较常用的材质,比如基本材质(MeshBasicMaterial),冯氏材质(MeshPhongMaterial),物理材质(MeshPhysical),标准材质(MeshStandardMaterial)等,大部分时候,使用这些现有的材质,配合贴图和参数设置就可以实现我们想要的视觉效果。
但在某些场景下,只是使用现有材质,配合贴图和参数设置是没法实现我们想要的效果的,比如模型的碎片化效果和扭曲效果等。这时候我们通常有两种选择:1,使用shaderMaterial或者RawShaderMaterial, 编写shader代码;2,使用材质的基类Material提供的方法onBeforeCompile为现有材质增加功能。这篇文章的主题是第二种方法。至于第一种方法,我会再单独写一篇文章详细的介绍。
three.js的官方文档onBeforeCompile的介绍是这样的: An optional callback that is executed immediately before the shader program is compiled. This function is called with the shader source code as a parameter. Useful for the modification of built-in materials. 翻译一下,大意是,在编译shader程序之前立即执行的可选回调,此函数使用shader源码作为参数,用于修改内置材质。这个回调不支持.clone(), .copy(), .toJSON()等方法。
下面我就以three.js官网的一个demo为例对这个方法的使用方式做一个简单的介绍。相关代码如下:
function buildTwistMaterial( amount ) {
var material = new THREE.MeshNormalMaterial();
material.onBeforeCompile = function ( shader ) {
shader.uniforms.time = { value: 0 };
shader.vertexShader = 'uniform float time;\n' + shader.vertexShader;
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
[
float theta = sin( time + position.y ) / ${ amount.toFixed( 1 ) };`,
'float c = cos( theta );',
'float s = sin( theta );',
'mat3 m = mat3( c, 0, s, 0, 1, 0, -s, 0, c );',
'vec3 transformed = vec3( position ) * m;',
'vNormal = vNormal * m;'
].join( '\n' )
);
material.userData.shader = shader;
};
// Make sure WebGLRenderer doesnt reuse a single program
material.customProgramCacheKey = function () {
return amount;
};
return material;
}
从函数名我们大致可以猜出来这个函数想实现模型的扭曲效果。在函数里使用的是法线材质(MeshNormalMateriaL),对于法线材质的具体使用方法感兴趣的同学可以查阅three.js的官方文档详细的了解,这里不做过多的介绍。
我们看到在获取法线材质(MeshNormalMaterial)的实例后,紧接着就调用了onBeforeCompile这个方法。在这个方法的回调函数里拿到了材质的shader代码。我们可以打开chrome浏览器的调试工具,打印一下shader这个变量:

我们现在看一下这个demo中扭曲效果实现的逻辑。shader.uniforms.time = { value: 0 } 这行代码的作用是通过uniforms这个对象传入了一个自定义的变量time, 我们可以看到在render函数里,time这个变量是随着动画函数requestAnimationFrame的刷新,一直在变化的:
function animate() {
requestAnimationFrame( animate );
render();
stats.update();
}
function render() {
if ( materialShader ) {
materialShader.uniforms.time.value = performance.now() / 1000;
}
renderer.render( scene, camera );
}
我们接着分析buildTwistMaterial这个函数。通过uniforms传入time这个自定义变量之后,我们需要在shader代码里定义一个同名变量time对它进行接收,shader.vertexShader = 'uniform float time;\n' + shader.vertexShader; 这行代码就起到了接收time变量的作用。
紧接着这个等式是用我们编写的代码——也是实现扭曲效果的主要逻辑,对shader里原有代码#include <begin_vertex>进行替换,这样就把扭曲效果的实现逻辑加入到了顶点着色器中。float theta = sin( time + position.y ) / ${ amount.toFixed( 1 ) }; 这行代码是根据我们传入的time变量,得到了一个theta变量,可以把它当成一个弧度角来用。在这个等式中,position.y, 表示顶点的y轴坐标,amount是传入的一个变量。接下来求出了theta角的正弦值sin(theta)和余弦值cos(theta), 然后我们获得了一个3×3的矩阵m,熟悉shader编码的同学应该可以很容易的看出这是一个旋转矩阵,旋转轴是y轴。等式vNormal = vNormal * m;的作用是使用法线矩阵vNormal和旋转矩阵相乘得到新的法线矩阵。
因为变量time是随着时间变化的,旋转矩阵m也是随着时间变化的,最终法线矩阵vNormal也是随着时间变化的。经过上述一系列操作之后,我们就实现了模型的扭曲效果。
当然这只是onBeforeCompile应用的一个简单例子,我们可以根据产品或者场景的需求,添加或者修改相应的逻辑,实现我们想要的效果。希望这篇文章可以给大家带来小小的帮助。