ts实现渲染器-3-绘制三角形

绘制三角形

前言:观察上次文章代码与最后绘制的线框图,会发现我们的模型由一个个顶点所组成的三角形构成。那本节自然就引出一个问题,怎么对三角形填充像素,再形象点就是怎么判断一个点在不在三角形中?下面我们带着问题来解决问题

1.如何画一个三角形?

在平面中三个不共线的顶点组成一个三角形,自然我们用上文中我们实现了绘制像素与绘制线段的算法,来绘制一个空心的三角形,只用将三个顶点链接起来即可。

那么接下来问题变成我们怎么填充这个空心三角形?

文章给出通用方法是寻找三个点组成的最大线框,然后从最左下点一直遍历到右上角并判断像素点在不在三角形中。

那这个方法中寻找线框显然不是难点,难点是怎么判断点在不在三角形中。好让我们带着疑问回顾下线性代数中的矩阵运算

下面寻找线框函数

ts 复制代码
getBoxRange = (p: Point2[]) => {
    const boxmin = [this.w, this.h]
    const boxmax = [0, 0]
    for (let i = 0; i < 3; i++) {
      for (let j = 0; j < 2; j++) {
        boxmin[0] = Math.min(boxmin[0], p[i].x)
        boxmin[1] = Math.min(boxmin[1], p[i].y)
        boxmax[0] = Math.max(boxmax[0], p[i].x)
        boxmax[1] = Math.max(boxmax[1], p[i].y)
      }
    }
    return { boxmin, boxmax }
  }

2. 向量运算与绘制三角形

图形学中大量运用线性代数知识,本文默认读者对该科目有基本认知, 本文就不着重对向量介绍,只会介绍运算对应的几何意义,进行探讨。 首先推荐一个视频能加深线性代数的认知: www.bilibili.com/video/BV1ib...

1.构造向量

模型顶点信息中包含着x,y等坐标点信息,怎么将点转化为二维空间的向量呢?显然将点首尾对应x,y相减就得到对应两点组成的向量。下图为构建三角形三边向量的过程

2.向量叉乘

在三维空间中,两个三维向量 <math xmlns="http://www.w3.org/1998/Math/MathML"> a a </math>a 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> b b </math>b 做叉乘,会得到一个和已知两个向量垂直 的新向量 <math xmlns="http://www.w3.org/1998/Math/MathML"> a × b a×b </math>a×b。 既然叉乘产生的是一个新向量,那么它肯定有个方向,我们一般用右手定则来判断:将右手食指指向 <math xmlns="http://www.w3.org/1998/Math/MathML"> a a </math>a 的方向、中指指向 <math xmlns="http://www.w3.org/1998/Math/MathML"> b b </math>b 的方向,则此时拇指的方向即为 <math xmlns="http://www.w3.org/1998/Math/MathML"> a × b a×b </math>a×b 的方向。

综上所述,我们可以对向量叉乘做一个严谨的定义:

<math xmlns="http://www.w3.org/1998/Math/MathML"> a × b = ∥ a ∥∥ b ∥ s i n ( θ ) n a×b=∥a∥∥b∥sin(θ) n </math>a×b=∥a∥∥b∥sin(θ) n

其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> θ θ </math>θ 表示 <math xmlns="http://www.w3.org/1998/Math/MathML"> a a </math>a 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> b b </math>b 在它们所定义的平面上的夹角 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( 0 ∘ ≤ θ ≤ 18 0 ∘ ) (0^{\circ}≤ θ ≤180^{\circ}) </math>(0∘≤θ≤180∘)。 <math xmlns="http://www.w3.org/1998/Math/MathML"> ∥ a ∥ ∥a∥ </math>∥a∥ 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> ∥ b ∥ ∥b∥ </math>∥b∥ 是向量 <math xmlns="http://www.w3.org/1998/Math/MathML"> a a </math>a 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> b b </math>b 的模长,而 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 则是一个与 <math xmlns="http://www.w3.org/1998/Math/MathML"> a a </math>a 、 <math xmlns="http://www.w3.org/1998/Math/MathML"> b b </math>b 所构成的平面垂直的单位向量,方向由右手定则决定。

3. 利用三角重心坐标画三角形

求出 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ u v 1 ] [u\quad v\quad 1] </math>[uv1] 后要得到点P的位置要经过P点公式逆向运算的到

3. ts实现向量运算类

这里推荐篇文章: juejin.cn/post/684490...

下面代码只有一点需要注意,向量叉积其实是求行列式对应的代数余子式的过程,二维向量叉乘就对应是一个值,三维对应叉乘为一个向量

ts 复制代码
class Vector {
  components: number[]
  constructor(...components: number[]) {
    this.components = components
  }
  toString = () => this.components
  add = ({ components }: { components: number[] }) => {
    return new Vector(...components.map((component, index) => this.components[index] + component))
  }
  sub = ({ components }: { components: number[] }) => {
    return new Vector(...components.map((component, index) => this.components[index] - component))
  }
  dot = ({ components }: { components: number[] }) => {
    return components.reduce((acc, component, index) => acc + component * this.components[index], 0)
  }
  length = () => {
    return Math.hypot(...this.components)
  }
  scaleBy = (factor: number) => {
    return new Vector(...this.components.map((component) => component * factor))
  }
  normalize = () => {
    const length = this.length()

    return this.scaleBy(1 / length)
  }

  crossProduct2 = ({ components }: { components: number[] }) => {
    return this.components[0] * components[1] - this.components[1] * components[0]
  }
  crossProduct3 = ({ components }: { components: number[] }) => {
    return new Vector(
      this.components[1] * components[2] - this.components[2] * components[1],
      this.components[2] * components[0] - this.components[0] * components[2],
      this.components[0] * components[1] - this.components[1] * components[0]
    )
  }
}

export { Vector }

4.绘制三角形

首先是第一种方式实现的代码与效果

ts 复制代码
drawTriangle2 = (tPoint: Point2[], color: string) => {
    this.color = color
    const AB = new Vector(tPoint[1].x - tPoint[0].x, tPoint[1].y - tPoint[0].y)
    const BC = new Vector(tPoint[2].x - tPoint[1].x, tPoint[2].y - tPoint[1].y)
    const CA = new Vector(tPoint[0].x - tPoint[2].x, tPoint[0].y - tPoint[2].y)

    const { boxmin, boxmax } = this.getBoxRange(tPoint)

    const p: Point2 = { x: 0, y: 0 }
    for (p.x = boxmin[0]; p.x <= boxmax[0]; p.x++) {
      for (p.y = boxmin[1]; p.y <= boxmax[1]; p.y++) {
        const AP = new Vector(p.x - tPoint[0].x, p.y - tPoint[0].y)
        const BP = new Vector(p.x - tPoint[1].x, p.y - tPoint[1].y)
        const CP = new Vector(p.x - tPoint[2].x, p.y - tPoint[2].y)

        const ABp = AB.crossProduct2(AP)
        const BCp = BC.crossProduct2(BP)
        const CAp = CA.crossProduct2(CP)

        if (ABp >= 0 && BCp >= 0 && CAp >= 0) {
          this.drawPixel(p.x, p.y)
        }
      }
    }
  }
  
  const testDrawTrangle = () => {
  const canvas = document.querySelector('canvas') as HTMLCanvasElement
  const render = new Render(canvas)
  const A: Point2 = { x: 10, y: 10 }
  const B: Point2 = { x: 150, y: 30 }
  const C: Point2 = { x: 70, y: 160 }

  render.drawTriangle([A, B, C], 'red')
}

三角重心绘制方式

ts 复制代码
drawTriangle = (tPoint: Point2[], color: string) => {
    this.color = color
    const { boxmin, boxmax } = this.getBoxRange(tPoint)
    const p: Point2 = { x: 0, y: 0 }

    for (p.x = boxmin[0]; p.x <= boxmax[0]; p.x++) {
      for (p.y = boxmin[1]; p.y <= boxmax[1]; p.y++) {
        const x = new Vector(tPoint[1].x - tPoint[0].x, tPoint[2].x - tPoint[0].x, tPoint[0].x - p.x)
        const y = new Vector(tPoint[1].y - tPoint[0].y, tPoint[2].y - tPoint[0].y, tPoint[0].y - p.y)
        let xy = x.crossProduct3(y)

        if (Math.abs(xy.components[2]) < 1) {
          xy = new Vector(-1, 1, 1)
        }
        //这是对向量运算得到对应点p的值
        xy = new Vector(
          1 - (xy.components[0] + xy.components[1]) / xy.components[2],
          xy.components[0] / xy.components[2],
          xy.components[1] / xy.components[2]
        )
        if (xy.components[0] >= 0 && xy.components[1] >= 0 && xy.components[2] >= 0) {
          this.drawPixel(p.x, p.y)
        }
      }
    }
  }

显然画出的三角形一致

5.绘制模型

1.随机着色

我们对每个不同三角形绘制不同颜色

ts 复制代码
const init = async () => {
  const canvas = document.querySelector('canvas') as HTMLCanvasElement
  const render = new Render(canvas)
  const { vertices, faceVertices } = await load('./teapot.obj')
  for (const item of faceVertices) {
    // 遍历三角面片
    const pointList: Point2[] = []
    for (let i = 0; i < 3; i++) {
      // 获取三角面片的三个顶点
      const v0 = parseInt(item[i][0]) - 1
      const x0 = ((vertices[v0].x + 3) * render.w) / 6
      const y0 = ((vertices[v0].y + 1) * render.h) / 6
      pointList.push({ x: x0, y: y0 })
    }

    const color = `rgb(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255}, 255)`
    render.drawTriangle(pointList, color)
  }
}

2.添加最简单光源

随机着色的好处是可以很清楚的表现出模型各个三角形的轮廓,但是也失去了模型的辨识度,很多细节都丢失了。

我们在这里引入一个非常简单的光照模型,认为单位面积上接收到的光,和平面法线与光照方向的余弦值成正比

所以着色的思路就很清晰了:

  • 我们要先定义一个三维空间里的光照方向(向量) ,然后计算三维空间里各个三角形的法线(向量)
  • 两个向量归一化后,然后计算这两个向量的点乘,会得到一个值
  • 这个值小于 0,说明光在三角形的另一侧,从物理上看是照射不到三角形表面的,所以直接舍弃此三角形
  • 这个值大于 0,值越大 ,说明单位面积上接收到的光越多 ,三角形越亮

生成代码:

ts 复制代码
const init = async () => {
  const canvas = document.querySelector('canvas') as HTMLCanvasElement
  const render = new Render(canvas)
  const { vertices, faceVertices } = await load('./teapot.obj')
  const light = new Vector(0, 0, -1)
  for (const item of faceVertices) {
    // 遍历三角面片
    const pointList: Point2[] = []
    const screenCoords: Vector[] = []
    const worldCoords: Vector[] = []
    for (let i = 0; i < 3; i++) {
      // 获取三角面片的三个顶点
      const v0 = parseInt(item[i][0]) - 1
      const x0 = ((vertices[v0].x + 3) * render.w) / 6
      const y0 = ((vertices[v0].y + 1) * render.h) / 6
      pointList.push({ x: x0, y: y0 })
      screenCoords.push(new Vector(x0, y0, 0))
      worldCoords.push(new Vector(vertices[v0].x, vertices[v0].y, vertices[v0].z))
    }
    const n = worldCoords[2].sub(worldCoords[0]).crossProduct3(worldCoords[1].sub(worldCoords[0])).normalize()

    const intensity = n.dot(light)

    if (intensity > 0) {
      const color = `rgb(${intensity * 255}, ${intensity * 255}, ${intensity * 255}, 255)`

      render.drawTriangle(pointList, color)
    }
  }
}

效果不错,但是茶壶盖子哪里很奇怪,显然是背后的三角形叠加渲染的结果。下一节课就是处理这个问题。

源代码: github.com/miemieooop/...

相关推荐
吕彬-前端14 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(二)
前端·react.js·前端框架
小白小白从不日白35 分钟前
react hooks--useCallback
前端·react.js·前端框架
恩婧43 分钟前
React项目中使用发布订阅模式
前端·react.js·前端框架·发布订阅模式
mez_Blog44 分钟前
个人小结(2.0)
前端·javascript·vue.js·学习·typescript
珊珊而川1 小时前
【浏览器面试真题】sessionStorage和localStorage
前端·javascript·面试
森叶1 小时前
Electron 安装包 asar 解压定位问题实战
前端·javascript·electron
drebander1 小时前
ubuntu 安装 chrome 及 版本匹配的 chromedriver
前端·chrome
软件技术NINI1 小时前
html知识点框架
前端·html
深情废杨杨1 小时前
前端vue-插值表达式和v-html的区别
前端·javascript·vue.js
GHUIJS1 小时前
【vue3】vue3.3新特性真香
前端·javascript·vue.js