Refine Gwanaksan summit marker proportions and inscription
This commit is contained in:
@@ -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.')
|
||||
Reference in New Issue
Block a user