[图形学笔记系列] 实现一个简单的软光栅渲染效果-01

·  阅读 148

上一篇:[图形学笔记系列]渲染管线

目标

canvas画布上渲染出如下所示的效果。 image.png (素材都是源自:Tiny renderer or how OpenGL works: software rendering in 500 lines of code

实现思路

参照之前的笔记《[图形学笔记系列]渲染管线》,实现一个简单的光栅化渲染效果。

实践部分

需要用到的工具

canvas像素点着色的工具函数

主要是利用canvasgetImageData、putImageData方法,实现canvas像素级的操控

image.png 上图为getImageData的返回值,该对象显示此canvas使用srgb颜色协议,data属性为8位无符号整形固定数组,存储的是每个像素的rgba值,按顺序从左到右,从上到下分别存储每个像素的rgba值,值的范围为[0~255],也就是说数组的大小为widthheight4width*height*4

image.png

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()
复制代码

image.png 可以看到一条从左上角向右小角的直线,这其实也表明像素坐标是以左上角为中心,向右的横轴为x轴,向下的竖轴为y轴,如下所示。 image.png

用于矩阵运算的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()
复制代码

image.png 就是上面这个样子,在后面它会变得越来越接近目标。

纹理图片的读取

需要写一个读取纹理图片,并能够获取指定纹理坐标下像素存储的颜色值

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()
        })
复制代码

相当于把图片画出来了

image.png

未完待续...

下一篇:[图形学笔记系列] 实现一个简单的软光栅渲染效果-02

参考资料

GAMES101-现代计算机图形学入门-闫令琪

Fundamentals of Computer Graphics, Fourth Edition

Tiny renderer or how OpenGL works: software rendering in 500 lines of code

资源推荐

零基础如何学习计算机图形学

分类:
前端