性能优化-图片小地图

前言

小地图是游戏中常见的功能,为了实现效果,我们一般会挂上一个Mask组件,实现只展示一小部分的效果。
小地图常常是一张比较大的图片,直觉上渲染的部分变小了,性能也会有对应的提升,实际情况是怎样呢?
Mask组件基于 模板测试 实现了剔除内容,在渲染管线中,模板测试发生在“ 测试与混合 ”阶段( 在片元着色器之后
也就是说,实际上图片还是被“完整”地绘制了一遍,只是只展示了一小部分:crazy_face:

是不是有亿点点被欺骗的感觉?
并不尽然, 虽然模板测试发生得比较晚,但还是有一定的性能优化效果
那有没有办法真正实现,只绘制一小块区域呢?本文中,将会通过 修改图片裁剪区域 ,来实现 减少绘制区域 的效果。
本文是一个很简单的优化效果实现, 如果你看到这里已经大概猜到了实现方式,就不必继续往下看了

1. 编码环境

浏览器:Chrome
开发语言:TypeScript
引擎版本:CocosCreator 2.6.2

2. 研究过程

开头提到 图片裁剪区域 ,好像有一点熟悉的感觉???
没错,说的就是图片资源在属性面板中的 Trim属性

image

一般情况下我们不会改动相关属性值,因为Cocos已经帮我们自动处理了!
导入图像资源后生成的 SpriteFrame 默认会进行自动剪裁,去除原始图片周围的透明像素区域。这样我们在使用 SpriteFrame 渲染 Sprite 时,将会获得有效图像更精确的大小。
–Cocos文档

Trim属性会影响SpriteFrame在渲染时的UV值 ,因此实现了裁剪图片的效果。具体原理:
Trim属性值在游戏中,会被设置到SpriteFrame的 rect 属性上。来看一下UV值的计算方式:

// 2d\assets\sprite-frame.ts
const tex = this.texture;
const texw = tex.width;
const texh = tex.height;

// l、r、b、t分别对应左、右、下、上
const l = texw === 0 ? 0 : rect.x / texw;
const r = texw === 0 ? 1 : (rect.x + rect.width) / texw;
const b = texh === 0 ? 1 : (rect.y + rect.height) / texh;
const t = texh === 0 ? 0 : rect.y / texh;

uv[0] = r;
uv[1] = t;
// 省略其他赋值

rect值根据图片宽高归一化之后,得到的就是uv值了。

如果我们能够在代码中动态地修改 rect ,不就能实现裁剪渲染区域,以及移动的效果了吗?
幸运的是,Cocos提供了修改 rect 的接口,我们可以很轻松地实现这一功能。

3. 实现思路

我们通过spriteFrame.rect就可以实现rect的设置和获取,它包含了getset函数。

// 2d\assets\sprite-frame.ts
get rect () {
    return this._rect;
}

set rect (value) {
    if (this._rect.equals(value)) {
        return;
    }

    this._rect.set(value);
    if (this._texture) {
        this._calculateUV();
    }
    this._calcTrimmedBorder();
}

当我们修改rect时,Cocos也同时进行了UV的计算。

rect中,包含了x、y、width、height四个属性值,大部分情况下我们只会更改xy,而widthheight只要一开始的时候设定好就可以了。

小地图的移动就不多讲了,当角色移动时设置新的小地图位置即可。

4. 代码实现

首先,我们在初始时,设置小地图的宽高,即rectwidthheight属性。

// ViewMap.ts
start() {
    this.spfMap = this.spriteMap.spriteFrame;

    // 设置小地图大小
    let mapTransform = this.spriteMap.node.getComponent(UITransform);
    let rect = this.spfMap.rect.clone();
    rect.width = mapTransform.width;
    rect.height = mapTransform.height;
  	// 你也可以继续修改x、y来修改小地图的初始位置
    this.spfMap.rect = rect;
}

我们将小地图宽高设置为与小地图节点大小一致。

为了简单模拟游戏中移动小地图的情况,我们使用触摸来移动地图。

// ViewMap.ts
start() {
    this.node.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
}

onTouchMove(event: EventTouch) {
    // 更新小地图裁剪起始位置
    let rect = this.spfMap.rect.clone();

    // 保证rect不会超出小地图尺寸
    let x = rect.x - event.getDeltaX();
    rect.x = Math.max(Math.min(x, this.spfMap.width - rect.width), 0);
    let y = rect.y + event.getDeltaY();
    rect.y = Math.max(Math.min(y, this.spfMap.height - rect.height), 0);

    this.spfMap.rect = rect;
}

结束了!感觉一切都很顺利。
emm​:expressionless:… 好像顺利才是最不对劲的事情…

游戏画面中出现了小地图被裁剪出来的区域!

可是我们滑动屏幕的时候,小地图一动也不动,但毫无疑问,我们已经实现了裁剪的效果。
这是为什么呢:face_with_monocle:


:star: 拓展阅读
Cocos为了优化渲染时的性能,只有当某个部分(比如UV值)发生改变后,才会重新填充渲染数据,但这部分超出了本文的讨论范围,感兴趣的同学可以自己进行了解~


我们确实修改了UV值,但Cocos不知道,自然也不会修改到渲染数据中,那怎么通知Cocos进行一次填充呢?
这个时候Cocos又提供了一个函数!
我们可以调用sprite.markForUpdateRenderData(),通知Cocos渲染数据发生了改变。

// 2d\framework\ui-renderer.ts
/**
 * @en Marks the render data of the current component as modified so that the render data is recalculated.
 * @zh 标记当前组件的渲染数据为已修改状态,这样渲染数据才会重新计算。
 * @param enable Marked necessary to update or not
 */
public markForUpdateRenderData (enable = true) {}

注:Sprite继承了UIRenderer

稍微改动一下我们的代码:

// ViewMap.ts
onTouchMove(event: EventTouch) {
    // 更新小地图裁剪起始位置
    let rect = this.spfMap.rect.clone();

    // 保证rect不会超出小地图尺寸
    let x = rect.x - event.getDeltaX();
    rect.x = Math.max(Math.min(x, this.spfMap.width - rect.width), 0);
    let y = rect.y + event.getDeltaY();
    rect.y = Math.max(Math.min(y, this.spfMap.height - rect.height), 0);

    this.spfMap.rect = rect;
    // 在最后通知一下Cocos
    this.spriteMap.markForUpdateRenderData();
}

现在可以流畅地滑动我们的小地图了!
做完了,是个很简单的优化吧~

5. 效果对比

在这里我们对三种情况进行对比。

  1. 无Mask(完整渲染整张地图)
  2. Mask
  3. Mask+Trim

情况1显然不符合实际需求,这里只是为了做比较~

测试方式:
由于渲染管线中的耗时并不在Cocos的统计中,因此我们采取对比帧率的方式进行比较。
我们将最大FPS设置为300,并将同一张图片创建200、500、1000次,作为多组测试数据,图片分辨率为19201080,Mask大小为200200。
根据测试环境、数据的不同,数据会有出入,仅供参考

不移动小地图时:


通过对比可以看出,开启了Mask,还是有一定的优化效果的。不过… 对比裁剪,效果还是差了一些。

持续移动地图时,测试结果几乎和不移动一模一样(在误差范围内),要不是看到图片在动,甚至以为我测试代码写错了…

对比只使用Mask,Mask+Trim的优化效果分别为75.4%、87.7%、87.0%。:tada::tada::tada:

6. 注意事项

  1. 进行rect裁剪后,得到的是一个矩形区域,如果你需要的是一个圆形小地图,那仍然需要一个Mask组件
  2. 我们针对spriteFrame进行了裁剪区域的修改,如果spriteFrame存在共享的情况,可能会导致其他地方显示错误(记得重置回默认值!)。

7. 总结

本文提出了一种针对大尺寸图片但只需要显示一小部分的情况进行优化的方式,并不仅仅可以用在小地图上。
小地图的优化除了类似本文中的方式以外,还有很多优化方式,比如进行分块裁剪,这样同时减少了加载时间(但增加了加载次数)。
本文提及的技术暂未在项目中实际运用,欢迎讨论与指正。

文末特别感谢一下@YipLee提供的关于渲染管线的帮助~

5赞

这个感觉使用场景非常的有限。。

是的… 单纯就是感兴趣做出来玩一下