How to correctly position html elements in three js coordinate system? - three.js

I hopefully have a simple problem I can't get an answer to.
I have three js geometric spheres which move in a box. I place this box at the centre of the scene. The mechanics of how the spheres stay in the box is irrelevant. What is important is the spheres move about the origin (0,0) and the canvas always fills the page.
I want to draw a line from the moving spheres to a div or img element on the page. To do this I would assume I have to transform the css coordinates to three js coordinates. I found something I thought did something like this (Note: Over use of somethings to signify I am probably mistaken)
I can add a html element to the same scene/camera as webgl renderer but obviously using a different renderer but I am unsure how to proceed from there?
Basically I want to know:
How should I change the size of the div preserving aspect ratio if need be?
In essence I want the div or element to fill screen at some camera depth.
How to place the div at the centre of the scene by default?
Mines seems to be shifted 1000 in z direction but this might be the size of the div(img) which I have to bring into view.
How to draw a line between the webgl sphere and html div/img?
thanks in advance!

Unfortunately you have asked 3 questions, it is tricky to address them all at once.
I will explain how to position DIV element on top of some 3D object. My example would be a tooltip that appears when you hover the object by mouse: http://jsfiddle.net/mmalex/ycnh0wze/
So let's get started,
First of all you need to subscribe mouse events and convert 2D coordinates of a mouse to relative coordinates on the viewport. Very well explained you will find it here: Get mouse clicked point's 3D coordinate in three.js
Having 2D coordinates, raycast the object. These steps are quite trivial, but for completeness I provide the code chunk.
var raycaster = new THREE.Raycaster();
function handleManipulationUpdate() {
// cleanup previous results, mouse moved and they're obsolete now
latestMouseIntersection = undefined;
hoveredObj = undefined;
raycaster.setFromCamera(mouse, camera);
{
var intersects = raycaster.intersectObjects(tooltipEnabledObjects);
if (intersects.length > 0) {
// keep point in 3D for next steps
latestMouseIntersection = intersects[0].point;
// remember what object was hovered, as we will need to extract tooltip text from it
hoveredObj = intersects[0].object;
}
}
... // do anything else
//with some conditions it may show or hide tooltip
showTooltip();
}
// Following two functions will convert mouse coordinates
// from screen to three.js system (where [0,0] is in the middle of the screen)
function updateMouseCoords(event, coordsObj) {
coordsObj.x = ((event.clientX - renderer.domElement.offsetLeft + 0.5) / window.innerWidth) * 2 - 1;
coordsObj.y = -((event.clientY - renderer.domElement.offsetTop + 0.5) / window.innerHeight) * 2 + 1;
}
function onMouseMove(event) {
updateMouseCoords(event, mouse);
handleManipulationUpdate();
}
window.addEventListener('mousemove', onMouseMove, false);
And finally see the most important part, DIV element placement. To understand the code it is essential to get convenient with Vector3.project method.
The sequence of calculations is as follows:
Get 2D mouse coordinates,
Raycast object and remember 3D coordinate of intersection (if any),
Project 3D coordinate back into 2D (this step may seem redundant here, but what if you want to trigger object tooltip programmatically? You won't have mouse coordinates)
Mess around to place DIV centered above 2D point, with nice margin.
// This will move tooltip to the current mouse position and show it by timer.
function showTooltip() {
var divElement = $("#tooltip");
//element found and mouse hovers some object?
if (divElement && latestMouseIntersection) {
//hide until tooltip is ready (prevents some visual artifacts)
divElement.css({
display: "block",
opacity: 0.0
});
//!!! === IMPORTANT ===
// DIV element is positioned here
var canvasHalfWidth = renderer.domElement.offsetWidth / 2;
var canvasHalfHeight = renderer.domElement.offsetHeight / 2;
var tooltipPosition = latestMouseProjection.clone().project(camera);
tooltipPosition.x = (tooltipPosition.x * canvasHalfWidth) + canvasHalfWidth + renderer.domElement.offsetLeft;
tooltipPosition.y = -(tooltipPosition.y * canvasHalfHeight) + canvasHalfHeight + renderer.domElement.offsetTop;
var tootipWidth = divElement[0].offsetWidth;
var tootipHeight = divElement[0].offsetHeight;
divElement.css({
left: `${tooltipPosition.x - tootipWidth/2}px`,
top: `${tooltipPosition.y - tootipHeight - 5}px`
});
//get text from hovered object (we store it in .userData)
divElement.text(hoveredObj.userData.tooltipText);
divElement.css({
opacity: 1.0
});
}
}

Related

How to draw a line with the mouse on a 3D surface in threejs

I have been looking for an example of how to draw a line with the mouse on a 3D surface within a scene in threejs to achieve the following image but not been able to find an example this specific and was hoping someone could provide an example of how to do this by using the mouse and clicking down to start, dragging, then using mouse up to get the end position. Then using these positions in the 3D space to draw the line.
What you need is to raycast points to the plane both in mousedown and mousemove, you should do it on mousemove and not in mouseup since the mouse may be released outside of your plane, so keeping track of where the mouse was last during the drag option is the best approach imo.
renderer.domElement.addEventListener('mousedown', e => {
if (e.button === 0) {
isMouseDown = true;
const x = (e.clientX / ctx.renderer.domElement.clientWidth) * 2 - 1;
const y = -(e.clientY / ctx.renderer.domElement.clientHeight) * 2 + 1;
rayCaster.setFromCamera({ x, y }, ctx.camera);
const intersections = rayCaster.intersectObject(plane);
if (intersections && intersections.length) {
hitPoint.copy(intersections[0].point);
lineGeometry.setFromPoints([ hitPoint, intersections[0].point ]);
}
}
});
Sample Pen

How to accelerate calculations when update messive position from 3d to screen (hud)

I want to update hud positon form 3d position to 2d when mouse moving. Since it may have a large number of 3d objects to project to the screen position, I meet a performance problem.
Are there any way to accelerate calculations? The following is how I calculate 3d object position on 2d screen.
function toScreenPosition(obj) {
var vector = new THREE.Vector3();
//calculate screen half size
var widthHalf = 0.5 * renderer.context.canvas.width;
var heightHalf = 0.5 * renderer.context.canvas.height;
//get 3d object position
obj.updateMatrixWorld();
vector.setFromMatrixPosition(obj.matrixWorld);
vector.project(this.camera);
//get 2d position on screen
vector.x = (vector.x * widthHalf) + widthHalf;
vector.y = -(vector.y * heightHalf) + heightHalf;
return {
x: vector.x,
y: vector.y
};
}
Rather than repositioning your HUD in world space every time your camera moves, add your HUD object(s) to your camera object, and position them only once. Then, when your camera moves, your HUD moves along with it, because the camera's transformation is cascaded to it's children.
yourCamera.add(yourHUD);
yourHUD.position.z = 10;
Note that doing it this way (or even positioning it the way you were) may allow scene objects to clip through your HUD geometry, or even appear between your HUD and the camera, obscuring the HUD. If that's what you want, great! If not, you could move your HUD to a second render pass, allowing it to remain "on top."
First, here is an example of your function rewritten for (almost) optimal performance as written in the comments above, the renderloop is obviously just an example to illustrate where to do which calls:
var width = renderer.context.canvas.width;
var height = renderer.context.canvas.height;
// has to be called whenever the canvas-size changes
function onCanvasResize() {
width = renderer.context.canvas.width;
height = renderer.context.canvas.height;
});
var projMatrix = new THREE.Matrix4();
// renderloop-function, called per animation-frame
function render() {
// just needed once per frame (even better would be
// once per camera-movement)
projMatrix.multiplyMatrices(
camera.projectionMatrix,
projMatrix.getInverse(camera.matrixWorld)
);
hudObjects.forEach(function(obj) {
toScreenPosition(obj, projMatrix);
});
}
// wrapped in IIFE to store the local vector-variable (this pattern
// is used everywhere in three.js)
var toScreenPosition = (function() {
var vector = new THREE.Vector3();
return function __toScreenPosition(obj, projectionMatrix) {
// this could potentially be left away, but isn't too
// expensive as there are 'needsUpdate'-checks in place
obj.updateMatrixWorld();
vector.setFromMatrixPosition(obj.matrixWorld);
vector.applyMatrix4(projectionMatrix);
vector.x = (vector.x + 1) * width / 2;
vector.y = (1 - vector.y) * height / 2;
// might want to consider returning a Vector3-instance
// instead, depends on how the result is used
return {x: vector.x, y: vector.y};
}
}) ();
But, considering you want to render a HUD, it would be better to do that independently of the main-scene, making all of the above computations obsolete and also allowing you to choose a different coordinate-system for sizing and positioning of HUD-elements.
I have an example for this here: https://codepen.io/usefulthink/pen/ZKPvPB. There I used an orthographic camera and a seperate scene to render HUD-Elements on top of the 3d-scene. No extra computations required. Plus I can specify the size and position of HUD-elements conveniently in pixel-units (The same would work using a perspective camera, only requires a bit more trigonometry to get it right).

How to convert world rotation to screen rotation?

I need to convert the position and rotation on a 3d object to screen position and rotation. I can convert the position easily but not the rotation. I've attempted to convert the rotation of the camera but it does not match up.
Attached is an example plunkr & conversion code.
The white facebook button should line up with the red plane.
https://plnkr.co/edit/0MOKrc1lc2Bqw1MMZnZV?p=preview
function toScreenPosition(position, camera, width, height) {
var p = new THREE.Vector3(position.x, position.y, position.z);
var vector = p.project(camera);
vector.x = (vector.x + 1) / 2 * width;
vector.y = -(vector.y - 1) / 2 * height;
return vector;
}
function updateScreenElements() {
var btn = document.querySelector('#btn-share')
var pos = plane.getWorldPosition();
var vec = toScreenPosition(pos, camera, canvas.width, canvas.height);
var translate = "translate3d("+vec.x+"px,"+vec.y+"px,"+vec.z+"px)";
var euler = camera.getWorldRotation();
var rotate = "rotateX("+euler.x+"rad)"+
" rotateY("+(euler.y)+"rad)"+
" rotateY("+(euler.z)+"rad)";
btn.style.transform= translate+ " "+rotate;
}
... And a screenshot of the issue.
I would highly recommend not trying to match this to the camera space, but instead to apply the image as a texture map to the red plane, and then use a raycast to see whether a click goes over the plane. You'll save yourself headache in translating and rotating and then hiding the symbol when it's behind the cube, etc
check out the THREEjs examples to see how to use the Raycaster. It's a lot more flexible and easier than trying to do rotations and matching. Then whatever the 'btn' onclick function is, you just call when you detect a raycast collision with the plane

Three.js StereoEffect displays meshes across 2 eyes

I have a THREE.js scene using StereoEffect renderer. However, when I add new meshes to the scene, they are displayed across the two eyes instead of being duplicated for each eye. I believe THREE.js is supposed to do it automatically and I don't have to duplicate them myself ? (I tried duplicating them but it is a lot of annoying calculation and I could not manage it)
My meshes are actually transparent planes, and I add a DOM element on the top of them to have a flat display.
Illustrating Example
OK I finally found it ! I tried putting back on a texture (not invisible) on my meshes and I discovered the problem.
When we use StereoEffect and we see that our mesh is duplicated on both views, it is actually an illusion : THREE.JS puts an image there, but the actual object is invisible, put exactly at the middle of the two images !
See image here : explanation
If you use raycaster for instance, it will tell you there's not an instersection where you see the mesh, but at the center of the line from left image to right image ! Same for mesh.position.
So what I did was keep an invisible texture, and create two div tags that I placed symmetrically around the mesh position :
var middleX = window.offsetWidth/2;
//left div
if(this.element.id.indexOf("-2") == -1){
var posL = coords2d.x - middleX/2;
this.element.style.left = posL + 'px';
//Hide if gets on the right part of the screen
if(posL > middleX) this.element.style.display = 'none';
}
//right div
else{
var posR = coords2d.x + middleX/2;
this.element.style.left = posR + 'px';
//Hide if gets on the left part of the screen
if(posR < middleX) this.element.style.display = 'none';
}
That gives the illusion that my mesh is there, put it is just empty divs.
Then, to check if someone clicks on my mesh, I do the opposite : I go back to the real position of the mesh before sending it to raycaster !
function hasClicked(e) {
e.preventDefault();
var clientX,clientY;
//GET REAL POSITION OF OBJECT
var middleX = window.offsetWidth/2;
//Right screen
if(e.clientX>middleX){
clientX = e.clientX - middleX/2;
}
//Left screen
else {
clientX = e.clientX + middleX/2;
}
clientY = e.clientY; //Keep same Y coordinate
//TRANSFORM THESE COORDS ON SCREEN INTO THREE.JS COORDS
var mouse = new THREE.Vector2();
mouse.x = (clientX / window.innerWidth) * 2 - 1;
mouse.y = -(clientY / window.innerHeight) * 2 + 1;
//USE RAYCASTER
raycaster.setFromCamera(mouse, camera);
var intersects = raycaster.intersectObjects(arrowManager.storage);
if (intersects.length > 0) {
//It works !!!
}
}
And it works perfectly fine !! Hope this can help someone else, I was getting really desperate here ;)

Getting position of Object3D after panning

This is probably something simple I'm overlooking. But I'm trying to get the position of an Object3D after panning. I'm using OrbitControls.
I've tried object.position, but the result returned is always the same. This is an example fiddle:
https://jsfiddle.net/jmg157/b9xxdubc/
I tried grid.position, but again, it always returned (0, 0, 0), as I set it initially. I also tried setting an ID for the DOM element (container), and using jQuery's position() method, but no luck with that either.
So if I right click and pan around the grid, how can I get the new position of the grid on the screen (i.e., top, left)?
As always, many thanks!
This seemed to do the trick, thanks to the answer of this question: Converting 3D position to 2d screen position [r69!]
I just call this method whenever there is a change in controls (i.e., panning, rotating, etc.) to get the updated position.
function getScreenPosition(object, camera) {
var vector = new THREE.Vector3();
var widthHalf = 0.5 * renderer.context.canvas.width;
var heightHalf = 0.5 * renderer.context.canvas.height;
object.updateMatrixWorld();
vector.setFromMatrixPosition(object.matrixWorld);
vector.project(camera);
vector.x = (vector.x * widthHalf) + widthHalf;
vector.y = -(vector.y * heightHalf) + heightHalf;
return vector;
}
Where left is vector.x and top is vector.y.

Resources