这家公司太牛逼了,虽然这次不是重新造轮子!动画蒙版

01 目的

本文的目的是介绍如何在场景(可能含有多个spine动画)上实现动画蒙版(也就是遮罩mask会动会变形), 根据实现方式的不一样, 会有如下的效果:

图片
图片

02 收获

  1. 了解到什么是模板测试
  2. 了解到矩阵变换
  3. 了解 Cocos Creator 基础的渲染流
  4. 了解 Cocos Creator 里如何写一个 shader 并传递 uinform 参数
  5. 本文涉及到的素材和代码:

一个spine动画(从spine官方示例中取得) 一个圆形蒙版图片(ps里随便画一个)

1

本文中涉及到的所有代码都可以在这里(https://github.com/laomoi/ccc-test-mask)找到

cocos creator 工程在2.3.1下测试通过。

03 蒙版测试实现动画蒙版

3.1 什么是模板测试(stencil test)

在opengl渲染管线中, 当片元着色器处理完着色之后, 着色结果在实际写入颜色缓冲之前, 可以进行模板测试从而可以丢弃一些片元的着色结果。

而gpu是如何知道改丢弃那些片元呢? 主要取决于模板缓冲中的值。

模板缓冲区跟颜色缓冲区类似, 它也是一块画布, 我们假设它的分辨率跟屏幕分辨率一样,只是它每个像素的精度是8位。

当我们开启了模板测试之后, 某个座标(x,y)的片元颜色在实际写入颜色缓冲之前, gpu会从模板缓冲区同样的座标(x,y)取到该点的模板值进行后继判断:

当这个值符合我们的设定规则时(比如:该模板值>0时不丢弃片元), 则让片元生效, 否则丢弃该片元。

所以我们可以把模板缓冲区理解为一个筛沙子的竹篮子, 值为0的地方我们可以认为是镂空的

当开启了模板测试之后,后继所有的图元绘制都需要在竹篮子里筛一遍, 直到关闭模板测试为止。

要实现我们上文中的图形效果, 我们可以脑补一下模板缓冲中每一帧的变化:

2

通过这样的模板缓冲变化, 就可以不断的让每帧多显示一些动画内容, 从而实现我们要的效果。

(想更全面理解模板测试的话, 可以看LearnOpengl这篇文章

https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/02%20Stencil%20testing/

3.2 如何在每一帧里正确的填充模板缓冲区来实现我们的目的。

我们只需要按照下面的步骤来通知webgl/opengl, 即可往模板缓冲区里写入我们要的形状:

  1. 先清空模板缓冲区, 也就是所有像素的模板值都置为0
  2. 开启模板缓冲区写入
  3. 按照正常的图元绘制方式画圆形遮罩图
  4. 圆形遮罩图的片元着色器中开启alpha test, 比如我们设定一个alpha阈值0.1, 如果片元的alpha度大于该阈值, 就把1写入模板缓冲区, 否则丢弃该片元(相当于该位置的模板缓冲值保留原0的取值)

我们只需要每一帧都这么执行这几步,只是每一帧里不断把圆形遮罩图的y座标不断的往上增加即可实现我们要的效果。

所幸的是我们不需要自己操作webgl/opengl的api来做这几步, cocos creator的 cc.Mask组件已经帮我们做了这几件事:

3

我们只需要 把我们的圆形遮罩图(shadow.png)拖入 mask 组件的 sprite frame 即可。

不过接下来我们遇到的问题是, cc.Mask 目前的设计是比较死板的, 它的模板测试只能针对它下面的子节点,  在我们这个例子中,节点是这样的关系:

4

我们希望 hero-mask 这个节点的y座标每帧往上增加一些,这样模板缓冲里的圆形才能往上升, 但是我们又希望保持 spine 节点(hero-pro)在屏幕上的座标不动,但是目前cc.Mask的设计导致了我们无法同时做到这2点, 因为移动 hero-mask 的 y 座标会导致下面的 spine 节点也一起移动。

看上去我们只能祭出魔改引擎这招大杀器了, 在打开引擎底层的 cc.Mask 代码阅读之前, 我们需要先补习一下 Cocos Creator v2.x 之后的渲染流机制。

Cocos Creator v2.x 的渲染流(render-flow)本质上其实跟以前版本并没有太大的区别,只是使用了一个单独的数字renderFlag来记录单个节点的所有dirty状态

图片

这个renderFlag的每一位(bit)记录了一个dirtyFLag, 如图所示。

比如说第3位设置为1, 则表示节点的本地座标或者缩放等局部属性发生了变化, 在绘制这个节点的时候,需要调用 updateLocalMatrix() 来进行更新, 本质上其实也是if判断。

每个节点绘制的时候,是按照上图表示的顺序, 从上到下进行判断, 本文中仅关注以下几个dirtyFlag的变化:

  • LOCAL_TRANSFORM(本地座标等属性)
  • WORLD_TRANSFORM(世界座标等属性)
  • UPDATE_RENDER_DATA(顶点数据属性)
  • RENDER(渲染本身,以及其他操作比如开启模板测试)
  • CHILDREN(遍历子节点)
  • POST_RENDER(完成所有子节点遍历后的渲染函数,比如用于关闭模板测试)

我们忽略掉其他不关心的 dirtyFlag, 用一个比较简单的伪代码函数来描述整套渲染的流程(跟实际代码并不完全一致, 只是可以大体描述):

6

(想深入学习的可以阅读 Cocos 源码中的render-flow.js)

上图红框中标记的代码是我们目前最关心的2行代码, 在 updateRenderData() 中会生成顶点数据, 在fillBuffers()会把这些顶点数据写入顶点缓冲区.

我们这里不考虑修改 fillBuffers(),我们只需要魔改 cc.Mask.updateRenderData() 函数的实现,让(圆形遮罩图片)的顶点数据每帧向上移动即可, 只要没有修改节点本身的本地矩阵和世界矩阵,  也就不会影响下面的2个 spine子节点。

3.3 第1种魔改方法

修改assembler.updateWorldVetex()

打开引擎的 mask-assembler.js 和 2d/simple.js,assembler-2d.js 可以看到如下代码

7
图片

hero-mask 这个节点在绘制本身的时候, 从 updateRenderData() 最终会调用到 updateWorldVertex() 函数, 这个函数的内容是我们需要魔改的重点。

首先我们需要先看懂 updateWorldVertex() 里面的这些代码究竟在做什么事情。

这里先简单补习一下矩阵变换的意义。

在图形渲染中, 我们通常使用一个 4x4的矩阵来表示点的仿射变换(缩放, 旋转, 斜切, 移动), 虽然在 2D 世界里其实我们用一个 3x3 的矩阵也够用了, 不过为了兼容3D世界的座标系, 所以我们统一使用的是4x4。重新看一下我们的节点树结构:

8

我们使用M1矩阵来表示 节点 hero-mask 节点的本地变换矩阵, M2来表示场景根节点的世界变换矩阵, 那么 hero-mask 的世界变换矩阵就是 M3 = M2 x M1,有了这个M3之后, 我们就可以很方便的把 hero-mask 的任意一个本地座标换算到世界座标:

顶点的世界座标 =  M3 x (hero-mask顶点的本地座标)

那么这个矩阵和座标的乘法具体是怎么计算的呢, 如下图所示:

10

回头再来看 updateWorldVertex() 里面的代码, this._local是圆形遮罩4个顶点的本地座标,

com.node._worldMatrix 就是我们上面提到的M3, 也就是 hero-mask 的世界变换矩阵,

我们把M3 x 顶点本地座标 就会得到4个顶点的世界座标, 然后存入 renderData的vDatas 数组中。

我们现在魔改的方法就是,我们需要在 M3 x 顶点本地座标之前,先对顶点本地座标做一个临时变换,让顶点的本地y座标往上增加之后,再去跟 M3 相乘即可。

在这个例子中我们可以简单的修改 local.y += distance 来达到目的,不过如果以后我们想对圆形遮罩想做一些更复杂的变换,比如缩放旋转之类的,那么就得用一个矩阵变换来做了:

11

更详细的细节可以看我上面提供的源码。

这种修改JS层的 updateWorldVertex 方法在原生平台下并不是总是生效,如果 hero-mask 的父节点每帧都在移动或者变形导致 hero-mask 每帧都会触发原生层重新计算顶点的世界座标。

*因为在native层的渲染跟js层并不完全一致,* 比如 maskAssembler 在原生层是在 fillBuffers() 里会判断 world transform 是否 dirty, 从而去重新计算顶点的世界座标。

这样即使在 js 层 updateRenderData() 的时候你修改了顶点的世界座标数据, 但是原生层的 fillBuffers() 是在后面执行的,会导致这个修改失效。

3.4 第2种魔改方法

把 mask节点拆成2个节点

根据引擎大佬提供的另外一种思路,我实现了第2种魔改方法。

我们通过上述描述应该知道了整个绘制是这样的流程:

  1. hero-mask 节点打开模板测试开关
  2. 打开模板缓冲写入,绘制圆形遮罩到模板缓冲上, 关闭模板缓冲写入
  3. 绘制 hero-mask 节点下的2个 spine 子节点
  4. hero-mask 节点关闭模板测试开关

我们把整个显示结构改成下图所示:

12

begin-mask 和 end-mask 节点都挂载了自定义的 mask 组件:

13

只是一个开启了 fillBuffers() 用来开启模板测试, 另外一个开启了 postBuffers() 用来关闭模板测试。

同样的我们在代码里让 beginMask 这个节点做一个向上运动动画即可, 因为这次 spine 节点不再是 beginMask 节点的子节点,

所以 beginMask 的移动不会影响到 spine 节点。

整个显示效果跟第一个魔改方法一样。

注意这种修改方法,在原生层要生效的话需要修改 C++ 代码, 因为原生层的 fillBuffers 和 PostFillBuffers()是否调用跟JS层无关。

在上面提供的源码里我同时也修改了 C++ 代码里的 MaskAssembler.cpp 代码,增加了 setBeginMask() 和 setEndMask()方法, 有兴趣可以看一下。

04 自定义材质实现动画蒙版

上面使用模板测试的方法来实现遮罩, 它的优点是实现简单, 而且支持多层遮罩嵌套(最多8层), 缺点是遮罩边缘比较生硬。

如果大家使用过photoshop等美术工具里面的图层蒙版, 就会发现这些美术工具里的蒙版是支持边缘羽化的, 我猜测他们实际上就是用了一张带透明度的蒙版纹理,覆盖在原图层上面。

原图层某像素的颜色.alpha *= 蒙版纹理在在该像素点上的alpha

通过这种方式就可以实现类似片元剔除和羽化的功能。

实现原理非常简单, 缺点是它需要修改需要被遮罩的所有子节点的片元着色器, 也就是修改他们的材质, 在新的片元着色器中传入这张遮罩图的纹理, 进行纹理采样之后 再乘上原来片元的颜色即可。

也就是对片元着色器做如下修改即可:

14

红色框是我们在片元着色器中新加的代码,texture2 是那张半透明的白色遮罩图的纹理, mask_uv 是一个 uv 座标,是一个 vec2 的结构,表示对应在渲染该片元时,应该去白色遮罩图的哪个座标上进行采样从而得到叠加的 alpha 度。

(关于什么是uv座标这里不阐述)

如何在片元着色器中得到一个正确的 mask_uv 是我们接下来要重点解决的问题,如下图所示:

15

当我们在渲染这个spine节点的头发上这个蓝色点时,如果我们能知道这个蓝色点的世界座标,那我们就可以反推计算这个点在这个圆形白色遮罩图上的本地座标, 得到这个本地座标之后, 再进行座标映射,就可以得到这个座标对应的mask_uv值。

我们在上面的模板测试中提到, 一个本地座标 localPt 乘以一个世界变换矩阵M就可以得到世界座标 worldPt , 同理我们可以知道, 把一个世界座标 worldPt 乘以这个M的逆矩阵就可以得到对应的 localPt, 也就是:

worldPt = M x localPt
localPt = M的逆矩阵 x worldPt

(计算一个矩阵的逆矩阵, Cocos Creator已经给我们提供了方法:mat4.invert())

接下来要做一个本地座标到 uv 座标的映射变换,因为我们得到的是图片内部的本地座标(x,y), 图片的中心点座标是(0,0), 最终我们要得到uv座标范围是[0,1]的值, 已知图片的宽度w, 高度h:

16

很容易可以算出来:

u = (x + w/2) / w = x/w + 0.5 v = (y + h/2) /h = y/h + 0.5

我们可以把这个纹理座标的映射放入顶点着色器里去算, 但是这样需要把 w 和 h 当作 uniform 传入顶点着色器,所以我们干脆再对原来计算好的逆矩阵把纹理座标映射的计算叠加上去, 整个代码如下:

17

顶点着色器里代码如下:

18

有几点需要注意:

  1. 我们在传递一个矩阵数据给片元着色器时,需要把这个4x4的矩阵 转换为一个float32的数组;
  2. 圆形白色遮罩这张纹理,在编辑器里必须关掉Packable的属性,如果这个纹理被动态合入了大纹理集,那uv座标就没法正确计算;
  3. cocos creator的默认顶点着色器里有一个CC_USE_MODEL的宏, 当定义了这个宏的时候,意味着顶点数据里的顶点数据并不是世界座标, 我们需要额外再乘上一个 cc_matWorld 才能得到我们要的世界座标;
  4. spine的着色器跟上图代码中贴出来的略有不同, 有兴趣的可以看下上面我提供的源码。

到这里为止我们已经提供了2种不同方式实现动画的动态蒙版, 在实际项目开发时, 我们大部分情况并不会用代码的方式来控制这个圆形遮罩的运动, 而是让美术在编辑器里把整套动画都做好.

当然现在这套编辑器目前仅是公司内使用, 等以后或许有机会把编辑器放出来给大家试用。

在后面的时间里,我们会陆续给大家分享一些我们项目中用到的一些魔改技巧或者图形效果之类的, 希望可以给cocos社区贡献一份绵薄之力。



乐府互娱 是9102年成立的的一家非常年轻的游戏公司, 位于上海目前游戏新秀扎堆的漕河泾开发区, 有兴趣进一步了解的可以点开我们的官网:http://www.lovengame.com


本文分享自微信公众号 - Creator星球游戏开发社区(creator-star)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章