From fdfd939640f40a6e0cc82daff4cf86e4b0173a10 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 6 Jun 2015 23:32:12 -0400 Subject: [PATCH] Export lamps as RT lights :D This includes changes to the light baking code to ensure that we don't bake runtime lights. This code has several places it could be optimized in the future when we have larger ages to test against. --- korman/exporter/convert.py | 13 ++- korman/exporter/explosions.py | 5 + korman/exporter/manager.py | 35 +++++- korman/exporter/mesh.py | 11 +- korman/exporter/physics.py | 1 - korman/exporter/rtlight.py | 200 ++++++++++++++++++++++++++++++++ korman/operators/op_lightmap.py | 61 +++++++++- korman/render.py | 4 + 8 files changed, 310 insertions(+), 20 deletions(-) create mode 100644 korman/exporter/rtlight.py 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