(本文由原作者轉自出處,轉載請保留信息)
前言
本教程爲北京交通大學2020年春季學期計算機與信息技術學院開設的計算機圖形學課程準備,使用OpenGL作爲圖形渲染引擎,並使用freeglut創建窗口,處理鍵盤,鼠標輸入等。內容涵蓋從OpenGL環境的配置一直到創建簡單的帶有光源的三維場景。計算機圖形學這門課程相比於之前所學的專業課如web基礎訓練,計算思維導論等,入門門檻較高,需要一些高等數學中空間解析幾何,線性代數的知識;對於沒有圖形編程或者圖像處理基礎的同學來說,需要理解和掌握的新的概念也很多;程序代碼量較大,需要一定編碼能力;加之同學們從學長學姐那邊聽到的各種妖魔化這門課程的言論;因此我這裏出了一個針對這門課程的教程,本配有相關代碼及Visual studio工程;同時也算是作爲學院科協信息部部長的工作。同時也因爲只是針對課程的教程,也會有很多相關計算機圖形學的內容的缺失,有興趣的同學可以自行學習更多內容。
如轉載本教程請保留出處。
什麼是計算機圖形學
簡單地說,計算機圖形學的主要研究內容就是研究如何在計算機中表示圖形、以及利用計算機進行圖形的計算、處理和顯示的相關原理與算法。圖形通常由點、線、面、體等幾何元素和灰度、色彩、線型、線寬等非幾何屬性組成。從處理技術上來看,圖形主要分爲兩類,一類是基於線條信息表示的,如工程圖、等高線地圖、曲面的線框圖等,另一類是明暗圖,也就是通常所說的真實感圖形。
計算機圖形學一個主要的目的就是要利用計算機產生令人賞心悅目的真實感圖形。爲此,必須創建圖形所描述的場景的幾何表示,再用某種光照模型,計算在假想的光源、紋理、材質屬性下的光照明效果。所以計算機圖形學與另一門學科計算機輔助幾何設計有着密切的關係。事實上,圖形學也把可以表示幾何場景的曲線曲面造型技術和實體造型技術作爲其主要的研究內容。同時,真實感圖形計算的結果是以數字圖象的方式提供的,計算機圖形學也就和圖像處理有着密切的關係。
圖形與圖像兩個概念間的區別越來越模糊,但還是有區別的:圖像純指計算機內以位圖形式存在的灰度信息,而圖形含有幾何屬性,或者說更強調場景的幾何表示,是由場景的幾何模型和景物的物理屬性共同組成的。
計算機圖形學的研究內容非常廣泛,如圖形硬件、圖形標準、圖形交互技術、光柵圖形生成算法、曲線曲面造型、實體造型、真實感圖形計算與顯示算法、非真實感繪製,以及計算可視化、計算機動畫、自然景物仿真、虛擬現實等。
(來自wikipedia,更多信息可見這裏)
什麼是OpenGL
OpenGL(全稱:Open Graphics Library,譯名:開放圖形庫或者“開放式圖形庫”)是用於渲染2D、3D矢量圖形的跨語言、跨平臺的應用程序編程接口(API)。這個接口由近350個不同的函數調用組成,用來從簡單的圖形比特繪製複雜的三維景象,常用於CAD、虛擬現實、科學可視化程序和電子遊戲開發。
OpenGL規範由1992年成立的OpenGL架構評審委員會(ARB)維護。ARB由一些對創建一個統一的、普遍可用的API特別感興趣的公司組成。根據OpenGL官方網站,2002年6月的ARB投票成員包括3Dlabs、Apple Computer、ATI Technologies、Dell Computer、Evans & Sutherland、Hewlett-Packard、IBM、Intel、Matrox、NVIDIA、SGI和Sun Microsystems,Microsoft曾是創立成員之一,但已於2003年3月退出。
(以上來自wikipedia,更多信息可見這裏)
本次教程將使用OpenGL4.0,主要在Windows平臺上使用Visual studio,配之以freeglut, glew兩個第三方庫(沒有使用glm數學庫,因爲相關數學方面的東西屬於是考試內容,因此教程中將自行實現所需的數學函數),利用可編程渲染管線實現一個分形三角形的繪製,效果如圖:
據說計算機圖形學課程最讓人頭疼的問題就是配置一個可以編譯和運行OpenGL應用程序的環境。 相傳有同學從課程開始到結束都沒能配出環境,如果找助教幫忙他們會先讓你裝個VS2013再說 。依照前面說的,本教程使用freeglut和glew兩個第三方庫。freeglut是一個用來創建支持OpenGL渲染的窗口,以及能夠處理鍵盤鼠標等輸入的庫,並且使用起來相當簡單(相比於直接使用Windows API創建OpenGL設備上下文(Device Context)和渲染上下文(Render Context),或是其它類似的庫例如glfw)。glew是一個讓你能夠使用現代OpenGL特性的庫(由於微軟公司有自家的產品Direct3D與OpenGL是競爭對手關係,Windows系統自帶的OpenGL API只支持到OpenGL1.1,爲了能使用更新的OpenGL需要glew)。
freeglut和glew的文件我已經打包整合到一起,可以前往百度網盤下載,提取碼: ed1h。下面把它們安裝到你的Visual studio裏面(這個方法適用於VS2015,2017,2019及以上):
如果你電腦是Windows 10系統,那麼會在裏面看到一個叫“10”的文件夾,進入:
這樣就完成了頭文件的安裝
進入OpenGLENV下的lib文件夾,裏面有兩個子文件夾,x86和x64,分別對應32位版本和64位版本的庫文件
將x86文件夾內的兩個.lib文件選中,複製粘貼到C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\um\x86裏面,將x64文件夾內的兩個.lib文件選中,複製粘貼到C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\um\x64裏面(注意路徑,和之前說的一樣,你的版本號可能跟我的不一樣,有多個版本的話可以多個版本里都安裝),就完成了庫文件的安裝
進入OpenGLEnv下的bin文件夾,裏面有兩個子文件夾,System32和SysWOW64,分佈對應64位和32位的運行時動態鏈接庫(沒錯,沒寫反)。將System32子文件夾下的兩個.dll文件複製粘貼到C:\Windows\System32裏面,將SysWOW64子文件裏面的兩個.dll文件複製粘貼到C:\Windows\SysWOW64文件夾裏面即可。這樣就完成了運行庫的安裝。
至此OpenGL開發環境安裝完成,可以試驗一下,創建一些新的C++項目,加入下面的代碼(或者直接下載教程的FirstOpenGL工程)
目前階段不必理解代碼的含義,這只是爲了測試你剛纔的OpenGL環境是否成功安裝,代碼有意地使用了現代OpenGL的特性去實現,如果代碼能夠順利編譯運行看到金色分形三角形,那麼恭喜你,可以正式開始計算機圖形學和OpenGL的學習了。本教程會盡量一直使用這樣單個文件的源代碼形式,便於交作業
#include <Windows.h> //這個頭文件要在opengl的頭文件之前
#include <gl/glew.h> //這個頭文件要在其它opengl的頭文件之前
#include <gl/glut.h>
#include <utility>
#include <string>
#include <vector>
#include <iostream>
#include <algorithm>
#include <ctime>
#include <cstdlib>
#define makeString(x) #x
#pragma comment(lib, "opengl32.lib")
#pragma comment(lib, "freeglut.lib")
#ifdef _WIN64 //如果編譯64位程序
#pragma comment(lib, "glew64.lib")
#else
#pragma comment(lib, "glew32.lib")
#endif
//畫面的寬度和高度(像素)
constexpr int viewWidth = 800;
constexpr int viewHeight = 800;
class GLProgram
{
union {
struct {
GLuint vshader, fshader;
}shaders;
GLuint shadersByIndex[sizeof(shaders) / sizeof(GLuint)];
};
constexpr static int shadersGLTable[] = {GL_VERTEX_SHADER, GL_FRAGMENT_SHADER};
GLuint program;
void createShader(const std::string& code, int index) {
const GLchar* codea[1] = { code.c_str() };
GLuint& shader = shadersByIndex[index];
shader = glCreateShader(shadersGLTable[index]);
if (!shader)
throw std::runtime_error("Can not create shader(glCreateShader)");
glShaderSource(shader, 1, codea, nullptr);
glCompileShader(shader);
GLint res;
glGetShaderiv(shader, GL_COMPILE_STATUS, &res);
if (res == GL_FALSE) {
GLint len;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);
if (len > 0) {
std::string info;
info.resize(len + 1);
int w;
glGetShaderInfoLog(shader, len, &w, (GLchar*)(info.c_str()));
info[w] = 0;
throw std::runtime_error("Shader compilation error:" + info);
}
}
}
public:
constexpr static int INDEX_VERTEX_SHADER = 0;
constexpr static int INDEX_FRAGMENT_SHADER = 1;
GLProgram() {
memset(&shaders, 0, sizeof(shaders));
program = 0;
}
~GLProgram() {
if (shaders.vshader)
glDeleteShader(shaders.vshader);
if (shaders.fshader)
glDeleteShader(shaders.fshader);
if (program)
glDeleteProgram(program);
}
void createVShader(const std::string& code) {
createShader(code, INDEX_VERTEX_SHADER);
}
void createFShader(const std::string& code) {
createShader(code, INDEX_FRAGMENT_SHADER);
}
void createProgram() {
program = glCreateProgram();
if (!program)
throw std::runtime_error("Fail to create opengl program(glCreateProgram)");
glAttachShader(program, shaders.vshader);
glAttachShader(program, shaders.fshader);
glLinkProgram(program);
GLint res;
glGetProgramiv(program, GL_LINK_STATUS, &res);
if (res == GL_FALSE) {
GLint len;
glGetProgramiv(shaders.fshader, GL_INFO_LOG_LENGTH, &len);
if (len > 0) {
std::string info;
info.resize(len + 1);
int w;
glGetProgramInfoLog(shaders.fshader, len, &w, (GLchar*)(info.c_str()));
info[w] = 0;
throw std::runtime_error("Fail to link opengl program:" + info);
}
}
}
void useProgram() const {
if (!program) {
throw std::runtime_error("Trying to use an invalid opengl program");
}
glUseProgram(program);
}
GLuint getProgram() const { return program; }
operator GLuint() const { //對象被強制轉換爲GLuint時調用的函數
return program;
}
operator int() const { return program; }
};
//初始化OpenGL
void initGL() {
GLenum err = glewInit();
if (err != GLEW_OK) {
MessageBox(0, TEXT("初始化GLEW失敗"), TEXT("錯誤"), 0);
exit(1);
}
if (!GLEW_VERSION_4_0) { //檢查OpenGL4.0支持
MessageBox(0, TEXT("本計算機未支持OpenGL4.0"), TEXT("錯誤"), 0);
exit(1);
}
glEnable(GL_FRAMEBUFFER_SRGB); //開啓伽馬校正
}
struct Point {
float x, y;
Point middle(const Point& p) const { //與另一個點的中點
return Point{ (x + p.x) / 2, (y + p.y) / 2 };
}
};
struct Triangle {
Point points[3];
};
struct Color {
float r, g, b, a;
};
//下面是頂點着色器和片段着色器的代碼,功能非常基礎,只是爲了測試着色器功能是否正常
static const char* vertexShaderCode = makeString(
#version 400\n
layout(location = 0) in vec2 pos;
out vec4 vo_pos;
void main() {
gl_Position = vec4(pos.x, pos.y, 0.0, 1.0);
}
);
static const char* fragmentShaderCode = makeString(
#version 400\n
uniform vec4 color;
void main() {
gl_FragColor = color;
}
);
class RenderState {
unsigned long long tick; //計時數
//TODO: 將繪製需要的資源定義在這裏
GLProgram program;
constexpr static int MAX_DEPTH = 9;
GLuint VAO, VBO;
//每一幀邏輯數據更新的工作
void update() {
tick++;
}
public:
RenderState() {
tick = 0;
}
//在第一次繪製之前的工作
void preScene() {
printf("PreScene");
program.createVShader(vertexShaderCode);
program.createFShader(fragmentShaderCode);
program.createProgram();
program.useProgram();
//這裏創建一個VAO和VBO可以儲存一個三角形的三個頂點
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glVertexAttribPointer(0, 2, GL_FLOAT, true, 2 * sizeof(float), nullptr);
glEnableVertexAttribArray(0);
//實際上繪製完後應該調用glDeleteBuffer等銷燬資源...然而glut窗口關閉時是直接退出的,沒有機會銷燬資源...
//就先這樣了
}
void recursiveDraw(const Triangle& tr, int depth = 0) {
if (depth == MAX_DEPTH)
return;
//更新頂點緩衝的數據:將頂點數據換成當前三角形的三個頂點
glBufferData(GL_ARRAY_BUFFER, sizeof(tr), &tr, GL_DYNAMIC_DRAW);
glUniform4f(glGetUniformLocation(program, "color"), 0.88, 0.76, 0.42, 1.0); //金色
glDrawArrays(GL_TRIANGLES, 0, 3);
//將中間掏空
Point m1 = tr.points[0].middle(tr.points[1]);
Point m2 = tr.points[0].middle(tr.points[2]);
Point m3 = tr.points[1].middle(tr.points[2]);
Triangle m = { m1, m3, m2 }; //由三邊中點構成的三角形
glBufferData(GL_ARRAY_BUFFER, sizeof(m), &m, GL_DYNAMIC_DRAW);
glUniform4f(glGetUniformLocation(program, "color"), 0.0, 0.0, 0.0, 0.0);
glDrawArrays(GL_TRIANGLES, 0, 3);
//繪製下一層:
recursiveDraw(Triangle{ tr.points[0], m1, m2 }, depth + 1);
recursiveDraw(Triangle{ m1, tr.points[1], m3 }, depth + 1);
recursiveDraw(Triangle{ m2, m3, tr.points[2] }, depth + 1);
}
//每一幀繪製的工作
void render() {
update(); //先更新邏輯,後面進行渲染
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); //設置透明色爲背景色
recursiveDraw(Triangle{ Point{0.0, 0.882}, Point{-1.0, -0.85}, Point{1.0, -0.85} });
glutSwapBuffers();
glutPostRedisplay();
}
}state;
void render() {
state.render();
}
int main(int argc, char* argv[]) {
srand(time(NULL));
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE); //RGBA顏色模式,雙緩衝
glutInitWindowSize(viewWidth, viewHeight); //窗口的寬和高
glutCreateWindow("OpenGL Demo");
//下面兩行代碼注意一定放在glutCreateWindow的後面才行,否則會因爲沒有創建opengl上下文而失敗。
glViewport(0, 0, viewWidth, viewHeight);
initGL();
state.preScene();
glutDisplayFunc(render);
glutMainLoop();
return 0;
}