線段裁剪是圖形學中一個非常重要的東西。
線段裁剪簡單來說,就是截取能在屏幕中顯示出來的部分,這樣做可以減少計算機在渲染過程中的計算量,加快渲染速度。
下面開始實現線段裁剪函數bool ClipLine(int* x1, int* y1, int* x2, int* y2)。
1.劃分區域,對區域編碼
劃分區域是爲了更好的區分線段所在位置,爲裁剪做準備:
線段端點在中心不必處理,若在其他區域,便需要裁剪至中心範圍內
編碼由來參照下面的代碼
#define _C 0x00 //0000 中心
#define _N 0x01 //0001 北
#define _S 0x02 //0010 南
#define _W 0x04 //0100 西
#define _E 0x08 //1000 東
#define _EN (_E | _N) //1001 東北
#define _ES (_E | _S) //1010 東南
#define _WS (_W | _S) //0110 西南
#define _WN (_W | _N) //0101 西北
2.下面就是判斷兩點的所屬區域
//緩存,取得兩點值,裁剪過程中不能修改原值
int port1_x = *x1;
int port1_y = *y1;
int port2_x = *x2;
int port2_y = *y2;
//這裏的判斷隱藏了一個技巧,取<= >=是可以避免下面判斷0值問題
char port1 = _C;
//判斷點1的位置
if (port1_x <= 0)
port1 |= _W;
else if (port1_x >= _CLIENT_W)
port1 |= _E;
if (port1_y <= 0)
port1 |= _N;
else if (port1_y >= _CLIENT_H)
port1 |= _S;
char port2 = _C;
//判斷點2的位置
if (port2_x <= 0)
port2 |= _W;
else if (port2_x >= _CLIENT_W)
port2 |= _E;
if (port2_y <= 0)
port2 |= _N;
else if (port2_y >= _CLIENT_H)
port2 |= _S;
3.進行初判斷
得到點1、點2的位置後,是可以直接去初次判斷某些無法裁剪的情況
比如:
(1)只要線段兩端點都在中心(0000)處,不需要裁剪,可以直接繪製
(2)當線段兩端點有共同區域時,是不可能繪製出來顯示到屏幕中的
//兩點在中心,無需裁剪
if (port1 == _C && port2 == _C)
return false;
//同時包含相同方向,比如東南和東北,都包括東,無需裁剪
if ((port1 & port2) != 0)
return false;
4.進行裁剪
(1)正方向(正北、正南、正東、正西)上的裁剪如下
case _W:
{
port1_x = 0;
port1_y = (int)(*y2 + (double)(*x2 * (*y1 - *y2)) / (*x2 - *x1) + 0.5);
break;
}
PS:上面的公式中後面+0.5的意義主要是爲了double轉int時四捨五入,如果不加0.5,編譯器會在轉換時向下取整,即1.9轉爲int時爲1。
上面舉了一個正西的例子,其他三個方向類似推導即可
case _E:
{
port1_x = _CLIENT_W - 1;
port1_y = (int)(*y1 - (double)((*x1 - (_CLIENT_W - 1)) * (*y1 - *y2)) / (*x1 - *x2) + 0.5);
break;
}
case _N:
{
port1_x = (int)(*x1 - *y1 * (double)(*x2 - *x1) / (*y2 - *y1) + 0.5);
port1_y = 0;
break;
}
case _S:
{
port1_x = (int)(*x2 - (double)((*x2 - *x1) * (_CLIENT_H - 1 - *y2)) / (*y1 - *y2) + 0.5);
port1_y = _CLIENT_H - 1;
break;
}
(2)非正方向(東北、西北、東南、西南)上的裁剪
代碼如下:
case _WN:
{
//按西方向裁剪
port1_x = 0;
port1_y = (int)(*y2 + (double)(*x2 * (*y1 - *y2)) / (*x2 - *x1) + 0.5);
if (port1_y < 0)
{
//按北方向裁剪
port1_x = (int)(*x1 - *y1 * (double)(*x2 - *x1) / (*y2 - *y1) + 0.5);
port1_y = 0;
}
break;
}
其餘的非正方向同理
5.點位置裁剪完畢後需要判斷
不在屏幕內的會被剔除,這樣的判斷也能確定點2是否能被裁剪到屏幕內
//判斷點1裁剪後的位置
if (port1_x < 0 || port1_x > _CLIENT_W - 1 || port1_y < 0 || port1_y > _CLIENT_H - 1)
{
return false;
}
6.點2的裁剪略過(類似點1),最後再處理一下
//裁剪成功
*x1 = port1_x;
*y1 = port1_y;
*x2 = port2_x;
*y2 = port2_y;
return true;
7.完整代碼+測試如下
#include <iostream>
#define _CLIENT_W 640
#define _CLIENT_H 480
#define _C 0x00 //00000000
#define _N 0x01 //00000001
#define _S 0x02 //00000010
#define _W 0x04 //00000100
#define _E 0x08 //00001000
#define _EN (_E | _N) //00001001
#define _ES (_E | _S) //00001010
#define _WS (_W | _S) //00000110
#define _WN (_W | _N) //00000101
bool ClipLine(int* x1, int* y1, int* x2, int* y2)
{
//緩存,取得兩點值
int port1_x = *x1;
int port1_y = *y1;
int port2_x = *x2;
int port2_y = *y2;
//這裏的判斷隱藏了一個技巧,取<= >=是可以避免下面判斷0值問題
char port1 = _C;
//判斷點1的位置
if (port1_x <= 0)
port1 |= _W;
else if (port1_x >= _CLIENT_W)
port1 |= _E;
if (port1_y <= 0)
port1 |= _N;
else if (port1_y >= _CLIENT_H)
port1 |= _S;
char port2 = _C;
//判斷點2的位置
if (port2_x <= 0)
port2 |= _W;
else if (port2_x >= _CLIENT_W)
port2 |= _E;
if (port2_y <= 0)
port2 |= _N;
else if (port2_y >= _CLIENT_H)
port2 |= _S;
//兩點在中心,無需裁剪
if (port1 == _C && port2 == _C)
return false;
//同時包含相同方向,比如東南和東北,都包括東,無需裁剪
if ((port1 & port2) != 0)
return false;
//裁剪時需使用相似三角形原理
//裁剪點1
switch (port1)
{
case _W:
{
port1_x = 0;
port1_y = (int)(*y2 + (double)(*x2 * (*y1 - *y2)) / (*x2 - *x1) + 0.5);
break;
}
case _E:
{
port1_x = _CLIENT_W - 1;
port1_y = (int)(*y1 - (double)((*x1 - (_CLIENT_W - 1)) * (*y1 - *y2)) / (*x1 - *x2) + 0.5);
break;
}
case _N:
{
port1_x = (int)(*x1 - *y1 * (double)(*x2 - *x1) / (*y2 - *y1) + 0.5);
port1_y = 0;
break;
}
case _S:
{
port1_x = (int)(*x2 - (double)((*x2 - *x1) * (_CLIENT_H - 1 - *y2)) / (*y1 - *y2) + 0.5);
port1_y = _CLIENT_H - 1;
break;
}
case _WN:
{
port1_x = 0;
port1_y = (int)(*y2 + (double)(*x2 * (*y1 - *y2)) / (*x2 - *x1) + 0.5);
if (port1_y < 0)
{
port1_x = (int)(*x1 - *y1 * (double)(*x2 - *x1) / (*y2 - *y1) + 0.5);
port1_y = 0;
}
break;
}
case _WS:
{
port1_x = 0;
port1_y = (int)(*y2 + (double)(*x2 * (*y1 - *y2)) / (*x2 - *x1) + 0.5);
if (port1_y > _CLIENT_H - 1)
{
port1_x = (int)(*x2 - (double)((*x2 - *x1) * (_CLIENT_H - 1 - *y2)) / (*y1 - *y2) + 0.5);
port1_y = _CLIENT_H - 1;
}
break;
}
case _EN:
{
port1_x = _CLIENT_W - 1;
port1_y = (int)(*y1 - (double)((*x1 - (_CLIENT_W - 1)) * (*y1 - *y2)) / (*x1 - *x2) + 0.5);
if (port1_y < 0)
{
port1_x = (int)(*x1 - *y1 * (double)(*x2 - *x1) / (*y2 - *y1) + 0.5);
port1_y = 0;
}
break;
}
case _ES:
{
port1_x = _CLIENT_W - 1;
port1_y = (int)(*y1 - (double)((*x1 - (_CLIENT_W - 1)) * (*y1 - *y2)) / (*x1 - *x2) + 0.5);
if (port1_y > _CLIENT_H - 1)
{
port1_x = (int)(*x2 - (double)((*x2 - *x1) * (_CLIENT_H - 1 - *y2)) / (*y1 - *y2) + 0.5);
port1_y = _CLIENT_H - 1;
}
break;
}
}
//判斷點1裁剪後的位置
if (port1_x < 0 || port1_x > _CLIENT_W - 1 || port1_y < 0 || port1_y > _CLIENT_H - 1)
{
return false;
}
//剩下的情況點2裁剪後一定在Certain
//裁剪點2
switch (port2)
{
case _W:
{
port2_x = 0;
port2_y = (int)(*y2 - (double)(*x2 * (*y1 - *y2)) / (*x1 - *x2) + 0.5);
break;
}
case _E:
{
port2_x = _CLIENT_W - 1;
port2_y = (int)(*y1 + (double)(((_CLIENT_W - 1) - *x1) * (*y2 - *y1)) / (*x2 - *x1) + 0.5);
break;
}
case _N:
{
port2_x = (int)(*x2 - *y2 * (double)(*x1 - *x2) / (*y1 - *y2) + 0.5);
port2_y = 0;
break;
}
case _S:
{
port2_x = (int)(*x1 + (double)((*x2 - *x1) * (_CLIENT_H - 1 - *y1)) / (*y2 - *y1) + 0.5);
port2_y = _CLIENT_H - 1;
break;
}
case _WN:
{
port2_x = 0;
port2_y = (int)(*y2 - (double)(*x2 * (*y1 - *y2)) / (*x1 - *x2) + 0.5);
if (port2_y < 0)
{
port2_x = (int)(*x2 - *y2 * (double)(*x1 - *x2) / (*y1 - *y2) + 0.5);
port2_y = 0;
}
break;
}
case _WS:
{
port2_x = 0;
port2_y = (int)(*y2 - (double)(*x2 * (*y1 - *y2)) / (*x1 - *x2) + 0.5);
if (port2_y > _CLIENT_H - 1)
{
port2_x = (int)(*x1 + (double)((*x2 - *x1) * (_CLIENT_H - 1 - *y1)) / (*y2 - *y1) + 0.5);
port2_y = _CLIENT_H - 1;
}
break;
}
case _EN:
{
port2_x = _CLIENT_W - 1;
port2_y = (int)(*y1 + (double)(((_CLIENT_W - 1) - *x1) * (*y2 - *y1)) / (*x2 - *x1) + 0.5);
if (port2_y < 0)
{
port2_x = (int)(*x2 - *y2 * (double)(*x1 - *x2) / (*y1 - *y2) + 0.5);
port2_y = 0;
}
break;
}
case _ES:
{
port2_x = _CLIENT_W - 1;
port2_y = (int)(*y1 + (double)(((_CLIENT_W - 1) - *x1) * (*y2 - *y1)) / (*x2 - *x1) + 0.5);
if (port2_y > _CLIENT_H - 1)
{
port2_x = (int)(*x1 + (double)((*x2 - *x1) * (_CLIENT_H - 1 - *y1)) / (*y2 - *y1) + 0.5);
port2_y = _CLIENT_H - 1;
}
break;
}
}
//裁剪成功
*x1 = port1_x;
*y1 = port1_y;
*x2 = port2_x;
*y2 = port2_y;
return true;
}
void main()
{
int x1 = -100;
int y1 = -100;
int x2 = 1000;
int y2 = 500;
std::cout << "裁剪前:" << std::endl;
std::cout << x1 << "---" << y1 << std::endl;
std::cout << x2 << "---" << y2 << std::endl;
bool result = ClipLine(&x1, &y1, &x2, &y2);
if (result)
{
std::cout << "裁剪後:" << std::endl;
std::cout << x1 << "---" << y1 << std::endl;
std::cout << x2 << "---" << y2 << std::endl;
}
else
{
std::cout << "線段不在視口中" << std::endl;
}
system("pause");
}
結果: