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.
    一个三角形画完了, 把所有的三角形都画完.整个立体图形就都出来了.

  • 相关阅读:
    [置顶] MapReduce 编程之 倒排索引
    java学习之路---线程(重点)
    CentOS下用Tomcat+Zookeeper+Nginx+Solr完美搭建SolrCloud平台(五)
    qsort的几种用法
    两道水题(月之数)(排序)
    快排
    Red and Black(简单dfs)
    zb的生日(暴搜dfs)
    又见01背包
    五子棋
  • 原文地址:https://www.cnblogs.com/bridge7839/p/13938520.html
Copyright © 2011-2022 走看看