WIP: Import bones
Signed-off-by: Lucas Schwiderski <lucas@lschwiderski.de>
This commit is contained in:
parent
302e0e4863
commit
2e7282956f
1 changed files with 277 additions and 37 deletions
|
@ -178,12 +178,12 @@ def find(arr, f):
|
||||||
return val, key
|
return val, key
|
||||||
|
|
||||||
|
|
||||||
def create_object(self, context, name, node_data, geometries):
|
def create_mesh(self, context, name, node_data, geo_data):
|
||||||
"""
|
"""
|
||||||
Create a Blender object from a BSI node definition
|
Create a Blender object from a BSI node definition
|
||||||
and additional data from the file.
|
and additional data from the file.
|
||||||
"""
|
"""
|
||||||
print("[create_object]", name, node_data)
|
|
||||||
# A list of vectors that represent vertex locations.
|
# A list of vectors that represent vertex locations.
|
||||||
vertices = []
|
vertices = []
|
||||||
# A list of vectors, where each vector contains three indices into `vertices`.
|
# A list of vectors, where each vector contains three indices into `vertices`.
|
||||||
|
@ -192,10 +192,8 @@ def create_object(self, context, name, node_data, geometries):
|
||||||
uv_name = "UVMap"
|
uv_name = "UVMap"
|
||||||
uvs = []
|
uvs = []
|
||||||
|
|
||||||
tri_count = int(geometries["indices"]["size"]) / 3
|
for i, index_stream in enumerate(geo_data["indices"]["streams"]):
|
||||||
|
data_stream = geo_data["streams"][i]
|
||||||
for i, index_stream in enumerate(geometries["indices"]["streams"]):
|
|
||||||
data_stream = geometries["streams"][i]
|
|
||||||
stride = data_stream["stride"] / 4
|
stride = data_stream["stride"] / 4
|
||||||
|
|
||||||
for channel in data_stream["channels"]:
|
for channel in data_stream["channels"]:
|
||||||
|
@ -230,7 +228,15 @@ def create_object(self, context, name, node_data, geometries):
|
||||||
# Blender is able to create normals from the face definition
|
# Blender is able to create normals from the face definition
|
||||||
# (i.e. the order in which the faces vertices were defined)
|
# (i.e. the order in which the faces vertices were defined)
|
||||||
# and that seems to be good enough
|
# and that seems to be good enough
|
||||||
self.report({'INFO'}, "Ignoring custom normals")
|
# self.report({'INFO'}, "Ignoring custom normals")
|
||||||
|
continue
|
||||||
|
elif channel["name"] in {'ALPHA', 'COLOR'}:
|
||||||
|
# Not sure if this can be intended as a primitive material,
|
||||||
|
# but I'll assume it's not. And while Blender does feature
|
||||||
|
# the concept of viewport-only vertex colors, that's rather
|
||||||
|
# low priority to implement.
|
||||||
|
# self.report({'INFO'}, "Ignoring vertex color data")
|
||||||
|
continue
|
||||||
elif channel["name"] == 'TEXCOORD':
|
elif channel["name"] == 'TEXCOORD':
|
||||||
uv_name = "UVMap{}".format(channel["index"] + 1)
|
uv_name = "UVMap{}".format(channel["index"] + 1)
|
||||||
uv_data = data_stream["data"]
|
uv_data = data_stream["data"]
|
||||||
|
@ -238,16 +244,16 @@ def create_object(self, context, name, node_data, geometries):
|
||||||
uvs = [uv_defs[index_stream[j]] for j in range(0, len(index_stream))]
|
uvs = [uv_defs[index_stream[j]] for j in range(0, len(index_stream))]
|
||||||
else:
|
else:
|
||||||
# TODO: Implement other channel types
|
# TODO: Implement other channel types
|
||||||
self.report(
|
# self.report(
|
||||||
{'WARNING'},
|
# {'WARNING'},
|
||||||
"Unknown channel type: {}".format(channel["name"])
|
# "Unknown channel type: {}".format(channel["name"])
|
||||||
)
|
# )
|
||||||
|
continue
|
||||||
|
|
||||||
mesh = bpy.data.meshes.new(name)
|
mesh = bpy.data.meshes.new(name)
|
||||||
mesh.from_pydata(vertices, [], faces)
|
mesh.from_pydata(vertices, [], faces)
|
||||||
|
|
||||||
if len(uvs) > 0:
|
if len(uvs) > 0:
|
||||||
print("{} has UVs".format(name))
|
|
||||||
uv_layer = mesh.uv_layers.new(name=uv_name)
|
uv_layer = mesh.uv_layers.new(name=uv_name)
|
||||||
uv_layer.data.foreach_set("uv", unpack_list(uvs))
|
uv_layer.data.foreach_set("uv", unpack_list(uvs))
|
||||||
|
|
||||||
|
@ -271,22 +277,193 @@ def matrix_from_list(list):
|
||||||
return Matrix(rows)
|
return Matrix(rows)
|
||||||
|
|
||||||
|
|
||||||
def import_node(self, context, name, node_data, global_data):
|
def import_joint(
|
||||||
"""Import a BSI node. Recurses into child nodes."""
|
self,
|
||||||
print("[import_node]", name, node_data)
|
context,
|
||||||
if "geometries" in node_data:
|
name,
|
||||||
geometry_name = node_data["geometries"][0]
|
node_data,
|
||||||
print("[import_node] Building geometry '{}' for object '{}'".format(geometry_name, name))
|
armature,
|
||||||
geometries = global_data["geometries"][geometry_name]
|
parent_bone,
|
||||||
mesh = create_object(self, context, name, node_data, geometries)
|
parent_rotation,
|
||||||
obj = bpy.data.objects.new(mesh.name, mesh)
|
global_data
|
||||||
else:
|
):
|
||||||
print("[import_node] Adding empty for '{}'".format(name))
|
"""
|
||||||
obj = bpy.data.objects.new(name, None)
|
Imports a joint and all of its children.
|
||||||
# Decrease axis size to prevent overcrowding in the viewport
|
In BSI (smilar to Maya) skeletons are defined as a series of joints,
|
||||||
obj.empty_display_size = 0.1
|
with bones added virtually in between, when needed.
|
||||||
|
|
||||||
|
In Blender, skeletons are defined with bones, where each bone has a `head`
|
||||||
|
and `tail`. So we need to convert the list of joints to a series of bones
|
||||||
|
instead.
|
||||||
|
The challenge here is the fact that joints have rotation data, whereas
|
||||||
|
`head` and `tail` for bones don't. This changes how the position data has
|
||||||
|
to be treated, as it is relative to the respective previous joint.
|
||||||
|
Compared to the code that imports mesh objects, we can't just apply
|
||||||
|
the matrix to the bone and have it position itself relative to its parent.
|
||||||
|
Instead, we have to manually keep track of the parent's rotation.
|
||||||
|
"""
|
||||||
|
if "local" not in node_data:
|
||||||
|
raise RuntimeError("No position value for joint '{}'".format(name))
|
||||||
|
|
||||||
|
mat = matrix_from_list(node_data["local"])
|
||||||
|
translation, rotation, _ = mat.decompose()
|
||||||
|
|
||||||
|
if name.endswith("_scale"):
|
||||||
|
# print("Skipping joint '{}'".format(name))
|
||||||
|
bone = parent_bone
|
||||||
|
else:
|
||||||
|
bone = armature.edit_bones.new(name)
|
||||||
|
if parent_bone:
|
||||||
|
# The values for `bone.head` and `bone.tail` are relative to their
|
||||||
|
# parent, so we need to apply that first.
|
||||||
|
bone.parent = parent_bone
|
||||||
|
bone.use_connect = True
|
||||||
|
# bone.head = parent_bone.tail
|
||||||
|
else:
|
||||||
|
bone.head = Vector((0, 0, 0))
|
||||||
|
|
||||||
|
if parent_rotation:
|
||||||
|
print("[import_joint] {} Parent @ Local:".format(name), parent_rotation, parent_rotation @ translation)
|
||||||
|
bone.tail = bone.head + (parent_rotation @ translation)
|
||||||
|
else:
|
||||||
|
bone.tail = bone.head + translation
|
||||||
|
|
||||||
|
print("[import_joint] {} Local:".format(name), translation)
|
||||||
|
print("[import_joint] {} Bone:".format(name), bone.head, bone.tail)
|
||||||
|
|
||||||
|
if "children" in node_data:
|
||||||
|
for child_name, child_data in node_data["children"].items():
|
||||||
|
if child_data["parent"] != name:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Assigned parent '{}' doesn't match actual parent node '{}'"
|
||||||
|
.format(child_data["parent"], name)
|
||||||
|
)
|
||||||
|
|
||||||
|
if child_name.startswith("j_"):
|
||||||
|
child_bone = import_joint(
|
||||||
|
self,
|
||||||
|
context,
|
||||||
|
child_name,
|
||||||
|
child_data,
|
||||||
|
armature,
|
||||||
|
bone,
|
||||||
|
rotation,
|
||||||
|
global_data
|
||||||
|
)
|
||||||
|
child_bone.parent = bone
|
||||||
|
else:
|
||||||
|
# DEBUG: ignoring these for now
|
||||||
|
continue
|
||||||
|
# Not entirely sure, but I think these are considered
|
||||||
|
# "controller nodes" in Maya. Would make sense based on
|
||||||
|
# name.
|
||||||
|
if "children" in child_data:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Controller node '{}' has children."
|
||||||
|
.format(child_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
if "geometries" not in child_data:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Controller node '{}' has no geometry."
|
||||||
|
.format(child_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
child_obj = import_node(
|
||||||
|
self,
|
||||||
|
context,
|
||||||
|
child_name,
|
||||||
|
child_data,
|
||||||
|
global_data,
|
||||||
|
)
|
||||||
|
# TODO: How to parent to a bone?
|
||||||
|
child_obj.parent = bone
|
||||||
|
|
||||||
|
return bone
|
||||||
|
|
||||||
|
|
||||||
|
def import_armature(self, context, name, node_data, global_data):
|
||||||
|
armature = context.blend_data.armatures.new(name)
|
||||||
|
|
||||||
|
# An armature itself cannot exist in the view layer.
|
||||||
|
# We need to create an object from it
|
||||||
|
obj = bpy.data.objects.new(name, armature)
|
||||||
|
context.collection.objects.link(obj)
|
||||||
|
|
||||||
|
# Armature needs to be in EDIT mode to allow adding bones
|
||||||
|
context.view_layer.objects.active = obj
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
|
||||||
|
|
||||||
|
if "local" not in node_data:
|
||||||
|
raise RuntimeError("No position value for joint '{}'".format(name))
|
||||||
|
|
||||||
|
mat = matrix_from_list(node_data["local"])
|
||||||
|
obj.matrix_local = mat
|
||||||
|
# DEBUG
|
||||||
|
mat_loc, mat_rot, mat_scale = mat.decompose()
|
||||||
|
print("[import_joint] {}".format(name), mat_loc, mat_rot)
|
||||||
|
|
||||||
|
if "children" in node_data:
|
||||||
|
for child_name, child_data in node_data["children"].items():
|
||||||
|
if child_data["parent"] != name:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Assigned parent '{}' doesn't match actual parent node '{}'"
|
||||||
|
.format(child_data["parent"], name)
|
||||||
|
)
|
||||||
|
|
||||||
|
if child_name.startswith("j_"):
|
||||||
|
import_joint(
|
||||||
|
self,
|
||||||
|
context,
|
||||||
|
child_name,
|
||||||
|
child_data,
|
||||||
|
armature,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
global_data
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# DEBUG: Ignoring these for now
|
||||||
|
continue
|
||||||
|
# Not entirely sure, but I think these are considered
|
||||||
|
# "controller nodes" in Maya. Would make sense based on
|
||||||
|
# name.
|
||||||
|
if "children" in child_data:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Controller node '{}' has children."
|
||||||
|
.format(child_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
child_obj = import_node(
|
||||||
|
self,
|
||||||
|
context,
|
||||||
|
child_name,
|
||||||
|
child_data,
|
||||||
|
global_data,
|
||||||
|
)
|
||||||
|
child_obj.parent = obj
|
||||||
|
|
||||||
|
# Disable EDIT mode
|
||||||
|
context.view_layer.objects.active = obj
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def import_geometry(self, context, name, node_data, global_data):
|
||||||
|
if len(node_data["geometries"]) > 1:
|
||||||
|
self.report(
|
||||||
|
{'WARNING'},
|
||||||
|
"More than one geometry for node '{}'.".format(name)
|
||||||
|
)
|
||||||
|
|
||||||
|
geometry_name = node_data["geometries"][0]
|
||||||
|
geo_data = global_data["geometries"][geometry_name]
|
||||||
|
|
||||||
|
mesh = create_mesh(self, context, name, node_data, geo_data)
|
||||||
|
obj = bpy.data.objects.new(mesh.name, mesh)
|
||||||
obj.matrix_world = Matrix()
|
obj.matrix_world = Matrix()
|
||||||
|
|
||||||
if "local" in node_data:
|
if "local" in node_data:
|
||||||
mat = matrix_from_list(node_data["local"])
|
mat = matrix_from_list(node_data["local"])
|
||||||
obj.matrix_local = mat
|
obj.matrix_local = mat
|
||||||
|
@ -295,8 +472,64 @@ def import_node(self, context, name, node_data, global_data):
|
||||||
for child_name, child_data in node_data["children"].items():
|
for child_name, child_data in node_data["children"].items():
|
||||||
if child_data["parent"] != name:
|
if child_data["parent"] != name:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Assigned parent '%s' doesn't match actual parent node '%s'"
|
"Assigned parent '{}' doesn't match actual parent node '{}'"
|
||||||
% (child_data["parent"], name)
|
.format(child_data["parent"], name)
|
||||||
|
)
|
||||||
|
|
||||||
|
child_obj = import_node(
|
||||||
|
self,
|
||||||
|
context,
|
||||||
|
child_name,
|
||||||
|
child_data,
|
||||||
|
global_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(child_obj, bpy.types.Object):
|
||||||
|
raise RuntimeError(
|
||||||
|
"Node of type '{}' cannot be child of a geometry node."
|
||||||
|
.format(type(child_obj))
|
||||||
|
)
|
||||||
|
|
||||||
|
child_obj.parent = obj
|
||||||
|
|
||||||
|
# Make sure all objects are linked to the current collection.
|
||||||
|
# Otherwise they won't show up in the outliner.
|
||||||
|
collection = context.collection
|
||||||
|
collection.objects.link(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def import_node(self, context, name, node_data, global_data):
|
||||||
|
"""Import a BSI node. Recurses into child nodes."""
|
||||||
|
has_geometry = "geometries" in node_data
|
||||||
|
is_joint = name in global_data["joint_index"]
|
||||||
|
|
||||||
|
if is_joint:
|
||||||
|
if has_geometry:
|
||||||
|
raise RuntimeError("Found geometry data in joint '{}'".format(name))
|
||||||
|
|
||||||
|
if not name.startswith("j_"):
|
||||||
|
raise RuntimeError("Invalid name for joint: '{}".format(name))
|
||||||
|
|
||||||
|
self.report({'INFO'}, "Creating Armature '{}'".format(name))
|
||||||
|
return import_armature(self, context, name, node_data, global_data)
|
||||||
|
|
||||||
|
if has_geometry:
|
||||||
|
return import_geometry(self, context, name, node_data, global_data)
|
||||||
|
else:
|
||||||
|
if name != "root_point":
|
||||||
|
self.report({'WARNING'}, "Unknown kind of node: '{}'. Falling back to Empty.".format(name))
|
||||||
|
|
||||||
|
obj = bpy.data.objects.new(name, None)
|
||||||
|
# Decrease axis size to prevent overcrowding in the viewport
|
||||||
|
obj.empty_display_size = 0.1
|
||||||
|
|
||||||
|
if "children" in node_data:
|
||||||
|
for child_name, child_data in node_data["children"].items():
|
||||||
|
if child_data["parent"] != name:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Assigned parent '{}' doesn't match actual parent node '{}'"
|
||||||
|
.format(child_data["parent"], name)
|
||||||
)
|
)
|
||||||
|
|
||||||
child_obj = import_node(
|
child_obj = import_node(
|
||||||
|
@ -312,27 +545,34 @@ def import_node(self, context, name, node_data, global_data):
|
||||||
# Otherwise they won't show up in the outliner.
|
# Otherwise they won't show up in the outliner.
|
||||||
collection = context.collection
|
collection = context.collection
|
||||||
collection.objects.link(obj)
|
collection.objects.link(obj)
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def load(self, context, filepath, *, relpath=None):
|
def load(self, context, filepath, *, relpath=None):
|
||||||
global_data = parse_sjson(filepath)
|
global_data = parse_sjson(filepath)
|
||||||
|
|
||||||
print(global_data)
|
|
||||||
|
|
||||||
# Nothing to do if there are no nodes
|
# Nothing to do if there are no nodes
|
||||||
if "nodes" not in global_data:
|
if "nodes" not in global_data:
|
||||||
self.report({'WARNING'}, "No nodes to import in {}".format(filepath))
|
self.report({'WARNING'}, "No nodes to import in {}".format(filepath))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
view_layer = context.view_layer
|
# Build joint index
|
||||||
|
joint_index = []
|
||||||
|
if "skins" in global_data:
|
||||||
|
for v in global_data["skins"].values():
|
||||||
|
for joint in v["joints"]:
|
||||||
|
name = joint["name"]
|
||||||
|
if name in global_data["geometries"]:
|
||||||
|
self.report({'ERROR'}, "Found joint with mesh data.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
if name not in joint_index:
|
||||||
|
joint_index.append(joint['name'])
|
||||||
|
|
||||||
|
global_data["joint_index"] = joint_index
|
||||||
|
|
||||||
for name, node_data in global_data["nodes"].items():
|
for name, node_data in global_data["nodes"].items():
|
||||||
obj = import_node(self, context, name, node_data, global_data)
|
import_node(self, context, name, node_data, global_data)
|
||||||
|
|
||||||
obj.select_set(True)
|
|
||||||
# One of the objects should be set as active.
|
|
||||||
# This is the easiest to implement and perfectly fine.
|
|
||||||
view_layer.objects.active = obj
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
Loading…
Add table
Reference in a new issue