通过方法onBeforeCompile为three.js现有材质增加功能

2,813 阅读2分钟

熟悉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这个变量:

可以看到shader是一个对象,shader对象有name, uniforms, vertexShader, fragmentShader四个属性,其中vertexShader是顶点着色器代码,fragmentShader是片元着色器代码,vertexShader和fragmentShader都是字符串格式的,uniforms是一个对象,它的属性,我们在实例化材质的时候可以直接作为材质的属性进行设置或者修改。

我们现在看一下这个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应用的一个简单例子,我们可以根据产品或者场景的需求,添加或者修改相应的逻辑,实现我们想要的效果。希望这篇文章可以给大家带来小小的帮助。