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

Math 屏幕空间中投影球体的半径,math,3d,geometry,projection,Math,3d,Geometry,Projection,我试图在投影到屏幕空间后,找到球体的可见大小(以像素为单位)。球体以原点为中心,相机正对着它。因此,投影球体应该是二维的完美圆。我知道这个现存的问题。然而,这里给出的公式似乎不能产生我想要的结果。它太小了百分之几。我认为这是因为它没有正确地考虑透视图。投影到屏幕空间后,您看不到球体的一半,但由于透视缩短(您只看到球体的一个帽状物,而不是整个半球),看到的球体明显减少 如何导出精确的二维边界圆?事实上,使用透视投影,您需要计算球体“地平线”相对于眼睛/相机中心的高度(该“地平线”由眼睛与球体相切的

我试图在投影到屏幕空间后,找到球体的可见大小(以像素为单位)。球体以原点为中心,相机正对着它。因此,投影球体应该是二维的完美圆。我知道这个现存的问题。然而,这里给出的公式似乎不能产生我想要的结果。它太小了百分之几。我认为这是因为它没有正确地考虑透视图。投影到屏幕空间后,您看不到球体的一半,但由于透视缩短(您只看到球体的一个帽状物,而不是整个半球),看到的球体明显减少


如何导出精确的二维边界圆?

事实上,使用透视投影,您需要计算球体“地平线”相对于眼睛/相机中心的高度(该“地平线”由眼睛与球体相切的光线确定)

符号:

d
:眼睛和球体中心之间的距离
r
:球体的半径
l
:眼睛与球体“地平线”上的点之间的距离,
l=sqrt(d^2-r^2)

h
:球体“地平线”的高度/半径
theta
:“地平线”圆锥体与眼睛的(半)角
phi
:θ的互补角

h / l = cos(phi)
但是:

因此,最后:

h = l * r / d = sqrt(d^2 - r^2) * r / d
然后,一旦您有了
h
,只需应用标准公式(您链接的问题中的公式)即可获得规范化视口中的投影半径
pr

pr = cot(fovy / 2) * h / z
使用
z
从眼睛到球体“地平线”平面的距离:

因此:

最后,将
pr
乘以
height/2
,得到以像素为单位的实际屏幕半径

下面是一个小演示。可以分别使用
n
/
f
m
/
p
s
/
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>

投影球
身体{
背景色:#000000;
}

让球体具有半径
r
,并在距离观察者
d
一定距离处可见。投影平面距离观察者
f


球体在半角
asin(r/d)
下可见,因此视半径为
f.tan(asin(r/d))
,可以写成
f。r/sqrt(d^2-r^2)
。[错误的公式是
f.r/d
]

老问题,但非常简单:

渲染具有两个相同球体的帧:一个位于屏幕中间,一个位于角落。确保两个球体的中心与摄影机的距离相等

拍摄一个屏幕截图并将其加载到您最喜爱的图像编辑器中

测量居中球体的范围。这一个应该是一个均匀的、不扭曲的圆。它的半径将与球体的立体角成比例

测量偏移球体的范围。你需要得到左右半径。(左侧将大于右侧。)假设使用垂直视野创建投影矩阵,则顶部和底部的变化不大

将偏移球体的左侧和右侧除以居中球体的半径。使用这些值计算扭曲球体半径与未扭曲球体半径的比率

现在,当需要球体的边界时,根据球体的投影中心位置在中心半径和扭曲半径之间插值

这将使您的计算简化为基本算术。这不是一种“真正的数学”方法,但它速度快,效果很好。特别是如果您这样做是为了(比如)计算要在平铺渲染器中剔除的几千个点光源的边界


(我更喜欢立体角版本。)

上面的答案很好,但我需要一个不知道视野的解决方案,只是一个在世界和屏幕空间之间转换的矩阵,所以我必须调整该解决方案

  • 重复使用另一个答案中的一些变量名称,计算球形帽的起点(线
    h
    与线
    d
    相交的点):

    其中,
    capCenter
    sphereCenter
    是世界空间中的点,
    sphereNormal
    是从球体中心指向相机的沿着
    d
    的标准化向量

  • 将点转换为屏幕空间:

    capCenter2 = matrix.transform(capCenter)
    
  • 1
    (或任意数量)添加到
    x
    像素坐标:

    capCenter2.x += 1
    
  • 将其转换回世界空间:

    capCenter2 = matrix.inverse().transform(capCenter2)
    
  • 测量世界空间中原始点和新点之间的距离,并将其除以为获得比例因子而添加的量:

    scaleFactor = 1 / capCenter.distance(capCenter2)
    
  • 将该比例因子乘以cap radius
    h
    ,得到以像素为单位的可见屏幕半径:

    screenRadius = h * scaleFactor
    

  • 哇,非常感谢你的详细回答!如果你使用GeoGebra这样的工具来创建二维图形,那么你能告诉我它的名称吗?@knivil我只是使用Inkscape。演示现在似乎已经过时了,不是吗?
    capCenter2.x += 1
    
    capCenter2 = matrix.inverse().transform(capCenter2)
    
    scaleFactor = 1 / capCenter.distance(capCenter2)
    
    screenRadius = h * scaleFactor