星球大戰:論在安卓上如何把view弄成碎片
英文原文:Star Wars: The Force Awakens Or How to Crumble View Into Tiny Pieces on Android。
上個月我們發布了ios上的史詩級動畫效果星球大戰,而你肯定也知道我們還會為安卓用戶做一次同樣的事情。備受矚目的星球大戰的現在有了安卓開發者版本 ,非常榮幸能跟你分享我們的開發機密。
首先,在我們的星球 大戰動畫中,有兩個具有挑戰性的地方:視圖破碎成小塊以及飛舞的星場。實現過程中有許多有趣的事情。
如何把view分裂成小的碎片
當我們星球大戰動畫中的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 重寫這些混亂的邏輯將是非常有意思的事情。
在這里查看動畫:
推薦閱讀:
來自: http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/1221/3793.html