--- /dev/null
+target/
+*.class
+/.idea/
+/.classpath
+/.project
+/.settings/
+/*.iml
--- /dev/null
+* aukio-3d-demos-2
+
+Demos rebuilt one by one from aukio-3d-demos (legacy engine) to run on
+Aukio 3D 2 (the rewrite). This repo is the migration checklist: each
+ported demo lands here as a Main class, and each gets an oracle
+comparison against the legacy engine's render of the same scene.
+
+* Hard rules for agents
+
+1. Both engines are dependencies ON PURPOSE (aukio-3d 1.x legacy +
+ aukio-3d-2 2.x). Packages never collide: e3d (legacy) vs e3d2 (new).
+2. A port is DONE only when its oracle comparison runs and the drift
+ is either below the harness tolerance or explicitly NAMED in this
+ file (like the poc oracle policy: new drift = defect unless listed
+ with a justification).
+3. Demos port by NEED (user picks), smallest/highest-value first.
+ Porting is mechanical: SolidPolygon quads -> addTriangle,
+ LightSource -> lights.add, pose -> camera.set (legacy -Y up maps to
+ e3d2 +Y up by negating y at the boundary).
+4. No old-API adapters. The legacy god-classes are the complexity
+ ceiling we escaped; we do not wrap them.
+
+* Migration checklist
+
+| legacy demo | status | oracle |
+| MinimalExample | PORTED 2026-10-11 | compare.sh: 158px (0.051%), meanAbs 0.12 — named edge/fill-rule class |
+| HouseDemo | PORTED 2026-10-14 (colored lights, lit-side bounce, falloff landed in engine; furniture winding fixed with the Shapes inside-out repair) | house-compare.sh: 210763px (68.6%), meanAbs 28.99 — named tonal classes: ambient base (40 vs 24), bounce gains, texel-vs-vertex lightmap grain; geometry/pools/shadows structurally matching; poc bake 2.0s vs legacy 11.5s |
+
+* Layout
+
+- =eu.svjatoslav.aukio.e3d2demos.<Name>Main= — the ported demo (new
+ engine only).
+- =oracle/<Name>Oracle.java + scripts= — legacy render + diff, the
+ same harness shape as aukio-3d-2/oracle.
--- /dev/null
+package eu.svjatoslav.aukio.e3d2demos.oracle;
+
+import eu.svjatoslav.aukio.e3d.examples.house_demo.HouseDemo;
+import eu.svjatoslav.aukio.e3d.geometry.Point3D;
+import eu.svjatoslav.aukio.e3d.headless.Snapshot;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection;
+import eu.svjatoslav.aukio.e3d.renderer.raster.gi.GlobalIllumination;
+import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightSource;
+import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager;
+import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.LightmappedCompositeShape;
+import eu.svjatoslav.aukio.e3d2.kernel.ImageStore;
+import eu.svjatoslav.aukio.e3d2.kernel.Kernel;
+import eu.svjatoslav.aukio.e3d2.kernel.Substrate;
+import eu.svjatoslav.aukio.e3d2.systems.CullPass;
+import eu.svjatoslav.aukio.e3d2.systems.RasterPass;
+import eu.svjatoslav.aukio.e3d2.systems.TransformPass;
+import eu.svjatoslav.aukio.e3d2.systems.gi.GiPass;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.File;
+
+/**
+ * House oracle: the HouseDemo scene baked through BOTH engines' GI and
+ * rendered from the same viewpoint (the demo's default: kitchen, looking
+ * west through both doorways).
+ *
+ * Drift is NAMED, not zero (same policy as aukio-3d-2/oracle): the direct
+ * models differ by design (legacy inverse-square falloff + additive lights
+ * vs e3d2 no-falloff + per-channel max-pool; legacy ambient 24 raised to
+ * 40 here to match the e3d2 AMBIENT constant), and the bounce samplers
+ * differ. Light markers are EXCLUDED on both sides: geometry enclosing a
+ * light point swallows its shadow rays (blacked-out lamp), and the two
+ * engines' marker shapes differ — the oracle compares GI, not markers.
+ *
+ * Pose/scale conventions follow oracle/GiOracle.java in aukio-3d-2
+ * (640x480, poc scale 300, per-engine angle formulas — proven aligned).
+ */
+public final class HouseOracle {
+
+ private static final int W = 640, H = 480;
+ private static final int BLACK = 0xFF000000;
+
+ // Eye (shared logical position; legacy negates y). Target: living-room
+ // center — kitchen view west through both doorways.
+ private static final double EX = 480.94, EY = 130.17, EZ = 126.27;
+ private static final double TX = -800, TY = 130, TZ = 0;
+
+ public static void main(final String[] args) throws Exception {
+ final File outDir = new File(args.length > 0 ? args[0] : "target/house-oracle");
+ outDir.mkdirs();
+
+ // ---------- e3d2 side ----------
+ final Substrate data = new Substrate();
+ eu.svjatoslav.aukio.e3d2demos.HouseDemo.buildScene(data);
+ eu.svjatoslav.aukio.e3d2demos.HouseDemo.addLights(data, false);
+
+ final long t0 = System.currentTimeMillis();
+ final GiPass gi = new GiPass(4, 1e9);
+ gi.apply(data);
+ final long dl = System.currentTimeMillis() + 300_000;
+ while (!gi.isQuiescent() && System.currentTimeMillis() < dl) {
+ Thread.sleep(50);
+ }
+ System.out.println("poc converged=" + gi.isConverged()
+ + " bakeMs=" + (System.currentTimeMillis() - t0));
+ gi.stop();
+
+ data.image = new ImageStore(W, H);
+ data.image.clear(BLACK);
+ final double fx = TX - EX, fy = TY - EY, fz = TZ - EZ;
+ final double fl = Math.sqrt(fx * fx + fy * fy + fz * fz);
+ data.camera.set(EX, EY, EZ, Math.atan2(fx, fz), Math.asin(fy / fl), 300);
+ final Kernel kernel = new Kernel();
+ kernel.add(new TransformPass());
+ kernel.add(new CullPass());
+ kernel.add(new RasterPass());
+ kernel.runOnce(data);
+ writePng(new File(outDir, "house-poc.png"), data.image.pixels);
+
+ // ---------- Aukio 3D (legacy) side, y negated ----------
+ final ShapeCollection scene = new ShapeCollection();
+ final LightmappedCompositeShape house = new LightmappedCompositeShape();
+ house.setLightmappingEnabled(true);
+ house.setLightmapUnitsPerTexel(3.0);
+ HouseDemo.buildHouse(house);
+ HouseDemo.addFurniture(house);
+ scene.addShape(house);
+
+ final LightingManager lighting = new LightingManager();
+ lighting.setAmbientLight(new Color(40, 40, 40, 255)); // match e3d2 AMBIENT
+ lighting.setSkyColor(new Color(18, 24, 34, 255)); // match e3d2 SKY_*
+ lighting.addLight(new LightSource(new Point3D(-800, -240, 0),
+ Color.hex("FFD890"), 6.0));
+ lighting.addLight(new LightSource(new Point3D(0, -240, 0),
+ Color.hex("D8E4FF"), 5.0));
+ lighting.addLight(new LightSource(new Point3D(800, -240, 0),
+ Color.hex("FFB060"), 6.0));
+ lighting.addLight(new LightSource(new Point3D(250, -60, -250),
+ Color.hex("60FF90"), 2.0));
+
+ final double ay = -EY;
+ final double afx = TX - EX, afy = -TY - ay, afz = TZ - EZ;
+ final double afl = Math.sqrt(afx * afx + afy * afy + afz * afz);
+ final String pose = EX + ", " + ay + ", " + EZ + ", "
+ + Math.atan2(-afx, afz) + ", " + (-Math.asin(afy / afl)) + ", 0.0";
+
+ // warm-up render FROM THE FINAL VIEWPOINT: the legacy GI snapshot
+ // collects only what the warm-up frame queued (see GiOracle).
+ Snapshot.renderInto(scene, Snapshot.cameraFromPose(pose),
+ new RenderingContext(W, H, 1), BLACK);
+
+ final long t1 = System.currentTimeMillis();
+ final GlobalIllumination aukioGi = new GlobalIllumination(scene, lighting, 4);
+ aukioGi.start();
+ final long dl2 = System.currentTimeMillis() + 600_000;
+ while (!aukioGi.isConverged() && System.currentTimeMillis() < dl2) {
+ Thread.sleep(250);
+ }
+ System.out.println("aukio converged=" + aukioGi.isConverged()
+ + " bakeMs=" + (System.currentTimeMillis() - t1));
+ aukioGi.stop();
+
+ final RenderingContext ctx = new RenderingContext(W, H, 1);
+ Snapshot.renderInto(scene, Snapshot.cameraFromPose(pose), ctx, BLACK);
+ for (int i = 0; i < ctx.pixels.length; i++) {
+ ctx.pixels[i] |= 0xFF000000;
+ }
+ writePng(new File(outDir, "house-aukio.png"), ctx.pixels);
+
+ // ---------- diff ----------
+ final int[] a = data.image.pixels;
+ final int[] b = ctx.pixels;
+ final int[] heat = new int[W * H];
+ int over = 0;
+ long sumAbs = 0;
+ int maxDiff = 0;
+ for (int i = 0; i < W * H; i++) {
+ int d = 0;
+ for (int ch = 0; ch < 3; ch++) {
+ d = Math.max(d, Math.abs(((a[i] >> (ch * 8)) & 0xFF) - ((b[i] >> (ch * 8)) & 0xFF)));
+ }
+ sumAbs += d;
+ maxDiff = Math.max(maxDiff, d);
+ if (d > 16) {
+ over++;
+ heat[i] = 0xFF000000 | (Math.min(255, d * 4) << 16);
+ }
+ }
+ writePng(new File(outDir, "house-diff.png"), heat);
+ System.out.printf("pixels>16: %d (%.3f%%), meanAbs=%.2f, max=%d%n",
+ over, 100.0 * over / (W * H), (double) sumAbs / (W * H), maxDiff);
+ System.out.println("out: " + outDir.getAbsolutePath());
+ }
+
+ private static void writePng(final File f, final int[] argb) throws Exception {
+ final BufferedImage img = new BufferedImage(W, H, BufferedImage.TYPE_INT_ARGB);
+ img.setRGB(0, 0, W, H, argb, 0, W);
+ ImageIO.write(img, "png", f);
+ }
+}
--- /dev/null
+#!/bin/sh
+# House GI oracle: HouseDemo scene through e3d2 GI and legacy Aukio 3D GI,
+# side by side + diff. Same harness shape as aukio-3d-2/oracle/gi-compare.sh.
+set -e
+cd "${0%/*}/.."
+
+mvn -q compile
+
+LEGACY=../aukio-3d
+LEGACY_DEMOS=../aukio-3d-demos
+for R in "$LEGACY" "$LEGACY_DEMOS"; do
+ if [ ! -d "$R/target/classes" ]; then
+ (cd "$R" && mvn -q compile)
+ fi
+ if [ ! -f "$R/target/cp.txt" ]; then
+ (cd "$R" && mvn -q dependency:build-classpath -Dmdep.outputFile=target/cp.txt)
+ fi
+done
+
+# demos-2 pom depends on BOTH engines; its classpath covers e3d2 + e3d.
+if [ ! -f target/cp.txt ]; then
+ mvn -q dependency:build-classpath -Dmdep.outputFile=target/cp.txt
+fi
+
+CP="target/classes:$(cat target/cp.txt):$LEGACY/target/classes:$LEGACY_DEMOS/target/classes:$(cat "$LEGACY/target/cp.txt"):$(cat "$LEGACY_DEMOS/target/cp.txt")"
+mkdir -p target/oracle-classes
+javac -cp "$CP" -d target/oracle-classes oracle/HouseOracle.java
+java -cp "target/oracle-classes:$CP" eu.svjatoslav.aukio.e3d2demos.oracle.HouseOracle target/house-oracle
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>eu.svjatoslav</groupId>
+ <artifactId>aukio-3d-demos-2</artifactId>
+ <version>2.0.0-SNAPSHOT</version>
+ <packaging>jar</packaging>
+
+ <properties>
+ <maven.compiler.release>21</maven.compiler.release>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <dependencies>
+ <!-- the new engine -->
+ <dependency>
+ <groupId>eu.svjatoslav</groupId>
+ <artifactId>aukio-3d-2</artifactId>
+ <version>2.0.0-SNAPSHOT</version>
+ </dependency>
+ </dependencies>
+
+</project>
--- /dev/null
+package eu.svjatoslav.aukio.e3d2demos;
+
+import eu.svjatoslav.aukio.e3d2.ProceduralTerrainSource;
+import eu.svjatoslav.aukio.e3d2.app.AwtShell;
+import eu.svjatoslav.aukio.e3d2.kernel.GuiStore;
+import eu.svjatoslav.aukio.e3d2.kernel.ImageStore;
+import eu.svjatoslav.aukio.e3d2.kernel.Kernel;
+import eu.svjatoslav.aukio.e3d2.kernel.Substrate;
+import eu.svjatoslav.aukio.e3d2.systems.CameraPass;
+import eu.svjatoslav.aukio.e3d2.systems.CullPass;
+import eu.svjatoslav.aukio.e3d2.systems.RasterPass;
+import eu.svjatoslav.aukio.e3d2.systems.SpaceMousePass;
+import eu.svjatoslav.aukio.e3d2.systems.TransformPass;
+import eu.svjatoslav.aukio.e3d2.systems.WorldStreamer;
+import eu.svjatoslav.aukio.e3d2.systems.gui.DiagnosticsHudPass;
+import eu.svjatoslav.aukio.e3d2.systems.gui.GuiPass;
+import eu.svjatoslav.aukio.e3d2.systems.gui.GuiRenderPass;
+import eu.svjatoslav.aukio.e3d2.systems.gui.SdfFont;
+
+/**
+ * The interactive app: streaming terrain + GUI + HUD in an AWT window.
+ * WASD + QE fly, mouse looks, Quit button works. Everything the shell
+ * needs beyond the kernel is wired here — this is the reference for
+ * how an application assembles the engine.
+ */
+public final class AwtShellMain {
+
+ public static void main(final String[] args) {
+ final Substrate data = new Substrate();
+ data.image = new ImageStore(960, 600);
+ data.camera.set(16, 9, 16, 0.35, -0.28, 450);
+ data.guiFont = SdfFont.build(48);
+
+ data.gui.panels.add(new GuiStore.Panel(0, 0, 960, 44, 0xC0182028));
+ data.gui.labels.add(new GuiStore.Label(12, 33, 0.45, 0xFFEEE8D5,
+ "AUKIO 3D — WASD + QE fly, mouse looks"));
+ data.gui.buttons.add(new GuiStore.Button(840, 7, 108, 30, "quit", "Quit"));
+
+ final Kernel kernel = new Kernel();
+ kernel.add(new WorldStreamer(new ProceduralTerrainSource(), 32, 3));
+ kernel.add(new GuiPass());
+ kernel.add(new SpaceMousePass());
+ kernel.add(new CameraPass());
+ kernel.add(new TransformPass());
+ kernel.add(new CullPass());
+ kernel.add(new RasterPass());
+ kernel.add(new DiagnosticsHudPass());
+ kernel.add(new GuiRenderPass());
+
+ final AwtShell shell = new AwtShell(data, kernel, "Aukio 3D 2");
+ shell.open();
+
+ // app-level click handling: poll the click queue (data, not callbacks)
+ new Thread(() -> {
+ while (true) {
+ if (!data.gui.clicks.isEmpty()) {
+ final String id = data.gui.clicks.remove(0);
+ if ("quit".equals(id)) {
+ shell.close();
+ System.exit(0);
+ }
+ }
+ try {
+ Thread.sleep(50);
+ } catch (final InterruptedException e) {
+ return;
+ }
+ }
+ }, "click-pump").start();
+ }
+}
--- /dev/null
+package eu.svjatoslav.aukio.e3d2demos;
+
+import eu.svjatoslav.aukio.e3d2.app.AwtShell;
+import eu.svjatoslav.aukio.e3d2.kernel.ImageStore;
+import eu.svjatoslav.aukio.e3d2.kernel.Kernel;
+import eu.svjatoslav.aukio.e3d2.kernel.Substrate;
+import eu.svjatoslav.aukio.e3d2.shapes.Shapes;
+import eu.svjatoslav.aukio.e3d2.systems.CameraPass;
+import eu.svjatoslav.aukio.e3d2.systems.CullPass;
+import eu.svjatoslav.aukio.e3d2.systems.LinePass;
+import eu.svjatoslav.aukio.e3d2.systems.RasterPass;
+import eu.svjatoslav.aukio.e3d2.systems.TransformPass;
+import eu.svjatoslav.aukio.e3d2.systems.gui.GuiRenderPass;
+import eu.svjatoslav.aukio.e3d2.systems.gui.SdfFont;
+
+/**
+ * Port of legacy CoordinateSystemDemo: three colored arrows from the
+ * origin (+X red, +Y green, +Z blue) on a reference grid, with labels.
+ *
+ * Built entirely on the e3d2 shape gallery (e3d2.shapes.Shapes):
+ * - SolidPolygonArrow -> Shapes.arrow (legacy proportions: tip radius
+ * 2.5x body, tip length 5x body; consistent outward single winding,
+ * CullPass drops backfaces, translucent faces blend once).
+ * - Grid3D -> Shapes.grid (LineStore: glow, alpha fade at distance).
+ * - ForwardOrientedTextBlock -> LabelPass: screen-space SDF labels
+ * anchored to the projected arrow tips (always facing the viewer,
+ * distance-scaled unclamped, hidden behind the camera).
+ * - Arrow colors carry the legacy alpha B4 (71%): RasterPass
+ * implements legacy's pass-2 translucency with painter's-algorithm
+ * ordering.
+ * - Legacy is -Y up, e3d2 is +Y up: the +Y arrow points UP here — the
+ * axis-to-color mapping is preserved, the world is right-side up.
+ *
+ * Deterministic: no time, no unseeded randomness.
+ */
+public final class CoordinateSystemDemo {
+
+ /** Length of each axis arrow (legacy ARROW_LENGTH). */
+ public static final double ARROW_LENGTH = 200;
+ /** Shaft radius (legacy BODY_RADIUS). */
+ private static final double BODY_RADIUS = 6;
+ /** Label distance past the arrow tip (legacy LABEL_OFFSET). */
+ public static final double LABEL_OFFSET = 20;
+ /** Legacy LineAppearance(1, hex("505064FF")). */
+ private static final int GRID_COLOR = 0xFF505064;
+ private static final double GRID_WIDTH = 1;
+ /** Legacy Grid3D(-300..300, step 50). */
+ private static final int GRID_EXTENT = 300;
+ private static final int GRID_STEP = 50;
+ /** Start camera: diagonal 3/4 view from -x/-z, above the grid —
+ * all three axes read at comparable length (the legacy doc
+ * screenshot was taken after flying, not at buildScene's pose,
+ * so the pose is chosen for composition, not ported). */
+ private static final double START_X = -250, START_Y = 140, START_Z = -250;
+
+ public static void buildScene(final Substrate data) {
+ Shapes.grid(data, -GRID_EXTENT, -GRID_EXTENT, GRID_EXTENT,
+ GRID_EXTENT, 0, GRID_STEP, GRID_COLOR, GRID_WIDTH);
+ // legacy colors with alpha B4 (71%)
+ Shapes.arrow(data, 0, 0, 0, ARROW_LENGTH, 0, 0, BODY_RADIUS,
+ 0xB4FF0000); // +X red, legacy FF0000B4
+ Shapes.arrow(data, 0, 0, 0, 0, ARROW_LENGTH, 0, BODY_RADIUS,
+ 0xB400FF00); // +Y green, legacy 00FF00B4
+ Shapes.arrow(data, 0, 0, 0, 0, 0, ARROW_LENGTH, BODY_RADIUS,
+ 0xB40000FF); // +Z blue, legacy 0000FFB4
+ // axis labels are NOT geometry: LabelPass (in the kernel, see
+ // main) overlays forward-oriented SDF text at the tips
+ }
+
+ /** The three axis labels, anchored just past the arrow tips. */
+ public static LabelPass labels() {
+ final double at = ARROW_LENGTH + LABEL_OFFSET;
+ return new LabelPass()
+ .add(at, 0, 0, "X", 0xFFFF4040)
+ .add(0, at, 0, "Y", 0xFF40FF40)
+ .add(0, 0, at, "Z", 0xFF9090FF);
+ }
+
+ public static void main(final String[] args) {
+ final int w = 800, h = 600;
+ final Substrate data = new Substrate();
+ data.image = new ImageStore(w, h);
+ data.guiFont = SdfFont.build(48); // atlas for the axis labels
+ buildScene(data);
+ // aim at the origin: yaw = atan2(dx, dz), pitch = asin(dy/len)
+ final double len = Math.sqrt(START_X * START_X + START_Y * START_Y
+ + START_Z * START_Z);
+ data.camera.set(START_X, START_Y, START_Z,
+ Math.atan2(-START_X, -START_Z), Math.asin(-START_Y / len),
+ w / 2.5);
+ final Kernel kernel = new Kernel();
+ kernel.add(new CameraPass());
+ kernel.add(new TransformPass());
+ kernel.add(new CullPass());
+ kernel.add(new RasterPass());
+ kernel.add(new LinePass());
+ kernel.add(labels());
+ kernel.add(new GuiRenderPass());
+ new AwtShell(data, kernel, "Coordinate System (Aukio 3D 2)").open();
+ }
+}
--- /dev/null
+package eu.svjatoslav.aukio.e3d2demos;
+
+import eu.svjatoslav.aukio.e3d2.app.AwtShell;
+import eu.svjatoslav.aukio.e3d2.kernel.ImageStore;
+import eu.svjatoslav.aukio.e3d2.kernel.Kernel;
+import eu.svjatoslav.aukio.e3d2.kernel.Substrate;
+import eu.svjatoslav.aukio.e3d2.shapes.Shapes;
+import eu.svjatoslav.aukio.e3d2.systems.CameraPass;
+import eu.svjatoslav.aukio.e3d2.systems.CullPass;
+import eu.svjatoslav.aukio.e3d2.systems.RasterPass;
+import eu.svjatoslav.aukio.e3d2.systems.TransformPass;
+import eu.svjatoslav.aukio.e3d2.systems.gi.GiPass;
+
+/**
+ * Port of legacy HouseDemo: a walkable house interior with three rooms in a
+ * row (living room, hall, kitchen), each lit by a differently colored lamp,
+ * plus a dim green desk lamp in the hall. The point of the demo is GLOBAL
+ * ILLUMINATION in realtime: GiPass bakes per-texel lightmaps on background
+ * threads while you walk around — shadows, light falloff gradients and
+ * colored bounce converge live over the first seconds.
+ *
+ * Engine-difference adaptations (legacy -> e3d2):
+ * - Legacy -Y up maps to e3d2 +Y up by negating y at the boundary.
+ * - Winding: CullPass keeps the face whose winding normal points AWAY from
+ * the viewer, so surfaces are wound with the normal pointing INTO the
+ * wall (opposite of legacy's shading-normal convention). GI bounce still
+ * gathers toward the room: GiPass orients the hemisphere toward the lit
+ * side (gen-0 litSide vote), independent of winding.
+ * - Subdivision is kept from legacy (~100x80-unit cells): GiPass bakes
+ * 0.5 units/texel clamped to 32 texels per triangle, so 100-unit cells
+ * yield ~3 units/texel effective — the same density legacy used
+ * (setLightmapUnitsPerTexel(3.0)).
+ * - Legacy LightSource(color, intensity): e3d2 LightStore folds intensity
+ * into the color channels (r,g,b may exceed 255), and GiPass applies the
+ * SAME attenuation curve as legacy (1/(1 + 0.0001·d²)), so the demo
+ * passes the legacy colors×intensity verbatim — no tuning constants.
+ * - Ambient is the engine constant (40) instead of legacy's 0x181818 (24)
+ * — named drift class "ambient handling".
+ *
+ * Deterministic scene; camera flies with CameraPass (WASD/arrows/mouse
+ * drag; SpaceMouse when present).
+ */
+public final class HouseDemo {
+
+ /** Wall height in world units. */
+ private static final double WALL_HEIGHT = 320;
+ /** Thickness of partition walls between rooms (two surfaces apart). */
+ private static final double PARTITION_THICKNESS = 10;
+ /** Doorway half-width in world units (full width 200). */
+ private static final double DOOR_HALF_WIDTH = 100;
+ /** Doorway height in world units. */
+ private static final double DOOR_HEIGHT = 240;
+
+ // House layout: three rooms side by side along X.
+ // X from -1200 to +1200, partitions at -400 and +400.
+ // Z from -350 to +350. Floor at Y=0, ceiling at Y=+320 (+Y up).
+ private static final double X_MIN = -1200;
+ private static final double X_MAX = 1200;
+ private static final double Z_MIN = -350;
+ private static final double Z_MAX = 350;
+ private static final double FLOOR_Y = 0;
+ private static final double CEIL_Y = WALL_HEIGHT;
+
+ private static final int WALL_COLOR = 0xFFD8D0C0;
+ private static final int FLOOR_COLOR = 0xFF989080;
+ private static final int CEILING_COLOR = 0xFFE8E8E8;
+
+ private HouseDemo() {
+ }
+
+ /** House geometry (walls, floors, ceilings, furniture), no lights. */
+ public static void buildScene(final Substrate data) {
+ buildHouse(data);
+ addFurniture(data);
+ }
+
+ /**
+ * One colored light per room, plus the dim green desk lamp — legacy
+ * positions (y negated), colors and intensities VERBATIM: intensity is
+ * folded into the color channels and GiPass applies legacy's
+ * attenuation curve, so no retuning is needed. When {@code markers} is
+ * set, each lamp gets a small colored cube BELOW its position (the
+ * LightSourceMarker port). The GI oracle passes false: markers near
+ * lamps shadow the bake differently per engine and would drown the GI
+ * comparison.
+ */
+ public static void addLights(final Substrate data, final boolean markers) {
+ addLight(data, -800, 240, 0, 0xFF, 0xD8, 0x90, 6.0, markers); // living, warm yellow
+ addLight(data, 0, 240, 0, 0xD8, 0xE4, 0xFF, 5.0, markers); // hall, cool white
+ addLight(data, 800, 240, 0, 0xFF, 0xB0, 0x60, 6.0, markers); // kitchen, warm orange
+ addLight(data, 250, 60, -250, 0x60, 0xFF, 0x90, 2.0, markers); // hall corner, green
+ }
+
+ private static void addLight(final Substrate data,
+ final double x, final double y, final double z,
+ final int r, final int g, final int b,
+ final double intensity, final boolean marker) {
+ data.lights.add(x, y, z, r * intensity, g * intensity, b * intensity);
+ if (marker) {
+ // The marker must NOT enclose the light point: any geometry
+ // around it swallows every shadow ray and blacks out the lamp
+ // (rays end inside the marker). Hang it 20 units below.
+ Shapes.cube(data, x, y - 20, z, 12, 0xFF000000 | (r << 16) | (g << 8) | b);
+ }
+ }
+
+ /** Builds floor, ceiling, exterior walls and the two partition walls. */
+ public static void buildHouse(final Substrate data) {
+ // Floor: visible from inside (above) — winding normal down.
+ addGridSurface(data,
+ X_MIN, FLOOR_Y, Z_MIN,
+ X_MAX - X_MIN, 0, 0, 0, 0, Z_MAX - Z_MIN,
+ 24, 7, FLOOR_COLOR, 0, 1, 0, false);
+
+ // Ceiling: visible from below — winding normal up.
+ addGridSurface(data,
+ X_MIN, CEIL_Y, Z_MIN,
+ X_MAX - X_MIN, 0, 0, 0, 0, Z_MAX - Z_MIN,
+ 24, 7, CEILING_COLOR, 0, -1, 0, false);
+
+ // Front wall (z = Z_MIN): visible from +Z (inside).
+ addGridSurface(data,
+ X_MIN, FLOOR_Y, Z_MIN,
+ X_MAX - X_MIN, 0, 0, 0, CEIL_Y - FLOOR_Y, 0,
+ 24, 4, WALL_COLOR, 0, 0, 1, false);
+
+ // Back wall (z = Z_MAX): visible from -Z (inside).
+ addGridSurface(data,
+ X_MIN, FLOOR_Y, Z_MAX,
+ X_MAX - X_MIN, 0, 0, 0, CEIL_Y - FLOOR_Y, 0,
+ 24, 4, WALL_COLOR, 0, 0, -1, false);
+
+ // West exterior wall (x = X_MIN): visible from +X (inside).
+ addGridSurface(data,
+ X_MIN, FLOOR_Y, Z_MIN,
+ 0, 0, Z_MAX - Z_MIN, 0, CEIL_Y - FLOOR_Y, 0,
+ 7, 4, WALL_COLOR, 1, 0, 0, false);
+
+ // East exterior wall (x = X_MAX): visible from -X (inside).
+ addGridSurface(data,
+ X_MAX, FLOOR_Y, Z_MIN,
+ 0, 0, Z_MAX - Z_MIN, 0, CEIL_Y - FLOOR_Y, 0,
+ 7, 4, WALL_COLOR, -1, 0, 0, false);
+
+ // Partition walls at x = -400 and x = +400, with a centered doorway.
+ // Each partition is two surfaces PARTITION_THICKNESS apart, one
+ // visible from each room (legacy wall-thickness convention).
+ addPartition(data, -400);
+ addPartition(data, 400);
+ }
+
+ /** Builds one partition wall (two thin surfaces with a doorway). */
+ private static void addPartition(final Substrate data, final double x) {
+ final double half = PARTITION_THICKNESS / 2;
+ // Surface facing the western room (visible from -X).
+ addGridSurface(data,
+ x - half, FLOOR_Y, Z_MIN,
+ 0, 0, Z_MAX - Z_MIN, 0, CEIL_Y - FLOOR_Y, 0,
+ 7, 4, WALL_COLOR, -1, 0, 0, true);
+ // Surface facing the eastern room (visible from +X).
+ addGridSurface(data,
+ x + half, FLOOR_Y, Z_MIN,
+ 0, 0, Z_MAX - Z_MIN, 0, CEIL_Y - FLOOR_Y, 0,
+ 7, 4, WALL_COLOR, 1, 0, 0, true);
+ }
+
+ /**
+ * Adds a flat surface subdivided into a grid of quads (two triangles
+ * per cell). Winding is chosen so the quad is VISIBLE from the
+ * {@code inward} side: CullPass keeps the winding whose normal points
+ * away from the viewer, so the winding normal must point OPPOSITE to
+ * {@code inward}.
+ *
+ * @param cx,cy,cz the grid origin corner
+ * @param ux..uz full edge vector along the first axis
+ * @param vx..vz full edge vector along the second axis
+ * @param uSegs number of cells along U
+ * @param vSegs number of cells along V
+ * @param color surface color (argb)
+ * @param inx..inz direction the surface faces (visible side)
+ * @param hasDoorway if true, cells overlapping the centered doorway
+ * opening are skipped (door spans |U-center| <
+ * DOOR_HALF_WIDTH, from V start up to DOOR_HEIGHT)
+ */
+ private static void addGridSurface(final Substrate data,
+ final double cx, final double cy, final double cz,
+ final double ux, final double uy, final double uz,
+ final double vx, final double vy, final double vz,
+ final int uSegs, final int vSegs,
+ final int color,
+ final double inx, final double iny, final double inz,
+ final boolean hasDoorway) {
+ final double du = 1.0 / uSegs;
+ final double dv = 1.0 / vSegs;
+ final double uLen = Math.sqrt(ux * ux + uy * uy + uz * uz);
+ final double vLen = Math.sqrt(vx * vx + vy * vy + vz * vz);
+
+ for (int i = 0; i < uSegs; i++) {
+ for (int j = 0; j < vSegs; j++) {
+ if (hasDoorway) {
+ // Cell center distance from the wall centerline along U,
+ // and cell base height along V, in world units.
+ final double cellCenterU = (i + 0.5) * du * uLen;
+ final double cellBaseV = j * dv * vLen;
+ final boolean withinDoorWidth =
+ Math.abs(cellCenterU - uLen / 2) < DOOR_HALF_WIDTH;
+ final boolean withinDoorHeight = cellBaseV < DOOR_HEIGHT;
+ if (withinDoorWidth && withinDoorHeight) {
+ continue;
+ }
+ }
+
+ final double f00u = i * du, f00v = j * dv;
+ final double f10u = (i + 1) * du, f10v = j * dv;
+ final double f11u = (i + 1) * du, f11v = (j + 1) * dv;
+ final double f01u = i * du, f01v = (j + 1) * dv;
+
+ // quad corners
+ final double p00x = cx + ux * f00u + vx * f00v;
+ final double p00y = cy + uy * f00u + vy * f00v;
+ final double p00z = cz + uz * f00u + vz * f00v;
+ final double p10x = cx + ux * f10u + vx * f10v;
+ final double p10y = cy + uy * f10u + vy * f10v;
+ final double p10z = cz + uz * f10u + vz * f10v;
+ final double p11x = cx + ux * f11u + vx * f11v;
+ final double p11y = cy + uy * f11u + vy * f11v;
+ final double p11z = cz + uz * f11u + vz * f11v;
+ final double p01x = cx + ux * f01u + vx * f01v;
+ final double p01y = cy + uy * f01u + vy * f01v;
+ final double p01z = cz + uz * f01u + vz * f01v;
+
+ // Winding normal of (p00, p10, p11) is (p10-p00) x (p11-p00).
+ // CullPass keeps the face whose normal points AWAY from the
+ // viewer: keep the order when the normal points opposite to
+ // the visible (inward) direction, flip otherwise.
+ final double e1x = p10x - p00x, e1y = p10y - p00y, e1z = p10z - p00z;
+ final double e2x = p11x - p00x, e2y = p11y - p00y, e2z = p11z - p00z;
+ final double nx = e1y * e2z - e1z * e2y;
+ final double ny = e1z * e2x - e1x * e2z;
+ final double nz = e1x * e2y - e1y * e2x;
+ final boolean normalAwayFromViewer = nx * inx + ny * iny + nz * inz < 0;
+
+ if (normalAwayFromViewer) {
+ quad(data, color, p00x, p00y, p00z, p10x, p10y, p10z,
+ p11x, p11y, p11z, p01x, p01y, p01z);
+ } else {
+ quad(data, color, p00x, p00y, p00z, p01x, p01y, p01z,
+ p11x, p11y, p11z, p10x, p10y, p10z);
+ }
+ }
+ }
+ }
+
+ /** One quad as two triangles: (a,b,c) + (a,c,d). */
+ private static void quad(final Substrate data, final int color,
+ final double ax, final double ay, final double az,
+ final double bx, final double by, final double bz,
+ final double cx, final double cy, final double cz,
+ final double dx, final double dy, final double dz) {
+ data.triangles.addTriangle(ax, ay, az, bx, by, bz, cx, cy, cz, color);
+ data.triangles.addTriangle(ax, ay, az, cx, cy, cz, dx, dy, dz, color);
+ }
+
+ /** Furniture cubes — legacy centers with y negated (sofa, table, counter). */
+ public static void addFurniture(final Substrate data) {
+ Shapes.cube(data, -800, 70, 150, 70, 0xFF8A4A3A); // sofa, living room
+ Shapes.cube(data, 0, 60, -100, 60, 0xFF5A4A6A); // table, hall
+ Shapes.cube(data, 800, 75, 100, 75, 0xFF3A5A4A); // counter, kitchen
+ }
+
+ /** Default pose: from the kitchen looking west through both doorways,
+ * all three rooms in a row. Aims level at the living-room center. */
+ public static void setDefaultPose(final Substrate data, final int width) {
+ final double px = 480.94, py = 130.17, pz = 126.27;
+ final double fx = -800 - px, fy = 130 - py, fz = 0 - pz;
+ final double fl = Math.sqrt(fx * fx + fy * fy + fz * fz);
+ data.camera.set(px, py, pz,
+ Math.atan2(fx, fz), Math.asin(fy / fl), width / 3.0);
+ }
+
+ public static void main(final String[] args) {
+ final int w = 800, h = 600;
+ final Substrate data = new Substrate();
+ data.image = new ImageStore(w, h);
+ buildScene(data);
+ addLights(data, true);
+ setDefaultPose(data, w);
+ final Kernel kernel = new Kernel();
+ kernel.add(new CameraPass());
+ kernel.add(new TransformPass());
+ kernel.add(new CullPass());
+ kernel.add(new RasterPass());
+ kernel.add(new GiPass(4, 1e9));
+ new AwtShell(data, kernel, "House (Aukio 3D 2)").open();
+ }
+}
--- /dev/null
+package eu.svjatoslav.aukio.e3d2demos;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.svjatoslav.aukio.e3d2.kernel.GuiStore;
+import eu.svjatoslav.aukio.e3d2.kernel.Pass;
+import eu.svjatoslav.aukio.e3d2.kernel.SdfFontStore;
+import eu.svjatoslav.aukio.e3d2.kernel.Substrate;
+import eu.svjatoslav.aukio.e3d2.systems.gui.GuiRenderPass;
+
+/**
+ * Forward-oriented text labels anchored to 3D points — the e3d2 answer
+ * to legacy ForwardOrientedTextBlock: instead of text GEOMETRY (a
+ * plaque quad facing the start pose), each label is screen-space SDF
+ * text anchored to a projected 3D point. Always faces the viewer,
+ * never mirrored, scales with distance like world geometry (UNCLAMPED
+ * — user decision 2026-10-13), hides when the anchor is behind the
+ * camera.
+ *
+ * Demo-local on purpose: generic 3D-anchored labels become an ENGINE
+ * feature when a consumer outside demos exists.
+ */
+public final class LabelPass implements Pass {
+
+ /** World size the label emulates at the anchor's depth. */
+ private static final double WORLD_SIZE = 30;
+ /** Hide closer than this camera-space z (near-plane margin). */
+ private static final double HIDE_Z = 5;
+
+ private static final class Entry {
+ double x, y, z;
+ String text;
+ int argb;
+ GuiStore.Label label;
+ }
+
+ private final List<Entry> entries = new ArrayList<>();
+
+ /** Adds a label anchored at world (x,y,z). Returns this (fluent). */
+ public LabelPass add(final double x, final double y, final double z,
+ final String text, final int argb) {
+ final Entry e = new Entry();
+ e.x = x;
+ e.y = y;
+ e.z = z;
+ e.text = text;
+ e.argb = argb;
+ entries.add(e);
+ return this;
+ }
+
+ @Override
+ public int order() {
+ return 249; // with diag-hud, just before gui-render (250)
+ }
+
+ @Override
+ public String name() {
+ return "labels";
+ }
+
+ @Override
+ public void apply(final Substrate data) {
+ if (data.guiFont == null || data.image == null) {
+ return;
+ }
+ for (final Entry e : entries) {
+ if (e.label == null) {
+ e.label = new GuiStore.Label(0, 0, 0.2, e.argb, e.text);
+ data.gui.labels.add(e.label);
+ }
+ place(data, e);
+ }
+ }
+
+ /** Projects the anchor with exactly TransformPass's math, then
+ * moves the label's screen position/scale. */
+ private void place(final Substrate data, final Entry e) {
+ final double cosY = Math.cos(data.camera.yaw);
+ final double sinY = Math.sin(data.camera.yaw);
+ final double cosP = Math.cos(data.camera.pitch);
+ final double sinP = Math.sin(data.camera.pitch);
+ final double dx = e.x - data.camera.x;
+ final double dy = e.y - data.camera.y;
+ final double dz = e.z - data.camera.z;
+ // inverse yaw about +Y, then pitch about +X (TransformPass)
+ final double x1 = dx * cosY - dz * sinY;
+ final double z1 = dx * sinY + dz * cosY;
+ final double cy1 = dy * cosP - z1 * sinP;
+ final double cz1 = dy * sinP + z1 * cosP;
+ if (cz1 < HIDE_Z) {
+ e.label.text = ""; // behind the near plane: hide
+ return;
+ }
+ e.label.text = e.text;
+ final double sx = data.image.width / 2.0 + (x1 / cz1) * data.camera.scale;
+ final double sy = data.image.height / 2.0 - (cy1 / cz1) * data.camera.scale;
+ // perspective scale, UNCLAMPED (user decision 2026-10-13):
+ // labels shrink to specks far away and blow up point-blank,
+ // exactly like world geometry
+ final double s = WORLD_SIZE * data.camera.scale
+ / (cz1 * SdfFontStore.CELL);
+ e.label.scale = s;
+ e.label.x = (int) (sx - GuiRenderPass.textWidth(data.guiFont, e.text, s) / 2.0);
+ e.label.y = (int) (sy + SdfFontStore.CELL * s * 0.25); // baseline ~centered
+ }
+}
--- /dev/null
+package eu.svjatoslav.aukio.e3d2demos;
+
+import eu.svjatoslav.aukio.e3d2.app.AwtShell;
+import eu.svjatoslav.aukio.e3d2.kernel.ImageStore;
+import eu.svjatoslav.aukio.e3d2.kernel.Kernel;
+import eu.svjatoslav.aukio.e3d2.kernel.Substrate;
+import eu.svjatoslav.aukio.e3d2.systems.CameraPass;
+import eu.svjatoslav.aukio.e3d2.systems.CullPass;
+import eu.svjatoslav.aukio.e3d2.systems.RasterPass;
+import eu.svjatoslav.aukio.e3d2.systems.TransformPass;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.File;
+
+/**
+ * Port of legacy MinimalExample: one red box (-50..50), camera pulled
+ * back. Legacy coordinates (-Y up) map to e3d2 (+Y up) by negating y —
+ * the box is symmetric, so only the camera translation changes.
+ *
+ * Default: opens the interactive window (like the legacy ViewFrame —
+ * WASD/QE fly, mouse looks). Arg "shot": headless one-frame render to
+ * target/minimal-example.png for the oracle harness.
+ *
+ * Deterministic: no time, no unseeded randomness (golden harness rule,
+ * same as legacy).
+ */
+public final class MinimalExampleMain {
+
+ public static void buildScene(final Substrate data) {
+ final int red = 0xFFFF0000;
+ // 12 triangles of the box (outward windings)
+ quad(data, -50, -50, 50, 50, -50, 50, 50, 50, 50, -50, 50, 50, red); // front
+ quad(data, 50, -50, -50, -50, -50, -50, -50, 50, -50, 50, 50, -50, red); // back
+ quad(data, -50, 50, 50, 50, 50, 50, 50, 50, -50, -50, 50, -50, red); // top
+ quad(data, -50, -50, -50, 50, -50, -50, 50, -50, 50, -50, -50, 50, red); // bottom
+ quad(data, 50, -50, 50, 50, -50, -50, 50, 50, -50, 50, 50, 50, red); // right
+ quad(data, -50, -50, -50, -50, -50, 50, -50, 50, 50, -50, 50, -50, red); // left
+ }
+
+ private static void quad(final Substrate d,
+ final double x0, final double y0, final double z0,
+ final double x1, final double y1, final double z1,
+ final double x2, final double y2, final double z2,
+ final double x3, final double y3, final double z3,
+ final int color) {
+ d.triangles.addTriangle(x0, y0, z0, x1, y1, z1, x2, y2, z2, color);
+ d.triangles.addTriangle(x0, y0, z0, x2, y2, z2, x3, y3, z3, color);
+ }
+
+ private static Kernel pipeline() {
+ final Kernel kernel = new Kernel();
+ kernel.add(new TransformPass());
+ kernel.add(new CullPass());
+ kernel.add(new RasterPass());
+ return kernel;
+ }
+
+ private static Substrate scene(final int w, final int h) {
+ final Substrate data = new Substrate();
+ data.image = new ImageStore(w, h);
+ buildScene(data);
+ // legacy (0,-100,-300) in -Y-up == e3d2 (0,100,-300); look at origin:
+ // pitch = asin(-100/hypot(100,300)) = -0.322, yaw 0; scale = w/3
+ data.camera.set(0, 100, -300, 0, Math.asin(-100.0 / Math.hypot(100, 300)),
+ w / 3.0);
+ return data;
+ }
+
+ public static void main(final String[] args) throws Exception {
+ final int w = 640, h = 480;
+ if (args.length > 0 && args[0].equals("shot")) {
+ final Substrate data = scene(w, h);
+ data.image.clear(0xFF102030);
+ pipeline().runOnce(data);
+ final File out = new File("target/minimal-example.png");
+ out.getParentFile().mkdirs();
+ final BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
+ img.setRGB(0, 0, w, h, data.image.pixels, 0, w);
+ ImageIO.write(img, "png", out);
+ System.out.println("wrote " + out.getAbsolutePath());
+ return;
+ }
+ final Substrate data = scene(w, h);
+ final Kernel kernel = new Kernel();
+ kernel.add(new CameraPass());
+ kernel.add(new TransformPass());
+ kernel.add(new CullPass());
+ kernel.add(new RasterPass());
+ new AwtShell(data, kernel, "Minimal example (Aukio 3D 2)").open();
+ }
+}
--- /dev/null
+package eu.svjatoslav.aukio.e3d2demos;
+
+import eu.svjatoslav.aukio.e3d2.app.AwtShell;
+import eu.svjatoslav.aukio.e3d2.kernel.ImageStore;
+import eu.svjatoslav.aukio.e3d2.kernel.Kernel;
+import eu.svjatoslav.aukio.e3d2.kernel.Substrate;
+import eu.svjatoslav.aukio.e3d2.systems.CameraPass;
+import eu.svjatoslav.aukio.e3d2.systems.CullPass;
+import eu.svjatoslav.aukio.e3d2.systems.RasterPass;
+import eu.svjatoslav.aukio.e3d2.systems.TransformPass;
+
+/**
+ * Port of legacy RGBCubeDemo: a 10x10x10 grid of square plates on flat
+ * XY planes; grid position maps to color channel (x -> R, y -> G,
+ * z -> B), so flying around explores a volumetric RGB color cube.
+ * Each plate is textured solid-color with a black border (legacy
+ * TextureGenerator.solidWithBorder: 32 px texture, 3 px border).
+ *
+ * Engine-difference adaptations:
+ * - CullPass culls by winding with no per-triangle switch, so each
+ * plate is emitted in BOTH windings (reversed duplicates make it
+ * two-sided, replacing legacy setBackfaceCulling(false)).
+ * - RasterPass multiplies texel by the triangle's flat color, so the
+ * base color is WHITE (identity) and the texture carries the color.
+ * - Legacy aukio-3d is -Y up, e3d2 is +Y up: y is negated at the
+ * boundary. The grid is symmetric in y so only the camera pose
+ * changes; the camera sits at the legacy position (mapped) and is
+ * AIMED at the cube center (the legacy angles were hand-tuned).
+ *
+ * Deterministic: no time, no unseeded randomness.
+ */
+public final class RGBCubeDemo {
+
+ /** Number of plates per dimension. */
+ private static final int GRID_SIZE = 10;
+ /** Size of each plate in world units. */
+ private static final double SQUARE_SIZE = 40;
+ /** Spacing between plate centers (20% gap, as legacy). */
+ private static final double SPACING = SQUARE_SIZE * 1.25;
+ /** Centering offset. */
+ private static final double HALF_GRID = GRID_SIZE / 2.0;
+ /** Texture size in pixels (legacy TEXTURE_SIZE). */
+ private static final int TEXTURE_SIZE = 32;
+ /** Black border width in pixels (legacy BORDER_WIDTH). */
+ private static final int BORDER = 3;
+
+ public static void buildScene(final Substrate data) {
+ final double maxIndex = GRID_SIZE - 1;
+ for (int gz = 0; gz < GRID_SIZE; gz++) {
+ for (int gy = 0; gy < GRID_SIZE; gy++) {
+ for (int gx = 0; gx < GRID_SIZE; gx++) {
+ final double x = (gx - HALF_GRID) * SPACING + SPACING / 2;
+ // -Y-up legacy -> +Y-up e3d2: negate y
+ final double y = -((gy - HALF_GRID) * SPACING + SPACING / 2);
+ final double z = (gz - HALF_GRID) * SPACING + SPACING / 2;
+ final int rgb = ((int) (gx / maxIndex * 255) << 16)
+ | ((int) (gy / maxIndex * 255) << 8)
+ | (int) (gz / maxIndex * 255);
+ plate(data, x, y, z, solidWithBorder(data, rgb));
+ }
+ }
+ }
+ }
+
+ /** Legacy TextureGenerator.solidWithBorder: fill with a black frame. */
+ private static int solidWithBorder(final Substrate data, final int rgb) {
+ final int[] px = new int[TEXTURE_SIZE * TEXTURE_SIZE];
+ for (int ty = 0; ty < TEXTURE_SIZE; ty++) {
+ for (int tx = 0; tx < TEXTURE_SIZE; tx++) {
+ final boolean border = tx < BORDER || ty < BORDER
+ || tx >= TEXTURE_SIZE - BORDER || ty >= TEXTURE_SIZE - BORDER;
+ px[ty * TEXTURE_SIZE + tx] = border ? 0xFF000000 : 0xFF000000 | rgb;
+ }
+ }
+ return data.textures.add(TEXTURE_SIZE, TEXTURE_SIZE, px);
+ }
+
+ /** One textured plate, both windings (two-sided). Base color white:
+ * RasterPass multiplies texel by base color, so white = identity. */
+ private static void plate(final Substrate d, final double x,
+ final double y, final double z, final int tex) {
+ final double h = SQUARE_SIZE / 2;
+ final double x0 = x - h, x1 = x + h, y0 = y - h, y1 = y + h;
+ // visible from -z (winding proven by MinimalExampleMain's front
+ // face): (ll,lr,ur),(ll,ur,ul); uv: u right, v down
+ tri(d, tex, x0, y0, z, 0, 1, x1, y0, z, 1, 1, x1, y1, z, 1, 0);
+ tri(d, tex, x0, y0, z, 0, 1, x1, y1, z, 1, 0, x0, y1, z, 0, 0);
+ // visible from +z: reversed winding, same uv on the same corners
+ tri(d, tex, x0, y1, z, 0, 0, x1, y1, z, 1, 0, x1, y0, z, 1, 1);
+ tri(d, tex, x0, y1, z, 0, 0, x1, y0, z, 1, 1, x0, y0, z, 0, 1);
+ }
+
+ /** One triangle of a plate, with uv. */
+ private static void tri(final Substrate d, final int tex,
+ final double ax, final double ay, final double az,
+ final double au, final double av,
+ final double bx, final double by, final double bz,
+ final double bu, final double bv,
+ final double cx, final double cy, final double cz,
+ final double cu, final double cv) {
+ d.triangles.addTriangle(ax, ay, az, bx, by, bz, cx, cy, cz,
+ 0xFFFFFFFF, au, av, bu, bv, cu, cv, tex);
+ }
+
+ public static void main(final String[] args) {
+ final int w = 800, h = 600;
+ final Substrate data = new Substrate();
+ data.image = new ImageStore(w, h);
+ buildScene(data);
+ // Legacy camera position (-405.73, -407.15, -713.60) with y
+ // negated; aimed exactly at the cube center (origin):
+ // yaw = atan2(dx, dz), pitch = asin(dy/len) for direction
+ // (dx, dy, dz) = -position (yaw 0 = +Z, +yaw right, +pitch up).
+ final double px = -405.73, py = 407.15, pz = -713.60;
+ final double len = Math.sqrt(px * px + py * py + pz * pz);
+ data.camera.set(px, py, pz,
+ Math.atan2(-px, -pz), Math.asin(-py / len), w / 3.0);
+ final Kernel kernel = new Kernel();
+ kernel.add(new CameraPass());
+ kernel.add(new TransformPass());
+ kernel.add(new CullPass());
+ kernel.add(new RasterPass());
+ new AwtShell(data, kernel, "RGB Cube (Aukio 3D 2)").open();
+ }
+}
--- /dev/null
+package eu.svjatoslav.aukio.e3d2demos;
+
+import eu.svjatoslav.aukio.e3d2.app.AwtShell;
+import eu.svjatoslav.aukio.e3d2.kernel.ImageStore;
+import eu.svjatoslav.aukio.e3d2.kernel.Kernel;
+import eu.svjatoslav.aukio.e3d2.kernel.Substrate;
+import eu.svjatoslav.aukio.e3d2.shapes.Shapes;
+import eu.svjatoslav.aukio.e3d2.systems.CameraPass;
+import eu.svjatoslav.aukio.e3d2.systems.CullPass;
+import eu.svjatoslav.aukio.e3d2.systems.LinePass;
+import eu.svjatoslav.aukio.e3d2.systems.RasterPass;
+import eu.svjatoslav.aukio.e3d2.systems.TransformPass;
+import eu.svjatoslav.aukio.e3d2.systems.gui.GuiRenderPass;
+import eu.svjatoslav.aukio.e3d2.systems.gui.SdfFont;
+
+/**
+ * Shape gallery: one of each e3d2.shapes.Shapes solid in a row on the
+ * reference grid, with forward-oriented labels. The e3d2 counterpart
+ * of legacy's composite-shape collection (SolidPolygonArrow, Cube,
+ * Cone, Cylinder, Pyramid, Sphere, Grid3D).
+ *
+ * Deterministic: no time, no unseeded randomness.
+ */
+public final class ShapesGalleryDemo {
+
+ private static final int GRID_COLOR = 0xFF505064;
+ private static final double START_X = -60, START_Y = 160, START_Z = -420;
+
+ public static void buildScene(final Substrate data) {
+ Shapes.grid(data, -400, -200, 400, 200, 0, 50, GRID_COLOR, 1);
+ Shapes.cube(data, -300, 30, 0, 60, 0xFFB0B040);
+ Shapes.sphere(data, -180, 35, 0, 35, 16, 10, 0xFF40C8FF);
+ Shapes.cylinder(data, -60, 0, 0, 0, 70, 0, 25, 12, 0xFFFFC040);
+ Shapes.cone(data, 60, 0, 0, 0, 70, 0, 30, 12, 0xFFFF60C0);
+ Shapes.pyramid(data, 180, 0, 0, 32, 70, 0xFFC0FF60);
+ Shapes.arrow(data, 300, 0, 0, 0, 70, 0, 10, 0xFFFF5040);
+ }
+
+ /** One label above each shape. */
+ public static LabelPass labels() {
+ return new LabelPass()
+ .add(-300, 85, 0, "cube", 0xFFB0FFB0)
+ .add(-180, 85, 0, "sphere", 0xFF40C8FF)
+ .add(-60, 85, 0, "cylinder", 0xFFFFC040)
+ .add(60, 85, 0, "cone", 0xFFFF60C0)
+ .add(180, 85, 0, "pyramid", 0xFFC0FF60)
+ .add(300, 85, 0, "arrow", 0xFFFF5040);
+ }
+
+ public static void main(final String[] args) {
+ final int w = 1024, h = 600;
+ final Substrate data = new Substrate();
+ data.image = new ImageStore(w, h);
+ data.guiFont = SdfFont.build(48);
+ buildScene(data);
+ final double len = Math.sqrt(START_X * START_X + START_Y * START_Y
+ + START_Z * START_Z);
+ data.camera.set(START_X, START_Y, START_Z,
+ Math.atan2(-START_X, -START_Z), Math.asin(-START_Y / len),
+ w / 2.5);
+ final Kernel kernel = new Kernel();
+ kernel.add(new CameraPass());
+ kernel.add(new TransformPass());
+ kernel.add(new CullPass());
+ kernel.add(new RasterPass());
+ kernel.add(new LinePass());
+ kernel.add(labels());
+ kernel.add(new GuiRenderPass());
+ new AwtShell(data, kernel, "Shape Gallery (Aukio 3D 2)").open();
+ }
+}