基础算法:
1、试想给出两个点,连一条线,考虑屏幕像素是一块一块的,简单的想法是起点朝着终点的方向一块一块的画点,足够多的点就连成了一条线,具体实现:
(这里需要注意static_cast < type-id > ( expression )是把expression强制转换成type-id类型。)
void line1(int x0, int y0, int x1, int y1, TGAImage& image, const TGAColor& color)
{
for (float t = 0.0f; t < 1.0f; t += 0.01f) //求出起点到终点之间尽量多的点的坐标
{
int x = static_cast<int>(x0 + (x1 - x0) * t);
int y = static_cast<int>(y0 + (y1 - y0) * t);
image.set(x, y, color);
}
}
2、起点(x0,y0),终点(x1,y1),然后中间点(x,y),引入斜率相关概念。(x - x0)/(x1-x0) = (y-y0)/(y1-y) = t; 然后得出方程式:y=(1-t)*y0+t*y1,代码
void line2(int x0, int y0, int x1, int y1, TGAImage& image, const TGAColor& color)
{
for (float x = x0; x <= x1; x+=0.1)
{
float t = (x - x0) / static_cast<float>(x1 - x0);
int y = static_cast<int>(y0 * (1.0 - t) + y1 * t);
image.set(x, y, color);
}
}
上述本质上就是对x进行采样,根据x的步长来确定精准度,每次采样的x0到x1范围越大、递增值越小,线条精度越高。但有的时候x0到x1的跨越范围很小,y0到y1的范围跨越很大,就会造成失真,比如x0=0;x1=10,而y0=0,y1=100这种情况,优化逻辑就是做两次采样,并判断选择,代码如下:
void line2_transpose(int x0, int y0, int x1, int y1, TGAImage& image, const TGAColor& color)
{
for (int y = y0; y <= y1; y++)
{
float t = (y - y0) / static_cast<float>(y1 - y0);
int x = static_cast<int>(x0 * (1.0f - t) + x1 * t);
image.set(x, y, color);
}
}
void line3(int x0, int y0, int x1, int y1, TGAImage& image, const TGAColor& color)
{
int dx = std::abs(x1 - x0);
int dy = std::abs(y1 - y0);
if (dx >= dy)
{
if (x0 < x1)
{
line2(x0, y0, x1, y1, image, color);
}
else
{
line2(x1, y1, x0, y0, image, color);
}
}
else
{
if (y0 < y1)
{
line2_transpose(x0, y0, x1, y1, image, color);
}
else
{
line2_transpose(x1, y1, x0, y0, image, color);
}
}
}
进阶算法:
1、DDA直线算法
2、中点画线算法
3、Bresenham画线算法
对于真实的直线,我们的横坐标变化dx,那么纵坐标就跟着变化dy(对应上图中横坐标增加1,纵坐标增加slope)。所以,当x = x0 + 1时,真实的直线上点坐标应该是(x0 + 1, y0 + 1 * slope),那么我们下一个可能选择的点就是(x0+1,y0)或者(x0+1, y0+1),我们通过比较y0 + 1 * slope和这两个待选点的中点位置来确定选择的结果,即x0 + 1点的y坐标是int(y0 + 1 * slope + 0.5)。
此时:
真实的y值为y0 + 1 * slope,y0和y0 + 1的中点为(2 * y0 + 1) / 2,即y0 + 0.5,所以我们实际上只需要比较1 * slope和0.5就可以:
1) 如果1 * slope > 0.5,说明真实的y值更靠近y0 + 1,那么下一个点就应该选择(x0 + 1, y0 + 1);
2) 如果1 * slope < 0.5,说明真实的y值更靠近y0,那么下一个点选择(x0 + 1, y0);
所以,我们就可以用斜率的大小来估计下一个点选哪一个。
在上面的代码中,derror就是斜率值,即我们所讨论的slope。
我们目前一直讨论的都只是(x0,y0)右边的第一个点如何选择,那么更后边的点该怎么判断呢?
我们还是以y0为参考基准,那么后续第n个点的真实y坐标就是y0 + n * slope,而它需要跟谁进行比较呢?
所以在代码中增加了一个error变量来累计误差,每次error大于0.5的时候,需要给y值加1,同时给error减1来平衡y+1带来的影响。
Timings: fifth and final attem
void line(int x0, int y0, int x1, int y1, TGAImage& image, const TGAColor& color)
{
bool steep = false;
if (std::abs(x0 - x1) < std::abs(y0 - y1)) {
std::swap(x0, y0);
std::swap(x1, y1);
steep = true;//如果dy大于dx,那么就翻转x、y坐标
}
if (x0 > x1) {
std::swap(x0, x1);//如果逆序,那么就交换起点终点坐标
std::swap(y0, y1);
}
int dx = x1 - x0;
int dy = std::abs(y1 - y0);
int d = 2 * dy - dx;
//判断斜率, 如果2dy-dx<0推出dy/dx<1/2,斜率小于0.5,则代表选择y,反之大于0.5则代表选择y+1
int incrE = 2 * dy;
int incrNE = 2 * dy - 2 * dx;
int x = x0, y = y0;
if (steep) { //第一个像素点直接着色
image.set(y, x, color);
}
else {
image.set(x, y, color);
}
for (int x = x0 + 1; x <= x1; x++)
{
if (d < 0)
{
d += incrE;
}
else
{
if (y1 > y0)
y++;
else
y--;
d += incrNE;
}
if (steep) {
image.set(y, x, color);
}
else {
image.set(x, y, color);
}
}
}
void line5(int x0, int y0, int x1, int y1, TGAImage& image, const TGAColor& color)
{
bool steep = false;
if (std::abs(x0 - x1) < std::abs(y0 - y1)) {
std::swap(x0, y0);
std::swap(x1, y1);
steep = true;
}
if (x0 > x1) {
std::swap(x0, x1);
std::swap(y0, y1);
}
int dx = x1 - x0;
int dy = y1 - y0;
int derror2 = std::abs(dy) * 2; //2dy
int error2 = 0;
int y = y0;
for (int x = x0; x <= x1; x++) {
if (steep) {
image.set(y, x, color); //steep为true表示x和y交换过,因此要颠倒
}
else {
image.set(x, y, color);
}
error2 += derror2;
if (error2 > dx) {
y += (y1 > y0 ? 1 : -1);
error2 -= dx * 2;
}
}
}