pyforge3d 2.1.0__tar.gz → 2.1.1__tar.gz

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 (135) hide show
  1. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/PKG-INFO +3 -3
  2. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/README.md +1 -1
  3. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/pyproject.toml +2 -2
  4. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/__init__.py +2 -2
  5. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/animation/ik_fabrik.py +2 -1
  6. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/animation/system.py +1 -3
  7. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/audio/clip.py +1 -1
  8. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/character.py +24 -3
  9. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/collision/detection.py +1 -4
  10. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/collision/gjk.py +12 -1
  11. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ecs/entity.py +2 -2
  12. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/facade.py +15 -10
  13. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/input.py +26 -7
  14. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/particle/emitter.py +3 -2
  15. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/realtime/shaders.py +17 -10
  16. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/realtime/window_renderer.py +258 -52
  17. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/scene/node.py +2 -2
  18. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/sim/world.py +4 -0
  19. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ui/panels.py +2 -4
  20. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ui/system.py +1 -1
  21. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/viewer.py +39 -1
  22. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/Cargo.lock +0 -0
  23. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/Cargo.toml +0 -0
  24. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/LICENSE +0 -0
  25. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/_core.pyi +0 -0
  26. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/animation/__init__.py +0 -0
  27. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/animation/clip.py +0 -0
  28. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/animation/player.py +0 -0
  29. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/animation/skeleton.py +0 -0
  30. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/app.py +0 -0
  31. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/audio/__init__.py +0 -0
  32. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/audio/null_driver.py +0 -0
  33. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/audio/openal_driver.py +0 -0
  34. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/audio/source.py +0 -0
  35. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/audio/system.py +0 -0
  36. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/backend.py +0 -0
  37. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/camera.py +0 -0
  38. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/collision/__init__.py +0 -0
  39. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/collision/epa.py +0 -0
  40. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/collision/heightfield.py +0 -0
  41. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/collision/layers.py +0 -0
  42. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/collision/raycast.py +0 -0
  43. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/constraints/__init__.py +0 -0
  44. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/constraints/base.py +0 -0
  45. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/constraints/joint_type.py +0 -0
  46. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/constraints/joints.py +0 -0
  47. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/contact/__init__.py +0 -0
  48. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/contact/solver.py +0 -0
  49. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/dynamics/__init__.py +0 -0
  50. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/dynamics/aba.py +0 -0
  51. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/dynamics/crba.py +0 -0
  52. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/dynamics/model.py +0 -0
  53. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/dynamics/rnea.py +0 -0
  54. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ecs/__init__.py +0 -0
  55. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ecs/bridge.py +0 -0
  56. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ecs/component.py +0 -0
  57. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ecs/serialization.py +0 -0
  58. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ecs/system.py +0 -0
  59. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ecs/transform.py +0 -0
  60. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/editor/__init__.py +0 -0
  61. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/editor/editor_app.py +0 -0
  62. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/editor/gizmo.py +0 -0
  63. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/editor/layout.py +0 -0
  64. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/errors.py +0 -0
  65. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/events.py +0 -0
  66. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/io/__init__.py +0 -0
  67. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/io/mesh_data.py +0 -0
  68. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/io/obj_loader.py +0 -0
  69. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/io/world_snapshot.py +0 -0
  70. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/logging.py +0 -0
  71. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/math/__init__.py +0 -0
  72. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/math/inertia.py +0 -0
  73. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/math/quaternion.py +0 -0
  74. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/math/se3.py +0 -0
  75. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/math/spatial.py +0 -0
  76. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/model/__init__.py +0 -0
  77. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/model/kinematics.py +0 -0
  78. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/model/robot_config.py +0 -0
  79. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/model/urdf_loader.py +0 -0
  80. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/particle/__init__.py +0 -0
  81. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/particle/presets.py +0 -0
  82. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/particle/system.py +0 -0
  83. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/profiler.py +0 -0
  84. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/py.typed +0 -0
  85. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/recorder.py +0 -0
  86. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/__init__.py +0 -0
  87. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/base.py +0 -0
  88. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/deferred/__init__.py +0 -0
  89. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/deferred/renderer.py +0 -0
  90. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/hq/__init__.py +0 -0
  91. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/hq/raytracer.py +0 -0
  92. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/hq/renderer.py +0 -0
  93. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/hq/scene.py +0 -0
  94. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/passes/__init__.py +0 -0
  95. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/passes/base.py +0 -0
  96. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/realtime/__init__.py +0 -0
  97. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/realtime/context.py +0 -0
  98. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/realtime/meshes.py +0 -0
  99. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/realtime/renderer.py +0 -0
  100. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/bloom_down.frag +0 -0
  101. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/bloom_up.frag +0 -0
  102. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/fullscreen.vert +0 -0
  103. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/gbuffer.frag +0 -0
  104. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/gbuffer.vert +0 -0
  105. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/lighting.frag +0 -0
  106. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/pbr.wgsl +0 -0
  107. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/shadow.frag +0 -0
  108. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/shadow.vert +0 -0
  109. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/ssao.frag +0 -0
  110. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/ssao_blur.frag +0 -0
  111. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/tonemap.frag +0 -0
  112. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/update_particles.comp +0 -0
  113. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/snapshot.py +0 -0
  114. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/wgpu_backend/__init__.py +0 -0
  115. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/wgpu_backend/pipeline.py +0 -0
  116. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/wgpu_backend/renderer.py +0 -0
  117. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/robot/__init__.py +0 -0
  118. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/robot/presets.py +0 -0
  119. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/robot/robot.py +0 -0
  120. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/scene/__init__.py +0 -0
  121. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/scene/manager.py +0 -0
  122. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/scene/prefab.py +0 -0
  123. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/sim/__init__.py +0 -0
  124. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/sim/domain_rand.py +0 -0
  125. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/sim/jax_batch.py +0 -0
  126. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ui/__init__.py +0 -0
  127. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ui/backend.py +0 -0
  128. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ui/canvas.py +0 -0
  129. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d_core/Cargo.toml +0 -0
  130. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d_core/benches/physics_bench.rs +0 -0
  131. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d_core/src/bvh.rs +0 -0
  132. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d_core/src/gjk_epa.rs +0 -0
  133. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d_core/src/lib.rs +0 -0
  134. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d_core/src/math_simd.rs +0 -0
  135. {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d_core/src/pgs_solver.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyforge3d
3
- Version: 2.1.0
3
+ Version: 2.1.1
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Intended Audience :: Developers
@@ -46,7 +46,7 @@ Provides-Extra: docs
46
46
  Provides-Extra: render
47
47
  Provides-Extra: rl
48
48
  License-File: LICENSE
49
- Summary: Pure-Python 3D physics game engine — fast like native, beautiful like simulation.
49
+ Summary: Pure-Python 3D physics game engine — own dynamics, own rules, no compromises.
50
50
  Keywords: game-engine,physics,simulation,3d,rigid-body,collision,rendering,robotics,reinforcement-learning,numpy,jax
51
51
  Author-email: iruki <me@iruki.dev>
52
52
  Requires-Python: >=3.12
@@ -59,7 +59,7 @@ Project-URL: Repository, https://github.com/iruki-dev/forge3d
59
59
 
60
60
  # forge3d
61
61
 
62
- > **Pure-Python 3D game engine — easy like pygame, beautiful like simulation.**
62
+ > **Pure-Python 3D physics game engine — own dynamics, own rules, no compromises.**
63
63
 
64
64
  [![PyPI version](https://img.shields.io/pypi/v/pyforge3d.svg)](https://pypi.org/project/pyforge3d/)
65
65
  [![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/)
@@ -1,6 +1,6 @@
1
1
  # forge3d
2
2
 
3
- > **Pure-Python 3D game engine — easy like pygame, beautiful like simulation.**
3
+ > **Pure-Python 3D physics game engine — own dynamics, own rules, no compromises.**
4
4
 
5
5
  [![PyPI version](https://img.shields.io/pypi/v/pyforge3d.svg)](https://pypi.org/project/pyforge3d/)
6
6
  [![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/)
@@ -6,8 +6,8 @@ build-backend = "maturin"
6
6
 
7
7
  [project]
8
8
  name = "pyforge3d"
9
- version = "2.1.0"
10
- description = "Pure-Python 3D physics game engine — fast like native, beautiful like simulation."
9
+ version = "2.1.1"
10
+ description = "Pure-Python 3D physics game engine — own dynamics, own rules, no compromises."
11
11
  readme = "README.md"
12
12
  license = { file = "LICENSE" }
13
13
  requires-python = ">=3.12"
@@ -1,6 +1,6 @@
1
1
  """forge3d — pure-Python 3D game engine.
2
2
 
3
- "Fast like native, beautiful like simulation."
3
+ "Pure-Python 3D physics game engine — own dynamics, own rules, no compromises."
4
4
  Coordinate system: z-up, SI units (metres, kg, seconds).
5
5
 
6
6
  Minimal example (14 lines)::
@@ -121,7 +121,7 @@ from forge3d.scene import Prefab, SceneManager, SceneNode
121
121
  from forge3d.ui import Canvas, DebugPanel, HierarchyPanel, InspectorPanel, UISystem
122
122
  from forge3d.viewer import Viewer
123
123
 
124
- __version__ = "2.1.0"
124
+ __version__ = "2.1.1"
125
125
 
126
126
  # ── API 안정성 선언 ───────────────────────────────────────────────────────────
127
127
  # Stable (v3까지 Breaking change 없음):
@@ -1,6 +1,7 @@
1
1
  """FABRIK IK 솔버 — Forward And Backward Reaching Inverse Kinematics.
2
2
 
3
- 참고: Aristidou & Lasenby (2011), "FABRIK: A fast, iterative solver for the Inverse Kinematics problem"
3
+ 참고: Aristidou & Lasenby (2011),
4
+ "FABRIK: A fast, iterative solver for the Inverse Kinematics problem"
4
5
  """
5
6
 
6
7
  from __future__ import annotations
@@ -33,11 +33,9 @@ class AnimationSystem(System):
33
33
 
34
34
  # AnimationPlayer가 있으면 체인 위치 읽기
35
35
  try:
36
- player = ew.get_component(e, AnimationPlayer) # type: ignore[assignment]
36
+ anim_player: AnimationPlayer = ew.get_component(e, AnimationPlayer)
37
37
  except KeyError:
38
38
  continue
39
-
40
- anim_player: AnimationPlayer = player
41
39
  joint_positions = anim_player.skeleton.joint_positions()
42
40
  if len(joint_positions) < 2:
43
41
  continue
@@ -69,7 +69,7 @@ def _load_wav_stdlib(path: Path) -> tuple[np.ndarray, int]:
69
69
  sr = wf.getframerate()
70
70
  raw = wf.readframes(n_frames)
71
71
 
72
- dtype_map = {1: np.int8, 2: np.int16, 4: np.int32}
72
+ dtype_map: dict[int, type[np.signedinteger]] = {1: np.int8, 2: np.int16, 4: np.int32}
73
73
  dtype = dtype_map.get(sampwidth, np.int16)
74
74
  data = np.frombuffer(raw, dtype=dtype).astype(np.float32)
75
75
  # 정규화
@@ -50,6 +50,7 @@ class CharacterController:
50
50
  height: float,
51
51
  radius: float,
52
52
  ground_layer_mask: int = 0xFFFF,
53
+ ground_check_hz: float = 60.0,
53
54
  ) -> None:
54
55
  self._world = world
55
56
  self.body = body
@@ -57,7 +58,16 @@ class CharacterController:
57
58
  self._radius = float(radius)
58
59
  self._ground_layer_mask = ground_layer_mask
59
60
  self._grounded = False
60
- self._vertical_vel = 0.0 # m/s, used for jump/glide tracking
61
+ self._vertical_vel = 0.0
62
+
63
+ # Throttle ground-detection raycast. At 10 Hz (bots) this cuts the
64
+ # per-move cost from ~1 ms to ~0.017 ms with no gameplay difference.
65
+ self._ground_check_interval = 1.0 / max(1.0, float(ground_check_hz))
66
+ self._ground_check_timer = 0.0
67
+
68
+ # Jump cooldown prevents infinite jumping when the capsule is still
69
+ # close to the ground in the frame right after a jump.
70
+ self._jump_cooldown: float = 0.0
61
71
 
62
72
  # ── State queries ─────────────────────────────────────────────────────────
63
73
 
@@ -117,11 +127,12 @@ class CharacterController:
117
127
  ----------
118
128
  impulse : Upward speed added in m/s (think: initial jump velocity).
119
129
  """
120
- if self._grounded:
130
+ if self._grounded and self._jump_cooldown <= 0.0:
121
131
  vel = self.body.velocity.copy()
122
132
  vel[2] = float(impulse)
123
133
  self.body.set_velocity(vel)
124
134
  self._grounded = False
135
+ self._jump_cooldown = 0.40 # 400 ms before the next jump is allowed
125
136
 
126
137
  def glide(self, target_fall_speed: float = -1.5, dt: float = 1 / 60) -> None:
127
138
  """Reduce falling speed to *target_fall_speed* for a glide effect.
@@ -141,7 +152,17 @@ class CharacterController:
141
152
  # ── Internal ──────────────────────────────────────────────────────────────
142
153
 
143
154
  def _update_ground(self, dt: float) -> None:
144
- """Update is_grounded via downward raycast."""
155
+ """Update is_grounded via downward raycast (throttled + jump-cooldown aware)."""
156
+ # Decrement jump cooldown unconditionally every call
157
+ if self._jump_cooldown > 0.0:
158
+ self._jump_cooldown = max(0.0, self._jump_cooldown - dt)
159
+ self._grounded = False # never re-ground during cooldown
160
+ return
161
+
162
+ self._ground_check_timer += dt
163
+ if self._ground_check_timer < self._ground_check_interval:
164
+ return
165
+ self._ground_check_timer = 0.0
145
166
  pos = self.body.position
146
167
  ray_len = self._height / 2.0 + self._radius + self._GROUND_RAY_EXTRA
147
168
  hit = self._world.raycast(
@@ -586,10 +586,7 @@ def _capsule_vs_sphere(cap: Any, ia: int, sph: Any, ib: int) -> list[ContactPoin
586
586
  if depth <= 0.0:
587
587
  return []
588
588
  # Normal from body_b (sphere) to body_a (capsule) = opposite of to_sphere
589
- if dist > 1e-10:
590
- normal = -to_sphere / dist # from sphere toward capsule
591
- else:
592
- normal = np.array([0.0, 0.0, 1.0])
589
+ normal = -to_sphere / dist if dist > 1e-10 else np.array([0.0, 0.0, 1.0])
593
590
  # Contact position on capsule surface in direction of sphere
594
591
  contact_pos = closest + float(cap.shape_params["radius"]) * (-normal)
595
592
  return [ContactPoint(ia, ib, contact_pos.copy(), normal.copy(), float(depth))]
@@ -61,7 +61,18 @@ def _body_support(body: Any, d: np.ndarray) -> np.ndarray:
61
61
  if body.shape_type == "box":
62
62
  R = quat_to_rot(body.quat)
63
63
  return _support_box(body.pos, R, body.shape_params["half_extents"], d)
64
- if body.shape_type in ("mesh", "capsule"):
64
+ if body.shape_type == "capsule":
65
+ # Capsule: cylinder swept by sphere. Axis = local Z, half-length hl.
66
+ # Support = sphere_centre_along_d + radius*d_hat
67
+ # where sphere_centre = pos ± hl * body_z_axis
68
+ R = quat_to_rot(body.quat)
69
+ r = float(body.shape_params["radius"])
70
+ hl = float(body.shape_params["half_length"])
71
+ d_local = R.T @ d
72
+ # tip in direction of d — R[:,2] is a zero-copy column view, no allocation
73
+ tip = body.pos + float(np.sign(d_local[2] + 1e-300)) * hl * R[:, 2]
74
+ return _support_sphere(tip, r, d)
75
+ if body.shape_type == "mesh":
65
76
  R = quat_to_rot(body.quat)
66
77
  hull_verts = body.shape_params["hull_vertices"]
67
78
  return _support_convex_hull(body.pos, R, hull_verts, d)
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import itertools
6
6
  from collections.abc import Iterator
7
- from typing import TypeVar
7
+ from typing import Any, TypeVar
8
8
 
9
9
  from forge3d.ecs.component import Component
10
10
  from forge3d.errors import Forge3dError
@@ -91,7 +91,7 @@ class EntityWorld:
91
91
  for e in list(smallest.keys()):
92
92
  if e not in self._alive:
93
93
  continue
94
- row: list[object] = [e]
94
+ row: list[Any] = [e]
95
95
  ok = True
96
96
  for t in types:
97
97
  store = self._components.get(t, {})
@@ -1,6 +1,6 @@
1
1
  """forge3d public Facade — World, Body, Shape, Material.
2
2
 
3
- "Fast like native, beautiful like simulation."
3
+ "Pure-Python 3D physics game engine — own dynamics, own rules, no compromises."
4
4
  Coordinate system: z-up, SI units.
5
5
 
6
6
  Users only need::
@@ -448,7 +448,7 @@ class World:
448
448
  self._materials: dict[str, Material] = {}
449
449
  self._camera: tuple | None = None
450
450
  self._robots: list[Any] = []
451
- self._welds: dict[int, tuple[int, np.ndarray]] = {}
451
+ self._welds: dict[int, tuple[int, np.ndarray, np.ndarray]] = {}
452
452
  # Event system
453
453
  from forge3d.events import EventDispatcher
454
454
 
@@ -615,9 +615,16 @@ class World:
615
615
  restitution: float = 0.3,
616
616
  friction: float = 0.5,
617
617
  static: bool = False,
618
+ collision_layer: int = 0x0001,
619
+ collision_mask: int = 0xFFFF,
618
620
  ) -> Body:
619
621
  """Add a convex-hull rigid body from a MeshData object.
620
622
 
623
+ Use ``collision_mask=0`` for visual-only decorative bodies that should
624
+ not participate in physics collision checks (trees, rocks, props).
625
+ This is important for performance: mesh GJK is expensive and decorative
626
+ bodies with large AABBs would otherwise be checked every frame.
627
+
621
628
  Typical use::
622
629
 
623
630
  from forge3d.io import load_obj
@@ -637,6 +644,8 @@ class World:
637
644
  restitution=restitution,
638
645
  friction=friction,
639
646
  static=static,
647
+ collision_layer=collision_layer,
648
+ collision_mask=collision_mask,
640
649
  )
641
650
  body = Body(self._physics, bid)
642
651
  self._register_body(body)
@@ -736,6 +745,7 @@ class World:
736
745
  mass: float = 70.0,
737
746
  name: str = "character",
738
747
  ground_layer_mask: int = 0xFFFF,
748
+ ground_check_hz: float = 60.0,
739
749
  ) -> Any:
740
750
  """Add a capsule-based character controller.
741
751
 
@@ -781,6 +791,7 @@ class World:
781
791
  height=height,
782
792
  radius=radius,
783
793
  ground_layer_mask=ground_layer_mask,
794
+ ground_check_hz=ground_check_hz,
784
795
  )
785
796
 
786
797
  def add(self, obj: Any) -> Any:
@@ -1299,20 +1310,14 @@ class World:
1299
1310
  from forge3d.math.quaternion import quat_multiply, quat_to_rot
1300
1311
 
1301
1312
  _ZEROS3 = np.zeros(3)
1302
- for body_id, weld_data in self._welds.items():
1303
- # Support both old 2-tuple format (pos only) and new 3-tuple (pos + rot)
1304
- if len(weld_data) == 2:
1305
- anchor_id, offset = weld_data
1306
- rel_q = None
1307
- else:
1308
- anchor_id, offset, rel_q = weld_data
1313
+ for body_id, (anchor_id, offset, rel_q) in self._welds.items():
1309
1314
  try:
1310
1315
  anchor = self._physics._get_body(anchor_id)
1311
1316
  except RuntimeError:
1312
1317
  continue
1313
1318
  R_anchor = quat_to_rot(anchor.quat)
1314
1319
  new_pos = anchor.pos + R_anchor @ offset
1315
- new_quat = quat_multiply(anchor.quat, rel_q) if rel_q is not None else anchor.quat
1320
+ new_quat = quat_multiply(anchor.quat, rel_q)
1316
1321
  self._physics.update_body_pose(body_id, new_pos, new_quat, vel=_ZEROS3, omega=_ZEROS3)
1317
1322
 
1318
1323
  def _sync_robot(self, robot: Any) -> None:
@@ -222,9 +222,11 @@ class _InputBuilder:
222
222
  "_keys_pressed",
223
223
  "_keys_released",
224
224
  "_mouse_pos",
225
- "_prev_mouse_pos",
225
+ "_frame_mdx", # accumulated delta-x across all events this frame
226
+ "_frame_mdy", # accumulated delta-y across all events this frame
226
227
  "_mouse_buttons",
227
228
  "_scroll_accum",
229
+ "_skip_next_delta", # discard first delta after cursor warp
228
230
  )
229
231
 
230
232
  def __init__(self) -> None:
@@ -232,9 +234,11 @@ class _InputBuilder:
232
234
  self._keys_pressed: set[str] = set()
233
235
  self._keys_released: set[str] = set()
234
236
  self._mouse_pos: tuple[float, float] = (0.0, 0.0)
235
- self._prev_mouse_pos: tuple[float, float] = (0.0, 0.0)
237
+ self._frame_mdx: float = 0.0
238
+ self._frame_mdy: float = 0.0
236
239
  self._mouse_buttons: set[int] = set()
237
240
  self._scroll_accum: float = 0.0
241
+ self._skip_next_delta: bool = True # skip first event (cursor may jump)
238
242
 
239
243
  # ── Event handlers (called by the windowing layer) ────────────────────────
240
244
 
@@ -249,9 +253,25 @@ class _InputBuilder:
249
253
  self._keys_released.add(key)
250
254
 
251
255
  def on_mouse_move(self, x: float, y: float) -> None:
252
- self._prev_mouse_pos = self._mouse_pos
256
+ # Accumulate all sub-frame events instead of only keeping the last.
257
+ # Previously _prev_mouse_pos was overwritten each call, discarding all
258
+ # but the final event's delta — losing up to 90% of motion at high refresh.
259
+ if not self._skip_next_delta:
260
+ self._frame_mdx += x - self._mouse_pos[0]
261
+ self._frame_mdy += y - self._mouse_pos[1]
262
+ self._skip_next_delta = False
253
263
  self._mouse_pos = (x, y)
254
264
 
265
+ def reset_mouse_delta(self) -> None:
266
+ """Discard any accumulated delta and skip the next warp event.
267
+
268
+ Call this immediately after enabling/disabling cursor capture so the
269
+ large position jump from GLFW's cursor warp doesn't cause a view lurch.
270
+ """
271
+ self._frame_mdx = 0.0
272
+ self._frame_mdy = 0.0
273
+ self._skip_next_delta = True
274
+
255
275
  def on_mouse_down(self, button: int) -> None:
256
276
  self._mouse_buttons.add(button)
257
277
 
@@ -265,14 +285,12 @@ class _InputBuilder:
265
285
 
266
286
  def build(self) -> Input:
267
287
  """Return an immutable :class:`Input` for the current frame."""
268
- dx = self._mouse_pos[0] - self._prev_mouse_pos[0]
269
- dy = self._mouse_pos[1] - self._prev_mouse_pos[1]
270
288
  return Input(
271
289
  _keys_held=frozenset(self._keys_held),
272
290
  _keys_pressed=frozenset(self._keys_pressed),
273
291
  _keys_released=frozenset(self._keys_released),
274
292
  _mouse_pos=self._mouse_pos,
275
- _mouse_delta=(dx, dy),
293
+ _mouse_delta=(self._frame_mdx, self._frame_mdy),
276
294
  _mouse_buttons=frozenset(self._mouse_buttons),
277
295
  _scroll_delta=self._scroll_accum,
278
296
  )
@@ -282,7 +300,8 @@ class _InputBuilder:
282
300
  self._keys_pressed.clear()
283
301
  self._keys_released.clear()
284
302
  self._scroll_accum = 0.0
285
- self._prev_mouse_pos = self._mouse_pos
303
+ self._frame_mdx = 0.0
304
+ self._frame_mdy = 0.0
286
305
 
287
306
  def feed_pygame_event(self, event: Any) -> None:
288
307
  """Feed a pygame event into the builder (deprecated — use glfw callbacks).
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass
6
+ from typing import Any
6
7
 
7
8
  from forge3d.ecs.component import Component
8
9
 
@@ -27,7 +28,7 @@ class ParticleEmitter(Component):
27
28
  active: bool = True
28
29
 
29
30
  @classmethod
30
- def preset(cls, name: str, **kwargs: object) -> ParticleEmitter:
31
+ def preset(cls, name: str, **kwargs: Any) -> ParticleEmitter:
31
32
  """VFX 프리셋 팩토리."""
32
33
  presets = {
33
34
  "sparks": {
@@ -71,6 +72,6 @@ class ParticleEmitter(Component):
71
72
  "color_end": (0.5, 0.7, 1.0, 0.0),
72
73
  },
73
74
  }
74
- base = presets.get(name, {})
75
+ base: dict[str, Any] = dict(presets.get(name, {}))
75
76
  base.update(kwargs)
76
77
  return cls(**base)
@@ -21,7 +21,7 @@ void main() {
21
21
 
22
22
  SHADOW_FRAG = """
23
23
  #version 330 core
24
- void main() { } // depth written automatically
24
+ void main() { } // depth written automatically to depth attachment
25
25
  """
26
26
 
27
27
  # ── Main PBR pass (Cook-Torrance BRDF + PCF shadows + optional texture) ──────
@@ -65,6 +65,7 @@ uniform vec3 u_ambient_color; // ambient RGB
65
65
  uniform vec3 u_mat_color; // base albedo (linear RGB [0,1])
66
66
  uniform float u_metallic; // 0 = dielectric, 1 = metal
67
67
  uniform float u_roughness; // 0 = mirror, 1 = fully diffuse
68
+ uniform float u_emissive; // emissive multiplier (0 = none, 3+ = bright glow)
68
69
 
69
70
  // Camera
70
71
  uniform vec3 u_eye;
@@ -74,7 +75,7 @@ uniform float u_fog_density;
74
75
  uniform vec3 u_fog_color;
75
76
 
76
77
  // Textures
77
- uniform sampler2D u_shadow_map;
78
+ uniform sampler2DShadow u_shadow_map; // hardware PCF comparison
78
79
  uniform sampler2D u_albedo_map;
79
80
  uniform int u_has_texture; // 1 = sample albedo_map, 0 = use u_mat_color
80
81
 
@@ -85,19 +86,24 @@ in vec4 v_shadow_coord;
85
86
 
86
87
  out vec4 frag_color;
87
88
 
88
- // ── PCF shadow factor ─────────────────────────────────────────────────────────
89
- float shadow_factor() {
89
+ // ── PCF shadow factor (hardware sampler2DShadow) ──────────────────────────────
90
+ // N and L are pre-normalised in main() — pass them in to avoid redundant work.
91
+ float shadow_factor(vec3 N, vec3 L) {
90
92
  vec3 proj = v_shadow_coord.xyz / v_shadow_coord.w;
91
93
  proj = proj * 0.5 + 0.5;
92
94
  if (proj.x < 0.0 || proj.x > 1.0 || proj.y < 0.0 || proj.y > 1.0 || proj.z > 1.0)
93
95
  return 1.0;
96
+ // Normal-based bias prevents acne on faces parallel to light
97
+ float cos_theta = clamp(dot(N, L), 0.0, 1.0);
98
+ float bias = max(0.008 * (1.0 - cos_theta), 0.002);
99
+ float ref = proj.z - bias;
100
+ // 3×3 PCF using hardware comparison — sampler2DShadow returns 0 or 1
94
101
  float shadow = 0.0;
95
- float texel = 1.0 / 2048.0;
102
+ float texel = 1.0 / float(textureSize(u_shadow_map, 0).x);
96
103
  for (int dx = -1; dx <= 1; dx++) {
97
104
  for (int dy = -1; dy <= 1; dy++) {
98
- float closest = texture(u_shadow_map,
99
- proj.xy + vec2(float(dx), float(dy)) * texel).r;
100
- shadow += (proj.z - 0.003 > closest) ? 0.0 : 1.0;
105
+ vec2 off = proj.xy + vec2(float(dx), float(dy)) * texel;
106
+ shadow += texture(u_shadow_map, vec3(off, ref));
101
107
  }
102
108
  }
103
109
  return shadow / 9.0;
@@ -160,13 +166,14 @@ void main() {
160
166
  vec3 kD = (1.0 - kS) * (1.0 - metallic);
161
167
  vec3 specular = D * G * F / max(4.0 * NdotV * NdotL, 0.001);
162
168
 
163
- float sf = shadow_factor();
169
+ float sf = shadow_factor(N, L);
164
170
  vec3 Lo = (kD * albedo / PI + specular) * u_light_color * NdotL * sf;
165
171
 
166
172
  // Ambient: simple Lambertian (no IBL in this pass)
167
173
  vec3 ambient = u_ambient_color * albedo * (0.2 + 0.8 * (1.0 - metallic));
168
174
 
169
- vec3 color = ambient + Lo;
175
+ // Emissive: added before tonemapping so bright values stay bright
176
+ vec3 color = ambient + Lo + albedo * u_emissive;
170
177
 
171
178
  // Reinhard HDR tone mapping + gamma correction
172
179
  color = color / (color + vec3(1.0));