31

I need Three.js code to convert 3D object coordinates to 2d ones in a 'div' element so that I can place text labels where they need to be (without those labels scaling/moving/rotating along with the 3D movement). Unfortunately, all of the examples that I have seen and tried so far seem to be using obsolete functions/techniques. In my case, I believe that I am using r69 of Three.js.

Here is an example of an 'older' technique that just produces errors for me:

Three.js: converting 3d position to 2d screen position

Here is a snippet of some newer code (?) that doesn't provide sufficient context for me to get working, but looks a lot cleaner:

https://github.com/mrdoob/three.js/issues/5533

Community
  • 1
  • 1
Darren Enns
  • 313
  • 1
  • 4
  • 5
  • I appreciate the help that I have received for my 1st posting! It looks to me like *both* solutions below can work for my ultimate solution. However, in my case, I need to be able to assign text labels *dynamically* (and to multiple objects). The only technique that I know of to do this is to create a 'div', and assign text/position to it, and then 'document.body.appendChild' the div. When I do this in a loop, however, these text labels accumulate -- since they are not being 'erased' from the div. Should I open a new case? – Darren Enns Dec 11 '14 at 21:15

3 Answers3

44

I've written for my project the following function; it receives an THREE.Object3D instance and a camera as a parameters and returns the position on the screen.

function toScreenPosition(obj, camera)
{
    var vector = new THREE.Vector3();

    var widthHalf = 0.5*renderer.context.canvas.width;
    var heightHalf = 0.5*renderer.context.canvas.height;

    obj.updateMatrixWorld();
    vector.setFromMatrixPosition(obj.matrixWorld);
    vector.project(camera);

    vector.x = ( vector.x * widthHalf ) + widthHalf;
    vector.y = - ( vector.y * heightHalf ) + heightHalf;

    return { 
        x: vector.x,
        y: vector.y
    };

};

Then I created a THREE.Object3D just to hold the div position (it's attached to a mesh in the scene) and when needed it can easily converted to screen position using the toScreenPosition function and it updates the coordinates of the div element.

var proj = toScreenPosition(divObj, camera);

divElem.style.left = proj.x + 'px';
divElem.style.top = proj.y + 'px';

Here a fiddle with a demo.

meirm
  • 1,111
  • 12
  • 21
  • Thanks for trying to help, especially since this is my first question to this community :) However, I am having trouble with some of the syntax/construction of your function. Could you clarify what parameters need to be assigned or passed to make this work properly? It looks like it should require: (1) div (2) camera (3) object. I am a fairly new Javascript writer, so any confusion tends to lead me astray. I am currently getting 'undefined' for X and Y, which tells me that I am not calling your function properly. – Darren Enns Dec 10 '14 at 21:19
  • Good example! To make it more realistic, though, I changed the object to live outside of the origin i.e. "sphereMesh.position.set(100, 100, 100);", and for some reason the text label does not follow this relocation (nor does the label 'move' as I pan/zoom/etc.). Since I am very new at Three.js, the problem could easily be something that I am doing wrong. – Darren Enns Dec 11 '14 at 13:56
  • Can you please give me a link to jsFiddle? Anyway, in my example, the function that update the div position are bind to the camera change event, if you change the position by other way (not by OrbitControl) you have to call this function also to take this changes for the div. – meirm Dec 11 '14 at 14:41
  • I added the positioning of sphereMesh immediately below its declaration (line 23 in your jsFiddle), so I would assume that it would be taken into account during label positioning. I just used my mouse (i.e. OrbitControl) to change the camera viewpoint. It looks like the label continues to assume that the object is located around the origin (0,0,0). – Darren Enns Dec 11 '14 at 15:01
  • I see, instead of scene.add(divObj) write sphereMesh.add(divObj), to solve this – meirm Dec 11 '14 at 15:09
  • Yes! That seems to work :) Thanks! I need to figure out how to position the text closer to the object, though. At the same time I am also working on the 1st solution above, which is a radically different approach. A possible benefit (not sure yet) of your solution is that OrbitControls automatically moves the text labels -- not just plotting them in an initially-correct location. – Darren Enns Dec 11 '14 at 20:04
  • Hmmm...just noticed that in your example the text label is hardcoded in the 'div' declaration. Any idea how difficult it would be to make this dynamic (for multiple objects)? – Darren Enns Dec 11 '14 at 20:34
  • From playing around, I think it is possible to dynamically create a 'div' to hold the label and then 'document.body.appendChild' it -- but that introduces other challenges for me (as I mentioned in my additional comments at the top). – Darren Enns Dec 11 '14 at 21:20
  • Here is my attempt (1st time using jsFiddle) to simulate a loop creating *two* objects, each with their own label. However, only the 2nd one actually gets created :( http://jsfiddle.net/darethehair/kgxeuz24/12/ – Darren Enns Dec 11 '14 at 23:07
  • your latest jsFiddle looks good! However, I am still struggling with the fact that the text labels are not adjacent to the centers of the spheres -- and when zoomed out are not nearby at all :( – Darren Enns Dec 12 '14 at 15:03
  • Correct me if I'm wrong, but I think if `vector.z < 1`, the camera is looking towards the object, and if `vector.z > 1`, it's looking away from it. This can be used in situations when you want to make some DOM elements appear next to a three.js 3D object, and you don't want that element to appear on the "other side" of the camera (as the projection "pierces" the camera plane). This is irrelevant for the fiddle in the demo as there the object is always in front of us, but if we used different cam controls to look away from it, the text from the fiddle would appear exactly behind us. – nh2 Dec 30 '16 at 19:37
  • FYI, the JSFiddle here doesn't seem to work, looks like it needs an update. – 10000RubyPools Nov 01 '17 at 00:05
  • 2
    Keep in mind that `canvas.width` and `canvas.height` may not reflect real width and height of canvas, especially in full-screen apps. Then use `getBoundingClientRect` instead. – Krzysztof Grzybek Jun 05 '18 at 18:33
  • To get the labels attached to the circles, I had to update lines 94/95 in jsfiddle as follows: vector.x = (vector.x + 1) / 2 * widthHalf; vector.y = -(vector.y - 1) / 2 * heightHalf; – Bart Theeten Jul 06 '18 at 14:17
  • Hello @meirm is there any reason you don't divide off `vector.z` after projecting it? When I try it on my data, `vector.z` is not equal to 1 after projecting – Carpetfizz Jan 09 '19 at 00:57
23

You can convert a 3D position to screen coordinates by using a pattern like so:

var vector = new THREE.Vector3();
var canvas = renderer.domElement;

vector.set( 1, 2, 3 );

// map to normalized device coordinate (NDC) space
vector.project( camera );

// map to 2D screen space
vector.x = Math.round( (   vector.x + 1 ) * canvas.width  / 2 );
vector.y = Math.round( ( - vector.y + 1 ) * canvas.height / 2 );
vector.z = 0;

three.js r.69

Sarath
  • 8,472
  • 11
  • 44
  • 75
WestLangley
  • 92,014
  • 9
  • 230
  • 236
  • 2
    This looks radically simpler than previous suggestions! However, I am confused: where is the object referenced -- for which we desire to convert its 3D coordinates into 2D ones? – Darren Enns Dec 10 '14 at 23:12
  • 3
    Sorry. Typo fixed. The question was about converting a 3d position to screen space. – WestLangley Dec 10 '14 at 23:18
  • 1
    Every object has a position property that is a vector3. If you apply this solutiion to your object position, you will move it. But I don't see any utility in it ... – vals Dec 11 '14 at 18:44
  • Yes, by extracting my objects XYZ coords, and using those values to set the coordinates in the 'vector' variable in your example, your code works fine for (ultimately) determining the 2D screen location :) Thanks! However, beyond the question I asked, I can't see any easy way for these labels to 'follow' movements using OrbitControls (like proposed solution #2 below does). You did, though, answer my question using a minimum of code :) – Darren Enns Dec 11 '14 at 20:21
  • Since I am new here, I guess I can only 'accept' one answer? Too bad :( – Darren Enns Dec 11 '14 at 20:23
  • P.S. I think that by using a technique similar to is used in the jsFiddle for solution #2 below, it would be possible to link the 'div' containing the text label to a new object, which is then -- in turn -- linked to a mesh which is automatically moved by OrbitControls. – Darren Enns Dec 11 '14 at 21:17
  • Note that if the position is relative to a nested Object3D you will need to convert the local position to a world position before projecting: `object.localToWorld(position);` – Felix Turner Jan 08 '16 at 19:49
  • @WestLangley Can you give me a link of the math theory about the code above? – aboutqx Feb 23 '16 at 07:53
  • @aboutqx See the math links in http://stackoverflow.com/questions/11966779/learning-webgl-and-three-js/11970687#11970687 – WestLangley Feb 23 '16 at 18:12
  • I would use `canvas.offsetWidth` instead – strider Aug 31 '16 at 06:07
  • Hi could you provide the reverse code? I have 2d coordinates and want to have the 3d coordinate... of course i would provide the z-coordinate – wutzebaer Nov 24 '16 at 18:51
  • @WestLangley Please help; I'm working on something for work, and I have tried everything (side note: I have used this exact method before). One of the changes that I think is making your solution not work is that I have rotated the scene because there are a lot of objects in it. Would this affect the outcome? I can't seem to find any other error. I can put together a jsfiddle tonight, and I might even have to open up another SO question tomorrow, if I still can't figure it out. I put together a jsfiddle that is much more simple, and it works. I will try to find right balance for next jsfiddle – dylnmc Oct 09 '17 at 21:35
  • @WestLangley et all, I have posted here: https://stackoverflow.com/questions/46667395/three-js-vector3-to-2d-screen-coordinate-with-rotated-scene ... sorry for raiding the comments on this question, but I am in desperate need of help >_ – dylnmc Oct 10 '17 at 13:11
8

For me this function works (Three.js version 69):

function createVector(x, y, z, camera, width, height) {
        var p = new THREE.Vector3(x, y, z);
        var vector = p.project(camera);

        vector.x = (vector.x + 1) / 2 * width;
        vector.y = -(vector.y - 1) / 2 * height;

        return vector;
    }
Julia Savinkova
  • 523
  • 7
  • 18
  • 1
    Glad to have another suggestion :) Looks very similar to WestLangley's above (with shorter syntax). As I mention above, though, all these techniques don't place the text *immediately* beside the object I am trying to label. Not sure what I would need to tweak to accomplish that. – Darren Enns Dec 12 '14 at 18:20