@@ -9,14 +9,112 @@ import { Theme, ThemeTokens } from '/src/engine/theme/index.js';
99import { drawFrame , drawPanel } from '/src/engine/debug/index.js' ;
1010import { World } from '/src/engine/ecs/index.js' ;
1111import { 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
1414const 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
1621function 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+
20118export 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