diff --git a/korman/exporter/etlight.py b/korman/exporter/etlight.py index 8f7b79f..9e78ff0 100644 --- a/korman/exporter/etlight.py +++ b/korman/exporter/etlight.py @@ -14,10 +14,11 @@ # along with Korman. If not, see . 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) diff --git a/korman/operators/op_lightmap.py b/korman/operators/op_lightmap.py index 87119a1..43bb85c 100644 --- a/korman/operators/op_lightmap.py +++ b/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.") - return {"FINISHED"} + 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) diff --git a/korman/properties/modifiers/render.py b/korman/properties/modifiers/render.py index 1b7eea8..c56daa5 100644 --- a/korman/properties/modifiers/render.py +++ b/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,7 +463,11 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties, PlasmaMod if not self.bake_lightmap: return - lightmap_im = bpy.data.images.get("{}_LIGHTMAPGEN.png".format(bo.name)) + 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) # or baking is turned off. Either way, bail out. @@ -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) diff --git a/korman/ui/modifiers/render.py b/korman/ui/modifiers/render.py index a454512..d051e5f 100644 --- a/korman/ui/modifiers/render.py +++ b/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) diff --git a/korman/ui/ui_toolbox.py b/korman/ui/ui_toolbox.py index b8c1539..fac984a 100644 --- a/korman/ui/ui_toolbox.py +++ b/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)))