יום חמישי, 29 באוגוסט 2013

Android OpenGL UV Mapping





ראינו שלא פשוט לבנות קובייה ועוד יותר מסובך לצבוע אותה, אבל שוב חוזרים לאותה שאלה מה קורה כאשר האובייקט מורכב מיותר מ 6 משטחים? גם הפעם עולם התלת מימד נותן לנו כלים להתמודד עם אובייקטים מורכבים בעזרת שיטת ה UV Map שמאפשרת לצבוע אותם בצורה יעילה וחסכונית.

UV Map

שיטה לפריסת אובייקט תלת מימדי לדו מימד, עד עכשיו חיברנו משטח בודד לטקסטורה בודדת שזה לא הפתרון המושלם והוא בזבזני ולא ידידותי, בעזרת UV Coordinates ניתן לחבר מספר משטחים לנקודות ספציפיות בטקסטורה עצמה כך שכל משולש \ מרובע יצייר רק את התחום שלו, עוד יתרון מאוד חושב שניתן לשלב באותה טקסטורה מספר אבייקטים שונים ולחסוך מקום וקבצים.  

חבית בדו מימד
ניתן לראות שכל משטחי החבית נפרסים על על תמונה בודדת עם קואורדינטות עבור כל Vertex, התמונה תתלבש על האובייקט כשנטען אותה ל OpenGL, זו שיטה שמתאימה למשחקים ומאוד פופלרית, חשוב מאוד לדעת שיש חלק מאוד אומנותי שמצריך קישורי ציור בתהליך הכנת הטקסטורה ויש המון שיטות וטכניקות בקיצור עולם ומלואו ,מי שמעוניין להיכנס אליו יותר לעומק מומלץ למצוא תוכנת תלת מימד ולחפור אותה עד שיצא עשן ולמי שזה לא מתאים לא חסר מודלים שמוכנים מראש.


חבית בתוכנת תלת מימד


*.OBJ


לפני שניכנס לפורמט פעם נוספת חשוב להדגיש שחלק מתוכנות התלת מימד שומרות בקובץ את המידע של ה Vertex כמרובע ולא כמשולש, ברוב המקרים זה בסדר אבל בגלל שאנחנו עובדים על OpenGL ES שמותאם ל Android ולא ניתן לעבוד עם מרובעים, יש להמיר את האובייקט למשולשים ורק אח"כ לשמור את המידע לקובץ, במקרה שלי Modo שומרת את המשטחים כמרובעים ללא אפשרויות נוספות, נאלצתי להוריד סקריפט מיוחד שיעשה זאת, להורדת הסקריפט לחץ כאן.

קובץ ה Obj מכיל את המידע בנוגע לאובייקט, למען הסדר הטוב אני יחזור על דברים שנאמרו באחד המאמרים בנושא על מנת שנשמור על הרצף,בתחילת הקובץ מופיעות נקודות ה Vertex של האובייקט עצמו עם סימון V מדובר על X Y Z עבור כל נקודה, לאחר מכן מגיע רצף נקודות ה Normal שעליהם נפרט במאמרים הבאים.
v -0.0200 0.0400 -1.0200
vn -0.4004 -0.7179 -0.5695

לאחר הקואורדינטות של האובייקט עצמו מתחיל רצף הנקודות של הטקסטורה, בדומה למאמר האחרון בו מערך הקואורדינטות היה כחלק מהקוד במקרה הזה אנחנו שואבים אותו מהקובץ אבל קיים הבדל בהתייחסות בין הסטנדרט של OBJ לבין OpenGL, הקובץ מתייחס לקוארדינטה 0.0 ל Top Left בזמן ש OpenGL מתייחס לקוארדינטה כ Bottom Left לכן עלינו להפוך את הטקסטורה כפי שנראה בקוד.

vt 0.5646 0.9954

הפורמט Obj מכיל בתוכו קישור לקובץ ה Material שמכיל בתוכו קישור לטקסטורה ופרמטרים נוספים.

mtllib barrel1.mtl

בסוף הקובץ מופעים האינדקסים של המשטחים שנצייר על המסך, לכל משטח יש 3 אינדקסים שמצבעים על הנתונים במערכים שנטענו לפני שהגענו לאנידקסים (V,VN,VT) , ניתן לראות בדוגמה שלמשטח באינדקס 20 תואמות הקואורדינטות במיקום 1 במערך של ה Normal Vertex ולמיקום 18 במערך של ה Texture Vertex, אבל גם פה יש מגבלה של ה OpenGL, לא ניתן לעבוד עם יותר מאינדקס אחד ועלינו להכין מראש מערך מסודר שיטען ברצף את הקוארדינטות כפי שנראה בהמשך.

f 20/1/18 8/2/7 21/3/19


ObjectParser

התוכנית עוברת מתיחת פנים מהפעם האחרונה,נוספו מחלקות חדשות ומספר פונקציות נוספות על מנת להשלים את התהליך,  יש לסדר את האינדקסים ולהכין את ה Buffers לקראת הכנסה ל OpenGL, נא לעקוב אחרי ההערות בקוד.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.Vector;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import android.util.Log;

public class ObjectParser {

//support class
public class vtPoints {
public float V, T;
}

//support class
public class vnPoints {
public float N1, N2;
}

//support class
public class vLayer {
public float X, Y, Z;
public vtPoints vt = new vtPoints();
public vnPoints vn = new vnPoints();
}

//array contains all the sorted information
private ArrayList<vLayer> vSlice = new ArrayList<vLayer>();

// arrays contains all points (vertex/normal)
private ArrayList<vtPoints> vtSlice = new ArrayList<vtPoints>();
private ArrayList<vnPoints> vnSlice = new ArrayList<vnPoints>();

//arrays contains all the indices information
private Vector<Short> vPointer = new Vector<Short>();
private Vector<Short> vtPointer = new Vector<Short>();
private Vector<Short> vnPointer = new Vector<Short>();

//buffers to load to opengl
private FloatBuffer vertexBuffer;
private ShortBuffer faceBuffer;
private FloatBuffer textureBuffer;
private FloatBuffer normalBuffer;

//reader of the file
private BufferedReader reader;

private Context ctx;
private GL10 gl;
private float mCubeRotation;

//textures pointers array
private int texturesPointers[];

public ObjectParser(Context context, String _filename, GL10 _gl) {
gl = _gl;
ctx = context;
try {
reader = new BufferedReader(new InputStreamReader(ctx.getAssets()
.open(_filename)));
} catch (IOException e) {
e.printStackTrace();
}

loadFile();
sortArrays();
}

public void Draw() {

gl.glLoadIdentity();
gl.glTranslatef(0.1f, -0.1f, -10.0f);

// rotate the object
gl.glRotatef(mCubeRotation, 0.0f, 0.5f, 0.1f);
gl.glEnable(GL10.GL_TEXTURE_2D);

//load buffers
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
gl.glNormalPointer(GL10.GL_FLOAT, 0, normalBuffer);

gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);

if (texturesPointers[0] != 0) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, texturesPointers[0]);
}

gl.glDrawElements(GL10.GL_TRIANGLES, faceBuffer.capacity(),
GL10.GL_UNSIGNED_SHORT, faceBuffer);

gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
   gl.glDisableClientState(GL10.GL_NORMAL_ARRAY);

mCubeRotation -= 0.25f;
}

private void sortArrays() {
for (int i = 0; i < vPointer.size(); i++) {

// get the vertex by the obj face indices
vLayer _layer = (vLayer) vSlice.get(vPointer.get(i));

// get the texture coordinates from obj texture indices
vtPoints _vt = (vtPoints) vtSlice.get(vtPointer.get(i));

// get the normal coordinates from obj texture indices
vnPoints _vn = (vnPoints) vnSlice.get(vnPointer.get(i));

// update the vertex layer with the new coordinates
_layer.vt = _vt;
_layer.vn = _vn;

}

//rearrange buffers by indices
Vector<Float> v = new Vector<Float>();
Vector<Float> vt = new Vector<Float>();
Vector<Float> vn = new Vector<Float>();

for (int i = 0; i < vSlice.size(); i++) {

vLayer _layer = vSlice.get(i);

v.add(_layer.X);
v.add(_layer.Y);
v.add(_layer.Z);

vt.add(_layer.vt.V);
vt.add(_layer.vt.T);

vn.add(_layer.vn.N1);
vn.add(_layer.vn.N2);

}

loadBuffer(v, vt, vn, vPointer);

}

private void loadBuffer(Vector<Float> v, Vector<Float> vt,
Vector<Float> vn, Vector<Short> f) {

//buffer for vertex
ByteBuffer byteBuf = ByteBuffer.allocateDirect(v.size() * 4);
byteBuf.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuf.asFloatBuffer();
vertexBuffer.put(toPrimitiveArrayF(v));
vertexBuffer.position(0);

//buffer for texture
byteBuf = ByteBuffer.allocateDirect(vt.size() * 4);
byteBuf.order(ByteOrder.nativeOrder());
textureBuffer = byteBuf.asFloatBuffer();
textureBuffer.put(toPrimitiveArrayF(vt));
textureBuffer.position(0);

//buffer for normal
byteBuf = ByteBuffer.allocateDirect(vn.size() * 4 * 3);
byteBuf.order(ByteOrder.nativeOrder());
normalBuffer = byteBuf.asFloatBuffer();
normalBuffer.put(toPrimitiveArrayF(vn));
normalBuffer.position(0);

//buffer for indices -> faces
ByteBuffer fBuf = ByteBuffer.allocateDirect(f.size() * 2);
fBuf.order(ByteOrder.nativeOrder());
faceBuffer = fBuf.asShortBuffer();
faceBuffer.put(toPrimitiveArrayS(f));
faceBuffer.position(0);
}

private void loadFile() {
String line = null;
try {
while ((line = reader.readLine()) != null) {

// faces (3 indices)
if (line.startsWith("f")) {
processFace(line);
Log.d("Face Loaded", line);
}

//texture vertices
else if (line.startsWith("vt")) {
processVT(line);
Log.d("Texture coordinates Loaded", line);
}

//nornal vertices
else if (line.startsWith("vn")) {
processVN(line);
Log.d("Normal coordinates Loaded", line);
}

// vertices
else if (line.startsWith("v")) {
processV(line);
Log.d("Vetex Loaded", line);
}

//material
else if (line.startsWith("mtllib")) {
loadMaterial(line);
Log.d("Material Loaded", line);
}
}
} catch (IOException e) {
e.printStackTrace();
}

}

// load material file
private void loadMaterial(String line) {

String splitter[] = line.split(" ");
String filename = splitter[1];
BufferedReader textureReader;
try {
textureReader = new BufferedReader(new InputStreamReader(ctx
.getAssets().open(filename)));
String tLine = null;

while ((tLine = textureReader.readLine()) != null) {
if (tLine.startsWith("map_Kd")) {
loadTexture(tLine.split(" ")[1]);
}
}

} catch (IOException e) {
e.printStackTrace();
}

}

// load bitmap from the material file
private void loadTexture(String TextureMap) {
Bitmap tMap = null;
try {
// flip image because the different 
//sorting between OpenGL and OBJ standards
tMap = BitmapFactory.decodeStream(ctx.getAssets().open(TextureMap));
Matrix flip = new Matrix();
flip.postScale(1f, -1f);
Bitmap bitmap = Bitmap.createBitmap(tMap, 0, 0, tMap.getWidth(),
tMap.getHeight(), flip, true);
tMap = bitmap;
} catch (IOException e) {
e.printStackTrace();
return;
}

texturesPointers = new int[1];
GLES20.glGenTextures(texturesPointers.length, texturesPointers, 0);
gl.glBindTexture(GL10.GL_TEXTURE_2D, texturesPointers[0]);

// texture behavior
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER,
GL10.GL_LINEAR);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER,
GL10.GL_LINEAR);
gl.glTexParameterf(GLES20.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
GL10.GL_CLAMP_TO_EDGE);
gl.glTexParameterf(GLES20.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
GL10.GL_CLAMP_TO_EDGE);

// insert the bitmap into open openGL buffer
gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE,
GL10.GL_DECAL);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, tMap, 0);

// no need this anymore...
tMap.recycle();

}

// export indices
private void processFace(String line) {

String[] tokens = line.split("[ ]+");
int c = tokens.length;

Vector<Short> tmpFaces = new Vector<Short>();
Vector<Short> tmpVn = new Vector<Short>();
Vector<Short> tmpVt = new Vector<Short>();
for (int i = 1; i < tokens.length; i++) {

// vertices indices
Short s = Short.valueOf(tokens[i].split("/")[0]);
s--;
tmpFaces.add(s);

// texture indices
s = Short.valueOf(tokens[i].split("/")[1]);
s--;
tmpVt.add(s);

// normal indices
s = Short.valueOf(tokens[i].split("/")[2]);
s--;
tmpVn.add(s);

}
vPointer.addAll(tmpFaces);
vtPointer.addAll(tmpVt);
vnPointer.addAll(tmpVn);

}

// export vertex coordinates
private void processV(String line) {

// split space
String[] tokens = line.split("[ ]+");
vLayer _layer = new vLayer();

int c = tokens.length;
for (int i = 1; i < c; i++) {

switch (i) {
case 1:
_layer.X = Float.valueOf(tokens[i]);
break;
case 2:
_layer.Y = Float.valueOf(tokens[i]);
break;
case 3:
_layer.Z = Float.valueOf(tokens[i]);
break;
default:
break;
}

}
vSlice.add(_layer);
}

// export normal coordinates
private void processVN(String line) {
String[] tokens = line.split("[ ]+");
int c = tokens.length;
vnPoints _layer = new vnPoints();
for (int i = 1; i < c; i++) {

switch (i) {
case 1:
_layer.N1 = Float.valueOf(tokens[i]);
break;
case 2:
_layer.N2 = Float.valueOf(tokens[i]);
break;
default:
break;
}

}
vnSlice.add(_layer);
}

// export texture coordinates
private void processVT(String line) {
String[] tokens = line.split("[ ]+");
int c = tokens.length;
vtPoints _layer = new vtPoints();
for (int i = 1; i < c; i++) {

switch (i) {
case 1:
_layer.V = Float.valueOf(tokens[i]);
break;
case 2:
_layer.T = Float.valueOf(tokens[i]);
break;
default:
break;
}

}

vtSlice.add(_layer);
}

// convert Short Vector to Short Array
private short[] toPrimitiveArrayS(Vector<Short> vector) {
short[] s;
s = new short[vector.size()];
for (int i = 0; i < vector.size(); i++) {
s[i] = vector.get(i);
}
return s;
}

// convert Float Vector to Float Array
private float[] toPrimitiveArrayF(Vector<Float> vector) {
float[] f;
f = new float[vector.size()];
for (int i = 0; i < vector.size(); i++) {
f[i] = vector.get(i);
}
return f;
}

}


המחלקה מקבלת את שם הקובץ ופותחת אותו מתיקיית ה Assets לאחר מכן עוברת שורה שורה בקובץ (LoadFile) ומוצאת את הנתונים של ה Normal, Vertex ו Texture, אוספת את מיקום הטקסטורה מקובץ ה Material ולסיום אוספת את האינדקסים לאחר מכן מייצרת מערך מסוג vLayer עבור שורות ה Vertices , וטוענת לתוכו את הקוראדינטות של ה Normal וה Texture ע"פ האידקסים שלהם.

דוגמה מהמכשיר

חבית במכשיר

הורדת הפרוייקט


סיכום

ראינו כמה העסק מסובך אבל מאוד מאתגר, עכשיו אפשר לתכנן אובייקטים מורכבים ולצבוע אותם,ניתן להתחיל להכין את הסביבה עבור משחק התלת מימד הראשון שנעשה באנדרואיד.

בהצלחה.

יום שני, 12 באוגוסט 2013

Android OpenGL Texture





אחרי שלמדנו לטעון אובייקטים למנוע OpenGL במכשיר, השלב הבא הוא להכניס צבע למודלים אבל לפני שנקפוץ למים העמוקים נתחיל עם אובייקט שנראה פשוט כמו קובייה כבר ראינו שגם קובייה זה אובייקט מורכב במיוחד ב OpenGL אז כדאי להכיר אותו לפני שהולכים לאובייקטים מסובכים יותר.

לקובייה יש 6 משטחים שמורכבים מ 12 משולשים כבר דיברנו על זה במאמר הפתיחה, חשוב לחזור על זה כי בנוסף לנקודות ה Vertices נוספות  נקודות חדשות עבור הטקסטורה.



על מנת להבין את הרעיון יש להסתכל על כל משטח באופן עצמי לדוגמה ניקח את המשטח הראשון שאנחנו מציירים ונוסיף לנו נקודות ציון בסדר שאנחנו רוצים שהוא יפרוס את הטקסטורה על גבי המשטח בעזרת מערך ו Buffer שנוסיף לתהליך.

private float texture[] = {
// Mapping coordinates for the vertices
//FaceA
0.0f, 1.0f,//top left
1.0f, 1.0f,//top right
0.0f,0.0f, //bottom left
1.0f, 0.0f,//bottom right
          }


כפי שניתן לראות אנחנו מציבים במערך Texture את הנקודה השמאלית עליונה ואח"כ את הימנית העליונה ובעצם גורמים לטקסטורה להיות מצוירת מלמעלה ללמטה , אם נשנה את הסדר ונטען את הנקודה השמאלית העליונה עם השמאלית התחתונה נצייר את הטקסטורה משמאל לימין.

private float texture[] = {
// Mapping coordinates for the vertices
//FaceA
0.0f, 1.0f,//top left
0.0f,0.0f, //bottom left
1.0f, 1.0f,//top right
1.0f, 0.0f,//bottom right
        }



דוגמה נוספת למה קורה כאשר מציירים את הנקודות התחתונות ואח"כ את הנקודות העליונות.

private float texture[] = {
// Mapping coordinates for the vertices
//FaceA
0.0f,0.0f, //bottom left
1.0f, 0.0f,//bottom right
0.0f, 1.0f,//top left
1.0f, 1.0f,//top right
  }

את השאר תנחשו לבד


אפשר לצייר את הטקסטורה על המשטח עם כיוון השעון או נגד הכל תלוי בסדר הנקודות שנכנס ל Buffer עבור כל משטח כפי שניתן לראות במחלקה CubeTextue.



CubeTexture

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLES20;
import android.opengl.GLUtils;

public class CubeTexture {

private GL10 gl;
private Context ctx;
private FloatBuffer mVertexBuffer;
private FloatBuffer mTextureBuffer;
private ByteBuffer mIndexBuffer;
private int texturesPointers[];
private Bitmap texturesMaps[];
private float mCubeRotation;
private float vertices[] = {
// Vertices according to faces
//X     Y      Z
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
1.0f,1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f,
1.0f, 1.0f,1.0f,
1.0f, 1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
-1.0f,-1.0f, 1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, 1.0f,
-1.0f, -1.0f,-1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, -1.0f,
1.0f,1.0f, -1.0f, 
};

private float texture[] = {
// Mapping coordinates for the vertices
//FaceA
0.0f, 1.0f,//top left
1.0f, 1.0f,//top right
0.0f,0.0f, //bottom left
1.0f, 0.0f,//bottom right
//Face B
0.0f, 1.0f,
1.0f, 1.0f,
0.0f,0.0f,
1.0f, 0.0f,
//Face C
0.0f, 1.0f,
1.0f, 1.0f,
0.0f,0.0f,
1.0f, 0.0f,
//Face D
0.0f, 1.0f,
1.0f, 1.0f,
0.0f,0.0f,
1.0f, 0.0f,
//Face E
0.0f, 1.0f,
1.0f, 1.0f,
0.0f,0.0f,
1.0f, 0.0f,
//Face F
0.0f, 1.0f,
1.0f, 1.0f,
0.0f,0.0f,
1.0f, 0.0f,
};

private byte indices[] = {
// Faces definition
0, 1, 3, 0, 3, 2, 4, 5, 7, 4, 7, 6, 8, 9, 11, 8, 11, 10, 12, 13,
15, 12, 15, 14, 16, 17, 19, 16, 19, 18, 20, 21, 23, 20, 23, 22, };
public CubeTexture(Context _ctx, GL10 _gl)
{
ctx = _ctx;
gl = _gl;
init();
}
//load bitmaps into openGL
private void loadTextures()
{
//for each face of the cube
texturesMaps = new Bitmap[6];
try {
//using the assets folder
texturesMaps[0] = BitmapFactory.decodeStream(ctx.getAssets().open("sideA.jpg"));
texturesMaps[1] = BitmapFactory.decodeStream(ctx.getAssets().open("sideB.jpg"));
texturesMaps[2] = BitmapFactory.decodeStream(ctx.getAssets().open("sideC.jpg"));
texturesMaps[3] = BitmapFactory.decodeStream(ctx.getAssets().open("sideD.jpg"));
texturesMaps[4] = BitmapFactory.decodeStream(ctx.getAssets().open("sideE.jpg"));
texturesMaps[5] = BitmapFactory.decodeStream(ctx.getAssets().open("sideF.jpg"));
} catch (IOException e) {
e.printStackTrace();
}
//create pointers to texture buffer
texturesPointers = new int[texturesMaps.length];
//generate the texture in the opengl
GLES20.glGenTextures(texturesPointers.length, texturesPointers, 0);

for (int i = 0; i < texturesMaps.length; i++) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, texturesPointers[i]);

//texture behavior 
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER,
GL10.GL_LINEAR);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER,
GL10.GL_LINEAR);
gl.glTexParameterf(GLES20.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
GL10.GL_CLAMP_TO_EDGE);
gl.glTexParameterf(GLES20.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
GL10.GL_CLAMP_TO_EDGE);

//insert the bitmap into openGL buffer
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texturesMaps[i], 0);
//no need anymore
texturesMaps[i].recycle();
}
}
private void init()
{
// buffer for vertex coordinates
ByteBuffer byteBuf = ByteBuffer.allocateDirect(vertices.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
mVertexBuffer = byteBuf.asFloatBuffer();
mVertexBuffer.put(vertices);
mVertexBuffer.position(0);

  // buffer for textures coordinates
byteBuf = ByteBuffer.allocateDirect(texture.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
mTextureBuffer = byteBuf.asFloatBuffer();
mTextureBuffer.put(texture);
mTextureBuffer.position(0);

   // buffer for faces
mIndexBuffer = ByteBuffer.allocateDirect(indices.length);
mIndexBuffer.put(indices);
mIndexBuffer.position(0);

loadTextures();

}
public void Draw()
{
gl.glLoadIdentity();
gl.glTranslatef(0.1f, 0.0f, -10.0f);
gl.glRotatef(mCubeRotation, 1.0f, 1.0f, 1.0f);
gl.glFrontFace(GL10.GL_CW);

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mVertexBuffer);
gl.glEnable(GL10.GL_TEXTURE_2D);

gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mTextureBuffer);

//texture for each face of the cube
for (int i = 0; i < 6; ++i) {

if (texturesPointers[i] != 0) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, texturesPointers[i]);
}
mIndexBuffer.position(6 * i); 
gl.glDrawElements(GL10.GL_TRIANGLES, 6, GL10.GL_UNSIGNED_BYTE,
mIndexBuffer);

}

gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glDisable(GL10.GL_TEXTURE_2D);

mCubeRotation -= -0.25f;
}
}

טוענים 6 תמונות מתיקיית ה Assets לטיפוסים של Bitmap, מייצרים Buffer בזיכרון של ה OpenGL ובעזרת מערך של  Pointers מצביעים לטקסטורות, מכניסים את ה Bitmap ל Buffer ומשחררים את  ה Bitmap מהזיכרון, בשלב ה Draw מחלקים את המשטחים וטוענים לכל משטח את הטקסטורה מהזיכרון שהגדרנו.

להורדת התוכנית:


סיכום:

אז כמו תמיד מתחילים מהדברים הפשוטים בשלב הבא נטען את נתוני הטקסטורה מקובץ ה OBJ ונשכלל את ה ObjectParser מהמאמר הקודם.

בהצלחה...

יום שבת, 10 באוגוסט 2013

Android OpenGL Complex Models




במאמר הקודם שיחקנו עם אובייקטים כמו קובייה ומשולש ולמרות שהם צורות בסיסיות עדיין העסק מסובך ואם זה מסובך בשלב כזה התחלתי מה קורה עם אובייקטים יותר מורכבים?, כאן נחלץ לעזרתנו עולם התלת מימד שבעזרת תוכנות מתוחכמות מאפשר לנו לתכנן אובייקטים מורכבים ואח"כ לטעון אותם למנוע ה OpenGL שבאפליקציה.


הכנה מקדימה

לפני שנתחיל להיכנס לקוד עלינו להעזר בתוכנה תלת מימד, רובן עובדות באותה צורה אבל מסובכות מאוד , זו סביבה תלת מימדית מלאה משלב המודל עד שלב האנימציה וה Render לכן כדאי לחפש אחת שהיא נוחה או לקנות ספר טוב, בכל מקרה את האובייקט יצרתי ב Modo של חברת Luxology , אני חושב שזו התוכנה הכי ידידותית שיש כרגע בשוק, אבל תמיד אפשר לחפש ב Google אובייקטים מוכנים יש בשפע.


גלגל שיניים

 *.OBJ

פורמט ותיק מתחום התלת מימד שנקרא Wavefront OBJ, ניתן ליצא אותו בכל תוכנת תלת מימד שבשוק, זה קובץ טקסטואלי שמכיל בתוכו את נתוני ה Vertex ו Textures, מבנה הקובץ פשוט, הוא מורכב משורות שמכילות את המידע על האובייקט לדוגמה רצף של Vertices מהגלגל שיניים.

//V for Vertices 
v -0.0179763 -0.000869437 -1.04022
v -0.0910632 0.00717378 -1.04074
v -0.161893 0.00603062 -1.02813
v -0.232611 0.00575502 -1.01982
v -0.301366 0.00604349 -1.00169
v -0.371174 0.00721471 -0.987629
//....


הפורמט מכיל מידע על ה  Normal Vertices שהם דומים ל Vertices רק שבעזרתם ניתן להפוך את האובייקט ליותר חלק (Smooth), נקודת Vertex אחת יכולה להיות מחוברת ליותר ממשטח אחד, Normal Vertices היא נקודה ממוצעת של ה Vertices שמתחברים בין המשטחים.

//VN for Vertices Normal
vn -0.175242 0 -0.984525
vn -0.146028 0 -0.98928
vn -0.186303 0 -0.982492
vn -0.226327 0 -0.974051
vn -0.197461 0 -0.980311
vn -0.629326 0 -0.777142
//....


המשטחים של ה Vertices על פיו נקבע סדר הצביעה במסך.

//F for Faces
f 98//1 2//1 3//2 99//2
f 99//2 3//2 4//3 100//3
f 100//3 4//3 5//4 101//4
f 101//4 5//4 6//5 102//5
f 106//6 10//6 11//7 107//7
//....

מידע שקשור ל Texture של האובייקט.

//VT for Vertices Texture 
vt 0.466116 0.136309
vt 0.497293 0.162254
vt 0.477739 0.17729
vt 0.458501 0.145891
vt 0.450565 0.182356
vt 0.443936 0.147743
//....

קיימות אפשרויות נוספות בפורמט , ניתן להצביע לקובץ ה Material עצמו שנוצר ברגע שמייצאים את האובייקט מתוכנת התלת מימד, שימו לב - האפשרות הזו לא קיימת בכל התוכנות.

אחרי שהכרנו את המבנה הבסיסי של קובץ OBJ השלב הבא הוא לכתוב Parser שאוסף את המידע מהקובץ ומעביר אותו למנגנונים של ה OpenGL בדומה למאמר הקודם, אבל לפני שנתחיל חשוב לייצר בפרויקט תיקיית Assets שלתוכה נעתיק את הקובץ.


ObjectParser

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import java.util.Vector;
import javax.microedition.khronos.opengles.GL10;
import android.util.Log;

public class ObjectParser {

private Vector<Short> faces=new Vector<Short>();
private Vector<Float> v=new Vector<Float>();
private FloatBuffer vertexBuffer;
private ShortBuffer faceBuffer;
private FloatBuffer mColorBuffer;
private BufferedReader reader;
private GL10 gl;
private float mCubeRotation;
float[] colors;

public ObjectParser(InputStreamReader stream, GL10 _gl)
{
gl = _gl;
reader = new BufferedReader(stream);
loadFile();
loadBuffer();
}
public void Draw()
{
gl.glLoadIdentity();
gl.glTranslatef(0.1f, 0.0f, -5.0f);
// rotate the cube
gl.glRotatef(mCubeRotation, 1.0f, 1.0f, 1.0f);

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
gl.glColorPointer(4, GL10.GL_FLOAT, 0, mColorBuffer);
gl.glDrawElements(GL10.GL_TRIANGLES, faceBuffer.capacity(),
GL10.GL_UNSIGNED_SHORT,faceBuffer);
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
mCubeRotation -= 0.25f;
}
//create color for each vertex
private void createColorBuffer()
{
colors = new float[v.size() * 4];
float f = 1.0f;
for (int i = 0; i < v.size() * 4;i++)
{
//0f, 1f, 0f, 1f
colors[i] = 0f;
i++;
colors[i] = f;
i++;
colors[i] = 0f;
i++;
colors[i] = 1f;
f= f - 0.0004f;
}
}
private void loadBuffer()
{
createColorBuffer();
ByteBuffer byteBuf = ByteBuffer.allocateDirect(v.size() * 4);
byteBuf.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuf.asFloatBuffer();
vertexBuffer.put(toPrimitiveArrayF(v));
vertexBuffer.position(0);
byteBuf = ByteBuffer.allocateDirect(colors.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
mColorBuffer = byteBuf.asFloatBuffer();
mColorBuffer.put(colors);
mColorBuffer.position(0);
ByteBuffer fBuf = ByteBuffer.allocateDirect(faces.size() * 2);
fBuf.order(ByteOrder.nativeOrder());
faceBuffer = fBuf.asShortBuffer();
faceBuffer.put(toPrimitiveArrayS(faces));
faceBuffer.position(0);
mCubeRotation = -0.25f;
}
    //read object file and process lines
    private  void loadFile()
{
String line = null;
try {
while((line = reader.readLine()) != null)
{
//polygon faces
if(line.startsWith("f")){
processFace(line);
Log.d("Face Loaded", line);
}
//vertices
if(line.startsWith("v")){
processV(line);
Log.d("Vetex Loaded", line);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
    
    //export vertices from line 
    private void processV(String line){
   
           //split spaces
String [] tokens=line.split("[ ]+"); 
int c=tokens.length; 
for(int i=1; i<c; i++){ 
//add vertex to array
v.add(Float.valueOf(tokens[i]));
}
}
    
    //export polygon face from line and convert to triangles
private void processFace(String line){
String [] tokens=line.split("[ ]+");
int c=tokens.length;
Vector<Short> tmpFaces=new Vector<Short>();
Vector<Short> tmpVn=new Vector<Short>();
for(int i=1; i<tokens.length; i++){
Short s=Short.valueOf(tokens[i].split("//")[0]);
s--;
tmpFaces.add(s);
s=Short.valueOf(tokens[i].split("//")[1]);
s--;
tmpVn.add(s);
}
faces.addAll(triangulate(tmpFaces));
}

//convert polygon to triangles.
public  Vector<Short> triangulate(Vector<Short> polygon){
Vector<Short> triangles=new Vector<Short>();
for(int i=1; i<polygon.size()-1; i++){
triangles.add(polygon.get(0));
triangles.add(polygon.get(i));
triangles.add(polygon.get(i+1));
}
return triangles;
}
//convert Vector to Float Array
private float[] toPrimitiveArrayF(Vector<Float> vector) {
float[] f;
f = new float[vector.size()];
for (int i = 0; i < vector.size(); i++) {
f[i] = vector.get(i);
}
return f;
}
//convert Short Vector to Short Array
private static short[] toPrimitiveArrayS(Vector<Short> vector){
short[] s;
s=new short[vector.size()];
for (int i=0; i<vector.size(); i++){
s[i]=vector.get(i);
}
return s;
}
}


המחלקה מקבלת את המידע כ Stream ומתחילה לקרוא שורה, שורה בעזרת ה Reader , אם השורה מתחילה עם האות V אנחנו מעבדים את השורה כ Vertex ואם השורה מתחילה עם האות F מעבדים את השורה כ Face, ממלאים 2 מערכים ואח"כ מייצרים מהם Buffers שטוענים ל OpenGL.

MyRender

import java.io.IOException;
import java.io.InputStreamReader;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
import android.opengl.GLSurfaceView.Renderer;

public class MyRender extends GLSurfaceView implements Renderer {

private Context ctx;
private ObjectParser _model;
public MyRender(Context context) {
super(context);
ctx = context;
}

@Override
public void onDrawFrame(GL10 arg0) {
_model.Draw();
}

@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 / (float) height, 0.1f,
100.0f);
gl.glViewport(0, 0, width, height);
//enter to ModelView Projection Mode Settings
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
}

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig arg1) {
gl.glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
gl.glClearDepthf(1.0f);
gl.glEnable(GL10.GL_DEPTH_TEST);
gl.glDepthFunc(GL10.GL_LEQUAL);
gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
try {
//loading object from assets folder
_model = new ObjectParser(new InputStreamReader(ctx.getAssets().open("gear1.obj")),gl);
} catch (IOException e) {
e.printStackTrace();
}
}
}

בעזרת פונקצית getAssets נוכל לקרוא קבצים ישירות מהתיקייה בניגוד לעבודה עם Resources שצריכים לעדכן את התוכנית בכל קובץ חדש שנוסף לפרויקט.


תמונה מהמכשיר:



הורדת הפרויקט:


סיכום:

עד עכשיו ראינו כיצד לטעון אובייקטים ל OpenGL ואפשר להגיד שזה החלק הפשוט, השלב הבא הרבה יותר מורכב, עולם הטקסטורות הוא סוג של אומנות ודורש מיומנויות מיוחדות שעליהן נעבור במאמרים הבאים.

בהצלחה...