diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index b74cca8..c534be1 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -23,6 +23,7 @@ from . import logger from . import manager from . import mesh from . import physics +from . import rtlight from . import utils class Exporter: @@ -44,6 +45,7 @@ class Exporter: self.mesh = mesh.MeshConverter(self) self.report = logger.ExportAnalysis() self.physics = physics.PhysicsConverter(self) + self.light = rtlight.LightConverter(self) # Step 1: Gather a list of objects that we need to export # We should do this first so we can sanity check @@ -120,10 +122,7 @@ class Exporter: """Ensures that the SceneObject has a CoordinateInterface""" if not so.coord: print(" Exporting CoordinateInterface") - - ci = self.mgr.find_create_key(bo, plCoordinateInterface) - so.coord = ci - ci = ci.object + ci = self.mgr.find_create_key(bo, plCoordinateInterface).object # Now we have the "fun" work of filling in the CI ci.localToWorld = utils.matrix44(bo.matrix_basis) @@ -163,8 +162,12 @@ class Exporter: # or add a silly special case :( pass + def _export_lamp_blobj(self, so, bo): + # We'll just redirect this to the RT Light converter... + self.light.export_rtlight(so, bo) + def _export_mesh_blobj(self, so, bo): if bo.data.materials: - so.draw = self.mesh.export_object(bo) + self.mesh.export_object(bo) else: print(" No material(s) on the ObData, so no drawables") diff --git a/korman/exporter/explosions.py b/korman/exporter/explosions.py index bff86fb..ec0d53a 100644 --- a/korman/exporter/explosions.py +++ b/korman/exporter/explosions.py @@ -18,6 +18,11 @@ class ExportError(Exception): super(Exception, self).__init__(value) +class BlenderOptionNotSupportedError(ExportError): + def __init__(self, opt): + super(ExportError, self).__init__("Unsupported Blender Option: '{}'".format(opt)) + + class GLLoadError(ExportError): def __init__(self, image): super(ExportError, self).__init__("Failed to load '{}' into OpenGL".format(image.name)) diff --git a/korman/exporter/manager.py b/korman/exporter/manager.py index f77afc1..7706ea7 100644 --- a/korman/exporter/manager.py +++ b/korman/exporter/manager.py @@ -91,9 +91,29 @@ class ExportManager: if isinstance(pl, plObjInterface): if so is None: - pl.owner = self.find_key(bl, plSceneObject) + key = self.find_key(bl, plSceneObject) + # prevent race conditions + if key is None: + so = self.add_object(plSceneObject, name=name, loc=location) + key = so.key + else: + so = key.object + pl.owner = key else: pl.owner = so.key + + # The things I do to make life easy... + # This is something of a God function now. + if isinstance(pl, plAudioInterface): + so.audio = pl.key + elif isinstance(pl, plCoordinateInterface): + so.coord = pl.key + elif isinstance(pl, plDrawInterface): + so.draw = pl.key + elif isinstance(pl, plSimulationInterface): + so.sim = pl.key + else: + so.addInterface(pl.key) elif isinstance(pl, plModifier): so.addModifier(pl.key) @@ -138,15 +158,18 @@ class ExportManager: self._nodes[location] = None return location - def find_create_key(self, bl_obj, pClass): - key = self.find_key(bl_obj, pClass) + def find_create_key(self, bl_obj, pClass, so=None): + key = self.find_key(bl_obj, pClass, so=so) if key is None: - key = self.add_object(pl=pClass, bl=bl_obj).key + key = self.add_object(pl=pClass, bl=bl_obj, so=so).key return key - def find_key(self, bl_obj, pClass): + def find_key(self, bl_obj, pClass, so=None): """Given a blender Object and a Plasma class, find (or create) an exported plKey""" - location = self._pages[bl_obj.plasma_object.page] + if so is None: + location = self._pages[bl_obj.plasma_object.page] + else: + location = so.key.location index = plFactory.ClassIndex(pClass.__name__) for key in self.mgr.getKeys(location, index): diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index cdb4b7c..599cdf9 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -109,8 +109,12 @@ class MeshConverter: raise explosions.TooManyUVChannelsError(bo, bm) geospan.format = numUVWchans - # TODO: Props - # TODO: RunTime lights (requires libHSPlasma feature) + # Harvest lights + permaLights, permaProjs = self._exporter().light.find_material_light_keys(bo, bm) + for i in permaLights: + geospan.addPermaLight(i) + for i in permaProjs: + geospan.addPermaProjs(i) # If this object has a CI, we don't need xforms here... if self._mgr.has_coordiface(bo): @@ -245,7 +249,6 @@ class MeshConverter: diface = self._mgr.add_object(pl=plDrawInterface, bl=bo) for dspan_key, idx in drawables: diface.addDrawable(dspan_key, idx) - return diface.key def _export_mesh(self, bo): # Step 0.8: If this mesh wants to be lit, we need to go ahead and generate it. @@ -290,10 +293,10 @@ class MeshConverter: return geospans def _export_static_lighting(self, bo): + bpy.context.scene.objects.active = bo if bo.plasma_modifiers.lightmap.enabled: print(" Baking lightmap...") print("====") - bpy.context.scene.objects.active = bo bpy.ops.object.plasma_lightmap_autobake() print("====") else: diff --git a/korman/exporter/physics.py b/korman/exporter/physics.py index 102a26c..be5faa0 100644 --- a/korman/exporter/physics.py +++ b/korman/exporter/physics.py @@ -63,7 +63,6 @@ class PhysicsConverter: simIface = self._mgr.add_object(pl=plSimulationInterface, bl=bo) physical = self._mgr.add_object(pl=plGenericPhysical, bl=bo, name=name) - so.sim = simIface.key simIface.physical = physical.key physical.object = so.key physical.sceneNode = self._mgr.get_scene_node(bl=bo) diff --git a/korman/exporter/rtlight.py b/korman/exporter/rtlight.py new file mode 100644 index 0000000..0fc8bbf --- /dev/null +++ b/korman/exporter/rtlight.py @@ -0,0 +1,200 @@ +# 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 . + +from PyHSPlasma import * +import weakref + +from .explosions import * +from . import utils + +_BL2PL = { + "POINT": plOmniLightInfo, + "SPOT": plSpotLightInfo, + "SUN": plDirectionalLightInfo, +} + +class LightConverter: + def __init__(self, exporter): + self._exporter = weakref.ref(exporter) + self._converter_funcs = { + "POINT": self._convert_point_lamp, + "SPOT": self._convert_spot_lamp, + "SUN": self._convert_sun_lamp, + } + + def _convert_point_lamp(self, bl, pl): + print(" [OmniLightInfo '{}']".format(bl.name)) + self._convert_shared_pointspot(bl, pl) + + def _convert_spot_lamp(self, bl, pl): + print(" [SpotLightInfo '{}']".format(bl.name)) + self._convert_shared_pointspot(bl, pl) + + # Spot lights have a few more things... + spot_size = bl.spot_size + pl.spotOuter = spot_size + + blend = max(0.001, bl.spot_blend) + pl.spotInner = spot_size - (blend*spot_size) + + if bl.use_halo: + pl.falloff = bl.halo_intensity + else: + pl.falloff = 1.0 + + def _convert_shared_pointspot(self, bl, pl): + # So sue me, this was taken from pyprp2... + dist = bl.distance + if lamp.falloff_type == "LINEAR_QUADRATIC_WEIGHTED": + print(" Attenuation: Linear Quadratic Weighted") + pl.attenQuadratic = lamp.quadratic_attenuation / dist + pl.attenLinear = lamp.linear_attenuation / dist + pl.attenConst = 1.0 + elif lamp.falloff_type == "CONSTANT": + print(" Attenuation: Konstant") + pl.attenQuadratic = 0.0 + pl.attenLinear = 0.0 + pl.attenConst = 1.0 + elif lamp.falloff_type == "INVERSE_SQUARE": + print(" Attenuation: Inverse Square") + pl.attenQuadratic = lamp.quadratic_attenuation / dist + pl.attenLinear = 0.0 + pl.attenConst = 1.0 + elif lamp.falloff_type == "INVERSE_LINEAR": + print(" Attenuation: Inverse Linear") + pl.attenQuadratic = 0.0 + pl.attenLinear = lamp.quadratic_attenuation / dist + pl.attenConst = 1.0 + else: + raise BlenderOptionNotSupportedError(lamp.falloff_type) + + if bl.use_sphere: + print(" Sphere Cutoff: {}".format(dist)) + pl.attenCutoff = dist + else: + pl.attenCutoff = dist * 2 + + def _convert_sun_lamp(self, bl, pl): + print(" [DirectionalLightInfo '{}']".format(bl.name)) + + def _create_light_key(self, bo, bl_light, so): + try: + xlate = _BL2PL[bl_light.type] + return self.mgr.find_create_key(bo, xlate, so=so) + except LookupError: + raise BlenderOptionNotSupported("Object ('{}') lamp type '{}'".format(bo.name, bl_light.type)) + + def export_rtlight(self, so, bo): + bl_light = bo.data + + # The specifics be here... + pl_light = self._create_light_key(bo, bl_light, so).object + self._converter_funcs[bl_light.type](bl_light, pl_light) + + # Light color nonsense + energy = bl_light.energy * 2 + if bl_light.use_negative: + color = [(0.0 - i) * energy for i in bl_light.color] + else: + color = [i * energy for i in bl_light.color] + color_str = "({:.4f}, {:.4f}, {:.4f})".format(color[0], color[1], color[2]) + color.append(1.0) + + # Apply the colors + if bl_light.use_diffuse: + print(" Diffuse: {}".format(color_str)) + pl_light.diffuse = hsColorRGBA(*color) + else: + print(" Diffuse: OFF") + pl_light.diffuse = hsColorRGBA(0.0, 0.0, 0.0, 1.0) + if bl_light.use_specular: + print(" Specular: {}".format(color_str)) + pl_light.setProperty(plLightInfo.kLPHasSpecular, True) + pl_light.specular = hsColorRGBA(*color) + else: + print(" Specular: OFF") + pl_light.specular = hsColorRGBA(0.0, 0.0, 0.0, 1.0) + + # AFAICT ambient lighting is never set in PlasmaMax... + # If you can think of a compelling reason to support it, be my guest. + pl_light.ambient = hsColorRGBA(0.0, 0.0, 0.0, 1.0) + + # Now, let's apply the matrices... + # Science indicates that Plasma RT Lights should *always* have mats, even if there is a CI + l2w = utils.matrix44(bo.matrix_local) + pl_light.lightToWorld = l2w + pl_light.worldToLight = l2w.inverse() + + # *Sigh* + pl_light.sceneNode = self.mgr.get_scene_node(location=so.key.location) + + def find_material_light_keys(self, bo, bm): + """Given a blender material, we find the keys of all matching Plasma RT Lights. + NOTE: We return a tuple of lists: ([permaLights], [permaProjs])""" + print(" Searching for runtime lights...") + permaLights = [] + permaProjs = [] + + # We're going to inspect the material's light group. + # If there is no light group, we'll say that there is no runtime lighting... + # If there is, we will harvest all Blender lamps in that light group that are Plasma Objects + lg = bm.light_group + if lg is not None: + for obj in lg.objects: + if obj.type != "LAMP": + # moronic... + continue + elif not obj.plasma_object.enabled: + # who cares? + continue + lamp = obj.data + + # Check to see if they only want this light to work on its layer... + if lamp.use_own_layer: + # Pairs up elements from both layers sequences such that we can compare + # to see if the lamp and object are in the same layer. + # If you can think of a better way, be my guest. + test = zip(bo.layers, obj.layers) + for i in test: + if i == (True, True): + break + else: + # didn't find a layer where both lamp and object were, skip it. + print(" [{}] '{}': not in same layer, skipping...".format(lamp.type, obj.name)) + continue + + # This is probably where PermaLight vs PermaProj should be sorted out... + pl_light = self._create_light_key(bo, lamp, None) + if self._is_projection_lamp(lamp): + print(" [{}] PermaProj '{}'".format(lamp.type, obj.name)) + permaProj.append(pl_light) + # TODO: run this through the material exporter... + # need to do some work to make the texture slot code not assume it's working with a material + else: + print(" [{}] PermaLight '{}'".format(lamp.type, obj.name)) + permaLights.append(pl_light) + + return (permaLights, permaProjs) + + def _is_projection_lamp(self, bl_light): + for tex in bl_light.texture_slots: + if tex is None or tex.texture is None: + continue + return True + return False + + @property + def mgr(self): + return self._exporter().mgr diff --git a/korman/operators/op_lightmap.py b/korman/operators/op_lightmap.py index 8047c59..7a5ae3a 100644 --- a/korman/operators/op_lightmap.py +++ b/korman/operators/op_lightmap.py @@ -16,24 +16,67 @@ import bpy from ..helpers import GoodNeighbor +def _fetch_lamp_objects(): + for obj in bpy.data.objects: + if obj.type == "LAMP": + yield obj + class _LightingOperator: + def __init__(self): + self._old_lightgroups = {} + @classmethod def poll(cls, context): if context.object is not None: return context.scene.render.engine == "PLASMA_GAME" + def _generate_lightgroups(self, mesh): + """Makes a new light group for the baking process that excludes all Plasma RT lamps""" + shouldibake = False + + for material in mesh.materials: + lg = material.light_group + self._old_lightgroups[material] = lg + + # TODO: faux-lightgroup caching for the entire export process. you dig? + if lg is None or len(lg.objects) == 0: + source = _fetch_lamp_objects() + else: + source = lg.objects + dest = bpy.data.groups.new("_LIGHTMAPGEN_{}".format(material.name)) + + for obj in source: + if obj.plasma_object.enabled: + continue + dest.objects.link(obj) + shouldibake = True + material.light_group = dest + return shouldibake + def _hide_textures(self, mesh, toggle): for mat in mesh.materials: for tex in mat.texture_slots: if tex is not None and tex.use: toggle.track(tex, "use", False) + def _pop_lightgroups(self): + for material, lg in self._old_lightgroups.items(): + _fake = material.light_group + if _fake is not None: + _fake.user_clear() + bpy.data.groups.remove(_fake) + material.light_group = lg + self._old_lightgroups.clear() + class LightmapAutobakeOperator(_LightingOperator, bpy.types.Operator): bl_idname = "object.plasma_lightmap_autobake" bl_label = "Bake Lightmap" bl_options = {"INTERNAL"} + def __init__(self): + super().__init__() + def execute(self, context): with GoodNeighbor() as toggle: # We need to ensure that we bake onto the "BlahObject_LIGHTMAPGEN" image @@ -74,6 +117,8 @@ class LightmapAutobakeOperator(_LightingOperator, bpy.types.Operator): bpy.ops.object.mode_set(mode="OBJECT") # Associate the image with all the new UVs + # NOTE: no toggle here because it's the artist's problem if they are looking at our + # super swagalicious LIGHTMAPGEN uvtexture... for i in mesh.uv_textures.active.data: i.image = im @@ -87,8 +132,9 @@ class LightmapAutobakeOperator(_LightingOperator, bpy.types.Operator): self._hide_textures(obj.data, toggle) # Now, we *finally* bake the lightmap... - # FIXME: Don't bake Plasma RT lights - bpy.ops.object.bake_image() + if self._generate_lightgroups(mesh): + bpy.ops.object.bake_image() + self._pop_lightgroups() # Done! return {"FINISHED"} @@ -99,6 +145,9 @@ class LightmapAutobakePreviewOperator(_LightingOperator, bpy.types.Operator): bl_label = "Preview Lightmap" bl_options = {"INTERNAL"} + def __init__(self): + super().__init__() + def execute(self, context): bpy.ops.object.plasma_lightmap_autobake() @@ -116,6 +165,9 @@ class VertexColorLightingOperator(_LightingOperator, bpy.types.Operator): bl_label = "Bake Vertex Color Lighting" bl_options = {"INTERNAL"} + def __init__(self): + super().__init__() + def execute(self, context): with GoodNeighbor() as toggle: mesh = context.active_object.data @@ -129,7 +181,6 @@ class VertexColorLightingOperator(_LightingOperator, bpy.types.Operator): # Prepare to bake... self._hide_textures(mesh, toggle) - # TODO: don't bake runtime lights # Bake settings render = context.scene.render @@ -137,7 +188,9 @@ class VertexColorLightingOperator(_LightingOperator, bpy.types.Operator): toggle.track(render, "use_bake_to_vertex_color", True) # Bake - bpy.ops.object.bake_image() + if self._generate_lightgroups(mesh): + bpy.ops.object.bake_image() + self._pop_lightgroups() # And done! return {"FINISHED"} diff --git a/korman/render.py b/korman/render.py index f52835a..0e6f536 100644 --- a/korman/render.py +++ b/korman/render.py @@ -42,6 +42,10 @@ def _whitelist_all(mod): if hasattr(attr, "COMPAT_ENGINES"): getattr(attr, "COMPAT_ENGINES").add("PLASMA_GAME") +from bl_ui import properties_data_lamp +_whitelist_all(properties_data_lamp) +del properties_data_lamp + from bl_ui import properties_render _whitelist_all(properties_render) del properties_render