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