1. 程式人生 > >Computer Graphics(7)

Computer Graphics(7)

Shadow Mapping陰影貼圖

基本思路

陰影貼圖的思想較為簡單:首先選擇光源所在的位置為視角進行渲染,按照陰影產生的原理,我們所能看到的東西能被點亮,而反之看不到的部分則處在陰影之中。
容易想到的解決思路是:對光源發出的射線上的點進行遍歷,並記錄第一個與物體相交的點。如果在這條光射線上的點比這個交點距離光源的距離更遠,那麼較遠的點處在陰影之中。
但是在渲染過程中逐一對不同方向上的射線、同一射線上的無數個點進行計算比較顯然是不切實際的,所以考慮開啟深度測試,使用深度測試的方法來簡化實現的過程。
這裡我們考慮從光源的透檢視來渲染場景,並將深度值的結果儲存在紋理之中——也就是說,對光源的透檢視所見的最近的深度值進行取樣,所得到的這個深度值就是我們在光源的角度下透檢視能夠見到的第一個片元。所有的這些深度值被稱作深度貼圖(depth map)。
有了深度貼圖之後,我們可以在渲染原有基本場景的基礎上直接使用深度貼圖來計算片元是否需要調整成陰影即可。

STEP1深度貼圖的獲取

  • 幀緩衝的概念
    在前面的作業中,我們用到的螢幕緩衝有很多:用於寫入顏色值的顏色緩衝、用於寫入深度資訊的深度緩衝和允許我們根據一些條件來丟棄特定片段的模板緩衝。所有的這些緩衝結合起來叫做幀緩衝(Frame Buffer)。
    個人理解:幀緩衝可以看做是某一幀對應的所有資訊的合集。而在我們前面實現的場景中,我們是在預設的幀緩衝上進行的,如果設定了自己的幀緩衝,那麼我們可以直接對已經存在的場景進行處理(而不是需要重新建立符合新要求的場景)。
    幀緩衝的具體實現方式參考教程。
  • 準備工作
    我們需要將深度貼圖儲存在一個紋理中來用於後續對於陰影的計算,所以首先我們為深度貼圖建立一個幀緩衝物件(FBO):
GLuint depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);

然後我們為這個幀緩衝建立一些附件,並將這些附件附加到幀緩衝上。
首先是紋理附件。按照作業四中的方法建立一個紋理,不同之處在於此次實驗中我們只關心深度值,所以將紋理的格式指定為GL_DEPTH_COMPONENT,然後將生成的深度紋理作為幀緩衝的深度緩衝附加到幀緩衝上:

glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
// 紋理附加
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0
)
; // 在這次作業中,我們需要的僅僅是深度緩衝,顏色緩衝是沒有必要的 // 所以需要設定下面兩個語句來告訴OpenGL我們不使用任何顏色資料進行渲染 glDrawBuffer(GL_NONE); glReadBuffer(GL_NONE); glBindFramebuffer(GL_FRAMEBUFFER, 0);
  • 生成深度貼圖(render loop中的處理)
    使用深度貼圖來渲染場景。與前面的處理結合,這裡分兩步進行渲染:首先渲染深度貼圖,然後使用深度貼圖渲染場景:
// 從光源的角度渲染深度緩衝
simpleDepthShader.use();
simpleDepthShader.setMat4("lightSpaceMatrix", lightSpaceMatrix);
// viewpoint設定!!
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
// 關於depthMap深度貼圖的著色器內容設定,見STEP2
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, woodTexture);
renderScene(simpleDepthShader);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

// 重置視點
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// 利用深度貼圖渲染場景
// --------------------------------------------------------------
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader.use();
glm::mat4 projection = glm::perspective(camera.Zoom, (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
...// 設定shader
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, woodTexture);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, depthMap);
renderScene(shader);
  • 光源設定
    這裡是對光源空間的變換。用來確保為物體選擇了合適的投影(正交/透視)和檢視矩陣(使用lookat獲得)。
    首先是對於光線的選擇:(bonus1內容)
// 透視光線,通常用於點光源、舞臺光線等。同時注意如果選擇使用透視光線,光源對應的著色器也需要進行相應的處理
if (perspective) {
    Shader debugDepthQuad("debug_quad.vs.txt", "debug_quad_perspective.fs.txt");
    lightProjection = glm::perspective(glm::radians(45.0f)*10, (GLfloat)SHADOW_WIDTH / (GLfloat)SHADOW_HEIGHT, near_plane, far_plane);
    ortho = false;
}
// 正交投影光線,用於平行光線的設定,如太陽光等
if (ortho) {
    lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);
    perspective = false;
}

接著,由於我們需要從光源的角度來觀察場景,那麼檢視矩陣可以採用lookat函式獲取:

glm::mat4 lightView = glm::lookAt(glm::vec(-2.0f, 4.0f, -1.0f), glm::vec3(0.0f), glm::vec3(1.0));

二者相乘就得到了我們在光源空間內變換需要的矩陣:glm::mat4 lightSpaceMatrix = lightProjection * lightView;

STEP2渲染至深度貼圖(shader source的處理)

在STEP1中,我們已經知道了如何得到深度貼圖,並且知道了利用光的透檢視(深度貼圖)渲染的大致過程。下面是我們在渲染過程中需要用到的著色器的設定。

  • 將頂點變換到光源空間(對應深度貼圖獲取過程中的渲染深度緩衝)
    即對cube的頂點進行lightSpaceMatrix變換,使其變換到光源空間,以便進一步獲取深度貼圖。所以在頂點著色器中對頂點位置處理即可;而由於這裡僅僅是對深度進行處理,所以片段著色器設定為空即可:
// vertex shader
#version 330 core
layout (location = 0) in vec3 position;
uniform mat4 lightSpaceMatrix;
uniform mat4 model;
void main()
{
    gl_Position = lightSpaceMatrix * model * vec4(position, 1.0f);
}
  • 深度貼圖渲染到四邊形上
    對於正交投影的處理較為簡單,
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D depthMap;
uniform float near_plane;
uniform float far_plane;
// 透視投影時用到,將非線性的深度值轉換為線性的,從而得到易於觀察的深度值
float LinearizeDepth(float depth)
{
    float z = depth * 2.0 - 1.0; // Back to NDC 
    return (2.0 * near_plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane));    
}
void main()
{             
    float depthValue = texture(depthMap, TexCoords).r;
    // FragColor = vec4(vec3(LinearizeDepth(depthValue) / far_plane), 1.0); // 透視
    FragColor = vec4(vec3(depthValue), 1.0); // 正交
}

STEP3渲染陰影

STEP1、2已經得到了完整的深度貼圖,接下來就是生成陰影的步驟,顯然依舊是在著色器(shader mapping對應的著色器)中對場景進行處理。
首先是在頂點著色器中,我們需要對場景中的片元判斷其是否在陰影之中,實現如下:

void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0f);

    vs_out.FragPos = vec3(model * vec4(position, 1.0));
    vs_out.Normal = transpose(inverse(mat3(model))) * normal;
    vs_out.TexCoords = texCoords;
    // 變換到光源空間的座標   
    vs_out.FragPosLightSpace = lightSpaceMatrix * vec4(vs_out.FragPos, 1.0);
}

而對於片段著色器,我們選擇Phong模型進行渲染。對於陰影部分,我們設定一個shadow值,如果片元在陰影內則shadow=1,反之則shadow=0。回憶上次作業中,Phong模型中主要的光照分量是環境光、漫反射光和鏡面反射光,顯然這裡我們需要將陰影對應的係數shadow與漫反射分量和鏡面反射分量相乘即可。
但是在實際實現的過程中,我們需要對shadow的值進行更細緻的處理,這裡放在函式ShadowCalculation中,實現如下:

float ShadowCalculation(vec4 fragPosLightSpace)
{
    // 透視除法,對於正交投影沒有任何影響
    vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
    // normalize
    projCoords = projCoords * 0.5 + 0.5;
    // 獲取光源座標下最近的深度值
    float closestDepth = texture(shadowMap, projCoords.xy).r; 
    // 獲取當前片元在光源座標下的深度值
    float currentDepth = projCoords.z;



    // 越界處理
    if(projCoords.z > 1.0)
        shadow = 0.0;

    return shadow;
}

然後將這個shadow係數新增到lighting結果的計算中去即可:

float shadow = ShadowCalculation(fs_in.FragPosLightSpace);       
    vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color; 

STEP4陰影優化

  • 針對陰影中的線條
    這種情況是由於多個片元從同一個深度值取樣所造成的。比如當多個片元從同一個斜坡的深度紋理畫素中取樣時,有的在地板上有的在地板下,也就是說有的片元被認為在陰影中,有的不在,於是產生了條紋。可以通過新增偏移值處理:
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
float shadow = currentDepth - bias > closestDepth  ? 1.0 : 0.0;
  • 針對場景一半明亮一半暗
    這是由於取樣過多和座標超出光的正交視錐的結果。可以通過儲存一個邊框顏色,然後將深度貼圖的紋理環繞選項設定為GL_CLAMP_TO_BORDER來解決取樣過多的問題:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
GLfloat borderColor[] = { 1.0, 1.0, 1.0, 1.0 };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

而對於第二種情況,可以通過檢查邊界來優化。在上面我們已經處理過。

Bonus

1、分別實現正交、透視投影下的shadow mapping

在前面已經實現。具體需要修改的地方是render loop中新增對投影方式的選擇,以及shadow mapping對應片段著色器中著色方式。

2、陰影優化

  • 對陰影鋸齒邊的簡單處理
    PCF的思路是從深度貼圖中多次取樣,然後對不同的結果進行平均即可得到較柔和的陰影,實現如下:
float shadow = 0.0;
vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
// 多次取樣並平均
for(int x = -1; x <= 1; ++x)
{
    for(int y = -1; y <= 1; ++y)
    {
        float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r; 
        shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;        
    }    
}
shadow /= 9.0;