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>
148 lines
5.6 KiB
Python
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
|