Files
sunrise/scripts/sculpt-photo-marker.py
T

81 lines
5.5 KiB
Python

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