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

发布于 2024-08-18 02:50:34 字数 1861 浏览 4 评论 0原文

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

我的算法是首先按 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]->y 渲染到 order[2]->y,增加 < code>x_start 和 x_end 相差一个增量。渲染顶部部分时,增量为 x_delta[0]x_delta[1]。渲染底部时,增量为 x_delta[0]x_delta[2]。然后我们在扫描线上的 x_start 和 x_end 之间进行线性插值。 UV 坐标以相同的方式插值,按 y 排序,从开始和结束开始,每个步骤都会应用增量。

除非我尝试进行透视正确的 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 缓冲区问题吗?

I'm making a software rasterizer, and I've run into a bit of a snag: I can't seem to get perspective-correct texture mapping to work.

My algorithm is to first sort the coordinates to plot by y. This returns a highest, lowest and center point. I then walk across the scanlines using the delta's:

// 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;

And then we render from order[0]->y to order[2]->y, increasing the x_start and x_end by a delta. When rendering the top part, the delta's are x_delta[0] and x_delta[1]. When rendering the bottom part, the delta's are x_delta[0] and x_delta[2]. Then we linearly interpolate between x_start and x_end on our scanline. UV coordinates are interpolated in the same way, ordered by y, starting at begin and end, to which delta's are applied each step.

This works fine except when I try to do perspective correct UV mapping. The basic algorithm is to take UV/z and 1/z for each vertex and interpolate between them. For each pixel, the UV coordinate becomes UV_current * z_current. However, this is the result:

alt text

The inversed part tells you where the delta's are flipped. As you can see, the two triangles both seem to be going towards different points in the horizon.

Here's what I use to calculate the Z at a point in space:

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;
}

Am I right, is it a z buffer issue?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

流星番茄 2024-08-25 02:50:34

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个斜率(左侧和右侧)
    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
  }
  • 在 x[0] 上设置 currentLeftX 和 currentRightX
  • 在 leftDeltaU, currentLeftV 上设置 currentLeftU on leftDeltaV 和 currentLeftZ on leftDeltaZ
  • 计算第一个 Y 范围的起点和终点:startY = ceil(y[0]); endY = ceil(y[1])
  • 预步 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]) + u[0 ]
  • 并使用 halfwayX 计算 U、V 和 z 的步进器:
    if(halfwayX - x[1] == 0){ 斜率U=0, 斜率V=0, 斜率Z=0 } else { 斜率U = (halfwayU - U[1]) / (halfwayX - x[1])} //(对于 v 和 z 也是如此)
  • 对 Y 顶部进行裁剪(因此计算我们要开始绘制的位置,以防三角形的顶部超出屏幕(或超出裁剪矩形))
  • y=startY; y <结束Y;是++)
    {

    • Y 超出屏幕底部了吗?停止渲染!
    • 计算第一条水平线的 startX 和 endX
      leftCurX = ceil(startx); leftCurY = ceil(endy);
    • 将要绘制的线剪切到屏幕的左侧水平边框(或剪切区域)
    • 准备一个指向目标缓冲区的指针(每次都通过数组索引来完成太慢了)
      无符号 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
  • 虚拟线
    • 虚拟线
      • 虚拟线
      • 可选:检查 zbuffer 之前在该坐标处绘制的像素是否高于或低于我们的像素。
      • 绘制像素
      • startZ += 斜率Z;起始U+=斜率U;起始V += 斜率V; //更新所有插值器
    • } x 循环结束
    • leftCurX+= leftDeltaX; rightCurX += rightDeltaX;左CurU+=右DeltaU;左CurV += 右DeltaV; leftCurZ += rightDeltaZ; //更新Y插值器
  • } y循环结束

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

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

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

ZBuffer has nothing to do with it.

THe ZBuffer is only useful when triangles are overlapping and you want to make sure that they are drawn correctly (e.g. correctly ordered in the Z). The ZBuffer will, for every pixel of the triangle, determine if a previously placed pixel is nearer to the camera, and if so, not draw the pixel of your triangle.

Since you are drawing 2 triangles which don't overlap, this can not be the issue.

I've made a software rasterizer in fixed point once (for a mobile phone), but I don't have the sources on my laptop. So let me check tonight, how I did it. In essence what you've got is not bad! A thing like this could be caused by a very small error

General tips in debugging this is to have a few test triangles (slope left-side, slope right-side, 90 degree angles, etc etc) and step through it with the debugger and see how your logic deals with the cases.

EDIT:

peudocode of my rasterizer (only U, V and Z are taken into account... if you also want to do gouraud you also have to do everything for R G and B similar as to what you are doing for U and V and Z:

The idea is that a triangle can be broken down in 2 parts. The top part and the bottom part. The top is from y[0] to y[1] and the bottom part is from y[1] to y[2]. For both sets you need to calculate the step variables with which you are interpolating. The below example shows you how to do the top part. If needed I can supply the bottom part too.

Please note that I do already calculate the needed interpolation offsets for the bottom part in the below 'pseudocode' fragment

  • first order the coords(x,y,z,u,v) in the order so that coord[0].y < coord[1].y < coord[2].y
  • next check if any 2 sets of coordinates are identical (only check x and y). If so don't draw
  • exception: does the triangle have a flat top? if so, the first slope will be infinite
  • exception2: does the triangle have a flat bottom (yes triangles can have these too ;^) ) then the last slope too will be infinite
  • calculate 2 slopes (left side and right side)
    leftDeltaX = (x[1] - x[0]) / (y[1]-y[0]) and rightDeltaX = (x[2] - x[0]) / (y[2]-y[0])
  • the second part of the triangle is calculated dependent on: if the left side of the triangle is now really on the leftside (or needs swapping)

code fragment:

 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
  }
  • set the currentLeftX and currentRightX both on x[0]
  • set currentLeftU on leftDeltaU, currentLeftV on leftDeltaV and currentLeftZ on leftDeltaZ
  • calc start and endpoint for first Y range: startY = ceil(y[0]); endY = ceil(y[1])
  • prestep x,u,v and z for the fractional part of y for subpixel accuracy (I guess this is also needed for floats)
    For my fixedpoint algorithms this was needed to make the lines and textures give the illusion of moving in much finer steps then the resolution of the display)
  • calculate where x should be at y[1]: halfwayX = (x[2]-x[0]) * (y[1]-y[0]) / (y[2]-y[0]) + x[0]
    and same for U and V and z: halfwayU = (u[2]-u[0]) * (y[1]-y[0]) / (y[2]-y[0]) + u[0]
  • and using the halfwayX calculate the stepper for the U and V and z:
    if(halfwayX - x[1] == 0){ slopeU=0, slopeV=0, slopeZ=0 } else { slopeU = (halfwayU - U[1]) / (halfwayX - x[1])} //(and same for v and z)
  • do clipping for the Y top (so calculate where we are going to start to draw in case the top of the triangle is off screen (or off the clipping rectangle))
  • for y=startY; y < endY; y++)
    {

    • is Y past bottom of screen? stop rendering!
    • calc startX and endX for the first horizontal line
      leftCurX = ceil(startx); leftCurY = ceil(endy);
    • clip the line to be drawn to the left horizontal border of the screen (or clipping region)
    • prepare a pointer to the destination buffer (doing it through array indexes everytime is too slow)
      unsigned int buf = destbuf + (ypitch) + startX; (unsigned int in case you are doing 24bit or 32 bits rendering)
      also prepare your ZBuffer pointer here (if you are using this)
    • for(x=startX; x < endX; x++)
      {

      • now for perspective texture mapping (using no bilineair interpolation you do the following):

code fragment:

         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
  • dummy line
    • dummy line
      • dummy line
      • optional: check the zbuffer if the previously plotted pixel at this coordinate is higher or lower then ours.
      • plot the pixel
      • startZ += slopeZ; startU+=slopeU; startV += slopeV; //update all interpolators
    • } end of x loop
    • leftCurX+= leftDeltaX; rightCurX += rightDeltaX; leftCurU+= rightDeltaU; leftCurV += rightDeltaV; leftCurZ += rightDeltaZ; //update Y interpolators
  • } end of y loop

    //this is the end of the first part. We now have drawn half the triangle. from the top, to the middle Y coordinate.
    // we now basically do the exact same thing but now for the bottom half of the triangle (using the other set of interpolators)

sorry about the 'dummy lines'.. they were needed to get the markdown codes in sync. (took me a while to get everything sort off looking as intended)

let me know if this helps you solve the problem you are facing!

情绪少女 2024-08-25 02:50:34

我不知道我可以帮助解决你的问题,但是我当时读过的关于软件渲染的最好的书籍之一可以在线获取 图形编程黑皮书,作者:Michael Abrash。

I don't know that I can help with your question, but one of the best books on software rendering that I had read at the time is available online Graphics Programming Black Book by Michael Abrash.

好听的两个字的网名 2024-08-25 02:50:34

如果您要插值 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

If you are interpolating 1/z, you need to multiply UV/z by z, not 1/z. Assuming you have this:

UV = UV_current * z_current

and z_current is interpolating 1/z, you should change it to:

UV = UV_current / z_current

And then you might want to rename z_current to something like one_over_z_current.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文