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>
168 lines
6.3 KiB
Python
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)
|