Fork me on GitHub

Android Game Project 核心 Renderer.cpp

Renderer 类图组成:

  • 类名:Renderer

  • 属性

    • EGLDisplay display_: 用于OpenGL ES渲染的显示设备。它是一个与本地显示系统相关联的EGL显示连接。
    • EGLSurface surface_: OpenGL ES渲染的表面。这是一个EGL表面,代表可以渲染OpenGL ES图形的绘图目标。
    • EGLContext context_: OpenGL ES渲染的上下文。它是一个封装了OpenGL ES状态机的EGL渲染上下文。
    • int width_: 渲染表面的宽度,以像素为单位。
    • int height_: 渲染表面的高度,以像素为单位。
    • bool shaderNeedsNewProjectionMatrix_: 一个标志,指示是否需要为着色器生成新的投影矩阵。当渲染表面的大小改变时,这个标志会被设置为true
    • std::unique_ptr shader_: 指向当前使用的Shader对象的智能指针。Shader对象用于编译、链接和使用顶点和片段着色器。
    • std::vector models_: 包含所有要渲染的模型的容器。每个Model对象包含顶点数据、索引数据和纹理数据。

    方法

    • 析构函数 ~Renderer(): 清理Renderer对象,包括释放EGL资源(如显示设备、渲染表面和上下文)。
    • void render(): 执行渲染循环的一次迭代。这包括更新渲染状态、绘制模型和交换渲染表面的缓冲区。
    • void initRenderer(): 初始化渲染器,包括设置EGL上下文、选择EGL配置、创建渲染表面和上下文、初始化OpenGL ES状态和加载着色器。
    • void updateRenderArea(): 更新渲染区域的大小。如果渲染表面的大小发生变化,此方法更新width_height_属性,并标记需要为着色器生成新的投影矩阵。
    • void createModels(): 创建演示模型。这个方法加载模型的顶点、索引和纹理数据,然后将模型添加到models_容器中。
    • void handleInput(): 处理输入事件,如触摸和按键。这个方法从Android的输入事件队列中读取事件,并根据需要响应这些事件。
  • 关联和依赖关系

    • 依赖于Shader类:由于shader_属性和在initRenderer()方法中对Shader的调用。
    • 依赖于VertexIndex类型:在createModels()方法中使用这些类型来创建模型。
    • 依赖于TextureAsset类:在createModels()方法中加载纹理资源。
    • 使用了标准库中的类型,如std::vectorstd::unique_ptr

这个类图还可以展示Renderer类如何与Android的本地应用粘合层(native_app_glue)和OpenGL ES 3.0交互,特别是如何处理EGL上下文、表面创建和渲染循环。

下面是一个简化的类图表示,重点在于Renderer类及其直接关系:

1
2
3
4
5
6
7
8
[Renderer] --|> [EGLDisplay]
[Renderer] --|> [EGLSurface]
[Renderer] --|> [EGLContext]
[Renderer] ---> [Shader]
[Renderer] --* [Model]
[Model] --* [Vertex]
[Model] --* [Index]
[Model] ---> [TextureAsset]

说明:

  • --|> 表示拥有或创建关系。
  • ---> 表示依赖关系。
  • --* 表示包含或集合关系。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
#include "Renderer.h"

#include <game-activity/native_app_glue/android_native_app_glue.h>
#include <GLES3/gl3.h>
#include <memory>
#include <vector>
#include <android/imagedecoder.h>

#include "AndroidOut.h"
#include "Shader.h"
#include "Utility.h"
#include "TextureAsset.h"

// 宏定义,执行glGetString并将结果输出到logcat
#define PRINT_GL_STRING(s) {aout << #s": "<< glGetString(s) << std::endl;}

// 宏定义,如果glGetString返回一个空格分隔的列表,则将每个元素打印在新行上
/*!
* @brief if glGetString returns a space separated list of elements, prints each one on a new line
*
* This works by creating an istringstream of the input c-style string. Then that is used to create
* a vector -- each element of the vector is a new element in the input string. Finally a foreach
* loop consumes this and outputs it to logcat using @a aout
*/
#define PRINT_GL_STRING_AS_LIST(s) { \
std::istringstream extensionStream((const char *) glGetString(s));\
std::vector<std::string> extensionList(\
std::istream_iterator<std::string>{extensionStream},\
std::istream_iterator<std::string>());\
aout << #s":\n";\
for (auto& extension: extensionList) {\
aout << extension << "\n";\
}\
aout << std::endl;\
}

// 定义一种颜色,玉米花蓝色。可以直接发送给glClearColor函数。
#define CORNFLOWER_BLUE 100 / 255.f, 149 / 255.f, 237 / 255.f, 1

// 顶点着色器和片段着色器的代码,通常这些会从资源文件中加载
static const char *vertex = R"vertex(#version 300 es
in vec3 inPosition;
in vec2 inUV;

out vec2 fragUV;

uniform mat4 uProjection;

void main() {
fragUV = inUV;
gl_Position = uProjection * vec4(inPosition, 1.0);
}
)vertex";

static const char *fragment = R"fragment(#version 300 es
precision mediump float;

in vec2 fragUV;

uniform sampler2D uTexture;

out vec4 outColor;

void main() {
outColor = texture(uTexture, fragUV);
}
)fragment";


// 投影矩阵的半高度,这将给你一个从-2到2的高度为4的可渲染区域
static constexpr float kProjectionHalfHeight = 2.f;


// 投影矩阵的近平面距离。由于这是一个正交投影矩阵,负值便于排序(避免在0处的z-fighting)
static constexpr float kProjectionNearPlane = -1.f;

// 投影矩阵的远平面距离。与近平面等距的设置便于处理。
/*!
* The far plane distance for the projection matrix. Since this is an orthographic porjection
* matrix, it's convenient to have the far plane equidistant from 0 as the near plane.
*/
static constexpr float kProjectionFarPlane = 1.f;

// Renderer析构函数,处理EGL上下文的清理工作。
Renderer::~Renderer() {
if (display_ != EGL_NO_DISPLAY) {
eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if (context_ != EGL_NO_CONTEXT) {
eglDestroyContext(display_, context_);
context_ = EGL_NO_CONTEXT;
}
if (surface_ != EGL_NO_SURFACE) {
eglDestroySurface(display_, surface_);
surface_ = EGL_NO_SURFACE;
}
eglTerminate(display_);
display_ = EGL_NO_DISPLAY;
}
}

// 渲染函数,包括渲染过程中的各种状态更新和绘制调用。
void Renderer::render() {
// Check to see if the surface has changed size. This is _necessary_ to do every frame when
// using immersive mode as you'll get no other notification that your renderable area has
// changed.
updateRenderArea();

// When the renderable area changes, the projection matrix has to also be updated. This is true
// even if you change from the sample orthographic projection matrix as your aspect ratio has
// likely changed.
if (shaderNeedsNewProjectionMatrix_) {
// a placeholder projection matrix allocated on the stack. Column-major memory layout
float projectionMatrix[16] = {0};

// build an orthographic projection matrix for 2d rendering
Utility::buildOrthographicMatrix(
projectionMatrix,
kProjectionHalfHeight,
float(width_) / height_,
kProjectionNearPlane,
kProjectionFarPlane);

// send the matrix to the shader
// Note: the shader must be active for this to work. Since we only have one shader for this
// demo, we can assume that it's active.
shader_->setProjectionMatrix(projectionMatrix);

// make sure the matrix isn't generated every frame
shaderNeedsNewProjectionMatrix_ = false;
}

// clear the color buffer
glClear(GL_COLOR_BUFFER_BIT);

// Render all the models. There's no depth testing in this sample so they're accepted in the
// order provided. But the sample EGL setup requests a 24 bit depth buffer so you could
// configure it at the end of initRenderer
if (!models_.empty()) {
for (const auto &model: models_) {
shader_->drawModel(model);
}
}

// Present the rendered image. This is an implicit glFlush.
auto swapResult = eglSwapBuffers(display_, surface_);
assert(swapResult == EGL_TRUE);
}

// 初始化渲染器,设置EGL上下文和OpenGL状态。
void Renderer::initRenderer() {
// Choose your render attributes
constexpr
EGLint attribs[] = {
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_BLUE_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_RED_SIZE, 8,
EGL_DEPTH_SIZE, 24,
EGL_NONE
};

// The default display is probably what you want on Android
auto display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(display, nullptr, nullptr);

// figure out how many configs there are
EGLint numConfigs;
eglChooseConfig(display, attribs, nullptr, 0, &numConfigs);

// get the list of configurations
std::unique_ptr < EGLConfig[] > supportedConfigs(new EGLConfig[numConfigs]);
eglChooseConfig(display, attribs, supportedConfigs.get(), numConfigs, &numConfigs);

// Find a config we like.
// Could likely just grab the first if we don't care about anything else in the config.
// Otherwise hook in your own heuristic
auto config = *std::find_if(
supportedConfigs.get(),
supportedConfigs.get() + numConfigs,
[&display](const EGLConfig &config) {
EGLint red, green, blue, depth;
if (eglGetConfigAttrib(display, config, EGL_RED_SIZE, &red)
&& eglGetConfigAttrib(display, config, EGL_GREEN_SIZE, &green)
&& eglGetConfigAttrib(display, config, EGL_BLUE_SIZE, &blue)
&& eglGetConfigAttrib(display, config, EGL_DEPTH_SIZE, &depth)) {

aout << "Found config with " << red << ", " << green << ", " << blue << ", "
<< depth << std::endl;
return red == 8 && green == 8 && blue == 8 && depth == 24;
}
return false;
});

aout << "Found " << numConfigs << " configs" << std::endl;
aout << "Chose " << config << std::endl;

// create the proper window surface
EGLint format;
eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format);
EGLSurface surface = eglCreateWindowSurface(display, config, app_->window, nullptr);

// Create a GLES 3 context
EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
EGLContext context = eglCreateContext(display, config, nullptr, contextAttribs);

// get some window metrics
auto madeCurrent = eglMakeCurrent(display, surface, surface, context);
assert(madeCurrent);

display_ = display;
surface_ = surface;
context_ = context;

// make width and height invalid so it gets updated the first frame in @a updateRenderArea()
width_ = -1;
height_ = -1;

PRINT_GL_STRING(GL_VENDOR);
PRINT_GL_STRING(GL_RENDERER);
PRINT_GL_STRING(GL_VERSION);
PRINT_GL_STRING_AS_LIST(GL_EXTENSIONS);

shader_ = std::unique_ptr<Shader>(
Shader::loadShader(vertex, fragment, "inPosition", "inUV", "uProjection"));
assert(shader_);

// Note: there's only one shader in this demo, so I'll activate it here. For a more complex game
// you'll want to track the active shader and activate/deactivate it as necessary
shader_->activate();

// setup any other gl related global states
glClearColor(CORNFLOWER_BLUE);

// enable alpha globally for now, you probably don't want to do this in a game
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

// get some demo models into memory
createModels();
}

// 更新渲染区域的大小,如果有变化,则更新视口和投影矩阵。
void Renderer::updateRenderArea() {
EGLint width;
eglQuerySurface(display_, surface_, EGL_WIDTH, &width);

EGLint height;
eglQuerySurface(display_, surface_, EGL_HEIGHT, &height);

if (width != width_ || height != height_) {
width_ = width;
height_ = height;
glViewport(0, 0, width, height);

// make sure that we lazily recreate the projection matrix before we render
shaderNeedsNewProjectionMatrix_ = true;
}
}

// 创建演示模型的函数。
/**
* @brief Create any demo models we want for this demo.
*/
void Renderer::createModels() {
/*
* This is a square:
* 0 --- 1
* | \ |
* | \ |
* | \ |
* 3 --- 2
*/
std::vector <Vertex> vertices = {
Vertex(Vector3{1, 1, 0}, Vector2{0, 0}), // 0
Vertex(Vector3{-1, 1, 0}, Vector2{1, 0}), // 1
Vertex(Vector3{-1, -1, 0}, Vector2{1, 1}), // 2
Vertex(Vector3{1, -1, 0}, Vector2{0, 1}) // 3
};
std::vector <Index> indices = {
0, 1, 2, 0, 2, 3
};

// loads an image and assigns it to the square.
//
// Note: there is no texture management in this sample, so if you reuse an image be careful not
// to load it repeatedly. Since you get a shared_ptr you can safely reuse it in many models.
auto assetManager = app_->activity->assetManager;
auto spAndroidRobotTexture = TextureAsset::loadAsset(assetManager, "android_robot.png");

// Create a model and put it in the back of the render list.
models_.emplace_back(vertices, indices, spAndroidRobotTexture);
}

// 处理输入事件的函数,如触摸和按键事件。
void Renderer::handleInput() {
// handle all queued inputs
auto *inputBuffer = android_app_swap_input_buffers(app_);
if (!inputBuffer) {
// no inputs yet.
return;
}

// handle motion events (motionEventsCounts can be 0).
for (auto i = 0; i < inputBuffer->motionEventsCount; i++) {
auto &motionEvent = inputBuffer->motionEvents[i];
auto action = motionEvent.action;

// Find the pointer index, mask and bitshift to turn it into a readable value.
auto pointerIndex = (action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK)
>> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
aout << "Pointer(s): ";

// get the x and y position of this event if it is not ACTION_MOVE.
auto &pointer = motionEvent.pointers[pointerIndex];
auto x = GameActivityPointerAxes_getX(&pointer);
auto y = GameActivityPointerAxes_getY(&pointer);

// determine the action type and process the event accordingly.
switch (action & AMOTION_EVENT_ACTION_MASK) {
case AMOTION_EVENT_ACTION_DOWN:
case AMOTION_EVENT_ACTION_POINTER_DOWN:
aout << "(" << pointer.id << ", " << x << ", " << y << ") "
<< "Pointer Down";
break;

case AMOTION_EVENT_ACTION_CANCEL:
// treat the CANCEL as an UP event: doing nothing in the app, except
// removing the pointer from the cache if pointers are locally saved.
// code pass through on purpose.
case AMOTION_EVENT_ACTION_UP:
case AMOTION_EVENT_ACTION_POINTER_UP:
aout << "(" << pointer.id << ", " << x << ", " << y << ") "
<< "Pointer Up";
break;

case AMOTION_EVENT_ACTION_MOVE:
// There is no pointer index for ACTION_MOVE, only a snapshot of
// all active pointers; app needs to cache previous active pointers
// to figure out which ones are actually moved.
for (auto index = 0; index < motionEvent.pointerCount; index++) {
pointer = motionEvent.pointers[index];
x = GameActivityPointerAxes_getX(&pointer);
y = GameActivityPointerAxes_getY(&pointer);
aout << "(" << pointer.id << ", " << x << ", " << y << ")";

if (index != (motionEvent.pointerCount - 1)) aout << ",";
aout << " ";
}
aout << "Pointer Move";
break;
default:
aout << "Unknown MotionEvent Action: " << action;
}
aout << std::endl;
}
// clear the motion input count in this buffer for main thread to re-use.
android_app_clear_motion_events(inputBuffer);

// handle input key events.
for (auto i = 0; i < inputBuffer->keyEventsCount; i++) {
auto &keyEvent = inputBuffer->keyEvents[i];
aout << "Key: " << keyEvent.keyCode << " ";
switch (keyEvent.action) {
case AKEY_EVENT_ACTION_DOWN:
aout << "Key Down";
break;
case AKEY_EVENT_ACTION_UP:
aout << "Key Up";
break;
case AKEY_EVENT_ACTION_MULTIPLE:
// Deprecated since Android API level 29.
aout << "Multiple Key Actions";
break;
default:
aout << "Unknown KeyEvent Action: " << keyEvent.action;
}
aout << std::endl;
}
// clear the key input count too.
android_app_clear_key_events(inputBuffer);
}
,