從零開始構建自己的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多比對機制
本篇講到這裏,下一篇說一下具體實現細節

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章