THREE.js添加多个castShadow光源报错解决及原因分析

作者:白露时间:2026-10-30 20:07:06浏览:399

本文分析THREE.js中添加多个castShadow光源时报错Varyings over maximum register limit的原因。通过追踪WebGLProgram源码,发现每个启用阴影的光源会占用varying变量名额,导致着色器寄存器超限。文章详细解释了prefixVertex和vertexShader中阴影相关代码的生成机制,并指出varying变量总数限制通常为15个,建议控制阴影光源数量。

在THREE.js场景中引入带有castShadow属性的光源。

近期在THREE.js项目中配置了约30个具备castShadow功能的光源,随后控制台输出错误信息。

THREE.WebGLProgram: shader error:  0 35715 false gl.getProgramInfoLog Varyings over maximum register limit

本文旨在记录并解析该报错产生的具体原因。

理解varying变量的含义

首先解析Varyings over maximum register limit的具体含义。当Varyings变量注册数超过系统限制时,会触发此报错。那么,什么是Varying?着色器语言中定义了三种变量类型:

  • attribute:用于从外部向顶点着色器传输数据,通常用于传递顶点信息;
  • uniform:用于从外部向顶点或片元着色器传输变量,类似于常量,只读不可修改。常用于传递变换矩阵、材质属性、光照参数及颜色等;
  • varying:用于从顶点着色器向片元着色器传输数据,在传输过程中会对该变量进行线性插值,因此“varying”(变化的)一词准确表达了其特性。

varying变量的数量限制

上述varying指代的是着色器语言中的varying变量,即当varying变量的数量超出最大允许值时会报错。那么,varying变量的最大定义数量是多少?

查阅相关资料可知,varying变量的限制数量与具体实现相关。通过访问相关网站搜索Max Varying Vectors,在我的设备中查询结果显示为15个。

THREE.js报错原因分析

触发报错的THREE.js版本为110,而在分析报错原因时使用的是119版本(手头仅有该版本的源码)。

首先在源码中搜索gl.getProgramInfoLog以定位报错位置。发现报错代码位于WebGLProgram.js文件的WebGLProgram函数中,该函数的主要功能是创建program并进行编译:

function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {
    const gl = renderer.getContext();
    // ...
    const program = gl.createProgram(); // 创建一个program
    // ...
    // 动态生成顶点着色器和片元着色器的源代码
    const glVertexShader = WebGLShader( gl, gl.VERTEX_SHADER, vertexGlsl ); // 创建并编译顶点着色器
    const glFragmentShader = WebGLShader( gl, gl.FRAGMENT_SHADER, fragmentGlsl ); // 创建并编译片元着色器
    gl.attachShader( program, glVertexShader ); // 和program绑定
    gl.attachShader( program, glFragmentShader ); // 和program绑定
    // ...
    gl.linkProgram( program );
    // ... 检查上述过程是否报错
    const programLog = gl.getProgramInfoLog( program ).trim(); // 获取报错信息
    if (...) { // 出错判断
        console.error(... 'gl.getProgramInfoLog' ...) // 报错位置
    }
}

从代码可以看出,该函数首先创建一个program,随后为其添加顶点着色器和片元着色器,最后执行编译。那么,编译的对象究竟是什么?

推测编译的对象应为文本,即将文本编译为可执行的代码片段。这种推测是否正确?

顶点着色器和片元着色器由着色器语言编写,这是一种类C语言,不同于我们熟悉的JavaScript。上述创建着色器的WebGLShader是THREE.js封装的函数,其第三个参数为源码的字符串形式。随后调用compileShader进行编译:

function WebGLShader( gl, type, string ) {
    const shader = gl.createShader( type );
    gl.shaderSource( shader, string );
    gl.compileShader( shader );
    return shader;
}

因此,上述错误可能是由动态生成的着色器源码问题引起。由于varying变量用于从顶点着色器向片元着色器传输数据,因此同一个varying变量必须在两个着色器中同时声明,我们只需分析其中一个即可。这里分析的是顶点着色器,即上述vertexGlsl变量:

let vertexShader = parameters.vertexShader;
// ...
if ( parameters.isRawShaderMaterial ) { // 自定义着色器,不考虑
} else { // THREE.js提供的着色器
    prefixVertex = [...]
}
const vertexGlsl = prefixVertex + vertexShader;

我们找到了与vertexGlsl相关的两个变量prefixVertex和vertexShader。

分析prefixVertex文本

报错仅在开启castShadow后出现,因此检查其中是否包含与castShadow相关的代码。首先查看prefixVertex内部是否存在shadowMapEnabled,这似乎存在关联:

parameters.shadowMapEnabled ? '#define USE_SHADOWMAP' : '',
parameters.shadowMapEnabled ? '#define ' + shadowMapTypeDefine : '',

接下来查看parameters以进行验证。该变量是WebGLProgram的参数,因此需要继续查找调用WebGLProgram的位置。最终发现是WebGLPrograms.js文件中的acquireProgram函数进行了调用:

function acquireProgram( parameters, cacheKey ) {
    // ...
    program = new WebGLProgram( renderer, cacheKey, parameters, bindingStates );
    // ...
}

parameters是acquireProgram的参数,因此继续查找该函数的调用位置。WebGLRenderer.js中的initMaterial函数调用了它:

function initMaterial( material, scene, object ) {
    // ...
    const shadowsArray = currentRenderState.state.shadowsArray;
    // ...
    const parameters = programCache.getParameters( material, lights.state, shadowsArray, ... );
    // ...
    program = programCache.acquireProgram( parameters, programCacheKey );
    // ...
}

搜索programCache = 发现:

programCache = new WebGLPrograms( _this, extensions, capabilities, bindingStates );

因此再次回到WebGLPrograms.js文件查看getParameters函数:

function getParameters( material, lights, shadows, scene, nClipPlanes, nClipIntersection, object ) {
    // ...
    shadowMapEnabled: renderer.shadowMap.enabled && shadows.length > 0
    // ...
}

再次回到initMaterial在调用getParameters时传入的shadows变量是什么?发现其为shadowsArray:

const shadowsArray = currentRenderState.state.shadowsArray;
currentRenderState = renderStates.get( scene, _currentArrayCamera || camera ); // 找了一处赋值
renderStates = new WebGLRenderStates()

WebGLStates内部使用了一个WeakMap,其key为scene,value为另一个WeakMap,该map的key为camera,value为WebGLRenderState。注意前缀是WebGLRenderStates,带有一个s:

renderState = new WebGLRenderState();
renderStates.set( scene, new WeakMap() );
renderStates.get( scene ).set( camera, renderState );

接下来查看WebGLRenderState,其中包含一个pushShadow方法,与前面的const shadowsArray = currentRenderState.state.shadowsArray;似乎能够对应:

function pushShadow( shadowLight ) {
    shadowsArray.push( shadowLight );
}

接下来,查看pushShadow的调用位置。在WebGLRenderer.js文件的compile方法中进行了调用:

this.compile = function ( scene, camera ) {
    // 前面讲过renderStates是一个双层的WeakMap,先根据scene获取一次,再根据camera获取一次
    currentRenderState = renderStates.get( scene, camera );
    currentRenderState.init();
    // 收集光源信息
    scene.traverse( function ( object ) {
        if ( object.isLight ) { // 是光源
            currentRenderState.pushLight( object );
            if ( object.castShadow ) { // 光源设置了投影
                currentRenderState.pushShadow( object );
            }
        }
    } );
    currentRenderState.setupLights( camera );
    const compiled = new WeakMap();
    scene.traverse( function ( object ) {
        // ... initMaterial
    } );
};

compile方法首先根据scene和camera获取相关的renderState,然后遍历场景对象,将带有castShadow的光源放入shadowsArray中。随后开始初始化材质,在初始化材质时会编译前述的顶点着色器和片元着色器。

回到代码片段shadowMapEnabled: renderer.shadowMap.enabled && shadows.length > 0,当在场景中添加了带有castShadow的光源时,该shadows数组的长度将大于0,因此shadowMapEnabled为true。

那么,prefixVertex文本中将包含#define USE_SHADOWMAP:

parameters.shadowMapEnabled ? '#define USE_SHADOWMAP' : '',

分析prefixVertex后发现,相关操作是在顶点着色器代码中添加了#define USE_SHADOWMAP,并未看到varying声明。看来varying声明应该出现在vertexShader文本中。

分析vertexShader文本

在WebGLProgram函数中,vertexShader是parameters的一个属性:

function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {
    let vertexShader = parameters.vertexShader;
}

同样,我们查看WebGLPrograms.js文件,了解getParameters函数中vertexShader是如何得出的:

function getParameters( material, lights, shadows, scene, nClipPlanes, nClipIntersection, object ) {
    // ...
    const shaderID = shaderIDs[ material.type ];
    // ...
    let vertexShader, fragmentShader;
    if ( shaderID ) { // 内置顶点着色器/片元着色器代码
        const shader = ShaderLib[ shaderID ];
        vertexShader = shader.vertexShader;
        fragmentShader = shader.fragmentShader;
    }
    // ...
}

查看shaderIDs:

const shaderIDs = {
    MeshDepthMaterial: 'depth',
    MeshDistanceMaterial: 'distanceRGBA',
    MeshNormalMaterial: 'normal',
    MeshBasicMaterial: 'basic',
    MeshLambertMaterial: 'lambert',
    MeshPhongMaterial: 'phong',
    MeshToonMaterial: 'toon',
    MeshStandardMaterial: 'physical',
    MeshPhysicalMaterial: 'physical',
    MeshMatcapMaterial: 'matcap',
    LineBasicMaterial: 'basic',
    LineDashedMaterial: 'dashed',
    PointsMaterial: 'points',
    ShadowMaterial: 'shadow',
    SpriteMaterial: 'sprite'
};

假设材质为MeshStandardMaterial,shaderID即为'physical',然后查看ShaderLib,它在ShaderLib.js文件中定义:

ShaderLib.physical = {
    // ...
    vertexShader: ShaderChunk.meshphysical_vert,
    // ...
}
import meshphysical_vert from './ShaderLib/meshphysical_vert.glsl.js';
export const ShaderChunk = {
    // ...
    meshphysical_vert: meshphysical_vert,
    // ...
}

终于找到线索,即meshphysical_vert.glsl.js文件,查找与shadowmap相关的代码:

#include <shadowmap_pars_vertex>

查看shadowmap_pars_vertex.glsl.js文件:

export default /* glsl */`
#ifdef USE_SHADOWMAP
    #if NUM_DIR_LIGHT_SHADOWS > 0
        uniform mat4 directionalShadowMatrix[ NUM_DIR_LIGHT_SHADOWS ];
        varying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];
        # ...
    #endif
    #if NUM_SPOT_LIGHT_SHADOWS > 0
        uniform mat4 spotShadowMatrix[ NUM_SPOT_LIGHT_SHADOWS ];
        varying vec4 vSpotShadowCoord[ NUM_SPOT_LIGHT_SHADOWS ];
        # ...
    #endif
    #if NUM_POINT_LIGHT_SHADOWS > 0
        uniform mat4 pointShadowMatrix[ NUM_POINT_LIGHT_SHADOWS ];
        varying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ];
        # ...
    #endif
#endif
`;

注意开头的#ifdef USE_SHADOWMAP,还记得prefixVertex最后生成了什么吗?正是USE_SHADOWMAP:

#define USE_SHADOWMAP

如果没有这个define,那么ifdef的判断将失败,也就不会执行中间部分的代码。

假设使用的是spotLight,将看到一个长度为NUM_SPOT_LIGHT_SHADOWS的vec4数组。该数组的长度是多少,就会占用多少个varying变量的名额:

varying vec4 vSpotShadowCoord[ NUM_SPOT_LIGHT_SHADOWS ];

推测NUM_SPOT_LIGHT_SHADOWS变量即为启用了castShadow的光源数量。进行验证?

在源代码中搜索NUM_SPOT_LIGHT_SHADOWS,可在WebGLProgram.js文件中找到函数:

function replaceLightNums( string, parameters ) {
    return string
        // ...
        .replace( /NUM_SPOT_LIGHT_SHADOWS/g, parameters.numSpotLightShadows )
        // ...
}

在WebGLProgram之后,会对vertexShader进行进一步处理。其中包括replaceLightNums:

vertexShader = replaceLightNums( vertexShader, parameters );

最后,只需查看parameters.numSpotLightShadows这个变量:

function getParameters( material, lights, shadows, scene, nClipPlanes, nClipIntersection, object ) {
    // ...
    numSpotLightShadows: lights.spotShadowMap.length,
    // ...
}

lights正是我们前面分析过的currentRenderState.state.shadowsArray,该变量的spotShadowMap变量是在WebGLLights的setup函数中设置的:

function setup( lights, shadows, camera ) {
    // ...
    let numSpotShadows = 0;
    // ...
    for ( let i = 0, l = lights.length; i < l; i ++ ) {
        // ...
        if ( light.castShadow ) {
            // ...
            numSpotShadows ++;
            // ...
        }
        // ...
    }
    // ...
    state.spotShadowMap.length = numSpotShadows;
    // ...
}

至此,分析基本完成:n个启用castShadow的光源将占用n个varying变量。

顶点着色器中,除了castShadow光源占用的varying外,还会存在其他varying变量。所有varying变量总和不得超过15个,在我的示例中,castShadow光源最多约为12或13个。

解决方案?

如何在场景中添加超过varying限制数量的启用castShadow的光源?

经过搜索,有观点指出可以使用WebGLDeferredRenderer,但该功能因几年前缺乏资源维护已被移除。

以上为THREE.js添加多个castShadow光源报错解决及原因分析的详细内容,更多关于THREE.js添加多个光源报错的资料请关注本站其它相关文章!

作者最新文章