OpenGL ES——导入.stl格式的3D模型

2018-07-03 13:28:02 浏览数 (1)

前言

在上一章中,我们使用OpenGL ES绘制了一个平平无奇的三角形。那么如何绘制3D模型呢?其实,在计算机的世界中,所有的3D模型都是由无数的三角平面拼接而成。

通常我们使用.stl格式来记录一个3D模型的三角平面信息,根据.stl文件中记录的三角平面信息,我们能够还原出一个完整的3D模型。

因此,本文将介绍,如何从.stl解析出三角平面信息,并绘制出来。

STL Format

STL(https://en.wikipedia.org/wiki/STL_(file_format))是一种文件格式,格式如:

代码语言:javascript复制
明码://字符段意义
solidfilenamestl//文件路径及文件名
facetnormalxyz//三角面片法向量的3个分量值
outerloop
vertexxyz//三角面片第一个顶点坐标
vertexxyz//三角面片第二个顶点坐标
vertexxyz//三角面片第三个顶点坐标
endloop
endfacet//完成一个三角面片定义
 
......//其他facet
 
endsolidfilenamestl//整个STL文件定义结束

其中,每个三角面的信息分为三部分:顶点坐标、法线分量、属性位。

顶点坐标

和上一章我们画三角形的原理类似,三角形的三个顶点坐标,将决定三角平面的位置与形态。

法向量

三点只能确定一个三角形的平面,但平面有两面,到底哪一面是对外的,却无法确定。此时,我们可以设置一个法线,法线的方向就是三角形平面的外面。法线的方向,由xyz三个轴上的分量长度决定。

值得注意的是,如果我们不设置法线,或设置(0,0,0)。则会根据三角形三个顶点的加载顺序,以右手定则的形式,确定三角形平面的外面。

属性位

After these follows a 2-byte ("short") unsigned integer that is the "attribute byte count" – in the standard format, this should be zero because most software does not understand anything else.[6](https://en.wikipedia.org/wiki/STL_(file_format)#cite_note-burns-6)

不常使用的保留位置。

解析

注释写得比较清楚,不再赘述。在这一段解析中,我们不仅会获得三角形平面的顶点坐标和法向量数组,我们还提供了计算最大半径,计算中心点等方法。

代码语言:javascript复制
public class STLPoint {
    public float x;
    public float y;
    public float z;

    public STLPoint(float x, float y, float z) {
        this.x = x;
        this.y = y;
        this.z = z;

    }
}
代码语言:javascript复制
public class STLUtils {

    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));
    }

}
代码语言:javascript复制
import java.io.IOException;
import java.io.InputStream;
import java.nio.FloatBuffer;

/**
 * Package com.hc.opengl
 * Created by HuaChao on 2016/7/28.
 */
public class STLModel {
    //三角面个数
    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 STLPoint getCentrePoint() {
        float cx = minX   (maxX - minX) / 2;
        float cy = minY   (maxY - minY) / 2;
        float cz = minZ   (maxZ - minZ) / 2;
        return new STLPoint(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 = STLUtils.floatToBuffer(verts);
    }

    //设置顶点数组法向量的同时,设置对应的Buffer
    public void setVnorms(float[] vnorms) {
        this.vnorms = vnorms;
        vnormBuffer = STLUtils.floatToBuffer(vnorms);
    }

    //···
    //其他属性对应的setter、getter函数
    //···


    public int getFacetCount() {
        return facetCount;
    }

    public void setFacetCount(int facetCount) {
        this.facetCount = facetCount;
    }

    public float[] getVerts() {
        return verts;
    }

    public float[] getVnorms() {
        return vnorms;
    }

    public short[] getRemarks() {
        return remarks;
    }

    public void setRemarks(short[] remarks) {
        this.remarks = remarks;
    }

    public FloatBuffer getVertBuffer() {
        return vertBuffer;
    }

    public void setVertBuffer(FloatBuffer vertBuffer) {
        this.vertBuffer = vertBuffer;
    }

    public FloatBuffer getVnormBuffer() {
        return vnormBuffer;
    }

    public void setVnormBuffer(FloatBuffer vnormBuffer) {
        this.vnormBuffer = vnormBuffer;
    }

    public void parserBinStl(InputStream in) throws IOException{
//前面80字节是文件头,用于存贮文件名;
        in.skip(80);

        //紧接着用 4 个字节的整数来描述模型的三角面片个数
        byte[] bytes = new byte[4];
        in.read(bytes);// 读取三角面片个数
        int facetCount = STLUtils.byte4ToInt(bytes, 0);
        setFacetCount(facetCount);
        if (facetCount == 0) {
            in.close();
            return ;
        }

        // 每个三角面片占用固定的50个字节
        byte[] facetBytes = new byte[50 * facetCount];
        // 将所有的三角面片读取到字节数组
        in.read(facetBytes);
        //数据读取完毕后,可以把输入流关闭
        in.close();


        parseModel( facetBytes);



    }


    /**
     * 解析模型数据,包括顶点数据、法向量数据、所占空间范围等
     */
    private void parseModel(byte[] facetBytes) {
        int facetCount = 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];
        //保存所有三角面的属性信息
        //After these follows a 2-byte ("short") unsigned integer that is the "attribute byte count" – in the standard format, this should be zero because most software does not understand anything else.[6] from wiki
        short[] remarks = new short[facetCount];

        int stlOffset = 0;
        try {
            for (int i = 0; i < facetCount; i  ) {

                for (int j = 0; j < 4; j  ) {
                    float x = STLUtils.byte4ToFloat(facetBytes, stlOffset);
                    float y = STLUtils.byte4ToFloat(facetBytes, stlOffset   4);
                    float z = STLUtils.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) {
                            minX = maxX = x;
                            minY = maxY = y;
                            minZ = maxZ = z;
                        } else {
                            minX = Math.min(minX, x);
                            minY = Math.min(minY, y);
                            minZ = Math.min(minZ, z);
                            maxX = Math.max(maxX, x);
                            maxY = Math.max(maxY, y);
                            maxZ = Math.max(maxZ, z);
                        }
                    }
                }
                short r = STLUtils.byte2ToShort(facetBytes, stlOffset);
                stlOffset = stlOffset   2;
                remarks[i] = r;
            }
        } catch (Exception e) {

        }
        //将读取的数据设置到Model对象中
        setVerts(verts);
        setVnorms(vnorms);
        setRemarks(remarks);

    }

}

渲染3D模型

创建画布

代码语言:javascript复制
    private STLModel model;
    private STLPoint mCenterPoint;
    private STLPoint eye = new STLPoint(0, 0, -3);
    private STLPoint up = new STLPoint(0, 1, 0);
    private STLPoint center = new STLPoint(0, 0, 0);
    private float mScalef = 1;
    private float mDegree = 0;

    @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();
    }

在画布完成创建时,我们需要进行一些初始工作:

  • 开启深度缓存和阴影模式
  • 计算缩放比例
  • 计算中心点

设置投影矩阵

代码语言:javascript复制
    @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();
    }

这里与上一节的例子类似,不再赘述。

代码语言:javascript复制
    @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============================//

    }

gluLookAt

gluLookAt方法非常有趣。它决定了我们看物体的角度。

想象一下,当我们要看一个物体时,我们有三个属于可以改变:

  1. 我的眼睛的位置
  2. 物体的位置
  3. 我目光的角度 eyecenterup这三个量就决定了这三个属性。 其中up = 0,1,0时,表示我是正着头在看,up=1,1,0,表示我是歪着头45度在看。依此类推。

余下的部分都是非常套路的绘制三角形,不再重复分析。

以上,就是通过STL文件,导致三维模型数据并绘制的全过程。

如有问题,欢迎指正。

0 人点赞