three.js 中 webgl_tiled_forward 是比较难理解的一个官方样例,我第一次看时,看得一头雾水,看得快睡着了,比较枯燥。。。
这个例子,就是展示场景中 有多个光源时,如何提升渲染效率;思路就是把屏幕按行列分割成一个个 tile 格子,片元着色器去计算光照时,根据片元所在tile 格子,剔除不相关光源;这样每个片元着色器,在计算光照时 就不用考虑所有光源,极大提升了渲染的性能。
以上,是比较粗略的概括,现在结合代码来,详细看看。
首先,这个例子中 创建了 32个会影响渲染的点光源。
负责创建光源的代码在 init 函数中:
const Heads = [
{ type: 'physical', uniforms: { 'diffuse': 0x888888, 'metalness': 1.0, 'roughness': 0.66 }, defines: {} },
{ type: 'standard', uniforms: { 'diffuse': 0x666666, 'metalness': 0.1, 'roughness': 0.33 }, defines: {} },
{ type: 'phong', uniforms: { 'diffuse': 0x777777, 'shininess': 20 }, defines: {} },
{ type: 'phong', uniforms: { 'diffuse': 0x555555, 'shininess': 10 }, defines: { TOON: 1 } }
];
function init( geom ) {
const sphereGeom = new THREE.SphereGeometry( 0.5, 32, 32 );
const tIndex = Math.round( Math.random() * 3 );
Object.keys( Heads ).forEach( function ( t, index ) {
以上,Heads 保存了四个 参数对象 用于创建四种不同的材质,这里有四次循环。
function init( geom ) {
const sphereGeom = new THREE.SphereGeometry( 0.5, 32, 32 );
const tIndex = Math.round( Math.random() * 3 );
Object.keys( Heads ).forEach( function ( t, index ) {
...
for ( let i = 0; i < 8; i ++ ) {
for ( let i = 0; i < 8; i ++ ) {
const color = new THREE.Color().setHSL( Math.random(), 1.0, 0.5 );
const l = new THREE.Group();
l.add( new THREE.Mesh(
sphereGeom,
new THREE.MeshBasicMaterial( {
color: color
} )
) );
l.add( new THREE.Mesh(
sphereGeom,
new THREE.MeshBasicMaterial( {
color: color,
transparent: true,
opacity: 0.033
} )
) );
l.children[ 1 ].scale.set( 6.66, 6.66, 6.66 );
l._light = {
color: color,
radius: RADIUS,
decay: 1,
sy: Math.random(),
sr: Math.random(),
sc: Math.random(),
py: Math.random() * Math.PI,
pr: Math.random() * Math.PI,
pc: Math.random() * Math.PI,
dir: Math.random() > 0.5 ? 1 : - 1
};
lights.push( l );
g.add( l );
}
以上,对四个材质参数中每个,都循环 8 次,创建 8个 点光源 ’壳子‘,用于在渲染时,直观地 实时的 展示点光源的位置,颜色和运动方式。
每个 ’壳子‘ 由两个球形网格组成,第一个球形网格比较小,表示球形灯的灯芯;第二个球形网格等比放大 6.66 倍表示球形灯发光后形成的光晕,每个球形灯的发光颜色随机生成
_light 对象保存真正创建点光源要用的参数(color radius decay) 以及 决定光源在渲染过程中如何运动的参数
init 函数只是间接的创建点光源,真正创建点光源是在shader 代码中,这些光源不是通常意义上的光源,没有添加到场景树中。这些光源只作用于特定的材质 ShaderMaterial
点光源在片元着色器中创建,只影响光照计算,只作用于特定材质。
函数 ‘function update( now )’ 对 32个片元着色器光源进行位置更新
const State = {
rows: 0,
cols: 0,
width: 0,
height: 0,
tileData: { value: null },
tileTexture: { value: null },
lightTexture: {
value: new THREE.DataTexture( new Float32Array( 32 * 2 * 4 ), 32, 2, THREE.RGBAFormat, THREE.FloatType )
},
};
function resizeTiles() {
const width = window.innerWidth;
const height = window.innerHeight;
State.width = width;
State.height = height;
State.cols = Math.ceil( width / 32 );
State.rows = Math.ceil( height / 32 );
State.tileData.value = [ width, height, 0.5 / Math.ceil( width / 32 ), 0.5 / Math.ceil( height / 32 ) ];
State.tileTexture.value = new THREE.DataTexture( new Uint8Array( State.cols * State.rows * 4 ), State.cols, State.rows );
}
以上,每个tile格子都是 32 x 32 像素的正方形格子;
lightTexture 用于告知每个片元着色器,32个光源中 每个光源的位置 颜色 光源影响范围 radius,衰减系数 decay
tileTexture 用于告知每个片元着色器,所有 tile 格子中的 每个格子 会被 32个光源中 哪些光源影响光照计算
‘function tileLights( renderer, scene, camera )’ 负责更新这两个纹理
tileLights 调用了 lightBounds 方法计算 着色器光源 投影到屏幕后的二维包围盒坐标;包围盒大小只与前面的 radius 参数成正比
格子的划分是基于屏幕坐标系的,lightBounds 计算出的包围盒也是屏幕坐标系的,tileLights 对 lights 数组做迭代时,前半段更新 lightTexture ,后半段更新 tileTexture,计算出每个格子tile 分别会被哪些 着色器光源影响。
tileLights 方法的调用时机
scene.onBeforeRender = tileLights;
最后,提一下在 THREE.ShaderChunk[ ‘lights_fragment_end’ ] 尾部添加的片元着色器代码:
THREE.ShaderChunk[ 'lights_fragment_end' ] += [
'',
'#if defined TILED_FORWARD',
'vec2 tUv = floor(gl_FragCoord.xy / tileData.xy * 32.) / 32. + tileData.zw;',
'vec4 tile = texture2D(tileTexture, tUv);',
'for (int i=0; i < 4; i++) {',
' float tileVal = tile.x * 255.;',
' tile.xyzw = tile.yzwx;',
' if(tileVal == 0.){ continue; }',
' float tileDiv = 128.;',
' for (int j=0; j < 8; j++) {',
' if (tileVal < tileDiv) { tileDiv *= 0.5; continue; }',
' tileVal -= tileDiv;',
' tileDiv *= 0.5;',
' PointLight pointlight;',
' float uvx = (float(8 * i + j) + 0.5) / 32.;',
' vec4 lightData = texture2D(lightTexture, vec2(uvx, 0.));',
' vec4 lightColor = texture2D(lightTexture, vec2(uvx, 1.));',
' pointlight.position = lightData.xyz;',
' pointlight.distance = lightData.w;',
' pointlight.color = lightColor.rgb;',
' pointlight.decay = lightColor.a;',
' getPointLightInfo( pointlight, geometry, directLight );',
' RE_Direct( directLight, geometry, material, reflectedLight );',
' }',
'}',
'#endif'
].join( '\n' );
以上,有点费解,tileTexture中每个像素 有 x y z w 四个分量
tile.xyzw = tile.yzwx; 在一个长度为 4 的循环,每个分量都被读取出来,乘以 255.0 保存到浮点数变量 tileVal 中, tileVal 为 0时,表示当前片元所属的格子,没有被任何着色器光源影响到。
长度为 8 的子循环,是对 8比特分量的每一个 位(比特) 进行读取,如果一个位 值为1,就表示 该格子 有 被编号为 i = n,j = m 的着色器光源影响到 (0 <= n <= 3, 0 <= m <= 7),然后要创建点光源,带入到各种光照模型中进行光照计算(lambert blinn-phong,standard, physics-pbr等光照模型)。