הגדרת הגרפיקה ב-OpenGL ES

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

יש שתי דרכים לצייר תמונות למסך עבור משחק C או C++ : OpenGL ES, או Vulkan.

לפני שמתחילים

אם עדיין לא עשית זאת, להגדיר אובייקט GameActivity ב- פרויקט Android.

הגדרת משתנים של OpenGL ES

  1. אתה צריך מסך, Surface, הקשר, וגם הגדרות כדי לעבד את המשחק. מוסיפים את הבאים ממשתני OpenGL ES לקובץ הכותרת של מנוע המשחק:

    class NativeEngine {
     //...
     private:
      EGLDisplay mEglDisplay;
      EGLSurface mEglSurface;
      EGLContext mEglContext;
      EGLConfig mEglConfig;
    
      bool mHasFocus, mIsVisible, mHasWindow;
      bool mHasGLObjects;
      bool mIsFirstFrame;
    
      int mSurfWidth, mSurfHeight;
    }
    
  2. ב- constructor של מנוע המשחק, מאתחלים את ערכי ברירת המחדל של של המשתנים.

    NativeEngine::NativeEngine(struct android_app *app) {
      //...
      mEglDisplay = EGL_NO_DISPLAY;
      mEglSurface = EGL_NO_SURFACE;
      mEglContext = EGL_NO_CONTEXT;
      mEglConfig = 0;
    
      mHasFocus = mIsVisible = mHasWindow = false;
      mHasGLObjects = false;
      mIsFirstFrame = true;
    
      mSurfWidth = mSurfHeight = 0;
    }
    
  3. מאתחלים את התצוגה כדי לבצע רינדור.

    bool NativeEngine::InitDisplay() {
      if (mEglDisplay != EGL_NO_DISPLAY) {
        return true;
      }
    
      mEglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
      if (EGL_FALSE == eglInitialize(mEglDisplay, 0, 0)) {
        LOGE("NativeEngine: failed to init display, error %d", eglGetError());
        return false;
      }
      return true;
    }
    
  4. המשטח יכול להיות מאגר נתונים זמני שמוקצה על ידי EGL, או החלון שהוקצה על ידי מערכת ההפעלה של Android. מאתחלים את הפלטפורמה הזו:

    bool NativeEngine::InitSurface() {
      ASSERT(mEglDisplay != EGL_NO_DISPLAY);
      if (mEglSurface != EGL_NO_SURFACE) {
        return true;
      }
    
      EGLint numConfigs;
      const EGLint attribs[] = {
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, // request OpenGL ES 2.0
        EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
        EGL_BLUE_SIZE, 8,
        EGL_GREEN_SIZE, 8,
        EGL_RED_SIZE, 8,
        EGL_DEPTH_SIZE, 16,
        EGL_NONE
      };
    
      // Pick the first EGLConfig that matches.
      eglChooseConfig(mEglDisplay, attribs, &mEglConfig, 1, &numConfigs);
      mEglSurface = eglCreateWindowSurface(mEglDisplay, mEglConfig, mApp->window,
                                           NULL);
      if (mEglSurface == EGL_NO_SURFACE) {
        LOGE("Failed to create EGL surface, EGL error %d", eglGetError());
        return false;
      }
      return true;
    }
    
  5. אתחול ההקשר של הרינדור. הדוגמה הזו יוצרת ההקשר של OpenGL ES 2.0:

    bool NativeEngine::InitContext() {
      ASSERT(mEglDisplay != EGL_NO_DISPLAY);
      if (mEglContext != EGL_NO_CONTEXT) {
        return true;
      }
    
      // OpenGL ES 2.0
      EGLint attribList[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
      mEglContext = eglCreateContext(mEglDisplay, mEglConfig, NULL, attribList);
      if (mEglContext == EGL_NO_CONTEXT) {
        LOGE("Failed to create EGL context, EGL error %d", eglGetError());
        return false;
      }
      return true;
    }
    
  6. צריך לקבוע את ההגדרות של OpenGL ES לפני השרטוט. דוגמה זו מתבצעת ב- בתחילת כל פריים. היא מאפשרת לבצע בדיקת עומק, מגדירה את הצבע הבהיר שחור, ומנקה את חוצץ הצבע והעומק.

    void NativeEngine::ConfigureOpenGL() {
      glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
      glEnable(GL_DEPTH_TEST);
      glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
    }
    

עיבוד באמצעות לולאת המשחק

  1. המשחק בלופ מבצע עיבוד של פריים וחוזר על עצמו ללא הגבלת זמן, עד שהמשתמש עוזב. בין הפריימים, המשחק עשוי:

    • עיבוד אירועים כמו אירועי קלט, פלט אודיו ואירועים רישות.

    • מעדכנים את הלוגיקה של המשחק ואת ממשק המשתמש.

    • עיבוד מסגרת לתצוגה.

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

    void NativeEngine::GameLoop() {
      // Loop indefinitely.
      while (1) {
        int events;
        struct android_poll_source* source;
    
        // If not animating, block until we get an event.
        while ((ALooper_pollAll(IsAnimating() ? 0 : -1, NULL, &events,
                                (void **) &source)) >= 0) {
          // Process events.
          ...
        }
    
        // Render a frame.
        if (IsAnimating()) {
            DoFrame();
        }
      }
    }
    
  2. באמצעות method DoFrame, שולחים שאילתה למאפייני הפלטפורמה הנוכחיים, מבקשים SceneManager כדי לעבד פריים ולהחליף את מאגר הנתונים הזמני של המסך.

    void NativeEngine::DoFrame() {
      ...
      // Query the current surface dimension.
      int width, height;
      eglQuerySurface(mEglDisplay, mEglSurface, EGL_WIDTH, &width);
      eglQuerySurface(mEglDisplay, mEglSurface, EGL_HEIGHT, &height);
    
      // Handle dimension changes.
      SceneManager *mgr = SceneManager::GetInstance();
      if (width != mSurfWidth || height != mSurfHeight) {
        mSurfWidth = width;
        mSurfHeight = height;
        mgr->SetScreenSize(mSurfWidth, mSurfHeight);
        glViewport(0, 0, mSurfWidth, mSurfHeight);
      }
      ...
      // Render scenes and objects.
      mgr->DoFrame();
    
      // Swap buffers.
      if (EGL_FALSE == eglSwapBuffers(mEglDisplay, mEglSurface)) {
        HandleEglError(eglGetError());
      }
    }
    

עיבוד של סצנות ואובייקטים

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

    void SceneManager::DoFrame() {
      if (mSceneToInstall) {
        InstallScene(mSceneToInstall);
        mSceneToInstall = NULL;
      }
    
      if (mHasGraphics && mCurScene) {
        mCurScene->DoFrame();
      }
    }
    
  2. בהתאם למשחק, סצנה עשויה להכיל רקע, טקסט, דמויות sprite האובייקטים של המשחק. צריך להציג אותן לפי הסדר שמתאים למשחק שלכם. הדוגמה הזו יוצר את הרקע, הטקסט והווידג'טים:

    void UiScene::DoFrame() {
      // clear screen
      glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      glDisable(GL_DEPTH_TEST);
    
      RenderBackground();
    
      // Render the "Please Wait" sign and do nothing else
      if (mWaitScreen) {
        SceneManager *mgr = SceneManager::GetInstance();
        mTextRenderer->SetFontScale(WAIT_SIGN_SCALE);
        mTextRenderer->SetColor(1.0f, 1.0f, 1.0f);
        mTextRenderer->RenderText(S_PLEASE_WAIT, mgr->GetScreenAspect() * 0.5f,
                                  0.5f);
        glEnable(GL_DEPTH_TEST);
        return;
      }
    
      // Render all the widgets.
      for (int i = 0; i < mWidgetCount; ++i) {
        mWidgets[i]->Render(mTrivialShader, mTextRenderer, mShapeRenderer,
              (mFocusWidget < 0) ? UiWidget::FOCUS_NOT_APPLICABLE :
              (mFocusWidget == i) ? UiWidget::FOCUS_YES : UiWidget::FOCUS_NO,tf);
      }
      glEnable(GL_DEPTH_TEST);
    }
    

משאבים

בקישורים הבאים תוכלו למצוא מידע נוסף על OpenGL ES ו-Vulkan:

  • OpenGL ES – תמונות וגרפיקה ב-Android.

  • OpenGL ES – סקירה כללית ב'מקור Android'.

  • Vulkan – תחילת העבודה ב-NDK.

  • Vulkan – סקירה כללית במקור Android.

  • הסבר על לולאות משחקים ב-Android – לומדים איך לעבוד בקצב פריימים, מאגרי תורים זמניים, טיפול בקריאות חוזרות (callbacks) של VSYNC וניהול שרשורים.