"""Photo-guided summit monument: broad natural boulder, recessed face and Hanja.""" import bpy,bmesh,math,random from mathutils import Vector from mathutils.noise import noise_vector from pathlib import Path s=bpy.data.scenes['Summit slab and marker'];bpy.context.window.scene=s root=bpy.data.objects['summit-marker'];collection=root.users_collection[0] if root.get('photoMonument'):raise RuntimeError('Edit the photo monument in place; do not rebuild cumulatively.') for child in list(root.children): child.parent=None;child.name='Previous marker - '+child.name;child.hide_render=True;child.hide_set(True) # Deliberately lopsided shoulder and broad, flared foot traced from supplied reference. outline=[(-1.24,-.10),(-1.18,.22),(-1.04,.55),(-.91,.90),(-.66,1.24),(-.43,1.51),(-.16,1.76),(.03,2.02),(.26,1.98),(.48,1.70),(.61,1.36),(.75,1.00),(.88,.62),(1.04,.28),(1.22,-.06),(.65,-.14),(-.10,-.12),(-.70,-.16)] center=Vector((.06,.85));segments=8;edge=[] for a,b in zip(outline,outline[1:]+outline[:1]): for j in range(segments): t=j/segments;edge.append((a[0]*(1-t)+b[0]*t,a[1]*(1-t)+b[1]*t)) def grain(x,z,scale=18):return noise_vector(Vector((x*scale,z*scale,2.7)))[0] def front(x,z): # A lightly dressed, leaning face, recessed into the original uncut rock. width=max(.16,.57-.18*z);panel=abs((x-.18)/width) rim=min(1,max(0,(panel-.76)/.45)) seam=math.exp(-((x+.35-.19*z)/.045)**2)*.045 return -.48+.13*x+.05*z + rim*(-.13+.075*grain(x,z,5))+.007*grain(x,z,27)+seam verts=[];faces=[];colors=[] N=len(edge);R=36 for back in [False,True]: for k in range(R+1): r=max(.0001,k/R) for ex,ez in edge: x=center.x+(ex-center.x)*r;z=center.y+(ez-center.y)*r y=(.15+.43*(1-r*r)+.075*grain(x,z,7)) if back else front(x,z) verts.append((x,y,z)) panel=abs((x-.18)/max(.16,.57-.18*z)) smooth=min(1,max(0,(panel-.8)/.4));smooth=smooth*smooth*(3-2*smooth) if back:smooth=1 weather=.035*grain(x,z,9)+.022*grain(x,z,75)+.018*grain(x,z,210) stain=.075*math.exp(-((x+.32-.18*z)/.07)**2) base=(.48,.43,.33) if smooth<.5 else (.24,.235,.19) # Continuous lighter inscription field and darker naturally weathered margins. base=tuple(a*(1-smooth)+b*smooth for a,b in zip((.53,.48,.37),(.255,.25,.205))) colors.append((*[max(.06,c+weather-stain) for c in base],1)) offset=(R+1)*N if back else 0 for k in range(R): for j in range(N): a=offset+k*N+j;b=offset+k*N+(j+1)%N;c=a+N;d=b+N faces.extend([(a,b,c),(b,d,c)]) offset=(R+1)*N for j in range(N): a=R*N+j;b=R*N+(j+1)%N;faces.extend([(a,a+offset,b),(b,a+offset,b+offset)]) mesh=bpy.data.meshes.new('Photo-traced natural granite');mesh.from_pydata(verts,[],faces);mesh.update() body=bpy.data.objects.new('Photo matched summit boulder',mesh);collection.objects.link(body);body.parent=root bm=bmesh.new();bm.from_mesh(mesh);bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces));bm.to_mesh(mesh);bm.free() def en(owner,p,v):return next(i.identifier for i in owner.bl_rna.properties[p].enum_items if i.identifier==v) col=mesh.color_attributes.new(name='Granite weathering',type=en(bpy.types.Attribute,'data_type','FLOAT_COLOR'),domain=en(bpy.types.Attribute,'domain','POINT')) for i,c in enumerate(colors):col.data[i].color=c for p in mesh.polygons:p.use_smooth=True mat=bpy.data.materials.new('Monument authored granite');mat.use_nodes=True bs=next(n for n in mat.node_tree.nodes if n.type=='BSDF_PRINCIPLED');bs.inputs['Roughness'].default_value=.96 vc=mat.node_tree.nodes.new('ShaderNodeVertexColor');vc.layer_name=col.name;mat.node_tree.links.new(vc.outputs['Color'],bs.inputs['Base Color']);mesh.materials.append(mat) ink=bpy.data.materials.new('Dark carved Hanja');ink.use_nodes=True bs=next(n for n in ink.node_tree.nodes if n.type=='BSDF_PRINCIPLED');bs.inputs['Base Color'].default_value=(.012,.014,.010,1);bs.inputs['Roughness'].default_value=.93 font=bpy.data.fonts.load('C:/Windows/Fonts/HANBatangB.ttf') for i,ch in enumerate('冠岳山'): c=bpy.data.curves.new('Carved '+ch,next(i.identifier for i in bpy.data.curves.bl_rna.functions['new'].parameters['type'].enum_items if i.identifier=='FONT')) c.body=ch;c.font=font;c.size=.36;c.align_x=en(bpy.types.TextCurve,'align_x','CENTER');c.extrude=.001 o=bpy.data.objects.new(c.name,c);collection.objects.link(o);o.parent=root;o.location=(.17,0,1.44-i*.43);o.rotation_euler=(math.pi/2,0,0);c.materials.append(ink) for other in s.objects:other.select_set(False) o.select_set(True);bpy.context.view_layer.objects.active=o bpy.ops.object.convert(target=next(i.identifier for i in bpy.ops.object.convert.get_rna_type().properties['target'].enum_items if i.identifier=='MESH')) # Conform every glyph vertex to the dressed rock face, no floating flat sign. transform=o.matrix_basis.copy() for v in o.data.vertices: p=transform@v.co;p.y=front(p.x,p.z)-.003;v.co=p o.location=(0,0,0);o.rotation_euler=(0,0,0) root['photoMonument']=True s.camera.location=root.location+Vector((.9,-6.3,2.8));s.camera.rotation_euler=(root.location+Vector((0,0,.95))-s.camera.location).to_track_quat('-Z','Y').to_euler();s.camera.data.lens=55 s.render.resolution_x=1000;s.render.resolution_y=950;s.render.resolution_percentage=100 s.render.filepath='D:/project/sunrise/assets/summit/blender-plateau-study/marker-photo-match.png' bpy.ops.render.render(write_still=True) print('Photo marker:',len(verts),'vertices;',len(faces),'faces; natural shoulders, flared foot, conformed Hanja')