Minecraft那些方塊用OpenGL怎麼畫比較好?


這個...如果只是畫一個磚塊的話,那真是想怎麼畫就怎麼畫,現代GPU很強力的.說一下MC1.8之後的渲染方法吧(準確說是1.9.4,不過我覺得1.8~1.11的渲染方式差不多) MC1.8之後的磚塊渲染分為三個階段,磚塊烘焙、區塊更新和區塊渲染.

磚塊烘焙是1.8開始新增的階段,在啟動遊戲時以及更換材質包(現在叫資源包了)時進行,從1.8開始MC要求通過json格式的外部文件來定義磚塊模型,這一步就是要將磚塊模型從json文本格式轉換成能由OpenGL渲染出來的二進位頂點數據,值得一提的是磚塊烘焙的最小單位不是磚塊,而是四邊形面(Quad),這是因為在接下來的區塊更新階段允許只渲染磚塊的某幾個面.你問三角形面怎麼辦? 在Forge的OBJ模型載入器中三角面會被轉換成第四個頂點和第三個頂點重合的四邊形面...

區塊更新發生在區塊被載入或者發生變化時,首先需要說明的是後面提到的區塊並不是指16x256x16的標準區塊,而是16x16x16的渲染區塊. 這一步會遍歷整個渲染區塊,逐磚塊地判斷磚塊是否存在、如果存在那麼哪個面該被渲染,判斷是否該被渲染的原理是首先根據磚塊的包圍盒(BoundingBox)判斷磚塊在此方向上是否填滿了整個空間,沒填滿的話會直接判為渲染,否則還要檢測與其相鄰的磚塊是否為不透明(Opaque)磚塊,倘若鄰接磚塊不是不透明("不是不透明"的磚塊不一定是透明磚塊,也有可能是上文提到的不能填滿一個格子的磚塊)的話則渲染,否則會被判定為不可見面,直接跳過.舉個例子:

上圖中岩漿和火炬在被埋入地下後依然會被渲染,因為它們沒有填滿整個空間;岩漿和火炬周圍的沙子也會渲染,因為岩漿和火炬不是不透明磚塊;冰沒有被渲染但是它周圍的沙子被渲染了;只有螢石和它周圍的沙子完全沒有被渲染.

在確定了一個面要被渲染後,程序會將磚塊烘焙得到的面頂點數據進行位置變換(從以磚塊為原點的坐標系變換到以渲染區塊為原點的坐標系)、光照以及AO計算,然後傳進一個緩衝區,MC原版和Forge在這裡有很大差異,原版會直接將頂點數據複製入緩衝,然後修改緩衝數據來修正位置和光照;Forge則實現了另一套完整的變換管線,在管線中完成頂點的位置變換和光照等,最後將修正完的數據傳入緩衝區.

在遍歷完所有磚塊後,程序會根據區塊的最外層(也就是XYZ三個分量有任何一個值為0或15的位置)有無磚塊來判斷區塊的鄰接可見性,鄰接可見性不是用於判斷磚塊本身是否可見,而是判斷它是否會遮掩旁邊鄰接的其他區塊,如果磚塊數量小於256會被直接判定為全方向可見(因為此時的磚塊總數不足以填滿任何一個方向) 如果等於4096會被判為無可見性.

在完成了一個渲染區塊的更新後,程序會將它上傳給OpenGL,如果是主線程的話這一步會立即進行,如果是在工作線程那緩衝區會被臨時存入一個隊列,由主線程在進行區塊渲染前統一上傳.如果使用VBO的話直接glBufferData,使用DisplayList的話就用VA進行渲染重新編譯一遍.

區塊渲染則是則是MC每一幀都在做的事,這一步最重要的工作是收集需要被渲染的區塊,首先程序會選取一個初始區塊作為搜索起點,通常來說是玩家所在的那個渲染區塊.然後程序會從玩家的位置向初始區塊做一次洪水填充演算法,判斷從玩家的位置是否存在到區塊最外層的"通路",即玩家是否能看到區塊外的景物,舉個栗子,如果區塊是一個房間的話,此步驟就是在檢測是否存在一扇窗戶或一堵破損的牆能讓房間內的人看見外面,這樣做的目的是為了檢查玩家是否在一個密封的環境內,顯然如果玩家是在一個密封的盒子內的話是無需渲染外部的區塊的;另外如果只有一面通向外部,而玩家又是背對著那一面,則程序也會判定無需渲染外部,可以想像此時的玩家是在死胡同中面壁...

然後就到了收集演算法的關鍵部分 - 從初始區塊做一次廣度優先搜索來收集所有可見的區塊 (如果之前程序判定玩家在面壁或者是在小黑屋中,則會跳過此步驟) 初始區塊鄰接的6個區塊(東南西北上下)會被無條件地加入到搜索隊列中.區塊在被添加到搜索隊列時會有一個欄位用於記錄之前搜索時所走過的"歷史方向",比如初始區塊向上找到了區塊A,區塊A向北找到了區塊B,區塊B向西找到了區塊C,那麼區塊C的那個欄位就會記錄到"上,北,西",這個欄位用於防止深度優先搜索時發生重複搜索(手段之一,另一個手段是為區塊添加時間戳) 區塊在搜索周圍時絕對不會向歷史方向的反方向進行搜索,顯然它有一個缺點是在必要時無法折回,比如在凹字形空間中,初始區塊是在凹字的左上角的話,是無法搜索到右上角的,但這個缺點正好被利用來實現遮掩剔除,算是個不是bug的bug吧.

在搜索時,一個被搜索的區塊首先會被標記為待渲染,然後檢測它周圍相鄰的區塊 (按照之前的規則,歷史方向的反方向位置的區塊不會被檢查) 根據前文提到的鄰接可見性結合歷史方向來判斷那個相鄰的區塊是否能被看見.如果能看見的話,那就再做一次喜聞樂見的平截錐體裁剪測試,通過測試的話就加入搜索隊列,沒通過的話,恭喜,它死在了革命勝利前夕.

最後一步就是將之前收集到的被標記為待渲染的區塊拉去渲染了,這一步就沒什麼技巧了,就是DrawCall嘛,你問"高級OpenGL"里的遮掩測試哪去了? 那破東西Mojang就從來沒有一天正確編寫過,在過去的版本中開了它會有各種區塊憑空消失啥的,因此在1.9.4里那個選項已經沒啦.


直接貼一篇好文章,Minecraft Like Rendering Experiments in OpenGL 4


謝邀,作為一個試著仿寫過 Minecraft 的人(當然,棄坑了)就來稍微答一下。可能不是最優的方法,如差錯歡迎指出。

首先,下面所說的一個區塊是 16 * 16 * 16 個方塊(Minecraft 的「區塊」是 16 * 256 * 16,不過渲染和存儲還是按照 16 * 16 * 16 的單位來的)。那麼我們為每一個區塊建立一個 VAO,這樣每一幀渲染中可以對每個區塊做個視錐裁剪(用區塊的外包圍盒),如果在視錐內看得見的區塊才進行繪製,這樣可以省去大部分區塊的繪製,畢竟在視錐內的區塊算是少數。

然後來說一下具體的準備區塊頂點數據的方法。簡單起見,這裡就假設所有的方塊都是完整的,不考慮異形方塊和透明方塊的情況。

方法挺簡單:

  • 遍歷每個方塊的每一個面;

  • 判斷這個面對著的是否為空氣;
  • 如果是,就將這個面的數據塞到頂點數據裡面。

代碼寫出來大概是這樣(隨手寫的,僅作示範):

struct Chunk {

std::uint16_t data[16][16][16];

};

int renderChunk(Chunk chunk) {

for(int x = 0; x &< 16; ++x) { for(int y = 0; y &< 16; ++y) { for(int z = 0; z &< 16; ++z) { auto id = chunk.data[x][y][z]; if(id) { if(x &> 0 !chunk.data[x - 1][y][z]) /* Face X- */;
if(x &< 16 - 1 !chunk.data[x + 1][y][z]) /* Face X+ */; if(y &> 0 !chunk.data[x][y - 1][z]) /* Face Y- */;
if(y &< 16 - 1 !chunk.data[x][y + 1][z]) /* Face Y+ */; if(z &> 0 !chunk.data[x][y][z - 1]) /* Face Z- */;
if(z &< 16 - 1 !chunk.data[x][y][z] - 1) /* Face Z+ */; } } } } }

這樣就不用繪製不必要的面,最後把數據塞給VBO完事了。

另外的一件事是將所有方塊的紋理都預先合併到同一張大紋理上,這樣繪製時就只用一張紋理就行,記錄下每一個原紋理對應的大紋理坐標,在生成上面所說的區塊頂點數據時把紋理坐標寫進去就行了。

還有非常重要的一點:

不要用 glBegin / glEnd ,用現代OpenGL

不要用 glBegin / glEnd,用現代OpenGL

不要用 glBegin / glEnd,用現代OpenGL

當然,也聽說過有利用GS來繪製的方法,由於沒有親自試過就不講了。


講個奇葩的思路,全遊戲都是方塊,有沒有人想過建一個distance field然後ray marching.


minecraft本來就是用的一個封裝得比較簡單的opengl庫,lwjgl,直接去看代碼唄,forge社區有反混淆後的代碼。


我猜需要維護一個動態八叉樹類似的演算法把多個塊合併


關注一下,但是我並不是提供答案而是同求答案的 …

我在嘗試在 CPU 上對八叉樹進行 raycasting,樹的葉子結點連接到 4x4x4 的 一個大塊 (因為某一篇文章說在葉子結點裡放置 chunks 會對 OpenGL 渲染比較方便,所以我也拿同樣的結構來進行 raycasting 了)

我的實現很慢,因為我印象中看到過一些視頻是能做到實時的。

總之呢,我現在的狀況是這樣…… 以下場景在 CPU 上畫出來只有 7 fps 左右

gprof 說沒有某一行是特別熱的熱點,可能是整個遍歷的函數都有問題 …

更新(這個就當這幾天的臨時筆記)

以下場景稍微快一點,但還是沒有解決最根本的問題。

更新:導入了 gltracy 大大的 SVO 模型(https://github.com/ephtracy/voxel-model/blob/master/svo/)

2016-12-17 更新

我發現我沒法讓 CPU 上的 Raycasting 變得非常快,所以就轉回 OpenGL 渲染了

視錐裁剪是必要的

比如下圖,總共的 16x16x16 塊 數有 11830 個,但是在視錐範圍內只有 3631 個可見。

裁剪與不裁剪就是 15 FPS 與 40 FPS 的差距。

如果將所有的塊用 octree 組織起來,然後在通過遍歷 octree 的方法來選取需要傳給 GPU 的塊的話,那裁剪還能夠更高效應該。

裁剪的方法,就是將視錐看作 5 個三維空間的平面 所包圍的一個空間。如果說一個 chunk 在某個平面上 屬於 正方向,那這個 chunk 就完全在那個平面之外。那就要被剔除。因為只要完全處於 一個 平面之外就可以判定為不可見,所以這個循環是可以提前結束的。

2016-12-22 更新

加入了 SSAO 以及簡單的編輯功能。

為了加入 SSAO ,不得不把所有 Legacy OpenGL 的代碼全都換成了「現代 OpenGL」,以便能夠使用 GLSL 3.3 以及能夠將深度緩衝畫入貼圖中。

左圖是有 SSAO 的,右圖是沒有 SSAO 的。

我的 SSAO 是基本照抄了這個(yunus-idrisov/SSAO)的(俄羅斯人寫的!非常帶感),除了透視矩陣的含義不同需要在 Shader 中進行修改以外其它地方都是基本一樣的。

2016-12-27

然後,加了一點顏色 和 Shadow Mapping

不過都有很多需要調整的地方 :(

2016-12-31

加了一點點 Deferred Shading 的基礎,弄了一點跨年專用動圖……不過發不上來 orz

新年快樂!好快樂啊!

2017年1月

主要是在尋找為什麼我一個屏幕能堆出幾百萬個三角形導致幀數降低很多的原因。

另外, 有篇帖子說,SSAO可以不用在屏幕空間計算,而是直接烘培到幾何體中:Ambient occlusion for Minecraft-like worlds

這樣的話,又能把幀數增加幾幀了。


晚上不想寫文書...於是就寫了一個。高中生,自學,輕噴。

先上圖:

重點:在GLSL Shader里通過生成隨機數來決定色塊對初始顏色(棕色/綠色)的偏離。

再上代碼:

GLSL Fragment Shader (這裡是精華所在 lol ):

#version 330
in vec3 Normal;
in vec3 FragPos;
out vec4 FragColor;
vec3 rand(vec3 Vec){
vec2 co =Vec.zy;
if(Vec.z==0 || Vec.z==19){
co=Vec.xy;
}
if(Vec.y==0 || Vec.y==19){
co=Vec.xz;
}
float r= fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453)/10;
float g= fract(sin(dot(co.xy ,vec2(20.00313,98.0326))) * 53379.9)/10;
float b= fract(sin(dot(co.xy ,vec2(10.429,71.1015))) * 71111.6)/10;

vec3 result = vec3(r,g,b);
return result;
}
int truncate(float f){
int r=int(f);
if(f-r&>0){
return r;
}
else{
return r-1;
}
}
void main() {
vec3 ColorBlockPosition= vec3(truncate((FragPos.x+0.5)*20),truncate((FragPos.y+0.5)*20),truncate((FragPos.z+0.5)*20));
vec3 deviation = rand(ColorBlockPosition);
vec3 Green = vec3(30.0/255.0,120.0/255.0,30.0/255.0);
vec3 Brown = vec3(70.0/255.0,40.0/255.0,30.0/255.0);

FragColor = vec4 (Brown+deviation,1);

if(ColorBlockPosition.y==19){
FragColor = vec4 (Green+deviation,1);
}
else if(ColorBlockPosition.y==18){
if(deviation.x&<0.07){ FragColor = vec4 (Green+deviation,1); } } else if(ColorBlockPosition.y==17){ if(deviation.x&<0.03){ FragColor = vec4 (Green+deviation,1); } } }

GLSL Vertex Shader (這裡就是zz,是個人都會):

#version 410
uniform mat4 MVP;
uniform mat4 xBodyRotMat;
uniform mat4 yBodyRotMat;

layout(location= 3 ) in vec3 vPos;
layout(location= 4 ) in vec3 vNorm;

out vec3 Normal;
out vec3 FragPos;

vec3 devideByW(vec4 inVec){
return vec3(inVec.x/inVec.w,inVec.y/inVec.w,inVec.z/inVec.w);
}
void main()
{
gl_Position = vec4(vPos,1.0);
gl_Position = yBodyRotMat * gl_Position;
gl_Position = xBodyRotMat * gl_Position;
gl_Position = MVP * gl_Position;

vec4 tempNorm=normalize(vec4(vNorm,1));
tempNorm=yBodyRotMat*tempNorm;
tempNorm=xBodyRotMat*tempNorm;
Normal=devideByW(tempNorm);

FragPos=vPos;

}

C++部分:(也沒什麼好說的)

struct MCBlock{
GLfloat vertices[288] = {
// Positions // Normals
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,

-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,

-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,

0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,

-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,

-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
};

GLuint VBO,VAO, vertex_shader, fragment_shader, program;
GLint mvp_location, vPos_location, vNorm_location, xBodyRotMat_location, yBodyRotMat_location ;

string vertex_shader_text =readFile("MCBlock_vs.glsl");
string fragment_shader_text =readFile("MCBlock_fs.glsl");

MCBlock(){
// auto vst=vertex_shader_text;
const char* fss=fragment_shader_text.c_str();
const char* vst=vertex_shader_text.c_str();
vertex_shader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex_shader, 1, vst, NULL);
glCompileShader(vertex_shader);
fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment_shader, 1, fss, NULL);
glCompileShader(fragment_shader);
program = glCreateProgram();
glAttachShader(program, vertex_shader);
glAttachShader(program, fragment_shader);
glLinkProgram(program);

vPos_location = glGetAttribLocation(program, "vPos");
vNorm_location = glGetAttribLocation(program, "vNorm");

xBodyRotMat_location = glGetUniformLocation(program,"xBodyRotMat");
yBodyRotMat_location = glGetUniformLocation(program,"yBodyRotMat");
mvp_location = glGetUniformLocation(program, "MVP");

glGenVertexArrays(1,VAO);
glGenBuffers(1, VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glEnableVertexAttribArray(vPos_location);
glVertexAttribPointer(vPos_location, 3, GL_FLOAT, GL_FALSE,
sizeof(float) * 6, (void*) (0));

glEnableVertexAttribArray(vNorm_location);
glVertexAttribPointer(vNorm_location, 3, GL_FLOAT, GL_FALSE,
sizeof(float) * 6, (void* )(sizeof(float) * 3) );

};

void draw(mat4x4 mvpMat, mat4x4 xBodyRotMat,mat4x4 yBodyRotMat){
glUseProgram(program);
glBindVertexArray(VAO);
glUniformMatrix4fv(mvp_location,1,GL_FALSE,(const GLfloat*) mvpMat);
glUniformMatrix4fv(xBodyRotMat_location,1,GL_FALSE,(const GLfloat*) xBodyRotMat);
glUniformMatrix4fv(yBodyRotMat_location,1,GL_FALSE,(const GLfloat*) yBodyRotMat);
glDrawArrays(GL_TRIANGLES,0,36);
glBindVertexArray(0);
}
};

至於為什麼有Normal.....因為一開始想加上光照,後來又想回去寫文書,所以就算了吧。

啦啦啦。


github上有開源實現


歪個樓, 下面這個blog好多

Code, stories, randomness, etc.


推薦閱讀:

Unity-Shader和OpenGL-Shader有什麼不同之處?
Shader 在現在圖形管線中可以負責多少部分?
如何評價CSDN學院姜雪偉的《3D遊戲引擎高端實戰培訓》課程?
在 OS X 下怎麼寫新版本的 OpenGL?

TAG:OpenGL | 我的世界Minecraft |