« November 2016 | Main | January 2017 »

December 12, 2016

用分布式压缩贴图加快 Unity3D 的打包过程

U3D 的打包流程,谁用谁知道。

由于输出 ios 包必须在 xcode 环境,跑在 Mac 系统上,所以为了定期版本打包,我们采购了配置比较高的垃圾桶来做。一台大约要三万 RMB 左右。

但我觉得这个方案的性价比太低了。

经过简单的考察,我发现,打包流程中最慢的环节是贴图压缩。在不同的平台,需要把原始贴图文件压缩成对应平台的压缩贴图格式: ios 平台对应的是 PVR 压缩格式;Android 平台对应的是 ETC 压缩格式,等等。

u3d 自己也意识到压缩贴图太慢,所以官方给出了一个 CacheServer 方案。

本质上 CacheServer 只是一个文件 cache 服务器,它记录了用贴图原文件和转换参数(u3d 的 meta 文件)以及转换器版本等信息构成的串的 md5 值作为文件的索引。

第一个做转换的人,在本地压缩完毕后,会将结果上传到 CacheServer ,然后后面的人需要做相同的压缩时,去 CacheServer 查找是否之前有人做过同样的工作,避免重复操作。

当源文件相同、转换参数也完全一样时,结果会被缓存。由于日常几乎所有贴图都被人压缩过,这样极大的减少了定期打包的时间。


CacheServer 的设计虽然简单,却不能最好的解决问题。实际打包时,还是需要很长的时间。这是因为,定期打包需要输出各个平台的包,而日常开发只会稳定在一个平台上。比如日常工作使用的都是 Android 平台的话,等打 ios 包时,依旧需要压缩 pvr 贴图。

CacheServer 只做结果缓存,所以不可能在 Server 那里针对源图自动做多个版本的压缩格式;而且它保存的是转换参数的 hash 值,丢失了参数信息。btw,它的实现也是非常粗糙的。

U3D 转换图片的另一个问题是,导入贴图时会在本地机器上进行大量的贴图压缩工作,这个工作是极耗 CPU 的,把工作机卡上几秒不能动弹是常用的事,非常影响工作心情。

综上,我想实现一个远程压缩贴图的方案。

U3D 调用的是一个叫作 PVRTexTool 的命令行工具压缩贴图的。这是 PowerVR 公司提供的官方工具,幸运的是,它有 Linux 版本。这样我们就可以有办法用廉价的 linux 服务器来做这件事情,而不必采购昂贵的 Mac 垃圾桶。

最简单的方案是替换掉本地的 PVRTexTool 命令行工具,将源贴图上传到远程服务器,然后远程调用该工具压缩贴图,最后再把结果文件传回来。

做过改造之后,打包流程中最消耗 CPU 的工作就被转移走了。经过简单的测试,配合一个性能并不算强的 8 核服务器辅助压缩贴图,在 Mac Mini 上打包也可以比 4 核的垃圾桶工作站快上 30% 。而实际使用时,可以用采购垃圾桶一半的钱买到性能高几倍的 32 核服务器。

更重要的是,这个远程打包服务,还记录了每次压缩贴图请求的原始图片和压缩参数。我们可以写一个脚本,安排在夜间运行,找出白天提交的压缩请求,把命令行中的 -f 参数换掉(控制生成何种平台的压缩格式),为每个平台的压缩格式都生成一次。这可以解决只用 CacheServer 不能自动生成全平台压缩贴图的问题。

除了打包机外,开发人员的桌面开发机也可以部署这个工具,提高导入图片的使用体验(不再依靠本地工具压缩图片,不会因为导入贴图而卡住工作机)。


我花了一个周末用 skynet 实现了这么一个小工具。在我的 github 仓库里可以找到 。由于是自己使用,所以并不想写部署说明,也不打算解答使用上的问题。如果有游戏公司想用又不想自己折腾,可以提供一次性的付费技术支持服务。

December 06, 2016

用 Ascii 画关系图

写备注总有时候光靠文字描述不是很清楚,这个时候特别想在文档中附个关系图。用 graphviz 的 DOT 来描述图固然不错,但是它需要额外工具来生成才能直观的查看。如果我们想在纯文本环境附上插图,使用 ascii art 是最佳选择。

我搜了一下,perl 的 Graph::Easy 是唯一能找到的支持 ascii 输出的关系图绘制软件。

但是这个东西有个问题,就是对中文支持不行,虽然文档中说完全支持 Unicode ,但显然作者并没有把汉字用在 Ascii 输出上过。

问题是这样产生的:UTF8 中,汉字是 3 字节,而对于显示来说,等宽字体的汉字是 2 倍普通字母宽。这样在排版时,你必须知道一个字符串在视觉上是多宽,才可能用 ascii 字符拼接出正确的方框和连线。

比如,这个帖子就讨论了类似的问题

一开始,我希望找到 Graph::Easy 库中取 label 宽度的函数,让它正确返回 label 的视觉宽度。修改后,发现还是不行。这是因为,这个东西在 ascii 输出时,是用一个 2D 的字符数组模拟了虚拟画布(framebuffer)。它认为,framebuffer 上的一个矩形区域,就应该是 w * h 个字符构成的区域。如果想进一步的让它在 framebuffer 上定位正确,还需要修改它计算坐标的地方。可是,写 framebuffer 是一个随机过程,你向左边写一个一个字符,如果是汉字的话,会改变右边的 x 坐标。也就是把右侧所有的字符都挤了一个位置。如果再重排右侧的所有数据代价就太大了,而且它的内部模块也并没有抽象出这样的定位函数。

后来我想到一个取巧的做法,再读取任何 label 串时,都把这个串处理一下,把其中的汉字全部追加一个不可能在正常文本中存在的字符 U+FFFF ,让每个汉字都真的占据两个字符位。这样的字符串处理函数原本就在模块内存在(因为它本身就要去做类似 \n 的转义),这样就可以让整个排版过程正常了。

接下来要改的是最终把 framebuffer 序列化回字符串的过程,把里面所有的 U+FFFF 都删掉,就可以正确的输出了。

ps. 本来想提个 PR ,但是 github 上的那个仓库似乎不是原作者在维护的, CPAN 上的版本也不知道怎么提 issue ,所以还是自己玩玩就行了吧。


2017 年 5 月 2 日补充:

有同学找我要 patch ,我附在这里:基于 Graph-Easy 0.76 版。

diff --git a/lib/Graph/Easy.pm b/lib/Graph/Easy.pm
index 0ae40fd..b67bacc 100644
--- a/lib/Graph/Easy.pm
+++ b/lib/Graph/Easy.pm
@@ -1570,7 +1570,9 @@ sub as_ascii
   # select 'ascii' characters
   $self->{_ascii_style} = 0;

-  $self->_as_ascii(@_);
+  my $asc = $self->_as_ascii(@_);
+  $asc =~ s/(\x{FFFF})//g;
+  $asc;
   }

 sub _as_ascii
diff --git a/lib/Graph/Easy/Node.pm b/lib/Graph/Easy/Node.pm
index b58f538..6d7d7c7 100644
--- a/lib/Graph/Easy/Node.pm
+++ b/lib/Graph/Easy/Node.pm
@@ -1503,6 +1503,9 @@ sub label

   $label = $self->_un_escape($label) if !$_[0] && $label =~ /\\[EGHNT]/;

+  # placeholder for han chars
+  $label =~ s/([\x{4E00}-\x{9FFF}])/$1\x{FFFF}/g;
+
   $label;
   }