一个不等边的 3D 盒子如何填充视口,无论它的透视方向如何?

How can a 3D box with unequal sides fill the viewport, no matter its orientation in perspective?

如随附的 (three.js) live snippet (also at jsfiddle.net/gpolyn/une6tst5/21) * 所示,我有一个不等边的框,查看者可以通过拖动来重新定位。视口中最左、最右、最上或最下的框角由绿色方点动态指示。

我的建模挑战如下:对于给定的视口,呈现我的框,以便通过所有位置,它们之间距离最长 window 的点位于它们各自的视口边缘

因此,对于一个对象方向,对象可能会在视口的左右边缘显示左右虚线角,而另一种方向可能会导致在视口的顶部和底部显示绿色虚线角顶部和底部。

我目前的方法使用边界球体,但这并没有实现我的 每个 甚至许多对象方向的目标。

我怀疑更好的方法可能位于以下某处:

  1. 根据最极端物体点的window坐标,修改视图或投影矩阵或两者,以表示物体
  2. 将边界球方法换成边界框方法
  3. 获取绿色虚线角周围'virtual'帧的window坐标,并将帧图像投影到window(类似于1.)

* 我的代码在很大程度上取决于 Eric Haines 在 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 whosebug.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>

找到了一个合理的解决方案(包含在此处的实时代码段中),这主要归功于这两个相关帖子:

  • Move camera to fit 3D scene

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>