Files
sunrise/scripts/create-rain-gear.py

187 lines
9.7 KiB
Python

"""Run with Blender MCP. Separate cartoon rain gear for the rounded v10 sheep."""
import bpy, bmesh, math, json, struct, sys
from mathutils import Vector, Matrix
ROOT='D:/project/sunrise'
with bpy.data.libraries.load(ROOT+'/assets/characters/ivory-lamb-v10.blend',link=False) as (src,dst):
dst.scenes=src.scenes
reference=dst.scenes[0]
reference.view_layers[0].update()
scene=bpy.data.scenes.new('Cartoon rain gear')
bpy.context.window.scene=scene
def material(name,hexcolor):
mat=bpy.data.materials.new(name); mat.use_nodes=True
rgb=[int(hexcolor[i:i+2],16)/255 for i in (0,2,4)]
rgb=[v/12.92 if v<.04045 else ((v+.055)/1.055)**2.4 for v in rgb]
mat.diffuse_color=(*rgb,1)
nodes=mat.node_tree.nodes; nodes.clear()
out=nodes.new('ShaderNodeOutputMaterial'); node=nodes.new('ShaderNodeEmission')
node.inputs['Color'].default_value=(*rgb,1)
mat.node_tree.links.new(node.outputs[0],out.inputs['Surface'])
return mat
yellow=material('Raincoat yellow','E9BE69')
red=material('Umbrella red','BE6264')
cream=material('Umbrella ivory','F5EDDD')
ink=material('SheepV2_Ink_Gear','67564C')
def empty(name,parent=None):
obj=bpy.data.objects.new(name,None);scene.collection.objects.link(obj);obj.parent=parent
return obj
def mesh(name,vertices,faces,mat,parent):
data=bpy.data.meshes.new(name);data.from_pydata(vertices,[],faces);data.update()
obj=bpy.data.objects.new(name,data);scene.collection.objects.link(obj)
obj.parent=parent;data.materials.append(mat)
return obj
def stroke(name,points,radius,mat,parent):
curve=bpy.data.curves.new(name,'CURVE');curve.dimensions='3D'
curve.bevel_depth=radius;curve.bevel_resolution=3
spline=curve.splines.new('POLY');spline.points.add(len(points)-1)
for p,co in zip(spline.points,points):p.co=(*co,1)
obj=bpy.data.objects.new(name,curve);scene.collection.objects.link(obj);obj.parent=parent
curve.materials.append(mat)
bpy.ops.object.select_all(action='DESELECT');obj.select_set(True);bpy.context.view_layer.objects.active=obj
bpy.ops.object.convert(target='MESH')
return bpy.context.object
def sphere(name,location,scale,mat,parent):
bpy.ops.mesh.primitive_uv_sphere_add(segments=24,ring_count=12,location=location)
obj=bpy.context.object;obj.name=name;obj.scale=scale;obj.parent=parent
obj.data.materials.append(mat)
bpy.ops.object.transform_apply(location=False,rotation=False,scale=True)
return obj
umbrella=empty('Umbrella')
# The grip is in the raised right hoof; the tilted shaft centers the canopy above the head.
grip=Vector((.58,-.40,.82)); top=Vector((0,-.05,3.18))
stroke('Umbrella shaft',[grip,top],.022,ink,umbrella)
def canopy_point(radius,sector,across):
# Taut fabric curves inward between the eight projecting rib tips.
scallop=math.sin(math.pi*across)
r=1.20*radius*(1-.10*scallop*radius**3)
a=(sector+across)*math.pi/4
return (r*math.cos(a),-.05+r*math.sin(a),3.18-.65*radius**1.4+.10*scallop*radius**3)
for sector in range(8):
vertices=[];faces=[]
for row in range(9):
for col in range(5):
vertices.append(canopy_point(row/8,sector,col/4))
for row in range(8):
for col in range(4):
i=row*5+col;faces.append((i,i+1,i+6,i+5))
panel=mesh('Canopy panel',vertices,faces,red,umbrella)
bpy.context.view_layer.objects.active=panel
solid=panel.modifiers.new('Canopy thickness','SOLIDIFY');solid.thickness=.018
bpy.ops.object.modifier_apply(modifier=solid.name)
stroke('Canopy rib',[canopy_point(i/16,sector,0) for i in range(17)],.009,ink,umbrella)
stroke('Canopy rim',[canopy_point(1,sector,i/12) for i in range(13)],.012,ink,umbrella)
sphere('Rib tip',canopy_point(1,sector,0),(.025,.025,.025),ink,umbrella)
stroke('Umbrella tip',[(0,-.05,3.17),(0,-.05,3.33)],.022,ink,umbrella)
sphere('Grip',grip,(.052,.052,.12),red,umbrella)
stroke('Hook handle',[grip,(grip.x,grip.y,grip.z-.16)]+[
(grip.x-.10+.10*math.cos(a),grip.y,grip.z-.16-.10*math.sin(a))
for a in [i*math.pi/16 for i in range(17)]],.033,red,umbrella)
coat=empty('Raincoat')
for joint_name in ('Body','ArmLeft','ArmRight'):
pivot=next(o for o in reference.objects if o.name.split('.')[0]==joint_name)
source=next(o for o in reference.objects if o.parent==pivot and o.type=='MESH' and not o.name.startswith('Preview') and not o.get('sheepOutline')==False)
data=source.data.copy(); bm=bmesh.new();bm.from_mesh(data)
dark=[f for f in bm.faces if 'Ink' in source.data.materials[f.material_index].name]
bmesh.ops.delete(bm,geom=dark,context='FACES')
if joint_name=='Body':
# Keep the torso component; do not turn the wool tail into a coat bubble.
unseen=set(bm.verts);components=[]
while unseen:
pending=[unseen.pop()];component=set(pending)
while pending:
v=pending.pop()
for edge in v.link_edges:
n=edge.other_vert(v)
if n in unseen:unseen.remove(n);component.add(n);pending.append(n)
components.append(component)
keep=max(components,key=len)
bmesh.ops.delete(bm,geom=[v for v in bm.verts if v not in keep],context='VERTS')
bm.to_mesh(data);bm.free()
data.materials.clear();data.materials.append(yellow)
anchor=empty('Coat'+joint_name,coat)
if joint_name!='Body':anchor.matrix_local=pivot.matrix_world.copy()
obj=bpy.data.objects.new('Raincoat '+joint_name,data);scene.collection.objects.link(obj);obj.parent=anchor
transform=source.matrix_world if joint_name=='Body' else pivot.matrix_world.inverted()@source.matrix_world
center=Vector((0,0,.68)) if joint_name=='Body' else Vector((.08 if joint_name=='ArmRight' else -.08,0,-.20))
for v in data.vertices:
v.co=center+(transform@v.co-center)*1.075
if joint_name=='Body':
# A short raincoat hem leaves both wool legs and hoof tips exposed.
bm=bmesh.new();bm.from_mesh(data)
bmesh.ops.bisect_plane(bm,geom=list(bm.verts)+list(bm.edges)+list(bm.faces),
plane_co=(0,0,.42),plane_no=(0,0,1),clear_inner=True)
boundary=[edge for edge in bm.edges if edge.is_boundary]
bmesh.ops.holes_fill(bm,edges=boundary,sides=0)
for v in bm.verts:
flare=1+.06*max(0,1-(v.co.z-.42)/.25)
v.co.x*=flare;v.co.y*=flare
bm.to_mesh(data);bm.free()
for p in data.polygons:p.material_index=0
# A folded hood behind the neck, plus simple front fasteners.
sphere('CoatFoldedHood',(0,.48,1.18),(.38,.20,.17),yellow,coat)
hood=empty('CoatHood',coat)
head_pivot=next(o for o in reference.objects if o.name.split('.')[0]=='Head')
hood.matrix_local=head_pivot.matrix_world.copy()
vertices=[];faces=[]
def hood_point(theta,phi):
x=.98*math.sin(theta)*math.cos(phi)
y=-.85*math.cos(theta)
z=.02+.96*math.sin(theta)*math.sin(phi)
z+=.12*math.exp(-(x/.22)**2-((theta-1.5)/.35)**2)*max(0,math.sin(phi))**8
return (x,y,z)
for row in range(17):
theta=1.12+(math.pi-1.12)*row/16
for col in range(49):vertices.append(hood_point(theta,col*math.tau/48))
for row in range(16):
for col in range(48):
i=row*49+col;faces.append((i,i+1,i+50,i+49))
hood_shell=mesh('Rain hood shell',vertices,faces,yellow,hood)
bpy.context.view_layer.objects.active=hood_shell
solid=hood_shell.modifiers.new('Hood fabric','SOLIDIFY');solid.thickness=.025
bpy.ops.object.modifier_apply(modifier=solid.name)
stroke('Hood opening rim',[hood_point(1.12,i*math.tau/96) for i in range(97)],.025,yellow,hood)
stroke('Coat placket',[(0,-.72,z) for z in (.47,.65,.90,1.07)],.012,ink,coat)
for z in (.54,.77,1.00):sphere('Coat button',(0,-.735,z),(.032,.022,.032),cream,coat)
for obj in scene.objects:
if obj.type!='MESH':continue
bm=bmesh.new();bm.from_mesh(obj.data);bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces));bm.to_mesh(obj.data);bm.free()
for p in obj.data.polygons:p.use_smooth=True
obj.data.update()
old=obj.data.attributes.get('_outline_offset')
if old:obj.data.attributes.remove(old)
offsets=obj.data.attributes.new('_outline_offset','FLOAT_VECTOR','POINT')
for v,item in zip(obj.data.vertices,offsets.data):item.vector=v.normal*.012
def export(root,path):
bpy.ops.object.select_all(action='DESELECT')
root.select_set(True)
for obj in root.children_recursive:obj.select_set(True)
bpy.context.view_layer.objects.active=root
bpy.ops.export_scene.gltf(filepath=path,use_selection=True,use_active_scene=True,export_format='GLB',export_yup=True,export_extras=True,export_attributes=True)
data=open(path,'rb').read();size=struct.unpack_from('<I',data,12)[0]
doc=json.loads(data[20:20+size]);binary=data[20+size:]
# Repeated runs in a live Blender session must retain the runtime attachment names.
for node in doc.get('nodes',[]):
base=node.get('name','').split('.')[0]
if base in ('Umbrella','Raincoat','CoatBody','CoatArmLeft','CoatArmRight','CoatHood','CoatFoldedHood'):node['name']=base
doc.setdefault('extensionsUsed',[]).append('KHR_materials_unlit')
for mat in doc['materials']:
mat['pbrMetallicRoughness']={'baseColorFactor':mat.pop('emissiveFactor')+[1],'metallicFactor':0,'roughnessFactor':1}
mat.setdefault('extensions',{})['KHR_materials_unlit']={}
encoded=json.dumps(doc,separators=(',',':')).encode();encoded+=b' '*((-len(encoded))%4)
with open(path,'wb') as f:
f.write(struct.pack('<III',0x46546C67,2,20+len(encoded)+len(binary)))
f.write(struct.pack('<II',len(encoded),0x4E4F534A));f.write(encoded);f.write(binary)
export(umbrella,ROOT+'/public/characters/umbrella.glb')
if '--umbrella-only' not in sys.argv:export(coat,ROOT+'/public/characters/raincoat.glb')
bpy.data.libraries.write(ROOT+'/assets/characters/rain-gear.blend',{scene})
print('Umbrella and articulated raincoat exported')