diff --git a/korman/exporter/animation.py b/korman/exporter/animation.py
index fd1ac63..8099423 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,41 +352,37 @@ 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):
+ 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
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
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)
@@ -317,6 +394,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
new file mode 100644
index 0000000..fd47497
--- /dev/null
+++ b/korman/exporter/camera.py
@@ -0,0 +1,225 @@
+# 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 *
+from .. import helpers
+from . import utils
+
+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 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 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)
+ 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, camera_trans=[]):
+ brain = getattr(self, "_export_{}_camera".format(camera_type))(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, trans):
+ 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..
+ fov = props.fov
+ mod.fovW, mod.fovH = math.degrees(fov), math.degrees(fov * (3.0 / 4.0))
+
+ # 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
+
+ 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):
+ 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):
+ 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
+
+ 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(*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
+ if props.rail_pos == "farthest":
+ path.flags |= plAnimPath.kFarthest
+ 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
+
+ @property
+ def _report(self):
+ return self._exporter().report
diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py
index 4387751..558d9f6 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)
@@ -230,7 +232,6 @@ class Exporter:
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)
# And now we puke out the modifiers...
for mod in bl_obj.plasma_modifiers.modifiers:
@@ -238,16 +239,21 @@ 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!
+ # 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, camera.transitions)
+
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/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/nodes/node_messages.py b/korman/nodes/node_messages.py
index 1dbfe24..6c90411 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:
@@ -306,6 +299,55 @@ 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())
+ 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()
+ 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)
+ msg.setCmd(plCameraMsg.kCut, self.cut)
+ 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")
+ layout.prop(self, "cut")
+
+
class PlasmaEnableMsgNode(PlasmaMessageNode, bpy.types.Node):
bl_category = "MSG"
bl_idname = "PlasmaEnableMsgNode"
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 3377f32..5db0098 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,70 @@ 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_follow", "Auto Follow Camera", "Automatically generated follow camera"),
+ ("manual", "Manual Camera", "User specified camera object")],
+ default="manual",
+ 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:
+ assert self.camera_type[:4] == "auto"
+
+ # Wheedoggy! We get to export the doggone camera now.
+ 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
+ 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)
+ msg.setCmd(plCameraMsg.kSetAsPrimary, camera_props.primary_camera)
+ 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..cb80608
--- /dev/null
+++ b/korman/properties/prop_camera.py
@@ -0,0 +1,246 @@
+# 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
+
+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"),
+ ("rail", "Rail Camera", "The camera follows an object by moving along a line")]
+
+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",
+ 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",
+ description="The camera immediately moves to its new position",
+ options=set())
+
+
+class PlasmaManualTransition(bpy.types.PropertyGroup):
+ camera = PointerProperty(name="Camera",
+ description="The camera from which this transition is intended",
+ 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",
+ 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())
+
+ # 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 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",
+ 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"})
+
+ # 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
+ 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),)
+ 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=PlasmaManualTransition,
+ name="Transitions",
+ description="",
+ options=set())
+ active_transition_index = IntProperty(options={"HIDDEN"})
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..52eb6e7 100644
--- a/korman/ui/modifiers/region.py
+++ b/korman/ui/modifiers/region.py
@@ -14,6 +14,25 @@
# along with Korman. If not, see .
import bpy
+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:
+ 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)
+
+ _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))
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..d0533fc
--- /dev/null
+++ b/korman/ui/ui_camera.py
@@ -0,0 +1,272 @@
+# 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 .. 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
+ 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")
+
+ # 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()
+ col.label("Camera Mode:")
+ col = col.column()
+ col.alert = cam_type != "fixed" 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")
+
+def draw_camera_poa_props(layout, cam_type, props):
+ trans = props.transition
+
+ # PoA Tracking
+ 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 = col.column()
+ col.active = cam_type == "follow"
+ col.prop(trans, "poa_cut", text="Cut Animation")
+
+ # PoA Offset
+ col = split.column()
+ col.label("Point of Attention Offset:")
+ 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)
+ 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_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 = col.column()
+ col.active = cam_type == "follow"
+ col.prop(trans, "pos_cut", text="Cut Animation")
+
+ # 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")
+
+def draw_circle_camera_props(layout, props):
+ # Circle Camera Stuff
+ 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")
+
+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):
+ 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", text="Transition From")
+ 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", 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", text="Cut Transition")