saumwacht-godot/assets-source/phase4/blender/reference_sculpt_detail.py
Martin Stickelbroeck c2b447610c Baseline: Saumwacht 0.15.0 (Phase 20A) vor dem Umbau zu Champions of Luneraria
Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern
unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore
unversioniert; letztere sind unter ../backups gesichert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:00:36 +02:00

99 lines
5.3 KiB
Python

"""Phase 10 sculpt pass: physical eroded stone and furrowed bark.
Adds mid-frequency relief to selected large surfaces. Flat ornaments, crystal
facets, leaf ribs and joint names are preserved. The editable .blend also keeps
an optional high-resolution sculpt modifier stack; the GLB uses the budgeted
game mesh, so twenty copies of a hero do not load its offline sculpt resolution.
"""
import bpy, bmesh
from math import sin, cos
from mathutils import Vector, noise
def sculpt_game_surfaces(spec):
if spec['faction'] not in ['glutpakt','wurzelbund','rift'] and spec['role'] not in ['worker','core']:
return
candidates=[]
for obj in list(bpy.context.scene.objects):
if obj.type!='MESH' or not obj.data.materials: continue
mat=obj.data.materials[0].name
kind='bark' if mat.startswith('SW_HD_W_') and any(k in mat for k in ['bark','wood']) else 'rock' if any(k in mat for k in ['SW_HD_basalt','SW_HD_edge']) else ''
# Tiny strands and thin trim already carry explicit modeled details.
if not kind or len(obj.data.polygons)<20 or any(k in obj.name for k in ['Grain','Vein','Ring','Trim','Rim','Groove','Toe','Fang']): continue
bpy.context.view_layer.update()
if spec["faction"]=="wurzelbund" and any(k in obj.name for k in ["Scale","Rib","Growth","Split"]): continue
extent=max(obj.dimensions)
if extent < .16: continue
candidates.append((extent,obj,kind))
candidates.sort(key=lambda row:row[0],reverse=True)
for extent,obj,kind in candidates[:18]:
bpy.context.view_layer.objects.active=obj
# Bake existing scale, keeping the semantic part transform intact.
from mathutils import Matrix
obj.data.transform(Matrix.Diagonal((*obj.scale,1)))
obj.scale=(1,1,1)
bm=bmesh.new();bm.from_mesh(obj.data)
bmesh.ops.subdivide_edges(bm,edges=list(bm.edges),cuts=1,use_grid_fill=True)
bm.normal_update()
amplitude=min(.014,extent*.023) if kind=='bark' else min(.009,extent*.012)
for v in bm.verts:
p=v.co
n=noise.noise_vector(p*19+Vector((1.7,3.2,.6)))
if kind=='bark':
grain=noise.noise(Vector((p.x*46,p.y*46,p.z*5.5)))
ridges=sin(p.x*103+p.y*81+n.z*1.3)
amount=(grain*.7+ridges*.3)*amplitude
else:
amount=(n.x*.8+noise.noise(p*61)*.2)*amplitude
v.co+=v.normal*amount
bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces))
bm.to_mesh(obj.data);bm.free();obj.data.update()
for face in obj.data.polygons: face.use_smooth=True
obj['sculpt_surface']=kind
obj['relief_depth_m']=amplitude
def add_offline_sculpt_modifiers(objects):
"""Only called AFTER GLB export. Render-visible high mesh remains editable."""
for obj in objects:
if obj.type!='MESH' or not obj.get('sculpt_surface'): continue
subdiv=obj.modifiers.new('HIGH RES · surface refinement','SUBSURF')
subdiv.subdivision_type='SIMPLE';subdiv.levels=1;subdiv.render_levels=2
tex=bpy.data.textures.new(obj.name+'_Microrelief',type='CLOUDS')
tex.noise_scale=.012;tex.noise_depth=2
displace=obj.modifiers.new('HIGH RES · sculpted microrelief','DISPLACE')
displace.texture=tex;displace.strength=.0018;displace.mid_level=.5
def add_offline_materials(objects, project):
"""Editable Blender PBR nodes; optional project maps, standalone-safe fallback.
Runtime shaders remain Godot-authored. Blender sources use the same oak
height map where available, with procedural pores for the other surfaces.
Images are external resources, avoiding 33 duplicate copies of a 4K map.
"""
materials={m for o in objects if o.type=='MESH' for m in o.data.materials if m}
for mat in materials:
if mat.get('phase10_pbr') or not mat.name.startswith('SW_HD_'): continue
nodes=mat.node_tree.nodes;links=mat.node_tree.links
bsdf=nodes.get('Principled BSDF')
if not bsdf or bsdf.inputs['Emission Strength'].default_value>0: continue
coord=nodes.new('ShaderNodeTexCoord');coord.location=(-700,100)
grain=nodes.new('ShaderNodeTexNoise');grain.location=(-470,100)
grain.inputs['Scale'].default_value=65;grain.inputs['Detail'].default_value=4
links.new(coord.outputs['Object'],grain.inputs['Vector'])
height=grain.outputs['Fac']
is_bark=mat.name.startswith('SW_HD_W_') and any(k in mat.name for k in ['bark','wood','groove'])
path=project/'assets/materials/phase10/oak-height-4k.png'
if is_bark and path.exists():
image=nodes.new('ShaderNodeTexImage');image.location=(-460,-150)
image.image=bpy.data.images.load(str(path),check_existing=True)
image.image.colorspace_settings.name='Non-Color'
image.projection='BOX';image.projection_blend=.25
links.new(coord.outputs['Generated'],image.inputs['Vector'])
height=image.outputs['Color']
bump=nodes.new('ShaderNodeBump');bump.location=(-120,-80)
bump.inputs['Strength'].default_value=.32 if is_bark else .16
bump.inputs['Distance'].default_value=.018 if is_bark else .009
links.new(height,bump.inputs['Height']);links.new(bump.outputs['Normal'],bsdf.inputs['Normal'])
mat['phase10_pbr']=True