知识点!!简单的卡通水体渲染教程

通过本篇教程你将学到如何做风格化水体的渲染,包含的知识点有如何使用天空立方体贴图作反射,如何巧用噪声贴图作纹理扰动并顺便做出浮沫效果,如何巧用uv做出边沿雾效果。

水体渲染是游戏中比较有挑战的一种效果,实现难度也有深有浅,这里笔者希望使用一种简单高效的方法实现一个简单美观的风格化水体效果,最终实现效果如上,性能非常优秀,对移动设备非常友好,下面是实现过程。

1、天空反射

天空的反射需要用到两个东西,分别是

  • 环境立方体贴图

  • 噪声贴图

  • 菲涅尔反射

1.1、环境立方体贴图

想要做出水体流动的感觉有非常多的方法,其中使用uv偏移是最简单并且性能最好的方法,该方案绝大多数的做法都是对一张法线贴图作uv缩放和偏移,并作光影计算从而表现出流动的水面,该方案的确能做出相当不做的风格化水体效果,但是笔者这次不想这么做,因为法线贴图的采样还原在笔者看来还是不够精简,甚至水体对光源的明暗变化笔者也不想计算,于是笔者选择了直接对环境立方体贴图做采样,表现一个简单的水面反射效果。代码如下:

vec3  v = normalize(v_view);
vec3  r = -v;
vec3  reflectColor = texture(envTexture, r).rgb;

以上代码中,笔者对反射做了一个计算优化,直接对视角向量取反即 r = -v,常规做法是r = reflect(v, n),其中reflect(v, n) = v - 2.0 * dot(n, v) * n。由reflect表达式就能看出笔者的写法效率要远高于常规做法,少了2次的乘法计算和1次点成计算。而笔者的计算优化成立的原因是对于天空的反射,如果仅仅让视觉上看起来像反射,我们其实可以不用关心反射方向的正确性,读者可以自己作个图细品下。完整代码如下:

vec4 frag () {
    vec3  n = normalize(v_normal);
    vec3  v = normalize(v_view);
    vec3  r = -v;

    vec3  reflectColor = texture(envTexture, r).rgb;
    return vec4(reflectColor, mainColor.a);
}

此时读者应该能得到一个镜子一般的水面,毫无美感,并且丝毫也感受不出这是水。

1.2、噪声贴图

表现水体的核心有两点,一个是流动感,另一个是扭曲感。而这两点都可以通过对噪声贴图进行uv偏移实现。

本文使用的噪声贴图:


代码如下:

vec4 vert() {
    StandardVertInput In;
    CCVertInput(In);
    mat4 matWorld, matWorldIT;
    CCGetWorldMatrixFull(matWorld, matWorldIT);
    vec4 worldPos = matWorld * In.position;
    v_uv.xy = worldPos.xz * 0.1 + cc_time.x * 0.05;
    ...
}

vec4 frag () {
    float t = texture(noiseTexture, v_uv.xy).r;
    vec3  n = normalize(v_normal);
    vec3  v = normalize(v_view);
    vec3  r = -v + t * 0.03;

    vec3  reflectColor = texture(envTexture, r).rgb;
    return vec4(reflectColor, mainColor.a);
}

以上代码中,笔者作uv偏移是基于世界坐标偏移的,而不是简单的v_uv.xy += cc_time.x * 0.05,这里的原因是基于世界坐标作偏移可以随意调整水面大小,而不会拉伸噪声贴图,造成失真。这里还有一点需要注意的是我们的噪声贴图的wrap mode需要设置为repeat即重复模式。

1.3、菲涅尔反射

加入流动感和扭曲感后,我们的水体终于看起来像水了,目前还存在一个问题,水面任何视角的反射表现都是一样的,这是不正确的,这里需要引出一个现象叫菲涅尔反射(fresnel),简单的讲,就是视线垂直于表面时,反射较弱,而当视线非垂直表面时,夹角越小,反射越明显。代码如下:

float fresnel = mix(0.15, 1.0, pow(1.0 - dot(n, v), 3.0));

常规的fresnel反射公式为fresnel = pow(1.0 - dot(n, v), x),x为指数系数,而这里笔者使用了mix函数,将fresnel的数值映射到0.15到1.0之间,确保视角与水面垂直时,也是存在反射的。

mix(x, y, a)是一个混合函数,等价于 x×(1−a)+y×a.

完整的代码如下:

vec4 frag () {
    float t = texture(noiseTexture, v_uv.xy).r;
    vec3  n = normalize(v_normal);
    vec3  v = normalize(v_view);
    vec3  r = -v + t * 0.03;

    vec3  reflectColor = texture(envTexture, r).rgb;
    float fresnel = mix(0.15, 1.0, pow(1.0 - dot(n, v), 3.0));
    vec3  color = mix(mainColor.rgb, reflectColor, fresnel);
    return vec4(color, mainColor.a);
}

加入菲涅尔反射前后对比:


2、 浮沫

目前我们的水面虽然有了些许流动感,但还不够明显,所以我们需要在水面上制造一些浮沫,突出水的流动。浮沫有几个特点,1、位置不固定,2、大小也不固定。观察我们的噪声贴图,你会发现噪声贴图上的一些白色图案刚好符合我们需求,我们只要想个办法将它提取出来就可以了,所以我们对上述代码做如下改动:

vec4 frag () {
    float t = texture(noiseTexture, v_uv.xy).r;
    ...
    color = mix(color, vec3(1.0), step(0.9, t));
    ...
}

首先我用step函数对噪声作了一次过滤,将大于0.9的噪声提取了出来,并用mix混合函数,将水的颜色和白色(vec3(1.0)是白色)进行混合得到带有白色浮沫的水面。

step(edge, x)是一个阶跃函数,等价于x < edge ? 0: 1。

另外大多时候我们使用step,提取出来的图案,都是有锯齿感的,所以需要作抗锯齿,这时就需要使用smoothstep函数。修改如下:

vec4 frag () {
    float t = texture(noiseTexture, v_uv.xy).r;
    ...
    color = mix(color, vec3(1.0), smoothstep(0.9, 0.91, t));
    ...
}

改动非常少,仅仅是将step(0.9, t)替换为smoothstep(0.9, 0.91, t)。smoothstep(0.9, 0.91, t)的作用是将t在[0.9, 0.91]的范围内作平滑处理,当t < 0.9时,取0,当t > 0.91时,取1。

smoothstep(edge0, edge1, x)是一个三次平滑阶跃函数,可以将x在[edge0, edge1]之间做一个平滑过渡,大多时候都用来消除锯齿。

完整的代码如下:

vec4 frag () {
    float t = texture(noiseTexture, v_uv.xy).r;
    vec3  n = normalize(v_normal);
    vec3  v = normalize(v_view);
    vec3  r = -v + t * 0.03;

    vec3  reflectColor = texture(envTexture, r).rgb;
    float fresnel = mix(0.15, 1.0, pow(1.0 - dot(n, v), 3.0));
    vec3  color = mix(mainColor.rgb, reflectColor, fresnel);
    color = mix(color, vec3(1.0), smoothstep(0.9, 0.901, t));
    return vec4(color, mainColor.a);
}

效果如下:


3、加入天空盒

这里的天空盒没用skybox,因为使用skybox,水面的边沿线消除有些困难,所以笔者使用了一个球体。效果如下:

读者可以看到在水面和天空的交界处有一个明显的边沿线,下面我们就要消除掉这个边沿线。消除这个边沿线,我们首先想到的是使用雾,将远处的水面与天空盒用雾来模糊掉。

常规雾效果以及带来的问题:

但是使用常规的雾效会带来一个问题,当相机进行远近移动时,雾的效果会产生变化,水面的边沿线还是没解决,所以我们要换个思路实现。其实消除这个边沿线的思路很简单,我们只要让远处水面的颜色与天空盒一致就好了。于是笔者写了下面这段代码:

vec4 vert() {
    ...
    v_uv.zw = a_texCoord;
    ...
}

vec4 frag() {
    ...
    vec2 d = v_uv.zw - vec2(0.5, 0.5);
    color = mix(color, rimColor.rgb, rimColor.a * smoothstep(0.0, 0.27, dot(d,d)));
    ...
}

效果如下:


完美解决问题,原理是这样的,我们将与水面中心距离大于一定范围内的区域颜色设置成rimColor(rimColor的颜色基本与天空盒的颜色一致) 并且用smoothstep,对一定范围内的距离值做了平滑处理。但是在实际计算中,笔者作了一个计算优化,笔者没有直接使用距离值即sqrt(dot(d,d)),而是使用了距离的平方值即dot(d, d),原因是求平方根比较废性能,如果仅仅是比大小,其实没必要开根号。

完整的代码如下:

vec4 vert() {
    StandardVertInput In;
    CCVertInput(In);
    mat4 matWorld, matWorldIT;
    CCGetWorldMatrixFull(matWorld, matWorldIT);
    vec4 worldPos = matWorld * In.position;
    v_normal = normalize((matWorldIT * vec4(In.normal, 0.0)).xyz);
    v_view = cc_cameraPos.xyz - worldPos.xyz;
    v_uv.xy = worldPos.xz * 0.1 + cc_time.x * 0.05;
    v_uv.zw = a_texCoord;
    return cc_matProj * cc_matView * worldPos;
}

vec4 frag () {
    float t = texture(noiseTexture, v_uv.xy).r;
    vec3  n = normalize(v_normal);
    vec3  v = normalize(v_view);
    vec3  r = -v + t * 0.03;

    vec3  reflectColor = texture(envTexture, r).rgb;
    float fresnel = mix(0.15, 1.0, pow(1.0 - dot(n, v), 3.0));
    vec3  color = mix(mainColor.rgb, reflectColor, fresnel);
    color = mix(color, vec3(1.0), smoothstep(0.9, 0.91, t));
    vec2 d = v_uv.zw - vec2(0.5, 0.5);
    color = mix(color, rimColor.rgb, rimColor.a * smoothstep(0.0, 0.27, dot(d,d)));
    return vec4(color, mainColor.a);
}

最后在相机前摆上一些粒子烘托下气氛:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容