"""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')