From 6ef3f6e889244ce85cfbb416f13c4d66da1f5e4a Mon Sep 17 00:00:00 2001 From: Pyogenics Date: Sun, 27 Apr 2025 15:54:35 +0100 Subject: [PATCH 1/9] Initial version 2 export support --- io_scene_a3d/A3D.py | 100 ++++++++++++++++++++- io_scene_a3d/A3DBlenderExporter.py | 134 +++++++++++++++++++++++++++++ io_scene_a3d/A3DObjects.py | 50 ++++++++++- io_scene_a3d/IOTools.py | 13 ++- io_scene_a3d/__init__.py | 37 +++++++- 5 files changed, 326 insertions(+), 8 deletions(-) create mode 100644 io_scene_a3d/A3DBlenderExporter.py diff --git a/io_scene_a3d/A3D.py b/io_scene_a3d/A3D.py index 3c96d8e..97b685b 100644 --- a/io_scene_a3d/A3D.py +++ b/io_scene_a3d/A3D.py @@ -20,7 +20,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ''' -from .IOTools import unpackStream, readNullTerminatedString, calculatePadding +from io import BytesIO + +from .IOTools import unpackStream, packStream, readNullTerminatedString, calculatePadding from . import A3DObjects ''' @@ -65,6 +67,23 @@ class A3D: self.readRootBlock2(stream) elif self.version == 3: self.readRootBlock3(stream) + else: + raise RuntimeError(f"Unknown A3D version: {self.version}") + + def write(self, stream, version=2): + # Write header data + stream.write(A3D_SIGNATURE) + packStream("<2H", stream, version, 0) + + # Write root block + if version == 1: + self.writeRootBlock1(stream) + elif version == 2: + self.writeRootBlock2(stream) + elif version == 3: + self.writeRootBlock3(stream) + else: + raise RuntimeError(f"Unknown A3D version: {version} whilst writing A3D") ''' Root data blocks @@ -72,6 +91,9 @@ class A3D: def readRootBlock1(self, stream): raise RuntimeError("Version 1 files are not supported yet") + def writeRootBlock1(self, stream): + raise RuntimeError("Version 1 files are not supported yet") + def readRootBlock2(self, stream): # Verify signature signature, _ = unpackStream("<2I", stream) @@ -85,6 +107,21 @@ class A3D: self.readTransformBlock2(stream) self.readObjectBlock2(stream) + def writeRootBlock2(self, stream): + buffer = BytesIO() + + # Write data to the buffer + print("Writing root block") + self.writeMaterialBlock2(buffer) + self.writeMeshBlock2(buffer) + self.writeTransformBlock2(buffer) + self.writeObjectBlock2(buffer) + + # Write buffer to stream + packStream("<2I", stream, A3D_ROOTBLOCK_SIGNATURE, buffer.tell()) + buffer.seek(0, 0) + stream.write(buffer.read()) + def readRootBlock3(self, stream): # Verify signature signature, length = unpackStream("<2I", stream) @@ -101,6 +138,9 @@ class A3D: padding = calculatePadding(length) stream.read(padding) + def writeRootBlock3(self, stream): + raise RuntimeError("Version 3 files are not supported yet") + ''' Material data blocks ''' @@ -117,6 +157,20 @@ class A3D: material.read2(stream) self.materials.append(material) + def writeMaterialBlock2(self, stream): + buffer = BytesIO() + + # Write data to the buffer + print("Writing material block") + packStream(" + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +''' + +from . import A3DObjects +from .A3DObjects import ( + A3D_VERTEXTYPE_COORDINATE, + A3D_VERTEXTYPE_UV1, + A3D_VERTEXTYPE_NORMAL1, + A3D_VERTEXTYPE_UV2, + A3D_VERTEXTYPE_COLOR, + A3D_VERTEXTYPE_NORMAL2 +) + +class A3DBlenderExporter: + def __init__(self, modelData, objects): + self.modelData = modelData + self.objects = objects + + def exportData(self): + print("Exporting blender data to A3D") + + # Process objects + materials = {} + meshes = [] + transforms = {} + objects = [] + for ob in self.objects: + me = ob.data + + # Process materials + for ma in me.materials: + # Make sure we haven't processed this data block already + if ma.name in materials: + continue + + materialData = A3DObjects.A3DMaterial() + materialData.name = ma.name + materialData.diffuseMap = "" + colorR, colorG, colorB, _ = ma.diffuse_color + materialData.color = (colorR, colorG, colorB) + + materials[ma.name] = materialData + # Create mesh + mesh = self.buildA3DMesh(me) + meshes.append(mesh) + # Create transform + transform = A3DObjects.A3DTransform() + transform.position = ob.location + rotationW, rotationX, rotationY, rotationZ = ob.rotation_quaternion + transform.rotation = (rotationX, rotationY, rotationZ, rotationW) + transform.scale = ob.scale + transforms[ob.name] = transform + # Create object + objec = A3DObjects.A3DObject() + objec.name = ob.name + objec.meshID = len(meshes) - 1 + objec.transformID = len(transforms) - 1 + objects.append(objec) + # Create parentIDs + transformParentIDs = [] + for ob in self.objects: + parentOB = ob.parent + if (parentOB == None) or (ob.name not in transforms): + transformParentIDs.append(0) #XXX: this is only for version 2 + else: + parentIndex = list(transforms.keys()).index(ob.name) + transformParentIDs.append(parentIndex) + + self.modelData.materials = materials.values() + self.modelData.meshes = meshes + self.modelData.transforms = transforms.values() + self.modelData.transformParentIDs = transformParentIDs + self.modelData.objects = objects + + def buildA3DMesh(self, me): + mesh = A3DObjects.A3DMesh() + mesh.vertexCount = len(me.vertices) + + # Create vertex buffers + coordinateBuffer = A3DObjects.A3DVertexBuffer() + coordinateBuffer.bufferType = A3D_VERTEXTYPE_COORDINATE + normal1Buffer = A3DObjects.A3DVertexBuffer() + normal1Buffer.bufferType = A3D_VERTEXTYPE_NORMAL1 + for vertex in me.vertices: + coordinateBuffer.data.append(vertex.co) + normal1Buffer.data.append(vertex.normal) + uv1Buffer = A3DObjects.A3DVertexBuffer() + uv1Buffer.bufferType = A3D_VERTEXTYPE_UV1 + uv1Data = me.uv_layers[0] + for vertex in uv1Data.uv: + uv1Buffer.data.append(vertex.vector) + mesh.vertexBufferCount = 2 #XXX: We only do coordinate, normal1 and uv1 + mesh.vertexBuffers = [coordinateBuffer, normal1Buffer] + + # Create submeshes + indexArrays = {} # material_index: index array + lastMaterialIndex = None + for polygon in me.polygons: + if polygon.material_index != lastMaterialIndex: + indexArrays[polygon.material_index] = [] + + indexArrays[polygon.material_index] += polygon.vertices + lastMaterialIndex = polygon.material_index + submeshes = [] + for materialID, indexArray in indexArrays.items(): + submesh = A3DObjects.A3DSubmesh() + submesh.indexCount = len(indexArray) + submesh.indices = indexArray + submesh.materialID = materialID + submesh.smoothingGroups = [0] * (len(indexArray)//3) # Just set all faces to 0 + submeshes.append(submesh) + mesh.submeshCount = len(submeshes) + mesh.submeshes = submeshes + + return mesh diff --git a/io_scene_a3d/A3DObjects.py b/io_scene_a3d/A3DObjects.py index dac8ad5..ba1d7cf 100644 --- a/io_scene_a3d/A3DObjects.py +++ b/io_scene_a3d/A3DObjects.py @@ -20,7 +20,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ''' -from .IOTools import unpackStream, readNullTerminatedString, readLengthPrefixedString, calculatePadding +from .IOTools import unpackStream, packStream, readNullTerminatedString, writeNullTerminatedString, readLengthPrefixedString, calculatePadding class A3DMaterial: def __init__(self): @@ -35,6 +35,12 @@ class A3DMaterial: print(f"[A3DMaterial name: {self.name} color: {self.color} diffuse map: {self.diffuseMap}]") + def write2(self, stream): + writeNullTerminatedString(stream, self.name) + colorR, colorG, colorB = self.color + packStream("<3f", stream, colorR, colorG, colorB) + writeNullTerminatedString(stream, self.diffuseMap) + def read3(self, stream): self.name = readLengthPrefixedString(stream) self.color = unpackStream("<3f", stream) @@ -70,7 +76,16 @@ class A3DMesh: self.submeshes.append(submesh) print(f"[A3DMesh name: {self.name} bbox max: {self.bboxMax} bbox min: {self.bboxMin} vertex buffers: {len(self.vertexBuffers)} submeshes: {len(self.submeshes)}]") - + + def write2(self, stream): + packStream("<2I", stream, self.vertexCount, self.vertexBufferCount) + for vertexBuffer in self.vertexBuffers: + vertexBuffer.write2(stream) + + packStream(" Date: Sun, 27 Apr 2025 20:23:45 +0100 Subject: [PATCH 2/9] Correct parent behaviour for both import and export on version 2 models --- io_scene_a3d/A3DBlenderExporter.py | 6 +++--- io_scene_a3d/A3DBlenderImporter.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/io_scene_a3d/A3DBlenderExporter.py b/io_scene_a3d/A3DBlenderExporter.py index 0f3f922..7e5d875 100644 --- a/io_scene_a3d/A3DBlenderExporter.py +++ b/io_scene_a3d/A3DBlenderExporter.py @@ -79,11 +79,11 @@ class A3DBlenderExporter: transformParentIDs = [] for ob in self.objects: parentOB = ob.parent - if (parentOB == None) or (ob.name not in transforms): + if (parentOB == None) or (parentOB.name not in transforms): transformParentIDs.append(0) #XXX: this is only for version 2 else: - parentIndex = list(transforms.keys()).index(ob.name) - transformParentIDs.append(parentIndex) + parentIndex = list(transforms.keys()).index(parentOB.name) + transformParentIDs.append(parentIndex+1) self.modelData.materials = materials.values() self.modelData.meshes = meshes diff --git a/io_scene_a3d/A3DBlenderImporter.py b/io_scene_a3d/A3DBlenderImporter.py index c0be3c2..7635340 100644 --- a/io_scene_a3d/A3DBlenderImporter.py +++ b/io_scene_a3d/A3DBlenderImporter.py @@ -71,11 +71,11 @@ class A3DBlenderImporter: for obI, ob in enumerate(objects): # Assign parents parentID = self.modelData.transformParentIDs[obI] - if parentID == 0 and self.modelData.version < 3: + if self.modelData.version < 3: # version 2 models use 0 to signify empty parent - continue - elif parentID == -1: - # version 3 models use -1 to signify empty parent + parentID -= 1 + if parentID == -1: + # empty parent continue parentOB = objects[parentID] ob.parent = parentOB From 437d9f079c75e9b3e023cd03a0f4e4754327b564 Mon Sep 17 00:00:00 2001 From: Pyogenics Date: Mon, 28 Apr 2025 11:28:40 +0100 Subject: [PATCH 3/9] UV export --- io_scene_a3d/A3DBlenderExporter.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/io_scene_a3d/A3DBlenderExporter.py b/io_scene_a3d/A3DBlenderExporter.py index 7e5d875..927617e 100644 --- a/io_scene_a3d/A3DBlenderExporter.py +++ b/io_scene_a3d/A3DBlenderExporter.py @@ -106,10 +106,16 @@ class A3DBlenderExporter: uv1Buffer = A3DObjects.A3DVertexBuffer() uv1Buffer.bufferType = A3D_VERTEXTYPE_UV1 uv1Data = me.uv_layers[0] - for vertex in uv1Data.uv: - uv1Buffer.data.append(vertex.vector) - mesh.vertexBufferCount = 2 #XXX: We only do coordinate, normal1 and uv1 - mesh.vertexBuffers = [coordinateBuffer, normal1Buffer] + uv1Vertices = [(0.0, 0.0)] * mesh.vertexCount + for polygon in me.polygons: + i0, i1, i2 = polygon.vertices + uv1Vertices[i0] = uv1Data.uv[polygon.loop_start].vector + uv1Vertices[i1] = uv1Data.uv[polygon.loop_start+1].vector + uv1Vertices[i2] = uv1Data.uv[polygon.loop_start+2].vector + uv1Buffer.data = uv1Vertices + + mesh.vertexBufferCount = 3 #XXX: We only do coordinate, normal1 and uv1 + mesh.vertexBuffers = [coordinateBuffer, uv1Buffer, normal1Buffer] # Create submeshes indexArrays = {} # material_index: index array From 56a55c8516a1077d7105312cef89809db58d78cd Mon Sep 17 00:00:00 2001 From: Pyogenics Date: Wed, 30 Apr 2025 18:45:17 +0100 Subject: [PATCH 4/9] Initial version 3 export support --- io_scene_a3d/A3D.py | 91 +++++++++++++++++++++++++++++- io_scene_a3d/A3DBlenderExporter.py | 31 ++++++++-- io_scene_a3d/A3DObjects.py | 55 ++++++++++++++++-- io_scene_a3d/IOTools.py | 11 +++- 4 files changed, 175 insertions(+), 13 deletions(-) diff --git a/io_scene_a3d/A3D.py b/io_scene_a3d/A3D.py index 97b685b..8725ec6 100644 --- a/io_scene_a3d/A3D.py +++ b/io_scene_a3d/A3D.py @@ -139,7 +139,19 @@ class A3D: stream.read(padding) def writeRootBlock3(self, stream): - raise RuntimeError("Version 3 files are not supported yet") + buffer = BytesIO() + + # Write data to the buffer + print("Writing root block") + self.writeMaterialBlock3(buffer) + self.writeMeshBlock3(buffer) + self.writeTransformBlock3(buffer) + self.writeObjectBlock3(buffer) + + # Write buffer to stream + packStream("<2I", stream, A3D_ROOTBLOCK_SIGNATURE, buffer.tell()) + buffer.seek(0, 0) + stream.write(buffer.read()) ''' Material data blocks @@ -188,6 +200,24 @@ class A3D: padding = calculatePadding(length) stream.read(padding) + def writeMaterialBlock3(self, stream): + buffer = BytesIO() + + # Write data to the buffer + print("Writing material block") + packStream(" Date: Wed, 30 Apr 2025 19:14:42 +0100 Subject: [PATCH 5/9] Add version selection to export options --- io_scene_a3d/__init__.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/io_scene_a3d/__init__.py b/io_scene_a3d/__init__.py index ae31c7e..adba9da 100644 --- a/io_scene_a3d/__init__.py +++ b/io_scene_a3d/__init__.py @@ -22,7 +22,7 @@ SOFTWARE. import bpy from bpy.types import Operator, OperatorFileListElement, AddonPreferences -from bpy.props import StringProperty, BoolProperty, CollectionProperty, FloatProperty +from bpy.props import StringProperty, BoolProperty, CollectionProperty, FloatProperty, EnumProperty from bpy_extras.io_utils import ImportHelper, ExportHelper from .A3D import A3D @@ -109,8 +109,18 @@ class ExportA3D(Operator, ExportHelper): filter_glob: StringProperty(default="*.a3d", options={'HIDDEN'}) filename_ext: StringProperty(default=".a3d", options={'HIDDEN'}) + a3d_version: EnumProperty( + items=( + ("2", "A3D2", "Version 2 files are used to store map geometry like props and simple models like drones and particle effects"), + ("3", "A3D3", "Version 3 files are used to store tank turret and hull models") + ), + description="A3D file version", + default="2", + name="version" + ) + def draw(self, context): - pass + export_panel_options_a3d(self.layout, self) def invoke(self, context, event): return ExportHelper.invoke(self, context, event) @@ -119,12 +129,12 @@ class ExportA3D(Operator, ExportHelper): print(f"Exporting blender data to {self.filepath}") modelData = A3D() - modelExporter = A3DBlenderExporter(modelData, bpy.context.selected_objects) + modelExporter = A3DBlenderExporter(modelData, bpy.context.selected_objects, version=int(self.a3d_version)) modelExporter.exportData() # Write file with open(self.filepath, "wb") as file: - modelData.write(file, version=2) + modelData.write(file, version=int(self.a3d_version)) return {"FINISHED"} @@ -196,6 +206,12 @@ def import_panel_options_a3d(layout, operator): body.prop(operator, "try_import_textures") body.prop(operator, "reset_empty_transform") +def export_panel_options_a3d(layout, operator): + header, body = layout.panel("alternativa_import_options", default_closed=False) + header.label(text="Options") + if body: + body.prop(operator, "a3d_version") + def import_panel_options_battlemap(layout, operator): header, body = layout.panel("tanki_battlemap_import_options", default_closed=False) header.label(text="Options") From 655e8247d08af29e2bc55fe60f490c2daf5aaf52 Mon Sep 17 00:00:00 2001 From: Pyogenics Date: Wed, 30 Apr 2025 19:55:44 +0100 Subject: [PATCH 6/9] Fixed bug where A3D mesh names were not being set on export --- io_scene_a3d/A3DBlenderExporter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/io_scene_a3d/A3DBlenderExporter.py b/io_scene_a3d/A3DBlenderExporter.py index 2dae96c..08702cc 100644 --- a/io_scene_a3d/A3DBlenderExporter.py +++ b/io_scene_a3d/A3DBlenderExporter.py @@ -106,6 +106,7 @@ class A3DBlenderExporter: def buildA3DMesh(self, me, ob): mesh = A3DObjects.A3DMesh() + mesh.name = me.name mesh.vertexCount = len(me.vertices) # Create vertex buffers From c2580a4273baf88d3ad0cf51a4d95cf15010ce08 Mon Sep 17 00:00:00 2001 From: Pyogenics Date: Thu, 1 May 2025 21:29:45 +0100 Subject: [PATCH 7/9] Import orphaned transforms --- io_scene_a3d/A3DBlenderExporter.py | 19 ++++++++++++++-- io_scene_a3d/A3DBlenderImporter.py | 36 ++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/io_scene_a3d/A3DBlenderExporter.py b/io_scene_a3d/A3DBlenderExporter.py index 08702cc..e4cf8f4 100644 --- a/io_scene_a3d/A3DBlenderExporter.py +++ b/io_scene_a3d/A3DBlenderExporter.py @@ -46,6 +46,17 @@ class A3DBlenderExporter: objects = [] for ob in self.objects: me = ob.data + if me == None: + # Create a transform for this object without the object itself + transform = A3DObjects.A3DTransform() + transform.position = ob.location + rotationW, rotationX, rotationY, rotationZ = ob.rotation_quaternion + transform.rotation = (rotationX, rotationY, rotationZ, rotationW) + transform.scale = ob.scale + transform.name = ob.name + transforms[ob.name] = transform + + continue # Process materials for ma in me.materials: @@ -128,8 +139,12 @@ class A3DBlenderExporter: uv1Vertices[i2] = uv1Data.uv[polygon.loop_start+2].vector uv1Buffer.data = uv1Vertices - mesh.vertexBufferCount = 3 #XXX: We only do coordinate, normal1 and uv1 - mesh.vertexBuffers = [coordinateBuffer, uv1Buffer, normal1Buffer] + normal2Buffer = A3DObjects.A3DVertexBuffer() + normal2Buffer.bufferType = A3D_VERTEXTYPE_NORMAL2 + normal2Buffer.data = normal1Buffer.data + + mesh.vertexBufferCount = 4 #XXX: We only do coordinate, normal1 and uv1 + mesh.vertexBuffers = [coordinateBuffer, uv1Buffer, normal1Buffer, normal2Buffer] # Create submeshes indexArrays = {} # material_index: index array diff --git a/io_scene_a3d/A3DBlenderImporter.py b/io_scene_a3d/A3DBlenderImporter.py index 7635340..db1f12e 100644 --- a/io_scene_a3d/A3DBlenderImporter.py +++ b/io_scene_a3d/A3DBlenderImporter.py @@ -39,7 +39,7 @@ def mirrorUVY(uv): return (x, 1-y) class A3DBlenderImporter: - def __init__(self, modelData, directory, reset_empty_transform=True, try_import_textures=True): + def __init__(self, modelData, directory, reset_empty_transform=True, try_import_textures=True, import_unused_transforms=True): self.modelData = modelData self.directory = directory self.materials = [] @@ -48,6 +48,7 @@ class A3DBlenderImporter: # User settings self.reset_empty_transform = reset_empty_transform self.try_import_textures = try_import_textures + self.import_unused_transforms = import_unused_transforms def importData(self): print("Importing A3D model data into blender") @@ -79,7 +80,38 @@ class A3DBlenderImporter: continue parentOB = objects[parentID] ob.parent = parentOB - + if self.import_unused_transforms: + # First identify which transforms have not been used + unusedTransformIndices = list( + range(len(self.modelData.transforms)) + ) + print(unusedTransformIndices) + for objectData in self.modelData.objects: + unusedTransformIndices.remove(objectData.transformID) + + # Create empty objects for each transform + for transformID in unusedTransformIndices: + transformData = self.modelData.transforms[transformID] + ob = bpy.data.objects.new(transformData.name, None) + ob.empty_display_size = 10 + ob.location = transformData.position + ob.rotation_mode = "QUATERNION" + x, y, z, w = transformData.rotation + ob.rotation_quaternion = (w, x, y, z) + ob.scale = transformData.scale + objects.append(ob) + + # Assign parent + parentID = self.modelData.transformParentIDs[transformID] + if self.modelData.version < 3: + # version 2 models use 0 to signify empty parent + parentID -= 1 + if parentID == -1: + # empty parent + continue + parentOB = objects[parentID] + ob.parent = parentOB + return objects ''' From 3655a0c79d71ec57933722a66c9d6c9a8c613dbf Mon Sep 17 00:00:00 2001 From: Pyogenics Date: Wed, 21 May 2025 18:18:14 +0100 Subject: [PATCH 8/9] Handle transforms, parents and objects better --- io_scene_a3d/A3DBlenderImporter.py | 81 ++++++++++++++---------------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/io_scene_a3d/A3DBlenderImporter.py b/io_scene_a3d/A3DBlenderImporter.py index db1f12e..a06b6aa 100644 --- a/io_scene_a3d/A3DBlenderImporter.py +++ b/io_scene_a3d/A3DBlenderImporter.py @@ -39,7 +39,7 @@ def mirrorUVY(uv): return (x, 1-y) class A3DBlenderImporter: - def __init__(self, modelData, directory, reset_empty_transform=True, try_import_textures=True, import_unused_transforms=True): + def __init__(self, modelData, directory, reset_empty_transform=True, try_import_textures=True): self.modelData = modelData self.directory = directory self.materials = [] @@ -48,7 +48,6 @@ class A3DBlenderImporter: # User settings self.reset_empty_transform = reset_empty_transform self.try_import_textures = try_import_textures - self.import_unused_transforms = import_unused_transforms def importData(self): print("Importing A3D model data into blender") @@ -65,52 +64,29 @@ class A3DBlenderImporter: # Create objects objects = [] - for objectData in self.modelData.objects: - ob = self.buildBlenderObject(objectData) + for transformID, transformData in enumerate(self.modelData.transforms): + # Find out if this transform is used by an object + ob = None + for objectData in self.modelData.objects: + if objectData.transformID == transformID: + ob = self.buildBlenderObject(objectData) + break + + # Empty transform, create an empty object to represent it + if ob == None: + ob = self.buildBlenderEmptyObject(transformData) + objects.append(ob) - # Assign object parents and link to collection - for obI, ob in enumerate(objects): - # Assign parents - parentID = self.modelData.transformParentIDs[obI] + + # Assign parents + for objectID, parentID in enumerate(self.modelData.transformParentIDs): if self.modelData.version < 3: - # version 2 models use 0 to signify empty parent + # version 2 models use 0 to signify empty parent so everything is shifted up parentID -= 1 if parentID == -1: - # empty parent continue - parentOB = objects[parentID] - ob.parent = parentOB - if self.import_unused_transforms: - # First identify which transforms have not been used - unusedTransformIndices = list( - range(len(self.modelData.transforms)) - ) - print(unusedTransformIndices) - for objectData in self.modelData.objects: - unusedTransformIndices.remove(objectData.transformID) - - # Create empty objects for each transform - for transformID in unusedTransformIndices: - transformData = self.modelData.transforms[transformID] - ob = bpy.data.objects.new(transformData.name, None) - ob.empty_display_size = 10 - ob.location = transformData.position - ob.rotation_mode = "QUATERNION" - x, y, z, w = transformData.rotation - ob.rotation_quaternion = (w, x, y, z) - ob.scale = transformData.scale - objects.append(ob) - - # Assign parent - parentID = self.modelData.transformParentIDs[transformID] - if self.modelData.version < 3: - # version 2 models use 0 to signify empty parent - parentID -= 1 - if parentID == -1: - # empty parent - continue - parentOB = objects[parentID] - ob.parent = parentOB + ob = objects[objectID] + ob.parent = objects[parentID] return objects @@ -268,4 +244,21 @@ class A3DBlenderImporter: # Apply image addImageTextureToMaterial(image, ma.node_tree) - return ob \ No newline at end of file + return ob + + def buildBlenderEmptyObject(self, transformData): + # Create the object + ob = bpy.data.objects.new(transformData.name, None) + ob.empty_display_size = 10 # Assume that the model is in alternativa scale (x100) + + # Set transform + ob.location = transformData.position + ob.scale = transformData.scale + ob.rotation_mode = "QUATERNION" + x, y, z, w = transformData.rotation + ob.rotation_quaternion = (w, x, y, z) + if self.reset_empty_transform: + if transformData.scale == (0.0, 0.0, 0.0): ob.scale = (1.0, 1.0, 1.0) + if transformData.rotation == (0.0, 0.0, 0.0, 0.0): ob.rotation_quaternion = (1.0, 0.0, 0.0, 0.0) + + return ob From 7283d77f48571d9b2d6a0486f9e497a85a4471e5 Mon Sep 17 00:00:00 2001 From: Pyogenics Date: Wed, 21 May 2025 18:38:18 +0100 Subject: [PATCH 9/9] Export flip UV y component --- io_scene_a3d/A3DBlenderExporter.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/io_scene_a3d/A3DBlenderExporter.py b/io_scene_a3d/A3DBlenderExporter.py index e4cf8f4..267a206 100644 --- a/io_scene_a3d/A3DBlenderExporter.py +++ b/io_scene_a3d/A3DBlenderExporter.py @@ -30,6 +30,10 @@ from .A3DObjects import ( A3D_VERTEXTYPE_NORMAL2 ) +def mirrorUVY(uv): + x, y = uv + return (x, 1-y) + class A3DBlenderExporter: def __init__(self, modelData, objects, version=2): self.modelData = modelData @@ -134,17 +138,17 @@ class A3DBlenderExporter: uv1Vertices = [(0.0, 0.0)] * mesh.vertexCount for polygon in me.polygons: i0, i1, i2 = polygon.vertices - uv1Vertices[i0] = uv1Data.uv[polygon.loop_start].vector - uv1Vertices[i1] = uv1Data.uv[polygon.loop_start+1].vector - uv1Vertices[i2] = uv1Data.uv[polygon.loop_start+2].vector + uv1Vertices[i0] = mirrorUVY(uv1Data.uv[polygon.loop_start].vector) + uv1Vertices[i1] = mirrorUVY(uv1Data.uv[polygon.loop_start+1].vector) + uv1Vertices[i2] = mirrorUVY(uv1Data.uv[polygon.loop_start+2].vector) uv1Buffer.data = uv1Vertices normal2Buffer = A3DObjects.A3DVertexBuffer() normal2Buffer.bufferType = A3D_VERTEXTYPE_NORMAL2 normal2Buffer.data = normal1Buffer.data - mesh.vertexBufferCount = 4 #XXX: We only do coordinate, normal1 and uv1 - mesh.vertexBuffers = [coordinateBuffer, uv1Buffer, normal1Buffer, normal2Buffer] + mesh.vertexBufferCount = 3 #XXX: We only do coordinate, normal1 and uv1 + mesh.vertexBuffers = [coordinateBuffer, uv1Buffer, normal1Buffer] # Create submeshes indexArrays = {} # material_index: index array