前言
抖音在2024年春节期间推出了欢笑中国年系列活动,为用户带来了全新的体验和乐趣。而SAR Creator则为该项目研发工作提供了重要的技术支持。SAR Creator是一款基于 Typescript 的高性能、轻量化的互动解决方案,目前支持了浏览器和跨端框架平台,服务于字节内部的各种互动业务。
这些绚烂多彩的互动场景当然也离不开实时渲染技术的支持,因此本文将专门介绍春节活动招财神龙和神龙探宝中SAR Creator渲染相关的业务实践经验以及技术探索和尝试。
春节—招财神龙
春节—神龙探宝
比如抖音欢笑中国年系列文章《招财神龙互动技术揭秘》中就有提到,项目中“家”场景就是由2D元素以及不同材质支持的3D元素共同组成的。出于性能和美术效果的考虑,各3D模型使用的材质会有所不同,比如无光照Unlit材质、基于物理的PBR材质。对于阴影这种移动端性能消耗比较大的特性,不同物体的接收也会做特殊处理。这些材质的选择以及光照阴影的支持都是依托于SAR Creator材质库能力的支持。如下图所示即为SAR Creator Unlit材质(左图)和PBR材质(右图)的示例。
无光照Unlit材质示例
基于物理的PBR材质示例
此外,SAR Creator支持使用ShaderGraph插件制作自定义材质,帮助用户制作更多可定制化的特效。神龙探宝项目中实现了多种基于ShaderGraph的特殊效果,包括:入场溶解特效、分区解冻特效、拖尾特效等。下图展示了利用ShaderGraph定制卡通风格水体的效果。
ShaderGraph卡通水体
除了上述春节活动中顺利落地的渲染效果外,我们还尝试做了很多效果提升的技术探索,比如后处理辉光效果、凹凸贴图等。希望可以更好的提升美术设计师的设计体验和最终的渲染效果。
bloom辉光
招财神龙渲染实践
“招财神龙”活动是2024年春节游戏化玩法之一,活动整体采用3D场景(龙在家场景)+ 2D场景(龙寻宝场景)结合的方式。在招财神龙的活动中,设计同学基于SAR Creator编辑器,进行场景搭建和效果还原工作;研发同学基于SAR Creator渲染能力,快速进行技术方案选型和实施。
2D&3D混合渲染
对于活动中的“龙”和“小女孩”元素,我们采用3D模型,提供更为逼真的体验感。而针对场景中的房子、炮仗等,我们使用2D贴片来呈现。通过调整相机的远近平面、fov等参数,展示出小女孩在炮仗前、龙在房子前、龙在炮仗后的视觉假象。
材质库
SAR Creator提供了Unlit、PBR、Uber和NPR等多种材质的选择。
例如,这次“招财神龙”中的白天/黑夜场景场景,小女孩和龙的皮肤颜色等需要有不同的表现,就是基于材质的“颜色贴图”能力来实现的。
针对PBR材质来说,设计同学还基于SAR Creator提供的金属度、粗糙度来进行小女孩身体细节的调整。
为了追求更佳的视觉体验,在小女孩的模型上,设计同学为不同的部位(身体、头发和衣服)赋予了不同的PBR材质的实例,再通过调整不同PBR材质的金属度、粗糙度属性,微调受光条件下,不同部位的表现细节。
这次活动,我们不仅使用了PBR材质,综合性能和实用性的角度,还使用了Sar Creator提供的Unlit材质。比如“龙”模型中的身体、胡须、眼睛等模块。Unlit材质是一种简单的、不受光源影响的材质,在技术选型时,为了平衡性能和效果,通常是活动开发的首选。
光照阴影
除了上述所说的这些材质,为了实现场景中元素的真实性,设计同学借助SAR Creator提供的渲染能力,利用灯光、阴影来优化渲染场景。
SAR Creator提供了平行光的方向、颜色、强度等属性,使得设计同学可以调整出不同效果。
为了更好的光照效果,我们这次使用了两个平行光,利用PBR受光的特性,可以实现更贴近真实世界的效果。
只使用了环境光
使用了一个平行光(小女孩鞋子、脸、手部等部分都收到了光照影响)
使用了两个平行光微调(小女孩背部收到了光照,更贴近真实效果)
只有光照,没有阴影的话,同样也不符合物理世界的客观规律。SAR Creator通过在光源上设置“投射阴影”,在需要显示阴影的物体上,设置“接受阴影”,即可快速的实现阴影的效果。
无阴影
有阴影
利用SAR Creator提供的ShadowMaterial这种自实现的材质,我们还能通过调整颜色、透明度等常用属性,快速调整出设计师想要的阴影效果。
神龙探宝渲染实践
神龙探宝是2024年春节系列活动中的一个以2D场景为主的互动玩法,其尝试并成功落地了多种特效渲染技术。本章节主要有三个特效渲染技术点可分享给大家。分别是:入场溶解特效、分区解冻特效、拖尾特效。
入场溶解
实现入场溶解特效核心是采样一张溶解图(可低分辨率128x128),通过动画step.edge即可。该方案主要通过ShaderGraph可视化界面开发Shader帮助实现美术预期效果,具体节点实现实现如下图所示:
如想了解更多技术细节,各位同学可用WebGPU版ShaderGraph在线体验(https://deepkolos.github.io/shader-graph-wgsl/?graph=demoSummberDissolve)(PC Chrome113+),也欢迎内部同学直接体验SAR Creator。
地图分区解冻
地图分区解冻需要实现的效果是支持分区块单独控制其处于解冻/未解冻状态。表现效果会随着解冻状态的变化而变化。
如果按照传统前端实现估计需要7个区域小图+1张底图=8张图片去实现, 即需要消耗8个绘制指令(DrawCall)。虽然传统方式可以通过动态合批的方式优化绘制指令(DrawCall)为1个,但合批操作本身也有耗时,且每次资源替换+小图位置调整,会带来额外工作量。而利用ShaderGraph插件定义支持图片存储特定贴图IDMap的Shader可解决这些问题,只需一张JPG 一张PNG 即可。首先我们需要将区域信息存储在A通道,比如区域A = 0.9 区域B = 0.8 以此类推。
未点亮
已点亮
解冻过程
然后在Shader中根据点亮前后纹理采样颜色值,混合计算出最终像素颜色值,以实现每个区域的解冻/未解冻状态变化。具体计算逻辑如下所示:
如想了解更多技术细节,该例子也可用WebGPU版ShaderGraph在线体验(https://deepkolos.github.io/shader-graph-wgsl/?graph=demoCustomMap)(PC Chrome113+)。
然而在上述效果的实现基础上,设计同学提出了更高的渲染需求,要求冰冻区域沿边缘浸染已解冻区域,来避免硬边缘。由于项目时间节奏比较紧张,综合考虑时间成本和收益后,边缘浸染需求最终没有推进支持。通过简单的调研,该效果可能的一个解法是:增加7个2D光照,通过光照计算范围来实现冰冻浸染效果,但问题在于没法实现沿边缘浸染。各位如果有什么好的思路也可以分享下,也许可以通过ShaderGraph插件快速支持这种定制需求。
粒子拖尾+几何拖尾
设计效果
落地实现
我们可以分析表格第三列中效果参考的构成,得出技术要点为: 几何拖尾+粒子拖尾+头部星光。头部星光较为简单,只需要一个Sprite/Plane+Tween增加下旋转动画即可。下面将主要介绍粒子拖尾和几何拖尾的技术实现。
粒子拖尾
目前SAR Creator现有非常强大的粒子系统,可快速实现粒子效果,提效非常明显。
但完整的粒子系统在功能强大的同时包体积也相对较大,为了兼顾粒子效果的同时也避免包体积问题,需实现简易版拖尾粒子。通过ShaderGraph结合EmitOverDisatance,抽离出的粒子拖尾特效资源打包后只有7.66KB。下图左为: 简易版粒子拖尾+EmitOverDistance+ShaderGraph联动,下图中/右为: 粒子系统示例和ShaderGraph定制材质的参数设置。
目前粒子拖尾已集成进SAR Creator以及ShaderGraph插件,方便用户更加直观的调试材质特效。
GPU Instancing是一个DrawCall绘制大量相同几何,姿态不同的技术。
所以简易版拖尾粒子本质上是一个GPU Instancing几何更新器。
大量粒子的位移动画通过Shader使用GPU并行算力完成,节约宝贵的CPU算力。
感兴趣代码实现,可查看👉开源实现(https://github.com/deepkolos/three-js-trail) or 线上Demo(https://deepkolos.github.io/three-js-trail/),或者SAR Creator中直接体验。
几何拖尾
几何拖尾本质上也是一个几何更新器,不过并非更新Instanced数据,而是几何本身数据Position+Index,使用下图可直观了解几何拖尾的关键逻辑。
如上图左所示为几何拖尾的几何部分实现,参考效果的游动效果则需要在Shader中增加UV动画+拖尾沿Brush重心缩小,所以几何拖尾同样支持ShaderGraph扩展材质。
如感兴趣代码实现,可查看👉开源实现(https://github.com/deepkolos/three-js-trail) or 线上Demo(https://deepkolos.github.io/three-js-trail/),或者SAR Creator中直接体验。
ShaderGraph探索
ShaderGraph自定义Shader相较于研发编写定制材质而言主要优势在于更高的自由度。SAR Creator通过ShaderGraph插件可以边看中间结果,边理解特效的实现方式。同时帮助用户更好的调试渲染结果高度不可控的特效。此外,ShaderGraph插件实现思路和节点能力实现方式与Unity ShaderGraph基本一致,用户可以以极低成本的方式参考Unity已有特效并搬运到SAR Creator上。
比如抖音故障效果:
再或者卡通水体WebGPU版ShaderGraph在线预览(https://deepkolos.github.io/shader-graph-wgsl/?graph=demoCartoonWater):
渲染技术探索
除了在项目实际落地的渲染技术外,我们也在春节项目中尝试探索渲染技术可能应用场景。下面我们将通过后处理篇和材质篇来进一步介绍其中的技术点。
后处理篇
比如在“招财神龙”的龙须上,我们希望能增加辉光bloom的效果。bloom是屏幕后处理效果中较为常用的一种,表现为高光物体带有泛光效果,通常会搭配HDR来得到更好的效果。
在技术方案的实现上:针对此类特定区域的辉光,我们引入了亮度阈值。第一步,对原场景图进行筛选时,所有小于这个阈值的像素都会被筛掉,仅保留大于等于该亮度阈值的区域,即我们的龙须区域。第二步,对上一步操作的结果龙须区域进行模糊操作,达到光溢出的效果。最后,我们将处理过的图像和原图像进行叠加,就得到了最终的效果。
bloom渲染流程示意图
bloom渲染流程中的第一步:过滤高亮区域,我们在shader的属性中加一个lumaThreshold,然后提取图片像素亮度进行step过滤,在片段着色器中代码示例如下:
uniform float lumaThreshold;
float luminance(vec3 color) {
return dot(color,vec3( 0.299,0.587,0.114 ));
}
void main{
vec4 texel = texture2D( tDiffuse,vUv );
float luminosity = luminance(texel.xyz);
float contribute = step( lumaThreshold,luminosity );
gl_FragColor = texel * contribute;
}
bloom渲染流程中的第二步,图像模糊算法在后处理渲染领域中占据着重要的地位,后处理中所采用模糊算法的优劣,直接决定了后处理管线最终的渲染品质和消耗性能的多少。高品质后处理:十种图像模糊算法的总结与实现(https://zhuanlan.zhihu.com/p/125744132) 对十种模糊算法进行总结、对比和盘点,其中双重模糊(dual blur) 获得了高性价比评价,故SAR Creator中bloom的模糊算法采用双重模糊进行了实现。双重模糊的核心思路在于模糊的过程中进行了降采样和升采样,即对RT进行了降采样以及升采样。
部分代码示例如下:
// 新建renderTexture数组
for (let i = 0; i < downSampleNum; i++) {
this.fboArr[i] = new RenderTexture(0,0,fboOptions);
if (i !== downSampleNum - 1) {
this.fboArr[(downSampleNum - 1) * 2 - i] = new RenderTexture(0,0,fboOptions);
}
}
// 下采样
for (let i = 0; i < downSampleNum - 1; i++) {
uniforms.tDiffuse.value = fboArr[i].texture;
uniforms.halfPixel.value.set(1 / fboArr[i].width,1 / fboArr[i].height);
fsQuad.render(renderer,fboArr[i + 1]);
}
// 上采样
const n = downSampleNum;
for (let i = downSampleNum - 1; i < (downSampleNum - 1) * 2; i++) {
uniforms.tDiffuse.value = fboArr[i].texture;
uniforms.downTexture.value = fboArr[2 * n - i - 3].texture;
uniforms.halfPixel.value.set(1 / fboArr[i].width,1 / fboArr[i].height);
fsQuad.render(renderer,fboArr[i + 1]);
}
此外,SAR Creator提供了多种blur kernel,设计师可以切换对比,调整出自己想要的光晕效果。
所有的模糊算法都是利用周遭像素值加权叠加计算得到结果的,权重则取决于距离。部分模糊算法的shader实现如下:
#if BLUR_KERNEL == 0 // --------------- Kawase ---------------
void blurKernel() {
#if SAMPLE_PHASE == 0 // down
vec4 sum = texture2D(tDiffuse,vUv) * 4.0
+ texture2D(tDiffuse,uv01.xy) + texture2D(tDiffuse,uv01.zw)
+ texture2D(tDiffuse,uv23.xy) + texture2D(tDiffuse,uv23.zw);
gl_FragColor = sum * 0.125;
#elif SAMPLE_PHASE == 1 // up
vec4 sum = texture2D(tDiffuse,uv01.xy) + texture2D(tDiffuse,uv23.xy)
+ texture2D(tDiffuse,uv45.xy) + texture2D(tDiffuse,uv67.xy)
+ (texture2D(tDiffuse,uv01.zw) + texture2D(tDiffuse,uv23.zw)
+ texture2D(tDiffuse,uv45.zw) + texture2D(tDiffuse,uv67.zw)) * 2.0;
gl_FragColor = sum * 0.0833;
#endif
}
#elif BLUR_KERNEL == 1 // --------------- Box4Tap ---------------
void blurKernel() {
vec4 sum = texture2D(tDiffuse,vUv + uvOffset.xy) + texture2D(tDiffuse,vUv + uvOffset.zy)
+ texture2D(tDiffuse,vUv + uvOffset.xw) + texture2D(tDiffuse,vUv + uvOffset.zw);
gl_FragColor = sum * 0.25;
}
#elif BLUR_KERNEL == 2 // --------------- Tent9Tap ---------------
void blurKernel() {
vec4 sum = texture2D(tDiffuse,vUv + uvOffset.xy)
+ texture2D(tDiffuse,vUv - uvOffset.xy)
+ texture2D(tDiffuse,vUv + uvOffset.zy)
+ texture2D(tDiffuse,+ vUv - uvOffset.zy)
+ (texture2D(tDiffuse,vUv - uvOffset.wy)
+ texture2D(tDiffuse,vUv + uvOffset.zw)
+ texture2D(tDiffuse,vUv + uvOffset.xw)
+ texture2D(tDiffuse,vUv + uvOffset.wy)) * 2.0
+ texture2D(tDiffuse,vUv) * 4.0;
gl_FragColor = sum * 0.0625;
}
材质篇
互动活动场景中经常会出现3D地形,如果通过建模软件来生成三角形面片几何体的方式支持该需求的话,会存在三角面片数过高从而消耗性能过大的问题。此外,设计师很难得到所需要的凹凸起伏的建模,设计调整成本过高。一般而言,设计师会希望通过一张灰度图来表示相应区域的高低,从而通过控制平面中各个着色点在垂直方向上的偏移来表达起伏。
而支持这种实现的技术就是位移贴图(DisplacementMap)。如下图1、2所示展示了PBR材质修改位移贴图缩放指数(DisplacementScale)为0和2的区别。如下图3所示展示了控制高低的位移贴图。
位移贴图的实现方式是在材质顶点着色器VertexShader中基于原始顶点信息、结合位移贴图对顶点偏移的计算,得到实际展示的顶点位置,具体shader中定义和实现如下所示:
// 前置信息定义
#ifdef USE_DISPLACEMENTMAP
vec2 displacementUV; // 位移贴图UV
uniform mat3 displacementUVMatrix; // 位移贴图UV变换锯齿
uniform sampler2D displacementMap; // 位移贴图
uniform float displacementScale; // 位移缩放比例
uniform float displacementBias; // 位移偏移
#endif
// 顶点计算
#ifdef USE_DISPLACEMENTMAP
positionV4.xyz += normalize( positionV4.xyz ) * ( texture2D( displacementMap, displacementUV ).x * displacementScale + displacementBias );
#endif
此外,与位移贴图调整当前绘制点顶点不同,还可以通过调整当前绘制点法线计算的方式使得渲染结果展示出一种比较细致的凹凸感,即凹凸贴图(BumpMap)技术。如下图1、2所示展示了PBR材质修改凹凸贴图缩放指数(BumpMapScale)为0和100的区别。如下图3所示展示了控制表面粗糙感的凹凸贴图。
凹凸贴图的实现方式是在材质片元着色器FragmentShader中基于原始顶点/法线信息、结合凹凸贴图,计算出调整后的法线结果,具体shader中定义和实现如下所示:
// 前置信息定义
#ifdef USE_BUMPMAP
varying vec2 bumpUV; // 凹凸贴图UV
uniform sampler2D bumpMap; // 凹凸贴图
uniform float bumpScale; // 凹凸缩放比例
#endif
#ifdef USE_BUMPMAP
vec2 dHdxy_fwd() {
vec2 dSTdx = dFdx( bumpUV );
vec2 dSTdy = dFdy( bumpUV );
float Hll = bumpScale * texture2D( bumpMap,bumpUV ).x;
float dBx = bumpScale * texture2D( bumpMap,bumpUV + dSTdx ).x - Hll;
float dBy = bumpScale * texture2D( bumpMap,bumpUV + dSTdy ).x - Hll;
return vec2( dBx,dBy );
}
vec3 perturbNormalArb( vec3 surf_pos,vec3 surf_norm,vec2 dHdxy,float faceDirection ) {
vec3 vSigmaX = normalize( dFdx( surf_pos.xyz ) );
vec3 vSigmaY = normalize( dFdy( surf_pos.xyz ) );
vec3 vN = surf_norm;
vec3 R1 = cross( vSigmaY,vN );
vec3 R2 = cross( vN,vSigmaX );
float fDet = dot( vSigmaX,R1 ) * faceDirection;
vec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 );
return normalize( abs( fDet ) * surf_norm - vGrad );
}
#endif
// 法线信息计算
#ifdef USE_BUMPMAP
normal = perturbNormalArb( posWorld,normal,dHdxy_fwd(),faceDirection );
#endif
所以我们可以看到,渲染引擎除了提供通用的基础材质外,往往需要在原有材质基础上灵活迭代,根据用户使用场景和需求不断支持新特性。
未来展望
SAR Creator 材质库、ShaderGraph特效、后处理等渲染能力在24年春节活动中得到进一步完善和项目验证,为互动场景提供了不错的视觉效果。当然在业务落地过程中,我们也发现了一些不足之处。比如后处理可选效果还不够多、功能还不够完善,目前只初步支持bloom和fxaa后处理。比如美术工作流还不够高效,美术资产的制作和落地过程中可能存在卡点,需要多方沟通协调。后续我们会和美术设计师保持更紧密的沟通,更深入的了解用户需求,提供更多开箱即用的材质特性、后处理效果和ShaderGraph特效能力。