Skip to content

Commit bbfbb8c

Browse files
author
DavidQ
committed
Improve Sample 1605 readability with vehicle-facing rotation and a behind-the-car chase camera<BUILD_PR_LEVEL_17_10_SAMPLE_1605_CHASE_CAMERA_AND_VEHICLE_FACING>
1 parent 84ab23b commit bbfbb8c

8 files changed

Lines changed: 280 additions & 46 deletions

docs/dev/CODEX_COMMANDS.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ MODEL: GPT-5.3-codex
22
REASONING: high
33

44
COMMAND:
5-
Create BUILD_PR_LEVEL_17_9_SAMPLE_1605_DRIVING_CONTROL_FIX.
5+
Create BUILD_PR_LEVEL_17_10_SAMPLE_1605_CHASE_CAMERA_AND_VEHICLE_FACING.
66

77
Goal:
8-
Fix Sample 1605 - 3D Driving Sandbox so left/right steering no longer gets stuck.
8+
Make Sample 1605 read clearly as a driving sandbox by ensuring the driven object turns with heading and the camera follows from behind.
99

1010
Constraints:
1111
- one PR purpose only
@@ -14,18 +14,22 @@ Constraints:
1414
- no zip output from Codex
1515
- keep 2D and networking untouched
1616
- do not modify start_of_day
17+
- preserve the prior 1605 steering fix
1718

1819
Implement:
1920
1. Inspect samples/phase-16/1605/DrivingSandbox3DScene.js.
20-
2. Fix the steering/input-state bug causing left/right navigation to stick.
21-
3. Preserve current visible 3D output and forward/reverse behavior.
22-
4. Add or extend the smallest targeted behavioral sanity check if useful.
21+
2. Ensure the rendered vehicle/box orientation matches heading.
22+
3. Convert or tune the current camera into a chase camera that stays behind the driven object by default.
23+
4. Keep framing simple, stable, and readable.
24+
5. Preserve forward/reverse and current steering behavior.
25+
6. Extend the smallest targeted behavioral sanity check only if useful.
2326

2427
Validate:
25-
- verify left turn works while held
26-
- verify right turn works while held
27-
- verify steering stops when released
28-
- verify opposite turn can be engaged immediately
28+
- verify vehicle visibly turns with heading
29+
- verify camera stays behind the vehicle during normal driving
30+
- verify vehicle remains visible during turning
31+
- verify forward/reverse still function
32+
- verify steering still behaves correctly
2933
- verify sample still renders on load
3034
- run targeted smoke for 1605
3135
- update:

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Fix stuck left/right steering behavior in Sample 1605 driving controls with targeted validation<BUILD_PR_LEVEL_17_9_SAMPLE_1605_DRIVING_CONTROL_FIX>
1+
Improve Sample 1605 readability with vehicle-facing rotation and a behind-the-car chase camera<BUILD_PR_LEVEL_17_10_SAMPLE_1605_CHASE_CAMERA_AND_VEHICLE_FACING>

docs/dev/reports/change_summary.txt

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
Observed issue:
2-
- Sample 1605 renders and loads
3-
- steering behavior gets stuck left/right
2+
- Sample 1605 steering no longer sticks
3+
- sample still feels hard to read as a driving sandbox
4+
- camera can lose the vehicle
5+
- vehicle heading may not be visually obvious
46

57
Applied fix:
6-
- isolated steering correction in samples/phase-16/1605/DrivingSandbox3DScene.js
7-
- removed reverse-dependent steering inversion so A/D turn direction remains consistent
8-
- preserved 1605 rendering path and forward/reverse speed model
9-
- extended existing targeted runtime sanity in tests/runtime/Phase16VisibilitySanity.test.mjs with 1605 steering behavior checks:
10-
- left hold
11-
- right hold
12-
- release stop
13-
- immediate opposite engagement
14-
- reverse motion
8+
- tuned Sample 1605 to a stable chase camera profile:
9+
- behind-car position based on current heading
10+
- chase look-ahead target
11+
- mild yaw lag for readable turning
12+
- kept prior steering fix and existing forward/reverse behavior intact
13+
- made vehicle facing explicit by drawing a heading-oriented vehicle wireframe and front marker in render path
14+
- extended existing targeted runtime sanity in tests/runtime/Phase16VisibilitySanity.test.mjs to cover:
15+
- chase camera behind-car behavior
16+
- visible heading distinction during turn
17+
- vehicle visibility during turning/reverse
18+
- retained steering/reverse behavior checks
1519

1620
Validation outcome:
1721
- PASS Phase16VisibilitySanity

docs/dev/reports/launch_smoke_report.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Launch Smoke Report
22

3-
Generated: 2026-04-15T20:27:13.340Z
3+
Generated: 2026-04-15T20:33:44.997Z
44

55
Filters: games=false, samples=true, tools=false, sampleRange=1605-1605
66

docs/dev/reports/validation_checklist.txt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
Sample 1605 driving control checklist
2-
[x] left turn works while held
3-
[x] right turn works while held
4-
[x] steering stops on release
5-
[x] opposite direction can engage immediately
1+
Sample 1605 chase camera checklist
2+
[x] vehicle visibly turns with heading
3+
[x] camera stays behind during normal driving
4+
[x] vehicle remains visible during turning
5+
[x] steering still behaves correctly
66
[x] forward/reverse still function
77
[x] sample renders visibly on load
88
[x] targeted smoke for 1605 passes
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# BUILD PR: 17.10 Sample 1605 Chase Camera And Vehicle Facing
2+
3+
## Purpose
4+
Improve Sample 1605 readability as a driving sample by making the driven object clearly face its heading and keeping the camera behind the vehicle.
5+
6+
## Why This PR Exists
7+
The current 1605 sample may now accept steering correctly, but it still reads poorly as a driving sandbox because:
8+
- the driven object can feel visually disconnected from heading
9+
- the camera can lose the vehicle
10+
- navigation feels less like driving and more like free movement in 3D space
11+
12+
A driving sample should prioritize orientation clarity over camera experimentation.
13+
14+
## Scope
15+
Surgically improve Sample 1605 only:
16+
- make the driven box/vehicle clearly rotate to match heading
17+
- keep the camera behind the driven object by default
18+
- preserve existing visible 3D output and current steering fix
19+
- keep the sample simple and readable
20+
21+
## In Scope
22+
- samples/phase-16/1605/DrivingSandbox3DScene.js
23+
- minimal targeted sanity extension if needed
24+
- docs/dev/reports/* validation updates
25+
26+
## Out of Scope
27+
- no changes to 1601-1604 or 1606-1608 unless a truly shared safe defect is proven
28+
- no engine-wide camera abstraction work
29+
- no advanced vehicle physics rewrite
30+
- no 2D or networking changes
31+
- no repo-wide scanning
32+
- no zip output from Codex
33+
34+
## Desired Behavior
35+
- left/right steering changes vehicle heading
36+
- the driven object visually turns with heading
37+
- camera follows from behind the driven object
38+
- camera framing remains stable enough that the player does not lose the vehicle during normal driving
39+
- forward/reverse remain usable
40+
- the sample remains a simple teaching sample, not a full racer
41+
42+
## Required Fix Direction
43+
Implement the smallest valid correction:
44+
1. align rendered vehicle orientation with movement heading
45+
2. place/update camera as a chase camera behind the heading vector
46+
3. keep a readable height and distance offset
47+
4. ensure the vehicle remains visible during turning and reversing
48+
5. preserve current steering release behavior
49+
50+
## Acceptance Criteria
51+
- [ ] the box/vehicle visibly turns with heading
52+
- [ ] the camera stays behind the driven object during normal driving
53+
- [ ] the vehicle remains easy to track during turning
54+
- [ ] forward/reverse still function
55+
- [ ] steering still behaves correctly after the prior fix
56+
- [ ] sample remains visible and playable
57+
- [ ] targeted Phase 16 sanity still passes where affected
58+
- [ ] no 2D regression introduced
59+
- [ ] no networking regression introduced
60+
61+
## Validation
62+
- targeted behavioral check for 1605 heading + chase camera behavior
63+
- targeted smoke for 1605
64+
- update docs/dev/reports/change_summary.txt
65+
- update docs/dev/reports/validation_checklist.txt

samples/phase-16/1605/DrivingSandbox3DScene.js

Lines changed: 144 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,112 @@ import { Theme, ThemeTokens } from '/src/engine/theme/index.js';
99
import { drawFrame, drawPanel } from '/src/engine/debug/index.js';
1010
import { World } from '/src/engine/ecs/index.js';
1111
import { stepWorldPhysics3D } from '/src/engine/systems/index.js';
12-
import { createProjectionViewport, drawGroundGrid, drawWireBox } from '../shared/threeDWireframe.js';
12+
import { createProjectionViewport, drawGroundGrid, drawWireBox, projectPoint } from '../shared/threeDWireframe.js';
1313

1414
const theme = new Theme(ThemeTokens);
15+
const BOX_EDGES = [
16+
[0, 1], [1, 2], [2, 3], [3, 0],
17+
[4, 5], [5, 6], [6, 7], [7, 4],
18+
[0, 4], [1, 5], [2, 6], [3, 7],
19+
];
1520

1621
function clamp(value, min, max) {
1722
return Math.max(min, Math.min(max, value));
1823
}
1924

25+
function normalizeAngle(angle) {
26+
let value = angle;
27+
const fullTurn = Math.PI * 2;
28+
while (value > Math.PI) value -= fullTurn;
29+
while (value < -Math.PI) value += fullTurn;
30+
return value;
31+
}
32+
33+
function rotateYaw(localX, localZ, yaw) {
34+
const cos = Math.cos(yaw);
35+
const sin = Math.sin(yaw);
36+
return {
37+
x: localX * cos + localZ * sin,
38+
z: -localX * sin + localZ * cos,
39+
};
40+
}
41+
42+
function createOrientedBoxVertices(transform3D, size3D, yaw) {
43+
const halfWidth = size3D.width * 0.5;
44+
const halfHeight = size3D.height * 0.5;
45+
const halfDepth = size3D.depth * 0.5;
46+
const centerX = transform3D.x + halfWidth;
47+
const centerY = transform3D.y + halfHeight;
48+
const centerZ = transform3D.z + halfDepth;
49+
50+
const localVertices = [
51+
{ x: -halfWidth, y: -halfHeight, z: -halfDepth },
52+
{ x: halfWidth, y: -halfHeight, z: -halfDepth },
53+
{ x: halfWidth, y: halfHeight, z: -halfDepth },
54+
{ x: -halfWidth, y: halfHeight, z: -halfDepth },
55+
{ x: -halfWidth, y: -halfHeight, z: halfDepth },
56+
{ x: halfWidth, y: -halfHeight, z: halfDepth },
57+
{ x: halfWidth, y: halfHeight, z: halfDepth },
58+
{ x: -halfWidth, y: halfHeight, z: halfDepth },
59+
];
60+
61+
return localVertices.map((localVertex) => {
62+
const rotated = rotateYaw(localVertex.x, localVertex.z, yaw);
63+
return {
64+
x: centerX + rotated.x,
65+
y: centerY + localVertex.y,
66+
z: centerZ + rotated.z,
67+
};
68+
});
69+
}
70+
71+
function drawOrientedWireBox(renderer, transform3D, size3D, yaw, cameraState, viewport, color) {
72+
const vertices = createOrientedBoxVertices(transform3D, size3D, yaw);
73+
const projected = vertices.map((vertex) => projectPoint(vertex, cameraState, viewport));
74+
75+
for (const [startIndex, endIndex] of BOX_EDGES) {
76+
const start = projected[startIndex];
77+
const end = projected[endIndex];
78+
if (!start || !end) {
79+
continue;
80+
}
81+
renderer.drawLine(start.x, start.y, end.x, end.y, color, 2);
82+
}
83+
}
84+
85+
function drawVehicleHeadingMarker(renderer, transform3D, size3D, yaw, cameraState, viewport) {
86+
const halfWidth = size3D.width * 0.5;
87+
const halfHeight = size3D.height * 0.5;
88+
const halfDepth = size3D.depth * 0.5;
89+
const centerX = transform3D.x + halfWidth;
90+
const centerY = transform3D.y + halfHeight;
91+
const centerZ = transform3D.z + halfDepth;
92+
93+
const roofLocal = { x: 0, y: halfHeight * 0.55, z: 0 };
94+
const frontLocal = { x: 0, y: halfHeight * 0.45, z: halfDepth + 0.15 };
95+
const noseLocal = { x: 0, y: halfHeight * 0.45, z: halfDepth + 0.95 };
96+
97+
const toWorld = (local) => {
98+
const rotated = rotateYaw(local.x, local.z, yaw);
99+
return {
100+
x: centerX + rotated.x,
101+
y: centerY + local.y,
102+
z: centerZ + rotated.z,
103+
};
104+
};
105+
106+
const roof = projectPoint(toWorld(roofLocal), cameraState, viewport);
107+
const front = projectPoint(toWorld(frontLocal), cameraState, viewport);
108+
const nose = projectPoint(toWorld(noseLocal), cameraState, viewport);
109+
110+
if (roof && front) {
111+
renderer.drawLine(roof.x, roof.y, front.x, front.y, '#fde68a', 2);
112+
}
113+
if (front && nose) {
114+
renderer.drawLine(front.x, front.y, nose.x, nose.y, '#fde68a', 3);
115+
}
116+
}
117+
20118
export default class DrivingSandbox3DScene extends Scene {
21119
constructor() {
22120
super();
@@ -28,6 +126,13 @@ export default class DrivingSandbox3DScene extends Scene {
28126
this.accel = 20;
29127
this.drag = 10;
30128
this.turnRate = 1.8;
129+
this.chaseDistance = 9.4;
130+
this.chaseHeight = 5.4;
131+
this.chaseLookAhead = 2.8;
132+
this.chaseYawLerp = 0.2;
133+
this.cameraPitch = -0.38;
134+
this.cameraYaw = 0;
135+
this.cameraInitialized = false;
31136
this.distance = 0;
32137
this.viewport = {
33138
x: 40,
@@ -107,14 +212,29 @@ export default class DrivingSandbox3DScene extends Scene {
107212
}
108213

109214
const car = this.world.requireComponent(this.carId, 'transform3D');
215+
const forwardX = Math.sin(this.heading);
216+
const forwardZ = Math.cos(this.heading);
217+
const lookTargetX = car.x + forwardX * this.chaseLookAhead;
218+
const lookTargetZ = car.z + forwardZ * this.chaseLookAhead;
219+
const cameraX = car.x - forwardX * this.chaseDistance;
220+
const cameraZ = car.z - forwardZ * this.chaseDistance;
221+
const targetYaw = Math.atan2(lookTargetX - cameraX, lookTargetZ - cameraZ);
222+
223+
if (!this.cameraInitialized) {
224+
this.cameraYaw = targetYaw;
225+
this.cameraInitialized = true;
226+
} else {
227+
this.cameraYaw += normalizeAngle(targetYaw - this.cameraYaw) * this.chaseYawLerp;
228+
}
229+
110230
this.camera3D.setPosition({
111-
x: car.x - Math.sin(this.heading) * 8.5,
112-
y: car.y + 4.8,
113-
z: car.z - Math.cos(this.heading) * 8.5,
231+
x: cameraX,
232+
y: car.y + this.chaseHeight,
233+
z: cameraZ,
114234
});
115235
this.camera3D.setRotation({
116-
x: -0.35,
117-
y: this.heading,
236+
x: this.cameraPitch,
237+
y: this.cameraYaw,
118238
z: 0,
119239
});
120240
}
@@ -163,8 +283,8 @@ export default class DrivingSandbox3DScene extends Scene {
163283
renderer.strokeRect(this.viewport.x, this.viewport.y, this.viewport.width, this.viewport.height, '#d8d5ff', 2);
164284

165285
const cameraState = this.camera3D?.getState?.() ?? {
166-
position: { x: 0, y: 4.8, z: 0 },
167-
rotation: { x: -0.35, y: 0, z: 0 },
286+
position: { x: 0, y: 5.4, z: -0.9 },
287+
rotation: { x: this.cameraPitch, y: this.cameraYaw, z: 0 },
168288
};
169289
const projectionViewport = createProjectionViewport(this.viewport);
170290

@@ -184,21 +304,29 @@ export default class DrivingSandbox3DScene extends Scene {
184304
projectionViewport,
185305
);
186306

187-
const entities = this.world.getEntitiesWith('transform3D', 'size3D', 'renderable3D').map((entityId) => ({
188-
transform3D: this.world.requireComponent(entityId, 'transform3D'),
189-
size3D: this.world.requireComponent(entityId, 'size3D'),
190-
renderable3D: this.world.requireComponent(entityId, 'renderable3D'),
191-
}));
192-
entities.sort((left, right) => right.transform3D.z - left.transform3D.z);
307+
const car = this.world.requireComponent(this.carId, 'transform3D');
308+
const carSize = this.world.requireComponent(this.carId, 'size3D');
309+
const staticEntities = this.world
310+
.getEntitiesWith('transform3D', 'size3D', 'renderable3D')
311+
.filter((entityId) => entityId !== this.carId)
312+
.map((entityId) => ({
313+
transform3D: this.world.requireComponent(entityId, 'transform3D'),
314+
size3D: this.world.requireComponent(entityId, 'size3D'),
315+
renderable3D: this.world.requireComponent(entityId, 'renderable3D'),
316+
}));
193317

194-
entities.forEach(({ transform3D, size3D, renderable3D }) => {
318+
staticEntities.sort((left, right) => right.transform3D.z - left.transform3D.z);
319+
staticEntities.forEach(({ transform3D, size3D, renderable3D }) => {
195320
drawWireBox(renderer, transform3D, size3D, cameraState, projectionViewport, renderable3D.color, 2);
196321
});
197322

198-
const car = this.world.requireComponent(this.carId, 'transform3D');
323+
drawOrientedWireBox(renderer, car, carSize, this.heading, cameraState, projectionViewport, '#38bdf8');
324+
drawVehicleHeadingMarker(renderer, car, carSize, this.heading, cameraState, projectionViewport);
325+
199326
drawPanel(renderer, 620, 34, 300, 126, 'Driving Runtime', [
200327
`Car: x=${car.x.toFixed(2)} y=${car.y.toFixed(2)} z=${car.z.toFixed(2)}`,
201328
`Speed: ${this.speed.toFixed(2)} u/s | Heading: ${this.heading.toFixed(2)} rad`,
329+
`Chase yaw: ${this.cameraYaw.toFixed(2)} rad`,
202330
`Track distance: ${this.distance.toFixed(1)} u`,
203331
`Moved entities: ${this.lastPhysicsSummary.movedEntities}`,
204332
`Resolved collisions: ${this.lastPhysicsSummary.collisionCount}`,

0 commit comments

Comments
 (0)