455 lines
13 KiB
Python
455 lines
13 KiB
Python
bl_info = {
|
|
"name": "Game Exporter",
|
|
"description": "Exports the game data.",
|
|
"author": "Austin Morlan",
|
|
"blender": (2, 93, 0),
|
|
"location": "File > Export > Game (.bin)",
|
|
"category": "Import-Export",
|
|
}
|
|
|
|
import bpy
|
|
import math
|
|
import re
|
|
import os
|
|
import numpy as np
|
|
import wave
|
|
from dataclasses import dataclass
|
|
|
|
from bpy_extras.io_utils import ExportHelper
|
|
from bpy.props import StringProperty, BoolProperty, EnumProperty
|
|
from bpy.types import Operator
|
|
|
|
@dataclass
|
|
class Vertex:
|
|
position: np.array
|
|
normal: np.array
|
|
texcoord: np.array
|
|
|
|
@dataclass
|
|
class Texture:
|
|
data: bytearray
|
|
width: int
|
|
height: int
|
|
channels: int
|
|
|
|
@dataclass
|
|
class Material:
|
|
name: str
|
|
texture: Texture
|
|
|
|
@dataclass
|
|
class Mesh:
|
|
name: str
|
|
material_idx: int
|
|
indices: list
|
|
vertices: list
|
|
|
|
@dataclass
|
|
class Collider:
|
|
position: np.array
|
|
extents: np.array
|
|
|
|
@dataclass
|
|
class Tile:
|
|
x: int
|
|
y: int
|
|
|
|
@dataclass
|
|
class Audio:
|
|
name: str
|
|
data: np.array
|
|
length: int
|
|
channel_count: int
|
|
sample_rate: int
|
|
volume: np.array
|
|
is_looping: bool
|
|
|
|
@dataclass
|
|
class Transform:
|
|
position: np.array
|
|
rotation: np.array
|
|
scale: np.array
|
|
|
|
@dataclass
|
|
class Object:
|
|
transform: Transform
|
|
|
|
def load_mesh(materials, mesh):
|
|
# Calculate triangles and split normals
|
|
mesh.calc_loop_triangles()
|
|
mesh.calc_normals_split()
|
|
|
|
# Collect all of the indices so that we know how many unique vertices we have
|
|
indices = []
|
|
max_index = 0
|
|
for tri in mesh.loop_triangles:
|
|
loop_idx0 = tri.loops[0]
|
|
loop_idx1 = tri.loops[1]
|
|
loop_idx2 = tri.loops[2]
|
|
|
|
indices.append(loop_idx0)
|
|
indices.append(loop_idx1)
|
|
indices.append(loop_idx2)
|
|
|
|
max_index = max(max_index, max(loop_idx0, max(loop_idx1, loop_idx2)))
|
|
|
|
indices = np.array(indices, dtype="uint16")
|
|
|
|
vertices = [None] * (max_index+1)
|
|
for tri in mesh.loop_triangles:
|
|
vert_idx0 = tri.vertices[0]
|
|
vert_idx1 = tri.vertices[1]
|
|
vert_idx2 = tri.vertices[2]
|
|
loop_idx0 = tri.loops[0]
|
|
loop_idx1 = tri.loops[1]
|
|
loop_idx2 = tri.loops[2]
|
|
|
|
p0 = np.array([
|
|
mesh.vertices[vert_idx0].co[0],
|
|
mesh.vertices[vert_idx0].co[1],
|
|
mesh.vertices[vert_idx0].co[2]], dtype="float32")
|
|
|
|
p1 = np.array([
|
|
mesh.vertices[vert_idx1].co[0],
|
|
mesh.vertices[vert_idx1].co[1],
|
|
mesh.vertices[vert_idx1].co[2]], dtype="float32")
|
|
|
|
p2 = np.array([
|
|
mesh.vertices[vert_idx2].co[0],
|
|
mesh.vertices[vert_idx2].co[1],
|
|
mesh.vertices[vert_idx2].co[2]], dtype="float32")
|
|
|
|
n0 = np.array([
|
|
tri.split_normals[0][0],
|
|
tri.split_normals[0][1],
|
|
tri.split_normals[0][2]], dtype="float32")
|
|
|
|
n1 = np.array([
|
|
tri.split_normals[1][0],
|
|
tri.split_normals[1][1],
|
|
tri.split_normals[1][2]], dtype="float32")
|
|
|
|
n2 = np.array([
|
|
tri.split_normals[2][0],
|
|
tri.split_normals[2][1],
|
|
tri.split_normals[2][2]], dtype="float32")
|
|
|
|
t0 = np.array([
|
|
mesh.uv_layers.active.data[loop_idx0].uv[0],
|
|
mesh.uv_layers.active.data[loop_idx0].uv[1]], dtype="float32")
|
|
|
|
t1 = np.array([
|
|
mesh.uv_layers.active.data[loop_idx1].uv[0],
|
|
mesh.uv_layers.active.data[loop_idx1].uv[1]], dtype="float32")
|
|
|
|
t2 = np.array([
|
|
mesh.uv_layers.active.data[loop_idx2].uv[0],
|
|
mesh.uv_layers.active.data[loop_idx2].uv[1]], dtype="float32")
|
|
|
|
v0 = Vertex(p0, n0, t0)
|
|
v1 = Vertex(p1, n1, t1)
|
|
v2 = Vertex(p2, n2, t2)
|
|
|
|
vertices[loop_idx0] = v0
|
|
vertices[loop_idx1] = v1
|
|
vertices[loop_idx2] = v2
|
|
|
|
material_idx = 65535
|
|
if len(mesh.materials) > 0:
|
|
material = mesh.materials[0]
|
|
if material != None:
|
|
material_name = mesh.materials[0].name
|
|
|
|
for i, m in enumerate(materials):
|
|
if m.name == material_name:
|
|
material_idx = i
|
|
break
|
|
|
|
return Mesh(mesh.name, material_idx, indices, vertices)
|
|
|
|
def load_material(material):
|
|
node = material.node_tree.nodes['Image Texture']
|
|
|
|
data = bytearray([int(p * 255) for p in node.image.pixels])
|
|
width = node.image.size[0]
|
|
height = node.image.size[1]
|
|
channels = node.image.channels
|
|
|
|
texture = Texture(data, width, height, channels)
|
|
|
|
return Material(material.name, texture)
|
|
|
|
def load_transform(item):
|
|
position = np.array([round(item.location.x, 4), round(item.location.y, 4), round(item.location.z, 4)], dtype="float32")
|
|
rotation = np.array([round(item.rotation_euler.x, 4), round(item.rotation_euler.y, 4), round(item.rotation_euler.z, 4)], dtype="float32")
|
|
scale = np.array([round(item.scale.x, 4), round(item.scale.y, 4), round(item.scale.z, 4)], dtype="float32")
|
|
|
|
return Transform(position, rotation, scale)
|
|
|
|
def load_collider(c):
|
|
position = np.array([round(c.location.x, 4), round(c.location.y, 4), round(c.location.z, 4)], dtype="float32")
|
|
extents = np.array([round(c.scale.x/2, 4), round(c.scale.y/2, 4), round(c.scale.z, 4)], dtype="float32")
|
|
|
|
return Collider(position, extents)
|
|
|
|
def load_collision(collision):
|
|
colliders = []
|
|
|
|
for c in collision:
|
|
colliders.append(load_collider(c))
|
|
|
|
return colliders
|
|
|
|
def load_layout(layout):
|
|
tiles = []
|
|
|
|
for t in layout:
|
|
# Offset origin (-1) and scale (/2)
|
|
# Tiles are 2x2 in world but we want to view them as 1x1 for routing purposes
|
|
x = (t.location.x - 1) / 2
|
|
y = (t.location.y - 1) / 2
|
|
|
|
tiles.append(Tile(x, y))
|
|
|
|
return tiles
|
|
|
|
def load_audio(items):
|
|
audio = []
|
|
|
|
for i in items:
|
|
path = bpy.path.abspath(i.fv_audio_source.file_path)
|
|
wav = wave.open(path, 'rb')
|
|
|
|
frame_count = wav.getnframes()
|
|
channel_count = wav.getnchannels()
|
|
sample_rate = wav.getframerate()
|
|
sample_width = wav.getsampwidth()
|
|
volume = np.array(i.fv_audio_source.volume, dtype="float32")
|
|
is_looping = i.fv_audio_source.is_looping
|
|
|
|
data_bytes = wav.readframes(frame_count)
|
|
int_to_float = 1.0/32768.0
|
|
|
|
data_int = np.frombuffer(data_bytes, dtype="int16")
|
|
data_f32 = []
|
|
|
|
for d in data_int:
|
|
val = float(d*int_to_float)
|
|
data_f32.append(val)
|
|
|
|
data = np.array(data_f32, dtype="float32")
|
|
|
|
audio.append(Audio(i.name, data, frame_count, channel_count, sample_rate, volume, is_looping))
|
|
|
|
return audio
|
|
|
|
def traverse_children(obj):
|
|
if len(obj.children) == 0:
|
|
return []
|
|
|
|
for child in obj.children:
|
|
children = []
|
|
children += traverse_children(child)
|
|
|
|
def load_node(node):
|
|
children = traverse_children(node)
|
|
|
|
for child in children:
|
|
print(child)
|
|
|
|
def serialize_meshes(fp, meshes):
|
|
mesh_count = len(meshes)
|
|
fp.write(mesh_count.to_bytes(2, byteorder='little'))
|
|
|
|
for m in meshes:
|
|
name = m.name
|
|
name_length = len(name)+1
|
|
fp.write(name_length.to_bytes(2, byteorder='little'))
|
|
fp.write(name.encode('UTF-8'))
|
|
fp.write("\0".encode('UTF-8'))
|
|
|
|
fp.write(m.material_idx.to_bytes(2, byteorder='little'))
|
|
|
|
index_count = m.indices.size
|
|
fp.write(index_count.to_bytes(2, byteorder='little'))
|
|
fp.write(bytearray(m.indices))
|
|
|
|
vertex_count = len(m.vertices)
|
|
fp.write(vertex_count.to_bytes(2, byteorder='little'))
|
|
for vertex in m.vertices:
|
|
fp.write(bytearray(vertex.position))
|
|
fp.write(bytearray(vertex.normal))
|
|
fp.write(bytearray(vertex.texcoord))
|
|
|
|
def serialize_materials(fp, materials):
|
|
material_count = len(materials)
|
|
fp.write(material_count.to_bytes(2, byteorder='little'))
|
|
|
|
for m in materials:
|
|
name = m.name
|
|
name_length = len(name)+1
|
|
fp.write(name_length.to_bytes(2, byteorder='little'))
|
|
fp.write(name.encode('UTF-8'))
|
|
fp.write("\0".encode('UTF-8'))
|
|
fp.write(m.texture.width.to_bytes(2, byteorder='little'))
|
|
fp.write(m.texture.height.to_bytes(2, byteorder='little'))
|
|
fp.write(m.texture.channels.to_bytes(1, byteorder='little'))
|
|
fp.write(bytearray(m.texture.data))
|
|
|
|
def serialize_object(fp, obj):
|
|
fp.write(bytearray(obj.transform.position))
|
|
fp.write(bytearray(obj.transform.rotation))
|
|
fp.write(bytearray(obj.transform.scale))
|
|
|
|
def serialize_objects(fp, objects):
|
|
object_count = len(objects)
|
|
fp.write(object_count.to_bytes(2, byteorder='little'))
|
|
|
|
for obj in objects:
|
|
serialize_object(fp, obj)
|
|
|
|
def serialize_collider(fp, collider):
|
|
fp.write(bytearray(collider.position))
|
|
fp.write(bytearray(collider.extents))
|
|
|
|
def serialize_colliders(fp, colliders):
|
|
collider_count = len(colliders)
|
|
fp.write(collider_count.to_bytes(2, byteorder='little'))
|
|
|
|
for collider in colliders:
|
|
serialize_collider(fp, collider)
|
|
|
|
def serialize_layout(fp, layout):
|
|
tile_count = 32
|
|
tiles = [0] * tile_count
|
|
|
|
for tile in layout:
|
|
x = int(tile.x)
|
|
y = int(tile.y)
|
|
|
|
value = tiles[y] | (1 << (31-x))
|
|
tiles[y] = value
|
|
|
|
fp.write(tile_count.to_bytes(1, byteorder='little'))
|
|
for tile in tiles:
|
|
fp.write(tile.to_bytes(4, byteorder='little'))
|
|
|
|
def serialize_audio(fp, audio):
|
|
audio_count = len(audio)
|
|
fp.write(audio_count.to_bytes(2, byteorder='little'))
|
|
|
|
for a in audio:
|
|
name = a.name
|
|
name_length = len(name)+1
|
|
fp.write(name_length.to_bytes(2, byteorder='little'))
|
|
fp.write(name.encode('UTF-8'))
|
|
fp.write("\0".encode('UTF-8'))
|
|
fp.write(a.length.to_bytes(4, byteorder='little'))
|
|
fp.write(a.sample_rate.to_bytes(4, byteorder='little'))
|
|
fp.write(a.channel_count.to_bytes(1, byteorder='little'))
|
|
fp.write(bytearray(a.volume))
|
|
fp.write(a.is_looping.to_bytes(1, byteorder='little'))
|
|
fp.write(bytearray(a.data))
|
|
|
|
def serialize_world(path):
|
|
print(f'Serializing world')
|
|
|
|
materials = []
|
|
for m in bpy.data.materials:
|
|
if m.name == "Dots Stroke":
|
|
continue
|
|
|
|
material = load_material(m)
|
|
materials.append(material)
|
|
|
|
meshes = []
|
|
for m in bpy.data.meshes:
|
|
mesh = load_mesh(materials, m)
|
|
meshes.append(mesh)
|
|
|
|
coins = []
|
|
for o in bpy.data.collections['Coins'].objects:
|
|
transform = load_transform(o)
|
|
obj = Object(transform)
|
|
coins.append(obj)
|
|
|
|
eyeballs = []
|
|
for o in bpy.data.collections['Eyeballs'].objects:
|
|
transform = load_transform(o)
|
|
obj = Object(transform)
|
|
eyeballs.append(obj)
|
|
|
|
pedestals = []
|
|
for o in bpy.data.collections['Pedestals'].objects:
|
|
transform = load_transform(o)
|
|
obj = Object(transform)
|
|
pedestals.append(obj)
|
|
|
|
score_hundreds = Object(load_transform(bpy.data.objects["ScoreHundreds"]))
|
|
score_tens = Object(load_transform(bpy.data.objects["ScoreTens"]))
|
|
score_ones = Object(load_transform(bpy.data.objects["ScoreOnes"]))
|
|
score_coin = Object(load_transform(bpy.data.objects["ScoreCoin"]))
|
|
score_collider = load_collider(bpy.data.objects["ScoreCollider"])
|
|
|
|
# Collision
|
|
colliders = load_collision(bpy.data.collections['Walls'].objects)
|
|
|
|
# Level layout
|
|
layout = load_layout(bpy.data.collections['Layout'].objects)
|
|
|
|
# Load audio
|
|
music = load_audio(bpy.data.collections['Music'].objects)
|
|
sfx = load_audio(bpy.data.collections['SFX'].objects)
|
|
|
|
fp = open(path, "wb")
|
|
fp.write("AJM".encode('UTF-8'))
|
|
fp.write("\0".encode('UTF-8'))
|
|
|
|
serialize_meshes(fp, meshes)
|
|
serialize_materials(fp, materials)
|
|
serialize_objects(fp, coins)
|
|
serialize_objects(fp, eyeballs)
|
|
serialize_objects(fp, pedestals)
|
|
serialize_object(fp, score_hundreds)
|
|
serialize_object(fp, score_tens)
|
|
serialize_object(fp, score_ones)
|
|
serialize_object(fp, score_coin)
|
|
serialize_collider(fp, score_collider)
|
|
serialize_colliders(fp, colliders)
|
|
serialize_layout(fp, layout)
|
|
serialize_audio(fp, music)
|
|
serialize_audio(fp, sfx)
|
|
|
|
fp.close()
|
|
|
|
def export(path):
|
|
serialize_world(path)
|
|
return {'FINISHED'}
|
|
|
|
class ExportGame(Operator, ExportHelper):
|
|
bl_idname = "game_export.scene_text"
|
|
bl_label = "Export Game (bin) File"
|
|
filename_ext = ".bin"
|
|
filter_glob: StringProperty(
|
|
default="*.bin",
|
|
options={'HIDDEN'},
|
|
maxlen=255)
|
|
|
|
def execute(self, context):
|
|
return export(self.filepath)
|
|
|
|
def menu_func_export(self, context):
|
|
self.layout.operator(ExportGame.bl_idname, text="Game (.bin) Custom")
|
|
|
|
def register():
|
|
bpy.utils.register_class(ExportGame)
|
|
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
|
|
|
|
def unregister():
|
|
bpy.utils.unregister_class(ExportGame)
|
|
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
|
|
|
|
if __name__ == "__main__":
|
|
register()
|
|
|