|
|
|
# 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 PyHSPlasma import *
|
|
|
|
import weakref
|
|
|
|
|
|
|
|
from .explosions import *
|
|
|
|
from . import utils
|
|
|
|
|
|
|
|
_BL2PL = {
|
|
|
|
"AREA": plLimitedDirLightInfo,
|
|
|
|
"POINT": plOmniLightInfo,
|
|
|
|
"SPOT": plSpotLightInfo,
|
|
|
|
"SUN": plDirectionalLightInfo,
|
|
|
|
}
|
|
|
|
|
|
|
|
_FAR_POWER = 15.0
|
|
|
|
|
|
|
|
SHADOW_RESOLUTION = {
|
|
|
|
"ABYSMAL": 64,
|
|
|
|
"LOW": 128,
|
|
|
|
"NORMAL": 256,
|
|
|
|
"HIGH": 512,
|
|
|
|
}
|
|
|
|
|
|
|
|
class LightConverter:
|
|
|
|
def __init__(self, exporter):
|
|
|
|
self._exporter = weakref.ref(exporter)
|
|
|
|
self._converter_funcs = {
|
|
|
|
"AREA": self._convert_area_lamp,
|
|
|
|
"POINT": self._convert_point_lamp,
|
|
|
|
"SPOT": self._convert_spot_lamp,
|
|
|
|
"SUN": self._convert_sun_lamp,
|
|
|
|
}
|
|
|
|
|
|
|
|
def _convert_attenuation(self, bl, pl):
|
|
|
|
# If you change these calculations, be sure to update the AnimationConverter!
|
|
|
|
intens, attenEnd = self.convert_attenuation(bl)
|
|
|
|
if bl.falloff_type == "CONSTANT":
|
|
|
|
self._report.msg("Attenuation: No Falloff", indent=2)
|
|
|
|
pl.attenConst = intens
|
|
|
|
pl.attenLinear = 0.0
|
|
|
|
pl.attenQuadratic = 0.0
|
|
|
|
pl.attenCutoff = attenEnd
|
|
|
|
elif bl.falloff_type == "INVERSE_LINEAR":
|
|
|
|
self._report.msg("Attenuation: Inverse Linear", indent=2)
|
|
|
|
pl.attenConst = 1.0
|
|
|
|
pl.attenLinear = self.convert_attenuation_linear(intens, attenEnd)
|
|
|
|
pl.attenQuadratic = 0.0
|
|
|
|
pl.attenCutoff = attenEnd
|
|
|
|
elif bl.falloff_type == "INVERSE_SQUARE":
|
|
|
|
self._report.msg("Attenuation: Inverse Square", indent=2)
|
|
|
|
pl.attenConst = 1.0
|
|
|
|
pl.attenLinear = 0.0
|
|
|
|
pl.attenQuadratic = self.convert_attenuation_quadratic(intens, attenEnd)
|
|
|
|
pl.attenCutoff = attenEnd
|
|
|
|
else:
|
|
|
|
raise BlenderOptionNotSupportedError(bl.falloff_type)
|
|
|
|
|
|
|
|
def convert_attenuation(self, lamp):
|
|
|
|
intens = abs(lamp.energy)
|
|
|
|
attenEnd = lamp.distance if lamp.use_sphere else lamp.distance * 2
|
|
|
|
return (intens, attenEnd)
|
|
|
|
|
|
|
|
def convert_attenuation_linear(self, intensity, end):
|
|
|
|
return max(0.0, (intensity * _FAR_POWER - 1.0) / end)
|
|
|
|
|
|
|
|
def convert_attenuation_quadratic(self, intensity, end):
|
|
|
|
return max(0.0, (intensity * _FAR_POWER - 1.0) / pow(end, 2))
|
|
|
|
|
|
|
|
def _convert_area_lamp(self, bl, pl):
|
|
|
|
self._report.msg("[LimitedDirLightInfo '{}']", bl.name, indent=1)
|
|
|
|
|
|
|
|
pl.width = bl.size
|
|
|
|
pl.depth = bl.size if bl.shape == "SQUARE" else bl.size_y
|
|
|
|
pl.height = bl.plasma_lamp.size_height
|
|
|
|
|
|
|
|
def _convert_point_lamp(self, bl, pl):
|
|
|
|
self._report.msg("[OmniLightInfo '{}']", bl.name, indent=1)
|
|
|
|
self._convert_attenuation(bl, pl)
|
|
|
|
|
|
|
|
def _convert_spot_lamp(self, bl, pl):
|
|
|
|
self._report.msg("[SpotLightInfo '{}']", bl.name, indent=1)
|
|
|
|
self._convert_attenuation(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_sun_lamp(self, bl, pl):
|
|
|
|
self._report.msg("[DirectionalLightInfo '{}']", bl.name, indent=1)
|
|
|
|
|
|
|
|
def export_rtlight(self, so, bo):
|
|
|
|
bl_light = bo.data
|
|
|
|
|
|
|
|
# The specifics be here...
|
|
|
|
pl_light = self.get_light_key(bo, bl_light, so).object
|
|
|
|
self._converter_funcs[bl_light.type](bl_light, pl_light)
|
|
|
|
|
|
|
|
# Light color nonsense
|
|
|
|
# Please note that these calculations are duplicated in the AnimationConverter
|
|
|
|
energy = bl_light.energy
|
|
|
|
if bl_light.use_negative:
|
|
|
|
diff_color = [(0.0 - i) * energy for i in bl_light.color]
|
|
|
|
spec_color = [(0.0 - i) for i in bl_light.color]
|
|
|
|
else:
|
|
|
|
diff_color = [i * energy for i in bl_light.color]
|
|
|
|
spec_color = [i for i in bl_light.color]
|
|
|
|
|
|
|
|
diff_str = "({:.4f}, {:.4f}, {:.4f})".format(*diff_color)
|
|
|
|
diff_color.append(energy)
|
|
|
|
|
|
|
|
spec_str = "({:.4f}, {:.4f}, {:.4f})".format(*spec_color)
|
|
|
|
spec_color.append(energy)
|
|
|
|
|
|
|
|
# Do we *only* want a shadow?
|
|
|
|
shadow_only = bl_light.shadow_method != "NOSHADOW" and bl_light.use_only_shadow
|
|
|
|
|
|
|
|
# Apply the colors
|
|
|
|
if bl_light.use_diffuse:
|
|
|
|
self._report.msg("Diffuse: {}", diff_str, indent=2)
|
|
|
|
pl_light.diffuse = hsColorRGBA(*diff_color)
|
|
|
|
else:
|
|
|
|
self._report.msg("Diffuse: OFF", indent=2)
|
|
|
|
pl_light.diffuse = hsColorRGBA(0.0, 0.0, 0.0, energy)
|
|
|
|
|
|
|
|
if bl_light.use_specular:
|
|
|
|
self._report.msg("Specular: {}", spec_str, indent=2)
|
|
|
|
pl_light.setProperty(plLightInfo.kLPHasSpecular, True)
|
|
|
|
pl_light.specular = hsColorRGBA(*spec_color)
|
|
|
|
else:
|
|
|
|
self._report.msg("Specular: OFF", indent=2)
|
|
|
|
pl_light.specular = hsColorRGBA(0.0, 0.0, 0.0, energy)
|
|
|
|
|
|
|
|
rtlamp = bl_light.plasma_lamp
|
|
|
|
has_lg = rtlamp.has_light_group(bo)
|
|
|
|
if has_lg:
|
|
|
|
pl_light.setProperty(plLightInfo.kLPHasIncludes, True)
|
|
|
|
pl_light.setProperty(plLightInfo.kLPIncludesChars, rtlamp.affect_characters)
|
|
|
|
if rtlamp.cast_shadows:
|
|
|
|
self._export_shadow_master(bo, rtlamp, pl_light)
|
|
|
|
pl_light.setProperty(plLightInfo.kLPShadowOnly, shadow_only)
|
|
|
|
if self.mgr.getVer() != pvPrime:
|
|
|
|
pl_light.setProperty(plLightInfo.kLPShadowLightGroup, has_lg)
|
|
|
|
|
|
|
|
# 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()
|
|
|
|
|
|
|
|
# Soft Volume science
|
|
|
|
sv_mod, sv_key = bo.plasma_modifiers.softvolume, None
|
|
|
|
if sv_mod.enabled:
|
|
|
|
sv_key = sv_mod.get_key(self._exporter())
|
|
|
|
elif rtlamp.lamp_region:
|
|
|
|
sv_bo = rtlamp.lamp_region
|
|
|
|
sv_mod = sv_bo.plasma_modifiers.softvolume
|
|
|
|
if not sv_mod.enabled:
|
|
|
|
raise ExportError("'{}': '{}' is not a SoftVolume".format(bo.name, sv_bo.name))
|
|
|
|
sv_key = sv_mod.get_key(self._exporter())
|
|
|
|
pl_light.softVolume = sv_key
|
|
|
|
|
|
|
|
# Is this a projector?
|
|
|
|
projectors = tuple(self.get_projectors(bl_light))
|
|
|
|
if projectors:
|
|
|
|
self._export_rt_projector(bo, pl_light, projectors)
|
|
|
|
|
|
|
|
# If the lamp has any sort of animation attached, then it needs to be marked movable.
|
|
|
|
# Otherwise, Plasma may not use it for lighting.
|
|
|
|
if bo.plasma_object.has_animation_data and not has_lg:
|
|
|
|
pl_light.setProperty(plLightInfo.kLPMovable, True)
|
|
|
|
|
|
|
|
# *Sigh*
|
|
|
|
pl_light.sceneNode = self.mgr.get_scene_node(location=so.key.location)
|
|
|
|
|
|
|
|
def _export_rt_projector(self, bo, pl_light, tex_slots):
|
|
|
|
mat = self._exporter().mesh.material
|
|
|
|
slot = tex_slots[0]
|
|
|
|
|
|
|
|
# There is a Material available in the caller, but that is for the parent Mesh. We are a
|
|
|
|
# projection Lamp with our own faux Material. Unfortunately, Plasma only supports projecting
|
|
|
|
# one layer. We could exploit the fUnderLay and fOverLay system to export everything, but meh.
|
|
|
|
if len(tex_slots) > 1:
|
|
|
|
self._report.warn("Only one texture slot can be exported per Lamp. Picking the first one: '{}'".format(slot.name), indent=3)
|
|
|
|
layer = mat.export_texture_slot(bo, None, None, slot, 0, blend_flags=False)
|
|
|
|
state = layer.state
|
|
|
|
|
|
|
|
# Colors science'd from PRPs
|
|
|
|
layer.preshade = hsColorRGBA(0.5, 0.5, 0.5)
|
|
|
|
layer.runtime = hsColorRGBA(0.5, 0.5, 0.5)
|
|
|
|
|
|
|
|
# Props for projectors...
|
|
|
|
# Note that we tell the material exporter to (try not to) do any blend flags for us
|
|
|
|
layer.UVWSrc |= plLayer.kUVWPosition
|
|
|
|
if bo.data.type == "SPOT":
|
|
|
|
state.miscFlags |= hsGMatState.kMiscPerspProjection
|
|
|
|
else:
|
|
|
|
state.miscFlags |= hsGMatState.kMiscOrthoProjection
|
|
|
|
state.ZFlags |= hsGMatState.kZNoZWrite
|
|
|
|
pl_light.setProperty(plLightInfo.kLPCastShadows, False)
|
|
|
|
|
|
|
|
if slot.blend_type == "ADD":
|
|
|
|
state.blendFlags |= hsGMatState.kBlendAdd
|
|
|
|
pl_light.setProperty(plLightInfo.kLPOverAll, True)
|
|
|
|
elif slot.blend_type == "MULTIPLY":
|
|
|
|
# From PlasmaMAX
|
|
|
|
state.blendFlags |= hsGMatState.kBlendMult | hsGMatState.kBlendInvertColor | hsGMatState.kBlendInvertFinalColor
|
|
|
|
pl_light.setProperty(plLightInfo.kLPOverAll, True)
|
|
|
|
|
|
|
|
pl_light.projection = layer.key
|
|
|
|
|
|
|
|
def _export_shadow_master(self, bo, rtlamp, pl_light):
|
|
|
|
pClass = plDirectShadowMaster if isinstance(pl_light, plDirectionalLightInfo) else plPointShadowMaster
|
|
|
|
shadow = self.mgr.find_create_object(pClass, bl=bo)
|
|
|
|
|
|
|
|
shadow.attenDist = rtlamp.shadow_falloff
|
|
|
|
shadow.maxDist = rtlamp.shadow_distance
|
|
|
|
shadow.minDist = rtlamp.shadow_distance * 0.75
|
|
|
|
shadow.maxSize = SHADOW_RESOLUTION[rtlamp.shadow_quality]
|
|
|
|
# NOTE: minSize appears to be unused.
|
|
|
|
shadow.power = rtlamp.shadow_power / 100.0
|
|
|
|
shadow.setProperty(plShadowMaster.kSelfShadow, rtlamp.shadow_self)
|
|
|
|
|
|
|
|
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])"""
|
|
|
|
self._report.msg("Searching for runtime lights...", indent=1)
|
|
|
|
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.
|
|
|
|
self._report.msg("[{}] '{}': not in same layer, skipping...",
|
|
|
|
lamp.type, obj.name, indent=2)
|
|
|
|
continue
|
|
|
|
|
|
|
|
# This is probably where PermaLight vs PermaProj should be sorted out...
|
|
|
|
pl_light = self.get_light_key(obj, lamp, None)
|
|
|
|
if self._is_projection_lamp(lamp):
|
|
|
|
self._report.msg("[{}] PermaProj '{}'", lamp.type, obj.name, indent=2)
|
|
|
|
permaProjs.append(pl_light)
|
|
|
|
else:
|
|
|
|
self._report.msg("[{}] PermaLight '{}'", lamp.type, obj.name, indent=2)
|
|
|
|
permaLights.append(pl_light)
|
|
|
|
|
|
|
|
if len(permaLights) > 8:
|
|
|
|
self._report.warn("More than 8 RT lamps on material: '{}'", bm.name, indent=1)
|
|
|
|
|
|
|
|
return (permaLights, permaProjs)
|
|
|
|
|
|
|
|
def get_light_key(self, bo, bl_light, so):
|
|
|
|
try:
|
|
|
|
xlate = _BL2PL[bl_light.type]
|
|
|
|
return self.mgr.find_create_key(xlate, bl=bo, so=so)
|
|
|
|
except LookupError:
|
|
|
|
raise BlenderOptionNotSupportedError("Object ('{}') lamp type '{}'".format(bo.name, bl_light.type))
|
|
|
|
|
|
|
|
def get_projectors(self, bl_light):
|
|
|
|
for tex in bl_light.texture_slots:
|
|
|
|
if tex is not None and tex.texture is not None:
|
|
|
|
yield tex
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
@property
|
|
|
|
def _report(self):
|
|
|
|
return self._exporter().report
|