在投影到屏幕空间后,我试图找到球体的可见大小(以像素为单位)。球体以原点为中心,相机正对着它。因此,投影的球体应该是二维的完美圆。我知道这个1现有问题。但是,那里给出的公式似乎没有产生我想要的结果。它太小了几个百分点。我认为这是因为它没有正确考虑透视。投射到屏幕空间后,由于透视缩短(你只看到球体的一个上限而不是整个半球2),你看不到一半的球体,但显着更少。
如何推导出精确的二维边界圆?
答案 0 :(得分:12)
实际上,通过透视投影,您需要从相机的眼睛/中心计算球体“地平线”的高度(这个“地平线”由来自与球体相切的眼睛的光线决定)。
符号:
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
/ 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>
答案 1 :(得分:1)
让球体具有半径r
,并且与观察者距离d
。投影平面与观察者的距离为f
。
球体在半角asin(r/d)
下看到,因此表观半径为f.tan(asin(r/d))
,可写为f . r / sqrt(d^2 - r^2)
。 [错误的公式为f . r / d
。]
答案 2 :(得分:0)
老问题,但是简单的黑客攻击:
渲染一个具有两个相同球体的框架:一个位于屏幕中间,另一个位于角落。确保两个球体的中心与相机等距。
截取屏幕截图并将其加载到您最喜爱的图像编辑器中。
测量居中球体的范围。这个应该是一个统一的,没有扭曲的圆圈。它的半径与球体的立体角成正比。
测量偏移球体的范围。你需要得到左右半径。 (左侧将大于右侧。)假设您使用垂直视场创建投影矩阵,顶部和底部不会发生太大变化。
将偏移球体的左侧和右侧除以居中球体的半径。使用这些值计算扭曲球体半径与未失真版本的半径之比。
现在,当您需要球体的边界时,根据球体的投影中心位置在居中半径和扭曲半径之间进行插值。
这会将您的计算减少到基本算术。它不是真正的数学&#34;这样做的方式,但速度快,效果很好。特别是如果您正在这样做,比如计算几千个点光源的边界来剔除平铺渲染器。
(我更喜欢立体角版。)
答案 3 :(得分:0)
上面图解说明的可接受的答案非常好,但是我需要一个不知道视野的解决方案,而只是一个在世界和屏幕空间之间转换的矩阵,因此我不得不调整解决方案。
从另一个答案中重用一些变量名,计算球形帽的起点(线h
与线d
相交的点):
capOffset = cos(asin(l / d)) * r
capCenter = sphereCenter + ( sphereNormal * capOffset )
其中capCenter
和sphereCenter
是世界空间中的点,sphereNormal
是沿d
指向的,从球体中心到相机的归一化向量。
将点转换为屏幕空间:
capCenter2 = matrix.transform(capCenter)
向1
像素坐标中添加x
(或任意数量):
capCenter2.x += 1
将其转换回世界空间:
capCenter2 = matrix.inverse().transform(capCenter2)
测量世界空间中原始点与新点之间的距离,并除以您添加的数量即可得到比例因子:
scaleFactor = 1 / capCenter.distance(capCenter2)
将该比例因子乘以上限半径h
,以得到可见的屏幕半径(以像素为单位):
screenRadius = h * scaleFactor