WIP: X3D HAnim with Blender bones (no animation yet present) #23

Draft
John W Carlson wants to merge 9 commits from yottzumm/io_scene_x3d:main into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.

View File

@ -18,6 +18,7 @@ material_cache = {}
conversion_scale = 1.0 conversion_scale = 1.0
EPSILON = 0.0000001 # Very crude. EPSILON = 0.0000001 # Very crude.
TIME_MULTIPLIER = 250
Bujus_Krachus marked this conversation as resolved Outdated

Ideally be a tad more concise, maybe something along the lines like TIME_MULTIPLIER

Ideally be a tad more concise, maybe something along the lines like `TIME_MULTIPLIER`
def imageConvertCompat(path): def imageConvertCompat(path):
@ -359,6 +360,9 @@ class vrmlNode(object):
'node_type', 'node_type',
'parent', 'parent',
'children', 'children',
'skeleton', # TODO no joints, segments or sites yet.

Usually todos follow this pattern: TODO: i want to be awesome!

Usually todos follow this pattern: `TODO: i want to be awesome!`
'skinCoord', # this is intentionally NOT skin_coord, but I don't know if HAnim should be here at all

snake_case: skin_coord

snake_case: `skin_coord`
'skin',
'parent', 'parent',
'array_data', 'array_data',
'reference', 'reference',
@ -1594,8 +1598,11 @@ def translateTexTransform(node, ancestry):
def getFinalMatrix(node, mtx, ancestry, global_matrix): def getFinalMatrix(node, mtx, ancestry, global_matrix):
transform_nodes = [node_tx for node_tx in ancestry if node_tx.getSpec() == 'Transform'] transform_nodes = [node_tx for node_tx in ancestry if node_tx.getSpec() in ('Transform', 'HAnimHumanoid', 'HAnimJoint', 'HAnimSite', 'HAnimDisplacer')]
if node.getSpec() == 'Transform': if node.getSpec() in ('Transform', 'HAnimHumanoid', 'HAnimJoint', 'HAnimSite', 'HAnimDisplacer'):
# This comment is here so I can quick replace the above in testing
Bujus_Krachus marked this conversation as resolved Outdated

dead code, either remove the commented out code pieces or explain why it's currently not used

dead code, either remove the commented out code pieces or explain why it's currently not used
#transform_nodes = [node_tx for node_tx in ancestry if node_tx.getSpec() == 'Transform']
#if node.getSpec() == 'Transform':
transform_nodes.append(node) transform_nodes.append(node)
transform_nodes.reverse() transform_nodes.reverse()
@ -1639,6 +1646,16 @@ def set_new_float_color_attribute(bpymesh, color_data, name: str = "ColorPerCorn
bpymesh.color_attributes.new(name, 'FLOAT_COLOR', 'CORNER') bpymesh.color_attributes.new(name, 'FLOAT_COLOR', 'CORNER')
bpymesh.color_attributes[name].data.foreach_set("color", color_data) bpymesh.color_attributes[name].data.foreach_set("color", color_data)
# TODO not tested
def set_new_float_color_attribute_curve(bpycurve, color_data, name: str = "ColorPerCorner", convert_to_linear: bool = True):
if (convert_to_linear):
# convert color spaces to account for api changes from legacy to newer api
color_data = [srgb_to_linear(col_val) for col_val in color_data]
mat = bpy.data.materials.new(name="ColorMaterial")
mat.color = (1, 0, 0, 1)
curve_obj.data.materials.append(mat)
# Assumes that the mesh has polygons. # Assumes that the mesh has polygons.
def importMesh_ApplyColors(bpymesh, geom, ancestry): def importMesh_ApplyColors(bpymesh, geom, ancestry):
colors = geom.getChildBySpec(['ColorRGBA', 'Color']) colors = geom.getChildBySpec(['ColorRGBA', 'Color'])
@ -1907,6 +1924,80 @@ def importMesh_TriangleFanSet(geom, ancestry):
bpymesh.polygons.foreach_set("vertices", [x for x in triangles()]) bpymesh.polygons.foreach_set("vertices", [x for x in triangles()])
return importMesh_FinalizeTriangleMesh(bpymesh, geom, ancestry) return importMesh_FinalizeTriangleMesh(bpymesh, geom, ancestry)
# TODO, not for this release
def processColors_IndexedLineSet(geom, ancestry, bpycurve, lines):
colors = geom.getChildBySpec(['ColorRGBA', 'Color'])
index = geom.getFieldAsArray('coordIndex', 0, ancestry)
if colors:
cco = []
if colors.getSpec() == 'ColorRGBA':
rgb = colors.getFieldAsArray('color', 4, ancestry)
else:
# Array of arrays; no need to flatten
rgb = [c + [1.0] for c in colors.getFieldAsArray('color', 3, ancestry)]
color_per_vertex = geom.getFieldAsBool('colorPerVertex', True, ancestry)
color_index = geom.getFieldAsArray('colorIndex', 0, ancestry)
has_color_index = len(color_index) != 0
has_valid_color_index = index.count(-1) == color_index.count(-1)
# rebuild a corrupted colorIndex field (assuming the end of face markers -1 are missing)
if has_color_index and not has_valid_color_index:
# remove all -1 beforehand to ensure clean working copy
color_index = [x for x in color_index if x != -1]
# copy all -1 from coordIndex to colorIndex
for i, v in enumerate(index):
if v == -1:
color_index.insert(i, -1)
if color_per_vertex and has_color_index: # Color per vertex with index
cco = [cco for f in processPerVertexIndex(color_index)
for v in f
for cco in rgb[v]]
elif color_per_vertex: # Color per vertex without index
# use vertex value by default, however if lengths mismatch use the positional value to access rgb value
# ain't ideal by far, but should most likely work
try:
cco = [cco for f in lines
for v in f
for cco in rgb[v]]
except IndexError:
print("reattempting reading color_per_vertex without index by using positional value because vertex value failed")
cco = [cco for f in lines
for (i, v) in enumerate(f)
for cco in rgb[i]]
elif color_index: # Color per face with index
cco = [cco for (i, f) in enumerate(lines)
for j in f
for cco in rgb[color_index[i]]]
elif len(lines) > len(rgb): # Static color per face without index, when all lines have the same color.
# Exported from SOLIDWORKS, see: `blender/blender-addons#105398`.
cco = [cco for (i, f) in enumerate(lines)
for j in f
for cco in rgb[0]]
else: # Color per face without index
cco = [cco for (i, f) in enumerate(lines)
for j in f
for cco in rgb[i]]
for i, spline in enumerate(bpycurve.splines):
# Example: Color based on spline index
# TODO do RGBA
if cco is not None:
color = cco[i * 3:(i+1) * 3]
color.append(1) # TODO includ transporency
else:
color = (1, 0, 0, 1)
print(f"Color = {color}")
# Apply color to the spline directly
spline.material_index = i # Assign a material index (optional)
# Create a material for the spline
if len(bpy.data.materials) <= i:
mat = bpy.data.materials.new(name="ColorMaterial_" + str(i))
mat.diffuse_color = color
bpycurve.materials.append(mat)
def importMesh_IndexedFaceSet(geom, ancestry): def importMesh_IndexedFaceSet(geom, ancestry):
# Saw the following structure in X3Ds: the first mesh has a huge set # Saw the following structure in X3Ds: the first mesh has a huge set
@ -2503,6 +2594,8 @@ def importMesh_IndexedLineSet(geom, ancestry):
nu.points.add(len(line) - 1) # the new nu has 1 point to begin with nu.points.add(len(line) - 1) # the new nu has 1 point to begin with
for il, pt in zip(line, nu.points): for il, pt in zip(line, nu.points):
pt.co[0:3] = points[il] pt.co[0:3] = points[il]
# TODO
# processColors_IndexedLineSet(geom, ancestry, bpycurve, lines)
return bpycurve return bpycurve
@ -2850,6 +2943,8 @@ def appearance_LoadImageTextureFile(ima_urls, node):
bpyima = None bpyima = None
for f in ima_urls: for f in ima_urls:
dirname = os.path.dirname(node.getFilename()) dirname = os.path.dirname(node.getFilename())
if f.startswith('"'):
f = f[1:-1] # strip quotes (I want to strip both quotes, front and tail. I am not sure if this works)
bpyima = image_utils.load_image(f, dirname, bpyima = image_utils.load_image(f, dirname,
place_holder=False, place_holder=False,
recursive=False, recursive=False,
@ -3163,7 +3258,7 @@ def importShape_ProcessObject(
# Can transform data or object, better the object so we can instance # Can transform data or object, better the object so we can instance
# the data # the data
# bpymesh.transform(getFinalMatrix(node)) # bpymesh.transform(getFinalMatrix(node))
bpyob = node.blendObject = bpy.data.objects.new(vrmlname, bpydata) bpyob = node.blendData = node.blendObject = bpy.data.objects.new(vrmlname, bpydata)
bpyob.matrix_world = getFinalMatrix(node, None, ancestry, global_matrix) bpyob.matrix_world = getFinalMatrix(node, None, ancestry, global_matrix)
bpycollection.objects.link(bpyob) bpycollection.objects.link(bpyob)
bpyob.select_set(True) bpyob.select_set(True)
@ -3171,6 +3266,8 @@ def importShape_ProcessObject(
if DEBUG: if DEBUG:
bpyob["source_line_no"] = geom.lineno bpyob["source_line_no"] = geom.lineno
return bpyob
def importText(geom, ancestry): def importText(geom, ancestry):
fmt = geom.getChildBySpec('FontStyle') fmt = geom.getChildBySpec('FontStyle')
@ -3253,6 +3350,7 @@ def importShape(bpycollection, node, ancestry, global_matrix):
bpydata = None bpydata = None
geom_spec = geom.getSpec() geom_spec = geom.getSpec()
coord = geom.getChildBySpec('Coordinate')
# ccw is handled by every geometry importer separately; some # ccw is handled by every geometry importer separately; some
# geometries are easier to flip than others # geometries are easier to flip than others
@ -3266,14 +3364,127 @@ def importShape(bpycollection, node, ancestry, global_matrix):
# There are no geometry importers that can legally return # There are no geometry importers that can legally return
# no object. It's either a bpy object, or an exception # no object. It's either a bpy object, or an exception
importShape_ProcessObject( bpypo = importShape_ProcessObject(
bpycollection, vrmlname, bpydata, geom, geom_spec, bpycollection, vrmlname, bpydata, geom, geom_spec,
node, bpymat, tex_has_alpha, texmtx, node, bpymat, tex_has_alpha, texmtx,
ancestry, global_matrix) ancestry, global_matrix)
if bpypo is None:
print('ImportX3D warning: importShape_ProcessObject did not return a shape to return for HAnim "%s"' % vrmlname)
else: else:
print('\tImportX3D warning: unsupported type "%s"' % geom_spec) print('\tImportX3D warning: unsupported type "%s"' % geom_spec)
bpypo = None
return [ geom, bpypo, coord ]
def importHAnimHumanoid(bpycollection, node, ancestry, global_matrix, joints, segments, jointSkin):
Bujus_Krachus marked this conversation as resolved Outdated

dead code

dead code
vrmlname = node.getDefName()
# print(vrmlname)
prefix = ''
if vrmlname:
first_underscore = vrmlname.find('_')
if first_underscore > 0:
prefix = vrmlname[:first_underscore+1]
else:
vrmlname = 'HAnimHumanoid'
Bujus_Krachus marked this conversation as resolved
Review

like above could get simplified using or

like above could get simplified using `or`
# Create armature and object
armature_data = bpy.data.armatures.new(prefix+"humanoid_root")
skeleton = bpy.data.objects.new(vrmlname, armature_data)
skeleton.matrix_world = getFinalMatrix(node, None, ancestry, global_matrix)
# Link object to collection and make it active
bpycollection.objects.link(skeleton)
bpy.context.view_layer.objects.active = skeleton
skeleton.select_set(True)
# Enter edit mode
Bujus_Krachus marked this conversation as resolved
Review

like above could get simplified using or

like above could get simplified using `or`
bpy.ops.object.mode_set(mode='EDIT')
# Store reference to the object on the node
bpyob = node.blendData = node.blendObject = skeleton
# Process children joints, including USE, if present
child = node.getChildBySpec('HAnimJoint') # 'HAnimJoint'
if child:
Bujus_Krachus marked this conversation as resolved
Review

better move the nl after the validation section

better move the nl after the validation section
first_joint_name = child.getDefName() or child.getFieldAsString('name', None, ancestry)
joint_center = child.getFieldAsFloatTuple('center', (0.0, 0.0, 0.0), ancestry)
if DEBUG:
Bujus_Krachus marked this conversation as resolved Outdated

is unpacking needed or just the conversion to tuple?

joints.append((child_bone_name, tuple(child_center), tuple(parent_center), skinCoordWeight, skinCoordIndex))

is unpacking needed or just the conversion to tuple? `joints.append((child_bone_name, tuple(child_center), tuple(parent_center), skinCoordWeight, skinCoordIndex))`
print(f"Joint {first_joint_name} {joint_center}")
importHAnimJoint(joints, segments, child, ancestry, first_joint_name, parent_center=joint_center[:])
# Create bones for each joint
for joint_name, joint_start, joint_end, skinCoordWeight, skinCoordIndex in joints:
if not joint_name:
joint_name = vrmlname
new_segment = armature_data.edit_bones.new(joint_name)
child.blendData = child.blendObject = new_segment
matrix_world_inv = skeleton.matrix_world.inverted()
new_segment.head = joint_end
new_segment.tail = joint_start
# if joint_name != vrmlname:
jointSkin[joint_name] = {
'skinCoordWeight' : skinCoordWeight,
'skinCoordIndex' : skinCoordIndex
}
for segment in segments:
parent_joint, child_joint = segment
if parent_joint in skeleton.data.edit_bones:
parent = skeleton.data.edit_bones[parent_joint] # some things don't have a parent
else:
parent = None
if child_joint in skeleton.data.edit_bones:
child = skeleton.data.edit_bones[child_joint]
else:
child = armature_data.edit_bones.new(child_joint)
child.parent = parent
else:
print("Couldn't find child HAnimJoint")
return skeleton
def importHAnimJoints(joints, segments, children, ancestry, parent_bone_name, parent_center=[0, 0, 0]):
for child in children:
child_bone_name = child.getDefName() or child.getFieldAsString('name', None, ancestry) or parent_bone_name
segments.append((parent_bone_name, child_bone_name))
importHAnimJoint(joints, segments, child, ancestry, parent_bone_name, parent_center)
def importHAnimJoint(joints, segments, child, ancestry, parent_bone_name=None, parent_center=[0, 0, 0]):
if child:
child_bone_name = child.getDefName()
if not child_bone_name:
child_bone_name = child.getFieldAsString('name', None, ancestry)
if not child_bone_name:
child_bone_name = 'Armature'
child_center = child.getFieldAsFloatTuple('center', None, ancestry)
skinCoordWeight = child.getFieldAsArray('skinCoordWeight', 0, ancestry)
skinCoordIndex = child.getFieldAsArray('skinCoordIndex', 0, ancestry)
# I don't understand reviewer's comment:
# "better move the nl after the validation section"
if skinCoordWeight is None:
skinCoordWeight = ()
if skinCoordIndex is None:
skinCoordIndex = ()
if not child_center:
child_center = [0, 0, 0]
joints.append((child_bone_name, tuple(child_center), tuple(parent_center), skinCoordWeight, skinCoordIndex))
# print(f"Joint IHAJ {joints[-1]}")
children = child.getChildrenBySpec('HAnimJoint')
if children:
importHAnimJoints(joints, segments, children, ancestry, child_bone_name, child_center)
else:
childname = child.getFieldAsString('name', '', ancestry)
if DEBUG:
print(f"Didn't find children, {children} for {childname}")
else:
print(f"Didn't find child, {child}")
# ----------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------
# Lighting # Lighting
@ -3416,10 +3627,24 @@ def importTransform(bpycollection, node, ancestry, global_matrix):
bpyob.matrix_world = getFinalMatrix(node, None, ancestry, global_matrix) bpyob.matrix_world = getFinalMatrix(node, None, ancestry, global_matrix)
# so they are not too annoying # so the EMPTY is not too annoying
bpyob.empty_display_type = 'PLAIN_AXES' bpyob.empty_display_type = 'PLAIN_AXES'
bpyob.empty_display_size = 0.2 bpyob.empty_display_size = 0.2
def importHAnimSegment(bpycollection, node, ancestry, global_matrix):
name = node.getDefName() or node.getFieldAsString('name', None, ancestry) or 'HAnimSegment'
bpyob = node.blendData = node.blendObject = bpy.data.objects.new(name, None)
bpycollection.objects.link(bpyob)
bpyob.select_set(True)
bpyob.matrix_world = getFinalMatrix(node, None, ancestry, global_matrix)
bpyob.empty_display_type = 'PLAIN_AXES'
Bujus_Krachus marked this conversation as resolved Outdated

curoff += 3

`curoff += 3`
# so the EMPTY is not too annoying
bpyob.empty_display_size = 0.2
return bpyob
#def importTimeSensor(node): #def importTimeSensor(node):
def action_fcurve_ensure(action, data_path, array_index): def action_fcurve_ensure(action, data_path, array_index):
@ -3429,7 +3654,6 @@ def action_fcurve_ensure(action, data_path, array_index):
return action.fcurves.new(data_path=data_path, index=array_index) return action.fcurves.new(data_path=data_path, index=array_index)
def translatePositionInterpolator(node, action, ancestry): def translatePositionInterpolator(node, action, ancestry):
key = node.getFieldAsArray('key', 0, ancestry) key = node.getFieldAsArray('key', 0, ancestry)
keyValue = node.getFieldAsArray('keyValue', 3, ancestry) keyValue = node.getFieldAsArray('keyValue', 3, ancestry)
@ -3437,26 +3661,62 @@ def translatePositionInterpolator(node, action, ancestry):
loc_x = action_fcurve_ensure(action, "location", 0) loc_x = action_fcurve_ensure(action, "location", 0)
loc_y = action_fcurve_ensure(action, "location", 1) loc_y = action_fcurve_ensure(action, "location", 1)
loc_z = action_fcurve_ensure(action, "location", 2) loc_z = action_fcurve_ensure(action, "location", 2)
if DEBUG:
print (f"key {key} keyValue {keyValue} {action}")
for i, time in enumerate(key): for i, time in enumerate(key):
try: try:
x, y, z = keyValue[i] x, y, z = keyValue[i]
except: if DEBUG:
print (f"i {i} x {x} y {y} z {z}")
except: # There's 4 exception possible here, so just wildcard
continue continue
loc_x.keyframe_points.insert(time, x) loc_x.keyframe_points.insert(time*TIME_MULTIPLIER, x)
loc_y.keyframe_points.insert(time, y) loc_y.keyframe_points.insert(time*TIME_MULTIPLIER, y)
loc_z.keyframe_points.insert(time, z) loc_z.keyframe_points.insert(time*TIME_MULTIPLIER, z)
for fcu in (loc_x, loc_y, loc_z):
for kf in fcu.keyframe_points:
kf.interpolation = 'BEZIER'
Bujus_Krachus marked this conversation as resolved Outdated

could get indented further, as pose_bone is None if ``skeleton` is None

could get indented further, as `pose_bone` is None if ``skeleton` is None
def translateCoordinateInterpolator(node, action, ancestry):
key = node.getFieldAsArray('key', 0, ancestry)
keyValue = node.getFieldAsArray('keyValue', 0, ancestry)
offset = int(len(keyValue) / len(key) / 3) # values divide by times divided by axes
if DEBUG:
print(f"ci {offset} = {len(keyValue)} / {len(key)}")
loc_x = action_fcurve_ensure(action, "location", 0)
loc_y = action_fcurve_ensure(action, "location", 1)
loc_z = action_fcurve_ensure(action, "location", 2)
curoff = 0
for i, time in enumerate(key): # loop through time
# 0 1 2
for off in range(offset): # for each data point
# 0 1 2 up to offset
# curoff = i*offset+off
#print(f" coordinate index {off} num coordinates {offset} time index {i} time {time} current offset {curoff}")
# then a vec3f
x = keyValue[curoff+0]
y = keyValue[curoff+1]
z = keyValue[curoff+2]
loc_x.keyframe_points.insert(time*TIME_MULTIPLIER, x)
loc_y.keyframe_points.insert(time*TIME_MULTIPLIER, y)
loc_z.keyframe_points.insert(time*TIME_MULTIPLIER, z)
curoff += 3
for fcu in (loc_x, loc_y, loc_z): for fcu in (loc_x, loc_y, loc_z):
for kf in fcu.keyframe_points: for kf in fcu.keyframe_points:
kf.interpolation = 'LINEAR' kf.interpolation = 'LINEAR'
def translateOrientationInterpolator(node, action, ancestry, to_node):
def translateOrientationInterpolator(node, action, ancestry):
key = node.getFieldAsArray('key', 0, ancestry) key = node.getFieldAsArray('key', 0, ancestry)
keyValue = node.getFieldAsArray('keyValue', 4, ancestry) keyValue = node.getFieldAsArray('keyValue', 4, ancestry)
Bujus_Krachus marked this conversation as resolved Outdated

which errors are expected? Currently it's a wildcard, thus ideally provide the exception type

which errors are expected? Currently it's a wildcard, thus ideally provide the exception type
node.rotation_mode = 'XYZ'
rot_x = action_fcurve_ensure(action, "rotation_euler", 0) rot_x = action_fcurve_ensure(action, "rotation_euler", 0)
rot_y = action_fcurve_ensure(action, "rotation_euler", 1) rot_y = action_fcurve_ensure(action, "rotation_euler", 1)
rot_z = action_fcurve_ensure(action, "rotation_euler", 2) rot_z = action_fcurve_ensure(action, "rotation_euler", 2)
@ -3464,22 +3724,35 @@ def translateOrientationInterpolator(node, action, ancestry):
for i, time in enumerate(key): for i, time in enumerate(key):
try: try:
x, y, z, w = keyValue[i] x, y, z, w = keyValue[i]
except: except: # There's 4 exception possible here, so just wildcard
continue continue
mtx = translateRotation((x, y, z, w)) mtx = translateRotation((x, y, z, w))
eul = mtx.to_euler() eul = mtx.to_euler()
rot_x.keyframe_points.insert(time, eul.x) rot_x.keyframe_points.insert(time*TIME_MULTIPLIER, eul.x)
rot_y.keyframe_points.insert(time, eul.y) rot_y.keyframe_points.insert(time*TIME_MULTIPLIER, eul.y)
rot_z.keyframe_points.insert(time, eul.z) rot_z.keyframe_points.insert(time*TIME_MULTIPLIER, eul.z)
for fcu in (rot_x, rot_y, rot_z): for fcu in (rot_x, rot_y, rot_z):
for kf in fcu.keyframe_points: for kf in fcu.keyframe_points:
kf.interpolation = 'LINEAR' kf.interpolation = 'BEZIER'
def translateBoneOrientationInterpolator(node, action, ancestry, to_id=None, skeleton=None):
key = node.getFieldAsArray('key', 0, ancestry)
keyValue = node.getFieldAsArray('keyValue', 4, ancestry)
pose_bone = None
if skeleton:
pose_bone = skeleton.pose.bones.get(to_id)
if pose_bone:
pose_bone.rotation_mode = 'AXIS_ANGLE'
for time, (x, y, z, w) in zip(key, keyValue):
pose_bone.rotation_axis_angle = (w, x, y, z)
pose_bone.keyframe_insert(data_path="rotation_axis_angle", frame=time * TIME_MULTIPLIER)
# Untested! # Untested!
def translateScalarInterpolator(node, action, ancestry): def translateScaleInterpolator(node, action, ancestry):
key = node.getFieldAsArray('key', 0, ancestry) key = node.getFieldAsArray('key', 0, ancestry)
keyValue = node.getFieldAsArray('keyValue', 4, ancestry) keyValue = node.getFieldAsArray('keyValue', 4, ancestry)
@ -3490,13 +3763,26 @@ def translateScalarInterpolator(node, action, ancestry):
for i, time in enumerate(key): for i, time in enumerate(key):
try: try:
x, y, z = keyValue[i] x, y, z = keyValue[i]
except: except: # There's 4 exception possible here, so just wildcard
continue continue
sca_x.keyframe_points.new(time, x) sca_x.keyframe_points.insert(time*TIME_MULTIPLIER, x)
sca_y.keyframe_points.new(time, y) sca_y.keyframe_points.insert(time*TIME_MULTIPLIER, y)
Bujus_Krachus marked this conversation as resolved Outdated

print needed?

print needed?
sca_z.keyframe_points.new(time, z) sca_z.keyframe_points.insert(time*TIME_MULTIPLIER, z)
def translateScalarInterpolator(node, action, ancestry, to_node, data_path):
key = node.getFieldAsArray('key', 0, ancestry)
keyValue = node.getFieldAsArray('keyValue', 0, ancestry)
scalar = action_fcurve_ensure(action, data_path, 0)
for i, time in enumerate(key):
try:
s = keyValue[i]
except: # There's 4 exception possible here, so just wildcard
continue
scalar.keyframe_points.insert(time*TIME_MULTIPLIER, s)
def translateTimeSensor(node, action, ancestry): def translateTimeSensor(node, action, ancestry):
""" """
@ -3527,14 +3813,7 @@ def translateTimeSensor(node, action, ancestry):
if loop: if loop:
time_cu.extend = Blender.IpoCurve.ExtendTypes.CYCLIC # or - EXTRAP, CYCLIC_EXTRAP, CONST, time_cu.extend = Blender.IpoCurve.ExtendTypes.CYCLIC # or - EXTRAP, CYCLIC_EXTRAP, CONST,
def importRouteFromTo(node, from_id, from_type, to_id, to_type, ancestry, skeleton, hasMesh):
def importRoute(node, ancestry):
"""
Animation route only at the moment
"""
if not hasattr(node, 'fields'):
return
routeIpoDict = node.getRouteIpoDict() routeIpoDict = node.getRouteIpoDict()
@ -3542,11 +3821,63 @@ def importRoute(node, ancestry):
try: try:
action = routeIpoDict[act_id] action = routeIpoDict[act_id]
except: except:
action = routeIpoDict[act_id] = bpy.data.actions.new('web3d_ipo') action = routeIpoDict[act_id] = bpy.data.actions.new(act_id)
#print(f"return action {act_id} {action}")
return action return action
# for getting definitions # for getting definitions
defDict = node.getDefDict() defDict = node.getDefDict()
if from_type == 'value_changed':
if to_type in ('set_translation', 'set_position'): # set translation may need some matrix multiplication
action = getIpo(to_id)
set_data_from_node = defDict[from_id]
if DEBUG:
print(f"Trying to create a position interpolator for something from {from_id} to {to_id} (may need something special?)")
translatePositionInterpolator(set_data_from_node, action, ancestry)
if to_type in {'rotation', "set_rotation"} and defDict[to_id].getSpec() == 'TextureTransform':
action = getIpo(to_id)
set_data_from_node = defDict[from_id]
to_node = defDict[to_id]
Bujus_Krachus marked this conversation as resolved
Review

any specific error to be expected?

any specific error to be expected?
translateScalarInterpolator(set_data_from_node, action, ancestry, to_node, "rotation")
elif to_type in {'set_orientation', 'rotation', "set_rotation"}:
action = getIpo(to_id)
set_data_from_node = defDict[from_id]
to_node = defDict[to_id]
if skeleton and skeleton.pose.bones.get(to_id):
Bujus_Krachus marked this conversation as resolved Outdated

could be simplified using or

could be simplified using `or`
# print(f"Creating animation for joint {to_id}")
translateBoneOrientationInterpolator(set_data_from_node, action, ancestry, to_id, skeleton)
if not hasMesh:
Bujus_Krachus marked this conversation as resolved Outdated

print needed?

print needed?
# print(f"Creating orientation animation for {to_id}")
Bujus_Krachus marked this conversation as resolved Outdated

wi?

wi?
translateOrientationInterpolator(set_data_from_node, action, ancestry, to_node)
if to_type == 'set_scale':
action = getIpo(to_id)
set_data_from_node = defDict[from_id]
translateScaleInterpolator(set_data_from_node, action, ancestry)
if to_type == 'set_point':
action = getIpo(to_id)
set_data_from_node = defDict[from_id]
translateCoordinateInterpolator(set_data_from_node, action, ancestry)
elif from_type == 'bindTime':
action = getIpo(from_id)
time_node = defDict[to_id]
translateTimeSensor(time_node, action, ancestry)
def importRoute(node, ancestry, skeleton=None, hasMesh=None):
"""
Animation route only at the moment
"""
if node.getFieldAsString("fromNode", None, ancestry) and node.getFieldAsString("toNode", None, ancestry) and node.getFieldAsString("fromField", None, ancestry) and node.getFieldAsString("toField", None, ancestry):
pass
elif not hasattr(node, 'fields'):
# print(f"return not hasattr fields")
return
""" """
Handles routing nodes to each other Handles routing nodes to each other
@ -3557,50 +3888,36 @@ ROUTE vpTs.fraction_changed TO vpOI.set_fraction
ROUTE champFly001.bindTime TO vpTs.set_startTime ROUTE champFly001.bindTime TO vpTs.set_startTime
""" """
#from_id, from_type = node.id[1].split('.') if len(node.fields) <= 0:
#to_id, to_type = node.id[3].split('.') from_id = node.getFieldAsString("fromNode", None, ancestry)
from_type = node.getFieldAsString("fromField", None, ancestry)
#value_changed to_id = node.getFieldAsString("toNode", None, ancestry)
set_position_node = None to_type = node.getFieldAsString("toField", None, ancestry)
set_orientation_node = None if from_id and from_type and to_id and to_type:
time_node = None # print(f"ROUTE from {from_id}.{from_type} to {to_id}.{to_type}")
importRouteFromTo(node, from_id, from_type, to_id, to_type, ancestry, skeleton, hasMesh)
else:
for field in node.fields: for field in node.fields:
# print(f"return field {field}")
if field and field[0] == 'ROUTE': if field and field[0] == 'ROUTE':
try:
from_id, from_type = field[1].split('.') from_id, from_type = field[1].split('.')
to_id, to_type = field[3].split('.') to_id, to_type = field[3].split('.')
except: # print(f"ROUTE from {from_id}.{from_type} to {to_id}.{to_type}")
print("Warning, invalid ROUTE", field) importRouteFromTo(node, from_id, from_type, to_id, to_type, ancestry, skeleton, hasMesh)
continue
if from_type == 'value_changed':
if to_type == 'set_position':
action = getIpo(to_id)
set_data_from_node = defDict[from_id]
translatePositionInterpolator(set_data_from_node, action, ancestry)
if to_type in {'set_orientation', 'rotation'}:
action = getIpo(to_id)
set_data_from_node = defDict[from_id]
translateOrientationInterpolator(set_data_from_node, action, ancestry)
if to_type == 'set_scale':
action = getIpo(to_id)
set_data_from_node = defDict[from_id]
translateScalarInterpolator(set_data_from_node, action, ancestry)
elif from_type == 'bindTime':
action = getIpo(from_id)
time_node = defDict[to_id]
translateTimeSensor(time_node, action, ancestry)
def importSkinWeights(obj, joint, jointCoord, end):
group = obj.vertex_groups.get(joint) or obj.vertex_groups.new(name=joint)
# print(f"Created group {joint}")
# print(f"Index {end} joint {joint}")
for weight_index in range(len(jointCoord['skinCoordIndex'])):
# print(f"Index {end} joint {joint} {jointCoord['skinCoordIndex'][weight_index]} weight {jointCoord['skinCoordWeight'][weight_index]}")
group.add([jointCoord['skinCoordIndex'][weight_index]], jointCoord['skinCoordWeight'][weight_index], 'REPLACE')
def load_web3d( def load_web3d(
bpycontext, bpycontext,
filepath, filepath,
*, *,
PREF_FLAT=False, PREF_FLAT=False, # So Tranforms will be imported
PREF_CIRCLE_DIV=16, PREF_CIRCLE_DIV=16,
file_unit='M', file_unit='M',
global_scale=1.0, global_scale=1.0,
@ -3637,6 +3954,33 @@ def load_web3d(
# fill with tuples - (node, [parents-parent, parent]) # fill with tuples - (node, [parents-parent, parent])
all_nodes = root_node.getSerialized([], []) all_nodes = root_node.getSerialized([], [])
Bujus_Krachus marked this conversation as resolved Outdated

dead code & print needed?

dead code & print needed?
all_shapes = []
skeleton = None
Bujus_Krachus marked this conversation as resolved Outdated

print needed?

print needed?
meshobj = None
shape = None
site = None
displacers = {}
skinCoord = None
hAnimJoint = None
hAnimSegment = None
hAnimSite = None
group = None
# collect shapes for sites
for node, ancestry in all_nodes:
Bujus_Krachus marked this conversation as resolved
Review

dead code

dead code
spec = node.getSpec()
if spec.endswith('Shape'):
shape = importShape(bpycollection, node, ancestry, global_matrix)
if shape:
if shape[1]:
bpy.context.view_layer.objects.active = shape[1]
shape.append(site)
site = None
all_shapes.append(shape)
Bujus_Krachus marked this conversation as resolved
Review

dead code & print needed above?

dead code & print needed above?
elif spec.endswith('HAnimSite'):
site = node
for node, ancestry in all_nodes: for node, ancestry in all_nodes:
Bujus_Krachus marked this conversation as resolved Outdated

more readable and a extra unnecessary check gets saved:

if meshobj:
    if child_joint not in imported:
         ...
    if parent_joint not in imported:
        ...
more readable and a extra unnecessary check gets saved: ```python if meshobj: if child_joint not in imported: ... if parent_joint not in imported: ... ```
#if 'castle.wrl' not in node.getFilename(): #if 'castle.wrl' not in node.getFilename():
# continue # continue
@ -3652,12 +3996,82 @@ def load_web3d(
# Note, include this function so the VRML/X3D importer can be extended # Note, include this function so the VRML/X3D importer can be extended
# by an external script. - gets first pick # by an external script. - gets first pick
pass pass
if spec == 'Shape': if spec in {'PointLight', 'DirectionalLight', 'SpotLight'}:
Bujus_Krachus marked this conversation as resolved Outdated

dead code

dead code
importShape(bpycollection, node, ancestry, global_matrix)
elif spec in {'PointLight', 'DirectionalLight', 'SpotLight'}:
importLamp(bpycollection, node, spec, ancestry, global_matrix) importLamp(bpycollection, node, spec, ancestry, global_matrix)
elif spec == 'Viewpoint': elif spec == 'Viewpoint':
importViewpoint(bpycollection, node, ancestry, global_matrix) importViewpoint(bpycollection, node, ancestry, global_matrix)
elif spec == 'HAnimHumanoid':
joints = []
segments = []
jointSkin = {}
skeleton = importHAnimHumanoid(bpycollection, node, ancestry, global_matrix, joints, segments, jointSkin)
skinCoord = node.getChildBySpec('Coordinate')
if skinCoord:
#if skinCoord.getFieldAsString("containerField", None, ancestry) == "skinCoord":
if DEBUG:
print(f"Skin coord is {skinCoord}")
for shape in all_shapes:
if shape:
if DEBUG:
print(f"Skin mesh is found")
if shape[0] and shape[1] and shape[2] and skinCoord.getRealNode().getDefName() == shape[2].getRealNode().getDefName():
if DEBUG:
print("Got mesh obj")
meshobj = shape[1]
meshobj.modifiers.new(name='ArmatureToMesh', type='ARMATURE')
meshobj.modifiers['ArmatureToMesh'].object = skeleton
else:
print(f"DEFs match? missing shape[:]? skinCoord.getRealNode().getDefName() == shape[2].getRealNode().getDefName()")
else:
print(f"no shape {shape} ? all shapes is {all_shapes}")
else:
print("No skinCoord, no skin weights, no skin animation")
if DEBUG:
print(f"mesh is {meshobj}")
#bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.object.mode_set(mode="OBJECT")
imported = []
if DEBUG:
print(f"Number of segments {len(segments)}")
for segment in segments:
parent_joint, child_joint = segment
# print(f"Segment {parent_joint} {child_joint} loading weights")
if meshobj:
if child_joint not in imported:
importSkinWeights(meshobj, child_joint, jointSkin[child_joint], "child")
imported.append(child_joint)
if parent_joint not in imported:
importSkinWeights(meshobj, parent_joint, jointSkin[parent_joint], "parent")
imported.append(parent_joint)
elif spec in ('HAnimSegment'):
child_segment_name = node.getDefName()
Bujus_Krachus marked this conversation as resolved Outdated

second check of skeleton is not needed, as it's above already

second check of skeleton is not needed, as it's above already
hAnimSegment = importHAnimSegment(bpycollection, node, ancestry, global_matrix)
attachMesh(all_shapes, child_segment_name, hAnimSegment) # mesh is in all_shapes
hAnimSegment.parent = skeleton
hAnimSegment.parent_bone = parent_joint_name
hAnimSegment.parent_type = 'BONE'
elif spec in ('HAnimDisplacer'):
# TODO Intended to be implemented
#if meshobj:
#importHAnimDisplacer(node, ancestry, meshobj, displacers)
pass
elif spec in ('HAnimHumanoid'):
humanoid_name = node.getDefName()
Bujus_Krachus marked this conversation as resolved Outdated

dead code

dead code
hAnimHumanoid = importTransform(bpycollection, node, ancestry, global_matrix)
elif spec in ('HAnimJoint'):
parent_joint_name = node.getDefName()
hAnimJoint = importTransform(bpycollection, node, ancestry, global_matrix)
elif spec in ('Group'):
group_name = node.getDefName()
group = importTransform(bpycollection, node, ancestry, global_matrix)
elif spec in ('HAnimSite'):
site_name = node.getDefName()
hAnimSite = importTransform(bpycollection, node, ancestry, global_matrix)
attachMesh(all_shapes, site_name, hAnimSite) # mesh is in all_shapes
elif spec == 'Transform': elif spec == 'Transform':
# Only use transform nodes when we are not importing a flat object hierarchy # Only use transform nodes when we are not importing a flat object hierarchy
if PREF_FLAT == False: if PREF_FLAT == False:
@ -3666,13 +4080,20 @@ def load_web3d(
# These are delt with later within importRoute # These are delt with later within importRoute
elif spec=='PositionInterpolator': elif spec=='PositionInterpolator':
action = bpy.data.ipos.new('web3d_ipo', 'Object') action = bpy.data.ipos.new('web3d_ipo', 'Object')
translatePositionInterpolator(node, action) translatePositionInterpolator(node, action, ancestry)
''' '''
# After we import all nodes, route events - anim paths # After we import all nodes, route events - anim paths
if skeleton:
bpy.ops.object.mode_set(mode='POSE')
for node, ancestry in all_nodes: for node, ancestry in all_nodes:
importRoute(node, ancestry) importRoute(node, ancestry, skeleton, meshobj)
bpy.context.scene.frame_set(0)
if not skeleton and shape is not None and shape[1]:
bpy.context.view_layer.objects.active = shape[1]
bpy.ops.object.mode_set(mode='OBJECT')
for node, ancestry in all_nodes: for node, ancestry in all_nodes:
if node.isRoot(): if node.isRoot():
# we know that all nodes referenced from will be in # we know that all nodes referenced from will be in
@ -3684,16 +4105,42 @@ def load_web3d(
# Assign anim curves # Assign anim curves
node = defDict[key] node = defDict[key]
# print(f"key {key} action {action} node {node}")
bone = None
if skeleton:
if key in skeleton.pose.bones:
bone = skeleton.pose.bones[key]
else:
print(f"There's no pose bone associated with key {key}, probably using a regular interpolator")
else:
print(f"There's no skeleton")
if node.blendData is None: # Add an object if we need one for animation if node.blendData is None: # Add an object if we need one for animation
bpyob = node.blendData = node.blendObject = bpy.data.objects.new('AnimOb', None) # , name) # print(f"Adding some blendData to the node for {key}. Did you forget to add it?")
bpycollection.objects.link(bpyob) node.blendData = node.blendObject = bpy.data.objects.new(key, None)
bpyob.select_set(True) bpycollection.objects.link(node.blendObject)
node.blendObject.select_set(True)
if node.blendData.animation_data is None: if hasattr(node.blendData, "animation_data"):
if not node.blendData.animation_data:
#print(f"Adding animation data for {node.blendData.name} 2 ")
node.blendData.animation_data_create() node.blendData.animation_data_create()
else:
#print(f"Node {node.blendData.name} has animation_data")
pass
if not node.blendData.animation_data.action:
#print(f"Setting an action {node.blendData.name}")
node.blendData.animation_data.action = action node.blendData.animation_data.action = action
else:
# print(f"Node {node.blendData.name} has actionnode. {node.blendData.animation_data.action}")
pass
# to disable NLA, comment out these 3 lines
# print(f"Adding an nla_track to {node.blendData.name} {key}")
track = node.blendData.animation_data.nla_tracks.new()
track.name = "NLATRACK "+key
node.blendData.animation_data.nla_tracks[track.name].strips.new(name=key, start=0, action=bpy.data.actions[key])
else:
# print(f"Node {node.blendData.name} has blendData, but no animation_data")
pass
# Add in hierarchy # Add in hierarchy
if PREF_FLAT is False: if PREF_FLAT is False:
child_dict = {} child_dict = {}
@ -3721,13 +4168,36 @@ def load_web3d(
# Parent # Parent
for parent, children in child_dict.items(): for parent, children in child_dict.items():
if parent and children:
for c in children: for c in children:
if c:
if type(c) == type(parent):
Bujus_Krachus marked this conversation as resolved Outdated

what's the effect, why is this change needed?

what's the effect, why is this change needed?
c.parent = parent c.parent = parent
else:
if isinstance(c, bpy.types.EditBone):
if DEBUG:
print(f"Child is EditBone")
if isinstance(parent, bpy.types.EditBone):
if DEBUG:
print(f"Parent is EditBone")
c.parent = skeleton # Armature object
print(f"Can't handle parent-child relationship, child {c} type {type(c)}, parent {parent} type {type(parent)}")
else:
print("Not a child")
else:
print("Children or parent may be None")
# update deps # update deps
bpycontext.view_layer.update() bpycontext.view_layer.update()
del child_dict del child_dict
def attachMesh(all_shapes, parent_name, parent_obj):
# print(f"Found parent {parent_name}")
for shape in all_shapes:
if shape and shape[0] and shape[1] and shape[2] and shape[3]:
if shape[3].getDefName() and parent_name == shape[3].getDefName(): # shape[3] is shape's parent node
meshobj = shape[1]
meshobj.parent = parent_obj
def load_with_profiler( def load_with_profiler(
context, context,
@ -3756,7 +4226,7 @@ def load(context,
# loadWithProfiler(operator, context, filepath, global_matrix) # loadWithProfiler(operator, context, filepath, global_matrix)
load_web3d(context, filepath, load_web3d(context, filepath,
PREF_FLAT=True, PREF_FLAT=False, # So Tranforms will be imported
PREF_CIRCLE_DIV=16, PREF_CIRCLE_DIV=16,
global_scale=global_scale, global_scale=global_scale,
global_matrix=global_matrix, global_matrix=global_matrix,