saumwacht-godot/assets-source/phase4/blender/reference_geometry.py
Martin Stickelbroeck c2b447610c Baseline: Saumwacht 0.15.0 (Phase 20A) vor dem Umbau zu Champions of Luneraria
Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern
unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore
unversioniert; letztere sind unter ../backups gesichert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:00:36 +02:00

168 lines
6.3 KiB
Python

"""Editable low-poly shapes used for direct Phase-3 icon matching.
All dimensions are metres, Blender Z-up and -Y forward. Geometry stays solid,
faceted and separated by meaningful part names; there are no image billboards.
"""
from math import sin, cos, pi
from mathutils import Vector
from common import mesh_object, ellipsoid, cylinder, beam, ring
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)