THREE.js添加多個(gè)castShadow光源報(bào)錯(cuò)解決及原因分析
THREE.js在場(chǎng)景中添加castShadow的光源
最近使用THREE.js在場(chǎng)景中添加了30個(gè)左右castShadow的光源,然后在控制臺(tái)報(bào)錯(cuò):
THREE.WebGLProgram: shader error: 0 35715 false gl.getProgramInfoLog Varyings over maximum register limit
本文記錄下這個(gè)報(bào)錯(cuò)的原因。
varying的含義
首先看下Varyings over maximum register limit是什么意思?
Varyings變量注冊(cè)數(shù)超過(guò)限制,那么什么是Varying呢?
著色器語(yǔ)言提供了三種變量類(lèi)型:
- attribute:從外部傳輸給頂點(diǎn)著色器的變量,一般用于傳輸頂點(diǎn)數(shù)據(jù);
- uniform:從外部傳輸給頂點(diǎn)著色器或者片元著色器的變量,類(lèi)似于常量,只能用不能修改。一般用于傳輸變換矩陣、材質(zhì)、光照和顏色等信息;
- varying:從頂點(diǎn)著色器給片元著色器傳輸信息的變量,傳輸?shù)臅r(shí)候會(huì)對(duì)該變量進(jìn)行線性插值,所以varying(變化的)這個(gè)單詞很能表達(dá)這個(gè)變化的意思。
varying變量的個(gè)數(shù)限制
上述的varying就是指著色器語(yǔ)言中的varying變量,也就是varying變量的數(shù)量超出最大限制了。那么我們最多可以定義多少個(gè)varying變量呢?
通過(guò)查找資料發(fā)現(xiàn),這個(gè)varying變量的數(shù)量和具體的實(shí)現(xiàn)相關(guān),點(diǎn)擊這個(gè)網(wǎng)站在里面搜索Max Varying Vectors。我的電腦顯示的是15個(gè)。
THREE.js為什么會(huì)報(bào)錯(cuò)
報(bào)錯(cuò)的THREE.js版本是110,報(bào)錯(cuò)原因分析的時(shí)候用的是119版本(手頭上只有119版本的源碼)。
首先,在源碼中搜索gl.getProgramInfoLog看下大概是代碼哪個(gè)位置報(bào)的錯(cuò)。發(fā)現(xiàn)報(bào)錯(cuò)的代碼在WebGLProgram.js文件的WebGLProgram函數(shù),這個(gè)函數(shù)的功能大概就是創(chuàng)建一個(gè)program并編譯:
function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {
const gl = renderer.getContext();
// ...
const program = gl.createProgram(); // 創(chuàng)建一個(gè)program
// ...
// 動(dòng)態(tài)生成頂點(diǎn)著色器和片元著色器的源代碼
const glVertexShader = WebGLShader( gl, gl.VERTEX_SHADER, vertexGlsl ); // 創(chuàng)建并編譯頂點(diǎn)著色器
const glFragmentShader = WebGLShader( gl, gl.FRAGMENT_SHADER, fragmentGlsl ); // 創(chuàng)建并編譯片元著色器
gl.attachShader( program, glVertexShader ); // 和program綁定
gl.attachShader( program, glFragmentShader ); // 和program綁定
// ...
gl.linkProgram( program );
// ... 檢查上述過(guò)程是否報(bào)錯(cuò)
const programLog = gl.getProgramInfoLog( program ).trim(); // 獲取報(bào)錯(cuò)信息
if (...) { // 出錯(cuò)判斷
console.error(... 'gl.getProgramInfoLog' ...) // 報(bào)錯(cuò)位置
}
}從代碼中可以看出,這個(gè)函數(shù)首先創(chuàng)建了一個(gè)program,然后給這個(gè)program添加頂點(diǎn)和片元著色器,然后編譯。那么編譯編的是什么呢?
我覺(jué)得編譯編的應(yīng)該是文本,把文本編譯成可以執(zhí)行的代碼片段。到底對(duì)不對(duì)呢?
我們知道,頂點(diǎn)著色器和片元著色器是使用著色器語(yǔ)言編寫(xiě)的,這是一種類(lèi)C的語(yǔ)言,并不是我們熟悉的javascript。上面創(chuàng)建著色器的WebGLShader是THREE.js封裝的一個(gè)函數(shù),它的第三個(gè)參數(shù)就是源碼的字符串形式。然后調(diào)用compileShader進(jìn)行編譯:
function WebGLShader( gl, type, string ) {
const shader = gl.createShader( type );
gl.shaderSource( shader, string );
gl.compileShader( shader );
return shader;
}所以,上述錯(cuò)誤有可能就是動(dòng)態(tài)生成的著色器源碼有問(wèn)題。因?yàn)関arying變量用于從頂點(diǎn)著色器往片元著色器中傳輸數(shù)據(jù),所以同一個(gè)varying變量在兩個(gè)著色器里面都要聲明,所以我們只需要分析一個(gè)就行。我分析的是頂點(diǎn)著色器,也就是上面的vertexGlsl變量:
let vertexShader = parameters.vertexShader;
// ...
if ( parameters.isRawShaderMaterial ) { // 自定義著色器,不考慮
} else { // THREE.js提供的著色器
prefixVertex = [...]
}
const vertexGlsl = prefixVertex + vertexShader;我們找到了與vertexGlsl相關(guān)的兩個(gè)變量prefixVertex和vertexShader。
prefixVertex文本分析
報(bào)錯(cuò)是在開(kāi)啟castShadow之后才有的,所以看下里面有沒(méi)有和castShadow相關(guān)的代碼。首先prefixVertex里面有一個(gè)shadowMapEnabled,感覺(jué)有點(diǎn)關(guān)系:
parameters.shadowMapEnabled ? '#define USE_SHADOWMAP' : '', parameters.shadowMapEnabled ? '#define ' + shadowMapTypeDefine : '',
接下來(lái)看下parameters來(lái)驗(yàn)證下,可以看到這個(gè)變量是WebGLProgram的參數(shù),那么就得接著往下找哪調(diào)用了WebGLProgram。最后發(fā)現(xiàn)是WebGLPrograms.js文件里面的acquireProgram函數(shù)調(diào)用了:
function acquireProgram( parameters, cacheKey ) {
// ...
program = new WebGLProgram( renderer, cacheKey, parameters, bindingStates );
// ...
}parameters是acquireProgram的參數(shù),所以接著看下這個(gè)函數(shù)是在哪調(diào)用的。WebGLRenderer.js里面的initMaterial函數(shù)調(diào)用了它:
function initMaterial( material, scene, object ) {
// ...
const shadowsArray = currentRenderState.state.shadowsArray;
// ...
const parameters = programCache.getParameters( material, lights.state, shadowsArray, ... );
// ...
program = programCache.acquireProgram( parameters, programCacheKey );
// ...
}搜索下programCache = 發(fā)現(xiàn):
programCache = new WebGLPrograms( _this, extensions, capabilities, bindingStates );
所以再次回到WebGLPrograms.js文件看下getParameters函數(shù):
function getParameters( material, lights, shadows, scene, nClipPlanes, nClipIntersection, object ) {
// ...
shadowMapEnabled: renderer.shadowMap.enabled && shadows.length > 0
// ...
}再次回到initMaterial在調(diào)用getParameters時(shí)傳入的shadows變量是啥?發(fā)現(xiàn)是shadowsArray:
const shadowsArray = currentRenderState.state.shadowsArray;
currentRenderState = renderStates.get( scene, _currentArrayCamera || camera ); // 找了一處賦值
renderStates = new WebGLRenderStates()
WebGLStates內(nèi)部使用了一個(gè)WeakMap,它的key是scene,它的值又是一個(gè)WeakMap,這個(gè)map的key是camera,value是WebGLRenderState。注意前面是WebGLRenderStates,有一個(gè)s:
renderState = new WebGLRenderState(); renderStates.set( scene, new WeakMap() ); renderStates.get( scene ).set( camera, renderState );
接下來(lái)看下WebGLRenderState,里面有一個(gè)pushShadow方法,和前面的const shadowsArray = currentRenderState.state.shadowsArray;感覺(jué)能對(duì)應(yīng)上:
function pushShadow( shadowLight ) {
shadowsArray.push( shadowLight );
}接下來(lái),就是看下pushShadow是在哪調(diào)用的,在WebGLRenderer.js文件的compile方法中有調(diào)用:
this.compile = function ( scene, camera ) {
// 前面講過(guò)renderStates是一個(gè)雙層的WeakMap,先根據(jù)scene獲取一次,再根據(jù)camera獲取一次
currentRenderState = renderStates.get( scene, camera );
currentRenderState.init();
// 收集光源信息
scene.traverse( function ( object ) {
if ( object.isLight ) { // 是光源
currentRenderState.pushLight( object );
if ( object.castShadow ) { // 光源設(shè)置了投影
currentRenderState.pushShadow( object );
}
}
} );
currentRenderState.setupLights( camera );
const compiled = new WeakMap();
scene.traverse( function ( object ) {
// ... initMaterial
} );
};compile方法首先根據(jù)scene和camera獲取到相關(guān)renderState,然后遍歷場(chǎng)景對(duì)象,把castShadow的光源放到shadowsArray里面。后面開(kāi)始初始化材質(zhì),初始化材質(zhì)的時(shí)候會(huì)編譯前面說(shuō)到的頂點(diǎn)著色器和片元著色器。
我們回到代碼片段shadowMapEnabled: renderer.shadowMap.enabled && shadows.length > 0,當(dāng)我們?cè)趫?chǎng)景中添加了castShadow的光源的時(shí)候,這個(gè)shadows數(shù)組的長(zhǎng)度就是大于0的,所以shadowMapEnabled就是true。
那么,prefixVertex文本里面就會(huì)包含#define USE_SHADOWMAP:
parameters.shadowMapEnabled ? '#define USE_SHADOWMAP' : '',
分析完prefixVertex會(huì)發(fā)現(xiàn)相關(guān)的就是在頂點(diǎn)著色器代碼中添加了#define USE_SHADOWMAP,并沒(méi)有看到varying聲明??磥?lái)varying聲明應(yīng)該會(huì)在vertexShader文本中。
vertexShader文本分析
在WebGLProgram函數(shù)中,vertexShader是parameters的一個(gè)屬性:
function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {
let vertexShader = parameters.vertexShader;
}同樣,我們找到WebGLPrograms.js文件看下getParameters函數(shù)里面vertexShader是如何得出來(lái)的:
function getParameters( material, lights, shadows, scene, nClipPlanes, nClipIntersection, object ) {
// ...
const shaderID = shaderIDs[ material.type ];
// ...
let vertexShader, fragmentShader;
if ( shaderID ) { // 內(nèi)置頂點(diǎn)著色器/片元著色器代碼
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'
};我們假設(shè)材質(zhì)是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相關(guān)的代碼:
#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
`;注意開(kāi)頭的#ifdef USE_SHADOWMAP,還記得prefixVertex最后生成了個(gè)啥嗎?不正是USE_SHADOWMAP嘛:
#define USE_SHADOWMAP
如果沒(méi)有這個(gè)define,那么ifdef這個(gè)判斷就會(huì)失敗,就不會(huì)走到中間這部分代碼了。
假設(shè)我們使用的是spotLight,我們看到會(huì)聲明一個(gè)長(zhǎng)度是NUM_SPOT_LIGHT_SHADOWS的vec4數(shù)組。這個(gè)數(shù)組的長(zhǎng)度是多少,就會(huì)占用多少個(gè)varying變量的名額:
varying vec4 vSpotShadowCoord[ NUM_SPOT_LIGHT_SHADOWS ];
猜測(cè)一下,NUM_SPOT_LIGHT_SHADOWS變量應(yīng)該就是啟用了castShadow的光源的數(shù)量。驗(yàn)證一下?
源代碼中搜索NUM_SPOT_LIGHT_SHADOWS,會(huì)在WebGLProgram.js文件中發(fā)現(xiàn)函數(shù):
function replaceLightNums( string, parameters ) {
return string
// ...
.replace( /NUM_SPOT_LIGHT_SHADOWS/g, parameters.numSpotLightShadows )
// ...
}在WebGLProgram后面會(huì)對(duì)vertexShader做進(jìn)一步處理。其中就包括replaceLightNums:
vertexShader = replaceLightNums( vertexShader, parameters );
最后,只需要看下parameters.numSpotLightShadows這個(gè)變量:
function getParameters( material, lights, shadows, scene, nClipPlanes, nClipIntersection, object ) {
// ...
numSpotLightShadows: lights.spotShadowMap.length,
// ...
}lights正好是前面我們分析過(guò)的currentRenderState.state.shadowsArray,這個(gè)變量的spotShadowMap變量是在WebGLLights的setup函數(shù)里面設(shè)置的:
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個(gè)啟用castShadow的光源會(huì)占用n個(gè)varying變量。
頂點(diǎn)著色器除了castShadow的光源占用的varying之外,還會(huì)有其他的varying變量,所有varying變量加起來(lái)不能超過(guò)15個(gè),在我的例子中,castShadow的光源最多大概12、13個(gè)左右。
解決方案?
那么如何在場(chǎng)景中添加超過(guò)varying限制數(shù)量的啟用castShadow的光源呢?
搜索了下,看到有說(shuō)可以使用WebGLDeferredRenderer的,但是這個(gè)在前幾年就因?yàn)闆](méi)有資源維護(hù)給移除了。
以上就是THREE.js添加多個(gè)castShadow光源報(bào)錯(cuò)解決及原因分析的詳細(xì)內(nèi)容,更多關(guān)于THREE.js添加多個(gè)光源報(bào)錯(cuò)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
js默認(rèn)文本框粘貼事件完美實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了js默認(rèn)文本框粘貼事件完美實(shí)現(xiàn)詳解,2023-01-01
JS前端監(jiān)控采集用戶(hù)行為的N種姿勢(shì)
這篇文章主要為大家介紹了JS前端監(jiān)控采集用戶(hù)行為的N種姿勢(shì),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
微信小程序?qū)崿F(xiàn)多個(gè)按鈕toggle功能的實(shí)例
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)多個(gè)按鈕toggle功能的實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-06-06
關(guān)于js typeof 與 instanceof 判斷數(shù)據(jù)類(lèi)型區(qū)別及開(kāi)發(fā)使用
我們經(jīng)常會(huì)遇到判斷一個(gè)變量的數(shù)據(jù)類(lèi)型或該變量是否為空值的情況,你是如何去選擇判斷類(lèi)型操作符的?本文來(lái)記錄一下我們開(kāi)發(fā)人員必須掌握關(guān)于 typeof 和 instanceof 的知識(shí)點(diǎn)及在開(kāi)發(fā)中的使用建議,同時(shí)在面試過(guò)程中也經(jīng)常會(huì)遇到這樣的問(wèn)題,需要的朋友可參考下文章內(nèi)容2021-10-10
微信小程序 火車(chē)票查詢(xún)實(shí)例講解
這篇文章主要介紹了微信小程序 火車(chē)票查詢(xún)實(shí)例講解的相關(guān)資料,這里提供了簡(jiǎn)單實(shí)例來(lái)說(shuō)明微信小程序的框架及如何開(kāi)發(fā),需要的朋友可以參考下2016-10-10
微信小程序 this和that詳解及簡(jiǎn)單實(shí)例
這篇文章主要介紹了微信小程序 this和that詳解及簡(jiǎn)單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-02-02

