如何计算给定坐标处相机可见的矩形的大小?

Xåp*_* - 4 javascript frustum perspectivecamera three.js

我制作了一个小 Three.js 应用程序,它将一堆圆圈从画布底部移动到顶部:

let renderer, scene, light, circles, camera;

initialize();
animate();

function initialize() {
  renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

  scene = new THREE.Scene();

  light = new THREE.AmbientLight();
  scene.add(light);

  circles = new THREE.Group();
  scene.add(circles);

  camera = new THREE.PerspectiveCamera(45, renderer.domElement.clientWidth / renderer.domElement.clientHeight, 1);
  camera.position.z = circles.position.z + 500;
}


function animate() {
  // Update each circle.
  Array.from(circles.children).forEach(circle => {
    if (circle.position.y < visibleBox(circle.position.z).max.y) {
      circle.position.y += 4;
    } else {
      circles.remove(circle);
    }
  });

  // Create a new circle.
  let circle = new THREE.Mesh();
  circle.geometry = new THREE.CircleGeometry(30, 30);
  circle.material = new THREE.MeshToonMaterial({ color: randomColor(), transparent: true, opacity: 0.5 });
  circle.position.z = _.random(camera.position.z - camera.far, camera.position.z - (camera.far / 10));
  circle.position.x = _.random(visibleBox(circle.position.z).min.x, visibleBox(circle.position.z).max.x);
  circle.position.y = visibleBox(circle.position.z).min.y;
  circles.add(circle);

  // Render the scene.
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}

function visibleBox(z) {
 return new THREE.Box2(
   new THREE.Vector2(-1000, -1000),
   new THREE.Vector2(1000, 1000)
 );
}

function randomColor() {
  return `#${ _.sampleSize("abcdef0123456789", 6).join("")}`;
}
Run Code Online (Sandbox Code Playgroud)
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
}
Run Code Online (Sandbox Code Playgroud)
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/87/three.js">
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js">
</script>
Run Code Online (Sandbox Code Playgroud)

我使用该函数visibleBox(z)来确定在何处创建和销毁每个圆。我已经为这个函数硬编码了一个返回值,但我希望它计算给定深度下相机可见的矩形的大小,z.

截头体

换句话说,我希望每个圆都准确地创建在相机视锥体的底部(上图中红色矩形的底部边缘),并在它到达视锥体的顶部(顶部边缘红色矩形)。

那么,我如何计算这个矩形?

Rab*_*d76 5

像这样更改函数:

function visibleBox(z) {
    var t = Math.tan( THREE.Math.degToRad( camera.fov ) / 2 )
    var h = t * 2 * z;
    var w = h * camera.aspect;
    return new THREE.Box2(new THREE.Vector2(-w, h), new THREE.Vector2(w, -h));
}
Run Code Online (Sandbox Code Playgroud)

并像这样设置圆圈位置:

circle.position.z = _.random(-camera.near, -camera.far);
var visBox = visibleBox(circle.position.z)
circle.position.x = _.random(visBox.min.x, visBox.max.x);
circle.position.y = visBox.min.y;
Run Code Online (Sandbox Code Playgroud)

代码演示:

function visibleBox(z) {
    var t = Math.tan( THREE.Math.degToRad( camera.fov ) / 2 )
    var h = t * 2 * z;
    var w = h * camera.aspect;
    return new THREE.Box2(new THREE.Vector2(-w, h), new THREE.Vector2(w, -h));
}
Run Code Online (Sandbox Code Playgroud)
circle.position.z = _.random(-camera.near, -camera.far);
var visBox = visibleBox(circle.position.z)
circle.position.x = _.random(visBox.min.x, visBox.max.x);
circle.position.y = visBox.min.y;
Run Code Online (Sandbox Code Playgroud)


解释

投影矩阵描述了从场景的 3D 点到视口的 2D 点的映射。它从眼睛空间转换到剪辑空间,剪辑空间中的坐标通过除以w剪辑坐标的分量转换为归一化设备坐标(NDC)。NDC 在 (-1,-1,-1) 到 (1,1,1) 范围内。

在此处输入图片说明


在透视投影中,深度值和到相机的 z 距离之间的关系不是线性的。
透视投影矩阵如下所示:

r = right, l = left, b = bottom, t = top, n = near, f = far

2*n/(r-l)      0              0               0
0              2*n/(t-b)      0               0
(r+l)/(r-l)    (t+b)/(t-b)    -(f+n)/(f-n)    -1    
0              0              -2*f*n/(f-n)    0
Run Code Online (Sandbox Code Playgroud)

由此得出视图空间中 z 坐标与归一化设备坐标 z 分量和深度之间的关系:

z_ndc = ( -z_eye * (f+n)/(f-n) - 2*f*n/(f-n) ) / -z_eye
depth = (z_ndc + 1.0) / 2.0
Run Code Online (Sandbox Code Playgroud)

反向操作如下所示:

n = near, f = far

z_ndc = 2.0 * depth - 1.0;
z_eye = 2.0 * n * f / (f + n - z_ndc * (f - n));
Run Code Online (Sandbox Code Playgroud)

如果透视投影矩阵已知,则可以按如下方式完成:

A = prj_mat[2][2]
B = prj_mat[3][2]
z_eye = B / (A + z_ndc)
Run Code Online (Sandbox Code Playgroud)

请参阅如何在现代 OpenGL 中使用片段着色器中的 gl_FragCoord.z 线性渲染深度?


视空间中的投影面积与视空间的Z坐标之间的关系是线性的。它取决于视场角和纵横比。

在此处输入图片说明

标准化的设备尺寸可以转换为视图空间中的尺寸,如下所示:

aspect = w / h
tanFov = tan( fov_y * 0.5 );

size_x = ndx_size_x * (tanFov * aspect) * z_eye;
size_y = ndx_size_y * tanFov * z_eye;
Run Code Online (Sandbox Code Playgroud)

如果透视投影矩阵已知并且投影是对称的(视线在视口的中心并且视野没有位移),则可以这样做:

size_x = ndx_size_x * / (prj_mat[0][0] * z_eye);
size_y = ndx_size_y * / (prj_mat[1][1] * z_eye);
Run Code Online (Sandbox Code Playgroud)

参见图+长宽比+查看从投影矩阵(HMD OST校准)矩阵的场


请注意,标准化设备坐标中的每个位置都可以通过逆投影矩阵转换为视图空间坐标:

mat4 inversePrjMat = inverse( prjMat );
vec4 viewPosH      = inversePrjMat * vec3( ndc_x, ndc_y, 2.0 * depth - 1.0, 1.0 );
vec3 viewPos       = viewPos.xyz / viewPos.w;
Run Code Online (Sandbox Code Playgroud)

请参阅如何在给定视图空间深度值和 ndc xy 的情况下恢复视图空间位置


这意味着具有特定深度的未投影矩形可以这样计算:

vec4 viewLowerLeftH  = inversePrjMat * vec3( -1.0, -1.0, 2.0 * depth - 1.0, 1.0 );
vec4 viewUpperRightH = inversePrjMat * vec3(  1.0,  1.0, 2.0 * depth - 1.0, 1.0 );
vec3 viewLowerLeft   = viewLowerLeftH.xyz / viewLowerLeftH.w;
vec3 viewUpperRight  = viewUpperRightH.xyz / viewUpperRightH.w;
Run Code Online (Sandbox Code Playgroud)