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

爷,独闯天下 提交于 2020-01-07 00:34:28

问题


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>

回答1:


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

  • Move camera to fit 3D scene
  • Convert screen coordinate to world coordinate WITHOUT GLUUNPROJECT

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>


来源:https://stackoverflow.com/questions/37923651/how-can-a-3d-box-with-unequal-sides-fill-the-viewport-no-matter-its-orientation

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!