OpenGL 圖形庫的使用(二十五)—— 高級OpenGL之幀緩沖Framebuffers

版本記錄

版本號 時間
V1.0 2018.01.14

前言

OpenGL 圖形庫項目中一直也沒用過,最近也想學著使用這個圖形庫,感覺還是很有意思,也就自然想著好好的總結(jié)一下,希望對大家能有所幫助。下面內(nèi)容來自歡迎來到OpenGL的世界
1. OpenGL 圖形庫使用(一) —— 概念基礎
2. OpenGL 圖形庫使用(二) —— 渲染模式、對象、擴展和狀態(tài)機
3. OpenGL 圖形庫使用(三) —— 著色器、數(shù)據(jù)類型與輸入輸出
4. OpenGL 圖形庫使用(四) —— Uniform及更多屬性
5. OpenGL 圖形庫使用(五) —— 紋理
6. OpenGL 圖形庫使用(六) —— 變換
7. OpenGL 圖形庫的使用(七)—— 坐標系統(tǒng)之五種不同的坐標系統(tǒng)(一)
8. OpenGL 圖形庫的使用(八)—— 坐標系統(tǒng)之3D效果(二)
9. OpenGL 圖形庫的使用(九)—— 攝像機(一)
10. OpenGL 圖形庫的使用(十)—— 攝像機(二)
11. OpenGL 圖形庫的使用(十一)—— 光照之顏色
12. OpenGL 圖形庫的使用(十二)—— 光照之基礎光照
13. OpenGL 圖形庫的使用(十三)—— 光照之材質(zhì)
14. OpenGL 圖形庫的使用(十四)—— 光照之光照貼圖
15. OpenGL 圖形庫的使用(十五)—— 光照之投光物
16. OpenGL 圖形庫的使用(十六)—— 光照之多光源
17. OpenGL 圖形庫的使用(十七)—— 光照之復習總結(jié)
18. OpenGL 圖形庫的使用(十八)—— 模型加載之Assimp
19. OpenGL 圖形庫的使用(十九)—— 模型加載之網(wǎng)格
20. OpenGL 圖形庫的使用(二十)—— 模型加載之模型
21. OpenGL 圖形庫的使用(二十一)—— 高級OpenGL之深度測試
22. OpenGL 圖形庫的使用(二十二)—— 高級OpenGL之模板測試Stencil testing
23. OpenGL 圖形庫的使用(二十三)—— 高級OpenGL之混合Blending
24. OpenGL 圖形庫的使用(二十四)—— 高級OpenGL之面剔除Face culling

幀緩沖

到目前為止,我們已經(jīng)使用了很多屏幕緩沖了:用于寫入顏色值的顏色緩沖、用于寫入深度信息的深度緩沖和允許我們根據(jù)一些條件丟棄特定片段的模板緩沖。這些緩沖結(jié)合起來叫做幀緩沖(Framebuffer),它被儲存在內(nèi)存中。OpenGL允許我們定義我們自己的幀緩沖,也就是說我們能夠定義我們自己的顏色緩沖,甚至是深度緩沖和模板緩沖。

我們目前所做的所有操作都是在默認幀緩沖的渲染緩沖上進行的。默認的幀緩沖是在你創(chuàng)建窗口的時候生成和配置的(GLFW幫我們做了這些)。有了我們自己的幀緩沖,我們就能夠有更多方式來渲染了。

你可能不能很快理解幀緩沖的應用,但渲染你的場景到不同的幀緩沖能夠讓我們在場景中加入類似鏡子的東西,或者做出很酷的后期處理效果。首先我們會討論它是如何工作的,之后我們將來實現(xiàn)這些炫酷的后期處理效果。


創(chuàng)建一個幀緩沖

和OpenGL中的其它對象一樣,我們會使用一個叫做glGenFramebuffers的函數(shù)來創(chuàng)建一個幀緩沖對象(Framebuffer Object, FBO)

unsigned int fbo;
glGenFramebuffers(1, &fbo);

這種創(chuàng)建和使用對象的方式我們已經(jīng)見過很多次了,所以它的使用函數(shù)也和其它的對象類似。首先我們創(chuàng)建一個幀緩沖對象,將它綁定為激活的(Active)幀緩沖,做一些操作,之后解綁幀緩沖。我們使用glBindFramebuffer來綁定幀緩沖。

glBindFramebuffer(GL_FRAMEBUFFER, fbo);

在綁定到GL_FRAMEBUFFER目標之后,所有的讀取和寫入幀緩沖的操作將會影響當前綁定的幀緩沖。我們也可以使用GL_READ_FRAMEBUFFERGL_DRAW_FRAMEBUFFER,將一個幀緩沖分別綁定到讀取目標或?qū)懭肽繕?。綁定?code>GL_READ_FRAMEBUFFER的幀緩沖將會使用在所有像是glReadPixels的讀取操作中,而綁定到GL_DRAW_FRAMEBUFFER的幀緩沖將會被用作渲染、清除等寫入操作的目標。大部分情況你都不需要區(qū)分它們,通常都會使用GL_FRAMEBUFFER,綁定到兩個上。

不幸的是,我們現(xiàn)在還不能使用我們的幀緩沖,因為它還不完整(Complete),一個完整的幀緩沖需要滿足以下的條件:

  • 附加至少一個緩沖(顏色、深度或模板緩沖)。
  • 至少有一個顏色附件(Attachment)。
  • 所有的附件都必須是完整的(保留了內(nèi)存)。
  • 每個緩沖都應該有相同的樣本數(shù)。

如果你不知道什么是樣本,不要擔心,我們將在之后的教程中講到。

從上面的條件中可以知道,我們需要為幀緩沖創(chuàng)建一些附件,并將附件附加到幀緩沖上。在完成所有的條件之后,我們可以以GL_FRAMEBUFFER為參數(shù)調(diào)用glCheckFramebufferStatus,檢查幀緩沖是否完整。它將會檢測當前綁定的幀緩沖,并返回規(guī)范中這些值的其中之一。如果它返回的是GL_FRAMEBUFFER_COMPLETE,幀緩沖就是完整的了。

if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
  // 執(zhí)行勝利的舞蹈

之后所有的渲染操作將會渲染到當前綁定幀緩沖的附件中。由于我們的幀緩沖不是默認幀緩沖,渲染指令將不會對窗口的視覺輸出有任何影響。出于這個原因,渲染到一個不同的幀緩沖被叫做離屏渲染(Off-screen Rendering)。要保證所有的渲染操作在主窗口中有視覺效果,我們需要再次激活默認幀緩沖,將它綁定到0。

glBindFramebuffer(GL_FRAMEBUFFER, 0);

在完成所有的幀緩沖操作之后,不要忘記刪除這個幀緩沖對象:

glDeleteFramebuffers(1, &fbo);

在完整性檢查執(zhí)行之前,我們需要給幀緩沖附加一個附件。附件是一個內(nèi)存位置,它能夠作為幀緩沖的一個緩沖,可以將它想象為一個圖像。當創(chuàng)建一個附件的時候我們有兩個選項:紋理或渲染緩沖對象(Renderbuffer Object)。

1. 紋理附件

當把一個紋理附加到幀緩沖的時候,所有的渲染指令將會寫入到這個紋理中,就想它是一個普通的顏色/深度或模板緩沖一樣。使用紋理的優(yōu)點是,所有渲染操作的結(jié)果將會被儲存在一個紋理圖像中,我們之后可以在著色器中很方便地使用它。

為幀緩沖創(chuàng)建一個紋理和創(chuàng)建一個普通的紋理差不多:

unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

主要的區(qū)別就是,我們將維度設置為了屏幕大?。ūM管這不是必須的),并且我們給紋理的data參數(shù)傳遞了NULL。對于這個紋理,我們僅僅分配了內(nèi)存而沒有填充它。填充這個紋理將會在我們渲染到幀緩沖之后來進行。同樣注意我們并不關心環(huán)繞方式或多級漸遠紋理,我們在大多數(shù)情況下都不會需要它們。

如果你想將你的屏幕渲染到一個更小或更大的紋理上,你需要(在渲染到你的幀緩沖之前)再次調(diào)用glViewport,使用紋理的新維度作為參數(shù),否則只有一小部分的紋理或屏幕會被渲染到這個紋理上。

現(xiàn)在我們已經(jīng)創(chuàng)建好一個紋理了,要做的最后一件事就是將它附加到幀緩沖上了:

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);

glFrameBufferTexture2D有以下的參數(shù):

  • target:幀緩沖的目標(繪制、讀取或者兩者皆有)
  • attachment:我們想要附加的附件類型。當前我們正在附加一個顏色附件。注意最后的0意味著我們可以附加多個顏色附件。我們將在之后的教程中提到。
  • textarget:你希望附加的紋理類型
  • texture:要附加的紋理本身
  • level:多級漸遠紋理的級別。我們將它保留為0。

除了顏色附件之外,我們還可以附加一個深度和模板緩沖紋理到幀緩沖對象中。要附加深度緩沖的話,我們將附件類型設置為GL_DEPTH_ATTACHMENT。注意紋理的格式(Format)和內(nèi)部格式(Internalformat)類型將變?yōu)?code>GL_DEPTH_COMPONENT,來反映深度緩沖的儲存格式。要附加模板緩沖的話,你要將第二個參數(shù)設置為GL_STENCIL_ATTACHMENT,并將紋理的格式設定為GL_STENCIL_INDEX。

也可以將深度緩沖和模板緩沖附加為一個單獨的紋理。紋理的每32位數(shù)值將包含24位的深度信息和8位的模板信息。要將深度和模板緩沖附加為一個紋理的話,我們使用GL_DEPTH_STENCIL_ATTACHMENT類型,并配置紋理的格式,讓它包含合并的深度和模板值。將一個深度和模板緩沖附加為一個紋理到幀緩沖的例子可以在下面找到:

glTexImage2D(
  GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 800, 600, 0, 
  GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL
);

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0);

2. 渲染緩沖對象附件

渲染緩沖對象(Renderbuffer Object)是在紋理之后引入到OpenGL中,作為一個可用的幀緩沖附件類型的,所以在過去紋理是唯一可用的附件。和紋理圖像一樣,渲染緩沖對象是一個真正的緩沖,即一系列的字節(jié)、整數(shù)、像素等。渲染緩沖對象附加的好處是,它會將數(shù)據(jù)儲存為OpenGL原生的渲染格式,它是為離屏渲染到幀緩沖優(yōu)化過的。

渲染緩沖對象直接將所有的渲染數(shù)據(jù)儲存到它的緩沖中,不會做任何針對紋理格式的轉(zhuǎn)換,讓它變?yōu)橐粋€更快的可寫儲存介質(zhì)。然而,渲染緩沖對象通常都是只寫的,所以你不能讀取它們(比如使用紋理訪問)。當然你仍然還是能夠使用glReadPixels來讀取它,這會從當前綁定的幀緩沖,而不是附件本身,中返回特定區(qū)域的像素。

因為它的數(shù)據(jù)已經(jīng)是原生的格式了,當寫入或者復制它的數(shù)據(jù)到其它緩沖中時是非??斓?。所以,交換緩沖這樣的操作在使用渲染緩沖對象時會非??臁N覀冊诿總€渲染迭代最后使用的glfwSwapBuffers,也可以通過渲染緩沖對象實現(xiàn):只需要寫入一個渲染緩沖圖像,并在最后交換到另外一個渲染緩沖就可以了。渲染緩沖對象對這種操作非常完美。

創(chuàng)建一個渲染緩沖對象的代碼和幀緩沖的代碼很類似:

unsigned int rbo;
glGenRenderbuffers(1, &rbo);

類似,我們需要綁定這個渲染緩沖對象,讓之后所有的渲染緩沖操作影響當前的rbo:

glBindRenderbuffer(GL_RENDERBUFFER, rbo);

由于渲染緩沖對象通常都是只寫的,它們會經(jīng)常用于深度和模板附件,因為大部分時間我們都不需要從深度和模板緩沖中讀取值,只關心深度和模板測試。我們需要深度和模板值用于測試,但不需要對它們進行采樣,所以渲染緩沖對象非常適合它們。當我們不需要從這些緩沖中采樣的時候,通常都會選擇渲染緩沖對象,因為它會更優(yōu)化一點。

創(chuàng)建一個深度和模板渲染緩沖對象可以通過調(diào)用glRenderbufferStorage函數(shù)來完成:

glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);

創(chuàng)建一個渲染緩沖對象和紋理對象類似,不同的是這個對象是專門被設計作為圖像使用的,而不是紋理那樣的通用數(shù)據(jù)緩沖(General Purpose Data Buffer)。這里我們選擇GL_DEPTH24_STENCIL8作為內(nèi)部格式,它封裝了24位的深度和8位的模板緩沖。

最后一件事就是附加這個渲染緩沖對象:

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

渲染緩沖對象能為你的幀緩沖對象提供一些優(yōu)化,但知道什么時候使用渲染緩沖對象,什么時候使用紋理是很重要的。通常的規(guī)則是,如果你不需要從一個緩沖中采樣數(shù)據(jù),那么對這個緩沖使用渲染緩沖對象會是明智的選擇。如果你需要從緩沖中采樣顏色或深度值等數(shù)據(jù),那么你應該選擇紋理附件。性能方面它不會產(chǎn)生非常大的影響的。


渲染到紋理

既然我們已經(jīng)知道幀緩沖(大概)是怎么工作的了,是時候?qū)嵺`它們了。我們將會將場景渲染到一個附加到幀緩沖對象上的顏色紋理中,之后將在一個橫跨整個屏幕的四邊形上繪制這個紋理。這樣視覺輸出和沒使用幀緩沖時是完全一樣的,但這次是打印到了一個四邊形上。這為什么很有用呢?我們會在下一部分中知道原因。

首先要創(chuàng)建一個幀緩沖對象,并綁定它,這些都很直觀:

unsigned int framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);

接下來我們需要創(chuàng)建一個紋理圖像,我們將它作為一個顏色附件附加到幀緩沖上。我們將紋理的維度設置為窗口的寬度和高度,并且不初始化它的數(shù)據(jù):

// 生成紋理
unsigned int texColorBuffer;
glGenTextures(1, &texColorBuffer);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);

// 將它附加到當前綁定的幀緩沖對象
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0);  

我們還希望OpenGL能夠進行深度測試(如果你需要的話還有模板測試),所以我們還需要添加一個深度(和模板)附件到幀緩沖中。由于我們只希望采樣顏色緩沖,而不是其它的緩沖,我們可以為它們創(chuàng)建一個渲染緩沖對象。還記得當我們不需要采樣緩沖的時候,渲染緩沖對象是更好的選擇嗎?

創(chuàng)建一個渲染緩沖對象不是非常復雜。我們需要記住的唯一事情是,我們將它創(chuàng)建為一個深度和模板附件渲染緩沖對象。我們將它的內(nèi)部格式設置為GL_DEPTH24_STENCIL8,對我們來說這個精度已經(jīng)足夠了。

unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo); 
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);  
glBindRenderbuffer(GL_RENDERBUFFER, 0);

當我們?yōu)殇秩揪彌_對象分配了足夠的內(nèi)存之后,我們可以解綁這個渲染緩沖。

接下來,作為完成幀緩沖之前的最后一步,我們將渲染緩沖對象附加到幀緩沖的深度和模板附件上:

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

最后,我們希望檢查幀緩沖是否是完整的,如果不是,我們將打印錯誤信息。

if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
    std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);

記得要解綁幀緩沖,保證我們不會不小心渲染到錯誤的幀緩沖上。

現(xiàn)在這個幀緩沖就完整了,我們只需要綁定這個幀緩沖對象,讓渲染到幀緩沖的緩沖中而不是默認的幀緩沖中。之后的渲染指令將會影響當前綁定的幀緩沖。所有的深度和模板操作都會從當前綁定的幀緩沖的深度和模板附件中(如果有的話)讀取。如果你忽略了深度緩沖,那么所有的深度測試操作將不再工作,因為當前綁定的幀緩沖中不存在深度緩沖。

所以,要想繪制場景到一個紋理上,我們需要采取以下的步驟:

  • 將新的幀緩沖綁定為激活的幀緩沖,和往常一樣渲染場景
  • 綁定默認的幀緩沖
  • 繪制一個橫跨整個屏幕的四邊形,將幀緩沖的顏色緩沖作為它的紋理。

我們將會繪制深度測試小節(jié)中的場景,但這次使用的是舊的箱子紋理。

為了繪制這個四邊形,我們將會新創(chuàng)建一套簡單的著色器。我們將不會包含任何花哨的矩陣變換,因為我們提供的是標準化設備坐標的頂點坐標,所以我們可以直接將它們設定為頂點著色器的輸出。頂點著色器是這樣的:

#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aTexCoords;

out vec2 TexCoords;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); 
    TexCoords = aTexCoords;
}

并沒有太復雜的東西。片段著色器會更加基礎,我們做的唯一一件事就是從紋理中采樣:

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D screenTexture;

void main()
{ 
    FragColor = texture(screenTexture, TexCoords);
}

接著就靠你來為屏幕四邊形創(chuàng)建并配置一個VAO了。幀緩沖的一個渲染迭代將會有以下的結(jié)構(gòu):

// 第一處理階段(Pass)
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 我們現(xiàn)在不使用模板緩沖
glEnable(GL_DEPTH_TEST);
DrawScene();    

// 第二處理階段
glBindFramebuffer(GL_FRAMEBUFFER, 0); // 返回默認
glClearColor(1.0f, 1.0f, 1.0f, 1.0f); 
glClear(GL_COLOR_BUFFER_BIT);

screenShader.use();  
glBindVertexArray(quadVAO);
glDisable(GL_DEPTH_TEST);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glDrawArrays(GL_TRIANGLES, 0, 6);  

要注意一些事情。第一,由于我們使用的每個幀緩沖都有它自己一套緩沖,我們希望設置合適的位,調(diào)用glClear,清除這些緩沖。第二,當繪制四邊形時,我們將禁用深度測試,因為我們是在繪制一個簡單的四邊形,并不需要關系深度測試。在繪制普通場景的時候我們將會重新啟用深度測試。

有很多步驟都可能會出錯,所以如果你沒有得到輸出的話,嘗試調(diào)試程序,并重新閱讀本節(jié)的相關部分。如果所有的東西都能夠正常工作,你將會得到下面這樣的視覺輸出:

左邊展示的是視覺輸出,它和深度測試中是完全一樣的,但這次是渲染在一個簡單的四邊形上。如果我們使用線框模式渲染場景,就會變得很明顯,我們在默認的幀緩沖中只繪制了一個簡單的四邊形。

你可以在這里找到程序的源代碼。

#include <glad/glad.h>  
#include <GLFW/glfw3.h> 
#include <stb_image.h>  
#include <glm/glm.hpp>  
#include <glm/gtc/matrix_transform.hpp>  
#include <glm/gtc/type_ptr.hpp>  
#include <learnopengl/shader_m.h>  
#include <learnopengl/camera.h>  
#include <learnopengl/model.h>  
#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);
unsigned int loadTexture(const char *path);

// settings
const unsigned int SCR_WIDTH = 1280;
const unsigned int SCR_HEIGHT = 720;

// camera
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = (float)SCR_WIDTH / 2.0;
float lastY = (float)SCR_HEIGHT / 2.0;
bool firstMouse = true;

// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;

int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "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);
    glfwSetCursorPosCallback(window, mouse_callback);
    glfwSetScrollCallback(window, scroll_callback);

    // tell GLFW to capture our mouse
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }
// configure global opengl state  
// -----------------------------  glEnable(GL_DEPTH_TEST); 
// build and compile shaders  
// ------------------------- 
Shader shader("5.1.framebuffers.vs", "5.1.framebuffers.fs"); 
Shader screenShader("5.1.framebuffers_screen.vs", "5.1.framebuffers_screen.fs");

// set up vertex data (and buffer(s)) and configure vertex attributes
    // ------------------------------------------------------------------
    float cubeVertices[] = {
        // positions          // texture Coords
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

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

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

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

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

        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
    };
    float planeVertices[] = {
        // positions          // texture Coords 
         5.0f, -0.5f,  5.0f,  2.0f, 0.0f,
        -5.0f, -0.5f,  5.0f,  0.0f, 0.0f,
        -5.0f, -0.5f, -5.0f,  0.0f, 2.0f,

         5.0f, -0.5f,  5.0f,  2.0f, 0.0f,
        -5.0f, -0.5f, -5.0f,  0.0f, 2.0f,
         5.0f, -0.5f, -5.0f,  2.0f, 2.0f
    };
    float quadVertices[] = { // vertex attributes for a quad that fills the entire screen in Normalized Device Coordinates.
        // positions   // texCoords
        -1.0f,  1.0f,  0.0f, 1.0f,
        -1.0f, -1.0f,  0.0f, 0.0f,
         1.0f, -1.0f,  1.0f, 0.0f,

        -1.0f,  1.0f,  0.0f, 1.0f,
         1.0f, -1.0f,  1.0f, 0.0f,
         1.0f,  1.0f,  1.0f, 1.0f
    };
// cube VAO
    unsigned int cubeVAO, cubeVBO;
    glGenVertexArrays(1, &cubeVAO);
    glGenBuffers(1, &cubeVBO);
    glBindVertexArray(cubeVAO);
    glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVertices), &cubeVertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    // plane VAO
    unsigned int planeVAO, planeVBO;
    glGenVertexArrays(1, &planeVAO);
    glGenBuffers(1, &planeVBO);
    glBindVertexArray(planeVAO);
    glBindBuffer(GL_ARRAY_BUFFER, planeVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(planeVertices), &planeVertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    // screen quad VAO
    unsigned int quadVAO, quadVBO;
    glGenVertexArrays(1, &quadVAO);
    glGenBuffers(1, &quadVBO);
    glBindVertexArray(quadVAO);
    glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));

    // load textures
    // -------------
    unsigned int cubeTexture = loadTexture(FileSystem::getPath("resources/textures/marble.jpg").c_str());
    unsigned int floorTexture = loadTexture(FileSystem::getPath("resources/textures/metal.png").c_str());

    // shader configuration
    // --------------------
    shader.use();
    shader.setInt("texture1", 0);

    screenShader.use();
    screenShader.setInt("screenTexture", 0);
// framebuffer configuration
    // -------------------------
    unsigned int framebuffer;
    glGenFramebuffers(1, &framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
    // create a color attachment texture
    unsigned int textureColorbuffer;
    glGenTextures(1, &textureColorbuffer);
    glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureColorbuffer, 0);
    // create a renderbuffer object for depth and stencil attachment (we won't be sampling these)
    unsigned int rbo;
    glGenRenderbuffers(1, &rbo);
    glBindRenderbuffer(GL_RENDERBUFFER, rbo);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, SCR_WIDTH, SCR_HEIGHT); // use a single renderbuffer object for both a depth AND stencil buffer.
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo); // now actually attach it
    // now that we actually created the framebuffer and added all attachments we want to check if it is actually complete now
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    // draw as wireframe
    //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // per-frame time logic
        // --------------------
        float currentFrame = glfwGetTime();
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;

        // input
        // -----
        processInput(window);


        // render
        // ------
        // bind to framebuffer and draw scene as we normally would to color texture 
        glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
        glEnable(GL_DEPTH_TEST); // enable depth testing (is disabled for rendering screen-space quad)

        // make sure we clear the framebuffer's content
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        shader.use();
        glm::mat4 model;
        glm::mat4 view = camera.GetViewMatrix();
        glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
        shader.setMat4("view", view);
        shader.setMat4("projection", projection);
        // cubes
        glBindVertexArray(cubeVAO);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, cubeTexture);
        model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f));
        shader.setMat4("model", model);
        glDrawArrays(GL_TRIANGLES, 0, 36);
        model = glm::mat4();
        model = glm::translate(model, glm::vec3(2.0f, 0.0f, 0.0f));
        shader.setMat4("model", model);
        glDrawArrays(GL_TRIANGLES, 0, 36);
        // floor
        glBindVertexArray(planeVAO);
        glBindTexture(GL_TEXTURE_2D, floorTexture);
        shader.setMat4("model", glm::mat4());
        glDrawArrays(GL_TRIANGLES, 0, 6);
        glBindVertexArray(0);

        // now bind back to default framebuffer and draw a quad plane with the attached framebuffer color texture
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glDisable(GL_DEPTH_TEST); // disable depth test so screen-space quad isn't discarded due to depth test.
        // clear all relevant buffers
        glClearColor(1.0f, 1.0f, 1.0f, 1.0f); // set clear color to white (not really necessery actually, since we won't be able to see behind the quad anyways)
        glClear(GL_COLOR_BUFFER_BIT);

        screenShader.use();
        glBindVertexArray(quadVAO);
        glBindTexture(GL_TEXTURE_2D, textureColorbuffer);   // use the color attachment texture as the texture of the quad plane
        glDrawArrays(GL_TRIANGLES, 0, 6);


        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // optional: de-allocate all resources once they've outlived their purpose:
    // ------------------------------------------------------------------------
    glDeleteVertexArrays(1, &cubeVAO);
    glDeleteVertexArrays(1, &planeVAO);
    glDeleteVertexArrays(1, &quadVAO);
    glDeleteBuffers(1, &cubeVBO);
    glDeleteBuffers(1, &planeVBO);
    glDeleteBuffers(1, &quadVBO);

    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);

    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        camera.ProcessKeyboard(FORWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        camera.ProcessKeyboard(BACKWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        camera.ProcessKeyboard(LEFT, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        camera.ProcessKeyboard(RIGHT, deltaTime);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

// glfw: whenever the mouse moves, this callback is called
// -------------------------------------------------------
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    if (firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }

    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top

    lastX = xpos;
    lastY = ypos;

    camera.ProcessMouseMovement(xoffset, yoffset);
}

// glfw: whenever the mouse scroll wheel scrolls, this callback is called
// ----------------------------------------------------------------------
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    camera.ProcessMouseScroll(yoffset);
}

// utility function for loading a 2D texture from file
// ---------------------------------------------------
unsigned int loadTexture(char const * path)
{
    unsigned int textureID;
    glGenTextures(1, &textureID);

    int width, height, nrComponents;
    unsigned char *data = stbi_load(path, &width, &height, &nrComponents, 0);
    if (data)
    {
        GLenum format;
        if (nrComponents == 1)
            format = GL_RED;
        else if (nrComponents == 3)
            format = GL_RGB;
        else if (nrComponents == 4)
            format = GL_RGBA;

        glBindTexture(GL_TEXTURE_2D, textureID);
        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        stbi_image_free(data);
    }
    else
    {
        std::cout << "Texture failed to load at path: " << path << std::endl;
        stbi_image_free(data);
    }

    return textureID;
}

所以這個有什么用處呢?因為我們能夠以一個紋理圖像的方式訪問已渲染場景中的每個像素,我們可以在片段著色器中創(chuàng)建出非常有趣的效果。這些有趣效果統(tǒng)稱為后期處理Post-processing效果。


后期處理

既然整個場景都被渲染到了一個紋理上,我們可以簡單地通過修改紋理數(shù)據(jù)創(chuàng)建出一些非常有意思的效果。在這一部分中,我們將會向你展示一些流行的后期處理效果,并告訴你改如何使用創(chuàng)造力創(chuàng)建你自己的效果。

讓我們先從最簡單的后期處理效果開始。

1. 反相

我們現(xiàn)在能夠訪問渲染輸出的每個顏色,所以在(譯注:屏幕的)片段著色器中返回這些顏色的反相(Inversion)并不是很難。我們將會從屏幕紋理中取顏色值,然后用1.0減去它,對它進行反相:

void main()
{
    FragColor = vec4(vec3(1.0 - texture(screenTexture, TexCoords)), 1.0);
}

盡管反相是一個相對簡單的后期處理效果,它已經(jīng)能創(chuàng)造一些奇怪的效果了:

在片段著色器中僅僅使用一行代碼,就能讓整個場景的顏色都反相了。很酷吧?

2. 灰度

另外一個很有趣的效果是,移除場景中除了黑白灰以外所有的顏色,讓整個圖像灰度化(Grayscale)。很簡單的實現(xiàn)方式是,取所有的顏色分量,將它們平均化:

void main()
{
    FragColor = texture(screenTexture, TexCoords);
    float average = (FragColor.r + FragColor.g + FragColor.b) / 3.0;
    FragColor = vec4(average, average, average, 1.0);
}

這已經(jīng)能創(chuàng)造很好的結(jié)果了,但人眼會對綠色更加敏感一些,而對藍色不那么敏感,所以為了獲取物理上更精確的效果,我們需要使用加權(quán)的(Weighted)通道:

void main()
{
    FragColor = texture(screenTexture, TexCoords);
    float average = 0.2126 * FragColor.r + 0.7152 * FragColor.g + 0.0722 * FragColor.b;
    FragColor = vec4(average, average, average, 1.0);
}

你可能不會立刻發(fā)現(xiàn)有什么差別,但在更復雜的場景中,這樣的加權(quán)灰度效果會更真實一點。


核效果

在一個紋理圖像上做后期處理的另外一個好處是,我們可以從紋理的其它地方采樣顏色值。比如說我們可以在當前紋理坐標的周圍取一小塊區(qū)域,對當前紋理值周圍的多個紋理值進行采樣。我們可以結(jié)合它們創(chuàng)建出很有意思的效果。

(Kernel)(或卷積矩陣(Convolution Matrix))是一個類矩陣的數(shù)值數(shù)組,它的中心為當前的像素,它會用它的核值乘以周圍的像素值,并將結(jié)果相加變成一個值。所以,基本上我們是在對當前像素周圍的紋理坐標添加一個小的偏移量,并根據(jù)核將結(jié)果合并。下面是核的一個例子:

這個核取了8個周圍像素值,將它們乘以2,而把當前的像素乘以-15。這個核的例子將周圍的像素乘上了一個權(quán)重,并將當前像素乘以一個比較大的負權(quán)重來平衡結(jié)果。

你在網(wǎng)上找到的大部分核將所有的權(quán)重加起來之后都應該會等于1,如果它們加起來不等于1,這就意味著最終的紋理顏色將會比原紋理值更亮或者更暗了。

核是后期處理一個非常有用的工具,它們使用和實驗起來都很簡單,網(wǎng)上也能找到很多例子。我們需要稍微修改一下片段著色器,讓它能夠支持核。我們假設使用的核都是3x3核(實際上大部分核都是):

const float offset = 1.0 / 300.0;  

void main()
{
    vec2 offsets[9] = vec2[](
        vec2(-offset,  offset), // 左上
        vec2( 0.0f,    offset), // 正上
        vec2( offset,  offset), // 右上
        vec2(-offset,  0.0f),   // 左
        vec2( 0.0f,    0.0f),   // 中
        vec2( offset,  0.0f),   // 右
        vec2(-offset, -offset), // 左下
        vec2( 0.0f,   -offset), // 正下
        vec2( offset, -offset)  // 右下
    );

    float kernel[9] = float[](
        -1, -1, -1,
        -1,  9, -1,
        -1, -1, -1
    );

    vec3 sampleTex[9];
    for(int i = 0; i < 9; i++)
    {
        sampleTex[i] = vec3(texture(screenTexture, TexCoords.st + offsets[i]));
    }
    vec3 col = vec3(0.0);
    for(int i = 0; i < 9; i++)
        col += sampleTex[i] * kernel[i];

    FragColor = vec4(col, 1.0);
}

在片段著色器中,我們首先為周圍的紋理坐標創(chuàng)建了一個9個vec2偏移量的數(shù)組。偏移量是一個常量,你可以按照你的喜好自定義它。之后我們定義一個核,在這個例子中是一個銳化(Sharpen)核,它會采樣周圍的所有像素,銳化每個顏色值。最后,在采樣時我們將每個偏移量加到當前紋理坐標上,獲取需要采樣的紋理,之后將這些紋理值乘以加權(quán)的核值,并將它們加到一起。

這個銳化核看起來是這樣的:

這能創(chuàng)建一些很有趣的效果,比如說你的玩家打了麻醉劑所感受到的效果。

1. 模糊

創(chuàng)建模糊(Blur)效果的核是這樣的:

由于所有值的和是16,所以直接返回合并的采樣顏色將產(chǎn)生非常亮的顏色,所以我們需要將核的每個值都除以16。最終的核數(shù)組將會是:

float kernel[9] = float[](
    1.0 / 16, 2.0 / 16, 1.0 / 16,
    2.0 / 16, 4.0 / 16, 2.0 / 16,
    1.0 / 16, 2.0 / 16, 1.0 / 16  
);

通過在片段著色器中改變核的float數(shù)組,我們完全改變了后期處理效果。它現(xiàn)在看起來是這樣子的:

這樣的模糊效果創(chuàng)造了很多的可能性。我們可以隨著時間修改模糊的量,創(chuàng)造出玩家醉酒時的效果,或者在主角沒帶眼鏡的時候增加模糊。模糊也能夠讓我們來平滑顏色值,我們將在之后教程中使用到。

你可以看到,只要我們有了這個核的實現(xiàn),創(chuàng)建炫酷的后期處理特效是非常容易的事。我們再來看最后一個很流行的效果來結(jié)束本節(jié)的討論。

2. 邊緣檢測

下面的邊緣檢測(Edge-detection)核和銳化核非常相似:

這個核高亮了所有的邊緣,而暗化了其它部分,在我們只關心圖像的邊角的時候是非常有用的。

你可能不會奇怪,像是Photoshop這樣的圖像修改工具/濾鏡使用的也是這樣的核。因為顯卡處理片段的時候有著極強的并行處理能力,我們可以很輕松地在實時的情況下逐像素對圖像進行處理。所以圖像編輯工具在圖像處理的時候會更傾向于使用顯卡。

注意,核在對屏幕紋理的邊緣進行采樣的時候,由于還會對中心像素周圍的8個像素進行采樣,其實會取到紋理之外的像素。由于環(huán)繞方式默認是GL_REPEAT,所以在沒有設置的情況下取到的是屏幕另一邊的像素,而另一邊的像素本不應該對中心像素產(chǎn)生影響,這就可能會在屏幕邊緣產(chǎn)生很奇怪的條紋。為了消除這一問題,我們可以將屏幕紋理的環(huán)繞方式都設置為GL_CLAMP_TO_EDGE。這樣子在取到紋理外的像素時,就能夠重復邊緣的像素來更精確地估計最終的值了。

后記

未完,待續(xù)~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內(nèi)容