效果大致如下(本來(lái)想上傳GIF圖的,可惜大于10M),還有下面的筆記是直接從MD復(fù)制過(guò)來(lái)的,有點(diǎn)丑,感興趣的可以看看我的MD版,顏值高多了,附鏈接https://jmx-paper.oss-cn-beijing.aliyuncs.com/ShaderToy%E4%BC%98%E7%A7%80%E4%BB%A3%E7%A0%81%E9%98%85%E8%AF%BB/Elevated%E4%BB%A3%E7%A0%81%E8%A7%A3%E6%9E%90.md
Elevated代碼解析
作者:iq,網(wǎng)址:https://www.shadertoy.com/view/MdX3Rr
標(biāo)簽:procedural, 3d, raymarching, distancefield, terrain, motionblur
總共兩個(gè)部分:Image,Buffer A
Image
voidmainImage(outvec4fragColor,invec2fragCoord)
{
vec2uv=fragCoord/iResolution.xy;
vec4data=texture(iChannel0,uv);
?
vec3col=vec3(0.0);
if(data.w<0.0)
?? {
col=data.xyz;
?? }
else
?? {
// decompress velocity vector
floatss=mod(data.w,256.0)/255.0;
floatst=floor(data.w/256.0)/255.0;
?
// motion blur (linear blur across velocity vectors
vec2dir=(-1.0+2.0*vec2(ss,st))*0.25;
col=vec3(0.0);
for(inti=0;i<32;i++)
? ? ?? {
floath=float(i)/31.0;
vec2pos=uv+dir*h;
col+=texture(iChannel0,pos).xyz;
? ? ?? }
col/=32.0;
?? }
// vignetting?
? ? col*=0.5+0.5*pow(16.0*uv.x*uv.y*(1.0-uv.x)*(1.0-uv.y),0.1);
?
col=clamp(col,0.0,1.0);
col=col*0.6+0.4*col*col*(3.0-2.0*col)+vec3(0.0,0.0,0.04);
?
fragColor=vec4(col,1.0);
}
讀取BufferA的計(jì)算結(jié)果,據(jù)此來(lái)說(shuō),xyz分量存儲(chǔ)的是最終計(jì)算結(jié)果(color),w存的是速度向量。
vec4data=texture(iChannel0,uv);
如果速度小于0,則說(shuō)明場(chǎng)景靜止,直接取xyz分量,否則進(jìn)行運(yùn)動(dòng)模糊(motion blur)
進(jìn)行運(yùn)動(dòng)模糊時(shí),首先進(jìn)行對(duì)速度矢量進(jìn)行解壓縮。
floatss=mod(data.w,256.0)/255.0;
floatst=floor(data.w/256.0)/255.0;
第一個(gè)是用w分量對(duì)256求模,然后除以255,第二個(gè)是用w分量除以246,取整后除以255,為什么這樣解碼,估計(jì)答案在Buffer A里面。
利用解碼得到的ss,st計(jì)算速度向量,區(qū)間重映射為[-0.25,0.25],這里為什么是0.25?我在測(cè)試中改為[-1,1]后,運(yùn)動(dòng)模糊效果過(guò)于眼中,場(chǎng)景明顯有條紋以及暈眩感,這里可能是調(diào)節(jié)的結(jié)果
vec2dir=(-1.0+2.0*vec2(ss,st))*0.25;
接下來(lái)是簡(jiǎn)單的運(yùn)動(dòng)模糊,累加32次后平均
for(inti=0;i<32;i++)
{
floath=float(i)/31.0;
vec2pos=uv+dir*h;
col+=texture(iChannel0,pos).xyz;
}
col/=32.0;
然后是Vignetting效果(漸暈;光暈,光損失;暗角)
col*=0.5+0.5*pow(16.0*uv.x*uv.y*(1.0-uv.x)*(1.0-uv.y),0.1);
這個(gè)效果是這樣的

這行代碼的呈現(xiàn)結(jié)果是:場(chǎng)景由黃,變白亮,有點(diǎn)像黃昏和白天的區(qū)別。
col=col*0.5+0.5*col*col*(3.0-2.0*col)+vec3(0.0,0.0,0.04);
總體來(lái)說(shuō),Image是對(duì)Buffer A的結(jié)果進(jìn)行運(yùn)動(dòng)模糊,以及色彩調(diào)節(jié)等處理,比較簡(jiǎn)單,我們可以參考的是 簡(jiǎn)單的運(yùn)動(dòng)模糊 和 vignetting 效果。
Buffer A
這里的代碼是主要功能的實(shí)現(xiàn),有幾百行,就不在開(kāi)頭貼出來(lái)了,我們從主函數(shù)開(kāi)始分析。
一開(kāi)始,對(duì)運(yùn)行時(shí)間進(jìn)行處理,用作之后的相機(jī)移動(dòng),這里還用了鼠標(biāo)輸入,用作加速移動(dòng)(可以理解為瞬移)
floattime=iTime*0.1-0.1+0.3+4.0*iMouse.x/iResolution.x;
照相機(jī)的處理
然后我們進(jìn)入了moveCamera函數(shù),參數(shù)為:時(shí)間,觀看位置,觀看方向,cr和fl不知道是什么,然后其中使用的全局變量 SC 為 250.0
voidmoveCamera(floattime,outvec3oRo,outvec3oTa,outfloatoCr,outfloatoFl)
{
? ? vec3ro=camPath(time);
? ? vec3ta=camPath(time+3.0);
? ? ro.y=terrainL(ro.xz)+22.0*SC;
? ? ta.y=ro.y-20.0*SC;
? ? floatcr=0.2*cos(0.1*time);
oRo=ro;
oTa=ta;
oCr=cr;
oFl=3.0;
}
vec3camPath(floattime)
{
? ? returnSC*1100.0*vec3(cos(0.0+0.23*time),0.0,cos(1.5+0.21*time) );
}
對(duì)于camPath,明顯是計(jì)算相機(jī)的x,z位置,這里的問(wèn)題的常量SC和1100為什么這么大,暫且不知。推測(cè)原因是采樣擴(kuò)大,坐標(biāo)范圍極大,畢竟我們這里顯示的是無(wú)邊的地形。此外,SC是全局變量,是有很多意義的,但調(diào)節(jié)的效果無(wú)法歸納其作用,暫時(shí)可以理解為某個(gè)值的單位變量,而1100這個(gè)常量根據(jù)調(diào)整的結(jié)果,可以理解為相機(jī)的移動(dòng)速度。
然后根據(jù)terrainL計(jì)算此時(shí)相機(jī)xz位置對(duì)應(yīng)的地形高度。(這里的算法就不做介紹了,個(gè)人估計(jì)是額外的地形生成算法),然后往上面做一個(gè)偏移,求出觀察位置的Y(高度)以及視線向量的Y。有三個(gè)函數(shù),本質(zhì)是相同的,后綴L,M,H分別對(duì)應(yīng)地形生成檢測(cè)的精度等級(jí)。
floatterrainL(invec2x)
{
? ? vec2p=x*0.003/SC;
floata=0.0;
floatb=1.0;
? ? vec2d=vec2(0.0);
for(inti=0;i<3;i++)
?? {
vec3n=noised(p);
d+=n.yz;
a+=b*n.x/(1.0+dot(d,d));
? ? ? ? b*=0.5;
p=m2*p*2.0;
?? }
?
? ? returnSC*120.0*a;
}
回到主函數(shù),得到了幾個(gè)相機(jī)采數(shù)之后,就是設(shè)置相機(jī),獲得反V矩陣。比較簡(jiǎn)單和常見(jiàn),就是通過(guò)叉乘進(jìn)行計(jì)算求值。然后cr的作用就出現(xiàn)了。
mat3setCamera(invec3ro,invec3ta,infloatcr)
{
? ? vec3cw=normalize(ta-ro);
? ? vec3cp=vec3(sin(cr),cos(cr),0.0);
? ? vec3cu=normalize(cross(cw,cp) );
? ? vec3cv=normalize(cross(cu,cw) );
returnmat3(cu,cv,cw);
}
然后進(jìn)入抗鋸齒的循環(huán)之中,將屏幕坐標(biāo)p重映射回裁剪空間,然后使用fl和得到的相機(jī)矩陣,計(jì)算射線在世界空間的值。因此fl可以理解為裁剪平面的位置。
vec3rd=cam*normalize(vec3(s,fl));
天空,雪和山地的處理
在之后,進(jìn)入渲染的總函數(shù)Render中
vec4res=render(ro,rd);
t=min(t,res.w);
這一部分是進(jìn)行加速,縮小[tmin,tmax]的區(qū)間范圍
floatmaxh=300.0*SC;
floattp=(maxh-ro.y)/rd.y;
if(tp>0.0)
{
if(ro.y>maxh)tmin=max(tmin,tp);
elsetmax=min(tmax,tp);
}
然后分析interesct函數(shù),這里也是進(jìn)行了常規(guī)的相交測(cè)試,或者說(shuō)距離場(chǎng)測(cè)試,返回射線移動(dòng)的距離。關(guān)于terrainM函數(shù),和之前一樣暫不討論。
floatinteresct(invec3ro,invec3rd,infloattmin,infloattmax)
{
floatt=tmin;
? ? for(inti=0;i<300;i++)
? ? {
//RayMarching
vec3pos=ro+t*rd;
//計(jì)算高度插值
? ? ? ? floath=pos.y-terrainM(pos.xz);
//0.0015*t起到一個(gè)優(yōu)化加速的效果
? ? ? ? if(abs(h)<(0.0015*t)||t>tmax)break;
? ? ? ? t+=0.4*h;
? ? }
?
returnt;
}
? 如果返回結(jié)果大于tmax,這說(shuō)明沒(méi)有擊中地形,我們要==渲染天空,這里的天空渲染實(shí)在巧妙,可以借鑒==
? ```c#
// sky? ? 根據(jù)Y的坐標(biāo)模擬天空漸變的藍(lán)色 ,第二行和海平面處理近似,但變化沒(méi)有那么急劇,效果相對(duì)于給藍(lán)天套了一層由下至上逐漸稀釋的白霧
? col = vec3(0.3,0.5,0.85) - rd.y*rd.y*0.5;
? col = mix( col, 0.85*vec3(0.7,0.75,0.85), pow( 1.0-max(rd.y,0.0), 4.0 ) );
? // sun 增光來(lái)達(dá)到模擬太陽(yáng)光暈的效果,有點(diǎn)像經(jīng)典高光的計(jì)算
? col += 0.25*vec3(1.0,0.7,0.4)*pow( sundot,5.0 );
? col += 0.25*vec3(1.0,0.8,0.6)*pow( sundot,64.0 );
? col += 0.2*vec3(1.0,0.8,0.6)*pow( sundot,512.0 );
? // clouds 不太懂的云模擬
? vec2 sc = ro.xz + rd.xz*(SC*1000.0-ro.y)/rd.y;
? col = mix( col, vec3(1.0,0.95,1.0), 0.5*smoothstep(0.5,0.8,fbm(0.0005*sc/SC)) );
? // horizon 邏輯簡(jiǎn)單但適用的地平線模擬,在海平面0處附近生效
col = mix( col, 0.68*vec3(0.4,0.65,1.0), pow( 1.0-max(rd.y,0.0), 16.0 ) );
? t = -1.0;
? 其中,fbm代表分?jǐn)?shù)布朗運(yùn)動(dòng)
? float fbm( vec2 p )
? {
? ? ? float f = 0.0;
? ? ? f += 0.5000*texture( iChannel0, p/256.0 ).x; p = m2*p*2.02;
? ? ? f += 0.2500*texture( iChannel0, p/256.0 ).x; p = m2*p*2.03;
? ? ? f += 0.1250*texture( iChannel0, p/256.0 ).x; p = m2*p*2.01;
? ? ? f += 0.0625*texture( iChannel0, p/256.0 ).x;
? ? return f/0.9375;
? }
? 如果返回結(jié)果小于tmax,則開(kāi)始渲染地面,首先簡(jiǎn)單的計(jì)算擊中點(diǎn)的法線,這是地形計(jì)算法線的版本,具體法線計(jì)算的各種情況可見(jiàn)IQ6
? vec3 calcNormal( in vec3 pos, float t )
? {
? ? ? vec2? eps = vec2( 0.001*t, 0.0 );
? ? ? return normalize( vec3( terrainH(pos.xz-eps.xy) - terrainH(pos.xz+eps.xy),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 2.0*eps.x,
? ? ? ? ? ? ? ? ? ? ? ? ? ? terrainH(pos.xz-eps.yx) - terrainH(pos.xz+eps.yx) ) );
? }
? 然后,計(jì)算巖石的顏色,這里的核心代碼是第一行和第二行,后續(xù)都是一些優(yōu)化和增加隨機(jī)性,但是確實(shí)看不太懂,最后一行是添加細(xì)節(jié)的顏色變化。
? float r = texture( iChannel0, (7.0/SC)*pos.xz/256.0 ).x;
? col = (r*0.25+0.75)*0.9*mix( vec3(0.08,0.05,0.03), vec3(0.10,0.09,0.08), texture(iChannel0,0.00007*vec2(pos.x,pos.y*48.0)/SC).x );
? //在效果上體現(xiàn)為:增加后,顏色由泛白變得正常
col = mix( col, 0.20*vec3(0.45,.30,0.15)*(0.50+0.50*r),smoothstep(0.70,0.9,nor.y) );
? //無(wú)明顯效果
col = mix( col, 0.15*vec3(0.30,.30,0.10)*(0.25+0.75*r),smoothstep(0.95,1.0,nor.y) );
? //相當(dāng)于細(xì)節(jié)貼圖
? col *= 0.1+1.8*sqrt(fbm(pos.xz*0.04)*fbm(pos.xz*0.005));
? 雪的計(jì)算。首先,對(duì)于參數(shù)h,我們知道的是他跟地形的高度有關(guān),除以單位值SC得到高度的無(wú)符號(hào)數(shù)值,然后加上一個(gè)分?jǐn)?shù)布朗的相關(guān)隨機(jī)值,關(guān)于參數(shù)內(nèi)部除以SC,這個(gè)是無(wú)所謂的,對(duì)于效果沒(méi)有影響,竊以為是統(tǒng)一格式,畢竟前面除了??偨Y(jié)來(lái)說(shuō),這個(gè)參數(shù)決定了海拔越高,越容易被雪覆蓋的真實(shí)場(chǎng)景特性。對(duì)于參數(shù)e,則是和地形的法向量相關(guān),當(dāng)然還會(huì)有高度的影響:這里的規(guī)則是,海拔越高,出現(xiàn)雪所要求的地形法向量范圍越大——海拔高的情況,除非是峭壁,不然都有很大概率被雪覆蓋,而在海拔低的地區(qū),則很難出現(xiàn)雪,除非法向量無(wú)限接近(0,1,0)的平地,而這里,據(jù)我觀察,會(huì)有一個(gè)問(wèn)題,那就是會(huì)導(dǎo)致零星雪(海拔低但完全平行的點(diǎn)會(huì)出現(xiàn)雪,但是因?yàn)榈匦问请S機(jī)生成的,它的周圍的點(diǎn)大概率不會(huì)平行,那么就不會(huì)被雪覆蓋。這樣就會(huì)很奇怪)。對(duì)于參數(shù)o,就公式而言,和法向量的x分量和海拔高度有關(guān)(正相關(guān)),就效果而言,有無(wú),雪的分布基本無(wú)變化。但是仔細(xì)分析會(huì)有這樣的想法:場(chǎng)景中,太陽(yáng)的x坐標(biāo)是-0.8,那么nor.x是負(fù)值的情況下,則說(shuō)明該點(diǎn)所在坡是正對(duì)著太陽(yáng)的,那么很明顯,這種雪的覆蓋率應(yīng)該會(huì)降低,在通過(guò)海拔進(jìn)行修正(只要海拔夠高,管你有沒(méi)有對(duì)著太陽(yáng),當(dāng)然,峭壁除外)。最后,這三個(gè)參數(shù)進(jìn)行相乘,決定該點(diǎn)是否被雪覆蓋。
float h = smoothstep(55.0,80.0,pos.y/SC + 25.0*fbm(0.01*pos.xz/SC) );
? float e = smoothstep(1.0-0.5*h,1.0-0.1*h,nor.y);
float o = 0.3 + 0.7*smoothstep(0.0,0.1,nor.x+h*h);
? float s = h*e*o;
? col = mix( col, 0.29*vec3(0.62,0.65,0.7), smoothstep( 0.1, 0.9, s ) );
光照計(jì)算
//環(huán)境光:越水平,環(huán)境光的強(qiáng)度越強(qiáng)
float amb = clamp(0.5+0.5*nor.y,0.0,1.0);
//漫反射
float dif = clamp( dot( light1, nor ), 0.0, 1.0 );
//
float bac = clamp( 0.2 + 0.8*dot( normalize( vec3(-light1.x, 0.0, light1.z ) ), nor ), 0.0, 1.0 );
//陰影參數(shù)計(jì)算
float sh = 1.0;
if( dif>=0.0001 ) sh = softShadow(pos+light1*SC*0.05,light1);
首先,計(jì)算環(huán)境光,這里簡(jiǎn)單的進(jìn)行了模擬:越水平,環(huán)境光越強(qiáng)。然后計(jì)算漫反射,比較簡(jiǎn)單。然后對(duì)參數(shù)bac,待定,暫時(shí)不知道其含義。之后,計(jì)算陰影,具體函數(shù)如下:明顯是RayMarching中比較常見(jiàn)的柔和陰影計(jì)算,沒(méi)有什么意料之外的操作。(這一點(diǎn),在IQ博客系列閱讀中有過(guò)分析和介紹)
float softShadow(in vec3 ro, in vec3 rd )
{
? ? float res = 1.0;
? ? float t = 0.001;
for( int i=0; i<80; i++ )
{
? ? vec3? p = ro + t*rd;
? ? ? ? float h = p.y - terrainM( p.xz );
res = min( res, 16.0*h/t );
t += h;
if( res<0.001 ||p.y>(SC*200.0) ) break;
}
return clamp( res, 0.0, 1.0 );
}
在之后,是光強(qiáng)lin的具體計(jì)算,依次計(jì)算了實(shí)際具體的環(huán)境光,漫反射(當(dāng)然,陰影參數(shù)應(yīng)該在這里使用到),還有bac,最后和col相乘。這里比較意外的是,在陰影參數(shù)的使用上,對(duì)RGB三個(gè)通道進(jìn)行了不同的變化——R通道的衰減速度是要慢于G,B通道,雖然這個(gè)處理對(duì)于整個(gè)場(chǎng)景的表現(xiàn)沒(méi)有明顯影響,但還是要注意。此外,關(guān)于bac,其有無(wú)同樣對(duì)于場(chǎng)景表現(xiàn)無(wú)影響。
vec3 lin? = vec3(0.0);
lin += dif*vec3(8.00,5.00,3.00)*1.3*vec3( sh, sh*sh*0.5+0.5*sh, sh*sh*0.8+0.2*sh );
lin += amb*vec3(0.40,0.60,1.00)*1.2;
lin += bac*vec3(0.40,0.50,0.60);
col *= lin;
下面兩行公式的意義不知。在效果上,增刪與否對(duì)于表現(xiàn)無(wú)明顯影響。參數(shù)s的再次使用,應(yīng)該是讓雪和山地的光照計(jì)算產(chǎn)生一定的差異,畢竟是不同的物質(zhì),雪的光吸收應(yīng)該弱于山地,所以雪覆蓋的地方,是1,而山地則是0.7。
col += (0.7+0.3*s)*(0.04+0.96*pow(clamp(1.0+dot(hal,rd),0.0,1.0),5.0))*
? ? ? ? ? ? ? vec3(7.0,5.0,3.0)*dif*sh*
? ? ? ? ? ? ? pow( clamp(dot(nor,hal), 0.0, 1.0),16.0);
col += s*0.65*pow(fre,4.0)*vec3(0.3,0.5,0.6)*smoothstep(0.0,0.6,ref.y);
霧的計(jì)算。比較簡(jiǎn)單,比較常規(guī)的霧的冪計(jì)算方法。注釋的地方是讓霧的顏色和太陽(yáng)位置掛鉤。
float fo = 1.0-exp(-pow(0.001*t/SC,1.5) );
vec3 fco = 0.65*vec3(0.4,0.65,1.0);// + 0.1*vec3(1.0,0.8,0.5)*pow( sundot, 4.0 );
col = mix( col, fco, fo );
最后,映射回伽馬空間,返回最終Color和射線步進(jìn)的距離。
// sun scatter
col += 0.3*vec3(1.0,0.7,0.3)*pow( sundot, 8.0 );
// gamma
col = sqrt(col);
return vec4( col, t );
運(yùn)動(dòng)模糊的處理
// old camera position
float oldTime = time - 0.1 * 1.0/24.0; // 1/24 of a second blur
vec3 oldRo, oldTa; float oldCr, oldFl;
moveCamera( oldTime, oldRo, oldTa, oldCr, oldFl );
mat3 oldCam = setCamera( oldRo, oldTa, oldCr );
// world space
#if AA>1
vec3 rd = cam * normalize(vec3(p,fl));
#endif
vec3 wpos = ro + rd*t;
// camera space
vec3 cpos = vec3( dot( wpos - oldRo, oldCam[0] ),
? ? ? ? ? ? ? ? ? dot( wpos - oldRo, oldCam[1] ),
? ? ? ? ? ? ? ? ? dot( wpos - oldRo, oldCam[2] ) );
// ndc space
vec2 npos = oldFl * cpos.xy / cpos.z;
// screen space
vec2 spos = 0.5 + 0.5*npos*vec2(iResolution.y/iResolution.x,1.0);
// compress velocity vector in a single float
vec2 uv = fragCoord/iResolution.xy;
spos = clamp( 0.5 + 0.5*(spos - uv)/0.25, 0.0, 1.0 );
vel = floor(spos.x*255.0) + floor(spos.y*255.0)*256.0;
首先,時(shí)間time減去1/24,然后依據(jù)之前說(shuō)明的相機(jī)相關(guān)函數(shù),得到坐標(biāo)系變化矩陣。
// old camera position
float oldTime = time - 0.1 * 1.0/24.0; // 1/24 of a second blur
vec3 oldRo, oldTa; float oldCr, oldFl;
moveCamera( oldTime, oldRo, oldTa, oldCr, oldFl );
mat3 oldCam = setCamera( oldRo, oldTa, oldCr );
然后,依靠t得到當(dāng)前點(diǎn)的世界坐標(biāo)wpos,在依據(jù)常規(guī)流程計(jì)算出該點(diǎn)在舊時(shí)間的屏幕空間坐標(biāo)
// camera space
vec3 cpos = vec3( dot( wpos - oldRo, oldCam[0] ),
? ? ? ? ? ? ? ? ? dot( wpos - oldRo, oldCam[1] ),
? ? ? ? ? ? ? ? ? dot( wpos - oldRo, oldCam[2] ) );
// ndc space
vec2 npos = oldFl * cpos.xy / cpos.z;
// screen space
vec2 spos = 0.5 + 0.5*npos*vec2(iResolution.y/iResolution.x,1.0);
最后,壓縮速度
// compress velocity vector in a single float
vec2 uv = fragCoord/iResolution.xy;
spos = clamp( 0.5 + 0.5*(spos - uv)/0.25, 0.0, 1.0 );
vel = floor(spos.x*255.0) + floor(spos.y*255.0)*256.0;