1. 三种绘制像素性能测试
第一种 FillRect 绘制矩形方式
绘制10万像素点花费44ms 在 amd 7840u处理器中
typescript
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)
}
第二种 ImageData 单缓冲机制
绘制10万像素点花费1474ms
ts
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)
}
第三种 ImageData 双缓冲机制
绘制10万像素点花费10ms
ts
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)
}
2. 本文采用绘制方式
第三种是最快, 但会引出一个问题。canvas本身原点在左上角,这样导致渲染模型后会出现颠倒问题,而且putImageData这个方法与canvas本身原点的位置不相关,导致要实现像素级的翻转来倒转。因为本文教学行为比较多,故采用第一种方式。 下面是模型的颠倒图
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
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)
但我们这个obj文件是纯文本格式, 所以可以大幅简化模型的加载,直接使用Text()函数方法导出数据
ts
const load = async (url: string) => {
const text = await (await fetch(url)).text()
return parse(text)
}
2. 解码加载后的数据
我对ThreeJs代码阅读加简化后。写的parse方法
ts
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.渲染模型
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) {
// 遍历三角面片
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等操作)。 获取模型点信息后调用绘制线段方法执行。
下面效果图
下面是Render 类
ts
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 }
下一节课: 将对三角形上色