diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py index 720c2b3..e03725c 100644 --- a/korman/exporter/convert.py +++ b/korman/exporter/convert.py @@ -38,7 +38,7 @@ class Exporter: with logger.ExportLogger("{}_export.log".format(self.age_name)) as _log: # Step 0: Init export resmgr and stuff self.mgr = manager.ExportManager(globals()[self._op.version]) - self.mesh = mesh.MeshConverter() + self.mesh = mesh.MeshConverter(self.mgr) self.report = logger.ExportAnalysis() # Step 1: Gather a list of objects that we need to export @@ -56,6 +56,9 @@ class Exporter: # Step 3: Export all the things! self._export_scene_objects() + # Step 3.9: Finalize geometry... + self.mesh.finalize() + # Step 4: FINALLY. Let's write the PRPs and crap. self.mgr.save_age(self._op.filepath) @@ -103,8 +106,7 @@ class Exporter: parent_ci = self.mgr.find_create_key(parent, plCoordinateInterface).object parent_ci.addChild(so.key) else: - self.report.warn("oversight", - "You have parented Plasma Object '{}' to '{}', which has not been marked for export. \ + self.report.warn("You have parented Plasma Object '{}' to '{}', which has not been marked for export. \ The object may not appear in the correct location or animate properly.".format( bo.name, parent.name)) @@ -153,5 +155,4 @@ class Exporter: pass def _export_mesh_blobj(self, so, bo): - # TODO - pass \ No newline at end of file + so.draw = self.mesh.export_object(bo) \ No newline at end of file diff --git a/korman/exporter/explosions.py b/korman/exporter/explosions.py index 0198c0a..5e08df5 100644 --- a/korman/exporter/explosions.py +++ b/korman/exporter/explosions.py @@ -18,6 +18,14 @@ class ExportError(Exception): super(Exception, self).__init__(value) +class TooManyVerticesError(ExportError): + def __init__(self, mesh, matname, vertcount): + msg = "There are too many vertices ({}) on the mesh data '{}' associated with material '{}'".format( + vertcount, mesh, matname + ) + super(ExportError, self).__init__(msg) + + class UndefinedPageError(ExportError): mistakes = {} diff --git a/korman/exporter/logger.py b/korman/exporter/logger.py index 2145ddb..f6dd6ab 100644 --- a/korman/exporter/logger.py +++ b/korman/exporter/logger.py @@ -21,26 +21,20 @@ class ExportAnalysis: to look through all of the gobbledygook in the export log. """ - _notices = {} - _warnings = {} + _porting = [] + _warnings = [] def save(self): # TODO pass - def _stash(self, _d, category, message): - if category not in _d: - _d[category] = [message,] - else: - _d[category].append(message) + def port(self, message): + self._porting.append(message) + print("PORTING: {}".format(message)) - def note(self, category, message): - self._stash(self._notices, category, message) - print("NOTICE {}: {}".format(category, message)) - - def warn(self, category, message): - self._stash(self._warnings, category, message) - print("WARNING {}: {}".format(category, message)) + def warn(self, message): + self._warnings.append(message) + print("WARNING: {}".format(message)) class ExportLogger: diff --git a/korman/exporter/manager.py b/korman/exporter/manager.py index 2fe7e07..bf81afd 100644 --- a/korman/exporter/manager.py +++ b/korman/exporter/manager.py @@ -19,6 +19,27 @@ from PyHSPlasma import * from . import explosions +# These objects have to be in the plSceneNode pool in order to be loaded... +# NOTE: We are using Factory indices because I doubt all of these classes are implemented. +_pool_types = ( + plFactory.ClassIndex("plPostEffectMod"), + plFactory.ClassIndex("pfGUIDialogMod"), + plFactory.ClassIndex("plMsgForwarder"), + plFactory.ClassIndex("plClothingItem"), + plFactory.ClassIndex("plArmatureEffectFootSound"), + plFactory.ClassIndex("plDynaFootMgr"), + plFactory.ClassIndex("plDynaRippleMgr"), + plFactory.ClassIndex("plDynaBulletMgr"), + plFactory.ClassIndex("plDynaPuddleMgr"), + plFactory.ClassIndex("plATCAnim"), + plFactory.ClassIndex("plEmoteAnim"), + plFactory.ClassIndex("plDynaRippleVSMgr"), + plFactory.ClassIndex("plDynaTorpedoMgr"), + plFactory.ClassIndex("plDynaTorpedoVSMgr"), + plFactory.ClassIndex("plClusterGroup"), +) + + class ExportManager: """Friendly resource-managing helper class.""" @@ -49,8 +70,8 @@ class ExportManager: location = self._pages[bl.plasma_object.page] # pl can be a class or an instance. - # This is one of those "sanity" things to ensure we don't suddenly - # passing around the key of an uninitialized object. + # This is one of those "sanity" things to ensure we don't suddenly startpassing around the + # key of an uninitialized object. if isinstance(pl, type(object)): assert name or bl if name is None: @@ -62,11 +83,7 @@ class ExportManager: if node: # All objects must be in the scene node if isinstance(pl, plSceneObject): node.addSceneObject(pl.key) - else: - # FIXME: determine which types belong here... - # Probably anything that's not a plModifier or a plBitmap... - # Remember that the idea is that Plasma needs to deliver refs to load the age. - # It's harmless to have too many refs here (though the file size will be big, heh) + elif pl.ClassIndex() in _pool_types: node.addPoolObject(pl.key) return pl # we may have created it... @@ -125,6 +142,14 @@ class ExportManager: return key return None + def get_location(self, bl_obj): + """Returns the Page Location of a given Blender Object""" + return self._pages[bl_obj.plasma_object.page] + + def get_scene_node(self, location): + """Gets a Plasma Page's plSceneNode key""" + return self._nodes[location].key + def get_textures_page(self, obj): """Returns the page that Blender Object obj's textures should be exported to""" # The point of this is to account for per-page textures... diff --git a/korman/exporter/mesh.py b/korman/exporter/mesh.py index 21ae4f3..53e90c8 100644 --- a/korman/exporter/mesh.py +++ b/korman/exporter/mesh.py @@ -16,6 +16,240 @@ import bpy from PyHSPlasma import * +from . import explosions +from . import utils + +_MAX_VERTS_PER_SPAN = 0xFFFF +_WARN_VERTS_PER_SPAN = 0x8000 + +class _RenderLevel: + MAJOR_OPAQUE = 0 + MAJOR_FRAMEBUF = 1 + MAJOR_DEFAULT = 2 + MAJOR_BLEND = 4 + MAJOR_LATE = 8 + + _MAJOR_SHIFT = 28 + _MINOR_MASK = ((1 << _MAJOR_SHIFT) - 1) + + def __init__(self): + self.level = 0 + + def __hash__(self): + return hash(self.level) + + def _get_major(self): + return self.level >> _MAJOR_SHIFT + def _set_major(self, value): + self.level = ((value << _MAJOR_SHIFT) & 0xFFFFFFFF) | self.minor + major = property(_get_major, _set_major) + + def _get_minor(self): + return self.level & _MINOR_MASK + def _set_minor(self, value): + self.level = ((self.major << _MAJOR_SHIFT) & 0xFFFFFFFF) | value + minor = property(_get_minor, _set_minor) + + +class _DrawableCriteria: + def __init__(self, hsgmat): + _layer = hsgmat.layers[0].object # better doggone well have a layer... + self.blend_span = bool(_layer.state.blendFlags & hsGMatState.kBlendMask) + self.criteria = 0 # TODO + self.render_level = _RenderLevel() + + def __eq__(self, other): + if not isinstance(other, _DrawableCriteria): + return False + for i in ("blend_span", "render_level", "criteria"): + if getattr(self, i) != getattr(other, i): + return False + return True + + def __hash__(self): + return hash(self.render_level) ^ hash(self.blend_span) ^ hash(self.criteria) + + @property + def span_type(self): + if self.blend_span: + return "BlendSpans" + else: + return "Spans" + + class MeshConverter: - # TODO - pass + _dspans = {} + _mesh_geospans = {} + + def __init__(self, mgr): + self._mgr = mgr + + def _create_geospan(self, bo, bm, hsgmat): + """Initializes a plGeometrySpan from a Blender Object and an hsGMaterial""" + geospan = plGeometrySpan() + geospan.material = hsgmat + + # TODO: Props + # TODO: RunTime lights (requires libHSPlasma feature) + + # If this object has a CI, we don't need xforms here... + if self._mgr.find_key(bo, plCoordinateInterface) is not None: + geospan.localToWorld = hsMatrix44() + geospan.worldToLocal = hsMatrix44() + else: + geospan.worldToLocal = utils.matrix44(bo.matrix_basis) + geospan.localToWorld = geospan.worldToLocal.inverse() + return geospan + + def finalize(self): + """Prepares all baked Plasma geometry to be flushed to the disk""" + for loc in self._dspans.values(): + for dspan in loc.values(): + print("Finalizing DSpan: {}".format(dspan.key.name)) + + # This mega-function does a lot: + # 1. Converts SourceSpans (geospans) to Icicles and bakes geometry into plGBuffers + # 2. Calculates the Icicle bounds + # 3. Builds the plSpaceTree + # 4. Clears the SourceSpans + dspan.composeGeometry(True, True) + + def _export_geometry(self, mesh, geospans): + geodata = [None] * len(mesh.materials) + geoverts = [None] * len(mesh.vertices) + for i, garbage in enumerate(geodata): + geodata[i] = { + "blender2gs": [None] * len(mesh.vertices), + "triangles": [], + "vertices": [], + } + + # Go ahead and naively convert all vertices into TempVertices for the GeoSpans + for i, source in enumerate(mesh.vertices): + vertex = plGeometrySpan.TempVertex() + vertex.color = hsColor32(red=255, green=0, blue=0, alpha=255) # FIXME trollface.jpg testing hacks + vertex.normal = utils.vector3(source.normal) + vertex.position = utils.vector3(source.co) + print(vertex.position) + geoverts[i] = vertex + + # Convert Blender faces into things we can stuff into libHSPlasma + for tessface in mesh.tessfaces: + data = geodata[tessface.material_index] + face_verts = [] + + # Convert to per-material indices + for i in tessface.vertices: + if data["blender2gs"][i] is None: + data["blender2gs"][i] = len(data["vertices"]) + data["vertices"].append(geoverts[i]) + face_verts.append(data["blender2gs"][i]) + + # Convert to triangles, if need be... + if len(face_verts) == 3: + data["triangles"] += face_verts + elif len(face_verts) == 4: + data["triangles"] += (face_verts[0], face_verts[1], face_verts[2]) + data["triangles"] += (face_verts[0], face_verts[2], face_verts[3]) + + # Time to finish it up... + for i, data in enumerate(geodata): + geospan = geospans[i] + numVerts = len(data["vertices"]) + + # Soft vertex limit at 0x8000 for PotS and below. Works fine as long as it's a uint16 + # MOUL only allows signed int16s, however :/ + if numVerts > _MAX_VERTS_PER_SPAN or (numVerts > _WARN_VERTS_PER_SPAN and self._mgr.getVer() >= pvMoul): + raise explosions.TooManyVerticesError(mesh.name, geospan.material.name, numVerts) + elif numVerts > _WARN_VERTS_PER_SPAN: + pass # FIXME + + # If we're still here, let's add our data to the GeometrySpan + geospan.indices = data["triangles"] + geospan.vertices = data["vertices"] + + def export_object(self, bo): + # Have we already exported this mesh? + try: + drawables = self._mesh_geospans[bo.data] + except LookupError: + drawables = self._export_mesh(bo) + + # Create the DrawInterface + diface = self._mgr.add_object(pl=plDrawInterface, bl=bo) + for dspan_key, idx in drawables: + diface.addDrawable(dspan_key, idx) + return diface.key + + def _export_mesh(self, bo): + # First, we need to grab the object's mesh... + mesh = bo.data + mesh.update(calc_tessface=True) + + # Step 1: Export all of the doggone materials. + geospans = self._export_material_spans(bo, mesh) + + # Step 2: Export Blender mesh data to Plasma GeometrySpans + self._export_geometry(mesh, geospans) + + # Step 3: Add plGeometrySpans to the appropriate DSpan and create indices + _diindices = {} + for geospan in geospans: + dspan = self._find_create_dspan(bo, geospan.material.object) + 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(self, bo, bm): + """Exports a single Material Slot as an hsGMaterial""" + # FIXME HACKS + hsgmat = self._mgr.add_object(hsGMaterial, name=bm.name, bl=bo) + fake_layer = self._mgr.add_object(plLayer, name="{}_AutoLayer".format(bm.name), bl=bo) + hsgmat.addLayer(fake_layer.key) + # ... + + return hsgmat.key + + def _export_material_spans(self, bo, mesh): + """Exports all Materials and creates plGeometrySpans""" + geospans = [None] * len(mesh.materials) + for i, blmat in enumerate(mesh.materials): + hsgmat = self._export_material(bo, blmat) + geospans[i] = self._create_geospan(bo, blmat, hsgmat) + return geospans + + def _find_create_dspan(self, bo, hsgmat): + location = self._mgr.get_location(bo) + if location not in self._dspans: + self._dspans[location] = {} + + # This is where we figure out which DSpan this goes into. To vaguely summarize the rules... + # BlendSpans: anything with an alpha blended layer + # [... document me ...] + # We're using pass index to do just what it was designed for. Cyan has a nicer "depends on" + # draw component, but pass index is the Blender way, so that's what we're doing. + crit = _DrawableCriteria(hsgmat) + crit.render_level.level += bo.pass_index + + if crit not in self._dspans[location]: + # AgeName_[District_]_Page_RenderLevel_Crit[Blend]Spans + # Just because it's nice to be consistent + node = self._mgr.get_scene_node(location) + name = "{}_{:08X}_{:X}{}".format(node.name, crit.render_level.level, crit.criteria, crit.span_type) + dspan = self._mgr.add_object(pl=plDrawableSpans, name=name, loc=location) + dspan.sceneNode = node # AddViaNotify + self._dspans[location][crit] = dspan + return dspan + else: + return self._dspans[location][crit] diff --git a/korman/exporter/utils.py b/korman/exporter/utils.py index 2feb872..8539ade 100644 --- a/korman/exporter/utils.py +++ b/korman/exporter/utils.py @@ -24,3 +24,7 @@ def matrix44(blmat): hsmat[2, i] = blmat[i][2] hsmat[3, i] = blmat[i][3] return hsmat + +def vector3(blvec): + """Converts a mathutils.Vector to an hsVector3""" + return hsVector3(blvec.x, blvec.y, blvec.z)