最近应用 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 版本(手头上只有 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
相干的两个变量 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,它的值又是一个 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 和 WebGL 理解的不深,如有谬误,欢送在评论区留言探讨。