計算機圖形學作業( 二):使用Bresenham算法畫直線和圓,並使用光柵化算法填充三角形

計算機圖形學作業( 二):使用Bresenham算法畫直線和圓,並使用光柵化算法填充三角形

Bresenham算法畫直線

原理

首先,觀察下圖:

在這裏插入圖片描述

設一條直線爲 y=mx+B\ y=mx+B,那麼上圖圖中的參數爲:

在這裏插入圖片描述

然後觀察下圖:

在這裏插入圖片描述

在圖中,紅色點爲當前的點,我們要計算出下一個點是取高位的黃色點,還是低位的黃色點,就要比較這兩個點誰距離直線最近,結合之前的圖,可得 dupper\ d_{upper} dlower\ d_{lower}的大小 。

在這裏插入圖片描述

如果 dlowerdupper>0\ d_{lower}-d_{upper}>0,則取上方的黃色點,否則取下方的黃色點。
設定一個參數 pi\ p_i,如下圖:

在這裏插入圖片描述

經過計算可得:

在這裏插入圖片描述

且有以下關係:

在這裏插入圖片描述

算法

繪製斜率在[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算法繪製;否則,按以下方法繪製直線:

  1. 找出這兩點誰的橫座標較小,然後把橫座標較小的點平移到原點,橫座標較大的點平移到對應位置。
  2. 對橫座標較大的點做一系列變換(關於直線y=x或y=0等對稱),使得兩點斜率在[0,1]之內
  3. 使用Bresenham算法繪製出直線,然後再把這條直線作步驟2相反的一系列變換即可。

結果

畫出三角形並光柵化後,結果如下圖所示:

在這裏插入圖片描述

使用光柵化算法填充三角形

光柵化算法有三種:

  1. Edge-walking
  2. Edge-equation
  3. Barycentric-coordinate based

我使用的算法是Edge-equation

算法僞代碼

在這裏插入圖片描述

算法解釋

  1. 根據三角形的三個點,計算三條邊的一般式方程Ax+By+C=0,其中A=Y2-Y1,B=X1-X2,C=X2Y1-X1Y2。
  2. 將三條邊“中心化”,取三角形的中點,即((X1+X2+X3)/3, (Y1+Y2+Y3)/3),分別代入三個直線方程,若最終結果小於0,則把該方程的三個參數A、B、C分別乘以-1。
  3. 計算三角形外接矩陣,該矩陣的兩條邊分別與X軸Y軸平行。即在三角形的三個頂點中,找到最小的x值leftX和最大的x值rightX,那麼外接矩陣的橫邊就從leftX開始到rightX結束,同理豎邊從bottomY開始到topY結束。
  4. 遍歷外接矩陣的每一個點,每個點都代入三個直線方程,若結果都大於0,則該點在三角形內,畫出該點。

結果

在這裏插入圖片描述

Bresenham算法畫圓

原理

我是用八分法畫圓,因爲圓上的一點,可以有另外在圓上的7個對稱點,我們只需要畫八分之一的圓。如下圖:

在這裏插入圖片描述

我們只需要畫第一象限的上半部分的八分之一圓。畫圖時,計算出的點作對稱變換就可得到其餘7個對稱點,最後就能獲得一整個圓的所有點。用GL_POINTS即可畫出圓。

具體推導方法我是參考了這篇博客https://blog.csdn.net/mayh554024289/article/details/44781531,故不再贅述。

算法

我們假設圓心在原點(若不在原點,只需要作簡單平移變換即可),並且主要畫第一象限的上半部分的八分之一圓,其餘部分用對稱方法解決。

  1. 取初始值x=0, y=radius, endX=radius/(float)sqrt(2), d=1.25-radius
  2. 如果d<=0,令(x+1, y)取代(x,y),並且d=d+2x+2;否則令(x+1, y-1)取代(x,y),並且d=d+2(x-y)+5。
  3. 畫出當前(x,y)點和其餘7個對稱點。
  4. 當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;
		}
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章