Commit 7d1e0011 authored by Kang's avatar Kang
Browse files

add comments for better understnading and add the camera button

parent 2761c9ae
// Import necessary tools and libraries
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import * as THREE from "three"; import * as THREE from "three";
import { XRButton } from "three/examples/jsm/webxr/XRButton"; import { XRButton } from "three/examples/jsm/webxr/XRButton";
import { BlockManager } from "./BlockManager"; import { BlockManager } from "./BlockManager"; // manages placed 3D objects in the scene
import { Cube } from "../model/Cube"; import { Cube } from "../model/Cube"; // custom class for a simple cube
import { GLTFModel } from "../model/GLTFModel"; import { GLTFModel } from "../model/GLTFModel"; // custom loader for .glb 3D models
import { ARBottomBar } from "./ARBottomBar"; import { ARBottomBar } from "./ARBottomBar"; // bottom UI for selecting objects
export const XRRenderer: React.FC = () => { export const XRRenderer: React.FC = () => {
const mountRef = useRef<HTMLDivElement>(null);
const overlayRef = useRef<HTMLDivElement>(null);
const [isSessionActive, setIsSessionActive] = useState(false); // References to DOM elements (we connect three.js canvas + overlay here)
const [selectedObject, setSelectedObject] = useState<string>("Cube"); const mountRef = useRef<HTMLDivElement>(null); // the div that holds the canvas
const overlayRef = useRef<HTMLDivElement>(null); // the overlay UI inside AR (buttons etc.)
const reticleRef = useRef<THREE.Mesh | null>(null);// reference to the green reticle ring
// State variables
const [isSessionActive, setIsSessionActive] = useState(false); // true when AR is running
const [selectedObject, setSelectedObject] = useState<string>("Cube"); // default object type
// The main setup logic — runs once when component mounts
useEffect(() => { useEffect(() => {
if (!mountRef.current || !overlayRef.current) return; if (!mountRef.current || !overlayRef.current) return;
// --- Scene setup --- // --- 1️⃣ Scene setup ---
const scene = new THREE.Scene(); const scene = new THREE.Scene();
scene.background = null; scene.background = null; // WebXR automatically shows camera feed behind 3D scene
// Camera: standard perspective camera
const camera = new THREE.PerspectiveCamera( const camera = new THREE.PerspectiveCamera(
70, 70, // field of view (in degrees)
window.innerWidth / window.innerHeight, window.innerWidth / window.innerHeight, // aspect ratio
0.01, 0.01, // near clipping plane
20 20 // far clipping plane
); );
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); // Renderer: where 3D things are drawn
const renderer = new THREE.WebGLRenderer({
antialias: true, // smooth edges
alpha: true, // allow transparency (so camera feed is visible)
preserveDrawingBuffer: true, // so we can take screenshots later
});
renderer.setSize(window.innerWidth, window.innerHeight); renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true; renderer.xr.enabled = true; // turn on WebXR support
renderer.xr.setReferenceSpaceType("local-floor"); renderer.xr.setReferenceSpaceType("local-floor"); // reference at floor level
mountRef.current.appendChild(renderer.domElement); mountRef.current.appendChild(renderer.domElement); // attach to our div
// --- AR Button --- // --- 2️⃣ Create AR Button (to enter AR mode) ---
const button = XRButton.createButton(renderer, { const button = XRButton.createButton(renderer, {
requiredFeatures: ["hit-test", "dom-overlay"], requiredFeatures: ["hit-test", "dom-overlay"], // needed to detect planes + show UI overlay
optionalFeatures: ["local-floor"], optionalFeatures: ["local-floor"], // extra feature (some devices only)
domOverlay: { root: overlayRef.current }, domOverlay: { root: overlayRef.current }, // attach HTML overlay (UI)
}); });
mountRef.current.appendChild(button); mountRef.current.appendChild(button);
// --- Lighting --- // --- 3️⃣ Add simple lighting ---
const light = new THREE.AmbientLight(0xffffff, 0.5); const light = new THREE.AmbientLight(0xffffff, 0.5); // soft white light
scene.add(light); scene.add(light);
// --- Block manager --- // --- 4️⃣ Block manager (handles placed 3D objects) ---
const blockManager = new BlockManager(scene); const blockManager = new BlockManager(scene);
// --- Placement indicator (reticle) --- // --- 5️⃣ Create reticle (green ring where we can place objects) ---
const reticleGeometry = new THREE.RingGeometry(0.1, 0.12, 32); const reticleGeometry = new THREE.RingGeometry(0.1, 0.12, 32);
const reticleMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); const reticleMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const reticle = new THREE.Mesh(reticleGeometry, reticleMaterial); const reticle = new THREE.Mesh(reticleGeometry, reticleMaterial);
reticle.rotation.x = -Math.PI / 2; reticle.rotation.x = -Math.PI / 2; // lay flat on the ground
reticle.visible = false; reticle.visible = false; // hidden until we detect a plane
scene.add(reticle); scene.add(reticle);
reticleRef.current = reticle; // save for later reference
// --- Hit-test setup --- // --- 6️⃣ WebXR hit-test setup (detect real-world planes) ---
let hitTestSource: XRHitTestSource | null = null; let hitTestSource: XRHitTestSource | null = null;
let localRefSpace: XRReferenceSpace | null = null; let localRefSpace: XRReferenceSpace | null = null;
// Called when AR session starts (when user clicks "Enter AR")
const sessionStartHandler = async () => { const sessionStartHandler = async () => {
setIsSessionActive(true); setIsSessionActive(true); // show bottom UI
const session = renderer.xr.getSession(); const session = renderer.xr.getSession();
// Reference space: maps AR coordinates to real world
localRefSpace = await session.requestReferenceSpace("local-floor"); localRefSpace = await session.requestReferenceSpace("local-floor");
// Viewer space: based on device’s camera position
const viewerSpace = await session.requestReferenceSpace("viewer"); const viewerSpace = await session.requestReferenceSpace("viewer");
// Hit-test source: sends out rays from device to find planes
hitTestSource = await session.requestHitTestSource({ space: viewerSpace }); hitTestSource = await session.requestHitTestSource({ space: viewerSpace });
}; };
// Called when AR session ends
const sessionEndHandler = () => { const sessionEndHandler = () => {
setIsSessionActive(false); setIsSessionActive(false);
hitTestSource = null; hitTestSource = null;
localRefSpace = null; localRefSpace = null;
reticle.visible = false; reticle.visible = false; // hide reticle when AR stops
}; };
// Listen to AR session start/end
renderer.xr.addEventListener("sessionstart", sessionStartHandler); renderer.xr.addEventListener("sessionstart", sessionStartHandler);
renderer.xr.addEventListener("sessionend", sessionEndHandler); renderer.xr.addEventListener("sessionend", sessionEndHandler);
// --- Handle resize --- // --- 7️⃣ Adjust to window size changes ---
const handleResize = () => { const handleResize = () => {
camera.aspect = window.innerWidth / window.innerHeight; camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
...@@ -86,14 +108,18 @@ export const XRRenderer: React.FC = () => { ...@@ -86,14 +108,18 @@ export const XRRenderer: React.FC = () => {
}; };
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
// --- Animation loop --- // --- 8️⃣ Animation loop (runs every frame) ---
renderer.setAnimationLoop((timestamp, frame) => { renderer.setAnimationLoop((timestamp, frame) => {
// Only run hit-test logic if AR session and frame exist
if (frame && hitTestSource && localRefSpace) { if (frame && hitTestSource && localRefSpace) {
// Check if there’s a detected surface (like a floor or table)
const hitTestResults = frame.getHitTestResults(hitTestSource); const hitTestResults = frame.getHitTestResults(hitTestSource);
if (hitTestResults.length > 0) { if (hitTestResults.length > 0) {
// Take the first hit and get its position in space
const hit = hitTestResults[0]; const hit = hitTestResults[0];
const pose = hit.getPose(localRefSpace); const pose = hit.getPose(localRefSpace);
if (pose) { if (pose) {
// Move the reticle to that position
reticle.visible = true; reticle.visible = true;
reticle.position.set( reticle.position.set(
pose.transform.position.x, pose.transform.position.x,
...@@ -102,20 +128,23 @@ export const XRRenderer: React.FC = () => { ...@@ -102,20 +128,23 @@ export const XRRenderer: React.FC = () => {
); );
} }
} else { } else {
// No surface found → hide the reticle
reticle.visible = false; reticle.visible = false;
} }
} }
// Render 3D scene on each frame
renderer.render(scene, camera); renderer.render(scene, camera);
}); });
// === Function to add object at reticle position === // --- 9️⃣ Add object to the scene when user selects one ---
const addSelectedObject = (name: string) => { const addSelectedObject = (name: string) => {
if (!reticle.visible) return; // don't place in mid-air if (!reticle.visible) return; // only place when reticle is visible
const position = reticle.position.clone(); const position = reticle.position.clone(); // get current reticle position
let objectToAdd; let objectToAdd;
// Choose what object to add based on selection
if (name === "Cube") { if (name === "Cube") {
objectToAdd = new Cube(position); objectToAdd = new Cube(position);
} else if (name === "Watermill") { } else if (name === "Watermill") {
...@@ -123,38 +152,109 @@ export const XRRenderer: React.FC = () => { ...@@ -123,38 +152,109 @@ export const XRRenderer: React.FC = () => {
} else if (name === "Tree") { } else if (name === "Tree") {
objectToAdd = new GLTFModel("/tree.glb", position, "/Textures/variation-a.png"); objectToAdd = new GLTFModel("/tree.glb", position, "/Textures/variation-a.png");
} else { } else {
objectToAdd = new Cube(position); objectToAdd = new Cube(position); // fallback
} }
// Add the object to the scene (via manager)
blockManager.addObject(objectToAdd); blockManager.addObject(objectToAdd);
}; };
// Expose function for debug // Expose function globally for debugging in browser console
// You can type `window.addSelectedObject("Cube")` to spawn manually
(window as any).addSelectedObject = addSelectedObject; (window as any).addSelectedObject = addSelectedObject;
// --- Cleanup --- // --- 🔚 Cleanup when component unmounts ---
return () => { return () => {
renderer.xr.removeEventListener("sessionstart", sessionStartHandler); renderer.xr.removeEventListener("sessionstart", sessionStartHandler);
renderer.xr.removeEventListener("sessionend", sessionEndHandler); renderer.xr.removeEventListener("sessionend", sessionEndHandler);
window.removeEventListener("resize", handleResize); window.removeEventListener("resize", handleResize);
renderer.dispose(); renderer.dispose(); // free GPU memory
mountRef.current?.removeChild(renderer.domElement); mountRef.current?.removeChild(renderer.domElement);
if (reticleRef.current === reticle) reticleRef.current = null;
};
}, []); // empty dependency array → runs only once
// --- 🔸 Screenshot function ---
const captureAndDownload = () => {
// Get the <canvas> element from the renderer
const canvas = (mountRef.current?.querySelector("canvas") ||
document.querySelector("canvas")) as HTMLCanvasElement | null;
if (!canvas || !overlayRef.current) return;
// Hide UI overlay for a clean capture
const overlayEl = overlayRef.current;
const prevPointer = overlayEl.style.pointerEvents;
const prevOpacity = overlayEl.style.opacity;
overlayEl.style.pointerEvents = "none";
overlayEl.style.opacity = "0";
// Hide reticle (optional)
const reticle = reticleRef.current;
const prevReticleVisible = reticle?.visible ?? false;
if (reticle) reticle.visible = false;
// Wait one frame → take screenshot
requestAnimationFrame(() => {
canvas.toBlob((blob) => {
// Restore UI and reticle visibility
overlayEl.style.pointerEvents = prevPointer;
overlayEl.style.opacity = prevOpacity;
if (reticle) reticle.visible = prevReticleVisible;
if (!blob) return;
// Create download link
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
const ts = new Date().toISOString().replace(/[:.]/g, "-");
a.href = url;
a.download = `ar-snapshot-${ts}.png`;
document.body.appendChild(a);
a.click(); // trigger download
a.remove();
URL.revokeObjectURL(url);
}, "image/png");
});
}; };
}, []);
// --- When user clicks an object in the bottom bar --- // --- 🔸 Handle user selection from bottom bar ---
const handleSelect = (name: string) => { const handleSelect = (name: string) => {
setSelectedObject(name); setSelectedObject(name); // update state
(window as any).addSelectedObject?.(name); (window as any).addSelectedObject?.(name); // add object immediately
}; };
// --- 🔸 Render layout ---
return ( return (
<div style={{ width: "100vw", height: "100vh", position: "relative" }}> <div style={{ width: "100vw", height: "100vh", position: "relative" }}>
{/* where the WebGL canvas mounts */}
<div ref={mountRef} style={{ width: "100%", height: "100%" }} /> <div ref={mountRef} style={{ width: "100%", height: "100%" }} />
{/* AR overlay that floats above 3D view */}
<div <div
ref={overlayRef} ref={overlayRef}
style={{ position: "absolute", width: "100%", height: "100%" }} style={{ position: "absolute", width: "100%", height: "100%" }}
> >
{/* 📷 Screenshot button (top-right corner) */}
<button
onClick={captureAndDownload}
aria-label="Save snapshot"
style={{
position: "absolute",
top: 12,
right: 12,
zIndex: 1000,
padding: "10px 12px",
borderRadius: 12,
border: "none",
background: "white",
fontSize: 18,
boxShadow: "0 2px 8px rgba(0,0,0,0.25)",
cursor: "pointer",
}}
>
📷
</button>
{/* Bottom bar appears only during active AR session */}
{isSessionActive && ( {isSessionActive && (
<ARBottomBar <ARBottomBar
objectOptions={[ objectOptions={[
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment