Compare commits
46 Commits
1.0.0-init
...
v1.0.0-bin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8fe36205b | ||
|
|
1fc2856e84 | ||
|
|
4851dec975 | ||
|
|
20ed280a9d | ||
|
|
b1794014dc | ||
|
|
91ee61d692 | ||
|
|
d971ebade3 | ||
|
|
4866a3ff8a | ||
|
|
4b2ba7eba1 | ||
|
|
8a96286bae | ||
|
|
5b35a26c6e | ||
|
|
8a41d7a47a | ||
|
|
79a6d9d786 | ||
|
|
45a32c2ba4 | ||
|
|
2ce9b3aa75 | ||
|
|
bbabbda16f | ||
|
|
f07e9a58ee | ||
|
|
8141194dc1 | ||
|
|
a245b6b1a0 | ||
|
|
6cfab91dc4 | ||
|
|
46c8b0ebdf | ||
|
|
1d35ea7b0f | ||
|
|
a4d62b33e7 | ||
|
|
34d40f70a2 | ||
|
|
f9de035859 | ||
|
|
ac96886e46 | ||
|
|
880746a9ce | ||
|
|
92d66a20d4 | ||
|
|
caf3caee50 | ||
|
|
d3988cd10f | ||
|
|
db68e3c8f4 | ||
|
|
24dbdaeb1c | ||
|
|
044b7338ad | ||
|
|
02a87f4a05 | ||
|
|
45314b11e8 | ||
|
|
110c387ec9 | ||
|
|
73fce85791 | ||
|
|
67327c7fb5 | ||
|
|
11fdb83627 | ||
|
|
e4e395b6e1 | ||
|
|
643d23d6e7 | ||
|
|
696a65e5a2 | ||
|
|
e8fd653c80 | ||
|
|
019b0c54e8 | ||
|
|
eb9e774592 | ||
|
|
647c665566 |
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Build and cache
|
||||||
|
__pycache__/
|
||||||
|
*.zip
|
||||||
|
|
||||||
|
# Editor files
|
||||||
|
.venv/
|
||||||
|
.vscode/
|
||||||
67
README.md
@@ -1,7 +1,64 @@
|
|||||||
# io_scene_a3d
|
# WIP io_scene_a3d
|
||||||
Blender plugin to load A3D 3.2 and 3.3 models (3.1 not supported), 3.2 is most complete 3.3 is not so complete; this code will eventually be merged into the alternativa3d_tools github repo.
|
Blender plugin to import the proprietary model format `A3D` used by the game [Tanki Online](https://tankionline.com/en/) from [Alternativa Games](https://alternativa.games/), it is not compatible with older the formats used by the flash based Alternativa3D engine (see [this plugin by Davide Jones](https://github.com/davidejones/alternativa3d_tools) instead).
|
||||||
|
|
||||||
The code can read all the A3D3 data but not all of it is imported yet, some material data + transforms + some vertex data and the codebase could use a cleanup.
|
## File format
|
||||||
|
Check the wiki for file format documentation.
|
||||||
|
|
||||||
- https://github.com/davidejones/alternativa3d_tools/issues/9
|
## Installation
|
||||||
- https://github.com/davidejones/alternativa3d_tools
|
### Requirments: Blender version 4.2+
|
||||||
|
|
||||||
|
Firstly download the repository by clicking the "Code" button and then "Download ZIP".<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
In blender, go to Edit > Preferences and click the "add-ons" button. From there click the arrow on the top right and click "Install from disk".<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
Select the zip folder you downloaded and you should be good to go.
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|

|
||||||
|
|
||||||
|
## Status
|
||||||
|
### Work in progress, the project is mostly complete for readonly file access.
|
||||||
|
### A3D1
|
||||||
|
No support, I have never seen one of these files and 99.999% of people will only be using A3D2 and A3D3 files so there isn't much point supporting them.
|
||||||
|
### A3D2
|
||||||
|
Full readonly support, not all data is imported into blender.
|
||||||
|
#### Import
|
||||||
|
- [x] Materials
|
||||||
|
- diffuse map data is not used by the plugin because it references files that are only accessible if you work at Alternativa Games (such as texture `.psd` source files)
|
||||||
|
- [x] Meshes
|
||||||
|
- - [x] Submesh data
|
||||||
|
- - [x] Coordinates
|
||||||
|
- - [ ] Normals (data not imported into blender)
|
||||||
|
- - [x] UVs
|
||||||
|
- - [ ] Vertex colour (data not imported into blender, not very useful anyway)
|
||||||
|
- - [ ] Smoothing groups
|
||||||
|
- [x] Transform
|
||||||
|
- [x] Object data
|
||||||
|
#### Export
|
||||||
|
- [ ] Materials
|
||||||
|
- [ ] Meshes
|
||||||
|
- [ ] Transfoms
|
||||||
|
- [ ] Objects
|
||||||
|
### A3D3
|
||||||
|
Full readonly support, not all data is imported into blender.
|
||||||
|
#### Import
|
||||||
|
- [x] Materials
|
||||||
|
- diffuse map data is not used by the plugin because it references files that are only accessible if you work at Alternativa Games (such as texture `.psd` source files)
|
||||||
|
- [x] Meshes
|
||||||
|
- - [x] Submesh data
|
||||||
|
- - [x] Coordinates
|
||||||
|
- - [ ] Normals (data not imported into blender)
|
||||||
|
- - [x] UVs
|
||||||
|
- - [ ] Vertex colour (data not imported into blender, not very useful anyway)
|
||||||
|
- - [ ] Boundbox (data not imported into blender, blender calculates its own boundbox data)
|
||||||
|
- [x] Transforms
|
||||||
|
- [x] Objects
|
||||||
|
#### Export
|
||||||
|
- [ ] Materials
|
||||||
|
- [ ] Meshes
|
||||||
|
- [ ] Transfoms
|
||||||
|
- [ ] Objects
|
||||||
|
|||||||
BIN
images/demo1.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
images/demo2.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
images/demo3.png
Normal file
|
After Width: | Height: | Size: 469 KiB |
BIN
images/demo4.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
images/demo5.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
images/demo6.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
images/demo7.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
images/demo8.png
Normal file
|
After Width: | Height: | Size: 975 KiB |
BIN
images/step1.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
images/step2.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
243
io_scene_a3d/A3D.py
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
'''
|
||||||
|
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, calculatePadding
|
||||||
|
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.version = 0
|
||||||
|
|
||||||
|
self.materials = []
|
||||||
|
self.meshes = []
|
||||||
|
self.transforms = []
|
||||||
|
self.transformParentIDs = []
|
||||||
|
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
|
||||||
|
self.version, _ = unpackStream("<2H", stream) # Likely major.minor version code
|
||||||
|
print(f"Reading A3D version {self.version}")
|
||||||
|
|
||||||
|
if self.version == 1:
|
||||||
|
self.readRootBlock1(stream)
|
||||||
|
elif self.version == 2:
|
||||||
|
self.readRootBlock2(stream)
|
||||||
|
elif self.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):
|
||||||
|
# Verify signature
|
||||||
|
signature, length = unpackStream("<2I", stream)
|
||||||
|
if signature != A3D_ROOTBLOCK_SIGNATURE:
|
||||||
|
raise RuntimeError(f"Invalid root data block signature: {signature}")
|
||||||
|
|
||||||
|
# Read data
|
||||||
|
self.readMaterialBlock3(stream)
|
||||||
|
self.readMeshBlock3(stream)
|
||||||
|
self.readTransformBlock3(stream)
|
||||||
|
self.readObjectBlock3(stream)
|
||||||
|
|
||||||
|
# Padding
|
||||||
|
padding = calculatePadding(length)
|
||||||
|
stream.read(padding)
|
||||||
|
|
||||||
|
'''
|
||||||
|
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)
|
||||||
|
|
||||||
|
def readMaterialBlock3(self, stream):
|
||||||
|
# Verify signature
|
||||||
|
signature, length, 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 and length {length}")
|
||||||
|
for _ in range(materialCount):
|
||||||
|
material = A3DObjects.A3DMaterial()
|
||||||
|
material.read3(stream)
|
||||||
|
self.materials.append(material)
|
||||||
|
|
||||||
|
# Padding
|
||||||
|
padding = calculatePadding(length)
|
||||||
|
stream.read(padding)
|
||||||
|
|
||||||
|
'''
|
||||||
|
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)
|
||||||
|
|
||||||
|
def readMeshBlock3(self, stream):
|
||||||
|
# Verify signature
|
||||||
|
signature, length, 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 and length {length}")
|
||||||
|
for _ in range(meshCount):
|
||||||
|
mesh = A3DObjects.A3DMesh()
|
||||||
|
mesh.read3(stream)
|
||||||
|
self.meshes.append(mesh)
|
||||||
|
|
||||||
|
# Padding
|
||||||
|
padding = calculatePadding(length)
|
||||||
|
stream.read(padding)
|
||||||
|
|
||||||
|
'''
|
||||||
|
Transform data blocks
|
||||||
|
'''
|
||||||
|
def readTransformBlock2(self, stream):
|
||||||
|
# Verify signature
|
||||||
|
signature, _, transformCount = unpackStream("<3I", stream)
|
||||||
|
if signature != A3D_TRANSFORMBLOCK_SIGNATURE:
|
||||||
|
raise RuntimeError(f"Invalid transform data block signature: {signature}")
|
||||||
|
|
||||||
|
# Read data
|
||||||
|
print(f"Reading transform block with {transformCount} transforms")
|
||||||
|
for _ in range(transformCount):
|
||||||
|
transform = A3DObjects.A3DTransform()
|
||||||
|
transform.read2(stream)
|
||||||
|
self.transforms.append(transform)
|
||||||
|
# Read parent ids
|
||||||
|
for _ in range(transformCount):
|
||||||
|
parentID, = unpackStream("<i", stream)
|
||||||
|
self.transformParentIDs.append(parentID)
|
||||||
|
|
||||||
|
def readTransformBlock3(self, stream):
|
||||||
|
# Verify signature
|
||||||
|
signature, length, transformCount = unpackStream("<3I", stream)
|
||||||
|
if signature != A3D_TRANSFORMBLOCK_SIGNATURE:
|
||||||
|
raise RuntimeError(f"Invalid transform data block signature: {signature}")
|
||||||
|
|
||||||
|
# Read data
|
||||||
|
print(f"Reading transform block with {transformCount} transforms and length {length}")
|
||||||
|
transforms = []
|
||||||
|
for _ in range(transformCount):
|
||||||
|
transform = A3DObjects.A3DTransform()
|
||||||
|
transform.read3(stream)
|
||||||
|
self.transforms.append(transform)
|
||||||
|
# Read parent ids
|
||||||
|
for _ in range(transformCount):
|
||||||
|
parentID, = unpackStream("<i", stream)
|
||||||
|
self.transformParentIDs.append(parentID)
|
||||||
|
|
||||||
|
# Padding
|
||||||
|
padding = calculatePadding(length)
|
||||||
|
stream.read(padding)
|
||||||
|
|
||||||
|
'''
|
||||||
|
Object data blocks
|
||||||
|
'''
|
||||||
|
def readObjectBlock2(self, stream):
|
||||||
|
# Verify signature
|
||||||
|
signature, _, objectCount = unpackStream("<3I", stream)
|
||||||
|
if signature != A3D_OBJECTBLOCK_SIGNATURE:
|
||||||
|
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)
|
||||||
|
|
||||||
|
def readObjectBlock3(self, stream):
|
||||||
|
# Verify signature
|
||||||
|
signature, length, objectCount = unpackStream("<3I", stream)
|
||||||
|
if signature != A3D_OBJECTBLOCK_SIGNATURE:
|
||||||
|
raise RuntimeError(f"Invalid object data block signature: {signature}")
|
||||||
|
|
||||||
|
# Read data
|
||||||
|
print(f"Reading object block with {objectCount} objects and length {length}")
|
||||||
|
for _ in range(objectCount):
|
||||||
|
objec = A3DObjects.A3DObject()
|
||||||
|
objec.read3(stream)
|
||||||
|
self.objects.append(objec)
|
||||||
|
|
||||||
|
# Padding
|
||||||
|
padding = calculatePadding(length)
|
||||||
|
stream.read(padding)
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
'''
|
|
||||||
Copyright (C) 2024 Pyogenics <https://www.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.
|
|
||||||
'''
|
|
||||||
|
|
||||||
class A3D3Mesh:
|
|
||||||
def __init__(self, coordinates, uv1, normals, uv2, colors, unknown, submeshes):
|
|
||||||
# Vertex data
|
|
||||||
self.coordinates = coordinates
|
|
||||||
self.uv1 = uv1
|
|
||||||
self.normals = normals
|
|
||||||
self.uv2 = uv2
|
|
||||||
self.colors = colors
|
|
||||||
self.unknown = unknown
|
|
||||||
|
|
||||||
self.submeshes = submeshes
|
|
||||||
self.faces = [] # Aggregate of all submesh face data, easier for blender importing
|
|
||||||
|
|
||||||
# Object data
|
|
||||||
self.name = ""
|
|
||||||
self.transform = None
|
|
||||||
|
|
||||||
class A3D3Submesh:
|
|
||||||
def __init__(self, faces, smoothingGroups, material):
|
|
||||||
self.faces = faces
|
|
||||||
self.smoothingGroups = smoothingGroups
|
|
||||||
self.material = material
|
|
||||||
|
|
||||||
class A3D3Transform:
|
|
||||||
def __init__(self, position, rotation, scale, name):
|
|
||||||
self.position = position
|
|
||||||
self.rotation = rotation
|
|
||||||
self.scale = scale
|
|
||||||
self.name = name
|
|
||||||
self.parentID = 0
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
'''
|
|
||||||
Copyright (C) 2024 Pyogenics <https://www.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 .A3DIOTools import unpackStream, readNullTerminatedString
|
|
||||||
from .A3D3Shared import A3D3Mesh, A3D3Submesh, A3D3Transform
|
|
||||||
|
|
||||||
'''
|
|
||||||
A3D version 3 type 2
|
|
||||||
'''
|
|
||||||
class A3D3_2:
|
|
||||||
def __init__(self):
|
|
||||||
# Object data
|
|
||||||
self.materialNames = [] # Used to lookup names from materialID
|
|
||||||
self.materials = {}
|
|
||||||
self.meshes = []
|
|
||||||
self.transforms = []
|
|
||||||
|
|
||||||
'''
|
|
||||||
IO
|
|
||||||
'''
|
|
||||||
def readSubmesh(self, stream):
|
|
||||||
print("Reading submesh")
|
|
||||||
|
|
||||||
faceCount, = unpackStream("<I", stream)
|
|
||||||
faces = []
|
|
||||||
for _ in range(faceCount):
|
|
||||||
face = unpackStream("<3H", stream)
|
|
||||||
faces.append(face)
|
|
||||||
smoothGroups = []
|
|
||||||
for _ in range(faceCount):
|
|
||||||
smoothGroup, = unpackStream("<I", stream)
|
|
||||||
smoothGroups.append(smoothGroup)
|
|
||||||
materialID, = unpackStream("<H", stream)
|
|
||||||
|
|
||||||
material = self.materialNames[materialID]
|
|
||||||
submesh = A3D3Submesh(faces, smoothGroups, material)
|
|
||||||
return submesh
|
|
||||||
|
|
||||||
def readVertices(self, vertexCount, floatCount, stream):
|
|
||||||
vertices = []
|
|
||||||
for _ in range(vertexCount):
|
|
||||||
vertex = unpackStream(f"{floatCount}f", stream)
|
|
||||||
vertices.append(vertex)
|
|
||||||
return vertices
|
|
||||||
|
|
||||||
def readMaterialBlock(self, stream):
|
|
||||||
print("Reading material block")
|
|
||||||
marker, _, materialCount = unpackStream("<3I", stream)
|
|
||||||
if marker != 4:
|
|
||||||
raise RuntimeError(f"Invalid material block marker: {marker}")
|
|
||||||
|
|
||||||
for _ in range(materialCount):
|
|
||||||
materialName = readNullTerminatedString(stream)
|
|
||||||
_ = unpackStream("3f", stream)
|
|
||||||
diffuseMap = readNullTerminatedString(stream)
|
|
||||||
|
|
||||||
self.materialNames.append(materialName)
|
|
||||||
self.materials[materialName] = diffuseMap
|
|
||||||
|
|
||||||
def readMeshBlock(self, stream):
|
|
||||||
print("Reading mesh block")
|
|
||||||
marker, _, meshCount = unpackStream("<3I", stream)
|
|
||||||
if marker != 2:
|
|
||||||
raise RuntimeError(f"Invalid mesh block marker: {marker}")
|
|
||||||
|
|
||||||
for meshI in range(meshCount):
|
|
||||||
print(f"Reading mesh {meshI}")
|
|
||||||
|
|
||||||
# Vertices
|
|
||||||
coordinates = []
|
|
||||||
uv1 = []
|
|
||||||
normals = []
|
|
||||||
uv2 = []
|
|
||||||
colors = []
|
|
||||||
unknown = []
|
|
||||||
|
|
||||||
submeshes = []
|
|
||||||
|
|
||||||
# Read vertex buffers
|
|
||||||
vertexCount, vertexBufferCount = unpackStream("<2I", stream)
|
|
||||||
for vertexBufferI in range(vertexBufferCount):
|
|
||||||
print(f"Reading vertex buffer {vertexBufferI} with {vertexCount} vertices")
|
|
||||||
|
|
||||||
bufferType, = unpackStream("<I", stream)
|
|
||||||
if bufferType == 1:
|
|
||||||
coordinates = self.readVertices(vertexCount, 3, stream)
|
|
||||||
elif bufferType == 2:
|
|
||||||
uv1 = self.readVertices(vertexCount, 2, stream)
|
|
||||||
elif bufferType == 3:
|
|
||||||
normals = self.readVertices(vertexCount, 3, stream)
|
|
||||||
elif bufferType == 4:
|
|
||||||
uv2 = self.readVertices(vertexCount, 2, stream)
|
|
||||||
elif bufferType == 5:
|
|
||||||
colors = self.readVertices(vertexCount, 4, stream)
|
|
||||||
elif bufferType == 6:
|
|
||||||
unknown = self.readVertices(vertexCount, 3, stream)
|
|
||||||
else:
|
|
||||||
raise RuntimeError(f"Unknown vertex buffer type {bufferType}")
|
|
||||||
|
|
||||||
# Read submeshes
|
|
||||||
submeshCount, = unpackStream("<I", stream)
|
|
||||||
for _ in range(submeshCount):
|
|
||||||
submesh = self.readSubmesh(stream)
|
|
||||||
submeshes.append(submesh)
|
|
||||||
|
|
||||||
mesh = A3D3Mesh(coordinates, uv1, normals, uv2, colors, unknown, submeshes)
|
|
||||||
mesh.faces += submesh.faces
|
|
||||||
self.meshes.append(mesh)
|
|
||||||
|
|
||||||
def readTransformBlock(self, stream):
|
|
||||||
print("Reading transform block")
|
|
||||||
marker, _, transformCount = unpackStream("<3I", stream)
|
|
||||||
if marker != 3:
|
|
||||||
raise RuntimeError(f"Invalid transform block marker: {marker}")
|
|
||||||
|
|
||||||
for _ in range(transformCount):
|
|
||||||
position = unpackStream("<3f", stream)
|
|
||||||
rotation = unpackStream("<4f", stream)
|
|
||||||
scale = unpackStream("<3f", stream)
|
|
||||||
|
|
||||||
transform = A3D3Transform(position, rotation, scale, "")
|
|
||||||
self.transforms.append(transform)
|
|
||||||
for transformI in range(transformCount):
|
|
||||||
transformID, = unpackStream("<I", stream)
|
|
||||||
self.transforms[transformI].id = transformID
|
|
||||||
|
|
||||||
# Heirarchy data
|
|
||||||
def readObjectBlock(self, stream):
|
|
||||||
print("Reading object block")
|
|
||||||
marker, _, objectCount = unpackStream("<3I", stream)
|
|
||||||
if marker != 5:
|
|
||||||
raise RuntimeError(f"Invalid object block marker: {marker}")
|
|
||||||
|
|
||||||
for _ in range(objectCount):
|
|
||||||
objectName = readNullTerminatedString(stream)
|
|
||||||
meshID, transformID = unpackStream("<2I", stream)
|
|
||||||
|
|
||||||
self.meshes[meshID].transform = self.transforms[transformID]
|
|
||||||
self.meshes[meshID].name = objectName
|
|
||||||
|
|
||||||
'''
|
|
||||||
Drivers
|
|
||||||
'''
|
|
||||||
def read(self, stream):
|
|
||||||
print("Reading A3D3 type 2")
|
|
||||||
|
|
||||||
self.readMaterialBlock(stream)
|
|
||||||
self.readMeshBlock(stream)
|
|
||||||
self.readTransformBlock(stream)
|
|
||||||
self.readObjectBlock(stream)
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
'''
|
|
||||||
Copyright (C) 2024 Pyogenics <https://www.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 .A3DIOTools import unpackStream, readString, calculatePadding
|
|
||||||
from .A3D3Shared import A3D3Mesh, A3D3Submesh, A3D3Transform
|
|
||||||
|
|
||||||
'''
|
|
||||||
A3D version 3 type 3
|
|
||||||
'''
|
|
||||||
class A3D3_3:
|
|
||||||
def __init__(self):
|
|
||||||
self.materials = {}
|
|
||||||
self.materialNames = []
|
|
||||||
self.meshes = []
|
|
||||||
self.transforms = []
|
|
||||||
|
|
||||||
def readSubmesh(self, stream):
|
|
||||||
print("Reading submesh")
|
|
||||||
|
|
||||||
indexCount, = unpackStream("<I", stream)
|
|
||||||
faces = []
|
|
||||||
for _ in range(indexCount//3):
|
|
||||||
face = unpackStream("<3H", stream)
|
|
||||||
faces.append(face)
|
|
||||||
|
|
||||||
paddingSize = calculatePadding(indexCount*2)
|
|
||||||
stream.read(paddingSize)
|
|
||||||
|
|
||||||
submesh = A3D3Submesh(faces, [], 0) # XXX: Maybe this should be `None` instead of 0?
|
|
||||||
return submesh
|
|
||||||
|
|
||||||
def readVertices(self, vertexCount, floatCount, stream):
|
|
||||||
vertices = []
|
|
||||||
for _ in range(vertexCount):
|
|
||||||
vertex = unpackStream(f"{floatCount}f", stream)
|
|
||||||
vertices.append(vertex)
|
|
||||||
return vertices
|
|
||||||
|
|
||||||
def readMaterialBlock(self, stream):
|
|
||||||
print("Reading material block")
|
|
||||||
marker, _, materialCount = unpackStream("<3I", stream)
|
|
||||||
if marker != 4:
|
|
||||||
raise RuntimeError(f"Invalid material block marker: {marker}")
|
|
||||||
|
|
||||||
for _ in range(materialCount):
|
|
||||||
materialName = readString(stream)
|
|
||||||
floats = unpackStream("<3f", stream)
|
|
||||||
|
|
||||||
diffuseMap = readString(stream)
|
|
||||||
print(f"{materialName} {floats} {diffuseMap}")
|
|
||||||
|
|
||||||
self.materialNames.append(materialName)
|
|
||||||
self.materials[materialName] = diffuseMap
|
|
||||||
|
|
||||||
def readMeshBlock(self, stream):
|
|
||||||
print("Reading mesh block")
|
|
||||||
marker, _, meshCount = unpackStream("<3I", stream)
|
|
||||||
if marker != 2:
|
|
||||||
raise RuntimeError(f"Invalid mesh block marker: {marker}")
|
|
||||||
|
|
||||||
for meshI in range(meshCount):
|
|
||||||
print(f"Reading mesh {meshI} @ {stream.tell()}")
|
|
||||||
|
|
||||||
# Vertices
|
|
||||||
coordinates = []
|
|
||||||
uv1 = []
|
|
||||||
normals = []
|
|
||||||
uv2 = []
|
|
||||||
colors = []
|
|
||||||
unknown = []
|
|
||||||
|
|
||||||
submeshes = []
|
|
||||||
|
|
||||||
meshName = readString(stream)
|
|
||||||
unknownFloats = unpackStream("7f", stream)
|
|
||||||
print(f"{meshName} {unknownFloats}")
|
|
||||||
|
|
||||||
# Read vertex buffers
|
|
||||||
vertexCount, vertexBufferCount = unpackStream("<2I", stream)
|
|
||||||
for vertexBufferI in range(vertexBufferCount):
|
|
||||||
print(f"Reading vertex buffer {vertexBufferI} with {vertexCount} vertices")
|
|
||||||
|
|
||||||
bufferType, = unpackStream("<I", stream)
|
|
||||||
if bufferType == 1:
|
|
||||||
coordinates = self.readVertices(vertexCount, 3, stream)
|
|
||||||
elif bufferType == 2:
|
|
||||||
uv1 = self.readVertices(vertexCount, 2, stream)
|
|
||||||
elif bufferType == 3:
|
|
||||||
normals = self.readVertices(vertexCount, 3, stream)
|
|
||||||
elif bufferType == 4:
|
|
||||||
uv2 = self.readVertices(vertexCount, 2, stream)
|
|
||||||
elif bufferType == 5:
|
|
||||||
colors = self.readVertices(vertexCount, 4, stream)
|
|
||||||
elif bufferType == 6:
|
|
||||||
unknown = self.readVertices(vertexCount, 3, stream)
|
|
||||||
else:
|
|
||||||
raise RuntimeError(f"Unknown vertex buffer type {bufferType}")
|
|
||||||
|
|
||||||
# Read submeshes
|
|
||||||
submeshCount, = unpackStream("<I", stream)
|
|
||||||
for _ in range(submeshCount):
|
|
||||||
submesh = self.readSubmesh(stream)
|
|
||||||
submeshes.append(submesh)
|
|
||||||
|
|
||||||
mesh = A3D3Mesh(coordinates, uv1, normals, uv2, colors, unknown, submeshes)
|
|
||||||
mesh.faces += submesh.faces
|
|
||||||
self.meshes.append(mesh)
|
|
||||||
|
|
||||||
def readTransformBlock(self, stream):
|
|
||||||
print("Reading transform block")
|
|
||||||
marker, _, transformCount = unpackStream("<3I", stream)
|
|
||||||
if marker != 3:
|
|
||||||
raise RuntimeError(f"Invalid transform block marker: {marker}")
|
|
||||||
|
|
||||||
for _ in range(transformCount):
|
|
||||||
name = readString(stream)
|
|
||||||
position = unpackStream("<3f", stream)
|
|
||||||
rotation = unpackStream("<4f", stream)
|
|
||||||
scale = unpackStream("<3f", stream)
|
|
||||||
|
|
||||||
print(f"{name} {position} {rotation} {scale}")
|
|
||||||
transform = A3D3Transform(position, rotation, scale, name)
|
|
||||||
self.transforms.append(transform)
|
|
||||||
for transformI in range(transformCount):
|
|
||||||
transformID, = unpackStream("<I", stream)
|
|
||||||
self.transforms[transformI].id = transformID
|
|
||||||
|
|
||||||
# Heirarchy data
|
|
||||||
def readObjectBlock(self, stream):
|
|
||||||
print("Reading object block")
|
|
||||||
marker, _, objectCount = unpackStream("<3I", stream)
|
|
||||||
if marker != 5:
|
|
||||||
raise RuntimeError(f"Invalid object block marker: {marker}")
|
|
||||||
|
|
||||||
for _ in range(objectCount):
|
|
||||||
meshID, transformID, materialCount = unpackStream("<3I", stream)
|
|
||||||
for materialI in range(materialCount):
|
|
||||||
materialID, = unpackStream("<i", stream)
|
|
||||||
if materialID >= 0:
|
|
||||||
self.meshes[meshID].submeshes[materialI].material = self.materialNames[materialID]
|
|
||||||
|
|
||||||
self.meshes[meshID].transform = self.transforms[transformID]
|
|
||||||
|
|
||||||
def read(self, stream):
|
|
||||||
print("Reading A3D3 type 3")
|
|
||||||
|
|
||||||
self.readMaterialBlock(stream)
|
|
||||||
self.readMeshBlock(stream)
|
|
||||||
self.readTransformBlock(stream)
|
|
||||||
self.readObjectBlock(stream)
|
|
||||||
239
io_scene_a3d/A3DBlenderImporter.py
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
'''
|
||||||
|
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.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy_extras.node_shader_utils import PrincipledBSDFWrapper
|
||||||
|
from bpy_extras.image_utils import load_image
|
||||||
|
|
||||||
|
from .A3DObjects import (
|
||||||
|
A3D_VERTEXTYPE_COORDINATE,
|
||||||
|
A3D_VERTEXTYPE_UV1,
|
||||||
|
A3D_VERTEXTYPE_NORMAL1,
|
||||||
|
A3D_VERTEXTYPE_UV2,
|
||||||
|
A3D_VERTEXTYPE_COLOR,
|
||||||
|
A3D_VERTEXTYPE_NORMAL2
|
||||||
|
)
|
||||||
|
from .BlenderMaterialUtils import addImageTextureToMaterial
|
||||||
|
|
||||||
|
def mirrorUVY(uv):
|
||||||
|
x, y = uv
|
||||||
|
return (x, 1-y)
|
||||||
|
|
||||||
|
class A3DBlenderImporter:
|
||||||
|
def __init__(self, modelData, directory, reset_empty_transform=True, try_import_textures=True):
|
||||||
|
self.modelData = modelData
|
||||||
|
self.directory = directory
|
||||||
|
self.materials = []
|
||||||
|
self.meshes = []
|
||||||
|
|
||||||
|
# User settings
|
||||||
|
self.reset_empty_transform = reset_empty_transform
|
||||||
|
self.try_import_textures = try_import_textures
|
||||||
|
|
||||||
|
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
|
||||||
|
objects = []
|
||||||
|
for objectData in self.modelData.objects:
|
||||||
|
ob = self.buildBlenderObject(objectData)
|
||||||
|
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
|
||||||
|
continue
|
||||||
|
elif parentID == -1:
|
||||||
|
# version 3 models use -1 to signify empty parent
|
||||||
|
continue
|
||||||
|
parentOB = objects[parentID]
|
||||||
|
ob.parent = parentOB
|
||||||
|
|
||||||
|
return objects
|
||||||
|
|
||||||
|
'''
|
||||||
|
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
|
||||||
|
blenderCoordinates = []
|
||||||
|
for coordinate in coordinates:
|
||||||
|
blenderCoordinates += coordinate # Blender doesn't like tuples
|
||||||
|
me.vertices.add(len(blenderCoordinates)//3)
|
||||||
|
me.vertices.foreach_set("co", blenderCoordinates)
|
||||||
|
# Aggregate submesh data and import
|
||||||
|
indices = []
|
||||||
|
for submesh in meshData.submeshes:
|
||||||
|
indices += submesh.indices
|
||||||
|
me.loops.add(len(indices))
|
||||||
|
me.loops.foreach_set("vertex_index", indices)
|
||||||
|
me.polygons.add(len(indices)//3)
|
||||||
|
me.polygons.foreach_set("loop_start", range(0, len(indices), 3))
|
||||||
|
|
||||||
|
# UVs
|
||||||
|
if len(uv1) != 0:
|
||||||
|
uvData = me.uv_layers.new(name="UV1").data
|
||||||
|
for po in me.polygons:
|
||||||
|
uvData[po.loop_start].uv = mirrorUVY(uv1[indices[po.loop_start]])
|
||||||
|
uvData[po.loop_start+1].uv = mirrorUVY(uv1[indices[po.loop_start+1]])
|
||||||
|
uvData[po.loop_start+2].uv = mirrorUVY(uv1[indices[po.loop_start+2]])
|
||||||
|
if len(uv2) != 0:
|
||||||
|
uvData = me.uv_layers.new(name="UV2").data
|
||||||
|
for po in me.polygons:
|
||||||
|
uvData[po.loop_start].uv = mirrorUVY(uv2[indices[po.loop_start]])
|
||||||
|
uvData[po.loop_start+1].uv = mirrorUVY(uv2[indices[po.loop_start+1]])
|
||||||
|
uvData[po.loop_start+2].uv = mirrorUVY(uv2[indices[po.loop_start+2]])
|
||||||
|
|
||||||
|
# Apply materials (version 2)
|
||||||
|
faceIndexBase = 0
|
||||||
|
for submeshI, submesh in enumerate(meshData.submeshes):
|
||||||
|
if submesh.materialID == None or len(self.materials) == 0: #XXX: perhaps try add a material slot to the object so we still make use of the submesh data instead of skipping it when there are no materials?
|
||||||
|
# if materialID is None then this is a version 3 model submesh
|
||||||
|
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
|
||||||
|
|
||||||
|
#XXX: call this before we assign split normals, if you do not it causes a segmentation fault
|
||||||
|
me.validate()
|
||||||
|
|
||||||
|
# Split normals
|
||||||
|
if len(normal1) != 0:
|
||||||
|
me.normals_split_custom_set_from_vertices(normal1)
|
||||||
|
elif len(normal2) != 0:
|
||||||
|
me.normals_split_custom_set_from_vertices(normal2)
|
||||||
|
|
||||||
|
# Finalise
|
||||||
|
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)
|
||||||
|
if self.reset_empty_transform:
|
||||||
|
if transform.scale == (0.0, 0.0, 0.0): ob.scale = (1.0, 1.0, 1.0)
|
||||||
|
if transform.rotation == (0.0, 0.0, 0.0, 0.0): ob.rotation_quaternion = (1.0, 0.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
# Attempt to load textures
|
||||||
|
if self.try_import_textures and len(me.materials) != 0:
|
||||||
|
ma = me.materials[0] # Assume this is the main material
|
||||||
|
name = name.lower()
|
||||||
|
if name == "hull" or name == "turret":
|
||||||
|
# lightmap.webp
|
||||||
|
print("Load lightmap")
|
||||||
|
|
||||||
|
# Load image
|
||||||
|
image = load_image("lightmap.webp", self.directory, check_existing=True)
|
||||||
|
# Apply image
|
||||||
|
addImageTextureToMaterial(image, ma.node_tree)
|
||||||
|
elif "track" in name:
|
||||||
|
# tracks.webp
|
||||||
|
print("Load tracks")
|
||||||
|
|
||||||
|
# Load image
|
||||||
|
image = load_image("tracks.webp", self.directory, check_existing=True)
|
||||||
|
# Apply image
|
||||||
|
addImageTextureToMaterial(image, ma.node_tree)
|
||||||
|
elif "wheel" in name:
|
||||||
|
# wheels.webp
|
||||||
|
print("Load wheels")
|
||||||
|
|
||||||
|
# Load image
|
||||||
|
image = load_image("wheels.webp", self.directory, check_existing=True)
|
||||||
|
# Apply image
|
||||||
|
addImageTextureToMaterial(image, ma.node_tree)
|
||||||
|
|
||||||
|
return ob
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
'''
|
|
||||||
Copyright (C) 2024 Pyogenics <https://www.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")
|
|
||||||
|
|
||||||
def calculatePadding(length):
|
|
||||||
# (it basically works with rounding)
|
|
||||||
paddingSize = (((length + 3) // 4) * 4) - length
|
|
||||||
return paddingSize
|
|
||||||
|
|
||||||
def readString(stream):
|
|
||||||
length, = unpackStream("<I", stream)
|
|
||||||
string = stream.read(length)
|
|
||||||
|
|
||||||
paddingSize = calculatePadding(length)
|
|
||||||
stream.read(paddingSize)
|
|
||||||
|
|
||||||
return string.decode("utf8")
|
|
||||||
202
io_scene_a3d/A3DObjects.py
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
'''
|
||||||
|
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, readLengthPrefixedString, calculatePadding
|
||||||
|
|
||||||
|
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):
|
||||||
|
self.name = readLengthPrefixedString(stream)
|
||||||
|
self.color = unpackStream("<3f", stream)
|
||||||
|
self.diffuseMap = readLengthPrefixedString(stream)
|
||||||
|
|
||||||
|
print(f"[A3DMaterial name: {self.name} color: {self.color} diffuse map: {self.diffuseMap}]")
|
||||||
|
|
||||||
|
class A3DMesh:
|
||||||
|
def __init__(self):
|
||||||
|
self.name = ""
|
||||||
|
self.bboxMax = None
|
||||||
|
self.bboxMin = None
|
||||||
|
self.vertexBuffers = []
|
||||||
|
self.submeshes = []
|
||||||
|
|
||||||
|
self.vertexCount = 0
|
||||||
|
self.vertexBufferCount = 0
|
||||||
|
self.submeshCount = 0
|
||||||
|
|
||||||
|
def read2(self, stream):
|
||||||
|
# Read vertex buffers
|
||||||
|
self.vertexCount, self.vertexBufferCount = unpackStream("<2I", stream)
|
||||||
|
for _ in range(self.vertexBufferCount):
|
||||||
|
vertexBuffer = A3DVertexBuffer()
|
||||||
|
vertexBuffer.read2(self.vertexCount, stream)
|
||||||
|
self.vertexBuffers.append(vertexBuffer)
|
||||||
|
|
||||||
|
# Read submeshes
|
||||||
|
self.submeshCount, = unpackStream("<I", stream)
|
||||||
|
for _ in range(self.submeshCount):
|
||||||
|
submesh = A3DSubmesh()
|
||||||
|
submesh.read2(stream)
|
||||||
|
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 read3(self, stream):
|
||||||
|
# Read mesh info
|
||||||
|
self.name = readLengthPrefixedString(stream)
|
||||||
|
# XXX: bbox order maybe incorrect, check this (might be min then max and not max then min)
|
||||||
|
self.bboxMax = unpackStream("<3f", stream)
|
||||||
|
self.bboxMin = unpackStream("<3f", stream)
|
||||||
|
stream.read(4) # XXX: Unknown float value
|
||||||
|
|
||||||
|
# Read vertex buffers
|
||||||
|
self.vertexCount, self.vertexBufferCount = unpackStream("<2I", stream)
|
||||||
|
for _ in range(self.vertexBufferCount):
|
||||||
|
vertexBuffer = A3DVertexBuffer()
|
||||||
|
vertexBuffer.read2(self.vertexCount, stream)
|
||||||
|
self.vertexBuffers.append(vertexBuffer)
|
||||||
|
|
||||||
|
# Read submeshes
|
||||||
|
self.submeshCount, = unpackStream("<I", stream)
|
||||||
|
for _ in range(self.submeshCount):
|
||||||
|
submesh = A3DSubmesh()
|
||||||
|
submesh.read3(stream)
|
||||||
|
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)}]")
|
||||||
|
|
||||||
|
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.indices = []
|
||||||
|
self.smoothingGroups = []
|
||||||
|
self.materialID = None
|
||||||
|
|
||||||
|
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
|
||||||
|
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 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)
|
||||||
|
|
||||||
|
print(f"[A3DSubmesh indices: {len(self.indices)} smoothing groups: {len(self.smoothingGroups)} materialID: {self.materialID}]")
|
||||||
|
|
||||||
|
class A3DTransform:
|
||||||
|
def __init__(self):
|
||||||
|
self.name = ""
|
||||||
|
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}]")
|
||||||
|
|
||||||
|
def read3(self, stream):
|
||||||
|
self.name = readLengthPrefixedString(stream)
|
||||||
|
self.position = unpackStream("<3f", stream)
|
||||||
|
self.rotation = unpackStream("<4f", stream)
|
||||||
|
self.scale = unpackStream("<3f", stream)
|
||||||
|
|
||||||
|
print(f"[A3DTransform name: {self.name} position: {self.position} rotation: {self.rotation} scale: {self.scale}]")
|
||||||
|
|
||||||
|
class A3DObject:
|
||||||
|
def __init__(self):
|
||||||
|
self.name = ""
|
||||||
|
self.meshID = None
|
||||||
|
self.transformID = None
|
||||||
|
self.materialIDs = []
|
||||||
|
|
||||||
|
self.materialCount = 0
|
||||||
|
|
||||||
|
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} materialIDs: {len(self.materialIDs)}]")
|
||||||
|
|
||||||
|
def read3(self, stream):
|
||||||
|
self.meshID, self.transformID, self.materialCount = unpackStream("<3I", stream)
|
||||||
|
|
||||||
|
# Read material IDs
|
||||||
|
for _ in range(self.materialCount):
|
||||||
|
materialID, = unpackStream("<i", stream)
|
||||||
|
self.materialIDs.append(materialID)
|
||||||
|
|
||||||
|
print(f"[A3DObject name: {self.name} meshID: {self.meshID} transformID: {self.transformID} materialIDs: {len(self.materialIDs)}]")
|
||||||
163
io_scene_a3d/AlternativaProtocol.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
'''
|
||||||
|
Copyright (c) 2025 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 zlib import decompress
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from .IOTools import unpackStream
|
||||||
|
|
||||||
|
def unwrapPacket(stream):
|
||||||
|
print("Unwrapping packet")
|
||||||
|
|
||||||
|
# Determine size and compression
|
||||||
|
packetFlags = int.from_bytes(stream.read(1))
|
||||||
|
compressedPacket = (packetFlags & 0b01000000) > 0
|
||||||
|
|
||||||
|
packetLength = 0
|
||||||
|
packetLengthType = packetFlags & 0b10000000
|
||||||
|
if packetLengthType == 0:
|
||||||
|
# This is a short packet
|
||||||
|
packetLength = int.from_bytes(stream.read(1))
|
||||||
|
packetLength += (packetFlags & 0b00111111) << 8 # Part of the length is embedded in the flags field
|
||||||
|
else:
|
||||||
|
# This is a long packet
|
||||||
|
packetLength = int.from_bytes(stream.read(3), "big")
|
||||||
|
packetLength += (packetFlags & 0b00111111) << 24
|
||||||
|
|
||||||
|
# Decompress the packet if needed
|
||||||
|
packetData = stream.read(packetLength)
|
||||||
|
if compressedPacket:
|
||||||
|
print("Decompressing packet")
|
||||||
|
packetData = decompress(packetData)
|
||||||
|
|
||||||
|
return BytesIO(packetData)
|
||||||
|
|
||||||
|
def readOptionalMask(stream):
|
||||||
|
print("Reading optional mask")
|
||||||
|
|
||||||
|
optionalMask = []
|
||||||
|
|
||||||
|
# Determine mask type (there are multiple length types)
|
||||||
|
maskFlags = int.from_bytes(stream.read(1))
|
||||||
|
maskLengthType = maskFlags & 0b10000000
|
||||||
|
if maskLengthType == 0:
|
||||||
|
# Short mask: 5 optional bits + upto 3 extra bytes
|
||||||
|
# First read the integrated optional bits
|
||||||
|
integratedOptionalBits = maskFlags << 3 # Trim flag bits so we're left with the optionals and some padding bits
|
||||||
|
for bitI in range(7, 2, -1): #0b11111000 left to right
|
||||||
|
optional = (integratedOptionalBits & 2**bitI) == 0
|
||||||
|
optionalMask.append(optional)
|
||||||
|
|
||||||
|
# Now read the external bytes
|
||||||
|
externalByteCount = (maskFlags & 0b01100000) >> 5
|
||||||
|
externalBytes = stream.read(externalByteCount)
|
||||||
|
for externalByte in externalBytes:
|
||||||
|
for bitI in range(7, -1, -1): #0b11111111 left to right
|
||||||
|
optional = (externalByte & 2**bitI) == 0
|
||||||
|
optionalMask.append(optional)
|
||||||
|
else:
|
||||||
|
# This type of mask encodes an extra length/count field to increase the number of possible optionals significantly
|
||||||
|
maskLengthType = maskFlags & 0b01000000
|
||||||
|
externalByteCount = 0
|
||||||
|
if maskLengthType == 0:
|
||||||
|
# Medium mask: stores number of bytes used for the optional mask in the last 6 bits of the flags
|
||||||
|
externalByteCount = maskFlags & 0b00111111
|
||||||
|
else:
|
||||||
|
# Long mask: # Medium mask: stores number of bytes used for the optional mask in the last 6 bits of the flags + 2 extra bytes
|
||||||
|
externalByteCount = (maskFlags & 0b00111111) << 16
|
||||||
|
externalByteCount += int.from_bytes(stream.read(2), "big")
|
||||||
|
|
||||||
|
# Read the external bytes
|
||||||
|
externalBytes = stream.read(externalByteCount)
|
||||||
|
for externalByte in externalBytes:
|
||||||
|
for bitI in range(7, -1, -1): #0b11111111 left to right
|
||||||
|
optional = (externalByte & 2**bitI) == 0
|
||||||
|
optionalMask.append(optional)
|
||||||
|
|
||||||
|
optionalMask.reverse()
|
||||||
|
return optionalMask
|
||||||
|
|
||||||
|
'''
|
||||||
|
Array type readers
|
||||||
|
'''
|
||||||
|
def readArrayLength(packet):
|
||||||
|
arrayLength = 0
|
||||||
|
|
||||||
|
arrayFlags = int.from_bytes(packet.read(1))
|
||||||
|
arrayLengthType = arrayFlags & 0b10000000
|
||||||
|
if arrayLengthType == 0:
|
||||||
|
# Short array
|
||||||
|
arrayLength = arrayFlags & 0b01111111
|
||||||
|
else:
|
||||||
|
# Long array
|
||||||
|
arrayLengthType = arrayFlags & 0b01000000
|
||||||
|
if arrayLengthType == 0:
|
||||||
|
# Length in last 6 bits of flags + next byte
|
||||||
|
arrayLength = (arrayFlags & 0b00111111) << 8
|
||||||
|
arrayLength += int.from_bytes(packet.read(1))
|
||||||
|
else:
|
||||||
|
# Length in last 6 bits of flags + next 2 byte
|
||||||
|
arrayLength = (arrayFlags & 0b00111111) << 16
|
||||||
|
arrayLength += int.from_bytes(packet.read(2), "big")
|
||||||
|
|
||||||
|
return arrayLength
|
||||||
|
|
||||||
|
def readObjectArray(packet, objReader, optionalMask):
|
||||||
|
arrayLength = readArrayLength(packet)
|
||||||
|
objects = []
|
||||||
|
for _ in range(arrayLength):
|
||||||
|
obj = objReader()
|
||||||
|
obj.read(packet, optionalMask)
|
||||||
|
objects.append(obj)
|
||||||
|
|
||||||
|
return objects
|
||||||
|
|
||||||
|
def readString(packet):
|
||||||
|
stringLength = readArrayLength(packet)
|
||||||
|
string = packet.read(stringLength)
|
||||||
|
string = string.decode("utf-8")
|
||||||
|
|
||||||
|
return string
|
||||||
|
|
||||||
|
def readInt16Array(packet):
|
||||||
|
arrayLength = readArrayLength(packet)
|
||||||
|
integers = unpackStream(f"{arrayLength}h", packet)
|
||||||
|
|
||||||
|
return list(integers)
|
||||||
|
|
||||||
|
def readIntArray(packet):
|
||||||
|
arrayLength = readArrayLength(packet)
|
||||||
|
integers = unpackStream(f"{arrayLength}i", packet)
|
||||||
|
|
||||||
|
return list(integers)
|
||||||
|
|
||||||
|
def readInt64Array(packet):
|
||||||
|
arrayLength = readArrayLength(packet)
|
||||||
|
integers = unpackStream(f"{arrayLength}q", packet)
|
||||||
|
|
||||||
|
return list(integers)
|
||||||
|
|
||||||
|
def readFloatArray(packet):
|
||||||
|
arrayLength = readArrayLength(packet)
|
||||||
|
floats = unpackStream(f">{arrayLength}f", packet)
|
||||||
|
|
||||||
|
return list(floats)
|
||||||
276
io_scene_a3d/BattleMap.py
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
'''
|
||||||
|
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.
|
||||||
|
'''
|
||||||
|
|
||||||
|
from .IOTools import unpackStream
|
||||||
|
from . import AlternativaProtocol
|
||||||
|
|
||||||
|
'''
|
||||||
|
Objects
|
||||||
|
'''
|
||||||
|
class AtlasRect:
|
||||||
|
def __init__(self):
|
||||||
|
self.height = 0
|
||||||
|
self.libraryName = ""
|
||||||
|
self.name = ""
|
||||||
|
self.width = 0
|
||||||
|
self.x = 0
|
||||||
|
self.y = 0
|
||||||
|
|
||||||
|
def read(self, stream, optionalMask):
|
||||||
|
self.height, = unpackStream(">I", stream)
|
||||||
|
self.libraryName = AlternativaProtocol.readString(stream)
|
||||||
|
self.name = AlternativaProtocol.readString(stream)
|
||||||
|
self.width, self.x, self.y = unpackStream(">3I", stream)
|
||||||
|
|
||||||
|
class CollisionBox:
|
||||||
|
def __init__(self):
|
||||||
|
self.position = (0.0, 0.0, 0.0)
|
||||||
|
self.rotation = (0.0, 0.0, 0.0)
|
||||||
|
self.size = (0.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
def read(self, stream, optionalMask):
|
||||||
|
self.position = unpackStream(">3f", stream)
|
||||||
|
self.rotation = unpackStream(">3f", stream)
|
||||||
|
self.size = unpackStream(">3f", stream)
|
||||||
|
|
||||||
|
class CollisionPlane:
|
||||||
|
def __init__(self):
|
||||||
|
self.length = 0.0
|
||||||
|
self.position = (0.0, 0.0, 0.0)
|
||||||
|
self.rotation = (0.0, 0.0, 0.0)
|
||||||
|
self.width = 0.0
|
||||||
|
|
||||||
|
def read(self, stream, optionalMask):
|
||||||
|
self.length, = unpackStream(">d", stream)
|
||||||
|
self.position = unpackStream(">3f", stream)
|
||||||
|
self.rotation = unpackStream(">3f", stream)
|
||||||
|
self.width, = unpackStream(">d", stream)
|
||||||
|
|
||||||
|
class CollisionTriangle:
|
||||||
|
def __init__(self):
|
||||||
|
self.length = 0.0
|
||||||
|
self.position = (0.0, 0.0, 0.0)
|
||||||
|
self.rotation = (0.0, 0.0, 0.0)
|
||||||
|
self.v0 = (0.0, 0.0, 0.0)
|
||||||
|
self.v1 = (0.0, 0.0, 0.0)
|
||||||
|
self.v2 = (0.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
def read(self, stream, optionalMask):
|
||||||
|
self.length, = unpackStream(">d", stream)
|
||||||
|
self.position = unpackStream(">3f", stream)
|
||||||
|
self.rotation = unpackStream(">3f", stream)
|
||||||
|
self.v0 = unpackStream(">3f", stream)
|
||||||
|
self.v1 = unpackStream(">3f", stream)
|
||||||
|
self.v2 = unpackStream(">3f", stream)
|
||||||
|
|
||||||
|
class ScalarParameter:
|
||||||
|
def __init__(self):
|
||||||
|
self.name = ""
|
||||||
|
self.value = 0.0
|
||||||
|
|
||||||
|
def read(self, stream, optionalMask):
|
||||||
|
self.name = AlternativaProtocol.readString(stream)
|
||||||
|
self.value, = unpackStream(">f", stream)
|
||||||
|
|
||||||
|
class TextureParameter:
|
||||||
|
def __init__(self):
|
||||||
|
self.name = ""
|
||||||
|
self.textureName = ""
|
||||||
|
|
||||||
|
# Optional
|
||||||
|
self.libraryName = None
|
||||||
|
|
||||||
|
def read(self, stream, optionalMask):
|
||||||
|
if optionalMask.pop():
|
||||||
|
self.libraryName = AlternativaProtocol.readString(stream)
|
||||||
|
self.name = AlternativaProtocol.readString(stream)
|
||||||
|
self.textureName = AlternativaProtocol.readString(stream)
|
||||||
|
|
||||||
|
class Vector2Parameter:
|
||||||
|
def __init__(self):
|
||||||
|
self.name = ""
|
||||||
|
self.value = (0.0, 0.0)
|
||||||
|
|
||||||
|
def __init__(self, stream, optionalMask):
|
||||||
|
self.name = AlternativaProtocol.readString(stream)
|
||||||
|
self.value = unpackStream(">2f", stream)
|
||||||
|
|
||||||
|
class Vector3Parameter:
|
||||||
|
def __init__(self):
|
||||||
|
self.name = ""
|
||||||
|
self.value = (0.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
def __init__(self, stream, optionalMask):
|
||||||
|
self.name = AlternativaProtocol.readString(stream)
|
||||||
|
self.value = unpackStream(">3f", stream)
|
||||||
|
|
||||||
|
class Vector4Parameter:
|
||||||
|
def __init__(self):
|
||||||
|
self.name = ""
|
||||||
|
self.value = (0.0, 0.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
def read(self, stream, optionalMask):
|
||||||
|
self.name = AlternativaProtocol.readString(stream)
|
||||||
|
self.value = unpackStream(">4f", stream)
|
||||||
|
|
||||||
|
'''
|
||||||
|
Main objects
|
||||||
|
'''
|
||||||
|
class Atlas:
|
||||||
|
def __init__(self):
|
||||||
|
self.height = 0
|
||||||
|
self.name = ""
|
||||||
|
self.padding = 0
|
||||||
|
self.rects = []
|
||||||
|
self.width = 0
|
||||||
|
|
||||||
|
def read(self, stream, optionalMask):
|
||||||
|
self.height, unpackStream(">i", stream)
|
||||||
|
self.name = AlternativaProtocol.readString(stream)
|
||||||
|
self.padding = unpackStream(">I", stream)
|
||||||
|
self.rects = AlternativaProtocol.readObjectArray(stream, AtlasRect, optionalMask)
|
||||||
|
self.width, = unpackStream(">I", stream)
|
||||||
|
|
||||||
|
class Batch:
|
||||||
|
def __init__(self):
|
||||||
|
self.materialID = 0
|
||||||
|
self.name = ""
|
||||||
|
self.position = (0.0, 0.0, 0.0)
|
||||||
|
self.propIDs = ""
|
||||||
|
|
||||||
|
def read(self, stream, optionalMask):
|
||||||
|
self.materialID, = unpackStream(">I", stream)
|
||||||
|
self.name = AlternativaProtocol.readString(stream)
|
||||||
|
self.position = unpackStream(">3f", stream)
|
||||||
|
self.propIDs = AlternativaProtocol.readString(stream)
|
||||||
|
|
||||||
|
class CollisionGeometry:
|
||||||
|
def __init__(self):
|
||||||
|
self.boxes = []
|
||||||
|
self.planes = []
|
||||||
|
self.triangles = []
|
||||||
|
|
||||||
|
def read(self, stream, optionalMask):
|
||||||
|
self.boxes = AlternativaProtocol.readObjectArray(stream, CollisionBox, optionalMask)
|
||||||
|
self.planes = AlternativaProtocol.readObjectArray(stream, CollisionPlane, optionalMask)
|
||||||
|
self.triangles = AlternativaProtocol.readObjectArray(stream, CollisionTriangle, optionalMask)
|
||||||
|
|
||||||
|
class Material:
|
||||||
|
def __init__(self):
|
||||||
|
self.ID = 0
|
||||||
|
self.name = ""
|
||||||
|
self.shader = ""
|
||||||
|
self.textureParameters = None
|
||||||
|
|
||||||
|
# Optional
|
||||||
|
self.scalarParameters = None
|
||||||
|
self.vector2Parameters = None
|
||||||
|
self.vector3Parameters = None
|
||||||
|
self.vector4Parameters = None
|
||||||
|
|
||||||
|
def read(self, stream, optionalMask):
|
||||||
|
self.ID, = unpackStream(">I", stream)
|
||||||
|
self.name = AlternativaProtocol.readString(stream)
|
||||||
|
if optionalMask.pop():
|
||||||
|
self.scalarParameters = AlternativaProtocol.readObjectArray(stream, ScalarParameter, optionalMask)
|
||||||
|
self.shader = AlternativaProtocol.readString(stream)
|
||||||
|
self.textureParameters = AlternativaProtocol.readObjectArray(stream, TextureParameter, optionalMask)
|
||||||
|
if optionalMask.pop():
|
||||||
|
self.vector2Parameters = AlternativaProtocol.readObjectArray(stream, Vector2Parameter, optionalMask)
|
||||||
|
if optionalMask.pop():
|
||||||
|
self.vector3Parameters = AlternativaProtocol.readObjectArray(stream, Vector3Parameter, optionalMask)
|
||||||
|
if optionalMask.pop():
|
||||||
|
self.vector4Parameters = AlternativaProtocol.readObjectArray(stream, Vector4Parameter, optionalMask)
|
||||||
|
|
||||||
|
class SpawnPoint:
|
||||||
|
def __init__(self):
|
||||||
|
self.position = (0.0, 0.0, 0.0)
|
||||||
|
self.rotation = (0.0, 0.0, 0.0)
|
||||||
|
self.type = 0
|
||||||
|
|
||||||
|
def read(self, stream, optionalMask):
|
||||||
|
self.position = unpackStream(">3f", stream)
|
||||||
|
self.rotation = unpackStream(">3f", stream)
|
||||||
|
self.type, = unpackStream(">I", stream)
|
||||||
|
|
||||||
|
class Prop:
|
||||||
|
def __init__(self):
|
||||||
|
self.ID = 0
|
||||||
|
self.libraryName = ""
|
||||||
|
self.materialID = 0
|
||||||
|
self.name = ""
|
||||||
|
self.position = (0.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
# Optional
|
||||||
|
self.groupName = None
|
||||||
|
self.rotation = None
|
||||||
|
self.scale = None
|
||||||
|
|
||||||
|
def read(self, stream, optionalMask):
|
||||||
|
if optionalMask.pop():
|
||||||
|
self.groupName = AlternativaProtocol.readString(stream)
|
||||||
|
self.ID, = unpackStream(">I", stream)
|
||||||
|
self.libraryName = AlternativaProtocol.readString(stream)
|
||||||
|
self.materialID, = unpackStream(">I", stream)
|
||||||
|
self.name = AlternativaProtocol.readString(stream)
|
||||||
|
self.position = unpackStream(">3f", stream)
|
||||||
|
if optionalMask.pop():
|
||||||
|
self.rotation = unpackStream(">3f", stream)
|
||||||
|
if optionalMask.pop():
|
||||||
|
self.scale = unpackStream(">3f", stream)
|
||||||
|
|
||||||
|
'''
|
||||||
|
Main
|
||||||
|
'''
|
||||||
|
class BattleMap:
|
||||||
|
def __init__(self):
|
||||||
|
self.atlases = []
|
||||||
|
self.batches = []
|
||||||
|
self.collisionGeometry = None
|
||||||
|
self.collisionGeometryOutsideGamingZone = None
|
||||||
|
self.materials = []
|
||||||
|
self.spawnPoints = []
|
||||||
|
self.staticGeometry = []
|
||||||
|
|
||||||
|
'''
|
||||||
|
IO
|
||||||
|
'''
|
||||||
|
def read(self, stream):
|
||||||
|
print("Reading BattleMap")
|
||||||
|
|
||||||
|
# Read packet
|
||||||
|
packet = AlternativaProtocol.unwrapPacket(stream)
|
||||||
|
optionalMask = AlternativaProtocol.readOptionalMask(packet)
|
||||||
|
|
||||||
|
# Read data
|
||||||
|
if optionalMask.pop():
|
||||||
|
self.atlases = AlternativaProtocol.readObjectArray(packet, Atlas, optionalMask)
|
||||||
|
if optionalMask.pop():
|
||||||
|
self.batches = AlternativaProtocol.readObjectArray(packet, Batch, optionalMask)
|
||||||
|
self.collisionGeometry = CollisionGeometry()
|
||||||
|
self.collisionGeometry.read(packet, optionalMask)
|
||||||
|
self.collisionGeometryOutsideGamingZone = CollisionGeometry()
|
||||||
|
self.collisionGeometryOutsideGamingZone.read(packet, optionalMask)
|
||||||
|
self.materials = AlternativaProtocol.readObjectArray(packet, Material, optionalMask)
|
||||||
|
if optionalMask.pop():
|
||||||
|
self.spawnPoints = AlternativaProtocol.readObjectArray(packet, SpawnPoint, optionalMask)
|
||||||
|
self.staticGeometry = AlternativaProtocol.readObjectArray(packet, Prop, optionalMask)
|
||||||
445
io_scene_a3d/BattleMapBlenderImporter.py
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
'''
|
||||||
|
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 json import load
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy_extras.image_utils import load_image
|
||||||
|
from bpy_extras.node_shader_utils import PrincipledBSDFWrapper
|
||||||
|
import bmesh
|
||||||
|
from mathutils import Matrix
|
||||||
|
|
||||||
|
from .A3D import A3D
|
||||||
|
from .A3DBlenderImporter import A3DBlenderImporter
|
||||||
|
from .BlenderMaterialUtils import addImageTextureToMaterial, decodeIntColorToTuple
|
||||||
|
|
||||||
|
class BattleMapBlenderImporter:
|
||||||
|
# Allows subsequent map loads to be faster
|
||||||
|
libraryCache = {}
|
||||||
|
|
||||||
|
def __init__(self, mapData, lightmapData, propLibrarySourcePath, map_scale_factor=0.01, import_static_geom=True, import_collision_geom=False, import_spawn_points=False, import_lightmapdata=False):
|
||||||
|
self.mapData = mapData
|
||||||
|
self.lightmapData = lightmapData
|
||||||
|
self.propLibrarySourcePath = propLibrarySourcePath
|
||||||
|
self.map_scale_factor = map_scale_factor
|
||||||
|
self.import_static_geom = import_static_geom
|
||||||
|
self.import_collision_geom = import_collision_geom
|
||||||
|
self.import_spawn_points = import_spawn_points
|
||||||
|
self.import_lightmapdata = import_lightmapdata
|
||||||
|
|
||||||
|
# Cache for collision meshes, don't cache triangles because they are set using unique vertices
|
||||||
|
self.collisionPlaneMesh = None
|
||||||
|
self.collisionBoxMesh = None
|
||||||
|
|
||||||
|
self.materials = {}
|
||||||
|
|
||||||
|
def importData(self):
|
||||||
|
print("Importing BattleMap data into blender")
|
||||||
|
|
||||||
|
# Process materials
|
||||||
|
for materialData in self.mapData.materials:
|
||||||
|
ma = self.createBlenderMaterial(materialData)
|
||||||
|
self.materials[materialData.ID] = ma
|
||||||
|
|
||||||
|
# Static geometry
|
||||||
|
propObjects = []
|
||||||
|
if self.import_static_geom:
|
||||||
|
# Load props
|
||||||
|
for propData in self.mapData.staticGeometry:
|
||||||
|
ob = self.getBlenderProp(propData)
|
||||||
|
propObjects.append(ob)
|
||||||
|
print(f"Loaded {len(propObjects)} prop objects")
|
||||||
|
|
||||||
|
# Collision geometry
|
||||||
|
collisionObjects = []
|
||||||
|
if self.import_collision_geom:
|
||||||
|
# Create collision meshes
|
||||||
|
self.collisionPlaneMesh = bpy.data.meshes.new("collisionPlane")
|
||||||
|
bm = bmesh.new()
|
||||||
|
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1.0)
|
||||||
|
bm.to_mesh(self.collisionPlaneMesh)
|
||||||
|
bm.free()
|
||||||
|
|
||||||
|
self.collisionBoxMesh = bpy.data.meshes.new("collisionBox")
|
||||||
|
bm = bmesh.new()
|
||||||
|
bmesh.ops.create_cube(bm)
|
||||||
|
bm.to_mesh(self.collisionBoxMesh)
|
||||||
|
bm.free()
|
||||||
|
|
||||||
|
# Load collision meshes
|
||||||
|
collisionTriangles = self.mapData.collisionGeometry.triangles + self.mapData.collisionGeometryOutsideGamingZone.triangles
|
||||||
|
collisionTriangleObjects = self.createBlenderCollisionTriangles(collisionTriangles)
|
||||||
|
collisionPlanes = self.mapData.collisionGeometry.planes + self.mapData.collisionGeometryOutsideGamingZone.planes
|
||||||
|
collisionPlaneObjects = self.createBlenderCollisionPlanes(collisionPlanes)
|
||||||
|
collisionBoxes = self.mapData.collisionGeometry.boxes + self.mapData.collisionGeometryOutsideGamingZone.boxes
|
||||||
|
collisionBoxObjects = self.createBlenderCollisionBoxes(collisionBoxes)
|
||||||
|
|
||||||
|
collisionObjects += collisionTriangleObjects
|
||||||
|
collisionObjects += collisionPlaneObjects
|
||||||
|
collisionObjects += collisionBoxObjects
|
||||||
|
print(f"Loaded {len(collisionObjects)} collision objects")
|
||||||
|
|
||||||
|
# Spawn points
|
||||||
|
spawnPointObjects = []
|
||||||
|
if self.import_spawn_points:
|
||||||
|
# Create spawn points
|
||||||
|
for spawnPointData in self.mapData.spawnPoints:
|
||||||
|
ob = self.createBlenderSpawnPoint(spawnPointData)
|
||||||
|
spawnPointObjects.append(ob)
|
||||||
|
print(f"Loaded {len(spawnPointObjects)} spawn points")
|
||||||
|
|
||||||
|
# Create container object to store all our objects
|
||||||
|
objects = propObjects + collisionObjects + spawnPointObjects
|
||||||
|
mapOB = bpy.data.objects.new("BattleMap", None)
|
||||||
|
mapOB.empty_display_size = 100 # Alternativa use a x100 scale
|
||||||
|
mapOB.scale = (self.map_scale_factor, self.map_scale_factor, self.map_scale_factor)
|
||||||
|
objects.append(mapOB)
|
||||||
|
|
||||||
|
# Create empty objects to group each type of object
|
||||||
|
if self.import_static_geom:
|
||||||
|
groupOB = bpy.data.objects.new("StaticGeometry", None)
|
||||||
|
groupOB.parent = mapOB
|
||||||
|
objects.append(groupOB)
|
||||||
|
for ob in propObjects:
|
||||||
|
ob.parent = groupOB
|
||||||
|
if self.import_collision_geom:
|
||||||
|
groupOB = bpy.data.objects.new("CollisionGeometry", None)
|
||||||
|
groupOB.parent = mapOB
|
||||||
|
objects.append(groupOB)
|
||||||
|
for ob in collisionObjects:
|
||||||
|
ob.parent = groupOB
|
||||||
|
if self.import_spawn_points:
|
||||||
|
groupOB = bpy.data.objects.new("SpawnPoints", None)
|
||||||
|
groupOB.parent = mapOB
|
||||||
|
objects.append(groupOB)
|
||||||
|
for ob in spawnPointObjects:
|
||||||
|
ob.parent = groupOB
|
||||||
|
|
||||||
|
# Lighting data
|
||||||
|
if self.import_lightmapdata:
|
||||||
|
# Create a sun light object
|
||||||
|
li = bpy.data.lights.new("DirectionalLight", "SUN")
|
||||||
|
li.color = decodeIntColorToTuple(self.lightmapData.lightColour)
|
||||||
|
|
||||||
|
ob = bpy.data.objects.new(li.name, li)
|
||||||
|
ob.location = (0.0, 0.0, 1000.0) # Just place it like 10 meters off the ground (in alternativa units)
|
||||||
|
lightAngleX, lightAngleZ = self.lightmapData.lightAngle
|
||||||
|
ob.rotation_mode = "XYZ"
|
||||||
|
ob.rotation_euler = (lightAngleX, 0.0, lightAngleZ)
|
||||||
|
|
||||||
|
ob.parent = mapOB
|
||||||
|
objects.append(ob)
|
||||||
|
|
||||||
|
# Set ambient world light
|
||||||
|
scene = bpy.context.scene
|
||||||
|
if scene.world == None:
|
||||||
|
wd = bpy.data.worlds.new("map")
|
||||||
|
scene.world = wd
|
||||||
|
world = scene.world
|
||||||
|
world.use_nodes = False
|
||||||
|
world.color = decodeIntColorToTuple(self.lightmapData.ambientLightColour)
|
||||||
|
|
||||||
|
return objects
|
||||||
|
|
||||||
|
def getPropLibrary(self, libraryName):
|
||||||
|
# First check if we've already loaded the required prop library
|
||||||
|
if not libraryName in self.libraryCache:
|
||||||
|
# Load the proplib
|
||||||
|
libraryPath = f"{self.propLibrarySourcePath}/{libraryName}"
|
||||||
|
library = PropLibrary(libraryPath)
|
||||||
|
self.libraryCache[libraryName] = library
|
||||||
|
|
||||||
|
return self.libraryCache[libraryName]
|
||||||
|
|
||||||
|
def tryLoadTexture(self, textureName, libraryName):
|
||||||
|
if libraryName == None:
|
||||||
|
# For some reason Remaster proplib is alwaus marked as None? This is not true for the ny2024 remaster prop lib though
|
||||||
|
libraryName = "Remaster"
|
||||||
|
|
||||||
|
propLibrary = self.getPropLibrary(libraryName)
|
||||||
|
texture = propLibrary.getTexture(f"{textureName}.webp")
|
||||||
|
return texture
|
||||||
|
|
||||||
|
'''
|
||||||
|
Blender data builders
|
||||||
|
'''
|
||||||
|
def getBlenderProp(self, propData):
|
||||||
|
# Load prop
|
||||||
|
propLibrary = self.getPropLibrary(propData.libraryName)
|
||||||
|
prop = propLibrary.getProp(propData.name, propData.groupName)
|
||||||
|
propOB = prop.mainObject.copy() # We want to use a copy of the prop object
|
||||||
|
|
||||||
|
# Assign data
|
||||||
|
propOB.name = f"{propData.name}_{propData.ID}"
|
||||||
|
propOB.location = propData.position
|
||||||
|
propOB.rotation_mode = "XYZ"
|
||||||
|
propRotation = propData.rotation
|
||||||
|
if propRotation == None:
|
||||||
|
propRotation = (0.0, 0.0, 0.0)
|
||||||
|
propOB.rotation_euler = propRotation
|
||||||
|
propScale = propData.scale
|
||||||
|
if propScale == None:
|
||||||
|
propScale = (1.0, 1.0, 1.0)
|
||||||
|
propOB.scale = propScale
|
||||||
|
|
||||||
|
# Lighting info
|
||||||
|
if self.import_lightmapdata:
|
||||||
|
lightingMapObject = None
|
||||||
|
for mapObject in self.lightmapData.mapObjects:
|
||||||
|
if mapObject.index == propData.ID:
|
||||||
|
lightingMapObject = mapObject
|
||||||
|
break
|
||||||
|
if lightingMapObject != None:
|
||||||
|
#XXX: do something with lightingMapObject.recieveShadows??
|
||||||
|
propOB.visible_shadow = lightingMapObject.castShadows
|
||||||
|
|
||||||
|
# Material
|
||||||
|
ma = self.materials[propData.materialID]
|
||||||
|
if len(propOB.data.materials) != 0:
|
||||||
|
# Create a duplicate mesh object if it needs a different material, XXX: could probably cache these to reuse datablocks
|
||||||
|
if propOB.data.materials[0] != ma:
|
||||||
|
propOB.data = propOB.data.copy()
|
||||||
|
propOB.data.materials[0] = ma
|
||||||
|
|
||||||
|
return propOB
|
||||||
|
|
||||||
|
def createBlenderCollisionTriangles(self, collisionTriangles):
|
||||||
|
objects = []
|
||||||
|
for collisionTriangle in collisionTriangles:
|
||||||
|
# Create the mesh
|
||||||
|
me = bpy.data.meshes.new("collisionTriangle")
|
||||||
|
|
||||||
|
# Create array for coordinate data, blender doesn't like tuples
|
||||||
|
vertices = []
|
||||||
|
vertices += collisionTriangle.v0
|
||||||
|
vertices += collisionTriangle.v1
|
||||||
|
vertices += collisionTriangle.v2
|
||||||
|
|
||||||
|
# Assign coordinates
|
||||||
|
me.vertices.add(3)
|
||||||
|
me.vertices.foreach_set("co", vertices)
|
||||||
|
me.loops.add(3)
|
||||||
|
me.loops.foreach_set("vertex_index", [0, 1, 2])
|
||||||
|
me.polygons.add(1)
|
||||||
|
me.polygons.foreach_set("loop_start", [0])
|
||||||
|
|
||||||
|
me.validate()
|
||||||
|
me.update()
|
||||||
|
|
||||||
|
# Create object
|
||||||
|
ob = bpy.data.objects.new("collisionTriangle", me)
|
||||||
|
ob.location = collisionTriangle.position
|
||||||
|
ob.rotation_mode = "XYZ"
|
||||||
|
ob.rotation_euler = collisionTriangle.rotation
|
||||||
|
#print(collisionTriangle.length) # XXX: how to handle collisionTriangle.length?
|
||||||
|
|
||||||
|
objects.append(ob)
|
||||||
|
|
||||||
|
return objects
|
||||||
|
|
||||||
|
def createBlenderCollisionPlanes(self, collisionPlanes):
|
||||||
|
objects = []
|
||||||
|
for collisionPlane in collisionPlanes:
|
||||||
|
# Create object
|
||||||
|
ob = bpy.data.objects.new("collisionPlane", self.collisionPlaneMesh)
|
||||||
|
ob.location = collisionPlane.position
|
||||||
|
ob.rotation_mode = "XYZ"
|
||||||
|
ob.rotation_euler = collisionPlane.rotation
|
||||||
|
ob.scale = (collisionPlane.width*0.5, collisionPlane.length*0.5, 1.0) # Unsure why they double the width and length, could be because of central origin?
|
||||||
|
|
||||||
|
objects.append(ob)
|
||||||
|
|
||||||
|
return objects
|
||||||
|
|
||||||
|
def createBlenderCollisionBoxes(self, collisionBoxes):
|
||||||
|
objects = []
|
||||||
|
for collisionBox in collisionBoxes:
|
||||||
|
# Create object
|
||||||
|
ob = bpy.data.objects.new("collisionBox", self.collisionBoxMesh)
|
||||||
|
ob.location = collisionBox.position
|
||||||
|
ob.rotation_mode = "XYZ"
|
||||||
|
ob.rotation_euler = collisionBox.rotation
|
||||||
|
ob.scale = collisionBox.size
|
||||||
|
|
||||||
|
objects.append(ob)
|
||||||
|
|
||||||
|
return objects
|
||||||
|
|
||||||
|
def createBlenderSpawnPoint(self, spawnPointData):
|
||||||
|
#TODO: implement spawn type name lookup
|
||||||
|
ob = bpy.data.objects.new(f"SpawnPoint_{spawnPointData.type}", None)
|
||||||
|
ob.empty_display_type = "ARROWS"
|
||||||
|
ob.empty_display_size = 100 # The map will be at 100x scale so it's a good idea to match that here
|
||||||
|
ob.location = spawnPointData.position
|
||||||
|
ob.rotation_mode = "XYZ"
|
||||||
|
ob.rotation_euler = spawnPointData.rotation
|
||||||
|
|
||||||
|
return ob
|
||||||
|
|
||||||
|
def createBlenderMaterial(self, materialData):
|
||||||
|
ma = bpy.data.materials.new(f"{materialData.ID}_{materialData.name}")
|
||||||
|
|
||||||
|
# Shader specific logic
|
||||||
|
if materialData.shader == "TankiOnline/SingleTextureShader" or materialData.shader == "TankiOnline/SingleTextureShaderWinter":
|
||||||
|
bsdf = PrincipledBSDFWrapper(ma, is_readonly=False, use_nodes=True)
|
||||||
|
bsdf.roughness_set(1.0)
|
||||||
|
bsdf.ior_set(1.0)
|
||||||
|
|
||||||
|
# Try load texture
|
||||||
|
textureParameter = materialData.textureParameters[0]
|
||||||
|
texture = self.tryLoadTexture(textureParameter.textureName, textureParameter.libraryName)
|
||||||
|
|
||||||
|
addImageTextureToMaterial(texture, ma.node_tree)
|
||||||
|
elif materialData.shader == "TankiOnline/SpriteShader":
|
||||||
|
bsdf = PrincipledBSDFWrapper(ma, is_readonly=False, use_nodes=True)
|
||||||
|
bsdf.roughness_set(1.0)
|
||||||
|
bsdf.ior_set(1.0)
|
||||||
|
|
||||||
|
# Try load texture
|
||||||
|
textureParameter = materialData.textureParameters[0]
|
||||||
|
texture = self.tryLoadTexture(textureParameter.textureName, textureParameter.libraryName)
|
||||||
|
|
||||||
|
addImageTextureToMaterial(texture, ma.node_tree, linkAlpha=True)
|
||||||
|
elif materialData.shader == "TankiOnline/Terrain":
|
||||||
|
# XXX: still need to figure out how to do the terrain properly, all manual attempts have yielded mixed results
|
||||||
|
bsdf = PrincipledBSDFWrapper(ma, is_readonly=False, use_nodes=True)
|
||||||
|
bsdf.roughness_set(1.0)
|
||||||
|
bsdf.ior_set(1.0)
|
||||||
|
bsdf.base_color_set((0.0, 0.0, 0.0))
|
||||||
|
else:
|
||||||
|
pass # Unknown shader
|
||||||
|
|
||||||
|
return ma
|
||||||
|
|
||||||
|
class PropLibrary:
|
||||||
|
propGroups = {}
|
||||||
|
def __init__(self, directory):
|
||||||
|
self.directory = directory
|
||||||
|
self.libraryInfo = {}
|
||||||
|
|
||||||
|
# Load library info
|
||||||
|
with open(f"{self.directory}/library.json", "r") as file: self.libraryInfo = load(file)
|
||||||
|
print(f"Loaded prop library: " + self.libraryInfo["name"])
|
||||||
|
|
||||||
|
def getProp(self, propName, groupName):
|
||||||
|
# Create the prop group if it's not already loaded
|
||||||
|
if not groupName in self.propGroups:
|
||||||
|
self.propGroups[groupName] = {}
|
||||||
|
|
||||||
|
# Load the prop if it's not already loaded
|
||||||
|
if not propName in self.propGroups[groupName]:
|
||||||
|
# Find the prop group
|
||||||
|
groupInfo = None
|
||||||
|
for group in self.libraryInfo["groups"]:
|
||||||
|
if group["name"] == groupName:
|
||||||
|
groupInfo = group
|
||||||
|
break
|
||||||
|
if groupInfo == None:
|
||||||
|
raise RuntimeError(f"Unable to find prop group with name {groupName} in " + self.libraryInfo["name"])
|
||||||
|
|
||||||
|
# Find the prop
|
||||||
|
propInfo = None
|
||||||
|
for prop in groupInfo["props"]:
|
||||||
|
if prop["name"] == propName:
|
||||||
|
propInfo = prop
|
||||||
|
break
|
||||||
|
if propInfo == None:
|
||||||
|
raise RuntimeError(f"Unable to find prop with name {propName} in {groupName} from " + self.libraryInfo["name"])
|
||||||
|
|
||||||
|
# Create the prop
|
||||||
|
prop = Prop()
|
||||||
|
meshInfo = propInfo["mesh"]
|
||||||
|
spriteInfo = propInfo["sprite"]
|
||||||
|
if meshInfo != None:
|
||||||
|
modelPath = f"{self.directory}/" + meshInfo["file"]
|
||||||
|
prop.loadModel(modelPath)
|
||||||
|
elif spriteInfo != None:
|
||||||
|
prop.loadSprite(propInfo)
|
||||||
|
else:
|
||||||
|
#XXX: Uhhhhhh, empty prop?
|
||||||
|
pass
|
||||||
|
self.propGroups[groupName][propName] = prop
|
||||||
|
|
||||||
|
return self.propGroups[groupName][propName]
|
||||||
|
|
||||||
|
def getTexture(self, textureName):
|
||||||
|
im = load_image(textureName, self.directory)
|
||||||
|
return im
|
||||||
|
|
||||||
|
class Prop:
|
||||||
|
def __init__(self):
|
||||||
|
self.objects = []
|
||||||
|
self.mainObject = None
|
||||||
|
|
||||||
|
def loadModel(self, modelPath):
|
||||||
|
fileExtension = modelPath.split(".")[-1]
|
||||||
|
if fileExtension == "a3d":
|
||||||
|
modelData = A3D()
|
||||||
|
with open(modelPath, "rb") as file: modelData.read(file)
|
||||||
|
|
||||||
|
# Import the model
|
||||||
|
modelImporter = A3DBlenderImporter(modelData, None, reset_empty_transform=False, try_import_textures=False)
|
||||||
|
self.objects = modelImporter.importData()
|
||||||
|
elif fileExtension == "3ds":
|
||||||
|
bpy.ops.import_scene.max3ds(filepath=modelPath, use_apply_transform=False)
|
||||||
|
for ob in bpy.context.selectable_objects:
|
||||||
|
# The imported objects are added to the active collection, remove them
|
||||||
|
bpy.context.collection.objects.unlink(ob)
|
||||||
|
|
||||||
|
# Correct the origin XXX: this does not work for all cases, investigate more
|
||||||
|
ob.animation_data_clear()
|
||||||
|
x, y, z = -ob.location.x, -ob.location.y, -ob.location.z
|
||||||
|
objectOrigin = Matrix.Translation((x, y, z))
|
||||||
|
ob.data.transform(objectOrigin)
|
||||||
|
ob.location = (0.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
self.objects.append(ob)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Unknown model file extension: {fileExtension}")
|
||||||
|
|
||||||
|
# Identify the main parent object
|
||||||
|
for ob in self.objects:
|
||||||
|
if ob.parent == None: self.mainObject = ob
|
||||||
|
if self.mainObject == None:
|
||||||
|
raise RuntimeError(f"Unable to find the parent object for: {modelPath}")
|
||||||
|
|
||||||
|
def loadSprite(self, propInfo):
|
||||||
|
spriteInfo = propInfo["sprite"]
|
||||||
|
|
||||||
|
# Create a plane we can use for the sprite
|
||||||
|
me = bpy.data.meshes.new(propInfo["name"])
|
||||||
|
|
||||||
|
# bm = bmesh.new()
|
||||||
|
# bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=spriteInfo["scale"]*100)
|
||||||
|
# bm.to_mesh(me)
|
||||||
|
# bm.free()
|
||||||
|
|
||||||
|
ob = bpy.data.objects.new(me.name, me)
|
||||||
|
|
||||||
|
# Assign data
|
||||||
|
ob.scale = (spriteInfo["width"], 1.0, spriteInfo["height"]) #XXX: this should involve spriteInfo["scale"] probably?
|
||||||
|
spriteOrigin = Matrix.Translation((0.0, spriteInfo["originY"], 0.0))
|
||||||
|
me.transform(spriteOrigin)
|
||||||
|
|
||||||
|
# Finalise
|
||||||
|
self.objects.append(ob)
|
||||||
|
self.mainObject = ob
|
||||||
51
io_scene_a3d/BlenderMaterialUtils.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
'''
|
||||||
|
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.
|
||||||
|
'''
|
||||||
|
|
||||||
|
'''
|
||||||
|
Functions
|
||||||
|
'''
|
||||||
|
def addImageTextureToMaterial(image, node_tree, linkAlpha=False):
|
||||||
|
nodes = node_tree.nodes
|
||||||
|
links = node_tree.links
|
||||||
|
|
||||||
|
# Check if this material has already been edited
|
||||||
|
if len(nodes) > 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create nodes
|
||||||
|
bsdfNode = nodes.get("Principled BSDF")
|
||||||
|
textureNode = nodes.new(type="ShaderNodeTexImage")
|
||||||
|
links.new(textureNode.outputs["Color"], bsdfNode.inputs["Base Color"])
|
||||||
|
if linkAlpha:
|
||||||
|
links.new(textureNode.outputs["Alpha"], bsdfNode.inputs["Alpha"])
|
||||||
|
|
||||||
|
# Apply image
|
||||||
|
if image != None: textureNode.image = image
|
||||||
|
|
||||||
|
def decodeIntColorToTuple(intColor):
|
||||||
|
# Fromat is argb
|
||||||
|
a = (intColor >> 24) & 255
|
||||||
|
r = (intColor >> 16) & 255
|
||||||
|
g = (intColor >> 8) & 255
|
||||||
|
b = intColor & 255
|
||||||
|
|
||||||
|
return (r/255, g/255, b/255)
|
||||||
50
io_scene_a3d/IOTools.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
'''
|
||||||
|
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", errors="ignore")
|
||||||
|
|
||||||
|
def calculatePadding(length):
|
||||||
|
# (it basically works with rounding)
|
||||||
|
paddingSize = (((length + 3) // 4) * 4) - length
|
||||||
|
return paddingSize
|
||||||
|
|
||||||
|
def readLengthPrefixedString(stream):
|
||||||
|
length, = unpackStream("<I", stream)
|
||||||
|
string = stream.read(length)
|
||||||
|
|
||||||
|
paddingSize = calculatePadding(length)
|
||||||
|
stream.read(paddingSize)
|
||||||
|
|
||||||
|
return string.decode("utf8", errors="ignore")
|
||||||
113
io_scene_a3d/LightmapData.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
'''
|
||||||
|
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 .IOTools import unpackStream
|
||||||
|
from . import AlternativaProtocol
|
||||||
|
|
||||||
|
class LightmapData:
|
||||||
|
def __init__(self):
|
||||||
|
self.lightColour = (0.0, 0.0, 0.0)
|
||||||
|
self.ambientLightColour = (0.0, 0.0, 0.0)
|
||||||
|
self.lightAngle = (0.0, 0.0) # (x, z)
|
||||||
|
self.lightmaps = []
|
||||||
|
self.mapObjects = []
|
||||||
|
|
||||||
|
def read(self, stream):
|
||||||
|
print("Reading LightmapData")
|
||||||
|
|
||||||
|
# There is no signature so just start reading data and hope this is actually a lightmap data file
|
||||||
|
version, = unpackStream("<I", stream)
|
||||||
|
print(f"Reading LightmapData version {version}")
|
||||||
|
|
||||||
|
if version == 1:
|
||||||
|
self.read1(stream)
|
||||||
|
elif version == 2:
|
||||||
|
self.read2(stream)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Unknown LightmapData version: {version}")
|
||||||
|
|
||||||
|
'''
|
||||||
|
Version specific readers
|
||||||
|
'''
|
||||||
|
def read1(self, stream):
|
||||||
|
raise RuntimeError("Version 1 LightmapData is not implemented yet")
|
||||||
|
|
||||||
|
def read2(self, stream):
|
||||||
|
# Light info
|
||||||
|
self.lightColour, self.ambientLightColour = unpackStream("<2I", stream)
|
||||||
|
self.lightAngle = unpackStream("<2f", stream)
|
||||||
|
|
||||||
|
# Lightmaps
|
||||||
|
lightmapCount, = unpackStream("<I", stream)
|
||||||
|
print(f"Reading {lightmapCount} lightmaps")
|
||||||
|
for _ in range(lightmapCount):
|
||||||
|
lightmap = AlternativaProtocol.readString(stream)
|
||||||
|
self.lightmaps.append(lightmap)
|
||||||
|
|
||||||
|
# Map objects
|
||||||
|
mapObjectCount, = unpackStream("<I", stream)
|
||||||
|
print(f"Reading {mapObjectCount} map objects")
|
||||||
|
for _ in range(mapObjectCount):
|
||||||
|
mapObject = MapObject()
|
||||||
|
mapObject.read(stream)
|
||||||
|
self.mapObjects.append(mapObject)
|
||||||
|
|
||||||
|
#XXX: there is more data but do we actually care about it?
|
||||||
|
|
||||||
|
print(f"[LightmapData2 lightColour: {hex(self.lightColour)} ambientLightColour: {hex(self.ambientLightColour)} lightAngle: {self.lightAngle}]")
|
||||||
|
|
||||||
|
'''
|
||||||
|
Objects
|
||||||
|
'''
|
||||||
|
class MapObject:
|
||||||
|
def __init__(self):
|
||||||
|
self.index = 0
|
||||||
|
self.lightmapIndex = 0
|
||||||
|
self.lightmapScaleOffset = (0.0, 0.0, 0.0, 0.0)
|
||||||
|
self.UV1 = []
|
||||||
|
self.UV2 = []
|
||||||
|
self.castShadows = False
|
||||||
|
self.recieveShadows = False
|
||||||
|
|
||||||
|
def read(self, stream):
|
||||||
|
self.index, self.lightmapIndex = unpackStream("<2i", stream)
|
||||||
|
|
||||||
|
# Read lightmap data
|
||||||
|
if self.lightmapIndex >= 0:
|
||||||
|
self.lightmapScaleOffset = unpackStream("<4f", stream)
|
||||||
|
|
||||||
|
# Check if we have UVs and read them
|
||||||
|
hasUVs, = unpackStream("b", stream)
|
||||||
|
if hasUVs > 0:
|
||||||
|
vertexCount, = unpackStream("<I", stream)
|
||||||
|
for _ in range(vertexCount//2):
|
||||||
|
UV1 = unpackStream("<2f", stream)
|
||||||
|
self.UV1.append(UV1)
|
||||||
|
UV2 = unpackStream("<2f", stream)
|
||||||
|
self.UV2.append(UV2)
|
||||||
|
|
||||||
|
# Light settings
|
||||||
|
castShadows, recieveShadows = unpackStream("2b", stream)
|
||||||
|
self.castShadows = castShadows > 0
|
||||||
|
self.recieveShadows = recieveShadows > 0
|
||||||
|
|
||||||
|
print(f"[MapObject index: {self.index} lightmapIndex: {self.lightmapIndex} lightmapScaleOffset: {self.lightmapScaleOffset} UV1: {len(self.UV1)} UV2: {len(self.UV2)} castShadows: {self.castShadows} recieveShadows: {self.recieveShadows}]")
|
||||||
@@ -1,196 +1,208 @@
|
|||||||
'''
|
'''
|
||||||
Copyright (C) 2024 Pyogenics <https://www.github.com/Pyogenics>
|
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:
|
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 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.
|
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.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
bl_info = {
|
|
||||||
"name": "Modern A3D",
|
|
||||||
"description": "Support for modern a3d models",
|
|
||||||
"author": "Pyogenics, https://www.github.com/Pyogenics",
|
|
||||||
"version": (1, 0, 0),
|
|
||||||
"blender": (4, 0, 0),
|
|
||||||
"location": "File > Import-Export",
|
|
||||||
"category": "Import-Export"
|
|
||||||
}
|
|
||||||
|
|
||||||
import bmesh
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Operator
|
from bpy.types import Operator, OperatorFileListElement, AddonPreferences
|
||||||
from bpy.props import StringProperty
|
from bpy.props import StringProperty, BoolProperty, CollectionProperty, FloatProperty
|
||||||
from bpy_extras.io_utils import ImportHelper
|
from bpy_extras.io_utils import ImportHelper
|
||||||
|
|
||||||
from .A3D3_2 import A3D3_2
|
from .A3D import A3D
|
||||||
from .A3D3_3 import A3D3_3
|
from .A3DBlenderImporter import A3DBlenderImporter
|
||||||
from .A3DIOTools import unpackStream
|
from .BattleMap import BattleMap
|
||||||
|
from .BattleMapBlenderImporter import BattleMapBlenderImporter
|
||||||
|
from .LightmapData import LightmapData
|
||||||
|
|
||||||
|
from os.path import isdir
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
'''
|
||||||
|
Addon preferences
|
||||||
|
'''
|
||||||
|
class Preferences(AddonPreferences):
|
||||||
|
bl_idname = __package__
|
||||||
|
|
||||||
|
propLibrarySourcePath: StringProperty(name="Prop library source path", subtype='DIR_PATH')
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.prop(self, "propLibrarySourcePath")
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Operators
|
Operators
|
||||||
'''
|
'''
|
||||||
class ImportA3DModern(Operator, ImportHelper):
|
class ImportA3D(Operator, ImportHelper):
|
||||||
bl_idname = "import_scene.a3dmodern"
|
bl_idname = "import_scene.alternativa"
|
||||||
bl_label = "Import A3D"
|
bl_label = "Import A3D"
|
||||||
bl_description = "Import an A3D model"
|
bl_description = "Import an A3D model"
|
||||||
|
bl_options = {'PRESET', 'UNDO'}
|
||||||
|
|
||||||
filter_glob: StringProperty(default="*.a3d", options={'HIDDEN'})
|
filter_glob: StringProperty(default="*.a3d", options={'HIDDEN'})
|
||||||
|
directory: StringProperty(subtype="DIR_PATH", options={'HIDDEN'})
|
||||||
|
files: CollectionProperty(type=OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
|
||||||
|
|
||||||
|
# User options
|
||||||
|
create_collection: BoolProperty(name="Create collection", description="Create a collection to hold all the model objects", default=False)
|
||||||
|
try_import_textures: BoolProperty(name="Search for textures", description="Automatically search for lightmap, track and wheel textures and attempt to apply them", default=True)
|
||||||
|
reset_empty_transform: BoolProperty(name="Reset empty transforms", description="Reset rotation and scale if it is set to 0, more useful for version 2 models like props", default=True)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
import_panel_options_a3d(self.layout, self)
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
return ImportHelper.invoke(self, context, event)
|
return ImportHelper.invoke(self, context, event)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
filepath = self.filepath
|
importStartTime = time()
|
||||||
print(f"Importing A3D scene from {filepath}")
|
|
||||||
|
|
||||||
|
objects = []
|
||||||
|
for file in self.files:
|
||||||
|
filepath = f"{self.directory}/{file.name}"
|
||||||
|
# Read the file
|
||||||
|
print(f"Reading A3D data from {filepath}")
|
||||||
|
modelData = A3D()
|
||||||
with open(filepath, "rb") as file:
|
with open(filepath, "rb") as file:
|
||||||
signature = file.read(4)
|
modelData.read(file)
|
||||||
if signature != b"A3D\0":
|
|
||||||
raise RuntimeError(f"Invalid A3D signature: {signature}")
|
|
||||||
|
|
||||||
variant, _, rootBlockMarker, _ = unpackStream("<2H2I", file)
|
# Import data into blender
|
||||||
if rootBlockMarker != 1:
|
modelImporter = A3DBlenderImporter(modelData, self.directory, self.reset_empty_transform, self.try_import_textures)
|
||||||
raise RuntimeError(f"Invalid root block marker: {rootBlockMarker}")
|
objects += modelImporter.importData()
|
||||||
|
|
||||||
if variant == 3:
|
# Link objects to collection
|
||||||
a3d = A3D3_3()
|
collection = bpy.context.collection
|
||||||
a3d.read(file)
|
if self.create_collection:
|
||||||
|
collection = bpy.data.collections.new("Collection")
|
||||||
|
bpy.context.collection.children.link(collection)
|
||||||
|
for obI, ob in enumerate(objects):
|
||||||
|
collection.objects.link(ob)
|
||||||
|
|
||||||
for mesh in a3d.meshes:
|
importEndTime = time()
|
||||||
blenderMesh = self.createBlenderMeshMin(mesh)
|
self.report({'INFO'}, f"Imported {len(objects)} objects in {importEndTime-importStartTime}s")
|
||||||
blenderObject = bpy.data.objects.new(mesh.name, blenderMesh)
|
|
||||||
bpy.context.collection.objects.link(blenderObject)
|
|
||||||
elif variant == 2:
|
|
||||||
a3d = A3D3_2()
|
|
||||||
a3d.read(file)
|
|
||||||
|
|
||||||
# Create our materials
|
|
||||||
materials = {}
|
|
||||||
for materialName in a3d.materialNames:
|
|
||||||
materials[materialName] = bpy.data.materials.new(materialName)
|
|
||||||
|
|
||||||
a3dMesh = a3d.meshes[0]
|
|
||||||
blenderMesh = self.createBlenderMesh(a3dMesh, materials)
|
|
||||||
blenderObject = bpy.data.objects.new(a3dMesh.name, blenderMesh)
|
|
||||||
bpy.context.collection.objects.link(blenderObject)
|
|
||||||
elif variant == 1:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.report({"INFO"}, f"Loaded A3D")
|
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
def createBlenderMeshMin(self, mesh):
|
class ImportBattleMap(Operator, ImportHelper):
|
||||||
me = bpy.data.meshes.new(mesh.name)
|
bl_idname = "import_scene.tanki_battlemap"
|
||||||
bm = bmesh.new()
|
bl_label = "Import map"
|
||||||
|
bl_description = "Import a BIN format Tanki Online map file"
|
||||||
|
bl_options = {'PRESET', 'UNDO'}
|
||||||
|
|
||||||
for coord in mesh.coordinates:
|
filter_glob: StringProperty(default="*.bin", options={'HIDDEN'})
|
||||||
bm.verts.new(coord)
|
directory: StringProperty(subtype="DIR_PATH", options={'HIDDEN'})
|
||||||
bm.verts.ensure_lookup_table()
|
|
||||||
bm.verts.index_update()
|
|
||||||
for face in mesh.faces:
|
|
||||||
v1, v2, v3 = face
|
|
||||||
bm.faces.new([
|
|
||||||
bm.verts[v1],
|
|
||||||
bm.verts[v2],
|
|
||||||
bm.verts[v3]
|
|
||||||
])
|
|
||||||
|
|
||||||
layers = []
|
# User options
|
||||||
if len(mesh.uv1) != 0:
|
import_static_geom: BoolProperty(name="Import static geometry", description="Static geometry includes all the visual aspects of the map", default=True)
|
||||||
layers.append(
|
import_collision_geom: BoolProperty(name="Import collision geometry", description="Collision geometry defines the geometry used for collision checks and cannot normally be seen by players", default=False)
|
||||||
(bm.loops.layers.uv.new("UV1"), mesh.uv1)
|
import_spawn_points: BoolProperty(name="Import spawn points", description="Places a marker at locations where tanks can spawn", default=False)
|
||||||
)
|
import_lightmapdata: BoolProperty(name="Import lighting information", description="Loads the lightmapdata file which stores information about the sun, ambient lighting and shadow settings. Only works on remaster maps.", default=True)
|
||||||
print("has UV1")
|
map_scale_factor: FloatProperty(name="Map scale", description="Sets the map's default scale, maps and models are at a 100x scale so this allows you to directly import the map in the right size.", default=0.01, min=0.0, soft_max=1.0)
|
||||||
if len(mesh.uv2) != 0:
|
|
||||||
layers.append(
|
|
||||||
(bm.loops.layers.uv.new("UV2"), mesh.uv2)
|
|
||||||
)
|
|
||||||
print("has UV2")
|
|
||||||
for face in bm.faces:
|
|
||||||
for loop in face.loops:
|
|
||||||
for uvLayer, uvData in layers: loop[uvLayer].uv = uvData[loop.vert.index]
|
|
||||||
loop.vert.normal = mesh.normals[loop.vert.index]
|
|
||||||
|
|
||||||
bm.to_mesh(me)
|
def draw(self, context):
|
||||||
me.update()
|
import_panel_options_battlemap(self.layout, self)
|
||||||
|
|
||||||
return me
|
def invoke(self, context, event):
|
||||||
|
return ImportHelper.invoke(self, context, event)
|
||||||
|
|
||||||
def createBlenderMesh(self, mesh, materials):
|
def execute(self, context):
|
||||||
me = bpy.data.meshes.new(mesh.name)
|
print(f"Reading BattleMap data from {self.filepath}")
|
||||||
bm = bmesh.new()
|
importStartTime = time()
|
||||||
|
|
||||||
for coord in mesh.coordinates:
|
# lightmapdata files only exist for remaster maps
|
||||||
bm.verts.new(coord)
|
lightmapData = LightmapData()
|
||||||
bm.verts.ensure_lookup_table()
|
if self.import_lightmapdata:
|
||||||
bm.verts.index_update()
|
try:
|
||||||
for face in mesh.faces:
|
with open(f"{self.directory}/lightmapdata", "rb") as file: lightmapData.read(file)
|
||||||
v1, v2, v3 = face
|
except:
|
||||||
bm.faces.new([
|
print("Couldn't open lightmapdata file, ignoring")
|
||||||
bm.verts[v1],
|
self.import_lightmapdata = False
|
||||||
bm.verts[v2],
|
|
||||||
bm.verts[v3]
|
|
||||||
])
|
|
||||||
|
|
||||||
layers = []
|
# read map data
|
||||||
if len(mesh.uv1) != 0:
|
mapData = BattleMap()
|
||||||
layers.append(
|
with open(self.filepath, "rb") as file:
|
||||||
(bm.loops.layers.uv.new("UV1"), mesh.uv1)
|
mapData.read(file)
|
||||||
)
|
|
||||||
print("has UV1")
|
|
||||||
if len(mesh.uv2) != 0:
|
|
||||||
layers.append(
|
|
||||||
(bm.loops.layers.uv.new("UV2"), mesh.uv2)
|
|
||||||
)
|
|
||||||
print("has UV2")
|
|
||||||
for face in bm.faces:
|
|
||||||
for loop in face.loops:
|
|
||||||
for uvLayer, uvData in layers: loop[uvLayer].uv = uvData[loop.vert.index]
|
|
||||||
loop.vert.normal = mesh.normals[loop.vert.index]
|
|
||||||
|
|
||||||
bm.to_mesh(me)
|
# Import data into blender
|
||||||
me.update()
|
preferences = context.preferences.addons[__package__].preferences
|
||||||
|
if not isdir(preferences.propLibrarySourcePath):
|
||||||
|
raise RuntimeError("Please set a valid prop library folder in addon properties!")
|
||||||
|
mapImporter = BattleMapBlenderImporter(mapData, lightmapData, preferences.propLibrarySourcePath, self.map_scale_factor, self.import_static_geom, self.import_collision_geom, self.import_spawn_points, self.import_lightmapdata)
|
||||||
|
objects = mapImporter.importData()
|
||||||
|
|
||||||
# Materials
|
# Link objects
|
||||||
for submesh in mesh.submeshes:
|
collection = bpy.context.collection
|
||||||
material = materials[submesh.material]
|
for ob in objects:
|
||||||
me.materials.append(material)
|
collection.objects.link(ob)
|
||||||
materialI = len(me.materials) - 1
|
|
||||||
for polygon in me.polygons:
|
|
||||||
polygon.material_index = materialI
|
|
||||||
|
|
||||||
return me
|
importEndTime = time()
|
||||||
|
self.report({'INFO'}, f"Imported {len(objects)} objects in {importEndTime-importStartTime}s")
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Menu
|
Menu
|
||||||
'''
|
'''
|
||||||
|
def import_panel_options_a3d(layout, operator):
|
||||||
|
header, body = layout.panel("alternativa_import_options", default_closed=False)
|
||||||
|
header.label(text="Options")
|
||||||
|
if body:
|
||||||
|
body.prop(operator, "create_collection")
|
||||||
|
body.prop(operator, "try_import_textures")
|
||||||
|
body.prop(operator, "reset_empty_transform")
|
||||||
|
|
||||||
|
def import_panel_options_battlemap(layout, operator):
|
||||||
|
header, body = layout.panel("tanki_battlemap_import_options", default_closed=False)
|
||||||
|
header.label(text="Options")
|
||||||
|
if body:
|
||||||
|
body.prop(operator, "import_static_geom")
|
||||||
|
body.prop(operator, "import_collision_geom")
|
||||||
|
body.prop(operator, "import_spawn_points")
|
||||||
|
body.prop(operator, "import_lightmapdata")
|
||||||
|
body.prop(operator, "map_scale_factor")
|
||||||
|
|
||||||
def menu_func_import_a3d(self, context):
|
def menu_func_import_a3d(self, context):
|
||||||
self.layout.operator(ImportA3DModern.bl_idname, text="A3D Modern")
|
self.layout.operator(ImportA3D.bl_idname, text="Alternativa3D HTML5 (.a3d)")
|
||||||
|
|
||||||
|
def menu_func_import_battlemap(self, context):
|
||||||
|
self.layout.operator(ImportBattleMap.bl_idname, text="Tanki Online BattleMap (.bin)")
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Register
|
Registration
|
||||||
'''
|
'''
|
||||||
classes = {
|
classes = [
|
||||||
ImportA3DModern
|
Preferences,
|
||||||
}
|
ImportA3D,
|
||||||
|
ImportBattleMap
|
||||||
|
]
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
# Register classes
|
|
||||||
for c in classes:
|
for c in classes:
|
||||||
bpy.utils.register_class(c)
|
bpy.utils.register_class(c)
|
||||||
# File > Import-Export
|
|
||||||
bpy.types.TOPBAR_MT_file_import.append(menu_func_import_a3d)
|
bpy.types.TOPBAR_MT_file_import.append(menu_func_import_a3d)
|
||||||
# bpy.types.TOPBAR_MT_file_export.append(menu_func_export_dava)
|
bpy.types.TOPBAR_MT_file_import.append(menu_func_import_battlemap)
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
# Unregister classes
|
|
||||||
for c in classes:
|
for c in classes:
|
||||||
bpy.utils.unregister_class(c)
|
bpy.utils.unregister_class(c)
|
||||||
# Remove `File > Import-Export`
|
|
||||||
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import_a3d)
|
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import_a3d)
|
||||||
# bpy.types.TOPBAR_MT_file_export.remove(menu_func_export_dava)
|
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import_battlemap)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
register()
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
'''
|
|
||||||
Copyright (C) 2024 Pyogenics <https://www.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 .A3DIOTools import unpackStream
|
|
||||||
from .A3D3_2 import A3D3_2
|
|
||||||
from .A3D3_3 import A3D3_3
|
|
||||||
|
|
||||||
def readA3D(file):
|
|
||||||
signature = file.read(4)
|
|
||||||
if signature != b"A3D\0":
|
|
||||||
raise RuntimeError(f"Invalid A3D signature: {signature}")
|
|
||||||
|
|
||||||
variant, _, rootBlockMarker, _ = unpackStream("<2H2I", file)
|
|
||||||
if rootBlockMarker != 1:
|
|
||||||
raise RuntimeError(f"Invalid root block marker: {rootBlockMarker}")
|
|
||||||
|
|
||||||
if variant == 3:
|
|
||||||
a3d = A3D3_3()
|
|
||||||
a3d.read(file)
|
|
||||||
elif variant == 2:
|
|
||||||
a3d = A3D3_2()
|
|
||||||
a3d.read(file)
|
|
||||||
elif variant == 1:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise RuntimeError(f"Unknown A3D variant: {variant}")
|
|
||||||
|
|
||||||
from sys import argv
|
|
||||||
if __name__ == "__main__":
|
|
||||||
with open(argv[1], "rb") as file:
|
|
||||||
readA3D(file)
|
|
||||||
36
io_scene_a3d/blender_manifest.toml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
|
id = "alternativa3d_tanki_format"
|
||||||
|
version = "1.0.0"
|
||||||
|
name = "Alternativa3D file format (Tanki Online HTML5)"
|
||||||
|
tagline = "Import-Export Alternativa3D 3D models used by Tanki Online HTML5"
|
||||||
|
maintainer = "Pyogenics <https://github.com/Pyogenics>"
|
||||||
|
type = "add-on"
|
||||||
|
|
||||||
|
website = "https://github.com/MapMakersAndProgrammers/io_scene_a3d"
|
||||||
|
|
||||||
|
tags = ["Import-Export"]
|
||||||
|
|
||||||
|
blender_version_min = "4.2.0"
|
||||||
|
|
||||||
|
license = [
|
||||||
|
"SPDX:MIT",
|
||||||
|
]
|
||||||
|
copyright = [
|
||||||
|
"2024 Pyogenics",
|
||||||
|
]
|
||||||
|
|
||||||
|
# wheels = [
|
||||||
|
# ]
|
||||||
|
|
||||||
|
[permissions]
|
||||||
|
files = "Import-Export Alternativa3D 3D model files"
|
||||||
|
|
||||||
|
# [build]
|
||||||
|
# # These are the default build excluded patterns.
|
||||||
|
# # You only need to edit them if you want different options.
|
||||||
|
# paths_exclude_pattern = [
|
||||||
|
# "__pycache__/",
|
||||||
|
# "/.git/",
|
||||||
|
# "/*.zip",
|
||||||
|
# ]
|
||||||