nicer floor explainer: terrain overview with grid and polygon numbers, zoom on both crate needles with the accepted slivers, side view
This commit is contained in:
1 parent
8baed2e87c
commit
74f8fec341
6 files changed
+203
-130
No files matched your search
@@ -0,0 +1,25 @@
|
||||
import sys,struct,numpy as np; sys.path.insert(0,'.'); from floor import *
|
||||
t=443; x,y,z,rot,mi,_=mp['A'][t]; recs=[r for g in trak[mi].coll for r in g]
|
||||
def planes(r):
|
||||
return [struct.unpack_from('<bbbBh',r,2+6*k) for k in range(5)]
|
||||
def corners(r):
|
||||
P=planes(r); ne=4 if r[0]&1 else 3; E=P[1:1+ne]; S=P[0]; pts=[]
|
||||
for i in range(ne):
|
||||
a,b=E[i],E[(i+1)%ne]
|
||||
A=np.array([S[:3],a[:3],b[:3]],float); B=np.array([S[4],a[4],b[4]],float)/32
|
||||
pts.append(np.linalg.solve(A,B)) # n127*32*p = d -> n127.p = d/32 (p in units)...
|
||||
return pts
|
||||
# local (engine) -> world: local=(lz_w', -lx_w') for rot 3072 => dx=-lz, dz=lx
|
||||
a=-rot/4096*2*math.pi
|
||||
def toworld(p):
|
||||
lx,ly,lz=p
|
||||
# invert lx=c*dx+s*dz, lz=c*dz-s*dx with s=sin(a'),c=cos(a'), a'=(-rot)*2pi/4096
|
||||
s,c=math.sin(a),math.cos(a)
|
||||
dx=c*lx-s*lz; dz=s*lx+c*lz
|
||||
return (x+dx,y+ly,-z+dz)
|
||||
for i in [int(v) for v in sys.argv[1:]]:
|
||||
r=recs[i]; P=planes(r)
|
||||
print('poly',i,'type',r[0],'surface n',[v/127 for v in P[0][:3]])
|
||||
for p in corners(r):
|
||||
|
||||
print(' local',np.round(p,4),' world',np.round(toworld(p),4))
|
||||
@@ -0,0 +1,137 @@
|
||||
# Usage: python3 tools/floorre/explain_t1l4m002.py tools/floorre OUT.png (WAD via env WAD, default t1l4m002 path in floor.py; needs Pillow, numpy)
|
||||
# Explainer: Kuzco floating next to the crate in t1l4m002 (builds on floorre/plot.py)
|
||||
import sys, math
|
||||
sys.path.insert(0, sys.argv[1]) # floorre dir (floor.py, corners.py)
|
||||
OUT = sys.argv[2]
|
||||
sys.argv = sys.argv[:1]
|
||||
from floor import * # mp, trak, floor_test
|
||||
from corners import corners
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
SS = 2 # supersampling
|
||||
F = lambda n, b=False: ImageFont.truetype('DejaVuSans-Bold.ttf' if b else 'DejaVuSans.ttf', n * SS)
|
||||
K = (57.204, 2.474, 43.733)
|
||||
T = 443
|
||||
TX, TY, TZ, ROT, MI, _ = mp['A'][T]
|
||||
recs = [r for g in trak[MI].coll for r in g]
|
||||
|
||||
def xf(B):
|
||||
rot = B[1]; tx, ty, tz = B[3] / 4096, B[4] / 4096, B[5] / 4096
|
||||
a = -rot / 4096 * 2 * math.pi; s, c = math.sin(a), math.cos(a)
|
||||
return lambda p: (tx + c * p[0] - s * p[2], ty + p[1], tz + s * p[0] + c * p[2])
|
||||
|
||||
RED, BLUE, MAG, PINK = (215, 25, 25), (20, 90, 230), (210, 0, 210), (255, 70, 150)
|
||||
|
||||
def panel(X0, Z0, span, size, detail):
|
||||
S = size / span
|
||||
img = Image.new('RGB', (size * SS, size * SS), 'white'); d = ImageDraw.Draw(img, 'RGBA')
|
||||
P = lambda x, z: ((x - X0) * S * SS, (size - (z - Z0) * S) * SS)
|
||||
# rendered terrain coloured by height (like the agent's plot)
|
||||
for t in range(mp['N']):
|
||||
B = mp['B'][t]
|
||||
if not (X0 - 2 <= B[3] / 4096 <= X0 + span + 2 and Z0 - 2 <= B[5] / 4096 <= Z0 + span + 2): continue
|
||||
m = trak[mp['midx'][t]]; f = xf(B)
|
||||
for fc in sorted(m.faces, key=lambda fc: sum(m.verts[v][1] for v in fc['v'])):
|
||||
Q = [f((m.verts[v][0], m.verts[v][1], -m.verts[v][2])) for v in fc['v']]
|
||||
h = sum(p[1] for p in Q) / len(Q); g = int(max(0, min(255, (h - 1.5) / 2 * 255)))
|
||||
d.polygon([P(p[0], p[2]) for p in Q], fill=(g, 200 - g // 2, 255 - g, 255),
|
||||
outline=(130, 130, 130) if not detail else None)
|
||||
# cell grid
|
||||
for k in range(int(X0) - 1, int(X0 + span) + 2):
|
||||
d.line([P(k, Z0 - 1), P(k, Z0 + span + 1)], fill=(0, 140, 0), width=SS * (2 if detail else 1))
|
||||
for k in range(int(Z0) - 1, int(Z0 + span) + 2):
|
||||
d.line([P(X0 - 1, k), P(X0 + span + 1, k)], fill=(0, 140, 0), width=SS * (2 if detail else 1))
|
||||
return img, d, P, S
|
||||
|
||||
def collision(d, P, labels, sliver=None, fsz=12):
|
||||
f = xf(mp['B'][T])
|
||||
if sliver:
|
||||
for (x, z, w, i) in sliver:
|
||||
a, b = P(x, z), P(x + w, z - w)
|
||||
d.rectangle([a[0], a[1], b[0], b[1]], fill=PINK + (170,))
|
||||
for i, r in enumerate(recs):
|
||||
C = [f(p) for p in corners(r)]
|
||||
col = RED if i in (11, 15) else BLUE if i == 2 else (20, 20, 20)
|
||||
wid = 3 if i in (2, 11, 15) else 1
|
||||
pts = [P(p[0], p[2]) for p in C]
|
||||
if i in (11, 15): d.polygon(pts, fill=RED + (70,))
|
||||
d.line(pts + pts[:1], fill=col, width=wid * SS)
|
||||
if labels and (labels == 'all' or i in labels):
|
||||
cx = sum(p[0] for p in pts) / len(pts); cy = sum(p[1] for p in pts) / len(pts)
|
||||
txt = str(i) if labels == 'all' else f'poly {i} · mat {r[1]}'
|
||||
d.text((cx, cy), txt, fill=col, font=F(fsz, i in (2, 11, 15)), anchor='mm',
|
||||
stroke_width=SS * 2, stroke_fill='white')
|
||||
|
||||
def sliver_cells(x0, z0, x1, z1, step):
|
||||
out = []; tx, ty, tz = int(round(TX * 4096)), int(round(TY * 4096)), int(round(-TZ * 4096))
|
||||
z = z0
|
||||
while z < z1:
|
||||
x = x0
|
||||
while x < x1:
|
||||
if int(x) == 57 and int(z) == 43:
|
||||
_, res = floor_test(int(round(x * 4096)), int(round(2.27 * 4096)), int(round(z * 4096)), tx, ty, tz, ROT, recs)
|
||||
best = None
|
||||
for i, i7, ok, vals, tol, Y in res:
|
||||
if ok and i7 >= -0x80 and (best is None or i7 < best[0]): best = (i7, i)
|
||||
if best and best[1] in (11, 15): out.append((x, z, step, best[1]))
|
||||
x += step
|
||||
z += step
|
||||
return out
|
||||
|
||||
W = 1500
|
||||
# --- left: overview (3 x 3 cells) ---
|
||||
ov, d, P, S = panel(56.0, 42.0, 3.0, 640, False)
|
||||
collision(d, P, 'all', fsz=11)
|
||||
for t in range(mp['N']):
|
||||
B = mp['B'][t]; x, z = B[3] / 4096, B[5] / 4096
|
||||
if 56 <= x <= 59 and 42 <= z <= 45:
|
||||
d.text(P(x - 0.45, z - 0.38), f'tile {t}', fill=(110, 0, 140), font=F(12, True), stroke_width=SS * 2, stroke_fill='white')
|
||||
k = P(K[0], K[2]); d.ellipse([k[0] - 7 * SS, k[1] - 7 * SS, k[0] + 7 * SS, k[1] + 7 * SS], fill=MAG, outline='black', width=SS)
|
||||
ZX0, ZZ0, ZSP = 57.06, 43.16, 0.76
|
||||
a, b = P(ZX0, ZZ0 + ZSP), P(ZX0 + ZSP, ZZ0)
|
||||
d.rectangle([a, b], outline=(255, 140, 0), width=3 * SS)
|
||||
ov = ov.resize((640, 640), Image.LANCZOS)
|
||||
|
||||
# --- right: zoom on the crate's side with both needles ---
|
||||
zm, d, P, S = panel(ZX0, ZZ0, ZSP, 820, True)
|
||||
collision(d, P, [2, 11, 15], sliver=sliver_cells(ZX0, ZZ0, ZX0 + ZSP, ZZ0 + ZSP, 0.0012), fsz=15)
|
||||
k = P(K[0], K[2]); r = 9 * SS
|
||||
d.ellipse([k[0] - r, k[1] - r, k[0] + r, k[1] + r], fill=MAG, outline='black', width=2 * SS)
|
||||
d.text((k[0], k[1] + 30 * SS), 'Kuzco\n57.204 / 43.733\n(measured live)', fill=MAG, font=F(15, True), anchor='ma',
|
||||
align='center', stroke_width=SS * 2, stroke_fill='white')
|
||||
tip = P(57.315, 43.729)
|
||||
y = tip[1] - 34 * SS
|
||||
d.line([(k[0], y), (tip[0], y)], fill='black', width=2 * SS)
|
||||
for xq in (k[0], tip[0]): d.line([(xq, y - 8 * SS), (xq, y + 8 * SS)], fill='black', width=2 * SS)
|
||||
d.text(((k[0] + tip[0]) / 2, y - 6 * SS), '0.11 outside poly 15', fill='black', font=F(14, True), anchor='md',
|
||||
stroke_width=SS * 2, stroke_fill='white')
|
||||
d.text((P(57.38, 43.80)), 'needle tips 13.5°', fill=RED, font=F(14, True), stroke_width=SS * 2, stroke_fill='white')
|
||||
d.text((P(ZX0 + 0.01, ZZ0 + 0.035)), 'pink = where the engine still takes the needle as floor\n(edge tolerance ≈ 0.02 → sliver ≈ 0.19 long, 0.01 wide)',
|
||||
fill=(190, 20, 110), font=F(14, True), stroke_width=SS * 2, stroke_fill='white')
|
||||
zm = zm.resize((820, 820), Image.LANCZOS)
|
||||
|
||||
# --- bottom: side view ---
|
||||
sv = Image.new('RGB', (W * SS, 300 * SS), 'white'); d = ImageDraw.Draw(sv)
|
||||
H = lambda v: (270 - (v - 2.2) * 600) * SS
|
||||
x0, x1 = 80 * SS, 900 * SS
|
||||
d.line([(x0, H(2.270)), (x1, H(2.270))], fill=BLUE, width=6 * SS)
|
||||
for i in range(x0, x1, 28 * SS): d.line([(i, H(2.474)), (i + 14 * SS, H(2.474))], fill=RED, width=5 * SS)
|
||||
kx = 300 * SS
|
||||
d.rectangle([kx - 16 * SS, H(2.474 + 0.12), kx + 16 * SS, H(2.474)], outline=MAG, width=3 * SS)
|
||||
d.text((kx + 24 * SS, H(2.474 + 0.06)), "Kuzco's feet", fill=MAG, font=F(15, True), anchor='lm')
|
||||
d.line([(560 * SS, H(2.474)), (560 * SS, H(2.270))], fill='black', width=2 * SS)
|
||||
d.text((572 * SS, (H(2.474) + H(2.270)) / 2), '0.20 in the air', fill='black', font=F(17, True), anchor='lm')
|
||||
d.text((x1 + 20 * SS, H(2.474)), 'poly 15 plane extended to Kuzco: y 2.474 ← chosen', fill=RED, font=F(16, True), anchor='lm')
|
||||
d.text((x1 + 20 * SS, H(2.270)), 'poly 2 (path) really under Kuzco: y 2.270', fill=BLUE, font=F(16, True), anchor='lm')
|
||||
d.text((20 * SS, 14 * SS), 'Side view at Kuzco\'s position: the game takes the highest passing floor up to feet + 0.25', fill='black', font=F(20, True))
|
||||
sv = sv.resize((W, 300), Image.LANCZOS)
|
||||
|
||||
out = Image.new('RGB', (W, 110 + 820 + 300 + 20), 'white')
|
||||
d = ImageDraw.Draw(out)
|
||||
d.text((20, 14), 'Why Kuzco stands in the air next to the crate · Jungle Day 2 (t1l4m002), tile 443', fill='black', font=ImageFont.truetype('DejaVuSans-Bold.ttf', 26))
|
||||
d.text((20, 54), 'Top view, colours = terrain height, green lines = grid cells, numbers = collision polygons of tile 443 (all material 0). '
|
||||
'Red = the two needle triangles 11 and 15 (steep crate bevels),\nblue = poly 2 (the path). Units = grid cells; Kuzco is about 0.6 tall.',
|
||||
fill=(60, 60, 60), font=ImageFont.truetype('DejaVuSans.ttf', 15))
|
||||
out.paste(ov, (20, 110 + 90)); out.paste(zm, (680, 110))
|
||||
d.text((20, 110 + 60), 'Overview (3 × 3 cells), orange box = zoom →', fill='black', font=ImageFont.truetype('DejaVuSans-Bold.ttf', 17))
|
||||
out.paste(sv, (0, 110 + 820 + 20))
|
||||
out.save(OUT); print('saved', OUT, out.size)
|
||||
@@ -0,0 +1,40 @@
|
||||
import sys,struct,math; sys.path.insert(0,'/home/elias/projects/llamamod/tools'); import geomlib as G
|
||||
ch,wf,trak,mp=G.load_level(__import__('os').environ.get('WAD','/home/elias/.claude/jobs/90c6eed7/tmp/exitre/t1l4m002.wad'))
|
||||
_exe=open('/home/elias/projects/llamamod/re/groove.exe','rb').read()
|
||||
SIN=list(struct.unpack_from('<4096i',_exe,0x174318))
|
||||
def c8(b): return b-256 if b>127 else b
|
||||
def floor_test(px,py,pz,tx,ty,tz,rot,recs,dbg=False,probe_add=0x400):
|
||||
"""exact re-implementation of FUN_004036d0 floor branch (flags 0x200000 off). ints 20.12"""
|
||||
lx=px-tx; lz=pz-tz; ly=py-ty+probe_add
|
||||
u=(-rot)&0xffffffff
|
||||
if u:
|
||||
s=SIN[u&0xfff]; c=SIN[(u+0x400)&0xfff]
|
||||
lx,lz=(s*lz+c*lx)>>12,(c*lz-s*lx)>>12
|
||||
out=[]
|
||||
for i,r in enumerate(recs):
|
||||
if r[1]==0x11: continue
|
||||
ny=c8(r[3]); n14=ny*32
|
||||
if n14<=0: continue
|
||||
h=(((struct.unpack_from('<h',r,6)[0]*4096 - c8(r[4])*32*lz - c8(r[2])*32*lx) - n14*ly))
|
||||
q=abs(h)//abs(n14); i6= q if (h>=0)==(n14>=0) else -q # C truncating division
|
||||
i7=-i6
|
||||
Y=ly+i6
|
||||
tol=-0x40-((ny*-0x20+0x1000)>>6)
|
||||
ne=4 if r[0]&1 else 3
|
||||
vals=[]
|
||||
ok=True
|
||||
for k in range(ne):
|
||||
nx,nyy,nz,_,d=struct.unpack_from('<bbbBh',r,8+6*k)
|
||||
v=((nyy*32*Y+nz*32*lz+nx*32*lx)>>12)-d
|
||||
vals.append(v)
|
||||
if v<tol: ok=False
|
||||
out.append((i,i7,ok,vals,tol,ty+Y))
|
||||
return (lx,ly,lz),out
|
||||
if __name__=='__main__':
|
||||
t=443; x,y,z,rot,mi,_=mp['A'][t]
|
||||
recs=[r for g in trak[mi].coll for r in g]
|
||||
P=[float(a) for a in sys.argv[1:4]]
|
||||
loc,out=floor_test(*[int(round(v*4096)) for v in P],int(round(x*4096)),int(round(y*4096)),int(round(-z*4096)),rot,recs)
|
||||
print('local',[v/4096 for v in loc])
|
||||
for i,i7,ok,vals,tol,Y in out:
|
||||
if i7>=-0x80: print(f'poly {i:2d} dist {i7/4096:+.4f} planeY {Y/4096:.4f} pass={ok} edges {vals} tol {tol}')
|
||||
Reference in new issue
Block a user