"""Author and bake a dedicated light-grey granite material in Blender. The procedural material is retained for editing; the temporary bake plane is removed. Other terrain textures and the monument's silhouette stay unchanged. Run detail-marker-inscription.py afterwards to rebake the preserved lettering. """ import bpy, json, math from pathlib import Path from mathutils import Vector from mathutils.noise import noise_vector ROOT = Path('D:/project/sunrise') spec = json.loads((ROOT/'scripts/constants/marker-granite.json').read_text()) scene = bpy.data.scenes['Summit slab and marker'] bpy.context.window.scene = scene body = bpy.data.objects['Photo matched summit boulder'] mat = bpy.data.materials.get('Marker grey granite authoring') or bpy.data.materials.new('Marker grey granite authoring') mat.use_nodes = True mat.use_fake_user = True nodes, links = mat.node_tree.nodes, mat.node_tree.links nodes.clear() bs = nodes.new('ShaderNodeBsdfPrincipled') out = nodes.new('ShaderNodeOutputMaterial') links.new(bs.outputs['BSDF'],out.inputs['Surface']) uv = nodes.new('ShaderNodeTexCoord') def noise(scale): n = nodes.new('ShaderNodeTexNoise') n.inputs['Scale'].default_value = scale n.inputs['Detail'].default_value = 2 links.new(uv.outputs['UV'],n.inputs['Vector']) return n grain = noise(spec['grainScale']) fine = noise(spec['fineScale']) cloud = noise(18) ramp = nodes.new('ShaderNodeValToRGB') for i,(position,color) in enumerate(spec['grainColors']): element = ramp.color_ramp.elements[i] if i < 2 else ramp.color_ramp.elements.new(position) element.position = position element.color = color links.new(grain.outputs['Fac'],ramp.inputs['Fac']) crystal = nodes.new('ShaderNodeTexVoronoi') crystal.inputs['Scale'].default_value = spec['crystalScale'] links.new(uv.outputs['UV'],crystal.inputs['Vector']) mica = nodes.new('ShaderNodeValToRGB') mica.color_ramp.elements[0].position = .12 mica.color_ramp.elements[0].color = (.11,.12,.13,1) mica.color_ramp.elements[1].position = .26 mica.color_ramp.elements[1].color = (1,1,1,1) links.new(crystal.outputs['Distance'],mica.inputs['Fac']) mix = nodes.new('ShaderNodeMixRGB') mix.blend_type = next(i.identifier for i in mix.bl_rna.properties['blend_type'].enum_items if i.identifier == 'MULTIPLY') mix.inputs[0].default_value = .8 links.new(ramp.outputs['Color'],mix.inputs[1]) links.new(mica.outputs['Color'],mix.inputs[2]) cloud_ramp = nodes.new('ShaderNodeValToRGB') cloud_ramp.color_ramp.elements[0].position = .3 cloud_ramp.color_ramp.elements[0].color = (.36,.38,.37,1) cloud_ramp.color_ramp.elements[1].position = .65 cloud_ramp.color_ramp.elements[1].color = (1,1,1,1) links.new(cloud.outputs['Fac'],cloud_ramp.inputs['Fac']) weather = nodes.new('ShaderNodeMixRGB') weather.blend_type = mix.blend_type weather.inputs[0].default_value = .5 links.new(mix.outputs[0],weather.inputs[1]) links.new(cloud_ramp.outputs[0],weather.inputs[2]) # The same source as the underlying summit rock anchors the monument's palette. terrain = nodes.new('ShaderNodeTexImage') terrain.image = bpy.data.images.load(str(ROOT/'public/terrain/materials/rock-color.jpg'),check_existing=True) links.new(uv.outputs['UV'],terrain.inputs['Vector']) tone = nodes.new('ShaderNodeHueSaturation') tone.inputs['Saturation'].default_value = spec['terrainSaturation'] tone.inputs['Value'].default_value = spec['terrainBrightness'] links.new(terrain.outputs['Color'],tone.inputs['Color']) slab = bpy.data.objects['SLAB_SURFACE'] root = bpy.data.objects['summit-marker'] nearest = min(slab.data.vertices,key=lambda v:(v.co.x-root.location.x)**2+(v.co.y-root.location.y)**2) loop = next(l for l in slab.data.loops if l.vertex_index == nearest.index) ground_uv = slab.data.uv_layers.active.data[loop.index].uv ground = nodes.new('ShaderNodeTexImage') ground.image = bpy.data.images['Summit unique weathered granite'] ground.inputs['Vector'].default_value = (ground_uv.x,ground_uv.y,0) palette = nodes.new('ShaderNodeMixRGB') palette.inputs[0].default_value = spec['groundPaletteBlend'] links.new(tone.outputs['Color'],palette.inputs[1]) links.new(ground.outputs['Color'],palette.inputs[2]) surface = nodes.new('ShaderNodeMixRGB') surface.inputs[0].default_value = spec['terrainBlend'] links.new(weather.outputs[0],surface.inputs[1]) links.new(palette.outputs[0],surface.inputs[2]) links.new(surface.outputs[0],bs.inputs['Base Color']) bs.inputs['Roughness'].default_value = spec['roughness'] bump = nodes.new('ShaderNodeBump') bump.inputs['Distance'].default_value = spec['relief'] bump.inputs['Strength'].default_value = .65 links.new(grain.outputs['Fac'],bump.inputs['Height']) micro = nodes.new('ShaderNodeBump') micro.inputs['Distance'].default_value = spec['relief']*.18 micro.inputs['Strength'].default_value = .4 terrain_normal = nodes.new('ShaderNodeTexImage') terrain_normal.image = bpy.data.images.load(str(ROOT/'public/terrain/materials/rock-normal.jpg'),check_existing=True) terrain_normal.image.colorspace_settings.name = 'Non-Color' links.new(uv.outputs['UV'],terrain_normal.inputs['Vector']) normal = nodes.new('ShaderNodeNormalMap') normal.inputs['Strength'].default_value = .65 links.new(terrain_normal.outputs['Color'],normal.inputs['Color']) links.new(normal.outputs['Normal'],bump.inputs['Normal']) links.new(fine.outputs['Fac'],micro.inputs['Height']) links.new(bump.outputs['Normal'],micro.inputs['Normal']) links.new(micro.outputs['Normal'],bs.inputs['Normal']) mesh = bpy.data.meshes.new('Temporary granite bake plane') mesh.from_pydata([(0,0,0),(1,0,0),(1,1,0),(0,1,0)],[],[(0,1,2,3)]) mesh.uv_layers.new(name='UVMap') for loop in mesh.loops: mesh.uv_layers.active.data[loop.index].uv = mesh.vertices[loop.vertex_index].co.xy plane = bpy.data.objects.new(mesh.name,mesh) scene.collection.objects.link(plane) mesh.materials.append(mat) for obj in scene.objects: obj.select_set(False) plane.select_set(True) bpy.context.view_layer.objects.active = plane target = nodes.new('ShaderNodeTexImage') nodes.active = target images = {} try: for suffix,bake_type in [('color','DIFFUSE'),('normal','NORMAL')]: name = 'marker-grey-granite-'+suffix image = bpy.data.images.get(name) or bpy.data.images.new(name,width=spec['resolution'],height=spec['resolution'],alpha=False) if suffix == 'normal': image.colorspace_settings.name = 'Non-Color' target.image = image scene.render.bake.use_pass_direct = False scene.render.bake.use_pass_indirect = False scene.render.bake.use_pass_color = True bpy.ops.object.bake(type=next(i.identifier for i in bpy.ops.object.bake.get_rna_type().properties['type'].enum_items if i.identifier == bake_type)) image.filepath_raw = str(ROOT/'public/terrain/materials'/(name+'.png')) image.file_format = next(i.identifier for i in image.bl_rna.properties['file_format'].enum_items if i.identifier == 'PNG') image.save() image.pack() images[suffix] = image finally: bpy.data.objects.remove(plane,do_unlink=True) bpy.data.meshes.remove(mesh) # Replace only this monument's source textures; surrounding rocks keep theirs. for i,material in enumerate(list(body.data.materials)): if material.users > 1: material = material.copy() body.data.materials[i] = material for n in material.node_tree.nodes: if n.type != 'TEX_IMAGE' or not n.image: continue if n.image.name.startswith(('rock-color','marker-grey-granite-color')): n.image = images['color'] elif n.image.name.startswith(('rock-normal','marker-grey-granite-normal')): n.image = images['normal'] shader = next(n for n in material.node_tree.nodes if n.type == 'BSDF_PRINCIPLED') shader.inputs['Roughness'].default_value = spec['roughness'] # Recompute neutral weathering, rather than repeatedly brightening the old tint. colors = body.data.color_attributes['Granite weathering'] for v in body.data.vertices: x,y,z = v.co panel = abs((x-.18)/max(.16,.57-.18*z)) rim = min(1,max(0,(panel-.8)/.4)) rim = rim*rim*(3-2*rim) weathering = .045*noise_vector(Vector((x*9,z*9,2.7)))[0] seam = .05*math.exp(-((x+.32-.18*z)/.07)**2) flecks = noise_vector(Vector((x*31,z*31,y*31)))[0] patina = .20*max(0,min(1,(flecks-.05)*3))*rim cleft = .12*math.exp(-((x+.18-.12*z)/.085)**2) grey = spec['faceTint']*(1-rim)+spec['rimTint']*rim+weathering-seam-patina-cleft colors.data[v.index].color = (grey,grey,grey,1) body['graniteFinish'] = 'Light neutral grey quartz/feldspar, dark mica specks, granular microrelief; photo-guided' print('Dedicated grey granite baked; marker source nodes updated, geometry and inscription unchanged.')