zoukankan      html  css  js  c++  java
  • 图形学-从画点到三维模型

    图形学

    图形学就是在一个二维的平面上展示三维模型.
    这里我们用H5的canvas来演示. 我们不会用canvas的任何画图的方法, 只是把他当作一个屏幕来使用.

    三维模型

    要将三维物体,表现在二维平面上, 首先我们要有一个三维的模型.我们要先知道,如何在一个三维坐标系中构造一个物体的模型.
    这个模型是怎么做的呢. 他一般包含一下几点

    中心点和旋转角度.

    中心点一般用来确定一个模型在三维坐标系中的具体位置. 一般用 x, y, z 表示.中心点移动, 模型上的所有坐标跟着移动. 模型旋转时,绕中心点旋转.

    三角形网络 和 顶点.

    模型的具体形状是由三角形拼成的. 如图所示

    所有的模型都是有一个一个三角形拼成的. 三角形少, 精度就小.三角形多,精度就大. 也就越接近显示中的物体. 以一个正方体为例, 6个面, 一个面需要两个三角形拼成, 一共12个三角形.
    三角形的点就是顶点. 一般有很多三角形的顶点是重复的. 立方体一共八个顶点. 代码实践中我们是顶点做一个数组. 三角形做一个数组.

    	 let points = [
                -1, 1,  -1,     // 0
                1,  1,  -1,     // 1
                -1, -1, -1,     // 2
                1,  -1, -1,     // 3
                -1, 1,  1,      // 4
                1,  1,  1,      // 5
                -1, -1, 1,      // 6
                1,  -1, 1,      // 7
            ]
    
            let vertices = []
            for (let i = 0; i < points.length; i += 3) {
                let v = GuaVector.new(points[i], points[i+1], points[i+2])
                // let c = GuaColor.randomColor()
                let c = GuaColor.red()
                vertices.push(GuaVertex.new(v, c))
            }
    
            // 12 triangles * 3 vertices each = 36 vertex indices
            let indices = [
                // 12
                [0, 1, 2],
                [1, 3, 2],
                [1, 7, 3],
                [1, 5, 7],
                [5, 6, 7],
                [5, 4, 6],
                [4, 0, 6],
                [0, 2, 6],
                [0, 4, 5],
                [5, 1, 0],
                [2, 3, 7],
                [2, 7, 6],
            ]
    

    在上述代码中, points就是顶点的数组, 每三个元素是一组坐标,表示该顶点的x,y,z.
    indices是三角形的数组, 第一个元素[0, 1, 2] 表示第一个三角形. 这里的0表示 顶点数组里的第0个顶点.

    顶点的属性.

    顶点的属性包括,坐标(x, y, z), 颜色(rgba), 法向量(fx, fy, fz), 贴图.

    • 坐标就是这个点在三维坐标系中的坐标.
    • 颜色就是rgba颜色.
    • 法向量就是这个点的垂直方向. 法向量属性这里和我们的数学尝试有些区别. 一个点为什么会有法向量呢?
      原因在于我们这里的顶点并不是数学意义上的点. 数学上的点是没有大小,宽高,方向的. 但是我们这里的顶点有. 严格意义上来说 这里的顶点是一个长宽都是1像素的面,是面的最小组成单元 .他有坐标, 有宽高都是1,既然是面当然有法向量. 又因为他足够小.能够近似的看做点.
    • 贴图. 贴图指的是, 这个模型的颜色不是单一的颜色, 而是一张图. 一般会有一个贴图文件. 贴图文件里保存的是每一个顶点上应该填充的颜色. 顶点的贴图属性里保存的就是这个颜色值在贴图文件中的位置.

    视角

    视角就是三维空间中我们观察模型时的视线. 视角有三个属性.
    
    • position: 视角的坐标,就是眼睛的坐标
    • target: 我们的视角观察的目标的坐标. position和target连起来就是视线
    • up: 是position到target的距离.
      这三个属性确定一个视角. 视角移动的话, 三维模型的投影也会发生变化.

    光照阴影

    光照阴影(shading). 在二维坐标系中模拟显示中的物体还要考虑光照问题. 我们看到的颜色是物体反射光的颜色. 光线照射到物体上的角度不同, 物体上光线的强度就不同.
    就是有一个光源, 他发射除了光线, 光线照射到 物体上, 由于角度不同导致了, 虽然物体上的颜色相同, 但是看起来颜色有区别, 有高光, 有阴影.比如物体模型是红色, 光线与三角形是垂直关系90°展示正红色, 不是垂直是60°, 是暗红色, 随着角度越来越小,颜色越来越暗. 也就是说点的颜色要根据法向量和光线的夹角来计算.
    根据计算方式不同, 会有不同效果.
    如下图所示

    这是不同的光照模型和不同精度下模型的效果.  从上到下是精度越来越高, 从左到右是 光照的计算越来越精细.
    
    • 左1(a1), (flat shading)是一个三角形用一个法向量, 所以一个平面的光照是一样的.
    • 左2(b1),(goraud shading) 是一个顶点一个法向量, 一个三角形就有三个法向量. 其他点用顶点的光照计算插值.
    • 左3(c1), (phong shading) 是先 一个顶点一个法向量, 然后,用这三个顶点的法向量 插值计算其他点的法向量, 然后再计算光照. 所以c1即便模型精度很低, 但是也依然有了一个精度相对高的光照.

    这是不同光照下的法向量方向.

    二维平面

    像素

    要在二维平面上展示图像, 基础是画点. 一张图片是由像素组成的. 比如 一张分辨率为50*100的png图片. 那么这张图长宽有50个像素, 高有100个像素.像素就是图片最基础的组成单元. 所有的图片都是由像素组成. 
    像素的属性包括: 坐标x, y表示像素的位置. rgba表示像素的颜色.比如红色用rgba表示就是(255, 0, 0, 255). rgba的每一个值都是0-255. 用二进制表示刚好一个字节的数据量. 
    

    以H5中的canvas图片的像素为例.

    	  var c=document.getElementById("myCanvas");
            var ctx=c.getContext("2d");
            ctx.fillStyle="red";
            ctx.fillRect(0, 0, 50, 50);
            
            // 获取canvas上的像素数据. 参数(x, y, w, h)
            var imgData=ctx.getImageData(0, 0, 50, 50);
    
            imgData.data[1] =  255
            ctx.putImageData(imgData,10,10);
            // 使用putImageData之后, 会直接改变canvas上的像素颜色.
    

    这里的imgData就是canvas中的像素数据. 格式如下

    		{
    			height: 50,
    	         50,
    	        data: [
    	            255, 0,0, 255,
    	            255, 0,0, 255,
    	            255, 0,0, 255,
    	            ......
    	            ]
    		}
    

    这里的imgData表示这张canvas图片, 高50像素, 宽50像素. data里面的数据是从左上角开始,从左到右, 从上到下依次排列. 每4个元素表示该像素的颜色信息. 比如第一个像素的rgba是(255, 0, 0, 255).
    一共由 50 * 50 = 2500个像素, 每个像素用4个数据表示颜色. 所以在data里由10000个元素.

    所以只要我们有了一张图的像素信息, 就能直接把这张图展示出来.

    光栅化

    我们现在有了一个三维的立体模型, 屏幕是二维的, 要在一个二维屏幕上展示三维图形, 就需要把三维图形投影到二维平面上, 用像素展示出来, 称为为光栅化.
    比如:

    上图就是一个立方体在二维平面中的投影. 如果我们转变观察这个立方体的角度, 这个投影的形状也会发生改变. 从三维立体图形, 到二维平面的投影图, 我们可以通过三角函数计算, 获得最终的尺寸. 那么针对一个三维模型, 我们要考虑他的旋转角度, 相对于远点的位移, 观察的视角, 以及投影面. 这些都考虑到后, 我们就能得到一个贴近于真是的投影图.

    矩阵

    上面说了, 要从三维模型的坐标, 考虑很多条件, 用三角函数来计算出最终投影到平面上的二维坐标. 但是这样一个个计算太过复杂, 一般都是通过矩阵来直接计算的. 矩阵能将三维模型的坐标点, 直接转换成要投影的二维坐标, 常用的矩阵包括:

    • 计算旋转角度的旋转矩阵rotation
    • 计算模型位移的位移矩阵translation
    • 确认观察视角的视角矩阵view
    • 把三维坐标转换成二维坐标的投影矩阵projuction

    矩阵是跟着模型走的, 一个单独的模型, 比如说房间里的一张床, 一个桌子, 当他移动的时候, 所有的点都跟着移动. 当他旋转的时候, 所有的点都跟着旋转.这个模型有一个中心点和旋转角度来确定他在三维空间的位置, 比如水平旋转了30°, 根据中心点, 和 旋转角度, 一个桌子上的点就能算出旋转后的位置, 旋转矩阵就是做这个地, 位移矩阵就是计算当中心点移动了10个像素后, 其他点的位置. 旋转矩阵和位移矩阵是计算模型在移动后的三维空间的坐标, 视角矩阵是当三维空间的视角变化后, 模型在三维空间的坐标变化. 投影矩阵是将三维坐标转换成二维坐标.

    	    // 视角矩阵
    	    const view = Matrix.lookAtLH(position, target, up)
            // 投影矩阵
            const projection = Matrix.perspectiveFovLH(0.8, w / h, 0.1, 1)
    
            // 得到 mesh 中点在世界中的坐标
            const rotation = Matrix.rotation(mesh.rotation)	//旋转矩阵
            const translation = Matrix.translation(mesh.position)	//位移矩阵
            const world = rotation.multiply(translation)	// 世界矩阵
    
            // transform就是最后的矩阵.
            const transform = world.multiply(view).multiply(projection)
    

    矩阵可以相乘, 相乘的效果与依次使用是一样的.

    深度

    一个三维模型投影到二维平面, 因为我们看到是投影, 看到正面的话,就不能看到背面. 而且正面和背面虽然在二维平面的坐标是一样的, 但是颜色是不一样的. 那么我们怎么确定具体画哪个点呢?
    就用深度.
    一个三维的顶点经过矩阵计算后得到的除了x,y二维坐标, 还有一个深度. 这个深度是和视角的距离. 如果和视角的距离近, 那么就展示,如果相对较远, 就不画. 一般来说是保存一个 二维平面上已经画好的点的数组 .这个点的信息就包括他在二维平面上的坐标,颜色,深度. 再画点时, 先判断这个点的坐标是否已经画过了, 如果没有,就画上. 如果画过了, 对比深度.如果当前点的深度更近,就把原来的覆盖掉, 否则不画.

    插值

    我们的图形是以三角形为基础的, 图像信息都是存储在顶点上的, 顶点上的坐标, 颜色都有了, 但是我们不能只画顶点, 那么两个顶点之间的点的图像信息是怎么得到的呢?
    就是用插值计算得到的.
    比如说有两个点.

    • A点,坐标(10, 10).颜色红色(255, 0, 0, 255)
    • B点, 坐标(50, 50). 颜色绿色(0, 255, 0, 255)

    现在要问处于这两点之间的C点的颜色和坐标.
    首先计算factor, 过渡因子.
    首先计算AB的长度.

    var x1 = 10, y1 = 10, x2 = 50, y2 = 50 
    // 长度s
    var s = (  (x2 - x1) ** 2 + (y2 - y1) ** 2 ) ** 0.5
    
    // 假设我们要画AB上距离A点10像素的点, 根据点距A的距离占总距离的比例,计算到他的坐标
    var factor = 10 / s
    
    var x = factor * (x2 - x1) + x1
    var y = factor * (y2 - y1) + y1
    
    

    颜色的插值计算与坐标一样.
    C点是AB的中心点, 那么C的坐标就是(30, 30), 颜色(128, 128, 0, 255).所有的计算中有小数的四舍五入.
    画线就是把AB两点之间的所有的点, 根据这个点的所在位置,计算插值, 然后把所有的点都画出来.

    画三角形

    现在二维平面上的三角形点的信息有了, 根据插值我们能画线了. 那么三角形这个面怎么画呢?

    首先我们有三角形的三个顶点A B C按Y轴坐标排列, a.y > b.y > c.y.
    对AC边求得M点(m.y == b.y). 三角形就被划分成了AMB, MBC两个三角形.
    然后先画AMB.
    从上到下, 从A点开始画平行线. 根据距离A点的高度, 算出factor,计算插值, 然后找出AM上的sx点和 AB上的ex点. sx-ex线与MB平行. 这算是在AMB三角形中填了一条线. 然后再用平行线一条一条把整个AMB填满, 从上到下, 如果A距离MB的高度为10, 那就是10条线.
    AMB画完了再画MBC.
    一个三角形画完了, 把所有的三角形都画完.整个立体图形就都出来了.

  • 相关阅读:
    移动开发 Native APP、Hybrid APP和Web APP介绍
    urllib与urllib2的学习总结(python2.7.X)
    fiddler及postman讲解
    接口测试基础
    UiAutomator2.0 和1.x 的区别
    adb shell am instrument 命令详解
    GT问题记录
    HDU 2492 Ping pong (树状数组)
    CF 567C Geometric Progression
    CF 545E Paths and Trees
  • 原文地址:https://www.cnblogs.com/bridge7839/p/13938520.html
Copyright © 2011-2022 走看看