最近使用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.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了解的不深,如有错误,欢迎在评论区留言讨论。