Browse Source

Merge pull request #105 from Hoikas/cameras

Cameras
pull/115/head
Adam Johnson 6 years ago committed by GitHub
parent
commit
10b65e9ab9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 137
      korman/exporter/animation.py
  2. 225
      korman/exporter/camera.py
  3. 16
      korman/exporter/convert.py
  4. 13
      korman/exporter/utils.py
  5. 13
      korman/helpers.py
  6. 3
      korman/idprops.py
  7. 58
      korman/nodes/node_messages.py
  8. 2
      korman/properties/__init__.py
  9. 65
      korman/properties/modifiers/region.py
  10. 246
      korman/properties/prop_camera.py
  11. 1
      korman/ui/__init__.py
  12. 19
      korman/ui/modifiers/region.py
  13. 272
      korman/ui/ui_camera.py

137
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)

225
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 <http://www.gnu.org/licenses/>.
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

16
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:

13
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)

13
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"""

3
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"

58
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"

2
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)

65
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"

246
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 <http://www.gnu.org/licenses/>.
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"})

1
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 <http://www.gnu.org/licenses/>.
from .ui_camera import *
from .ui_lamp import *
from .ui_list import *
from .ui_menus import *

19
korman/ui/modifiers/region.py

@ -14,6 +14,25 @@
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
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")

272
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 <http://www.gnu.org/licenses/>.
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")
Loading…
Cancel
Save