mirror of
https://github.com/MapMakersAndProgrammers/io_scene_a3d.git
synced 2025-10-25 09:29:07 -07:00
Merge initial a3d export support
Initial a3d export support
This commit is contained in:
@@ -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("<I", buffer, len(self.materials))
|
||||
for material in self.materials:
|
||||
material.write2(buffer)
|
||||
|
||||
# Write buffer to stream
|
||||
packStream("<2I", stream, A3D_MATERIALBLOCK_SIGNATURE, buffer.tell())
|
||||
buffer.seek(0, 0)
|
||||
stream.write(buffer.read())
|
||||
|
||||
def readMaterialBlock3(self, stream):
|
||||
# Verify signature
|
||||
signature, length, materialCount = unpackStream("<3I", stream)
|
||||
@@ -134,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("<I", buffer, len(self.materials))
|
||||
for material in self.materials:
|
||||
material.write3(buffer)
|
||||
|
||||
# Write buffer to stream
|
||||
packStream("<2I", stream, A3D_MATERIALBLOCK_SIGNATURE, buffer.tell())
|
||||
buffer.seek(0, 0)
|
||||
stream.write(buffer.read())
|
||||
|
||||
# Padding
|
||||
paddingSize = calculatePadding(buffer.tell())
|
||||
stream.write(b"\x00" * paddingSize)
|
||||
|
||||
'''
|
||||
Mesh data blocks
|
||||
'''
|
||||
@@ -150,6 +234,20 @@ class A3D:
|
||||
mesh.read2(stream)
|
||||
self.meshes.append(mesh)
|
||||
|
||||
def writeMeshBlock2(self, stream):
|
||||
buffer = BytesIO()
|
||||
|
||||
# Write data to the buffer
|
||||
print("Writing mesh block")
|
||||
packStream("<I", buffer, len(self.meshes))
|
||||
for mesh in self.meshes:
|
||||
mesh.write2(buffer)
|
||||
|
||||
# Write buffer to stream
|
||||
packStream("<2I", stream, A3D_MESHBLOCK_SIGNATURE, buffer.tell())
|
||||
buffer.seek(0, 0)
|
||||
stream.write(buffer.read())
|
||||
|
||||
def readMeshBlock3(self, stream):
|
||||
# Verify signature
|
||||
signature, length, meshCount = unpackStream("<3I", stream)
|
||||
@@ -167,6 +265,24 @@ class A3D:
|
||||
padding = calculatePadding(length)
|
||||
stream.read(padding)
|
||||
|
||||
def writeMeshBlock3(self, stream):
|
||||
buffer = BytesIO()
|
||||
|
||||
# Write data to the buffer
|
||||
print("Writing mesh block")
|
||||
packStream("<I", buffer, len(self.meshes))
|
||||
for mesh in self.meshes:
|
||||
mesh.write3(buffer)
|
||||
|
||||
# Write buffer to stream
|
||||
packStream("<2I", stream, A3D_MESHBLOCK_SIGNATURE, buffer.tell())
|
||||
buffer.seek(0, 0)
|
||||
stream.write(buffer.read())
|
||||
|
||||
# Padding
|
||||
paddingSize = calculatePadding(buffer.tell())
|
||||
stream.write(b"\x00" * paddingSize)
|
||||
|
||||
'''
|
||||
Transform data blocks
|
||||
'''
|
||||
@@ -187,6 +303,22 @@ class A3D:
|
||||
parentID, = unpackStream("<i", stream)
|
||||
self.transformParentIDs.append(parentID)
|
||||
|
||||
def writeTransformBlock2(self, stream):
|
||||
buffer = BytesIO()
|
||||
|
||||
# Write data to the buffer
|
||||
print("Writing transform block")
|
||||
packStream("<I", buffer, len(self.transforms))
|
||||
for transform in self.transforms:
|
||||
transform.write2(buffer)
|
||||
for parentID in self.transformParentIDs:
|
||||
packStream("<i", buffer, parentID)
|
||||
|
||||
# Write buffer to stream
|
||||
packStream("<2I", stream, A3D_TRANSFORMBLOCK_SIGNATURE, buffer.tell())
|
||||
buffer.seek(0, 0)
|
||||
stream.write(buffer.read())
|
||||
|
||||
def readTransformBlock3(self, stream):
|
||||
# Verify signature
|
||||
signature, length, transformCount = unpackStream("<3I", stream)
|
||||
@@ -195,7 +327,6 @@ class A3D:
|
||||
|
||||
# Read data
|
||||
print(f"Reading transform block with {transformCount} transforms and length {length}")
|
||||
transforms = []
|
||||
for _ in range(transformCount):
|
||||
transform = A3DObjects.A3DTransform()
|
||||
transform.read3(stream)
|
||||
@@ -209,6 +340,26 @@ class A3D:
|
||||
padding = calculatePadding(length)
|
||||
stream.read(padding)
|
||||
|
||||
def writeTransformBlock3(self, stream):
|
||||
buffer = BytesIO()
|
||||
|
||||
# Write data to the buffer
|
||||
print("Writing transform block")
|
||||
packStream("<I", buffer, len(self.transforms))
|
||||
for transform in self.transforms:
|
||||
transform.write3(buffer)
|
||||
for parentID in self.transformParentIDs:
|
||||
packStream("<i", buffer, parentID)
|
||||
|
||||
# Write buffer to stream
|
||||
packStream("<2I", stream, A3D_TRANSFORMBLOCK_SIGNATURE, buffer.tell())
|
||||
buffer.seek(0, 0)
|
||||
stream.write(buffer.read())
|
||||
|
||||
# Padding
|
||||
paddingSize = calculatePadding(buffer.tell())
|
||||
stream.write(b"\x00" * paddingSize)
|
||||
|
||||
'''
|
||||
Object data blocks
|
||||
'''
|
||||
@@ -225,6 +376,20 @@ class A3D:
|
||||
objec.read2(stream)
|
||||
self.objects.append(objec)
|
||||
|
||||
def writeObjectBlock2(self, stream):
|
||||
buffer = BytesIO()
|
||||
|
||||
# Write data to the buffer
|
||||
print("Writing object block")
|
||||
packStream("<I", buffer, len(self.objects))
|
||||
for objec in self.objects:
|
||||
objec.write2(buffer)
|
||||
|
||||
# Write buffer to stream
|
||||
packStream("<2I", stream, A3D_OBJECTBLOCK_SIGNATURE, buffer.tell())
|
||||
buffer.seek(0, 0)
|
||||
stream.write(buffer.read())
|
||||
|
||||
def readObjectBlock3(self, stream):
|
||||
# Verify signature
|
||||
signature, length, objectCount = unpackStream("<3I", stream)
|
||||
@@ -240,4 +405,22 @@ class A3D:
|
||||
|
||||
# Padding
|
||||
padding = calculatePadding(length)
|
||||
stream.read(padding)
|
||||
stream.read(padding)
|
||||
|
||||
def writeObjectBlock3(self, stream):
|
||||
buffer = BytesIO()
|
||||
|
||||
# Write data to the buffer
|
||||
print("Writing object block")
|
||||
packStream("<I", buffer, len(self.objects))
|
||||
for objec in self.objects:
|
||||
objec.write3(buffer)
|
||||
|
||||
# Write buffer to stream
|
||||
packStream("<2I", stream, A3D_OBJECTBLOCK_SIGNATURE, buffer.tell())
|
||||
buffer.seek(0, 0)
|
||||
stream.write(buffer.read())
|
||||
|
||||
# Padding
|
||||
paddingSize = calculatePadding(buffer.tell())
|
||||
stream.write(b"\x00" * paddingSize)
|
||||
|
||||
181
io_scene_a3d/A3DBlenderExporter.py
Normal file
181
io_scene_a3d/A3DBlenderExporter.py
Normal file
@@ -0,0 +1,181 @@
|
||||
'''
|
||||
Copyright (c) 2025 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 . 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
|
||||
@@ -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
|
||||
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
|
||||
|
||||
@@ -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("<I", stream, self.submeshCount)
|
||||
for submesh in self.submeshes:
|
||||
submesh.write2(stream)
|
||||
|
||||
def read3(self, stream):
|
||||
# Read mesh info
|
||||
self.name = readLengthPrefixedString(stream)
|
||||
@@ -95,6 +116,24 @@ class A3DMesh:
|
||||
|
||||
print(f"[A3DMesh name: {self.name} bbox max: {self.bboxMax} bbox min: {self.bboxMin} vertex buffers: {len(self.vertexBuffers)} submeshes: {len(self.submeshes)}]")
|
||||
|
||||
def write3(self, stream):
|
||||
writeLengthPrefixedString(stream, self.name)
|
||||
bboxMaxX, bboxMaxY, bboxMaxZ = self.bboxMax
|
||||
packStream("<3f", stream, bboxMaxX, bboxMaxY, bboxMaxZ)
|
||||
bboxMinX, bboxMinY, bboxMinZ = self.bboxMin
|
||||
packStream("<3f", stream, bboxMinX, bboxMinY, bboxMinZ)
|
||||
packStream("<f", stream, 0.0) # XXX: Unknown float value!
|
||||
|
||||
# Write vertex buffers
|
||||
packStream("<2I", stream, self.vertexCount, self.vertexBufferCount)
|
||||
for vertexBuffer in self.vertexBuffers:
|
||||
vertexBuffer.write2(stream)
|
||||
|
||||
# Write submeshes
|
||||
packStream("<I", stream, self.submeshCount)
|
||||
for submesh in self.submeshes:
|
||||
submesh.write3(stream)
|
||||
|
||||
A3D_VERTEXTYPE_COORDINATE = 1
|
||||
A3D_VERTEXTYPE_UV1 = 2
|
||||
A3D_VERTEXTYPE_NORMAL1 = 3
|
||||
@@ -126,6 +165,12 @@ class A3DVertexBuffer:
|
||||
|
||||
print(f"[A3DVertexBuffer data: {len(self.data)} buffer type: {self.bufferType}]")
|
||||
|
||||
def write2(self, stream):
|
||||
packStream("<I", stream, self.bufferType)
|
||||
for vertex in self.data:
|
||||
for vertexElement in vertex:
|
||||
packStream("<f", stream, vertexElement)
|
||||
|
||||
class A3DSubmesh:
|
||||
def __init__(self):
|
||||
self.indices = []
|
||||
@@ -135,25 +180,43 @@ class A3DSubmesh:
|
||||
self.indexCount = 0
|
||||
|
||||
def read2(self, stream):
|
||||
self.indexCount, = unpackStream("<I", stream) # This is just the face count so multiply it by 3
|
||||
self.indexCount *= 3
|
||||
faceCount, = unpackStream("<I", stream)
|
||||
self.indexCount = faceCount * 3
|
||||
self.indices = list(unpackStream(f"<{self.indexCount}H", stream))
|
||||
self.smoothingGroups = list(unpackStream(f"<{self.indexCount//3}I", stream))
|
||||
self.materialID, = unpackStream("<H", stream)
|
||||
|
||||
print(f"[A3DSubmesh indices: {len(self.indices)} smoothing groups: {len(self.smoothingGroups)} materialID: {self.materialID}]")
|
||||
|
||||
def write2(self, stream):
|
||||
faceCount = self.indexCount // 3
|
||||
packStream("<I", stream, faceCount)
|
||||
for index in self.indices:
|
||||
packStream("<H", stream, index)
|
||||
for smoothingGroup in self.smoothingGroups:
|
||||
packStream("<I", stream, smoothingGroup)
|
||||
packStream("<H", stream, self.materialID)
|
||||
|
||||
def read3(self, stream):
|
||||
# Read indices
|
||||
self.indexCount, = unpackStream("<I", stream)
|
||||
self.indices = list(unpackStream(f"<{self.indexCount}H", stream))
|
||||
|
||||
# Padding
|
||||
padding = calculatePadding(self.indexCount*2) # Each index is 2 bytes
|
||||
stream.read(padding)
|
||||
paddingSize = calculatePadding(self.indexCount*2) # Each index is 2 bytes
|
||||
stream.read(paddingSize)
|
||||
|
||||
print(f"[A3DSubmesh indices: {len(self.indices)} smoothing groups: {len(self.smoothingGroups)} materialID: {self.materialID}]")
|
||||
|
||||
def write3(self, stream):
|
||||
packStream("<I", stream, self.indexCount)
|
||||
for index in self.indices:
|
||||
packStream("<H", stream, index)
|
||||
|
||||
# Padding
|
||||
paddingSize = calculatePadding(self.indexCount*2) # Each index is 2 bytes
|
||||
stream.write(b"\x00" * paddingSize)
|
||||
|
||||
class A3DTransform:
|
||||
def __init__(self):
|
||||
self.name = ""
|
||||
@@ -168,6 +231,14 @@ class A3DTransform:
|
||||
|
||||
print(f"[A3DTransform position: {self.position} rotation: {self.rotation} scale: {self.scale}]")
|
||||
|
||||
def write2(self, stream):
|
||||
positionX, positionY, positionZ = self.position
|
||||
packStream("<3f", stream, positionX, positionY, positionZ)
|
||||
rotationX, rotationY, rotationZ, rotationW = self.rotation
|
||||
packStream("<4f", stream, rotationX, rotationY, rotationZ, rotationW)
|
||||
scaleX, scaleY, scaleZ = self.scale
|
||||
packStream("<3f", stream, scaleX, scaleY, scaleZ)
|
||||
|
||||
def read3(self, stream):
|
||||
self.name = readLengthPrefixedString(stream)
|
||||
self.position = unpackStream("<3f", stream)
|
||||
@@ -176,6 +247,15 @@ class A3DTransform:
|
||||
|
||||
print(f"[A3DTransform name: {self.name} position: {self.position} rotation: {self.rotation} scale: {self.scale}]")
|
||||
|
||||
def write3(self, stream):
|
||||
writeLengthPrefixedString(stream, self.name)
|
||||
positionX, positionY, positionZ = self.position
|
||||
packStream("<3f", stream, positionX, positionY, positionZ)
|
||||
rotationX, rotationY, rotationZ, rotationW = self.rotation
|
||||
packStream("<4f", stream, rotationX, rotationY, rotationZ, rotationW)
|
||||
scaleX, scaleY, scaleZ = self.scale
|
||||
packStream("<3f", stream, scaleX, scaleY, scaleZ)
|
||||
|
||||
class A3DObject:
|
||||
def __init__(self):
|
||||
self.name = ""
|
||||
@@ -191,6 +271,10 @@ class A3DObject:
|
||||
|
||||
print(f"[A3DObject name: {self.name} meshID: {self.meshID} transformID: {self.transformID} materialIDs: {len(self.materialIDs)}]")
|
||||
|
||||
def write2(self, stream):
|
||||
writeNullTerminatedString(stream, self.name)
|
||||
packStream("<2I", stream, self.meshID, self.transformID)
|
||||
|
||||
def read3(self, stream):
|
||||
self.meshID, self.transformID, self.materialCount = unpackStream("<3I", stream)
|
||||
|
||||
@@ -199,4 +283,9 @@ class A3DObject:
|
||||
materialID, = unpackStream("<i", stream)
|
||||
self.materialIDs.append(materialID)
|
||||
|
||||
print(f"[A3DObject name: {self.name} meshID: {self.meshID} transformID: {self.transformID} materialIDs: {len(self.materialIDs)}]")
|
||||
print(f"[A3DObject name: {self.name} meshID: {self.meshID} transformID: {self.transformID} materialIDs: {len(self.materialIDs)}]")
|
||||
|
||||
def write3(self, stream):
|
||||
packStream("<3I", stream, self.meshID, self.transformID, self.materialCount)
|
||||
for materialID in self.materialIDs:
|
||||
packStream("<i", stream, materialID)
|
||||
|
||||
@@ -20,20 +20,29 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
'''
|
||||
|
||||
from struct import unpack, calcsize
|
||||
from struct import unpack, pack, calcsize
|
||||
|
||||
def unpackStream(format, stream):
|
||||
size = calcsize(format)
|
||||
data = stream.read(size)
|
||||
return unpack(format, data)
|
||||
|
||||
def packStream(format, stream, *data):
|
||||
packedData = pack(format, *data)
|
||||
stream.write(packedData)
|
||||
|
||||
def readNullTerminatedString(stream):
|
||||
string = b""
|
||||
char = stream.read(1)
|
||||
while char != b"\x00":
|
||||
string += char
|
||||
char = stream.read(1)
|
||||
return string.decode("utf8", errors="ignore")
|
||||
return string.decode("utf-8", errors="ignore")
|
||||
|
||||
def writeNullTerminatedString(stream, string):
|
||||
string = string.encode("utf-8")
|
||||
stream.write(string)
|
||||
stream.write(b"\x00")
|
||||
|
||||
def calculatePadding(length):
|
||||
# (it basically works with rounding)
|
||||
@@ -47,4 +56,13 @@ def readLengthPrefixedString(stream):
|
||||
paddingSize = calculatePadding(length)
|
||||
stream.read(paddingSize)
|
||||
|
||||
return string.decode("utf8", errors="ignore")
|
||||
return string.decode("utf8", errors="ignore")
|
||||
|
||||
def writeLengthPrefixedString(stream, string):
|
||||
string = string.encode("utf-8")
|
||||
|
||||
packStream("<I", stream, len(string))
|
||||
stream.write(string)
|
||||
|
||||
paddingSize = calculatePadding(len(string))
|
||||
stream.write(b"\x00" * paddingSize)
|
||||
@@ -22,11 +22,12 @@ SOFTWARE.
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator, OperatorFileListElement, AddonPreferences
|
||||
from bpy.props import StringProperty, BoolProperty, CollectionProperty, FloatProperty
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from bpy.props import StringProperty, BoolProperty, CollectionProperty, FloatProperty, EnumProperty
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
|
||||
from .A3D import A3D
|
||||
from .A3DBlenderImporter import A3DBlenderImporter
|
||||
from .A3DBlenderExporter import A3DBlenderExporter
|
||||
from .BattleMap import BattleMap
|
||||
from .BattleMapBlenderImporter import BattleMapBlenderImporter
|
||||
from .LightmapData import LightmapData
|
||||
@@ -99,6 +100,44 @@ class ImportA3D(Operator, ImportHelper):
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
class ExportA3D(Operator, ExportHelper):
|
||||
bl_idname = "export_scene.alternativa"
|
||||
bl_label = "Export A3D"
|
||||
bl_description = "Export an A3D model"
|
||||
bl_options = {'PRESET', 'UNDO'}
|
||||
|
||||
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):
|
||||
export_panel_options_a3d(self.layout, self)
|
||||
|
||||
def invoke(self, context, event):
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
|
||||
def execute(self, context):
|
||||
print(f"Exporting blender data to {self.filepath}")
|
||||
|
||||
modelData = A3D()
|
||||
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=int(self.a3d_version))
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
class ImportBattleMap(Operator, ImportHelper):
|
||||
bl_idname = "import_scene.tanki_battlemap"
|
||||
bl_label = "Import map"
|
||||
@@ -167,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")
|
||||
@@ -180,6 +225,9 @@ def import_panel_options_battlemap(layout, operator):
|
||||
def menu_func_import_a3d(self, context):
|
||||
self.layout.operator(ImportA3D.bl_idname, text="Alternativa3D HTML5 (.a3d)")
|
||||
|
||||
def menu_func_export_a3d(self, context):
|
||||
self.layout.operator(ExportA3D.bl_idname, text="Alternativa3D HTML5 (.a3d)")
|
||||
|
||||
def menu_func_import_battlemap(self, context):
|
||||
self.layout.operator(ImportBattleMap.bl_idname, text="Tanki Online BattleMap (.bin)")
|
||||
|
||||
@@ -189,6 +237,7 @@ Registration
|
||||
classes = [
|
||||
Preferences,
|
||||
ImportA3D,
|
||||
ExportA3D,
|
||||
ImportBattleMap
|
||||
]
|
||||
|
||||
@@ -196,12 +245,14 @@ def register():
|
||||
for c in classes:
|
||||
bpy.utils.register_class(c)
|
||||
bpy.types.TOPBAR_MT_file_import.append(menu_func_import_a3d)
|
||||
bpy.types.TOPBAR_MT_file_export.append(menu_func_export_a3d)
|
||||
bpy.types.TOPBAR_MT_file_import.append(menu_func_import_battlemap)
|
||||
|
||||
def unregister():
|
||||
for c in classes:
|
||||
bpy.utils.unregister_class(c)
|
||||
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import_a3d)
|
||||
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export_a3d)
|
||||
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import_battlemap)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user