3D渲染技术分享:一种高效的卡通水体渲染

2023-06-01 0 255

今天宿苞预览了两个关于米老鼠河面图形的音频,音频中提及了 炫烨 元老的该文。

搜寻了一会,发现只有高峰论坛有。

抱着蹭新一波关注度的态度,将该文捡拾了回来,并松动了文件格式。

接下去,有请大家重访一下炫烨元老的米老鼠地下水渲染,体会元老对技术细节的掌控力。

3D渲染技术分享:一种高效的卡通水体渲染
3D渲染技术分享:一种高效的卡通水体渲染

通过第一集讲义你将教给怎样做米老鼠化地下水的图形,包涵的习题有:

怎样采用夜空正方体图形作散射怎样充分运用噪音图形作着色反气旋并别忘了作出贝状效用怎样充分运用 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); }

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

3D渲染技术分享:一种高效的卡通水体渲染

1.2、噪音图形

表现地下水的核心有两点,两个是壳状感,另两个是扭曲感。而这两点都可以通过对噪音图形进行 uv 偏转同时实现。

本文采用的噪音图形如下:

3D渲染技术分享:一种高效的卡通水体渲染

噪音图相关代码如下:

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)。

到这一步,我们可以看到壳状的水波效用,如下所示:

3D渲染技术分享:一种高效的卡通水体渲染

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); }

加入红腺散射前后对比:

3D渲染技术分享:一种高效的卡通水体渲染
3D渲染技术分享:一种高效的卡通水体渲染

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.0,当 t > 0.91 时,取1.0。

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); }

效用如下:

3D渲染技术分享:一种高效的卡通水体渲染

3、夜空盒

这里的夜空盒没用引擎的 skybox,因为采用 skybox 河面的边缘线消除有些困难,所以本栏采用了两个球体。效用如下:

3D渲染技术分享:一种高效的卡通水体渲染

将河面与夜空盒结合后,效用如下所示:

3D渲染技术分享:一种高效的卡通水体渲染

眼尖的读者肯定可以看到,在河面和夜空的交界处有两个明显的边缘线。

要消除这个边缘线,我们首先想到的是采用雾,将远处的河面与夜空盒用雾来模糊掉。

但是采用常规的雾效会带来两个问题,当相机进行远近终端时,雾的效用会产生变化,河面的边缘线还是没解决。如下所示:

3D渲染技术分享:一种高效的卡通水体渲染

所以我们要换个思路同时实现。

我们只要让远处河面的颜色与夜空盒一致就好了,于是本栏写了上面这段代码:

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))); … }

完美解决问题,效用如下:

3D渲染技术分享:一种高效的卡通水体渲染

我们将与河面中心距离大于一定范围内的区域颜色设置成 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); }

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

3D渲染技术分享:一种高效的卡通水体渲染

至此,咱们的教程结束,你学废了吗?

4、写在最后

由于炫烨元老当初太忙,没有准备DEMO,高峰论坛里除了高喊 元老666 却什么都不能做。

在这里特别感谢宿苞,替大家同时实现了该文中的DEMO效用并制作了音频讲义。

获得方式1:打开Dashboard->商城,搜寻 宿苞

3D渲染技术分享:一种高效的卡通水体渲染

同时,借此机会推荐两个神器。

名称:Cinestation

作者:炫烨

用途:3D格斗游戏镜头制作

价格:免费

3D渲染技术分享:一种高效的卡通水体渲染

更多3D图形图形相关内容

请关注 麒麟子

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务