I want to position and rotate an entity (e.g. an A-Frame text entity) so that it is always just in front of another (e.g. a box), i.e. the text is positioned slightly in front of the box from the point of view of the camera, facing the camera. I can use the look-at component to get the text to face the camera, but I am lost about how to position the text. I need a generic solution, since the box may vary in size and position, so hard-coding the position of the text won't work.
The aim is to have something like a tooltip that appears in front of the box (once I have the basic idea working, I'll make it so that the text only appears when you are hovering on the box).
There are multiple ways of doing this, here are two ideas:
1. Aligning HTML elements with 3D objects
This is an amazing resource which is worth checking out (as is the entire Fundamentals manual), which I used below (simplified, I hope this way the idea is more clear)
The idea is quite simple:
<p>
element with css, using the above x/y values:p {
z-index: 10;
position: absolute;
/* let us position them inside the container */
left: 0;
/* make their default position the top left of the container */
top: 0;
cursor: pointer;
/* change the cursor to a hand when over us */
font-size: large;
user-select: none;
/* don't let the text get selected */
}
<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
<script>
AFRAME.registerComponent("foo", {
schema: {text: {default: ""}},
init: function() {
// grab the div, which will contain all <p> elements
const layoutParent = document.querySelector("#overlays")
// wait until loaded
this.el.addEventListener("loaded", evt => {
const layoutEl = document.createElement("p") // create the overlay element
layoutEl.innerHTML = this.data.text // set the inner text
layoutParent.appendChild(layoutEl) // append to the parent
// keep references to use in the "tick" function
this.layoutEl = layoutEl
this.mesh = this.el.getObject3D("mesh")
})
this.tmpV = new THREE.Vector3(0,0,0); // for later use
},
tick: function() {
// ignore if there is no mesh/text yet
if (!this.mesh || !this.layoutEl) return;
const tmpV = this.tmpV;
const canvas = this.el.sceneEl.canvas
const camera = this.el.sceneEl.camera
// get the world position
this.mesh.getWorldPosition(tmpV);
// get the normalized screen coordinate of that position
tmpV.project(camera);
// convert the normalized position to CSS coordinates
const x = (tmpV.x * .5 + .5) * canvas.clientWidth;
const y = (tmpV.y * -.5 + .5) * canvas.clientHeight;
// move the elem to that position
this.layoutEl.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
}
})
</script>
<div id="overlays">
</div>
<a-scene>
<a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"
foo="text: some overlay"></a-box>
</a-scene>
Hope the comments make it clear - though you should really check the manual
2. Using 3D text
You could calculate the box -> camera
vector, and use it to offset the overlay text - which combined with lookAt
should get the effect You're looking for.
A simpler way of the same idea would be:
look-at
a-text
in the rig, offseting its z by the box bounding sphere radius.<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script>
AFRAME.registerComponent("spherical-tooltip", {
schema: {
text: {
default: ""
}
},
init: function() {
this.el.addEventListener("loaded", evt => {
const mesh = this.el.getObject3D("mesh")
// the timeout is an ugly hack,
// the bounding sphere isn't set yet
setTimeout(evt => {
const bsphere = mesh.geometry.boundingSphere
// the main rig
const rig = document.createElement("a-entity");
rig.setAttribute("look-at", "[camera]")
this.el.appendChild(rig)
// setup the text, and offset the position
const tooltip = document.createElement("a-text");
tooltip.setAttribute("color", "black")
tooltip.setAttribute("value", this.data.text)
tooltip.setAttribute("align", "center")
tooltip.setAttribute("position", {x: 0, y: 0, z: bsphere.radius})
rig.appendChild(tooltip)
}, 150)
})
}
})
</script>
<a-scene>
<a-box spherical-tooltip="text: tooltip" position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>
</a-scene>