像素完美投影矩阵?

Pixel-perfect projection matrix?

我正在尝试了解我应该将相机位置放置在 lookat 函数(或模型矩阵中的对象)中多远,以获得像素完美的坐标以传递给顶点着色器。

这对于正交投影矩阵来说实际上很简单,但我无法想象数学如何用于透视投影。

这是我使用的透视矩阵:

glm::mat4 projection = glm::perspective(45.0f, (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 10000.0f);

着色器中的顶点乘法非常简单:

gl_Position = projection * view * model * vec4(position.xy, 0.0f, 1.0);

我基本上是想在屏幕上显示一个需要旋转的四边形并显示透视效果(因此我不能使用正交投影),但我想在像素坐标中指定位置和方式大它应该出现在屏幕上。

好吧,如果你想使用梯形视锥,它只能有一个像素坐标 "z-plane"。

基础数学

如果您使用标准相机,则 (0,0,0) 处的相机的基本数学公式为

alpha 是垂直 fov(在你的情况下是 45°)

target_y = tan(alpha/2) * z 距离 * ((pixel_y/height)*2-1)
target_x = tan(alpha/2) * z 距离 * ((pixel_x/width)*纵横比*2-1)

反转投影

至于一般情况。您可以 "un-project" 在所有变换应该结束于特定点之前找到 3D 中的一个点。

基本上你需要取消计算。

gl_Position = projection * view * model * vec4(position.xy, 0.0f, 1.0);

因此,如果您有最终职位并想恢复它,您可以:

unprojection =  model^-1 * view^-1 *projection^-1 * gl_Position //not actual glsl notation, '^-1' being the inverse

这基本上就是 gluUnProjectglm::gtc::matrix_transform::unProject 等函数所做的。

但是你应该注意,在你应用投影矩阵之后的最终剪辑-space通常是[-1,-1,0]到[1,1,1],所以如果你想输入像素坐标,您可以应用一个额外的矩阵来转换成 space.

类似于:

               [2/width,        0,     0    -1]
               [      0, 2/height,     0    -1]
screenToClip = [      0,        0,     1     0]
               [      0,        0,     0     1]

会将 [0,0,0,1] 转换为 [-1,-1,0,1] 并将 [width,height,0,1] 转换为 [1,1,0,1]

此外,您可能最好尝试使用 0.5 之类的一些 z 值,以确保您处于视锥体内并且不会在前面或后面附近剪裁。

我将扩展 PeterT 的答案,并在此处留下我用来通过非投影找到截锥体平面之一的世界坐标的实用代码

这假设有一个基本的视图矩阵(相机位置在 0,0,0)

glm::mat4 projectionInv(0);
glm::mat4 projection = glm::perspective(45.0f, (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 500.0f);
projectionInv = glm::inverse(projection);

std::vector<glm::vec4> NDCCube;
NDCCube.push_back(glm::vec4(-1.0f, -1.0f,   -1.0f,     1.0f));
NDCCube.push_back(glm::vec4(1.0f, -1.0f,    -1.0f,     1.0f));
NDCCube.push_back(glm::vec4(1.0f, -1.0f,     1.0f,     1.0f));
NDCCube.push_back(glm::vec4(-1.0f, -1.0f,    1.0f,     1.0f));
NDCCube.push_back(glm::vec4(-1.0f, 1.0f,    -1.0f,     1.0f));
NDCCube.push_back(glm::vec4(1.0f, 1.0f,     -1.0f,     1.0f));
NDCCube.push_back(glm::vec4(1.0f, 1.0f,      1.0f,     1.0f));
NDCCube.push_back(glm::vec4(-1.0f, 1.0f,     1.0f,     1.0f));

std::vector<glm::vec3> frustumVertices;

for (int i = 0; i < 8; i++)
{
    glm::vec4 tempvec;
    tempvec = projectionInv * NDCCube.at(i); //multiply by projection matrix inverse to obtain frustum vertex
    frustumVertices.push_back(glm::vec3(tempvec.x /= tempvec.w, tempvec.y /= tempvec.w, tempvec.z /= tempvec.w));
}

请记住,如果您的透视距离低于我在投影矩阵中设置的距离,这些坐标将不会最终出现在屏幕上

您可以使用 60 度视野实现此效果。基本上你想把相机放在离观察平面一定距离的地方,这样相机就会形成一个等边三角形,中心点在屏幕的顶部和底部。

这里有一些代码可以做到这一点:

float fovy = 60.0f; // field of view - degrees
float aspect = nScreenWidth / nScreenHeight;
float zNearClip = 0.1f;
float zFarClip = nScreenHeight*2.0f;
float degToRad = MF_PI / 180.0f;
float fH = tanf(fovY * degToRad / 2.0f) * zNearClip;
float fW = fH * aspect;

glFrustum(-fW, fW, -fH, fH, zNearClip, zFarClip);

float nCameraDistance = sqrtf( nScreenHeight * nScreenHeight - 0.25f * nScreenHeight * nScreenHeight); 

glTranslatef(0, 0, -nCameraDistance);

您也可以使用 90 度视野。在这种情况下,相机距离是 window 高度的 1/2。不过,这个有很多透视感。

在 90 度的情况下,您可以将相机推出整个高度,然后对 x 和 y 分量应用 2 倍缩放(即:glScale (2,2,1)。

这是我的意思的图片:

如果您碰巧知道要精确显示像素的 "some item" 的世界坐标宽度,这最终会变成一些微不足道的三角函数(适用于 y FOV 或 x FOV):

S = Width of item in world coordinates
T = "Pixel Exact" size of item (say, the width of the texture)
h = Z distance to the object
a = 2 * h * tan(Phi / 2)
b = a / cos(phi / 2)
r = Total screen resolution (width or height depending on the FOV you want)

a = 2 * h * tan(Phi / 2) = (r / T) * S
Theta = atan(2*h / a)
Phi = 180 - 2*Theta

其中 b 是三角形的边,a 是三角形的底边,h 是三角形的高,theta 是等边三角形两个等角的夹角,Phi 是所得的 FOV

所以最终代码可能类似于

float frustumWidth = (float(ScreenWidth) / TextureWidth) * InWorldItemWidth; float theta = glm::degrees(atan((2 * zDistance) / frustumWidth)); float PixelPerfectFOV = 180 - 2 * theta;