5

Goal

I want to update the Camera Position so that a Plane World Position matches a DIV Screen Position.

before update after update different perspective

First thoughts

I need to calculate camera.position.z - so that the planes face matches the size of the DIV - even when resizing the canvas.

this.computeZ = function(meshHandle, cameraHandle, faceHeight, targetHeight){
    var face = meshHandle.geometry.vertices[2]
    var vFOV = cameraHandle.fov * Math.PI / 180;  
    var vHeightPartial = 2 * Math.tan( vFOV / 2 );
    var p1 = faceHeight * window.innerHeight;
    var p2 = face.z * vHeightPartial;
    var p3 = targetHeight * vHeightPartial;
    var p4 = targetHeight * p2;
    var p5 = p1 + p4;
    var z = p5/p3;
    return z;
}

See computeZ in action here.

Next steps

The world face size now matches the DIV screen pixel size.

Next we need to find a camera.position.x and camera.position.y - so that the face directly overlaps the DIV.

I've studied...
How to Fit Camera to Object
Three.js - Width of view
THREE.JS: Get object size with respect to camera and object position on screen
Converting World coordinates to Screen coordinates in Three.js using Projection

...But have been struggling to build something that works for computeX and computeY

Please help

Take a look at the computeX and computeY functions in the fiddle I've provided. These functions are my best attempt - but do not work.

How do I build these functions?

Update

I've come up with a solution with the help of Craig's post. This class builds on his methods to cover resize events.

Community
  • 1
  • 1
Dan Kanze
  • 18,485
  • 28
  • 81
  • 134
  • Checkout this article, it might help you: http://learningthreejs.com/blog/2013/04/30/closing-the-gap-between-html-and-webgl/ – mika May 03 '17 at 01:03
  • 1
    The math used here might be helpful too https://github.com/mikatalk/webgla-meetup-three-intro/blob/master/src/js/steps/02/Step02.js#L38-L42 – mika May 03 '17 at 01:05
  • 1
    @mika That math is exactly what's needed. Elegant one-liner, as opposed to some complicated function. Worked for me. – Kalnode Apr 04 '23 at 22:42

1 Answers1

6

<!DOCTYPE html>

<html>

<head>
    <title>SO code</title>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/three.js/85/three.min.js"></script>
    <style>
          html,body{
    height:100%;
    width:100%;
    padding:0px;
    margin:0px;
  }
  #content{
    width:100%;
    height:100%;
    position:relative;
  }
  #box{
    position:absolute;
    background:orange;
    height:100px;
    width:100px;
    bottom:100px;
    right:100px;
  }
    </style>
    </head>
 <body>
 <div id="content">
   <div id="box"></div>
 </div>
<script>
 function Terrain(){

   this.container = document.getElementById('content');
   this.camera;
   this.scene; 
   this.renderer;
   this.light;

   this.computeZ = function(meshHandle, cameraHandle, faceHeight, targetHeight){
    var face = meshHandle.geometry.vertices[2]
   var vFOV = cameraHandle.fov * Math.PI / 180;  
   var vHeightPartial = 2 * Math.tan( vFOV / 2 );
   var p1 = faceHeight * window.innerHeight;
   var p2 = face.z * vHeightPartial;
   var p3 = targetHeight * vHeightPartial;
   var p4 = targetHeight * p2;
   var p5 = p1 + p4;
   var z = p5/p3;
      
   //calculate dom element center coordinate
   var screenPositionX = 0;
   var screenPositionY = 0;
   var div = document.getElementById('box');
   var divDim = div.getBoundingClientRect();
   screenPositionX = (divDim.left + divDim.right) / 2;
   screenPositionY = (divDim.bottom + divDim.top) / 2;
   var vector = new THREE.Vector3((screenPositionX / window.innerWidth) * 2 -1, (screenPositionY / window.innerHeight) * 2 -1, 0.5);
    //unproject camera
   vector = vector.unproject(this.camera);
   var distanceZ = this.camera.position.z - vector.z ;
   var offsetX = vector.x * (z-10) / distanceZ;
   var offsetY = vector.y * (z-10) / distanceZ;
   var cameraPosition = new THREE.Vector3(offsetX,offsetY,z);
   return cameraPosition;
  }
   
  this.computeX = function(meshHandle, cameraHandle, faceHeight, targetWidth){
    var div = document.getElementById('box');
   var divDim = div.getBoundingClientRect();
   var y =  ((divDim.left + (targetWidth/2)) / window.innerHeight ) * 2 + 1;
   return y;
   }
   
   this.computeY = function(meshHandle, cameraHandle, faceHeight, targetHeight){
   var div = document.getElementById('box');
   var divDim = div.getBoundingClientRect();
   var y =  ((divDim.top + (targetHeight/2)) / window.innerHeight ) * 2 + 1;
   return y;
   }
   this.onDocumentClick = function(event)
   {
      var vector = new THREE.Vector3(( event.clientX / (window.innerWidth) ) * 2 - 1, -( event.clientY / window.innerHeight ) * 2 + 1, 0.5);
                vector = vector.unproject(this.camera);

                console.log(vector);
   }
   this.init();
 }

 Terrain.prototype.init = function () {

  this.camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 );
  this.scene = new THREE.Scene();

  this.geometry = new THREE.BoxGeometry(20, 20, 20);
  this.material = new THREE.MeshPhongMaterial();
  this.mesh = new THREE.Mesh( this.geometry, this.material );
  this.scene.add( this.mesh );
  
  var ambient = new THREE.AmbientLight( 0x00ff00, 0.5 );
  this.scene.add( ambient );

  this.renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
  this.renderer.setPixelRatio( window.devicePixelRatio );
  this.renderer.setSize( window.innerWidth, window.innerHeight );
  this.container.appendChild(this.renderer.domElement );
   
  window.animations['terrain'] = this.animate.bind(this);
  window.addEventListener( 'resize', this.onWindowResize.bind(this), false );
  document.addEventListener('click',this.onDocumentClick.bind(this), false);
 }

 Terrain.prototype.onWindowResize = function(){
  this.renderer.setSize( window.innerWidth, window.innerHeight );
   this.camera.aspect = window.innerWidth / window.innerHeight;
   this.camera.updateProjectionMatrix();
 }

 Terrain.prototype.animate = function(){
    //this.camera.position.x = this.computeX(this.mesh, this.camera, 20, 100);
   //this.camera.position.y = this.computeY(this.mesh, this.camera, 20, 100);
   this.renderer.render( this.scene, this.camera );
 }

 function animate(){
   for(var i in window.animations){
     window.animations[i]();
   };
   window.requestAnimationFrame(animate);
 }

 window.animations = {};
 var terrain = new Terrain();
 window.requestAnimationFrame(animate);
 var newPosition = terrain.computeZ(terrain.mesh,terrain.camera,20,100);
 terrain.camera.position.x -= newPosition.x;
 terrain.camera.position.y += newPosition.y;
 terrain.camera.position.z += newPosition.z;
</script>
</body>
</html>

To solve the problem, I flow these two steps, 1: get the dom element coordinate in 3D space. 2: using similar triangles calculate offset of x and y. I will show you how to use similar triangles.

enter image description here

Cause the camera is a perspective camera. now the dom element, the camera and the mesh can make a triangle, if we ignore the Y dimension, the triangle would look like this picture.

Now, we know dom element coordinate in 3D space, and you got the right 'z', we also know the green cube's depth. and we need to calculate the offset x(purple line in the picture). It's obviously that these triangles in the picture are similar triangles. vector.x / offsetX = vector.z / z - mesh.geometry.parameters.depth. and we can do the same thing to get the offsetY.

Craig.Li
  • 1,336
  • 10
  • 17
  • Thank you for putting so much time and effort into this explanation. I was able to build on your solution for resize events. Feel free to consult http://jsfiddle.net/gigablox/gryeejyj/ for reference :) When bounty becomes available, I'll reward you for your efforts. – Dan Kanze May 03 '17 at 18:07
  • @DanKanze Thank you, your question is very interesting. – Craig.Li May 03 '17 at 23:36
  • An alternative solution with a fixed camera position to move objects within the scene rather then the camera. http://jsfiddle.net/gigablox/e28eotue/4/ – Dan Kanze Aug 30 '17 at 18:48