| Feature | Environment Raycast | Scene Understanding |
|---|---|---|
Room scanning required | No | Yes |
Works immediately | Yes | After scanning |
Provides surface position | Yes | Yes |
Provides surface normal | Yes | Yes |
Detects planes/meshes | No | Yes |
Semantic labels | No | Yes |
Best for | Instant placement | Rich scene data |
EnvironmentRaycastSystem - Core system that manages WebXR hit-test sourcesEnvironmentRaycastTarget - Component that positions an entity at raycast hit pointsRaycastSpace - Enum for ray source selection (Left, Right, Viewer, Screen)import {
World,
SessionMode,
EnvironmentRaycastTarget,
RaycastSpace,
} from '@iwsdk/core';
World.create(document.getElementById('scene-container'), {
xr: {
sessionMode: SessionMode.ImmersiveAR,
features: {
hitTest: { required: true }, // Enable WebXR hit-test
},
},
features: {
environmentRaycast: true, // Enable EnvironmentRaycastSystem
},
}).then((world) => {
// Create a reticle that follows the raycast
const reticleMesh = createReticleMesh(); // Your reticle geometry
const reticle = world.createTransformEntity(reticleMesh);
reticle.addComponent(EnvironmentRaycastTarget, {
space: RaycastSpace.Right, // Use right controller
maxDistance: 10, // Maximum raycast distance in meters
});
// The reticle now automatically:
// - Moves to where the controller points at real surfaces
// - Orients to match the surface normal
// - Hides when there's no hit
});
World.create(container, {
xr: {
sessionMode: SessionMode.ImmersiveAR,
features: {
hitTest: { required: true }, // Required for environment raycast
},
},
features: {
environmentRaycast: true,
},
});
hitTest WebXR feature must be enabled for environment raycasting to work.const previewMesh = createPreviewMesh();
const previewEntity = world.createTransformEntity(previewMesh);
previewEntity.addComponent(EnvironmentRaycastTarget, {
space: RaycastSpace.Right,
maxDistance: 10,
});
space - Ray source: RaycastSpace.Left, Right, Viewer, or Screen (default: Right)maxDistance - Maximum raycast distance in meters (default: 100)offsetPosition - Offset from ray origin (default: undefined)offsetQuaternion - Rotation offset for ray direction (default: undefined)xrHitTestResult - The underlying XRHitTestResult when there’s a hit, undefined otherwiseinputSource - For Screen mode: the XRInputSource that triggered the hit// Check if there's a valid hit
const xrResult = entity.getValue(EnvironmentRaycastTarget, 'xrHitTestResult');
if (xrResult) {
console.log('Hit detected at:', entity.object3D.position);
}
| Space | Description | Best For |
|---|---|---|
RaycastSpace.Right | Right controller’s target ray | Quest controller placement |
RaycastSpace.Left | Left controller’s target ray | Left-handed users |
RaycastSpace.Viewer | Head/gaze direction | Gaze-based placement |
RaycastSpace.Screen | Screen touch (phone AR) | Tap-to-place on phones |
import {
AssetManager,
createSystem,
EnvironmentRaycastTarget,
RaycastSpace,
} from '@iwsdk/core';
class PlacementSystem extends createSystem({
targets: { required: [EnvironmentRaycastTarget] },
}) {
private previewEntity: Entity | null = null;
init() {
// Create preview object
const previewMesh = AssetManager.getGLTF('myObject').scene.clone();
this.previewEntity = this.world.createTransformEntity(previewMesh);
this.previewEntity.addComponent(EnvironmentRaycastTarget, {
space: RaycastSpace.Right,
maxDistance: 10,
});
}
update() {
const triggerPressed = this.input.gamepads.right?.getSelectStart();
if (triggerPressed && this.previewEntity) {
const xrResult = this.previewEntity.getValue(
EnvironmentRaycastTarget,
'xrHitTestResult',
);
// Only place if there's a valid hit and preview is visible
if (xrResult && this.previewEntity.object3D?.visible) {
this.spawnObject(
this.previewEntity.object3D.position.clone(),
this.previewEntity.object3D.quaternion.clone(),
);
}
}
}
private spawnObject(position: Vector3, quaternion: Quaternion) {
const mesh = AssetManager.getGLTF('myObject').scene.clone();
mesh.position.copy(position);
mesh.quaternion.copy(quaternion);
this.scene.add(mesh);
this.world.createTransformEntity(mesh);
}
}
RaycastSpace.Screen to detect where the user taps:const reticle = world.createTransformEntity(reticleMesh);
reticle.addComponent(EnvironmentRaycastTarget, {
space: RaycastSpace.Screen, // Tracks screen touch
maxDistance: 10,
});
// In your system, check for the input source
class TapPlaceSystem extends createSystem({
targets: { required: [EnvironmentRaycastTarget] },
}) {
update() {
this.queries.targets.entities.forEach((entity) => {
const inputSource = entity.getValue(
EnvironmentRaycastTarget,
'inputSource',
);
const xrResult = entity.getValue(
EnvironmentRaycastTarget,
'xrHitTestResult',
);
// inputSource is set when user is touching the screen
if (inputSource && xrResult) {
// Place object at touch point
this.placeObject(entity.object3D.position.clone());
}
});
}
}
const reticle = world.createTransformEntity(reticleMesh);
reticle.addComponent(EnvironmentRaycastTarget, {
space: RaycastSpace.Viewer, // Uses head/gaze direction
maxDistance: 5,
});
// Left hand reticle
const leftReticle = world.createTransformEntity(leftMesh);
leftReticle.addComponent(EnvironmentRaycastTarget, {
space: RaycastSpace.Left,
});
// Right hand reticle
const rightReticle = world.createTransformEntity(rightMesh);
rightReticle.addComponent(EnvironmentRaycastTarget, {
space: RaycastSpace.Right,
});
hitTest: { required: true } in XR featuresenvironmentRaycast: true in World featuresmaxDistance isn’t too smallspace property matches your intended controllerRaycastSpace.Leftclass DebugSystem extends createSystem({
targets: { required: [EnvironmentRaycastTarget] },
}) {
update() {
this.queries.targets.entities.forEach((entity) => {
const xrResult = entity.getValue(
EnvironmentRaycastTarget,
'xrHitTestResult',
);
const visible = entity.object3D?.visible;
console.log({
hasHit: !!xrResult,
visible,
position: entity.object3D?.position,
});
});
}
}
EnvironmentRaycastTarget creates a WebXR hit-test sourceRaycastSpace for your input methodexamples/environment-raycast - AR plant placement with controller-based raycastingcd immersive-web-sdk pnpm install pnpm run build:tgz cd examples/environment-raycast npm install npm run dev