414 lines
17 KiB
Python
414 lines
17 KiB
Python
"""
|
|
|
|
RenderPipeline
|
|
|
|
Copyright (c) 2014-2016 tobspr <tobias.springer1@gmail.com>
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
|
|
"""
|
|
|
|
|
|
# Disable the "exactly one space required after comma" message, for the input
|
|
# bindings it looks nicer to insert some spaces (see the setup method)
|
|
# pylint: disable=bad-whitespace
|
|
|
|
from __future__ import print_function
|
|
|
|
import sys
|
|
import os
|
|
import subprocess
|
|
from typing import Optional
|
|
|
|
from panda3d.core import ModifierButtons, Vec3, PStatClient
|
|
from panda3d.core import Point3, CurveFitter
|
|
from panda3d.core import KeyboardButton,MouseButton,Point2,LPoint2f,NodePath
|
|
from direct.showbase.ShowBase import ShowBase
|
|
import math
|
|
|
|
|
|
class MovementController(object):
|
|
|
|
""" This is a helper class, used to controll the camera and enable various
|
|
debugging features. It is not really part of the pipeline, but included to
|
|
view the demo scenes. """
|
|
|
|
def __init__(self, showbase):
|
|
self.showbase:ShowBase = showbase
|
|
self.movement = [0, 0, 0]
|
|
self.velocity = Vec3(0.0)
|
|
self.hpr_movement = [0, 0]
|
|
self.speed = 0.4
|
|
self.initial_position = Vec3(0)
|
|
self.initial_destination = Vec3(0)
|
|
self.initial_hpr = Vec3(0)
|
|
self.mouse_enabled = False
|
|
self.last_mouse_pos = [0, 0]
|
|
self.mouse_sensivity = 0.7
|
|
self.keyboard_hpr_speed = 0.4
|
|
self.use_hpr = False
|
|
self.smoothness = 6.0
|
|
self.bobbing_amount = 1.5
|
|
self.bobbing_speed = 0.5
|
|
|
|
def set_initial_position(self, pos, target):
|
|
""" Sets the initial camera position """
|
|
self.initial_position = pos
|
|
self.initial_destination = target
|
|
self.use_hpr = False
|
|
self.reset_to_initial()
|
|
|
|
def set_initial_position_hpr(self, pos, hpr):
|
|
""" Sets the initial camera position """
|
|
self.initial_position = pos
|
|
self.initial_hpr = hpr
|
|
self.use_hpr = True
|
|
self.reset_to_initial()
|
|
|
|
def reset_to_initial(self):
|
|
""" Resets the camera to the initial position """
|
|
self.showbase.camera.set_pos(self.initial_position)
|
|
|
|
if self.use_hpr:
|
|
self.showbase.camera.set_hpr(self.initial_hpr)
|
|
else:
|
|
self.showbase.camera.look_at(
|
|
self.initial_destination.x, self.initial_destination.y,
|
|
self.initial_destination.z)
|
|
|
|
def set_movement(self, direction, amount):
|
|
self.movement[direction] = amount
|
|
|
|
def set_hpr_movement(self, direction, amount):
|
|
self.hpr_movement[direction] = amount
|
|
|
|
def set_mouse_enabled(self, enabled):
|
|
self.mouse_enabled = enabled
|
|
|
|
def increase_speed(self):
|
|
self.speed *= 1.4
|
|
|
|
def decrease_speed(self):
|
|
self.speed *= 0.6
|
|
|
|
def unbind(self):
|
|
""" Unbinds the movement controler and restores the previous state """
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def clock_obj(self):
|
|
""" Returns the global clock object """
|
|
return self.showbase.taskMgr.globalClock
|
|
|
|
def setup(self):
|
|
""" Attaches the movement controller and inits the keybindings """
|
|
# x
|
|
self.showbase.accept("raw-w", self.set_movement, [0, 1])
|
|
self.showbase.accept("raw-w-up", self.set_movement, [0, 0])
|
|
self.showbase.accept("raw-s", self.set_movement, [0, -1])
|
|
self.showbase.accept("raw-s-up", self.set_movement, [0, 0])
|
|
|
|
# y
|
|
self.showbase.accept("raw-a", self.set_movement, [1, -1])
|
|
self.showbase.accept("raw-a-up", self.set_movement, [1, 0])
|
|
self.showbase.accept("raw-d", self.set_movement, [1, 1])
|
|
self.showbase.accept("raw-d-up", self.set_movement, [1, 0])
|
|
|
|
# z
|
|
self.showbase.accept("space", self.set_movement, [2, 1])
|
|
self.showbase.accept("space-up", self.set_movement, [2, 0])
|
|
self.showbase.accept("raw-e", self.set_movement, [2, 1])
|
|
self.showbase.accept("raw-e-up", self.set_movement, [2, 0])
|
|
# self.showbase.accept("shift", self.set_movement, [2, -1])
|
|
# self.showbase.accept("shift-up", self.set_movement, [2, 0])
|
|
self.showbase.accept("raw-q", self.set_movement, [2, -1])
|
|
self.showbase.accept("raw-q-up", self.set_movement, [2, 0])
|
|
|
|
# wireframe + debug + buffer viewer
|
|
self.showbase.accept("f3", self.showbase.toggle_wireframe)
|
|
self.showbase.accept("f11", lambda: self.showbase.win.save_screenshot("screenshot.png"))
|
|
self.showbase.accept("j", self.print_position)
|
|
|
|
# mouse
|
|
self.showbase.accept("mouse1", self.set_mouse_enabled, [True])
|
|
self.showbase.accept("mouse1-up", self.set_mouse_enabled, [False])
|
|
|
|
# arrow mouse navigation
|
|
self.showbase.accept("arrow_up", self.set_hpr_movement, [1, 1])
|
|
self.showbase.accept("arrow_up-up", self.set_hpr_movement, [1, 0])
|
|
self.showbase.accept("arrow_down", self.set_hpr_movement, [1, -1])
|
|
self.showbase.accept("arrow_down-up", self.set_hpr_movement, [1, 0])
|
|
self.showbase.accept("arrow_left", self.set_hpr_movement, [0, 1])
|
|
self.showbase.accept("arrow_left-up", self.set_hpr_movement, [0, 0])
|
|
self.showbase.accept("arrow_right", self.set_hpr_movement, [0, -1])
|
|
self.showbase.accept("arrow_right-up", self.set_hpr_movement, [0, 0])
|
|
|
|
# increase / decrease speed
|
|
self.showbase.accept("+", self.increase_speed)
|
|
self.showbase.accept("-", self.decrease_speed)
|
|
|
|
# disable modifier buttons to be able to move while pressing shift for
|
|
# example
|
|
self.showbase.mouseWatcherNode.set_modifier_buttons(ModifierButtons())
|
|
self.showbase.buttonThrowers[0].node().set_modifier_buttons(ModifierButtons())
|
|
|
|
# disable pandas builtin mouse control
|
|
self.showbase.disableMouse()
|
|
|
|
# add ourself as an update task which gets executed very early before
|
|
# the rendering
|
|
self.update_task = self.showbase.addTask(self.update, "RP_UpdateMovementController", sort=-40)
|
|
|
|
# Hotkeys to connect to pstats and reset the initial position
|
|
self.showbase.accept("1", PStatClient.connect)
|
|
self.showbase.accept("3", self.reset_to_initial)
|
|
|
|
self.focus_distance = 8.0
|
|
self.focus_target:Optional[NodePath] = None
|
|
self.last_mouse = None
|
|
self.pivot = Vec3(0, 0, 0)
|
|
self.radius = self.focus_distance
|
|
self.azimuth = 0.0
|
|
self.elevation = 0.0
|
|
self.showbase.accept("mouse1", self._orbit_mouse1)
|
|
self.showbase.accept("alt-mouse1",self._orbit_mouse1)
|
|
self.showbase.task_mgr.add(self._update_orbit, "update_orbit")
|
|
|
|
self.last_mouse_pan = None
|
|
self.showbase.accept("mouse2", self._on_mouse2_down)
|
|
self.showbase.accept("wheel_up", lambda: self._on_wheel(1))
|
|
self.showbase.accept("wheel_down", lambda: self._on_wheel(-1))
|
|
|
|
def _on_mouse2_down(self):
|
|
self.last_mouse_pan = Point2(self.showbase.mouseWatcherNode.getMouse())
|
|
|
|
def _on_wheel(self, direction):
|
|
# direction: +1 for wheel_up (zoom in), -1 for wheel_down (zoom out)
|
|
zoom_factor = 0.9 if direction > 0 else 1.1
|
|
self.focus_distance += zoom_factor
|
|
# self.radius = max(0.5, min(200.0, self.focus_distance * zoom_factor))
|
|
forward = self.showbase.camera.getQuat(self.showbase.render).getForward()
|
|
self.showbase.camera.set_pos(self.showbase.camera.get_pos() + forward.normalized() * direction * zoom_factor)
|
|
|
|
def _orbit_mouse1(self):
|
|
forward = self.showbase.camera.getQuat(self.showbase.render).getForward()
|
|
if self.focus_target and not self.focus_target.is_empty():
|
|
self.pivot = self.focus_target.get_pos(self.showbase.render)
|
|
else:
|
|
self.pivot = self.showbase.camera.getPos(self.showbase.render) + forward.normalized() * self.focus_distance
|
|
offset = self.showbase.camera.getPos(self.showbase.render) - self.pivot
|
|
self.radius = offset.length()
|
|
if self.radius < 1e-5:
|
|
self.radius = self.focus_distance
|
|
offset = Vec3(0, -self.radius, 0)
|
|
self.azimuth = math.atan2(offset.x, offset.y)
|
|
self.elevation = math.asin(max(-1.0, min(1.0, offset.z / self.radius)))
|
|
self.last_mouse = Point2(self.showbase.mouseWatcherNode.get_mouse())
|
|
|
|
def _refresh_camera_from_angles(self):
|
|
cos_el = math.cos(self.elevation)
|
|
x = math.sin(self.azimuth) * cos_el
|
|
y = math.cos(self.azimuth) * cos_el
|
|
z = math.sin(self.elevation)
|
|
|
|
self.showbase.camera.setPos(self.pivot + Vec3(x, y, z) * self.radius)
|
|
self.showbase.camera.lookAt(self.pivot)
|
|
|
|
def _update_orbit(self, task):
|
|
if self.showbase.mouseWatcherNode.isButtonDown(MouseButton.two()):
|
|
current = Point2(self.showbase.mouseWatcherNode.getMouse())
|
|
if self.last_mouse_pan is not None:
|
|
delta = current - self.last_mouse_pan
|
|
right = self.showbase.camera.getQuat(self.showbase.render).getRight()
|
|
up = self.showbase.camera.getQuat(self.showbase.render).getUp()
|
|
pan_scale = -self.radius * 0.5
|
|
offset = (right * delta.x + up * delta.y) * pan_scale
|
|
self.showbase.camera.setPos(self.showbase.camera.getPos(self.showbase.render) + offset)
|
|
self.last_mouse_pan = current
|
|
return task.cont
|
|
|
|
if not self.showbase.mouseWatcherNode.isButtonDown(MouseButton.one()) or not self.showbase.mouseWatcherNode.isButtonDown(KeyboardButton.alt()):
|
|
return task.cont
|
|
|
|
if not self.showbase.mouseWatcherNode.hasMouse() or self.last_mouse is None:
|
|
return task.cont
|
|
|
|
current = Point2(self.showbase.mouseWatcherNode.getMouse())
|
|
delta = current - self.last_mouse
|
|
self.last_mouse = current
|
|
|
|
# Screen space is [-1, 1], so multiply by a comfortable radians-per-unit factor.
|
|
rot_speed = math.pi # 180° per full screen swipe.
|
|
self.azimuth += delta.x * rot_speed
|
|
self.elevation -= delta.y * rot_speed
|
|
self.elevation = max(math.radians(-85.0), min(math.radians(85.0), self.elevation))
|
|
|
|
self._refresh_camera_from_angles()
|
|
|
|
return task.cont
|
|
|
|
def focus_on(self,node:NodePath,margin=1.2):
|
|
"""聚焦:让 cam_np 对准 node 并退到能完整看见它的位置。
|
|
margin > 1 给一点边距。"""
|
|
if not node or node.is_empty():
|
|
return
|
|
self.focus_target = node
|
|
cam_np = self.showbase.camera
|
|
lens = self.showbase.camLens
|
|
bounds = node.get_tight_bounds(self.showbase.render)
|
|
if not bounds or None in bounds:
|
|
return # 目标没几何或未就位
|
|
bmin, bmax = bounds
|
|
center = (bmin + bmax) * 0.5
|
|
radius = (bmax - bmin).length() * 0.5
|
|
|
|
# 取水平/垂直 FOV 较小的那个作为限制
|
|
fovx, fovy = [math.radians(a) for a in lens.get_fov()]
|
|
fov = min(fovx, fovy)
|
|
dist = (radius / math.tan(fov * 0.5)) * margin
|
|
|
|
forward = cam_np.get_quat(self.showbase.render).get_forward() # 相机当前朝向
|
|
cam_np.set_pos(center - forward * dist)
|
|
cam_np.look_at(center)
|
|
|
|
def print_position(self):
|
|
""" Prints the camera position and hpr """
|
|
pos = self.showbase.cam.get_pos(self.showbase.render)
|
|
hpr = self.showbase.cam.get_hpr(self.showbase.render)
|
|
print("(Vec3({}, {}, {}), Vec3({}, {}, {})),".format(
|
|
pos.x, pos.y, pos.z, hpr.x, hpr.y, hpr.z))
|
|
|
|
def update(self, task):
|
|
""" Internal update method """
|
|
|
|
delta = self.clock_obj.get_dt()
|
|
|
|
# Update mouse first
|
|
if self.showbase.mouseWatcherNode.has_mouse():
|
|
x = self.showbase.mouseWatcherNode.get_mouse_x()
|
|
y = self.showbase.mouseWatcherNode.get_mouse_y()
|
|
self.current_mouse_pos = (x * self.showbase.camLens.get_fov().x * self.mouse_sensivity,
|
|
y * self.showbase.camLens.get_fov().y * self.mouse_sensivity)
|
|
|
|
if self.mouse_enabled:
|
|
diffx = self.last_mouse_pos[0] - self.current_mouse_pos[0]
|
|
diffy = self.last_mouse_pos[1] - self.current_mouse_pos[1]
|
|
|
|
# Don't move in the very beginning
|
|
if self.last_mouse_pos[0] == 0 and self.last_mouse_pos[1] == 0:
|
|
diffx = 0
|
|
diffy = 0
|
|
|
|
self.showbase.camera.set_h(self.showbase.camera.get_h() + diffx)
|
|
self.showbase.camera.set_p(self.showbase.camera.get_p() - diffy)
|
|
|
|
self.last_mouse_pos = self.current_mouse_pos[:]
|
|
|
|
# Compute movement in render space
|
|
movement_direction = (Vec3(self.movement[1], self.movement[0], 0) *
|
|
self.speed * delta * 100.0)
|
|
|
|
# transform by the camera direction
|
|
camera_quaternion = self.showbase.camera.get_quat(self.showbase.render)
|
|
translated_direction = camera_quaternion.xform(movement_direction)
|
|
|
|
# z-force is inddpendent of camera direction
|
|
translated_direction.add_z(
|
|
self.movement[2] * delta * 120.0 * self.speed)
|
|
|
|
self.velocity += translated_direction * 0.15
|
|
|
|
# apply the new position
|
|
self.showbase.camera.set_pos(self.showbase.camera.get_pos() + self.velocity)
|
|
|
|
# transform rotation (keyboard keys)
|
|
rotation_speed = self.keyboard_hpr_speed * 100.0
|
|
rotation_speed *= delta
|
|
self.showbase.camera.set_hpr(
|
|
self.showbase.camera.get_hpr() + Vec3(
|
|
self.hpr_movement[0], self.hpr_movement[1], 0) * rotation_speed)
|
|
|
|
# fade out velocity
|
|
self.velocity = self.velocity * max(
|
|
0.0, 1.0 - delta * 60.0 / max(0.01, self.smoothness))
|
|
|
|
# bobbing
|
|
ftime = self.clock_obj.get_frame_time()
|
|
rotation = (ftime % self.bobbing_speed) / self.bobbing_speed
|
|
rotation = (min(rotation, 1.0 - rotation) * 2.0 - 0.5) * 2.0
|
|
if self.velocity.length_squared() > 1e-5 and self.speed > 1e-5:
|
|
rotation *= self.bobbing_amount
|
|
rotation *= min(1, self.velocity.length()) / self.speed * 0.5
|
|
else:
|
|
rotation = 0
|
|
self.showbase.camera.set_r(rotation)
|
|
return task.cont
|
|
|
|
def play_motion_path(self, points, point_duration=1.2):
|
|
""" Plays a motion path from the given set of points """
|
|
fitter = CurveFitter()
|
|
for i, (pos, hpr) in enumerate(points):
|
|
fitter.add_xyz_hpr(i, pos, hpr)
|
|
|
|
fitter.compute_tangents(1.0)
|
|
curve = fitter.make_hermite()
|
|
print("Starting motion path with", len(points), "CVs")
|
|
|
|
self.showbase.render2d.hide()
|
|
self.showbase.aspect2d.hide()
|
|
|
|
self.curve = curve
|
|
self.curve_time_start = self.clock_obj.get_frame_time()
|
|
self.curve_time_end = self.clock_obj.get_frame_time() + len(points) * point_duration
|
|
self.delta_time_sum = 0.0
|
|
self.delta_time_count = 0
|
|
self.showbase.addTask(self.camera_motion_update, "RP_CameraMotionPath", sort=-50)
|
|
self.showbase.taskMgr.remove(self.update_task)
|
|
|
|
def camera_motion_update(self, task):
|
|
if self.clock_obj.get_frame_time() > self.curve_time_end:
|
|
print("Camera motion path finished")
|
|
|
|
# Print performance stats
|
|
avg_ms = self.delta_time_sum / self.delta_time_count
|
|
print("Average frame time (ms): {:4.1f}".format(avg_ms * 1000.0))
|
|
print("Average frame rate: {:4.1f}".format(1.0 / avg_ms))
|
|
|
|
self.update_task = self.showbase.addTask(
|
|
self.update, "RP_UpdateMovementController", sort=-50)
|
|
self.showbase.render2d.show()
|
|
self.showbase.aspect2d.show()
|
|
return task.done
|
|
|
|
lerp = (self.clock_obj.get_frame_time() - self.curve_time_start) /\
|
|
(self.curve_time_end - self.curve_time_start)
|
|
lerp *= self.curve.get_max_t()
|
|
|
|
pos, hpr = Point3(0), Vec3(0)
|
|
self.curve.evaluate_xyz(lerp, pos)
|
|
self.curve.evaluate_hpr(lerp, hpr)
|
|
|
|
self.showbase.camera.set_pos(pos)
|
|
self.showbase.camera.set_hpr(hpr)
|
|
|
|
self.delta_time_sum += self.clock_obj.get_dt()
|
|
self.delta_time_count += 1
|
|
|
|
return task.cont
|