mirror of
https://github.com/H-uru/korman.git
synced 2025-07-14 22:36:52 +00:00
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.
This commit is contained in:
@ -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")
|
||||
|
@ -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))
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
200
korman/exporter/rtlight.py
Normal file
200
korman/exporter/rtlight.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
@ -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"}
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user