Android OpenGLES2.0(十四)——Obj格式3D模型加载

在《OpenGLES系列》文章中,最开始的几篇讲的就是OpenGL世界中各种形体的构建,但是那些形体都是规则的简单形体,遇到复杂的形体,比如说一个人、一朵花,怎么办呢?自然是通过其他工具类似于Maya、3DMax等3D建模工具,做好模型导出来,然后用OpenGLES加载导出的模型文件。模型的加载大同小异,本篇博客是以Obj格式的3D模型为例。

模型文件

本篇博客例子中加载的是一个帽子,资源是在网上随便找的一个。加载出来如图所示:


格式如下:

# File exported by ZBrush version 4.2
# www.zbrush.com
#Vertex Count 4898
#Face Count 4848
#Auto scale x=0.211538 y=0.211538 z=0.211538
#Auto offset x=-0.000000 y=-0.412507 z=-0.000000
v -0.62500745 3.93329608 0.0000001
v -0.00002446 3.32622414 1.33471741
v 1.47657442 2.55452877 1.37523436
v -1.01934254 3.90772931 0.00000007
...省略若干行...
g default
f 990 991 987 986
f 991 874 873 987
f 972 971 991 990
f 971 55 874 991
f 987 992 988 986
...省略若干行

加载这个模型文件前,我们需要先知道这些数据代表的是什么。针对这个文件,#号开头的,是描述模型文件的相关信息。以v开头的,表示的是顶点坐标。以f开头的,表示一个面,后面跟的四个值是索引。一个v,后面的三个数,代表一个点的xyz,4个点组成了一个四边形。
为什么是4个点?不是说在OpenGLES中基本几何是三角形么?这样问就有点尴尬了,因为模型文件是我在网上随便下的,自己选的模型,跪着也要加载出来。有什么关系,一个四边形不就是两个三角形么。
这个模型文件只有v、f两类数据,但是一个炫酷的模型,往往是包含很多数据的,主要的数据类型如下:

  1. 顶点数据(Vertex data):
  1. v 几何体顶点(Geometric vertices)
  2. vt 贴图坐标点(Texture vertices)
  3. vn 顶点法线(Vertex normals)
  4. vp 参数空格顶点 (Parameter space vertices)
  1. 自由形态曲线(Free-form curve)/表面属性(surface attributes):
  1. deg 度(Degree)
  2. bmat 基础矩阵(Basis matrix)
  3. step 步尺寸(Step size)
  4. cstype 曲线或表面类型 (Curve or surface type)
  1. 元素(Elements):
  1. p 点(Point)
  2. l 线(Line)
  3. f 面(Face)
  4. curv 曲线(Curve)
  5. curv2 2D曲线(2D curve)
  6. surf 表面(Surface)
  1. 自由形态曲线(Free-form curve)/表面主体陈述(surface body statements):
  1. parm 参数值(Parameter values )
  2. trim 外部修剪循环(Outer trimming loop)
  3. hole 内部整修循环(Inner trimming loop)
  4. scrv 特殊曲线(Special curve)
  5. sp 特殊的点(Special point)
  6. end 结束陈述(End statement)
  1. 自由形态表面之间的连接(Connectivity between free-form surfaces):
  1. con 连接 (Connect)
  1. 成组(Grouping):
  1. g 组名称(Group name)
  2. s 光滑组(Smoothing group)
  3. mg 合并组(Merging group)
  4. o 对象名称(Object name)
  1. 显示(Display)/渲染属性(render attributes):
  1. bevel 导角插值(Bevel interpolation)
  2. c_interp 颜色插值(Color interpolation)
  3. d_interp 溶解插值(Dissolve interpolation)
  4. lod 细节层次(Level of detail)
  5. usemtl 材质名称(Material name)
  6. mtllib 材质库(Material library)
  7. shadow_obj 投射阴影(Shadow casting)
  8. trace_obj 光线跟踪(Ray tracing)
  9. ctech 曲线近似技术(Curve approximation technique)
  10. stech 表面近似技术 (Surface approximation technique)

模型加载

知道了模型文件的内容和格式,加载起来就不是什么问题了:

public class ObjReader {

    public static void read(InputStream stream,Obj3D obj3D){
        ArrayList<Float> alv=new ArrayList<Float>();//原始顶点坐标列表
        ArrayList<Float> alvResult=new ArrayList<Float>();//结果顶点坐标列表
        ArrayList<Float> norlArr=new ArrayList<>();
        float[] ab=new float[3],bc=new float[3],norl=new float[3];
        try{
            InputStreamReader isr=new InputStreamReader(stream);
            BufferedReader br=new BufferedReader(isr);
            String temps=null;
            while((temps=br.readLine())!=null)
            {
                String[] tempsa=temps.split("[ ]+");
                if(tempsa[0].trim().equals("v")) {//此行为顶点坐标
                    alv.add(Float.parseFloat(tempsa[1]));
                    alv.add(Float.parseFloat(tempsa[2]));
                    alv.add(Float.parseFloat(tempsa[3]));
                }  else if(tempsa[0].trim().equals("f")) {//此行为三角形面
                    int a=Integer.parseInt(tempsa[1])-1;
                    int b=Integer.parseInt(tempsa[2])-1;
                    int c=Integer.parseInt(tempsa[3])-1;
                    int d=Integer.parseInt(tempsa[4])-1;
                    //abc和acd两个三角形组成的四边形

                    alvResult.add(alv.get(a*3));
                    alvResult.add(alv.get(a*3+1));
                    alvResult.add(alv.get(a*3+2));
                    alvResult.add(alv.get(b*3));
                    alvResult.add(alv.get(b*3+1));
                    alvResult.add(alv.get(b*3+2));
                    alvResult.add(alv.get(c*3));
                    alvResult.add(alv.get(c*3+1));
                    alvResult.add(alv.get(c*3+2));

                    alvResult.add(alv.get(a*3));
                    alvResult.add(alv.get(a*3+1));
                    alvResult.add(alv.get(a*3+2));
                    alvResult.add(alv.get(c*3));
                    alvResult.add(alv.get(c*3+1));
                    alvResult.add(alv.get(c*3+2));
                    alvResult.add(alv.get(d*3));
                    alvResult.add(alv.get(d*3+1));
                    alvResult.add(alv.get(d*3+2));

                    //这里也是因为下载模型文件的坑。下了个出了顶点和面啥也没有的模型文件
                    //为了有3d效果,给它加个光照,自己计算顶点法线
                    //用面法向量策略。按理说点法向量更适合这种光滑的3D模型,但是计算起来太复杂了,so
                    //既然主要讲3D模型加载,就先用面法向量策略来吧
                    //通常3D模型里面会包含法向量信息的。
                    //法向量的计算,ABC三个空间点,他们的法向量为向量AB与向量BC的外积,所以有:
                    for (int i=0;i<3;i++){
                        ab[i]=alv.get(a*3+i)-alv.get(b*3+i);
                        bc[i]=alv.get(b*3+i)-alv.get(c*3+i);
                    }
                    norl[0]=ab[1]*bc[2]-ab[2]*bc[1];
                    norl[1]=ab[2]*bc[0]-ab[0]*bc[2];
                    norl[2]=ab[0]*bc[1]-ab[1]*bc[0];

                    //上面两个三角形,传入了6个顶点,这里循环6次,简单粗暴
                    for (int i=0;i<6;i++){
                        norlArr.add(norl[0]);
                        norlArr.add(norl[1]);
                        norlArr.add(norl[2]);
                    }
                }
            }

            //这些就是比较熟悉的了,一切都为了能够把数据给GPU
            int size=alvResult.size();
            float[] vXYZ=new float[size];
            for(int i=0;i<size;i++){
                vXYZ[i]=alvResult.get(i);
            }
            ByteBuffer byteBuffer=ByteBuffer.allocateDirect(4*size);
            byteBuffer.order(ByteOrder.nativeOrder());
            obj3D.vert=byteBuffer.asFloatBuffer();
            obj3D.vert.put(vXYZ);
            obj3D.vert.position(0);
            obj3D.vertCount=size/3;
            int vbSize=norlArr.size();
            float[] vbArr=new float[size];
            for(int i=0;i<size;i++){
                vbArr[i]=norlArr.get(i);
            }
            ByteBuffer vb=ByteBuffer.allocateDirect(4*vbSize);
            vb.order(ByteOrder.nativeOrder());
            obj3D.vertNorl=vb.asFloatBuffer();
            obj3D.vertNorl.put(vbArr);
            obj3D.vertNorl.position(0);
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    public static class Obj3D{
        public FloatBuffer vert;
        public int vertCount;
        public FloatBuffer vertNorl;
    }
}

模型渲染

模型的渲染,和之前绘制各种形体也差不多了,往GPU传数据就不用说了,为了让3D模型呈现出立体效果,示例中,增加了简单而不靠谱的光照。所以看得出来,虽然加载出来有立体效果,但是能看到比较明显的网格。当然,光照不是本篇博客的重点,在后续博客里面再详细讨论下光照的问题。
顶点Shader为:

attribute vec3 vPosition;
attribute vec2 vCoord;
uniform mat4 vMatrix;

varying vec2 textureCoordinate;

attribute vec3 vNormal;         //法向量
varying vec4 vDiffuse;          //用于传递给片元着色器的散射光最终强度


//返回散射光强度
vec4 pointLight(vec3 normal,vec3 lightLocation,vec4 lightDiffuse){
    //变换后的法向量
    vec3 newTarget=normalize((vMatrix*vec4(normal+vPosition,1)).xyz-(vMatrix*vec4(vPosition,1)).xyz);
    //表面点与光源的方向向量
    vec3 vp=normalize(lightLocation-(vMatrix*vec4(vPosition,1)).xyz);
    return lightDiffuse*max(0.0,dot(newTarget,vp));
}

void main(){
    gl_Position = vMatrix*vec4(vPosition,1);
    textureCoordinate = vCoord;

   vec4 at=vec4(1.0,1.0,1.0,1.0);   //光照强度
   vec3 pos=vec3(50.0,200.0,50.0);      //光照位置
   vDiffuse=pointLight(vNormal,pos,at);
}

片元Shader:

precision mediump float;
varying vec2 textureCoordinate;
uniform sampler2D vTexture;
varying vec4 vDiffuse;//接收从顶点着色器过来的散射光分量
void main() {
    vec4 finalColor=vec4(1.0);
    //给此片元颜色值
    gl_FragColor=finalColor*vDiffuse+finalColor*vec4(0.15,0.15,0.15,1.0);
}

着色器中散射光强度的计算,是根据散射光的公式来的,光照公式在Android OpenGLES2.0(一)——了解OpenGLES2.0光照中有讲到。

编译着色器,linkProgram,传入从Obj文件读取的值,然后和渲染一个立方体一样,渲染出模型就OK了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容