saumwacht-godot/assets-source/phase4/blender/common.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

148 lines
5.6 KiB
Python

"""Shared primitive helpers. Blender coordinates: +Z up, -Y forward.
Every helper creates one named, flat-shaded part. No joining, textures, scene
deletion or export here. Angles are radians; sizes are metres.
"""
from math import pi
import re
import bpy
from mathutils import Vector
def _finish(obj, name, mat=None):
name = re.sub(r'[^A-Za-z0-9_]', '_', name)
obj.name = name
obj['part_name'] = name
if obj.type == 'MESH':
obj.data.name = name + '_Mesh'
for face in obj.data.polygons:
face.use_smooth = False
if mat is not None:
obj.data.materials.append(mat)
return obj
def box(name, loc, size, mat, rotation=None):
bpy.ops.mesh.primitive_cube_add(size=1, location=loc)
obj = bpy.context.object
obj.scale = size
if rotation is not None:
obj.rotation_euler = rotation
return _finish(obj, name, mat)
def ellipsoid(name, loc, scale, mat, subdivisions=1):
bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=subdivisions, radius=1, location=loc)
obj = bpy.context.object
obj.scale = scale
return _finish(obj, name, mat)
def cylinder(name, loc, radius, depth, mat, vertices=10, rotation=None):
return cone(name, loc, radius, radius, depth, mat, vertices, rotation)
def cone(name, loc, radius1, radius2, depth, mat, vertices=10, rotation=None):
bpy.ops.mesh.primitive_cone_add(vertices=vertices, radius1=radius1,
radius2=radius2, depth=depth, location=loc)
obj = bpy.context.object
if rotation is not None:
obj.rotation_euler = rotation
return _finish(obj, name, mat)
def beam(name, start, end, radius, mat, vertices=8):
start, end = Vector(start), Vector(end)
direction = end - start
if direction.length < 1e-6:
raise ValueError(name + ': beam endpoints must differ')
obj = cylinder(name, (start + end) * 0.5, radius, direction.length, mat, vertices)
obj.rotation_euler = direction.to_track_quat('Z', 'Y').to_euler()
return obj
def ring(name, loc, major_radius, minor_radius, mat, rotation=None, segments=12):
bpy.ops.mesh.primitive_torus_add(major_segments=segments, minor_segments=4,
location=loc, major_radius=major_radius,
minor_radius=minor_radius)
obj = bpy.context.object
if rotation is not None:
obj.rotation_euler = rotation
return _finish(obj, name, mat)
def mesh_object(name, vertices, faces, mat):
mesh = bpy.data.meshes.new(name + '_Mesh')
mesh.from_pydata(vertices, [], faces)
mesh.update()
obj = bpy.data.objects.new(name, mesh)
bpy.context.scene.collection.objects.link(obj)
return _finish(obj, name, mat)
def empty(name, loc=(0, 0, 0)):
name = re.sub(r'[^A-Za-z0-9_]', '_', name)
obj = bpy.data.objects.new(name, None)
obj['part_name'] = name
obj.location = loc
obj.empty_display_type = 'PLAIN_AXES'
obj.empty_display_size = 0.12
bpy.context.scene.collection.objects.link(obj)
return obj
PALETTES = {
'glutpakt': ['292B35', 'EF6B3B', 'D9A441', 'D8CEC0'],
'nietenkonvent': ['247A80', 'C79A48', '657481', 'E6DCCA'],
'nebelchor': ['45456F', '9578CF', '82DBCE', 'C4D6E5'],
'wurzelbund': ['685441', '56845A', 'A7BA6C', 'D996AF'],
'neutral': ['536375', 'A8654D', '9578CF', 'D8D2BF'],
'worker': ['A8654D', '536375', 'D9A441', '82DBCE'],
'terrain': ['657481', '292B35', 'D9A441', '56845A'],
'team0': ['292B35', 'D9A441', '82DBCE', 'D8CEC0'],
'team1': ['292B35', 'D9A441', 'E9967C', 'D8CEC0'],
}
def srgb_to_linear(value):
return value / 12.92 if value <= 0.04045 else ((value + 0.055) / 1.055) ** 2.4
def make_materials(palette_name, overrides=None):
"""Hex colours match Phase 3; shader inputs use linear RGB for glTF export.
Override individual keys with RGB hex strings in an entry script, e.g.
PALETTE_OVERRIDES = {'accent': 'FFAA55'}. All materials stay opaque and matte.
"""
colours = dict(zip(('primary', 'secondary', 'accent', 'light'), PALETTES[palette_name]))
if palette_name == 'wurzelbund':
colours['water'] = '82DBCE' # Dew/spring accent from the Phase 3 healer icons.
colours['wood'] = '9B805C'
colours['bark_dark'] = '483A2F'
if palette_name in ('neutral', 'worker'):
colours['shadow'] = '202832'
if palette_name == 'neutral':
colours['rift'] = '82DBCE'
colours.update(overrides or {})
result = {}
for key, hex_colour in colours.items():
value = hex_colour.lstrip('#')
rgb = [srgb_to_linear(int(value[i:i + 2], 16) / 255) for i in (0, 2, 4)]
name = 'SW_' + palette_name + '_' + key + '_' + value
# A previous user-edited material must never be reset by another run.
mat = bpy.data.materials.new(name)
mat['material_name'] = name
mat.diffuse_color = (*rgb, 1)
mat.use_nodes = True
shader = mat.node_tree.nodes.get('Principled BSDF')
shader.inputs['Base Color'].default_value = (*rgb, 1)
shader.inputs['Metallic'].default_value = 0.08 if palette_name in ('nietenkonvent', 'team0', 'team1') else 0.0
shader.inputs['Roughness'].default_value = 0.82
if palette_name == 'glutpakt' and key == 'secondary':
shader.inputs['Emission Color'].default_value = (*rgb, 1)
shader.inputs['Emission Strength'].default_value = 0.18
if palette_name == 'glutpakt' and key == 'accent':
shader.inputs['Metallic'].default_value = 0.5
shader.inputs['Roughness'].default_value = 0.62
result[key] = mat
return result