引言:
第二篇本來想講講攝影機的內容,但是還是想從渲染開始講起。因爲我們希望引擎更專注於渲染,等渲染結束我們再去慢慢的寫攝影機和一些數學運算吧。
該篇主要思考基礎材質類如何封裝以及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多比對機制
本篇講到這裏,下一篇說一下具體實現細節