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 引擎)了 。

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

     

  • 相关阅读:
    Delphi 的字符及字符串[4] 字符串、字符指针与字符数组
    Delphi 的字符及字符串[5] 字符串与 Windows API
    WinAPI: FindWindow、FindWindowEx 查找窗口
    java LookAndFeel 美化 Substance使用
    持久化和对象关系映射ORM技术
    java 更换皮肤问题Cannot refer to a nonfinal variable inside an inner class defined in a different method
    java Swing可视化开发工具
    php ORM 持久层框架与简单代码实现
    生成Substance皮肤Menu项的代码
    三层架构实现
  • 原文地址:https://www.cnblogs.com/KSongKing/p/10961057.html
Copyright © 2011-2022 走看看