OpenGL ES 3. OBJ文件渲染

Hayley ·
更新时间:2024-09-21
· 622 次阅读

大家好,接下来将为大家介绍OpenGL ES 3. OBJ文件渲染。

上一节介绍了OBJ文件及其文本结构方面的内容,接下来将会介绍如何将OBJ文件加载到内存,并通过OpenGL的方式渲染出来。

1、OBJ文件解析类

由于OBJ文本文件是按照一定的规则储存的(详见上一节内容介绍),所以,我们首先介绍OBJ文件的加载解析,加载后用于渲染物体的 LoadedObjectVertexNormalTexture 类。介绍加载顶点坐标、三角形面、纹理坐标等信息。

import android.opengl.GLES30; //加载后的物体——仅携带顶点信息,颜色随机 public class LoadedObjectVertexNormalTexture { int mProgram;//自定义渲染管线着色器程序id int muMVPMatrixHandle;//总变换矩阵引用 int muMMatrixHandle;//位置、旋转变换矩阵 int maPositionHandle; //顶点位置属性引用 int maNormalHandle; //顶点法向量属性引用 int maLightLocationHandle;//光源位置属性引用 int maCameraHandle; //摄像机位置属性引用 int maTexCoorHandle; //顶点纹理坐标属性引用 String mVertexShader;//顶点着色器代码脚本 String mFragmentShader;//片元着色器代码脚本 int vCount=0; FloatBuffer mVertexBuffer;//顶点坐标数据缓冲 FloatBuffer mNormalBuffer;//顶点法向量数据缓冲 FloatBuffer mTexCoorBuffer;//顶点纹理坐标数据缓冲 public LoadedObjectVertexNormalTexture(MySurfaceView mv,float[] vertices,float[] normals,float texCoors[]) {//构造器 //初始化顶点坐标、法向量、纹理坐标数据 initVertexData(vertices,normals,texCoors); //初始化着色器 initShader(mv); } //初始化顶点坐标、法向量、纹理坐标数据的方法 public void initVertexData(float[] vertices,float[] normals,float texCoors[]) { //顶点坐标数据的初始化================begin============================ vCount=vertices.length/3; //创建顶点坐标数据缓冲 //vertices.length*4是因为一个整数四个字节 ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4); vbb.order(ByteOrder.nativeOrder());//设置字节顺序 mVertexBuffer = vbb.asFloatBuffer();//转换为Float型缓冲 mVertexBuffer.put(vertices);//向缓冲区中放入顶点坐标数据 mVertexBuffer.position(0);//设置缓冲区起始位置 //特别提示:由于不同平台字节顺序不同数据单元不是字节的一定要经过ByteBuffer //转换,关键是要通过ByteOrder设置nativeOrder(),否则有可能会出问题 //顶点坐标数据的初始化================end============================ //顶点法向量数据的初始化================begin============================ ByteBuffer cbb = ByteBuffer.allocateDirect(normals.length*4); cbb.order(ByteOrder.nativeOrder());//设置字节顺序 mNormalBuffer = cbb.asFloatBuffer();//转换为Float型缓冲 mNormalBuffer.put(normals);//向缓冲区中放入顶点法向量数据 mNormalBuffer.position(0);//设置缓冲区起始位置 //特别提示:由于不同平台字节顺序不同数据单元不是字节的一定要经过ByteBuffer //转换,关键是要通过ByteOrder设置nativeOrder(),否则有可能会出问题 //顶点着色数据的初始化================end============================ //顶点纹理坐标数据的初始化================begin============================ ByteBuffer tbb = ByteBuffer.allocateDirect(texCoors.length*4); tbb.order(ByteOrder.nativeOrder());//设置字节顺序 mTexCoorBuffer = tbb.asFloatBuffer();//转换为Float型缓冲 mTexCoorBuffer.put(texCoors);//向缓冲区中放入顶点纹理坐标数据 mTexCoorBuffer.position(0);//设置缓冲区起始位置 //特别提示:由于不同平台字节顺序不同数据单元不是字节的一定要经过ByteBuffer //转换,关键是要通过ByteOrder设置nativeOrder(),否则有可能会出问题 //顶点纹理坐标数据的初始化================end============================ } //绘制加载物体的方法 public void drawSelf(int texId) { //制定使用某套着色器程序 GLES30.glUseProgram(mProgram); //将最终变换矩阵传入着色器程序 GLES30.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0); //将位置、旋转变换矩阵传入着色器程序 GLES30.glUniformMatrix4fv(muMMatrixHandle, 1, false, MatrixState.getMMatrix(), 0); //将光源位置传入着色器程序 GLES30.glUniform3fv(maLightLocationHandle, 1, MatrixState.lightPositionFB); //将摄像机位置传入着色器程序 GLES30.glUniform3fv(maCameraHandle, 1, MatrixState.cameraFB); // 将顶点位置数据传入渲染管线 GLES30.glVertexAttribPointer (maPositionHandle, 3, GLES30.GL_FLOAT, false, 3*4, mVertexBuffer); //将顶点法向量数据传入渲染管线 GLES30.glVertexAttribPointer (maNormalHandle, 3, GLES30.GL_FLOAT, false, 3*4, mNormalBuffer); //将顶点纹理坐标数据传入渲染管线 GLES30.glVertexAttribPointer (maTexCoorHandle, 2, GLES30.GL_FLOAT, false, 2*4, mTexCoorBuffer); //启用顶点位置、法向量、纹理坐标数据数组 GLES30.glEnableVertexAttribArray(maPositionHandle); GLES30.glEnableVertexAttribArray(maNormalHandle); GLES30.glEnableVertexAttribArray(maTexCoorHandle); //启用纹理坐标数据数组 //绑定纹理 GLES30.glActiveTexture(GLES30.GL_TEXTURE0);//启用0号纹理 GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texId);//绑定纹理 //绘制加载的物体 GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, vCount); } }

具体的加载工具类LoadUtil:alv为原始顶点坐标列表--直接从obj文件中加载、alvResult为结果顶点坐标列表--按面组织好、alt为原始纹理坐标列表、altResult为纹理坐标结果列表、aln为原始法向量列表、alnResult法向量结果列表。

public class LoadUtil { //从obj文件中加载携带顶点信息的物体 public static LoadedObjectVertexNormalTexture loadFromFile (String fname, Resources r,MySurfaceView mv) { //加载后物体的引用 LoadedObjectVertexNormalTexture lo=null; //原始顶点坐标列表--直接从obj文件中加载 ArrayList alv=new ArrayList(); //结果顶点坐标列表--按面组织好 ArrayList alvResult=new ArrayList(); //原始纹理坐标列表 ArrayList alt=new ArrayList(); //纹理坐标结果列表 ArrayList altResult=new ArrayList(); //原始法向量列表 ArrayList aln=new ArrayList(); //法向量结果列表 ArrayList alnResult=new ArrayList(); try { InputStream in=r.getAssets().open(fname); InputStreamReader isr=new InputStreamReader(in); BufferedReader br=new BufferedReader(isr); String temps=null; //扫面文件,根据行类型的不同执行不同的处理逻辑 while((temps=br.readLine())!=null) {//读取一行文本 String[] tempsa=temps.split("[ ]+");//将文本行用空格符切分 if(tempsa[0].trim().equals("v")) {//此行为顶点坐标行 //若为顶点坐标行则提取出此顶点的XYZ坐标添加到原始顶点坐标列表中 alv.add(Float.parseFloat(tempsa[1])); alv.add(Float.parseFloat(tempsa[2])); alv.add(Float.parseFloat(tempsa[3])); } else if(tempsa[0].trim().equals("vt")) {//此行为纹理坐标行 //若为纹理坐标行则提取ST坐标并添加进原始纹理坐标列表中 alt.add(Float.parseFloat(tempsa[1])); alt.add(1-Float.parseFloat(tempsa[2])); } else if(tempsa[0].trim().equals("vn")) {//此行为法向量行 //若为纹理坐标行则提取ST坐标并添加进原始纹理坐标列表中 aln.add(Float.parseFloat(tempsa[1]));//放进aln列表中 aln.add(Float.parseFloat(tempsa[2])); //放进aln列表中 aln.add(Float.parseFloat(tempsa[3])); //放进aln列表中 } else if(tempsa[0].trim().equals("f")) {//此行为三角形面 //计算第0个顶点的索引,并获取此顶点的XYZ三个坐标 int index=Integer.parseInt(tempsa[1].split("/")[0])-1; float x0=alv.get(3*index); float y0=alv.get(3*index+1); float z0=alv.get(3*index+2); alvResult.add(x0); alvResult.add(y0); alvResult.add(z0); //计算第1个顶点的索引,并获取此顶点的XYZ三个坐标 index=Integer.parseInt(tempsa[2].split("/")[0])-1; float x1=alv.get(3*index); float y1=alv.get(3*index+1); float z1=alv.get(3*index+2); alvResult.add(x1); alvResult.add(y1); alvResult.add(z1); //计算第2个顶点的索引,并获取此顶点的XYZ三个坐标 index=Integer.parseInt(tempsa[3].split("/")[0])-1; float x2=alv.get(3*index); float y2=alv.get(3*index+1); float z2=alv.get(3*index+2); alvResult.add(x2); alvResult.add(y2); alvResult.add(z2); //将纹理坐标组织到结果纹理坐标列表中 //第0个顶点的纹理坐标 int indexTex=Integer.parseInt(tempsa[1].split("/")[1])-1; altResult.add(alt.get(indexTex*2)); altResult.add(alt.get(indexTex*2+1)); //第1个顶点的纹理坐标 indexTex=Integer.parseInt(tempsa[2].split("/")[1])-1; altResult.add(alt.get(indexTex*2)); altResult.add(alt.get(indexTex*2+1)); //第2个顶点的纹理坐标 indexTex=Integer.parseInt(tempsa[3].split("/")[1])-1; altResult.add(alt.get(indexTex*2)); altResult.add(alt.get(indexTex*2+1)); //================================================= //计算第0个顶点的法向量索引 int indexN=Integer.parseInt(tempsa[1].split("/")[2])-1;//获取法向量编号 float nx0=aln.get(3*indexN);//获取法向量的x值 float ny0=aln.get(3*indexN+1);//获取法向量的y值 float nz0=aln.get(3*indexN+2);//获取法向量的z值 alnResult.add(nx0);//放入alnResult列表 alnResult.add(ny0);//放入alnResult列表 alnResult.add(nz0); //放入alnResult列表 //计算第1个顶点的索引,并获取此顶点的XYZ三个坐标 indexN=Integer.parseInt(tempsa[2].split("/")[2])-1; float nx1=aln.get(3*indexN); float ny1=aln.get(3*indexN+1); float nz1=aln.get(3*indexN+2); alnResult.add(nx1); alnResult.add(ny1); alnResult.add(nz1); //计算第2个顶点的索引,并获取此顶点的XYZ三个坐标 indexN=Integer.parseInt(tempsa[3].split("/")[2])-1; float nx2=aln.get(3*indexN); float ny2=aln.get(3*indexN+1); float nz2=aln.get(3*indexN+2); alnResult.add(nx2); alnResult.add(ny2); alnResult.add(nz2); } } //生成顶点数组 int size=alvResult.size(); float[] vXYZ=new float[size]; for(int i=0;i<size;i++) { vXYZ[i]=alvResult.get(i); } //生成纹理数组 size=altResult.size(); float[] tST=new float[size]; for(int i=0;i<size;i++) { tST[i]=altResult.get(i); } //生成法向量数组 size=alnResult.size();//获取法向量列表的大小 float[] nXYZ=new float[size];//创建存放法向量的数组 for(int i=0;i<size;i++) { nXYZ[i]=alnResult.get(i);//将法向量值存入数组 } //创建加载物体对象 lo=new LoadedObjectVertexNormalTexture(mv,vXYZ,nXYZ,tST); } catch(Exception e) { Log.d("load error", "load error"); e.printStackTrace(); } return lo;//返回创建的物体对象的引用 } }

2、介绍用于渲染整个 3D 场景的 MySurfaceView 类:自定义渲染器SceneRenderer、并设置渲染模式为主动渲染RENDERMODE_CONTINUOUSLY。

class MySurfaceView extends GLSurfaceView { private final float TOUCH_SCALE_FACTOR = 180.0f/320;//角度缩放比例 private SceneRenderer mRenderer;//场景渲染器 private float mPreviousY;//上次的触控位置Y坐标 private float mPreviousX;//上次的触控位置X坐标 int textureId;//系统分配的纹理id public MySurfaceView(Context context) { super(context); this.setEGLContextClientVersion(3); //设置使用OPENGL ES3.0 mRenderer = new SceneRenderer(); //创建场景渲染器 setRenderer(mRenderer); //设置渲染器 setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);//设置渲染模式为主动渲染 } //触摸事件回调方法 @Override public boolean onTouchEvent(MotionEvent e) { float y = e.getY(); float x = e.getX(); switch (e.getAction()) { case MotionEvent.ACTION_MOVE: float dy = y - mPreviousY;//计算触控笔Y位移 float dx = x - mPreviousX;//计算触控笔X位移 mRenderer.yAngle += dx * TOUCH_SCALE_FACTOR;//设置沿y轴旋转角度 mRenderer.xAngle+= dy * TOUCH_SCALE_FACTOR;//设置沿x轴旋转角度 requestRender();//重绘画面 } mPreviousY = y;//记录触控笔位置 mPreviousX = x;//记录触控笔位置 return true; } private class SceneRenderer implements GLSurfaceView.Renderer { float yAngle;//绕Y轴旋转的角度 float xAngle; //绕X轴旋转的角度 //从指定的obj文件中加载的对象 LoadedObjectVertexNormalTexture lovo; public void onDrawFrame(GL10 gl) { //清除深度缓冲与颜色缓冲 GLES30.glClear( GLES30.GL_DEPTH_BUFFER_BIT | GLES30.GL_COLOR_BUFFER_BIT); //坐标系推远 MatrixState.pushMatrix(); MatrixState.translate(0, -16f, -60f); //绕Y轴、X轴旋转 MatrixState.rotate(yAngle, 0, 1, 0); MatrixState.rotate(xAngle, 1, 0, 0); //若加载的物体不为空则绘制物体 if(lovo!=null) { lovo.drawSelf(textureId); } MatrixState.popMatrix(); } public void onSurfaceChanged(GL10 gl, int width, int height) { //设置视窗大小及位置 GLES30.glViewport(0, 0, width, height); //计算GLSurfaceView的宽高比 float ratio = (float) width / height; //调用此方法计算产生透视投影矩阵 MatrixState.setProjectFrustum(-ratio, ratio, -1, 1, 2, 100); //调用此方法产生摄像机9参数位置矩阵 MatrixState.setCamera(0,0,0,0f,0f,-1f,0f,1.0f,0.0f); } public void onSurfaceCreated(GL10 gl, EGLConfig config) { //设置屏幕背景色RGBA GLES30.glClearColor(0.0f,0.0f,0.0f,1.0f); //打开深度检测 GLES30.glEnable(GLES30.GL_DEPTH_TEST); //打开背面剪裁 GLES30.glEnable(GLES30.GL_CULL_FACE); //初始化变换矩阵 MatrixState.setInitStack(); //初始化光源位置 MatrixState.setLightLocation(40, 10, 20); //加载要绘制的物体 lovo=LoadUtil.loadFromFile("ch_t.obj", MySurfaceView.this.getResources(),MySurfaceView.this); //加载纹理图 textureId=initTexture(R.drawable.ghxp); } } public int initTexture(int drawableId)//textureId { //生成纹理ID int[] textures = new int[1]; GLES30.glGenTextures ( 1, //产生的纹理id的数量 textures, //纹理id的数组 0 //偏移量 ); int textureId=textures[0]; GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId); GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER,GLES30.GL_NEAREST); GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,GLES30.GL_TEXTURE_MAG_FILTER,GLES30.GL_LINEAR); GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S,GLES30.GL_REPEAT); GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T,GLES30.GL_REPEAT); //通过输入流加载图片===============begin=================== InputStream is = this.getResources().openRawResource(drawableId); Bitmap bitmapTmp; try { bitmapTmp = BitmapFactory.decodeStream(is); } finally { try { is.close(); } catch(IOException e) { e.printStackTrace(); } } //通过输入流加载图片===============end===================== GLUtils.texImage2D ( GLES30.GL_TEXTURE_2D, //纹理类型 0, GLUtils.getInternalFormat(bitmapTmp), bitmapTmp, //纹理图像 GLUtils.getType(bitmapTmp), 0 //纹理边框尺寸 ); bitmapTmp.recycle(); //纹理加载成功后释放图片 return textureId; } }

onSurfaceCreated 方法的实现,其中在初始化变换矩阵后创建了 LoadedObjectVertexNormalTexture 类的对象。

3、渲染举例示例:

最后,欢迎大家一起交流学习:微信:liaosy666 ; QQ:2209115372 。


作者:SunnyLiaoSu



opengl obj文件

需要 登录 后方可回复, 如果你还没有账号请 注册新账号