From d3988cd10f922b80c5d4e8fdefda6017a735c370 Mon Sep 17 00:00:00 2001 From: Pyogenics Date: Sun, 19 Jan 2025 12:04:28 +0000 Subject: [PATCH] Extract blender import code to improve code quality --- io_scene_a3d/A3DBlenderImporter.py | 195 +++++++++++++++++++++++++++++ io_scene_a3d/__init__.py | 169 ++++--------------------- 2 files changed, 220 insertions(+), 144 deletions(-) create mode 100644 io_scene_a3d/A3DBlenderImporter.py diff --git a/io_scene_a3d/A3DBlenderImporter.py b/io_scene_a3d/A3DBlenderImporter.py new file mode 100644 index 0000000..bd2d3ab --- /dev/null +++ b/io_scene_a3d/A3DBlenderImporter.py @@ -0,0 +1,195 @@ +''' +Copyright (c) 2024 Pyogenics + +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. +''' + +import bpy +from bpy_extras.node_shader_utils import PrincipledBSDFWrapper + +from .A3DObjects import ( + A3D_VERTEXTYPE_COORDINATE, + A3D_VERTEXTYPE_UV1, + A3D_VERTEXTYPE_NORMAL1, + A3D_VERTEXTYPE_UV2, + A3D_VERTEXTYPE_COLOR, + A3D_VERTEXTYPE_NORMAL2 +) + +class A3DBlenderImporter: + def __init__(self, modelData, create_collection=True): + self.modelData = modelData + self.materials = [] + self.meshes = [] + + # User settings + self.create_collection = create_collection + + def importData(self): + print("Importing A3D model data into blender") + + # Create materials + for materialData in self.modelData.materials: + ma = self.buildBlenderMaterial(materialData) + self.materials.append(ma) + + # Build meshes + for meshData in self.modelData.meshes: + me = self.buildBlenderMesh(meshData) + self.meshes.append(me) + + # Create objects + collection = bpy.context.collection # By default use the current active collection + if self.create_collection: + collection = bpy.data.collections.new("Object") + bpy.context.collection.children.link(collection) + for objectData in self.modelData.objects: + ob = self.buildBlenderObject(objectData) + collection.objects.link(ob) + + ''' + Blender data builders + ''' + def buildBlenderMaterial(self, materialData): + ma = bpy.data.materials.new(materialData.name) + maWrapper = PrincipledBSDFWrapper(ma, is_readonly=False, use_nodes=True) + maWrapper.base_color = materialData.color + maWrapper.roughness = 1.0 + + return ma + + def buildBlenderMesh(self, meshData): + me = bpy.data.meshes.new(meshData.name) + + # Gather all vertex data + coordinates = [] + uv1 = [] + normal1 = [] + uv2 = [] + colors = [] + normal2 = [] + for vertexBuffer in meshData.vertexBuffers: + if vertexBuffer.bufferType == A3D_VERTEXTYPE_COORDINATE: + coordinates += vertexBuffer.data + elif vertexBuffer.bufferType == A3D_VERTEXTYPE_UV1: + uv1 += vertexBuffer.data + elif vertexBuffer.bufferType == A3D_VERTEXTYPE_NORMAL1: + normal1 += vertexBuffer.data + elif vertexBuffer.bufferType == A3D_VERTEXTYPE_UV2: + uv2 += vertexBuffer.data + elif vertexBuffer.bufferType == A3D_VERTEXTYPE_COLOR: + colors += vertexBuffer.data + elif vertexBuffer.bufferType == A3D_VERTEXTYPE_NORMAL2: + normal2 += vertexBuffer.data + + # Add blender vertices + blenderVertexIndices = [] + blenderVertices = [] + blenderUV1s = [] + blenderUV2s = [] + for submesh in meshData.submeshes: + polygonCount = len(submesh.indices) // 3 + me.vertices.add(polygonCount*3) + me.loops.add(polygonCount*3) + me.polygons.add(polygonCount) + + for indexI in range(submesh.indexCount): + index = submesh.indices[indexI] + blenderVertexIndices.append(indexI) + blenderVertices += list(coordinates[index]) + if len(uv1) != 0: + for indexI in range(submesh.indexCount): + index = submesh.indices[indexI] + x, y = uv1[index] + blenderUV1s.append((x, 1-y)) + if len(uv2) != 0: + for indexI in range(submesh.indexCount): + index = submesh.indices[indexI] + x, y = uv2[index] + blenderUV2s.append((x, 1-y)) + me.vertices.foreach_set("co", blenderVertices) + me.polygons.foreach_set("loop_start", range(0, len(blenderVertices)//3, 3)) + me.loops.foreach_set("vertex_index", blenderVertexIndices) + + # UVs + if len(uv1) != 0: + uvData = me.uv_layers.new(name="UV1").data + for polygonI, po in enumerate(me.polygons): + indexI = polygonI * 3 + uvData[po.loop_start].uv = blenderUV1s[blenderVertexIndices[indexI]] + uvData[po.loop_start+1].uv = blenderUV1s[blenderVertexIndices[indexI+1]] + uvData[po.loop_start+2].uv = blenderUV1s[blenderVertexIndices[indexI+2]] + if len(uv2) != 0: + uvData = me.uv_layers.new(name="UV2").data + for polygonI, po in enumerate(me.polygons): + indexI = polygonI * 3 + uvData[po.loop_start].uv = blenderUV2s[blenderVertexIndices[indexI]] + uvData[po.loop_start+1].uv = blenderUV2s[blenderVertexIndices[indexI+1]] + uvData[po.loop_start+2].uv = blenderUV2s[blenderVertexIndices[indexI+2]] + + # Apply materials (version 2) + faceIndexBase = 0 + for submeshI, submesh in enumerate(meshData.submeshes): + if submesh.materialID == None: + continue + me.materials.append(self.materials[submesh.materialID]) + for faceI in range(submesh.indexCount//3): + me.polygons[faceI+faceIndexBase].material_index = submeshI + faceIndexBase += submesh.indexCount//3 + + # Finalise + me.validate() + me.update() + return me + + def buildBlenderObject(self, objectData): + me = self.meshes[objectData.meshID] + mesh = self.modelData.meshes[objectData.meshID] + transform = self.modelData.transforms[objectData.transformID] + + # Apply materials to mesh (version 3) + for materialID in objectData.materialIDs: + if materialID == -1: + continue + me.materials.append(self.materials[materialID]) + # Set the default material to the first one we added + for polygon in me.polygons: + polygon.material_index = 0 + + # Select a name for the blender object + #XXX: review this, maybe we should just stick to the name we are given + name = "" + if objectData.name != "": + name = objectData.name + elif mesh.name != "": + name = mesh.name + else: + name = transform.name + + # Create the object + ob = bpy.data.objects.new(name, me) + + # Set transform + ob.location = transform.position + ob.scale = transform.scale + ob.rotation_mode = "QUATERNION" + x, y, z, w = transform.rotation + ob.rotation_quaternion = (w, x, y, z) + + return ob \ No newline at end of file diff --git a/io_scene_a3d/__init__.py b/io_scene_a3d/__init__.py index 1104ef8..137b9ec 100644 --- a/io_scene_a3d/__init__.py +++ b/io_scene_a3d/__init__.py @@ -1,20 +1,32 @@ -import bmesh +''' +Copyright (c) 2024 Pyogenics + +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. +''' + import bpy from bpy.types import Operator from bpy.props import StringProperty, BoolProperty from bpy_extras.io_utils import ImportHelper -from bpy_extras.node_shader_utils import PrincipledBSDFWrapper -from bpy_extras.image_utils import load_image from .A3D import A3D -from .A3DObjects import ( - A3D_VERTEXTYPE_COORDINATE, - A3D_VERTEXTYPE_UV1, - A3D_VERTEXTYPE_NORMAL1, - A3D_VERTEXTYPE_UV2, - A3D_VERTEXTYPE_COLOR, - A3D_VERTEXTYPE_NORMAL2 -) +from .A3DBlenderImporter import A3DBlenderImporter ''' Operators @@ -47,139 +59,8 @@ class ImportA3D(Operator, ImportHelper): modelData.read(file) # Import data into blender - print("Importing mesh data into blender") - # Create materials - materials = [] - for material in modelData.materials: - ma = bpy.data.materials.new(material.name) - maWrapper = PrincipledBSDFWrapper(ma, is_readonly=False, use_nodes=True) - maWrapper.base_color = material.color - maWrapper.roughness = 1.0 - - materials.append(ma) - # Build meshes - meshes = [] - for mesh in modelData.meshes: - me = bpy.data.meshes.new(mesh.name) - - # Gather all vertex data - coordinates = [] - uv1 = [] - normal1 = [] - uv2 = [] - colors = [] - normal2 = [] - for vertexBuffer in mesh.vertexBuffers: - if vertexBuffer.bufferType == A3D_VERTEXTYPE_COORDINATE: - coordinates += vertexBuffer.data - elif vertexBuffer.bufferType == A3D_VERTEXTYPE_UV1: - uv1 += vertexBuffer.data - elif vertexBuffer.bufferType == A3D_VERTEXTYPE_NORMAL1: - normal1 += vertexBuffer.data - elif vertexBuffer.bufferType == A3D_VERTEXTYPE_UV2: - uv2 += vertexBuffer.data - elif vertexBuffer.bufferType == A3D_VERTEXTYPE_COLOR: - colors += vertexBuffer.data - elif vertexBuffer.bufferType == A3D_VERTEXTYPE_NORMAL2: - normal2 += vertexBuffer.data - - # Add blender vertices - blenderVertexIndices = [] - blenderVertices = [] - blenderUV1s = [] - blenderUV2s = [] - for submesh in mesh.submeshes: - polygonCount = len(submesh.indices) // 3 - me.vertices.add(polygonCount*3) - me.loops.add(polygonCount*3) - me.polygons.add(polygonCount) - - for indexI in range(submesh.indexCount): - index = submesh.indices[indexI] - blenderVertexIndices.append(indexI) - blenderVertices += list(coordinates[index]) - if len(uv1) != 0: - for indexI in range(submesh.indexCount): - index = submesh.indices[indexI] - x, y = uv1[index] - blenderUV1s.append((x, 1-y)) - if len(uv2) != 0: - for indexI in range(submesh.indexCount): - index = submesh.indices[indexI] - x, y = uv2[index] - blenderUV2s.append((x, 1-y)) - me.vertices.foreach_set("co", blenderVertices) - me.polygons.foreach_set("loop_start", range(0, len(blenderVertices)//3, 3)) - me.loops.foreach_set("vertex_index", blenderVertexIndices) - - # UVs - if len(uv1) != 0: - uvData = me.uv_layers.new(name="UV1").data - for polygonI, po in enumerate(me.polygons): - indexI = polygonI * 3 - uvData[po.loop_start].uv = blenderUV1s[blenderVertexIndices[indexI]] - uvData[po.loop_start+1].uv = blenderUV1s[blenderVertexIndices[indexI+1]] - uvData[po.loop_start+2].uv = blenderUV1s[blenderVertexIndices[indexI+2]] - if len(uv2) != 0: - uvData = me.uv_layers.new(name="UV2").data - for polygonI, po in enumerate(me.polygons): - indexI = polygonI * 3 - uvData[po.loop_start].uv = blenderUV2s[blenderVertexIndices[indexI]] - uvData[po.loop_start+1].uv = blenderUV2s[blenderVertexIndices[indexI+1]] - uvData[po.loop_start+2].uv = blenderUV2s[blenderVertexIndices[indexI+2]] - - # Apply materials (version 2) - faceIndexBase = 0 - for submeshI, submesh in enumerate(mesh.submeshes): - if submesh.materialID == None: - continue - me.materials.append(materials[submesh.materialID]) - for faceI in range(submesh.indexCount//3): - me.polygons[faceI+faceIndexBase].material_index = submeshI - faceIndexBase += submesh.indexCount//3 - - # Finalise - me.validate() - me.update() - meshes.append(me) - # Create objects - collection = bpy.context.collection # By default use the active collection - if self.create_collection: - collection = bpy.data.collections.new("Object") - bpy.context.collection.children.link(collection) - for objec in modelData.objects: - me = meshes[objec.meshID] - mesh = modelData.meshes[objec.meshID] - transform = modelData.transforms[objec.transformID] - - # Select a name for the blender object - name = "" - if objec.name != "": - name = objec.name - elif mesh.name != "": - name = mesh.name - else: - name = transform.name - - # Create the object - ob = bpy.data.objects.new(name, me) - collection.objects.link(ob) - - # Set transform - ob.location = transform.position - ob.scale = transform.scale - ob.rotation_mode = "QUATERNION" - x, y, z, w = transform.rotation - ob.rotation_quaternion = (w, x, y, z) - - # Apply materials (version 3) - for materialID in objec.materialIDs: - if materialID == -1: - continue - me.materials.append(materials[materialID]) - # Set the default material to the first one we added - for polygon in me.polygons: - polygon.material_index = 0 + modelImporter = A3DBlenderImporter(modelData, self.create_collection) + modelImporter.importData() return {"FINISHED"}