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
+194
View File
@@ -0,0 +1,194 @@
"""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('<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.ops.wm.save_as_mainfile(filepath=ROOT+'/assets/characters/red-sheep-v2.blend',copy=True)
print('V2 exported, continuous surfaces, four unlit materials. Preview source saved.')