@woosh/meep-engine 2.164.0 → 2.165.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 (256) hide show
  1. package/build/bundle-worker-terrain.js +1 -1
  2. package/package.json +1 -1
  3. package/src/core/binary/BitImage2.d.ts +91 -0
  4. package/src/core/binary/BitImage2.d.ts.map +1 -0
  5. package/src/core/binary/BitImage2.js +242 -0
  6. package/src/core/geom/2d/oriented-box/min_area_rect_2d.d.ts +22 -0
  7. package/src/core/geom/2d/oriented-box/min_area_rect_2d.d.ts.map +1 -0
  8. package/src/core/geom/2d/oriented-box/min_area_rect_2d.js +69 -0
  9. package/src/core/geom/2d/packing/skyline.d.ts +116 -0
  10. package/src/core/geom/2d/packing/skyline.d.ts.map +1 -0
  11. package/src/core/geom/2d/packing/skyline.js +258 -0
  12. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts +18 -0
  13. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts.map +1 -0
  14. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.js +86 -0
  15. package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +486 -0
  16. package/src/core/geom/3d/atlas/BFF_PLAN.md +174 -0
  17. package/src/core/geom/3d/atlas/Chart.d.ts +58 -0
  18. package/src/core/geom/3d/atlas/Chart.d.ts.map +1 -0
  19. package/src/core/geom/3d/atlas/Chart.js +77 -0
  20. package/src/core/geom/3d/atlas/REVIEW_02.md +392 -0
  21. package/src/core/geom/3d/atlas/atlas_bench_lib.d.ts +73 -0
  22. package/src/core/geom/3d/atlas/atlas_bench_lib.d.ts.map +1 -0
  23. package/src/core/geom/3d/atlas/atlas_bench_lib.js +315 -0
  24. package/src/core/geom/3d/atlas/atlas_chart_type.d.ts +17 -0
  25. package/src/core/geom/3d/atlas/atlas_chart_type.d.ts.map +1 -0
  26. package/src/core/geom/3d/atlas/atlas_chart_type.js +16 -0
  27. package/src/core/geom/3d/atlas/atlas_compute_charts.d.ts +25 -0
  28. package/src/core/geom/3d/atlas/atlas_compute_charts.d.ts.map +1 -0
  29. package/src/core/geom/3d/atlas/atlas_compute_charts.js +188 -0
  30. package/src/core/geom/3d/atlas/atlas_extract_chart.d.ts +17 -0
  31. package/src/core/geom/3d/atlas/atlas_extract_chart.d.ts.map +1 -0
  32. package/src/core/geom/3d/atlas/atlas_extract_chart.js +107 -0
  33. package/src/core/geom/3d/atlas/atlas_generate.d.ts +39 -0
  34. package/src/core/geom/3d/atlas/atlas_generate.d.ts.map +1 -0
  35. package/src/core/geom/3d/atlas/atlas_generate.js +43 -0
  36. package/src/core/geom/3d/atlas/atlas_test_fixtures.d.ts +84 -0
  37. package/src/core/geom/3d/atlas/atlas_test_fixtures.d.ts.map +1 -0
  38. package/src/core/geom/3d/atlas/atlas_test_fixtures.js +266 -0
  39. package/src/core/geom/3d/atlas/atlas_write_chart_uvs.d.ts +25 -0
  40. package/src/core/geom/3d/atlas/atlas_write_chart_uvs.d.ts.map +1 -0
  41. package/src/core/geom/3d/atlas/atlas_write_chart_uvs.js +36 -0
  42. package/src/core/geom/3d/atlas/data/atlas_build_edge_dihedrals.d.ts +22 -0
  43. package/src/core/geom/3d/atlas/data/atlas_build_edge_dihedrals.d.ts.map +1 -0
  44. package/src/core/geom/3d/atlas/data/atlas_build_edge_dihedrals.js +62 -0
  45. package/src/core/geom/3d/atlas/data/atlas_build_edge_lengths.d.ts +9 -0
  46. package/src/core/geom/3d/atlas/data/atlas_build_edge_lengths.d.ts.map +1 -0
  47. package/src/core/geom/3d/atlas/data/atlas_build_edge_lengths.js +33 -0
  48. package/src/core/geom/3d/atlas/data/atlas_build_face_areas.d.ts +9 -0
  49. package/src/core/geom/3d/atlas/data/atlas_build_face_areas.d.ts.map +1 -0
  50. package/src/core/geom/3d/atlas/data/atlas_build_face_areas.js +50 -0
  51. package/src/core/geom/3d/atlas/io/atlas_mesh_weld_colocals.d.ts +27 -0
  52. package/src/core/geom/3d/atlas/io/atlas_mesh_weld_colocals.d.ts.map +1 -0
  53. package/src/core/geom/3d/atlas/io/atlas_mesh_weld_colocals.js +39 -0
  54. package/src/core/geom/3d/atlas/io/bt_loop_uv.d.ts +22 -0
  55. package/src/core/geom/3d/atlas/io/bt_loop_uv.d.ts.map +1 -0
  56. package/src/core/geom/3d/atlas/io/bt_loop_uv.js +57 -0
  57. package/src/core/geom/3d/atlas/pack/ChartPlacement.d.ts +36 -0
  58. package/src/core/geom/3d/atlas/pack/ChartPlacement.d.ts.map +1 -0
  59. package/src/core/geom/3d/atlas/pack/ChartPlacement.js +46 -0
  60. package/src/core/geom/3d/atlas/pack/atlas_pack_charts.d.ts +62 -0
  61. package/src/core/geom/3d/atlas/pack/atlas_pack_charts.d.ts.map +1 -0
  62. package/src/core/geom/3d/atlas/pack/atlas_pack_charts.js +551 -0
  63. package/src/core/geom/3d/atlas/param/atlas_chart_param_validity.d.ts +22 -0
  64. package/src/core/geom/3d/atlas/param/atlas_chart_param_validity.d.ts.map +1 -0
  65. package/src/core/geom/3d/atlas/param/atlas_chart_param_validity.js +73 -0
  66. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_bff.d.ts +37 -0
  67. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_bff.d.ts.map +1 -0
  68. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_bff.js +270 -0
  69. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_lscm.d.ts +23 -0
  70. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_lscm.d.ts.map +1 -0
  71. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_lscm.js +224 -0
  72. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_ortho.d.ts +14 -0
  73. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_ortho.d.ts.map +1 -0
  74. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_ortho.js +38 -0
  75. package/src/core/geom/3d/atlas/param/atlas_chart_stretch_metric.d.ts +22 -0
  76. package/src/core/geom/3d/atlas/param/atlas_chart_stretch_metric.d.ts.map +1 -0
  77. package/src/core/geom/3d/atlas/param/atlas_chart_stretch_metric.js +86 -0
  78. package/src/core/geom/3d/atlas/param/atlas_piecewise_resegment.d.ts +20 -0
  79. package/src/core/geom/3d/atlas/param/atlas_piecewise_resegment.d.ts.map +1 -0
  80. package/src/core/geom/3d/atlas/param/atlas_piecewise_resegment.js +183 -0
  81. package/src/core/geom/3d/atlas/param/fit_plane_basis.d.ts +27 -0
  82. package/src/core/geom/3d/atlas/param/fit_plane_basis.d.ts.map +1 -0
  83. package/src/core/geom/3d/atlas/param/fit_plane_basis.js +66 -0
  84. package/src/core/geom/3d/atlas/segment/RegionGraph.d.ts +31 -0
  85. package/src/core/geom/3d/atlas/segment/RegionGraph.d.ts.map +1 -0
  86. package/src/core/geom/3d/atlas/segment/RegionGraph.js +40 -0
  87. package/src/core/geom/3d/atlas/segment/atlas_build_region_graph.d.ts +23 -0
  88. package/src/core/geom/3d/atlas/segment/atlas_build_region_graph.d.ts.map +1 -0
  89. package/src/core/geom/3d/atlas/segment/atlas_build_region_graph.js +136 -0
  90. package/src/core/geom/3d/atlas/segment/atlas_compute_planar_regions.d.ts +24 -0
  91. package/src/core/geom/3d/atlas/segment/atlas_compute_planar_regions.d.ts.map +1 -0
  92. package/src/core/geom/3d/atlas/segment/atlas_compute_planar_regions.js +77 -0
  93. package/src/core/geom/3d/atlas/segment/atlas_merge_charts.d.ts +27 -0
  94. package/src/core/geom/3d/atlas/segment/atlas_merge_charts.d.ts.map +1 -0
  95. package/src/core/geom/3d/atlas/segment/atlas_merge_charts.js +113 -0
  96. package/src/core/geom/3d/atlas/segment/atlas_segment_charts.d.ts +44 -0
  97. package/src/core/geom/3d/atlas/segment/atlas_segment_charts.d.ts.map +1 -0
  98. package/src/core/geom/3d/atlas/segment/atlas_segment_charts.js +275 -0
  99. package/src/core/geom/3d/atlas/segment/atlas_segment_charts_atomic.d.ts +22 -0
  100. package/src/core/geom/3d/atlas/segment/atlas_segment_charts_atomic.d.ts.map +1 -0
  101. package/src/core/geom/3d/atlas/segment/atlas_segment_charts_atomic.js +199 -0
  102. package/src/core/geom/3d/line/line3_compute_point_segment_point_distance_unfolded.d.ts +32 -0
  103. package/src/core/geom/3d/line/line3_compute_point_segment_point_distance_unfolded.d.ts.map +1 -0
  104. package/src/core/geom/3d/line/line3_compute_point_segment_point_distance_unfolded.js +64 -0
  105. package/src/core/geom/3d/mat4/m4_from_rotation_translation_scale.d.ts +20 -0
  106. package/src/core/geom/3d/mat4/m4_from_rotation_translation_scale.d.ts.map +1 -0
  107. package/src/core/geom/3d/mat4/m4_from_rotation_translation_scale.js +66 -0
  108. package/src/core/geom/3d/sphere/harmonics/sh3_dering_optimize_positive.d.ts.map +1 -1
  109. package/src/core/geom/3d/sphere/harmonics/sh3_dering_optimize_positive.js +13 -11
  110. package/src/core/geom/3d/sphere/sphere_compute_bounding_of_2_spheres.d.ts.map +1 -1
  111. package/src/core/geom/3d/sphere/sphere_compute_bounding_of_2_spheres.js +4 -3
  112. package/src/core/geom/3d/topology/simplify/decimate_edge_collapse_snap.d.ts.map +1 -1
  113. package/src/core/geom/3d/topology/simplify/decimate_edge_collapse_snap.js +4 -3
  114. package/src/core/geom/3d/topology/simplify/quadratic/Quadric3.d.ts.map +1 -1
  115. package/src/core/geom/3d/topology/simplify/quadratic/Quadric3.js +4 -3
  116. package/src/core/geom/3d/topology/simplify/quadratic/apply_border_penalty_to_vertex_quadrics.d.ts.map +1 -1
  117. package/src/core/geom/3d/topology/simplify/quadratic/apply_border_penalty_to_vertex_quadrics.js +10 -6
  118. package/src/core/geom/3d/topology/struct/TopoMesh.js +2 -2
  119. package/src/core/geom/3d/topology/struct/TopoTriangle.d.ts +0 -2
  120. package/src/core/geom/3d/topology/struct/TopoTriangle.d.ts.map +1 -1
  121. package/src/core/geom/3d/topology/struct/TopoTriangle.js +2 -2
  122. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_face_find_path_polyanya.d.ts.map +1 -1
  123. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_face_find_path_polyanya.js +8 -27
  124. package/src/core/geom/vec3/v3_cotangent.d.ts +20 -0
  125. package/src/core/geom/vec3/v3_cotangent.d.ts.map +1 -0
  126. package/src/core/geom/vec3/v3_cotangent.js +29 -0
  127. package/src/core/geom/vec3/v3_matrix3_multiply.d.ts +19 -0
  128. package/src/core/geom/vec3/v3_matrix3_multiply.d.ts.map +1 -0
  129. package/src/core/geom/vec3/v3_matrix3_multiply.js +30 -0
  130. package/src/core/geom/vec3/v3_multiply.d.ts +16 -0
  131. package/src/core/geom/vec3/v3_multiply.d.ts.map +1 -0
  132. package/src/core/geom/vec3/v3_multiply.js +19 -0
  133. package/src/core/math/linalg/cg/cg_solve.d.ts +24 -0
  134. package/src/core/math/linalg/cg/cg_solve.d.ts.map +1 -0
  135. package/src/core/math/linalg/cg/cg_solve.js +106 -0
  136. package/src/core/math/linalg/cg/cg_solve_normal_equations.d.ts +21 -0
  137. package/src/core/math/linalg/cg/cg_solve_normal_equations.d.ts.map +1 -0
  138. package/src/core/math/linalg/cg/cg_solve_normal_equations.js +59 -0
  139. package/src/core/math/linalg/eigen/matrix_symmetric_3x3_eigen.d.ts +19 -0
  140. package/src/core/math/linalg/eigen/matrix_symmetric_3x3_eigen.d.ts.map +1 -0
  141. package/src/core/math/linalg/eigen/matrix_symmetric_3x3_eigen.js +117 -0
  142. package/src/core/math/linalg/sparse/SparseMatrixCSR.d.ts +50 -0
  143. package/src/core/math/linalg/sparse/SparseMatrixCSR.d.ts.map +1 -0
  144. package/src/core/math/linalg/sparse/SparseMatrixCSR.js +88 -0
  145. package/src/core/math/linalg/sparse/spm_matvec.d.ts +9 -0
  146. package/src/core/math/linalg/sparse/spm_matvec.d.ts.map +1 -0
  147. package/src/core/math/linalg/sparse/spm_matvec.js +22 -0
  148. package/src/core/math/linalg/sparse/spm_matvec_transpose.d.ts +12 -0
  149. package/src/core/math/linalg/sparse/spm_matvec_transpose.d.ts.map +1 -0
  150. package/src/core/math/linalg/sparse/spm_matvec_transpose.js +29 -0
  151. package/src/core/math/physics/mie/MIE_PARTICLES_STANDARD_PRECOMPUTED.d.ts +39 -0
  152. package/src/core/math/physics/mie/MIE_PARTICLES_STANDARD_PRECOMPUTED.js +22 -1
  153. package/src/core/math/physics/mie/compute_mie_particle_properties_rgb.d.ts.map +1 -1
  154. package/src/core/math/physics/mie/compute_mie_particle_properties_rgb.js +32 -9
  155. package/src/core/math/physics/mie/compute_mie_phase.d.ts +2 -3
  156. package/src/core/math/physics/mie/compute_mie_phase.d.ts.map +1 -1
  157. package/src/core/math/physics/mie/compute_mie_phase.js +1 -3
  158. package/src/core/math/physics/mie/lorenz_mie_coefs.d.ts +0 -1
  159. package/src/core/math/physics/mie/lorenz_mie_coefs.d.ts.map +1 -1
  160. package/src/core/math/physics/mie/lorenz_mie_coefs.js +0 -7
  161. package/src/engine/ecs/attachment/TransformAttachmentBinding.js +3 -3
  162. package/src/engine/ecs/gui/hud/HeadsUpDisplaySystem.js +2 -2
  163. package/src/engine/ecs/terrain/ecs/Terrain.d.ts.map +1 -1
  164. package/src/engine/ecs/terrain/ecs/Terrain.js +4 -3
  165. package/src/engine/ecs/terrain/tiles/TerrainTile.js +2 -2
  166. package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
  167. package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +9 -8
  168. package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
  169. package/src/engine/graphics/GraphicsEngine.js +2 -1
  170. package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +2 -2
  171. package/src/engine/graphics/ecs/decal/v2/prototypeDecalSystem.js +2 -2
  172. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.js +2 -2
  173. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.d.ts.map +1 -1
  174. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js +0 -1
  175. package/src/engine/graphics/ecs/path/tube/build/computeFrenetFrames.d.ts.map +1 -1
  176. package/src/engine/graphics/ecs/path/tube/build/computeFrenetFrames.js +8 -7
  177. package/src/engine/graphics/geometry/MikkT/AvgTSpace.d.ts.map +1 -1
  178. package/src/engine/graphics/geometry/MikkT/AvgTSpace.js +11 -6
  179. package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.d.ts.map +1 -1
  180. package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.js +2 -3
  181. package/src/engine/graphics/geometry/MikkT/CalcTexArea.d.ts.map +1 -1
  182. package/src/engine/graphics/geometry/MikkT/CalcTexArea.js +3 -4
  183. package/src/engine/graphics/geometry/MikkT/DegenEpilogue.js +4 -4
  184. package/src/engine/graphics/geometry/MikkT/EvalTspace.d.ts.map +1 -1
  185. package/src/engine/graphics/geometry/MikkT/EvalTspace.js +11 -8
  186. package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.d.ts.map +1 -1
  187. package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.js +7 -8
  188. package/src/engine/graphics/geometry/MikkT/GenerateTSpaces.js +8 -8
  189. package/src/engine/graphics/geometry/MikkT/InitTriInfo.d.ts.map +1 -1
  190. package/src/engine/graphics/geometry/MikkT/InitTriInfo.js +30 -25
  191. package/src/engine/graphics/geometry/MikkT/MikkTSpace.js +7 -7
  192. package/src/engine/graphics/geometry/MikkT/NormalizeSafe.d.ts +0 -1
  193. package/src/engine/graphics/geometry/MikkT/NormalizeSafe.d.ts.map +1 -1
  194. package/src/engine/graphics/geometry/MikkT/NormalizeSafe.js +6 -4
  195. package/src/engine/graphics/geometry/MikkT/STSpace.d.ts +0 -2
  196. package/src/engine/graphics/geometry/MikkT/STSpace.d.ts.map +1 -1
  197. package/src/engine/graphics/geometry/MikkT/STSpace.js +3 -3
  198. package/src/engine/graphics/geometry/MikkT/v3_scale_dot_sub_normalize.d.ts +0 -2
  199. package/src/engine/graphics/geometry/MikkT/v3_scale_dot_sub_normalize.d.ts.map +1 -1
  200. package/src/engine/graphics/geometry/MikkT/v3_scale_dot_sub_normalize.js +0 -1
  201. package/src/engine/graphics/impostors/octahedral/ImpostorBaker.d.ts +0 -2
  202. package/src/engine/graphics/impostors/octahedral/ImpostorBaker.d.ts.map +1 -1
  203. package/src/engine/graphics/impostors/octahedral/ImpostorBaker.js +4 -3
  204. package/src/engine/graphics/impostors/octahedral/grid/HemiOctahedralUvEncoder.js +2 -2
  205. package/src/engine/graphics/impostors/octahedral/grid/UvEncoder.d.ts +17 -7
  206. package/src/engine/graphics/impostors/voxel/VoxelImpostorBaker.js +2 -2
  207. package/src/engine/graphics/particles/particular/engine/utils/volume/ParticleVolume.js +3 -3
  208. package/src/engine/graphics/particles/particular/engine/utils/volume/prototypeParticleVolume.js +4 -3
  209. package/src/engine/graphics/render/forward_plus/LightManager.js +2 -2
  210. package/src/engine/graphics/render/forward_plus/model/Decal.d.ts +0 -2
  211. package/src/engine/graphics/render/forward_plus/model/Decal.d.ts.map +1 -1
  212. package/src/engine/graphics/render/forward_plus/model/Decal.js +3 -3
  213. package/src/engine/graphics/render/forward_plus/prototype/prototypeLightManager.js +4 -3
  214. package/src/engine/graphics/render/visibility/hiz/prototypeHiZ.js +6 -4
  215. package/src/engine/graphics/sh3/lightmap/ARTIFACTS_PLAN.md +146 -0
  216. package/src/engine/graphics/sh3/lightmap/CODE_REVIEW.md +121 -0
  217. package/src/engine/graphics/sh3/lightmap/LIGHTMAPPER_REVIEW.md +183 -0
  218. package/src/engine/graphics/sh3/lightmap/bake_lightmap_for_scene.d.ts +90 -0
  219. package/src/engine/graphics/sh3/lightmap/bake_lightmap_for_scene.d.ts.map +1 -0
  220. package/src/engine/graphics/sh3/lightmap/bake_lightmap_for_scene.js +678 -0
  221. package/src/engine/graphics/sh3/lightmap/lightmap_build_geometry.d.ts +32 -0
  222. package/src/engine/graphics/sh3/lightmap/lightmap_build_geometry.d.ts.map +1 -0
  223. package/src/engine/graphics/sh3/lightmap/lightmap_build_geometry.js +155 -0
  224. package/src/engine/graphics/sh3/lightmap/lightmap_denoise.d.ts +49 -0
  225. package/src/engine/graphics/sh3/lightmap/lightmap_denoise.d.ts.map +1 -0
  226. package/src/engine/graphics/sh3/lightmap/lightmap_denoise.js +136 -0
  227. package/src/engine/graphics/sh3/lightmap/lightmap_dilate.d.ts +19 -0
  228. package/src/engine/graphics/sh3/lightmap/lightmap_dilate.d.ts.map +1 -0
  229. package/src/engine/graphics/sh3/lightmap/lightmap_dilate.js +89 -0
  230. package/src/engine/graphics/sh3/lightmap/lightmap_gather.d.ts +54 -0
  231. package/src/engine/graphics/sh3/lightmap/lightmap_gather.d.ts.map +1 -0
  232. package/src/engine/graphics/sh3/lightmap/lightmap_gather.js +168 -0
  233. package/src/engine/graphics/sh3/lightmap/lightmap_rasterize_triangle.d.ts +30 -0
  234. package/src/engine/graphics/sh3/lightmap/lightmap_rasterize_triangle.d.ts.map +1 -0
  235. package/src/engine/graphics/sh3/lightmap/lightmap_rasterize_triangle.js +145 -0
  236. package/src/engine/graphics/sh3/lightmap/lightmap_seam_fix.d.ts +38 -0
  237. package/src/engine/graphics/sh3/lightmap/lightmap_seam_fix.d.ts.map +1 -0
  238. package/src/engine/graphics/sh3/lightmap/lightmap_seam_fix.js +152 -0
  239. package/src/engine/graphics/sh3/lightmap/prototypeLightmapBaker.d.ts +2 -0
  240. package/src/engine/graphics/sh3/lightmap/prototypeLightmapBaker.d.ts.map +1 -0
  241. package/src/engine/graphics/sh3/lightmap/prototypeLightmapBaker.js +186 -0
  242. package/src/engine/graphics/sh3/path_tracer/PathTracedMesh.d.ts.map +1 -1
  243. package/src/engine/graphics/sh3/path_tracer/PathTracedMesh.js +2 -2
  244. package/src/engine/graphics/sh3/path_tracer/PathTracer.js +2 -2
  245. package/src/engine/graphics/sh3/path_tracer/prepare_scene_gltf.d.ts.map +1 -1
  246. package/src/engine/graphics/sh3/path_tracer/prepare_scene_gltf.js +6 -4
  247. package/src/engine/graphics/sh3/path_tracer/prototypePathTracer.js +3 -3
  248. package/src/engine/graphics/sh3/path_tracer/texture/sample_material.d.ts.map +1 -1
  249. package/src/engine/graphics/sh3/path_tracer/texture/sample_material.js +5 -4
  250. package/src/engine/graphics/sh3/sky/hosek/prototype_hosek.js +2 -2
  251. package/src/engine/input/devices/KeyboardDevice.d.ts +61 -14
  252. package/src/engine/input/devices/KeyboardDevice.d.ts.map +1 -1
  253. package/src/engine/input/devices/KeyboardDevice.js +41 -15
  254. package/src/engine/input/devices/isHTMLElementFocusable.d.ts.map +1 -1
  255. package/src/engine/input/devices/isHTMLElementFocusable.js +2 -9
  256. package/src/engine/sound/simulation/prototypeAcousticSolver.js +261 -143
@@ -6,7 +6,6 @@ import {
6
6
  MeshStandardMaterial,
7
7
  SphereGeometry
8
8
  } from "three";
9
- import { AABB3 } from "../../../core/geom/3d/aabb/AABB3.js";
10
9
  import { Ray3 } from "../../../core/geom/3d/ray/Ray3.js";
11
10
  import { BoxShape3D } from "../../../core/geom/3d/shape/BoxShape3D.js";
12
11
  import { CapsuleShape3D } from "../../../core/geom/3d/shape/CapsuleShape3D.js";
@@ -33,9 +32,7 @@ import { ShadedGeometrySystem } from "../../graphics/ecs/mesh-v2/ShadedGeometryS
33
32
  import {
34
33
  AmbientOcclusionPostProcessEffect
35
34
  } from "../../graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js";
36
- import { MaterialTransformer, ProbeResolutionStage } from "../../graphics/sh3/gi/material/MaterialTransformer.js";
37
- import { LightProbeVolume } from "../../graphics/sh3/lpv/LightProbeVolume.js";
38
- import { LightProbeVolumeBaker } from "../../graphics/sh3/lpv/LightProbeVolumeBaker.js";
35
+ import { bake_lightmap_for_scene } from "../../graphics/sh3/lightmap/bake_lightmap_for_scene.js";
39
36
  import { setup_environment_sky_from_ecd } from "../../graphics/sh3/sky/hosek/setup_environment_sky_from_ecd.js";
40
37
  import {
41
38
  sampler2d_sample_equirectangular_direction
@@ -200,6 +197,8 @@ const SHACK_DOOR_HALF = 0.5, SHACK_DOOR_H = 2.0; // a 1 m doorway on the west wa
200
197
  // occluder index re-syncs on transform change) — closed it seals the opening, open it clears it.
201
198
  const DOOR_T = 0.2; // door slab thickness (sits within the opening)
202
199
  const DOOR_SLIDE_SPEED = 3.0; // m/s vertical travel
200
+ const DOOR_OPEN_CLEARANCE = 0.2; // extra drop when open, so the lowered slab's top sinks clear of the floor
201
+ // (y=0) instead of sitting coplanar with it and z-fighting the seam
203
202
 
204
203
  // -- Orbiting external source (WorldMode layer), circling the scene from outside ------------------------
205
204
  const ORBIT_CLIP = "data/sounds/music/matsmusic/d1/Fiverr Alexgoldring WorldMode LAYER 1.ogg";
@@ -209,17 +208,18 @@ const ORBIT_HEIGHT = 5; // a touch above the walls, so line-of-sig
209
208
  const ORBIT_SPEED = 0.18; // rad/s (~35 s per lap)
210
209
  const ORBIT_DIST_MIN = 12, ORBIT_DIST_MAX = 90; // carries far — clearly audible from the centre with LoS
211
210
 
212
- // -- Light probe volume (baked diffuse GI). Baked once at startup by path-tracing each grid probe, so the probe
213
- // COUNT is the bake-cost knob. After the bake the IBL is switched off (scene.environment = null) so the LPV is
214
- // the only indirect light the sky stays only as the backdrop.
211
+ // -- Baked indirect lighting (a single shared lightmap). Baked once at startup by path-tracing the scene's
212
+ // UV-atlas texels (mixed mode: indirect bounces + sky only direct light stays realtime), so the atlas
213
+ // resolution + samples/texel are the bake-cost knobs. After the bake the IBL is switched off
214
+ // (scene.environment = null) so the baked lightmap is the only indirect light — the sky stays as the backdrop.
215
215
  //
216
- // The volume must ENCLOSE every rendered surface with margin: a surface sitting exactly on the hull boundary
217
- // makes the per-fragment tetrahedral-mesh lookup flip in/out across neighbouring pixels (salt-and-pepper noise
218
- // with the tet grid showing through). In particular the bottom is BELOW the platform (y0 = -4, platform top is
219
- // y=0) so the floor is INTERIOR; ny=5 over y∈[-4,12] lands a probe LAYER exactly on the floor (y = -4,0,4,8,12),
220
- // where it captures the floor's incident sky/sun light. The x/z bounds overhang the platform for the same reason.
221
- const LPV_BOUNDS = [-46, -4, -18, 18, 16, 18]; // x0,y0,z0, x1,y1,z1 overhangs the platform; floor y=0 is interior
222
- const LPV_RESOLUTION = new Vector3(30, 10, 20); // counts/axis; ny=5 over y∈[-4,12] a layer at y=0
216
+ // Unlike the old probe volume there are NO bounds to tune: the charter derives the atlas from the geometry
217
+ // itself, so every ShadedGeometry surface is covered. The bake rewrites each mesh with a uv2 channel and points
218
+ // its material at the shared lightmap; the acoustic occluder/collider shapes are untouched (lightmap is visuals
219
+ // only), so the acoustic simulation is unaffected.
220
+ const LIGHTMAP_RESOLUTION = 1024; // shared atlas size (texels per side)
221
+ const LIGHTMAP_SAMPLES = 1024; // hemisphere gather samples per fully-baked texel
222
+ const LIGHTMAP_MAX_BOUNCE = 4; // path-tracer maximum bounce depth
223
223
 
224
224
  // -- Point lights co-located with each emissive source marker (a warm/cool glow in its room) -----------
225
225
  const SOURCE_LIGHT_INTENSITY = 6, SOURCE_LIGHT_DISTANCE = 12;
@@ -227,15 +227,18 @@ const SOURCE_LIGHT_INTENSITY = 6, SOURCE_LIGHT_DISTANCE = 12;
227
227
  // Wired in the configuration callback (needs the EngineConfiguration); baked against in main().
228
228
  let acousticSim = null;
229
229
 
230
- // Set in setupLightProbes() once the graphics MaterialManager exists. Lit scene materials are obtained THROUGH
231
- // it (see litMaterial) so the LPV MaterialTransformer compile-step actually patches them a bare
232
- // `new MeshStandardMaterial(...)` never passes through the transform chain, so it would get NO probe GI.
230
+ // The graphics MaterialManager, captured at the top of main(). Every lit scene material is obtained THROUGH it
231
+ // (see litMaterial) so the engine's registered compile-steps patch itin particular the forward+ clustered-
232
+ // lighting transformer (registered by the LightSystem's forward+ plugin), which is what makes the per-source
233
+ // POINT lights actually illuminate a surface. A bare `new MeshStandardMaterial(...)` skips the compile chain and
234
+ // so receives NO clustered (point/spot) lighting — only the directional sun.
233
235
  let materialManager = null;
234
236
 
235
237
  /**
236
- * A managed lit material: routed through the {@link MaterialManager} so every registered compile-step (notably
237
- * the LPV {@link MaterialTransformer}) patches it. Use this for every lit surface instead of a bare
238
- * `new MeshStandardMaterial` that is what makes the baked probe lighting visible.
238
+ * A managed lit material: routed through the {@link MaterialManager} so every registered compile-step patches it
239
+ * notably the forward+ clustered-lighting transformer, so the per-source POINT lights light it. Use this for
240
+ * every lit surface instead of a bare `new MeshStandardMaterial`. The baked lightmap is applied on top by
241
+ * {@link bake_lightmap_for_scene}, which sets `material.lightMap` on the obtained material directly.
239
242
  *
240
243
  * @param {object} params MeshStandardMaterial parameters (color, roughness, emissive, …)
241
244
  * @returns {MeshStandardMaterial} the managed (compiled) material to hand to {@link ShadedGeometry.from}
@@ -273,31 +276,13 @@ function setupSound(config, engine) {
273
276
  config.addSystem(new SoundListenerSystem(engine.sound.context));
274
277
  }
275
278
 
276
- /**
277
- * Create the light probe volume + its material compile-step. MUST run before any scene geometry compiles — the
278
- * transformer injects the irradiance-sampling shader chunk into lit materials AT compile time — so it is called
279
- * at the very top of {@link main}. The bake itself runs later, once the geometry exists ({@link bakeLightProbes}).
280
- *
281
- * @param {Engine} engine
282
- * @returns {{volume:LightProbeVolume, transformer:MaterialTransformer}}
283
- */
284
- function setupLightProbes(engine) {
285
- materialManager = engine.graphics.getMaterialManager();
286
- const volume = new LightProbeVolume();
287
- // Fragment (per-pixel) sampling: this scene is big flat boxes, so vertex sampling (only the 4 face corners)
288
- // would barely read; fragment is costlier but shows the baked GI gradient across a wall/floor.
289
- const transformer = new MaterialTransformer({ volume, stage: ProbeResolutionStage.Fragment });
290
- materialManager.addCompileStep(transformer);
291
- return { volume, transformer };
292
- }
293
-
294
279
  const _skyScratch = new Float32Array(4);
295
280
 
296
281
  /**
297
- * A path-tracer background sampler over the Hosek sky equirectangular texture, by ray direction (+Y up). The LPV
298
- * baker's path tracer otherwise has a BLACK background ({@link PathTracedScene} default), so without this the
299
- * bake captures only the sun bouncing off surfaces — no sky-dome fill. It samples the SAME sharp sky texture the
300
- * renderer applies, so the baked GI matches the visible sky.
282
+ * A path-tracer background sampler over the Hosek sky equirectangular texture, by ray direction (+Y up). The
283
+ * lightmap baker's path tracer otherwise has a BLACK background ({@link PathTracedScene} default), so without
284
+ * this the bake captures only the sun bouncing off surfaces — no sky-dome fill. It samples the SAME sharp sky
285
+ * texture the renderer applies, so the baked GI matches the visible sky.
301
286
  *
302
287
  * @param {THREE.DataTexture} skyTexture equirectangular Float32 RGBA sky (the return of setup_environment_sky_from_ecd)
303
288
  * @returns {function(out:number[], out_offset:number, direction:number[], direction_offset:number): void}
@@ -322,44 +307,118 @@ function makeSkyBackgroundSampler(skyTexture) {
322
307
  }
323
308
 
324
309
  /**
325
- * Bake the LPV against the now-populated scene (path-traces each grid probe — {@link LPV_RESOLUTION} drives the
326
- * cost), giving the path tracer the Hosek SKY as its background radiance (its default is black) so the bake picks
327
- * up sky-dome fill, not just the sun. Then switch the IBL OFF (`scene.environment = null`) so the baked diffuse
328
- * GI is the only indirect light; the sky stays as the visible backdrop (`scene.background`). Finally push the
329
- * baked irradiance into the already-compiled materials.
310
+ * Bake a single shared lightmap against the now-populated scene ({@link bake_lightmap_for_scene} charts every
311
+ * {@link ShadedGeometry} into one UV atlas and path-traces its texels {@link LIGHTMAP_RESOLUTION} +
312
+ * {@link LIGHTMAP_SAMPLES} drive the cost), overriding the baker's default procedural sky with the Hosek SKY as
313
+ * its background radiance so the baked sky-dome fill matches the visible sky. The bake rewrites each mesh with a
314
+ * uv2 channel and points its material at the shared lightmap, so it lights itself once the texels resolve. Then
315
+ * switch the IBL OFF (`scene.environment = null`) so the baked lightmap is the only indirect light; the sky
316
+ * stays as the visible backdrop (`scene.background`).
330
317
  *
331
318
  * @param {Engine} engine
332
319
  * @param {EntityComponentDataset} ecd
333
- * @param {{volume:LightProbeVolume, transformer:MaterialTransformer}} lpv
334
320
  * @param {THREE.DataTexture} skyTexture the equirectangular sky to sample as the bake's background radiance
335
321
  */
336
- async function bakeLightProbes(engine, ecd, lpv, skyTexture) {
337
- const b = LPV_BOUNDS;
338
- // build_grid populates the probe positions AND the (grid) tetrahedral mesh the bake/interp need.
339
- lpv.volume.build_grid(new AABB3(b[0], b[1], b[2], b[3], b[4], b[5]), LPV_RESOLUTION);
340
-
341
- const baker = new LightProbeVolumeBaker();
342
- // Give the path tracer the sky (its background defaults to black) — otherwise escaped rays return no
343
- // radiance and the bake is sun-bounce only, so surfaces in shadow / facing away go dark.
344
- baker._ren.scene.__background_sampler = makeSkyBackgroundSampler(skyTexture);
345
-
346
- // Path-trace each probe (via the executor's task group) and wait for the whole bake to finish.
347
- const task = baker.bake(lpv.volume, ecd);
348
- engine.executor.runGroup(task);
349
- await task.promise();
350
-
351
- engine.graphics.scene.environment = null; // IBL off — only the LPV contributes indirect light now
352
- lpv.transformer.update();
322
+ async function bakeLightmap(engine, ecd, skyTexture) {
323
+ const group = bake_lightmap_for_scene({
324
+ ecd,
325
+ lightmap_resolution: LIGHTMAP_RESOLUTION,
326
+ samples_per_texel: LIGHTMAP_SAMPLES,
327
+ max_bounce: LIGHTMAP_MAX_BOUNCE,
328
+ // Sample the Hosek sky as the bake's background (its default is the procedural RTIW sky) — escaped rays
329
+ // then return the SAME sky the renderer shows, so the baked fill matches the visible sky.
330
+ background: makeSkyBackgroundSampler(skyTexture),
331
+ });
332
+
333
+ // Run the bake on the executor (incremental — the partial atlas uploads each cycle) and wait for it to finish.
334
+ // While it runs, poll the group's progress each frame to drive an unobtrusive corner progress bar + ETA.
335
+ const progress = makeBakeProgressBar();
336
+ const start = performance.now();
337
+ let running = true;
338
+
339
+ engine.executor.runGroup(group);
340
+
341
+ (function poll() {
342
+ if (!running) {
343
+ return;
344
+ }
345
+ progress.set(group.computeProgress(), performance.now() - start);
346
+ requestAnimationFrame(poll);
347
+ })();
348
+
349
+ await group.promise();
350
+
351
+ running = false;
352
+ progress.remove();
353
+
354
+ engine.graphics.scene.environment = null; // IBL off — only the baked lightmap contributes indirect light now
355
+ }
356
+
357
+ /**
358
+ * A small, unobtrusive lightmap-bake progress bar pinned to the bottom-left corner, showing percent done + a
359
+ * live ETA. The ETA is a linear extrapolation from elapsed time (`elapsed × (1 − p) / p`), reading "estimating…"
360
+ * until there is enough progress to extrapolate. Returns a handle to drive + dismiss it.
361
+ *
362
+ * @returns {{set: function(fraction:number, elapsedMs:number): void, remove: function(): void}}
363
+ */
364
+ function makeBakeProgressBar() {
365
+ const container = document.createElement("div");
366
+ Object.assign(container.style, {
367
+ position: "absolute", left: "10px", bottom: "10px", zIndex: "1000",
368
+ width: "210px", padding: "8px 10px",
369
+ font: "12px sans-serif", color: "#cfe6ff",
370
+ background: "rgba(18,26,34,0.85)", border: "1px solid #3a6ea5", borderRadius: "4px",
371
+ pointerEvents: "none", userSelect: "none",
372
+ });
373
+
374
+ const label = document.createElement("div");
375
+ label.style.marginBottom = "6px";
376
+ label.style.letterSpacing = "0.3px";
377
+ label.textContent = "Baking lightmap…";
378
+
379
+ const track = document.createElement("div");
380
+ Object.assign(track.style, {
381
+ height: "6px", width: "100%", background: "rgba(255,255,255,0.12)", borderRadius: "3px", overflow: "hidden",
382
+ });
383
+
384
+ const fill = document.createElement("div");
385
+ Object.assign(fill.style, { height: "100%", width: "0%", background: "#5fa8ff", transition: "width 0.1s linear" });
386
+
387
+ track.appendChild(fill);
388
+ container.appendChild(label);
389
+ container.appendChild(track);
390
+ document.body.appendChild(container);
391
+
392
+ return {
393
+ set(fraction, elapsedMs) {
394
+ const p = Math.max(0, Math.min(1, fraction));
395
+ fill.style.width = `${(p * 100).toFixed(1)}%`;
396
+ const eta = p > 0.02 ? formatDuration((elapsedMs / p) * (1 - p)) : "estimating…";
397
+ label.textContent = `Baking lightmap… ${(p * 100).toFixed(0)}% · ETA ${eta}`;
398
+ },
399
+ remove() {
400
+ container.remove();
401
+ },
402
+ };
403
+ }
404
+
405
+ /** Format a millisecond duration as a compact "Xm Ys" / "Ys" string for the bake ETA. */
406
+ function formatDuration(ms) {
407
+ const totalSeconds = Math.max(0, Math.round(ms / 1000));
408
+ const minutes = Math.floor(totalSeconds / 60);
409
+ const seconds = totalSeconds % 60;
410
+ return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
353
411
  }
354
412
 
355
413
  async function main(engine) {
356
414
  const em = engine.entityManager;
357
415
  const ecd = em.dataset;
358
416
 
359
- // Light probe volume create the volume + its material compile-step BEFORE any geometry compiles (the
360
- // transformer injects the irradiance-sampling shader chunk at material-compile time). Baked + the IBL
361
- // switched off later, once the scene exists (see bakeLightProbes, after the acoustic bake).
362
- const lpv = setupLightProbes(engine);
417
+ // Capture the MaterialManager up front: every lit material is obtained through it (see litMaterial) so the
418
+ // engine's compile-steps patch it notably the forward+ clustered-lighting transformer that lights surfaces
419
+ // with the per-source POINT lights. (That transformer is registered by the LightSystem's forward+ plugin,
420
+ // acquired in buildLights below; the obtain() calls happen later, as the world geometry is built.)
421
+ materialManager = engine.graphics.getMaterialManager();
363
422
 
364
423
  // -- Systems ---------------------------------------------------------
365
424
  EngineHarness.addFpsCounter(engine);
@@ -416,17 +475,21 @@ async function main(engine) {
416
475
  buildScatteredOccluders(ecd);
417
476
  buildSmallShack(ecd);
418
477
  buildSlidingDoors(ecd); // house door + cathedral entrance, built OPEN (so the bake sees the openings clear)
419
- // The audio sources are spawned on the FIRST click (setupInteraction), once the context has resumed —
420
- // the engine starts ticking inside EngineHarness.initialize, so a source built here would be promoted
421
- // and play on the still-suspended context (the autoplay-policy warning). Deferring the spawn to the
422
- // gesture means its first playEvent always lands on a running context.
478
+
479
+ // The three fixed sources' MARKERS (emissive sphere + point light) are built now, before the bake, so the
480
+ // lightmap captures their glow + the rooms they light. Only their AudioEmitter is deferred to the first
481
+ // click (startSources): the engine ticks inside EngineHarness.initialize, so an autoplay emitter built here
482
+ // would link + play on the still-suspended context (the autoplay-policy warning); attaching it on the
483
+ // gesture means its first playEvent lands on a running context. The moving orbiter source is spawned whole
484
+ // on that click (its light moves, so it is not baked).
485
+ const staticSources = buildStaticSourceMarkers(ecd);
423
486
 
424
487
  // -- Player ----------------------------------------------------------
425
488
  const player = buildPlayerEntity(ecd);
426
489
  fpsSystem.groundResolver = makePhysicsGroundResolver(physicsSystem, player.entity);
427
490
  motion.playerEntity = player.entity; // door-proximity overlap tests against the player capsule
428
491
  buildInputBindings(ecd, player, motion);
429
- setupInteraction(engine, ecd);
492
+ setupInteraction(engine, ecd, staticSources);
430
493
 
431
494
  // -- Bake the probe field against the now-populated occluder index ----
432
495
  // The acoustic entities above linked into acousticSim.occluderIndex on build; bake the visibility
@@ -459,13 +522,14 @@ async function main(engine) {
459
522
 
460
523
  logHelp();
461
524
 
462
- // -- Light probe volume: bake the diffuse GI against the populated scene, then switch the IBL off so the LPV
463
- // is the only indirect light. Done LAST (after the audio path is fully wired) — it path-traces ≈240 probes,
464
- // a few seconds; the scene renders IBL-lit until it finishes, then switches to the baked GI. --------------
465
- console.log("%c[lpv] baking light probes…", "color:#ffcc66;font-weight:bold");
466
- console.time("[lpv] bake");
467
- await bakeLightProbes(engine, ecd, lpv, skyTexture);
468
- console.timeEnd("[lpv] bake");
525
+ // -- Lightmap: bake the indirect GI into a shared atlas against the populated scene, then switch the IBL off
526
+ // so the baked lightmap is the only indirect light. Done LAST (after the audio path is fully wired) — it
527
+ // path-traces the atlas texels over a few seconds; the scene renders IBL-lit and uploads the partial atlas
528
+ // each cycle, then switches to the fully-baked GI when it finishes. -------------------------------------
529
+ console.log("%c[lightmap] baking…", "color:#ffcc66;font-weight:bold");
530
+ console.time("[lightmap] bake");
531
+ await bakeLightmap(engine, ecd, skyTexture);
532
+ console.timeEnd("[lightmap] bake");
469
533
  }
470
534
 
471
535
  /**
@@ -838,7 +902,9 @@ function spawnSlidingDoor(ecd, { center, size, zoneCenter, zoneHalf, label }) {
838
902
  const collider = new Collider();
839
903
  collider.shape = BoxShape3D.from_size(size.x, size.y, size.z);
840
904
 
841
- const openY = center.y - size.y; // slid fully below the floor → the opening is clear
905
+ // slid fully below the floor → the opening is clear; the extra DOOR_OPEN_CLEARANCE sinks the slab's top
906
+ // past the floor seam (its open top lands at y = -DOOR_OPEN_CLEARANCE) so it doesn't z-fight the floor.
907
+ const openY = center.y - size.y - DOOR_OPEN_CLEARANCE;
842
908
 
843
909
  // START open (slid down): the doors begin open AND, since the acoustic bake runs after the world is built,
844
910
  // it sees the openings clear rather than sealed. E closes them.
@@ -1017,12 +1083,13 @@ class PrototypeMotionSystem extends System {
1017
1083
  // The MainMenu loop, shared by the house, cathedral and shack sources (same clip, different rooms/acoustics).
1018
1084
  const MAIN_MENU_CLIP = "data/sounds/music/matsmusic/d1/Fiverr Alexgoldring MainMenu.ogg";
1019
1085
 
1020
- /** Sources spawned on the first click. House, cathedral and the little SHACK all play the SAME MainMenu loop (so
1021
- * the difference you hear is purely the room acoustics); a fourth source orbits the scene from outside. (The
1022
- * paths have spaces the browser percent-encodes them on fetch, and the dev server serves it straight from
1023
- * `app/`.) */
1024
- function buildSources(ecd) {
1025
- spawnAudioSource(ecd, {
1086
+ // The three FIXED sources house, cathedral and the little SHACK all play the SAME MainMenu loop (so the
1087
+ // difference you hear is purely the room acoustics). Their markers (an emissive sphere + a co-located point
1088
+ // light) are built at world time so the lightmap BAKES them both the marker's glow and the room each one
1089
+ // lights. Only the AudioEmitter is deferred to the first click (see startSources). (The paths have spaces — the
1090
+ // browser percent-encodes them on fetch, and the dev server serves it straight from `app/`.)
1091
+ const STATIC_SOURCE_SPECS = [
1092
+ {
1026
1093
  id: "house-mainmenu",
1027
1094
  clipPath: MAIN_MENU_CLIP,
1028
1095
  position: SRC,
@@ -1031,8 +1098,8 @@ function buildSources(ecd) {
1031
1098
  markerColor: 0xffcc66,
1032
1099
  markerEmissive: 0xff8800,
1033
1100
  lightDistance: SOURCE_LIGHT_DISTANCE / 2, // a tighter orange glow in the small dead room
1034
- });
1035
- spawnAudioSource(ecd, {
1101
+ },
1102
+ {
1036
1103
  id: "cathedral-mainmenu",
1037
1104
  clipPath: MAIN_MENU_CLIP,
1038
1105
  position: CATH_SRC,
@@ -1040,8 +1107,8 @@ function buildSources(ecd) {
1040
1107
  distanceMax: CATH_SRC_DIST_MAX,
1041
1108
  markerColor: 0x99ccff,
1042
1109
  markerEmissive: 0x3388ff,
1043
- });
1044
- spawnAudioSource(ecd, {
1110
+ },
1111
+ {
1045
1112
  id: "shack-mainmenu",
1046
1113
  clipPath: MAIN_MENU_CLIP,
1047
1114
  position: new Vector3(SHACK_X, 1.2, SHACK_Z),
@@ -1049,22 +1116,59 @@ function buildSources(ecd) {
1049
1116
  distanceMax: SRC_DIST_MAX,
1050
1117
  markerColor: 0xcc99ff,
1051
1118
  markerEmissive: 0x8844cc,
1052
- });
1119
+ },
1120
+ ];
1053
1121
 
1054
- // A third source ORBITS the whole scene from outside (a WorldMode layer). Acoustic + a long max distance,
1055
- // so from the centre you hear it swing in and out as buildings break the line of sight. The
1056
- // PrototypeMotionSystem drives its position each frame around (ORBIT_CX, ORBIT_CZ).
1057
- const orbiter = spawnAudioSource(ecd, {
1058
- id: "orbit-worldmode",
1059
- clipPath: ORBIT_CLIP,
1122
+ /**
1123
+ * Build the three FIXED sources' visual markers (an emissive sphere + a co-located point light each). Called at
1124
+ * world-build time, BEFORE the lightmap bake, so the bake captures each source's glow and the room it lights.
1125
+ * The audio is attached later, on the first click ({@link startSources}).
1126
+ *
1127
+ * @param {EntityComponentDataset} ecd
1128
+ * @returns {{entity:number, spec:object}[]} one record per static source, consumed by {@link startSources}
1129
+ */
1130
+ function buildStaticSourceMarkers(ecd) {
1131
+ return STATIC_SOURCE_SPECS.map((spec) => ({ entity: buildSourceMarker(ecd, spec), spec }));
1132
+ }
1133
+
1134
+ /**
1135
+ * Start the audio on the first click, once the context has resumed: attach a (3D, looping, autoplay)
1136
+ * {@link AudioEmitter} to each pre-built static marker — so it links and autoplays on a RUNNING context (routing
1137
+ * is fixed at link, so the emitter must first appear on a resumed context) — and spawn the moving orbiter whole.
1138
+ *
1139
+ * @param {EntityComponentDataset} ecd
1140
+ * @param {{entity:number, spec:object}[]} staticSources the markers built by {@link buildStaticSourceMarkers}
1141
+ */
1142
+ function startSources(ecd, staticSources) {
1143
+ for (const { entity, spec } of staticSources) {
1144
+ addSourceEmitter(ecd, entity, spec);
1145
+ }
1146
+ spawnOrbiter(ecd);
1147
+ }
1148
+
1149
+ /**
1150
+ * A fourth source that ORBITS the whole scene from outside (a WorldMode layer): acoustic + a long max distance,
1151
+ * so from the centre you hear it swing in and out as buildings break the line of sight. Spawned whole on the
1152
+ * first click (marker + emitter + {@link PrototypeMover}, which drives its position each frame around
1153
+ * (ORBIT_CX, ORBIT_CZ)); because its light MOVES it is deliberately not baked into the lightmap.
1154
+ *
1155
+ * @param {EntityComponentDataset} ecd
1156
+ */
1157
+ function spawnOrbiter(ecd) {
1158
+ const entity = buildSourceMarker(ecd, {
1060
1159
  position: new Vector3(ORBIT_CX + ORBIT_RADIUS, ORBIT_HEIGHT, ORBIT_CZ),
1061
- distanceMin: ORBIT_DIST_MIN,
1062
- distanceMax: ORBIT_DIST_MAX,
1063
1160
  markerColor: 0x99ff99,
1064
1161
  markerEmissive: 0x33cc55,
1065
1162
  lightIntensity: 9,
1066
1163
  lightDistance: 15, // ~30% smaller than the other markers' default reach
1067
1164
  });
1165
+ addSourceEmitter(ecd, entity, {
1166
+ id: "orbit-worldmode",
1167
+ clipPath: ORBIT_CLIP,
1168
+ distanceMin: ORBIT_DIST_MIN,
1169
+ distanceMax: ORBIT_DIST_MAX,
1170
+ });
1171
+
1068
1172
  const mover = new PrototypeMover();
1069
1173
  mover.kind = "orbit";
1070
1174
  mover.cx = ORBIT_CX;
@@ -1072,42 +1176,22 @@ function buildSources(ecd) {
1072
1176
  mover.radius = ORBIT_RADIUS;
1073
1177
  mover.height = ORBIT_HEIGHT;
1074
1178
  mover.speed = ORBIT_SPEED;
1075
- ecd.addComponentToEntity(orbiter.entity, mover); // PrototypeMotionSystem links it via (PrototypeMover, Transform)
1179
+ ecd.addComponentToEntity(entity, mover); // PrototypeMotionSystem links it via (PrototypeMover, Transform)
1076
1180
  }
1077
1181
 
1078
1182
  /**
1079
- * Spawn one 3D looping {@link AudioEmitter} (acoustic on) with a small emissive marker sphere so it's visible.
1080
- * Looping + 3D + autoplay the emitter is "managed": it sounds once the listener (player) exists and is
1081
- * within `distanceMax`, after the context resumes on the first click.
1183
+ * Build a source's visual: a small emissive marker sphere + a co-located point light (its colour taken from the
1184
+ * marker's emissive tint, so each source casts a coloured glow into its room). No {@link AudioEmitter} that is
1185
+ * attached separately ({@link addSourceEmitter}), so a static marker can be baked into the lightmap while its
1186
+ * audio waits for the first click. Not an acoustic occluder (no {@link AcousticBody}).
1082
1187
  *
1083
1188
  * @param {EntityComponentDataset} ecd
1084
- * @param {{id:string, clipPath:string, position:Vector3, distanceMin:number, distanceMax:number,
1085
- * markerColor:number, markerEmissive:number, lightIntensity?:number, lightDistance?:number}} p
1086
- * @returns {{entity:number, transform:Transform}} the spawned entity + its transform (so a mover can drive it)
1189
+ * @param {{position:Vector3, markerColor:number, markerEmissive:number, lightIntensity?:number,
1190
+ * lightDistance?:number}} p
1191
+ * @returns {number} the spawned entity id (so an emitter / mover can be attached later)
1087
1192
  */
1088
- function spawnAudioSource(ecd, { id, clipPath, position, distanceMin, distanceMax, markerColor, markerEmissive,
1089
- lightIntensity = SOURCE_LIGHT_INTENSITY, lightDistance = SOURCE_LIGHT_DISTANCE }) {
1090
- const event = EventDescription.from(
1091
- id,
1092
- SampleAudioClip.from(clipPath, { loop: true }),
1093
- {
1094
- is3D: true,
1095
- distanceMin,
1096
- distanceMax,
1097
- // Physically-based (sound-pressure-fitted) rolloff, sampled across [min, max] in metres: full
1098
- // volume within distanceMin, a natural log-ish falloff to silence at distanceMax. Reuses the
1099
- // engine's own falloff + curve builder rather than a hand-rolled line.
1100
- attenuation: buildAttenuationCurve(distanceMin, distanceMax, interpolate_irradiance_smith),
1101
- },
1102
- );
1103
-
1104
- const emitter = new AudioEmitter();
1105
- emitter.event = event;
1106
- emitter.acoustic = true; // occlusion / transmission / pathing
1107
- emitter.sourceRadius = 0.3; // a little volume so occlusion fades in/out rather than popping
1108
- emitter.autoplay = true;
1109
- emitter.volume.set(1);
1110
-
1193
+ function buildSourceMarker(ecd, { position, markerColor, markerEmissive,
1194
+ lightIntensity = SOURCE_LIGHT_INTENSITY, lightDistance = SOURCE_LIGHT_DISTANCE }) {
1111
1195
  const markerGeom = new SphereGeometry(0.3, 16, 16);
1112
1196
  const markerMat = litMaterial({
1113
1197
  color: markerColor,
@@ -1119,8 +1203,6 @@ function spawnAudioSource(ecd, { id, clipPath, position, distanceMin, distanceMa
1119
1203
  sg.setFlag(ShadedGeometryFlags.Visible);
1120
1204
  sg.setFlag(ShadedGeometryFlags.CastShadow);
1121
1205
 
1122
- // A point light co-located with the marker — the markers were already emissive, this lets each one cast a
1123
- // coloured glow into its room (the light colour is taken from the marker's emissive tint).
1124
1206
  const light = new Light();
1125
1207
  light.type.set(Light.Type.POINT);
1126
1208
  light.color.setRGBUint8((markerEmissive >> 16) & 0xff, (markerEmissive >> 8) & 0xff, markerEmissive & 0xff);
@@ -1131,7 +1213,6 @@ function spawnAudioSource(ecd, { id, clipPath, position, distanceMin, distanceMa
1131
1213
  transform.position.copy(position);
1132
1214
 
1133
1215
  const eb = new Entity()
1134
- .add(emitter)
1135
1216
  .add(sg)
1136
1217
  .add(light)
1137
1218
  .add(Tag.fromJSON(["AudioSource"]))
@@ -1139,7 +1220,43 @@ function spawnAudioSource(ecd, { id, clipPath, position, distanceMin, distanceMa
1139
1220
  .add(transform);
1140
1221
  eb.build(ecd);
1141
1222
 
1142
- return { entity: eb.id, transform };
1223
+ return eb.id;
1224
+ }
1225
+
1226
+ /**
1227
+ * Attach a 3D looping, acoustic, autoplay {@link AudioEmitter} to a pre-built source marker entity. Done on the
1228
+ * first click, once the context has resumed, so the emitter links (and autoplays) on a RUNNING context — the
1229
+ * autoplay-policy-safe path. It shares the marker's Transform, so it sounds from the marker and tracks it if a
1230
+ * mover drives it.
1231
+ *
1232
+ * @param {EntityComponentDataset} ecd
1233
+ * @param {number} entity the marker built by {@link buildSourceMarker}
1234
+ * @param {{id:string, clipPath:string, distanceMin:number, distanceMax:number}} p
1235
+ */
1236
+ function addSourceEmitter(ecd, entity, { id, clipPath, distanceMin, distanceMax }) {
1237
+ const event = EventDescription.from(
1238
+ id,
1239
+ SampleAudioClip.from(clipPath, { loop: true }),
1240
+ {
1241
+ is3D: true,
1242
+ distanceMin,
1243
+ distanceMax,
1244
+ // Physically-based (sound-pressure-fitted) rolloff, sampled across [min, max] in metres: full
1245
+ // volume within distanceMin, a natural log-ish falloff to silence at distanceMax. Reuses the
1246
+ // engine's own falloff + curve builder rather than a hand-rolled line.
1247
+ attenuation: buildAttenuationCurve(distanceMin, distanceMax, interpolate_irradiance_smith),
1248
+ },
1249
+ );
1250
+
1251
+ const emitter = new AudioEmitter();
1252
+ emitter.event = event;
1253
+ emitter.acoustic = true; // occlusion / transmission / pathing
1254
+ emitter.sourceRadius = 0.3; // a little volume so occlusion fades in/out rather than popping
1255
+ emitter.autoplay = true;
1256
+ emitter.volume.set(1);
1257
+
1258
+ // (AudioEmitter, Transform) completes on this add → AudioEmitterSystem links it (and autoplays) immediately.
1259
+ ecd.addComponentToEntity(entity, emitter);
1143
1260
  }
1144
1261
 
1145
1262
  /**
@@ -1311,15 +1428,16 @@ function buildInputBindings(ecd, player, motion) {
1311
1428
  * Wire the canvas click to (a) start audio and (b) capture the mouse.
1312
1429
  *
1313
1430
  * The autoplay policy requires a user gesture before audio: the engine creates the context suspended and
1314
- * starts ticking immediately, so we must not let the source play before the first click. On that click we
1315
- * resume the context and — only once `resume()` has actually resolved — spawn the source, so its first
1316
- * `playEvent` lands on a running context (no autoplay warning). Pointer lock is (re)requested every click;
1317
- * ESC (browser default) releases it.
1431
+ * starts ticking immediately, so we must not let a source play before the first click. On that click we resume
1432
+ * the context and — only once `resume()` has actually resolved — start the sources (attach emitters to the
1433
+ * pre-built markers + spawn the orbiter), so their first `playEvent` lands on a running context (no autoplay
1434
+ * warning). Pointer lock is (re)requested every click; ESC (browser default) releases it.
1318
1435
  *
1319
1436
  * @param {Engine} engine
1320
1437
  * @param {EntityComponentDataset} ecd
1438
+ * @param {{entity:number, spec:object}[]} staticSources the static source markers to attach emitters to
1321
1439
  */
1322
- function setupInteraction(engine, ecd) {
1440
+ function setupInteraction(engine, ecd, staticSources) {
1323
1441
  const el = engine.graphics.domElement;
1324
1442
  const captureEl = engine.viewStack.el;
1325
1443
 
@@ -1328,9 +1446,9 @@ function setupInteraction(engine, ecd) {
1328
1446
  captureEl.addEventListener("click", () => {
1329
1447
  if (!audioStarted) {
1330
1448
  audioStarted = true;
1331
- // resume() returns the context.resume() promise — spawn the sources only after it resolves, so the
1332
- // first playEvent lands on a running context.
1333
- engine.sound.resumeContext().then(() => buildSources(ecd));
1449
+ // resume() returns the context.resume() promise — start the sources (attach emitters to the pre-built
1450
+ // markers + spawn the orbiter) only after it resolves, so the first playEvent lands on a running context.
1451
+ engine.sound.resumeContext().then(() => startSources(ecd, staticSources));
1334
1452
  }
1335
1453
 
1336
1454
  if (document.pointerLockElement === el || !el.requestPointerLock) return;