筆者介紹:姜雪偉,IT公司技術合伙人,IT高級講師,CSDN社區專家,特邀編輯,暢銷書作者,國家專利發明人;已出版書籍:《手把手教你架構3D游戲引擎》電子工業出版社和《Unity3D實戰核心技術詳解》電子工業出版社等。
CSDN課程視頻網址:http://edu.csdn.net/lecturer/144
深度測試在游戲引擎中使用的非常多,比如在Unity3D引擎中,在UI之間的遮擋設置可以通過其深度值進行設置,在3D場景中也會根據其Z值進行設置其前后關系。這些都會用到深度測試技術,由于Unity3D引擎是跨平臺的,它在移動端的渲染使用的是OpenGL,所以后面的系列文章就介紹關于OpenGL使用的核心技術。
深度測試需要一個存儲區域,這個存儲區域就稱為深度緩沖,它就像顏色緩沖用于存儲所有的片段顏色,深度緩沖和顏色緩沖區有相同的寬度和高度。深度緩沖由窗口系統自動創建并將其深度值存儲為 16、 24 或 32 位浮點數,在大多數系統中深度緩沖區為24位。
使用深度測試就必須要啟用它,當深度測試啟用的時候, OpenGL 測試深度緩沖區內的深度值。OpenGL 執行深度測試的時候,如果此測試通過,深度緩沖內的值將被設為新的深度值。如果深度測試失敗,則丟棄該片段。深度測試在片段著色器運行之后,它是在屏幕空間中執行的。屏幕空間坐標直接有關的視區,由OpenGL的glViewport
函數給定,并且可以通過GLSL的片段著色器中內置的 gl_FragCoord
變量訪問。gl_FragCoord
的 X 和 y 表示該片段的屏幕空間坐標 ((0,0) 在左下角)。gl_FragCoord
還包含一個 z 坐標,它包含了片段的實際深度值。此 z 坐標值是與深度緩沖區的內容進行比較的值。
深度測試默認是關閉的,要啟用深度測試的話,我們需要用GL_DEPTH_TEST
選項來打開它,函數如下:
glEnable(GL_DEPTH_TEST); 一旦啟用深度測試,如果片段通過深度測試,OpenGL自動在深度緩沖區存儲片段的 z 值,如果深度測試失敗,那么相應地丟棄該片段。如果啟用深度測試,那么在每個渲染之前還應使用GL_DEPTH_BUFFER_BIT
清除深度緩沖區,否則深度緩沖區將保留上一次進行深度測試時所寫的深度值。函數如下:glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);有時,我們也不需要深度測試,那就關閉它,就是禁止緩沖區寫入,函數如下:glDepthMask(GL_FALSE);在OpenGL中經常使用的深度測試函數為可以通過調用glDepthFunc
它包含很多參數:
GL_ALWAYS 永遠通過測試GL_NEVER 永遠不通過測試GL_LESS 在片段深度值小于緩沖區的深度時通過測試GL_EQUAL 在片段深度值等于緩沖區的深度時通過測試GL_LEQUAL 在片段深度值小于等于緩沖區的深度時通過測試GL_GREATER 在片段深度值大于緩沖區的深度時通過測試GL_NOTEQUAL 在片段深度值不等于緩沖區的深度時通過測試GL_GEQUAL 在片段深度值大于等于緩沖區的深度時通過測試其中默認使用GL_LESS
,它的含義是這將丟棄深度值高于或等于當前深度緩沖區的值的片段。接下來仔介紹深度值的計算,深度緩沖主要是通過計算深度值來比較大小,在深度緩沖區中包含深度值介于
0.0
和1.0
之間,從觀察者看到其內容與場景中的所有對象的 z 值進行了比較。這些視圖空間中的 z 值可以在投影平頭截體的近平面和遠平面之間的任何值。我們因此需要一些方法來轉換這些視圖空間 z 值到 [0,1] 的范圍內,下面的 (線性) 方程把 z 值轉換為 0.0 和 1.0 之間的值 :
這里far和near是我們用來提供到投影矩陣設置可見視圖截錐的遠近值。關于這方面技術的講解,可以查看我之前寫的博客,方程帶內錐截體的深度值 z,并將其轉換到 [0,1] 范圍。在下面的圖給出 z 值和其相應的深度值的關系:
然而,在實踐中是幾乎從來不使用這樣的線性深度緩沖區。正確的投影特性的非線性深度方程是和1/z成正比的 ,由于非線性函數是和 1/z 成正比,例如1.0 和 2.0 之間的 z 值,將變為 1.0 到 0.5之間, 這樣在z非常小的時候給了我們很高的精度。方程如下所示:
要記住的重要一點是在深度緩沖區的值不是線性的屏幕空間 (它們在視圖空間投影矩陣應用之前是線性)。值為 0.5 在深度緩沖區并不意味著該對象的 z 值是投影平頭截體的中間;頂點的 z 值是實際上相當接近近平面!你可以看到 z 值和產生深度緩沖區的值在下列圖中的非線性關系:
正如你所看到,一個附近的物體的小的 z 值因此給了我們很高的深度精度。變換 (從觀察者的角度) 的 z 值的方程式被嵌入在投影矩陣,所以當我們變換頂點坐標從視圖到裁剪,然后到非線性方程應用了的屏幕空間中。關于投影矩陣,讀者可以查看之前寫的博客。
屏幕空間的深度值是非線性的,這個讀者可以自己測試,在這里我把原理說一下:屏幕空間的深度值是非線性如他們在z很小的時候有很高的精度,較大的 z 值有較低的精度。該片段的深度值會迅速增加,所以幾乎所有頂點的深度值接近 1.0。如果我們小心的靠近物體,你最終可能會看到的色彩越來越暗,意味著它們的 z 值越來越小,這清楚地表明深度值的非線性特性。近的物體相對遠的物體對的深度值比對象較大的影響。只移動幾英寸就能讓暗色完全變亮。
但是我們可以讓深度值變換回線性。要實現這一目標我們需要讓點應用投影變換逆的逆變換,成為單獨的深度值的過程。這意味著我們必須首先重新變換范圍 [0,1] 中的深度值為單位化的設備坐標(normalized device coordinates)范圍內 [-1,1] (裁剪空間(clip space))。然后,我們想要反轉非線性方程 :
就像在投影矩陣做的那樣并將此反轉方程應用于所得到的深度值。然后,結果是一個線性的深度值。
首先,我們需要 NDC 深度值轉換:
float z = depth * 2.0 - 1.0;然后把我們所得到的 z 值應用逆轉換來檢索的線性深度值:float linearDepth = (2.0 * near) / (far + near - z * (far - near));注意在轉換上述方程是并不是從方程:
精確的逆方程,這個方程從投影矩陣中導出,可以從新使用等式2將他轉換為非線性深度值。這個方程也會考慮使用[0,1] 而不是 [near,far]范圍內的 z 值 。
這不是從投影矩陣推導出的準確公式;這個方程是除以far的結果。深度值的范圍一直到far,這作為一個介于 0.0 和 1.0 之間的顏色值并不合適。除以far的值把深度值映射到介于 0.0 和 1.0,更適合用于演示目的。
這個能夠將屏幕空間的非線性深度值轉變為線性深度值的完整的片段著色器如下所示:
#version 330 coreout vec4 color;float LinearizeDepth(float depth){ float near = 0.1; float far = 100.0; float z = depth * 2.0 - 1.0; // Back to NDC return (2.0 * near) / (far + near - z * (far - near));}void main(){ float depth = LinearizeDepth(gl_FragCoord.z); color = vec4(vec3(depth), 1.0f);}接下來介紹在程序中經常遇到的問題,深度沖突,就是在物體前后關系設置時會出現重疊現象,導致前后不分,在引擎中通常的做法有幾種:1、也是最重要的技巧是讓物體之間不要離得太近,以至于他們的三角形重疊。
2、是盡可能把近平面設置得遠一些。
3、是放棄一些性能來得到更高的深度值的精度。大多數的深度緩沖區都是24位。但現在顯卡支持32位深度值,這讓深度緩沖區的精度提高了一大節。所以犧牲一些性能你會得到更精確的深度測試,減少深度沖突。
最后把實現深度測試的Shader代碼給讀者展示一下:
首先展示的是片段著色器:
#version 330 coreout vec4 color;float near = 1.0; float far = 100.0; float LinearizeDepth(float depth) { float z = depth * 2.0 - 1.0; // Back to NDC return (2.0 * near * far) / (far + near - z * (far - near)); }void main(){ float depth = LinearizeDepth(gl_FragCoord.z) / far; // divide by far to get depth in range [0,1] for visualization purposes. color = vec4(vec3(depth), 1.0f);}其次展示的是頂點著色器:#version 330 corelayout (location = 0) in vec3 position;layout (location = 1) in vec2 texCoords;out vec2 TexCoords;uniform mat4 model;uniform mat4 view;uniform mat4 PRojection;void main(){ gl_Position = projection * view * model * vec4(position, 1.0f); TexCoords = texCoords;}下面就是處理Shader腳本的C++代碼,在這里把核心代碼接口展示一下,代碼如下所示:// 定義視口大小 glViewport(0, 0, screenWidth, screenHeight); // 啟用深度測試 glEnable(GL_DEPTH_TEST); // glDepthFunc(GL_ALWAYS); // Set to always pass the depth test (same effect as glDisable(GL_DEPTH_TEST)) // 編譯shader腳本 Shader shader("depth_testing.vs", "depth_testing.frag");在后面就是對Shader進行傳值操作:// 清空緩存 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(camera.Zoom, (float)screenWidth/(float)screenHeight, 0.1f, 100.0f); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));在這里把Shader類代碼展示如下:#ifndef SHADER_H#define SHADER_H#include <GL/glew.h>#include <string>#include <fstream>#include <sstream>#include <iostream>class Shader{public: GLuint Program; // Constructor generates the shader on the fly Shader(const GLchar* vertexPath, const GLchar* fragmentPath, const GLchar* geometryPath = nullptr) { // 1. Retrieve the vertex/fragment source code from filePath std::string vertexCode; std::string fragmentCode; std::string geometryCode; std::ifstream vShaderFile; std::ifstream fShaderFile; std::ifstream gShaderFile; // ensures ifstream objects can throw exceptions: vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); gShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); try { // Open files vShaderFile.open(vertexPath); fShaderFile.open(fragmentPath); std::stringstream vShaderStream, fShaderStream; // Read file's buffer contents into streams vShaderStream << vShaderFile.rdbuf(); fShaderStream << fShaderFile.rdbuf(); // close file handlers vShaderFile.close(); fShaderFile.close(); // Convert stream into string vertexCode = vShaderStream.str(); fragmentCode = fShaderStream.str(); // If geometry shader path is present, also load a geometry shader if(geometryPath != nullptr) { gShaderFile.open(geometryPath); std::stringstream gShaderStream; gShaderStream << gShaderFile.rdbuf(); gShaderFile.close(); geometryCode = gShaderStream.str(); } } catch (std::ifstream::failure e) { std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl; } const GLchar* vShaderCode = vertexCode.c_str(); const GLchar * fShaderCode = fragmentCode.c_str(); // 2. Compile shaders GLuint vertex, fragment; GLint success; GLchar infoLog[512]; // Vertex Shader vertex = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex, 1, &vShaderCode, NULL); glCompileShader(vertex); checkCompileErrors(vertex, "VERTEX"); // Fragment Shader fragment = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragment, 1, &fShaderCode, NULL); glCompileShader(fragment); checkCompileErrors(fragment, "FRAGMENT"); // If geometry shader is given, compile geometry shader GLuint geometry; if(geometryPath != nullptr) { const GLchar * gShaderCode = geometryCode.c_str(); geometry = glCreateShader(GL_GEOMETRY_SHADER); glShaderSource(geometry, 1, &gShaderCode, NULL); glCompileShader(geometry); checkCompileErrors(geometry, "GEOMETRY"); } // Shader Program this->Program = glCreateProgram(); glAttachShader(this->Program, vertex); glAttachShader(this->Program, fragment); if(geometryPath != nullptr) glAttachShader(this->Program, geometry); glLinkProgram(this->Program); checkCompileErrors(this->Program, "PROGRAM"); // Delete the shaders as they're linked into our program now and no longer necessery glDeleteShader(vertex); glDeleteShader(fragment); if(geometryPath != nullptr) glDeleteShader(geometry); } // Uses the current shader void Use() { glUseProgram(this->Program); }private: void checkCompileErrors(GLuint shader, std::string type) { GLint success; GLchar infoLog[1024]; if(type != "PROGRAM") { glGetShaderiv(shader, GL_COMPILE_STATUS, &success); if(!success) { glGetShaderInfoLog(shader, 1024, NULL, infoLog); std::cout << "| ERROR::::SHADER-COMPILATION-ERROR of type: " << type << "|/n" << infoLog << "/n| -- --------------------------------------------------- -- |" << std::endl; } } else { glGetProgramiv(shader, GL_LINK_STATUS, &success); if(!success) { glGetProgramInfoLog(shader, 1024, NULL, infoLog); std::cout << "| ERROR::::PROGRAM-LINKING-ERROR of type: " << type << "|/n" << infoLog << "/n| -- --------------------------------------------------- -- |" << std::endl; } } }};#endif另外在處理時需要用到攝像機代碼,完整代碼如下所示:#pragma once// Std. Includes#include <vector>// GL Includes#include <GL/glew.h>#include <glm/glm.hpp>#include <glm/gtc/matrix_transform.hpp>// Defines several possible options for camera movement. Used as abstraction to stay away from window-system specific input methodsenum Camera_Movement { FORWARD, BACKWARD, LEFT, RIGHT};// Default camera valuesconst GLfloat YAW = -90.0f;const GLfloat PITCH = 0.0f;const GLfloat SPEED = 3.0f;const GLfloat SENSITIVTY = 0.25f;const GLfloat ZOOM = 45.0f;// An abstract camera class that processes input and calculates the corresponding Eular Angles, Vectors and Matrices for use in OpenGLclass Camera{public: // Camera Attributes glm::vec3 Position; glm::vec3 Front; glm::vec3 Up; glm::vec3 Right; glm::vec3 WorldUp; // Eular Angles GLfloat Yaw; GLfloat Pitch; // Camera options GLfloat MovementSpeed; GLfloat MouseSensitivity; GLfloat Zoom; // Constructor with vectors Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), GLfloat yaw = YAW, GLfloat pitch = PITCH) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVTY), Zoom(ZOOM) { this->Position = position; this->WorldUp = up; this->Yaw = yaw; this->Pitch = pitch; this->updateCameraVectors(); } // Constructor with scalar values Camera(GLfloat posX, GLfloat posY, GLfloat posZ, GLfloat upX, GLfloat upY, GLfloat upZ, GLfloat yaw, GLfloat pitch) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVTY), Zoom(ZOOM) { this->Position = glm::vec3(posX, posY, posZ); this->WorldUp = glm::vec3(upX, upY, upZ); this->Yaw = yaw; this->Pitch = pitch; this->updateCameraVectors(); } // Returns the view matrix calculated using Eular Angles and the LookAt Matrix glm::mat4 GetViewMatrix() { return glm::lookAt(this->Position, this->Position + this->Front, this->Up); } // Processes input received from any keyboard-like input system. Accepts input parameter in the form of camera defined ENUM (to abstract it from windowing systems) void ProcessKeyboard(Camera_Movement direction, GLfloat deltaTime) { GLfloat velocity = this->MovementSpeed * deltaTime; if (direction == FORWARD) this->Position += this->Front * velocity; if (direction == BACKWARD) this->Position -= this->Front * velocity; if (direction == LEFT) this->Position -= this->Right * velocity; if (direction == RIGHT) this->Position += this->Right * velocity; } // Processes input received from a mouse input system. Expects the offset value in both the x and y direction. void ProcessMouseMovement(GLfloat xoffset, GLfloat yoffset, GLboolean constrainPitch = true) { xoffset *= this->MouseSensitivity; yoffset *= this->MouseSensitivity; this->Yaw += xoffset; this->Pitch += yoffset; // Make sure that when pitch is out of bounds, screen doesn't get flipped if (constrainPitch) { if (this->Pitch > 89.0f) this->Pitch = 89.0f; if (this->Pitch < -89.0f) this->Pitch = -89.0f; } // Update Front, Right and Up Vectors using the updated Eular angles this->updateCameraVectors(); } // Processes input received from a mouse scroll-wheel event. Only requires input on the vertical wheel-axis void ProcessMouseScroll(GLfloat yoffset) { if (this->Zoom >= 1.0f && this->Zoom <= 45.0f) this->Zoom -= yoffset; if (this->Zoom <= 1.0f) this->Zoom = 1.0f; if (this->Zoom >= 45.0f) this->Zoom = 45.0f; }private: // Calculates the front vector from the Camera's (updated) Eular Angles void updateCameraVectors() { // Calculate the new Front vector glm::vec3 front; front.x = cos(glm::radians(this->Yaw)) * cos(glm::radians(this->Pitch)); front.y = sin(glm::radians(this->Pitch)); front.z = sin(glm::radians(this->Yaw)) * cos(glm::radians(this->Pitch)); this->Front = glm::normalize(front); // Also re-calculate the Right and Up vector this->Right = glm::normalize(glm::cross(this->Front, this->WorldUp)); // Normalize the vectors, because their length gets closer to 0 the more you look up or down which results in slower movement. this->Up = glm::normalize(glm::cross(this->Right, this->Front)); }};以上就是關于深度測試的介紹,供參考。。。。。。。
新聞熱點
疑難解答