Skip to content

Commit ce4872e

Browse files
author
DavidQ
committed
Add Advanced 3D Samples batch 1 (1609-1611)
1 parent 76ad98f commit ce4872e

17 files changed

Lines changed: 968 additions & 51 deletions

docs/dev/CODEX_COMMANDS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
MODEL: GPT-5.3-codex
22
REASONING: high
3-
COMMAND: Execute BUILD_PR_LEVEL_17_23_PHASE16_VALIDATION_SWEEP, validate Phase 16, fix minimal issues, package results to <project folder>/tmp/BUILD_PR_LEVEL_17_23_PHASE16_VALIDATION_SWEEP.zip
3+
COMMAND: Implement samples 1609-1611 as described, integrate into samples/index.html, ensure runnable, package to <project folder>/tmp/BUILD_PR_LEVEL_17_25_ADVANCED_3D_SAMPLES_BATCH_1.zip

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Phase 16 validation sweep and stabilization
1+
Add Advanced 3D Samples batch 1 (1609-1611)
Lines changed: 5 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,11 @@
11
# Launch Smoke Report
22

3-
Generated: 2026-04-15T22:18:30.369Z
3+
Generated: 2026-04-16T14:29:40.101Z
44

5-
Filters: games=true, samples=true, tools=true, sampleRange=1601-1608
5+
Filters: games=false, samples=true, tools=false, sampleRange=1609-1611
66

77
| Status | Type | Label | Path | Notes | Steps |
88
| --- | --- | --- | --- | --- | --- |
9-
| PASS | game | _template | games\_template\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
10-
| PASS | game | AITargetDummy | games\AITargetDummy\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
11-
| PASS | game | Asteroids | games\Asteroids\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
12-
| PASS | game | Bouncing-ball | games\Bouncing-ball\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
13-
| PASS | game | Breakout | games\Breakout\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
14-
| PASS | game | Gravity | games\Gravity\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
15-
| PASS | game | GravityWell | games\GravityWell\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
16-
| PASS | game | MultiBallChaos | games\MultiBallChaos\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
17-
| PASS | game | Orbit | games\Orbit\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
18-
| PASS | game | PacmanFullAI | games\PacmanFullAI\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
19-
| PASS | game | PacmanLite | games\PacmanLite\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
20-
| PASS | game | PaddleIntercept | games\PaddleIntercept\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
21-
| PASS | game | Pong | games\Pong\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
22-
| PASS | game | ProjectileLab | games\ProjectileLab\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
23-
| PASS | game | SolarSystem | games\SolarSystem\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
24-
| PASS | game | SpaceDuel | games\SpaceDuel\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
25-
| PASS | game | SpaceInvaders | games\SpaceInvaders\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
26-
| PASS | game | Thruster | games\Thruster\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
27-
| PASS | game | vector-arcade-sample | games\vector-arcade-sample\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
28-
| PASS | sample | 1601 | samples\phase-16\1601\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
29-
| PASS | sample | 1602 | samples\phase-16\1602\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
30-
| PASS | sample | 1603 | samples\phase-16\1603\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
31-
| PASS | sample | 1604 | samples\phase-16\1604\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
32-
| PASS | sample | 1605 | samples\phase-16\1605\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
33-
| PASS | sample | 1606 | samples\phase-16\1606\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
34-
| PASS | sample | 1607 | samples\phase-16\1607\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
35-
| PASS | sample | 1608 | samples\phase-16\1608\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
36-
| PASS | tool | 3D Asset Viewer | tools\3D Asset Viewer\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
37-
| PASS | tool | 3D Camera Path Editor | tools\3D Camera Path Editor\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
38-
| PASS | tool | 3D Map Editor | tools\3D Map Editor\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
39-
| PASS | tool | Asset Browser | tools\Asset Browser\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
40-
| PASS | tool | Asset Pipeline Tool | tools\Asset Pipeline Tool\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
41-
| PASS | tool | Palette Browser | tools\Palette Browser\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
42-
| PASS | tool | Parallax Scene Studio | tools\Parallax Scene Studio\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
43-
| PASS | tool | Performance Profiler | tools\Performance Profiler\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
44-
| PASS | tool | Physics Sandbox | tools\Physics Sandbox\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
45-
| PASS | tool | Replay Visualizer | tools\Replay Visualizer\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
46-
| PASS | tool | Sprite Editor | tools\Sprite Editor\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
47-
| PASS | tool | State Inspector | tools\State Inspector\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
48-
| PASS | tool | Tile Model Converter | tools\Tile Model Converter\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
49-
| PASS | tool | Tilemap Studio | tools\Tilemap Studio\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
50-
| PASS | tool | Tool Host | tools\Tool Host\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
51-
| PASS | tool | Vector Asset Studio | tools\Vector Asset Studio\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
52-
| PASS | tool | Vector Map Editor | tools\Vector Map Editor\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
9+
| PASS | sample | 1609 | samples\phase-16\1609\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
10+
| PASS | sample | 1610 | samples\phase-16\1610\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
11+
| PASS | sample | 1611 | samples\phase-16\1611\index.html | | npm install --prefix ./tmp ws → npm run test:launch-smoke |
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
- [ ] all panels render
1+
- [ ] all 3 samples load
22
- [ ] no console errors
3-
- [ ] tests pass
4-
- [ ] roadmap consistent
3+
- [ ] visible functionality per sample
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# BUILD_PR_LEVEL_17_25_ADVANCED_3D_SAMPLES_BATCH_1
2+
3+
Implement:
4+
- samples/1609-lighting-demo
5+
- samples/1610-hybrid-2d-3d
6+
- samples/1611-multiplayer-sync
7+
8+
Requirements:
9+
- runnable from samples/index.html
10+
- demonstrate feature clearly
11+
- include minimal assets if needed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# PLAN_PR_LEVEL_17_25_ADVANCED_3D_SAMPLES_BATCH_1
2+
3+
Purpose:
4+
Continue Level 17 by implementing first batch of Advanced 3D Samples.
5+
6+
Scope (bundled to reduce churn):
7+
- 1609 Lighting Demo
8+
- 1610 Hybrid 2D/3D World Sample
9+
- 1611 Multiplayer Sync Demo
10+
11+
Constraints:
12+
- testable samples
13+
- follow existing sample structure
14+
- no engine rewrites

samples/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,9 @@ <h2>Phase 16 - 3D Capability Track</h2>
481481
<a class="live" href="./phase-16/1606/index.html" title="Experiment with gravity, bounce, and impulse controls in a constrained 3D arena." data-tags="camera3d, gravity, impulse, physics3d, scene, themetokens" data-primary="physics-playground">Sample 1606 - 3D Physics Playground</a>
482482
<a class="live" href="./phase-16/1607/index.html" title="Pilot and fire in a minimal 3D lane shooter against incoming asteroids." data-tags="camera3d, movement3d, shooter3d, collision3d, scene, themetokens" data-primary="space-shooter">Sample 1607 - 3D Space Shooter</a>
483483
<a class="live" href="./phase-16/1608/index.html" title="Crawl a compact 3D dungeon, collect a relic, and unlock the exit route." data-tags="camera3d, crawler3d, maze3d, movement3d, physics3d, scene, themetokens" data-primary="dungeon-crawler">Sample 1608 - 3D Dungeon Crawler</a>
484+
<a class="live" href="./phase-16/1609/index.html" title="Showcases dynamic light movement and wireframe shading changes across a compact 3D set." data-tags="camera3d, lighting3d, render3d, scene, themetokens" data-primary="lighting-demo">Sample 1609 - Lighting Demo</a>
485+
<a class="live" href="./phase-16/1610/index.html" title="Renders one shared world in both a 2D tactical map and a 3D perspective viewport." data-tags="camera3d, hybrid2d3d, render3d, scene, themetokens" data-primary="hybrid-2d-3d">Sample 1610 - Hybrid 2D/3D World</a>
486+
<a class="live" href="./phase-16/1611/index.html" title="Demonstrates client prediction, delayed authority updates, and replica sync in a 3D lane." data-tags="camera3d, multiplayer3d, networking, sync, scene, themetokens" data-primary="multiplayer-sync">Sample 1611 - Multiplayer Sync Demo</a>
484487
</div>
485488
</section>
486489
</div>
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
LightingDemo3DScene.js
6+
*/
7+
import { Scene } from '/src/engine/scene/index.js';
8+
import { Theme, ThemeTokens } from '/src/engine/theme/index.js';
9+
import { drawFrame, drawPanel } from '/src/engine/debug/index.js';
10+
import {
11+
applyPhase16CameraMode,
12+
createPhase16ViewState,
13+
createProjectionViewport,
14+
drawDepthBackdrop,
15+
drawGroundGrid,
16+
drawPhase16DebugOverlay,
17+
drawWireBox,
18+
stepPhase16ViewToggles,
19+
} from '../shared/threeDWireframe.js';
20+
21+
const theme = new Theme(ThemeTokens);
22+
23+
function clamp(value, min, max) {
24+
return Math.max(min, Math.min(max, value));
25+
}
26+
27+
function parseHexColor(color) {
28+
if (typeof color !== 'string' || !color.startsWith('#') || color.length !== 7) {
29+
return null;
30+
}
31+
const r = Number.parseInt(color.slice(1, 3), 16);
32+
const g = Number.parseInt(color.slice(3, 5), 16);
33+
const b = Number.parseInt(color.slice(5, 7), 16);
34+
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
35+
return null;
36+
}
37+
return { r, g, b };
38+
}
39+
40+
function shadeColor(color, brightness) {
41+
const rgb = parseHexColor(color);
42+
if (!rgb) {
43+
return color;
44+
}
45+
const scale = clamp(brightness, 0.2, 1.2);
46+
const r = Math.round(clamp(rgb.r * scale, 0, 255));
47+
const g = Math.round(clamp(rgb.g * scale, 0, 255));
48+
const b = Math.round(clamp(rgb.b * scale, 0, 255));
49+
return `rgb(${r}, ${g}, ${b})`;
50+
}
51+
52+
export default class LightingDemo3DScene extends Scene {
53+
constructor() {
54+
super();
55+
this.viewport = {
56+
x: 40,
57+
y: 170,
58+
width: 860,
59+
height: 320,
60+
focalLength: 460,
61+
};
62+
this.viewState = createPhase16ViewState();
63+
this.lightAngle = 0;
64+
this.lightHeight = 1.6;
65+
this.lightSpeed = 1.8;
66+
this.lightPulse = 0;
67+
this.lastInputLabel = 'idle';
68+
this.cameraYaw = 0;
69+
this.cameraPitch = -0.36;
70+
this.cameraDistance = 16;
71+
this.cameraHeight = 7;
72+
73+
this.cubes = [
74+
{ transform3D: { x: -6.8, y: -0.2, z: 8.0 }, size3D: { width: 3.2, height: 3.2, depth: 3.2 }, color: '#60a5fa' },
75+
{ transform3D: { x: -1.6, y: -0.2, z: 12.8 }, size3D: { width: 3.0, height: 4.2, depth: 3.0 }, color: '#f59e0b' },
76+
{ transform3D: { x: 3.0, y: -0.2, z: 9.8 }, size3D: { width: 4.0, height: 2.5, depth: 4.0 }, color: '#22c55e' },
77+
{ transform3D: { x: 6.6, y: -0.2, z: 15.0 }, size3D: { width: 2.6, height: 3.6, depth: 2.6 }, color: '#f472b6' },
78+
];
79+
}
80+
81+
setCamera3D(camera3D) {
82+
this.camera3D = camera3D;
83+
this.syncCamera();
84+
}
85+
86+
getLightPosition() {
87+
return {
88+
x: Math.cos(this.lightAngle) * 8.5,
89+
y: this.lightHeight + Math.sin(this.lightPulse) * 0.5 + 4.0,
90+
z: 11 + Math.sin(this.lightAngle) * 7.5,
91+
};
92+
}
93+
94+
syncCamera() {
95+
if (!this.camera3D) {
96+
return;
97+
}
98+
const focusPoint = { x: 0, y: 1.5, z: 12 };
99+
const basePose = {
100+
position: {
101+
x: focusPoint.x + Math.sin(this.cameraYaw) * this.cameraDistance,
102+
y: this.cameraHeight + Math.sin(this.cameraPitch) * 1.4,
103+
z: focusPoint.z - Math.cos(this.cameraYaw) * this.cameraDistance,
104+
},
105+
rotation: {
106+
x: this.cameraPitch,
107+
y: this.cameraYaw,
108+
z: 0,
109+
},
110+
};
111+
applyPhase16CameraMode(this.camera3D, this.viewState, basePose, focusPoint);
112+
}
113+
114+
step3DPhysics(dt, engine) {
115+
const input = engine.input;
116+
stepPhase16ViewToggles(this.viewState, input);
117+
118+
const left = input?.isDown('ArrowLeft') || input?.isDown('KeyA');
119+
const right = input?.isDown('ArrowRight') || input?.isDown('KeyD');
120+
const up = input?.isDown('ArrowUp') || input?.isDown('KeyW');
121+
const down = input?.isDown('ArrowDown') || input?.isDown('KeyS');
122+
123+
if (left) this.lightAngle -= this.lightSpeed * dt;
124+
if (right) this.lightAngle += this.lightSpeed * dt;
125+
if (up) this.lightHeight = clamp(this.lightHeight + 3.4 * dt, 0.5, 4.8);
126+
if (down) this.lightHeight = clamp(this.lightHeight - 3.4 * dt, 0.5, 4.8);
127+
if (input?.isDown('KeyQ')) this.cameraYaw -= 1.1 * dt;
128+
if (input?.isDown('KeyE')) this.cameraYaw += 1.1 * dt;
129+
130+
this.lastInputLabel = [left ? 'left' : '', right ? 'right' : '', up ? 'up' : '', down ? 'down' : '']
131+
.filter(Boolean)
132+
.join('+') || 'idle';
133+
134+
this.lightPulse += dt * 1.8;
135+
this.syncCamera();
136+
}
137+
138+
getLightBrightness(target, lightPosition) {
139+
const center = {
140+
x: target.transform3D.x + target.size3D.width * 0.5,
141+
y: target.transform3D.y + target.size3D.height * 0.5,
142+
z: target.transform3D.z + target.size3D.depth * 0.5,
143+
};
144+
const dx = center.x - lightPosition.x;
145+
const dy = center.y - lightPosition.y;
146+
const dz = center.z - lightPosition.z;
147+
const distance = Math.hypot(dx, dy, dz);
148+
const attenuation = clamp(1 - distance / 26, 0.2, 1);
149+
const verticalBias = clamp((lightPosition.y - center.y + 2.5) / 6, 0.35, 1.05);
150+
return attenuation * verticalBias;
151+
}
152+
153+
render(renderer) {
154+
drawFrame(renderer, theme, [
155+
'Sample 1609 - 3D Lighting Demo',
156+
'Demonstrates dynamic light position affecting wireframe shading.',
157+
'Light angle: Left/Right | Light height: Up/Down | Orbit camera: Q/E | Camera: C | Debug: V',
158+
]);
159+
160+
renderer.strokeRect(this.viewport.x, this.viewport.y, this.viewport.width, this.viewport.height, '#d8d5ff', 2);
161+
drawDepthBackdrop(renderer, this.viewport);
162+
163+
const cameraState = this.camera3D?.getState?.() ?? {
164+
position: { x: 0, y: 7, z: -4 },
165+
rotation: { x: -0.36, y: 0, z: 0 },
166+
};
167+
const projectionViewport = createProjectionViewport(this.viewport);
168+
drawGroundGrid(renderer, { minX: -10, maxX: 10, minZ: 4, maxZ: 20, y: -0.2, step: 2 }, cameraState, projectionViewport);
169+
170+
const lightPosition = this.getLightPosition();
171+
const sortedCubes = [...this.cubes].sort((left, right) => right.transform3D.z - left.transform3D.z);
172+
sortedCubes.forEach((cube) => {
173+
const brightness = this.getLightBrightness(cube, lightPosition);
174+
drawWireBox(
175+
renderer,
176+
cube.transform3D,
177+
cube.size3D,
178+
cameraState,
179+
projectionViewport,
180+
shadeColor(cube.color, brightness + 0.25),
181+
{ lineWidth: 2.1, depthCueEnabled: true }
182+
);
183+
});
184+
drawWireBox(
185+
renderer,
186+
{
187+
x: lightPosition.x - 0.35,
188+
y: lightPosition.y - 0.35,
189+
z: lightPosition.z - 0.35,
190+
},
191+
{ width: 0.7, height: 0.7, depth: 0.7 },
192+
cameraState,
193+
projectionViewport,
194+
'#fef08a',
195+
{ lineWidth: 2.5, depthCueEnabled: false }
196+
);
197+
198+
drawPanel(renderer, 620, 34, 300, 170, 'Lighting Runtime', [
199+
`Light position: x=${lightPosition.x.toFixed(2)} y=${lightPosition.y.toFixed(2)} z=${lightPosition.z.toFixed(2)}`,
200+
`Light angle: ${this.lightAngle.toFixed(2)} rad`,
201+
`Light height offset: ${this.lightHeight.toFixed(2)}`,
202+
`Light input: ${this.lastInputLabel}`,
203+
`Camera yaw: ${this.cameraYaw.toFixed(2)}`,
204+
`Camera mode: ${this.viewState.cameraMode}`,
205+
]);
206+
207+
drawPhase16DebugOverlay(renderer, this.viewport, this.viewState, [
208+
`Light pulse: ${Math.sin(this.lightPulse).toFixed(2)}`,
209+
'Shading model: distance attenuation + vertical bias',
210+
]);
211+
}
212+
}

samples/phase-16/1609/index.html

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!--
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
index.html
6+
-->
7+
<!DOCTYPE html>
8+
<html lang="en">
9+
<head>
10+
<meta charset="UTF-8" />
11+
<title>Sample 1609 - 3D Lighting Demo</title>
12+
<link rel="stylesheet" href="../../../src/engine/ui/baseLayout.css" />
13+
</head>
14+
<body>
15+
<main>
16+
<h1>Sample 1609 - 3D Lighting Demo</h1>
17+
<p>Demonstrates simple dynamic lighting on wireframe geometry with movable light direction.</p>
18+
<canvas id="game" width="960" height="540"></canvas>
19+
20+
<section>
21+
<h3>Engine Classes Used</h3>
22+
<ul>
23+
<li>Engine</li>
24+
<li>Scene</li>
25+
<li>Camera3D</li>
26+
</ul>
27+
</section>
28+
</main>
29+
30+
<script type="module" src="/samples/shared/sampleDetailPageEnhancement.js"></script>
31+
<script type="module" src="./main.js"></script>
32+
</body>
33+
</html>

samples/phase-16/1609/main.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
main.js
6+
*/
7+
import Engine from '/src/engine/core/Engine.js';
8+
import { InputService } from '/src/engine/input/index.js';
9+
import { Theme, ThemeTokens } from '/src/engine/theme/index.js';
10+
import LightingDemo3DScene from './LightingDemo3DScene.js';
11+
12+
const theme = new Theme(ThemeTokens);
13+
theme.applyDocumentTheme();
14+
15+
const canvas = document.getElementById('game');
16+
const input = new InputService();
17+
18+
const engine = new Engine({
19+
canvas,
20+
width: 960,
21+
height: 540,
22+
fixedStepMs: 1000 / 60,
23+
input,
24+
});
25+
26+
engine.setScene(new LightingDemo3DScene());
27+
engine.start();

0 commit comments

Comments
 (0)