[AI] Exporting Three.JS from Blender by pulling teeth.

John Carlson yottzumm at gmail.com
Sun Mar 1 00:06:09 PST 2026


>From another planet:

Untested!  Need Blender humanoid model (skinned+armature) with ‘Walk’ and
‘Wave’ actions.

Let me know if you want to pursue anything this direct (skipping
X3D/glTF/USD).

Also consider exporting X3D JavaScript/TypeScript SAI from Blender,
probably for use in X_ITE.

John


>From Google Search!

=========
Blender Python below
=========

import bpy
import os
import json

# Configuration
output_file = os.path.join(bpy.path.abspath("//"), "HumanoidComplete.js")
mesh_obj = bpy.context.active_object
armature_obj = mesh_obj.parent

if not (mesh_obj and mesh_obj.type == 'MESH' and armature_obj):
raise Exception("Select the Mesh object (must be parented to an Armature)")

def get_action_data(action_name):
action = bpy.data.actions.get(action_name)
if not action: return None
fps = bpy.context.scene.render.fps
tracks = []
for fcurve in action.fcurves:
if "pose.bones" not in fcurve.data_path: continue
bone_name = fcurve.data_path.split('"')[1]
p_type = "position" if "location" in fcurve.data_path else "quaternion" if
"quaternion" in fcurve.data_path else "scale"
times = [round(kp.co[0] / fps, 3) for kp in fcurve.keyframe_points]
values = [round(kp.co[1], 4) for kp in fcurve.keyframe_points]
tracks.append({"name": f"{bone_name}.{p_type}", "times": times, "values":
values})
return {"name": action_name, "duration": round(action.frame_range[1]/fps,
3), "tracks": tracks}

# 1. EXTRACT MESH DATA (Triangulated for WebGL)
depsgraph = bpy.context.evaluated_depsgraph_get()
mesh = mesh_obj.evaluated_get(depsgraph).to_mesh()
mesh.calc_loop_triangles()

positions, uvs, colors = [], [], []
skin_idx, skin_wgt = [], []
bone_map = {b.name: i for i, b in enumerate(armature_obj.data.bones)}

for tri in mesh.loop_triangles:
for loop_idx in tri.loops:
v_idx = mesh.loops[loop_idx].vertex_index
v = mesh.vertices[v_idx]
positions.extend([round(v.co.x, 4), round(v.co.z, 4), round(-v.co.y, 4)])
if mesh.uv_layers.active:
uv = mesh.uv_layers.active.data[loop_idx].uv
uvs.extend([round(uv.x, 4), round(1.0 - uv.y, 4)])
if mesh.color_attributes.active:
c = mesh.color_attributes.active.data[loop_idx].color
colors.extend([round(c[0], 3), round(c[1], 3), round(c[2], 3)])

# Weights (Top 4)
grps = sorted(v.groups, key=lambda g: g.weight, reverse=True)[:4]
idx, wgt = [0]*4, [0.0]*4
for i, g in enumerate(grps):
name = mesh_obj.vertex_groups[g.group].name
if name in bone_map:
idx[i], wgt[i] = bone_map[name], round(g.weight, 4)
skin_idx.extend(idx); skin_wgt.extend(wgt)

# 2. EXTRACT SKELETON
bones = []
for b in armature_obj.data.bones:
mat = b.matrix_local if not b.parent else b.parent.matrix_local.inverted()
@ b.matrix_local
bones.append({"name": b.name, "parent": b.parent.name if b.parent else
None,
"pos": [round(mat.translation.x, 4), round(mat.translation.z, 4),
round(-mat.translation.y, 4)]})

# 3. WRITE TO JS
with open(output_file, 'w') as f:
f.write("import * as THREE from 'three';\n\nexport const createHumanoid =
() => {\n")
f.write(f" const geo = new THREE.BufferGeometry();\n")
f.write(f" geo.setAttribute('position', new
THREE.Float32BufferAttribute({positions}, 3));\n")
f.write(f" geo.setAttribute('uv', new THREE.Float32BufferAttribute({uvs},
2));\n")
f.write(f" geo.setAttribute('color', new
THREE.Float32BufferAttribute({colors}, 3));\n")
f.write(f" geo.setAttribute('skinIndex', new
THREE.Uint16BufferAttribute({skin_idx}, 4));\n")
f.write(f" geo.setAttribute('skinWeight', new
THREE.Float32BufferAttribute({skin_wgt}, 4));\n\n")

f.write(" const bones = [];\n const boneMap = {};\n")
for b in bones:
f.write(f" const {b['name']} = new THREE.Bone(); {b['name']}.name =
'{b['name']}';\n")
f.write(f"
{b['name']}.position.set({b['pos'][0]},{b['pos'][1]},{b['pos'][2]});\n")
f.write(f" boneMap['{b['name']}'] = {b['name']};
bones.push({b['name']});\n")
for b in bones:
if b['parent']: f.write(f"
boneMap['{b['parent']}'].add(boneMap['{b['name']}']);\n")

f.write(f"\n const walk = {json.dumps(get_action_data('Walk'))};\n")
f.write(f" const wave = {json.dumps(get_action_data('Wave'))};\n")
f.write(" const clips = [walk, wave].filter(d => d).map(d => new
THREE.AnimationClip(d.name, d.duration, d.tracks.map(t => new
THREE.KeyframeTrack(t.name, t.times, t.values))));\n")

f.write("\n const mesh = new THREE.SkinnedMesh(geo, new
THREE.MeshStandardMaterial({vertexColors: true, skinning: true}));\n")
f.write(" mesh.add(bones[0]); mesh.bind(new THREE.Skeleton(bones));\n")
f.write(" return { mesh, animations: clips };\n};")


=============
JavaScript below
=============
import { createHumanoid } from './HumanoidComplete.js';

const { mesh, animations } = createHumanoid();
scene.add(mesh);

const mixer = new THREE.AnimationMixer(mesh);
const walkAction =
mixer.clipAction(THREE.AnimationClip.findByName(animations, 'Walk'));
walkAction.play();

// Main loop
function animate() {
mixer.update(clock.getDelta());
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://web3d.org/pipermail/ai_web3d.org/attachments/20260301/3d1da0d4/attachment.html>


More information about the AI mailing list