diff --git a/io_scene_a3d/A3D.py b/io_scene_a3d/A3D.py index 3c96d8e..8725ec6 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,21 @@ class A3D: padding = calculatePadding(length) stream.read(padding) + def writeRootBlock3(self, stream): + 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 ''' @@ -117,6 +169,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 +) + +def mirrorUVY(uv): + x, y = uv + return (x, 1-y) + +class A3DBlenderExporter: + def __init__(self, modelData, objects, version=2): + self.modelData = modelData + self.objects = objects + self.version = version + + def exportData(self): + print("Exporting blender data to A3D") + + # Process objects + materials = {} + meshes = [] + transforms = {} + 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: + # 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, ob) + 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 + transform.name = ob.name + transforms[ob.name] = transform + # Create object + objec = A3DObjects.A3DObject() + objec.name = ob.name + objec.meshID = len(meshes) - 1 + objec.transformID = len(transforms) - 1 + materialIDs = [] + for ma in me.materials: + materialID = list(materials.keys()).index(ma.name) + materialIDs.append(materialID) + objec.materialCount = len(materialIDs) + objec.materialIDs = materialIDs + objects.append(objec) + # Create parentIDs + transformParentIDs = [] + for ob in self.objects: + parentOB = ob.parent + if (parentOB == None) or (parentOB.name not in transforms): + if self.version < 3: + transformParentIDs.append(0) + else: + transformParentIDs.append(-1) + else: + parentIndex = list(transforms.keys()).index(parentOB.name) + if self.version < 3: + parentIndex += 1 # Version 2 uses 0 to signify empty parent + 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, ob): + mesh = A3DObjects.A3DMesh() + mesh.name = me.name + 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] + uv1Vertices = [(0.0, 0.0)] * mesh.vertexCount + for polygon in me.polygons: + i0, i1, i2 = polygon.vertices + 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 = 3 #XXX: We only do coordinate, normal1 and uv1 + mesh.vertexBuffers = [coordinateBuffer, uv1Buffer, 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 + + # Bound box data + bounds = [] + for bound in ob.bound_box: + x, y, z = bound + bounds.append((x, y, z)) + mesh.bboxMax = max(bounds) + mesh.bboxMin = min(bounds) + + return mesh diff --git a/io_scene_a3d/A3DBlenderImporter.py b/io_scene_a3d/A3DBlenderImporter.py index c0be3c2..a06b6aa 100644 --- a/io_scene_a3d/A3DBlenderImporter.py +++ b/io_scene_a3d/A3DBlenderImporter.py @@ -64,22 +64,30 @@ 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] - if parentID == 0 and self.modelData.version < 3: - # version 2 models use 0 to signify empty parent + + # Assign parents + for objectID, parentID in enumerate(self.modelData.transformParentIDs): + if self.modelData.version < 3: + # version 2 models use 0 to signify empty parent so everything is shifted up + parentID -= 1 + if parentID == -1: continue - elif parentID == -1: - # version 3 models use -1 to signify empty parent - continue - parentOB = objects[parentID] - ob.parent = parentOB - + ob = objects[objectID] + ob.parent = objects[parentID] + return objects ''' @@ -236,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 diff --git a/io_scene_a3d/A3DObjects.py b/io_scene_a3d/A3DObjects.py index dac8ad5..e0298a9 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, writeLengthPrefixedString, 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) @@ -42,6 +48,12 @@ class A3DMaterial: print(f"[A3DMaterial name: {self.name} color: {self.color} diffuse map: {self.diffuseMap}]") + def write3(self, stream): + writeLengthPrefixedString(stream, self.name) + colorR, colorG, colorB = self.color + packStream("<3f", stream, colorR, colorG, colorB) + writeLengthPrefixedString(stream, self.diffuseMap) + class A3DMesh: def __init__(self): self.name = "" @@ -70,7 +82,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("