透视纠正纹理映射;Z距离计算可能有误。

8
我正在制作一款软件光栅化器,但遇到了一个小问题:似乎无法实现透视纠正的纹理映射。
我的算法是首先按照y坐标对要绘制的坐标进行排序。这将返回一个最高点、最低点和中心点。然后我使用增量沿着扫描线行走:
// ordering by y is put here

order[0] = &a_Triangle.p[v_order[0]];
order[1] = &a_Triangle.p[v_order[1]];
order[2] = &a_Triangle.p[v_order[2]];

float height1, height2, height3;

height1 = (float)((int)(order[2]->y + 1) - (int)(order[0]->y));
height2 = (float)((int)(order[1]->y + 1) - (int)(order[0]->y));
height3 = (float)((int)(order[2]->y + 1) - (int)(order[1]->y));

// x 

float x_start, x_end;
float x[3];
float x_delta[3];

x_delta[0] = (order[2]->x - order[0]->x) / height1;
x_delta[1] = (order[1]->x - order[0]->x) / height2;
x_delta[2] = (order[2]->x - order[1]->x) / height3;

x[0] = order[0]->x;
x[1] = order[0]->x;
x[2] = order[1]->x;

然后,我们从order[0]->yorder[2]->y 进行渲染,通过增加 x_startx_end 来实现delta的变化。在渲染顶部时,delta的值是 x_delta[0]x_delta[1]。在渲染底部时,delta的值是 x_delta[0]x_delta[2]。然后,我们在线性扫描线上在x_start和x_end之间进行插值处理。UV坐标以相同的方式按y排序,并从开始和结束位置开始,每一步应用delta。

这种方法很好用,但是当我尝试进行透视校正UV映射时就会出问题。基本算法是对于每个顶点,取UV/z1/z,并在它们之间进行插值处理。对于每个像素,UV坐标变为UV_current * z_current。 然而,结果如下所示:

alt text

倒置的部分告诉您delta何时翻转。如您所见,两个三角形都似乎指向地平线上的不同点。

以下是我用于计算空间中某点Z值的代码:

float GetZToPoint(Vec3 a_Point)
{
    Vec3 projected = m_Rotation * (a_Point - m_Position);

    // #define FOV_ANGLE 60.f
    // static const float FOCAL_LENGTH = 1 / tanf(_RadToDeg(FOV_ANGLE) / 2);
    // static const float DEPTH = HALFHEIGHT * FOCAL_LENGTH; 
    float zcamera = DEPTH / projected.z;

    return zcamera;
}

我理解你在问是否存在z缓冲区问题。
3个回答

5
ZBuffer与此无关。
当三角形重叠并且您想确保它们正确绘制(例如在Z中正确排序)时,ZBuffer才有用。对于三角形的每个像素,ZBuffer将确定以前放置的像素是否更靠近相机,如果是,则不绘制您三角形的像素。
由于您正在绘制不重叠的2个三角形,这不可能是问题所在。
我曾经制作过一个固定点软件光栅化器(用于移动电话),但我没有源代码。所以让我今晚查一下我是如何做到的。实质上,您拥有的东西并不坏!这样的事情可能是由非常小的错误引起的。
调试此问题的一般提示是具有几个测试三角形(左侧斜率,右侧斜率,90度角等等),并使用调试器逐步处理它,并查看您的逻辑如何处理这些情况。
我的光栅化器的伪代码(仅考虑U、V和Z...如果您还想进行gouraud,则必须像您为U、V和Z所做的那样对R G和B执行所有操作:
思路是将三角形分解为2个部分。顶部和底部。顶部从y [0]到y [1],底部从y [1]到y [2]。对于两组,您需要计算用于插值的步骤变量。下面的示例显示了如何执行顶部部分。如果需要,我也可以提供底部部分。
请注意,我已经在下面的“伪代码”片段中计算了底部所需的插值偏移量
- 首先按照coord [0] .y < coord [1] .y < coord [2] .y的顺序对坐标(x,y,z,u,v)进行排序 - 接下来检查任何2组坐标是否相同(仅检查x和y)。如果是,则不要绘制 - 异常:三角形是否有平顶?如果是,则第一个斜率将是无限大 - 异常2:三角形是否有平底(是的,三角形也可以有这些;^)),那么最后一个斜率也将是无限大 - 计算2个斜率(左侧和右侧) leftDeltaX =(x [1] - x [0])/(y [1] - y [0])和rightDeltaX =(x [2] - x [0])/(y [2] - y [0]) - 三角形的第二部分取决于:如果三角形的左侧现在确实在左侧(或需要交换)
代码片段:
 if (leftDeltaX < rightDeltaX)
 {
      leftDeltaX2 = (x[2]-x[1]) / (y[2]-y[1])
      rightDeltaX2 = rightDeltaX
      leftDeltaU = (u[1]-u[0]) / (y[1]-y[0]) //for texture mapping
      leftDeltaU2 = (u[2]-u[1]) / (y[2]-y[1])
      leftDeltaV = (v[1]-v[0]) / (y[1]-y[0]) //for texture mapping
      leftDeltaV2 = (v[2]-v[1]) / (y[2]-y[1])
      leftDeltaZ = (z[1]-z[0]) / (y[1]-y[0]) //for texture mapping
      leftDeltaZ2 = (z[2]-z[1]) / (y[2]-y[1])
 }
 else
 {
      swap(leftDeltaX, rightDeltaX);
      leftDeltaX2 = leftDeltaX;
      rightDeltaX2 = (x[2]-x[1]) / (y[2]-y[1])
      leftDeltaU = (u[2]-u[0]) / (y[2]-y[0]) //for texture mapping
      leftDeltaU2 = leftDeltaU
      leftDeltaV = (v[2]-v[0]) / (y[2]-y[0]) //for texture mapping
      leftDeltaV2 = leftDeltaV
      leftDeltaZ = (z[2]-z[0]) / (y[2]-y[0]) //for texture mapping
      leftDeltaZ2 = leftDeltaZ
  }
  • 将currentLeftX和currentRightX都设置为x[0]
  • 将currentLeftU设置为leftDeltaU,将currentLeftV设置为leftDeltaV,将currentLeftZ设置为leftDeltaZ
  • 计算第一个Y值范围的起点和终点:startY = ceil(y[0]);endY = ceil(y[1])
  • 预处理x、u、v和z的分数部分以获得子像素精度(我猜测对于浮点数也是必要的。对于我的定点算法,这是必需的,以使线条和纹理给人以比显示分辨率更精细的移动幻觉)
  • 计算y[1]处的x位置:halfwayX = (x [2]-x[0]) * (y[1]-y[0]) / (y[2]-y[0]) + x[0];同样的,对于U、V和z也是如此:halfwayU = (u [2]-u [0]) * (y[1]-y [0]) / (y[2]-y [0]) + u [0]
  • 利用halfwayX计算U、V和z的步进器:如果(halfwayX - x[1] == 0){ slopeU=0, slopeV=0, slopeZ=0 } else { slopeU = (halfwayU - U[1]) / (halfwayX - x[1])}(v和z同理)
  • 对Y顶部进行剪切(因此在三角形顶部超出屏幕(或剪辑矩形)时计算我们将开始绘制的位置)
  • for(y=startY; y < endY; y++) {
    • Y是否超过屏幕底部?停止渲染!
    • 计算第一行水平线的startX和endX leftCurX = ceil(startx); leftCurY = ceil(endy);
    • 将要绘制的线剪裁到屏幕左边框(或剪辑区域)
    • 准备指向目标缓冲区的指针(每次通过数组索引进行太慢了)unsigned int buf = destbuf + (ypitch) + startX;(如果您正在使用24位或32位渲染,则为unsigned int)在这里还要准备ZBuffer指针(如果您使用的话)
    • for(x=startX; x < endX; x++) {
      • 现在对于透视纹理映射(不使用双线性插值),您需要执行以下操作:

代码片段:

         float tv = startV / startZ
         float tu = startU / startZ;
         tv %= texturePitch;  //make sure the texture coordinates stay on the texture if they are too wide/high
         tu %= texturePitch;  //I'm assuming square textures here. With fixed point you could have used &=
         unsigned int *textPtr = textureBuf+tu + (tv*texturePitch);   //in case of fixedpoints one could have shifted the tv. Now we have to multiply everytime. 
         int destColTm = *(textPtr);  //this is the color (if we only use texture mapping)  we'll be needing for the pixel
  • 虚线
    • 虚线
      • 虚线
      • 可选:检查z缓冲区,如果在此坐标处绘制的前一个像素比我们的高或低。
      • 绘制像素
      • startZ += slopeZ; startU+=slopeU; startV += slopeV; //更新所有插值器
    • } x循环结束
    • leftCurX+= leftDeltaX; rightCurX += rightDeltaX; leftCurU+= rightDeltaU; leftCurV += rightDeltaV; leftCurZ += rightDeltaZ; //更新Y插值器
  • } y循环结束

    //这是第一部分的结尾。我们现在已经画了三角形的一半。从顶部到中间的Y坐标。 //现在我们基本上做完全相同的事情,但现在是为三角形的底部半部分(使用另一组插值器)

抱歉有“虚拟行”..它们需要使markdown代码保持同步。(花了我一段时间才让所有东西看起来像预期的那样)

如果这有助于您解决面临的问题,请告诉我!


调试的另一个技巧是,使用旧式的非增量 U 和 V 坐标计算每个纹理点的像素,并将其与算法计算出的增量值进行比较(考虑到由于舍入误差而产生的一些偏差)。 - Keith Randall
1
哇...这是一个非常完整的回答!:D - Goz
goz:现在只希望这对他有用,因为根据图片判断,他可能已经有了大部分代码。我认为他可能计算插值值时出了错。但是希望如果他将他的算法与我的放在一起,他可以找出哪一个是错的;^) - Toad
太棒了!我在设置赏金后几分钟内就解决了我的问题。:S 但是这还是很奇怪,我必须将u和v除以1/z并乘以1/z才能得到正确的uv坐标。然而,我已经放弃了我的渲染方法,转而采用更传统的“在三角形周围画一个框,确定每个斜率的最小和最大x”,但是你的代码应该有助于修复我的糟糕代码! - knight666
@knight666:从图片和你的解释来看,你已经做得非常好了。有时候只是一个小细节或其他愚蠢的问题就会破坏整个渲染过程,而且很难追踪。但是当你最终成功时,这种感觉真的太棒了;^) - Toad

0

我不确定我能帮助你的问题,但是在当时我读过的关于软件渲染最好的书籍之一是Michael Abrash所著的《图形编程黑皮书》,它可以在线获取。


0

如果你正在插值1/z,那么你需要将UV/z乘以z,而不是1/z。假设你有以下内容:

UV = UV_current * z_current

并且z_current正在插值1/z,你应该将其更改为:

UV = UV_current / z_current

然后你可能想将z_current重命名为类似于one_over_z_current的名称。


这可能是一种加速技巧:他需要除以z。但由于乘法比除法快,所以他乘以1/z。 - Toad
你无法避免透视纠正纹理映射中的除法。你可以在像素范围内近似它,但你无法消除它。假设你想要看起来正确的东西。 - MSN

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