106 lines
5.7 KiB
Python
106 lines
5.7 KiB
Python
"""Run in the existing Blender studies via MCP; preserve walking geometry and facilities.
|
|
|
|
Photo-estimated weathering, not photogrammetry. Existing scan maps are reused.
|
|
"""
|
|
import bpy, math, json
|
|
from pathlib import Path
|
|
from mathutils import Vector, noise
|
|
|
|
ROOT = Path('D:/project/sunrise')
|
|
OUT = ROOT/'assets/summit/near-route-refinement'
|
|
TILE = 1.8
|
|
rocks = bpy.data.collections['Photo rocks - export']
|
|
slab = bpy.data.objects['SLAB_SURFACE']
|
|
before_slab = [tuple(v.co) for v in slab.data.vertices]
|
|
report = []
|
|
|
|
def joint(p):
|
|
# Uneven, slightly wandering vertical fractures; avoid horizontal stacked bands.
|
|
warp = .16*math.sin(p.z*.43+p.y*.27)
|
|
distance = min(abs(p.x+.16*p.y+.07*p.z+warp-k) for k in (-23.4,-19.1,-14.7,-11.2,-5.2,.8,6.5))
|
|
return math.exp(-(distance/.30)**2)
|
|
|
|
for obj in rocks.objects:
|
|
if obj.get('route_detail_pass'):
|
|
raise RuntimeError('Refinement already applied: edit the existing result, do not accumulate.')
|
|
mesh = obj.data
|
|
maximum = 0
|
|
for v in mesh.vertices:
|
|
p = v.co.copy()
|
|
fade = max(0,min(1,(p.z+28)/6))
|
|
# Retain supporting tops and overall silhouette; recess the exposed side faces.
|
|
side = max(0,1-abs(v.normal.z))
|
|
amount = -.14*joint(p)*side*fade
|
|
v.co += v.normal*amount
|
|
maximum = max(maximum,abs(amount))
|
|
mesh.update()
|
|
uv = mesh.uv_layers.active
|
|
colors = mesh.color_attributes.active_color
|
|
for face in mesh.polygons:
|
|
axis = max(range(3),key=lambda a:abs(face.normal[a]))
|
|
for li in face.loop_indices:
|
|
p = mesh.vertices[mesh.loops[li].vertex_index].co
|
|
uv.data[li].uv = Vector((p.y,p.z) if axis==0 else (p.x,p.z) if axis==1 else (p.x,p.y))/TILE
|
|
patch = noise.noise_vector(p*.26).x
|
|
stain = max(0,noise.noise_vector(p*.61+Vector((3,9,2))).y)
|
|
shade = .93+.055*patch-.10*joint(p)-.045*stain
|
|
colors.data[li].color = (shade,shade*.985,shade*.95,1)
|
|
obj['route_detail_pass'] = True
|
|
obj['texture_tile_metres'] = TILE
|
|
report.append({'object':obj.name,'vertices':len(mesh.vertices),'maxRecessMetres':maximum})
|
|
|
|
def setup_material(mat, vertex_colors=False):
|
|
nodes,links = mat.node_tree.nodes,mat.node_tree.links
|
|
bsdf = next(n for n in nodes if n.type=='BSDF_PRINCIPLED')
|
|
color = next(n for n in nodes if n.type=='TEX_IMAGE' and n.image and 'rock-color' in n.image.name)
|
|
# Use identical two-scale sampling in Blender and the application's ground shader.
|
|
texcoord = nodes.new('ShaderNodeTexCoord')
|
|
mapping = nodes.new('ShaderNodeVectorMath');mapping.operation = next(i.identifier for i in mapping.bl_rna.properties['operation'].enum_items if i.identifier=='MULTIPLY_ADD')
|
|
mapping.inputs[1].default_value=(1.37,1.37,1.37)
|
|
mapping.inputs[2].default_value=(.31,.73,0)
|
|
links.new(texcoord.outputs['UV'],mapping.inputs[0])
|
|
separate=nodes.new('ShaderNodeSeparateXYZ');links.new(texcoord.outputs['UV'],separate.inputs[0])
|
|
def math_node(operation,a,b=0):
|
|
node=nodes.new('ShaderNodeMath')
|
|
node.operation=next(i.identifier for i in node.bl_rna.properties['operation'].enum_items if i.identifier==operation)
|
|
for socket,value in zip(node.inputs,(a,b)):
|
|
if isinstance(value,(int,float)):socket.default_value=value
|
|
else:links.new(value,socket)
|
|
return node.outputs[0]
|
|
blend=math_node('ADD',.5,math_node('MULTIPLY',.3,math_node('SINE',math_node('ADD',math_node('MULTIPLY',separate.outputs['X'],.71),math_node('SINE',math_node('MULTIPLY',separate.outputs['Y'],.53))))))
|
|
def mixed(image, original=None):
|
|
a = original or nodes.new('ShaderNodeTexImage');a.image=image
|
|
b = nodes.new('ShaderNodeTexImage');b.image=image
|
|
links.new(mapping.outputs[0],b.inputs['Vector'])
|
|
mix=nodes.new('ShaderNodeMixRGB');links.new(blend,mix.inputs[0])
|
|
links.new(a.outputs['Color'],mix.inputs[1]);links.new(b.outputs['Color'],mix.inputs[2])
|
|
return mix.outputs[0]
|
|
base=mixed(color.image,color)
|
|
if vertex_colors:
|
|
attr=nodes.new('ShaderNodeVertexColor');attr.layer_name='Weathering'
|
|
mul=nodes.new('ShaderNodeMixRGB');mul.blend_type=next(i.identifier for i in mul.bl_rna.properties['blend_type'].enum_items if i.identifier=='MULTIPLY');mul.inputs[0].default_value=1
|
|
links.new(base,mul.inputs[1]);links.new(attr.outputs['Color'],mul.inputs[2]);base=mul.outputs[0]
|
|
links.new(base,bsdf.inputs['Base Color'])
|
|
for kind in ('normal','roughness'):
|
|
img=bpy.data.images.load(str(ROOT/f'public/terrain/materials/rock-{kind}.jpg'),check_existing=True)
|
|
img.colorspace_settings.name='Non-Color'
|
|
signal=mixed(img)
|
|
if kind=='normal':
|
|
normal=nodes.new('ShaderNodeNormalMap');normal.inputs['Strength'].default_value=.55
|
|
links.new(signal,normal.inputs['Color']);links.new(normal.outputs['Normal'],bsdf.inputs['Normal'])
|
|
else:
|
|
links.new(signal,bsdf.inputs['Roughness'])
|
|
|
|
setup_material(bpy.data.materials['Photo study / weathered granite'],True)
|
|
# Copy the material so the summit marker retains its existing authored finish.
|
|
surface=slab.data.materials[0].copy();surface.name='Route slab / weathered granite detail'
|
|
slab.data.materials[0]=surface
|
|
setup_material(surface)
|
|
for face in slab.data.polygons:
|
|
for li in face.loop_indices:
|
|
p=slab.data.vertices[slab.data.loops[li].vertex_index].co
|
|
slab.data.uv_layers.active.data[li].uv=(p.x/TILE,p.y/TILE)
|
|
assert before_slab==[tuple(v.co) for v in slab.data.vertices]
|
|
(OUT/'refinement.json').write_text(json.dumps({'walkingVerticesUnchanged':True,'tileMetres':TILE,'normalStrength':.55,'rocks':report},indent=2))
|
|
print('Refined',len(report),'rock meshes; walking surface vertices unchanged.')
|