"""Saumwacht · Fernkämpfer · 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 = 'g05' # 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': 'g05', 'name': 'Funkenlanze', 'faction': 'glutpakt', 'palette': 'glutpakt', 'role': 'ranged', 'upgraded': False, 'height': 1.45, 'footprint': 1.85, 'file': 'unit_g05.glb', 'triangle_budget': 180000, 'detail_level': 'high'}, {'id': 'g06', 'name': 'Glasballiste', 'faction': 'glutpakt', 'palette': 'glutpakt', 'role': 'ranged', 'upgraded': True, 'height': 1.5999999999999999, 'footprint': 1.85, 'file': 'unit_g06.glb', 'triangle_budget': 180000, 'detail_level': 'high'}, {'id': 'm05', 'name': 'Bolzenknecht', 'faction': 'nietenkonvent', 'palette': 'nietenkonvent', 'role': 'ranged', 'upgraded': False, 'height': 1.45, 'footprint': 1.85, 'file': 'unit_m05.glb'}, {'id': 'm06', 'name': 'Linienmeister', 'faction': 'nietenkonvent', 'palette': 'nietenkonvent', 'role': 'ranged', 'upgraded': True, 'height': 1.5999999999999999, 'footprint': 1.85, 'file': 'unit_m06.glb'}, {'id': 'n05', 'name': 'Fernflüsterer', 'faction': 'nebelchor', 'palette': 'nebelchor', 'role': 'ranged', 'upgraded': False, 'height': 1.45, 'footprint': 1.85, 'file': 'unit_n05.glb'}, {'id': 'n06', 'name': 'Fernrufer', 'faction': 'nebelchor', 'palette': 'nebelchor', 'role': 'ranged', 'upgraded': True, 'height': 1.5999999999999999, 'footprint': 1.85, 'file': 'unit_n06.glb'}, {'id': 'w05', 'name': 'Samenschleuder', 'faction': 'wurzelbund', 'palette': 'wurzelbund', 'role': 'ranged', 'upgraded': False, 'height': 1.45, 'footprint': 1.85, 'file': 'unit_w05.glb', 'triangle_budget': 180000, 'detail_level': 'high'}, {'id': 'w06', 'name': 'Zapfenkrone', 'faction': 'wurzelbund', 'palette': 'wurzelbund', 'role': 'ranged', 'upgraded': True, 'height': 1.5999999999999999, 'footprint': 1.85, 'file': 'unit_w06.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, sin, pi, sqrt def _add(a, b): return tuple(a[i] + b[i] for i in range(3)) def _mul(a, scale): return tuple(v * scale for v in a) def _cross(a, b): return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]) def _unit(a): length = sqrt(sum(v * v for v in a)) return tuple(v / length for v in a) def _frame(axis): w = _unit(axis) reference = (0, 0, 1) if abs(w[2]) < .9 else (0, 1, 0) u = _unit(_cross(reference, w)) return u, _cross(w, u), w def _lathe(name, loc, profile, mat, axis=(0, 0, 1), segments=10): """Closed profile, including real inner walls; zero radii become poles. Profile travels from the back/bottom up the outside, down the inside. Independent pole vertices avoid degenerate zero-area triangles. """ u, v, w = _frame(axis) verts, loops, faces = [], [], [] for radius, depth in profile: if radius == 0: loops.append([len(verts)]) verts.append(_add(loc, _mul(w, depth))) continue loop = [] for k in range(segments): angle = 2 * pi * k / segments offset = _add(_mul(w, depth), _add(_mul(u, radius * cos(angle)), _mul(v, radius * sin(angle)))) loop.append(len(verts)) verts.append(_add(loc, offset)) loops.append(loop) for j in range(len(loops)): low, high = loops[j], loops[(j + 1) % len(loops)] if len(low) == 1 and len(high) == 1: continue for k in range(segments): nxt = (k + 1) % segments if len(low) == 1: faces.append((low[0], high[nxt], high[k])) elif len(high) == 1: faces.append((low[k], low[nxt], high[0])) else: faces.append((low[k], low[nxt], high[nxt], high[k])) mesh_object(name, verts, faces, mat) def _cup(name, loc, radius, depth, mat, axis=(0, 0, 1), segments=10): # The rim is 13% thick; the inset bottom is visible from above/front. _lathe(name, loc, [(0, 0), (radius * .5, .04), (radius * .73, depth * .35), (radius, depth), (radius * .87, depth), (radius * .59, depth * .35), (0, depth * .20)], mat, axis, segments) def _horn(name, loc, radius, depth, mat, axis=(0, -1, .18)): _lathe(name, loc, [(radius * .16, 0), (radius * .25, depth * .42), (radius * .58, depth * .76), (radius, depth), (radius * .84, depth), (radius * .43, depth * .72), (radius * .1, depth * .35), (0, depth * .29), (0, 0)], mat, axis, 10) def _spike(name, start, end, radius, mat, sides=5): axis = tuple(end[i] - start[i] for i in range(3)) u, v, _ = _frame(axis) verts = [_add(start, _add(_mul(u, radius * cos(2 * pi * k / sides)), _mul(v, radius * sin(2 * pi * k / sides)))) for k in range(sides)] verts.append(end) faces = [tuple(reversed(range(sides)))] faces.extend((k, (k + 1) % sides, sides) for k in range(sides)) mesh_object(name, verts, faces, mat) def _leaf(name, loc, width, height, mat, lean=0): # Thick diamond leaf: large central ridge, only eight triangles. x, y, z = loc verts = [(x, y, z), (x - width / 2, y, z + height * .42), (x + lean, y + .05, z + height), (x + width / 2, y, z + height * .42), (x + lean * .3, y - .09, z + height * .45), (x + lean * .3, y + .055, z + height * .45)] faces = [(0, 4, 1), (1, 4, 2), (2, 4, 3), (3, 4, 0), (1, 5, 0), (2, 5, 1), (3, 5, 2), (0, 5, 3)] mesh_object(name, verts, faces, mat) def _eyes(loc, mat, spread=.075, scale=.038): for side, suffix in ((-1, "L"), (1, "R")): p = (loc[0] + side * spread, loc[1], loc[2]) ellipsoid("Head_Eye" + suffix, p, (scale, .023, scale * .75), mat) def _mask(name, loc, scale, mat, eyes_mat): ellipsoid(name, loc, (.15 * scale, .075 * scale, .22 * scale), mat) for sign, suffix in ((-1, "L"), (1, "R")): ellipsoid(name + "_Eye" + suffix, (loc[0] + sign * .052 * scale, loc[1] - .071 * scale, loc[2] + .023 * scale), (.028 * scale, .012 * scale, .046 * scale), eyes_mat) def _basalt_legs(mat, wide=False): spread = .32 if wide else .22 for side, suffix in ((-1, "L"), (1, "R")): ellipsoid("Leg" + suffix + "_Foot", (side * spread, -.075, .12), (.20 if wide else .15, .24, .12), mat) beam("Leg" + suffix + "_Shin", (side * spread, 0, .15), (side * spread * .7, .02, .49), .095, mat, vertices=6) def _mechanical_legs(mat, wide=False): spread = .27 if wide else .20 for side, suffix in ((-1, "L"), (1, "R")): box("Leg" + suffix + "_Foot", (side * spread, -.07, .07), (.22, .33, .14), mat["accent"]) beam("Leg" + suffix + "_Shin", (side * spread, 0, .14), (side * spread, .08, .35), .065, mat["accent"], vertices=6) ellipsoid("Leg" + suffix + "_Knee", (side * spread, .08, .35), (.10, .10, .10), mat["secondary"]) beam("Leg" + suffix + "_Thigh", (side * spread, .08, .35), (side * spread * .66, 0, .57), .09, mat["primary"], vertices=6) def _root_feet(mat, count=4, spread=.43, top=.60): for k in range(count): a = 2 * pi * k / count + pi / 4 base = (spread * cos(a), spread * sin(a), .055) elbow = (spread * .6 * cos(a), spread * .6 * sin(a), .24) suffix = str(k + 1) beam("Leg_Root" + suffix, base, elbow, .09, mat, vertices=5) beam("Leg_RootUpper" + suffix, elbow, (0, .02, top), .12, mat, vertices=5) ellipsoid("Leg_RootToe" + suffix, base, (.16, .10, .055), mat) def _mist_robe(mat, upgraded=False): # Alternating long cloth points touch the floor; the upper body floats. cone("Body_Robe", (0, .035, .49), .31 if upgraded else .25, .13, .74, mat["primary"], vertices=7) for k in range(5): a = 2 * pi * k / 5 _spike("Body_ClothPoint" + str(k), (.16 * cos(a), .035 + .16 * sin(a), .35), (.31 * cos(a), .035 + .27 * sin(a), .015), .11, mat["secondary"] if k % 2 else mat["primary"]) ellipsoid("Body_Chest", (0, 0, .90), (.23, .16, .23), mat["secondary"]) def _glut_ranged(up, m): _basalt_legs(m["primary"], up) ellipsoid("Body_Core", (0, .035, .69), (.30 if up else .21, .22, .27), m["primary"]) ellipsoid("Head_Helm", (0, -.015, 1.12), (.19, .16, .20), m["light"]) _eyes((0, -.17, 1.13), m["secondary"]) box("Body_Ember", (0, -.197, .71), (.11, .045, .19), m["secondary"]) if up: # Broad glass bow arms distinguish the upgrade from the slender lance. box("Weapon_Rail", (0, -.20, .96), (.10, .98, .09), m["accent"]) for s, suffix in ((-1, "L"), (1, "R")): beam("Arm" + suffix, (s * .2, 0, .88), (s * .30, -.15, .91), .075, m["primary"], vertices=6) _spike("Weapon_GlassBow" + suffix, (s * .06, -.25, .99), (s * .62, -.04, 1.25), .16, m["secondary"]) beam("Weapon_String" + suffix, (s * .62, -.04, 1.25), (0, .22, 1.03), .012, m["light"], vertices=4) beam("Weapon_Bolt", (0, .20, 1.02), (0, -.64, 1.02), .025, m["light"]) _spike("Weapon_BoltHead", (0, -.57, 1.02), (0, -.80, 1.02), .08, m["secondary"]) else: beam("ArmL", (-.15, 0, .89), (-.25, -.07, .70), .07, m["primary"]) beam("ArmR", (.15, 0, .90), (.30, -.19, .91), .075, m["primary"]) beam("Weapon_Lance", (.29, .34, .81), (.29, -.60, 1.05), .038, m["accent"]) _spike("Weapon_GlassTip", (.29, -.53, 1.03), (.29, -.81, 1.11), .105, m["secondary"]) def _nieten_ranged(up, m): _mechanical_legs(m) cone("Body_Cuirass", (0, .02, .78), .21, .27, .43, m["primary"], vertices=6) box("Body_Plate", (0, -.18, .80), (.27, .09, .22), m["secondary"]) beam("Head_Neck", (0, 0, 1.0), (0, 0, 1.10), .075, m["accent"]) box("Head_Housing", (0, -.015, 1.22), (.29, .25, .25), m["light"]) box("Head_Visor", (0, -.146, 1.23), (.18, .028, .05), m["primary"]) weapon_x = 0 if up else .29 beam("ArmL", (-.21, 0, .92), (-.28, -.13, .77), .065, m["accent"]) beam("ArmR", (.21, 0, .92), (weapon_x + .07, -.24, .91), .065, m["accent"]) for s, suffix in ((-1, "L"), (1, "R")): box("Weapon_Rail" + suffix, (weapon_x + s * .055, -.30, .97), (.055, .82 if up else .61, .07), m["accent"]) tip = (weapon_x + s * (.44 if up else .23), -.27, .97) beam("Weapon_Bow" + suffix, (weapon_x, -.43, .97), tip, .055, m["secondary"], vertices=6) beam("Weapon_String" + suffix, tip, (weapon_x, .02, .97), .011, m["light"], vertices=4) _spike("Weapon_Bolt", (weapon_x, -.44, .98), (weapon_x, -.78 if up else -.66, .98), .052, m["light"]) if up: box("Weapon_Sight", (0, -.13, 1.08), (.08, .25, .09), m["primary"]) _spike("Head_Crest", (0, .03, 1.34), (0, .10, 1.58), .075, m["secondary"]) for s, suffix in ((-1, "L"), (1, "R")): box("Arm" + suffix + "_Pauldron", (s * .27, .01, .96), (.16, .25, .13), m["secondary"]) def _nebel_ranged(up, m): _mist_robe(m, up) _mask("Head_Mask", (0, -.105, 1.18), 1.0, m["light"], m["primary"]) _leaf("Head_Hood", (0, .055, 1.16), .35, .37, m["primary"]) for s, suffix in ((-1, "L"), (1, "R")): beam("Arm" + suffix, (s * .18, 0, .94), (s * .31, -.18, .77), .055, m["secondary"], vertices=5) horn_sides = (-1, 1) if up else (1,) for s in horn_sides: suffix = "L" if s < 0 else "R" root = (s * .25, -.12, .79) elbow = (s * .46, .07, 1.08 if up else .97) mouth = (s * (.48 if up else .31), -.03, 1.27 if up else 1.02) beam("Weapon_HornStem" + suffix, root, elbow, .055, m["secondary"]) beam("Weapon_HornCurve" + suffix, elbow, mouth, .072, m["light"]) _horn("Weapon_HornBell" + suffix, mouth, .19 if up else .16, .40 if up else .49, m["light"]) axis = _unit((0, -1, .18)) lip = _add(mouth, _mul(axis, .39 if up else .48)) _lathe("Weapon_MintLip" + suffix, lip, [(.19 if up else .16, 0), (.19 if up else .16, .025), (.16 if up else .132, .025), (.16 if up else .132, 0)], m["accent"], axis) def _wurzel_ranged(up, m): _root_feet(m["primary"], 3, .34, .56) ellipsoid("Body_Trunk", (0, .025, .76), (.19, .16, .29), m["primary"]) ellipsoid("Head_Seed", (0, -.025, 1.13), (.17, .15, .20), m["secondary"]) _eyes((0, -.17, 1.13), m["accent"], spread=.057, scale=.03) _leaf("Body_LeafMantle", (0, -.14, .59), .43, .40, m["accent"]) for s, suffix in ((-1, "L"), (1, "R")): shoulder = (s * .17, 0, .91) hand = (s * .29, -.21, .85) beam("Arm" + suffix, shoulder, hand, .07, m["primary"], vertices=5) _leaf("Arm" + suffix + "_Leaf", (s * .26, -.13, .81), .21, .26, m["secondary"], lean=s * .1) beam("Weapon_VineLower", (.28, -.17, .73), (.44, -.08, .93), .046, m["primary"], vertices=5) beam("Weapon_VineUpper", (.44, -.08, .93), (.40, -.17, 1.17), .04, m["primary"], vertices=5) _cup("Weapon_SeedCapsule", (.24, -.19, .88), .19 if up else .15, .20, m["accent"], axis=(0, -.7, .7), segments=8) ellipsoid("Weapon_Seed", (.24, -.30, 1.02), (.075, .075, .10), m["light"]) if up: # Overlapping tiers make a pinecone crown, not a mushroom cap. for layer in range(3): z = 1.24 + layer * .10 radius = .23 - layer * .035 cone("Head_ConeTier" + str(layer), (0, .025, z), radius, radius * .47, .16, m["primary"], vertices=7) for k in range(5): a = 2 * pi * k / 5 + layer * .3 _leaf("Head_ConeScale" + str(layer) + "_" + str(k), (radius * .75 * cos(a), .025 + radius * .75 * sin(a), z - .05), .11, .16, m["accent"]) else: _leaf("Head_LeafCrest", (0, .04, 1.22), .22, .25, m["accent"], .08) def _glut_artillery(up, m): _basalt_legs(m["primary"], True) ellipsoid("Body_Foundation", (0, .025, .48), (.39, .31, .22), m["primary"]) _cup("Weapon_Crater", (0, .02, .52), .55 if up else .43, .63 if up else .50, m["primary"], segments=12) _cup("Weapon_EmberLining", (0, .02, .66), .37 if up else .29, .24 if up else .20, m["secondary"], segments=10) for s, suffix in ((-1, "L"), (1, "R")): ellipsoid("Arm" + suffix + "_Slab", (s * .39, .0, .65), (.13, .19, .25), m["primary"]) ellipsoid("Head_Eye" + suffix, (s * .13, -.28, .53), (.055, .025, .035), m["secondary"]) for k in range(3 if up else 2): ellipsoid("Weapon_Coal" + str(k), ((k - (1 if up else .5)) * .12, .015, .80 if up else .77), (.105, .10, .10), m["accent"]) if up: for k in range(5): a = .2 + pi * k / 4 _spike("Body_CraterFin" + str(k), (.40 * cos(a), .03 + .39 * sin(a), .87), (.46 * cos(a), .03 + .43 * sin(a), 1.37), .095, m["primary"]) def _nieten_artillery(up, m): _mechanical_legs(m, True) box("Body_Chassis", (0, .035, .67), (.56 if up else .45, .37, .34), m["primary"]) for s, suffix in ((-1, "L"), (1, "R")): beam("Arm" + suffix + "_Piston", (s * .25, .1, .60), (s * .34, .17, .96), .065, m["accent"]) cylinder("Arm" + suffix + "_Collar", (s * .31, .15, .84), .095, .15, m["secondary"], vertices=8) bell_positions = (-.29, .29) if up else (0,) for i, x in enumerate(bell_positions): base = (x, .08, .92) _horn("Weapon_Resonator" + str(i), base, .27 if up else .34, .48, m["secondary"], axis=(0, -1, .3)) # A dark interior cavity and a slim central piston remain visible. beam("Weapon_Clapper" + str(i), (x, .045, .94), (x, -.26, 1.035), .038, m["accent"]) ellipsoid("Weapon_ClapperHead" + str(i), (x, -.27, 1.04), (.09, .07, .09), m["light"]) box("Head_Visor", (0, -.164, .68), (.20, .03, .075), m["light"]) for x, suffix in ((-.24, "L"), (.24, "R")): cylinder("Body_Exhaust" + suffix, (x, .25, 1.03), .075, .50 if up else .34, m["accent"], vertices=8) if up: box("Body_Brow", (0, -.10, 1.34), (.66, .14, .085), m["primary"]) cone("Head_Metronome", (0, -.03, 1.47), .10, 0, .20, m["light"], vertices=4) def _nebel_artillery(up, m): # Face forward/slightly up: the cavity remains visible to an elevated camera. _mist_robe(m, up) bell_base = (0, -.16, 1.05 if up else .85) radius, depth = (.45, .60) if up else (.38, .50) _horn("Body_CathedralBell", bell_base, radius, depth, m["light"], axis=(0, -1, .12)) beam("Weapon_ClapperStem", (0, -.27, 1.09 if up else .90), (0, -.70 if up else -.60, 1.0 if up else .80), .045, m["accent"]) ellipsoid("Weapon_Clapper", (0, -.73 if up else -.63, 1.0 if up else .80), (.13, .10, .13), m["accent"]) for s, suffix in ((-1, "L"), (1, "R")): beam("Arm" + suffix, (s * .18, .02, .96), (s * .44, -.08, .63), .07, m["secondary"], vertices=5) _spike("Arm" + suffix + "_Sickle", (s * .44, -.08, .63), (s * .49, -.19, .94), .10, m["primary"]) _mask("Head_Mask", (0, .035, 1.49 if up else 1.38), .65, m["secondary"], m["accent"]) if up: for i, x in enumerate((-.27, 0, .27)): beam("Head_SpireBase" + str(i), (x * .65, .08, 1.16), (x, .09, 1.42), .075, m["primary"], vertices=5) _spike("Head_Spire" + str(i), (x, .09, 1.40), (x * .65, -.01, 1.71 if i == 1 else 1.59), .075, m["light"]) def _mushroom(name, loc, radius, height, m): x, y, z = loc cone(name + "_Stem", (x, y, z + height * .45), .095, .07, height * .9, m["primary"], vertices=7) # An angular overhanging cap and light gill ring form a readable roof. cone(name + "_Gills", (x, y, z + height * .86), radius * .91, radius * .5, .10, m["accent"], vertices=10) cone(name + "_Cap", (x, y, z + height), radius, radius * .15, .22, m["light"], vertices=10) def _wurzel_artillery(up, m): _root_feet(m["primary"], 5, .50 if up else .43, .56) ellipsoid("Body_Moss", (0, .02, .48), (.32, .26, .18), m["secondary"]) _cup("Weapon_SporeChalice", (0, -.10, .50), .40 if up else .43, .43, m["light"], segments=12) _cup("Weapon_SporeInterior", (0, -.10, .60), .28, .18, m["secondary"], segments=10) for i, x in enumerate((-.105, .105)): ellipsoid("Weapon_Spore" + str(i), (x, -.08, .81), (.06, .06, .075), m["accent"]) for s, suffix in ((-1, "L"), (1, "R")): _leaf("Arm" + suffix + "_Leaf", (s * .30, -.05, .46), .20, .36, m["accent"], lean=s * .14) if up: _mushroom("Body_RoofL", (-.34, .12, .47), .30, .67, m) _mushroom("Body_RoofR", (.34, .18, .47), .29, .81, m) _mushroom("Body_RoofCrown", (0, .33, .54), .39, .98, m) for x, suffix in ((-.12, "L"), (.12, "R")): beam("Body_Buttress" + suffix, (x * 3, .33, .12), (x, .31, 1.21), .075, m["primary"], vertices=5) def _glut_support(up, m): _basalt_legs(m["primary"], up) ellipsoid("Body_FurnaceBack", (0, .11, .79), (.34 if up else .28, .20, .37), m["primary"]) _cup("Body_OpenHearth", (0, .015, .85), .28 if up else .23, .33, m["accent"], axis=(0, -1, 0), segments=8) ellipsoid("Body_Ember", (0, -.10, .85), (.12, .085, .15), m["secondary"]) ellipsoid("Head_Capstone", (0, .01, 1.17), (.21, .17, .15), m["light"]) _eyes((0, -.15, 1.16), m["secondary"], spread=.075) for s, suffix in ((-1, "L"), (1, "R")): beam("Arm" + suffix, (s * .26, .01, .92), (s * .33, -.33, .63), .09, m["primary"], vertices=6) _cup("Weapon_WarmingBowl", (0, -.38, .52), .36 if up else .29, .22, m["accent"], segments=10) ellipsoid("Weapon_WarmCoal", (0, -.38, .61), (.15, .12, .07), m["secondary"]) if up: # Upper-half sun rays stay behind the face, safely within the footprint. for k in range(7): a = pi * k / 6 start = (.30 * cos(a), .09, 1.12 + .30 * sin(a)) end = (.48 * cos(a), .11, 1.12 + .48 * sin(a)) _spike("Head_SunRay" + str(k), start, end, .065, m["accent"]) cone("Head_Crown", (0, .0, 1.34), .16, .23, .12, m["accent"], vertices=8) def _nieten_support(up, m): _mechanical_legs(m, up) cone("Body_Coat", (0, .02, .73), .37 if up else .24, .20, .51, m["primary"], vertices=6) box("Body_PendulumCase", (0, -.18, .79), (.18, .10, .30), m["secondary"]) beam("Body_Pendulum", (0, -.24, .89), (.045, -.24, .64), .018, m["accent"]) ellipsoid("Body_PendulumWeight", (.045, -.24, .65), (.07, .035, .07), m["light"]) ellipsoid("Head_Casing", (0, .015, 1.14), (.17, .14, .17), m["accent"]) ellipsoid("Head_Dial", (0, -.119, 1.14), (.135, .025, .135), m["light"]) beam("Head_DialHand", (0, -.15, 1.14), (.055, -.15, 1.22), .015, m["primary"], vertices=4) for s, suffix in ((-1, "L"), (1, "R")): elbow = (s * .35, .0, .91) hand = (s * .45, -.13, 1.10 if s > 0 else .89) beam("Arm" + suffix + "_Upper", (s * .2, .02, .95), elbow, .06, m["accent"]) beam("Arm" + suffix + "_Fore", elbow, hand, .047, m["secondary"]) ellipsoid("Arm" + suffix + "_Hand", hand, (.065, .065, .065), m["light"]) beam("Weapon_Baton", (.45, -.13, 1.06), (.61, -.22, 1.45), .019, m["light"], vertices=5) if up: for k, x in enumerate((-.18, 0, .18)): _spike("Head_CrownTine" + str(k), (x, .015, 1.28), (x, .015, 1.56 if k == 1 else 1.47), .06, m["secondary"]) box("Head_MetronomeBridge", (0, .015, 1.36), (.45, .12, .07), m["secondary"]) for s in (-1, 1): _leaf("Body_CoatTail" + str(s), (s * .20, .20, .33), .24, .59, m["secondary"], lean=s * .16) else: _spike("Head_Spindle", (0, .015, 1.28), (0, .015, 1.45), .055, m["secondary"]) def _nebel_support(up, m): _mist_robe(m, up) _mask("Head_MainMask", (0, -.13, 1.17), 1.0, m["light"], m["primary"]) for s, suffix in ((-1, "L"), (1, "R")): beam("Arm" + suffix + "_Lower", (s * .17, 0, .88), (s * .37, -.04, .82), .062, m["secondary"], vertices=5) _spike("Arm" + suffix + "_Sickle", (s * .37, -.04, .82), (s * .49, -.04, 1.21), .095, m["primary"]) _leaf("Body_SickleCollar" + suffix, (s * .13, .02, .88), .29, .43 if up else .29, m["secondary"], lean=s * .29) if up: positions = ((-.51, -.035, 1.46), (0, .11, 1.65), (.51, -.035, 1.46)) for i, p in enumerate(positions): _mask("Head_CompanionMask" + str(i), p, .67, m["light"], m["primary"]) for i, x in enumerate((-.12, 0, .12)): _spike("Head_Crown" + str(i), (x, -.035, 1.31), (x * 1.2, -.035, 1.53 if i == 1 else 1.45), .045, m["accent"]) else: _mask("Head_CompanionMask0", (.49, -.035, 1.44), .70, m["light"], m["primary"]) ellipsoid("Body_ChoirGem", (0, -.19, .83), (.055, .035, .09), m["accent"]) def _flower(name, loc, scale, m): for k in range(5): a = 2 * pi * k / 5 ellipsoid(name + "_Petal" + str(k), (loc[0] + cos(a) * .09 * scale, loc[1] + sin(a) * .09 * scale, loc[2]), (.085 * scale, .07 * scale, .035 * scale), m["light"]) ellipsoid(name + "_Heart", (loc[0], loc[1], loc[2] + .025 * scale), (.052 * scale, .052 * scale, .035 * scale), m["accent"]) def _wurzel_support(up, m): _root_feet(m["primary"], 5 if up else 3, .45 if up else .31, .55) # Two counter-leaning stems create an open woven body in the upgrade. if up: for s, suffix in ((-1, "L"), (1, "R")): beam("Body_WovenRoot" + suffix, (s * .20, .02, .38), (-s * .17, .015, .83), .105, m["primary"], vertices=5) beam("Body_WovenRootTop" + suffix, (-s * .17, .015, .83), (s * .11, .025, 1.16), .085, m["primary"], vertices=5) else: ellipsoid("Body_Trunk", (0, .025, .72), (.19, .16, .29), m["primary"]) ellipsoid("Head_Seed", (0, -.03, 1.12), (.15, .14, .18), m["secondary"]) _eyes((0, -.17, 1.12), m["accent"], spread=.05, scale=.026) for s, suffix in ((-1, "L"), (1, "R")): beam("Arm" + suffix, (s * .15, 0, .94), (s * .28, -.27, .69), .065, m["primary"], vertices=5) _leaf("Arm" + suffix + "_Leaf", (s * .23, -.21, .68), .21, .28, m["secondary"], lean=s * .12) _cup("Weapon_LeafBasin", (0, -.31, .62), .30 if up else .23, .20, m["accent"], segments=8) # Water uses the optional shared mint accent, with a leaf-colour fallback. # Opaque faceted geometry avoids sorting issues until the VFX integration. water = m.get("water", m["accent"]) cylinder("Weapon_Pool", (0, -.31, .739), .21 if up else .145, .025, water, vertices=10) ellipsoid("Weapon_Dew", (0, -.31, .86), (.085, .075, .13), water) _spike("Weapon_DewTip", (0, -.31, .93), (0, -.31, 1.035), .059, water) if up: for s, suffix in ((-1, "L"), (1, "R")): beam("Body_CanopyStem" + suffix, (s * .17, .11, .88), (s * .36, .14, 1.28), .055, m["primary"], vertices=5) _leaf("Head_Canopy" + suffix, (s * .12, .10, 1.17), .56, .36, m["secondary"], lean=s * .28) _flower("Head_Flower" + suffix, (s * .31, .12, 1.43), 1.0, m) _flower("Head_FlowerCrown", (0, .07, 1.53), .9, m) else: _leaf("Head_LeafCap", (0, .02, 1.23), .33, .25, m["accent"], -.06) _RANGED = {"glutpakt": _glut_ranged, "nietenkonvent": _nieten_ranged, "nebelchor": _nebel_ranged, "wurzelbund": _wurzel_ranged} _ARTILLERY = {"glutpakt": _glut_artillery, "nietenkonvent": _nieten_artillery, "nebelchor": _nebel_artillery, "wurzelbund": _wurzel_artillery} _SUPPORT = {"glutpakt": _glut_support, "nietenkonvent": _nieten_support, "nebelchor": _nebel_support, "wurzelbund": _wurzel_support} def build_ranged(spec, mat): """Build IDs *05 (base) and *06 (upgraded).""" _RANGED[spec["faction"]](spec["upgraded"], mat) def build_artillery(spec, mat): """Build IDs *07 (base) and *08 (upgraded).""" _ARTILLERY[spec["faction"]](spec["upgraded"], mat) def build_support(spec, mat): """Build IDs *09 (base) and *10 (upgraded).""" _SUPPORT[spec["faction"]](spec["upgraded"], 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(' 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()