在屏幕空间中投影球体的半径

14

我正在尝试找到将球投影到屏幕空间后的可见像素大小。该球以原点为中心,摄像机正对着它。因此,投影后的球应该在二维平面上呈现完美的圆形。我已经了解了这个1现有问题。然而,那里给出的公式似乎无法产生我想要的结果。它小了几个百分点。我认为这是因为它没有正确考虑透视的缘故。在投影到屏幕空间后,你看不到球体的一半,而是显著地少,由于透视收缩(你只能看到球体的一部分,而不是整个半球 2)。

我该如何推导出一个精确的二维边界圆?

3个回答

25

使用透视投影,需要计算从相机的眼睛/中心到球体“地平线”的高度(这个“地平线”由切线为球的射线确定)。

符号说明:

Notations

d:眼睛和球心之间的距离
r:球体半径
l:眼睛和球体“地平线”上一点之间的距离,l = sqrt(d^2 - r^2)
h:球体“地平线”的高度/半径
theta:从眼睛看“地平线”圆锥体的(半)角度
phi:补角,即theta的补集

h / l = cos(phi)

但是:

r / d = cos(phi)

所以,最终:

h = l * r / d = sqrt(d^2 - r^2) * r / d

然后,一旦你得到了 h ,只需应用标准公式(来自你链接的那个问题)即可在规范化视口中获得投影半径 pr

pr = cot(fovy / 2) * h / z

z 为观察者到球面“地平线”的距离:

z = l * cos(theta) = sqrt(d^2 - r^2) * h / r

所以:

pr = cot(fovy / 2) * r / sqrt(d^2 - r^2)
最后,将pr乘以height / 2以获取实际屏幕半径(以像素为单位)。接下来是一个使用three.js完成的小演示。可以通过使用n/fm/ps/w键对应的功能分别更改球体距离、半径和摄像机的垂直视野。在屏幕空间中渲染的黄色线段显示了球体在屏幕空间中半径的计算结果。这个计算是在computeProjectedRadius()函数中完成的。projected-sphere.js:
"use strict";

function computeProjectedRadius(fovy, d, r) {
  var fov;

  fov = fovy / 2 * Math.PI / 180.0;

//return 1.0 / Math.tan(fov) * r / d; // Wrong
  return 1.0 / Math.tan(fov) * r / Math.sqrt(d * d - r * r); // Right
}

function Demo() {
  this.width = 0;
  this.height = 0;

  this.scene = null;
  this.mesh = null;
  this.camera = null;

  this.screenLine = null;
  this.screenScene = null;
  this.screenCamera = null;

  this.renderer = null;

  this.fovy = 60.0;
  this.d = 10.0;
  this.r = 1.0;
  this.pr = computeProjectedRadius(this.fovy, this.d, this.r);
}

Demo.prototype.init = function() {
  var aspect;
  var light;
  var container;

  this.width = window.innerWidth;
  this.height = window.innerHeight;

  // World scene
  aspect = this.width / this.height;
  this.camera = new THREE.PerspectiveCamera(this.fovy, aspect, 0.1, 100.0);

  this.scene = new THREE.Scene();
  this.scene.add(THREE.AmbientLight(0x1F1F1F));

  light = new THREE.DirectionalLight(0xFFFFFF);
  light.position.set(1.0, 1.0, 1.0).normalize();
  this.scene.add(light);

  // Screen scene
  this.screenCamera = new THREE.OrthographicCamera(-aspect, aspect,
                                                   -1.0, 1.0,
                                                   0.1, 100.0);
  this.screenScene = new THREE.Scene();

  this.updateScenes();

  this.renderer = new THREE.WebGLRenderer({
    antialias: true
  });
  this.renderer.setSize(this.width, this.height);
  this.renderer.domElement.style.position = "relative";
  this.renderer.autoClear = false;

  container = document.createElement('div');
  container.appendChild(this.renderer.domElement);
  document.body.appendChild(container);
}

Demo.prototype.render = function() {
  this.renderer.clear();
  this.renderer.setViewport(0, 0, this.width, this.height);
  this.renderer.render(this.scene, this.camera);
  this.renderer.render(this.screenScene, this.screenCamera);
}

Demo.prototype.updateScenes = function() {
  var geometry;

  this.camera.fov = this.fovy;
  this.camera.updateProjectionMatrix();

  if (this.mesh) {
    this.scene.remove(this.mesh);
  }

  this.mesh = new THREE.Mesh(
    new THREE.SphereGeometry(this.r, 16, 16),
    new THREE.MeshLambertMaterial({
      color: 0xFF0000
    })
  );
  this.mesh.position.z = -this.d;
  this.scene.add(this.mesh);

  this.pr = computeProjectedRadius(this.fovy, this.d, this.r);

  if (this.screenLine) {
    this.screenScene.remove(this.screenLine);
  }

  geometry = new THREE.Geometry();
  geometry.vertices.push(new THREE.Vector3(0.0, 0.0, -1.0));
  geometry.vertices.push(new THREE.Vector3(0.0, -this.pr, -1.0));

  this.screenLine = new THREE.Line(
    geometry,
    new THREE.LineBasicMaterial({
      color: 0xFFFF00
    })
  );

  this.screenScene = new THREE.Scene();
  this.screenScene.add(this.screenLine);
}

Demo.prototype.onKeyDown = function(event) {
  console.log(event.keyCode)
  switch (event.keyCode) {
    case 78: // 'n'
      this.d /= 1.1;
      this.updateScenes();
      break;
    case 70: // 'f'
      this.d *= 1.1;
      this.updateScenes();
      break;
    case 77: // 'm'
      this.r /= 1.1;
      this.updateScenes();
      break;
    case 80: // 'p'
      this.r *= 1.1;
      this.updateScenes();
      break;
    case 83: // 's'
      this.fovy /= 1.1;
      this.updateScenes();
      break;
    case 87: // 'w'
      this.fovy *= 1.1;
      this.updateScenes();
      break;
  }
}

Demo.prototype.onResize = function(event) {
  var aspect;

  this.width = window.innerWidth;
  this.height = window.innerHeight;

  this.renderer.setSize(this.width, this.height);

  aspect = this.width / this.height;
  this.camera.aspect = aspect;
  this.camera.updateProjectionMatrix();

  this.screenCamera.left = -aspect;
  this.screenCamera.right = aspect;
  this.screenCamera.updateProjectionMatrix();
}

function onLoad() {
  var demo;

  demo = new Demo();
  demo.init();

  function animationLoop() {
    demo.render();
    window.requestAnimationFrame(animationLoop);
  }

  function onResizeHandler(event) {
    demo.onResize(event);
  }

  function onKeyDownHandler(event) {
    demo.onKeyDown(event);
  }

  window.addEventListener('resize', onResizeHandler, false);
  window.addEventListener('keydown', onKeyDownHandler, false);
  window.requestAnimationFrame(animationLoop);
}

index.html:

<!DOCTYPE html>
<html>
  <head>
    <title>Projected sphere</title>
      <style>
        body {
            background-color: #000000;
        }
      </style>
      <script src="http://cdnjs.cloudflare.com/ajax/libs/three.js/r61/three.min.js"></script>
      <script src="projected-sphere.js"></script>
    </head>
    <body onLoad="onLoad()">
      <div id="container"></div>
    </body>
</html>

哇,非常感谢您提供如此详细的答案! - BuschnicK
如果您使用像GeoGebra这样的工具来创建2D图形,请告诉我它的名称,好吗? - knivil
@knivil 我只是使用了Inkscape。 - user3146587
这个演示现在好像已经死了,是吧? - user362515

2
让球体半径为r,观察者距离球体d。投影平面距离观察者f
球体在半角度asin(r/d)下可见,因此表观半径为f.tan(asin(r/d)),可以写成f . r / sqrt(d^2 - r^2)。[错误的公式是f . r / d]。

2
上面的示例答案非常好,但我需要一种不需要了解视野的解决方案,只需要一个将世界坐标系和屏幕坐标系之间进行转换的矩阵,因此我不得不对解决方案进行调整。
  1. Reusing some variable names from the other answer, calculate the start point of the spherical cap (the point where line h meets line d):

    capOffset = cos(asin(l / d)) * r
    capCenter = sphereCenter + ( sphereNormal * capOffset )
    

    where capCenter and sphereCenter are points in world space, and sphereNormal is a normalized vector pointing along d, from the sphere center towards the camera.

  2. Transform the point to screen space:

    capCenter2 = matrix.transform(capCenter)
    
  3. Add 1 (or any amount) to the x pixel coordinate:

    capCenter2.x += 1
    
  4. Transform it back to world space:

    capCenter2 = matrix.inverse().transform(capCenter2)
    
  5. Measure the distance between the original and new points in world space, and divide into the amount you added to get a scale factor:

    scaleFactor = 1 / capCenter.distance(capCenter2)
    
  6. Multiply that scale factor by the cap radius h to get the visible screen radius in pixels:

    screenRadius = h * scaleFactor
    

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接