【LWJGL官方教程】渲染

原文:https://github.com/SilverTiger/lwjgl3-tutorial/wiki/Rendering
譯註:並沒有逐字逐句翻譯一切,只翻譯了自己覺得有用的部分。另外此翻譯僅供參考,請一切以原文爲準。代碼例子文件鏈接什麼的都請去原鏈接查找。括號裏的內容一般也是譯註,供理解參考用。總目錄傳送門:http://blog.csdn.net/zoharwolf/article/details/49472857

這次教程終於要用OpenGL 3.2核心profile來做渲染了。
注意源代碼也提供了OpenGL 2.1的一個版本。

Creating the context 創建上下文

在我們開始之前,先要告訴GLFW要用的是OpenGL 3.2核心profile的上下文,用以下代碼就可以:

glfwDefaultWindowHints();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
long window = glfwCreateWindow(width, height, title, NULL, NULL);

我們先設置窗口的hint一律爲默認,用glfwDefaultWindowHints()即可。這樣做,防止之前已經用其他hint創建過其他窗口影響到我們這次的創建。
GLFW_CONTEXT_VERSION_MAJOR和GLFW_CONTEXT_VERSION_MINOR顧名思義,就是要告訴GLFW創建的是3.2版本的上下文。
在GLFW_OPENGL_PROFILE,我們指定要使用核心功能。如果你想用低於3.2版本的OpenGL,你需要指定的是GLFW_OPENGL_ANY_PROFILE,默認其實就是這個值。
GLFW_OPENGL_FORWARD_COMPAT這個hint指定OpenGL的上下文是否應該向前兼容,如果設定成TRUE,它會停用所有的廢棄功能。如果是用OpenGL3.0版本以下,請無視這個hint。

如果你的圖形卡不支持 OpenGL 3.2版本,創建的窗口會是NULL,如果那樣的話,試着用以下代碼創建一個傳統的OpenGL 2.1上下文。

glfwDefaultWindowHints();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
long window = glfwCreateWindow(width, height, title, NULL, NULL);

Vertex Array Objects 頂點數組對象

現在,我們有了窗口,窗口裏也有我們要的上下文了,可以開始初始化渲染。
首先應該創建一個頂點數組對象,簡稱VAO(Vertex Array Object),它用來儲存所有的鏈接,這些鏈接用來連結你的頂點緩衝對象和屬性們。所以,記得要最先創建VAO然後綁定它。

int vao = glGenVertexArrays();
glBindVertexArray(vao);

你能在LWJGL3的GL30類裏找到這兩個方法。因爲版本是3,所以如果你用的是OpenGL2.1上下文,就別用它們了。

Vertex Buffer Objects 頂點緩衝對象

在老式的固定功能管線裏,你只能在每次glBegin(target)和glEnd()間的渲染調用裏傳遞你的頂點(指老版本的OpenGL的機理),但是現代的方法卻是把它們放在頂點緩衝對象VBO(Vertext Buffer Object)裏。
VBO用來儲存所有你GPU裏的頂點數據。LWJGL裏,你需要創建一個緩衝將頂點傳到GPU。在我們的簡單例子裏,就用入門教程裏的那個三角形。創建緩衝的話,就用LWJGL裏的BufferUtil來創建一個合適的FloatBuffer。

注意,爲了方便起見,頂點是逆時針排序的。

FloatBuffer vertices = BufferUtils.createFloatBuffer(3 * 6);
vertices.put(-0.6f).put(-0.4f).put(0f).put(1f).put(0f).put(0f);
vertices.put(0.6f).put(-0.4f).put(0f).put(0f).put(1f).put(0f);
vertices.put(0f).put(0.6f).put(0f).put(0f).put(0f).put(1f);
vertices.flip();

別忘了來一下vertices.flip()!這很重要,因爲不這樣的話你JVM會崩出一個EXCEPTION_ACCESS_VIOLATION。
創建緩衝之後,把它上傳到GPU。但是在那之前,我們需要先創建並綁定一個VBO。

int vbo = glGenBuffers();
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);

這樣我們有了一個存在GPU上的VBO。需要注意我們用的是一個交錯VBO,既有頂點數據又有色彩數據。我們馬上就會看到怎樣指定數據了。

其實也可以用兩個VBO分別存頂點和色彩數據。

Shaders 着色器(一般就直接稱之爲shader了)

初始化的下一步是創建和編譯shader。在OpenGL裏,用的是GLSL(OpenGL Shading Language),跟C語言略像。
本教程我們就用兩個簡單的shader,一個頂點shader,一個片斷shader,在每個shader程序裏一般也都要有這兩個shader。

Vertex Shaders 頂點shader

頂點shader用每個頂點和它的屬性去計算最終的頂點位置(如果以後用鑲嵌shader或幾何shader的話,頂點shader可能僅僅是計算一個更新後的頂點位置)。它也傳遞片段shader需要的數據,比如色彩和紋理座標。
然後,來看看我們的shader例子。

#version 150 core

in vec3 position;
in vec3 color;

out vec3 vertexColor;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
    vertexColor = color;
    mat4 mvp = projection * view * model;
    gl_Position = mvp * vec4(position, 1.0);
}

第一行指定使用的GLSL版本。OpenGL 3.2使用GLSL版本是1.50,OpenGL 2.1用的版本是1.20。想快遞查找所有的對應版本,看這篇教程的結尾處表格。
in關鍵字表示這些變量是從你的程序代碼裏傳進來的,在這裏是兩個三元向量,保存位置和色彩用,具體值由每個頂點提供。
out變量將會傳到下一個shader去,在這裏也就是要傳到片段shader,值也是由每個頂點提供。
最後,uniform變量是全局GLSL變量,也由程序代碼傳來,區別是,它們在每個頂點裏的值相同。

最後看main方法,在頂點shader裏,你實際上只需要設置gl_Position來決定最終的頂點位置。
在本例中,我們有out變量vertexColor,所以我們得告訴shader變量裏面是什麼。
我們還有model、view、projection矩陣用來計算MVP矩陣,注意你必須用反順序計算它們。(MVP矩陣,在本教程後面會詳細介紹)

最後我們用MVP矩陣乘以position,得到最終的頂點位置。

如果你用OpenGL 2.1,用以下shader代替上面的。

#version 120attribute vec3 position;
attribute vec3 color;

varying vec3 vertexColor;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
    vertexColor = color;
    mat4 mvp = projection * view * model;
    gl_Position = mvp * vec4(position, 1.0);
}

這shader和其他頂點shader相似,只是關鍵字略有變化。你可以把attribute就當成in來看,它們都來自程序代碼。varying就是out,傳到下一個shader。

Fragment Shaders 片段shader

通過頂點shader,頂點們被轉換爲基本的象素點,這些象素被稱爲片段。片段shader會計算最後每個片段要輸出的色彩。

#version 150 core

in vec3 vertexColor;

out vec4 fragColor;

void main() {
    fragColor = vec4(vertexColor, 1.0);
}

注意in變量和頂點shader中的out變量有同樣的名字。
out變量用來儲存片段的輸出色彩。
main方法中,只需要傳遞轉換後的頂點顏色到out變量裏,然後out變量輸出片段色彩。

如果你用OpenGL 2.1,用以下shader替代上面的。

#version 120
varying vec3 vertexColor;

void main() {
    gl_FragColor = vec4(vertexColor, 1.0);
}

你會看到其實跟其他片段shader幾乎一樣。
varying變量就是in變量,也跟之前的頂點shader裏的varying變量名字相同。
傳統OpenGL 2.1裏不能用out變量,所以你必須用內置變量gl_FragColor來輸出色彩。

Compiling Shaders 編譯shader

現在我們瞭解這些shader怎樣工作了,我們最後創建並編譯它們。
和之前處理東西的流程相似,需要調用glCreateShader(type)來創建。之後設定shader源並編譯它。shader源就是連在一起的GLSL代碼,可以直接寫成一個string,但是注意裏面要有/n來換行,至少在聲明版本的那行代碼後必須要換行。

int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, vertexSource);
glCompileShader(vertexShader);

int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, fragmentSource);
glCompileShader(fragmentShader);

最好要檢查一下編譯是否成功,用shader info log來看。

int status = glGetShaderi(shader, GL_COMPILE_STATUS);
if (status != GL_TRUE) {
    throw new RuntimeException(glGetShaderInfoLog(shader));
}

Shader Programs shader程序

編譯後,還差最後一步就可以渲染了。必須把它和shader程序連接在一起。
現在猜猜要怎樣處理shader程序,對,調用glCreateProgram()。然後把你的shader們掛在程序上連結在一起。

int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glBindFragDataLocation(shaderProgram, 0, "fragColor");
glLinkProgram(shaderProgram);

你可能注意到了,glBindFragDataLocation(shaderProgram, 0, “fragColor”)這句之前沒有解釋過,這是因爲這句可寫可不寫,因爲我們在片段shader裏只有一個out變量,所以默認它就會被綁定一個序號0。
這命令仍然只在GL30裏纔有效,所以用OpenGL2.1上下文的就別寫了。
連結程序後,用程序的info log檢查一下是否成功。

int status = glGetProgrami(shaderProgram, GL_LINK_STATUS);
if (status != GL_TRUE) {
    throw new RuntimeException(glGetProgramInfoLog(shaderProgram));
}

之後,可以使用shader程序了。

glUseProgram(shaderProgram);

Specify Vertex Attributes 指定頂點屬性

在本教程一開始,我們把頂點和色彩數據放進了VBO,但是我們還沒有告訴程序如何使用這些數據。可以用頂點屬性來完成這項工作。設置一個頂點屬性需要三步:首先取到屬性位置,然後激活它,最後指向頂點屬性。

int floatSize = 4;

int posAttrib = glGetAttribLocation(shaderProgram, "position");
glEnableVertexAttribArray(posAttrib);
glVertexAttribPointer(posAttrib, 3, GL_FLOAT, false, 6 * floatSize, 0);

int colAttrib = glGetAttribLocation(shaderProgram, "color");
glEnableVertexAttribArray(colAttrib);
glVertexAttribPointer(colAttrib, 3, GL_FLOAT, false, 6 * floatSize, 3 * floatSize);

剛開始的兩個方法如其字面意思,之後的glVertexAttribPointer(location, size, type, normalized, stride, offset)需要解釋一下。
location不言而喻,就是我們剛得到的屬性位置;
size是我們需要告訴程序每個頂點的數據包含了多少值;
type指明屬性用哪種數據類型,本例中是float類型;
normailized是你是否想要程序標準化這些值,不過一般這裏都用false,更多關於標準化的詳細內容可以參照這裏
stride和offset,這個很重要,你需要傳入的是字節數。stride指的是在相鄰的兩個頂點屬性之間隔了幾個字節,本例中每個頂點有六個float數值,所以stride值爲 6*一個float佔字節數= 6*4 = 24。offset則指定了第一個頂點屬性的偏移量,在我們的VBO裏,頂點位置數據是每個頂點六數值中一開始的那個數值,所以偏移量就是0。而三個位置數值之後纔是顏色數據,所以顏色屬性的依稀量是3*一個float佔字節數= 3*4 = 12。

Set uniform variables 設置uniform變量

初始化完成了。最後就是設置uniform變量了。
本教程我們有三個矩陣作爲uniform變量,但是你也可以傳其他類型的數據。如果你不曉得怎樣做向量和矩陣的數學計算,你應該看看這裏這裏,或者參考裏提到的鏈接。(反正全是英文,不如去百度搜搜中文的,其實就是些線代的基本知識)
但是其實如果你不想的話,你並不需要自己去實現這些東西,因爲在本教程裏已經有非常基礎的向量和矩陣類提供了,你可以在這裏看看它們。
想要一個更完整的數學計算庫,你應該看看Java OpenGL Math Library(JOML)。(親自試用了過了,這個JOML裏的矩陣格式和範例代碼裏的矩陣是轉置關係,使用的時候居然不用flip……)
設置uniform變量和設置頂點屬性相似,先拿到位置然後再設置它。

int uniModel = glGetUniformLocation(shaderProgram, "model");
Matrix4f model = new Matrix4f();
glUniformMatrix4fv(uniModel, false, model.getBuffer());

int uniView = glGetUniformLocation(shaderProgram, "view");
Matrix4f view = new Matrix4f();
glUniformMatrix4fv(uniView, false, view.getBuffer());

int uniProjection = glGetUniformLocation(shaderProgram, "projection");
float ratio = 640f / 480f;
Matrix4f projection = Matrix4f.orthographic(-ratio, ratio, -1f, 1f, -1f, 1f);
glUniformMatrix4fv(uniProjection, false, projection.getBuffer());

glUniformMatrix4fv(location, transpose, values)的參數很明顯,location就是得到位置。transpose如果設定爲true,矩陣在使用前會被轉置。values設一個想要填充的緩衝。
爲了計算一個正交矩陣,可以看glOrtho(left, right, bottom, top, near, far),裏面描述了怎樣計算它。(其實也可以百度一下glOrtho看看中文的介紹)
你自己寫的時候需要注意,當你把值放入FloatBuffer中時,你應該清楚在OpenGL裏,矩陣是列主序矩陣。(列主序矩陣可以參見這裏:http://blog.csdn.net/zhihuier/article/details/9098179

現在是時候解釋一下model、view、projection矩陣了。(即MVP矩陣,模型視圖投影矩陣)
模型(model)矩陣會將物體的本地座標計算到世界座標上。在OpenGL中,你使用它來縮放、平移、旋轉物體。(本地→世界)
視圖(view)矩陣將世界座標計算爲視點座標,視點座標用於決定攝像機位置。但是實際上你並沒有移動攝像機,你移動的是世界,所以爲了得到正確的視圖矩陣,你必須以攝像機平移相反的方向平移世界。(想象一下電腦屏幕是攝像機,當你想移動攝像機移動畫面的時候其實是整個世界在向反方向動,你的電腦並沒有動)(世界→攝像機)
投影(projection)矩陣將視點座標計算成修剪座標,你用這矩陣來實現一個正交或透視的矩陣。(攝像機→修剪)
上面提過了,計算MVP的時候必須反着乘三個矩陣。這是因爲最後其實是:新矩陣 = 投影矩陣 * 視圖矩陣 * 模型矩陣 * 向量,所以實際上它的計算順序是:新矩陣 = 投影矩陣 * (視圖矩陣 * (模型矩陣 * 向量)) (譯註:矩陣乘法滿足結合律,所以這句話的意思其實是說,本地→世界的轉換矩陣M肯定要和向量相乘才能正確生效,故MVP不能正着乘,不然就是M*V*P*向量,P和向量相乘了,計算結果肯定不對。至於爲何不是“向量*M*V*P”這樣的順序,這是因爲前邊提過,openGL裏用的是列主序矩陣,向量用的也都是列向量,所以應該倒過來乘才能正確計算。如果是在DirectX裏,用的是行主序矩陣和行向量,那就是正着乘了。)

Rendering 渲染

最後我們渲染我們的三角形,只需要調用glDrawArrays(type, first, count)。別忘了在那之前清一下屏。

glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLES, 0, 3);

第一個方法就是清除屏幕緩衝裏的色彩。如果不這樣做會得到很詭異的效果。
glDrawArrays(type, first, count)會描畫目前綁定的VBO。type值大部分情況下都是GL_TRIANGLES,你也可以用一些其他的三角形類型比如GL_TRIANGLE_STRIP,或者其他類型比如GL_POINTS。既然我們要畫出所有頂點,start那就設成0,有三個頂點要畫,所以count是3。

Interpolating with alpha value 使用alpha插值

但是一個不動的三角形非常無聊,所以我們讓它像入門範例裏那樣轉起來!還得在之前的遊戲循環教程裏的update(float delta)和render(float alpha)嗎?現在就是用到它們的時候了。
要轉動三角形,我們需要設置我們的模型矩陣爲一個旋轉矩陣,源代碼中已經提供了方法,所以只要自己實現它就可以了,參見glRotate(angle, x, y, z).
除了旋轉矩陣我們還需要當前的角度和每秒要轉的角度。在範例代碼中,三角形每秒旋轉50度。

private int uniModel;
private float angle = 0f;
private float anglePerSecond = 50f;

在我們的update方法裏,我們用delta值計算當前循環的角度,在render方法裏,我們只需將當前的角度用到模型矩陣上。

public void update(float delta) {
    angle += delta * anglePerSecond;
}

public void render(float alpha) {
    glClear(GL_COLOR_BUFFER_BIT);

    Matrix4f model = Matrix4f.rotate(angle, 0f, 0f, 1f);
    glUniformMatrix4fv(uniModel, false, model.getBuffer());

    glDrawArrays(GL_TRIANGLES, 0, 3);
}

對可變時步來講,這樣就已經OK了。再看看固定時步會怎樣,假設UPS設定爲5,這個循環會變得結結巴巴的。
怎麼辦?這就需要用的alpha插值了。
爲了用到它,我們還需要另一個變量,上次循環時的角度。

private int uniModel;
private float previousAngle = 0f;
private float angle = 0f;
private float anglePerSecond = 50f;

這樣我們就可以插入本次角度和上次角度的中間狀態。你仔細想想就會意識到,你的屏幕將會延遲一幀,但是這沒什麼問題。所有的專業的遊戲都是這樣做的,使用固定時步,你不會注意到的。
需要在update方法裏設置之前的角度。

public void update(float delta) {
    previousAngle = angle;
    angle += delta * anglePerSecond;
}

插值將會在render方法中生效,並應用在旋轉矩陣之上。

public void render(float alpha) {
    glClear(GL_COLOR_BUFFER_BIT);

    float lerpAngle = (1f - alpha) * previousAngle + alpha * angle;
    Matrix4f model = Matrix4f.rotate(lerpAngle, 0f, 0f, 1f);
    glUniformMatrix4fv(uniModel, false, model.getBuffer());

    glDrawArrays(GL_TRIANGLES, 0, 3);
}

通過兩種狀態之前的插值,無論是5UPS還是60UPS都沒問題,它看起來將始終是一個平滑的變換。

Cleaning up 清除

你的程序結束時,好習慣是要將圖形數據清除掉。

glDeleteVertexArrays(vao);
glDeleteBuffers(vbo);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
glDeleteProgram(shaderProgram);

下一篇,講怎樣使用材質。

發佈了13 篇原創文章 · 獲贊 9 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章