zoukankan      html  css  js  c++  java
  • 谈谈 图形学 的 从头建设发展之路

    图形学 要 解决的 几个问题:

     

    1 模型,

    2 透视

    3 边界判断 和 遮挡

    4 光影 重力 和 机械运动 等 物理效果

     

    模型的部分,我们先从 二维图形 开始说起,  比如  “如何判断一个点在 多边形 内?” ,  这个问题我将之称为 “边界判断”  问题,

    假设我们设计一个游戏的话,  就会考虑这个问题,不过对于 魂斗罗 超级玛丽 这样的 8 位机 游戏, 角色 道具 之间的 碰撞 应该是 直接比较 角色 道具 的 像素, 还没有用到图形学,因为 那个时代的 游戏 的 分辨率 不高,  角色 道具 的 像素数量 很少,    直接比较 像素 即可 。

     

    对于 图形学 来说,  如何解决  边界判断  问题?

    我想过一个办法,就是 把 多边形 分解为 若干个 三角形,然后 判断 点 是否在 这些 三角形 里, 但是 有 网友 说了一个 更直接 的 办法,就是 连线, 就是 让 点 和 多边形 的 每个端点 连线,如果这些连线和 多边形 的 边 有相交 则 点 在  多边形 外,  没有相交 则 点 在 多边形 内,  确实 这办法 很好,  但是只能处理 “没有凹陷” 的 多边形 。当然,分解为 三角形 的 方法 也只能 处理  “没有凹陷” 的 多边形 ,  在 有 凹陷 的 情况下,  分解三角形 本身 就是个 困难的 工作,  需要 判断 多边形 的 边 的 “哪一侧” 是 “内侧”,    这个问题 是 边界判断 的 根本问题 。

     

    如何 判断 多边形 的 边 的 “哪一侧” 是 “内侧”,      最一般 也是 最复杂 的 情况 是,  任意给定一些点 和 一个顺序, 按照这个顺序 把 点 连接起来  构成一个 多边形  。

    这种情况是 最一般 也是 最复杂  的   。

    比如,    给   A , B , C , ……   X , Y , Z     这样一些点,      按照   A , B , C , ……   X , Y , Z    的 顺序 连起来 。

    这要 考虑 交叉 的 情况,  即使没有 交叉,   要 知道 边 的 哪一侧 是 内侧,   也是 很麻烦 的 。

    这需要 判断 最后一点(Z) 和 第一点(A) 连接 的 闭合方向 是 顺时针 还是 逆时针,

    这个 顺时针 逆时针 并不是 ZA 方向,  而是 要 判断 本次 “环绕” 是在 上次 “环绕” 的 内部 还是 外部  。

     

    但是,有一种办法 可以让 问题 简化,  就是 人为 的 给出 某条边 的 内侧,   这样可以 递推 出 所有 边 的 内侧,

    比如 , 指定 AB 的 “右边” 是 内侧,  即 A 到 B 的 方向 的 右边 为 内侧,   这样可以递推出 BC , CD , DE ,  ……   XY , YZ  的 右边 是 内侧 。

    这样,只要 判断 点 是否在 所有 边 的 内侧 就是 判断 点 是否在 多边形 内部 。

     

    这是 可行 的,  因为 模型 是 人 建立 的,  所以 人 当然可以 指定 边 的 哪一侧 是 内侧 ,  这样 问题 就 大大 简化 了  。

     

    如果 把 边界判断 问题 推广到 三维 的 话,   情况 会 更复杂,    如果由 一些 三角形面 来组成一个 体 的话,  这类似与 二维 由 一些 线段 组成 多边形,

    那么,  对于 这些 三角形面 是否 能 组成一个  封闭 体 ,   这是一个问题,    二维 的 情况 只要 点 顺序 的 连接,  最后一点 和 第一点 相连, 就是一个 封闭多边形, 当然,这里面 存在 线段 交叉 的 情况,但 交叉了 也是 封闭 的,或者 可以 按 交叉 的 方式 另行 处理  。

    但是 三维 的 情况 就 复杂 了,    空间 中的 面 是 会 扭曲 的,    这 不仅仅 发生在 连续曲面,   用 三角形面 组成 的 “组合面” 也 会 扭曲,

    事实上,  三角形面 组成的 组合面 可以模拟 连续曲面 和 逼近,  这也是 3D 建模 的 基础  。

     

    由于 面 的 扭曲,   所以 三维  中 要 判断  面 是否 构成一个 封闭体  就 很复杂,    我觉得 单纯 用 算法 大概 很难 实现  。

    这里的 面 包括 了 连续曲面 和 组合面  。

     

    面 的  扭曲,   我们可以 举个例子,     就以  拓扑学 的 经典例子 来看:

    把 一条 纸带 扭一下  再  首尾  粘 起来,    这就 形成了一个 纸带圈,    因为 扭 了 一下, 这个 纸带圈 就有点 神奇,

    我们把 一只 蚂蚁 放在 纸带 上,   它可以 从 纸带 的 内侧 爬到 外侧,   又从 外侧 爬到 内侧,    如此 循环   。

    把  纸带 看作一个 面,  让 这个 面  延展 出去,   是 不能 组成一个 封闭体 的 ,

    这是因为  有 那个  扭曲 的 存在,   因为这个 扭曲,    即使  延展 出去 的 面 封闭 了,    扭曲 那里 仍然 会 自然 的 形成 一个  “洞”,

    或者说 一个 隧道,   两端 与 外界 相通,   感觉 好像 空间的 漏洞  ……

     

    当然,     你可以用  一些 其它 的 面 把 隧道 的 两端 给 堵起来,  但 这就不是 纸带(面) 本身 延展 的 结果 了  。

     

    所以,   我的看法 是 ,  3D 建模 中,    一个 体 是否 封闭, 是由 建模 的 人 依靠 直观 来 实现 的,

    在 体 封闭 的 前提 上,   和  二维  一样,     人为 的 指定 某个 单元面 的 哪一侧 是 内侧,  这样可以 递推 知道 所有 的 单元面 的 内侧,

    这样 就可以 解决 三维 下 的 边界判断 问题  。

    只要 判断 点 是否在 所有 单元面 的 内侧 就可以 判断 点 是否在 封闭体 内  。

     

    这里说的   单元面 是 指 组成   封闭体  的 简单图形平面,         最基本 的 是 三角形面,

    理论上,     所有的 多边形面 都可以由 三角形面 构成,       不过 对于一些 特定 的 模型,  可以   直接使用      矩形 梯形 或 其它 多边形 来 作为 单元面 ,

    比如        长方体,        直接用 矩形面 组成就可以   。

     

    三角形面 有一个 特点 是  可以 组成  扭曲  的 组合面 ,    矩形面  不能 组成  扭曲  的 组合面  ,    所以 三角形面 是 基本 的 单元面  。

     

    接下来 说说 透视 ,       透视 是 美术,  也就是 画画 里 的 术语,     透视 所指 的 就是 对于 人眼 来说,   远处 的 物体 变小 的 视觉效果,   以及 矩形 看起来是 平行四边形 、 圆形 看起来像 椭圆形 这样的效果  。

    对于 3D 来说,     模型 最终 投影 并 渲染 到  屏幕 应该 满足 透视 效果,  这样看起来才符合人的视觉效果  。

    那 如何 计算 透视  呢?

    我们来看看    人眼(照相机)  的 成像示意图 :

     

    如图,     线段 AB  经过 瞳孔(镜头)  在 视网膜(胶片) 上的 成像 是  ba,    是一个 倒立相反 的 像,

    物理学 中,  我们知道, 凸透镜 的 成像 是 倒立相反 的,     眼睛(照相机) 的 成像 就是 凸透镜 原理  。

     

    可以看到   abo 和 ABo   两个 三角形 是 相似三角形,   ab 和 AB  之间 存在一个 比例关系:    ab / oh = AB / oH ,

    ab = AB  *  ( oh / oH )   ,              oH   是  AB 到 瞳孔(镜头) 的 距离,   所以, 像 的 大小 和 物体 离 瞳孔(镜头) 的 距离 成  反比  。

     

    像 就是 有 透视效果 的 投影 ,   我们也可以把 像 称为 透视投影  。

     

    那么,  根据上面 这个 反比 关系,是不是可以来 计算 透视投影 了呢? 还不行, 因为 最关键 的 是 要 确定 组成 物体(模型) 的 单元面 的 端点 的 位置,

    所以,  还是 需要 根据 成像 的 原理 来 计算 单元面 端点 的 位置 ,  以此 得到 物体(模型) 在 视平面 上的 透视投影  。

    上图 中 的  视网膜(胶片)  就是    视平面  。

     

    因为  像  是 倒立相反 的,我们 还要 把 像  “反转”  过来,  才能得到  正立  的 像,   在 计算机  里 这 很简单,   像 是 一个 位图,    位图 是 一个 二维数组,  只要 以 倒序 的 方式 输出 数组 的 数据 得到的 就是 “反转” 的 像,  倒序 就是 以 从 最后一个 元素  到  第一个元素 的 顺序 访问 元素  。

     

    虽然 3D 中 需要 根据 成像 原理 计算 像(透视投影),     但是, 在 2D 的 第一视角游戏 等 一些 模拟 3D 效果 的 场合(俗称 “假 3D”)  可以 使用 上述 的 反比关系 模拟 3D 透视效果,  比如  2D 下 的 第一视角 赛车游戏 什么的  。

     

    接下来说 遮挡,   遮挡 是个 麻烦事,  遮挡 是 图形学 里的 一个  大问题,  我们先来看看    二维   里 的 遮挡 是 怎么 计算 的 ,

    二维 里 的 遮挡 比如 浏览器 里的 元素 之间 的 叠放,   还有 Windows 操作系统 里 窗口 之间 的 叠放 。

    只要 根据 元素(窗口) 的  叠放层次 来 渲染  就可以 。

     

    不过 我们 先说说   “渲染”   这个词,    渲染 的 英文 是  Render  ,   但是 在  Windows 编程 里 ,通常说  “绘制” ,  比如    绘制窗口,   所以, .Net 的 System.Windows.Forms 下 的 Control 基类 有  OnPaint()  和  OnPaintBackground()  虚方法,  而 窗口(Form) 和 所有的 控件 都是 继承 Control 基类 的,

    Paint 就是 画画, 绘制 的 意思,     还有一个 近义词,就是 Draw,    Draw 也有 画画 的 意思,      所以 .Net 提供 GDI+ 的 名字空间 System.Drawing 是用 Drawing 来命名  。  里面 那些 矢量图形 的 绘制方法 大概 也是  Draw()   ,    我猜的,   记不清了  。

    当然,    System.Drawing   名字空间 下 还有一个 重要的 角色 是 Brush  ,    刷子 ,     又称  画刷  。

     

    我觉得  Draw  主要是 画线条,   Paint  则 偏重于   涂色,以及 整个 综合 的 绘画过程  。

     

    以 浏览器 为例,   设 有  1 到 n 层 元素,   1 层 为 最低层,  n 层 为 最高层,    则 从 最低层 开始 渲染,  逐层 渲染 至 最高层,

    渲染  就是 输出 像素   。

     

    这样 高层 会 覆盖 底层 的 内容(像素),    这样 就 实现 了  叠放(遮挡) 效果  。

     

    再说说 半透明 的 问题,     假设 2 层 的 透明度 是 40% ,  那 在  2 层 遮挡  1 层 的 区域,  应该 有 40% 的 像素 显示 1 层 的 像素,   这 40% 的 1 层 像素, 应该 均匀 的 分布 在 遮挡区域 内 ,    这样 来 实现 半透明 的 效果  。

    对于 多层叠放 和 多层半透明,   一样的方法,  逐层 计算 即可 。

     

    当然 大家 会问,   能不能 不用 这种 笨办法,  能不能 计算出  每一层 被 上层 遮挡 后  “露出”  的 部分,  然后 每一层 只 渲染 露出 的 部分 ?

     

    也可以  。  但是 遮挡 会产生 许多 不规则 的 图形,   虽然 浏览器 的 元素 和 Windows 窗口 都是 矩形,  即便如此,   遮挡 也会 产生 各种 不规则 图形,

    就像 围棋盘 一样  。

    不规则图形 是指 “露出” 的 部分,   因为 露出 部分 是 不规则图形,    所以计算起来 比较 麻烦,    如果 元素(窗口) 小 而 密集,  那 露出 部分 会 呈 不规则 和 “碎片化”,   这样 计算量 也不小  。

     

    在 三维 下,   计算 露出 部分 就 更复杂,   因为  三维 模型 之间 遮挡 的 露出部分 的 投影 会是 各种 奇怪 的 不规则的 图形,  这个 投影 包括 几何投影 和 透视投影  。

    所以,我放弃了  三维  下  计算 露出 部分 的 算法  。

     

    退而求其次,   像 二维 那样,  根据 叠放(遮挡) 的  层级,   让 每个 模型 的 每个 单元面 一一 渲染,    如何?

    这首先需要 知道 叠放(遮挡) 的 层级,

    用通俗的话说,     叠放(遮挡) 的 层级 就是 单元面 谁在前 谁在后,   这个  单元面 包含 所有模型 的 所有单元面,

    同一个 模型 的 单元面 也有 前后 遮挡 的 关系,      因为 一个 物体 有 “正面” 和 “背面” 。

    要给 所有模型 的 所有单元面 计算出 遮挡层级,      也就是说,  要给 所有模型 的 所有单元面 排一个序,  看看 谁在前 谁在后,就像 给 幼儿园 的 小朋友 排队一样 。

     

    假设 场景 里的 所有模型 由  n 个 单元面 组成,    每个 单元面 需要 和 其它 的 所有单元面 都 比较一次 谁在前 谁在后,   

    那么, 需要 比较 的 次数 是      (n - 1) + (n - 2) + (n - 3) + (n - 4) + …… + 1     。

     

    比较 的 算法 是 看 2 个 单元面 在 视平面 上的 投影 有没有 相交,   如果没有 则 2 者 不存在 遮挡关系,  如果有,  则 取 投影 中的 任意一点,  比较 该 点 到 单元面 的 距离,  距离大的 单元面 靠后,  距离小 的 单元面 靠前,  距离大 的 单元面 被 距离小 的 单元面 遮挡  。

    这里说的 投影 包括了  几何投影 和 透视投影 ,     投影点 到 单元面 的 距离 就是 投影点 沿着 投影线 到 单元面 的 距离 。

    当然,  几何投影 和 透视投影 的 投影方式 不同,     投影线 也 不一样 。

    这个 方法 可以用于 计算 单元面 在 几何投影 上 是否有 遮挡, 也可以用于 计算 单元面 在 透视投影 上 是否有遮挡 。

     

    可以看到,         (n - 1) + (n - 2) + (n - 3) + (n - 4) + …… + 1         这个 比较 次数 不小  。

    假设 组成 所有模型 的 单元面 是 10000 个,  那么   (n - 1) + (n - 2) + (n - 3) + (n - 4) + …… + 1   就等于    10000 * (9998 / 2) + 5555  =   49995555  ,   差不多 5 千万,  或者说 接近   10000 * 10000 / 2 = 5 千万   。

     

    10000 * (9998 / 2) + 5555  =   49995555                   这是 高斯先生 的 算法,   高斯  小学 时候 老师 出了 一道题, 让 同学们 从 1 加 到 100,  然后 高斯 很快就算好了,他的算法是 100 + 1 = 101 ,   99 + 2 = 101 ,    99 + 3 = 101   ,  一共有 50 个 101,    所以  1 + 2 + 3 + …… + 100 = 101 * 50 = 5050  。

     

    一个 比较细腻 的 模型 ,比如 人体模型,  似乎 很容易 花费  1 万 个 单元面,   当然 这是 推测 。

     

    所以,  一个  和 现实世界 比较 接近 的 场景 中 所有模型 的 单元面 数量 也是 可观 的, 计算这些 单元面 的 遮挡层次 的 计算量 也很 可观  。

     

    所以 我 暂时 对 这种 做法 持 保留态度,    不过我们还是可以看看 如果 知道 所有 单元面 的 遮挡层级,   那么 如何 来 渲染  。

    其实 和 二维 差不多,   区别 是 三维 下 要 先 计算 透视投影,   

    三维 的 渲染 是 先 计算 透视投影,    把 透视投影 输出像素 到  位图 ,   位图 就是 渲染结果 。

     

    和 二维一样,    从 最低层 的 单元面 开始 渲染,  逐层 渲染 至 最高层  ,     这样  高层 会 覆盖 底层 的 内容(像素),    这样 就 实现 了  叠放(遮挡) 效果  。

     

    对于 半透明 的 效果,   也 和 二维 一样,   在 渲染 中,    如果 单元面 遮挡了 某些 物体,   则  在 该 单元面 的 渲染结果 中 以 透明度 为 比例 显示 上层 渲染结果 的 像素  。   假设 单元面 的 透明度 是 40%,那么 在 单元面 的 渲染结果(输出像素)中  应该有 40% 的 像素 显示的是 上层 的 渲染结果 的 像素  。    这 40% 的 像素 是 均匀 的 分布在 单元面 的 渲染结果 里 的  。

     

    显然, 这个做法 的 前提 是 计算出 所有 单元面 的 遮挡层次,   但是,  上文讨论了,   计算 所有 单元面 的 遮挡层次  的 计算量 是 很大的,  所以 3D 里的 半透明 效果 是 比较 麻烦 的 。    这里 还没有 考虑 半透明 介质 在 不同角度 上 的 透明度 的 变化 呢  。    比如,  一块 玻璃,   把 它 放 斜,   相当于 厚度 增加了, 透明度 会 降低  。

    还有 折射 半反射  ,,,

     

    那么,  能不能 不 计算出 所有 单元面 的 遮挡层次 而 实现 遮挡 效果 ?    有一个 办法,  是 个 笨办法  。

    就是 每个 单元面 独立 渲染,    不需要 考虑 单元面 渲染 的 顺序,     在 输出 每个像素 的 时候, 比较 当前 位置 的 有没有 其它 单元面 已经 输出 的 像素, 如果没有, 则 直接 输出像素,   如果有,   则 比较 当前 像素 表示 的 投影点 到 单元面 的 距离 ,      这个 距离 是 投影点 沿 投影线 到 单元面 的 距离 ,     距离 大 的 表示 靠后, 距离 小 的 表示 靠前 ,   靠前 覆盖 靠后   。       也就是说,  如果 当前 像素 的 距离 小,则 覆盖 已有像素, 否则 保持 原有像素  。

     

     

    假设  位图 的 分辨率 是 1000 * 700 ,    位图 的 像素 数   1000 * 700 = 70 万 ,    那么 比较输出像素  的  次数  可能 小于 70 万,  也可能 远大于 70 万,

    如果 模型 比较小 或者 离 “镜头” 比较远,     那么,  模型 的 像(透视投影) 就  很小,    像(透视投影) 的 像素 数量 也少,   这种情况下,  所有 模型 的 所有 单元面 的  像(透视投影) 的 像素 加起来 可能 小于 70 万,  当然 也可能 大于  。

     

    这里要 提 一下 背景,

    背景 也是 一个 或 多个 模型,    是 一种 特殊 的 模型, 它 的 特点 是  在 “最底层”,  不会 遮挡 其它 模型, 只会被 其它 模型 遮挡  。

    所以,    背景   最先 被 渲染,   且 不需要 遮挡比较 计算  。

    但 问题 是,   如果  背景 由 多个 模型 组成,这些 模型 之间 也可能 存在 遮挡,  这样 还是 需要 遮挡比较 计算  。

    实际应用中 可能 存在 “固定背景” ,    就是 把 背景 预先 渲染 好,    而 人物 道具 等 实时模型 直接在 背景 渲染好的 位图 上 继续 实时渲染 ,

    这样可以  减少 计算量, 省时省力,  就好像 拍电影 在 演员 身后 用 一块 幕布 做 背景 那样,     就像 照相馆 一样 。

      

    假设  场景  有 100 个 模型,   平均 每个 模型 的  像(透视投影) 的 像素 是 10 万 ,  那么 ,  100 个 模型 的 输出像素 数量 是   100 * 10 万  = 1000 万 ,   也就是要 计算   1000 万 次 遮挡比较  。

     

    把   每次 输出像素 时 的 比较 操作 的 时间复杂度 看作 O(1) ,   那么,   对于 这 100  个 模型 的 场景,   渲染 时 计算 遮挡比较 时间复杂度 是  O( 1000 万 ),

    假设 CPU 每次 比较输出 像素 的 操作 耗时 100 纳秒(ns),    1 秒钟 可以 比较输出 像素  1000 万 次 ,

    那么, CPU   1 秒钟 可以  渲染  1000 万 / 1000 万 = 1 个       这样 的  100 个 模型 的 场景  。

     

    当然 这些 是 推演 和 估算,    但 也 大概可以看出     3D 需要 密集 的 浮点计算 以及 把 计算 独立 到 GPU 里 进行 的 原因 了 吧  ~  !

    同样 也可以看出,    分辨率  是   3D 的 一个 重要指标,         分辨率 的 增长 会 带来 显著的 计算量 增长,  随之带来 对 硬件 性能 的 要求 的 增长  。

    当然 这里 的 渲染 仅仅 是 计算 遮挡,   没有 包括 皮肤材质 、光影 等  。

     

    我们可以看看  3D 国漫 ,      比如 《画江湖 之 不良人》,     不良人 第一季 第二季 的 最高 分辨率 是  720 P   ,    直到  第三季,  才变成 了  1080 P 。

    第一季 大概是   2014 年 出的,   第三季 应该是 到了  2019 年,     从  720 P 到 1080 P ,   走了 5 年,  不容易 啊 !

     

    需要说明的是,    第 3 种 方法 (就是上面这种 每个 单元面 独立 渲染,  每个 像素 在 渲染 时 比较 到 单元面 的 距离) 不能 实现 半透明 效果 ,   实现 半透明 必须 第 2 种 方法(计算出 所有 单元面 的 遮挡层次,   按 层次 渲染)  。

    这是 因为 半透明 必须 考虑 一个 整体 的 效果,   即 在 上层 单元面 上 按比例 均匀 的 显示 下层内容 的 像素 ,    比例 就是 透明度  。

    这 需要 按 层次 渲染 和 按 单元面 呈现 半透明 效果  。

     

    上面 我们 讨论 了 模型 边界判断 透视 遮挡  ,   这 4 个 问题 是 图形学 的 基本问题,    这 4 个  问题 解决了,   皮肤 材质 光影 重力 机械运动 以及 其它 种种 问题 都是   添砖加瓦 的 工作量 问题  。

     

    于是, 我们 可以 来 总结 一些 基本 的 库函数 :

    1    求得 简单图形面(三角形面 矩形面) 在 任意 平面 上的 几何投影

    2    求得 简单图形面(三角形面 矩形面)  的 透视投影,   “摄像机” 镜头 和 简单图形面 的 距离 角度 可以 任意 设置

    3    求得 多个 简单图形面(三角形面 矩形面) 之间 有 遮挡 效果 的 透视投影

     

    以上 的 透视投影 不 包含 皮肤 材质 光影,   只 包含 简单图形面 的 边 ,     实现了 这 3 个 库函数,  实际上 这 已经 是一个 简单 的 图形引擎(3D 引擎)了 。

    这篇 文章 可以作为 图形学 的 理论基础  。

     

  • 相关阅读:
    Ubuntu 16 安装redis客户端
    crontab 参数详解
    PHP模拟登录发送闪存
    Nginx配置端口访问的网站
    Linux 增加对外开放的端口
    Linux 实用指令之查看端口开启情况
    无敌的极路由
    不同的域名可以指向同一个项目
    MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error
    Redis 创建多个端口
  • 原文地址:https://www.cnblogs.com/KSongKing/p/10961057.html
Copyright © 2011-2022 走看看