Android OpenGLES2.0(十六)——3D模型贴图及光照处理(obj+mtl)

在Android OpenGLES2.0(十四)——Obj格式3D模型加载中实现了Obj格式的3D模型的加载,加载的是一个没有贴图,没有光照处理的帽子,为了呈现出立体效果,“手动”加了光照,拥有贴图的纹理及光照又该怎么加载呢?

模型文件

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


obj内容格式如下:

# Wavefront OBJ file
# Exported by Misfit Model 3D 1.3.8
# Thu Sep 27 20:02:52 2012

mtllib pikachu.mtl

# 191 Vertices
v 34.493484 75.31411 -39.308891
v 27.34606 45.516556 -47.155548
#...省略若干行
vt 0.859513 0.676464
vt 0.769048 0.0597
#...省略若干行
vn -0.068504 -0.433852 -0.898376
vn 0.422088 -0.855411 -0.30019
#...省略若干行
usemtl pikagen
o DrawCall_25
g DrawCall_25

f 2/1/1 1/2/2 3/3/3
f 1/4/4 2/5/5 4/6/6
#...省略若干行
usemtl pikagen
o DrawCall_262
g DrawCall_262

f 2/58/58 3/59/59 17/60/60
f 2/61/61 17/62/62 6/63/63
#...省略若干行

mtl文件内容格式如下:

# Material file for pikachu.obj

newmtl eye
    Ns 0
    d 1
    illum 2
    Kd 0.8 0.8 0.8
    Ks 0.0 0.0 0.0
    Ka 0.2 0.2 0.2
    map_Kd eye1.png

newmtl mouth
    Ns 0
    d 1
    illum 2
    Kd 0.8 0.8 0.8
    Ks 0.0 0.0 0.0
    Ka 0.2 0.2 0.2
    map_Kd mouth1.png

newmtl pikagen
    Ns 0
    d 1
    illum 2
    Kd 0.8 0.8 0.8
    Ks 0.0 0.0 0.0
    Ka 0.2 0.2 0.2
    map_Kd pikagen.png

关于Obj的内容格式,在上篇博客中已经做了总结,本篇博客中使用的obj,可以看到f后面的不再跟的是4个数字,而是f 2/58/58 3/59/59 17/60/60这种样子的三组数,每一组都表示为顶点坐标索引/贴图坐标点索引/顶点法线索引,三个顶点组成一个三角形。而头部的mtllib pikachu.mtl则指明使用的材质库。
而mtl格式文件中,主要数据类型为:

newmtl name #name为材质名称
Ns exponent #exponent指定材质的反射指数,定义了反射高光度

Ka r g b    #环境光反射,g和b两参数是可选的,如果只指定了r的值,则g和b的值都等于r的值
Kd r g b    #漫反射
Ks r g b    #镜面光反射
# Ka Kd Ks 都还有其他两种格式,可查阅其他资料:
#Kd spectral file.rfl factor
#Kd xyz x y z

map_Kd picture.png  #固有纹理贴图
map_Ka picture1.png #阴影纹理贴图
map_Ks picture2.png #高光纹理贴图
illum 2             #光照模型
#光照模型属性如下:
#0. 色彩开,阴影色关
#1. 色彩开,阴影色开
#2. 高光开
#3. 反射开,光线追踪开
#4. 透明: 玻璃开 反射:光线追踪开
#5. 反射:菲涅尔衍射开,光线追踪开
#6. 透明:折射开 反射:菲涅尔衍射关,光线追踪开
#7. 透明:折射开 反射:菲涅尔衍射开,光线追踪开
#8. 反射开,光线追踪关
#9. 透明: 玻璃开 反射:光线追踪关
#10. 投射阴影于不可见表面

模型及贴图加载

模型加载和之前的模型加载大同小异,不同的是,这次我们需要将模型的贴图坐标、顶点法线也一起加载,并传入到shader中。其他参数,有的自然也要取到。
模型加载以obj文件为入口,解析obj文件,从中获取到mtl文件相对路径,然后解析mtl文件。将材质库拆分为诸多的单一材质。obj对象的 加载,根据具使用材质不同来分解为多个3D模型。具体加载过程如下:

建立保存单个材质的类

public class MtlInfo {

    //还有其他相关信息,需要的时候一起添加进来

    public String newmtl;
    public float[] Ka=new float[3];     //阴影色
    public float[] Kd=new float[3];     //固有色
    public float[] Ks=new float[3];     //高光色
    public float[] Ke=new float[3];     //
    public float Ns;                    //shininess
    public String map_Kd;               //固有纹理贴图
    public String map_Ks;               //高光纹理贴图
    public String map_Ka;               //阴影纹理贴图

    //denotes the illumination model used by the material.
    // illum = 1 indicates a flat material with no specular highlights,
    // so the value of Ks is not used.
    // illum = 2 denotes the presence of specular highlights,
    // and so a specification for Ks is required.
    public int illum;
}

建立保存拥有单一材质的3D对象的类

public class Obj3D {
    public FloatBuffer vert;
    public int vertCount;
    public FloatBuffer vertNorl;
    public FloatBuffer vertTexture;

    public MtlInfo mtl;

    private ArrayList<Float> tempVert;
    private ArrayList<Float> tempVertNorl;
    public ArrayList<Float> tempVertTexture;

    public int textureSMode;
    public int textureTMode;

    public void addVert(float d){
        if(tempVert==null){
            tempVert=new ArrayList<>();
        }
        tempVert.add(d);
    }

    public void addVertTexture(float d){
        if(tempVertTexture==null){
            tempVertTexture=new ArrayList<>();
        }
        tempVertTexture.add(d);
    }

    public void addVertNorl(float d){
        if(tempVertNorl==null){
            tempVertNorl=new ArrayList<>();
        }
        tempVertNorl.add(d);
    }

    public void dataLock(){
        if(tempVert!=null){
            setVert(tempVert);
            tempVert.clear();
            tempVert=null;
        }
        if(tempVertTexture!=null){
            setVertTexture(tempVertTexture);
            tempVertTexture.clear();
            tempVertTexture=null;
        }
        if(tempVertNorl!=null){
            setVertNorl(tempVertNorl);
            tempVertNorl.clear();
            tempVertNorl=null;
        }
    }

    public void setVert(ArrayList<Float> data){
        int size=data.size();
        ByteBuffer buffer=ByteBuffer.allocateDirect(size*4);
        buffer.order(ByteOrder.nativeOrder());
        vert=buffer.asFloatBuffer();
        for (int i=0;i<size;i++){
            vert.put(data.get(i));
        }
        vert.position(0);
        vertCount=size/3;
    }

    public void setVertNorl(ArrayList<Float> data){
        int size=data.size();
        ByteBuffer buffer=ByteBuffer.allocateDirect(size*4);
        buffer.order(ByteOrder.nativeOrder());
        vertNorl=buffer.asFloatBuffer();
        for (int i=0;i<size;i++){
            vertNorl.put(data.get(i));
        }
        vertNorl.position(0);
    }

    public void setVertTexture(ArrayList<Float> data){
        int size=data.size();
        ByteBuffer buffer=ByteBuffer.allocateDirect(size*4);
        buffer.order(ByteOrder.nativeOrder());
        vertTexture=buffer.asFloatBuffer();
        for (int i=0;i<size;){
            vertTexture.put(data.get(i));
            i++;
            vertTexture.put(data.get(i));
            i++;
        }
        vertTexture.position(0);
    }

}

实现材质库的解析方法

public static HashMap<String,MtlInfo> readMtl(InputStream stream){
    HashMap<String,MtlInfo> map=new HashMap<>();
    try{
        InputStreamReader isr=new InputStreamReader(stream);
        BufferedReader br=new BufferedReader(isr);
        String temps;
        MtlInfo mtlInfo=new MtlInfo();
        while((temps=br.readLine())!=null)
        {
            String[] tempsa=temps.split("[ ]+");
            switch (tempsa[0].trim()){
                case "newmtl":  //材质
                    mtlInfo=new MtlInfo();
                    mtlInfo.newmtl=tempsa[1];
                    map.put(tempsa[1],mtlInfo);
                    break;
                case "illum":     //光照模型
                    mtlInfo.illum=Integer.parseInt(tempsa[1]);
                    break;
                case "Kd":
                    read(tempsa,mtlInfo.Kd);
                    break;
                case "Ka":
                    read(tempsa,mtlInfo.Ka);
                    break;
                case "Ke":
                    read(tempsa,mtlInfo.Ke);
                    break;
                case "Ks":
                    read(tempsa,mtlInfo.Ks);
                    break;
                case "Ns":
                    mtlInfo.Ns=Float.parseFloat(tempsa[1]);
                case "map_Kd":
                    mtlInfo.map_Kd=tempsa[1];
                    break;
            }
        }
    }catch (Exception e){
        e.printStackTrace();
    }
    return map;
}

private static void read(String[] value,ArrayList<Float> list){
    for (int i=1;i<value.length;i++){
        list.add(Float.parseFloat(value[i]));
    }
}

private static void read(String[] value,float[] fv){
    for (int i=1;i<value.length&&i<fv.length+1;i++){
        fv[i-1]=Float.parseFloat(value[i]);
    }
}

实现3D对象拆分解析的方法

public static List<Obj3D> readMultiObj(Context context,String file){
    boolean isAssets;
    ArrayList<Obj3D> data=new ArrayList<>();
    ArrayList<Float> oVs=new ArrayList<Float>();//原始顶点坐标列表
    ArrayList<Float> oVNs=new ArrayList<>();    //原始顶点法线列表
    ArrayList<Float> oVTs=new ArrayList<>();    //原始贴图坐标列表
    HashMap<String,MtlInfo> mTls=null;
    HashMap<String,Obj3D> mObjs=new HashMap<>();
    Obj3D nowObj=null;
    MtlInfo nowMtl=null;
    try{
        String parent;
        InputStream inputStream;
        if (file.startsWith("assets/")){
            isAssets=true;
            String path=file.substring(7);
            parent=path.substring(0,path.lastIndexOf("/")+1);
            inputStream=context.getAssets().open(path);
            Log.e("obj",parent);
        }else{
            isAssets=false;
            parent=file.substring(0,file.lastIndexOf("/")+1);
            inputStream=new FileInputStream(file);
        }
        InputStreamReader isr=new InputStreamReader(inputStream);
        BufferedReader br=new BufferedReader(isr);
        String temps;
        while((temps=br.readLine())!=null){
            if("".equals(temps)){

            }else{
                String[] tempsa=temps.split("[ ]+");
                switch (tempsa[0].trim()){
                    case "mtllib":  //材质
                        InputStream stream;
                        if (isAssets){
                            stream=context.getAssets().open(parent+tempsa[1]);
                        }else{
                            stream=new FileInputStream(parent+tempsa[1]);
                        }
                        mTls=readMtl(stream);
                        break;
                    case "usemtl":  //采用纹理
                        if(mTls!=null){
                            nowMtl=mTls.get(tempsa[1]);
                        }
                        if(mObjs.containsKey(tempsa[1])){
                            nowObj=mObjs.get(tempsa[1]);
                        }else{
                            nowObj=new Obj3D();
                            nowObj.mtl=nowMtl;
                            mObjs.put(tempsa[1],nowObj);
                        }
                        break;
                    case "v":       //原始顶点
                        read(tempsa,oVs);
                        break;
                    case "vn":      //原始顶点法线
                        read(tempsa,oVNs);
                        break;
                    case "vt":
                        read(tempsa,oVTs);
                        break;
                    case "f":
                        for (int i=1;i<tempsa.length;i++){
                            String[] fs=tempsa[i].split("/");
                            int index;
                            if(fs.length>0){
                                //顶点索引
                                index=Integer.parseInt(fs[0])-1;
                                nowObj.addVert(oVs.get(index*3));
                                nowObj.addVert(oVs.get(index*3+1));
                                nowObj.addVert(oVs.get(index*3+2));
                            }
                            if(fs.length>1){
                                //贴图
                                index=Integer.parseInt(fs[1])-1;
                                nowObj.addVertTexture(oVTs.get(index*2));
                                nowObj.addVertTexture(oVTs.get(index*2+1));
                            }
                            if(fs.length>2){
                                //法线索引
                                index=Integer.parseInt(fs[2])-1;
                                nowObj.addVertNorl(oVNs.get(index*3));
                                nowObj.addVertNorl(oVNs.get(index*3+1));
                                nowObj.addVertNorl(oVNs.get(index*3+2));
                            }
                        }
                        break;
                }
            }
        }
    }catch (Exception e){
        e.printStackTrace();
    }
    for (Map.Entry<String, Obj3D> stringObj3DEntry : mObjs.entrySet()) {
        Obj3D obj = stringObj3DEntry.getValue();
        data.add(obj);
        obj.dataLock();
    }
    return data;
}

顶点着色器及片元着色器

顶点着色器

attribute vec3 vPosition;
attribute vec2 vCoord;
uniform mat4 vMatrix;
uniform vec3 vKa;
uniform vec3 vKd;
uniform vec3 vKs;

varying vec2 textureCoordinate;

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

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

    vec3 lightLocation=vec3(0.0,-200.0,-500.0);      //光照位置
    vec3 camera=vec3(0,200.0,0);
    float shininess=10.0;             //粗糙度,越小越光滑

    vec3 newNormal=normalize((vMatrix*vec4(vNormal+vPosition,1)).xyz-(vMatrix*vec4(vPosition,1)).xyz);
    vec3 vp=normalize(lightLocation-(vMatrix*vec4(vPosition,1)).xyz);
    vDiffuse=vec4(vKd,1.0)*max(0.0,dot(newNormal,vp));                //计算散射光的最终强度

    vec3 eye= normalize(camera-(vMatrix*vec4(vPosition,1)).xyz);
    vec3 halfVector=normalize(vp+eye);    //求视线与光线的半向量
    float nDotViewHalfVector=dot(newNormal,halfVector);   //法线与半向量的点积
    float powerFactor=max(0.0,pow(nDotViewHalfVector,shininess));     //镜面反射光强度因子
    vSpecular=vec4(vKs,1.0)*powerFactor;               //计算镜面光的最终强度

    vAmbient=vec4(vKa,1.0);
}

片元着色器

precision mediump float;
varying vec2 textureCoordinate;
uniform sampler2D vTexture;
varying vec4 vDiffuse;          //接收从顶点着色器过来的散射光分量
varying vec4 vAmbient;          //接收传递给片元着色器的环境光分量
varying vec4 vSpecular;         //接收传递给片元着色器的镜面光分量
void main() {
    vec4 finalColor=texture2D(vTexture,textureCoordinate);
    gl_FragColor=finalColor*vAmbient+finalColor*vSpecular+finalColor*vDiffuse;
}

启动加载及渲染

完成了以上准备工作,就可以调用readMultiObj方法,将obj文件读成一个或多个带有各项参数的3D模型类,然后将每一个3D模型的参数传入shader中,进而进行渲染:

List<Obj3D> model=ObjReader.readMultiObj(this,"assets/3dres/pikachu.obj");
List<ObjFilter2> filters=new ArrayList<>();
for (int i=0;i<model.size();i++){
    ObjFilter2 f=new ObjFilter2(getResources());
    f.setObj3D(model.get(i));
    filters.add(f);
}
mGLView.setRenderer(new GLSurfaceView.Renderer() {
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        for (ObjFilter2 f:filters){
            f.create();
        }
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        for (ObjFilter2 f:filters){
            f.onSizeChanged(width, height);
            float[] matrix= Gl2Utils.getOriginalMatrix();
            Matrix.translateM(matrix,0,0,-0.3f,0);
            Matrix.scaleM(matrix,0,0.008f,0.008f*width/height,0.008f);
            f.setMatrix(matrix);
        }
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
        for (ObjFilter2 f:filters){
            Matrix.rotateM(f.getMatrix(),0,0.3f,0,1,0);
            f.draw();
        }
    }
});
mGLView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容