无论其透视方向如何,侧面不相等的3D框如何填充视口? [英] How can a 3D box with unequal sides fill the viewport, no matter its orientation in perspective?
问题描述
如随附的( three.js ]中所示)的实时代码段(也位于 jsfiddle.net/gpolyn/une6tst5/21 )*,我有一个侧面不相等的盒子,观众可以重新放置通过拖动。视口中最左,右,顶部或底部的角由绿色正方形点动态表示。
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)。
* 我的代码很大程度上取决于Eric Haines在 www.realtimerendering.com/udacity/transforms.html ,而绿点技术则来自 WestLangley
* 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>
推荐答案
找到了一个合理的解决方案(包含在实时代码段中,在此),这主要归功于以下两个相关帖子:
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>
这篇关于无论其透视方向如何,侧面不相等的3D框如何填充视口?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!