v0.53.0: Kaleidostrobe, the 2002 iTunes visualizer, as the fourth style
Reverse-engineered from its PowerPC binary and checked against the original running under emulation: textures byte for byte and every triangle, over 3,600 frames. 25 fps like the plugin; its colour maps and wave images are extracted from its own code; tests/kaleidostrobe_golden.npz was recorded from the original.
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
"""Run the emulated original and the Python port side by side."""
|
||||
import math, sys, random
|
||||
sys.path.insert(0, __file__.rsplit('/', 1)[0])
|
||||
sys.path.insert(0, str(__import__('pathlib').Path(__file__).resolve().parents[2]))
|
||||
import numpy as np
|
||||
from plugin import Plugin
|
||||
from lintunes.kaleidostrobe.engine import Kaleidostrobe
|
||||
|
||||
def rotz(a):
|
||||
r = math.radians(a); c, s = math.cos(r), math.sin(r)
|
||||
m = np.eye(4); m[0, 0] = c; m[0, 1] = -s; m[1, 0] = s; m[1, 1] = c
|
||||
return m
|
||||
|
||||
def trans(x, y, z):
|
||||
m = np.eye(4); m[:3, 3] = (x, y, z); return m
|
||||
|
||||
def gl_triangles(calls):
|
||||
"""(texture, alpha, [(st×3, eye-xyz×3)]) per glBegin batch, plus textures."""
|
||||
stack = [np.eye(4)]; tex = None; alpha = 1.0; st = (0, 0)
|
||||
out = []; cur = None; mode = None
|
||||
for name, a in calls:
|
||||
M = stack[-1]
|
||||
if name == '_glLoadIdentity': stack[-1] = np.eye(4)
|
||||
elif name == '_gluLookAt': stack[-1] = stack[-1] @ trans(0, 0, -15)
|
||||
elif name == '_glPushMatrix': stack.append(stack[-1].copy())
|
||||
elif name == '_glPopMatrix': stack.pop()
|
||||
elif name == '_glRotatef': stack[-1] = stack[-1] @ rotz(a[0])
|
||||
elif name == '_glTranslatef': stack[-1] = stack[-1] @ trans(*a[:3])
|
||||
elif name == '_glBindTexture': tex = a[1]
|
||||
elif name == '_glColor4f': alpha = a[3]
|
||||
elif name == '_glBegin': cur = []; mode = a[0]
|
||||
elif name == '_glTexCoord2f': st = a[:2]
|
||||
elif name in ('_glVertex2f', '_glVertex3f'):
|
||||
v = (a[0], a[1], a[2] if name == '_glVertex3f' else 0.0, 1.0)
|
||||
cur.append((st, (M @ np.array(v))[:3]))
|
||||
elif name == '_glEnd':
|
||||
tris = []
|
||||
if mode == 4:
|
||||
tris = [cur[i:i + 3] for i in range(0, len(cur), 3)]
|
||||
elif mode == 6:
|
||||
tris = [[cur[0], cur[i], cur[i + 1]] for i in range(1, len(cur) - 1)]
|
||||
for t in tris:
|
||||
out.append((tex, alpha, t))
|
||||
return out
|
||||
|
||||
def engine_triangles(frame):
|
||||
out = []
|
||||
for layer in frame.layers:
|
||||
base = trans(0, 0, -15) @ trans(0, 0, layer.lift) @ rotz(layer.turn)
|
||||
for t in layer.triangles:
|
||||
M = base @ rotz(t.turn)
|
||||
out.append((layer.texture + 1, layer.alpha,
|
||||
[(t.st[i], (M @ np.array((*t.xyz[i], 1.0)))[:3]) for i in range(3)]))
|
||||
return out
|
||||
|
||||
def spectra(frame, rnd):
|
||||
"""Fake iTunes spectra: a kick every ~13 frames, noise elsewhere."""
|
||||
kick = (frame % 13) < 2
|
||||
row = [rnd.randrange(0, 40) for _ in range(512)]
|
||||
for b in range(4):
|
||||
row[b] = rnd.randrange(180, 256) if kick else rnd.randrange(20, 120)
|
||||
for b in range(4, 32):
|
||||
row[b] = rnd.randrange(60, 200)
|
||||
return [row, row[:]]
|
||||
|
||||
def main(frames=200, seed_ticks=1000, w=640, h=480):
|
||||
p = Plugin(rect=(0, 0, h, w), ticks=seed_ticks)
|
||||
e = Kaleidostrobe(seed=seed_ticks, ticks=seed_ticks)
|
||||
e.set_size(w, h)
|
||||
rnd = random.Random(7)
|
||||
tex_bad = geo_bad = 0
|
||||
t0 = p.k.micro
|
||||
for f in range(frames):
|
||||
spec = spectra(f, rnd)
|
||||
start = len(p.k.gl)
|
||||
p.k.micro += 40001
|
||||
ticks = seed_ticks + (p.k.micro - t0) * 60 // 1_000_000
|
||||
calls = p.frame(None, spec, dt_us=0, dticks=ticks - p.k.ticks)
|
||||
ours = e.step(np.array(spec, np.uint8), ticks)
|
||||
# textures: the two glTexImage2D uploads of this frame
|
||||
idx = [i for i in range(start, len(p.k.gl)) if p.k.gl[i][0] == '_glTexImage2D']
|
||||
theirs = [np.frombuffer(p.k.textures[i], np.uint8).reshape(128, 128, 4) for i in idx]
|
||||
for t in range(2):
|
||||
d = int((theirs[t] != ours.textures[t]).any(-1).sum())
|
||||
if d:
|
||||
tex_bad += 1
|
||||
if tex_bad <= 5:
|
||||
print(f'frame {f} tex {t}: {d} texels differ')
|
||||
a, b = gl_triangles(calls), engine_triangles(ours)
|
||||
ok = len(a) == len(b) and all(
|
||||
x[0] == y[0] and abs(x[1] - y[1]) < 1e-6 and
|
||||
all(np.allclose(x[2][i][0], y[2][i][0], atol=1e-4) and
|
||||
np.allclose(x[2][i][1], y[2][i][1], atol=1e-3) for i in range(3))
|
||||
for x, y in zip(a, b))
|
||||
if not ok:
|
||||
geo_bad += 1
|
||||
if geo_bad <= 3:
|
||||
print(f'frame {f}: geometry differs ({len(a)} vs {len(b)} triangles)')
|
||||
for x, y in list(zip(a, b))[:2]:
|
||||
print(' orig', x); print(' port', y)
|
||||
if f % 50 == 0:
|
||||
print(f'frame {f}: beat={ours.beat} beats={e.beat_count} objects={e.bottom_object},{e.top_object} '
|
||||
f'map={e.cur_map} wave={e.cur_wave} delta={e.cur_delta}')
|
||||
print(f'{frames} frames: texture mismatches {tex_bad}, geometry mismatches {geo_bad}')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(int(sys.argv[1]) if len(sys.argv) > 1 else 200)
|
||||
Reference in New Issue
Block a user