ShaderToy系列之一: 炫酷的海洋渲染Seascape


关于ShaderToy

Shadertoy是GLSL图形程序员的一个在线playground,基于WebGL的便携和跨平台特性,让本来需要繁琐开发环境的shader开发变得简单,随时随地都可以创建,修改以及分享自己的写的shader。在ShaderToy上能随意地发现很多乍看不可思议的效果,比如:

ShaderToy实现

乍一看这么炫酷的效果,其原理一定很深奥吧!其实不然,shadertoy本身的原理十分简单,我们只需要做极少的工作就能把shadertoy上的shader源码原封不动的拿到我们自己的GL项目里面来(参考github)。

因为本身没有数据输入,所以所有的计算都是在fragment shader里面完成的。ray tracing, procedure sdf, volumn rendering是这里常用的手段。

炫酷的海洋渲染Seascape

seascape

Seascape是我在shadertoy上看到的第一个令人称奇的特效,只用了短短150行shader代码。下面来分析一下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// main
void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
    vec2 uv = fragCoord.xy / iResolution.xy;
    uv = uv * 2.0 - 1.0;
    uv.x *= iResolution.x / iResolution.y;    
    float time = iGlobalTime * 0.3 + iMouse.x*0.01;
        
    // ray
    vec3 ang = vec3(sin(time*3.0)*0.1,sin(time)*0.2+0.3,time);    
    vec3 ori = vec3(0.0,3.5,time*5.0); //原点
    vec3 dir = normalize(vec3(uv.xy,-2.0)); dir.z += length(uv) * 0.15;//广角
    dir = normalize(dir) * fromEuler(ang);//dir = normalize(dir);去掉euler angle便于研究
    
    // tracing
    vec3 p;
    heightMapTracing(ori,dir,p);
    vec3 dist = p - ori;
    vec3 n = getNormal(p, dot(dist,dist) * EPSILON_NRM);
    vec3 light = normalize(vec3(0.0,1.0,0.8)); 
             
    // color
    vec3 color = mix(
        getSkyColor(dir),
        getSeaColor(p,n,light,dir,dist),
    	pow(smoothstep(0.0,-0.05,dir.y),0.3));
        
    // post
    fragColor = vec4(pow(color,vec3(0.75)), 1.0);
}

mainImage是shadertoy里的main函数,fragColor是像素着色器的色彩输出。所有shadertoy着色器都需要用的两个uniform变量是iResolution和iGlobalTime,前者配合fragCoord可以得到像素位置,iGlobalTime用来计时。line10的原点表明RayTrace的原点是沿着z轴向画面外移动。可以把12行的fromEuler注释掉,这样的话去掉镜头摇摆,可以更加方便我们的研究

Trace阶段,首先heightMapTracing得到dir方向上的视线终点p,计算p点处的法线方向n。这里涉及到海洋渲染的核心函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
float map_detailed(vec3 p) {
    float freq = SEA_FREQ;//SEA_FREQ用来控制水波的频率
    float amp = SEA_HEIGHT;//SEA_HEIGHT控制浪高
    float choppy = SEA_CHOPPY; //SEA_CHOPPY控制波涛宽度	
    vec2 uv = p.xz; uv.x *= 0.75;
    
    float d, h = 0.0;    
    for(int i = 0; i < ITER_FRAGMENT; i++) {//浪的位置需要在几个位置上做采样叠加
        d = sea_octave((uv+SEA_TIME)*freq,choppy);
        d += sea_octave((uv-SEA_TIME)*freq,choppy);
        h += d * amp;        
        uv *= octave_m; //uv变换,下一个采样位置
        freq *= 1.9; amp *= 0.22;
        choppy = mix(choppy,1.0,0.2);
    }
    return p.y - h;
}

float sea_octave(vec2 uv, float choppy) {
    uv += noise(uv);//加上noise使海面看上去更加真实
    vec2 wv = 1.0-abs(sin(uv));
    vec2 swv = abs(cos(uv));    
    wv = mix(wv,swv,wv);
    return pow(1.0-pow(wv.x * wv.y,0.65),choppy);
}

float noise( in vec2 p ) {
    vec2 i = floor( p );
    vec2 f = fract( p );	
    vec2 u = f*f*(3.0-2.0*f);
    return -1.0+2.0*mix( mix( hash( i + vec2(0.0,0.0) ), 
                     hash( i + vec2(1.0,0.0) ), u.x),
                mix( hash( i + vec2(0.0,1.0) ), 
                     hash( i + vec2(1.0,1.0) ), u.x), u.y);
}

用我们的老朋友Matlab看一下TDM的这个海洋函数,

sea_octave

最后是着色阶段,getSkyColor就是很简单的视线算法,根据dir.y来计算天空的颜色。核心在getSeaColor这个函数上(注意看如何mix反射的天空颜色和水体自身的颜色)

1
2
3
4
5
6
7
8
9
10
11
12
vec3 getSeaColor(vec3 p, vec3 n, vec3 l, vec3 eye, vec3 dist) {  
    //菲涅尔系数计算
    float fresnel = clamp(1.0 - dot(n,-eye), 0.0, 1.0);
    fresnel = pow(fresnel,3.0) * 0.65;
    vec3 reflected = getSkyColor(reflect(eye,n));    //反射天空色
    vec3 refracted = SEA_BASE + diffuse(n,l,80.0) * SEA_WATER_COLOR * 0.12;  //水体颜色
    vec3 color = mix(refracted,reflected,fresnel);//fresnel越大,反射色的比例越大
    float atten = max(1.0 - dot(dist,dist) * 0.001, 0.0);
    color += SEA_WATER_COLOR * (p.y - SEA_HEIGHT) * 0.18 * atten;
    color += vec3(specular(n,l,eye,60.0));
    return color;
}

mainImage的最后是gamma矫正,至此代码分析完毕。


上篇: 关于任意凸多边形的CPU剪裁问题