"""Blender MCP: continuous silhouette modeling of the approved red sheep.""" import bpy, math, os, bmesh, struct, json from mathutils import Vector ROOT = 'D:/project/sunrise' for name in ('RedSheep','Body','Head','ArmLeft','ArmRight','LegLeft','LegRight'): previous=bpy.data.objects.get(name) if previous: previous.name='Previous_'+name scene=bpy.data.scenes.new('Red Sheep - concept matched') bpy.context.window.scene=scene scene.view_settings.view_transform='Standard' palette={'Red':'D94B3D','Cream':'FFF0D6','Ink':'352724','Gold':'EAAF45'} mats={} for name,color in palette.items(): m=bpy.data.materials.new('SheepV2_'+name); m.use_nodes=True rgb=[int(color[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] m.diffuse_color=(*rgb,1) nodes=m.node_tree.nodes; nodes.clear() out=nodes.new('ShaderNodeOutputMaterial'); emission=nodes.new('ShaderNodeEmission') emission.inputs['Color'].default_value=(*rgb,1) m.node_tree.links.new(emission.outputs[0],out.inputs['Surface']) mats[name]=m def empty(name,loc,parent=None): o=bpy.data.objects.new(name,None); scene.collection.objects.link(o) o.parent=parent; o.location=loc return o root=empty('RedSheep',(0,0,0)); parts={} detail_names={'Eye','Eye glint','Nose','Smile','Muzzle center','Horn spiral','Sun emblem','Sun ray'} def smooth_outline(points,steps=5): result=[] for i in range(len(points)): p0,p1,p2,p3=[Vector(points[j%len(points)]) for j in (i-1,i,i+1,i+2)] for k in range(steps): t=k/steps result.append(.5*((2*p1)+(-p0+p2)*t+(2*p0-5*p1+4*p2-p3)*t*t+(-p0+3*p1-3*p2+p3)*t*t*t)) return result def puff(name,outline,center,depth,color,parent,rings=16): """A single closed rounded volume; silhouette lobes share vertices.""" outline=smooth_outline(outline); n=len(outline); verts=[(0,-depth,0)] for row in range(1,rings): a=math.pi*row/rings; r=math.sin(a) verts.extend([(p.x*r,-depth*math.cos(a),p.y*r) for p in outline]) verts.append((0,depth,0)); faces=[(0,1+(i+1)%n,1+i) for i in range(n)] for row in range(rings-2): a=1+row*n; b=a+n faces.extend([(a+i,a+(i+1)%n,b+(i+1)%n,b+i) for i in range(n)]) end=len(verts)-1; a=1+(rings-2)*n faces.extend([(a+i,a+(i+1)%n,end) for i in range(n)]) mesh=bpy.data.meshes.new(name); mesh.from_pydata(verts,[],faces); mesh.update() o=bpy.data.objects.new(name,mesh); scene.collection.objects.link(o) o.parent=parent; o.location=center; mesh.materials.append(mats[color]) bm=bmesh.new(); bm.from_mesh(mesh) bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces)); bm.to_mesh(mesh); bm.free() offset=mesh.attributes.new('_outline_offset','FLOAT_VECTOR','POINT') for v,item in zip(mesh.vertices,offset.data): item.vector=(v.co.x*.035,v.co.y*.04,v.co.z*.035) for p in mesh.polygons: p.use_smooth=True parts.setdefault((parent,name in detail_names),[]).append(o) return o def ellipse(rx,rz,n=16): return [(rx*math.cos(2*math.pi*i/n),rz*math.sin(2*math.pi*i/n)) for i in range(n)] def stroke(name,coords,radius,color,parent): bpy.ops.object.select_all(action='DESELECT') curve=bpy.data.curves.new(name,'CURVE'); curve.dimensions='3D' curve.bevel_depth=radius; curve.bevel_resolution=3 spline=curve.splines.new('BEZIER'); spline.bezier_points.add(len(coords)-1) for point,xyz in zip(spline.bezier_points,coords): point.co=xyz; point.handle_left_type='AUTO'; point.handle_right_type='AUTO' o=bpy.data.objects.new(name,curve); scene.collection.objects.link(o) o.data.materials.append(mats[color]); o.parent=parent bpy.context.view_layer.objects.active=o; o.select_set(True) bpy.ops.object.convert(target='MESH') parts.setdefault((parent,name in detail_names),[]).append(bpy.context.object) body=empty('Body',(0,0,0),root) torso=[(0,.61),(.34,.57),(.49,.40),(.56,.18),(.61,-.10),(.58,-.34),(.48,-.44),(.43,-.55),(.25,-.60),(0,-.54),(-.25,-.60),(-.43,-.55),(-.48,-.44),(-.58,-.34),(-.61,-.10),(-.56,.18),(-.49,.40),(-.34,.57)] body_obj=puff('Continuous wool body',torso,(0,0,1.12),.40,'Red',body) tail=[(0,.23),(.16,.21),(.22,.08),(.20,-.05),(.12,-.18),(0,-.21),(-.16,-.16),(-.23,-.02),(-.20,.13)] puff('Round tail',tail,(0,.41,.92),.21,'Red',body) for side,sign in [('Left',-1),('Right',1)]: leg=empty('Leg'+side,(sign*.28,0,.66),root) puff('Short leg',[(.18,.06),(.20,-.13),(.18,-.40),(-.18,-.40),(-.20,-.13),(-.18,.06)],(0,0,-.04),.19,'Cream',leg) puff('Broad hoof',[(0,.13),(.17,.13),(.21,.04),(.21,-.10),(.14,-.14),(-.15,-.14),(-.21,-.10),(-.21,.04),(-.15,.13)],(0,-.04,-.51),.23,'Ink',leg) arm=empty('Arm'+side,(sign*.48,0,1.55),root) puff('Wool sleeve',[(0,.13),(.18,.06),(.23,-.13),(.18,-.30),(.02,-.34),(-.16,-.28),(-.20,-.08),(-.16,.08)],(sign*.06,0,-.10),.23,'Red',arm) puff('Cream wrist',ellipse(.18,.23),(sign*.14,-.035,-.42),.18,'Cream',arm) puff('Cloven mitten',[(0,.14),(.10,.16),(.19,.07),(.19,-.08),(.09,-.17),(-.08,-.17),(-.18,-.08),(-.18,.07),(-.10,.15)],(sign*.15,-.06,-.58),.18,'Ink',arm) head=empty('Head',(0,0,2.20),root) crown=[(0,.80),(.19,.76),(.30,.64),(.49,.66),(.66,.53),(.69,.33),(.80,.23),(.82,.03),(.76,-.14),(.81,-.30),(.72,-.48),(.54,-.51),(.43,-.64),(.23,-.68),(0,-.65),(-.23,-.68),(-.43,-.64),(-.54,-.51),(-.72,-.48),(-.81,-.30),(-.76,-.14),(-.82,.03),(-.80,.23),(-.69,.33),(-.66,.53),(-.49,.66),(-.30,.64),(-.19,.76)] puff('Continuous wool crown',crown,(0,0,0),.48,'Red',head) face=[(0,.29),(.15,.31),(.25,.39),(.35,.29),(.51,.23),(.57,.03),(.59,-.20),(.48,-.41),(.25,-.50),(0,-.52),(-.25,-.50),(-.48,-.41),(-.59,-.20),(-.57,.03),(-.51,.23),(-.35,.29),(-.25,.39),(-.15,.31)] face_obj=puff('Cream face',face,(0,-.36,0),.29,'Cream',head) def face_y(x,z): hit,loc,normal,index=face_obj.ray_cast(Vector((x,-10,z))-face_obj.location,Vector((0,1,0))) assert hit, (x,z) return loc.y+face_obj.location.y for sign in (-1,1): ear=[(0,.15),(.17,.12),(.28,.01),(.37,-.13),(.31,-.23),(.12,-.24),(-.06,-.16),(-.13,-.02)] puff('Drooping ear',[(sign*x,z) for x,z in ear],(sign*.70,-.015,-.18),.12,'Cream',head) puff('Ram horn',ellipse(.29,.32),(sign*.80,-.18,.29),.23,'Ink',head) spiral=[] for i in range(48): a=.4+i/47*math.pi*1.65; r=.195-i/47*.135 spiral.append((sign*(.80+math.cos(a)*r),-.413,.29+math.sin(a)*r)) stroke('Horn spiral',spiral,.025,'Cream',head) eye_y=face_y(sign*.235,-.075)-.008 puff('Eye',ellipse(.087,.112),(sign*.235,eye_y,-.075),.025,'Ink',head) puff('Eye glint',ellipse(.019,.022),(sign*.235+.025,eye_y-.026,-.045),.006,'Cream',head) puff('Nose',[(0,-.035),(.058,.016),(-.058,.016)],(0,face_y(0,-.245)-.012,-.245),.016,'Ink',head) stroke('Smile',[(x,face_y(x,z)-.012,z) for x,z in [(-.13,-.31),(-.10,-.34),(-.06,-.345),(0,-.30),(.06,-.345),(.10,-.34),(.13,-.31)]],.018,'Ink',head) stroke('Muzzle center',[(0,face_y(0,z)-.012,z) for z in (-.25,-.30)],.016,'Ink',head) puff('Sun emblem',[(.14,0),(.13,.06),(.09,.11),(0,.14),(-.09,.11),(-.13,.06),(-.14,0)],(0,-.402,1.34),.012,'Gold',body) for a in (0,math.pi/4,math.pi/2,3*math.pi/4,math.pi): stroke('Sun ray',[(r*math.cos(a),-.408,1.34+r*math.sin(a)) for r in (.20,.27)],.023,'Gold',body) # Conform the emblem to the chest rather than leaving a floating flat decal. for o in parts[(body,True)]: for v in o.data.vertices: p=v.co+o.location hit,loc,normal,index=body_obj.ray_cast(Vector((p.x,-10,p.z))-body_obj.location,Vector((0,1,0))) if hit: v.co.y=loc.y+body_obj.location.y-.008-o.location.y for (pivot,detail),objects in parts.items(): bpy.ops.object.select_all(action='DESELECT') for o in objects: o.select_set(True) bpy.context.view_layer.objects.active=objects[0]; bpy.ops.object.join() bpy.context.object.name=pivot.name+('Details' if detail else 'Mesh') bpy.context.object['sheepOutline']=not detail bpy.ops.object.select_all(action='DESELECT') for o in scene.objects: o.select_set(True) bpy.context.view_layer.objects.active=root bpy.ops.export_scene.gltf(filepath=ROOT+'/public/characters/red-sheep.glb',use_selection=True,use_active_scene=True,export_format='GLB',export_yup=True,export_extras=True,export_attributes=True) # Blender exports pure emission as PBR emissive. Store the intended flat colors # as standard glTF unlit materials, also understood by the existing web loader. path=ROOT+'/public/characters/red-sheep.glb' data=open(path,'rb').read(); size=struct.unpack_from('