Skip to content

Commit 61e3c8c

Browse files
committed
Finish basic gravity simulation
1 parent 1fe07c0 commit 61e3c8c

File tree

5 files changed

+358
-82
lines changed

5 files changed

+358
-82
lines changed

main.js

+184-35
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import * as THREE from 'three';
22
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';
44
import GradientSkydome from './src/GradientSkydome';
5+
import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
56

67
// Set up camera
78
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.01, 5000 );
@@ -11,9 +12,11 @@ camera.position.set( 0, 2, 5 );
1112
const renderer = new THREE.WebGLRenderer( { antialias: true } );
1213
renderer.xr.enabled = true;
1314
renderer.setSize( window.innerWidth, window.innerHeight );
14-
renderer.setAnimationLoop( animateVR ); // Start running animation loop
15+
renderer.setAnimationLoop( animateWeb ); // Start running animation loop
1516

16-
const sessionInit = { requiredFeatures: ["plane-detection"] };
17+
const sessionInit = { requiredFeatures: [
18+
"plane-detection"
19+
] };
1720

1821
// Append DOM elements
1922
document.body.appendChild( renderer.domElement );
@@ -31,10 +34,10 @@ const light = new THREE.DirectionalLight( 0xffffff, 1 );
3134
light.position.set( 1, 1, 1 );
3235
scene.add( light );
3336

34-
const particles = setupSimulation( scene );
37+
const [ plane, particles ] = setupSimulation( scene );
3538
let simulationEnabled = true;
3639

37-
function animateVR() {
40+
function animateWeb() {
3841
if ( simulationEnabled ) {
3942
updateSimulationVR();
4043
}
@@ -51,11 +54,16 @@ renderer.xr.addEventListener('sessionstart', () => {
5154
return;
5255
}
5356

57+
resetParticleHeight();
5458
setupXRSession( session );
5559
} );
5660

5761
function setupXRSession( session ) {
5862
renderer.setAnimationLoop( ( timestamp, frame ) => {
63+
if ( controllerIsSelecting ) {
64+
handleControllerSelectMove();
65+
}
66+
5967
if ( frame && simulationEnabled ) {
6068
onXRFrame( timestamp, frame );
6169
}
@@ -64,20 +72,16 @@ function setupXRSession( session ) {
6472
}
6573

6674
function onXRFrame( timestamp, frame ) {
67-
const detectedPlanes = frame.detectedPlanes;
6875
const referenceSpace = renderer.xr.getReferenceSpace();
76+
const detectedPlanes = frame.detectedPlanes;
77+
78+
// showXRPlaneWireframe( frame, referenceSpace, detectedPlanes );
6979

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+
}
8185
}
8286

8387
//
@@ -102,11 +106,15 @@ window.addEventListener( 'mousedown', ( event) => {
102106
const intersects = mouseRaycaster.intersectObjects( particles, true );
103107
if ( intersects.length > 0 ) {
104108
simulationEnabled = false;
109+
105110
mouseSelectedObject = intersects[ 0 ].object;
111+
mouseSelectedObject.updateColor( 0x00ff00 );
106112

107-
//
113+
// Notes:
108114
// intersects[0].point: the exact 3D world coordinates where the ray hits the object
109115
// intersects[0].object.position: the position of the object’s origin
116+
117+
//
110118
mouseObjectOffset.copy( intersects[0].point ).sub( mouseSelectedObject.position );
111119

112120
mouseObjectDepth = camera.position.distanceTo( mouseSelectedObject.position );
@@ -132,55 +140,196 @@ window.addEventListener( 'mousemove', ( event) => {
132140
} );
133141

134142
window.addEventListener( 'mouseup', () => {
135-
mouseSelectedObject = null;
136-
simulationEnabled = true;
143+
if ( mouseSelectedObject ) {
144+
mouseSelectedObject.resetColor();
145+
mouseSelectedObject = null;
146+
147+
simulationEnabled = true;
148+
}
137149
} );
138150

139151
//
140152
// VR CONTROLLER EVENTS
141153
//
142154

143-
const controller = renderer.xr.getController( 0 ); // Get the first controller
155+
const controller = renderer.xr.getController( 1 ); // Right controller
144156
scene.add( controller );
145157

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+
146167
const controllerRaycaster = new THREE.Raycaster();
147168
let controllerSelectedObject = null;
148169
let controllerObjectOffset = new THREE.Vector3();
149170
let controllerObjectDepth = 0;
171+
let controllerIsSelecting = false;
150172

151173
// VR controller's equivalent of "mouse down"
152174
controller.addEventListener( 'selectstart', ( event ) => {
153175
// 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 );
155178

156179
// Check for intersections with objects
157180
const intersects = controllerRaycaster.intersectObjects( particles, true );
181+
158182
if ( intersects.length > 0 ) {
159183
simulationEnabled = false;
184+
160185
controllerSelectedObject = intersects[ 0 ].object;
186+
controllerSelectedObject.updateColor( 0x00ff00 );
161187

162188
// Compute object offset and depth
163189
controllerObjectOffset.copy( intersects[ 0 ].point ).sub( controllerSelectedObject.position );
164190
controllerObjectDepth = controller.position.distanceTo( controllerSelectedObject.position );
165-
166-
console.log( 'Controller grabbed:', controllerSelectedObject );
167191
}
192+
193+
controllerIsSelecting = true;
168194
} );
169195

170196
// 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;
179200
}
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+
}
181212

182213
// VR controller's equivalent of "mouse up"
183214
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+
}
186236
} );
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+

src/Particle.js

+22-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import * as THREE from 'three';
22

33
export default class Particle extends THREE.Mesh {
44
constructor( options = {} ) {
5-
// Default options that can be overridden
65
const {
76
position = [0, 0, 0],
87
radius = 0.05,
@@ -19,7 +18,27 @@ export default class Particle extends THREE.Mesh {
1918

2019
super( geometry, material );
2120
this.position.set( ...position );
22-
this.velocity = velocity;
2321
this.offset = radius;
22+
this.color = color;
23+
this.velocity = velocity;
24+
this.boundingSphere = new THREE.Sphere( this.position, radius );
25+
this.frustumCulled = false;
26+
}
27+
28+
updateColor( newColor ) {
29+
this.material.color.set( newColor );
30+
}
31+
32+
resetColor() {
33+
this.material.color.set( this.color );
34+
}
35+
36+
collidesWith( rectangle ) {
37+
if ( !this.boundingSphere || !rectangle.boundingBox ) {
38+
console.warn( "Bounding volumes are undefined!" );
39+
return false;
40+
}
41+
42+
return this.boundingSphere.intersectsBox( rectangle.boundingBox );
2443
}
25-
}
44+
}

src/Rectangle.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as THREE from 'three';
2+
3+
export default class Rectangle extends THREE.Mesh {
4+
constructor( options = {} ) {
5+
const {
6+
position = [0, 0, 0],
7+
width = 5,
8+
height = 5,
9+
color = 0xff0000
10+
} = options;
11+
12+
// const geometry = new THREE.PlaneGeometry( width, height );
13+
const geometry = new THREE.BoxGeometry(width, 0.1, height);
14+
const material = new THREE.MeshStandardMaterial( {
15+
color,
16+
side: THREE.DoubleSide
17+
} )
18+
19+
super( geometry, material );
20+
// this.rotation.x = -Math.PI / 2; // Rotate to lie flat on the XZ plane (horizontal)
21+
this.position.set( ...position );
22+
this.color = color;
23+
24+
this.boundingBox = new THREE.Box3().setFromObject( this );
25+
}
26+
27+
updateColor( newColor ) {
28+
this.material.color.set( newColor );
29+
}
30+
31+
resetColor() {
32+
this.material.color.set( this.color );
33+
}
34+
}

0 commit comments

Comments
 (0)