saumwacht-godot/assets-source/phase4/standalone/create_lane.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

3153 lines
161 KiB
Python

"""Saumwacht · Lane-Segment · eigenständiges Blender-Python-Skript.
In Blender: Scripting > Text > Open > diese Datei > Run Script.
Vorhandene Szenen bleiben erhalten. GLB und .blend werden automatisch exportiert.
Generiert aus den modularen Quellen mit tools/build_standalone_model_scripts.py.
Getestet mit Blender 5.2.1; verwendet grundlegende bpy/glTF-APIs.
"""
from pathlib import Path
import sys
import bpy
# === ANPASSBARE PARAMETER ================================================
ASSET_ID = 'map_lane_segment' # Verfügbare IDs stehen unmittelbar unter diesem Block.
EXPORT_DIR = None # Optional: r"C:/Spiel/assets/models" oder "/home/.../models".
BLEND_DIR = None # Optionaler Ordner für die bearbeitbare .blend-Datei.
HEIGHT = None # None = Katalogmaß; sonst Zielhöhe in Metern, z. B. 1.5.
PALETTE_OVERRIDES = {} # Beispiel: {'accent': 'D9A441'}; RGB-Hex ohne Alpha.
KEEP_SCENE = True # Generiertes Modell nach Ausführung in Blender anzeigen.
# Formteile und Proportionen sind weiter unten als kleine Funktionen editierbar.
# Bei großen Waffen begrenzt der Zell-Footprint ggf. die endgültige Höhe.
# Dieses Skript braucht nur Blender/bpy, keine Projektmodule oder JSON-Dateien.
def _script_directory():
candidate = globals().get('__file__', '')
if candidate and Path(candidate).is_file():
return Path(candidate).resolve().parent
text = getattr(getattr(bpy.context, 'space_data', None), 'text', None)
if text is not None and text.filepath:
return Path(bpy.path.abspath(text.filepath)).resolve().parent
return Path.cwd()
SCRIPT_DIR = _script_directory()
PROJECT = next((p for p in [SCRIPT_DIR, *SCRIPT_DIR.parents]
if (p / 'project.godot').is_file()), SCRIPT_DIR)
IN_PROJECT = (PROJECT / 'project.godot').is_file()
SOURCE = PROJECT / 'assets-source' / 'phase4' if IN_PROJECT else SCRIPT_DIR / 'saumwacht-source'
DEFAULT_EXPORT_DIR = PROJECT / 'assets' / 'models' if IN_PROJECT else SCRIPT_DIR / 'saumwacht-export'
DEFAULT_BLEND_DIR = SOURCE / 'blend'
# === VERFÜGBARE VARIANTEN ===============================================
ASSET_SPECS = [{'id': 'map_lane_segment',
'name': 'Lane-Segment',
'faction': 'terrain',
'palette': 'terrain',
'role': 'lane',
'upgraded': False,
'height': 0,
'file': 'map_lane_segment.glb',
'triangle_budget': 180000,
'detail_level': 'high'}]
def get_spec(asset_id):
for spec in ASSET_SPECS:
if spec["id"] == asset_id:
return dict(spec)
raise ValueError("Unbekannte ID für dieses Skript: " + asset_id)
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
from math import sin, cos, pi
from mathutils import Vector
def r_plate(name, loc, outline, depth, mat, bulge=.035):
"""Solid X/Z silhouette with a faceted front and an editable local origin."""
area = sum(a[0]*b[1]-b[0]*a[1] for a,b in zip(outline, outline[1:]+outline[:1]))
if area < 0:
outline = list(reversed(outline))
n = len(outline)
v = [(x,-depth/2,z) for x,z in outline] + [(x,depth/2,z) for x,z in outline]
v.append((sum(p[0] for p in outline)/n, -depth/2-bulge, sum(p[1] for p in outline)/n))
f = [(2*n,i,(i+1)%n) for i in range(n)]
f.append(tuple(range(2*n-1,n-1,-1)))
f += [(i,i+n,(i+1)%n+n,(i+1)%n) for i in range(n)]
obj=mesh_object(name,v,f,mat)
obj.location=loc
return obj
def r_block(name, loc, size, mat, cut=.16):
w,d,h=size
x,z=w/2,h/2
c=min(w,h)*cut
outline=[(-x+c,-z),(x-c,-z),(x,-z+c),(x,z-c),
(x-c,z),(-x+c,z),(-x,z-c),(-x,-z+c)]
return r_plate(name,loc,outline,d,mat,min(d*.10,.035))
def r_rock(name, loc, size, mat, twist=0):
obj=ellipsoid(name,loc,size,mat,subdivisions=1)
obj.rotation_euler=(.12+twist*.2,twist*.16,twist)
return obj
def r_sweep(name, points, radii, mat, sides=6, ribbon=False):
"""A single bent solid, tapered to a true pole for claws/flames/roots."""
vertices,loops,faces=[],[],[]
previous_u=None
for i,(pos,radius) in enumerate(zip(points,radii)):
pos=Vector(pos)
tangent=Vector(points[min(i+1,len(points)-1)])-Vector(points[max(i-1,0)])
tangent.normalize()
if previous_u is None:
guide=Vector((0,1,0)) if ribbon else min([Vector((1,0,0)),Vector((0,1,0)),Vector((0,0,1))],key=lambda v:abs(tangent.dot(v)))
if abs(tangent.dot(guide))>.98:
guide=Vector((1,0,0))
u=tangent.cross(guide).normalized()
else:
u=(previous_u-tangent*previous_u.dot(tangent)).normalized()
if u.length<.001:
guide=min([Vector((1,0,0)),Vector((0,1,0)),Vector((0,0,1))],key=lambda v:abs(tangent.dot(v)))
u=tangent.cross(guide).normalized()
previous_u=u
v=tangent.cross(u)
if radius==0:
loops.append([len(vertices)])
vertices.append(pos)
else:
loop=[]
for k in range(sides):
angle=2*pi*k/sides
loop.append(len(vertices))
vertices.append(pos+radius*(u*cos(angle)+v*sin(angle)*(.22 if ribbon else 1)))
loops.append(loop)
if len(loops[0])>1:
faces.append(tuple(reversed(loops[0])))
for lower,upper in zip(loops,loops[1:]):
for k in range(sides):
j=(k+1)%sides
if len(upper)==1:
faces.append((lower[k],lower[j],upper[0]))
elif len(lower)==1:
faces.append((lower[0],upper[j],upper[k]))
else:
# Curving rings do not make planar quads; give each triangle its own normal.
faces.extend([(lower[k],lower[j],upper[j]),(lower[k],upper[j],upper[k])])
if len(loops[-1])>1:
faces.append(tuple(loops[-1]))
origin=Vector(points[0])
obj=mesh_object(name,[p-origin for p in vertices],faces,mat)
obj.location=origin
return obj
def r_arch_shell(name, loc, profile, mat, segments=12):
"""Solid rear 210-degree bell shell, with actual open front and closed cut edges."""
area=sum(a[0]*b[1]-b[0]*a[1] for a,b in zip(profile,profile[1:]+profile[:1]))
if area<0:
profile=list(reversed(profile))
verts=[]
for radius,z in profile:
for k in range(segments+1):
a=-pi/12+7*pi/6*k/segments
verts.append((radius*cos(a),radius*sin(a),z))
n=segments+1
faces=[]
for i in range(len(profile)):
j=(i+1)%len(profile)
for k in range(segments):
faces.append((i*n+k,i*n+k+1,j*n+k+1,j*n+k))
faces.append(tuple(i*n for i in range(len(profile))))
faces.append(tuple(i*n+segments for i in reversed(range(len(profile)))))
obj=mesh_object(name,verts,faces,mat)
obj.location=loc
return obj
def r_lathe(name, loc, profile, mat, axis=(0,0,1), segments=12):
"""Closed lathed profile; supports bowls, bells and their real inner walls."""
w=Vector(axis).normalized()
guide=Vector((0,0,1)) if abs(w.z)<.9 else Vector((0,1,0))
u=guide.cross(w).normalized()
v=w.cross(u)
verts,loops,faces=[],[],[]
for radius,z in profile:
loop=[]
for k in range(segments if radius else 1):
a=2*pi*k/segments
loop.append(len(verts))
verts.append(w*z+radius*(u*cos(a)+v*sin(a)))
loops.append(loop)
for lower,upper in zip(loops,loops[1:]+loops[:1]):
if len(lower)==len(upper)==1:
continue
for k in range(segments):
j=(k+1)%segments
if len(lower)==1:
faces.append((lower[0],upper[j],upper[k]))
elif len(upper)==1:
faces.append((lower[k],lower[j],upper[0]))
else:
faces.append((lower[k],lower[j],upper[j],upper[k]))
obj=mesh_object(name,verts,faces,mat)
obj.location=loc
return obj
def r_band(name, start, end, radius, mat, sides=8):
return beam(name,start,end,radius,mat,vertices=sides)
def r_disc(name, loc, radius, depth, mat):
return cylinder(name,loc,radius,depth,mat,vertices=10,rotation=(pi/2,0,0))
def r_inset(name, loc, outline, depth, rim, face):
"""Raised face inset in a silhouette-shaped metal border."""
r_plate(name+'_Rim',loc,outline,depth,rim)
r_plate(name+'_Face',(loc[0],loc[1]-depth*.55,loc[2]),
[(x*.82,z*.88) for x,z in outline],depth*.28,face)
def r_sun(name, loc, radius, m, rays=8):
x,y,z=loc
r_disc(name+'_Ring',loc,radius,.04,m['accent'])
r_disc(name+'_Face',(x,y-.027,z),radius*.72,.035,m['secondary'])
for k in range(rays):
a=2*pi*k/rays
r_sweep(name+'_Ray'+str(k),[(x+radius*.88*cos(a),y,z+radius*.88*sin(a)),
(x+radius*1.42*cos(a),y,z+radius*1.42*sin(a))],
[radius*.14,0],m['accent'],4)
from math import pi, sin, cos
import hashlib
import random
import bpy
import bmesh
from mathutils import Matrix, Vector
def hd_finish(obj, width=.012, segments=3, smooth=False):
"""Bake scale before bevel so round-overs have consistent physical width."""
obj.data.transform(Matrix.Diagonal((*obj.scale, 1)))
obj.scale = (1,1,1)
bpy.context.view_layer.objects.active = obj
if width:
mod = obj.modifiers.new('Crafted_edge_radius','BEVEL')
mod.width = width
mod.segments = segments
mod.limit_method = 'ANGLE'
mod.angle_limit = .32
mod.use_clamp_overlap = True
bpy.ops.object.modifier_apply(modifier=mod.name)
for face in obj.data.polygons:
face.use_smooth = smooth or len(face.vertices) == 4
if width or smooth:
normal = obj.modifiers.new('Weighted_surface_normals','WEIGHTED_NORMAL')
normal.keep_sharp = True
normal.weight = 50
bpy.ops.object.modifier_apply(modifier=normal.name)
return obj
def hd_box(name, loc, size, mat, radius=.012, rotation=None):
return hd_finish(box(name,loc,size,mat,rotation),radius)
def hd_plate(name,loc,outline,depth,mat,radius=.01,bulge=.0):
return hd_finish(r_plate(name,loc,outline,depth,mat,bulge),radius)
def hd_rock(name,loc,size,mat,seed=0):
obj=ellipsoid(name,loc,size,mat,subdivisions=2)
rng=random.Random(name+str(seed))
# Separate broad chisel facets with chipped edges, not a smooth sphere.
for v in obj.data.vertices:
v.co *= rng.uniform(.94,1.045)
obj.rotation_euler=(.07*seed,.12*seed,.17*seed)
return hd_finish(obj,.009,2)
def hd_rod(name,a,b,radius,mat,sides=20):
return hd_finish(beam(name,a,b,radius,mat,sides),min(radius*.18,.007),2,True)
def hd_ring(name,loc,radius,thickness,mat,rotation=None,segments=40):
obj=ring(name,loc,radius,thickness,mat,rotation,segments)
for face in obj.data.polygons: face.use_smooth=True
return obj
def hd_disc(name,loc,radius,depth,mat):
return hd_finish(cylinder(name,loc,radius,depth,mat,32,(pi/2,0,0)),.006,3,True)
def hd_curve(name,points,radii,mat,sides=12,steps=5):
"""Catmull-Rom centerline and tapered cross sections for claws and cables."""
pts=[Vector(p) for p in points]
positions=[]; widths=[]
for i in range(len(pts)-1):
p0,p1,p2,p3=pts[max(0,i-1)],pts[i],pts[i+1],pts[min(len(pts)-1,i+2)]
for k in range(steps):
t=k/steps
p=.5*((2*p1)+(-p0+p2)*t+(2*p0-5*p1+4*p2-p3)*t*t+(-p0+3*p1-3*p2+p3)*t*t*t)
positions.append(tuple(p)); widths.append(radii[i]*(1-t)+radii[i+1]*t)
positions.append(tuple(pts[-1]));widths.append(radii[-1])
obj=r_sweep(name,positions,widths,mat,sides)
for face in obj.data.polygons:face.use_smooth=True
return obj
def hd_materials(m):
"""A small shared PBR palette; values are linear RGB for glTF."""
definitions={
'basalt':('41434B',.05,.77,0), 'edge':('62636A',.10,.71,0),
'dark':('171A21',.15,.66,0), 'gold':('C99645',.78,.42,0),
'gold_light':('F1CA7C',.68,.39,0), 'ember':('FF5210',.05,.40,1.25),
'hot':('FFC65E',.0,.36,2.2), 'ceramic':('D8CDB2',.10,.37,0),
'bone':('BAC8CF',.16,.55,0), 'rift':('CE609B',.10,.35,2.0),
}
for key,(hexcolor,metal,rough,emission) in definitions.items():
name='SW_HD_'+key
mat=bpy.data.materials.new(name)
mat['material_name']=name
c=tuple(srgb_to_linear(int(hexcolor[i:i+2],16)/255) for i in (0,2,4))
mat.diffuse_color=(*c,1);mat.use_nodes=True
node=mat.node_tree.nodes.get('Principled BSDF')
node.inputs['Base Color'].default_value=(*c,1)
node.inputs['Metallic'].default_value=metal
node.inputs['Roughness'].default_value=rough
if emission:
node.inputs['Emission Color'].default_value=(*c,1)
node.inputs['Emission Strength'].default_value=emission
m[key]=mat
return m
def hd_rivet(name,loc,m,r=.018):
hd_disc(name+'_Seat',loc,r*1.5,.009,m['dark'])
hd_disc(name+'_Head',(loc[0],loc[1]-.009,loc[2]),r,.018,m['gold_light'])
def hd_trim(name,points,m,r=.012):
return hd_curve(name,points,[r]*len(points),m['gold_light'],8,2)
def hd_sun(name,loc,r,m):
x,y,z=loc
hd_disc(name+'_Base',(x,y,z),r,.024,m['gold'])
hd_disc(name+'_Face',(x,y-.020,z),r*.68,.018,m['dark'])
hd_disc(name+'_Heart',(x,y-.033,z),r*.43,.022,m['gold_light'])
for i in range(12):
a=i*pi/6
hd_plate(name+'_Ray'+str(i),(x,y-.032,z),[(r*.64*cos(a-.13),r*.64*sin(a-.13)),
(r*.96*cos(a),r*.96*sin(a)),(r*.64*cos(a+.13),r*.64*sin(a+.13))],.016,m['gold_light'],.002)
def hd_legs(m,spread=.30,height=.50,slim=False):
radius=.070 if slim else .13
for s,side in [(-1,'L'),(1,'R')]:
n='Leg'+side
hip=(s*spread*.8,.055,height)
knee=(s*spread,.06,height*.53)
ankle=(s*spread,-.035,.13)
hd_rod(n+'_InnerThigh',hip,knee,radius*.64,m['dark'])
hd_rod(n+'_InnerShin',knee,ankle,radius*.64,m['dark'])
hd_rock(n+'_ThighArmor',tuple((Vector(hip)+Vector(knee))*.5),(radius*1.24,radius*.90,height*.24),m['basalt'],s)
hd_rock(n+'_ShinArmor',tuple((Vector(knee)+Vector(ankle))*.5),(radius*1.10,radius,height*.22),m['basalt'],2*s)
for p,label in [(hip,'Hip'),(knee,'Knee'),(ankle,'Ankle')]:
hd_disc(n+'_'+label+'Pivot',(p[0],p[1]-radius*.8,p[2]),radius*.72,.055,m['gold'])
hd_disc(n+'_'+label+'Pin',(p[0],p[1]-radius*1.12,p[2]),radius*.36,.022,m['dark'])
hd_disc(n+'_'+label+'Rim',(p[0],p[1]-radius*1.27,p[2]),radius*.16,.025,m['gold_light'])
# Three individually modeled tapered foot/toe plates.
for j in range(3):
x=ankle[0]+(j-1)*radius*.70
toe=[(-radius*.40,-.045),(radius*.40,-.045),(radius*.30,.060),(0,.090),(-radius*.32,.060)]
hd_plate(n+'_Toe'+str(j),(x,-.135,.065),toe,.26 if not slim else .20,m['basalt'],.013)
hd_box(n+'_ToeCap'+str(j),(x,-.225,.075),(radius*.62,.045,.048),m['gold'],.008)
hd_box(n+'_AnkleCuff',(ankle[0],-.05,.19),(radius*2.65,.22,.065),m['gold'],.012)
if not slim:
hd_plate(n+'_KneeGuard',(knee[0],-.103,knee[2]),[(-.10,.09),(.10,.09),(.085,-.08),(0,-.15),(-.085,-.08)],.075,m['edge'],.014)
else:
hd_plate(n+'_ShinGold',(knee[0],-.095,knee[2]-.10),[(-.057,.14),(.057,.14),(.026,-.11),(0,-.21)],.046,m['gold'],.005)
hd_plate(n+'_ShinEmber',(knee[0],-.126,knee[2]-.11),[(-.018,.08),(.018,.08),(0,-.14)],.015,m['ember'],.002)
def hd_arm(name,shoulder,elbow,hand,m,radius=.16,fingers=True):
hd_rod(name+'_UpperCore',shoulder,elbow,radius*.56,m['dark'])
hd_rod(name+'_ForearmCore',elbow,hand,radius*.64,m['dark'])
hd_rock(name+'_Elbow',elbow,(radius*.90,)*3,m['basalt'],2)
hd_rock(name+'_UpperArmor',tuple((Vector(shoulder)+Vector(elbow))*.5),(radius*1.08,radius*.88,Vector(shoulder).lerp(Vector(elbow),.5).z*.17),m['basalt'],1)
hd_rock(name+'_ForearmArmor',tuple((Vector(elbow)+Vector(hand))*.5),(radius*1.20,radius,Vector(elbow).lerp(Vector(hand),.5).z*.28),m['basalt'],3)
hd_rock(name+'_Shoulder',shoulder,(radius*1.48,radius*1.24,radius*1.15),m['basalt'],2)
hd_disc(name+'_ShoulderJoint',(shoulder[0],shoulder[1]-radius*.85,shoulder[2]),radius*.66,.052,m['gold'])
for t,label in [(.58,'UpperBand'),(.85,'WristBand')]:
p=Vector(elbow).lerp(Vector(hand),t)
d=(Vector(hand)-Vector(elbow)).normalized()*.040
hd_rod(name+'_'+label,tuple(p-d),tuple(p+d),radius*1.27,m['gold'],24)
hd_rivet(name+'_'+label+'Rivet',(p.x,p.y-radius*1.25,p.z),m)
hd_rock(name+'_Palm',hand,(radius*1.15,radius*.81,radius*1.06),m['basalt'],4)
if fingers:
for j in range(4):
x=hand[0]+(j-1.5)*radius*.46
for k in range(2):
hd_box(name+'_Finger%d_%d'%(j,k),(x,hand[1]-.09-k*.04,hand[2]-.045-k*.07),
(radius*.40,radius*.53,radius*.55),m['edge'] if k==0 else m['basalt'],radius*.08)
s=-1 if hand[0]<0 else 1
hd_rock(name+'_Thumb',(hand[0]-s*radius,hand[1]-.025,hand[2]-.04),(radius*.46,radius*.62,radius*.76),m['basalt'],1)
def hd_tower(name,loc,size,m):
x,y,z=loc;w,d,h=size
hd_box(name+'_StoneKeep',(x,y,z),(w,d,h),m['basalt'],.018)
hd_box(name+'_LowerCornice',(x,y,z-h*.31),(w*1.04,d*1.04,.045),m['gold'],.008)
hd_box(name+'_UpperCornice',(x,y,z+h*.34),(w*1.055,d*1.055,.045),m['edge'],.008)
for j in range(3):
px=x+(j-1)*w*.26
hd_box(name+'_WindowRecess'+str(j),(px,y-d*.5-.014,z),(w*.12,.018,h*.42),m['dark'],.006)
hd_box(name+'_WindowFire'+str(j),(px,y-d*.5-.026,z),(w*.052,.009,h*.30),m['ember'],.003)
hd_box(name+'_WindowLintel'+str(j),(px,y-d*.5-.020,z+h*.25),(w*.19,.07,.028),m['edge'],.003)
for k,(dx,dy) in enumerate([(-1,-1),(1,-1),(-1,1),(1,1)]):
hd_box(name+'_Merlon'+str(k),(x+dx*w*.35,y+dy*d*.34,z+h*.55),(w*.29,d*.31,h*.29),m['basalt'],.010)
hd_box(name+'_MerlonCap'+str(k),(x+dx*w*.35,y+dy*d*.34,z+h*.70),(w*.31,d*.33,.032),m['edge'],.004)
def hd_tank(up,m):
hd_legs(m,.295,.46)
hd_rock('Body_ArmoredCore',(0,.04,.82),(.42,.31,.48),m['dark'],1)
# Broad tapered chest formed from five slabs, with a narrow recessed furnace.
for s,side in [(-1,'L'),(1,'R')]:
outline=[(s*.045,.45),(s*.34,.40),(s*.41,.20),(s*.34,-.27),(s*.05,-.46)]
hd_plate('Body_Chest'+side,(0,-.28,.84),outline,.22,m['basalt'],.020,.018)
chest_pieces=[[(.078,.40),(.28,.365),(.335,.21),(.18,.075),(.083,.17)],
[(.19,.045),(.337,.17),(.298,-.12),(.21,-.18),(.067,-.052),(.087,.125)],
[(.070,-.094),(.215,-.217),(.286,-.162),(.222,-.30),(.068,-.385)]]
for j,outline_piece in enumerate(chest_pieces):
hd_plate('Body_ChestRelief'+side+str(j),(0,-.411-j*.006,.84),[(s*x,z) for x,z in outline_piece],.066,m['basalt'] if j%2 else m['edge'],.007,.021)
hd_trim('Body_Border'+side,[(s*.05,-.437,.40),(s*.36,-.407,.62),(s*.40,-.385,1.17),(s*.27,-.394,1.28)],m,.021)
# Chisel seams divide the armor into larger readable stone layers.
hd_curve('Body_Seam'+side,[(s*.065,-.418,.72),(s*.18,-.410,.87),(s*.12,-.409,1.01),(s*.32,-.404,1.14)],[.009]*4,m['dark'],6,1)
hd_arm('Arm'+side,(s*.49,.0,1.04),(s*.62,-.01,.76),(s*.65,-.14,.46),m,.155)
hd_plate('Arm'+side+'_Pauldron',(s*.48,-.105,1.12),[(-.21,.05),(-.13,.17),(.13,.17),(.21,.04),(.15,-.15),(-.15,-.15)],.34,m['gold'],.016)
hd_plate('Arm'+side+'_PauldronStone',(s*.48,-.139,1.16),[(-.17,.03),(-.10,.14),(.10,.14),(.17,.03),(.11,-.10),(-.11,-.10)],.33,m['basalt'],.014)
if up:
hd_tower('Arm'+side+'_Castle',(s*.48,.01,1.31),(.38,.40,.29),m)
hd_tower('Arm'+side+'_Bastion',(s*.65,-.08,.68),(.29,.32,.24),m)
for z in [.59,1.19]:
hd_rivet('Body_'+side+'Fastener'+str(z),(s*.31,-.414,z),m,.023)
hd_box('Head_FurnaceRecess',(0,-.402,1.09),(.095,.055,.32),m['dark'],.007)
hd_box('Head_Fire',(0,-.432,1.10),(.039,.025,.258),m['ember'],.008)
hd_box('Head_FireHeart',(0,-.446,1.10),(.012,.005,.19),m['hot'],.002)
for s in [-1,1]:
hd_box('Body_CenterGold'+str(s),(s*.061,-.44,1.02),(.023,.034,.51),m['gold'],.006)
hd_plate('Body_BottomGold',(0,-.443,.70),[(-.072,.05),(.072,.05),(0,-.13)],.030,m['gold'],.005)
if up:
hd_tower('Head_Crown',(0,.01,1.46),(.28,.31,.31),m)
hd_box('Body_SpineGold',(0,-.446,.61),(.025,.040,.26),m['gold'],.005)
def hd_coal(up,m):
hd_legs(m,.25,.39)
hd_rock('Body_LavaCore',(0,.025,.76),(.375,.27,.39),m['ember'],2)
# Interlocking shell plates leave true gaps above an orange inner body.
for j,z in enumerate([.53,.81,1.07]):
for k in range(7):
a=2*pi*k/7+.20*(j%2)
if sin(a)<-.42:continue
pos=(.33*cos(a),.24*sin(a),z)
hd_rock('Body_Shell%d_%d'%(j,k),pos,(.19,.135,.205),m['basalt'],j*3+k)
hd_rock('Head_JawCavity',(0,-.245,.77),(.37,.15,.235),m['dark'],0)
hd_plate('Head_MouthEmber',(0,-.345,.78),[(-.30,.07),(-.17,.10),(0,.02),(.17,.10),(.30,.07),(.24,-.08),(0,-.15),(-.24,-.08)],.028,m['ember'],.008)
for s,side in [(-1,'L'),(1,'R')]:
hd_rock('Head_Brow'+side,(s*.19,-.26,1.025),(.22,.17,.145),m['basalt'],s*2)
hd_curve('Head_Eye'+side,[(s*.29,-.419,1.02),(s*.18,-.433,.99),(s*.09,-.413,.97)],[.012,.019,.007],m['hot'],8,3)
hd_rock('Head_Cheek'+side,(s*.32,-.24,.82),(.15,.13,.22),m['basalt'],4*s)
for j in range(4):
x=s*(.035+j*.07)
hd_curve('Head_UpperTooth'+side+str(j),[(x,-.32,.96-j*.016),(x,-.405,.85-j*.016),(x-s*.010,-.413,.80-j*.012)],[.055,.029,0],m['edge'],8,3)
hd_curve('Head_LowerTooth'+side+str(j),[(x,-.28,.54+j*.02),(x,-.38,.67+j*.01),(x,-.40,.72+j*.01)],[.057,.027,0],m['basalt'],8,3)
hd_arm('Arm'+side,(s*.44,.03,.98),(s*.54,-.015,.72),(s*.57,-.16,.52),m,.14,False)
for j in range(3):
x=s*.58+(j-1)*.14
hd_curve('Weapon'+side+'_Claw'+str(j),[(x,-.20,.60),(x+s*.035,-.37,.48),(x+s*.01,-.48,.26),(x-s*.070,-.41,.18)],
[.10 if up else .075,.095 if up else .068,.045,0],m['gold'],16,6)
hd_rivet('Arm'+side+'_Knuckle'+str(j),(x,-.27,.63),m,.021)
hd_plate('Arm'+side+'_ShoulderTrim',(s*.42,-.125,1.085),[(-.10,.04),(.10,.04),(.08,-.08),(0,-.12),(-.08,-.08)],.04,m['gold'],.008)
for k in range(5 if up else 3):
x=(k-(2 if up else 1))*.15
hd_curve('Body_DorsalSpine'+str(k),[(x,.12,1.02),(x*1.12,.13,1.26+(.10 if k%2==0 else 0)),(x*.95,.10,1.43 if up else 1.32)], [.125,.085,0],m['basalt'],9,3)
hd_rock('Head_Chin',(0,-.20,.53),(.24,.19,.12),m['basalt'],5)
def hd_lancer(m):
hd_legs(m,.23,.74,True)
hd_rod('Body_Spine',(0,.045,.61),(0,.03,1.24),.085,m['dark'])
for s,side in [(-1,'L'),(1,'R')]:
hd_plate('Body_Cuirass'+side,(0,-.025,1.07),[(s*.025,.17),(s*.23,.13),(s*.16,-.13),(s*.025,-.22)],.20,m['gold'],.011)
hd_plate('Body_ChestStone'+side,(0,-.071,1.08),[(s*.048,.13),(s*.18,.10),(s*.12,-.10),(s*.048,-.15)],.16,m['basalt'],.008)
hd_arm('Arm'+side,(s*.24,.01,1.21),(s*.33,-.02,1.03),(s*.30,-.23,.96),m,.065)
hd_plate('Arm'+side+'_ShoulderFin',(s*.28,0,1.23),[(s*-.07,-.07),(s*.20,-.035),(s*.03,.16)],.15,m['gold'],.007)
hd_plate('Body_EmberHeart',(0,-.162,1.03),[(-.028,.10),(.028,.10),(0,-.13)],.019,m['ember'],.003)
hd_rod('Head_Neck',(0,.03,1.21),(0,.03,1.37),.06,m['dark'])
hd_plate('Head_Helmet',(0,-.01,1.47),[(-.19,.105),(0,-.005),(.19,.105),(.072,-.09),(0,-.21),(-.072,-.09)],.22,m['gold'],.008)
hd_plate('Head_Visor',(0,-.135,1.40),[(-.052,.018),(.052,.018),(0,-.116)],.017,m['ember'],.003)
hd_curve('Head_Crest',[(0,.07,1.49),(0,.12,1.64),(-.06,.11,1.76)],[.061,.034,0],m['gold_light'],10,4)
a=Vector((-.71,-.265,1.01)); b=Vector((.70,-.29,.94))
hd_rod('Weapon_LanceShaft',a,b,.046,m['dark'],24)
hd_rod('Weapon_LanceEmber',a+Vector((0,-.042,0)),b+Vector((0,-.042,0)),.011,m['ember'],10)
for k in range(11):
t=.05+k*.087;p=a.lerp(b,t)
hd_rod('Weapon_LanceCollar'+str(k),p-Vector((.012,0,0)),p+Vector((.012,0,0)),.065,m['gold'],24)
hd_curve('Weapon_SpearHead',[b,b+Vector((.14,0,-.005)),b+Vector((.46,0,-.02))],[.06,.092,0],m['ember'],8,3)
for s in [-1,1]:
hd_curve('Weapon_SpearEdge'+str(s),[b+Vector((0,0,s*.07)),b+Vector((.17,0,s*.085)),b+Vector((.47,0,-.02))],[.024,.020,0],m['gold_light'],8,3)
def hd_ballista(m):
hd_legs(m,.33,.62)
hd_box('Body_Gearbox',(0,.02,.66),(.49,.43,.26),m['basalt'],.035)
for s,side in [(-1,'L'),(1,'R')]:
hd_disc('Body_Crank'+side,(s*.23,-.21,.73),.16,.055,m['gold'])
hd_disc('Body_CrankInset'+side,(s*.23,-.247,.73),.10,.018,m['dark'])
for k in range(8):
a=k*pi/4
hd_box('Body_CrankTooth'+side+str(k),(s*.23+.137*cos(a),-.212,.73+.137*sin(a)),(.055,.067,.052),m['gold'],.006,rotation=(0,a,0))
hd_box('Weapon_Rail'+side,(s*.13,-.11,.99),(.095,1.15,.14),m['basalt'],.018)
hd_box('Weapon_RailTrim'+side,(s*.13,-.11,1.068),(.052,1.17,.020),m['gold'],.005)
bow=[(s*.11,-.46,1.00),(s*.39,-.47,.97),(s*.67,-.31,1.01),(s*.80,.045,1.12)]
hd_curve('Weapon_GlassLimb'+side,bow,[.095,.12,.095,.051],m['ember'],12,7)
hd_curve('Weapon_LimbEdge'+side,[(x,y,z+.068) for x,y,z in bow],[.022,.025,.022,.017],m['gold'],10,7)
hd_curve('Weapon_DrawCable'+side,[bow[-1],(s*.37,.26,1.09),(0,.45,1.075)],[.011,.011,.011],m['dark'],10,3)
hd_rod('Weapon_LimbClamp'+side,(s*.18,-.52,.95),(s*.18,-.39,1.055),.089,m['gold'],12)
hd_box('Weapon_RearHousing',(0,.46,1.06),(.38,.25,.29),m['basalt'],.023)
hd_box('Weapon_RearTrim',(0,.40,1.08),(.42,.055,.33),m['gold'],.012)
hd_rod('Weapon_CentralBolt',(0,.38,1.085),(0,-.70,1.085),.035,m['ember'],16)
hd_curve('Weapon_BoltHead',[(0,-.62,1.085),(0,-.76,1.085),(0,-1.00,1.085)],[.040,.085,0],m['ember'],8,3)
for k in range(6):
hd_box('Weapon_RailRivet'+str(k),(.17,-.54+k*.18,1.10),(.025,.025,.026),m['gold_light'],.005)
def hd_mortar(up,m):
hd_legs(m,.29,.42)
hd_rock('Body_LowerCore',(0,.035,.60),(.38,.28,.25),m['basalt'],3)
radius=.49 if up else .435
# Closed outer/inner profile: deep bore with individual radial barrel staves.
profile=[(0,0),(.22,.025),(radius*.83,.24),(radius,.64),(radius*.85,.68),(radius*.74,.60),(radius*.48,.16),(0,.14)]
obj=r_lathe('Body_MortarBore',(0,.07,.51),profile,m['dark'],segments=48)
hd_finish(obj,.006,2,True)
for k in range(12):
a=2*pi*k/12
x=radius*.80*cos(a);y=.07+radius*.80*sin(a)
o=hd_rock('Body_MortarStave'+str(k),(x,y,.86),(.15,.13,.29),m['basalt'],k)
o.rotation_euler[2]=a
p=(radius*.98*cos(a),.07+radius*.98*sin(a),1.125)
hd_box('Body_RimClasp'+str(k),p,(.115,.13,.15),m['gold'],.014,rotation=(0,0,a-pi/2))
hd_ring('Body_MortarLip',(0,.07,1.175),radius*.91,.033,m['gold_light'],segments=64)
hd_ring('Body_MortarBelt',(0,.07,.68),radius*.72,.027,m['gold'],segments=48)
pool_z=1.015 if up else .79
hd_finish(cylinder('Weapon_LavaPool',(0,.07,pool_z),radius*.68,.038,m['ember'],48),.005,2,True)
if not up:
hd_rock('Weapon_EmberBoulder',(0,.02,.87),(.18,.18,.17),m['ember'],1)
for k in range(7):
a=k*2*pi/7
hd_rock('Weapon_BoulderCrust'+str(k),(.14*cos(a),.02+.14*sin(a),.89),(.074,.068,.10),m['basalt'],k)
else:
for k in range(3):
hd_curve('Weapon_LavaEddy'+str(k),[(.26*cos(a+k),.07+.26*sin(a+k),pool_z+.026) for a in [0,.6,1.1,1.8,2.4]], [.009]*5,m['hot'],8,3)
for s,side in [(-1,'L'),(1,'R')]:
hd_arm('Arm'+side,(s*.46,.055,.99),(s*.59,-.04,.73),(s*.43,-.29,.52),m,.155)
hd_plate('Body_FrontBadge',(0,-.355,.83),[(-.15,.17),(.15,.17),(.11,-.12),(0,-.25),(-.11,-.12)],.060,m['gold'],.009)
hd_plate('Body_FrontBadgeStone',(0,-.397,.84),[(-.10,.13),(.10,.13),(.070,-.10),(0,-.18),(-.070,-.10)],.029,m['basalt'],.006)
hd_plate('Body_FrontSigil',(0,-.42,.83),[(-.024,.09),(.025,.12),(.037,-.065),(0,-.14),(-.036,-.065)],.018,m['ember'],.003)
def hd_hearth(up,m):
hd_legs(m,.28,.45)
hd_rock('Body_HearthBack',(0,.13,.88),(.43,.32,.44),m['basalt'],1)
# Deep oven vault: an open front arch with a visible brick-lined interior.
hd_plate('Body_OvenDark',(0,-.20,.99),[(-.30,-.25),(-.30,.10),(-.20,.27),(0,.35),(.20,.27),(.30,.10),(.30,-.25)],.085,m['dark'],.016)
if not up:
for j in range(3):
for k in range(3):
hd_box('Body_HearthBrick%d_%d'%(j,k),((k-1)*.15,-.251,.83+j*.10),(.136,.024,.083),m['basalt'],.009)
for k in range(3):
x=(k-1)*.12
hd_curve('Flame_Oven'+str(k),[(x,-.29,.79),(x+.04,-.30,.93),(x-.015,-.30,1.06+(k%2)*.15)], [.068,.058,0],m['ember'],12,6)
for k in range(11):
a=pi*k/10
hd_box('Body_ArchStone'+str(k),(.335*cos(a),-.16,1.03+.335*sin(a)),(.125,.25,.19),m['ceramic'],.018,rotation=(0,pi/2-a,0))
for s in [-1,1]:
for j in range(2):
hd_box('Body_Pillar%d_%d'%(s,j),(s*.335,-.16,.85+j*.14),(.15,.25,.13),m['ceramic'],.015)
hd_box('Head_Chimney',(0,.045,1.42),(.35,.31,.25),m['ceramic'],.029)
hd_box('Head_ChimneyRecess',(0,-.119,1.41),(.21,.015,.10),m['dark'],.012)
hd_box('Head_ChimneyCap',(0,.04,1.56),(.40,.35,.07),m['ceramic'],.017)
else:
hd_disc('Body_SunSocket',(0,-.30,1.22),.25,.06,m['dark'])
hd_ring('Body_SunFrame',(0,-.36,1.22),.248,.042,m['gold'],(pi/2,0,0),64)
hd_rock('Body_SunHeart',(0,-.34,1.22),(.203,.092,.203),m['ember'],0)
for j in range(3):
points=[]
for k in range(10):
a=k*.30+j*2*pi/3
r=.018+k*.017
points.append((r*cos(a),-.443+.025*(r/.18)**2,1.22+r*sin(a)))
hd_curve('Flame_SolarEddy'+str(j),points,[.006]*10,m['hot'],8,3)
for k in range(9):
a=pi*k/8
hd_curve('Body_HaloRay'+str(k),[(.30*cos(a),-.13,1.22+.30*sin(a)),(.44*cos(a),-.13,1.22+.44*sin(a))],[.045,0],m['gold'],10,3)
for s,side in [(-1,'L'),(1,'R')]:
hd_plate('Body_Mantle'+side,(s*.40,.07,.99),[(s*-.06,.40),(s*.15,.32),(s*.19,-.24),(s*.10,-.35),(s*-.13,-.20)],.27,m['ceramic'],.020)
hd_sun('Body_MantleSun'+side,(s*.43,-.085,1.19),.082,m)
hd_plate('Body_Tabard'+side,(s*.40,-.16,.46),[(-.10,.20),(.10,.20),(.105,-.22),(0,-.32),(-.105,-.22)],.045,m['ceramic'],.011)
hd_sun('Body_TabardSun'+side,(s*.40,-.19,.42),.060,m)
r=.36 if up else .30
obj=r_lathe('Weapon_HearthBowl',(0,-.40,.54),[(0,0),(.15,.02),(r,.16),(r*1.04,.22),(r*.88,.23),(r*.81,.18),(.16,.06),(0,.06)],m['gold'],segments=48)
hd_finish(obj,.009,3,True)
hd_ring('Weapon_BowlRim',(0,-.40,.762),r*.97,.022,m['gold_light'],segments=48)
hd_finish(cylinder('Weapon_EmberBed',(0,-.40,.697),r*.79,.023,m['ember'],40),.005,2,True)
for k in range(5):
x=(k-2)*.085;h=.19+(.15 if k==2 else .06*(k%2))
hd_curve('Weapon_Flame'+str(k),[(x,-.40,.70),(x-.035,-.38,.70+h*.35),(x+.035,-.38,.70+h*.72),(x-.02,-.38,.70+h)], [.050,.049,.023,0],m['hot'] if k%2 else m['ember'],12,6)
for s,side in [(-1,'L'),(1,'R')]:
hd_arm('Arm'+side,(s*.42,.02,1.05),(s*.55,-.16,.80),(s*.34,-.41,.65),m,.15)
hd_sun('Arm'+side+'_Seal',(s*.45,-.155,1.06),.095,m)
hd_sun('Weapon_BowlSeal',(0,-.702,.65),.063,m)
if up:
hd_curve('Body_SolarStream',[(0,-.44,1.11),(.05,-.45,.98),(-.03,-.43,.83),(0,-.43,.72)],[.023,.024,.016,.013],m['hot'],12,6)
def build_high_glutpakt(spec,m):
hd_materials(m)
role=spec['role'];up=spec['upgraded']
if role=='melee':hd_coal(up,m)
elif role=='tank':hd_tank(up,m)
elif role=='ranged':hd_ballista(m) if up else hd_lancer(m)
elif role=='artillery':hd_mortar(up,m)
elif role=='support':hd_hearth(up,m)
def build_high_enemy(spec,m):
hd_materials(m)
id=spec['id']
if id in ('e05','e06','e09','e10'):
build_expedition_enemy(id,m)
return
if id in ('e01','e02'):
heavy=id=='e02'
hd_rock('Body_Core',(0,0,.52),(.44,.55,.30),m['dark'],2)
# Segmented back carapace, raised central ridge and six clawed legs.
for j in range(4):
y=-.34+j*.25
width=.46 if heavy else .40
hd_plate('Body_Carapace'+str(j),(0,y,.62),[(-width,0),(-width*.67,.23),(0,.35),(width*.67,.23),(width,0),(0,-.09)],.26,m['bone'] if j%2 else m['basalt'],.024)
hd_curve('Body_SeamGlow'+str(j),[(-width*.69,y-.145,.83),(0,y-.147,.95),(width*.69,y-.145,.83)],[.012,.015,.012],m['rift'],8,2)
if heavy:
hd_curve('Body_Spine'+str(j),[(0,y,.94),(0,y+.06,1.15),(0,y+.13,1.28)],[.10,.055,0],m['bone'],12,5)
hd_rock('Head_Skull',(0,-.65,.54),(.29,.20,.19),m['basalt'],3)
for s,side in [(-1,'L'),(1,'R')]:
for k in range(3):
y=-.38+k*.35;n='Leg'+side+str(k)
hip=(s*.30,y,.53);knee=(s*.61,y-.11,.36);foot=(s*.77,y-.27,.06)
hd_rod(n+'_Upper',hip,knee,.070 if heavy else .054,m['basalt'])
hd_rock(n+'_Joint',knee,(.085,)*3,m['bone'],k)
hd_curve(n+'_Shin',[knee,(s*.75,y-.20,.20),foot],[.076,.045,0],m['bone'],12,4)
hd_rod(n+'_Inset',tuple(Vector(hip)+Vector((0,-.04,.035))),tuple(Vector(knee)+Vector((0,-.04,.035))),.014,m['rift'],8)
hd_rock('Head_Eye'+side,(s*.115,-.816,.60),(.055,.021,.036),m['rift'],0)
hd_curve('Head_Mandible'+side,[(s*.19,-.74,.45),(s*.29,-.91,.40),(s*.17,-1.00,.44)],[.070,.046,0],m['bone'],12,6)
elif id=='e07':
hd_rock('Body_DeerCore',(0,.12,1.12),(.38,.62,.42),m['dark'],1)
for j in range(5):
y=-.38+j*.22
hd_rock('Body_BackArmor'+str(j),(0,y,1.37),(.40,.19,.21),m['basalt'],j)
for s,side in [(-1,'L'),(1,'R')]:
for j,y in enumerate([-.35,.57]):
n='Leg'+side+str(j)
hip=(s*.27,y,1.08); knee=(s*.36,y+.10,.60); foot=(s*.36,y-.02,.08)
hd_rod(n+'_Upper',hip,knee,.082,m['dark'])
hd_rock(n+'_Knee',knee,(.12,.13,.14),m['basalt'],s)
hd_rod(n+'_Shin',knee,foot,.060,m['bone'])
hd_box(n+'_Hoof',(foot[0],foot[1]-.04,.10),(.19,.27,.20),m['dark'],.025)
hd_box(n+'_ClovenLine',(foot[0],foot[1]-.179,.105),(.018,.012,.16),m['rift'],.004)
for j in range(4):
hd_curve('Body_Rib'+side+str(j),[(s*.12,-.16+j*.16,1.43),(s*.35,-.14+j*.16,1.26),(s*.32,-.13+j*.16,.99)],[.065,.074,.037],m['bone'],10,3)
hd_curve('Head_Antler'+side,[(s*.17,-.39,1.89),(s*.36,-.25,2.17),(s*.56,-.10,2.42),(s*.50,.05,2.61)],[.075,.065,.036,0],m['bone'],12,7)
for k in range(3):
z=2.05+k*.14;x=s*(.28+k*.13);y=-.30+k*.09
hd_curve('Head_AntlerProng'+side+str(k),[(x,y,z),(x+s*.19,y-.05,z+.15),(x+s*.20,y-.13,z+.28)],[.041,.028,0],m['bone'],10,5)
hd_rock('Head_Eye'+side,(s*.155,-.77,1.82),(.060,.041,.039),m['rift'],0)
hd_rock('Head_Neck',(0,-.36,1.54),(.23,.26,.37),m['basalt'],2)
hd_rock('Head_Skull',(0,-.58,1.79),(.25,.24,.25),m['bone'],4)
hd_rock('Head_Muzzle',(0,-.81,1.69),(.16,.24,.13),m['bone'],3)
hd_box('Head_Nose',(0,-1.01,1.69),(.21,.055,.12),m['dark'],.022)
hd_curve('Body_Tail',[(0,.70,1.20),(0,.89,1.05),(0,.95,.88)],[.075,.053,0],m['bone'],10,5)
else:
colossus=id=='e08'; ghost=id=='e03'
if colossus:
# A stone portcullis knight, with layered breastplates and battlements.
mm=dict(m);mm['gold']=m['bone'];mm['gold_light']=m['bone'];mm['ember']=m['rift'];mm['hot']=m['rift']
hd_tank(True,mm)
for s in [-1,1]:
hd_curve('Body_RiftHorn'+str(s),[(s*.30,.06,1.48),(s*.40,.03,1.73),(s*.25,.04,1.86)],[.095,.066,0],m['bone'],12,5)
return
hd_rock('Body_Torso',(0,.025,1.08),(.24,.17,.34),m['dark'],2)
for j in range(5):
z=.93+j*.09
for s,side in [(-1,'L'),(1,'R')]:
hd_curve('Body_Rib'+side+str(j),[(s*.04,-.12,z),(s*.20,-.12,z+.03),(s*.23,.025,z+.01)],[.025,.033,.022],m['bone'],10,3)
hd_rock('Head_Skull',(0,-.005,1.63),(.20,.145,.24),m['bone'],1)
hd_plate('Head_Face',(0,-.16,1.59),[(-.14,.08),(.14,.08),(.10,-.12),(0,-.18),(-.10,-.12)],.045,m['dark'],.012)
for s,side in [(-1,'L'),(1,'R')]:
hd_curve('Head_Eye'+side,[(s*.035,-.20,1.65),(s*.12,-.20,1.68)],[.020,.015],m['rift'],8,2)
hd_arm('Arm'+side,(s*.24,.0,1.29),(s*.38,-.09,1.09),(s*.43,-.31,1.15),m,.065)
if ghost:
for k in range(7):
a=2*pi*k/7
hd_curve('Body_Shroud'+str(k),[(.18*cos(a),.14*sin(a),1.0),(.25*cos(a+.3),.21*sin(a+.3),.59),(.14*cos(a+.6),.16*sin(a+.6),.18),(.30*cos(a+.8),.21*sin(a+.8),.04)],[.08,.09,.048,0],m['basalt'] if k%2 else m['dark'],12,6)
hd_ring('Body_RiftHalo',(0,.08,1.71),.31,.019,m['rift'],(pi/2,0,0),48)
else:
hd_legs(m,.18,.85,True)
for s in [-1,1]:
hd_curve('Weapon_Bow'+str(s),[(0,-.44,1.11),(s*.37,-.44,1.11),(s*.64,-.25,1.15)],[.05,.044,.022],m['bone'],12,6)
hd_curve('Weapon_String'+str(s),[(s*.64,-.25,1.15),(0,-.16,1.15)],[.009,.009],m['rift'],8,2)
hd_rod('Weapon_Arrow',(0,-.15,1.15),(0,-.92,1.15),.02,m['rift'],12)
def hd_mask(name,loc,r,m):
x,y,z=loc
outline=[(-r*.78,r*.58),(-r*.44,r),(.44*r,r),(.78*r,.58*r),(.61*r,-.62*r),(0,-r),(-.61*r,-.62*r)]
hd_plate(name+'_Porcelain',loc,outline,r*.32,m['bone'],.015,.025)
for s in [-1,1]:
hd_plate(name+'_EyeSocket'+str(s),(x+s*r*.33,y-r*.195,z+r*.15),[(-r*.21,r*.08),(r*.21,r*.08),(r*.16,-r*.12),(-r*.12,-r*.16)],.025,m['dark'],.003)
hd_curve(name+'_EyeLight'+str(s),[(x+s*r*.33-r*.13,y-r*.23,z+r*.12),(x+s*r*.33+r*.12,y-r*.23,z+r*.12)],[r*.027]*2,m['rift'],8,2)
hd_plate(name+'_Nose',(x,y-r*.22,z-r*.10),[(-r*.12,r*.25),(r*.12,r*.25),(0,-r*.1)],.055,m['edge'],.004)
for k in range(3):
hd_box(name+'_MouthSlit'+str(k),(x+(k-1)*r*.16,y-r*.18,z-r*.54),(r*.055,.017,r*.18),m['dark'],.003)
def build_expedition_enemy(id,m):
"""Four distinct expedition silhouettes; every part uses the same rigid rig contract."""
stone=dict(m); stone['gold']=m['bone'];stone['gold_light']=m['edge']
if id=='e05':
# Broad hunchback; massive capped stone gauntlets distinguish this bruiser.
hd_legs(stone,.32,.60)
hd_rock('Body_BruteCore',(0,.06,1.03),(.43,.31,.48),m['dark'],2)
for j in range(3):
z=.75+j*.22
for s in [-1,1]:
hd_plate('Body_Chest%d_%d'%(j,s),(s*.21,-.245,z),[(-.20,-.09),(.18,-.10),(.22,.11),(-.14,.14)],.15,m['basalt'],.024,.035)
hd_curve('Body_RiftCrack',[(0,-.34,.66),(.025,-.36,.91),(-.03,-.36,1.14),(0,-.34,1.30)],[.018]*4,m['rift'],8,3)
for j in range(4):
hd_rock('Body_BackSlab'+str(j),(0,.23,.76+j*.21),(.45,.23,.17),m['basalt'],j)
hd_mask('Head_Mask',(0,-.25,1.49),.20,m)
for s,side in [(-1,'L'),(1,'R')]:
hd_arm('Arm'+side,(s*.43,.045,1.32),(s*.65,-.02,1.00),(s*.65,-.15,.54),stone,.175)
hd_box('Arm'+side+'_StoneHammer',(s*.65,-.17,.52),(.42,.48,.36),m['basalt'],.06)
for k in range(3):
hd_box('Arm'+side+'_Knuckle'+str(k),(s*.65+(k-1)*.12,-.419,.57),(.10,.07,.18),m['bone'],.022)
hd_box('Arm'+side+'_Cuff',(s*.65,-.11,.72),(.44,.38,.075),m['edge'],.018)
for k in range(3):
hd_curve('Body_ShoulderSpike'+side+str(k),[(s*(.29+k*.13),.05,1.41),(s*(.36+k*.16),.065,1.70-k*.07)],[.10,0],m['bone'],12,5)
elif id in ('e06','e09'):
boss=id=='e09'
# Floating knotted robe and staff. The judge has a wide suspended mask crown.
hd_rock('Body_RobeCore',(0,.015,1.02),(.23,.19,.41),m['dark'],3)
for k in range(9):
a=k*2*pi/9
hd_curve('Body_RobeFold'+str(k),[(.19*cos(a),.14*sin(a),1.31),(.25*cos(a+.15),.22*sin(a+.15),.84),(.30*cos(a+.25),.27*sin(a+.25),.32),(.37*cos(a+.2),.30*sin(a+.2),.10)], [.065,.082,.054,.009],m['basalt'] if k%2 else m['dark'],12,7)
for j in range(4):
hd_ring('Body_KnotRing'+str(j),(0,0,.92+j*.085),.245-j*.012,.019,m['bone'],segments=40)
hd_mask('Head_Face',(0,-.015,1.60),.235 if boss else .195,m)
for s,side in [(-1,'L'),(1,'R')]:
hd_arm('Arm'+side,(s*.26,0,1.29),(s*.38,-.11,1.04),(s*.43,-.26,1.22),stone,.081)
hd_plate('Arm'+side+'_Pauldron',(s*.28,0,1.34),[(-.12,.02),(0,.16),(.16,.06),(.12,-.13),(-.10,-.13)],.19,m['bone'],.018)
hd_curve('Weapon_Staff',[(.47,-.28,.12),(.46,-.28,.73),(.45,-.27,1.35),(.49,-.27,1.83)],[.028,.035,.035,.025],m['bone'],12,8)
for k in range(5):
hd_ring('Weapon_StaffCollar'+str(k),(.475,-.275,.68+k*.12),.045,.012,m['edge'],segments=24)
hd_ring('Weapon_StaffKnot',(.49,-.27,1.82),.17,.03,m['bone'],(pi/2,0,0),48)
hd_rock('Weapon_StaffHeart',(.49,-.27,1.82),(.105,.06,.105),m['rift'],0)
for s in [-1,1]:
hd_curve('Weapon_KnotBranch'+str(s),[(.48+s*.06,-.27,1.68),(.48+s*.20,-.27,1.84),(.48+s*.11,-.26,1.99)],[.028,.022,0],m['bone'],10,7)
if boss:
hd_ring('Body_JudgeHalo',(0,.12,1.53),.65,.034,m['bone'],(pi/2,0,0),72)
hd_ring('Body_JudgeGlow',(0,.135,1.53),.60,.012,m['rift'],(pi/2,0,0),72)
for k in range(5):
a=.12*pi+k*.19*pi
x=.65*cos(a);z=1.53+.65*sin(a)
hd_mask('Body_CourtMask'+str(k),(x,.065,z),.16,m)
hd_curve('Body_CrownSpike'+str(k),[(x,.14,z+.12),(x*1.07,.14,z+.32)],[.065,0],m['bone'],10,5)
for s in [-1,1]:
hd_curve('Body_Chains'+str(s),[(s*.54,.10,1.63),(s*.64,-.03,1.07),(s*.46,-.16,.47)],[.018]*3,m['bone'],10,9)
else:
for j in range(3):
hd_ring('Body_HealingKnot'+str(j),((j-1)*.13,-.205,1.09),.084,.019,m['rift'],(pi/2,0,.4*j),40)
else:
# Final boss: four heavy legs, segmented carapace, actual open maw and a crown.
hd_rock('Body_CrownCore',(0,.13,.98),(.54,.58,.52),m['dark'],1)
for s,side in [(-1,'L'),(1,'R')]:
for j,y in enumerate([-.10,.64]):
n='Leg'+side+str(j);hip=(s*.40,y,.92);knee=(s*.66,y+.06,.51);foot=(s*.70,y-.10,.12)
hd_rod(n+'_Inner',hip,knee,.10,m['dark'])
hd_rock(n+'_Thigh',tuple((Vector(hip)+Vector(knee))*.5),(.18,.19,.30),m['basalt'],j)
hd_rock(n+'_Shin',tuple((Vector(knee)+Vector(foot))*.5),(.16,.18,.24),m['basalt'],j+1)
hd_disc(n+'_Joint',(knee[0],knee[1]-.18,knee[2]),.105,.045,m['bone'])
for k in range(3):
x=foot[0]+(k-1)*.09
hd_curve(n+'_Claw'+str(k),[(x,y-.05,.19),(x,y-.27,.10),(x,y-.39,.055)],[.064,.043,0],m['bone'],12,5)
for j in range(4):
hd_rock('Body_Shell'+side+str(j),(s*.37,.02+j*.20,1.20),(.25,.19,.28),m['basalt'],j)
hd_curve('Body_Ridge'+side+str(j),[(s*.32,.02+j*.20,1.37),(s*.41,.06+j*.20,1.67),(s*.30,.14+j*.20,1.78)],[.105,.069,0],m['bone'],12,6)
hd_disc('Head_MawVoid',(0,-.43,.99),.40,.15,m['dark'])
hd_ring('Head_MawRim',(0,-.53,.99),.38,.065,m['basalt'],(pi/2,0,0),64)
hd_disc('Head_RiftThroat',(0,-.522,.99),.22,.025,m['rift'])
for k in range(14):
a=k*2*pi/14
hd_curve('Head_Tooth'+str(k),[(.35*cos(a),-.58,.99+.35*sin(a)),(.29*cos(a),-.63,.99+.29*sin(a)),(.19*cos(a+.1),-.60,.99+.19*sin(a+.1))],[.058,.044,0],m['bone'],12,5)
for s in [-1,1]:
hd_rock('Head_Brow'+str(s),(s*.24,-.34,1.41),(.27,.17,.16),m['basalt'],s)
hd_curve('Head_Eye'+str(s),[(s*.12,-.498,1.41),(s*.31,-.49,1.45)],[.023,.029],m['rift'],10,4)
hd_ring('Head_CrownBand',(0,.02,1.63),.36,.052,m['bone'],segments=64)
for k in range(7):
a=k*2*pi/7
hd_curve('Head_CrownPoint'+str(k),[(.36*cos(a),.02+.36*sin(a),1.62),(.47*cos(a),.02+.47*sin(a),1.91),(.37*cos(a),.02+.37*sin(a),2.20 if k%2 else 2.06)],[.095,.065,0],m['bone'],12,7)
hd_rock('Head_CrownGem'+str(k),(.38*cos(a),.02+.38*sin(a),1.69),(.055,)*3,m['rift'],k)
hd_curve('Body_Tail',[(0,.61,.86),(0,.97,.66),(.18,1.13,.35),(.30,1.22,.25)],[.14,.11,.06,0],m['basalt'],16,8)
def enhance_existing(spec,m):
"""Round existing neutral/environment geometry and add constructed details."""
for obj in list(bpy.context.scene.objects):
if obj.type!='MESH':continue
dimensions=obj.dimensions
width=min(min(dimensions)*.09,.014)
hd_finish(obj,max(.001,width),3)
if spec['role']=='lane':
hd_materials(m)
# Detailed edge stones, anchored brass plates and inset light wells.
for s in [-1,1]:
for j in range(4):
y=-1.5+j
hd_box('Body_EdgeStone%d_%d'%(s,j),(s*6.13,y,.16),(.30,.91,.29),m['basalt'],.035)
hd_box('Body_EdgeCap%d_%d'%(s,j),(s*6.13,y,.32),(.34,.92,.060),m['gold'],.014)
for j,y in enumerate([-.95,.95]):
hd_box('Body_LightRecess%d_%d'%(s,j),(s*6.13,y,.364),(.15,.22,.035),m['dark'],.009)
hd_box('Body_LightGlass%d_%d'%(s,j),(s*6.13,y,.387),(.065,.14,.013),m['ember'],.005)
elif spec['role']=='core':
hd_materials(m)
for k in range(8):
a=k*pi/4
hd_box('Body_PlinthArmor'+str(k),(1.02*cos(a),1.02*sin(a),.18),(.37,.25,.29),m['basalt'],.029,rotation=(0,0,a+pi/2))
hd_ring('Body_PlinthGold'+str(k),(1.04*cos(a),1.04*sin(a),.35),.068,.012,m['gold'],segments=24)
hd_ring('Body_OuterRim',(0,0,.36),1.19,.026,m['gold'],segments=72)
def finalize_high_geometry():
"""Resolve tiny bevel slivers and retain valid corner normals after triangulation.
Thin inset discs can produce normals across their back edge when weighted
normals are computed on n-gons. Triangulate first, then constrain only those
corner normals that point below the actual triangle surface.
"""
for obj in bpy.context.scene.objects:
if obj.type != 'MESH': continue
mesh=obj.data
bm=bmesh.new(); bm.from_mesh(mesh)
bmesh.ops.remove_doubles(bm,verts=list(bm.verts),dist=.000001)
bmesh.ops.dissolve_degenerate(bm,edges=list(bm.edges),dist=.000001)
bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces))
bmesh.ops.triangulate(bm,faces=list(bm.faces),quad_method='BEAUTY',ngon_method='EAR_CLIP')
bm.to_mesh(mesh);bm.free();mesh.update()
normals=[tuple(n.vector) for n in mesh.corner_normals]
for face in mesh.polygons:
for index in face.loop_indices:
if Vector(normals[index]).dot(face.normal)<.01:
normals[index]=tuple(face.normal)
mesh.normals_split_custom_set(normals)
from math import sin, cos, pi
import bpy
from mathutils import Vector
def living_materials(m):
colors={'bark':('503D2D',.85,0),'wood':('836343',.79,0),
'endgrain':('92764E',.86,0),'groove':('312B24',.93,0),'moss':('3E6041',.90,0),
'leaf':('7D9850',.65,0),'vein':('A9B875',.67,0),
'petal':('AD6D8A',.56,0),'heart':('E9BD79',.52,0),
'water':('65D6B5',.18,.65)}
for key,(hexcolor,rough,emission) in colors.items():
name='SW_HD_W_'+key
mat=bpy.data.materials.new(name);mat['material_name']=name;mat.use_nodes=True
c=tuple(srgb_to_linear(int(hexcolor[i:i+2],16)/255) for i in (0,2,4))
mat.diffuse_color=(*c,1)
node=mat.node_tree.nodes.get('Principled BSDF')
node.inputs['Base Color'].default_value=(*c,1)
node.inputs['Roughness'].default_value=rough
if emission:
node.inputs['Emission Color'].default_value=(*c,1)
node.inputs['Emission Strength'].default_value=emission
m[key]=mat
return m
def living_blob(name,loc,scale,mat):
obj=ellipsoid(name,loc,scale,mat,subdivisions=2 if max(scale) < .085 else 3)
if 'bark' in mat.name or 'moss' in mat.name:
for v in obj.data.vertices:
p=v.co
p *= 1+.045*sin(p.x*27+p.z*13)*cos(p.y*21)
for face in obj.data.polygons:face.use_smooth=True
return obj
def living_leaf(name,start,end,width,m,key='leaf'):
"""Closed lenticular leaf, curved midrib and six modeled secondary veins."""
a,b=Vector(start),Vector(end);d=b-a
tangent=d.normalized();side=tangent.cross(Vector((0,-1,.3))).normalized()
if side.length<.1:side=Vector((1,0,0))
normal=side.cross(tangent).normalized()
verts=[]; rows=9; sides=8
for j in range(rows):
t=.005+.99*j/(rows-1)
center=a+d*t+normal*(sin(t*pi)*width*.20)
w=max(.001,sin(t*pi)**.85*width*(1+.045*sin(t*13*pi)))
for k in range(sides):
angle=2*pi*k/sides
verts.append(tuple(center+side*cos(angle)*w+normal*sin(angle)*w*.18))
faces=[tuple(reversed(range(sides)))]
for j in range(rows-1):
for k in range(sides):
n=(k+1)%sides;faces.append((j*sides+k,j*sides+n,(j+1)*sides+n,(j+1)*sides+k))
faces.append(tuple((rows-1)*sides+k for k in range(sides)))
obj=mesh_object(name,verts,faces,m[key])
for face in obj.data.polygons:face.use_smooth=True
points=[a+d*t+normal*(sin(t*pi)*width*.39+.005) for t in [0,.3,.65,1]]
hd_curve(name+'_Midrib',points,[.009,.009,.006,.001],m['vein'],6,3)
for j in range(3):
t=.25+j*.20
center=a+d*t+normal*(sin(t*pi)*width*.39+.008)
for sign in [-1,1]:
tip=a+d*(t+.12)+side*sign*width*sin((t+.12)*pi)*.8+normal*width*.24
hd_curve(name+'_Vein%d_%d'%(j,sign),[center,(center+tip)*.5+normal*.005,tip],[.004,.003,.001],m['vein'],5,2)
return obj
def living_branch(name,points,radius,m,grooves=3):
points=[Vector(p) for p in points]
obj=hd_curve(name,points,[radius*(1-.60*j/(len(points)-1)) for j in range(len(points))],m['bark'],16,7)
for vertex in obj.data.vertices:
p=vertex.co
vertex.co += Vector((sin(p.z*35+p.y*17),cos(p.z*27+p.x*18),sin(p.x*19+p.y*31)))*radius*.11
for k in range(grooves):
angle=2*pi*k/max(1,grooves)
ridges=[p+Vector((cos(angle),sin(angle),0))*radius*(.96-.57*j/(len(points)-1)) for j,p in enumerate(points)]
hd_curve(name+'_Grain'+str(k),ridges,[radius*.11]*(len(points)-1)+[.001],m['wood'],6,5)
def living_roots(m,count=4,spread=.43,height=.55,radius=.12):
for k in range(count):
a=2*pi*k/count+pi/4
group=('LegL' if cos(a)<0 else 'LegR')+('1' if k>=2 else '')
hip=Vector((.13*cos(a),.13*sin(a),height))
knee=Vector((spread*.70*cos(a),spread*.60*sin(a),.31))
foot=Vector((spread*cos(a),spread*sin(a),.12))
living_branch(group+'_Root',[hip,knee,foot],radius,m,5)
for j in range(3):
angle=a+(j-1)*.48
tip=foot+Vector((.24*cos(angle),.24*sin(angle),-.10))
hd_curve(group+'_Toe'+str(j),[knee*.18+foot*.82,foot+Vector((.09*cos(angle),.09*sin(angle),.025)),tip],[radius*.65,radius*.4,.003],m['wood'],12,6)
hd_curve(group+'_Moss',[hip+Vector((0,-radius*.6,0)),knee+Vector((0,-radius*.7,0)),foot],[radius*.28,radius*.22,.001],m['moss'],10,5)
living_leaf(group+'_Sprout',knee,knee+Vector((.14*cos(a),-.035,.17)),.055,m)
def living_flower(name,loc,m,scale=1):
center=Vector(loc)
for k in range(5):
angle=k*2*pi/5
p=center+Vector((.035*cos(angle),-.008,.035*sin(angle)))*scale
obj=living_blob(name+'_Petal'+str(k),p,(.027*scale,.012*scale,.040*scale),m['petal'])
obj.rotation_euler.y=pi/2-angle
living_blob(name+'_Pollen',center+Vector((0,-.022,0))*scale,(.017*scale,)*3,m['heart'])
def living_trunk(m,height=.85,radius=.14,base=.35,count=4):
for k in range(count):
phase=2*pi*k/count
points=[]
for j in range(7):
t=j/6;a=phase+t*pi*1.8
points.append((radius*cos(a),radius*sin(a),base+t*height))
living_branch('Body_Braid'+str(k),points,.073 if count<5 else .09,m,3)
for k in range(8):
a=k*2.4;z=base+.15+k*height*.08
start=(radius*cos(a),radius*sin(a),z)
living_leaf('Body_TrunkLeaf'+str(k),start,(.34*cos(a),.30*sin(a),z+.16),.064,m)
def living_bark(name,loc,width,height,m,seed=0):
outline=[(-width*.46,height*.44),(-width*.05,height*.56),(width*.49,height*.35),(width*.46,-height*.29),(width*.12,-height*.54),(-width*.44,-height*.40)]
obj=hd_plate(name,loc,outline,.075,m['wood'],.012,.045)
x,y,z=loc
for j in range(4):
dx=(j-1.5)*width*.19
hd_curve(name+'_Fissure'+str(j),[(x+dx,y-.074,z-height*.29),(x+dx+.008*sin(seed+j),y-.096,z),(x+dx-.01,y-.068,z+height*.33)],[.005,.008,.004],m['bark'],6,3)
return obj
def living_thorn(up,m):
living_roots(m,4 if up else 2,.38 if up else .32,.52,.12)
if not up:
living_blob('Body_MossHeart',(0,.02,.83),(.34,.28,.40),m['moss'])
for k in range(6):
a=k*pi/3
points=[]
for j in range(6):
t=j/5;radius=.18+.17*sin(t*pi)
points.append((radius*cos(a+t*.8),radius*.86*sin(a+t*.8),.48+t*.68))
living_branch('Body_WovenBark'+str(k),points,.037,m,2)
for k in range(4):
a=k*pi/2
living_leaf('Body_ShoulderLeaf'+str(k),(.20*cos(a),.20*sin(a),1.07),(.34*cos(a),.31*sin(a),1.24),.064,m)
for k in range(9):
a=k*2*pi/9
# Local bark plates turn as a whole, including their carved lines.
before=set(bpy.context.scene.objects)
living_bark('Body_Bark'+str(k),(0,-.275,.84),.23,.54,m,k)
for o in set(bpy.context.scene.objects)-before:
from mathutils import Matrix
mat=Matrix.Rotation(a,4,'Z');o.location=mat@o.location;o.rotation_euler.z=a
hd_curve('Body_Thorn'+str(k),[(.28*cos(a),.25*sin(a),.88),(.45*cos(a),.39*sin(a),1.01),(.56*cos(a),.48*sin(a),1.08)],[.08,.045,.001],m['leaf'],12,6)
hd_plate('Head_EyeShadow',(0,-.339,.85),[(-.20,.026),(.20,.026),(.12,-.029),(-.16,-.025)],.025,m['groove'],.004)
for sign in [-1,1]:
hd_disc('Head_Eye'+str(sign),(sign*.11,-.36,.85),.017,.014,m['heart'])
living_leaf('Head_Sprout'+str(sign),(0,0,1.18),(sign*.20,.02,1.48),.093,m)
living_flower('Body_Blossom',(-.24,-.20,1.11),m)
else:
living_trunk(m,.60,.23,.31,5)
living_blob('Head_JawDark',(0,-.12,.94),(.36,.32,.19),m['groove'])
for upper,z in [(True,1.13),(False,.72)]:
name='Head_Upper' if upper else 'Head_Lower'
living_blob(name+'_Jaw',(0,-.14,z),(.41,.38,.13),m['bark'])
for row in range(3):
hd_curve(name+'_Lip'+str(row),[(-.32,.02,z),(-.40,-.25,z+.015*row),(-.20,-.51,z+.02*row),(0,-.54,z),( .20,-.51,z+.02*row),(.40,-.25,z+.015*row),(.32,.02,z)],[.05]*7,m['moss'] if row==0 else m['wood'],12,5)
for k in range(9):
a=pi+(k/8)*pi
x=.35*cos(a);y=-.17+.34*sin(a)
hd_curve(name+'_Fang'+str(k),[(x,y,z+(-.05 if upper else .06)),(x*.97,y-.015,z+(-.11 if upper else .10)),(x*.9,y-.03,z+(-.24 if upper else .20))],[.045,.028,.001],m['heart'],12,5)
for k in range(6):
a=k*pi/3
living_leaf('Head_JawLeaf'+str(k),(.26*cos(a),.21*sin(a),1.13),(.49*cos(a),.41*sin(a),1.30),.11,m)
living_flower('Head_JawFlower',(-.32,-.20,1.26),m)
def living_log(name,loc,r,m):
hd_disc(name+'_Bark',loc,r,.15,m['bark'])
x,y,z=loc
cut=hd_disc(name+'_Cut',(x,y-.086,z),r*.86,.025,m['endgrain'])
# Local UVs retain the centre of each log when parts are batched for Godot.
uv=cut.data.uv_layers.active or cut.data.uv_layers.new(name='GrowthRings')
for loop in cut.data.loops:
v=cut.data.vertices[loop.vertex_index].co
uv.data[loop.index].uv=(.5+v.x/(r*1.72),.5+v.y/(r*1.72))
for j in range(6):
radius=r*(.11+j*.127)
verts=[];faces=[];segments=64;sides=6
for k in range(segments):
a=k*2*pi/segments
organic=1+.045*sin(a*3+j*.61)+.035*cos(a*7-j)
for side in range(sides):
b=side*2*pi/sides
rr=radius*organic+.0019*cos(b)
verts.append((x+rr*cos(a),y-.102+.0019*sin(b),z+rr*sin(a)))
for k in range(segments):
for side in range(sides):
a=k*sides+side;b=k*sides+(side+1)%sides
c=((k+1)%segments)*sides+(side+1)%sides;d=((k+1)%segments)*sides+side
faces.append((a,b,c,d))
mesh_object(name+'_Growth'+str(j),verts,faces,m['bark'])
for k in range(13):
a=k*2*pi/13
hd_curve(name+'_RoughEdge'+str(k),[(x+r*.92*cos(a),y+.045,z+r*.92*sin(a)),(x+r*cos(a),y-.025,z+r*sin(a)),(x+r*.94*cos(a),y-.09,z+r*.94*sin(a))],[.017,.027,.015],m['wood'],8,4)
for k in range(3):
a=k*2.7+r
hd_curve(name+'_Split'+str(k),[(x+r*.84*cos(a),y-.105,z+r*.84*sin(a)),(x+r*.66*cos(a+.015),y-.109,z+r*.66*sin(a+.015)),(x+r*.50*cos(a-.023),y-.109,z+r*.50*sin(a-.023))],[.0038,.0023,.0007],m['groove'],5,2)
def living_tank(up,m):
living_roots(m,4,.44,.57,.14)
living_trunk(m,.70,.20,.32,5)
living_blob('Body_InnerMoss',(0,.08,.86),(.41,.31,.40),m['moss'])
if up:
from mathutils import Matrix
for row in range(3):
for k in range(8):
a=k*2*pi/8+row*.24
before=set(bpy.context.scene.objects)
living_bark('Body_OverlappingBark%d_%d'%(row,k),(0,-.30,.62+row*.23),.22,.31,m,k+row)
rotation=Matrix.Rotation(a,4,'Z')
for obj in set(bpy.context.scene.objects)-before:
obj.location=rotation@obj.location;obj.location.y+=.08;obj.rotation_euler.z=a
for k,(x,y,z,r) in enumerate([(-.28,-.22,1.04,.31),(.28,-.19,.87,.27),(0,.02,1.34,.20),(-.36,-.18,.55,.15),(.39,-.15,.57,.15),(0,.37,1.02,.26)]):
living_log('Body_Log'+str(k),(x,y,z),r,m)
else:
for row in range(3):
for k in range(7):
a=k*2*pi/7+row*.45
from mathutils import Matrix
before=set(bpy.context.scene.objects)
living_bark('Body_Cork%d_%d'%(row,k),(0,-(.40-row*.07),.64+row*.23),.26,.31,m,k+row)
rotation=Matrix.Rotation(a,4,'Z')
for obj in set(bpy.context.scene.objects)-before:
obj.location=rotation@obj.location;obj.rotation_euler.z=a
for k in range(11):
a=k*2.4
z=1.22+(k%3)*.07
living_leaf('Head_Canopy'+str(k),(.12*cos(a),.10*sin(a),z),(.37*cos(a),.30*sin(a),z+.15),.073,m)
for k in range(5):
living_flower('Body_Flower'+str(k),(-.30+k*.15,-.26,1.10+.12*sin(k*2)),m,.7)
def living_cup(name,loc,r,m,flower=False):
x,y,z=loc
material=m['petal'] if flower else m['moss']
obj=r_lathe(name+'_Shell',loc,[(0,0),(r*.2,.02),(r*.42,.07),(r*.8,.19),(r,.32),(r*1.02,.35),(r*.94,.365),(r*.76,.24),(r*.4,.12),(0,.09)],material,segments=48)
for p in obj.data.polygons:p.use_smooth=True
hd_ring(name+'_Rim',(x,y,z+.35),r,.024,m['leaf'],segments=64)
count=(12 if r > .2 else 5) if flower else 10
for k in range(count):
a=k*2*pi/count
hd_curve(name+'_Rib'+str(k),[(x+r*.2*cos(a),y+r*.2*sin(a),z+.06),(x+r*.54*cos(a),y+r*.54*sin(a),z+.12),(x+r*.90*cos(a),y+r*.9*sin(a),z+.30)],[.015,.017,.008],m['wood'] if not flower else m['petal'],8,5)
if flower:
living_leaf(name+'_Fringe'+str(k),(x+r*.98*cos(a),y+r*.98*sin(a),z+.34),(x+r*1.08*cos(a),y+r*1.08*sin(a),z+.17),.047,m)
def living_ranged(up,m):
living_roots(m,4 if up else 2,.32,.48,.09)
living_trunk(m,.77,.09,.32,4)
living_cup('Head_Launcher',(0,-.04,1.03),.30 if up else .26,m)
living_blob('Head_Seed',(0,-.04,1.31),(.15,.145,.20 if up else .12),m['wood'])
for k in range(7):
a=k*2*pi/7
hd_curve('Head_SeedSeam'+str(k),[(.10*cos(a),-.04+.10*sin(a),1.24),(.151*cos(a),-.04+.145*sin(a),1.34),(0,-.04,1.51 if up else 1.43)],[.006,.007,.001],m['bark'],6,4)
for k in range(12 if up else 5):
a=k*2*pi/(6 if up else 5)
row=k//6 if up else 0
living_leaf('Head_LauncherScale'+str(k),(.13*cos(a),-.04+.13*sin(a),1.06+.12*row),(.36*cos(a),-.04+.36*sin(a),1.51+.08*row),.13 if up else .10,m,'wood' if up else 'leaf')
for sign in [-1,1]:
living_leaf('Body_BroadLeaf'+str(sign),(sign*.04,0,.64),(sign*.43,-.02,.94),.13,m)
living_flower('Body_Flower'+str(sign),(sign*.11,-.11,.81),m,.65)
def living_artillery(up,m):
living_roots(m,4,.43,.56,.12)
living_trunk(m,.92 if up else .65,.14,.32,4)
positions=[(-.30,-.03,.78,.30),(.30,.03,.68,.29),(0,.10,1.29,.43)] if up else [(0,0,.96,.48)]
for k,(x,y,z,r) in enumerate(positions):
living_branch('Body_FungalTrunk'+str(k),[(0,0,.4),(x*.6,y,.74),(x,y,z+.02)],.11,m,4)
living_cup('Head_Cup'+str(k),(x,y,z),r,m,True)
for j in range(5):
a=j*2*pi/5
living_blob('Weapon_Spore%d_%d'%(k,j),(x+r*.30*cos(a),y+r*.30*sin(a),z+.18),(.045,.043,.083),m['heart'])
for k in range(4 if up else 2):
sign=-1 if k%2 else 1
living_cup('Body_SmallCup'+str(k),(sign*.26,.04,.48+.13*k),.11,m,True)
def living_support(up,m):
living_roots(m,4 if up else 2,.39,.52,.105)
if up:
for sign in [-1,1]:
for j in range(3):
living_branch('Body_Arch%d_%d'%(sign,j),[(sign*.18,.07,.36),(sign*(.36+j*.015),.10,.83),(sign*.26,.07,1.27),(sign*.07,.08,1.48) ],.088-j*.012,m,3)
for k in range(24):
a=k*2.4;row=k//8
living_leaf('Head_Canopy'+str(k),(.05*cos(a),.06,1.48-row*.07),(.45*cos(a),.06+.34*sin(a),1.28-row*.10),.13,m)
else:
living_trunk(m,.93,.09,.31,4)
for k in range(5):
a=k*2*pi/5
living_leaf('Head_Crown'+str(k),(.02*cos(a),0,1.18),(.36*cos(a),.17*sin(a),1.45+(k==2)*.17),.14,m)
r=.33 if up else .25; z=.79
living_cup('Weapon_Basin',(0,-.22,z-.16),r,m)
water=hd_finish(cylinder('Weapon_Water',(0,-.22,z+.075),r*.81,.025,m['water'],64),0,0,True)
for k in range(3):
hd_ring('Weapon_Ripple'+str(k),(0,-.22,z+.091),r*(.18+k*.24),.006,m['vein'],segments=48)
if not up:living_blob('Weapon_DewOrb',(0,-.22,z+.18),(.095,.095,.095),m['water'])
for sign,side in [(-1,'L'),(1,'R')]:
living_branch('Arm'+side+'_BasinVine',[(sign*.12,0,z+.25),(sign*.28,-.11,z+.22),(sign*r,-.22,z+.025)],.048,m,3)
for k in range(6 if up else 3):
a=k*2.4
living_flower('Body_Blossom'+str(k),(.31*cos(a),-.15,1.25+.20*sin(a)),m,.85)
def build_high_wurzelbund(spec,m):
living_materials(m)
{'melee':living_thorn,'tank':living_tank,'ranged':living_ranged,'artillery':living_artillery,'support':living_support}[spec['role']](spec['upgraded'],m)
def build_high_worker(spec,m):
"""Clay and brass gatherer: real joints, apron, gloves, tool and reservoir."""
hd_materials(m)
for sign,side in [(-1,'L'),(1,'R')]:
name='Leg'+side
hd_rod(name+'_Shin',(sign*.15,.02,.16),(sign*.15,.02,.37),.065,m['dark'])
hd_box(name+'_Boot',(sign*.15,-.05,.095),(.22,.32,.16),m['basalt'],.045)
hd_box(name+'_Sole',(sign*.15,-.055,.035),(.235,.33,.045),m['dark'],.012)
hd_ring(name+'_Cuff',(sign*.15,.02,.22),.082,.019,m['gold'],segments=40)
for j in range(3):
hd_box(name+'_ToeRidge'+str(j),(sign*.15,-.13+j*.055,.179),(.18,.018,.012),m['gold'],.003)
living_blob('Body_ClayTorso',(0,.025,.53),(.23,.17,.25),m['ceramic'])
hd_plate('Body_LeatherApron',(0,-.154,.48),[(-.17,.17),(.17,.17),(.21,-.20),(-.21,-.20)],.045,m['basalt'],.018,.02)
for j in range(7):
hd_disc('Body_ApronRivet'+str(j),(-.16+j*.053,-.19,.40),.009,.012,m['gold_light'])
hd_box('Body_ToolPocket',(0,-.195,.50),(.20,.025,.10),m['gold'],.012)
for sign in [-1,1]:
hd_curve('Body_Harness'+str(sign),[(sign*.17,-.18,.58),(sign*.15,-.13,.73),(sign*.12,.05,.78),(sign*.17,.17,.59)],[.019]*4,m['gold'],10,5)
living_blob('Head_Hood',(0,.015,.85),(.22,.19,.21),m['basalt'])
hd_disc('Head_FaceRecess',(0,-.18,.85),.145,.03,m['dark'])
for sign in [-1,1]:
hd_disc('Head_GoggleSocket'+str(sign),(sign*.075,-.205,.87),.064,.036,m['gold'])
hd_disc('Head_GoggleLens'+str(sign),(sign*.075,-.229,.87),.048,.019,m['bone'])
hd_disc('Head_GoggleGlint'+str(sign),(sign*.069,-.241,.885),.016,.009,m['ceramic'])
hd_plate('Head_Cap',(0,-.01,1.005),[(-.24,0),(-.14,.055),(.14,.055),(.24,0),(.14,-.016),(-.14,-.016)],.37,m['gold'],.012)
hd_disc('Head_LampHousing',(0,-.19,1.017),.038,.035,m['dark'])
hd_disc('Head_Lamp',(0,-.213,1.017),.025,.013,m['hot'])
hd_finish(cylinder('Body_Reservoir',(0,.235,.62),.16,.35,m['bone'],32),.01,3,True)
for z in [.45,.79]:
hd_ring('Body_TankRim'+str(z),(0,.235,z),.17,.025,m['gold'],segments=48)
for k in range(8):
a=k*pi/4
hd_rod('Body_TankBrace'+str(k),(.17*cos(a),.235+.17*sin(a),.45),(.17*cos(a),.235+.17*sin(a),.79),.012,m['gold'])
for sign,side in [(-1,'L'),(1,'R')]:
a=(sign*.21,.0,.66);b=(sign*.30,-.08,.50);c=(sign*.29,-.19,.55)
hd_rod('Arm'+side+'_Upper',a,b,.065,m['basalt'])
hd_rod('Arm'+side+'_Forearm',b,c,.06,m['ceramic'])
hd_disc('Arm'+side+'_Elbow',(b[0],b[1]-.04,b[2]),.053,.04,m['gold'])
living_blob('Arm'+side+'_Glove',c,(.066,.06,.072),m['dark'])
for j in range(3):
hd_rod('Arm'+side+'_Finger'+str(j),(c[0]+(j-1)*.025,c[1]-.045,c[2]),(c[0]+(j-1)*.025,c[1]-.049,c[2]-.055),.013,m['gold'])
hd_rod('WeaponL_Handle',(-.30,-.22,.23),(-.30,-.22,.93),.025,m['gold'])
hd_curve('WeaponL_Fork',[(-.44,-.22,.87),(-.40,-.22,.76),(-.30,-.22,.74),(-.20,-.22,.76),(-.16,-.22,.87)],[.022]*5,m['bone'],12,5)
hd_rod('WeaponR_Probe',(.29,-.22,.53),(.40,-.34,.72),.027,m['gold'])
def build_high_core(spec,m):
"""Cut mineral held by an engraved stone/brass cradle and satellite shards."""
hd_materials(m)
team=int(spec.get('team',0))
crystal=bpy.data.materials.new('SW_HD_Core_%d'%team);crystal['material_name']=crystal.name;crystal.use_nodes=True
color=('65D9C1' if team==0 else 'EE9B82')
c=tuple(srgb_to_linear(int(color[i:i+2],16)/255) for i in (0,2,4))
node=crystal.node_tree.nodes.get('Principled BSDF');node.inputs['Base Color'].default_value=(*c,1)
node.inputs['Metallic'].default_value=.28;node.inputs['Roughness'].default_value=.20
node.inputs['Emission Color'].default_value=(*c,1);node.inputs['Emission Strength'].default_value=.65
crystal.diffuse_color=(*c,1)
for j,(r,z,h) in enumerate([(1.20,.12,.24),(1.07,.27,.10),(.88,.39,.18),(.68,.51,.12)]):
hd_finish(cylinder('Body_CoreStep'+str(j),(0,0,z),r,h,m['basalt'],48),.027,3)
hd_ring('Body_CoreStepRim'+str(j),(0,0,z+h*.44),r*.98,.018,m['gold'],segments=64)
if team == 1:
corners=[Vector((.97*cos(k*2*pi/3),.97*sin(k*2*pi/3),.367)) for k in range(3)]
for k in range(3):
hd_rod('Team_Triangle'+str(k),corners[k],corners[(k+1)%3],.022,crystal)
else:
hd_ring('Team_Circle',(0,0,.367),.97,.022,crystal,segments=64)
for k in range(12):
a=k*pi/6
hd_box('Body_PlinthBlock'+str(k),(1.08*cos(a),1.08*sin(a),.21),(.23,.25,.24),m['edge'],.025,rotation=(0,0,a))
hd_ring('Body_Seal'+str(k),(1.06*cos(a),1.06*sin(a),.346),.042,.009,m['gold_light'],segments=24)
hd_rod('Body_RadialInlay'+str(k),(.90*cos(a),.90*sin(a),.453),(.60*cos(a),.60*sin(a),.453),.012,crystal)
for k in range(6):
a=k*pi/3
hd_curve('Body_Cradle'+str(k),[(.66*cos(a),.66*sin(a),.47),(.75*cos(a),.75*sin(a),.67),(.60*cos(a),.60*sin(a),1.02),(.43*cos(a),.43*sin(a),1.14)],[.09,.083,.060,.035],m['gold'],16,7)
hd_ring('Body_CradleCollar'+str(k),(.60*cos(a),.60*sin(a),.90),.065,.015,m['gold_light'],segments=32)
def shard(name,loc,r,height,tilt=0):
vertices=[(0,0,-height*.45)]
for j,(radius,z) in enumerate([(r*.78,-height*.23),(r,height*.13),(r*.72,height*.31)]):
for k in range(7):
a=k*2*pi/7+.10*j
vertices.append((radius*cos(a),radius*sin(a),z))
vertices.append((r*.16,0,height*.57));faces=[]
for k in range(7):faces.append((0,1+(k+1)%7,1+k))
for j in range(2):
for k in range(7):
a=1+j*7+k;b=1+j*7+(k+1)%7;faces.append((a,b,b+7,a+7))
for k in range(7):faces.append((15+k,15+(k+1)%7,22))
obj=mesh_object(name,vertices,faces,crystal);obj.location=loc;obj.rotation_euler=(tilt,tilt*.4,0)
hd_finish(obj,.006,2)
shard('Core_CrystalMain',(0,0,1.41),.41,1.68)
for k in range(5):
a=k*2*pi/5
shard('Core_CrystalSatellite'+str(k),(.43*cos(a),.43*sin(a),.89+(k%2)*.10),.15,.71,.18*cos(a))
hd_ring('Body_CrystalHalo',(0,0,1.19),.60,.021,m['gold_light'],segments=72)
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
from math import pi, sin, cos
import bpy
import hashlib
def g_legs(m, spread=.28, height=.40, lean=0, slim=False):
"""Bent stone legs, joint rings and two separate forward toes."""
for s,side in [(-1,'L'),(1,'R')]:
ankle=(s*spread,-.015,.14)
knee=(s*spread*1.1,.03+lean,height*.61)
hip=(s*spread*.72,.06,height)
radius=.075 if slim else .155
r_band('Leg'+side+'_Thigh',hip,knee,radius,m['primary'],6)
r_band('Leg'+side+'_Shin',knee,ankle,radius*.9,m['primary'],6)
r_rock('Leg'+side+'_Knee',knee,(radius*1.2,radius,radius*1.1),m['primary'],s*.2)
r_disc('Leg'+side+'_Joint',(knee[0],knee[1]-.085,knee[2]),radius*.60,.06,m['accent'])
r_band('Leg'+side+'_AnkleBand',(ankle[0],ankle[1],.10),(ankle[0],ankle[1],.17),radius*1.1,m['accent'])
for j in range(2):
x=ankle[0]+(j-.5)*(.12 if slim else .15)
r_block('Leg'+side+'_Toe'+str(j),(x,-.11,.065),(.13 if slim else .18,.34,.13),m['primary'])
def g_arm(name, shoulder, elbow, hand, m, radius=.16, gold=True):
r_rock(name+'_Shoulder',shoulder,(radius*1.40,radius*1.24,radius*1.36),m['primary'],.23)
r_band(name+'_Upper',shoulder,elbow,radius,m['primary'],7)
r_rock(name+'_Elbow',elbow,(radius*1.12,)*3,m['primary'],.7)
r_band(name+'_Lower',elbow,hand,radius*1.1,m['primary'],7)
if gold:
a=tuple(elbow[i]*.35+hand[i]*.65 for i in range(3))
b=tuple(elbow[i]*.15+hand[i]*.85 for i in range(3))
r_band(name+'_Cuff',a,b,radius*1.24,m['accent'],8)
r_rock(name+'_Hand',hand,(radius*1.35,radius*1.2,radius*1.25),m['primary'],-.4)
def g_coal(up,m):
"""Wide jaw-dominated rock creature, short legs, hanging claws and a crest."""
g_legs(m,.26,.40)
r_rock('Body_MoltenCore',(0,.02,.73),(.43,.30,.50),m['secondary'],.16)
r_rock('Body_BackRock',(0,.15,.79),(.46,.27,.46),m['primary'],.37)
# A real wide mouth is framed by stone teeth, rather than a separate white face.
jaw=[(-.36,.10),(-.28,.20),(-.16,.16),(0,.23),(.17,.14),(.30,.18),(.38,.06),
(.29,-.18),(.16,-.09),(.02,-.23),(-.13,-.10),(-.29,-.19)]
r_plate('Head_GlowingJaw',(0,-.298,.75),jaw,.045,m['secondary'])
for s,side in [(-1,'L'),(1,'R')]:
brow=[(-.20,-.04),(-.04,.02),(.19,-.12),(.20,.17),(-.04,.22),(-.19,.11)]
r_plate('Head_Brow'+side,(s*.19,-.335,.97),[(s*x,z) for x,z in brow],.16,m['primary'],.06)
r_sweep('Head_Eye'+side,[(s*.29,-.431,.92),(s*.11,-.434,.95)],[.015,.024],m['secondary'],4)
r_rock('Head_Cheek'+side,(s*.34,-.19,.73),(.19,.18,.27),m['primary'],s*.28)
for j in range(3):
x=s*(.07+j*.095)
r_sweep('Head_UpperFang'+side+str(j),[(x,-.39,.95-j*.022),(x+s*.018,-.408,.79-j*.036)],
[.069,0],m['primary'],4)
r_sweep('Head_LowerFang'+side+str(j),[(x,-.315,.49+j*.025),(x,-.395,.65+j*.035)],
[.075,0],m['primary'],4)
sh=(s*.45,.01,.91)
elbow=(s*.58,-.045,.66)
hand=(s*(.65 if up else .59),-.18,.46)
g_arm('Arm'+side,sh,elbow,hand,m,.145,up)
r_rock('Arm'+side+'_GoldPlate',(s*.47,-.08,1.035),(.105,.08,.13),m['accent'])
for j in range(2):
x=hand[0]+(j-.5)*.18
if up:
points=[(x,-.245,.57),(x+s*.04,-.42,.46),(x+s*.03,-.52,.27),(x-s*.065,-.48,.15)]
radii=[.12,.12,.065,0]
else:
points=[(x,-.245,.51),(x+s*.025,-.36,.43),(x+s*.01,-.42,.30),(x-s*.025,-.40,.24)]
radii=[.075,.07,.038,0]
r_sweep('Weapon'+side+'_Claw'+str(j),points,radii,m['accent'],6)
if up:
r_sweep('Body_ShoulderHorn'+side,[(s*.40,.03,1.03),(s*.53,.03,1.24),(s*.46,.02,1.33)],
[.15,.09,0],m['primary'],5)
crest=[(0,.15,1.14),(0,.13,1.34 if up else 1.29),(0,.08,1.47 if up else 1.34)]
r_sweep('Head_CragCrown',crest,[.22,.12,0],m['primary'],5)
r_rock('Head_CrownGold',(0,-.055,1.19),(.075,.055,.115),m['accent'])
if up:
for s in [-1,1]:
r_sweep('Body_DorsalSpire'+str(s),[(s*.20,.21,1.11),(s*.26,.21,1.42)],
[.13,0],m['primary'],5)
def g_tower(name,loc,size,m,slots=2):
"""Solid shoulder fortress with true separated crenellations."""
x,y,z=loc
w,d,h=size
r_block(name+'_Keep',loc,size,m['primary'])
r_block(name+'_GoldBelt',(x,y,z-h*.28),(w*1.04,d*1.04,.065),m['accent'],.08)
for j in range(slots):
bx=x+(j-(slots-1)/2)*w*.42
box(name+'_Window'+str(j),(bx,y-d*.5-.041,z),(.037,.017,h*.32),m['secondary'])
for j,(dx,dy) in enumerate([(-1,-1),(1,-1),(-1,1),(1,1)]):
r_block(name+'_Merlon'+str(j),(x+dx*w*.34,y+dy*d*.31,z+h*.56),
(w*.26,d*.38,h*.34),m['primary'],.04)
def g_tank(up,m):
g_legs(m,.29,.42)
r_block('Body_StoneMass',(0,.04,.85),(.84,.54,.94),m['primary'])
shield=[(-.38,.47),(.38,.47),(.39,-.26),(0,-.51),(-.39,-.26)]
r_plate('Body_TowerShield_Rim',(0,-.29,.82),shield,.15,m['accent'])
r_plate('Body_TowerShield_Face',(0,-.375,.82),[(x*.90,z*.94) for x,z in shield],.045,m['primary'],.065)
# Vertical furnace slit is the focal mark of both icons.
sigil=[(-.035,.30),(.035,.30),(.035,-.08),(0,-.13),(-.035,-.08)]
r_plate('Head_VerticalEmber',(0,-.466,1.00),sigil,.025,m['secondary'])
for s,side in [(-1,'L'),(1,'R')]:
r_band('Body_ShieldLine'+side,(s*.065,-.457,.69),(s*.065,-.457,1.22),.021,m['accent'],4)
g_arm('Arm'+side,(s*.48,.035,1.03),(s*.59,.03,.73),(s*.63,-.13,.47),m,.19)
if up:
g_tower('Arm'+side+'_Castle',(s*.48,.035,1.12),(.42,.47,.37),m)
g_tower('Arm'+side+'_Bastion',(s*.61,-.07,.65),(.30,.39,.30),m,1)
else:
r_block('Arm'+side+'_ShoulderGold',(s*.48,-.04,1.13),(.35,.37,.12),m['accent'])
if up:
g_tower('Head_CentralKeep',(0,.06,1.37),(.30,.32,.37),m,1)
r_block('Body_ShieldRidge',(0,-.45,.85),(.08,.045,.95),m['accent'],.05)
r_plate('Body_ShieldEmber',(0,-.482,1.01),[(-.022,.17),(.022,.17),(.022,-.14),(0,-.18),(-.022,-.14)],.02,m['secondary'])
def g_lancer(m):
g_legs(m,.235,.69,.12,True)
r_plate('Body_Cuirass',(0,.02,.98),[(-.24,.19),(.24,.19),(.15,-.13),(0,-.29),(-.15,-.13)],.26,m['primary'],.06)
r_inset('Body_ChestCleft',(0,-.145,.98),[(-.10,.17),(.08,.17),(.035,-.20),(-.01,-.25)],.025,m['accent'],m['secondary'])
r_band('Head_Neck',(0,.02,1.13),(0,-.04,1.38),.078,m['primary'],5)
r_plate('Head_SpearHelm',(0,-.045,1.42),[(-.16,.08),(0,.02),(.16,.10),(.065,-.04),(0,-.19),(-.065,-.04)],.16,m['accent'],.035)
r_plate('Head_EmberVisor',(0,-.148,1.33),[(-.04,.05),(.055,.05),(0,-.15)],.03,m['secondary'])
r_sweep('Head_HelmCrest',[(0,.00,1.43),(-.035,.10,1.66)],[.085,0],m['accent'],4)
for s,side in [(-1,'L'),(1,'R')]:
sh=(s*.245,0,1.14)
hand=(s*.28,-.25,.91)
r_band('Arm'+side+'_Upper',sh,(s*.34,-.04,.95),.067,m['primary'],5)
r_band('Arm'+side+'_Forearm',(s*.34,-.04,.95),hand,.067,m['primary'],5)
r_rock('Arm'+side+'_Hand',hand,(.075,.065,.075),m['primary'])
r_plate('Arm'+side+'_Pauldron',(s*.27,0,1.17),[(s*-.095,-.07),(s*.17,-.04),(s*.075,.10)],.16,m['accent'])
# Long reinforced lance runs across both hands, held at waist height.
start=(-.66,-.23,.94)
end=(.60,-.30,.94)
r_band('Weapon_LanceCore',start,end,.065,m['primary'],8)
for j in range(5):
t=.10+j*.19
p=tuple(start[i]*(1-t)+end[i]*t for i in range(3))
q=(p[0]+.038,p[1]-.002,p[2])
r_band('Weapon_LanceCollar'+str(j),p,q,.086,m['accent'],8)
r_sweep('Weapon_GlassSpearhead',[end,(.73,-.307,.94),(.98,-.32,.94)],[.10,.13,0],m['secondary'],4)
def g_ballista(m):
"""Headless two-legged siege crossbow, matching the G06 image silhouette."""
g_legs(m,.32,.57,.12)
r_block('Body_BallistaChassis',(0,.05,.64),(.48,.48,.34),m['primary'])
r_block('Weapon_Rail',(0,-.05,.90),(.21,1.05,.20),m['primary'])
r_block('Weapon_RearHousing',(0,.44,.98),(.32,.32,.27),m['primary'])
box('Weapon_EmberRail',(0,-.10,1.01),(.07,1.08,.023),m['secondary'])
for i,y in enumerate([-.54,-.17,.33]):
r_block('Weapon_RailCollar'+str(i),(0,y,.92),(.26,.065,.245),m['accent'],.08)
for s,side in [(-1,'L'),(1,'R')]:
bow=[(s*.12,-.40,.94),(s*.39,-.43,.92),(s*.70,-.24,.96),(s*.81,.04,1.03)]
r_sweep('Weapon_GlassLimb'+side,bow,[.115,.14,.105,.065],m['secondary'],5)
r_band('Weapon_BowBinding'+side,(s*.16,-.42,.92),(s*.22,-.42,.92),.15,m['accent'],6)
r_band('Weapon_Drawstring'+side,bow[-1],(0,.45,1.03),.014,m['primary'],4)
r_disc('Body_Crank'+side,(s*.20,-.13,.69),.13,.07,m['accent'])
r_disc('Body_CrankInset'+side,(s*.20,-.175,.69),.09,.025,m['primary'])
r_sweep('Weapon_CrystalBolt',[(0,-.54,.99),(0,-.68,.99),(0,-.96,.99)],[.06,.10,0],m['secondary'],4)
def g_mortar(up,m):
"""Huge bowl torso on squat stone legs; upgrade widens the magma crater."""
g_legs(m,.29,.41)
r_rock('Body_Foundation',(0,.035,.55),(.41,.29,.29),m['primary'])
radius=.52 if up else .45
axis=(0,-.24,.97)
r_lathe('Body_Mortar',(0,.08,.54),[(0,0),(.25,.02),(radius*.90,.29),(radius,.65),
(radius*.84,.65),(radius*.73,.28),(0,.13)],m['primary'],axis,12)
# Wide gold lip, open centre and visible lower ember bed.
r_lathe('Body_MortarRim',(0,.08,.54),[(radius*1.018,.57),(radius*1.035,.663),
(radius*.815,.663),(radius*.81,.60)],m['accent'],axis,12)
r_lathe('Weapon_LavaPool',(0,-.015,.99 if up else .91),[(0,0),(.36 if up else .23,0),
(.36 if up else .23,.035),(0,.035)],m['secondary'],axis,12)
for s,side in [(-1,'L'),(1,'R')]:
g_arm('Arm'+side,(s*.45,.05,.95),(s*.59,-.03,.70),(s*.46,-.27,.53),m,.17)
r_block('Body_RimClasp'+side,(s*radius*.71,-.26,1.16),(.11,.16,.16),m['accent'])
if up:
r_inset('Body_FrontSigil',(0,-.44,.96),[(-.14,.19),(.14,.19),(.06,-.15),(0,-.20),(-.06,-.15)],.065,m['accent'],m['secondary'])
for k in range(4):
a=k*pi/2+.3
r_rock('Weapon_LavaRock'+str(k),(.23*cos(a),-.015+.20*sin(a),1.025),(.09,.085,.04),m['primary'])
else:
r_rock('Weapon_EmberBoulder',(0,-.02,.91),(.19,.16,.18),m['secondary'],.2)
def g_hearth(up,m):
"""Broad oven body with cream hood; no human head. Upgrade has a sun core."""
g_legs(m,.28,.42)
r_rock('Body_StoneBack',(0,.10,.85),(.45,.30,.53),m['primary'])
# Arched opening is a thick half-ring and two pillars, not a forward horn.
if not up:
pts=[(.34*cos(pi*k/10),-.11,1.03+.34*sin(pi*k/10)) for k in range(11)]
r_sweep('Body_CeramicArch',pts,[.105]*len(pts),m['light'],6)
for s in [-1,1]:
r_block('Body_CeramicPillar'+str(s),(s*.34,-.12,.89),(.19,.25,.36),m['light'])
r_block('Head_Chimney',(0,.075,1.39),(.37,.35,.22),m['light'])
r_block('Head_ChimneyShadow',(0,-.109,1.37),(.23,.025,.11),m['primary'])
else:
r_rock('Body_SunCore',(0,-.22,1.13),(.23,.09,.25),m['secondary'])
ring('Body_SunHalo',(0,-.19,1.17),.35,.045,m['accent'],rotation=(pi/2,0,0),segments=12)
for k in range(7):
a=pi*k/6
r_sweep('Body_HaloRay'+str(k),[(.34*cos(a),-.15,1.17+.34*sin(a)),
(.49*cos(a),-.14,1.17+.49*sin(a))],[.065,0],m['accent'],4)
for s,side in [(-1,'L'),(1,'R')]:
r_plate('Body_CeramicMantle'+side,(s*.42,.06,.90),[(s*-.06,.38),(s*.14,.30),(s*.20,-.24),(s*.11,-.40),(s*-.08,-.21)],.27,m['light'])
r_plate('Body_CeramicTabard'+side,(s*.49,-.06,.39),[(s*-.08,.23),(s*.08,.25),(s*.14,-.21),(s*.04,-.29),(s*-.10,-.20)],.04,m['light'])
bowl_z=.64
r_lathe('Weapon_HearthBowl',(0,-.39,bowl_z-.16),[(0,0),(.17,.02),(.37 if up else .30,.17),
(.39 if up else .32,.23),(.33 if up else .26,.23),(.23,.10),(0,.08)],m['accent'],segments=12)
cylinder('Weapon_EmberBed',(0,-.39,bowl_z+.015),.30 if up else .23,.028,m['secondary'],vertices=12)
for s,side in [(-1,'L'),(1,'R')]:
g_arm('Arm'+side,(s*.41,.015,1.03),(s*.54,-.13,.76),(s*.36,-.40,.61),m,.17)
r_disc('Arm'+side+'_Sigil',(s*.46,-.16,1.01),.12,.045,m['accent'])
for k,(x,z) in enumerate([(-.12,.26),(.06,.40),(.18,.20)]):
r_sweep('Weapon_Flame'+str(k),[(x,-.39,bowl_z),(x-.045,-.37,bowl_z+z*.42),
(x+.025,-.35,bowl_z+z*.78),(x-.04,-.36,bowl_z+z)],
[.07,.09,.045,0],m['secondary'],5)
if up:
r_sweep('Body_SolarStream',[(0,-.29,1.00),(.03,-.38,.85),(0,-.39,.70)],
[.06,.05,.055],m['secondary'],5)
r_sun('Weapon_BowlSun',(0,-.69,.66),.095,m)
def build_glutpakt(spec,m):
role=spec['role']
up=spec['upgraded']
if role=='melee': g_coal(up,m)
elif role=='tank': g_tank(up,m)
elif role=='ranged': g_ballista(m) if up else g_lancer(m)
elif role=='artillery': g_mortar(up,m)
elif role=='support': g_hearth(up,m)
else: raise ValueError('Unknown Glutpakt role: '+role)
finish_glutpakt_surfaces(m)
def finish_glutpakt_surfaces(m):
"""Stable basalt/gold facet values, matching the chipped stone in the icons.
Material changes only: retain the original solid geometry and triangle cap.
Each editable face keeps a material slot; no image projection or randomness.
"""
for key, factors in [('primary', (.78, 1.0, 1.32, 1.65)), ('accent', (.76, 1.0, 1.25))]:
original = m[key]
palette = []
for index, factor in enumerate(factors):
mat = original.copy()
mat.name = original['material_name'] + '_Facet' + str(index)
mat['material_name'] = mat.name
base = list(original.diffuse_color)
base[:3] = [min(1.0, v * factor) for v in base[:3]]
mat.diffuse_color = base
mat.node_tree.nodes.get('Principled BSDF').inputs['Base Color'].default_value = base
palette.append(mat)
for obj in bpy.context.scene.objects:
if obj.type != 'MESH' or not obj.data.materials or obj.data.materials[0] != original:
continue
obj.data.materials.clear()
for mat in palette:
obj.data.materials.append(mat)
seed = int(hashlib.sha256(obj['part_name'].encode()).hexdigest()[:8], 16)
for face in obj.data.polygons:
# Slightly brighter upward facets, varied side facets.
face.material_index = min(len(palette)-1, (seed + face.index * 7) % len(palette))
from math import sin, cos, pi
def n_ribbon(name,points,widths,mat):
if name.startswith('Head_ChoirHorn'):
return r_sweep(name,points,widths,mat,6,ribbon=True)
# One interpolated section per span gives flowing curves instead of zigzag rods.
from mathutils import Vector
refined=[]; radii=[]
for i in range(len(points)-1):
p0=Vector(points[max(i-1,0)]); p1=Vector(points[i])
p2=Vector(points[i+1]); p3=Vector(points[min(i+2,len(points)-1)])
for t in (0,.5):
p=.5*((2*p1)+(-p0+p2)*t+(2*p0-5*p1+4*p2-p3)*t*t+(-p0+3*p1-3*p2+p3)*t*t*t)
refined.append(tuple(p)); radii.append(widths[i]*(1-t)+widths[i+1]*t)
refined.append(points[-1]);radii.append(widths[-1])
return r_sweep(name,refined,radii,mat,6,ribbon=True)
def n_mask(name,loc,scale,m,singing=False):
x,y,z=loc
outline=[(-.12,.14),(-.05,.24),(.055,.22),(.145,.11),(.12,-.075),(0,-.24),(-.12,-.08)]
r_plate(name,loc,[(a*scale,b*scale) for a,b in outline],.09*scale,m['light'],.055*scale)
for s in [-1,1]:
r_plate(name+'_Eye'+str(s),(x+s*.065*scale,y-.091*scale,z+.025*scale),
[(s*-.032*scale,.005*scale),(s*.030*scale,.027*scale),(s*.027*scale,-.014*scale),(s*-.019*scale,-.040*scale)],
.012,m['accent'],.003)
if singing:
r_rock(name+'_Mouth',(x,y-.092*scale,z-.09*scale),(.035*scale,.018*scale,.061*scale),m['primary'])
def n_wisps(m,wide=False,tall=False):
"""Tapering curved ribbons replace human legs and the rigid cone skirt."""
top=1.16 if tall else .90
narrow=.55 if tall and not wide else 1
for s,side in [(-1,'L'),(1,'R')]:
spread=.28 if wide else .16
pts=[(s*spread,.08,top),(s*.08,.06,top*.75),(s*.20,.025,top*.54),
(s*.075,0,top*.34),(s*.20,-.015,top*.17),(s*.26,-.025,.055),(s*.36,-.025,.075)]
n_ribbon('Body_Wisp'+side,pts,[v*narrow for v in [.15 if wide else .11,.18,.14,.085,.048,.03,0]],m['secondary'])
pts2=[(s*.16,.16,top*.86),(s*.33,.10,top*.64),(s*.18,.07,top*.39),
(s*.35,.03,.20),(s*.30,.015,.08),(s*.22,0,.01)]
n_ribbon('Body_Tail'+side,pts2,[v*narrow for v in [.09,.105,.085,.057,.025,0]],m['primary'])
n_ribbon('Body_CentralFlow',[(0,.06,top),(-.10,-.055,top*.73),(.13,-.11,top*.46),(-.04,-.09,top*.19),(.07,-.08,.12)],
[v*narrow for v in [.18,.24 if wide else .16,.16,.045,0]],m['primary'])
def n_hood(m,up=False,z=1.22):
n_mask('Head_Mask',(0,-.15,z),1.0,m)
n_ribbon('Head_CurledHood',[(.14,.045,z-.10),(.09,.06,z+.15),(-.10,.04,z+.28),
(-.22,.015,z+.31),(-.25,0,z+.42),(-.15,-.01,z+.43)],
[.18,.17,.13,.075,.04,0],m['secondary'])
def n_crescent(name,loc,scale,m,light=True,flip=1):
x,y,z=loc
pts=[(x-flip*.13*scale,y,z+.17*scale),(x-flip*.17*scale,y,z),
(x-flip*.045*scale,y-.01,z-.14*scale),(x+flip*.15*scale,y-.02,z-.13*scale),
(x+flip*.23*scale,y-.02,z-.025*scale)]
n_ribbon(name,pts,[0,.085*scale,.10*scale,.06*scale,0],m['light'] if light else m['secondary'])
def n_tank(up,m):
n_wisps(m,up)
n_hood(m,up,1.20 if up else 1.14)
for k in range(3):
z=.89-k*.16
w=(.40 if up else .27)-k*.045
outline=[(-w,.10),(0,.045),(w,.10),(w*.65,-.11),(0,-.21),(-w*.65,-.11)]
# Each layer is a thick wrapped plate, with a light inner leading edge.
r_plate('Body_WrappedArmor'+str(k),(0,-.12,z),outline,.26,m['light'] if up and k%2==0 else m['secondary'])
for s,side in [(-1,'L'),(1,'R')]:
n_ribbon('Arm'+side,[(s*.19,.04,1.02),(s*.32,0,.94),(s*.38,-.06,.75)],
[.11,.13,.08],m['secondary'])
if up:
n_ribbon('Arm'+side+'_GreatShield',[(s*.29,-.12,.67),(s*.51,-.08,.81),
(s*.48,-.05,1.13),(s*.37,-.01,1.39)],
[0,.19,.18,0],m['light'])
n_ribbon('Arm'+side+'_ShieldInlay',[(s*.35,-.145,.74),(s*.48,-.12,.90),(s*.43,-.09,1.15)],
[0,.11,0],m['secondary'])
else:
n_crescent('Weapon'+side+'_Claw',(s*.33,-.09,.79),.82,m,flip=s)
def n_dancer(up,m):
n_wisps(m,False,True)
n_hood(m,up,1.31)
for s,side in [(-1,'L'),(1,'R')]:
high=(s<0 and not up)
tip=(s*.46,-.15,1.49 if high else .87)
n_ribbon('Arm'+side,[(s*.12,.025,1.12),(s*.31,.015,1.20 if high else 1.02),tip],
[.085,.10,.055],m['secondary'])
if not up:
r_sweep('Weapon'+side+'_Sickle',[tip,(tip[0]+s*.12,-.17,tip[2]-.12),
(tip[0]+s*.11,-.19,tip[2]-.28)],[.045,.045,0],m['light'],5)
if up:
pivot=(.27,-.25,.94)
r_rock('Weapon_ShearPivot',pivot,(.065,.04,.065),m['accent'])
for s,side in [(-1,'Lower'),(1,'Upper')]:
n_ribbon('Weapon_Shear'+side,[pivot,(.48,-.25,.94+s*.16),(.70,-.21,.94+s*.24),(.78,-.17,.94+s*.40)],
[.045,.115,.095,0],m['light'])
pts=[(.22,-.245,.94),(.01,-.24,.94+s*.14),(-.16,-.21,.94+s*.10),
(-.15,-.20,.94-s*.045),(.02,-.225,.94-s*.02),(.20,-.245,.94)]
n_ribbon('Weapon_ShearLoop'+side,pts,[.025,.035,.035,.035,.035,.025],m['light'])
n_ribbon('Head_CrescentCrown',[(.04,.03,1.44),(-.10,.02,1.64),(-.04,0,1.76)],
[.095,.105,0],m['light'])
def n_horn(name,loc,radius,depth,m,axis):
r_lathe(name,loc,[(0,0),(.035,.015),(.048,depth*.42),(radius*.55,depth*.78),
(radius,depth),(radius*.82,depth),(radius*.42,depth*.78),(.022,depth*.30),(0,depth*.29)],
m['light'],axis,10)
from mathutils import Vector
lip=Vector(loc)+Vector(axis).normalized()*depth
r_lathe(name+'_MintLip',lip,[(radius*1.015,-.025),(radius*1.025,.01),
(radius*.80,.01),(radius*.80,-.025)],m['accent'],axis,10)
def n_ranged(up,m):
n_wisps(m,False,True)
n_hood(m,up,1.29)
if up:
for s,side in [(-1,'L'),(1,'R')]:
n_ribbon('Arm'+side,[(s*.14,0,1.06),(s*.28,-.16,.97),(s*.43,-.20,1.04)],
[.09,.10,.025],m['light'])
n_ribbon('Weapon_HornArch'+side,[(s*.14,.09,1.24),(s*.28,.10,1.48),
(s*.45,.05,1.49),(s*.55,-.05,1.32)],
[.14,.13,.115,.08],m['secondary'])
n_horn('Weapon_Horn'+side,(s*.54,-.025,1.36),.23,.43,m,(s*.28,-.8,-.35))
else:
n_horn('Weapon_MouthHorn',(.025,-.25,1.24),.13,.60,m,(1,-.22,-.03))
n_ribbon('ArmL',[(-.18,0,1.06),(-.23,-.17,1.10),(.16,-.25,1.19)],
[.10,.095,.035],m['light'])
n_ribbon('ArmR',[(.15,0,1.07),(.30,-.12,1.02),(.38,-.27,1.19)],
[.09,.10,.025],m['secondary'])
def n_bell(up,m):
if up:
n_wisps(m,True)
r_arch_shell('Body_CathedralShell',(0,.045,0),[(.13,1.48),(.22,1.15),(.43,.58),(.48,.48),
(.43,.45),(.35,.60),(.15,1.17),(.085,1.48)],m['secondary'])
ring('Body_BellSkirt',(0,.045,.49),.46,.055,m['light'],segments=14)
arch=[(-.38,-.13,.54),(-.29,-.115,.98),(-.12,-.05,1.34),(0,-.01,1.45),
(.12,-.05,1.34),(.29,-.115,.98),(.38,-.13,.54)]
n_ribbon('Body_CathedralFrontArch',arch,[.035]*len(arch),m['light'])
beam('Weapon_ClapperChain',(0,-.055,1.27),(0,-.075,.84),.016,m['accent'],vertices=6)
r_rock('Weapon_Clapper',(0,-.075,.80),(.125,.11,.15),m['accent'])
n_mask('Body_ArchMask',(0,-.07,1.22),.48,m)
for s,side in [(-1,'L'),(1,'R')]:
n_ribbon('Head_Spire'+side,[(s*.17,.055,1.27),(s*.28,.055,1.53),
(s*.24,.05,1.69)],[.085,.07,0],m['light'])
n_crescent('Arm'+side+'_Sickle',(s*.43,0,.89),1,m,flip=s)
n_ribbon('Head_CentralSpire',[(0,.05,1.42),(-.025,.06,1.71),(.02,.02,1.83)],
[.10,.065,0],m['light'])
else:
n_wisps(m,True)
n_hood(m,False,1.39)
axis=(0,-.85,-.40)
r_lathe('Weapon_HangingBell',(0,.03,1.15),[(0,0),(.16,.02),(.22,.14),(.28,.42),(.44,.68),
(.45,.73),(.38,.73),(.23,.43),(.16,.15),(0,.12)],m['secondary'],axis,12)
from mathutils import Vector
lip=Vector((0,.03,1.15))+Vector(axis).normalized()*.70
r_lathe('Weapon_BellRim',lip,[(.455,0),(.46,.045),(.375,.045),(.375,0)],m['light'],axis,12)
beam('Weapon_ClapperStem',(0,-.08,1.10),(0,-.55,.80),.018,m['accent'],vertices=6)
r_rock('Weapon_Clapper',(0,-.56,.78),(.10,.085,.115),m['accent'])
for s,side in [(-1,'L'),(1,'R')]:
n_ribbon('Arm'+side,[(s*.18,.07,1.21),(s*.41,.02,1.13),
(s*.36,-.26,1.02),(s*.23,-.30,1.12)],
[.13,.15,.11,.02],m['secondary'])
def n_singer(up,m):
n_wisps(m,up,True)
n_hood(m,up,1.33)
# Singing mask has an open mouth, and floating masks have their own wisps.
r_rock('Head_SingingMouth',(0,-.248,1.23),(.035,.018,.061),m['primary'])
for k in range(2):
r_plate('Body_ChoirCollar'+str(k),(0,-.115,1.08-k*.13),[(-.25,.12),(0,.045),(.25,.12),(.17,-.045),(0,-.14),(-.17,-.045)],
.17,m['light'] if k==0 else m['secondary'])
r_rock('Body_ChoirGem',(0,-.225,.93),(.065,.045,.105),m['accent'])
for s,side in [(-1,'L'),(1,'R')]:
n_ribbon('Arm'+side,[(s*.18,0,1.04),(s*.32,-.035,.91),(s*.40,-.11,1.02)],
[.11,.12,.05],m['secondary'])
n_crescent('Arm'+side+'_Cuff',(s*.41,-.10,1.04),1,m,flip=-s)
floating=[(.42,.06,1.52)] if not up else [(-.40,.09,1.53),(.38,.08,1.50),(.28,.16,1.86)]
for k,loc in enumerate(floating):
x,y,z=loc
n_mask('Head_ChoirMask'+str(k),loc,.60,m,True)
n_ribbon('Head_ChoirWisp'+str(k),[(x,y,z-.08),(x-.055,y+.025,z-.20),(x+.02,y,z-.27)],
[.06,.04,0],m['secondary'])
for s in [-1,1]:
n_ribbon('Head_ChoirHorn'+str(k)+'_'+str(s),[(x+s*.06,y+.02,z+.09),
(x+s*.13,y+.02,z+.20),(x+s*.09,y,z+.25)],[.045,.045,0],m['light'])
if up:
for s in [-1,1]:
n_ribbon('Head_GreatHorn'+str(s),[(s*.10,.06,1.46),(s*.23,.04,1.65),(s*.16,0,1.79)],
[.09,.08,0],m['light'])
def build_nebelchor(spec,m):
role=spec['role']; up=spec['upgraded']
{'tank':n_tank,'melee':n_dancer,'ranged':n_ranged,'artillery':n_bell,'support':n_singer}[role](up,m)
from math import sin, cos, pi
def m_joint(name,loc,radius,m):
r_disc(name,loc,radius,.065,m['secondary'])
r_disc(name+'_Pin',(loc[0],loc[1]-.04,loc[2]),radius*.55,.026,m['accent'])
def m_legs(m,tall=False,wide=False,four=False):
height=.73 if tall else .43
spread=.27 if wide else .20
for k in range(4 if four else 2):
s=-1 if k%2==0 else 1
y=.29 if k>1 else -.045
x=s*spread
hip=(x*.68,y+.045,height)
knee=(x*1.22,y+.09,height*.58)
ankle=(x,y,.11)
side=str(k)
r_band('Leg'+side+'_Piston',hip,knee,.055 if tall else .085,m['accent'],6)
r_band('Leg'+side+'_Shin',knee,ankle,.048 if tall else .07,m['accent'],6)
m_joint('Leg'+side+'_Knee',(knee[0],knee[1]-.02,knee[2]),.065 if tall else .09,m)
r_plate('Leg'+side+'_ThighArmor',(hip[0],y-.022,height*.82),
[(-.07,.12),(.07,.10),(.035,-.12),(-.045,-.09)],.08,m['light'])
r_plate('Leg'+side+'_Greave',(x,y-.045,height*.35),
[(-.055,.12),(.07,.15),(.035,-.16),(-.045,-.09)],.07,m['primary'])
for j in range(2):
toe=(x+(j-.5)*.095,y-.16,.02)
r_sweep('Leg'+side+'_Toe'+str(j),[(x+(j-.5)*.04,y,.095),toe],[.065,0],m['accent'],5)
r_band('Leg'+side+'_AnkleGold',(x,y,.10),(x,y,.15),.067,m['secondary'],8)
def m_face(name,loc,m,round_face=False,tall=False):
x,y,z=loc
if round_face:
r_disc(name+'_BrassFrame',loc,.145,.055,m['secondary'])
r_disc(name+'_Ceramic',(x,y-.038,z),.118,.035,m['light'])
for s in [-1,1]:
r_disc(name+'_Eye'+str(s),(x+s*.051,y-.065,z+.027),.027,.015,m['accent'])
box(name+'_Mouth',(x,y-.068,z-.048),(.068,.012,.013),m['accent'])
else:
r_plate(name+'_Face',loc,[(-.09,.10),(.085,.10),(.074,-.03),(0,-.15),(-.076,-.025)],.09,m['light'])
for s in [-1,1]:
r_plate(name+'_Eye'+str(s),(x+s*.04,y-.075,z+.016),[(-.023,.01),(.023,.01),(0,-.013)],.012,m['accent'])
if tall:
r_plate(name+'_Crest',(x,y+.035,z+.12),[(-.11,-.05),(.10,-.04),(-.04,.24)],.10,m['primary'])
def m_torso(m,up=False,z=.98):
outline=[(-.22,.20),(.22,.20),(.14,-.04),(0,-.30),(-.14,-.04)]
r_inset('Body_Breastplate',(0,.025,z),outline,.25,m['secondary'],m['primary'])
r_band('Body_Waist',(0,.03,z-.30),(0,.03,z-.12),.085,m['accent'],8)
for s,side in [(-1,'L'),(1,'R')]:
m_joint('Arm'+side+'_Shoulder',(s*.22,.015,z+.11),.078,m)
r_plate('Body_HipPlate'+side,(s*.16,.04,z-.26),[(s*-.07,.13),(s*.08,.07),(s*.12,-.14),(0,-.10)],.09,m['light'])
def m_boiler(up,m):
m_legs(m,False,True)
if not up:
ellipsoid('Body_RoundKettle',(0,.04,.83),(.40,.31,.43),m['primary'],subdivisions=2)
ring('Body_Belt',(0,.04,.64),.335,.036,m['secondary'],segments=12)
m_face('Head',(0,-.27,.99),m,True)
r_disc('Body_PressureGauge',(-.22,-.24,.86),.095,.035,m['secondary'])
r_disc('Body_GaugeFace',(-.22,-.266,.86),.071,.018,m['light'])
r_band('Body_GaugeNeedle',(-.22,-.28,.86),(-.19,-.28,.90),.009,m['accent'],4)
r_lathe('Body_Chimney',(-.15,.09,1.14),[(.075,0),(.075,.23),(.053,.23),(.053,.025)],m['accent'],segments=8)
ring('Body_ChimneyLip',(-.15,.09,1.37),.076,.018,m['secondary'],segments=8)
else:
r_block('Body_HeavyChest',(0,.04,.89),(.65,.45,.60),m['primary'])
r_inset('Body_CeramicChest',(0,-.22,.89),[(-.13,.27),(.13,.27),(.11,-.20),(0,-.25),(-.11,-.20)],.09,m['secondary'],m['light'])
m_face('Head',(0,-.005,1.28),m)
for s,side in [(-1,'L'),(1,'R')]:
cylinder('Body_PressureTank'+side,(s*.30,.16,1.20),.14,.47,m['light'],vertices=10)
for z in [1.00,1.40]:
cylinder('Body_TankBand'+side+str(z),(s*.30,.16,z),.155,.05,m['accent'],vertices=10)
cylinder('Body_ValveStem'+side,(s*.30,.16,1.50),.025,.13,m['secondary'],vertices=6)
ring('Body_ValveWheel'+side,(s*.30,.16,1.56),.07,.016,m['secondary'],segments=8)
for s,side in [(-1,'L'),(1,'R')]:
sh=(s*.40,.04,.98); elbow=(s*.48,-.035,.73); hand=(s*.46,-.14,.57)
r_band('Arm'+side+'_Upper',sh,elbow,.075,m['accent'],8)
r_band('Arm'+side+'_Forearm',elbow,hand,.10,m['accent'],8)
m_joint('Arm'+side+'_Joint',(s*.40,-.075,.98),.13,m)
r_block('Arm'+side+'_Fist',hand,(.23,.23,.23),m['accent'])
r_band('Arm'+side+'_Cuff',(s*.46,-.11,.64),(s*.46,-.12,.59),.13,m['secondary'],8)
if up:
r_block('Arm'+side+'_Pauldron',(s*.40,.04,1.07),(.29,.38,.25),m['primary'])
r_inset('Arm'+side+'_Shield',(s*.49,-.16,.79),[(-.14,.23),(.14,.23),(.15,-.13),(0,-.23),(-.15,-.13)],.13,m['secondary'],m['primary'] if s>0 else m['light'])
def m_shear(up,m):
m_legs(m,True,up)
m_torso(m,up,1.02)
m_face('Head',(0,-.035,1.35),m,False,not up)
levels=[(1.20,1.57),(1.09,1.11),(.96,.71)] if up else [(1.16,1.00)]
for s,side in [(-1,'L'),(1,'R')]:
for k,(root_z,end_z) in enumerate(levels):
shoulder=(s*.18,.04,root_z)
elbow=(s*.39,.035,(root_z+end_z)/2)
wrist=(s*.58,-.06,end_z)
r_band('Arm'+side+str(k)+'_Rod',shoulder,elbow,.047,m['accent'],6)
r_band('Arm'+side+str(k)+'_Housing',elbow,wrist,.065,m['primary'],6)
m_joint('Arm'+side+str(k)+'_Pivot',(wrist[0],wrist[1]-.025,wrist[2]),.065,m)
directions=[.18] if up else [-.20,.20]
for j,dz in enumerate(directions):
points=[wrist,(s*.76,-.08,end_z+dz*.65),(s*.97,-.06,end_z+dz)]
r_sweep('Weapon'+side+str(k)+'_Blade'+str(j),points,[.040,.115,0],m['accent'],4,ribbon=True)
def m_crossbow(name,loc,width,length,m):
x,y,z=loc
r_block(name+'_Rail',loc,(.14,length,.11),m['accent'])
for s in [-1,1]:
points=[(x+s*.07,y-length*.27,z),(x+s*width*.33,y-length*.34,z),
(x+s*width*.50,y,z+.035)]
r_sweep(name+'_Bow'+str(s),points,[.065,.065,.025],m['accent'],5)
r_band(name+'_String'+str(s),points[-1],(x,y+length*.42,z+.025),.012,m['secondary'],4)
r_band(name+'_Bolt',(x,y+length*.35,z+.071),(x,y-length*.70,z+.071),.024,m['light'],5)
r_sweep(name+'_BoltTip',[(x,y-length*.60,z+.07),(x,y-length*.85,z+.07)],[.06,0],m['secondary'],4)
for j,yoff in enumerate([-.2,.15]):
r_block(name+'_Binding'+str(j),(x,y+yoff*length,z),(.19,.04,.15),m['secondary'])
def m_ranged(up,m):
m_legs(m,True,False,up)
m_torso(m,up,1.01)
m_face('Head',(0,-.025,1.37),m,False,True)
if not up:
r_disc('Head_Scope',(.073,-.094,1.39),.044,.055,m['secondary'])
for k in range(3):
r_band('Body_QuiverBolt'+str(k),(-.14+k*.055,.23,1.0),(-.14+k*.055,.23,1.43),.017,m['secondary'],5)
for s,side in [(-1,'L'),(1,'R')]:
hand=(s*.23,-.23,1.07 if up else .88)
r_band('Arm'+side+'_Upper',(s*.23,.02,1.16),(s*.32,-.025,.97),.048,m['accent'],6)
r_band('Arm'+side+'_Lower',(s*.32,-.025,.97),hand,.063,m['primary'],6)
m_joint('Arm'+side+'_Wrist',hand,.048,m)
m_crossbow('Weapon_Crossbow',(0 if up else .27,-.26,1.11 if up else .89),1.25 if up else .48,.84 if up else .53,m)
def m_resonator(up,m):
m_legs(m,False,True,up)
r_block('Body_SiegeChassis',(0,.10,.62),(.64,.50,.31),m['primary'])
m_face('Head',(0,-.015,1.26 if up else 1.22),m,False,False)
centres=[-.31,.31] if up else [0]
for j,x in enumerate(centres):
# Body is the horizontal resonator, not a thin man carrying a trumpet.
axis=(0,-1,0)
r_lathe('Body_ResonatorHousing'+str(j),(x,.30,.91),[(0,0),(.26,0),(.29,.18),(.27,.46),(0,.46)],m['primary'],axis,12)
if up:
r_lathe('Weapon_ImpactDisc'+str(j),(x,-.17,.91),[(0,0),(.30,0),(.36,.08),(.34,.13),(.12,.16),(0,.16)],m['secondary'],axis,12)
r_band('Weapon_DiscPiston'+str(j),(x,-.28,.91),(x,-.48,.91),.075,m['accent'],8)
else:
r_lathe('Weapon_ResonatorBell',(x,-.09,.91),[(.13,0),(.17,.15),(.35,.42),(.40,.47),(.34,.47),(.12,.14),(0,.12),(0,0)],m['secondary'],axis,12)
r_band('Weapon_ResonatorPiston',(x,-.21,.91),(x,-.63,.91),.078,m['accent'],8)
for s in [-1,1]:
r_block('Body_HousingLatch'+str(j)+'_'+str(s),(x+s*.265,-.02,.91),(.065,.10,.19),m['secondary'])
def m_conductor(up,m):
m_legs(m,not up,False)
if up:
# Broad floor-length open coat, cream stole and large exposed pendulum.
r_lathe('Body_Coat',(0,.075,.12),[(0,0),(.50,0),(.20,.94),(.12,1.11),(0,1.11)],m['primary'],segments=8)
for s,side in [(-1,'L'),(1,'R')]:
r_plate('Body_CreamStole'+side,(s*.20,-.205,.84),[(-.065,.38),(.065,.38),(.11,-.40),(0,-.48),(-.055,-.37)],.035,m['light'])
r_band('Body_CoatGold'+side,(s*.10,-.26,1.17),(s*.45,-.26,.19),.02,m['secondary'],4)
r_plate('Body_PendulumOpening',(0,-.28,.76),[(-.09,.34),(.09,.34),(.13,-.34),(-.13,-.34)],.025,m['accent'])
r_band('Body_PendulumStem',(0,-.309,1.08),(0,-.309,.55),.022,m['secondary'],5)
r_disc('Body_PendulumBob',(0,-.325,.53),.13,.035,m['secondary'])
m_face('Head',(0,-.04,1.40),m)
for j,x in enumerate([-.12,0,.12]):
r_sweep('Head_Crown'+str(j),[(x,.025,1.48),(x*1.25,.025,1.78 if j==1 else 1.68)],
[.035,0],m['secondary'],4)
else:
m_torso(m,False,1.01)
m_face('Head',(0,-.025,1.37),m,True)
r_band('Body_PendulumStem',(0,-.17,1.06),(0,-.17,.80),.017,m['secondary'],5)
r_disc('Body_PendulumBob',(0,-.19,.78),.06,.028,m['secondary'])
for s,side in [(-1,'L'),(1,'R')]:
hand=(s*.53,-.11,1.52 if s<0 else 1.12)
elbow=(s*.41,.015,1.20 if s<0 else 1.02)
r_band('Arm'+side+'_Upper',(s*.22,.03,1.20),elbow,.045,m['accent'],6)
r_band('Arm'+side+'_Forearm',elbow,hand,.046,m['accent'],6)
m_joint('Arm'+side+'_Elbow',elbow,.065,m)
r_rock('Arm'+side+'_Palm',hand,(.055,.04,.07),m['accent'])
for j in range(3):
end=(hand[0]+s*.10,hand[1]-.02,hand[2]+(j-1)*.046)
r_band('Arm'+side+'_Finger'+str(j),hand,end,.012,m['accent'],4)
if s<0:
r_band('Weapon_Baton',hand,(hand[0]+.20,hand[1],hand[2]+.33),.012,m['secondary'],4)
def build_nietenkonvent(spec,m):
{'tank':m_boiler,'melee':m_shear,'ranged':m_ranged,'artillery':m_resonator,'support':m_conductor}[spec['role']](spec['upgraded'],m)
from math import sin,cos,pi
def w_leaf(name,start,end,width,m,colour='accent'):
points=[start,tuple(start[i]*.55+end[i]*.45 for i in range(3)),end]
return r_sweep(name,points,[.015,width,0],m[colour],6,ribbon=True)
def w_flower(name,loc,m,scale=1):
x,y,z=loc
for k in range(5):
a=k*2*pi/5
r_rock(name+'_Petal'+str(k),(x+.038*scale*cos(a),y-.006,z+.038*scale*sin(a)),
(.033*scale,.014*scale,.029*scale),m['light'])
r_rock(name+'_Heart',(x,y-.02,z),(.02*scale,.016*scale,.02*scale),m['accent'])
def w_roots(m,count=4,spread=.43,height=.49,thick=.09):
for k in range(count):
a=2*pi*k/count+pi/4
root=(spread*cos(a),spread*sin(a),.11)
elbow=(spread*.67*cos(a),spread*.67*sin(a),height*.53)
hip=(spread*.28*cos(a),spread*.28*sin(a),height)
r_sweep('Leg_Root'+str(k),[hip,elbow,root],[thick*1.6,thick,thick*.8],m['primary'],6)
r_sweep('Leg_Moss'+str(k),[(hip[0],hip[1]-.025,hip[2]),(elbow[0]+.025,elbow[1]-.03,elbow[2]),
(root[0],root[1]-.025,root[2])],[thick*.32,thick*.22,0],m['secondary'],5)
for j in range(3):
aa=a+(j-1)*.53
tip=(root[0]+.18*cos(aa),root[1]+.18*sin(aa),.015)
r_sweep('Leg_RootToe'+str(k)+'_'+str(j),[root,tip],[thick*.65,0],m['wood'],5)
def w_vine(name,height,radius,m,base=(0,0,.3),phase=0,thickness=.065):
x,y,z=base
points=[]
for k in range(9):
t=k/8
a=phase+t*pi*1.8
points.append((x+radius*cos(a),y+radius*sin(a),z+t*height))
r_sweep(name,points,[thickness*(1-.35*k/8) for k in range(9)],m['primary'],6)
return points
def w_thorn(up,m):
w_roots(m,4 if up else 2,.33,.46,.11)
if not up:
r_rock('Body_MossCore',(0,.02,.79),(.38,.29,.43),m['secondary'])
for k in range(7):
a=2*pi*k/7
p=(.29*cos(a),.23*sin(a),.80)
obj=r_plate('Body_BarkPlate'+str(k),p,[(-.16,.16),(0,.34),(.15,.16),(.13,-.16),(0,-.30),(-.13,-.12)],.10,m['wood'],.05)
obj.rotation_euler=(0,0,a+pi/2)
r_sweep('Body_Thorn'+str(k),[(.36*cos(a),.27*sin(a),.84),(.54*cos(a),.43*sin(a),.97)],
[.085,0],m['accent'],5)
r_plate('Head_EyeSlit',(0,-.306,.77),[(-.19,.024),(.19,.024),(.10,-.012),(-.15,-.016)],.025,m['bark_dark'])
for s in [-1,1]:
r_plate('Head_Eye'+str(s),(s*.115,-.327,.778),[(-.035,.007),(.035,.007),(0,-.01)],.013,m['accent'])
w_leaf('Head_Leaf'+str(s),(0,.03,1.14),(s*.19,.02,1.42),.075,m)
w_flower('Body_Flower',(-.22,-.21,1.03),m)
else:
# The upgrade is a broad open-jawed vine predator, not a round head on sticks.
r_rock('Body_VineMass',(0,.08,.66),(.34,.26,.33),m['primary'])
r_rock('Head_UpperJaw',(0,-.18,1.05),(.42,.40,.20),m['primary'])
r_rock('Head_LowerJaw',(0,-.22,.72),(.38,.39,.12),m['wood'])
r_rock('Head_MouthShadow',(0,-.19,.87),(.29,.23,.19),m['bark_dark'])
for s,side in [(-1,'L'),(1,'R')]:
r_sweep('Head_GreenLip'+side,[(s*.34,.04,1.12),(s*.40,-.22,1.09),(s*.23,-.51,1.06),(0,-.55,1.04)],
[.045,.043,.037,.03],m['secondary'],5)
for k in range(3):
x=s*(.08+k*.12)
y=-.52+k*.06
r_sweep('Head_UpperFang'+side+str(k),[(x,y,1.025),(x,y-.01,.83)],[.045,0],m['wood'],5)
r_sweep('Head_LowerFang'+side+str(k),[(x,y,.73),(x,y,.85)],[.035,0],m['wood'],5)
w_leaf('Head_JawLeaf'+side,(s*.32,-.12,1.03),(s*.48,-.11,1.26),.12,m)
w_flower('Head_JawFlower'+side,(s*.36,-.15,1.10),m,.8)
for k in range(3):
w_vine('Body_BraidedVine'+str(k),.80,.23,m,phase=k*2*pi/3,thickness=.075)
def w_log(name,loc,radius,m):
r_disc(name+'_Bark',loc,radius,.10,m['primary'])
r_disc(name+'_Cut',(loc[0],loc[1]-.065,loc[2]),radius*.88,.038,m['wood'])
for k,f in enumerate([.37,.65]):
ring(name+'_GrowthRing'+str(k),(loc[0],loc[1]-.09,loc[2]),radius*f,
.008,m['bark_dark'],rotation=(pi/2,0,0),segments=10)
def w_tank(up,m):
w_roots(m,4,.43,.58,.12)
r_rock('Body_MossDome',(0,.08,.88),(.48,.35,.46),m['secondary'])
if not up:
for row in range(3):
z=.65+row*.22
radius=.41-row*.085
for k in range(5):
a=k*2*pi/5+row*.55
obj=r_plate('Body_CorkTile'+str(row)+'_'+str(k),(radius*cos(a),.08+radius*.78*sin(a),z),
[(-.16,.11),(.14,.14),(.17,-.08),(0,-.18),(-.15,-.09)],.07,m['wood'],.035)
obj.rotation_euler=(-.25-row*.17,0,a+pi/2)
w_leaf('Body_CrownLeaf',(0,0,1.20),(.23,-.02,1.43),.14,m)
for s in [-1,1]:
w_flower('Body_Flower'+str(s),(s*.27,-.22,1.05),m)
else:
# Large upright stump shields across body/shoulders, no turtle head.
for k,(x,y,z,r) in enumerate([(-.29,-.15,1.02,.32),(.27,-.18,.90,.25),
(0,.02,1.30,.19),(-.37,-.12,.55,.15),(.39,-.12,.55,.14)]):
w_log('Body_LogShield'+str(k),(x,y,z),r,m)
for s in [-1,1]:
w_leaf('Head_Sprout'+str(s),(s*.20,.07,1.33),(s*.36,.08,1.57),.06,m)
w_flower('Body_Flower'+str(s),(s*.36,-.21,1.15),m,.8)
def w_seed_head(name,loc,radius,m,pine=False):
x,y,z=loc
r_lathe(name+'_Cup',loc,[(0,0),(radius*.45,.02),(radius,.21),
(radius*.97,.29),(radius*.77,.27),(radius*.57,.11),(0,.07)],m['secondary'],segments=10)
r_rock(name+'_Seed',(x,y,z+.23),(radius*.59,radius*.58,radius*.52),m['wood'])
if pine:
for layer in range(3):
rr=radius*(1-layer*.18)
for k in range(5):
a=k*2*pi/5+layer*.44
p=(x+rr*.60*cos(a),y+rr*.60*sin(a),z+.10+layer*.12)
outline=[(-rr*.42,-.08),(rr*.42,-.08),(rr*.31,.10),(0,.25),(-rr*.31,.10)]
obj=r_plate(name+'_ConeScale'+str(layer)+'_'+str(k),p,outline,.045,m['wood'])
obj.rotation_euler=(-.25,0,a+pi/2)
else:
for s in [-1,1]:
w_leaf(name+'_LipLeaf'+str(s),(x+s*radius*.8,y,z+.18),(x+s*radius*1.03,y,z+.43),.08,m)
def w_ranged(up,m):
w_roots(m,4 if up else 3,.33,.47,.085)
w_vine('Body_TwistedStem',.68 if up else .76,.09,m,phase=.5,thickness=.07)
w_vine('Body_ClimbingVine',.73,.055,m,phase=3.3,thickness=.038)
w_seed_head('Head_SeedLauncher',(0,-.025,1.02),.29 if up else .23,m,up)
for s in [-1,1]:
w_leaf('Body_BroadLeaf'+str(s),(s*.04,0,.69),(s*.41,-.02,.91),.115,m)
w_flower('Body_Flower'+str(s),(s*.12,-.04,.84),m,.65)
def w_chalice(name,loc,radius,m):
x,y,z=loc
sides=8 if radius<.2 else 12
fringe=3 if radius<.2 else 6
r_lathe(name+'_PinkCup',loc,[(0,0),(radius*.38,.035),(radius*.77,.16),
(radius,.27),(radius*.89,.30),(radius*.65,.17),(0,.065)],m['light'],segments=sides)
ring(name+'_MossRim',(x,y,z+.285),radius*.945,.026 if radius>=.2 else .012,m['accent'],segments=sides)
for k in range(fringe):
a=2*pi*k/fringe
r_sweep(name+'_MossFringe'+str(k),[(x+radius*.96*cos(a),y+radius*.96*sin(a),z+.28),
(x+radius*cos(a),y+radius*sin(a),z+.11-(k%2)*.05)],
[.032,0],m['secondary'],4)
def w_artillery(up,m):
w_roots(m,4,.41,.52,.11)
if up:
positions=[(-.30,-.03,.82,.31),(.28,.03,.74,.30),(0,.10,1.30,.43)]
for k,(x,y,z,r) in enumerate(positions):
r_sweep('Body_FungalTrunk'+str(k),[(x*.3,.08,.40),(x*.80,y,.76),(x,y,z+.03)],
[.12,.085,.065],m['primary'],6)
w_chalice('Head_FungalTier'+str(k),(x,y,z),r,m)
for k in range(3):
x=(-1 if k%2 else 1)*(.35+.04*k)
w_chalice('Body_SmallFungus'+str(k),(x,.04,.55+.12*k),.12,m)
else:
w_vine('Body_ChaliceTrunk',.60,.08,m,thickness=.12)
w_chalice('Head_SporeChalice',(0,0,.93),.48,m)
for k in range(4):
a=k*2*pi/4
r_rock('Weapon_Spore'+str(k),(.14*cos(a),.12*sin(a),1.075),(.065,.055,.10),m['wood'])
for s in [-1,1]:
w_chalice('Body_SideFungus'+str(s),(s*.21,.04,.65),.10,m)
def w_support(up,m):
w_roots(m,4 if up else 3,.39,.50,.10)
if up:
# Open root arch and broad basin form the entire body, without a human face.
for s in [-1,1]:
r_sweep('Body_SpringArch'+str(s),[(s*.31,.10,.40),(s*.40,.11,.83),
(s*.31,.09,1.24),(s*.11,.07,1.43),(0,.07,1.42)],
[.12,.105,.09,.06,.045],m['primary'],6)
r_sweep('Body_FrontVine'+str(s),[(s*.25,-.05,.35),(s*.10,-.11,.60),
(s*.32,-.03,.96),(s*.13,.06,1.29)],
[.07,.07,.06,.02],m['wood'],5)
for k in range(7):
a=k*2*pi/7
w_leaf('Head_CanopyLeaf'+str(k),(.07*cos(a),.08,1.39),(.43*cos(a),.08+.22*sin(a),1.20),.13,m)
for k,(x,z) in enumerate([(-.33,1.25),(.30,1.19),(-.15,1.43),(.34,.89)]):
w_flower('Body_Blossom'+str(k),(x,-.07,z),m)
radius,z=.34,.83
else:
w_vine('Body_LivingTrunk',.91,.07,m,thickness=.10)
w_vine('Body_WindingVine',.95,.12,m,phase=2.7,thickness=.038)
for k,(dx,dz) in enumerate([(-.34,0),(.34,-.02),(.03,.25)]):
w_leaf('Head_LeafCrown'+str(k),(0,.03,1.17),(dx,0,1.33+dz),.14,m)
w_flower('Head_Flower',(-.12,-.05,1.19),m,.8)
radius,z=.25,.72
r_lathe('Weapon_LeafBasin',(0,-.24,z-.12),[(0,0),(radius*.4,.02),(radius,.19),
(radius*.88,.21),(radius*.67,.11),(0,.05)],m['accent'],segments=10)
cylinder('Weapon_Water',(0,-.24,z+.035),radius*.78,.025,m['water'],vertices=12)
for s in [-1,1]:
r_sweep('Arm_BasinVine'+str(s),[(s*.10,0,z+.20),(s*.28,-.09,z+.18),
(s*radius,-.24,z+.025)],[.05,.045,.02],m['primary'],5)
if not up:
r_rock('Weapon_DewOrb',(0,-.24,z+.16),(.12,.10,.13),m['water'])
def build_wurzelbund(spec,m):
{'melee':w_thorn,'tank':w_tank,'ranged':w_ranged,'artillery':w_artillery,'support':w_support}[spec['role']](spec['upgraded'],m)
from math import sin,cos,pi
def s_legs(m,heavy=False):
spread=.25 if heavy else .18
for s,side in [(-1,'L'),(1,'R')]:
hip=(s*spread*.65,.03,.63)
knee=(s*spread*1.10,.07,.37)
foot=(s*spread,-.05,.12)
r_band('Leg'+side+'_Thigh',hip,knee,.095 if heavy else .060,m['primary'],6)
r_band('Leg'+side+'_Shin',knee,foot,.080 if heavy else .049,m['primary'],6)
r_block('Leg'+side+'_Boot',(foot[0],-.11,.075),(.19 if heavy else .13,.29,.15),m['primary'])
if heavy:
r_plate('Leg'+side+'_KneePlate',(knee[0],-.02,knee[2]),[(-.115,.08),(.11,.08),(.11,-.045),(0,-.13),(-.12,-.04)],.10,m['primary'])
r_band('Leg'+side+'_Strap',(foot[0],-.05,.19),(foot[0],-.05,.245),.091,m['secondary'],6)
def s_mask(loc,m):
r_plate('Head_Visor',loc,[(-.12,.10),(0,.19),(.12,.10),(.10,-.10),(0,-.17),(-.10,-.10)],.13,m['light'],.028)
for s in [-1,1]:
r_plate('Head_Eye'+str(s),(loc[0]+s*.05,loc[1]-.098,loc[2]+.03),[(-.036,.013),(.036,.013),(s*.027,-.017)],.016,m['shadow'],.001)
r_band('Head_VisorSlit',(loc[0],loc[1]-.10,loc[2]+.015),(loc[0],loc[1]-.10,loc[2]-.135),.013,m['shadow'],4)
def s_hood(m,z=1.31):
r_rock('Head_HoodBack',(0,.04,z),(.20,.16,.25),m['primary'])
s_mask((0,-.10,z),m)
r_sweep('Head_HoodRim',[(-.14,-.18,z-.15),(-.17,-.17,z+.07),(-.10,-.12,z+.24),
(0,-.08,z+.29),(.14,-.16,z+.10),(.14,-.18,z-.15)],
[.045]*6,m['primary'],5)
r_sweep('Body_Scarf',[(-.18,-.03,z-.20),(0,-.14,z-.24),(.18,-.03,z-.20)],
[.05,.055,.04],m['accent'],5)
def s_arm(name,shoulder,elbow,hand,m,heavy=False):
r_band(name+'_Upper',shoulder,elbow,.085 if heavy else .053,m['primary'],6)
r_band(name+'_Lower',elbow,hand,.105 if heavy else .055,m['primary'],6)
r_rock(name+'_Glove',hand,(.115,.095,.095) if heavy else (.065,.055,.055),m['primary'])
if heavy:
r_block(name+'_Pauldron',shoulder,(.31,.32,.22),m['secondary'])
r_plate(name+'_PauldronCap',(shoulder[0],shoulder[1]-.17,shoulder[2]),
[(-.16,.095),(.16,.095),(.18,-.075),(-.18,-.075)],.055,m['primary'])
def s_heavy_torso(m):
r_inset('Body_Armor',(0,.035,.93),[(-.26,.23),(.26,.23),(.22,-.12),(0,-.29),(-.22,-.12)],
.34,m['secondary'],m['primary'])
r_block('Body_Belt',(0,0,.68),(.43,.32,.07),m['secondary'])
r_plate('Body_Tabard',(0,-.17,.49),[(-.095,.20),(.095,.20),(.12,-.10),(.02,-.25),(-.07,-.13)],.035,m['accent'])
r_rock('Head_HelmetBack',(0,.015,1.29),(.165,.16,.20),m['primary'])
s_mask((0,-.115,1.29),m)
r_plate('Head_Crest',(0,.015,1.45),[(-.022,-.09),(.022,-.09),(.025,.09),(-.02,.13)],.22,m['secondary'])
def s_scout(m):
s_legs(m)
r_plate('Body_LeatherCuirass',(0,.035,.93),[(-.19,.18),(.19,.18),(.13,-.24),(-.13,-.24)],.25,m['primary'])
s_hood(m)
r_band('Body_DiagonalBelt',(-.15,-.14,1.10),(.14,-.16,.77),.035,m['secondary'],4)
for s in [-1,1]:
r_plate('Body_SplitCoat'+str(s),(s*.15,.04,.62),[(-.09,.15),(.10,.15),(.16,-.20),(-.04,-.12)],.07,m['primary'])
r_sweep('Body_TornScarf'+str(s),[(s*.14,.13,1.16),(s*.23,.22,.94),(s*.43,.27,.72),(s*.54,.29,.57)],
[.07,.10,.07,0],m['accent'],5,ribbon=True)
for k in range(3):
r_band('Body_QuiverBolt'+str(k),(-.13+k*.07,.22,.95),(-.13+k*.07,.22,1.43),.022,m['secondary'],5)
s_arm('ArmL',(-.20,0,1.06),(-.27,-.11,.91),(-.06,-.23,.96),m)
s_arm('ArmR',(.20,0,1.06),(.30,-.08,.96),(.28,-.23,.98),m)
r_block('Weapon_CrossbowStock',(.16,-.22,.98),(.12,.53,.09),m['secondary'])
for s in [-1,1]:
points=[(.16,-.36,1.00),(.16+s*.20,-.35,1.00),(.16+s*.26,-.24,1.02)]
r_sweep('Weapon_Bow'+str(s),points,[.036,.035,.02],m['primary'],5)
r_band('Weapon_String'+str(s),points[-1],(.16,-.01,1.01),.009,m['light'],4)
r_band('Weapon_Bolt',(.16,-.03,1.045),(.16,-.67,1.045),.013,m['light'],4)
def s_tank(m):
s_legs(m,True); s_heavy_torso(m)
s_arm('ArmL',(-.30,.015,1.08),(-.42,-.035,.85),(-.40,-.10,.70),m,True)
s_arm('ArmR',(.30,.015,1.08),(.38,-.05,.90),(.30,-.22,.89),m,True)
r_inset('Weapon_TowerShield',(.28,-.33,.79),[(-.29,.48),(.29,.48),(.29,-.40),(.22,-.49),(-.22,-.49),(-.29,-.40)],
.12,m['light'],m['secondary'])
r_block('Weapon_ShieldSpine',(.28,-.432,.79),(.075,.055,.92),m['primary'])
r_rock('Weapon_ShieldBoss',(.28,-.51,.81),(.12,.14,.18),m['primary'])
for s in [-1,1]:
for z in [.40,1.16]:
r_block('Weapon_Corner'+str(s)+str(z),(.28+s*.23,-.44,z),(.13,.05,.11),m['light'])
def s_singer(m):
r_lathe('Body_LongRobe',(0,.03,.02),[(0,0),(.45,0),(.21,.83),(.18,1.13),(0,1.13)],m['primary'],segments=10)
s_hood(m,1.36)
for s in [-1,1]:
r_plate('Body_CreamStole'+str(s),(s*.11,-.22,.66),[(-.045,.43),(.045,.43),(.09,-.42),(0,-.53),(-.05,-.35)],.035,m['light'])
r_sweep('Body_RaggedCloak'+str(s),[(s*.18,.09,1.17),(s*.34,.15,.94),
(s*.41,.19,.64),(s*.52,.17,.30),(s*.41,.16,.06)],
[.09,.11,.12,.10,0],m['accent'],5,ribbon=True)
s_arm('ArmL',(-.18,0,1.10),(-.26,-.06,.89),(-.12,-.22,.81),m)
s_arm('ArmR',(.18,0,1.10),(.30,-.11,1.20),(.40,-.19,1.28),m)
axis=(1,-.16,.07)
r_lathe('Weapon_Trumpet',(.06,-.20,1.32),[(0,0),(.025,.015),(.038,.31),(.12,.56),(.23,.72),
(.18,.72),(.078,.55),(.022,.28),(0,.27)],m['secondary'],axis,10)
def s_hammer(m):
s_legs(m,True); s_heavy_torso(m)
s_arm('ArmL',(-.30,.02,1.08),(-.39,-.06,.90),(-.23,-.24,.84),m,True)
s_arm('ArmR',(.30,.02,1.08),(.36,-.08,1.00),(.30,-.27,1.04),m,True)
r_band('Weapon_Haft',(-.52,-.22,.70),(.54,-.30,1.19),.039,m['secondary'],8)
r_block('Weapon_Head',(.58,-.30,1.21),(.32,.34,.38),m['primary'])
# Solid sloping wedge and separate raised triangular engraving.
outline=[(-.19,.18),(.15,.26),(.26,-.04),(-.05,-.21),(-.19,-.09)]
r_plate('Weapon_Wedge',(.65,-.31,1.25),outline,.43,m['primary'])
r_plate('Weapon_TriangleRim',(.67,-.57,1.29),[(-.10,-.04),(.12,-.04),(.01,.15)],.024,m['light'])
r_plate('Weapon_TriangleInset',(.68,-.59,1.30),[(-.055,-.015),(.062,-.015),(.004,.09)],.025,m['primary'])
def s_rift(m):
s_legs(m)
# Four separate ribs leave an actual void through the chest.
for s in [-1,1]:
r_sweep('Body_HollowRib'+str(s),[(0,.03,.72),(s*.23,.02,.99),(s*.14,.02,1.23),(0,.025,1.29)],
[.06,.10,.11,.06],m['primary'],5)
r_sweep('Body_RiftEdge'+str(s),[(0,-.065,.80),(s*.14,-.08,.99),(s*.08,-.08,1.16)],
[.009,.012,.008],m['rift'],4)
r_plate('Body_TornCloth'+str(s),(s*.23,.055,.95),[(-.045,.28),(.055,.30),(.13,-.17),(.02,-.10),(-.06,-.28)],.025,m['secondary'])
r_plate('Body_HipRag'+str(s),(s*.15,.035,.56),[(-.05,.16),(.06,.17),(.12,-.20),(.03,-.13),(-.03,-.25)],.025,m['secondary'])
r_rock('Head_FacelessHelm',(0,.035,1.46),(.12,.10,.20),m['shadow'])
r_sweep('Head_MintCrack',[(-.025,-.076,1.57),(.028,-.081,1.44),(.01,-.078,1.29)],
[.012,.008,0],m['rift'],4)
for s in [-1,1]:
r_sweep('Head_Antler'+str(s),[(s*.08,.01,1.42),(s*.25,.01,1.56),
(s*.23,.025,1.73),(s*.17,.02,1.79)],
[.05,.065,.04,0],m['light'],5)
r_sweep('Head_InnerHorn'+str(s),[(s*.13,.035,1.54),(s*.14,.04,1.84)],
[.045,0],m['light'],5)
s_arm('ArmL',(-.19,.02,1.17),(-.31,0,.95),(-.38,-.08,.73),m)
s_arm('ArmR',(.19,.02,1.17),(.31,-.03,.99),(.43,-.12,1.13),m)
for j in range(3):
x=-.38+(j-1)*.035
r_sweep('ArmL_Claw'+str(j),[(x,-.08,.73),(x-.02,-.09,.63),(x+.025,-.1,.55)],[.015,.013,0],m['primary'],4)
r_rock('Weapon_RiftOrb',(.43,-.12,1.23),(.11,.09,.13),m['accent'])
r_rock('Weapon_RiftLight',(.43,-.202,1.23),(.07,.025,.095),m['rift'])
def s_worker(m):
# Squat clay body, oversized hood, apron, forked collector and round lantern.
ellipsoid('Body_Clay',(0,.025,.45),(.29,.21,.30),m['primary'],subdivisions=2)
for s,side in [(-1,'L'),(1,'R')]:
r_rock('Leg'+side+'_Foot',(s*.18,-.055,.075),(.17,.18,.10),m['primary'])
r_rock('Arm'+side+'_Shoulder',(s*.28,0,.64),(.13,.13,.15),m['primary'])
r_rock('Arm'+side+'_Hand',(s*.36,-.13,.59),(.13,.11,.12),m['primary'])
ellipsoid('Head_Hood',(0,.035,.96),(.31,.25,.32),m['secondary'],subdivisions=2)
ellipsoid('Head_ClayFace',(0,-.18,.96),(.24,.10,.245),m['primary'],subdivisions=2)
r_sweep('Head_HoodEdge',[(-.23,-.235,.80),(-.29,-.21,1.02),(-.17,-.18,1.21),(0,-.17,1.25),
(.17,-.18,1.21),(.29,-.21,1.02),(.23,-.235,.80)],
[.035]*7,m['secondary'],6)
for s in [-1,1]:
r_disc('Head_EyeSocket'+str(s),(s*.083,-.28,.99),.041,.023,m['shadow'])
r_rock('Head_Eye'+str(s),(s*.083,-.3,.99),(.022,.017,.028),m['light'])
r_band('Body_ApronStrap'+str(s),(s*.18,-.16,.72),(s*.18,-.17,.31),.025,m['secondary'],4)
r_block('Body_Buckle'+str(s),(s*.18,-.19,.67),(.065,.025,.07),m['accent'])
r_plate('Body_Apron',(0,-.19,.39),[(-.20,.25),(.20,.25),(.27,-.19),(-.27,-.19)],.04,m['secondary'])
r_block('Body_ApronPocket',(0,-.235,.36),(.31,.035,.18),m['secondary'])
r_band('Weapon_CollectorStaff',(-.37,-.13,.22),(-.37,-.13,1.10),.027,m['accent'],7)
for s in [-1,1]:
r_sweep('Weapon_CollectorProng'+str(s),[(-.37,-.13,1.07),(-.37+s*.13,-.13,1.20),
(-.37+s*.13,-.13,1.38),(-.37+s*.08,-.13,1.46)],
[.035,.038,.032,.02],m['accent'],5)
r_rock('Weapon_CollectedSpark',(-.37,-.13,1.30),(.055,.045,.11),m['light'])
r_rock('Weapon_LanternGlass',(.40,-.15,.39),(.14,.12,.16),m['light'])
for z in [.27,.53]:
cylinder('Weapon_LanternCap'+str(z),(.40,-.15,z),.11,.035,m['accent'],vertices=10)
ring('Weapon_LanternHandle',(.40,-.15,.59),.075,.015,m['accent'],rotation=(pi/2,0,0),segments=10)
def build_neutral(spec,m):
{'s01':s_scout,'s02':s_tank,'s03':s_singer,'s04':s_hammer,'s05':s_rift,'worker':s_worker}[spec['id']](m)
from math import cos, pi, sin
def _prism(name, outline, front_y, back_y, material):
"""Extrude an X/Z outline along Y, useful for readable masks and wedges."""
area = sum(outline[i][0] * outline[(i + 1) % len(outline)][1]
- outline[(i + 1) % len(outline)][0] * outline[i][1]
for i in range(len(outline)))
if area < 0:
outline = list(reversed(outline))
count = len(outline)
vertices = [(x, front_y, z) for x, z in outline]
vertices += [(x, back_y, z) for x, z in outline]
faces = [tuple(range(count)), tuple(range(2 * count - 1, count - 1, -1))]
for i in range(count):
following = (i + 1) % count
faces.append((i, i + count, following + count, following))
return mesh_object(name, vertices, faces, material)
def _figure(spec, nominal_height):
"""Scaled primitive wrappers keep species proportions stable at any height."""
scale = float(spec.get("height", nominal_height)) / nominal_height
def point(value):
return tuple(component * scale for component in value)
class Figure:
def box(self, name, loc, size, material, rotation=None):
return box(name, point(loc), point(size), material, rotation=rotation)
def ellipsoid(self, name, loc, size, material, subdivisions=1):
return ellipsoid(name, point(loc), point(size), material, subdivisions=subdivisions)
def cylinder(self, name, loc, radius, depth, material, vertices=8, rotation=None):
return cylinder(name, point(loc), radius * scale, depth * scale,
material, vertices=vertices, rotation=rotation)
def cone(self, name, loc, radius1, radius2, depth, material, vertices=8, rotation=None):
return cone(name, point(loc), radius1 * scale, radius2 * scale, depth * scale,
material, vertices=vertices, rotation=rotation)
def beam(self, name, start, end, radius, material, vertices=6):
return beam(name, point(start), point(end), radius * scale,
material, vertices=vertices)
def ring(self, name, loc, major_radius, minor_radius, material,
rotation=None, segments=10):
return ring(name, point(loc), major_radius * scale, minor_radius * scale,
material, rotation=rotation, segments=segments)
def prism(self, name, outline, front_y, back_y, material):
return _prism(name, [(x * scale, z * scale) for x, z in outline],
front_y * scale, back_y * scale, material)
return Figure()
def _boots(f, material, x=0.22, width=0.26, height=0.18, depth=0.42):
for side, sign in (("L", -1), ("R", 1)):
f.box("Leg" + side + "_Boot", (sign * x, -0.075, height / 2),
(width, depth, height), material)
def _eyes(f, material, z, y, separation=0.10):
for side, sign in (("L", -1), ("R", 1)):
f.box("Head_Eye" + side, (sign * separation, y, z),
(0.065, 0.025, 0.034), material)
def build_lane(spec, mat):
"""12 m traversable width, 4 m repeat length, deck at Z=0.
Side decoration remains strictly outside X=[-6,+6], and nothing extends
past Y=[-2,+2], so consecutive copies meet without a transverse obstacle.
"""
box("Body_LaneFoundation", (0, 0, -0.195), (12, 4, 0.31), mat["primary"])
for column in range(6):
box("Body_LaneSlab_%02d" % column, (-5 + column * 2, 0, -0.02),
(2, 4, 0.04), mat["primary"])
for side, sign in (("L", -1), ("R", 1)):
box("Body_" + side + "RetainingLip", (sign * 6.15, 0, -0.10),
(0.30, 4, 0.50), mat["secondary"])
for index, y in enumerate((-1.42, 0, 1.42)):
ellipsoid("Body_" + side + "Basalt_%02d" % index,
(sign * 6.46, y, -0.05),
(0.26, 0.42, 0.26 if index != 1 else 0.30), mat["secondary"])
for index, y in enumerate((-1.15, 1.15)):
box("Body_" + side + "BrassMarker_%02d" % index,
(sign * 6.15, y, 0.165), (0.19, 0.20, 0.03), mat["accent"])
ellipsoid("Body_" + side + "Moss", (sign * 6.39, 0.65, 0.02),
(0.17, 0.34, 0.13), mat["light"])
empty("Anchor_Deck", (0, 0, 0))
empty("Anchor_JoinNorth", (0, 2, 0))
empty("Anchor_JoinSouth", (0, -2, 0))
def build_core(spec, mat):
"""A grounded crystal cradle with a physical team circle or triangle."""
triangular = int(spec.get("team", 0)) == 1
sides = 3 if triangular else 12
cylinder("Body_CorePlinth", (0, 0, 0.13), 1.26, 0.26,
mat["primary"], vertices=sides)
cylinder("Body_BrassPlinthRim", (0, 0, 0.285), 1.18, 0.05,
mat["secondary"], vertices=sides)
cylinder("Body_CradleStep", (0, 0, 0.37), 0.85, 0.12,
mat["primary"], vertices=8)
if triangular:
points = [(1.06 * sin(i * 2 * pi / 3), -1.06 * cos(i * 2 * pi / 3), 0.34)
for i in range(3)]
for i in range(3):
beam("Team_Triangle_%d" % i, points[i], points[(i + 1) % 3],
0.045, mat["accent"], vertices=6)
else:
ring("Team_Circle", (0, 0, 0.345), 1.025, 0.045, mat["accent"], segments=20)
# Multiple low-poly rings create a recognizable mineral, not a sphere.
vertices = [(0, 0, 0.51)]
for radius, z in ((0.37, 0.70), (0.58, 1.16), (0.36, 1.72)):
vertices.extend((radius * cos(i * pi / 3), radius * sin(i * pi / 3), z)
for i in range(6))
vertices.append((0, 0, 2.20))
faces = [(0, 1 + (i + 1) % 6, 1 + i) for i in range(6)]
for row in range(2):
start = 1 + 6 * row
for i in range(6):
following = (i + 1) % 6
faces.append((start + i, start + following,
start + 6 + following, start + 6 + i))
faces.extend((13 + i, 13 + (i + 1) % 6, 19) for i in range(6))
mesh_object("Body_CoreCrystal", vertices, faces, mat["accent"])
for index in range(4):
angle = pi / 4 + index * pi / 2
x, y = sin(angle), cos(angle)
beam("Arm_Cradle_%d" % index, (x * 0.72, y * 0.72, 0.42),
(x * 0.47, y * 0.47, 0.88), 0.075, mat["secondary"])
ellipsoid("Arm_CradleClasp_%d" % index,
(x * 0.48, y * 0.48, 0.85), (0.12, 0.12, 0.09), mat["light"])
empty("Anchor_CoreEffect", (0, 0, 1.25))
def build_worker(spec, mat):
"""Small clay Funkenleser with a fork and a framed energy container."""
f = _figure(spec, 1.1)
_boots(f, mat["primary"], x=0.18, width=0.23, height=0.14, depth=0.31)
for side, sign in (("L", -1), ("R", 1)):
f.beam("Leg" + side, (sign * 0.16, 0, 0.14),
(sign * 0.15, 0, 0.36), 0.10, mat["primary"])
f.ellipsoid("Body_Clay", (0, 0, 0.49), (0.30, 0.21, 0.30), mat["primary"])
f.prism("Body_Apron", [(-0.20, 0.68), (-0.24, 0.30), (0.24, 0.30), (0.20, 0.68)],
-0.23, -0.19, mat["secondary"])
f.box("Body_Buckle", (0, -0.248, 0.56), (0.10, 0.035, 0.07), mat["accent"])
f.ellipsoid("Head_Hood", (0, 0.005, 0.88), (0.26, 0.235, 0.22), mat["secondary"])
f.ellipsoid("Head_ClayFace", (0, -0.175, 0.88), (0.195, 0.10, 0.17), mat["primary"])
_eyes(f, mat["secondary"], 0.905, -0.274, separation=0.075)
f.beam("ArmL", (-0.23, 0, 0.64), (-0.40, -0.19, 0.52), 0.085, mat["primary"])
f.beam("ArmR", (0.23, 0, 0.64), (0.39, -0.23, 0.54), 0.085, mat["primary"])
f.ellipsoid("ArmL_Hand", (-0.40, -0.19, 0.52), (0.105, 0.09, 0.09), mat["primary"])
f.ellipsoid("ArmR_Hand", (0.39, -0.23, 0.54), (0.105, 0.09, 0.09), mat["primary"])
f.beam("Weapon_SparkForkHandle", (-0.43, -0.20, 0.13), (-0.43, -0.20, 0.78),
0.032, mat["accent"])
for sign in (-1, 1):
f.beam("Weapon_SparkForkProng_%d" % sign, (-0.43, -0.20, 0.78),
(-0.43 + sign * 0.10, -0.20, 0.99), 0.026, mat["accent"])
f.cylinder("Weapon_ContainerBase", (0.44, -0.24, 0.32), 0.11, 0.045, mat["accent"])
f.cylinder("Weapon_ContainerLid", (0.44, -0.24, 0.61), 0.11, 0.045, mat["accent"])
# An open brass cage gives glass-like legibility without alpha sorting.
for index in range(4):
angle = index * pi / 2
f.beam("Weapon_ContainerRib_%d" % index,
(0.44 + cos(angle) * 0.085, -0.24 + sin(angle) * 0.085, 0.33),
(0.44 + cos(angle) * 0.085, -0.24 + sin(angle) * 0.085, 0.60),
0.016, mat["accent"], vertices=4)
f.ellipsoid("Weapon_CapturedSpark", (0.44, -0.24, 0.47),
(0.058, 0.058, 0.105), mat["light"])
def _scout(spec, mat):
f = _figure(spec, 1.70)
_boots(f, mat["primary"], x=0.18, width=0.23, height=0.18)
for side, sign in (("L", -1), ("R", 1)):
f.beam("Leg" + side, (sign * 0.18, 0, 0.17),
(sign * 0.15, 0, 0.78), 0.087, mat["primary"])
f.box("Leg" + side + "_ShinPlate", (sign * 0.18, -0.095, 0.35),
(0.13, 0.05, 0.24), mat["secondary"])
f.cone("Body_Armor", (0, 0, 0.97), 0.22, 0.28, 0.49, mat["primary"], vertices=6)
f.box("Body_Belt", (0, 0, 0.77), (0.44, 0.30, 0.10), mat["secondary"])
f.ellipsoid("Head_Hood", (0, 0.015, 1.42), (0.23, 0.21, 0.25), mat["primary"])
f.cone("Head_HoodPoint", (0, 0.055, 1.60), 0.145, 0, 0.20, mat["primary"], vertices=5)
f.prism("Head_Mask", [(-0.15, 1.54), (-0.13, 1.32), (0, 1.26), (0.13, 1.32), (0.15, 1.54)],
-0.21, -0.165, mat["light"])
_eyes(f, mat["primary"], 1.46, -0.225, separation=0.075)
f.beam("ArmL_Upper", (-0.24, 0, 1.12), (-0.34, -0.17, 0.96), 0.078, mat["primary"])
f.beam("ArmL_Fore", (-0.34, -0.17, 0.96), (-0.06, -0.37, 1.02), 0.071, mat["primary"])
f.beam("ArmR_Upper", (0.24, 0, 1.12), (0.35, -0.15, 0.97), 0.078, mat["primary"])
f.beam("ArmR_Fore", (0.35, -0.15, 0.97), (0.25, -0.43, 1.07), 0.071, mat["primary"])
f.box("Weapon_DartLauncherStock", (0.19, -0.34, 1.07), (0.19, 0.44, 0.15), mat["secondary"])
f.cylinder("Weapon_DartLauncherBarrel", (0.19, -0.66, 1.09),
0.065, 0.34, mat["light"], rotation=(pi / 2, 0, 0))
f.cylinder("Weapon_DartLauncherBore", (0.19, -0.836, 1.09),
0.04, 0.008, mat["primary"], rotation=(pi / 2, 0, 0))
f.box("Body_DartQuiver", (-0.14, 0.23, 1.02), (0.23, 0.17, 0.45), mat["secondary"],
rotation=(0, -0.16, 0))
for index in range(3):
x = -0.23 + index * 0.085
f.beam("Weapon_SpareDart_%d" % index, (x, 0.24, 1.10), (x - 0.06, 0.24, 1.47),
0.024, mat["light"], vertices=4)
def _ram(spec, mat):
f = _figure(spec, 1.48)
_boots(f, mat["primary"], x=0.27, width=0.35, height=0.20, depth=0.45)
for side, sign in (("L", -1), ("R", 1)):
f.box("Leg" + side, (sign * 0.27, 0, 0.35), (0.25, 0.30, 0.35), mat["primary"])
f.ellipsoid("Arm" + side + "_Pauldron", (sign * 0.43, 0, 1.05),
(0.27, 0.25, 0.25), mat["secondary"])
f.box("Body_RustArmor", (0, 0, 0.83), (0.74, 0.48, 0.67), mat["secondary"])
f.box("Body_UpperLamella", (0, -0.256, 1.01), (0.71, 0.06, 0.18), mat["primary"])
f.box("Body_LowerLamella", (0, -0.261, 0.80), (0.71, 0.06, 0.16), mat["primary"])
f.ellipsoid("Head_Helmet", (0, 0, 1.29), (0.27, 0.23, 0.19), mat["primary"])
f.box("Head_BrowGuard", (0, -0.23, 1.35), (0.45, 0.085, 0.10), mat["light"])
_eyes(f, mat["accent"], 1.275, -0.235)
f.beam("ArmL", (-0.43, 0, 1.03), (-0.43, -0.40, 0.81), 0.14, mat["primary"])
f.beam("ArmR", (0.43, 0, 1.03), (0.61, -0.18, 0.72), 0.14, mat["primary"])
f.box("ArmR_HammerFist", (0.61, -0.19, 0.66), (0.30, 0.28, 0.29), mat["secondary"])
outline = [(-0.63, 1.09), (-0.68, 0.34), (-0.48, 0.21), (0.45, 0.21),
(0.65, 0.34), (0.60, 1.09)]
f.prism("Weapon_RamShield", outline, -0.54, -0.39, mat["secondary"])
f.box("Weapon_ShieldTopRim", (-0.015, -0.575, 1.05), (1.18, 0.07, 0.095), mat["light"])
f.prism("Weapon_CentralWedge", [(-0.13, 0.94), (-0.18, 0.37), (0, 0.28), (0.18, 0.37), (0.13, 0.94)],
-0.74, -0.53, mat["primary"])
def _singer(spec, mat):
f = _figure(spec, 1.82)
_boots(f, mat["primary"], x=0.18, width=0.23, height=0.12, depth=0.32)
f.cone("Body_BellRobe", (0, 0, 0.63), 0.43, 0.20, 1.10, mat["primary"], vertices=8)
for index, x in enumerate((-0.22, 0, 0.22)):
f.prism("Body_RobeFold_%d" % index,
[(x - 0.055, 0.15), (x + 0.055, 0.15), (x * 0.45, 1.09)],
-0.32, -0.24, mat["accent"])
f.ellipsoid("Body_Mantle", (0, 0, 1.24), (0.37, 0.27, 0.21), mat["accent"])
f.ellipsoid("Head_DeepHood", (0, 0, 1.57), (0.26, 0.25, 0.25), mat["primary"])
f.prism("Head_SingerMask", [(-0.13, 1.68), (-0.12, 1.47), (0, 1.41), (0.12, 1.47), (0.13, 1.68)],
-0.246, -0.21, mat["light"])
_eyes(f, mat["primary"], 1.60, -0.257, separation=0.065)
f.beam("ArmL", (-0.29, 0, 1.27), (-0.34, -0.37, 1.05), 0.085, mat["primary"])
f.beam("ArmR", (0.29, 0, 1.27), (0.36, -0.43, 1.10), 0.085, mat["primary"])
f.ellipsoid("ArmL_Hand", (-0.34, -0.37, 1.05), (0.095, 0.075, 0.075), mat["light"])
f.ellipsoid("ArmR_Hand", (0.36, -0.43, 1.10), (0.095, 0.075, 0.075), mat["light"])
f.beam("Weapon_HornPipe", (-0.30, -0.37, 1.08), (0.36, -0.43, 1.13), 0.075, mat["secondary"])
# A true open bell: two rings connected by a tapered shell, no filled mouth.
vertices = []
for y, radius in ((-0.43, 0.075), (-0.84, 0.28), (-0.81, 0.235), (-0.47, 0.042)):
for index in range(10):
angle = index * pi / 5
vertices.append((0.36 + cos(angle) * radius, y, 1.13 + sin(angle) * radius))
faces = []
for row in range(3):
for i in range(10):
j = (i + 1) % 10
faces.append((row * 10 + i, row * 10 + j, (row + 1) * 10 + j, (row + 1) * 10 + i))
scale = float(spec.get("height", 1.82)) / 1.82
mesh_object("Weapon_ResonanceHorn", [tuple(v * scale for v in point) for point in vertices],
faces, mat["secondary"])
f.ring("Weapon_HornRim", (0.36, -0.84, 1.13), 0.258, 0.023, mat["light"],
rotation=(pi / 2, 0, 0))
def _breaker(spec, mat):
f = _figure(spec, 1.82)
_boots(f, mat["primary"], x=0.28, width=0.36, height=0.22, depth=0.49)
for side, sign in (("L", -1), ("R", 1)):
f.box("Leg" + side, (sign * 0.26, 0, 0.47), (0.28, 0.31, 0.54), mat["primary"])
f.box("Leg" + side + "_Knee", (sign * 0.27, -0.19, 0.51),
(0.27, 0.13, 0.24), mat["secondary"])
f.box("Arm" + side + "_Pauldron", (sign * 0.48, 0, 1.29),
(0.42, 0.52, 0.34), mat["primary"], rotation=(0, sign * 0.14, 0))
f.cone("Body_SteelTorso", (0, 0, 1.01), 0.35, 0.47, 0.70, mat["primary"], vertices=6)
f.prism("Body_RustChestInset", [(-0.28, 1.29), (0, 0.95), (0.28, 1.29)],
-0.397, -0.35, mat["secondary"])
f.box("Head_Helmet", (0, 0, 1.61), (0.43, 0.40, 0.42), mat["primary"])
f.prism("Head_Faceplate", [(-0.17, 1.76), (-0.17, 1.50), (0, 1.44), (0.17, 1.50), (0.17, 1.76)],
-0.23, -0.19, mat["light"])
f.box("Head_Visor", (0, -0.246, 1.66), (0.25, 0.026, 0.045), mat["primary"])
f.beam("ArmL", (-0.47, 0, 1.25), (-0.31, -0.50, 0.92), 0.12, mat["primary"])
f.beam("ArmR", (0.47, 0, 1.25), (0.30, -0.50, 1.20), 0.12, mat["primary"])
f.ellipsoid("ArmL_Hand", (-0.31, -0.50, 0.92), (0.145, 0.12, 0.12), mat["secondary"])
f.ellipsoid("ArmR_Hand", (0.30, -0.50, 1.20), (0.145, 0.12, 0.12), mat["secondary"])
f.beam("Weapon_HammerShaft", (-0.65, -0.51, 0.77), (0.66, -0.51, 1.36),
0.055, mat["secondary"])
f.prism("Weapon_KeilhammerHead", [(0.40, 1.26), (0.43, 1.58), (0.82, 1.80),
(1.02, 1.61), (0.90, 1.18)],
-0.79, -0.24, mat["primary"])
# Offset along the sloped edge normal; coplanar cap faces would flicker.
f.prism("Weapon_ChiselEdge", [(0.832, 1.814), (1.032, 1.624), (0.992, 1.354), (0.932, 1.644)],
-0.80, -0.23, mat["light"])
def _rift(spec, mat):
f = _figure(spec, 2.08)
_boots(f, mat["primary"], x=0.22, width=0.21, height=0.12, depth=0.35)
for side, sign in (("L", -1), ("R", 1)):
f.beam("Leg" + side + "_Shin", (sign * 0.22, 0, 0.12),
(sign * 0.30, 0.12, 0.45), 0.069, mat["primary"])
f.beam("Leg" + side + "_Thigh", (sign * 0.30, 0.12, 0.45),
(sign * 0.17, 0, 0.81), 0.083, mat["primary"])
f.ellipsoid("Body_Pelvis", (0, 0, 0.83), (0.25, 0.16, 0.16), mat["primary"])
# Separate ribs bound real negative space; there is deliberately no chest fill.
for side, sign in (("L", -1), ("R", 1)):
f.beam("Body_" + side + "LowerRib", (sign * 0.15, 0, 0.89),
(sign * 0.31, 0, 1.18), 0.095, mat["primary"])
f.beam("Body_" + side + "UpperRib", (sign * 0.31, 0, 1.18),
(sign * 0.21, 0, 1.52), 0.10, mat["primary"])
f.beam("Body_" + side + "RiftSeam", (sign * 0.17, -0.080, 0.95),
(sign * 0.245, -0.080, 1.18), 0.019, mat["accent"], vertices=4)
f.beam("Body_" + side + "RiftUpperSeam", (sign * 0.245, -0.080, 1.18),
(sign * 0.16, -0.080, 1.46), 0.019, mat["accent"], vertices=4)
f.beam("Body_Collar", (-0.21, 0, 1.52), (0.21, 0, 1.52), 0.095, mat["primary"])
f.ellipsoid("Head_HollowShell", (0, 0, 1.73), (0.20, 0.155, 0.23), mat["primary"])
f.box("Head_RiftMouth", (0, -0.153, 1.69), (0.17, 0.025, 0.043), mat["accent"])
for index, (x, z) in enumerate(((-0.19, 1.90), (-0.10, 2.02), (0.10, 2.02), (0.19, 1.90))):
f.beam("Head_BrokenCrown_%d" % index, (x, 0, z - 0.13), (x * 1.18, 0, z + 0.06),
0.038, mat["light"], vertices=5)
f.beam("ArmL_Upper", (-0.30, 0, 1.47), (-0.48, 0.02, 1.13), 0.068, mat["primary"])
f.beam("ArmL_Fore", (-0.48, 0.02, 1.13), (-0.46, -0.18, 0.77), 0.056, mat["primary"])
for index in range(3):
f.beam("ArmL_Claw_%d" % index, (-0.50 + index * 0.043, -0.18, 0.79),
(-0.51 + index * 0.049, -0.25, 0.64), 0.020, mat["light"], vertices=4)
f.beam("ArmR_Upper", (0.30, 0, 1.47), (0.49, 0, 1.10), 0.068, mat["primary"])
f.beam("ArmR_Fore", (0.49, 0, 1.10), (0.37, -0.33, 1.23), 0.056, mat["primary"])
f.ellipsoid("ArmR_CradlingHand", (0.37, -0.33, 1.23), (0.12, 0.09, 0.052), mat["primary"])
f.ellipsoid("Weapon_DrainOrb", (0.37, -0.34, 1.40), (0.13, 0.13, 0.13), mat["accent"])
f.ring("Weapon_DrainOrbit", (0.37, -0.34, 1.40), 0.175, 0.015,
mat["light"], rotation=(0.35, 0.20, 0), segments=10)
def build_mercenary(spec, mat):
"""Dispatch five neutral mercenaries, retaining recognizable role silhouettes."""
builders = {"s01": _scout, "s02": _ram, "s03": _singer,
"s04": _breaker, "s05": _rift}
identifier = str(spec["id"])
if identifier not in builders:
raise ValueError("Unknown Saumwacht mercenary: " + identifier)
builders[identifier](spec, mat)
def _builder(role):
return globals()["build_" + role]
import hashlib
import json
from pathlib import Path
import struct
import bpy
from mathutils import Vector
def bounds(objects):
bpy.context.view_layer.update()
points = [obj.matrix_world @ Vector(corner) for obj in objects if obj.type == 'MESH'
for corner in obj.bound_box]
if not points:
raise ValueError('No mesh geometry was generated')
return (Vector([min(point[i] for point in points) for i in range(3)]),
Vector([max(point[i] for point in points) for i in range(3)]))
def _bake_parts(objects):
"""Keep each part's origin, but bake its local rotation/scale into the mesh."""
for obj in objects:
if obj.type != 'MESH':
continue
obj.data.transform(obj.matrix_basis.to_3x3().to_4x4())
obj.rotation_euler = (0, 0, 0)
obj.scale = (1, 1, 1)
obj.data.update()
def _fit_to_cell(objects, spec):
low, high = bounds(objects)
dims = high - low
if spec['role'] == 'lane':
# Tile top stays at Z=0; thickness and decorative lip may cross that plane.
return 1.0
factor = min(spec['height'] / dims.z, spec['footprint'] / max(dims.x, dims.y))
center = Vector(((low.x + high.x) / 2, (low.y + high.y) / 2, low.z))
for obj in objects:
obj.location = (obj.location - center) * factor
obj.scale *= factor
_bake_parts(objects)
return factor
def _canonical_glb_names(path):
"""Make GLB part names independent of other objects open in Blender.
Blender may add .001 to datablock names in a populated session. Exported
custom properties retain logical part names; this changes JSON names only,
preserving all mesh buffers and material data produced by bpy's exporter.
"""
raw = path.read_bytes()
magic, version, length = struct.unpack_from('<4sII', raw)
if (magic, version, length) != (b'glTF', 2, len(raw)):
raise ValueError('Invalid GLB container')
chunks, offset = [], 12
while offset < len(raw):
size, kind = struct.unpack_from('<I4s', raw, offset)
payload = raw[offset + 8:offset + 8 + size]
if kind == b'JSON':
doc = json.loads(payload)
for material in doc.get('materials', []):
logical_name = material.get('extras', {}).get('material_name')
if logical_name:
material['name'] = logical_name
names = set()
for node in doc.get('nodes', []):
name = node.get('extras', {}).get('part_name')
if name:
if name in names:
raise ValueError('Duplicate logical part: ' + name)
names.add(name)
node['name'] = name
payload = json.dumps(doc, ensure_ascii=False, separators=(',', ':')).encode('utf-8')
payload += b' ' * (-len(payload) % 4)
chunks.append(struct.pack('<I4s', len(payload), kind) + payload)
offset += 8 + size
content = b''.join(chunks)
path.write_bytes(struct.pack('<4sII', b'glTF', 2, len(content) + 12) + content)
def _mesh_statistics(objects):
triangles = 0
for obj in objects:
if obj.type == 'MESH':
obj.data.calc_loop_triangles()
triangles += len(obj.data.loop_triangles)
return triangles
def build_one(asset_id, export_dir=None, blend_dir=None, keep_scene=True,
height=None, palette_overrides=None):
spec = get_spec(asset_id)
if height is not None:
if height <= 0:
raise ValueError('HEIGHT must be positive')
spec['height'] = height
export_dir = Path(export_dir or DEFAULT_EXPORT_DIR).expanduser().resolve()
blend_dir = Path(blend_dir or DEFAULT_BLEND_DIR).expanduser().resolve()
export_dir.mkdir(parents=True, exist_ok=True)
blend_dir.mkdir(parents=True, exist_ok=True)
record_dir = SOURCE / 'records' if export_dir == DEFAULT_EXPORT_DIR.resolve() else export_dir / '_records'
record_dir.mkdir(parents=True, exist_ok=True)
previous_scene = bpy.context.window.scene
scene = bpy.data.scenes.new('Saumwacht_' + asset_id)
scene['saumwacht_generated'] = True
scene.unit_settings.system = 'METRIC'
scene.unit_settings.scale_length = 1.0
bpy.context.window.scene = scene
completed = False
try:
materials = make_materials(spec['palette'], palette_overrides)
if spec['role'] == 'worker':
build_high_worker(spec, materials)
elif spec['role'] == 'core':
build_high_core(spec, materials)
elif spec['faction'] == 'glutpakt':
build_high_glutpakt(spec, materials)
elif spec['faction'] == 'nebelchor':
build_nebelchor(spec, materials)
elif spec['faction'] == 'rift':
build_high_enemy(spec, materials)
elif spec['faction'] == 'nietenkonvent':
build_nietenkonvent(spec, materials)
elif spec['faction'] == 'wurzelbund':
build_high_wurzelbund(spec, materials)
elif spec['faction'] == 'neutral':
build_neutral(spec, materials)
else:
_builder(spec['role'])(spec, materials)
if (spec['faction'] == 'neutral' and spec['role'] != 'worker') or spec['role'] == 'lane':
enhance_existing(spec, materials)
if spec.get("detail_level") == "high":
sculpt_game_surfaces(spec)
finalize_high_geometry()
objects = list(scene.objects)
_bake_parts(objects)
factor = _fit_to_cell(objects, spec)
low, high = bounds(objects)
triangles = _mesh_statistics(objects)
cap = int(spec.get('triangle_budget', 3000 if spec['role'] in ('core', 'lane') else 1800))
if triangles > cap:
raise ValueError(f'{asset_id}: {triangles} triangles exceeds {cap}')
root = empty('Model_' + asset_id)
root['asset_id'] = asset_id
root['faction'] = spec['faction']
root['role'] = spec['role']
root['forward_axis'] = '+Z in glTF/Godot'
root['phase'] = 7 if spec.get('detail_level') == 'high' else 4
for obj in objects:
obj.parent = root
marker = empty('Forward', (0, -0.75, 0))
marker.parent = root
bpy.ops.object.select_all(action='DESELECT')
for obj in scene.objects:
obj.select_set(True)
bpy.context.view_layer.objects.active = root
glb_path = export_dir / spec['file']
bpy.ops.export_scene.gltf(filepath=str(glb_path), export_format='GLB',
use_selection=True, use_active_scene=True, export_yup=True,
export_apply=True, export_materials='EXPORT',
export_extras=True, export_cameras=False,
export_lights=False, export_animations=False)
_canonical_glb_names(glb_path)
blend_path = blend_dir / (asset_id + '.blend')
# Offline sculpt detail is saved only after the runtime export.
add_offline_sculpt_modifiers(objects)
add_offline_materials(objects, PROJECT)
# Write this scene and its dependencies, never the user's other scenes.
bpy.data.libraries.write(str(blend_path), {scene}, path_remap='RELATIVE_ALL', fake_user=True, compress=True)
resource_path = 'res://' + glb_path.relative_to(PROJECT).as_posix() if glb_path.is_relative_to(PROJECT) else None
record = {**spec, 'resource_path': resource_path,
'export_path': str(glb_path), 'blend_path': str(blend_path),
'triangles': triangles, 'offline_sculpt_parts': sum(bool(o.get('sculpt_surface')) for o in objects),
'offline_render_triangles': sum(len(o.data.loop_triangles)*(16 if o.get('sculpt_surface') else 1) for o in objects if o.type == 'MESH'), 'mesh_parts': sum(obj.type == 'MESH' for obj in objects),
'parts': [obj.get('part_name', obj.name) for obj in objects],
'bounds_blender': {'min': list(low), 'max': list(high)},
'bounds_godot': {'min': [low.x, low.z, -high.y], 'max': [high.x, high.z, -low.y]},
'scale_applied': factor, 'blender_version': bpy.app.version_string,
'sha256': hashlib.sha256(glb_path.read_bytes()).hexdigest(),
'source': 'Original authored bpy geometry. Sculpted surface relief and editable high-resolution modifiers; runtime materials are assigned by Godot.',
'status': 'exported'}
(record_dir / (asset_id + '.json')).write_text(json.dumps(record, ensure_ascii=False, indent=2) + '\n')
completed = True
print(f'EXPORTED {asset_id}: {triangles} triangles, {record["mesh_parts"]} parts -> {glb_path}')
return record
finally:
if not keep_scene or not completed:
bpy.context.window.scene = previous_scene
owned = list(scene.objects)
meshes = [obj.data for obj in owned if obj.type == 'MESH']
materials = {mat for mesh in meshes for mat in mesh.materials if mat is not None}
bpy.data.scenes.remove(scene)
for obj in owned:
if obj.users == 0:
bpy.data.objects.remove(obj)
for mesh in meshes:
if mesh.users == 0:
bpy.data.meshes.remove(mesh)
for mat in materials:
if mat.users == 0:
bpy.data.materials.remove(mat)
def main():
# CLI-Optionen sind optional; im Blender-Scripting-Tab genügt "Run Script".
import argparse
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--id', default=ASSET_ID)
parser.add_argument('--export-dir', default=EXPORT_DIR)
parser.add_argument('--blend-dir', default=BLEND_DIR)
parser.add_argument('--height', type=float, default=HEIGHT)
parser.add_argument('--no-keep-scene', action='store_true')
args = parser.parse_args(sys.argv[sys.argv.index('--') + 1:] if '--' in sys.argv else [])
build_one(args.id, export_dir=args.export_dir, blend_dir=args.blend_dir,
height=args.height, palette_overrides=PALETTE_OVERRIDES,
keep_scene=KEEP_SCENE and not args.no_keep_scene)
print('STANDALONE_PASS:', args.id)
if __name__ == '__main__':
main()