feat: initialize sunrise with 100-player shared rooms
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
"""Photo-guided Blender study. Run through Blender MCP; export only the rock collection.
|
||||
|
||||
Blender: X east, Y north, Z up, origin at radar base. Geometry and sizes are estimates.
|
||||
The existing facility scene is retained. Source is saved to its own .blend file.
|
||||
"""
|
||||
import bpy, bmesh, math, json, random
|
||||
from pathlib import Path
|
||||
from mathutils import Vector, noise
|
||||
|
||||
ROOT = Path('D:/project/sunrise')
|
||||
OUT = ROOT / 'assets/summit/blender-rock-study'
|
||||
context = json.loads((OUT/'context.json').read_text(encoding='utf-8'))
|
||||
SCENE_NAME = 'Photo-guided summit rocks'
|
||||
if bpy.data.scenes.get(SCENE_NAME):
|
||||
raise RuntimeError('Study already exists; edit it in place rather than rebuilding.')
|
||||
scene = bpy.data.scenes.new(SCENE_NAME)
|
||||
bpy.context.window.scene = scene
|
||||
rocks = bpy.data.collections.new('Photo rocks - export')
|
||||
study = bpy.data.collections.new('Terrain and radar - context only')
|
||||
scene.collection.children.link(rocks)
|
||||
scene.collection.children.link(study)
|
||||
|
||||
def enum(owner, prop, wanted):
|
||||
return next(item.identifier for item in owner.bl_rna.properties[prop].enum_items if item.identifier == wanted)
|
||||
|
||||
def material(name, color):
|
||||
mat = bpy.data.materials.new(name)
|
||||
mat.use_nodes = True
|
||||
node = next(n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED')
|
||||
node.inputs['Base Color'].default_value = (*color,1)
|
||||
node.inputs['Roughness'].default_value = .94
|
||||
mat.diffuse_color = (*color,1)
|
||||
return mat
|
||||
|
||||
granite = material('Photo study / weathered granite', (.44,.43,.38))
|
||||
nodes=granite.node_tree.nodes
|
||||
bsdf=next(n for n in nodes if n.type=='BSDF_PRINCIPLED')
|
||||
tex=nodes.new('ShaderNodeTexImage')
|
||||
tex.image=bpy.data.images.load(str(ROOT/'public/terrain/materials/rock-color.jpg'),check_existing=True)
|
||||
granite.node_tree.links.new(tex.outputs['Color'],bsdf.inputs['Base Color'])
|
||||
|
||||
def mesh_object(name, vertices, faces, collection, mat):
|
||||
mesh=bpy.data.meshes.new(name)
|
||||
mesh.from_pydata(vertices,[],faces);mesh.update()
|
||||
obj=bpy.data.objects.new(name,mesh);collection.objects.link(obj)
|
||||
mesh.materials.append(mat)
|
||||
return obj
|
||||
|
||||
def sculpt_loft(name, outline, levels, seed, relief=.18):
|
||||
# Non-convex plan retains deep vertical joints instead of rounding them into potatoes.
|
||||
rng=random.Random(seed)
|
||||
count=len(outline)
|
||||
center=Vector((sum(p[0] for p in outline)/count,sum(p[1] for p in outline)/count))
|
||||
verts=[]
|
||||
offsets=[rng.uniform(-.75,.75) for _ in outline]
|
||||
for level,(z,scale,dx,dy) in enumerate(levels):
|
||||
for j,(x,y) in enumerate(outline):
|
||||
verts.append((center.x+(x-center.x)*scale+dx,
|
||||
center.y+(y-center.y)*scale+dy,z+offsets[j]*(.3 if level==0 else 1)))
|
||||
faces=[]
|
||||
for level in range(len(levels)-1):
|
||||
for j in range(count):
|
||||
k=(j+1)%count
|
||||
faces.append((level*count+j,level*count+k,(level+1)*count+k,(level+1)*count+j))
|
||||
faces.append(tuple(reversed(range(count))))
|
||||
faces.append(tuple((len(levels)-1)*count+j for j in range(count)))
|
||||
obj=mesh_object(name,verts,faces,rocks,granite)
|
||||
bm=bmesh.new();bm.from_mesh(obj.data)
|
||||
bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces))
|
||||
bmesh.ops.triangulate(bm,faces=list(bm.faces))
|
||||
bmesh.ops.subdivide_edges(bm,edges=list(bm.edges),cuts=3,use_grid_fill=True)
|
||||
bm.normal_update()
|
||||
for vertex in bm.verts:
|
||||
p=vertex.co.copy()
|
||||
# Small weathering preserves the authored fracture planes and crest silhouette.
|
||||
amount=noise.noise_vector(p*.72+Vector((seed,0,0))).x*relief
|
||||
vertex.co+=vertex.normal*amount
|
||||
bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces))
|
||||
bm.to_mesh(obj.data);bm.free();obj.data.update()
|
||||
uv=obj.data.uv_layers.new(name='Granite metre UV')
|
||||
colors=obj.data.color_attributes.new(name='Weathering',type=enum(bpy.types.Attribute,'data_type','FLOAT_COLOR'),domain=enum(bpy.types.Attribute,'domain','CORNER'))
|
||||
for face in obj.data.polygons:
|
||||
n=face.normal
|
||||
# Stable projection per face, at the same 3m texture scale as the app ground.
|
||||
axis=max(range(3),key=lambda a:abs(n[a]))
|
||||
for loop_index in face.loop_indices:
|
||||
p=obj.data.vertices[obj.data.loops[loop_index].vertex_index].co
|
||||
uv.data[loop_index].uv=((p.y,p.z) if axis==0 else (p.x,p.z) if axis==1 else (p.x,p.y))
|
||||
uv.data[loop_index].uv/=3
|
||||
shade=.86+.10*noise.noise_vector(p*.22+Vector((seed,0,0))).x
|
||||
colors.data[loop_index].color=(shade,shade*.99,shade*.95,1)
|
||||
face.use_smooth=True
|
||||
# Auto-smooth via sharp edges: small surface weathering smooth, major joints retain ridges.
|
||||
bm=bmesh.new();bm.from_mesh(obj.data)
|
||||
for edge in bm.edges:
|
||||
if edge.is_manifold and edge.calc_face_angle()>math.radians(35):edge.smooth=False
|
||||
bm.to_mesh(obj.data);bm.free()
|
||||
obj['reference']='User photos; authored fracture outline, estimated metres'
|
||||
return obj
|
||||
|
||||
# Radar buttress: a continuous narrow crown opening out into a broad fractured wall.
|
||||
sculpt_loft('Radar / north granite wall',
|
||||
[(-9,-4),(-5,-7),(4,-6),(9,-2),(10,5),(7,8),(4,7),(3,10),(-1,12),(-2,9),(-5,11),(-9,7)],
|
||||
[(-34,1.5,1,7),(-20,1.25,.8,3),(-10,1.08,0,1),(-3,.91,0,0),(-.7,.67,0,-1)],11,.15)
|
||||
# One connected western rock. Concave outline produces the vertical clefts in the photo.
|
||||
sculpt_loft('Western outcrop / rooted jointed wall',
|
||||
[(-25,0),(-20,-3),(-14,-2),(-11,2),(-12,6),(-14,7),(-13,10),(-16,12),(-17,9),(-20,13),(-23,11),(-22,7),(-25,6)],
|
||||
[(-33,1.38,0,7),(-20,1.2,0,3),(-10,1,0,1),(-2,.86,0,0),(2,.57,.2,-1)],23,.19)
|
||||
# The thin upper slab is rooted well inside the wall; a slanted crown, no symmetric apex.
|
||||
sculpt_loft('Western outcrop / leaning crown slab',
|
||||
[(-19,-1),(-16,-2),(-14,0),(-14,3),(-16,5),(-18,4),(-19,2)],
|
||||
[(-7,1.1,0,0),(0,1,0,0),(5,.69,-.7,-.4),(9,.31,-1.7,-.8)],31,.10)
|
||||
sculpt_loft('Western outcrop / broken lower shoulder',
|
||||
[(-26,1),(-22,-1),(-20,2),(-20,6),(-22,8),(-25,7)],
|
||||
[(-20,1.15,0,2),(-8,1,0,0),(0,.67,.4,-.4),(3,.27,1,-.7)],47,.16)
|
||||
# Sloping foot is a ridge, not a row of freestanding pillars.
|
||||
sculpt_loft('Western outcrop / descending rock apron',
|
||||
[(-28,12),(-23,10),(-18,13),(-15,20),(-15,27),(-20,34),(-25,33),(-29,24)],
|
||||
[(-38,1.15,0,0),(-27,1,0,0),(-20,.77,0,-2),(-13,.40,0,-5)],59,.25)
|
||||
|
||||
sculpt_loft('Western / fractured front ledge',[(-23,8),(-20,7),(-17,9),(-17,12),(-19,14),(-23,12)],[(-19,1.1,0,0),(-11,1,0,0),(-7,.72,0,-.3)],71,.22)
|
||||
sculpt_loft('Western / angular shoulder plates',[(-16,6),(-13,5),(-11,8),(-12,11),(-15,12),(-17,10)],[(-15,1.12,0,0),(-6,1,0,0),(-2,.6,-.6,-.7)],81,.2)
|
||||
sculpt_loft('Western / low bedding shelf',[(-26,16),(-22,14),(-19,17),(-20,21),(-24,23),(-27,20)],[(-29,1.12,0,0),(-20,1,0,0),(-16,.62,.3,-.4)],91,.23)
|
||||
sculpt_loft('Radar / split foreground ledge',[(2,9),(5,7),(8,10),(7,14),(4,15),(1,13)],[(-26,1.15,0,0),(-16,1,0,0),(-11,.63,-.6,-1)],101,.17)
|
||||
|
||||
# Match actual terrain contact and camera. This collection is excluded from the GLB.
|
||||
rows,cols=context['rows'],context['columns']
|
||||
faces=[]
|
||||
for row in range(rows-1):
|
||||
for col in range(cols-1):
|
||||
a=row*cols+col;faces.append((a,a+1,a+1+cols,a+cols))
|
||||
terrain=mesh_object('App terrain / contact reference',context['vertices'],faces,study,material('Study terrain',(.21,.26,.16)))
|
||||
root=bpy.data.objects.get('weather-radar')
|
||||
copies={}
|
||||
for source in [root,*root.children_recursive]:
|
||||
obj=source.copy()
|
||||
if source.data:obj.data=source.data.copy()
|
||||
study.objects.link(obj);copies[source]=obj
|
||||
for source,obj in copies.items():
|
||||
obj.parent=copies.get(source.parent)
|
||||
copies[root].location=(0,0,0)
|
||||
|
||||
camera_data=bpy.data.cameras.new('Photo comparison camera')
|
||||
camera=bpy.data.objects.new(camera_data.name,camera_data);study.objects.link(camera)
|
||||
camera.location=Vector(context['eye'])
|
||||
camera.rotation_euler=(Vector((-8,7,-6))-camera.location).to_track_quat('-Z','Y').to_euler()
|
||||
camera_data.lens=43;scene.camera=camera
|
||||
scene.render.resolution_x=1200;scene.render.resolution_y=850;scene.render.resolution_percentage=100
|
||||
scene.world=bpy.data.worlds.new('Photo study sky');scene.world.use_nodes=True
|
||||
bg=next(n for n in scene.world.node_tree.nodes if n.type=='BACKGROUND')
|
||||
bg.inputs['Color'].default_value=(.55,.64,.72,1);bg.inputs['Strength'].default_value=.65
|
||||
sun_data=bpy.data.lights.new('Photo study sun',enum(bpy.types.Light,'type','SUN'))
|
||||
sun_data.energy=2.2;sun_data.angle=.15
|
||||
sun=bpy.data.objects.new(sun_data.name,sun_data);study.objects.link(sun)
|
||||
sun.rotation_euler=(math.radians(32),math.radians(-25),math.radians(-45))
|
||||
try:scene.render.engine='CYCLES'
|
||||
except TypeError as exc:print(exc)
|
||||
scene.cycles.samples=24
|
||||
|
||||
# Reference images remain local and are not exported with the model.
|
||||
for i,path in enumerate([
|
||||
'C:/Users/horoli/AppData/Local/Temp/codex-clipboard-453c752b-f410-4d6b-97ad-8056e3dac06d.png',
|
||||
'C:/Users/horoli/AppData/Local/Temp/codex-clipboard-8cc46291-7018-4d33-a23e-553db9844d43.png']):
|
||||
if Path(path).exists():
|
||||
obj=bpy.data.objects.new('Photo reference '+str(i+1),None);study.objects.link(obj)
|
||||
obj.empty_display_type=enum(bpy.types.Object,'empty_display_type','IMAGE')
|
||||
obj.data=bpy.data.images.load(path,check_existing=True);obj.empty_display_size=30
|
||||
obj.location=(45+i*35,0,5);obj.rotation_euler=(math.pi/2,0,0)
|
||||
|
||||
def export_rocks():
|
||||
import io_scene_gltf2
|
||||
for obj in scene.objects:obj.select_set(False)
|
||||
for obj in rocks.objects:obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active=next(iter(rocks.objects))
|
||||
fmt=next(item[0] for item in io_scene_gltf2.get_format_items(None,bpy.context) if item[0]=='GLB')
|
||||
# App reuses its rock textures; UVs and vertex weathering travel with the geometry.
|
||||
mats=next(item.identifier for item in bpy.ops.export_scene.gltf.get_rna_type().properties['export_materials'].enum_items if item.identifier=='NONE')
|
||||
bpy.ops.export_scene.gltf(filepath=str(ROOT/'public/models/summit/radar-rocks.glb'),export_format=fmt,
|
||||
use_selection=True,use_active_scene=True,export_yup=True,export_apply=True,
|
||||
export_materials=mats,export_animations=False,export_cameras=False,export_lights=False,export_extras=True)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(OUT/'photo-guided-rocks.blend'))
|
||||
print('Exported photo-guided rocks:',sum(len(o.data.polygons) for o in rocks.objects),'faces')
|
||||
|
||||
bpy.app.driver_namespace['photo_rocks']=globals()
|
||||
print('Study ready:',len(rocks.objects),'rock meshes. Original scene retained.')
|
||||
Reference in New Issue
Block a user