@woosh/meep-engine 3.60.4 → 3.60.6

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 (419) hide show
  1. package/CHANGELOG.md +2621 -0
  2. package/CONTRIBUTING.md +7 -0
  3. package/MIGRATION_2_TO_3.md +136 -0
  4. package/SECURITY.md +4 -0
  5. package/editor/EditorCameraNavigation.d.ts +2 -1
  6. package/editor/EditorCameraNavigation.d.ts.map +1 -1
  7. package/editor/EditorCameraNavigation.js +51 -57
  8. package/package.json +6 -2
  9. package/src/engine/EngineHarness.d.ts +3 -9
  10. package/src/engine/EngineHarness.d.ts.map +1 -1
  11. package/src/engine/EngineHarness.js +6 -8
  12. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts +9 -6
  13. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts.map +1 -1
  14. package/src/engine/graphics/camera/makeOrbitalCameraController.js +44 -71
  15. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts +64 -0
  16. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts.map +1 -0
  17. package/src/engine/graphics/camera/orbital/MomentumAxis.js +187 -0
  18. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts +86 -0
  19. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts.map +1 -0
  20. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.js +319 -0
  21. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts +34 -10
  22. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts.map +1 -1
  23. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +116 -30
  24. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts +1 -7
  25. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
  26. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +1 -37
  27. package/src/engine/graphics/ecs/water/Water.d.ts +8 -0
  28. package/src/engine/graphics/ecs/water/Water.d.ts.map +1 -1
  29. package/src/engine/graphics/ecs/water/Water.js +8 -0
  30. package/src/engine/graphics3/WaterSystem.d.ts.map +1 -1
  31. package/src/engine/graphics3/WaterSystem.js +731 -734
  32. package/src/engine/graphics3/decal/DecalTileShader.d.ts +61 -0
  33. package/src/engine/graphics3/decal/DecalTileShader.d.ts.map +1 -0
  34. package/src/engine/graphics3/decal/DecalTileShader.js +138 -0
  35. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts +10 -4
  36. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts.map +1 -1
  37. package/src/engine/graphics3/decal/GPUDecalRenderer.js +149 -44
  38. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts +25 -0
  39. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts.map +1 -0
  40. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.js +55 -0
  41. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts +22 -0
  42. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts.map +1 -0
  43. package/src/engine/graphics3/decal/graph_gather_decal_tiles.js +70 -0
  44. package/src/engine/graphics3/decal/shader_decal_composite.d.ts +7 -5
  45. package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
  46. package/src/engine/graphics3/decal/shader_decal_composite.js +48 -44
  47. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts +12 -0
  48. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts.map +1 -0
  49. package/src/engine/graphics3/decal/shader_decal_tile_copy.js +78 -0
  50. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts +11 -0
  51. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts.map +1 -0
  52. package/src/engine/graphics3/decal/shader_gather_decal_tiles.js +80 -0
  53. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts +13 -0
  54. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts.map +1 -0
  55. package/src/engine/input/devices/mouse/MouseEventButtons.js +23 -0
  56. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts +4 -0
  57. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts.map +1 -1
  58. package/src/engine/input/ecs/controllers/KeyboardCameraController.js +9 -45
  59. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  60. package/src/shade/playground/water_refraction/README.md +5 -6
  61. package/src/shade/playground/water_refraction/index.html +1 -1
  62. package/src/shade/renderer/Renderer.d.ts +11 -0
  63. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  64. package/src/shade/renderer/Renderer.js +50 -6
  65. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
  66. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
  67. package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
  68. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  69. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  70. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  71. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  72. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  73. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +28 -45
  74. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts.map +1 -1
  75. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +111 -119
  76. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +22 -3
  77. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
  78. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +84 -3
  79. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts +19 -0
  80. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts.map +1 -0
  81. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.js +25 -0
  82. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
  83. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
  84. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
  85. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts +15 -0
  86. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts.map +1 -1
  87. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.js +38 -0
  88. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
  89. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
  90. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
  91. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
  92. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
  93. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts +28 -59
  94. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts.map +1 -1
  95. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.js +84 -789
  96. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
  97. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +1 -8
  98. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +7 -6
  99. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
  100. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +12 -11
  101. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.d.ts +2 -2
  102. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.js +2 -2
  103. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +1 -4
  104. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
  105. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +1 -10
  106. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
  107. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
  108. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
  109. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
  110. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
  111. package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
  112. package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
  113. package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
  114. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
  115. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
  116. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
  117. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
  118. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
  119. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
  120. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
  121. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  122. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
  123. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
  124. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
  125. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
  126. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
  127. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
  128. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
  129. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
  130. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
  131. package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
  132. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
  133. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  134. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
  135. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
  136. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
  137. package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
  138. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  139. package/src/shade/renderer/material/GPUMaterialManager.d.ts +11 -1
  140. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  141. package/src/shade/renderer/material/GPUMaterialManager.js +16 -8
  142. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
  143. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  144. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
  145. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts +7 -3
  146. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts.map +1 -1
  147. package/src/shade/renderer/postprocess/gtao/GTAO.js +26 -13
  148. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts +14 -0
  149. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts.map +1 -1
  150. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.js +196 -68
  151. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.d.ts.map +1 -1
  152. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.js +42 -28
  153. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.d.ts.map +1 -1
  154. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.js +429 -414
  155. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts +16 -0
  156. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts.map +1 -0
  157. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.js +156 -0
  158. package/src/shade/renderer/scene/GPUSceneContext.d.ts +2 -2
  159. package/src/shade/renderer/scene/GPUSceneContext.js +5 -5
  160. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  161. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
  162. package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
  163. package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
  164. package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
  165. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
  166. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
  167. package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
  168. package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
  169. package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
  170. package/src/shade/renderer/water/shader_water.js +22 -13
  171. package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
  172. package/src/shade/renderer/water/shader_water_avboit.js +7 -7
  173. package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
  174. package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
  175. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
  176. package/editor/REWORK_PLAN_2026_08.md +0 -692
  177. package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
  178. package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
  179. package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
  180. package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
  181. package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
  182. package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
  183. package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
  184. package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
  185. package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
  186. package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
  187. package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
  188. package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
  189. package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
  190. package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
  191. package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
  192. package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
  193. package/src/REVIEW_2026_08_06/adv-core.md +0 -511
  194. package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
  195. package/src/REVIEW_2026_08_06/adv-view.md +0 -446
  196. package/src/REVIEW_2026_08_06/api-core.md +0 -296
  197. package/src/REVIEW_2026_08_06/api-engine.md +0 -195
  198. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
  199. package/src/REVIEW_2026_08_06/core-binary.md +0 -348
  200. package/src/REVIEW_2026_08_06/core-collection.md +0 -376
  201. package/src/REVIEW_2026_08_06/core-math.md +0 -221
  202. package/src/REVIEW_2026_08_06/core-process.md +0 -266
  203. package/src/REVIEW_2026_08_06/core-science.md +0 -223
  204. package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
  205. package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
  206. package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
  207. package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
  208. package/src/REVIEW_2026_08_06/engine-input.md +0 -197
  209. package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
  210. package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
  211. package/src/REVIEW_2026_08_06/generation.md +0 -176
  212. package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
  213. package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
  214. package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
  215. package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
  216. package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
  217. package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
  218. package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
  219. package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
  220. package/src/REVIEW_2026_08_06/view.md +0 -667
  221. package/src/REVIEW_2026_08_06.md +0 -610
  222. package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
  223. package/src/core/bvh2/bvh3/NOTES.md +0 -2
  224. package/src/core/bvh8/NOTES.md +0 -63
  225. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
  226. package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
  227. package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
  228. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
  229. package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
  230. package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
  231. package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
  232. package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
  233. package/src/core/geom/3d/gjk/NOTES.md +0 -8
  234. package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
  235. package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
  236. package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
  237. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
  238. package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
  239. package/src/engine/REVIEW_2026_07_03.md +0 -211
  240. package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
  241. package/src/engine/control/first-person/DESIGN.md +0 -658
  242. package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
  243. package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
  244. package/src/engine/control/first-person/TODO.md +0 -160
  245. package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
  246. package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
  247. package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
  248. package/src/engine/ecs/ik/PLAN.md +0 -179
  249. package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
  250. package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
  251. package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
  252. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
  253. package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
  254. package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
  255. package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
  256. package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
  257. package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
  258. package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
  259. package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
  260. package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
  261. package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
  262. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
  263. package/src/engine/network/CONGESTION_CONTROL.md +0 -87
  264. package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
  265. package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
  266. package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
  267. package/src/engine/physics/cloth/PLAN.md +0 -3056
  268. package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
  269. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
  270. package/src/engine/physics/fluid/solver/NOTES.md +0 -46
  271. package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
  272. package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
  273. package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
  274. package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
  275. package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
  276. package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
  277. package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
  278. package/src/format/image/avif/DECISIONS.md +0 -545
  279. package/src/format/image/jpeg/NOTES.md +0 -3
  280. package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
  281. package/src/generation/COORDINATES.md +0 -54
  282. package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
  283. package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
  284. package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
  285. package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
  286. package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
  287. package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
  288. package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
  289. package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
  290. package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
  291. package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
  292. package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
  293. package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
  294. package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
  295. package/src/shade/RENDERER_CONTRACT.md +0 -697
  296. package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
  297. package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
  298. package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
  299. package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
  300. package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
  301. package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
  302. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
  303. package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
  304. package/src/shade/renderer/NOTES.md +0 -78
  305. package/src/shade/renderer/PLAN.md +0 -8
  306. package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
  307. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
  308. package/src/shade/renderer/atmosphere/NOTES.md +0 -6
  309. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
  310. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
  311. package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
  312. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
  313. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
  314. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
  315. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
  316. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
  317. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
  318. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
  319. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
  320. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
  321. package/src/shade/renderer/camera/orbital/README.md +0 -81
  322. package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
  323. package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
  324. package/src/shade/renderer/camera/orbital/index.js +0 -9
  325. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
  326. package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
  327. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
  328. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
  329. package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
  330. package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
  331. package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
  332. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
  333. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
  334. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -481
  335. package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
  336. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
  337. package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
  338. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
  339. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
  340. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
  341. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
  342. package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -529
  343. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts +0 -47
  344. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts.map +0 -1
  345. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.js +0 -80
  346. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts +0 -16
  347. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts.map +0 -1
  348. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.js +0 -67
  349. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts +0 -17
  350. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts.map +0 -1
  351. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.js +0 -145
  352. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts +0 -12
  353. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts.map +0 -1
  354. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.js +0 -105
  355. package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
  356. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4306
  357. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
  358. package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
  359. package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
  360. package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
  361. package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
  362. package/src/shade/renderer/hiz/NOTES.md +0 -3
  363. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
  364. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
  365. package/src/shade/renderer/light/cluster/NOTES.md +0 -11
  366. package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
  367. package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
  368. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
  369. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
  370. package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
  371. package/src/shade/renderer/particles/DESIGN.md +0 -1011
  372. package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -280
  373. package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
  374. package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
  375. package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
  376. package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
  377. package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
  378. package/src/shade/renderer/postprocess/nss/NOTES.md +0 -332
  379. package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
  380. package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
  381. package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
  382. package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
  383. package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
  384. package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
  385. package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
  386. package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
  387. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
  388. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
  389. package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
  390. package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
  391. package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -391
  392. package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -153
  393. package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
  394. package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
  395. package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
  396. package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
  397. package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
  398. package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
  399. package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
  400. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
  401. package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
  402. package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -927
  403. package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
  404. package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
  405. package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
  406. package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
  407. package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
  408. package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
  409. package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
  410. package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
  411. package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
  412. package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
  413. package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -3
  414. package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
  415. package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
  416. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
  417. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
  418. package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
  419. package/src/shade/wgsl/NOTES.md +0 -4
@@ -1,658 +0,0 @@
1
- # FirstPersonPlayerController — Design
2
-
3
- > **Status: shipped.** `FirstPersonPlayerController.js`, its system, its serialization adapter and
4
- > its specs all exist; this document is the design they were built from, kept as the rationale
5
- > rather than as a plan. The line that used to sit here said "No implementation yet" and was still
6
- > saying it above ~3,700 lines of working code.
7
- >
8
- > **What this controller is FOR, which is the thing to read before adopting it.** It is an
9
- > opinionated, feel-first, tunable controller: §1's two goals are "feel alive" and "be
10
- > configurable", and it cites Mirror's Edge Catalyst, Star Citizen stance/breathing and
11
- > critically-damped spring cameras. **If you arrive with a movement model of your own — a
12
- > Quake-lineage `pmove`, a physics-driven platformer, a rollback netcode design with its own step
13
- > function — this is not a substrate for it and is not trying to be.** Configurability cannot
14
- > express "this specific set of floating-point operations, exactly", and a fixed movement model is
15
- > usually about exactly that. Bring your own controller and use {@link KinematicMover} underneath
16
- > it, which is controller-agnostic and is what the collision half of this directory actually is.
17
- >
18
- > A port that pointed a Quake III movement model at this concluded the same thing correctly, then
19
- > treated the whole directory as decided and re-implemented the mover's standoff and depenetration
20
- > from the raw query layer — about six hours and two wrong fixes (GAP-009, GAP-021). Hence this
21
- > paragraph, and hence the note at the top of DESIGN_COLLISION.md.
22
- >
23
- > Scope: one entity, one body, one camera. No skeleton, no avatar mesh, no sound, no input wiring.
24
-
25
- ---
26
-
27
- ## 1. Goals
28
-
29
- The controller has two intertwined goals. Every design decision below should be justified against at least one of them.
30
-
31
- 1. **Feel alive.** Convey weight, inertia, breath, exertion, and consequence. The player should sense a body even when the screen shows only the world.
32
- 2. **Be configurable.** Every parameter that contributes to "feel" must be exposed and tunable per-entity, with sensible defaults. Different characters (heavy soldier vs. light scout vs. drunk NPC) must be expressible by data alone.
33
-
34
- ### Non-goals (explicitly out of scope)
35
-
36
- - Visual representation of the body (arms, weapon, third-person mesh, shadow).
37
- - Input wiring (keyboard, gamepad, touch). The controller exposes an **intent surface**; binding is the job of `InputController` or a script.
38
- - Collision and ground-truth physics resolution. This document assumes a separate physics or capsule-controller layer reports `grounded`, `groundNormal`, and resolves penetration. The controller writes desired velocity / impulses.
39
- - Skeletal pose evaluation. The controller exports **pose channels** (numeric outputs) intended for a future skeleton system to consume.
40
- - Sound. The controller exports **events** intended for a future audio system to consume.
41
- - Networking / rollback. The component is designed to be deterministic per fixed update so this is possible later, but no replication is included.
42
-
43
- ---
44
-
45
- ## 2. Research notes
46
-
47
- A condensed reading of public sources informed this design:
48
-
49
- - **3Cs framework** (Camera, Character, Controls) — these three are interdependent; tuning one warps the others. Treat them as one design surface even though they live in separate components. Joel Nilsson (Massive, *Avatar*) and the gamedevpills writeup are the canonical references.
50
- - **Mirror's Edge Catalyst camera** (DICE / Erik Söderholm) — the hardest problem is "not feeling like a box that glides around". Solution: animate the camera with body weight, not just track a capsule. Inform jump anticipation, land recovery, breathing, head tilt with the same data the body uses.
51
- - **Star Citizen FPS stances & breathing** — breathing is an *active subsystem*, not decoration. Breath rate scales with stamina/exertion. Camera sway has 1s rest pauses at full stamina and shrinks to ~0.2s pauses when depleted. Heavy gear amplifies it.
52
- - **Playtank "First-Person 3Cs: Camera"** — a layered transform hierarchy (Root → Head → Noise → Camera) lets effects compose without coupling. Anti-motion-sickness: keep horizon stable by default, make roll/shake opt-in.
53
- - **NeoFPS additive transforms** — effects are an additive stack; translation and rotation compose independently so layer order doesn't matter for one but does for the other.
54
- - **Spring-roll-call** (Daniel Holden) — critically damped springs with half-life parameterization beat lerp for camera smoothing. Under-damped springs are the right tool for landing recovery (they overshoot and ring slightly, which reads as compression).
55
- - **Coyote time + jump buffer + variable jump height** — three small forgiveness tricks that single-handedly elevate a "stiff" controller. Coyote ~80–120 ms, buffer ~80–150 ms, fall gravity multiplier 1.5–2.5×.
56
- - **Gait research** — a walk/run cycle is two footfalls per stride; cadence scales roughly with √(g·L) where L is leg length (the pendulum approximation). Heel-strike events happen at the bottom of the vertical bob.
57
-
58
- ---
59
-
60
- ## 3. Architectural overview
61
-
62
- ```
63
- ┌────────────────────────────────────────────────────────────────────┐
64
- │ ENTITY │
65
- │ │
66
- │ ┌──────────────────────┐ ┌──────────────┐ ┌────────────────┐ │
67
- │ │ FirstPersonPlayer- │ │ Transform │ │ Motion │ │
68
- │ │ Controller │ │ (BODY) │ │ (velocity) │ │
69
- │ │ │ │ │ │ │ │
70
- │ │ - config │ │ │ │ │ │
71
- │ │ - intent (RW) │ └──────────────┘ └────────────────┘ │
72
- │ │ - state (RO) │ ▲ │
73
- │ │ - signals (RO) │ │ written by System │
74
- │ │ - pose channels (RO) │ │ │
75
- │ └──────────────────────┘ │ │
76
- │ ▲ │ │
77
- │ │ ┌─────────┴────────┐ │
78
- │ │ │ Camera entity │ │
79
- │ │ │ (child or │ │
80
- │ │ │ referenced) │ │
81
- │ │ │ - Transform (EYE)│ │
82
- │ │ │ - Camera │ │
83
- │ │ └──────────────────┘ │
84
- │ │ │
85
- │ └── intent set by: InputController binding / AI script │
86
- │ pose channels read by: future skeleton system │
87
- │ signals consumed by: future audio / FX systems │
88
- └────────────────────────────────────────────────────────────────────┘
89
- ```
90
-
91
- ### Two transforms, not one
92
-
93
- The controller drives **two** transforms:
94
-
95
- 1. **Body transform** — the entity's own `Transform`. Position is the ground-projected foot/capsule origin. Yaw is the body's facing. Pitch and roll on the body are always zero. The body is what a physics system would integrate.
96
- 2. **Eye transform** — a separate transform (camera entity, attached via `TransformAttachment` or held as a referenced entity). Position is body + (height − crouchDip) + (bob + breath + landKick + shake). Rotation is body-yaw × eye-pitch × (lean roll). This is where all the "feel" lives.
97
-
98
- Why split: body kinematics must remain physics-clean (no jitter into the collider); the camera must be free to lie, dip, and oscillate without affecting collision or AI line-of-sight.
99
-
100
- ### Five processing layers
101
-
102
- Each frame the system walks these in order:
103
-
104
- | Layer | Reads | Writes | Cadence |
105
- |---|---|---|---|
106
- | **L0 Intent** | (external) | `intent.*` | event-driven |
107
- | **L1 Locomotion** | `intent`, `state.grounded` | `Motion.velocity`, `Transform` (body) | fixed |
108
- | **L2 Pose** | `state`, `config` | `pose.*` channels | fixed |
109
- | **L3 Camera** | `pose`, `state` | eye `Transform`, `Camera.fov` | variable (per-frame) |
110
- | **L4 Events** | derived from `state` transitions | `signals.*` | fixed (where possible) |
111
-
112
- L1, L2, L4 run in `fixedUpdate` so they're deterministic and not framerate-coupled. L3 runs in `update` because camera smoothing must be at render rate to avoid judder.
113
-
114
- ---
115
-
116
- ## 4. The component: `FirstPersonPlayerController`
117
-
118
- Standard meep component contract: `typeName`, `toJSON/fromJSON`, `copy/clone/equals/hash`. Fields are grouped into five sub-objects so the public API stays legible.
119
-
120
- ```
121
- class FirstPersonPlayerController {
122
- static typeName = "FirstPersonPlayerController"
123
-
124
- config : FirstPersonConfig // tunables — serialized
125
- intent : FirstPersonIntent // written by input/AI — transient
126
- state : FirstPersonState // read-only outputs — transient
127
- pose : FirstPersonPose // read-only outputs for skeleton — transient
128
- signals : FirstPersonSignals // Signal<...> instances — transient
129
-
130
- /** Reference to the eye/camera entity; created on link if absent. */
131
- eyeEntity : EntityReference
132
- }
133
- ```
134
-
135
- Only `config` is serialized. Everything else is rebuilt on `link()`.
136
-
137
- ### 4.1 `FirstPersonConfig` — the tunable surface
138
-
139
- Grouped by feel-axis so designers can browse without scrolling past 80 floats.
140
-
141
- ```
142
- config = {
143
- body: {
144
- height : 1.80 // m, standing eye height
145
- crouchHeight : 1.10 // m, crouched eye height
146
- radius : 0.35 // m, capsule radius (for downstream physics)
147
- mass : 80 // kg, drives bob amplitude & land impact
148
- },
149
-
150
- motion: {
151
- walkSpeed : 3.0 // m/s
152
- sprintSpeed : 6.0 // m/s
153
- crouchSpeed : 1.4 // m/s
154
- airControl : 0.25 // [0..1] how much intent influences air velocity
155
- groundAccel : 35 // m/s² (~ "snap" of start-up)
156
- groundDecel : 50 // m/s² when intent zero
157
- airAccel : 8 // m/s²
158
- turnRateMax : Inf // rad/s — Inf = instantaneous (mouse-look default)
159
- },
160
-
161
- jump: {
162
- peakHeight : 1.2 // m, the *desired* clean-jump apex
163
- timeToApex : 0.40 // s — together with peakHeight → gravity & impulse
164
- coyoteTime : 0.10 // s of grace after walking off a ledge
165
- bufferTime : 0.12 // s a queued jump remains valid before landing
166
- fallGravityMult : 2.0 // gravity ×N once vy < 0 or jump released early
167
- cutGravityMult : 1.8 // gravity ×N if jump released before apex (variable height)
168
- anticipation: { // squash before take-off
169
- duration : 0.08 // s
170
- dipAmount : 0.06 // m, eye dips this much
171
- curve : "ease-out-quad"
172
- }
173
- },
174
-
175
- landing: {
176
- softThreshold : 3.0 // m/s vertical — below = soft signal
177
- hardThreshold : 7.5 // m/s vertical — above = hard signal
178
- recovery: {
179
- dipPerVy : 0.012 // m of eye dip per m/s impact velocity
180
- dipMax : 0.18 // m, clamp
181
- spring: {
182
- halfLife : 0.12 // s — under-damped (zeta < 1) for the ring
183
- zeta : 0.55
184
- }
185
- }
186
- },
187
-
188
- crouch: {
189
- transitionTime : 0.18 // s to lerp eye height
190
- mode : "hold" // "hold" | "toggle" — affects intent semantics
191
- },
192
-
193
- bob: {
194
- // Cadence is derived from speed: freq = stepFreqAtWalk * (speed / walkSpeed) ^ stepFreqExp
195
- stepFreqAtWalk : 1.8 // Hz (≈ 1.8 footfalls/s, both feet)
196
- stepFreqExp : 0.9 // sub-linear so sprint isn't frantic
197
- verticalAmpAtWalk: 0.045 // m
198
- lateralAmpAtWalk : 0.025 // m — figure-8 (vert is 2x lateral freq)
199
- ampMassScale : 0.005 // m per kg over reference (80kg)
200
- rollAtWalkDeg : 0.6 // deg, sympathetic head roll
201
- // Pattern: vertical(t) = A_v * |sin(πft)| (footfall = trough)
202
- // lateral (t) = A_l * sin(2πft / 2) (one full cycle = 2 steps)
203
- // roll (t) = A_r * sin(2πft / 2) (in phase with lateral)
204
- },
205
-
206
- breath: {
207
- // Resting respiration — 14 breaths/min ≈ 0.23 Hz
208
- rateRestHz : 0.23
209
- rateMaxHz : 0.70 // panting at full exertion
210
- amplitudeRestM : 0.004 // tiny — barely visible at rest
211
- amplitudeMaxM : 0.022 // visible chest heave when exhausted
212
- pitchAmpRestDeg : 0.10
213
- pitchAmpMaxDeg : 0.55
214
- // Optional micro-noise on top of the sine, perlin@0.7Hz, ±20% of amp
215
- noiseAmount : 0.20
216
- },
217
-
218
- exertion: {
219
- // Single scalar [0..1]. Inputs raise it, time decays it.
220
- sprintRiseRate : 0.20 // /s while sprinting
221
- jumpRise : 0.08 // per jump
222
- idleDecayRate : 0.12 // /s while walking or still
223
- // Carry-over: after exertion peaks, breath rate decays slower than amplitude
224
- // (you keep breathing hard for a moment after stopping).
225
- rateDecayHalfLife: 4.0 // s
226
- ampDecayHalfLife : 2.0 // s
227
- },
228
-
229
- lean: {
230
- enabled : true // ACCESSIBILITY — make false to disable roll
231
- maxRollDeg : 2.5 // deg per (lateral m/s² / 9.81)
232
- spring: { halfLife: 0.18, zeta: 1.0 }
233
- },
234
-
235
- fov: {
236
- base : 70 // deg
237
- sprintAdd : 6 // deg added when at full sprint speed
238
- crouchAdd : -3
239
- smoothHalfLife : 0.20 // s
240
- },
241
-
242
- look: {
243
- pitchMinDeg : -85
244
- pitchMaxDeg : 85
245
- invertY : false
246
- // Sensitivity itself belongs to the input layer, not here.
247
- }
248
- }
249
- ```
250
-
251
- Every nested object is itself a normal object that round-trips through `toJSON`. Serialization adapter just walks the tree.
252
-
253
- ### 4.2 `FirstPersonIntent` — the control surface
254
-
255
- This is what input bindings or AI scripts write to. The system reads it once per fixed update and zeroes the *consumable* bits (`look`).
256
-
257
- ```
258
- intent = {
259
- move : Vector2 // [-1..1, -1..1] — forward+/right+ in body-local (state)
260
- look : Vector2 // accumulated delta yaw/pitch in radians (consumed each fixed step)
261
- jump : boolean // HOLD STATE — true while button held; edge detected inside system
262
- crouch : boolean // HOLD STATE — meaning depends on config.crouch.mode
263
- sprint : boolean // HOLD STATE — sprint applies only when moveY > 0 and grounded
264
- }
265
- ```
266
-
267
- Three input semantics live here:
268
- - **Delta**: `look` — accumulated between fixed updates, zeroed on consume.
269
- - **State (hold)**: `move`, `crouch`, `sprint`, `jump` — latched. Always reflects the current physical button/stick state.
270
- - **Edge**: detected internally by the system from `jump` (and from `crouch` when `config.crouch.mode === "toggle"`). The intent surface never exposes "I just pressed it" — that's the system's bookkeeping. This is what guarantees a held jump button doesn't auto-repeat.
271
-
272
- That makes all three common input topologies trivial to wire:
273
- - Mouse + keyboard: mouse writes `look += delta`, WASD writes `move`, space-down/space-up writes `jump`.
274
- - Gamepad: right stick writes `look = stick * sensitivity * dt`, left stick writes `move`, A-down/A-up writes `jump`.
275
- - AI: scripts write the same fields directly, no input device required. Scripts that want a "single jump" set `jump = true` for one fixed step then `jump = false`.
276
-
277
- ### 4.3 `FirstPersonState` — derived, read-only
278
-
279
- ```
280
- state = {
281
- // Grounding
282
- grounded : boolean
283
- timeSinceGrounded : number // s — feeds coyote time
284
- groundNormal : Vector3 // unit, set by physics layer; default Up
285
- surfaceTag : string? // optional, "grass" | "wood" | ... for FX
286
-
287
- // Locomotion
288
- speed : number // m/s, horizontal magnitude of Motion.velocity
289
- speedNormalized : number // [0..1] vs sprintSpeed — drives bob amp/freq
290
- moveMode : enum // Idle | Walk | Sprint | Crouch | Air
291
-
292
- // Vertical
293
- verticalSpeed : number // signed m/s
294
- airborneTime : number // s since leaving ground
295
- fallDistance : number // m fallen since last grounded
296
-
297
- // Jump bookkeeping
298
- jumpBufferRemaining: number // s — set when intent.jump fires; consumed on land
299
- inJumpAnticipation : boolean // true during the squash window
300
- isVariableJumpCut : boolean // true if button released before apex
301
-
302
- // Exertion & breath
303
- exertion : number // [0..1], smoothed
304
- breathPhase : number // [0..1) — fraction through current breath cycle
305
- breathRateHz : number // current actual rate
306
- breathAmplitudeM : number // current actual amplitude
307
-
308
- // Stride
309
- stridePhase : number // [0..1) — fraction through 2-step cycle
310
- stepCount : integer // monotonic; useful for "every Nth step" effects
311
-
312
- // Pose
313
- eyeHeight : number // current m above body origin (lerped during crouch)
314
-
315
- // Tilt
316
- leanRollRad : number // current eye roll
317
- }
318
- ```
319
-
320
- ### 4.4 `FirstPersonPose` — channels for a skeleton system
321
-
322
- These are the *outputs* a future humanoid rig should drive itself from. Designed so the same controller can later feed:
323
- - a first-person arms rig (arms + weapon),
324
- - a third-person full-body rig,
325
- - a network-replicated remote rig.
326
-
327
- ```
328
- pose = {
329
- // World-space body
330
- rootPosition : Vector3
331
- rootYawRad : number // body facing
332
-
333
- // Head decoupled from body (the "look")
334
- headYawRad : number // = body yaw for most controllers
335
- headPitchRad : number
336
- headRollRad : number // includes lean
337
-
338
- // Locomotion phase — for animation playback
339
- locomotionPhase : number // [0..1), synced to stridePhase
340
- locomotionSpeed : number // m/s, for blend tree
341
- locomotionStrafe : number // [-1..1], for strafe-blend space
342
-
343
- // Action state — for state machine driving
344
- actionState : enum // Grounded | Anticipating | Airborne | Landing
345
- crouchAmount : number // [0..1] interpolation between standing/crouched poses
346
- aimPitch : number // = headPitchRad, surfaced separately for IK
347
- }
348
- ```
349
-
350
- The pose channels are intentionally *redundant* with `state` — they're a stable contract for downstream systems, even if `state` internals churn.
351
-
352
- ### 4.5 `FirstPersonSignals` — event surface
353
-
354
- ```
355
- signals = {
356
- onFootStep : Signal<{ side: "L"|"R", speed: number, surfaceTag: string? }>
357
- onJumpStart : Signal<{ peakHeight: number }> // fires at impulse, after anticipation
358
- onJumpApex : Signal<{}>
359
- onLeaveGround: Signal<{ reason: "jump"|"fall" }>
360
- onLand : Signal<{ verticalSpeed: number, kind: "soft"|"hard" }>
361
- onCrouchEnter: Signal<{}>
362
- onCrouchExit : Signal<{}>
363
- onSprintStart: Signal<{}>
364
- onSprintStop : Signal<{}>
365
- onBreathIn : Signal<{ amplitude: number, rateHz: number }> // peak inhale
366
- onBreathOut : Signal<{ amplitude: number, rateHz: number }> // peak exhale
367
- }
368
- ```
369
-
370
- All signals fire from `fixedUpdate` so listeners see consistent state. `onFootStep` fires twice per stride at the troughs of vertical bob. The `side` flips each footstep.
371
-
372
- ---
373
-
374
- ## 5. The system: `FirstPersonPlayerControllerSystem`
375
-
376
- ```
377
- class FirstPersonPlayerControllerSystem extends System {
378
- dependencies = [FirstPersonPlayerController, Transform, Motion]
379
- components_used = [
380
- spec(Transform, RW),
381
- spec(Motion, RW),
382
- spec(Camera, RW), // on the eye entity
383
- ]
384
-
385
- fixedUpdate(dt) { /* L1, L2, L4 — deterministic */ }
386
- update(dt) { /* L3 — camera composition at render rate */ }
387
-
388
- link(controller, transform, motion, entity) { /* spawn eye entity if missing, build signals, snapshot defaults */ }
389
- unlink(controller, transform, motion, entity) { /* tear down eye entity, clear signals */ }
390
- }
391
- ```
392
-
393
- ### 5.1 `fixedUpdate` — pseudocode
394
-
395
- ```
396
- for each entity (controller, bodyTransform, motion):
397
- // ---- L1: Locomotion -------------------------------------------------
398
- consumeLookDelta(controller.intent.look) → apply to bodyYaw, eyePitch (clamped)
399
- desiredSpeed = pickSpeed(controller, state.moveMode)
400
- desiredVel = bodyLocal(intent.move) * desiredSpeed
401
- accel = state.grounded ? config.motion.groundAccel : config.motion.airAccel
402
- motion.velocity.xz = stepTowards(motion.velocity.xz, desiredVel, accel * dt)
403
-
404
- // Jump
405
- if intent.jump consumed:
406
- state.jumpBufferRemaining = config.jump.bufferTime
407
- state.jumpBufferRemaining -= dt
408
-
409
- canJump = (state.grounded || state.timeSinceGrounded < config.jump.coyoteTime)
410
- && state.jumpBufferRemaining > 0
411
- if canJump and not state.inJumpAnticipation:
412
- beginJumpAnticipation() // schedules impulse after anticipation.duration
413
- signals.onJumpStart.send(...)
414
-
415
- integrateAnticipationAndImpulse(dt)
416
- applyGravity(dt, with fallGravityMult / cutGravityMult)
417
- integrate(bodyTransform.position += motion.velocity * dt)
418
-
419
- // Ground detection feedback (set externally by physics layer or assumed flat)
420
- updateGroundedFromExternalContact()
421
-
422
- // ---- L2: Pose -------------------------------------------------------
423
- updateExertion(dt)
424
- updateBreath(dt) // advances breathPhase, computes amp/rate
425
- updateStride(dt) // advances stridePhase when grounded & moving
426
- updateCrouch(dt) // lerps eyeHeight
427
- updateLeanTarget(dt) // computes target roll from lateral accel; springed in L3
428
-
429
- // ---- L4: Events -----------------------------------------------------
430
- if stridePhase wrapped past 0 or 0.5:
431
- signals.onFootStep.send({ side: nextSide(), ... })
432
- detectGroundedEdges() // sends onLeaveGround / onLand
433
- detectJumpApex()
434
- detectBreathPeaks() // sends onBreathIn / onBreathOut
435
- detectCrouchEdges()
436
- detectSprintEdges()
437
-
438
- // Publish pose channels
439
- writePoseChannels()
440
- ```
441
-
442
- ### 5.2 `update` (variable rate) — camera composition
443
-
444
- ```
445
- for each entity:
446
- eyeT = eye transform
447
- body = body transform
448
-
449
- // Base eye position (body local)
450
- base = (0, state.eyeHeight, 0)
451
-
452
- // Additive offsets — composed in the body's local space, then world-transformed
453
- bobOffset = computeBob(state.stridePhase, state.speedNormalized, config)
454
- breathOffset = computeBreath(state.breathPhase, state.breathAmplitudeM, config)
455
- landOffset = landingSpring.evaluate(dt) // under-damped, decays to 0
456
- anticipOffset= state.inJumpAnticipation ? anticipationCurve.eval() : 0
457
- shakeOffset = externalShakeStack.evaluate(dt) // future: gunfire, explosions
458
-
459
- localEyePos = base + (bobOffset + breathOffset + landOffset + anticipOffset + shakeOffset)
460
- eyeT.position = body.position + bodyRotation * localEyePos
461
-
462
- // Rotation: yaw from body, pitch from look, roll from lean + bob
463
- rollTotal = leanSpring.evaluate(dt) + bobRoll(state.stridePhase, config)
464
- eyeT.rotation = quatYaw(body.yaw) * quatPitch(state.eyePitch) * quatRoll(rollTotal)
465
-
466
- // FOV
467
- fovTarget = config.fov.base
468
- + lerp(0, config.fov.sprintAdd, state.speedNormalized * sprintness)
469
- + (state.moveMode == Crouch ? config.fov.crouchAdd : 0)
470
- camera.fov = springTowards(camera.fov, fovTarget, config.fov.smoothHalfLife, dt)
471
- ```
472
-
473
- ### 5.3 Key formulas
474
-
475
- **Jump from peak height & time-to-apex** (lets designers think in m and seconds, not Newtons):
476
- ```
477
- g = 2 * peakHeight / (timeToApex²)
478
- initialVy = 2 * peakHeight / timeToApex
479
- ```
480
- On variable-height cut: switch gravity to `g * cutGravityMult` once button released *and* `vy > 0`.
481
- On descent: switch gravity to `g * fallGravityMult` once `vy <= 0`.
482
- This matches the classic Sonic/Mario "fast fall" feel.
483
-
484
- **Stride frequency from speed**:
485
- ```
486
- f = config.bob.stepFreqAtWalk
487
- * (max(speed, ε) / config.motion.walkSpeed) ^ config.bob.stepFreqExp
488
- ```
489
- At rest `f → 0` and bob amplitude clamps to 0, so the player stops bobbing when they stop.
490
-
491
- **Bob shape**:
492
-
493
- `stridePhase ∈ [0, 1)` wraps once per stride (= 2 footsteps).
494
-
495
- Lateral and roll alternate ONCE per stride (head shifts right during right-foot midstance, left during left-foot midstance):
496
- ```
497
- phase = stridePhase * 2π // one full cycle per stride (= 2 steps)
498
- lateral = A_l * sin(phase) * intensity // 1 full L↔R cycle per stride
499
- roll = A_r * sin(phase) * intensity // in phase with lateral
500
- ```
501
-
502
- Vertical is modeled as an **impulse-driven under-damped spring** rather than a sinusoid — the smooth `|sin|` curve we initially specified didn't match real gait dynamics (head is *highest* at midstance, *dips* at footfall under loading response, then is *pushed back up* by the standing leg). At each footfall event:
503
- ```
504
- verticalImpactSpring.velocity -= A_v * impactKickMultiplier * intensity
505
- ```
506
- Then each fixed step the spring damps toward zero with `halfLife ≈ 0.14 s`, `ζ ≈ 0.5`:
507
- ```
508
- verticalOffset = verticalImpactSpring.value // negative during impact dip,
509
- // positive during overshoot/push
510
- ```
511
-
512
- The under-damped response produces the four-phase curve:
513
- 1. **Impact** — sharp dip below neutral as the foot arrests the head's fall
514
- 2. **Recovery** — value rises back through neutral
515
- 3. **Push** — overshoot above neutral, modeling energy injection from the standing leg
516
- 4. **Settle** — back toward neutral until the next footfall kicks it again
517
-
518
- At fast cadence (sprint) the spring barely settles between footfalls and the head oscillates continuously. At slow cadence (back-pedal) each impact is more punctuated, with visible settling between steps. This emerges from the dynamics — no separate handling needed.
519
-
520
- **Intensity envelope.** The `intensity` factor used by the lateral/roll/kick amplitudes is itself a critically damped spring around `max(speedNormalized, backwardness)` (or zero when airborne), with `halfLife ≈ 0.22 s`. This is what prevents the "whiplash" of stopping motion — bob amplitudes fade smoothly to zero instead of being cut off when speed drops below the stride threshold.
521
-
522
- > An even earlier draft of this doc used `sin(phase / 2)` for lateral and roll, intending a "half-rate" oscillation. That formula actually produces only the first *half* of a sine wave per stridePhase cycle (range [0, π) of sin → all non-negative), which manifested as a one-sided sustained offset for the duration of one stride. The current `sin(phase)` formulation alternates correctly.
523
-
524
- **Footstep detection**: emit `onFootStep` when `stridePhase` crosses `0` (right foot) or `0.5` (left foot), but only if `state.grounded && speed > minStepSpeed`. The boundary check uses the *previous* phase so it fires exactly once even at huge dt.
525
-
526
- **Breath cycle**:
527
- ```
528
- breathRateHz = lerp(rateRest, rateMax, exertion) decaying with rateDecayHalfLife
529
- breathAmpM = lerp(ampRest, ampMax, exertion) decaying with ampDecayHalfLife
530
- breathPhase += breathRateHz * dt; wrap to [0,1)
531
-
532
- verticalBreath = -breathAmpM * sin(2π * breathPhase) * (1 + noiseAmount * perlin(t*0.7))
533
- pitchBreath = breathPitchAmp * cos(2π * breathPhase) // gentle nod, 90° out of phase
534
- ```
535
- Negative `vertical` so peak exhale dips the eye slightly (chest collapses). `onBreathIn` fires at phase `0.25`, `onBreathOut` at `0.75`.
536
-
537
- **Exertion update**:
538
- ```
539
- rise = (sprintActive ? sprintRiseRate : 0) + jumpRiseDelta(this frame)
540
- fall = idleDecayRate * (1 - rise > 0 ? 0 : 1)
541
- exertion = clamp(exertion + (rise - fall) * dt, 0, 1)
542
- ```
543
- Then `breathRateHz` and `breathAmpM` track `exertion` through *separate* exponential lags — rate hangs around longer than amplitude, matching how real recovery works (you breathe fast for a while after the chest has stopped heaving).
544
-
545
- **Critically damped spring** (used for FOV, lean, crouch height):
546
- ```
547
- y = halfLife / 0.6931
548
- α = exp(-y * dt)
549
- β = (x - target)
550
- v = (v - β * y) * α
551
- x = target + (β + v * dt / y) * α
552
- ```
553
- For landing recovery, use the same form with `zeta = 0.55` so it overshoots once before settling.
554
-
555
- ### 5.4 Edge cases and rules
556
-
557
- - **Look-pitch clamp** is *non-wrapping*: the pitch is stored as a separate scalar so it cannot accidentally roll over. Yaw on the body wraps freely.
558
- - **Posture is intent-driven; ceilings only block RISING, never push you down**: low geometry constrains posture in exactly one direction. If `intent.crouch` releases but an *already-crouched* (or -prone) entity cannot grow into the taller posture (headroom up-cast collides), the controller *holds* the lower posture and exposes `state.crouchBlocked = true`; standing resumes automatically when clear. The converse is deliberately NOT done: a *standing* player who walks under a low overhang is **not** auto-crouched, and a crouching player who meets a sub-crouch overhang is **not** auto-proned. Their current-posture capsule simply doesn't fit, so the mover blocks their forward motion — "if I can't clear it, I don't move forward." Dropping to a lower stance is always the player's choice (press crouch / slide), never the ceiling's. This rule lives in the controller because it's universal; the headroom cast itself is a service-injection point.
559
- - **Sprint requires forward intent**: sideways-only or backwards movement should not consume exertion at sprint rates. Specifically `sprint = intent.sprint && intent.move.y > 0.5 && state.grounded`.
560
- - **No bob in air**: stride does not advance while `!grounded`. Phase freezes so landing doesn't re-fire a stale footstep.
561
- - **Jump cancel during anticipation**: if grounded becomes false during anticipation (e.g. ground pulled out), drop the queued impulse and emit `onLeaveGround{reason:"fall"}` without `onJumpStart`.
562
- - **Disable when paused**: the system should respect a `controller.enabled` flag (or scene pause) and freeze `update` but still allow `fixedUpdate` to integrate gravity if desired. This is a configuration concern, not behavior — exposed via the existing engine pause mechanism.
563
-
564
- ---
565
-
566
- ## 6. Accessibility & motion sickness
567
-
568
- Three settings every player should be able to toggle (defaults shown):
569
-
570
- | Setting | Default | Effect when off |
571
- |---|---|---|
572
- | `config.lean.enabled` | true | No roll on strafe |
573
- | `config.bob.verticalAmpAtWalk` | (nonzero) | Set to 0 to disable bob |
574
- | `config.fov.sprintAdd` | 6 | Set to 0 to disable FOV kick (a known nausea trigger) |
575
-
576
- The Playtank article calls these out specifically — keeping the horizon flat and the FOV stable accommodates a meaningful fraction of players who otherwise can't play first-person at all. They are exposed as plain config knobs, not buried.
577
-
578
- ---
579
-
580
- ## 7. Integration points (deferred work)
581
-
582
- These don't ship with the controller but the design preserves a clean attachment surface for each:
583
-
584
- | Future system | How it hooks in |
585
- |---|---|
586
- | **Capsule physics / collision** | Reads `Motion.velocity` and resolves; writes `state.grounded`, `state.groundNormal`, `state.surfaceTag`. Suggested via a small `GroundContact` component the physics system populates and the controller reads. |
587
- | **First-person arms rig** | Reads `pose.headPitchRad`, `pose.locomotionPhase`, `state.actionState`. Driven by an animation graph keyed to the same stride phase the bob uses, so feet animate in sync with footstep events. |
588
- | **Third-person body rig** | Reads `pose.rootYawRad`, `pose.locomotionSpeed/Strafe`, `pose.crouchAmount`, `state.actionState`. Built around a standard blend-space. |
589
- | **Sound** | Subscribes to `signals.onFootStep` (modulated by `surfaceTag`), `onLand` (kind = soft/hard), `onJumpStart`, `onBreathIn/Out` (volume = amplitude). |
590
- | **Camera shake from external sources** (gunfire, explosions) | Pushes into the `externalShakeStack` on the controller — an additive transform list. The controller's L3 evaluates it; sources only push. |
591
- | **Stamina / damage / status** | A separate component can write into `controller.config.exertion.*` at runtime — e.g. an injury raises `idleDecayRate` to a negative value (exertion can never recover fully). |
592
- | **Networking** | Because L1+L2+L4 are deterministic in `fixedUpdate` and the intent surface is small, the controller can be rolled back / replayed by writing intent and re-running fixed updates. No design changes needed. |
593
-
594
- ---
595
-
596
- ## 8. File layout
597
-
598
- ```
599
- engine/control/first-person/
600
- DESIGN.md (this file)
601
- FirstPersonPlayerController.js (component class)
602
- FirstPersonPlayerControllerSystem.js (system class)
603
- FirstPersonPlayerControllerSerializationAdapter.js
604
- FirstPersonPlayerController.spec.js (component contract)
605
- FirstPersonPlayerControllerSystem.spec.js (system behavior)
606
- math/
607
- criticallyDampedSpring.js
608
- underDampedSpring.js
609
- computeJumpFromApex.js
610
- pose/
611
- FirstPersonPose.js (the pose-channel struct)
612
- ```
613
-
614
- Math helpers live as standalone pure functions so they're independently testable and reusable.
615
-
616
- ---
617
-
618
- ## 9. Resolved design decisions
619
-
620
- The open questions from the previous draft are now answered:
621
-
622
- 1. **Body pitch** — **No pitch on the body, ever.** Body holds only yaw. Pitch lives on the eye. Standard FPS convention; keeps physics integration clean.
623
- 2. **Eye representation** — **Separate entity** with its own `Transform` + `Camera`, parented to the body via `TransformAttachment`. Holds the door open for vehicle-mount / turret-style reparenting later without refactor.
624
- 3. **Look intent shape** — **`Vector2` delta** (yaw on `x`, pitch on `y`). Matches mouse semantics; yaw-only sources (turret-style AI) just write `(x, 0)`.
625
- 4. **Bob curve evaluation** — **Runtime sine.** Cheap on modern CPUs. No LUT.
626
- 5. **Look processing rate** — **Inside `fixedUpdate`.** Determinism wins. Input bindings accumulate look delta into `intent.look` (a `Vector2`) between fixed updates; the system consumes and zeroes it once per fixed step. Cost: at very low fixed rates (e.g. 30 Hz) mouse feel becomes slightly chunky; mitigation if it ever bites is to raise `fixedUpdateStepSize` from default ~60 Hz to 120 Hz for entities with this controller, *not* to move look back to render rate.
627
-
628
- ### Jump-button repeat (added requirement)
629
-
630
- **Holding the jump button must not produce repeated jumps.** A jump must consume an explicit *button-down edge*; holding the button after a jump produces no further jumps until the button is released and pressed again.
631
-
632
- Implementation:
633
-
634
- - `intent.jump` is a **hold-state boolean** (true while the button is held). The input binding sets it on key-down and clears it on key-up.
635
- - The system tracks `_jumpButtonWasDown` internally. A jump is *requested* only on the false→true edge, not while the value remains true.
636
- - Pressing space then holding it across grounded → airborne → grounded transitions produces exactly **one** jump. The player must release and re-press to jump again.
637
- - Auto-repeat from the OS is already filtered by `KeyboardDevice` (`event.repeat` check), so the edge fires cleanly once per physical press.
638
- - Bonus: the same edge gives us **variable jump height** — the true→false edge (release) cuts gravity on the ascending portion of the jump. Two birds, one signal.
639
-
640
- The same hold-state-plus-edge pattern is used for crouch (when `config.crouch.mode === "toggle"`); when in `"hold"` mode the held value is used directly.
641
-
642
- ---
643
-
644
- ## 10. Sources
645
-
646
- - gamedevpills — [The 3Cs Framework: Character, Camera, Controls](https://www.gamedevpills.com/p/the-3cs-framework-character-camera)
647
- - Playtank — [First-Person 3Cs: Camera](https://playtank.io/2023/05/12/first-person-3cs-camera/)
648
- - Roberts Space Industries — [Design Notes: FPS Stances & Breathing](https://robertsspaceindustries.com/en/comm-link/engineering/14653-Design-Notes-FPS-Stances-Breathing)
649
- - NeoFPS — [Additive Transforms and Effects](https://docs.neofps.com/manual/fpcam-additive-transforms-and-effects.html)
650
- - Daniel Holden — [Spring-It-On: The Game Developer's Spring-Roll-Call](https://theorangeduck.com/page/spring-roll-call)
651
- - Allen Chou — [Game Math: Precise Control over Numeric Springing](https://allenchou.net/2015/04/game-math-precise-control-over-numeric-springing/)
652
- - Ryan Juckett — [Damped Springs](https://www.ryanjuckett.com/damped-springs/)
653
- - GDC Vault — [Animation Bootcamp: Giving Purpose to First-Person Animation](https://www.gdcvault.com/play/1017633/Animation-Bootcamp-Giving-Purpose-to)
654
- - GDC Vault — [The Art of First Person Animation for Destiny](https://www.gdcvault.com/play/1022297/The-Art-of-First-Person)
655
- - Palos Publishing — [Simulating Breathing and Idle Motion Procedurally](https://palospublishing.com/simulating-breathing-and-idle-motion-procedurally/)
656
- - Ketra Games — [Coyote Time and Jump Buffering](https://www.ketra-games.com/2021/08/coyote-time-and-jump-buffering.html)
657
- - Opsive — [First Person Lean](https://opsive.com/support/documentation/ultimate-character-controller/character/abilities/included-abilities/first-person-lean/) and [Character Foot Effects](https://opsive.com/support/documentation/ultimate-character-controller/surface-system/character-foot-effects/)
658
- - CTRL500 — [Mirror's Edge Catalyst: Keeping the camera in your face](https://ctrl500.com/developers-corner/mirrors-edge-catalyst-keeping-the-camera-in-your-face/) (DICE / Erik Söderholm)