יום שלישי, ינואר 4

גרפיקה תלת מימד\openGL:הקוביה ההונגרית


אנדרואיד מספק כמה תוכניות מעניינות לדוגמא. אחת מהן היא הקוביה ההונגרית. שמעתי טענות  על כך שהתוכניות אינן מלוות בשום הסברים ולעיתים קשה לעקוב וללמוד מהן. החלטתי להרים את הכפפה ולהציג את התוכנית +ההסברים.
לקבלת רקע על openGL מומלץ לעיין בפוסט הקודם - גרפיקה בתלת מימד\openGL: חלק 1.

תמונה 1: צילום מסך תוך כדי סיבוב המשטח התחתון.




רקע
אסקור כאן דגשים בפעולת המערכת. בפרקים הבאים אחזור לנושאים המוזכרים כאן ביתר פרוט.
  • התוכנית מציגה מערכת עם 27 קוביות (cubes).
  • כל קוביה בנויה מ-6 פנים (faces), סהכ 128 פנים.
  • כל אחד מהפנים בנוי מ-4 קודקודים. לכל אחד מהפנים מוצמד צבע - אחד מששת הצבעים בהתאם לפאת הקוביה בה הוא נמצא.
  • לכל קודקוד 3 קואורדינטות - x,y,z, שמתארות את מיקומו במרחב.
  • Buffers- נשתמש בשלושה buffers מסוג nio:
  • Vertex Buffer  - כאן נשמרים 3 הקואורדינטות עבור כל הקודקודים של כל הפנים.
  • Color Buffer - לכל אחד מהקודקודים מוצמד צבע (ARGB), בהתאם לפנים אליהן הוא שייך. עבור כל קודקוד נשמרים 4 ערכי הצבע ב-buffer הזה, והם נשלפים יחד עם הקודקוד בזמן הציור.
  • Index Buffer - עבור כל אחד מהפנים, יש כאמור 4 קודקודים ב- Vertex Buffer. את הריבוע נצייר בעזרת שני משולשים, כך שאם למשל קודקודי הריבוע מאוחסנים ב-Vertex Buffer בכתובות: 1,2,3,4, האינדקסים ש-Index Buffer יכולים להיות:
    • 1
    • 2
    • 3
    • 2
    • 3
    • 4
  • Layer - לקוביה (הגדולה) שלושה צירי סיבוב: x,y,x. בכל אחד מהצירים ישנם 3 "משטחים" אותם אפשר לסובב. אלה ה-Layers. ישנם סהכ 9 "Layers". לכל אחד מה-Layers נצמיד מראש את הקוביות ששיכות אליו, כך שבזמן הסיבוב של אותו layer נוכל לטפל בכל קוביה בהתאם.
  • סיבוב ה-Layer- כל סיבוב הוא של 90 מעלות או pi/2 בכל פעם. רק layer אחד יכול להסתובב בכל רגע נתון. פעולת הסיבוב ב-90 מעלות נמשכת 25 מחזורים, היות שה-mAngleIncrement נקבע להיות pi/50. בכל אחת מהפעמים נעשות הפעולות הבאות ( מתחילים ב- (setAngle(mCurrentAngle שמופעלת מתוך animate, שמופעלת בכל מחזור ציור :
    • איטרציה על כל הקוביות השייכות ל-layer ופעולה על כל אחד כפי שמתואר בנקודה הבאה.
    • ביצוע animateTransform על כל אחת מהקוביות הנ"ל. ה-פרמטר של המתודה היא מטריצת סיבוב הקורדינטות, בהתאם לזוית הסיבוב (במקרה שלנו למשל, הזוית מתחילה ב-pi/50 וגדלה עד pi/2). המטריצה היא בגודל 3*3 , והיא תסובב את הקואורדינטות x,y,z בהתאם. מדובר בפעולה טריגונומטרית פשוטה, שתוסבר בפירוט בהמשך.
    • פרוט animateTransform:
      • איטרציה על רשימת הקודקודים שמוצמדת לאותה קוביה, וקריאה ל- transformVertex. הקריאה נעשית עם הפרמטרים הבאים: מטריצת הסיבוב הנ"ל, וה-Vertex Buffer. האינדקס כל קודקוד יסייע בשליפתו לצורך עידכונו (הכפלתו במטריצת הסיבוב). הקריאה היא למתודה vertex.update
      • ב-update הנ"ל נשלפים 3 הקואורדינטות של הקודקוד מה-buffer, מכפילים אותם במטרית הסיבוב, ומחזירים אותם למקומם.
    • התהליך הזה כאמור נמשך במנות של pi/50, עד שמסתיים הסיבוב ב-pi/2. נראה את הסיום בהתקיים התנאי:  if ((mAngleIncrement > 0f && mCurrentAngle >= mEndAngle) ||
                       (mAngleIncrement < 0f && mCurrentAngle <= mEndAngle)). הסיבוב אצלנו הוא בכיוון אחד בלבד - זוית שלילית - נגד כ השעון. 
    • בסיבוב אמנם נעו הקוביות ששיכות ל-layer מסוים, אבל כל קוביה שייכת ליותר מ-layer אחד. למעשה היא שייכת לשלושה layers. עקב התזוזה של הקוביות הנ"ל, יש לשייך אותן עכשיו ל-layers מחדש. הפעולה הזו נעשית במתודה-updateLayers.
    • על המתודה updateLayers:  כפי שתואר למעלה, יש לעדכן את השיוך של הקוביות ל-layers, בהתאם למצב אחרי הסיבוב. למה לעדכן את ה-layer? כי בתנועת הסיבוב הבאה של כל layer נצטרך לדעת אילו קוביות משויכות אליו. עדכון השיוך נעשה בעזרת mLayerPermutations.
    • mLayerPermutations: זוהי מטריצה בעלת 9 שורות. כל שורה מתייחסת ל-layer. אורכה של כל שורה הוא 27, איבר עבור כל אחת מהקוביות. השורות הנ"ל מתארות את התזוזה של כל אחת מהקוביות שתיגרם כתוצאה מסיבוב אותו layer ב-pi/2. בתהליך ה-updateLayers שנעשה ב-init וגם בכל פעם אחרי השלמת סיבוב,
      • הוקטור mPermutation שגודלו 27 מחזיק את הקישור בין אוביקטי הוקביות (שמתקשר בסופו של דבר ל-buffers לקודקודים ולצבעים) לבין המיקום בקוביה. בסוף כל סיבוב מעדכנים את הוקטור הזה ע"י הוקטור של ה-layer שהסתובב:
            for (int i = 0; i < 27; i++) {
                newPermutation[i] = mPermutation[mCurrentLayerPermutation[i]];
             }
             mPermutation = newPermutation;
             updateLayers();
  •  כעת בעזרת בתוך ה-updateLayers נעדכן את רשימת ה-cubes של כל -layer לפי mPermutation.
עד כאן רקע כללי. הנקודות הנ"ל עוד יטחנו בהמשך, ואני מסתכן שיאשימו אותי על עודף חזרות.




Kube - ה-Activity הראשי.

זהו השלד של המערכת.
  • מכאן ניזום תהליך האיתחול - החל ב-onCreate.
  • וכאן ממומש ה- callback שמטפל בסיבוב ה-layers של הקוביה - animate.
  נציג את ה-class ואחכ נדון בפרטיו.

public class Kube extends Activity implements KubeRenderer.AnimationCallback {

    private GLWorld makeGLWorld()
    {
        GLWorld world = new GLWorld();

        int one = 0x10000;
        int half = 0x08000;
        GLColor red = new GLColor(one, 0, 0);
        GLColor green = new GLColor(0, one, 0);
        GLColor blue = new GLColor(0, 0, one);
        GLColor yellow = new GLColor(one, one, 0);
        GLColor orange = new GLColor(one, half, 0);
        GLColor white = new GLColor(one, one, one);
        GLColor black = new GLColor(0, 0, 0);

        // coordinates for our cubes
        float c0 = -1.0f;
        float c1 = -0.38f;
        float c2 = -0.32f;
        float c3 = 0.32f;
        float c4 = 0.38f;
        float c5 = 1.0f;

        // top back, left to right
        mCubes[0]  = new Cube(world, c0, c4, c0, c1, c5, c1);
        mCubes[1]  = new Cube(world, c2, c4, c0, c3, c5, c1);
        mCubes[2]  = new Cube(world, c4, c4, c0, c5, c5, c1);
        // top middle, left to right
        mCubes[3]  = new Cube(world, c0, c4, c2, c1, c5, c3);
        mCubes[4]  = new Cube(world, c2, c4, c2, c3, c5, c3);
        mCubes[5]  = new Cube(world, c4, c4, c2, c5, c5, c3);
        // top front, left to right
        mCubes[6]  = new Cube(world, c0, c4, c4, c1, c5, c5);
        mCubes[7]  = new Cube(world, c2, c4, c4, c3, c5, c5);
        mCubes[8]  = new Cube(world, c4, c4, c4, c5, c5, c5);
        // middle back, left to right
        mCubes[9]  = new Cube(world, c0, c2, c0, c1, c3, c1);
        mCubes[10] = new Cube(world, c2, c2, c0, c3, c3, c1);
        mCubes[11] = new Cube(world, c4, c2, c0, c5, c3, c1);
        // middle middle, left to right
        mCubes[12] = new Cube(world, c0, c2, c2, c1, c3, c3);
        mCubes[13] = null;
        mCubes[14] = new Cube(world, c4, c2, c2, c5, c3, c3);
        // middle front, left to right
        mCubes[15] = new Cube(world, c0, c2, c4, c1, c3, c5);
        mCubes[16] = new Cube(world, c2, c2, c4, c3, c3, c5);
        mCubes[17] = new Cube(world, c4, c2, c4, c5, c3, c5);
        // bottom back, left to right
        mCubes[18] = new Cube(world, c0, c0, c0, c1, c1, c1);
        mCubes[19] = new Cube(world, c2, c0, c0, c3, c1, c1);
        mCubes[20] = new Cube(world, c4, c0, c0, c5, c1, c1);
        // bottom middle, left to right
        mCubes[21] = new Cube(world, c0, c0, c2, c1, c1, c3);
        mCubes[22] = new Cube(world, c2, c0, c2, c3, c1, c3);
        mCubes[23] = new Cube(world, c4, c0, c2, c5, c1, c3);
        // bottom front, left to right
        mCubes[24] = new Cube(world, c0, c0, c4, c1, c1, c5);
        mCubes[25] = new Cube(world, c2, c0, c4, c3, c1, c5);
        mCubes[26] = new Cube(world, c4, c0, c4, c5, c1, c5);

        // paint the sides
        int i, j;
        // set all faces black by default
        for (i = 0; i < 27; i++) {
            Cube cube = mCubes[i];
            if (cube != null) {
                for (j = 0; j < 6; j++)
                    cube.setFaceColor(j, black);
            }
        }

        // paint top
        for (i = 0; i < 9; i++)
            mCubes[i].setFaceColor(Cube.kTop, orange);
        // paint bottom
        for (i = 18; i < 27; i++)
            mCubes[i].setFaceColor(Cube.kBottom, red);
        // paint left
        for (i = 0; i < 27; i += 3)
            mCubes[i].setFaceColor(Cube.kLeft, yellow);
        // paint right
        for (i = 2; i < 27; i += 3)
            mCubes[i].setFaceColor(Cube.kRight, white);
        // paint back
        for (i = 0; i < 27; i += 9)
            for (j = 0; j < 3; j++)
                mCubes[i + j].setFaceColor(Cube.kBack, blue);
        // paint front
        for (i = 6; i < 27; i += 9)
            for (j = 0; j < 3; j++)
                mCubes[i + j].setFaceColor(Cube.kFront, green);

        for (i = 0; i < 27; i++)
            if (mCubes[i] != null)
                world.addShape(mCubes[i]);

        // initialize our permutation to solved position
        mPermutation = new int[27];
        for (i = 0; i < mPermutation.length; i++)
            mPermutation[i] = i;

        createLayers();
        updateLayers();

        world.generate();

        return world;
    }

    private void createLayers() {
        mLayers[kUp] = new Layer(Layer.kAxisY);
        mLayers[kDown] = new Layer(Layer.kAxisY);
        mLayers[kLeft] = new Layer(Layer.kAxisX);
        mLayers[kRight] = new Layer(Layer.kAxisX);
        mLayers[kFront] = new Layer(Layer.kAxisZ);
        mLayers[kBack] = new Layer(Layer.kAxisZ);
        mLayers[kMiddle] = new Layer(Layer.kAxisX);
        mLayers[kEquator] = new Layer(Layer.kAxisY);
        mLayers[kSide] = new Layer(Layer.kAxisZ);
    }

    private void updateLayers() {
        Layer layer;
        GLShape[] shapes;
        int i, j, k;

        // up layer
        layer = mLayers[kUp];
        shapes = layer.mShapes;
        for (i = 0; i < 9; i++)
            shapes[i] = mCubes[mPermutation[i]];

        // down layer
        layer = mLayers[kDown];
        shapes = layer.mShapes;
        for (i = 18, k = 0; i < 27; i++)
            shapes[k++] = mCubes[mPermutation[i]];

        // left layer
        layer = mLayers[kLeft];
        shapes = layer.mShapes;
        for (i = 0, k = 0; i < 27; i += 9)
            for (j = 0; j < 9; j += 3)
                shapes[k++] = mCubes[mPermutation[i + j]];

        // right layer
        layer = mLayers[kRight];
        shapes = layer.mShapes;
        for (i = 2, k = 0; i < 27; i += 9)
            for (j = 0; j < 9; j += 3)
                shapes[k++] = mCubes[mPermutation[i + j]];

        // front layer
        layer = mLayers[kFront];
        shapes = layer.mShapes;
        for (i = 6, k = 0; i < 27; i += 9)
            for (j = 0; j < 3; j++)
                shapes[k++] = mCubes[mPermutation[i + j]];

        // back layer
        layer = mLayers[kBack];
        shapes = layer.mShapes;
        for (i = 0, k = 0; i < 27; i += 9)
            for (j = 0; j < 3; j++)
                shapes[k++] = mCubes[mPermutation[i + j]];

        // middle layer
        layer = mLayers[kMiddle];
        shapes = layer.mShapes;
        for (i = 1, k = 0; i < 27; i += 9)
            for (j = 0; j < 9; j += 3)
                shapes[k++] = mCubes[mPermutation[i + j]];

        // equator layer
        layer = mLayers[kEquator];
        shapes = layer.mShapes;
        for (i = 9, k = 0; i < 18; i++)
            shapes[k++] = mCubes[mPermutation[i]];

        // side layer
        layer = mLayers[kSide];
        shapes = layer.mShapes;
        for (i = 3, k = 0; i < 27; i += 9)
            for (j = 0; j < 3; j++)
                shapes[k++] = mCubes[mPermutation[i + j]];
    }

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        // We don't need a title either.
        requestWindowFeature(Window.FEATURE_NO_TITLE);

        mView = new GLSurfaceView(getApplication());
        mRenderer = new KubeRenderer(makeGLWorld(), this);
        mView.setRenderer(mRenderer);
        setContentView(mView);
    }

    @Override
    protected void onResume()
    {
        super.onResume();
        mView.onResume();
    }

    @Override
    protected void onPause()
    {
        super.onPause();
        mView.onPause();
    }

    public void animate() {
         // change our angle of view
        mRenderer.setAngle(mRenderer.getAngle() + 1.2f);

        if (mCurrentLayer == null) {
            int layerID = mRandom.nextInt(9);
            mCurrentLayer = mLayers[layerID];
            mCurrentLayerPermutation = mLayerPermutations[layerID];
            mCurrentLayer.startAnimation();
            boolean direction = mRandom.nextBoolean();
            int count = mRandom.nextInt(3) + 1;

            count = 1;
            direction = false;
            mCurrentAngle = 0;
             if (direction) {
                mAngleIncrement = (float)Math.PI / 50;
                   mEndAngle = mCurrentAngle + ((float)Math.PI * count) / 2f;
               } else {
                mAngleIncrement = -(float)Math.PI / 50;
                   mEndAngle = mCurrentAngle - ((float)Math.PI * count) / 2f;
            }
        }

         mCurrentAngle += mAngleIncrement;

         if ((mAngleIncrement > 0f && mCurrentAngle >= mEndAngle) ||
                 (mAngleIncrement < 0f && mCurrentAngle <= mEndAngle)) {
             mCurrentLayer.setAngle(mEndAngle);
             mCurrentLayer.endAnimation();
             mCurrentLayer = null;

             // adjust mPermutation based on the completed layer rotation
             int[] newPermutation = new int[27];
             for (int i = 0; i < 27; i++) {
                newPermutation[i] = mPermutation[mCurrentLayerPermutation[i]];
 //                newPermutation[i] = mCurrentLayerPermutation[mPermutation[i]];
             }
             mPermutation = newPermutation;
             updateLayers();

         } else {
             mCurrentLayer.setAngle(mCurrentAngle);
         }
    }

    GLSurfaceView mView;
    KubeRenderer mRenderer;
    Cube[] mCubes = new Cube[27];
    // a Layer for each possible move
    Layer[] mLayers = new Layer[9];
    // permutations corresponding to a pi/2 rotation of each layer about its axis
    static int[][] mLayerPermutations = {
            // permutation for UP layer
            { 2, 5, 8, 1, 4, 7, 0, 3, 6, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 },
            // permutation for DOWN layer
            { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20, 23, 26, 19, 22, 25, 18, 21, 24 },
            // permutation for LEFT layer
            { 6, 1, 2, 15, 4, 5, 24, 7, 8, 3, 10, 11, 12, 13, 14, 21, 16, 17, 0, 19, 20, 9, 22, 23, 18, 25, 26 },
            // permutation for RIGHT layer
            { 0, 1, 8, 3, 4, 17, 6, 7, 26, 9, 10, 5, 12, 13, 14, 15, 16, 23, 18, 19, 2, 21, 22, 11, 24, 25, 20 },
            // permutation for FRONT layer
            { 0, 1, 2, 3, 4, 5, 24, 15, 6, 9, 10, 11, 12, 13, 14, 25, 16, 7, 18, 19, 20, 21, 22, 23, 26, 17, 8 },
            // permutation for BACK layer
            { 18, 9, 0, 3, 4, 5, 6, 7, 8, 19, 10, 1, 12, 13, 14, 15, 16, 17, 20, 11, 2, 21, 22, 23, 24, 25, 26 },
            // permutation for MIDDLE layer
            { 0, 7, 2, 3, 16, 5, 6, 25, 8, 9, 4, 11, 12, 13, 14, 15, 22, 17, 18, 1, 20, 21, 10, 23, 24, 19, 26 },
            // permutation for EQUATOR layer
            { 0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 14, 17, 10, 13, 16, 9, 12, 15, 18, 19, 20, 21, 22, 23, 24, 25, 26 },
            // permutation for SIDE layer
            { 0, 1, 2, 21, 12, 3, 6, 7, 8, 9, 10, 11, 22, 13, 4, 15, 16, 17, 18, 19, 20, 23, 14, 5, 24, 25, 26 }
    };



    // current permutation of starting position
    int[] mPermutation;

    // for random cube movements
    Random mRandom = new Random(System.currentTimeMillis());
    // currently turning layer
    Layer mCurrentLayer = null;
    // current and final angle for current Layer animation
    float mCurrentAngle, mEndAngle;
    // amount to increment angle
    float mAngleIncrement;
    int[] mCurrentLayerPermutation;

    // names for our 9 layers (based on notation from http://www.cubefreak.net/notation.html)
    static final int kUp = 0;
    static final int kDown = 1;
    static final int kLeft = 2;
    static final int kRight = 3;
    static final int kFront = 4;
    static final int kBack = 5;
    static final int kMiddle = 6;
    static final int kEquator = 7;
    static final int kSide = 8;

}
תקציר המתודות שבתוך Kube:

  • makeGLWorld - בניית הקוביה, ויצירת כל האובייקטים. המתודה מופעלת פעם אחת בהתחלה מתוך onCreate.
  • createLayers- מתודת עזר של makeGLWorld. יוצרת את 9 האובייקטים של ה-layers.
  •  updateLayers - מבצעת את הקישור בין ה-cubes לבין המיקום שלבם על גבי הקוביה הגדולה. הקישורים משתנים אחרי כל סיבוב של Layer. מופעלת בהתחלה ועם כל סיום של סיבוב של layer - ב 90 מעלות.
  • onCreate - יוצרת את ה-GLSurfaceView, מחברת את ה-renderer, ומפעילה את GLWorld.
  • onResume ו- onPause - את ה-callbacks האלה של ה-Activity LifeCycle צריך כזכור לממש כשעובדים עם openGlView.
  • animate - מופעלת בכל onFrameDraw. אחראית על האנימציה - סיבוב ה-layers.

המתודות של Kube ביתר פרוט
 נתחיל לפי הסדר:
onCreate


הנה המתודה שוב:


   @Override
  1.     protected void onCreate(Bundle savedInstanceState)
  2.     {
  3.         super.onCreate(savedInstanceState);

  4.         // We don't need a title either.
  5.         requestWindowFeature(Window.FEATURE_NO_TITLE);

  6.         mView = new GLSurfaceView(getApplication());
  7.         mRenderer = new KubeRenderer(makeGLWorld(), this);
  8.         mView.setRenderer(mRenderer);
  9.         setContentView(mView);
  10.     }

הסבר למתודה onCreate זהה כמעט לחלוטין - עד כמה שזכור לי - ניתן בפוסט הראשון בנושא openGL.
נשים לב לשורה 9 - הפעלת ה-constructor של ה-renederer. הפרמטר הראשון, מופעלת המתודה makeGlWorld. היא מחזירה את אובייקט מסוג GlWorld שמחזיק בין השאר את שלושת ה-nio Buffers, ואת הרשימות של הקודקודים VertexList והצורות ShapeList, , עליהם גם נרחיב בהמשך.

makeGlWorld - הזכרנו אותה כבר מספר פעמים. המתודה מנהלת את בניית האלמנטים הגרפיים. היא בונה את האובייקט world שמחזיק את ה-buffers.
היא מורכבת משישה חלקים - כל חלק צבוע בצבע שונה, בנוסף לשלוש מתודות עזר אותן היא מפעילה בסוף.
אם ננסה להגדיר את התהליכים שמופעלים במתודה, הרי שהיא יוצרת 27 אובייקטים מסוג Cube. בתהליך היצירה מתבצע מאין רקורסיה שבו מתוך כל cube יוצרים שישה אובייקטים של face ומתוך כל face 4 אובייקטים של vertex , ומכניסים אותם לbuffer.

 המשך ההסבר בין שורות הקוד.




    private GLWorld makeGLWorld()
    {
יצירת ואיתחול אובייקט מסוג GLWorld. ניצור במערכת אובייקט אחד כזה, וכבר נאמר שהוא מכיל את ה-buffers ואת רשימת ה-Vertex. בהמשך כשניצור את אובייקטי ה cubes, ה-constructor של כל אחד מהם  יקבל את world  כפרמטר.


        GLWorld world = new GLWorld();

המשך עבודת ההכנה:
יצירת שישה אובייקטים של צבע. שישה צבעים עבור ששת פאות הקוביה הגדולה. נראה בהמשך, שאובייקט צבע יוצמד  לכל vertex, וישמר ב-color buffer כך שכל face של הקוביה יצבע בצבע המתאים.


        int one = 0x10000;
        int half = 0x08000;
        GLColor red = new GLColor(one, 0, 0);
        GLColor green = new GLColor(0, one, 0);
        GLColor blue = new GLColor(0, 0, one);
        GLColor yellow = new GLColor(one, one, 0);
        GLColor orange = new GLColor(one, half, 0);
        GLColor white = new GLColor(one, one, one);
        GLColor black = new GLColor(0, 0, 0);


כעת ניצור את 27 הקוביות. תחילה מוגדרות 6 קווארדינטות במרחב.



        // coordinates for our cubes
        float c0 = -1.0f;
        float c1 = -0.38f;
        float c2 = -0.32f;
        float c3 = 0.32f;
        float c4 = 0.38f;
        float c5 = 1.0f;


נתבונן באחת מפאות הקוביה הגדולה ונראה את הקואורדינטות הנ"ל על גביה.

בניית ה- Cubes - ראה קוד למטה-: ה-prototype של ה- Cube Constructor נראה כך:
public Cube(GLWorld world, float left, float bottom, float back, float right, float top, float front)
חוץ הפרמטר הראשון - world - עליו דיברנו, נמסרות שש קואורדינטות עבור כל אחת מ27 הקוביות הקטנות לפי הסדר הבא:
המרחק של הפאה השמאלית ממרכז הקוביה הגדולה על ציר x.
המרחק של הפאה התחתונה ממרכז הקוביה הגדולה על ציר y.
המרחק של הפאה האחורית ממרכז הקוביה הגדולה על ציר z.
המרחק של הפאה הימנית ממרכז הקוביה הגדולה על ציר x.
המרחק של הפאה העליונה ממרכז הקוביה הגדולה על ציר y.
המרחק של הפאה הקדמית ממרכז הקוביה הגדולה על ציר z.

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

לקבלת אוריינטציה מרחבית - אם ניקח כדוגמא את הקוביה הראשונה - [cube[0 - הרי היא ממוקמת למעלה משמאל מאחור. בצד שמאל קואורדינטת x שלילית, קואורדינטת z שלילית, ואילו y חיובית.
המיספור של הקוביות מתחיל מלמעלה בצד השמאלי אחורי. הקוביה עם האינדקס הגדול ביותר - 124 - נמצאת ב-layer התחתון בצד הקדמי ימני.
נעזוב כרגע את ה-Cube constructor. נוסיף פרטים לגביו בהמשך בסעיף על ה- Cube class.



        // top back, left to right
        mCubes[0]  = new Cube(world, c0, c4, c0, c1, c5, c1);
        mCubes[1]  = new Cube(world, c2, c4, c0, c3, c5, c1);
        mCubes[2]  = new Cube(world, c4, c4, c0, c5, c5, c1);
        // top middle, left to right
        mCubes[3]  = new Cube(world, c0, c4, c2, c1, c5, c3);
        mCubes[4]  = new Cube(world, c2, c4, c2, c3, c5, c3);
        mCubes[5]  = new Cube(world, c4, c4, c2, c5, c5, c3);
        // top front, left to right
        mCubes[6]  = new Cube(world, c0, c4, c4, c1, c5, c5);
        mCubes[7]  = new Cube(world, c2, c4, c4, c3, c5, c5);
        mCubes[8]  = new Cube(world, c4, c4, c4, c5, c5, c5);
        // middle back, left to right
        mCubes[9]  = new Cube(world, c0, c2, c0, c1, c3, c1);
        mCubes[10] = new Cube(world, c2, c2, c0, c3, c3, c1);
        mCubes[11] = new Cube(world, c4, c2, c0, c5, c3, c1);
        // middle middle, left to right
        mCubes[12] = new Cube(world, c0, c2, c2, c1, c3, c3);
        mCubes[13] = null;
        mCubes[14] = new Cube(world, c4, c2, c2, c5, c3, c3);
        // middle front, left to right
        mCubes[15] = new Cube(world, c0, c2, c4, c1, c3, c5);
        mCubes[16] = new Cube(world, c2, c2, c4, c3, c3, c5);
        mCubes[17] = new Cube(world, c4, c2, c4, c5, c3, c5);
        // bottom back, left to right
        mCubes[18] = new Cube(world, c0, c0, c0, c1, c1, c1);
        mCubes[19] = new Cube(world, c2, c0, c0, c3, c1, c1);
        mCubes[20] = new Cube(world, c4, c0, c0, c5, c1, c1);
        // bottom middle, left to right
        mCubes[21] = new Cube(world, c0, c0, c2, c1, c1, c3);
        mCubes[22] = new Cube(world, c2, c0, c2, c3, c1, c3);
        mCubes[23] = new Cube(world, c4, c0, c2, c5, c1, c3);
        // bottom front, left to right
        mCubes[24] = new Cube(world, c0, c0, c4, c1, c1, c5);
        mCubes[25] = new Cube(world, c2, c0, c4, c3, c1, c5);
        mCubes[26] = new Cube(world, c4, c0, c4, c5, c1, c5);


הצמדת הצבע לפאות של הקוביות הקטנות:
הקוד בהמשך, תחילה הסבר.
הצמדת הצבע לכל face ולמעשה לכל קודוד נעשית ע"י הכנסת הצבע למקום המתאים ב-color buffer. ברקורסיה מגיעים מהקוביה עד האובייקטים של הקודקודים שלה, ומכניסים את הצבע במקום המתאים, לפי האינדקס של הקודקוד, בבפר של הצבעים. תחילה נצבע הכל בשחור. בהמשך נצבע ת הפאות החיצוניות בששת הצבעים. ניצור אובייקט cube זמני לצורך הכנסת הצבע השחור. לא נשתמש באובייקט הזה יותר.

        // paint the sides
        int i, j;
        // set all faces black by default
        for (i = 0; i < 27; i++) {
            Cube cube = mCubes[i];
            if (cube != null) {
                for (j = 0; j < 6; j++)
                    cube.setFaceColor(j, black);
            }
        }


כעת נצמיד את הצבעים לקוביות הקטנות. כל אחד מהצבעים מחובר ל9 פאות של תשע קוביות קטנות. ה-loop הראשון למשל (ראה למטה ברקע כתום), צובע את הפאה העליונה של הקוביה הגדולה בכתום. לצורך זה הוא מצמיד את הצבע לפאה העליונה של קוביות 0-8 (Cube.kTop).


        // paint top
        for (i = 0; i < 9; i++)
            mCubes[i].setFaceColor(Cube.kTop, orange);
        // paint bottom
        for (i = 18; i < 27; i++)
            mCubes[i].setFaceColor(Cube.kBottom, red);
        // paint left
        for (i = 0; i < 27; i += 3)
            mCubes[i].setFaceColor(Cube.kLeft, yellow);
        // paint right
        for (i = 2; i < 27; i += 3)
            mCubes[i].setFaceColor(Cube.kRight, white);
        // paint back
        for (i = 0; i < 27; i += 9)
            for (j = 0; j < 3; j++)
                mCubes[i + j].setFaceColor(Cube.kBack, blue);
        // paint front
        for (i = 6; i < 27; i += 9)
            for (j = 0; j < 3; j++)
                mCubes[i + j].setFaceColor(Cube.kFront, green);


נסתכל על המתודה setFaceColor שבתוך ה-Cube class (או ליתר דיוק בתוך ה-GlShape class שהוא ה-superclass של Cube, :

public void setFaceColor(int face, GLColor color) {
        mFaceList.get(face).setColor(color);
    }

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


ולחלק הבא באותה מתודה: נוסיף את כל הקוביות לרשימה בתוך אובייקט הworld תוך שימוש במתודה addShape. למה זה טוב? כשנרצה לעדכן את ה-buffers למשל, נעשה איטרציה על הרשימה, נסרוק את כל הקוביות, נוכל להוציא מכולן, אחת אחת את האינקסים של הקודקודים למשל, ולהכניס ל-buffer שנמצא בתוך ה-world object.


        for (i = 0; i < 27; i++)
            if (mCubes[i] != null)
                world.addShape(mCubes[i]);
הנה addShape:
מתבצעים שם שני דברים בשתי שורות:
  1. הוספת הצורה לרשימה.
  2. צבירת כמות הקודקודים ע"י חיבור shape.getIndexCount. מתוך המתודה הזו של ה-shape עושים איטרציה על כל ה-faceשל ה-shape ומשם מסכמים את כמות כל הקודקודים של הקוביה.


  1.     public void addShape(GLShape shape) {
            mShapeList.add(shape);

  2.         mIndexCount += shape.getIndexCount();
        }
נעבור לחלק הבא של makeGlWorld: כאן מאתחלים את וקטור הפרמוטציות. וקטור זה מקשר בין האובייקטים של הקוביות לבין מיקומן של הקוביה הגדולה. המיקום הזה משתנה כשמסובבים את ה-layers.
מה מבנה ה-array? יש בו 27 מקומות. כל מקום מתייחס למיקום בקוביה הגדולה. למשל: מקומות 0-8 נמצאים על הפאה העליונה. בנקודת ההתחלה - כשהקוביה "פתורה", הקוביות מסודרות כך שקוביה  0 נמצאת במקום 0 וכן הלאה עד קוביה 26 שנמצאת במקום 26. באופן זה מאתחלים את הוקטור כאן. מה יקרה כשנסובב את אחד ה-layers? הוקטור הנ"ל יצטרך להשתנות. נראה זאת בהמשך. מה השימוש בוקטור הזה? נדמה לי שהזכרתי את זה, אבל אני מהמר על כך שאזכיר זאת שוב  - ראה animation בהמשך.

        // initialize our permutation to solved position
        mPermutation = new int[27];
        for (i = 0; i < mPermutation.length; i++)
            mPermutation[i] = i;

מכאן מופעלות עוד 3 מתודות.
createLAyers יוצרת אובייקטים מסוג Layer. יש תשעה כאלה - 3 על כל אחד מהצירים x,y,x. כל Layer מאגד את כל הקוביות שינועו ביחד כשנסובב אותו, כך שכל קוביה שייכת לשלושה layers (אגב, הקוביה המרכזית - קוביה 13 - אינה נראית והיא אף מוגדרת כ-null כך שלא מתייחסים אליה).

        createLayers();
        updateLayers();

        world.generate();

        return world;
    }
 הנה createLayers:

   private void createLayers() {
        mLayers[kUp] = new Layer(Layer.kAxisY);
        mLayers[kDown] = new Layer(Layer.kAxisY);
        mLayers[kLeft] = new Layer(Layer.kAxisX);
        mLayers[kRight] = new Layer(Layer.kAxisX);
        mLayers[kFront] = new Layer(Layer.kAxisZ);
        mLayers[kBack] = new Layer(Layer.kAxisZ);
        mLayers[kMiddle] = new Layer(Layer.kAxisX);
        mLayers[kEquator] = new Layer(Layer.kAxisY);
        mLayers[kSide] = new Layer(Layer.kAxisZ);
    }
מאתחלים את 9 ה-layers. בסיס הנתונים של כל אובייקט Layer מכיל
  • את ציר הסיבוב של ה-layer  שהוא - kAxisX,kAxisY או kAxisZ, 
  • מערך של ה-cubes ששיכים לאותו layer,
  • וקטור טרנספורמציה לסיבוב הקואורדינטות של ה-shapes ששיכים ל-layer בהתאם לזוית הסיבוב. 
אני עוד נחזור לנושא הטרנספורמציה בסעף מיוחד בפוסט זה. ה-constructor הנ"ל מוסר את ציר הסיבוב של ה-layer.
 שימו לב לאינדקסים של כל אחד מה-layers. למשל kUp הוא 0 וכן הלאה עד 8.
המתודה הבאה שמופעלת מתוך makeWorld:
updateLayers

כאן מעדכנים את השיוך בין הקוביות ל-layers או במילים אחרות בין האובייקטים של 27 הקוביות לבין המיקום שלהם על גבי הקוביה הגדולה. המיקום הזה משתנה עם סיבוב ה-layer. את המתודה הזו מפעילים באיתחול (שם אנחנו נמצאים כרגע, מתוך makeWorld, ובסיום כל פעולת "אנימציה" - סיבוב של layer ב 90 מעלות.


 המתודה כאן למטה, אך הנה עוד מספר הסברים עליה. התהליך של חיבור ה-cubes ל-layers חוזר על עצמו באופן דומה עבור כל ה-layers.
עבור כל layer, מצמידים את הקוביות ששיכות אליו, בהתאם לאינדקסים של וקטור הפרמוטציות mPermutation.
 הזכרנו שעבור קוביה פתורה

mPermutation[i]=i

הקוד:
 private void updateLayers() {
        Layer layer;
        GLShape[] shapes;
        int i, j, k;
הפאה העליונה: 9 האינדקסים הראשונים.
        // up layer
        layer = mLayers[kUp];
        shapes = layer.mShapes;
        for (i = 0; i < 9; i++)
            shapes[i] = mCubes[mPermutation[i]];


הפאה התחתונה: 9 האינדקסים האחרונים בוקטור (18-26):


        // down layer
        layer = mLayers[kDown];
        shapes = layer.mShapes;
        for (i = 18, k = 0; i < 27; i++)
            shapes[k++] = mCubes[mPermutation[i]];


הפאה השמאלית: קוביות 0,3,6,9,12,15,18,21,24
        // left layer
        layer = mLayers[kLeft];
        shapes = layer.mShapes;
        for (i = 0, k = 0; i < 27; i += 9)
            for (j = 0; j < 9; j += 3)
                shapes[k++] = mCubes[mPermutation[i + j]];

הפאה הימנית:
קוביות 2,5,8,11,14,17,20,23,26

        // right layer
        layer = mLayers[kRight];
        shapes = layer.mShapes;
        for (i = 2, k = 0; i < 27; i += 9)
            for (j = 0; j < 9; j += 3)
                shapes[k++] = mCubes[mPermutation[i + j]];


הפאה הקדמית: 

קוביות 6,7,8,15,16,17,24,25,26
        // front layer
        layer = mLayers[kFront];
        shapes = layer.mShapes;
        for (i = 6, k = 0; i < 27; i += 9)
            for (j = 0; j < 3; j++)
                shapes[k++] = mCubes[mPermutation[i + j]];

וכן הלאה....

        // back layer
        layer = mLayers[kBack];
        shapes = layer.mShapes;
        for (i = 0, k = 0; i < 27; i += 9)
            for (j = 0; j < 3; j++)
                shapes[k++] = mCubes[mPermutation[i + j]];

        // middle layer
        layer = mLayers[kMiddle];
        shapes = layer.mShapes;
        for (i = 1, k = 0; i < 27; i += 9)
            for (j = 0; j < 9; j += 3)
                shapes[k++] = mCubes[mPermutation[i + j]];

        // equator layer
        layer = mLayers[kEquator];
        shapes = layer.mShapes;
        for (i = 9, k = 0; i < 18; i++)
            shapes[k++] = mCubes[mPermutation[i]];

        // side layer
        layer = mLayers[kSide];
        shapes = layer.mShapes;
        for (i = 3, k = 0; i < 27; i += 9)
            for (j = 0; j < 3; j++)
                shapes[k++] = mCubes[mPermutation[i + j]];
    }
והמתודה האחרונה שמופעלת ע"י makeWorld"
:world.generate:

כאן יוצרים את שלושת ה-nio  Buffers ותוך כדי הפעלת מתודות אחרות, מאכלסים אותם בקודקודים, צבעים ואינדקסים.
המתודה כאן למטה.
3 החלקים הראשונים של המתודה, יוצרים את 3 ה-nio buffers.  תהליך יצירת ה-buffer נידון בפוסט הראשון של openGL - קישור למעלה.
פרוט 3 ה-buffers:
  • ה-buffer הראשון מחזיק את הצבע - buffer מסוג color לא היה לנו בדוגמא הקודמת. גודלו כגודל רשימת הקודקודים - זה משהו כמו מס קוביות * מס פאות בקוביה* מס קודקודים בפאה כלומר 27*6*4 - 648?. כל זה, כפול 4 - גודל כל כתובת,  וכפול  4 לשמירת 4 גורמים - ARGB.
  • ה-buffer השני מחזיק את הקודקודים. גודלו כגודל רשימת הקודקודים, כפול 4 - ארבעה בתים לכל קודקוד, כפול 3 - עבור x,y,z.
  •  ה-buffer השלישי מחזיק את האינדקסים. גודלו כגודל כמות האינדקסים (אותם ספרנו תוך כדי הכנסת הקוביות), כפול 2 - 2 בתים לכתובת.  עבור כל face מציירים שני משולשים כלומר 6 אינדקסים, או 36 אינדקסים לקוביה קטנה. כפול 27 קוביות.


public void generate() {      
        ByteBuffer bb = ByteBuffer.allocateDirect(mVertexList.size()*4*4);
        bb.order(ByteOrder.nativeOrder());
        mColorBuffer = bb.asIntBuffer();

        bb = ByteBuffer.allocateDirect(mVertexList.size()*4*3);
        bb.order(ByteOrder.nativeOrder());
        mVertexBuffer = bb.asIntBuffer();

        bb = ByteBuffer.allocateDirect(mIndexCount*2);
        bb.order(ByteOrder.nativeOrder());
        mIndexBuffer = bb.asShortBuffer();

כאן עושים איטרציה על רשימת הקודקודים - ומכניסים אחד אחד ל mVertexBuffer ול- mColorBuffer.  

        Iterator<GLVertex> iter2 = mVertexList.iterator();
        while (iter2.hasNext()) {
            GLVertex vertex = iter2.next();
            vertex.put(mVertexBuffer, mColorBuffer);
        }
בהמשך נעקוב מקרוב אחרי הכנסת האלמנטים ל-buffer במסגרת ה-vertex class.

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

        Iterator<GLShape> iter3 = mShapeList.iterator();
        while (iter3.hasNext()) {
            GLShape shape = iter3.next();
            shape.putIndices(mIndexBuffer);
        }
    }
המתודה החרונה של Kube:
animate

כשקראנו ל-constructor של ה-renderer הכנסנו את animate כפרמטר. ה-constructor הכניס אותה כ-callback שיופעל בכל קריאה ל-onDrawFrame.
המתודה למטה, צבעונית כרגיל. מה היא מבצעת? ב-ג-ד-ו-ל: מטפלת בעידכון זוית סיבוב הקוביה הגדולה (זה החלק הקטן  שמתבצע בשורה הראשונה) ומטפלת בסיבוב ה-layers .
מבצעת את הפעולות הבאות:
  • קביעת זוית סיבוב הקוביה הגדולה - הסיבוב מתבצע ב-onDraw
  • הגרלה אקראית של ה-   layer, אותו נסובב ב 90 מעלות.
  • סיבוב ה-layer במשך 25 מחזורים, כל פעם הזזה של 1/25 מהזוית.
  • בסוף התהליך - הפעלת updateLayers - ראה פרוט למעלה. נמשיך עם הסברים בתוך הקוד.

נסקור את הקוד בחלקים לפי הסדר, כשההסברים בראש כל חלק.

הגדלת זוית הסיבוב של הקוביה  ב-1.2. כאן מדובר על סיבוב הקוביה כולה סביב הצירים בתוך onDrawFrame, ע"י המתודה rotate של ספריית ה- openGL.  ה-rotate הזה מבוצע ב-onDrawFrame ולא קשור לסיבוב שנעסוק בו מהשורה הבאה ואילך.
public void animate() {
         // change our angle of view
        mRenderer.setAngle(mRenderer.getAngle() + 1.2f);

מכאן ואילך - טיפול בסיבוב ה-layers.
הקטע הבא מתבצע בתחילת תהליך הסיבוב של 90 מעלות.
mCurrentLayyer==null משמעו - אפשר להבחור layer ולהתחיל לסובב אותו. אם זה לא null. אחרת, תהליך הסיבוב כבר החל מתבצע.התהליך אורך 25 מחזורים.
שורה 2 - הגרלת ה-layer שיסובב.
שורה  3 - שליפת האובייקט של ה-layer הנ"ל.
שורה 4 - שליפת וקטור הפרמוטציות של ה-layer הנ"ל. לכל layer יש וקטור פרמוטציות מוכן מראש - hard coded - ראה בתחתית ה-class. 
לדוגמא, נתבונן בוקטור הראשון:
            // permutation for UP layer
            { 2, 5, 8, 1, 4, 7, 0, 3, 6, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 },
הוא משוייך ל-layer העליון. יש בוקטור 27 איברים, והוא  מתאר את תזוזת כל אחת מהקוביות כתוצאה מסיבוב ה-layer - ב 90 מעלות נגד כיוון השעון. זה הכיוון בו אנו נסובב.
סיבוב ה-layer העליון מזיז את הקוביות 0-8 בלבד. קוביות 9-26 לא יזוזו, לכן האינדקסים שלהם ממפים 1:1. למשל: מקום 9 ישאר ב-9.
אנדקסים 0-8 מתארים סיבוב ב-90 מעלות נגד כיוון השעון: למשל: קוביות 2,5,8 עוברות למקום 0,1,2 בהתאמה.
עד כאן ההסבר על mLayerPermutations.

שורה 5: startAnuimation. אם זיכרוני אינו מטעני, לא מתבצע שם כלום!
שורה 6: הגדרה של כיוון הסיבוב. פעולה מיותרת, כי בהמשך- שורה 9- נעקוף החלטה זו ונאלץ תמיד כיוון false,  =  נגד כיוון השעון תמיד.
שורה 7 גם כן מיותרת, כי 8 עוקפת את ההחלטה וקובעת count=1 בכל מקרה, כלומר סיבוב אחד של 90 מעלות בכל "סיבוב".
שורה 10: איפוס הזווית. משתנה זה יצבור את הזוית עד שיגיע ל- 90 מעלות.
שורות 11-17: קביעת זוית הסיבוב בכל מעבר, בהתאם לכיוון הסיבוב. היות שאצלנו הכיוון תמיד false - הגדלת הזוית תהיה תמיד שלילית - -pi/50 במינוס.
עד כאן החלק האפור, שמתבצע בתחילת כל מהלך סיבוב.
  1.         if (mCurrentLayer == null) {
  2.             int layerID = mRandom.nextInt(9);
  3.             mCurrentLayer = mLayers[layerID];
  4.             mCurrentLayerPermutation = mLayerPermutations[layerID];
  5.             mCurrentLayer.startAnimation();
  6.             boolean direction = mRandom.nextBoolean();
  7.             int count = mRandom.nextInt(3) + 1;
  8.             count = 1;
  9.             direction = false;
  10.             mCurrentAngle = 0;
  11.              if (direction) {
  12.                 mAngleIncrement = (float)Math.PI / 50;
  13.                    mEndAngle = mCurrentAngle + ((float)Math.PI * count) / 2f;
  14.                } else {
  15.                 mAngleIncrement = -(float)Math.PI / 50;
  16.                    mEndAngle = mCurrentAngle - ((float)Math.PI * count) / 2f;
  17.             }
  18.         }
  19.  
 הגדלת הזוית בכל אחד מ-25 המעברים:
  1.  
  2.          mCurrentAngle += mAngleIncrement;
  3.  
  4.  
  5.  

 החלק הבא יתבצע כשיושלם הסיבוב של 90 מעלות.
שורה 4: פעולת הסיבוב של ה-layer. נפרט את התהליך בהמשך.
שורה 5: עידכון וקטור הסיבוב עבור כל אחת מהצורות של ה-layer - "מוסיפים" לו סיבוב של מינוס 90 מעלות.
שורה 6: במחזור הבא נגריל mCurrentLayer חדש.
שורות 8-13: עידכון ה-mPermutation - הוקטור שמחזיק את המיקום של כל אחד מ-cubes על גבי הקוביה הגדולה. ראה הסבר למעלה על ה-mPermutation. העידכון דרוש לביצוע השורה הבאה.
ה-updateLayer. מעדכן את מרכיבי ה-layer לאחר הסיבוב, בעזרת ה-mPermutation.
  1.  
  2.          if ((mAngleIncrement > 0f && mCurrentAngle >= mEndAngle) ||
  3.                  (mAngleIncrement < 0f && mCurrentAngle <= mEndAngle)) {
  4.              mCurrentLayer.setAngle(mEndAngle);
  5.              mCurrentLayer.endAnimation();
  6.              mCurrentLayer = null;
  7.              // adjust mPermutation based on the completed layer rotation
  8.              int[] newPermutation = new int[27];
  9.              for (int i = 0; i < 27; i++) {
  10.                 newPermutation[i] = mPermutation[mCurrentLayerPermutation[i]];
  11.  //                newPermutation[i] = mCurrentLayerPermutation[mPermutation[i]];
  12.              }
  13.              mPermutation = newPermutation;
  14.              updateLayers();
  15.          } else {
ובמקרה שהסיבוב ב-90 מעלות לא הסתיים, מגדילים את הזוית. בהמשך אפרט את התהליך של setAngel.

             mCurrentLayer.setAngle(mCurrentAngle);
         }
    }

עד כאן ה-Kube Class.


הסבר על סיבוב הקואורדינטות

נעקוב אחרי התהליך שמתחיל בתוך animate עם הקריאה למתודה mCurrentLayer.setAngle.
הזדמנות מצוינת להציג את המתודה setAngle הנ"ל השייכת ל-Layer class.
המתודה כאן למטה, אך תחילה ניזכר במשוואות אוילר ונגיע לבניית מטריצת הרוטציה.
ניקח  נקודה במישור הדו מימדי (אנחנו בכל מקרה דנים בסיבוב layer במישור הדו מימדי).
הנקודה z מיוצגת ע"י הקואורדינטות הקרטזיות כ: z=x+iy.
כעת נסובב את הוקטור בזוית a. נסתייע קצת באויילר ונכפיל את הוקטור באקספוננט עם מעריך דימיוני:

exp(ia)*z = exp(ia)*(x+iy) = (cos a + isin a)*(x+iy) = (x cos a - y sin a) + i*(xcos a + sina)

מכאן, הקואורדינטות החדשות הן:
x_rotated = xcos a - ysina
y_rotated = xcos a+ sin a
ציר הסיבוב במישור הדו מימדי הנ"ל הוא z. טרנספורמציה דומה מתאימה גם ל2 המישורים האחרים :
  • מישור x=קבוע וסיבוב סביב ציר x, 
  • ומישור y=קבוע וסיבוב סביב ציר y.
מטריצות הטרנספורמציה הנ"ל נבנות בשורות 1-25 למטה, עבור 3 צירי הסיבוב. כזכור, ביצירת כל אחד מה-layers קבענו את ציר הסיבוב שלו, והוא חלק מבסיס הנתונים של ה-layer class.
לאחר בניית מטריצת הסיבוב, נפעיל אותה על כל הצורות השייכות ל-layer - החלק הכתום.
--->המשך הסבר למטה.
    public void setAngle(float angle) {
        // normalize the angle
        float twopi = (float)Math.PI *2f;
        while (angle >= twopi) angle -= twopi;
        while (angle < 0f) angle += twopi;
//        mAngle = angle;
       
        float sin = (float)Math.sin(angle);
        float cos = (float)Math.cos(angle);
       
        float[][] m = mTransform.m;
        switch (mAxis) {
  1.             case kAxisX:
  2.                 m[1][1] = cos;
  3.                 m[1][2] = sin;
  4.                 m[2][1] = -sin;
  5.                 m[2][2] = cos;
  6.                 m[0][0] = 1f;
  7.                 m[0][1] = m[0][2] = m[1][0] = m[2][0] = 0f;
  8.                 break;
  9.             case kAxisY:
  10.                 m[0][0] = cos;
  11.                 m[0][2] = sin;
  12.                 m[2][0] = -sin;
  13.                 m[2][2] = cos;
  14.                 m[1][1] = 1f;
  15.                 m[0][1] = m[1][0] = m[1][2] = m[2][1] = 0f;
  16.                 break;
  17.             case kAxisZ:
  18.                 m[0][0] = cos;
  19.                 m[0][1] = sin;
  20.                 m[1][0] = -sin;
  21.                 m[1][1] = cos;
  22.                 m[2][2] = 1f;
  23.                 m[2][0] = m[2][1] = m[0][2] = m[1][2] = 0f;
  24.                 break;
  25.         }
       
        for (int i = 0; i < mShapes.length; i++) {
            GLShape shape = mShapes[i];
            if (shape != null) {
                Log.d("in shape set angle"," "+shape.mCubeIndex);
                shape.animateTransform(mTransform);
            }
        }
    }

בכתום הנ"ל, מופעלת המתודה animateTransform עבור כל קוביה ששיכת ל-layer. נעקוב אחרי הפעולה - ראה בין השורות למטה.



    public void animateTransform(M4 transform) {
        mAnimateTransform = transform;
       
        if (mTransform != null)
            transform = mTransform.multiply(transform);

כל צורה\קוביה מכילה את רשימת הקודקודים שלה - VertexList. כאן הרשימה נסרקת ע"י iterator, ומופעלת מתודת transformVertex של אובייקט ה-world. נזכור שזה האובייקט שמכיל את ה-buffers, ושם נשלוף את הקודקודים של הקוביה הנ"ל (ושל כל הקוביות השיכות ל-layer אחת אחרי השניה), ונכפיל אותם במטריצת הסיבוב.
  1.         Iterator<GLVertex> iter = mVertexList.iterator();
  2.         while (iter.hasNext()) {
  3.             GLVertex vertex = iter.next();
  4.             mWorld.transformVertex(vertex, transform);
  5.         }
    }
אז נמשיך במעקב אחרי התהליך ונעבור ל-mWorld.transformVertex:
 במתודה שלפנינו, מפעילים את המתודה של ה-vertex class. הפעם נוסף למטריצת הסיבוב יש פרמטר נוסף - ה-buffer.
public void transformVertex(GLVertex vertex, M4 transform) {
        vertex.update(mVertexBuffer, transform);
    }

נמשיך ל-vertex class:
הסברים בפנים.

   
    public void update(IntBuffer vertexBuffer, M4 transform) {
        // skip to location of vertex in mVertex buffer
נציב את מצביע ה-buffer על המקום הנכון. כל קודקוד תופס 3 מקומות זיכרון עבור x,y,z.
        vertexBuffer.position(index * 3);
   
        if (transform == null) {
            vertexBuffer.put(toFixed(x));
            vertexBuffer.put(toFixed(y));
            vertexBuffer.put(toFixed(z));           
        } else {
אני חושב שאנחנו מסיימים את עיכון הקואורדינטות בקרוב:

  1.             GLVertex temp = new GLVertex();
  2.             transform.multiply(this, temp);
  3.             vertexBuffer.put(toFixed(temp.x));
  4.             vertexBuffer.put(toFixed(temp.y));
  5.             vertexBuffer.put(toFixed(temp.z));
        }
    }
הנה ה-transform.multiply :
הכפלת המטריצה בקואורדינטות x,y,x.

    public void multiply(GLVertex src, GLVertex dest) {
        dest.x = src.x * m[0][0] + src.y * m[1][0] + src.z * m[2][0] + m[3][0];
        dest.y = src.x * m[0][1] + src.y * m[1][1] + src.z * m[2][1] + m[3][1];
        dest.z = src.x * m[0][2] + src.y * m[1][2] + src.z * m[2][2] + m[3][2];
    }

מ.ש.ל.


 האלמנטים של openGL

את המתודות של ה-openGL הכרנו בפוסט הקודם.  הן נמצאות ב-KubeRenderer.

הנה ה-class:

class KubeRenderer implements GLSurfaceView.Renderer {
    public interface AnimationCallback {
        void animate();
    }

    public KubeRenderer(GLWorld world, AnimationCallback callback) {
        mWorld = world;
        mCallback = callback;
    }

  1.     public void onDrawFrame(GL10 gl) {
  2.          if (mCallback != null) {
  3.              mCallback.animate();
  4.          }

  5.         /*
  6.          * Usually, the first thing one might want to do is to clear
  7.          * the screen. The most efficient way of doing this is to use
  8.          * glClear(). However we must make sure to set the scissor
  9.          * correctly first. The scissor is always specified in window
  10.          * coordinates:
  11.          */

  12.         gl.glClearColor(0.5f,0.5f,0.5f,1);
  13.         gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

  14.         /*
  15.          * Now we're ready to draw some 3D object
  16.          */

  17.         gl.glMatrixMode(GL10.GL_MODELVIEW);
  18.         gl.glLoadIdentity();
  19.         gl.glTranslatef(0, 0, -3.0f);
  20.         gl.glScalef(0.5f, 0.5f, 0.5f);
  21.         gl.glRotatef(mAngle,        0, 1, 0);
  22.         gl.glRotatef(mAngle*0.25f,  1, 0, 0);

  23.         gl.glColor4f(0.7f, 0.7f, 0.7f, 1.0f);
  24.         gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
  25.         gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
  26.         gl.glEnable(GL10.GL_CULL_FACE);
  27.         gl.glShadeModel(GL10.GL_SMOOTH);
  28.         gl.glEnable(GL10.GL_DEPTH_TEST);

  29.         mWorld.draw(gl);
  30.     }

    public void onSurfaceChanged(GL10 gl, int width, int height) {
        gl.glViewport(0, 0, width, height);

        /*
         * Set our projection matrix. This doesn't have to be done
         * each time we draw, but usually a new projection needs to be set
         * when the viewport is resized.
         */

        float ratio = (float)width / height;
        gl.glMatrixMode(GL10.GL_PROJECTION);
        gl.glLoadIdentity();
        gl.glFrustumf(-ratio, ratio, -1, 1, 2, 12);

        /*
         * By default, OpenGL enables features that improve quality
         * but reduce performance. One might want to tweak that
         * especially on software renderer.
         */
        gl.glDisable(GL10.GL_DITHER);
        gl.glActiveTexture(GL10.GL_TEXTURE0);
    }

    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        // Nothing special, don't have any textures we need to recreate.
    }

    public void setAngle(float angle) {
        mAngle = angle;
    }

    public float getAngle() {
        return mAngle;
    }

    private GLWorld mWorld;
    private AnimationCallback mCallback;
    private float mAngle;
}

ה-renderer כזכור הוא ה-interface של GLSurfaceView אותו צריך לממש, וכולל שלוש מתודות:
  1. onSurfaceCreated
  2. onSurfaceChanged
  3. onDrawFrame

שלושת הנ"ל, בנוסף ל-constructor מודגשות בצבע.

onSurfaceCreated - כאן ממש לא מבצע כלום, קחו בחשבון שלא משתמשים ב-texture כך שאין צורך להכין אותה.
onSurfaceChanged-  גם כאן אין יותר מידי. 
אם ניזכר בשלושת המתודות שקשורות בתאור הסצינה, 2 מהן מופעלות כאן:
glViewport - מגדירה את גודל המסך.
glFrustumf- מגדירה את ה-containder בו נמצא העצם ואת המרחק מהצופה.
המתודה השלישית lookAt לא מופעלתת, כך שנעשה שימוש בפרמטרים הדיפולטיבים:

המצלמה נמצאת בנקודה 0,0,0
האובייקט נמצא ב- 100-,0,0
המצלמה מיושרת לאורך ציר y.
זה אקוויולנטי ל:

(gluLookAt(0,0,0,0,0,-100,0,1,0.

   ( gl.glMatrixMode(GL10.GL_PROJECTION:
הפקודות הבאות מתייחסות למטריצת ה-projection.
האפשרויות האחרות הן פעולות על:
MODELVIEW
TEXTURE
COLOR


onDrawFrame
גם מתודה זו לא מורכבת מאד.
בתחילת המתודה מופעל ה-callback שהוצמד (animate) עם הפעלת ה-constructor.  ה-animate מטפל בסיבוב ה-layers של הקוביה, כפי שתואר בפרוט למעלה.
שימו לב לרוטציה על הצירים x ו-y בשורות 25-26.

המשך הציור נעשה במתודת העזר
 mWorld.draw. למה שם? כמובן, כי ה-buffers נמצאים ב-class הזה. הנה המתודה מתוך ה-GLWorld class:

  public void draw(GL10 gl)
    {
        mColorBuffer.position(0);
        mVertexBuffer.position(0);
        mIndexBuffer.position(0);

        gl.glFrontFace(GL10.GL_CW);
        gl.glShadeModel(GL10.GL_FLAT);
        gl.glVertexPointer(3, GL10.GL_FIXED, 0, mVertexBuffer);
        gl.glColorPointer(4, GL10.GL_FIXED, 0, mColorBuffer);
        gl.glDrawElements(GL10.GL_TRIANGLES, mIndexCount, GL10.GL_UNSIGNED_SHORT, mIndexBuffer);
        count++;
    }


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

אין תגובות:

הוסף רשומת תגובה