diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 842f0a7..edf378e 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -53,7 +53,8 @@ class Exporter: self.animation = animation.AnimationConverter(self) self.sumfile = sumfile.SumFile() - # Step 0.9: Init the progress mgr + # Step 0.8: Init the progress mgr + 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: @@ -65,44 +66,46 @@ class Exporter: self.report.progress_add_step("Composing Geometry") self.report.progress_start("EXPORTING AGE") - # Step 1: Create the age info and the pages - self._export_age_info() + # Step 0.9: Apply modifiers to all meshes temporarily. + with self.mesh: + # Step 1: Create the age info and the pages + self._export_age_info() - # Step 2: Gather a list of objects that we need to export, given what the user has told - # us to export (both in the Age and Object Properties)... fun - self._collect_objects() + # Step 2: Gather a list of objects that we need to export, given what the user has told + # us to export (both in the Age and Object Properties)... fun + self._collect_objects() - # Step 2.5: Run through all the objects we collected in Step 2 and see if any relationships - # that the artist made requires something to have a CoordinateInterface - self._harvest_actors() + # Step 2.5: Run through all the objects we collected in Step 2 and see if any relationships + # 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 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() + # Step 3: Export all the things! + self._export_scene_objects() - # Step 3.1: Ensure referenced logic node trees are exported - self._export_referenced_node_trees() + # Step 3.1: Ensure referenced logic node trees are exported + self._export_referenced_node_trees() - # Step 3.2: Now that all Plasma Objects (save Mipmaps) are exported, we do any post - # processing that needs to inspect those objects - self._post_process_scene_objects() + # Step 3.2: Now that all Plasma Objects (save Mipmaps) are exported, we do any post + # processing that needs to inspect those objects + self._post_process_scene_objects() - # Step 4: Finalize... - self.mesh.material.finalize() - self.mesh.finalize() + # Step 4: Finalize... + self.mesh.material.finalize() + self.mesh.finalize() - # Step 5: FINALLY. Let's write the PRPs and crap. - self.mgr.save_age(Path(self._op.filepath)) + # Step 5: FINALLY. Let's write the PRPs and crap. + self.mgr.save_age(Path(self._op.filepath)) - # Step 5.1: Save out the export report. - # If the export fails and this doesn't save, we have bigger problems than - # these little warnings and notices. - self.report.progress_end() - self.report.save() + # Step 5.1: Save out the export report. + # If the export fails and this doesn't save, we have bigger problems than + # these little warnings and notices. + self.report.progress_end() + self.report.save() def _bake_static_lighting(self): oven = etlight.LightBaker(self.report) diff --git a/korman/exporter/etlight.py b/korman/exporter/etlight.py index 6898e2a..119242b 100644 --- a/korman/exporter/etlight.py +++ b/korman/exporter/etlight.py @@ -17,24 +17,25 @@ import bpy from bpy.app.handlers import persistent from .logger import ExportProgressLogger -from .mesh import _VERTEX_COLOR_LAYERS +from .mesh import _MeshManager, _VERTEX_COLOR_LAYERS from ..helpers import * _NUM_RENDER_LAYERS = 20 -class LightBaker: +class LightBaker(_MeshManager): """ExportTime Lighting""" def __init__(self, report=None): self._lightgroups = {} if report is None: self._report = ExportProgressLogger() - self.add_progress_steps(self._report) + self.add_progress_steps(self._report, True) self._report.progress_start("PREVIEWING LIGHTING") self._own_report = True else: self._report = report self._own_report = False + super().__init__(self._report) self._uvtexs = {} def __del__(self): @@ -42,7 +43,9 @@ class LightBaker: self._report.progress_end() @staticmethod - def add_progress_steps(report): + def add_progress_steps(report, add_base=False): + if add_base: + _MeshManager.add_progress_presteps(report) report.progress_add_step("Searching for Bahro") report.progress_add_step("Baking Static Lighting") diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index 711243c..7161220 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -18,6 +18,7 @@ from PyHSPlasma import * from math import fabs import weakref +from ..exporter.logger import ExportProgressLogger from . import explosions from .. import helpers from . import material @@ -111,7 +112,71 @@ class _GeoData: self.vertices = [] -class MeshConverter: + +class _MeshManager: + def __init__(self, report=None): + if report is not None: + self._report = report + self._overrides = {} + + @staticmethod + def add_progress_presteps(report): + report.progress_add_step("Applying Blender Mods") + + def _build_prop_dict(self, bstruct): + props = {} + for i in bstruct.bl_rna.properties: + ident = i.identifier + if ident == "rna_type": + continue + props[ident] = getattr(bstruct, ident) if getattr(i, "array_length", 0) == 0 else tuple(getattr(bstruct, ident)) + return props + + def __enter__(self): + self._report.progress_advance() + self._report.progress_range = len(bpy.data.objects) + + # Some modifiers like "Array" will procedurally generate new geometry that will impact + # lightmap generation. The Blender Internal renderer does not seem to be smart enough to + # take this into account. Thus, we temporarily apply modifiers to ALL meshes (even ones that + # are not exported) such that we can generate proper lighting. + scene = bpy.context.scene + for i in bpy.data.objects: + if i.type == "MESH" and i.is_modified(scene, "RENDER"): + # Remember, storing actual pointers to the Blender objects can cause bad things to + # happen because Blender's memory management SUCKS! + self._overrides[i.name] = { "mesh": i.data.name, "modifiers": [] } + i.data = i.to_mesh(scene, True, "RENDER", calc_tessface=False) + + # If the modifiers are left on the object, the lightmap bake can break under some + # situations. Therefore, we now cache the modifiers and clear them away... + if i.plasma_object.enabled: + cache_mods = self._overrides[i.name]["modifiers"] + for mod in i.modifiers: + cache_mods.append(self._build_prop_dict(mod)) + i.modifiers.clear() + self._report.progress_increment() + return self + + def __exit__(self, type, value, traceback): + data_bos, data_meshes = bpy.data.objects, bpy.data.meshes + for obj_name, override in self._overrides.items(): + bo = data_bos.get(obj_name) + + # Reapply the old mesh + trash_mesh, bo.data = bo.data, data_meshes.get(override["mesh"]) + data_meshes.remove(trash_mesh) + + # If modifiers were removed, reapply them now. + for cached_mod in override["modifiers"]: + mod = bo.modifiers.new(cached_mod["name"], cached_mod["type"]) + for key, value in cached_mod.items(): + if key in {"name", "type"}: + continue + setattr(mod, key, value) + + +class MeshConverter(_MeshManager): def __init__(self, exporter): self._exporter = weakref.ref(exporter) self.material = material.MaterialConverter(exporter) @@ -119,6 +184,9 @@ class MeshConverter: self._dspans = {} self._mesh_geospans = {} + # _report is a property on this subclass + super().__init__() + def _calc_num_uvchans(self, bo, mesh): max_user_texs = plGeometrySpan.kUVCountMask num_user_texs = len(mesh.tessface_uv_textures) @@ -403,8 +471,11 @@ class MeshConverter: diface.addDrawable(dspan_key, idx) def _export_mesh(self, bo): - # Step 0.7: Update the mesh such that we can do things and schtuff... - mesh = bo.to_mesh(bpy.context.scene, True, "RENDER", calc_tessface=True) + # Previously, this called bo.to_mesh to apply modifiers. However, due to limitations in the + # lightmap generation, this is now done for all modified mesh objects before any Plasma data + # is exported. + mesh = bo.data + mesh.calc_tessface() # Step 0.8: Figure out which materials are attached to this object. Because Blender is backwards, # we can actually have materials that are None. gotdawgit!!! @@ -412,33 +483,32 @@ class MeshConverter: if not materials: return None - with helpers.TemporaryObject(mesh, bpy.data.meshes.remove): - # Step 1: Export all of the doggone materials. - geospans = self._export_material_spans(bo, mesh, materials) - - # Step 2: Export Blender mesh data to Plasma GeometrySpans - self._export_geometry(bo, mesh, materials, geospans) - - # Step 3: Add plGeometrySpans to the appropriate DSpan and create indices - _diindices = {} - for geospan, pass_index in geospans: - dspan = self._find_create_dspan(bo, geospan.material.object, pass_index) - self._report.msg("Exported hsGMaterial '{}' geometry into '{}'", - geospan.material.name, dspan.key.name, indent=1) - idx = dspan.addSourceSpan(geospan) - if dspan not in _diindices: - _diindices[dspan] = [idx,] - else: - _diindices[dspan].append(idx) - - # Step 3.1: Harvest Span indices and create the DIIndices - drawables = [] - for dspan, indices in _diindices.items(): - dii = plDISpanIndex() - dii.indices = indices - idx = dspan.addDIIndex(dii) - drawables.append((dspan.key, idx)) - return drawables + # Step 1: Export all of the doggone materials. + geospans = self._export_material_spans(bo, mesh, materials) + + # Step 2: Export Blender mesh data to Plasma GeometrySpans + self._export_geometry(bo, mesh, materials, geospans) + + # Step 3: Add plGeometrySpans to the appropriate DSpan and create indices + _diindices = {} + for geospan, pass_index in geospans: + dspan = self._find_create_dspan(bo, geospan.material.object, pass_index) + self._report.msg("Exported hsGMaterial '{}' geometry into '{}'", + geospan.material.name, dspan.key.name, indent=1) + idx = dspan.addSourceSpan(geospan) + if dspan not in _diindices: + _diindices[dspan] = [idx,] + else: + _diindices[dspan].append(idx) + + # Step 3.1: Harvest Span indices and create the DIIndices + drawables = [] + for dspan, indices in _diindices.items(): + dii = plDISpanIndex() + dii.indices = indices + idx = dspan.addDIIndex(dii) + drawables.append((dspan.key, idx)) + return drawables def _export_material_spans(self, bo, mesh, materials): """Exports all Materials and creates plGeometrySpans""" diff --git a/korman/operators/op_lightmap.py b/korman/operators/op_lightmap.py index 4cad03d..bdbdd57 100644 --- a/korman/operators/op_lightmap.py +++ b/korman/operators/op_lightmap.py @@ -41,10 +41,10 @@ class LightmapAutobakePreviewOperator(_LightingOperator, bpy.types.Operator): with GoodNeighbor() as toggle: toggle.track(context.scene, "layers", tuple(context.scene.layers)) - bake = LightBaker() - if not bake.bake_static_lighting([context.active_object,]): - self.report({"INFO"}, "No valid lights found to bake.") - return {"FINISHED"} + with LightBaker() as bake: + if not bake.bake_static_lighting([context.active_object,]): + self.report({"INFO"}, "No valid lights found to bake.") + return {"FINISHED"} tex = bpy.data.textures.get("LIGHTMAPGEN_PREVIEW") if tex is None: