SOURCE / PINNED RELEASE
Made of little things.
Body Journal
- Release
- e8049f8da4e2…
- Author-recorded commit
- 61c04bf0e8aa…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-6c4554844c7
Archive hash verified: 4369d778da046a8b…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
"""Build the CC0 Body Journal skin asset with Blender and unmodified MPFB 2.0.17.
BLENDER_USER_RESOURCES must point to a disposable build directory.
blender --background --factory-startup --python scripts/prepare-skin.py -- MPFB_SOURCE OUTPUT_DIR
The running user's Blender scene/preferences are never read or overwritten.
"""
import bpy, sys, os, json, math, struct, hashlib, addon_utils
from pathlib import Path
from array import array
from mathutils import Vector, Matrix
if not bpy.app.background or not os.environ.get('BLENDER_USER_RESOURCES'):
raise RuntimeError('Use a dedicated background Blender process with a disposable BLENDER_USER_RESOURCES directory.')
args = sys.argv[sys.argv.index('--') + 1:]
source, output = [Path(p).resolve() for p in args]
output.mkdir(parents=True, exist_ok=True)
repo = output / 'build-extension'
repo.mkdir(exist_ok=True)
if not (repo / 'mpfb').exists():
(repo / 'mpfb').symlink_to(source / 'src' / 'mpfb', target_is_directory=True)
bpy.context.preferences.extensions.repos.new(name='Body Journal build', module='body_journal', custom_directory=str(repo))
addon_utils.enable('bl_ext.body_journal.mpfb', default_set=True)
from bl_ext.body_journal.mpfb.services.humanservice import HumanService
from bl_ext.body_journal.mpfb.services.targetservice import TargetService
# This is a dedicated background build, never the user's active scene.
for obj in list(bpy.data.objects): bpy.data.objects.remove(obj, do_unlink=True)
variants = []
metadata = {'id': 'mpfb-skin-1', 'source': 'MakeHuman/MPFB 2.0.17 (80919fa)', 'license': 'CC0-1.0', 'variants': [], 'arrays': {}}
body_ids = None
indices, uvs, weights, skin_indices = [], [], [], []
bone_names, parents, bone_tails = [], [], []
def web(v, scale): return [v.x * scale, v.z * scale, -v.y * scale]
def align(pb, direction):
rot = (pb.tail-pb.head).rotation_difference(direction.normalized())
pb.matrix = Matrix.LocRotScale(pb.head, rot @ pb.matrix.to_quaternion(), Vector((1,1,1)))
bpy.context.view_layer.update()
for gender in (0.0, 0.5, 1.0):
for weight in (0.0, 0.5, 1.0):
before = set(bpy.data.objects)
macros = TargetService.get_default_macro_info_dict()
macros.update(age=0.5, gender=gender, weight=weight)
body = HumanService.create_human(mask_helpers=False, macro_detail_dict=macros)
rig = HumanService.add_builtin_rig(body, 'game_engine')
for side, sign in (('l',1),('r',-1)):
direction = Vector((sign * math.sin(math.radians(20)), 0, -math.cos(math.radians(20))))
align(rig.pose.bones['upperarm_'+side], direction)
align(rig.pose.bones['lowerarm_'+side], direction)
bpy.context.view_layer.update()
evaluated = body.evaluated_get(bpy.context.evaluated_depsgraph_get())
mesh = evaluated.to_mesh()
if body_ids is None:
group = body.vertex_groups['body'].index
body_ids = [v.index for v in body.data.vertices if any(g.group == group and g.weight > .5 for g in v.groups)]
remap = {old: new for new, old in enumerate(body_ids)}
bone_names = [b.name for b in rig.data.bones]
parents = [bone_names.index(b.parent.name) if b.parent else -1 for b in rig.data.bones]
groups = {g.index: bone_names.index(g.name) for g in body.vertex_groups if g.name in bone_names}
mesh.calc_loop_triangles()
corner_uvs = []
for tri in mesh.loop_triangles:
if all(v in remap for v in tri.vertices):
indices.extend(remap[v] for v in tri.vertices)
for loop in tri.loops: corner_uvs.extend(mesh.uv_layers.active.data[loop].uv)
uvs = corner_uvs
for old in body_ids:
influences = sorted([(groups[g.group],g.weight) for g in body.data.vertices[old].groups if g.group in groups and g.weight > 0],key=lambda x:-x[1])[:4]
if not influences: raise ValueError('Unweighted surface vertex')
total = sum(w for _,w in influences)
influences += [(0,0)] * (4-len(influences))
skin_indices.extend(b for b,_ in influences)
weights.extend(w/total for _,w in influences)
# Normalize all variants to the same nominal height. User height sets runtime scale.
floor = min(mesh.vertices[i].co.z for i in body_ids)
height = max(mesh.vertices[i].co.z for i in body_ids) - floor
scale = 1.72 / height
positions = [n for i in body_ids for n in web(mesh.vertices[i].co - Vector((0,0,floor)), scale)]
heads = [n for name in bone_names for n in web(rig.pose.bones[name].head - Vector((0,0,floor)), scale)]
tails = [web(rig.pose.bones[name].tail - Vector((0,0,floor)), scale) for name in bone_names]
variants.append((positions, heads))
metadata['variants'].append({'gender': gender, 'weight': weight})
if gender == .5 and weight == .5: bone_tails = tails
evaluated.to_mesh_clear()
for obj in set(bpy.data.objects)-before: bpy.data.objects.remove(obj, do_unlink=True)
print('Prepared variant', gender, weight, flush=True)
metadata.update(vertices=len(body_ids), triangles=len(indices)//3, bones=[{'name':n,'parent':p} for n,p in zip(bone_names,parents)])
payload = bytearray()
def pack(name, values, dtype):
while len(payload) % 4: payload.append(0)
data = array(dtype, values)
if sys.byteorder != 'little': data.byteswap()
metadata['arrays'][name] = {'offset':len(payload),'length':len(values),'type':dtype}
payload.extend(data.tobytes())
pack('indices', indices, 'H')
pack('uvCorners', uvs, 'f')
pack('skinIndices', skin_indices, 'H')
pack('skinWeights', weights, 'f')
for i,(positions, heads) in enumerate(variants):
pack('positions'+str(i), positions, 'f')
pack('heads'+str(i), heads, 'f')
header = json.dumps(metadata,separators=(',',':')).encode()
while len(header)%4: header += b' '
binary = b'BJSKIN01' + struct.pack('<I',len(header)) + header + payload
(output/'body-skin.bin').write_bytes(binary)
# Editable Blender source: original topology, UVs, four strongest normalized weights,
# authoring shape targets, game-engine hierarchy. Runtime additionally fits bone heads.
positions, heads = variants[4]
def blender_vec(v): return (v[0],-v[2],v[1])
verts = [blender_vec(positions[i:i+3]) for i in range(0,len(positions),3)]
faces = [indices[i:i+3] for i in range(0,len(indices),3)]
mesh = bpy.data.meshes.new('Body Journal surface'); mesh.from_pydata(verts,[],faces); mesh.update()
body = bpy.data.objects.new('Body Journal skin',mesh); bpy.context.collection.objects.link(body)
uv = mesh.uv_layers.new(name='MakeHuman UV')
for i,loop in enumerate(mesh.loops): uv.data[i].uv = uvs[i*2:i*2+2]
for polygon in mesh.polygons: polygon.use_smooth=True
body.shape_key_add(name='Basis')
for i,(positions,_) in enumerate(variants):
if i == 4: continue
key=body.shape_key_add(name='Shape %s weight %s' % (metadata['variants'][i]['gender'],metadata['variants'][i]['weight']))
for v,j in zip(key.data,range(0,len(positions),3)): v.co=blender_vec(positions[j:j+3])
armature=bpy.data.armatures.new('Body Journal rig'); rig=bpy.data.objects.new('Body Journal rig',armature); bpy.context.collection.objects.link(rig)
bpy.context.view_layer.objects.active=rig; rig.select_set(True); bpy.ops.object.mode_set(mode='EDIT')
for i,name in enumerate(bone_names):
b=armature.edit_bones.new(name); b.head=blender_vec(heads[3*i:3*i+3]); b.tail=blender_vec(bone_tails[i])
if (b.tail-b.head).length < .001: b.tail.z += .01
if parents[i]>=0: b.parent=armature.edit_bones[bone_names[parents[i]]]
bpy.ops.object.mode_set(mode='OBJECT')
for name in bone_names: body.vertex_groups.new(name=name)
for i in range(len(body_ids)):
for j in range(4):
w=weights[i*4+j]
if w: body.vertex_groups[skin_indices[i*4+j]].add([i],w,'REPLACE')
modifier=body.modifiers.new('Skinning','ARMATURE'); modifier.object=rig; body.parent=rig
mat=bpy.data.materials.new('Ivory illustrative skin'); mat.diffuse_color=(.65,.59,.51,1); mat.use_nodes=True
mat.node_tree.nodes.get('Principled BSDF').inputs['Base Color'].default_value=(*(.65,.59,.51),1)
mat.node_tree.nodes.get('Principled BSDF').inputs['Roughness'].default_value=.72
mesh.materials.append(mat)
rig.show_in_front=True
body['model_id']='mpfb-skin-1'; body['asset_license']='CC0-1.0'; body['source']='MakeHuman/MPFB v2.0.17, commit 80919fa'
body['note']='Illustrative skin model; no clinical validation. Runtime fits joint locations for shape changes.'
bpy.ops.object.select_all(action='DESELECT'); body.select_set(True); bpy.context.view_layer.objects.active=body
bpy.ops.wm.save_as_mainfile(filepath=str(output/'body-skin.blend'),compress=True)
report={**{k:metadata[k] for k in ['id','source','license','vertices','triangles']},'bones':len(bone_names),'shapeSamples':len(variants),'bytes':len(binary),'sha256':hashlib.sha256(binary).hexdigest()}
(output/'provenance.json').write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(report),flush=True)
