web前端技术分享
WebGLRenderer渲染流程

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源码实现有如下这些代码行: image.png image.png image.png 这些正是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在未来是否会得到这方面的优化。