From da30e3f0c7b46a9d174b384cc90a32db714f187a Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 8 Jul 2018 22:04:57 -0400 Subject: [PATCH 01/10] Implement basic cameras This implments basic follow and fixed cameras. Currently, animating cameras and/or their field of view are/is not supported. --- korman/exporter/camera.py | 104 +++++++++++++++++ korman/exporter/convert.py | 7 ++ korman/idprops.py | 3 + korman/properties/__init__.py | 2 + korman/properties/modifiers/region.py | 61 ++++++++++ korman/properties/prop_camera.py | 160 ++++++++++++++++++++++++++ korman/render.py | 4 + korman/ui/__init__.py | 1 + korman/ui/modifiers/region.py | 8 ++ korman/ui/ui_camera.py | 128 +++++++++++++++++++++ 10 files changed, 478 insertions(+) create mode 100644 korman/exporter/camera.py create mode 100644 korman/properties/prop_camera.py create mode 100644 korman/ui/ui_camera.py diff --git a/korman/exporter/camera.py b/korman/exporter/camera.py new file mode 100644 index 0000000..7524e70 --- /dev/null +++ b/korman/exporter/camera.py @@ -0,0 +1,104 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy +import math +from PyHSPlasma import * +import weakref + +from .explosions import * + +class CameraConverter: + def __init__(self, exporter): + self._exporter = weakref.ref(exporter) + + def _convert_brain(self, so, bo, camera_props, brain): + trans_props = camera_props.transition + + brain.poaOffset = hsVector3(*camera_props.poa_offset) + if isinstance(brain, plCameraBrain1_Avatar): + brain.offset = hsVector3(*camera_props.pos_offset) + if camera_props.poa_type == "object": + brain.subject = self._mgr.find_create_key(plSceneObject, bl=camera_props.poa_object) + + brain.xPanLimit = camera_props.x_pan_angle / 2.0 + brain.zPanLimit = camera_props.y_pan_angle / 2.0 + brain.panSpeed = camera_props.pan_rate + if camera_props.limit_zoom: + brain.setFlags(plCameraBrain1.kZoomEnabled, True) + brain.zoomMax = camera_props.zoom_max * (4.0 / 3.0) + brain.zoomMin = camera_props.zoom_min * (4.0 / 3.0) + brain.zoomRate = camera_props.zoom_rate + + brain.acceleration = trans_props.pos_acceleration + brain.deceleration = trans_props.pos_deceleration + brain.velocity = trans_props.pos_velocity + brain.poaAcceleration = trans_props.poa_acceleration + brain.poaDeceleration = trans_props.poa_deceleration + brain.poaVelocity = trans_props.poa_velocity + + if trans_props.pos_cut: + brain.setFlags(plCameraBrain1.kCutPos, True) + if trans_props.poa_cut: + brain.setFlags(plCameraBrain1.kCutPOA, True) + if camera_props.poa_type == "avatar": + brain.setFlags(plCameraBrain1.kFollowLocalAvatar, True) + if camera_props.maintain_los: + brain.setFlags(plCameraBrain1.kMaintainLOS, True) + if camera_props.poa_worldspace: + brain.setFlags(plCameraBrain1.kWorldspacePOA, True) + if camera_props.pos_worldspace: + brain.setFlags(plCameraBrain1.kWorldspacePos, True) + if camera_props.ignore_subworld: + brain.setFlags(plCameraBrain1.kIgnoreSubworldMovement, True) + if camera_props.fall_vertical: + brain.setFlags(plCameraBrain1.kVerticalWhenFalling, True) + if camera_props.fast_run: + brain.setFlags(plCameraBrain1.kSpeedUpWhenRunning, True) + + def export_camera(self, so, bo, camera_type, camera_props): + mod = self._export_camera_modifier(so, bo, camera_props) + brain = getattr(self, "_export_{}_camera".format(camera_type))(so, bo, camera_props) + mod.brain = brain.key + + def _export_camera_modifier(self, so, bo, props): + # PlasmaMAX allows the user to specify the horizontal OR vertical FOV, but not both. + # We only allow setting horizontal FOV (how often do you look up?), however. + # Plasma assumes 4:3 aspect ratio... + mod = self._mgr.find_create_object(plCameraModifier, so=so) + fov = props.fov + mod.fovW, mod.fovH = math.degrees(fov), math.degrees(fov * (3.0 / 4.0)) + + # TODO: do we need to do something about animations here? + return mod + + def _export_fixed_camera(self, so, bo, props): + brain = self._mgr.find_create_object(plCameraBrain1_Fixed, so=so) + self._convert_brain(so, bo, props, brain) + # TODO: animations??? + return brain + + def _export_follow_camera(self, so, bo, props): + brain = self._mgr.find_create_object(plCameraBrain1_Avatar, so=so) + self._convert_brain(so, bo, props, brain) + return brain + + @property + def _mgr(self): + return self._exporter().mgr + + @property + def _report(self): + return self._exporter().report diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 8bf5da0..75d7206 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -20,6 +20,7 @@ from PyHSPlasma import * import time from . import animation +from . import camera from . import explosions from . import etlight from . import logger @@ -52,6 +53,7 @@ class Exporter: self.light = rtlight.LightConverter(self) self.animation = animation.AnimationConverter(self) self.sumfile = sumfile.SumFile() + self.camera = camera.CameraConverter(self) # Step 0.8: Init the progress mgr self.mesh.add_progress_presteps(self.report) @@ -236,6 +238,11 @@ class Exporter: mod.export(self, bl_obj, sceneobject) inc_progress() + def _export_camera_blobj(self, so, bo): + # Hey, guess what? Blender's camera data is utter crap! + camera = bo.data.plasma_camera + self.camera.export_camera(so, bo, camera.camera_type, camera.settings) + def _export_empty_blobj(self, so, bo): # We don't need to do anything here. This function just makes sure we don't error out # or add a silly special case :( diff --git a/korman/idprops.py b/korman/idprops.py index 1423943..66328d2 100644 --- a/korman/idprops.py +++ b/korman/idprops.py @@ -124,6 +124,9 @@ def poll_animated_objects(self, value): return True return False +def poll_camera_objects(self, value): + return value.type == "CAMERA" + def poll_empty_objects(self, value): return value.type == "EMPTY" diff --git a/korman/properties/__init__.py b/korman/properties/__init__.py index dc98a70..aed2561 100644 --- a/korman/properties/__init__.py +++ b/korman/properties/__init__.py @@ -15,6 +15,7 @@ import bpy +from .prop_camera import * from .prop_lamp import * from . import modifiers from .prop_object import * @@ -23,6 +24,7 @@ from .prop_world import * def register(): + bpy.types.Camera.plasma_camera = bpy.props.PointerProperty(type=PlasmaCamera) bpy.types.Lamp.plasma_lamp = bpy.props.PointerProperty(type=PlasmaLamp) bpy.types.Object.plasma_net = bpy.props.PointerProperty(type=PlasmaNet) bpy.types.Object.plasma_object = bpy.props.PointerProperty(type=PlasmaObject) diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index 99cde54..dfff86a 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -22,6 +22,7 @@ from ...helpers import TemporaryObject from ... import idprops from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz +from ..prop_camera import PlasmaCameraProperties from .physics import bounds_types footstep_surface_ids = { @@ -58,6 +59,66 @@ footstep_surfaces = [("dirt", "Dirt", "Dirt"), ("woodfloor", "Wood Floor", "Wood Floor"), ("woodladder", "Wood Ladder", "Wood Ladder")] +class PlasmaCameraRegion(PlasmaModifierProperties): + pl_id = "camera_rgn" + + bl_category = "Region" + bl_label = "Camera Region" + bl_description = "Camera Region" + bl_icon = "CAMERA_DATA" + + camera_type = EnumProperty(name="Camera Type", + description="What kind of camera should be used?", + items=[("auto", "Auto Follow Camera", "Automatically generated camera"), + ("manual", "Manual Camera", "User specified camera object")], + options=set()) + camera_object = PointerProperty(name="Camera", + description="Switches to this camera", + type=bpy.types.Object, + poll=idprops.poll_camera_objects, + options=set()) + auto_camera = PointerProperty(type=PlasmaCameraProperties, options=set()) + + def export(self, exporter, bo, so): + if self.camera_type == "manual": + if self.camera_object is None: + raise ExportError("Camera Modifier '{}' does not specify a valid camera object".format(self.id_data.name)) + camera_so_key = exporter.mgr.find_create_key(plSceneObject, bl=self.camera_object) + camera_props = self.camera_object.data.plasma_camera.settings + else: + # Wheedoggy! We get to export the doggone camera now. + camera_props = self.auto_camera + exporter.camera.export_camera(so, bo, "follow", camera_props) + + # Setup physical stuff + phys_mod = bo.plasma_modifiers.collision + simIface, physical = exporter.physics.generate_physical(bo, so, phys_mod.bounds, self.key_name) + physical.memberGroup = plSimDefs.kGroupDetector + physical.reportGroup = 1 << plSimDefs.kGroupAvatar + simIface.setProperty(plSimulationInterface.kPinned, True) + physical.setProperty(plSimulationInterface.kPinned, True) + + # I don't feel evil enough to make this generate a logic tree... + msg = plCameraMsg() + msg.BCastFlags |= plMessage.kLocalPropagate | plMessage.kBCastByType + msg.setCmd(plCameraMsg.kRegionPushCamera) + if camera_props.primary_camera: + msg.setCmd(plCameraMsg.kSetAsPrimary) + msg.newCam = camera_so_key + + region = exporter.mgr.find_create_object(plCameraRegionDetector, so=so) + region.addMessage(msg) + + def harvest_actors(self): + if self.camera_type == "manual": + if self.camera_object is None: + raise ExportError("Camera Modifier '{}' does not specify a valid camera object".format(self.id_data.name)) + camera = self.camera_object.data.plasma_camera.settings + else: + camera = self.auto_camera + return camera.harvest_actors() + + class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz): pl_id = "footstep" diff --git a/korman/properties/prop_camera.py b/korman/properties/prop_camera.py new file mode 100644 index 0000000..fd5b7fe --- /dev/null +++ b/korman/properties/prop_camera.py @@ -0,0 +1,160 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy +from bpy.props import * +import math + +camera_types = [("follow", "Follow Camera", "Camera follows an object."), + ("fixed", "Fixed Camera", "Camera is fixed in one location.")] + +class PlasmaTransition(bpy.types.PropertyGroup): + poa_acceleration = FloatProperty(name="PoA Acceleration", + description="Rate the camera's Point of Attention tracking velocity increases in feet per second squared", + min=-100.0, max=100.0, precision=0, default=60.0, + unit="ACCELERATION", options=set()) + poa_deceleration = FloatProperty(name="PoA Deceleration", + description="Rate the camera's Point of Attention tracking velocity decreases in feet per second squared", + min=-100.0, max=100.0, precision=0, default=60.0, + unit="ACCELERATION", options=set()) + poa_velocity = FloatProperty(name="PoA Velocity", + description="Maximum velocity of the camera's Point of Attention tracking", + min=-100.0, max=100.0, precision=0, default=60.0, + unit="VELOCITY", options=set()) + poa_cut = BoolProperty(name="Cut Transition", + description="The camera immediately begins tracking the Point of Attention", + options=set()) + + pos_acceleration = FloatProperty(name="Position Acceleration", + description="Rate the camera's positional velocity increases in feet per second squared", + min=-100.0, max=100.0, precision=0, default=60.0, + unit="ACCELERATION", options=set()) + pos_deceleration = FloatProperty(name="Position Deceleration", + description="Rate the camera's positional velocity decreases in feet per second squared", + min=-100.0, max=100.0, precision=0, default=60.0, + unit="ACCELERATION", options=set()) + pos_velocity = FloatProperty(name="Position Max Velocity", + description="Maximum positional velocity of the camera", + min=-100.0, max=100.0, precision=0, default=60.0, + unit="VELOCITY", options=set()) + pos_cut = BoolProperty(name="Cut Transition", + description="The camera immediately moves to its new position", + options=set()) + + +class PlasmaCameraProperties(bpy.types.PropertyGroup): + # Point of Attention + poa_type = EnumProperty(name="Point of Attention", + description="The point of attention that this camera tracks", + items=[("avatar", "Track Local Player", "Camera tracks the player's avatar"), + ("object", "Track Object", "Camera tracks an object in the scene"), + ("none", "Don't Track", "Camera does not track anything")], + options=set()) + poa_object = PointerProperty(name="PoA Object", + description="Object the camera should track as its Point of Attention", + type=bpy.types.Object, + options=set()) + poa_offset = FloatVectorProperty(name="PoA Offset", + description="Offset from the point of attention's origin to track", + soft_min=-50.0, soft_max=50.0, + size=3, default=(0.0, 0.0, 3.0), + options=set()) + poa_worldspace = BoolProperty(name="Worldspace Offset", + description="Point of Attention Offset is in worldspace coordinates", + options=set()) + + # Position Offset + pos_offset = FloatVectorProperty(name="Position Offset", + description="Offset the camera's position", + soft_min=-50.0, soft_max=50.0, + size=3, default=(0.0, 10.0, 3.0), + options=set()) + pos_worldspace = BoolProperty(name="Worldspace Offset", + description="Position offset is in worldspace coordinates", + options=set()) + + # Default Transition + transition = PointerProperty(type=PlasmaTransition, options=set()) + + # Limit Panning + x_pan_angle = FloatProperty(name="X Degrees", + description="Maximum camera pan angle in the X direction", + min=0.0, max=math.radians(180.0), precision=0, default=math.radians(90.0), + subtype="ANGLE", options=set()) + y_pan_angle = FloatProperty(name="Y Degrees", + description="Maximum camera pan angle in the Y direction", + min=0.0, max=math.radians(180.0), precision=0, default=math.radians(90.0), + subtype="ANGLE", options=set()) + pan_rate = FloatProperty(name="Pan Velocity", + description="", + min=0.0, precision=1, default=50.0, + unit="VELOCITY", options=set()) + + # Zooming + fov = FloatProperty(name="Default FOV", + description="Horizontal Field of View angle", + min=0.0, max=math.radians(180.0), precision=0, default=math.radians(70.0), + subtype="ANGLE") + limit_zoom = BoolProperty(name="Limit Zoom", + description="The camera allows zooming per artist limitations", + options=set()) + zoom_max = FloatProperty(name="Max FOV", + description="Maximum camera FOV when zooming", + min=0.0, max=math.radians(180.0), precision=0, default=math.radians(120.0), + subtype="ANGLE", options=set()) + zoom_min = FloatProperty(name="Min FOV", + description="Minimum camera FOV when zooming", + min=0.0, max=math.radians(180.0), precision=0, default=math.radians(35.0), + subtype="ANGLE", options=set()) + zoom_rate = FloatProperty(name="Zoom Velocity", + description="Velocity of the camera's zoom in degrees per second", + min=0.0, max=180.0, precision=0, default=90.0, + unit="VELOCITY", options=set()) + + # Miscellaneous Movement Props + maintain_los = BoolProperty(name="Maintain LOS", + description="The camera should maintain line-of-sight with the object it's tracking", + options=set()) + fall_vertical = BoolProperty(name="Fall Camera", + description="The camera will orient itself vertically when the local player begins falling", + options=set()) + fast_run = BoolProperty(name="Faster When Falling", + description="The camera's velocity will have a floor when the local player is falling", + options=set()) + ignore_subworld = BoolProperty(name="Ignore Subworld Movement", + description="The camera will not be parented to any subworlds", + options=set()) + + # Core Type Properties + primary_camera = BoolProperty(name="Primary Camera", + description="The camera should be considered the Age's primary camera.", + options=set()) + + def harvest_actors(self): + if self.poa_type == "object": + return set((self.poa_object.name),) + return set() + + +class PlasmaCamera(bpy.types.PropertyGroup): + camera_type = EnumProperty(name="Camera Type", + description="", + items=camera_types, + options=set()) + settings = PointerProperty(type=PlasmaCameraProperties, options=set()) + transitions = CollectionProperty(type=PlasmaTransition, + name="Transitions", + description="", + options=set()) diff --git a/korman/render.py b/korman/render.py index 393b9eb..e51304e 100644 --- a/korman/render.py +++ b/korman/render.py @@ -36,6 +36,10 @@ properties_material.MATERIAL_PT_transp.COMPAT_ENGINES.add("PLASMA_GAME") properties_material.MATERIAL_PT_shadow.COMPAT_ENGINES.add("PLASMA_GAME") del properties_material +from bl_ui import properties_data_camera +properties_data_camera.DATA_PT_context_camera.COMPAT_ENGINES.add("PLASMA_GAME") +del properties_data_camera + from bl_ui import properties_data_mesh properties_data_mesh.DATA_PT_uv_texture.COMPAT_ENGINES.add("PLASMA_GAME") properties_data_mesh.DATA_PT_vertex_colors.COMPAT_ENGINES.add("PLASMA_GAME") diff --git a/korman/ui/__init__.py b/korman/ui/__init__.py index c1f3e3f..dde0d43 100644 --- a/korman/ui/__init__.py +++ b/korman/ui/__init__.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU General Public License # along with Korman. If not, see . +from .ui_camera import * from .ui_lamp import * from .ui_list import * from .ui_menus import * diff --git a/korman/ui/modifiers/region.py b/korman/ui/modifiers/region.py index 58b6b23..80dc272 100644 --- a/korman/ui/modifiers/region.py +++ b/korman/ui/modifiers/region.py @@ -14,6 +14,14 @@ # along with Korman. If not, see . import bpy +from ..ui_camera import draw_camera_properties + +def camera_rgn(modifier, layout, context): + layout.prop(modifier, "camera_type") + if modifier.camera_type == "manual": + layout.prop(modifier, "camera_object", icon="CAMERA_DATA") + else: + draw_camera_properties("follow", modifier.auto_camera, layout, context) def footstep(modifier, layout, context): layout.prop(modifier, "bounds") diff --git a/korman/ui/ui_camera.py b/korman/ui/ui_camera.py new file mode 100644 index 0000000..43a3e71 --- /dev/null +++ b/korman/ui/ui_camera.py @@ -0,0 +1,128 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy + +class CameraButtonsPanel: + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "data" + + @classmethod + def poll(cls, context): + return (context.camera and context.scene.render.engine == "PLASMA_GAME") + + +class PlasmaCameraPanel(CameraButtonsPanel, bpy.types.Panel): + bl_label = "Plasma Camera" + + def draw(self, context): + camera = context.camera.plasma_camera + layout = self.layout + + layout.prop(camera, "camera_type") + layout.separator() + draw_camera_properties(camera.camera_type, camera.settings, layout, context) + + +class PlasmaCameraTransitionPanel(CameraButtonsPanel, bpy.types.Panel): + bl_label = "Plasma Transitions" + + def draw(self, context): + pass + + +def draw_camera_properties(cam_type, props, layout, context): + trans = props.transition + + def _draw_gated_prop(layout, props, gate_prop, actual_prop): + row = layout.row(align=True) + row.prop(props, gate_prop, text="") + row = row.row(align=True) + row.active = getattr(props, gate_prop) + row.prop(props, actual_prop) + + # Point of Attention + split = layout.split() + col = split.column() + col.label("Camera Mode:") + col = col.column() + col.alert = cam_type == "follow" and props.poa_type == "none" + col.prop(props, "poa_type", text="") + col.alert = False + row = col.row() + row.active = props.poa_type == "object" + row.prop(props, "poa_object", text="") + col.separator() + col.prop(props, "primary_camera") + + # Miscellaneous + col = split.column() + col.label("Tracking Settings:") + col.prop(props, "maintain_los") + col.prop(props, "fall_vertical") + col.prop(props, "fast_run") + col.prop(props, "ignore_subworld") + + # PoA Tracking + layout.separator() + split = layout.split() + col = split.column() + col.label("Default Tracking Transition:") + col.prop(trans, "poa_acceleration", text="Acceleration") + col.prop(trans, "poa_deceleration", text="Deceleration") + col.prop(trans, "poa_velocity", text="Maximum Velocity") + col.prop(trans, "poa_cut") + + # PoA Offset + col = split.column() + col.label("Point of Attention Offset:") + col.prop(props, "poa_offset", text="") + col.prop(props, "poa_worldspace") + + # Position Tracking (only for follow cams) + layout.separator() + split = layout.split() + col = split.column() + + # Position Transitions + col.label("Default Position Transition:") + col.prop(trans, "pos_acceleration", text="Acceleration") + col.prop(trans, "pos_deceleration", text="Deceleration") + col.prop(trans, "pos_velocity", text="Maximum Velocity") + col.prop(trans, "pos_cut") + + # Position Offsets + col = split.column() + col.active = cam_type == "follow" + col.label("Position Offset:") + col.prop(props, "pos_offset", text="") + col.prop(props, "pos_worldspace") + + # Camera Panning + layout.separator() + split = layout.split() + col = split.column() + col.label("Limit Panning:") + col.prop(props, "x_pan_angle") + col.prop(props, "y_pan_angle") + + # Camera Zoom + col = split.column() + col.label("Field of View:") + col.prop(props, "fov") + _draw_gated_prop(col, props, "limit_zoom", "zoom_min") + _draw_gated_prop(col, props, "limit_zoom", "zoom_max") + _draw_gated_prop(col, props, "limit_zoom", "zoom_rate") From de23d7764976b904d8f3ed644dd412b40487ce68 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 14 Jul 2018 22:17:26 -0400 Subject: [PATCH 02/10] Implement Circle Cameras This includes circle cameras of the manual and auto-region variety --- korman/exporter/camera.py | 54 ++++++++++++++++++++++++++- korman/properties/modifiers/region.py | 8 +++- korman/properties/prop_camera.py | 42 ++++++++++++++++++++- korman/ui/ui_camera.py | 16 +++++++- 4 files changed, 113 insertions(+), 7 deletions(-) diff --git a/korman/exporter/camera.py b/korman/exporter/camera.py index 7524e70..7efffb6 100644 --- a/korman/exporter/camera.py +++ b/korman/exporter/camera.py @@ -28,8 +28,6 @@ class CameraConverter: trans_props = camera_props.transition brain.poaOffset = hsVector3(*camera_props.poa_offset) - if isinstance(brain, plCameraBrain1_Avatar): - brain.offset = hsVector3(*camera_props.pos_offset) if camera_props.poa_type == "object": brain.subject = self._mgr.find_create_key(plSceneObject, bl=camera_props.poa_object) @@ -84,6 +82,55 @@ class CameraConverter: # TODO: do we need to do something about animations here? return mod + def _export_circle_camera(self, so, bo, props): + brain = self._mgr.find_create_object(plCameraBrain1_Circle, so=so) + self._convert_brain(so, bo, props, brain) + + # Circle Camera specific stuff ahoy! + if props.poa_type == "avatar": + brain.circleFlags |= plCameraBrain1_Circle.kCircleLocalAvatar + elif props.poa_type == "object": + brain.poaObject = self._mgr.find_create_key(plSceneObject, bl=props.poa_object) + else: + self._report.warn("Circle Camera '{}' has no Point of Attention. Is this intended?", bo.name, indent=3) + if props.circle_pos == "farthest": + brain.circleFlags |= plCameraBrain1_Circle.kFarthest + + # If no center object is specified, we will use the camera object's location. + # We will use a simple vector for this object to make life simpler, however, + # specifying an actual center allows you to do interesting things like animate the center... + # Fascinating! Therefore, we will expose the Plasma Object... + if props.circle_center is None: + brain.center = hsVector3(*bo.location) + else: + brain.centerObject = self._mgr.find_create_key(plSceneObject, bl=props.circle_center) + # This flag has no effect in CWE, but I'm using it for correctness' sake + brain.circleFlags |= plCameraBrain1_Circle.kHasCenterObject + + # PlasmaMAX forces these values so the circle camera is rather slow, which is somewhat + # sensible to me. Fast circular motion seems like a poor idea to me... If we want these + # values to be customizable, we probably want add some kind of range limitation or at least + # a flashing red light in the UI... + brain.acceleration = 10.0 + brain.deceleration = 10.0 + brain.velocity = 15.0 + + # Related to above, circle cameras use a slightly different velocity method. + # This is stored in Plasma as a fraction of the circle's circumference. It makes + # more sense to me to present it in terms of degrees per second, however. + # NOTE that Blender returns radians!!! + brain.cirPerSec = props.circle_velocity / (2 * math.pi) + + # I consider this clever... If we have a center object, we use the magnitude of the displacement + # of the camera's object to the center object (at frame 0) to determine the radius. If no center + # object is specified, we allow the user to specify the radius. + # Well, it's clever until you realize it's the same thing Cyan did in PlasmaMAX... But it's harder + # here because Blendsucks. + brain.radius = props.get_circle_radius(bo) + + # Done already? + return brain + def _export_fixed_camera(self, so, bo, props): brain = self._mgr.find_create_object(plCameraBrain1_Fixed, so=so) self._convert_brain(so, bo, props, brain) @@ -93,6 +140,9 @@ class CameraConverter: def _export_follow_camera(self, so, bo, props): brain = self._mgr.find_create_object(plCameraBrain1_Avatar, so=so) self._convert_brain(so, bo, props, brain) + + # Follow camera specific stuff ahoy! + brain.offset = hsVector3(*camera_props.pos_offset) return brain @property diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index dfff86a..8d3d6c7 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -69,7 +69,8 @@ class PlasmaCameraRegion(PlasmaModifierProperties): camera_type = EnumProperty(name="Camera Type", description="What kind of camera should be used?", - items=[("auto", "Auto Follow Camera", "Automatically generated camera"), + items=[("auto_follow", "Auto Follow Camera", "Automatically generated follow camera"), + ("auto_circle", "Auto Circle Camera", "Automatically generated circle camera"), ("manual", "Manual Camera", "User specified camera object")], options=set()) camera_object = PointerProperty(name="Camera", @@ -86,9 +87,12 @@ class PlasmaCameraRegion(PlasmaModifierProperties): camera_so_key = exporter.mgr.find_create_key(plSceneObject, bl=self.camera_object) camera_props = self.camera_object.data.plasma_camera.settings else: + assert self.camera_type[:4] == "auto" + # Wheedoggy! We get to export the doggone camera now. camera_props = self.auto_camera - exporter.camera.export_camera(so, bo, "follow", camera_props) + camera_type = self.camera_type[5:] + exporter.camera.export_camera(so, bo, camera_type, camera_props) # Setup physical stuff phys_mod = bo.plasma_modifiers.collision diff --git a/korman/properties/prop_camera.py b/korman/properties/prop_camera.py index fd5b7fe..d637ce3 100644 --- a/korman/properties/prop_camera.py +++ b/korman/properties/prop_camera.py @@ -17,8 +17,9 @@ import bpy from bpy.props import * import math -camera_types = [("follow", "Follow Camera", "Camera follows an object."), - ("fixed", "Fixed Camera", "Camera is fixed in one location.")] +camera_types = [("circle", "Circle Camera", "The camera circles a fixed point"), + ("follow", "Follow Camera", "The camera follows an object"), + ("fixed", "Fixed Camera", "The camera is fixed in one location")] class PlasmaTransition(bpy.types.PropertyGroup): poa_acceleration = FloatProperty(name="PoA Acceleration", @@ -142,6 +143,43 @@ class PlasmaCameraProperties(bpy.types.PropertyGroup): description="The camera should be considered the Age's primary camera.", options=set()) + # Cricle Camera + def _get_circle_radius(self): + # This is coming from the UI, so we need to get the active object from + # Blender's context and pass that on to the actual getter. + return self.get_circle_radius(bpy.context.object) + def _set_circle_radius(self, value): + # Don't really care about error checking... + self.circle_radius_value = value + + circle_center = PointerProperty(name="Center", + description="Center of the circle camera's orbit", + type=bpy.types.Object, + options=set()) + circle_pos = EnumProperty(name="Position on Circle", + description="The point on the circle the camera moves to", + items=[("closest", "Closest Point", "The camera moves to point on the circle closest to the Point of Attention"), + ("farthest", "Farthest Point", "The point farthest from the Point of Attention")], + options=set()) + circle_velocity = FloatProperty(name="Velocity", + description="Velocity of the circle camera in degrees per second", + min=0.0, max=math.radians(360.0), precision=0, default=math.radians(36.0), + subtype="ANGLE", options=set()) + circle_radius_ui = FloatProperty(name="Radius", + description="Radius at which the circle camera should orbit the Point of Attention", + min=0.0, get=_get_circle_radius, set=_set_circle_radius, options=set()) + circle_radius_value = FloatProperty(name="INTERNAL: Radius", + description="Radius at which the circle camera should orbit the Point of Attention", + min=0.0, default=8.5, options={"HIDDEN"}) + + def get_circle_radius(self, bo): + """Gets the circle camera radius for this camera when it is attached to the given Object""" + assert bo is not None + if self.circle_center is not None: + vec = bo.location - self.circle_center.location + return vec.magnitude + return self.circle_radius_value + def harvest_actors(self): if self.poa_type == "object": return set((self.poa_object.name),) diff --git a/korman/ui/ui_camera.py b/korman/ui/ui_camera.py index 43a3e71..cf74f6d 100644 --- a/korman/ui/ui_camera.py +++ b/korman/ui/ui_camera.py @@ -59,7 +59,7 @@ def draw_camera_properties(cam_type, props, layout, context): col = split.column() col.label("Camera Mode:") col = col.column() - col.alert = cam_type == "follow" and props.poa_type == "none" + col.alert = cam_type != "fixed" and props.poa_type == "none" col.prop(props, "poa_type", text="") col.alert = False row = col.row() @@ -98,6 +98,7 @@ def draw_camera_properties(cam_type, props, layout, context): col = split.column() # Position Transitions + col.active = cam_type != "circle" col.label("Default Position Transition:") col.prop(trans, "pos_acceleration", text="Acceleration") col.prop(trans, "pos_deceleration", text="Deceleration") @@ -126,3 +127,16 @@ def draw_camera_properties(cam_type, props, layout, context): _draw_gated_prop(col, props, "limit_zoom", "zoom_min") _draw_gated_prop(col, props, "limit_zoom", "zoom_max") _draw_gated_prop(col, props, "limit_zoom", "zoom_rate") + + # Circle Camera Stuff + layout.separator() + split = layout.split() + col = split.column() + col.active = cam_type == "circle" + col.label("Circle Camera:") + col.prop(props, "circle_center", text="") + col.prop(props, "circle_pos", text="") + col.prop(props, "circle_velocity") + row = col.row(align=True) + row.active = props.circle_center is None + row.prop(props, "circle_radius_ui") From c656b0e51af5a875043aa7304ddac35d08f96b0a Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 15 Jul 2018 21:22:38 -0400 Subject: [PATCH 03/10] Camera transform and FOV animations --- korman/exporter/animation.py | 100 +++++++++++++++++++++++++++++-- korman/exporter/camera.py | 15 +++-- korman/exporter/convert.py | 2 +- korman/nodes/node_messages.py | 9 +-- korman/properties/prop_camera.py | 11 ++++ korman/ui/modifiers/region.py | 2 +- korman/ui/ui_camera.py | 21 ++++++- 7 files changed, 140 insertions(+), 20 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index fd1ac63..6f4a292 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -51,7 +51,10 @@ class AnimationConverter: # form of separation, but Blender's NLA editor is way confusing and appears to not work with # things that aren't the typical position, rotation, scale animations. applicators = [] - applicators.append(self._convert_transform_animation(bo.name, obj_fcurves, bo.matrix_basis)) + if isinstance(bo.data, bpy.types.Camera): + applicators.append(self._convert_camera_animation(bo, so, obj_fcurves, data_fcurves)) + else: + applicators.append(self._convert_transform_animation(bo.name, obj_fcurves, bo.matrix_basis)) if bo.plasma_modifiers.soundemit.enabled: applicators.extend(self._convert_sound_volume_animation(bo.name, obj_fcurves, bo.plasma_modifiers.soundemit)) if isinstance(bo.data, bpy.types.Lamp): @@ -102,6 +105,84 @@ class AnimationConverter: atcanim.easeOutMax = 1.0 atcanim.easeOutLength = 1.0 + def _convert_camera_animation(self, bo, so, obj_fcurves, data_fcurves): + if data_fcurves: + # The hard part about this crap is that FOV animations are not stored in ATC Animations + # instead, FOV animation keyframes are held inside of the camera modifier. Cyan's solution + # in PlasmaMAX appears to be for any xform keyframe, add two messages to the camera modifier + # representing the FOV at that point. Makes more sense to me to use each FOV keyframe instead + fov_fcurve = next((i for i in data_fcurves if i.data_path == "plasma_camera.settings.fov"), None) + if fov_fcurve: + # NOTE: this is another critically important key ordering in the SceneObject modifier + # list. CameraModifier calls into AGMasterMod code that assumes the AGModifier + # is already available. Should probably consider adding some code to libHSPlasma + # to order the SceneObject modifier key vector at some point. + anim_key = self.get_animation_key(bo) + camera = self._mgr.find_create_object(plCameraModifier, so=so) + cam_key = camera.key + aspect, fps = (3.0 / 4.0), self._bl_fps + degrees = math.degrees + fov_fcurve.update() + + # Seeing as how we're transforming the data entirely, we'll just use the fcurve itself + # instead of our other animation helpers. But ugh does this mess look like sloppy C. + keyframes = fov_fcurve.keyframe_points + num_keyframes = len(keyframes) + has_fov_anim = bool(num_keyframes) + i = 0 + while i < num_keyframes: + this_keyframe = keyframes[i] + next_keyframe = keyframes[0] if i+1 == num_keyframes else keyframes[i+1] + + # So remember, these are messages. When we hit a keyframe, we're dispatching a message + # representing the NEXT desired FOV. + this_frame_time = this_keyframe.co[0] / fps + next_frame_num, next_frame_value = next_keyframe.co + next_frame_time = next_frame_num / fps + + # This message is held on the camera modifier and sent to the animation... It calls + # back when the animation reaches the keyframe time, causing the FOV message to be sent. + cb_msg = plEventCallbackMsg() + cb_msg.event = kTime + cb_msg.eventTime = this_frame_time + cb_msg.index = i + cb_msg.repeats = -1 + cb_msg.addReceiver(cam_key) + anim_msg = plAnimCmdMsg() + anim_msg.animName = "(Entire Animation)" + anim_msg.time = this_frame_time + anim_msg.sender = anim_key + anim_msg.addReceiver(anim_key) + anim_msg.addCallback(cb_msg) + anim_msg.setCmd(plAnimCmdMsg.kAddCallbacks, True) + camera.addMessage(anim_msg, anim_key) + + # This is the message actually changes the FOV. Interestingly, it is sent at + # export-time and while playing the game, the camera modifier just steals its + # parameters and passes them to the brain. Can't make this stuff up. + cam_msg = plCameraMsg() + cam_msg.addReceiver(cam_key) + cam_msg.setCmd(plCameraMsg.kAddFOVKeyFrame, True) + cam_config = cam_msg.config + cam_config.accel = next_frame_time # Yassss... + cam_config.fovW = degrees(next_frame_value) + cam_config.fovH = degrees(next_frame_value * aspect) + camera.addFOVInstruction(cam_msg) + + i += 1 + else: + has_fov_anim = False + else: + has_fov_anim = False + + # If we exported any FOV animation at all, then we need to ensure there is an applicator + # returned from here... At bare minimum, we'll need the applicator with an empty + # CompoundController. This should be sufficient to keep CWE from crashing... + applicator = self._convert_transform_animation(bo.name, obj_fcurves, bo.matrix_basis, allow_empty=has_fov_anim) + camera = locals().get("camera", self._mgr.find_create_object(plCameraModifier, so=so)) + camera.animated = applicator is not None + return applicator + def _convert_lamp_color_animation(self, name, fcurves, lamp): if not fcurves: return None @@ -271,15 +352,16 @@ class AnimationConverter: applicator.channel = channel yield applicator - def _convert_transform_animation(self, name, fcurves, xform): - if not fcurves: + def _convert_transform_animation(self, name, fcurves, xform, allow_empty=False): + if not fcurves and not allow_empty: return None pos = self.make_pos_controller(fcurves, xform) rot = self.make_rot_controller(fcurves, xform) scale = self.make_scale_controller(fcurves, xform) if pos is None and rot is None and scale is None: - return None + if not allow_empty: + return None tm = plCompoundController() tm.X = pos @@ -317,6 +399,16 @@ class AnimationConverter: master = self._mgr.find_create_object(plAGMasterMod, so=so, bl=bo) return mod, master + def get_animation_key(self, bo, so=None): + # we might be controlling more than one animation. isn't that cute? + # https://www.youtube.com/watch?v=hspNaoxzNbs + # (but obviously this is not wrong...) + group_mod = bo.plasma_modifiers.animation_group + if group_mod.enabled: + return self._mgr.find_create_key(plMsgForwarder, bl=bo, so=so, name=group_mod.key_name) + else: + return self.get_anigraph_keys(bo, so)[1] + def make_matrix44_controller(self, fcurves, pos_path, scale_path, pos_default, scale_default): def convert_matrix_keyframe(**kwargs): pos = kwargs.get(pos_path) diff --git a/korman/exporter/camera.py b/korman/exporter/camera.py index 7efffb6..7b6715c 100644 --- a/korman/exporter/camera.py +++ b/korman/exporter/camera.py @@ -67,19 +67,25 @@ class CameraConverter: brain.setFlags(plCameraBrain1.kSpeedUpWhenRunning, True) def export_camera(self, so, bo, camera_type, camera_props): + brain = getattr(self, "_export_{}_camera".format(camera_type))(so, bo, camera_props, allow_anim) mod = self._export_camera_modifier(so, bo, camera_props) - brain = getattr(self, "_export_{}_camera".format(camera_type))(so, bo, camera_props) mod.brain = brain.key def _export_camera_modifier(self, so, bo, props): + mod = self._mgr.find_create_object(plCameraModifier, so=so) + # PlasmaMAX allows the user to specify the horizontal OR vertical FOV, but not both. # We only allow setting horizontal FOV (how often do you look up?), however. - # Plasma assumes 4:3 aspect ratio... - mod = self._mgr.find_create_object(plCameraModifier, so=so) + # Plasma assumes 4:3 aspect ratio.. fov = props.fov mod.fovW, mod.fovH = math.degrees(fov), math.degrees(fov * (3.0 / 4.0)) - # TODO: do we need to do something about animations here? + # This junk is only valid for animated cameras... + # Animation exporter should have already run by this point :) + if mod.animated: + mod.startAnimOnPush = props.start_on_push + mod.stopAnimOnPop = props.stop_on_pop + mod.resetAnimOnPop = props.reset_on_pop return mod def _export_circle_camera(self, so, bo, props): @@ -134,7 +140,6 @@ class CameraConverter: def _export_fixed_camera(self, so, bo, props): brain = self._mgr.find_create_object(plCameraBrain1_Fixed, so=so) self._convert_brain(so, bo, props, brain) - # TODO: animations??? return brain def _export_follow_camera(self, so, bo, props): diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 75d7206..f939fd2 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -229,8 +229,8 @@ class Exporter: # sort, and barf out a CI. sceneobject = self.mgr.find_create_object(plSceneObject, bl=bl_obj) self._export_actor(sceneobject, bl_obj) - export_fn(sceneobject, bl_obj) self.animation.convert_object_animations(bl_obj, sceneobject) + export_fn(sceneobject, bl_obj) # And now we puke out the modifiers... for mod in bl_obj.plasma_modifiers.modifiers: diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index 1dbfe24..237b88a 100644 --- a/korman/nodes/node_messages.py +++ b/korman/nodes/node_messages.py @@ -243,14 +243,7 @@ class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageWithCallbacksNode, if self.anim_type == "OBJECT": if not obj.plasma_object.has_animation_data: self.raise_error("invalid animation") - group = obj.plasma_modifiers.animation_group - if group.enabled: - # we might be controlling more than one animation. isn't that cute? - # https://www.youtube.com/watch?v=hspNaoxzNbs - # (but obviously this is not wrong...) - target = exporter.mgr.find_create_key(plMsgForwarder, bl=obj, name=group.key_name) - else: - _agmod_trash, target = exporter.animation.get_anigraph_keys(obj) + target = exporter.animation.get_animation_key(obj) else: material = self.target_material if material is None: diff --git a/korman/properties/prop_camera.py b/korman/properties/prop_camera.py index d637ce3..b7a0f68 100644 --- a/korman/properties/prop_camera.py +++ b/korman/properties/prop_camera.py @@ -172,6 +172,17 @@ class PlasmaCameraProperties(bpy.types.PropertyGroup): description="Radius at which the circle camera should orbit the Point of Attention", min=0.0, default=8.5, options={"HIDDEN"}) + # Animation + start_on_push = BoolProperty(name="Start on Push", + description="Start playing the camera's animation when the camera is activated", + options=set()) + stop_on_pop = BoolProperty(name="Pause on Pop", + description="Pauses the camera's animation when the camera is no longer activated", + options=set()) + reset_on_pop = BoolProperty(name="Reset on Pop", + description="Reset the camera's animation to the beginning when the camera is no longer activated", + options=set()) + def get_circle_radius(self, bo): """Gets the circle camera radius for this camera when it is attached to the given Object""" assert bo is not None diff --git a/korman/ui/modifiers/region.py b/korman/ui/modifiers/region.py index 80dc272..bbf3a07 100644 --- a/korman/ui/modifiers/region.py +++ b/korman/ui/modifiers/region.py @@ -21,7 +21,7 @@ def camera_rgn(modifier, layout, context): if modifier.camera_type == "manual": layout.prop(modifier, "camera_object", icon="CAMERA_DATA") else: - draw_camera_properties("follow", modifier.auto_camera, layout, context) + draw_camera_properties("follow", modifier.auto_camera, layout, context, True) def footstep(modifier, layout, context): layout.prop(modifier, "bounds") diff --git a/korman/ui/ui_camera.py b/korman/ui/ui_camera.py index cf74f6d..18143c9 100644 --- a/korman/ui/ui_camera.py +++ b/korman/ui/ui_camera.py @@ -44,7 +44,7 @@ class PlasmaCameraTransitionPanel(CameraButtonsPanel, bpy.types.Panel): pass -def draw_camera_properties(cam_type, props, layout, context): +def draw_camera_properties(cam_type, props, layout, context, force_no_anim=False): trans = props.transition def _draw_gated_prop(layout, props, gate_prop, actual_prop): @@ -53,6 +53,17 @@ def draw_camera_properties(cam_type, props, layout, context): row = row.row(align=True) row.active = getattr(props, gate_prop) row.prop(props, actual_prop) + def _is_camera_animated(cam_type, props, context, force_no_anim): + if force_no_anim or cam_type == "rail": + return False + # Check for valid animation data on either the object or the camera + # TODO: Should probably check for valid FCurve channels at some point??? + for i in (props.id_data, context.object): + if i.animation_data is None: + continue + if i.animation_data.action is not None: + return True + return False # Point of Attention split = layout.split() @@ -140,3 +151,11 @@ def draw_camera_properties(cam_type, props, layout, context): row = col.row(align=True) row.active = props.circle_center is None row.prop(props, "circle_radius_ui") + + # Animated Camera Stuff + col = split.column() + col.active = _is_camera_animated(cam_type, props, context, force_no_anim) + col.label("Animation:") + col.prop(props, "start_on_push") + col.prop(props, "stop_on_pop") + col.prop(props, "reset_on_pop") From e21b8d7032f3796efe1198cc99b56cfbcabbc4d6 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 5 Aug 2018 17:00:45 -0400 Subject: [PATCH 04/10] Rail Cameras --- korman/exporter/animation.py | 37 ++++++++++++--------------- korman/exporter/camera.py | 43 +++++++++++++++++++++++++++++++- korman/exporter/convert.py | 9 +++---- korman/exporter/utils.py | 13 ++++++++++ korman/helpers.py | 13 ++++++++++ korman/properties/prop_camera.py | 3 ++- korman/ui/ui_camera.py | 18 ++++++++++--- 7 files changed, 105 insertions(+), 31 deletions(-) diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py index 6f4a292..8099423 100644 --- a/korman/exporter/animation.py +++ b/korman/exporter/animation.py @@ -353,6 +353,21 @@ class AnimationConverter: yield applicator def _convert_transform_animation(self, name, fcurves, xform, allow_empty=False): + tm = self.convert_transform_controller(fcurves, xform, allow_empty) + if tm is None and not allow_empty: + return None + + applicator = plMatrixChannelApplicator() + applicator.enabled = True + applicator.channelName = name + channel = plMatrixControllerChannel() + channel.controller = tm + applicator.channel = channel + channel.affine = utils.affine_parts(xform) + + return applicator + + def convert_transform_controller(self, fcurves, xform, allow_empty=False): if not fcurves and not allow_empty: return None @@ -367,27 +382,7 @@ class AnimationConverter: tm.X = pos tm.Y = rot tm.Z = scale - - applicator = plMatrixChannelApplicator() - applicator.enabled = True - applicator.channelName = name - channel = plMatrixControllerChannel() - channel.controller = tm - applicator.channel = channel - - # Decompose the matrix into the 90s-era 3ds max affine parts sillyness - # All that's missing now is something like "(c) 1998 HeadSpin" oh wait... - affine = hsAffineParts() - affine.T = hsVector3(*xform.to_translation()) - affine.K = hsVector3(*xform.to_scale()) - affine.F = -1.0 if xform.determinant() < 0.0 else 1.0 - rot = xform.to_quaternion() - affine.Q = utils.quaternion(rot) - rot.normalize() - affine.U = utils.quaternion(rot) - channel.affine = affine - - return applicator + return tm def get_anigraph_keys(self, bo=None, so=None): mod = self._mgr.find_create_key(plAGModifier, so=so, bl=bo) diff --git a/korman/exporter/camera.py b/korman/exporter/camera.py index 7b6715c..a39485a 100644 --- a/korman/exporter/camera.py +++ b/korman/exporter/camera.py @@ -19,6 +19,8 @@ from PyHSPlasma import * import weakref from .explosions import * +from .. import helpers +from . import utils class CameraConverter: def __init__(self, exporter): @@ -67,7 +69,7 @@ class CameraConverter: brain.setFlags(plCameraBrain1.kSpeedUpWhenRunning, True) def export_camera(self, so, bo, camera_type, camera_props): - brain = getattr(self, "_export_{}_camera".format(camera_type))(so, bo, camera_props, allow_anim) + brain = getattr(self, "_export_{}_camera".format(camera_type))(so, bo, camera_props) mod = self._export_camera_modifier(so, bo, camera_props) mod.brain = brain.key @@ -138,6 +140,7 @@ class CameraConverter: return brain def _export_fixed_camera(self, so, bo, props): + self._exporter().animation.convert_object_animations(bo, so) brain = self._mgr.find_create_object(plCameraBrain1_Fixed, so=so) self._convert_brain(so, bo, props, brain) return brain @@ -150,6 +153,44 @@ class CameraConverter: brain.offset = hsVector3(*camera_props.pos_offset) return brain + def _export_rail_camera(self, so, bo, props): + brain = self._mgr.find_create_object(plCameraBrain1_Fixed, so=so) + self._convert_brain(so, bo, props, brain) + + rail = self._mgr.find_create_object(plRailCameraMod, so=so) + rail.followFlags |= plLineFollowMod.kForceToLine + if props.poa_type == "object": + rail.followMode = plLineFollowMod.kFollowObject + rail.refObj = self._mgr.find_create_key(plSceneObject, bl=props.poa_object) + else: + rail.followMode = plLineFollowMod.kFollowLocalAvatar + if bo.parent: + rail.pathParent = self._mgr.find_create_key(plSceneObject, bl=bo.parent) + + # The rail is defined by a position controller in Plasma. Cyan uses a separate + # path object, but it makes more sense to me to just animate the camera with + # the details of the path... + pos_fcurves = tuple(i for i in helpers.fetch_fcurves(bo, False) if i.data_path == "location") + pos_ctrl = self._exporter().animation.convert_transform_controller(pos_fcurves, bo.matrix_basis) + if pos_ctrl is None: + raise ExportError("'{}': Rail Camera lacks appropriate rail keyframes".format(bo.name)) + path = plAnimPath() + path.controller = pos_ctrl + path.affineParts = utils.affine_parts(bo.matrix_local) + begin, end = bo.animation_data.action.frame_range + for fcurve in pos_fcurves: + f1, f2 = fcurve.evaluate(begin), fcurve.evaluate(end) + if abs(f1 - f2) > 0.001: + break + else: + # The animation is a loop + path.flags |= plAnimPath.kWrap + path.length = end / bpy.context.scene.render.fps + rail.path = path + brain.rail = rail.key + + return brain + @property def _mgr(self): return self._exporter().mgr diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index f939fd2..cc526bc 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -229,7 +229,6 @@ class Exporter: # sort, and barf out a CI. sceneobject = self.mgr.find_create_object(plSceneObject, bl=bl_obj) self._export_actor(sceneobject, bl_obj) - self.animation.convert_object_animations(bl_obj, sceneobject) export_fn(sceneobject, bl_obj) # And now we puke out the modifiers... @@ -240,19 +239,19 @@ class Exporter: def _export_camera_blobj(self, so, bo): # Hey, guess what? Blender's camera data is utter crap! + # NOTE: Animation export is dependent on camera type, so we'll do that later. camera = bo.data.plasma_camera self.camera.export_camera(so, bo, camera.camera_type, camera.settings) def _export_empty_blobj(self, so, bo): - # We don't need to do anything here. This function just makes sure we don't error out - # or add a silly special case :( - pass + self.animation.convert_object_animations(bo, so) def _export_lamp_blobj(self, so, bo): - # We'll just redirect this to the RT Light converter... + self.animation.convert_object_animations(bo, so) self.light.export_rtlight(so, bo) def _export_mesh_blobj(self, so, bo): + self.animation.convert_object_animations(bo, so) if bo.data.materials: self.mesh.export_object(bo) else: diff --git a/korman/exporter/utils.py b/korman/exporter/utils.py index 3471a33..569201d 100644 --- a/korman/exporter/utils.py +++ b/korman/exporter/utils.py @@ -15,6 +15,19 @@ from PyHSPlasma import * +def affine_parts(xform): + # Decompose the matrix into the 90s-era 3ds max affine parts sillyness + # All that's missing now is something like "(c) 1998 HeadSpin" oh wait... + affine = hsAffineParts() + affine.T = hsVector3(*xform.to_translation()) + affine.K = hsVector3(*xform.to_scale()) + affine.F = -1.0 if xform.determinant() < 0.0 else 1.0 + rot = xform.to_quaternion() + affine.Q = quaternion(rot) + rot.normalize() + affine.U = quaternion(rot) + return affine + def color(blcolor, alpha=1.0): """Converts a Blender Color into an hsColorRGBA""" return hsColorRGBA(blcolor.r, blcolor.g, blcolor.b, alpha) diff --git a/korman/helpers.py b/korman/helpers.py index 6ae2bf2..fcd60c3 100644 --- a/korman/helpers.py +++ b/korman/helpers.py @@ -51,6 +51,19 @@ class TemporaryObject: def ensure_power_of_two(value): return pow(2, math.floor(math.log(value, 2))) +def fetch_fcurves(id_data, data_fcurves=True): + """Given a Blender ID, yields its FCurves""" + def _fetch(source): + if source is not None and source.action is not None: + for i in source.action.fcurves: + yield i + + # This seems rather unpythonic IMO + for i in _fetch(id_data.animation_data): + yield i + if data_fcurves: + for i in _fetch(id_data.data.animation_data): + yield i def find_modifier(bo, modid): """Given a Blender Object, finds a given modifier and returns it or None""" diff --git a/korman/properties/prop_camera.py b/korman/properties/prop_camera.py index b7a0f68..6b88549 100644 --- a/korman/properties/prop_camera.py +++ b/korman/properties/prop_camera.py @@ -19,7 +19,8 @@ import math camera_types = [("circle", "Circle Camera", "The camera circles a fixed point"), ("follow", "Follow Camera", "The camera follows an object"), - ("fixed", "Fixed Camera", "The camera is fixed in one location")] + ("fixed", "Fixed Camera", "The camera is fixed in one location"), + ("rail", "Rail Camera", "The camera follows an object by moving along a line")] class PlasmaTransition(bpy.types.PropertyGroup): poa_acceleration = FloatProperty(name="PoA Acceleration", diff --git a/korman/ui/ui_camera.py b/korman/ui/ui_camera.py index 18143c9..e4ef144 100644 --- a/korman/ui/ui_camera.py +++ b/korman/ui/ui_camera.py @@ -47,6 +47,18 @@ class PlasmaCameraTransitionPanel(CameraButtonsPanel, bpy.types.Panel): def draw_camera_properties(cam_type, props, layout, context, force_no_anim=False): trans = props.transition + def _draw_alert_prop(layout, props, the_prop, cam="", min=None, max=None, **kwargs): + can_alert = not cam or cam == cam_type + if can_alert: + value = getattr(props, the_prop) + if min is not None and value < min: + layout.alert = True + if max is not None and value > max: + layout.alert = True + layout.prop(props, the_prop, **kwargs) + layout.alert = False + else: + layout.prop(props, the_prop, **kwargs) def _draw_gated_prop(layout, props, gate_prop, actual_prop): row = layout.row(align=True) row.prop(props, gate_prop, text="") @@ -111,9 +123,9 @@ def draw_camera_properties(cam_type, props, layout, context, force_no_anim=False # Position Transitions col.active = cam_type != "circle" col.label("Default Position Transition:") - col.prop(trans, "pos_acceleration", text="Acceleration") - col.prop(trans, "pos_deceleration", text="Deceleration") - col.prop(trans, "pos_velocity", text="Maximum Velocity") + _draw_alert_prop(col, trans, "pos_acceleration", cam="rail", max=10.0, text="Acceleration") + _draw_alert_prop(col, trans, "pos_deceleration", cam="rail", max=10.0, text="Deceleration") + _draw_alert_prop(col, trans, "pos_velocity", cam="rail", max=10.0, text="Maximum Velocity") col.prop(trans, "pos_cut") # Position Offsets From 02eb0e71fceccc8d41f945a917648b22f7ae672c Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 12 Aug 2018 16:13:13 -0400 Subject: [PATCH 05/10] Fix some camera bugs due to code shuffling --- korman/exporter/camera.py | 2 +- korman/properties/modifiers/region.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/korman/exporter/camera.py b/korman/exporter/camera.py index a39485a..9e315a7 100644 --- a/korman/exporter/camera.py +++ b/korman/exporter/camera.py @@ -150,7 +150,7 @@ class CameraConverter: self._convert_brain(so, bo, props, brain) # Follow camera specific stuff ahoy! - brain.offset = hsVector3(*camera_props.pos_offset) + brain.offset = hsVector3(*props.pos_offset) return brain def _export_rail_camera(self, so, bo, props): diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index 8d3d6c7..73672ff 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -93,6 +93,7 @@ class PlasmaCameraRegion(PlasmaModifierProperties): camera_props = self.auto_camera camera_type = self.camera_type[5:] exporter.camera.export_camera(so, bo, camera_type, camera_props) + camera_so_key = so.key # Setup physical stuff phys_mod = bo.plasma_modifiers.collision From 1ca9456870947b13b39ad2f30393adf0acabf3fd Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 12 Aug 2018 16:14:01 -0400 Subject: [PATCH 06/10] Rework camera UI to not be so monolithic --- korman/exporter/camera.py | 5 +- korman/properties/prop_camera.py | 17 +- korman/render.py | 4 - korman/ui/modifiers/region.py | 23 ++- korman/ui/ui_camera.py | 256 +++++++++++++++++++------------ 5 files changed, 198 insertions(+), 107 deletions(-) diff --git a/korman/exporter/camera.py b/korman/exporter/camera.py index 9e315a7..420ed79 100644 --- a/korman/exporter/camera.py +++ b/korman/exporter/camera.py @@ -140,7 +140,8 @@ class CameraConverter: return brain def _export_fixed_camera(self, so, bo, props): - self._exporter().animation.convert_object_animations(bo, so) + if props.anim_enabled: + self._exporter().animation.convert_object_animations(bo, so) brain = self._mgr.find_create_object(plCameraBrain1_Fixed, so=so) self._convert_brain(so, bo, props, brain) return brain @@ -185,6 +186,8 @@ class CameraConverter: else: # The animation is a loop path.flags |= plAnimPath.kWrap + if props.rail_pos == "farthest": + path.flags |= plAnimPath.kFarthest path.length = end / bpy.context.scene.render.fps rail.path = path brain.rail = rail.key diff --git a/korman/properties/prop_camera.py b/korman/properties/prop_camera.py index 6b88549..93d814a 100644 --- a/korman/properties/prop_camera.py +++ b/korman/properties/prop_camera.py @@ -159,8 +159,8 @@ class PlasmaCameraProperties(bpy.types.PropertyGroup): options=set()) circle_pos = EnumProperty(name="Position on Circle", description="The point on the circle the camera moves to", - items=[("closest", "Closest Point", "The camera moves to point on the circle closest to the Point of Attention"), - ("farthest", "Farthest Point", "The point farthest from the Point of Attention")], + items=[("closest", "Closest Point", "The camera moves to the point on the circle closest to the Point of Attention"), + ("farthest", "Farthest Point", "The camera moves to the point on the circle farthest from the Point of Attention")], options=set()) circle_velocity = FloatProperty(name="Velocity", description="Velocity of the circle camera in degrees per second", @@ -174,16 +174,29 @@ class PlasmaCameraProperties(bpy.types.PropertyGroup): min=0.0, default=8.5, options={"HIDDEN"}) # Animation + anim_enabled = BoolProperty(name="Animation Enabled", + description="Export the camera's animation", + default=True, + options=set()) start_on_push = BoolProperty(name="Start on Push", description="Start playing the camera's animation when the camera is activated", + default=True, options=set()) stop_on_pop = BoolProperty(name="Pause on Pop", description="Pauses the camera's animation when the camera is no longer activated", + default=True, options=set()) reset_on_pop = BoolProperty(name="Reset on Pop", description="Reset the camera's animation to the beginning when the camera is no longer activated", options=set()) + # Rail + rail_pos = EnumProperty(name="Position on Rail", + description="The point on the rail the camera moves to", + items=[("closest", "Closest Point", "The camera moves to the point on the rail closest to the Point of Attention"), + ("farthest", "Farthest Point", "The camera moves to the point on the rail farthest from the Point of Attention")], + options=set()) + def get_circle_radius(self, bo): """Gets the circle camera radius for this camera when it is attached to the given Object""" assert bo is not None diff --git a/korman/render.py b/korman/render.py index e51304e..393b9eb 100644 --- a/korman/render.py +++ b/korman/render.py @@ -36,10 +36,6 @@ properties_material.MATERIAL_PT_transp.COMPAT_ENGINES.add("PLASMA_GAME") properties_material.MATERIAL_PT_shadow.COMPAT_ENGINES.add("PLASMA_GAME") del properties_material -from bl_ui import properties_data_camera -properties_data_camera.DATA_PT_context_camera.COMPAT_ENGINES.add("PLASMA_GAME") -del properties_data_camera - from bl_ui import properties_data_mesh properties_data_mesh.DATA_PT_uv_texture.COMPAT_ENGINES.add("PLASMA_GAME") properties_data_mesh.DATA_PT_vertex_colors.COMPAT_ENGINES.add("PLASMA_GAME") diff --git a/korman/ui/modifiers/region.py b/korman/ui/modifiers/region.py index bbf3a07..2b18446 100644 --- a/korman/ui/modifiers/region.py +++ b/korman/ui/modifiers/region.py @@ -14,14 +14,33 @@ # along with Korman. If not, see . import bpy -from ..ui_camera import draw_camera_properties +from .. import ui_camera def camera_rgn(modifier, layout, context): layout.prop(modifier, "camera_type") if modifier.camera_type == "manual": layout.prop(modifier, "camera_object", icon="CAMERA_DATA") else: - draw_camera_properties("follow", modifier.auto_camera, layout, context, True) + cam_type = modifier.camera_type[5:] + cam_props = modifier.auto_camera + + def _draw_props(layout, cb): + for i in cb: + layout.separator() + i(layout, cam_type, cam_props) + def _draw_circle_cam_props(layout, cam_type, props): + # needs a sublayout that we can deactivate because the ui_camera + # version assumes we are most definitely a circle camera... + col = layout.column() + col.active = cam_type == "circle" + col.label("Circle Camera:") + ui_camera.draw_circle_camera_props(col, props) + + _draw_props(layout, (ui_camera.draw_camera_mode_props, + ui_camera.draw_camera_poa_props, + ui_camera.draw_camera_pos_props, + ui_camera.draw_camera_manipulation_props, + _draw_circle_cam_props)) def footstep(modifier, layout, context): layout.prop(modifier, "bounds") diff --git a/korman/ui/ui_camera.py b/korman/ui/ui_camera.py index e4ef144..56179c9 100644 --- a/korman/ui/ui_camera.py +++ b/korman/ui/ui_camera.py @@ -14,69 +14,45 @@ # along with Korman. If not, see . import bpy +from .. import helpers + +def _draw_alert_prop(layout, props, the_prop, cam_type, alert_cam="", min=None, max=None, **kwargs): + can_alert = not alert_cam or alert_cam == cam_type + if can_alert: + value = getattr(props, the_prop) + if min is not None and value < min: + layout.alert = True + if max is not None and value > max: + layout.alert = True + layout.prop(props, the_prop, **kwargs) + layout.alert = False + else: + layout.prop(props, the_prop, **kwargs) + +def _draw_gated_prop(layout, props, gate_prop, actual_prop): + row = layout.row(align=True) + row.prop(props, gate_prop, text="") + row = row.row(align=True) + row.active = getattr(props, gate_prop) + row.prop(props, actual_prop) + +def draw_camera_manipulation_props(layout, cam_type, props): + # Camera Panning + split = layout.split() + col = split.column() + col.label("Limit Panning:") + col.prop(props, "x_pan_angle") + col.prop(props, "y_pan_angle") -class CameraButtonsPanel: - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "data" - - @classmethod - def poll(cls, context): - return (context.camera and context.scene.render.engine == "PLASMA_GAME") - - -class PlasmaCameraPanel(CameraButtonsPanel, bpy.types.Panel): - bl_label = "Plasma Camera" - - def draw(self, context): - camera = context.camera.plasma_camera - layout = self.layout - - layout.prop(camera, "camera_type") - layout.separator() - draw_camera_properties(camera.camera_type, camera.settings, layout, context) - - -class PlasmaCameraTransitionPanel(CameraButtonsPanel, bpy.types.Panel): - bl_label = "Plasma Transitions" - - def draw(self, context): - pass - - -def draw_camera_properties(cam_type, props, layout, context, force_no_anim=False): - trans = props.transition - - def _draw_alert_prop(layout, props, the_prop, cam="", min=None, max=None, **kwargs): - can_alert = not cam or cam == cam_type - if can_alert: - value = getattr(props, the_prop) - if min is not None and value < min: - layout.alert = True - if max is not None and value > max: - layout.alert = True - layout.prop(props, the_prop, **kwargs) - layout.alert = False - else: - layout.prop(props, the_prop, **kwargs) - def _draw_gated_prop(layout, props, gate_prop, actual_prop): - row = layout.row(align=True) - row.prop(props, gate_prop, text="") - row = row.row(align=True) - row.active = getattr(props, gate_prop) - row.prop(props, actual_prop) - def _is_camera_animated(cam_type, props, context, force_no_anim): - if force_no_anim or cam_type == "rail": - return False - # Check for valid animation data on either the object or the camera - # TODO: Should probably check for valid FCurve channels at some point??? - for i in (props.id_data, context.object): - if i.animation_data is None: - continue - if i.animation_data.action is not None: - return True - return False + # Camera Zoom + col = split.column() + col.label("Field of View:") + col.prop(props, "fov") + _draw_gated_prop(col, props, "limit_zoom", "zoom_min") + _draw_gated_prop(col, props, "limit_zoom", "zoom_max") + _draw_gated_prop(col, props, "limit_zoom", "zoom_rate") +def draw_camera_mode_props(layout, cam_type, props): # Point of Attention split = layout.split() col = split.column() @@ -99,8 +75,10 @@ def draw_camera_properties(cam_type, props, layout, context, force_no_anim=False col.prop(props, "fast_run") col.prop(props, "ignore_subworld") +def draw_camera_poa_props(layout, cam_type, props): + trans = props.transition + # PoA Tracking - layout.separator() split = layout.split() col = split.column() col.label("Default Tracking Transition:") @@ -115,17 +93,22 @@ def draw_camera_properties(cam_type, props, layout, context, force_no_anim=False col.prop(props, "poa_offset", text="") col.prop(props, "poa_worldspace") +def draw_camera_pos_props(layout, cam_type, props): + trans = props.transition + # Position Tracking (only for follow cams) - layout.separator() split = layout.split() col = split.column() # Position Transitions col.active = cam_type != "circle" col.label("Default Position Transition:") - _draw_alert_prop(col, trans, "pos_acceleration", cam="rail", max=10.0, text="Acceleration") - _draw_alert_prop(col, trans, "pos_deceleration", cam="rail", max=10.0, text="Deceleration") - _draw_alert_prop(col, trans, "pos_velocity", cam="rail", max=10.0, text="Maximum Velocity") + _draw_alert_prop(col, trans, "pos_acceleration", cam_type, + alert_cam="rail", max=10.0, text="Acceleration") + _draw_alert_prop(col, trans, "pos_deceleration", cam_type, + alert_cam="rail", max=10.0, text="Deceleration") + _draw_alert_prop(col, trans, "pos_velocity", cam_type, + alert_cam="rail", max=10.0, text="Maximum Velocity") col.prop(trans, "pos_cut") # Position Offsets @@ -135,39 +118,116 @@ def draw_camera_properties(cam_type, props, layout, context, force_no_anim=False col.prop(props, "pos_offset", text="") col.prop(props, "pos_worldspace") - # Camera Panning - layout.separator() - split = layout.split() - col = split.column() - col.label("Limit Panning:") - col.prop(props, "x_pan_angle") - col.prop(props, "y_pan_angle") - - # Camera Zoom - col = split.column() - col.label("Field of View:") - col.prop(props, "fov") - _draw_gated_prop(col, props, "limit_zoom", "zoom_min") - _draw_gated_prop(col, props, "limit_zoom", "zoom_max") - _draw_gated_prop(col, props, "limit_zoom", "zoom_rate") - +def draw_circle_camera_props(layout, props): # Circle Camera Stuff - layout.separator() - split = layout.split() - col = split.column() - col.active = cam_type == "circle" - col.label("Circle Camera:") - col.prop(props, "circle_center", text="") - col.prop(props, "circle_pos", text="") - col.prop(props, "circle_velocity") - row = col.row(align=True) + layout.prop(props, "circle_center") + layout.prop(props, "circle_pos") + layout.prop(props, "circle_velocity") + row = layout.row(align=True) row.active = props.circle_center is None row.prop(props, "circle_radius_ui") - # Animated Camera Stuff - col = split.column() - col.active = _is_camera_animated(cam_type, props, context, force_no_anim) - col.label("Animation:") - col.prop(props, "start_on_push") - col.prop(props, "stop_on_pop") - col.prop(props, "reset_on_pop") +class CameraButtonsPanel: + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "data" + + @classmethod + def poll(cls, context): + return (context.camera and context.scene.render.engine == "PLASMA_GAME") + + +class PlasmaCameraTypePanel(CameraButtonsPanel, bpy.types.Panel): + bl_label = "" + bl_options = {"HIDE_HEADER"} + + def draw(self, context): + camera = context.camera.plasma_camera + self.layout.prop(camera, "camera_type") + + +class PlasmaCameraModePanel(CameraButtonsPanel, bpy.types.Panel): + bl_label = "Camera Tracking" + + def draw(self, context): + camera = context.camera.plasma_camera + draw_camera_mode_props(self.layout, camera.camera_type, camera.settings) + + +class PlasmaCameraAttentionPanel(CameraButtonsPanel, bpy.types.Panel): + bl_label = "Point of Attention Tracking" + + def draw(self, context): + camera = context.camera.plasma_camera + draw_camera_poa_props(self.layout, camera.camera_type, camera.settings) + + +class PlasmaCameraPositionPanel(CameraButtonsPanel, bpy.types.Panel): + bl_label = "Position Tracking" + + def draw(self, context): + camera = context.camera.plasma_camera + draw_camera_pos_props(self.layout, camera.camera_type, camera.settings) + + +class PlasmaCameraCirclePanel(CameraButtonsPanel, bpy.types.Panel): + bl_label = "Circle Camera" + + def draw(self, context): + camera = context.camera.plasma_camera + draw_circle_camera_props(self.layout, camera.settings) + + @classmethod + def poll(cls, context): + return super().poll(context) and context.camera.plasma_camera.camera_type == "circle" + + +class PlasmaCameraAnimationPanel(CameraButtonsPanel, bpy.types.Panel): + bl_label = "Camera Animation" + bl_options = {"DEFAULT_CLOSED"} + + def draw(self, context): + layout = self.layout + camera = context.camera.plasma_camera + props = camera.settings + + split = layout.split() + col = split.column() + col.label("Animation:") + col.active = props.anim_enabled and any(helpers.fetch_fcurves(context.object)) + col.prop(props, "start_on_push") + col.prop(props, "stop_on_pop") + col.prop(props, "reset_on_pop") + + col = split.column() + col.active = camera.camera_type == "rail" + col.label("Rail:") + col.prop(props, "rail_pos", text="") + + def draw_header(self, context): + self.layout.active = any(helpers.fetch_fcurves(context.object)) + self.layout.prop(context.camera.plasma_camera.settings, "anim_enabled", text="") + + +class PlasmaCameraViewPanel(CameraButtonsPanel, bpy.types.Panel): + bl_label = "Camera Lens" + + def draw(self, context): + camera = context.camera.plasma_camera + draw_camera_manipulation_props(self.layout, camera.camera_type, camera.settings) + + +class TransitionListUI(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): + if item.camera is None: + layout.label("[Default Transition]") + else: + layout.label(item.camera.name, icon="CAMERA_DATA") + layout.prop(item, "enabled", text="") + + +class PlasmaCameraTransitionPanel(CameraButtonsPanel, bpy.types.Panel): + bl_label = "Transitions" + + def draw(self, context): + pass From 0f26deb65ec90d30f408fe36ca508863fa80d010 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 12 Aug 2018 18:49:30 -0400 Subject: [PATCH 07/10] Camera transition overrides --- korman/exporter/camera.py | 26 ++++++++++++++++++--- korman/exporter/convert.py | 2 +- korman/properties/prop_camera.py | 25 +++++++++++++++++++- korman/ui/ui_camera.py | 39 ++++++++++++++++++++++++++++++-- 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/korman/exporter/camera.py b/korman/exporter/camera.py index 420ed79..4b1d907 100644 --- a/korman/exporter/camera.py +++ b/korman/exporter/camera.py @@ -68,12 +68,12 @@ class CameraConverter: if camera_props.fast_run: brain.setFlags(plCameraBrain1.kSpeedUpWhenRunning, True) - def export_camera(self, so, bo, camera_type, camera_props): + def export_camera(self, so, bo, camera_type, camera_props, camera_trans=[]): brain = getattr(self, "_export_{}_camera".format(camera_type))(so, bo, camera_props) - mod = self._export_camera_modifier(so, bo, camera_props) + mod = self._export_camera_modifier(so, bo, camera_props, camera_trans) mod.brain = brain.key - def _export_camera_modifier(self, so, bo, props): + def _export_camera_modifier(self, so, bo, props, trans): mod = self._mgr.find_create_object(plCameraModifier, so=so) # PlasmaMAX allows the user to specify the horizontal OR vertical FOV, but not both. @@ -88,6 +88,26 @@ class CameraConverter: mod.startAnimOnPush = props.start_on_push mod.stopAnimOnPop = props.stop_on_pop mod.resetAnimOnPop = props.reset_on_pop + + for manual_trans in trans: + if not manual_trans.enabled or manual_trans.mode == "auto": + continue + cam_trans = plCameraModifier.CamTrans() + if manual_trans.camera: + cam_trans.transTo = self._mgr.find_create_key(plCameraModifier, bl=manual_trans.camera) + cam_trans.ignore = manual_trans.mode == "ignore" + + trans_info = manual_trans.transition + cam_trans.cutPos = trans_info.pos_cut + cam_trans.cutPOA = trans_info.poa_cut + cam_trans.accel = trans_info.pos_acceleration + cam_trans.decel = trans_info.pos_deceleration + cam_trans.velocity = trans_info.pos_velocity + cam_trans.poaAccel = trans_info.poa_acceleration + cam_trans.poaDecel = trans_info.poa_deceleration + cam_trans.poaVelocity = trans_info.poa_velocity + mod.addTrans(cam_trans) + return mod def _export_circle_camera(self, so, bo, props): diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index cc526bc..8fcf6d0 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -241,7 +241,7 @@ class Exporter: # Hey, guess what? Blender's camera data is utter crap! # NOTE: Animation export is dependent on camera type, so we'll do that later. camera = bo.data.plasma_camera - self.camera.export_camera(so, bo, camera.camera_type, camera.settings) + self.camera.export_camera(so, bo, camera.camera_type, camera.settings, camera.transitions) def _export_empty_blobj(self, so, bo): self.animation.convert_object_animations(bo, so) diff --git a/korman/properties/prop_camera.py b/korman/properties/prop_camera.py index 93d814a..7420e95 100644 --- a/korman/properties/prop_camera.py +++ b/korman/properties/prop_camera.py @@ -17,6 +17,8 @@ import bpy from bpy.props import * import math +from .. import idprops + camera_types = [("circle", "Circle Camera", "The camera circles a fixed point"), ("follow", "Follow Camera", "The camera follows an object"), ("fixed", "Fixed Camera", "The camera is fixed in one location"), @@ -56,6 +58,26 @@ class PlasmaTransition(bpy.types.PropertyGroup): options=set()) +class PlasmaManualTransition(bpy.types.PropertyGroup): + camera = PointerProperty(name="Camera", + description="Camera to transition to", + type=bpy.types.Object, + poll=idprops.poll_camera_objects, + options=set()) + transition = PointerProperty(type=PlasmaTransition, options=set()) + mode = EnumProperty(name="Transition Mode", + description="Type of transition that should occur between the two cameras", + items=[("ignore", "Ignore Camera", "Ignore this camera and do not transition"), + ("auto", "Auto", "Auto transition as defined by the two cameras' properies"), + ("manual", "Manual", "Manually defined transition")], + default="auto", + options=set()) + enabled = BoolProperty(name="Enabled", + description="Export this transition", + default=True, + options=set()) + + class PlasmaCameraProperties(bpy.types.PropertyGroup): # Point of Attention poa_type = EnumProperty(name="Point of Attention", @@ -217,7 +239,8 @@ class PlasmaCamera(bpy.types.PropertyGroup): items=camera_types, options=set()) settings = PointerProperty(type=PlasmaCameraProperties, options=set()) - transitions = CollectionProperty(type=PlasmaTransition, + transitions = CollectionProperty(type=PlasmaManualTransition, name="Transitions", description="", options=set()) + active_transition_index = IntProperty(options={"HIDDEN"}) diff --git a/korman/ui/ui_camera.py b/korman/ui/ui_camera.py index 56179c9..15d8fc9 100644 --- a/korman/ui/ui_camera.py +++ b/korman/ui/ui_camera.py @@ -14,7 +14,9 @@ # along with Korman. If not, see . import bpy + from .. import helpers +from . import ui_list def _draw_alert_prop(layout, props, the_prop, cam_type, alert_cam="", min=None, max=None, **kwargs): can_alert = not alert_cam or alert_cam == cam_type @@ -220,7 +222,7 @@ class PlasmaCameraViewPanel(CameraButtonsPanel, bpy.types.Panel): class TransitionListUI(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): if item.camera is None: - layout.label("[Default Transition]") + layout.label("[Default Camera]") else: layout.label(item.camera.name, icon="CAMERA_DATA") layout.prop(item, "enabled", text="") @@ -230,4 +232,37 @@ class PlasmaCameraTransitionPanel(CameraButtonsPanel, bpy.types.Panel): bl_label = "Transitions" def draw(self, context): - pass + layout = self.layout + camera = context.camera.plasma_camera + + ui_list.draw_list(layout, "TransitionListUI", "camera", camera, "transitions", + "active_transition_index", rows=3, maxrows=4) + + try: + item = camera.transitions[camera.active_transition_index] + trans = item.transition + except: + pass + else: + layout.separator() + box = layout.box() + box.prop(item, "camera") + box.prop(item, "mode") + + box.separator() + split = box.split() + split.active = item.mode == "manual" + + col = split.column() + col.label("Tracking Transition:") + col.prop(trans, "poa_acceleration", text="Acceleration") + col.prop(trans, "poa_deceleration", text="Deceleration") + col.prop(trans, "poa_velocity", text="Maximum Velocity") + col.prop(trans, "poa_cut") + + col = split.column() + col.label("Position Transition:") + col.prop(trans, "pos_acceleration", text="Acceleration") + col.prop(trans, "pos_deceleration", text="Deceleration") + col.prop(trans, "pos_velocity", text="Maximum Velocity") + col.prop(trans, "pos_cut") From e2b931fe9e53393c53326b7af88d92d4bfbdf179 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 13 Aug 2018 21:13:33 -0400 Subject: [PATCH 08/10] Add Camera Message node --- korman/nodes/node_messages.py | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index 237b88a..7f0fab8 100644 --- a/korman/nodes/node_messages.py +++ b/korman/nodes/node_messages.py @@ -299,6 +299,50 @@ class PlasmaAnimCmdMsgNode(idprops.IDPropMixin, PlasmaMessageWithCallbacksNode, "texture_name": bpy.data.textures} +class PlasmaCameraMsgNode(PlasmaMessageNode, bpy.types.Node): + bl_category = "MSG" + bl_idname = "PlasmaCameraMsgNode" + bl_label = "Camera" + bl_width_default = 200 + + cmd = EnumProperty(name="Command", + description="Command to send to the camera system", + items=[("push", "Push Camera", "Pushes a new camera onto the camera stack and transitions to it"), + ("pop", "Pop Camera", "Pops the camera off the camera stack"), + ("disablefp", "Disable First Person", "Forces the camera into third person if it is currently in first person and disables first person mode"), + ("enablefp", "Enable First Person", "Reenables the first person camera and switches back to it if the player was in first person previously")], + options=set()) + camera = PointerProperty(name="Camera", + type=bpy.types.Object, + poll=idprops.poll_camera_objects, + options=set()) + + def convert_message(self, exporter, so): + msg = plCameraMsg() + msg.BCastFlags |= plMessage.kLocalPropagate | plMessage.kBCastByType + if self.cmd in {"push", "pop"}: + if self.camera is not None: + msg.newCam = exporter.mgr.find_create_key(plSceneObject, bl=self.camera) + # It appears that kRegionPopCamera is unused. pushing is controlled by observing + # the presence of the kResponderTrigger command. + msg.setCmd(plCameraMsg.kResponderTrigger, self.cmd == "push") + msg.setCmd(plCameraMsg.kRegionPushCamera, True) + msg.setCmd(plCameraMsg.kSetAsPrimary, self.camera is None + or self.camera.data.plasma_camera.settings.primary_camera) + elif self.cmd == "disablefp": + msg.setCmd(plCameraMsg.kResponderSetThirdPerson) + elif self.cmd == "enablefp": + msg.setCmd(plCameraMsg.kResponderUndoThirdPerson) + else: + raise RuntimeError() + return msg + + def draw_buttons(self, context, layout): + layout.prop(self, "cmd") + if self.cmd in {"push", "pop"}: + layout.prop(self, "camera") + + class PlasmaEnableMsgNode(PlasmaMessageNode, bpy.types.Node): bl_category = "MSG" bl_idname = "PlasmaEnableMsgNode" From c91eb329393c33a278c604d8c20eb964c2ab3d8c Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 15 Aug 2018 18:44:57 -0400 Subject: [PATCH 09/10] Nuke auto circle camera Tey're probably just a disaster waiting to happen. If you want a circle camera, that's sufficiently advanced to require a new camera object. Auto cameras are just a dumb helper. --- korman/properties/modifiers/region.py | 2 +- korman/ui/modifiers/region.py | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index 73672ff..2c1ce42 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -70,8 +70,8 @@ class PlasmaCameraRegion(PlasmaModifierProperties): camera_type = EnumProperty(name="Camera Type", description="What kind of camera should be used?", items=[("auto_follow", "Auto Follow Camera", "Automatically generated follow camera"), - ("auto_circle", "Auto Circle Camera", "Automatically generated circle camera"), ("manual", "Manual Camera", "User specified camera object")], + default="manual", options=set()) camera_object = PointerProperty(name="Camera", description="Switches to this camera", diff --git a/korman/ui/modifiers/region.py b/korman/ui/modifiers/region.py index 2b18446..52eb6e7 100644 --- a/korman/ui/modifiers/region.py +++ b/korman/ui/modifiers/region.py @@ -28,19 +28,11 @@ def camera_rgn(modifier, layout, context): for i in cb: layout.separator() i(layout, cam_type, cam_props) - def _draw_circle_cam_props(layout, cam_type, props): - # needs a sublayout that we can deactivate because the ui_camera - # version assumes we are most definitely a circle camera... - col = layout.column() - col.active = cam_type == "circle" - col.label("Circle Camera:") - ui_camera.draw_circle_camera_props(col, props) _draw_props(layout, (ui_camera.draw_camera_mode_props, ui_camera.draw_camera_poa_props, ui_camera.draw_camera_pos_props, - ui_camera.draw_camera_manipulation_props, - _draw_circle_cam_props)) + ui_camera.draw_camera_manipulation_props)) def footstep(modifier, layout, context): layout.prop(modifier, "bounds") From 97305ee3e9c4e6d6d4bb760164a3193b3b33d8f2 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 15 Aug 2018 18:46:34 -0400 Subject: [PATCH 10/10] Clarify camera transitions/cutting What was previously marked as transition cuts would really only cut goal and POA tracking. Transition cutting requires the addition of a transition ovveride. The engine supports cutting transitions on plCameraMsg receipt by the virtual camera. The option looked sloppy on regions (and should probably be controlled in the transitions), but it has been added to camera message nodes for more control. --- korman/exporter/camera.py | 6 ++++-- korman/nodes/node_messages.py | 5 +++++ korman/properties/modifiers/region.py | 3 +-- korman/properties/prop_camera.py | 6 +++--- korman/ui/ui_camera.py | 16 ++++++++++------ 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/korman/exporter/camera.py b/korman/exporter/camera.py index 4b1d907..fd47497 100644 --- a/korman/exporter/camera.py +++ b/korman/exporter/camera.py @@ -49,9 +49,11 @@ class CameraConverter: brain.poaDeceleration = trans_props.poa_deceleration brain.poaVelocity = trans_props.poa_velocity - if trans_props.pos_cut: + if isinstance(brain, plCameraBrain1_Avatar): + brain.setFlags(plCameraBrain1.kCutPos, trans_props.pos_cut) + brain.setFlags(plCameraBrain1.kCutPOA, trans_props.poa_cut) + else: brain.setFlags(plCameraBrain1.kCutPos, True) - if trans_props.poa_cut: brain.setFlags(plCameraBrain1.kCutPOA, True) if camera_props.poa_type == "avatar": brain.setFlags(plCameraBrain1.kFollowLocalAvatar, True) diff --git a/korman/nodes/node_messages.py b/korman/nodes/node_messages.py index 7f0fab8..6c90411 100644 --- a/korman/nodes/node_messages.py +++ b/korman/nodes/node_messages.py @@ -316,6 +316,9 @@ class PlasmaCameraMsgNode(PlasmaMessageNode, bpy.types.Node): type=bpy.types.Object, poll=idprops.poll_camera_objects, options=set()) + cut = BoolProperty(name="Cut Transition", + description="Immediately swap over to the new camera without a transition animation", + options=set()) def convert_message(self, exporter, so): msg = plCameraMsg() @@ -329,6 +332,7 @@ class PlasmaCameraMsgNode(PlasmaMessageNode, bpy.types.Node): msg.setCmd(plCameraMsg.kRegionPushCamera, True) msg.setCmd(plCameraMsg.kSetAsPrimary, self.camera is None or self.camera.data.plasma_camera.settings.primary_camera) + msg.setCmd(plCameraMsg.kCut, self.cut) elif self.cmd == "disablefp": msg.setCmd(plCameraMsg.kResponderSetThirdPerson) elif self.cmd == "enablefp": @@ -341,6 +345,7 @@ class PlasmaCameraMsgNode(PlasmaMessageNode, bpy.types.Node): layout.prop(self, "cmd") if self.cmd in {"push", "pop"}: layout.prop(self, "camera") + layout.prop(self, "cut") class PlasmaEnableMsgNode(PlasmaMessageNode, bpy.types.Node): diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index 2c1ce42..cf60c4c 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -107,8 +107,7 @@ class PlasmaCameraRegion(PlasmaModifierProperties): msg = plCameraMsg() msg.BCastFlags |= plMessage.kLocalPropagate | plMessage.kBCastByType msg.setCmd(plCameraMsg.kRegionPushCamera) - if camera_props.primary_camera: - msg.setCmd(plCameraMsg.kSetAsPrimary) + msg.setCmd(plCameraMsg.kSetAsPrimary, camera_props.primary_camera) msg.newCam = camera_so_key region = exporter.mgr.find_create_object(plCameraRegionDetector, so=so) diff --git a/korman/properties/prop_camera.py b/korman/properties/prop_camera.py index 7420e95..cb80608 100644 --- a/korman/properties/prop_camera.py +++ b/korman/properties/prop_camera.py @@ -37,7 +37,7 @@ class PlasmaTransition(bpy.types.PropertyGroup): description="Maximum velocity of the camera's Point of Attention tracking", min=-100.0, max=100.0, precision=0, default=60.0, unit="VELOCITY", options=set()) - poa_cut = BoolProperty(name="Cut Transition", + poa_cut = BoolProperty(name="Cut", description="The camera immediately begins tracking the Point of Attention", options=set()) @@ -53,14 +53,14 @@ class PlasmaTransition(bpy.types.PropertyGroup): description="Maximum positional velocity of the camera", min=-100.0, max=100.0, precision=0, default=60.0, unit="VELOCITY", options=set()) - pos_cut = BoolProperty(name="Cut Transition", + pos_cut = BoolProperty(name="Cut", description="The camera immediately moves to its new position", options=set()) class PlasmaManualTransition(bpy.types.PropertyGroup): camera = PointerProperty(name="Camera", - description="Camera to transition to", + description="The camera from which this transition is intended", type=bpy.types.Object, poll=idprops.poll_camera_objects, options=set()) diff --git a/korman/ui/ui_camera.py b/korman/ui/ui_camera.py index 15d8fc9..d0533fc 100644 --- a/korman/ui/ui_camera.py +++ b/korman/ui/ui_camera.py @@ -87,7 +87,9 @@ def draw_camera_poa_props(layout, cam_type, props): col.prop(trans, "poa_acceleration", text="Acceleration") col.prop(trans, "poa_deceleration", text="Deceleration") col.prop(trans, "poa_velocity", text="Maximum Velocity") - col.prop(trans, "poa_cut") + col = col.column() + col.active = cam_type == "follow" + col.prop(trans, "poa_cut", text="Cut Animation") # PoA Offset col = split.column() @@ -111,7 +113,9 @@ def draw_camera_pos_props(layout, cam_type, props): alert_cam="rail", max=10.0, text="Deceleration") _draw_alert_prop(col, trans, "pos_velocity", cam_type, alert_cam="rail", max=10.0, text="Maximum Velocity") - col.prop(trans, "pos_cut") + col = col.column() + col.active = cam_type == "follow" + col.prop(trans, "pos_cut", text="Cut Animation") # Position Offsets col = split.column() @@ -222,7 +226,7 @@ class PlasmaCameraViewPanel(CameraButtonsPanel, bpy.types.Panel): class TransitionListUI(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): if item.camera is None: - layout.label("[Default Camera]") + layout.label("[Default Transition]") else: layout.label(item.camera.name, icon="CAMERA_DATA") layout.prop(item, "enabled", text="") @@ -246,7 +250,7 @@ class PlasmaCameraTransitionPanel(CameraButtonsPanel, bpy.types.Panel): else: layout.separator() box = layout.box() - box.prop(item, "camera") + box.prop(item, "camera", text="Transition From") box.prop(item, "mode") box.separator() @@ -258,11 +262,11 @@ class PlasmaCameraTransitionPanel(CameraButtonsPanel, bpy.types.Panel): col.prop(trans, "poa_acceleration", text="Acceleration") col.prop(trans, "poa_deceleration", text="Deceleration") col.prop(trans, "poa_velocity", text="Maximum Velocity") - col.prop(trans, "poa_cut") + col.prop(trans, "poa_cut", text="Cut Transition") col = split.column() col.label("Position Transition:") col.prop(trans, "pos_acceleration", text="Acceleration") col.prop(trans, "pos_deceleration", text="Deceleration") col.prop(trans, "pos_velocity", text="Maximum Velocity") - col.prop(trans, "pos_cut") + col.prop(trans, "pos_cut", text="Cut Transition")