转载请注明出处:【huachao1001的专栏:http://blog.csdn.net/huachao1001】
*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
前面两篇文章我们介绍了OpenGL相关的基本知识,现在我们已经会绘制基本的图案了,但是还远远不能满足我们的需求。我们要做的是显示任意的模型,这也是本文所要做的事情。在阅读本文之前,请先确保你已经看过我前面两篇文章:
- 【Android OpenGL入门 】
- 【Android OpenGL 显示基本图形及相关概念解读 】
虽然标题是说显示任意3D文件,但是本文主要是以STL格式文件为例。其他的格式本质上都是一样的,只是解析部分的代码不同而已。接下来我们开始学习~
1 STL文件
它是标准的3D文件格式,一般3D打印机都是支持打印STL文件,关于STL文件的格式、以及相关介绍请参考百度百科:【stl格式】。当然了,我在代码的注释中也会进行相关解释。
1.1 解析准备
首先,在解析STL文件格式之前,我们需要进行构思。我们无非就是把STL文件中的三角形的顶点信息提取出来。因此我们的主要目标就是把所有点信息读取出来。
但是,3D模型的坐标位置很随机,大小也随机。而不同的模型所处的位置不同,为了能够让模型处于手机显示中心,我们必须对模型进行移动、放缩处理。使得任意大小、任意位置的模型都能在我们的GLSurfaceView中以“相同”的大小显示。
因此,我们不仅仅要读取顶点信息,而且还要获取模型的边界信息。我们想象成一个立方体,这个立方体刚好包裹住模型。即我们要读取x、y、z三个方向上的最大值最小值。
1.2 开始解析
首先,我们定义一个Model类,用于表示一个模型对象:
package com.hc.opengl;import java.nio.FloatBuffer;/** * Package com.hc.opengl * Created by HuaChao on 2016/7/28. */public class Model { private int facetCount; private float[] verts; private float[] vnorms; private short[] remarks; private FloatBuffer vertBuffer; private FloatBuffer vnormBuffer; 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; } public void setVerts(float[] verts) { this.verts = verts; vertBuffer = Util.floatToBuffer(verts); } public void setVnorms(float[] vnorms) { this.vnorms = vnorms; vnormBuffer = Util.floatToBuffer(vnorms); } }
- 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
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 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
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
接下来就是将stl文件转换成Model对象,我们定义一个STLReader类:
package com.hc.opengl;import android.content.Context;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;/** * Package com.hc.opengl * Created by HuaChao on 2016/7/28. */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); } public Model parserBinStl(InputStream in) throws IOException { if (stlLoadListener != null) stlLoadListener.onstart(); Model model = new Model(); in.skip(80); byte[] bytes = new byte[4]; in.read(bytes); int facetCount = Util.byte4ToInt(bytes, 0); model.setFacetCount(facetCount); if (facetCount == 0) { in.close(); return model; } 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个字节用来描述三角面片的属性信息 * **/ float[] verts = new float[facetCount * 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.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); }}
- 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
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 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
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
注意到,我们需要频繁的将byte数组转为short、float类型,我们直接把这些函数装到一个工具类Util中:
package com.hc.opengl;import java.nio.ByteBuffer;import java.nio.ByteOrder;import java.nio.FloatBuffer;/** * Package com.hc.opengl * Created by HuaChao on 2016/7/28. */public class Util { public static FloatBuffer floatToBuffer(float[] a) { ByteBuffer bb = ByteBuffer.allocateDirect(a.length * 4); 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)); }}
- 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
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 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
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
为了更好的表示三维坐标系下的一个点,我们定义Point类:
/** * Package com.hc.opengl * Created by HuaChao on 2016/7/28. */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
上一节我们只是拿数据而已,还没开始绘制,真正的大招现在才开始。因为我们目标是显示任意模型,因此,必须把模型移动到我们的“视野”中,才能看得到(当然了,如果图形本身就是在我们的视野中,那就不一定需要这样的操作了)。废话不多说,直接看源码:
/** * Package com.hc.opengl * Created by HuaChao on 2016/7/28. */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); gl.glRotatef(mDegree, 0, 1, 0); gl.glScalef(mScalef, mScalef, mScalef); gl.glTranslatef(-mCenterPoint.x, -mCenterPoint.y, -mCenterPoint.z); 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); } @Override public void onSurfaceChanged(GL10 gl, int width, int 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); float r = model.getR(); mScalef = 0.5f / r; mCenterPoint = model.getCentrePoint(); }}
- 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
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 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
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
在MainActivity中不断调用旋转函数:
package com.hc.opengl;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(); 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(); } }}
- 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
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 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
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
3 最后一步
一切看起来都已经完成了,但似乎少了点什么。啊哈~,少了STL文件,其实网上有很多STL模型文件免费下载,大家可以随便搜索。我下载了一个胡巴的模型:
下载完成后,运行如下:
看到结果是不是觉得很失望?貌似看不到轮廓,其实,主要是跟灯光有关,我们程序中没有设置灯光。我们知道,我们在真实世界中看到物体主要是物体表面发生漫反射。我们所看到的物体跟光源的位置、物体的材质等等有关。另外,也可以通过贴纹理来做到。但是到目前为止,我们还没有这些知识,代码里面也没有涉及到这些,因此我们这能看到当前这个样子。后面我们会继续深入学习相关知识,欢迎关注~。
好啦,最后献上源码吧~,注意,下载的源码中Model
类的getCentrePoint
函数需要修改,请以本文中的Model
类为主。
源码地址:http://download.csdn.net/detail/huachao1001/9588619