AFrame: reparenting an element keeping its world position, rotation - javascript

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.

Related

Is there any way for animating the text (atext) in aframe [WebAR]?

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.

Using an Object's postion for an Event in AFrame

I'm trying to make a component that checks the current position of a sphere in an AFrame scene and when it hits a specific coordinate and when it does it fires an event (In example below it resets it to its default position):
AFRAME.registerComponent("trackball", {
update: function() {
let pos = this.el.getAttribute("position");
if (pos == {x:-21.821,y: 1,z: 0})
{
this.el.setAttribute("position", "-21.821 5 0");
}
}
});
I'm not sure what format is returned when .getAttribute("position") is called so that may be why it's not working. I am running AFrame 1.1.0.
First of all update is called when attributes are changed via setAttribute(). If you want a function that is called on each render frame, then use tick().
Secondly, try using a range, instead of a fixed point, it's very likely that the object will move past the point between two ticks.
Something like this:
<script src="https://aframe.io/releases/1.1.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/n5ro/aframe-physics-system#v4.0.1/dist/aframe-physics-system.min.js"></script>
<script>
AFRAME.registerComponent("trackball", {
tick: function() {
let pos = this.el.getAttribute("position");
if (pos.y < 0.5) {
// reset position
this.el.setAttribute("position", "0 3 -4")
// sync
this.el.components["dynamic-body"].syncToPhysics();
}
}
});
</script>
<a-scene physics cursor="rayOrigin: mouse">
<a-sphere position="0 1.25 -5" radius="0.25" color="#EF2D5E" dynamic-body trackball></a-sphere>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" static-body></a-plane>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
Also try using the object3D properties instead setAttribute() and getAttribute() when dealing with frequently called functions (which certainly applies to tick()):
<script src="https://aframe.io/releases/1.1.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/n5ro/aframe-physics-system#v4.0.1/dist/aframe-physics-system.min.js"></script>
<script>
AFRAME.registerComponent("trackball", {
// iife to initialize the "origin point" once
tick: (function() {
const origin = new THREE.Vector3(0, 3, -4);
const y_range = 0.5;
return function() {
// check if the ball is out of range
const pos = this.el.object3D.position
if (pos.y < y_range) {
// reset position
pos.copy(origin);
// sync
this.el.components["dynamic-body"].syncToPhysics();
}
}
})()
});
</script>
<a-scene physics cursor="rayOrigin: mouse">
<a-sphere position="0 1.25 -4" radius="0.25" color="#EF2D5E" dynamic-body trackball></a-sphere>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" static-body></a-plane>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
Keep in mind, updating the position in such manner is more performant, but will cause getAttribute("position") to return the last position set via setAttribute("position", new_position)

A-FRAME Different Components Are Being Called At The Same Time

I have successfully implemented the ability to click on different markers and make their object scale or rotate every time you click on them.
My problem is, when I have two different markers with two different components they are both being called on each click. It baffles me. Why would they both be being called?
UPDATE!!!
I have not fixed the problem but I have narrowed down to what I believe is causing it.
I have multiple markers printed out and hanging around my office. When I focus my camera on the different markers they are basically centered on my screen. If I center the marker on my camera, both markers are on the same spot. And, they both fire.
If I have print outs of two markers side by side and both are on my screen at the same time then if I click one they click independently of each other.
If I use my hands to move the print outs and lay one marker on top of the other. Then they go back to both firing at the same time.
It definitely has something to do with the area of the screen the marker take up.
I hope that makes sense.
Is there a way to "clear" or "refresh" things when a marker is found and lost?
Ultimate goal is to have a user walk down a hallway with barcodes on the wall so that they can see virtual paintings. The user is definitely going to focus in on the barcode, so the above issue is going to be present.
Any advice is appreciated!
Here is my JavaScript code:
AFRAME.registerComponent('marker-image-click', {
init: function() {
const objImageImage = document.querySelector('#image-image');
let intRotationX = objImageImage.getAttribute('rotation').x;
let intRotationY = objImageImage.getAttribute('rotation').y;
let intRotationZ = objImageImage.getAttribute('rotation').z;
objImageImage.addEventListener('click', function(ev, target) {
if (objImageImage.object3D.visible === true) {
console.log('click-image');
objImageImage.setAttribute('rotation', {x: intRotationX, y: intRotationY, z: intRotationZ});
intRotationX += 6.0;
intRotationY += 6.0;
intRotationZ += 6.0;
}
});
}
});
AFRAME.registerComponent('marker-avocado-click', {
init: function() {
const objEntityAvocado = document.querySelector('#entity-avocado');
let intScaleX = objEntityAvocado.getAttribute('scale').x;
let intScaleY = objEntityAvocado.getAttribute('scale').y;
let intScaleZ = objEntityAvocado.getAttribute('scale').z;
objEntityAvocado.addEventListener('click', function(ev, target) {
if (objEntityAvocado.object3D.visible === true) {
console.log('click-avocado');
objEntityAvocado.setAttribute('scale', {x: intScaleX, y: intScaleY, z: intScaleZ});
intScaleX += 0.25;
intScaleY += 0.25;
intScaleZ += 0.25;
}
});
}
});
Here is my HTML code:
<a-marker marker-image-lostfound marker-image-click id="marker-image" type="barcode" value="2" emitevents="true">
<a-entity cursor="rayOrigin: mouse"
raycaster="objects: .clickable-image; useWorldCoordinates: true;"> <!-- must add this for clicking -->
<a-image id="image-image" class="clickable-image"
src="images/test.png"
position="0 0 0" rotation="-90 0 0" scale="1 1 1"
width="" height="" opacity="1"> <!-- note the class="clickable" for clicking -->
</a-image>
</a-marker>
<a-marker marker-avocado-lostfound marker-avocado-click id="marker-avocado" type="barcode" value="3" emitevents="true">
<a-entity cursor="rayOrigin: mouse"
raycaster="objects: .clickable-avocado; useWorldCoordinates: true;"> <!-- must add this for clicking -->
</a-entity>
<a-entity id="entity-avocado" class="clickable-avocado"
gltf-model="models/Avocado/Avocado.gltf"
position="0 0 0.3" rotation="-90 0 0" scale="10 10 10"> <!-- note the class="clickable" for clicking -->
</a-entity>
</a-marker>

Registered Component click event in Aframe Version 0.8.2 and master isn't working

I was using this exact code in the previous version of Aframe 0.8.0 And the clicking was working normally.
However, when I tried using the 0.8.2 or the master version it showed no signs of clicking and without showing any errors
This is my registered component which I would like to detect clicks on the entities it is attached to
AFRAME.registerComponent('change-color-on-hover', {
init: function () {
var data = this.data;
var el = this.el; // <a-box>
var defaultColor = el.getAttribute('material').color;
/**
* Attach 'click' event
*/
el.addEventListener('click', function () {
alert('clicked');
});
}});
The Element where I want to detect the click (I am setting its position later and they it's appearing where they should be)
<a-image position="" src="#blue_target_rendered" height="30" width="40" depth="1" shadow event-set__click="_event: click; color:black" change-color-on-hover look-at="[camera]" id="hotspot-{{$hotspot->id}}" data-link-to="{{$hotspot->link_to}}" data-link-from="{{$hotspot->link_from}}">
With a setup like this:
<a-entity id="cameraParent" position="0 0 0" >
<a-entity id="cam" camera="zoom:1;" look-controls collider-check position="0 0 0" >
<a-entity cursor=" rayOrigin: mouse" geometry="primitive: ring; radiusInner: 0; radiusOuter: 0" material="color: black; shader: flat"></a-entity>
<a-entity raycaster="showLine: true; far: 1000"></a-entity>
</a-entity>
</a-entity>
The line component created by the raycaster is interfering with the rays emitted by the cursor = rayOrigin: mouse. In other words, you're clicking on the element with the line on each click.
If you reposition the raycaster a bit:
<a-entity position="0 -0.1 0" raycaster="showLine:true; far: 1000"></a-entity>
The mouse cursor should be working fine.

Click event does not provide the mesh name in gltf model

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.

Categories

Resources