@woosh/meep-engine 3.27.0 → 3.28.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 (255) hide show
  1. package/build/bundle-worker-terrain.js +1 -1
  2. package/package.json +1 -1
  3. package/src/core/geom/3d/mat4/m4_linear_determinant.d.ts +15 -0
  4. package/src/core/geom/3d/mat4/m4_linear_determinant.d.ts.map +1 -0
  5. package/src/core/geom/3d/mat4/m4_linear_determinant.js +24 -0
  6. package/src/core/geom/3d/mat4/m4_normal_matrix3.d.ts +4 -2
  7. package/src/core/geom/3d/mat4/m4_normal_matrix3.d.ts.map +1 -1
  8. package/src/core/geom/3d/mat4/m4_normal_matrix3.js +21 -11
  9. package/src/engine/Engine.d.ts.map +1 -1
  10. package/src/engine/Engine.js +38 -1
  11. package/src/engine/graphics3/GraphicsEngine.d.ts +5 -1
  12. package/src/engine/graphics3/GraphicsEngine.d.ts.map +1 -1
  13. package/src/engine/graphics3/GraphicsEngine.js +834 -830
  14. package/src/shade/RENDERER_CONTRACT.md +19 -10
  15. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +1 -1
  16. package/src/shade/playground/vgeo_runtime/README.md +27 -13
  17. package/src/shade/playground/vgeo_runtime/main.js +18 -5
  18. package/src/shade/playground/vgeo_runtime/sample_asset.d.ts +18 -0
  19. package/src/shade/playground/vgeo_runtime/sample_asset.d.ts.map +1 -0
  20. package/src/shade/playground/{vgeo_viewer → vgeo_runtime}/sample_asset.js +9 -8
  21. package/src/shade/playground/vgeo_scene/README.md +408 -0
  22. package/src/shade/playground/vgeo_scene/index.html +144 -0
  23. package/src/shade/playground/vgeo_scene/main.d.ts +2 -0
  24. package/src/shade/playground/vgeo_scene/main.d.ts.map +1 -0
  25. package/src/shade/playground/vgeo_scene/main.js +1716 -0
  26. package/src/shade/renderer/Renderer.d.ts +4 -1
  27. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  28. package/src/shade/renderer/Renderer.js +5 -2
  29. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +1 -2
  30. package/src/shade/renderer/geometry/meshlet/MeshletBatch.d.ts.map +1 -1
  31. package/src/shade/renderer/geometry/meshlet/MeshletBatch.js +3 -29
  32. package/src/shade/renderer/geometry/meshlet/encoding/ENCODED_ATTRIBUTE_DEFAULT_VALUES.d.ts +22 -0
  33. package/src/shade/renderer/geometry/meshlet/encoding/ENCODED_ATTRIBUTE_DEFAULT_VALUES.d.ts.map +1 -0
  34. package/src/shade/renderer/geometry/meshlet/encoding/ENCODED_ATTRIBUTE_DEFAULT_VALUES.js +49 -0
  35. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +4 -2
  36. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +10 -8
  37. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +12 -11
  38. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +1 -1
  39. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
  40. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +1 -1
  41. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_open_document.d.ts.map +1 -1
  42. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_open_document.js +5 -9
  43. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevel.d.ts +32 -0
  44. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevel.d.ts.map +1 -0
  45. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelOptions.d.ts +59 -0
  46. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelOptions.d.ts.map +1 -0
  47. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelSummary.d.ts +44 -0
  48. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelSummary.d.ts.map +1 -0
  49. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.d.ts +36 -0
  50. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.d.ts.map +1 -0
  51. package/src/shade/renderer/geometry/virtual/build/{vgeo_build_levels.js → level/vgeo_build_levels.js} +61 -95
  52. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_root_group.d.ts +19 -0
  53. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_root_group.d.ts.map +1 -0
  54. package/src/shade/renderer/geometry/virtual/build/{vgeo_build_root_group.js → level/vgeo_build_root_group.js} +1 -1
  55. package/src/shade/renderer/geometry/virtual/build/mesh/VirtualGeometryMesh.d.ts +73 -0
  56. package/src/shade/renderer/geometry/virtual/build/mesh/VirtualGeometryMesh.d.ts.map +1 -0
  57. package/src/shade/renderer/geometry/virtual/build/{VirtualGeometryMesh.js → mesh/VirtualGeometryMesh.js} +7 -7
  58. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts +29 -0
  59. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts.map +1 -0
  60. package/src/shade/renderer/geometry/virtual/build/{vgeo_compute_vertex_normals.js → mesh/vgeo_compute_vertex_normals.js} +3 -3
  61. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_tangents.d.ts +31 -0
  62. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_tangents.d.ts.map +1 -0
  63. package/src/shade/renderer/geometry/virtual/build/{vgeo_compute_vertex_tangents.js → mesh/vgeo_compute_vertex_tangents.js} +5 -5
  64. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts +46 -0
  65. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts.map +1 -0
  66. package/src/shade/renderer/geometry/virtual/build/{vgeo_normalize_source.js → mesh/vgeo_normalize_source.js} +9 -9
  67. package/src/shade/renderer/geometry/virtual/build/page/vgeo_assemble_pages.d.ts +13 -0
  68. package/src/shade/renderer/geometry/virtual/build/page/vgeo_assemble_pages.d.ts.map +1 -0
  69. package/src/shade/renderer/geometry/virtual/build/{vgeo_assemble_pages.js → page/vgeo_assemble_pages.js} +9 -9
  70. package/src/shade/renderer/geometry/virtual/build/page/vgeo_build_page_nodes.d.ts +13 -0
  71. package/src/shade/renderer/geometry/virtual/build/page/vgeo_build_page_nodes.d.ts.map +1 -0
  72. package/src/shade/renderer/geometry/virtual/build/{vgeo_build_page_nodes.js → page/vgeo_build_page_nodes.js} +4 -4
  73. package/src/shade/renderer/geometry/virtual/build/page/vgeo_page_node_count.d.ts +22 -0
  74. package/src/shade/renderer/geometry/virtual/build/page/vgeo_page_node_count.d.ts.map +1 -0
  75. package/src/shade/renderer/geometry/virtual/build/{vgeo_page_node_count.js → page/vgeo_page_node_count.js} +2 -2
  76. package/src/shade/renderer/geometry/virtual/build/partition/VGeoFaceGraph.d.ts +52 -0
  77. package/src/shade/renderer/geometry/virtual/build/partition/VGeoFaceGraph.d.ts.map +1 -0
  78. package/src/shade/renderer/geometry/virtual/build/{VGeoFaceGraph.js → partition/VGeoFaceGraph.js} +1 -1
  79. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_graph_components.d.ts +14 -0
  80. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_graph_components.d.ts.map +1 -0
  81. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_graph.d.ts +29 -0
  82. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_graph.d.ts.map +1 -0
  83. package/src/shade/renderer/geometry/virtual/build/{vgeo_partition_graph.js → partition/vgeo_partition_graph.js} +2 -2
  84. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_subgraph.d.ts +41 -0
  85. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_subgraph.d.ts.map +1 -0
  86. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_subgraph.js +92 -0
  87. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set.d.ts +10 -0
  88. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set.d.ts.map +1 -0
  89. package/src/shade/renderer/geometry/virtual/build/{vgeo_split_face_set.js → partition/vgeo_split_face_set.js} +12 -55
  90. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.d.ts +15 -0
  91. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.d.ts.map +1 -0
  92. package/src/shade/renderer/geometry/virtual/build/{vgeo_split_face_set_packed.js → partition/vgeo_split_face_set_packed.js} +3 -3
  93. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_bounding_sphere.d.ts +12 -0
  94. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_bounding_sphere.d.ts.map +1 -0
  95. package/src/shade/renderer/geometry/virtual/build/{vgeo_bounding_sphere.js → sphere/vgeo_bounding_sphere.js} +3 -3
  96. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_containing_sphere.d.ts +18 -0
  97. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_containing_sphere.d.ts.map +1 -0
  98. package/src/shade/renderer/geometry/virtual/build/{vgeo_containing_sphere.js → sphere/vgeo_containing_sphere.js} +3 -3
  99. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_sphere_inflate_to_contain.d.ts +9 -0
  100. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_sphere_inflate_to_contain.d.ts.map +1 -0
  101. package/src/shade/renderer/geometry/virtual/build/{vgeo_sphere_inflate_to_contain.js → sphere/vgeo_sphere_inflate_to_contain.js} +1 -1
  102. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.d.ts +24 -0
  103. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.d.ts.map +1 -0
  104. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.d.ts +20 -0
  105. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.d.ts.map +1 -0
  106. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.d.ts +14 -0
  107. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.d.ts.map +1 -0
  108. package/src/shade/renderer/geometry/virtual/build/{vgeo_extract_submesh.js → submesh/vgeo_extract_submesh.js} +5 -5
  109. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.d.ts +12 -0
  110. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.d.ts.map +1 -0
  111. package/src/shade/renderer/geometry/virtual/build/{vgeo_gather_faces.js → submesh/vgeo_gather_faces.js} +2 -2
  112. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_narrow_indices.d.ts +11 -0
  113. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_narrow_indices.d.ts.map +1 -0
  114. package/src/shade/renderer/geometry/virtual/build/{vgeo_narrow_indices.js → submesh/vgeo_narrow_indices.js} +3 -3
  115. package/src/shade/renderer/geometry/virtual/build/vgeo_build.d.ts.map +1 -1
  116. package/src/shade/renderer/geometry/virtual/build/vgeo_build.js +9 -11
  117. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attributes_read.d.ts +8 -2
  118. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attributes_read.d.ts.map +1 -1
  119. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attributes_read.js +8 -2
  120. package/src/shade/renderer/geometry/virtual/format/page/VGEO_GROUP_OFFSET.d.ts +15 -0
  121. package/src/shade/renderer/geometry/virtual/format/page/VGEO_GROUP_OFFSET.d.ts.map +1 -0
  122. package/src/shade/renderer/geometry/virtual/format/page/VGEO_GROUP_OFFSET.js +15 -0
  123. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.d.ts +28 -0
  124. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.d.ts.map +1 -1
  125. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.js +7 -3
  126. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts.map +1 -1
  127. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.js +9 -4
  128. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts.map +1 -1
  129. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.js +18 -5
  130. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
  131. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +9 -1
  132. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +21 -2
  133. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts +52 -2
  134. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts.map +1 -1
  135. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.js +97 -1
  136. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +4 -4
  137. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts.map +1 -1
  138. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.js +10 -0
  139. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.d.ts.map +1 -1
  140. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.js +59 -34
  141. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts +8 -8
  142. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts.map +1 -1
  143. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.js +11 -88
  144. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.d.ts +19 -0
  145. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.d.ts.map +1 -0
  146. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.js +98 -0
  147. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.d.ts +31 -0
  148. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.d.ts.map +1 -0
  149. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.js +99 -0
  150. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_queue.d.ts +17 -0
  151. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_queue.d.ts.map +1 -0
  152. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_queue.js +39 -0
  153. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_reseed.d.ts.map +1 -1
  154. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_reseed.js +7 -63
  155. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts.map +1 -1
  156. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.js +13 -30
  157. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts.map +1 -1
  158. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.js +6 -52
  159. package/src/shade/renderer/loader/gltf/load_gltf.d.ts.map +1 -1
  160. package/src/shade/renderer/loader/gltf/load_gltf.js +0 -19
  161. package/src/shade/renderer/material/chunk_material_draw_side.d.ts +14 -0
  162. package/src/shade/renderer/material/chunk_material_draw_side.d.ts.map +1 -0
  163. package/src/shade/renderer/material/chunk_material_draw_side.js +34 -0
  164. package/src/shade/renderer/material/standard/fragment_gbuffer.d.ts.map +1 -1
  165. package/src/shade/renderer/material/standard/fragment_gbuffer.js +8 -1
  166. package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.d.ts.map +1 -1
  167. package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.js +13 -1
  168. package/src/shade/renderer/rasterize/bucket/construct_primitive_state.d.ts +19 -5
  169. package/src/shade/renderer/rasterize/bucket/construct_primitive_state.d.ts.map +1 -1
  170. package/src/shade/renderer/rasterize/bucket/construct_primitive_state.js +23 -5
  171. package/src/shade/renderer/rasterize/bucket/filter_materials_by_bucket.d.ts.map +1 -1
  172. package/src/shade/renderer/rasterize/bucket/filter_materials_by_bucket.js +12 -5
  173. package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.d.ts +22 -0
  174. package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.d.ts.map +1 -0
  175. package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.js +36 -0
  176. package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.d.ts +15 -0
  177. package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.d.ts.map +1 -0
  178. package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.js +35 -0
  179. package/src/shade/renderer/rasterize/expand/bucket/RASTERIZATION_BUCKET_LIMIT.d.ts +10 -1
  180. package/src/shade/renderer/rasterize/expand/bucket/RASTERIZATION_BUCKET_LIMIT.d.ts.map +1 -1
  181. package/src/shade/renderer/rasterize/expand/bucket/RASTERIZATION_BUCKET_LIMIT.js +38 -29
  182. package/src/shade/renderer/rasterize/expand/bucket/chunk_rasterization_material_bucket.d.ts +5 -0
  183. package/src/shade/renderer/rasterize/expand/bucket/chunk_rasterization_material_bucket.d.ts.map +1 -1
  184. package/src/shade/renderer/rasterize/expand/bucket/chunk_rasterization_material_bucket.js +63 -55
  185. package/src/shade/renderer/rasterize/expand/graph_meshlets_rasterize.d.ts.map +1 -1
  186. package/src/shade/renderer/rasterize/expand/graph_meshlets_rasterize.js +1 -0
  187. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
  188. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +44 -53
  189. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_brick4.d.ts.map +1 -1
  190. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_brick4.js +6 -2
  191. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_ibl.d.ts.map +1 -1
  192. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_ibl.js +6 -2
  193. package/src/shade/renderer/rasterize/forward/vertex_shader_chunk.d.ts.map +1 -1
  194. package/src/shade/renderer/rasterize/forward/vertex_shader_chunk.js +19 -2
  195. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +7 -4
  196. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts.map +1 -1
  197. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.js +3 -2
  198. package/src/shade/renderer/rasterize/native/chunk_rasterize_triangle.d.ts.map +1 -1
  199. package/src/shade/renderer/rasterize/native/chunk_rasterize_triangle.js +15 -4
  200. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_alpha_tested_pass_descriptor.d.ts.map +1 -1
  201. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_alpha_tested_pass_descriptor.js +16 -5
  202. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_pass_descriptor.d.ts.map +1 -1
  203. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_pass_descriptor.js +16 -5
  204. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.d.ts.map +1 -1
  205. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.js +2 -1
  206. package/src/shade/renderer/rasterize/native/rasterization_shader_resources.d.ts.map +1 -1
  207. package/src/shade/renderer/rasterize/native/rasterization_shader_resources.js +4 -0
  208. package/src/shade/renderer/rasterize/native/viz/viz_rasterization_alpha_tested_pass_descriptor.d.ts.map +1 -1
  209. package/src/shade/renderer/rasterize/native/viz/viz_rasterization_alpha_tested_pass_descriptor.js +16 -7
  210. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  211. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +78 -88
  212. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.d.ts.map +1 -1
  213. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +21 -31
  214. package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
  215. package/src/shade/renderer/scene/GPUSceneContext.js +2 -0
  216. package/src/shade/renderer/scene/NODE3D_STRUCT.d.ts +19 -0
  217. package/src/shade/renderer/scene/NODE3D_STRUCT.d.ts.map +1 -1
  218. package/src/shade/renderer/scene/NODE3D_STRUCT.js +34 -0
  219. package/src/shade/renderer/scene/chunk_node3d_flags.d.ts +9 -0
  220. package/src/shade/renderer/scene/chunk_node3d_flags.d.ts.map +1 -0
  221. package/src/shade/renderer/scene/chunk_node3d_flags.js +23 -0
  222. package/src/shade/renderer/scene/chunk_scene_database_access.d.ts.map +1 -1
  223. package/src/shade/renderer/scene/chunk_scene_database_access.js +15 -1
  224. package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts.map +1 -1
  225. package/src/shade/renderer/scene/rows/GPUSceneRows.js +9 -1
  226. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_compute_normal_matrix_from_m4.d.ts +7 -0
  227. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_compute_normal_matrix_from_m4.d.ts.map +1 -1
  228. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_compute_normal_matrix_from_m4.js +16 -4
  229. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_mat4_linear_determinant.d.ts +12 -0
  230. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_mat4_linear_determinant.d.ts.map +1 -0
  231. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_mat4_linear_determinant.js +18 -0
  232. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_triangle_list_reverse_winding.d.ts +24 -0
  233. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_triangle_list_reverse_winding.d.ts.map +1 -0
  234. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_triangle_list_reverse_winding.js +33 -0
  235. package/src/shade/playground/vgeo_viewer/README.md +0 -150
  236. package/src/shade/playground/vgeo_viewer/cut_geometry.js +0 -460
  237. package/src/shade/playground/vgeo_viewer/index.html +0 -77
  238. package/src/shade/playground/vgeo_viewer/main.js +0 -1343
  239. package/src/shade/playground/vgeo_viewer/select_cut.js +0 -481
  240. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.d.ts +0 -258
  241. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.d.ts.map +0 -1
  242. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.d.ts +0 -49
  243. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.d.ts.map +0 -1
  244. package/src/shade/renderer/loader/gltf/fix_up_material_sides.js +0 -109
  245. package/src/shade/renderer/rasterize/expand/graph_triangles_filter_by_material_side.js +0 -78
  246. package/src/shade/renderer/rasterize/expand/shader_filter_triangle_by_material_side.js +0 -100
  247. package/src/shade/renderer/rasterize/forward/chunk_material_side_should_draw.js +0 -26
  248. package/src/shade/renderer/shader/chunk/jobs/chunk_job_system.d.ts +0 -3
  249. package/src/shade/renderer/shader/chunk/jobs/chunk_job_system.d.ts.map +0 -1
  250. /package/src/shade/renderer/geometry/virtual/build/{VGeoLevel.js → level/VGeoLevel.js} +0 -0
  251. /package/src/shade/renderer/geometry/virtual/build/{VGeoLevelOptions.js → level/VGeoLevelOptions.js} +0 -0
  252. /package/src/shade/renderer/geometry/virtual/build/{VGeoLevelSummary.js → level/VGeoLevelSummary.js} +0 -0
  253. /package/src/shade/renderer/geometry/virtual/build/{vgeo_graph_components.js → partition/vgeo_graph_components.js} +0 -0
  254. /package/src/shade/renderer/geometry/virtual/build/{VGeoGatheredFaces.js → submesh/VGeoGatheredFaces.js} +0 -0
  255. /package/src/shade/renderer/geometry/virtual/build/{VGeoSubmesh.js → submesh/VGeoSubmesh.js} +0 -0
@@ -1,830 +1,834 @@
1
- import { assert } from "../../core/assert.js";
2
- import Signal from "../../core/events/signal/Signal.js";
3
- import { Vector2 } from "../../core/geom/Vector2.js";
4
- import { max2 } from "../../core/math/max2.js";
5
- import { TextureDescriptor } from "../../shade/descriptor/texture/TextureDescriptor.js";
6
- import { PerspectiveCamera } from "../../shade/renderer/camera/PerspectiveCamera.js";
7
- import { DynamicResolutionScaling } from "../../shade/renderer/DynamicResolutionScaling.js";
8
- import { Renderer } from "../../shade/renderer/Renderer.js";
9
- import { Scene } from "../../shade/renderer/scene/Scene.js";
10
- import { Sampler2D } from "../graphics/texture/sampler/Sampler2D.js";
11
- import EmptyView from "../../view/elements/EmptyView.js";
12
- import { make_default_environment } from "./make_default_environment.js";
13
- import { shade_camera_projection_ray } from "./shade_camera_projection_ray.js";
14
- import { ShadeCameraAdapter } from "./ShadeCameraAdapter.js";
15
-
16
- /**
17
- * @typedef {import("../../core/geom/Vector3.js").Vector3} Vector3
18
- * @typedef {import("../../shade/renderer/extension/FramePhase.js").FramePhase} FramePhase
19
- * @typedef {import("../../shade/renderer/extension/RenderExtension.js").RenderExtension} RenderExtension
20
- * @typedef {import("../../shade/renderer/scene/GPUSceneContext.js").GPUSceneContext} GPUSceneContext
21
- * @typedef {import("../../shade/renderer/texture/ShadeTexture.js").ShadeTexture} ShadeTexture
22
- * @typedef {import("../graphics/ecs/camera/Camera.js").Camera} Camera
23
- */
24
-
25
- /**
26
- * What an offscreen picture is drawn into. Not the canvas's presentation format on purpose: a
27
- * caller reading pixels back wants a channel order it can name, and the presentation format is
28
- * whatever the platform prefers.
29
- *
30
- * @type {string}
31
- */
32
- const OFFSCREEN_FORMAT = "rgba8unorm";
33
-
34
- /**
35
- * Frames an offscreen picture is drawn before it is read.
36
- *
37
- * More than one because the frame is temporal: anti-aliasing, and every effect that reads last
38
- * frame's depth, need a history to have been written before what they produce is the picture rather
39
- * than the first draft of it. Small because the history is dropped by the resize that precedes it,
40
- * so this is how long convergence takes from nothing rather than how long it takes to forget a
41
- * previous subject.
42
- *
43
- * @type {number}
44
- */
45
- const OFFSCREEN_FRAMES = 8;
46
-
47
- /**
48
- * The frame rate dynamic resolution scaling holds, in frames per second.
49
- *
50
- * Thirty, by the sponsor's decision (D39). It is a **floor**, not a target the game aims to sit at:
51
- * the controller is silent while frames are cheaper than 33 ms, and everything it does happens on
52
- * hardware that cannot hold that. Raising it would mean trading resolution away on machines that
53
- * were coping.
54
- *
55
- * @type {number}
56
- */
57
- const TARGET_FRAME_RATE = 30;
58
-
59
- /**
60
- * The renderer meep 3 talks to: Shade behind the shape `GraphicsEngine` presented.
61
- *
62
- * It implements the **bootstrap seam** and nothing else — `{viewport, domElement, camera,
63
- * on.{preRender, postRender, contextLost, contextRestored, contextFailed}, needDraw, autoDraw,
64
- * start(), stop(), render(), updateSize()}` — because that is what `Engine` needs in order to boot,
65
- * and because the facade is deliberately not allowed to grow past §4.2 (D14).
66
- *
67
- * **Three things have been let through since, each by name.** {@link add_extension} /
68
- * {@link remove_extension} (E9), because an extension has to survive the restart that replaces the
69
- * renderer holding it, and
70
- * {@link scene_context} (E13, D29), because the managers a system needs — animation above all —
71
- * hang off Shade's per-scene GPU state and there is no other door to them. Both take or return
72
- * something the caller already owns; neither hands out the renderer.
73
- *
74
- * {@link renderer} is the third, and much the widest: it hands the renderer straight out, so
75
- * everything the other two are shaped to avoid is reachable through it. It is an escape hatch for
76
- * callers that have no other route, not a migration target — the old engine's 44
77
- * `.renderer`/`getRenderer()` call sites are each still a migration item.
78
- *
79
- * What is deliberately absent, and stays absent: `getRenderer()` (the old engine's accessor, and
80
- * the shape those call sites are being ported off), `layers` (CPU visibility dies with GPU
81
- * culling), `scene` (entities reach the scene through the mesh system, not through the facade),
82
- * and the three-shaped material manager.
83
- *
84
- * **Lifecycle differs from the old engine on purpose.** `GraphicsEngine` starts inside the `Engine`
85
- * constructor, which is why stopping an engine used to leave a restarted one with no renderer at
86
- * all. This one starts from `Engine.start()` and stops symmetrically, so `start → stop → start`
87
- * returns a working renderer (E1).
88
- */
89
- export class GraphicsEngine {
90
- /**
91
- * Duck-type the rest of the engine checks instead of `instanceof`, which is the convention
92
- * the legacy `CameraSystem` already used and `HeadsUpDisplaySystem` was moved onto in Phase 0.
93
- *
94
- * @returns {boolean}
95
- */
96
- get isGraphicsEngine() {
97
- return true;
98
- }
99
-
100
- /**
101
- * Shade's renderer. `null` before a successful {@link start} and again after {@link stop} —
102
- * the states a caller has to tolerate, which is why {@link render} checks rather than assumes.
103
- *
104
- * @type {Renderer|null}
105
- */
106
- #renderer = null;
107
-
108
- /**
109
- * Danger zone.
110
- * Be careful with what you do, with great Renderer comes great responsibility.
111
- * @returns {Renderer|null}
112
- */
113
- get renderer(){
114
- return this.#renderer;
115
- }
116
-
117
- /**
118
- * Trades internal resolution for frame time — see {@link dynamic_resolution}.
119
- *
120
- * @type {DynamicResolutionScaling}
121
- */
122
- #dynamic_resolution = new DynamicResolutionScaling();
123
-
124
- /**
125
- * When the previous frame was drawn, in the clock {@link performance} keeps. Seeded by
126
- * {@link start} so that the first frame after one has a real interval behind it rather than a
127
- * special case.
128
- *
129
- * @type {number}
130
- */
131
- #last_render_timestamp = 0;
132
-
133
- /**
134
- * Extensions registered through {@link add_extension}, in the order they were added — which is
135
- * not the order they record in: that is resolved from their own declarations, per phase, by the
136
- * registry the renderer holds.
137
- *
138
- * The engine holds them rather than the renderer because a renderer is replaced by every
139
- * restart while the systems that registered are not, and because a system has to be able to
140
- * register before a device exists.
141
- *
142
- * @type {RenderExtension[]}
143
- */
144
- #extensions = [];
145
-
146
- /**
147
- * The scene Shade draws.
148
- *
149
- * There is no getter, deliberately. The old engine handed its scene out and the editor wrote
150
- * into the graph directly — 14 call sites of coupling that §2.1 retires. Content reaches the
151
- * renderer through the system that owns it, which tells the facade *which* scene to draw and
152
- * keeps the graph to itself.
153
- *
154
- * @type {Scene}
155
- */
156
- #scene = new Scene();
157
-
158
- /**
159
- * @type {HTMLCanvasElement|null}
160
- */
161
- #canvas = null;
162
-
163
- /**
164
- * The camera the frame is drawn from. Owned here rather than handed in, because it is Shade's
165
- * and a caller building one would have to know that; what drives it is
166
- * {@link camera_sync_from_transform}, from whatever transform the active camera entity has.
167
- *
168
- * @type {PerspectiveCamera}
169
- */
170
- #camera = new PerspectiveCamera();
171
-
172
- constructor() {
173
- /**
174
- * The camera, in the shape the engine's existing consumers read it — `projectionMatrix` and
175
- * `matrixWorldInverse`, which is what `HeadsUpDisplaySystem` multiplies together. The real
176
- * Shade camera is on its `camera` property.
177
- *
178
- * @type {ShadeCameraAdapter}
179
- */
180
- this.camera = new ShadeCameraAdapter(this.#camera);
181
-
182
- this.on = {
183
- /**
184
- * **The camera-finalized phase.** Camera matrices are final and the frame has not been
185
- * submitted — which is what §2.1 already defines `preRender` to mean, so the old
186
- * engine's `visibilityConstructionStarted` / `visibilityConstructionEnded` do not
187
- * survive as signals of their own. Their subscribers — the legacy camera and light
188
- * systems, the editor's symbolic display — landed here when their systems were ported.
189
- *
190
- * @type {Signal}
191
- */
192
- preRender: new Signal(),
193
-
194
- /**
195
- * The frame has been submitted. Note that submitted is not finished — the GPU is still
196
- * working when this fires, on this renderer as on the last one.
197
- *
198
- * @type {Signal}
199
- */
200
- postRender: new Signal(),
201
-
202
- /**
203
- * The GPU device was lost. Nothing renders after this.
204
- *
205
- * @type {Signal}
206
- */
207
- contextLost: new Signal(),
208
-
209
- /**
210
- * **Never fires.** Kept because the contract has it and subscribers expect the shape,
211
- * but Shade does not recover a lost device (R6): everything the renderer built lived on
212
- * it, and a message plus a reload is the honest response. A subscriber here is waiting
213
- * for something that will not happen.
214
- *
215
- * @type {Signal<number>}
216
- */
217
- contextRestored: new Signal(),
218
-
219
- /**
220
- * There will be no rendering — no WebGPU, no adapter, or hardware below the floor.
221
- * Carries a {@link ShadeDeviceFailure}, whose `message` is written to be shown.
222
- *
223
- * @type {Signal<ShadeDeviceFailure>}
224
- */
225
- contextFailed: new Signal(),
226
- };
227
-
228
-
229
-
230
- /**
231
- * Final render resolution in whole device pixels. Zero until the viewport has a size.
232
- *
233
- * @readonly
234
- * @type {Vector2}
235
- */
236
- this.output_resolution = new Vector2(0, 0);
237
-
238
- /**
239
- * The DOM element the canvas lives in. Created here rather than in {@link start} so that
240
- * layout can be wired before a device exists — and so a failed start still leaves something
241
- * to put a message in.
242
- *
243
- * @type {EmptyView}
244
- */
245
- this.viewport = new EmptyView();
246
- this.viewport.size.onChanged.add(this.updateSize, this);
247
-
248
- /**
249
- * @type {HTMLCanvasElement|null}
250
- */
251
- this.domElement = null;
252
-
253
- /**
254
- * Whether the next frame has anything new to show. With {@link autoDraw} off, this is what
255
- * gates rendering at all.
256
- *
257
- * @type {boolean}
258
- */
259
- this.needDraw = true;
260
-
261
- /**
262
- * Draw every tick regardless of {@link needDraw}. On-demand rendering — menus and the
263
- * editor that should not burn frames — is turning this off.
264
- *
265
- * @type {boolean}
266
- */
267
- this.autoDraw = true;
268
-
269
- /**
270
- * @type {number}
271
- */
272
- this.frameIndex = 0;
273
-
274
- /*
275
- Wired here rather than in `start`, because the closures read the field and the field is
276
- re-assigned on every start: a controller that captured one renderer would keep steering it
277
- after a context restart replaced it.
278
- */
279
- this.#dynamic_resolution.get_scale = () => this.#renderer.internal_resolution_scale;
280
- this.#dynamic_resolution.set_scale = v => {
281
- this.#renderer.internal_resolution_scale = v;
282
- };
283
-
284
- /*
285
- Stated rather than inherited. It happens to equal the controller's own default today, and
286
- that is not a reason to leave it unsaid: the game's target is the game's decision (D39), and
287
- a library default is free to move without anybody meaning the game to follow it.
288
- */
289
- this.#dynamic_resolution.target_frame_rate = TARGET_FRAME_RATE;
290
- }
291
-
292
- /**
293
- * Trades internal resolution for frame time, and is the only thing in this engine that does.
294
- *
295
- * Exposed so that it can be turned off or re-targeted — a measurement that wants a fixed
296
- * resolution, or a settings screen that offers the choice — and for no other reason: it drives
297
- * itself from {@link render}, and nothing has to call it.
298
- *
299
- * **It targets {@link TARGET_FRAME_RATE}**, which makes it a floor-holder rather than a
300
- * frame-rate governor: it does nothing at all until a frame costs more than 33 ms, and on
301
- * hardware that clears the budget it sits at full resolution and never engages.
302
- *
303
- * @returns {DynamicResolutionScaling}
304
- */
305
- get dynamic_resolution() {
306
- return this.#dynamic_resolution;
307
- }
308
-
309
- /**
310
- * Draw this scene from now on.
311
- *
312
- * The narrowest seam that works: the facade is told what to draw, rather than handing out a
313
- * graph for anyone to write into. Shade's own `render` takes the scene as an argument, so this
314
- * matches how the renderer underneath already thinks about it.
315
- *
316
- * @param {Scene} scene
317
- */
318
- set_scene(scene) {
319
- assert.defined(scene, 'scene');
320
- assert.equal(scene.isScene, true, 'scene.isScene !== true');
321
-
322
- this.#scene = scene;
323
-
324
- /*
325
- Shade lights with indirect by default and a scene without an environment renders unlit, so
326
- the engine supplies one rather than leaving every caller to. Turning global illumination off
327
- is the thing that takes a decision, not turning it on.
328
-
329
- A scene that arrives with an environment keeps it: the scene owns its lighting and this only
330
- fills in what is missing. Otherwise handing over a scene would silently discard the
331
- environment it was built with, and the fix would be to call things in a particular order.
332
- */
333
- if (scene.lights.environment === undefined) {
334
- scene.lights.environment = make_default_environment();
335
- }
336
- }
337
-
338
- /**
339
- * Set the scene's image-based-lighting source, replacing whatever it was lighting with.
340
- *
341
- * No prefiltering happens here, unlike the equivalent on the old engine: Shade convolves the map
342
- * into its irradiance and roughness mips on the GPU, notices when the texture it was given has
343
- * been swapped, and redoes that work itself. So this is an assignment and the renderer catches up
344
- * on the next frame — safe before startup as well as after.
345
- *
346
- * The texture must be **octahedral**, which is the projection Shade samples the environment with.
347
- * `load_environment_map` and `resample_equirectangular_to_octahedral` are the ways in; a cube or
348
- * equirectangular texture handed over directly would sample as garbage rather than fail.
349
- *
350
- * There is deliberately no `set_environment_texture` counterpart. On the old engine that meant
351
- * "and show it as the skybox too", and Shade has no background of its own to show it on — the
352
- * visible sky is its own piece of work rather than a second line here.
353
- *
354
- * @param {ShadeTexture} texture octahedral-encoded spherical radiance
355
- */
356
- set_environment_map(texture) {
357
- assert.defined(texture, 'texture');
358
- assert.equal(texture.isShadeTexture, true, 'texture.isShadeTexture !== true');
359
-
360
- this.#scene.lights.environment = texture;
361
-
362
- this.needDraw = true;
363
- }
364
-
365
- /**
366
- * Record work into the frame at a named point in it — §4.8's extensibility surface, and the
367
- * honest replacement for the old engine's `preRender` / `buffersRendered` / `postRender`
368
- * signals, each of which maps to a phase.
369
- *
370
- * The extension is handed a `FrameContext` when its phase comes round and records with the
371
- * `graph_*` helpers, exactly as the built-in passes do. Resource handles come from the frame's
372
- * records rather than from a renderer: the frame graph is what knows which resources a pass
373
- * touched, and work recorded around it cannot be ordered or aliased against the rest of the
374
- * frame.
375
- *
376
- * **Registering does not require a device.** Extensions are held here and applied to the
377
- * renderer when one exists, and again after a restart — a system registers at startup without
378
- * knowing where in the lifecycle it is.
379
- *
380
- * @param {RenderExtension} extension
381
- * @returns {RenderExtension} `extension`, for handing back to {@link remove_extension}
382
- */
383
- add_extension(extension) {
384
- assert.defined(extension, 'extension');
385
- assert.isFunction(extension.record, 'extension.record');
386
-
387
- this.#extensions.push(extension);
388
-
389
- if (this.#renderer !== null) {
390
- this.#renderer.add_extension(extension);
391
- }
392
-
393
- return extension;
394
- }
395
-
396
- /**
397
- * How many extensions are registered against a phase.
398
- *
399
- * Read-only, and here because a system that registers one otherwise has no way to say that it
400
- * did: the extension is private to the system that owns it, and "start → stop → start leaks
401
- * nothing" is a property a gate has to be able to state. It says how many, not what they are,
402
- * and hands out nothing.
403
- *
404
- * @param {FramePhase} phase
405
- * @returns {number}
406
- */
407
- extension_count(phase) {
408
- assert.isNonNegativeInteger(phase, 'phase');
409
-
410
- let count = 0;
411
-
412
- for (let i = 0; i < this.#extensions.length; i++) {
413
- if (this.#extensions[i].phase === phase) {
414
- count++;
415
- }
416
- }
417
-
418
- return count;
419
- }
420
-
421
- /**
422
- * Take an extension back out. Identity is the object, so the phase does not have to be
423
- * repeated.
424
- *
425
- * @param {RenderExtension} extension
426
- * @returns {boolean} whether it was registered
427
- */
428
- remove_extension(extension) {
429
- const index = this.#extensions.indexOf(extension);
430
-
431
- if (index === -1) {
432
- return false;
433
- }
434
-
435
- this.#extensions.splice(index, 1);
436
-
437
- if (this.#renderer !== null) {
438
- this.#renderer.remove_extension(extension);
439
- }
440
-
441
- return true;
442
- }
443
-
444
- /**
445
- * Shade's GPU-side state for a scene the caller already owns — and with it the managers that
446
- * hang off it, of which the animation manager is the one Phase 4 needs.
447
- *
448
- * **The one thing D14 lets through, and D29 is why** (E13). The alternative was a system
449
- * registering a frame pass that draws nothing, purely to be handed `context.view.scene` when
450
- * its phase came round — which would have made the animation system claim a moment in drawing
451
- * for work that is not drawing. A named method costs one line of surface and says what it is.
452
- *
453
- * **This still hands out no graph.** `scene` is an argument, not a getter: only the system that
454
- * owns a scene can ask about it, so the coupling §2.1 retires — the facade handing its graph to
455
- * anyone who asks — is not what this is. What comes back is the renderer's own bookkeeping for
456
- * that scene, which is Shade's to shape and no one else's.
457
- *
458
- * @param {Scene} scene
459
- * @returns {GPUSceneContext|null} `null` while there is no device — before a successful
460
- * {@link start} and again after {@link stop}. A restart builds a **new** context, so a caller
461
- * holding ids the old one issued has to notice the object changed and register again.
462
- */
463
- scene_context(scene) {
464
- assert.defined(scene, 'scene');
465
- assert.equal(scene.isScene, true, 'scene.isScene !== true');
466
-
467
- const renderer = this.#renderer;
468
-
469
- if (renderer === null) {
470
- return null;
471
- }
472
-
473
- return renderer.scenes.obtain(scene);
474
- }
475
-
476
- /**
477
- * Draw a scene of somebody else's into a texture of that size, and hand back its pixels.
478
- *
479
- * **Tooling only.** This is a GPU readback and I-4 forbids those on any gameplay-affecting path;
480
- * what it is here for is pictures of things — an asset library's icons, a thumbnail, a capture.
481
- *
482
- * **No renderer and no device leave the facade** (D14). What the caller brings is a scene and a
483
- * camera, both of which it built; what it gets back is a `Sampler2D`. The frame itself goes
484
- * through `Renderer.render_to_target` (D50), so the picture is drawn by the renderer that is
485
- * already running, with the geometry and the textures it already holds resident.
486
- *
487
- * **The renderer is resized for the call and put back afterwards.** Everything a frame is built
488
- * out of is sized from the output resolution, so a 64 x 64 picture means a 64 x 64 frame; the
489
- * temporal history that costs is dropped either way by the resize.
490
- *
491
- * @param {Scene} scene
492
- * @param {Camera} camera
493
- * @param {number} width texels
494
- * @param {number} height texels
495
- * @returns {Promise<Sampler2D>} RGBA, one byte a channel
496
- * @throws {Error} when there is no device — before a successful {@link start}, or after
497
- * {@link stop}
498
- */
499
- async render_to_sampler(scene, camera, width, height) {
500
- assert.defined(scene, 'scene');
501
- assert.defined(camera, 'camera');
502
- assert.isNonNegativeInteger(width, 'width');
503
- assert.isNonNegativeInteger(height, 'height');
504
- assert.greaterThan(width, 0, 'width');
505
- assert.greaterThan(height, 0, 'height');
506
-
507
- const renderer = this.#renderer;
508
-
509
- if (renderer === null) {
510
- throw new Error('cannot render without a device — start() has not run, or stop() has');
511
- }
512
-
513
- const target = renderer.graphics.textures.contextFromDescriptor(TextureDescriptor.from({
514
- label: "GraphicsEngine/offscreen",
515
- size: [width, height, 1],
516
- format: OFFSCREEN_FORMAT,
517
- usage: GPUTextureUsage.RENDER_ATTACHMENT
518
- | GPUTextureUsage.TEXTURE_BINDING
519
- | GPUTextureUsage.COPY_SRC
520
- }));
521
-
522
- const pixel_ratio = renderer.pixel_ratio;
523
- const automatic_exposure = renderer.feature_automatic_exposure_enabled;
524
-
525
- try {
526
- renderer.pixel_ratio = 1;
527
- renderer.resize(width, height);
528
-
529
- /*
530
- A picture of one object against nothing has no scene to read an exposure off, and an
531
- adapting one would take the frames it adapts over — so a preview is drawn at the fixed
532
- exposure instead, which is what a studio light rig is for.
533
- */
534
- renderer.feature_automatic_exposure_enabled = false;
535
-
536
- for (let i = 0; i < OFFSCREEN_FRAMES; i++) {
537
- renderer.render_to_target(camera, scene, 1 / 60, target);
538
- }
539
-
540
- const data = await target.download();
541
-
542
- return new Sampler2D(data, 4, width, height);
543
- } finally {
544
- target.destroy();
545
-
546
- renderer.feature_automatic_exposure_enabled = automatic_exposure;
547
- renderer.pixel_ratio = pixel_ratio;
548
-
549
- this.updateSize();
550
- }
551
- }
552
-
553
- /**
554
- * Whether there is a device to render with.
555
- *
556
- * @returns {boolean}
557
- */
558
- get is_running() {
559
- return this.#renderer !== null;
560
- }
561
-
562
- /**
563
- * Stand a device up and attach its canvas to the viewport.
564
- *
565
- * Asynchronous because acquiring a WebGPU device is — there is no synchronous form of it — and
566
- * because failing to get one is a normal outcome that has to be reported rather than thrown
567
- * into a constructor. A failure raises `on.contextFailed` with a message written for a person
568
- * and rethrows, so a caller that wants to handle it can and one that does not still fails
569
- * loudly.
570
- *
571
- * @returns {Promise<void>}
572
- */
573
- async start() {
574
- if (this.#renderer !== null) {
575
- // already running; starting twice is a caller error rather than a no-op worth hiding
576
- throw new Error('GraphicsEngine is already started');
577
- }
578
-
579
- const canvas = document.createElement("canvas");
580
-
581
- canvas.classList.add("graphics-engine-render-canvas");
582
-
583
- const style = canvas.style;
584
-
585
- style.userSelect = style.webkitUserSelect = style.mozUserSelect = "none";
586
- // see https://www.w3.org/TR/pointerevents/#the-touch-action-css-property
587
- style.touchAction = "none";
588
-
589
- this.#canvas = canvas;
590
-
591
- const renderer = new Renderer();
592
-
593
- try {
594
- await renderer.initialize({ context: canvas.getContext('webgpu') });
595
- } catch (failure) {
596
- this.#canvas = null;
597
-
598
- this.on.contextFailed.send1(failure);
599
-
600
- throw failure;
601
- }
602
-
603
- renderer.onDeviceFailure.add(this.#handle_device_failure, this);
604
-
605
- // Extensions outlive the device: they are registered against the engine, not against a
606
- // renderer that a restart replaces. This is what makes a system able to register once, at
607
- // startup, without knowing whether a device exists yet.
608
- for (let i = 0; i < this.#extensions.length; i++) {
609
- renderer.add_extension(this.#extensions[i]);
610
- }
611
-
612
- this.#renderer = renderer;
613
- this.domElement = canvas;
614
-
615
- this.adopt_canvas(canvas);
616
-
617
- this.updateSize();
618
-
619
- // so the first frame's interval is a real one rather than the whole of the boot
620
- this.#last_render_timestamp = performance.now();
621
- }
622
-
623
- /**
624
- * Re-point the viewport at a canvas, and the page with it. This is the wrinkle E1 named:
625
- * `start` builds a *new* canvas, so a restart would otherwise leave the dead one mounted and
626
- * the live one detached. `View.el` is read unguarded all over the view layer, so it is replaced
627
- * rather than cleared — it always holds an element.
628
- *
629
- * The inline `pointer-events` of the element being replaced comes along: the engine opts the
630
- * viewport back in (`auto`) under a game view that lets clicks fall through (`none`), and it
631
- * does so on the placeholder element before there is a canvas. A canvas that did not inherit
632
- * the opt-in inherited the fall-through instead, and every pointer device bound to the canvas
633
- * — the editor's tools — was deaf to every press.
634
- *
635
- * Public so that the swap can be driven without a device.
636
- *
637
- * @param {HTMLCanvasElement} canvas
638
- */
639
- adopt_canvas(canvas) {
640
- const previous = this.viewport.el;
641
-
642
- if (previous != null && previous !== canvas && previous.style !== undefined && previous.style.pointerEvents !== "") {
643
- canvas.style.pointerEvents = previous.style.pointerEvents;
644
- }
645
-
646
- this.viewport.el = canvas;
647
-
648
- if (previous != null && previous !== canvas && previous.parentNode !== null) {
649
- previous.parentNode.replaceChild(canvas, previous);
650
- }
651
- }
652
-
653
- /**
654
- * Tear the device down and let go of the canvas.
655
- *
656
- * Symmetric with {@link start}, and that symmetry is the point: the old engine started in a
657
- * constructor and could not be restarted.
658
- */
659
- stop() {
660
- const renderer = this.#renderer;
661
-
662
- if (renderer === null) {
663
- // never started, or stopped twice — nothing to undo
664
- return;
665
- }
666
-
667
- renderer.onDeviceFailure.remove(this.#handle_device_failure, this);
668
- renderer.destroy();
669
-
670
- this.#renderer = null;
671
- this.#canvas = null;
672
- this.domElement = null;
673
-
674
- // `viewport.el` is deliberately left pointing at the spent canvas rather than cleared: the
675
- // view layer reads it without checking, and the next `start` swaps it for a live one.
676
- }
677
-
678
- /**
679
- * @param {ShadeDeviceFailure} failure
680
- */
681
- #handle_device_failure(failure) {
682
- this.on.contextLost.dispatch(failure);
683
- }
684
-
685
- /**
686
- * A pointer position in viewport pixels, as a clip-space point.
687
- *
688
- * Unchanged from the old engine, down to writing into a caller's vector: it is arithmetic on the
689
- * viewport size and has nothing to do with which renderer is underneath.
690
- *
691
- * @param {Vector2|Vector3} input viewport pixels, origin top-left
692
- * @param {Vector2|Vector3} result written with −1…1, +Y up
693
- */
694
- normalizeViewportPoint(input, result) {
695
- const size = this.viewport.size;
696
-
697
- // shifted by half a pixel, so the value names the centre of the pixel rather than its corner
698
- result.x = ((input.x + 0.5) / size.x) * 2 - 1;
699
- result.y = -((input.y + 0.5) / size.y) * 2 + 1;
700
- }
701
-
702
- /**
703
- * A ray from the eye through a point on the viewport, in world space.
704
- *
705
- * Same name, same arguments, same meaning as on the old engine — and the same expectation that
706
- * `x` and `y` are **clip-space**, which {@link normalizeViewportPoint} is what produces. What is
707
- * behind it is Shade's camera rather than three's; see {@link shade_camera_projection_ray}.
708
- *
709
- * This is the only picking primitive the game actually needs. Every gameplay pick is a terrain
710
- * raycast — an exact query against the terrain's own representation, which no part of the renderer
711
- * is involved in — so a working ray is the whole of the renderer's contribution to it.
712
- *
713
- * @param {number} x clip space, −1…1
714
- * @param {number} y clip space, −1…1, +Y up
715
- * @param {Vector3} source written with the ray's origin
716
- * @param {Vector3} direction written with a unit direction
717
- */
718
- viewportProjectionRay(x, y, source, direction) {
719
- shade_camera_projection_ray(this.#camera, x, y, source, direction);
720
- }
721
-
722
- /**
723
- * Size the drawing buffer to the viewport.
724
- *
725
- * Shade's `resize` takes a **CSS** size and applies the device pixel ratio itself, which is the
726
- * opposite of what the old engine's renderer wanted. `output_resolution` is then the
727
- * device-pixel figure, for everything that has to land on the same texel grid.
728
- *
729
- * **There is no engine-level `pixelRatio` any more, and that is the answer to GAP-027 rather
730
- * than an oversight.** The field was a public `Vector1` that `updateSize` read, and nothing
731
- * bound its `onChanged` — the constructor bound `viewport.size.onChanged` and stopped — so
732
- * writing it changed nothing until the window happened to be resized, which on a full-screen
733
- * game is never. The first symptom is a setting that appears dead and then works perfectly the
734
- * moment somebody drags the window. Calling `updateSize()` by hand did not rescue it either:
735
- * `Renderer.resize` asserts whole pixels and the product was passed unfloored, so any
736
- * fractional ratio threw. Between the two, the property had no working use at any point in its
737
- * life.
738
- *
739
- * Render resolution belongs to the renderer, which already has the hooks for it —
740
- * `internal_resolution_scale` takes any positive number, floors internally, and is upscaled
741
- * back by the renderer's own TAA rather than by the browser stretching a smaller canvas, which
742
- * is the better answer on quality grounds as well. It is reachable through the `renderer`
743
- * getter. A second, half-wired knob on this facade was only ever going to be found by someone
744
- * who then had to discover why it did nothing.
745
- */
746
- updateSize() {
747
- const size = this.viewport.size;
748
-
749
- const css_width = max2(0, size.x);
750
- const css_height = max2(0, size.y);
751
-
752
- const renderer = this.#renderer;
753
-
754
- if (renderer !== null) {
755
- renderer.resize(css_width, css_height);
756
- }
757
-
758
- if (this.domElement !== null) {
759
- this.domElement.style.width = size.x + "px";
760
- this.domElement.style.height = size.y + "px";
761
- }
762
-
763
- const device_pixel_ratio = window.devicePixelRatio;
764
-
765
- // clamped to 1 for the same reason the old engine clamped it: a collapsed viewport still
766
- // allocates attachments, and a zero-sized one is not a legal target
767
- this.output_resolution.set(
768
- max2(1, Math.floor(css_width * device_pixel_ratio)),
769
- max2(1, Math.floor(css_height * device_pixel_ratio))
770
- );
771
- }
772
-
773
- /**
774
- * Draw a frame, if there is anything to draw it with.
775
- *
776
- * @param {number} [time_delta_seconds]
777
- * @returns {boolean} whether a frame was submitted
778
- */
779
- render(time_delta_seconds = 1 / 60) {
780
- const renderer = this.#renderer;
781
-
782
- if (renderer === null) {
783
- return false;
784
- }
785
-
786
- const viewport_size = this.viewport.size;
787
-
788
- if (viewport_size.x <= 0 || viewport_size.y <= 0) {
789
- /*
790
- There is no surface to draw into. Every boot passes through this — the renderer starts
791
- before the game view is laid out — and so does any collapsed or unmounted view.
792
- Drawing anyway is worse than drawing nothing: `output_resolution` and the renderer's
793
- own resolutions both floor at one texel, and a one-texel target is a size the frame's
794
- mip chains cannot be built from, so what a frame produces here is a run of invalid
795
- textures rather than a picture. `needDraw` is left set, so the next frame retries.
796
- */
797
- return false;
798
- }
799
-
800
- /*
801
- The **refresh** interval, measured here rather than taken from `time_delta_seconds`. What
802
- the controller steers by has to be how long a frame actually took, wall clock, including
803
- everything outside this call; the argument is the simulation's delta, which is clamped and
804
- is not the same quantity. Measuring between calls is also what makes this correct for any
805
- driver — the engine's animation frame, or a probe calling `render` directly.
806
- */
807
- const now = performance.now();
808
-
809
- this.#dynamic_resolution.notify_frame((now - this.#last_render_timestamp) / 1000);
810
-
811
- this.#last_render_timestamp = now;
812
-
813
- // Aspect follows the surface being drawn to, so it is refreshed here rather than left to
814
- // whoever last resized: a camera describing a viewport that has since changed shape is the
815
- // classic source of a stretched frame.
816
- this.#camera.aspect = renderer.aspect_ratio;
817
- this.#camera.update();
818
-
819
- this.on.preRender.dispatch();
820
-
821
- const submitted = renderer.render(this.#camera, this.#scene, time_delta_seconds);
822
-
823
- this.frameIndex++;
824
- this.needDraw = false;
825
-
826
- this.on.postRender.dispatch();
827
-
828
- return submitted;
829
- }
830
- }
1
+ import { assert } from "../../core/assert.js";
2
+ import Signal from "../../core/events/signal/Signal.js";
3
+ import { Vector2 } from "../../core/geom/Vector2.js";
4
+ import { max2 } from "../../core/math/max2.js";
5
+ import { TextureDescriptor } from "../../shade/descriptor/texture/TextureDescriptor.js";
6
+ import { PerspectiveCamera } from "../../shade/renderer/camera/PerspectiveCamera.js";
7
+ import { DynamicResolutionScaling } from "../../shade/renderer/DynamicResolutionScaling.js";
8
+ import { Renderer } from "../../shade/renderer/Renderer.js";
9
+ import { Scene } from "../../shade/renderer/scene/Scene.js";
10
+ import { Sampler2D } from "../graphics/texture/sampler/Sampler2D.js";
11
+ import EmptyView from "../../view/elements/EmptyView.js";
12
+ import { make_default_environment } from "./make_default_environment.js";
13
+ import { shade_camera_projection_ray } from "./shade_camera_projection_ray.js";
14
+ import { ShadeCameraAdapter } from "./ShadeCameraAdapter.js";
15
+
16
+ /**
17
+ * @typedef {import("../../core/geom/Vector3.js").Vector3} Vector3
18
+ * @typedef {import("../../shade/renderer/extension/FramePhase.js").FramePhase} FramePhase
19
+ * @typedef {import("../../shade/renderer/extension/RenderExtension.js").RenderExtension} RenderExtension
20
+ * @typedef {import("../../shade/renderer/scene/GPUSceneContext.js").GPUSceneContext} GPUSceneContext
21
+ * @typedef {import("../../shade/renderer/texture/ShadeTexture.js").ShadeTexture} ShadeTexture
22
+ * @typedef {import("../graphics/ecs/camera/Camera.js").Camera} Camera
23
+ */
24
+
25
+ /**
26
+ * What an offscreen picture is drawn into. Not the canvas's presentation format on purpose: a
27
+ * caller reading pixels back wants a channel order it can name, and the presentation format is
28
+ * whatever the platform prefers.
29
+ *
30
+ * @type {string}
31
+ */
32
+ const OFFSCREEN_FORMAT = "rgba8unorm";
33
+
34
+ /**
35
+ * Frames an offscreen picture is drawn before it is read.
36
+ *
37
+ * More than one because the frame is temporal: anti-aliasing, and every effect that reads last
38
+ * frame's depth, need a history to have been written before what they produce is the picture rather
39
+ * than the first draft of it. Small because the history is dropped by the resize that precedes it,
40
+ * so this is how long convergence takes from nothing rather than how long it takes to forget a
41
+ * previous subject.
42
+ *
43
+ * @type {number}
44
+ */
45
+ const OFFSCREEN_FRAMES = 8;
46
+
47
+ /**
48
+ * The frame rate dynamic resolution scaling holds, in frames per second.
49
+ *
50
+ * Thirty, by the sponsor's decision (D39). It is a **floor**, not a target the game aims to sit at:
51
+ * the controller is silent while frames are cheaper than 33 ms, and everything it does happens on
52
+ * hardware that cannot hold that. Raising it would mean trading resolution away on machines that
53
+ * were coping.
54
+ *
55
+ * @type {number}
56
+ */
57
+ const TARGET_FRAME_RATE = 30;
58
+
59
+ /**
60
+ * The renderer meep 3 talks to: Shade behind the shape `GraphicsEngine` presented.
61
+ *
62
+ * It implements the **bootstrap seam** and nothing else — `{viewport, domElement, camera,
63
+ * on.{preRender, postRender, contextLost, contextRestored, contextFailed}, needDraw, autoDraw,
64
+ * start(), stop(), render(), updateSize()}` — because that is what `Engine` needs in order to boot,
65
+ * and because the facade is deliberately not allowed to grow past §4.2 (D14).
66
+ *
67
+ * **Three things have been let through since, each by name.** {@link add_extension} /
68
+ * {@link remove_extension} (E9), because an extension has to survive the restart that replaces the
69
+ * renderer holding it, and
70
+ * {@link scene_context} (E13, D29), because the managers a system needs — animation above all —
71
+ * hang off Shade's per-scene GPU state and there is no other door to them. Both take or return
72
+ * something the caller already owns; neither hands out the renderer.
73
+ *
74
+ * {@link renderer} is the third, and much the widest: it hands the renderer straight out, so
75
+ * everything the other two are shaped to avoid is reachable through it. It is an escape hatch for
76
+ * callers that have no other route, not a migration target — the old engine's 44
77
+ * `.renderer`/`getRenderer()` call sites are each still a migration item.
78
+ *
79
+ * What is deliberately absent, and stays absent: `getRenderer()` (the old engine's accessor, and
80
+ * the shape those call sites are being ported off), `layers` (CPU visibility dies with GPU
81
+ * culling), `scene` (entities reach the scene through the mesh system, not through the facade),
82
+ * and the three-shaped material manager.
83
+ *
84
+ * **Lifecycle differs from the old engine on purpose.** `GraphicsEngine` starts inside the `Engine`
85
+ * constructor, which is why stopping an engine used to leave a restarted one with no renderer at
86
+ * all. This one starts from `Engine.start()` and stops symmetrically, so `start → stop → start`
87
+ * returns a working renderer (E1).
88
+ */
89
+ export class GraphicsEngine {
90
+ /**
91
+ * Duck-type the rest of the engine checks instead of `instanceof`, which is the convention
92
+ * the legacy `CameraSystem` already used and `HeadsUpDisplaySystem` was moved onto in Phase 0.
93
+ *
94
+ * @returns {boolean}
95
+ */
96
+ get isGraphicsEngine() {
97
+ return true;
98
+ }
99
+
100
+ /**
101
+ * Shade's renderer. `null` before a successful {@link start} and again after {@link stop} —
102
+ * the states a caller has to tolerate, which is why {@link render} checks rather than assumes.
103
+ *
104
+ * @type {Renderer|null}
105
+ */
106
+ #renderer = null;
107
+
108
+ /**
109
+ * Danger zone.
110
+ * Be careful with what you do, with great Renderer comes great responsibility.
111
+ * @returns {Renderer|null}
112
+ */
113
+ get renderer(){
114
+ return this.#renderer;
115
+ }
116
+
117
+ /**
118
+ * Trades internal resolution for frame time — see {@link dynamic_resolution}.
119
+ *
120
+ * @type {DynamicResolutionScaling}
121
+ */
122
+ #dynamic_resolution = new DynamicResolutionScaling();
123
+
124
+ /**
125
+ * When the previous frame was drawn, in the clock {@link performance} keeps. Seeded by
126
+ * {@link start} so that the first frame after one has a real interval behind it rather than a
127
+ * special case.
128
+ *
129
+ * @type {number}
130
+ */
131
+ #last_render_timestamp = 0;
132
+
133
+ /**
134
+ * Extensions registered through {@link add_extension}, in the order they were added — which is
135
+ * not the order they record in: that is resolved from their own declarations, per phase, by the
136
+ * registry the renderer holds.
137
+ *
138
+ * The engine holds them rather than the renderer because a renderer is replaced by every
139
+ * restart while the systems that registered are not, and because a system has to be able to
140
+ * register before a device exists.
141
+ *
142
+ * @type {RenderExtension[]}
143
+ */
144
+ #extensions = [];
145
+
146
+ /**
147
+ * The scene Shade draws.
148
+ *
149
+ * There is no getter, deliberately. The old engine handed its scene out and the editor wrote
150
+ * into the graph directly — 14 call sites of coupling that §2.1 retires. Content reaches the
151
+ * renderer through the system that owns it, which tells the facade *which* scene to draw and
152
+ * keeps the graph to itself.
153
+ *
154
+ * @type {Scene}
155
+ */
156
+ #scene = new Scene();
157
+
158
+ /**
159
+ * @type {HTMLCanvasElement|null}
160
+ */
161
+ #canvas = null;
162
+
163
+ /**
164
+ * The camera the frame is drawn from. Owned here rather than handed in, because it is Shade's
165
+ * and a caller building one would have to know that; what drives it is
166
+ * {@link camera_sync_from_transform}, from whatever transform the active camera entity has.
167
+ *
168
+ * @type {PerspectiveCamera}
169
+ */
170
+ #camera = new PerspectiveCamera();
171
+
172
+ constructor() {
173
+ /**
174
+ * The camera, in the shape the engine's existing consumers read it — `projectionMatrix` and
175
+ * `matrixWorldInverse`, which is what `HeadsUpDisplaySystem` multiplies together. The real
176
+ * Shade camera is on its `camera` property.
177
+ *
178
+ * @type {ShadeCameraAdapter}
179
+ */
180
+ this.camera = new ShadeCameraAdapter(this.#camera);
181
+
182
+ this.on = {
183
+ /**
184
+ * **The camera-finalized phase.** Camera matrices are final and the frame has not been
185
+ * submitted — which is what §2.1 already defines `preRender` to mean, so the old
186
+ * engine's `visibilityConstructionStarted` / `visibilityConstructionEnded` do not
187
+ * survive as signals of their own. Their subscribers — the legacy camera and light
188
+ * systems, the editor's symbolic display — landed here when their systems were ported.
189
+ *
190
+ * @type {Signal}
191
+ */
192
+ preRender: new Signal(),
193
+
194
+ /**
195
+ * The frame has been submitted. Note that submitted is not finished — the GPU is still
196
+ * working when this fires, on this renderer as on the last one.
197
+ *
198
+ * @type {Signal}
199
+ */
200
+ postRender: new Signal(),
201
+
202
+ /**
203
+ * The GPU device was lost. Nothing renders after this.
204
+ *
205
+ * @type {Signal}
206
+ */
207
+ contextLost: new Signal(),
208
+
209
+ /**
210
+ * **Never fires.** Kept because the contract has it and subscribers expect the shape,
211
+ * but Shade does not recover a lost device (R6): everything the renderer built lived on
212
+ * it, and a message plus a reload is the honest response. A subscriber here is waiting
213
+ * for something that will not happen.
214
+ *
215
+ * @type {Signal<number>}
216
+ */
217
+ contextRestored: new Signal(),
218
+
219
+ /**
220
+ * There will be no rendering — no WebGPU, no adapter, or hardware below the floor.
221
+ * Carries a {@link ShadeDeviceFailure}, whose `message` is written to be shown.
222
+ *
223
+ * @type {Signal<ShadeDeviceFailure>}
224
+ */
225
+ contextFailed: new Signal(),
226
+ };
227
+
228
+
229
+
230
+ /**
231
+ * Final render resolution in whole device pixels. Zero until the viewport has a size.
232
+ *
233
+ * @readonly
234
+ * @type {Vector2}
235
+ */
236
+ this.output_resolution = new Vector2(0, 0);
237
+
238
+ /**
239
+ * The DOM element the canvas lives in. Created here rather than in {@link start} so that
240
+ * layout can be wired before a device exists — and so a failed start still leaves something
241
+ * to put a message in.
242
+ *
243
+ * @type {EmptyView}
244
+ */
245
+ this.viewport = new EmptyView();
246
+ this.viewport.size.onChanged.add(this.updateSize, this);
247
+
248
+ /**
249
+ * @type {HTMLCanvasElement|null}
250
+ */
251
+ this.domElement = null;
252
+
253
+ /**
254
+ * Whether the next frame has anything new to show. With {@link autoDraw} off, this is what
255
+ * gates rendering at all.
256
+ *
257
+ * @type {boolean}
258
+ */
259
+ this.needDraw = true;
260
+
261
+ /**
262
+ * Draw every tick regardless of {@link needDraw}. On-demand rendering — menus and the
263
+ * editor that should not burn frames — is turning this off.
264
+ *
265
+ * @type {boolean}
266
+ */
267
+ this.autoDraw = true;
268
+
269
+ /**
270
+ * @type {number}
271
+ */
272
+ this.frameIndex = 0;
273
+
274
+ /*
275
+ Wired here rather than in `start`, because the closures read the field and the field is
276
+ re-assigned on every start: a controller that captured one renderer would keep steering it
277
+ after a context restart replaced it.
278
+ */
279
+ this.#dynamic_resolution.get_scale = () => this.#renderer.internal_resolution_scale;
280
+ this.#dynamic_resolution.set_scale = v => {
281
+ this.#renderer.internal_resolution_scale = v;
282
+ };
283
+
284
+ /*
285
+ Stated rather than inherited. It happens to equal the controller's own default today, and
286
+ that is not a reason to leave it unsaid: the game's target is the game's decision (D39), and
287
+ a library default is free to move without anybody meaning the game to follow it.
288
+ */
289
+ this.#dynamic_resolution.target_frame_rate = TARGET_FRAME_RATE;
290
+ }
291
+
292
+ /**
293
+ * Trades internal resolution for frame time, and is the only thing in this engine that does.
294
+ *
295
+ * Exposed so that it can be turned off or re-targeted — a measurement that wants a fixed
296
+ * resolution, or a settings screen that offers the choice — and for no other reason: it drives
297
+ * itself from {@link render}, and nothing has to call it.
298
+ *
299
+ * **It targets {@link TARGET_FRAME_RATE}**, which makes it a floor-holder rather than a
300
+ * frame-rate governor: it does nothing at all until a frame costs more than 33 ms, and on
301
+ * hardware that clears the budget it sits at full resolution and never engages.
302
+ *
303
+ * @returns {DynamicResolutionScaling}
304
+ */
305
+ get dynamic_resolution() {
306
+ return this.#dynamic_resolution;
307
+ }
308
+
309
+ /**
310
+ * Draw this scene from now on.
311
+ *
312
+ * The narrowest seam that works: the facade is told what to draw, rather than handing out a
313
+ * graph for anyone to write into. Shade's own `render` takes the scene as an argument, so this
314
+ * matches how the renderer underneath already thinks about it.
315
+ *
316
+ * @param {Scene} scene
317
+ */
318
+ set_scene(scene) {
319
+ assert.defined(scene, 'scene');
320
+ assert.equal(scene.isScene, true, 'scene.isScene !== true');
321
+
322
+ this.#scene = scene;
323
+
324
+ /*
325
+ Shade lights with indirect by default and a scene without an environment renders unlit, so
326
+ the engine supplies one rather than leaving every caller to. Turning global illumination off
327
+ is the thing that takes a decision, not turning it on.
328
+
329
+ A scene that arrives with an environment keeps it: the scene owns its lighting and this only
330
+ fills in what is missing. Otherwise handing over a scene would silently discard the
331
+ environment it was built with, and the fix would be to call things in a particular order.
332
+ */
333
+ if (scene.lights.environment === undefined) {
334
+ scene.lights.environment = make_default_environment();
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Set the scene's image-based-lighting source, replacing whatever it was lighting with.
340
+ *
341
+ * No prefiltering happens here, unlike the equivalent on the old engine: Shade convolves the map
342
+ * into its irradiance and roughness mips on the GPU, notices when the texture it was given has
343
+ * been swapped, and redoes that work itself. So this is an assignment and the renderer catches up
344
+ * on the next frame — safe before startup as well as after.
345
+ *
346
+ * The texture must be **octahedral**, which is the projection Shade samples the environment with.
347
+ * `load_environment_map` and `resample_equirectangular_to_octahedral` are the ways in; a cube or
348
+ * equirectangular texture handed over directly would sample as garbage rather than fail.
349
+ *
350
+ * There is deliberately no `set_environment_texture` counterpart. On the old engine that meant
351
+ * "and show it as the skybox too", and Shade has no background of its own to show it on — the
352
+ * visible sky is its own piece of work rather than a second line here.
353
+ *
354
+ * @param {ShadeTexture} texture octahedral-encoded spherical radiance
355
+ */
356
+ set_environment_map(texture) {
357
+ assert.defined(texture, 'texture');
358
+ assert.equal(texture.isShadeTexture, true, 'texture.isShadeTexture !== true');
359
+
360
+ this.#scene.lights.environment = texture;
361
+
362
+ this.needDraw = true;
363
+ }
364
+
365
+ /**
366
+ * Record work into the frame at a named point in it — §4.8's extensibility surface, and the
367
+ * honest replacement for the old engine's `preRender` / `buffersRendered` / `postRender`
368
+ * signals, each of which maps to a phase.
369
+ *
370
+ * The extension is handed a `FrameContext` when its phase comes round and records with the
371
+ * `graph_*` helpers, exactly as the built-in passes do. Resource handles come from the frame's
372
+ * records rather than from a renderer: the frame graph is what knows which resources a pass
373
+ * touched, and work recorded around it cannot be ordered or aliased against the rest of the
374
+ * frame.
375
+ *
376
+ * **Registering does not require a device.** Extensions are held here and applied to the
377
+ * renderer when one exists, and again after a restart — a system registers at startup without
378
+ * knowing where in the lifecycle it is.
379
+ *
380
+ * @param {RenderExtension} extension
381
+ * @returns {RenderExtension} `extension`, for handing back to {@link remove_extension}
382
+ */
383
+ add_extension(extension) {
384
+ assert.defined(extension, 'extension');
385
+ assert.isFunction(extension.record, 'extension.record');
386
+
387
+ this.#extensions.push(extension);
388
+
389
+ if (this.#renderer !== null) {
390
+ this.#renderer.add_extension(extension);
391
+ }
392
+
393
+ return extension;
394
+ }
395
+
396
+ /**
397
+ * How many extensions are registered against a phase.
398
+ *
399
+ * Read-only, and here because a system that registers one otherwise has no way to say that it
400
+ * did: the extension is private to the system that owns it, and "start → stop → start leaks
401
+ * nothing" is a property a gate has to be able to state. It says how many, not what they are,
402
+ * and hands out nothing.
403
+ *
404
+ * @param {FramePhase} phase
405
+ * @returns {number}
406
+ */
407
+ extension_count(phase) {
408
+ assert.isNonNegativeInteger(phase, 'phase');
409
+
410
+ let count = 0;
411
+
412
+ for (let i = 0; i < this.#extensions.length; i++) {
413
+ if (this.#extensions[i].phase === phase) {
414
+ count++;
415
+ }
416
+ }
417
+
418
+ return count;
419
+ }
420
+
421
+ /**
422
+ * Take an extension back out. Identity is the object, so the phase does not have to be
423
+ * repeated.
424
+ *
425
+ * @param {RenderExtension} extension
426
+ * @returns {boolean} whether it was registered
427
+ */
428
+ remove_extension(extension) {
429
+ const index = this.#extensions.indexOf(extension);
430
+
431
+ if (index === -1) {
432
+ return false;
433
+ }
434
+
435
+ this.#extensions.splice(index, 1);
436
+
437
+ if (this.#renderer !== null) {
438
+ this.#renderer.remove_extension(extension);
439
+ }
440
+
441
+ return true;
442
+ }
443
+
444
+ /**
445
+ * Shade's GPU-side state for a scene the caller already owns — and with it the managers that
446
+ * hang off it, of which the animation manager is the one Phase 4 needs.
447
+ *
448
+ * **The one thing D14 lets through, and D29 is why** (E13). The alternative was a system
449
+ * registering a frame pass that draws nothing, purely to be handed `context.view.scene` when
450
+ * its phase came round — which would have made the animation system claim a moment in drawing
451
+ * for work that is not drawing. A named method costs one line of surface and says what it is.
452
+ *
453
+ * **This still hands out no graph.** `scene` is an argument, not a getter: only the system that
454
+ * owns a scene can ask about it, so the coupling §2.1 retires — the facade handing its graph to
455
+ * anyone who asks — is not what this is. What comes back is the renderer's own bookkeeping for
456
+ * that scene, which is Shade's to shape and no one else's.
457
+ *
458
+ * @param {Scene} scene
459
+ * @returns {GPUSceneContext|null} `null` while there is no device — before a successful
460
+ * {@link start} and again after {@link stop}. A restart builds a **new** context, so a caller
461
+ * holding ids the old one issued has to notice the object changed and register again.
462
+ */
463
+ scene_context(scene) {
464
+ assert.defined(scene, 'scene');
465
+ assert.equal(scene.isScene, true, 'scene.isScene !== true');
466
+
467
+ const renderer = this.#renderer;
468
+
469
+ if (renderer === null) {
470
+ return null;
471
+ }
472
+
473
+ return renderer.scenes.obtain(scene);
474
+ }
475
+
476
+ /**
477
+ * Draw a scene of somebody else's into a texture of that size, and hand back its pixels.
478
+ *
479
+ * **Tooling only.** This is a GPU readback and I-4 forbids those on any gameplay-affecting path;
480
+ * what it is here for is pictures of things — an asset library's icons, a thumbnail, a capture.
481
+ *
482
+ * **No renderer and no device leave the facade** (D14). What the caller brings is a scene and a
483
+ * camera, both of which it built; what it gets back is a `Sampler2D`. The frame itself goes
484
+ * through `Renderer.render_to_target` (D50), so the picture is drawn by the renderer that is
485
+ * already running, with the geometry and the textures it already holds resident.
486
+ *
487
+ * **The renderer is resized for the call and put back afterwards.** Everything a frame is built
488
+ * out of is sized from the output resolution, so a 64 x 64 picture means a 64 x 64 frame; the
489
+ * temporal history that costs is dropped either way by the resize.
490
+ *
491
+ * @param {Scene} scene
492
+ * @param {Camera} camera
493
+ * @param {number} width texels
494
+ * @param {number} height texels
495
+ * @returns {Promise<Sampler2D>} RGBA, one byte a channel
496
+ * @throws {Error} when there is no device — before a successful {@link start}, or after
497
+ * {@link stop}
498
+ */
499
+ async render_to_sampler(scene, camera, width, height) {
500
+ assert.defined(scene, 'scene');
501
+ assert.defined(camera, 'camera');
502
+ assert.isNonNegativeInteger(width, 'width');
503
+ assert.isNonNegativeInteger(height, 'height');
504
+ assert.greaterThan(width, 0, 'width');
505
+ assert.greaterThan(height, 0, 'height');
506
+
507
+ const renderer = this.#renderer;
508
+
509
+ if (renderer === null) {
510
+ throw new Error('cannot render without a device — start() has not run, or stop() has');
511
+ }
512
+
513
+ const target = renderer.graphics.textures.contextFromDescriptor(TextureDescriptor.from({
514
+ label: "GraphicsEngine/offscreen",
515
+ size: [width, height, 1],
516
+ format: OFFSCREEN_FORMAT,
517
+ usage: GPUTextureUsage.RENDER_ATTACHMENT
518
+ | GPUTextureUsage.TEXTURE_BINDING
519
+ | GPUTextureUsage.COPY_SRC
520
+ }));
521
+
522
+ const pixel_ratio = renderer.pixel_ratio;
523
+ const automatic_exposure = renderer.feature_automatic_exposure_enabled;
524
+
525
+ try {
526
+ renderer.pixel_ratio = 1;
527
+ renderer.resize(width, height);
528
+
529
+ /*
530
+ A picture of one object against nothing has no scene to read an exposure off, and an
531
+ adapting one would take the frames it adapts over — so a preview is drawn at the fixed
532
+ exposure instead, which is what a studio light rig is for.
533
+ */
534
+ renderer.feature_automatic_exposure_enabled = false;
535
+
536
+ for (let i = 0; i < OFFSCREEN_FRAMES; i++) {
537
+ renderer.render_to_target(camera, scene, 1 / 60, target);
538
+ }
539
+
540
+ const data = await target.download();
541
+
542
+ return new Sampler2D(data, 4, width, height);
543
+ } finally {
544
+ target.destroy();
545
+
546
+ renderer.feature_automatic_exposure_enabled = automatic_exposure;
547
+ renderer.pixel_ratio = pixel_ratio;
548
+
549
+ this.updateSize();
550
+ }
551
+ }
552
+
553
+ /**
554
+ * Whether there is a device to render with.
555
+ *
556
+ * @returns {boolean}
557
+ */
558
+ get is_running() {
559
+ return this.#renderer !== null;
560
+ }
561
+
562
+ /**
563
+ * Stand a device up and attach its canvas to the viewport.
564
+ *
565
+ * Asynchronous because acquiring a WebGPU device is — there is no synchronous form of it — and
566
+ * because failing to get one is a normal outcome that has to be reported rather than thrown
567
+ * into a constructor. A failure raises `on.contextFailed` with a message written for a person
568
+ * and rethrows, so a caller that wants to handle it can and one that does not still fails
569
+ * loudly.
570
+ *
571
+ * @returns {Promise<void>}
572
+ */
573
+ async start() {
574
+ if (this.#renderer !== null) {
575
+ // already running; starting twice is a caller error rather than a no-op worth hiding
576
+ throw new Error('GraphicsEngine is already started');
577
+ }
578
+
579
+ const canvas = document.createElement("canvas");
580
+
581
+ canvas.classList.add("graphics-engine-render-canvas");
582
+
583
+ const style = canvas.style;
584
+
585
+ style.userSelect = style.webkitUserSelect = style.mozUserSelect = "none";
586
+ // see https://www.w3.org/TR/pointerevents/#the-touch-action-css-property
587
+ style.touchAction = "none";
588
+
589
+ this.#canvas = canvas;
590
+
591
+ const renderer = new Renderer();
592
+
593
+ try {
594
+ await renderer.initialize({ context: canvas.getContext('webgpu') });
595
+ } catch (failure) {
596
+ this.#canvas = null;
597
+
598
+ this.on.contextFailed.send1(failure);
599
+
600
+ throw failure;
601
+ }
602
+
603
+ renderer.onDeviceFailure.add(this.#handle_device_failure, this);
604
+
605
+ // Extensions outlive the device: they are registered against the engine, not against a
606
+ // renderer that a restart replaces. This is what makes a system able to register once, at
607
+ // startup, without knowing whether a device exists yet.
608
+ for (let i = 0; i < this.#extensions.length; i++) {
609
+ renderer.add_extension(this.#extensions[i]);
610
+ }
611
+
612
+ this.#renderer = renderer;
613
+ this.domElement = canvas;
614
+
615
+ this.adopt_canvas(canvas);
616
+
617
+ this.updateSize();
618
+
619
+ // so the first frame's interval is a real one rather than the whole of the boot
620
+ this.#last_render_timestamp = performance.now();
621
+ }
622
+
623
+ /**
624
+ * Re-point the viewport at a canvas, and the page with it. This is the wrinkle E1 named:
625
+ * `start` builds a *new* canvas, so a restart would otherwise leave the dead one mounted and
626
+ * the live one detached. `View.el` is read unguarded all over the view layer, so it is replaced
627
+ * rather than cleared — it always holds an element.
628
+ *
629
+ * The inline `pointer-events` of the element being replaced comes along: the engine opts the
630
+ * viewport back in (`auto`) under a game view that lets clicks fall through (`none`), and it
631
+ * does so on the placeholder element before there is a canvas. A canvas that did not inherit
632
+ * the opt-in inherited the fall-through instead, and every pointer device bound to the canvas
633
+ * — the editor's tools — was deaf to every press.
634
+ *
635
+ * Public so that the swap can be driven without a device.
636
+ *
637
+ * @param {HTMLCanvasElement} canvas
638
+ */
639
+ adopt_canvas(canvas) {
640
+ const previous = this.viewport.el;
641
+
642
+ if (previous != null && previous !== canvas && previous.style !== undefined && previous.style.pointerEvents !== "") {
643
+ canvas.style.pointerEvents = previous.style.pointerEvents;
644
+ }
645
+
646
+ this.viewport.el = canvas;
647
+
648
+ if (previous != null && previous !== canvas && previous.parentNode !== null) {
649
+ previous.parentNode.replaceChild(canvas, previous);
650
+ }
651
+ }
652
+
653
+ /**
654
+ * Tear the device down and let go of the canvas.
655
+ *
656
+ * Symmetric with {@link start}, and that symmetry is the point: the old engine started in a
657
+ * constructor and could not be restarted.
658
+ */
659
+ stop() {
660
+ const renderer = this.#renderer;
661
+
662
+ if (renderer === null) {
663
+ // never started, or stopped twice — nothing to undo
664
+ return;
665
+ }
666
+
667
+ renderer.onDeviceFailure.remove(this.#handle_device_failure, this);
668
+ renderer.destroy();
669
+
670
+ this.#renderer = null;
671
+ this.#canvas = null;
672
+ this.domElement = null;
673
+
674
+ // `viewport.el` is deliberately left pointing at the spent canvas rather than cleared: the
675
+ // view layer reads it without checking, and the next `start` swaps it for a live one.
676
+ }
677
+
678
+ /**
679
+ * @param {ShadeDeviceFailure} failure
680
+ */
681
+ #handle_device_failure(failure) {
682
+ this.on.contextLost.dispatch(failure);
683
+ }
684
+
685
+ /**
686
+ * A pointer position in viewport pixels, as a clip-space point.
687
+ *
688
+ * Unchanged from the old engine, down to writing into a caller's vector: it is arithmetic on the
689
+ * viewport size and has nothing to do with which renderer is underneath.
690
+ *
691
+ * @param {Vector2|Vector3} input viewport pixels, origin top-left
692
+ * @param {Vector2|Vector3} result written with −1…1, +Y up
693
+ */
694
+ normalizeViewportPoint(input, result) {
695
+ const size = this.viewport.size;
696
+
697
+ // shifted by half a pixel, so the value names the centre of the pixel rather than its corner
698
+ result.x = ((input.x + 0.5) / size.x) * 2 - 1;
699
+ result.y = -((input.y + 0.5) / size.y) * 2 + 1;
700
+ }
701
+
702
+ /**
703
+ * A ray from the eye through a point on the viewport, in world space.
704
+ *
705
+ * Same name, same arguments, same meaning as on the old engine — and the same expectation that
706
+ * `x` and `y` are **clip-space**, which {@link normalizeViewportPoint} is what produces. What is
707
+ * behind it is Shade's camera rather than three's; see {@link shade_camera_projection_ray}.
708
+ *
709
+ * This is the only picking primitive the game actually needs. Every gameplay pick is a terrain
710
+ * raycast — an exact query against the terrain's own representation, which no part of the renderer
711
+ * is involved in — so a working ray is the whole of the renderer's contribution to it.
712
+ *
713
+ * @param {number} x clip space, −1…1
714
+ * @param {number} y clip space, −1…1, +Y up
715
+ * @param {Vector3} source written with the ray's origin
716
+ * @param {Vector3} direction written with a unit direction
717
+ */
718
+ viewportProjectionRay(x, y, source, direction) {
719
+ shade_camera_projection_ray(this.#camera, x, y, source, direction);
720
+ }
721
+
722
+ /**
723
+ * Size the drawing buffer to the viewport.
724
+ *
725
+ * Shade's `resize` takes a **CSS** size and applies the device pixel ratio itself, which is the
726
+ * opposite of what the old engine's renderer wanted. `output_resolution` is then the
727
+ * device-pixel figure, for everything that has to land on the same texel grid.
728
+ *
729
+ * **There is no engine-level `pixelRatio` any more, and that is the answer to GAP-027 rather
730
+ * than an oversight.** The field was a public `Vector1` that `updateSize` read, and nothing
731
+ * bound its `onChanged` — the constructor bound `viewport.size.onChanged` and stopped — so
732
+ * writing it changed nothing until the window happened to be resized, which on a full-screen
733
+ * game is never. The first symptom is a setting that appears dead and then works perfectly the
734
+ * moment somebody drags the window. Calling `updateSize()` by hand did not rescue it either:
735
+ * `Renderer.resize` asserts whole pixels and the product was passed unfloored, so any
736
+ * fractional ratio threw. Between the two, the property had no working use at any point in its
737
+ * life.
738
+ *
739
+ * Render resolution belongs to the renderer, which already has the hooks for it —
740
+ * `internal_resolution_scale` takes any positive number, floors internally, and is upscaled
741
+ * back by the renderer's own TAA rather than by the browser stretching a smaller canvas, which
742
+ * is the better answer on quality grounds as well. It is reachable through the `renderer`
743
+ * getter. A second, half-wired knob on this facade was only ever going to be found by someone
744
+ * who then had to discover why it did nothing.
745
+ */
746
+ updateSize() {
747
+ const size = this.viewport.size;
748
+
749
+ const css_width = max2(0, size.x);
750
+ const css_height = max2(0, size.y);
751
+
752
+ const renderer = this.#renderer;
753
+
754
+ if (renderer !== null) {
755
+ renderer.resize(css_width, css_height);
756
+ }
757
+
758
+ if (this.domElement !== null) {
759
+ this.domElement.style.width = size.x + "px";
760
+ this.domElement.style.height = size.y + "px";
761
+ }
762
+
763
+ const device_pixel_ratio = window.devicePixelRatio;
764
+
765
+ // clamped to 1 for the same reason the old engine clamped it: a collapsed viewport still
766
+ // allocates attachments, and a zero-sized one is not a legal target
767
+ this.output_resolution.set(
768
+ max2(1, Math.floor(css_width * device_pixel_ratio)),
769
+ max2(1, Math.floor(css_height * device_pixel_ratio))
770
+ );
771
+ }
772
+
773
+ /**
774
+ * Draw a frame, if there is anything to draw it with.
775
+ *
776
+ * @param {number} [time_delta_seconds] simulation seconds to advance the frame by — GPU
777
+ * particles, the animation tick and exposure adaptation all step by this. Defaults to zero, so
778
+ * a caller that just wants the current state on screen (a scene transition, a preview) draws it
779
+ * without moving anything. It deliberately does NOT default to a frame's worth of time: doing
780
+ * that silently ran the simulation at a fixed 60 Hz regardless of the real clock.
781
+ * @returns {boolean} whether a frame was submitted
782
+ */
783
+ render(time_delta_seconds = 0) {
784
+ const renderer = this.#renderer;
785
+
786
+ if (renderer === null) {
787
+ return false;
788
+ }
789
+
790
+ const viewport_size = this.viewport.size;
791
+
792
+ if (viewport_size.x <= 0 || viewport_size.y <= 0) {
793
+ /*
794
+ There is no surface to draw into. Every boot passes through this — the renderer starts
795
+ before the game view is laid out — and so does any collapsed or unmounted view.
796
+ Drawing anyway is worse than drawing nothing: `output_resolution` and the renderer's
797
+ own resolutions both floor at one texel, and a one-texel target is a size the frame's
798
+ mip chains cannot be built from, so what a frame produces here is a run of invalid
799
+ textures rather than a picture. `needDraw` is left set, so the next frame retries.
800
+ */
801
+ return false;
802
+ }
803
+
804
+ /*
805
+ The **refresh** interval, measured here rather than taken from `time_delta_seconds`. What
806
+ the controller steers by has to be how long a frame actually took, wall clock, including
807
+ everything outside this call; the argument is the simulation's delta, which is clamped and
808
+ is not the same quantity. Measuring between calls is also what makes this correct for any
809
+ driver — the engine's animation frame, or a probe calling `render` directly.
810
+ */
811
+ const now = performance.now();
812
+
813
+ this.#dynamic_resolution.notify_frame((now - this.#last_render_timestamp) / 1000);
814
+
815
+ this.#last_render_timestamp = now;
816
+
817
+ // Aspect follows the surface being drawn to, so it is refreshed here rather than left to
818
+ // whoever last resized: a camera describing a viewport that has since changed shape is the
819
+ // classic source of a stretched frame.
820
+ this.#camera.aspect = renderer.aspect_ratio;
821
+ this.#camera.update();
822
+
823
+ this.on.preRender.dispatch();
824
+
825
+ const submitted = renderer.render(this.#camera, this.#scene, time_delta_seconds);
826
+
827
+ this.frameIndex++;
828
+ this.needDraw = false;
829
+
830
+ this.on.postRender.dispatch();
831
+
832
+ return submitted;
833
+ }
834
+ }