查看原文
其他

QQ 25年技术巡礼丨技术探索下的清新设计,打造轻盈简约的QQ9

QQ技术团队 腾讯云开发者 2024-05-24

👉目录

1 极光动画

2 从灰阶图到多彩图

3 性能优化

4 灵动的 3D 企鹅

5 展望




1999 年 2 月 10 日,QQ 首个版本发布。2024 年是 QQ 25 周年,这款承载几代人回忆的互联网产品仍旧没有停止自我转型的创新脚步。在技术方面,QQ 近期完成了再造底层架构的 NT(New Tech)项目,在手机 QQ 9 上,也发布了全新升级的视觉和体验设计。

最新发布的手机 QQ 9.0 界面轻盈换新,简洁纯粹,氛围轻松,上线后收获了许多网友的好评。腾讯云开发者社区联手 QQ 技术团队,撰写了本篇文章,向大家介绍其中像极光一样灵动的动效,和如弹簧一般可以自由拨动的3D企鹅的技术实现,以及对于视觉打磨和性能优化背后的故事。QQ 25周年技术巡礼系列文章陆续产出中,请大家持续关注腾讯云开发者公众号。





01



极光动画

一天,设计师同学找到开发同学说想要做一种新的动画效果,能够像极光或者流水一样自由的变幻,看上去非常酷炫的一种效果,同时能够适配 QQ 的日间和夜间主题。

经过多次与设计同学的沟通与打磨,最终呈现的效果如下:


   1.1 破题


如何破题,还得从大自然寻找灵感,在自然界有许多形象而生动的场景,例如飘浮的云朵、起伏的山川、延绵的水纹等等,这些场景看似没有规律,但又富有线性艺术之美。


这些图形都有一些共同的特性:随机、连续、平滑和自然,而在计算机图形学中用于模拟这些内容就是噪音(Noise)。

   1.2 随机性


最简单的随机图像,就是模拟一台老式的黑白电视机,而生成的白噪图,这种算法也非常简单,只需要在每个像素点随机生成一个单通道的灰度颜色就可以了。

(白噪图)

然而这虽然模拟了随机性,但每个相邻像素之间缺少内在联系,也就是上面提到的,缺少连续性。

   1.3 连续性


要让每个像素点产生联系,形成连续效应,可以通过插值计算来实现。我们可以把图像分成若干个网格,然后在每个网格内进行顶点插值计算,如下图所示,在二维图形中,每个网格会有4个顶点(同理,如果是在三维图形中,就会有8个顶点)。生成的噪音图形如下右图所示。


   1.4 平滑性


上面的图像已经成功的解决了“随机”和“连续”这两个问题,但却存在线条化和网格化的问题,为此我们要想办法消除这种现象,而我们常用的手段就是平滑处理。


在 Shader 中常用的平滑函数公式如下:

f(x) = 3x^2-2x^3

当然还有很多其它的平滑公式,在图形处理中平滑是非常关键也非常常用的手段,例如下面是一种改进后的平滑公式,相比于上面的平滑公式,会保留更多的线条细节。



f(x) = 6x^5-15x^4+10x^3

平滑后的效果如下,看起来的确是要更加自然了,而这种通过插值计算的噪音算法,通常被称为 value 噪音。下面的噪音分别是用两种不同的平滑公式优化后的效果。


   1.5 自然性


上面的噪音算法已经满足了大部分的预设目标了,但仔细观察后,黑色的条状形还是显得有些生硬,明显有人为痕迹,不够自然。

我们又进一步调研了一下目前主流的一些噪音算法,下面列举了一些常见的噪音效果,当然还有更多的噪音,这里就不一一列举了。


Perlin 噪音相对会更加贴近我们想要的效果,Perlin 噪音在宏观视角上也是重复的,但当把噪音的频率适当调整后,在微观的视角上就会呈现出一种随机而又自然的效果。


关于 Perlin 噪音,我们使用的是其改进后的算法,在二维图形中,噪音由原来4个顶点的插值计算下降到了3个顶点的插值计算,以达到减少运算量的效果。在三维图形中由8个顶点降到了4个顶点,运算量减少了一半。





02



从灰阶图到多彩图

上面我们已经介绍了如何利用噪音算法模拟那些看似随机但又显得十分自然的图像,下面内容将继续为大家介绍如何将噪音变成极光动画的。

现在我们已经有了一张单通道的灰度图片,如何把灰度图转化成一张多彩图呢?


   2.1 灰阶映射


我们首先想到的是预设一组颜色列表,如下图所示,现从把灰度值从白到黑映射成对应的颜色。(试想一下,如果我们从一些封面图片提取颜色列表,是不是可以生成更多有趣的极光动画。)


显然,这样简单的映射是远远不够的,不同的色块之间还需要平滑的过渡,为此我们想到的是利用渐变效果,让不同的色块都有效的衔接起来。

// 获取噪音灰度值float gray = noise(x, y);// 颜色区间大小float rangeSize = 1 / colorCount;// 对应的颜色索引int colorIndex = ceil(gray / rangeSize);// 把gray值转换成两个不同颜色的区间值gray -= rangeSize * colorIndex;gray /= rangeSize;// 颜色插值计算mix(color1, color2, gray);

下面是优化后的效果。


   2.2 色域问题


经过上面的处理,效果看上去一切都是正常的,但如果我们仔细观察,就会发现我们的颜色似乎变少了,是的,还记得我们最初的颜色列表么,颜色 #FAE366 没有呈现出来,这是怎么回事呢?

我们统计了 1KW 次灰度值(0~1)出现的情况,发现其正好符合正态分布的情况,两端灰度值出现的概率非常少,导致在进行灰阶映射时,两端颜色没有被呈现出来。


如果直接过滤两端的数据,则会破坏连续性;如果调整映射策略,各个颜色按相同概率均匀映射,似乎也是一个不错的选择,但大家还记得前面多次提到的平滑么。


下面描述了线性映射与平滑映射的区别,例如#FAE366颜色原来的映射区间是0.85~1,改成平滑映射后,映射区间变成了0.75~1,映射范围扩大了67%。



通过平滑处理后,两端颜色可以被很好的呈现出来,两端颜色呈现的概率成几十倍的提升,足见平滑处理的重要性。


最后,让我们来看一下优化后的效果,左边是优化前的效果,右边是优化后的效果,看起来效果的确好了很多。


   2.3 亮度平滑


上面的效果虽然实现了渐变,但在视觉效果上还有改进的空间,首先是颜色过于明亮了,其次是极光效果没有与周围的背景融合成一个整体。


为此我们需要对图片亮度进行平滑处理,我们当然可以继续使用前面的平滑函数(感兴趣的同学也可以尝试更多的平滑公式,例如余弦函数、贝塞尔曲线等),从中心点向四周进行亮度的平滑渐变。最后记得选择一个合适的频率,让 Perlin 噪音看上去更加的自然。



   2.4 舞动起来


到目前为止,我们得到的还只是一张静态图片,我们需要让图像舞动起来。为此,我们需要使用到 3D Perlin 噪音,由于 Perlin 噪音在任意方向都是平滑、连续的。于是我们要做的就是定期的,朝着固定方向切割 3D 空间就可以了。  想必大家也都想到了,比较简单的方案,就是沿着 Z 轴方向切割就可以了。

另外如果想让动画变得更加自然,也可以叠加一种随机算法,例如下图中的分形布朗运动,让切割的速度忽快忽慢,产生一种更加自然的效果。


最后,让我们来看一下落地后的效果:



   2.5 二次渲染


纯原生算法实时渲染的实现方式,保证了极光动画可以在 Mac、Web 等平台快速实现,同时我们可以针对各个场景进行再次渲染,实现更加炫酷的菊花、走马灯等效果:




03



性能优化

一开始我们使用的是 CPU 进行 Perlin 算法的计算,再把得到的结果转成 Bitmap 进行渲染。由于手机中屏幕像素特别多,大屏手机动则高达 300W 以上的像素,单次计算耗时需要几秒钟。由于计算量过大,我们首先想到的是进行下采样计算,把计算的像素控制在 1W 以内,这样在 iPhone 上的耗时就降到 6ms 以内,在 Android 上也降到了 15ms 以内,但由于 CPU 的负载波动,偶尔会出现掉帧的问题。


同时我们把动画的帧率从 60FPS 降低到了 30FPS,但这样的性能损耗,对于 CPU 还是有不少的压力,特别是在一些 Android 低端机型上,压力会更大。如何进一步提升性能呢? 当然我们最容易想到的就是利用硬件加速,也就是 GPU 来提升渲染性能。

   3.1 渲染管线


iOS 和 Android 目前有三套 GPU 的框架,分别是 Metal、OpenGL、Vulkan. 而传统的渲染管线主要是利用 Vertex Shader 和 Fragment Shader。



首先,我们将之前 CPU 计算转 Bitmap 的流程进行了简单修改,通过共享内存的方式,直接使用 GPU 进行渲染。经过这样的优化后,性能得到了很大的提升。


然而,噪音部分的计算依然还是用的 CPU,这一块还存在不少的性能开销。仔细观察会发现,噪音中不同像素点的计算其实都是相互独立的,是完全可以利用 GPU 进行高并发计算的。


   3.2 Compute Shader


随着最近 AI 模型大量使用 GPU 进行训练,GPU 也提供了 Compute Shader 的能力,允许进行高并发的复杂逻辑运算任务,这被称之为 GPGPU(Generic-Purpose GPU)。其特点是同时开辟多个线程组,每个线程组又有多个线程同时进行高并发运算。


这里要注意的是在一些低端机型中,只支持均匀线程组,如下左图所示,但由于要处理的任务并不刚好其整数倍,难免会有一些边界溢出的问题,因此需要我们特别留意,并做好保护措施。



同时,为了最大程度发挥 Compute Shader 的性能,我们需要优化计算量,在移动端,通常情况下 GPU 一次 Dispatch 会调用32个线程,所以,numThreads 的乘积最好是这个值的整数倍。在Metal中可以直接获取这个数值来进行性能优化并调整计算量。


同时,在 Android 进行数据读取时,要留意数据传输大端序模式与小端序模式的问题。避免陷入模式的困境,而造成不必要的性能开销。


基于这些理论知识,我们把噪音部分的计算转成了 Compute Shader 计算后,性能提升了几十倍,安卓和 iOS 最终都可以在1到2毫秒完成渲染。


   3.3 兼容性


在 OpenGL ES 3.1 版本中就可以使用 Compute Shader 了,而 Metal 很早就支持了,目前 iOS 全部机型都支持了 Compute Shader,但是也要注意 Metal 3.0 要求 iOS16 以及 iPhone 11 以上的机型,因此可以按照需要兼容的设备和系统选择合适的 Metal 版本。


Android 的兼容率也在97%以上,随着时间的推移,兼容率也会上升,针对 GPU 渲染失败的用户,将降级到异步 CPU 渲染,即可保证所有用户都可以体验。


此外,我们还遇到了一些其它兼容问题,例如下图中的白屏和花屏问题。



其原因是在 Android 部分机型中,会使用 CPU Cache,每次调用 Compute Shader 计算后,需要调用 glMapBufferRange 来同步数据。在 iOS 中要注意均匀线程组的边界溢出造成的 GPU 异常问题。


性能与兼容性总结:



CPU增量
内存增量
兼容率
Android
4.23%
19.9M
96%
iOS
4.17%
6M
100%

Android 兼容问题集中在一些 Android6.0 的低端设备上面,iOS 对于 Compute Shader 是全面兼容的。



04



灵动的 3D 企鹅

为了突出 QQ 的品牌,我们在9.0版本中增加了 3D 企鹅的形象,考虑到跨平台的一致体验,使用了 filament 引擎(https://github.com/google/filament),该引擎是 Google 开源的一款移动端轻量级的引擎,相比 UE4,体量更小,加载速度更快,几乎感觉不到加载时间。


fialment 引擎目前支持 glb 和 gltf 两种格式的模型,里面包含了摄像机、纹理、皮肤、模型顶点数据、材质、光照、顶点数据、动画、骨骼等。下面是详细的介绍:



   4.1 光线问题


下图中,左侧是设计图,右侧是渲染图,可以看出来,两者存在明显的差异,原因是在不同的 3D 引擎中,实际渲染出来的效果本身就存在差异的。原因有很多,其中很关键的一部分,是不同引擎对于光线的支持能力存在差异。



例如下面是 filament 引擎支持的光源类型。


enum class Type : uint8_t { SUN, // 太阳光 DIRECTIONAL, // 指向光 POINT, // 点光源 FOCUSED_SPOT, // 聚光灯 SPOT, // 斑点光源};

为了避免这种差异的产生,我们可以利用一些开源工具先用 filament 引擎实际渲染一下,看一下效果。例如我们可以在 VS Code 中安装 gltf 的插件,选择 filament 引擎进行渲染。

   4.2 材质问题


为了营造更加酷炫的 3D 企鹅效果,我们想到把极光映射在 3D 企鹅上面,形成一种玻璃质感的效果。然而 3D 材质里面并没有一种材质叫玻璃材质。

首先我们利用了 emissiveFactor 特性,让模型本身发出一种淡蓝色的光,再结合 PBR(Physically Based Rendering)材质的物理特性。 其中有两个重要的参数 Metallic 和 Roughness,Metallic 参数指定材质是金属还是非金属,Roughness 参数则控制着表面的粗糙程度,这样的确有了一点类似玻璃质感。


更多材质可以参考 Filament 材质说明:https://jerkwin.github.io/filamentcn/Materials.md.html

但上面的效果依然不是特别满意,于是我们再次改进了策略,我们给 3D 模型增加了一些透明度,同时把前后光源的强度设置成一样的强度。




除了上面的策略,还可以利用天空盒方案,把光源映射到 3D 企鹅上面,但这样的话要修改反射率甚至专门编写 Shader。而另一种思路是把极光看做是一种光,通过后处理的办法,进行光线混色处理。




   4.3 弹簧问题


为了在拨动 3D 企鹅时,呈现一种弹性回弹的效果,最容易想到的是系统的弹簧动画 SpringAnimation。


但 filament 引擎并非是 UI 控件,无法直接使用。虽然也可以通过一些巧妙的方法间接的使用系统的弹簧动画,来作用到 filament 引擎上面,但这些手段并不是非常推荐。

其实我们也可以直接利用弹簧公式,动态收敛旋转速度,在此过程中,我们可以把速度转化为旋转的角度,来模拟弹性回弹的效果。


f = -k·x


   4.4 优化弹簧效果


一种更加贴近现实的弹簧效果是 阻尼谐振子模型(Damped Harmonic Oscillator),运动不仅受到弹性系数的影响,还受到与速度成正比的阻尼力的影响。一个典型的阻尼谐振子系统可以用以下的二阶微分方程来描述:

m * d²x/dt² + c * dx/dt + k * x = 0

其中:
  • m 是系统的质量。
  • c 是阻尼系数,它决定了阻尼力的大小。
  • k 是弹簧的刚度系数,它决定了恢复力的大小。
  • x 是系统的位移。
  • dx/dt 是系统的速度。
  • d²x/dt² 是系统的加速度。

根据阻尼系数 c 的不同,阻尼谐振子可以分为三种情况:
  1. 欠阻尼(Underdamped):当 c² < 4mk 时,系统会发生振荡,但振幅随时间逐渐减小,直到最终停止。这是最常见的情况,通常在动画中模拟弹簧时会使用这种情况。
  2. 临界阻尼(Critically Damped):当 c² = 4mk 时,系统会以最快的速度返回到平衡位置,而不会发生振荡。这种情况通常用于需要快速稳定的系统,如汽车悬挂系统。
  3. 过阻尼(Overdamped):当 c² > 4mk 时,系统返回到平衡位置的速度会比临界阻尼慢,且不会发生振荡。这种情况在需要非常平稳、没有振荡的系统中使用。

(三种不同阻尼的效果)

利用欠阻尼的效果,让速度进行快速收敛。



这里我们最终优化后的效果。


   4.5 一个彩蛋


最后再补充一点,在手 Q 的关于页,通过旋转企鹅三圈,会触发一个彩蛋哦,欢迎大家来试试。



05



展望

目前极光使用的颜色列表,依赖极光编辑器来完成,如果利用 HCT 的色彩原理,通过指定品牌色可以衍生出几个相近的颜色,就可以为不同的主题、背景定制不同的极光效果,生成更加丰富的效果。


-End-
原创作者 | 袁智、赵松、苏海涛

 


那些年发布过的手机 QQ 版本,你印象最深的功能和故事是什么?欢迎评论留言。我们将选取1则优质的评论,送出腾讯Q哥公仔1个(见下图)。3月21日中午12点开奖。


📢📢欢迎加入腾讯云开发者社群,享前沿资讯、大咖干货,找兴趣搭子,交同城好友,更有鹅厂招聘机会、限量周边好礼等你来~


(长按图片立即扫码)








继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存