"""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('Ivory baby lamb - soft features') bpy.context.window.scene=scene scene.view_settings.view_transform='Standard' palette={'Red':'F5EDDD','Cream':'F0E1CD','Ink':'67564C','Gold':'BE6264'} 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','Scarf end'} 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 fuse_wool(base,clumps,parent): bpy.ops.object.select_all(action='DESELECT') base.select_set(True) for obj in clumps: parts[(parent,False)].remove(obj) obj.select_set(True) bpy.context.view_layer.objects.active=base bpy.ops.object.join() remesh=base.modifiers.new('Joined fleece','REMESH') remesh.mode='VOXEL'; remesh.voxel_size=.022; remesh.use_smooth_shade=True bpy.ops.object.modifier_apply(modifier=remesh.name) smooth=base.modifiers.new('Soft fleece roots','SMOOTH') smooth.factor=1; smooth.iterations=3 bpy.ops.object.modifier_apply(modifier=smooth.name) base.data.update() old=base.data.attributes.get('_outline_offset') if old: base.data.attributes.remove(old) offset=base.data.attributes.new('_outline_offset','FLOAT_VECTOR','POINT') for vertex,item in zip(base.data.vertices,offset.data): item.vector=vertex.normal*.014 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) body.scale.y=1.20 body_obj=puff('Fleece core',ellipse(.47,.49),(0,0,1.00),.40,'Red',body) body_clumps=[] for row,(z,rx,ry,count) in enumerate([(.66,.34,.28,8),(.96,.43,.35,10),(1.25,.34,.28,8)]): for i in range(count): a=2*math.pi*(i+.43*row)/count size=[.24,.265,.23,.25][(i+row)%4] body_clumps.append(puff('Body fleece lock',ellipse(size,size*1.08), (rx*math.cos(a),ry*math.sin(a),z+.025*math.sin(i*2.3)),size*.94,'Red',body)) fuse_wool(body_obj,body_clumps,body) puff('Tucked tail',ellipse(.10,.135),(0,.52,.79),.115,'Red',body) for side,sign in [('Left',-1),('Right',1)]: leg=empty('Leg'+side,(sign*.29,0,.22),root) leg.scale.y=1.15 puff('Hidden wool leg',ellipse(.175,.12),(0,0,-.025),.21,'Red',leg) puff('Small hoof tip',ellipse(.16,.075),(0,-.025,-.145),.19,'Ink',leg) arm=empty('Arm'+side,(sign*.51,0,1.43),root) arm.scale.y=1.15 puff('Soft wool arm',[(0,.17),(.16,.10),(.19,-.12),(.16,-.30),(.09,-.36),(-.09,-.36),(-.16,-.26),(-.17,-.06),(-.14,.11)],(sign*.08,0,-.10),.22,'Red',arm) puff('Small hand hoof',ellipse(.13,.085),(sign*.10,-.015,-.465),.15,'Ink',arm) head=empty('Head',(0,0,1.91),root) head.scale=(.88,1.04,.88) crown_obj=puff('Crown fleece core',ellipse(.63,.57),(0,0,0),.48,'Red',head) head_clumps=[] for row,(z,rx,ry,count) in enumerate([(-.29,.48,.37,9),(.08,.51,.41,10),(.43,.39,.30,8)]): for i in range(count): a=2*math.pi*(i+.38*row)/count x,y=rx*math.cos(a),ry*math.sin(a) if y<-.10 and z<.3: continue size=[.25,.28,.24,.265][(i+2*row)%4] head_clumps.append(puff('Crown fleece lock',ellipse(size,size*.95), (x,y,z+.02*math.sin(i*1.8)),size,'Red',head)) for x,y,z,size in [(-.34,-.28,.43,.28),(.02,-.32,.48,.30),(.36,-.25,.43,.26)]: head_clumps.append(puff('Forehead fleece lock',ellipse(size,size*.9),(x,y,z),size,'Red',head)) fuse_wool(crown_obj,head_clumps,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),.23,'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*.8,z*.8) for x,z in ear],(sign*.72,-.08,-.035),.17,'Cream',head) puff('Ram horn',ellipse(.16,.18),(sign*.72,.035,.54),.18,'Ink',head) spiral=[] for i in range(48): a=.4+i/47*math.pi*1.65; r=.105-i/47*.07 spiral.append((sign*(.72+math.cos(a)*r),-.147,.54+math.sin(a)*r)) stroke('Horn spiral',spiral,.014,'Cream',head) eye=puff('Eye',ellipse(.062,.074),(sign*.23,0,-.12),.01,'Ink',head) # Flush markings follow the face instead of protruding like beads. for v in eye.data.vertices: p=v.co+eye.location v.co.y=face_y(p.x,p.z)-.004 puff('Nose',ellipse(.035,.023),(0,face_y(0,-.26)-.004,-.26),.008,'Ink',head) stroke('Smile',[(x,face_y(x,z)-.006,z) for x,z in [(-.055,-.30),(-.028,-.316),(0,-.30),(.028,-.316),(.055,-.30)]],.009,'Ink',head) stroke('Muzzle center',[(0,face_y(0,z)-.006,z) for z in (-.275,-.30)],.009,'Ink',head) # Red scarf: rounded collar and softly rounded tails on the plump chest. bpy.ops.mesh.primitive_torus_add(major_segments=64,minor_segments=12,location=(0,0,1.47),major_radius=.45,minor_radius=.10) collar=bpy.context.object; collar.name='Scarf collar'; collar.scale.y=1.05 bpy.ops.object.transform_apply(location=False,rotation=False,scale=True) collar.parent=body; collar.data.materials.append(mats['Gold']) for p in collar.data.polygons: p.use_smooth=True parts.setdefault((body,True),[]).append(collar) for x,z,rx,rz in [(.25,1.20,.11,.27),(.43,1.30,.09,.19)]: end=puff('Scarf end',[(rx,rz*.8),(rx,-rz*.8),(rx*.6,-rz),(-rx*.6,-rz),(-rx,-rz*.8),(-rx,rz*.8)],(x,0,z),.035,'Gold',body) for vertex in end.data.vertices: p=vertex.co+end.location hit,location,normal,index=body_obj.ray_cast(Vector((p.x,-10,p.z))-body_obj.location,Vector((0,1,0))) vertex.co.y+=(location.y+body_obj.location.y-.025) if hit else -.42 # Lower the intact wool body onto the hoof tips; no exposed leg columns. for child in root.children: if not child.name.startswith('Leg'): child.location.z-=.34 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('