I have this webxr code, it is detecting the floor (horizontal surface) correctly (after taking sometime) but its failing when I move my camera towards the wall. I am using chrome on andriod. What I am doing wrong? Is there a way to detect vertical surfaces.
WebXR AR Surface Detection
Start AR
let scene, camera, renderer, xrSession, xrRefSpace;
let hitTestSource = null;
let reticle = null;
let glbModel = null;
// Load GLB model
function loadModel() {
return new Promise((resolve, reject) => {
const loader = new THREE.GLTFLoader();
loader.load(
'ac.glb', // Replace with your GLB file path
(gltf) => {
glbModel = gltf.scene;
// Scale your model if needed
glbModel.scale.set(1, 1, 1);
resolve(glbModel);
},
(xhr) => {
console.log((xhr.loaded / xhr.total * 100) + '% loaded');
},
(error) => {
console.error('Error loading model:', error);
reject(error);
}
);
});
}
async function initAR() {
if (!navigator.xr) {
alert("WebXR not supported on this device!");
return;
}
try {
// Load model before starting AR
await loadModel();
const session = await navigator.xr.requestSession("immersive-ar", {
requiredFeatures: ["hit-test", "local-floor"],
optionalFeatures: ["dom-overlay"],
domOverlay: { root: document.body }
});
session.addEventListener("end", () => {
xrSession = null;
document.getElementById("startAR").style.display = "block";
});
session.addEventListener("select", placeObjectAtReticle);
xrSession = session;
document.getElementById("startAR").style.display = "none";
setupXRScene(session);
} catch (err) {
console.error("WebXR session failed:", err);
alert("Error starting AR: " + err.message);
}
}
function setupXRScene(session) {
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
const gl = canvas.getContext("webgl", { xrCompatible: true });
renderer = new THREE.WebGLRenderer({
alpha: true,
canvas: canvas,
context: gl
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true;
renderer.xr.setSession(session);
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 100);
// Improved reticle
reticle = new THREE.Group();
const reticleRing = new THREE.Mesh(
new THREE.RingGeometry(0.15, 0.2, 32),
new THREE.MeshBasicMaterial({
color: 0x00ff00,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.8,
depthTest: false
})
);
reticle.add(reticleRing);
reticle.visible = false;
scene.add(reticle);
// Multiple hit test sources
/*session.requestReferenceSpace("viewer").then((viewerRefSpace) => {
Promise.all([
session.requestHitTestSource({
space: viewerRefSpace,
offsetRay: new XRRay()
}),
session.requestHitTestSource({
space: viewerRefSpace,
offsetRay: new XRRay({
direction: { x: 0, y: -0.5, z: -1 }
})
}),
session.requestHitTestSource({
space: viewerRefSpace,
offsetRay: new XRRay({
direction: { x: 0, y: 0, z: -1 }
})
})
]).then(sources => {
hitTestSource = sources;
console.log("Hit test sources created successfully");
}).catch(err => {
console.error("Hit test source creation failed:", err);
});
});*/
////another method
session.requestReferenceSpace("viewer").then((viewerRefSpace) => {
Promise.all([
// Default downward hit test (for floors)
session.requestHitTestSource({
space: viewerRefSpace,
offsetRay: new XRRay({ origin: {x: 0, y: 0, z: 0}, direction: {x: 0, y: -0.5, z: -1} })
}),
// Forward ray for detecting walls
session.requestHitTestSource({
space: viewerRefSpace,
offsetRay: new XRRay({ origin: {x: 0, y: 1.5, z: 0}, direction: {x: 0, y: 0, z: -1} }) // Ray at eye level
})
]).then(sources => {
hitTestSource = sources;
console.log("Hit test sources created successfully");
}).catch(err => {
console.error("Hit test source creation failed:", err);
});
});
///////////
session.requestReferenceSpace("local").then((refSpace) => {
xrRefSpace = refSpace;
session.requestAnimationFrame(onXRFrame);
});
}
function onXRFrame(time, frame) {
frame.session.requestAnimationFrame(onXRFrame);
if (!frame || !xrRefSpace || !hitTestSource) return;
const pose = frame.getViewerPose(xrRefSpace);
if (pose) {
let hitPose = null;
// Check all hit test sources
for (let source of hitTestSource) {
const hitTestResults = frame.getHitTestResults(source);
if (hitTestResults.length > 0) {
hitPose = hitTestResults[0].getPose(xrRefSpace);
break;
}
}
if (hitPose) {
reticle.visible = true;
const matrix = new THREE.Matrix4();
matrix.fromArray(hitPose.transform.matrix);
const position = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
const scale = new THREE.Vector3();
matrix.decompose(position, quaternion, scale);
reticle.position.copy(position);
reticle.quaternion.copy(quaternion);
// Calculate surface orientation
const normal = new THREE.Vector3(0, 1, 0);
normal.applyQuaternion(quaternion);
const angleWithVertical = normal.angleTo(new THREE.Vector3(0, 1, 0)) * (180 / Math.PI);
// Update reticle color
const reticleMaterial = reticle.children[0].material;
if (angleWithVertical < 20) {
reticleMaterial.color.setHex(0x00ff00); // Green for horizontal
} else if (angleWithVertical > 70) {
reticleMaterial.color.setHex(0x0000ff); // Blue for vertical
} else {
reticleMaterial.color.setHex(0xffff00); // Yellow for angled
}
} else {
reticle.visible = false;
}
}
renderer.render(scene, camera);
}
function placeObjectAtReticle() {
if (!reticle.visible || !glbModel) return;
const modelClone = glbModel.clone();
modelClone.position.copy(reticle.position);
modelClone.quaternion.copy(reticle.quaternion);
scene.add(modelClone);
}
document.getElementById("startAR").addEventListener("click", initAR);
</script>