From da30e3f0c7b46a9d174b384cc90a32db714f187a Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 8 Jul 2018 22:04:57 -0400 Subject: [PATCH] Implement basic cameras This implments basic follow and fixed cameras. Currently, animating cameras and/or their field of view are/is not supported. --- korman/exporter/camera.py | 104 +++++++++++++++++ korman/exporter/convert.py | 7 ++ korman/idprops.py | 3 + korman/properties/__init__.py | 2 + korman/properties/modifiers/region.py | 61 ++++++++++ korman/properties/prop_camera.py | 160 ++++++++++++++++++++++++++ korman/render.py | 4 + korman/ui/__init__.py | 1 + korman/ui/modifiers/region.py | 8 ++ korman/ui/ui_camera.py | 128 +++++++++++++++++++++ 10 files changed, 478 insertions(+) create mode 100644 korman/exporter/camera.py create mode 100644 korman/properties/prop_camera.py create mode 100644 korman/ui/ui_camera.py diff --git a/korman/exporter/camera.py b/korman/exporter/camera.py new file mode 100644 index 0000000..7524e70 --- /dev/null +++ b/korman/exporter/camera.py @@ -0,0 +1,104 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy +import math +from PyHSPlasma import * +import weakref + +from .explosions import * + +class CameraConverter: + def __init__(self, exporter): + self._exporter = weakref.ref(exporter) + + def _convert_brain(self, so, bo, camera_props, brain): + trans_props = camera_props.transition + + brain.poaOffset = hsVector3(*camera_props.poa_offset) + if isinstance(brain, plCameraBrain1_Avatar): + brain.offset = hsVector3(*camera_props.pos_offset) + if camera_props.poa_type == "object": + brain.subject = self._mgr.find_create_key(plSceneObject, bl=camera_props.poa_object) + + brain.xPanLimit = camera_props.x_pan_angle / 2.0 + brain.zPanLimit = camera_props.y_pan_angle / 2.0 + brain.panSpeed = camera_props.pan_rate + if camera_props.limit_zoom: + brain.setFlags(plCameraBrain1.kZoomEnabled, True) + brain.zoomMax = camera_props.zoom_max * (4.0 / 3.0) + brain.zoomMin = camera_props.zoom_min * (4.0 / 3.0) + brain.zoomRate = camera_props.zoom_rate + + brain.acceleration = trans_props.pos_acceleration + brain.deceleration = trans_props.pos_deceleration + brain.velocity = trans_props.pos_velocity + brain.poaAcceleration = trans_props.poa_acceleration + brain.poaDeceleration = trans_props.poa_deceleration + brain.poaVelocity = trans_props.poa_velocity + + if trans_props.pos_cut: + brain.setFlags(plCameraBrain1.kCutPos, True) + if trans_props.poa_cut: + brain.setFlags(plCameraBrain1.kCutPOA, True) + if camera_props.poa_type == "avatar": + brain.setFlags(plCameraBrain1.kFollowLocalAvatar, True) + if camera_props.maintain_los: + brain.setFlags(plCameraBrain1.kMaintainLOS, True) + if camera_props.poa_worldspace: + brain.setFlags(plCameraBrain1.kWorldspacePOA, True) + if camera_props.pos_worldspace: + brain.setFlags(plCameraBrain1.kWorldspacePos, True) + if camera_props.ignore_subworld: + brain.setFlags(plCameraBrain1.kIgnoreSubworldMovement, True) + if camera_props.fall_vertical: + brain.setFlags(plCameraBrain1.kVerticalWhenFalling, True) + if camera_props.fast_run: + brain.setFlags(plCameraBrain1.kSpeedUpWhenRunning, True) + + def export_camera(self, so, bo, camera_type, camera_props): + mod = self._export_camera_modifier(so, bo, camera_props) + brain = getattr(self, "_export_{}_camera".format(camera_type))(so, bo, camera_props) + mod.brain = brain.key + + def _export_camera_modifier(self, so, bo, props): + # PlasmaMAX allows the user to specify the horizontal OR vertical FOV, but not both. + # We only allow setting horizontal FOV (how often do you look up?), however. + # Plasma assumes 4:3 aspect ratio... + mod = self._mgr.find_create_object(plCameraModifier, so=so) + fov = props.fov + mod.fovW, mod.fovH = math.degrees(fov), math.degrees(fov * (3.0 / 4.0)) + + # TODO: do we need to do something about animations here? + return mod + + def _export_fixed_camera(self, so, bo, props): + brain = self._mgr.find_create_object(plCameraBrain1_Fixed, so=so) + self._convert_brain(so, bo, props, brain) + # TODO: animations??? + return brain + + def _export_follow_camera(self, so, bo, props): + brain = self._mgr.find_create_object(plCameraBrain1_Avatar, so=so) + self._convert_brain(so, bo, props, brain) + return brain + + @property + def _mgr(self): + return self._exporter().mgr + + @property + def _report(self): + return self._exporter().report diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 8bf5da0..75d7206 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -20,6 +20,7 @@ from PyHSPlasma import * import time from . import animation +from . import camera from . import explosions from . import etlight from . import logger @@ -52,6 +53,7 @@ class Exporter: self.light = rtlight.LightConverter(self) self.animation = animation.AnimationConverter(self) self.sumfile = sumfile.SumFile() + self.camera = camera.CameraConverter(self) # Step 0.8: Init the progress mgr self.mesh.add_progress_presteps(self.report) @@ -236,6 +238,11 @@ class Exporter: mod.export(self, bl_obj, sceneobject) inc_progress() + def _export_camera_blobj(self, so, bo): + # Hey, guess what? Blender's camera data is utter crap! + camera = bo.data.plasma_camera + self.camera.export_camera(so, bo, camera.camera_type, camera.settings) + def _export_empty_blobj(self, so, bo): # We don't need to do anything here. This function just makes sure we don't error out # or add a silly special case :( diff --git a/korman/idprops.py b/korman/idprops.py index 1423943..66328d2 100644 --- a/korman/idprops.py +++ b/korman/idprops.py @@ -124,6 +124,9 @@ def poll_animated_objects(self, value): return True return False +def poll_camera_objects(self, value): + return value.type == "CAMERA" + def poll_empty_objects(self, value): return value.type == "EMPTY" diff --git a/korman/properties/__init__.py b/korman/properties/__init__.py index dc98a70..aed2561 100644 --- a/korman/properties/__init__.py +++ b/korman/properties/__init__.py @@ -15,6 +15,7 @@ import bpy +from .prop_camera import * from .prop_lamp import * from . import modifiers from .prop_object import * @@ -23,6 +24,7 @@ from .prop_world import * def register(): + bpy.types.Camera.plasma_camera = bpy.props.PointerProperty(type=PlasmaCamera) bpy.types.Lamp.plasma_lamp = bpy.props.PointerProperty(type=PlasmaLamp) bpy.types.Object.plasma_net = bpy.props.PointerProperty(type=PlasmaNet) bpy.types.Object.plasma_object = bpy.props.PointerProperty(type=PlasmaObject) diff --git a/korman/properties/modifiers/region.py b/korman/properties/modifiers/region.py index 99cde54..dfff86a 100644 --- a/korman/properties/modifiers/region.py +++ b/korman/properties/modifiers/region.py @@ -22,6 +22,7 @@ from ...helpers import TemporaryObject from ... import idprops from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz +from ..prop_camera import PlasmaCameraProperties from .physics import bounds_types footstep_surface_ids = { @@ -58,6 +59,66 @@ footstep_surfaces = [("dirt", "Dirt", "Dirt"), ("woodfloor", "Wood Floor", "Wood Floor"), ("woodladder", "Wood Ladder", "Wood Ladder")] +class PlasmaCameraRegion(PlasmaModifierProperties): + pl_id = "camera_rgn" + + bl_category = "Region" + bl_label = "Camera Region" + bl_description = "Camera Region" + bl_icon = "CAMERA_DATA" + + camera_type = EnumProperty(name="Camera Type", + description="What kind of camera should be used?", + items=[("auto", "Auto Follow Camera", "Automatically generated camera"), + ("manual", "Manual Camera", "User specified camera object")], + options=set()) + camera_object = PointerProperty(name="Camera", + description="Switches to this camera", + type=bpy.types.Object, + poll=idprops.poll_camera_objects, + options=set()) + auto_camera = PointerProperty(type=PlasmaCameraProperties, options=set()) + + def export(self, exporter, bo, so): + if self.camera_type == "manual": + if self.camera_object is None: + raise ExportError("Camera Modifier '{}' does not specify a valid camera object".format(self.id_data.name)) + camera_so_key = exporter.mgr.find_create_key(plSceneObject, bl=self.camera_object) + camera_props = self.camera_object.data.plasma_camera.settings + else: + # Wheedoggy! We get to export the doggone camera now. + camera_props = self.auto_camera + exporter.camera.export_camera(so, bo, "follow", camera_props) + + # Setup physical stuff + phys_mod = bo.plasma_modifiers.collision + simIface, physical = exporter.physics.generate_physical(bo, so, phys_mod.bounds, self.key_name) + physical.memberGroup = plSimDefs.kGroupDetector + physical.reportGroup = 1 << plSimDefs.kGroupAvatar + simIface.setProperty(plSimulationInterface.kPinned, True) + physical.setProperty(plSimulationInterface.kPinned, True) + + # I don't feel evil enough to make this generate a logic tree... + msg = plCameraMsg() + msg.BCastFlags |= plMessage.kLocalPropagate | plMessage.kBCastByType + msg.setCmd(plCameraMsg.kRegionPushCamera) + if camera_props.primary_camera: + msg.setCmd(plCameraMsg.kSetAsPrimary) + msg.newCam = camera_so_key + + region = exporter.mgr.find_create_object(plCameraRegionDetector, so=so) + region.addMessage(msg) + + def harvest_actors(self): + if self.camera_type == "manual": + if self.camera_object is None: + raise ExportError("Camera Modifier '{}' does not specify a valid camera object".format(self.id_data.name)) + camera = self.camera_object.data.plasma_camera.settings + else: + camera = self.auto_camera + return camera.harvest_actors() + + class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz): pl_id = "footstep" diff --git a/korman/properties/prop_camera.py b/korman/properties/prop_camera.py new file mode 100644 index 0000000..fd5b7fe --- /dev/null +++ b/korman/properties/prop_camera.py @@ -0,0 +1,160 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy +from bpy.props import * +import math + +camera_types = [("follow", "Follow Camera", "Camera follows an object."), + ("fixed", "Fixed Camera", "Camera is fixed in one location.")] + +class PlasmaTransition(bpy.types.PropertyGroup): + poa_acceleration = FloatProperty(name="PoA Acceleration", + description="Rate the camera's Point of Attention tracking velocity increases in feet per second squared", + min=-100.0, max=100.0, precision=0, default=60.0, + unit="ACCELERATION", options=set()) + poa_deceleration = FloatProperty(name="PoA Deceleration", + description="Rate the camera's Point of Attention tracking velocity decreases in feet per second squared", + min=-100.0, max=100.0, precision=0, default=60.0, + unit="ACCELERATION", options=set()) + poa_velocity = FloatProperty(name="PoA Velocity", + description="Maximum velocity of the camera's Point of Attention tracking", + min=-100.0, max=100.0, precision=0, default=60.0, + unit="VELOCITY", options=set()) + poa_cut = BoolProperty(name="Cut Transition", + description="The camera immediately begins tracking the Point of Attention", + options=set()) + + pos_acceleration = FloatProperty(name="Position Acceleration", + description="Rate the camera's positional velocity increases in feet per second squared", + min=-100.0, max=100.0, precision=0, default=60.0, + unit="ACCELERATION", options=set()) + pos_deceleration = FloatProperty(name="Position Deceleration", + description="Rate the camera's positional velocity decreases in feet per second squared", + min=-100.0, max=100.0, precision=0, default=60.0, + unit="ACCELERATION", options=set()) + pos_velocity = FloatProperty(name="Position Max Velocity", + description="Maximum positional velocity of the camera", + min=-100.0, max=100.0, precision=0, default=60.0, + unit="VELOCITY", options=set()) + pos_cut = BoolProperty(name="Cut Transition", + description="The camera immediately moves to its new position", + options=set()) + + +class PlasmaCameraProperties(bpy.types.PropertyGroup): + # Point of Attention + poa_type = EnumProperty(name="Point of Attention", + description="The point of attention that this camera tracks", + items=[("avatar", "Track Local Player", "Camera tracks the player's avatar"), + ("object", "Track Object", "Camera tracks an object in the scene"), + ("none", "Don't Track", "Camera does not track anything")], + options=set()) + poa_object = PointerProperty(name="PoA Object", + description="Object the camera should track as its Point of Attention", + type=bpy.types.Object, + options=set()) + poa_offset = FloatVectorProperty(name="PoA Offset", + description="Offset from the point of attention's origin to track", + soft_min=-50.0, soft_max=50.0, + size=3, default=(0.0, 0.0, 3.0), + options=set()) + poa_worldspace = BoolProperty(name="Worldspace Offset", + description="Point of Attention Offset is in worldspace coordinates", + options=set()) + + # Position Offset + pos_offset = FloatVectorProperty(name="Position Offset", + description="Offset the camera's position", + soft_min=-50.0, soft_max=50.0, + size=3, default=(0.0, 10.0, 3.0), + options=set()) + pos_worldspace = BoolProperty(name="Worldspace Offset", + description="Position offset is in worldspace coordinates", + options=set()) + + # Default Transition + transition = PointerProperty(type=PlasmaTransition, options=set()) + + # Limit Panning + x_pan_angle = FloatProperty(name="X Degrees", + description="Maximum camera pan angle in the X direction", + min=0.0, max=math.radians(180.0), precision=0, default=math.radians(90.0), + subtype="ANGLE", options=set()) + y_pan_angle = FloatProperty(name="Y Degrees", + description="Maximum camera pan angle in the Y direction", + min=0.0, max=math.radians(180.0), precision=0, default=math.radians(90.0), + subtype="ANGLE", options=set()) + pan_rate = FloatProperty(name="Pan Velocity", + description="", + min=0.0, precision=1, default=50.0, + unit="VELOCITY", options=set()) + + # Zooming + fov = FloatProperty(name="Default FOV", + description="Horizontal Field of View angle", + min=0.0, max=math.radians(180.0), precision=0, default=math.radians(70.0), + subtype="ANGLE") + limit_zoom = BoolProperty(name="Limit Zoom", + description="The camera allows zooming per artist limitations", + options=set()) + zoom_max = FloatProperty(name="Max FOV", + description="Maximum camera FOV when zooming", + min=0.0, max=math.radians(180.0), precision=0, default=math.radians(120.0), + subtype="ANGLE", options=set()) + zoom_min = FloatProperty(name="Min FOV", + description="Minimum camera FOV when zooming", + min=0.0, max=math.radians(180.0), precision=0, default=math.radians(35.0), + subtype="ANGLE", options=set()) + zoom_rate = FloatProperty(name="Zoom Velocity", + description="Velocity of the camera's zoom in degrees per second", + min=0.0, max=180.0, precision=0, default=90.0, + unit="VELOCITY", options=set()) + + # Miscellaneous Movement Props + maintain_los = BoolProperty(name="Maintain LOS", + description="The camera should maintain line-of-sight with the object it's tracking", + options=set()) + fall_vertical = BoolProperty(name="Fall Camera", + description="The camera will orient itself vertically when the local player begins falling", + options=set()) + fast_run = BoolProperty(name="Faster When Falling", + description="The camera's velocity will have a floor when the local player is falling", + options=set()) + ignore_subworld = BoolProperty(name="Ignore Subworld Movement", + description="The camera will not be parented to any subworlds", + options=set()) + + # Core Type Properties + primary_camera = BoolProperty(name="Primary Camera", + description="The camera should be considered the Age's primary camera.", + options=set()) + + def harvest_actors(self): + if self.poa_type == "object": + return set((self.poa_object.name),) + return set() + + +class PlasmaCamera(bpy.types.PropertyGroup): + camera_type = EnumProperty(name="Camera Type", + description="", + items=camera_types, + options=set()) + settings = PointerProperty(type=PlasmaCameraProperties, options=set()) + transitions = CollectionProperty(type=PlasmaTransition, + name="Transitions", + description="", + options=set()) diff --git a/korman/render.py b/korman/render.py index 393b9eb..e51304e 100644 --- a/korman/render.py +++ b/korman/render.py @@ -36,6 +36,10 @@ properties_material.MATERIAL_PT_transp.COMPAT_ENGINES.add("PLASMA_GAME") properties_material.MATERIAL_PT_shadow.COMPAT_ENGINES.add("PLASMA_GAME") del properties_material +from bl_ui import properties_data_camera +properties_data_camera.DATA_PT_context_camera.COMPAT_ENGINES.add("PLASMA_GAME") +del properties_data_camera + from bl_ui import properties_data_mesh properties_data_mesh.DATA_PT_uv_texture.COMPAT_ENGINES.add("PLASMA_GAME") properties_data_mesh.DATA_PT_vertex_colors.COMPAT_ENGINES.add("PLASMA_GAME") diff --git a/korman/ui/__init__.py b/korman/ui/__init__.py index c1f3e3f..dde0d43 100644 --- a/korman/ui/__init__.py +++ b/korman/ui/__init__.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU General Public License # along with Korman. If not, see . +from .ui_camera import * from .ui_lamp import * from .ui_list import * from .ui_menus import * diff --git a/korman/ui/modifiers/region.py b/korman/ui/modifiers/region.py index 58b6b23..80dc272 100644 --- a/korman/ui/modifiers/region.py +++ b/korman/ui/modifiers/region.py @@ -14,6 +14,14 @@ # along with Korman. If not, see . import bpy +from ..ui_camera import draw_camera_properties + +def camera_rgn(modifier, layout, context): + layout.prop(modifier, "camera_type") + if modifier.camera_type == "manual": + layout.prop(modifier, "camera_object", icon="CAMERA_DATA") + else: + draw_camera_properties("follow", modifier.auto_camera, layout, context) def footstep(modifier, layout, context): layout.prop(modifier, "bounds") diff --git a/korman/ui/ui_camera.py b/korman/ui/ui_camera.py new file mode 100644 index 0000000..43a3e71 --- /dev/null +++ b/korman/ui/ui_camera.py @@ -0,0 +1,128 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy + +class CameraButtonsPanel: + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "data" + + @classmethod + def poll(cls, context): + return (context.camera and context.scene.render.engine == "PLASMA_GAME") + + +class PlasmaCameraPanel(CameraButtonsPanel, bpy.types.Panel): + bl_label = "Plasma Camera" + + def draw(self, context): + camera = context.camera.plasma_camera + layout = self.layout + + layout.prop(camera, "camera_type") + layout.separator() + draw_camera_properties(camera.camera_type, camera.settings, layout, context) + + +class PlasmaCameraTransitionPanel(CameraButtonsPanel, bpy.types.Panel): + bl_label = "Plasma Transitions" + + def draw(self, context): + pass + + +def draw_camera_properties(cam_type, props, layout, context): + trans = props.transition + + def _draw_gated_prop(layout, props, gate_prop, actual_prop): + row = layout.row(align=True) + row.prop(props, gate_prop, text="") + row = row.row(align=True) + row.active = getattr(props, gate_prop) + row.prop(props, actual_prop) + + # Point of Attention + split = layout.split() + col = split.column() + col.label("Camera Mode:") + col = col.column() + col.alert = cam_type == "follow" and props.poa_type == "none" + col.prop(props, "poa_type", text="") + col.alert = False + row = col.row() + row.active = props.poa_type == "object" + row.prop(props, "poa_object", text="") + col.separator() + col.prop(props, "primary_camera") + + # Miscellaneous + col = split.column() + col.label("Tracking Settings:") + col.prop(props, "maintain_los") + col.prop(props, "fall_vertical") + col.prop(props, "fast_run") + col.prop(props, "ignore_subworld") + + # PoA Tracking + layout.separator() + split = layout.split() + col = split.column() + col.label("Default Tracking Transition:") + col.prop(trans, "poa_acceleration", text="Acceleration") + col.prop(trans, "poa_deceleration", text="Deceleration") + col.prop(trans, "poa_velocity", text="Maximum Velocity") + col.prop(trans, "poa_cut") + + # PoA Offset + col = split.column() + col.label("Point of Attention Offset:") + col.prop(props, "poa_offset", text="") + col.prop(props, "poa_worldspace") + + # Position Tracking (only for follow cams) + layout.separator() + split = layout.split() + col = split.column() + + # Position Transitions + col.label("Default Position Transition:") + col.prop(trans, "pos_acceleration", text="Acceleration") + col.prop(trans, "pos_deceleration", text="Deceleration") + col.prop(trans, "pos_velocity", text="Maximum Velocity") + col.prop(trans, "pos_cut") + + # Position Offsets + col = split.column() + col.active = cam_type == "follow" + col.label("Position Offset:") + col.prop(props, "pos_offset", text="") + col.prop(props, "pos_worldspace") + + # Camera Panning + layout.separator() + split = layout.split() + col = split.column() + col.label("Limit Panning:") + col.prop(props, "x_pan_angle") + col.prop(props, "y_pan_angle") + + # Camera Zoom + col = split.column() + col.label("Field of View:") + col.prop(props, "fov") + _draw_gated_prop(col, props, "limit_zoom", "zoom_min") + _draw_gated_prop(col, props, "limit_zoom", "zoom_max") + _draw_gated_prop(col, props, "limit_zoom", "zoom_rate")