第一章 绪论
计算机图形学是利用计算机研究图形的表示、生成、处理和显示的学科。
光栅图形学:其核心过程是光栅化,即将矢量图形转换为像素网格的过程。
光栅就是通过一栏一栏的像素绘成图像
1.1 计算机图形学的研究内容
计算机中图形的表示方法,以及利用计算机进行图形的计算、处理和显示的相关原理与算法,构成了计算机图形学的主要研究内容。图形通常由点、线、面、体等几何元素和灰度色彩、线型、线宽等非几何属性组成。从处理技术上来看,图形主要分为两类:一类基于线条信息表示
,如工程图、等高线地图和曲面的线框图等;另一类是明暗图(shading)
,也就是通常所说的真实感图形。
计算机图形学的一个主要目的就是要利用计算机产生令人赏心悦目的真实感图形。
为此,一般先建立目标图形所描述场景的几何表示
,再采用某种光照模型
,计算在假想的光源.纹理、材质属性下几何模型的光照效果。
计算机图形学的研究内容非常广泛
,如图形硬件、图形标准、图形交互技术、光栅图形生成算法、曲线曲面造型、实体造型、真实感图形计算与显示算法,以及科学计算可视化、计算机动画、自然景物仿真和虚拟现实等。作为一本面向计算机专业的本科生和非计算机专业的研究生的图形学教材,本书着重讨论与光栅图形生成、曲线曲面造型和真实感图形生成相关的原理与算法。
1.2 计算机图形学发展历程
1950年
,第一台图形显示器作为美国麻省理工学院(MIT)旋风1号(WhirlwindⅠ)计算机的附件诞生了。该显示器用一个类似于示波器的阴极射线管(CRT)来显示一些简单的
1962年
,MIT林肯实验室的 IvanE.Sutherland
发表了题为“Sketchpad:一个人机交互通信的图形系统”的博士论文,他在论文中首次使用了计算机图形学(ComputerGraphics)这个术语,证明了交互计算机图形学是一个有价值的研究领域,从而确定了计算机图形学作为一个崭新的科学分支的独立地位。
同在20世纪60年代早期
,法国雷诺汽车公司的工程师 Pierre Bézier发展了一套被后人称为 Bézier 曲线、曲面的理论,成功地用于几何外形设计,并开发了用于汽车外形设计的 UNISURF 系统。Coons的方法和 Bézier的方法是 CAGD(计算机辅助几何设计)领域的开创性工作。值得一提的是,计算机图形学的最高奖是以 Coons 的名字命名的
20世纪70年代是计算机图形学发展过程中一个重要的历史时期
。由于光栅显示器的诞生,早在60年代就已萌芽的光栅图形学算法便迅速发展起来。区域填充、裁剪、消隐等基本图形概念及其相应的算法纷纷诞生,图形学进人了第一个兴盛时期。
同在20世纪70年代
,计算机图形学的另外两个重要进展是真实感图形学和实体造型技术的产生。1970年,Bouknight 提出了第一个光反射模型。1971年,Gourand 提出“漫反射模型十插值”的思想,被称为 Gourand 明暗处理。1975年,Phong 提出了著名的简单光照模型–Phong模型”。这些都是真实感图形学的开创性工作。1980 年
,Whitted 提出了一个光透视模型--Whitted 模型
,并第一次给出光线跟踪算法的范例,实现了 Whitted模型。1984年,美国 Cornell(康内尔)大学和日本广岛大学的学者分别将热辐射工程中的辐射度方法引人到计算机图形学中,成功地模拟了理想漫反射
1.3 计算机图形学的应用及研究前沿
- 计算机辅助设计与制造
- 可视化
- 真实感图形实时绘制与自然景物仿真
- 计算机动画
- 用户接口
- 计算机艺术
1.4 图形设备
高质量的计算机图形离不开高性能的计算机图形硬件设备。一个图形系统通常由图形处理器、图形输入设备和输出设备构成。
图形显示设备
彩色 CRT 显示器
图 1.7给出了CRT的工作原理。
高速的电子東由电子枪发出,经过聚焦系统、加速系统和磁偏转系统就会到达荧光屏的特定位置
。荧光物质在高速电子的轰击下会发生电子跃迁,即电子吸收能量从低能态变为高能态,由于高能态很不稳定,在很短的时间内荧光物质的电子会从高能态重新回到低能态,这时将发出荧光,屏幕上的那一点就亮了
。从这种发光原理可以看出,这样的光不会持续很久,因为很快所有的电子都将回到低能态,不会再有光发出。所以要保持显示一幅稳定的画面,必须不断地发射电子束。 那么电子東是如何发出的,又是如何控制它的强弱的呢?由图1.7可以看出,电子枪由一个加热器、一个金属阴极和一个电平控制器组成。当加热器加到一定高温时,金属阴极上的电子就会摆脱能垒的束缚,进射出去。而电平控制器是用来控制电子束强弱的,当加上正电压时,电子柬就会大量通过,将会在屏幕上形成较亮的点;当控制电平加上负电压时,依据所加电压的大小,电子束被部分或全部阻截,通过的电子很少,屏幕上的点也就比较暗。
想要保持稳定图像,那么就需要不断地发射电子束。
刷新一次是指电子束从上到下将荧光屏扫描一次
。只有刷新频率达到一定值后,图像才能稳定显示。大约达到60Hz时,人眼才能感觉不到屏幕闪烁,但要使人眼觉得舒服,一般必须有85Hz以上的刷新频率。
隔行扫描
:能够满足扫描速度较慢的显示机器,区别于传统的逐行扫描,隔行扫描能够模拟将扫描频率加倍,但是效果比不上真正的逐行60Hz
LCD 液晶显示器
- 基本原理
液晶是一种介于液体和固体之间的特殊物质,它具有液体的流态性质和固体的光学性质。当液晶受到电压的影响时,就会改变它的物理性质而发生形变,此时通过它的光的折射角度就会发生变化,而产生色彩。液晶屏幕后面有一个背光,这个光源先穿过第一层偏光板,再来到液晶体上,而当光线透过液晶体时,就会产生光线的色泽改变。从液晶体射出来的光线,还必须经过一块彩色滤光片以及第二块偏光板。由于两块偏光板的偏振方向成90°,再加上电压的变化和一些其他的装置,液晶显示器就能显示想要的颜色了。
图形处理设备
一个光栅显示系统离不开图形处理器,图形处理器是图形系统结构的重要元件,是连接计算机和显示终端的纽带,可以说有显示系统就有图形处理器(俗称显卡)。
图形输入设备
设备名称 | 用途描述 |
---|---|
键盘 | 用于录入文本、发布命令和选择菜单项等。 |
鼠标 | 通过移动和点击来控制光标位置和进行操作。 |
光笔 | 通过光电技术检测屏幕上的位置,用于图形绘制和标记。 |
数字化仪 | 用于绘画着色或交互式选择坐标位置,可输入二维或三维空间的坐标值。 |
触摸板 | 通过手指触摸来控制光标移动和进行操作。 |
图形扫描仪 | 将纸质文档或图像扫描成数字格式,以便计算机进行处理和存储。 |
手写输入板 | 用于手写输入,将手写笔迹转换为数字信号。 |
语音输入设备 | 通过语音识别技术将语音转换为文本或命令。 |
数据手套 | 用于动作捕捉,让计算机识别手部运动,常用于虚拟现实系统。 |
操纵杆 | 用于游戏或模拟操作,通过物理按钮和操纵杆与计算机交互。 |
按钮盒和旋钮 | 按钮和开关常用来输入预定的功能,旋钮用于输入标量值。 |
三维扫描仪 | 用于捕捉物体的三维形状和深度信息,常用于3D建模和仿真。 |
深度相机 | 用于捕捉物体的深度信息,提供三维感知能力。 |
光穹 | 一种高精度的三维扫描设备,用于在光学条件下捕捉物体的完整三维信息。 |
1.5 最新研究方向
绘制:
- 表面几何细节实时绘制
- 半透明材质实时编辑
- 动态场景实时绘制
视频:
- 视频补全
几何:
- 拓扑结构自动修复
- 积分不变量技术
第二章 二维变换
2.1 向量
什么是向量? 我们所使用的所有点和向量都是基于某一坐标系定义的。
从几何的角度看,向量是具有长度和方向的实体
,但是没有位置。而点是只有位置,没有长度和方向
向量表示一个点到另一个点的位移。假设在XoY直角坐标系中,一个向量可以由它的不同方向的分量表示。
向量的基本操作
- 向量
相加减
: 向量(2, 6) + 向量(3, 1) = (5, 7) - 标量的
数乘
: 3 * 向量(1,4) = (3, 12)
向量线性组合
:掌握了向量的加法和数乘,就可以定义任意多个向量的线性组合
- 有两种特殊的线性组合在计算机图形学中很重要
- 仿射组合:
线性组合的系数之和=1
,a1 + a2 + a3 + … + an = 1 凸组合
:在仿射组合的基础上,还要求每个系数>0
- 仿射组合:
- 点乘和叉乘
代数公式和几何公式实际上是等价的
,它们从不同的角度描述了同一个数学概念。
余弦定理:用一个向量来描述一篇新闻,
当夹角的余弦接近于1时,两条新闻相似,从而可以归成一类
;夹角的余弦越小,两条新闻越不相关
叉乘: 两个向量的叉积是另一个三维向量
。叉积只对三维向量有意义。它有许多有用的属性,但最常用的一个是它与原来的两个向量都正交
。 经常利用这个属性来求平面的法向量
。
2.2 坐标系
什么是坐标系? 坐标系是建立图形与数之间对应联系的参考系
坐标系的分类
- 从维度上来看,分为一维、二维、三维等
- 从坐标轴之间的空间关系,可分为直角坐标系、极坐标系、圆柱坐标系、球坐标系等
在对一个事物进行建模(使用数学几何信息来描述物体)
和观察(viewing)
的过程中,并不总是使用同一个坐标系来考虑。图形显示的过程就是几何(对象)模型在不同坐标系之间的映射变换
计算机图形学中坐标系的分类
世界坐标系
:世界坐标系是计算机图形学中用于定义整个虚拟世界空间的全局坐标系。它是所有物体和场景的共同参考框架
,所有物体的位置、方向和大小等属性都是相对于世界坐标系来定义和计算的。
建模坐标系
:又称为局部坐标系。每个物体(对象)有它自己的局部中心和坐标系
观察坐标系
:观察坐标系是以观察者(如相机、视点等)为中心的坐标系。
它定义了观察者的位置、观察方向以及观察区域的范围和形状,用于将世界坐标系中的物体转换到观察者视角下进行投影和显示。
设备坐标系
:适合特定输出设备输出对象的坐标系。
比如屏幕坐标系,在多数情况下,对于每一个具体的显示设备,都有一个单独的坐标系统- 定义图形在特定输出设备上的显示位置和大小,每个坐标对应设备上的一个像素。
- 与具体设备的分辨率相关,坐标范围由设备的分辨率决定。
- 在定义了显示窗口的情况下,可在设备坐标系中进一步定义称为视区的有限区域,视区中的成像即为实际所观察到的。
注意:设备坐标是整数
规范化坐标系
:规范化坐标系是一种与设备无关的坐标系统,用于图形处理过程中,确保输出的图形在任何设备上都能保持一致。通常用于将观察坐标系中的坐标转换为设备坐标系之前的中间步骤。其坐标范围一般从-1到1或者0到1
2.3 二维图形变换
一个简单的图形,通过各种变换(如:比例、旋转、镜象 、错切、平移等)
可以形成一个丰富多彩的图形或图案
在计算机动画中,经常有几个物体之间的相对运动,可以通过平移和旋转这些物体的局部坐标系得到这种动画效果
图形变换的基本原理
- 图形变化了,但原图形的
连边规则没有改变
- 图形的变化,是因为
顶点位置的改变决定的
变换图形就是要变换图形的几何关系,即改变顶点的坐标;同时,保持图形的原拓扑关系不变
仿射变换:是一种二维坐标到二维坐标之间的线性变换
- 平直性:直线经过变换之后依然是直线
- 平行性:平行线依然是平行线,且直线上 点的位置顺序不变)
2.4 齐次坐标
向量(x, y)经过变换后的点坐标为(x’, y’),这个变换过程可以写成如下矩阵形式
这种用三维向量表示二维向量,或者一般而言,用一个n+1维的向量表示一个n维向量的方法称为齐次坐标表示法
2.5 平移变换
2.6 比例变换
2.7 对称变换
2.8 旋转变换
2.9 错切变换
2.10 复合变换
复合变换是指图形作一次以上的几何变换,变换结果是每次的变换矩阵相乘。从另一方面看,任何一个复杂的几何变换都可以看作基本几何变换的组合形式。
2.11 窗口、视区及变换
世界坐标系中要显示的区域(通常在观察坐标系内定义)称为窗口
。窗口映射到显示器(设备)上的区域称为视区
窗口就是假定你的前方有一个能够透过光的玻璃,按物理学来说你能看到的区域。而视区就是将你能看到的事物映射到二维屏幕上。
如何将窗口内的图形在视区中显示出来呢? 必须经过将窗口到视区的变换处理,这种变换就是观察变换(Viewing Transformation)
2.12 最重要的
最重要的就是这个变换矩阵的每个元素会对二维图形造成什么影响
第三章 光栅图形学
光栅图形显示器可以看做一个像素的矩阵
。在光栅显示器上显示的任何一种图形,实际上都是一些具有一种或多种颜色的像素集合
。
首先介绍几个重要的概念:
图形的光栅化:
确定最佳逼近图形的像素点集合,并使用指定属性写像素的过程
裁剪:
任何图形进行光栅化时,必须显示在屏幕的一个窗口里,超出窗口的图形部分不予显示。确定一个图形的哪些部分在窗口内,必须显示;哪些部分落在窗口之外,不该显示的过程
走样和反走样:
对图形进行光栅化时,由于显示器的空间分辨率有限,对于非水平垂直、±45°的直线
,因像素逼近误差,使所画图形产生畸变(台阶、锯齿)的现象称之为走样(aliasing)。用于减少或消除走样的技术称为反走样(antialiasing)。
消隐:
在真实感图形绘制过程中,由于投影变换失去了深度信息,会导致图形的二义性。消隐就是为了消除这类二义性,在绘制时消除被遮挡的不可见的线或面,以得到物体的真实图形。
3.1 直线段的扫描转换算法
在数学上,理想的直线是没有宽度的,它是由无数个点构成的集合。对直线进行光栅化时,只能在显示器所给定的有限个像素组成的矩阵中,确定最佳逼近于该直线的一组像素,并且按扫描线顺序。
DDA数值微分算法
DDA算法基于直线的微分方程,DDA 算法基于直线的微分方程 $d y = k dx$ 和 $d x = \frac{1}{k} d y$,其中 $ k $ 是直线的斜率。通过选择最大位移方向作为步长方向,每次在该方向上移动一个单位长度,并根据斜率计算出另一个方向的增量,从而确定下一个像素点的位置。
- 确定直线方程和增量
- 根据给定的起点 $ P_0(x_0, y_0) $ 和终点 $ P_1(x_1, y_1) $,计算直线的斜率 $ k = \frac{y_1 - y_0}{x_1 - x_0} $。
- 选择在 $ x $ 或 $ y $ 方向作为步长方向,通常选择较大位移方向。若 $ |x_1 - x_0| > |y_1 - y_0| $,则以 $ x $ 方向为步长方向,否则以 $ y $ 方向为步长方向。
选择较大位移方向是为了提高精度
- 计算增量
- 若步长方向为 $ x $,则每次在 $ x $ 方向移动一个单位长度,即 $ \Delta x = 1 $,对应的 $ y $ 方向增量为 $ \Delta y = k $。
- 若步长方向为 $ y $,则每次在 $ y $ 方向移动一个单位长度,即 $ \Delta y = 1 $,对应的 $ x $ 方向增量为 $ \Delta x = \frac{1}{k} $。
- 逐点绘制
- 从起点开始,每次在步长方向移动一个单位长度,并根据计算的增量确定另一个方向的坐标,得到下一个像素点的坐标。
- 重复该过程,直到到达终点。
已知过端点 $P_0(x_0, y_0)$, $P_1(x_1, y_1)$ 的直线段 $L(P_0, P_1)$,直线斜率 $k = \frac{y_1 - y_0}{x_1 - x_0}$。画线过程为:从 $x$ 的左端点
$x_0$ 开始,向 $x$ 右端点步进,步长 = 1(像素),按 $y = kx + b$ 计算相应的 $y$ 坐标,并取像素点 $(x, \text{round}(y))$ 作为当前点的坐标。但
这样做,计算每一个点需要做一个乘法、一个加法。设步长为 $\Delta x$,有 $x_{i+1} = x_i + \Delta x$,于是:
$$y_{i+1} = kx_{i+1} + b = kx_i + k \Delta x + b = y_i + k \Delta x$$
当 $\Delta x = 1$ 时,则有 $y_{i+1} = y_i + k$;即 $x$ 每递增 1,$y$ 递增 $k$(即直线斜率)。这样,计算就由一个乘法和一个加法减少为一个加法。
在c++中,
int强转
是截断小数点的,所以要加上0.5来确保四舍五入应当注意: 图中上述算法仅适用于
|k|<1
的情形。在这种情况下,x每增加1,y最多增加1。当|k| > 1
时,必须把x,y的地位互换,y每增加1,x相应增加1/k。在这个算法中,y与k必须用浮点数表示,而且每一步都要对y进行四舍五入后取整,这使得该算法不利于硬件实现。
- 优点
- 易于编程实现。
- 计算量较小,适合计算机图形学中的直线绘制。
- 缺点
- 绘制速度相对较慢,因为需要进行浮点运算。
- 精度有限,受设备精度的影响较大。
#include "widget.h"
#include "ui_widget.h"
#define cout qDebug()
Widget::Widget(QWidget *parent): QWidget(parent) , ui(new Ui::Widget)
{
ui->setupUi(this);
this->resize(800,400);
image = new QImage(rect().width(), rect().height(), QImage::Format_RGB32);
image->fill(Qt::white); // 初始化图像背景为白色
QPoint start(0, 0);
QPoint end(image->rect().width() - 1, image->rect().height() - 1);
// 使用DDA算法在Qimage上设置直线【像素点集合】
DDACalc(&start, &end, *image);update();
}
Widget::~Widget()
{
delete ui;
delete image; // 释放图像内存
}
void Widget::paintEvent(QPaintEvent *event) {
// 创建一个用于绘制到窗口的画家
QPainter painter(this);
// 绘制文字
painter.setRenderHint(QPainter::Antialiasing);
painter.setRenderHint(QPainter::NonCosmeticBrushPatterns); // 启用高抗锯齿
painter.setPen(Qt::blue);
painter.setFont(QFont("Arial", 16));
painter.drawText(QRect(0, 0, 200, 30), Qt::AlignCenter, "DDA算法画直线");
painter.drawImage(QRect(0, 0, image->rect().width(), image->rect().height()), *image);
// Qt自带的绘制直线函数
// QPoint start(0, 0);
// QPoint end(image->rect().width() - 1, image->rect().height() - 1);
// painter.drawLine(start, end);
}
// 此处只考虑了从左往右绘制的情况
void Widget::DDACalc(const QPoint *start, const QPoint *end, QImage &img) {
int dx = end->x() - start->x();
int dy = end->y() - start->y();
float k = dy / float(dx); // 求出斜率k
cout << "k = " <<k;
int steps = qMax(dx, dy);
qDebug() << steps;
if (steps == 0) { // 起点和终点重合
img.setPixel(start->x(), start->y(), qRgb(0, 0, 255));
return;
}
float x = start->x();
float y = start->y();
if(steps == dx){
for(int i = x; i <=steps; i++){
image->setPixel(i, int(y + 0.5), qRgb(0, 0, 255));
y = y + k;
}
}else{
for (int i = y; i<= steps; i++) {
image->setPixel(int(x + 0.5), i, qRgb(0, 0, 255));
x = x + 1.0 /k;
}
}
}
中点画线法
https://zhuanlan.zhihu.com/p/468849410
算法核心思想:通过判断直线中点与理想直线的位置关系,决定下一像素点的选择。
- 参数预处理
- 输入起点
(x0, y0)
和终点(x1, y1)
- 计算坐标增量:
dx = x1 - x0
dy = y1 - y0
- 确定直线的主方向:
- 斜率绝对值 < 1:以
x
为步进方向(水平主方向) - 斜率绝对值 ≥ 1:以
y
为步进方向(垂直主方向)
- 斜率绝对值 < 1:以
- 决策变量公式推导
以 斜率 0 ≤ k ≤ 1 为例(其他斜率通过对称性处理):
- 直线隐式方程:
F(x, y) = dy·x - dx·y + dx·y0 - dy·x0 = 0
- 中点
M(x+1, y+0.5)
到直线的距离由F(M)
的符号决定:F(M) < 0
→ 中点在直线下方 → 选择上方像素(x+1, y+1)
F(M) ≥ 0
→ 中点在上方或恰在直线上 → 选择下方像素(x+1, y)
- 决策变量初始值:
d = 2·dy - dx
(简化后的整数形式)
- 递推公式
- 若
d < 0
:
选择下方像素(x+1, y)
,更新决策变量:
d = d + 2·dy
- 若
d ≥ 0
:
选择上方像素(x+1, y+1)
,更新决策变量:
d = d + 2·(dy - dx)
void Widget::midpointLine(const QPoint& start, const QPoint& end, QImage& img) {
int x0 = start.x();
int y0 = start.y();
int x1 = end.x();
int y1 = end.y();
int dx = x1 - x0;
int dy = y1 - y0;
int d, x = x0, y = y0;
// 斜率小于1的情况
if (abs(dy) <= abs(dx)) {
d = dx - 2 * dy ;
while (x <= x1) {
img.setPixel(x, y, qRgb(0, 0, 0)); // 设置像素点颜色
if (d >= 0) {
d -= 2 * dy;
} else {
d += -2 * dy + 2 * dx;
y++;
}
x++;
}
}
// 斜率大于1的情况
else {
d = dy - 2 * dx;
while (y <= y1) {
img.setPixel(x, y, qRgb(0, 0, 0)); // 设置像素点颜色
if (d >= 0) {
d -= 2 * dx;
} else {
d += - 2 * dx + 2 * dy;
x++;
}
y++;
}
}
}
Bresenham算法
void Widget::Bresenham(const QPoint& start, const QPoint& end, QImage& img) {
int x0 = start.x();
int y0 = start.y();
int x1 = end.x();
int y1 = end.y();
int dx = abs(x1 - x0);
int dy = abs(y1 - y0);
int sx = (x0 < x1) ? 1 : -1; // x方向步进(左/右)
int sy = (y0 < y1) ? 1 : -1; // y方向步进(上/下)
bool swapAxis = dy > dx; // 判断是否需要交换主轴
int error = swapAxis ? 2*dx - dy : 2*dy - dx; // 初始化误差项
int x = x0, y = y0;
// 根据主轴选择循环次数
int steps = swapAxis ? dy : dx;
for (int i = 0; i <= steps; i++) {
img.setPixel(x, y, qRgb(0, 0, 255));
if (swapAxis) {
// 以y轴为主方向
if (error >= 0) {
x += sx; // 副方向步进(x轴)
error -= 2*dy; // 修正误差项
}
error += 2*dx; // 主方向步进(y轴)
y += sy;
} else {
// 以x轴为主方向
if (error >= 0) {
y += sy; // 副方向步进(y轴)
error -= 2*dx; // 修正误差项
}
error += 2*dy; // 主方向步进(x轴)
x += sx;
}
}
}
3.2 多边形扫描算法
多边形分为凸多边形(任意两顶点间的连线均在多边形内)如图2-7(a)所示,凹多边形(任意两顶点间的连线可能有不在多边形内的部分)如图2-7(b)所示,含内环的多边形如图 2-7(c)所示
3.3 裁剪算法
使用计算机处理图形信息时,计算机内部存储的图形往往比较大,而屏幕显示的只是图的一部分,因此需要裁剪,提高显示效率。
3.2.1 直线段裁剪
Cohen-Sutherland 裁剪算法
该算法的思想是: 对于每条线段PP的处理分为3种情况。
若$P_1,P_2$完全在窗口内,则显示该线段,简称
取
之若$P_1,P_2$:明显在窗口外,则丢弃该线段,简称
弃
之。若线段既不满足“取”的条件,也不满足“弃”的条件,则在交点处把线段分为两段,其中一段完全在窗口外,可弃之;然后对另一段重复上述处理。
#include <iostream>
#include <vector>
using namespace std;
// 定义区域编码的位掩码
const int INSIDE = 0; // 0000
const int LEFT = 1; // 0001
const int RIGHT = 2; // 0010
const int BOTTOM = 4; // 0100
const int TOP = 8; // 1000
// 视口边界坐标
double x_min = 100, x_max = 400;
double y_min = 100, y_max = 300;
// 计算点的区域编码
int computeCode(double x, double y) {
int code = INSIDE;
if (x < x_min) code |= LEFT;
else if (x > x_max) code |= RIGHT;
if (y < y_min) code |= BOTTOM;
else if (y > y_max) code |= TOP;
return code;
}
// Cohen-Sutherland 裁剪算法
bool cohenSutherlandClip(double &x0, double &y0, double &x1, double &y1) {
int code0 = computeCode(x0, y0);
int code1 = computeCode(x1, y1);
bool accept = false;
while (true) {
if (!(code0 | code1)) { // 完全在视口内
accept = true;
break;
} else if (code0 & code1) { // 完全在视口外
break;
} else {
// 至少有一个点在外部,选择外部点进行裁剪
int codeOut = code0 ? code0 : code1;
double x, y;
// 计算与边界的交点
if (codeOut & TOP) { // 与上边界相交
x = x0 + (x1 - x0) * (y_max - y0) / (y1 - y0);
y = y_max;
} else if (codeOut & BOTTOM) { // 与下边界相交
x = x0 + (x1 - x0) * (y_min - y0) / (y1 - y0);
y = y_min;
} else if (codeOut & RIGHT) { // 与右边界相交
y = y0 + (y1 - y0) * (x_max - x0) / (x1 - x0);
x = x_max;
} else if (codeOut & LEFT) { // 与左边界相交
y = y0 + (y1 - y0) * (x_min - x0) / (x1 - x0);
x = x_min;
}
// 更新外部点为交点
if (codeOut == code0) {
x0 = x;
y0 = y;
code0 = computeCode(x0, y0);
} else {
x1 = x;
y1 = y;
code1 = computeCode(x1, y1);
}
}
}
return accept;
}
int main() {
// 示例线段:起点(50,150),终点(450,250)
double x0 = 50, y0 = 150;
double x1 = 450, y1 = 250;
if (cohenSutherlandClip(x0, y0, x1, y1)) {
cout << "裁剪后的线段端点:\n";
cout << "(" << x0 << ", " << y0 << ") -> ("
<< x1 << ", " << y1 << ")\n";
} else {
cout << "线段完全在视口外\n";
}
return 0;
}
中点裁剪算法
梁友栋裁剪算法
3.2.2 多边形裁剪 Suther land-Hodgeman算法
计算机图形学 学习笔记(五):多边形裁剪(Suther land-Hodgeman),文字裁剪_2sutherland的九区域代码的定义:-CSDN博客
基本策略是,先读取最开始的顶点V0和V3
,对窗口左边
做处理,得到交点L1和V3
作为处理窗口下一条边的输入,然后对窗口上边
做处理,输出L1和新的交点L2
,然后这样不断读取下去,直到所有的点都处理过
第四章 真实感图形学
真实感图形学是计算机图形学中一个重要的组成部分,它的基本要求就是在计算机中生成二维场景的真实感图形图像,包括各类自然现象。
4.1 颜色视觉
从不同的角度来看,对于颜色有不同的要素或特性:
- 心理学和视觉角度:色调,饱和度和亮度
- 光学:主波长,纯度(对应于饱和度),明度(对应于亮度)
饱和度
是指颜色的纯度,例如鲜红色的饱和度高,而粉红色的饱和度低。亮度
就是光的强度,是光给人刺激的强度。
三色学说:任何一种颜色可以用红、绿、蓝三原色按照不同比例混合来得到
光学补充知识
- 正常情况下,
光沿直线传播
,当遇到不同的介质的分界面时,会产生反射和折射现象 - 反射定律:入射角等于反射角
折射定律:对应介质的折射率之比等于
入射线和折射线与法线构成的夹角的sin值
之比
- 能量关系:能量是守恒的
光的度量:
- 立体角
- 点发光强度
4.2 简单光照模型
Pong光照模型
简单光照模型(Phong和Blinn-Phong)和明暗处理 - 芒果和小猫 - 博客园
Blinn-Phong光照模型与着色方法 - Tim’s Note
基本组成
相关向量
- 法线向量(Normal Vector):垂直于表面的向量,用于表示表面的方向。
- 视图向量(View Vector):指向观察者方向的向量。
- 光线向量(Light Vector):指向光源方向的向量。
- 反射向量(Reflection Vector):表示光线在表面反射后的方向。
特点
- 简单高效:Phong模型相对简单,计算成本较低,能够快速地为物体表面提供较为真实的光照效果。
- 可调节性强:通过调整各个反射系数和高光指数等参数,可以模拟不同材质的光照效果。
在Phong光照模型中,确定某个点的RGB值需要综合考虑环境光、漫反射光和镜面反射光这三种光照分量。以下是具体的计算过程:
1. 环境光(Ambient Light)
环境光是模拟场景中普遍存在的光,与物体表面的朝向无关。其计算公式为:
$ I_a = k_a \cdot I_{a_{\text{source}}} $
其中:
- $ I_a $ 是环境光对物体表面的贡献。
- $ k_a $ 是物体表面对环境光的反射系数,通常是一个 RGB 向量。
- $ I_{a_{\text{source}}} $ 是环境光的强度,也是一个 RGB 向量。
例如,假设环境光的强度为 $ I_{a_{\text{source}}} = (0.2, 0.2, 0.2) $,物体表面对环境光的反射系数为 $ k_a = 0.1 $,则环境光的贡献为:
$ I_a = 0.1 \cdot (0.2, 0.2, 0.2) = (0.02, 0.02, 0.02) $2. 漫反射光(Diffuse Light)
漫反射光是光线照射到物体表面后,向各个方向均匀反射的光。其计算公式为:
$ I_d = k_d \cdot I_{d_{\text{source}}} \cdot \max(0, \vec{N} \cdot \vec{L}) $
其中:
- $ I_d $ 是漫反射光对物体表面的贡献。
- $ k_d $ 是物体表面对漫反射光的反射系数,通常是一个 RGB 向量。
- $ I_{d_{\text{source}}} $ 是光源的漫反射强度,也是一个 RGB 向量。
- $ \vec{N} $ 是物体表面的法线向量,需要归一化。
- $ \vec{L} $ 是从物体表面点指向光源的方向向量,也需要归一化。
- $ \vec{N} \cdot \vec{L} $ 是法线向量和光线方向向量的点积,表示光线与表面的夹角。
例如,假设光源的漫反射强度为 $ I_{d_{\text{source}}} = (1.0, 1.0, 1.0) $,物体表面对漫反射光的反射系数为 $ k_d = 0.5 $,法线向量为 $ \vec{N} = (0, 1, 0) $,光线方向向量为 $ \vec{L} = (0, 1, 0) $,则漫反射光的贡献为:
$ \vec{N} \cdot \vec{L} = 1 $
$ I_d = 0.5 \cdot (1.0, 1.0, 1.0) \cdot 1 = (0.5, 0.5, 0.5) $3. 镜面反射光(Specular Light)
镜面反射光是光线照射到光滑表面后,按照反射定律反射的部分。其计算公式为:
$ I_s = k_s \cdot I_{s_{\text{source}}} \cdot \max(0, \vec{R} \cdot \vec{V})^n $
其中:
- $ I_s $ 是镜面反射光对物体表面的贡献。
- $ k_s $ 是物体表面对镜面反射光的反射系数,通常是一个 RGB 向量。
- $ I_{s_{\text{source}}} $ 是光源的镜面反射强度,也是一个 RGB 向量。
- $ \vec{R} $ 是反射光线方向向量,可以通过公式 $ \vec{R} = 2(\vec{N} \cdot \vec{L})\vec{N} - \vec{L} $ 计算得到。
- $ \vec{V} $ 是从物体表面点指向观察者的方向向量,需要归一化。
- $ n $ 是高光指数,用于控制高光的锐度,数值越大,高光越集中。
例如,假设光源的镜面反射强度为 $ I_{s_{\text{source}}} = (1.0, 1.0, 1.0) $,物体表面对镜面反射光的反射系数为 $ k_s = 0.5 $,高光指数为 $ n = 32 $,反射光线方向向量为 $ \vec{R} = (0, 1, 0) $,观察者方向向量为 $ \vec{V} = (0, 1, 0) $,则镜面反射光的贡献为:
$ \vec{R} \cdot \vec{V} = 1 $
$ I_s = 0.5 \cdot (1.0, 1.0, 1.0) \cdot 1^{32} = (0.5, 0.5, 0.5) $4. 最终颜色计算
将上述三种光照分量相加,得到物体表面某一点的最终颜色:
$ I = I_a + I_d + I_s $假设物体的颜色为 $ \text{objectColor} = (1.0, 0.5, 0.3) $,则最终颜色为:
$ I = (0.02, 0.02, 0.02) + (0.5, 0.5, 0.5) + (0.5, 0.5, 0.5) $
$ I = (1.02, 1.02, 1.02) $由于 RGB 值的范围是 [0, 1],因此需要将最终颜色值限制在 [0, 1] 范围内:
$ I = \text{clamp}(I, 0, 1) = (1.0, 1.0, 1.0) $最终,该点的 RGB 值为 $ (1.0, 1.0, 1.0) $,表示该点在 Phong 光照模型下的颜色为白色。
示例代码(基于 OpenGL 的片段着色器)
以下是一个简单的 OpenGL 片段着色器代码,用于计算 Phong 光照模型下的颜色:
>// 片段着色器 >#version 330 core >out vec4 FragColor; >in vec3 FragPos; // 片段位置 >in vec3 Normal; // 片段法线 >uniform vec3 lightPos; // 光源位置 >uniform vec3 viewPos; // 观察者位置 >uniform vec3 lightColor; // 光源颜色 >uniform vec3 objectColor; // 物体颜色 >void main() >{ // 环境光 float ka = 0.1; vec3 ambient = ka * lightColor; // 漫反射 float kd = 1.0; vec3 N = normalize(Normal); vec3 L = normalize(lightPos - FragPos); float NdotL = max(dot(N, L), 0.0); vec3 diffuse = kd * NdotL * lightColor; // 镜面反射 float ks = 0.5; float shininess = 32.0; vec3 V = normalize(viewPos - FragPos); vec3 R = reflect(-L, N); float spec = pow(max(dot(V, R), 0.0), shininess); vec3 specular = ks * spec * lightColor; // 最终颜色 vec3 result = (ambient + diffuse + specular) * objectColor; FragColor = vec4(result, 1.0); >}
4.3 增量式光照模型
Phong光照明模型中光源和视点都被假定为无穷远,最后的光强计算公式就变为物体表面法向量的函数。这样对于当前流行的显示系统中用多边形表示的物体来说,它们中的每一个多边形由于法向一致,因而多边形内部像素的颜色都是相同的。
因此在不同法向的多边形邻接处,不仅有光强突变,而且还会产生马赫带效应_百度百科,即人类视觉系统夸大具有不同常量光强的两个相邻区域之间的光强不连续性。
为了保证多边形之间的光滑过渡,使连续的多边形呈现匀称的光强分布,可采用下面将要介绍的增量式光照明模型。模型的基本思想是:在每一个多边形的顶点处,计算合适的光照明强度或其他参数;然后在各个多边形内部进行均匀插值;最后得到多边形的光滑颜色分布。它包含两种主要形式:
双线性光强插值
和双线性法向插值
,又被分别称为Gouraud明暗处理
和Phong明暗处理
。
双线性光强插值