1
1
import * as THREE from 'three' ;
2
2
import { XRButton } from 'three/examples/jsm/Addons.js' ;
3
- import { setupSimulation , updateSimulationVR , updateSimulationXR } from './src/Simulation' ;
3
+ import { setupSimulation , updateSimulationVR , updateSimulationXR , resetParticleHeight } from './src/Simulation' ;
4
4
import GradientSkydome from './src/GradientSkydome' ;
5
+ import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js' ;
5
6
6
7
// Set up camera
7
8
const camera = new THREE . PerspectiveCamera ( 75 , window . innerWidth / window . innerHeight , 0.01 , 5000 ) ;
@@ -11,9 +12,11 @@ camera.position.set( 0, 2, 5 );
11
12
const renderer = new THREE . WebGLRenderer ( { antialias : true } ) ;
12
13
renderer . xr . enabled = true ;
13
14
renderer . setSize ( window . innerWidth , window . innerHeight ) ;
14
- renderer . setAnimationLoop ( animateVR ) ; // Start running animation loop
15
+ renderer . setAnimationLoop ( animateWeb ) ; // Start running animation loop
15
16
16
- const sessionInit = { requiredFeatures : [ "plane-detection" ] } ;
17
+ const sessionInit = { requiredFeatures : [
18
+ "plane-detection"
19
+ ] } ;
17
20
18
21
// Append DOM elements
19
22
document . body . appendChild ( renderer . domElement ) ;
@@ -31,10 +34,10 @@ const light = new THREE.DirectionalLight( 0xffffff, 1 );
31
34
light . position . set ( 1 , 1 , 1 ) ;
32
35
scene . add ( light ) ;
33
36
34
- const particles = setupSimulation ( scene ) ;
37
+ const [ plane , particles ] = setupSimulation ( scene ) ;
35
38
let simulationEnabled = true ;
36
39
37
- function animateVR ( ) {
40
+ function animateWeb ( ) {
38
41
if ( simulationEnabled ) {
39
42
updateSimulationVR ( ) ;
40
43
}
@@ -51,11 +54,16 @@ renderer.xr.addEventListener('sessionstart', () => {
51
54
return ;
52
55
}
53
56
57
+ resetParticleHeight ( ) ;
54
58
setupXRSession ( session ) ;
55
59
} ) ;
56
60
57
61
function setupXRSession ( session ) {
58
62
renderer . setAnimationLoop ( ( timestamp , frame ) => {
63
+ if ( controllerIsSelecting ) {
64
+ handleControllerSelectMove ( ) ;
65
+ }
66
+
59
67
if ( frame && simulationEnabled ) {
60
68
onXRFrame ( timestamp , frame ) ;
61
69
}
@@ -64,20 +72,16 @@ function setupXRSession( session ) {
64
72
}
65
73
66
74
function onXRFrame ( timestamp , frame ) {
67
- const detectedPlanes = frame . detectedPlanes ;
68
75
const referenceSpace = renderer . xr . getReferenceSpace ( ) ;
76
+ const detectedPlanes = frame . detectedPlanes ;
77
+
78
+ // showXRPlaneWireframe( frame, referenceSpace, detectedPlanes );
69
79
70
- detectedPlanes . forEach ( plane => {
71
- if ( plane ) {
72
- const pose = frame . getPose ( plane . planeSpace , referenceSpace ) ;
73
-
74
- if ( pose ) {
75
- let position = pose . transform . position ;
76
- console . log ( `Plane detected at: x=${ position . x } , y=${ position . y } , z=${ position . z } ` ) ;
77
- updateSimulationXR ( plane , pose ) ;
78
- }
79
- }
80
- } )
80
+ if ( xrEnabled ) {
81
+ updateSimulationXR ( detectedPlanes , frame , referenceSpace ) ;
82
+ } else {
83
+ updateSimulationVR ( ) ;
84
+ }
81
85
}
82
86
83
87
//
@@ -102,11 +106,15 @@ window.addEventListener( 'mousedown', ( event) => {
102
106
const intersects = mouseRaycaster . intersectObjects ( particles , true ) ;
103
107
if ( intersects . length > 0 ) {
104
108
simulationEnabled = false ;
109
+
105
110
mouseSelectedObject = intersects [ 0 ] . object ;
111
+ mouseSelectedObject . updateColor ( 0x00ff00 ) ;
106
112
107
- //
113
+ // Notes:
108
114
// intersects[0].point: the exact 3D world coordinates where the ray hits the object
109
115
// intersects[0].object.position: the position of the object’s origin
116
+
117
+ //
110
118
mouseObjectOffset . copy ( intersects [ 0 ] . point ) . sub ( mouseSelectedObject . position ) ;
111
119
112
120
mouseObjectDepth = camera . position . distanceTo ( mouseSelectedObject . position ) ;
@@ -132,55 +140,196 @@ window.addEventListener( 'mousemove', ( event) => {
132
140
} ) ;
133
141
134
142
window . addEventListener ( 'mouseup' , ( ) => {
135
- mouseSelectedObject = null ;
136
- simulationEnabled = true ;
143
+ if ( mouseSelectedObject ) {
144
+ mouseSelectedObject . resetColor ( ) ;
145
+ mouseSelectedObject = null ;
146
+
147
+ simulationEnabled = true ;
148
+ }
137
149
} ) ;
138
150
139
151
//
140
152
// VR CONTROLLER EVENTS
141
153
//
142
154
143
- const controller = renderer . xr . getController ( 0 ) ; // Get the first controller
155
+ const controller = renderer . xr . getController ( 1 ) ; // Right controller
144
156
scene . add ( controller ) ;
145
157
158
+ // Create visible controller model with a laser pointer
159
+ const controllerModelFactory = new XRControllerModelFactory ( ) ;
160
+ const controllerGrip = renderer . xr . getControllerGrip ( 1 ) ;
161
+ controllerGrip . add ( controllerModelFactory . createControllerModel ( controllerGrip ) ) ;
162
+ scene . add ( controllerGrip ) ;
163
+
164
+ const laser = createLaserBeam ( controller ) ;
165
+ laser . visible = true ;
166
+
146
167
const controllerRaycaster = new THREE . Raycaster ( ) ;
147
168
let controllerSelectedObject = null ;
148
169
let controllerObjectOffset = new THREE . Vector3 ( ) ;
149
170
let controllerObjectDepth = 0 ;
171
+ let controllerIsSelecting = false ;
150
172
151
173
// VR controller's equivalent of "mouse down"
152
174
controller . addEventListener ( 'selectstart' , ( event ) => {
153
175
// Use controller position and direction to cast a ray
154
- controllerRaycaster . setFromMatrixPosition ( controller . matrixWorld ) ;
176
+ controllerRaycaster . ray . origin . setFromMatrixPosition ( controller . matrixWorld ) ;
177
+ controllerRaycaster . ray . direction . set ( 0 , 0 , - 1 ) . applyQuaternion ( controller . quaternion ) ;
155
178
156
179
// Check for intersections with objects
157
180
const intersects = controllerRaycaster . intersectObjects ( particles , true ) ;
181
+
158
182
if ( intersects . length > 0 ) {
159
183
simulationEnabled = false ;
184
+
160
185
controllerSelectedObject = intersects [ 0 ] . object ;
186
+ controllerSelectedObject . updateColor ( 0x00ff00 ) ;
161
187
162
188
// Compute object offset and depth
163
189
controllerObjectOffset . copy ( intersects [ 0 ] . point ) . sub ( controllerSelectedObject . position ) ;
164
190
controllerObjectDepth = controller . position . distanceTo ( controllerSelectedObject . position ) ;
165
-
166
- console . log ( 'Controller grabbed:' , controllerSelectedObject ) ;
167
191
}
192
+
193
+ controllerIsSelecting = true ;
168
194
} ) ;
169
195
170
196
// VR controller's equivalent of "mouse move"
171
- controller . addEventListener ( 'squeezemove' , ( ) => {
172
- if ( controllerSelectedObject ) {
173
- const controllerIntersect = new THREE . Vector3 ( ) ;
174
- controllerRaycaster . setFromMatrixPosition ( controller . matrixWorld ) ;
175
- controllerRaycaster . ray . at ( controllerObjectDepth , controllerIntersect ) ;
176
-
177
- // Move object
178
- controllerSelectedObject . position . copy ( controllerIntersect . sub ( controllerObjectOffset ) ) ;
197
+ function handleControllerSelectMove ( ) {
198
+ if ( ! controllerSelectedObject ) {
199
+ return ;
179
200
}
180
- } ) ;
201
+
202
+ controllerRaycaster . ray . origin . setFromMatrixPosition ( controller . matrixWorld ) ;
203
+ controllerRaycaster . ray . direction . set ( 0 , 0 , - 1 ) . applyQuaternion ( controller . quaternion ) ;
204
+
205
+ // Calculate the new intersection point along the ray
206
+ const controllerIntersect = new THREE . Vector3 ( ) ;
207
+ controllerRaycaster . ray . at ( controllerObjectDepth , controllerIntersect ) ;
208
+
209
+ // Move object
210
+ controllerSelectedObject . position . copy ( controllerIntersect . sub ( controllerObjectOffset ) ) ;
211
+ }
181
212
182
213
// VR controller's equivalent of "mouse up"
183
214
controller . addEventListener ( 'selectend' , ( ) => {
184
- controllerSelectedObject = null ;
185
- simulationEnabled = true ;
215
+ controllerIsSelecting = false ;
216
+
217
+ if ( controllerSelectedObject ) {
218
+ controllerSelectedObject . resetColor ( ) ;
219
+ controllerSelectedObject = null ;
220
+
221
+ simulationEnabled = true ;
222
+ }
223
+ } ) ;
224
+
225
+ let xrEnabled = false ;
226
+
227
+ controller . addEventListener ( 'squeeze' , ( ) => {
228
+ xrEnabled = ! xrEnabled ;
229
+ resetParticleHeight ( ) ;
230
+
231
+ if ( xrEnabled ) {
232
+ skydome . visible = false ;
233
+ } else {
234
+ skydome . visible = true ;
235
+ }
186
236
} ) ;
237
+
238
+ function createLaserBeam ( controller ) {
239
+ const material = new THREE . LineBasicMaterial ( { color : 0xff0000 } ) ; // Red beam
240
+ const points = [ new THREE . Vector3 ( 0 , 0 , 0 ) , new THREE . Vector3 ( 0 , 0 , - 1 ) ] ; // Start & end
241
+ const geometry = new THREE . BufferGeometry ( ) . setFromPoints ( points ) ;
242
+ const laser = new THREE . Line ( geometry , material ) ;
243
+
244
+ laser . scale . z = 5 ; // Length of the beam
245
+ controller . add ( laser ) ;
246
+ return laser ;
247
+ }
248
+
249
+ //
250
+ // DEBUG UTILITY
251
+ //
252
+
253
+ // Track planes already created
254
+ const planeMap = new Map ( ) ;
255
+
256
+ function showXRPlaneWireframe ( frame , referenceSpace , detectedPlanes ) {
257
+ // Track which planes we've updated this frame
258
+ const updatedPlanes = new Set ( ) ;
259
+
260
+ detectedPlanes . forEach ( plane => {
261
+ if ( ! plane ) {
262
+ return ;
263
+ }
264
+
265
+ const pose = frame . getPose ( plane . planeSpace , referenceSpace ) ;
266
+ if ( ! pose ) {
267
+ return ;
268
+ }
269
+
270
+ // Use plane.planeSpace as a unique identifier
271
+ const planeId = plane . planeSpace ;
272
+ updatedPlanes . add ( planeId ) ;
273
+
274
+ if ( planeMap . has ( planeId ) ) {
275
+ // Update existing plane
276
+ updatePlaneTransform ( planeMap . get ( planeId ) , plane , pose ) ;
277
+ } else {
278
+ // Create new plane and store it
279
+ const visualPlane = createPlaneMeshFromXRPlane ( plane , pose ) ;
280
+ scene . add ( visualPlane ) ;
281
+ planeMap . set ( planeId , visualPlane ) ;
282
+ }
283
+ } ) ;
284
+
285
+ // Remove planes that weren't updated this frame
286
+ // (they might have disappeared from detection)
287
+ planeMap . forEach ( ( visualPlane , planeId ) => {
288
+ if ( ! updatedPlanes . has ( planeId ) ) {
289
+ scene . remove ( visualPlane ) ;
290
+ planeMap . delete ( planeId ) ;
291
+ }
292
+ } ) ;
293
+ }
294
+
295
+ function createPlaneMeshFromXRPlane ( xrPlane , pose ) {
296
+ // Get the plane dimensions
297
+ const geometry = new THREE . PlaneGeometry ( 1 , 1 ) ;
298
+
299
+ const material = new THREE . MeshBasicMaterial ( {
300
+ color : 0x44FF44 ,
301
+ transparent : true ,
302
+ opacity : 0.5 ,
303
+ side : THREE . DoubleSide ,
304
+ wireframe : true
305
+ } ) ;
306
+
307
+ const planeMesh = new THREE . Mesh ( geometry , material ) ;
308
+
309
+ // Apply initial transform
310
+ updatePlaneTransform ( planeMesh , xrPlane , pose ) ;
311
+
312
+ return planeMesh ;
313
+ }
314
+
315
+ function updatePlaneTransform ( visualPlane , xrPlane , pose ) {
316
+ // Extract position and orientation from pose
317
+ const position = pose . transform . position ;
318
+ const orientation = pose . transform . orientation ;
319
+
320
+ // Update position
321
+ visualPlane . position . set ( position . x , position . y , position . z ) ;
322
+
323
+ // Update rotation from quaternion
324
+ visualPlane . quaternion . set (
325
+ orientation . x ,
326
+ orientation . y ,
327
+ orientation . z ,
328
+ orientation . w
329
+ ) ;
330
+
331
+ // Apply any plane orientation corrections
332
+ // XR planes are typically Y-up, but Three.js might expect Z-up
333
+ visualPlane . rotateX ( - Math . PI / 2 ) ;
334
+ }
335
+
0 commit comments