写点什么

快手动效渲染引擎 Crab,解锁“游戏化动效”开发新方式!

作者:快手技术
  • 2024-12-31
    北京
  • 本文字数:7576 字

    阅读完需:约 25 分钟

导读:在上一篇文章中,我们全方位地解析了快手 Vision 动效平台的整体架构及其演进思路。快手前端动效大揭秘:告别低效,vision 平台来袭!今天,我们将进一步深入,详细介绍 Vision 动效平台的渲染引擎——Crab,并分享在复杂动效渲染场景下积累的实践经验和精彩案例。

一、项目背景

1.1 快手大型活动中的动效


动效在设计和用户体验领域中有重要的价值,表现力强的动效不仅能够激发受众用户的兴趣,提高参与度,还能提高留存和用户活跃度,最终增强用户对产品的粘性,因此活动中的动效越来越复杂。


下图是我们开发过的具有复杂动效的活动案例。


可以看到,在这些活动中,最显眼的 KV 部分由持续播放的动效进行承接,并且动效需要在用户游玩活动的过程中进行反馈。另外,因为这些动效的持续时间非常长,且位置显眼,所以为了保证用户的体验,需要这些动效在尽可能多的设备上正常展示。


总的来说,这类动效有三个特点:高表现力,高可交互性和高兼容度,为了方便说明,我们将同时满足这三个特点的动效称为「游戏化动效」

1.2 “常规”动效的实现方案和局限


一般情况下,动效的实现可以分成两种类型的方案:关键帧方案以及逐帧方案。


常规的关键帧方案有 CSS 和 Lottie,比较简单的动效选用 CSS 更好,比较复杂的动效选择 Lottie 可以更好的保证开发效率和动效还原。CSS 和 Lottie 的优势是通用和常规,但缺点是只能适合实现基础 Transform 或矢量图形变化的平面动效。


常规的逐帧方案有序列帧,以及 APNG,视频和透明视频这些针对不同场景的改进版本的类序列帧方案。逐帧方案的动效单帧表现力上限非常高,但代价是几乎没有可交互能力,基本只能制作单纯的播片逻辑动效。



总的来说,“常规”的动效实现方案只能满足可交互性和表现力的其中一种,很难兼顾,无法满足业务中游戏化动效的需求。


1.3 如何实现兼顾表现力和可交互性?


要实现兼顾可交互性和表现力的动效,就需要相比常规动效实现手段对动画元素控制性更强的实现手段。对于这个需求,最适配的方案就是使用 WebGL


WebGL 是一个给基于 OpenGL ES 的低级 3D 图形 API 使用的开放 Web 标准。通过 WebGL 的能力,我们对动画元素的控制粒度可以精细到该动画元素的细分图元层面,对动画元素渲染表现控制可以精细到单个像素层面。有了这种控制精度,就有了同时兼顾可交互性和表现力的底层支持。


为什么是 Crab?

WebGL 的 API 是低级 3D 图形 API,在实际业务使用中很难直接使用这个 API,下面是使用 WebGL 直接绘制一张三角形的示例:

function createShader(gl, type, source) {    const shader = gl.createShader(type);    gl.shaderSource(shader, source);    gl.compileShader(shader);    return shader;}  function createProgram(    gl,    vertexShader,    fragmentShader) {    const program = gl.createProgram();    gl.attachShader(program, vertexShader);    gl.attachShader(program, fragmentShader);    gl.linkProgram(program);    return program;}
let fragStr = `#version 300 esprecision mediump float;
in vec2 position;out vec4 outColor;  
void main () {    outColor = vec4(1., 0., 0., 1.);}
`;
let vertStr = `#version 300 esin vec2 a_position;uniform vec2 u_resolution;out vec2 position;

void main() {    position = a_position;    gl_Position = vec4(a_position * 2. - 1., 0. ,1.);}`;
let positions = new Float32Array([.5, 1., 1., 0., 0., 0.]);

let canvas = document.querySelector('#can');let video = document.querySelector('video');
canvas.width = 640;canvas.height = 320;
let gl = canvas.getContext('webgl2');let program = createProgram(    gl,     createShader(gl, gl.VERTEX_SHADER, vertStr),    createShader(gl, gl.FRAGMENT_SHADER, fragStr));
gl.useProgram(program);gl.viewport(0, 0, canvas.width, canvas.height);

const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
const positionbuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionbuffer);
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(positionAttributeLocation);
const size = 2;const type = gl.FLOAT;const normalize =false;const stride = 0;const offset = 0;
gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);gl.bindVertexArray(vao);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
const primitiveType = gl.TRIANGLES;const doffset = 0;const count = 3;gl.drawArrays(primitiveType, doffset, count);
复制代码


近百行的代码量才能渲染出如下的一个简单的三角形:



因此我们应通过封装完成的引擎来使用 WebGL 能力。


在进行动效渲染引擎的挑选之前,我们首先明确了四点挑选原则:

  • 实现轻量化:实现动效时,只引入所需的引擎内容,控制依赖包的体积

  • 可扩展性:可以方便的进行动效的渲染特性和交互特性的开发,而无需任何细节都强依赖于引擎的具体能力支持

  • 便于沉淀:渲染特性和交互特性的扩展有相对标准的接口和使用规范,方便进行沉淀和日后其他场景的复用。

  • 游戏化动效和业务在同一仓库下维护:减少协作成本,便于开发、调试和维护。


基于这四点原则,我们调研了已有的可选方案,发现它们都不能完全满足我们的需求,因此我们选择了自研 Crab 动效渲染引擎。



二、Crab 简介


Crab 是一款可在支持 WebGL 的环境(Web,快手小游戏等)中使用的游戏化动效渲染引擎。

2.1 流程 Crab 的分层架构方式


Crab 大体可以分为接入层、资产抽象层、扩展层、运行层和功能层,其中接入层、资产抽象层、扩展层、运行层都位于引擎的核心包中,功能层则由业务使用中积累的一些具有通用性功能的第一方或第三方的独立包组成。



这种分层架构方式有什么好处?


我们的游戏化动效引擎实现的指导原则就是上面提到过的实现轻量化、可扩展性、便于沉淀以及方便维护,这种架构方式可以很好满足我们的要求:

  • 可扩展性: 扩展层的 RenderProcessor 和 RenderPipeline 提供了渲染上的扩展能力, Component 和 System 提供了逻辑上的扩展能力, 通过它们,我们保证了这个引擎的扩展能力

  • 便于沉淀: 使用扩展层扩展出来的功能如果有沉淀价值, 可以单独发包, 作为功能层的一员

  • 实现轻量化: 正如刚才提到的, 大部分具体功能都位于功能层的不同包内, 所以我们的核心包比较轻量, 在使用功能的时候也可以只引入对应功能的包.

  • 游戏化动效和业务在同一仓库下维护: 作为一个主要使用 TS 和 WebGL API 的前端库,Crab 可以自然的在前端项目中使用。

2.2 Crab 一个 Tick 的处理流程


接下来我们从 Crab 的一个 Tick 入手,介绍下扩展层的实现方式。



Crab 在一个 Tick 的不同时机,放置了许多可以执行逻辑钩子的阶段,以此作为暴露交互能力的接口。使用者可以注册自己的钩子来执行自定义的逻辑操作


在这些逻辑钩子中间的渲染处理部分(Render Processor)则用来执行渲染操作,同样提供暴露渲染能力的接口。

2.2.1 逻辑处理部分


Crab 中渲染的场景是通过一个由许多节点组成的树状结构来描述的,节点上可以挂载不同的组件,组件中可以包含关于该节点的不同描述信息,比如 Transform 节点用于描述一个节点的位置信息,Renderable 节点用于描述一个可渲染节点的渲染信息。


节点组件中的信息可以被用户注册的逻辑钩子读写,用户可以通过实现和注册 System 的方式来进行逻辑钩子的实现和注册。不同的 System 可以指定只对满足某些特定条件的节点生效(比如必须挂载某个组件/必须没有挂载某个组件)以及要注册哪些阶段的逻辑钩子。


通过这种方式,使用者可以将不同的描述信息放入不同的组件中,将不同的操作逻辑放入不同的 System 中,有相关性的组件和 System 可以自然的组合作为一个功能的实现,可以增强代码的可读性并简化进行功能沉淀所需的前置操作。


2.2.2 渲染处理部分


渲染处理部分(Render Processor)主要由 1 个或多个渲染阶段(Render Stage)连接组成,每个渲染阶段的渲染结果可以作为该帧画面的最终展示效果,或者作为渲染出一帧画面需要的中间结果。每个渲染阶段可以执行场景中匹配的可渲染节点中的渲染管线(Render Pipeline),渲染管线是 Crab 中渲染的最小单元,会执行一次 Draw Call,以及该 Draw Call 相关的数据绑定,标志设置等逻辑。


使用不同的渲染阶段和连接方式,以及不同的渲染管线设置,就可以满足不同的渲染需求。



渲染阶段

渲染阶段是组成 Crab 的渲染处理部分的基础模块,Crab 提供了四种类型的渲染阶段,来满足从 FrameBuffer/场景到 FrameBuffer/屏幕的不同连接需求。


四种渲染阶段都可选接收用户传入的渲染信息,比如 Shader,Viewport,是否刷新 FrameBuffer 等。Crab 中的渲染阶段具有自由度很高的可扩展性,且可以像乐高积木一样互相拼接,用户可以利用它自由实现阴影,延迟渲染,后处理效果等渲染特性,也可以组合各种不同的渲染特性,积累自定义渲染流程,具有通用性的自定义渲染流程也可以通过发布为 Crab 功能层的一部分,获得更多的使用。



渲染管线

渲染管线是执行渲染的最小单元。内部维护了引用的 Shader 信息,以及对应的 Shader 变量,数据和宏开关等与一次 Draw Call 相关的信息。当要执行一个渲染管线的渲染时,Crab 会根据其包含的数据和宏等生成实例 ID,以此判断渲染执行时是否需要创建新的实例,减少实例创建和切换成本。


渲染管线一般被装载在可渲染(Renderable)组件中,Crab 的内置渲染 System 会自动执行匹配节点的可渲染组件中的渲染管线进行渲染,使用者可以直接使用 Crab 已经封装的可渲染组件,比如通用 Unlit 渲染组件、通用 Blinn-Phong 渲染组件、粒子渲染组件、Spine 渲染组件等来拼装需要的效果。使用者也可以扩展自己的可渲染组件,通过传入 Shader、混合模式等信息来实现自定义的渲染效果。



三、游戏化动效的应用与实践


在业务中使用 Crab 支持游戏化动效的过程中,我们也积累了一些通用的方案和功能,比如粒子系统,Spine 支持、一些材质和后处理效果等。



3.1 2D 动效方案应用

Spine

Spine 是一种基于 2D 的骨骼动画实现方案,相比于常规动效,它超越了传统 2D 动画的限制,能带来 2.5D 的体验。相比于 3D 动效,它的制作和渲染更加轻量,是一种比较平衡的动画方案。


Crab 对使用 Spine 动画时常用的动画播放、皮肤叠加/替换以及挂点功能进行了封装和支持,同时还提供了 Spine 资产的加载器,使用者在使用过程中可以不关注 Spine 的实现细节,从而更好的聚焦在业务逻辑等其他部分的实现上。


置换贴图


置换贴图是 AE 提供的一项能力,它能实现类似于 Spine 的 2D 骨骼动画的简易的效果,且素材生产成本相比 Spine 更低,因此在 2D 的氛围动效等相对重要性不高的动效实现时是一种具有性价比的实现方案。


置换贴图的实现原理:


要实现置换贴图的效果,需要三个素材,分别是:

  • 一张颜色贴图,用于表明渲染的像素色值

  • 一张位移系数贴图,用来表明每个像素进行形变的相对强度

  • 一组包含全局位移方向和强度的动画片段

设计师在 AE 中对一个图层的全局位移方向和强度进行动画关键帧的设置,就可以得到一个非线形形变的动画效果。简单说明的话,置换贴图效果可以被看作只由一根骨骼控制的,在一个贴图分辨率大小的平面网格上的骨骼动画。



3.2 3D 动效方案应用

模型材质


上限最高的渲染方式自然是基于物理的渲染方式,但是这种渲染方式有一个问题在于,在简单的光照条件且不引入光线追踪时,渲染效果一般比较粗糙,而如果设置复杂的光照条件和材质参数,则渲染参数的调试成本和性能消耗都会很大。


对于这种性能和渲染效果上的困境,我们在实践中选择了另一种材质——Matcap 材质。Matcap 材质的本质是通过预计算贴图保留了一个材质在各个可见方向上的颜色信息,可以很好的保留 C4D 等软件中的渲染效果;引擎实时渲染时,只需要读取贴图,当场景中不存在动态光源时,可以在较低的性能消耗下,实现接近离线渲染的表现效果。



Matcap 材质的实现原理


要实现这个材质,需要三种贴图:索引贴图,Matcap 贴图以及可选的颜色贴图。


当进行着色时,首先访问索引贴图,获得该像素点对应的 Matcap 贴图索引,然后依据该点上的法线向量,从对应的 Matcap 贴图获取特定位置的像素作为最终使用的像素即可。


对于颜色比较少的材质,直接使用 Matcap 贴图上的颜色信息没什么问题,但如果颜色比较多,需要的 Matcap 贴图的数量将会是难以接受的,为了避免 Matcap 贴图数量的暴涨,可以使用一张颜色贴图,最终颜色不再直接使用 Matcap 贴图中存储的颜色,而是通过 Mapcap 贴图中的颜色和颜色贴图中的颜色混合得到。



模型动作

动作过渡

要实现一个生动的 3D 角色,需要保证它持续活动着,最简单的方法就是时刻保持在模型上至少应用一个动画片段。


不同的动画片段之间切换的时候,如果不做任何优化,效果会很生硬,对此我们应用了动画过渡和混合的能力,可以在不增加设计师工作量的前提下,提高动画的流畅度。


未应用动作过渡

应用动作过渡


顶点形变动画

骨骼动画是基于关节 Transform 混合的动画,虽然在大部分情况下的表现都足够优秀,但是在诸如表情动画等对形变的精细性要求更高的场景下往往显得不够生动。


对此我们应用了顶点形变动画的方案,顶点形变动画的原理是保存网格的多份快照,并在动画过程中改变不同快照的应用权重,从而实现精细动画的效果。

表情快照

动画效果


多快照的兼容性实现


顶点快照信息一般使用贴图的方式在 Shader 中进行使用,最理想的实现方式是使用 Texture Array 的方式存储顶点快照,但 Texture Array 是在 WebGL2 上才全面支持的特性,对只能使用 WebGL1 的设备,如果用普通的 2D 贴图存储顶点快照,则受限于贴图数量的限制,以及材质的其他特性所需贴图(颜色贴图,法线贴图,AO 贴图等)挤占位置,会导致可以应用的快照数量非常有限,为了让只能使用 WebGL1 的设备也可以实现多快照的顶点形变动画,我们在这种情况下将所有快照存放在同一个 2D 贴图中,并实现专门的查找函数查找对应的顶点快照信息,在 WebGL1 的上下文中实现了兼容更多快照的顶点形变动画。


3.3 特效应用

粒子系统


粒子系统是非常常见的特效方案,可以用于模拟火、烟、云、水、落叶等自然现象,也可以用来模拟发光轨迹、速度线等抽象视觉效果。


Crab 中提供了一套粒子系统以供使用。

Crab 的粒子系统

Crab 的粒子系统实现参考了 Unity 的粒子系统组织方式,由多个组件构成,这些组件可以氛围核心组件和可选组件。



核心组件中的主组件负责控制粒子系统的基本参数,比如生命周期、粒子存活时间等,发射组件负责控制粒子的发射。


可选组件实现了不同的功能,比如形状组件可以控制发射器的形状,Velocity Over Lifetime 组件控制粒子生命周期中的速度变化等等。使用者可以根据想要的效果只启用对应的可选组件。

Shader Effects


对于一些特殊的,或者不具有通用性的特效,一种值得考虑的实现方式就是使用 Shader

在一些情况下,Shader 实现会比使用通用方式性能更好,或得到特别的表现效果。


借助 Crab 提供的渲染阶段和渲染管线上的扩展能力,使用者可以自由实现期望的 Shader 效果。

下为使用渲染阶段实现 Crab 文字内流体效果的部分代码示意:

this.scene.renderProcess  .addStage(advectVelPass)  .addStage(disturbVelPass)  .addStage(advergencePass)  .addStage(iteraGroup)  .addStage(applyForcePass)  .addStage(disturbDyePass)  .addStage(advectDyePass)  .addStage(bloomGroup)  .addStage(displayPass);
复制代码


四、游戏化动效的应用与实践


对于一个场景的游戏化动效,里面不同的原子动效,研发会接收到来自上游的许多不同种类的交付素材。其中既有来自平面动效交付链路的 Lottie、序列帧、AE 参数等,也有来自游戏化动效交付链路的 3D 模型、粒子特效和模型材质等。



这么多种类的来自不同链路的动效交付,给我们带来了几个挑战:

  • 混合播放

交付的素材中既会有 3D 模型等游戏化素材, 又会有 Lottie, 序列帧等平面动效素材, 如何保证这些不同来源的资产可以一同展示?

  • 素材生产

一些动效素材, 比如粒子特效参数, 材质参数等, 必须基于动效渲染引擎进行所见即所得的编辑来生产, 如何实现?

  • 素材转换

FBX 模型,、序列帧等素材,无在 runtime 直接使用的表现不佳,需要进行格式转换。每种类型的素材需要的转换方式不同,如何降低转换过程中的心智负担?

交付痛点的解决方案

为了解决上述的痛点,我们为 Crab 开发了一个编辑器,编辑器提供了素材的导入导出、格式转换、编辑以及预览功能。


上游提供的任意已经支持的动效素材类型,经过编辑器中可选的格式转换以及编辑处理之后,都可以导出为可以在 Crab 直接进行使用的动效素材,而编辑器提供的预览功能也可以保证导出的原子动效的效果符合设计同学的预期,将走查问题尽量前置。



交付示例

以材质交付为例,当在编辑器的属性面板编辑材质参数时,主视区的预览效果可以实时更新,当设计师编辑完成后,可以直接导出为研发在 runtime 时可用的材质素材。



五、游戏化动效的性能指标和调优


Crab 内部执行渲染操作时,会执行一些基础的性能优化,比如资产在显存创建后,及时销毁资产在内存的缓存,减少不必要的 WebGL 上下文切换,如活跃 shader program 切换、混合模式切换等。


除了 Crab 引擎内部的优化,在特定场景的调优对一个游戏化动效的性能常具有更显著的影响。在进行游戏化动效开发时,主要关注的性能指标有以下几个:帧率、Draw Call 数量,三角形数量,内存占用以及卡顿率。


  • 帧率:我们可以通过锁定引擎帧率的方式,控制每秒执行的逻辑计算和渲染计算的数量,从而减少对计算资源的占用,提供页面整体的性能。

  • Draw Call 数量:我们可以通过视锥剪裁或使用实例化渲染的方式来减少 Draw Call 的数量。比如当场景中存在 N 个相同模型的时候,使用实例化渲染可以将 Draw Call 的数量从 N 次减少到一次。

  • 三角形数量:一帧渲染使用的三角形数量对性能影响很大,对一些不太重要或不必要的场景物体,我们会尽量减少它包含的三角形数量,比如使用单面片或十字面片,可将物体所需的三角形数量降低至 10 个内。

  • 内存占用:要降低内存占用,主要可以通过减少贴图分辨率或使用压缩贴图的方式来进行优化,另外也要注意使用网格的物体三角形的数量不要爆炸,否则也会对内存占用带来负面影响。

  • 卡顿率:对于卡顿率,我们一般可以通过优化 shader 中的逻辑实现进行优化,比如在 shader 中要尽量避免动态循环次数的循环语句和条件语句的使用。


以上就是本篇文章的全部内容,介绍了自研的 Crab 动效渲染引擎,以及 Crab 在实际业务的使用中落地过的具体动效方案,交付方案和性能调优方案。希望能给看到这里的你带来一些收获,如果有任何问题或建议,也欢迎留言,不吝赐教。

- END -


用户头像

快手技术

关注

还未添加个人签名 2024-05-15 加入

快手官方技术号,即时播报快手技术实践的最新动态 关注微信公众号「快手技术」

评论

发布
暂无评论
快手动效渲染引擎Crab,解锁“游戏化动效”开发新方式!_Java_快手技术_InfoQ写作社区