THREE.js添加多個castShadow光源報錯解決及原因分析
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變量注冊數(shù)超過限制,那么什么是Varying呢?
著色器語言提供了三種變量類型:
- attribute:從外部傳輸給頂點著色器的變量,一般用于傳輸頂點數(shù)據(jù);
- uniform:從外部傳輸給頂點著色器或者片元著色器的變量,類似于常量,只能用不能修改。一般用于傳輸變換矩陣、材質(zhì)、光照和顏色等信息;
- varying:從頂點著色器給片元著色器傳輸信息的變量,傳輸?shù)臅r候會對該變量進行線性插值,所以varying(變化的)這個單詞很能表達這個變化的意思。
varying變量的個數(shù)限制
上述的varying
就是指著色器語言中的varying
變量,也就是varying變量的數(shù)量超出最大限制了。那么我們最多可以定義多少個varying變量呢?
通過查找資料發(fā)現(xiàn),這個varying變量的數(shù)量和具體的實現(xiàn)相關(guān),點擊這個網(wǎng)站在里面搜索Max Varying Vectors
。我的電腦顯示的是15個。
THREE.js為什么會報錯
報錯的THREE.js版本是110,報錯原因分析的時候用的是119版本(手頭上只有119版本的源碼)。
首先,在源碼中搜索gl.getProgramInfoLog
看下大概是代碼哪個位置報的錯。發(fā)現(xiàn)報錯的代碼在WebGLProgram.js文件的WebGLProgram函數(shù),這個函數(shù)的功能大概就是創(chuàng)建一個program并編譯:
function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) { const gl = renderer.getContext(); // ... const program = gl.createProgram(); // 創(chuàng)建一個program // ... // 動態(tài)生成頂點著色器和片元著色器的源代碼 const glVertexShader = WebGLShader( gl, gl.VERTEX_SHADER, vertexGlsl ); // 創(chuàng)建并編譯頂點著色器 const glFragmentShader = WebGLShader( gl, gl.FRAGMENT_SHADER, fragmentGlsl ); // 創(chuàng)建并編譯片元著色器 gl.attachShader( program, glVertexShader ); // 和program綁定 gl.attachShader( program, glFragmentShader ); // 和program綁定 // ... gl.linkProgram( program ); // ... 檢查上述過程是否報錯 const programLog = gl.getProgramInfoLog( program ).trim(); // 獲取報錯信息 if (...) { // 出錯判斷 console.error(... 'gl.getProgramInfoLog' ...) // 報錯位置 } }
從代碼中可以看出,這個函數(shù)首先創(chuàng)建了一個program,然后給這個program添加頂點和片元著色器,然后編譯。那么編譯編的是什么呢?
我覺得編譯編的應(yīng)該是文本,把文本編譯成可以執(zhí)行的代碼片段。到底對不對呢?
我們知道,頂點著色器和片元著色器是使用著色器語言編寫的,這是一種類C的語言,并不是我們熟悉的javascript。上面創(chuàng)建著色器的WebGLShader是THREE.js封裝的一個函數(shù),它的第三個參數(shù)就是源碼的字符串形式。然后調(diào)用compileShader
進行編譯:
function WebGLShader( gl, type, string ) { const shader = gl.createShader( type ); gl.shaderSource( shader, string ); gl.compileShader( shader ); return shader; }
所以,上述錯誤有可能就是動態(tài)生成的著色器源碼有問題。因為varying變量用于從頂點著色器往片元著色器中傳輸數(shù)據(jù),所以同一個varying變量在兩個著色器里面都要聲明,所以我們只需要分析一個就行。我分析的是頂點著色器,也就是上面的vertexGlsl
變量:
let vertexShader = parameters.vertexShader; // ... if ( parameters.isRawShaderMaterial ) { // 自定義著色器,不考慮 } else { // THREE.js提供的著色器 prefixVertex = [...] } const vertexGlsl = prefixVertex + vertexShader;
我們找到了與vertexGlsl
相關(guān)的兩個變量prefixVertex
和vertexShader
。
prefixVertex文本分析
報錯是在開啟castShadow之后才有的,所以看下里面有沒有和castShadow相關(guān)的代碼。首先prefixVertex里面有一個shadowMapEnabled,感覺有點關(guān)系:
parameters.shadowMapEnabled ? '#define USE_SHADOWMAP' : '', parameters.shadowMapEnabled ? '#define ' + shadowMapTypeDefine : '',
接下來看下parameters來驗證下,可以看到這個變量是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ù),所以接著看下這個函數(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
時傳入的shadows
變量是啥?發(fā)現(xiàn)是shadowsArray
:
const shadowsArray = currentRenderState.state.shadowsArray;
currentRenderState = renderStates.get( scene, _currentArrayCamera || camera ); // 找了一處賦值
renderStates = new WebGLRenderStates()
WebGLStates內(nèi)部使用了一個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;
感覺能對應(yīng)上:
function pushShadow( shadowLight ) { shadowsArray.push( shadowLight ); }
接下來,就是看下pushShadow是在哪調(diào)用的,在WebGLRenderer.js文件的compile方法中有調(diào)用:
this.compile = function ( scene, camera ) { // 前面講過renderStates是一個雙層的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,然后遍歷場景對象,把castShadow的光源放到shadowsArray里面。后面開始初始化材質(zhì),初始化材質(zhì)的時候會編譯前面說到的頂點著色器和片元著色器。
我們回到代碼片段shadowMapEnabled: renderer.shadowMap.enabled && shadows.length > 0
,當我們在場景中添加了castShadow的光源的時候,這個shadows數(shù)組的長度就是大于0的,所以shadowMapEnabled就是true。
那么,prefixVertex文本里面就會包含#define USE_SHADOWMAP
:
parameters.shadowMapEnabled ? '#define USE_SHADOWMAP' : '',
分析完prefixVertex會發(fā)現(xiàn)相關(guān)的就是在頂點著色器代碼中添加了#define USE_SHADOWMAP
,并沒有看到varying聲明??磥韛arying聲明應(yīng)該會在vertexShader文本中。
vertexShader文本分析
在WebGLProgram函數(shù)中,vertexShader是parameters的一個屬性:
function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) { let vertexShader = parameters.vertexShader; }
同樣,我們找到WebGLPrograms.js文件看下getParameters函數(shù)里面vertexShader是如何得出來的:
function getParameters( material, lights, shadows, scene, nClipPlanes, nClipIntersection, object ) { // ... const shaderID = shaderIDs[ material.type ]; // ... let vertexShader, fragmentShader; if ( shaderID ) { // 內(nèi)置頂點著色器/片元著色器代碼 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 `;
注意開頭的#ifdef USE_SHADOWMAP
,還記得prefixVertex最后生成了個啥嗎?不正是USE_SHADOWMAP
嘛:
#define USE_SHADOWMAP
如果沒有這個define
,那么ifdef
這個判斷就會失敗,就不會走到中間這部分代碼了。
假設(shè)我們使用的是spotLight,我們看到會聲明一個長度是NUM_SPOT_LIGHT_SHADOWS
的vec4數(shù)組。這個數(shù)組的長度是多少,就會占用多少個varying變量的名額:
varying vec4 vSpotShadowCoord[ NUM_SPOT_LIGHT_SHADOWS ];
猜測一下,NUM_SPOT_LIGHT_SHADOWS
變量應(yīng)該就是啟用了castShadow
的光源的數(shù)量。驗證一下?
源代碼中搜索NUM_SPOT_LIGHT_SHADOWS
,會在WebGLProgram.js文件中發(fā)現(xiàn)函數(shù):
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
函數(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個啟用castShadow的光源會占用n個varying變量。
頂點著色器除了castShadow的光源占用的varying之外,還會有其他的varying變量,所有varying變量加起來不能超過15個,在我的例子中,castShadow的光源最多大概12、13個左右。
解決方案?
那么如何在場景中添加超過varying限制數(shù)量的啟用castShadow的光源呢?
搜索了下,看到有說可以使用WebGLDeferredRenderer
的,但是這個在前幾年就因為沒有資源維護給移除了。
以上就是THREE.js添加多個castShadow光源報錯解決及原因分析的詳細內(nèi)容,更多關(guān)于THREE.js添加多個光源報錯的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于JavaScript代碼實現(xiàn)微信掃一掃下載APP
有很多人在做微信的掃一掃下載。但是在微信更新之后微信將該功能給禁止掉了,也不能說是全面禁止吧,因為騰訊、微信是一家嘛,通過應(yīng)用寶審核的應(yīng)用好像還是可以通過掃一掃直接下載的,下面通過本篇文章給大家介紹微信掃一掃下載app的代碼片段,感興趣的朋友一起看看吧2015-12-12