0

As shown in the included (three.js) live snippet (also at jsfiddle.net/gpolyn/une6tst5/21) *, I have a box with unequal sides that a viewer may re-position by dragging. The extreme left, right, top or bottom box corners in the viewport are dynamically indicated by green square dots.

My modeling challenge is as follows: For a given viewport, present my box so that through all positions the dots with the longest window distance between them are at their respective viewport edges.

So, for one object orientation, the object may be presented with left and right dotted corners at the viewport's left and right edges, while another orientation might result in a presentation of the top and bottom green dotted corners at the viewport's top and bottom.

My current approach uses a bounding sphere, but that doesn't accomplish my goal for every, or even many, object orientations.

I suspect a better approach may lie somewhere among these:

  1. Depending on window coordinates of most extreme object points, modify the view or projection matrix or both, to represent the object
  2. Swap out the bounding sphere approach for a bounding box approach
  3. Get the window coordinates of a 'virtual' frame around the green dotted corners and project the framed image onto the window (similar to 1.)

* My code depends heavily on an excellent presentation by Eric Haines at www.realtimerendering.com/udacity/transforms.html, while the green dot technique is from one of the many highly useful three.js answers posted on this forum by WestLangley

 var renderer, scene, camera, controls;
 var object;
 var vertices3;
 var cloud;
 var boxToBufferAlphaMapping = {
   0: 0,
   2: 1,
   1: 2,
   3: 4,
   6: 7,
   7: 10,
   5: 8,
   4: 6
 }
 var lastAlphas = [];
 var canvasWidth, canvasHeight;
 var windowMatrix;
 var boundingSphere;

 init();
 render();
 afterInit();
 animate();

 function init() {

   canvasWidth = window.innerWidth;
   canvasHeight = window.innerHeight;

   // renderer
   renderer = new THREE.WebGLRenderer({
     antialias: true
   });
   renderer.setSize(canvasWidth, canvasHeight);
   document.body.appendChild(renderer.domElement);

   // scene
   scene = new THREE.Scene();

   // object
   var geometry = new THREE.BoxGeometry(4, 4, 6);

   // too lazy to add edges without EdgesHelper...
   var material = new THREE.MeshBasicMaterial({
     transparent: true,
     opacity: 0
   });
   var cube = new THREE.Mesh(geometry, material);
   object = cube;

   // bounding sphere used for orbiting control in render
   object.geometry.computeBoundingSphere();
   boundingSphere = object.geometry.boundingSphere;

   cube.position.set(2, 2, 3)
     // awkward, but couldn't transfer cube position to sphere...
   boundingSphere.translate(new THREE.Vector3(2, 2, 3));

   // save vertices for subsequent use
   vertices = cube.geometry.vertices;

   var edges = new THREE.EdgesHelper(cube)
   scene.add(edges);
   scene.add(cube);
   addGreenDotsToScene(geometry);

   // camera
   camera = new THREE.PerspectiveCamera(17, window.innerWidth / window.innerHeight, 1, 10000);
   camera.position.set(20, 20, 20);

   // controls
   controls = new THREE.OrbitControls(camera);
   controls.maxPolarAngle = 0.5 * Math.PI;
   controls.minAzimuthAngle = 0;
   controls.maxAzimuthAngle = 0.5 * Math.PI;
   controls.enableZoom = false;

   // ambient
   scene.add(new THREE.AmbientLight(0x222222));

   // axes
   scene.add(new THREE.AxisHelper(20));

 }

  // determine which object points are in the most extreme top-,
  // left-, right- and bottom-most positions in the window space
  // and illuminate them
 function addExtrema() {

   // object view-space points, using view (camera) matrix
   var viewSpacePts = vertices3.map(function(vt) {
     return vt.clone().applyMatrix4(camera.matrixWorldInverse);
   })

   // object clip coords, using projection matrix
   var clipCoords = viewSpacePts.map(function(vt) {
     return vt.applyMatrix4(camera.projectionMatrix);
   })

   // w-divide clip coords for NDC
   var ndc = clipCoords.map(function(vt) {
     return vt.divideScalar(vt.w);
   })

   // object window coordinates, using window matrix
   var windowCoords = ndc.map(function(vt) {
     return vt.applyMatrix4(windowMatrix);
   })

   // arbitrary selection to start
   var topIdx = 0,
     bottomIdx = 0,
     leftIdx = 0,
     rightIdx = 0;
   var top = windowCoords[0].y;
   var bottom = windowCoords[0].y
   var right = windowCoords[0].x;
   var left = windowCoords[0].x;

   for (var i = 1; i < windowCoords.length; i++) {
     vtx = windowCoords[i];
     if (vtx.x < left) {
       left = vtx.x;
       leftIdx = i;
     } else if (vtx.x > right) {
       right = vtx.x;
       rightIdx = i;
     }

     if (vtx.y < bottom) {
       bottom = vtx.y;
       bottomIdx = i;
     } else if (vtx.y > top) {
       top = vtx.y;
       topIdx = i;
     }
   }

   var alphas = cloud.geometry.attributes.alpha;

   // make last points invisible
   lastAlphas.forEach(function(alphaIndex) {
     alphas.array[alphaIndex] = 0.0;
   });
   // now, make new points visible...
   // (boxToBufferAlphaMapping is a BufferGeometry-Object3D geometry
   // map between the object and green dots)
   alphas.array[boxToBufferAlphaMapping[rightIdx]] = 1.0;
   alphas.array[boxToBufferAlphaMapping[bottomIdx]] = 1.0;
   alphas.array[boxToBufferAlphaMapping[topIdx]] = 1.0;
   alphas.array[boxToBufferAlphaMapping[leftIdx]] = 1.0;

   // store visible points for next cycle
   lastAlphas = [boxToBufferAlphaMapping[rightIdx]];
   lastAlphas.push(boxToBufferAlphaMapping[bottomIdx])
   lastAlphas.push(boxToBufferAlphaMapping[topIdx])
   lastAlphas.push(boxToBufferAlphaMapping[leftIdx])

   alphas.needsUpdate = true;

 }

 function addGreenDotsToScene(geometry) {

   var bg = new THREE.BufferGeometry();
   bg.fromGeometry(geometry);
   bg.translate(2, 2, 3); // yucky, and quick

   var numVertices = bg.attributes.position.count;
   var alphas = new Float32Array(numVertices * 1); // 1 values per vertex

   for (var i = 0; i < numVertices; i++) {
     alphas[i] = 0;
   }

   bg.addAttribute('alpha', new THREE.BufferAttribute(alphas, 1));

   var uniforms = {
     color: {
       type: "c",
       value: new THREE.Color(0x00ff00)
     },
   };

   var shaderMaterial = new THREE.ShaderMaterial({
     uniforms: uniforms,
     vertexShader: document.getElementById('vertexshader').textContent,
     fragmentShader: document.getElementById('fragmentshader').textContent,
     transparent: true
   });

   cloud = new THREE.Points(bg, shaderMaterial);
   scene.add(cloud);

 }

 function afterInit() {

   windowMatrix = new THREE.Matrix4();
   windowMatrix.set(canvasWidth / 2, 0, 0, canvasWidth / 2, 0, canvasHeight / 2, 0, canvasHeight / 2, 0, 0, 0.5, 0.5, 0, 0, 0, 1);

   var vertices2 = object.geometry.vertices.map(function(vtx) {
     return (new THREE.Vector4(vtx.x, vtx.y, vtx.z));
   });

   // create 'world-space' geometry points, using
   // model ('world') matrix
   vertices3 = vertices2.map(function(vt) {
     return vt.applyMatrix4(object.matrixWorld);
   })

 }

 function render() {

   var dist = boundingSphere.distanceToPoint(camera.position);

   // from stackoverflow.com/questions/14614252/how-to-fit-camera-to-object
   var height = boundingSphere.radius * 2;
   var fov = 2 * Math.atan(height / (2 * dist)) * (180 / Math.PI);

   // not sure why, but factor is needed to maximize fit of object
   var mysteryFactor = 0.875;
   camera.fov = fov * mysteryFactor;
   camera.updateProjectionMatrix();
   camera.lookAt(boundingSphere.center);

   renderer.render(scene, camera);

 }

 function animate() {

   requestAnimationFrame(animate);
   render();
   addExtrema()

 }
   body {
     background-color: #000;
     margin: 0px;
     overflow: hidden;
   }
   
<script src="https://rawgit.com/mrdoob/three.js/dev/build/three.min.js"></script>
<script src="https://rawgit.com/mrdoob/three.js/master/examples/js/controls/OrbitControls.js"></script>
<script type="x-shader/x-vertex" id="vertexshader">

  attribute float alpha; varying float vAlpha; void main() { vAlpha = alpha; vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); gl_PointSize = 8.0; gl_Position = projectionMatrix * mvPosition; }

</script>

<script type="x-shader/x-fragment" id="fragmentshader">

  uniform vec3 color; varying float vAlpha; void main() { gl_FragColor = vec4( color, vAlpha ); }

</script>
Community
  • 1
  • 1
gallygator
  • 377
  • 1
  • 4
  • 16
  • 1
    Your problem may be considerably easier if you are willing to use an orthographic camera, instead. Otherwise, this may help: http://stackoverflow.com/questions/13350875/three-js-width-of-view/13351534#13351534 – WestLangley Jun 20 '16 at 15:15
  • @WestLangley I acknowledge your first point. As for your reference, thanks, but the technique described there seems takes for granted that the height or width dimensions used in my FOV calculation would be taken only when my subject box is facing the camera. – gallygator Jun 20 '16 at 16:10
  • for this to be possible the aspect ratio of the VP must be variable –  Jun 20 '16 at 20:49
  • @willywonkadailyblah to clarify, by viewing plane, do you mean one of the 'view' or 'projection' matrices, illustrated, for example, at [www.realtimerendering.com/udacity/transforms.html](http://www.realtimerendering.com/udacity/transforms.html)? – gallygator Jun 20 '16 at 22:08
  • 1
    @gallygator view _port_, sorry –  Jun 20 '16 at 23:08
  • @willywonkadailyblah no, I don't think so – gallygator Jun 23 '16 at 22:27

1 Answers1

0

Found a reasonable solution (included in live snippet, here), largely thanks to these two related posts:

var renderer, scene, camera, controls;
var object;
var vertices3;
var cloud;
var boxToBufferAlphaMapping = {
  0: 0,
  2: 1,
  1: 2,
  3: 4,
  6: 7,
  7: 10,
  5: 8,
  4: 6
}
var lastAlphas = [];
var canvasWidth, canvasHeight;
var windowMatrix;
var boundingSphere;
var figure;
var fovWidth, fovDistance, fovHeight;
var newFov, newLookAt;
var dist, height, fov;
var aspect;
var CONSTANT_FOR_FOV_CALC = 180 / Math.PI;
var mat3;
var CORNERS = 8;
var ndc = new Array(CORNERS);
var USE_GREEN_DOTS = false;


init();
render();
afterInit();
animate();

function init() {

  mat3 = new THREE.Matrix4();

  canvasWidth = window.innerWidth;
  canvasHeight = window.innerHeight;
  aspect = canvasWidth / canvasHeight;
  // renderer
  renderer = new THREE.WebGLRenderer({
    antialias: true
  });
  renderer.setSize(canvasWidth, canvasHeight);
  document.body.appendChild(renderer.domElement);

  // scene
  scene = new THREE.Scene();

  // object
  var geometry = new THREE.BoxGeometry(4, 4, 6);

  // too lazy to add edges without EdgesHelper...
  var material = new THREE.MeshBasicMaterial({
    transparent: true,
    opacity: 0
  });
  var cube = new THREE.Mesh(geometry, material);
  object = cube;

  // bounding sphere used for orbiting control in render
  object.geometry.computeBoundingSphere();
  boundingSphere = object.geometry.boundingSphere;

  cube.position.set(2, 2, 3)
    // awkward, but couldn't transfer cube position to sphere...
  boundingSphere.translate(new THREE.Vector3(2, 2, 3));

  // save vertices for subsequent use
  vertices = cube.geometry.vertices;

  var edges = new THREE.EdgesHelper(cube)
  scene.add(edges);
  scene.add(cube);

  if (USE_GREEN_DOTS) addGreenDotsToScene(geometry);

  // camera
  camera = new THREE.PerspectiveCamera(17, window.innerWidth / window.innerHeight, 1, 10000);
  camera.position.set(20, 20, 20);

  // controls
  controls = new THREE.OrbitControls(camera);
  controls.maxPolarAngle = 0.5 * Math.PI;
  controls.minAzimuthAngle = 0;
  controls.maxAzimuthAngle = 0.5 * Math.PI;
  controls.enableZoom = false;

  // ambient
  scene.add(new THREE.AmbientLight(0x222222));

  // axes
  scene.add(new THREE.AxisHelper(20));

  // initial settings
  dist = boundingSphere.distanceToPoint(camera.position);
  height = boundingSphere.radius * 2;
  fov = 2 * Math.atan(height / (2 * dist)) * (CONSTANT_FOR_FOV_CALC);
  newFov = fov;
  newLookAt = new THREE.Vector3(2, 2, 3); // center of box

}

function addExtrema() {

  // thread A  
  mat3.multiplyMatrices(camera.matrixWorld, mat3.getInverse(camera.projectionMatrix));

  // thread B 
  var scratchVar;

  for (var i = 0; i < CORNERS; i++) {

    scratchVar = vertices3[i].clone().applyMatrix4(camera.matrixWorldInverse);
    scratchVar.applyMatrix4(camera.projectionMatrix);

    scratchVar.divideScalar(scratchVar.w)
    ndc[i] = scratchVar;

  }

  // arbitrary selection to start
  var topIdx = 0,
    bottomIdx = 0,
    leftIdx = 0,
    rightIdx = 0;
  var top = ndc[0].y;
  var bottom = ndc[0].y
  var right = ndc[0].x;
  var left = ndc[0].x;
  var closestVertex, closestVertexDistance = Number.POSITIVE_INFINITY;
  var vtx;

  for (var i = 1; i < CORNERS; i++) {

    vtx = ndc[i];

    if (vtx.x < left) {
      left = vtx.x;
      leftIdx = i;
    } else if (vtx.x > right) {
      right = vtx.x;
      rightIdx = i;
    }

    if (vtx.y < bottom) {
      bottom = vtx.y;
      bottomIdx = i;
    } else if (vtx.y > top) {
      top = vtx.y;
      topIdx = i;
    }

    if (vtx.z < closestVertexDistance) {
      closestVertex = i;
      closestVertexDistance = vtx.z;
    }

  }


  var yNDCPercentCoverage = (Math.abs(ndc[topIdx].y) + Math.abs(ndc[bottomIdx].y)) / 2;
  yNDCPercentCoverage = Math.min(1, yNDCPercentCoverage);

  var xNDCPercentCoverage = (Math.abs(ndc[leftIdx].x) + Math.abs(ndc[rightIdx].x)) / 2;
  xNDCPercentCoverage = Math.min(1, xNDCPercentCoverage);

  var ulCoords = [ndc[leftIdx].x, ndc[topIdx].y, closestVertexDistance, 1]
  var blCoords = [ndc[leftIdx].x, ndc[bottomIdx].y, closestVertexDistance, 1]
  var urCoords = [ndc[rightIdx].x, ndc[topIdx].y, closestVertexDistance, 1]

  var ul = new THREE.Vector4().fromArray(ulCoords);
  ul.applyMatrix4(mat3).divideScalar(ul.w);

  var bl = new THREE.Vector4().fromArray(blCoords);
  bl.applyMatrix4(mat3).divideScalar(bl.w);

  var ur = new THREE.Vector4().fromArray(urCoords);
  ur.applyMatrix4(mat3).divideScalar(ur.w);

  var center = new THREE.Vector3();
  center.addVectors(ur, bl);
  center.divideScalar(2);

  var dist = camera.position.distanceTo(center);
  newLookAt = center;

  var upperLeft = new THREE.Vector3().fromArray(ul.toArray().slice(0, 3));

  if ((1 - yNDCPercentCoverage) < (1 - xNDCPercentCoverage)) { // height case
    var bottomLeft = new THREE.Vector3().fromArray(bl.toArray().slice(0, 3));
    var height = upperLeft.distanceTo(bottomLeft);
    newFov = 2 * Math.atan(height / (2 * dist)) * (CONSTANT_FOR_FOV_CALC);
  } else { // width case
    var upperRight = new THREE.Vector3().fromArray(ur.toArray().slice(0, 3));
    var width = upperRight.distanceTo(upperLeft);
    newFov = 2 * Math.atan((width / aspect) / (2 * dist)) * (CONSTANT_FOR_FOV_CALC);
  }

  if (USE_GREEN_DOTS) {
    var alphas = cloud.geometry.attributes.alpha;

    // make last points invisible
    lastAlphas.forEach(function(alphaIndex) {
      alphas.array[alphaIndex] = 0.0;
    });
    // now, make new points visible...
    // (boxToBufferAlphaMapping is a BufferGeometry-Object3D geometry
    // map between the object and green dots)
    alphas.array[boxToBufferAlphaMapping[rightIdx]] = 1.0;
    alphas.array[boxToBufferAlphaMapping[bottomIdx]] = 1.0;
    alphas.array[boxToBufferAlphaMapping[topIdx]] = 1.0;
    alphas.array[boxToBufferAlphaMapping[leftIdx]] = 1.0;

    // store visible points for next cycle
    lastAlphas = [boxToBufferAlphaMapping[rightIdx]];
    lastAlphas.push(boxToBufferAlphaMapping[bottomIdx])
    lastAlphas.push(boxToBufferAlphaMapping[topIdx])
    lastAlphas.push(boxToBufferAlphaMapping[leftIdx])

    alphas.needsUpdate = true;
  }

}

function addGreenDotsToScene(geometry) {

  var bg = new THREE.BufferGeometry();
  bg.fromGeometry(geometry);
  bg.translate(2, 2, 3); // yucky, and quick

  var numVertices = bg.attributes.position.count;
  var alphas = new Float32Array(numVertices * 1); // 1 values per vertex

  for (var i = 0; i < numVertices; i++) {
    alphas[i] = 0;
  }

  bg.addAttribute('alpha', new THREE.BufferAttribute(alphas, 1));

  var uniforms = {
    color: {
      type: "c",
      value: new THREE.Color(0x00ff00)
    },
  };

  var shaderMaterial = new THREE.ShaderMaterial({
    uniforms: uniforms,
    vertexShader: document.getElementById('vertexshader').textContent,
    fragmentShader: document.getElementById('fragmentshader').textContent,
    transparent: true
  });

  cloud = new THREE.Points(bg, shaderMaterial);
  scene.add(cloud);

}

function afterInit() {

  windowMatrix = new THREE.Matrix4();
  windowMatrix.set(canvasWidth / 2, 0, 0, canvasWidth / 2, 0, canvasHeight / 2, 0, canvasHeight / 2, 0, 0, 0.5, 0.5, 0, 0, 0, 1);

  var vertices2 = object.geometry.vertices.map(function(vtx) {
    return (new THREE.Vector4(vtx.x, vtx.y, vtx.z));
  });

  // create 'world-space' geometry points, using
  // model ('world') matrix
  vertices3 = vertices2.map(function(vt) {
    return vt.applyMatrix4(object.matrixWorld);
  })

}

function render() {

  camera.lookAt(newLookAt);
  camera.fov = newFov;
  camera.updateProjectionMatrix();
  renderer.render(scene, camera);

}

function animate() {

  requestAnimationFrame(animate);
  render();
  addExtrema()

}
body {
  background-color: #000;
  margin: 0px;
  overflow: hidden;
}
<script src="https://rawgit.com/mrdoob/three.js/dev/build/three.min.js"></script>
<script src="https://rawgit.com/mrdoob/three.js/master/examples/js/controls/OrbitControls.js"></script>
<script type="x-shader/x-vertex" id="vertexshader">

  attribute float alpha; varying float vAlpha; void main() { vAlpha = alpha; vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); gl_PointSize = 8.0; gl_Position = projectionMatrix * mvPosition; }

</script>

<script type="x-shader/x-fragment" id="fragmentshader">

  uniform vec3 color; varying float vAlpha; void main() { gl_FragColor = vec4( color, vAlpha ); }

</script>
Community
  • 1
  • 1
gallygator
  • 377
  • 1
  • 4
  • 16