@zephyr3d/scene 0.9.25 → 0.9.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/dist/animation/hair/gpu_hair_simulation.js +1090 -0
  2. package/dist/animation/hair/gpu_hair_simulation.js.map +1 -0
  3. package/dist/animation/joint_dynamics/controller.js +7 -5
  4. package/dist/animation/joint_dynamics/controller.js.map +1 -1
  5. package/dist/animation/joint_dynamics/joint_dynamics_system.js +81 -5
  6. package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
  7. package/dist/animation/morphtrack.js +3 -4
  8. package/dist/animation/morphtrack.js.map +1 -1
  9. package/dist/animation/skeleton.js +58 -24
  10. package/dist/animation/skeleton.js.map +1 -1
  11. package/dist/animation/spring/multi_chain_spring_system.js +577 -179
  12. package/dist/animation/spring/multi_chain_spring_system.js.map +1 -1
  13. package/dist/animation/spring/spring_collider.js +4 -3
  14. package/dist/animation/spring/spring_collider.js.map +1 -1
  15. package/dist/animation/spring/spring_node_pose_tracker.js +40 -0
  16. package/dist/animation/spring/spring_node_pose_tracker.js.map +1 -0
  17. package/dist/animation/spring/spring_particle.js +2 -0
  18. package/dist/animation/spring/spring_particle.js.map +1 -1
  19. package/dist/animation/spring/spring_system.js +11 -3
  20. package/dist/animation/spring/spring_system.js.map +1 -1
  21. package/dist/animation/spring_modifier.js +10 -0
  22. package/dist/animation/spring_modifier.js.map +1 -1
  23. package/dist/app/engine.js +53 -43
  24. package/dist/app/engine.js.map +1 -1
  25. package/dist/app/scriptingsystem.js +119 -6
  26. package/dist/app/scriptingsystem.js.map +1 -1
  27. package/dist/app/scriptregistry.js +3 -2
  28. package/dist/app/scriptregistry.js.map +1 -1
  29. package/dist/asset/assetmanager.js +284 -67
  30. package/dist/asset/assetmanager.js.map +1 -1
  31. package/dist/asset/loaders/zhair/zhair_format.js +288 -0
  32. package/dist/asset/loaders/zhair/zhair_format.js.map +1 -0
  33. package/dist/asset/loaders/zhair/zhair_loader.js +221 -0
  34. package/dist/asset/loaders/zhair/zhair_loader.js.map +1 -0
  35. package/dist/asset/model.js +178 -67
  36. package/dist/asset/model.js.map +1 -1
  37. package/dist/camera/camera.js +193 -21
  38. package/dist/camera/camera.js.map +1 -1
  39. package/dist/camera/perspectivecamera.js +3 -1
  40. package/dist/camera/perspectivecamera.js.map +1 -1
  41. package/dist/index.d.ts +4019 -1278
  42. package/dist/index.js +15 -5
  43. package/dist/index.js.map +1 -1
  44. package/dist/material/eye.js +932 -0
  45. package/dist/material/eye.js.map +1 -0
  46. package/dist/material/hair.js +917 -0
  47. package/dist/material/hair.js.map +1 -0
  48. package/dist/material/hairstrand.js +842 -0
  49. package/dist/material/hairstrand.js.map +1 -0
  50. package/dist/material/hairstrand_data.js +162 -0
  51. package/dist/material/hairstrand_data.js.map +1 -0
  52. package/dist/material/lambert.js +5 -2
  53. package/dist/material/lambert.js.map +1 -1
  54. package/dist/material/material.js +338 -300
  55. package/dist/material/material.js.map +1 -1
  56. package/dist/material/meshmaterial.js +120 -19
  57. package/dist/material/meshmaterial.js.map +1 -1
  58. package/dist/material/mixins/albedocolor.js +1 -1
  59. package/dist/material/mixins/albedocolor.js.map +1 -1
  60. package/dist/material/mixins/lightmodel/blinnphong.js +5 -3
  61. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  62. package/dist/material/mixins/lightmodel/lambert.js +5 -4
  63. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  64. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +3 -3
  65. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  66. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +5 -5
  67. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  68. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +3 -3
  69. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  70. package/dist/material/mixins/lit.js +73 -18
  71. package/dist/material/mixins/lit.js.map +1 -1
  72. package/dist/material/mixins/pbr/common.js +4 -4
  73. package/dist/material/mixins/pbr/common.js.map +1 -1
  74. package/dist/material/mixins/texture.js +48 -11
  75. package/dist/material/mixins/texture.js.map +1 -1
  76. package/dist/material/mtoon.js +6 -5
  77. package/dist/material/mtoon.js.map +1 -1
  78. package/dist/material/pbrblueprint.js +146 -28
  79. package/dist/material/pbrblueprint.js.map +1 -1
  80. package/dist/material/pbrblueprint_instance.js +501 -0
  81. package/dist/material/pbrblueprint_instance.js.map +1 -0
  82. package/dist/material/pbrmr.js +15 -16
  83. package/dist/material/pbrmr.js.map +1 -1
  84. package/dist/material/shader/helper.js +330 -20
  85. package/dist/material/shader/helper.js.map +1 -1
  86. package/dist/material/shader/terrain_blend.js +55 -0
  87. package/dist/material/shader/terrain_blend.js.map +1 -0
  88. package/dist/material/skin.js +192 -32
  89. package/dist/material/skin.js.map +1 -1
  90. package/dist/material/spriteblueprint.js +16 -1
  91. package/dist/material/spriteblueprint.js.map +1 -1
  92. package/dist/material/subsurfaceprofile.js +86 -23
  93. package/dist/material/subsurfaceprofile.js.map +1 -1
  94. package/dist/material/water.js +3 -1
  95. package/dist/material/water.js.map +1 -1
  96. package/dist/posteffect/bloom.js +167 -7
  97. package/dist/posteffect/bloom.js.map +1 -1
  98. package/dist/posteffect/coloradjust.js +2 -1
  99. package/dist/posteffect/coloradjust.js.map +1 -1
  100. package/dist/posteffect/compositor.js.map +1 -1
  101. package/dist/posteffect/fxaa.js +2 -1
  102. package/dist/posteffect/fxaa.js.map +1 -1
  103. package/dist/posteffect/grayscale.js +3 -1
  104. package/dist/posteffect/grayscale.js.map +1 -1
  105. package/dist/posteffect/motionblur.js +4 -1
  106. package/dist/posteffect/motionblur.js.map +1 -1
  107. package/dist/posteffect/posteffect.js.map +1 -1
  108. package/dist/posteffect/skinsss.js +368 -69
  109. package/dist/posteffect/skinsss.js.map +1 -1
  110. package/dist/posteffect/ssgi.js +16 -9
  111. package/dist/posteffect/ssgi.js.map +1 -1
  112. package/dist/posteffect/ssr.js +3 -0
  113. package/dist/posteffect/ssr.js.map +1 -1
  114. package/dist/posteffect/sss.js +91 -34
  115. package/dist/posteffect/sss.js.map +1 -1
  116. package/dist/posteffect/taa.js.map +1 -1
  117. package/dist/posteffect/tonemap.js +44 -1
  118. package/dist/posteffect/tonemap.js.map +1 -1
  119. package/dist/render/abuffer_oit.js +45 -39
  120. package/dist/render/abuffer_oit.js.map +1 -1
  121. package/dist/render/cull_visitor.js +19 -0
  122. package/dist/render/cull_visitor.js.map +1 -1
  123. package/dist/render/depthpass.js +36 -2
  124. package/dist/render/depthpass.js.map +1 -1
  125. package/dist/render/drawable_mixin.js +26 -1
  126. package/dist/render/drawable_mixin.js.map +1 -1
  127. package/dist/render/dualdepthpeeling_oit.js +52 -51
  128. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  129. package/dist/render/envlight.js +1 -1
  130. package/dist/render/envlight.js.map +1 -1
  131. package/dist/render/fbm_wavegenerator.js +3 -0
  132. package/dist/render/fbm_wavegenerator.js.map +1 -1
  133. package/dist/render/gerstner_wavegenerator.js +3 -0
  134. package/dist/render/gerstner_wavegenerator.js.map +1 -1
  135. package/dist/render/globalbindgroup_allocator.js +3 -0
  136. package/dist/render/globalbindgroup_allocator.js.map +1 -1
  137. package/dist/render/oit.js +29 -0
  138. package/dist/render/oit.js.map +1 -0
  139. package/dist/render/render_queue.js +3 -0
  140. package/dist/render/render_queue.js.map +1 -1
  141. package/dist/render/rendergraph/device_pool_allocator.js.map +1 -1
  142. package/dist/render/rendergraph/executor.js +2 -0
  143. package/dist/render/rendergraph/executor.js.map +1 -1
  144. package/dist/render/rendergraph/forward_plus_builder.js +61 -1
  145. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  146. package/dist/render/rendergraph/rendergraph.js.map +1 -1
  147. package/dist/render/rendergraph/texture_affinity_cache.js +1 -1
  148. package/dist/render/rendergraph/texture_affinity_cache.js.map +1 -1
  149. package/dist/render/rendergraph/types.js.map +1 -1
  150. package/dist/render/shadow_mask_pass.js +73 -8
  151. package/dist/render/shadow_mask_pass.js.map +1 -1
  152. package/dist/render/sky.js +166 -126
  153. package/dist/render/sky.js.map +1 -1
  154. package/dist/render/weightedblended_oit.js +36 -35
  155. package/dist/render/weightedblended_oit.js.map +1 -1
  156. package/dist/scene/basesprite.js +8 -0
  157. package/dist/scene/basesprite.js.map +1 -1
  158. package/dist/scene/batchgroup.js +3 -0
  159. package/dist/scene/batchgroup.js.map +1 -1
  160. package/dist/scene/environment.js +4 -1
  161. package/dist/scene/environment.js.map +1 -1
  162. package/dist/scene/hair.js +963 -0
  163. package/dist/scene/hair.js.map +1 -0
  164. package/dist/scene/light.js +5 -1
  165. package/dist/scene/light.js.map +1 -1
  166. package/dist/scene/mesh.js +481 -47
  167. package/dist/scene/mesh.js.map +1 -1
  168. package/dist/scene/meshdrawable.js +6 -0
  169. package/dist/scene/meshdrawable.js.map +1 -1
  170. package/dist/scene/msdftext.js +3 -0
  171. package/dist/scene/msdftext.js.map +1 -1
  172. package/dist/scene/msdftextsprite.js +3 -0
  173. package/dist/scene/msdftextsprite.js.map +1 -1
  174. package/dist/scene/particlesys.js +6 -0
  175. package/dist/scene/particlesys.js.map +1 -1
  176. package/dist/scene/scene.js +3 -0
  177. package/dist/scene/scene.js.map +1 -1
  178. package/dist/scene/scene_node.js +6 -1
  179. package/dist/scene/scene_node.js.map +1 -1
  180. package/dist/scene/terrain-cm/grass.js +3 -0
  181. package/dist/scene/terrain-cm/grass.js.map +1 -1
  182. package/dist/scene/terrain-cm/grassmaterial.js +3 -0
  183. package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
  184. package/dist/scene/terrain-cm/terrain-cm.js +6 -0
  185. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  186. package/dist/scene/terrain-cm/texture_cache.js +309 -0
  187. package/dist/scene/terrain-cm/texture_cache.js.map +1 -0
  188. package/dist/scene/water.js +6 -0
  189. package/dist/scene/water.js.map +1 -1
  190. package/dist/shaders/atmosphere.js.map +1 -1
  191. package/dist/shaders/diffusion.js +54 -0
  192. package/dist/shaders/diffusion.js.map +1 -0
  193. package/dist/shaders/shadow.js +33 -21
  194. package/dist/shaders/shadow.js.map +1 -1
  195. package/dist/shaders/temporal.js +71 -33
  196. package/dist/shaders/temporal.js.map +1 -1
  197. package/dist/shadow/dom.js +546 -0
  198. package/dist/shadow/dom.js.map +1 -0
  199. package/dist/shadow/pcf_opt.js +3 -5
  200. package/dist/shadow/pcf_opt.js.map +1 -1
  201. package/dist/shadow/pcf_pd.js +1 -1
  202. package/dist/shadow/pcf_pd.js.map +1 -1
  203. package/dist/shadow/receiver_bias.js +41 -0
  204. package/dist/shadow/receiver_bias.js.map +1 -0
  205. package/dist/shadow/shader.js +20 -60
  206. package/dist/shadow/shader.js.map +1 -1
  207. package/dist/shadow/shadow_impl.js +89 -0
  208. package/dist/shadow/shadow_impl.js.map +1 -1
  209. package/dist/shadow/shadow_region.js +50 -50
  210. package/dist/shadow/shadow_region.js.map +1 -1
  211. package/dist/shadow/shadowmapper.js +150 -37
  212. package/dist/shadow/shadowmapper.js.map +1 -1
  213. package/dist/shapes/shape.js +11 -2
  214. package/dist/shapes/shape.js.map +1 -1
  215. package/dist/shapes/sphere.js +50 -9
  216. package/dist/shapes/sphere.js.map +1 -1
  217. package/dist/utility/blueprint/common/constants.js +112 -2
  218. package/dist/utility/blueprint/common/constants.js.map +1 -1
  219. package/dist/utility/blueprint/material/func.js +142 -49
  220. package/dist/utility/blueprint/material/func.js.map +1 -1
  221. package/dist/utility/blueprint/material/ir.js +490 -25
  222. package/dist/utility/blueprint/material/ir.js.map +1 -1
  223. package/dist/utility/blueprint/material/texture.js +898 -4
  224. package/dist/utility/blueprint/material/texture.js.map +1 -1
  225. package/dist/utility/serialization/manager.js +431 -100
  226. package/dist/utility/serialization/manager.js.map +1 -1
  227. package/dist/utility/serialization/scene/animation.js +370 -3
  228. package/dist/utility/serialization/scene/animation.js.map +1 -1
  229. package/dist/utility/serialization/scene/batch.js +4 -0
  230. package/dist/utility/serialization/scene/batch.js.map +1 -1
  231. package/dist/utility/serialization/scene/camera.js +133 -1
  232. package/dist/utility/serialization/scene/camera.js.map +1 -1
  233. package/dist/utility/serialization/scene/common.js +26 -5
  234. package/dist/utility/serialization/scene/common.js.map +1 -1
  235. package/dist/utility/serialization/scene/hair.js +1141 -0
  236. package/dist/utility/serialization/scene/hair.js.map +1 -0
  237. package/dist/utility/serialization/scene/light.js +84 -4
  238. package/dist/utility/serialization/scene/light.js.map +1 -1
  239. package/dist/utility/serialization/scene/material.js +1867 -166
  240. package/dist/utility/serialization/scene/material.js.map +1 -1
  241. package/dist/utility/serialization/scene/mesh.js +231 -14
  242. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  243. package/dist/utility/serialization/scene/node.js +141 -7
  244. package/dist/utility/serialization/scene/node.js.map +1 -1
  245. package/dist/utility/serialization/scene/particle.js +4 -0
  246. package/dist/utility/serialization/scene/particle.js.map +1 -1
  247. package/dist/utility/serialization/scene/primitive.js +18 -0
  248. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  249. package/dist/utility/serialization/scene/sprite.js +4 -0
  250. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  251. package/dist/utility/serialization/scene/terrain.js +4 -0
  252. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  253. package/dist/utility/serialization/scene/text.js +4 -0
  254. package/dist/utility/serialization/scene/text.js.map +1 -1
  255. package/dist/utility/serialization/scene/water.js +4 -0
  256. package/dist/utility/serialization/scene/water.js.map +1 -1
  257. package/dist/utility/serialization/types.js.map +1 -1
  258. package/dist/utility/shprojector.js +96 -95
  259. package/dist/utility/shprojector.js.map +1 -1
  260. package/dist/values.js +49 -2
  261. package/dist/values.js.map +1 -1
  262. package/package.json +3 -3
@@ -1,5 +1,5 @@
1
1
  import { Vector3, Vector4, Vector2, REVERSE_Z, DEPTH_FARTHEST, DEPTH_REDUCE_CLOSER, DEPTH_REDUCE_FARTHER } from '@zephyr3d/base';
2
- import { RENDER_PASS_TYPE_DEPTH, RENDER_PASS_TYPE_SHADOWMAP, RENDER_PASS_TYPE_OBJECT_COLOR, RENDER_PASS_TYPE_LIGHT, MaterialVaryingFlags, MORPH_WEIGHTS_VECTOR_COUNT, MORPH_TARGET_POSITION, MORPH_TARGET_NORMAL, MORPH_TARGET_TANGENT, MORPH_ATTRIBUTE_VECTOR_COUNT } from '../../values.js';
2
+ import { RENDER_PASS_TYPE_DEPTH, RENDER_PASS_TYPE_SHADOWMAP, RENDER_PASS_TYPE_OBJECT_COLOR, RENDER_PASS_TYPE_LIGHT, MaterialVaryingFlags, MAX_SKIN_EXTRA_INFLUENCE_PAIRS, MORPH_WEIGHTS_VECTOR_COUNT, MORPH_TARGET_POSITION, MORPH_TARGET_NORMAL, MORPH_TARGET_TANGENT, MORPH_ATTRIBUTE_VECTOR_COUNT, LIGHT_TYPE_DIRECTIONAL } from '../../values.js';
3
3
  import { ProgramBuilder } from '@zephyr3d/device';
4
4
  import { decodeNormalizedFloatFromRGBA, linearToGamma } from '../../shaders/misc.js';
5
5
  import { getSamplerOptions, fetchSampler } from '../../utility/misc.js';
@@ -7,6 +7,7 @@ import { PHYSICAL_BAKE_EXPOSURE } from '../../utility/physical.js';
7
7
  import { getDevice } from '../../app/api.js';
8
8
  import { getDefaultAtmosphereParams, getAtmosphereParamsStruct } from '../../shaders/atmosphere.js';
9
9
  import { getDefaultHeightFogParams, getHeightFogParamsStruct, calculateFog } from '../../shaders/fog.js';
10
+ import { getShadowReceiverNoL, getShadowReceiverBiasFactor } from '../../shadow/receiver_bias.js';
10
11
 
11
12
  const UNIFORM_NAME_LIGHT_BUFFER = 'Z_UniformLightBuffer';
12
13
  const UNIFORM_NAME_LIGHT_INDEX_TEXTURE = 'Z_UniformLightIndexTex';
@@ -32,6 +33,8 @@ const UNIFORM_NAME_INSTANCE_DATA_OFFSET = 'Z_UniformInstanceDataOffset';
32
33
  const UNIFORM_NAME_BONE_MATRICES = 'Z_UniformBoneMatrices';
33
34
  const UNIFORM_NAME_BONE_TEXTURE_SIZE = 'Z_UniformBoneTexSize';
34
35
  const UNIFORM_NAME_BONE_INV_BIND_MATRIX = 'Z_UniformBoneInvBindMatrix';
36
+ const UNIFORM_NAME_SKIN_INFLUENCE_DATA = 'Z_UniformSkinInfluenceData';
37
+ const UNIFORM_NAME_SKIN_INFLUENCE_INFO = 'Z_UniformSkinInfluenceInfo';
35
38
  const UNIFORM_NAME_MORPH_DATA = 'Z_UniformMorphData';
36
39
  const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
37
40
  /**
@@ -46,6 +49,7 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
46
49
  /** @internal */ static SKIN_MATRIX_NAME = 'Z_SkinMatrix';
47
50
  static SKIN_PREV_MATRIX_NAME = 'Z_PrevSkinMatrix';
48
51
  static SKIN_BONE_OFFSET = 'Z_boneOffset';
52
+ static RESOLVED_VERTEX_NORMAL_NAME = 'Z_ResolvedVertexNormal';
49
53
  /** @internal */ static _drawableBindGroupLayouts = {};
50
54
  /** @internal */ static _lightUniformShadow = {
51
55
  sunDir: new Vector3(),
@@ -101,6 +105,12 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
101
105
  static getBoneInvBindMatrixUniformName() {
102
106
  return UNIFORM_NAME_BONE_INV_BIND_MATRIX;
103
107
  }
108
+ static getSkinInfluenceDataUniformName() {
109
+ return UNIFORM_NAME_SKIN_INFLUENCE_DATA;
110
+ }
111
+ static getSkinInfluenceInfoUniformName() {
112
+ return UNIFORM_NAME_SKIN_INFLUENCE_INFO;
113
+ }
104
114
  static getMorphDataUniformName() {
105
115
  return UNIFORM_NAME_MORPH_DATA;
106
116
  }
@@ -183,10 +193,22 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
183
193
  pb.vec4('directionCutoff'),
184
194
  pb.mat4('viewMatrix'),
185
195
  pb.vec4('depthBias'),
196
+ // Same slot the lighting pass exposes through getShadowImplParams. A
197
+ // caster normally has no use for it, but one that encodes something
198
+ // beyond depth - deep opacity map layers - needs the same parameters the
199
+ // receiver will decode with.
200
+ pb.vec4('implParams'),
186
201
  pb.int('lightType')
187
202
  ]);
188
203
  scope.camera = cameraStruct().uniform(0);
189
204
  scope.light = lightStruct().uniform(0);
205
+ // Lets the shadow implementation add its own caster inputs. Only deep
206
+ // opacity maps use this, to read the frontmost depth its first pass wrote.
207
+ const shadowLight = ctx.renderPass.light;
208
+ const casterParams = shadowLight ? ctx.shadowMapInfo?.get(shadowLight) : null;
209
+ if (casterParams) {
210
+ casterParams.impl?.declareCasterUniforms(scope, casterParams);
211
+ }
190
212
  } else if (ctx.renderPass.type === RENDER_PASS_TYPE_DEPTH || ctx.renderPass.type === RENDER_PASS_TYPE_OBJECT_COLOR) {
191
213
  scope.camera = cameraStruct().uniform(0);
192
214
  } else if (ctx.renderPass.type === RENDER_PASS_TYPE_LIGHT) {
@@ -322,6 +344,8 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
322
344
  return null;
323
345
  }
324
346
  const pb = scope.$builder;
347
+ const isWebGL = pb.getDevice().type === 'webgl';
348
+ const supportsTextureLoad = !isWebGL;
325
349
  const funcNameGetBoneMatrixFromTexture = 'Z_getBoneMatrixFromTexture';
326
350
  pb.func(funcNameGetBoneMatrixFromTexture, [
327
351
  pb.int('boneIndex')
@@ -354,6 +378,33 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
354
378
  this.$l.m2 = scope.$g[funcNameGetBoneMatrixFromTexture](pb.int(blendIndices[2]));
355
379
  this.$l.m3 = scope.$g[funcNameGetBoneMatrixFromTexture](pb.int(blendIndices[3]));
356
380
  this.$l.m = pb.add(pb.mul(this.m0, blendWeights.x), pb.mul(this.m1, blendWeights.y), pb.mul(this.m2, blendWeights.z), pb.mul(this.m3, blendWeights.w));
381
+ this.$l.skinInfo = this[UNIFORM_NAME_SKIN_INFLUENCE_INFO];
382
+ this.$if(pb.greaterThan(this.skinInfo.z, 4), function() {
383
+ this.$l.vertexIndex = isWebGL ? pb.int(scope.$inputs.zFakeVertexID) : pb.int(scope.$builtins.vertexIndex);
384
+ this.$l.texWidth = pb.int(this.skinInfo.x);
385
+ this.$l.pairCount = pb.int(this.skinInfo.w);
386
+ this.$for(pb.int('pairIndex'), 0, MAX_SKIN_EXTRA_INFLUENCE_PAIRS, function() {
387
+ this.$if(pb.greaterThanEqual(this.pairIndex, this.pairCount), function() {
388
+ this.$break();
389
+ });
390
+ this.$l.pixelIndex = pb.add(pb.mul(this.vertexIndex, this.pairCount), this.pairIndex);
391
+ this.$l.xIndex = pb.mod(this.pixelIndex, this.texWidth);
392
+ this.$l.yIndex = pb.div(this.pixelIndex, this.texWidth);
393
+ if (supportsTextureLoad) {
394
+ this.$l.extra = pb.textureLoad(this[UNIFORM_NAME_SKIN_INFLUENCE_DATA], pb.ivec2(this.xIndex, this.yIndex), 0);
395
+ } else {
396
+ this.$l.u = pb.div(pb.add(pb.float(this.xIndex), 0.5), this.skinInfo.x);
397
+ this.$l.v = pb.div(pb.add(pb.float(this.yIndex), 0.5), this.skinInfo.y);
398
+ this.$l.extra = pb.textureSampleLevel(this[UNIFORM_NAME_SKIN_INFLUENCE_DATA], pb.vec2(this.u, this.v), 0);
399
+ }
400
+ this.$if(pb.greaterThan(this.extra.y, 0), function() {
401
+ this.m = pb.add(this.m, pb.mul(scope.$g[funcNameGetBoneMatrixFromTexture](pb.int(this.extra.x)), this.extra.y));
402
+ });
403
+ this.$if(pb.greaterThan(this.extra.w, 0), function() {
404
+ this.m = pb.add(this.m, pb.mul(scope.$g[funcNameGetBoneMatrixFromTexture](pb.int(this.extra.z)), this.extra.w));
405
+ });
406
+ });
407
+ });
357
408
  this.$return(pb.mul(invBindMatrix, this.m));
358
409
  });
359
410
  return scope.$g[funcNameGetSkinningMatrix]();
@@ -389,7 +440,8 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
389
440
  });
390
441
  this.$l.weight = morphInfo.at(pb.add(1, this.i)).at(this.j);
391
442
  this.$if(pb.notEqual(this.weight, 0), function() {
392
- this.$l.pixelIndex = pb.float(pb.add(this.offset, pb.mul(this.index, this.numVertices), this.vertexIndex));
443
+ this.$l.targetIndex = pb.int(morphInfo.at(pb.add(1 + MORPH_WEIGHTS_VECTOR_COUNT, this.i)).at(this.j));
444
+ this.$l.pixelIndex = pb.float(pb.add(this.offset, pb.mul(this.targetIndex, this.numVertices), this.vertexIndex));
393
445
  this.$l.xIndex = pb.mod(this.pixelIndex, this.texWidth);
394
446
  this.$l.yIndex = pb.floor(pb.div(this.pixelIndex, this.texWidth));
395
447
  this.$l.u = pb.div(pb.add(this.xIndex, 0.5), this.texWidth);
@@ -405,7 +457,8 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
405
457
  this.$l.j = pb.compAnd(this.t, 3);
406
458
  this.$l.weight = morphInfo.at(pb.add(1, this.i)).at(this.j);
407
459
  this.$if(pb.notEqual(this.weight, 0), function() {
408
- this.$l.pixelIndex = pb.float(pb.add(this.offset, pb.mul(this.t, this.numVertices), this.vertexIndex));
460
+ this.$l.targetIndex = pb.int(morphInfo.at(pb.add(1 + MORPH_WEIGHTS_VECTOR_COUNT, this.i)).at(this.j));
461
+ this.$l.pixelIndex = pb.float(pb.add(this.offset, pb.mul(this.targetIndex, this.numVertices), this.vertexIndex));
409
462
  this.$l.xIndex = pb.mod(this.pixelIndex, this.texWidth);
410
463
  this.$l.yIndex = pb.floor(pb.div(this.pixelIndex, this.texWidth));
411
464
  this.$l.u = pb.div(pb.add(this.xIndex, 0.5), this.texWidth);
@@ -417,17 +470,166 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
417
470
  }
418
471
  this.$return(this.value);
419
472
  });
420
- const pos = 1 + MORPH_WEIGHTS_VECTOR_COUNT + (attrib >> 2);
473
+ const pos = 1 + MORPH_WEIGHTS_VECTOR_COUNT * 2 + (attrib >> 2);
421
474
  const comp = attrib & 3;
422
475
  const offset = scope[this.getMorphInfoUniformName()][pos][comp];
423
476
  return scope[funcName](pb.int(offset));
424
477
  }
478
+ /**
479
+ * Normalize a morphed direction without allowing cancellation between
480
+ * multiple morph targets to turn a valid base direction into NaN/noise.
481
+ *
482
+ * Morph normal/tangent data is additive. When two targets cancel the base
483
+ * direction, the zero-length result is not a meaningful direction, so the
484
+ * authored vertex direction is the least surprising fallback.
485
+ * @internal
486
+ */ static defineNormalizeMorphedDirection(scope) {
487
+ const pb = scope.$builder;
488
+ const funcName = 'Z_normalizeMorphedDirection';
489
+ pb.func(funcName, [
490
+ pb.vec3('base'),
491
+ pb.vec3('delta')
492
+ ], function() {
493
+ this.$l.morphed = pb.add(this.base, this.delta);
494
+ this.$l.lengthSquared = pb.dot(this.morphed, this.morphed);
495
+ this.$if(pb.greaterThan(this.lengthSquared, 1e-6), function() {
496
+ this.$return(pb.mul(this.morphed, pb.inverseSqrt(this.lengthSquared)));
497
+ });
498
+ this.$l.baseLengthSquared = pb.dot(this.base, this.base);
499
+ this.$if(pb.greaterThan(this.baseLengthSquared, 1e-6), function() {
500
+ this.$return(pb.mul(this.base, pb.inverseSqrt(this.baseLengthSquared)));
501
+ });
502
+ this.$return(pb.vec3(0, 0, 1));
503
+ });
504
+ return funcName;
505
+ }
506
+ /**
507
+ * Mixes morph directions according to the amount of positional deformation
508
+ * contributed by each active target. This avoids letting a target with a
509
+ * large authored normal delta dominate a target that actually moves the
510
+ * surface much farther at the current vertex.
511
+ *
512
+ * The additive result remains the fallback for assets without position
513
+ * morph data, and for vertices whose position deltas are all zero.
514
+ * @internal
515
+ */ static calculateMorphDirectionByDisplacement(scope, base, attrib) {
516
+ const pb = scope.$builder;
517
+ const isWebGL1 = pb.getDevice().type === 'webgl';
518
+ if (pb.shaderKind !== 'vertex') {
519
+ throw new Error(`ShaderHelper.calculateMorphDirectionByDisplacement(): must be called at vertex stage`);
520
+ }
521
+ const that = this;
522
+ const funcName = 'Z_calculateMorphDirectionByDisplacement';
523
+ const normalizeName = this.defineNormalizeMorphedDirection(scope);
524
+ pb.func(funcName, [
525
+ pb.vec3('base'),
526
+ pb.int('directionOffset'),
527
+ pb.int('positionOffset')
528
+ ], function() {
529
+ this.$l.vertexIndex = isWebGL1 ? pb.int(scope.$inputs.zFakeVertexID) : pb.int(scope.$builtins.vertexIndex);
530
+ const morphInfo = scope[that.getMorphInfoUniformName()];
531
+ this.$l.metaData = pb.ivec4(morphInfo[0]);
532
+ this.$l.texWidth = pb.float(this.metaData.x);
533
+ this.$l.texHeight = pb.float(this.metaData.y);
534
+ this.$l.numVertices = this.metaData.z;
535
+ this.$l.numTargets = this.metaData.w;
536
+ this.$l.directionDelta = pb.vec3(0);
537
+ this.$l.weightedDirection = pb.vec3(0);
538
+ this.$l.weightSum = pb.float(0);
539
+ const accumulateTarget = (targetScope)=>{
540
+ targetScope.$if(pb.notEqual(targetScope.weight, 0), function() {
541
+ this.$l.targetIndex = pb.int(targetScope['targetIndex']);
542
+ this.$l.positionValue = pb.vec3(0);
543
+ this.$l.directionValue = pb.vec3(0);
544
+ this.$if(pb.greaterThanEqual(targetScope['directionOffset'], 0), function() {
545
+ this.$l.pixelIndex = pb.float(pb.add(targetScope['directionOffset'], pb.mul(this.targetIndex, targetScope['numVertices']), targetScope['vertexIndex']));
546
+ this.$l.xIndex = pb.mod(this.pixelIndex, targetScope['texWidth']);
547
+ this.$l.yIndex = pb.floor(pb.div(this.pixelIndex, targetScope['texWidth']));
548
+ this.$l.u = pb.div(pb.add(this.xIndex, 0.5), targetScope['texWidth']);
549
+ this.$l.v = pb.div(pb.add(this.yIndex, 0.5), targetScope['texHeight']);
550
+ this.directionValue = pb.textureSampleLevel(scope[that.getMorphDataUniformName()], pb.vec2(this.u, this.v), 0).xyz;
551
+ });
552
+ this.$if(pb.greaterThanEqual(targetScope['positionOffset'], 0), function() {
553
+ this.$l.pixelIndex = pb.float(pb.add(targetScope['positionOffset'], pb.mul(this.targetIndex, targetScope['numVertices']), targetScope['vertexIndex']));
554
+ this.$l.xIndex = pb.mod(this.pixelIndex, targetScope['texWidth']);
555
+ this.$l.yIndex = pb.floor(pb.div(this.pixelIndex, targetScope['texWidth']));
556
+ this.$l.u = pb.div(pb.add(this.xIndex, 0.5), targetScope['texWidth']);
557
+ this.$l.v = pb.div(pb.add(this.yIndex, 0.5), targetScope['texHeight']);
558
+ this.positionValue = pb.textureSampleLevel(scope[that.getMorphDataUniformName()], pb.vec2(this.u, this.v), 0).xyz;
559
+ });
560
+ this.directionDelta = pb.add(this.directionDelta, pb.mul(this.directionValue, targetScope.weight));
561
+ this.$l.targetDirection = scope.$g[normalizeName](this.base, pb.mul(this.directionValue, targetScope.weight));
562
+ this.$l.displacementWeight = pb.mul(pb.abs(targetScope.weight), pb.sqrt(pb.dot(this.positionValue, this.positionValue)));
563
+ this.weightedDirection = pb.add(this.weightedDirection, pb.mul(this.targetDirection, this.displacementWeight));
564
+ this.weightSum = pb.add(this.weightSum, this.displacementWeight);
565
+ });
566
+ };
567
+ if (isWebGL1) {
568
+ this.$for(pb.int('i'), 0, MORPH_WEIGHTS_VECTOR_COUNT, function() {
569
+ this.$for(pb.int('j'), 0, 4, function() {
570
+ this.$l.index = pb.add(pb.mul(this.i, 4), this.j);
571
+ this.$if(pb.greaterThanEqual(this.index, this.numTargets), function() {
572
+ this.$break();
573
+ });
574
+ this.$l.weight = morphInfo.at(pb.add(1, this.i)).at(this.j);
575
+ this.$l.targetIndex = pb.int(morphInfo.at(pb.add(1 + MORPH_WEIGHTS_VECTOR_COUNT, this.i)).at(this.j));
576
+ accumulateTarget(this);
577
+ });
578
+ });
579
+ } else {
580
+ this.$for(pb.int('t'), 0, this.numTargets, function() {
581
+ this.$l.i = pb.sar(this.t, 2);
582
+ this.$l.j = pb.compAnd(this.t, 3);
583
+ this.$l.weight = morphInfo.at(pb.add(1, this.i)).at(this.j);
584
+ this.$l.targetIndex = pb.int(morphInfo.at(pb.add(1 + MORPH_WEIGHTS_VECTOR_COUNT, this.i)).at(this.j));
585
+ accumulateTarget(this);
586
+ });
587
+ }
588
+ this.$l.fallback = scope.$g[normalizeName](this.base, this.directionDelta);
589
+ this.$if(pb.lessThan(this.weightSum, 1e-6), function() {
590
+ this.$return(this.fallback);
591
+ });
592
+ this.$l.weightedLengthSquared = pb.dot(this.weightedDirection, this.weightedDirection);
593
+ this.$if(pb.greaterThan(this.weightedLengthSquared, 1e-6), function() {
594
+ this.$return(pb.mul(this.weightedDirection, pb.inverseSqrt(this.weightedLengthSquared)));
595
+ });
596
+ this.$return(this.fallback);
597
+ });
598
+ const directionPos = 1 + MORPH_WEIGHTS_VECTOR_COUNT * 2 + (attrib >> 2);
599
+ const directionComp = attrib & 3;
600
+ const positionPos = 1 + MORPH_WEIGHTS_VECTOR_COUNT * 2 + (MORPH_TARGET_POSITION >> 2);
601
+ const positionComp = MORPH_TARGET_POSITION & 3;
602
+ const morphInfo = scope[this.getMorphInfoUniformName()];
603
+ return scope.$g[funcName](base.xyz, pb.int(morphInfo[directionPos][directionComp]), pb.int(morphInfo[positionPos][positionComp]));
604
+ }
605
+ /** @internal */ static orthogonalizeMorphedTangent(scope, normal, tangent) {
606
+ const pb = scope.$builder;
607
+ const funcName = 'Z_orthogonalizeMorphedTangent';
608
+ pb.func(funcName, [
609
+ pb.vec3('normal'),
610
+ pb.vec3('tangent')
611
+ ], function() {
612
+ this.$l.normalLengthSquared = pb.dot(this.normal, this.normal);
613
+ this.$if(pb.greaterThan(this.normalLengthSquared, 1e-6), function() {
614
+ this.$l.n = pb.mul(this.normal, pb.inverseSqrt(this.normalLengthSquared));
615
+ this.$l.projected = pb.sub(this.tangent, pb.mul(this.n, pb.dot(this.n, this.tangent)));
616
+ this.$l.tangentLengthSquared = pb.dot(this.projected, this.projected);
617
+ this.$if(pb.greaterThan(this.tangentLengthSquared, 1e-6), function() {
618
+ this.$return(pb.mul(this.projected, pb.inverseSqrt(this.tangentLengthSquared)));
619
+ });
620
+ });
621
+ this.$return(this.tangent);
622
+ });
623
+ return scope.$g[funcName](normal.xyz, tangent.xyz);
624
+ }
425
625
  /** @internal */ static prepareSkinAnimation(scope) {
426
626
  if (!this.hasSkinning(scope)) {
427
627
  return;
428
628
  }
429
629
  const that = this;
430
630
  const pb = scope.$builder;
631
+ const isWebGL = pb.getDevice().type === 'webgl';
632
+ const supportsTextureLoad = !isWebGL;
431
633
  const funcNameGetBoneMatrixFromTexture = 'Z_getBoneMatrixFromTexture';
432
634
  pb.func(funcNameGetBoneMatrixFromTexture, [
433
635
  pb.float('boneIndex'),
@@ -461,6 +663,33 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
461
663
  this.$l.m2 = scope.$g[funcNameGetBoneMatrixFromTexture](blendIndices[2], this.boneOffset);
462
664
  this.$l.m3 = scope.$g[funcNameGetBoneMatrixFromTexture](blendIndices[3], this.boneOffset);
463
665
  this.$l.m = pb.add(pb.mul(this.m0, blendWeights.x), pb.mul(this.m1, blendWeights.y), pb.mul(this.m2, blendWeights.z), pb.mul(this.m3, blendWeights.w));
666
+ this.$l.skinInfo = this[UNIFORM_NAME_SKIN_INFLUENCE_INFO];
667
+ this.$if(pb.greaterThan(this.skinInfo.z, 4), function() {
668
+ this.$l.vertexIndex = isWebGL ? pb.int(scope.$inputs.zFakeVertexID) : pb.int(scope.$builtins.vertexIndex);
669
+ this.$l.texWidth = pb.int(this.skinInfo.x);
670
+ this.$l.pairCount = pb.int(this.skinInfo.w);
671
+ this.$for(pb.int('pairIndex'), 0, MAX_SKIN_EXTRA_INFLUENCE_PAIRS, function() {
672
+ this.$if(pb.greaterThanEqual(this.pairIndex, this.pairCount), function() {
673
+ this.$break();
674
+ });
675
+ this.$l.pixelIndex = pb.add(pb.mul(this.vertexIndex, this.pairCount), this.pairIndex);
676
+ this.$l.xIndex = pb.mod(this.pixelIndex, this.texWidth);
677
+ this.$l.yIndex = pb.div(this.pixelIndex, this.texWidth);
678
+ if (supportsTextureLoad) {
679
+ this.$l.extra = pb.textureLoad(this[UNIFORM_NAME_SKIN_INFLUENCE_DATA], pb.ivec2(this.xIndex, this.yIndex), 0);
680
+ } else {
681
+ this.$l.u = pb.div(pb.add(pb.float(this.xIndex), 0.5), this.skinInfo.x);
682
+ this.$l.v = pb.div(pb.add(pb.float(this.yIndex), 0.5), this.skinInfo.y);
683
+ this.$l.extra = pb.textureSampleLevel(this[UNIFORM_NAME_SKIN_INFLUENCE_DATA], pb.vec2(this.u, this.v), 0);
684
+ }
685
+ this.$if(pb.greaterThan(this.extra.y, 0), function() {
686
+ this.m = pb.add(this.m, pb.mul(scope.$g[funcNameGetBoneMatrixFromTexture](this.extra.x, this.boneOffset), this.extra.y));
687
+ });
688
+ this.$if(pb.greaterThan(this.extra.w, 0), function() {
689
+ this.m = pb.add(this.m, pb.mul(scope.$g[funcNameGetBoneMatrixFromTexture](this.extra.z, this.boneOffset), this.extra.w));
690
+ });
691
+ });
692
+ });
464
693
  this.$return(pb.mul(invBindMatrix, this.m));
465
694
  });
466
695
  const motionVector = !!this.getUnjitteredViewProjectionMatrix(scope);
@@ -535,6 +764,19 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
535
764
  }
536
765
  }
537
766
  /**
767
+ * Gets the skinning matrix in the vertex shader, if the mesh being drawn is skinned.
768
+ *
769
+ * @remarks
770
+ * Lets a material apply the same skinning transform to its own uniform-supplied
771
+ * object-space vectors that {@link ShaderHelper.resolveVertexNormal} applies to
772
+ * the normal attribute, so such vectors stay attached to the deforming mesh.
773
+ *
774
+ * @param scope - Current shader scope, must be vertex stage
775
+ * @returns The skinning matrix of type mat4, or null when the mesh is not skinned
776
+ */ static getSkinMatrix(scope) {
777
+ return scope[this.SKIN_MATRIX_NAME] ?? null;
778
+ }
779
+ /**
538
780
  * Calculates the normal vector of type vec3 in object space
539
781
  *
540
782
  * @param scope - Current shader scope
@@ -550,6 +792,10 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
550
792
  if (!funcScope || !funcScope.$isMain()) {
551
793
  throw new Error(`ShaderHelper.resolveVertexNormal(): must be called at entry function`);
552
794
  }
795
+ const useVertexAttrib = !normal;
796
+ if (useVertexAttrib && scope[this.RESOLVED_VERTEX_NORMAL_NAME]) {
797
+ return scope[this.RESOLVED_VERTEX_NORMAL_NAME];
798
+ }
553
799
  if (!normal) {
554
800
  if (!scope.$getVertexAttrib('normal')) {
555
801
  scope.$inputs.Z_normal = pb.vec3().attrib('normal');
@@ -557,13 +803,17 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
557
803
  normal = scope.$getVertexAttrib('normal');
558
804
  }
559
805
  if (this.hasMorphing(scope)) {
560
- normal = pb.normalize(pb.add(normal, this.calculateMorphDelta(scope, MORPH_TARGET_NORMAL).xyz));
806
+ normal = this.calculateMorphDirectionByDisplacement(scope, normal, MORPH_TARGET_NORMAL);
561
807
  }
808
+ let resolvedNormal = normal;
562
809
  if (scope[this.SKIN_MATRIX_NAME]) {
563
- return pb.mul(scope[this.SKIN_MATRIX_NAME], pb.vec4(normal, 0)).xyz;
564
- } else {
565
- return normal;
810
+ resolvedNormal = pb.mul(scope[this.SKIN_MATRIX_NAME], pb.vec4(normal, 0)).xyz;
811
+ }
812
+ if (useVertexAttrib) {
813
+ scope.$l[this.RESOLVED_VERTEX_NORMAL_NAME] = resolvedNormal;
814
+ resolvedNormal = scope[this.RESOLVED_VERTEX_NORMAL_NAME];
566
815
  }
816
+ return resolvedNormal;
567
817
  }
568
818
  /**
569
819
  * Calculates the tangent vector of type vec3 in object space
@@ -588,13 +838,16 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
588
838
  tangent = scope.$getVertexAttrib('tangent');
589
839
  }
590
840
  if (this.hasMorphing(scope)) {
591
- tangent = pb.normalize(pb.add(tangent, pb.vec4(this.calculateMorphDelta(scope, MORPH_TARGET_TANGENT).xyz, 0)));
841
+ tangent = pb.vec4(this.calculateMorphDirectionByDisplacement(scope, tangent.xyz, MORPH_TARGET_TANGENT), tangent.w);
592
842
  }
593
843
  if (scope[this.SKIN_MATRIX_NAME]) {
594
- return pb.vec4(pb.mul(scope[this.SKIN_MATRIX_NAME], pb.vec4(tangent.xyz, 0)).xyz, tangent.w);
595
- } else {
596
- return tangent;
844
+ tangent = pb.vec4(pb.mul(scope[this.SKIN_MATRIX_NAME], pb.vec4(tangent.xyz, 0)).xyz, tangent.w);
597
845
  }
846
+ if (this.hasMorphing(scope) && scope.$getVertexAttrib('normal')) {
847
+ const resolvedNormal = scope[this.RESOLVED_VERTEX_NORMAL_NAME] ?? this.resolveVertexNormal(scope);
848
+ tangent = pb.vec4(this.orthogonalizeMorphedTangent(scope, resolvedNormal, tangent.xyz), tangent.w);
849
+ }
850
+ return tangent;
598
851
  }
599
852
  /**
600
853
  * Gets the uniform variable of type mat4 which holds the world matrix of current object to be drawn
@@ -676,10 +929,12 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
676
929
  scope[UNIFORM_NAME_BONE_MATRICES] = pb.tex2D().uniform(1).sampleType('unfilterable-float');
677
930
  scope[UNIFORM_NAME_BONE_INV_BIND_MATRIX] = pb.mat4().uniform(1);
678
931
  scope[UNIFORM_NAME_BONE_TEXTURE_SIZE] = pb.vec2().uniform(1);
932
+ scope[UNIFORM_NAME_SKIN_INFLUENCE_DATA] = pb.tex2D().uniform(1).sampleType('unfilterable-float');
933
+ scope[UNIFORM_NAME_SKIN_INFLUENCE_INFO] = pb.vec4().uniform(1);
679
934
  }
680
935
  if (morphing) {
681
936
  scope[UNIFORM_NAME_MORPH_DATA] = pb.tex2D().uniform(1).sampleType('unfilterable-float');
682
- scope[UNIFORM_NAME_MORPH_INFO] = pb.vec4[1 + MORPH_WEIGHTS_VECTOR_COUNT + MORPH_ATTRIBUTE_VECTOR_COUNT]().uniformBuffer(1);
937
+ scope[UNIFORM_NAME_MORPH_INFO] = pb.vec4[1 + MORPH_WEIGHTS_VECTOR_COUNT * 2 + MORPH_ATTRIBUTE_VECTOR_COUNT]().uniformBuffer(1);
683
938
  }
684
939
  }
685
940
  /** @internal */ static prepareVertexShaderCommon(pb, ctx) {
@@ -735,8 +990,10 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
735
990
  directionCutoff: light.directionAndCutoff,
736
991
  viewMatrix: light.viewMatrix,
737
992
  depthBias: shadowMapParams.depthBiasValues[0],
993
+ implParams: shadowMapParams.impl.getParams(this._lightUniformShadow.implParams),
738
994
  lightType: light.lightType
739
995
  });
996
+ shadowMapParams.impl?.applyCasterUniforms(bindGroup, shadowMapParams);
740
997
  }
741
998
  }
742
999
  /** @internal */ static setFogUniforms(bindGroup, withAerialPerspective, fogType, additive, atmosphereParams, heightFogParams, aerialPerspectiveLUT, skyDistantLightLUT) {
@@ -1291,18 +1548,67 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
1291
1548
  return scope[UNIFORM_NAME_SHADOW_MAP];
1292
1549
  }
1293
1550
  /**
1551
+ * Per-cascade scale for both bias terms.
1552
+ *
1553
+ * @remarks
1554
+ * Only `depthBiasValues[0]` is uploaded; every cascade's value is recovered by
1555
+ * multiplying with this ratio. It is derived from the `.x` components, but
1556
+ * since `.x` and the normal offset `.y` are both proportional to the cascade's
1557
+ * texel world size, the same ratio applies to either.
1558
+ * @internal
1559
+ */ static getShadowCascadeBiasScale(scope, split) {
1560
+ const pb = scope.$builder;
1561
+ const splitFlags = pb.vec4(pb.float(pb.equal(split, 0)), pb.float(pb.equal(split, 1)), pb.float(pb.equal(split, 2)), pb.float(pb.equal(split, 3)));
1562
+ return pb.dot(this.getDepthBiasScales(scope), splitFlags);
1563
+ }
1564
+ /**
1565
+ * Moves the receiver along its normal before the shadow lookup.
1566
+ *
1567
+ * @remarks
1568
+ * Grazing-angle self-shadowing cannot be fixed with a depth bias: the depth
1569
+ * spanned by one shadow texel is `texelWorldSize * tan(theta)`, which diverges
1570
+ * as NdotL approaches zero. Offsetting the sample position sideways moves it
1571
+ * out of the offending texel instead, and the required distance is bounded.
1572
+ *
1573
+ * `depthBiasValues.y` carries the world-space distance of one `normalBias`
1574
+ * unit (see `ShadowMapper.calcDepthBiasParams`).
1575
+ * @internal
1576
+ */ static applyShadowNormalOffset(lightType, scope, worldPos, worldNormal, NdotL, cascadeScale) {
1577
+ const pb = scope.$builder;
1578
+ const depthBiasParam = this.getDepthBiasValues(scope);
1579
+ const receiverNoL = getShadowReceiverNoL(scope, NdotL, lightType);
1580
+ // sin(theta): 0 when facing the light, 1 at grazing incidence.
1581
+ const sinTheta = pb.sqrt(pb.clamp(pb.sub(1, pb.mul(receiverNoL, receiverNoL)), 0, 1));
1582
+ let offset = pb.mul(depthBiasParam.y, getShadowReceiverBiasFactor(scope), sinTheta);
1583
+ if (cascadeScale) {
1584
+ offset = pb.mul(offset, cascadeScale);
1585
+ }
1586
+ if (lightType !== LIGHT_TYPE_DIRECTIONAL) {
1587
+ // Perspective shadow cameras: one texel covers more world space further
1588
+ // from the light. depthBiasValues.w is the far/near footprint ratio.
1589
+ const posRange = this.getLightPositionAndRangeForShadow(scope);
1590
+ const linearDepth = pb.clamp(pb.div(pb.distance(worldPos, posRange.xyz), pb.max(posRange.w, 1e-6)), 0, 1);
1591
+ offset = pb.mul(offset, pb.mix(1, depthBiasParam.w, linearDepth));
1592
+ }
1593
+ // Guard against a degenerate interpolated normal rather than normalize()ing
1594
+ // a possibly-zero vector into NaN.
1595
+ const unitNormal = pb.mul(worldNormal, pb.div(1, pb.max(pb.length(worldNormal), 1e-6)));
1596
+ return pb.add(worldPos, pb.mul(unitNormal, offset));
1597
+ }
1598
+ /**
1294
1599
  * Calculates shadow of current fragment
1295
1600
  *
1296
1601
  * @param scope - Shader scope
1297
1602
  * @param NoL - NdotL vector
1298
1603
  * @returns Shadow of current fragment, 1 means no shadow and 0 means full shadowed.
1299
- */ static calculateShadow(scope, worldPos, NoL, ctx) {
1604
+ */ static calculateShadow(scope, worldPos, worldNormal, NoL, ctx) {
1300
1605
  const pb = scope.$builder;
1301
1606
  const that = this;
1302
1607
  const shadowMapParams = ctx.shadowMapInfo.get(ctx.currentShadowLight);
1303
1608
  const funcName = 'Z_calculateShadow';
1304
1609
  pb.func(funcName, [
1305
1610
  pb.vec3('worldPos'),
1611
+ pb.vec3('worldNormal'),
1306
1612
  pb.float('NoL')
1307
1613
  ], function() {
1308
1614
  if (shadowMapParams.numShadowCascades > 1) {
@@ -1313,18 +1619,21 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
1313
1619
  this.$l.comparison = pb.vec4(pb.greaterThan(pb.vec4(this.linearDepth), this.splitDistances));
1314
1620
  this.$l.cascadeFlags = pb.vec4(pb.float(pb.greaterThan(this.shadowCascades, 0)), pb.float(pb.greaterThan(this.shadowCascades, 1)), pb.float(pb.greaterThan(this.shadowCascades, 2)), pb.float(pb.greaterThan(this.shadowCascades, 3)));
1315
1621
  this.$l.split = pb.int(pb.dot(this.comparison, this.cascadeFlags));
1622
+ // Normal offset bias: move the receiver out of the shadow texel that
1623
+ // would self-occlude it before projecting into shadow space. Scaled per
1624
+ // cascade, because each cascade has its own texel world size.
1625
+ this.$l.biasedPos = that.applyShadowNormalOffset(shadowMapParams.lightType, this, this.worldPos, this.worldNormal, this.NoL, that.getShadowCascadeBiasScale(this, this.split));
1316
1626
  if (ctx.device.type === 'webgl') {
1317
1627
  this.$l.shadowVertex = pb.vec4();
1318
1628
  this.$for(pb.int('cascade'), 0, 4, function() {
1319
1629
  this.$if(pb.equal(this.cascade, this.split), function() {
1320
- this.shadowVertex = that.calculateShadowSpaceVertex(this, pb.vec4(this.worldPos, 1), this.cascade);
1630
+ this.shadowVertex = that.calculateShadowSpaceVertex(this, pb.vec4(this.biasedPos, 1), this.cascade);
1321
1631
  this.$break();
1322
1632
  });
1323
1633
  });
1324
1634
  } else {
1325
- this.$l.shadowVertex = that.calculateShadowSpaceVertex(this, pb.vec4(this.worldPos, 1), this.split);
1635
+ this.$l.shadowVertex = that.calculateShadowSpaceVertex(this, pb.vec4(this.biasedPos, 1), this.split);
1326
1636
  }
1327
- const shadowMapParams = ctx.shadowMapInfo.get(ctx.currentShadowLight);
1328
1637
  this.$l.shadow = shadowMapParams.impl.computeShadowCSM(shadowMapParams, this, this.shadowVertex, this.NoL, this.split);
1329
1638
  this.shadow = pb.clamp(this.shadow, 0, 1);
1330
1639
  this.$l.shadowDistance = that.getShadowCameraParams(scope).w;
@@ -1336,8 +1645,9 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
1336
1645
  });
1337
1646
  this.$return(this.shadow);
1338
1647
  } else {
1339
- this.$l.shadowVertex = that.calculateShadowSpaceVertex(this, pb.vec4(this.worldPos, 1));
1340
- const shadowMapParams = ctx.shadowMapInfo.get(ctx.currentShadowLight);
1648
+ // Normal offset bias - see the cascaded branch above.
1649
+ this.$l.biasedPos = that.applyShadowNormalOffset(shadowMapParams.lightType, this, this.worldPos, this.worldNormal, this.NoL);
1650
+ this.$l.shadowVertex = that.calculateShadowSpaceVertex(this, pb.vec4(this.biasedPos, 1));
1341
1651
  this.$l.shadow = shadowMapParams.impl.computeShadow(shadowMapParams, this, this.shadowVertex, this.NoL);
1342
1652
  this.shadow = pb.clamp(this.shadow, 0, 1);
1343
1653
  this.$l.shadowDistance = that.getShadowCameraParams(scope).w;
@@ -1347,7 +1657,7 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
1347
1657
  this.$return(this.shadow);
1348
1658
  }
1349
1659
  });
1350
- return pb.getGlobalScope()[funcName](worldPos, NoL);
1660
+ return pb.getGlobalScope()[funcName](worldPos, worldNormal, NoL);
1351
1661
  }
1352
1662
  static applyFog(scope, worldPos, color, ctx) {
1353
1663
  const pb = scope.$builder;