# 第十四課:渲染到紋理
“渲染到紋理”是一系列特效方法之一。基本思想是:像通常那樣渲染一個場景——只是這次是渲染到可以重用的紋理中。
應用包括:游戲(in-game)相機、后期處理(post-processing)以及你能想象到一切.
## 渲染到紋理
我們有三個任務:創建要渲染的紋理對象;將紋理渲染到對象上;使用生成的紋理。
## 創建渲染目標(Render Target)
我們要渲染的對象叫做幀緩存。它像一個容器,用來存紋理和一個可選的深度緩沖區(depth buffer)。在OpenGL中我們可以像創建其他對象一樣創建它:
~~~
// The framebuffer, which regroups 0, 1, or more textures, and 0 or 1 depth buffer.
GLuint FramebufferName = 0;
glGenFramebuffers(1, &FramebufferName);
glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName);
~~~
現在需要創建紋理,紋理中包含著色器的RGB輸出。這段代碼非常的經典:
~~~
// The texture we're going to render to
GLuint renderedTexture;
glGenTextures(1, &renderedTexture);
// "Bind" the newly created texture : all future texture functions will modify this texture
glBindTexture(GL_TEXTURE_2D, renderedTexture);
// Give an empty image to OpenGL ( the last "0" )
glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, 1024, 768, 0,GL_RGB, GL_UNSIGNED_BYTE, 0);
// Poor filtering. Needed !
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
~~~
同時還需要一個深度緩沖區(depth buffer)。這是可選的,取決于紋理中實際需要畫的東西;由于我們渲染的是小猴Suzanne,所以需要深度測試。
~~~
// The depth buffer
GLuint depthrenderbuffer;
glGenRenderbuffers(1, &depthrenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, depthrenderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, 1024, 768);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthrenderbuffer);
~~~
最后,配置frameBuffer。
~~~
// Set "renderedTexture" as our colour attachement #0
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, renderedTexture, 0);
// Set the list of draw buffers.
GLenum DrawBuffers[2] = {GL_COLOR_ATTACHMENT0};
glDrawBuffers(1, DrawBuffers); // "1" is the size of DrawBuffers
~~~
這個過程中可能出現一些錯誤,取決于GPU的性能;下面是檢查的方法:
~~~
// Always check that our framebuffer is ok
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
return false;
~~~
## 渲染到紋理
渲染到紋理很直觀。簡單地綁定幀緩存,然后像往常一樣畫場景。輕松搞定!
~~~
// Render to our framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName);
glViewport(0,0,1024,768); // Render on the whole framebuffer, complete from the lower left corner to the upper right
~~~
fragment shader只需稍作調整:
~~~
layout(location = 0) out vec3 color;
~~~
這意味著每當修改變量“color”時,實際修改了0號渲染目標;這是因為之前調用了`glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, renderedTexture, 0);
注意:最后一個參數表示mipmap的級別,這個0和GL_COLOR_ATTACHMENT0沒有任何關系。
## 使用渲染出的紋理
我們將畫一個簡單的鋪滿屏幕的四邊形。需要buffer、shader、ID……
~~~
// The fullscreen quad's FBO
GLuint quad_VertexArrayID;
glGenVertexArrays(1, &quad_VertexArrayID);
glBindVertexArray(quad_VertexArrayID);
static const GLfloat g_quad_vertex_buffer_data[] = {
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
-1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
};
GLuint quad_vertexbuffer;
glGenBuffers(1, &quad_vertexbuffer);
glBindBuffer(GL_ARRAY_BUFFER, quad_vertexbuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(g_quad_vertex_buffer_data), g_quad_vertex_buffer_data, GL_STATIC_DRAW);
// Create and compile our GLSL program from the shaders
GLuint quad_programID = LoadShaders( "Passthrough.vertexshader", "SimpleTexture.fragmentshader" );
GLuint texID = glGetUniformLocation(quad_programID, "renderedTexture");
GLuint timeID = glGetUniformLocation(quad_programID, "time");
~~~
現在想渲染到屏幕上的話,必須把glBindFramebuffer的第二個參數設為0。
~~~
// Render to the screen
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0,0,1024,768); // Render on the whole framebuffer, complete from the lower left corner to the upper right
~~~
我們用下面這個shader來畫全屏的四邊形:
~~~
#version 330 core
in vec2 UV;
out vec3 color;
uniform sampler2D renderedTexture;
uniform float time;
void main(){
color = texture( renderedTexture, UV + 0.005*vec2( sin(time+1024.0*UV.x),cos(time+768.0*UV.y)) ).xyz;
}
~~~
這段代碼只是簡單地采樣紋理,加上一個隨時間變化的微小偏移。
## 結果

## 進一步探索
## 使用深度
在一些情況下,使用已渲染的紋理可能需要深度。本例中,像下面這樣,簡單地渲染到紋理中:
~~~
glTexImage2D(GL_TEXTURE_2D, 0,GL_DEPTH_COMPONENT24, 1024, 768, 0,GL_DEPTH_COMPONENT, GL_FLOAT, 0);
~~~
(“24”是精度。你可以按需從16,24,32中選。通常24剛好)
上面這些已經足夠您起步了。課程源碼中有完整的實現。
運行可能有點慢,因為驅動無法使用[Hi-Z](http://developer.amd.com/media/gpu_assets/Depth_in-depth.pdf)這類優化。
下圖的深度層次已經經過手動“優化”。通常,深度紋理不會這么清晰。深度紋理中,近 = Z接近0 = 顏色深; 遠 = Z接近1 = 顏色淺。

## 多重采樣
能夠用多重采樣紋理來替代基礎紋理:只需要在C++代碼中將glTexImage2D替換為[glTexImage2DMultisample](http://www.opengl.org/sdk/docs/man3/xhtml/glTexImage2DMultisample.xml),在fragment shader中將`sampler2D/texture`替換為`sampler2DMS/texelFetch`。
但要注意:`texelFetch`多出了一個參數,表示采樣的數量。換句話說,就是沒有自動“濾波”(在多重采樣中,正確的術語是“分辨率(resolution)”)功能。
所以需要你自己解決多重采樣的紋理,另外,非多重采樣紋理,是多虧另一個著色器。
沒有什么難點,只是體積龐大。
## 多重渲染目標
你可能需要同時寫多個紋理。
簡單地創建若干紋理(都要有正確、一致的大小!),調用glFramebufferTexture,為每一個紋理設置一個不同的color attachement,用更新的參數(如`(2,{GL_COLOR_ATTACHMENT0,GL_COLOR_ATTACHMENT1,GL_DEPTH_ATTACHMENT})`一樣)調用glDrawBuffers,然后在片斷著色器中多添加一個輸出變量:
~~~
layout(location = 1) out vec3 normal_tangentspace; // or whatever
~~~
提示1:如果真需要在紋理中輸出向量,浮點紋理也是有的,可以用16或32位精度代替8位……看看[glTexImage2D](http://www.opengl.org/sdk/docs/man/xhtml/glTexImage2D.xml)的參考手冊(搜GL_FLOAT)。提示2:對于以前版本的OpenGL,請使用glFragData[1] = myvalue。
## 練習
- 試使用`glViewport(0,0,512,768)`代替`glViewport(0,0,1024,768)`;(幀緩存、屏幕兩種情況都試試)
- 在最后一個fragment shader中嘗試一下用其他UV坐標
- 試用一個真正的變換矩陣變換四邊形。首先用硬編碼方式。然后嘗試使用`controls.hpp`里面的函數,觀察到了什么現象?
> ? [http://www.opengl-tutorial.org/](http://www.opengl-tutorial.org/)
> Written with [StackEdit](https://stackedit.io/).