美文网首页opengl
Android OpenGL显示3D模型文件

Android OpenGL显示3D模型文件

作者: CHSmile | 来源:发表于2017-10-27 14:47 被阅读74次

    1 STL文件

    它是标准的3D文件格式,一般3D打印机都是支持打印STL文件,关于STL文件的格式、以及相关介绍请参考百度百科:【stl格式】。当然了,我在代码的注释中也会进行相关解释。

    1.1 解析准备

    首先,在解析STL文件格式之前,我们需要进行构思。我们无非就是把STL文件中的三角形的顶点信息提取出来。因此我们的主要目标就是把所有点信息读取出来。
    但是,3D模型的坐标位置很随机,大小也随机。而不同的模型所处的位置不同,为了能够让模型处于手机显示中心,我们必须对模型进行移动、放缩处理。使得任意大小、任意位置的模型都能在我们的GLSurfaceView中以“相同”的大小显示。
    因此,我们不仅仅要读取顶点信息,而且还要获取模型的边界信息。我们想象成一个立方体,这个立方体刚好包裹住模型。即我们要读取x、y、z三个方向上的最大值最小值。

    1.2 开始解析

    首先,我们定义一个Model类,用于表示一个模型对象:

    public class Model {
        //三角面个数
        private int facetCount;
        //顶点坐标数组
        private float[] verts;
        //每个顶点对应的法向量数组
        private float[] vnorms;
        //每个三角面的属性信息
        private short[] remarks;
    
        //顶点数组转换而来的Buffer
        private FloatBuffer vertBuffer;
    
        //每个顶点对应的法向量转换而来的Buffer
        private FloatBuffer vnormBuffer;
        //以下分别保存所有点在x,y,z方向上的最大值、最小值
        float maxX;
        float minX;
        float maxY;
        float minY;
        float maxZ;
        float minZ;
    
        //返回模型的中心点
         //注意,下载的源码中,此函数修改修正如下
        public Point getCentrePoint() {
    
            float cx = minX + (maxX - minX) / 2;
            float cy = minY + (maxY - minY) / 2;
            float cz = minZ + (maxZ - minZ) / 2;
            return new Point(cx, cy, cz);
        }
    
        //包裹模型的最大半径
        public float getR() {
            float dx = (maxX - minX);
            float dy = (maxY - minY);
            float dz = (maxZ - minZ);
            float max = dx;
            if (dy > max)
                max = dy;
            if (dz > max)
                max = dz;
            return max;
        }
    
        //设置顶点数组的同时,设置对应的Buffer
        public void setVerts(float[] verts) {
            this.verts = verts;
            vertBuffer = Util.floatToBuffer(verts);
        }
    
        //设置顶点数组法向量的同时,设置对应的Buffer
        public void setVnorms(float[] vnorms) {
            this.vnorms = vnorms;
            vnormBuffer = Util.floatToBuffer(vnorms);
        }
    
       //···
       //其他属性对应的setter、getter函数
       //···
    
    }
    

    接下来就是将stl文件转换成Model对象,我们定义一个STLReader类:

    public class STLReader {
        private StlLoadListener stlLoadListener;
    
        public Model parserBinStlInSDCard(String path)
                                 throws IOException {
    
            File file = new File(path);
            FileInputStream fis = new FileInputStream(file);
            return parserBinStl(fis);
        }
    
        public Model parserBinStlInAssets(Context context, String fileName) 
                                throws IOException {
    
            InputStream is = context.getAssets().open(fileName);
            return parserBinStl(is);
        }
        //解析二进制的Stl文件
        public Model parserBinStl(InputStream in) throws IOException {
            if (stlLoadListener != null)
                stlLoadListener.onstart();
            Model model = new Model();
            //前面80字节是文件头,用于存贮文件名;
            in.skip(80);
    
            //紧接着用 4 个字节的整数来描述模型的三角面片个数
            byte[] bytes = new byte[4];
            in.read(bytes);// 读取三角面片个数
            int facetCount = Util.byte4ToInt(bytes, 0);
            model.setFacetCount(facetCount);
            if (facetCount == 0) {
                in.close();
                return model;
            }
    
            // 每个三角面片占用固定的50个字节
            byte[] facetBytes = new byte[50 * facetCount];
            // 将所有的三角面片读取到字节数组
            in.read(facetBytes);
            //数据读取完毕后,可以把输入流关闭
            in.close();
    
    
            parseModel(model, facetBytes);
    
    
            if (stlLoadListener != null)
                stlLoadListener.onFinished();
            return model;
        }
    
        /**
         * 解析模型数据,包括顶点数据、法向量数据、所占空间范围等
         */
        private void parseModel(Model model, byte[] facetBytes) {
            int facetCount = model.getFacetCount();
            /**
             *  每个三角面片占用固定的50个字节,50字节当中:
             *  三角片的法向量:(1个向量相当于一个点)*(3维/点)*(4字节浮点数/维)=12字节
             *  三角片的三个点坐标:(3个点)*(3维/点)*(4字节浮点数/维)=36字节
             *  最后2个字节用来描述三角面片的属性信息
             * **/
            // 保存所有顶点坐标信息,一个三角形3个顶点,一个顶点3个坐标轴
            float[] verts = new float[facetCount * 3 * 3];
            // 保存所有三角面对应的法向量位置,
            // 一个三角面对应一个法向量,一个法向量有3个点
            // 而绘制模型时,是针对需要每个顶点对应的法向量,因此存储长度需要*3
            // 又同一个三角面的三个顶点的法向量是相同的,
            // 因此后面写入法向量数据的时候,只需连续写入3个相同的法向量即可
            float[] vnorms = new float[facetCount * 3 * 3];
            //保存所有三角面的属性信息
            short[] remarks = new short[facetCount];
    
            int stlOffset = 0;
            try {
                for (int i = 0; i < facetCount; i++) {
                    if (stlLoadListener != null) {
                        stlLoadListener.onLoading(i, facetCount);
                    }
                    for (int j = 0; j < 4; j++) {
                        float x = Util.byte4ToFloat(facetBytes, stlOffset);
                        float y = Util.byte4ToFloat(facetBytes, stlOffset + 4);
                        float z = Util.byte4ToFloat(facetBytes, stlOffset + 8);
                        stlOffset += 12;
    
                        if (j == 0) {//法向量 
                            vnorms[i * 9] = x;
                            vnorms[i * 9 + 1] = y;
                            vnorms[i * 9 + 2] = z;
                            vnorms[i * 9 + 3] = x;
                            vnorms[i * 9 + 4] = y;
                            vnorms[i * 9 + 5] = z;
                            vnorms[i * 9 + 6] = x;
                            vnorms[i * 9 + 7] = y;
                            vnorms[i * 9 + 8] = z;
                        } else {//三个顶点
                            verts[i * 9 + (j - 1) * 3] = x;
                            verts[i * 9 + (j - 1) * 3 + 1] = y;
                            verts[i * 9 + (j - 1) * 3 + 2] = z;
    
                            //记录模型中三个坐标轴方向的最大最小值
                            if (i == 0 && j == 1) {
                                model.minX = model.maxX = x;
                                model.minY = model.maxY = y;
                                model.minZ = model.maxZ = z;
                            } else {
                                model.minX = Math.min(model.minX, x);
                                model.minY = Math.min(model.minY, y);
                                model.minZ = Math.min(model.minZ, z);
                                model.maxX = Math.max(model.maxX, x);
                                model.maxY = Math.max(model.maxY, y);
                                model.maxZ = Math.max(model.maxZ, z);
                            }
                        }
                    }
                    short r = Util.byte2ToShort(facetBytes, stlOffset);
                    stlOffset = stlOffset + 2;
                    remarks[i] = r;
                }
            } catch (Exception e) {
                if (stlLoadListener != null) {
                    stlLoadListener.onFailure(e);
                } else {
                    e.printStackTrace();
                }
            }
            //将读取的数据设置到Model对象中
            model.setVerts(verts);
            model.setVnorms(vnorms);
            model.setRemarks(remarks);
    
        }
    
        public static interface StlLoadListener {
            void onstart();
    
            void onLoading(int cur, int total);
    
            void onFinished();
    
            void onFailure(Exception e);
        }
    }
    

    注意到,我们需要频繁的将byte数组转为short、float类型,我们直接把这些函数装到一个工具类Util中:

    public class Util {
    
        public static FloatBuffer floatToBuffer(float[] a) {
            //先初始化buffer,数组的长度*4,因为一个float占4个字节
            ByteBuffer bb = ByteBuffer.allocateDirect(a.length * 4);
            //数组排序用nativeOrder
            bb.order(ByteOrder.nativeOrder());
            FloatBuffer buffer = bb.asFloatBuffer();
            buffer.put(a);
            buffer.position(0);
            return buffer;
        }
    
        public static int byte4ToInt(byte[] bytes, int offset) {
            int b3 = bytes[offset + 3] & 0xFF;
            int b2 = bytes[offset + 2] & 0xFF;
            int b1 = bytes[offset + 1] & 0xFF;
            int b0 = bytes[offset + 0] & 0xFF;
            return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0;
        }
    
        public static short byte2ToShort(byte[] bytes, int offset) {
            int b1 = bytes[offset + 1] & 0xFF;
            int b0 = bytes[offset + 0] & 0xFF;
            return (short) ((b1 << 8) | b0);
        }
    
        public static float byte4ToFloat(byte[] bytes, int offset) {
    
            return Float.intBitsToFloat(byte4ToInt(bytes, offset));
        }
    
    }
    

    为了更好的表示三维坐标系下的一个点,我们定义Point类:

    public class Point {
        public float x;
        public float y;
        public float z;
    
        public Point(float x, float y, float z) {
            this.x = x;
            this.y = y;
            this.z = z;
    
        }
    }
    

    2 编写Render
    上一节我们只是拿数据而已,还没开始绘制,真正的大招现在才开始。因为我们目标是显示任意模型,因此,必须把模型移动到我们的“视野”中,才能看得到(当然了,如果图形本身就是在我们的视野中,那就不一定需要这样的操作了)。废话不多说,直接看源码:

    public class GLRenderer implements GLSurfaceView.Renderer {
    
        private Model model;
        private Point mCenterPoint;
        private Point eye = new Point(0, 0, -3);
        private Point up = new Point(0, 1, 0);
        private Point center = new Point(0, 0, 0);
        private float mScalef = 1;
        private float mDegree = 0;
    
        public GLRenderer(Context context) {
            try {
    
                model = new STLReader().parserBinStlInAssets(context, "huba.stl");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        public void rotate(float degree) {
            mDegree = degree;
        }
    
        @Override
        public void onDrawFrame(GL10 gl) {
            // 清除屏幕和深度缓存
            gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
    
    
            gl.glLoadIdentity();// 重置当前的模型观察矩阵
    
    
            //眼睛对着原点看 
            GLU.gluLookAt(gl, eye.x, eye.y, eye.z, center.x,
                    center.y, center.z, up.x, up.y, up.z);
    
            //为了能有立体感觉,通过改变mDegree值,让模型不断旋转
            gl.glRotatef(mDegree, 0, 1, 0);
    
            //将模型放缩到View刚好装下
            gl.glScalef(mScalef, mScalef, mScalef);
            //把模型移动到原点
            gl.glTranslatef(-mCenterPoint.x, -mCenterPoint.y,
                    -mCenterPoint.z);
    
    
            //===================begin==============================//
    
            //允许给每个顶点设置法向量
            gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
            // 允许设置顶点
            gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
            // 允许设置颜色
    
            //设置法向量数据源
            gl.glNormalPointer(GL10.GL_FLOAT, 0, model.getVnormBuffer());
            // 设置三角形顶点数据源
            gl.glVertexPointer(3, GL10.GL_FLOAT, 0, model.getVertBuffer());
    
            // 绘制三角形
            gl.glDrawArrays(GL10.GL_TRIANGLES, 0, model.getFacetCount() * 3);
    
            // 取消顶点设置
            gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
            //取消法向量设置
            gl.glDisableClientState(GL10.GL_NORMAL_ARRAY);
    
            //=====================end============================//
    
        }
    
    
        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
    
            // 设置OpenGL场景的大小,(0,0)表示窗口内部视口的左下角,(width, height)指定了视口的大小
            gl.glViewport(0, 0, width, height);
    
            gl.glMatrixMode(GL10.GL_PROJECTION); // 设置投影矩阵
            gl.glLoadIdentity(); // 设置矩阵为单位矩阵,相当于重置矩阵
            GLU.gluPerspective(gl, 45.0f, ((float) width) / height, 1f, 100f);// 设置透视范围
    
            //以下两句声明,以后所有的变换都是针对模型(即我们绘制的图形)
            gl.glMatrixMode(GL10.GL_MODELVIEW);
            gl.glLoadIdentity();
    
    
        }
    
        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            gl.glEnable(GL10.GL_DEPTH_TEST); // 启用深度缓存
            gl.glClearDepthf(1.0f); // 设置深度缓存值
            gl.glDepthFunc(GL10.GL_LEQUAL); // 设置深度缓存比较函数
            gl.glShadeModel(GL10.GL_SMOOTH);// 设置阴影模式GL_SMOOTH
            float r = model.getR();
            //r是半径,不是直径,因此用0.5/r可以算出放缩比例
            mScalef = 0.5f / r;
            mCenterPoint = model.getCentrePoint();
        }
    }
    

    在MainActivity中不断调用旋转函数:

    public class MainActivity extends AppCompatActivity {
    
        private boolean supportsEs2;
        private GLSurfaceView glView;
        private float rotateDegreen = 0;
        private GLRenderer glRenderer;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            checkSupported();
    
            if (supportsEs2) {
                glView = new GLSurfaceView(this);
                glRenderer = new GLRenderer(this);
                glView.setRenderer(glRenderer);
                setContentView(glView);
            } else {
                setContentView(R.layout.activity_main);
                Toast.makeText(this, "当前设备不支持OpenGL ES 2.0!", Toast.LENGTH_SHORT).show();
            }
        }
    
        public void rotate(float degree) {
            glRenderer.rotate(degree);
            glView.invalidate();
        }
    
        private Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                rotate(rotateDegreen);
            }
        };
    
        @Override
        protected void onResume() {
            super.onResume();
            if (glView != null) {
                glView.onResume();
    
                //不断改变rotateDegreen值,实现旋转
                new Thread() {
                    @Override
                    public void run() {
                        while (true) {
                            try {
                                sleep(100);
    
                                rotateDegreen += 5;
                                handler.sendEmptyMessage(0x001);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
    
                        }
                    }
                }.start();
            }
    
    
        }
    
        private void checkSupported() {
            ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
            ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
            supportsEs2 = configurationInfo.reqGlEsVersion >= 0x2000;
    
            boolean isEmulator = Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1
                    && (Build.FINGERPRINT.startsWith("generic")
                    || Build.FINGERPRINT.startsWith("unknown")
                    || Build.MODEL.contains("google_sdk")
                    || Build.MODEL.contains("Emulator")
                    || Build.MODEL.contains("Android SDK built for x86"));
    
            supportsEs2 = supportsEs2 || isEmulator;
        }
    
        @Override
        protected void onPause() {
            super.onPause();
            if (glView != null) {
                glView.onPause();
            }
        }
    }
    

    3 最后一步
    一切看起来都已经完成了,但似乎少了点什么。啊哈~,少了STL文件,其实网上有很多STL模型文件免费下载,大家可以随便搜索。我下载了一个胡巴的模型:

    模型截图
    模型截图
    下载完成后,运行如下:
    运行结果
    看到结果是不是觉得很失望?貌似看不到轮廓,其实,主要是跟灯光有关,我们程序中没有设置灯光。我们知道,我们在真实世界中看到物体主要是物体表面发生漫反射。我们所看到的物体跟光源的位置、物体的材质等等有关。另外,也可以通过贴纹理来做到。
    源码地址https://github.com/changhaismile/OpenGLDemo

    相关文章

      网友评论

        本文标题: Android OpenGL显示3D模型文件

        本文链接:https://www.haomeiwen.com/subject/bfnrpxtx.html