塞尔达的3D渲染风格,能在小游戏跑起来?
2021.05.19 by Cocos
品牌新闻 Cocos Creator

渲染系统作为游戏引擎的核心模块,是引擎画面表现力的天花板,直接决定了游戏所能输出给玩家的内容上限。Cocos Creator 3.x 的渲染系统,从架构到设计都是以面向未来、高性能、跨平台为目标,支持开发者制作出更加精致的游戏画面。新版本 Cocos Creator 3.1 已发布,欢迎大家前往下载体验!

近日引擎渲染组使用 Cocos Creator 3.1,参考塞尔达/原神的吉卜力(Ghibli) 卡通渲染风格做了有趣的尝试,大家先看一段编辑器里的操作视频:Cocos 塞尔达 3D 渲染风格Cocos的视频

由宫崎骏开创的吉卜力风格,并不追求对真实世界的高度逼真再现,而是从色彩、明暗、对比度等方面进行风格独特的艺术表达,在绘画感上更接近于水彩,并大量使用亮色作为主色调,通过艺术化的颜色来展现世界,而非纯粹追求写实,使画面更有卡通幻想色彩。

重要的事情写在前面:

1. 上述视频里的 demo 的所有素材和源码都已开源

仓库地址https://github.com/cocos-creator/cartoon-vegetation

2. 可以扫描下方二维码直接进入微信小游戏体验,方便懒得下载编译的朋友:

这里强调一下,构建发布到小游戏平台可运行,只是为了尝试性能的极限情况如何,以及方便大家快速体验。在性能内存约束最大的小游戏上都能运行的话,PC Web、手机原生的性能就更没问题了。

小游戏版本的已知问题:- 默认设置下,由于 ShadowMap 精度不够导致树叶有些闪动。在高端安卓手机上可以通过游戏内「左上角齿轮」 -> 开启「High Quality」解决该问题。但 iPhone12 由于没有 JIT,开启 High Quality 之后会掉帧到30以下。- 已知在部分安卓手机上由于不支持 WebGL 的 float texture 导致「Bend Glass」,也就是角色行走处的草被压平效果无法正确出来。该问题在 iPhone 上没有。(哎,所以引擎的全平台适配真是个苦力活)

下文将详细介绍植被渲染的部分以及模型与人物互动,希望对大家有所帮助。

01

工具

引擎版本:Cocos Creator 3.1

DCC 工具:Blender

Demo 中的人物是一个塞尔达爱好者的同人作品,作者提供了很多人物模型来给大家免费使用。地址https://twitter.com/artstoff

由于下载下来的模型是没有动作的,可以在以下网站上搜索一些简单的动作来使用。地址https://www.mixamo.com/

02

植被渲染

初始状态

植被初始状态相当于引擎默认的 unlit 材质,只能设置贴图和基础颜色。整个画面都是绿色,显得比较平淡,没有任何氛围和辨识度。

vec2 uv = mainTiling.xy + mainTiling.zw * v_uv;vec4 col = texture(mainTexture, uv); 

添加修改色

我们可以添加草地的修改色,并基于噪声贴图得到一个平滑随机数 0-1,在原有基础色进行平滑过渡,这样草地颜色会变得更丰富明亮。

float rand = texture(randMap, worldPos.xz * randMapTiling).r;col.rgb = mix(col.rgb, hue.rgb, rand * hue.a); 

模拟 AO

一般草地的根部或者树叶根部受光度比较少,因此颜色应该比叶尖处更暗一些,我们可以简单利用 uv 值来得到草地根部到顶端的明暗度差,基于这个数据来模拟计算 AO 值,或者把明暗信息在建模时预先存储在顶点颜色中。

float getMask () {    #if Mask_Type == Mask_Channel_Color        return a_color.r;    #elif Mask_Type == Mask_Channel_Uv        return 1. - a_texCoord.y;    #else        return 0.;    #endif}

图中使用 AO mask 作为颜色输出,可以明显看到 AO mask 的值从顶部到根部逐渐变小。

float mask = getMask();float ao = mix(col.a, col.a * mask, ambientOcclusion);col.rgb *= ao; 

把 ambientOcclusion 暴露为材质参数,结合之前颜色输出的效果如下图:

摆动草地

草地颜色已经比较丰富了,现在可以赋予草地生命力,让它随风摆动起来。

先简单让它按照 Sine(正弦) 曲线随时间摆动,然后通过 windStrength 和 windSpeed 调节参数。

需要注意的是能被吹动的只有叶尖,根部是不需要摆动的。这和我们之前得到是 AO Mask 明暗度的过度是一样的。所以我们可以利用之前计算好的 Mask 值乘上 Sine,来做简单模拟。

vec4 offset = vec4(0.);
float strength = windStrength;float sine = sin(windSpeed * (cc_time.x ));sine = sine * mask * strength;
// 计算 xz 平面偏移offset.xz = vec2(sine);
// 计算 y 方向弯曲度float windWeight = length(offset.xz) + 0.0001;windWeight = pow(windWeight, 1.5);offset.y = windWeight * mask;

我们发现草左右来回大幅度摆动看起来是比较奇怪的,正常情况下草地被吹动后回摆的幅度是小而自然的。

因此需要把 Sine 计算结果的范围由 (-1, 1) 重新映射到 (0, 1) 上,并通过参数 windSwinging 来调节摆动幅度。

sine = mix(sine * 0.5 + 0.5, sine, windSwinging); 

现在看上去好一些了,但是摆动太规整没有层次感。所以我们需要给每个顶点计算不一样的摆动值,这里简单使用模型坐标系下的坐标值计算顶点强度。

float f = length(positionOS.xz) * windRandVertex;// 重新计算 sin 值,rand 为之前用 noise 贴图获取的噪声值,rand随机范围0-1float sine = sin(s.speed * (cc_time.x + rand + f)); 

阵风效果

风不是一直连续刮过来的,一般是一股一股风吹过来。下图来自现实中阵风吹过草地的效果,可以看到草地在局部中会形成明暗变化。

我们可以使用噪声贴图来模拟这一效果。

vec2 gustUV = (worldPos.xz * windGustFrequency * windSpeed) + (cc_time.x * windSpeed * windGustFrequency) * -windDirection。xy;
float gust = texture(windMap, gustUV).r;
gust *= windGustStrength * mask;

// col 为之前计算的结果
col.rgb += (gust * v_color.a * windGustTint);

雾效与天空盒

到现在草地的效果已经有了,不过玩家还感受不出场景的纵深感,并且场景的背景还是默认色。我们可以利用引擎内置的雾效和天空盒来增加画面细节。

天空盒对于增强画面感是非常重要的,当处于玩家视角的情况下,大概有四分之一到一半的画面是由天空盒来填充的,选择一个合适的天空盒非常重要。

这个场景我选了一个偏淡的天空盒,搭配偏白色的雾效可以使场景尽头和天空盒的色调比较接近。

阴影

这次风格化渲染的实现中没有使用光照模型来计算光照效果,而是使用阴影计算的结果来增加画面细节的。

// getShadowAttenuation 参考了引擎内部计算阴影的逻辑来获取阴影强度
float getShadowAttenuation () {
    float shadowAttenuation = 0.0;

    #if CC_RECEIVE_SHADOW
        
        // cc_shadowInfo 的定义可以在引擎中 cc-shadow.chunk 文件中找到, 其中的数据格式为 :
        // x -> width; y -> height; z -> pcf; w -> bais;
        // z -> pcf 对应的是场景中设置 pcf 选项的值
        float pcf = cc_shadowInfo.z + 0.001;

        // CCGetShadowFactorXX 为引擎内部 PCF 阴影计算方法, 后面的数字表示采样 shadowmap 的次数,采样次数越多获得的阴影模糊效果越好,表现得越柔软,消耗的性能也越高
        if (pcf > 3.0) {
            shadowAttenuation = CCGetShadowFactorX25();
        }
        else if (3.0 > pcf && pcf > 2.0) {
            shadowAttenuation = CCGetShadowFactorX9();
        }
        else if (2.0 > pcf && pcf > 1.0) {
            shadowAttenuation = CCGetShadowFactorX5();
        }
        else {
            shadowAttenuation = CCGetShadowFactorX1();
        }
    #endif

    return shadowAttenuation;
}

float shadowAttenuation = getShadowAttenuation();
**PCF** 阴影设置:

![12](./pics/12.png)

// 获取阴影强度,并暴露参数 shadowIntensity 自由调节阴影强度
shadowAttenuation = 1. - min(shadowAttenuation, shadowIntensity);

col.rgb *= shadowAttenuation;

半透明效果

这里说的半透明效果不是指玻璃那种能透过物体看到其他物体的效果,而是指阳光能穿过树叶继续照射的效果。当我们的视线向着阳光的方向看树叶的时候,会发现树叶显得更加明亮。

按照上面的描述我们可以概括为当树叶到摄像机的方向,与阳光到树叶方向一致的时候树叶应该显得更加明亮,使用 overlay 叠加效果可以简单模拟明亮的效果。

// 主光源也就是太阳光的方向
vec3 ld = normalize(cc_mainLitDir.xyz);

// viewDirectionWS 为树叶到摄像机的方向
vec3 viewDirectionWS = normalize(cc_cameraPos.xyz - worldPos.xyz);

// translucency 暴露为材质参数方便调整效果
float VdotL = max(0., dot(viewDirectionWS, ld)) * translucency;
VdotL = pow(VdotL, 4.) * 8.;

float tMask = VdotL * shadowAttenuation;

vec3 tColor = col.rgb + BlendOverlay(cc_mainLitColor.rgb * cc_mainLitColor.w, color);

col.rgb = mix(col.rgb, tColor, tMask);

与植被的互动

当玩家在草地上移动时,草地受到玩家碰撞挤压,应该是会明显向周围弯曲的。

要做到这一点,我们需要将希望产生交互的物体绘制到一张高度贴图上,贴图中的信息包括物体的高度、物体在 XZ 轴上挤压的方向、挤压的力度。

渲染到高度图中的信息:

float mask = -v_normal.y * heightStrength;// * v_color.r;
float height = (v_position.y + heightOffset);
// 将值从 (-1, 1) 重新映射到 (0, 1)vec2 dir = (v_normal.xz * extendStrength) * 0.5 + 0.5;
vec4 heightMapInfo = vec4(dir.x, height, dir.y, mask); 

生成高度图的方法是用一个垂直向下的摄像机拍摄所有需要互动的物体,在 Demo 中摄像机会一直跟随主角移动,可以随时修改摄像机拍摄的范围。

当渲染物体到高度图上的时候,我们并不需要把原有主角整个完整渲染上去,因为主角的面数一般会比较多,为了节约一些性能,可以用一个大小相近但是面数比较少的物体来做近似渲染。

参考下图,使用一个圆柱体来代替主角渲染到高度图上,并且我们可以自由改变圆柱体大小来控制渲染范围。

下面再看下在草地材质中如何获取到高度图里面的信息:

// cc_grass_bend_uv 是在自定义管线里面计算的结果
// cc_grass_bend_uv.xy 为高度图摄像机的世界坐标
// cc_grass_bend_uv.z  为高度图摄像机拍摄的范围

// 使用像素的世界坐标值减去高度图摄像机的世界坐标再除以范围就可以得到高度图中的 uv 坐标
vec2 getBendMapUV(in vec3 wPos) {
    vec2 uv = (wPos.xz - cc_grass_bend_uv.xy) / cc_grass_bend_uv.z + 0.5;
	return uv;			
}

// cc_grass_bend_map 就是高度贴图了
vec4 getBendVector(vec3 wPos)  {
	vec2 uv = getBendMapUV(wPos);
	vec4 v = texture(cc_grass_bend_map, uv);

	//Remap from 0.1 to -1.1
	v.x = v.x * 2.0 - 1.0;
	v.z = v.z * 2.0 - 1.0;

	return v;
}

以上就是 Cocos 引擎渲染组为大家献上的吉卜力卡通风格渲染的全部分享,视频、源码、小游戏体验、实现步骤讲解都已奉上。

评论美三代,转发富一生。如果您喜欢 Cocos 渲染组的这期分享,欢迎关注微信公众号【COCOS】了解更多信息,最后希望大家多留言、多转发哦!