最近在学习openGL,就找了几个相关的开源项目,一边理解,一边记录~ 这篇文章要介绍的项目来自久负盛名的yalantis
阅读此文需要一点OpenGL基础,比如纹理坐标。
首先简要翻译一下官方原理介绍:
<星战: 原力觉醒> 如何在安卓中粉碎视图
首先,我们面临两个挑战:View粉碎和斗转星移的背景。我有好几个有趣的方案来实现它们。
如何粉碎View
当原力击中View时,View被粉碎成了4000块。这告诉我们两点:1. 原力很强大 2. 如果用Canvas来生成这些碎片,恐怕性能上不行。
所以我选择强大的OpenGL。首先,我需要对要击碎的View截屏,将其纹理传输到openGL的内存中。然后去渲染碎片效果。下面是具体的步骤:
- 截屏。就是通俗的做法。
Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888)
Canvas canvas = new Canvas(bitmap);
super.draw(canvas);
- 将纹理传输到openGL内存。
- 将图片转化成碎片。
虽然OpenGL3.1的Android Extension Pack有一个tessellation shader可以轻松实现我们的需求:将一个平面转化成大量的三角形图元。而且OpenGL3.1 Android Extension Pack 允许我们在GPU上产生顶点数据,而不是仅仅在CPU上。
但是! 考虑到OpenGL2.0的市场占有率还不低,我还是选择hard 模式吧。
如何把一个View碎成4000块?我们可以挨个切下每一块碎片!当然,我只是开个玩笑。如果我们生成了上千个纹理,大概手机都要融化了。相反,我们将使用一个大的BitMap纹理,并且对每一个顶点设置纹理坐标(UV坐标)。
final float stepX = 1f / mStarWarsRenderer.sizeX;
final float stepY = 1f / mStarWarsRenderer.sizeY;
sizeX指的是X轴上的碎片数目
stepY指的是Y轴上的碎片数目
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擅长异步计算. 所有的坐标计算我都放在顶点着色器里了. 我只需要一个变量来产生动画,这个变量通过 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();
最后, 在顶点着色器里,我们将这个值传给碎片:
vec4 pos = a_Position;
pos.y += u_DeltaPos;
gl_Position = u_MVPMatrix * calcPos;
如何斗转星移
可以通过粒子效果库Leonids library画星星。此库用的是Canvas,上手也比较容易。然而,粒子数较多时,性能仍然是个问题,特别在旧手机上。
考虑到性能,我采用了跟碎片效果类似的方案,并且在顶点着色器中实现斗转星移的效果。
用纹理来画星星当然很容易,也可以用片段着色器附加一些使用技巧实现:使用公式来渲染星星。
// Render a star
float color = smoothstep(1.0, 0.0, length(v_TexCoordinate - vec2(0.5)) / v_Radius);
gl_FragColor = vec4(color);
后一种方法不仅能达成效果,还能增加30%的帧率。在大多数情况下,后一种方法渲染都比前一种方法快。
我采用了后一种方法,并且在我的旧手机Nexus 4上渲染100 000颗星星,仍有60 FPS(16ms)的帧率,很不错。
接下来回对OpenGL相关的点做一些解析。
分析开源项目,找准切入点很重要。我主要是想看怎么用OpenGl实现碎片效果,所以就先分析StarWarsRenderer这个类。
我写了些注释,来方便理解代码:
public class StarWarsRenderer implements
GLSurfaceView.Renderer {
//略去成员变量声明
//构造函数 声明配置和监听器
public StarWarsRenderer(StarWarsTilesGLSurfaceView glSurfaceView,TilesFrameLayout TilesFrameLayout, int animationDuration, int numberOfTilesX) {
mGlSurfaceView = glSurfaceView;
mListener = TilesFrameLayout;
mAnimationDuration = animationDuration;
mNumberOfTilesX = numberOfTilesX;
}
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
// 常规的清屏操作
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
// Use culling to remove back faces.
//开启剔除操作 默认剔除背面,如果要剔除正面:glCullFace(GL_FRONT)
GLES20.glEnable(GLES20.GL_CULL_FACE);
//顺时针表示正面 这样如果顶点数组是按顺时针排列的,就是告诉OpenGL是在绘制正面,反之就是背面。
GLES20.glFrontFace(GLES20.GL_CW);
// Enable depth testing
//开启深度检测,这样后面被挡住的部分(按Z值区分前后)就不会被绘制,更多请参考glPolygonOffest函数
GLES20.glEnable(GLES20.GL_DEPTH_TEST);
//从这一行一直到Matrix.setLookAtM 都是在设置照相机(观察者)的位置和视角,默认的是照相机正立放在原点,
// 相机顶部朝Y轴,可以发现下面的代码就是默认设置,所以删掉也不影响。
//详细介绍:http://blog.csdn.net/kkae8643150/article/details/52805738
//http://www.cnblogs.com/kesalin/archive/2012/12/06/3D_math.html
// Position the eye in front of the origin.
final float eyeX = 0.0f;
final float eyeY = 0.0f;
final float eyeZ = 0.0f;
// We are looking toward the distance
final float lookX = 0.0f;
final float lookY = 0.0f;
final float lookZ = 1.0f;
// Set our up vector. This is where our head would be pointing were we holding the camera.
final float upX = 0.0f;
final float upY = 1.0f;
final float upZ = 0.0f;
Matrix.setLookAtM(mViewMatrix, 0, eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ);
// 加载着色器和项目(program)
final String vertexShader = RawResourceReader.readTextFileFromRawResource(mGlSurfaceView.getContext(), R.raw.tiles_vert);
final String fragmentShader = RawResourceReader.readTextFileFromRawResource(mGlSurfaceView.getContext(), R.raw.tiles_frag);
final int vertexShaderHandle = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertexShader);
final int fragmentShaderHandle = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShader);
programHandle = ShaderHelper.createAndLinkProgram(vertexShaderHandle, fragmentShaderHandle,
new String[]{"a_Position", "a_Normal", "a_TexCoordinate"});
// Initialize the accumulated rotation matrix
Matrix.setIdentityM(mAccumulatedRotation, 0);
}
private void genTilesData() {
//生成碎片的顶点坐标等值, 生成算法不是我们的重点,先略过
Executors.newSingleThreadExecutor().submit(new GenerateVerticesData(this));
}
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
sizeX = mNumberOfTilesX;
sizeY = height * sizeX / width;
// Set the OpenGL viewport to the same size as the surface.
GLES20.glViewport(0, 0, width, height);
// Create a new perspective projection matrix. The height will stay the same
// while the width will vary as per aspect ratio.
final float ratio = (float) width / height;
final float left = -ratio;
final float right = ratio;
final float bottom = -1.0f;
final float top = 1.0f;
final float near = 1.0f;
final float far = 10.0f;
this.ratio = ratio;
// 建议改成perspectiveM的方式,frustumM方法在某些情况下有bug
Matrix.frustumM(mProjectionMatrix, 0, left, right, bottom, top, near, far);
genTilesData();
}
@Override
public void onDrawFrame(GL10 gl10) {
logFrame();
drawGl();
if (!requestedReveal && mAndroidDataHandle > 0) {
requestedReveal = true;
mListener.reveal();
}
}
private void drawGl() {
//常规操作,绑定变量地址,有些代码冗余
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
if (mAndroidDataHandle > 0) {
GLES20.glUseProgram(programHandle);
// Set program handles
mvpMatrixHandle = GLES20.glGetUniformLocation(programHandle, "u_MVPMatrix");
mvMatrixHandle = GLES20.glGetUniformLocation(programHandle, "u_MVMatrix");
textureUniformHandle = GLES20.glGetUniformLocation(programHandle, "u_Texture");
deltaPosHandle = GLES20.glGetUniformLocation(programHandle, "u_DeltaPos");
positionHandle = GLES20.glGetAttribLocation(programHandle, "a_Position");
normalHandle = GLES20.glGetAttribLocation(programHandle, "a_Normal");
textureCoordinateHandle = GLES20.glGetAttribLocation(programHandle, "a_TexCoordinate");
tileXyHandle = GLES20.glGetAttribLocation(programHandle, "a_TileXY");
Matrix.setIdentityM(mModelMatrix, 0);
Matrix.translateM(mModelMatrix, 0, 0.0f, 0.0f, PLANE_HEIGHT);
// Set a matrix that contains the current rotation.
Matrix.setIdentityM(mCurrentRotation, 0);
Matrix.multiplyMM(mTemporaryMatrix, 0, mCurrentRotation, 0, mAccumulatedRotation, 0);
System.arraycopy(mTemporaryMatrix, 0, mAccumulatedRotation, 0, 16);
Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0);
// Pass in the modelview matrix.
GLES20.glUniformMatrix4fv(mvMatrixHandle, 1, false, mMVPMatrix, 0);
Matrix.multiplyMM(mTemporaryMatrix, 0, mProjectionMatrix, 0, mMVPMatrix, 0);
System.arraycopy(mTemporaryMatrix, 0, mMVPMatrix, 0, 16);
// Pass in the combined matrix.
GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, mMVPMatrix, 0);
// Pass in u_Gravity
GLES20.glUniform1f(deltaPosHandle, deltaPosX);
// Pass in the texture information
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
// Bind the texture to this unit.
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mAndroidDataHandle);
GLES20.glUniform1i(textureUniformHandle, 0);
if (mPlane != null) {
mPlane.render();
}
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
}
}
public int getTilesCount() {
return sizeX * sizeY;
}
public void logFrame() {
frames++;
timePassed = (System.nanoTime() - startTime) / 1_000_000;
if(timePassed >= 1000) {
Timber.d("%d tiles @ %d fps", getTilesCount(), frames);
frames = 0;
startTime = System.nanoTime();
}
}
public void startAnimation() {
//动画改变的是tiles_vert.glsl里的u_DeltaPos值
//动画结束时,u_DeltaPos = deltaPosX = -10。
//而tiles_vert.glsl里gl_Position.w = 5, 所以x y的可见取值范围是-5~5
//这也是GenerateVerticesData产生的坐标的取值范围。
//所以动画结束时,gl_Position的Y值最大为5-10 = -5,
//也就是最上方的点,被映射到屏幕底部了。
//有些人会问,为什么gl_Position.w = 5,这个是由
//Matrix.translateM(mModelMatrix, 0, 0.0f, 0.0f, 5f);确定的。
// 因为这个5f会在后面的矩阵运算中参与gl_Position.w的生成。
//所以这个值要和PLANE_HEIGHT一致。
//作者为什么不直接用PLANE_HEIGHT代替哇,哭.jpg.
// ps:发现PLANE_HEIGHT设置为1.0 10.0都没问题,设置成20.0就有问题。
//然鹅没必要探究,设置成1,与归一化坐标范围一致,最简单。
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
animator = ValueAnimator.ofFloat(0, -PLANE_HEIGHT * 2); // plane height
animator.setDuration(mAnimationDuration);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
deltaPosX = value;
mGlSurfaceView.requestRender();
}
});
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mGlSurfaceView.requestRender();
}
@Override
public void onAnimationEnd(Animator animation) {
mListener.onAnimationFinished();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animator.start();
}
});
}
public void updateTexture(final Bitmap bitmap) {
mGlSurfaceView.queueEvent(new Runnable() {
@Override
public void run() {
requestedReveal = false;
mAndroidDataHandle = TextureHelper.loadTexture(bitmap);
mGlSurfaceView.requestRender();
}
});
}
public void cancelAnimation() {
if (animator != null && animator.isRunning()) {
animator.removeAllListeners();
animator.cancel();
}
}
}
网友评论