diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 98f1cea..b3133cc 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -61,7 +61,7 @@ class Exporter: self.mesh.add_progress_presteps(self.report) self.report.progress_add_step("Collecting Objects") self.report.progress_add_step("Harvesting Actors") - if self._op.bake_lighting: + if self._op.lighting_method != "skip": etlight.LightBaker.add_progress_steps(self.report) self.report.progress_add_step("Exporting Scene Objects") self.report.progress_add_step("Exporting Logic Nodes") @@ -86,8 +86,7 @@ class Exporter: # 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() + self._bake_static_lighting() # Step 3: Export all the things! self._export_scene_objects() @@ -113,8 +112,10 @@ class Exporter: self.report.save() def _bake_static_lighting(self): - oven = etlight.LightBaker(self.report) - oven.bake_static_lighting(self._objects) + lighting_method = self._op.lighting_method + if lighting_method != "skip": + oven = etlight.LightBaker(self.report) + oven.bake_static_lighting(self._objects) def _collect_objects(self): scene = bpy.context.scene diff --git a/korman/exporter/etlight.py b/korman/exporter/etlight.py index 402e870..9295b08 100644 --- a/korman/exporter/etlight.py +++ b/korman/exporter/etlight.py @@ -78,9 +78,9 @@ class LightBaker(_MeshManager): self._select_only(objs, toggle) bpy.ops.object.bake_image() - def _bake_vcols(self, objs): + def _bake_vcols(self, objs, layers): with GoodNeighbor() as toggle: - bpy.context.scene.layers = (True,) * _NUM_RENDER_LAYERS + bpy.context.scene.layers = layers self._apply_render_settings(toggle, True) self._select_only(objs, toggle) bpy.ops.object.bake_image() @@ -147,14 +147,15 @@ class LightBaker(_MeshManager): if value: if key[0] == "lightmap": num_objs = len(value) - self._report.msg("{} Lightmap(s) [H:{:X}]", num_objs, hash(key), indent=1) + self._report.msg("{} Lightmap(s) [H:{:X}]", num_objs, hash(key[1:]), indent=1) if largest_pass > 1 and num_objs < round(largest_pass * 0.02): - obj_names = ", ".join((i.name for i in value)) - self._report.warn("Small lightmap pass! Objects: {}".format(obj_names), indent=2) + pass_names = set((i.plasma_modifiers.lightmap.bake_pass_name for i in value)) + pass_msg = ", ".join(pass_names) + self._report.warn("Small lightmap bake pass! Bake Pass(es): {}".format(pass_msg), indent=2) self._bake_lightmaps(value, key[1:]) elif key[0] == "vcol": - self._report.msg("{} Crap Light(s)", len(value), indent=1) - self._bake_vcols(value) + self._report.msg("{} Vertex Color(s) [H:{:X}]", len(value), hash(key[1:]), indent=1) + self._bake_vcols(value, key[1:]) else: raise RuntimeError(key[0]) inc_progress() @@ -219,8 +220,9 @@ class LightBaker(_MeshManager): 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",): [] } - bake_vcol = bake[("vcol",)] + default_layers = tuple((True,) * _NUM_RENDER_LAYERS) + 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": @@ -231,15 +233,24 @@ class LightBaker(_MeshManager): mods = i.plasma_modifiers lightmap_mod = mods.lightmap if lightmap_mod.enabled: + if lightmap_mod.bake_pass_name: + bake_pass = bake_passes.get(lightmap_mod.bake_pass_name, None) + if bake_pass is None: + raise ExportError("Bake Lighting '{}': Could not find pass '{}'".format(i.name, lightmap_mod.bake_pass_name)) + lm_layers = tuple(bake_pass.render_layers) + else: + lm_layers = default_layers + # In order for Blender to be able to bake this properly, at least one of the # layers this object is on must be selected. We will sanity check this now. - lm_layers, obj_layers = tuple(lightmap_mod.render_layers), tuple(i.layers) + obj_layers = tuple(i.layers) lm_active_layers = set((i for i, value in enumerate(lm_layers) if value)) obj_active_layers = set((i for i, value in enumerate(obj_layers) if value)) if not lm_active_layers & obj_active_layers: - raise ExportError("Lightmap '{}': At least one layer the object is on must be selected".format(i.name)) + raise ExportError("Bake Lighting '{}': At least one layer the object is on must be selected".format(i.name)) - key = ("lightmap",) + lm_layers + 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: @@ -356,9 +367,11 @@ class LightBaker(_MeshManager): def _prep_for_vcols(self, bo, toggle): mesh = bo.data + modifier = bo.plasma_modifiers.lightmap vcols = mesh.vertex_colors # Create a special light group for baking + user_lg = modifier.lights if modifier.enabled else None if not self._generate_lightgroup(bo): return False diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index c7bb955..b321d54 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -195,7 +195,8 @@ class MeshConverter(_MeshManager): # Lightmapping requires its own LIGHTMAPGEN channel # NOTE: the LIGHTMAPGEN texture has already been created, so it is in num_user_texs - if bo.plasma_modifiers.lightmap.enabled: + lm = bo.plasma_modifiers.lightmap + if lm.enabled and lm.bake_type == "lightmap": num_user_texs -= 1 max_user_texs -= 1 @@ -261,12 +262,13 @@ class MeshConverter(_MeshManager): bumpmap = self.material.get_bump_layer(bo) # Locate relevant vertex color layers now... + lm = bo.plasma_modifiers.lightmap color, alpha = None, None for vcol_layer in mesh.tessface_vertex_colors: name = vcol_layer.name.lower() if name in _VERTEX_COLOR_LAYERS: color = vcol_layer.data - elif name == "autocolor" and color is None and not bo.plasma_modifiers.lightmap.enabled: + elif name == "autocolor" and color is None and not lm.bake_lightmap: color = vcol_layer.data elif name == "alpha": alpha = vcol_layer.data diff --git a/korman/operators/op_export.py b/korman/operators/op_export.py index 97f2e44..7134114 100644 --- a/korman/operators/op_export.py +++ b/korman/operators/op_export.py @@ -37,10 +37,6 @@ class ExportOperator(bpy.types.Operator): "description": "Profiles the exporter using cProfile", "default": False}), - "bake_lighting": (BoolProperty, {"name": "Bake Static Lights", - "description": "Bake all lightmaps and vertex shading on export", - "default": True}), - "verbose": (BoolProperty, {"name": "Display Verbose Log", "description": "Shows the verbose export log in the console", "default": False}), @@ -58,6 +54,18 @@ class ExportOperator(bpy.types.Operator): ("use", "Use Texture Cache", "Use (and update, if needed) cached textures."), ("rebuild", "Rebuild Texture Cache", "Rebuilds the texture cache from scratch.")], "default": "use"}), + + "lighting_method": (EnumProperty, {"name": "Static Lighting", + "description": "Static Lighting Settings", + "items": [("skip", "Don't Bake Lighting", "Static lighting is not baked during this export (fastest export)"), + ("bake", "Bake Lighting", "Static lighting is baked according to your specifications"), + ("force_vcol", "Force Vertex Color Bake", "All static lighting is baked as vertex colors (faster export)"), + ("force_lightmap", "Force Lightmap Bake", "All static lighting is baked as lightmaps (slower export)")], + "default": "bake"}), + + "export_active": (BoolProperty, {"name": "INTERNAL: Export currently running", + "default": False, + "options": {"SKIP_SAVE"}}), } # This wigs out and very bad things happen if it's not directly on the operator... @@ -77,7 +85,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(self, "version") layout.prop(age, "texcache_method", text="") - layout.prop(age, "bake_lighting") + layout.prop(age, "lighting_method") row = layout.row() row.enabled = ConsoleToggler.is_platform_supported() row.prop(age, "show_console") @@ -89,6 +97,12 @@ class ExportOperator(bpy.types.Operator): return getattr(bpy.context.scene.world.plasma_age, attr) raise AttributeError(attr) + def __setattr__(self, attr, value): + if attr in self._properties: + setattr(bpy.context.scene.world.plasma_age, attr, value) + else: + super().__setattr__(attr, value) + @property def has_reports(self): return hasattr(self.report) @@ -120,6 +134,7 @@ class ExportOperator(bpy.types.Operator): with _UiHelper(context) as _ui: e = exporter.Exporter(self) try: + self.export_active = True if self.profile_export: profile = path.with_name("{}_cProfile".format(ageName)) profile = cProfile.runctx("e.run()", globals(), locals(), str(profile)) @@ -136,6 +151,8 @@ class ExportOperator(bpy.types.Operator): stats = stats.sort_stats("time", "calls") stats.print_stats() return {"FINISHED"} + finally: + self.export_active = False def invoke(self, context, event): # Called when a user hits "export" from the menu @@ -157,7 +174,8 @@ class ExportOperator(bpy.types.Operator): for name, (prop, options) in cls._properties.items(): # Hide these settings from being seen on the age properties age_options = dict(options) - age_options["options"] = {"HIDDEN"} + bl_options = age_options.setdefault("options", set()) + bl_options.add("HIDDEN") # Now do the majick setattr(PlasmaAge, name, prop(**age_options)) diff --git a/korman/properties/__init__.py b/korman/properties/__init__.py index 4da436b..d8ea5e6 100644 --- a/korman/properties/__init__.py +++ b/korman/properties/__init__.py @@ -20,6 +20,7 @@ from .prop_image import * from .prop_lamp import * from . import modifiers from .prop_object import * +from .prop_scene import * from .prop_texture import * from .prop_world import * @@ -30,6 +31,7 @@ def register(): bpy.types.Lamp.plasma_lamp = bpy.props.PointerProperty(type=PlasmaLamp) bpy.types.Object.plasma_net = bpy.props.PointerProperty(type=PlasmaNet) bpy.types.Object.plasma_object = bpy.props.PointerProperty(type=PlasmaObject) + bpy.types.Scene.plasma_scene = bpy.props.PointerProperty(type=PlasmaScene) bpy.types.Texture.plasma_layer = bpy.props.PointerProperty(type=PlasmaLayer) bpy.types.World.plasma_age = bpy.props.PointerProperty(type=PlasmaAge) bpy.types.World.plasma_fni = bpy.props.PointerProperty(type=PlasmaFni) diff --git a/korman/properties/modifiers/render.py b/korman/properties/modifiers/render.py index bd4ae04..4378ddc 100644 --- a/korman/properties/modifiers/render.py +++ b/korman/properties/modifiers/render.py @@ -142,12 +142,14 @@ class PlasmaFollowMod(idprops.IDPropObjectMixin, PlasmaModifierProperties): return True -class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties): +class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties, PlasmaModifierUpgradable): pl_id = "lightmap" bl_category = "Render" - bl_label = "Lightmap" - bl_description = "Auto-Bake Lightmap" + bl_label = "Bake Lighting" + bl_description = "Auto-Bake Static Lighting" + + deprecated_properties = {"render_layers"} quality = EnumProperty(name="Quality", description="Resolution of lightmap", @@ -156,15 +158,28 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties): ("256", "256px", "256x256 pixels"), ("512", "512px", "512x512 pixels"), ("1024", "1024px", "1024x1024 pixels"), + ("2048", "2048px", "2048x2048 pixels"), ]) + bake_type = EnumProperty(name="Bake To", + description="Destination for baked lighting data", + items=[ + ("lightmap", "Lightmap Texture", "Bakes lighting to a lightmap texture"), + ("vcol", "Vertex Colors", "Bakes lighting to vertex colors"), + ], + options=set()) + render_layers = BoolVectorProperty(name="Layers", - description="Render layers to use for baking", - options=set(), + description="DEPRECATED: Render layers to use for baking", + options={"HIDDEN"}, subtype="LAYER", size=_NUM_RENDER_LAYERS, default=((True,) * _NUM_RENDER_LAYERS)) + bake_pass_name = StringProperty(name="Bake Pass", + description="Pass in which to bake lighting", + options=set()) + lights = PointerProperty(name="Light Group", description="Group that defines the collection of lights to bake", type=bpy.types.Group) @@ -172,7 +187,26 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties): uv_map = StringProperty(name="UV Texture", description="UV Texture used as the basis for the lightmap") + @property + def bake_lightmap(self): + if not self.enabled: + return False + age = bpy.context.scene.world.plasma_age + if age.export_active: + if age.lighting_method == "force_lightmap": + return True + elif self.bake_type == "lightmap" and age.lighting_method == "bake": + return True + else: + return False + else: + return self.bake_type == "lightmap" + def export(self, exporter, bo, so): + # If we're exporting vertex colors, who gives a rat's behind? + 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) @@ -229,16 +263,38 @@ class PlasmaLightMapGen(idprops.IDPropMixin, PlasmaModifierProperties): def key_name(self): return "{}_LIGHTMAPGEN".format(self.id_data.name) + @property + def latest_version(self): + return 2 + @property def resolution(self): return int(self.quality) + def upgrade(self): + # In version 1, bake passes were assigned on a per modifier basis by setting + # the view layers on the modifier. Version 2 moves them into a global list + # that can be selected by name in the modifier + if self.current_version < 2: + bake_passes = bpy.context.scene.plasma_scene.bake_passes + render_layers = tuple(self.render_layers) + + # Try to find a render pass matching, if possible... + bake_pass = next((i for i in bake_passes if tuple(i.render_layers) == render_layers), None) + if bake_pass is None: + bake_pass = bake_passes.add() + bake_pass.display_name = "Pass {}".format(len(bake_passes)) + bake_pass.render_layers = render_layers + self.bake_pass_name = bake_pass.display_name + self.property_unset("render_layers") + self.current_version = 2 + class PlasmaLightingMod(PlasmaModifierProperties): pl_id = "lighting" bl_category = "Render" - bl_label = "Lighting" + bl_label = "Lighting Info" bl_description = "Fine tune Plasma lighting settings" force_rt_lights = BoolProperty(name="Force RT Lighting", @@ -252,10 +308,10 @@ class PlasmaLightingMod(PlasmaModifierProperties): @property def allow_preshade(self): - bo = self.id_data - if bo.plasma_modifiers.water_basic.enabled: + mods = self.id_data.plasma_modifiers + if mods.water_basic.enabled: return False - if bo.plasma_modifiers.lightmap.enabled: + if mods.lightmap.bake_lightmap: return False return True @@ -284,12 +340,12 @@ class PlasmaLightingMod(PlasmaModifierProperties): @property def want_rt_lights(self): """Gets whether or not this object ought to be lit dynamically""" - bo = self.id_data - if bo.plasma_modifiers.lightmap.enabled: + mods = self.id_data.plasma_modifiers + if mods.lightmap.enabled and mods.lightmap.bake_type == "lightmap": return False - if bo.plasma_modifiers.water_basic.enabled: + if mods.water_basic.enabled: return True - if bo.plasma_object.has_transform_animation: + if self.id_data.plasma_object.has_transform_animation: return True return False diff --git a/korman/properties/prop_scene.py b/korman/properties/prop_scene.py new file mode 100644 index 0000000..074df1e --- /dev/null +++ b/korman/properties/prop_scene.py @@ -0,0 +1,46 @@ +# 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.props import * + +from ..exporter.etlight import _NUM_RENDER_LAYERS + +class PlasmaBakePass(bpy.types.PropertyGroup): + def _get_display_name(self): + return self.name + def _set_display_name(self, value): + for i in bpy.data.objects: + lm = i.plasma_modifiers.lightmap + if lm.bake_pass_name == self.name: + lm.bake_pass_name = value + self.name = value + + display_name = StringProperty(name="Pass Name", + get=_get_display_name, + set=_set_display_name, + options=set()) + + render_layers = BoolVectorProperty(name="Layers to Bake", + description="Render layers to use for baking", + options=set(), + subtype="LAYER", + size=_NUM_RENDER_LAYERS, + default=((True,) * _NUM_RENDER_LAYERS)) + + +class PlasmaScene(bpy.types.PropertyGroup): + bake_passes = CollectionProperty(type=PlasmaBakePass) + active_pass_index = IntProperty(options={"HIDDEN"}) diff --git a/korman/ui/__init__.py b/korman/ui/__init__.py index d1ef238..3369bb7 100644 --- a/korman/ui/__init__.py +++ b/korman/ui/__init__.py @@ -20,6 +20,7 @@ from .ui_list import * from .ui_menus import * from .ui_modifiers import * from .ui_object import * +from .ui_render_layer import * from .ui_texture import * from .ui_toolbox import * from .ui_world import * diff --git a/korman/ui/modifiers/render.py b/korman/ui/modifiers/render.py index df55ee4..482c412 100644 --- a/korman/ui/modifiers/render.py +++ b/korman/ui/modifiers/render.py @@ -16,6 +16,7 @@ import bpy from .. import ui_list +from ...exporter.mesh import _VERTEX_COLOR_LAYERS def fademod(modifier, layout, context): layout.prop(modifier, "fader_type") @@ -73,27 +74,42 @@ def lighting(modifier, layout, context): col.label("Other Plasma lights {} be cast at runtime.".format("will" if modifier.rt_lights else "will NOT"), icon="LAYER_USED") + map_type = "a lightmap" if lightmap.enabled and lightmap.bake_type == "lightmap" else "vertex colors" if lightmap.enabled and lightmap.lights: - col.label(" All '{}' lights will be baked to a lightmap".format(lightmap.lights), + col.label("All '{}' lights will be baked to {}".format(lightmap.lights.name, map_type), icon="LAYER_USED") elif have_static_lights: light_type = "Blender-only" if modifier.rt_lights else "unanimated" - map_type = "a lightmap" if lightmap.enabled else "vertex colors" col.label("Other {} lights will be baked to {}.".format(light_type, map_type), icon="LAYER_USED") else: col.label("No static lights will be baked.", icon="LAYER_USED") def lightmap(modifier, layout, context): - layout.row(align=True).prop(modifier, "quality", expand=True) - layout.prop(modifier, "render_layers", text="Active Render Layers") + pl_scene = context.scene.plasma_scene + is_texture = modifier.bake_type == "lightmap" + + layout.prop(modifier, "bake_type") + if modifier.bake_type == "vcol": + col_layer = next((i for i in modifier.id_data.data.vertex_colors if i.name.lower() in _VERTEX_COLOR_LAYERS), None) + if col_layer is not None: + layout.label("Mesh color layer '{}' will override this lighting.".format(col_layer.name), icon="ERROR") + + col = layout.column() + col.active = is_texture + col.prop(modifier, "quality") + layout.prop_search(modifier, "bake_pass_name", pl_scene, "bake_passes", icon="RENDERLAYERS") layout.prop(modifier, "lights") - layout.prop_search(modifier, "uv_map", context.active_object.data, "uv_textures") + col = layout.column() + col.active = is_texture + col.prop_search(modifier, "uv_map", context.active_object.data, "uv_textures") # Lightmaps can only be applied to objects with opaque materials. - if any((i.use_transparency for i in modifier.id_data.data.materials if i is not None)): + 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: - operator = layout.operator("object.plasma_lightmap_preview", "Preview Lightmap", icon="RENDER_STILL") + col = layout.column() + col.active = is_texture + operator = col.operator("object.plasma_lightmap_preview", "Preview Lightmap", icon="RENDER_STILL") # 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 diff --git a/korman/ui/ui_render_layer.py b/korman/ui/ui_render_layer.py new file mode 100644 index 0000000..d87f04b --- /dev/null +++ b/korman/ui/ui_render_layer.py @@ -0,0 +1,53 @@ +# 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 . import ui_list + +class RenderLayerButtonsPanel: + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render_layer" + + @classmethod + def poll(cls, context): + return context.scene.render.engine == "PLASMA_GAME" + + +class BakePassUI(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0): + layout.prop(item, "display_name", emboss=False, text="", icon="RENDERLAYERS") + + +class PlasmaBakePassPanel(RenderLayerButtonsPanel, bpy.types.Panel): + bl_label = "Plasma Bake Passes" + + def draw(self, context): + layout = self.layout + scene = context.scene.plasma_scene + + ui_list.draw_list(layout, "BakePassUI", "scene", scene, "bake_passes", + "active_pass_index", name_prefix="Pass", + name_prop="display_name", rows=3, maxrows=3) + + active_pass_index = scene.active_pass_index + try: + bake_pass = scene.bake_passes[active_pass_index] + except: + pass + else: + box = layout.box() + box.prop(bake_pass, "display_name") + box.prop(bake_pass, "render_layers") diff --git a/korman/ui/ui_world.py b/korman/ui/ui_world.py index 6cac4e4..1400280 100644 --- a/korman/ui/ui_world.py +++ b/korman/ui/ui_world.py @@ -133,18 +133,18 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel): col = split.column() col.label("Export Settings:") - col.prop(age, "texcache_method", text="") - col.prop(age, "bake_lighting") - cons_ui = col.column() - cons_ui.enabled = ConsoleToggler.is_platform_supported() - cons_ui.prop(age, "verbose") - cons_ui.prop(age, "show_console") + col.enabled = ConsoleToggler.is_platform_supported() + col.prop(age, "verbose") + col.prop(age, "show_console") col = split.column() col.label("Plasma Settings:") col.prop(age, "age_sdl") col.prop(age, "use_texture_page") + layout.separator() + layout.prop(age, "lighting_method") + layout.prop(age, "texcache_method") class PlasmaEnvironmentPanel(AgeButtonsPanel, bpy.types.Panel):