优化编辑器场景灯光效果;层级树选中时父级递归展开保证能看到选中项;更新并美化底部控制台形式;修复TransformGizmo的ctrl+z撤回功能;接入中英文字体;升级并美化Inspector检查器面板;集成右上角场景相机旋转变换小组件
This commit is contained in:
parent
4f5eaeb92b
commit
785aafb094
BIN
fonts/FontAwesome7Free-Solid-900.otf
Normal file
BIN
fonts/FontAwesome7Free-Solid-900.otf
Normal file
Binary file not shown.
BIN
fonts/monaco.ttf
Normal file
BIN
fonts/monaco.ttf
Normal file
Binary file not shown.
BIN
fonts/msyh.ttc
Normal file
BIN
fonts/msyh.ttc
Normal file
Binary file not shown.
27
src/camera_view_gizmo/README.md
Normal file
27
src/camera_view_gizmo/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Camera View Gizmo
|
||||
|
||||
Independent Panda3D camera navigation widget.
|
||||
|
||||
Features:
|
||||
|
||||
- top-right overlay widget
|
||||
- six axis-aligned camera views
|
||||
- configurable smooth transition, default `0.2s`
|
||||
- perspective / orthographic toggle button
|
||||
- no dependency on ImGui or this project's transform gizmo
|
||||
|
||||
Minimal usage:
|
||||
|
||||
```python
|
||||
from src.plugins.camera_view_gizmo import CameraViewGizmo, CameraViewGizmoConfig
|
||||
|
||||
gizmo = CameraViewGizmo(
|
||||
base,
|
||||
camera_np=base.camera,
|
||||
get_pivot=lambda: (0, 0, 0),
|
||||
config=CameraViewGizmoConfig(),
|
||||
)
|
||||
```
|
||||
|
||||
For hosts that keep their own orbit-controller state, pass an
|
||||
`on_camera_changed` callback and sync the host controller from the camera pose.
|
||||
9
src/camera_view_gizmo/__init__.py
Normal file
9
src/camera_view_gizmo/__init__.py
Normal file
@ -0,0 +1,9 @@
|
||||
from .camera_view_gizmo import CameraViewGizmo, ProjectionMode, ViewFace
|
||||
from .config import CameraViewGizmoConfig
|
||||
|
||||
__all__ = [
|
||||
"CameraViewGizmo",
|
||||
"CameraViewGizmoConfig",
|
||||
"ProjectionMode",
|
||||
"ViewFace",
|
||||
]
|
||||
777
src/camera_view_gizmo/camera_view_gizmo.py
Normal file
777
src/camera_view_gizmo/camera_view_gizmo.py
Normal file
@ -0,0 +1,777 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Dict, Optional
|
||||
|
||||
import panda3d.core as p3d
|
||||
from direct.showbase.DirectObject import DirectObject
|
||||
from direct.showbase.ShowBase import ShowBase
|
||||
from direct.showbase.ShowBaseGlobal import globalClock
|
||||
from direct.task import Task
|
||||
|
||||
from .config import CameraViewGizmoConfig
|
||||
|
||||
HAS_IMGUI = False
|
||||
if importlib.util.find_spec("imgui_bundle"):
|
||||
from imgui_bundle import imgui
|
||||
HAS_IMGUI = True
|
||||
|
||||
|
||||
class ViewFace:
|
||||
POS_X = "+x"
|
||||
NEG_X = "-x"
|
||||
POS_Y = "+y"
|
||||
NEG_Y = "-y"
|
||||
POS_Z = "+z"
|
||||
NEG_Z = "-z"
|
||||
|
||||
ALL = (POS_X, NEG_X, POS_Y, NEG_Y, POS_Z, NEG_Z)
|
||||
|
||||
|
||||
class ProjectionMode:
|
||||
PERSPECTIVE = "perspective"
|
||||
ORTHOGRAPHIC = "orthographic"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _EndpointVisual:
|
||||
face: str
|
||||
axis: p3d.Vec3
|
||||
color: p3d.Vec4
|
||||
label: str
|
||||
root: p3d.NodePath
|
||||
card: p3d.NodePath
|
||||
text: p3d.NodePath
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _TransitionState:
|
||||
start_pos: p3d.Point3
|
||||
end_pos: p3d.Point3
|
||||
start_pivot: p3d.Point3
|
||||
end_pivot: p3d.Point3
|
||||
start_up: p3d.Vec3
|
||||
end_up: p3d.Vec3
|
||||
duration: float
|
||||
elapsed: float = 0.0
|
||||
|
||||
|
||||
class CameraViewGizmo(DirectObject):
|
||||
"""
|
||||
Independent Panda3D camera navigation widget.
|
||||
|
||||
It renders a small overlay in the top-right corner, mirrors the main
|
||||
camera orientation, supports six axis-aligned views, and can toggle the
|
||||
main camera lens between perspective and orthographic projection.
|
||||
"""
|
||||
|
||||
FACE_AXIS: Dict[str, p3d.Vec3] = {
|
||||
ViewFace.POS_X: p3d.Vec3(1, 0, 0),
|
||||
ViewFace.NEG_X: p3d.Vec3(-1, 0, 0),
|
||||
ViewFace.POS_Y: p3d.Vec3(0, 1, 0),
|
||||
ViewFace.NEG_Y: p3d.Vec3(0, -1, 0),
|
||||
ViewFace.POS_Z: p3d.Vec3(0, 0, 1),
|
||||
ViewFace.NEG_Z: p3d.Vec3(0, 0, -1),
|
||||
}
|
||||
|
||||
FACE_LABEL: Dict[str, str] = {
|
||||
ViewFace.POS_X: "+X",
|
||||
ViewFace.NEG_X: "-X",
|
||||
ViewFace.POS_Y: "+Y",
|
||||
ViewFace.NEG_Y: "-Y",
|
||||
ViewFace.POS_Z: "+Z",
|
||||
ViewFace.NEG_Z: "-Z",
|
||||
}
|
||||
|
||||
OPPOSITE_FACE: Dict[str, str] = {
|
||||
ViewFace.POS_X: ViewFace.NEG_X,
|
||||
ViewFace.NEG_X: ViewFace.POS_X,
|
||||
ViewFace.POS_Y: ViewFace.NEG_Y,
|
||||
ViewFace.NEG_Y: ViewFace.POS_Y,
|
||||
ViewFace.POS_Z: ViewFace.NEG_Z,
|
||||
ViewFace.NEG_Z: ViewFace.POS_Z,
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base: ShowBase,
|
||||
camera_np: Optional[p3d.NodePath] = None,
|
||||
lens: Optional[p3d.Lens] = None,
|
||||
*,
|
||||
window: Optional[p3d.GraphicsOutput] = None,
|
||||
config: Optional[CameraViewGizmoConfig] = None,
|
||||
get_pivot: Optional[Callable[[], p3d.LPoint3f | p3d.LPoint3d | p3d.LVecBase3f | tuple[float, float, float]]] = None,
|
||||
on_camera_changed: Optional[Callable[["CameraViewGizmo"], None]] = None,
|
||||
interaction_blocker: Optional[Callable[[], bool]] = None,
|
||||
register_events: bool = True,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.base = base
|
||||
self.camera_np = camera_np or getattr(base, "camera", None) or getattr(base, "cam", None)
|
||||
if self.camera_np is None:
|
||||
raise RuntimeError("CameraViewGizmo requires a valid Panda3D camera transform NodePath")
|
||||
self.camera_lens_np = self._resolve_camera_lens_np(self.camera_np) or getattr(base, "cam", None)
|
||||
if self.camera_lens_np is None or self.camera_lens_np.isEmpty():
|
||||
raise RuntimeError("CameraViewGizmo could not resolve a Panda3D Camera node with lens")
|
||||
self.camera_node: p3d.Camera = self.camera_lens_np.node()
|
||||
self.render = base.render
|
||||
self.window = window or base.win
|
||||
self.config = config or CameraViewGizmoConfig()
|
||||
self.get_pivot = get_pivot or (lambda: p3d.Point3(0, 0, 0))
|
||||
self.on_camera_changed = on_camera_changed
|
||||
self.interaction_blocker = interaction_blocker
|
||||
self._events_registered = bool(register_events)
|
||||
self._projection_icon_hovered = False
|
||||
self._hover_face: str | None = None
|
||||
self._pointer_inside_widget = False
|
||||
self._pointer_pressed_on_widget = False
|
||||
self._pending_pointer_xy: tuple[float, float] | None = None
|
||||
self._last_region_rect: tuple[float, float, float, float] | None = None
|
||||
self._transition: _TransitionState | None = None
|
||||
self._stored_perspective_lens: p3d.Lens | None = None
|
||||
self._lens = lens or self.camera_node.getLens()
|
||||
self._projection_mode = self._detect_projection_mode(self._lens)
|
||||
|
||||
self._widget_root = p3d.NodePath("camera_view_gizmo_render")
|
||||
self._model_root = self._widget_root.attachNewNode("camera_view_gizmo_root")
|
||||
self._ui_root = self._widget_root.attachNewNode("camera_view_gizmo_ui")
|
||||
self._overlay_camera_np: p3d.NodePath | None = None
|
||||
self._overlay_dr: p3d.DisplayRegion | None = None
|
||||
self._axis_line_nodes: dict[str, p3d.NodePath] = {}
|
||||
self._endpoints: dict[str, _EndpointVisual] = {}
|
||||
self._center_perspective_np: p3d.NodePath | None = None
|
||||
self._center_orthographic_np: p3d.NodePath | None = None
|
||||
|
||||
self._setup_overlay()
|
||||
self._build_visuals()
|
||||
if self._events_registered:
|
||||
self._register_events()
|
||||
|
||||
self.base.taskMgr.add(self._update_task, "camera-view-gizmo-update")
|
||||
|
||||
def _resolve_camera_lens_np(self, node_path: p3d.NodePath | None) -> p3d.NodePath | None:
|
||||
if node_path is None or node_path.isEmpty():
|
||||
return None
|
||||
try:
|
||||
if hasattr(node_path.node(), "getLens"):
|
||||
return node_path
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
candidate = node_path.find("**/+Camera")
|
||||
if candidate is not None and not candidate.isEmpty():
|
||||
return candidate
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_hovering(self) -> bool:
|
||||
return self._pointer_inside_widget
|
||||
|
||||
@property
|
||||
def captures_mouse(self) -> bool:
|
||||
return self._pointer_inside_widget or self._pointer_pressed_on_widget
|
||||
|
||||
@property
|
||||
def projection_mode(self) -> str:
|
||||
return self._projection_mode
|
||||
|
||||
def destroy(self) -> None:
|
||||
self.ignoreAll()
|
||||
self.base.taskMgr.remove("camera-view-gizmo-update")
|
||||
if self._overlay_dr is not None and self.window is not None:
|
||||
self.window.removeDisplayRegion(self._overlay_dr)
|
||||
self._overlay_dr = None
|
||||
if self._widget_root is not None:
|
||||
self._widget_root.removeNode()
|
||||
self._widget_root = None
|
||||
|
||||
def _is_interaction_blocked(self) -> bool:
|
||||
blocker = self.interaction_blocker
|
||||
if blocker is not None:
|
||||
try:
|
||||
return bool(blocker())
|
||||
except Exception:
|
||||
pass
|
||||
if HAS_IMGUI:
|
||||
try:
|
||||
io = imgui.get_io()
|
||||
if io and io.want_capture_mouse:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
def snap_to_face(self, face: str, duration: float | None = None) -> None:
|
||||
face = str(face).lower()
|
||||
if face not in self.FACE_AXIS:
|
||||
raise ValueError(f"Unsupported view face: {face}")
|
||||
|
||||
pivot = self._get_pivot_point()
|
||||
axis = p3d.Vec3(self.FACE_AXIS[face])
|
||||
current_pos = self.camera_np.getPos(self.render)
|
||||
distance = max((current_pos - pivot).length(), 0.5)
|
||||
end_pos = pivot + axis * distance
|
||||
start_up = self.camera_np.getQuat(self.render).getUp()
|
||||
end_up = self._up_vector_for_face(face)
|
||||
|
||||
actual_duration = self.config.transition_duration if duration is None else max(0.0, float(duration))
|
||||
if actual_duration <= 1e-4:
|
||||
self._transition = None
|
||||
self._apply_camera_pose(end_pos, pivot, end_up)
|
||||
return
|
||||
|
||||
self._transition = _TransitionState(
|
||||
start_pos=p3d.Point3(current_pos),
|
||||
end_pos=p3d.Point3(end_pos),
|
||||
start_pivot=p3d.Point3(pivot),
|
||||
end_pivot=p3d.Point3(pivot),
|
||||
start_up=p3d.Vec3(start_up),
|
||||
end_up=p3d.Vec3(end_up),
|
||||
duration=actual_duration,
|
||||
)
|
||||
|
||||
def toggle_projection(self) -> None:
|
||||
if self._projection_mode == ProjectionMode.PERSPECTIVE:
|
||||
self.set_projection_mode(ProjectionMode.ORTHOGRAPHIC)
|
||||
else:
|
||||
self.set_projection_mode(ProjectionMode.PERSPECTIVE)
|
||||
|
||||
def set_projection_mode(self, mode: str) -> None:
|
||||
mode = str(mode).lower()
|
||||
if mode == self._projection_mode:
|
||||
return
|
||||
if mode == ProjectionMode.ORTHOGRAPHIC:
|
||||
self._switch_to_orthographic()
|
||||
elif mode == ProjectionMode.PERSPECTIVE:
|
||||
self._switch_to_perspective()
|
||||
else:
|
||||
raise ValueError(f"Unsupported projection mode: {mode}")
|
||||
self._projection_mode = mode
|
||||
self._notify_camera_changed()
|
||||
|
||||
def _setup_overlay(self) -> None:
|
||||
if self.window is None:
|
||||
raise RuntimeError("CameraViewGizmo requires an active Panda3D graphics output")
|
||||
|
||||
self._overlay_dr = self.window.makeDisplayRegion()
|
||||
self._overlay_dr.setSort(self.config.overlay_sort)
|
||||
self._overlay_dr.setClearColorActive(False)
|
||||
self._overlay_dr.setClearDepthActive(True)
|
||||
|
||||
cam = p3d.Camera("camera_view_gizmo_cam")
|
||||
cam_lens = p3d.OrthographicLens()
|
||||
cam_lens.setFilmSize(self.config.widget_film_size, self.config.widget_film_size)
|
||||
cam_lens.setNearFar(-100.0, 100.0)
|
||||
cam.setLens(cam_lens)
|
||||
cam.setScene(self._widget_root)
|
||||
self._overlay_camera_np = self._widget_root.attachNewNode(cam)
|
||||
self._overlay_camera_np.setPos(0, -12, 0)
|
||||
self._overlay_camera_np.lookAt(0, 0, 0)
|
||||
self._overlay_dr.setCamera(self._overlay_camera_np)
|
||||
|
||||
def _build_visuals(self) -> None:
|
||||
self._build_axes()
|
||||
self._build_center_projection_toggle()
|
||||
|
||||
def _build_axes(self) -> None:
|
||||
axis_specs = [
|
||||
("x", p3d.Vec3(1, 0, 0), self.config.x_axis_rgba),
|
||||
("y", p3d.Vec3(0, 1, 0), self.config.y_axis_rgba),
|
||||
("z", p3d.Vec3(0, 0, 1), self.config.z_axis_rgba),
|
||||
]
|
||||
for axis_name, axis_vec, rgba in axis_specs:
|
||||
self._axis_line_nodes[axis_name] = self._create_axis_line(axis_name, axis_vec, rgba)
|
||||
|
||||
for face in ViewFace.ALL:
|
||||
axis = self.FACE_AXIS[face]
|
||||
positive = face.startswith("+")
|
||||
base_rgba = self._color_for_axis(axis)
|
||||
if not positive:
|
||||
base_rgba = self._lighten_rgba(base_rgba, self.config.negative_axis_rgba_scale)
|
||||
endpoint = self._create_endpoint(face, axis, base_rgba, self.FACE_LABEL[face])
|
||||
self._endpoints[face] = endpoint
|
||||
|
||||
def _build_center_projection_toggle(self) -> None:
|
||||
self._center_perspective_np = self._create_center_circle_icon()
|
||||
self._center_orthographic_np = self._create_center_cube_icon()
|
||||
self._ui_root.setPos(0.0, 0.0, self._projection_icon_center_z())
|
||||
|
||||
def _create_center_circle_icon(self) -> p3d.NodePath:
|
||||
root = self._ui_root.attachNewNode("camera_view_gizmo_center_circle")
|
||||
radius = self.config.center_icon_size
|
||||
|
||||
ring = p3d.LineSegs("camera_view_gizmo_center_ring")
|
||||
ring.setThickness(self.config.center_icon_line_thickness)
|
||||
ring.setColor(*self.config.center_icon_rgba)
|
||||
segments = 28
|
||||
for index in range(segments + 1):
|
||||
angle = (math.tau * index) / segments
|
||||
x = math.cos(angle) * radius
|
||||
z = math.sin(angle) * radius
|
||||
if index == 0:
|
||||
ring.moveTo(x, 0.0, z)
|
||||
else:
|
||||
ring.drawTo(x, 0.0, z)
|
||||
ring_np = root.attachNewNode(ring.create())
|
||||
ring_np.setDepthWrite(False)
|
||||
ring_np.setDepthTest(False)
|
||||
ring_np.setTransparency(p3d.TransparencyAttrib.MAlpha)
|
||||
ring_np.setBin("fixed", 7)
|
||||
return root
|
||||
|
||||
def _create_center_cube_icon(self) -> p3d.NodePath:
|
||||
root = self._ui_root.attachNewNode("camera_view_gizmo_center_cube")
|
||||
cube = p3d.LineSegs("camera_view_gizmo_center_cube_lines")
|
||||
cube.setThickness(self.config.center_icon_line_thickness)
|
||||
cube.setColor(*self.config.center_icon_rgba)
|
||||
|
||||
size = self.config.center_icon_size * 0.78
|
||||
corners = [
|
||||
p3d.Point3(-size, -size, -size),
|
||||
p3d.Point3(size, -size, -size),
|
||||
p3d.Point3(size, size, -size),
|
||||
p3d.Point3(-size, size, -size),
|
||||
p3d.Point3(-size, -size, size),
|
||||
p3d.Point3(size, -size, size),
|
||||
p3d.Point3(size, size, size),
|
||||
p3d.Point3(-size, size, size),
|
||||
]
|
||||
edges = [
|
||||
(0, 1), (1, 2), (2, 3), (3, 0),
|
||||
(4, 5), (5, 6), (6, 7), (7, 4),
|
||||
(0, 4), (1, 5), (2, 6), (3, 7),
|
||||
]
|
||||
for start_idx, end_idx in edges:
|
||||
cube.moveTo(corners[start_idx])
|
||||
cube.drawTo(corners[end_idx])
|
||||
cube_np = root.attachNewNode(cube.create())
|
||||
cube_np.setDepthWrite(False)
|
||||
cube_np.setDepthTest(False)
|
||||
cube_np.setTransparency(p3d.TransparencyAttrib.MAlpha)
|
||||
cube_np.setBin("fixed", 7)
|
||||
root.setHpr(45.0, -26.0, 0.0)
|
||||
return root
|
||||
|
||||
def _create_axis_line(self, axis_name: str, axis_vec: p3d.Vec3, rgba: tuple[float, float, float, float]) -> p3d.NodePath:
|
||||
segs = p3d.LineSegs(f"camera_view_gizmo_axis_{axis_name}")
|
||||
segs.setThickness(self.config.axis_thickness)
|
||||
length = self.config.axis_length
|
||||
segs.setColor(rgba[0], rgba[1], rgba[2], self.config.axis_line_alpha_back)
|
||||
segs.moveTo(*(axis_vec * -length))
|
||||
segs.drawTo(0, 0, 0)
|
||||
segs.setColor(rgba[0], rgba[1], rgba[2], self.config.axis_line_alpha_front)
|
||||
segs.moveTo(0, 0, 0)
|
||||
segs.drawTo(*(axis_vec * length))
|
||||
axis_np = self._model_root.attachNewNode(segs.create())
|
||||
axis_np.setTransparency(p3d.TransparencyAttrib.MAlpha)
|
||||
axis_np.setDepthWrite(False)
|
||||
axis_np.setBin("fixed", 2)
|
||||
return axis_np
|
||||
|
||||
def _create_endpoint(
|
||||
self,
|
||||
face: str,
|
||||
axis: p3d.Vec3,
|
||||
rgba: p3d.Vec4,
|
||||
label: str,
|
||||
) -> _EndpointVisual:
|
||||
root = self._model_root.attachNewNode(f"camera_view_gizmo_endpoint_{face}")
|
||||
root.setPos(*(axis * self.config.axis_length))
|
||||
|
||||
card_maker = p3d.CardMaker(f"camera_view_gizmo_endpoint_card_{face}")
|
||||
radius = self.config.endpoint_radius * self.config.endpoint_size_scale
|
||||
card_maker.setFrame(-radius, radius, -radius, radius)
|
||||
card = root.attachNewNode(card_maker.generate())
|
||||
card.setBillboardPointEye()
|
||||
card.setTransparency(p3d.TransparencyAttrib.MAlpha)
|
||||
card.setDepthWrite(False)
|
||||
card.setBin("fixed", 3)
|
||||
card.setColor(rgba)
|
||||
|
||||
text_node = p3d.TextNode(f"camera_view_gizmo_endpoint_label_{face}")
|
||||
text_node.setAlign(p3d.TextNode.ACenter)
|
||||
text_node.setText(label)
|
||||
text_node.setTextColor(*self.config.endpoint_text_rgba)
|
||||
if self.config.endpoint_font:
|
||||
font = self.base.loader.loadFont(self.config.endpoint_font)
|
||||
if font:
|
||||
text_node.setFont(font)
|
||||
text = root.attachNewNode(text_node)
|
||||
text.setBillboardPointEye()
|
||||
label_scale = self.config.endpoint_label_scale * self.config.endpoint_size_scale
|
||||
text.setPos(0, -0.01, -(label_scale * 0.38))
|
||||
text.setScale(label_scale)
|
||||
text.setDepthWrite(False)
|
||||
text.setDepthTest(False)
|
||||
text.setBin("fixed", 4)
|
||||
|
||||
return _EndpointVisual(
|
||||
face=face,
|
||||
axis=p3d.Vec3(axis),
|
||||
color=p3d.Vec4(rgba),
|
||||
label=label,
|
||||
root=root,
|
||||
card=card,
|
||||
text=text,
|
||||
)
|
||||
|
||||
def _register_events(self) -> None:
|
||||
self.accept("mouse1", self._on_mouse1_down)
|
||||
self.accept("mouse1-up", self._on_mouse1_up)
|
||||
self.accept("mouse-move", self._on_mouse_move)
|
||||
|
||||
def _on_mouse1_down(self, evt=None) -> None:
|
||||
if self._is_interaction_blocked():
|
||||
self._pointer_pressed_on_widget = False
|
||||
return
|
||||
pointer = self._extract_pointer_xy(evt)
|
||||
if pointer is None:
|
||||
self._pointer_pressed_on_widget = False
|
||||
return
|
||||
self._pending_pointer_xy = pointer
|
||||
self._pointer_pressed_on_widget = self._is_pointer_in_region(pointer)
|
||||
if not self._pointer_pressed_on_widget:
|
||||
return
|
||||
|
||||
local = self._pointer_to_widget_local(pointer)
|
||||
if local is None:
|
||||
return
|
||||
|
||||
if self._is_projection_toggle_hit(local):
|
||||
self.toggle_projection()
|
||||
return
|
||||
|
||||
face = self._pick_face(local)
|
||||
if face:
|
||||
face = self._resolve_face_click_target(face)
|
||||
self.snap_to_face(face)
|
||||
|
||||
def handle_mouse1_down(self, x: float, y: float) -> None:
|
||||
self._on_mouse1_down({"x": x, "y": y})
|
||||
|
||||
def handle_mouse1_up(self, x: float, y: float) -> None:
|
||||
self._on_mouse1_up({"x": x, "y": y})
|
||||
|
||||
def handle_mouse_move(self, x: float, y: float) -> None:
|
||||
self._on_mouse_move({"x": x, "y": y})
|
||||
|
||||
def _on_mouse1_up(self, evt=None) -> None:
|
||||
pointer = self._extract_pointer_xy(evt)
|
||||
if pointer is not None:
|
||||
self._pending_pointer_xy = pointer
|
||||
self._pointer_pressed_on_widget = False
|
||||
|
||||
def _on_mouse_move(self, evt=None) -> None:
|
||||
if self._is_interaction_blocked():
|
||||
self._pointer_inside_widget = False
|
||||
self._projection_icon_hovered = False
|
||||
self._hover_face = None
|
||||
return
|
||||
pointer = self._extract_pointer_xy(evt)
|
||||
if pointer is not None:
|
||||
self._pending_pointer_xy = pointer
|
||||
|
||||
def _update_task(self, task: Task):
|
||||
self._update_display_region()
|
||||
self._update_hover_state()
|
||||
self._sync_widget_orientation()
|
||||
self._advance_transition(globalClock.getDt())
|
||||
self._update_visual_state()
|
||||
return task.cont
|
||||
|
||||
def _update_display_region(self) -> None:
|
||||
if self.window is None or self._overlay_dr is None:
|
||||
return
|
||||
win_x = max(1, self.window.getXSize())
|
||||
win_y = max(1, self.window.getYSize())
|
||||
size_px = float(min(self.config.size_px, win_x, win_y))
|
||||
margin = float(self.config.margin_px)
|
||||
left = max(0.0, win_x - size_px - margin)
|
||||
right = min(float(win_x), win_x - margin)
|
||||
top = max(0.0, margin)
|
||||
bottom = min(float(win_y), margin + size_px)
|
||||
if right <= left or bottom <= top:
|
||||
return
|
||||
rect = (left, top, right, bottom)
|
||||
if rect == self._last_region_rect:
|
||||
return
|
||||
self._last_region_rect = rect
|
||||
self._overlay_dr.setDimensions(
|
||||
left / win_x,
|
||||
right / win_x,
|
||||
max(0.0, (win_y - bottom) / win_y),
|
||||
min(1.0, (win_y - top) / win_y),
|
||||
)
|
||||
|
||||
def _update_hover_state(self) -> None:
|
||||
if self._is_interaction_blocked():
|
||||
self._pointer_inside_widget = False
|
||||
self._hover_face = None
|
||||
self._projection_icon_hovered = False
|
||||
self._pointer_pressed_on_widget = False
|
||||
return
|
||||
pointer = self._get_pointer_xy()
|
||||
if pointer is None or not self._is_pointer_in_region(pointer):
|
||||
self._pointer_inside_widget = False
|
||||
self._hover_face = None
|
||||
self._projection_icon_hovered = False
|
||||
return
|
||||
self._pointer_inside_widget = True
|
||||
local = self._pointer_to_widget_local(pointer)
|
||||
self._projection_icon_hovered = self._is_projection_toggle_hit(local) if local is not None else False
|
||||
self._hover_face = None if self._projection_icon_hovered else self._pick_face(local)
|
||||
|
||||
def _sync_widget_orientation(self) -> None:
|
||||
quat = p3d.Quat(self.camera_np.getQuat(self.render))
|
||||
quat.invertInPlace()
|
||||
self._model_root.setQuat(quat)
|
||||
|
||||
def _advance_transition(self, dt: float) -> None:
|
||||
if self._transition is None:
|
||||
return
|
||||
self._transition.elapsed = min(self._transition.elapsed + max(0.0, float(dt)), self._transition.duration)
|
||||
t = self._transition.elapsed / self._transition.duration
|
||||
t = t * t * (3.0 - 2.0 * t)
|
||||
|
||||
pos = self._lerp_point(self._transition.start_pos, self._transition.end_pos, t)
|
||||
pivot = self._lerp_point(self._transition.start_pivot, self._transition.end_pivot, t)
|
||||
up = self._lerp_vec(self._transition.start_up, self._transition.end_up, t)
|
||||
self._apply_camera_pose(pos, pivot, up)
|
||||
if self._transition.elapsed >= self._transition.duration:
|
||||
self._transition = None
|
||||
|
||||
def _update_visual_state(self) -> None:
|
||||
center_color = self.config.center_icon_hover_rgba if self._projection_icon_hovered else self.config.center_icon_rgba
|
||||
if self._center_perspective_np is not None:
|
||||
if self._projection_mode == ProjectionMode.PERSPECTIVE:
|
||||
self._center_perspective_np.show()
|
||||
else:
|
||||
self._center_perspective_np.hide()
|
||||
self._center_perspective_np.setColor(*center_color)
|
||||
if self._center_orthographic_np is not None:
|
||||
if self._projection_mode == ProjectionMode.ORTHOGRAPHIC:
|
||||
self._center_orthographic_np.show()
|
||||
else:
|
||||
self._center_orthographic_np.hide()
|
||||
self._center_orthographic_np.setColor(*center_color)
|
||||
|
||||
for face, endpoint in self._endpoints.items():
|
||||
local_pos = self._model_root.getQuat().xform(endpoint.axis * self.config.axis_length)
|
||||
depth = max(-1.0, min(1.0, float(local_pos.y / max(1e-6, self.config.axis_length))))
|
||||
frontness = max(0.0, (-depth + 1.0) * 0.5)
|
||||
front_radius = self.config.endpoint_radius * self.config.endpoint_size_scale
|
||||
back_radius = self.config.endpoint_radius_back * self.config.endpoint_size_scale
|
||||
radius = back_radius + (front_radius - back_radius) * frontness
|
||||
if face == self._hover_face:
|
||||
radius *= self.config.endpoint_hover_scale
|
||||
endpoint.card.setScale(radius / max(front_radius, 1e-6))
|
||||
alpha = 0.58 + 0.42 * frontness
|
||||
endpoint.card.setColor(endpoint.color.x, endpoint.color.y, endpoint.color.z, alpha)
|
||||
endpoint.card.setY(-0.06 * depth)
|
||||
endpoint.text.setSa(0.72 + 0.28 * frontness)
|
||||
|
||||
for axis_name, axis_np in self._axis_line_nodes.items():
|
||||
axis_vec = {
|
||||
"x": p3d.Vec3(1, 0, 0),
|
||||
"y": p3d.Vec3(0, 1, 0),
|
||||
"z": p3d.Vec3(0, 0, 1),
|
||||
}[axis_name]
|
||||
local_axis = self._model_root.getQuat().xform(axis_vec)
|
||||
axis_np.setY(0.03 * local_axis.y)
|
||||
|
||||
def _detect_projection_mode(self, lens: p3d.Lens) -> str:
|
||||
return ProjectionMode.PERSPECTIVE if lens.isPerspective() else ProjectionMode.ORTHOGRAPHIC
|
||||
|
||||
def _switch_to_orthographic(self) -> None:
|
||||
current_lens = self.camera_node.getLens()
|
||||
self._stored_perspective_lens = current_lens.makeCopy()
|
||||
|
||||
ortho = p3d.OrthographicLens()
|
||||
aspect = self._safe_aspect_ratio()
|
||||
pivot = self._get_pivot_point()
|
||||
cam_pos = self.camera_np.getPos(self.render)
|
||||
distance = max((cam_pos - pivot).length(), 0.5)
|
||||
fov_y = float(current_lens.getFov()[1])
|
||||
ortho_height = max(0.1, 2.0 * distance * math.tan(math.radians(fov_y) * 0.5))
|
||||
ortho.setFilmSize(max(0.1, ortho_height * aspect), ortho_height)
|
||||
try:
|
||||
ortho.setNearFar(current_lens.getNear(), current_lens.getFar())
|
||||
except Exception:
|
||||
ortho.setNearFar(-1000.0, 1000.0)
|
||||
self.camera_node.setLens(ortho)
|
||||
|
||||
def _switch_to_perspective(self) -> None:
|
||||
perspective = self._stored_perspective_lens.makeCopy() if self._stored_perspective_lens else p3d.PerspectiveLens()
|
||||
self.camera_node.setLens(perspective)
|
||||
|
||||
def _apply_camera_pose(self, pos: p3d.Point3, pivot: p3d.Point3, up: p3d.Vec3) -> None:
|
||||
self.camera_np.setPos(self.render, pos)
|
||||
safe_up = p3d.Vec3(up)
|
||||
if safe_up.lengthSquared() <= 1e-8:
|
||||
safe_up = p3d.Vec3(0, 0, 1)
|
||||
safe_up.normalize()
|
||||
self.camera_np.lookAt(pivot, safe_up)
|
||||
self._notify_camera_changed()
|
||||
|
||||
def _notify_camera_changed(self) -> None:
|
||||
if self.on_camera_changed is not None:
|
||||
self.on_camera_changed(self)
|
||||
|
||||
def _get_pointer_xy(self) -> tuple[float, float] | None:
|
||||
if self._pending_pointer_xy is not None:
|
||||
return self._pending_pointer_xy
|
||||
win = self.window
|
||||
if win is not None and hasattr(win, "getPointer"):
|
||||
pointer = win.getPointer(0)
|
||||
return float(pointer.getX()), float(pointer.getY())
|
||||
return self._pending_pointer_xy
|
||||
|
||||
def _extract_pointer_xy(self, evt=None) -> tuple[float, float] | None:
|
||||
if isinstance(evt, dict) and "x" in evt and "y" in evt:
|
||||
return float(evt["x"]), float(evt["y"])
|
||||
return self._get_pointer_xy()
|
||||
|
||||
def _is_pointer_in_region(self, pointer: tuple[float, float] | None) -> bool:
|
||||
if pointer is None or self._last_region_rect is None:
|
||||
return False
|
||||
x, y = pointer
|
||||
left, top, right, bottom = self._last_region_rect
|
||||
return left <= x <= right and top <= y <= bottom
|
||||
|
||||
def _pointer_to_widget_local(self, pointer: tuple[float, float] | None) -> tuple[float, float] | None:
|
||||
if pointer is None or self._last_region_rect is None:
|
||||
return None
|
||||
x, y = pointer
|
||||
left, top, right, bottom = self._last_region_rect
|
||||
width = max(1e-6, right - left)
|
||||
height = max(1e-6, bottom - top)
|
||||
nx = ((x - left) / width) * 2.0 - 1.0
|
||||
ny = (1.0 - ((y - top) / height)) * 2.0 - 1.0
|
||||
half_film = self.config.widget_film_size * 0.5
|
||||
return nx * half_film, ny * half_film
|
||||
|
||||
def _pick_face(self, local_xy: tuple[float, float] | None) -> str | None:
|
||||
if local_xy is None:
|
||||
return None
|
||||
x, z = local_xy
|
||||
best_face = None
|
||||
best_dist_sq = None
|
||||
for face, endpoint in self._endpoints.items():
|
||||
pos = self._model_root.getQuat().xform(endpoint.axis * self.config.axis_length)
|
||||
radius = (
|
||||
self.config.endpoint_radius
|
||||
if face.startswith("+")
|
||||
else self.config.endpoint_radius_back
|
||||
) * self.config.endpoint_size_scale
|
||||
dx = x - pos.x
|
||||
dz = z - pos.z
|
||||
dist_sq = dx * dx + dz * dz
|
||||
if dist_sq > (radius * radius):
|
||||
continue
|
||||
if best_face is None or dist_sq < best_dist_sq:
|
||||
best_face = face
|
||||
best_dist_sq = dist_sq
|
||||
return best_face
|
||||
|
||||
def _is_projection_toggle_hit(self, local_xy: tuple[float, float] | None) -> bool:
|
||||
if local_xy is None:
|
||||
return False
|
||||
x, z = local_xy
|
||||
z -= self._projection_icon_center_z()
|
||||
radius = self.config.center_icon_hit_radius
|
||||
return (x * x + z * z) <= (radius * radius)
|
||||
|
||||
def _resolve_face_click_target(self, face: str) -> str:
|
||||
normalized_face = str(face).lower()
|
||||
if normalized_face not in self.FACE_AXIS:
|
||||
return normalized_face
|
||||
if self._is_camera_aligned_to_face(normalized_face):
|
||||
return self.OPPOSITE_FACE.get(normalized_face, normalized_face)
|
||||
return normalized_face
|
||||
|
||||
def _is_camera_aligned_to_face(self, face: str, tolerance_deg: float = 8.0) -> bool:
|
||||
pivot = self._get_pivot_point()
|
||||
cam_pos = self.camera_np.getPos(self.render)
|
||||
view_vec = p3d.Vec3(cam_pos - pivot)
|
||||
if view_vec.lengthSquared() <= 1e-8:
|
||||
return False
|
||||
view_vec.normalize()
|
||||
|
||||
target_axis = p3d.Vec3(self.FACE_AXIS[face])
|
||||
if target_axis.lengthSquared() <= 1e-8:
|
||||
return False
|
||||
target_axis.normalize()
|
||||
|
||||
dot = max(-1.0, min(1.0, float(view_vec.dot(target_axis))))
|
||||
tolerance_cos = math.cos(math.radians(max(0.0, float(tolerance_deg))))
|
||||
return dot >= tolerance_cos
|
||||
|
||||
def _get_pivot_point(self) -> p3d.Point3:
|
||||
value = self.get_pivot()
|
||||
if isinstance(value, p3d.NodePath):
|
||||
return p3d.Point3(value.getPos(self.render))
|
||||
if isinstance(value, p3d.LVecBase3):
|
||||
return p3d.Point3(value)
|
||||
return p3d.Point3(*value)
|
||||
|
||||
def _up_vector_for_face(self, face: str) -> p3d.Vec3:
|
||||
if face in {ViewFace.POS_Z, ViewFace.NEG_Z}:
|
||||
return p3d.Vec3(*self.config.top_bottom_up_axis)
|
||||
return p3d.Vec3(*self.config.horizontal_up_axis)
|
||||
|
||||
def _safe_aspect_ratio(self) -> float:
|
||||
win = self.window
|
||||
if win is None:
|
||||
return 1.0
|
||||
x = max(1, win.getXSize())
|
||||
y = max(1, win.getYSize())
|
||||
return float(x) / float(y)
|
||||
|
||||
def _color_for_axis(self, axis: p3d.Vec3) -> p3d.Vec4:
|
||||
if abs(axis.x) > 0.5:
|
||||
return p3d.Vec4(*self.config.x_axis_rgba)
|
||||
if abs(axis.y) > 0.5:
|
||||
return p3d.Vec4(*self.config.y_axis_rgba)
|
||||
return p3d.Vec4(*self.config.z_axis_rgba)
|
||||
|
||||
def _projection_icon_center_z(self) -> float:
|
||||
endpoint_extent = self.config.endpoint_radius * self.config.endpoint_size_scale
|
||||
return -(
|
||||
self.config.axis_length
|
||||
+ endpoint_extent
|
||||
+ self.config.projection_icon_gap
|
||||
+ self.config.center_icon_hit_radius
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _lighten_rgba(color: p3d.Vec4, factor: float) -> p3d.Vec4:
|
||||
t = max(0.0, min(1.0, float(factor)))
|
||||
return p3d.Vec4(
|
||||
color.x + (1.0 - color.x) * t,
|
||||
color.y + (1.0 - color.y) * t,
|
||||
color.z + (1.0 - color.z) * t,
|
||||
color.w,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _lerp_point(a: p3d.Point3, b: p3d.Point3, t: float) -> p3d.Point3:
|
||||
return p3d.Point3(
|
||||
a.x + (b.x - a.x) * t,
|
||||
a.y + (b.y - a.y) * t,
|
||||
a.z + (b.z - a.z) * t,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _lerp_vec(a: p3d.Vec3, b: p3d.Vec3, t: float) -> p3d.Vec3:
|
||||
value = p3d.Vec3(
|
||||
a.x + (b.x - a.x) * t,
|
||||
a.y + (b.y - a.y) * t,
|
||||
a.z + (b.z - a.z) * t,
|
||||
)
|
||||
if value.lengthSquared() > 1e-8:
|
||||
value.normalize()
|
||||
return value
|
||||
63
src/camera_view_gizmo/config.py
Normal file
63
src/camera_view_gizmo/config.py
Normal file
@ -0,0 +1,63 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class CameraViewGizmoConfig:
|
||||
# 组件在屏幕中的像素尺寸(正方形边长)
|
||||
size_px: int = 148
|
||||
# 组件距离窗口右上角的外边距
|
||||
margin_px: int = 8
|
||||
# 点击六视图后相机过渡动画时长(秒)
|
||||
transition_duration: float = 0.2
|
||||
# 三轴从中心延伸到端点的长度
|
||||
axis_length: float = 1.0
|
||||
# 六方向小方块整体缩放倍数
|
||||
endpoint_size_scale: float = 1.5
|
||||
# 正方向端点方块的基础半径
|
||||
endpoint_radius: float = 0.22
|
||||
# 反方向端点方块的基础半径
|
||||
endpoint_radius_back: float = 0.16
|
||||
# 鼠标悬停在端点上时的放大倍率
|
||||
endpoint_hover_scale: float = 1.25
|
||||
# 端点文字标签的基础缩放
|
||||
endpoint_label_scale: float = 0.18
|
||||
# 三轴线条的粗细
|
||||
axis_thickness: float = 3.0
|
||||
# Overlay 正交镜头的视口尺寸,影响整体构图比例
|
||||
widget_film_size: float = 4.6
|
||||
# 底部投影切换图标的尺寸
|
||||
center_icon_size: float = 0.28
|
||||
# 底部投影切换图标的点击命中半径
|
||||
center_icon_hit_radius: float = 0.36
|
||||
# 底部投影切换图标线条粗细
|
||||
center_icon_line_thickness: float = 2.2
|
||||
# 底部投影切换图标与六方向视图之间的间距
|
||||
projection_icon_gap: float = 0.24
|
||||
# Overlay DisplayRegion 的渲染排序值,越大越后绘制
|
||||
overlay_sort: int = 8
|
||||
# X 轴正方向的颜色
|
||||
x_axis_rgba: tuple[float, float, float, float] = (0.93, 0.34, 0.29, 1.0)
|
||||
# Y 轴正方向的颜色
|
||||
y_axis_rgba: tuple[float, float, float, float] = (0.22, 0.74, 0.34, 1.0)
|
||||
# Z 轴正方向的颜色
|
||||
z_axis_rgba: tuple[float, float, float, float] = (0.31, 0.57, 0.95, 1.0)
|
||||
# 反方向端点颜色向白色提亮的比例
|
||||
negative_axis_rgba_scale: float = 0.46
|
||||
# 朝向屏幕前方的轴线透明度
|
||||
axis_line_alpha_front: float = 0.95
|
||||
# 朝向屏幕后方的轴线透明度
|
||||
axis_line_alpha_back: float = 0.42
|
||||
# 透视/正交切换图标的默认颜色
|
||||
center_icon_rgba: tuple[float, float, float, float] = (0.92, 0.93, 0.96, 0.95)
|
||||
# 透视/正交切换图标悬停时的高亮颜色
|
||||
center_icon_hover_rgba: tuple[float, float, float, float] = (1.0, 0.95, 0.64, 1.0)
|
||||
# 六方向端点文字的颜色
|
||||
endpoint_text_rgba: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0)
|
||||
# 六方向端点文字使用的字体路径,None 表示使用默认字体
|
||||
endpoint_font: str | None = None
|
||||
# 顶视图/底视图时相机的朝上方向
|
||||
top_bottom_up_axis: tuple[float, float, float] = (0.0, 1.0, 0.0)
|
||||
# 侧视图/前后视图时相机的朝上方向
|
||||
horizontal_up_axis: tuple[float, float, float] = (0.0, 0.0, 1.0)
|
||||
34
src/camera_view_gizmo/demo_basic.py
Normal file
34
src/camera_view_gizmo/demo_basic.py
Normal file
@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from direct.showbase.ShowBase import ShowBase
|
||||
import panda3d.core as p3d
|
||||
|
||||
from src.plugins.camera_view_gizmo import CameraViewGizmo, CameraViewGizmoConfig
|
||||
|
||||
|
||||
class DemoApp(ShowBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.disableMouse()
|
||||
|
||||
self.target = p3d.Point3(0, 0, 0)
|
||||
|
||||
model = self.loader.loadModel("models/environment")
|
||||
model.reparentTo(self.render)
|
||||
model.setScale(0.05)
|
||||
model.setPos(-8, 42, 0)
|
||||
|
||||
self.camera.setPos(14, -18, 10)
|
||||
self.camera.lookAt(self.target)
|
||||
|
||||
self.gizmo = CameraViewGizmo(
|
||||
self,
|
||||
camera_np=self.camera,
|
||||
get_pivot=lambda: self.target,
|
||||
config=CameraViewGizmoConfig(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = DemoApp()
|
||||
app.run()
|
||||
@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from imgui_bundle import hello_imgui, imgui, immapp, immvision # type: ignore
|
||||
|
||||
from .config import INI_FILENAME, WINDOW_TITLE
|
||||
@ -13,9 +15,12 @@ class EditorApp:
|
||||
|
||||
self.selected_node_path = "SceneRoot/PreviewRoot/Teapot"
|
||||
self.renderer.select_node_path(self.selected_node_path)
|
||||
self._hierarchy_auto_expand_paths = self._ancestor_paths(self.selected_node_path)
|
||||
self.viewport_texture: immvision.GlTexture | None = None
|
||||
self.viewport_frame_version = -1
|
||||
self.viewport_frame_shape: tuple[int, int, int] | None = None
|
||||
self._default_font = None
|
||||
self._inspector_transform_edits: dict[tuple[str, str], tuple[float, float, float]] = {}
|
||||
self.render_scale = 1.0
|
||||
self._viewport_mouse_buttons = {"lmb": False, "mmb": False, "rmb": False}
|
||||
self._viewport_keys = {
|
||||
@ -28,6 +33,17 @@ class EditorApp:
|
||||
"r": False,
|
||||
"space": False,
|
||||
}
|
||||
self._console_entries = [
|
||||
("Info", "Editor scene loaded", "Scene"),
|
||||
("Info", "编辑器启动完成,中文字体显示测试:控制台中文日志已启用", "Console"),
|
||||
("Info", "Panda3D offscreen viewport connected to ImGui", "Renderer"),
|
||||
("Info", "simplepbr preview pipeline active", "Renderer"),
|
||||
("Warning", "Viewport selection uses render picking; very small objects may need zooming in", "Picker"),
|
||||
]
|
||||
self._console_show_info = True
|
||||
self._console_show_warnings = True
|
||||
self._console_show_errors = True
|
||||
self._console_collapse = False
|
||||
|
||||
def build_runner_params(self) -> tuple[hello_imgui.RunnerParams, immapp.AddOnsParams]:
|
||||
runner = hello_imgui.RunnerParams()
|
||||
@ -38,19 +54,43 @@ class EditorApp:
|
||||
|
||||
runner.imgui_window_params.menu_app_title = "RobotMetaCore"
|
||||
runner.imgui_window_params.show_menu_bar = True
|
||||
runner.imgui_window_params.show_status_bar = True
|
||||
runner.imgui_window_params.show_status_bar = False
|
||||
runner.imgui_window_params.show_status_fps = False
|
||||
runner.imgui_window_params.default_imgui_window_type = (
|
||||
hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space
|
||||
)
|
||||
|
||||
runner.callbacks.show_status = self._show_status_bar
|
||||
runner.callbacks.before_exit = self._shutdown
|
||||
runner.callbacks.load_additional_fonts = self._load_fonts
|
||||
runner.docking_params = self._build_docking_layout()
|
||||
runner.docking_params.layout_condition = hello_imgui.DockingLayoutCondition.application_start
|
||||
|
||||
addons = immapp.AddOnsParams()
|
||||
return runner, addons
|
||||
|
||||
def _load_fonts(self) -> None:
|
||||
fonts_dir = Path(__file__).resolve().parents[2] / "fonts"
|
||||
font_size = 15.0
|
||||
|
||||
monaco_params = hello_imgui.FontLoadingParams()
|
||||
monaco_params.inside_assets = False
|
||||
self._default_font = hello_imgui.load_font(
|
||||
str(fonts_dir / "monaco.ttf"),
|
||||
font_size,
|
||||
monaco_params,
|
||||
)
|
||||
|
||||
msyh_params = hello_imgui.FontLoadingParams()
|
||||
msyh_params.inside_assets = False
|
||||
msyh_params.merge_to_last_font = True
|
||||
hello_imgui.load_font(
|
||||
str(fonts_dir / "msyh.ttc"),
|
||||
font_size,
|
||||
msyh_params,
|
||||
)
|
||||
|
||||
imgui.get_io().font_default = self._default_font
|
||||
|
||||
def _build_docking_layout(self) -> hello_imgui.DockingParams:
|
||||
layout = hello_imgui.DockingParams()
|
||||
|
||||
@ -90,9 +130,9 @@ class EditorApp:
|
||||
gui_function_=self.show_inspector,
|
||||
),
|
||||
hello_imgui.DockableWindow(
|
||||
label_="Stats",
|
||||
label_="Console",
|
||||
dock_space_name_="BottomSpace",
|
||||
gui_function_=self.show_stats,
|
||||
gui_function_=self.show_console,
|
||||
),
|
||||
]
|
||||
return layout
|
||||
@ -171,6 +211,9 @@ class EditorApp:
|
||||
if hovered and io.mouse_wheel != 0.0:
|
||||
self.renderer.queue_input_event("wheel", delta=io.mouse_wheel)
|
||||
|
||||
if hovered and ctrl_down and imgui.is_key_pressed(imgui.Key.z):
|
||||
self.renderer.queue_input_event("undo_transform")
|
||||
|
||||
if hovered or self._viewport_mouse_buttons["rmb"]:
|
||||
key_states = {
|
||||
"w": imgui.is_key_down(imgui.Key.w),
|
||||
@ -241,8 +284,10 @@ class EditorApp:
|
||||
|
||||
self._handle_viewport_input(hovered, rect_min)
|
||||
renderer_selected_path = self.renderer.selected_node_path()
|
||||
if renderer_selected_path:
|
||||
if renderer_selected_path and renderer_selected_path != self.selected_node_path:
|
||||
self.selected_node_path = renderer_selected_path
|
||||
self._inspector_transform_edits.clear()
|
||||
self._hierarchy_auto_expand_paths = self._ancestor_paths(renderer_selected_path)
|
||||
# if hovered:
|
||||
# imgui.set_tooltip("Alt+LMB orbit\nMMB pan\nAlt+RMB or Wheel dolly\nRMB look + WASD/QE fly\nF focus")
|
||||
else:
|
||||
@ -260,65 +305,217 @@ class EditorApp:
|
||||
|
||||
for index, node in enumerate(nodes):
|
||||
self._draw_node_tree(node, f"root-{index}")
|
||||
self._hierarchy_auto_expand_paths.clear()
|
||||
|
||||
def _draw_node_tree(self, node, node_key: str) -> None:
|
||||
tree_flags = imgui.TreeNodeFlags_.span_full_width
|
||||
tree_flags = imgui.TreeNodeFlags_.span_full_width | imgui.TreeNodeFlags_.open_on_arrow
|
||||
if not node.children:
|
||||
tree_flags |= imgui.TreeNodeFlags_.leaf
|
||||
if node.path == self.selected_node_path:
|
||||
tree_flags |= imgui.TreeNodeFlags_.selected
|
||||
|
||||
if node.path in self._hierarchy_auto_expand_paths:
|
||||
imgui.set_next_item_open(True, imgui.Cond_.always)
|
||||
opened = imgui.tree_node_ex(f"{node.name}##{node_key}", tree_flags)
|
||||
if imgui.is_item_clicked():
|
||||
if imgui.is_item_clicked() and not imgui.is_item_toggled_open():
|
||||
self.selected_node_path = node.path
|
||||
self._inspector_transform_edits.clear()
|
||||
self.renderer.select_node_path(node.path)
|
||||
self._hierarchy_auto_expand_paths = self._ancestor_paths(node.path)
|
||||
|
||||
if opened:
|
||||
for index, child in enumerate(node.children):
|
||||
self._draw_node_tree(child, f"{node_key}-{index}")
|
||||
imgui.tree_pop()
|
||||
|
||||
def _ancestor_paths(self, path: str) -> set[str]:
|
||||
parts = path.split("/")
|
||||
return {"/".join(parts[:index]) for index in range(2, len(parts))}
|
||||
|
||||
def show_inspector(self) -> None:
|
||||
node = self.renderer.node_snapshot(self.selected_node_path)
|
||||
if node is None:
|
||||
imgui.text_unformatted("Select a node from Hierarchy.")
|
||||
return
|
||||
|
||||
imgui.text(f"Path: {node.path}")
|
||||
imgui.text(f"Children: {len(node.children)}")
|
||||
imgui.separator()
|
||||
imgui.text("Transform")
|
||||
imgui.bullet_text(f"Position: ({node.pos[0]:.2f}, {node.pos[1]:.2f}, {node.pos[2]:.2f})")
|
||||
imgui.bullet_text(f"Rotation: ({node.hpr[0]:.2f}, {node.hpr[1]:.2f}, {node.hpr[2]:.2f})")
|
||||
imgui.bullet_text(f"Scale: ({node.scale[0]:.2f}, {node.scale[1]:.2f}, {node.scale[2]:.2f})")
|
||||
imgui.text_unformatted(node.name)
|
||||
imgui.text_colored((0.58, 0.62, 0.68, 1.0), node.path)
|
||||
imgui.spacing()
|
||||
|
||||
camera = self.renderer.camera_state()
|
||||
imgui.separator()
|
||||
imgui.text("Camera")
|
||||
imgui.bullet_text(f"Target: ({camera.target_x:.2f}, {camera.target_y:.2f}, {camera.target_z:.2f})")
|
||||
imgui.bullet_text(f"Distance: {camera.distance:.2f}")
|
||||
imgui.bullet_text(f"Heading: {camera.heading_deg:.2f}")
|
||||
imgui.bullet_text(f"Pitch: {camera.pitch_deg:.2f}")
|
||||
if imgui.collapsing_header("Transform", imgui.TreeNodeFlags_.default_open):
|
||||
self._draw_transform_fields(node)
|
||||
|
||||
def show_stats(self) -> None:
|
||||
ready, size, fps, last_error, _ = self.renderer.snapshot()
|
||||
frame_shape = self.viewport_frame_shape
|
||||
imgui.text(f"Renderer ready: {ready}")
|
||||
if frame_shape is not None:
|
||||
imgui.text(f"Frame shape: {frame_shape[1]} x {frame_shape[0]} x {frame_shape[2]}")
|
||||
else:
|
||||
imgui.text("Frame shape: n/a")
|
||||
imgui.text(f"Requested size: {size[0]} x {size[1]}")
|
||||
imgui.text(f"Panda worker FPS: {fps:.1f}")
|
||||
_, self.render_scale = imgui.slider_float("Render Scale", self.render_scale, 0.4, 1.0, "%.2f")
|
||||
imgui.separator()
|
||||
imgui.text_wrapped(
|
||||
"The original black screen came from rendering Panda3D inside the same "
|
||||
"OpenGL GUI frame used by Hello ImGui. This version moves Panda3D rendering "
|
||||
"to a worker thread, runs simplepbr on the offscreen buffer, uploads only "
|
||||
"the latest RGBA frame to a GlTexture, and renders the viewport with a raw "
|
||||
"ImGui draw list instead of ImmVision."
|
||||
if imgui.collapsing_header("Node Info", imgui.TreeNodeFlags_.default_open):
|
||||
self._draw_readonly_property("Children", str(len(node.children)))
|
||||
|
||||
def _draw_transform_fields(self, node) -> None:
|
||||
table_flags = (
|
||||
imgui.TableFlags_.sizing_stretch_prop
|
||||
| imgui.TableFlags_.borders_inner_v
|
||||
| imgui.TableFlags_.pad_outer_x
|
||||
)
|
||||
if not imgui.begin_table("InspectorTransformTable", 2, table_flags):
|
||||
return
|
||||
|
||||
imgui.table_setup_column("Property", imgui.TableColumnFlags_.width_fixed, 72.0)
|
||||
imgui.table_setup_column("Value", imgui.TableColumnFlags_.width_stretch)
|
||||
self._draw_transform_row("Position", "pos", node.pos, "%.3f")
|
||||
self._draw_transform_row("Rotation", "hpr", node.hpr, "%.2f")
|
||||
self._draw_transform_row("Scale", "scale", node.scale, "%.3f")
|
||||
imgui.end_table()
|
||||
|
||||
def _draw_transform_row(
|
||||
self,
|
||||
label: str,
|
||||
field: str,
|
||||
value: tuple[float, float, float],
|
||||
value_format: str,
|
||||
) -> None:
|
||||
imgui.table_next_row()
|
||||
imgui.table_set_column_index(0)
|
||||
imgui.align_text_to_frame_padding()
|
||||
imgui.text_unformatted(label)
|
||||
imgui.table_set_column_index(1)
|
||||
edit_key = (self.selected_node_path, field)
|
||||
current_value = self._inspector_transform_edits.get(edit_key, value)
|
||||
changed, value_tuple, active = self._draw_axis_float_inputs(field, current_value, value_format)
|
||||
if changed:
|
||||
self._inspector_transform_edits[edit_key] = value_tuple
|
||||
self.renderer.queue_input_event(
|
||||
"set_transform",
|
||||
path=self.selected_node_path,
|
||||
field=field,
|
||||
value=value_tuple,
|
||||
)
|
||||
if not active:
|
||||
self._inspector_transform_edits.pop(edit_key, None)
|
||||
|
||||
def _draw_axis_float_inputs(
|
||||
self,
|
||||
field: str,
|
||||
value: tuple[float, float, float],
|
||||
value_format: str,
|
||||
) -> tuple[bool, tuple[float, float, float], bool]:
|
||||
axis_specs = (
|
||||
("X", (0.94, 0.30, 0.28, 1.0)),
|
||||
("Y", (0.32, 0.78, 0.36, 1.0)),
|
||||
("Z", (0.32, 0.52, 1.0, 1.0)),
|
||||
)
|
||||
available_width = max(imgui.get_content_region_avail().x, 120.0)
|
||||
label_width = 14.0
|
||||
spacing = 6.0
|
||||
input_width = max((available_width - (label_width + spacing) * 3.0) / 3.0, 44.0)
|
||||
values = [float(component) for component in value]
|
||||
changed = False
|
||||
active = False
|
||||
|
||||
imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(spacing, 0.0))
|
||||
try:
|
||||
for index, (axis, color) in enumerate(axis_specs):
|
||||
if index > 0:
|
||||
imgui.same_line()
|
||||
imgui.text_colored(color, axis)
|
||||
imgui.same_line()
|
||||
imgui.set_next_item_width(input_width)
|
||||
axis_changed, axis_value = imgui.input_float(
|
||||
f"##{field}_{axis}",
|
||||
values[index],
|
||||
0.0,
|
||||
0.0,
|
||||
value_format,
|
||||
)
|
||||
if axis_changed:
|
||||
values[index] = float(axis_value)
|
||||
changed = True
|
||||
active = active or imgui.is_item_active()
|
||||
finally:
|
||||
imgui.pop_style_var()
|
||||
|
||||
return changed, (values[0], values[1], values[2]), active
|
||||
|
||||
def _draw_readonly_property(self, label: str, value: str) -> None:
|
||||
if imgui.begin_table(f"InspectorReadonly{label}", 2, imgui.TableFlags_.sizing_stretch_prop):
|
||||
imgui.table_setup_column("Property", imgui.TableColumnFlags_.width_fixed, 72.0)
|
||||
imgui.table_setup_column("Value", imgui.TableColumnFlags_.width_stretch)
|
||||
imgui.table_next_row()
|
||||
imgui.table_set_column_index(0)
|
||||
imgui.text_colored((0.68, 0.72, 0.78, 1.0), label)
|
||||
imgui.table_set_column_index(1)
|
||||
imgui.text_unformatted(value)
|
||||
imgui.end_table()
|
||||
|
||||
def show_console(self) -> None:
|
||||
_, _, _, last_error, _ = self.renderer.snapshot()
|
||||
entries = list(self._console_entries)
|
||||
if last_error:
|
||||
imgui.separator()
|
||||
imgui.text_colored((1.0, 0.4, 0.4, 1.0), last_error)
|
||||
entries.append(("Error", last_error, "Renderer"))
|
||||
visible_entries = self._filtered_console_entries(entries)
|
||||
if self._console_collapse:
|
||||
visible_entries = self._collapsed_console_entries(visible_entries)
|
||||
|
||||
if imgui.button("Clear"):
|
||||
self._console_entries.clear()
|
||||
imgui.same_line()
|
||||
if imgui.button("Copy All"):
|
||||
imgui.set_clipboard_text("\n".join(self._format_console_entry(entry) for entry in visible_entries))
|
||||
imgui.same_line()
|
||||
_, self._console_collapse = imgui.checkbox("Collapse", self._console_collapse)
|
||||
imgui.same_line()
|
||||
_, self._console_show_info = imgui.checkbox(
|
||||
f"Log ({self._console_count('Info')})",
|
||||
self._console_show_info,
|
||||
)
|
||||
imgui.same_line()
|
||||
_, self._console_show_warnings = imgui.checkbox(
|
||||
f"Warning ({self._console_count('Warning')})",
|
||||
self._console_show_warnings,
|
||||
)
|
||||
imgui.same_line()
|
||||
_, self._console_show_errors = imgui.checkbox(
|
||||
f"Error ({self._console_count('Error') + (1 if last_error else 0)})",
|
||||
self._console_show_errors,
|
||||
)
|
||||
|
||||
imgui.separator()
|
||||
if imgui.begin_child("ConsoleLogList", (0.0, 0.0), imgui.ChildFlags_.borders):
|
||||
console_text = "\n".join(self._format_console_entry(entry) for entry in visible_entries)
|
||||
available = imgui.get_content_region_avail()
|
||||
imgui.input_text_multiline(
|
||||
"##ConsoleSelectableText",
|
||||
console_text,
|
||||
(available.x, available.y),
|
||||
imgui.InputTextFlags_.read_only,
|
||||
)
|
||||
imgui.end_child()
|
||||
|
||||
def _console_count(self, level: str) -> int:
|
||||
return sum(1 for entry_level, _, _ in self._console_entries if entry_level == level)
|
||||
|
||||
def _filtered_console_entries(self, entries: list[tuple[str, str, str]]) -> list[tuple[str, str, str]]:
|
||||
return [
|
||||
entry
|
||||
for entry in entries
|
||||
if (
|
||||
(entry[0] == "Info" and self._console_show_info)
|
||||
or (entry[0] == "Warning" and self._console_show_warnings)
|
||||
or (entry[0] == "Error" and self._console_show_errors)
|
||||
)
|
||||
]
|
||||
|
||||
def _collapsed_console_entries(self, entries: list[tuple[str, str, str]]) -> list[tuple[str, str, str]]:
|
||||
counts: dict[tuple[str, str, str], int] = {}
|
||||
ordered_entries: list[tuple[str, str, str]] = []
|
||||
for entry in entries:
|
||||
if entry not in counts:
|
||||
ordered_entries.append(entry)
|
||||
counts[entry] = 0
|
||||
counts[entry] += 1
|
||||
return [
|
||||
(level, f"{message} ({counts[entry]})" if counts[entry] > 1 else message, source)
|
||||
for entry in ordered_entries
|
||||
for level, message, source in [entry]
|
||||
]
|
||||
|
||||
def _format_console_entry(self, entry: tuple[str, str, str]) -> str:
|
||||
level, message, source = entry
|
||||
return f"[{level}] [{source}] {message}"
|
||||
|
||||
@ -23,7 +23,8 @@ from panda3d.core import (
|
||||
from .config import apply_panda_runtime_config
|
||||
from .scene import build_preview_scene
|
||||
from .types import CameraState, SceneNodeSnapshot
|
||||
from src.TransformGizmo.transform_gizmo import TransformGizmo, TransformGizmoMode
|
||||
from src.transform_gizmo.transform_gizmo import TransformGizmo, TransformGizmoMode
|
||||
from src.camera_view_gizmo import CameraViewGizmo, CameraViewGizmoConfig
|
||||
from src.tools.camera_orbit_controller import CameraOrbitController
|
||||
from src.tools.picker_ray import PickerRay
|
||||
|
||||
@ -121,6 +122,7 @@ class PandaRendererThread(threading.Thread):
|
||||
base: ShowBase | None = None
|
||||
camera_controller: CameraOrbitController | None = None
|
||||
transform_gizmo: TransformGizmo | None = None
|
||||
camera_view_gizmo: CameraViewGizmo | None = None
|
||||
picker_ray: PickerRay | None = None
|
||||
previous_task_signal = PandaTaskModule.signal
|
||||
try:
|
||||
@ -168,10 +170,10 @@ class PandaRendererThread(threading.Thread):
|
||||
use_emission_maps=True,
|
||||
use_occlusion_maps=True,
|
||||
enable_shadows=False,
|
||||
exposure=0.35,
|
||||
exposure=0.15,
|
||||
)
|
||||
|
||||
scene_root = build_preview_scene(base)
|
||||
scene_root = build_preview_scene(base, camera_np)
|
||||
scene_root.set_python_tag("scene_path", scene_root.get_name())
|
||||
self._update_scene_snapshot(scene_root)
|
||||
camera_controller = CameraOrbitController(
|
||||
@ -193,6 +195,16 @@ class PandaRendererThread(threading.Thread):
|
||||
self._sync_camera_snapshot(camera_controller)
|
||||
transform_gizmo = TransformGizmo(base, camera_np=camera_np, use_overlay=False)
|
||||
transform_gizmo.set_mode(TransformGizmoMode.MOVE)
|
||||
camera_view_gizmo = CameraViewGizmo(
|
||||
base,
|
||||
camera_np=camera_np,
|
||||
window=buffer,
|
||||
get_pivot=camera_controller.get_pivot,
|
||||
on_camera_changed=lambda _gizmo: camera_controller.sync_from_camera(camera_controller.get_pivot()),
|
||||
interaction_blocker=lambda: False,
|
||||
register_events=False,
|
||||
config=CameraViewGizmoConfig(size_px=132, overlay_sort=20),
|
||||
)
|
||||
picker_ray = PickerRay(base, pick_root=scene_root, camera=camera_np)
|
||||
self._attach_gizmo_to_selected(transform_gizmo)
|
||||
|
||||
@ -218,7 +230,7 @@ class PandaRendererThread(threading.Thread):
|
||||
last_time = now
|
||||
smoothed_dt = smoothed_dt * 0.9 + dt * 0.1
|
||||
|
||||
self._drain_input_events(camera_controller, transform_gizmo, picker_ray)
|
||||
self._drain_input_events(camera_controller, transform_gizmo, camera_view_gizmo, picker_ray)
|
||||
base.taskMgr.step()
|
||||
self._sync_camera_snapshot(camera_controller)
|
||||
|
||||
@ -260,6 +272,11 @@ class PandaRendererThread(threading.Thread):
|
||||
transform_gizmo.detach()
|
||||
except Exception:
|
||||
pass
|
||||
if camera_view_gizmo is not None:
|
||||
try:
|
||||
camera_view_gizmo.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
if base is not None:
|
||||
try:
|
||||
base.destroy()
|
||||
@ -270,6 +287,7 @@ class PandaRendererThread(threading.Thread):
|
||||
self,
|
||||
camera_controller: CameraOrbitController,
|
||||
transform_gizmo: TransformGizmo | None,
|
||||
camera_view_gizmo: CameraViewGizmo | None,
|
||||
picker_ray: PickerRay | None,
|
||||
) -> None:
|
||||
while True:
|
||||
@ -305,6 +323,19 @@ class PandaRendererThread(threading.Thread):
|
||||
x = payload.get("x")
|
||||
y = payload.get("y")
|
||||
ndc = self._event_ndc(payload)
|
||||
if x is not None and y is not None and camera_view_gizmo is not None:
|
||||
if button == "lmb" and down:
|
||||
camera_view_gizmo.handle_mouse1_down(float(x), float(y))
|
||||
elif button == "lmb" and not down:
|
||||
camera_view_gizmo.handle_mouse1_up(float(x), float(y))
|
||||
view_gizmo_captures_left = (
|
||||
camera_view_gizmo is not None
|
||||
and button == "lmb"
|
||||
and camera_view_gizmo.captures_mouse
|
||||
)
|
||||
if view_gizmo_captures_left:
|
||||
camera_controller.set_button(button, False)
|
||||
continue
|
||||
event_payload = {"x": ndc[0], "y": ndc[1], "ndc": True} if ndc is not None else None
|
||||
if transform_gizmo is not None:
|
||||
if button == "lmb" and down:
|
||||
@ -336,10 +367,18 @@ class PandaRendererThread(threading.Thread):
|
||||
elif kind == "mouse_move":
|
||||
x = payload.get("x")
|
||||
y = payload.get("y")
|
||||
if x is not None and y is not None and not (transform_gizmo is not None and transform_gizmo.is_dragging):
|
||||
if x is not None and y is not None and camera_view_gizmo is not None:
|
||||
camera_view_gizmo.handle_mouse_move(float(x), float(y))
|
||||
view_gizmo_captures = camera_view_gizmo is not None and camera_view_gizmo.captures_mouse
|
||||
if (
|
||||
x is not None
|
||||
and y is not None
|
||||
and not view_gizmo_captures
|
||||
and not (transform_gizmo is not None and transform_gizmo.is_dragging)
|
||||
):
|
||||
camera_controller.move_pointer(float(x), float(y))
|
||||
ndc = self._event_ndc(payload)
|
||||
if transform_gizmo is not None and ndc is not None:
|
||||
if transform_gizmo is not None and ndc is not None and not view_gizmo_captures:
|
||||
transform_gizmo.world.messenger.send("mouse-move", [{"x": ndc[0], "y": ndc[1], "ndc": True}])
|
||||
elif kind == "wheel":
|
||||
delta = float(payload.get("delta", 0.0))
|
||||
@ -352,6 +391,16 @@ class PandaRendererThread(threading.Thread):
|
||||
camera_controller.set_key(key, bool(payload.get("down", False)))
|
||||
elif kind == "focus":
|
||||
camera_controller.focus_on_current_target()
|
||||
elif kind == "undo_transform":
|
||||
if transform_gizmo is not None:
|
||||
transform_gizmo.undo_last()
|
||||
elif kind == "set_transform":
|
||||
path = str(payload.get("path", ""))
|
||||
field = str(payload.get("field", ""))
|
||||
value = payload.get("value")
|
||||
self._set_node_transform(path, field, value)
|
||||
if transform_gizmo is not None:
|
||||
self._attach_gizmo_to_selected(transform_gizmo)
|
||||
elif kind == "select_path":
|
||||
path = str(payload.get("path", ""))
|
||||
self._set_selected_node_path(path)
|
||||
@ -416,6 +465,22 @@ class PandaRendererThread(threading.Thread):
|
||||
with self._state_lock:
|
||||
self._selected_node_path = path
|
||||
|
||||
def _set_node_transform(self, path: str, field: str, value: object) -> None:
|
||||
node = self._node_path_lookup.get(path)
|
||||
if node is None or value is None:
|
||||
return
|
||||
try:
|
||||
x, y, z = (float(component) for component in value) # type: ignore[operator]
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
|
||||
if field == "pos":
|
||||
node.set_pos(x, y, z)
|
||||
elif field == "hpr":
|
||||
node.set_hpr(x, y, z)
|
||||
elif field == "scale":
|
||||
node.set_scale(x, y, z)
|
||||
|
||||
def _sync_camera_snapshot(self, camera_controller: CameraOrbitController) -> None:
|
||||
target_x, target_y, target_z, distance, yaw, pitch = camera_controller.camera_state_tuple()
|
||||
with self._state_lock:
|
||||
|
||||
@ -7,45 +7,47 @@ from panda3d.core import (
|
||||
AmbientLight,
|
||||
DirectionalLight,
|
||||
Material,
|
||||
PointLight,
|
||||
Vec3,
|
||||
NodePath
|
||||
)
|
||||
|
||||
from .types import CameraState
|
||||
|
||||
|
||||
def build_preview_scene(base: ShowBase):
|
||||
def _setup_editor_lighting(base: ShowBase, camera_np:NodePath) -> None:
|
||||
ambient = AmbientLight("editorAmbient")
|
||||
ambient.set_color((0.28, 0.30, 0.33, 1.0))
|
||||
base.render.set_light(base.render.attachNewNode(ambient))
|
||||
|
||||
head_light = DirectionalLight("editorHeadLight")
|
||||
head_light.set_color((0.72, 0.75, 0.80, 1.0))
|
||||
head_np = camera_np.attachNewNode(head_light)
|
||||
base.render.set_light(head_np)
|
||||
|
||||
key_light = DirectionalLight("editorKeyLight")
|
||||
key_light.set_color((0.62, 0.60, 0.54, 1.0))
|
||||
key_np = base.render.attachNewNode(key_light)
|
||||
key_np.set_hpr(-35.0, -45.0, 0.0)
|
||||
base.render.set_light(key_np)
|
||||
|
||||
fill_light = DirectionalLight("editorFillLight")
|
||||
fill_light.set_color((0.34, 0.39, 0.47, 1.0))
|
||||
fill_np = base.render.attachNewNode(fill_light)
|
||||
fill_np.set_hpr(130.0, -20.0, 0.0)
|
||||
base.render.set_light(fill_np)
|
||||
|
||||
rim_light = DirectionalLight("editorRimLight")
|
||||
rim_light.set_color((0.28, 0.30, 0.34, 1.0))
|
||||
rim_np = base.render.attachNewNode(rim_light)
|
||||
rim_np.set_hpr(180.0, -10.0, 0.0)
|
||||
base.render.set_light(rim_np)
|
||||
|
||||
|
||||
def build_preview_scene(base: ShowBase, camera_np):
|
||||
base.render.set_shader_auto()
|
||||
base.setBackgroundColor(0.16, 0.17, 0.19, 1.0)
|
||||
base.disableMouse()
|
||||
|
||||
ambient = AmbientLight("ambient")
|
||||
ambient.set_color((0.10, 0.10, 0.11, 1.0))
|
||||
base.render.set_light(base.render.attachNewNode(ambient))
|
||||
|
||||
key_light = DirectionalLight("keyLight")
|
||||
key_light.set_color((1.45, 1.40, 1.30, 1.0))
|
||||
key_np = base.render.attachNewNode(key_light)
|
||||
key_np.set_hpr(-38.0, -34.0, 0.0)
|
||||
base.render.set_light(key_np)
|
||||
|
||||
fill_light = DirectionalLight("fillLight")
|
||||
fill_light.set_color((0.38, 0.44, 0.52, 1.0))
|
||||
fill_np = base.render.attachNewNode(fill_light)
|
||||
fill_np.set_hpr(118.0, -18.0, 0.0)
|
||||
base.render.set_light(fill_np)
|
||||
|
||||
rim_light = DirectionalLight("rimLight")
|
||||
rim_light.set_color((0.30, 0.28, 0.26, 1.0))
|
||||
rim_np = base.render.attachNewNode(rim_light)
|
||||
rim_np.set_hpr(180.0, -8.0, 0.0)
|
||||
base.render.set_light(rim_np)
|
||||
|
||||
bounce_light = PointLight("bounceLight")
|
||||
bounce_light.set_color((0.22, 0.24, 0.28, 1.0))
|
||||
bounce_np = base.render.attachNewNode(bounce_light)
|
||||
bounce_np.set_pos(0.0, -1.5, 4.0)
|
||||
base.render.set_light(bounce_np)
|
||||
_setup_editor_lighting(base, camera_np)
|
||||
|
||||
scene_root = base.render.attachNewNode("SceneRoot")
|
||||
preview_root = scene_root.attachNewNode("PreviewRoot")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user