Skip to content

北工大图形学-期中后

更新: 12/13/2025 字数: 0 字 时长: 0 分钟

Animation Update

画面撕裂

众所周知,像让人眼觉得物体在动,至少需要24帧 (FPS - Frames Per Second) 。当刷新率快于 GPU 渲染时,可能会导致画面撕裂 (Screen Tearing) ,解决这个问题通常有几种方法:

  1. 垂直同步 V-Sync: 强制 GPU 等待显示器刷新,可以消除撕裂,但可能引入输入延迟。
  2. 双/三重缓冲 Double/Triple Buffering:使用多个缓冲区(framebuffer)。GPU 在后缓冲区中绘制,而显示器显示前缓冲区的内容。绘制完成后,两者交换。

如果产生掉帧 (Frame Drop) ,可以增加帧率,每秒60帧就不赖。

模拟运动

对于每帧,我们计算:

  1. 物体新的 transformations
  2. 新的光照(有时)
  3. 新的颜色/材质(偶尔)
  4. 新的几何体(很少)

我们将物体的位置 p 视为时间 t 的函数 。在每一帧,我们都会为物体计算新的变换。

  • 对于刚体(比如一个纯纯长方体)运动的模拟,我们可以简单将其运动分解为平移和旋转。比如对于一个火车头,我们仅让起往前走,然后让它可以转弯,就可以模拟一个运动。

  • 对于关节式的模型 (articulated model) :人类或动物不像火车那样是一个刚体,而是关节式的。它们由骨骼(Bones)和连接骨骼的关节(Joints)组成。

树状结构

在图形学中,骨骼是指模型上一个可以被单独变换的部分,每个骨骼都有自己的局部坐标系;关节是连接两个骨骼的点,允许骨骼相对于彼此旋转或移动,关节的自由度 Freedom 决定了关节的可以转动的角度。

当我们尝试去绘制一个人的时候,我们会先定一个根物体,然后在根物体上不断添加子物体,让根物体带动子物体运动,以方便实现一个部位带动另一个部位的效果。举个例子,根物体是人的盆骨,它有子物体躯干,躯干有子物体大臂,大臂有子物体小臂,如此类推,当躯干移动的时候,子物体手臂会自然地跟着动起来。

坐标系变换链 Transformation Chain

既然我们已经知道人体的树状结构,那实际上我们该如何去实现呢?

我们让每个部位都在它父节点的局部坐标系 (Local Coordinate System) 中定义。

比方说,我们要去绘制左手:

  1. 左手定义在前臂 (Forearm) 的坐标系里。
  2. 前臂定义在上臂 (Upper Arm) 的坐标系里。
  3. 上臂定义在躯干 (Chest) 的坐标系里。
  4. 躯干定义在骨盆 (Pelvis) 的坐标系里。
  5. 骨盆定义在世界 (World) 坐标系里。

那么要画出左手在世界中的位置,我们就需要把不同部位变换矩阵连乘起来:

Mworld=MpelvisMchestMupperArmMforearmMhand

在代码中实现我们需要用到栈 Stack 后进先出 (LIFO) 的特性来保存和恢复变换矩阵。例如:

  • glPushMatrix(): 备份当前状态
  • glPopMatrix(): 恢复之前的状态

其实这本身也是一个深度优先搜索 DFS 的过程:

kotlin
// 遍历树状结构
fun drawNode(node: Node) {
    glPushMatrix() // 备份当前矩阵
    glMultMatrix(node.transform) // 应用当前节点的变换
    drawGeometry(node.geometry) // 绘制当前节点的几何体

    // 递归绘制子节点
    for (child in node.children) {
        drawNode(child) 
    }
    glPopMatrix() // 恢复之前的矩阵
}

姿态生成 Pose Generation

  1. 正向运动学 Forward Kinematics, FK:

    • 给定每个关节的角度,计算末端执行器(如手或脚)的位置。
    • 但你很多时候很难确定每个关节要旋转多少,才能让手到达某个位置。
  2. 逆向运动学 Inverse Kinematics, IK:

    • 确定目标位置(End Effector),通过算法(Solver)反推一连串部位(Joint Chains)的旋转角度。
    • 性能开销大于 FK。
  3. 动作捕捉 Motion Capture, Mocap

    • 只能动捕人或动物。
    • 贵。
  4. 物理模拟 Physics Simulation:

    • 设置好重力、摩擦力、质量,让物理引擎去算。
    • 很难调整出精细的动作。
  5. 关键帧插值 Keyframe Interpolation:

    • 只做出几个关键帧,通过计算机计算出中间过渡。
    • 需要好的动画师。

Physical & Biology Update

光在现实世界满足波粒二象性,其波长会影响我们对其颜色的感知,在图形学中我们把光看作是延直线传播的粒子流。

人眼的视网膜上主要有两种细胞帮我们形成图像:

  • 视杆细胞 Rods:大约1.2亿个,对光非常敏感,但只能感知亮度(画面黑白),且分辨率低。
  • 视锥细胞 Cones:大约600万个,负责颜色感知和更细致的画面。

人的视锥细胞只有三中,分别对红 Red、绿 Green、蓝 Blue 三种光波长最敏感,所以为了模拟现实世界的颜色,只要混合不同比例的 R、G、B 三种光,大脑就会以为自己看到了某种特定的颜色。

光在视网膜上的强度是一个连续函数 I(x,y),计算机是离散机器,无法处理无限精度的函数。因此,为了模拟现实中的光强,我们对屏幕上的每个像素,计算对应区域里光照的积分:

Pij=...I(x,y)dy dx

像素 Pij 是该区域内所有光强度的总和。

这里介绍三种渲染图像的方式:

  1. 光线追踪 Ray Tracing:从摄像机向场景中的每个像素发射光线,追踪这些光线与物体的碰撞、反射和折射。

    • 能够自然地处理阴影、反射、透明度和全局光照 Global Illumination
    • 通常使用递归算法来计算光线的多次反弹
    • 计算成本昂贵,速度慢
  2. 投影渲染 Projective Rendering / Rasterization:将 3D 物体表示为三角形网格 Mesh of triangles。

    • 使用矩阵变换将 3D 坐标直接投影到 2D 屏幕坐标上
    • 通过插值填充三角形内部的像素颜色(光栅化)
    • 通常使用 Phong 或 Gouraud 等局部光照模型,速度快,但难以反射和全局光影
  3. 基于图像的渲染 Image-Based Rendering, IBR:不使用 3D 模型,而是通过拼接和插值现有的照片来生成新视角。

    • 逼真,因为源头就是照片
    • 像是 Google 街景 (Street View)、VR 全景看房
    • 静态,但很难移动物体或改变光照,交互性差

Lighting & Shadows

Phong 光照模型

Phong 模型认为,物体表面某一点的总光照强度 Itotal 是由四个独立的分量叠加而成的:

  • Ispecular:镜面反射 Specular Reflection,光照到光滑表面会产生聚光的亮点。
  • Idiffuse:漫反射 Diffuse Reflection,光照到粗糙表面会均匀散射。
  • Iambient:环境光 Ambient 模拟背景光或间接光照。
  • Iemitted:自发光 Emitted 光源自身发出的光。
Itotal=Ispecular+Idiffuse+Iambient+Iemitted

为了计算光照,需要定义三个关键的方向向量:

  • 法向量 n:物体表面的法向量,用于确定表面的朝向
  • 光线向量 vl:从物体表面一点指向光源的方向向量 lp
  • 视线向量 ve:从物体表面一点指向眼睛/相机的方向向量 ep

接下来,介绍一下各个光强分量 I 的计算方法。

  1. 镜面反射 Specular Reflection:

    • 模拟光滑表面的高光,光线照射到表面后会主要向一个方向反射。

    计算:

    • 先计算光线向量和视线向量的中间向量,该向量称为半程向量/角平分线向量 vb
    • 半程向量与表面法线 n 的夹角越小,高光越强。
    • 公式:
      • 其中 ns 是一个你可以自定义的指数项,该数值越高,高光点越小越闪亮,你可以对不同材质的表面设置不同的高光指数 ns
      • k 系数是光源强度 l 与 物体表面反射率/反照率 r 的乘积。
    Ispecular(p)=kspecular(nvb||n||||vb||)ns
  2. 漫反射 Diffuse Reflection:

    • 光线照到粗糙表面上,其凹凸不平的表面导致光线向四面八方均匀地散射。与镜面反射不同,漫反射的亮度与视线角度无关,只取决于光线入射角。

    • 公式,其结果大小与法线 n 与光线向量 vl 的夹角余弦值有关。光线越垂直于表面,表面越亮:

      Idiffuse(p)=kdiffusecosθl=kdiffusenvl||n||||vl||
  3. 环境光 Ambient Lighting:

    环境光是为了模拟那些在环境中反弹多次、方向难以确定的杂散光子。通常为了简化计算,Iambient 会直接设定为一个常数,用来确保阴影区域不会变成纯黑。

  4. 自发光 Emitted Light:

    用来模拟那些本身就会发光的物体,比如电灯泡之类的。此类物体会向周围均匀发光,且在渲染时忽略其它外界入射光的影响。

将上述四个分量带入到最初的公式,整个公式会变成这样,注意到有多个不同的 lr,本质是图形学中是为了简能够渲染出特殊效果和简化计算而将光源强度拆成了多个分量:

Itotal(p)=Ispecular(p)+Idiffuse(p)+Iambient(p)+Iemitted(p)=lspecularrspecular(nvb||n||||vb||)ns+ldiffuserdiffusenvl||n||||vl||+lambientrambient+kemitted

TIP

一个表面当然可以同时有镜面反射和漫反射,毕竟现实不存在绝对光滑的平面。

阴影生成

阴影是位于掩体后的光的缺失 absence of light。

计算阴影我们需要处理两种情况:

  1. 背光面
  2. 被遮挡面,此类阴影称为投影阴影

背光面比较好处理,我们首先需要一种方法来判断一个面是否是背光面。我们可以通过计算光线向量 vl 和表面法线 n 的点积,如果 nv 为负值,说明表面法线与光线方向夹角大于 90 度,表面背向光源,只显示环境光和自发光。

这里介绍两种实现投影阴影的方法:

  1. 阴影贴图 Shadow Mapping

    • 从光源的视角渲染场景,记录光源能看到的最近的物体的距离,生成深度图 Depth Map。
    • 在摄像机渲染图像时,计算每个像素点到光源的距离。
    • 利用光源视角中深度图中的值和像素点到光源的距离比较,判断该像素点之前是否还有其它物体离光源更近,自身是否被遮挡。
  2. 光线追踪 Raytracing

    • 当视线射线击中物体表面的某一点时,生成一条指向光源的新射线,称之为阴影射线 Shadow Ray。
    • 检测阴影射线是否击中了其他任何物体,击中则说明原点在阴影中。

Color Update

颜色模型与生成方式

颜色模型 Color Models

其实先前介绍的 RGB 颜色模型具有一定局限性,由于视锥细胞对红绿蓝三种颜色的敏感度不同,RGB 模型并不能完美模拟出所有自然界人眼可见的颜色。为了解决这个问题,国际照明委员会 (CIE) 定义了一个更科学的标准:CIE XYZ 色彩空间(XYZ Colour Space)。

这是一个涵盖了所有可见光颜色的坐标系统,所有坐标值都是正数。在 XYZ 色度图中,RGB 所涵盖的颜色范围只能表现为一个三角形区域,一般会形象地叫做 RGB 马蹄图。

Limb 注

然而,由于目前的显示器基于 RGB 模型进行颜色显示的限制,其仍只能显示其 RGB 色域内的颜色。现代显示器会通过色域映射和 HDR 技术让颜色表现尽可能丰富,观感更接近真实世界。

除了 RGB,还有一些在特定场景下非常好用的颜色模型:

  • YUV 模型

    • 为了兼容黑白电视信号。
    • 将亮度 Y 与色度 U, V 分离。彩色电视信号会保留 Y 信号供给黑白电视使用。
  • HSV 模型 Hue, Saturation, Value

    • 基于色轮 Colour Wheel,专门用于调色,比如 PS 里的调色板就是基于 HSV 模型。
    • H:色相,用角度表示
    • S:饱和度
    • V:亮度

颜色生成

对于电子屏幕和打印机来说,颜色生成方式也会有所不同:

  • 加色模式 Additive Colour:通过不同颜色光的叠加,是电子屏幕、舞台灯光显示颜色的方式。例如,红光和绿光叠加会产生黄色光。
  • 减色模式 Subtractive Colour:颜料通过吸收/减去特定波长的光来显示颜色,是打印机、绘画使用的方式。例如,叶子吸收了红色和蓝色的光,只反射了绿色光,所以显绿色。

同时,为了在白纸上打印出颜色,会使用 CMYK 模型(青色 Cyan、品红 Magenta、黄色 Yellow、黑色 Key/Black)而非 RGB 模型。通过混合不同的 CMY 颜料,可以吸收掉不同波长的光,从而显示出各种颜色。

此外,如果一束彩光照射到有颜色的物体上时,需要对 R, G, B 三个通道分别进行计算,最终颜色是每个通道光强的乘积。假设光的颜色是 Clight=(R,G,B),物体的颜色是 Cobj=(r,g,b),那么最终颜色 Cfinal 计算如下:

Cfinal=(Rr,Gg,Bb)

输入与输出设备

输入设备:捕捉光线

一切输入设备皆为传感器 Sensors:光子携带能量(量子),撞击传感器时能量被转移,传感器利用光敏化学物质吸收光线并测量单元区域内接收到的总光强。

常见的捕捉方式:

  1. 胶片摄影(Film):使用碘化银晶体。
  2. 数码摄影(Digital/CCDs):使用 CCD。
  3. 非可见光成像:红外线、X射线等虽人眼不可见,但可以被特定传感器捕捉。
  4. 主动探测 (Radar/Lidar):先发射光再接收反射。

输出设备:显示图像

  1. 反射型/减色模式 Reflective, Subtractive:利用减色原理(CMYK),分辨率通常很高
  2. 发射型/加色模式 Emissive, Additive:电子枪发射电子撞击屏幕玻璃上的磷光体,使其发光;或 LED/OLED 自发光。
  3. 偏振型/加色模式 Polarizing, Additive:背光是偏振光 Polarized Light,前方的液晶在通电时会改变偏振方向,从而控制光线通过与否,形成图像。这是液晶屏的原理。

色域 Gamut 指一个设备能显示的所有颜色的集合。不同设备的色域不同,不同传感器对频率的敏感度也不同,所以在一个设备出厂前,通常会进行色彩校准 Calibration,以确保颜色的准确性。

Frame Buffer

帧缓冲区是显卡上的一块特殊内存区域,用于存储图像数据。除了我们熟悉的双缓冲和三重缓冲外,还有一种用于用于立体成像的四重缓冲 Quad Buffering,其为左眼和右眼画面各分配两个缓冲区,常用于 VR。

帧缓冲区不仅仅存颜色,它包含多个不同用途的子缓冲区:

  1. 颜色缓冲区 Color Buffer:存储 RGBA 颜色信息。
  2. 深度缓冲区 Depth Buffer:存储每个像素的 Z-depth,用于判断物体的前后遮挡关系。
  3. 模版缓冲区 Stencil Buffer:用于遮罩 Masking,标记了哪些像素是开启或关闭的,决定是否在该位置进行绘制。
  4. 累积缓冲区 Accumulation Buffer:用于存储多帧图像的累积结果,用于实现运动模糊、景深、软阴影等效果。

这两者都依赖于帧缓冲区的细节:

  • Blending:指结合几何对象(例如处理透明物体)
  • Compositing:指结合整个图像(如累积渲染结果)

在 OpenGL 中,你经常需要控制这些缓冲区:

  • 设置读写目标:
    • glDrawBuffer():决定画到哪个缓冲区,默认是 GL_BACK(后缓冲区)
    • glReadBuffer():决定从哪个缓冲区读取数据
  • 清除缓冲区:
    • glClear():非常耗时,通常每帧只做一次
    • 它将所有像素重置为 glClearColor() 指定的颜色
  • 遮罩:
    • 你可以暂时锁住某个缓冲区,禁止其写入。
    • 例如 glDepthMask(GL_FALSE) 可以关闭深度写入

当所有的几何体都被光栅化变成屏幕上的一个个潜在像素点后,必须经过一系列测试才能显示在屏幕上,这些潜在的像素点被称为片元 Fragments。

测试顺序 Pipeline:

  1. 裁剪测试 Scissor Test:定义屏幕上一个矩形区域,只有在这个矩形内的片元会被保留。
  2. Alpha 测试:检查片元的 Alpha 值透明度。
  3. 模版测试 Stencil Test:将片元位置与“模版缓冲区”中的值进行比对,这是一个非常灵活的遮罩系统,你可以设定只在模版值为 1 的地方绘制。
  4. 深度测试 Depth Test:检查片元的深度值,决定是否覆盖已有像素。

接下来讲讲累积缓冲区的妙用:

  • 抗锯齿 Anti-Aliasing

    • 像素是方块,斜线会有锯齿 (Jaggies)。
    • 解法有很多,ppt 里介绍了 FSAA 和 MSAA:渲染比屏幕分辨率更多的像素,然后通过取平均值的方式将其压缩到实际分辨率更小的屏幕。
    • 也可以通过累计抖动 Accumulation Jittering:渲染画面多次,每次将摄像机微小移动,然后将渲染结果混合起来。这能让边缘变柔和。
  • 混合与透明度 Blending & Translucency

    • 当一个新的像素要画在已经存在的像素上面时,我们不直接覆盖,而是将两者颜色混合。
    • Alpha 通道:颜色中的 A 分量通常代表不透明度 Opacity。
    FinalColor=(Alpha×NewColor)+((1Alpha)×OldColor)
    • 渲染顺序:为了保证混合正确,必须先画不透明物体,再画半透明物体。
  • 运动模糊 Motion Blur

    • 如果物体动得太快,人眼会看到模糊的轨迹。
    • 通过渲染多帧,每帧之间物体稍微移动一点,然后把这些帧叠加在一起,实现运动模糊。
  • 景深 Depth of Field

    • 就像是相机对焦一样,只有焦点附近的物体是清晰的,远离焦点的物体会变得模糊。
    • 保持焦点平面不动,微微抖动摄像机的位置,将摄像机多次渲染结果叠加。焦点处的物体仍会清晰,而远处的物体会变得模糊。
  • 软阴影 Soft Shadows

    • 真实世界的阴影边缘通常是模糊的,因为光源不是点光源,而是有一定面积的。
    • 通过对光源位置进行微小抖动,多次渲染阴影,然后将结果叠加,实现软阴影效果。
  • 雾 Fog

    • 利用深度缓冲区的值,让远处的物体颜色逐渐混合成雾的颜色(通常是灰色或白色),模拟大气散射。
  • 多边形偏移 Polygon Offset

    • 当你尝试在球的表面绘制线框时,由于线的深度值与球面非常接近,而深度缓冲精度有限,可能会出现深度冲突导致线条闪烁。
    • 在 OpenGL 中,可以使用 glPolygonOffset() 函数为多边形的深度值添加一个偏移量,从而避免这种冲突。

Curves & Circles

画圆

在图形学中,我们经常用连续性来衡量一条线是否平滑,有两种描述连续性的方式:

  • C0 连续 Position Continuous:曲线在某点是连在一起的,没有断开(ex:折线)。
  • C1 连续 Tangent Continuous:不仅点连在一起,而且在该点两侧的导数/斜率也是相同的,此类曲线过渡光滑,没有尖角。

这里介绍圆的三种表示方法,假设假设圆心为 q(qx,qy),半径为 r

  1. 隐式形式 Implicit Form

    • 公式: (xqx)2+(yqy)2=r2
    • 向量形式: (pq)(pq)=r2
    • 适合用于判断点是否在圆上或圆内,但不适合用来绘制:遍历 xrr 绘制容易导致线条不连续。
  2. 显式形式 Explicit Form

    • 公式: y=qy±r2(xqx)2
    • y 表示为 x 的函数,更直观。
    • 开根计算耗时,且在圆的左右边缘斜率无穷大,不易处理。
  3. 参数形式 Parametric Form

    • 公式: P(t)=(qx+rsint,qy+rcost)0t2π
    • 速度慢,因为 sincos 运算开销大。

最适合绘制圆的方法是直线逼近 Line Approximation:将圆看作由 N 条直线段组成。计算 N 个顶点,用直线连接它们。

线与圆的求交

已知条件:

  • 线方程: p=s+vt
  • 圆方程: (pq)(pq)=r2

联立,得到一个二次方程:

t2(vv)+2t(v(sq))+((sq)(sq)r2)=0

其形式形似于 at2+bt+c=0,我们只需求 Δ,即可计算出交点个数:

  • Δ<0:无交点
  • Δ=0:一个交点(切线)
  • Δ>0:两个交点

贝塞尔曲线 Bézier Curves

基本概念

如果我们想画一条任意形状的平滑曲线,比如汽车的外形、字体的轮廓,光靠圆是不够用的。我们需要一种更通用的数学方式描述平滑曲线,这就是贝塞尔曲线的由来。

贝塞尔曲线的构建完全基于一个简单的概念:线性插值 (Lerp)

给定两个点 p0p1,以及参数 t (0t1),线段上的一点可以表示为:

p(t)=(1t)p0+tp1

如果我们在两点之间插值,就能得到一条直线;如果我们对插值得到的点再进行插值并不断重复,就能得到曲线。

德卡斯特里奥算法 De Casteljau Algorithm

德卡斯特里奥算法描述了如何通过递归插值生成贝塞尔曲线。

想象你有三个控制点 P0,P1,P2,我们定义一个比例参数 t,然后进行如下插值:

  1. P0P1 之间找到点 A(比例为 t
  2. P1P2 之间找到点 B(比例为 t
  3. 连接 AB,在这条连线上再找到点 C(比例为 t
  4. t 从 0 变化到 1 时,点 C 的位置会同时受到点 AB 位置改变和 t 大小改变所带来的影响,点 C 最终的移动轨迹会形成一条二次贝塞尔曲线。

代数推导:

P(t)=(1t)2P0+2t(1t)P1+t2P2
scala
def bezierQuadratic(P0: Point, P1: Point, P2: Point, t: Float): Point =
    (1 - t) * (1 - t) * P0 +
    2 * t * (1 - t) * P1 +
    t * t * P2

for t from 0 to 1 step 0.01:
    val point = bezierQuadratic(P0, P1, P2, t)
    drawPoint(point)

三次贝塞尔曲线 Cubic Bézier Curves

这是应用最广泛的形式,由4个控制点定义:

  • P1,P4:端点。曲线一定会穿过这两个点。
  • P2,P3:控制点。这两个点分别和 P1,P4 的连线决定曲线端点处的切线方向。它们不会被曲线穿过。

凸包性质 Convex Hull Property

指整条贝塞尔曲线一定完全包含在由4个控制点构成的凸多边形内部。

分段贝塞尔曲线 Piecewise Béziers

一条贝塞尔曲线通常不足以描述复杂的形状,我们通常需要把多条曲线首尾相连,而这就引出了新连续性的问题:如何实现光滑连接(C1 连续)两条贝塞尔曲线?

为了解决这个问题,我们需要使衔接的两条曲线斜率匹配 slopes to match:对于三次贝塞尔曲线,使第一条曲线的最后两个控制点(P3,P4)和第二条曲线的前两个控制点(Q1,Q2,其中 Q1=P4),这三个点必须在一条直线上,即共线 Collinear。

高级曲线

Hermite 曲线

这是一种在工程和绘图软件中常用的曲线表示法。与三次贝塞尔曲线需要4个控制点不同,Hermite 曲线也是三次多项式,但它是由以下4个参数定义的:

  • 2个端点 P1,P4:曲线的起点和终点。
  • 2个切线向量 R1,R4:曲线在起点和终点的斜率。

Hermite 曲线与贝塞尔曲线在数学上是等价的,可以互相转换。

B-样条 B-Splines

贝塞尔曲线有一个问题,牵一发而动全身:移动一个控制点,整条曲线都会变。B-样条解决了这个问题,它提供了更好的局部控制性。

你只需要给出一长串控制点序列,算法会每次抓取4个相邻的点来生成一段曲线(样条),然后将这些样条首尾相连,形成一条完整的曲线。这个特性也令B-样条组成的曲线天然具有至少 C1 的连续性,并且非常容易拼接和微调。

TIP

B-样条的控制点被称为节点 Knots。

结束!

更新: 12/13/2025 字数: 0 字 时长: 0 分钟