WIP: Add initial BSI import
So far, it's only capable of importing basic mesh information. Signed-off-by: Lucas Schwiderski <lucas@lschwiderski.de>
This commit is contained in:
parent
acc2dd9da0
commit
80a8e239d3
2 changed files with 338 additions and 10 deletions
|
@ -28,10 +28,12 @@ bl_info = {
|
||||||
# Reload sub modules if they are already loaded
|
# Reload sub modules if they are already loaded
|
||||||
if "bpy" in locals():
|
if "bpy" in locals():
|
||||||
import importlib
|
import importlib
|
||||||
if "unit_export" in locals():
|
if "export_unit" in locals():
|
||||||
importlib.reload(unit_export)
|
importlib.reload(export_unit)
|
||||||
if "material_export" in locals():
|
if "export_material" in locals():
|
||||||
importlib.reload(material_export)
|
importlib.reload(export_material)
|
||||||
|
if "import_bsi" in locals():
|
||||||
|
importlib.reload(import_bsi)
|
||||||
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
@ -47,13 +49,14 @@ from bpy.props import (
|
||||||
PointerProperty,
|
PointerProperty,
|
||||||
)
|
)
|
||||||
from bpy_extras.io_utils import (
|
from bpy_extras.io_utils import (
|
||||||
ExportHelper,
|
ImportHelper,
|
||||||
axis_conversion,
|
axis_conversion,
|
||||||
orientation_helper,
|
orientation_helper,
|
||||||
path_reference_mode,
|
path_reference_mode,
|
||||||
)
|
)
|
||||||
from bitsquid.unit import export as unit_export
|
from bitsquid.unit import export as export_unit
|
||||||
from bitsquid.material import export as material_export
|
from bitsquid.material import export as export_material
|
||||||
|
from bitsquid import import_bsi
|
||||||
|
|
||||||
|
|
||||||
class BitsquidSettings(PropertyGroup):
|
class BitsquidSettings(PropertyGroup):
|
||||||
|
@ -110,9 +113,9 @@ class OBJECT_OT_bitsquid_export(Operator):
|
||||||
object = context.active_object
|
object = context.active_object
|
||||||
if object.bitsquid.export_materials:
|
if object.bitsquid.export_materials:
|
||||||
for material_slot in object.material_slots.values():
|
for material_slot in object.material_slots.values():
|
||||||
material_export.save(self, context, material_slot.material)
|
export_material.save(self, context, material_slot.material)
|
||||||
|
|
||||||
return unit_export.save(self, context, object)
|
return export_unit.save(self, context, object)
|
||||||
|
|
||||||
|
|
||||||
class OBJECT_PT_bitsquid(Panel):
|
class OBJECT_PT_bitsquid(Panel):
|
||||||
|
@ -159,7 +162,7 @@ class MATERIAL_OT_bitsquid_export(Operator):
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
material = context.active_material
|
material = context.active_material
|
||||||
return unit_export.save(self, context, material)
|
return export_unit.save(self, context, material)
|
||||||
|
|
||||||
|
|
||||||
class MATERIAL_PT_bitsquid(Panel):
|
class MATERIAL_PT_bitsquid(Panel):
|
||||||
|
@ -183,8 +186,42 @@ class MATERIAL_PT_bitsquid(Panel):
|
||||||
layout.operator("object.bitsquid_export_material", text="Export .material")
|
layout.operator("object.bitsquid_export_material", text="Export .material")
|
||||||
|
|
||||||
|
|
||||||
|
@orientation_helper(axis_forward='-Z', axis_up='Y')
|
||||||
|
class ImportBSI(bpy.types.Operator, ImportHelper):
|
||||||
|
"""Load a Bitsquid .bsi File"""
|
||||||
|
bl_idname = "import_scene.bsi"
|
||||||
|
bl_label = "Import BSI"
|
||||||
|
bl_options = {'PRESET', 'UNDO'}
|
||||||
|
|
||||||
|
filename_ext = ".bsi"
|
||||||
|
filter_glob: StringProperty(
|
||||||
|
default="*.bsi;*.bsiz",
|
||||||
|
options={'HIDDEN'},
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
keywords = self.as_keywords(ignore=("axis_forward",
|
||||||
|
"axis_up",
|
||||||
|
"filter_glob",
|
||||||
|
))
|
||||||
|
|
||||||
|
if bpy.data.is_saved and context.preferences.filepaths.use_relative_paths:
|
||||||
|
import os
|
||||||
|
keywords["relpath"] = os.path.dirname(bpy.data.filepath)
|
||||||
|
|
||||||
|
return import_bsi.load(self, context, **keywords)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def menu_func_import(self, context):
|
||||||
|
self.layout.operator(ImportBSI.bl_idname, text="Bitsquid Object (.bsi)")
|
||||||
|
|
||||||
|
|
||||||
# Register
|
# Register
|
||||||
classes = [
|
classes = [
|
||||||
|
ImportBSI,
|
||||||
BitsquidSettings,
|
BitsquidSettings,
|
||||||
SCENE_PT_bitsquid,
|
SCENE_PT_bitsquid,
|
||||||
BitsquidObjectSettings,
|
BitsquidObjectSettings,
|
||||||
|
@ -201,6 +238,8 @@ def register():
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
register_class(cls)
|
register_class(cls)
|
||||||
|
|
||||||
|
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
|
||||||
|
|
||||||
bpy.types.Scene.bitsquid = PointerProperty(type=BitsquidSettings)
|
bpy.types.Scene.bitsquid = PointerProperty(type=BitsquidSettings)
|
||||||
bpy.types.Object.bitsquid = PointerProperty(type=BitsquidObjectSettings)
|
bpy.types.Object.bitsquid = PointerProperty(type=BitsquidObjectSettings)
|
||||||
bpy.types.Material.bitsquid = PointerProperty(type=BitsquidMaterialSettings)
|
bpy.types.Material.bitsquid = PointerProperty(type=BitsquidMaterialSettings)
|
||||||
|
|
289
addons/bitsquid/import_bsi.py
Normal file
289
addons/bitsquid/import_bsi.py
Normal file
|
@ -0,0 +1,289 @@
|
||||||
|
# Bitsquid Blender Tools
|
||||||
|
# Copyright (C) 2021 Lucas Schwiderski
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
import zlib
|
||||||
|
import json
|
||||||
|
import bpy
|
||||||
|
import mathutils
|
||||||
|
|
||||||
|
from bpy_extras.io_utils import unpack_list
|
||||||
|
|
||||||
|
|
||||||
|
def parse_sjson(file_path, skip_editor_data=True):
|
||||||
|
"""
|
||||||
|
Translate Bitsquid .bsi SJSON data to plain JSON,
|
||||||
|
then parse into a python dictionary.
|
||||||
|
|
||||||
|
Taken from `bsi_import` in the Vermintide 2 SDK.
|
||||||
|
"""
|
||||||
|
return_dict = {}
|
||||||
|
try:
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
if data[:4] == 'bsiz':
|
||||||
|
data = zlib.decompress(data[8:])
|
||||||
|
|
||||||
|
data = data.decode("utf-8")
|
||||||
|
|
||||||
|
file_lines = ['{\n']
|
||||||
|
inside_list = False
|
||||||
|
check_editor_data = 1 if skip_editor_data else 0
|
||||||
|
|
||||||
|
for line in data.splitlines():
|
||||||
|
if check_editor_data:
|
||||||
|
if check_editor_data > 1:
|
||||||
|
if line[0] == '}':
|
||||||
|
check_editor_data = 0
|
||||||
|
|
||||||
|
continue
|
||||||
|
elif line[:18] == 'editor_metadata = ':
|
||||||
|
check_editor_data = 2
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Strip trailing whitespace,
|
||||||
|
# including line break and carriage return
|
||||||
|
line = line.rstrip()
|
||||||
|
if line[-1] in ['\n', '\r']:
|
||||||
|
line = line[:-1]
|
||||||
|
|
||||||
|
if ' = ' in line:
|
||||||
|
line_parts = line.split(' = ')
|
||||||
|
|
||||||
|
if '[ ' in line_parts[-1] and ' ]' in line_parts[-1]:
|
||||||
|
new_end = ''
|
||||||
|
end_parts = line_parts[-1].split(' ]')
|
||||||
|
short_len = len(end_parts) - 1
|
||||||
|
|
||||||
|
for i, end_part in enumerate(end_parts):
|
||||||
|
if not end_part:
|
||||||
|
if i < short_len:
|
||||||
|
new_end += ' ]'
|
||||||
|
continue
|
||||||
|
|
||||||
|
if '[ ' not in end_part:
|
||||||
|
new_end += end_part + ' ]'
|
||||||
|
continue
|
||||||
|
|
||||||
|
sub_part_pre = end_part.rpartition('[ ')
|
||||||
|
new_end = ''.join((
|
||||||
|
new_end,
|
||||||
|
sub_part_pre[0],
|
||||||
|
'[ ',
|
||||||
|
', '.join(sub_part_pre[-1].split(' ')),
|
||||||
|
' ]'
|
||||||
|
))
|
||||||
|
|
||||||
|
if i < short_len and end_parts[i + 1] and end_parts[i + 1][:2] == ' [':
|
||||||
|
new_end += ','
|
||||||
|
|
||||||
|
line_parts[-1] = new_end
|
||||||
|
|
||||||
|
# Handle indentation
|
||||||
|
if '\t' in line_parts[0]:
|
||||||
|
line_start = line_parts[0].rpartition('\t')
|
||||||
|
else:
|
||||||
|
tab_len = len(line_parts[0]) - len(line_parts[0].lstrip())
|
||||||
|
|
||||||
|
if tab_len > 4:
|
||||||
|
line_start = [line_parts[0][:tab_len - 4], '' , line_parts[0].lstrip()]
|
||||||
|
elif tab_len > 0:
|
||||||
|
line_start = ['', '', line_parts[0].lstrip()]
|
||||||
|
else:
|
||||||
|
line_start = line_parts[0].rpartition('\t')
|
||||||
|
|
||||||
|
if line_start[-1][0] == '"':
|
||||||
|
new_line = ''.join((
|
||||||
|
''.join(line_start[:-1]),
|
||||||
|
line_start[-1],
|
||||||
|
': ',
|
||||||
|
''.join(line_parts[1:])
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
new_line = ''.join((
|
||||||
|
''.join(line_start[:-1]),
|
||||||
|
'"',
|
||||||
|
line_start[-1],
|
||||||
|
'": ',
|
||||||
|
''.join(line_parts[1:])
|
||||||
|
))
|
||||||
|
|
||||||
|
if not line_parts[-1][-1] == ',':
|
||||||
|
new_line += ','
|
||||||
|
elif ']' in line or '}' in line:
|
||||||
|
new_line = line + ','
|
||||||
|
if '} {' in new_line:
|
||||||
|
new_line = new_line.replace('} {', '}, {')
|
||||||
|
if file_lines[-1][-2] == ',':
|
||||||
|
file_lines[-1] = file_lines[-1][:-2] + '\n'
|
||||||
|
elif inside_list and ']' not in line:
|
||||||
|
tab_len = len(line) - len(line.lstrip())
|
||||||
|
new_line = line[:tab_len] + ', '.join([x for x in line[tab_len:].split(' ') if x])
|
||||||
|
else:
|
||||||
|
new_line = line
|
||||||
|
|
||||||
|
if new_line[-2:] in ['[,', '{,'] or new_line[-3:] in ['[ ,', '{ ,']:
|
||||||
|
new_line = new_line[:-1]
|
||||||
|
|
||||||
|
if inside_list:
|
||||||
|
if ']' in line:
|
||||||
|
inside_list = False
|
||||||
|
elif not new_line[-1] in ['[', '{', ',']:
|
||||||
|
new_line += ','
|
||||||
|
elif '[' in line:
|
||||||
|
inside_list = True
|
||||||
|
|
||||||
|
file_lines.append(''.join(('\t', new_line.lstrip(), '\n')))
|
||||||
|
|
||||||
|
# To save memory...
|
||||||
|
if len(file_lines) > 100000:
|
||||||
|
file_lines = [''.join(file_lines)]
|
||||||
|
|
||||||
|
if file_lines[-1][-2] == ',':
|
||||||
|
file_lines[-1] = file_lines[-1][:-2] + '\n'
|
||||||
|
|
||||||
|
file_lines.append('}\n')
|
||||||
|
return_dict = json.loads(''.join(file_lines))
|
||||||
|
except ValueError:
|
||||||
|
print(file_path.replace('\\', '/') + ': SJSON file contains a syntax error')
|
||||||
|
return return_dict
|
||||||
|
|
||||||
|
|
||||||
|
def find(arr, f):
|
||||||
|
for key, val in arr.items():
|
||||||
|
if f(val, key):
|
||||||
|
return val, key
|
||||||
|
|
||||||
|
|
||||||
|
def create_object(self, context, name, node_data, geometries):
|
||||||
|
print("[create_object]", name, node_data)
|
||||||
|
vertices = []
|
||||||
|
faces = []
|
||||||
|
edges = []
|
||||||
|
|
||||||
|
tri_count = int(geometries["indices"]["size"]) / 3
|
||||||
|
|
||||||
|
for i, index_stream in enumerate(geometries["indices"]["streams"]):
|
||||||
|
data_stream = geometries["streams"][i]
|
||||||
|
stride = data_stream["stride"] / 4
|
||||||
|
|
||||||
|
for channel in data_stream['channels']:
|
||||||
|
stream_data = data_stream["data"]
|
||||||
|
if channel['name'] == 'POSITION':
|
||||||
|
if stride != 3:
|
||||||
|
raise RuntimeError("stride != 3 cannot be handled")
|
||||||
|
|
||||||
|
# Get vertex positions.
|
||||||
|
# Iterate over data in sets of three values that represent
|
||||||
|
# `x`, `y` and `z`.
|
||||||
|
for j in range(0, len(stream_data), 3):
|
||||||
|
vertices.append((
|
||||||
|
stream_data[j],
|
||||||
|
stream_data[j + 1],
|
||||||
|
stream_data[j + 2],
|
||||||
|
))
|
||||||
|
|
||||||
|
# Get face definitions. Values are vertex indices.
|
||||||
|
# Iteration works just like vertices above.
|
||||||
|
for j in range(0, len(index_stream), 3):
|
||||||
|
faces.append((
|
||||||
|
index_stream[j],
|
||||||
|
index_stream[j + 1],
|
||||||
|
index_stream[j + 2],
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
self.report(
|
||||||
|
{'WARNING'},
|
||||||
|
"Unknown channel name: {}".format(channel["name"])
|
||||||
|
)
|
||||||
|
|
||||||
|
print(vertices, faces)
|
||||||
|
|
||||||
|
mesh = bpy.data.meshes.new(name)
|
||||||
|
mesh.from_pydata(vertices, edges, faces)
|
||||||
|
|
||||||
|
return mesh
|
||||||
|
|
||||||
|
|
||||||
|
def import_node(self, context, name, node_data, global_data):
|
||||||
|
print("[import_node]", name, node_data)
|
||||||
|
if "geometries" in node_data:
|
||||||
|
geometry_name = node_data["geometries"][0]
|
||||||
|
print("[import_node] Building geometry '{}' for object '{}'".format(geometry_name, name))
|
||||||
|
geometries = global_data["geometries"][geometry_name]
|
||||||
|
mesh = create_object(self, context, name, node_data, geometries)
|
||||||
|
print(mesh)
|
||||||
|
|
||||||
|
obj = bpy.data.objects.new(mesh.name, mesh)
|
||||||
|
# TODO: Apply `local` tranformation
|
||||||
|
else:
|
||||||
|
print("[import_node] Adding empty for '{}'".format(name))
|
||||||
|
# TODO: Extract position and rotation from `local` matrix,
|
||||||
|
# if it's not the identity matrix
|
||||||
|
# NOTE: Low priority, Fatshark seems to stick with
|
||||||
|
# identity matrices here
|
||||||
|
obj = bpy.data.objects.new(name, None)
|
||||||
|
|
||||||
|
if "children" in node_data:
|
||||||
|
for child_name, child_data in node_data["children"].items():
|
||||||
|
if child_data["parent"] != name:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Assigned parent '%s' doesn't match actual parent node '%s'"
|
||||||
|
% (child_data["parent"], name)
|
||||||
|
)
|
||||||
|
|
||||||
|
child_obj = import_node(
|
||||||
|
self,
|
||||||
|
context,
|
||||||
|
child_name,
|
||||||
|
child_data,
|
||||||
|
global_data,
|
||||||
|
)
|
||||||
|
child_obj.parent = obj
|
||||||
|
|
||||||
|
# Make sure all objects are linked to the current collection
|
||||||
|
collection = context.collection
|
||||||
|
collection.objects.link(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def load(self, context, filepath, *, relpath=None):
|
||||||
|
global_data = parse_sjson(filepath)
|
||||||
|
|
||||||
|
print(global_data)
|
||||||
|
|
||||||
|
# Nothing to do if there are no nodes
|
||||||
|
if "nodes" not in global_data:
|
||||||
|
self.report({'WARNING'}, "No nodes to import in {}".format(filepath))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
view_layer = context.view_layer
|
||||||
|
global_matrix = mathutils.Matrix()
|
||||||
|
|
||||||
|
for name, node_data in global_data["nodes"].items():
|
||||||
|
obj = import_node(self, context, name, node_data, global_data)
|
||||||
|
|
||||||
|
view_layer.objects.active = obj
|
||||||
|
obj.select_set(True)
|
||||||
|
|
||||||
|
# we could apply this anywhere before scaling.
|
||||||
|
obj.matrix_world = global_matrix
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
Loading…
Add table
Add a link
Reference in a new issue