Files
sunrise/scripts/refine-summit-plateau.py
T

77 lines
4.7 KiB
Python

"""Refine the existing Blender slab, preserving a shared regular height lattice."""
import bpy, math, json
import numpy as np
from pathlib import Path
from mathutils import Vector
from mathutils.bvhtree import BVHTree
ROOT=Path('D:/project/sunrise')
s=bpy.data.scenes['Summit slab and marker'];bpy.context.window.scene=s
slab=bpy.data.objects['SLAB_SURFACE']
if slab.get('jointStudyVersion'):raise RuntimeError('Study already refined; edit it in place.')
backup=slab.copy();backup.data=slab.data.copy();backup.name='Slab before joint refinement';s.collection.objects.link(backup);backup.hide_render=True;backup.hide_set(True)
bvh=BVHTree.FromObject(slab,bpy.context.evaluated_depsgraph_get())
outline=bpy.data.objects['WALKABLE_OUTLINE'];poly=[(v.co.x,v.co.y) for v in outline.data.vertices]
joints=[ [(-26,7),(-18,10),(-9,12),(0,11),(12,15)], [(-26,22),(-17,25),(-8,26),(1,29),(14,28)], [(-24,39),(-14,40),(-4,43),(6,42),(14,46)], [(-20,54),(-10,51),(0,54),(11,53)], [(-18,-7),(-15,4),(-11,12),(-14,23),(-9,33),(-11,43),(-7,57)], [(3,-9),(1,3),(3,11),(-1,21),(2,29),(0,40),(5,53)] ]
def distances(x,y,lines):
out=np.full(np.broadcast(x,y).shape,1e5)
for line in lines:
for (ax,ay),(bx,by) in zip(line,line[1:]):
dx=bx-ax;dy=by-ay;t=np.clip(((x-ax)*dx+(y-ay)*dy)/(dx*dx+dy*dy),0,1)
out=np.minimum(out,np.hypot(x-ax-t*dx,y-ay-t*dy))
return out
def edge_distance(x,y):
d=distances(x,y,[poly+[poly[0]]]);hit=np.zeros(np.broadcast(x,y).shape,dtype=bool)
for (ax,ay),(bx,by) in zip(poly,poly[1:]+poly[:1]):
if ay!=by:hit^=((ay>y)!=(by>y))&(x<(bx-ax)*(y-ay)/(by-ay)+ax)
return np.where(hit,-d,d)
def sculpt(x,y):
d=edge_distance(x,y);fade=1-np.clip((d+1)/5,0,1);fade=fade*fade*(3-2*fade)
cracks=distances(x,y,joints)
roll=.72*np.sin(x*.19+y*.055)*np.cos(y*.13)+.36*np.sin(y*.33+x*.09)
shoulders=.42*np.tanh((y-18-.18*x)/.85)+.32*np.tanh((y-38+.13*x)/.7)
cut=.50*np.exp(-(cracks/.38)**2)+.15*np.exp(-(cracks/1.2)**2)
lip=-.7*np.exp(-((d+.25)/1.1)**2)
return fade*(roll+shoulders-cut+lip)
# Ten subdivisions per original 5m interval. Retain every original XY coordinate.
xs=sorted(set(round(v.co.x,6) for v in slab.data.vertices));ys=sorted(set(round(v.co.y,6) for v in slab.data.vertices))
xs=[a+(b-a)*j/10 for a,b in zip(xs,xs[1:]) for j in range(10)]+[xs[-1]]
ys=[a+(b-a)*j/10 for a,b in zip(ys,ys[1:]) for j in range(10)]+[ys[-1]]
verts=[]
for y in ys:
for x in xs:
hit=bvh.ray_cast(Vector((x,y,100)),Vector((0,0,-1)))[0]
if hit is None:hit=bvh.find_nearest(Vector((x,y,0)))[0]
verts.append((x,y,hit.z+float(sculpt(np.asarray(x),np.asarray(y)))))
nx=len(xs);faces=[]
for j in range(len(ys)-1):
for i in range(nx-1):
a=j*nx+i;faces.extend([(a,a+1,a+nx),(a+1,a+nx+1,a+nx)])
mesh=bpy.data.meshes.new('Sculpted jointed granite lattice');mesh.from_pydata(verts,[],faces);mesh.update();slab.data=mesh
# Unique atlas: warm granite grains, large weathering patches and matching fissures.
N=1536
X,Y=np.meshgrid(np.linspace(xs[0],xs[-1],N),np.linspace(ys[0],ys[-1],N))
D=distances(X,Y,joints)
rng=np.random.default_rng(720)
weather=.035*np.sin(X*.41+np.sin(Y*.17))+.025*np.cos(Y*.57-X*.13)
grain=rng.normal(0,.018,(N,N));fleck=(rng.random((N,N))>.96)*-.055
crack=.22*np.exp(-(D/.16)**2)+.065*np.exp(-(D/.7)**2)
rgb=np.stack([.64+weather+grain+fleck-crack,.59+weather+grain+fleck-crack,.49+weather+grain+fleck-crack],axis=-1)
rgba=np.concatenate([np.clip(rgb,0,1),np.ones((N,N,1))],axis=2).astype(np.float32)
im=bpy.data.images.new('Summit unique weathered granite',width=N,height=N,alpha=True);im.pixels.foreach_set(rgba.ravel());im.update()
im.filepath_raw=str(ROOT/'public/terrain/materials/plateau-color.png')
im.file_format=next(i.identifier for i in im.bl_rna.properties['file_format'].enum_items if i.identifier=='PNG');im.save()
mat=bpy.data.materials.new('Summit unique granite atlas');mat.use_nodes=True
bsdf=next(n for n in mat.node_tree.nodes if n.type=='BSDF_PRINCIPLED');bsdf.inputs['Roughness'].default_value=.92
tex=mat.node_tree.nodes.new('ShaderNodeTexImage');tex.image=im;mat.node_tree.links.new(tex.outputs['Color'],bsdf.inputs['Base Color']);mesh.materials.append(mat)
uv=mesh.uv_layers.new(name='Unique plateau atlas')
for p in mesh.polygons:
p.use_smooth=True
for li in p.loop_indices:
v=mesh.vertices[mesh.loops[li].vertex_index].co;uv.data[li].uv=((v.x-xs[0])/(xs[-1]-xs[0]),(v.y-ys[0])/(ys[-1]-ys[0]))
slab['jointStudyVersion']=1
slab['atlasBounds']=[xs[0],-ys[-1],xs[-1],-ys[0]]
marker=bpy.data.objects['summit-marker'];marker.location.z+=float(sculpt(np.asarray(marker.location.x),np.asarray(marker.location.y)))
print('Sculpted',len(verts),'vertices;',len(faces),'triangles; six intersecting fracture traces and weathered edge lip')