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
+106
View File
@@ -0,0 +1,106 @@
"""Blender MCP study: shared summit slab, editable outline and summit stone."""
import bpy,bmesh,json,math
from pathlib import Path
from mathutils import Vector
ROOT=Path('D:/project/sunrise')
OUT=ROOT/'assets/summit/blender-plateau-study'
data=json.loads((OUT/'context.json').read_text(encoding='utf-8'))
if bpy.data.scenes.get('Summit slab and marker'):
raise RuntimeError('Edit the existing study; do not overwrite it by rebuilding.')
scene=bpy.data.scenes.new('Summit slab and marker');bpy.context.window.scene=scene
collection=bpy.data.collections.new('Summit slab authoring');scene.collection.children.link(collection)
def enum(owner,prop,value):
return next(i.identifier for i in owner.bl_rna.properties[prop].enum_items if i.identifier==value)
def mat(name,color):
m=bpy.data.materials.new(name);m.use_nodes=True
n=next(n for n in m.node_tree.nodes if n.type=='BSDF_PRINCIPLED')
n.inputs['Base Color'].default_value=(*color,1);n.inputs['Roughness'].default_value=.95
m.diffuse_color=(*color,1);return m
stone=mat('Summit pale weathered granite',(.58,.55,.47))
texture=stone.node_tree.nodes.new('ShaderNodeTexImage')
texture.image=bpy.data.images.load(str(ROOT/'public/terrain/materials/rock-color.jpg'),check_existing=True)
stone.node_tree.links.new(texture.outputs['Color'],next(n for n in stone.node_tree.nodes if n.type=='BSDF_PRINCIPLED').inputs['Base Color'])
earth=mat('Plateau transition context',(.19,.24,.14))
ink=mat('Recessed summit inscription',(.025,.025,.022))
def objmesh(name,verts,faces):
mesh=bpy.data.meshes.new(name);mesh.from_pydata(verts,[],faces);mesh.update()
obj=bpy.data.objects.new(name,mesh);collection.objects.link(obj);return obj
# X east, Y north, Z above 620m. Editable boundary is the single source of truth.
boundary=[(-17,-59),(-6,-61),(4,-61),(11,-56),(15,-49),(15,-40),(15,-33),
(12,-23),(11,-16),(10,-5),(9,6),(2,14),(-7,14),(-12,12),(-18,4),
(-22,-4),(-25,-13),(-27,-22),(-26,-31),(-25,-40),(-22,-50)]
outline=objmesh('WALKABLE_OUTLINE',[(x,-z,0) for x,z in boundary],[])
edges=[(i,(i+1)%len(boundary)) for i in range(len(boundary))]
outline.data.clear_geometry();outline.data.from_pydata([(x,-z,0) for x,z in boundary],edges,[])
outline.hide_render=True;outline.display_type=enum(bpy.types.Object,'display_type','WIRE')
def signed_distance(x,y):
hit=False;distance=1e9
points=[(a,-b) for a,b in boundary]
for i,(ax,ay) in enumerate(points):
bx,by=points[(i+1)%len(points)];dx=bx-ax;dy=by-ay
t=max(0,min(1,((x-ax)*dx+(y-ay)*dy)/(dx*dx+dy*dy)))
distance=min(distance,math.hypot(x-ax-t*dx,y-ay-t*dy))
if (ay>y)!=(by>y) and x<(bx-ax)*(y-ay)/(by-ay)+ax:hit=not hit
return -distance if hit else distance
def slab_height(x,north):
# Broad photograph-like shoulder and gentle exposed slope, with shallow joints.
ramp=max(-14,min(49,north))*.185
shoulder=.30*(math.tanh((north-27)/6)-math.tanh(-27/6))
weather=.10*math.sin(x*.25)*math.sin(north*.19)
joint=-.12*math.exp(-((x+.18*north+6)/1.1)**2)
return ramp+shoulder+weather+joint
vertices=[]
for x,north,elevation in data['vertices']:
d=signed_distance(x,north)
t=max(0,min(1,d/7));blend=1-t*t*(3-2*t)
height=(elevation-620)*(1-blend)+slab_height(x,north)*blend
vertices.append((x,north,height))
slab=objmesh('SLAB_SURFACE',vertices,data['faces']);slab.data.materials.append(stone);slab.data.materials.append(earth)
uv=slab.data.uv_layers.new(name='Granite UV metres')
for face in slab.data.polygons:
center=sum((slab.data.vertices[i].co for i in face.vertices),Vector())/len(face.vertices)
face.material_index=0 if signed_distance(center.x,center.y)<0 else 1
face.use_smooth=True
for li in face.loop_indices:
p=slab.data.vertices[slab.data.loops[li].vertex_index].co;uv.data[li].uv=(p.x/3,p.y/3)
slab['heightDatum']=620
slab['note']='App reuses these exact terrain vertices for rendered surface and walking height.'
root=bpy.data.objects.new('summit-marker',None);collection.objects.link(root)
root.location=(-12,22,slab_height(-12,22))
root['blockedRadius']=1.1
# Irregular broad stone with a cut face for the vertical inscription; no pedestal.
front=[(-.82,-.43,-.10),(.76,-.43,-.10),(.65,-.43,1.72),(.35,-.43,2.12),(-.50,-.43,1.94)]
back=[(-.65,.40,-.12),(.61,.36,-.12),(.53,.26,1.68),(.28,.17,2.05),(-.40,.28,1.86)]
faces=[tuple(reversed(range(5))),tuple(range(5,10))]+[(i,(i+1)%5,(i+1)%5+5,i+5) for i in range(5)]
body=objmesh('Marker granite body',front+back,faces);body.parent=root;body.data.materials.append(stone)
bpy.context.view_layer.objects.active=body;body.select_set(True)
bevel=body.modifiers.new('Weathered stone edges',enum(bpy.types.Modifier,'type','BEVEL'));bevel.width=.07;bevel.segments=2
bpy.ops.object.modifier_apply(modifier=bevel.name)
bm=bmesh.new();bm.from_mesh(body.data);bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces));bm.to_mesh(body.data);bm.free()
uv=body.data.uv_layers.new(name='Stone UV')
for face in body.data.polygons:
for li in face.loop_indices:
p=body.data.vertices[body.data.loops[li].vertex_index].co;uv.data[li].uv=(p.x/1.3,p.z/1.3)
font=bpy.data.fonts.load('C:/Windows/Fonts/malgunbd.ttf')
for i,letter in enumerate('관악산'):
curve=bpy.data.curves.new('Inscription '+letter,next(t.identifier for t in bpy.data.curves.bl_rna.functions['new'].parameters['type'].enum_items if t.identifier=='FONT'))
curve.body=letter;curve.font=font;curve.size=.42;curve.extrude=.005
curve.align_x=enum(bpy.types.TextCurve,'align_x','CENTER')
o=bpy.data.objects.new('Inscription '+letter,curve);collection.objects.link(o);o.parent=root
o.location=(0,-.435,1.48-i*.47);o.rotation_euler=(math.pi/2,0,0);curve.materials.append(ink)
for other in scene.objects:other.select_set(False)
o.select_set(True);bpy.context.view_layer.objects.active=o
bpy.ops.object.convert(target=next(t.identifier for t in bpy.ops.object.convert.get_rna_type().properties['target'].enum_items if t.identifier=='MESH'))
camdata=bpy.data.cameras.new('Plateau inspection camera');cam=bpy.data.objects.new(camdata.name,camdata);collection.objects.link(cam)
cam.location=(12,-18,17);cam.rotation_euler=(Vector((-9,23,4))-cam.location).to_track_quat('-Z','Y').to_euler();camdata.lens=38;scene.camera=cam
scene.render.resolution_x=1200;scene.render.resolution_y=850;scene.render.resolution_percentage=80
scene.world=bpy.data.worlds.new('Plateau daylight');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=(.58,.66,.76,1);background.inputs['Strength'].default_value=.7
light=bpy.data.lights.new('Slab sun',enum(bpy.types.Light,'type','SUN'));light.energy=2;light.angle=.2
sun=bpy.data.objects.new(light.name,light);collection.objects.link(sun);sun.rotation_euler=(.6,-.4,-.5)
scene.render.engine='CYCLES';scene.cycles.samples=24
bpy.app.driver_namespace['summit_plateau']=globals()
print('Blender plateau:',len(vertices),'shared height vertices; boundary:',len(boundary),'points')