Friday, July 15, 2011

Android - Rendering 3D Blender Models (Part 1)



I decided I to create a simple OpenGL ES example for Android. I found many examples with hard coded values for a model, but I wanted to create the model in a 3D modeling tool and import it into my app. I wanted to do this using a model made in Blender due to my experience with the tool and its FREE! Here are the steps I took to accomplish this.

Exporting a 3D Model from Blender

1) Once your model is ready to export, select only the objects you want.
2) Go to: File -> Export -> Wavefront (.obj)
3) Next you will see a screen with an Export OBJ tab on the left. I found the most success only the following items selected:

- Selection Only
- Normals
- UVs
- Triangulate
- Objects as OBJ Objects
- Keep Vertex Order

4) Export your Wavefront file
5) Place the .obj files into your Android App's assets folder

Creating your OpenGL ES Android App Structure

After some research into OpenGL ES I became most comfortable with the following structure:




I will work from the bottom up while walking through this example.


Parsing a Wavefront File

Your Wavefront file should have the following format (learn more):
 // Vertices  
 v 1.797824 0.168228 5.381136  
 ...  
   
 // Normals  
 vn 0.000000 1.000000 0.000000  
 ...  
   
 // Faces  
 f 21//1 1//2 2//3  
 ...  

I found a great example of parsing a Wavefront file in the Earth Live Wallpaper project. The following is a stripped down version of their parsing method. I've stripped this down because my example does not include the object's textures.

1:  private void LoadObjFile() {  
2:              
3:      try   
4:      {  
5:           AssetManager am = mContext.getAssets();  
6:        String str;  
7:        String[] tmp;  
8:        String[] ftmp;  
9:        float v;  
10:        ArrayList<Float> vlist = new ArrayList<Float>();  
11:        ArrayList<Float> nlist = new ArrayList<Float>();  
12:        ArrayList<Fp> fplist = new ArrayList<Fp>();  
13:          
14:        BufferedReader inb = new BufferedReader(new InputStreamReader(am.open("androidmodel.obj")), 1024);  
15:          
16:        while ((str = inb.readLine()) != null)   
17:        {  
18:             tmp = str.split(" ");  
19:               
20:             //Parse the vertices  
21:             if (tmp[0].equalsIgnoreCase("v"))   
22:             {  
23:                  for (int i = 1; i < 4; i++)   
24:                  {  
25:                       v = Float.parseFloat(tmp[i]);  
26:              vlist.add(v);  
27:            }  
28:             }  
29:             //Parse the vertex normals  
30:          if (tmp[0].equalsIgnoreCase("vn"))   
31:          {  
32:               for (int i = 1; i < 4; i++)   
33:               {  
34:                    v = Float.parseFloat(tmp[i]);  
35:              nlist.add(v);  
36:            }  
37:          }  
38:          //Parse the faces/indices  
39:          if (tmp[0].equalsIgnoreCase("f"))   
40:          {  
41:               for (int i = 1; i < 4; i++)   
42:               {  
43:                    ftmp = tmp[i].split("/");  
44:                      
45:                    long chi = Integer.parseInt(ftmp[0]) - 1;  
46:                    int cht = 0;  
47:                    if(!ftmp[1].equals(""))  
48:                         cht = Integer.parseInt(ftmp[1]) - 1;  
49:              int chn = Integer.parseInt(ftmp[2]) - 1;  
50:    
51:              fplist.add(new Fp(chi, cht, chn));  
52:            }  
53:               NBFACES++;  
54:          }  
55:        }  
56:          
57:        ByteBuffer vbb = ByteBuffer.allocateDirect(fplist.size() * 4 * 3);  
58:        vbb.order(ByteOrder.nativeOrder());  
59:        mVertexBuffer = vbb.asFloatBuffer();  
60:    
61:        ByteBuffer nbb = ByteBuffer.allocateDirect(fplist.size() * 4 * 3);  
62:        nbb.order(ByteOrder.nativeOrder());  
63:        mNormBuffer = nbb.asFloatBuffer();  
64:    
65:        for (int j = 0; j < fplist.size(); j++)   
66:        {  
67:             mVertexBuffer.put(vlist.get((int) (fplist.get(j).Vi * 3)));  
68:          mVertexBuffer.put(vlist.get((int) (fplist.get(j).Vi * 3 + 1)));  
69:          mVertexBuffer.put(vlist.get((int) (fplist.get(j).Vi * 3 + 2)));  
70:    
71:          mNormBuffer.put(nlist.get(fplist.get(j).Ni * 3));  
72:          mNormBuffer.put(nlist.get((fplist.get(j).Ni * 3) + 1));  
73:          mNormBuffer.put(nlist.get((fplist.get(j).Ni * 3) + 2));  
74:        }  
75:          
76:        mIndexBuffer = CharBuffer.allocate(fplist.size());  
77:        for (int j = 0; j < fplist.size(); j++)   
78:        {  
79:             mIndexBuffer.put((char) j);  
80:        }  
81:          
82:        mVertexBuffer.position(0);  
83:        mNormBuffer.position(0);  
84:        mIndexBuffer.position(0);  
85:    
86:      } catch (FileNotFoundException e)   
87:      {  
88:           e.printStackTrace();  
89:      } catch (IOException e)   
90:      {  
91:           e.printStackTrace();  
92:      }  
93:       }  

Thursday, July 7, 2011

Android - Animations

Page Transitions

To override the current page transitions when switching activities you can use the overridePendingTransition method. The code snippet shows an example of using the pre-defined Android animations fade_in and fade_out. We need to provide resource ids to the overridePendingTransition method representing the incoming activity and outgoing activity animations.

 Intent i = new Intent(this, ListViewExampleActivity.class);  
 startActivity(i);  
 overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);  


View Animations

In this example we will make our own custom animation. The easiest way to create your own animation xml file in Eclipse follows:

1) Right click your project in the Package Explorer
2) New -> Android XML File
3) Select "Animation" as your type, this will place your xml into "res/anim"

The following code shows an example of a scale animation. This animation is using the bounce interpolator. This is much like easing functions in Silverlight. This causes the animation to have a bounce effect. The animation defines the from and to scales, starting at 0 and ending at 1. It also moves the X and Y pivots to 50% so that as the element scales up it scales from the center of the object. Finally the duration is set to 500 milliseconds.

 <?xml version="1.0" encoding="utf-8"?>  
 <set xmlns:android="http://schemas.android.com/apk/res/android"  
       android:interpolator="@android:anim/bounce_interpolator">  
   <scale   
       android:fromXScale="0.0" android:toXScale="1.0"   
       android:fromYScale="0.0" android:toYScale="1.0"  
       android:pivotX="50%" android:pivotY="50%"  
       android:fillAfter="true"   
       android:duration="500">  
   </scale>  
 </set>  

To apply this animation use the following code:

 Button animBttn = (Button)findViewById(R.id.AnimationButton);  
 animBttn.setOnClickListener(this);  
       
 Animation bttnAnim = AnimationUtils.loadAnimation(mContext, R.anim.button_animation);  
 animBttn.setAnimation(bttnAnim);  

If you would like to trigger the animation we can use the startAnimation method.

 animBttn.startAnimation(bttnAnim);  


List View Animations

This time we will use a translate animation.

 <?xml version="1.0" encoding="utf-8"?>  
 <set xmlns:android="http://schemas.android.com/apk/res/android"  
       android:interpolator="@android:anim/overshoot_interpolator" >  
      <translate   
           android:fromXDelta="-100%"  
           android:toXDelta="0%"  
           android:duration="500">  
      </translate>  
 </set>  

In order to apply this animation to individual list view items we must also create a layoutAnimation xml file.

 <?xml version="1.0" encoding="utf-8"?>  
 <layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"   
                  android:animation="@anim/listitem_animation"/>  

To attach the animation to the ListView we will use the setLayoutAnimation method.

 ListView lv = (ListView)findViewById(R.id.ExampleList);  
 lv.setAdapter(new ArrayAdapter<String>(this, R.layout.listitem, PLANETS));  
       
 //We need to load a LayoutAnimation instead of just an Animation   
 //to set the animation for each individual list item  
 LayoutAnimationController controller = AnimationUtils.loadLayoutAnimation(mContext,R.anim.list_layoutcontroller);  
 //This is the delay between each animation as a fraction of the animation duration  
 controller.setDelay(0.25f);  
 //Be sure to set the LayoutAnimation, using setAnimation will set the animation for the entire listview  
 lv.setLayoutAnimation(controller);  

java.lang.OutOfMemoryError: bitmap size exceeds VM

Recently while working on an Android app I started running into this error:

 07-07 10:59:14.519: ERROR/AndroidRuntime(20429): FATAL EXCEPTION: main  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429): java.lang.OutOfMemoryError: bitmap size exceeds VM budget  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:470)  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:284)  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:309)  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at android.graphics.drawable.Drawable.createFromPath(Drawable.java:800)    
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1611)  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1663)  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at android.app.ActivityThread.access$1500(ActivityThread.java:117)  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:931)  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at android.os.Handler.dispatchMessage(Handler.java:99)  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at android.os.Looper.loop(Looper.java:130)  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at android.app.ActivityThread.main(ActivityThread.java:3683)  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at java.lang.reflect.Method.invokeNative(Native Method)  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at java.lang.reflect.Method.invoke(Method.java:507)  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)  
 07-07 10:59:14.519: ERROR/AndroidRuntime(20429):   at dalvik.system.NativeStart.main(Native Method)  

This error was stemming from the large bitmaps I was switching out between activities. When I monitored the Heap Size for my app in the DDMS I was always well under the 16MB cap when the error occurred. I started manually running the garbage collector and even started scaling down my images to no avail. I found that this issue had been reported (Issue 8488) and promptly declined. In the bug report comments you can see that this issue is highly debated whether this is a bug or working as expected.

The best explanation I could find was that the Dalvik heap is used for your Java memory allocations and the Native heap is used for other things like your bitmaps. The DDMS is only reporting the Dalvik heap size. So when switching between activities and setting a new bitmap to an ImageView source I was inflating the total heap size beyond the cap. I was never nulling out the ImageView source references therefore eventually running out of memory.

I also found a nice solution in the comments for this issue. Here is a "BetterActivity" class that extends Activity. This handles nulling out all of the ImageViews to release the memory and running the garbage collector.

1:  import android.app.Activity;  
2:  import android.view.LayoutInflater;  
3:  import android.view.View;  
4:  import android.view.ViewGroup;  
5:  import android.view.ViewGroup.LayoutParams;  
6:  import android.widget.ImageView;  
7:    
8:  public abstract class BetterActivity extends Activity  
9:  {  
10:        private ViewGroup mContentView = null;  
11:          
12:        @Override  
13:        protected void onResume()  
14:        {  
15:             System.gc();  
16:             super.onResume();  
17:        }  
18:     
19:        @Override  
20:        protected void onPause()  
21:        {  
22:             super.onPause();  
23:             System.gc();  
24:        }  
25:     
26:        @Override  
27:        public void setContentView(int layoutResID)  
28:        {  
29:             ViewGroup mainView = (ViewGroup)LayoutInflater.from(this).inflate(layoutResID, null);  
30:             setContentView(mainView);  
31:        }  
32:     
33:        @Override  
34:        public void setContentView(View view)  
35:        {  
36:             super.setContentView(view);  
37:             mContentView = (ViewGroup)view;  
38:        }  
39:     
40:        @Override  
41:        public void setContentView(View view, LayoutParams params)  
42:        {  
43:             super.setContentView(view, params);  
44:             mContentView = (ViewGroup)view;  
45:        }  
46:     
47:        @Override  
48:        protected void onDestroy()  
49:        {  
50:             super.onDestroy();  
51:               
52:             nullViewDrawablesRecursive(mContentView);  
53:             mContentView = null;  
54:             System.gc();  
55:        }  
56:     
57:        private void nullViewDrawablesRecursive(View view)  
58:        {  
59:             if(view != null)  
60:             {  
61:               try  
62:               {  
63:                    ViewGroup viewGroup = (ViewGroup)view;  
64:                      
65:                    int childCount = viewGroup.getChildCount();  
66:                      
67:                    for(int index = 0; index < childCount; index++)  
68:                    {  
69:                         View child = viewGroup.getChildAt(index);  
70:                      nullViewDrawablesRecursive(child);  
71:                    }  
72:               }  
73:               catch(Exception e){}  
74:            
75:               nullViewDrawable(view);  
76:             }    
77:        }  
78:    
79:        private void nullViewDrawable(View view)  
80:        {  
81:             try  
82:             {  
83:                  view.setBackgroundDrawable(null);  
84:             }  
85:             catch(Exception e){}  
86:               
87:             try  
88:             {  
89:                  ImageView imageView = (ImageView)view;  
90:                  imageView.setImageDrawable(null);  
91:                  imageView.setBackgroundDrawable(null);  
92:             }  
93:             catch(Exception e){}  
94:        }  
95:  }  
96: