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>
99 lines
5.3 KiB
Python
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
|