You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

327 lines
13 KiB

# 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")
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")
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")
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)
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)
with self._report.indent():
self._convert_attenuation(bl, pl)
def _convert_spot_lamp(self, bl, pl):
self._report.msg("[SpotLightInfo '{}']", bl.name)
with self._report.indent():
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)
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(f"Diffuse: {diff_str}")
pl_light.diffuse = hsColorRGBA(*diff_color)
else:
self._report.msg("Diffuse: OFF")
pl_light.diffuse = hsColorRGBA(0.0, 0.0, 0.0, energy)
if bl_light.use_specular:
self._report.msg(f"Specular: {spec_str}")
pl_light.setProperty(plLightInfo.kLPHasSpecular, True)
pl_light.specular = hsColorRGBA(*spec_color)
else:
self._report.msg(f"Specular: OFF")
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(f"Only one texture slot can be exported per Lamp. Picking the first one: '{slot.name}'")
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...")
permaLights = []
permaProjs = []
with self._report.indent():
# 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(f"[{lamp.type}] '{obj.name}': not in same layer, skipping...")
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(f"[{lamp.type}] PermaProj '{obj.name}'")
permaProjs.append(pl_light)
else:
self._report.msg(f"[{lamp.type}] PermaLight '{obj.name}'", lamp.type, obj.name)
permaLights.append(pl_light)
if len(permaLights) > 8:
self._report.warn(f"More than 8 RT lamps on material: '{bm.name}'")
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