渲染:從光路原理到WGSL實(shí)踐)
在 Web 端做玻璃材質(zhì)表面看只是把透明度調(diào)低實(shí)際上要同時(shí)處理透射、折射、菲涅爾反射、吸收和環(huán)境反射任何一環(huán)簡(jiǎn)化過(guò)頭物體看起來(lái)就像塑料或者金屬。WebGPU 作為新一代 Web Graphics API把渲染管線、著色器、資源和同步方式交給開(kāi)發(fā)者顯式控制配套使用 WGSL 著色語(yǔ)言這讓 Web 端渲染玻璃材質(zhì)時(shí)比 WebGL 時(shí)代有了更清晰的實(shí)現(xiàn)路徑。這篇文章會(huì)從一個(gè)最小可運(yùn)行的 WebGPU 場(chǎng)景入手實(shí)現(xiàn)一個(gè)玻璃球的效果并解釋背后的光路計(jì)算、顏色緩沖重采樣、參數(shù)調(diào)節(jié)和常見(jiàn)問(wèn)題排查方式。這篇文章適合已經(jīng)了解 WebGL 或圖形學(xué)基礎(chǔ)、想遷移到 WebGPU 的開(kāi)發(fā)者。讀完可以親手跑通一個(gè)包含折射與菲涅爾反射的示例也能把同樣的思路遷移到更完整的場(chǎng)景中。1. 先搞清楚玻璃材質(zhì)在實(shí)時(shí)渲染里到底難點(diǎn)在哪1.1 玻璃的視覺(jué)特征不是單一屬性玻璃給人的視覺(jué)印象由四件事共同決定透射光線穿過(guò)玻璃繼續(xù)傳播背景物體仍然可見(jiàn)。折射光線進(jìn)入玻璃后方向改變背景被扭曲。菲涅爾反射視線與玻璃表面夾角越平反射越強(qiáng)。吸收光線穿過(guò)有一定厚度的玻璃時(shí)某些波長(zhǎng)被吸收玻璃呈現(xiàn)顏色。這四件事不能分開(kāi)處理。只做透射不處理折射玻璃就是一個(gè)透明色塊只做反射不處理折射玻璃就變成鏡子不處理菲涅爾反射強(qiáng)度和視角無(wú)關(guān)畫面會(huì)非常假。1.2 實(shí)時(shí)渲染通常用近似而不是完整模擬真實(shí)玻璃需要求解折射積分、多次內(nèi)反射和散射成本極高。實(shí)時(shí)渲染采用一組工程近似背景先渲染成一張顏色紋理玻璃片元采樣這張紋理就得到透射顏色。用 Snell 折射定律算一個(gè)偏移方向采樣背景紋理時(shí)加上偏移就得到折射扭曲。用 Schlick 菲涅爾近似計(jì)算反射占比在反射顏色和折射顏色之間混合。厚度相關(guān)的吸收用簡(jiǎn)單的指數(shù)衰減模擬。這套組合在離線渲染和實(shí)時(shí)渲染里方向一致區(qū)別只在數(shù)值精度和實(shí)現(xiàn)邊界。WebGPU 里可以用 texture_2d 保存背景顏色用 texture_cube 保存環(huán)境反射用 WGSL 在片元著色器里完成上述計(jì)算。1.3 WebGL 時(shí)代的痛點(diǎn)在哪里WebGL 2 已經(jīng)能實(shí)現(xiàn)類似效果但工程上很別扭渲染管線的狀態(tài)分散在大量全局 API 調(diào)用里難以組織。平臺(tái)擴(kuò)展差異大例如 float 紋理、MRT、離屏渲染在不同設(shè)備上表現(xiàn)不一致。沒(méi)有統(tǒng)一的計(jì)算著色器抽象需要各種 hack。資源的生命周期、緩沖和紋理同步需要開(kāi)發(fā)者自己維護(hù)缺少 bind group 這樣的結(jié)構(gòu)化綁定方式。WebGPU 把渲染邏輯拆成 Pipeline、BindGroup、RenderPass 等可獨(dú)立描述的對(duì)象GPU 驅(qū)動(dòng)可以更充分地重排和復(fù)用資源。對(duì)玻璃材質(zhì)這種需要多 pass 配合、離屏紋理采樣和反射貼圖查詢的場(chǎng)景代碼組織方式清晰很多。2. 環(huán)境準(zhǔn)備特性檢測(cè)、瀏覽器支持和最小工程2.1 瀏覽器支持和特性檢測(cè)WebGPU 的 API 形態(tài)已經(jīng)比較穩(wěn)定但不同瀏覽器仍在持續(xù)演進(jìn)。以近年來(lái)的主流瀏覽器狀態(tài)看Chrome 和 Edge 已經(jīng)默認(rèn)開(kāi)啟 WebGPUSafari 在較新版本中也提供了支持Firefox 仍在實(shí)驗(yàn)通道中。這個(gè)結(jié)論會(huì)隨瀏覽器版本變化所以代碼里永遠(yuǎn)應(yīng)該做特性檢測(cè)而不是默認(rèn)瀏覽器一定支持。async function initWebGPU(canvas: HTMLCanvasElement) { if (!navigator.gpu) { throw new Error(當(dāng)前瀏覽器不支持 WebGPU請(qǐng)升級(jí)瀏覽器或開(kāi)啟實(shí)驗(yàn)特性); } const adapter await navigator.gpu.requestAdapter(); if (!adapter) { throw new Error(沒(méi)有可用的 GPUAdapter請(qǐng)檢查瀏覽器和顯卡驅(qū)動(dòng)); } const device await adapter.requestDevice(); const context canvas.getContext(webgpu) as GPUCanvasContext; const format navigator.gpu.getPreferredCanvasFormat(); context.configure({ device, format, alphaMode: premultiplied, }); return { device, context, format }; }getPreferredCanvasFormat很重要不同平臺(tái)合成器對(duì)bgra8unorm和rgba8unorm的處理效率不同不要寫死格式。alphaMode如果使用 premultiplied后續(xù)紋理和混合系數(shù)也要按預(yù)乘 alpha 的思路處理。2.2 瀏覽器端支持矩陣參考瀏覽器狀態(tài)使用建議Chrome / Edge默認(rèn)支持開(kāi)發(fā)調(diào)試首選Safari 較新版本已提供 WebGPU發(fā)布前必須真機(jī)驗(yàn)證Firefox實(shí)驗(yàn)通道不能作為唯一目標(biāo)環(huán)境特性檢測(cè)代碼放在項(xiàng)目入口最前面。如果檢測(cè)失敗直接展示降級(jí)頁(yè)面不要讓它帶著錯(cuò)誤繼續(xù)渲染。2.3 最小工程結(jié)構(gòu)使用 TypeScript 和 Vite 是最快捷的組合不需要引入任何 WebGPU 封裝庫(kù)先體驗(yàn)原生 API。npm create vitelatest webgpu-glass -- --template vanilla-ts cd webgpu-glass npm install npm run dev工程里核心文件只有兩個(gè)index.html放 canvas。src/main.ts負(fù)責(zé)初始化、創(chuàng)建流水線、啟動(dòng)渲染循環(huán)。!doctype html html langzh-CN body canvas idgpu-canvas width1024 height768/canvas script typemodule src/src/main.ts/script /body /html不要急著寫玻璃材質(zhì)。先確保 WebGPU 設(shè)備能創(chuàng)建、render pass能執(zhí)行、三角形能畫出來(lái)否則后續(xù)排查時(shí)無(wú)法判斷是圖形邏輯問(wèn)題還是環(huán)境問(wèn)題。3. 玻璃材質(zhì)背后的三塊核心計(jì)算3.1 把視覺(jué)現(xiàn)象拆成可編程參數(shù)視覺(jué)現(xiàn)象物理來(lái)源實(shí)時(shí)近似方式關(guān)鍵參數(shù)反射表面 FresnelSchlick 近似f0粗糙度折射Snell 定律偏移 UV 采樣背景eta透射光線穿透采樣背景紋理背景紋理吸收介質(zhì)厚度指數(shù)衰減absorption 系數(shù)環(huán)境反射遠(yuǎn)距離環(huán)境光Cube Map 采樣環(huán)境貼圖這套參數(shù)是后續(xù)著色器代碼的骨架。把參數(shù)列清楚寫 WGSL 時(shí)只是把公式填進(jìn)去。3.2 Fresnel 的 Schlick 近似真實(shí)世界中的 Fresnel 隨入射角變化但精確求解非常昂貴。Schlick 給出一個(gè)五階多項(xiàng)式近似F(cosTheta) F0 (1 - F0) * pow(1 - cosTheta, 5)其中F0是垂直入射時(shí)的反射率。玻璃這類電介質(zhì)的F0大約在0.04左右。金屬的F0帶顏色且數(shù)值高這就是為什么玻璃材質(zhì)不能直接用金屬那套 PBR 參數(shù)。WGSL 實(shí)現(xiàn)fn fresnelSchlick(cosTheta: f32, f0: vec3f) - vec3f { return f0 (vec3f(1.0) - f0) * pow(1.0 - cosTheta, 5.0); }cosTheta應(yīng)該使用max(dot(normal, viewDir), 0.0)做一次截?cái)啾苊獗趁婀庹债a(chǎn)生負(fù)值。3.3 折射方向的 Snell 定律折射方向可以用eta * viewDir - (eta * cosI sqrt(k)) * normal計(jì)算其中eta是入射介質(zhì)折射率除以折射介質(zhì)折射率??諝庹凵渎始s1.0玻璃折射率約1.5所以光線從空氣進(jìn)入玻璃時(shí)eta 1.0 / 1.5。fn refractDirection(viewDir: vec3f, normal: vec3f, eta: f32) - vec3f { let cosI clamp(dot(viewDir, normal), -1.0, 1.0); let k 1.0 - eta * eta * (1.0 - cosI * cosI); if (k 0.0) { return vec3f(0.0); // 全反射方向非法 } return eta * viewDir - (eta * cosI sqrt(k)) * normal; }當(dāng)k 0.0時(shí)發(fā)生全反射此時(shí)不應(yīng)該采樣折射應(yīng)該直接返回反射顏色。這個(gè)分支在玻璃邊緣和扁平表面尤其重要。3.4 Screen-Space Refraction把背景當(dāng)作折射源透射和折射都需要讀取背景顏色。實(shí)時(shí)渲染里最直接的方法是把不透明場(chǎng)景先渲染到一張紋理玻璃片元按折射方向偏移采樣坐標(biāo)再乘上 Fresnel 權(quán)重。這個(gè)方法叫 Screen-Space Refraction優(yōu)點(diǎn)是成本低、效果好缺點(diǎn)是需要額外一次 Render Pass而且偏移過(guò)大時(shí)會(huì)出現(xiàn)背景邊緣缺失。偏移量需要根據(jù)eta和物體表面到背景的距離做保守控制。4. 實(shí)現(xiàn)一個(gè)可運(yùn)行的玻璃球示例4.1 先規(guī)劃 Pass 順序再寫代碼玻璃材質(zhì)的正確渲染順序是Opague Pass渲染場(chǎng)景中所有不透明物體到sceneTexture。Glass Pass渲染玻璃球采樣sceneTexture獲得透射顏色采樣環(huán)境立方體貼圖獲得反射顏色最后輸出到 canvas。如果只有一個(gè) pass玻璃沒(méi)有背景可采樣折射無(wú)從談起。如果先渲染玻璃后渲染背景玻璃會(huì)被背景覆蓋。Pass 順序本身就是透明材質(zhì)渲染約束的一部分。4.2 創(chuàng)建離屏顏色紋理sceneTexture必須同時(shí)具備RENDER_ATTACHMENT和TEXTURE_BINDING兩種 usage前者用于不透明 pass 寫入后者用于玻璃 pass 讀取。const sceneTexture device.createTexture({ size: { width: canvas.width, height: canvas.height }, format: rgba16float, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, }); const sceneTextureView sceneTexture.createView();這里使用rgba16float而不是普通 8 位紋理是為了保留更高的顏色精度。玻璃折射會(huì)壓縮或拉伸顏色范圍低精度紋理容易出現(xiàn)色帶。4.3 不透明 Pass寫背景進(jìn)紋理function renderOpaquePass( commandEncoder: GPUCommandEncoder, pipeline: GPURenderPipeline, uniformBindGroup: GPUBindGroup, sceneTextureView: GPUTextureView ) { const passEncoder commandEncoder.beginRenderPass({ colorAttachments: [ { view: sceneTextureView, clearValue: { r: 0.12, g: 0.18, b: 0.25, a: 1.0 }, loadOp: clear, storeOp: store, }, ], }); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, uniformBindGroup); passEncoder.draw(36); // 示例中使用簡(jiǎn)單立方體或幾何體 passEncoder.end(); }這個(gè) pass 不需要寫玻璃球只負(fù)責(zé)給玻璃提供背景。實(shí)際項(xiàng)目中可以放一張桌子、幾本書或者任意有細(xì)節(jié)的模型。4.4 玻璃球的 WGSL 著色器先定義 uniform 結(jié)構(gòu)存放矩陣和材質(zhì)參數(shù)struct SceneUniform { viewProjection: mat4x4f, worldMatrix: mat4x4f, cameraPosition: vec3f, eta: f32, // 折射率比空氣/玻璃 thickness: f32, // 吸收厚度 padding: vec2f, };頂點(diǎn)著色器負(fù)責(zé)計(jì)算世界坐標(biāo)、法線、裁剪坐標(biāo)和屏幕 UVstruct VSInput { location(0) position: vec3f, location(1) normal: vec3f, }; struct VSOutput { builtin(position) clipPosition: vec4f, location(0) worldPosition: vec3f, location(1) worldNormal: vec3f, location(2) screenUV: vec2f, }; group(0) binding(0) varuniform scene: SceneUniform; vertex fn vs_main(in: VSInput) - VSOutput { var out: VSOutput; out.worldPosition (scene.worldMatrix * vec4f(in.position, 1.0)).xyz; out.clipPosition scene.viewProjection * vec4f(out.worldPosition, 1.0); out.worldNormal mat3x3f(scene.worldMatrix) * in.normal; out.screenUV out.clipPosition.xy / out.clipPosition.w * 0.5 0.5; out.screenUV.y 1.0 - out.screenUV.y; return out; }注意屏幕 UV 的 Y 軸翻轉(zhuǎn)。WebGPU 的 NDC 坐標(biāo)和 UV 坐標(biāo)方向不同不翻轉(zhuǎn)會(huì)導(dǎo)致折射采樣上下顛倒。片元著色器是玻璃材質(zhì)核心group(0) binding(1) var sceneTexture: texture_2df32; group(0) binding(2) var sceneSampler: sampler; group(0) binding(3) var envTexture: texture_cubef32; group(0) binding(4) var envSampler: sampler; fn fresnelSchlick(cosTheta: f32, f0: vec3f) - vec3f { return f0 (vec3f(1.0) - f0) * pow(1.0 - cosTheta, 5.0); } fn refractDirection(viewDir: vec3f, normal: vec3f, eta: f32) - vec3f { let cosI clamp(dot(viewDir, normal), -1.0, 1.0); let k 1.0 - eta * eta * (1.0 - cosI * cosI); if (k 0.0) { return vec3f(0.0); } return eta * viewDir - (eta * cosI sqrt(k)) * normal; } fragment fn fs_main(in: VSOutput) - location(0) vec4f { let viewDir normalize(scene.cameraPosition - in.worldPosition); let normal normalize(in.worldNormal); let cosTheta clamp(dot(viewDir, normal), 0.0, 1.0); let f0 vec3f(0.04); let fresnel fresnelSchlick(cosTheta, f0); // 折射采樣 let refractDir refractDirection(-viewDir, normal, scene.eta); let uvOffset refractDir.xy * 0.06; let refractedColor textureSample(sceneTexture, sceneSampler, in.screenUV uvOffset).rgb; // 反射采樣 let reflectDir reflect(-viewDir, normal); let reflectedColor textureSample(envTexture, envSampler, reflectDir).rgb; // 吸收 let absorption exp(-vec3f(0.6, 0.3, 0.1) * scene.thickness); let mixedColor mix(refractedColor * absorption, reflectedColor, fresnel); // 玻璃透明度Fresnel 越高反射占比越大 let alpha clamp(max(fresnel.r, max(fresnel.g, fresnel.b)) 0.2, 0.0, 1.0); return vec4f(mixedColor, alpha); }fresnelSchlick的結(jié)果是vec3f因?yàn)樗С植噬?F0。玻璃的 F0 接近無(wú)色但擴(kuò)展成彩色 F0 后可以表現(xiàn)有色玻璃。alpha的計(jì)算在這里是簡(jiǎn)化處理讓玻璃的輪廓不完全透明。真正要精確控制應(yīng)該引入透射率模型。4.5 玻璃 Pass 輸出到 canvas玻璃 pass 直接寫入 canvas 的當(dāng)前紋理function renderGlassPass( commandEncoder: GPUCommandEncoder, pipeline: GPURenderPipeline, bindGroup: GPUBindGroup, canvasTexture: GPUTextureView ) { const passEncoder commandEncoder.beginRenderPass({ colorAttachments: [ { view: canvasTexture, clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, loadOp: clear, storeOp: store, }, ], }); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.draw(vertexCount); passEncoder.end(); }玻璃 pass 采樣的是sceneTexture寫入的是canvasTexture兩者是不同紋理不會(huì)產(chǎn)生讀寫沖突。4.6 主循環(huán)串聯(lián)function frame() { const commandEncoder device.createCommandEncoder(); renderOpaquePass( commandEncoder, opaquePipeline, opaqueBindGroup, sceneTextureView ); const currentTexture context.getCurrentTexture(); renderGlassPass( commandEncoder, glassPipeline, glassBindGroup, currentTexture.createView() ); device.queue.submit([commandEncoder.finish()]); requestAnimationFrame(frame); }為什么getCurrentTexture要放在每個(gè)幀渲染時(shí)調(diào)用而不是初始化時(shí)調(diào)用一次因?yàn)槊總€(gè)幀 canvas 背后的紋理都會(huì)由瀏覽器的交換鏈重新分配提前保存的紋理 view 會(huì)指向已經(jīng)失效或不屬于當(dāng)前幀的資源。5. 關(guān)鍵參數(shù)調(diào)節(jié)與生產(chǎn)環(huán)境注意點(diǎn)5.1 參數(shù)速查表參數(shù)含義推薦起始值調(diào)大效果調(diào)小效果eta折射率比0.67折射彎曲更小折射彎曲更強(qiáng)f0垂直反射率0.04玻璃更亮更硬玻璃更透明thickness吸收厚度0.5玻璃顏色更深玻璃更通透uvOffset 系數(shù)折射偏移強(qiáng)度0.05背景扭曲更夸張背景扭曲更弱eta是入射介質(zhì)與折射介質(zhì)的折射率比值不是玻璃絕對(duì)折射率。如果玻璃環(huán)境變?yōu)樗聢?chǎng)景空氣和水的折射率就要一起更換。5.2 環(huán)境立方體貼圖的兩種來(lái)源玻璃反射效果依賴環(huán)境貼圖。生產(chǎn)項(xiàng)目可以從 HDR 環(huán)境貼圖轉(zhuǎn)換出 Cube Map 六面圖再上傳為texture_cube。學(xué)習(xí)項(xiàng)目可以先用程序生成漸變貼圖const envTexture device.createTexture({ size: { width: 256, height: 256, depthOrArrayLayers: 6 }, format: rgba8unorm, usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST, dimension: 2d, });上傳時(shí)每個(gè) face 對(duì)應(yīng)一個(gè)depthOrArrayLayers層。需要處理六張圖的 Y 軸朝向否則反射會(huì)上下顛倒。5.3 從學(xué)習(xí)環(huán)境切到生產(chǎn)環(huán)境要補(bǔ)什么學(xué)習(xí)示例通常只有一顆玻璃球、一張背景紋理、一個(gè)固定光源。生產(chǎn)環(huán)境要額外補(bǔ)MSAA 抗鋸齒玻璃邊緣和透明區(qū)域是鋸齒重災(zāi)區(qū)。深度處理玻璃和不透明物體之間要有正確的深度遮擋關(guān)系。物體排序多個(gè)透明物體會(huì)相互影響需要按距離排序。Render Bundle把靜態(tài)場(chǎng)景的繪制指令緩存起來(lái)減少 CPU 開(kāi)銷。錯(cuò)誤處理requestAdapter、requestDevice都可能失敗要給出明確反饋。性能監(jiān)控fragment shader 里同時(shí)采樣sceneTexture和envTexture移動(dòng)端開(kāi)銷明顯要做 draw call 和 shader 成本預(yù)算。6. 常見(jiàn)問(wèn)題排查從現(xiàn)象倒推根因6.1 全屏黑屏現(xiàn)象頁(yè)面能加載但 canvas 一直黑屏。可能原因WebGPU 初始化失敗、Render Pass 代碼沒(méi)有執(zhí)行、clear value 全部為 0。檢查方式控制臺(tái)查看是否有navigator.gpu報(bào)錯(cuò)在renderOpaquePass和renderGlassPass里分別加日志確認(rèn)兩個(gè) pass 都執(zhí)行了。解決先只跑不透明 pass確認(rèn)背景能顯示再接入玻璃 pass。6.2 折射采樣上下顛倒現(xiàn)象玻璃球里的背景倒立。原因頂點(diǎn)著色器沒(méi)有翻轉(zhuǎn) screenUV.y或者 cube map face 方向不一致。檢查方式把折射偏移改成 0看背景是否居中對(duì)比 canvas 的背景方向。解決頂點(diǎn)著色器里out.screenUV.y 1.0 - out.screenUV.y同時(shí)檢查環(huán)境貼圖上傳時(shí)的 face 朝向。6.3 玻璃邊緣反射太亮或太暗現(xiàn)象球體邊緣一片純白或者幾乎看不到反射。原因cosTheta沒(méi)有 clamp或者 f0 設(shè)置過(guò)大。檢查方式把fresnelSchlick的輸出接到return vec4f(fresnel, 1.0)單獨(dú)觀察 Fresnel 權(quán)重。解決確保cosTheta clamp(dot(viewDir, normal), 0.0, 1.0)玻璃 f0 用 0.04 起步。6.4 Bind Group 匹配報(bào)錯(cuò)現(xiàn)象創(chuàng)建 pipeline 后報(bào)bind group layout不匹配或運(yùn)行時(shí)報(bào)Buffer binding size mismatch。原因uniform 結(jié)構(gòu)體字節(jié)對(duì)齊不符合 WGSL 規(guī)則或者 bind group layout 與 pipeline layout 不一致。檢查方式對(duì)比createBindGroupLayout和 pipeline 的layout配置檢查 uniform 成員是否加了align或補(bǔ)齊 padding。解決統(tǒng)一用同一份 layout 描述創(chuàng)建 pipeline 和 bind group不要分別手寫兩份。6.5 玻璃遮蔽關(guān)系錯(cuò)誤現(xiàn)象玻璃后面的物體顯示在玻璃前面。原因玻璃 pass 沒(méi)有參與深度測(cè)試或兩個(gè) pass 使用不同 depth texture。檢查方式打開(kāi) WebGPU 的驗(yàn)證層輸出檢查 depth attachment 配置。解決不透明 pass 和玻璃 pass 使用同一個(gè) depth texture玻璃 pass 開(kāi)啟 depth test關(guān)閉 depth write 或按語(yǔ)義決定。7. 可復(fù)用檢查清單與擴(kuò)展方向7.1 玻璃材質(zhì)上線前檢查清單瀏覽器特性檢測(cè)是否在頁(yè)面入口執(zhí)行。canvas 和紋理尺寸是否匹配getCurrentTexture是否在每幀獲取。sceneTexture是否同時(shí)具備 RENDER_ATTACHMENT 和 TEXTURE_BINDING。screenUV 是否翻轉(zhuǎn)。cosTheta是否 clamp。eta是否是折射率比而不是單一折射率。Fresnel 輸出是否單獨(dú)驗(yàn)證過(guò)。吸收系數(shù)是否與玻璃厚度成正比。玻璃和不透明物體是否共享深度。移動(dòng)端是否測(cè)試過(guò)真機(jī)性能和能耗。環(huán)境貼圖是否包含反射方向正確的六面。7.2 擴(kuò)展方向真正的高質(zhì)量玻璃材質(zhì)會(huì)繼續(xù)加這些能力多次折射與內(nèi)反射用 compute shader 模擬光在玻璃內(nèi)部的多次彈射適合離線或局部場(chǎng)景。粗糙玻璃對(duì)折射采樣使用不同 mip level粗糙度越高采樣越模糊。焦散光線穿過(guò)玻璃后在地面形成亮斑需要額外的光子或 shadow pass 配合。包含折射的 PBR 管線把玻璃的透射和反射參數(shù)接入標(biāo)準(zhǔn) PBR 材質(zhì)輸入。對(duì)于初學(xué)者最有價(jià)值的練習(xí)不是直接抄代碼而是調(diào)參數(shù)。每改一個(gè)值觀察它對(duì) Fresnel、折射、吸收的影響。能把一個(gè)玻璃球的參數(shù)調(diào)到視覺(jué)上線再看懂這些公式在 WGSL 里的位置就可以去挑戰(zhàn)完整的 PBR 材質(zhì)系統(tǒng)了。