引言:
第二篇本来想讲讲摄影机的内容,但是还是想从渲染开始讲起。因为我们希望引擎更专注于渲染,等渲染结束我们再去慢慢的写摄影机和一些数学运算吧。
该篇主要思考基础材质类如何封装以及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多比对机制
本篇讲到这里,下一篇说一下具体实现细节