Android OpenGLES2.0(十六)——3D模型貼圖及光照處理(obj+mtl)

湖廣午王發表於2017-02-27

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);

OK,至此大功告成。

原始碼

所有的程式碼全部在一個專案中,託管在Github上——Android OpenGLES 2.0系列部落格的Demo


歡迎轉載,轉載請保留文章出處。湖廣午王的部落格[http://blog.csdn.net/junzia/article/details/58272305]


相關文章