返回介绍

光栅化

发布于 2025-02-18 12:46:46 字数 13786 浏览 0 评论 0 收藏 0

【译者注:感谢四川-平生小哥为我们翻译此段】

有很多不同类型的光栅化算法。我甚至知道在我的团队中有人向知名的 GPU 厂商提出了自己的光栅化算法。也多亏了他,我现在知道了什么是 折行书 并一直使用至今。

为了更正规,我们将在本教程中实现一个简单而有效的光栅化算法。正如我们在 CPU 中运行 3D 软件渲染引擎一般,它会耗费我们的大量 CPU 运算。当然,现今这个功能已经直接用 GPU 来帮我们完成。先让我们做一个练习。请拿出一张纸,然后画一个三角形,嗯……任意你所能画出来的三角形。我们要找出一个通用的方法可以得出任意类型的三角形。

如果我们按 Y 轴对每个三角形的三个点进行排序,保证 P1 后面是 P2, 然后是 P3 的话,最终将出现两种可能的情况:

你将会看到这两种情况:P2 在 P1 和 P3 的右侧 或 P2 在 P1 和 P3 的左侧。在本教程中,由于我们始终是从左到右(sx 到 se) 的顺序画线,所以就按照这个假设来处理这两种情况。

此外,我们要顺着左图中的红线自上而下 (从 P1.Y 到 P3.Y) 从左向右绘制。但是当到达 P2.Y 时我们需要稍微改变一下逻辑,因为这时两种情况的斜率都会发生改变。这就是为什么我们将扫描线处理分为两个步骤:从 P1.Y 向下移动到 P2.Y,然后从 P2.Y 最终移动到 P3.Y。

要了解我们使用算法的全部逻辑,可以在维基百科中找到词条: http://en.wikipedia.org/wiki/Slope 。它只是一些基本的数学运算。

为了能够适应这两种情况,你只需要进行简单的运算:

  • dP1P2 = P2.X - P1.X / P2.Y - P1.Y
  • dP1P3 = P3.X - P1.X / P3.Y - P1.Y

那我们如何得知是属于哪种情况呢?

  • P2 在右的第一种情况:dP1P2 > dP1P3
  • P2 在左的第二种情况:dP1P3 > dP1P2

现在已经有了算法的基本逻辑,我们需要知道如何计算上图中每条线上的 sx(起始的 x 坐标) 和 ex(结束的 x 坐标) 之间的 x。因此要首先计算出 sx 和 ex。由于我们知道当前所扫描到的 y 值、P1P3 和 P1P3 的斜率,因此我们不难得出 sx 和 ex 值。

以情况 1 为例。首先利用当前的 y 值来计算梯度。它将告诉我们在 P1.Y 和 P2.Y 之间进行处理时,我们当前所处的阶段。

  • 梯度 = 当前的 y 值 - P1.Y / P2.Y - P1.Y

因为 x 和 y 是线性连接,所以我们可以基于该梯度,利用 P1.X 和 P3.X 来计算 sx 插值,并利用 P1.X 和 P2.X 来计算 ex 插值。

如果您能够理解插值这个概念,那么你就能够理解剩下所有关于光纤和材质。而且你能够很好的阅读相关代码,也能够从头开始自己重写代码,而无须复制、粘贴下面的代码。

如果还不是很清楚的话,这里有一些关于光栅化的文章以供阅读:

现在,基于我们现有的算法描述说明让我们开始编写代码。首先,从设备对象删除 drawLine 和 drawBline 函数,并用下面的代码进行替换:

【译者注:C#代码】

// 将三维坐标和变换矩阵转换成二维坐标  
public Vector3 Project(Vector3 coord, Matrix transMat)
{
  // 进行坐标变换  
  var point = Vector3.TransformCoordinate(coord, transMat);
  // 变换后的坐标起始点是坐标系的中心点  
  // 但是,在屏幕上,我们以左上角为起始点  
  // 我们需要重新计算使他们的起始点变成左上角
  var x = point.X * bmp.PixelWidth + bmp.PixelWidth / 2.0f;
  var y = -point.Y * bmp.PixelHeight + bmp.PixelHeight / 2.0f;
  return (new Vector3(x, y, point.Z));
}

// 如果二维坐标在可视范围内则绘制
public void DrawPoint(Vector2 point, Color4 color)
{
  // 判断是否在屏幕内  
  if (point.X >= 0 && point.Y >= 0 && point.X < bmp.PixelWidth && point.Y < bmp.PixelHeight)
  {
    // 绘制一个点
    PutPixel((int)point.X, (int)point.Y, color);
  }
}

【译者注:TypeScript 代码】

// 将三维坐标和变换矩阵转换成二维坐标
public project(coord: BABYLON.Vector3, transMat: BABYLON.Matrix): BABYLON.Vector3 {
  // 进行坐标变换  
  var point = BABYLON.Vector3.TransformCoordinates(coord, transMat);
  // 变换后的坐标起始点是坐标系的中心点  
  // 但是,在屏幕上,我们以左上角为起始点  
  // 我们需要重新计算使他们的起始点变成左上角
  var x = point.x * this.workingWidth + this.workingWidth / 2.0;
  var y = -point.y * this.workingHeight + this.workingHeight / 2.0;
  return (new BABYLON.Vector3(x, y, point.z));
}

// 如果二维坐标在可视范围内则绘制
public drawPoint(point: BABYLON.Vector2, color: BABYLON.Color4): void {
  // 判断是否在屏幕内
  if(point.x >= 0 && point.y >= 0 && point.x < this.workingWidth && point.y < this.workingHeight) {
    // 绘制一个点
    this.putPixel(point.x, point.y, color);
  }
}

【译者注:JavaScript 代码】

// 将三维坐标和变换矩阵转换成二维坐标
Device.prototype.project = function (coord, transMat) {
  var point = BABYLON.Vector3.TransformCoordinates(coord, transMat);
  // 变换后的坐标起始点是坐标系的中心点  
  // 但是,在屏幕上,我们以左上角为起始点  
  // 我们需要重新计算使他们的起始点变成左上角
  var x = point.x * this.workingWidth + this.workingWidth / 2.0 >> 0;
  var y = -point.y * this.workingHeight + this.workingHeight / 2.0 >> 0;
  return (new BABYLON.Vector3(x, y, point.z));
};

// 如果二维坐标在可视范围内则绘制
Device.prototype.drawPoint = function (point, color) {
  // 判断是否在屏幕内
  if (point.x >= 0 && point.y >= 0 && point.x < this.workingWidth
                    && point.y < this.workingHeight) {
    // 绘制一个点
    this.putPixel(point.x, point.y, color);
  }
};

我们仅仅做了一些准备,下面则是最重要的部分,基于先前解释过的三角形的逻辑进行绘制。

【译者注:C#代码】

// 限制数值范围在 0 和 1 之间
float Clamp(float value, float min = 0, float max = 1)
{
  return Math.Max(min, Math.Min(value, max));
}

// 过渡插值
float Interpolate(float min, float max, float gradient)
{
  return min + (max - min) * Clamp(gradient);
}

// 在两点之间从左到右绘制一条线段
// papb -> pcpd
// pa, pb, pc, pd 在之前必须已经排好序
void ProcessScanLine(int y, Vector3 pa, Vector3 pb, Vector3 pc, Vector3 pd, Color4 color)
{
  // 由当前的 y 值,我们可以计算出梯度
  // 以此再计算出 起始 X(sx) 和 结束 X(ex)
  // 如果 pa.Y == pb.Y 或者 pc.Y== pd.y 的话,梯度强制为 1
  var gradient1 = pa.Y != pb.Y ? (y - pa.Y) / (pb.Y - pa.Y) : 1;
  var gradient2 = pc.Y != pd.Y ? (y - pc.Y) / (pd.Y - pc.Y) : 1;

  int sx = (int)Interpolate(pa.X, pb.X, gradient1);
  int ex = (int)Interpolate(pc.X, pd.X, gradient2);

  // 从左(sx) 向右(ex) 绘制一条线
  for (var x = sx; x < ex; x++)
  {
    DrawPoint(new Vector2(x, y), color);
  }
}

public void DrawTriangle(Vector3 p1, Vector3 p2, Vector3 p3, Color4 color)
{
  // 进行排序,p1 总在最上面,p2 总在最中间,p3 总在最下面
  if (p1.Y > p2.Y)
  {
    var temp = p2;
    p2 = p1;
    p1 = temp;
  }

  if (p2.Y > p3.Y)
  {
    var temp = p2;
    p2 = p3;
    p3 = temp;
  }

  if (p1.Y > p2.Y)
  {
    var temp = p2;
    p2 = p1;
    p1 = temp;
  }

  // 反向斜率
  float dP1P2, dP1P3;

  // http://en.wikipedia.org/wiki/Slope
  // 计算反向斜率
  if (p2.Y - p1.Y > 0)
    dP1P2 = (p2.X - p1.X) / (p2.Y - p1.Y);
  else
    dP1P2 = 0;

  if (p3.Y - p1.Y > 0)
    dP1P3 = (p3.X - p1.X) / (p3.Y - p1.Y);
  else
    dP1P3 = 0;

  // 对于第一种情况来说,三角形是这样的:
  // P1
  // -
  // -- 
  // - -
  // -  -
  // -   - P2
  // -  -
  // - -
  // -
  // P3
  if (dP1P2 > dP1P3)
  {
    for (var y = (int)p1.Y; y <= (int)p3.Y; y++)
    {
      if (y < p2.Y)
      {
        ProcessScanLine(y, p1, p3, p1, p2, color);
      }
      else
      {
        ProcessScanLine(y, p1, p3, p2, p3, color);
      }
    }
  }
  // 对于第二种情况来说,三角形是这样的:
  //     P1
  //    -
  //     -- 
  //    - -
  //   -  -
  // P2 -   - 
  //   -  -
  //    - -
  //    -
  //     P3
  else
  {
    for (var y = (int)p1.Y; y <= (int)p3.Y; y++)
    {
      if (y < p2.Y)
      {
        ProcessScanLine(y, p1, p2, p1, p3, color);
      }
      else
      {
        ProcessScanLine(y, p2, p3, p1, p3, color);
      }
    }
  }
}

【译者注:TypeScript 代码】

// 限制数值范围在 0 和 1 之间
public clamp(value: number, min: number = 0, max: number = 1): number {
  return Math.max(min, Math.min(value, max));
}

// 过渡插值
public interpolate(min: number, max: number, gradient: number) {
  return min + (max - min) * this.clamp(gradient);
}

// 在两点之间从左到右绘制一条线段
// papb -> pcpd
// pa, pb, pc, pd 在之前必须已经排好序
public processScanLine(y: number, pa: BABYLON.Vector3, pb: BABYLON.Vector3, 
             pc: BABYLON.Vector3, pd: BABYLON.Vector3, color: BABYLON.Color4): void {
  // 由当前的 y 值,我们可以计算出梯度
  // 以此再计算出 起始 X(sx) 和 结束 X(ex)
  // 如果 pa.Y == pb.Y 或者 pc.Y== pd.y 的话,梯度强制为 1
  var gradient1 = pa.y != pb.y ? (y - pa.y) / (pb.y - pa.y) : 1;
  var gradient2 = pc.y != pd.y ? (y - pc.y) / (pd.y - pc.y) : 1;

  var sx = this.interpolate(pa.x, pb.x, gradient1) >> 0;
  var ex = this.interpolate(pc.x, pd.x, gradient2) >> 0;

  // 从左(sx) 向右(ex) 绘制一条线
  for (var x = sx; x < ex; x++) {
    this.drawPoint(new BABYLON.Vector2(x, y), color);
  }
}

public drawTriangle(p1: BABYLON.Vector3, p2: BABYLON.Vector3, 
          p3: BABYLON.Vector3, color: BABYLON.Color4): void {
  // 进行排序,p1 总在最上面,p2 总在最中间,p3 总在最下面
  if (p1.y > p2.y) {
    var temp = p2;
    p2 = p1;
    p1 = temp;
  }

  if (p2.y > p3.y) {
    var temp = p2;
    p2 = p3;
    p3 = temp;
  }

  if (p1.y > p2.y) {
    var temp = p2;
    p2 = p1;
    p1 = temp;
  }

  // 反向斜率
  var dP1P2: number; var dP1P3: number;

  // http://en.wikipedia.org/wiki/Slope
  // 计算反向斜率
  if (p2.y - p1.y > 0)
    dP1P2 = (p2.x - p1.x) / (p2.y - p1.y);
  else
    dP1P2 = 0;

  if (p3.y - p1.y > 0)
    dP1P3 = (p3.x - p1.x) / (p3.y - p1.y);
  else
    dP1P3 = 0;

  // 对于第一种情况来说,三角形是这样的:
  // P1
  // -
  // -- 
  // - -
  // -  -
  // -   - P2
  // -  -
  // - -
  // -
  // P3
  if (dP1P2 > dP1P3) {
    for (var y = p1.y >> 0; y <= p3.y >> 0; y++)
    {
      if (y < p2.y) {
        this.processScanLine(y, p1, p3, p1, p2, color);
      }
      else {
        this.processScanLine(y, p1, p3, p2, p3, color);
      }
    }
  }
  // 对于第二种情况来说,三角形是这样的:
  //     P1
  //    -
  //     -- 
  //    - -
  //   -  -
  // P2 -   - 
  //   -  -
  //    - -
  //    -
  //     P3
  else {
    for (var y = p1.y >> 0; y <= p3.y >> 0; y++)
    {
      if (y < p2.y) {
        this.processScanLine(y, p1, p2, p1, p3, color);
      }
      else {
        this.processScanLine(y, p2, p3, p1, p3, color);
      }
    }
  }
}

【译者注:JavaScript 代码】

// 限制数值范围在 0 和 1 之间
Device.prototype.clamp = function (value, min, max) {
  if (typeof min === "undefined") { min = 0; }
  if (typeof max === "undefined") { max = 1; }
  return Math.max(min, Math.min(value, max));
};

// 过渡插值
Device.prototype.interpolate = function (min, max, gradient) {
  return min + (max - min) * this.clamp(gradient);
};

// 在两点之间从左到右绘制一条线段
// papb -> pcpd
// pa, pb, pc, pd 在之前必须已经排好序
Device.prototype.processScanLine = function (y, pa, pb, pc, pd, color) {
  // 由当前的 y 值,我们可以计算出梯度
  // 以此再计算出 起始 X(sx) 和 结束 X(ex)
  // 如果 pa.Y == pb.Y 或者 pc.Y== pd.y 的话,梯度强制为 1
  var gradient1 = pa.y != pb.y ? (y - pa.y) / (pb.y - pa.y) : 1;
  var gradient2 = pc.y != pd.y ? (y - pc.y) / (pd.y - pc.y) : 1;

  var sx = this.interpolate(pa.x, pb.x, gradient1) >> 0;
  var ex = this.interpolate(pc.x, pd.x, gradient2) >> 0;

  // 从左(sx) 向右(ex) 绘制一条线
  for (var x = sx; x < ex; x++) {
    this.drawPoint(new BABYLON.Vector2(x, y), color);
  }
};

Device.prototype.drawTriangle = function (p1, p2, p3, color) {
  // 进行排序,p1 总在最上面,p2 总在最中间,p3 总在最下面
  if (p1.y > p2.y) {
    var temp = p2;
    p2 = p1;
    p1 = temp;
  }
  if (p2.y > p3.y) {
    var temp = p2;
    p2 = p3;
    p3 = temp;
  }
  if (p1.y > p2.y) {
    var temp = p2;
    p2 = p1;
    p1 = temp;
  }

  // 反向斜率
  var dP1P2; var dP1P3;

  // http://en.wikipedia.org/wiki/Slope
  // 计算反向斜率
  if (p2.y - p1.y > 0) {
    dP1P2 = (p2.x - p1.x) / (p2.y - p1.y);
  } else {
    dP1P2 = 0;
  }

  if (p3.y - p1.y > 0) {
    dP1P3 = (p3.x - p1.x) / (p3.y - p1.y);
  } else {
    dP1P3 = 0;
  }

  // 对于第一种情况来说,三角形是这样的:
  // P1
  // -
  // -- 
  // - -
  // -  -
  // -   - P2
  // -  -
  // - -
  // -
  // P3
  if (dP1P2 > dP1P3) {
    for (var y = p1.y >> 0; y <= p3.y >> 0; y++) {
      if (y < p2.y) {
        this.processScanLine(y, p1, p3, p1, p2, color);
      } else {
        this.processScanLine(y, p1, p3, p2, p3, color);
      }
    }
  }
    // 对于第二种情况来说,三角形是这样的:
    //     P1
    //    -
    //     -- 
    //    - -
    //   -  -
    // P2 -   - 
    //   -  -
    //    - -
    //    -
    //     P3
  else {
    for (var y = p1.y >> 0; y <= p3.y >> 0; y++) {
      if (y < p2.y) {
        this.processScanLine(y, p1, p2, p1, p3, color);
      } else {
        this.processScanLine(y, p2, p3, p1, p3, color);
      }
    }
  }
};

你已经了解了如何处理两种三角形的填写以及扫描线中所做的操作了。

最后,你需要更新渲染函数,用 drawTriangle 来替代 drawLine 和 drawBline。我们还用了不同的灰色填充每个三角形。不然的话,整个画面一片灰你根本就看不出效果来。我们将在接下来的教程中学习到如何恰当的处理光照。

【译者注:C#代码】

var faceIndex = 0;
foreach (var face in mesh.Faces)
{
  var vertexA = mesh.Vertices[face.A];
  var vertexB = mesh.Vertices[face.B];
  var vertexC = mesh.Vertices[face.C];

  var pixelA = Project(vertexA, transformMatrix);
  var pixelB = Project(vertexB, transformMatrix);
  var pixelC = Project(vertexC, transformMatrix);

  var color = 0.25f + (faceIndex % mesh.Faces.Length) * 0.75f / mesh.Faces.Length;
  DrawTriangle(pixelA, pixelB, pixelC, new Color4(color, color, color, 1));
  faceIndex++;
}

【译者注:TypeScript 代码】

for (var indexFaces = 0; indexFaces < cMesh.Faces.length; indexFaces++) {
  var currentFace = cMesh.Faces[indexFaces];
  var vertexA = cMesh.Vertices[currentFace.A];
  var vertexB = cMesh.Vertices[currentFace.B];
  var vertexC = cMesh.Vertices[currentFace.C];

  var pixelA = this.project(vertexA, transformMatrix);
  var pixelB = this.project(vertexB, transformMatrix);
  var pixelC = this.project(vertexC, transformMatrix);

  var color: number = 0.25 + ((indexFaces % cMesh.Faces.length) / cMesh.Faces.length) * 0.75;
  this.drawTriangle(pixelA, pixelB, pixelC, new BABYLON.Color4(color, color, color, 1));
}

【译者注:JavaScript 代码】

for (var indexFaces = 0; indexFaces < cMesh.Faces.length; indexFaces++) {
  var currentFace = cMesh.Faces[indexFaces];
  var vertexA = cMesh.Vertices[currentFace.A];
  var vertexB = cMesh.Vertices[currentFace.B];
  var vertexC = cMesh.Vertices[currentFace.C];

  var pixelA = this.project(vertexA, transformMatrix);
  var pixelB = this.project(vertexB, transformMatrix);
  var pixelC = this.project(vertexC, transformMatrix);

  var color = 0.25 + ((indexFaces % cMesh.Faces.length) / cMesh.Faces.length) * 0.75;
  this.drawTriangle(pixelA, pixelB, pixelC, new BABYLON.Color4(color, color, color, 1));
}

结果应该是这样的:

运行代码

这是怎么回事?为什么感觉这么奇怪?!嗯~这是因为我们没有正确的把正面的三角形画在正面。【译者注:我是这么翻译的,你就这么一看~】

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

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

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文