計算機圖形學作業( 二):使用Bresenham算法畫直線和圓,並使用光柵化算法填充三角形
Bresenham算法畫直線
原理
首先,觀察下圖:
設一條直線爲,那麼上圖圖中的參數爲:
然後觀察下圖:
在圖中,紅色點爲當前的點,我們要計算出下一個點是取高位的黃色點,還是低位的黃色點,就要比較這兩個點誰距離直線最近,結合之前的圖,可得和的大小 。
如果,則取上方的黃色點,否則取下方的黃色點。
設定一個參數,如下圖:
經過計算可得:
且有以下關係:
算法
繪製斜率在[0,1]範圍內的直線算法如下:
拓展
由於使用Bresenham算法只能繪製斜率在[0,1]範圍內的直線,那麼要繪製其它斜率範圍的直線,就要進行座標平移、對稱變換。
首先,我在代碼中輸入三個正整數點座標,由於在OpenGL中座標範圍是 -1.0f~1.0f,所以我定義了一個函數normalize
,負責把正整數轉化爲 -1.0f~1.0f 的浮點數。因爲我的窗口大小是800*800,所以只需要寫成以下形式:
float normalize(int input) {
float result = float(input) / 800;
return result;
}
然後,對於給出的兩個點,我們要繪製這兩點的直線,首先計算出它們的斜率,若斜率不存在,則固定橫座標,循環縱座標畫出一個個點;若斜率在[0,1]範圍,則以Bresenham算法繪製;否則,按以下方法繪製直線:
- 找出這兩點誰的橫座標較小,然後把橫座標較小的點平移到原點,橫座標較大的點平移到對應位置。
- 對橫座標較大的點做一系列變換(關於直線y=x或y=0等對稱),使得兩點斜率在[0,1]之內
- 使用Bresenham算法繪製出直線,然後再把這條直線作步驟2相反的一系列變換即可。
結果
畫出三角形並光柵化後,結果如下圖所示:
使用光柵化算法填充三角形
光柵化算法有三種:
- Edge-walking
- Edge-equation
- Barycentric-coordinate based
我使用的算法是Edge-equation
算法僞代碼
算法解釋
- 根據三角形的三個點,計算三條邊的一般式方程Ax+By+C=0,其中A=Y2-Y1,B=X1-X2,C=X2Y1-X1Y2。
- 將三條邊“中心化”,取三角形的中點,即((X1+X2+X3)/3, (Y1+Y2+Y3)/3),分別代入三個直線方程,若最終結果小於0,則把該方程的三個參數A、B、C分別乘以-1。
- 計算三角形外接矩陣,該矩陣的兩條邊分別與X軸Y軸平行。即在三角形的三個頂點中,找到最小的x值leftX和最大的x值rightX,那麼外接矩陣的橫邊就從leftX開始到rightX結束,同理豎邊從bottomY開始到topY結束。
- 遍歷外接矩陣的每一個點,每個點都代入三個直線方程,若結果都大於0,則該點在三角形內,畫出該點。
結果
Bresenham算法畫圓
原理
我是用八分法畫圓,因爲圓上的一點,可以有另外在圓上的7個對稱點,我們只需要畫八分之一的圓。如下圖:
我們只需要畫第一象限的上半部分的八分之一圓。畫圖時,計算出的點作對稱變換就可得到其餘7個對稱點,最後就能獲得一整個圓的所有點。用GL_POINTS即可畫出圓。
具體推導方法我是參考了這篇博客https://blog.csdn.net/mayh554024289/article/details/44781531,故不再贅述。
算法
我們假設圓心在原點(若不在原點,只需要作簡單平移變換即可),並且主要畫第一象限的上半部分的八分之一圓,其餘部分用對稱方法解決。
- 取初始值x=0, y=radius, endX=radius/(float)sqrt(2), d=1.25-radius
- 如果d<=0,令(x+1, y)取代(x,y),並且d=d+2x+2;否則令(x+1, y-1)取代(x,y),並且d=d+2(x-y)+5。
- 畫出當前(x,y)點和其餘7個對稱點。
- 當x<=endX,重複步驟2~3。
結果
畫出的圓如下所示:
代碼
#include <iostream>
#include <algorithm>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
CONST INT MAX_LENGTH = 240000;
CONST INT WINDOW_SIZE = 800;
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
void getLineVertex(int x1, int y1, int x2, int y2, float vertices[MAX_LENGTH], int* length);
void edge_equations(int point1[2], int point2[2], int point3[2], float vertices[MAX_LENGTH], int* length);
void getCircleVertex(int originPointX, int originPointY, int radius, float vertices[MAX_LENGTH], int* length);
float normalize(int input);
void initImGUI(GLFWwindow* window);
void createAndRenderLineImGUI(GLFWwindow* window, bool *show_window, int point1[2], int point2[2], int point3[2], bool *isDrawLine);
void createAndRenderCircleImGUI(GLFWwindow* window, bool *show_window, int point1[2], int *radius, bool *isDrawLine);
int myMin(int a, int b, int c);
int myMax(int a, int b, int c);
//頂點着色器可以向片段着色器傳數據
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
"FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\0";
int main() {
//初始化opengl窗口和配置
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(WINDOW_SIZE, WINDOW_SIZE, "LearnOpenGL", NULL, NULL);
if (window == NULL) {
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
initImGUI(window);
//頂點着色器
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
//片段着色器
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
//着色器程序
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
bool show_window = true;
bool isDrawLine = true;
int point1[2] = { -150, -150 };
int point2[2] = { 150, -150 };
int point3[2] = { 0, 150 };
int radius = 100;
int originPoint[2] = { 0, 0 };
while (!glfwWindowShouldClose(window)) {
float vertices[MAX_LENGTH] = { 0 }; // 注意索引從0開始!
int length = 0;
//獲取線段點數據
if (isDrawLine) {
getLineVertex(point1[0], point1[1], point2[0], point2[1], vertices, &length);
getLineVertex(point2[0], point2[1], point3[0], point3[1], vertices, &length);
getLineVertex(point1[0], point1[1], point3[0], point3[1], vertices, &length);
edge_equations(point1, point2, point3, vertices, &length);
}
else {
//獲取圓數據
getCircleVertex(originPoint[0], originPoint[1], radius, vertices, &length);
}
unsigned int VAO;
unsigned int VBO;
//必須先綁定VA0
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
//再綁定VBO
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//再設置屬性
//位置屬性
//屬性位置值爲0的頂點屬性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
processInput(window);
//使用着色器程序
glUseProgram(shaderProgram);
glfwPollEvents();
//清除屏幕
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//畫imGUI
if (isDrawLine) {
createAndRenderLineImGUI(window, &show_window, point1, point2, point3, &isDrawLine);
}
else {
createAndRenderCircleImGUI(window, &show_window, originPoint, &radius, &isDrawLine);
}
//畫線段
//length是vertices的長度,而vertices三個元素爲一個點
//glBindVertexArray(VAO);
glDrawArrays(GL_POINTS, 0, length / 3);
glfwSwapBuffers(window);
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
}
glfwTerminate();
return 0;
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
glViewport(0, 0, width, height);
}
void processInput(GLFWwindow* window) {
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
glfwSetWindowShouldClose(window, true);
}
}
void getLineVertex(int x1, int y1, int x2, int y2, float vertices[MAX_LENGTH], int* length) {
int x, y, endx, endy;
float slope; //斜率
bool isSlopeExist = true;
//確定起初點
if (x1 < x2) {
x = x1;
y = y1;
endx = x2;
endy = y2;
}
else {
x = x2;
y = y2;
endx = x1;
endy = y1;
}
//計算斜率
if (x == endx) {
//斜率不存在
isSlopeExist = false;
}
else {
slope = ((float)endy - (float)y) / ((float)endx - (float)x);
}
//初始點
vertices[*length] = normalize(x);
(*length)++;
vertices[*length] = normalize(y);
(*length)++;
vertices[*length] = 0.0f;
(*length)++;
if (isSlopeExist == false) {
//斜率不存在時
int first;
int last;
if (y < endy) {
first = y;
last = endy;
}
else {
first = endy;
last = y;
}
for (int i = first; i <= last; i++) {
vertices[*length] = normalize(x);
(*length)++;
vertices[*length] = normalize(i);
(*length)++;
vertices[*length] = 0.0f;
(*length)++;
}
}
else {
//對稱變換前,先把起始點移到原點,其它點移到對應位置
int moveX = x;
int moveY = y;
x = x - moveX;
y = y - moveY;
endx = endx - moveX;
endy = endy - moveY;
//對稱變換
//利用對稱變換,將畫其他斜率線段轉化爲畫斜率 0 <= <= 1的線段
if (slope > 1) {
//第一象限上半部分
//起點不動,互換終點的x y
int temp = endy;
endy = endx;
endx = temp;
}
else if (slope < 0 && slope >= -1) {
//第二象限上半部分
//起點不動,終點的y變相反數
endy = -endy;
}
else if (slope < -1) {
//第二象限下半部分
//起點不動,終點的y變相反數,然後互換終點的x y
endy = -endy;
int temp = endy;
endy = endx;
endx = temp;
}
int deltaX = abs(endx - x);
int deltaY = abs(endy - y);
int p = 2 * deltaY - deltaX;
//只能畫斜率 0 <= <= 1的線段
while (x <= endx) {
if (p <= 0) {
x = x + 1;
y = y;
p = p + 2 * deltaY;
}
else {
x = x + 1;
y = y + 1;
p = p + 2 * deltaY - 2 * deltaX;
}
//利用對稱變換,將斜率 0 <= <= 1的線段上的點轉化爲原來斜率線段上的點
if (slope > 1) {
//第一象限上半部分
int temp = y;
y = x;
x = temp;
}
else if (slope < 0 && slope >= -1) {
//第二象限上半部分
//起點不動,終點的y變相反數
y = -y;
}
else if (slope < -1) {
//第二象限下半部分
//起點不動,互換x y, y變相反數
int temp = y;
y = x;
x = temp;
y = -y;
}
//對稱變換後,先把起始點移到原本位置,然後畫出來
x = x + moveX;
y = y + moveY;
vertices[*length] = normalize(x);
(*length)++;
vertices[*length] = normalize(y);
(*length)++;
vertices[*length] = 0.0f;
(*length)++;
//畫圖後,再恢復起始點
x = x - moveX;
y = y - moveY;
//利用對稱變換,恢復原來的點到斜率 0 <= <= 1的線段上,繼續計算下一個點
if (slope > 1) {
//第一象限上半部分
int temp = y;
y = x;
x = temp;
}
else if (slope < 0 && slope >= -1) {
//第二象限上半部分
//起點不動,終點的y變相反數
y = -y;
}
else if (slope < -1) {
//第二象限下半部分
//起點不動,y變相反數,互換x y
y = -y;
int temp = y;
y = x;
x = temp;
}
}
}
}
void edge_equations(int point1[2], int point2[2], int point3[2], float vertices[MAX_LENGTH], int* length) {
//求3條直線一般方程Ax+By+C=0的參數
//序號12爲point1和point2的參數,序號23爲point2和point3的參數,序號13爲point1和point3的參數
int A12, B12, C12;
int A23, B23, C23;
int A13, B13, C13;
A12 = point2[1] - point1[1];
B12 = point1[0] - point2[0];
C12 = point2[0] * point1[1] - point1[0] * point2[1];
A23 = point3[1] - point2[1];
B23 = point2[0] - point3[0];
C23 = point3[0] * point2[1] - point2[0] * point3[1];
A13 = point3[1] - point1[1];
B13 = point1[0] - point3[0];
C13 = point3[0] * point1[1] - point1[0] * point3[1];
//將三角形中點帶入各直線,若結果小於0,則將參數A、B、C都乘以-1。
float middleX = ((float)point1[0] + (float)point2[0] + (float)point3[0]) / 3;
float middleY = ((float)point1[1] + (float)point2[1] + (float)point3[1]) / 3;
float temp = A12 * middleX + B12 * middleY + C12;
if (temp < 0) {
A12 = -A12;
B12 = -B12;
C12 = -C12;
}
temp = A23 * middleX + B23 * middleY + C23;
if (temp < 0) {
A23 = -A23;
B23 = -B23;
C23 = -C23;
}
temp = A13 * middleX + B13 * middleY + C13;
if (temp < 0) {
A13 = -A13;
B13 = -B13;
C13 = -C13;
}
//計算兩條邊分別於x軸和y軸平行的外接矩形
int leftX = myMin(point1[0], point2[0], point3[0]);
int rightX = myMax(point1[0], point2[0], point3[0]);
int topY = myMax(point1[1], point2[1], point3[1]);
int bottomY = myMin(point1[1], point2[1], point3[1]);
//遍歷矩形,處理後的一般方程,三角形中的任意一個點代入3條曲線,都會使Ax+By+C的結果都大於0
for (int i = leftX; i <= rightX; i = i + 1) {
for (int j = bottomY; j <= topY; j = j + 1) {
if (A12 * i + B12 * j + C12 > 0 &&
A23 * i + B23 * j + C23 > 0 &&
A13 * i + B13 * j + C13 > 0) {
//防止數組越界
if ((*length) >= MAX_LENGTH - 3) break;
vertices[(*length)++] = normalize(i);
vertices[(*length)++] = normalize(j);
vertices[(*length)++] = 0.0f;
}
}
}
}
void getCircleVertex(int originPointX, int originPointY, int radius, float vertices[MAX_LENGTH], int* length) {
//移到原點,記錄偏移量
int moveX = originPointX;
int moveY = originPointY;
int x = 0;
int y = radius;
int end = (int)(radius * 1.0 / (float)sqrt(2));
float d = 1.25 - radius;
//加入初始點及對稱的其它7個點
while (x <= end) {
//加入新點及對稱的其它7個點
vertices[(*length)++] = normalize(x + moveX);
vertices[(*length)++] = normalize(y + moveY);
vertices[(*length)++] = 0.0f;
vertices[(*length)++] = normalize(y + moveX);
vertices[(*length)++] = normalize(-x + moveY);
vertices[(*length)++] = 0.0f;
vertices[(*length)++] = normalize(-x + moveX);
vertices[(*length)++] = normalize(-y + moveY);
vertices[(*length)++] = 0.0f;
vertices[(*length)++] = normalize(-y + moveX);
vertices[(*length)++] = normalize(x + moveY);
vertices[(*length)++] = 0.0f;
vertices[(*length)++] = normalize(y + moveX);
vertices[(*length)++] = normalize(x + moveY);
vertices[(*length)++] = 0.0f;
vertices[(*length)++] = normalize(x + moveX);
vertices[(*length)++] = normalize(-y + moveY);
vertices[(*length)++] = 0.0f;
vertices[(*length)++] = normalize(-y + moveX);
vertices[(*length)++] = normalize(-x + moveY);
vertices[(*length)++] = 0.0f;
vertices[(*length)++] = normalize(-x + moveX);
vertices[(*length)++] = normalize(y + moveY);
vertices[(*length)++] = 0.0f;
vertices[(*length)++] = normalize(x + moveX);
vertices[(*length)++] = normalize(y + moveY);
vertices[(*length)++] = 0.0f;
if (d <= 0) {
x = x + 1;
y = y;
d = d + 2 * x + 2;
}
else {
x = x + 1;
y = y - 1;
d = d + 2 * (x - y) + 5;
}
}
}
float normalize(int input) {
float result = float(input) / WINDOW_SIZE;
return result;
}
void initImGUI(GLFWwindow* window) {
//創建並綁定ImGui
const char* glsl_version = "#version 130";
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO(); (void)io;
ImGui::StyleColorsDark();
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init(glsl_version);
}
void createAndRenderLineImGUI(GLFWwindow* window, bool *show_window, int point1[2], int point2[2], int point3[2], bool *isDrawLine) {
//創建imgui
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
ImGui::Begin("Edit points", show_window, ImGuiWindowFlags_MenuBar);
//ImGui::ColorEdit3("top color", (float*)&topColor);
ImGui::Text("input 3 points");
ImGui::SliderInt2("point1", point1, -180, 180);
ImGui::SliderInt2("point2", point2, -180, 180);
ImGui::SliderInt2("point3", point3, -180, 180);
if (ImGui::Button("Draw Circle"))
(*isDrawLine) = false;
ImGui::End();
ImGui::Render();
int display_w, display_h;
glfwMakeContextCurrent(window);
glfwGetFramebufferSize(window, &display_w, &display_h);
glViewport(0, 0, display_w, display_h);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}
void createAndRenderCircleImGUI(GLFWwindow* window, bool *show_window, int point[2], int *radius, bool *isDrawLine) {
//創建imgui
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
ImGui::Begin("Edit points", show_window, ImGuiWindowFlags_MenuBar);
ImGui::Text("input 3 points");
ImGui::SliderInt2("point1", point, -WINDOW_SIZE, WINDOW_SIZE);
ImGui::SliderInt("point1", radius, 0, WINDOW_SIZE);
if (ImGui::Button("Draw Line"))
(*isDrawLine) = true;
ImGui::End();
ImGui::Render();
int display_w, display_h;
glfwMakeContextCurrent(window);
glfwGetFramebufferSize(window, &display_w, &display_h);
glViewport(0, 0, display_w, display_h);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}
int myMin(int a, int b, int c) {
if (a < b) {
if (a < c) {
return a;
}
else {
return c;
}
}
else {
if (b < c) {
return b;
}
else {
return c;
}
}
}
int myMax(int a, int b, int c) {
if (a > b) {
if (a > c) {
return a;
}
else {
return c;
}
}
else {
if (b > c) {
return b;
}
else {
return c;
}
}
}