关于javascript:THREEjs添加多个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呢?
着色器语言提供了三种变量类型:

  1. attribute:从内部传输给顶点着色器的变量,个别用于传输顶点数据;
  2. uniform:从内部传输给顶点着色器或者片元着色器的变量,相似于常量,只能用不能批改。个别用于传输变换矩阵、材质、光照和色彩等信息;
  3. varying:从顶点着色器给片元着色器传输信息的变量,传输的时候会对该变量进行线性插值,所以varying(变动的)这个单词很能表白这个变动的意思。

varying变量的个数限度

上述的varying就是指着色器语言中的varying变量,也就是varying变量的数量超出最大限度了。那么咱们最多能够定义多少个varying变量呢?

通过查找材料发现,这个varying变量的数量和具体的实现相干,点击这个网站在外面搜寻Max Varying Vectors。我的电脑显示的是15个。

THREE.js为什么会报错

报错的THREE.js版本是110,报错起因剖析的时候用的是119版本(手头上只有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,而后给这个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相干的两个变量prefixVertexvertexShader

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,它的值又是一个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变量是在WebGLLightssetup函数外面设置的:

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和WebGL理解的不深,如有谬误,欢送在评论区留言探讨。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理