前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OpenGL ES——导入.stl格式的3D模型

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

作者头像
Oceanlong
发布2018-07-03 13:28:02
1.8K0
发布2018-07-03 13:28:02
举报

前言

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

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

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

STL Format

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

代码语言: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%29#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文件,导致三维模型数据并绘制的全过程。

如有问题,欢迎指正。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.06.12 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • STL Format
    • 顶点坐标
      • 法向量
        • 属性位
          • 解析
          • 渲染3D模型
            • 创建画布
              • 设置投影矩阵
                • gluLookAt
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档