Refine Gwanaksan summit marker proportions and inscription

This commit is contained in:
2026-10-31 09:49:29 +09:00
parent 28a45ff8e3
commit 1b8698565a
20 changed files with 543 additions and 10 deletions
+178
View File
@@ -0,0 +1,178 @@
"""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.')