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>
206 lines
10 KiB
Python
206 lines
10 KiB
Python
"""Nebelchor: flowing ghost ribbons and mask/bell silhouettes from Phase 3."""
|
|
from math import sin, cos, pi
|
|
from common import ellipsoid, ring, beam
|
|
from reference_geometry import r_plate, r_rock, r_sweep, r_lathe, r_arch_shell
|
|
|
|
|
|
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)
|