从零开始构建自己的WebGL3D引擎—材质篇

引言:
第二篇本来想讲讲摄影机的内容,但是还是想从渲染开始讲起。因为我们希望引擎更专注于渲染,等渲染结束我们再去慢慢的写摄影机和一些数学运算吧。
该篇主要思考基础材质类如何封装以及shader如何处理和优化、shader编译、uniform上传的细节和更好的实现
1.要设计一个好的材质,我们先想想别的引擎是如何设计的,有什么好的地方、有什么不足的地方
Three.js
使用方法(目前先拿自定义的shaderMaterial举例)

var shaderMaterial = new THREE.ShaderMaterial( {
     
   uniforms: uniforms,
   vertexShader: document.getElementById( 'vertexshader' ).textContent,
   fragmentShader: document.getElementById( 'fragmentshader' ).textContent,
   blending: THREE.AdditiveBlending,
   depthTest: false,
   transparent: true,
   vertexColors: true
} );

我们首先要自己构造uniforms(这个还挺麻烦、其实你写完shader就可以正则匹配出uniforms了、然后是传染vertexShader和fragmentShader、然后是一些其他参数)
如果想使用光照和阴影那?(下面截个water.js中mirror的代码看看)

var mirrorShader = {
     
   uniforms: UniformsUtils.merge( [
      UniformsLib[ 'fog' ],
      UniformsLib[ 'lights' ],
      {
     
         "normalSampler": {
      value: null },
         "mirrorSampler": {
      value: null },
         "alpha": {
      value: 1.0 },
         "time": {
      value: 0.0 },
         "size": {
      value: 1.0 },
         "distortionScale": {
      value: 20.0 },
         "textureMatrix": {
      value: new Matrix4() },
         "sunColor": {
      value: new Color( 0x7F7F7F ) },
         "sunDirection": {
      value: new Vector3( 0.70707, 0.70707, 0 ) },
         "eye": {
      value: new Vector3() },
         "waterColor": {
      value: new Color( 0x555555 ) }
      }
   ] ),

   vertexShader: [
      'uniform mat4 textureMatrix;',
      'uniform float time;',
      'varying vec4 mirrorCoord;',
      'varying vec4 worldPosition;',
      ShaderChunk[ 'fog_pars_vertex' ],
      ShaderChunk[ 'shadowmap_pars_vertex' ],

      'void main() {',
      '  mirrorCoord = modelMatrix * vec4( position, 1.0 );',
      '  worldPosition = mirrorCoord.xyzw;',
      '  mirrorCoord = textureMatrix * mirrorCoord;',
      '  vec4 mvPosition =  modelViewMatrix * vec4( position, 1.0 );',
      '  gl_Position = projectionMatrix * mvPosition;',

      ShaderChunk[ 'fog_vertex' ],
      ShaderChunk[ 'shadowmap_vertex' ],

      '}'
   ].join( '\n' ),

   fragmentShader: [
        ...

      ShaderChunk[ 'common' ],
      ShaderChunk[ 'packing' ],
      ShaderChunk[ 'bsdfs' ],
      ShaderChunk[ 'fog_pars_fragment' ],
      ShaderChunk[ 'lights_pars_begin' ],
      ShaderChunk[ 'shadowmap_pars_fragment' ],
      ShaderChunk[ 'shadowmask_pars_fragment' ],
      'void main() {',
      '  vec4 noise = getNoise( worldPosition.xz * size );',
      '  vec3 surfaceNormal = normalize( noise.xzy * vec3( 1.5, 1.0, 1.5 ) );',

      '  vec3 diffuseLight = vec3(0.0);',
      '  vec3 specularLight = vec3(0.0);',

      '  vec3 worldToEye = eye-worldPosition.xyz;',
      '  vec3 eyeDirection = normalize( worldToEye );',
      '  sunLight( surfaceNormal, eyeDirection, 100.0, 2.0, 0.5, diffuseLight, specularLight );',
      '  float distance = length(worldToEye);',
      '  vec2 distortion = surfaceNormal.xz * ( 0.001 + 1.0 / distance ) * distortionScale;',
      '  vec3 reflectionSample = vec3( texture2D( mirrorSampler, mirrorCoord.xy / mirrorCoord.w + distortion ) );',
      '  float theta = max( dot( eyeDirection, surfaceNormal ), 0.0 );',
      '  float rf0 = 0.3;',
      '  float reflectance = rf0 + ( 1.0 - rf0 ) * pow( ( 1.0 - theta ), 5.0 );',
      '  vec3 scatter = max( 0.0, dot( surfaceNormal, eyeDirection ) ) * waterColor;',
      '  vec3 albedo = mix( ( sunColor * diffuseLight * 0.3 + scatter ) * getShadowMask(), ( vec3( 0.1 ) + reflectionSample * 0.9 + reflectionSample * specularLight ), reflectance);',
      '  vec3 outgoingLight = albedo;',
      '  gl_FragColor = vec4( outgoingLight, alpha );',
      ShaderChunk[ 'tonemapping_fragment' ],
      ShaderChunk[ 'fog_fragment' ],
      '}'
   ].join( '\n' )
};
var material = new ShaderMaterial( {
     
   fragmentShader: mirrorShader.fragmentShader,
   vertexShader: mirrorShader.vertexShader,
   uniforms: UniformsUtils.clone( mirrorShader.uniforms ),
   transparent: true,
   lights: true,
   side: side,
   fog: fog
} );

想用灯光和阴影是非常的复杂,不仅需要熟悉three.js内置的一些shader、而且还要注意开启lights、等等,其实自定义shader想用光照非常的麻烦。
自定义一个材质用起来是非常的复杂,又要手动写uniforms,又要很理解three的一些shader和渲染逻辑才能用它内置的光照啊、阴影啊以及其他特性。
实现细节
下面我们再来看看three.js材质的实现细节:
WebGLPrograms->WebGLProgram->WebGLUniforms->WebGLShader (大概是设计了这几个类来维护和管理材质)

看看真实的材质解析细节:1.通过一大推的过程来判断是否要更新材质

if ( material.version === materialProperties.__version ) {
     
   if ( materialProperties.program === undefined ) {
     
      material.needsUpdate = true;
   } else if ( material.fog && materialProperties.fog !== fog ) {
     
      material.needsUpdate = true;
   } else if ( materialProperties.environment !== environment ) {
     
      material.needsUpdate = true;
   } else if ( materialProperties.needsLights && ( materialProperties.lightsStateVersion !== lights.state.version ) ) {
     
      material.needsUpdate = true;
   } else if ( materialProperties.numClippingPlanes !== undefined &&
      ( materialProperties.numClippingPlanes !== _clipping.numPlanes ||
      materialProperties.numIntersection !== _clipping.numIntersection ) ) {
     
      material.needsUpdate = true;
   } else if ( materialProperties.outputEncoding !== _this.outputEncoding ) {
     
      material.needsUpdate = true;
   }
}
if ( material.version !== materialProperties.__version ) {
     
   initMaterial( material, scene, object );
   materialProperties.__version = material.version;

}

2.然后开始上传一些系统的uniforms(这里太多了就不讲了)、由于uniform是material的属性,因此还要做缓存,如果有改变才会上传,这里又涉及到判断缓存的性能问题。
因此three.js如果发现uniform是矩阵就直接上传,如果是vec2、vec3、vec4、float就先和缓存比对,然后上传,想当的浪费性能和麻烦

3.处理灯光的信息,每帧都要计算最终的灯光信息,想当的麻烦和耗费性能
(代码太多,不一一列出了)

问题思考
那我们该如何改进那?

opengl引擎材质设计(都大同小异,先以OGRE为例)
材质设计与细节
Material->Technique->Pass->TexUnit

Material下面首先是Technique,Technique主要是可以做LOD用,比如我们可以在近处指定渲染Material下的第1个Technique、远处指定渲染Material下的第2个Technique。
而且Technique和后处理也是关联在一起的、比如在执行Glow的时候该材质时该渲染Material下的哪个Technique。
Technique下是Pass,其实Pass相当于Three的材质的概念
Pass下是TexUnit,主要是管理贴图
这个材质类就非常合理了,既灵活又可以实现很多复杂的功能,如果用Three的材质结构就比较死板,很多问题比较难解决。但是仔细研究了下Three的源码发现Three有个数组
材质的概念。具体用法:

geometry.addGroup( offset, count, i );
let materialArr = [material1, material2,...]
let mesh = new THREE.Mesh(geometry, materialArr);

这个设计虽然感觉不太好理解,但是确实是个不错的设计,可以在geometry中分组指定哪些三角面使用哪个材质渲染,这样我们如果设计多个材质,所有三角面都渲染材质1、材质2、…
这样就相当于实现了多Pass的渲染了。也是个不错的选择。虽然在渲染的处理上稍微复杂了些,后面做后处理也会有点麻烦,但是笔者的引擎也准备选择这种方式,为什么那?因为js在
遍历对象的时候其实还是比较慢的,如果材质类设计成比较复杂的结构,无疑对性能又是比较大的开销。一切要为性能考虑啊。目前笔者材质类设计如下:

Material->WebGLShader

使用方式:

let shader = Beauty3D.ShaderLib.getShader('basic');
let material = new Beauty3D.Material({
     shader:shader})
material.setUniforms('color', '#ffffff');

我们多封装一个shader类来处理Shader,暴露给用户,material使用shader类自动构建uniforms,用户可以手动调用setUniforms更改uniform并且自动标脏,这样可以省去uniform多比对机制
本篇讲到这里,下一篇说一下具体实现细节

你可能感兴趣的:(three.js,shader,webgl)