@babylonjs/lite 1.11.0 → 1.12.0

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 (98) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/index.js +444 -368
  3. package/dist/index.js.map +1 -1
  4. package/index.d.ts +191 -4
  5. package/lib/audio/index.js +1 -0
  6. package/lib/audio/index.js.map +1 -1
  7. package/lib/audio/media-stream-output.js +44 -0
  8. package/lib/audio/media-stream-output.js.map +1 -0
  9. package/lib/engine/engine.js +1 -1
  10. package/lib/frame-graph/render-task.js +1 -1
  11. package/lib/frame-graph/render-task.js.map +1 -1
  12. package/lib/frame-graph/shadow-inputs.js +10 -1
  13. package/lib/frame-graph/shadow-inputs.js.map +1 -1
  14. package/lib/frame-graph/transmission.js +1 -16
  15. package/lib/frame-graph/transmission.js.map +1 -1
  16. package/lib/index.js +5 -3
  17. package/lib/index.js.map +1 -1
  18. package/lib/loader-gltf/gltf-json-asset.js +1 -1
  19. package/lib/loader-gltf/gltf-json-asset.js.map +1 -1
  20. package/lib/loader-gltf/gltf-multi-buffer.js +2 -10
  21. package/lib/loader-gltf/gltf-multi-buffer.js.map +1 -1
  22. package/lib/loader-gltf/gltf-pbr-builder-ext.js +4 -18
  23. package/lib/loader-gltf/gltf-pbr-builder-ext.js.map +1 -1
  24. package/lib/loader-gltf/gltf-pbr-builder.js +17 -14
  25. package/lib/loader-gltf/gltf-pbr-builder.js.map +1 -1
  26. package/lib/loader-gltf/gltf-sampler-desc.js +3 -16
  27. package/lib/loader-gltf/gltf-sampler-desc.js.map +1 -1
  28. package/lib/material/node/blocks/ambient-occlusion-block.js +1 -4
  29. package/lib/material/node/blocks/ambient-occlusion-block.js.map +1 -1
  30. package/lib/material/node/blocks/biplanar-block.js +3 -6
  31. package/lib/material/node/blocks/biplanar-block.js.map +1 -1
  32. package/lib/material/node/blocks/image-source.js +2 -5
  33. package/lib/material/node/blocks/image-source.js.map +1 -1
  34. package/lib/material/node/blocks/input-block.js +5 -26
  35. package/lib/material/node/blocks/input-block.js.map +1 -1
  36. package/lib/material/node/blocks/pbr-mr-helper-full.js +5 -8
  37. package/lib/material/node/blocks/pbr-mr-helper-full.js.map +1 -1
  38. package/lib/material/node/blocks/reflection-texture-block.js +1 -4
  39. package/lib/material/node/blocks/reflection-texture-block.js.map +1 -1
  40. package/lib/material/node/blocks/texture-block.js +2 -5
  41. package/lib/material/node/blocks/texture-block.js.map +1 -1
  42. package/lib/material/node/blocks/triplanar-block.js +3 -6
  43. package/lib/material/node/blocks/triplanar-block.js.map +1 -1
  44. package/lib/material/node/node-emitter.js +24 -1
  45. package/lib/material/node/node-emitter.js.map +1 -1
  46. package/lib/material/node/node-geometry-renderable.js +2 -2
  47. package/lib/material/node/node-material.js +1 -22
  48. package/lib/material/node/node-material.js.map +1 -1
  49. package/lib/material/node/node-types.js.map +1 -1
  50. package/lib/material/pbr/fragments/shadow-only-fragment.js +68 -0
  51. package/lib/material/pbr/fragments/shadow-only-fragment.js.map +1 -0
  52. package/lib/material/pbr/pbr-flag-bits.js.map +1 -1
  53. package/lib/material/pbr/pbr-material.js.map +1 -1
  54. package/lib/material/pbr/pbr-renderable.js +3 -0
  55. package/lib/material/pbr/pbr-renderable.js.map +1 -1
  56. package/lib/material/pbr/pbr-template.js +1 -0
  57. package/lib/material/pbr/pbr-template.js.map +1 -1
  58. package/lib/mesh/GaussianSplatting/gaussian-splatting-mesh.js +2 -8
  59. package/lib/mesh/GaussianSplatting/gaussian-splatting-mesh.js.map +1 -1
  60. package/lib/mesh/mesh-dispose.js +2 -0
  61. package/lib/mesh/mesh-dispose.js.map +1 -1
  62. package/lib/mesh/mesh.js +2 -8
  63. package/lib/mesh/mesh.js.map +1 -1
  64. package/lib/mesh/thin-instance-cull-binding.js +54 -3
  65. package/lib/mesh/thin-instance-cull-binding.js.map +1 -1
  66. package/lib/mesh/thin-instance-gpu-culling.js +241 -24
  67. package/lib/mesh/thin-instance-gpu-culling.js.map +1 -1
  68. package/lib/mesh/thin-instance.js +112 -1
  69. package/lib/mesh/thin-instance.js.map +1 -1
  70. package/lib/picking/gpu-picker.js +4 -2
  71. package/lib/picking/gpu-picker.js.map +1 -1
  72. package/lib/picking/picking-pipeline.js +1 -1
  73. package/lib/picking/picking-pipeline.js.map +1 -1
  74. package/lib/picking/picking-shader.js +38 -16
  75. package/lib/picking/picking-shader.js.map +1 -1
  76. package/lib/scene/scene-core.js +3 -0
  77. package/lib/scene/scene-core.js.map +1 -1
  78. package/lib/scene/scene-node.js +2 -6
  79. package/lib/scene/scene-node.js.map +1 -1
  80. package/lib/scene/transform-node.js +3 -0
  81. package/lib/scene/transform-node.js.map +1 -1
  82. package/lib/scene/world-matrix-state.js +7 -1
  83. package/lib/scene/world-matrix-state.js.map +1 -1
  84. package/lib/shadow/csm-directional-shadow-generator.js +22 -1
  85. package/lib/shadow/csm-directional-shadow-generator.js.map +1 -1
  86. package/lib/shadow/csm-shadow-task-hooks.js +19 -11
  87. package/lib/shadow/csm-shadow-task-hooks.js.map +1 -1
  88. package/lib/shadow/esm-directional-shadow-generator.js +2 -51
  89. package/lib/shadow/esm-directional-shadow-generator.js.map +1 -1
  90. package/lib/shadow/pcf-directional-shadow-generator.js +2 -48
  91. package/lib/shadow/pcf-directional-shadow-generator.js.map +1 -1
  92. package/lib/shadow/shadow-base.js +48 -1
  93. package/lib/shadow/shadow-base.js.map +1 -1
  94. package/lib/text/text-renderable.js +2 -10
  95. package/lib/text/text-renderable.js.map +1 -1
  96. package/lib/texture/texture-array.js +96 -0
  97. package/lib/texture/texture-array.js.map +1 -0
  98. package/package.json +3 -3
@@ -3,6 +3,7 @@ const PICK_DISCARD_INPUT = (
3
3
  `
4
4
  struct PickDiscardInput {
5
5
  worldPos: vec3f,
6
+ fragmentCoord: vec2f,
6
7
  pickId: u32,
7
8
  thinInstanceIndex: u32,
8
9
  hasThinInstance: u32,
@@ -21,18 +22,39 @@ return false;
21
22
  function pickDiscardSource(opts) {
22
23
  return opts?.discardWgsl ?? DEFAULT_PICK_DISCARD;
23
24
  }
24
- function pickDiscardStorageDecls(opts) {
25
+ const DEFAULT_PICK_WORLD_ADJUST = (
26
+ /* wgsl */
27
+ `
28
+ fn adjustPickWorld(worldPos: vec3f, instanceExtras: vec4f, thinInstanceIndex: u32) -> vec3f {
29
+ return worldPos;
30
+ }
31
+ `
32
+ );
33
+ function pickWorldAdjustSource(opts) {
34
+ return opts?.worldAdjustWgsl ?? DEFAULT_PICK_WORLD_ADJUST;
35
+ }
36
+ function pickStorageDecls(opts) {
25
37
  const storage = opts?.storage;
26
38
  if (!storage || storage.length === 0) {
27
39
  return "";
28
40
  }
29
41
  return storage.map((s, binding) => `@group(2) @binding(${binding}) var<storage, read> ${s.name}: ${s.type};`).join("\n");
30
42
  }
43
+ const PICK_SCENE = (
44
+ /* wgsl */
45
+ `
46
+ struct SceneUniforms {
47
+ viewProjection: mat4x4f,
48
+ fragmentCoord: vec2f,
49
+ };
50
+ @group(0) @binding(0) var<uniform> scene: SceneUniforms;
51
+ `
52
+ );
31
53
  const PICK_FS = (
32
54
  /* wgsl */
33
55
  `
34
56
  struct VsOut {
35
- @builtin(position) position: vec4f,
57
+ @builtin(position) p: vec4f,
36
58
  @location(0) @interpolate(flat) pickId: u32,
37
59
  @location(1) worldPos: vec3f,
38
60
  @location(2) @interpolate(flat) thinInstanceIndex: u32,
@@ -41,13 +63,12 @@ struct VsOut {
41
63
  };
42
64
  struct FsOut { @location(0) color: vec4f, @location(1) depth: vec4f };
43
65
  @fragment fn fs(input: VsOut) -> FsOut {
44
- let discardInput = PickDiscardInput(input.worldPos, input.pickId, input.thinInstanceIndex, input.hasThinInstance, input.instanceExtras);
45
- if (shouldDiscardPick(discardInput)) { discard; }
66
+ if (shouldDiscardPick(PickDiscardInput(input.worldPos, scene.fragmentCoord, input.pickId, input.thinInstanceIndex, input.hasThinInstance, input.instanceExtras))) { discard; }
46
67
  let id = input.pickId;
47
68
  let r = f32((id >> 16u) & 0xFFu) / 255.0;
48
69
  let g = f32((id >> 8u) & 0xFFu) / 255.0;
49
70
  let b = f32(id & 0xFFu) / 255.0;
50
- return FsOut(vec4f(r, g, b, 1.0), vec4f(input.position.z, 0.0, 0.0, 0.0));
71
+ return FsOut(vec4f(r, g, b, 1.0), vec4f(input.p.z, 0.0, 0.0, 0.0));
51
72
  }
52
73
  `
53
74
  );
@@ -55,21 +76,21 @@ function pickingShaderSource(opts) {
55
76
  return (
56
77
  /* wgsl */
57
78
  `
58
- struct SceneUniforms { viewProjection: mat4x4f };
79
+ ${PICK_SCENE}
59
80
  struct MeshUniforms {
60
81
  world: mat4x4f,
61
82
  pickId: u32,
62
83
  };
63
- @group(0) @binding(0) var<uniform> scene: SceneUniforms;
64
84
  @group(1) @binding(0) var<uniform> mesh: MeshUniforms;
65
85
  ${PICK_DISCARD_INPUT}
66
- ${pickDiscardStorageDecls(opts)}
86
+ ${pickStorageDecls(opts)}
67
87
  ${pickDiscardSource(opts)}
88
+ ${pickWorldAdjustSource(opts)}
68
89
  ${PICK_FS}
69
90
  @vertex fn vs(@location(0) position: vec3f) -> VsOut {
70
91
  var out: VsOut;
71
- let wp = (mesh.world * vec4f(position, 1.0)).xyz;
72
- out.position = scene.viewProjection * vec4f(wp, 1.0);
92
+ let wp = adjustPickWorld((mesh.world * vec4f(position, 1.0)).xyz, vec4f(0.0), 0xffffffffu);
93
+ out.p = scene.viewProjection * vec4f(wp, 1.0);
73
94
  out.pickId = mesh.pickId;
74
95
  out.worldPos = wp;
75
96
  out.thinInstanceIndex = 0xffffffffu;
@@ -84,16 +105,16 @@ function pickingThinInstanceShaderSource(opts) {
84
105
  return (
85
106
  /* wgsl */
86
107
  `
87
- struct SceneUniforms { viewProjection: mat4x4f };
108
+ ${PICK_SCENE}
88
109
  struct TIMeshUniforms {
89
110
  baseMeshPickId: u32,
90
111
  };
91
- @group(0) @binding(0) var<uniform> scene: SceneUniforms;
92
112
  @group(1) @binding(0) var<uniform> tiMesh: TIMeshUniforms;
93
113
  @group(1) @binding(1) var<storage, read> instances: array<mat4x4f>;
94
114
  ${PICK_DISCARD_INPUT}
95
- ${pickDiscardStorageDecls(opts)}
115
+ ${pickStorageDecls(opts)}
96
116
  ${pickDiscardSource(opts)}
117
+ ${pickWorldAdjustSource(opts)}
97
118
  ${PICK_FS}
98
119
  @vertex fn vs(@location(0) position: vec3f, @builtin(instance_index) instanceIndex: u32) -> VsOut {
99
120
  let m = instances[instanceIndex];
@@ -107,14 +128,15 @@ vec4f(m[1].xyz, 0.0),
107
128
  vec4f(m[2].xyz, 0.0),
108
129
  vec4f(m[3].xyz, 1.0),
109
130
  );
131
+ let extras = vec4f(m[0].w, m[1].w, m[2].w, m[3].w);
110
132
  var out: VsOut;
111
- let wp = (world * vec4f(position, 1.0)).xyz;
112
- out.position = scene.viewProjection * vec4f(wp, 1.0);
133
+ let wp = adjustPickWorld((world * vec4f(position, 1.0)).xyz, extras, instanceIndex);
134
+ out.p = scene.viewProjection * vec4f(wp, 1.0);
113
135
  out.pickId = tiMesh.baseMeshPickId + instanceIndex;
114
136
  out.worldPos = wp;
115
137
  out.thinInstanceIndex = instanceIndex;
116
138
  out.hasThinInstance = 1u;
117
- out.instanceExtras = vec4f(m[0].w, m[1].w, m[2].w, m[3].w);
139
+ out.instanceExtras = extras;
118
140
  return out;
119
141
  }
120
142
  `
@@ -1 +1 @@
1
- {"version":3,"file":"picking-shader.js","sources":["../../../src/picking/picking-shader.ts"],"sourcesContent":["/** WGSL shaders for GPU pick-ID rendering.\n * Outputs pick ID as rgba8unorm (location 0) and depth as r32float (location 1). */\n\nexport interface PickingShaderOptions {\n readonly discardWgsl?: string | null;\n readonly storage?: readonly { readonly name: string; readonly type: string }[];\n}\n\nconst PICK_DISCARD_INPUT = /* wgsl */ `\nstruct PickDiscardInput {\nworldPos: vec3f,\npickId: u32,\nthinInstanceIndex: u32,\nhasThinInstance: u32,\ninstanceExtras: vec4f,\n};\n`;\n\nconst DEFAULT_PICK_DISCARD = /* wgsl */ `\nfn shouldDiscardPick(input: PickDiscardInput) -> bool {\nreturn false;\n}\n`;\n\nfunction pickDiscardSource(opts?: PickingShaderOptions): string {\n return opts?.discardWgsl ?? DEFAULT_PICK_DISCARD;\n}\n\nfunction pickDiscardStorageDecls(opts?: PickingShaderOptions): string {\n const storage = opts?.storage;\n if (!storage || storage.length === 0) {\n return \"\";\n }\n return storage.map((s, binding) => `@group(2) @binding(${binding}) var<storage, read> ${s.name}: ${s.type};`).join(\"\\n\");\n}\n\n// ─── Shared structs + fragment shader ───────────────────────────────\n\nconst PICK_FS = /* wgsl */ `\nstruct VsOut {\n@builtin(position) position: vec4f,\n@location(0) @interpolate(flat) pickId: u32,\n@location(1) worldPos: vec3f,\n@location(2) @interpolate(flat) thinInstanceIndex: u32,\n@location(3) @interpolate(flat) hasThinInstance: u32,\n@location(4) @interpolate(flat) instanceExtras: vec4f,\n};\nstruct FsOut { @location(0) color: vec4f, @location(1) depth: vec4f };\n@fragment fn fs(input: VsOut) -> FsOut {\nlet discardInput = PickDiscardInput(input.worldPos, input.pickId, input.thinInstanceIndex, input.hasThinInstance, input.instanceExtras);\nif (shouldDiscardPick(discardInput)) { discard; }\nlet id = input.pickId;\nlet r = f32((id >> 16u) & 0xFFu) / 255.0;\nlet g = f32((id >> 8u) & 0xFFu) / 255.0;\nlet b = f32(id & 0xFFu) / 255.0;\nreturn FsOut(vec4f(r, g, b, 1.0), vec4f(input.position.z, 0.0, 0.0, 0.0));\n}\n`;\n\n// ─── Regular mesh picking shader ────────────────────────────────────\n\nexport function pickingShaderSource(opts?: PickingShaderOptions): string {\n return /* wgsl */ `\nstruct SceneUniforms { viewProjection: mat4x4f };\nstruct MeshUniforms {\nworld: mat4x4f,\npickId: u32,\n};\n@group(0) @binding(0) var<uniform> scene: SceneUniforms;\n@group(1) @binding(0) var<uniform> mesh: MeshUniforms;\n${PICK_DISCARD_INPUT}\n${pickDiscardStorageDecls(opts)}\n${pickDiscardSource(opts)}\n${PICK_FS}\n@vertex fn vs(@location(0) position: vec3f) -> VsOut {\nvar out: VsOut;\nlet wp = (mesh.world * vec4f(position, 1.0)).xyz;\nout.position = scene.viewProjection * vec4f(wp, 1.0);\nout.pickId = mesh.pickId;\nout.worldPos = wp;\nout.thinInstanceIndex = 0xffffffffu;\nout.hasThinInstance = 0u;\nout.instanceExtras = vec4f(0.0);\nreturn out;\n}\n`;\n}\n\n// ─── Thin-instance picking shader ───────────────────────────────────\n\nexport function pickingThinInstanceShaderSource(opts?: PickingShaderOptions): string {\n return /* wgsl */ `\nstruct SceneUniforms { viewProjection: mat4x4f };\nstruct TIMeshUniforms {\nbaseMeshPickId: u32,\n};\n@group(0) @binding(0) var<uniform> scene: SceneUniforms;\n@group(1) @binding(0) var<uniform> tiMesh: TIMeshUniforms;\n@group(1) @binding(1) var<storage, read> instances: array<mat4x4f>;\n${PICK_DISCARD_INPUT}\n${pickDiscardStorageDecls(opts)}\n${pickDiscardSource(opts)}\n${PICK_FS}\n@vertex fn vs(@location(0) position: vec3f, @builtin(instance_index) instanceIndex: u32) -> VsOut {\nlet m = instances[instanceIndex];\n// Treat the instance placement as an AFFINE transform: force the basis columns' homogeneous w to 0 and the\n// translation column's w to 1. Thin-instanced ShaderMaterials may pack per-instance data in those spare w\n// lanes (a sanctioned pattern — Lite injects world0..world3 and the app's own vertex shader zeroes them\n// before transforming). Picking only needs the transform, so packed values are exposed separately.\nlet world = mat4x4f(\nvec4f(m[0].xyz, 0.0),\nvec4f(m[1].xyz, 0.0),\nvec4f(m[2].xyz, 0.0),\nvec4f(m[3].xyz, 1.0),\n);\nvar out: VsOut;\nlet wp = (world * vec4f(position, 1.0)).xyz;\nout.position = scene.viewProjection * vec4f(wp, 1.0);\nout.pickId = tiMesh.baseMeshPickId + instanceIndex;\nout.worldPos = wp;\nout.thinInstanceIndex = instanceIndex;\nout.hasThinInstance = 1u;\nout.instanceExtras = vec4f(m[0].w, m[1].w, m[2].w, m[3].w);\nreturn out;\n}\n`;\n}\n"],"names":[],"mappings":"AAQA,MAAM,kBAAA;AAAA;AAAA,EAAgC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAUtC,MAAM,oBAAA;AAAA;AAAA,EAAkC;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAMxC,SAAS,kBAAkB,IAAA,EAAqC;AAC5D,EAAA,OAAO,MAAM,WAAA,IAAe,oBAAA;AAChC;AAEA,SAAS,wBAAwB,IAAA,EAAqC;AAClE,EAAA,MAAM,UAAU,IAAA,EAAM,OAAA;AACtB,EAAA,IAAI,CAAC,OAAA,IAAW,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG;AAClC,IAAA,OAAO,EAAA;AAAA,EACX;AACA,EAAA,OAAO,QAAQ,GAAA,CAAI,CAAC,CAAA,EAAG,OAAA,KAAY,sBAAsB,OAAO,CAAA,qBAAA,EAAwB,CAAA,CAAE,IAAI,KAAK,CAAA,CAAE,IAAI,CAAA,CAAA,CAAG,CAAA,CAAE,KAAK,IAAI,CAAA;AAC3H;AAIA,MAAM,OAAA;AAAA;AAAA,EAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAuBpB,SAAS,oBAAoB,IAAA,EAAqC;AACrE,EAAA;AAAA;AAAA,IAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQpB,kBAAkB;AAAA,EAClB,uBAAA,CAAwB,IAAI,CAAC;AAAA,EAC7B,iBAAA,CAAkB,IAAI,CAAC;AAAA,EACvB,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaT;AAIO,SAAS,gCAAgC,IAAA,EAAqC;AACjF,EAAA;AAAA;AAAA,IAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQpB,kBAAkB;AAAA,EAClB,uBAAA,CAAwB,IAAI,CAAC;AAAA,EAC7B,iBAAA,CAAkB,IAAI,CAAC;AAAA,EACvB,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwBT;;;;"}
1
+ {"version":3,"file":"picking-shader.js","sources":["../../../src/picking/picking-shader.ts"],"sourcesContent":["/** WGSL shaders for GPU pick-ID rendering.\n * Outputs pick ID as rgba8unorm (location 0) and depth as r32float (location 1). */\n\nexport interface PickingShaderOptions {\n readonly discardWgsl?: string | null;\n /** Optional WGSL replacing `adjustPickWorld` (default: identity). Internal pick pipelines can mirror\n * world-space vertex displacement using the declared storage, thin-instance index, and spare matrix w lanes. */\n readonly worldAdjustWgsl?: string | null;\n readonly storage?: readonly { readonly name: string; readonly type: string }[];\n}\n\nconst PICK_DISCARD_INPUT = /* wgsl */ `\nstruct PickDiscardInput {\nworldPos: vec3f,\nfragmentCoord: vec2f,\npickId: u32,\nthinInstanceIndex: u32,\nhasThinInstance: u32,\ninstanceExtras: vec4f,\n};\n`;\n\nconst DEFAULT_PICK_DISCARD = /* wgsl */ `\nfn shouldDiscardPick(input: PickDiscardInput) -> bool {\nreturn false;\n}\n`;\n\nfunction pickDiscardSource(opts?: PickingShaderOptions): string {\n return opts?.discardWgsl ?? DEFAULT_PICK_DISCARD;\n}\n\nconst DEFAULT_PICK_WORLD_ADJUST = /* wgsl */ `\nfn adjustPickWorld(worldPos: vec3f, instanceExtras: vec4f, thinInstanceIndex: u32) -> vec3f {\nreturn worldPos;\n}\n`;\n\nfunction pickWorldAdjustSource(opts?: PickingShaderOptions): string {\n return opts?.worldAdjustWgsl ?? DEFAULT_PICK_WORLD_ADJUST;\n}\n\nfunction pickStorageDecls(opts?: PickingShaderOptions): string {\n const storage = opts?.storage;\n if (!storage || storage.length === 0) {\n return \"\";\n }\n return storage.map((s, binding) => `@group(2) @binding(${binding}) var<storage, read> ${s.name}: ${s.type};`).join(\"\\n\");\n}\n\n// ─── Shared structs + fragment shader ───────────────────────────────\n\nconst PICK_SCENE = /* wgsl */ `\nstruct SceneUniforms {\nviewProjection: mat4x4f,\nfragmentCoord: vec2f,\n};\n@group(0) @binding(0) var<uniform> scene: SceneUniforms;\n`;\n\nconst PICK_FS = /* wgsl */ `\nstruct VsOut {\n@builtin(position) p: vec4f,\n@location(0) @interpolate(flat) pickId: u32,\n@location(1) worldPos: vec3f,\n@location(2) @interpolate(flat) thinInstanceIndex: u32,\n@location(3) @interpolate(flat) hasThinInstance: u32,\n@location(4) @interpolate(flat) instanceExtras: vec4f,\n};\nstruct FsOut { @location(0) color: vec4f, @location(1) depth: vec4f };\n@fragment fn fs(input: VsOut) -> FsOut {\nif (shouldDiscardPick(PickDiscardInput(input.worldPos, scene.fragmentCoord, input.pickId, input.thinInstanceIndex, input.hasThinInstance, input.instanceExtras))) { discard; }\nlet id = input.pickId;\nlet r = f32((id >> 16u) & 0xFFu) / 255.0;\nlet g = f32((id >> 8u) & 0xFFu) / 255.0;\nlet b = f32(id & 0xFFu) / 255.0;\nreturn FsOut(vec4f(r, g, b, 1.0), vec4f(input.p.z, 0.0, 0.0, 0.0));\n}\n`;\n\n// ─── Regular mesh picking shader ────────────────────────────────────\n\nexport function pickingShaderSource(opts?: PickingShaderOptions): string {\n return /* wgsl */ `\n${PICK_SCENE}\nstruct MeshUniforms {\nworld: mat4x4f,\npickId: u32,\n};\n@group(1) @binding(0) var<uniform> mesh: MeshUniforms;\n${PICK_DISCARD_INPUT}\n${pickStorageDecls(opts)}\n${pickDiscardSource(opts)}\n${pickWorldAdjustSource(opts)}\n${PICK_FS}\n@vertex fn vs(@location(0) position: vec3f) -> VsOut {\nvar out: VsOut;\nlet wp = adjustPickWorld((mesh.world * vec4f(position, 1.0)).xyz, vec4f(0.0), 0xffffffffu);\nout.p = scene.viewProjection * vec4f(wp, 1.0);\nout.pickId = mesh.pickId;\nout.worldPos = wp;\nout.thinInstanceIndex = 0xffffffffu;\nout.hasThinInstance = 0u;\nout.instanceExtras = vec4f(0.0);\nreturn out;\n}\n`;\n}\n\n// ─── Thin-instance picking shader ───────────────────────────────────\n\nexport function pickingThinInstanceShaderSource(opts?: PickingShaderOptions): string {\n return /* wgsl */ `\n${PICK_SCENE}\nstruct TIMeshUniforms {\nbaseMeshPickId: u32,\n};\n@group(1) @binding(0) var<uniform> tiMesh: TIMeshUniforms;\n@group(1) @binding(1) var<storage, read> instances: array<mat4x4f>;\n${PICK_DISCARD_INPUT}\n${pickStorageDecls(opts)}\n${pickDiscardSource(opts)}\n${pickWorldAdjustSource(opts)}\n${PICK_FS}\n@vertex fn vs(@location(0) position: vec3f, @builtin(instance_index) instanceIndex: u32) -> VsOut {\nlet m = instances[instanceIndex];\n// Treat the instance placement as an AFFINE transform: force the basis columns' homogeneous w to 0 and the\n// translation column's w to 1. Thin-instanced ShaderMaterials may pack per-instance data in those spare w\n// lanes (a sanctioned pattern — Lite injects world0..world3 and the app's own vertex shader zeroes them\n// before transforming). Picking only needs the transform, so packed values are exposed separately.\nlet world = mat4x4f(\nvec4f(m[0].xyz, 0.0),\nvec4f(m[1].xyz, 0.0),\nvec4f(m[2].xyz, 0.0),\nvec4f(m[3].xyz, 1.0),\n);\nlet extras = vec4f(m[0].w, m[1].w, m[2].w, m[3].w);\nvar out: VsOut;\nlet wp = adjustPickWorld((world * vec4f(position, 1.0)).xyz, extras, instanceIndex);\nout.p = scene.viewProjection * vec4f(wp, 1.0);\nout.pickId = tiMesh.baseMeshPickId + instanceIndex;\nout.worldPos = wp;\nout.thinInstanceIndex = instanceIndex;\nout.hasThinInstance = 1u;\nout.instanceExtras = extras;\nreturn out;\n}\n`;\n}\n"],"names":[],"mappings":"AAWA,MAAM,kBAAA;AAAA;AAAA,EAAgC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAWtC,MAAM,oBAAA;AAAA;AAAA,EAAkC;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAMxC,SAAS,kBAAkB,IAAA,EAAqC;AAC5D,EAAA,OAAO,MAAM,WAAA,IAAe,oBAAA;AAChC;AAEA,MAAM,yBAAA;AAAA;AAAA,EAAuC;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAM7C,SAAS,sBAAsB,IAAA,EAAqC;AAChE,EAAA,OAAO,MAAM,eAAA,IAAmB,yBAAA;AACpC;AAEA,SAAS,iBAAiB,IAAA,EAAqC;AAC3D,EAAA,MAAM,UAAU,IAAA,EAAM,OAAA;AACtB,EAAA,IAAI,CAAC,OAAA,IAAW,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG;AAClC,IAAA,OAAO,EAAA;AAAA,EACX;AACA,EAAA,OAAO,QAAQ,GAAA,CAAI,CAAC,CAAA,EAAG,OAAA,KAAY,sBAAsB,OAAO,CAAA,qBAAA,EAAwB,CAAA,CAAE,IAAI,KAAK,CAAA,CAAE,IAAI,CAAA,CAAA,CAAG,CAAA,CAAE,KAAK,IAAI,CAAA;AAC3H;AAIA,MAAM,UAAA;AAAA;AAAA,EAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAQ9B,MAAM,OAAA;AAAA;AAAA,EAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAsBpB,SAAS,oBAAoB,IAAA,EAAqC;AACrE,EAAA;AAAA;AAAA,IAAkB;AAAA,EACpB,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMV,kBAAkB;AAAA,EAClB,gBAAA,CAAiB,IAAI,CAAC;AAAA,EACtB,iBAAA,CAAkB,IAAI,CAAC;AAAA,EACvB,qBAAA,CAAsB,IAAI,CAAC;AAAA,EAC3B,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaT;AAIO,SAAS,gCAAgC,IAAA,EAAqC;AACjF,EAAA;AAAA;AAAA,IAAkB;AAAA,EACpB,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMV,kBAAkB;AAAA,EAClB,gBAAA,CAAiB,IAAI,CAAC;AAAA,EACtB,iBAAA,CAAkB,IAAI,CAAC;AAAA,EACvB,qBAAA,CAAsB,IAAI,CAAC;AAAA,EAC3B,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyBT;;;;"}
@@ -243,6 +243,9 @@ async function registerSceneWithShadowSupport(scene) {
243
243
  }
244
244
  const byOrder = (a, b) => a.order - b.order;
245
245
  async function ensureShadowTask(engine, scene) {
246
+ if (scene._frameGraph._tasks.some((task) => task.name === "shadow")) {
247
+ return;
248
+ }
246
249
  const { createShadowTask } = await import('../frame-graph/shadow-task.js');
247
250
  scene._frameGraph._tasks.unshift(createShadowTask(engine, scene));
248
251
  }
@@ -1 +1 @@
1
- {"version":3,"file":"scene-core.js","sources":["../../../src/scene/scene-core.ts"],"sourcesContent":["import type { EngineContext, RenderingContext } from \"../engine/engine.js\";\nimport { _vis, isRenderingContextRegistered, registerRenderingContext, unregisterRenderingContext } from \"../engine/engine.js\";\nimport type { SurfaceContext } from \"../engine/surface.js\";\nimport type { Camera } from \"../camera/camera.js\";\nimport type { LightBase } from \"../light/types.js\";\nimport type { Mesh } from \"../mesh/mesh.js\";\nimport { disposeMeshGpu } from \"../mesh/mesh-dispose.js\";\nimport { registerMeshScene, unregisterMeshScene, enqueueMaterialSwap } from \"./mesh-scene-registry.js\";\nimport { processMaterialSwaps } from \"./scene-material-swap.js\";\nimport type { AnimationGroup } from \"../animation/animation-group.js\";\nimport { tickAnimation } from \"../animation/animation-tick.js\";\nimport type { ShadowGenerator } from \"../shadow/shadow-generator.js\";\nimport type { FogConfig } from \"../material/standard/standard-material.js\";\nimport type { Renderable, PrePassRenderable, SceneUniformUpdater, MeshGroupBuilder } from \"../render/renderable.js\";\nimport type { TransformNode } from \"./transform-node.js\";\nimport type { SceneNode } from \"./scene-node.js\";\nimport type { EnvironmentTextures } from \"../loader-env/load-env.js\";\nimport type { FrameGraph } from \"../frame-graph/frame-graph.js\";\nimport { createFrameGraph, _appendTask } from \"../frame-graph/frame-graph.js\";\nimport { createRenderTask } from \"../frame-graph/render-task.js\";\nimport { createRenderTarget } from \"../engine/render-target.js\";\nimport type { AssetContainer } from \"../asset-container.js\";\nimport type { SceneLightGpuState } from \"../render/lights-ubo.js\";\nimport type { ClusteredLightContainer } from \"../light/clustered.js\";\nimport type { PickSource } from \"../picking/pick-contributor.js\";\nimport type { ToneMapping } from \"../material/pbr/tone-mapping.js\";\n\n/** Image processing configuration. */\nexport interface ImageProcessingConfig {\n exposure: number;\n contrast: number;\n toneMappingEnabled: boolean;\n /**\n * Tone mapping algorithm applied by PBR materials when `toneMappingEnabled` is true.\n * Undefined means the default {@link StandardToneMapping} (exponential). Assign a\n * built-in ({@link StandardToneMapping}, {@link AcesToneMapping}, {@link NeutralToneMapping})\n * or a custom {@link ToneMapping}.\n *\n * This is baked into the PBR shaders at `registerScene()` time. To change it after\n * registration, use `setSceneImageProcessing` so the affected pipelines are rebuilt.\n */\n toneMapping?: ToneMapping;\n}\n\n/** A clipping plane expressed as the coefficients `[a, b, c, d]` of `a·x + b·y + c·z + d`. */\nexport type ClipPlane = readonly [number, number, number, number];\n\n/** Top-level scene context — pure state, no attached methods. */\nexport interface SceneContext extends RenderingContext {\n /** Surface this scene renders into. Set at scene-creation time and immutable\n * afterwards — the default render task is sized and MSAA-matched to this surface,\n * and `registerScene` attaches the scene to it. For the engine's primary surface\n * (the common single-canvas case) this is the engine itself. The owning engine is\n * reachable via `scene.surface.engine`. */\n readonly surface: SurfaceContext;\n clearColor: GPUColorDict;\n camera: Camera | null;\n lights: LightBase[];\n imageProcessing: ImageProcessingConfig;\n\n /** All meshes added to the scene (standard + PBR). */\n meshes: Mesh[];\n\n /** Animation groups loaded from glTF or created manually. */\n animationGroups: AnimationGroup[];\n\n /** Fog configuration. Null = no fog. */\n fog: FogConfig | null;\n\n /** Scene clip plane as (normal.x, normal.y, normal.z, d). Matches Babylon.js Plane `dot(worldPosition, plane) > 0` discard semantics. */\n clipPlane: ClipPlane | null;\n\n /** Shadow generators registered on this scene. */\n shadowGenerators: ShadowGenerator[];\n\n /** Background material primaryColor (linear RGB). Default from Babylon createDefaultEnvironment. */\n environmentPrimaryColor?: [number, number, number];\n\n /** Environment cubemap Y rotation in radians. */\n envRotationY?: number;\n\n /** Fixed delta time in ms for deterministic animation. 0 = use real rAF delta. */\n fixedDeltaMs: number;\n\n /** All renderables in this scene. The active frame-graph tasks bucket them\n * (opaque / direct / transparent) at bind time based on `isTransparent`, `_direct`, and `_transmissive`. */\n /** @internal */\n _renderables: Renderable[];\n /** @internal Pre-pass work (shadow maps, compute, etc.). */\n _prePasses: PrePassRenderable[];\n /** Pick sources — one per optional pickable entity (GS mesh, billboard system, …). Registered by\n * the entity module via `registerPickSource` when the entity is added; each is pure data + a\n * dynamic-import thunk the GPU picker resolves (once) on the first pick, so rendering the entity\n * pulls no pick-pipeline bytes. Scene-core stays pick-agnostic apart from this opaque list. */\n /** @internal */\n _pickSources: PickSource[];\n /** @internal Scene uniform updaters (one per shared UBO). */\n _uniformUpdaters: SceneUniformUpdater[];\n /** @internal Opt-in feature writers for the SceneUniforms UBO (fog, clip plane, env SH).\n * Populated lazily via the scene-ubo-extras seam; run by the render task. */\n _sceneUboContributors?: ((data: Float32Array, scene: SceneContext) => void)[];\n /** @internal Per-frame callbacks run before rendering (animation, physics, etc.). */\n _beforeRender: ((deltaMs: number) => void)[];\n /** @internal Deferred builders — registered by loaders/factories, run once at startEngine(). */\n _deferredBuilders: (() => void | Promise<void>)[];\n /** @internal Mesh group registry — maps builder to its mesh list (internal bookkeeping). */\n _groups: Map<MeshGroupBuilder, Mesh[]>;\n\n // ─── Dispose infrastructure ────────────────────────────────\n /** @internal Shared cleanup callbacks (scene UBOs, lights UBOs, etc.). Registered by builders. */\n _disposables: (() => void)[];\n /** @internal Per-mesh cleanup callbacks (mesh UBOs, bind groups). For material swap + dispose. */\n _meshDisposables: Map<Mesh, (() => void)[]>;\n /** @internal Per-mesh cleanup callbacks for AUX (material-OVERRIDE) view packets that an explicit render\n * task registered on a mesh it does not own — e.g. a depth-prepass / SSAO no-colour view of a wall. Kept\n * SEPARATE from `_meshDisposables` because a MAIN-material swap (`processMaterialSwaps`, which rebuilds\n * only the main renderable) must NOT tear these down: they belong to another task, whose cached bundle\n * would then replay a destroyed system UBO (\"used in submit while destroyed\"). Drained only on a real mesh\n * removal (`removeFromScene`) and scene dispose, exactly like `_meshDisposables` minus the swap path. */\n _meshAuxDisposables: Map<Mesh, (() => void)[]>;\n /** @internal Meshes whose material was changed via setter — drained before each render frame. */\n _materialSwapQueue: Mesh[];\n /** @internal Monotonic counter bumped when the renderable list changes (add/remove/rebuild). */\n _renderableVersion: number;\n /** @internal Monotonic counter bumped ONLY when a material's renderables are rebuilt/swapped (material\n * swap drain or `rebuildMaterial`) — NOT on a geometry resize (which bumps `_renderableVersion` alone).\n * Lets consumers that cache material-view-derived GPU state (e.g. the CSM shadow tasks' no-color material\n * views) cheaply re-record on a geometry-only edit and only fully rebuild when a caster's material UBOs\n * were actually destroyed/recreated (which would otherwise leave their cached views dangling). */\n _materialEpoch: number;\n /** True once the initial deferred build (buildScene) has run. Meshes added after\n * this point are materialized via the per-frame swap drain rather than the\n * boot-only deferred-builder path. */\n /** @internal */\n _built: boolean;\n /** Builders whose deferred group build has COMPLETED. A mesh that joins one of these groups after the fact\n * (post-boot, or a glTF prop whose async load resolves DURING buildScene's drain and joins an already-built\n * group) is materialized via the per-frame swap drain: its group builder has already run and won't see it,\n * so without this it would cast shadows (it's in ctx.meshes) but never be drawn in the color pass. */\n /** @internal */\n _builtGroups: Set<MeshGroupBuilder>;\n\n // ─── Stashed internal state (typed to avoid `as any` casts) ────\n /** @internal */\n _envTextures?: EnvironmentTextures;\n /** @internal Scene-owned shared LightsUniforms UBO state (group 0 binding 1). */\n _lightGpuState?: SceneLightGpuState;\n\n /** Frame graph driving this scene's rendering. Created eagerly by\n * `createSceneContext` with a default `RenderTask` that mirrors\n * `_renderables` into the swapchain. User code may add additional tasks\n * (offscreen RTTs, post-FX, UI overlays, etc.). */\n /** @internal */\n _frameGraph: FrameGraph;\n\n /** @internal Optional clustered point-light container. Only populated by the clustered-light extension API. */\n _clusteredLightContainer?: ClusteredLightContainer;\n /** @internal Updates clustered light cells for the camera used by the current render pass. */\n _clusteredLightUpdater?: (camera: Camera | null | undefined, targetWidth: number, targetHeight: number) => void;\n}\n\n/** Options passed to the scene-context factory. */\nexport interface SceneContextOptions {\n defaultRenderTask?: boolean;\n}\n\n/** Create an empty scene context bound to the given `surface`. The default render task\n * is built against the surface's format, MSAA configuration, and swapchain RT — the\n * scene is permanently bound to that surface. Pass `engine` directly (since\n * `EngineContext extends SurfaceContext`) for the common single-canvas case, or pass\n * an auxiliary surface created via `createSurface`. */\nexport function createSceneContext(surface: SurfaceContext, options?: SceneContextOptions): SceneContext {\n const eng = surface.engine;\n\n // Closures below capture `ctx` by-reference via this object.\n const ctxLocal: Omit<SceneContext, \"_frameGraph\"> = {\n surface,\n clearColor: { r: 0.2, g: 0.2, b: 0.3, a: 1.0 },\n camera: null,\n lights: [],\n meshes: [],\n animationGroups: [],\n fog: null,\n clipPlane: null,\n shadowGenerators: [],\n imageProcessing: { exposure: 1.0, contrast: 1.0, toneMappingEnabled: false },\n _renderables: [],\n _prePasses: [],\n _pickSources: [],\n _uniformUpdaters: [],\n fixedDeltaMs: 0,\n _beforeRender: [],\n _deferredBuilders: [],\n _groups: new Map(),\n _disposables: [],\n _meshDisposables: new Map(),\n _meshAuxDisposables: new Map(),\n _materialSwapQueue: [],\n _renderableVersion: 0,\n _materialEpoch: 0,\n _built: false,\n _builtGroups: new Set(),\n _drawCallsPre: 0,\n\n _update(): void {\n // When the engine was created with `useFloatingOrigin: true`, mark\n // the active camera so `getViewMatrix` knows to zero its\n // translation column (the GPU view × world product is then the\n // eye-relative result the LWR offset trick produces). For non-LWR\n // engines `eng.useFloatingOrigin` is false and this is a single\n // boolean check per frame — the inner branch is dead.\n if (eng.useFloatingOrigin && ctx.camera && !ctx.camera._useFloatingOrigin) {\n ctx.camera._useFloatingOrigin = true;\n ctx.camera._viewVer = -1;\n ctx.camera._vpVer = -1;\n }\n const d = ctx.fixedDeltaMs > 0 ? ctx.fixedDeltaMs : eng._currentDelta;\n const encoder = eng._currentEncoder;\n let draws = 0;\n for (const cb of ctx._beforeRender) {\n cb(d);\n }\n if (ctx._materialSwapQueue.length > 0) {\n processMaterialSwaps(ctx);\n }\n for (const pp of ctx._prePasses) {\n draws += pp.execute(encoder, eng);\n }\n for (const u of ctx._uniformUpdaters) {\n u.update(eng);\n }\n ctx._drawCallsPre = draws;\n },\n _record(): number {\n return ctx._frameGraph.execute();\n },\n _resize(): void {\n // Canvas backing-store changed: rebuild the frame graph so canvas-sized\n // render targets get re-allocated at the new pixel size before the next record.\n ctx._frameGraph.build();\n },\n };\n\n const ctx = ctxLocal as SceneContext;\n // Eagerly attach the frame graph + a default swapchain render-pass task. The\n // graph drives all GPU work for this scene; user code can add more tasks\n // (offscreen RTTs, post-FX, UI overlays) before/after.\n const fg = createFrameGraph(eng);\n ctx._frameGraph = fg;\n if (options?.defaultRenderTask !== false) {\n // MSAA: render into an MSAA colour RT (which owns depth) and resolve into the\n // single-sample scRT. No MSAA: render straight into the colour-only\n // scRT with a task-owned single-sample depth buffer it builds/clears/frees.\n // All three reads (format / msaaSamples / scRT) come from the bound `surface`.\n const msaa = surface.msaaSamples > 1;\n const rt = msaa\n ? createRenderTarget({ lbl: \"scene-color\", format: surface.format, dFormat: \"depth24plus-stencil8\", samples: surface.msaaSamples, size: surface })\n : surface.scRT;\n const depth = msaa ? undefined : createRenderTarget({ lbl: \"scene-depth\", dFormat: \"depth24plus-stencil8\", samples: 1, size: surface });\n _appendTask(fg, createRenderTask({ name: \"scene\", rt, rst: msaa ? surface.scRT : undefined, depth, clrColor: ctx.clearColor }, eng, ctx));\n }\n ctx._disposables.push(() => fg.dispose());\n return ctx;\n}\n\n/** Register a callback to run before each rendered frame. */\nexport function onBeforeRender(scene: SceneContext, cb: (deltaMs: number) => void): void {\n (scene as SceneContext)._beforeRender.unshift(cb);\n}\n\n/** Register a callback to run when `disposeScene` is called. Used to tie\n * user-owned GPU resources (e.g. a `SpriteRenderer`) to the scene's lifetime. */\nexport function onSceneDispose(scene: SceneContext, cb: () => void): void {\n (scene as SceneContext)._disposables.push(cb);\n}\n\n/** Get the scene's frame graph. Always non-null — created in `createSceneContext`. */\nexport function getFrameGraph(scene: SceneContext): FrameGraph {\n return (scene as SceneContext)._frameGraph;\n}\n\nexport interface DeferredSceneRenderables {\n renderables: readonly Renderable[];\n dispose?: () => void;\n}\n\n/** @internal Register optional scene-hosted render work without teaching `addToScene` about the feature. */\nexport function addDeferredSceneRenderables(\n scene: SceneContext,\n build: (engine: EngineContext, scene: SceneContext) => DeferredSceneRenderables | Promise<DeferredSceneRenderables>\n): void {\n const ctx = scene as SceneContext;\n ctx._deferredBuilders.push(async () => {\n const built = await build(ctx.surface.engine, ctx);\n ctx._renderables.push(...built.renderables);\n if (built.dispose) {\n ctx._disposables.push(built.dispose);\n }\n });\n}\n\n/**\n * Adds an entity (mesh, light, camera, transform node, shadow generator, or asset container)\n * to the scene, dispatching on its type. Asset containers are unpacked and each contained\n * entity added recursively. Optional scene-hosted systems such as depth-hosted sprites\n * expose their own opt-in add functions so mesh-only scenes do not pay feature-specific\n * routing bytes here.\n * @param scene - The owning scene (pillar 4b: entities never reference the scene themselves).\n * @param entity - The entity (or asset container) to add.\n */\nexport function addToScene(scene: SceneContext, entity: Mesh | LightBase | Camera | ShadowGenerator | TransformNode | AssetContainer): void {\n const ctx = scene as SceneContext;\n // AssetContainer from loadGltf / loadBabylon — process each field present\n if (\"entities\" in entity) {\n const result = entity as AssetContainer;\n for (const e of result.entities) {\n addToScene(scene, e);\n }\n if (result.clearColor) {\n ctx.clearColor = result.clearColor;\n }\n if (result.camera && !ctx.camera) {\n ctx.camera = result.camera;\n }\n if (result.animationGroups?.length) {\n const engine = ctx.surface.engine;\n const groups = result.animationGroups;\n ctx.animationGroups.push(...groups);\n ctx._beforeRender.push((deltaMs: number) => {\n for (const g of groups) {\n tickAnimation(g, deltaMs, engine);\n }\n });\n }\n // Feature-owned scene wiring (e.g. EXT_lights_image_based installs its IBL\n // environment). Runs synchronously so the environment is registered before\n // registerScene() builds the scene UBO / PBR renderables.\n result._sceneSetup?.(ctx);\n return;\n }\n if (\"_gpu\" in entity && \"material\" in entity) {\n const mesh = entity as unknown as Mesh;\n ctx.meshes.push(mesh);\n registerMeshScene(ctx, mesh);\n const build = mesh.material ? (mesh.material as unknown as { _buildGroup?: MeshGroupBuilder })._buildGroup : undefined;\n if (build) {\n let group = ctx._groups.get(build);\n if (!group) {\n group = [];\n ctx._groups.set(build, group);\n ctx._deferredBuilders.push(async () => {\n const result = await build(ctx, group!);\n ctx._renderables.push(...result.renderables);\n if (result.updater) {\n ctx._uniformUpdaters.push(result.updater);\n }\n // This group's meshes now have renderables; a mesh that joins it LATER (post-boot or\n // mid-drain) won't be seen by this builder and must be materialized via a swap instead.\n ctx._builtGroups.add(build);\n });\n }\n group.push(mesh);\n // Materialize this mesh's renderable through the per-frame material-swap drain when the boot-only\n // deferred builder won't cover it: either after the initial build (`_built`), or when joining a group\n // whose builder has ALREADY completed (`_builtGroups`) — e.g. a glTF prop whose async load resolves\n // mid-drain and joins an already-built group. A mesh joining a group whose builder has NOT yet run (a\n // brand-new group, or one still pending in the drain) is built by that builder, so it must NOT enqueue\n // here — that would insert a SECOND renderable for it. buildScene drains the queue at the end.\n if (ctx._built || ctx._builtGroups.has(build)) {\n enqueueMaterialSwap(ctx, mesh);\n }\n }\n } else if (\"lightType\" in entity) {\n ctx.lights.push(entity as LightBase);\n }\n // Recurse into children of meshes, lights, cameras — set parent links\n const kids = (entity as unknown as SceneNode).children;\n if (kids?.length) {\n for (const child of kids) {\n (child as unknown as SceneNode).parent = entity as unknown as SceneNode;\n addToScene(scene, child);\n }\n }\n}\n\n/** Release all GPU resources owned by this scene. */\nexport function disposeScene(scene: SceneContext): void {\n const ctx = scene as SceneContext;\n unregisterRenderingContext(ctx.surface, ctx);\n for (const fn of ctx._disposables) {\n fn();\n }\n for (const fns of ctx._meshDisposables.values()) {\n for (const fn of fns) {\n fn();\n }\n }\n ctx._meshDisposables.clear();\n for (const fns of ctx._meshAuxDisposables.values()) {\n for (const fn of fns) {\n fn();\n }\n }\n ctx._meshAuxDisposables.clear();\n for (const mesh of ctx.meshes) {\n // Free the mesh's shared GPU buffers only when this was its LAST owning scene.\n if (unregisterMeshScene(ctx, mesh)) {\n disposeMeshGpu(mesh);\n }\n }\n ctx.meshes.length = 0;\n ctx._renderables.length = 0;\n ctx._prePasses.length = 0;\n ctx._pickSources.length = 0;\n ctx._uniformUpdaters.length = 0;\n ctx._beforeRender.length = 0;\n ctx._deferredBuilders.length = 0;\n ctx._disposables.length = 0;\n ctx._materialSwapQueue.length = 0;\n ctx.lights.length = 0;\n ctx.animationGroups.length = 0;\n ctx.shadowGenerators.length = 0;\n ctx.camera = null;\n}\n\n/** @internal Run all deferred builders (called by registerScene's boot step before the first frame). */\nexport async function buildScene(scene: SceneContext): Promise<void> {\n const ctx = scene as SceneContext;\n // Discard material-swap requests enqueued during scene SETUP — a mesh added, then re-materialed before boot\n // via the mesh.material setter (e.g. scene12 assigns each row's material AFTER addToScene). The deferred\n // builders below build every group's meshes fresh with their FINAL material, so those swaps are redundant;\n // processing them would insert a SECOND renderable per mesh (double-draw). Only swaps enqueued DURING the\n // drain below — an async mesh that joins an already-built group (see addToScene/_builtGroups) — must survive.\n ctx._materialSwapQueue.length = 0;\n while (ctx._deferredBuilders.length > 0) {\n const builders = [...ctx._deferredBuilders];\n ctx._deferredBuilders = [];\n await Promise.all(builders.map(async (b) => b()));\n }\n // Build the renderables for any meshes that joined an already-built group mid-drain (queued above) before\n // the first frame, instead of leaving them casting shadows but invisible in the color pass.\n processMaterialSwaps(ctx);\n ctx._renderableVersion++;\n ctx._built = true;\n}\n\n/**\n * Register a scene with the engine. Builds deferred work, sorts renderables by order,\n * and adds the scene to its bound surface's render list in overlay order. The scene is\n * always attached to `scene.surface` (which equals the engine itself in the\n * single-canvas case).\n */\nexport async function registerScene(scene: SceneContext): Promise<void> {\n const ctx = scene;\n const surface = ctx.surface;\n if (isRenderingContextRegistered(surface, ctx)) {\n return;\n }\n await buildScene(scene);\n ctx._renderables.sort(byOrder);\n await Promise.all(ctx._frameGraph._tasks.map((task) => task._preload?.()).filter((preload): preload is Promise<void> => preload !== undefined));\n ctx._frameGraph.build();\n if (surface._renderingContexts.length > 0) {\n const overlay = await import(\"./swapchain-overlay.js\");\n overlay.configureSwapchainOverlayScene(surface, ctx);\n }\n registerRenderingContext(surface, ctx);\n}\n\n/**\n * Register a scene with the engine and install the scene-owned shadow frame-graph task.\n * Use only for scenes that generate shadow maps. Like {@link registerScene}, the scene\n * is attached to `scene.surface` (and its owning engine is `scene.surface.engine`).\n */\nexport async function registerSceneWithShadowSupport(scene: SceneContext): Promise<void> {\n const ctx = scene as SceneContext;\n const surface = ctx.surface;\n if (isRenderingContextRegistered(surface, ctx)) {\n return;\n }\n await buildScene(scene);\n ctx._renderables.sort(byOrder);\n await ensureShadowTask(surface.engine, ctx);\n await Promise.all(ctx._frameGraph._tasks.map((task) => task._preload?.()).filter((preload): preload is Promise<void> => preload !== undefined));\n ctx._frameGraph.build();\n if (surface._renderingContexts.length > 0) {\n const overlay = await import(\"./swapchain-overlay.js\");\n overlay.configureSwapchainOverlayScene(surface, ctx);\n }\n registerRenderingContext(surface, ctx);\n}\n\nconst byOrder = (a: Renderable, b: Renderable): number => a.order - b.order;\n\nasync function ensureShadowTask(engine: EngineContext, scene: SceneContext): Promise<void> {\n const { createShadowTask } = await import(\"../frame-graph/shadow-task.js\");\n scene._frameGraph._tasks.unshift(createShadowTask(engine, scene));\n}\n\n/** Remove a previously-registered scene. Idempotent. Does not dispose scene resources.\n * The scene is always removed from `scene.surface`. */\nexport function unregisterScene(scene: SceneContext): void {\n unregisterRenderingContext(scene.surface, scene as SceneContext);\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scene-core.js","sources":["../../../src/scene/scene-core.ts"],"sourcesContent":["import type { EngineContext, RenderingContext } from \"../engine/engine.js\";\nimport { _vis, isRenderingContextRegistered, registerRenderingContext, unregisterRenderingContext } from \"../engine/engine.js\";\nimport type { SurfaceContext } from \"../engine/surface.js\";\nimport type { Camera } from \"../camera/camera.js\";\nimport type { LightBase } from \"../light/types.js\";\nimport type { Mesh } from \"../mesh/mesh.js\";\nimport { disposeMeshGpu } from \"../mesh/mesh-dispose.js\";\nimport { registerMeshScene, unregisterMeshScene, enqueueMaterialSwap } from \"./mesh-scene-registry.js\";\nimport { processMaterialSwaps } from \"./scene-material-swap.js\";\nimport type { AnimationGroup } from \"../animation/animation-group.js\";\nimport { tickAnimation } from \"../animation/animation-tick.js\";\nimport type { ShadowGenerator } from \"../shadow/shadow-generator.js\";\nimport type { FogConfig } from \"../material/standard/standard-material.js\";\nimport type { Renderable, PrePassRenderable, SceneUniformUpdater, MeshGroupBuilder } from \"../render/renderable.js\";\nimport type { TransformNode } from \"./transform-node.js\";\nimport type { SceneNode } from \"./scene-node.js\";\nimport type { EnvironmentTextures } from \"../loader-env/load-env.js\";\nimport type { FrameGraph } from \"../frame-graph/frame-graph.js\";\nimport { createFrameGraph, _appendTask } from \"../frame-graph/frame-graph.js\";\nimport { createRenderTask } from \"../frame-graph/render-task.js\";\nimport { createRenderTarget } from \"../engine/render-target.js\";\nimport type { AssetContainer } from \"../asset-container.js\";\nimport type { SceneLightGpuState } from \"../render/lights-ubo.js\";\nimport type { ClusteredLightContainer } from \"../light/clustered.js\";\nimport type { PickSource } from \"../picking/pick-contributor.js\";\nimport type { ToneMapping } from \"../material/pbr/tone-mapping.js\";\n\n/** Image processing configuration. */\nexport interface ImageProcessingConfig {\n exposure: number;\n contrast: number;\n toneMappingEnabled: boolean;\n /**\n * Tone mapping algorithm applied by PBR materials when `toneMappingEnabled` is true.\n * Undefined means the default {@link StandardToneMapping} (exponential). Assign a\n * built-in ({@link StandardToneMapping}, {@link AcesToneMapping}, {@link NeutralToneMapping})\n * or a custom {@link ToneMapping}.\n *\n * This is baked into the PBR shaders at `registerScene()` time. To change it after\n * registration, use `setSceneImageProcessing` so the affected pipelines are rebuilt.\n */\n toneMapping?: ToneMapping;\n}\n\n/** A clipping plane expressed as the coefficients `[a, b, c, d]` of `a·x + b·y + c·z + d`. */\nexport type ClipPlane = readonly [number, number, number, number];\n\n/** Top-level scene context — pure state, no attached methods. */\nexport interface SceneContext extends RenderingContext {\n /** Surface this scene renders into. Set at scene-creation time and immutable\n * afterwards — the default render task is sized and MSAA-matched to this surface,\n * and `registerScene` attaches the scene to it. For the engine's primary surface\n * (the common single-canvas case) this is the engine itself. The owning engine is\n * reachable via `scene.surface.engine`. */\n readonly surface: SurfaceContext;\n clearColor: GPUColorDict;\n camera: Camera | null;\n lights: LightBase[];\n imageProcessing: ImageProcessingConfig;\n\n /** All meshes added to the scene (standard + PBR). */\n meshes: Mesh[];\n\n /** Animation groups loaded from glTF or created manually. */\n animationGroups: AnimationGroup[];\n\n /** Fog configuration. Null = no fog. */\n fog: FogConfig | null;\n\n /** Scene clip plane as (normal.x, normal.y, normal.z, d). Matches Babylon.js Plane `dot(worldPosition, plane) > 0` discard semantics. */\n clipPlane: ClipPlane | null;\n\n /** Shadow generators registered on this scene. */\n shadowGenerators: ShadowGenerator[];\n\n /** Background material primaryColor (linear RGB). Default from Babylon createDefaultEnvironment. */\n environmentPrimaryColor?: [number, number, number];\n\n /** Environment cubemap Y rotation in radians. */\n envRotationY?: number;\n\n /** Fixed delta time in ms for deterministic animation. 0 = use real rAF delta. */\n fixedDeltaMs: number;\n\n /** All renderables in this scene. The active frame-graph tasks bucket them\n * (opaque / direct / transparent) at bind time based on `isTransparent`, `_direct`, and `_transmissive`. */\n /** @internal */\n _renderables: Renderable[];\n /** @internal Pre-pass work (shadow maps, compute, etc.). */\n _prePasses: PrePassRenderable[];\n /** Pick sources — one per optional pickable entity (GS mesh, billboard system, …). Registered by\n * the entity module via `registerPickSource` when the entity is added; each is pure data + a\n * dynamic-import thunk the GPU picker resolves (once) on the first pick, so rendering the entity\n * pulls no pick-pipeline bytes. Scene-core stays pick-agnostic apart from this opaque list. */\n /** @internal */\n _pickSources: PickSource[];\n /** @internal Scene uniform updaters (one per shared UBO). */\n _uniformUpdaters: SceneUniformUpdater[];\n /** @internal Opt-in feature writers for the SceneUniforms UBO (fog, clip plane, env SH).\n * Populated lazily via the scene-ubo-extras seam; run by the render task. */\n _sceneUboContributors?: ((data: Float32Array, scene: SceneContext) => void)[];\n /** @internal Per-frame callbacks run before rendering (animation, physics, etc.). */\n _beforeRender: ((deltaMs: number) => void)[];\n /** @internal Deferred builders — registered by loaders/factories, run once at startEngine(). */\n _deferredBuilders: (() => void | Promise<void>)[];\n /** @internal Mesh group registry — maps builder to its mesh list (internal bookkeeping). */\n _groups: Map<MeshGroupBuilder, Mesh[]>;\n\n // ─── Dispose infrastructure ────────────────────────────────\n /** @internal Shared cleanup callbacks (scene UBOs, lights UBOs, etc.). Registered by builders. */\n _disposables: (() => void)[];\n /** @internal Per-mesh cleanup callbacks (mesh UBOs, bind groups). For material swap + dispose. */\n _meshDisposables: Map<Mesh, (() => void)[]>;\n /** @internal Per-mesh cleanup callbacks for AUX (material-OVERRIDE) view packets that an explicit render\n * task registered on a mesh it does not own — e.g. a depth-prepass / SSAO no-colour view of a wall. Kept\n * SEPARATE from `_meshDisposables` because a MAIN-material swap (`processMaterialSwaps`, which rebuilds\n * only the main renderable) must NOT tear these down: they belong to another task, whose cached bundle\n * would then replay a destroyed system UBO (\"used in submit while destroyed\"). Drained only on a real mesh\n * removal (`removeFromScene`) and scene dispose, exactly like `_meshDisposables` minus the swap path. */\n _meshAuxDisposables: Map<Mesh, (() => void)[]>;\n /** @internal Meshes whose material was changed via setter — drained before each render frame. */\n _materialSwapQueue: Mesh[];\n /** @internal Monotonic counter bumped when the renderable list changes (add/remove/rebuild). */\n _renderableVersion: number;\n /** @internal Monotonic counter bumped ONLY when a material's renderables are rebuilt/swapped (material\n * swap drain or `rebuildMaterial`) — NOT on a geometry resize (which bumps `_renderableVersion` alone).\n * Lets consumers that cache material-view-derived GPU state (e.g. the CSM shadow tasks' no-color material\n * views) cheaply re-record on a geometry-only edit and only fully rebuild when a caster's material UBOs\n * were actually destroyed/recreated (which would otherwise leave their cached views dangling). */\n _materialEpoch: number;\n /** True once the initial deferred build (buildScene) has run. Meshes added after\n * this point are materialized via the per-frame swap drain rather than the\n * boot-only deferred-builder path. */\n /** @internal */\n _built: boolean;\n /** Builders whose deferred group build has COMPLETED. A mesh that joins one of these groups after the fact\n * (post-boot, or a glTF prop whose async load resolves DURING buildScene's drain and joins an already-built\n * group) is materialized via the per-frame swap drain: its group builder has already run and won't see it,\n * so without this it would cast shadows (it's in ctx.meshes) but never be drawn in the color pass. */\n /** @internal */\n _builtGroups: Set<MeshGroupBuilder>;\n\n // ─── Stashed internal state (typed to avoid `as any` casts) ────\n /** @internal */\n _envTextures?: EnvironmentTextures;\n /** @internal Scene-owned shared LightsUniforms UBO state (group 0 binding 1). */\n _lightGpuState?: SceneLightGpuState;\n\n /** Frame graph driving this scene's rendering. Created eagerly by\n * `createSceneContext` with a default `RenderTask` that mirrors\n * `_renderables` into the swapchain. User code may add additional tasks\n * (offscreen RTTs, post-FX, UI overlays, etc.). */\n /** @internal */\n _frameGraph: FrameGraph;\n\n /** @internal Optional clustered point-light container. Only populated by the clustered-light extension API. */\n _clusteredLightContainer?: ClusteredLightContainer;\n /** @internal Updates clustered light cells for the camera used by the current render pass. */\n _clusteredLightUpdater?: (camera: Camera | null | undefined, targetWidth: number, targetHeight: number) => void;\n}\n\n/** Options passed to the scene-context factory. */\nexport interface SceneContextOptions {\n defaultRenderTask?: boolean;\n}\n\n/** Create an empty scene context bound to the given `surface`. The default render task\n * is built against the surface's format, MSAA configuration, and swapchain RT — the\n * scene is permanently bound to that surface. Pass `engine` directly (since\n * `EngineContext extends SurfaceContext`) for the common single-canvas case, or pass\n * an auxiliary surface created via `createSurface`. */\nexport function createSceneContext(surface: SurfaceContext, options?: SceneContextOptions): SceneContext {\n const eng = surface.engine;\n\n // Closures below capture `ctx` by-reference via this object.\n const ctxLocal: Omit<SceneContext, \"_frameGraph\"> = {\n surface,\n clearColor: { r: 0.2, g: 0.2, b: 0.3, a: 1.0 },\n camera: null,\n lights: [],\n meshes: [],\n animationGroups: [],\n fog: null,\n clipPlane: null,\n shadowGenerators: [],\n imageProcessing: { exposure: 1.0, contrast: 1.0, toneMappingEnabled: false },\n _renderables: [],\n _prePasses: [],\n _pickSources: [],\n _uniformUpdaters: [],\n fixedDeltaMs: 0,\n _beforeRender: [],\n _deferredBuilders: [],\n _groups: new Map(),\n _disposables: [],\n _meshDisposables: new Map(),\n _meshAuxDisposables: new Map(),\n _materialSwapQueue: [],\n _renderableVersion: 0,\n _materialEpoch: 0,\n _built: false,\n _builtGroups: new Set(),\n _drawCallsPre: 0,\n\n _update(): void {\n // When the engine was created with `useFloatingOrigin: true`, mark\n // the active camera so `getViewMatrix` knows to zero its\n // translation column (the GPU view × world product is then the\n // eye-relative result the LWR offset trick produces). For non-LWR\n // engines `eng.useFloatingOrigin` is false and this is a single\n // boolean check per frame — the inner branch is dead.\n if (eng.useFloatingOrigin && ctx.camera && !ctx.camera._useFloatingOrigin) {\n ctx.camera._useFloatingOrigin = true;\n ctx.camera._viewVer = -1;\n ctx.camera._vpVer = -1;\n }\n const d = ctx.fixedDeltaMs > 0 ? ctx.fixedDeltaMs : eng._currentDelta;\n const encoder = eng._currentEncoder;\n let draws = 0;\n for (const cb of ctx._beforeRender) {\n cb(d);\n }\n if (ctx._materialSwapQueue.length > 0) {\n processMaterialSwaps(ctx);\n }\n for (const pp of ctx._prePasses) {\n draws += pp.execute(encoder, eng);\n }\n for (const u of ctx._uniformUpdaters) {\n u.update(eng);\n }\n ctx._drawCallsPre = draws;\n },\n _record(): number {\n return ctx._frameGraph.execute();\n },\n _resize(): void {\n // Canvas backing-store changed: rebuild the frame graph so canvas-sized\n // render targets get re-allocated at the new pixel size before the next record.\n ctx._frameGraph.build();\n },\n };\n\n const ctx = ctxLocal as SceneContext;\n // Eagerly attach the frame graph + a default swapchain render-pass task. The\n // graph drives all GPU work for this scene; user code can add more tasks\n // (offscreen RTTs, post-FX, UI overlays) before/after.\n const fg = createFrameGraph(eng);\n ctx._frameGraph = fg;\n if (options?.defaultRenderTask !== false) {\n // MSAA: render into an MSAA colour RT (which owns depth) and resolve into the\n // single-sample scRT. No MSAA: render straight into the colour-only\n // scRT with a task-owned single-sample depth buffer it builds/clears/frees.\n // All three reads (format / msaaSamples / scRT) come from the bound `surface`.\n const msaa = surface.msaaSamples > 1;\n const rt = msaa\n ? createRenderTarget({ lbl: \"scene-color\", format: surface.format, dFormat: \"depth24plus-stencil8\", samples: surface.msaaSamples, size: surface })\n : surface.scRT;\n const depth = msaa ? undefined : createRenderTarget({ lbl: \"scene-depth\", dFormat: \"depth24plus-stencil8\", samples: 1, size: surface });\n _appendTask(fg, createRenderTask({ name: \"scene\", rt, rst: msaa ? surface.scRT : undefined, depth, clrColor: ctx.clearColor }, eng, ctx));\n }\n ctx._disposables.push(() => fg.dispose());\n return ctx;\n}\n\n/** Register a callback to run before each rendered frame. */\nexport function onBeforeRender(scene: SceneContext, cb: (deltaMs: number) => void): void {\n (scene as SceneContext)._beforeRender.unshift(cb);\n}\n\n/** Register a callback to run when `disposeScene` is called. Used to tie\n * user-owned GPU resources (e.g. a `SpriteRenderer`) to the scene's lifetime. */\nexport function onSceneDispose(scene: SceneContext, cb: () => void): void {\n (scene as SceneContext)._disposables.push(cb);\n}\n\n/** Get the scene's frame graph. Always non-null — created in `createSceneContext`. */\nexport function getFrameGraph(scene: SceneContext): FrameGraph {\n return (scene as SceneContext)._frameGraph;\n}\n\nexport interface DeferredSceneRenderables {\n renderables: readonly Renderable[];\n dispose?: () => void;\n}\n\n/** @internal Register optional scene-hosted render work without teaching `addToScene` about the feature. */\nexport function addDeferredSceneRenderables(\n scene: SceneContext,\n build: (engine: EngineContext, scene: SceneContext) => DeferredSceneRenderables | Promise<DeferredSceneRenderables>\n): void {\n const ctx = scene as SceneContext;\n ctx._deferredBuilders.push(async () => {\n const built = await build(ctx.surface.engine, ctx);\n ctx._renderables.push(...built.renderables);\n if (built.dispose) {\n ctx._disposables.push(built.dispose);\n }\n });\n}\n\n/**\n * Adds an entity (mesh, light, camera, transform node, shadow generator, or asset container)\n * to the scene, dispatching on its type. Asset containers are unpacked and each contained\n * entity added recursively. Optional scene-hosted systems such as depth-hosted sprites\n * expose their own opt-in add functions so mesh-only scenes do not pay feature-specific\n * routing bytes here.\n * @param scene - The owning scene (pillar 4b: entities never reference the scene themselves).\n * @param entity - The entity (or asset container) to add.\n */\nexport function addToScene(scene: SceneContext, entity: Mesh | LightBase | Camera | ShadowGenerator | TransformNode | AssetContainer): void {\n const ctx = scene as SceneContext;\n // AssetContainer from loadGltf / loadBabylon — process each field present\n if (\"entities\" in entity) {\n const result = entity as AssetContainer;\n for (const e of result.entities) {\n addToScene(scene, e);\n }\n if (result.clearColor) {\n ctx.clearColor = result.clearColor;\n }\n if (result.camera && !ctx.camera) {\n ctx.camera = result.camera;\n }\n if (result.animationGroups?.length) {\n const engine = ctx.surface.engine;\n const groups = result.animationGroups;\n ctx.animationGroups.push(...groups);\n ctx._beforeRender.push((deltaMs: number) => {\n for (const g of groups) {\n tickAnimation(g, deltaMs, engine);\n }\n });\n }\n // Feature-owned scene wiring (e.g. EXT_lights_image_based installs its IBL\n // environment). Runs synchronously so the environment is registered before\n // registerScene() builds the scene UBO / PBR renderables.\n result._sceneSetup?.(ctx);\n return;\n }\n if (\"_gpu\" in entity && \"material\" in entity) {\n const mesh = entity as unknown as Mesh;\n ctx.meshes.push(mesh);\n registerMeshScene(ctx, mesh);\n const build = mesh.material ? (mesh.material as unknown as { _buildGroup?: MeshGroupBuilder })._buildGroup : undefined;\n if (build) {\n let group = ctx._groups.get(build);\n if (!group) {\n group = [];\n ctx._groups.set(build, group);\n ctx._deferredBuilders.push(async () => {\n const result = await build(ctx, group!);\n ctx._renderables.push(...result.renderables);\n if (result.updater) {\n ctx._uniformUpdaters.push(result.updater);\n }\n // This group's meshes now have renderables; a mesh that joins it LATER (post-boot or\n // mid-drain) won't be seen by this builder and must be materialized via a swap instead.\n ctx._builtGroups.add(build);\n });\n }\n group.push(mesh);\n // Materialize this mesh's renderable through the per-frame material-swap drain when the boot-only\n // deferred builder won't cover it: either after the initial build (`_built`), or when joining a group\n // whose builder has ALREADY completed (`_builtGroups`) — e.g. a glTF prop whose async load resolves\n // mid-drain and joins an already-built group. A mesh joining a group whose builder has NOT yet run (a\n // brand-new group, or one still pending in the drain) is built by that builder, so it must NOT enqueue\n // here — that would insert a SECOND renderable for it. buildScene drains the queue at the end.\n if (ctx._built || ctx._builtGroups.has(build)) {\n enqueueMaterialSwap(ctx, mesh);\n }\n }\n } else if (\"lightType\" in entity) {\n ctx.lights.push(entity as LightBase);\n }\n // Recurse into children of meshes, lights, cameras — set parent links\n const kids = (entity as unknown as SceneNode).children;\n if (kids?.length) {\n for (const child of kids) {\n (child as unknown as SceneNode).parent = entity as unknown as SceneNode;\n addToScene(scene, child);\n }\n }\n}\n\n/** Release all GPU resources owned by this scene. */\nexport function disposeScene(scene: SceneContext): void {\n const ctx = scene as SceneContext;\n unregisterRenderingContext(ctx.surface, ctx);\n for (const fn of ctx._disposables) {\n fn();\n }\n for (const fns of ctx._meshDisposables.values()) {\n for (const fn of fns) {\n fn();\n }\n }\n ctx._meshDisposables.clear();\n for (const fns of ctx._meshAuxDisposables.values()) {\n for (const fn of fns) {\n fn();\n }\n }\n ctx._meshAuxDisposables.clear();\n for (const mesh of ctx.meshes) {\n // Free the mesh's shared GPU buffers only when this was its LAST owning scene.\n if (unregisterMeshScene(ctx, mesh)) {\n disposeMeshGpu(mesh);\n }\n }\n ctx.meshes.length = 0;\n ctx._renderables.length = 0;\n ctx._prePasses.length = 0;\n ctx._pickSources.length = 0;\n ctx._uniformUpdaters.length = 0;\n ctx._beforeRender.length = 0;\n ctx._deferredBuilders.length = 0;\n ctx._disposables.length = 0;\n ctx._materialSwapQueue.length = 0;\n ctx.lights.length = 0;\n ctx.animationGroups.length = 0;\n ctx.shadowGenerators.length = 0;\n ctx.camera = null;\n}\n\n/** @internal Run all deferred builders (called by registerScene's boot step before the first frame). */\nexport async function buildScene(scene: SceneContext): Promise<void> {\n const ctx = scene as SceneContext;\n // Discard material-swap requests enqueued during scene SETUP — a mesh added, then re-materialed before boot\n // via the mesh.material setter (e.g. scene12 assigns each row's material AFTER addToScene). The deferred\n // builders below build every group's meshes fresh with their FINAL material, so those swaps are redundant;\n // processing them would insert a SECOND renderable per mesh (double-draw). Only swaps enqueued DURING the\n // drain below — an async mesh that joins an already-built group (see addToScene/_builtGroups) — must survive.\n ctx._materialSwapQueue.length = 0;\n while (ctx._deferredBuilders.length > 0) {\n const builders = [...ctx._deferredBuilders];\n ctx._deferredBuilders = [];\n await Promise.all(builders.map(async (b) => b()));\n }\n // Build the renderables for any meshes that joined an already-built group mid-drain (queued above) before\n // the first frame, instead of leaving them casting shadows but invisible in the color pass.\n processMaterialSwaps(ctx);\n ctx._renderableVersion++;\n ctx._built = true;\n}\n\n/**\n * Register a scene with the engine. Builds deferred work, sorts renderables by order,\n * and adds the scene to its bound surface's render list in overlay order. The scene is\n * always attached to `scene.surface` (which equals the engine itself in the\n * single-canvas case).\n */\nexport async function registerScene(scene: SceneContext): Promise<void> {\n const ctx = scene;\n const surface = ctx.surface;\n if (isRenderingContextRegistered(surface, ctx)) {\n return;\n }\n await buildScene(scene);\n ctx._renderables.sort(byOrder);\n await Promise.all(ctx._frameGraph._tasks.map((task) => task._preload?.()).filter((preload): preload is Promise<void> => preload !== undefined));\n ctx._frameGraph.build();\n if (surface._renderingContexts.length > 0) {\n const overlay = await import(\"./swapchain-overlay.js\");\n overlay.configureSwapchainOverlayScene(surface, ctx);\n }\n registerRenderingContext(surface, ctx);\n}\n\n/**\n * Register a scene with the engine and install the scene-owned shadow frame-graph task.\n * Use only for scenes that generate shadow maps. Like {@link registerScene}, the scene\n * is attached to `scene.surface` (and its owning engine is `scene.surface.engine`).\n */\nexport async function registerSceneWithShadowSupport(scene: SceneContext): Promise<void> {\n const ctx = scene as SceneContext;\n const surface = ctx.surface;\n if (isRenderingContextRegistered(surface, ctx)) {\n return;\n }\n await buildScene(scene);\n ctx._renderables.sort(byOrder);\n await ensureShadowTask(surface.engine, ctx);\n await Promise.all(ctx._frameGraph._tasks.map((task) => task._preload?.()).filter((preload): preload is Promise<void> => preload !== undefined));\n ctx._frameGraph.build();\n if (surface._renderingContexts.length > 0) {\n const overlay = await import(\"./swapchain-overlay.js\");\n overlay.configureSwapchainOverlayScene(surface, ctx);\n }\n registerRenderingContext(surface, ctx);\n}\n\nconst byOrder = (a: Renderable, b: Renderable): number => a.order - b.order;\n\nasync function ensureShadowTask(engine: EngineContext, scene: SceneContext): Promise<void> {\n // Idempotent: the scene keeps its `_frameGraph` (and its `_tasks`) across unregister/re-register\n // cycles, and `buildScene` does not clear the task list — so a plain unshift would stack a new\n // shadow task on every re-registration. Only add one when the scene has none.\n if (scene._frameGraph._tasks.some((task) => task.name === \"shadow\")) {\n return;\n }\n const { createShadowTask } = await import(\"../frame-graph/shadow-task.js\");\n scene._frameGraph._tasks.unshift(createShadowTask(engine, scene));\n}\n\n/** Remove a previously-registered scene. Idempotent. Does not dispose scene resources.\n * The scene is always removed from `scene.surface`. */\nexport function unregisterScene(scene: SceneContext): void {\n unregisterRenderingContext(scene.surface, scene as SceneContext);\n}\n"],"names":[],"mappings":"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"}
@@ -1,8 +1,6 @@
1
- import { mat4Compose } from '../math/mat4-compose.js';
2
- import { mat4Identity } from '../math/mat4-identity.js';
3
1
  import { ObservableVec3 } from '../math/observable-vec3.js';
4
2
  import { ObservableQuat } from '../math/observable-quat.js';
5
- import { createWorldMatrixState, attachWorldMatrixState } from './world-matrix-state.js';
3
+ import { createWorldMatrixState, attachWorldMatrixState, composeTrsLocalMatrix } from './world-matrix-state.js';
6
4
 
7
5
  function eulerToQuat(rx, ry, rz) {
8
6
  const cx = Math.cos(rx * 0.5), sx_ = Math.sin(rx * 0.5);
@@ -78,9 +76,7 @@ function createSceneNodeCore(name, matrix, px = 0, py = 0, pz = 0, qx = 0, qy =
78
76
  if (matrix) {
79
77
  return matrix;
80
78
  }
81
- const p = node.position, rq2 = node.rotationQuaternion, s = node.scaling;
82
- const isIdentity = p.x === 0 && p.y === 0 && p.z === 0 && rq2.x === 0 && rq2.y === 0 && rq2.z === 0 && rq2.w === 1 && s.x === 1 && s.y === 1 && s.z === 1;
83
- return isIdentity ? mat4Identity() : mat4Compose(p.x, p.y, p.z, rq2.x, rq2.y, rq2.z, rq2.w, s.x, s.y, s.z);
79
+ return composeTrsLocalMatrix(node.position, node.rotationQuaternion, node.scaling);
84
80
  });
85
81
  const onWmDirty = () => {
86
82
  if (!matrix) {
@@ -1 +1 @@
1
- {"version":3,"file":"scene-node.js","sources":["../../../src/scene/scene-node.ts"],"sourcesContent":["/** SceneNode — common base for all scene entities with TRS, parent, and children.\n *\n * Provides position, rotationQuaternion (source of truth), rotation (Euler XYZ proxy),\n * scaling, parent, worldMatrix, worldMatrixVersion, and children. */\n\nimport type { Mat4 } from \"../math/types.js\";\nimport type { LiteMetadata } from \"../metadata.js\";\nimport type { IWorldMatrixProvider } from \"./parentable.js\";\nimport { mat4Compose } from \"../math/mat4-compose.js\";\nimport { mat4Identity } from \"../math/mat4-identity.js\";\nimport { ObservableVec3 } from \"../math/observable-vec3.js\";\nimport { ObservableQuat } from \"../math/observable-quat.js\";\nimport { createWorldMatrixState, attachWorldMatrixState } from \"./world-matrix-state.js\";\n\n// ─── EulerProxy ──────────────────────────────────────────────────────\n\n/** Bidirectional Euler XYZ view over a quaternion.\n * Reads decompose the current quaternion on the fly; writes convert Euler→quat atomically. */\nexport interface EulerProxy {\n x: number;\n y: number;\n z: number;\n set(x: number, y: number, z: number): void;\n}\n\n// ─── SceneNode ───────────────────────────────────────────────────────\n\n/** Common base for all scene entities: TRS transform, parent/children hierarchy, and a cached world matrix. */\nexport interface SceneNode {\n name: string;\n children: SceneNode[];\n position: ObservableVec3;\n /** Quaternion rotation — source of truth for the local matrix. */\n rotationQuaternion: ObservableQuat;\n /** Euler XYZ bidirectional proxy — reads decompose current quat; writes update quat atomically. */\n rotation: EulerProxy;\n scaling: ObservableVec3;\n parent: IWorldMatrixProvider | null;\n readonly worldMatrix: Mat4;\n readonly worldMatrixVersion: number;\n /** @internal Raw local matrix for glTF matrix nodes. */\n _localMatrix?: Mat4;\n /** Self-visibility. Undefined/true = visible; `false` skips render + camera AABB.\n * Cascade is materialized at write-time by `setSubtreeVisible`. */\n visible?: boolean;\n /** User metadata. glTF loads populate `metadata.gltf.extras` when source extras exist. */\n metadata?: LiteMetadata;\n}\n\n// ─── Math helpers ─────────────────────────────────────────────────────\n\n/** Euler XYZ → quaternion (intrinsic XYZ order). */\nexport function eulerToQuat(rx: number, ry: number, rz: number): [number, number, number, number] {\n const cx = Math.cos(rx * 0.5),\n sx_ = Math.sin(rx * 0.5);\n const cy = Math.cos(ry * 0.5),\n sy_ = Math.sin(ry * 0.5);\n const cz = Math.cos(rz * 0.5),\n sz_ = Math.sin(rz * 0.5);\n return [sx_ * cy * cz + cx * sy_ * sz_, cx * sy_ * cz - sx_ * cy * sz_, cx * cy * sz_ + sx_ * sy_ * cz, cx * cy * cz - sx_ * sy_ * sz_];\n}\n\n/** Quaternion → Euler XYZ (inverse of eulerToQuat). */\nexport function quatToEulerXYZ(qx: number, qy: number, qz: number, qw: number): [number, number, number] {\n const sinY = 2 * (qx * qz + qw * qy);\n const ry = Math.asin(Math.max(-1, Math.min(1, sinY)));\n const rx = Math.atan2(-(2 * (qy * qz - qw * qx)), 1 - 2 * (qx * qx + qy * qy));\n const rz = Math.atan2(-(2 * (qx * qy - qw * qz)), 1 - 2 * (qy * qy + qz * qz));\n return [rx, ry, rz];\n}\n\n/** Create a live bidirectional EulerProxy backed by the given ObservableQuat.\n *\n * Euler⇄quaternion is many-to-one and `quatToEulerXYZ` is unstable at gimbal lock\n * (e.g. yaw near ±π/2), so re-deriving Euler from the quaternion on every read makes\n * per-axis updates (`node.rotation.x = …; node.rotation.y = …`) lossy and can flip the\n * node. To stay stable, the proxy caches the Euler triple it last applied and reuses it\n * while the quaternion is unchanged; it only re-derives from the quaternion when the\n * quaternion was written externally (detected via its version counter). */\nexport function createEulerProxy(rq: ObservableQuat): EulerProxy {\n let ex = 0;\n let ey = 0;\n let ez = 0;\n // Snapshot of rq.version at the last sync. -1 forces an initial derive.\n let syncedVersion = -1;\n\n const sync = (): void => {\n if (rq.version !== syncedVersion) {\n const e = quatToEulerXYZ(rq.x, rq.y, rq.z, rq.w);\n ex = e[0];\n ey = e[1];\n ez = e[2];\n syncedVersion = rq.version;\n }\n };\n\n const apply = (x: number, y: number, z: number): void => {\n ex = x;\n ey = y;\n ez = z;\n const [a, b, c, d] = eulerToQuat(x, y, z);\n rq.set(a, b, c, d);\n // The cached Euler is authoritative for this quaternion value, so adopt the\n // version we just produced — avoids an immediate lossy re-derive on next read.\n syncedVersion = rq.version;\n };\n\n return {\n get x() {\n sync();\n return ex;\n },\n set x(v: number) {\n sync();\n apply(v, ey, ez);\n },\n get y() {\n sync();\n return ey;\n },\n set y(v: number) {\n sync();\n apply(ex, v, ez);\n },\n get z() {\n sync();\n return ez;\n },\n set z(v: number) {\n sync();\n apply(ex, ey, v);\n },\n set: apply,\n };\n}\n\n// ─── Factory ──────────────────────────────────────────────────────────\n\n/** Create a SceneNode with given TRS (position and scaling in cartesian, rotation as quaternion). */\nexport function createSceneNode(name: string, px = 0, py = 0, pz = 0, qx = 0, qy = 0, qz = 0, qw = 1, sx = 1, sy = 1, sz = 1): SceneNode {\n return createSceneNodeCore(name, null, px, py, pz, qx, qy, qz, qw, sx, sy, sz);\n}\n\nexport function createSceneNodeFromMatrix(name: string, matrix: Mat4): SceneNode {\n return createSceneNodeCore(name, matrix);\n}\n\nfunction createSceneNodeCore(name: string, matrix: Mat4 | null, px = 0, py = 0, pz = 0, qx = 0, qy = 0, qz = 0, qw = 1, sx = 1, sy = 1, sz = 1): SceneNode {\n const wm = createWorldMatrixState(() => {\n if (matrix) {\n return matrix;\n }\n const p = node.position,\n rq = node.rotationQuaternion,\n s = node.scaling;\n const isIdentity = p.x === 0 && p.y === 0 && p.z === 0 && rq.x === 0 && rq.y === 0 && rq.z === 0 && rq.w === 1 && s.x === 1 && s.y === 1 && s.z === 1;\n return isIdentity ? mat4Identity() : mat4Compose(p.x, p.y, p.z, rq.x, rq.y, rq.z, rq.w, s.x, s.y, s.z);\n });\n const onWmDirty = () => {\n if (!matrix) {\n wm.markLocalDirty();\n }\n };\n\n const rq = new ObservableQuat(qx, qy, qz, qw, onWmDirty);\n\n const node: SceneNode = {\n name,\n children: [],\n position: new ObservableVec3(px, py, pz, onWmDirty),\n rotationQuaternion: rq,\n rotation: createEulerProxy(rq),\n scaling: new ObservableVec3(sx, sy, sz, onWmDirty),\n get parent() {\n return wm.parent;\n },\n set parent(v) {\n wm.parent = v;\n },\n get worldMatrix() {\n return wm.getWorldMatrix();\n },\n get worldMatrixVersion() {\n return wm.getWorldMatrixVersion();\n },\n };\n if (matrix) {\n node._localMatrix = matrix;\n }\n attachWorldMatrixState(node, wm);\n return node;\n}\n"],"names":["rq"],"mappings":";;;;;;AAoDO,SAAS,WAAA,CAAY,EAAA,EAAY,EAAA,EAAY,EAAA,EAA8C;AAC9F,EAAA,MAAM,EAAA,GAAK,IAAA,CAAK,GAAA,CAAI,EAAA,GAAK,GAAG,GACxB,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,EAAA,GAAK,GAAG,CAAA;AAC3B,EAAA,MAAM,EAAA,GAAK,IAAA,CAAK,GAAA,CAAI,EAAA,GAAK,GAAG,GACxB,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,EAAA,GAAK,GAAG,CAAA;AAC3B,EAAA,MAAM,EAAA,GAAK,IAAA,CAAK,GAAA,CAAI,EAAA,GAAK,GAAG,GACxB,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,EAAA,GAAK,GAAG,CAAA;AAC3B,EAAA,OAAO,CAAC,GAAA,GAAM,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK,MAAM,GAAA,EAAK,EAAA,GAAK,GAAA,GAAM,EAAA,GAAK,GAAA,GAAM,EAAA,GAAK,KAAK,EAAA,GAAK,EAAA,GAAK,GAAA,GAAM,GAAA,GAAM,GAAA,GAAM,EAAA,EAAI,KAAK,EAAA,GAAK,EAAA,GAAK,GAAA,GAAM,GAAA,GAAM,GAAG,CAAA;AAC1I;AAGO,SAAS,cAAA,CAAe,EAAA,EAAY,EAAA,EAAY,EAAA,EAAY,EAAA,EAAsC;AACrG,EAAA,MAAM,IAAA,GAAO,CAAA,IAAK,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK,EAAA,CAAA;AACjC,EAAA,MAAM,EAAA,GAAK,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAI,CAAC,CAAC,CAAA;AACpD,EAAA,MAAM,EAAA,GAAK,IAAA,CAAK,KAAA,CAAM,EAAE,KAAK,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK,EAAA,CAAA,CAAA,EAAM,CAAA,GAAI,CAAA,IAAK,EAAA,GAAK,EAAA,GAAK,KAAK,EAAA,CAAG,CAAA;AAC7E,EAAA,MAAM,EAAA,GAAK,IAAA,CAAK,KAAA,CAAM,EAAE,KAAK,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK,EAAA,CAAA,CAAA,EAAM,CAAA,GAAI,CAAA,IAAK,EAAA,GAAK,EAAA,GAAK,KAAK,EAAA,CAAG,CAAA;AAC7E,EAAA,OAAO,CAAC,EAAA,EAAI,EAAA,EAAI,EAAE,CAAA;AACtB;AAUO,SAAS,iBAAiB,EAAA,EAAgC;AAC7D,EAAA,IAAI,EAAA,GAAK,CAAA;AACT,EAAA,IAAI,EAAA,GAAK,CAAA;AACT,EAAA,IAAI,EAAA,GAAK,CAAA;AAET,EAAA,IAAI,aAAA,GAAgB,EAAA;AAEpB,EAAA,MAAM,OAAO,MAAY;AACrB,IAAA,IAAI,EAAA,CAAG,YAAY,aAAA,EAAe;AAC9B,MAAA,MAAM,CAAA,GAAI,eAAe,EAAA,CAAG,CAAA,EAAG,GAAG,CAAA,EAAG,EAAA,CAAG,CAAA,EAAG,EAAA,CAAG,CAAC,CAAA;AAC/C,MAAA,EAAA,GAAK,EAAE,CAAC,CAAA;AACR,MAAA,EAAA,GAAK,EAAE,CAAC,CAAA;AACR,MAAA,EAAA,GAAK,EAAE,CAAC,CAAA;AACR,MAAA,aAAA,GAAgB,EAAA,CAAG,OAAA;AAAA,IACvB;AAAA,EACJ,CAAA;AAEA,EAAA,MAAM,KAAA,GAAQ,CAAC,CAAA,EAAW,CAAA,EAAW,CAAA,KAAoB;AACrD,IAAA,EAAA,GAAK,CAAA;AACL,IAAA,EAAA,GAAK,CAAA;AACL,IAAA,EAAA,GAAK,CAAA;AACL,IAAA,MAAM,CAAC,GAAG,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA,GAAI,WAAA,CAAY,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AACxC,IAAA,EAAA,CAAG,GAAA,CAAI,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAGjB,IAAA,aAAA,GAAgB,EAAA,CAAG,OAAA;AAAA,EACvB,CAAA;AAEA,EAAA,OAAO;AAAA,IACH,IAAI,CAAA,GAAI;AACJ,MAAA,IAAA,EAAK;AACL,MAAA,OAAO,EAAA;AAAA,IACX,CAAA;AAAA,IACA,IAAI,EAAE,CAAA,EAAW;AACb,MAAA,IAAA,EAAK;AACL,MAAA,KAAA,CAAM,CAAA,EAAG,IAAI,EAAE,CAAA;AAAA,IACnB,CAAA;AAAA,IACA,IAAI,CAAA,GAAI;AACJ,MAAA,IAAA,EAAK;AACL,MAAA,OAAO,EAAA;AAAA,IACX,CAAA;AAAA,IACA,IAAI,EAAE,CAAA,EAAW;AACb,MAAA,IAAA,EAAK;AACL,MAAA,KAAA,CAAM,EAAA,EAAI,GAAG,EAAE,CAAA;AAAA,IACnB,CAAA;AAAA,IACA,IAAI,CAAA,GAAI;AACJ,MAAA,IAAA,EAAK;AACL,MAAA,OAAO,EAAA;AAAA,IACX,CAAA;AAAA,IACA,IAAI,EAAE,CAAA,EAAW;AACb,MAAA,IAAA,EAAK;AACL,MAAA,KAAA,CAAM,EAAA,EAAI,IAAI,CAAC,CAAA;AAAA,IACnB,CAAA;AAAA,IACA,GAAA,EAAK;AAAA,GACT;AACJ;AAKO,SAAS,eAAA,CAAgB,MAAc,EAAA,GAAK,CAAA,EAAG,KAAK,CAAA,EAAG,EAAA,GAAK,GAAG,EAAA,GAAK,CAAA,EAAG,KAAK,CAAA,EAAG,EAAA,GAAK,GAAG,EAAA,GAAK,CAAA,EAAG,KAAK,CAAA,EAAG,EAAA,GAAK,CAAA,EAAG,EAAA,GAAK,CAAA,EAAc;AACrI,EAAA,OAAO,mBAAA,CAAoB,IAAA,EAAM,IAAA,EAAM,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAE,CAAA;AACjF;AAEO,SAAS,yBAAA,CAA0B,MAAc,MAAA,EAAyB;AAC7E,EAAA,OAAO,mBAAA,CAAoB,MAAM,MAAM,CAAA;AAC3C;AAEA,SAAS,mBAAA,CAAoB,MAAc,MAAA,EAAqB,EAAA,GAAK,GAAG,EAAA,GAAK,CAAA,EAAG,EAAA,GAAK,CAAA,EAAG,EAAA,GAAK,CAAA,EAAG,KAAK,CAAA,EAAG,EAAA,GAAK,GAAG,EAAA,GAAK,CAAA,EAAG,KAAK,CAAA,EAAG,EAAA,GAAK,CAAA,EAAG,EAAA,GAAK,CAAA,EAAc;AACvJ,EAAA,MAAM,EAAA,GAAK,uBAAuB,MAAM;AACpC,IAAA,IAAI,MAAA,EAAQ;AACR,MAAA,OAAO,MAAA;AAAA,IACX;AACA,IAAA,MAAM,IAAI,IAAA,CAAK,QAAA,EACXA,MAAK,IAAA,CAAK,kBAAA,EACV,IAAI,IAAA,CAAK,OAAA;AACb,IAAA,MAAM,UAAA,GAAa,CAAA,CAAE,CAAA,KAAM,CAAA,IAAK,CAAA,CAAE,CAAA,KAAM,CAAA,IAAK,CAAA,CAAE,CAAA,KAAM,CAAA,IAAKA,GAAAA,CAAG,CAAA,KAAM,CAAA,IAAKA,GAAAA,CAAG,CAAA,KAAM,CAAA,IAAKA,GAAAA,CAAG,CAAA,KAAM,CAAA,IAAKA,GAAAA,CAAG,CAAA,KAAM,CAAA,IAAK,CAAA,CAAE,CAAA,KAAM,CAAA,IAAK,CAAA,CAAE,CAAA,KAAM,CAAA,IAAK,EAAE,CAAA,KAAM,CAAA;AACpJ,IAAA,OAAO,UAAA,GAAa,cAAa,GAAI,WAAA,CAAY,EAAE,CAAA,EAAG,CAAA,CAAE,CAAA,EAAG,CAAA,CAAE,CAAA,EAAGA,GAAAA,CAAG,GAAGA,GAAAA,CAAG,CAAA,EAAGA,GAAAA,CAAG,CAAA,EAAGA,GAAAA,CAAG,CAAA,EAAG,EAAE,CAAA,EAAG,CAAA,CAAE,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA;AAAA,EACzG,CAAC,CAAA;AACD,EAAA,MAAM,YAAY,MAAM;AACpB,IAAA,IAAI,CAAC,MAAA,EAAQ;AACT,MAAA,EAAA,CAAG,cAAA,EAAe;AAAA,IACtB;AAAA,EACJ,CAAA;AAEA,EAAA,MAAM,KAAK,IAAI,cAAA,CAAe,IAAI,EAAA,EAAI,EAAA,EAAI,IAAI,SAAS,CAAA;AAEvD,EAAA,MAAM,IAAA,GAAkB;AAAA,IACpB,IAAA;AAAA,IACA,UAAU,EAAC;AAAA,IACX,UAAU,IAAI,cAAA,CAAe,EAAA,EAAI,EAAA,EAAI,IAAI,SAAS,CAAA;AAAA,IAClD,kBAAA,EAAoB,EAAA;AAAA,IACpB,QAAA,EAAU,iBAAiB,EAAE,CAAA;AAAA,IAC7B,SAAS,IAAI,cAAA,CAAe,EAAA,EAAI,EAAA,EAAI,IAAI,SAAS,CAAA;AAAA,IACjD,IAAI,MAAA,GAAS;AACT,MAAA,OAAO,EAAA,CAAG,MAAA;AAAA,IACd,CAAA;AAAA,IACA,IAAI,OAAO,CAAA,EAAG;AACV,MAAA,EAAA,CAAG,MAAA,GAAS,CAAA;AAAA,IAChB,CAAA;AAAA,IACA,IAAI,WAAA,GAAc;AACd,MAAA,OAAO,GAAG,cAAA,EAAe;AAAA,IAC7B,CAAA;AAAA,IACA,IAAI,kBAAA,GAAqB;AACrB,MAAA,OAAO,GAAG,qBAAA,EAAsB;AAAA,IACpC;AAAA,GACJ;AACA,EAAA,IAAI,MAAA,EAAQ;AACR,IAAA,IAAA,CAAK,YAAA,GAAe,MAAA;AAAA,EACxB;AACA,EAAA,sBAAA,CAAuB,MAAM,EAAE,CAAA;AAC/B,EAAA,OAAO,IAAA;AACX;;;;"}
1
+ {"version":3,"file":"scene-node.js","sources":["../../../src/scene/scene-node.ts"],"sourcesContent":["/** SceneNode — common base for all scene entities with TRS, parent, and children.\n *\n * Provides position, rotationQuaternion (source of truth), rotation (Euler XYZ proxy),\n * scaling, parent, worldMatrix, worldMatrixVersion, and children. */\n\nimport type { Mat4 } from \"../math/types.js\";\nimport type { LiteMetadata } from \"../metadata.js\";\nimport type { IWorldMatrixProvider } from \"./parentable.js\";\nimport { ObservableVec3 } from \"../math/observable-vec3.js\";\nimport { ObservableQuat } from \"../math/observable-quat.js\";\nimport { createWorldMatrixState, attachWorldMatrixState, composeTrsLocalMatrix } from \"./world-matrix-state.js\";\n\n// ─── EulerProxy ──────────────────────────────────────────────────────\n\n/** Bidirectional Euler XYZ view over a quaternion.\n * Reads decompose the current quaternion on the fly; writes convert Euler→quat atomically. */\nexport interface EulerProxy {\n x: number;\n y: number;\n z: number;\n set(x: number, y: number, z: number): void;\n}\n\n// ─── SceneNode ───────────────────────────────────────────────────────\n\n/** Common base for all scene entities: TRS transform, parent/children hierarchy, and a cached world matrix. */\nexport interface SceneNode {\n name: string;\n children: SceneNode[];\n position: ObservableVec3;\n /** Quaternion rotation — source of truth for the local matrix. */\n rotationQuaternion: ObservableQuat;\n /** Euler XYZ bidirectional proxy — reads decompose current quat; writes update quat atomically. */\n rotation: EulerProxy;\n scaling: ObservableVec3;\n parent: IWorldMatrixProvider | null;\n readonly worldMatrix: Mat4;\n readonly worldMatrixVersion: number;\n /** @internal Raw local matrix for glTF matrix nodes. */\n _localMatrix?: Mat4;\n /** Self-visibility. Undefined/true = visible; `false` skips render + camera AABB.\n * Cascade is materialized at write-time by `setSubtreeVisible`. */\n visible?: boolean;\n /** User metadata. glTF loads populate `metadata.gltf.extras` when source extras exist. */\n metadata?: LiteMetadata;\n}\n\n// ─── Math helpers ─────────────────────────────────────────────────────\n\n/** Euler XYZ → quaternion (intrinsic XYZ order). */\nexport function eulerToQuat(rx: number, ry: number, rz: number): [number, number, number, number] {\n const cx = Math.cos(rx * 0.5),\n sx_ = Math.sin(rx * 0.5);\n const cy = Math.cos(ry * 0.5),\n sy_ = Math.sin(ry * 0.5);\n const cz = Math.cos(rz * 0.5),\n sz_ = Math.sin(rz * 0.5);\n return [sx_ * cy * cz + cx * sy_ * sz_, cx * sy_ * cz - sx_ * cy * sz_, cx * cy * sz_ + sx_ * sy_ * cz, cx * cy * cz - sx_ * sy_ * sz_];\n}\n\n/** Quaternion → Euler XYZ (inverse of eulerToQuat). */\nexport function quatToEulerXYZ(qx: number, qy: number, qz: number, qw: number): [number, number, number] {\n const sinY = 2 * (qx * qz + qw * qy);\n const ry = Math.asin(Math.max(-1, Math.min(1, sinY)));\n const rx = Math.atan2(-(2 * (qy * qz - qw * qx)), 1 - 2 * (qx * qx + qy * qy));\n const rz = Math.atan2(-(2 * (qx * qy - qw * qz)), 1 - 2 * (qy * qy + qz * qz));\n return [rx, ry, rz];\n}\n\n/** Create a live bidirectional EulerProxy backed by the given ObservableQuat.\n *\n * Euler⇄quaternion is many-to-one and `quatToEulerXYZ` is unstable at gimbal lock\n * (e.g. yaw near ±π/2), so re-deriving Euler from the quaternion on every read makes\n * per-axis updates (`node.rotation.x = …; node.rotation.y = …`) lossy and can flip the\n * node. To stay stable, the proxy caches the Euler triple it last applied and reuses it\n * while the quaternion is unchanged; it only re-derives from the quaternion when the\n * quaternion was written externally (detected via its version counter). */\nexport function createEulerProxy(rq: ObservableQuat): EulerProxy {\n let ex = 0;\n let ey = 0;\n let ez = 0;\n // Snapshot of rq.version at the last sync. -1 forces an initial derive.\n let syncedVersion = -1;\n\n const sync = (): void => {\n if (rq.version !== syncedVersion) {\n const e = quatToEulerXYZ(rq.x, rq.y, rq.z, rq.w);\n ex = e[0];\n ey = e[1];\n ez = e[2];\n syncedVersion = rq.version;\n }\n };\n\n const apply = (x: number, y: number, z: number): void => {\n ex = x;\n ey = y;\n ez = z;\n const [a, b, c, d] = eulerToQuat(x, y, z);\n rq.set(a, b, c, d);\n // The cached Euler is authoritative for this quaternion value, so adopt the\n // version we just produced — avoids an immediate lossy re-derive on next read.\n syncedVersion = rq.version;\n };\n\n return {\n get x() {\n sync();\n return ex;\n },\n set x(v: number) {\n sync();\n apply(v, ey, ez);\n },\n get y() {\n sync();\n return ey;\n },\n set y(v: number) {\n sync();\n apply(ex, v, ez);\n },\n get z() {\n sync();\n return ez;\n },\n set z(v: number) {\n sync();\n apply(ex, ey, v);\n },\n set: apply,\n };\n}\n\n// ─── Factory ──────────────────────────────────────────────────────────\n\n/** Create a SceneNode with given TRS (position and scaling in cartesian, rotation as quaternion). */\nexport function createSceneNode(name: string, px = 0, py = 0, pz = 0, qx = 0, qy = 0, qz = 0, qw = 1, sx = 1, sy = 1, sz = 1): SceneNode {\n return createSceneNodeCore(name, null, px, py, pz, qx, qy, qz, qw, sx, sy, sz);\n}\n\nexport function createSceneNodeFromMatrix(name: string, matrix: Mat4): SceneNode {\n return createSceneNodeCore(name, matrix);\n}\n\nfunction createSceneNodeCore(name: string, matrix: Mat4 | null, px = 0, py = 0, pz = 0, qx = 0, qy = 0, qz = 0, qw = 1, sx = 1, sy = 1, sz = 1): SceneNode {\n const wm = createWorldMatrixState(() => {\n if (matrix) {\n return matrix;\n }\n return composeTrsLocalMatrix(node.position, node.rotationQuaternion, node.scaling);\n });\n const onWmDirty = () => {\n if (!matrix) {\n wm.markLocalDirty();\n }\n };\n\n const rq = new ObservableQuat(qx, qy, qz, qw, onWmDirty);\n\n const node: SceneNode = {\n name,\n children: [],\n position: new ObservableVec3(px, py, pz, onWmDirty),\n rotationQuaternion: rq,\n rotation: createEulerProxy(rq),\n scaling: new ObservableVec3(sx, sy, sz, onWmDirty),\n get parent() {\n return wm.parent;\n },\n set parent(v) {\n wm.parent = v;\n },\n get worldMatrix() {\n return wm.getWorldMatrix();\n },\n get worldMatrixVersion() {\n return wm.getWorldMatrixVersion();\n },\n };\n if (matrix) {\n node._localMatrix = matrix;\n }\n attachWorldMatrixState(node, wm);\n return node;\n}\n"],"names":[],"mappings":"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"}
@@ -36,6 +36,9 @@ function cloneTransformNode(src) {
36
36
  return clone;
37
37
  }
38
38
  function cloneMeshNode(mesh) {
39
+ if (mesh._clone) {
40
+ throw Error(mesh._clone);
41
+ }
39
42
  const meshClone = {
40
43
  ...mesh,
41
44
  name: mesh.name + "_clone",
@@ -1 +1 @@
1
- {"version":3,"file":"transform-node.js","sources":["../../../src/scene/transform-node.ts"],"sourcesContent":["/** TransformNode — alias for SceneNode. A scene graph node with TRS, parent, and children.\n *\n * TransformNode is now a pure type alias for SceneNode, giving all scene entities\n * a common base. createTransformNode delegates to createSceneNode. */\n\nimport type { Mesh } from \"../mesh/mesh.js\";\nimport { initMeshTransform } from \"../mesh/mesh.js\";\nimport { retain } from \"../resource/ref-count.js\";\nimport type { SceneNode } from \"./scene-node.js\";\nimport { createSceneNode, createSceneNodeFromMatrix } from \"./scene-node.js\";\n\nexport type { SceneNode } from \"./scene-node.js\";\n\n/** TransformNode is a SceneNode — pure type alias, no extra fields. */\nexport type TransformNode = SceneNode;\n\n/** Create a TransformNode (SceneNode) with TRS values and lazy world matrix.\n * Parameters: name, position (px,py,pz), rotation quaternion (qx,qy,qz,qw), scaling (sx,sy,sz). */\nexport function createTransformNode(name: string, px = 0, py = 0, pz = 0, qx = 0, qy = 0, qz = 0, qw = 1, sx = 1, sy = 1, sz = 1): TransformNode {\n return createSceneNode(name, px, py, pz, qx, qy, qz, qw, sx, sy, sz);\n}\n\n/** Deep-clone a SceneNode tree. Meshes are shallow-cloned (shared GPU buffers — see\n * `cloneMeshNode` and `resource/ref-count.ts` for how disposal safely handles the sharing).\n * Lights, cameras, and other non-mesh/non-TN children are shallow-cloned. */\nexport function cloneTransformNode(src: SceneNode): SceneNode {\n if (\"_gpu\" in src) {\n return cloneMeshNode(src as unknown as Mesh);\n }\n\n const clone = src._localMatrix\n ? createSceneNodeFromMatrix(src.name + \"_clone\", src._localMatrix)\n : createTransformNode(\n src.name + \"_clone\",\n src.position.x,\n src.position.y,\n src.position.z,\n src.rotationQuaternion.x,\n src.rotationQuaternion.y,\n src.rotationQuaternion.z,\n src.rotationQuaternion.w,\n src.scaling.x,\n src.scaling.y,\n src.scaling.z\n );\n for (const child of src.children) {\n if (!(\"lightType\" in child)) {\n const childClone = cloneTransformNode(child);\n childClone.parent = clone;\n clone.children.push(childClone);\n } else {\n // Lights, cameras, other node types — shallow clone with fresh children array\n const childClone = { ...(child as Record<string, unknown>), name: (child as SceneNode).name + \"_clone\", children: [] } as unknown as SceneNode;\n childClone.parent = clone;\n clone.children.push(childClone);\n }\n }\n return clone;\n}\n\nfunction cloneMeshNode(mesh: Mesh): Mesh {\n const meshClone = {\n ...mesh,\n name: mesh.name + \"_clone\",\n children: [],\n // Share the SAME `_gpu` object (not a copy) — the wrapper's identity is the ref-count\n // key in resource/ref-count.ts, so both meshes must point at the exact same instance\n // for `disposeMeshGpu` to know they're co-owners of the underlying GPUBuffers.\n _gpu: mesh._gpu,\n } as unknown as Mesh;\n // `skeleton`/`vat`/`morphTargets`/`thinInstances` are already shared by reference via the `...mesh`\n // spread above. Register the extra ownership for all shared GPU resources so `disposeMeshGpu`\n // only destroys their buffers once the LAST owning mesh (source or clone) releases them —\n // otherwise disposing one mesh would free buffers the other still renders with.\n for (const r of [mesh._gpu, mesh.skeleton, mesh.vat, mesh.morphTargets, mesh.thinInstances]) {\n if (r) {\n retain(r);\n }\n }\n initMeshTransform(meshClone, mesh.position.x, mesh.position.y, mesh.position.z, 0, 0, 0, mesh.scaling.x, mesh.scaling.y, mesh.scaling.z);\n // Copy the source rotation as a QUATERNION — the Euler round-trip (mesh.rotation.x/y/z) is lossy\n // near gimbal lock and would skew the clone. set() marks the world matrix dirty so it recomputes.\n const rq = mesh.rotationQuaternion;\n meshClone.rotationQuaternion.set(rq.x, rq.y, rq.z, rq.w);\n for (const child of mesh.children) {\n const childClone = cloneTransformNode(child);\n childClone.parent = meshClone;\n meshClone.children.push(childClone);\n }\n return meshClone;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transform-node.js","sources":["../../../src/scene/transform-node.ts"],"sourcesContent":["/** TransformNode — alias for SceneNode. A scene graph node with TRS, parent, and children.\n *\n * TransformNode is now a pure type alias for SceneNode, giving all scene entities\n * a common base. createTransformNode delegates to createSceneNode. */\n\nimport type { Mesh } from \"../mesh/mesh.js\";\nimport { initMeshTransform } from \"../mesh/mesh.js\";\nimport { retain } from \"../resource/ref-count.js\";\nimport type { SceneNode } from \"./scene-node.js\";\nimport { createSceneNode, createSceneNodeFromMatrix } from \"./scene-node.js\";\n\nexport type { SceneNode } from \"./scene-node.js\";\n\n/** TransformNode is a SceneNode — pure type alias, no extra fields. */\nexport type TransformNode = SceneNode;\n\n/** Create a TransformNode (SceneNode) with TRS values and lazy world matrix.\n * Parameters: name, position (px,py,pz), rotation quaternion (qx,qy,qz,qw), scaling (sx,sy,sz). */\nexport function createTransformNode(name: string, px = 0, py = 0, pz = 0, qx = 0, qy = 0, qz = 0, qw = 1, sx = 1, sy = 1, sz = 1): TransformNode {\n return createSceneNode(name, px, py, pz, qx, qy, qz, qw, sx, sy, sz);\n}\n\n/** Deep-clone a SceneNode tree. Meshes are shallow-cloned (shared GPU buffers — see\n * `cloneMeshNode` and `resource/ref-count.ts` for how disposal safely handles the sharing).\n * Lights, cameras, and other non-mesh/non-TN children are shallow-cloned. */\nexport function cloneTransformNode(src: SceneNode): SceneNode {\n if (\"_gpu\" in src) {\n return cloneMeshNode(src as unknown as Mesh);\n }\n\n const clone = src._localMatrix\n ? createSceneNodeFromMatrix(src.name + \"_clone\", src._localMatrix)\n : createTransformNode(\n src.name + \"_clone\",\n src.position.x,\n src.position.y,\n src.position.z,\n src.rotationQuaternion.x,\n src.rotationQuaternion.y,\n src.rotationQuaternion.z,\n src.rotationQuaternion.w,\n src.scaling.x,\n src.scaling.y,\n src.scaling.z\n );\n for (const child of src.children) {\n if (!(\"lightType\" in child)) {\n const childClone = cloneTransformNode(child);\n childClone.parent = clone;\n clone.children.push(childClone);\n } else {\n // Lights, cameras, other node types — shallow clone with fresh children array\n const childClone = { ...(child as Record<string, unknown>), name: (child as SceneNode).name + \"_clone\", children: [] } as unknown as SceneNode;\n childClone.parent = clone;\n clone.children.push(childClone);\n }\n }\n return clone;\n}\n\nfunction cloneMeshNode(mesh: Mesh): Mesh {\n if (mesh._clone) {\n throw Error(mesh._clone);\n }\n const meshClone = {\n ...mesh,\n name: mesh.name + \"_clone\",\n children: [],\n // Share the SAME `_gpu` object (not a copy) — the wrapper's identity is the ref-count\n // key in resource/ref-count.ts, so both meshes must point at the exact same instance\n // for `disposeMeshGpu` to know they're co-owners of the underlying GPUBuffers.\n _gpu: mesh._gpu,\n } as unknown as Mesh;\n // `skeleton`/`vat`/`morphTargets`/`thinInstances` are already shared by reference via the `...mesh`\n // spread above. Register the extra ownership for all shared GPU resources so `disposeMeshGpu`\n // only destroys their buffers once the LAST owning mesh (source or clone) releases them —\n // otherwise disposing one mesh would free buffers the other still renders with.\n for (const r of [mesh._gpu, mesh.skeleton, mesh.vat, mesh.morphTargets, mesh.thinInstances]) {\n if (r) {\n retain(r);\n }\n }\n initMeshTransform(meshClone, mesh.position.x, mesh.position.y, mesh.position.z, 0, 0, 0, mesh.scaling.x, mesh.scaling.y, mesh.scaling.z);\n // Copy the source rotation as a QUATERNION — the Euler round-trip (mesh.rotation.x/y/z) is lossy\n // near gimbal lock and would skew the clone. set() marks the world matrix dirty so it recomputes.\n const rq = mesh.rotationQuaternion;\n meshClone.rotationQuaternion.set(rq.x, rq.y, rq.z, rq.w);\n for (const child of mesh.children) {\n const childClone = cloneTransformNode(child);\n childClone.parent = meshClone;\n meshClone.children.push(childClone);\n }\n return meshClone;\n}\n"],"names":[],"mappings":"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"}
@@ -1,5 +1,7 @@
1
1
  import { mat4MultiplyInto } from '../math/mat4-multiply-into.js';
2
2
  import { allocateMat4 } from '../math/_matrix-allocator.js';
3
+ import { mat4Compose } from '../math/mat4-compose.js';
4
+ import { mat4Identity } from '../math/mat4-identity.js';
3
5
 
4
6
  const WM_STATE = Symbol("wmState");
5
7
  function attachWorldMatrixState(host, state) {
@@ -15,6 +17,10 @@ function peekWorldMatrixState(p) {
15
17
  function _markWorldMatrixDirty(host) {
16
18
  peekWorldMatrixState(host)?._invalidate();
17
19
  }
20
+ function composeTrsLocalMatrix(position, rotation, scaling) {
21
+ const isIdentity = position.x === 0 && position.y === 0 && position.z === 0 && rotation.x === 0 && rotation.y === 0 && rotation.z === 0 && rotation.w === 1 && scaling.x === 1 && scaling.y === 1 && scaling.z === 1;
22
+ return isIdentity ? mat4Identity() : mat4Compose(position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, rotation.w, scaling.x, scaling.y, scaling.z);
23
+ }
18
24
  function createWorldMatrixState(getLocalMatrix) {
19
25
  let _worldVersion = 0;
20
26
  let _lastSeenParentVersion = -1;
@@ -98,5 +104,5 @@ function createWorldMatrixState(getLocalMatrix) {
98
104
  return state;
99
105
  }
100
106
 
101
- export { _markWorldMatrixDirty, attachWorldMatrixState, createWorldMatrixState };
107
+ export { _markWorldMatrixDirty, attachWorldMatrixState, composeTrsLocalMatrix, createWorldMatrixState };
102
108
  //# sourceMappingURL=world-matrix-state.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"world-matrix-state.js","sources":["../../../src/scene/world-matrix-state.ts"],"sourcesContent":["/** Shared version-based lazy world matrix computation.\n *\n * Each entity provides only getLocalMatrix(). This module handles:\n * - version tracking (_worldVersion bumped by own TRS changes and ancestor motion)\n * - caching and staleness detection (_cachedWorld nulled on any change)\n * - PUSH dirty propagation: a transform write invalidates the node AND all of its\n * descendants up front, so per-frame consumers can read worldMatrixVersion in\n * O(1) (a plain field read) instead of walking the parent chain.\n *\n * Ancestor-only motion (e.g. animating a parent transform node) must bump a\n * descendant's worldMatrixVersion so per-frame consumers re-upload its UBO.\n * Rather than have each node PULL up its parent chain on every read, this module\n * keeps its own child registry (driven by the reliably-called `parent` setter, NOT\n * the host's `children` array which loaders/setParent maintain inconsistently) and\n * PUSHES invalidation down on every local change or reparent. Reads are O(1) (a\n * plain field read); writers pay an O(subtree) push. This is a clear win for the\n * common case (few movers, many readers) and never worse than the old pull walk.\n *\n * Foreign parents — an IWorldMatrixProvider that is not tagged with our state\n * symbol (e.g. a user-supplied object) — cannot be pushed to because we never see\n * their writes. For those we keep a cheap PULL fallback that polls the parent's\n * public version on read. All in-engine hosts (mesh, scene node, camera, light,\n * Gaussian-splatting mesh) ARE tagged, so engine hierarchies are pure push.\n *\n * Backing storage comes from `allocateMat4()` — process-global lazy singleton\n * in `_matrix-allocator.ts`. F32 by default; F64 after an HPM engine is\n * constructed (see `docs/lite/architecture/36-high-precision-matrix.md`). */\n\nimport type { Mat4 } from \"../math/types.js\";\nimport type { IWorldMatrixProvider } from \"./parentable.js\";\nimport { mat4MultiplyInto } from \"../math/mat4-multiply-into.js\";\nimport type { Mat4Storage } from \"../math/types.js\";\nimport { allocateMat4 } from \"../math/_matrix-allocator.js\";\n\nexport interface WorldMatrixAccessors {\n /** Getter — returns lazily computed world matrix. */\n getWorldMatrix(): Mat4;\n /** Getter — returns current version. */\n getWorldMatrixVersion(): number;\n /** Call when own TRS changes. Invalidates cache + subtree, forces recompute. */\n markLocalDirty(): void;\n /** Reference to parent — set directly. */\n parent: IWorldMatrixProvider | null;\n /** @internal Push: mark this node and its whole subtree dirty (clean-guarded). */\n _invalidate(): void;\n /** @internal Register a same-module child so future invalidations reach it. */\n _addChild(child: WorldMatrixAccessors): void;\n /** @internal Deregister a child (reparent away). */\n _removeChild(child: WorldMatrixAccessors): void;\n}\n\nconst WM_STATE = Symbol(\"wmState\");\n\n/** Tag a host object (mesh, scene node, camera, light, …) with its world-matrix\n * state so children can register for push invalidation and call its accessor\n * closures directly, bypassing the host's property getters. */\nexport function attachWorldMatrixState(host: object, state: WorldMatrixAccessors): void {\n (host as unknown as Record<symbol, unknown>)[WM_STATE] = state;\n}\n\nfunction peekWorldMatrixState(p: IWorldMatrixProvider | null): WorldMatrixAccessors | null {\n if (p === null) {\n return null;\n }\n const s = (p as unknown as Record<symbol, unknown>)[WM_STATE];\n return (s as WorldMatrixAccessors | undefined) ?? null;\n}\n\n/** @internal Bump one engine object's world version without changing its transform values. */\nexport function _markWorldMatrixDirty(host: IWorldMatrixProvider): void {\n peekWorldMatrixState(host)?._invalidate();\n}\n\n/**\n * Create world matrix state for any entity type.\n *\n * @param getLocalMatrix - Entity-specific function that returns the local (pre-parent)\n * transform matrix. Called only when the cache is stale.\n */\nexport function createWorldMatrixState(getLocalMatrix: () => Mat4): WorldMatrixAccessors {\n let _worldVersion = 0;\n let _lastSeenParentVersion = -1;\n let _cachedWorld: Mat4 | null = null;\n const _ownedWorld: Mat4 = allocateMat4();\n let _parent: IWorldMatrixProvider | null = null;\n let _parentState: WorldMatrixAccessors | null = null;\n const _children: WorldMatrixAccessors[] = [];\n\n // Mark this node — and, transitively, its whole subtree — dirty, bumping the\n // version so per-frame consumers (which gate UBO uploads on worldMatrixVersion)\n // re-upload. Propagation is eager and unconditional: consumers may never read a\n // pure transform node's world matrix, so a \"skip if already dirty\" guard would\n // strand its descendants on a stale version when an ancestor moves every frame.\n // Caching is still lazy — getWorldMatrix recomputes only when _cachedWorld is\n // null — so reads stay cheap; only writers pay the O(subtree) push.\n function invalidate(): void {\n _cachedWorld = null;\n _worldVersion++;\n for (const child of _children) {\n child._invalidate();\n }\n }\n\n // Foreign parent (not tagged with our symbol): we never observe its writes, so\n // poll its public version on read and push a subtree invalidation when it moved.\n function pollForeignParent(): void {\n const pv = (_parent as IWorldMatrixProvider).worldMatrixVersion;\n if (pv !== _lastSeenParentVersion) {\n _lastSeenParentVersion = pv;\n invalidate();\n }\n }\n\n const state: WorldMatrixAccessors = {\n get parent(): IWorldMatrixProvider | null {\n return _parent;\n },\n set parent(p: IWorldMatrixProvider | null) {\n if (p === _parent) {\n return;\n }\n if (_parentState !== null) {\n _parentState._removeChild(state);\n }\n _parent = p;\n _parentState = peekWorldMatrixState(p);\n if (_parentState !== null) {\n _parentState._addChild(state);\n }\n _lastSeenParentVersion = -1;\n // Reparenting changes our world transform → dirty us and the subtree.\n invalidate();\n },\n\n markLocalDirty(): void {\n invalidate();\n },\n\n getWorldMatrix(): Mat4 {\n if (_parentState === null && _parent !== null) {\n pollForeignParent();\n }\n if (_cachedWorld !== null) {\n return _cachedWorld;\n }\n const local = getLocalMatrix();\n if (_parent !== null) {\n const pw = _parent.worldMatrix;\n mat4MultiplyInto(_ownedWorld as unknown as Mat4Storage, 0, pw as unknown as Mat4Storage, 0, local as unknown as Mat4Storage, 0);\n _cachedWorld = _ownedWorld;\n } else {\n _cachedWorld = local;\n }\n return _cachedWorld;\n },\n\n getWorldMatrixVersion(): number {\n if (_parentState === null && _parent !== null) {\n pollForeignParent();\n }\n return _worldVersion;\n },\n\n _invalidate(): void {\n invalidate();\n },\n\n _addChild(child: WorldMatrixAccessors): void {\n _children.push(child);\n },\n\n _removeChild(child: WorldMatrixAccessors): void {\n const i = _children.indexOf(child);\n if (i >= 0) {\n _children.splice(i, 1);\n }\n },\n };\n\n return state;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"world-matrix-state.js","sources":["../../../src/scene/world-matrix-state.ts"],"sourcesContent":["/** Shared version-based lazy world matrix computation.\n *\n * Each entity provides only getLocalMatrix(). This module handles:\n * - version tracking (_worldVersion bumped by own TRS changes and ancestor motion)\n * - caching and staleness detection (_cachedWorld nulled on any change)\n * - PUSH dirty propagation: a transform write invalidates the node AND all of its\n * descendants up front, so per-frame consumers can read worldMatrixVersion in\n * O(1) (a plain field read) instead of walking the parent chain.\n *\n * Ancestor-only motion (e.g. animating a parent transform node) must bump a\n * descendant's worldMatrixVersion so per-frame consumers re-upload its UBO.\n * Rather than have each node PULL up its parent chain on every read, this module\n * keeps its own child registry (driven by the reliably-called `parent` setter, NOT\n * the host's `children` array which loaders/setParent maintain inconsistently) and\n * PUSHES invalidation down on every local change or reparent. Reads are O(1) (a\n * plain field read); writers pay an O(subtree) push. This is a clear win for the\n * common case (few movers, many readers) and never worse than the old pull walk.\n *\n * Foreign parents — an IWorldMatrixProvider that is not tagged with our state\n * symbol (e.g. a user-supplied object) — cannot be pushed to because we never see\n * their writes. For those we keep a cheap PULL fallback that polls the parent's\n * public version on read. All in-engine hosts (mesh, scene node, camera, light,\n * Gaussian-splatting mesh) ARE tagged, so engine hierarchies are pure push.\n *\n * Backing storage comes from `allocateMat4()` — process-global lazy singleton\n * in `_matrix-allocator.ts`. F32 by default; F64 after an HPM engine is\n * constructed (see `docs/lite/architecture/36-high-precision-matrix.md`). */\n\nimport type { Mat4, Quat, Vec3 } from \"../math/types.js\";\nimport type { IWorldMatrixProvider } from \"./parentable.js\";\nimport { mat4MultiplyInto } from \"../math/mat4-multiply-into.js\";\nimport type { Mat4Storage } from \"../math/types.js\";\nimport { allocateMat4 } from \"../math/_matrix-allocator.js\";\nimport { mat4Compose } from \"../math/mat4-compose.js\";\nimport { mat4Identity } from \"../math/mat4-identity.js\";\n\nexport interface WorldMatrixAccessors {\n /** Getter — returns lazily computed world matrix. */\n getWorldMatrix(): Mat4;\n /** Getter — returns current version. */\n getWorldMatrixVersion(): number;\n /** Call when own TRS changes. Invalidates cache + subtree, forces recompute. */\n markLocalDirty(): void;\n /** Reference to parent — set directly. */\n parent: IWorldMatrixProvider | null;\n /** @internal Push: mark this node and its whole subtree dirty (clean-guarded). */\n _invalidate(): void;\n /** @internal Register a same-module child so future invalidations reach it. */\n _addChild(child: WorldMatrixAccessors): void;\n /** @internal Deregister a child (reparent away). */\n _removeChild(child: WorldMatrixAccessors): void;\n}\n\nconst WM_STATE = Symbol(\"wmState\");\n\n/** Tag a host object (mesh, scene node, camera, light, …) with its world-matrix\n * state so children can register for push invalidation and call its accessor\n * closures directly, bypassing the host's property getters. */\nexport function attachWorldMatrixState(host: object, state: WorldMatrixAccessors): void {\n (host as unknown as Record<symbol, unknown>)[WM_STATE] = state;\n}\n\nfunction peekWorldMatrixState(p: IWorldMatrixProvider | null): WorldMatrixAccessors | null {\n if (p === null) {\n return null;\n }\n const s = (p as unknown as Record<symbol, unknown>)[WM_STATE];\n return (s as WorldMatrixAccessors | undefined) ?? null;\n}\n\n/** @internal Bump one engine object's world version without changing its transform values. */\nexport function _markWorldMatrixDirty(host: IWorldMatrixProvider): void {\n peekWorldMatrixState(host)?._invalidate();\n}\n\n/** Compose a local TRS matrix, skipping the general composition path for the default transform. */\nexport function composeTrsLocalMatrix(position: Vec3, rotation: Quat, scaling: Vec3): Mat4 {\n const isIdentity =\n position.x === 0 &&\n position.y === 0 &&\n position.z === 0 &&\n rotation.x === 0 &&\n rotation.y === 0 &&\n rotation.z === 0 &&\n rotation.w === 1 &&\n scaling.x === 1 &&\n scaling.y === 1 &&\n scaling.z === 1;\n return isIdentity ? mat4Identity() : mat4Compose(position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, rotation.w, scaling.x, scaling.y, scaling.z);\n}\n\n/**\n * Create world matrix state for any entity type.\n *\n * @param getLocalMatrix - Entity-specific function that returns the local (pre-parent)\n * transform matrix. Called only when the cache is stale.\n */\nexport function createWorldMatrixState(getLocalMatrix: () => Mat4): WorldMatrixAccessors {\n let _worldVersion = 0;\n let _lastSeenParentVersion = -1;\n let _cachedWorld: Mat4 | null = null;\n const _ownedWorld: Mat4 = allocateMat4();\n let _parent: IWorldMatrixProvider | null = null;\n let _parentState: WorldMatrixAccessors | null = null;\n const _children: WorldMatrixAccessors[] = [];\n\n // Mark this node — and, transitively, its whole subtree — dirty, bumping the\n // version so per-frame consumers (which gate UBO uploads on worldMatrixVersion)\n // re-upload. Propagation is eager and unconditional: consumers may never read a\n // pure transform node's world matrix, so a \"skip if already dirty\" guard would\n // strand its descendants on a stale version when an ancestor moves every frame.\n // Caching is still lazy — getWorldMatrix recomputes only when _cachedWorld is\n // null — so reads stay cheap; only writers pay the O(subtree) push.\n function invalidate(): void {\n _cachedWorld = null;\n _worldVersion++;\n for (const child of _children) {\n child._invalidate();\n }\n }\n\n // Foreign parent (not tagged with our symbol): we never observe its writes, so\n // poll its public version on read and push a subtree invalidation when it moved.\n function pollForeignParent(): void {\n const pv = (_parent as IWorldMatrixProvider).worldMatrixVersion;\n if (pv !== _lastSeenParentVersion) {\n _lastSeenParentVersion = pv;\n invalidate();\n }\n }\n\n const state: WorldMatrixAccessors = {\n get parent(): IWorldMatrixProvider | null {\n return _parent;\n },\n set parent(p: IWorldMatrixProvider | null) {\n if (p === _parent) {\n return;\n }\n if (_parentState !== null) {\n _parentState._removeChild(state);\n }\n _parent = p;\n _parentState = peekWorldMatrixState(p);\n if (_parentState !== null) {\n _parentState._addChild(state);\n }\n _lastSeenParentVersion = -1;\n // Reparenting changes our world transform → dirty us and the subtree.\n invalidate();\n },\n\n markLocalDirty(): void {\n invalidate();\n },\n\n getWorldMatrix(): Mat4 {\n if (_parentState === null && _parent !== null) {\n pollForeignParent();\n }\n if (_cachedWorld !== null) {\n return _cachedWorld;\n }\n const local = getLocalMatrix();\n if (_parent !== null) {\n const pw = _parent.worldMatrix;\n mat4MultiplyInto(_ownedWorld as unknown as Mat4Storage, 0, pw as unknown as Mat4Storage, 0, local as unknown as Mat4Storage, 0);\n _cachedWorld = _ownedWorld;\n } else {\n _cachedWorld = local;\n }\n return _cachedWorld;\n },\n\n getWorldMatrixVersion(): number {\n if (_parentState === null && _parent !== null) {\n pollForeignParent();\n }\n return _worldVersion;\n },\n\n _invalidate(): void {\n invalidate();\n },\n\n _addChild(child: WorldMatrixAccessors): void {\n _children.push(child);\n },\n\n _removeChild(child: WorldMatrixAccessors): void {\n const i = _children.indexOf(child);\n if (i >= 0) {\n _children.splice(i, 1);\n }\n },\n };\n\n return state;\n}\n"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { createShadowParamsUBO } from './shadow-base.js';
2
2
  import { createUniformBuffer } from '../resource/gpu-buffers.js';
3
+ import { acquireTexture } from '../resource/gpu-pool.js';
3
4
  import { preloadCsmShadowTaskState, ensureCsmShadowTaskState, renderCsmShadowMap } from './csm-shadow-task-hooks.js';
4
5
  import { setCsmStdReceiverFactory, setCsmPbrReceiverFactory } from './csm-receiver-registry.js';
5
6
  import { createStdCsmShadowFragment } from '../material/standard/fragments/std-csm-shadow-fragment.js';
@@ -61,6 +62,26 @@ function createCsmDirectionalShadowGenerator(engine, _light, cfg = {}) {
61
62
  sg._renderShadowMap = (eng, state) => renderCsmShadowMap(eng, sg, state, csmCfg);
62
63
  return sg;
63
64
  }
65
+ function getCsmReceiverTexture(sg) {
66
+ if (sg._shadowType !== "csm") {
67
+ throw new Error("getCsmReceiverTexture requires a CSM shadow generator");
68
+ }
69
+ if (sg._csmReceiverTexture) {
70
+ return sg._csmReceiverTexture;
71
+ }
72
+ const texture = sg._depthTexture;
73
+ const receiverTexture = {
74
+ texture,
75
+ view: texture.createView({ dimension: "2d-array" }),
76
+ sampler: sg._depthSampler,
77
+ width: texture.width,
78
+ height: texture.height,
79
+ _sampleType: "depth"
80
+ };
81
+ acquireTexture(receiverTexture);
82
+ sg._csmReceiverTexture = receiverTexture;
83
+ return receiverTexture;
84
+ }
64
85
  function onCsmReceiverUpdate(sg, cb) {
65
86
  (sg._onReceiverData ??= []).push(cb);
66
87
  return () => {
@@ -75,5 +96,5 @@ function onCsmReceiverUpdate(sg, cb) {
75
96
  };
76
97
  }
77
98
 
78
- export { createCsmDirectionalShadowGenerator, onCsmReceiverUpdate };
99
+ export { createCsmDirectionalShadowGenerator, getCsmReceiverTexture, onCsmReceiverUpdate };
79
100
  //# sourceMappingURL=csm-directional-shadow-generator.js.map