OpenGLRender開發記錄(二): 陰影(shadowMap,PCF,PCSS)

目錄

  • 已實現功能
  • 陰影
    • shadowMap
    • PCF
    • PCSS
  • 實現
    • shadowMap
    • PCF
    • PCSS
    • 陰影

GitHub主頁:https://github.com/sdpyy1
OpenGLRender:https://github.com/sdpyy1/CppLearn/tree/main/OpenGL

已實現功能

除了上次實現IBL之外,項目目前新增了imGUI的渲染,更方便地進行調試
在這里插入圖片描述
可以隨意切換IBL貼圖、模型控制、燈光控制燈。 并且添加了一個PBR材質的地板。下一步就是實現陰影

陰影

shadowMap

這個東西很簡單,直接實現了,不講原理

PCF

取周圍一圈的像素求平均,讓陰影更軟

PCSS

① Blocker Search 階段(尋找遮擋者)
目的:估算遮擋物與被遮擋物之間的距離 → 用于計算 penumbra(陰影模糊程度)

步驟:
從當前 fragment 的 light space 坐標,投影到 shadow map 中:projCoords.xy

以該位置為中心,在 shadow map 中進行 小范圍采樣(通常 3x3 或 5x5):

收集所有 比當前 fragment 深度更小的樣本(說明它們擋住了光)

累加這些“遮擋者”的深度值

記錄 blocker 數量

若存在 blocker:

計算平均 blocker 深度 avgBlockerDepth

② Penumbra Size 計算階段(決定模糊程度)
目的:用當前 fragment 深度 與 avgBlockerDepth 的距離估算光源發散導致的陰影模糊程度

③ Filtering 階段(模糊陰影邊緣)
目的:根據 penumbra 大小,用可變范圍 PCF 模糊陰影邊緣

所以PCSS可以叫自適應PCF

實現

shadowMap

初始化一個FBO用于shadowMap的渲染,創建一張紋理存儲深度結果

void ShadowPass::init() {glGenFramebuffers(1, &shadowFBO);// 創建深度紋理glGenTextures(1, &shadowMap);glBindTexture(GL_TEXTURE_2D, shadowMap);glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,scene.width, scene.height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);// attach 深度紋理到FBOglBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowMap, 0);glDrawBuffer(GL_NONE);glReadBuffer(GL_NONE);glBindFramebuffer(GL_FRAMEBUFFER, 0);isInit = true;
}

下一步就是在光源視角下渲染,首先得找到攝像機的位置,其實就是MVP矩陣的VP用光源而不是用攝像機,下面是對于平行光的shadowMap渲染

void ShadowPass::render() {if (!isInit){std::cout << "shadowPass init" << std::endl;return;}glViewport(0, 0, scene.width, scene.height);glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);glClear(GL_DEPTH_BUFFER_BIT);//    glCullFace(GL_FRONT); // 可選,防止 Peter-panningshadowShader.bind();glm::mat4 lightProjection, lightView;float orthoSize = 10.0f;float near_plane = 0.1f;float far_plane = 100.0f; // 你可以再根據場景大小動態調整// 平行光使用正交投影lightProjection = glm::ortho(-orthoSize, orthoSize, -orthoSize, orthoSize, near_plane, far_plane);// TODO:只實現了平行光,他的position存儲的是方向,而不是位置,所以要取反lightView = glm::lookAt(-scene.lights[0]->position * 10.0f, glm::vec3(0.0f), glm::vec3(0.0, 1.0, 0.0));lightSpaceMatrix = lightProjection * lightView;shadowShader.setMat4("lightSpaceMatrix", lightSpaceMatrix);// 渲染所有模型(只寫深度)for (auto& model : scene.models) {glm::mat4 modelMatrix = model.getModelMatrix();shadowShader.setMat4("model", modelMatrix);model.draw(shadowShader);}shadowShader.unBind();
//    glCullFace(GL_BACK);glBindFramebuffer(GL_FRAMEBUFFER, 0);glViewport(0, 0, scene.width, scene.height);
}

頂點著色器,物體要乘以lightSpaceMatrix來轉到光源視角下

#version 330 core
layout (location = 0) in vec3 aPos;uniform mat4 model;
uniform mat4 lightSpaceMatrix;void main() {gl_Position = lightSpaceMatrix * model * vec4(aPos, 1.0);
}

片段著色器不需要執行操作

#version 330 core
void main() {// 空著就行,只寫深度
}

下一步將生成的shadowMap傳遞到lightPass來參與光照計算,另外需要把lightMatrix也傳遞過去,用于把模型距離值轉移到視角下來進行比較

float ShadowCalculation(vec3 fragPosWorld, vec3 normal) {vec4 fragPosLightSpace = lightSpaceMatrix * vec4(fragPosWorld, 1.0);vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;projCoords = projCoords * 0.5 + 0.5; // [-1,1] → [0,1]// 從深度貼圖采樣float closestDepth = texture(shadowMap, projCoords.xy).r;float currentDepth = projCoords.z;// 簡單 bias 防止 shadow acne
//    float bias = max(0.005 * (1.0 - dot(normal, normalize(lightPos))), 0.001);// 超出邊界不產生陰影if (projCoords.z > 1.0)return 0.0;// 進行一次比較return (currentDepth) > closestDepth ? 1.0 : 0.0;
}

有了計算陰影的函數后,只需要在計算光照的最后*(1-shadow)

    vec3 Lo = (kD * albedo / PI + specular) * radiance * NdotL * (1-ShadowCalculation(WorldPos, N));

結果如下,經典的自陰影現象,主要原因就是shadowMap的分辨率不足,一些不在同一高度的位置被記錄了相同高度,在主攝像機渲染時,某個位置在shadowMap存儲的高度比自己本來還要高,就會被認為是陰影(但是這種情況下很好分辨光源攝像機的覆蓋范圍,方便調試🙂)
在這里插入圖片描述
當我把shadowMap的分辨率提高后,自然就消失了,但這肯定不是最優在這里插入圖片描述
通常的做法是加一個自偏移,也就是說shadowMap存的高度和我用來比較的高度差異不超過bias,就認為沒有陰影

float ShadowCalculation(vec3 fragPosWorld, vec3 normal) {vec4 fragPosLightSpace = lightSpaceMatrix * vec4(fragPosWorld, 1.0);vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;projCoords = projCoords * 0.5 + 0.5; // [-1,1] → [0,1]// 從深度貼圖采樣float closestDepth = texture(shadowMap, projCoords.xy).r;float currentDepth = projCoords.z;// 簡單 bias 防止 shadow acnefloat bias = max(0.005 * (1.0 - dot(normal, normalize(lightPos))), 0.001);// 超出邊界不產生陰影if (projCoords.z > 1.0)return 0.0;// 進行一次比較return (currentDepth - bias) > closestDepth ? 1.0 : 0.0;
}

在這里插入圖片描述

PCF

陰影問題解決了,下面就是提升效果,當前的陰影是硬陰影,鋸齒很嚴重
在這里插入圖片描述
在這里插入圖片描述
PCF思路就是取周圍像素的shadow來取平均,柔化陰影邊界

        // --- PCF ---float shadow = 0.0;ivec2 texSize = textureSize(shadowMap, 0);vec2 texelSize = 1.0 /vec2(texSize);int range = 10;  // 5x5int samples = 0;for (int x = -range; x <= range; ++x) {for (int y = -range; y <= range; ++y) {float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;shadow += (currentDepth - bias > pcfDepth) ? 1.0 : 0.0;samples++;}}shadow /= float(samples);return shadow;

在這里插入圖片描述

PCSS

三步走,一些參數我已經提取出去當uniform,可以控制第一步的搜索半徑、第二步的半影大小、以及控制一下最大的濾波核大小

        float avgBlockerDepth = 0.0;int blockers = 0;ivec2 texSize = textureSize(shadowMap, 0);vec2 texelSize = 1.0 / vec2(texSize);int searchRadius = int(PCSSBlockerSearchRadius);for (int x = -searchRadius; x <= searchRadius; ++x) {for (int y = -searchRadius; y <= searchRadius; ++y) {float sampleDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;if (sampleDepth < currentDepth - bias) {avgBlockerDepth += sampleDepth;blockers++;}}}if (blockers == 0) return 0.0;avgBlockerDepth /= blockers;float penumbra = (currentDepth - avgBlockerDepth) * PCSSScale;int kernel = int(clamp(penumbra * float(texSize.x), 1.0, PCSSKernelMax));float shadow = 0.0;for (int x = -kernel; x <= kernel; ++x) {for (int y = -kernel; y <= kernel; ++y) {float sampleDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;shadow += (currentDepth - bias > sampleDepth) ? 1.0 : 0.0;}}shadow /= float((2 * kernel + 1) * (2 * kernel + 1));return shadow;

陰影

三種陰影可以寫在一起

float ShadowCalculation(vec3 fragPosWorld, vec3 normal) {vec4 fragPosLightSpace = lightSpaceMatrix * vec4(fragPosWorld, 1.0);vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;projCoords = projCoords * 0.5 + 0.5;if (projCoords.z > 1.0) return 0.0;float closestDepth = texture(shadowMap, projCoords.xy).r;float currentDepth = projCoords.z;float bias = max(0.005 * (1.0 - dot(normal, normalize(lightPos))), 0.001);// shadow type switchingif (shadowType == 0) {return 0.0; // no shadow}else if (shadowType == 1) {return (currentDepth - bias > closestDepth) ? 1.0 : 0.0; // hard shadow}else if (shadowType == 2) {// --- PCF ---float shadow = 0.0;ivec2 texSize = textureSize(shadowMap, 0);vec2 texelSize = 1.0 /vec2(texSize);int range = pcfScope;int samples = 0;for (int x = -range; x <= range; ++x) {for (int y = -range; y <= range; ++y) {float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;shadow += (currentDepth - bias > pcfDepth) ? 1.0 : 0.0;samples++;}}shadow /= float(samples);return shadow;}else if (shadowType == 3) {float avgBlockerDepth = 0.0;int blockers = 0;ivec2 texSize = textureSize(shadowMap, 0);vec2 texelSize = 1.0 / vec2(texSize);int searchRadius = int(PCSSBlockerSearchRadius);for (int x = -searchRadius; x <= searchRadius; ++x) {for (int y = -searchRadius; y <= searchRadius; ++y) {float sampleDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;if (sampleDepth < currentDepth - bias) {avgBlockerDepth += sampleDepth;blockers++;}}}if (blockers == 0) return 0.0;avgBlockerDepth /= blockers;float penumbra = (currentDepth - avgBlockerDepth) * PCSSScale;int kernel = int(clamp(penumbra * float(texSize.x), 1.0, PCSSKernelMax));float shadow = 0.0;for (int x = -kernel; x <= kernel; ++x) {for (int y = -kernel; y <= kernel; ++y) {float sampleDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;shadow += (currentDepth - bias > sampleDepth) ? 1.0 : 0.0;}}shadow /= float((2 * kernel + 1) * (2 * kernel + 1));return shadow;}return 0.0;
}

在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/90749.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/90749.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/90749.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Linux:日志亂碼

1、Linux日志亂碼可能是XShell客戶端編碼沒設置為UTF-8引起的&#xff0c;按照以下步驟&#xff0c;設置終端格式&#xff1a;中文版&#xff1a;打開Xshell會話屬性&#xff08;文件→屬性→終端→編碼&#xff09;&#xff0c;選擇與服務器一致的編碼格式&#xff08;如UTF-8…

Rouge:面向摘要自動評估的召回導向型指標——原理、演進與應用全景

“以n-gram重疊量化文本生成質量&#xff0c;為摘要評估提供可計算標尺” Rouge&#xff08;Recall-Oriented Understudy for Gisting Evaluation&#xff09; 是由 南加州大學信息科學研究所&#xff08;ISI&#xff09;的Chin-Yew Lin 于2004年提出的自動文本摘要評估指標&am…

[STM32][HAL]stm32wbxx 超聲波測距模塊實現(HY-SRF05)

前言 在電子技術應用中,距離測量是一個常見且重要的需求。超聲波模塊因其測量精度較高、成本較低、易于使用等優點,被廣泛應用于機器人避障、液位檢測、智能停車系統等領域。該文主要講解以stm32wb芯片為主控,用HAL庫來對HY-SRF05超聲波模塊進行代碼編寫,實現基本的驅動和測…

MySQL 性能調優實戰指南:從診斷到優化全解析

引言在日常的數據庫運維工作中&#xff0c;我們經常需要對 MySQL 數據庫進行診斷和性能分析。本文將介紹一套全面的 MySQL 診斷腳本&#xff0c;適用于 MySQL 8.0&#xff08;兼容 8.0.15 及以上版本&#xff09;&#xff0c;涵蓋事務鎖分析、性能瓶頸定位、配置檢查、連接狀態…

8. 狀態模式

目錄一、應用背景二、狀態模式2.1 解決的問題2.2 角色2.3 實現步驟三、通用設計類圖四、實現4.1 設計類圖4.2 狀態轉換圖4.3 代碼實現一、應用背景 某對象發生變化時&#xff0c;其所能做的操作也隨之變化。應用程序的可維護性和重用性差代碼的邏輯較復雜 二、狀態模式 2.1 …

php語法--foreach和in_array的使用

文章目錄foreach基礎語法&#xff1a;案例1&#xff1a;引用傳遞模式&#xff1a;嵌套數組處理&#xff1a;避免在循環中計算數組長度&#xff1a;使用引用減少內存拷貝&#xff1a;打印數組in_array基礎使用嚴格使用foreach 基礎語法&#xff1a; foreach ($iterable as $va…

ES6模塊詳解:核心語法與最佳實踐

以下是 EMAScript 6&#xff08;ES6&#xff09;模塊規范的核心要點及細節解析&#xff1a; &#x1f4e6; 一、核心語法導出&#xff08;export&#xff09; 命名導出&#xff1a;支持導出多個具名成員。export const a 1; export function b() { /* ... */ } // 或集中導出 …

Python day25

浙大疏錦行 Python day25. 內容&#xff1a; 異常處理&#xff0c;在日常的編碼工作過程中&#xff0c;為了避免由于各種bug導致的異常情況&#xff0c;我們需要引入異常處理機制&#xff0c;它的工作場景是當程序運行出現意外時&#xff0c;可以根據編碼規則處理響應的錯誤。…

mac llama_index agent算術式子計算示例

本文通過簡單數學計算&#xff0c;示例llama_index使用agent解決復雜任務過程。 假設mac本地llama_index環境已安裝&#xff0c;過程參考 mac測試ollama llamaindex-CSDN博客 測試mac筆記本內存8G&#xff0c;所以使用較小LLM完成示例。 ollama pull qwen3:1.7b qwen3:1.7b能…

uni-app小程序云效持續集成

創建項目 必須是 cli 命令行創建的 uni-app 小程序項目參考uni-app官方構建命令&#xff1a; npx degit dcloudio/uni-preset-vue#vite-ts my-vue3-project生成小程序代碼上傳密鑰 管理-開發設置-小程序代碼上傳生成的文件放在根目錄即可 安裝持續集成插件 pnpm install uni-mi…

uniapp+高德地圖實現打卡簽到、打卡日歷

一、注冊高德地圖。應用管理創建應用&#xff0c;分別添加Andriod平臺、Web服務、Web端、微信小程序四種類型的key。二、考勤規則打卡地點選擇位置代碼&#xff1a;<script setup lang"ts"> import { onMounted, onUnmounted, reactive, ref, watchEffect } fr…

CentOS 7.9 + GCC9 離線安裝 IWYU(Include What You Use)

本教程適用于 離線環境下在 CentOS 7.9 系統中使用 GCC 9 離線安裝 IWYU 的完整步驟&#xff0c;涵蓋 Clang 11.1.0 編譯、IWYU 構建以及頭文件自動優化流程。&#x1f4e5; 一、準備安裝包請提前下載以下源碼包&#xff08;可通過在線機器提前下載&#xff0c;再傳輸到離線環境…

基于Dapr Sidecar的微服務通信框架設計與性能優化實踐

基于Dapr Sidecar的微服務通信框架設計與性能優化實踐 一、技術背景與應用場景 隨著微服務架構的廣泛應用&#xff0c;分布式系統中服務間通信、可觀察性、可靠性等問題日益凸顯。Dapr&#xff08;Distributed Application Runtime&#xff09;作為一個開源的微服務運行時&…

Claude Code 超詳細完整指南(2025最新版)

&#x1f680; 終端AI編程助手 | 高頻使用點 生態工具 完整命令參考 最新MCP配置 &#x1f4cb; 目錄 &#x1f3af; 快速開始&#xff08;5分鐘上手&#xff09;&#x1f4e6; 詳細安裝指南 系統要求Windows安裝&#xff08;WSL方案&#xff09;macOS安裝Linux安裝安裝驗…

【lucene】SegmentReader初始化過程概述

readers[i] new SegmentReader(sis.info(i), sis.getIndexCreatedVersionMajor(), IOContext.READ); 這個方法已經把所有的文件都讀完了么&#xff1f;沒有“讀完”&#xff0c;但已經**全部“打開”**了。| 動作 | 是否發生 | |---|---| | **打開文件句柄 / mmap** | ? 立即完…

通俗理解主機的BIOS和UEFI啟動方式

“對于 22.04 版本&#xff0c;這些操作說明應適用于通過 BIOS 或 UEFI 兩種方式創建和運行啟動盤。”我們來詳細解釋一下這句話的含義&#xff0c;這句話的核心意思是&#xff1a;你按照這個教程制作出來的 Ubuntu U 盤&#xff0c;將擁有極佳的兼容性&#xff0c;無論是在老電…

Canal 1.1.7的安裝

數據庫操作的準備 1、開啟 Binlog 寫入功能&#xff0c;配置 binlog-format 為 ROW 模式&#xff0c;my.cnf 中配置如下: vi /etc/my.cnf [mysqld] log-binmysql-bin # 開啟 binlog binlog-formatROW # 選擇 ROW 模式 server_id1 # 配置 MySQL replaction 需要定義&#xff0c;…

python---類型轉換

文章目錄1. 基本類型轉換函數int() - 轉換為整數float() - 轉換為浮點數str() - 轉換為字符串bool() - 轉換為布爾值2. 其他類型轉換list() - 轉換為列表tuple() - 轉換為元組set() - 轉換為集合&#xff08;去重&#xff09;dict() - 轉換為字典3. 注意事項1. 兼容性&#xff…

JVM terminated. Exit code=1

出現JVM terminated. Exit code1錯誤通常是因為 Eclipse 所需的 Java 版本與系統中配置的 Java 版本不匹配。從錯誤信息中可以看到關鍵線索&#xff1a;-Dosgi.requiredJavaVersion21&#xff0c;表示此 Eclipse 版本需要 Java 21 或更高版本&#xff0c;但系統當前使用的是 Ja…

20250727-1-Kubernetes 網絡-Ingress介紹,部署Ingres_筆記

一、NodePort存在的不足 ?1. 四層負載均衡 ?? 實現技術: 基于iptables和ipvs實現 OSI層級: 位于傳輸層(第四層) 轉發依據: 基于IP地址和端口進行轉發 特點: 只能看到IP和端口信息 無法識別應用層協議內容 配置簡單但功能有限 2. 七層負載均衡 ?1)七層負載均衡的概念 ?…