likes
comments
collection
share

ts实现3d渲染器 2

作者站长头像
站长
· 阅读数 17

1. 三种绘制像素性能测试

第一种 FillRect 绘制矩形方式

绘制10万像素点花费44ms 在 amd 7840u处理器中

const colors = ['red', 'blue', 'orange', 'yellow', 'brown', 'green']
  console.time()
  for (let i = 0; i < 100000; i++) {
    let x = canvas.width * Math.random()
    let y = canvas.height * Math.random()
    let color = colors[Math.floor(Math.random() * colors.length)]
    drawPixelRect(ctx, x, y, color)
  }
  console.timeEnd()
const drawPixelRect = (ctx: CanvasRenderingContext2D, x: number, y: number, color: string) => {
  ctx.fillStyle = color
  ctx.fillRect(x, y, 1, 1)
}

ts实现3d渲染器 2

第二种 ImageData 单缓冲机制

绘制10万像素点花费1474ms

let colors = [
    { r: 255, g: 0, b: 0, a: 255 }, // red
    { r: 0, g: 255, b: 0, a: 255 }, // green
    { r: 0, g: 0, b: 255, a: 255 } // blue
  ]
  console.time()
  for (let i = 0; i < 100000; i++) {
    let x = canvas.width * Math.random()
    let y = canvas.height * Math.random()
    let color = colors[Math.floor(Math.random() * colors.length)]

    drawPixelSingle(ctx, canvasData, x, y, color)
  }

  console.timeEnd()
const drawPixelSingle = (ctx: CanvasRenderingContext2D, canvasData: ImageData, x: number, y: number, color: Color) => {
  canvasData.data[0] = color.r
  canvasData.data[1] = color.g
  canvasData.data[2] = color.b
  canvasData.data[3] = color.a
  ctx.putImageData(canvasData, x, y)
}

ts实现3d渲染器 2

第三种 ImageData 双缓冲机制

绘制10万像素点花费10ms

let colors = [
    { r: 255, g: 0, b: 0, a: 255 }, // red
    { r: 0, g: 255, b: 0, a: 255 }, // green
    { r: 0, g: 0, b: 255, a: 255 } // blue
  ]
  console.time()
  for (let i = 0; i < 100000; ++i) {
    const x = Math.round(canvas.width * Math.random())
    const y = Math.round(canvas.height * Math.random())
    const color = colors[Math.floor(Math.random() * colors.length)]
    drawPixel(ctx, canvasData, x, y, color)
  }
  updateCanvas(ctx, canvasData)
  console.timeEnd()

const drawPixel = (ctx: CanvasRenderingContext2D, canvasData: ImageData, x: number, y: number, color: Color) => {
  const index = (x + y * ctx.canvas.width) * 4
  canvasData.data[index] = color.r
  canvasData.data[index + 1] = color.g
  canvasData.data[index + 2] = color.b
  canvasData.data[index + 3] = color.a
}

const updateCanvas = (ctx: CanvasRenderingContext2D, canvasData: ImageData) => {
  ctx.putImageData(canvasData, 0, 0)
}

ts实现3d渲染器 2

2. 本文采用绘制方式

第三种是最快, 但会引出一个问题。canvas本身原点在左上角,这样导致渲染模型后会出现颠倒问题,而且putImageData这个方法与canvas本身原点的位置不相关,导致要实现像素级的翻转来倒转。因为本文教学行为比较多,故采用第一种方式。 下面是模型的颠倒图

ts实现3d渲染器 2

3. Obj模型文件

obj模型文件介绍

​ OBJ文件是Wavefront公司为它的一套基于工作站的3D建模和动画软件"Advanced Visualizer"开发的一种文件格式。OBJ是一种3D模型文件,因此不包含动画、材质特性、贴图路径、动力学、粒子等信息。OBJ文件主要支持多边形(Polygons)模型。OBJ文件支持三个点以上的面。OBJ文件支持法线和贴图坐标。

obj模型文件结构

介绍几个基础的结构:

v 几何体顶点 (Geometric vertices)

vt 贴图坐标点 (Texture vertices)

vn 顶点法线 (Vertex normals)

vp 参数空格顶点 (Parameter space vertices)

自由形态曲线(Free-form curve)/表面属性(surface attributes):

deg 度 (Degree)

bmat 基础矩阵 (Basis matrix)

step 步尺寸 (Step size)

cstype 曲线或表面类型 (Curve or surface type)

元素(Elements):

p 点 (Point)

l 线 (Line)

f 面 (Face)

curv 曲线 (Curve)

curv2 2D曲线 (2D curve)

surf 表面 (Surface)

obj文件实例

  OBJ文件记录一个四边形的代码:

  v -0.58 0.84 0

  v 2.68 1.17 0

  v 2.84 -2.03 0

  v -1.92 -2.89 0

  f 1 2 3 4

ts实现3d渲染器 2

4. 绘制模型的线框图我们需要提取模型的那些信息?

第一很显然我们需要模型的所有顶点信息也就是上面图片中的 v ,里面三个数对应着x, y, z 但有了图形上的一个个点,我们应该怎么知道它们的关系?那两点可以绘制一条线段? 这时候就需要 f 面信息

f 334/317/334 460/450/460 47/451/47

f 中334与460与47记录了三角形顶点的信息, 三个点组成一个面,我们要做的就是将三个点依次链接,绘制线段。注意(其中由于f面中存储的顶点信息是按1开始索引, 所以真实顶点列表要减去1来获取值)

5. 模仿ThreeJs加载模型数据

1. 加载模型数据

Three中采用 Fetch 封装成的 FileLoader 异步加载, 附上链接

FileLoader – three.js docs (threejs.org)

ts实现3d渲染器 2

但我们这个obj文件是纯文本格式, 所以可以大幅简化模型的加载,直接使用Text()函数方法导出数据

const load = async (url: string) => {
  const text = await (await fetch(url)).text()
  return parse(text)
}

2. 解码加载后的数据

我对ThreeJs代码阅读加简化后。写的parse方法

const parse = (text: string) => {
  //我们要使用的所有顶点信息
  const vertices: Vertice[] = []
  //所有面信息
  const faceVertices = []
  //下面两行根据Three中源码说法可以加载split速度
  if (text.indexOf('\r\n') !== -1) {
    // 替换所有\r\n为\n
    text = text.replace(/\r\n/g, '\n')
  }

  if (text.indexOf('\\\n') !== -1) {
    // 替换所有\\\n为空
    text = text.replace(/\\\n/g, '')
  }
  //先按行取数据
  const lines = text.split('\n')

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i].trimStart()
    if (line.length === 0) continue
    const lineFirstChar = line.charAt(0)
    if (lineFirstChar === '#') continue

    if (lineFirstChar === 'v') {
      const data = line.split(_face_vertex_data_separator_pattern)
      if (data[0] == 'v') {
        vertices.push({
          x: parseFloat(data[1]),
          y: parseFloat(data[2]),
          z: parseFloat(data[3])
        })
      } else {
        continue
      }
    } else if (lineFirstChar === 'f') {
      const lineData = line.slice(1).trim()
      const vertexData = lineData.split(_face_vertex_data_separator_pattern)
      const fVertices = []
      for (let j = 0, jl = vertexData.length; j < jl; j++) {
        const vertex = vertexData[j]
        if (vertex.length > 0) {
          const vertexParts = vertex.split('/')
          fVertices.push(vertexParts)
        }
      }
      faceVertices.push(fVertices)
    }
  }
 // 最后返回数据
  return { vertices, faceVertices }
}

6.渲染模型

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) {
    // 遍历三角面片
    for (let i = 0; i < 3; i++) {
      // 获取三角面片的三个顶点
      const v0 = parseInt(item[i][0]) - 1
      const v1 = parseInt(item[(i + 1) % 3][0]) - 1
      const x0 = ((vertices[v0].x + 3) * render.w) / 6
      const y0 = ((vertices[v0].y + 1) * render.h) / 6
      const x1 = ((vertices[v1].x + 3) * render.w) / 6
      const y1 = ((vertices[v1].y + 1) * render.h) / 6

      render.drawLine(x0, y0, x1, y1, 'white')
    }
  }
}

首先遍历所有面信息,遍历三角面点,将三角形3边对应连线(通过一个取余)。根据模型的大小调整渲染的位置(对应上面+3 +1 /6等操作)。 获取模型点信息后调用绘制线段方法执行。

下面效果图

ts实现3d渲染器 2

下面是Render 类

class Render {
  private canvas: HTMLCanvasElement
  private context: CanvasRenderingContext2D
  w: number
  h: number
  private color: string
  constructor(canvas: HTMLCanvasElement) {
    if (!canvas) {
      new DOMException('Canvas is not defined')
    }
    this.canvas = canvas
    this.context = this.canvas.getContext('2d')!
    this.w = this.canvas.width
    this.h = this.canvas.height
    this.context.translate(0, this.w)
    this.context.rotate(Math.PI)
    this.context.scale(-1, 1)
    this.color = 'white'
  }

  private drawPixel = (x: number, y: number) => {
    this.context.fillStyle = this.color
    this.context.fillRect(x, y, 1, 1)
  }
  drawLine = (x1: number, y1: number, x2: number, y2: number, color: string) => {
    this.color = color
    let x = x1
    let y = y1
    let dx = x2 - x1
    let dy = y2 - y1
    const ux = dx > 0 ? 1 : -1
    const uy = dy > 0 ? 1 : -1
    dx = Math.abs(dx)
    dy = Math.abs(dy)
    if (dx > dy) {
      let p = 2 * dy - dx
      for (let i = 0; i <= dx; i++) {
        this.drawPixel(x, y)
        x += ux
        if (p >= 0) {
          y += uy
          p += 2 * (dy - dx)
        } else {
          p += 2 * dy
        }
      }
    } else {
      let p = 2 * dx - dy
      for (let i = 0; i <= dy; i++) {
        this.drawPixel(x, y)
        y += uy
        if (p >= 0) {
          x += ux
          p += 2 * (dx - dy)
        } else {
          p += 2 * dx
        }
      }
    }
  }
}

export { Render }

下一节课: 将对三角形上色

最后项目源码:miemieooop/web-gpu (github.com)

转载自:https://juejin.cn/post/7287246372055597112
评论
请登录