From 2b3656af393f68b7c7538541c2cf9f331f6fbb67 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sun, 14 Feb 2016 20:45:05 -0500 Subject: [PATCH] Refactor lighting code to limit bake_image calls --- korman/exporter/convert.py | 10 + korman/exporter/etlight.py | 315 ++++++++++++++++++++++++++++++++ korman/exporter/mesh.py | 19 -- korman/helpers.py | 16 -- korman/operators/op_export.py | 13 +- korman/operators/op_lightmap.py | 231 +---------------------- 6 files changed, 332 insertions(+), 272 deletions(-) create mode 100644 korman/exporter/etlight.py diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index bf84a4b..9680c64 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -20,6 +20,7 @@ import time from . import animation from . import explosions +from . import etlight from . import logger from . import manager from . import mesh @@ -65,6 +66,11 @@ class Exporter: # that the artist made requires something to have a CoordinateInterface self._harvest_actors() + # Step 2.9: It is assumed that static lighting is available for the mesh exporter. + # Indeed, in PyPRP it was a manual step. So... BAKE NAO! + if self._op.bake_lighting: + self._bake_static_lighting() + # Step 3: Export all the things! self._export_scene_objects() @@ -91,6 +97,10 @@ class Exporter: end = time.process_time() print("\nExported {}.age in {:.2f} seconds".format(self.age_name, end-start)) + def _bake_static_lighting(self): + oven = etlight.LightBaker() + oven.bake_static_lighting(self._objects) + def _collect_objects(self): # Grab a naive listing of enabled pages age = bpy.context.scene.world.plasma_age diff --git a/korman/exporter/etlight.py b/korman/exporter/etlight.py new file mode 100644 index 0000000..d6fff0e --- /dev/null +++ b/korman/exporter/etlight.py @@ -0,0 +1,315 @@ +# 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 . + +import bpy + +from .mesh import _VERTEX_COLOR_LAYERS +from ..helpers import * + +_NUM_RENDER_LAYERS = 20 + +class LightBaker: + """ExportTime Lighting""" + + def __init__(self): + self._lightgroups = {} + self._uvtexs = {} + + def _apply_render_settings(self, toggle): + render = bpy.context.scene.render + toggle.track(render, "use_textures", False) + toggle.track(render, "use_shadows", True) + toggle.track(render, "use_envmaps", False) + toggle.track(render, "use_raytrace", True) + toggle.track(render, "bake_type", "FULL") + toggle.track(render, "use_bake_clear", True) + + def _associate_image_with_uvtex(self, uvtex, im): + # Associate the image with all the new UVs + # NOTE: no toggle here because it's the artist's problem if they are looking at our + # super swagalicious LIGHTMAPGEN uvtexture... + for i in uvtex.data: + i.image = im + + def _bake_lightmaps(self, objs, layers, toggle): + scene = bpy.context.scene + scene.layers = layers + toggle.track(scene.render, "use_bake_to_vertex_color", False) + self._select_only(objs) + bpy.ops.object.bake_image() + + def _bake_vcols(self, objs, toggle): + bpy.context.scene.layers = (True,) * _NUM_RENDER_LAYERS + toggle.track(bpy.context.scene.render, "use_bake_to_vertex_color", True) + self._select_only(objs) + bpy.ops.object.bake_image() + + def bake_static_lighting(self, objs): + """Bakes all static lighting for Plasma geometry""" + + print("\nBaking Static Lighting...") + bake = self._harvest_bakable_objects(objs) + + with GoodNeighbor() as toggle: + try: + # reduce the amount of indentation + self._bake_static_lighting(bake, toggle) + finally: + # this stuff has been observed to be problematic with GoodNeighbor + self._pop_lightgroups() + self._restore_uvtexs() + + def _bake_static_lighting(self, bake, toggle): + # Step 0.9: Make all layers visible. + # This prevents context operators from phailing. + bpy.context.scene.layers = (True,) * _NUM_RENDER_LAYERS + + # Step 1: Prepare... Apply UVs, etc, etc, etc + for key, value in bake.copy().items(): + if key[0] == "lightmap": + for i in value: + if not self._prep_for_lightmap(i, toggle): + bake[key].remove(i) + elif key[0] == "vcol": + for i in value: + if not self._prep_for_vcols(i, toggle): + bake[key].remove(i) + else: + raise RuntimeError(key[0]) + + # Step 2: BAKE! + self._apply_render_settings(toggle) + for key, value in bake.items(): + if not value: + continue + + if key[0] == "lightmap": + print(" {} Lightmap(s) [H:{:X}]".format(len(value), hash(key))) + self._bake_lightmaps(value, key[1:], toggle) + elif key[0] == "vcol": + print(" {} Crap Light(s)".format(len(value))) + self._bake_vcols(value, toggle) + else: + raise RuntimeError(key[0]) + + def _generate_lightgroup(self, mesh, user_lg=None): + """Makes a new light group for the baking process that excludes all Plasma RT lamps""" + + if user_lg is not None: + user_lg = bpy.data.groups.get(user_lg) + shouldibake = (user_lg and user_lg.objects) + + for material in mesh.materials: + if material is None: + # material is not assigned to this material... (why is this even a thing?) + continue + + # Already done it? + name = material.name + lg = material.light_group + if name in self._lightgroups: + # No, this is not Pythonic, but bpy_prop_collection is always "True", + # even when empty. Sigh. + return bool(len(lg.objects)) + else: + self._lightgroups[name] = lg + + if user_lg is None: + if not lg or len(lg.objects) == 0: + source = [i for i in bpy.data.objects if i.type == "LAMP"] + else: + source = lg.objects + dest = bpy.data.groups.new("_LIGHTMAPGEN_{}".format(name)) + + # Only use non-RT lights + for obj in source: + if obj.plasma_object.enabled: + continue + dest.objects.link(obj) + shouldibake = True + else: + dest = user_lg + material.light_group = dest + return shouldibake + + 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": + return i + return None + + def _harvest_bakable_objects(self, objs): + # The goal here is to minimize the calls to bake_image, so we are going to collect everything + # that needs to be baked and sort it out by configuration. + bake = { ("vcol",): [] } + for i in objs: + if i.type != "MESH": + continue + + mods = i.plasma_modifiers + if mods.lightmap.enabled: + ## TODO: customizable render layers + #key = ("lightmap",) + mods.lightmap.render_layers + key = ("lightmap",) + ((True,) * 20) + if key in bake: + bake[key].append(i) + else: + bake[key] = [i,] + elif not mods.water_basic.enabled: + vcols = i.data.vertex_colors + for j in _VERTEX_COLOR_LAYERS: + if j in vcols: + break + else: + bake[("vcol",)].append(i) + return bake + + def _pop_lightgroups(self): + for mat_name, lg in self._lightgroups.items(): + material = bpy.data.materials[mat_name] + _fake = material.light_group + if _fake is not None and _fake.name.startswith("_LIGHTMAPGEN"): + for i in _fake.objects: + _fake.objects.unlink(i) + _fake.user_clear() + bpy.data.groups.remove(_fake) + material.light_group = lg + self._lightgroups.clear() + + def _prep_for_lightmap(self, bo, toggle): + mesh = bo.data + modifier = bo.plasma_modifiers.lightmap + uv_textures = mesh.uv_textures + + # Create a special light group for baking + if not self._generate_lightgroup(mesh, modifier.light_group): + return False + + # We need to ensure that we bake onto the "BlahObject_LIGHTMAPGEN" image + data_images = bpy.data.images + im_name = "{}_LIGHTMAPGEN.png".format(bo.name) + size = modifier.resolution + + im = data_images.get(im_name) + if im is None: + im = data_images.new(im_name, width=size, height=size) + elif im.size[0] != size: + # Force delete and recreate the image because the size is out of date + im.user_clear() + data_images.remove(im) + 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) + if uvtex is not None: + uv_textures.remove(uvtex) + + # Make sure the object can be baked to. NOTE this also makes sure we can enter edit mode + # TROLLING LOL LOL LOL + toggle.track(bo, "hide", False) + toggle.track(bo, "hide_render", False) + toggle.track(bo, "hide_select", False) + + # Because the way Blender tracks active UV layers is massively stupid... + self._uvtexs[mesh.name] = uv_textures.active.name + + # We must make this the active object before touching any operators + bpy.context.scene.objects.active = bo + + # Originally, we used the lightmap unpack UV operator to make our UV texture, however, + # this tended to create sharp edges. There was already a discussion about this on the + # Guild of Writers forum, so I'm implementing a code version of dendwaler's process, + # as detailed here: https://forum.guildofwriters.org/viewtopic.php?p=62572#p62572 + uv_base = self._get_lightmap_uvtex(mesh, modifier) + if uv_base is not None: + uv_textures.active = uv_base + # this will copy the UVs to the new UV texture + uvtex = uv_textures.new("LIGHTMAPGEN") + uv_textures.active = uvtex + self._associate_image_with_uvtex(uvtex, im) + # here we go... + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.mesh.select_all(action="SELECT") + bpy.ops.uv.average_islands_scale() + bpy.ops.uv.pack_islands() + 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") + self._associate_image_with_uvtex(uvtex, im) + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.mesh.select_all(action="SELECT") + bpy.ops.uv.smart_project() + bpy.ops.object.mode_set(mode="OBJECT") + + # Now, set the new LIGHTMAPGEN uv layer as what we want to render to... + # 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" + i.active = value + i.active_render = value + + # Indicate we should bake + return True + + def _prep_for_vcols(self, bo, toggle): + mesh = bo.data + vcols = mesh.vertex_colors + + # Create a special light group for baking + if not self._generate_lightgroup(mesh): + return False + + # Make sure the object can be baked to. NOTE this also makes sure we can enter edit mode + # TROLLING LOL LOL LOL + toggle.track(bo, "hide", False) + toggle.track(bo, "hide_render", False) + toggle.track(bo, "hide_select", False) + + # I have heard tale of some moar "No valid image to bake to" boogs if there is a really + # old copy of the autocolor layer on the mesh. Nuke it. + autocolor = vcols.get("autocolor") + if autocolor is not None: + vcols.remove(autocolor) + autocolor = vcols.new("autocolor") + toggle.track(vcols, "active", autocolor) + + # Mark "autocolor" as our active render layer + for vcol_layer in mesh.vertex_colors: + autocol = vcol_layer.name == "autocolor" + toggle.track(vcol_layer, "active_render", autocol) + toggle.track(vcol_layer, "active", autocol) + mesh.update() + + # Indicate we should bake + return True + + def _restore_uvtexs(self): + for mesh_name, uvtex_name in self._uvtexs.items(): + mesh = bpy.data.meshes[mesh_name] + for i in mesh.uv_textures: + i.active = uvtex_name == i.name + mesh.uv_textures.active = mesh.uv_textures[uvtex_name] + + def _select_only(self, objs): + if isinstance(objs, bpy.types.Object): + for i in bpy.data.objects: + i.select = i == objs + else: + for i in bpy.data.objects: + i.select = i in objs diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index 17e6b7c..d096683 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -278,9 +278,6 @@ class MeshConverter: if not materials: return None - # Step 0.9: If this mesh wants to be lit, we need to go ahead and generate it. - self._export_static_lighting(bo) - with helpers.TemporaryObject(mesh, bpy.data.meshes.remove): # Step 1: Export all of the doggone materials. geospans = self._export_material_spans(bo, mesh, materials) @@ -330,22 +327,6 @@ class MeshConverter: geospans[i] = (self._create_geospan(bo, mesh, blmat, matKey), blmat.pass_index) return geospans - def _export_static_lighting(self, bo): - bpy.context.scene.objects.active = bo - mods = bo.plasma_modifiers - lm = mods.lightmap - if lm.enabled: - print(" Baking lightmap...") - bpy.ops.object.plasma_lightmap_autobake(light_group=lm.light_group) - elif not mods.water_basic.enabled: - for vcol_layer in bo.data.vertex_colors: - name = vcol_layer.name.lower() - if name in _VERTEX_COLOR_LAYERS: - break - else: - print(" Baking crappy vertex color lighting...") - bpy.ops.object.plasma_vertexlight_autobake() - def _find_create_dspan(self, bo, hsgmat, pass_index): location = self._mgr.get_location(bo) if location not in self._dspans: diff --git a/korman/helpers.py b/korman/helpers.py index 4c6029c..efc1fb4 100644 --- a/korman/helpers.py +++ b/korman/helpers.py @@ -47,22 +47,6 @@ class TemporaryObject: return getattr(self._obj, attr) -def ensure_object_can_bake(bo, toggle): - """Ensures that we can use Blender's baking operators on this object. Side effect: also ensures - that the object will enter edit mode when requested.""" - scene = bpy.context.scene - # we don't toggle.track this because it actually updates some kind of Blender internals... - # therefore the old and new value are equal. the export operator will take care of this for us - scene.layers = (True,) * len(scene.layers) - - toggle.track(bo, "hide", False) - toggle.track(bo, "hide_render", False) - toggle.track(bo, "hide_select", False) - - # Ensure only this object is selected - for i in bpy.data.objects: - i.select = i == bo - def ensure_power_of_two(value): return pow(2, math.floor(math.log(value, 2))) diff --git a/korman/operators/op_export.py b/korman/operators/op_export.py index 6fac605..658e421 100644 --- a/korman/operators/op_export.py +++ b/korman/operators/op_export.py @@ -37,13 +37,9 @@ class ExportOperator(bpy.types.Operator): "description": "Profiles the exporter using cProfile", "default": False}), - "regenerate_lightmaps": (BoolProperty, {"name": "Regenerate Lightmaps", - "description": "(Re-)Bake all lightmaps on export", - "default": True}), - - "regenerate_shading": (BoolProperty, {"name": "Regenerate Vertex Shading", - "description": "(Re-)Bake all vertex shading on export", - "default": True}), + "bake_lighting": (BoolProperty, {"name": "Bake Static Lights", + "description": "Bake all lightmaps and vertex shading on export", + "default": True}), "version": (EnumProperty, {"name": "Version", "description": "Version of the Plasma Engine to target", @@ -60,8 +56,7 @@ class ExportOperator(bpy.types.Operator): # The crazy mess we're doing with props on the fly means we have to explicitly draw them :( layout.prop(age, "version") - layout.prop(age, "regenerate_lightmaps") - layout.prop(age, "regenerate_shading") + layout.prop(age, "bake_lighting") layout.prop(age, "profile_export") def __getattr__(self, attr): diff --git a/korman/operators/op_lightmap.py b/korman/operators/op_lightmap.py index 8902720..c058da4 100644 --- a/korman/operators/op_lightmap.py +++ b/korman/operators/op_lightmap.py @@ -15,192 +15,16 @@ import bpy from bpy.props import * -from ..helpers import * -def _fetch_lamp_objects(): - for obj in bpy.data.objects: - if obj.type == "LAMP": - yield obj +from ..exporter.etlight import LightBaker class _LightingOperator: - def __init__(self): - self._old_lightgroups = {} @classmethod def poll(cls, context): if context.object is not None: return context.scene.render.engine == "PLASMA_GAME" - def _apply_render_settings(self, render, toggle): - toggle.track(render, "use_textures", False) - toggle.track(render, "use_shadows", True) - toggle.track(render, "use_envmaps", False) - toggle.track(render, "use_raytrace", True) - toggle.track(render, "bake_type", "FULL") - toggle.track(render, "use_bake_clear", True) - - def _generate_lightgroups(self, mesh, user_lg=None): - """Makes a new light group for the baking process that excludes all Plasma RT lamps""" - shouldibake = (user_lg and user_lg.objects) - - for material in mesh.materials: - if material is None: - # material is not assigned to this material... (why is this even a thing?) - continue - - lg = material.light_group - self._old_lightgroups[material] = lg - - if user_lg is None: - # TODO: faux-lightgroup caching for the entire export process. you dig? - if not lg or len(lg.objects) == 0: - source = _fetch_lamp_objects() - else: - source = lg.objects - dest = bpy.data.groups.new("_LIGHTMAPGEN_{}".format(material.name)) - - # Only use non-RT lights - for obj in source: - if obj.plasma_object.enabled: - continue - dest.objects.link(obj) - shouldibake = True - else: - dest = user_lg - material.light_group = dest - return shouldibake - - def _pop_lightgroups(self): - for material, lg in self._old_lightgroups.items(): - _fake = material.light_group - if _fake is not None and _fake.name.startswith("_LIGHTMAPGEN"): - for i in _fake.objects: - _fake.objects.unlink(i) - _fake.user_clear() - bpy.data.groups.remove(_fake) - material.light_group = lg - self._old_lightgroups.clear() - - -class LightmapAutobakeOperator(_LightingOperator, bpy.types.Operator): - bl_idname = "object.plasma_lightmap_autobake" - bl_label = "Bake Lightmap" - bl_options = {"INTERNAL"} - - light_group = StringProperty(name="Light Group") - force = BoolProperty(name="Force Lightmap Generation", default=False) - - def __init__(self): - super().__init__() - - def _associate_image_with_uvtex(self, uvtex, im): - # Associate the image with all the new UVs - # NOTE: no toggle here because it's the artist's problem if they are looking at our - # super swagalicious LIGHTMAPGEN uvtexture... - for i in uvtex.data: - i.image = im - - def _get_base_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": - return i - return None - - def execute(self, context): - obj = context.active_object - mesh = obj.data - modifier = obj.plasma_modifiers.lightmap - uv_textures = mesh.uv_textures - - with GoodNeighbor() as toggle: - # We need to ensure that we bake onto the "BlahObject_LIGHTMAPGEN" image - data_images = bpy.data.images - im_name = "{}_LIGHTMAPGEN.png".format(obj.name) - size = modifier.resolution - - im = data_images.get(im_name) - if im is None: - im = data_images.new(im_name, width=size, height=size) - elif im.size[0] != size: - # Force delete and recreate the image because the size is out of date - im.user_clear() - data_images.remove(im) - im = data_images.new(im_name, width=size, height=size) - elif not (context.scene.world.plasma_age.regenerate_lightmaps or self.force): - # we have a lightmap that matches our specs, so what gives??? - # this baking process is one slow thing. only do it if the user wants us to! - return {"CANCELLED"} - - # If there is a cached LIGHTMAPGEN uvtexture, nuke it - uvtex = uv_textures.get("LIGHTMAPGEN", None) - if uvtex is not None: - uv_textures.remove(uvtex) - - # Make sure the object can be baked to. NOTE this also makes sure we can enter edit mode - # TROLLING LOL LOL LOL - ensure_object_can_bake(obj, toggle) - - # Because the way Blender tracks active UV layers is massively stupid... - og_uv_map = uv_textures.active - - # Originally, we used the lightmap unpack UV operator to make our UV texture, however, - # this tended to create sharp edges. There was already a discussion about this on the - # Guild of Writers forum, so I'm implementing a code version of dendwaler's process, - # as detailed here: http://forum.guildofwriters.org/viewtopic.php?p=62572#p62572 - uv_base = self._get_base_uvtex(mesh, modifier) - if uv_base is not None: - uv_textures.active = uv_base - # this will copy the UVs to the new UV texture - uvtex = uv_textures.new("LIGHTMAPGEN") - uv_textures.active = uvtex - self._associate_image_with_uvtex(uvtex, im) - # here we go... - bpy.ops.object.mode_set(mode="EDIT") - bpy.ops.mesh.select_all(action="SELECT") - bpy.ops.uv.average_islands_scale() - bpy.ops.uv.pack_islands() - 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") - self._associate_image_with_uvtex(uvtex, im) - bpy.ops.object.mode_set(mode="EDIT") - bpy.ops.mesh.select_all(action="SELECT") - bpy.ops.uv.smart_project() - bpy.ops.object.mode_set(mode="OBJECT") - - # Now, set the new LIGHTMAPGEN uv layer as what we want to render to... - for i in uv_textures: - value = i.name == "LIGHTMAPGEN" - i.active = value - i.active_render = value - - # Bake settings - render = context.scene.render - toggle.track(render, "use_bake_to_vertex_color", False) - self._apply_render_settings(render, toggle) - - # Now, we *finally* bake the lightmap... - try: - light_group = bpy.data.groups[self.light_group] if self.light_group else None - if self._generate_lightgroups(mesh, light_group): - bpy.ops.object.bake_image() - im.pack(as_png=True) - self._pop_lightgroups() - finally: - for i, uv_tex in enumerate(uv_textures): - # once this executes the og_uv_map is apparently no longer in the UVTextures :/ - # search by name to find the REAL uv texture that we need. - if uv_tex.name == og_uv_map.name: - uv_textures.active = uv_tex - uv_textures.active_index = i - break - - # Done! - return {"FINISHED"} - class LightmapAutobakePreviewOperator(_LightingOperator, bpy.types.Operator): bl_idname = "object.plasma_lightmap_preview" @@ -213,7 +37,8 @@ class LightmapAutobakePreviewOperator(_LightingOperator, bpy.types.Operator): super().__init__() def execute(self, context): - bpy.ops.object.plasma_lightmap_autobake(light_group=self.light_group, force=True) + bake = LightBaker() + bake.bake_static_lighting([context.active_object,]) tex = bpy.data.textures.get("LIGHTMAPGEN_PREVIEW") if tex is None: @@ -222,53 +47,3 @@ class LightmapAutobakePreviewOperator(_LightingOperator, bpy.types.Operator): tex.image = bpy.data.images["{}_LIGHTMAPGEN.png".format(context.active_object.name)] return {"FINISHED"} - - -class VertexColorLightingOperator(_LightingOperator, bpy.types.Operator): - bl_idname = "object.plasma_vertexlight_autobake" - bl_label = "Bake Vertex Color Lighting" - bl_options = {"INTERNAL"} - - def __init__(self): - super().__init__() - - def execute(self, context): - with GoodNeighbor() as toggle: - obj = context.active_object - mesh = obj.data - vcols = mesh.vertex_colors - - # I have heard tale of some moar "No valid image to bake to" boogs if there is a really - # old copy of the autocolor layer on the mesh. Nuke it. - autocolor = vcols.get("autocolor") - if autocolor is not None: - if context.scene.world.plasma_age.regenerate_shading: - vcols.remove(autocolor) - else: - # we have autocolor already, don't regenerate it because they don't want it - return {"CANCELLED"} - autocolor = vcols.new("autocolor") - toggle.track(vcols, "active", autocolor) - - # Mark "autocolor" as our active render layer - for vcol_layer in mesh.vertex_colors: - autocol = vcol_layer.name == "autocolor" - toggle.track(vcol_layer, "active_render", autocol) - toggle.track(vcol_layer, "active", autocol) - mesh.update() - - # Bake settings - render = context.scene.render - toggle.track(render, "use_bake_to_vertex_color", True) - self._apply_render_settings(render, toggle) - - # Really and truly make sure we can bake... - ensure_object_can_bake(obj, toggle) - - # Bake - if self._generate_lightgroups(mesh): - bpy.ops.object.bake_image() - self._pop_lightgroups() - - # And done! - return {"FINISHED"}