From: Svjatoslav Agejenko Date: Sat, 24 Oct 2026 10:43:20 +0000 (+0300) Subject: initial commit X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=00edced715bb3412c96c9d3da2cf98786f4d84cf;p=aukio-3d-demos-2.git initial commit --- 00edced715bb3412c96c9d3da2cf98786f4d84cf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e75012 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +target/ +*.class +/.idea/ +/.classpath +/.project +/.settings/ +/*.iml diff --git a/AGENTS.org b/AGENTS.org new file mode 100644 index 0000000..48d3b6a --- /dev/null +++ b/AGENTS.org @@ -0,0 +1,34 @@ +* 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.Main= — the ported demo (new + engine only). +- =oracle/Oracle.java + scripts= — legacy render + diff, the + same harness shape as aukio-3d-2/oracle. diff --git a/oracle/HouseOracle.java b/oracle/HouseOracle.java new file mode 100644 index 0000000..5f89c31 --- /dev/null +++ b/oracle/HouseOracle.java @@ -0,0 +1,163 @@ +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); + } +} diff --git a/oracle/house-compare.sh b/oracle/house-compare.sh new file mode 100755 index 0000000..862f644 --- /dev/null +++ b/oracle/house-compare.sh @@ -0,0 +1,28 @@ +#!/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 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1b11ed6 --- /dev/null +++ b/pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + eu.svjatoslav + aukio-3d-demos-2 + 2.0.0-SNAPSHOT + jar + + + 21 + UTF-8 + + + + + + eu.svjatoslav + aukio-3d-2 + 2.0.0-SNAPSHOT + + + + diff --git a/src/main/java/eu/svjatoslav/aukio/e3d2demos/AwtShellMain.java b/src/main/java/eu/svjatoslav/aukio/e3d2demos/AwtShellMain.java new file mode 100644 index 0000000..44a9393 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d2demos/AwtShellMain.java @@ -0,0 +1,71 @@ +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(); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d2demos/CoordinateSystemDemo.java b/src/main/java/eu/svjatoslav/aukio/e3d2demos/CoordinateSystemDemo.java new file mode 100644 index 0000000..835b453 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d2demos/CoordinateSystemDemo.java @@ -0,0 +1,101 @@ +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(); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d2demos/HouseDemo.java b/src/main/java/eu/svjatoslav/aukio/e3d2demos/HouseDemo.java new file mode 100644 index 0000000..6df056e --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d2demos/HouseDemo.java @@ -0,0 +1,296 @@ +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(); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d2demos/LabelPass.java b/src/main/java/eu/svjatoslav/aukio/e3d2demos/LabelPass.java new file mode 100644 index 0000000..15eaeee --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d2demos/LabelPass.java @@ -0,0 +1,108 @@ +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 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 + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d2demos/MinimalExampleMain.java b/src/main/java/eu/svjatoslav/aukio/e3d2demos/MinimalExampleMain.java new file mode 100644 index 0000000..5d232f2 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d2demos/MinimalExampleMain.java @@ -0,0 +1,92 @@ +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(); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d2demos/RGBCubeDemo.java b/src/main/java/eu/svjatoslav/aukio/e3d2demos/RGBCubeDemo.java new file mode 100644 index 0000000..3d2b4b1 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d2demos/RGBCubeDemo.java @@ -0,0 +1,125 @@ +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(); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d2demos/ShapesGalleryDemo.java b/src/main/java/eu/svjatoslav/aukio/e3d2demos/ShapesGalleryDemo.java new file mode 100644 index 0000000..e626f1d --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d2demos/ShapesGalleryDemo.java @@ -0,0 +1,71 @@ +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(); + } +}