vantagekit 0.6.1__py3-none-any.whl
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.
- vantagekit/__init__.py +7 -0
- vantagekit/core/__init__.py +32 -0
- vantagekit/core/bindings.py +232 -0
- vantagekit/core/frame.py +123 -0
- vantagekit/core/rig_math.py +129 -0
- vantagekit/core/smoothing.py +126 -0
- vantagekit/core/targets.py +93 -0
- vantagekit/kit/__init__.py +20 -0
- vantagekit/kit/controls.py +202 -0
- vantagekit/kit/events.py +53 -0
- vantagekit/kit/input.py +152 -0
- vantagekit/kit/native_nav.py +84 -0
- vantagekit/kit/overlay.py +95 -0
- vantagekit/kit/rig.py +346 -0
- vantagekit-0.6.1.dist-info/METADATA +84 -0
- vantagekit-0.6.1.dist-info/RECORD +18 -0
- vantagekit-0.6.1.dist-info/WHEEL +4 -0
- vantagekit-0.6.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""A small badge over the viewport, top left, naming the camera view.
|
|
2
|
+
|
|
3
|
+
Drawn into the active viewport window's own overlay frame (ViewportWindow.get_frame),
|
|
4
|
+
so it moves and resizes with the viewport. The frame gets a build function (the pattern
|
|
5
|
+
Kit's own overlays use), so Kit can rebuild it at any time, including after an extension
|
|
6
|
+
reload hands the same named frame to a new instance. Every element lets mouse events
|
|
7
|
+
through, so the badge never blocks picking or navigation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import carb
|
|
13
|
+
import omni.ui as ui
|
|
14
|
+
|
|
15
|
+
# ABGR colors
|
|
16
|
+
COLORS = {
|
|
17
|
+
"free": 0xFFBBBBBB,
|
|
18
|
+
"chase": 0xFF40D0FF,
|
|
19
|
+
"pov": 0xFF60FF90,
|
|
20
|
+
"orbit": 0xFFFFB050,
|
|
21
|
+
}
|
|
22
|
+
NAMES = {"free": "FREE", "chase": "CHASE", "pov": "POINT OF VIEW", "orbit": "ORBIT"}
|
|
23
|
+
BACKGROUND = 0x5A000000 # about 35 percent black: the scene shows through
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ViewOverlay:
|
|
27
|
+
def __init__(self, name: str = "vantagekit.view"):
|
|
28
|
+
self._frame = None
|
|
29
|
+
self._label = None
|
|
30
|
+
self._hint = None
|
|
31
|
+
self._state = ("free", "", "")
|
|
32
|
+
try:
|
|
33
|
+
from omni.kit.viewport.utility import get_active_viewport_window
|
|
34
|
+
|
|
35
|
+
win = get_active_viewport_window()
|
|
36
|
+
self._frame = win.get_frame(name) if win is not None else None
|
|
37
|
+
except Exception as e:
|
|
38
|
+
carb.log_warn(f"[vantagekit] no viewport overlay: {e}")
|
|
39
|
+
if self._frame is None:
|
|
40
|
+
return
|
|
41
|
+
self._frame.set_build_fn(self._build)
|
|
42
|
+
self._frame.rebuild()
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def ready(self) -> bool:
|
|
46
|
+
return self._frame is not None
|
|
47
|
+
|
|
48
|
+
def _build(self) -> None:
|
|
49
|
+
mode, title, hint = self._state
|
|
50
|
+
with ui.VStack(opaque_for_mouse_events=False):
|
|
51
|
+
ui.Spacer(height=36, opaque_for_mouse_events=False)
|
|
52
|
+
with ui.HStack(height=0, opaque_for_mouse_events=False):
|
|
53
|
+
ui.Spacer(width=16, opaque_for_mouse_events=False)
|
|
54
|
+
with ui.ZStack(width=0, height=0, opaque_for_mouse_events=False):
|
|
55
|
+
ui.Rectangle(style={"background_color": BACKGROUND, "border_radius": 4})
|
|
56
|
+
with ui.VStack(width=0, height=0, opaque_for_mouse_events=False):
|
|
57
|
+
with ui.HStack(height=0, opaque_for_mouse_events=False):
|
|
58
|
+
ui.Spacer(width=10, opaque_for_mouse_events=False)
|
|
59
|
+
self._label = ui.Label(title, width=0, style=self._title_style(mode))
|
|
60
|
+
ui.Spacer(width=10, opaque_for_mouse_events=False)
|
|
61
|
+
with ui.HStack(height=0, opaque_for_mouse_events=False):
|
|
62
|
+
ui.Spacer(width=10, opaque_for_mouse_events=False)
|
|
63
|
+
self._hint = ui.Label(
|
|
64
|
+
hint, width=0, visible=bool(hint), style={"font_size": 13, "color": 0xFFCCCCCC}
|
|
65
|
+
)
|
|
66
|
+
ui.Spacer(width=10, opaque_for_mouse_events=False)
|
|
67
|
+
ui.Spacer(height=4, opaque_for_mouse_events=False)
|
|
68
|
+
ui.Spacer(opaque_for_mouse_events=False)
|
|
69
|
+
ui.Spacer(opaque_for_mouse_events=False)
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _title_style(mode: str) -> dict:
|
|
73
|
+
return {"font_size": 22, "color": COLORS.get(mode, 0xFFFFFFFF)}
|
|
74
|
+
|
|
75
|
+
def show(self, mode: str, title: str, hint: str = "") -> None:
|
|
76
|
+
"""Update the badge; cheap to call every frame (it only changes on a new value)."""
|
|
77
|
+
state = (mode, title, hint)
|
|
78
|
+
if state == self._state and self._label is not None:
|
|
79
|
+
return
|
|
80
|
+
self._state = state
|
|
81
|
+
if self._label is None or self._hint is None:
|
|
82
|
+
if self._frame is not None:
|
|
83
|
+
self._frame.rebuild()
|
|
84
|
+
return
|
|
85
|
+
self._label.text = title
|
|
86
|
+
self._label.style = self._title_style(mode)
|
|
87
|
+
self._hint.text = hint
|
|
88
|
+
self._hint.visible = bool(hint)
|
|
89
|
+
|
|
90
|
+
def destroy(self) -> None:
|
|
91
|
+
if self._frame is not None:
|
|
92
|
+
# detach our build function so a later rebuild cannot call into this instance
|
|
93
|
+
self._frame.set_build_fn(lambda: None)
|
|
94
|
+
self._frame.clear()
|
|
95
|
+
self._frame = self._label = self._hint = None
|
vantagekit/kit/rig.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""CameraRig: drives one USD camera in free, chase, point-of-view and orbit modes.
|
|
2
|
+
|
|
3
|
+
The rig reads the stage up axis and meters per unit, so the same settings (meters and
|
|
4
|
+
seconds) work on Y-up and Z-up stages in any unit. In free mode it writes nothing and Kit's
|
|
5
|
+
native navigation moves the camera; in the attached modes it writes one cached
|
|
6
|
+
xformOp:transform per frame and the camera is locked against native navigation
|
|
7
|
+
(NativeNavigation). Mode changes are the only time settings or op order change.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import math
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Optional, Sequence, Tuple
|
|
15
|
+
|
|
16
|
+
import carb
|
|
17
|
+
import omni.usd
|
|
18
|
+
from pxr import Gf, Usd, UsdGeom
|
|
19
|
+
|
|
20
|
+
from ..core.frame import UpFrame, Vec3, add, scale
|
|
21
|
+
from ..core.rig_math import (
|
|
22
|
+
chase_orbit_view,
|
|
23
|
+
chase_perspective,
|
|
24
|
+
look_at_matrix,
|
|
25
|
+
orbit_eye,
|
|
26
|
+
orbit_from_eye,
|
|
27
|
+
pov_view,
|
|
28
|
+
)
|
|
29
|
+
from ..core.smoothing import Smoothed3, SmoothedAngle, wrap_angle
|
|
30
|
+
from ..core.targets import TargetCycler
|
|
31
|
+
from .native_nav import NativeNavigation
|
|
32
|
+
|
|
33
|
+
MODES = ("free", "chase", "pov", "orbit")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class RigSettings:
|
|
38
|
+
views: Tuple[str, ...] = ("chase", "pov", "orbit")
|
|
39
|
+
body_forward: Optional[Sequence[float]] = None # target's forward axis; None: +Z (Y up), +X (Z up)
|
|
40
|
+
# chase: the default perspective, behind and above, horizon level. The camera looks
|
|
41
|
+
# at the pivot chase_look_up_m above the target; orbit turns around the same pivot,
|
|
42
|
+
# and leaving orbit for chase keeps the perspective set there.
|
|
43
|
+
chase_back_m: float = 12.5
|
|
44
|
+
chase_up_m: float = 1.8
|
|
45
|
+
chase_look_up_m: float = 1.8
|
|
46
|
+
chase_tau_target_s: float = 0.05
|
|
47
|
+
chase_tau_heading_s: float = 0.35
|
|
48
|
+
# point of view: fixed in the target's body frame
|
|
49
|
+
pov_forward_m: float = 6.0
|
|
50
|
+
pov_up_m: float = 2.5
|
|
51
|
+
pov_right_m: float = 0.0
|
|
52
|
+
# orbit
|
|
53
|
+
orbit_tau_target_s: float = 0.15
|
|
54
|
+
orbit_range_m: float = 40.0
|
|
55
|
+
orbit_min_m: float = 2.0
|
|
56
|
+
orbit_max_m: float = 20000.0
|
|
57
|
+
orbit_elevation_deg: float = 20.0
|
|
58
|
+
orbit_min_elevation_deg: float = -10.0
|
|
59
|
+
orbit_max_elevation_deg: float = 85.0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CameraRig:
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
camera_path: str,
|
|
66
|
+
targets: TargetCycler,
|
|
67
|
+
settings: RigSettings | None = None,
|
|
68
|
+
native: NativeNavigation | None = None,
|
|
69
|
+
usd_context=None,
|
|
70
|
+
name: str = "vantagekit",
|
|
71
|
+
):
|
|
72
|
+
self.camera_path = str(camera_path)
|
|
73
|
+
self.targets = targets
|
|
74
|
+
self.settings = settings or RigSettings()
|
|
75
|
+
self.native = native or NativeNavigation()
|
|
76
|
+
self.name = name
|
|
77
|
+
self._ctx = usd_context or omni.usd.get_context()
|
|
78
|
+
self.mode = "free"
|
|
79
|
+
self.view = self.settings.views[0]
|
|
80
|
+
self.frame = UpFrame("Y")
|
|
81
|
+
self._op = None
|
|
82
|
+
self._target = Smoothed3(self.settings.chase_tau_target_s)
|
|
83
|
+
self._heading = SmoothedAngle(self.settings.chase_tau_heading_s)
|
|
84
|
+
self._last_heading = 0.0
|
|
85
|
+
self.orbit_yaw = 0.0
|
|
86
|
+
self.orbit_elevation = math.radians(self.settings.orbit_elevation_deg)
|
|
87
|
+
self.orbit_range_m = self.settings.orbit_range_m
|
|
88
|
+
# Chase perspective, relative to the target's heading: (yaw, elevation, meters).
|
|
89
|
+
# Leaving orbit for chase keeps the perspective set in orbit; home() resets it.
|
|
90
|
+
self.chase_yaw_rel, self.chase_elevation, self.chase_range_m = self.default_chase()
|
|
91
|
+
self.home_view: Optional[Tuple[Vec3, Vec3]] = None # free-mode home (eye, look)
|
|
92
|
+
|
|
93
|
+
def default_chase(self) -> Tuple[float, float, float]:
|
|
94
|
+
s = self.settings
|
|
95
|
+
return chase_perspective(s.chase_back_m, s.chase_up_m, s.chase_look_up_m)
|
|
96
|
+
|
|
97
|
+
# stage ----------------------------------------------------------------------
|
|
98
|
+
@property
|
|
99
|
+
def stage(self) -> Usd.Stage | None:
|
|
100
|
+
return self._ctx.get_stage()
|
|
101
|
+
|
|
102
|
+
def refresh_frame(self) -> UpFrame:
|
|
103
|
+
stage = self.stage
|
|
104
|
+
if stage is not None:
|
|
105
|
+
self.frame = UpFrame(
|
|
106
|
+
str(UsdGeom.GetStageUpAxis(stage)),
|
|
107
|
+
UsdGeom.GetStageMetersPerUnit(stage),
|
|
108
|
+
self.settings.body_forward,
|
|
109
|
+
)
|
|
110
|
+
return self.frame
|
|
111
|
+
|
|
112
|
+
def reset_stage(self) -> None:
|
|
113
|
+
"""Call after a stage opens: drops cached ops and returns to free mode."""
|
|
114
|
+
self._op = None
|
|
115
|
+
self.mode = "free"
|
|
116
|
+
self.native.reset()
|
|
117
|
+
self.refresh_frame()
|
|
118
|
+
self.native.set_native(True, self.stage, self.camera_path)
|
|
119
|
+
|
|
120
|
+
def _prim(self):
|
|
121
|
+
stage = self.stage
|
|
122
|
+
return stage.GetPrimAtPath(self.camera_path) if stage else None
|
|
123
|
+
|
|
124
|
+
def eye(self) -> Vec3 | None:
|
|
125
|
+
prim = self._prim()
|
|
126
|
+
if not prim or not prim.IsValid():
|
|
127
|
+
return None
|
|
128
|
+
t = UsdGeom.Xformable(prim).ComputeLocalToWorldTransform(Usd.TimeCode.Default()).ExtractTranslation()
|
|
129
|
+
return (t[0], t[1], t[2])
|
|
130
|
+
|
|
131
|
+
# modes ----------------------------------------------------------------------
|
|
132
|
+
@property
|
|
133
|
+
def attached(self) -> bool:
|
|
134
|
+
return self.mode != "free"
|
|
135
|
+
|
|
136
|
+
def set_mode(self, mode: str) -> str:
|
|
137
|
+
if mode not in MODES:
|
|
138
|
+
raise ValueError(f"mode must be one of {MODES}, not {mode!r}")
|
|
139
|
+
if mode == self.mode:
|
|
140
|
+
return self.mode
|
|
141
|
+
stage = self.stage
|
|
142
|
+
if mode != "free":
|
|
143
|
+
prim = self._prim()
|
|
144
|
+
if not prim or not prim.IsValid():
|
|
145
|
+
carb.log_warn(f"[{self.name}] no camera at {self.camera_path}; staying in {self.mode}")
|
|
146
|
+
return self.mode
|
|
147
|
+
if self.targets.ensure(near=self.eye()) is None:
|
|
148
|
+
carb.log_warn(f"[{self.name}] no target to attach to; staying in {self.mode}")
|
|
149
|
+
return self.mode
|
|
150
|
+
self.refresh_frame()
|
|
151
|
+
if self.mode == "free":
|
|
152
|
+
self._prepare_op(prim)
|
|
153
|
+
self._activate_camera()
|
|
154
|
+
self.view = mode
|
|
155
|
+
self._enter(mode, self.mode)
|
|
156
|
+
self.mode = mode
|
|
157
|
+
self.native.set_native(mode == "free", stage, self.camera_path)
|
|
158
|
+
carb.log_info(f"[{self.name}] camera mode {mode} (target {self.targets.selected})")
|
|
159
|
+
return self.mode
|
|
160
|
+
|
|
161
|
+
def step_target(self, direction: int):
|
|
162
|
+
return self.targets.step(direction)
|
|
163
|
+
|
|
164
|
+
def _pivot(self, target: Sequence[float]) -> Vec3:
|
|
165
|
+
"""Chase and orbit both turn around this point above the target, so switching
|
|
166
|
+
between them does not move the camera."""
|
|
167
|
+
f = self.frame
|
|
168
|
+
return add(target, scale(f.up, f.to_units(self.settings.chase_look_up_m)))
|
|
169
|
+
|
|
170
|
+
def _enter(self, mode: str, prev: str) -> None:
|
|
171
|
+
s = self.settings
|
|
172
|
+
pose = self.targets.pose()
|
|
173
|
+
if pose is None:
|
|
174
|
+
return
|
|
175
|
+
# Keep the smoothed target and heading when moving between attached views
|
|
176
|
+
if prev == "free" or self._target.value is None:
|
|
177
|
+
self._target.snap(pose.position)
|
|
178
|
+
h = self.frame.heading(pose.orientation)
|
|
179
|
+
self._last_heading = h if h is not None else self._last_heading
|
|
180
|
+
self._heading.snap(self._last_heading)
|
|
181
|
+
self._target.tau = s.orbit_tau_target_s if mode == "orbit" else s.chase_tau_target_s
|
|
182
|
+
heading = self._heading.value if self._heading.value is not None else self._last_heading
|
|
183
|
+
# Chase and orbit share one vantage (angle from the heading, elevation, range).
|
|
184
|
+
# Point of view is a fixed seat on the target and never changes it, so chase, pov,
|
|
185
|
+
# orbit returns to the chase vantage instead of orbiting from the seat's eye (which
|
|
186
|
+
# started the orbit a few meters out, 2026-11-08).
|
|
187
|
+
if prev == "orbit" and mode in ("chase", "pov"):
|
|
188
|
+
# keep the perspective set in orbit, now turning with the target's heading
|
|
189
|
+
self.chase_yaw_rel = wrap_angle(self.orbit_yaw - heading)
|
|
190
|
+
self.chase_elevation = self.orbit_elevation
|
|
191
|
+
self.chase_range_m = self.orbit_range_m
|
|
192
|
+
if mode == "orbit":
|
|
193
|
+
if prev in ("chase", "pov"):
|
|
194
|
+
# start the orbit at the chase vantage
|
|
195
|
+
self.orbit_yaw = heading + self.chase_yaw_rel
|
|
196
|
+
self._set_elevation(self.chase_elevation)
|
|
197
|
+
self._set_range_m(self.chase_range_m)
|
|
198
|
+
else:
|
|
199
|
+
eye = self.eye()
|
|
200
|
+
if eye is not None:
|
|
201
|
+
yaw, elev, dist = orbit_from_eye(self._pivot(self._target.value), eye, self.frame)
|
|
202
|
+
self.orbit_yaw = yaw
|
|
203
|
+
self._set_elevation(elev)
|
|
204
|
+
self._set_range_m(dist * self.frame.meters_per_unit)
|
|
205
|
+
|
|
206
|
+
def home(self) -> None:
|
|
207
|
+
"""Attached: back to the default chase perspective (behind the target), in chase
|
|
208
|
+
and in orbit. Free: the saved home view, if any."""
|
|
209
|
+
if not self.attached:
|
|
210
|
+
if self.home_view is not None:
|
|
211
|
+
self.place(*self.home_view)
|
|
212
|
+
return
|
|
213
|
+
self.chase_yaw_rel, self.chase_elevation, self.chase_range_m = self.default_chase()
|
|
214
|
+
heading = self._heading.value if self._heading.value is not None else self._last_heading
|
|
215
|
+
self.orbit_yaw = heading + self.chase_yaw_rel
|
|
216
|
+
self._set_elevation(self.chase_elevation)
|
|
217
|
+
self._set_range_m(self.chase_range_m)
|
|
218
|
+
|
|
219
|
+
# orbit input ------------------------------------------------------------------
|
|
220
|
+
def _set_elevation(self, e: float) -> None:
|
|
221
|
+
lo = math.radians(self.settings.orbit_min_elevation_deg)
|
|
222
|
+
hi = math.radians(self.settings.orbit_max_elevation_deg)
|
|
223
|
+
self.orbit_elevation = max(lo, min(hi, e))
|
|
224
|
+
|
|
225
|
+
def _set_range_m(self, r: float) -> None:
|
|
226
|
+
self.orbit_range_m = max(self.settings.orbit_min_m, min(self.settings.orbit_max_m, r))
|
|
227
|
+
|
|
228
|
+
def orbit_by(self, d_yaw: float = 0.0, d_elevation: float = 0.0, range_factor: float = 1.0) -> None:
|
|
229
|
+
self.orbit_yaw += d_yaw
|
|
230
|
+
self._set_elevation(self.orbit_elevation + d_elevation)
|
|
231
|
+
self._set_range_m(self.orbit_range_m * range_factor)
|
|
232
|
+
|
|
233
|
+
# per frame --------------------------------------------------------------------
|
|
234
|
+
def update(self, dt: float) -> bool:
|
|
235
|
+
"""Advance one frame; returns True when the camera was written."""
|
|
236
|
+
if not self.attached:
|
|
237
|
+
return False
|
|
238
|
+
pose = self.targets.pose()
|
|
239
|
+
if pose is None:
|
|
240
|
+
return False
|
|
241
|
+
f = self.frame
|
|
242
|
+
s = self.settings
|
|
243
|
+
h = f.heading(pose.orientation)
|
|
244
|
+
if h is not None:
|
|
245
|
+
self._last_heading = h
|
|
246
|
+
target = self._target.update(pose.position, dt)
|
|
247
|
+
heading = self._heading.update(self._last_heading, dt)
|
|
248
|
+
if self.mode == "chase":
|
|
249
|
+
eye, look, up = chase_orbit_view(
|
|
250
|
+
target,
|
|
251
|
+
heading,
|
|
252
|
+
self.chase_yaw_rel,
|
|
253
|
+
self.chase_elevation,
|
|
254
|
+
f.to_units(self.chase_range_m),
|
|
255
|
+
f.to_units(s.chase_look_up_m),
|
|
256
|
+
f,
|
|
257
|
+
)
|
|
258
|
+
elif self.mode == "pov":
|
|
259
|
+
eye, look, up = pov_view(
|
|
260
|
+
pose.position,
|
|
261
|
+
pose.orientation,
|
|
262
|
+
f.to_units(s.pov_forward_m),
|
|
263
|
+
f.to_units(s.pov_up_m),
|
|
264
|
+
f.to_units(s.pov_right_m),
|
|
265
|
+
f,
|
|
266
|
+
)
|
|
267
|
+
else: # orbit
|
|
268
|
+
look, up = self._pivot(target), f.up
|
|
269
|
+
eye = orbit_eye(look, self.orbit_yaw, self.orbit_elevation, f.to_units(self.orbit_range_m), f)
|
|
270
|
+
return self._write(look_at_matrix(eye, look, up))
|
|
271
|
+
|
|
272
|
+
def place(self, eye: Sequence[float], look: Sequence[float]) -> bool:
|
|
273
|
+
"""Put the camera at eye looking at look (stage units), in any mode."""
|
|
274
|
+
prim = self._prim()
|
|
275
|
+
if not prim or not prim.IsValid():
|
|
276
|
+
return False
|
|
277
|
+
if self._op is None or not self._op_valid(prim):
|
|
278
|
+
self._prepare_op(prim)
|
|
279
|
+
return self._write(look_at_matrix(eye, look, self.refresh_frame().up))
|
|
280
|
+
|
|
281
|
+
# USD ----------------------------------------------------------------------------
|
|
282
|
+
def _activate_camera(self) -> None:
|
|
283
|
+
try:
|
|
284
|
+
from omni.kit.viewport.utility import get_active_viewport
|
|
285
|
+
|
|
286
|
+
vp = get_active_viewport()
|
|
287
|
+
if vp is not None and str(vp.camera_path) != self.camera_path:
|
|
288
|
+
vp.camera_path = self.camera_path
|
|
289
|
+
except Exception as e:
|
|
290
|
+
carb.log_warn(f"[{self.name}] could not switch the viewport camera: {e}")
|
|
291
|
+
|
|
292
|
+
def _op_valid(self, prim) -> bool:
|
|
293
|
+
ops = UsdGeom.Xformable(prim).GetOrderedXformOps()
|
|
294
|
+
return len(ops) == 1 and ops[0].GetOpType() == UsdGeom.XformOp.TypeTransform
|
|
295
|
+
|
|
296
|
+
def _prepare_op(self, prim) -> None:
|
|
297
|
+
"""One xformOp:transform holding the camera's current local transform, so taking
|
|
298
|
+
over the camera does not move it."""
|
|
299
|
+
xf = UsdGeom.Xformable(prim)
|
|
300
|
+
local = xf.GetLocalTransformation(Usd.TimeCode.Default())
|
|
301
|
+
if self._op_valid(prim):
|
|
302
|
+
self._op = xf.GetOrderedXformOps()[0]
|
|
303
|
+
return
|
|
304
|
+
xf.ClearXformOpOrder()
|
|
305
|
+
self._op = xf.AddTransformOp()
|
|
306
|
+
self._op.Set(local)
|
|
307
|
+
|
|
308
|
+
def _write(self, rows) -> bool:
|
|
309
|
+
prim = self._prim()
|
|
310
|
+
if not prim or not prim.IsValid():
|
|
311
|
+
return False
|
|
312
|
+
if self._op is None or not self._op.GetAttr().IsValid():
|
|
313
|
+
self._prepare_op(prim)
|
|
314
|
+
world = Gf.Matrix4d(*[c for row in rows for c in row])
|
|
315
|
+
parent = prim.GetParent()
|
|
316
|
+
if parent and parent.IsValid() and not parent.IsPseudoRoot():
|
|
317
|
+
pw = UsdGeom.Xformable(parent).ComputeLocalToWorldTransform(Usd.TimeCode.Default())
|
|
318
|
+
world = world * pw.GetInverse()
|
|
319
|
+
self._op.Set(world)
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
def status(self) -> dict:
|
|
323
|
+
return {
|
|
324
|
+
"mode": self.mode,
|
|
325
|
+
"view": self.view,
|
|
326
|
+
"target": self.targets.selected,
|
|
327
|
+
"camera": self.camera_path,
|
|
328
|
+
"up_axis": self.frame.axis,
|
|
329
|
+
"meters_per_unit": self.frame.meters_per_unit,
|
|
330
|
+
"eye": self.eye(),
|
|
331
|
+
"orbit": {
|
|
332
|
+
"yaw_deg": math.degrees(self.orbit_yaw),
|
|
333
|
+
"elevation_deg": math.degrees(self.orbit_elevation),
|
|
334
|
+
"range_m": self.orbit_range_m,
|
|
335
|
+
},
|
|
336
|
+
"chase": {
|
|
337
|
+
"yaw_rel_deg": math.degrees(self.chase_yaw_rel),
|
|
338
|
+
"elevation_deg": math.degrees(self.chase_elevation),
|
|
339
|
+
"range_m": self.chase_range_m,
|
|
340
|
+
},
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
def dispose(self) -> None:
|
|
344
|
+
self.native.dispose(self.stage, self.camera_path)
|
|
345
|
+
self._op = None
|
|
346
|
+
self.mode = "free"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vantagekit
|
|
3
|
+
Version: 0.6.1
|
|
4
|
+
Summary: Camera and input controls for viewers of terrain, ocean and vessel scenes: orbit, chase and point-of-view camera math, frame-rate independent smoothing, and bindings as data, with Omniverse Kit adapters.
|
|
5
|
+
Project-URL: Demo, https://lhzn-io.github.io/vantagekit/demo/
|
|
6
|
+
Project-URL: Issues, https://github.com/lhzn-io/vantagekit/issues
|
|
7
|
+
Project-URL: Source, https://github.com/lhzn-io/vantagekit
|
|
8
|
+
Author-email: Long Horizon Observatory <dfry@lhzn.io>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Long Horizon Observatory
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: camera,gamepad,input,navigation,omniverse,viewer
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: Intended Audience :: Science/Research
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Topic :: Multimedia :: Graphics :: 3D Rendering
|
|
38
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
39
|
+
Requires-Python: >=3.10
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# vantagekit (Python)
|
|
43
|
+
|
|
44
|
+
Camera and input controls for viewers of terrain, ocean and vessel scenes. This package is the
|
|
45
|
+
Python half of [vantagekit](https://github.com/lhzn-io/vantagekit), whose JavaScript modules give
|
|
46
|
+
three.js and MapLibre GL JS viewers the same navigation in the browser
|
|
47
|
+
([live demo](https://lhzn-io.github.io/vantagekit/demo/)).
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install vantagekit
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## vantagekit.core
|
|
54
|
+
|
|
55
|
+
Standard library only, so it runs anywhere Python 3.10 or later does: in tests, in tools, or in a
|
|
56
|
+
viewer built on any renderer.
|
|
57
|
+
|
|
58
|
+
- **Stage frames** for Y-up and Z-up scenes in any unit (`UpFrame`), with headings and yaw from
|
|
59
|
+
orientations.
|
|
60
|
+
- **Camera math** for orbit, chase and point-of-view cameras around a moving target (`orbit_eye`,
|
|
61
|
+
`look_at_matrix`). Every distance is in metres and every time constant in seconds.
|
|
62
|
+
- **Smoothing** that does not depend on frame rate (`Smoothed3`, `SmoothedAngle`, `exp_alpha`):
|
|
63
|
+
the filter is solved exactly for a target moving steadily between frames, so the lag behind a
|
|
64
|
+
moving target is its speed times the time constant at any frame time.
|
|
65
|
+
- **Bindings as data** (`Binding`, `DEFAULT_BINDINGS`): press, hold, axis and delta kinds, exact
|
|
66
|
+
modifiers, and ordered layers, so an app layer can take the sticks from the camera while it is on.
|
|
67
|
+
- **Target selection** (`CallbackTargets`, `Pose`).
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from vantagekit.core import SmoothedAngle, UpFrame, orbit_eye
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## vantagekit.kit
|
|
74
|
+
|
|
75
|
+
Adapters for Omniverse Kit 109 and 110 (Isaac Sim 6): a camera rig with free, chase,
|
|
76
|
+
point-of-view and orbit modes, a handover to Kit's own navigation, a keyboard, mouse and gamepad
|
|
77
|
+
input router, and a view badge. These import `carb` and `omni` and so work only inside Kit. A Kit
|
|
78
|
+
app usually loads them as the `lhzn.vantagekit` extension from the repository's `kit/exts` folder;
|
|
79
|
+
see the [main README](https://github.com/lhzn-io/vantagekit#omniverse-kit).
|
|
80
|
+
|
|
81
|
+
## Versions
|
|
82
|
+
|
|
83
|
+
The package version follows the repository's release tags, so `vantagekit==0.6.1` matches the
|
|
84
|
+
JavaScript modules served from `@v0.6.1`. MIT licence, Long Horizon Observatory.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
vantagekit/__init__.py,sha256=Dxr_68qGe3KmAbZyw2XycRuhhk6KdZ-uQ6JDhCptMfQ,224
|
|
2
|
+
vantagekit/core/__init__.py,sha256=BRDw-oV58f_ug6BPyA7awTfvVXu2HLzamYe7PrONNis,885
|
|
3
|
+
vantagekit/core/bindings.py,sha256=ieEvbEFXcajBeG61-mFr-raM7bgJ2cghJrh25giO3W4,9165
|
|
4
|
+
vantagekit/core/frame.py,sha256=TJ7tPOGebaKWNBMPxz-rXQA-Z-UHaQ5t61uNNcu0PWM,3925
|
|
5
|
+
vantagekit/core/rig_math.py,sha256=rB5h99daqe6jTfgLkblgLMGsUtSKYBoyeDeTxV8f6_0,4378
|
|
6
|
+
vantagekit/core/smoothing.py,sha256=sX49eI6icS3y5B8cByMp16U5__lSUKzYyQa7gGUbres,4552
|
|
7
|
+
vantagekit/core/targets.py,sha256=0K5JYo0yMjpWXkKQYwpVwSi_Ywp4CuD5x1rFblPlSGM,3009
|
|
8
|
+
vantagekit/kit/__init__.py,sha256=MQe8Uv4lOmCBoyJLkD1Js37_P5ZmalQqN8EKjsM4tcU,569
|
|
9
|
+
vantagekit/kit/controls.py,sha256=D4NUVa5xPpog9Q78WSf8Phin1zzzDcveD9C076mZRrQ,8164
|
|
10
|
+
vantagekit/kit/events.py,sha256=VB1nsbFU6qYf1AkpiKgtKySURO9ZuLgHK2sEhkq9X8s,1797
|
|
11
|
+
vantagekit/kit/input.py,sha256=RYRpOGo2uq46BASsA__SIagn9ngEInm4rI-kIIRZ7Z0,6584
|
|
12
|
+
vantagekit/kit/native_nav.py,sha256=oOHn2ZD8_1oIToBPsE36tHO3pg8aHtCOiuZDKmzGgF4,3542
|
|
13
|
+
vantagekit/kit/overlay.py,sha256=-byQznozLt6SS_tn-FF5qFz5hx2YjNZhiQhkFTdH9ao,4132
|
|
14
|
+
vantagekit/kit/rig.py,sha256=t-khZBK9eoUTffi8hhzg0xv4Z6pjvWYLU6hUCe8eLOw,14376
|
|
15
|
+
vantagekit-0.6.1.dist-info/METADATA,sha256=liTqVJCX-gF4Rf7d4-NOwYsAMvSK-59sNk9XQZVemWg,4344
|
|
16
|
+
vantagekit-0.6.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
17
|
+
vantagekit-0.6.1.dist-info/licenses/LICENSE,sha256=VqXVp0x0MUmTZo0dT3YluvQiXyrUVAIhd5gEv_BdA28,1081
|
|
18
|
+
vantagekit-0.6.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Long Horizon Observatory
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|