[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