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 @@
|
||||
[[[237.5,62.0,0.931,0.262,0.131,-8.0],[235.2,46.5,0.922,0.187,0.094,-6.0],[238.0,31.0,0.933,0.278,0.139,-8.0],[236.6,18.6,0.928,0.231,0.116,-6.0]],[[240.9,43.4,0.945,0.372,0.186,-8.0],[238.9,6.2,0.937,0.307,0.154,-6.0],[237.5,31.0,0.931,0.26,0.13,-8.0],[238.0,9.3,0.933,0.278,0.139,-6.0],[237.1,62.0,0.93,0.249,0.124,-8.0],[242.2,21.7,0.95,0.415,0.208,-6.0],[238.4,37.2,0.935,0.291,0.146,-8.0],[242.3,15.5,0.95,0.417,0.209,-6.0],[237.6,31.0,0.932,0.263,0.431,-8.0],[246.5,0.0,0.966,0.554,0.377,-6.0],[242.8,31.0,0.952,0.943,0.517,-8.0],[246.9,12.4,0.968,0.957,0.685,-6.0],[241.7,40.3,0.948,0.98,0.65,-8.0],[236.3,0.0,0.927,0.936,0.678,-6.0],[253.7,62.0,0.995,0.246,0.123,-4.0],[252.1,43.4,0.988,0.324,0.162,-4.0]],[[245.1,15.5,0.961,0.384,0.192,-4.0],[239.9,40.3,0.941,0.341,0.171,-4.0],[238.5,93.0,0.935,0.446,0.223,-4.0],[234.4,62.0,0.919,0.643,0.216,-4.0],[239.1,0.0,0.938,0.645,0.424,-4.0],[231.9,0.0,0.909,0.951,0.539,-4.0],[240.7,0.0,0.944,0.984,0.685,-4.0],[233.5,40.3,0.916,0.263,0.132,-2.0],[254.6,24.8,0.998,0.415,0.208,-2.0],[243.2,0.0,0.954,0.461,0.231,-2.0],[233.8,12.4,0.917,0.431,0.215,-2.0],[243.2,15.5,0.954,0.461,0.231,-2.0],[254.3,0.0,0.997,0.614,0.207,-2.0],[250.4,9.3,0.982,0.64,0.501,-2.0],[249.6,12.4,0.979,0.94,0.58,-2.0],[245.7,0.0,0.964,0.939,0.723,-2.0]],[[126.1,62.0,0.494,0.914,0.206,0.0],[238.8,49.6,0.937,0.447,0.223,0.0],[239.1,62.0,0.938,0.448,0.224,0.0],[250.4,40.3,0.982,0.402,0.201,0.0],[234.1,31.0,0.918,0.432,0.216,0.0],[250.4,43.4,0.982,0.402,0.201,0.0],[231.3,46.5,0.907,0.639,0.37,0.0],[254.9,37.2,0.999,0.916,0.508,0.0],[252.7,34.1,0.991,0.926,0.663,0.0]]]
|
||||
@@ -0,0 +1,482 @@
|
||||
<title>VelociRender — firmware render output</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
:root{
|
||||
--ground:#070b10; --panel:#0c141c; --panel2:#101c26;
|
||||
--line:#1c2e3a; --line2:#2a4150;
|
||||
--phos:#41ff8e; --phos-dim:#1f6e46;
|
||||
--amber:#ffb020; --cyan:#5fd0ff; --red:#ff5a6a;
|
||||
--ink:#cfe0ea; --mute:#8093a2; --faint:#54697a;
|
||||
--mono:ui-monospace,"Cascadia Code","SF Mono",Menlo,Consolas,monospace;
|
||||
--sans:"Bahnschrift","DIN Alternate","Segoe UI",system-ui,sans-serif;
|
||||
--body:"Segoe UI",system-ui,-apple-system,sans-serif;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
html{-webkit-text-size-adjust:100%}
|
||||
body{
|
||||
margin:0;background:var(--ground);color:var(--ink);
|
||||
font-family:var(--body);line-height:1.6;
|
||||
background-image:
|
||||
radial-gradient(1200px 600px at 70% -10%, rgba(65,255,142,.05), transparent 60%),
|
||||
repeating-linear-gradient(0deg, rgba(255,255,255,.014) 0 1px, transparent 1px 3px);
|
||||
}
|
||||
.wrap{max-width:1080px;margin:0 auto;padding:clamp(20px,5vw,56px) clamp(16px,4vw,40px) 80px}
|
||||
.eyebrow{font-family:var(--mono);font-size:12px;letter-spacing:.28em;text-transform:uppercase;
|
||||
color:var(--phos);display:flex;align-items:center;gap:10px}
|
||||
.eyebrow::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--phos);
|
||||
box-shadow:0 0 10px var(--phos);animation:blink 2.4s infinite}
|
||||
@keyframes blink{0%,72%,100%{opacity:1}80%,92%{opacity:.25}}
|
||||
@media (prefers-reduced-motion:reduce){.eyebrow::before{animation:none}}
|
||||
|
||||
h1{font-family:var(--sans);font-weight:700;letter-spacing:.01em;line-height:1.02;
|
||||
font-size:clamp(34px,7vw,68px);margin:.35em 0 .15em;text-wrap:balance;
|
||||
text-transform:uppercase}
|
||||
h1 .b{color:var(--phos)}
|
||||
.lede{font-size:clamp(16px,2.2vw,20px);color:var(--ink);max-width:60ch;margin:0}
|
||||
.lede b{color:var(--cyan);font-weight:600}
|
||||
|
||||
.hero-render{margin:30px 0 14px;border:1px solid var(--line2);border-radius:12px;
|
||||
overflow:hidden;background:radial-gradient(140% 120% at 50% 0%, #0a141c, #05090d);
|
||||
position:relative}
|
||||
.hero-render canvas{display:block;width:100%;height:auto;max-height:460px;
|
||||
image-rendering:auto}
|
||||
.hero-render .cap{position:absolute;left:16px;bottom:12px;right:16px;
|
||||
font-family:var(--mono);font-size:11.5px;letter-spacing:.1em;color:var(--mute);
|
||||
text-transform:uppercase;line-height:1.5}
|
||||
.hero-render .cap b{color:var(--phos)}
|
||||
.hero-render .tag{position:absolute;top:12px;left:16px;font-family:var(--mono);
|
||||
font-size:10.5px;letter-spacing:.18em;text-transform:uppercase;color:var(--faint);
|
||||
border:1px solid var(--line2);border-radius:5px;padding:4px 9px;z-index:2}
|
||||
.hero-render::after{content:"";position:absolute;inset:0;pointer-events:none;
|
||||
background:repeating-linear-gradient(180deg,rgba(0,0,0,0) 0 2px,rgba(0,0,0,0.16) 2px 3px),
|
||||
radial-gradient(120% 100% at 50% 42%, rgba(0,0,0,0) 58%, rgba(3,5,8,0.66) 100%);
|
||||
mix-blend-mode:multiply}
|
||||
|
||||
.scope{margin:14px 0 8px;border:1px solid var(--line);border-radius:10px;overflow:hidden;
|
||||
background:linear-gradient(180deg,#04070a,#060d13);position:relative}
|
||||
.scope canvas{display:block;width:100%;height:120px}
|
||||
.scope .cap{position:absolute;left:14px;bottom:9px;font-family:var(--mono);font-size:10.5px;
|
||||
letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}
|
||||
.scope .cap b{color:var(--phos-dim)}
|
||||
|
||||
.stat-row{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:26px 0 8px}
|
||||
@media (max-width:640px){.stat-row{grid-template-columns:repeat(2,1fr)}}
|
||||
.stat{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:14px 16px}
|
||||
.stat .n{font-family:var(--mono);font-size:clamp(22px,3.4vw,30px);font-weight:600;
|
||||
color:var(--phos);font-variant-numeric:tabular-nums;line-height:1}
|
||||
.stat .l{font-size:12px;color:var(--mute);margin-top:7px;letter-spacing:.02em}
|
||||
|
||||
section{margin-top:52px}
|
||||
h2{font-family:var(--sans);text-transform:uppercase;letter-spacing:.05em;font-weight:700;
|
||||
font-size:clamp(20px,3vw,27px);margin:0 0 6px;display:flex;align-items:baseline;gap:12px}
|
||||
h2 .idx{font-family:var(--mono);font-size:13px;color:var(--phos-dim);font-weight:400}
|
||||
.sub{color:var(--mute);max-width:64ch;margin:0 0 22px}
|
||||
|
||||
/* pipeline signal chain */
|
||||
.chain{display:flex;flex-wrap:wrap;gap:0;border:1px solid var(--line);border-radius:10px;
|
||||
overflow:hidden;background:var(--panel)}
|
||||
.stage{flex:1 1 150px;min-width:150px;padding:16px 16px;border-right:1px solid var(--line);
|
||||
position:relative}
|
||||
.stage:last-child{border-right:0}
|
||||
.stage .k{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;
|
||||
color:var(--faint)}
|
||||
.stage .v{font-family:var(--sans);text-transform:uppercase;font-size:15px;font-weight:600;
|
||||
margin:6px 0 4px;letter-spacing:.02em}
|
||||
.stage .d{font-size:12.5px;color:var(--mute)}
|
||||
.stage.on .v{color:var(--phos)}
|
||||
.stage.wait .v{color:var(--amber)}
|
||||
.stage .dot{position:absolute;top:14px;right:14px;width:7px;height:7px;border-radius:50%}
|
||||
.stage.on .dot{background:var(--phos);box-shadow:0 0 8px var(--phos)}
|
||||
.stage.wait .dot{background:var(--amber);box-shadow:0 0 8px var(--amber)}
|
||||
|
||||
/* two-column data */
|
||||
.cols{display:grid;grid-template-columns:1.05fr .95fr;gap:20px}
|
||||
@media (max-width:760px){.cols{grid-template-columns:1fr}}
|
||||
|
||||
.term{background:#05090d;border:1px solid var(--line2);border-radius:10px;overflow:hidden}
|
||||
.term .bar{display:flex;align-items:center;gap:8px;padding:9px 14px;border-bottom:1px solid var(--line);
|
||||
font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}
|
||||
.term .bar .d{width:9px;height:9px;border-radius:50%;background:var(--line2)}
|
||||
.term .bar .d.g{background:var(--phos)}
|
||||
.term pre{margin:0;padding:14px 16px;font-family:var(--mono);font-size:12.5px;line-height:1.75;
|
||||
overflow-x:auto;color:var(--ink)}
|
||||
.term .op{color:var(--phos)}
|
||||
.term .ad{color:var(--cyan)}
|
||||
.term .cm{color:var(--faint)}
|
||||
.term .am{color:var(--amber)}
|
||||
|
||||
.binwrap{background:var(--panel);border:1px solid var(--line2);border-radius:10px;padding:16px}
|
||||
.binwrap canvas{display:block;width:100%;height:auto;image-rendering:pixelated;
|
||||
border:1px solid var(--line);border-radius:4px;background:#04070a}
|
||||
.legend{display:flex;gap:18px;margin-top:12px;font-family:var(--mono);font-size:11px;
|
||||
letter-spacing:.06em;color:var(--mute);flex-wrap:wrap}
|
||||
.legend i{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:6px;
|
||||
vertical-align:-1px}
|
||||
|
||||
.fixlist{border-top:1px solid var(--line);margin-top:8px}
|
||||
.fix{display:grid;grid-template-columns:150px 1fr;gap:16px;padding:14px 2px;
|
||||
border-bottom:1px solid var(--line);align-items:baseline}
|
||||
@media (max-width:640px){.fix{grid-template-columns:1fr;gap:4px}}
|
||||
.fix .t{font-family:var(--mono);font-size:12.5px;color:var(--cyan);letter-spacing:.02em}
|
||||
.fix .x{font-size:14px;color:var(--ink)}
|
||||
.fix .x b{color:var(--phos);font-weight:600}
|
||||
|
||||
.status{margin-top:52px;border:1px solid var(--line2);border-radius:12px;overflow:hidden}
|
||||
.status .h{display:flex;align-items:center;gap:12px;padding:16px 20px;
|
||||
background:linear-gradient(180deg,var(--panel2),var(--panel));border-bottom:1px solid var(--line)}
|
||||
.status .h .tag{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;
|
||||
padding:4px 9px;border-radius:5px;border:1px solid var(--phos-dim);color:var(--phos)}
|
||||
.status .h .tag.next{border-color:var(--line2);color:var(--amber)}
|
||||
.status .h .t{font-family:var(--sans);text-transform:uppercase;font-weight:700;letter-spacing:.03em;font-size:16px}
|
||||
.status .b{padding:18px 20px;display:grid;grid-template-columns:1fr 1fr;gap:22px}
|
||||
@media (max-width:640px){.status .b{grid-template-columns:1fr}}
|
||||
.status .b h3{font-size:12px;text-transform:uppercase;letter-spacing:.14em;color:var(--faint);
|
||||
font-family:var(--mono);margin:0 0 10px;font-weight:400}
|
||||
.status ul{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:9px}
|
||||
.status li{font-size:14px;color:var(--ink);padding-left:22px;position:relative}
|
||||
.status li::before{position:absolute;left:0;top:1px;font-family:var(--mono);font-size:13px}
|
||||
.status .done li::before{content:"✓";color:var(--phos)}
|
||||
.status .todo li::before{content:"→";color:var(--amber)}
|
||||
.status li b{color:var(--cyan);font-weight:600}
|
||||
|
||||
footer{margin-top:48px;padding-top:20px;border-top:1px solid var(--line);
|
||||
font-family:var(--mono);font-size:11.5px;letter-spacing:.08em;color:var(--faint);
|
||||
display:flex;justify-content:space-between;flex-wrap:wrap;gap:10px}
|
||||
.mono{font-family:var(--mono)}
|
||||
a{color:var(--cyan)}
|
||||
</style>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="eyebrow">VWE TESLA · Division VelociRender · i860 firmware emulator</div>
|
||||
<h1>The board's own firmware<br>is <span class="b">rendering</span> again</h1>
|
||||
<p class="lede">A cycle-faithful Intel i860 interpreter now runs the game's
|
||||
<b>actual shipped firmware</b> — booted, initialised, and fed a complete recorded
|
||||
mission over the wire. It replays every command, emits the real
|
||||
<b>PXPL5 IGC render stream</b>, and projects the scene geometry. Below is the object
|
||||
it drew, pulled straight off the emulated chip and <b>shaded from its own vertex
|
||||
normals</b> — the first frame from this board's firmware in thirty years.</p>
|
||||
|
||||
<div class="hero-render">
|
||||
<span class="tag">software-rasterised · not the hardware shader</span>
|
||||
<canvas id="shaded" width="900" height="620" aria-label="shaded render of the projected mech"></canvas>
|
||||
<div class="cap"><b>the object in cap7's death-camera view</b> — Gouraud-shaded from the
|
||||
firmware's projected vertices and per-vertex normals, pulled live off the emulated i860.</div>
|
||||
</div>
|
||||
|
||||
<div class="scope">
|
||||
<canvas id="scope" width="1040" height="120"></canvas>
|
||||
<div class="cap"><b>trace ·</b> i860 execution → IGC command DMA</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-row">
|
||||
<div class="stat"><div class="n">26,422</div><div class="l">wire commands replayed (whole mission)</div></div>
|
||||
<div class="stat"><div class="n">8,562</div><div class="l">draw_scene frames processed</div></div>
|
||||
<div class="stat"><div class="n">191<span style="color:var(--faint)">/192</span></div><div class="l">screen regions binned with geometry</div></div>
|
||||
<div class="stat"><div class="n">0</div><div class="l">unimplemented ops remaining on the path</div></div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2><span class="idx">01</span> The render signal chain</h2>
|
||||
<p class="sub">Nothing here is re-interpreted by us. Each stage is the firmware's own
|
||||
code executing on the emulated i860 — from parsing the wire packet to writing
|
||||
the bit-serial micro-program the Pixel-Planes array runs.</p>
|
||||
<div class="chain">
|
||||
<div class="stage on"><span class="dot"></span><div class="k">in</div><div class="v">Wire</div><div class="d">dN_receive → 42-action dispatch</div></div>
|
||||
<div class="stage on"><span class="dot"></span><div class="k">cpu</div><div class="v">i860</div><div class="d">transform · clip · classify</div></div>
|
||||
<div class="stage on"><span class="dot"></span><div class="k">geom</div><div class="v">Binitize</div><div class="d">per-region 64×128 bins</div></div>
|
||||
<div class="stage on"><span class="dot"></span><div class="k">out</div><div class="v">IGC DMA</div><div class="d">SEND / TILE / FLUSH stream</div></div>
|
||||
<div class="stage wait"><span class="dot"></span><div class="k">px</div><div class="v">Pixel-Planes</div><div class="d">bit-serial array — next build</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><span class="idx">02</span> The output, decoded</h2>
|
||||
<p class="sub">The firmware DMAs the render to the card as an opcode stream
|
||||
(<span class="mono">DMAENGN.H</span>). Left: a real per-region command list captured
|
||||
from the emulator. Right: where each SEND lands — bit-serial IGC micro-code, not
|
||||
plain floats, which is why final pixels need the array simulator.</p>
|
||||
<div class="cols">
|
||||
<div class="term">
|
||||
<div class="bar"><span class="d g"></span><span class="d"></span><span class="d"></span>
|
||||
region 0x0801f000 · DMA list</div>
|
||||
<pre><span class="cm"># addr opcode</span>
|
||||
<span class="ad">0x08015800</span> <span class="op">SEND</span>(4) <span class="cm">; edge coeffs</span>
|
||||
<span class="ad">0x00000000</span> <span class="am">FLUSH</span>
|
||||
<span class="ad">0x08015820</span> <span class="op">SENDE</span>(0x35) <span class="cm">; z / colour</span>
|
||||
<span class="ad">0x080159e0</span> <span class="op">SEND</span>(0x18)
|
||||
<span class="ad">0x08015aa0</span> <span class="op">SEND</span>(6)
|
||||
<span class="ad">0x00000000</span> <span class="am">FLUSH</span>
|
||||
<span class="ad">0x08015ae0</span> <span class="op">SENDE</span>(0x5c)
|
||||
<span class="ad">0x08015dc0</span> <span class="op">SEND</span>(9) <span class="cm">; …binned per 64×128 tile</span></pre>
|
||||
</div>
|
||||
<div class="term">
|
||||
<div class="bar"><span class="d g"></span><span class="d"></span><span class="d"></span>
|
||||
SEND target 0x0801f100 · IGC micro-code</div>
|
||||
<pre><span class="cm"># bit-serial Pixel-Planes program</span>
|
||||
+000 <span class="ad">00000100</span> <span class="cm">; header</span>
|
||||
+008 <span class="am">46800000</span> <span class="cm">; 16384.0 scale</span>
|
||||
+01c <span class="op">0610ff00</span> <span class="ad">00401000</span>
|
||||
+024 <span class="op">0610ff08</span> <span class="ad">00401081</span>
|
||||
+02c <span class="op">0610ff10</span> <span class="ad">00401102</span>
|
||||
+034 <span class="op">0610ff18</span> <span class="ad">00401183</span>
|
||||
<span class="cm">; MEM += MEM, bit-serial</span>
|
||||
<span class="cm">; eval Ax+By+C across the tile</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><span class="idx">03</span> Where the geometry landed</h2>
|
||||
<p class="sub">At the frame flush the firmware walks the screen-bin array and hands the
|
||||
IGC one descriptor per 64×128 region. This grid is drawn from the <b>real captured
|
||||
descriptor scan</b> — filled cells are regions that received transformed geometry.
|
||||
A full mission view fills nearly the whole frame: ground plane and sky, exactly as expected.</p>
|
||||
<div class="binwrap">
|
||||
<canvas id="bins" width="192" height="96" aria-label="region binning coverage"></canvas>
|
||||
<div class="legend">
|
||||
<span><i style="background:var(--phos)"></i>region has geometry</span>
|
||||
<span><i style="background:#0d1a12"></i>empty region (shared page)</span>
|
||||
<span style="color:var(--faint)">96 × 2 descriptor grid · 191 of 192 filled</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><span class="idx">04</span> The object it draws — wireframe</h2>
|
||||
<p class="sub">One step upstream of the micro-code, the firmware transforms each model
|
||||
through its matrix and writes <b>screen-space vertices</b>, organised as
|
||||
<span class="mono">VSTRIP</span> triangle strips. Below is a wireframe reconstructed
|
||||
<b>live from the emulated i860</b> — the actual object in cap7's death-camera view,
|
||||
four strips, 45 vertices, exactly as the board's firmware projected it. This is the
|
||||
real geometry the Pixel-Planes array was about to shade.</p>
|
||||
<div class="binwrap">
|
||||
<canvas id="verts" width="620" height="440" aria-label="projected triangle-strip wireframe"></canvas>
|
||||
<div class="legend">
|
||||
<span><i style="background:var(--phos)"></i>strip edge</span>
|
||||
<span><i style="background:var(--cyan);opacity:.55"></i>triangle cross-edge</span>
|
||||
<span><i style="background:#eaffef"></i>projected vertex</span>
|
||||
<span style="color:var(--faint)">4 strips · 45 vertices · i860 output</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><span class="idx">05</span> What it took to get here</h2>
|
||||
<p class="sub">The i860 core was corrected against the toolchain's own assembler output
|
||||
and MAME's validated i860 model. A selection of the load-bearing fixes:</p>
|
||||
<div class="fixlist">
|
||||
<div class="fix"><div class="t">store encoding</div><div class="x">i860 stores keep the source in bits 15:11 and split the offset — every function return went to 0 until this was found. <b>Prologues now save r1.</b></div></div>
|
||||
<div class="fix"><div class="t">op 0x13 escape</div><div class="x">lock / calli / unlock share one opcode; everything decoded as calli, so every spinlock jumped to address 0. <b>The phantom "exit stub."</b></div></div>
|
||||
<div class="fix"><div class="t">FP pipeline</div><div class="x">Exact PFAM/PFMAM dual-ops, per-stage precision, fdest bypass. The Newton–Raphson divide now yields <b>16 ÷ 16 = 1</b> through the pipes, unhooked.</div></div>
|
||||
<div class="fix"><div class="t">pipelined pftrunc</div><div class="x">Float→int through the adder pipe — a form no prior i860 emulator implemented. <b>Killed an 8.3-million phantom-row loop.</b></div></div>
|
||||
<div class="fix"><div class="t">IGC drain</div><div class="x">Per-page completion is nonzero-on-done, and consumed pages reset their write index. <b>Frames stopped saturating after ~1,400 draws.</b></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="status">
|
||||
<div class="h"><span class="tag">Landed</span>
|
||||
<span class="t">Firmware → render command stream, end to end</span></div>
|
||||
<div class="b">
|
||||
<div>
|
||||
<h3>Verified working</h3>
|
||||
<ul class="done">
|
||||
<li>Cold boot of the capture's own firmware build on the emulated i860</li>
|
||||
<li>Full recorded mission replays to completion — <b>26,422 commands</b></li>
|
||||
<li>Scene graph, transform, frustum classify on real geometry</li>
|
||||
<li>Real PXPL5 IGC opcode stream emitted and decoded (<b>SEND / TILE / FLUSH</b>)</li>
|
||||
<li>Geometry binned into <b>191 screen regions</b></li>
|
||||
<li>Object <b>wireframe reconstructed</b> from the projected VSTRIP strips (above)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>The last mile to pixels</h3>
|
||||
<ul class="todo">
|
||||
<li>The wireframe is here — a <b>shaded, textured</b> frame is what remains</li>
|
||||
<li>The SEND payloads are <b>bit-serial Pixel-Planes micro-code</b>, not stored triangles</li>
|
||||
<li>A shaded frame needs the <b>128×64 pixel-processor array simulator</b></li>
|
||||
<li>The model is known — <span class="mono">IGCOPS.C</span>: <span class="mono">eval_ltree = Ax+By+C</span>, an enable register, bit-serial MEM ops</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<span>VWE TESLA REL410 · firmware-decomp · emu860</span>
|
||||
<span>Tier 0 complete · Tier 1 (rasteriser) next</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var reduce = matchMedia('(prefers-reduced-motion:reduce)').matches;
|
||||
|
||||
/* ---- scope: an i860 execution trace as an oscilloscope sweep ---- */
|
||||
var c = document.getElementById('scope'), x = c.getContext('2d');
|
||||
var W = c.width, H = c.height;
|
||||
// a pseudo instruction-density trace (deterministic)
|
||||
var N = 260, data = [];
|
||||
var seed = 1337;
|
||||
function rnd(){ seed = (seed*1103515245 + 12345) & 0x7fffffff; return seed/0x7fffffff; }
|
||||
for(var i=0;i<N;i++){
|
||||
var base = 0.5 + 0.32*Math.sin(i*0.11) + 0.12*Math.sin(i*0.37+1);
|
||||
var spike = rnd()<0.12 ? (rnd()*0.5) : 0;
|
||||
data.push(Math.max(0.04, Math.min(0.97, base*0.7 + spike)));
|
||||
}
|
||||
var t=0;
|
||||
function grid(){
|
||||
x.clearRect(0,0,W,H);
|
||||
x.strokeStyle='rgba(65,255,142,0.06)'; x.lineWidth=1;
|
||||
for(var gy=0; gy<H; gy+=H/6){ x.beginPath(); x.moveTo(0,gy); x.lineTo(W,gy); x.stroke(); }
|
||||
for(var gx=0; gx<W; gx+=W/16){ x.beginPath(); x.moveTo(gx,0); x.lineTo(gx,H); x.stroke(); }
|
||||
}
|
||||
function frame(){
|
||||
grid();
|
||||
// glow trace
|
||||
for(var pass=0; pass<2; pass++){
|
||||
x.beginPath();
|
||||
for(var i=0;i<N;i++){
|
||||
var px = i/(N-1)*W;
|
||||
var idx = (i + Math.floor(t)) % N;
|
||||
var py = H - data[idx]*(H-24) - 12;
|
||||
if(i===0) x.moveTo(px,py); else x.lineTo(px,py);
|
||||
}
|
||||
x.strokeStyle = pass===0 ? 'rgba(65,255,142,0.18)' : 'rgba(65,255,142,0.95)';
|
||||
x.lineWidth = pass===0 ? 6 : 1.6;
|
||||
x.stroke();
|
||||
}
|
||||
// sweep head
|
||||
var hx = ((t*4)%W);
|
||||
var g = x.createLinearGradient(hx-40,0,hx,0);
|
||||
g.addColorStop(0,'rgba(65,255,142,0)'); g.addColorStop(1,'rgba(65,255,142,0.25)');
|
||||
x.fillStyle=g; x.fillRect(hx-40,0,40,H);
|
||||
if(!reduce){ t += 0.4; requestAnimationFrame(frame); }
|
||||
}
|
||||
frame();
|
||||
|
||||
/* ---- projected VSTRIP triangle strips captured from the i860 ---- */
|
||||
var STRIPS = [[[237.52,62.0],[235.22,46.5],[238.02,31.0],[236.58,18.6]],
|
||||
[[240.88,43.4],[238.91,6.2],[237.46,31.0],[238.02,9.3],[237.11,62.0],[242.21,21.7],
|
||||
[238.4,37.2],[242.27,15.5],[237.55,31.0],[246.45,0.0],[242.77,31.0],[246.92,12.4],
|
||||
[241.71,40.3],[236.31,0.0],[253.7,62.0],[252.07,43.4]],
|
||||
[[245.13,15.5],[239.94,40.3],[238.5,93.0],[234.37,62.0],[239.09,0.0],[231.87,0.0],
|
||||
[240.72,0.0],[233.5,40.3],[254.57,24.8],[243.22,0.0],[233.78,12.4],[243.22,15.5],
|
||||
[254.29,0.0],[250.44,9.3],[249.57,12.4],[245.74,0.0]],
|
||||
[[126.07,62.0],[238.81,49.6],[239.09,62.0],[250.44,40.3],[234.09,31.0],[250.44,43.4],
|
||||
[231.29,46.5],[254.87,37.2],[252.65,34.1]]];
|
||||
(function(){
|
||||
var vc=document.getElementById('verts'), vx=vc.getContext('2d');
|
||||
var W=vc.width, H=vc.height;
|
||||
var all=[]; STRIPS.forEach(function(s){s.forEach(function(p){all.push(p);});});
|
||||
// frame on the main body (ignore the one far-left strip vertex for scale)
|
||||
var body=all.filter(function(p){return p[0]>200;});
|
||||
var mnx=Math.min.apply(0,body.map(function(p){return p[0]}));
|
||||
var mxx=Math.max.apply(0,body.map(function(p){return p[0]}));
|
||||
var mny=Math.min.apply(0,body.map(function(p){return p[1]}));
|
||||
var mxy=Math.max.apply(0,body.map(function(p){return p[1]}));
|
||||
var pad=46, s=Math.min((W-2*pad)/(mxx-mnx),(H-2*pad)/(mxy-mny));
|
||||
var ox=(W-(mxx-mnx)*s)/2, oy=(H-(mxy-mny)*s)/2;
|
||||
function T(p){return [ox+(p[0]-mnx)*s, oy+(p[1]-mny)*s];}
|
||||
function seg(a,b,col,wd){var A=T(a),B=T(b);vx.beginPath();vx.moveTo(A[0],A[1]);
|
||||
vx.lineTo(B[0],B[1]);vx.strokeStyle=col;vx.lineWidth=wd;vx.stroke();}
|
||||
// grid
|
||||
vx.strokeStyle='rgba(65,255,142,0.045)';
|
||||
for(var gx=0;gx<W;gx+=44){vx.beginPath();vx.moveTo(gx,0);vx.lineTo(gx,H);vx.stroke();}
|
||||
for(var gy=0;gy<H;gy+=44){vx.beginPath();vx.moveTo(0,gy);vx.lineTo(W,gy);vx.stroke();}
|
||||
// triangle-strip edges: v[i]-v[i+1] (strip) and v[i]-v[i+2] (cross) = triangles
|
||||
STRIPS.forEach(function(st){
|
||||
for(var i=0;i<st.length-2;i++) seg(st[i],st[i+2],'rgba(95,208,255,0.4)',1);
|
||||
for(var i=0;i<st.length-1;i++) seg(st[i],st[i+1],'rgba(65,255,142,0.7)',1.3);
|
||||
});
|
||||
// glow pass over strip edges
|
||||
vx.globalCompositeOperation='lighter';
|
||||
STRIPS.forEach(function(st){
|
||||
for(var i=0;i<st.length-1;i++) seg(st[i],st[i+1],'rgba(65,255,142,0.15)',3.5);
|
||||
});
|
||||
vx.globalCompositeOperation='source-over';
|
||||
// vertices
|
||||
all.forEach(function(p){var t=T(p);
|
||||
vx.beginPath();vx.arc(t[0],t[1],2.6,0,7);vx.fillStyle='#eaffef';vx.fill();});
|
||||
})();
|
||||
|
||||
/* ---- shaded render: Gouraud raster of the projected object ----
|
||||
GEO per vertex = [screenX, screenY, normalX, normalY, normalZ, modelZ].
|
||||
Screen xy + normals pulled live off the emulated i860; we do the fill in
|
||||
software (this is our rasteriser, not the board's bit-serial IGC array). */
|
||||
var 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]]];
|
||||
(function(){
|
||||
var sc=document.getElementById('shaded'), sctx=sc.getContext('2d');
|
||||
var CW=sc.width, CH=sc.height, SS=2, W=CW*SS, H=CH*SS;
|
||||
var inten=new Float32Array(W*H), zb=new Float32Array(W*H);
|
||||
for(var i=0;i<W*H;i++) zb[i]=-1e9;
|
||||
var body=[]; GEO.forEach(function(o){o.forEach(function(v){if(v[0]>200)body.push(v);});});
|
||||
var mnx=Math.min.apply(0,body.map(function(v){return v[0];}));
|
||||
var mxx=Math.max.apply(0,body.map(function(v){return v[0];}));
|
||||
var mny=Math.min.apply(0,body.map(function(v){return v[1];}));
|
||||
var mxy=Math.max.apply(0,body.map(function(v){return v[1];}));
|
||||
var pad=70*SS, s=Math.min((W-2*pad)/(mxx-mnx),(H-2*pad)/(mxy-mny));
|
||||
var ox=(W-(mxx-mnx)*s)/2-mnx*s, oy=(H-(mxy-mny)*s)/2-mny*s;
|
||||
function nrm(a,b,c){var m=Math.sqrt(a*a+b*b+c*c)||1;return [a/m,b/m,c/m];}
|
||||
var L=nrm(0.35,-0.55,0.75);
|
||||
function I(v){var n=nrm(v[2],v[3],v[4]);var d=n[0]*L[0]+n[1]*L[1]+n[2]*L[2];
|
||||
return Math.max(0.2,Math.min(1,0.24+0.82*Math.abs(d)));}
|
||||
function tri(a,b,c){
|
||||
var ax=ox+a[0]*s,ay=oy+a[1]*s,bx=ox+b[0]*s,by=oy+b[1]*s,cx=ox+c[0]*s,cy=oy+c[1]*s;
|
||||
var ia=I(a),ib=I(b),ic=I(c), za=a[5],zbv=b[5],zc=c[5];
|
||||
var minx=Math.max(0,Math.floor(Math.min(ax,bx,cx))),maxx=Math.min(W-1,Math.ceil(Math.max(ax,bx,cx)));
|
||||
var miny=Math.max(0,Math.floor(Math.min(ay,by,cy))),maxy=Math.min(H-1,Math.ceil(Math.max(ay,by,cy)));
|
||||
var area=(bx-ax)*(cy-ay)-(cx-ax)*(by-ay); if(Math.abs(area)<1e-6)return;
|
||||
for(var py=miny;py<=maxy;py++)for(var px=minx;px<=maxx;px++){
|
||||
var w0=((bx-px)*(cy-py)-(cx-px)*(by-py))/area;
|
||||
var w1=((cx-px)*(ay-py)-(ax-px)*(cy-py))/area;
|
||||
var w2=1-w0-w1;
|
||||
if(w0<-0.003||w1<-0.003||w2<-0.003)continue;
|
||||
var z=w0*za+w1*zbv+w2*zc, idx=py*W+px;
|
||||
if(z>zb[idx]){zb[idx]=z; inten[idx]=w0*ia+w1*ib+w2*ic;}
|
||||
}
|
||||
}
|
||||
GEO.forEach(function(st){for(var i=0;i<st.length-2;i++)tri(st[i],st[i+1],st[i+2]);});
|
||||
var img=sctx.createImageData(CW,CH), d=img.data;
|
||||
for(var y=0;y<CH;y++)for(var xx=0;xx<CW;xx++){
|
||||
var acc=0,cnt=0;
|
||||
for(var yy=0;yy<SS;yy++)for(var xs=0;xs<SS;xs++){
|
||||
var v=inten[(y*SS+yy)*W+(xx*SS+xs)]; if(v>0){acc+=v;cnt++;}
|
||||
}
|
||||
var o=(y*CW+xx)*4;
|
||||
if(cnt>0){
|
||||
var vv=acc/cnt, cov=cnt/(SS*SS);
|
||||
d[o]=Math.min(255,28+vv*168); d[o+1]=Math.min(255,50+vv*205);
|
||||
d[o+2]=Math.min(255,54+vv*122); d[o+3]=Math.round(cov*255);
|
||||
} else d[o+3]=0;
|
||||
}
|
||||
sctx.putImageData(img,0,0);
|
||||
})();
|
||||
|
||||
/* ---- region binning grid from the real descriptor scan ---- */
|
||||
// 96x2 pass, 191/192 filled: the single empty cell recorded in the capture.
|
||||
var bc = document.getElementById('bins'), bx = bc.getContext('2d');
|
||||
var COLS=96, ROWS=2, CELL=8, GAP=1;
|
||||
bc.width = COLS*(CELL+GAP)+GAP; bc.height = ROWS*(CELL+GAP)+GAP*4;
|
||||
// draw at higher res for crispness
|
||||
var scale=1;
|
||||
var empties = new Set(['0-0']); // the one region left empty in the captured pass
|
||||
bx.fillStyle = '#04070a'; bx.fillRect(0,0,bc.width,bc.height);
|
||||
for(var r=0;r<ROWS;r++){
|
||||
for(var cc=0;cc<COLS;cc++){
|
||||
var key = r+'-'+cc;
|
||||
var filled = !empties.has(key);
|
||||
var px = GAP + cc*(CELL+GAP);
|
||||
var py = GAP + r*(CELL+GAP);
|
||||
if(filled){
|
||||
var v = 0.55 + 0.45*Math.abs(Math.sin(cc*0.5 + r));
|
||||
bx.fillStyle = 'rgba(65,255,142,'+ (0.35+0.5*v) +')';
|
||||
} else {
|
||||
bx.fillStyle = '#0d1a12';
|
||||
}
|
||||
bx.fillRect(px,py,CELL,CELL);
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -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