From ceeeee6a2909b1ca81ad4a6d3a456a26960c81fc Mon Sep 17 00:00:00 2001 From: Tezeroth Date: Tue, 10 Dec 2024 15:21:44 +0000 Subject: [PATCH] commented code --- ar-cursor.js | 81 +++++++++++++++++---- index.html | 189 +++++++++++++++++++++++++++++++++++++++++++------ main.js | 174 ++++++++++++++++++++++++++++++++++++--------- model-utils.js | 139 ++++++++++++++++++++++++++++++++---- 4 files changed, 503 insertions(+), 80 deletions(-) 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

Thumbstick Behaviour +
- + 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; }