1、threejs中往往通过执行WebGLRenderer.render(scene, camera)方法来执行渲染操作。
该方法在src/renderers/WebGLRenderer.js源码1031行通过this.render = () => {}的方式进行定义。该赋值位于WebGLRenderer的构造函数中。不知道官方出于何种考虑,该构造函数源码竟然长达两千多行,很多的函数属性的定义都是在构造函数中通过赋值的方式定义的。一般来讲定义类的方法更好的实现就是使用class语法所支持的在类中直接定义函数属性,而不是再构造函数中通过this赋值,这样可以提高代码的可读性。但threejs官方可能出于某些考虑,选择了后者。 从WebGLRenderer.render->renderScene->renderObjects->renderObject->_this.renderBufferDirect->WebGLBufferRenderer.render的调用链你可以最终找到调用webgl底层接口进行图形绘制的代码: 源码位于src/renderers/webgl/WebGLBufferRenderer.render。
1
2
3
4
5
6
7 function render( start, count ) {
gl.drawArrays( mode, start, count );
info.update( count, mode, 1 );
}
如你所见gl.drawArrays是webgl底层的绘制接口,负责从顶点缓冲区读取数据并调用着色器程序进行渲染。
2、如果你熟知webgl你应该知道,在调用gl.drawArrays你还需要为webgl准备好绘制所需要的顶点缓冲区告诉webgl应该绘制位于哪些位置的点或面。
它位于WebGLRenderer.render方法调用链的另一条调用分支: WebGLRenderer.render->projectObject->WebGLObjects.update->WebGLGeometries.update->WebGLAttributes.update->WebGLAttributes.createBuffer。该方法位于源码src\renderers\webgl\WebGLAttributes.js。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76 function createBuffer( attribute, bufferType ) {
const array = attribute.array;
const usage = attribute.usage;
const buffer = gl.createBuffer();
gl.bindBuffer( bufferType, buffer );
gl.bufferData( bufferType, array, usage );
attribute.onUploadCallback();
let type;
if ( array instanceof Float32Array ) {
type = gl.FLOAT;
} else if ( array instanceof Uint16Array ) {
if ( attribute.isFloat16BufferAttribute ) {
if ( isWebGL2 ) {
type = gl.HALF_FLOAT;
} else {
throw new Error( 'THREE.WebGLAttributes: Usage of Float16BufferAttribute requires WebGL2.' );
}
} else {
type = gl.UNSIGNED_SHORT;
}
} else if ( array instanceof Int16Array ) {
type = gl.SHORT;
} else if ( array instanceof Uint32Array ) {
type = gl.UNSIGNED_INT;
} else if ( array instanceof Int32Array ) {
type = gl.INT;
} else if ( array instanceof Int8Array ) {
type = gl.BYTE;
} else if ( array instanceof Uint8Array ) {
type = gl.UNSIGNED_BYTE;
} else if ( array instanceof Uint8ClampedArray ) {
type = gl.UNSIGNED_BYTE;
} else {
throw new Error( 'THREE.WebGLAttributes: Unsupported buffer data format: ' + array );
}
return {
buffer: buffer,
type: type,
bytesPerElement: array.BYTES_PER_ELEMENT,
version: attribute.version
};
}
如你所见上述代码第8第9行就是webgl底层所提供的将顶点数据绑定至webgl顶点缓冲区的底层接口。
3、顶点数据源来自哪?
以加载3D文件渲染为例,对应的Loader工具会将文件解析为如下结构:
整体层级如上,Scene可包含多个Object3D子节点,Object3D也可以继续包含Object3D直到组成场景的最小单元Mesh。threejs对3D场景建立了了这样一个分层模型。实际上真正要渲染的就是Mesh,这些Mesh组成了整个场景,只是在空间对象的概念上给他们做了分组分层树状的数据结构。真正要渲染的就是位于这颗场景树的所有子节点也即Mesh对象。这一点你只需要在控制台将Scene对象打印一下就可以知道。而Mesh又包含geometry和material,gemetry就是Mesh的几何信息也即顶点缓冲区,material为几何体的材质信息。
场景中Mesh的gemetry中的顶点数据就在(2)中提及的函数调用链的WebGLObjects.update处被取出然后一步步传递下去。源码位于src\renderers\webgl\WebGLObjects.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58 function update( object ) {
const frame = info.render.frame;
const geometry = object.geometry;
const buffergeometry = geometries.get( object, geometry );
// Update once per frame
if ( updateMap.get( buffergeometry ) !== frame ) {
geometries.update( buffergeometry );
updateMap.set( buffergeometry, frame );
}
if ( object.isInstancedMesh ) {
if ( object.hasEventListener( 'dispose', onInstancedMeshDispose ) === false ) {
object.addEventListener( 'dispose', onInstancedMeshDispose );
}
if ( updateMap.get( object ) !== frame ) {
attributes.update( object.instanceMatrix, gl.ARRAY_BUFFER );
if ( object.instanceColor !== null ) {
attributes.update( object.instanceColor, gl.ARRAY_BUFFER );
}
updateMap.set( object, frame );
}
}
if ( object.isSkinnedMesh ) {
const skeleton = object.skeleton;
if ( updateMap.get( skeleton ) !== frame ) {
skeleton.update();
updateMap.set( skeleton, frame );
}
}
return buffergeometry;
}
如你所见第5第6行,将Mesh对象的geometry中的顶点数据取出赋值给buffergeometry变量,并作为参数传递给(2)中函数调用链后面的函数进行数据缓冲区的绑定。而geometry的index属性则存放了要绘制的三角片元的索引,在(1)中调用链的this.renderBufferDirect方法中获取,你可以在源码src/renderers/WebGLRenderer.js的第782行左右找到相关代码。geometry属性中除了顶点缓冲区还存放了纹理的uv缓冲区,matarial则存储了纹理资源。
4、有了顶点数据,纹理uv数据,纹理资源,现在整个webgl渲染任务只差着色器程序了。
从WebGLRenderer.render->renderScene->renderObjects->renderObject->_this.renderBufferDirect->setProgram->getProgram->WebGLPrograms.acquireProgram->new WebGLProgram()。
你可以看到WebGLProgram源码实现有如下这些代码行:
这些正是webgl底层提供的设置着色器程序的接口,而着色器程序的内容则来自src/renderers/shaders目录下实现的众多着色器程序,如果你不熟悉请先去学习基本的webgl知识。
除了这条直接的调用链,threejs还加入了辅助的缓存机制进行渲染性能的优化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114 function getProgram( material, scene, object ) {
if ( scene.isScene !== true ) scene = _emptyScene; // scene could be a Mesh, Line, Points, ...
const materialProperties = properties.get( material );
const lights = currentRenderState.state.lights;
const shadowsArray = currentRenderState.state.shadowsArray;
const lightsStateVersion = lights.state.version;
const parameters = programCache.getParameters( material, lights.state, shadowsArray, scene, object );
const programCacheKey = programCache.getProgramCacheKey( parameters );
let programs = materialProperties.programs;
// always update environment and fog - changing these trigger an getProgram call, but it's possible that the program doesn't change
materialProperties.environment = material.isMeshStandardMaterial ? scene.environment : null;
materialProperties.fog = scene.fog;
materialProperties.envMap = ( material.isMeshStandardMaterial ? cubeuvmaps : cubemaps ).get( material.envMap || materialProperties.environment );
if ( programs === undefined ) {
// new material
material.addEventListener( 'dispose', onMaterialDispose );
programs = new Map();
materialProperties.programs = programs;
}
let program = programs.get( programCacheKey );
if ( program !== undefined ) {
// early out if program and light state is identical
if ( materialProperties.currentProgram === program && materialProperties.lightsStateVersion === lightsStateVersion ) {
updateCommonMaterialProperties( material, parameters );
return program;
}
} else {
parameters.uniforms = programCache.getUniforms( material );
material.onBuild( object, parameters, _this );
material.onBeforeCompile( parameters, _this );
program = programCache.acquireProgram( parameters, programCacheKey );
programs.set( programCacheKey, program );
materialProperties.uniforms = parameters.uniforms;
}
const uniforms = materialProperties.uniforms;
if ( ( ! material.isShaderMaterial && ! material.isRawShaderMaterial ) || material.clipping === true ) {
uniforms.clippingPlanes = clipping.uniform;
}
updateCommonMaterialProperties( material, parameters );
// store the light setup it was created for
materialProperties.needsLights = materialNeedsLights( material );
materialProperties.lightsStateVersion = lightsStateVersion;
if ( materialProperties.needsLights ) {
// wire up the material to this renderer's lighting state
uniforms.ambientLightColor.value = lights.state.ambient;
uniforms.lightProbe.value = lights.state.probe;
uniforms.directionalLights.value = lights.state.directional;
uniforms.directionalLightShadows.value = lights.state.directionalShadow;
uniforms.spotLights.value = lights.state.spot;
uniforms.spotLightShadows.value = lights.state.spotShadow;
uniforms.rectAreaLights.value = lights.state.rectArea;
uniforms.ltc_1.value = lights.state.rectAreaLTC1;
uniforms.ltc_2.value = lights.state.rectAreaLTC2;
uniforms.pointLights.value = lights.state.point;
uniforms.pointLightShadows.value = lights.state.pointShadow;
uniforms.hemisphereLights.value = lights.state.hemi;
uniforms.directionalShadowMap.value = lights.state.directionalShadowMap;
uniforms.directionalShadowMatrix.value = lights.state.directionalShadowMatrix;
uniforms.spotShadowMap.value = lights.state.spotShadowMap;
uniforms.spotLightMatrix.value = lights.state.spotLightMatrix;
uniforms.spotLightMap.value = lights.state.spotLightMap;
uniforms.pointShadowMap.value = lights.state.pointShadowMap;
uniforms.pointShadowMatrix.value = lights.state.pointShadowMatrix;
// TODO (abelnation): add area lights shadow info to uniforms
}
const progUniforms = program.getUniforms();
const uniformsList = WebGLUniforms.seqWithValue( progUniforms.seq, uniforms );
materialProperties.currentProgram = program;
materialProperties.uniformsList = uniformsList;
return program;
}
关注第34行代码,这是先去读取缓存,然后通过if语句判断缓存中不存在,再调用的WebGLPrograms.acquireProgram方法去创建着色器程序。
当然在src/renderers/shaders目录下实现了众多不同的着色器程序,这里有不同的着色器程序主要是为了针对不同的材质。毕竟顶点确定,3D物体的几何结构基本就确定了。是因为不同的材质所以才有了不同的着色器程序,这里如果你感兴趣可以去了解以下各种不同材质,threejs都是如何处理的,甚至可以将其与其它的一些3D渲染软件进行对比。目前threejs在处理某些材质时还会有一些渲染效果不太好的地方。如果你拿某些材质的模型将threejs的渲染效果跟某些3D电脑软件相比是有一定差距的,尤其是各向异性材质差别更是明显。不知道threejs在未来是否会得到这方面的优化。