# 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 bpy.app.handlers import persistent 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, vcols): 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) toggle.track(render, "use_bake_to_vertex_color", vcols) 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): with GoodNeighbor() as toggle: scene = bpy.context.scene scene.layers = layers self._apply_render_settings(toggle, False) self._select_only(objs, toggle) bpy.ops.object.bake_image() def _bake_vcols(self, objs): with GoodNeighbor() as toggle: bpy.context.scene.layers = (True,) * _NUM_RENDER_LAYERS self._apply_render_settings(toggle, True) self._select_only(objs, toggle) 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 result = self._bake_static_lighting(bake, toggle) finally: # this stuff has been observed to be problematic with GoodNeighbor self._pop_lightgroups() self._restore_uvtexs() return result 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 print(" Preparing to bake...") for key in bake.keys(): if key[0] == "lightmap": for i in range(len(bake[key])-1, -1, -1): obj = bake[key][i] if not self._prep_for_lightmap(obj, toggle): print(" Lightmap '{}' will not be baked -- no applicable lights".format(obj.name)) bake[key].pop(i) elif key[0] == "vcol": for i in range(len(bake[key])-1, -1, -1): obj = bake[key][i] if not self._prep_for_vcols(obj, toggle): if self._has_valid_material(obj): print(" VCols '{}' will not be baked -- no applicable lights".format(obj.name)) bake[key].pop(i) else: raise RuntimeError(key[0]) print(" ...") # Step 2: BAKE! 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:]) elif key[0] == "vcol": print(" {} Crap Light(s)".format(len(value))) self._bake_vcols(value) else: raise RuntimeError(key[0]) # Return how many thingos we baked return sum(map(len, bake.values())) def _generate_lightgroup(self, bo, 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 is not None and bool(user_lg.objects)) mesh = bo.data 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 not in self._lightgroups: self._lightgroups[name] = lg if user_lg is None: if not lg or bool(lg.objects) is False: source = [i for i in bpy.data.objects if i.type == "LAMP"] else: source = lg.objects dest = bpy.data.groups.new("_LIGHTMAPGEN_{}".format(name)) # Rules: # 1) No animated lights, period. # 2) If we accept runtime lighting, no Plasma Objects rtl_mod = bo.plasma_modifiers.lighting for obj in source: if obj.plasma_object.has_animation_data: continue if rtl_mod.rt_lights and obj.plasma_object.enabled: continue dest.objects.link(obj) shouldibake = True else: # The aforementioned rules do not apply. You better hope you know WTF you are # doing. I'm not going to help! 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 _has_valid_material(self, bo): for material in bo.data.materials: if material is not None: return True return False 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 if bool(i.data.materials) is False: continue mods = i.plasma_modifiers if mods.lightmap.enabled: key = ("lightmap",) + tuple(mods.lightmap.render_layers) if key in bake: bake[key].append(i) else: bake[key] = [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) return bake def _pop_lightgroups(self): groups, materials = bpy.data.groups, bpy.data.materials for mat_name, lg in self._lightgroups.items(): material = materials[mat_name] _fake = material.light_group if _fake is not None: # I have seen issues where the light group is not always removed correctly if we # just call groups.remove(_fake) -- so let's search for this LG's name and remove # that result. Should actually fix the problem... I hope. group_name = "_LIGHTMAPGEN_{}".format(mat_name) _hack_lg = groups.get(group_name, None) if _hack_lg is not None: groups.remove(_hack_lg) else: print(" TITS! Group '{}' will be left over...".format(group_name)) 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(bo, 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 we can enter Edit Mode(TM) toggle.track(bo, "hide", 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(bo): return 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, toggle): if isinstance(objs, bpy.types.Object): toggle.track(objs, "hide_render", False) for i in bpy.data.objects: i.select = i == objs if isinstance(i.data, bpy.types.Mesh) and not self._has_valid_material(i): toggle.track(i, "hide_render", True) else: for i in bpy.data.objects: value = i in objs if value: toggle.track(i, "hide_render", False) 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.groups: if i.name.startswith("_LIGHTMAPGEN"): i.user_clear() bpy.data.groups.remove(i) for i in bpy.data.images: if i.name.endswith("_LIGHTMAPGEN.png"): i.user_clear() 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)