[X3D-Ecosystem] No bones about it.l!
John Carlson
yottzumm at gmail.com
Mon Dec 23 17:41:50 PST 2024
Apparently, Bone type has both head and head_local fields whereas EditBone
and PoseBone types have a head field, which I use. This may be more useful
than figuring out matrix_local.
There's also a tail_local with Bone, and all the bones have tail as well.
Here's some sample code for how to create several joints in Blender,and
export them to X3D. I'm not seeing HAnim.
> import bpy
> from mathutils import Vector
>
> def create_objects_for_joints():
> """Create basic objects to demonstrate joints"""
> # Clear existing objects
> bpy.ops.object.select_all(action='SELECT')
> bpy.ops.object.delete()
>
> # Create a cube as parent
> bpy.ops.mesh.primitive_cube_add(location=(0, 0, 0))
> parent = bpy.context.active_object
> parent.name = 'Parent'
>
> # Create a cylinder as child
> bpy.ops.mesh.primitive_cylinder_add(location=(3, 0, 0))
> child = bpy.context.active_object
> child.name = 'Child'
>
> return parent, child
>
> def create_fixed_joint(parent, child):
> """Create a fixed joint (Parent constraint)"""
> constraint = child.constraints.new('CHILD_OF')
> constraint.target = parent
> constraint.name = "Fixed Joint"
>
> def create_hinge_joint(parent, child, axis='X'):
> """Create a hinge joint (Rotation constraint)"""
> constraint = child.constraints.new('LIMIT_ROTATION')
> constraint.name = f"Hinge Joint ({axis})"
>
> # Lock all rotations except the specified axis
> setattr(constraint, 'use_limit_x', axis != 'X')
> setattr(constraint, 'use_limit_y', axis != 'Y')
> setattr(constraint, 'use_limit_z', axis != 'Z')
>
> # Set rotation limits for the free axis
> if axis == 'X':
> constraint.min_x = -1.5708 # -90 degrees
> constraint.max_x = 1.5708 # 90 degrees
> elif axis == 'Y':
> constraint.min_y = -1.5708
> constraint.max_y = 1.5708
> elif axis == 'Z':
> constraint.min_z = -1.5708
> constraint.max_z = 1.5708
>
> def create_ball_joint(parent, child):
> """Create a ball joint (Track To constraint)"""
> constraint = child.constraints.new('TRACK_TO')
> constraint.target = parent
> constraint.name = "Ball Joint"
> constraint.track_axis = 'TRACK_Z'
> constraint.up_axis = 'UP_Y'
>
> def create_prismatic_joint(parent, child, axis='X'):
> """Create a prismatic joint (Limit Location constraint)"""
> constraint = child.constraints.new('LIMIT_LOCATION')
> constraint.name = f"Prismatic Joint ({axis})"
>
> # Lock all translations except the specified axis
> setattr(constraint, 'use_min_x', axis != 'X')
> setattr(constraint, 'use_max_x', axis != 'X')
> setattr(constraint, 'use_min_y', axis != 'Y')
> setattr(constraint, 'use_max_y', axis != 'Y')
> setattr(constraint, 'use_min_z', axis != 'Z')
> setattr(constraint, 'use_max_z', axis != 'Z')
>
> # Set translation limits for the free axis
> if axis == 'X':
> constraint.min_x = -5
> constraint.max_x = 5
> elif axis == 'Y':
> constraint.min_y = -5
> constraint.max_y = 5
> elif axis == 'Z':
> constraint.min_z = -5
> constraint.max_z = 5
>
> def create_distance_joint(parent, child, distance=2.0):
> """Create a distance joint (Limit Distance constraint)"""
> constraint = child.constraints.new('LIMIT_DISTANCE')
> constraint.target = parent
> constraint.name = "Distance Joint"
> constraint.distance = distance
> constraint.limit_mode = 'LIMITDIST_ONSURFACE'
>
> def create_spring_joint(parent, child):
> """Create a spring joint (using Hooks)"""
> # Add Hook modifier to child
> hook_mod = child.modifiers.new(name="Spring Joint", type='HOOK')
> hook_mod.object = parent
> hook_mod.strength = 0.5 # Adjust spring stiffness
>
> def main():
> # Create example objects
> parent, child = create_objects_for_joints()
>
> # Example: Create a hinge joint
> create_hinge_joint(parent, child, 'Z')
>
> # Note: You would typically only use one joint type at a time
> # Uncomment the joint type you want to test:
>
> # create_fixed_joint(parent, child)
> # create_ball_joint(parent, child)
> # create_prismatic_joint(parent, child, 'X')
> # create_distance_joint(parent, child, 2.0)
> # create_spring_joint(parent, child)
>
> if __name__ == "__main__":
> main()
>
>
=====================================================================================================================
Export code
import bpy
import os
import xml.etree.ElementTree as ET
import mathutils
def setup_x3d_scene():
"""Create a basic X3D scene structure"""
x3d = ET.Element('X3D')
x3d.set('version', '3.3')
x3d.set('profile', 'Immersive')
scene = ET.SubElement(x3d, 'Scene')
return x3d, scene
def matrix_to_transform_string(matrix):
"""Convert Blender matrix to X3D transform string"""
# X3D uses row-major order
flat_matrix = [str(f) for f in sum((tuple(row) for row in matrix), ())]
return ' '.join(flat_matrix)
def export_object_geometry(obj):
"""Export object mesh data to X3D format"""
if obj.type != 'MESH':
return None
shape = ET.Element('Shape')
# Export material if present
if obj.active_material:
appearance = ET.SubElement(shape, 'Appearance')
material = ET.SubElement(appearance, 'Material')
material.set('diffuseColor', '0.8 0.8 0.8') # Default gray color
# Export geometry
if obj.data.name.startswith('Cube'):
box = ET.SubElement(shape, 'Box')
box.set('size', '2 2 2') # Default cube size
elif obj.data.name.startswith('Cylinder'):
cylinder = ET.SubElement(shape, 'Cylinder')
cylinder.set('radius', '1')
cylinder.set('height', '2')
return shape
def export_joint(parent, child, constraint, scene):
"""Export joint data to X3D format"""
transform_group = ET.SubElement(scene, 'Transform')
transform_group.set('translation', f"{parent.location.x}
{parent.location.y} {parent.location.z}")
# Export parent object
parent_shape = export_object_geometry(parent)
if parent_shape is not None:
transform_group.append(parent_shape)
# Handle different joint types
if constraint.type == 'LIMIT_ROTATION': # Hinge joint
joint = ET.SubElement(transform_group, 'HingeJoint')
joint.set('axisOfRotation', '0 0 1') # Z-axis by default
joint_body = ET.SubElement(joint, 'RigidBody')
child_transform = ET.SubElement(joint_body, 'Transform')
child_transform.set('translation', f"{child.location.x -
parent.location.x} "
f"{child.location.y -
parent.location.y} "
f"{child.location.z -
parent.location.z}")
child_shape = export_object_geometry(child)
if child_shape is not None:
child_transform.append(child_shape)
elif constraint.type == 'TRACK_TO': # Ball joint
joint = ET.SubElement(transform_group, 'BallJoint')
joint.set('anchorPoint', f"{parent.location.x} {parent.location.y}
{parent.location.z}")
joint_body = ET.SubElement(joint, 'RigidBody')
child_transform = ET.SubElement(joint_body, 'Transform')
child_transform.set('translation', f"{child.location.x -
parent.location.x} "
f"{child.location.y -
parent.location.y} "
f"{child.location.z -
parent.location.z}")
child_shape = export_object_geometry(child)
if child_shape is not None:
child_transform.append(child_shape)
elif constraint.type == 'LIMIT_LOCATION': # Prismatic joint
joint = ET.SubElement(transform_group, 'SliderJoint')
joint.set('axis', '1 0 0') # X-axis by default
joint_body = ET.SubElement(joint, 'RigidBody')
child_transform = ET.SubElement(joint_body, 'Transform')
child_transform.set('translation', f"{child.location.x -
parent.location.x} "
f"{child.location.y -
parent.location.y} "
f"{child.location.z -
parent.location.z}")
child_shape = export_object_geometry(child)
if child_shape is not None:
child_transform.append(child_shape)
def export_scene_to_x3d(filepath):
"""Export entire scene with joints to X3D file"""
x3d, scene = setup_x3d_scene()
# Export each object with constraints (joints)
for obj in bpy.data.objects:
if obj.constraints:
for constraint in obj.constraints:
if constraint.target: # If constraint has a target (parent)
export_joint(constraint.target, obj, constraint, scene)
# Write X3D file
tree = ET.ElementTree(x3d)
tree.write(filepath, encoding='utf-8', xml_declaration=True,
pretty_print=True)
def main():
# Example usage
output_path = "C:/temp/blender_joints.x3d" # Adjust path as needed
# First create some joints using the previous script...
parent, child = create_objects_for_joints() # From previous example
create_hinge_joint(parent, child, 'Z')
# Then export to X3D
export_scene_to_x3d(output_path)
print(f"Exported X3D file to: {output_path}")
if __name__ == "__main__":
main()
=========================================================================================
HAnim Export code:
import bpy
import xml.etree.ElementTree as ET
import mathutils
from math import degrees
def create_hanim_skeleton():
"""Create a basic humanoid skeleton structure"""
armature = bpy.data.armatures.new('HumanoidSkeleton')
armature_obj = bpy.data.objects.new('Humanoid', armature)
bpy.context.scene.collection.objects.link(armature_obj)
# Enter edit mode to add bones
bpy.context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='EDIT')
# Dictionary to store edit_bones for parenting
bones = {}
# Create spine and head
bones['HumanoidRoot'] = armature.edit_bones.new('HumanoidRoot')
bones['HumanoidRoot'].head = (0, 0, 0)
bones['HumanoidRoot'].tail = (0, 0, 0.1)
bones['vl5'] = armature.edit_bones.new('vl5')
bones['vl5'].head = (0, 0, 0.1)
bones['vl5'].tail = (0, 0, 0.3)
bones['vl5'].parent = bones['HumanoidRoot']
bones['Sacrum'] = armature.edit_bones.new('Sacrum')
bones['Sacrum'].head = (0, 0, 0.3)
bones['Sacrum'].tail = (0, 0, 0.5)
bones['Sacrum'].parent = bones['vl5']
# Create legs
for side in ['l', 'r']:
bones[f'{side}_hip'] = armature.edit_bones.new(f'{side}_hip')
bones[f'{side}_hip'].head = (0.1 if side == 'r' else -0.1, 0, 0.3)
bones[f'{side}_hip'].tail = (0.1 if side == 'r' else -0.1, 0, 0.2)
bones[f'{side}_hip'].parent = bones['Sacrum']
bones[f'{side}_knee'] = armature.edit_bones.new(f'{side}_knee')
bones[f'{side}_knee'].head = bones[f'{side}_hip'].tail
bones[f'{side}_knee'].tail = (0.1 if side == 'r' else -0.1, 0, 0.1)
bones[f'{side}_knee'].parent = bones[f'{side}_hip']
bones[f'{side}_ankle'] = armature.edit_bones.new(f'{side}_ankle')
bones[f'{side}_ankle'].head = bones[f'{side}_knee'].tail
bones[f'{side}_ankle'].tail = (0.1 if side == 'r' else -0.1, 0.1, 0)
bones[f'{side}_ankle'].parent = bones[f'{side}_knee']
bpy.ops.object.mode_set(mode='OBJECT')
return armature_obj
def matrix_to_transform_string(matrix):
"""Convert Blender matrix to X3D transform string"""
flat_matrix = [str(f) for f in sum((tuple(row) for row in matrix), ())]
return ' '.join(flat_matrix)
def create_hanim_joint_element(bone, parent_element):
"""Create X3D HAnimJoint element for a bone"""
joint = ET.SubElement(parent_element, 'HAnimJoint')
joint.set('name', bone.name)
# Calculate joint center (head position)
joint_center = bone.head_local
joint.set('center', f"{joint_center.x} {joint_center.y}
{joint_center.z}")
# Calculate rotation and translation from bone matrix
if bone.parent:
local_matrix = bone.parent.matrix_local.inverted() @
bone.matrix_local
pos, rot, scale = local_matrix.decompose()
# Convert rotation to axis-angle
axis_angle = rot.to_axis_angle()
if axis_angle[1] != 0:
joint.set('rotation', f"{axis_angle[0].x} {axis_angle[0].y}
{axis_angle[0].z} {axis_angle[1]}")
joint.set('translation', f"{pos.x} {pos.y} {pos.z}")
return joint
def export_hanim_to_x3d(armature_obj, filepath):
"""Export humanoid animation skeleton to X3D"""
# Create X3D document
x3d = ET.Element('X3D')
x3d.set('version', '3.3')
x3d.set('profile', 'Immersive')
scene = ET.SubElement(x3d, 'Scene')
# Create HAnimHumanoid
humanoid = ET.SubElement(scene, 'HAnimHumanoid')
humanoid.set('name', 'Humanoid')
humanoid.set('version', '2.0')
# Create skeleton
skeleton = ET.SubElement(humanoid, 'HAnimJoint')
skeleton.set('name', 'HumanoidRoot')
# Export joints hierarchy
def export_bone_hierarchy(bone, parent_element):
if bone.parent is None or bone.parent.parent is None:
joint_element = create_hanim_joint_element(bone, parent_element)
else:
joint_element = create_hanim_joint_element(bone, parent_element)
# Export child bones
for child in bone.children:
export_bone_hierarchy(child, joint_element)
# Start export from root bone
root_bone = armature_obj.data.bones[0]
export_bone_hierarchy(root_bone, skeleton)
# Add animation if present
if armature_obj.animation_data and armature_obj.animation_data.action:
action = armature_obj.animation_data.action
# Create TimeSensor
time_sensor = ET.SubElement(scene, 'TimeSensor')
time_sensor.set('DEF', 'AnimationTimer')
time_sensor.set('cycleInterval', str(action.frame_range[1] / 24.0))
# Assuming 24 fps
time_sensor.set('loop', 'true')
# Create animation for each bone
for fcurve in action.fcurves:
if fcurve.data_path.startswith('pose.bones'):
bone_name = fcurve.data_path.split('"')[1]
# Create OrientationInterpolator for rotation
if fcurve.array_index < 3: # rotation channels
interpolator = ET.SubElement(scene,
'OrientationInterpolator')
interpolator.set('DEF', f'{bone_name}_rot')
# Export keyframes
key_times = []
key_values = []
for keyframe in fcurve.keyframe_points:
time = keyframe.co.x / 24.0 # Convert frames to
seconds
value = keyframe.co.y
key_times.append(str(time))
key_values.append(str(value))
interpolator.set('key', ' '.join(key_times))
interpolator.set('keyValue', ' '.join(key_values))
# Write X3D file
tree = ET.ElementTree(x3d)
tree.write(filepath, encoding='utf-8', xml_declaration=True)
def main():
# Create a humanoid skeleton
armature_obj = create_hanim_skeleton()
# Export to X3D
export_path = "C:/temp/humanoid.x3d" # Adjust path as needed
export_hanim_to_x3d(armature_obj, export_path)
print(f"Exported H-Anim X3D file to: {export_path}")
if __name__ == "__main__":
main()
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://web3d.org/pipermail/x3d-ecosystem_web3d.org/attachments/20241223/0b4555db/attachment-0001.html>
More information about the X3D-Ecosystem
mailing list