如何从 VRFrameData 计算 FOV?
How to calculate FOV from VRFrameData?
VREyeParameters
, but that was deprecated. So now i am wondering: Is possible to calculate that using the view/projection matrices provided by VRFrameData
?
里面原来有视野信息
投影矩阵描述了从场景的 3D 点到视口的 2D 点的映射。投影矩阵从视图 space 变换到剪辑 space。剪辑 space 坐标为 Homogeneous coordinates。剪辑 space 中的坐标通过除以 w
转换为 (-1, -1, -1) 到 (1, 1, 1) 范围内的标准化设备坐标 (NDC)剪辑坐标的分量。
在透视投影中,投影矩阵描述了从针孔相机看到的世界中的 3D 点到视口的 2D 点的映射。
相机平截头体(截棱锥)中的眼睛 space 坐标映射到立方体(归一化设备坐标)。
如果你想知道视图space中相机视锥体的角点,那么你必须变换归一化设备的角点space (-1, -1, -1) , ..., (1, 1, 1) 通过 逆投影矩阵 。要得到 cartesian coordinates,结果的 X、Y 和 Z 分量必须除以结果的 W(第 4)分量。
glMatrix 是一个提供矩阵运算和数据类型的库,例如 mat4
和 vec4
:
projection = mat4.clone( VRFrameData.leftProjectionMatrix );
inverse_prj = mat4.create();
mat4.invert( inverse_prj, projection );
pt_ndc = [-1, -1, -1];
v4_ndc = vec4.fromValues( pt_ndc[0], pt_ndc[1], pt_ndc[2], 1 );
v4_view = vec4.create();
vec4.transformMat4( v4_view, v4_ndc, inverse_prj );
pt_view = [v4_view[0]/v4_view[3], v4_view[1]/v4_view[3], v4_view[2]/v4_view[3]];
视图坐标到世界坐标的变换可以通过逆视图矩阵来完成。
view = mat4.clone( VRFrameData.leftViewMatrix );
inverse_view = mat4.create();
mat4.invert( inverse_view, view );
v3_view = vec3.clone( pt_view );
v3_world = vec3.create();
mat4.transformMat4( v3_world, v3_view, inverse_view );
注意,左右投影矩阵不对称。这意味着视线不在平截头体的中心,左右眼不同。
进一步注意,透视投影矩阵如下所示:
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
其中:
a = w / h
ta = tan( fov_y / 2 );
2 * n / (r-l) = 1 / (ta * a)
2 * n / (t-b) = 1 / ta
如果投影是对称的,视线在视口中心,视野没有位移,那么矩阵可以简化:
1/(ta*a) 0 0 0
0 1/ta 0 0
0 0 -(f+n)/(f-n) -1
0 0 -2*f*n/(f-n) 0
这意味着视角可以通过以下方式计算:
fov_y = Math.atan( 1/prjMat[5] ) * 2; // prjMat[5] is prjMat[1][1]
宽高比为:
aspect = prjMat[5] / prjMat[0];
如果投影矩阵仅沿水平方向对称,则视野角的计算也有效。这意味着如果 -bottom
等于 top
。 2只眼睛的投影矩阵应该是这样的。
此外:
z_ndc = 2.0 * depth - 1.0;
z_eye = 2.0 * n * f / (f + n - z_ndc * (f - n));
通过替换投影矩阵的字段,这是:
A = prj_mat[2][2]
B = prj_mat[3][2]
z_eye = B / (A + z_ndc)
这意味着到近平面和远平面的距离可以通过以下方式计算:
A = prj_mat[10]; // prj_mat[10] is prj_mat[2][2]
B = prj_mat[14]; // prj_mat[14] is prj_mat[3][2]
near = - B / (A - 1);
far = - B / (A + 1);
SOHCAHTOA 发音为“So”、“cah”、“toe-ah”
- SOH -> 正弦(角度)= 斜边相反
- CAH -> 余弦(角度)= 与斜边相邻
- TOA -> 切线(角度)= 与邻线相反
告诉我们直角三角形各边与各种三角函数的关系
所以看一个平截头体图像,我们可以取直角三角形从眼睛到近平面到平截头体的顶部来计算视野的切线,我们可以使用反正切来转切线变回一个角度。
因为我们知道投影矩阵的结果采用我们的世界 space 截锥并将其转换为剪辑 space 并最终转换为归一化设备 space (-1, -1, -1)到(+1,+1,+1)我们可以通过将NDCspace中的对应点乘以投影矩阵的逆矩阵
得到我们需要的位置
eye = 0,0,0
centerAtNearPlane = inverseProjectionMatrix * (0,0,-1)
topCenterAtNearPlane = inverseProjectionMatrix * (0, 1, -1)
然后
opposite = topCenterAtNearPlane.y
adjacent = -centerAtNearPlane.z
halfFieldOfView = Math.atan2(opposite, adjacent)
fieldOfView = halfFieldOfView * 2
让我们测试一下
const m4 = twgl.m4;
const fovValueElem = document.querySelector("#fovValue");
const resultElem = document.querySelector("#result");
let fov = degToRad(45);
function updateFOV() {
fovValueElem.textContent = radToDeg(fov).toFixed(1);
// get a projection matrix from somewhere (like VR)
const projection = getProjectionMatrix();
// now that we have projection matrix recompute the FOV from it
const inverseProjection = m4.inverse(projection);
const centerAtZNear = m4.transformPoint(inverseProjection, [0, 0, -1]);
const topCenterAtZNear = m4.transformPoint(inverseProjection, [0, 1, -1]);
const opposite = topCenterAtZNear[1];
const adjacent = -centerAtZNear[2];
const halfFieldOfView = Math.atan2(opposite, adjacent);
const fieldOfView = halfFieldOfView * 2;
resultElem.textContent = radToDeg(fieldOfView).toFixed(1);
}
updateFOV();
function getProjectionMatrix() {
// doesn't matter. We just want a projection matrix as though
// someone else made it for us.
const aspect = 2 / 1;
// choose some zNear and zFar
const zNear = .5;
const zFar = 100;
return m4.perspective(fov, aspect, zNear, zFar);
}
function radToDeg(rad) {
return rad / Math.PI * 180;
}
function degToRad(deg) {
return deg / 180 * Math.PI;
}
document.querySelector("input").addEventListener('input', (e) => {
fov = degToRad(parseInt(e.target.value));
updateFOV();
});
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<input id="fov" type="range" min="1" max="179" value="45"><label>fov: <span id="fovValue"></span></label>
<div>computed fov: <span id="result"></span></div>
请注意,这是假设视锥体的中心直接位于眼睛前方。如果不是,那么您可能必须通过计算从眼睛到 centerAtZNear
的矢量长度来计算 adjacent
const v3 = twgl.v3;
...
const adjacent = v3.length(centerAtZNear);
VREyeParameters
, but that was deprecated. So now i am wondering: Is possible to calculate that using the view/projection matrices provided by VRFrameData
?
投影矩阵描述了从场景的 3D 点到视口的 2D 点的映射。投影矩阵从视图 space 变换到剪辑 space。剪辑 space 坐标为 Homogeneous coordinates。剪辑 space 中的坐标通过除以 w
转换为 (-1, -1, -1) 到 (1, 1, 1) 范围内的标准化设备坐标 (NDC)剪辑坐标的分量。
在透视投影中,投影矩阵描述了从针孔相机看到的世界中的 3D 点到视口的 2D 点的映射。
相机平截头体(截棱锥)中的眼睛 space 坐标映射到立方体(归一化设备坐标)。
如果你想知道视图space中相机视锥体的角点,那么你必须变换归一化设备的角点space (-1, -1, -1) , ..., (1, 1, 1) 通过 逆投影矩阵 。要得到 cartesian coordinates,结果的 X、Y 和 Z 分量必须除以结果的 W(第 4)分量。
glMatrix 是一个提供矩阵运算和数据类型的库,例如 mat4
和 vec4
:
projection = mat4.clone( VRFrameData.leftProjectionMatrix );
inverse_prj = mat4.create();
mat4.invert( inverse_prj, projection );
pt_ndc = [-1, -1, -1];
v4_ndc = vec4.fromValues( pt_ndc[0], pt_ndc[1], pt_ndc[2], 1 );
v4_view = vec4.create();
vec4.transformMat4( v4_view, v4_ndc, inverse_prj );
pt_view = [v4_view[0]/v4_view[3], v4_view[1]/v4_view[3], v4_view[2]/v4_view[3]];
视图坐标到世界坐标的变换可以通过逆视图矩阵来完成。
view = mat4.clone( VRFrameData.leftViewMatrix );
inverse_view = mat4.create();
mat4.invert( inverse_view, view );
v3_view = vec3.clone( pt_view );
v3_world = vec3.create();
mat4.transformMat4( v3_world, v3_view, inverse_view );
注意,左右投影矩阵不对称。这意味着视线不在平截头体的中心,左右眼不同。
进一步注意,透视投影矩阵如下所示:
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
其中:
a = w / h
ta = tan( fov_y / 2 );
2 * n / (r-l) = 1 / (ta * a)
2 * n / (t-b) = 1 / ta
如果投影是对称的,视线在视口中心,视野没有位移,那么矩阵可以简化:
1/(ta*a) 0 0 0
0 1/ta 0 0
0 0 -(f+n)/(f-n) -1
0 0 -2*f*n/(f-n) 0
这意味着视角可以通过以下方式计算:
fov_y = Math.atan( 1/prjMat[5] ) * 2; // prjMat[5] is prjMat[1][1]
宽高比为:
aspect = prjMat[5] / prjMat[0];
如果投影矩阵仅沿水平方向对称,则视野角的计算也有效。这意味着如果 -bottom
等于 top
。 2只眼睛的投影矩阵应该是这样的。
此外:
z_ndc = 2.0 * depth - 1.0;
z_eye = 2.0 * n * f / (f + n - z_ndc * (f - n));
通过替换投影矩阵的字段,这是:
A = prj_mat[2][2]
B = prj_mat[3][2]
z_eye = B / (A + z_ndc)
这意味着到近平面和远平面的距离可以通过以下方式计算:
A = prj_mat[10]; // prj_mat[10] is prj_mat[2][2]
B = prj_mat[14]; // prj_mat[14] is prj_mat[3][2]
near = - B / (A - 1);
far = - B / (A + 1);
SOHCAHTOA 发音为“So”、“cah”、“toe-ah”
- SOH -> 正弦(角度)= 斜边相反
- CAH -> 余弦(角度)= 与斜边相邻
- TOA -> 切线(角度)= 与邻线相反
告诉我们直角三角形各边与各种三角函数的关系
所以看一个平截头体图像,我们可以取直角三角形从眼睛到近平面到平截头体的顶部来计算视野的切线,我们可以使用反正切来转切线变回一个角度。
因为我们知道投影矩阵的结果采用我们的世界 space 截锥并将其转换为剪辑 space 并最终转换为归一化设备 space (-1, -1, -1)到(+1,+1,+1)我们可以通过将NDCspace中的对应点乘以投影矩阵的逆矩阵
得到我们需要的位置eye = 0,0,0
centerAtNearPlane = inverseProjectionMatrix * (0,0,-1)
topCenterAtNearPlane = inverseProjectionMatrix * (0, 1, -1)
然后
opposite = topCenterAtNearPlane.y
adjacent = -centerAtNearPlane.z
halfFieldOfView = Math.atan2(opposite, adjacent)
fieldOfView = halfFieldOfView * 2
让我们测试一下
const m4 = twgl.m4;
const fovValueElem = document.querySelector("#fovValue");
const resultElem = document.querySelector("#result");
let fov = degToRad(45);
function updateFOV() {
fovValueElem.textContent = radToDeg(fov).toFixed(1);
// get a projection matrix from somewhere (like VR)
const projection = getProjectionMatrix();
// now that we have projection matrix recompute the FOV from it
const inverseProjection = m4.inverse(projection);
const centerAtZNear = m4.transformPoint(inverseProjection, [0, 0, -1]);
const topCenterAtZNear = m4.transformPoint(inverseProjection, [0, 1, -1]);
const opposite = topCenterAtZNear[1];
const adjacent = -centerAtZNear[2];
const halfFieldOfView = Math.atan2(opposite, adjacent);
const fieldOfView = halfFieldOfView * 2;
resultElem.textContent = radToDeg(fieldOfView).toFixed(1);
}
updateFOV();
function getProjectionMatrix() {
// doesn't matter. We just want a projection matrix as though
// someone else made it for us.
const aspect = 2 / 1;
// choose some zNear and zFar
const zNear = .5;
const zFar = 100;
return m4.perspective(fov, aspect, zNear, zFar);
}
function radToDeg(rad) {
return rad / Math.PI * 180;
}
function degToRad(deg) {
return deg / 180 * Math.PI;
}
document.querySelector("input").addEventListener('input', (e) => {
fov = degToRad(parseInt(e.target.value));
updateFOV();
});
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<input id="fov" type="range" min="1" max="179" value="45"><label>fov: <span id="fovValue"></span></label>
<div>computed fov: <span id="result"></span></div>
请注意,这是假设视锥体的中心直接位于眼睛前方。如果不是,那么您可能必须通过计算从眼睛到 centerAtZNear
的矢量长度来计算adjacent
const v3 = twgl.v3;
...
const adjacent = v3.length(centerAtZNear);