Is there a simple way to figure out the name of the child mesh that was clicked in a gltf model? I am on 0.8.0 of aframe.
I tried the following -
HTML source:
<a-scene embedded="" raycaster-autorefresh cursor="rayOrigin: mouse">
<a-assets>
<a-asset-item id="male" src="../images/trying/scene.gltf">
</a-asset-item>
</a-assets>
<a-entity id="camera" camera="" position="0 1.6 0" look-controls wasd-controls>
</a-entity>
<a-entity gltf-model="#male" position="-14 -30 -125" rotation= "0 160 0" material-displacements></a-entity>
</a-scene>
Component source:
// first component
AFRAME.registerComponent('raycaster-autorefresh', {
init: function () {
var el = this.el;
this.el.addEventListener('object3dset', function () {
var cursorEl = el.querySelector('[raycaster]');
cursorEl.components.raycaster.refreshObjects();
});
}
});
// second component
var lastIndex = -1;
var colors = ['red', 'green', 'blue', 'black'];
AFRAME.registerComponent('material-displacements', {
init: function () {
this.el.addEventListener('object3dset', () => {
this.el.addEventListener('click', (evt) => {
console.log(evt.detail.intersection.object.name);
});
});
},
update: function () {
lastIndex = (lastIndex + 1) % colors.length;
console.log(lastIndex);
this.material = new THREE.MeshStandardMaterial({color: colors[lastIndex]});
const mesh = this.el.getObject3D('mesh');
console.log("mesh is", mesh);
console.log("material is", this.material);
if (mesh) {
mesh.traverse((node) => {
if (node.name==="bed_Wood_0") {
if (node.isMesh) node.material = this.material;
}
});
}
}
});
But, it only provides the name of the first child mesh. Similarly, evt.detail.intersection.point only provides the same x, y and z coordinates irrespective of where I click on the model.
My requirement is to be able to figure out which child mesh was clicked inside the gltf model
Similarly, evt.detail.intersection.point only provides the same x, y and z coordinates irrespective of where I click on the model.
This is a bug — update to A-Frame 0.8.2 per issue #3467. There should also be no need for raycaster-autorefresh in A-Frame 0.8+.
For the remaining issue of getting names from nodes attached to the one that was clicked, consider this code:
this.el.addEventListener('click', (evt) => {
var object = evt.detail.intersection.object;
// name of entity to which component is attached
console.log(this.el.getObject3D('mesh').name);
// name of object directly clicked
console.log(object.name);
// name of object's parent
console.log(object.parent.name);
// name of object and its children
object.traverse((node) => console.log(node.name));
});
All of the above will give names of nodes around the mesh that was clicked. Without a demo I can't guess which of them you expect to have clicked, or what nodes exist, so I think you'll need to experiment from there. Also see docs for THREE.Object3D.
Related
In Web AR,I need to animate atext component in aframe. How to achieve the text animation in aframe ? Not found any properties in atext component.
As far as I know, there is no simple way to treat the letters as separate entities. They're even not separate meshes - the component generates one geometry containing all letters.
Probably it would be better to create an animated model, or even to use an animated texture.
However, with a little bit javascript we can dig into the underlying THREE.js layer and split a text into separate letters.
One way would be to attach text components with a single letters to <a-entity> nodes.
Having <a-entity> nodes declared we can attach animations like we would to a normal a-frame entity (especially position / rotation ones). But there comes the issue of positioning:
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script>
AFRAME.registerComponent("splitter", {
init: function() {
// grab all <a-entity> letter nodes
const letter_nodes = document.querySelectorAll(".letter");
// grab the "text" configuration
const textData = this.el.getAttribute("text");
// create a temporary vector to store the position
const vec = new THREE.Vector3();
for (var i = 0; i < letter_nodes.length; i++) {
// set the "text" component in each letter node
letter_nodes[i].setAttribute('text', {
value: textData.value[i],
anchor: textData.align, // a-text binding
width: textData.width // a-text binding
})
// set position
vec.copy(this.el.getAttribute("position"));
vec.x += i * 0.1; // move the letters to the right
vec.y -= 0.2; // move them down
letter_nodes[i].setAttribute("position", vec)
}
}
})
</script>
<a-scene>
<!-- original text -->
<a-text value="foo" position="0 1.6 -1" splitter></a-text>
<!-- entities -->
<a-entity class="letter" animation="property: position; to: 0 1.2 -1; dur: 1000; dir: alternate; loop: true"></a-entity>
<a-entity class="letter" animation="property: rotation; to: 0 0 360; dur: 1000; dir: alternate; loop: true"></a-entity>
<a-entity class="letter"></a-entity>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
First of all - we need to get the original letters spacing, this is sloppy. From what I can tell, a-frames version of THREE.TextGeometry has a property visibleGlyphs, which has the position of the glyphs (as well as their heights, and offsets). We can use it to properly position our text.
Second of all - the position animation needs the global position. It would be better to input offsets, not target positions. To make it work, the text nodes could be child nodes of the .letter nodes.
A "generic" component could look like this:
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script>
AFRAME.registerComponent("text-splitter", {
init: function() {
const el = this.el;
// i'll use the child nodes as wrapper entities for letter entities
const letter_wrappers = el.children
// wait until the text component tells us that it's ready
this.el.addEventListener("object3dset", function objectset(evt) {
el.removeEventListener("object3dset", objectset); // react only once
const mesh = el.getObject3D("text") // grab the mesh
const geometry = mesh.geometry // grab the text geometry
// wait until the visibleGlyphs are set
const idx = setInterval(evt => {
if (!geometry.visibleGlyphs) return;
clearInterval(idx);
// we want data.height, data.yoffset and position from each glyph
const glyphs = geometry.visibleGlyphs
// do as many loops as there are <entity - glyph> pairs
const iterations = Math.min(letter_wrappers.length, glyphs.length)
const textData = el.getAttribute("text"); // original configuration
var text = textData.value.replace(/\s+/, "") // get rid of spaces
const letter_pos = new THREE.Vector3();
for (var i = 0; i < iterations; i++) {
// use the positions, heights, and offsets of the glyphs
letter_pos.set(glyphs[i].position[0], glyphs[i].position[1], 0);
letter_pos.y += (glyphs[i].data.height + glyphs[i].data.yoffset) / 2;
// convert the letter local position to world
mesh.localToWorld(letter_pos)
// convert the world position to the <a-text> position
el.object3D.worldToLocal(letter_pos)
// apply the text and position to the wrappers
const node = document.createElement("a-entity")
node.setAttribute("position", letter_pos)
node.setAttribute('text', {
value: text[i],
anchor: textData.align, // a-text binding
width: textData.width // a-text binding
})
letter_wrappers[i].appendChild(node)
}
// remove the original text
el.removeAttribute("text")
}, 100)
})
}
})
</script>
<a-scene>
<!-- child entities of the original a-text are used as letter wrappers -->
<a-text value="fo o" position="0 1.6 -1" text-splitter>
<a-entity animation="property: rotation; to: 0 0 360; dur: 1000; loop: true"></a-entity>
<a-entity animation="property: position; to: 0 0.25 0; dur: 500; dir: alternate; loop: true"></a-entity>
<a-entity animation="property: position; to: 0 -0.25 0; dur: 500; dir: alternate; loop: true"></a-entity>
</a-text>
<!-- just to see that the text is aligned properly-->
<a-text value="fo o" position="0 1.6 -1"></a-text>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
Here's an example of dynamically adding text entities + using anime.js to set up a timeline for each letter.
I'm trying to reparent an element (entity), keeping its position, rotation (and if possible, size, eg, scale) in the scene. Ideally, I would like to have a component (say "reparent") that when set on an entity, "moves" it to the specified parent, keeping the aspect of the entity in the scene. For example, for the next HTML code, the green entity would become a child of the red one, but would keep its position and rotation in the scene:
<a-scene id="scene" background="color: grey">
<a-entity id="red"
position="-1 3.5 -4" rotation="30 0 0" material="color: red"
geometry="primitive: cylinder"></a-entity>
<a-entity id="green" reparent="parent: #red"
position="-3 2 -4" rotation="0 45 0" material="color: green"
geometry="primitive: box"></a-entity>
</a-scene>
Apparently, this can be done at the three level using attach, but when I try to write a simple component using it, it doesn't work, apparently due to what the reparenting at the HTML does (I assume, because of AFrame).
I've written his test component to show the problem:
AFRAME.registerComponent('reparent', {
schema: {
parent: {type: 'selector', default: null},
},
init: function () {
const el = this.el;
const parent = this.data.parent;
move = function(evt) {
parent.object3D.attach(el.object3D);
console.log(el.object3D.parent.el.id, el.parentElement.id);
// parent.appendChild(el);
// console.log(el.object3D.parent.el.id, el.parentElement.id);
};
el.sceneEl.addEventListener('loaded', move);
}
});
When run, the result is red scene: the parent of object3D changed, but at the HTML level the parent of the element remains the scene. The green box appears as intended (in "-3 2 -4", world coordinates, and with the right rotation).
If I uncomment the two commented lines, I get a second line red red, but the green box disappears. In other words, now at the HTML the element got reparented, as intended, but somehow its object3D is not working anymore.
Any idea about why this fails, or some other idea for such a component?
All of this with AFrame 1.1.0.
After getting the advice that reusing the same element (entity) for reparenting is not a good idea (see Change parent of component and keep position), I explored the idea of copying the entity to be reparented in a new element under the new parent, and then remove the "old" one (instead of directly reparenting the entity). Granted, it is not the most clean hack in the world, but I hope it helps in my use case. So, I wrote a component:
AFRAME.registerComponent('reparent', {
schema: {
parent: {type: 'selector', default: null},
},
update: function () {
const el = this.el;
const parent = this.data.parent;
if (el.parentElement == parent) {
// We're already a child of the intended parent, do nothing
return;
};
// Reparent, once object3D is ready
reparent = function() {
// Attach the object3D to the new parent, to get position, rotation, scale
parent.object3D.attach(el.object3D);
let position = el.object3D.position;
let rotation = el.object3D.rotation;
let scale = el.object3D.scale;
// Create new element, copy the current one on it
let newEl = document.createElement(el.tagName);
if (el.hasAttributes()) {
let attrs = el.attributes;
for(var i = attrs.length - 1; i >= 0; i--) {
let attrName = attrs[i].name;
let attrVal = el.getAttribute(attrName);
newEl.setAttribute(attrName, attrVal);
};
};
// Listener for location, rotation,... when the new el is laded
relocate = function() {
newEl.object3D.location = location;
newEl.object3D.rotation = rotation;
newEl.object3D.scale = scale;
};
newEl.addEventListener('loaded', relocate, {'once': true});
// Attach the new element, and remove this one
parent.appendChild(newEl);
el.parentElement.removeChild(el);
};
if (el.getObject3D('mesh')) {
reparent();
} else {
el.sceneEl.addEventListener('object3dset', reparent, {'once': true});
};
}
});
You can check it works with the following HTML file:
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.1.0/aframe.min.js"></script>
<script src="reparent.js"></script>
</head>
<body>
<a-scene id="scene" background="color: grey">
<a-entity id="red"
position="1 2 -4" rotation="30 0 0" material="color: red"
geometry="primitive: cylinder"></a-entity>
<a-entity id="green" reparent="parent: #red"
position="-1 0 -4" rotation="0 45 0" material="color: green"
geometry="primitive: box"></a-entity>
<a-entity id="wf"
position="-1 0 -4" rotation="0 45 0" material="wireframe: true"
geometry="primitive: box"></a-entity>
</a-scene>
</body>
</html>
The wireframe entity is just to show how the green box stays exactly in the same position, although it was "reparented" under the red entity (explore the DOM to check that). As explained above, the green box is really a new one, cloned from the original one, which was then deleted by the component.
The component should work when its parent property is changed, allowing for dynamic reparenting when needed.
I have simple python based websocket server which passes video frames as base64 string to client.
My client code
var ws = new WebSocket("ws://127.0.0.1:5678/")
ws.onmessage = function (event) {
console.log("Event received: ", event.data.length)
let data = event.data.slice(0, -1);
document.getElementById('sky').setAttribute('src', "data:image/jpg;base64," + data);
};
When I use
//html
<img id="sky" src="" >
as image container, a "video" is produced.
But when I swtich to
<a-scene>
<a-assets>
<img id="sky" src="sky.png">
</a-assets>
<a-sky src="#sky"></a-sky>
</a-scene>
the image container get stuck at first frame, and although the src attribute of img element is updated, the image on web page doesn't change.
I've looked documentation and weren't able to find anything. What should I give attention?
You should access the <a-sky>'s material and texture and set the needsUpdate flag.
For example in a custom component:
AFRAME.registerComponent('foo', {
init: function() {
// wait until the object is loaded
this.el.addEventListener("loaded", e => {
// grab the mesh
var mesh = this.el.getObject3D("mesh")
// expose the material
this.material = mesh.material
})
},
tick: function() {
// when the material is exposed - set the update flag on each tick
if (!this.material) return;
this.material.map.needsUpdate = true
}
}
Canvas texture example here.
Or you can do this with a more direct approach, by accessing the material component:
// Providing the a-sky entity is loaded and ready
a_sky.components.material.material.map.needsUpdate = true
// Untangling it from the left to right:
// entity -> component list -> material component ->
// THREE.js Material object -> texture -> needsUpdate flag
I was trying to use a canvas as texture in my aframe project. I found some instructions here. It mentioned:
The texture will automatically refresh itself as the canvas changes.
However, I gave it a try today and the canvas could only be changed / updated in init function. Afterwards the update to canvas cannot be reflected. Here is my implementation:
module.exports = {
'canvas_component': {
schema: {
canvasId: { type: 'string' }
},
init: function () {
this.canvas = document.getElementById(this.data.canvasId);
this.ctx = this.canvas.getContext('2d');
this.ctx.fillStyle = "#FF0000";
this.ctx.fillRect(20, 20, 150, 100);
setTimeout(() => {
this.ctx.fillStyle = "#FFFF00";
this.ctx.fillRect(20, 20, 150, 100);
}, 2000);
}
}
The color change of of the texture was never changed. Is there anything I missed? Thank you so much for any advice.
I could never get it to work with those instructions (never checked out if bug or improper use though), but you can achieve the same with Three.js:
// assuming this is inside an aframe component
init: function() {
// we'll update this manually
this.texture = null
let canvas = document.getElementById("source-canvas");
// wait until the element is ready
this.el.addEventListener('loaded', e => {
// create the texture
this.texture = new THREE.CanvasTexture(canvas);
// get the references neccesary to swap the texture
let mesh = this.el.getObject3D('mesh')
mesh.material.map = this.texture
// if there was a map before, you should dispose it
})
},
tick: function() {
// if the texture is created - update it
if (this.texture) this.texture.needsUpdate = true
}
Check it out in this glitch.
Instead using the tick function, you could update the texture whenever you get any callback from changing the canvas (mouse events, source change).
The docs are out of date, I've made a pull request to update them. Here is the code that shows how to do it now:
src: https://github.com/aframevr/aframe/issues/4878
which points to: https://github.com/aframevr/aframe/blob/b164623dfa0d2548158f4b7da06157497cd4ea29/examples/test/canvas-texture/components/canvas-updater.js
We can quickly turn that into a component like this, for example:
/* global AFRAME */
AFRAME.registerComponent('live-canvas', {
dependencies: ['geometry', 'material'],
schema: {
src: { type: "string", default: "#id"}
},
init() {
if (!document.querySelector(this.data.src)) {
console.error("no such canvas")
return
}
this.el.setAttribute('material',{src:this.data.src})
},
tick() {
var el = this.el;
var material;
material = el.getObject3D('mesh').material;
if (!material.map) {
console.error("no material map")
this.el.removeAttribute('live-canvas')
return;
}
material.map.needsUpdate = true;
}
});
(remember to declate your components before your scene...)
usage:
<body>
<canvas id="example-canvas"></canvas>
<a-scene>
<a-box live-canvas="src:#example-canvas;"></a-box>
</a-scene>
</body>
live glitch code demo here:
https://glitch.com/edit/#!/live-canvas-demo?path=index.html%3A58%3A43
You can of course be more efficient than a tick handler if you just intentionally run the equivalent code manually whenever you update the canvas yourself, if that makes more sense / isn't happening frame-by-frame.
I'm working on a project for aframe in which at some point the user "enters" a room (really just getting teleported to a different point in the environment) on clicking on a button.
I have tried using Don McCurdy's checkpoint system as well as simple javascript, but I don't have much knowledge of js and would appreciate any help you could provide.
Example of what I have tried:
AFRAME.registerComponent('teleporter', {
init: function () {
var button = document.querySelector('#button');
var cam = document.querySelector('#camera');
button.addEventListener('click', function () {
cam.setAttribute('position', '10, 1.6, 10');
});
}
});
How about this:
You teleport to the button:
create a component, which will be attached to the button, and will teleport the camera to the button:
init: function () {
var cam = document.querySelector('a-camera');
var position = this.el.getAttribute("position");
this.el.addEventListener('click', function () {
cam.setAttribute('position', position);
});
}
fiddle here.
You specify where to teleport by defining a schema:
add a schema to the component:
AFRAME.registerComponent('teleporter', {
schema: {
position: {default: "0 0 0"},
},
init: function () {
var cam = document.querySelector('a-camera');
var position = this.data.position;
this.el.addEventListener('click', function () {
cam.setAttribute('position', position);
});
}
});
and use it like this:
<a-sphere position="5 1.25 -6" radius="1.25" color="#EFFD5E"
teleporter="position:5 1.6 -6"></a-sphere>
defining where You want the player to teleport after the click.
fiddle here.
Another approach would be very similar to your initial idea (where position should be without commas):
<a-box
position="0 2 2"
color="red"
class="clickable" onClick="document.querySelector('#cam').setAttribute('position','0 2 2')";
>
</a-box>
<a-box
position="10 2 2"
color="blue"
class="clickable" onClick="document.querySelector('#cam').setAttribute('position','10 2 2')";
>
</a-box>
Working example here:
http://curious-electric.com/aframe/teleport/