Статьи

Разработка игр для Android — OpenGL Texture Mapping

В предыдущих двух статьях ( статья 1 и статья 2 ) я пытался представить OpenGL ES на Android. Теперь давайте продолжим и будем опираться на них. В этой статье мы создадим рекламный щит (квадрат) и применим к нему текстуру. Текстура — это не более чем растровое изображение. Когда мы работаем в 2D, мы устанавливаем координату Z на 0. Мы рассмотрим 3D позже. Это очень полезно для использования в 2D-играх и является предпочтительным способом отображения изображений с использованием OpenGL. Это действительно очень быстро.

В предыдущих статьях нам удалось отобразить треугольники. Как отобразить квадрат тогда? Квадрат состоит из 2 треугольников.

Следующая диаграмма показывает вам это:

Площадь из треугольников

Здесь есть интересная вещь, которую стоит отметить. Квадрат ABDC вместо обычного ABCD . Почему это? Из-за того, что OpenGL соединяет треугольники вместе.

То, что вы видите здесь, это полоса треугольника . Треугольная полоса — это серия соединенных треугольников, в нашем случае 2 треугольника.

OpenGL рисует следующую треугольную полосу (которая представляет собой квадрат), используя вершины в следующем порядке:

Треугольник 1: V1 -> V2 -> V3

Треугольник 2: V3 -> V2 -> V4

Полоса треугольника

Он рисует первый треугольник, используя вершины по порядку, затем берет последнюю вершину из предыдущего треугольника и использует последнюю сторону треугольника в качестве основы для нового треугольника.

Это также имеет свои преимущества: мы удаляем избыточные данные из памяти.

Возьмите проект из предыдущей статьи и создайте новый класс с именем Square .

Если вы сравните класс Square с классом Triangle , вы заметите только одно отличие:

01
02
03
04
05
06
07
08
09
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
package net.obviam.opengl;
 
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
 
import javax.microedition.khronos.opengles.GL10;
 
public class Square {
 
    private FloatBuffer vertexBuffer;   // buffer holding the vertices
 
    private float vertices[] = {
            -1.0f, -1.0f,  0.0f,        // V1 - bottom left
            -1.0f,  1.0f,  0.0f,        // V2 - top left
             1.0f, -1.0f,  0.0f,        // V3 - bottom right
             1.0f,  1.0f,  0.0f         // V4 - top right
    };
 
    public Square() {
        // a float has 4 bytes so we allocate for each coordinate 4 bytes
        ByteBuffer vertexByteBuffer = ByteBuffer.allocateDirect(vertices.length * 4);
        vertexByteBuffer.order(ByteOrder.nativeOrder());
 
        // allocates the memory from the byte buffer
        vertexBuffer = vertexByteBuffer.asFloatBuffer();
 
        // fill the vertexBuffer with the vertices
        vertexBuffer.put(vertices);
 
        // set the cursor position to the beginning of the buffer
        vertexBuffer.position(0);
    }
 
    /** The draw method for the square with the GL context */
    public void draw(GL10 gl) {
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
 
        // set the colour for the square
        gl.glColor4f(0.0f, 1.0f, 0.0f, 0.5f);
 
        // Point to our vertex buffer
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
 
        // Draw the vertices as triangle strip
        gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);
 
        //Disable the client state before leaving
        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
    }
}

Разница в выделенных строках (13-18). Правильно, мы добавили еще одну вершину в массив вершин .

Теперь измените GlRenderer, чтобы вместо Треугольника мы использовали Квадрат .

01
02
03
04
05
06
07
08
09
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
package net.obviam.opengl;
 
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
 
import android.opengl.GLU;
import android.opengl.GLSurfaceView.Renderer;
 
public class GlRenderer implements Renderer {
 
    private Square      square;     // the square
 
    /** Constructor to set the handed over context */
    public GlRenderer() {
        this.square     = new Square();
    }
 
    @Override
    public void onDrawFrame(GL10 gl) {
        // clear Screen and Depth Buffer
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
 
        // Reset the Modelview Matrix
        gl.glLoadIdentity();
 
        // Drawing
        gl.glTranslatef(0.0f, 0.0f, -5.0f);     // move 5 units INTO the screen
                                                // is the same as moving the camera 5 units away
        square.draw(gl);                        // Draw the triangle
 
    }
 
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        if(height == 0) {                       //Prevent A Divide By Zero By
            height = 1;                         //Making Height Equal One
        }
 
        gl.glViewport(0, 0, width, height);     //Reset The Current Viewport
        gl.glMatrixMode(GL10.GL_PROJECTION);    //Select The Projection Matrix
        gl.glLoadIdentity();                    //Reset The Projection Matrix
 
        //Calculate The Aspect Ratio Of The Window
        GLU.gluPerspective(gl, 45.0f, (float)width / (float)height, 0.1f, 100.0f);
 
        gl.glMatrixMode(GL10.GL_MODELVIEW);     //Select The Modelview Matrix
        gl.glLoadIdentity();                    //Reset The Modelview Matrix
    }
 
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    }
}

Выполнение этого приведет к следующему результату:

Треугольная полоса, образующая квадрат

Рассматривая это, метод draw () в классе Square должен иметь смысл.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public void draw(GL10 gl) {
    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
 
    // set the colour for the square
    gl.glColor4f(0.0f, 1.0f, 0.0f, 0.5f);
 
    // Point to our vertex buffer
    gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
 
    // Draw the vertices as triangle strip
    gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);
 
    //Disable the client state before leaving
    gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
}

Сначала мы разрешаем OpenGL использовать массив вершин для рендеринга. Наш массив вершин содержит вершины для нашего квадрата.

gl.glVertexPointer (строка 5) сообщает рендереру opengl, откуда брать вершины и какого они типа.

Первый параметр сообщает, сколько координат используется для вершины. Мы используем 3 (x, y, z). Второй параметр говорит, что значения имеют тип float .

Третий параметр — это смещение между вершинами в массиве. Это называется рознь . У нас плотно упакованный массив, поэтому он равен 0 .

Наконец, последний параметр сообщает, где находятся вершины. Конечно, это наш буфер vertexBuffer .

gl.glDrawArrays в строке 11 говорит OpenGL нарисовать примитив. Что за примитив? Тот, который указан в первом параметре: GL10.GL_TRIANGLE_STRIP . Он берет вершины из ранее установленного буфера вершин и следует правилам полос треугольника, описанным ранее.

Второй параметр указывает начальный индекс для вершин в массиве.

Третий параметр сообщает OpenGL, сколько вершин использовать для многоугольника, которые должны быть визуализированы. Поскольку в предыдущем выражении ( gl.glVertexPointer ) мы указали, что 3 координаты определяют вершину, мы предоставим длину нашего массива вершин, разделенную на 3. В массиве 9 элементов, определяющих 3 вершины.

glDisableClientState (GL10.GL_VERTEX_ARRAY) отключает состояние рендеринга из массива, содержащего вершины.

Представьте, что glEnableClientState и glDisableClientState начинаются… заканчиваются в программе. Мы в основном вводим подпрограммы в рендерер OpenGL. После того, как мы ввели подпрограмму, мы устанавливаем переменные (буфер вершин, цвет и т. Д.) И выполняем другие подпрограммы (рисуем вершины). После того, как мы закончим, мы выходим из подпрограммы. Мы работаем в изоляции внутри рендера.

Убедитесь, что вы запустили приложение на этом этапе и поняли, что происходит.

Создание текстуры

Теперь самое интересное. Давайте загрузим изображение и создадим текстуру. Текстура это изображение.

Чтобы узнать, как вы можете загрузить изображения в вашем приложении для Android, ознакомьтесь с этой статьей .

Мы будем работать с классом Square, так как мы хотим применить текстуру к квадрату.

Нам нужно загрузить изображение, сообщить рендереру opengl, что мы хотим использовать его в качестве текстуры, и, наконец, мы сообщим рендереру, где именно на нашем примитиве (квадрате) отобразить его.

Думайте об этом, как будто вы кладете фольгу на окно или стену. Я предоставляю вам фольгу, содержащую изображение размера окна, и советую вам накрыть ею окно, чтобы левый верхний угол фольги находился в верхнем левом углу окна. Вот и все, давайте приступим к работе.

OpenGL использует вершины, чтобы определить, куда поместить материал. Поэтому нам нужно создать массив для изображения. Но на этот раз это будет 2D, поскольку растровое изображение похоже на лист бумаги, плоскую плоскость.

Добавьте массив координат для текстуры.

1
2
3
4
5
6
7
8
private FloatBuffer textureBuffer;  // buffer holding the texture coordinates
private float texture[] = {
        // Mapping coordinates for the vertices
        0.0f, 1.0f,     // top left     (V2)
        0.0f, 0.0f,     // bottom left  (V1)
        1.0f, 1.0f,     // top right    (V4)
        1.0f, 0.0f      // bottom right (V3)
};

Нам нужно создать textureBuffer аналогично vertexBuffer . Это происходит в конструкторе, и мы просто повторно используем byteBuffer . Проверьте новый конструктор:

01
02
03
04
05
06
07
08
09
10
11
12
13
public Square() {
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(vertices.length * 4);
    byteBuffer.order(ByteOrder.nativeOrder());
    vertexBuffer = byteBuffer.asFloatBuffer();
    vertexBuffer.put(vertices);
    vertexBuffer.position(0);
 
    byteBuffer = ByteBuffer.allocateDirect(texture.length * 4);
    byteBuffer.order(ByteOrder.nativeOrder());
    textureBuffer = byteBuffer.asFloatBuffer();
    textureBuffer.put(texture);
    textureBuffer.position(0);
}

Мы добавим важный метод в класс Square . Метод loadGLTexture . Это будет вызвано от средства визуализации, когда это запускается. Это происходит в методе onSurfaceCreated . Это загрузит изображение с диска и свяжет его с текстурой в репозитории OpenGL. Он будет в основном назначать внутренний идентификатор для обработанного изображения и будет использоваться API OpenGL для идентификации его среди других текстур.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** The texture pointer */
private int[] textures = new int[1];
 
public void loadGLTexture(GL10 gl, Context context) {
    // loading texture
    Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(),
            R.drawable.android);
 
    // generate one texture pointer
    gl.glGenTextures(1, textures, 0);
    // ...and bind it to our array
    gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
 
    // create nearest filtered texture
    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
 
    // Use Android GLUtils to specify a two-dimensional texture image from our bitmap
    GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
 
    // Clean up
    bitmap.recycle();
}

Нам нужен массив указателей текстуры. Здесь OpenGL будет хранить имена текстур, которые мы будем использовать в нашем приложении. Поскольку у нас есть только одно изображение, мы создадим массив размером 1.

Строка 06 загружает растровое изображение Android, которое было ранее скопировано в каталог / res / drawable-mdpi , поэтому идентификатор уже создан.

Примечание об этом растровом изображении. Рекомендуется быть квадратным. Это очень помогает при масштабировании. Поэтому убедитесь, что ваши растровые изображения для текстур являются квадратами (6 × 6, 12 × 12, 128 × 128 и т. Д.). Если не квадрат, убедитесь, что ширина и высота являются степенями 2 (2, 4, 8, 16, 32,…). Вы можете иметь растровое изображение 128 × 512, и оно идеально подходит для использования и оптимизировано.

Строка 10 генерирует имена для текстур. В нашем случае генерирует одно имя и сохраняет его в массиве текстур . Даже если он говорит имя , он на самом деле генерирует int . Немного смущает, но так оно и есть.

Строка 12 связывает текстуру с новым именем (texture [0]). Это означает, что все, что использует текстуры в этой подпрограмме, будет использовать связанную текстуру. Практически активирует текстуру. Связанная текстура — это активная текстура. Если бы у нас было несколько текстур и несколько квадратов, чтобы использовать их, нам пришлось бы привязать (активировать) соответствующие текстуры для каждого квадрата непосредственно перед тем, как они использовались для их активации.
Строки 15 и 16 устанавливают некоторые фильтры, которые будут использоваться для текстуры. Мы только что сообщили OpenGL, какие типы фильтров использовать, когда нужно уменьшить или расширить текстуру, чтобы покрыть квадрат. Мы выбрали несколько основных алгоритмов для масштабирования изображения. Не нужно беспокоиться об этом сейчас.

В строке 19 мы используем утилиты Android, чтобы указать изображение текстуры 2D для нашего растрового изображения. Он создает изображение (текстуру) внутри в своем родном формате на основе нашего растрового изображения.

в строке 22 мы освобождаем память. Это не следует забывать, так как память на устройстве очень ограничена, а изображения большие.

Теперь посмотрим, как был изменен метод draw () .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    public void draw(GL10 gl) {
        // bind the previously generated texture
        gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
 
        // Point to our buffers
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
 
        // Set the face rotation
        gl.glFrontFace(GL10.GL_CW);
 
        // Point to our vertex buffer
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
        gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);
 
        // Draw the vertices as triangle strip
        gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);
 
        //Disable the client state before leaving
        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
    }
}

Это не большая модификация из предыдущей статьи. Дополнения документированы и делают следующее:

Строка 03 связывает (активирует) текстуру с именем (целочисленным идентификатором), хранящимся в текстурах [0] .

Строка 07 включает отображение текстуры в текущем контексте OpenGL.

Строка 14 предоставляет контекст OpenGL с координатами текстуры.
После рисования примитива с текстурами мы отключаем отображение текстур вместе с примитивом рендеринга.

Важно — UV Mapping

Если вы внимательно посмотрите, порядок вершин в массиве координат отображения текстуры не соответствует порядку, представленному в массиве координат вершины квадрата.

Здесь очень хорошее объяснение координат наложения текстуры: http://iphonedevelopment.blogspot.com/2009/05/opengl-es-from-ground-up-part-6_25.html .

Я постараюсь объяснить это быстро, хотя. Изучите следующую диаграмму.

Квадратные и текстурные координаты

Квадрат состоит из 2 треугольников, вершины расположены в следующем порядке.

1 — внизу слева

2 — внизу справа

3 — верхний левый

4 — вверху справа

Обратите внимание на путь против часовой стрелки.

Координаты текстуры будут в следующем порядке: 1 -> 3 -> 2 -> 4

Просто запомните это отображение и поверните его, если вы начинаете свою форму из другого угла. Чтобы прочитать об УФ-картографировании, зайдите в статью в Википедии или поищите в сети.

В заключительной части, чтобы сделать эту работу, нам нужно предоставить контекст для нашего рендерера, чтобы мы могли загрузить текстуру при запуске.

Метод onSurfaceCreated будет выглядеть следующим образом.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    // Load the texture for the square
    square.loadGLTexture(gl, this.context);
 
    gl.glEnable(GL10.GL_TEXTURE_2D);            //Enable Texture Mapping ( NEW )
    gl.glShadeModel(GL10.GL_SMOOTH);            //Enable Smooth Shading
    gl.glClearColor(0.0f, 0.0f, 0.0f, 0.5f);    //Black Background
    gl.glClearDepthf(1.0f);                     //Depth Buffer Setup
    gl.glEnable(GL10.GL_DEPTH_TEST);            //Enables Depth Testing
    gl.glDepthFunc(GL10.GL_LEQUAL);             //The Type Of Depth Testing To Do
 
    //Really Nice Perspective Calculations
    gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
}

Строка 03 загружает текстуру. Остальные строки просто настраивают рендерер с некоторыми значениями. Вам не нужно беспокоиться о них сейчас.

Вам нужно будет предоставить контекст приложения для объекта Square , потому что сам объект загружает текстуру и должен знать путь к растровому изображению.

Просто предоставьте контекст для средства визуализации в методе OnCreate действия Run ( glSurfaceView.setRenderer (новый GlRenderer (this)); ), и все готово.

Убедитесь, что визуализатор имеет контекст, объявленный и установленный через конструктор.

Выдержка из класса GlRendered .

01
02
03
04
05
06
07
08
09
10
private Square      square;     // the square
private Context     context;
 
/** Constructor to set the handed over context */
public GlRenderer(Context context) {
    this.context = context;
 
    // initialise the square
    this.square = new Square();
}

Если вы запустите код, вы увидите квадрат с красивым андроидом, положенным поверх него.

Квадрат с текстурой Android

Загрузите исходный код и проект здесь (obviam.opengl.p03.tgz).

Ссылка: наложение текстур — OpenGL Android (отображение изображений с использованием OpenGL и квадратов) от нашего партнера по JCG Тамаса Яно из блога « Против зерна ».

Не забудьте проверить нашу новую Android игру ArkDroid (скриншоты ниже) . Ваш отзыв будет более чем полезным!
Статьи по Теме: