效果大致如下(本来想上传GIF图的,可惜大于10M),还有下面的笔记是直接从MD复制过来的,有点丑,感兴趣的可以看看我的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,网址:https://www.shadertoy.com/view/MdX3Rr
标签:procedural, 3d, raymarching, distancefield, terrain, motionblur
总共两个部分: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的计算结果,据此来说,xyz分量存储的是最终计算结果(color),w存的是速度向量。
vec4data=texture(iChannel0,uv);
如果速度小于0,则说明场景静止,直接取xyz分量,否则进行运动模糊(motion blur)
进行运动模糊时,首先进行对速度矢量进行解压缩。
floatss=mod(data.w,256.0)/255.0;
floatst=floor(data.w/256.0)/255.0;
第一个是用w分量对256求模,然后除以255,第二个是用w分量除以246,取整后除以255,为什么这样解码,估计答案在Buffer A里面。
利用解码得到的ss,st计算速度向量,区间重映射为[-0.25,0.25],这里为什么是0.25?我在测试中改为[-1,1]后,运动模糊效果过于眼中,场景明显有条纹以及晕眩感,这里可能是调节的结果
vec2dir=(-1.0+2.0*vec2(ss,st))*0.25;
接下来是简单的运动模糊,累加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);
这个效果是这样的
这行代码的呈现结果是:场景由黄,变白亮,有点像黄昏和白天的区别。
col=col*0.5+0.5*col*col*(3.0-2.0*col)+vec3(0.0,0.0,0.04);
总体来说,Image是对Buffer A的结果进行运动模糊,以及色彩调节等处理,比较简单,我们可以参考的是 简单的运动模糊 和 vignetting 效果。
Buffer A
这里的代码是主要功能的实现,有几百行,就不在开头贴出来了,我们从主函数开始分析。
一开始,对运行时间进行处理,用作之后的相机移动,这里还用了鼠标输入,用作加速移动(可以理解为瞬移)
floattime=iTime*0.1-0.1+0.3+4.0*iMouse.x/iResolution.x;
照相机的处理
然后我们进入了moveCamera函数,参数为:时间,观看位置,观看方向,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) );
}
对于camPath,明显是计算相机的x,z位置,这里的问题的常量SC和1100为什么这么大,暂且不知。推测原因是采样扩大,坐标范围极大,毕竟我们这里显示的是无边的地形。此外,SC是全局变量,是有很多意义的,但调节的效果无法归纳其作用,暂时可以理解为某个值的单位变量,而1100这个常量根据调整的结果,可以理解为相机的移动速度。
然后根据terrainL计算此时相机xz位置对应的地形高度。(这里的算法就不做介绍了,个人估计是额外的地形生成算法),然后往上面做一个偏移,求出观察位置的Y(高度)以及视线向量的Y。有三个函数,本质是相同的,后缀L,M,H分别对应地形生成检测的精度等级。
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;
}
回到主函数,得到了几个相机采数之后,就是设置相机,获得反V矩阵。比较简单和常见,就是通过叉乘进行计算求值。然后cr的作用就出现了。
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);
}
然后进入抗锯齿的循环之中,将屏幕坐标p重映射回裁剪空间,然后使用fl和得到的相机矩阵,计算射线在世界空间的值。因此fl可以理解为裁剪平面的位置。
vec3rd=cam*normalize(vec3(s,fl));
天空,雪和山地的处理
在之后,进入渲染的总函数Render中
vec4res=render(ro,rd);
t=min(t,res.w);
这一部分是进行加速,缩小[tmin,tmax]的区间范围
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函数,这里也是进行了常规的相交测试,或者说距离场测试,返回射线移动的距离。关于terrainM函数,和之前一样暂不讨论。
floatinteresct(invec3ro,invec3rd,infloattmin,infloattmax)
{
floatt=tmin;
for(inti=0;i<300;i++)
{
//RayMarching
vec3pos=ro+t*rd;
//计算高度插值
floath=pos.y-terrainM(pos.xz);
//0.0015*t起到一个优化加速的效果
if(abs(h)<(0.0015*t)||t>tmax)break;
t+=0.4*h;
}
returnt;
}
如果返回结果大于tmax,这说明没有击中地形,我们要==渲染天空,这里的天空渲染实在巧妙,可以借鉴==
```c#
// sky 根据Y的坐标模拟天空渐变的蓝色 ,第二行和海平面处理近似,但变化没有那么急剧,效果相对于给蓝天套了一层由下至上逐渐稀释的白雾
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 增光来达到模拟太阳光晕的效果,有点像经典高光的计算
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 逻辑简单但适用的地平线模拟,在海平面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代表分数布朗运动
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;
}
如果返回结果小于tmax,则开始渲染地面,首先简单的计算击中点的法线,这是地形计算法线的版本,具体法线计算的各种情况可见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) ) );
}
然后,计算岩石的颜色,这里的核心代码是第一行和第二行,后续都是一些优化和增加随机性,但是确实看不太懂,最后一行是添加细节的颜色变化。
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 );
//在效果上体现为:增加后,颜色由泛白变得正常
col = mix( col, 0.20*vec3(0.45,.30,0.15)*(0.50+0.50*r),smoothstep(0.70,0.9,nor.y) );
//无明显效果
col = mix( col, 0.15*vec3(0.30,.30,0.10)*(0.25+0.75*r),smoothstep(0.95,1.0,nor.y) );
//相当于细节贴图
col *= 0.1+1.8*sqrt(fbm(pos.xz*0.04)*fbm(pos.xz*0.005));
雪的计算。首先,对于参数h,我们知道的是他跟地形的高度有关,除以单位值SC得到高度的无符号数值,然后加上一个分数布朗的相关随机值,关于参数内部除以SC,这个是无所谓的,对于效果没有影响,窃以为是统一格式,毕竟前面除了。总结来说,这个参数决定了海拔越高,越容易被雪覆盖的真实场景特性。对于参数e,则是和地形的法向量相关,当然还会有高度的影响:这里的规则是,海拔越高,出现雪所要求的地形法向量范围越大——海拔高的情况,除非是峭壁,不然都有很大概率被雪覆盖,而在海拔低的地区,则很难出现雪,除非法向量无限接近(0,1,0)的平地,而这里,据我观察,会有一个问题,那就是会导致零星雪(海拔低但完全平行的点会出现雪,但是因为地形是随机生成的,它的周围的点大概率不会平行,那么就不会被雪覆盖。这样就会很奇怪)。对于参数o,就公式而言,和法向量的x分量和海拔高度有关(正相关),就效果而言,有无,雪的分布基本无变化。但是仔细分析会有这样的想法:场景中,太阳的x坐标是-0.8,那么nor.x是负值的情况下,则说明该点所在坡是正对着太阳的,那么很明显,这种雪的覆盖率应该会降低,在通过海拔进行修正(只要海拔够高,管你有没有对着太阳,当然,峭壁除外)。最后,这三个参数进行相乘,决定该点是否被雪覆盖。
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 ) );
光照计算
//环境光:越水平,环境光的强度越强
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 );
//阴影参数计算
float sh = 1.0;
if( dif>=0.0001 ) sh = softShadow(pos+light1*SC*0.05,light1);
首先,计算环境光,这里简单的进行了模拟:越水平,环境光越强。然后计算漫反射,比较简单。然后对参数bac,待定,暂时不知道其含义。之后,计算阴影,具体函数如下:明显是RayMarching中比较常见的柔和阴影计算,没有什么意料之外的操作。(这一点,在IQ博客系列阅读中有过分析和介绍)
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 );
}
在之后,是光强lin的具体计算,依次计算了实际具体的环境光,漫反射(当然,阴影参数应该在这里使用到),还有bac,最后和col相乘。这里比较意外的是,在阴影参数的使用上,对RGB三个通道进行了不同的变化——R通道的衰减速度是要慢于G,B通道,虽然这个处理对于整个场景的表现没有明显影响,但还是要注意。此外,关于bac,其有无同样对于场景表现无影响。
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;
下面两行公式的意义不知。在效果上,增删与否对于表现无明显影响。参数s的再次使用,应该是让雪和山地的光照计算产生一定的差异,毕竟是不同的物质,雪的光吸收应该弱于山地,所以雪覆盖的地方,是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);
雾的计算。比较简单,比较常规的雾的幂计算方法。注释的地方是让雾的颜色和太阳位置挂钩。
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和射线步进的距离。
// sun scatter
col += 0.3*vec3(1.0,0.7,0.3)*pow( sundot, 8.0 );
// gamma
col = sqrt(col);
return vec4( col, t );
运动模糊的处理
// 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;
首先,时间time减去1/24,然后依据之前说明的相机相关函数,得到坐标系变化矩阵。
// 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得到当前点的世界坐标wpos,在依据常规流程计算出该点在旧时间的屏幕空间坐标
// 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;