Skip to main content

World Query Module

Overview

The World Query Module enables you to place objects on real-world surfaces instantly and accurately. This lightweight solution is specifically designed for Spectacles' wearable platform.

Why Use World Query?

Traditional surface detection APIs like hitTestWorldMesh or DepthTexture.sampleDepthAtPoint can be computationally expensive for wearable devices. The World Query Module provides:

  • Instant placement: Fast hit testing for real surfaces
  • Lightweight performance: Optimized for wearable devices
  • Surface analysis: Depth and normal sampling at specific locations
  • Smoothing options: Results can be smoothed across multiple hits

How It Works

The World Query Hit Test performs ray casting against real surfaces by:

  1. Computing a depth map for the current view
  2. Intersecting the ray with depth data to find hit points
  3. Determining surface position and normal vectors
  4. Returning null if the ray falls outside the field of view

Performance Limitation

Due to the low, 5Hz update rate of the underlying depth data, the hit test works only for static or slowly moving objects. Consider use cases accordingly.

Basic Surface Placement

Setup Requirements

Before implementing World Query, ensure you have:

  1. Project configured for Spectacles with Interactive Preview enabled
  2. Spectacles Interaction Kit imported and initialized
  3. A target object to place on surfaces

Implementation Steps

Follow these steps to implement basic surface placement using the WorldQueryHit API:

Step 1: Create a new TypeScript file with the required imports

Step 2: Set up the hit test session with filtering options

Step 3: Handle hit test results and object placement

Step 4: Configure user interaction for spawning objects

Complete Example

// import required modules
const Interactor = require('SpectaclesInteractionKit.lspkg/Core/Interactor/Interactor');
const InteractorTriggerType = Interactor.InteractorTriggerType;
const InteractorInputType = Interactor.InteractorInputType;
const { SIK } = require('SpectaclesInteractionKit.lspkg/SIK');

const WorldQueryModule = require('LensStudio:WorldQueryModule');
const EPSILON = 0.01;

//@input int indexToSpawn
//@input SceneObject targetObject
//@input SceneObject[] objectsToSpawn
//@input bool filterEnabled

function WorldQueryHitExample() {
// Private variables
var primaryInteractor;
var hitTestSession;
var transform;
var lastHitResult; // Store last hit result for trigger end callback

function onAwake() {
// create new hit session
hitTestSession = createHitTestSession(script.filterEnabled);
if (!script.sceneObject) {
print('Please set Target Object input');
return;
}
transform = script.targetObject.getTransform();
// disable target object when surface is not detected
script.targetObject.enabled = false;
setObjectEnabled(script.indexToSpawn);

// Set up trigger end callback
setupTriggerEndCallback();

// create update event
script.createEvent('UpdateEvent').bind(onUpdate);
}

function setupTriggerEndCallback() {
// Get all interactors and set up trigger end callbacks
var allInteractors = SIK.InteractionManager.getInteractorsByType(
InteractorInputType.All
);

for (var i = 0; i < allInteractors.length; i++) {
var interactor = allInteractors[i];
interactor.onTriggerEnd.add(
(function (currentInteractor) {
return function () {
// Only place object if we have a valid hit result and this is the primary interactor
if (lastHitResult && primaryInteractor === currentInteractor) {
placeObject();
}
};
})(interactor)
);
}
}

function placeObject() {
if (!lastHitResult) return;

// Copy the plane/axis object
var parent = script.objectsToSpawn[script.indexToSpawn].getParent();
var newObject = parent.copyWholeHierarchy(
script.objectsToSpawn[script.indexToSpawn]
);
newObject.setParentPreserveWorldTransform(null);

// Set position and rotation from last hit
var hitPosition = lastHitResult.position;
var hitNormal = lastHitResult.normal;

var lookDirection;
if (1 - Math.abs(hitNormal.normalize().dot(vec3.up())) < EPSILON) {
lookDirection = vec3.forward();
} else {
lookDirection = hitNormal.cross(vec3.up());
}

var toRotation = quat.lookAt(lookDirection, hitNormal);
newObject.getTransform().setWorldPosition(hitPosition);
newObject.getTransform().setWorldRotation(toRotation);
}

function createHitTestSession(filterEnabled) {
// create hit test session with options
var options = HitTestSessionOptions.create();
options.filter = filterEnabled;

var session = WorldQueryModule.createHitTestSessionWithOptions(options);
return session;
}

function onHitTestResult(results) {
if (results === null) {
script.targetObject.enabled = false;
lastHitResult = null;
} else {
script.targetObject.enabled = true;
// Store hit result for potential trigger end callback
lastHitResult = results;

// get hit information
var hitPosition = results.position;
var hitNormal = results.normal;

//identifying the direction the object should look at based on the normal of the hit location.

var lookDirection;
if (1 - Math.abs(hitNormal.normalize().dot(vec3.up())) < EPSILON) {
lookDirection = vec3.forward();
} else {
lookDirection = hitNormal.cross(vec3.up());
}

var toRotation = quat.lookAt(lookDirection, hitNormal);
//set position and rotation
script.targetObject.getTransform().setWorldPosition(hitPosition);
script.targetObject.getTransform().setWorldRotation(toRotation);
}
}

function onUpdate() {
primaryInteractor =
SIK.InteractionManager.getTargetingInteractors().shift();
if (
primaryInteractor &&
primaryInteractor.isActive() &&
primaryInteractor.isTargeting()
) {
var rayStartOffset = new vec3(
primaryInteractor.startPoint.x,
primaryInteractor.startPoint.y,
primaryInteractor.startPoint.z + 30
);
var rayStart = rayStartOffset;
var rayEnd = primaryInteractor.endPoint;

hitTestSession.hitTest(rayStart, rayEnd, onHitTestResult);
} else {
script.targetObject.enabled = false;
}
}

function setObjectIndex(i) {
script.indexToSpawn = i;
}

function setObjectEnabled(indexToEnable) {
for (var i = 0; i < script.objectsToSpawn.length; i++) {
script.objectsToSpawn[i].enabled = i == indexToEnable;
}
}

// Initialize
onAwake();

// Expose public methods
script.setObjectIndex = setObjectIndex;
script.setObjectEnabled = setObjectEnabled;
}

// Register and initialize the script
script.WorldQueryHitExample = WorldQueryHitExample;
WorldQueryHitExample();

Integration Steps

  1. Save the script and add it to your scene
  2. Create a target object to place on surfaces
  3. Set the Target Object input in the script component
  4. Test in preview by moving the mouse and tapping, or send to Connected Spectacles

This example demonstrates the simplest method for spawning objects on surfaces using hand gestures (point to move, pinch to spawn).

Check out the World Query Hit - Spawn On Surface and Surface Detection assets in the Asset Library for ready-to-use implementations, or download them as unpacked packages from the GitHub repository.

Semantic Hit Testing

Ground Detection

World Query can identify specific surface types, such as ground surfaces. This enables automatic object placement based on surface classification.

Experimental Feature

You need to enable Experimental APIs in your project settings to use semantic hit testing functionality.

Classification Implementation

Enable semantic classification to detect ground surfaces:

// import required modules
const Interactor = require('SpectaclesInteractionKit.lspkg/Core/Interactor/Interactor');
const InteractorTriggerType = Interactor.InteractorTriggerType;
const InteractorInputType = Interactor.InteractorInputType;
const { SIK } = require('SpectaclesInteractionKit.lspkg/SIK');

const WorldQueryModule = require('LensStudio:WorldQueryModule');

function HitTestClassification() {
// Private variables
var hitTestSession;
var primaryInteractor;

function onAwake() {
hitTestSession = createHitTestSession();

script.createEvent('UpdateEvent').bind(onUpdate);
}

function createHitTestSession() {
var options = HitTestSessionOptions.create();
options.classification = true;

var session = WorldQueryModule.createHitTestSessionWithOptions(options);
return session;
}

function onHitTestResult(result) {
if (result === null) {
// Hit test failed
return;
}

var hitPosition = result.position;
var hitNormal = result.normal;
var hitClassification = result.classification;

switch (hitClassification) {
case SurfaceClassification.Ground:
print('Hit ground!');
break;
case SurfaceClassification.None:
print('Hit unknown surface!');
break;
}
}

function onUpdate() {
primaryInteractor =
SIK.InteractionManager.getTargetingInteractors().shift();
if (
primaryInteractor &&
primaryInteractor.isActive() &&
primaryInteractor.isTargeting()
) {
var rayStartOffset = new vec3(
primaryInteractor.startPoint.x,
primaryInteractor.startPoint.y,
primaryInteractor.startPoint.z + 30
);
var rayStart = rayStartOffset;
var rayEnd = primaryInteractor.endPoint;

hitTestSession.hitTest(rayStart, rayEnd, onHitTestResult);
}
}

// Initialize
onAwake();
}

// Register and initialize the script
script.HitTestClassification = HitTestClassification;
HitTestClassification();

Additional Resources

API Reference

Was this page helpful?
Yes
No