時(shí)渲染3D圓環(huán):SDF與Ray Marching實(shí)戰(zhàn))
從B站刷到那段Shader圓環(huán)特效的那一刻我整個(gè)人是愣住的——畫面里一個(gè)金屬質(zhì)感的3D圓環(huán)就這么在靜態(tài)照片上轉(zhuǎn)起來(lái)鏡頭還能繞過(guò)去看側(cè)面完全不像是“圖片里能跑出來(lái)的東西”。“還有高手”是當(dāng)時(shí)彈幕的真實(shí)反應(yīng)也是我想說(shuō)的。這件事真正有意思的點(diǎn)在于你不需要建模軟件也不需要視頻剪輯純粹用Shader在現(xiàn)代GPU上實(shí)時(shí)算出一個(gè)帶紋理的3D圓環(huán)在原本的二維圖片里自轉(zhuǎn)、公轉(zhuǎn)。整個(gè)過(guò)程的底層思維其實(shí)是一套完整的“從數(shù)學(xué)到像素”的渲染管線。今天這篇就拿GLSL/WebGL做載體把從零到跑起來(lái)的完整方案拆開講覆蓋原理、代碼、調(diào)參和踩坑記錄。這篇文章適合兩類人看一類是Shader入門之后想往上走一步的想知道“如何不用模型文件直接渲染三維物體”另一類是手頭有圖片素材、想在網(wǎng)頁(yè)或互動(dòng)作品里加一點(diǎn)“立體活物”效果的前端/互動(dòng)開發(fā)者。讀完你能復(fù)現(xiàn)一個(gè)可交互的旋轉(zhuǎn)圓環(huán)并且能理解它背后的SDF、光線步進(jìn)、UV貼圖三板斧是怎么回事。1. 整體設(shè)計(jì)與思路拆解把“2D圖片里的3D圓環(huán)”這個(gè)問(wèn)題拆成三塊1.1 先搞清楚你看到的效果到底是怎么算出來(lái)的我一開始也以為這個(gè)效果需要什么預(yù)烘焙的模型序列或者復(fù)雜的關(guān)鍵幀動(dòng)畫。真正動(dòng)手之后才明白它本質(zhì)上是在片元著色器里完成了一次“虛擬場(chǎng)景的實(shí)時(shí)求交”GPU對(duì)屏幕上每一個(gè)像素發(fā)出一條光線把這條光線丟進(jìn)一個(gè)只存在于函數(shù)里的3D場(chǎng)景一步步探測(cè)它是否碰到圓環(huán)表面碰到了就根據(jù)表面信息算顏色沒(méi)碰到就顯示原始圖片內(nèi)容。這里的核心關(guān)鍵詞是“函數(shù)即場(chǎng)景”。渲染一個(gè)圓環(huán)并不需要告訴GPU“圓環(huán)的頂點(diǎn)在哪”而是告訴它“空間中任意一個(gè)點(diǎn)離圓環(huán)表面有多遠(yuǎn)”。這個(gè)距離函數(shù)叫SDFSigned Distance Field有向距離場(chǎng)。GPU在每一幀對(duì)千萬(wàn)級(jí)別的射線做幾十次采樣最終在2D屏幕上還原出3D圓環(huán)。這種做法的妙處在于物體不是一堆離散三角形而是一個(gè)連續(xù)的函數(shù)旋轉(zhuǎn)、縮放、扭曲都變成了對(duì)函數(shù)的坐標(biāo)變換——這讓“讓圓環(huán)轉(zhuǎn)起來(lái)”成了加一行旋轉(zhuǎn)矩陣的事。為了讓照片場(chǎng)景和圓環(huán)融為一體我采用了兩層合成的思路先在光線步進(jìn)階段渲染出圓環(huán)的顏色和透明度再用透明度混合疊加到原圖片上。這樣原圖不用做任何預(yù)處理Shader跑起來(lái)就能看到一個(gè)圓環(huán)“長(zhǎng)”在圖片表面并轉(zhuǎn)動(dòng)。1.2 路徑選型為什么最終選擇Ray Marching SDF而不是傳統(tǒng)Mesh渲染在方案設(shè)計(jì)階段我考慮過(guò)三條路方案優(yōu)點(diǎn)缺點(diǎn)適用場(chǎng)景傳統(tǒng)Mesh渲染.obj模型貼圖美術(shù)流程成熟、貼圖直觀需要模型文件和解析器動(dòng)輒幾百KB和圖片融合生硬游戲引擎、正式3D場(chǎng)景頂點(diǎn)Shader做頂點(diǎn)動(dòng)畫性能極好只能整體晃動(dòng)無(wú)法呈現(xiàn)圓環(huán)厚度和內(nèi)部遮擋簡(jiǎn)單玩具效果Ray Marching SDF純代碼建模、零外部資源、天然帶遮擋/景深像素級(jí)計(jì)算著色器稍重參數(shù)需要調(diào)圖片合成、交互特效、金色大廳式演示我選了第三條。對(duì)“在圖片里放一個(gè)立體圓環(huán)并旋轉(zhuǎn)起來(lái)”這種場(chǎng)景Ray Marching是干凈利落的方案不需要文件資源、不依賴外部加載器甚至可以在ShaderToy里直接預(yù)覽。而且在2D圖片里要做出“真3D感”關(guān)鍵是物體邊緣要有正確的前后遮擋和自遮擋而這正是Ray Marching的強(qiáng)項(xiàng)——每一條光線都經(jīng)歷了真實(shí)的距離場(chǎng)求交看到的就是圓環(huán)在當(dāng)前角度下的真實(shí)輪廓。1.3 坐標(biāo)系和相機(jī)一張圖看懂圓環(huán)、屏幕與時(shí)間的關(guān)系在Shader里構(gòu)建3D場(chǎng)景第一步是定義坐標(biāo)系。我用的是右手坐標(biāo)系屏幕中心為原點(diǎn)X軸向右Y軸向上Z軸指向屏幕外。相機(jī)放在z軸負(fù)方向某個(gè)位置朝原點(diǎn)方向看。要讓圓環(huán)轉(zhuǎn)動(dòng)我選擇讓“場(chǎng)景”自己轉(zhuǎn)也就是對(duì)采樣點(diǎn)坐標(biāo)做時(shí)間相關(guān)的旋轉(zhuǎn)而不是移動(dòng)相機(jī)——這樣固定相機(jī)不會(huì)破壞原圖的構(gòu)圖圓環(huán)又能在照片上穩(wěn)定地自轉(zhuǎn)。圓環(huán)本體由兩個(gè)半徑控制主半徑R控制“環(huán)有多大”副半徑r控制“管子多粗”。這個(gè)參數(shù)化方式跟甜甜圈的造型邏輯一致。旋轉(zhuǎn)速度我設(shè)了slowRotate、tiltAngle、rollAngle三個(gè)自由度分別對(duì)應(yīng)繞Y軸自轉(zhuǎn)、繞X軸俯仰和繞Z軸翻滾。通常讓繞Y軸自轉(zhuǎn)為主其他兩個(gè)設(shè)為固定微調(diào)角度就能擺出很有節(jié)奏感的姿態(tài)。整個(gè)過(guò)程可以用一段GLSL偽代碼表示vec3 p worldPos - torusCenter; p.xz rotate2D(p.xz, u_time * spinSpeed); p rotateX(p, tiltAngle); p rotateZ(p, rollAngle); float d sdTorus(p, vec2(R, r));2. 核心實(shí)現(xiàn)細(xì)節(jié)與原理從數(shù)學(xué)公式到像素顏色2.1 圓環(huán)的距離函數(shù)sdTorus的推導(dǎo)與理解圓環(huán)距離函數(shù)看起來(lái)只有三行但它背后是幾何的理解。先看代碼float sdTorus(vec3 p, vec2 radii) { vec2 q vec2(length(p.xz) - radii.x, p.y); return length(q) - radii.y; }這個(gè)函數(shù)的思路是把三維點(diǎn)p先投影到XZ平面上算出它到圓心在XZ平面上的投影距離減去主半徑R后得到了“在圓環(huán)管的中心線上”的最近距離。這個(gè)結(jié)果再作為x坐標(biāo)把原始的y坐標(biāo)作為第二個(gè)分量組成一個(gè)二維向量q。此時(shí)問(wèn)題就變?yōu)檫@個(gè)二維點(diǎn)離原點(diǎn)也就是圓環(huán)截面的中心有多遠(yuǎn)再減去副半徑r就得到了三維點(diǎn)到圓環(huán)表面的真實(shí)距離。如果返回的值大于0點(diǎn)在圓環(huán)外小于0點(diǎn)在圓環(huán)內(nèi)部等于0剛好在表面。實(shí)際使用中我建議把sdTorus封裝為一個(gè)通用函數(shù)保留radii參數(shù)因?yàn)閾Q不同大小的環(huán)只需要改外部傳入的vec2不需要改函數(shù)內(nèi)部。這個(gè)函數(shù)的性能很好只用了兩個(gè)length和幾次加減在光追循環(huán)里調(diào)用128次也毫無(wú)壓力。2.2 光線步進(jìn)主循環(huán)射線怎么找到圓環(huán)表面有了距離函數(shù)接下來(lái)就是經(jīng)典的Ray Marching循環(huán)。對(duì)屏幕上每個(gè)像素先根據(jù)像素坐標(biāo)構(gòu)造一條射線起點(diǎn)是相機(jī)位置ro方向rd是把屏幕坐標(biāo)經(jīng)過(guò)相機(jī)透視變換后得到的單位向量。然后從t0開始不斷計(jì)算當(dāng)前點(diǎn)p到圓環(huán)的距離d如果d小于某個(gè)閾值說(shuō)明碰到了表面就跳出循環(huán)否則讓t前進(jìn)d這么遠(yuǎn)。為什么前進(jìn)距離恰好是d而不是固定步長(zhǎng)因?yàn)镾DF保證當(dāng)前點(diǎn)和場(chǎng)景表面之間至少隔著d距離直接跳到最近表面的位置是最快的路徑這個(gè)特性讓Ray Marching在數(shù)學(xué)上不會(huì)“穿透”物體。以下是完整的著色器主干段const int MAX_STEPS 128; const float MAX_DIST 12.0; const float EPS 0.0008; vec2 rayMarch(vec3 ro, vec3 rd) { float t 0.0; for (int i 0; i MAX_STEPS; i) { vec3 p ro rd * t; float d sdTorus(p, vec2(0.65, 0.28)); if (d EPS) { return vec2(t, 1.0); // 命中返回距離和標(biāo)記 } t d; if (t MAX_DIST) break; // 超出最遠(yuǎn)距離直接放棄 } return vec2(MAX_DIST, 0.0); // 未命中 }這個(gè)循環(huán)看似簡(jiǎn)單但隱藏著兩個(gè)重要細(xì)節(jié)。第一EPS判定閾值不能太大也不能太小太大時(shí)表面看起來(lái)會(huì)有一層“絨邊”太小則在低精度設(shè)備上容易漏掉薄面。我習(xí)慣的動(dòng)態(tài)方案參考穩(wěn)定在0.0008左右對(duì)圓環(huán)這個(gè)尺寸來(lái)說(shuō)足夠。第二如果場(chǎng)景里以后會(huì)加多個(gè)物體比如再加一個(gè)小球循環(huán)里需要分別對(duì)每個(gè)物體求距離取其中最小值作為這一步行進(jìn)距離。這部分代碼后續(xù)擴(kuò)展時(shí)可以預(yù)留一個(gè)getSceneDist函數(shù)把所有物體的距離函數(shù)統(tǒng)一丟進(jìn)去。2.3 表面法線與光照怎么讓圓環(huán)看起來(lái)“立體”Ray Marching得到命中點(diǎn)之后下一步是計(jì)算這個(gè)點(diǎn)的表面法線。傳統(tǒng)三角網(wǎng)格的法線是用叉積算的但我們的SDF場(chǎng)景沒(méi)有三角形所以我用數(shù)值微分法估算法線vec3 calcNormal(vec3 p) { vec2 e vec2(EPS, 0.0); return normalize(vec3( sdTorus(p e.xyy, params) - sdTorus(p - e.xyy, params), sdTorus(p e.yxy, params) - sdTorus(p - e.yxy, params), sdTorus(p e.yyx, params) - sdTorus(p - e.yyx, params) )); }思路很簡(jiǎn)單分別在x、y、z方向上各采樣兩次距離函數(shù)用差值近似梯度這個(gè)梯度就是法線方向。為此我需要把sdTorus的半徑參數(shù)提為全局變量或通過(guò)宏傳入保證calcNormal和rayMarch用的是同一組參數(shù)。光照我選了經(jīng)典的漫反射加一點(diǎn)高光和環(huán)境光float diffuse clamp(dot(normal, lightDir), 0.0, 1.0); float spec pow(clamp(reflect(-lightDir, normal).z, 0.0, 1.0), 32.0); vec3 col baseColor * (ambient diffuse * lightColor) spec * vec3(1.0);這套光照加上法線計(jì)算就能讓圓環(huán)在不同角度呈現(xiàn)出不同的亮暗——這是“立體感”的視覺(jué)根本。我的經(jīng)驗(yàn)是環(huán)境光不要給太高0.15左右就夠太高的環(huán)境光會(huì)讓圓環(huán)顯得發(fā)悶失去金屬質(zhì)感的對(duì)比度。光照方向我固定為左上角來(lái)跟常見UI圖標(biāo)的投影習(xí)慣保持一致也能利用好原圖中已有光照方向的對(duì)比。2.4 把2D圖片內(nèi)容變成圓環(huán)紋理UV映射的正確姿勢(shì)“圖片中的3D圓環(huán)”最誘人的一點(diǎn)是圓環(huán)表面可以帶原圖紋理而不是單純的一個(gè)顏色球。要讓紋理正確貼到環(huán)面上需要在命中點(diǎn)反算出它對(duì)應(yīng)的圓柱坐標(biāo)角度。我用的公式是vec2 torusUV(vec3 p) { float u atan(p.x, p.z) / 6.28318530 0.5; float v atan(p.y, length(p.xz) - R) / 6.28318530 0.5; return vec2(u, v); }其中u是繞Y軸旋轉(zhuǎn)一周的角度映射到[0,1]v是繞管截面一周的角度映射到[0,1]。這樣圓環(huán)表面上每個(gè)點(diǎn)都和圖片的二維坐標(biāo)建立了雙射圖片就能平鋪在環(huán)面上面。采樣時(shí)用texture2D(uTexture, uv)就能看到紋理隨著圓環(huán)轉(zhuǎn)動(dòng)而變化。這里有一個(gè)典型的坑直接用atan得到的角度在邊界處會(huì)從1跳到0造成一條明顯的接縫。要解決它紋理采樣時(shí)用fract使坐標(biāo)連續(xù)或者做兩層鏡像采樣來(lái)混合邊界。我實(shí)測(cè)fract在大多數(shù)情況下夠用紋理如果是無(wú)縫貼圖則完全看不出來(lái)如果是有明顯邊界的照片可以把UV映射稍微偏移讓接縫藏在圓環(huán)背面的暗部區(qū)域。3. 實(shí)操過(guò)程與核心代碼實(shí)現(xiàn)從零搭一個(gè)可復(fù)現(xiàn)的Shader圓環(huán)3.1 開發(fā)環(huán)境準(zhǔn)備ShaderToy作為快速原型工具我推薦用ShaderToy做開發(fā)不需要配置任何構(gòu)建工具瀏覽器打開就能跑。它的片段著色器默認(rèn)會(huì)提供iTime、iResolution、iChannel0、iMouse等uniform變量。把下面這段完整代碼丟進(jìn)去再在Channel 0里加載一張你喜歡的圖片就能第一時(shí)間看到效果。這里要注意ShaderToy使用的GLSL ES語(yǔ)法在精度聲明上有特定要求和標(biāo)準(zhǔn)桌面GLSL不完全一樣但本文代碼可以無(wú)縫兼容。precision highp float; uniform sampler2D uTexture; uniform vec2 uResolution; uniform float uTime; uniform vec2 uMouse; const float R 0.65; const float r 0.28; float sdTorus(vec3 p) { vec2 q vec2(length(p.xz) - R, p.y); return length(q) - r; } vec3 calcNormal(vec3 p) { vec2 e vec2(0.0008, 0.0); return normalize(vec3( sdTorus(p e.xyy) - sdTorus(p - e.xyy), sdTorus(p e.yxy) - sdTorus(p - e.yxy), sdTorus(p e.yyx) - sdTorus(p - e.yyx) )); } vec2 rayMarch(vec3 ro, vec3 rd) { float t 0.0; for (int i 0; i 96; i) { vec3 p ro rd * t; float d sdTorus(p); if (d 0.0008) return vec2(t, 1.0); t d; if (t 10.0) break; } return vec2(10.0, 0.0); } vec2 torusUV(vec3 p) { float u atan(p.x, p.z) / 6.28318530 0.5; float v atan(p.y, length(p.xz) - R) / 6.28318530 0.5; return vec2(u, v); } void main() { vec2 uv (gl_FragCoord.xy * 2.0 - uResolution) / min(uResolution.x, uResolution.y); vec3 ro vec3(0.0, 0.15, -2.6); vec3 rd normalize(vec3(uv, 1.8)); float rotY uTime * 0.6; float tiltX 0.35; float rollZ uTime * 0.12; // 旋轉(zhuǎn)射線起點(diǎn)和方向讓整個(gè)相機(jī)圍繞圓環(huán)旋轉(zhuǎn) // 這里采用翻轉(zhuǎn)坐標(biāo)的方式效果等于場(chǎng)景反向旋轉(zhuǎn) rd.xy mat2(cos(rollZ), -sin(rollZ), sin(rollZ), cos(rollZ)) * rd.xy; rd.xz mat2(cos(rotY), -sin(rotY), sin(rotY), cos(rotY)) * rd.xz; ro.xy mat2(cos(rollZ), -sin(rollZ), sin(rollZ), cos(rollZ)) * ro.xy; ro.xz mat2(cos(rotY), -sin(rotY), sin(rotY), cos(rotY)) * ro.xz; vec2 hit rayMarch(ro, rd); vec3 color texture2D(uTexture, uv * 0.5 0.5).rgb; if (hit.y 0.5) { vec3 p ro rd * hit.x; vec3 n calcNormal(p); vec3 lightDir normalize(vec3(-0.4, 0.8, 0.6)); float diff clamp(dot(n, lightDir), 0.0, 1.0); vec3 viewDir normalize(ro - p); vec3 halfDir normalize(lightDir viewDir); float spec pow(clamp(dot(n, halfDir), 0.0, 1.0), 48.0); vec3 texColor texture2D(uTexture, torusUV(p)).rgb; vec3 base mix(texColor, vec3(0.75, 0.85, 1.0), 0.35); vec3 shaded base * (0.12 0.85 * diff) vec3(1.0) * spec * 0.6; color mix(color, shaded, 0.92); } gl_FragColor vec4(color, 1.0); }這段代碼是完整可跑的。我把圖片既用作背景也用作紋理效果上圓環(huán)表面會(huì)反射出圖片的內(nèi)容視覺(jué)上非常統(tǒng)一??s放/移動(dòng)圓環(huán)只需要調(diào)整ro.z的距離和R的值即可。3.2 參數(shù)調(diào)節(jié)對(duì)照表與效果說(shuō)明自己上手跑的時(shí)候最容易問(wèn)的問(wèn)題是“為什么我的圓環(huán)看起來(lái)不對(duì)”。參數(shù)的影響往往是一環(huán)扣一環(huán)我整理了一張對(duì)照表參數(shù)作用范圍推薦初始值調(diào)大后的表現(xiàn)調(diào)小后的表現(xiàn)R主半徑圓環(huán)整體大小0.6~0.8圓環(huán)撐滿畫面變成小裝飾環(huán)r副半徑管的粗細(xì)0.25~0.35像游泳圈像細(xì)鐵絲rotY速度旋轉(zhuǎn)快慢0.5~0.8動(dòng)態(tài)感強(qiáng)但容易暈沉穩(wěn)適合展示EPS表面命中判定0.0008表面模糊、重影可能出現(xiàn)薄面穿透lightDir光照方向(-0.4,0.8,0.6)陰影位置變化立體感變化我在這上面花了不少時(shí)間找到一個(gè)規(guī)律R和r的比例最好保持在2.5到4之間太胖或太瘦都會(huì)讓圓環(huán)看起來(lái)像塑料面包或線圈不那么“高級(jí)”。旋轉(zhuǎn)速度跟圓環(huán)大小強(qiáng)相關(guān)圓環(huán)越大旋轉(zhuǎn)速度要適當(dāng)降低否則視覺(jué)上會(huì)覺(jué)得“甩得太快”。3.3 紋理合成的優(yōu)先級(jí)先顏色、再光照、再混合在實(shí)際合成中我調(diào)整過(guò)的順序很有講究。最開始時(shí)我先做背景再算圓環(huán)顏色然后直接替換背景色。結(jié)果發(fā)現(xiàn)圓環(huán)邊緣抗鋸齒很差而且背景和圓環(huán)疊加的部分會(huì)產(chǎn)生明顯的硬邊界。后來(lái)改為先算出完整的圓環(huán)著色結(jié)果再用透明度混合到背景上。透明度統(tǒng)一給0.92左右保留一點(diǎn)點(diǎn)背景透出這樣圓環(huán)有一種“和照片共生”的微妙質(zhì)感而不是一個(gè)硬貼上去的貼紙。另外一個(gè)細(xì)節(jié)是在命中圓環(huán)的像素上最終顏色應(yīng)該以圓環(huán)為主但仍然要摻一點(diǎn)點(diǎn)背景色讓邊緣有反光透進(jìn)來(lái)的錯(cuò)覺(jué)。這個(gè)混合比例我用0.08~0.15不同光照條件下可以微調(diào)。這個(gè)思路是從電影合成學(xué)里偷師的——真實(shí)物體永遠(yuǎn)不可能完全隔絕環(huán)境光。4. 常見問(wèn)題與調(diào)試實(shí)錄我踩過(guò)的那些坑4.1 圓環(huán)為什么“糊”了或者根本看不見我最初遇到的現(xiàn)象是圓環(huán)邊緣全是噪點(diǎn)像蒙了一層霧。排查半天發(fā)現(xiàn)是EPS設(shè)到了0.01判定距離太大導(dǎo)致射線在距離圓環(huán)表面還很遠(yuǎn)時(shí)就提前結(jié)束采樣點(diǎn)落在表面附近的震蕩區(qū)域法線方向亂跳。把EPS降到0.0008之后立刻恢復(fù)正常。如果你把代碼從桌面GLSL移植到WebGL還要注意float精度問(wèn)題建議在精度聲明里標(biāo)明highp否則移動(dòng)設(shè)備的mediump會(huì)導(dǎo)致距離函數(shù)在大坐標(biāo)時(shí)精度不足出現(xiàn)嚴(yán)重的塊狀噪點(diǎn)。如果圓環(huán)整個(gè)看不見優(yōu)先檢查相機(jī)方向和射線方向構(gòu)造。我把rd寫成normalize(vec3(uv, 1.8))這個(gè)1.8是等效焦距常數(shù)相當(dāng)于一個(gè)焦距偏廣角的鏡頭。如果你把它改得太小比如0.5視野會(huì)變得非常窄圓環(huán)可能跑出屏幕太大則圓環(huán)變小到看不清。建議固定在1.5~2.0之間。4.2 紋理滾動(dòng)的邊界接縫問(wèn)題紋理接縫是最影響觀感的問(wèn)題之一。我之前在一個(gè)深色背景下用帶漸變的紋理結(jié)果圓環(huán)側(cè)面有一條肉眼可見的豎線。原因正如前面提到的atan在從正π到負(fù)π跳變時(shí)紋理坐標(biāo)瞬間從1回到0采樣的顏色發(fā)生了斷裂。我的解決思路是給uv做平滑過(guò)渡判斷uv.x如果大于0.8就額外混合一次uv.x-1.0的采樣結(jié)果形成一個(gè)小的左右漸變帶。這個(gè)技巧加了之后雖然接縫處沒(méi)有完全消失但肉眼幾乎看不出斷裂了。更專業(yè)的做法是把紋理做成四方連續(xù)圖案讓接縫天然隱藏。4.3 性能分析和優(yōu)化老核顯也能穩(wěn)穩(wěn)60幀ShaderToy上跑這個(gè)效果時(shí)默認(rèn)情況下90多次光線步進(jìn)在高端顯卡上毫無(wú)壓力但集成顯卡上可能會(huì)掉到30幀以下。我做了一系列優(yōu)化最終讓一臺(tái)2018年的辦公本也跑到60幀把MAX_STEPS從128降到80縫隙視覺(jué)上沒(méi)有明顯變化但循環(huán)計(jì)算量下降37.5%提前退出命中后立刻返回不進(jìn)入后續(xù)步進(jìn)。背景大片區(qū)域在第一二次距離測(cè)試時(shí)就因?yàn)榫嚯x過(guò)大而提前break利用這個(gè)特性平均每像素只跑十幾次采樣關(guān)閉高光pow計(jì)算當(dāng)diff等于0時(shí)如果法線完全背向光源直接跳過(guò)Specular計(jì)算把動(dòng)態(tài)分支盡量寫成靜態(tài)判斷不要在每個(gè)像素里對(duì)uniform做復(fù)雜的分支例如把紋理混合比例作為常量寫死這些優(yōu)化做完后profiling數(shù)據(jù)很直觀圓環(huán)區(qū)域的采樣數(shù)量是背景區(qū)域的5~8倍但由于背景占畫面大部分平均壓力大幅降低。在這方面我的心得是“不要在還沒(méi)畫出來(lái)的地方浪費(fèi)算力”。4.4 其它常見問(wèn)題速查表現(xiàn)象可能原因解決方案圓環(huán)位置偏左/偏右射線方向與屏幕寬高比適配不對(duì)調(diào)整uv橫軸除以aspectRatio或者用uResolution.min紋理拉伸嚴(yán)重torusUV角度映射范圍不對(duì)確認(rèn)atan結(jié)果加0.5后落在[0,1]區(qū)間圓環(huán)閃爍/抖動(dòng)EPS過(guò)小或超出MAX_DIST邊界增大EPS到0.001或增加最大距離圓環(huán)和背景不融合混合透明度設(shè)得太高或太低調(diào)整mix系數(shù)到0.85~0.95區(qū)間只有背景看不見圓環(huán)旋轉(zhuǎn)矩陣用錯(cuò)或ro方向反了打印ro和rd檢查旋轉(zhuǎn)軸是否與期望一致5. 進(jìn)階實(shí)操與個(gè)人體會(huì)5.1 從單個(gè)圓環(huán)擴(kuò)展到組合場(chǎng)景圓環(huán)做完之后我很快就想給它加鄰居同場(chǎng)景里放幾個(gè)不同大小、不同旋轉(zhuǎn)方向的圓環(huán)形成一種星球環(huán)繞的效果。這里最重要的一點(diǎn)是把sdTorus抽成帶半徑參數(shù)的版本然后在getSceneDist里對(duì)每個(gè)圓環(huán)做一次獨(dú)立的坐標(biāo)偏移和旋轉(zhuǎn)返回所有圓環(huán)距離的最小值。光線步進(jìn)循環(huán)本身不用改一個(gè)圓形陣列就能形成豐富的構(gòu)圖。試過(guò)之后你會(huì)體會(huì)到SDF的組合邏輯非常有魅力把場(chǎng)景想成一組函數(shù)每個(gè)物體都往最近的表面貢獻(xiàn)距離碰撞自然被處理。如果加上iMouse交互讓圓環(huán)跟隨鼠標(biāo)旋轉(zhuǎn)則會(huì)大幅提升互動(dòng)感。基本思路是用鼠標(biāo)坐標(biāo)構(gòu)造一組目標(biāo)旋轉(zhuǎn)角再用lerp平滑過(guò)渡避免突變的跳躍。我個(gè)人覺(jué)得這個(gè)交互很適合放到個(gè)人主頁(yè)或者作品集里做一個(gè)全屏的“3D圓環(huán)控制臺(tái)”。5.2 我為什么建議你在圖片上做偽3D而不是直接上WebGL模型最后分享一個(gè)自己的體會(huì)很多人第一次看到“圖片里的3D圓環(huán)轉(zhuǎn)起來(lái)”時(shí)第一反應(yīng)是“這不是應(yīng)該用Three.js加載一個(gè)模型嗎”但實(shí)際做下來(lái)我反而覺(jué)得用SDF加紋理貼圖的方式更適合這類“單圖片增強(qiáng)”的場(chǎng)景。原因是2D圖片天然沒(méi)有深度信息模型再怎么真實(shí)也是“插入”圖片而不是“長(zhǎng)在”圖片里。而SDF方式可以通過(guò)光照方向、模糊陰影和半透明混合讓圓環(huán)和圖片之間生成視覺(jué)上的“共同環(huán)境”融合度反而更高。如果你想真正做圖片和3D物體融合的互動(dòng)作品這個(gè)思路比傳統(tǒng)模型管線更具性價(jià)比。我在實(shí)際使用中也養(yǎng)成了一個(gè)習(xí)慣每次調(diào)整完參數(shù)都會(huì)刻意截一張圓環(huán)在靜態(tài)狀態(tài)下的圖單獨(dú)檢查邊緣、接縫、光照是否自然。靜態(tài)觀察到了一定細(xì)膩程度動(dòng)起來(lái)的效果才有品質(zhì)感。這算是踩了無(wú)數(shù)坑之后沉淀出來(lái)的笨辦法但確實(shí)是有效的。