179 lines
8.5 KiB
Python
179 lines
8.5 KiB
Python
"""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.')
|