@woosh/meep-engine 3.60.4 → 3.60.5

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 (379) hide show
  1. package/CHANGELOG.md +2602 -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.map +1 -1
  63. package/src/shade/renderer/Renderer.js +7 -5
  64. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
  65. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
  66. package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
  67. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  68. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  69. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  70. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  71. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  72. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +2 -2
  73. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +2 -2
  74. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +2 -1
  75. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
  76. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +18 -1
  77. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
  78. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
  79. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
  80. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
  81. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
  82. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
  83. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
  84. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
  85. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
  86. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +1 -8
  87. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +5 -5
  88. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
  89. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +10 -10
  90. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +1 -4
  91. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
  92. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +1 -10
  93. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
  94. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
  95. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
  96. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
  97. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
  98. package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
  99. package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
  100. package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
  101. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
  102. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
  103. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
  104. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
  105. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
  106. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
  107. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
  108. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  109. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
  110. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
  111. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
  112. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
  113. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
  114. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
  115. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
  116. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
  117. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
  118. package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
  119. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
  120. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  121. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
  122. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
  123. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
  124. package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
  125. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  126. package/src/shade/renderer/material/GPUMaterialManager.d.ts +7 -0
  127. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  128. package/src/shade/renderer/material/GPUMaterialManager.js +11 -0
  129. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
  130. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  131. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
  132. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  133. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
  134. package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
  135. package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
  136. package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
  137. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
  138. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
  139. package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
  140. package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
  141. package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
  142. package/src/shade/renderer/water/shader_water.js +22 -13
  143. package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
  144. package/src/shade/renderer/water/shader_water_avboit.js +7 -7
  145. package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
  146. package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
  147. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
  148. package/editor/REWORK_PLAN_2026_08.md +0 -692
  149. package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
  150. package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
  151. package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
  152. package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
  153. package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
  154. package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
  155. package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
  156. package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
  157. package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
  158. package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
  159. package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
  160. package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
  161. package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
  162. package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
  163. package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
  164. package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
  165. package/src/REVIEW_2026_08_06/adv-core.md +0 -511
  166. package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
  167. package/src/REVIEW_2026_08_06/adv-view.md +0 -446
  168. package/src/REVIEW_2026_08_06/api-core.md +0 -296
  169. package/src/REVIEW_2026_08_06/api-engine.md +0 -195
  170. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
  171. package/src/REVIEW_2026_08_06/core-binary.md +0 -348
  172. package/src/REVIEW_2026_08_06/core-collection.md +0 -376
  173. package/src/REVIEW_2026_08_06/core-math.md +0 -221
  174. package/src/REVIEW_2026_08_06/core-process.md +0 -266
  175. package/src/REVIEW_2026_08_06/core-science.md +0 -223
  176. package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
  177. package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
  178. package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
  179. package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
  180. package/src/REVIEW_2026_08_06/engine-input.md +0 -197
  181. package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
  182. package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
  183. package/src/REVIEW_2026_08_06/generation.md +0 -176
  184. package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
  185. package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
  186. package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
  187. package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
  188. package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
  189. package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
  190. package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
  191. package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
  192. package/src/REVIEW_2026_08_06/view.md +0 -667
  193. package/src/REVIEW_2026_08_06.md +0 -610
  194. package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
  195. package/src/core/bvh2/bvh3/NOTES.md +0 -2
  196. package/src/core/bvh8/NOTES.md +0 -63
  197. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
  198. package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
  199. package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
  200. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
  201. package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
  202. package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
  203. package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
  204. package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
  205. package/src/core/geom/3d/gjk/NOTES.md +0 -8
  206. package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
  207. package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
  208. package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
  209. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
  210. package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
  211. package/src/engine/REVIEW_2026_07_03.md +0 -211
  212. package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
  213. package/src/engine/control/first-person/DESIGN.md +0 -658
  214. package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
  215. package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
  216. package/src/engine/control/first-person/TODO.md +0 -160
  217. package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
  218. package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
  219. package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
  220. package/src/engine/ecs/ik/PLAN.md +0 -179
  221. package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
  222. package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
  223. package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
  224. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
  225. package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
  226. package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
  227. package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
  228. package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
  229. package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
  230. package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
  231. package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
  232. package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
  233. package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
  234. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
  235. package/src/engine/network/CONGESTION_CONTROL.md +0 -87
  236. package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
  237. package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
  238. package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
  239. package/src/engine/physics/cloth/PLAN.md +0 -3056
  240. package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
  241. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
  242. package/src/engine/physics/fluid/solver/NOTES.md +0 -46
  243. package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
  244. package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
  245. package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
  246. package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
  247. package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
  248. package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
  249. package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
  250. package/src/format/image/avif/DECISIONS.md +0 -545
  251. package/src/format/image/jpeg/NOTES.md +0 -3
  252. package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
  253. package/src/generation/COORDINATES.md +0 -54
  254. package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
  255. package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
  256. package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
  257. package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
  258. package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
  259. package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
  260. package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
  261. package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
  262. package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
  263. package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
  264. package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
  265. package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
  266. package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
  267. package/src/shade/RENDERER_CONTRACT.md +0 -697
  268. package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
  269. package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
  270. package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
  271. package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
  272. package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
  273. package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
  274. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
  275. package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
  276. package/src/shade/renderer/NOTES.md +0 -78
  277. package/src/shade/renderer/PLAN.md +0 -8
  278. package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
  279. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
  280. package/src/shade/renderer/atmosphere/NOTES.md +0 -6
  281. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
  282. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
  283. package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
  284. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
  285. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
  286. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
  287. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
  288. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
  289. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
  290. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
  291. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
  292. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
  293. package/src/shade/renderer/camera/orbital/README.md +0 -81
  294. package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
  295. package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
  296. package/src/shade/renderer/camera/orbital/index.js +0 -9
  297. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
  298. package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
  299. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
  300. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
  301. package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
  302. package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
  303. package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
  304. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
  305. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
  306. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -481
  307. package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
  308. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
  309. package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
  310. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
  311. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
  312. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
  313. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
  314. package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -529
  315. package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
  316. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4306
  317. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
  318. package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
  319. package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
  320. package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
  321. package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
  322. package/src/shade/renderer/hiz/NOTES.md +0 -3
  323. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
  324. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
  325. package/src/shade/renderer/light/cluster/NOTES.md +0 -11
  326. package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
  327. package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
  328. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
  329. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
  330. package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
  331. package/src/shade/renderer/particles/DESIGN.md +0 -1011
  332. package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -280
  333. package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
  334. package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
  335. package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
  336. package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
  337. package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
  338. package/src/shade/renderer/postprocess/nss/NOTES.md +0 -332
  339. package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
  340. package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
  341. package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
  342. package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
  343. package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
  344. package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
  345. package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
  346. package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
  347. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
  348. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
  349. package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
  350. package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
  351. package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -391
  352. package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -153
  353. package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
  354. package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
  355. package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
  356. package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
  357. package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
  358. package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
  359. package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
  360. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
  361. package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
  362. package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -927
  363. package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
  364. package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
  365. package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
  366. package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
  367. package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
  368. package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
  369. package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
  370. package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
  371. package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
  372. package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
  373. package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -3
  374. package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
  375. package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
  376. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
  377. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
  378. package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
  379. 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)