diff --git a/ar-cursor.js b/ar-cursor.js
index c31165b..04b8272 100644
--- a/ar-cursor.js
+++ b/ar-cursor.js
@@ -1,57 +1,114 @@
/* jshint esversion: 9 */
/* global THREE, AFRAME */
+
+/**
+ * This script defines an "ar-cursor" component for A-Frame that emulates a cursor in AR mode.
+ * In AR mode, the user doesn't have a traditional screen-space cursor or controller ray visible by default.
+ * This component listens for WebXR "select" events (like a tap on the screen in AR mode) and:
+ * - Casts a ray from the AR session's input source (like where the user tapped).
+ * - Finds intersected elements in the A-Frame scene under that ray.
+ * - Emits a "click" event on the first visible intersected element.
+ * - Cancels any ongoing AR hit-test so the user can place objects or interact directly.
+ *
+ * The component relies on A-Frame's raycaster component to do intersection tests.
+ * After the AR session is started and a select event occurs, it updates the raycaster's origin
+ * and direction based on the pose of the input source at the time of selection.
+ * It then performs intersection checks and triggers a click event on the first visible element found.
+ */
+
(function() {
"use strict";
+
+ // A reusable vector for direction calculations
const direction = new THREE.Vector3();
AFRAME.registerComponent("ar-cursor", {
- dependencies: ["raycaster"],
+ dependencies: ["raycaster"], // Requires the raycaster component on this entity
+
init() {
const sceneEl = this.el;
+
+ // When the scene enters VR (which includes AR mode), check if it's AR mode.
+ // If so, add an event listener for the 'select' event on the xrSession.
sceneEl.addEventListener("enter-vr", () => {
if (sceneEl.is("ar-mode")) {
+ // sceneEl.xrSession is the active WebXR session
sceneEl.xrSession.addEventListener("select", this.onselect.bind(this));
}
});
},
+
+ /**
+ * onselect(event):
+ *
+ * Called when the user performs a "select" action in AR mode (e.g., tapping the screen).
+ * This function:
+ * - Gets the XR frame and input source.
+ * - Obtains the pose of the input source's targetRaySpace relative to the scene's reference space.
+ * - Updates the raycaster origin and direction based on the pose's position and orientation.
+ * - Performs intersection checks with the scene to find any clickable elements.
+ * - If it finds a visible element, it emits a click event on that element.
+ * - It also disables the AR hit-test if one was running, presumably so the user can now interact.
+ */
onselect(e) {
- const frame = e.frame;
- const inputSource = e.inputSource;
- const referenceSpace = this.el.renderer.xr.getReferenceSpace();
+ const frame = e.frame; // The XRFrame at the time of select
+ const inputSource = e.inputSource; // The XRInputSource that triggered the select (e.g., AR screen tap)
+ const referenceSpace = this.el.renderer.xr.getReferenceSpace(); // The XR reference space in which poses are computed
+
+ // Get the pose of the input source (like a ray) from the current frame
const pose = frame.getPose(inputSource.targetRaySpace, referenceSpace);
- if (!pose) return;
+ if (!pose) return; // If no pose, we can't do anything
const transform = pose.transform;
+ // Set direction to (0,0,-1), which points "forward" in local space,
+ // then rotate this direction by the transform's orientation (quaternion)
+ // so it matches the direction the input source is pointing in AR.
direction.set(0, 0, -1);
direction.applyQuaternion(transform.orientation);
+
+ // Update the raycaster component with the new origin and direction derived from the pose.
+ // This effectively sets up a ray going from the input source forward into the scene.
this.el.setAttribute("raycaster", {
origin: transform.position,
- direction
+ direction: direction
});
+
+ // Perform the intersection test now that we've updated the raycaster's origin and direction.
this.el.components.raycaster.checkIntersections();
+
+ // Get the array of intersected elements, sorted by distance.
const els = this.el.components.raycaster.intersectedEls;
+
+ // Loop through intersected elements:
+ // We only want to "click" the first one that is actually visible (not hidden by a parent).
for (const el of els) {
const obj = el.object3D;
let elVisible = obj.visible;
+
+ // Check all ancestors in the scene graph to ensure none are invisible.
obj.traverseAncestors(parent => {
if (parent.visible === false ) {
- elVisible = false
+ elVisible = false;
}
});
+
if (elVisible) {
-
- // Cancel the ar-hit-test behaviours
+ // If the element is visible, we stop the AR hit-test.
+ // The 'ar-hit-test' component is often used to place objects on surfaces.
+ // Canceling it means we no longer update placement,
+ // allowing the user to interact with the scene normally.
this.el.components['ar-hit-test'].hitTest = null;
this.el.components['ar-hit-test'].bboxMesh.visible = false;
- // Emit click on the element for events
+ // Emit a "click" event on the intersected element,
+ // passing intersection details (like where the ray hit).
const details = this.el.components.raycaster.getIntersection(el);
el.emit('click', details);
- // Don't go to the next element
+ // Stop after the first visible element. We don't want multiple clicks on multiple elements.
break;
}
}
}
});
-})();
\ No newline at end of file
+})();
diff --git a/index.html b/index.html
index 7949f75..6797eae 100644
--- a/index.html
+++ b/index.html
@@ -2,11 +2,22 @@
+ // WebXR requires a secure context (i.e. https:), so if the page is not served
+ // over HTTPS (and is not localhost), we redirect it to use HTTPS.
+ // if (location.hostname !== 'localhost' && window.location.protocol === 'http:') window.location.protocol = 'https:';
+
-
+
@@ -21,9 +32,10 @@
-
+
+
AFrame Handy Demo
@@ -37,7 +49,21 @@
+
+
+
@@ -62,25 +93,40 @@
+
+
+
+
-
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
-
+
@@ -134,17 +205,28 @@
-
+
-
+
+
-
+
@@ -158,21 +240,47 @@
+
-
+
-
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
Hello World
@@ -233,13 +373,16 @@
Settings
-
+
diff --git a/main.js b/main.js
index 5b10f64..13eb8b3 100644
--- a/main.js
+++ b/main.js
@@ -1,24 +1,37 @@
/* jshint esversion: 9 */
/* global THREE, AFRAME */
+/**
+ * This component hides the entity when AR hit testing starts and shows it again when VR mode is exited.
+ * Useful for indicators or placeholders that you only want visible before the AR scene is anchored.
+ */
AFRAME.registerComponent("hide-on-hit-test-start", {
init: function() {
var self = this;
+ // Listen for the "ar-hit-test-start" event fired by the scene when AR hit testing begins.
this.el.sceneEl.addEventListener("ar-hit-test-start", function() {
+ // When AR hit test starts, hide this element.
self.el.object3D.visible = false;
});
+ // When exiting VR (or AR, since AR is a subset of VR modes), show the element again.
this.el.sceneEl.addEventListener("exit-vr", function() {
self.el.object3D.visible = true;
});
}
});
+/**
+ * This component resets the entity's position and rotation to the origin (0,0,0) when AR mode starts.
+ * It listens to the scene's "enter-vr" event and checks if we are in AR mode.
+ */
AFRAME.registerComponent("origin-on-ar-start", {
init: function() {
var self = this.el;
this.el.sceneEl.addEventListener("enter-vr", function() {
+ // "ar-mode" state is set by A-Frame when AR is activated.
if (this.is("ar-mode")) {
+ // Reset the entity's position and rotation so it aligns with the real-world AR starting point.
self.setAttribute('position', {x:0,y:0,z:0});
self.setAttribute('rotation', {x:0,y:0,z:0});
}
@@ -27,59 +40,89 @@ AFRAME.registerComponent("origin-on-ar-start", {
});
+/**
+ * This component updates the current entity's position and rotation (quaternion) to match another element or the XR camera.
+ * Useful to keep objects aligned with a specific reference, like the user's head (camera) or another object in the scene.
+ */
AFRAME.registerComponent("match-position-by-id", {
schema: {
- default: ''
+ default: '' // The ID of the element to match position from, or special 'xr-camera' keyword.
},
tick() {
let obj;
-
+
+ // Special case: if the data is 'xr-camera', try to get the actual XR camera pose.
if (this.data === 'xr-camera') {
const xrCamera = this.el.sceneEl.renderer.xr.getCameraPose();
if (xrCamera) {
+ // If we have an XR camera pose, copy its position and orientation directly.
this.el.object3D.position.copy(xrCamera.transform.position);
this.el.object3D.quaternion.copy(xrCamera.transform.orientation);
return;
}
+ // If no XR camera pose available, fallback to the scene's camera.
obj = this.el.sceneEl.camera;
} else {
+ // For any other ID, grab the object3D of that element.
obj = document.getElementById(this.data).object3D;
}
+
+ // If we found the object, copy its position and orientation.
if (obj) {
this.el.object3D.position.copy(obj.position);
this.el.object3D.quaternion.copy(obj.quaternion);
}
-
}
});
+/**
+ * This component makes the entity follow the camera's position in the scene.
+ * It uses camera's world position and transforms it into the parent's local space,
+ * effectively keeping the entity at the camera's position relative to its parent.
+ */
AFRAME.registerComponent("xr-follow", {
schema: {},
init() {
+ // No initialization logic needed right now.
},
tick() {
const scene = this.el.sceneEl;
const camera = scene.camera;
const object3D = this.el.object3D;
+
+ // Get camera's world position
camera.getWorldPosition(object3D.position);
+
+ // Convert that position into the local coordinate system of the parent to maintain relative positioning.
object3D.parent.worldToLocal(object3D.position);
}
});
+/**
+ * This component triggers an exit from VR/AR mode when a specified event occurs on the entity.
+ * By default, the event is "click", but you can specify another event in the schema.
+ */
AFRAME.registerComponent("exit-on", {
schema: {
- default: 'click'
+ default: 'click' // The event that will cause VR exit
},
update(oldEvent) {
const newEvent = this.data;
+ // Remove old event listener (if changed)
this.el.removeEventListener(oldEvent, this.exitVR);
+ // Add new event listener
this.el.addEventListener(newEvent, this.exitVR);
},
exitVR() {
+ // When the event fires, exit VR mode.
this.sceneEl.exitVR();
}
});
+/**
+ * This component sets a physx-body attribute on the entity once its model has loaded.
+ * Used to ensure physics is applied after the model is ready.
+ */
AFRAME.registerComponent("physx-body-from-model", {
schema: {
type: 'string',
@@ -87,31 +130,49 @@ AFRAME.registerComponent("physx-body-from-model", {
},
init () {
const details = this.data;
+ // On load event callback
this.onLoad = function () {
+ // Set the physx-body attribute using the given details string.
this.setAttribute('physx-body', details);
+ // Remove this component so it doesn't re-run or interfere later.
this.removeAttribute('physx-body-from-model');
- }
+ };
+ // Listen for when the underlying 3D object is set on the element.
this.el.addEventListener('object3dset', this.onLoad);
},
remove () {
+ // Cleanup the event listener if component is removed early.
this.el.removeEventListener('object3dset', this.onLoad);
}
});
+/**
+ * This component toggles physics states when items are picked up and put down.
+ * On 'pickup', it adds a 'grabbed' state.
+ * On 'putdown', it removes that state and applies the captured linear and angular velocities
+ * from the user's hand controllers (if available) to make the object continue with realistic motion.
+ */
AFRAME.registerComponent("toggle-physics", {
events: {
pickup: function() {
+ // Add a 'grabbed' state so physics can respond accordingly (like making it kinematic)
this.el.addState('grabbed');
},
putdown: function(e) {
+ // Remove the 'grabbed' state, return to dynamic behavior.
this.el.removeState('grabbed');
+
+ // If we have frame and inputSource details, we can extract pose velocities to apply them to the object.
if (e.detail.frame && e.detail.inputSource) {
const referenceSpace = this.el.sceneEl.renderer.xr.getReferenceSpace();
const pose = e.detail.frame.getPose(e.detail.inputSource.gripSpace, referenceSpace);
+
if (pose && pose.angularVelocity) {
+ // Set the object's angular velocity based on the user's hand movement when releasing.
this.el.components['physx-body'].rigidBody.setAngularVelocity(pose.angularVelocity, true);
}
if (pose && pose.linearVelocity) {
+ // Set the object's linear velocity to simulate throwing or letting go with some momentum.
this.el.components['physx-body'].rigidBody.setLinearVelocity(pose.linearVelocity, true);
}
}
@@ -119,61 +180,83 @@ AFRAME.registerComponent("toggle-physics", {
}
});
+/**
+ * This component simulates climbing a ladder in VR/AR by manipulating the cameraRig position based on the user's hand positions.
+ * When a hand "grabs" a ladder rung, movement constraints are adjusted so the user can "pull" themselves up.
+ * Releasing the ladder returns movement to normal navigation.
+ */
AFRAME.registerComponent("ladder", {
schema: {
cameraRig: {
- default: ''
+ default: '' // Selector for the camera rig element
},
grabbables: {
- default: ''
+ default: '' // CSS selector for elements that can be grabbed on the ladder (like ladder handles)
}
},
init () {
+ // Bind event handler methods to keep 'this' context.
this.ladderGrab = this.ladderGrab.bind(this);
this.ladderRelease = this.ladderRelease.bind(this);
+
+ // Store initial positions and arrays for multiple hands.
this.startingRigPosition = new THREE.Vector3();
this.startingHandPosition = new THREE.Vector3();
- this.ladderHands = [];
- this.grabbables = [];
+ this.ladderHands = []; // Hands currently on the ladder
+ this.grabbables = []; // List of ladder parts that can be grabbed
this.cameraRig = document.querySelector(this.data.cameraRig);
- if (this.data.grabbables) for (const el of this.el.querySelectorAll(this.data.grabbables)) {
- this.grabbables.push(el);
- el.addEventListener('grabbed', this.ladderGrab);
- el.addEventListener('released', this.ladderRelease);
+
+ // If there are grabbable elements specified, set them up to listen for grab events.
+ if (this.data.grabbables) {
+ for (const el of this.el.querySelectorAll(this.data.grabbables)) {
+ this.grabbables.push(el);
+ // Listen for 'grabbed' and 'released' custom events to handle ladder interactions.
+ el.addEventListener('grabbed', this.ladderGrab);
+ el.addEventListener('released', this.ladderRelease);
+ }
}
},
ladderRelease(e) {
+ // On release, remove the hand from the ladderHands array.
const oldActiveHand = e.detail.byNoMagnet;
let index;
while ((index=this.ladderHands.indexOf(oldActiveHand))!==-1) this.ladderHands.splice(index,1);
const activeHand = this.ladderHands[0];
if (activeHand) {
+ // If there's still another hand on the ladder, reset starting positions for the camera rig.
this.startingHandPosition.copy(activeHand.object3D.position);
this.startingRigPosition.copy(this.cameraRig.object3D.position);
} else {
- // Turn on the navmesh if no hands on the ladder
+ // If no hands remain on the ladder, re-enable normal navigation via navmesh constraint.
this.cameraRig.setAttribute('simple-navmesh-constraint', 'enabled', true);
}
},
ladderGrab(e) {
+ // On grab, store the current hand position and rig position as reference points.
const activeHand = e.detail.byNoMagnet;
this.startingHandPosition.copy(activeHand.object3D.position);
this.startingRigPosition.copy(this.cameraRig.object3D.position);
+ // Put this hand at the front of the ladderHands array
this.ladderHands.unshift(activeHand);
this.holdingLadder = true;
- // Turn off the navmesh if holding the ladder
+ // Disable navmesh constraints to allow free "pulling" movement.
this.cameraRig.setAttribute('simple-navmesh-constraint', 'enabled', false);
},
tick () {
+ // Each frame, if at least one hand is on the ladder, adjust the cameraRig position so the user can simulate climbing.
const activeHand = this.ladderHands[0];
if (activeHand) {
+ // Calculate cameraRig position by offsetting its original position by how the hand moves.
this.cameraRig.object3D.position.subVectors(this.startingHandPosition, activeHand.object3D.position);
+ // Rotate this offset by the rig's current orientation
this.cameraRig.object3D.position.applyQuaternion(this.cameraRig.object3D.quaternion);
+ // Add the starting rig position to preserve the original reference frame
this.cameraRig.object3D.position.add(this.startingRigPosition);
}
},
remove () {
+ // Cleanup: remove event listeners from grabbables
this.grabbables.forEach(el => {
el.removeEventListener('grabbed', this.ladderGrab);
el.removeEventListener('released', this.ladderRelease);
@@ -181,6 +264,8 @@ AFRAME.registerComponent("ladder", {
}
});
+
+// Once the DOM content is fully loaded, run this setup function.
window.addEventListener("DOMContentLoaded", function() {
const sceneEl = document.querySelector("a-scene");
const message = document.getElementById("dom-overlay-message");
@@ -188,11 +273,13 @@ window.addEventListener("DOMContentLoaded", function() {
const cameraRig = document.getElementById("cameraRig");
const building = document.getElementById("building");
- // Once the building has loaded update the relfections
+ // Once the building's 3D object is set, update reflections in the reflection component if present.
building.addEventListener('object3dset', function () {
if (this.components && this.components.reflection) this.components.reflection.needsVREnvironmentUpdate = true;
}, {once: true});
+ // Set up pose and gamepad event listeners for elements with class 'pose-label'
+ // These will update a text element with the current pose or gamepad event name.
const labels = Array.from(document.querySelectorAll('.pose-label'));
for (const el of labels) {
el.parentNode.addEventListener('pose', function (event) {
@@ -203,42 +290,54 @@ window.addEventListener("DOMContentLoaded", function() {
});
}
+ // Watergun logic block:
+ // The watergun can be grabbed from the body or the slider part.
+ // If grabbed from the body: adjusts classes and constraints accordingly.
+ // If grabbed from the slider: sets a linear constraint target so the slider can move.
watergun: {
const watergun = document.getElementById("watergun");
const watergunSlider = watergun.firstElementChild;
+
watergun.addEventListener('grabbed', function (e) {
const by = e.detail.by;
if (e.target === watergun) {
+ // If the main watergun body was grabbed:
watergun.className = '';
+ // Determine which hand grabbed it and assign the slider's magnet class to opposite hand type.
if (by.dataset.right) watergunSlider.className = 'magnet-left';
if (by.dataset.left) watergunSlider.className = 'magnet-right';
}
if (e.target === watergunSlider) {
+ // If slider is grabbed directly, set linear constraint to the grabbing hand's no-magnet element.
watergun.setAttribute('linear-constraint', 'target', '#' + e.detail.byNoMagnet.id);
}
});
+
watergun.addEventListener('released', function (e) {
const by = e.detail.by;
+ // On release, remove the linear constraint target.
watergun.setAttribute('linear-constraint', 'target', '');
if (e.target === watergun) {
+ // Reset classes when watergun body is released.
watergun.className = 'magnet-right magnet-left';
watergunSlider.className = '';
}
});
}
- // If the user taps on any buttons or interactive elements we may add then prevent
- // Any WebXR select events from firing
+ // If the user interacts with the DOM overlay (like pressing a button),
+ // we prevent any WebXR select events that might conflict with 3D selections.
message.addEventListener("beforexrselect", e => {
e.preventDefault();
});
+ // When entering VR mode, if we are entering AR mode specifically, show messages guiding the user through AR interactions.
sceneEl.addEventListener("enter-vr", function() {
if (this.is("ar-mode")) {
- // Entered AR
+ // Clear message initially
message.textContent = "";
- // Hit testing is available
+ // Once AR hit testing starts, show scanning message.
this.addEventListener(
"ar-hit-test-start",
function() {
@@ -247,7 +346,7 @@ window.addEventListener("DOMContentLoaded", function() {
{ once: true }
);
- // Has managed to start doing hit testing
+ // Once a suitable surface is found:
this.addEventListener(
"ar-hit-test-achieved",
function() {
@@ -256,11 +355,10 @@ window.addEventListener("DOMContentLoaded", function() {
{ once: true }
);
- // User has placed an object
+ // Once the user selects a surface and places an object:
this.addEventListener(
"ar-hit-test-select",
function() {
- // Object placed for the first time
message.textContent = "Well done!";
},
{ once: true }
@@ -268,28 +366,39 @@ window.addEventListener("DOMContentLoaded", function() {
}
});
+ // When exiting VR/AR, show a message.
sceneEl.addEventListener("exit-vr", function() {
message.textContent = "Exited Immersive Mode";
});
});
-// Make the cheap windows look okay
+/**
+ * This component replaces materials of objects whose material names match certain filters (like "Window")
+ * with a custom translucent, reflective material for a more visually appealing window effect.
+ */
AFRAME.registerComponent('window-replace', {
schema: {
- default: ''
+ default: '' // Comma-separated filters for material names to replace
},
init() {
+ // When the object3d is set, we can iterate through the mesh and replace materials if needed.
this.el.addEventListener('object3dset', this.update.bind(this));
this.materials = new Map();
},
update() {
+ // Split the filters by comma and trim spaces
const filters = this.data.trim().split(',');
+
+ // Traverse the object's 3D hierarchy.
this.el.object3D.traverse(function (o) {
if (o.material) {
+ // Check if the object's material name contains any of the filtered keywords
if (filters.some(filter => o.material.name.includes(filter))) {
+ // Set renderOrder to ensure correct rendering order for transparency.
o.renderOrder = 1;
const m = o.material;
const sceneEl = this.el.sceneEl;
+ // If we have replaced this material before, reuse it. Otherwise, create a new one.
o.material = this.materials.has(m) ?
this.materials.get(m) :
new THREE.MeshPhongMaterial({
@@ -298,25 +407,26 @@ AFRAME.registerComponent('window-replace', {
lightMapIntensity: m.lightMapIntensity,
shininess: 90,
color: '#ffffff',
- emissive: '#999999',
- emissiveMap: m.map,
+ emissive: '#999999', // Give it a subtle glow
+ emissiveMap: m.map, // Use original texture as emissive map
transparent: true,
depthWrite: false,
map: m.map,
transparent: true,
side: THREE.DoubleSide,
- get envMap() {return sceneEl.object3D.environment},
+ get envMap() {return sceneEl.object3D.environment}, // Dynamically fetch environment map for reflections
combine: THREE.MixOperation,
- reflectivity: 0.6,
+ reflectivity: 0.6, // Moderately reflective
blending: THREE.CustomBlending,
blendEquation: THREE.MaxEquation,
toneMapped: m.toneMapped
});
- ;
- window.mat = o.material;
+
+ window.mat = o.material; // For debugging: assign to global window object
+ // Cache the created material so we don't recreate it multiple times.
this.materials.set(m, o.material);
}
}
}.bind(this));
}
-});
\ No newline at end of file
+});
diff --git a/model-utils.js b/model-utils.js
index bf1daac..0d674d3 100644
--- a/model-utils.js
+++ b/model-utils.js
@@ -1,42 +1,89 @@
/* global AFRAME, THREE */
+/**
+ * lightmap component:
+ *
+ * This component allows you to apply a lightmap texture to certain materials on a model.
+ * A lightmap is a texture that stores pre-baked lighting information and can be used
+ * to add subtle illumination and shading to the model without the need for dynamic lights.
+ *
+ * Schema Fields:
+ * - src: The URL or reference to a texture map that will be used as the lightmap.
+ * - intensity: A multiplier for controlling how bright the lightmap appears on the model.
+ * - filter: A comma-separated string of material name filters. Only materials whose names
+ * match one of these filters will get the lightmap applied.
+ * - basis: (Not fully used in the provided code) A boolean that might indicate if the
+ * texture should be considered a Basis compressed texture. Here it's defaulted to false
+ * and not referenced in code.
+ * - channel: An integer indicating which channel (UV set or texture channel) to use for
+ * the lightmap. The code sets `texture.channel = this.data.channel` as a custom property,
+ * but the default is channel 1.
+ *
+ * How it works:
+ * - On component initialization, the texture is loaded and flipped vertically (Y flipped).
+ * The flipping is disabled (texture.flipY = false) likely because of how the model's
+ * UV coordinates are set up or how the texture was baked.
+ * - Once the object3D is set on the entity (i.e., the model has loaded), it traverses
+ * the scene graph starting from `this.el.object3D`, looking for meshes that have materials.
+ * - If a material's name matches any of the provided filters, a new MeshPhongMaterial is
+ * created to replace the original material, incorporating the loaded lightMap texture
+ * and adjusting parameters to mimic the original material's properties where possible.
+ *
+ * This approach creates a "cache" of replaced materials so that if the same original material
+ * is encountered again, it reuses the same replacement material, improving performance.
+ */
AFRAME.registerComponent('lightmap', {
schema: {
src: {
- type: "map"
+ type: "map" // A-Frame "map" type means it expects a texture or image URL
},
intensity: {
- default: 1
+ default: 1 // The brightness multiplier for the lightmap
},
filter: {
- default: ''
+ default: '' // Filters to match material names, comma-separated
},
basis: {
- default: false
+ default: false // Indicates if basis compression might be expected (not used here)
},
channel: {
type: 'int',
- default: 1
+ default: 1 // The UV channel or texture coordinate channel to use for the lightmap
}
},
init() {
-
+ // If this.data.src is a string or an object, normalize to a string URL for texture loading
const src = typeof this.data.src === 'string' ? this.data.src : this.data.src.src;
+
+ // Load the texture using Three.js TextureLoader
const texture = new THREE.TextureLoader().load(src);
- texture.flipY = false;
- texture.channel = this.data.channel;
+ texture.flipY = false; // Lightmaps often require no vertical flip to match UVs
+ texture.channel = this.data.channel; // Store channel info on the texture (custom property)
this.texture = texture;
+ // When the object's 3D model is set (e.g. once a-gltf-model finishes loading),
+ // call the update method to apply the lightmaps to the filtered materials.
this.el.addEventListener('object3dset', this.update.bind(this));
+
+ // Map to store original material -> replaced material associations
this.materials = new Map();
},
update() {
+ // Split filters by comma and trim spaces.
+ // Each filter should be a substring of the material name that triggers a replacement.
const filters = this.data.filter.trim().split(',');
+
+ // Traverse the entire object3D hierarchy
+ // to find meshes that match the material name filters
this.el.object3D.traverse(function (o) {
if (o.material) {
+ // Check if any filter matches the material name
if (filters.some(filter => o.material.name.includes(filter))) {
const sceneEl = this.el.sceneEl;
const m = o.material;
+
+ // If we have replaced this material before, reuse the cached version
+ // Otherwise, create a new MeshPhongMaterial with the lightmap.
o.material = this.materials.has(m) ? this.materials.get(m) : new THREE.MeshPhongMaterial({
name: 'phong_' + m.name,
lightMap: this.texture,
@@ -46,11 +93,15 @@ AFRAME.registerComponent('lightmap', {
transparent: m.transparent,
side: m.side,
depthWrite: m.depthWrite,
- reflectivity: m.metalness,
+ reflectivity: m.metalness, // Using metalness as reflectivity approximation
toneMapped: m.toneMapped,
- get envMap() {return sceneEl.object3D.environment}
+ // envMap retrieval as a getter so environment maps update dynamically if scene changes
+ get envMap() {
+ return sceneEl.object3D.environment;
+ }
});
-
+
+ // Cache the new material for any other objects referencing the same original material
this.materials.set(m, o.material);
}
}
@@ -58,50 +109,108 @@ AFRAME.registerComponent('lightmap', {
}
});
+
+/**
+ * depthwrite component:
+ *
+ * This component allows you to toggle whether the material should write to the depth buffer.
+ * Writing to the depth buffer controls whether objects properly occlude each other.
+ * For example, you might want to disable depth writing for certain transparent objects
+ * so they don't appear as solid or to create special visual effects.
+ *
+ * Schema:
+ * - A boolean (default true) that determines if the material should write to the depth buffer.
+ *
+ * On update, it traverses the entity's 3D object and applies the depthWrite setting to all materials.
+ */
AFRAME.registerComponent('depthwrite', {
schema: {
- default: true
+ default: true // Whether depth writing is enabled (true) or disabled (false)
},
init() {
+ // Once the object is ready, apply the depthWrite property
this.el.addEventListener('object3dset', this.update.bind(this));
},
update() {
this.el.object3D.traverse(function (o) {
if (o.material) {
+ // Set each material's depthWrite property based on the component's data
o.material.depthWrite = this.data;
}
}.bind(this));
}
});
+
+/**
+ * hideparts component:
+ *
+ * This component allows you to hide specific named meshes within a model.
+ *
+ * Schema:
+ * - A comma-separated list of mesh names that should be hidden.
+ *
+ * How it works:
+ * - When the model is loaded, it traverses the entity's object3D.
+ * - If a mesh's name is included in the filter list, that mesh is set to invisible (visible = false).
+ *
+ * This is useful for removing certain parts of a model without editing the model itself.
+ */
AFRAME.registerComponent('hideparts', {
schema: {
- default: ""
+ default: "" // Comma-separated mesh names to hide
},
init() {
+ // Once the object's meshes are set, update the visibility
this.el.addEventListener('object3dset', this.update.bind(this));
},
update() {
+ // Split the input string into an array of part names
const filter = this.data.split(',');
+
+ // Traverse the entire object hierarchy
this.el.object3D.traverse(function (o) {
+ // Check if this object is a mesh and if its name is in the filter list
if (o.type === 'Mesh' && filter.includes(o.name)) {
+ // Hide the matched meshes
o.visible = false;
}
}.bind(this));
}
});
-AFRAME.registerComponent('no-tonemapping', {
+
+/**
+ * no-tonemapping component:
+ *
+ * This component sets toneMapped = false on certain materials.
+ * Tone mapping is a process that adjusts the brightness of the scene to fit into
+ * the display range. Sometimes you don't want certain objects to be affected by tone mapping,
+ * for example, UI elements or special effects.
+ *
+ * Schema:
+ * - A comma-separated string of material name filters. If a material's name matches
+ * one of these filters, that material's toneMapped property is set to false.
+ *
+ * How it works:
+ * - On object load, it checks all materials and, if they match the filters,
+ * sets toneMapped to false.
+ */
+AFRAME.register-component('no-tonemapping', {
schema: {
- default: ''
+ default: '' // Filters to match material names
},
init() {
+ // Apply once the object's materials are ready
this.el.addEventListener('object3dset', this.update.bind(this));
},
update() {
+ // Split filters by comma and trim spaces
const filters = this.data.trim().split(',');
+
this.el.object3D.traverse(function (o) {
if (o.material) {
+ // If any filter matches the material name, disable tone mapping
if (filters.some(filter => o.material.name.includes(filter))) {
o.material.toneMapped = false;
}