前言
在上一章中,我们使用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方法非常有趣。它决定了我们看物体的角度。
想象一下,当我们要看一个物体时,我们有三个属于可以改变:
- 我的眼睛的位置
- 物体的位置
- 我目光的角度 eye、center、up这三个量就决定了这三个属性。 其中up = 0,1,0时,表示我是正着头在看,up=1,1,0,表示我是歪着头45度在看。依此类推。
余下的部分都是非常套路的绘制三角形,不再重复分析。
以上,就是通过STL文件,导致三维模型数据并绘制的全过程。
如有问题,欢迎指正。