Elevated代码解析

Elevated代码解析

作者:iq,网址:https://www.shadertoy.com/view/MdX3Rr

标签:procedural, 3d, raymarching, distancefield, terrain, motionblur

总共两个部分:Image,Buffer A

在这里插入图片描述

Image

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord/iResolution.xy;
    vec4 data = texture( iChannel0, uv );

    vec3 col = vec3(0.0);
    if( data.w < 0.0 )
    {
        col = data.xyz;
    }
    else
    {
        // decompress velocity vector
        float ss = mod(data.w,256.0)/255.0;
        float st = floor(data.w/256.0)/255.0;

        // motion blur (linear blur across velocity vectors
        vec2 dir = (-1.0 + 2.0*vec2( ss, st ))*0.25;
        col = vec3(0.0);
        for( int i=0; i<32; i++ )
        {
            float h = float(i)/31.0;
            vec2 pos = 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存的是速度向量。

     vec4 data = texture( iChannel0, uv );
    
  • 如果速度小于0,则说明场景静止,直接取xyz分量,否则进行运动模糊(motion blur)

  • 进行运动模糊时,首先进行对速度矢量进行解压缩。

    float ss = mod(data.w,256.0)/255.0;
    float st = 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]后,运动模糊效果过于眼中,场景明显有条纹以及晕眩感,这里可能是调节的结果

    vec2 dir = (-1.0 + 2.0*vec2( ss, st ))*0.25;
    
  • 接下来是简单的运动模糊,累加32次后平均

    for( int i=0; i<32; i++ )
    {
         float h = float(i)/31.0;
         vec2 pos = 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

这里的代码是主要功能的实现,有几百行,就不在开头贴出来了,我们从主函数开始分析。

一开始,对运行时间进行处理,用作之后的相机移动,这里还用了鼠标输入,用作加速移动(可以理解为瞬移)

float time = iTime*0.1 - 0.1 + 0.3 + 4.0*iMouse.x/iResolution.x;

照相机的处理

然后我们进入了moveCamera函数,参数为:时间,观看位置,观看方向,cr和fl不知道是什么,然后其中使用的全局变量 SC 为 250.0

void moveCamera( float time, out vec3 oRo, out vec3 oTa, out float oCr, out float oFl )
{
	vec3 ro = camPath( time );
	vec3 ta = camPath( time + 3.0 );
	ro.y = terrainL( ro.xz ) + 22.0*SC;
	ta.y = ro.y - 20.0*SC;
	float cr = 0.2*cos(0.1*time);
    oRo = ro;
    oTa = ta;
    oCr = cr;
    oFl = 3.0;
}
vec3 camPath( float time )
{
	return SC*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分别对应地形生成的精度等级。

    float terrainL( in vec2 x )
    {
    	vec2  p = x*0.003/SC;
        float a = 0.0;
        float b = 1.0;
    	vec2  d = vec2(0.0);
        for( int i=0; i<3; i++ )
        {
            vec3 n = noised(p);
            d += n.yz;
            a += b*n.x/(1.0+dot(d,d));
    		b *= 0.5;
            p = m2*p*2.0;
        }
    
    	return SC*120.0*a;
    }
    

回到主函数,得到了几个相机采数之后,就是设置相机,获得反V矩阵。比较简单和常见,就是通过叉乘进行计算求值。然后cr的作用就出现了。

mat3 setCamera( in vec3 ro, in vec3 ta, in float cr )
{
	vec3 cw = normalize(ta-ro);
	vec3 cp = vec3(sin(cr), cos(cr),0.0);
	vec3 cu = normalize( cross(cw,cp) );
	vec3 cv = normalize( cross(cu,cw) );
    return mat3( cu, cv, cw );
}

然后进入抗锯齿的循环之中,将屏幕坐标p重映射回裁剪空间,然后使用fl和得到的相机矩阵,计算射线在世界空间的值。因此fl可以理解为裁剪平面的位置

vec3 rd = cam * normalize(vec3(s,fl));

天空,雪和山地的处理

在之后,进入渲染的总函数Render中

 vec4 res = render( ro, rd );
 t = min( t, res.w );
  • 这一部分是进行加速,缩小[tmin,tmax]的区间范围

    float maxh = 300.0*SC;
    float tp = (maxh-ro.y)/rd.y;
    if( tp>0.0 )
    {
        if( ro.y>maxh ) tmin = max( tmin, tp );
        else     tmax = min( tmax, tp );
    }
    
  • 然后分析interesct函数,这里也是进行了常规的相交测试,或者说距离场测试,返回射线移动的距离。关于terrainM函数,和之前一样暂不讨论。

    float interesct( in vec3 ro, in vec3 rd, in float tmin, in float tmax )
    {
        float t = tmin;
    	for( int i=0; i<300; i++ )
    	{
            //RayMarching
            vec3 pos = ro + t*rd;
            //计算高度插值
    		float h = pos.y - terrainM( pos.xz );
            //0.0015*t起到一个优化加速的效果
    		if( abs(h)<(0.0015*t) || t>tmax ) break;
    		t += 0.4*h;
    	}
    	return t;
    }
    

    如果返回结果大于tmax,这说明没有击中地形,我们要渲染天空,这里的天空渲染实在巧妙,可以借鉴

      // 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;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JMXIN422

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值