Định cấu hình đồ hoạ bằng OpenGL ES

Để vẽ các đối tượng và sprite trong trò chơi, bạn cần định cấu hình các biến màn hình, bề mặt (surface) và ngữ cảnh, thiết lập hoạt động kết xuất trong vòng lặp trò chơi, vẽ từng cảnh và đối tượng.

Có hai cách để vẽ hình ảnh lên màn hình cho trò chơi C hoặc C++, đó là sử dụng OpenGL ES hoặc Vulkan.

Trước khi bắt đầu

Thiết lập đối tượng GameActivity trong dự án Android, nếu bạn chưa làm việc này.

Thiết lập biến OpenGL ES

  1. Bạn cần có một màn hình, bề mặt, ngữ cảnhcấu hình để kết xuất trò chơi. Thêm các biến OpenGL ES sau vào tệp tiêu đề của công cụ phát triển trò chơi:

    class NativeEngine {
     //...
     private:
      EGLDisplay mEglDisplay;
      EGLSurface mEglSurface;
      EGLContext mEglContext;
      EGLConfig mEglConfig;
    
      bool mHasFocus, mIsVisible, mHasWindow;
      bool mHasGLObjects;
      bool mIsFirstFrame;
    
      int mSurfWidth, mSurfHeight;
    }
    
  2. Trong hàm khởi tạo cho công cụ phát triển trò chơi, hãy khởi động các giá trị mặc định cho biến.

    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. Khởi động màn hình để kết xuất.

    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. Bề mặt ở đây có thể là vùng đệm ngoài màn hình (pbuffer) do EGL phân bổ, hoặc một cửa sổ do Hệ điều hành Android phân bổ. Khởi động bề mặt này:

    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. Khởi động ngữ cảnh kết xuất. Ví dụ này sẽ tạo một ngữ cảnh 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. Định cấu hình chế độ cài đặt OpenGL ES trước khi vẽ. Ví dụ này được thực thi ở đầu mỗi khung. Việc này cho phép kiểm thử độ sâu, đặt màu trong suốt thành màu đen, cũng như xoá vùng đệm màu và độ sâu.

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

Kết xuất trong vòng lặp trò chơi

  1. Vòng lặp trò chơi kết xuất một khung và lặp lại vô hạn cho đến khi người dùng thoát. Giữa các khung hình, trò chơi của bạn có thể:

    Để kết xuất một khung vào màn hình, phương thức DoFrame được gọi vô thời hạn trong vòng lặp trò chơi:

    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. Trong phương thức DoFrame, hãy truy vấn các chiều kích thước của bề mặt hiện tại, yêu cầu SceneManager kết xuất một khung và hoán đổi vùng đệm hiển thị.

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

Kết xuất cảnh và đối tượng

  1. Vòng lặp trò chơi xử lý một hệ thống phân cấp các cảnh và đối tượng hiển thị cần kết xuất. Trong ví dụ về Endless Tunnel, SceneManager theo dõi nhiều cảnh, mỗi lần chỉ có một cảnh hoạt động. Trong ví dụ này, cảnh hiện tại sẽ được kết xuất:

    void SceneManager::DoFrame() {
      if (mSceneToInstall) {
        InstallScene(mSceneToInstall);
        mSceneToInstall = NULL;
      }
    
      if (mHasGraphics && mCurScene) {
        mCurScene->DoFrame();
      }
    }
    
  2. Tuỳ thuộc vào trò chơi, một cảnh có thể chứa nền, văn bản, sprite và đối tượng trò chơi. Hãy kết xuất chúng theo thứ tự phù hợp với trò chơi. Ví dụ này kết xuất nền, văn bản và tiện ích:

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

Tài nguyên

Đọc các nội dung dưới đây để biết thêm thông tin về OpenGL ES và Vulkan:

  • OpenGL ES – Hình ảnh và đồ hoạ trong Android.

  • OpenGL ES – Tổng quan trong Nguồn Android.

  • Vulkan – Bắt đầu trên NDK.

  • Vulkan – Tổng quan trong Nguồn Android.

  • Tìm hiểu vòng lặp trò chơi trên Android – tìm hiểu cách điều chỉnh tốc độ khung hình, xếp hàng đợi cho vùng đệm, xử lý lệnh gọi lại VSYNC và quản lý luồng.