Browse Source

Merge pull request #261 from Hoikas/lighting_noregen

Persistent Baked Lighting
pull/268/head
Adam Johnson 3 years ago committed by GitHub
parent
commit
1a60a47ef5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      korman/exporter/convert.py
  2. 140
      korman/exporter/etlight.py
  3. 133
      korman/operators/op_lightmap.py
  4. 19
      korman/properties/modifiers/render.py
  5. 23
      korman/ui/modifiers/render.py
  6. 8
      korman/ui/ui_toolbox.py

16
korman/exporter/convert.py

@ -14,10 +14,13 @@
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy
from ..korlib import ConsoleToggler
from pathlib import Path
from contextlib import ExitStack
from ..korlib import ConsoleToggler
from PyHSPlasma import *
import time
from . import animation
from . import camera
@ -44,7 +47,7 @@ class Exporter:
def run(self):
log = logger.ExportVerboseLogger if self._op.verbose else logger.ExportProgressLogger
with ConsoleToggler(self._op.show_console), log(self._op.filepath) as self.report:
with ConsoleToggler(self._op.show_console), log(self._op.filepath) as self.report, ExitStack() as self.context_stack:
# Step 0: Init export resmgr and stuff
self.mgr = manager.ExportManager(self)
self.mesh = mesh.MeshConverter(self)
@ -56,6 +59,7 @@ class Exporter:
self.image = image.ImageCache(self)
self.locman = locman.LocalizationConverter(self)
self.decal = decal.DecalConverter(self)
self.oven = etlight.LightBaker(self.report, stack=self.context_stack)
# Step 0.8: Init the progress mgr
self.mesh.add_progress_presteps(self.report)
@ -126,10 +130,8 @@ class Exporter:
self.report.raise_errors()
def _bake_static_lighting(self):
lighting_method = self._op.lighting_method
if lighting_method != "skip":
oven = etlight.LightBaker(self.report)
oven.bake_static_lighting(self._objects)
if self._op.lighting_method != "skip":
self.oven.bake_static_lighting(self._objects)
def _collect_objects(self):
scene = bpy.context.scene

140
korman/exporter/etlight.py

@ -14,10 +14,12 @@
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy
from bpy.app.handlers import persistent
from contextlib import ExitStack
import itertools
from .explosions import *
from .logger import ExportProgressLogger
from .logger import ExportProgressLogger, ExportVerboseLogger
from .mesh import _MeshManager, _VERTEX_COLOR_LAYERS
from ..helpers import *
@ -26,17 +28,25 @@ _NUM_RENDER_LAYERS = 20
class LightBaker(_MeshManager):
"""ExportTime Lighting"""
def __init__(self, report=None):
def __init__(self, report=None, stack=None, *, verbose=False):
self._lightgroups = {}
if report is None:
self._report = ExportProgressLogger()
self._report = ExportVerboseLogger() if verbose else ExportProgressLogger()
self.add_progress_steps(self._report, True)
self._report.progress_start("PREVIEWING LIGHTING")
self._report.progress_start("BAKING LIGHTING")
self._own_report = True
else:
self._report = report
self._own_report = False
super().__init__(self._report)
self._context_stack = stack
self.vcol_layer_name = "autocolor"
self.lightmap_name = "{}_LIGHTMAPGEN.png"
self.lightmap_uvtex_name = "LIGHTMAPGEN"
self.retain_lightmap_uvtex = True
self.force = False
self._lightmap_images = {}
self._uvtexs = {}
def __del__(self):
@ -77,6 +87,7 @@ class LightBaker(_MeshManager):
self._apply_render_settings(toggle, False)
self._select_only(objs, toggle)
bpy.ops.object.bake_image()
self._pack_lightmaps(objs)
def _bake_vcols(self, objs, layers):
with GoodNeighbor() as toggle:
@ -99,6 +110,8 @@ class LightBaker(_MeshManager):
# this stuff has been observed to be problematic with GoodNeighbor
self._pop_lightgroups()
self._restore_uvtexs()
if not self.retain_lightmap_uvtex:
self._remove_stale_uvtexes(bake)
return result
def _bake_static_lighting(self, bake, toggle):
@ -203,11 +216,17 @@ class LightBaker(_MeshManager):
material.light_group = dest
return shouldibake
def get_lightmap(self, bo):
return self._lightmap_images.get(bo.name)
def get_lightmap_name(self, bo):
return self.lightmap_name.format(bo.name)
def _get_lightmap_uvtex(self, mesh, modifier):
if modifier.uv_map:
return mesh.uv_textures[modifier.uv_map]
for i in mesh.uv_textures:
if i.name != "LIGHTMAPGEN":
if i.name not in {"LIGHTMAPGEN", self.lightmap_uvtex_name}:
return i
return None
@ -224,12 +243,37 @@ class LightBaker(_MeshManager):
bake, bake_passes = {}, bpy.context.scene.plasma_scene.bake_passes
bake_vcol = bake.setdefault(("vcol",) + default_layers, [])
for i in objs:
if i.type != "MESH":
continue
if bool(i.data.materials) is False:
continue
def lightmap_bake_required(obj) -> bool:
mod = obj.plasma_modifiers.lightmap
if mod.bake_lightmap:
if self.force:
return True
if mod.image is not None:
uv_texture_names = frozenset((i.name for i in obj.data.uv_textures))
if self.lightmap_uvtex_name in uv_texture_names:
self._report.msg("'{}': Skipping due to valid lightmap override", obj.name, indent=1)
else:
self._report.msg("'{}': Have lightmap but UVs are missing???", obj.name, indent=1)
return False
return True
return False
def vcol_bake_required(obj) -> bool:
if obj.plasma_modifiers.lightmap.bake_lightmap:
return False
vcol_layer_names = frozenset((vcol_layer.name.lower() for vcol_layer in obj.data.vertex_colors))
manual_layer_names = _VERTEX_COLOR_LAYERS & vcol_layer_names
if manual_layer_names:
self._report.msg("'{}': Skipping due to valid manual vertex color layer(s): '{}'", obj.name, manual_layer_names.pop(), indent=1)
return False
if self.force:
return True
if self.vcol_layer_name.lower() in vcol_layer_names:
self._report.msg("'{}': Skipping due to valid matching vertex color layer(s): '{}'", obj.name, self.vcol_layer_name, indent=1)
return False
return True
for i in filter(lambda x: x.type == "MESH" and bool(x.data.materials), objs):
mods = i.plasma_modifiers
lightmap_mod = mods.lightmap
if lightmap_mod.enabled:
@ -249,19 +293,25 @@ class LightBaker(_MeshManager):
if not lm_active_layers & obj_active_layers:
raise ExportError("Bake Lighting '{}': At least one layer the object is on must be selected".format(i.name))
if lightmap_bake_required(i) is False and vcol_bake_required(i) is False:
continue
method = "lightmap" if lightmap_mod.bake_lightmap else "vcol"
key = (method,) + lm_layers
bake_pass = bake.setdefault(key, [])
bake_pass.append(i)
elif mods.lighting.preshade:
vcols = i.data.vertex_colors
for j in _VERTEX_COLOR_LAYERS:
if j in vcols:
break
else:
bake_vcol.append(i)
self._report.msg("'{}': Bake to {}", i.name, method, indent=1)
elif mods.lighting.preshade and vcol_bake_required(i):
self._report.msg("'{}': Bake to vcol (crappy)", i.name, indent=1)
bake_vcol.append(i)
return bake
def _pack_lightmaps(self, objs):
for bo in objs:
im = self.get_lightmap(bo)
if im is not None and im.is_dirty:
im.pack(as_png=True)
def _pop_lightgroups(self):
materials = bpy.data.materials
for mat_name, lg in self._lightgroups.items():
@ -296,7 +346,7 @@ class LightBaker(_MeshManager):
# We need to ensure that we bake onto the "BlahObject_LIGHTMAPGEN" image
data_images = bpy.data.images
im_name = "{}_LIGHTMAPGEN.png".format(bo.name)
im_name = self.get_lightmap_name(bo)
size = modifier.resolution
im = data_images.get(im_name)
@ -306,9 +356,10 @@ class LightBaker(_MeshManager):
# Force delete and recreate the image because the size is out of date
data_images.remove(im)
im = data_images.new(im_name, width=size, height=size)
self._lightmap_images[bo.name] = im
# If there is a cached LIGHTMAPGEN uvtexture, nuke it
uvtex = uv_textures.get("LIGHTMAPGEN", None)
uvtex = uv_textures.get(self.lightmap_uvtex_name, None)
if uvtex is not None:
uv_textures.remove(uvtex)
@ -330,7 +381,7 @@ class LightBaker(_MeshManager):
uv_textures.active = uv_base
# this will copy the UVs to the new UV texture
uvtex = uv_textures.new("LIGHTMAPGEN")
uvtex = uv_textures.new(self.lightmap_uvtex_name)
uv_textures.active = uvtex
# if the artist hid any UVs, they will not be baked to... fix this now
@ -347,7 +398,7 @@ class LightBaker(_MeshManager):
else:
# same thread, see Sirius's suggestion RE smart unwrap. this seems to yield good
# results in my tests. it will be good enough for quick exports.
uvtex = uv_textures.new("LIGHTMAPGEN")
uvtex = uv_textures.new(self.lightmap_uvtex_name)
self._associate_image_with_uvtex(uvtex, im)
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_all(action="SELECT")
@ -358,7 +409,7 @@ class LightBaker(_MeshManager):
# NOTE that this will need to be reset by us to what the user had previously
# Not using toggle.track due to observed oddities
for i in uv_textures:
value = i.name == "LIGHTMAPGEN"
value = i.name == self.lightmap_uvtex_name
i.active = value
i.active_render = value
@ -375,21 +426,39 @@ class LightBaker(_MeshManager):
if not self._generate_lightgroup(bo, user_lg):
return False
autocolor = vcols.get("autocolor")
if autocolor is None:
autocolor = vcols.new("autocolor")
vcol_layer_name = self.vcol_layer_name
autocolor = vcols.get(vcol_layer_name)
needs_vcol_layer = autocolor is None
if needs_vcol_layer:
autocolor = vcols.new(vcol_layer_name)
toggle.track(vcols, "active", autocolor)
# Mark "autocolor" as our active render layer
for vcol_layer in mesh.vertex_colors:
autocol = vcol_layer.name == "autocolor"
autocol = vcol_layer.name == vcol_layer_name
toggle.track(vcol_layer, "active_render", autocol)
toggle.track(vcol_layer, "active", autocol)
mesh.update()
# Vertex colors are sort of ephemeral, so if we have an exit stack, we want to
# terminate this layer when the exporter is done. But, this is not an unconditional
# nukage. If we're in the lightmap operators, we clearly want this to persist for
# future exports as an optimization. We won't reach this point if there is already an
# autocolor layer (gulp).
if self._context_stack is not None and needs_vcol_layer:
self._context_stack.enter_context(TemporaryObject(vcol_layer, vcols.remove))
# Indicate we should bake
return True
def _remove_stale_uvtexes(self, bake):
lightmap_iter = itertools.chain.from_iterable((value for key, value in bake.items() if key[0] == "lightmap"))
for bo in lightmap_iter:
uv_textures = bo.data.uv_textures
uvtex = uv_textures.get(self.lightmap_uvtex_name, None)
if uvtex is not None:
uv_textures.remove(uvtex)
def _restore_uvtexs(self):
for mesh_name, uvtex_name in self._uvtexs.items():
mesh = bpy.data.meshes[mesh_name]
@ -422,20 +491,3 @@ class LightBaker(_MeshManager):
elif isinstance(i.data, bpy.types.Mesh) and not self._has_valid_material(i):
toggle.track(i, "hide_render", True)
i.select = value
@persistent
def _toss_garbage(scene):
"""Removes all LIGHTMAPGEN and autocolor garbage before saving"""
for i in bpy.data.images:
if i.name.endswith("_LIGHTMAPGEN.png"):
bpy.data.images.remove(i)
for i in bpy.data.meshes:
for uv_tex in i.uv_textures:
if uv_tex.name == "LIGHTMAPGEN":
i.uv_textures.remove(uv_tex)
for vcol in i.vertex_colors:
if vcol.name == "autocolor":
i.vertex_colors.remove(vcol)
# collects light baking garbage
bpy.app.handlers.save_pre.append(_toss_garbage)

133
korman/operators/op_lightmap.py

@ -16,10 +16,24 @@
import bpy
from bpy.props import *
from contextlib import contextmanager
import itertools
from ..exporter.etlight import LightBaker
from ..helpers import UiHelper
from ..korlib import ConsoleToggler
class _LightingOperator:
@contextmanager
def _oven(self, context):
if context.scene.world is not None:
verbose = context.scene.world.plasma_age.verbose
console = context.scene.world.plasma_age.show_console
else:
verbose = False
console = True
with UiHelper(context), ConsoleToggler(console), LightBaker(verbose=verbose) as oven:
yield oven
@classmethod
def poll(cls, context):
@ -32,22 +46,119 @@ class _LightingOperator:
class LightmapAutobakePreviewOperator(_LightingOperator, bpy.types.Operator):
bl_idname = "object.plasma_lightmap_preview"
bl_label = "Preview Lightmap"
bl_description = "Preview Lighting"
bl_options = {"INTERNAL"}
final = BoolProperty(name="Use this lightmap for export")
def __init__(self):
super().__init__()
def execute(self, context):
with self._oven(context) as bake:
if not self.final:
bake.lightmap_name = "{}_LIGHTMAPGEN_PREVIEW.png"
bake.lightmap_uvtex_name = "LIGHTMAPGEN_PREVIEW"
bake.force = self.final
bake.retain_lightmap_uvtex = self.final
if not bake.bake_static_lighting([context.object,]):
self.report({"WARNING"}, "No valid lights found to bake.")
return {"FINISHED"}
lightmap_mod = context.object.plasma_modifiers.lightmap
if lightmap_mod.bake_lightmap:
tex = bpy.data.textures.get("LIGHTMAPGEN_PREVIEW")
if tex is None:
tex = bpy.data.textures.new("LIGHTMAPGEN_PREVIEW", "IMAGE")
tex.extension = "CLIP"
image = bake.get_lightmap(context.object)
tex.image = image
if self.final:
lightmap_mod.image = image
else:
for i in context.object.data.vertex_colors:
i.active = i.name == bake.vcol_layer_name
return {"FINISHED"}
class LightmapBakeMultiOperator(_LightingOperator, bpy.types.Operator):
bl_idname = "object.plasma_lightmap_bake"
bl_label = "Bake Lighting"
bl_description = "Bake scene lighting to object(s)"
bake_selection = BoolProperty(name="Bake Selection",
description="Bake only the selected objects (else all objects)",
options=set())
def __init__(self):
super().__init__()
def execute(self, context):
all_objects = context.selected_objects if self.bake_selection else context.scene.objects
filtered_objects = [i for i in all_objects if i.type == "MESH" and i.plasma_object.enabled]
with self._oven(context) as bake:
bake.force = True
if not bake.bake_static_lighting(filtered_objects):
self.report({"WARNING"}, "Nothing was baked.")
return {"FINISHED"}
for i in filtered_objects:
lightmap_mod = i.plasma_modifiers.lightmap
if lightmap_mod.bake_lightmap:
lightmap_mod.image = bake.get_lightmap(i)
return {"FINISHED"}
class LightmapClearMultiOperator(_LightingOperator, bpy.types.Operator):
bl_idname = "object.plasma_lightmap_clear"
bl_label = "Clear Lighting"
bl_description = "Clear baked lighting"
clear_selection = BoolProperty(name="Clear Selection",
description="Clear only the selected objects (else all objects)",
options=set())
def __init__(self):
super().__init__()
def _iter_lightmaps(self, objects):
yield from filter(lambda x: x.type == "MESH" and x.plasma_modifiers.lightmap.bake_lightmap, objects)
def _iter_vcols(self, objects):
yield from filter(lambda x: x.type == "MESH" and not x.plasma_modifiers.lightmap.bake_lightmap, objects)
def execute(self, context):
with UiHelper(context):
with LightBaker() as bake:
if not bake.bake_static_lighting([context.active_object,]):
self.report({"INFO"}, "No valid lights found to bake.")
return {"FINISHED"}
tex = bpy.data.textures.get("LIGHTMAPGEN_PREVIEW")
if tex is None:
tex = bpy.data.textures.new("LIGHTMAPGEN_PREVIEW", "IMAGE")
tex.extension = "CLIP"
tex.image = bpy.data.images["{}_LIGHTMAPGEN.png".format(context.active_object.name)]
all_objects = context.selected_objects if self.clear_selection else context.scene.objects
for i in self._iter_lightmaps(all_objects):
i.plasma_modifiers.lightmap.image = None
for i in self._iter_vcols(all_objects):
vcols = i.data.vertex_colors
col_layer = vcols.get("autocolor")
if col_layer is not None:
vcols.remove(col_layer)
return {"FINISHED"}
@bpy.app.handlers.persistent
def _toss_garbage(scene):
"""Removes all LIGHTMAPGEN and autocolor garbage before saving"""
bpy_data = bpy.data
tex = bpy_data.textures.get("LIGHTMAPGEN_PREVIEW")
if tex is not None:
bpy_data.textures.remove(tex)
for i in bpy_data.images:
if i.name.endswith("_LIGHTMAPGEN_PREVIEW.png"):
bpy_data.images.remove(i)
for i in bpy_data.meshes:
uvtex = i.uv_textures.get("LIGHTMAPGEN_PREVIEW")
if uvtex is not None:
i.uv_textures.remove(uvtex)
# collects light baking garbage
bpy.app.handlers.save_pre.append(_toss_garbage)

19
korman/properties/modifiers/render.py

@ -435,6 +435,10 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties, PlasmaMod
uv_map = StringProperty(name="UV Texture",
description="UV Texture used as the basis for the lightmap")
image = PointerProperty(name="Baked Image",
description="Use this image instead of re-baking the lighting each export",
type=bpy.types.Image)
@property
def bake_lightmap(self):
if not self.enabled:
@ -459,24 +463,19 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties, PlasmaMod
if not self.bake_lightmap:
return
lightmap_im = bpy.data.images.get("{}_LIGHTMAPGEN.png".format(bo.name))
# If no lightmap image is found, then either lightmap generation failed (error raised by oven)
# or baking is turned off. Either way, bail out.
lightmap_im = self.image if self.image is not None else exporter.oven.get_lightmap(bo)
if lightmap_im is None:
return
mat_mgr = exporter.mesh.material
materials = mat_mgr.get_materials(bo)
# Find the stupid UVTex
uvw_src = 0
for i, uvtex in enumerate(bo.data.tessface_uv_textures):
if uvtex.name == "LIGHTMAPGEN":
uvw_src = i
break
else:
# TODO: raise exception
pass
uvtex_name = "LIGHTMAPGEN"
uvw_src = next((i for i, uvtex in enumerate(bo.data.uv_textures) if uvtex.name == uvtex_name), None)
if uvw_src is None:
raise ExportError("'{}': Lightmap UV Texture '{}' seems to be missing. Did you delete it?", bo.name, uvtex_name)
for matKey in materials:
layer = exporter.mgr.add_object(plLayer, name="{}_LIGHTMAPGEN".format(matKey.name), so=so)

23
korman/ui/modifiers/render.py

@ -196,24 +196,31 @@ def lightmap(modifier, layout, context):
col = layout.column()
col.active = is_texture
col.prop_search(modifier, "uv_map", context.active_object.data, "uv_textures")
col = layout.column()
col.active = is_texture
col.prop(modifier, "image", icon="IMAGE_RGB")
# Lightmaps can only be applied to objects with opaque materials.
if is_texture and any((i.use_transparency for i in modifier.id_data.data.materials if i is not None)):
layout.label("Transparent objects cannot be lightmapped.", icon="ERROR")
else:
col = layout.column()
col.active = is_texture
operator = col.operator("object.plasma_lightmap_preview", "Preview Lightmap", icon="RENDER_STILL")
row = layout.row(align=True)
if modifier.bake_lightmap:
row.operator("object.plasma_lightmap_preview", "Preview", icon="RENDER_STILL").final = False
row.operator("object.plasma_lightmap_preview", "Bake for Export", icon="RENDER_STILL").final = True
else:
row.operator("object.plasma_lightmap_preview", "Bake", icon="RENDER_STILL").final = True
# Kind of clever stuff to show the user a preview...
# We can't show images, so we make a hidden ImageTexture called LIGHTMAPGEN_PREVIEW. We check
# the backing image name to see if it's for this lightmap. If so, you have a preview. If not,
# well... It was nice knowing you!
tex = bpy.data.textures.get("LIGHTMAPGEN_PREVIEW")
if tex is not None and tex.image is not None:
im_name = "{}_LIGHTMAPGEN.png".format(context.active_object.name)
if tex.image.name == im_name:
layout.template_preview(tex, show_buttons=False)
if is_texture:
tex = bpy.data.textures.get("LIGHTMAPGEN_PREVIEW")
if tex is not None and tex.image is not None:
im_name = "{}_LIGHTMAPGEN_PREVIEW.png".format(context.active_object.name)
if tex.image.name == im_name:
layout.template_preview(tex, show_buttons=False)
def rtshadow(modifier, layout, context):
split = layout.split()

8
korman/ui/ui_toolbox.py

@ -45,7 +45,13 @@ class PlasmaToolboxPanel(ToolboxPanel, bpy.types.Panel):
col.label("Plasma Pages:")
col.operator("object.plasma_move_selection_to_page", icon="BOOKMARKS", text="Move to Page")
col.operator("object.plasma_select_page_objects", icon="RESTRICT_SELECT_OFF", text="Select Objects")
col.label("Lighting:")
col.operator("object.plasma_lightmap_bake", icon="RENDER_STILL", text="Bake All").bake_selection = False
col.operator("object.plasma_lightmap_bake", icon="RENDER_REGION", text="Bake Selection").bake_selection = True
col.operator("object.plasma_lightmap_clear", icon="X", text="Clear All").clear_selection = False
col.operator("object.plasma_lightmap_clear", icon="X", text="Clear Selection").clear_selection = True
col.label("Package Sounds:")
col.operator("object.plasma_toggle_sound_export", icon="MUTE_IPO_OFF", text="Enable All").enable = True
all_sounds_export = all((i.package for i in itertools.chain.from_iterable(i.plasma_modifiers.soundemit.sounds for i in bpy.context.selected_objects if i.plasma_modifiers.soundemit.enabled)))

Loading…
Cancel
Save