Shaded frame: Gouraud raster of the i860's projected geometry
Captures the object cap7's death-camera view draws (4 VSTRIP strips, 45 verts) with its per-vertex normals straight off the emulated i860, and shades it in software with a barycentric z-buffered fill (shade_render.py). This is our rasteriser showing the firmware's geometry lit by the firmware's own normals -- not the board's bit-serial Pixel-Planes array (that stays the Tier-1 build). cap7-geometry.json is the portable capture; render-readout.html is the published readout with the shaded frame as its hero. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
"""Shaded software rasteriser for geometry captured off the emulated i860.
|
||||
|
||||
Input: a JSON file of triangle strips, each vertex = [screenX, screenY, nx, ny, nz, modelZ]
|
||||
(as produced by capture_wireframe.py / vfull dumps). Screen xy and the per-vertex
|
||||
normal come straight out of the firmware's own VSTRIP transform; modelZ is the depth.
|
||||
|
||||
Output: a Gouraud-shaded PPM plus an ASCII preview. This is OUR rasteriser (a plain
|
||||
barycentric fill with a z-buffer) — it is NOT the board's bit-serial Pixel-Planes
|
||||
array. It exists to *show* the geometry the firmware projected, lit by the normals
|
||||
the firmware computed, without needing the full IGC micro-code simulator.
|
||||
|
||||
Usage: python shade_render.py geometry.json [out.ppm]
|
||||
"""
|
||||
import sys, json, math
|
||||
|
||||
# The object recovered from cap7's death-camera frame (4 strips, 45 verts), embedded so
|
||||
# the tool runs with no external capture. Regenerate by dumping vfull.pkl to this format.
|
||||
DEFAULT_GEO = [
|
||||
[[237.5,62,0.931,0.262,0.131,-8],[235.2,46.5,0.922,0.187,0.094,-6],[238,31,0.933,0.278,0.139,-8],[236.6,18.6,0.928,0.231,0.116,-6]],
|
||||
[[240.9,43.4,0.945,0.372,0.186,-8],[238.9,6.2,0.937,0.307,0.154,-6],[237.5,31,0.931,0.26,0.13,-8],[238,9.3,0.933,0.278,0.139,-6],[237.1,62,0.93,0.249,0.124,-8],[242.2,21.7,0.95,0.415,0.208,-6],[238.4,37.2,0.935,0.291,0.146,-8],[242.3,15.5,0.95,0.417,0.209,-6],[237.6,31,0.932,0.263,0.431,-8],[246.5,0,0.966,0.554,0.377,-6],[242.8,31,0.952,0.943,0.517,-8],[246.9,12.4,0.968,0.957,0.685,-6],[241.7,40.3,0.948,0.98,0.65,-8],[236.3,0,0.927,0.936,0.678,-6],[253.7,62,0.995,0.246,0.123,-4],[252.1,43.4,0.988,0.324,0.162,-4]],
|
||||
[[245.1,15.5,0.961,0.384,0.192,-4],[239.9,40.3,0.941,0.341,0.171,-4],[238.5,93,0.935,0.446,0.223,-4],[234.4,62,0.919,0.643,0.216,-4],[239.1,0,0.938,0.645,0.424,-4],[231.9,0,0.909,0.951,0.539,-4],[240.7,0,0.944,0.984,0.685,-4],[233.5,40.3,0.916,0.263,0.132,-2],[254.6,24.8,0.998,0.415,0.208,-2],[243.2,0,0.954,0.461,0.231,-2],[233.8,12.4,0.917,0.431,0.215,-2],[243.2,15.5,0.954,0.461,0.231,-2],[254.3,0,0.997,0.614,0.207,-2],[250.4,9.3,0.982,0.64,0.501,-2],[249.6,12.4,0.979,0.94,0.58,-2],[245.7,0,0.964,0.939,0.723,-2]],
|
||||
[[126.1,62,0.494,0.914,0.206,0],[238.8,49.6,0.937,0.447,0.223,0],[239.1,62,0.938,0.448,0.224,0],[250.4,40.3,0.982,0.402,0.201,0],[234.1,31,0.918,0.432,0.216,0],[250.4,43.4,0.982,0.402,0.201,0],[231.3,46.5,0.907,0.639,0.37,0],[254.9,37.2,0.999,0.916,0.508,0],[252.7,34.1,0.991,0.926,0.663,0]],
|
||||
]
|
||||
|
||||
LIGHT = (0.35, -0.55, 0.75) # upper-front key light
|
||||
SCALE = 7 # px per screen unit
|
||||
PAD = 6 # border in screen units
|
||||
|
||||
|
||||
def _n(a, b, c):
|
||||
m = math.sqrt(a * a + b * b + c * c) or 1.0
|
||||
return a / m, b / m, c / m
|
||||
|
||||
|
||||
def intensity(v):
|
||||
n = _n(v[2], v[3], v[4])
|
||||
L = _n(*LIGHT)
|
||||
d = n[0] * L[0] + n[1] * L[1] + n[2] * L[2]
|
||||
return max(0.18, min(1.0, 0.22 + 0.85 * abs(d))) # two-sided (abs) + ambient
|
||||
|
||||
|
||||
def render(geo):
|
||||
tris = []
|
||||
for strip in geo:
|
||||
for i in range(len(strip) - 2):
|
||||
tris.append((strip[i], strip[i + 1], strip[i + 2]))
|
||||
allv = [v for s in geo for v in s]
|
||||
body = [v for v in allv if v[0] > 200] or allv # frame on body, ignore far outlier
|
||||
mnx = min(v[0] for v in body); mxx = max(v[0] for v in body)
|
||||
mny = min(v[1] for v in body); mxy = max(v[1] for v in body)
|
||||
W = int(mxx - mnx) + 2 * PAD; H = int(mxy - mny) + 2 * PAD
|
||||
IW, IH = W * SCALE, H * SCALE
|
||||
fb = [[0.0] * IW for _ in range(IH)]
|
||||
zb = [[-1e9] * IW for _ in range(IH)]
|
||||
|
||||
def SX(v): return (v[0] - mnx + PAD) * SCALE
|
||||
def SY(v): return (v[1] - mny + PAD) * SCALE
|
||||
|
||||
for a, b, c in tris:
|
||||
ax, ay, bx, by, cx, cy = SX(a), SY(a), SX(b), SY(b), SX(c), SY(c)
|
||||
ia, ib, ic = intensity(a), intensity(b), intensity(c)
|
||||
za, zbv, zc = a[5], b[5], c[5]
|
||||
area = (bx - ax) * (cy - ay) - (cx - ax) * (by - ay)
|
||||
if abs(area) < 1e-6:
|
||||
continue
|
||||
x0 = max(0, int(min(ax, bx, cx))); x1 = min(IW - 1, int(max(ax, bx, cx)))
|
||||
y0 = max(0, int(min(ay, by, cy))); y1 = min(IH - 1, int(max(ay, by, cy)))
|
||||
for py in range(y0, y1 + 1):
|
||||
for px in range(x0, x1 + 1):
|
||||
w0 = ((bx - px) * (cy - py) - (cx - px) * (by - py)) / area
|
||||
w1 = ((cx - px) * (ay - py) - (ax - px) * (cy - py)) / area
|
||||
w2 = 1 - w0 - w1
|
||||
if w0 < -0.002 or w1 < -0.002 or w2 < -0.002:
|
||||
continue
|
||||
z = w0 * za + w1 * zbv + w2 * zc
|
||||
if z > zb[py][px]:
|
||||
zb[py][px] = z
|
||||
fb[py][px] = w0 * ia + w1 * ib + w2 * ic
|
||||
return fb, IW, IH
|
||||
|
||||
|
||||
def write_ppm(fb, IW, IH, path):
|
||||
img = bytearray(IW * IH * 3)
|
||||
for y in range(IH):
|
||||
for x in range(IW):
|
||||
v = fb[y][x]
|
||||
if v > 0:
|
||||
r = int(min(255, 28 + v * 168)); g = int(min(255, 50 + v * 205)); b = int(min(255, 54 + v * 122))
|
||||
else:
|
||||
r, g, b = 7, 11, 17
|
||||
o = (y * IW + x) * 3
|
||||
img[o], img[o + 1], img[o + 2] = r, g, b
|
||||
with open(path, 'wb') as f:
|
||||
f.write(b'P6\n%d %d\n255\n' % (IW, IH))
|
||||
f.write(bytes(img))
|
||||
|
||||
|
||||
def ascii_preview(fb, IW, IH):
|
||||
ramp = " .:-=+*#%@"
|
||||
out = []
|
||||
for ry in range(0, IH, SCALE * 2):
|
||||
line = " "
|
||||
for rx in range(0, IW, SCALE):
|
||||
v = fb[ry][rx]
|
||||
line += ramp[min(9, int(v * 9.99))] if v > 0 else ' '
|
||||
out.append(line)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def main():
|
||||
geo = DEFAULT_GEO
|
||||
if len(sys.argv) > 1 and not sys.argv[1].endswith('.ppm'):
|
||||
geo = json.load(open(sys.argv[1]))
|
||||
out = next((a for a in sys.argv[1:] if a.endswith('.ppm')), 'shade.ppm')
|
||||
fb, IW, IH = render(geo)
|
||||
print(ascii_preview(fb, IW, IH))
|
||||
write_ppm(fb, IW, IH, out)
|
||||
print("\nwrote %s (%dx%d)" % (out, IW, IH))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user