babylon.shadowGenerator.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. module BABYLON {
  2. export interface IShadowGenerator {
  3. getShadowMap(): RenderTargetTexture;
  4. dispose(): void;
  5. }
  6. export class ShadowGenerator implements IShadowGenerator {
  7. private static _FILTER_NONE = 0;
  8. private static _FILTER_EXPONENTIALSHADOWMAP = 1;
  9. private static _FILTER_POISSONSAMPLING = 2;
  10. private static _FILTER_BLUREXPONENTIALSHADOWMAP = 3;
  11. // Static
  12. public static get FILTER_NONE(): number {
  13. return ShadowGenerator._FILTER_NONE;
  14. }
  15. public static get FILTER_POISSONSAMPLING(): number {
  16. return ShadowGenerator._FILTER_POISSONSAMPLING;
  17. }
  18. public static get FILTER_EXPONENTIALSHADOWMAP(): number {
  19. return ShadowGenerator._FILTER_EXPONENTIALSHADOWMAP;
  20. }
  21. public static get FILTER_BLUREXPONENTIALSHADOWMAP(): number {
  22. return ShadowGenerator._FILTER_BLUREXPONENTIALSHADOWMAP;
  23. }
  24. // Members
  25. private _filter = ShadowGenerator.FILTER_NONE;
  26. public blurScale = 2;
  27. private _blurBoxOffset = 0;
  28. private _bias = 0.00005;
  29. private _lightDirection = Vector3.Zero();
  30. private _depthScale: number;
  31. public forceBackFacesOnly = false;
  32. public get bias(): number {
  33. return this._bias;
  34. }
  35. public set bias(bias: number) {
  36. this._bias = bias;
  37. }
  38. public get blurBoxOffset(): number {
  39. return this._blurBoxOffset;
  40. }
  41. public set blurBoxOffset(value: number) {
  42. if (this._blurBoxOffset === value) {
  43. return;
  44. }
  45. this._blurBoxOffset = value;
  46. if (this._boxBlurPostprocess) {
  47. this._boxBlurPostprocess.dispose();
  48. }
  49. var textureType: number;
  50. var caps = this._scene.getEngine().getCaps();
  51. if (this._useFullFloat) {
  52. textureType = Engine.TEXTURETYPE_FLOAT;
  53. }
  54. else {
  55. textureType = Engine.TEXTURETYPE_UNSIGNED_INT;
  56. }
  57. this._boxBlurPostprocess = new PostProcess("DepthBoxBlur", "depthBoxBlur", ["screenSize", "boxOffset"], [], 1.0 / this.blurScale, null, Texture.BILINEAR_SAMPLINGMODE, this._scene.getEngine(), false, "#define OFFSET " + value, textureType);
  58. this._boxBlurPostprocess.onApplyObservable.add(effect => {
  59. effect.setFloat2("screenSize", this._mapSize / this.blurScale, this._mapSize / this.blurScale);
  60. });
  61. }
  62. public get depthScale(): number {
  63. return this._depthScale !== undefined ? this._depthScale : this._light.getDepthScale();
  64. }
  65. public set depthScale(value: number) {
  66. this._depthScale = value;
  67. }
  68. public get filter(): number {
  69. return this._filter;
  70. }
  71. public set filter(value: number) {
  72. if (this._filter === value) {
  73. return;
  74. }
  75. this._filter = value;
  76. if (this.usePoissonSampling || this.useExponentialShadowMap || this.useBlurExponentialShadowMap) {
  77. this._shadowMap.anisotropicFilteringLevel = 16;
  78. this._shadowMap.updateSamplingMode(Texture.BILINEAR_SAMPLINGMODE);
  79. } else {
  80. this._shadowMap.anisotropicFilteringLevel = 1;
  81. this._shadowMap.updateSamplingMode(Texture.NEAREST_SAMPLINGMODE);
  82. }
  83. }
  84. public get useVarianceShadowMap(): boolean {
  85. Tools.Warn("VSM are now replaced by ESM. Please use useExponentialShadowMap instead.");
  86. return this.useExponentialShadowMap;
  87. }
  88. public set useVarianceShadowMap(value: boolean) {
  89. Tools.Warn("VSM are now replaced by ESM. Please use useExponentialShadowMap instead.");
  90. this.useExponentialShadowMap = value;
  91. }
  92. public get useExponentialShadowMap(): boolean {
  93. return this.filter === ShadowGenerator.FILTER_EXPONENTIALSHADOWMAP;
  94. }
  95. public set useExponentialShadowMap(value: boolean) {
  96. this.filter = (value ? ShadowGenerator.FILTER_EXPONENTIALSHADOWMAP : ShadowGenerator.FILTER_NONE);
  97. }
  98. public get usePoissonSampling(): boolean {
  99. return this.filter === ShadowGenerator.FILTER_POISSONSAMPLING;
  100. }
  101. public set usePoissonSampling(value: boolean) {
  102. this.filter = (value ? ShadowGenerator.FILTER_POISSONSAMPLING : ShadowGenerator.FILTER_NONE);
  103. }
  104. public get useBlurVarianceShadowMap(): boolean {
  105. Tools.Warn("VSM are now replaced by ESM. Please use useBlurExponentialShadowMap instead.");
  106. return this.useBlurExponentialShadowMap;
  107. }
  108. public set useBlurVarianceShadowMap(value: boolean) {
  109. Tools.Warn("VSM are now replaced by ESM. Please use useBlurExponentialShadowMap instead.");
  110. this.useBlurExponentialShadowMap = value;
  111. }
  112. public get useBlurExponentialShadowMap(): boolean {
  113. return this.filter === ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP;
  114. }
  115. public set useBlurExponentialShadowMap(value: boolean) {
  116. if (this._light.needCube() && value) {
  117. this.useExponentialShadowMap = true; // Blurring the cubemap is going to be too expensive. Reverting to unblurred version
  118. } else {
  119. this.filter = (value ? ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP : ShadowGenerator.FILTER_NONE);
  120. }
  121. }
  122. private _light: IShadowLight;
  123. private _scene: Scene;
  124. private _shadowMap: RenderTargetTexture;
  125. private _shadowMap2: RenderTargetTexture;
  126. private _darkness = 0;
  127. private _transparencyShadow = false;
  128. private _effect: Effect;
  129. private _viewMatrix = Matrix.Zero();
  130. private _projectionMatrix = Matrix.Zero();
  131. private _transformMatrix = Matrix.Zero();
  132. private _worldViewProjection = Matrix.Zero();
  133. private _cachedPosition: Vector3;
  134. private _cachedDirection: Vector3;
  135. private _cachedDefines: string;
  136. private _currentRenderID: number;
  137. private _downSamplePostprocess: PassPostProcess;
  138. private _boxBlurPostprocess: PostProcess;
  139. private _mapSize: number;
  140. private _currentFaceIndex = 0;
  141. private _currentFaceIndexCache = 0;
  142. private _useFullFloat = true;
  143. /**
  144. * Creates a ShadowGenerator object.
  145. * A ShadowGenerator is the required tool to use the shadows.
  146. * Each light casting shadows needs to use its own ShadowGenerator.
  147. * Required parameters :
  148. * - `mapSize` (integer), the size of the texture what stores the shadows. Example : 1024.
  149. * - `light` : the light object generating the shadows.
  150. * Documentation : http://doc.babylonjs.com/tutorials/shadows
  151. */
  152. constructor(mapSize: number, light: IShadowLight) {
  153. this._light = light;
  154. this._scene = light.getScene();
  155. this._mapSize = mapSize;
  156. light._shadowGenerator = this;
  157. // Texture type fallback from float to int if not supported.
  158. var textureType: number;
  159. var caps = this._scene.getEngine().getCaps();
  160. if (caps.textureFloat && caps.textureFloatLinearFiltering && caps.textureFloatRender) {
  161. this._useFullFloat = true;
  162. textureType = Engine.TEXTURETYPE_FLOAT;
  163. }
  164. else {
  165. this._useFullFloat = false;
  166. textureType = Engine.TEXTURETYPE_UNSIGNED_INT;
  167. }
  168. // Render target
  169. this._shadowMap = new RenderTargetTexture(light.name + "_shadowMap", mapSize, this._scene, false, true, textureType, light.needCube());
  170. this._shadowMap.wrapU = Texture.CLAMP_ADDRESSMODE;
  171. this._shadowMap.wrapV = Texture.CLAMP_ADDRESSMODE;
  172. this._shadowMap.anisotropicFilteringLevel = 1;
  173. this._shadowMap.updateSamplingMode(Texture.BILINEAR_SAMPLINGMODE);
  174. this._shadowMap.renderParticles = false;
  175. this._shadowMap.onBeforeRenderObservable.add((faceIndex: number) => {
  176. this._currentFaceIndex = faceIndex;
  177. });
  178. this._shadowMap.onAfterUnbindObservable.add(() => {
  179. if (!this.useBlurExponentialShadowMap) {
  180. return;
  181. }
  182. if (!this._shadowMap2) {
  183. this._shadowMap2 = new RenderTargetTexture(light.name + "_shadowMap", mapSize, this._scene, false, true, textureType);
  184. this._shadowMap2.wrapU = Texture.CLAMP_ADDRESSMODE;
  185. this._shadowMap2.wrapV = Texture.CLAMP_ADDRESSMODE;
  186. this._shadowMap2.updateSamplingMode(Texture.BILINEAR_SAMPLINGMODE);
  187. this._downSamplePostprocess = new PassPostProcess("downScale", 1.0 / this.blurScale, null, Texture.BILINEAR_SAMPLINGMODE, this._scene.getEngine(), false, textureType);
  188. this._downSamplePostprocess.onApplyObservable.add(effect => {
  189. effect.setTexture("textureSampler", this._shadowMap);
  190. });
  191. this.blurBoxOffset = 1;
  192. }
  193. this._scene.postProcessManager.directRender([this._downSamplePostprocess, this._boxBlurPostprocess], this._shadowMap2.getInternalTexture());
  194. });
  195. // Custom render function
  196. var renderSubMesh = (subMesh: SubMesh): void => {
  197. var mesh = subMesh.getRenderingMesh();
  198. var scene = this._scene;
  199. var engine = scene.getEngine();
  200. // Culling
  201. engine.setState(subMesh.getMaterial().backFaceCulling);
  202. // Managing instances
  203. var batch = mesh._getInstancesRenderList(subMesh._id);
  204. if (batch.mustReturn) {
  205. return;
  206. }
  207. var hardwareInstancedRendering = (engine.getCaps().instancedArrays !== null) && (batch.visibleInstances[subMesh._id] !== null) && (batch.visibleInstances[subMesh._id] !== undefined);
  208. if (this.isReady(subMesh, hardwareInstancedRendering)) {
  209. engine.enableEffect(this._effect);
  210. mesh._bind(subMesh, this._effect, Material.TriangleFillMode);
  211. var material = subMesh.getMaterial();
  212. this._effect.setFloat2("biasAndScale", this.bias, this.depthScale);
  213. this._effect.setMatrix("viewProjection", this.getTransformMatrix());
  214. this._effect.setVector3("lightPosition", this.getLight().position);
  215. if (this.getLight().needCube()) {
  216. this._effect.setFloat2("depthValues", scene.activeCamera.minZ, scene.activeCamera.maxZ);
  217. }
  218. // Alpha test
  219. if (material && material.needAlphaTesting()) {
  220. var alphaTexture = material.getAlphaTestTexture();
  221. this._effect.setTexture("diffuseSampler", alphaTexture);
  222. this._effect.setMatrix("diffuseMatrix", alphaTexture.getTextureMatrix());
  223. }
  224. // Bones
  225. if (mesh.useBones && mesh.computeBonesUsingShaders) {
  226. this._effect.setMatrices("mBones", mesh.skeleton.getTransformMatrices(mesh));
  227. }
  228. if (this.forceBackFacesOnly) {
  229. engine.setState(true, 0, false, true);
  230. }
  231. // Draw
  232. mesh._processRendering(subMesh, this._effect, Material.TriangleFillMode, batch, hardwareInstancedRendering,
  233. (isInstance, world) => this._effect.setMatrix("world", world));
  234. if (this.forceBackFacesOnly) {
  235. engine.setState(true, 0, false, false);
  236. }
  237. } else {
  238. // Need to reset refresh rate of the shadowMap
  239. this._shadowMap.resetRefreshCounter();
  240. }
  241. };
  242. this._shadowMap.customRenderFunction = (opaqueSubMeshes: SmartArray<SubMesh>, alphaTestSubMeshes: SmartArray<SubMesh>, transparentSubMeshes: SmartArray<SubMesh>): void => {
  243. var index: number;
  244. for (index = 0; index < opaqueSubMeshes.length; index++) {
  245. renderSubMesh(opaqueSubMeshes.data[index]);
  246. }
  247. for (index = 0; index < alphaTestSubMeshes.length; index++) {
  248. renderSubMesh(alphaTestSubMeshes.data[index]);
  249. }
  250. if (this._transparencyShadow) {
  251. for (index = 0; index < transparentSubMeshes.length; index++) {
  252. renderSubMesh(transparentSubMeshes.data[index]);
  253. }
  254. }
  255. };
  256. this._shadowMap.onClearObservable.add((engine: Engine) => {
  257. if (this.useExponentialShadowMap || this.useBlurExponentialShadowMap) {
  258. engine.clear(new Color4(0, 0, 0, 0), true, true, true);
  259. } else {
  260. engine.clear(new Color4(1.0, 1.0, 1.0, 1.0), true, true, true);
  261. }
  262. });
  263. }
  264. /**
  265. * Boolean : true when the ShadowGenerator is finally computed.
  266. */
  267. public isReady(subMesh: SubMesh, useInstances: boolean): boolean {
  268. var defines = [];
  269. if (this._useFullFloat) {
  270. defines.push("#define FULLFLOAT");
  271. }
  272. if (this.useExponentialShadowMap || this.useBlurExponentialShadowMap) {
  273. defines.push("#define ESM");
  274. }
  275. if (this.getLight().needCube()) {
  276. defines.push("#define CUBEMAP");
  277. }
  278. var attribs = [VertexBuffer.PositionKind];
  279. var mesh = subMesh.getMesh();
  280. var material = subMesh.getMaterial();
  281. // Alpha test
  282. if (material && material.needAlphaTesting()) {
  283. defines.push("#define ALPHATEST");
  284. if (mesh.isVerticesDataPresent(VertexBuffer.UVKind)) {
  285. attribs.push(VertexBuffer.UVKind);
  286. defines.push("#define UV1");
  287. }
  288. if (mesh.isVerticesDataPresent(VertexBuffer.UV2Kind)) {
  289. var alphaTexture = material.getAlphaTestTexture();
  290. if (alphaTexture.coordinatesIndex === 1) {
  291. attribs.push(VertexBuffer.UV2Kind);
  292. defines.push("#define UV2");
  293. }
  294. }
  295. }
  296. // Bones
  297. if (mesh.useBones && mesh.computeBonesUsingShaders) {
  298. attribs.push(VertexBuffer.MatricesIndicesKind);
  299. attribs.push(VertexBuffer.MatricesWeightsKind);
  300. if (mesh.numBoneInfluencers > 4) {
  301. attribs.push(VertexBuffer.MatricesIndicesExtraKind);
  302. attribs.push(VertexBuffer.MatricesWeightsExtraKind);
  303. }
  304. defines.push("#define NUM_BONE_INFLUENCERS " + mesh.numBoneInfluencers);
  305. defines.push("#define BonesPerMesh " + (mesh.skeleton.bones.length + 1));
  306. } else {
  307. defines.push("#define NUM_BONE_INFLUENCERS 0");
  308. }
  309. // Instances
  310. if (useInstances) {
  311. defines.push("#define INSTANCES");
  312. attribs.push("world0");
  313. attribs.push("world1");
  314. attribs.push("world2");
  315. attribs.push("world3");
  316. }
  317. // Get correct effect
  318. var join = defines.join("\n");
  319. if (this._cachedDefines !== join) {
  320. this._cachedDefines = join;
  321. this._effect = this._scene.getEngine().createEffect("shadowMap",
  322. attribs,
  323. ["world", "mBones", "viewProjection", "diffuseMatrix", "lightPosition", "depthValues", "biasAndScale"],
  324. ["diffuseSampler"], join);
  325. }
  326. return this._effect.isReady();
  327. }
  328. /**
  329. * Returns a RenderTargetTexture object : the shadow map texture.
  330. */
  331. public getShadowMap(): RenderTargetTexture {
  332. return this._shadowMap;
  333. }
  334. /**
  335. * Returns the most ready computed shadow map as a RenderTargetTexture object.
  336. */
  337. public getShadowMapForRendering(): RenderTargetTexture {
  338. if (this._shadowMap2) {
  339. return this._shadowMap2;
  340. }
  341. return this._shadowMap;
  342. }
  343. /**
  344. * Returns the associated light object.
  345. */
  346. public getLight(): IShadowLight {
  347. return this._light;
  348. }
  349. // Methods
  350. /**
  351. * Returns a Matrix object : the updated transformation matrix.
  352. */
  353. public getTransformMatrix(): Matrix {
  354. var scene = this._scene;
  355. if (this._currentRenderID === scene.getRenderId() && this._currentFaceIndexCache === this._currentFaceIndex) {
  356. return this._transformMatrix;
  357. }
  358. this._currentRenderID = scene.getRenderId();
  359. this._currentFaceIndexCache = this._currentFaceIndex;
  360. var lightPosition = this._light.position;
  361. Vector3.NormalizeToRef(this._light.getShadowDirection(this._currentFaceIndex), this._lightDirection);
  362. if (Math.abs(Vector3.Dot(this._lightDirection, Vector3.Up())) === 1.0) {
  363. this._lightDirection.z = 0.0000000000001; // Required to avoid perfectly perpendicular light
  364. }
  365. if (this._light.computeTransformedPosition()) {
  366. lightPosition = this._light.transformedPosition;
  367. }
  368. if (this._light.needRefreshPerFrame() || !this._cachedPosition || !this._cachedDirection || !lightPosition.equals(this._cachedPosition) || !this._lightDirection.equals(this._cachedDirection)) {
  369. this._cachedPosition = lightPosition.clone();
  370. this._cachedDirection = this._lightDirection.clone();
  371. Matrix.LookAtLHToRef(lightPosition, lightPosition.add(this._lightDirection), Vector3.Up(), this._viewMatrix);
  372. this._light.setShadowProjectionMatrix(this._projectionMatrix, this._viewMatrix, this.getShadowMap().renderList);
  373. this._viewMatrix.multiplyToRef(this._projectionMatrix, this._transformMatrix);
  374. }
  375. return this._transformMatrix;
  376. }
  377. /**
  378. * Returns the darkness value (float).
  379. */
  380. public getDarkness(): number {
  381. return this._darkness;
  382. }
  383. /**
  384. * Sets the ShadowGenerator darkness value (float <= 1.0).
  385. * Returns the ShadowGenerator.
  386. */
  387. public setDarkness(darkness: number): ShadowGenerator {
  388. if (darkness >= 1.0)
  389. this._darkness = 1.0;
  390. else if (darkness <= 0.0)
  391. this._darkness = 0.0;
  392. else
  393. this._darkness = darkness;
  394. return this;
  395. }
  396. /**
  397. * Sets the ability to have transparent shadow (boolean).
  398. * Returns the ShadowGenerator.
  399. */
  400. public setTransparencyShadow(hasShadow: boolean): ShadowGenerator {
  401. this._transparencyShadow = hasShadow;
  402. return this;
  403. }
  404. private _packHalf(depth: number): Vector2 {
  405. var scale = depth * 255.0;
  406. var fract = scale - Math.floor(scale);
  407. return new Vector2(depth - fract / 255.0, fract);
  408. }
  409. /**
  410. * Disposes the ShadowGenerator.
  411. * Returns nothing.
  412. */
  413. public dispose(): void {
  414. this._shadowMap.dispose();
  415. if (this._shadowMap2) {
  416. this._shadowMap2.dispose();
  417. }
  418. if (this._downSamplePostprocess) {
  419. this._downSamplePostprocess.dispose();
  420. }
  421. if (this._boxBlurPostprocess) {
  422. this._boxBlurPostprocess.dispose();
  423. }
  424. this._light._shadowGenerator = null;
  425. }
  426. /**
  427. * Serializes the ShadowGenerator and returns a serializationObject.
  428. */
  429. public serialize(): any {
  430. var serializationObject: any = {};
  431. serializationObject.lightId = this._light.id;
  432. serializationObject.mapSize = this.getShadowMap().getRenderSize();
  433. serializationObject.useExponentialShadowMap = this.useExponentialShadowMap;
  434. serializationObject.useBlurExponentialShadowMap = this.useBlurExponentialShadowMap;
  435. serializationObject.usePoissonSampling = this.usePoissonSampling;
  436. serializationObject.forceBackFacesOnly = this.forceBackFacesOnly;
  437. serializationObject.darkness = this.getDarkness();
  438. serializationObject.renderList = [];
  439. for (var meshIndex = 0; meshIndex < this.getShadowMap().renderList.length; meshIndex++) {
  440. var mesh = this.getShadowMap().renderList[meshIndex];
  441. serializationObject.renderList.push(mesh.id);
  442. }
  443. return serializationObject;
  444. }
  445. /**
  446. * Parses a serialized ShadowGenerator and returns a new ShadowGenerator.
  447. */
  448. public static Parse(parsedShadowGenerator: any, scene: Scene): ShadowGenerator {
  449. //casting to point light, as light is missing the position attr and typescript complains.
  450. var light = <PointLight>scene.getLightByID(parsedShadowGenerator.lightId);
  451. var shadowGenerator = new ShadowGenerator(parsedShadowGenerator.mapSize, light);
  452. for (var meshIndex = 0; meshIndex < parsedShadowGenerator.renderList.length; meshIndex++) {
  453. var meshes = scene.getMeshesByID(parsedShadowGenerator.renderList[meshIndex]);
  454. meshes.forEach(function (mesh) {
  455. shadowGenerator.getShadowMap().renderList.push(mesh);
  456. });
  457. }
  458. if (parsedShadowGenerator.usePoissonSampling) {
  459. shadowGenerator.usePoissonSampling = true;
  460. }
  461. else if (parsedShadowGenerator.useExponentialShadowMap) {
  462. shadowGenerator.useExponentialShadowMap = true;
  463. }
  464. else if (parsedShadowGenerator.useBlurExponentialShadowMap) {
  465. shadowGenerator.useBlurExponentialShadowMap = true;
  466. }
  467. // Backward compat
  468. else if (parsedShadowGenerator.useVarianceShadowMap) {
  469. shadowGenerator.useExponentialShadowMap = true;
  470. }
  471. else if (parsedShadowGenerator.useBlurVarianceShadowMap) {
  472. shadowGenerator.useBlurExponentialShadowMap = true;
  473. }
  474. if (parsedShadowGenerator.blurScale) {
  475. shadowGenerator.blurScale = parsedShadowGenerator.blurScale;
  476. }
  477. if (parsedShadowGenerator.blurBoxOffset) {
  478. shadowGenerator.blurBoxOffset = parsedShadowGenerator.blurBoxOffset;
  479. }
  480. if (parsedShadowGenerator.bias !== undefined) {
  481. shadowGenerator.bias = parsedShadowGenerator.bias;
  482. }
  483. if (parsedShadowGenerator.darkness) {
  484. shadowGenerator.setDarkness(parsedShadowGenerator.darkness);
  485. }
  486. shadowGenerator.forceBackFacesOnly = parsedShadowGenerator.forceBackFacesOnly;
  487. return shadowGenerator;
  488. }
  489. }
  490. }