英文原文: Star Wars: The Force Awakens Or How to Crumble View Into Tiny Pieces on Android 。
上个月我们发布了ios上的史诗级动画效果星球大战,而你肯定也知道我们还会为安卓用户做一次同样的事情。备受瞩目的星球大战的现在有了 安卓开发者版本 ,非常荣幸能跟你分享我们的开发机密。
首先,在我们的星球 大战动画中,有两个具有挑战性的地方:视图破碎成小块以及飞舞的星场。实现过程中有许多有趣的事情。
当我们星球大战动画中的view被外力点击之后,分裂成4000块碎片。这意味着两件事:1)外力确实非常大,2)在安卓上使用Canvas来创建如此复杂的图像应该会很慢,因为Canvas性能不佳。
但是OpenGL则完全相反,它要强大得多。而且在 iOS版的星球大战动画 中我们也是使用的OpenGL,UIDynamics和UIKit(Core Animation)无法应付起码的负荷。
鉴于动画的复杂性,我决定用OpenGL。
为了把安卓视图分裂成小块,我们需要截屏整个视图,把一个texture搬进OpenGL内存,并只渲染碎片的效果。让我们看看是如何做的:
1. 对view截图,这很简单:
Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888) Canvas canvas = new Canvas(bitmap); super.draw(canvas);
2.把一个texture搬进OpenGL内存。
3. 把bitmap分裂成碎片。
Android的扩展包,OpenGL ES 3.1的一个超集中有一个 tessellation shader就是完全做这个事情:它把一个平面分裂成许多三角形。而且跟OpenGL ES 2.0不一样的是,2.0里vertex data只能在cpu上产生,而有了扩展包我们可以在GPU上产生这些数据。
但不幸的是,OpenGL ES 3.1 和 AEP(即Android的扩展包的缩写)并不被大多数安卓设备支持。鉴于 56%的安卓设备上都是用的OpenGL ES 2.0,我们决定选择更为艰难的方式。
那么我们该如何把一个视图分裂成4000块呢?我们可以把一张图片一块块的切割!当然我是在开玩笑。如果我们创建了上千个新的texture,估计设备直接在手里化了。我们将使用一张大的 bitmap texture并 分配 UV (texture coordinates)到每个vertex :
final float stepX = 1f / mStarWarsRenderer.sizeX; final float stepY = 1f / mStarWarsRenderer.sizeY;
SizeX - 横向碎片的数目
SizeY - 纵向碎片的数目
for (int x = 0; x < mStarWarsRenderer.sizeX; x++) { for (int y = 0; y < mStarWarsRenderer.sizeY; y++) { final float u0 = x * stepX; final float v0 = y * stepY; final float u1 = u0 + stepX; final float v1 = v0 + stepY; // push values to buffer } }
我们希望尽可能把更多的计算移到GPU上,因为GPU擅长多个并行的计算。我对vertex shader中的所有位置(position)都做计算。用Android interpolator来动画它只需要一个变量:
// from 0 to plane height in OpenGL coordinates animator = ValueAnimator.ofFloat(0, -Const.PLANE_HEIGHT * 2); animator.setDuration(mAnimationDuration); animator.setInterpolator(new DecelerateInterpolator(1.3f)); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float value = (float) animation.getAnimatedValue(); mDeltaPosX = value; mGlSurfaceView.requestRender(); } }; animator.start();
最后,在vertex shader中我们应该把这个值添加到碎片位置:
vec4 pos = a_Position; pos.y += u_DeltaPos; gl_Position = u_MVPMatrix * calcPos;
星球大战动画的另一个部分是飞舞的星场。对于星星的绘制,我考虑过使用 Leonids 库。它使用的是标准的 Android Canva,使用起来很方便。但是让许多星星动画难免会导致性能问题,因此我们的动画就会显得不太负责任,尤其对于老的智能机来说。这就是我们用Leonids库测试动画时所得到的:
[绿线代表 60 FPS (16ms).。为了避免动画不流畅,我们不能越过这条线]
考虑到性能问题,我决定采取一种类似对碎片的处理方法,在 vertex shader中执行星星运动的动画。
我发现一个星星的texture非常简单,可以用shader的技巧来替代- 使用一个公式来渲染星星对象:
// Render a star float color = smoothstep(1.0, 0.0, length(v_TexCoordinate - vec2(0.5)) / v_Radius); gl_FragColor = vec4(color);
就如你看到的,我们得到了和使用图片一样的效果。这让我们每秒多了30%的帧。这个方案虽然不是每次都比texture lookup (比如使用一个image) 快,但多数情况下是。唯一办法是去测量。
左边是pngtexture
右边是产生的图片
当我在我用了三年的nexus4上测试这个方案的时候,我可以在60 FPS的情况下渲染100 000颗星星。
1.把fragment或者activity的主视图包裹在TilesFrameLayout中:
<com.yalantis.starwars.TilesFrameLayout android:id="@+id/tiles_frame_layout" android:layout_height="match_parent" android:layout_width="match_parent" app:sw_animationDuration="1500" app:sw_numberOfTilesX="35"> <!-- Your views go here --> </com.yalantis.starwars.TilesFrameLayout>
2.用这些属性调整动画:
app:sw_animationDuration – 持续时间 毫秒
app:sw_numberOfTilesX – the number of square tiles the plane is tessellated into broadwise
mTilesFrameLayout = (TilesFrameLayout) findViewById(R.id.tiles_frame); mTilesFrameLayout.setOnAnimationFinishedListener(this);
3.在fragment或者activity的onPause() 和 onResume()中,记得调用相应的方法:
@Override public void onResume() { super.onResume(); mTilesFrameLayout.onResume(); } @Override public void onPause() { super.onPause(); mTilesFrameLayout.onPause(); }
要开始动画,只需调用:
mTilesFrameLayout.startAnimation();
当动画结束的时候,将调用回调函数:
@Override public void onAnimationFinished() { // Hide or remove your view/fragment/activity here }
以上就是全部!
如果能在安卓扩展包更普及的时候用tessellation shaders 重写这些混乱的逻辑将是非常有意思的事情。
在这里查看动画: