feat: initialize sunrise with 100-player shared rooms

This commit is contained in:
2026-10-30 16:36:35 +09:00
commit 31d6eac4c2
257 changed files with 66063 additions and 0 deletions
+244
View File
@@ -0,0 +1,244 @@
"""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('<I',data,12)[0]
doc=json.loads(data[20:20+size]); binary=data[20+size:]
doc.setdefault('extensionsUsed',[]).append('KHR_materials_unlit')
for material in doc['materials']:
material['pbrMetallicRoughness']={'baseColorFactor':material.pop('emissiveFactor')+[1],'metallicFactor':0,'roughnessFactor':1}
material.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)
# Preview-only backface shells; the application creates its own equivalent outlines.
outline=bpy.data.materials.new('Preview ink outline'); outline.use_nodes=True
nodes=outline.node_tree.nodes; nodes.clear()
out=nodes.new('ShaderNodeOutputMaterial'); mix=nodes.new('ShaderNodeMixShader')
geo=nodes.new('ShaderNodeNewGeometry'); trans=nodes.new('ShaderNodeBsdfTransparent'); ink=nodes.new('ShaderNodeEmission')
ink.inputs['Color'].default_value=mats['Ink'].diffuse_color
links=outline.node_tree.links
links.new(geo.outputs['Backfacing'],mix.inputs[0]); links.new(trans.outputs[0],mix.inputs[1]); links.new(ink.outputs[0],mix.inputs[2]); links.new(mix.outputs[0],out.inputs['Surface'])
outline.surface_render_method='DITHERED'
for o in list(scene.objects):
if o.type!='MESH' or not o.get('sheepOutline',True): continue
hull=o.copy(); hull.data=o.data.copy(); hull.name='PreviewOutline_'+o.name
scene.collection.objects.link(hull)
offset=hull.data.attributes.get('_outline_offset')
for v,item in zip(hull.data.vertices,offset.data): v.co+=item.vector
bm=bmesh.new(); bm.from_mesh(hull.data)
dark=[f for f in bm.faces if hull.data.materials[f.material_index]==mats['Ink']]
bmesh.ops.delete(bm,geom=dark,context='FACES'); bm.to_mesh(hull.data); bm.free()
hull.data.materials.clear(); hull.data.materials.append(outline)
for p in hull.data.polygons: p.material_index=0
scene.world=bpy.data.worlds.new('Sheep cream backdrop'); scene.world.use_nodes=True
background=next(n for n in scene.world.node_tree.nodes if n.type=='BACKGROUND')
background.inputs['Color'].default_value=mats['Cream'].diffuse_color
background.inputs['Strength'].default_value=1
camdata=bpy.data.cameras.new('Sheep presentation'); cam=bpy.data.objects.new('Sheep presentation',camdata)
scene.collection.objects.link(cam); scene.camera=cam
cam.location=(3,-7,2.8); target=Vector((0,0,1.50)); cam.rotation_euler=(target-cam.location).to_track_quat('-Z','Y').to_euler()
camdata.type='ORTHO'; camdata.ortho_scale=3.8
scene.render.engine='CYCLES'; scene.cycles.samples=16
scene.cycles.transparent_max_bounces=32
scene.render.resolution_x=900; scene.render.resolution_y=1000; scene.render.resolution_percentage=100
scene.render.image_settings.file_format='PNG'
bpy.ops.object.select_all(action='DESELECT'); bpy.context.view_layer.objects.active=root
for area in bpy.context.screen.areas:
if area.type=='VIEW_3D':
area.spaces.active.overlay.show_overlays=False
area.spaces.active.shading.type='MATERIAL'
area.spaces.active.region_3d.view_distance=5
area.spaces.active.region_3d.view_location=(0,0,1.5)
area.spaces.active.region_3d.view_rotation=cam.rotation_euler.to_quaternion()
bpy.data.libraries.write(ROOT+'/assets/characters/ivory-lamb-v12.blend',{scene})
print('Rounded ivory lamb exported; dedicated source saved.')