Browse Source

Allow persistent baked lighting.

You can now bake "finalized" lighting to either vertex colors or a
lightmap image. This can be done either from the toolbox or from the
individual lightmap modifier. The purpose of doing this is to allow the
artist to opt-into a workflow where they can chose when to incur the
performance penalty, instead of on export. The downside is that the
artist now has to manually click a bake button. But, for some Ages,
especially "finished" ones receiving small updates, there is no need to
rebake the lighting each export.
pull/261/head
Adam Johnson 3 years ago
parent
commit
f52669e78f
Signed by: Hoikas
GPG Key ID: 0B6515D6FF6F271E
  1. 103
      korman/exporter/etlight.py
  2. 136
      korman/operators/op_lightmap.py
  3. 20
      korman/properties/modifiers/render.py
  4. 10
      korman/ui/modifiers/render.py
  5. 6
      korman/ui/ui_toolbox.py

103
korman/exporter/etlight.py

@ -14,10 +14,11 @@
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
import bpy
from bpy.app.handlers import persistent
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 +27,22 @@ _NUM_RENDER_LAYERS = 20
class LightBaker(_MeshManager):
"""ExportTime Lighting"""
def __init__(self, report=None):
def __init__(self, report=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.vcol_layer_name = "autocolor"
self.lightmap_name = "{}_LIGHTMAPGEN.png"
self.lightmap_uvtex_name = "LIGHTMAPGEN"
self.retain_lightmap_uvtex = True
self.force = False
self._uvtexs = {}
def __del__(self):
@ -99,6 +105,10 @@ 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)
else:
self._pack_lightmaps(bake)
return result
def _bake_static_lighting(self, bake, toggle):
@ -203,11 +213,14 @@ class LightBaker(_MeshManager):
material.light_group = dest
return shouldibake
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 +237,7 @@ 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
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,15 +257,42 @@ 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))
# OK, now that the sanity checking is done, we could opt-out if an image is already
# set and we're not being forced...
if not self.force:
want_image = lightmap_mod.bake_lightmap
if want_image and lightmap_mod.image is not None and self.lightmap_uvtex_name in i.data.uv_textures:
self._report.msg("'{}': Skipping due to valid lightmap override", i.name, indent=1)
continue
elif not want_image and any((vcol_layer.name.lower() in _VERTEX_COLOR_LAYERS for vcol_layer in i.data.vertex_colors)):
self._report.msg("'{}': Skipping due to valid vertex color layer", i.name, indent=1)
continue
method = "lightmap" if lightmap_mod.bake_lightmap else "vcol"
key = (method,) + lm_layers
bake_pass = bake.setdefault(key, [])
bake_pass.append(i)
self._report.msg("'{}': Bake to {}", i.name, method, indent=1)
elif mods.lighting.preshade:
if not any((vcol_layer.name.lower() in _VERTEX_COLOR_LAYERS for vcol_layer in i.data.vertex_colors)):
if self.force or not any((vcol_layer.name.lower() in _VERTEX_COLOR_LAYERS for vcol_layer in i.data.vertex_colors)):
self._report.msg("'{}': Bake to vcol (crappy)", i.name, indent=1)
bake_vcol.append(i)
return bake
def _pack_lightmaps(self, bake):
lightmap_iter = itertools.chain.from_iterable((value for key, value in bake.items() if key[0] == "lightmap"))
for bo in lightmap_iter:
im = bpy.data.images.get(self.get_lightmap_name(bo))
if im is not None and im.is_dirty:
im.pack(as_png=True)
# Blender bug? If there is no vertex color layer, some textured rendered objects
# seem to go KABLOOEY! So, make sure there is at least one dummy vcol layer.
vcols = bo.data.vertex_colors
if not bool(vcols):
# So the user knows what's happening
vcols.new("LIGHTMAPGEN_defeatcrash")
def _pop_lightgroups(self):
materials = bpy.data.materials
for mat_name, lg in self._lightgroups.items():
@ -292,7 +327,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)
@ -304,7 +339,7 @@ class LightBaker(_MeshManager):
im = data_images.new(im_name, width=size, height=size)
# 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)
@ -326,7 +361,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
@ -343,7 +378,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")
@ -354,7 +389,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
@ -371,14 +406,15 @@ class LightBaker(_MeshManager):
if not self._generate_lightgroup(bo, user_lg):
return False
autocolor = vcols.get("autocolor")
vcol_layer_name = self.vcol_layer_name
autocolor = vcols.get(vcol_layer_name)
if autocolor is None:
autocolor = vcols.new("autocolor")
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()
@ -386,6 +422,14 @@ class LightBaker(_MeshManager):
# 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]
@ -418,20 +462,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)

136
korman/operators/op_lightmap.py

@ -16,10 +16,25 @@
import bpy
from bpy.props import *
from contextlib import contextmanager
from ..exporter.etlight import LightBaker
from ..helpers import UiHelper
from ..korlib import ConsoleToggler
class _LightingOperator:
_FINAL_VERTEX_COLOR_LAYER = "Col"
@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,26 +47,133 @@ 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 draw(self, context):
layout = self.layout
layout.label("This will overwrite the following vertex color layer:")
layout.label(self._FINAL_VERTEX_COLOR_LAYER, icon="GROUP_VCOL")
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.")
with self._oven(context) as bake:
if self.final:
bake.vcol_layer_name = self._FINAL_VERTEX_COLOR_LAYER
else:
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"}
if context.object.plasma_modifiers.lightmap.bake_type == "lightmap":
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"
tex.image = bpy.data.images["{}_LIGHTMAPGEN.png".format(context.active_object.name)]
image = bpy.data.images[bake.get_lightmap_name(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 == "autocolor"
i.active = i.name == bake.vcol_layer_name
return {"FINISHED"}
def invoke(self, context, event):
# If this is a vertex color bake, we need to be sure that the user really
# wants to blow away any color layer they have.
if self.final and context.object.plasma_modifiers.lightmap.bake_type == "vcol":
if any((i.name == self._FINAL_VERTEX_COLOR_LAYER for i in context.object.data.vertex_colors)):
return context.window_manager.invoke_props_dialog(self)
return self.execute(context)
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
bake.vcol_layer_name = self._FINAL_VERTEX_COLOR_LAYER
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 = bpy.data.images[bake.get_lightmap_name(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 execute(self, context):
all_objects = context.selected_objects if self.clear_selection else context.scene.objects
for i in filter(lambda x: x.type == "MESH", all_objects):
lightmap_mod = i.plasma_modifiers.lightmap
if lightmap_mod.bake_lightmap:
lightmap_mod.image = None
else:
vcols = i.data.vertex_colors
col_layer = vcols.get(self._FINAL_VERTEX_COLOR_LAYER)
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)
vcol_layer = i.vertex_colors.get("autocolor")
if vcol_layer is not None:
i.vertex_colors.remove(vcol_layer)
# collects light baking garbage
bpy.app.handlers.save_pre.append(_toss_garbage)

20
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,6 +463,10 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties, PlasmaMod
if not self.bake_lightmap:
return
if self.image is not None:
lightmap_im = self.image
else:
# Gulp...
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)
@ -469,14 +477,10 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties, PlasmaMod
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)

10
korman/ui/modifiers/render.py

@ -196,13 +196,17 @@ 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.operator("object.plasma_lightmap_preview", "Preview Lightmap" if is_texture else "Preview Vertex Colors", icon="RENDER_STILL")
row = layout.row(align=True)
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
# 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
@ -211,7 +215,7 @@ def lightmap(modifier, layout, context):
if is_texture:
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)
im_name = "{}_LIGHTMAPGEN_PREVIEW.png".format(context.active_object.name)
if tex.image.name == im_name:
layout.template_preview(tex, show_buttons=False)

6
korman/ui/ui_toolbox.py

@ -46,6 +46,12 @@ class PlasmaToolboxPanel(ToolboxPanel, bpy.types.Panel):
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