上一篇:[图形学笔记系列]渲染管线
目标
在canvas
画布上渲染出如下所示的效果。
(素材都是源自:Tiny renderer or how OpenGL works: software rendering in 500 lines of code)
实现思路
参照之前的笔记《[图形学笔记系列]渲染管线》,实现一个简单的光栅化渲染效果。
实践部分
需要用到的工具
canvas像素点着色的工具函数
主要是利用canvas
的getImageData、putImageData
方法,实现canvas
像素级的操控
上图为
getImageData
的返回值,该对象显示此canvas
使用srgb
颜色协议,data
属性为8位无符号整形固定数组,存储的是每个像素的rgba
值,按顺序从左到右,从上到下分别存储每个像素的r
、g
、b
、a
值,值的范围为[0~255],也就是说数组的大小为。
function createCanvas(targetDom, width, height) {
let dom_canvas = document.createElement('canvas')
let ctx = dom_canvas.getContext('2d')
let frameBuffer = ctx.getImageData(0, 0, width, height)
dom_canvas.width = width
dom_canvas.height = height
targetDom.appendChild(dom_canvas)
function drawPixel(x, y, color) { //索引向下取整
let index = (Math.floor(x) + Math.floor(y) * width) * 4
for(let i = 0; i < 4; i++) {
frameBuffer.data[index + i] = color[i] * 255 //color值为0-1,约定,方便color参数的计算。
}
}
function draw() { //像frameBuffer的数据渲染出来
ctx.putImageData(frameBuffer, 0, 0)
}
function clear() { //清除画布
ctx.clearRect(0, 0, width, height)
frameBuffer = ctx.getImageData(0, 0, width, height)
}
return {drawPixel, draw, clear}
}
我们可以用上面的工具画一些图形,例如:
let {drawPixel, draw, clear} = createCanvas(document.querySelector('body'), 600, 600)
for(let i = 0; i < 600; i++) {
drawPixel(i, i, [0.5,0.5,0.5,1])
}
draw()
可以看到一条从左上角向右小角的直线,这其实也表明像素坐标是以左上角为中心,向右的横轴为x轴,向下的竖轴为y轴,如下所示。
用于矩阵运算的glMatrix
库
整个实现过程中会用到很多的矩阵运算,glMatrix
是一个轻便的js
矩阵运算库,非常容易使用,这是官方文档glmatrix.net/docs/ 功能查找起来也挺方便的,在具体用的时候再看。
模型数据的解析
为了得到上面渲染的效果,至少需要模型的顶点坐标、法线坐标、UV坐标以及纹理图片,所以需要一些解析数据的工具函数。
/*
* 偷个懒,顶点坐标、uv坐标以及法线坐标的数据我事先已经整理成数组了,面数组里存储每个面的三个顶点
* 及其对应的UV坐标和法线坐标,以索引的形式存储。
*/
function createModel(_vertices, _uvs, _normals, _faces) {
let length = _faces.length;
function getFace(index) {
let face = []
for(let i = 0; i < 3; i++) {
face.push(
{
vertice: _vertices[_faces[index-1][i][0]-1], //原始数据里索引是从1开始的,所以这里都要减1
uv: _uvs[_faces[index-1][i][1]-1],
normal: _normals[_faces[index-1][i][2]-1]
}
)
}
return face
}
return { getFace, length }
}
可以尝试把这些点画出来看一下
let {drawPixel, draw, clear} = createCanvas(document.querySelector('body'), 600, 600)
let model = createModel(verticles, uvs, normals, faces)
for(let i = 1; i <= model.length; i++) {
let face = model.getFace(i)
for(let j = 0; j < 3; j++) {
drawPixel((face[j].vertice[0]+1)*300, (-face[j].vertice[1]+1)*300, [0, 1, 0, 1])
//这里加了点视口变换
}
}
draw()
就是上面这个样子,在后面它会变得越来越接近目标。
纹理图片的读取
需要写一个读取纹理图片,并能够获取指定纹理坐标下像素存储的颜色值
async function createTexture(url, width, height) {
let dom_canvas = document.createElement('canvas')
dom_canvas.width = width
dom_canvas.height = height
let ctx = dom_canvas.getContext('2d')
function getTexture(uv) {
let u = uv[0] * width //uv坐标是从0-1,所以需要转换至像素坐标
let v = uv[1] * height
let color = []
let index = (Math.floor(u) + Math.floor(v) * width) * 4 //ImageData一维数组存储颜色,所以索引需要重新计算一下
for(let i = 0; i < 4; i++) {
color.push(img_data[index + i]/255)
}
return color
}
let img_data = await new Promise((resolve, reject) => { //用Promise包装一下纹理加载
let texture = new Image(width, height)
texture.src = url
texture.onload = () => {
ctx.drawImage(texture, 0, 0, width, height)
resolve(ctx.getImageData(0, 0, width, height).data)
}
texture.onerror = () => reject('Texture Error....')
})
return getTexture
}
我们可以利用前面的drawPixel
和上面的getTexture
把纹理图片画出来瞅瞅
let {drawPixel, draw, clear} = createCanvas(document.querySelector('body'), 600, 600)
let model = createModel(verticles, uvs, normals, faces)
let texture = createTexture('./african_head_diffuse.jpg', 600, 600)
texture.then((getTexture)=>{
for(let i = 0; i < 600; i++) {
for(let j = 0; j < 600; j++) {
drawPixel(i, j, getTexture([i/600, j/600]))
}
}
draw()
})
相当于把图片画出来了
未完待续...
参考资料
Fundamentals of Computer Graphics, Fourth Edition
Tiny renderer or how OpenGL works: software rendering in 500 lines of code