透视正确的纹理映射; z距离计算可能是错误的

kni*_*666 6 c++ zbuffer rasterizing

我正在制作一个软件光栅化器,我遇到了一些障碍:我似乎无法使透视校正纹理映射工作.

我的算法是首先对坐标进行排序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;
Run Code Online (Sandbox Code Playgroud)

然后我们从渲染order[0]->yorder[2]->y,增加x_startx_end由增量.渲染顶部时,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.但是,这是结果:

替代文字

反转部分告诉您三角形的翻转位置.如您所见,这两个三角形似乎都朝向地平线中的不同点.

这是我用来计算空间中某点的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;
}
Run Code Online (Sandbox Code Playgroud)

我是对的,是az缓冲区问题吗?

Toa*_*oad 5

ZBuffer 与此无关。

ZBuffer 仅在三角形重叠并且您希望确保它们正确绘制(例如,Z 中的顺序正确)时才有用。ZBuffer 将针对三角形的每个像素确定先前放置的像素是否更靠近相机,如果是,则不绘制三角形的像素。

由于您正在绘制两个不重叠的三角形,因此这不是问题。

我已经制作了一次定点软件光栅器(用于手机),但我的笔记本电脑上没有源代码。那么今晚让我检查一下我是如何做到的。从本质上讲,你所拥有的还不错!像这样的事情可能是由一个很小的错误引起的

调试的一般技巧是使用一些测试三角形(左侧倾斜、右侧倾斜、90 度角等),然后使用调试器逐步执行它,看看您的逻辑如何处理这些情况。

编辑:

我的光栅化器的伪代码(仅考虑 U、V 和 Z...如果您也想做 gouraud,您还必须对 RG 和 B 执行所有操作,类似于对 U、V 和 Z 执行的操作:

这个想法是三角形可以分成两部分。顶部部分和底部部分。顶部是从 y[0] 到 y[1],底部是从 y[1] 到 y[2]。对于这两个集合,您需要计算要插值的步骤变量。下面的示例向您展示了如何完成顶部部分。如果需要的话我也可以提供底部部分。

请注意,我已经计算了下面“伪代码”片段中底部所需的插值偏移量

  • 首先按顺序对 coords(x,y,z,u,v) 进行排序,使得 coord[0].y < coord[1].y < coord[2].y
  • 接下来检查任意 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
  }
Run Code Online (Sandbox Code Playgroud)
  • 在 x[0] 上设置 currentLeftX 和 currentRightX
  • 将 currentLeftU 设置为 leftDeltaU,将 currentLeftV 设置为 leftDeltaV,将 currentLeftZ 设置为 leftDeltaZ
  • 计算第一个 Y 范围的起点和终点:startY = ceil(y[0]); endY = ceil(y[1])
  • prestep x,u,v 和 z 用于 y 的小数部分,以实现子像素精度(我猜浮点数也需要这样做)对于我的定点算法,需要这样做才能使线条和纹理产生以更精细的步骤移动的错觉显示器的分辨率)
  • 计算 x 应该位于 y[1] 的位置: 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]) +你[0]
  • 并使用 halfwayX 计算 U、V 和 z 的步进器: if(halfwayX - x[1] == 0){lopeU=0,lopeV=0,slopeZ=0}else{slopeU=(halfwayU-U[1] ]) / (halfwayX - x[1])} //(v 和 z 相同)
  • 对 Y 顶部进行裁剪(因此计算我们要开始绘制的位置,以防三角形的顶部超出屏幕(或超出裁剪矩形))
  • 对于 y=startY;y < 结束Y;y++) {
    • Y 超出屏幕底部了吗?停止渲染!
    • 计算第一条水平线的 startX 和 endX leftCurX = ceil(startx); leftCurY = ceil(endy);
    • 将要绘制的线剪切到屏幕的左侧水平边框(或剪切区域)
    • 准备一个指向目标缓冲区的指针(每次都通过数组索引来完成太慢) unsigned int buf = destbuf + (yitch ) + startX; (unsigned int,如果您正在进行 24 位或 32 位渲染)还可以在此处准备您的 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
Run Code Online (Sandbox Code Playgroud)
  • 虚拟线
    • 虚拟线
      • 虚拟线
      • 可选:检查 zbuffer 之前在该坐标处绘制的像素是否高于或低于我们的像素。
      • 绘制像素
      • 起始Z += 斜率Z;起始U+=斜率U;起始V += 斜率V;//更新所有插值器
    • x 循环结束
    • leftCurX+= leftDeltaX; rightCurX += rightDeltaX; 左CurU+=右DeltaU;左CurV += 右DeltaV;leftCurZ += rightDeltaZ; //更新Y插值器
  • y 循环结束

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

对“虚拟行”感到抱歉……需要它们来同步降价代码。(我花了一段时间才让一切看起来都按预期进行)

让我知道这是否可以帮助您解决您面临的问题!