Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121import bpy, os, json, math, structfrom mathutils import Matrix, Vectorfrom mathutils.bvhtree import BVHTreeOUT=os.path.dirname(os.path.abspath(__file__))DEST=os.path.abspath(os.path.join(OUT,'../dist'))bpy.ops.wm.open_mainfile(filepath=os.path.join(OUT,'holiday_ghost_vtuber_rig.blend'))rig=bpy.data.objects['RIG | pose in Pose Mode']rig.matrix_world=Matrix.Identity(4)for prop in ['Head Yaw','Head Pitch','Head Roll','Gaze X','Gaze Y','Blink L','Blink R']:rig[prop]=0.0rig.update_tag();bpy.context.view_layer.update()sheet=bpy.data.objects['Ghost sheet | continuous quad drape']# Independent deformation rings spread the head's pull over the entire drape.# The lowest ring stays planted; the top blends into the existing head bone.ring_heights=[.12,.50,.90,1.25,1.60,1.85]bpy.ops.object.select_all(action='DESELECT');rig.select_set(True);bpy.context.view_layer.objects.active=rigbpy.ops.object.mode_set(mode='EDIT')for i,z in enumerate(ring_heights): b=rig.data.edit_bones.new('sheet.%02d'%i) b.head=(.14*(z/3.02)**1.3,0,z);b.tail=b.head+Vector((0,0,.14)) b.parent=rig.data.edit_bones['root']bpy.ops.object.mode_set(mode='OBJECT')for g in list(sheet.vertex_groups):sheet.vertex_groups.remove(g)ring_names=['sheet.%02d'%i for i in range(len(ring_heights))]+['head']groups=[sheet.vertex_groups.new(name=n) for n in ring_names]levels=ring_heights+[2.05]for v in sheet.data.vertices: z=v.co.z if z<=levels[0]:groups[0].add([v.index],1,'REPLACE');continue if z>=levels[-1]:groups[-1].add([v.index],1,'REPLACE');continue i=next(i for i in range(len(levels)-1) if levels[i]<=z<levels[i+1]) blend=(z-levels[i])/(levels[i+1]-levels[i]) groups[i].add([v.index],1-blend,'REPLACE');groups[i+1].add([v.index],blend,'REPLACE')bpy.context.view_layer.update()tree=BVHTree.FromPolygons([v.co for v in sheet.data.vertices],[list(p.vertices) for p in sheet.data.polygons])gaze_surfaces={}for side in ['L','R']: eye=bpy.data.objects['Eye.'+side] c=rig.matrix_world.inverted()@eye.matrix_world.translation dx=.085;dz=.05;grid=[] def surface(x,z): hit=tree.ray_cast(Vector((x,-3,z)),Vector((0,1,0)))[0] assert hit is not None return hit.y center_y=surface(c.x,c.z) for iy in range(9): row=[] for ix in range(9):row.append(surface(c.x+dx*(ix/4-1),c.z+dz*(iy/4-1))-center_y) grid.append(row) gaze_surfaces[side]={'travel':[dx,dz],'depth':grid}# Retain a deterministic sample of the authored tapered fiber strands for silhouette detail.fiber_counts={}for name,stride in [('Textile | hat woven nap',8),('Textile | cotton surface fibers',8),('Textile | imperfect loose hem threads',2)]: ob=bpy.data.objects[name];original=ob.data assert len(original.vertices)%12==0 keep=[i for strand in range(0,len(original.vertices)//12,stride) for i in range(strand*12,strand*12+12)] remap={old:new for new,old in enumerate(keep)} faces=[tuple(remap[i] for i in p.vertices) for p in original.polygons if all(i in remap for i in p.vertices)] data=bpy.data.meshes.new(name+' | real-time sample');data.from_pydata([original.vertices[i].co for i in keep],[],faces);data.update() for mat in original.materials:data.materials.append(mat) group_names=[g.name for g in ob.vertex_groups] weights=[[(g.group,g.weight) for g in original.vertices[i].groups] for i in keep] ob.data=data for p in data.polygons:p.use_smooth=True if 'hat woven' in name: for group in list(ob.vertex_groups):ob.vertex_groups.remove(group) for group_name in group_names:ob.vertex_groups.new(name=group_name) for i,ws in enumerate(weights): for group,w in ws:ob.vertex_groups[group].add([i],w,'REPLACE') else: for group in list(ob.vertex_groups):ob.vertex_groups.remove(group) fgroups=[ob.vertex_groups.new(name=n) for n in ring_names] for v in data.vertices: z=v.co.z if z<=levels[0]:fgroups[0].add([v.index],1,'REPLACE');continue if z>=levels[-1]:fgroups[-1].add([v.index],1,'REPLACE');continue i=next(i for i in range(len(levels)-1) if levels[i]<=z<levels[i+1]);blend=(z-levels[i])/(levels[i+1]-levels[i]) fgroups[i].add([v.index],1-blend,'REPLACE');fgroups[i+1].add([v.index],blend,'REPLACE') fiber_counts[name]=len(keep)//12objects=[o for o in [rig,*rig.children_recursive] if 'ORIGINAL smooth tube' not in o.name]report={'objects':[], 'fiber_strands':fiber_counts,'gaze_coefficients':json.load(open(os.path.join(OUT,'vtuber-rig-validation.json')))['surface_coefficients']}for o in objects: if o.type=='CURVE': if len(o.data.splines)>3000: for i in range(len(o.data.splines)-1,-1,-1): if i%8:o.data.splines.remove(o.data.splines[i]) o.data.bevel_resolution=0;o.data.resolution_u=2 if o.type=='MESH': report['objects'].append({'name':o.name,'vertices':len(o.data.vertices)}) for mod in o.modifiers: if mod.type=='SUBSURF':mod.levels=0;mod.render_levels=0 # Real-time PBR interpretation of the procedural Cycles materials. for slot in o.material_slots: m=slot.material if not m:continue if m.get('realtime_prepared'):continue old=m.node_tree.nodes if m.use_nodes else [] bs=next((n for n in old if n.type=='BSDF_PRINCIPLED'),None) base=tuple(bs.inputs['Base Color'].default_value) if bs else tuple(m.diffuse_color) rough=bs.inputs['Roughness'].default_value if bs else .5 metal=bs.inputs['Metallic'].default_value if bs else 0 name=m.name.lower() if 'ivory' in name or 'cotton' in name or 'sheet' in name:base=(.81,.755,.64,1);rough=.9 if 'hat' in name and ('cloth' in name or 'red' in name or 'felt' in name):base=(.37,.009,.017,1);rough=.88 if 'eye' in name:base=(.006,.004,.007,1);rough=.47 m.use_nodes=True;m.node_tree.nodes.clear() p=m.node_tree.nodes.new('ShaderNodeBsdfPrincipled');p.inputs['Base Color'].default_value=base;p.inputs['Roughness'].default_value=rough;p.inputs['Metallic'].default_value=metal output=m.node_tree.nodes.new('ShaderNodeOutputMaterial');m.node_tree.links.new(p.outputs['BSDF'],output.inputs['Surface']) m.diffuse_color=base;m['realtime_prepared']=Trueos.makedirs(DEST,exist_ok=True)bpy.ops.object.select_all(action='DESELECT')for o in objects:o.select_set(True)bpy.context.view_layer.objects.active=rigbpy.ops.export_scene.gltf(filepath=os.path.join(DEST,'ghost.glb'),export_format='GLB',use_selection=True,export_apply=False,export_animations=False,export_skins=True,export_morph=True,export_yup=True,export_extras=True)with open(os.path.join(DEST,'ghost.glb'),'rb') as f: f.seek(12);n,t=struct.unpack('<II',f.read(8));gltf=json.loads(f.read(n))report['bones']=[n for n in gltf['nodes'] if n['name'] in ['head','eye.L','eye.R']]report['triangles']=sum(gltf['accessors'][p['indices']]['count']//3 for m in gltf['meshes'] for p in m['primitives'])with open(os.path.join(DEST,'rig.json'),'w') as f:json.dump({'gaze':report['gaze_coefficients'],'gaze_surfaces':gaze_surfaces,'sheet_rings':[{'name':n,'height':h} for n,h in zip(ring_names,ring_heights)],'source':'holiday_ghost_vtuber_rig.blend','head_limits':{'yaw':30,'pitch':20,'roll':20}},f,indent=2)with open(os.path.join(OUT,'controller-export-validation.json'),'w') as f:json.dump(report,f,indent=2)print('CONTROLLER EXPORT COMPLETE',report['triangles'],flush=True)