279 lines
9.6 KiB
Markdown
279 lines
9.6 KiB
Markdown
---
|
||
title: OpenGL-直线的扫描转换
|
||
cover: https://pic.biss.click/image/c5457adc-214c-4a18-9aa6-43fbc0bfc2f4.webp
|
||
categories:
|
||
- 技术
|
||
- 学习
|
||
series: OpenGL
|
||
tags: OpenGL
|
||
abbrlink: 7207243b
|
||
summary: >-
|
||
这篇文章介绍了三种直线扫描转换算法:DDA算法、中点画线算法和Bresenham算法。
|
||
DDA算法通过计算直线起点和终点的坐标差值来确定步数和增量,依次绘制直线上的点。算法实现简单,适用于任意直线,但精度较低。
|
||
中点画线算法仅适用于斜率为0或1的直线,通过计算中点和斜率来确定直线上的像素点,效率较高。
|
||
Bresenham算法根据直线的斜率和误差项来确定像素点的位置,适用于任意直线,且精度高,但实现稍复杂。
|
||
date: 2026-04-11 19:01:02
|
||
---
|
||
|
||
这篇文章来介绍直线扫描转换算法
|
||
# DDA数值微分线段算法
|
||
## 算法简介
|
||
数值微分法即DDA法(Digital Differential Analyzer),是一种基于微分方程来生成直线的方法。在计算机图形学中,并没有线段的概念,而是一个个像素点组成了线段。
|
||
|
||
DDA法生成线段的步骤一般如下:
|
||
|
||
1. 有了起始点($x_1,y_1$)和终点($x_n,y_n$);
|
||
2. $$\Delta x =|x_n-x_1|, \Delta y=|y_n-y_1|$$
|
||
3. 比较$\Delta x$和$\Delta y$的大小;
|
||
steps=$\Delta x$和$\Delta y$中较大者;
|
||
4. $$step_x=\frac{\Delta x}{steps},step_y=\frac{\Delta y}{steps}$$
|
||
## 算法实现
|
||
DDA算法实现如下:
|
||
```cpp
|
||
#include <cmath>
|
||
#include <iostream>
|
||
#include <GL/glut.h>
|
||
|
||
// 窗口宽度和高度
|
||
const int WIDTH = 640;
|
||
const int HEIGHT = 480;
|
||
|
||
void drawDDALine(int x1, int y1, int x2, int y2) {
|
||
float x = x1;
|
||
float y = y1;
|
||
|
||
// 计算差值
|
||
int dx = x2 - x1;
|
||
int dy = y2 - y1;
|
||
|
||
// 确定步数,取 dx 和 dy 中绝对值较大的那个
|
||
int steps = std::abs(dx) > std::abs(dy) ? std::abs(dx) : std::abs(dy); //三元表达式
|
||
|
||
// 计算每一步的增量
|
||
float xIncrement = (float)dx / steps;
|
||
float yIncrement = (float)dy / steps;
|
||
|
||
// 开始绘制点
|
||
glBegin(GL_POINTS);
|
||
glVertex2i((int)round(x), (int)round(y)); // 绘制起点
|
||
|
||
for (int k = 0; k < steps; k++) {
|
||
x += xIncrement;
|
||
y += yIncrement;
|
||
// 将浮点坐标四舍五入取整转换为整数像素坐标
|
||
std::cout << (int)round(x) << ", " << (int)round(y)<<"\n";
|
||
glVertex2i((int)round(x), (int)round(y));
|
||
}
|
||
glEnd();
|
||
}
|
||
|
||
// 显示回调函数
|
||
void display() {
|
||
drawDDALine(0, 0, 50, 20);
|
||
glFlush();
|
||
}
|
||
|
||
// 初始化 OpenGL 设置
|
||
void init() {
|
||
// 设置背景颜色为白色
|
||
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
|
||
|
||
// 设置投影矩阵为 2D 正交投影
|
||
glMatrixMode(GL_PROJECTION);
|
||
glLoadIdentity();
|
||
// 定义可视区域,左下角(0,0),右上角(WIDTH, HEIGHT)
|
||
gluOrtho2D(0.0, WIDTH, 0.0, HEIGHT);
|
||
}
|
||
|
||
int main(int argc, char** argv) {
|
||
// 初始化 GLUT
|
||
glutInit(&argc, argv);
|
||
|
||
// 设置显示模式:单缓冲、RGB 颜色模式
|
||
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
|
||
|
||
// 设置窗口大小和位置
|
||
glutInitWindowSize(WIDTH, HEIGHT);
|
||
glutInitWindowPosition(100, 100);
|
||
|
||
// 创建窗口
|
||
glutCreateWindow("DDA算法");
|
||
|
||
// 注册回调函数
|
||
glutDisplayFunc(display);
|
||
|
||
// 初始化设置
|
||
init();
|
||
|
||
// 进入主循环
|
||
glutMainLoop();
|
||
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
# 中点画线算法
|
||
## 算法简介
|
||
只考虑当直线的斜率$|k|< 1$时的情况,假设现在有一条直线$(x_1,y_1,x_2,y_2)$,那么第一个点一定是$(x_1,y_1)$无疑,下一个点的$x$坐标为$x_1+1$,$y$坐标要么为$y1$要么为$y1+1$。关键在于每次取下一个点时,是取前一个的$y1$呢,还是$y1+1$,这时一定是取直线上点最靠近的那个了,而判断取哪个点就用到了中点,我们将中点代入直线中 $d=F(x_1+1,y_1+0.5)=a \cdot (x_1+1)+b \cdot (y_1+0.5)+c$。
|
||
|
||
1. 如果直线$d>=0$,则取下边的点也就是$(x_1+1,y_1)$。
|
||
2. 如果直线$d<0$,则取上边的点也就是$(x_1+1,y_1+1)$。
|
||
|
||
它的实际过程就是这样每次根据前边的点判断下一个点在哪,然后进行打亮,但这样每次判断的时候都得代入直线方程计算太麻烦了,我们将这俩种情况分别代入直线方程中可以找出规律:
|
||
|
||
1. 当直线$d>=0$时,经过化解得$d_1=d+a$;
|
||
2. 当直线$d<0$时,经过化解得$d_2=d+a+b$;
|
||
3. 初始值$d_0=a+0.5b$。
|
||
|
||
也就是说每次的增量要么为$a$,要么为$a+b$,那么这样判断的时候就简单多了,因为我们每次只是判断它的正负。所以给等式同时乘2,将其中浮点数0.5化为整数,这样硬件操作时无疑更快了。
|
||
|
||
## 算法实现
|
||
```cpp
|
||
#include <GL/freeglut.h>
|
||
#include <iostream>
|
||
#include <cmath>
|
||
|
||
// 中点画线算法函数 (仅针对斜率 0 <= k <= 1 的情况进行演示,其它象限需类比处理)
|
||
void drawLineMidpoint(int x0, int y0, int x1, int y1) {
|
||
int a = y0 - y1;
|
||
int b = x1 - x0;
|
||
int d = 2 * a + b;
|
||
int d1 = 2 * a;
|
||
int d2 = 2 * (a + b);
|
||
|
||
int x = x0, y = y0;
|
||
|
||
glBegin(GL_POINTS);
|
||
glVertex2i(x, y); // 画起点
|
||
|
||
while (x < x1) {
|
||
if (d < 0) {
|
||
x++;
|
||
y++;
|
||
d += d2;
|
||
} else {
|
||
x++;
|
||
d += d1;
|
||
}
|
||
glVertex2i(x, y);
|
||
}
|
||
glEnd();
|
||
}
|
||
|
||
// 渲染回调函数
|
||
void display() {
|
||
glClear(GL_COLOR_BUFFER_BIT);
|
||
glColor3f(1.0, 1.0, 1.0); // 设置画笔颜色为白色
|
||
|
||
// 调用算法画一条直线 (x0, y0) 到 (x1, y1)
|
||
// 注意:此处的坐标对应屏幕像素坐标
|
||
drawLineMidpoint(50, 50, 450, 300);
|
||
|
||
glFlush();
|
||
}
|
||
|
||
// 初始化设置
|
||
void init() {
|
||
glClearColor(0.0, 0.0, 0.0, 1.0); // 背景设为黑色
|
||
glMatrixMode(GL_PROJECTION);
|
||
glLoadIdentity();
|
||
// 设置正交投影矩阵,使坐标系与窗口像素对应
|
||
gluOrtho2D(0, 500, 0, 500);
|
||
}
|
||
|
||
int main(int argc, char** argv) {
|
||
glutInit(&argc, argv);
|
||
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
|
||
glutInitWindowSize(500, 500);
|
||
glutInitWindowPosition(100, 100);
|
||
glutCreateWindow("Midpoint Line Algorithm - FreeGLUT");
|
||
|
||
init();
|
||
glutDisplayFunc(display);
|
||
glutMainLoop();
|
||
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
# Bresenham算法
|
||
## 算法简介
|
||
假设我们需要由$(x_0, y_0)$这一点,绘画一直线至右下角的另一点$(x_1, y_1)$,x,y分别代表其水平及垂直坐标,并且$x_1 - x_0 > y_1 - y_0$。在此我们使用电脑系统常用的坐标系,即$x$坐标值沿$x$轴向右增长,$y$坐标值沿$y$轴向下增长。
|
||
|
||
因此x及y之值分别向右及向下增加,而两点之水平距离为$x_{1}-x_{0}$且垂直距离为$y_{1}-y_{0}$。由此得之,该线的斜率必定介乎于$0$至$1$之间。而此算法之目的,就是找出在$x_{0}$与$x_{1}$之间,第$x$行相对应的第$y$列,从而得出一像素点,使得该像素点的位置最接近原本的线。
|
||
|
||
对于由$(x_0, y_0)$及$(x_1, y_1)$两点所组成之直线,公式如下:
|
||
$$y-y_{0}={\frac {y_{1}-y_{0}}{x_{1}-x_{0}}}(x-x_{0})$$
|
||
因此,对于每一点的x,其y的值是
|
||
$${\frac {y_{1}-y_{0}}{x_{1}-x_{0}}}(x-x_{0})+y_{0}$$
|
||
因为$x$及$y$皆为整数,但并非每一点$x$所对应的$y$皆为整数,故此没有必要去计算每一点x所对应之$y$值。反之由于此线之斜率介乎于$1$至$0$之间,故此我们只需要找出当$x$到达那一个数值时,会使$y$上升$1$,若$x$尚未到此值,则$y$不变。至于如何找出相关的$x$值,则需依靠斜率。斜率之计算方法为$m=(y_{1}-y_{0})/(x_{1}-x_{0})$。由于此值不变,故可于运算前预先计算,减少运算次数。
|
||
|
||
要实行此算法,我们需计算每一像素点与该线之间的误差。于上述例子中,误差应为每一点$x$中,其相对的像素点之$y$值与该线实际之$y$值的差距。每当$y$的值增加$1$,误差的值就会增加$m$。每当误差的值超出$0.5$,线就会比较靠近下一个映像点,因此$y$的值便会加$1$,且误差减$1$。
|
||
|
||
## 算法实现
|
||
```cpp
|
||
#include <GL/freeglut.h>
|
||
#include <iostream>
|
||
#include <cmath>
|
||
|
||
// 通用 Bresenham 画线算法
|
||
void drawLineBresenham(int x0, int y0, int x1, int y1) {
|
||
int dx = abs(x1 - x0);
|
||
int dy = abs(y1 - y0);
|
||
int sx = (x0 < x1) ? 1 : -1; // X 方向步进
|
||
int sy = (y0 < y1) ? 1 : -1; // Y 方向步进
|
||
int err = dx - dy; // 初始误差项
|
||
|
||
glBegin(GL_POINTS);
|
||
while (true) {
|
||
glVertex2i(x0, y0); // 绘制当前点
|
||
|
||
if (x0 == x1 && y0 == y1) break; // 到达终点
|
||
|
||
int e2 = 2 * err;
|
||
// 判断是否在 X 方向步进
|
||
if (e2 > -dy) {
|
||
err -= dy;
|
||
x0 += sx;
|
||
}
|
||
// 判断是否在 Y 方向步进
|
||
if (e2 < dx) {
|
||
err += dx;
|
||
y0 += sy;
|
||
}
|
||
}
|
||
glEnd();
|
||
}
|
||
|
||
// 渲染回调
|
||
void display() {
|
||
glClear(GL_COLOR_BUFFER_BIT);
|
||
glColor3f(0.0f, 0.8f, 0.4f); // 设置一个好看的绿色(类似你图中的图标颜色)
|
||
|
||
// 绘制几条不同方向的线来测试算法的健壮性
|
||
drawLineBresenham(50, 50, 450, 400); // 第一象限
|
||
drawLineBresenham(50, 400, 450, 50); // 第四象限
|
||
drawLineBresenham(250, 50, 250, 450); // 垂直线
|
||
drawLineBresenham(50, 250, 450, 250); // 水平线
|
||
|
||
glFlush();
|
||
}
|
||
|
||
void init() {
|
||
glClearColor(0.1f, 0.1f, 0.1f, 1.0f); // 深色背景
|
||
glMatrixMode(GL_PROJECTION);
|
||
glLoadIdentity();
|
||
gluOrtho2D(0, 500, 0, 500); // 建立 500x500 的直角坐标系
|
||
}
|
||
|
||
int main(int argc, char** argv) {
|
||
glutInit(&argc, argv);
|
||
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
|
||
glutInitWindowSize(600, 600);
|
||
glutCreateWindow("Bresenham Line Algorithm");
|
||
init();
|
||
glutDisplayFunc(display);
|
||
glutMainLoop();
|
||
return 0;
|
||
}
|
||
``` |