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.
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/PKG-INFO +3 -3
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/README.md +1 -1
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/pyproject.toml +2 -2
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/__init__.py +2 -2
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/animation/ik_fabrik.py +2 -1
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/animation/system.py +1 -3
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/audio/clip.py +1 -1
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/character.py +24 -3
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/collision/detection.py +1 -4
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/collision/gjk.py +12 -1
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ecs/entity.py +2 -2
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/facade.py +15 -10
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/input.py +26 -7
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/particle/emitter.py +3 -2
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/realtime/shaders.py +17 -10
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/realtime/window_renderer.py +258 -52
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/scene/node.py +2 -2
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/sim/world.py +4 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ui/panels.py +2 -4
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ui/system.py +1 -1
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/viewer.py +39 -1
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/Cargo.lock +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/Cargo.toml +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/LICENSE +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/_core.pyi +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/animation/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/animation/clip.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/animation/player.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/animation/skeleton.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/app.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/audio/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/audio/null_driver.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/audio/openal_driver.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/audio/source.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/audio/system.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/backend.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/camera.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/collision/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/collision/epa.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/collision/heightfield.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/collision/layers.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/collision/raycast.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/constraints/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/constraints/base.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/constraints/joint_type.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/constraints/joints.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/contact/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/contact/solver.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/dynamics/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/dynamics/aba.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/dynamics/crba.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/dynamics/model.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/dynamics/rnea.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ecs/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ecs/bridge.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ecs/component.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ecs/serialization.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ecs/system.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ecs/transform.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/editor/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/editor/editor_app.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/editor/gizmo.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/editor/layout.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/errors.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/events.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/io/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/io/mesh_data.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/io/obj_loader.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/io/world_snapshot.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/logging.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/math/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/math/inertia.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/math/quaternion.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/math/se3.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/math/spatial.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/model/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/model/kinematics.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/model/robot_config.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/model/urdf_loader.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/particle/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/particle/presets.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/particle/system.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/profiler.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/py.typed +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/recorder.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/base.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/deferred/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/deferred/renderer.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/hq/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/hq/raytracer.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/hq/renderer.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/hq/scene.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/passes/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/passes/base.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/realtime/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/realtime/context.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/realtime/meshes.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/realtime/renderer.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/bloom_down.frag +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/bloom_up.frag +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/fullscreen.vert +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/gbuffer.frag +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/gbuffer.vert +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/lighting.frag +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/pbr.wgsl +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/shadow.frag +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/shadow.vert +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/ssao.frag +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/ssao_blur.frag +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/tonemap.frag +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/shaders/update_particles.comp +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/snapshot.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/wgpu_backend/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/wgpu_backend/pipeline.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/render/wgpu_backend/renderer.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/robot/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/robot/presets.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/robot/robot.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/scene/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/scene/manager.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/scene/prefab.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/sim/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/sim/domain_rand.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/sim/jax_batch.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ui/__init__.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ui/backend.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d/ui/canvas.py +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d_core/Cargo.toml +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d_core/benches/physics_bench.rs +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d_core/src/bvh.rs +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d_core/src/gjk_epa.rs +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d_core/src/lib.rs +0 -0
- {pyforge3d-2.1.0 → pyforge3d-2.1.1}/src/forge3d_core/src/math_simd.rs +0 -0
- {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.
|
|
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 —
|
|
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 —
|
|
62
|
+
> **Pure-Python 3D physics game engine — own dynamics, own rules, no compromises.**
|
|
63
63
|
|
|
64
64
|
[](https://pypi.org/project/pyforge3d/)
|
|
65
65
|
[](https://www.python.org/)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# forge3d
|
|
2
2
|
|
|
3
|
-
> **Pure-Python 3D game engine —
|
|
3
|
+
> **Pure-Python 3D physics game engine — own dynamics, own rules, no compromises.**
|
|
4
4
|
|
|
5
5
|
[](https://pypi.org/project/pyforge3d/)
|
|
6
6
|
[](https://www.python.org/)
|
|
@@ -6,8 +6,8 @@ build-backend = "maturin"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "pyforge3d"
|
|
9
|
-
version = "2.1.
|
|
10
|
-
description = "Pure-Python 3D physics game engine —
|
|
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
|
-
"
|
|
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.
|
|
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),
|
|
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
|
-
|
|
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
|
|
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
|
|
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[
|
|
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
|
-
"
|
|
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,
|
|
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)
|
|
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
|
-
"
|
|
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.
|
|
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
|
-
|
|
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=(
|
|
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.
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 /
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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));
|