Add new A3D2 reading

This commit is contained in:
Pyogenics
2024-11-23 17:39:51 +00:00
parent 019b0c54e8
commit e8fd653c80
4 changed files with 334 additions and 0 deletions

157
io_scene_a3d/A3D.py Normal file
View File

@@ -0,0 +1,157 @@
'''
Copyright (c) 2024 Pyogenics <https://github.com/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.
'''
from .IOTools import unpackStream, readNullTerminatedString
from . import A3DObjects
'''
A3D constants
'''
A3D_SIGNATURE = b"A3D\0"
A3D_ROOTBLOCK_SIGNATURE = 1
A3D_MATERIALBLOCK_SIGNATURE = 4
A3D_MESHBLOCK_SIGNATURE = 2
A3D_TRANSFORMBLOCK_SIGNATURE = 3
A3D_OBJECTBLOCK_SIGNATURE = 5
'''
A3D model object
'''
class A3D:
def __init__(self):
self.materials = []
self.meshes = []
self.transforms = {}
self.objects = []
'''
Main IO
'''
def read(self, stream):
# Check signature
signature = stream.read(4)
if signature != A3D_SIGNATURE:
raise RuntimeError(f"Invalid A3D signature: {signature}")
# Read file version and read version specific data
version, _ = unpackStream("<2H", stream) # Likely major.minor version code
print(f"Reading A3D version {version}")
if version == 1:
self.readRootBlock1(stream)
elif version == 2:
self.readRootBlock2(stream)
elif version == 3:
self.readRootBlock3(stream)
'''
Root data blocks
'''
def readRootBlock1(self, stream):
raise RuntimeError("Version 1 files are not supported yet")
def readRootBlock2(self, stream):
# Verify signature
signature, _ = unpackStream("<2I", stream)
if signature != A3D_ROOTBLOCK_SIGNATURE:
raise RuntimeError(f"Invalid root data block signature: {signature}")
# Read data
print(f"Reading root block")
self.readMaterialBlock2(stream)
self.readMeshBlock2(stream)
self.readTransformBlock2(stream)
self.readObjectBlock2(stream)
def readRootBlock3(self, stream):
raise RuntimeError("Version 3 files are not supported yet")
'''
Material data blocks
'''
def readMaterialBlock2(self, stream):
# Verify signature
signature, _, materialCount = unpackStream("<3I", stream)
if signature != A3D_MATERIALBLOCK_SIGNATURE:
raise RuntimeError(f"Invalid material data block signature: {signature}")
# Read data
print(f"Reading material block with {materialCount} materials")
for _ in range(materialCount):
material = A3DObjects.A3DMaterial()
material.read2(stream)
self.materials.append(material)
'''
Mesh data blocks
'''
def readMeshBlock2(self, stream):
# Verify signature
signature, _, meshCount = unpackStream("<3I", stream)
if signature != A3D_MESHBLOCK_SIGNATURE:
raise RuntimeError(f"Invalid mesh data block signature: {signature}")
# Read data
print(f"Reading mesh block with {meshCount} meshes")
for _ in range(meshCount):
mesh = A3DObjects.A3DMesh()
mesh.read2(stream)
self.meshes.append(mesh)
'''
Transform data blocks
'''
def readTransformBlock2(self, stream):
# Verify signature
signature, _, transformCount = unpackStream("<3I", stream)
if signature != A3D_TRANSFORMBLOCK_SIGNATURE:
print(f"{stream.tell()}")
raise RuntimeError(f"Invalid transform data block signature: {signature}")
# Read data
print(f"Reading transform block with {transformCount} transforms")
transforms = []
for _ in range(transformCount):
transform = A3DObjects.A3DTransform()
transform.read2(stream)
transforms.append(transform)
# Read and assign transform ids
for transformI in range(transformCount):
transformID, = unpackStream("<I", stream)
self.transforms[transformID] = transforms[transformI]
'''
Object data blocks
'''
def readObjectBlock2(self, stream):
# Verify signature
signature, _, objectCount = unpackStream("<3I", stream)
if signature != A3D_OBJECTBLOCK_SIGNATURE:
print(f"{stream.tell()}")
raise RuntimeError(f"Invalid object data block signature: {signature}")
# Read data
print(f"Reading object block with {objectCount} objects")
for _ in range(objectCount):
objec = A3DObjects.A3DObject()
objec.read2(stream)
self.objects.append(objec)

135
io_scene_a3d/A3DObjects.py Normal file
View File

@@ -0,0 +1,135 @@
'''
Copyright (c) 2024 Pyogenics <https://github.com/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.
'''
from .IOTools import unpackStream, readNullTerminatedString
class A3DMaterial:
def __init__(self):
self.name = ""
self.color = (0.0, 0.0, 0.0)
self.diffuseMap = ""
def read2(self, stream):
self.name = readNullTerminatedString(stream)
self.color = unpackStream("<3f", stream)
self.diffuseMap = readNullTerminatedString(stream)
print(f"[A3DMaterial name: {self.name} color: {self.color} diffuse map: {self.diffuseMap}]")
def read3(self, stream):
pass
class A3DMesh:
def __init__(self):
self.vertexBuffers = []
self.submeshes = []
def read2(self, stream):
# Read vertex buffers
vertexCount, bufferCount = unpackStream("<2I", stream)
for _ in range(bufferCount):
vertexBuffer = A3DVertexBuffer()
vertexBuffer.read2(vertexCount, stream)
self.vertexBuffers.append(vertexBuffer)
# Read submeshes
submeshCount, = unpackStream("<I", stream)
for _ in range(submeshCount):
submesh = A3DSubmesh()
submesh.read2(stream)
self.submeshes.append(submesh)
print(f"[A3DMesh vertex buffers: {len(self.vertexBuffers)} submeshes: {len(self.submeshes)}]")
A3D_VERTEXTYPE_COORDINATE = 1
A3D_VERTEXTYPE_UV1 = 2
A3D_VERTEXTYPE_NORMAL1 = 3
A3D_VERTEXTYPE_UV2 = 4
A3D_VERTEXTYPE_COLOR = 5
A3D_VERTEXTYPE_NORMAL2 = 6
# LUT for vertex buffer types -> vertex size
A3DVertexSize = {
A3D_VERTEXTYPE_COORDINATE: 3,
A3D_VERTEXTYPE_UV1: 2,
A3D_VERTEXTYPE_NORMAL1: 3,
A3D_VERTEXTYPE_UV2: 2,
A3D_VERTEXTYPE_COLOR: 4,
A3D_VERTEXTYPE_NORMAL2: 3
}
class A3DVertexBuffer:
def __init__(self):
self.data = []
self.bufferType = None
def read2(self, vertexCount, stream):
self.bufferType, = unpackStream("<I", stream)
if not (self.bufferType in A3DVertexSize.keys()):
raise RuntimeError(f"Unknown vertex buffer type: {self.bufferType}")
for _ in range(vertexCount):
vertexSize = A3DVertexSize[self.bufferType]
vertex = unpackStream(f"<{vertexSize}f", stream)
self.data.append(vertex)
print(f"[A3DVertexBuffer data: {len(self.data)} buffer type: {self.bufferType}]")
class A3DSubmesh:
def __init__(self):
self.faces = []
self.smoothingGroups = []
self.materialID = None
def read2(self, stream):
faceCount, = unpackStream("<I", stream)
for _ in range(faceCount):
face = unpackStream("<3H", stream)
self.faces.append(face)
for _ in range(faceCount):
smoothingGroup, = unpackStream("<I", stream)
self.smoothingGroups.append(smoothingGroup)
self.materialID, = unpackStream("<H", stream)
print(f"[A3DSubmesh faces: {len(self.faces)} smoothing groups: {len(self.smoothingGroups)} materialID: {self.materialID}]")
class A3DTransform:
def __init__(self):
self.position = (0.0, 0.0, 0.0)
self.rotation = (0.0, 0.0, 0.0, 0.0)
self.scale = (0.0, 0.0, 0.0)
def read2(self, stream):
self.position = unpackStream("<3f", stream)
self.rotation = unpackStream("<4f", stream)
self.scale = unpackStream("<3f", stream)
print(f"[A3DTransform position: {self.position} rotation: {self.rotation} scale: {self.scale}]")
class A3DObject:
def __init__(self):
self.name = ""
self.meshID = None
self.transformID = None
def read2(self, stream):
self.name = readNullTerminatedString(stream)
self.meshID, self.transformID = unpackStream("<2I", stream)
print(f"[A3DObject name: {self.name} meshID: {self.meshID} transformID: {self.transformID}]")

36
io_scene_a3d/IOTools.py Normal file
View File

@@ -0,0 +1,36 @@
'''
Copyright (c) 2024 Pyogenics <https://github.com/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.
'''
from struct import unpack, calcsize
def unpackStream(format, stream):
size = calcsize(format)
data = stream.read(size)
return unpack(format, data)
def readNullTerminatedString(stream):
string = b""
char = stream.read(1)
while char != b"\x00":
string += char
char = stream.read(1)
return string.decode("utf8")

6
io_scene_a3d/__main__.py Normal file
View File

@@ -0,0 +1,6 @@
from sys import argv
from .A3D import A3D
with open(argv[1], "rb") as f:
model = A3D()
model.read(f)