05
2017
10

Android移动开发-使用OpenGL来绘制3D纹理图的实现

OpenGL(全写Open Graphics Library)是指定义了一个跨编程语言、跨平台的编程接口规格的专业的图形程序接口。它用于三维图像(二维的亦可),是一个功能强大,调用方便的底层图形库。
OpenGL是行业领域中最为广泛接纳的 2D/3D 图形 API,其自诞生至今已催生了各种计算机平台及设备上的数千优秀应用程序。OpenGL是独立于视窗操作系统或其它操作系统的,亦是网络透明的。在包含CAD、内容创作、能源、娱乐、游戏开发、制造业、制药业及虚拟现实等行业领域中,OpenGL帮助程序员实现在 PC、工作站、超级计算机等硬件设备上的高性能、极具冲击力的高视觉表现力图形处理软件的开发。
虽然OpenGL本身是一种高效、简洁的开发图形库的接口,它定义了一个跨编程语言、跨平台的编程接口规范,主要用于三维图形编程。但在手机等手持终端上运行OpenGL有些不太适合,所以Android系统内置的是 OpenGL ES 的支持。

在3D绘制中进行的纹理贴图也很简单,与设置顶点颜色的步骤相似,只要三步,步骤如下:
Step1:设置启用贴图坐标数组;
Step2:设置贴图坐标的数组信息;
Step3:调用GL10的glBindTexture(int target, int texture)方法执行贴图。

下面的程序Dome示范了如何为一个立方体进行贴图,而且这个程序还提供了手势检测器,允许用户通过手势来改变该立方体的角度。

  • MainActivity.java逻辑代码如下:
package com.fukaimei.texture3d;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLSurfaceView;
import android.opengl.GLSurfaceView.Renderer;
import android.opengl.GLUtils;
import android.os.Bundle;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

public class MainActivity extends Activity implements OnGestureListener {
    // 定义旋转角度
    private float anglex = 0f;
    private float angley = 0f;
    static final float ROTATE_FACTOR = 60;
    // 定义手势检测器实例
    GestureDetector detector;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 创建一个GLSurfaceView,用于显示OpenGL绘制的图形
        GLSurfaceView glView = new GLSurfaceView(this);
        // 创建GLSurfaceView的内容绘制器
        MyRenderer myRender = new MyRenderer(this);
        // 为GLSurfaceView设置绘制器
        glView.setRenderer(myRender);
        setContentView(glView);
        // 创建手势检测器
        detector = new GestureDetector(this, this);
    }

    @Override
    public boolean onTouchEvent(MotionEvent me) {
        // 将该Activity上的触碰事件交给GestureDetector处理
        return detector.onTouchEvent(me);
    }

    @Override
    public boolean onFling(MotionEvent event1, MotionEvent event2,
                           float velocityX, float velocityY) {
        velocityX = velocityX > 2000 ? 2000 : velocityX;
        velocityX = velocityX < -2000 ? -2000 : velocityX;
        velocityY = velocityY > 2000 ? 2000 : velocityY;
        velocityY = velocityY < -2000 ? -2000 : velocityY;
        // 根据横向上的速度计算沿Y轴旋转的角度
        angley += velocityX * ROTATE_FACTOR / 4000;
        // 根据纵向上的速度计算沿X轴旋转的角度
        anglex += velocityY * ROTATE_FACTOR / 4000;
        return true;
    }

    @Override
    public boolean onDown(MotionEvent arg0) {
        return false;
    }

    @Override
    public void onLongPress(MotionEvent event) {
    }

    @Override
    public boolean onScroll(MotionEvent event1, MotionEvent event2,
                            float distanceX, float distanceY) {
        return false;
    }

    @Override
    public void onShowPress(MotionEvent event) {
    }

    @Override
    public boolean onSingleTapUp(MotionEvent event) {
        return false;
    }

    public class MyRenderer implements Renderer {
        // 立方体的顶点坐标(一共是36个顶点,组成12个三角形)
        private float[] cubeVertices = {-0.6f, -0.6f, -0.6f, -0.6f, 0.6f,
                -0.6f, 0.6f, 0.6f, -0.6f, 0.6f, 0.6f, -0.6f, 0.6f, -0.6f, -0.6f,
                -0.6f, -0.6f, -0.6f, -0.6f, -0.6f, 0.6f, 0.6f, -0.6f, 0.6f, 0.6f,
                0.6f, 0.6f, 0.6f, 0.6f, 0.6f, -0.6f, 0.6f, 0.6f, -0.6f, -0.6f,
                0.6f, -0.6f, -0.6f, -0.6f, 0.6f, -0.6f, -0.6f, 0.6f, -0.6f, 0.6f,
                0.6f, -0.6f, 0.6f, -0.6f, -0.6f, 0.6f, -0.6f, -0.6f, -0.6f, 0.6f,
                -0.6f, -0.6f, 0.6f, 0.6f, -0.6f, 0.6f, 0.6f, 0.6f, 0.6f, 0.6f,
                0.6f, 0.6f, -0.6f, 0.6f, 0.6f, -0.6f, -0.6f, 0.6f, 0.6f, -0.6f,
                -0.6f, 0.6f, -0.6f, -0.6f, 0.6f, 0.6f, -0.6f, 0.6f, 0.6f, 0.6f,
                0.6f, 0.6f, 0.6f, 0.6f, -0.6f, -0.6f, 0.6f, -0.6f, -0.6f, -0.6f,
                -0.6f, -0.6f, -0.6f, 0.6f, -0.6f, -0.6f, 0.6f, -0.6f, 0.6f, 0.6f,
                -0.6f, 0.6f, -0.6f,};
        // 定义立方体所需要的6个面(一共是12个三角形所需的顶点)
        private byte[] cubeFacets = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
                13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
                30, 31, 32, 33, 34, 35,};
        // 定义纹理贴图的坐标数据
        private float[] cubeTextures = {1.0000f, 1.0000f, 1.0000f, 0.0000f,
                0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 1.0000f, 1.0000f,
                1.0000f, 0.0000f, 1.0000f, 1.0000f, 1.0000f, 1.0000f, 0.0000f,
                1.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 1.0000f, 0.0000f,
                1.0000f, 1.0000f, 1.0000f, 1.0000f, 0.0000f, 1.0000f, 0.0000f,
                0.0000f, 0.0000f, 0.0000f, 1.0000f, 0.0000f, 1.0000f, 1.0000f,
                1.0000f, 1.0000f, 0.0000f, 1.0000f, 0.0000f, 0.0000f, 0.0000f,
                0.0000f, 1.0000f, 0.0000f, 1.0000f, 1.0000f, 1.0000f, 1.0000f,
                0.0000f, 1.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 1.0000f,
                0.0000f, 1.0000f, 1.0000f, 1.0000f, 1.0000f, 0.0000f, 1.0000f,
                0.0000f, 0.0000f, 0.0000f, 0.0000f, 1.0000f};
        private Context context;
        private FloatBuffer cubeVerticesBuffer;
        private ByteBuffer cubeFacetsBuffer;
        private FloatBuffer cubeTexturesBuffer;
        // 定义本程序所使用的纹理
        private int texture;

        public MyRenderer(Context main) {
            this.context = main;
            // 将立方体的顶点位置数据数组包装成FloatBuffer;
            cubeVerticesBuffer = floatBufferUtil(cubeVertices);
            // 将立方体的6个面(12个三角形)的数组包装成ByteBuffer
            cubeFacetsBuffer = ByteBuffer.wrap(cubeFacets);
            // 将立方体的纹理贴图的坐标数据包装成FloatBuffer
            cubeTexturesBuffer = floatBufferUtil(cubeTextures);
        }

        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            // 关闭抗抖动
            gl.glDisable(GL10.GL_DITHER);
            // 设置系统对透视进行修正
            gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST);
            gl.glClearColor(0, 0, 0, 0);
            // 设置阴影平滑模式
            gl.glShadeModel(GL10.GL_SMOOTH);
            // 启用深度测试
            gl.glEnable(GL10.GL_DEPTH_TEST);
            // 设置深度测试的类型
            gl.glDepthFunc(GL10.GL_LEQUAL);
            // 启用2D纹理贴图
            gl.glEnable(GL10.GL_TEXTURE_2D);
            // 装载纹理
            loadTexture(gl);
        }

        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            // 设置3D视窗的大小及位置
            gl.glViewport(0, 0, width, height);
            // 将当前矩阵模式设为投影矩阵
            gl.glMatrixMode(GL10.GL_PROJECTION);
            // 初始化单位矩阵
            gl.glLoadIdentity();
            // 计算透视视窗的宽度、高度比
            float ratio = (float) width / height;
            // 调用此方法设置透视视窗的空间大小。
            gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
        }

        public void onDrawFrame(GL10 gl) {
            // 清除屏幕缓存和深度缓存
            gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
            // 启用顶点坐标数据
            gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
            // 启用贴图坐标数组数据
            gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);   // ①
            // 设置当前矩阵模式为模型视图。
            gl.glMatrixMode(GL10.GL_MODELVIEW);
            gl.glLoadIdentity();
            // 把绘图中心移入屏幕2个单位
            gl.glTranslatef(0f, 0.0f, -2.0f);
            // 旋转图形
            gl.glRotatef(angley, 0, 1, 0);
            gl.glRotatef(anglex, 1, 0, 0);
            // 设置顶点的位置数据
            gl.glVertexPointer(3, GL10.GL_FLOAT, 0, cubeVerticesBuffer);
            // 设置贴图的坐标数据
            gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, cubeTexturesBuffer);  // ②
            // 执行纹理贴图
            gl.glBindTexture(GL10.GL_TEXTURE_2D, texture);  // ③
            // 按cubeFacetsBuffer指定的面绘制三角形
            gl.glDrawElements(GL10.GL_TRIANGLES, cubeFacetsBuffer.remaining(),
                    GL10.GL_UNSIGNED_BYTE, cubeFacetsBuffer);
            // 绘制结束
            gl.glFinish();
            // 禁用顶点、纹理坐标数组
            gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
            gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
            // 递增角度值以便每次以不同角度绘制
        }

        private void loadTexture(GL10 gl) {
            Bitmap bitmap = null;
            try {
                // 加载位图
                bitmap = BitmapFactory.decodeResource(context.getResources(),
                        R.drawable.sand);
                int[] textures = new int[1];
                // 指定生成N个纹理(第一个参数指定生成一个纹理)
                // textures数组将负责存储所有纹理的代号
                gl.glGenTextures(1, textures, 0);
                // 获取textures纹理数组中的第一个纹理
                texture = textures[0];
                // 通知OpenGL将texture纹理绑定到GL10.GL_TEXTURE_2D目标中
                gl.glBindTexture(GL10.GL_TEXTURE_2D, texture);
                // 设置纹理被缩小(距离视点很远时被缩小)时的滤波方式
                gl.glTexParameterf(GL10.GL_TEXTURE_2D,
                        GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
                // 设置纹理被放大(距离视点很近时被方法)时的滤波方式
                gl.glTexParameterf(GL10.GL_TEXTURE_2D,
                        GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
                // 设置在横向、纵向上都是平铺纹理
                gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
                        GL10.GL_REPEAT);
                gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
                        GL10.GL_REPEAT);
                // 加载位图生成纹理
                GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
            } finally {
                // 生成纹理之后,回收位图
                if (bitmap != null)
                    bitmap.recycle();
            }
        }
    }

    // 定义一个工具方法,将float[]数组转换为OpenGL ES所需的FloatBuffer
    private FloatBuffer floatBufferUtil(float[] arr) {
        FloatBuffer mBuffer;
        // 初始化ByteBuffer,长度为arr数组的长度*4,因为一个int占4个字节
        ByteBuffer qbb = ByteBuffer.allocateDirect(arr.length * 4);
        // 数组排列用nativeOrder
        qbb.order(ByteOrder.nativeOrder());
        mBuffer = qbb.asFloatBuffer();
        mBuffer.put(arr);
        mBuffer.position(0);
        return mBuffer;
    }
}

上面的Demo程序中标①、②、③序号的代码就完成纹理贴图的三个步骤;3D max 就不会像“复用”立方体的顶点——它是直接计算该立方体需要12个三角面,每个三角面需要的3个顶点,这样一共是36个顶点——其实有大量顶点的位置是相同,但3D max 不管这些。它认为这个立方体需要36个顶点,但个顶点的位置需要X、Y、Z三个坐标值,因此导出这个立方体的顶点坐标信息的数组就是一个长度为108的数组。

  • Demo程序运行效果界面截图如下(用户可以通过手势来旋转该立方体):
    这里写图片描述这里写图片描述这里写图片描述这里写图片描述

Demo程序源码下载地址

上一篇:Android沉浸式状态栏(一) 下一篇:Android移动开发-使用多线程进行网络聊天室通信的实现