zoukankan      html  css  js  c++  java
  • Canvas原生API(纯CPU)计算并渲染三维图

    Canvas原生API(纯CPU)计算并渲染三维图

    前端工程师学图形学:Games101 第三次作业

    利用Canvas画三维中的三角形并使用超采样实现抗锯齿

    最终完成功能

    1. Canvas 原生API实现三角形栅格化算法
    2. 实现 z-buffer 判断三角形先后关系
    3. 使用 super-sampling 处理 Anti-aliasing,也就是超采样实现抗锯齿

    第三次作业1

    展示2

    1 整体分析

    本次实验中,首先需要进行矩阵变换,将初始传入的三角形经过变换后到规范立方体内,这需要进行三种变换。设一个点的坐标变换为(x, y, z) -> (x', y', z')

    \[\begin{bmatrix} x' \\ y' \\ z' \\ 1 \end{bmatrix} = M_{presp} \times M_{view} \times M_{model} \times \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} \]

    每个矩阵的求解在之前的博客中都有讲解图形学 旋转与投影矩阵—2 - 知乎 (zhihu.com),这是其中一篇可供参考,因此,投影矩阵,视图矩阵和模型矩阵这里不再求解。现状,变换矩阵已经知道,现状需要将转换后的矩阵进行光栅化,在光栅化时,需要遍历屏幕上的每个像素点进行判断,该点是否在三角形内,如果在,则渲染,由于本文采用的 Canvas 进行渲染,因此需要对 Canvas 上的每个像素点进行判断。

    光栅化完成后,可得到一个充满颜色的三角形,如果渲染多个三角形,会产生覆盖现象,这个时候就需要判断深度,因此我们需要维护一个深度缓冲的数组,这个数组的大小为 canvas 的 width*height。当渲染后面的三角形时,首先判断该像素的当前深度是否小于预渲染像素的深度,如果小于,则渲染,否则,不进行处理。

    上述完成后,会得到一些一个带锯齿的三角形,为了解决锯齿问题,这里进行了超采样,即让一个像素点平分为 9 块正方形区域,看九块区域有多少在三角形内,占比情况,凭占比量设置该像素的颜色,最终完成抗锯齿的功能。

    总结,完成该实验的步骤如下

    1. 矩阵变换,投影,视图,模型变换
    2. 光栅化,使用 Canvas 原生 Api 画颜色
    3. 抗锯齿,超采样实现,将一个像素点分为 9 个正方形

    2 代码分析

    第一步:矩阵变换函数

    // 变换函数
    function getFinalPosition(position){
        const finalPosition = new THREE.Vector4().set(
            position.x,
            position.y,
            position.z,
            1
        ).applyMatrix4(perspMatrix).applyMatrix4(viewMatrix);
        finalPosition.set(
            finalPosition.x/finalPosition.w,
            finalPosition.y/finalPosition.w,
            finalPosition.z/finalPosition.w,
            1
        )
        return finalPosition;
    }
    

    输入三角形的坐标即可得到转换后的最终坐标,为了简单使用,这里没有使用到模型矩阵,仅仅用到了投影矩阵和视图矩阵。经过转换后,三角形坐标 x,y,z 都被规范到 [-1, 1] 之间了

    第二步:将像素坐标转换为屏幕坐标

    屏幕空间内,像素是从 (0, 0) 到 (width-1, height-1),渲染的范围为 (0, 0) 到 (width, height),width 和 height 是 Canvas DOM 的宽和高。注意:像素是一个一个方块,如下图所示。

    2_1像素

    由此可得,规范立方体到屏幕空间的坐标变换矩阵为

    \[M_{viewport} = \begin{bmatrix} \frac{width}{2} & 0 & 0 & \frac{width}{2} \\ 0 & \frac{height}{2} & 0 & \frac{height}{2} \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

    代码如下

    // 转换成屏幕坐标
    function transScreen(positions, width, height){
        const MViewPort = new THREE.Matrix4();
        MViewPort.set(
            width/2, 0, 0, width/2,
            0, -height/2, 0, height/2,
            0, 0, 1, 0,
            0, 0, 0, 1
        )
        positions.forEach(vec => vec.applyMatrix4(MViewPort));
    }
    

    第三步:光栅化

    光栅化:遍历相应像素点,判断该点是否在三角形内,在的话才继续处理,遍历范围是包围三角形最大的矩形盒。

    矩形盒

    求得三个顶点的宽高的最大最小值即可求出这个包围盒,分别为 minX, minY, maxX, maxY,开始遍历判断

    const ctx = canvas.getContext('2d');
    ...
    // 1. 遍历包围盒的每个像素
    for (let i = Math.round(minX); i < maxX; i++) {
            for (let j = Math.round(minY); j < maxY; j++) {
                ...
                // 2. 判断像素是否在三角形内
                if((count=getInner(point1, point2, point3, pixel))!==0){
                    ...
                    // 3. 创建颜色
                    switch (type) {
                        case 1:
                            data[0] = redColor;
                            data[1] = greenColor;
                            data[2] = blueColor;
                            data[3] = 透明度;
                            break;
                        case 2:
                            data[0] = redColor;
                            data[1] = greenColor;
                            data[2] = blueColor;
                            data[3] = 透明度;
                            break;
                    }
    				// 4. 赋予相应像素点颜色
                    ctx.putImageData(myImageData, i, j);
                }
            }
        }
    

    运行上述程序后,Canvas 绘画单个三角形的工作便完成了。

    我们需要维护一个深度数组,用来存储当前像素的深度

    const z_buffer = []
    for (let i = 0; i < height; i++) {
        const arr = [];
        z_buffer.push(arr)
        for (let j = 0; j < width; j++) {
            arr.push(-Number.MAX_VALUE)
        }
    }
    

    设置每个数字为无穷远,代表后续的每个三角形都比其像素点近,如果点在三角形内,判断当前深度并进行赋值

    // getZ 表示获取欲渲染像素点的深度,z_buffer 存储当前像素点深度
    const z = getZ(i+0.5, j+0.5);
    if(z<z_buffer[j][i]){
        // console.log('success');
        continue;
    }
    z_buffer[j][i] = z;
    

    第四步:抗锯齿

    将一个像素点分为 9 份相同大小的正方形,判断有多少份正方形在三角形内,最后凭占比赋予颜色

    // 获得一个像素点分成 9 份正方形后,在三角形内的个数
    function getInner(point1, point2, point3, pixel){
        let extend = {x:0, y:0, index: 0}
        for (let i = 1/6; i < 1; i+=1/3) {
            for (let j = 1/6; j < 1; j+=1/3) {
                extend.x = i;
                extend.y = j;
                if(isInner(point1, point2, point3, pixel, extend)) extend.index++;
            }
        }
          
        // 判断当前正方形是否在三角形内
        function isInner(point1, point2, point3, pixel, extend){
            pixel.x += extend.x;
            pixel.y += extend.y;
    
            const ab = new THREE.Vector3().subVectors(point2, point1);
            const bx = new THREE.Vector3().subVectors(pixel, point2);
            const direct1 = new THREE.Vector3().crossVectors(ab, bx);
    
            const bc = new THREE.Vector3().subVectors(point3, point2);
            const cx = new THREE.Vector3().subVectors(pixel, point3);
            const direct2 = new THREE.Vector3().crossVectors(bc, cx);
    
            const ca = new THREE.Vector3().subVectors(point1, point3);
            const ax = new THREE.Vector3().subVectors(pixel, point1);
            const direct3 = new THREE.Vector3().crossVectors(ca, ax);
    
            const f1 = direct1.dot(direct2);
            const f2 = direct2.dot(direct3);
    
            return Math.sign(f1) === 1 && Math.sign(f2) === 1;
        }
    
        return extend.index;
    }
    

    将像素点平均分成九份后,每份都为正方形,找出正方形中心,判断该中心是否在正方形内,最后总结出在三角形内的正方形个数,最后赋予颜色.

    count=getInner(point1, point2, point3, pixel);
    const rat = count/9;
    switch (type) {
        case 1:
            data[0] = 255 * rat + oriData[0] * (1-rat);
            data[1] = oriData[1] * (1-rat);
            data[2] = oriData[2] * (1-rat);
            data[3] = 255 * rat + oriData[3] * (1-rat);
            break;
        case 2:
            data[0] = oriData[0] * (1-rat);
            data[1] =255 * rat + oriData[1] * (1-rat);
            data[2] =255 * rat + oriData[2] * (1-rat);
            data[3] =255 * rat + oriData[3] * (1-rat);
            break;
    }
    

    3. 总结

    使用 Canvas 原生 API 实现三维图形的光栅化,能够加强我们都图形学坐标转换的整体印象,能使我们了解基本原理,对我们理解游戏等三维引擎的底层原理有很大的帮助。

    我这完整代码没有整理,不太好看,就不放出来了,需要源码交流的可以私聊,如果觉得有用,可以点个赞哦。

    希望读者在看完后能提出意见, 点个赞, 鼓励一下, 我们一起进步. 加油 !!
  • 相关阅读:
    【luogu P7418】Counting Graphs P(DP)(思维)(容斥)
    【luogu P7417】Minimizing Edges P(贪心)(思维)
    多边形序列(组合数)(高精)(NTT)
    【luogu P3803】【模板】多项式乘法(NTT)
    【luogu P1919】【模板】A*B Problem升级版(FFT快速傅里叶)
    【luogu P6139】【模板】广义后缀自动机(广义 SAM)
    【luogu P7529】Permutation G(几何)(数学)(DP)
    【luogu P5787】graph / 二分图 /【模板】线段树分治(扩展域并查集)(线段树分治)
    同桌的你(环套树)(DP)
    石子游戏(博弈论)(Spaly)
  • 原文地址:https://www.cnblogs.com/xiaxiangx/p/15770053.html
Copyright © 2011-2022 走看看