Threejs源码系列- WebGLRenderer (4)setProgram

81 阅读11分钟

setProgram

setProgram 方法会根据当前渲染上下文(相机、场景、几何体、材质、物体)的状态,判断是否需要切换着色器程序,并完成着色器程序的绑定、uniforms 变量的更新等工作,是连接 JavaScript 3D 逻辑与 WebGL 底层渲染的关键环节。

function setProgram( camera, scene, geometry, material, object ) {
    // 如果传入的场景不是Scene类型(可能是Mesh、Line等对象),则使用空场景作为默认场景
    if ( scene.isScene !== true ) scene = _emptyScene; 
    // 重置纹理单元,确保纹理使用的GPU资源正确初始化
    textures.resetTextureUnits();

    // 获取场景中的雾效配置
    const fog = scene.fog; 
    // 如果材质是MeshStandardMaterial,获取场景的环境贴图;否则为null
    const environment = material.isMeshStandardMaterial ? scene.environment : null;
    // 确定颜色空间:如果当前没有渲染目标,使用渲染器的输出颜色空间;
    // 如果是XR渲染目标,使用其纹理的颜色空间;否则使用线性SRGB颜色空间
    const colorSpace = ( _currentRenderTarget === null ) ? _this.outputColorSpace : ( _currentRenderTarget.isXRRenderTarget === true ? _currentRenderTarget.texture.colorSpace : LinearSRGBColorSpace );
    // 获取环境贴图:MeshStandardMaterial使用cubeuvmaps缓存,其他材质使用cubemaps缓存;
    // 优先使用材质自身的envMap,没有则使用场景的environment
    const envMap = ( material.isMeshStandardMaterial ? cubeuvmaps : cubemaps ).get( material.envMap || environment );
    // 判断是否使用顶点alpha通道:需开启顶点颜色,且几何体有颜色属性,且颜色属性包含4个分量(rgba)
    const vertexAlphas = material.vertexColors === true && !! geometry.attributes.color && geometry.attributes.color.itemSize === 4;
    // 判断是否使用顶点切线:需几何体有切线属性,且材质使用法线贴图或各向异性过滤>0
    const vertexTangents = !! geometry.attributes.tangent && ( !! material.normalMap || material.anisotropy > 0 );
    // 判断是否包含顶点动画目标(位置)
    const morphTargets = !! geometry.morphAttributes.position;
    // 判断是否包含顶点动画法线
    const morphNormals = !! geometry.morphAttributes.normal;
    // 判断是否包含顶点动画颜色
    const morphColors = !! geometry.morphAttributes.color;
    
    // 初始化色调映射为无映射
    let toneMapping = NoToneMapping;
    
    // 如果材质需要色调映射
    if ( material.toneMapped ) {
        // 当没有渲染目标或为XR渲染目标时,使用渲染器的色调映射配置
        if ( _currentRenderTarget === null || _currentRenderTarget.isXRRenderTarget === true ) {
            toneMapping = _this.toneMapping;
        }

    }
    // 获取任意存在的动画属性(位置/法线/颜色)
    const morphAttribute = geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color;
    // 计算动画目标的数量(如果有动画属性则取其长度,否则为0)
    const morphTargetsCount = ( morphAttribute !== undefined ) ? morphAttribute.length : 0;
    // 获取材质的缓存属性(存储渲染相关的状态信息)
    const materialProperties = properties.get( material );
    // 获取当前渲染状态中的灯光信息
    const lights = currentRenderState.state.lights;
    
    // 如果启用了裁剪平面功能
    if ( _clippingEnabled === true ) {
        // 如果启用了局部裁剪或相机发生变化
        if ( _localClippingEnabled === true || camera !== _currentCamera ) {
            // 判断是否使用缓存的裁剪状态:相机未变且材质未变时可使用缓存
            const useCache =
                camera === _currentCamera &&
                material.id === _currentMaterialId;

            // 设置裁剪状态(未来可能支持ClippingGroup对象,见#8465, #8379)
            clipping.setState( material, camera, useCache );

        }

    }

    /**
     * 判断是否需要切换着色器程序
     */

    // 声明变量用于标记是否需要切换着色器程序,初始值为false(默认不需要)
    let needsProgramChange = false;

    // 检查材质的版本是否与缓存的版本一致(用于判断材质是否有更新)
    if ( material.version === materialProperties.__version ) {
        // 1. 如果材质需要光照,且当前光照状态版本与缓存的版本不一致,需切换程序
        if ( materialProperties.needsLights && ( materialProperties.lightsStateVersion !== lights.state.version ) ) {

            needsProgramChange = true;
        // 2. 如果缓存的输出颜色空间与当前需要的颜色空间不一致,需切换程序
        } else if ( materialProperties.outputColorSpace !== colorSpace ) {

            needsProgramChange = true;
        // 3. 如果当前对象是批处理网格,但缓存中标记为非批处理,需切换程序
        } else if ( object.isBatchedMesh && materialProperties.batching === false ) {

            needsProgramChange = true;
        // 4. 如果当前对象不是批处理网格,但缓存中标记为批处理,需切换程序
        } else if ( ! object.isBatchedMesh && materialProperties.batching === true ) {

            needsProgramChange = true;
        // 5. 如果当前对象是实例化网格,但缓存中标记为非实例化,需切换程序
        } else if ( object.isInstancedMesh && materialProperties.instancing === false ) {

            needsProgramChange = true;
        // 6. 如果当前对象不是实例化网格,但缓存中标记为实例化,需切换程序
        } else if ( ! object.isInstancedMesh && materialProperties.instancing === true ) {

            needsProgramChange = true;
        // 7. 如果当前对象是骨骼动画网格,但缓存中标记为非骨骼动画,需切换程序
        } else if ( object.isSkinnedMesh && materialProperties.skinning === false ) {

            needsProgramChange = true;
        // 8. 如果当前对象不是骨骼动画网格,但缓存中标记为骨骼动画,需切换程序
        } else if ( ! object.isSkinnedMesh && materialProperties.skinning === true ) {

            needsProgramChange = true;
        // 9. 如果当前对象是实例化网格,缓存中标记启用实例颜色,但实际没有实例颜色
        } else if ( object.isInstancedMesh && materialProperties.instancingColor === true && object.instanceColor === null ) {

            needsProgramChange = true;
        // 10. 如果当前对象是实例化网格,缓存中标记禁用实例颜色,但实际有实例颜色
        } else if ( object.isInstancedMesh && materialProperties.instancingColor === false && object.instanceColor !== null ) {

            needsProgramChange = true;
        // 11. 如果当前对象是实例化网格,缓存中标记启用实例变形,但实际没有变形纹理
        } else if ( object.isInstancedMesh && materialProperties.instancingMorph === true && object.morphTexture === null ) {

            needsProgramChange = true;
        // 12. 如果当前对象是实例化网格,缓存中标记禁用实例变形,但实际有变形纹理
        } else if ( object.isInstancedMesh && materialProperties.instancingMorph === false && object.morphTexture !== null ) {

            needsProgramChange = true;
        // 13. 如果缓存的环境贴图与当前需要的环境贴图不一致
        } else if ( materialProperties.envMap !== envMap ) {

            needsProgramChange = true;
        // 14. 如果材质启用了雾化效果,且缓存的雾化设置与当前场景的雾化设置不一致
        } else if ( material.fog === true && materialProperties.fog !== fog ) {

            needsProgramChange = true;
        // 15. 如果缓存的裁剪平面数量或裁剪交集数量与当前不一致
        } else if ( materialProperties.numClippingPlanes !== undefined &&
            ( materialProperties.numClippingPlanes !== clipping.numPlanes ||
            materialProperties.numIntersection !== clipping.numIntersection ) ) {

            needsProgramChange = true;
        // 16. 如果缓存的顶点透明度设置与当前几何体的顶点透明度状态不一致
        } else if ( materialProperties.vertexAlphas !== vertexAlphas ) {

            needsProgramChange = true;
        // 17. 如果缓存的顶点切线设置与当前几何体的顶点切线状态不一致
        } else if ( materialProperties.vertexTangents !== vertexTangents ) {

            needsProgramChange = true;
        // 18. 如果缓存的变形目标设置与当前几何体的变形目标状态不一致
        } else if ( materialProperties.morphTargets !== morphTargets ) {

            needsProgramChange = true;
        // 19. 如果缓存的变形法线设置与当前几何体的变形法线状态不一致
        } else if ( materialProperties.morphNormals !== morphNormals ) {

            needsProgramChange = true;
        // 20. 如果缓存的变形颜色设置与当前几何体的变形颜色状态不一致
        } else if ( materialProperties.morphColors !== morphColors ) {

            needsProgramChange = true;
        // 21. 如果缓存的色调映射设置与当前需要的色调映射不一致
        } else if ( materialProperties.toneMapping !== toneMapping ) {

            needsProgramChange = true;
        // 22. 如果缓存的变形目标数量与当前几何体的变形目标数量不一致
        } else if ( materialProperties.morphTargetsCount !== morphTargetsCount ) {

            needsProgramChange = true;

        }
    // 如果材质版本不一致(材质发生了更新)
    } else {

         // 需要更换着色器程序,并更新缓存的材质版本
        needsProgramChange = true;
        materialProperties.__version = material.version;

    }

    /**
     * 获取或创建着色器程序
     */

    let program = materialProperties.currentProgram;

    if ( needsProgramChange === true ) {

        program = getProgram( material, scene, object );

    }

    let refreshProgram = false;
    let refreshMaterial = false;
    let refreshLights = false;
    /**
     * 更新渲染状态与 uniforms 变量
     */
    const p_uniforms = program.getUniforms(),
        m_uniforms = materialProperties.uniforms;
  
    if ( state.useProgram( program.program ) ) {
        //  切换了新的着色器程序,需要刷新状态
        refreshProgram = true;
        refreshMaterial = true;
        refreshLights = true;

    }

    if ( material.id !== _currentMaterialId ) {
         // 更新相机相关的 uniforms(投影矩阵、视图矩阵、相机位置等)
        _currentMaterialId = material.id;

        refreshMaterial = true;

    }

    // 如果需要刷新程序(着色器程序)或者当前相机与之前的相机不同
    if ( refreshProgram || _currentCamera !== camera ) {

        // 处理通用的相机相关 uniforms(着色器 uniform 变量)

        // 将相机的投影矩阵传递给着色器的 'projectionMatrix' 变量
        p_uniforms.setValue( _gl, 'projectionMatrix', camera.projectionMatrix );
        // 将相机的视图矩阵(世界矩阵的逆矩阵)传递给着色器的 'viewMatrix' 变量
        p_uniforms.setValue( _gl, 'viewMatrix', camera.matrixWorldInverse );

        // 获取着色器中 'cameraPosition' 变量的引用
        const uCamPos = p_uniforms.map.cameraPosition;

        // 如果着色器中存在 'cameraPosition' 变量
        if ( uCamPos !== undefined ) {
            // 计算相机在世界空间中的位置,并传递给着色器的 'cameraPosition' 变量
            uCamPos.setValue( _gl, _vector3.setFromMatrixPosition( camera.matrixWorld ) );

        }

        // 如果浏览器支持对数深度缓冲
        if ( capabilities.logarithmicDepthBuffer ) {
            // 计算对数深度缓冲的系数,并传递给着色器的 'logDepthBufFC' 变量
            p_uniforms.setValue( _gl, 'logDepthBufFC', 2.0 / ( Math.log( camera.far + 1.0 ) / Math.LN2 ) );

        }

        // 考虑将 isOrthographic 移至 UniformLib 和 WebGLMaterials(参考 GitHub 讨论)
        // 对特定类型的材质,传递相机是否为正交相机的标志
        if ( material.isMeshPhongMaterial ||
            material.isMeshToonMaterial ||
            material.isMeshLambertMaterial ||
            material.isMeshBasicMaterial ||
            material.isMeshStandardMaterial ||
            material.isShaderMaterial ) {
            
            // 将相机是否为正交相机的布尔值传递给着色器的 'isOrthographic' 变量
            p_uniforms.setValue( _gl, 'isOrthographic', camera.isOrthographicCamera === true );

        }

        // 如果当前相机与之前记录的相机不同(相机发生了切换)
        if ( _currentCamera !== camera ) {

            // 更新当前相机的引用为新相机
            _currentCamera = camera;

            // 光照相关的 uniform 变量依赖于相机,因此需要强制更新
            // 立即更新(如果当前材质支持光照)或在下次激活支持光照的材质时更新

            refreshMaterial = true;		// 在材质变化时设为 true,标记需要刷新材质相关 uniform在材质变化时设为 true,标记需要刷新材质相关 uniform
            refreshLights = true;		// 保持为 true 直到光照相关 uniform 更新完成

        }

    }

    /**
     *  检查当前渲染的对象是否是骨骼动画网格(SkinnedMesh)
     */
    if ( object.isSkinnedMesh ) {

        // 向着色器传递可选的绑定矩阵(bindMatrix)及其逆矩阵(bindMatrixInverse)
        // 这些矩阵用于将骨骼空间中的顶点坐标转换到模型空间
        p_uniforms.setOptional( _gl, object, 'bindMatrix' );
        p_uniforms.setOptional( _gl, object, 'bindMatrixInverse' );

        // 获取骨骼动画网格关联的骨骼系统(Skeleton)
        const skeleton = object.skeleton;

        // 如果骨骼系统存在
        if ( skeleton ) {

            // 如果骨骼纹理(boneTexture)尚未创建,则计算并生成它
            // 骨骼纹理是一种高效传递大量骨骼变换矩阵的方式(尤其当骨骼数量较多时)
            if ( skeleton.boneTexture === null ) skeleton.computeBoneTexture();

            // 将骨骼纹理传递给着色器的'boneTexture'变量,供GPU读取骨骼变换数据
            // 第三个参数textures用于管理纹理单元分配
            p_uniforms.setValue( _gl, 'boneTexture', skeleton.boneTexture, textures );

        }

    }

    if ( object.isBatchedMesh ) {
        // 批处理网格:矩阵纹理、ID 纹理、颜色纹理(批量传递多个子网格数据)
        // 向着色器传递可选的批处理纹理(batchingTexture)变量
        // 若着色器中定义了该变量则传递,否则忽略
        p_uniforms.setOptional( _gl, object, 'batchingTexture' );
        // 将批处理网格中存储的矩阵纹理(_matricesTexture)传递给着色器的'batchingTexture'变量
        // 第三个参数textures用于管理WebGL纹理单元的分配
        p_uniforms.setValue( _gl, 'batchingTexture', object._matricesTexture, textures );

    }
    /**
     * 变形目标(Morph Targets)更新
     */
    // 获取几何体中存储的变形目标属性集合(包含位置、法线、颜色等可能的变形数据)
    const morphAttributes = geometry.morphAttributes;

    // 检查是否存在位置、法线或颜色的变形目标数据
    if ( morphAttributes.position !== undefined || morphAttributes.normal !== undefined || ( morphAttributes.color !== undefined ) ) {

        // 如果存在任何变形目标数据,调用morphtargets.update更新相关着色器变量
        // 该方法会将变形目标的权重、偏移量等数据传递给GPU,实现模型的变形动画效果
        morphtargets.update( object, geometry, program );

    }

    // 检查是否需要更新阴影接收状态:
    // 1. 当材质需要刷新(refreshMaterial为true)时
    // 2. 或缓存的阴影接收状态与当前对象的阴影接收状态不一致时
    if ( refreshMaterial || materialProperties.receiveShadow !== object.receiveShadow ) {

        // 更新缓存中的阴影接收状态为当前对象的状态
        materialProperties.receiveShadow = object.receiveShadow;
        // 将当前对象的阴影接收状态(布尔值)传递给着色器的'receiveShadow'变量
        // 用于控制该对象是否接收其他物体投射的阴影
        p_uniforms.setValue( _gl, 'receiveShadow', object.receiveShadow );

    }

    // 检查当前材质是否为MeshGouraudMaterial,且设置了环境贴图
    if ( material.isMeshGouraudMaterial && material.envMap !== null ) {

        // 将环境贴图(envMap)赋值给材质uniform变量的envMap属性
        // 环境贴图用于模拟物体反射周围环境的效果
        m_uniforms.envMap.value = envMap;

        // 设置flipEnvMap变量:当环境贴图是普通立方体贴图(非渲染目标纹理)时,值为-1,否则为1
        // 该值用于处理立方体贴图的纹理坐标翻转,确保反射方向正确
        m_uniforms.flipEnvMap.value = ( envMap.isCubeTexture && envMap.isRenderTargetTexture === false ) ? - 1 : 1;

    }

    // 检查当前材质是否为MeshStandardMaterial,且自身未设置envMap,但场景设置了全局环境
    if ( material.isMeshStandardMaterial && material.envMap === null && scene.environment !== null ) {
        // 当材质未单独设置环境贴图但场景有全局环境时,使用场景的环境强度
        // 控制材质受全局环境影响的程度
        m_uniforms.envMapIntensity.value = scene.environmentIntensity;

    }
    /**
     * 材质与光照相关 uniforms
     */
    // 当需要刷新材质(refreshMaterial为true)时执行以下逻辑
    if ( refreshMaterial ) {

        // 向着色器传递色调映射曝光度(toneMappingExposure),控制画面整体亮度
        p_uniforms.setValue( _gl, 'toneMappingExposure', _this.toneMappingExposure );

        // 如果当前材质需要光照信息(如Phong、Standard等材质)
        if ( materialProperties.needsLights ) {

            // 标记所有光照相关的Uniform变量需要更新(根据refreshLights状态)
            // 例如环境光、平行光、点光源等的参数
            markUniformsLightsNeedsUpdate( m_uniforms, refreshLights );

        }

        // 刷新多个材质共有的Uniform变量

        // 如果场景有雾(fog)且当前材质启用了雾效果
        if ( fog && material.fog === true ) {

            // 更新雾相关的Uniform变量(如雾的颜色、密度、近/远平面等)
            materials.refreshFogUniforms( m_uniforms, fog );

        }

        // 刷新当前材质特有的Uniform变量
        // 传递材质、像素比、高度、透射渲染目标等参数,处理如颜色、纹理、透明度等材质属性
        materials.refreshMaterialUniforms( m_uniforms, material, _pixelRatio, _height, currentRenderState.state.transmissionRenderTarget[ camera.id ] );

        // 将所有材质Uniform变量上传到GPU
        WebGLUniforms.upload( _gl, getUniformList( materialProperties ), m_uniforms, textures );

    }

    // 当材质是ShaderMaterial且标记了uniforms需要更新时
    if ( material.isShaderMaterial && material.uniformsNeedUpdate === true ) {

        // 强制上传Uniform变量到GPU,并重置更新标记
        WebGLUniforms.upload( _gl, getUniformList( materialProperties ), m_uniforms, textures );
        material.uniformsNeedUpdate = false;

    }

    // 当材质是SpriteMaterial(精灵材质)时
    if ( material.isSpriteMaterial ) {

        // 向着色器传递精灵的中心点坐标(用于精灵的旋转/缩放基准点计算)
        p_uniforms.setValue( _gl, 'center', object.center );

    }

    /**
     *  处理通用的模型矩阵(将模型的空间变换信息传递给着色器)
     */ 
    // 模型视图矩阵:将模型坐标转换到视图(相机)坐标
    p_uniforms.setValue( _gl, 'modelViewMatrix', object.modelViewMatrix );
    // 法向量矩阵:用于将模型的法向量从模型空间转换到视图空间(考虑缩放影响)
    p_uniforms.setValue( _gl, 'normalMatrix', object.normalMatrix );
    // 模型矩阵:将模型坐标转换到世界坐标
    p_uniforms.setValue( _gl, 'modelMatrix', object.matrixWorld );

    // 处理UBO(Uniform Buffer Object,Uniform缓冲对象)
    // 仅对自定义着色器材质(ShaderMaterial)和原始着色器材质(RawShaderMaterial)生效
    if ( material.isShaderMaterial || material.isRawShaderMaterial ) {

        // 获取材质中定义的Uniform缓冲组
        const groups = material.uniformsGroups;

        // 遍历所有Uniform缓冲组
        for ( let i = 0, l = groups.length; i < l; i ++ ) {

            const group = groups[ i ];

            // 更新缓冲组的数据(将最新的Uniform值写入缓冲)
            uniformsGroups.update( group, program );
            // 将缓冲组绑定到着色器程序的指定绑定点
            uniformsGroups.bind( group, program );

        }

    }

    return program;

}