@woosh/meep-engine 3.27.0 → 3.29.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 (648) hide show
  1. package/build/bundle-worker-terrain.js +1 -1
  2. package/package.json +1 -1
  3. package/src/core/binary/BitSet.d.ts +16 -0
  4. package/src/core/binary/BitSet.d.ts.map +1 -1
  5. package/src/core/binary/BitSet.js +159 -49
  6. package/src/core/binary/allocator/OffsetAllocator.d.ts +48 -0
  7. package/src/core/binary/allocator/OffsetAllocator.d.ts.map +1 -1
  8. package/src/core/binary/allocator/OffsetAllocator.js +208 -9
  9. package/src/core/binary/compression/compress_bytes.d.ts +20 -0
  10. package/src/core/binary/compression/compress_bytes.d.ts.map +1 -0
  11. package/src/core/binary/compression/compress_bytes.js +35 -0
  12. package/src/core/binary/compression/decompress_bytes_into.d.ts +30 -0
  13. package/src/core/binary/compression/decompress_bytes_into.d.ts.map +1 -0
  14. package/src/core/binary/compression/decompress_bytes_into.js +87 -0
  15. package/src/core/binary/compression/deflate_decompress_bound.d.ts +27 -0
  16. package/src/core/binary/compression/deflate_decompress_bound.d.ts.map +1 -0
  17. package/src/core/binary/compression/deflate_decompress_bound.js +32 -0
  18. package/src/core/geom/3d/mat4/m4_linear_determinant.d.ts +15 -0
  19. package/src/core/geom/3d/mat4/m4_linear_determinant.d.ts.map +1 -0
  20. package/src/core/geom/3d/mat4/m4_linear_determinant.js +24 -0
  21. package/src/core/geom/3d/mat4/m4_normal_matrix3.d.ts +4 -2
  22. package/src/core/geom/3d/mat4/m4_normal_matrix3.d.ts.map +1 -1
  23. package/src/core/geom/3d/mat4/m4_normal_matrix3.js +21 -11
  24. package/src/core/geom/3d/quadric/quadric_attribute_add.d.ts +24 -0
  25. package/src/core/geom/3d/quadric/quadric_attribute_add.d.ts.map +1 -0
  26. package/src/core/geom/3d/quadric/quadric_attribute_add.js +31 -0
  27. package/src/core/geom/3d/quadric/quadric_attribute_reduce.d.ts +46 -0
  28. package/src/core/geom/3d/quadric/quadric_attribute_reduce.d.ts.map +1 -0
  29. package/src/core/geom/3d/quadric/quadric_attribute_reduce.js +104 -0
  30. package/src/core/geom/3d/quadric/quadric_attribute_set_from_triangle.d.ts +62 -0
  31. package/src/core/geom/3d/quadric/quadric_attribute_set_from_triangle.d.ts.map +1 -0
  32. package/src/core/geom/3d/quadric/quadric_attribute_set_from_triangle.js +158 -0
  33. package/src/core/geom/3d/quadric/quadric_attribute_size.d.ts +21 -0
  34. package/src/core/geom/3d/quadric/quadric_attribute_size.d.ts.map +1 -0
  35. package/src/core/geom/3d/quadric/quadric_attribute_size.js +24 -0
  36. package/src/core/geom/3d/quadric/quadric_attribute_solve.d.ts +34 -0
  37. package/src/core/geom/3d/quadric/quadric_attribute_solve.d.ts.map +1 -0
  38. package/src/core/geom/3d/quadric/quadric_attribute_solve.js +55 -0
  39. package/src/core/geom/3d/quadric/quadric_attribute_sub.d.ts +28 -0
  40. package/src/core/geom/3d/quadric/quadric_attribute_sub.d.ts.map +1 -0
  41. package/src/core/geom/3d/quadric/quadric_attribute_sub.js +35 -0
  42. package/src/core/geom/3d/topology/struct/binary/BinaryDataBlock.d.ts +15 -0
  43. package/src/core/geom/3d/topology/struct/binary/BinaryDataBlock.d.ts.map +1 -1
  44. package/src/core/geom/3d/topology/struct/binary/BinaryDataBlock.js +35 -0
  45. package/src/core/geom/3d/topology/struct/binary/BtWedgeSet.d.ts +93 -0
  46. package/src/core/geom/3d/topology/struct/binary/BtWedgeSet.d.ts.map +1 -0
  47. package/src/core/geom/3d/topology/struct/binary/BtWedgeSet.js +128 -0
  48. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_wedge_attribute_quadrics.d.ts +38 -0
  49. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_wedge_attribute_quadrics.d.ts.map +1 -0
  50. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_wedge_attribute_quadrics.js +113 -0
  51. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts +47 -1
  52. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts.map +1 -1
  53. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.js +677 -8
  54. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_live_count.d.ts +22 -0
  55. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_live_count.d.ts.map +1 -0
  56. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_live_count.js +46 -0
  57. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_per_vertex.d.ts +19 -0
  58. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_per_vertex.d.ts.map +1 -0
  59. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_per_vertex.js +49 -0
  60. package/src/core/graph/layout/layered/BoxLayoutOptions.d.ts +63 -0
  61. package/src/core/graph/layout/layered/BoxLayoutOptions.d.ts.map +1 -0
  62. package/src/core/graph/layout/layered/BoxLayoutOptions.js +80 -0
  63. package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/LayoutBoxGraph.d.ts +35 -0
  64. package/src/core/graph/layout/layered/LayoutBoxGraph.d.ts.map +1 -0
  65. package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/LayoutBoxGraph.js +53 -1
  66. package/src/core/graph/layout/layered/layout_assign_coordinates.d.ts.map +1 -0
  67. package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_assign_coordinates.js +72 -68
  68. package/src/core/graph/layout/layered/layout_assign_layers.d.ts.map +1 -0
  69. package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_assign_layers.js +4 -2
  70. package/src/core/graph/layout/layered/layout_break_cycles.d.ts.map +1 -0
  71. package/src/core/graph/layout/layered/layout_insert_lanes.d.ts.map +1 -0
  72. package/src/core/graph/layout/layered/layout_order_layers.d.ts.map +1 -0
  73. package/src/core/graph/layout/layered/layout_orient_links.d.ts.map +1 -0
  74. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.d.ts +7 -51
  75. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.d.ts.map +1 -1
  76. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.js +8 -69
  77. package/src/core/model/node-graph/visual/layout/layout_build_problem.js +1 -1
  78. package/src/core/model/node-graph/visual/layout/layout_node_graph.js +6 -6
  79. package/src/core/model/node-graph/visual/layout/route_grid_build.js +1 -1
  80. package/src/engine/Engine.d.ts.map +1 -1
  81. package/src/engine/Engine.js +38 -1
  82. package/src/engine/graphics3/DebugDrawSystem.d.ts +7 -5
  83. package/src/engine/graphics3/DebugDrawSystem.d.ts.map +1 -1
  84. package/src/engine/graphics3/DebugDrawSystem.js +206 -203
  85. package/src/engine/graphics3/DecalSystem.d.ts +4 -5
  86. package/src/engine/graphics3/DecalSystem.d.ts.map +1 -1
  87. package/src/engine/graphics3/DecalSystem.js +5 -5
  88. package/src/engine/graphics3/FogOfWarSystem.d.ts +7 -12
  89. package/src/engine/graphics3/FogOfWarSystem.d.ts.map +1 -1
  90. package/src/engine/graphics3/FogOfWarSystem.js +8 -12
  91. package/src/engine/graphics3/GraphicsEngine.d.ts +5 -1
  92. package/src/engine/graphics3/GraphicsEngine.d.ts.map +1 -1
  93. package/src/engine/graphics3/GraphicsEngine.js +834 -830
  94. package/src/engine/graphics3/prefab/prefab_instantiate.js +230 -230
  95. package/src/shade/RENDERER_CONTRACT.md +31 -17
  96. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +1 -1
  97. package/src/shade/playground/caldera/README.md +529 -0
  98. package/src/shade/playground/caldera/caldera_format.d.ts +151 -0
  99. package/src/shade/playground/caldera/caldera_format.d.ts.map +1 -0
  100. package/src/shade/playground/caldera/caldera_format.js +241 -0
  101. package/src/shade/playground/caldera/index.html +126 -0
  102. package/src/shade/playground/caldera/main.d.ts.map +1 -0
  103. package/src/shade/playground/caldera/main.js +1565 -0
  104. package/src/shade/playground/mesh_row_ceiling/README.md +85 -0
  105. package/src/shade/playground/mesh_row_ceiling/index.html +31 -0
  106. package/src/shade/playground/mesh_row_ceiling/main.d.ts +2 -0
  107. package/src/shade/playground/mesh_row_ceiling/main.d.ts.map +1 -0
  108. package/src/shade/playground/mesh_row_ceiling/main.js +389 -0
  109. package/src/shade/playground/vgeo_runtime/README.md +32 -16
  110. package/src/shade/playground/vgeo_runtime/main.js +21 -7
  111. package/src/shade/playground/{vgeo_viewer → vgeo_runtime}/sample_asset.d.ts +3 -3
  112. package/src/shade/playground/vgeo_runtime/sample_asset.d.ts.map +1 -0
  113. package/src/shade/playground/{vgeo_viewer → vgeo_runtime}/sample_asset.js +12 -11
  114. package/src/shade/playground/vgeo_scene/README.md +487 -0
  115. package/src/shade/playground/vgeo_scene/index.html +144 -0
  116. package/src/shade/playground/vgeo_scene/main.d.ts +2 -0
  117. package/src/shade/playground/vgeo_scene/main.d.ts.map +1 -0
  118. package/src/shade/playground/vgeo_scene/main.js +2217 -0
  119. package/src/shade/renderer/GraphicsContext.d.ts.map +1 -1
  120. package/src/shade/renderer/GraphicsContext.js +6 -0
  121. package/src/shade/renderer/Renderer.d.ts +6 -1
  122. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  123. package/src/shade/renderer/Renderer.js +28 -2
  124. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.d.ts.map +1 -1
  125. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.js +100 -8
  126. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +70 -35
  127. package/src/shade/renderer/extension/RenderExtension.d.ts +15 -18
  128. package/src/shade/renderer/extension/RenderExtension.d.ts.map +1 -1
  129. package/src/shade/renderer/extension/RenderExtension.js +15 -19
  130. package/src/shade/renderer/extension/RenderExtensionRegistry.d.ts +5 -5
  131. package/src/shade/renderer/extension/RenderExtensionRegistry.d.ts.map +1 -1
  132. package/src/shade/renderer/extension/RenderExtensionRegistry.js +29 -166
  133. package/src/shade/renderer/geometry/GPUGeometryManager.d.ts.map +1 -1
  134. package/src/shade/renderer/geometry/GPUGeometryManager.js +73 -16
  135. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts.map +1 -1
  136. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.js +18 -3
  137. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +663 -0
  138. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts +100 -5
  139. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts.map +1 -1
  140. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.js +688 -81
  141. package/src/shade/renderer/geometry/meshlet/MeshletBatch.d.ts.map +1 -1
  142. package/src/shade/renderer/geometry/meshlet/MeshletBatch.js +3 -29
  143. package/src/shade/renderer/geometry/meshlet/encoding/ENCODED_ATTRIBUTE_DEFAULT_VALUES.d.ts +22 -0
  144. package/src/shade/renderer/geometry/meshlet/encoding/ENCODED_ATTRIBUTE_DEFAULT_VALUES.d.ts.map +1 -0
  145. package/src/shade/renderer/geometry/meshlet/encoding/ENCODED_ATTRIBUTE_DEFAULT_VALUES.js +49 -0
  146. package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +540 -0
  147. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +159 -66
  148. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +40 -21
  149. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +15 -14
  150. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +358 -0
  151. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +58 -8
  152. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
  153. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +64 -10
  154. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_open_document.d.ts.map +1 -1
  155. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_open_document.js +5 -9
  156. package/src/shade/renderer/geometry/virtual/build/level/VGeoContribution.d.ts +40 -0
  157. package/src/shade/renderer/geometry/virtual/build/level/VGeoContribution.d.ts.map +1 -0
  158. package/src/shade/renderer/geometry/virtual/build/level/VGeoContribution.js +24 -0
  159. package/src/shade/renderer/geometry/virtual/build/level/VGeoFrontier.d.ts +91 -0
  160. package/src/shade/renderer/geometry/virtual/build/level/VGeoFrontier.d.ts.map +1 -0
  161. package/src/shade/renderer/geometry/virtual/build/level/VGeoFrontier.js +91 -0
  162. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupRepack.d.ts +24 -0
  163. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupRepack.d.ts.map +1 -0
  164. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupRepack.js +25 -0
  165. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupStep.d.ts +77 -0
  166. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupStep.d.ts.map +1 -0
  167. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupStep.js +84 -0
  168. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevel.d.ts.map +1 -0
  169. package/src/shade/renderer/geometry/virtual/build/{VGeoLevelOptions.d.ts → level/VGeoLevelOptions.d.ts} +19 -6
  170. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelOptions.d.ts.map +1 -0
  171. package/src/shade/renderer/geometry/virtual/build/{VGeoLevelOptions.js → level/VGeoLevelOptions.js} +20 -7
  172. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelSummary.d.ts.map +1 -0
  173. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster.d.ts +19 -0
  174. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster.d.ts.map +1 -0
  175. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster.js +58 -0
  176. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster_graph.d.ts +18 -0
  177. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster_graph.d.ts.map +1 -0
  178. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster_graph.js +116 -0
  179. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.d.ts +18 -0
  180. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.d.ts.map +1 -0
  181. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.js +545 -0
  182. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.d.ts +16 -0
  183. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.d.ts.map +1 -0
  184. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.js +110 -0
  185. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.d.ts +11 -0
  186. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.d.ts.map +1 -0
  187. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.js +308 -0
  188. package/src/shade/renderer/geometry/virtual/build/{vgeo_build_root_group.d.ts → level/vgeo_build_root_group.d.ts} +1 -1
  189. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_root_group.d.ts.map +1 -0
  190. package/src/shade/renderer/geometry/virtual/build/{vgeo_build_root_group.js → level/vgeo_build_root_group.js} +1 -1
  191. package/src/shade/renderer/geometry/virtual/build/level/vgeo_compute_group_boundary.d.ts +26 -0
  192. package/src/shade/renderer/geometry/virtual/build/level/vgeo_compute_group_boundary.d.ts.map +1 -0
  193. package/src/shade/renderer/geometry/virtual/build/level/vgeo_compute_group_boundary.js +123 -0
  194. package/src/shade/renderer/geometry/virtual/build/level/vgeo_error_bands.d.ts +26 -0
  195. package/src/shade/renderer/geometry/virtual/build/level/vgeo_error_bands.d.ts.map +1 -0
  196. package/src/shade/renderer/geometry/virtual/build/level/vgeo_error_bands.js +78 -0
  197. package/src/shade/renderer/geometry/virtual/build/level/vgeo_partition_into_groups.d.ts +24 -0
  198. package/src/shade/renderer/geometry/virtual/build/level/vgeo_partition_into_groups.d.ts.map +1 -0
  199. package/src/shade/renderer/geometry/virtual/build/level/vgeo_partition_into_groups.js +337 -0
  200. package/src/shade/renderer/geometry/virtual/build/level/vgeo_run_round.d.ts +29 -0
  201. package/src/shade/renderer/geometry/virtual/build/level/vgeo_run_round.d.ts.map +1 -0
  202. package/src/shade/renderer/geometry/virtual/build/level/vgeo_run_round.js +147 -0
  203. package/src/shade/renderer/geometry/virtual/build/level/vgeo_stitch_frontier.d.ts +37 -0
  204. package/src/shade/renderer/geometry/virtual/build/level/vgeo_stitch_frontier.d.ts.map +1 -0
  205. package/src/shade/renderer/geometry/virtual/build/level/vgeo_stitch_frontier.js +243 -0
  206. package/src/shade/renderer/geometry/virtual/build/level/vgeo_try_repack.d.ts +29 -0
  207. package/src/shade/renderer/geometry/virtual/build/level/vgeo_try_repack.d.ts.map +1 -0
  208. package/src/shade/renderer/geometry/virtual/build/level/vgeo_try_repack.js +117 -0
  209. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_CREASE_ANGLE.d.ts +46 -0
  210. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_CREASE_ANGLE.d.ts.map +1 -0
  211. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_CREASE_ANGLE.js +45 -0
  212. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.d.ts +64 -0
  213. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.d.ts.map +1 -0
  214. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.js +68 -0
  215. package/src/shade/renderer/geometry/virtual/build/{VirtualGeometryMesh.d.ts → mesh/VirtualGeometryMesh.d.ts} +13 -8
  216. package/src/shade/renderer/geometry/virtual/build/mesh/VirtualGeometryMesh.d.ts.map +1 -0
  217. package/src/shade/renderer/geometry/virtual/build/{VirtualGeometryMesh.js → mesh/VirtualGeometryMesh.js} +40 -18
  218. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_layout.d.ts +58 -0
  219. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_layout.d.ts.map +1 -0
  220. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_layout.js +79 -0
  221. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_read.d.ts +39 -0
  222. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_read.d.ts.map +1 -0
  223. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_read.js +68 -0
  224. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_write.d.ts +53 -0
  225. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_write.d.ts.map +1 -0
  226. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_write.js +243 -0
  227. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts +45 -0
  228. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts.map +1 -0
  229. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.js +300 -0
  230. package/src/shade/renderer/geometry/virtual/build/{vgeo_compute_vertex_tangents.d.ts → mesh/vgeo_compute_vertex_tangents.d.ts} +12 -8
  231. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_tangents.d.ts.map +1 -0
  232. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_tangents.js +151 -0
  233. package/src/shade/renderer/geometry/virtual/build/{vgeo_normalize_source.d.ts → mesh/vgeo_normalize_source.d.ts} +8 -1
  234. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts.map +1 -0
  235. package/src/shade/renderer/geometry/virtual/build/{vgeo_normalize_source.js → mesh/vgeo_normalize_source.js} +27 -12
  236. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_resolve_attribute_weights.d.ts +38 -0
  237. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_resolve_attribute_weights.d.ts.map +1 -0
  238. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_resolve_attribute_weights.js +81 -0
  239. package/src/shade/renderer/geometry/virtual/build/{vgeo_assemble_pages.d.ts → page/vgeo_assemble_pages.d.ts} +1 -1
  240. package/src/shade/renderer/geometry/virtual/build/page/vgeo_assemble_pages.d.ts.map +1 -0
  241. package/src/shade/renderer/geometry/virtual/build/{vgeo_assemble_pages.js → page/vgeo_assemble_pages.js} +9 -9
  242. package/src/shade/renderer/geometry/virtual/build/{vgeo_build_page_nodes.d.ts → page/vgeo_build_page_nodes.d.ts} +1 -1
  243. package/src/shade/renderer/geometry/virtual/build/page/vgeo_build_page_nodes.d.ts.map +1 -0
  244. package/src/shade/renderer/geometry/virtual/build/{vgeo_build_page_nodes.js → page/vgeo_build_page_nodes.js} +4 -4
  245. package/src/shade/renderer/geometry/virtual/build/page/vgeo_page_node_count.d.ts.map +1 -0
  246. package/src/shade/renderer/geometry/virtual/build/{vgeo_page_node_count.js → page/vgeo_page_node_count.js} +2 -2
  247. package/src/shade/renderer/geometry/virtual/build/{VGeoFaceGraph.d.ts → partition/VGeoFaceGraph.d.ts} +23 -5
  248. package/src/shade/renderer/geometry/virtual/build/partition/VGeoFaceGraph.d.ts.map +1 -0
  249. package/src/shade/renderer/geometry/virtual/build/{VGeoFaceGraph.js → partition/VGeoFaceGraph.js} +28 -21
  250. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_graph_components.d.ts.map +1 -0
  251. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_graph.d.ts.map +1 -0
  252. package/src/shade/renderer/geometry/virtual/build/{vgeo_partition_graph.js → partition/vgeo_partition_graph.js} +2 -2
  253. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_subgraph.d.ts +41 -0
  254. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_subgraph.d.ts.map +1 -0
  255. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_subgraph.js +92 -0
  256. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set.d.ts.map +1 -0
  257. package/src/shade/renderer/geometry/virtual/build/{vgeo_split_face_set.js → partition/vgeo_split_face_set.js} +13 -56
  258. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.d.ts.map +1 -0
  259. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.js +371 -0
  260. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_bounding_sphere.d.ts.map +1 -0
  261. package/src/shade/renderer/geometry/virtual/build/{vgeo_bounding_sphere.js → sphere/vgeo_bounding_sphere.js} +3 -3
  262. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_containing_sphere.d.ts.map +1 -0
  263. package/src/shade/renderer/geometry/virtual/build/{vgeo_containing_sphere.js → sphere/vgeo_containing_sphere.js} +3 -3
  264. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_sphere_inflate_to_contain.d.ts.map +1 -0
  265. package/src/shade/renderer/geometry/virtual/build/{vgeo_sphere_inflate_to_contain.js → sphere/vgeo_sphere_inflate_to_contain.js} +1 -1
  266. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.d.ts +39 -0
  267. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.d.ts.map +1 -0
  268. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.js +42 -0
  269. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.d.ts +37 -0
  270. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.d.ts.map +1 -0
  271. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.js +39 -0
  272. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.d.ts +23 -0
  273. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.d.ts.map +1 -0
  274. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.js +135 -0
  275. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.d.ts +24 -0
  276. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.d.ts.map +1 -0
  277. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.js +95 -0
  278. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_narrow_indices.d.ts.map +1 -0
  279. package/src/shade/renderer/geometry/virtual/build/{vgeo_narrow_indices.js → submesh/vgeo_narrow_indices.js} +3 -3
  280. package/src/shade/renderer/geometry/virtual/build/vgeo_build.d.ts +6 -2
  281. package/src/shade/renderer/geometry/virtual/build/vgeo_build.d.ts.map +1 -1
  282. package/src/shade/renderer/geometry/virtual/build/vgeo_build.js +37 -16
  283. package/src/shade/renderer/geometry/virtual/build/vgeo_subtree_bounds.d.ts +32 -0
  284. package/src/shade/renderer/geometry/virtual/build/vgeo_subtree_bounds.d.ts.map +1 -0
  285. package/src/shade/renderer/geometry/virtual/build/vgeo_subtree_bounds.js +93 -0
  286. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts +20 -6
  287. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts.map +1 -1
  288. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.js +22 -8
  289. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attributes_read.d.ts +8 -2
  290. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attributes_read.d.ts.map +1 -1
  291. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attributes_read.js +8 -2
  292. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_copy_corner.d.ts +16 -0
  293. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_copy_corner.d.ts.map +1 -0
  294. package/src/shade/renderer/geometry/virtual/format/attribute/{vgeo_layers_copy_vertex.js → vgeo_layers_copy_corner.js} +8 -8
  295. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_declare.d.ts +18 -7
  296. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_declare.d.ts.map +1 -1
  297. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_declare.js +19 -8
  298. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_like.d.ts +3 -3
  299. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_like.js +3 -3
  300. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_mesh_wedges.d.ts +27 -0
  301. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_mesh_wedges.d.ts.map +1 -0
  302. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_mesh_wedges.js +97 -0
  303. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET.d.ts +7 -3
  304. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET.d.ts.map +1 -1
  305. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET.js +21 -11
  306. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET_V4.d.ts +21 -0
  307. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET_V4.d.ts.map +1 -0
  308. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET_V4.js +41 -0
  309. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryCluster.d.ts +19 -1
  310. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryCluster.d.ts.map +1 -1
  311. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryCluster.js +20 -1
  312. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_DEFLATE.d.ts +25 -0
  313. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_DEFLATE.d.ts.map +1 -0
  314. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_DEFLATE.js +24 -0
  315. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_LZ4.d.ts +5 -0
  316. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_LZ4.d.ts.map +1 -1
  317. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_LZ4.js +5 -0
  318. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_codec_name.d.ts.map +1 -1
  319. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_codec_name.js +5 -0
  320. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_decode_frame_blob.d.ts +10 -3
  321. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_decode_frame_blob.d.ts.map +1 -1
  322. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_decode_frame_blob.js +37 -3
  323. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_encode_frame_blob.d.ts +21 -13
  324. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_encode_frame_blob.d.ts.map +1 -1
  325. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_encode_frame_blob.js +37 -13
  326. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_frame_decode_bound.d.ts +8 -2
  327. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_frame_decode_bound.d.ts.map +1 -1
  328. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_frame_decode_bound.js +14 -2
  329. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FORMAT_VERSION.d.ts +18 -0
  330. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FORMAT_VERSION.d.ts.map +1 -1
  331. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FORMAT_VERSION.js +19 -1
  332. package/src/shade/renderer/geometry/virtual/format/header/VGEO_HEADER_OFFSET.js +6 -0
  333. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MAX_LEVEL_COUNT.d.ts +21 -0
  334. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MAX_LEVEL_COUNT.d.ts.map +1 -0
  335. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MAX_LEVEL_COUNT.js +20 -0
  336. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MIN_READER_VERSION.d.ts +9 -0
  337. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MIN_READER_VERSION.d.ts.map +1 -1
  338. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MIN_READER_VERSION.js +10 -1
  339. package/src/shade/renderer/geometry/virtual/format/page/VGEO_GROUP_OFFSET.d.ts +15 -0
  340. package/src/shade/renderer/geometry/virtual/format/page/VGEO_GROUP_OFFSET.d.ts.map +1 -0
  341. package/src/shade/renderer/geometry/virtual/format/page/VGEO_GROUP_OFFSET.js +15 -0
  342. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET.d.ts +1 -2
  343. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET.js +10 -3
  344. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET_V5.d.ts +20 -0
  345. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET_V5.d.ts.map +1 -0
  346. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET_V5.js +43 -0
  347. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.d.ts +32 -2
  348. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.d.ts.map +1 -1
  349. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.js +15 -7
  350. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.d.ts.map +1 -1
  351. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.js +2 -2
  352. package/src/shade/renderer/geometry/virtual/format/read/VGeoDirectory.d.ts +6 -4
  353. package/src/shade/renderer/geometry/virtual/format/read/VGeoDirectory.d.ts.map +1 -1
  354. package/src/shade/renderer/geometry/virtual/format/read/VGeoDirectory.js +6 -4
  355. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts +13 -2
  356. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts.map +1 -1
  357. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.js +13 -2
  358. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageFetcherCore.d.ts +5 -1
  359. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageFetcherCore.d.ts.map +1 -1
  360. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageFetcherCore.js +14 -1
  361. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_worker_serve.d.ts.map +1 -1
  362. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_worker_serve.js +1 -0
  363. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_directory.d.ts +4 -1
  364. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_directory.d.ts.map +1 -1
  365. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_directory.js +53 -11
  366. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.d.ts.map +1 -1
  367. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.js +7 -3
  368. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts +12 -2
  369. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts.map +1 -1
  370. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.js +120 -37
  371. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts +6 -2
  372. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts.map +1 -1
  373. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.js +130 -47
  374. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts +2 -2
  375. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
  376. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +60 -22
  377. package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +385 -0
  378. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +21 -2
  379. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts +52 -2
  380. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts.map +1 -1
  381. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.js +97 -1
  382. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +4 -4
  383. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts.map +1 -1
  384. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.js +10 -0
  385. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.d.ts.map +1 -1
  386. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.js +9 -0
  387. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.d.ts.map +1 -1
  388. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.js +59 -34
  389. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts +8 -8
  390. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts.map +1 -1
  391. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.js +45 -88
  392. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.d.ts +19 -0
  393. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.d.ts.map +1 -0
  394. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.js +116 -0
  395. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.d.ts +31 -0
  396. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.d.ts.map +1 -0
  397. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.js +99 -0
  398. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_queue.d.ts +17 -0
  399. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_queue.d.ts.map +1 -0
  400. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_queue.js +39 -0
  401. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_reseed.d.ts.map +1 -1
  402. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_reseed.js +7 -63
  403. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts.map +1 -1
  404. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.js +13 -30
  405. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts.map +1 -1
  406. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.js +53 -71
  407. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.d.ts +1 -0
  408. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.js +22 -4
  409. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.d.ts +7 -2
  410. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.d.ts.map +1 -1
  411. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.js +8 -3
  412. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.d.ts +1 -0
  413. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.js +10 -0
  414. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.js +1 -1
  415. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.d.ts +13 -11
  416. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.d.ts.map +1 -1
  417. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.js +13 -11
  418. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts.map +1 -1
  419. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.js +4 -0
  420. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_evict.d.ts.map +1 -1
  421. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_evict.js +3 -4
  422. package/src/shade/renderer/gpu_primitive/indirect/IndirectComputeCommandBuildPass.d.ts +6 -2
  423. package/src/shade/renderer/gpu_primitive/indirect/IndirectComputeCommandBuildPass.d.ts.map +1 -1
  424. package/src/shade/renderer/gpu_primitive/indirect/IndirectComputeCommandBuildPass.js +90 -12
  425. package/src/shade/renderer/gpu_primitive/indirect/WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION.d.ts +22 -0
  426. package/src/shade/renderer/gpu_primitive/indirect/WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION.d.ts.map +1 -0
  427. package/src/shade/renderer/gpu_primitive/indirect/WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION.js +21 -0
  428. package/src/shade/renderer/loader/gltf/load_gltf.d.ts.map +1 -1
  429. package/src/shade/renderer/loader/gltf/load_gltf.js +0 -19
  430. package/src/shade/renderer/material/chunk_material_draw_side.d.ts +14 -0
  431. package/src/shade/renderer/material/chunk_material_draw_side.d.ts.map +1 -0
  432. package/src/shade/renderer/material/chunk_material_draw_side.js +34 -0
  433. package/src/shade/renderer/material/standard/fragment_gbuffer.d.ts.map +1 -1
  434. package/src/shade/renderer/material/standard/fragment_gbuffer.js +8 -1
  435. package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
  436. package/src/shade/renderer/particles/shaders/shader_particle_emit.js +13 -8
  437. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
  438. package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +3 -6
  439. package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.d.ts.map +1 -1
  440. package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.js +13 -1
  441. package/src/shade/renderer/rasterize/bucket/construct_primitive_state.d.ts +19 -5
  442. package/src/shade/renderer/rasterize/bucket/construct_primitive_state.d.ts.map +1 -1
  443. package/src/shade/renderer/rasterize/bucket/construct_primitive_state.js +23 -5
  444. package/src/shade/renderer/rasterize/bucket/filter_materials_by_bucket.d.ts.map +1 -1
  445. package/src/shade/renderer/rasterize/bucket/filter_materials_by_bucket.js +12 -5
  446. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
  447. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +4 -12
  448. package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.d.ts +22 -0
  449. package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.d.ts.map +1 -0
  450. package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.js +36 -0
  451. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.d.ts +0 -2
  452. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.d.ts.map +1 -1
  453. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.js +4 -5
  454. package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.d.ts +15 -0
  455. package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.d.ts.map +1 -0
  456. package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.js +35 -0
  457. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_mesh_filter_2way.d.ts.map +1 -1
  458. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_mesh_filter_2way.js +5 -3
  459. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way.d.ts.map +1 -1
  460. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way.js +11 -4
  461. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.d.ts.map +1 -1
  462. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.js +4 -3
  463. package/src/shade/renderer/rasterize/expand/bucket/RASTERIZATION_BUCKET_LIMIT.d.ts +10 -1
  464. package/src/shade/renderer/rasterize/expand/bucket/RASTERIZATION_BUCKET_LIMIT.d.ts.map +1 -1
  465. package/src/shade/renderer/rasterize/expand/bucket/RASTERIZATION_BUCKET_LIMIT.js +38 -29
  466. package/src/shade/renderer/rasterize/expand/bucket/chunk_rasterization_material_bucket.d.ts +5 -0
  467. package/src/shade/renderer/rasterize/expand/bucket/chunk_rasterization_material_bucket.d.ts.map +1 -1
  468. package/src/shade/renderer/rasterize/expand/bucket/chunk_rasterization_material_bucket.js +63 -55
  469. package/src/shade/renderer/rasterize/expand/bucket/meshlet/graph_sort_meshlets_by_bucket_id.d.ts.map +1 -1
  470. package/src/shade/renderer/rasterize/expand/bucket/meshlet/graph_sort_meshlets_by_bucket_id.js +3 -0
  471. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_count_by_rasterization_bucket.d.ts.map +1 -1
  472. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_count_by_rasterization_bucket.js +94 -90
  473. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.d.ts.map +1 -1
  474. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.js +13 -5
  475. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.d.ts.map +1 -1
  476. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.js +96 -89
  477. package/src/shade/renderer/rasterize/expand/graph_meshlets_filter_one_way.d.ts.map +1 -1
  478. package/src/shade/renderer/rasterize/expand/graph_meshlets_filter_one_way.js +3 -1
  479. package/src/shade/renderer/rasterize/expand/graph_meshlets_filter_two_way.d.ts.map +1 -1
  480. package/src/shade/renderer/rasterize/expand/graph_meshlets_filter_two_way.js +4 -1
  481. package/src/shade/renderer/rasterize/expand/graph_meshlets_rasterize.d.ts.map +1 -1
  482. package/src/shade/renderer/rasterize/expand/graph_meshlets_rasterize.js +1 -0
  483. package/src/shade/renderer/rasterize/expand/mesh/shader_mesh_expand_to_meshlets_prefix.d.ts.map +1 -1
  484. package/src/shade/renderer/rasterize/expand/mesh/shader_mesh_expand_to_meshlets_prefix.js +100 -89
  485. package/src/shade/renderer/rasterize/expand/mesh/shader_prefix_sum_to_command.d.ts.map +1 -1
  486. package/src/shade/renderer/rasterize/expand/mesh/shader_prefix_sum_to_command.js +31 -7
  487. package/src/shade/renderer/rasterize/expand/pass0/pass_mesh_cull.d.ts.map +1 -1
  488. package/src/shade/renderer/rasterize/expand/pass0/pass_mesh_cull.js +1 -7
  489. package/src/shade/renderer/rasterize/expand/pass1/filter_meshlets.2-way.compute.d.ts.map +1 -1
  490. package/src/shade/renderer/rasterize/expand/pass1/filter_meshlets.2-way.compute.js +161 -153
  491. package/src/shade/renderer/rasterize/fast/graph_extract_meshes_from_bucket.d.ts.map +1 -1
  492. package/src/shade/renderer/rasterize/fast/graph_extract_meshes_from_bucket.js +2 -7
  493. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
  494. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +44 -53
  495. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_brick4.d.ts.map +1 -1
  496. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_brick4.js +6 -2
  497. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_ibl.d.ts.map +1 -1
  498. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_ibl.js +6 -2
  499. package/src/shade/renderer/rasterize/forward/vertex_shader_chunk.d.ts.map +1 -1
  500. package/src/shade/renderer/rasterize/forward/vertex_shader_chunk.js +19 -2
  501. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +7 -4
  502. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts.map +1 -1
  503. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.js +3 -2
  504. package/src/shade/renderer/rasterize/native/chunk_rasterize_triangle.d.ts.map +1 -1
  505. package/src/shade/renderer/rasterize/native/chunk_rasterize_triangle.js +15 -4
  506. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_alpha_tested_pass_descriptor.d.ts.map +1 -1
  507. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_alpha_tested_pass_descriptor.js +16 -5
  508. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_pass_descriptor.d.ts.map +1 -1
  509. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_pass_descriptor.js +16 -5
  510. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.d.ts.map +1 -1
  511. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.js +2 -1
  512. package/src/shade/renderer/rasterize/native/rasterization_shader_resources.d.ts.map +1 -1
  513. package/src/shade/renderer/rasterize/native/rasterization_shader_resources.js +4 -0
  514. package/src/shade/renderer/rasterize/native/viz/viz_rasterization_alpha_tested_pass_descriptor.d.ts.map +1 -1
  515. package/src/shade/renderer/rasterize/native/viz/viz_rasterization_alpha_tested_pass_descriptor.js +16 -7
  516. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  517. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +81 -95
  518. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.d.ts.map +1 -1
  519. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +26 -39
  520. package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
  521. package/src/shade/renderer/scene/GPUSceneContext.js +2 -0
  522. package/src/shade/renderer/scene/NODE3D_STRUCT.d.ts +19 -0
  523. package/src/shade/renderer/scene/NODE3D_STRUCT.d.ts.map +1 -1
  524. package/src/shade/renderer/scene/NODE3D_STRUCT.js +34 -0
  525. package/src/shade/renderer/scene/chunk_node3d_flags.d.ts +9 -0
  526. package/src/shade/renderer/scene/chunk_node3d_flags.d.ts.map +1 -0
  527. package/src/shade/renderer/scene/chunk_node3d_flags.js +23 -0
  528. package/src/shade/renderer/scene/chunk_scene_database_access.d.ts.map +1 -1
  529. package/src/shade/renderer/scene/chunk_scene_database_access.js +15 -1
  530. package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts.map +1 -1
  531. package/src/shade/renderer/scene/rows/GPUSceneRows.js +9 -1
  532. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_compute_normal_matrix_from_m4.d.ts +7 -0
  533. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_compute_normal_matrix_from_m4.d.ts.map +1 -1
  534. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_compute_normal_matrix_from_m4.js +16 -4
  535. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_mat4_linear_determinant.d.ts +12 -0
  536. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_mat4_linear_determinant.d.ts.map +1 -0
  537. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_mat4_linear_determinant.js +18 -0
  538. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_triangle_list_reverse_winding.d.ts +24 -0
  539. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_triangle_list_reverse_winding.d.ts.map +1 -0
  540. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_triangle_list_reverse_winding.js +33 -0
  541. package/src/shade/renderer/shadow/map/DEFAULT_SHADOWMAP_UPDATE_BUDGET.d.ts +25 -0
  542. package/src/shade/renderer/shadow/map/DEFAULT_SHADOWMAP_UPDATE_BUDGET.d.ts.map +1 -0
  543. package/src/shade/renderer/shadow/map/DEFAULT_SHADOWMAP_UPDATE_BUDGET.js +24 -0
  544. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts +10 -0
  545. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  546. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +30 -21
  547. package/src/core/model/node-graph/visual/layout/LayoutBoxGraph.d.ts.map +0 -1
  548. package/src/core/model/node-graph/visual/layout/layout_assign_coordinates.d.ts.map +0 -1
  549. package/src/core/model/node-graph/visual/layout/layout_assign_layers.d.ts.map +0 -1
  550. package/src/core/model/node-graph/visual/layout/layout_break_cycles.d.ts.map +0 -1
  551. package/src/core/model/node-graph/visual/layout/layout_insert_lanes.d.ts.map +0 -1
  552. package/src/core/model/node-graph/visual/layout/layout_order_layers.d.ts.map +0 -1
  553. package/src/core/model/node-graph/visual/layout/layout_orient_links.d.ts.map +0 -1
  554. package/src/shade/playground/vgeo_viewer/README.md +0 -150
  555. package/src/shade/playground/vgeo_viewer/cut_geometry.d.ts +0 -61
  556. package/src/shade/playground/vgeo_viewer/cut_geometry.d.ts.map +0 -1
  557. package/src/shade/playground/vgeo_viewer/cut_geometry.js +0 -460
  558. package/src/shade/playground/vgeo_viewer/index.html +0 -77
  559. package/src/shade/playground/vgeo_viewer/main.d.ts.map +0 -1
  560. package/src/shade/playground/vgeo_viewer/main.js +0 -1343
  561. package/src/shade/playground/vgeo_viewer/sample_asset.d.ts.map +0 -1
  562. package/src/shade/playground/vgeo_viewer/select_cut.d.ts +0 -173
  563. package/src/shade/playground/vgeo_viewer/select_cut.d.ts.map +0 -1
  564. package/src/shade/playground/vgeo_viewer/select_cut.js +0 -481
  565. package/src/shade/renderer/geometry/virtual/build/VGeoFaceGraph.d.ts.map +0 -1
  566. package/src/shade/renderer/geometry/virtual/build/VGeoGatheredFaces.d.ts +0 -24
  567. package/src/shade/renderer/geometry/virtual/build/VGeoGatheredFaces.d.ts.map +0 -1
  568. package/src/shade/renderer/geometry/virtual/build/VGeoGatheredFaces.js +0 -25
  569. package/src/shade/renderer/geometry/virtual/build/VGeoLevel.d.ts.map +0 -1
  570. package/src/shade/renderer/geometry/virtual/build/VGeoLevelOptions.d.ts.map +0 -1
  571. package/src/shade/renderer/geometry/virtual/build/VGeoLevelSummary.d.ts.map +0 -1
  572. package/src/shade/renderer/geometry/virtual/build/VGeoSubmesh.d.ts +0 -20
  573. package/src/shade/renderer/geometry/virtual/build/VGeoSubmesh.d.ts.map +0 -1
  574. package/src/shade/renderer/geometry/virtual/build/VGeoSubmesh.js +0 -20
  575. package/src/shade/renderer/geometry/virtual/build/VirtualGeometryMesh.d.ts.map +0 -1
  576. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.d.ts.map +0 -1
  577. package/src/shade/renderer/geometry/virtual/build/vgeo_bounding_sphere.d.ts.map +0 -1
  578. package/src/shade/renderer/geometry/virtual/build/vgeo_build_levels.d.ts +0 -36
  579. package/src/shade/renderer/geometry/virtual/build/vgeo_build_levels.d.ts.map +0 -1
  580. package/src/shade/renderer/geometry/virtual/build/vgeo_build_levels.js +0 -1786
  581. package/src/shade/renderer/geometry/virtual/build/vgeo_build_page_nodes.d.ts.map +0 -1
  582. package/src/shade/renderer/geometry/virtual/build/vgeo_build_root_group.d.ts.map +0 -1
  583. package/src/shade/renderer/geometry/virtual/build/vgeo_compute_vertex_normals.d.ts +0 -29
  584. package/src/shade/renderer/geometry/virtual/build/vgeo_compute_vertex_normals.d.ts.map +0 -1
  585. package/src/shade/renderer/geometry/virtual/build/vgeo_compute_vertex_normals.js +0 -75
  586. package/src/shade/renderer/geometry/virtual/build/vgeo_compute_vertex_tangents.d.ts.map +0 -1
  587. package/src/shade/renderer/geometry/virtual/build/vgeo_compute_vertex_tangents.js +0 -143
  588. package/src/shade/renderer/geometry/virtual/build/vgeo_containing_sphere.d.ts.map +0 -1
  589. package/src/shade/renderer/geometry/virtual/build/vgeo_extract_submesh.d.ts +0 -14
  590. package/src/shade/renderer/geometry/virtual/build/vgeo_extract_submesh.d.ts.map +0 -1
  591. package/src/shade/renderer/geometry/virtual/build/vgeo_extract_submesh.js +0 -41
  592. package/src/shade/renderer/geometry/virtual/build/vgeo_gather_faces.d.ts +0 -12
  593. package/src/shade/renderer/geometry/virtual/build/vgeo_gather_faces.d.ts.map +0 -1
  594. package/src/shade/renderer/geometry/virtual/build/vgeo_gather_faces.js +0 -71
  595. package/src/shade/renderer/geometry/virtual/build/vgeo_graph_components.d.ts.map +0 -1
  596. package/src/shade/renderer/geometry/virtual/build/vgeo_narrow_indices.d.ts.map +0 -1
  597. package/src/shade/renderer/geometry/virtual/build/vgeo_normalize_source.d.ts.map +0 -1
  598. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.d.ts.map +0 -1
  599. package/src/shade/renderer/geometry/virtual/build/vgeo_partition_graph.d.ts.map +0 -1
  600. package/src/shade/renderer/geometry/virtual/build/vgeo_sphere_inflate_to_contain.d.ts.map +0 -1
  601. package/src/shade/renderer/geometry/virtual/build/vgeo_split_face_set.d.ts.map +0 -1
  602. package/src/shade/renderer/geometry/virtual/build/vgeo_split_face_set_packed.d.ts.map +0 -1
  603. package/src/shade/renderer/geometry/virtual/build/vgeo_split_face_set_packed.js +0 -207
  604. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_copy_vertex.d.ts +0 -16
  605. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_copy_vertex.d.ts.map +0 -1
  606. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.d.ts +0 -258
  607. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.d.ts.map +0 -1
  608. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.d.ts +0 -49
  609. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.d.ts.map +0 -1
  610. package/src/shade/renderer/loader/gltf/fix_up_material_sides.d.ts +0 -10
  611. package/src/shade/renderer/loader/gltf/fix_up_material_sides.d.ts.map +0 -1
  612. package/src/shade/renderer/loader/gltf/fix_up_material_sides.js +0 -109
  613. package/src/shade/renderer/rasterize/expand/graph_triangles_filter_by_material_side.d.ts +0 -17
  614. package/src/shade/renderer/rasterize/expand/graph_triangles_filter_by_material_side.d.ts.map +0 -1
  615. package/src/shade/renderer/rasterize/expand/graph_triangles_filter_by_material_side.js +0 -78
  616. package/src/shade/renderer/rasterize/expand/shader_filter_triangle_by_material_side.d.ts +0 -3
  617. package/src/shade/renderer/rasterize/expand/shader_filter_triangle_by_material_side.d.ts.map +0 -1
  618. package/src/shade/renderer/rasterize/expand/shader_filter_triangle_by_material_side.js +0 -100
  619. package/src/shade/renderer/rasterize/forward/chunk_material_side_should_draw.d.ts +0 -8
  620. package/src/shade/renderer/rasterize/forward/chunk_material_side_should_draw.d.ts.map +0 -1
  621. package/src/shade/renderer/rasterize/forward/chunk_material_side_should_draw.js +0 -26
  622. package/src/shade/renderer/shader/chunk/jobs/chunk_job_system.d.ts +0 -3
  623. package/src/shade/renderer/shader/chunk/jobs/chunk_job_system.d.ts.map +0 -1
  624. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_assign_coordinates.d.ts +0 -0
  625. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_assign_layers.d.ts +0 -0
  626. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_break_cycles.d.ts +0 -0
  627. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_break_cycles.js +0 -0
  628. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_insert_lanes.d.ts +0 -0
  629. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_insert_lanes.js +0 -0
  630. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_order_layers.d.ts +0 -0
  631. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_order_layers.js +0 -0
  632. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_orient_links.d.ts +0 -0
  633. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_orient_links.js +0 -0
  634. /package/src/shade/playground/{vgeo_viewer → caldera}/main.d.ts +0 -0
  635. /package/src/shade/renderer/geometry/virtual/build/{VGeoLevel.d.ts → level/VGeoLevel.d.ts} +0 -0
  636. /package/src/shade/renderer/geometry/virtual/build/{VGeoLevel.js → level/VGeoLevel.js} +0 -0
  637. /package/src/shade/renderer/geometry/virtual/build/{VGeoLevelSummary.d.ts → level/VGeoLevelSummary.d.ts} +0 -0
  638. /package/src/shade/renderer/geometry/virtual/build/{VGeoLevelSummary.js → level/VGeoLevelSummary.js} +0 -0
  639. /package/src/shade/renderer/geometry/virtual/build/{vgeo_page_node_count.d.ts → page/vgeo_page_node_count.d.ts} +0 -0
  640. /package/src/shade/renderer/geometry/virtual/build/{vgeo_graph_components.d.ts → partition/vgeo_graph_components.d.ts} +0 -0
  641. /package/src/shade/renderer/geometry/virtual/build/{vgeo_graph_components.js → partition/vgeo_graph_components.js} +0 -0
  642. /package/src/shade/renderer/geometry/virtual/build/{vgeo_partition_graph.d.ts → partition/vgeo_partition_graph.d.ts} +0 -0
  643. /package/src/shade/renderer/geometry/virtual/build/{vgeo_split_face_set.d.ts → partition/vgeo_split_face_set.d.ts} +0 -0
  644. /package/src/shade/renderer/geometry/virtual/build/{vgeo_split_face_set_packed.d.ts → partition/vgeo_split_face_set_packed.d.ts} +0 -0
  645. /package/src/shade/renderer/geometry/virtual/build/{vgeo_bounding_sphere.d.ts → sphere/vgeo_bounding_sphere.d.ts} +0 -0
  646. /package/src/shade/renderer/geometry/virtual/build/{vgeo_containing_sphere.d.ts → sphere/vgeo_containing_sphere.d.ts} +0 -0
  647. /package/src/shade/renderer/geometry/virtual/build/{vgeo_sphere_inflate_to_contain.d.ts → sphere/vgeo_sphere_inflate_to_contain.d.ts} +0 -0
  648. /package/src/shade/renderer/geometry/virtual/build/{vgeo_narrow_indices.d.ts → submesh/vgeo_narrow_indices.d.ts} +0 -0
@@ -0,0 +1,2217 @@
1
+ import { Color } from "../../../core/color/Color.js";
2
+ import { m4_linear_determinant } from "../../../core/geom/3d/mat4/m4_linear_determinant.js";
3
+ import Vector3 from "../../../core/geom/Vector3.js";
4
+ import { GIGABYTE } from "../../../core/science/units/memory/GIGABYTE.js";
5
+ import { EntityComponentDataset } from "../../../engine/ecs/EntityComponentDataset.js";
6
+ import { Transform64 } from "../../../engine/ecs/transform/Transform64.js";
7
+ import { OrbitalCameraController } from "../../renderer/camera/OrbitalCameraController.js";
8
+ import { PerspectiveCamera } from "../../renderer/camera/PerspectiveCamera.js";
9
+ import { FramePhase } from "../../renderer/extension/FramePhase.js";
10
+ import { RenderExtension } from "../../renderer/extension/RenderExtension.js";
11
+ import {
12
+ vgeo_byte_source_over_url
13
+ } from "../../renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.js";
14
+ import {
15
+ VGeoContainerReader
16
+ } from "../../renderer/geometry/virtual/format/read/VGeoContainerReader.js";
17
+ import {
18
+ VGeoReadOptions
19
+ } from "../../renderer/geometry/virtual/format/read/VGeoReadOptions.js";
20
+ import { VGEO_CUT_STAT } from "../../renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.js";
21
+ import {
22
+ VGeoResidencyOptions
23
+ } from "../../renderer/geometry/virtual/runtime/VGeoResidencyOptions.js";
24
+ import { VirtualGeometry } from "../../renderer/geometry/virtual/runtime/VirtualGeometry.js";
25
+ import { make_sunlight } from "../../renderer/light/make_sunlight.js";
26
+ import { place_light } from "../../renderer/light/place_light.js";
27
+ import { ShadeDrawSide } from "../../renderer/material/ShadeDrawSide.js";
28
+ import { StandardShadeMaterial } from "../../renderer/material/StandardShadeMaterial.js";
29
+ import { TransparencyMode } from "../../renderer/material/TransparencyMode.js";
30
+ import { ColorSpace } from "../../renderer/texture/ColorSpace.js";
31
+ import { ShadeTexture } from "../../renderer/texture/ShadeTexture.js";
32
+ import { TextureWrapType } from "../../renderer/texture/TextureWrapType.js";
33
+ import { ShadeImage } from "../../renderer/texture/source/ShadeImage.js";
34
+ import { Sampler2D } from "../../../engine/graphics/texture/sampler/Sampler2D.js";
35
+ import {
36
+ RasterizationCollectionType
37
+ } from "../../renderer/rasterize/expand/RasterizationCollectionType.js";
38
+ import { Renderer } from "../../renderer/Renderer.js";
39
+ import { place_primitive } from "../../renderer/scene/rows/place_primitive.js";
40
+ import { Scene } from "../../renderer/scene/Scene.js";
41
+ import { load_default_environment } from "../load_default_environment.js";
42
+ import { install_profile_hotkey } from "../profile_hotkey.js";
43
+
44
+ /**
45
+ * A **whole converted scene** of virtual geometry: thousands of containers, tens of thousands of
46
+ * instances, one cut.
47
+ *
48
+ * `playground/vgeo_runtime/` draws one `.vgeo` and is the page that proves the engine seam. This is
49
+ * the other question that seam raises and it cannot answer: a container placed once by a page that
50
+ * frames the camera on its own bounding sphere is a *viewer*, and design §1's claim is about a
51
+ * **scene** — that rendering cost is a function of the threshold and the resolution rather than of
52
+ * the source, across every asset in view at once, with instances sharing pages and one residency
53
+ * budget deciding between all of them.
54
+ *
55
+ * So everything here that is not in the runtime page is about breadth rather than depth:
56
+ *
57
+ * - **One residency, N containers.** Every asset registers with the same `VGeoResidency`, whose
58
+ * budgets are scene-wide by construction — the RAM tier's bytes and the VRAM tier's slots are
59
+ * counted across every reader, and a page evicted from one asset is room for another's. That is
60
+ * the thing a single-asset page cannot show, because with one reader there is nothing to
61
+ * arbitrate between.
62
+ * - **One cut, N instances.** `shader_vgeo_cut_collect` runs over the visible mesh rows and emits
63
+ * one instance record per virtual one; the descent is seeded with one item per instance and every
64
+ * cull and error test downstream runs in that instance's own object space. Seventeen thousand
65
+ * instances are seventeen thousand seeds of one wavefront, not seventeen thousand cuts.
66
+ * - **Instances share the asset.** A geometry placed by four hundred nodes is one reader, one
67
+ * geometry row, one page lookup and one set of resident pages. That is what `VirtualGeometry`'s
68
+ * `equals` is for, and it is most of why this is affordable at all.
69
+ *
70
+ * What it takes as input is a **scene manifest** — `tools/geometry/virtual/vgeo_scene.js` writes
71
+ * one out of a builder manifest — naming each container and the matrices it is placed at. The
72
+ * containers are served by ranges: nothing is downloaded, each is opened out of its own head.
73
+ *
74
+ * `__state()` is the whole surface a headless session needs, and the panel shows nothing it does
75
+ * not.
76
+ *
77
+ * @author Alex Goldring
78
+ * @copyright Company Named Limited (c) 2026
79
+ */
80
+
81
+ const element = id => document.getElementById(id);
82
+
83
+ /**
84
+ * Where the containers are served from when the page is opened without a `?scene=`.
85
+ *
86
+ * Same origin as the page, and that is the point: `tools/dev/vite_test_assets.js` serves this prefix
87
+ * out of the package's own `test_assets/` **and** out of an asset root outside the checkout, by
88
+ * range, so a fifty-gigabyte converted scene is reached from the dev server that is already running.
89
+ * It used to be `http://localhost:5200` — a second server, started by hand, that this page was
90
+ * useless without.
91
+ *
92
+ * @type {string}
93
+ */
94
+ const ASSET_PREFIX = '/test_assets';
95
+
96
+ /**
97
+ * What a scene manifest is named, and therefore what {@link resolve_scene_url} asks the asset root
98
+ * for when the page is opened with no parameters at all.
99
+ *
100
+ * @type {string}
101
+ */
102
+ const SCENE_SUFFIX = '.vgeoscene.json';
103
+
104
+ /**
105
+ * What the material sidecar beside a scene manifest is named.
106
+ *
107
+ * `vgeo_scene_materials.js` writes it: the material each container is, and the constants to shade
108
+ * that material with. It is a separate file from the scene because it answers to a separate source
109
+ * — the original project, extracted — and a scene nobody has done that to is still a scene, so the
110
+ * page opens with or without it.
111
+ *
112
+ * @type {string}
113
+ */
114
+ const MATERIALS_SUFFIX = '.vgeomaterials.json';
115
+
116
+ /**
117
+ * Head bytes read on the first request of every container.
118
+ *
119
+ * §10's always-resident set is the header, the level table and the root frame, contiguous and in
120
+ * that order, and a reader that guesses short pays a second round trip to grow. The default is
121
+ * 64 KB, which is right for a page that opens one container and wasteful for one that opens three
122
+ * thousand: measured across Zorah's 2,966, the resident head is 2,560 bytes at the median and
123
+ * 6,528 at the very worst, so 8 KB opens **every** container in the set in exactly one range
124
+ * request and reads 7.4 MB in total rather than 190 MB.
125
+ *
126
+ * A container whose head is larger than this is not refused — `#grow_head` fetches the rest — so
127
+ * the number is a throughput choice and not a limit.
128
+ *
129
+ * @type {number}
130
+ */
131
+ const HEAD_PREFIX_BYTES = 8 * 1024;
132
+
133
+ /**
134
+ * Containers being opened at once.
135
+ *
136
+ * A cold scene is one range request per container and they are independent, so the only thing this
137
+ * trades is head-of-line latency against the browser's own per-origin connection pool. Beyond about
138
+ * the pool's width the requests queue in the browser instead of on the wire and the number stops
139
+ * meaning anything.
140
+ *
141
+ * @type {number}
142
+ */
143
+ const OPEN_CONCURRENCY = 24;
144
+
145
+ /**
146
+ * What `?side=` accepts, for a session asking whether a surface is missing or merely culled.
147
+ *
148
+ * @type {Readonly<Record<string, number>>}
149
+ */
150
+ const DRAW_SIDES = Object.freeze({
151
+ front: ShadeDrawSide.Front,
152
+ back: ShadeDrawSide.Back,
153
+ double: ShadeDrawSide.Double,
154
+ });
155
+
156
+ /**
157
+ * Opens the VG frame before anything records a rasterization pass, because the seam inside those
158
+ * passes writes into the feedback buffer this creates.
159
+ */
160
+ class VGeoFrameStart extends RenderExtension {
161
+ name = "vgeo/frame start";
162
+
163
+ phase = FramePhase.FrameStart;
164
+
165
+ /**
166
+ * @param {VGeoScenePlayground} page
167
+ */
168
+ constructor(page) {
169
+ super();
170
+
171
+ this.page = page;
172
+ }
173
+
174
+ record(frame) {
175
+ this.page.runtime.begin_frame({ graph: frame.graph });
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Schedules the readback and closes the frame, after everything that could have cut has cut.
181
+ */
182
+ class VGeoFrameEnd extends RenderExtension {
183
+ name = "vgeo/frame end";
184
+
185
+ phase = FramePhase.BeforePresent;
186
+
187
+ /**
188
+ * @param {VGeoScenePlayground} page
189
+ */
190
+ constructor(page) {
191
+ super();
192
+
193
+ this.page = page;
194
+ }
195
+
196
+ record(frame) {
197
+ this.page.feedback.read(frame.graph);
198
+
199
+ this.page.runtime.end_frame();
200
+
201
+ this.page.frames++;
202
+ }
203
+ }
204
+
205
+ class VGeoScenePlayground {
206
+ /**
207
+ * @type {Renderer}
208
+ */
209
+ renderer;
210
+
211
+ /**
212
+ * @type {HTMLCanvasElement}
213
+ */
214
+ canvas;
215
+
216
+ /**
217
+ * @type {Scene}
218
+ */
219
+ scene;
220
+
221
+ /**
222
+ * @type {GPUSceneContext}
223
+ */
224
+ scene_context;
225
+
226
+ /**
227
+ * @type {PerspectiveCamera}
228
+ */
229
+ camera;
230
+
231
+ /**
232
+ * @type {OrbitalCameraController}
233
+ */
234
+ controller;
235
+
236
+ /**
237
+ * The one material the whole scene is drawn with.
238
+ *
239
+ * Not a simplification this page is making: a `.vgeo` carries **geometry only** — design §7 —
240
+ * so a converted set has no material data in it at all, and the source's 2,714 materials did
241
+ * not survive the conversion because nothing asked them to. Giving each container a tinted
242
+ * material of its own would read better and would be a lie about where the colour came from,
243
+ * and it would also put 2,714 material buckets in front of the draw path to say it with.
244
+ *
245
+ * @type {StandardShadeMaterial}
246
+ */
247
+ material;
248
+
249
+ /**
250
+ * What a **reflected** instance is drawn with — one whose transform has a negative determinant,
251
+ * which in Zorah is 2,722 of 17,424.
252
+ *
253
+ * By default this is {@link material}, and the scene is one bucket. `?mirrored=` splits it, and
254
+ * the split is the only way to ask the question at all: a reflection reverses the screen-space
255
+ * winding of every triangle of the instance, `cullMode` is pipeline state chosen per material
256
+ * bucket, and nothing between `construct_primitive_state` and the rasterizer sees a transform.
257
+ * So the sign of the determinant cannot reach the cull unless the instance is put in a
258
+ * different bucket, and putting it in one is both the diagnosis and, for a page, the fix.
259
+ *
260
+ * @type {StandardShadeMaterial}
261
+ */
262
+ mirror_material;
263
+
264
+ /**
265
+ * One material per distinct material the source scene used, when a sidecar was found.
266
+ *
267
+ * Empty when it was not, and {@link material} then draws everything as before.
268
+ *
269
+ * @type {StandardShadeMaterial[]}
270
+ */
271
+ materials = [];
272
+
273
+ /**
274
+ * Which of {@link materials} each container is, indexed the same as the scene manifest's
275
+ * geometries. `-1` for a container the sidecar had no material for.
276
+ *
277
+ * @type {number[]}
278
+ */
279
+ geometry_material = [];
280
+
281
+ /**
282
+ * Containers drawn with their own material rather than with {@link material}.
283
+ * @type {number}
284
+ */
285
+ materialised = 0;
286
+
287
+ /**
288
+ * Texture slots bound from the sidecar — zero when the scene has no texture coordinates to
289
+ * sample them with, which is the ordinary case for a converted set.
290
+ *
291
+ * @type {number}
292
+ */
293
+ textures_bound = 0;
294
+
295
+ /**
296
+ * What a container the sidecar had no material for is drawn with.
297
+ *
298
+ * Not {@link material}: that one is a pale grey chosen to read well when it is the *only*
299
+ * material in the frame, and at 0.72 it is brighter than almost anything the extraction
300
+ * produces. Six hundred of those among two thousand real surfaces do not look like missing
301
+ * data, they look like the scene — and they drag the exposure down over everything else. A
302
+ * neutral dielectric grey sits in the range the real materials occupy.
303
+ *
304
+ * @type {StandardShadeMaterial}
305
+ */
306
+ unmaterialised;
307
+
308
+ /**
309
+ * Instances placed with a negative-determinant transform.
310
+ * @type {number}
311
+ */
312
+ mirrored = 0;
313
+
314
+ /**
315
+ * The scene manifest as loaded, or `undefined` before it arrives.
316
+ * @type {object|undefined}
317
+ */
318
+ manifest;
319
+
320
+ /**
321
+ * Where the manifest was loaded from, which every container's URL is resolved against.
322
+ * @type {string}
323
+ */
324
+ base = '';
325
+
326
+ /**
327
+ * One entry per container that opened, held so nothing collects the reader out from under the
328
+ * residency and so a session can be told what is in the scene.
329
+ * @type {Array<{reader: VGeoContainerReader, geometry: VirtualGeometry, file: string, instances: number}>}
330
+ */
331
+ assets = [];
332
+
333
+ /**
334
+ * @type {GPUVirtualGeometryRuntime}
335
+ */
336
+ runtime;
337
+
338
+ /**
339
+ * @type {VGeoFeedbackLoop}
340
+ */
341
+ feedback;
342
+
343
+ /**
344
+ * @type {VGeoResidency}
345
+ */
346
+ residency;
347
+
348
+ /**
349
+ * @type {number}
350
+ */
351
+ frames = 0;
352
+
353
+ /**
354
+ * Containers opened, refused, and how many the manifest names.
355
+ * @type {number}
356
+ */
357
+ opened = 0;
358
+
359
+ /**
360
+ * @type {number}
361
+ */
362
+ refused = 0;
363
+
364
+ /**
365
+ * @type {number}
366
+ */
367
+ placed = 0;
368
+
369
+ /**
370
+ * @type {number}
371
+ */
372
+ total_containers = 0;
373
+
374
+ /**
375
+ * Wall-clock milliseconds the whole scene took to open, or 0 while it is still opening.
376
+ * @type {number}
377
+ */
378
+ open_ms = 0;
379
+
380
+ /**
381
+ * @type {number}
382
+ */
383
+ screen_height = 1;
384
+
385
+ /**
386
+ * @type {number}
387
+ */
388
+ screen_width = 1;
389
+
390
+ /**
391
+ * Frame time, exponentially smoothed — a scene page's one reading that is a cost rather than a
392
+ * count, and coarse enough to be honest about being one. A capture is what measures.
393
+ * @type {number}
394
+ */
395
+ frame_ms = 0;
396
+
397
+ /**
398
+ * @type {string}
399
+ */
400
+ status_text = '';
401
+
402
+ /**
403
+ * The dropout catcher, when `?catch=` armed one.
404
+ * @type {FlickerCatcher|undefined}
405
+ */
406
+ catcher;
407
+
408
+ /**
409
+ * Named viewpoints, so a session that cannot drag has somewhere to stand and a reader of the
410
+ * panel can get back to a reading. Seeded from the manifest's own camera.
411
+ * @type {Array<{name: string, eye: number[], target: number[]}>}
412
+ */
413
+ viewpoints = [];
414
+
415
+ /**
416
+ * @param {string} text
417
+ * @param {boolean} [error]
418
+ */
419
+ status(text, error = false) {
420
+ this.status_text = text;
421
+
422
+ const node = element('status');
423
+
424
+ node.textContent = text;
425
+ node.classList.toggle('error', error);
426
+ }
427
+
428
+ /**
429
+ * Bring the canvas, the renderer and the camera up to the size the window actually is.
430
+ *
431
+ * **Measured from the document, never from the canvas.** `Renderer` writes back both of the
432
+ * canvas's sizes when it configures the context, including an *inline* `style.width` in CSS
433
+ * pixels — so a `fit` that read `clientWidth` back would be reading the renderer its own last
434
+ * answer and the canvas would never change size again.
435
+ *
436
+ * The cut's screen height and field of view follow from the renderer rather than from the
437
+ * window because τ is a count of **render-target** pixels: `GPUViewCut.set_projection` takes
438
+ * them off the view every frame, so nothing here has to push them.
439
+ */
440
+ fit() {
441
+ const renderer = this.renderer;
442
+
443
+ const viewport = document.documentElement;
444
+
445
+ renderer.pixel_ratio = globalThis.devicePixelRatio || 1;
446
+
447
+ renderer.resize(
448
+ Math.max(1, Math.floor(viewport.clientWidth)),
449
+ Math.max(1, Math.floor(viewport.clientHeight))
450
+ );
451
+
452
+ this.camera.aspect = renderer.aspect_ratio;
453
+
454
+ const resolution = renderer.output_resolution;
455
+ const scale = renderer.internal_resolution_scale;
456
+
457
+ this.screen_width = Math.max(1, Math.floor(resolution.x * scale));
458
+ this.screen_height = Math.max(1, Math.floor(resolution.y * scale));
459
+ }
460
+
461
+ /**
462
+ * Open every container the manifest names and place its instances, as each one lands.
463
+ *
464
+ * **Placed as they arrive rather than at the end**, and that is the whole difference between a
465
+ * page you can look at in a second and one you wait a minute for. It costs nothing: a geometry
466
+ * that is not registered yet is a geometry the cut does not collect, and the residency's budgets
467
+ * are recomputed per reading rather than at registration, so a scene that grows under the camera
468
+ * is the case both tiers were designed for.
469
+ *
470
+ * A container that refuses — the wrong page size, a tier with no room for another root — leaves
471
+ * every other asset exactly as it was and is counted. Refusing one container out of three
472
+ * thousand is a hole in the scene, and a hole is what the count is for.
473
+ *
474
+ * @param {object} manifest
475
+ * @returns {Promise<void>}
476
+ */
477
+ async open_scene(manifest) {
478
+ const geometries = manifest.geometries;
479
+
480
+ this.total_containers = geometries.length;
481
+
482
+ const options = new VGeoReadOptions();
483
+
484
+ /*
485
+ Frame verification on, for the same reason the runtime page has it on: these are bytes off
486
+ a network rather than bytes a tool just wrote, and §10 leaves it off by default precisely
487
+ because a builder reading back its own output is not defending against one.
488
+ */
489
+ options.verify_frames = true;
490
+
491
+ // the runtime answers activation out of the arena; the reader's own counters would be
492
+ // tens of megabytes at seventeen thousand pages, read by nothing
493
+ options.track_selectability = false;
494
+
495
+ options.head_prefix_bytes = HEAD_PREFIX_BYTES;
496
+
497
+ const started = performance.now();
498
+
499
+ let next = 0;
500
+
501
+ const worker = async () => {
502
+ while (next < geometries.length) {
503
+ const index = next++;
504
+
505
+ await this.open_one(geometries[index], options, index);
506
+ }
507
+ };
508
+
509
+ const workers = [];
510
+
511
+ for (let i = 0; i < Math.min(OPEN_CONCURRENCY, geometries.length); i++) {
512
+ workers.push(worker());
513
+ }
514
+
515
+ await Promise.all(workers);
516
+
517
+ this.open_ms = performance.now() - started;
518
+
519
+ this.status(
520
+ `${this.opened.toLocaleString()} containers`
521
+ + `, ${this.placed.toLocaleString()} instances`
522
+ + `, opened in ${(this.open_ms / 1000).toFixed(1)} s`
523
+ + (this.refused > 0 ? ` — ${this.refused} refused` : '')
524
+ );
525
+ }
526
+
527
+ /**
528
+ * @param {object} record one manifest geometry
529
+ * @param {VGeoReadOptions} options
530
+ * @returns {Promise<void>}
531
+ */
532
+ async open_one(record, options, index = -1) {
533
+ const url = new URL(record.file, this.base).href;
534
+
535
+ try {
536
+ /*
537
+ The length comes from the manifest rather than from a `HEAD`. `vgeo_fetch_byte_source`
538
+ probes for it because a URL is usually all a caller has; here the builder wrote it down
539
+ beside the file name, and taking it saves 2,966 round trips that would tell this page
540
+ something it was already holding.
541
+ */
542
+ const source = vgeo_byte_source_over_url(url, record.bytes);
543
+
544
+ const reader = await VGeoContainerReader.open(source, options);
545
+
546
+ /*
547
+ Only the root page is resident. Everything below it arrives because the cut asked for
548
+ it, which at this scale is the whole point: the manifest names 46.7 GiB of geometry and
549
+ what is fetched is what a threshold in pixels selected out of it.
550
+ */
551
+ const geometry_row = this.residency.register(reader);
552
+
553
+ const geometry = new VirtualGeometry(reader);
554
+
555
+ geometry.vg_row = geometry_row;
556
+
557
+ const matrices = record.instances;
558
+
559
+ /*
560
+ The container's own material when the sidecar named one. There is no per-instance
561
+ material here and there is nothing to choose between: a `.vgeo` is one primitive of the
562
+ source document and a glTF primitive has exactly one material, so the container *is*
563
+ the material bucket.
564
+ */
565
+ const own_material = index < 0 ? undefined : this.materials[this.geometry_material[index]];
566
+ const material = own_material ?? this.unmaterialised ?? this.material;
567
+
568
+ if (own_material !== undefined) {
569
+ this.materialised++;
570
+ }
571
+
572
+ const transform = new Transform64();
573
+
574
+ for (let i = 0; i < matrices.length; i += 16) {
575
+ /*
576
+ The source matrix, decomposed. A transform row carries a TRS — the GPU composes the
577
+ world from it — so an instance carrying **shear** cannot be represented exactly and
578
+ is placed at the closest rigid-plus-scale fit instead. 391 of Zorah's 17,424 have
579
+ any, and the worst is about eleven degrees off orthogonal; the other 17,033 are
580
+ exact. That is a property of the engine's transform model rather than of this page,
581
+ and it is worth knowing when a wall looks subtly wrong.
582
+ */
583
+ const matrix = matrices.slice(i, i + 16);
584
+
585
+ transform.fromMatrix(matrix);
586
+
587
+ /*
588
+ The engine reverses a reflected instance's winding in the vertex shader, off the
589
+ row's own `NODE3D_GLOBAL_FLAG_MIRRORED`, so nothing here has to be done for the
590
+ picture to be right. Splitting the bucket is what `?mirrored=` is for: a
591
+ diagnostic that can tint the reflected half, or reproduce the old bug by reversing
592
+ it a second time. See the README.
593
+ */
594
+ const reflected = m4_linear_determinant(matrix) < 0;
595
+
596
+ if (reflected) {
597
+ this.mirrored++;
598
+ }
599
+
600
+ /*
601
+ `?mirrored=` splits reflected instances into a bucket of their own, which is a
602
+ diagnostic about winding rather than about materials — so it takes precedence when
603
+ it is on, and the container's own material is what draws otherwise.
604
+ */
605
+ place_primitive(
606
+ this.scene_context, geometry,
607
+ reflected && this.mirror_material !== this.material
608
+ ? this.mirror_material
609
+ : material,
610
+ transform
611
+ );
612
+
613
+ this.placed++;
614
+ }
615
+
616
+ this.assets.push({
617
+ reader,
618
+ geometry,
619
+ file: record.file,
620
+ instances: matrices.length / 16,
621
+ });
622
+
623
+ this.opened++;
624
+ } catch (error) {
625
+ this.refused++;
626
+
627
+ // one line per refusal rather than a thrown scene: 2,965 of 2,966 is a scene
628
+ console.warn(`${record.file} — ${error.message ?? error}`);
629
+ }
630
+ }
631
+
632
+ /**
633
+ * Stand the camera somewhere, in world units.
634
+ *
635
+ * Everything with a length in it is taken from the distance between the two points rather than
636
+ * from the scene's extent, which is the difference between a camera that works in a town and
637
+ * one that works in a hundred-kilometre bounding box: a near plane derived from Zorah's world
638
+ * radius is fifty metres, and fifty metres puts every building the viewer is standing in behind
639
+ * it.
640
+ *
641
+ * @param {number[]} eye
642
+ * @param {number[]} target
643
+ */
644
+ look(eye, target) {
645
+ const from = new Vector3(eye[0], eye[1], eye[2]);
646
+ const to = new Vector3(target[0], target[1], target[2]);
647
+
648
+ const distance = Math.max(Vector3.distance(from, to), 1e-3);
649
+
650
+ this.camera.near = distance * 0.002;
651
+ this.camera.far = Math.max(distance * 2000, this.world_radius * 4);
652
+
653
+ this.controller.distanceLimits.min = distance * 0.01;
654
+ this.controller.distanceLimits.max = Math.max(distance * 2000, this.world_radius * 4);
655
+ this.controller.movement_speed_scale = distance;
656
+
657
+ this.controller.look(from, to);
658
+
659
+ // the pixels the camera is about to cover have no correspondence in a TAA history written
660
+ // from where it was standing
661
+ this.renderer.indicate_view_change();
662
+ }
663
+
664
+ /**
665
+ * Half the diagonal of every instance's world box, which is the only length the manifest offers
666
+ * that is about the scene rather than about a viewpoint.
667
+ * @type {number}
668
+ */
669
+ world_radius = 1;
670
+
671
+ /**
672
+ * Everything a headless session needs, and everything the panel shows.
673
+ *
674
+ * @returns {object}
675
+ */
676
+ state() {
677
+ const stats = this.feedback.stats;
678
+ const pool = this.runtime.pool;
679
+
680
+ let bytes_read = this.residency.fetcher.worker_bytes_read;
681
+
682
+ for (const asset of this.assets) {
683
+ bytes_read += asset.reader.source.bytes_read;
684
+ }
685
+
686
+ return {
687
+ frames: this.frames,
688
+ frame_ms: this.frame_ms,
689
+ tau: this.runtime.tau,
690
+ status: this.status_text,
691
+ screen_width: this.screen_width,
692
+ screen_height: this.screen_height,
693
+ feedback_reads: this.feedback.read_count,
694
+ feedback_skipped: this.feedback.skipped_frames,
695
+ camera: {
696
+ eye: Array.from(this.camera.transform.translation),
697
+ target: this.controller.target.toJSON(),
698
+ distance: this.controller.distance,
699
+ },
700
+ scene: {
701
+ name: this.manifest?.source ?? '',
702
+ containers: this.opened,
703
+ declared: this.total_containers,
704
+ refused: this.refused,
705
+ instances: this.placed,
706
+ mirrored: this.mirrored,
707
+ materials: this.materials.length,
708
+ materialised: this.materialised,
709
+ faces: this.manifest === undefined ? 0 : scene_faces(this.manifest),
710
+ bytes: this.manifest?.total_bytes ?? 0,
711
+ bytes_read,
712
+ pages: this.manifest?.total_pages ?? 0,
713
+ open_ms: this.open_ms,
714
+ },
715
+ cut: {
716
+ drawn: stats[VGEO_CUT_STAT.DRAWN],
717
+ clamped: stats[VGEO_CUT_STAT.CLAMPED],
718
+ clamped_on_activation: stats[VGEO_CUT_STAT.CLAMPED_ON_ACTIVATION],
719
+ pages: stats[VGEO_CUT_STAT.PAGES],
720
+ items: stats[VGEO_CUT_STAT.ITEMS],
721
+ clusters_tested: stats[VGEO_CUT_STAT.CLUSTERS_TESTED],
722
+ culled_frustum: stats[VGEO_CUT_STAT.CULLED_FRUSTUM],
723
+ culled_cluster: stats[VGEO_CUT_STAT.CULLED_CLUSTER],
724
+ culled_lod: stats[VGEO_CUT_STAT.CULLED_LOD],
725
+ meshlets_dropped: stats[VGEO_CUT_STAT.MESHLETS_DROPPED],
726
+ wants_dropped: stats[VGEO_CUT_STAT.WANTS_DROPPED],
727
+ queue_dropped: stats[VGEO_CUT_STAT.QUEUE_DROPPED],
728
+ instances_dropped: stats[VGEO_CUT_STAT.INSTANCES_DROPPED],
729
+ wavefronts: stats[VGEO_CUT_STAT.WAVEFRONTS],
730
+ queue_remaining: stats[VGEO_CUT_STAT.QUEUE_REMAINING],
731
+ /*
732
+ The bound beside the use of it, because one is meaningless without the other: eight
733
+ wavefronts is a drained descent under a bound of 32 and a truncated one under a
734
+ bound of 8, and the panel could not tell them apart while it showed only the count.
735
+ */
736
+ max_iterations: this.runtime.max_iterations,
737
+ iteration_escalations: this.runtime.iteration_escalations,
738
+ },
739
+ wants: this.feedback.last_want_count,
740
+ touches: this.feedback.last_touch_count,
741
+ vram: {
742
+ pages: pool.resident_page_count,
743
+ bytes: pool.resident_bytes,
744
+ capacity: pool.tables.page_capacity,
745
+ evicted: this.residency.evicted_vram_count,
746
+ refused: this.residency.refused_install_count,
747
+ },
748
+ ram: {
749
+ pages: this.residency.ram_page_count,
750
+ bytes: this.residency.ram_resident_bytes,
751
+ evicted: this.residency.evicted_ram_count,
752
+ refused: this.residency.refused_fetch_count,
753
+ },
754
+ fetches_in_flight: this.residency.fetches_in_flight,
755
+ worker_fetches: this.residency.fetcher.worker_fetch_count,
756
+ in_process_fetches: this.residency.fetcher.in_process_fetch_count,
757
+ deferred: this.residency.deferred_want_count,
758
+ };
759
+ }
760
+ }
761
+
762
+ /**
763
+ * Catches a frame that is **not like its neighbours** and keeps it.
764
+ *
765
+ * A cut re-selected every frame has a shimmer floor set by TAA (see the README), and that is a
766
+ * property of the content. A *dropout* — a single frame missing a large part of what the frames
767
+ * either side of it drew — is not, and the two are nothing alike to measure even though both read
768
+ * as "flicker": the first moves a tenth of a per cent of the pixels, the second moves tens of per
769
+ * cent. This looks only for the second.
770
+ *
771
+ * **It has to sample the canvas, not the counters.** `__state()`'s cut statistics come from the
772
+ * feedback readback, which is capped at two in flight and is skipped on more than half the frames —
773
+ * so on any given frame `drawn` is a reading from one to three frames ago, and a one-frame
774
+ * disappearance need never appear in it at all. The pixels are the only per-frame truth available,
775
+ * and `drawImage` into a 2D canvas is the only way to get at them: the swap chain is not
776
+ * `preserveDrawingBuffer`, so `getImageData` cannot reach it.
777
+ *
778
+ * A frame is judged against the **median of its two neighbours on each side** rather than against a
779
+ * running average, because the thing being looked for is one frame wide and a mean that includes it
780
+ * hides it. That costs a two-frame delay before a verdict, which is why the full-resolution ring
781
+ * below is five deep: by the time a frame is convicted it is two frames old, and it still has to be
782
+ * there to be looked at.
783
+ */
784
+ class FlickerCatcher {
785
+ /**
786
+ * @param {HTMLCanvasElement} canvas the page's own, which is read and never written
787
+ * @param {number} threshold relative departure from the neighbours that counts, e.g. 0.15
788
+ * @param {boolean} freeze whether to stop on the first one and put it on screen
789
+ * @param {function(): string} describe answers a link that reopens the page where the camera
790
+ * stands — held per event, because where it happened is the one thing a report of one
791
+ * cannot be reconstructed without and the camera may have moved by the time it is read
792
+ */
793
+ constructor(canvas, threshold, freeze, describe) {
794
+ this.source = canvas;
795
+ this.threshold = threshold;
796
+ this.freeze = freeze;
797
+ this.describe = describe;
798
+
799
+ this.width = 128;
800
+ this.height = 72;
801
+
802
+ this.scratch = document.createElement('canvas');
803
+ this.scratch.width = this.width;
804
+ this.scratch.height = this.height;
805
+
806
+ this.context = this.scratch.getContext('2d', { willReadFrequently: true });
807
+
808
+ /** Full-resolution frames, so a convicted one can be looked at rather than only counted. */
809
+ this.ring = [];
810
+
811
+ for (let i = 0; i < 5; i++) {
812
+ const frame = document.createElement('canvas');
813
+
814
+ this.ring.push({ canvas: frame, context: frame.getContext('2d'), frame: -1, mean: 0 });
815
+ }
816
+
817
+ this.at = 0;
818
+ this.recent = [];
819
+ this.rows = [];
820
+
821
+ this.frames = 0;
822
+ this.events = [];
823
+ this.worst = 0;
824
+ this.frozen = false;
825
+
826
+ /**
827
+ * Frames the scene has to have been drawing for before anything is judged.
828
+ *
829
+ * The first lit frame departs from the black ones before it by 103%, which is a real
830
+ * observation about a page that has just started and nothing at all about a dropout. Five
831
+ * is enough to clear it and short enough that arming is not something you wait for.
832
+ * @type {number}
833
+ */
834
+ this.armed_after = 5;
835
+
836
+ this.drawing = 0;
837
+ }
838
+
839
+ /**
840
+ * One frame. Called after the render, from the page's own loop.
841
+ *
842
+ * @param {object} state what {@link VGeoScenePlayground.state} answered this frame
843
+ */
844
+ sample(state) {
845
+ if (this.frozen) {
846
+ return;
847
+ }
848
+
849
+ const source = this.source;
850
+
851
+ /*
852
+ The full-resolution copy is the expensive half — a whole-canvas `drawImage` every frame,
853
+ measured at rather more than the 128x72 read below — and it buys exactly one thing: a frame
854
+ that can be looked at after the fact. A run that is only counting does not need it, and
855
+ paying for it would slow the page enough to change the timing of whatever is being hunted.
856
+ */
857
+ if (this.freeze) {
858
+ const slot = this.ring[this.at % this.ring.length];
859
+
860
+ if (slot.canvas.width !== source.width || slot.canvas.height !== source.height) {
861
+ slot.canvas.width = source.width;
862
+ slot.canvas.height = source.height;
863
+ }
864
+
865
+ slot.context.drawImage(source, 0, 0);
866
+ slot.frame = state.frames;
867
+
868
+ this.at++;
869
+ }
870
+
871
+ this.context.drawImage(source, 0, 0, this.width, this.height);
872
+
873
+ const pixels = this.context.getImageData(0, 0, this.width, this.height).data;
874
+
875
+ let sum = 0;
876
+
877
+ for (let i = 0; i < pixels.length; i += 4) {
878
+ sum += 0.2126 * pixels[i] + 0.7152 * pixels[i + 1] + 0.0722 * pixels[i + 2];
879
+ }
880
+
881
+ const mean = sum / (this.width * this.height);
882
+
883
+ this.frames++;
884
+
885
+ if (state.cut.drawn > 0) {
886
+ this.drawing++;
887
+ }
888
+
889
+ this.rows.push([
890
+ state.frames, Math.round(mean * 100) / 100, state.cut.drawn, state.cut.clamped,
891
+ state.cut.queue_remaining, state.cut.meshlets_dropped, state.cut.instances_dropped,
892
+ state.cut.wavefronts, state.vram.pages, state.vram.refused, state.vram.evicted,
893
+ ]);
894
+
895
+ if (this.rows.length > 4096) {
896
+ this.rows.shift();
897
+ }
898
+
899
+ this.recent.push(mean);
900
+
901
+ if (this.recent.length > 5) {
902
+ this.recent.shift();
903
+ }
904
+
905
+ if (this.recent.length < 5 || this.drawing < this.armed_after) {
906
+ return;
907
+ }
908
+
909
+ const middle = this.recent[2];
910
+
911
+ /*
912
+ **Both sides, same direction, and the smaller of the two departures.** Judging the middle
913
+ against the median of all four neighbours looks right and is not: when the picture takes a
914
+ persistent *step* — the scene streaming in, a coarse level replaced by a fine one — two of
915
+ the four neighbours sit on the far side of it, the median lands between the two levels, and
916
+ the frame *before* the step is convicted for being where it always was. Every one of
917
+ fourteen close-range poses "caught" one that way, and not one of them was a dropout.
918
+
919
+ A frame that is genuinely alone is above (or below) the pair before it *and* the pair after
920
+ it. Taking the smaller of the two departures is what makes a step score zero: on a step one
921
+ of the two is ~0 by construction, whichever side the middle belongs to.
922
+ */
923
+ const before = (this.recent[0] + this.recent[1]) / 2;
924
+ const after = (this.recent[3] + this.recent[4]) / 2;
925
+
926
+ const from_before = (middle - before) / Math.max(before, 1e-6);
927
+ const from_after = (middle - after) / Math.max(after, 1e-6);
928
+
929
+ const departure = Math.sign(from_before) === Math.sign(from_after)
930
+ ? Math.min(Math.abs(from_before), Math.abs(from_after))
931
+ : 0;
932
+
933
+ const local = (before + after) / 2;
934
+
935
+ this.worst = Math.max(this.worst, departure);
936
+
937
+ if (departure < this.threshold) {
938
+ return;
939
+ }
940
+
941
+ const convicted = this.rows[this.rows.length - 3];
942
+
943
+ this.events.push({
944
+ frame: convicted[0],
945
+ departure: Math.round(departure * 10000) / 10000,
946
+ mean: Math.round(middle * 100) / 100,
947
+ neighbours: Math.round(local * 100) / 100,
948
+ url: this.describe(),
949
+ rows: this.rows.slice(Math.max(0, this.rows.length - 5)),
950
+ });
951
+
952
+ if (this.freeze) {
953
+ this.show(convicted[0]);
954
+ }
955
+ }
956
+
957
+ /**
958
+ * @returns {boolean} whether a frame that was caught can still be put on screen
959
+ */
960
+ get holds_frames() {
961
+ return this.freeze;
962
+ }
963
+
964
+ /**
965
+ * Put one of the held frames on screen and stop sampling, so what is on the page is that frame
966
+ * and a screenshot of the page is a screenshot of it.
967
+ *
968
+ * @param {number} frame
969
+ * @returns {string}
970
+ */
971
+ show(frame) {
972
+ const slot = this.ring.find(entry => entry.frame === frame);
973
+
974
+ if (slot === undefined) {
975
+ return `frame ${frame} is no longer held; the ring has `
976
+ + this.ring.map(entry => entry.frame).join(', ');
977
+ }
978
+
979
+ this.frozen = true;
980
+
981
+ for (const entry of this.ring) {
982
+ entry.canvas.remove();
983
+ }
984
+
985
+ /*
986
+ **Under the panel, not over it.** At a z-index above it the held frame covered the readings
987
+ that explain it and the camera-link button that says where it happened — which is the one
988
+ moment both are wanted. The panel is at 10.
989
+ */
990
+ slot.canvas.style.cssText = 'position:absolute;top:0;left:0;z-index:5;'
991
+ + `width:${this.source.style.width};height:${this.source.style.height}`;
992
+
993
+ document.body.append(slot.canvas);
994
+
995
+ return `frozen on frame ${frame}`;
996
+ }
997
+
998
+ /**
999
+ * @returns {object}
1000
+ */
1001
+ report() {
1002
+ return {
1003
+ frames: this.frames,
1004
+ threshold: this.threshold,
1005
+ worst: Math.round(this.worst * 10000) / 10000,
1006
+ events: this.events.length,
1007
+ frozen: this.frozen,
1008
+ held: this.ring.map(entry => entry.frame).filter(frame => frame >= 0),
1009
+ last: this.events[this.events.length - 1],
1010
+ };
1011
+ }
1012
+ }
1013
+
1014
+ /**
1015
+ * @param {object} manifest
1016
+ * @returns {number}
1017
+ */
1018
+ function scene_faces(manifest) {
1019
+ let faces = 0;
1020
+
1021
+ for (const geometry of manifest.geometries) {
1022
+ faces += geometry.faces * (geometry.instances.length / 16);
1023
+ }
1024
+
1025
+ return faces;
1026
+ }
1027
+
1028
+ /**
1029
+ * @param {number} bytes
1030
+ * @returns {string}
1031
+ */
1032
+ function human_bytes(bytes) {
1033
+ if (bytes < 1024) {
1034
+ return `${bytes} B`;
1035
+ }
1036
+
1037
+ if (bytes < 1024 * 1024) {
1038
+ return `${(bytes / 1024).toFixed(0)} KB`;
1039
+ }
1040
+
1041
+ if (bytes < 1024 * 1024 * 1024) {
1042
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1043
+ }
1044
+
1045
+ return `${(bytes / (1024 ** 3)).toFixed(2)} GB`;
1046
+ }
1047
+
1048
+ /**
1049
+ * Build the engine material a sidecar entry describes.
1050
+ *
1051
+ * **What is bound here is the `constant` block, not the textures**, and that is a property of this
1052
+ * asset rather than of the extraction. Zorah's containers carry position, normal and tangent and no
1053
+ * texture coordinates at all — the source document the conversion read is `POSITION` only — so
1054
+ * there is nothing to sample an albedo map *with* until a UV-carrying source is built and the
1055
+ * containers are made again from it. The material's `textures` are in the sidecar and the images
1056
+ * are on disk, waiting for that.
1057
+ *
1058
+ * The constants are not a stand-in for the textures. `albedo` is the extracted base colour map's
1059
+ * own alpha-weighted mean multiplied by the material's authored tint, `roughness` and `metallic`
1060
+ * come from the real ORM, and the draw side and alpha mode are what the material instance actually
1061
+ * overrode. What the surface loses is its detail, not its identity.
1062
+ *
1063
+ * @param {object} definition one entry of the sidecar's `materials`
1064
+ * @param {number} side what `?side=` asked for, which still wins
1065
+ * @returns {StandardShadeMaterial}
1066
+ */
1067
+ function make_scene_material(definition, side) {
1068
+ const material = new StandardShadeMaterial();
1069
+
1070
+ material.name = definition.name;
1071
+
1072
+ const constant = definition.constant ?? {};
1073
+ const albedo = constant.albedo ?? [0.72, 0.74, 0.78];
1074
+
1075
+ material.diffuse_color.set(albedo[0], albedo[1], albedo[2], 1);
1076
+
1077
+ /*
1078
+ With no ORM bound the material samples the engine's default white pixel, so every one of these
1079
+ factors is the value itself rather than a multiplier on a map.
1080
+ */
1081
+ material.roughness_factor = constant.roughness ?? 0.55;
1082
+ material.metallic_factor = constant.metallic ?? 0;
1083
+
1084
+ /*
1085
+ `ambient = fma(orm.r, ambient_factors.a, ambient_factors.b)`. `StandardShadeMaterial`
1086
+ constructs with `LinearModifier(1, 1)`, which against the default white ORM pixel is 2 and
1087
+ saturates to 1 in the G-buffer's unorm alpha — a material that has said nothing about occlusion
1088
+ is therefore unoccluded, which is the right answer and the reason the default is that pair.
1089
+
1090
+ This states it rather than reaching it by saturation, because the value here has to survive
1091
+ `bind_scene_textures` switching to (1, 0) when a real ORM arrives: Unreal's red channel *is*
1092
+ ambient occlusion and the conversion wants it applied, which is a decision about the source
1093
+ rather than a correction to the engine.
1094
+ */
1095
+ material.ambient_factors.a = 0;
1096
+ material.ambient_factors.b = 1;
1097
+
1098
+ const emissive = definition.emissive_factor ?? [0, 0, 0];
1099
+
1100
+ material.emissive_factor.set(emissive[0], emissive[1], emissive[2], 1);
1101
+
1102
+ /*
1103
+ A two-sided material is two-sided because the artist said so — foliage cards, cloth, thin
1104
+ stone tracery — and drawing it `Front` loses half of it. `?side=` overrides the lot, because
1105
+ its whole purpose is to ask whether something is missing or merely culled.
1106
+ */
1107
+ material.draw_side = side ?? (definition.draw_side === 'Double'
1108
+ ? ShadeDrawSide.Double
1109
+ : definition.draw_side === 'Back' ? ShadeDrawSide.Back : ShadeDrawSide.Front);
1110
+
1111
+ /*
1112
+ Alpha testing needs an alpha to test and the albedo texture is what carries it, so a masked
1113
+ material with no texture bound would discard by a constant and disappear entirely. Opaque is
1114
+ the honest fallback until the textures can be sampled.
1115
+ */
1116
+ material.transparency_mode = TransparencyMode.Opaque;
1117
+
1118
+ return material;
1119
+ }
1120
+
1121
+ /**
1122
+ * An environment of one constant radiance, for asking how much of a surface's brightness is its own
1123
+ * colour and how much is the sky.
1124
+ *
1125
+ * The real environment is an HDR sky with a sun in it, and a rough reflection integrates a wide
1126
+ * enough lobe to gather that sun — so comparing a surface against one pixel of the background
1127
+ * proves nothing about whether the image-based lighting is over-contributing. Against a **uniform**
1128
+ * environment the comparison is exact: no reflected direction can return more than the constant, so
1129
+ * a surface brighter than it is being lit by more than it reflects, and by how much.
1130
+ *
1131
+ * Big enough to carry a mip chain rather than 1×1 — `sample_environment_map` reads `lod` and
1132
+ * `lod + 1`, and a texture with one level has nowhere to read the second from. Every level is the
1133
+ * same value, so the roughness the sampler picks its lod from cannot change the answer.
1134
+ *
1135
+ * @param {number} value linear radiance, the same in all three channels
1136
+ * @returns {ShadeTexture}
1137
+ */
1138
+ function make_uniform_environment(value) {
1139
+ const size = 64;
1140
+
1141
+ // An environment is half-float; a float32 sampler uploads at twice the pitch the texture was
1142
+ // created with and the whole thing is refused. `Float16Array` converts on assignment, so these
1143
+ // are plain radiances rather than bit patterns — writing the bits instead stores the *number*
1144
+ // 14336 and lights the scene with it.
1145
+ const sampler = Sampler2D.float16(4, size, size);
1146
+
1147
+ for (let i = 0; i < size * size; i++) {
1148
+ sampler.data[i * 4] = value;
1149
+ sampler.data[i * 4 + 1] = value;
1150
+ sampler.data[i * 4 + 2] = value;
1151
+ sampler.data[i * 4 + 3] = 1;
1152
+ }
1153
+
1154
+ const image = ShadeImage.fromSampler2D(sampler);
1155
+
1156
+ image.color_space = ColorSpace.LinearSRGB;
1157
+
1158
+ const texture = ShadeTexture.from(image);
1159
+
1160
+ texture.wrapS = TextureWrapType.ClampToEdge;
1161
+ texture.wrapT = TextureWrapType.ClampToEdge;
1162
+
1163
+ return texture;
1164
+ }
1165
+
1166
+ /**
1167
+ * Fetch one extracted texture and make a {@link ShadeTexture} of it.
1168
+ *
1169
+ * `createImageBitmap` rather than `ShadeImage.fromArrayBuffer`: these are ordinary PNGs written by
1170
+ * the extraction and the browser's own decoder is what should read them.
1171
+ * `colorSpaceConversion: 'none'` matters — the extraction already wrote the exact bytes Unreal had,
1172
+ * and letting the browser convert them to the display profile on the way in shifts every albedo.
1173
+ *
1174
+ * A UDIM set has no place to go in a fixed material, so its first tile is taken and the caller is
1175
+ * told; composing the grid and rescaling the UVs belongs in the converter, not here.
1176
+ *
1177
+ * @param {object} slot one of a sidecar material's `textures`
1178
+ * @param {string} base where the textures are served from
1179
+ * @returns {Promise<ShadeTexture|null>}
1180
+ */
1181
+ async function load_scene_texture(slot, base) {
1182
+ const file = slot.files?.[0]?.file;
1183
+
1184
+ if (file === undefined || base === null) {
1185
+ return null;
1186
+ }
1187
+
1188
+ const url = `${base.replace(/\/$/, '')}/${file}`;
1189
+ const response = await fetch(url);
1190
+
1191
+ if (!response.ok) {
1192
+ throw new Error(`${url} answered HTTP ${response.status}`);
1193
+ }
1194
+
1195
+ const bitmap = await createImageBitmap(await response.blob(), {
1196
+ premultiplyAlpha: 'none',
1197
+ colorSpaceConversion: 'none',
1198
+ });
1199
+
1200
+ const image = ShadeImage.fromImageBitmap(bitmap);
1201
+
1202
+ /*
1203
+ A base colour map is sRGB encoded and everything else — normals, and an ORM's occlusion,
1204
+ roughness and metalness — is linear data that must not be decoded on the way in. The extraction
1205
+ read the flag off the Unreal asset and wrote it into the sidecar rather than guessing it from
1206
+ the slot, because the two disagree often enough to matter.
1207
+ */
1208
+ image.color_space = slot.srgb === true ? ColorSpace.SRGB : ColorSpace.None;
1209
+
1210
+ return ShadeTexture.from(image);
1211
+ }
1212
+
1213
+ /**
1214
+ * Bind the real maps onto the materials, where the scene can sample them.
1215
+ *
1216
+ * Returns what happened rather than throwing: one texture that will not load is one material drawn
1217
+ * from its constants, and the rest of the scene is unaffected.
1218
+ *
1219
+ * @param {object[]} definitions the sidecar's materials
1220
+ * @param {StandardShadeMaterial[]} materials what {@link make_scene_material} built from them
1221
+ * @param {string} base
1222
+ * @returns {Promise<{bound: number, failed: string[], udim: number}>}
1223
+ */
1224
+ async function bind_scene_textures(definitions, materials, base) {
1225
+ const cache = new Map();
1226
+ const failed = [];
1227
+
1228
+ let bound = 0;
1229
+ let udim = 0;
1230
+
1231
+ const obtain = async (slot) => {
1232
+ const key = slot.files?.[0]?.file;
1233
+
1234
+ if (key === undefined) {
1235
+ return null;
1236
+ }
1237
+
1238
+ if (!cache.has(key)) {
1239
+ cache.set(key, load_scene_texture(slot, base));
1240
+ }
1241
+
1242
+ return cache.get(key);
1243
+ };
1244
+
1245
+ const SLOTS = [
1246
+ ['albedo', 'texture_albedo'],
1247
+ ['normal', 'texture_normal'],
1248
+ ['orm', 'texture_orm'],
1249
+ ];
1250
+
1251
+ await Promise.all(definitions.map(async (definition, i) => {
1252
+ for (const [name, field] of SLOTS) {
1253
+ const slot = definition.textures?.[name];
1254
+
1255
+ if (slot === undefined || slot.missing === true) {
1256
+ continue;
1257
+ }
1258
+
1259
+ if (slot.udim === true) {
1260
+ udim++;
1261
+ }
1262
+
1263
+ try {
1264
+ const texture = await obtain(slot);
1265
+
1266
+ if (texture !== null) {
1267
+ materials[i][field] = texture;
1268
+ bound++;
1269
+ }
1270
+ } catch (error) {
1271
+ failed.push(`${definition.name}.${name}: ${error.message ?? error}`);
1272
+ }
1273
+ }
1274
+
1275
+ /*
1276
+ The tint already multiplied the texture's mean to make the constant. With the texture
1277
+ itself bound, the mean has to come back out or the surface is darkened by its own colour
1278
+ twice.
1279
+ */
1280
+ if (materials[i].texture_albedo !== undefined) {
1281
+ const tint = definition.diffuse_color ?? [1, 1, 1, 1];
1282
+
1283
+ materials[i].diffuse_color.set(tint[0], tint[1], tint[2], 1);
1284
+ }
1285
+
1286
+ /*
1287
+ Every one of these is a **multiplier on the map**, as glTF specifies — `roughness = orm.g *
1288
+ roughness_factor`, `metalness = orm.b * metallic_factor`. The constants that stood in for
1289
+ the map have to give way, or the map is scaled by the mean of itself.
1290
+
1291
+ glTF's default for both factors is 1 and `StandardShadeMaterial`'s own is 0 for metalness,
1292
+ which is right for a bare material with no map to multiply. The extraction wrote that 0
1293
+ through even where it was binding an ORM, and a factor of zero suppressing the map is the
1294
+ specified behaviour rather than a fault in it — so this is the extraction saying which of
1295
+ the two cases it is in.
1296
+ */
1297
+ if (materials[i].texture_orm !== undefined) {
1298
+ materials[i].roughness_factor = definition.roughness_factor ?? 1;
1299
+ materials[i].metallic_factor = definition.metallic_factor ?? 1;
1300
+
1301
+ materials[i].ambient_factors.a = 1;
1302
+ materials[i].ambient_factors.b = 0;
1303
+ }
1304
+ }));
1305
+
1306
+ return { bound, failed, udim };
1307
+ }
1308
+
1309
+ /**
1310
+ * Read the material sidecar beside a scene, if there is one.
1311
+ *
1312
+ * `?materials=<url>` names one explicitly and `?materials=off` refuses it, which is how the page
1313
+ * still shows what a converted scene looks like with nothing but geometry — the picture this page
1314
+ * had before there was anything to say about its surfaces.
1315
+ *
1316
+ * A sidecar that is absent is not an error. It is the ordinary case for any scene that has not had
1317
+ * its source project extracted.
1318
+ *
1319
+ * @param {string} scene_url
1320
+ * @param {URLSearchParams} parameters
1321
+ * @returns {Promise<object|null>}
1322
+ */
1323
+ async function load_scene_materials(scene_url, parameters) {
1324
+ const requested = parameters.get('materials');
1325
+
1326
+ if (requested === 'off') {
1327
+ return null;
1328
+ }
1329
+
1330
+ const url = requested !== null && requested !== ''
1331
+ ? new URL(requested, globalThis.location.href).href
1332
+ : scene_url.replace(new RegExp(`${SCENE_SUFFIX.replace(/\./g, '\\.')}$`), MATERIALS_SUFFIX);
1333
+
1334
+ if (url === scene_url) {
1335
+ return null;
1336
+ }
1337
+
1338
+ let response;
1339
+
1340
+ try {
1341
+ response = await fetch(url);
1342
+ } catch (error) {
1343
+ console.warn(`material sidecar ${url} — ${error.message ?? error}`);
1344
+
1345
+ return null;
1346
+ }
1347
+
1348
+ if (!response.ok) {
1349
+ // the ordinary case: this scene has no sidecar
1350
+ return null;
1351
+ }
1352
+
1353
+ const sidecar = await response.json();
1354
+
1355
+ if (sidecar.format !== 1) {
1356
+ console.warn(`${url} is a format-${sidecar.format} sidecar and this page reads format 1`);
1357
+
1358
+ return null;
1359
+ }
1360
+
1361
+ return sidecar;
1362
+ }
1363
+
1364
+ /**
1365
+ * Where the scene manifest is, out of `?scene=` or out of the asset root's own listing.
1366
+ *
1367
+ * The listing is what makes the page turn-key: the dev server answers `?find=` with the paths of
1368
+ * every manifest under every root it serves, so a page opened with no parameters at all finds the
1369
+ * one scene that is there. More than one and it takes the first and says which, because a page that
1370
+ * refuses to choose between two is a page that shows nothing.
1371
+ *
1372
+ * @returns {Promise<string>}
1373
+ */
1374
+ async function resolve_scene_url() {
1375
+ const requested = new URL(globalThis.location.href).searchParams.get('scene');
1376
+
1377
+ if (requested !== null) {
1378
+ return new URL(requested, globalThis.location.href).href;
1379
+ }
1380
+
1381
+ const listing = await fetch(`${ASSET_PREFIX}/?find=${encodeURIComponent(SCENE_SUFFIX)}`);
1382
+
1383
+ if (!listing.ok) {
1384
+ throw new Error(
1385
+ `no ?scene= was given and ${ASSET_PREFIX}/?find= answered HTTP ${listing.status};`
1386
+ + ` that listing is "tools/dev/vite_test_assets.js" under the dev server, so this is a`
1387
+ + ` page being served by something else — pass the manifest as ?scene=<url>`
1388
+ );
1389
+ }
1390
+
1391
+ const scenes = await listing.json();
1392
+
1393
+ if (scenes.length === 0) {
1394
+ throw new Error(
1395
+ `nothing under ${ASSET_PREFIX}/ is a ${SCENE_SUFFIX};`
1396
+ + ` build one with "node tools/geometry/virtual/vgeo_scene.js <manifest.vgeo.json>"`
1397
+ + ` under the asset root the dev server named at startup`
1398
+ );
1399
+ }
1400
+
1401
+ // the listing answers with a path, and a path is not a URL: a directory with a space in it is
1402
+ // ordinary on a Windows asset drive and is a 404 if it goes out unencoded
1403
+ const encoded = scenes[0].split('/').map(encodeURIComponent).join('/');
1404
+
1405
+ return new URL(`${ASSET_PREFIX}/${encoded}`, globalThis.location.href).href;
1406
+ }
1407
+
1408
+ /**
1409
+ * A number that is the same for the same scene and comfortably above what it will ever need.
1410
+ *
1411
+ * Three of the runtime's capacities are properties of the *scene* rather than of the engine, and
1412
+ * all three are knowable before a byte is fetched:
1413
+ *
1414
+ * - **`geometry_capacity`** is one row per container, and the default of 256 is an order of
1415
+ * magnitude short of a converted set.
1416
+ * - **`lookup_words`** is one word per page of every registered container, out of one arena. The
1417
+ * default of 65,536 holds a fourteenth of Zorah's 947,664, and it is allocated out of an
1418
+ * `OffsetAllocator` whose bins round *down* — it can refuse an allocation that fits — so the
1419
+ * slack here is the difference between a scene that registers and one that refuses its own
1420
+ * largest container for no visible reason.
1421
+ * - **`claim_word_capacity`** is one bit per (collected instance, page ordinal of its geometry).
1422
+ * The engine derives it from a hint of a thousand instances times the whole tier, which at this
1423
+ * scale is both far too small and far too large: the real bound is
1424
+ * `max instances of any one geometry × page capacity`, because ordinals come out of the tier and
1425
+ * the sum of `instances × ordinals` is maximised by giving every slot to the most-instanced
1426
+ * asset. For Zorah that is 381 × the tier, which is a fifth of what the default derivation asks
1427
+ * for and provably enough — doubled here, because an ordinal capacity is a high-water mark and a
1428
+ * scene that has churned can hold more of them than the tier holds pages.
1429
+ *
1430
+ * @param {object} manifest
1431
+ * @param {number} page_capacity
1432
+ * @returns {{geometry_capacity: number, lookup_words: number, claim_word_capacity: number, max_instances: number}}
1433
+ */
1434
+ function scene_capacities(manifest, page_capacity) {
1435
+ let max_instances = 1;
1436
+
1437
+ for (const geometry of manifest.geometries) {
1438
+ max_instances = Math.max(max_instances, geometry.instances.length / 16);
1439
+ }
1440
+
1441
+ return {
1442
+ geometry_capacity: manifest.geometry_count + 64,
1443
+ lookup_words: 1 << Math.ceil(Math.log2(Math.max(1 << 16, manifest.total_pages * 1.5))),
1444
+ claim_word_capacity: Math.ceil(max_instances * page_capacity / 32) * 2,
1445
+ max_instances,
1446
+ };
1447
+ }
1448
+
1449
+ async function start() {
1450
+ const canvas = element('view');
1451
+
1452
+ const page = new VGeoScenePlayground();
1453
+
1454
+ page.canvas = canvas;
1455
+
1456
+ const parameters = new URL(globalThis.location.href).searchParams;
1457
+
1458
+ page.status('reading the scene manifest…');
1459
+
1460
+ const scene_url = await resolve_scene_url();
1461
+
1462
+ const manifest_response = await fetch(scene_url);
1463
+
1464
+ if (!manifest_response.ok) {
1465
+ throw new Error(`${scene_url} answered HTTP ${manifest_response.status}`);
1466
+ }
1467
+
1468
+ const manifest = await manifest_response.json();
1469
+
1470
+ if (manifest.format !== 1) {
1471
+ throw new Error(
1472
+ `${scene_url} is a format-${manifest.format} scene and this page reads format 1`
1473
+ );
1474
+ }
1475
+
1476
+ page.manifest = manifest;
1477
+ page.base = scene_url;
1478
+
1479
+ const box = manifest.world_bounds;
1480
+
1481
+ page.world_radius = 0.5 * Math.hypot(box[3] - box[0], box[4] - box[1], box[5] - box[2]);
1482
+
1483
+ /*
1484
+ Page slots the VRAM tier is built with, over the engine's default of 1,024 and the runtime
1485
+ page's 4,096. A slot holds a page's *runtime rows* rather than its payload — an eighth of the
1486
+ page size — so this is a 96 MB arena; what it really spends is the decoded budget the pool
1487
+ derives from it, which is 12,288 pages of payload in the meshlet pool, or 768 MB.
1488
+
1489
+ **The number is what the τ slider costs, measured.** Zorah from its authored viewpoint at
1490
+ 1578×746 settles at 6,387 resident pages at τ=1 and 9,412 at τ=0.5, which is the slider's
1491
+ busiest end; at 8,192 slots that end fills the tier, `refused` climbs past two hundred
1492
+ thousand, `clamped` stops falling to zero and the cut is being selected by a **budget** rather
1493
+ than by τ — which is the one thing this page is not meant to be showing. A bigger window wants
1494
+ more, because the cut is a function of the resolution, so it is a URL parameter and `refused`
1495
+ is in the panel to say when it was the wrong one.
1496
+ */
1497
+ const page_capacity = Number(parameters.get('pages') ?? 16384);
1498
+
1499
+ const capacities = scene_capacities(manifest, page_capacity);
1500
+
1501
+ const renderer = new Renderer();
1502
+
1503
+ page.renderer = renderer;
1504
+
1505
+ await renderer.initialize({
1506
+ context: canvas.getContext('webgpu'),
1507
+ pixelRatio: globalThis.devicePixelRatio || 1,
1508
+ });
1509
+
1510
+ /*
1511
+ TAA on. A cut is re-selected every frame against a threshold in pixels, so the seams between
1512
+ two levels of detail land on different pixels from one frame to the next and every one of them
1513
+ is an aliased edge that crawls without a temporal filter — and the crawl reads as the cut being
1514
+ unstable when it is the rasterizer being point-sampled. It costs the readings nothing: TAA is
1515
+ downstream of the G-buffer, long after the cut has been selected and drawn.
1516
+ */
1517
+ renderer.feature_taa_enabled = true;
1518
+ renderer.feature_ssao_enabled = false;
1519
+ renderer.feature_bloom_enabled = false;
1520
+
1521
+ const scene = new Scene();
1522
+
1523
+ scene.dataset = new EntityComponentDataset();
1524
+
1525
+ page.scene = scene;
1526
+
1527
+ /*
1528
+ The scene's own sun when the manifest names one, and the engine's default otherwise.
1529
+
1530
+ A converted container carries geometry and nothing else — no material authoring, no lights —
1531
+ so the sun is most of what there is to shade it with, and which way it points is not
1532
+ decoration: the engine's default comes down from above and slightly behind the world axes,
1533
+ which over an outdoor scene authored around a different one puts every façade a viewer would
1534
+ stand in front of into ambient. `--sundirection` in Zorah's own config is the direction it
1535
+ shines *from*, which is the opposite of the direction a light points.
1536
+ */
1537
+ const sun = manifest.sun === undefined
1538
+ ? make_sunlight()
1539
+ : make_sunlight({
1540
+ direction: new Vector3(-manifest.sun[0], -manifest.sun[1], -manifest.sun[2]),
1541
+ });
1542
+
1543
+ /*
1544
+ `?sun=off` leaves the scene to its environment alone. Paired with `?env=uniform:<v>` it is a
1545
+ closed question rather than an argument: the only light is then a constant radiance from every
1546
+ direction, so a surface cannot return more of it than it reflects, and any surface brighter
1547
+ than the environment it is standing in is being lit by something that is not there.
1548
+ */
1549
+ if (parameters.get('sun') !== 'off') {
1550
+ place_light(scene, sun);
1551
+ }
1552
+
1553
+ /*
1554
+ Image-based lighting, and for the geometry rather than for the picture: a sun alone puts every
1555
+ surface facing away from it at ambient zero, which is exactly the silhouettes and the creases a
1556
+ cut is worth looking at. Not awaited — `test_assets/` is gitignored, so a checkout without it
1557
+ gets the sun alone and still draws every triangle the cut selected.
1558
+ */
1559
+ /*
1560
+ `?env=off` leaves it out, and it is a **diagnostic about the shading rather than about the
1561
+ geometry**: the environment is the only wide, neutral light in the scene, so it is the term to
1562
+ remove when a surface reads paler than its own albedo and the question is whether that colour
1563
+ is coming from the material at all. The renderer already treats a missing environment as an
1564
+ ordinary state — a checkout without `test_assets/` renders by the sun alone — so this needs
1565
+ nothing but skipping the load.
1566
+ */
1567
+ const environment = parameters.get('env');
1568
+
1569
+ if (environment === 'off') {
1570
+ // nothing: the sun alone
1571
+ } else if (environment !== null && environment.startsWith('uniform')) {
1572
+ scene.lights.environment = make_uniform_environment(Number(environment.split(':')[1] ?? 0.5));
1573
+ } else {
1574
+ load_default_environment(scene);
1575
+ }
1576
+
1577
+ const camera = new PerspectiveCamera();
1578
+
1579
+ camera.fov_degrees = manifest.camera?.fov ?? 60;
1580
+
1581
+ page.camera = camera;
1582
+ page.controller = new OrbitalCameraController(camera, document.body);
1583
+
1584
+ page.fit();
1585
+
1586
+ globalThis.addEventListener('resize', () => page.fit());
1587
+
1588
+ // one frame to let the renderer build its scene context, which is what owns the geometry manager
1589
+ renderer.render(camera, scene, 1 / 60);
1590
+
1591
+ const scene_context = renderer.scenes.obtain(scene);
1592
+
1593
+ page.scene_context = scene_context;
1594
+
1595
+ /*
1596
+ The residency's knobs, which are per-reading budgets rather than sizes — nothing here scales
1597
+ with how much is resident. What a scene changes about them is the breadth each reading has to
1598
+ cover: the wants of one reading now come from thousands of containers rather than from one, so
1599
+ a fetch budget sized for a single asset spends the whole reading on whichever asset happened to
1600
+ be first in the want list.
1601
+ */
1602
+ const residency = new VGeoResidencyOptions();
1603
+
1604
+ residency.ram_budget_bytes = 2 * GIGABYTE;
1605
+
1606
+ /*
1607
+ VRAM is a strict subset of RAM, so the RAM tier has to be able to hold the whole VRAM tier and
1608
+ every container's pinned root besides — 8,192 and 2,966 for Zorah, against a default of 16,384
1609
+ slots that the two together are already within sight of.
1610
+ */
1611
+ residency.ram_slot_capacity = Math.max(1 << 14, (page_capacity + manifest.geometry_count) * 2);
1612
+
1613
+ residency.max_fetches_per_reading = 64;
1614
+ residency.max_concurrent_fetches = 24;
1615
+ residency.max_promotions_per_reading = 128;
1616
+
1617
+ page.runtime = scene_context.enable_virtual_geometry({
1618
+ page_size: manifest.page_size,
1619
+ table_options: {
1620
+ page_capacity,
1621
+ geometry_capacity: capacities.geometry_capacity,
1622
+ lookup_words: capacities.lookup_words,
1623
+ },
1624
+ claim_word_capacity: capacities.claim_word_capacity,
1625
+ /*
1626
+ Pages one frame may ask for, across every view. A single asset's cut wants a handful once
1627
+ it has settled; a cold scene of three thousand containers wants from all of them at once,
1628
+ and a want the frame had no room for is one more reading before that page arrives.
1629
+ */
1630
+ want_capacity: 8192,
1631
+ /*
1632
+ Traversal items one wavefront may hold, over the engine's 1 << 16. This is the capacity a
1633
+ *scene* reaches first and the only one whose overflow is invisible without looking: at
1634
+ τ=0.5 with 17,424 instances the descent pushed past 65,536 items in flight and
1635
+ `VGEO_CUT_STAT.QUEUE_DROPPED` came back at 32,676 — a third of the frontier thrown away,
1636
+ which showed up not as an error but as `drawn` wandering over ten thousand clusters
1637
+ between one frame and the next as different subtrees lost the race.
1638
+
1639
+ It is two buffers of `capacity × 16` bytes per expansion site, so this is 4 MiB a site
1640
+ against the 1 MiB the default costs. At 1 << 18 the same view drops nothing.
1641
+ */
1642
+ queue_capacity: 1 << 18,
1643
+ /*
1644
+ The wavefront bound is the engine's and is deliberately **not** set here. It starts at 8
1645
+ and `GPUVirtualGeometryRuntime.note_incomplete_descent` raises it from what the cut
1646
+ reports, so this page gets the same escalation any host does — and pinning it here would
1647
+ hide the one behaviour worth watching from the page best placed to watch it.
1648
+
1649
+ `?iterations=` overrides it for an experiment. At 8 with no escalation, this scene at
1650
+ 3419×1286 left up to 13,467 items queued on 750 frames of 900 and `drawn` swung 11,857
1651
+ clusters; the panel's `dropped: queue N` row is that reading.
1652
+ */
1653
+ ...(parameters.has('iterations')
1654
+ ? { max_iterations: Number(parameters.get('iterations')), max_iteration_ceiling: Number(parameters.get('iterations')) }
1655
+ : {}),
1656
+ residency,
1657
+ });
1658
+
1659
+ page.residency = scene_context.virtual_geometry_residency;
1660
+ page.feedback = scene_context.virtual_geometry_feedback;
1661
+
1662
+ /*
1663
+ What the rasterization collections are primed to expect. Both of these grow on their own from
1664
+ what the frame reports, so priming is about the first seconds rather than the steady state —
1665
+ but the first seconds are when the scene is being placed, and a `Meshes` limit of 1e4 against
1666
+ 17,424 instances drops the difference on the floor and counts them as `instances_dropped` while
1667
+ the collection catches up.
1668
+ */
1669
+ const limits = renderer.graphics.collection_limits;
1670
+
1671
+ limits.prime(
1672
+ RasterizationCollectionType.Meshes,
1673
+ Math.max(1e4, Math.ceil(manifest.instance_count * 1.25))
1674
+ );
1675
+
1676
+ limits.prime(RasterizationCollectionType.Meshlets, 5e5);
1677
+
1678
+ const material = new StandardShadeMaterial();
1679
+
1680
+ material.diffuse_color.copy(new Color(0.72, 0.74, 0.78));
1681
+ material.roughness_factor = 0.55;
1682
+ material.metallic_factor = 0;
1683
+
1684
+ /*
1685
+ `?side=front|back|double` sets the whole scene's draw side, which is the cheapest way to ask
1686
+ whether what you are looking at is a winding problem: `double` culls nothing, so any surface
1687
+ that appears under it and not under `front` was being culled rather than being absent.
1688
+ */
1689
+ material.draw_side = DRAW_SIDES[parameters.get('side')] ?? ShadeDrawSide.Front;
1690
+
1691
+ page.material = material;
1692
+
1693
+ /*
1694
+ `?mirrored=flip|tint|double` puts reflected instances in a bucket of their own — see
1695
+ {@link VGeoScenePlayground.mirror_material}. Off by default: the engine reverses their winding
1696
+ per instance, so the default is already the correct picture.
1697
+
1698
+ - **tint** leaves the side alone and colours them, so you can see which geometry in the frame
1699
+ is reflected without changing what is drawn.
1700
+ - **flip** draws them `Back`-sided. This was the page's own fix before the engine had one; now
1701
+ it reverses a second time and reproduces the old inside-out picture on demand.
1702
+ - **double** draws both sides of them, which fills the holes and gets the shading wrong, and is
1703
+ here because it is the fix people reach for first.
1704
+ */
1705
+ const mirrored_mode = parameters.get('mirrored');
1706
+
1707
+ const mirror_material = new StandardShadeMaterial();
1708
+
1709
+ mirror_material.copy(material);
1710
+
1711
+ if (mirrored_mode === 'flip') {
1712
+ mirror_material.draw_side = ShadeDrawSide.Back;
1713
+ } else if (mirrored_mode === 'double') {
1714
+ mirror_material.draw_side = ShadeDrawSide.Double;
1715
+ } else if (mirrored_mode === 'tint') {
1716
+ mirror_material.diffuse_color.copy(new Color(0.85, 0.16, 0.12));
1717
+ }
1718
+
1719
+ page.mirror_material = mirrored_mode === null ? material : mirror_material;
1720
+
1721
+ /*
1722
+ The scene's own materials, when the source project has been extracted beside it. 3,163
1723
+ containers resolve to a few hundred distinct materials — a `.vgeo` is one glTF primitive and a
1724
+ primitive has one material — so this is a few hundred buckets in the draw path rather than one,
1725
+ which is the scene's real shape rather than a cost this page is choosing to pay.
1726
+ */
1727
+ const sidecar = await load_scene_materials(scene_url, parameters);
1728
+
1729
+ if (sidecar !== null) {
1730
+ const side = DRAW_SIDES[parameters.get('side')];
1731
+
1732
+ page.materials = sidecar.materials.map(definition => make_scene_material(definition, side));
1733
+ page.geometry_material = sidecar.geometry_material;
1734
+
1735
+ page.unmaterialised = make_scene_material({
1736
+ name: 'unmaterialised',
1737
+ constant: { albedo: [0.18, 0.18, 0.18], roughness: 0.7, metallic: 0 },
1738
+ }, side);
1739
+
1740
+ const noted = sidecar.materials.filter(m => (m.unsupported ?? []).length > 0).length;
1741
+
1742
+ page.status(
1743
+ `${sidecar.material_count} materials, `
1744
+ + `${noted} with something the standard material cannot carry`
1745
+ );
1746
+
1747
+ /*
1748
+ The maps themselves, but only when the containers can sample them. `uv0` is the sidecar
1749
+ saying the source document carried `TEXCOORD_0`; for a scene converted from a
1750
+ `POSITION`-only document there is nothing to sample *with*, and binding an albedo map would
1751
+ put one texel across every surface in the frame.
1752
+
1753
+ `?textures=off` refuses them, which is how the two halves are compared on the same scene.
1754
+ */
1755
+ if (sidecar.uv0 === true && parameters.get('textures') !== 'off') {
1756
+ page.status(`${sidecar.material_count} materials — loading textures…`);
1757
+
1758
+ const bound = await bind_scene_textures(
1759
+ sidecar.materials, page.materials, sidecar.texture_base,
1760
+ );
1761
+
1762
+ page.textures_bound = bound.bound;
1763
+
1764
+ for (const failure of bound.failed) {
1765
+ console.warn(`texture — ${failure}`);
1766
+ }
1767
+
1768
+ page.status(
1769
+ `${sidecar.material_count} materials, ${bound.bound} textures`
1770
+ + (bound.udim > 0 ? ` (${bound.udim} UDIM sets took their first tile)` : '')
1771
+ + (bound.failed.length > 0 ? `, ${bound.failed.length} failed` : '')
1772
+ );
1773
+ }
1774
+ }
1775
+
1776
+ globalThis.addEventListener('pagehide', () => page.feedback.destroy());
1777
+
1778
+ renderer.add_extension(new VGeoFrameStart(page));
1779
+ renderer.add_extension(new VGeoFrameEnd(page));
1780
+
1781
+ element('tau').addEventListener('input', event => {
1782
+ page.runtime.tau = Number(event.target.value);
1783
+
1784
+ element('tau-value').textContent = page.runtime.tau.toFixed(2);
1785
+ });
1786
+
1787
+ /*
1788
+ τ before the first container, for a session measuring one. Residency is a **high-water mark** —
1789
+ the tier only evicts under pressure — so a page swept downwards through the thresholds reports,
1790
+ at every step, the pages the *busiest* setting it has already been at asked for. A row of a τ
1791
+ table is only about τ if the page has been at nothing else, which means a load per row.
1792
+ */
1793
+ const requested_tau = Number(parameters.get('tau'));
1794
+
1795
+ if (Number.isFinite(requested_tau) && requested_tau > 0) {
1796
+ page.runtime.tau = requested_tau;
1797
+
1798
+ element('tau').value = String(requested_tau);
1799
+ element('tau-value').textContent = requested_tau.toFixed(2);
1800
+ }
1801
+
1802
+ // a drag on the τ slider is a drag on the body, and the camera controller listens there
1803
+ element('panel').addEventListener('pointerdown', event => event.stopPropagation());
1804
+
1805
+ install_profile_hotkey({
1806
+ renderer,
1807
+ name: "vgeo_scene",
1808
+ status: element('profile-status'),
1809
+ settings: () => {
1810
+ const state = page.state();
1811
+
1812
+ return [
1813
+ `τ=${state.tau}px`,
1814
+ state.scene.name,
1815
+ `${state.scene.containers} containers`,
1816
+ `${state.scene.instances} instances`,
1817
+ `${state.cut.drawn} clusters`,
1818
+ `${state.vram.pages} of ${state.vram.capacity} pages resident`,
1819
+ `${state.screen_width}×${state.screen_height}`,
1820
+ ];
1821
+ },
1822
+ });
1823
+
1824
+ globalThis.__state = () => page.state();
1825
+ globalThis.__page = page;
1826
+ globalThis.__look = (eye, target) => page.look(eye, target);
1827
+ globalThis.__camera_url = () => camera_url(page);
1828
+
1829
+ /*
1830
+ The camera before the first container, so the cut that runs while the scene is arriving is the
1831
+ one the viewpoint asked for rather than a cut of everything from the origin.
1832
+
1833
+ The manifest's camera when it has one — a converted scene's own authored pose, which is the
1834
+ only thing that knows which forty metres of a hundred-kilometre box the scene is *about*. With
1835
+ no camera, the box: wrong for Zorah and right for a scene that is its own extent, and the
1836
+ parameter overrides both.
1837
+ */
1838
+ const eye = read_vector(parameters.get('eye')) ?? manifest.camera?.eye;
1839
+ const target = read_vector(parameters.get('at')) ?? manifest.camera?.target;
1840
+
1841
+ const center = [
1842
+ (box[0] + box[3]) / 2, (box[1] + box[4]) / 2, (box[2] + box[5]) / 2,
1843
+ ];
1844
+
1845
+ page.viewpoints = [
1846
+ {
1847
+ name: 'scene',
1848
+ eye: [
1849
+ center[0] + page.world_radius * 0.9,
1850
+ center[1] + page.world_radius * 0.5,
1851
+ center[2] + page.world_radius * 0.9,
1852
+ ],
1853
+ target: center,
1854
+ },
1855
+ ];
1856
+
1857
+ if (manifest.camera !== undefined) {
1858
+ page.viewpoints.unshift({
1859
+ name: 'authored',
1860
+ eye: manifest.camera.eye,
1861
+ target: manifest.camera.target,
1862
+ });
1863
+ }
1864
+
1865
+ const start_view = eye !== undefined && target !== undefined
1866
+ ? { name: 'url', eye, target }
1867
+ : page.viewpoints[0];
1868
+
1869
+ page.look(start_view.eye, start_view.target);
1870
+
1871
+ wire_viewpoints(page);
1872
+ wire_camera_capture(page);
1873
+
1874
+ /*
1875
+ `?catch=0.15` arms the dropout catcher — see {@link FlickerCatcher} for why it reads pixels
1876
+ rather than counters. Off by default: it is a `drawImage` and a `getImageData` every frame, and
1877
+ a page that is a measurement harness should not pay for a diagnostic nobody asked for.
1878
+
1879
+ `&freeze=1` stops on the first one and leaves it on screen, which is what makes a screenshot of
1880
+ the page a screenshot of the offending frame. Without it the catcher keeps counting and the
1881
+ panel shows the running tally, which is the mode for flying around looking for one.
1882
+ */
1883
+ const catch_threshold = Number(parameters.get('catch'));
1884
+
1885
+ if (Number.isFinite(catch_threshold) && catch_threshold > 0) {
1886
+ page.catcher = new FlickerCatcher(
1887
+ canvas, catch_threshold, parameters.get('freeze') !== null, () => camera_url(page)
1888
+ );
1889
+
1890
+ globalThis.__caught = () => page.catcher.report();
1891
+ globalThis.__show_frame = frame => page.catcher.show(frame);
1892
+ globalThis.__catch_rows = () => page.catcher.rows;
1893
+ }
1894
+
1895
+ let stopped = false;
1896
+ let last_time;
1897
+
1898
+ function tick(time) {
1899
+ if (stopped) {
1900
+ return;
1901
+ }
1902
+
1903
+ const dt = last_time === undefined ? 1 / 60 : Math.min((time - last_time) / 1000, 0.05);
1904
+
1905
+ if (last_time !== undefined) {
1906
+ page.frame_ms += ((time - last_time) - page.frame_ms) * 0.1;
1907
+ }
1908
+
1909
+ last_time = time;
1910
+
1911
+ try {
1912
+ page.controller.update(dt);
1913
+
1914
+ renderer.render(camera, scene, dt);
1915
+ } catch (error) {
1916
+ stopped = true;
1917
+
1918
+ page.status(error.message ?? String(error), true);
1919
+
1920
+ console.error(error);
1921
+
1922
+ return;
1923
+ }
1924
+
1925
+ const state = page.state();
1926
+
1927
+ // after the render, so the canvas it reads is this frame's
1928
+ page.catcher?.sample(state);
1929
+
1930
+ paint(page, capacities, state);
1931
+
1932
+ requestAnimationFrame(tick);
1933
+ }
1934
+
1935
+ requestAnimationFrame(tick);
1936
+
1937
+ page.status(`opening ${manifest.geometry_count.toLocaleString()} containers…`);
1938
+
1939
+ /*
1940
+ Not awaited: the frame loop above is already running, and a scene that placed its first
1941
+ container two hundred frames before its last is the loop this page exists to show.
1942
+ */
1943
+ page.open_scene(manifest);
1944
+ }
1945
+
1946
+ /**
1947
+ * `"x,y,z"` out of a URL parameter.
1948
+ *
1949
+ * @param {string|null} text
1950
+ * @returns {number[]|undefined}
1951
+ */
1952
+ function read_vector(text) {
1953
+ if (text === null) {
1954
+ return undefined;
1955
+ }
1956
+
1957
+ const numbers = text.split(',').map(Number);
1958
+
1959
+ if (numbers.length !== 3 || numbers.some(n => !Number.isFinite(n))) {
1960
+ return undefined;
1961
+ }
1962
+
1963
+ return numbers;
1964
+ }
1965
+
1966
+ /**
1967
+ * A link that reopens the page **where it is standing now**, with everything else about how it was
1968
+ * opened left alone.
1969
+ *
1970
+ * This exists because a viewpoint is the one piece of state that a person has and a session does
1971
+ * not. Everything else this page can be told — the scene, τ, the tier's size, which side to draw —
1972
+ * is a URL parameter already; where the camera got dragged to is not, and a phenomenon that happens
1973
+ * "in many places" is useless to chase without one of them. So the button hands over a URL rather
1974
+ * than three numbers: paste it back and the page opens on the same view, at the same threshold,
1975
+ * with the same tier, and with whatever diagnostic was already armed still armed.
1976
+ *
1977
+ * `?eye=`/`?at=` were already the way in; this is the way out.
1978
+ *
1979
+ * @param {VGeoScenePlayground} page
1980
+ * @returns {string}
1981
+ */
1982
+ function camera_url(page) {
1983
+ const url = new URL(globalThis.location.href);
1984
+
1985
+ const state = page.state();
1986
+
1987
+ // nine significant digits, which is exact for a float and about half the characters an f64
1988
+ // print of one costs — the same trim `vgeo_scene.js` writes its matrices with
1989
+ const trim = value => Number(value.toPrecision(9));
1990
+
1991
+ const triple = values => values.map(trim).join(',');
1992
+
1993
+ url.searchParams.set('eye', triple(state.camera.eye));
1994
+ url.searchParams.set('at', triple([
1995
+ state.camera.target.x, state.camera.target.y, state.camera.target.z,
1996
+ ]));
1997
+
1998
+ /*
1999
+ τ as it stands rather than as it was asked for. The slider moves and the parameter does not
2000
+ follow it, so a link that carried only what the page was opened with would reopen at a
2001
+ different cut from the one being looked at — which for a page about a cut is the whole thing.
2002
+ */
2003
+ url.searchParams.set('tau', String(trim(state.tau)));
2004
+
2005
+ /*
2006
+ `URLSearchParams` percent-encodes the separators, and `eye=27.9%2C8.9%2C-4` is correct, ugly,
2007
+ and about to be read by a person and pasted into a message. A comma is legal unencoded in a
2008
+ query string, so it goes back.
2009
+ */
2010
+ return url.href.replace(/%2C/g, ',');
2011
+ }
2012
+
2013
+ /**
2014
+ * @param {VGeoScenePlayground} page
2015
+ */
2016
+ function wire_camera_capture(page) {
2017
+ const button = element('copy-camera');
2018
+ const output = element('camera-url');
2019
+
2020
+ button.addEventListener('click', async () => {
2021
+ const url = camera_url(page);
2022
+
2023
+ output.textContent = url;
2024
+ output.hidden = false;
2025
+
2026
+ /*
2027
+ The clipboard is a permission and a secure context away, and this page is served over
2028
+ plain HTTP from a dev server on whatever host someone pointed it at. So the text is put on
2029
+ screen first and copied second: the button works either way, and the label says which
2030
+ happened rather than failing silently.
2031
+ */
2032
+ try {
2033
+ await navigator.clipboard.writeText(url);
2034
+
2035
+ button.textContent = 'copied to clipboard';
2036
+
2037
+ // the controller's keyboard device listens on the body, and a click leaves focus here.
2038
+ // Only on the happy path: the fallback below has just made a selection, and moving
2039
+ // focus would drop it.
2040
+ document.body.focus();
2041
+ } catch {
2042
+ /*
2043
+ Selected rather than merely shown, so the fallback is one Ctrl+C rather than a drag
2044
+ across four lines of wrapped URL. `user-select: all` makes a click do the same thing,
2045
+ and this makes the click unnecessary.
2046
+ */
2047
+ const range = document.createRange();
2048
+
2049
+ range.selectNodeContents(output);
2050
+
2051
+ const selection = globalThis.getSelection();
2052
+
2053
+ selection.removeAllRanges();
2054
+ selection.addRange(range);
2055
+
2056
+ button.textContent = 'selected below — Ctrl+C';
2057
+ }
2058
+
2059
+ setTimeout(() => { button.textContent = 'copy camera link'; }, 2500);
2060
+ });
2061
+ }
2062
+
2063
+ /**
2064
+ * @param {VGeoScenePlayground} page
2065
+ */
2066
+ function wire_viewpoints(page) {
2067
+ const container = element('views');
2068
+
2069
+ container.innerHTML = '';
2070
+
2071
+ for (const viewpoint of page.viewpoints) {
2072
+ const button = document.createElement('button');
2073
+
2074
+ button.textContent = viewpoint.name;
2075
+
2076
+ button.addEventListener('click', () => {
2077
+ page.look(viewpoint.eye, viewpoint.target);
2078
+
2079
+ // the controller's keyboard device listens on the body, and a click leaves focus here
2080
+ document.body.focus();
2081
+ });
2082
+
2083
+ container.append(button);
2084
+ }
2085
+ }
2086
+
2087
+ /**
2088
+ * @param {VGeoScenePlayground} page
2089
+ * @param {object} capacities
2090
+ */
2091
+ function paint(page, capacities, state) {
2092
+ const scene = state.scene;
2093
+
2094
+ element('progress').firstElementChild.style.width =
2095
+ `${scene.declared === 0 ? 0 : (100 * scene.containers / scene.declared).toFixed(1)}%`;
2096
+
2097
+ rows('scene', [
2098
+ ['source', escape_html(scene.name)],
2099
+ ['containers', `${scene.containers.toLocaleString()} / ${scene.declared.toLocaleString()}`],
2100
+ ['instances', scene.instances.toLocaleString()],
2101
+ ['reflected', scene.mirrored.toLocaleString()],
2102
+ [
2103
+ 'materials',
2104
+ scene.materials === 0
2105
+ ? 'one — no sidecar'
2106
+ : `${scene.materials.toLocaleString()} over `
2107
+ + `${scene.materialised.toLocaleString()} containers`,
2108
+ ],
2109
+ ['source triangles', scene.faces.toLocaleString()],
2110
+ ['on the server', human_bytes(scene.bytes)],
2111
+ ['read', human_bytes(scene.bytes_read)],
2112
+ ['view', `${state.screen_width}×${state.screen_height}`],
2113
+ ['frame', `${state.frame_ms.toFixed(1)} ms`],
2114
+ ]);
2115
+
2116
+ rows('cut', [
2117
+ ['drawn clusters', state.cut.drawn.toLocaleString()],
2118
+ ['clamped', state.cut.clamped.toLocaleString()],
2119
+ ['pages visited', state.cut.pages.toLocaleString()],
2120
+ ['clusters tested', state.cut.clusters_tested.toLocaleString()],
2121
+ ['subtrees culled', (state.cut.culled_frustum + state.cut.culled_lod).toLocaleString()],
2122
+ ['clusters culled', state.cut.culled_cluster.toLocaleString()],
2123
+ [
2124
+ 'wavefronts',
2125
+ `${state.cut.wavefronts} / ${state.cut.max_iterations}`
2126
+ + (state.cut.iteration_escalations > 0 ? ` (${state.cut.iteration_escalations}↑)` : ''),
2127
+ ],
2128
+ ['wants', state.wants],
2129
+ ]);
2130
+
2131
+ /*
2132
+ Four counts of the cut giving up, kept apart from the six of it succeeding, because at this
2133
+ scale every one of them names a different capacity and a scene is the first thing big enough
2134
+ to reach any of them.
2135
+ */
2136
+ const dropped = [
2137
+ ['meshlets', state.cut.meshlets_dropped],
2138
+ ['instances', state.cut.instances_dropped],
2139
+ ['queue', state.cut.queue_dropped + state.cut.queue_remaining],
2140
+ ['wants', state.cut.wants_dropped],
2141
+ ].filter(([, count]) => count > 0);
2142
+
2143
+ rows('residency', [
2144
+ ['VRAM pages', `${state.vram.pages.toLocaleString()} / ${state.vram.capacity.toLocaleString()}`],
2145
+ ['VRAM bytes', human_bytes(state.vram.bytes)],
2146
+ ['RAM pages', state.ram.pages.toLocaleString()],
2147
+ ['RAM bytes', human_bytes(state.ram.bytes)],
2148
+ ['evicted', `${state.vram.evicted.toLocaleString()} / ${state.ram.evicted.toLocaleString()}`],
2149
+ ['fetching', state.fetches_in_flight],
2150
+ [
2151
+ 'refused',
2152
+ `<span class="${state.vram.refused > 0 ? 'bad' : 'good'}">`
2153
+ + `${state.vram.refused.toLocaleString()}</span>`,
2154
+ ],
2155
+ [
2156
+ 'dropped',
2157
+ dropped.length === 0
2158
+ ? '<span class="good">0</span>'
2159
+ : `<span class="bad">${dropped.map(([n, c]) => `${n} ${c.toLocaleString()}`).join(', ')}</span>`,
2160
+ ],
2161
+ ['claim words', capacities.claim_word_capacity.toLocaleString()],
2162
+ ]);
2163
+
2164
+ const catcher = page.catcher;
2165
+
2166
+ if (catcher === undefined) {
2167
+ return;
2168
+ }
2169
+
2170
+ const report = catcher.report();
2171
+
2172
+ element('catch-section').hidden = false;
2173
+
2174
+ rows('catch', [
2175
+ ['sampled', report.frames.toLocaleString()],
2176
+ ['threshold', `${(100 * report.threshold).toFixed(0)}%`],
2177
+ ['worst so far', `${(100 * report.worst).toFixed(1)}%`],
2178
+ [
2179
+ 'caught',
2180
+ `<span class="${report.events > 0 ? 'bad' : 'good'}">${report.events}</span>`,
2181
+ ],
2182
+ ['frozen', report.frozen ? `<span class="bad">yes</span>` : 'no'],
2183
+ ['last at frame', report.last === undefined ? '—' : report.last.frame],
2184
+ ]);
2185
+ }
2186
+
2187
+ /**
2188
+ * @param {string} id
2189
+ * @param {Array<[string, string|number]>} entries
2190
+ */
2191
+ function rows(id, entries) {
2192
+ element(id).innerHTML = entries
2193
+ .map(([name, value]) => `<div class="row"><span>${name}</span><span>${value}</span></div>`)
2194
+ .join('');
2195
+ }
2196
+
2197
+ /**
2198
+ * A manifest's source name lands in the panel's markup, so it is a string from outside the page in
2199
+ * the one place a string from outside the page can do something.
2200
+ *
2201
+ * @param {string} text
2202
+ * @returns {string}
2203
+ */
2204
+ function escape_html(text) {
2205
+ return text.replace(/[&<>"]/g, character => ({
2206
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;',
2207
+ })[character]);
2208
+ }
2209
+
2210
+ start().catch(error => {
2211
+ element('status').textContent = error.message ?? String(error);
2212
+ element('status').classList.add('error');
2213
+
2214
+ globalThis.__state = () => ({ error: error.message ?? String(error) });
2215
+
2216
+ console.error(error);
2217
+ });