Французская версия доступна здесь: https://medium.com/@xzan/opengl-le-guide-du-noob-pour-d%C3%A9veloppeur-android-78f069c7214d

Итак, вы решили попробовать OpenGL на Android? Прежде чем мы погрузимся в подробности, важно знать, во что вы ввязываетесь. Вы будете плакать, умолять и сомневаться в том, что, как вы думали, знали с начальной школы. Будьте уверены, это нормально!

Что такое OpenGL?

Чтобы быть уверенным, что мы говорим об одном и том же, давайте сразу проясним ситуацию. OpenGL - это программный интерфейс, который позволяет вам общаться с графическим драйвером устройства. Это может быть телефон, компьютер, экран телевизора или любое другое устройство, поддерживающее OpenGL. Ну да, устройство должно его поддерживать. Что касается устройств Android, они поддерживают:
* OpenGL ES 1.0 и 1.1 начиная с Android 1.0 (API 4)
* OpenGL ES 2.0. начиная с Android 2.2 (API 8)
* OpenGL ES 3.0 начиная с Android 4.3 (API 18) (почти)
* OpenGL ES 3.1, начиная с Android 5.0 (API 21)

ES что теперь? …

Вы уже догадались, это было слишком хорошо, чтобы быть правдой, есть загвоздка. Android не поддерживает OpenGL, но поддерживает OpenGL ES. OpenGL ES - это вариант спецификаций OpenGL для встраиваемых систем.
Хорошо, хорошо! Это не так уж и плохо, есть различия, но ничего серьезного. Это означает, что какой-то код, работающий на вашем компьютере, может работать не так, как на вашем телефоне, но почти не работает.
Ой!

Графический драйвер?

«Я думал, что Android создан на Java, и мне не нужно было заботиться об оборудовании, если я не буду использовать собственный код (C или C ++)»

Вы почти правы. Когда вы используете OpenGL, вы обращаетесь непосредственно к графическому драйверу, поэтому возможно, что один и тот же код Java не работает одинаково на всех телефонах. Но еще будет время позаботиться об этом, если и когда возникнут такие проблемы встаньте!

Давайте нырнем!

Я собираюсь говорить только о OpenGL ES 2.0, потому что он поддерживается большинством телефонов Android (Android 2.2+). Тем не менее, этого должно быть достаточно для запуска и запуска OpenGL ES 3.0 или 3.1, если он вам понадобится.

В следующих разделах я расскажу о некоторых важных моментах и ошибок, не вдаваясь в подробности реализации. Чтобы узнать, как все работает вместе, просмотрите пример проекта, который сопровождает эту статью: https://bitbucket.org/Xzan/opengl-example.
Обратите внимание, что все было помещено в один файл специально, чтобы облегчить чтение.

Или, что еще лучше, замечательное руководство на веб-сайте разработчика Android: https://developer.android.com/training/graphics/opengl/index.html

GLSurfaceView и средство визуализации

Нам нужно с чего-то начать, и, как правило, лучше начинать с самого начала. Чтобы не запутать вас с самого начала, мы собираемся начать с того, что вам нужно знать об Android на Java.

В нашем случае мы начинаем с добавления представления, в котором мы собираемся отображать результат наших команд OpenGL. Это представление называется GLSurfaceView и отвечает за создание потока для ваших команд OpenGL.
Затем входит в его интерфейс: GLSurfaceView.Renderer, который будет вызываться в трех ключевых моментах потока OpenGL GLSurfaceView:

  • на SurfaceCreated (GL10 gl, конфигурация EGLConfig)
  • на SurfaceChanged (GL10 gl, int width, int height)
  • на DrawFrame (GL10 gl)

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

* В SurfaceCreated вы инициализируете свою программу и начальные конфигурации. Вы можете увидеть этот метод как конструктор View. Этот метод вызывается один раз для каждого цикла обзора поверхности. Но Surface можно уничтожить, и этот метод будет вызван при создании следующего.

* на SurfaceChanged - хорошее место для создания ваших текстур (мы еще вернемся к этому) и (повторного) создания того, что зависит от размера вашего представления. Вы можете увидеть этот метод как View.onSizeChanged (int w, int h, int oldw, int oldh). Этот метод тоже не часто вызывается.

* Наконец, в DrawFrame вызывается каждый раз, когда ваши представления будут отображаться на экране, другими словами очень часто. Вы можете видеть это как метод View.onDraw (Canvas Canvas) и, следовательно, лучшие практики в отношении производительности также применяются здесь (например: не создавайте экземпляры объектов в этом методе и т. Д.).

Бонус: вы можете попросить, чтобы ваше представление не перерисовывалось каждый раз, а только тогда, когда оно грязное. Для этого вызовите GLSurfaceView.setRenderMode (int) с параметром RENDERMODE_WHEN_DIRTY. Затем вызовите GLSurfaceView.requestRender (), чтобы указать, что ваше представление грязное.

Примечание. SurfaceView не является видом, в отличие от других, он отображается под вашим действием, в котором есть отверстие, через которое вы можете видеть свою поверхность. Если вы хотите получить что-то более классическое, вы можете использовать TextureView. GLTextureView отсутствует, но вы можете найти реализацию от Романа Нурика в Музее: https://github.com/romannurik/muzei/blob/master/main/src/main/java/com/google/android/apps /muzei/render/GLTextureView.java .
Сам по себе OpenGL не невозможен без GLSurfaceView или GLTextureView, но использовать их гораздо проще, чем заботиться обо всем этом самостоятельно, особенно вначале.

Как работает OpenGL

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

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

  1. Мы передаем в вершинный шейдер координаты вершин. Он их преобразует и передает в растеризатор. Эти вершины образуют треугольники, которые являются базовыми кубиками 3D-сцены.
  2. Растеризатор заполнит треугольники фрагментами, чтобы их можно было отобразить на экране. Фрагменты - это набор состояний, которые позволяют вычислить конечные пиксели.
  3. Для каждого фрагмента вызывается шейдер фрагмента, который задает цвет для отображения на экране.
  4. В конечном итоге данные объединяются для визуализации на экране или отправки в текстуру в виде пикселей.

Программы GLSL (или шейдеры)

GLSL является сокращением от языка Open GL S hader L и является названием языка, на котором мы программируем OpenGL. Ключевое слово - «шейдер». Это странное слово, которое вы, возможно, слышали раньше, но не понимаете, на самом деле простое. Это часть программы OpenGL, которая будет выполняться на графическом процессоре.

Более того, существует несколько типов шейдеров. Из них два, которые нам интересны:

  • Шейдер вершин: отвечает за вычисление позиции рендеринга. Мы скармливаем ему набор атрибутов, связанных с точкой в ​​трехмерном пространстве, и он вычисляет положение на экране. Этот набор атрибутов чаще всего состоит из координат и цвета (или координат текстуры). Вершинный шейдер вызывается один раз для каждой вершины.
  • Шейдер фрагмент: отвечает за вычисление цвета для каждого пикселя. В качестве входных данных он получает выходные данные вершинного шейдера. Этот код выполняется для каждого пикселя вашего изображения. Чтобы было понятнее, графический процессор оптимизирует большинство своих вызовов, чтобы обеспечить максимальную производительность для рендеринга и способен вычислять значение нескольких пикселей параллельно. Мы часто представляем это так, как будто графический процессор вычисляет каждый пиксель одновременно, но важно выйти из этого положения: мы не начинаем с левого верхнего угла, а заканчиваем правым нижним. Если вы выберете информацию для пикселя [0,0], когда придет время вычислить пиксель [0,1], у вас не будет этой информации.

Очень простой пример этих двух шейдеров можно найти в примере проекта:

Vertex shader :
precision mediump float;
uniform mat4 uMVPMatrix;
attribute vec4 vPosition;
attribute vec4 vTextureCoordinate;
varying vec2 position;
void main() {
  gl_Position = uMVPMatrix * vPosition;
  position = vTextureCoordinate.xy;
}
Fragment shader :
precision mediump float;
uniform sampler2D uTexture;
varying vec2 position;
void main() {
  gl_FragColor = texture2D(uTexture, position);
}

В вершинном шейдере мы получаем 3 параметра:

  • uMVPMatrix: матрица, которая позволяет нам изменять точку обзора, поворот и масштаб.
  • vPosition: координаты, которые будут формировать нашу «полосу».
  • vTextureCoordinate: координаты каждой нашей вершины.

Во фрагментном шейдере мы получаем 2 параметра:

  • uTexture: текстура, содержащая изображение для показа.
  • position: параметр, полученный от вершинного шейдера, который содержит позицию отображаемого пикселя.

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

Введите матрицу (Системы координат)

Система координат OpenGL не заботится о размере экрана, как показано на рисунке ниже:

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

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

По-прежнему в примере проекта переданный массив состоит из 4 вершин, начиная с нижнего левого угла, переходя в нижний правый угол и заканчивая верхним. в левом углу и заканчивая в правом верхнем углу.

Координаты текстуры немного отличаются в зависимости от ориентации загруженного изображения.

Кроме того, если вы присмотритесь, вы заметите, что переданные координаты для позиции меняются от -1 до 1, а переданные координаты для текстур изменяется от 0 до 1. Кроме того, координаты позиции содержат глубину в качестве координаты Z, но это не обязательно, если вы используете только 2D.

В вершинном шейдере, приведенном в качестве примера, есть умножение между uMVPMatrix и vPosition. vPosition - это матрица, содержащая координаты, упомянутые ранее. uMVPMatrix - это матрица, сформированная с помощью служебных методов, предоставляемых классом Matrix из фреймворка Android. GLSL может выполнять умножение матриц и обрабатывать их просто и эффективно.
Еще один нюанс, я использовал vTextureCoordinate .xy. Это создает вектор размера 2, содержащий первое и второе значение вектора размера 4, который является vTextureCoordinate. Я мог бы сделать: vTextureCoordinate .xx, чтобы создать вектор размера 2 с первым значением vTextureCoordinate для обоих значений.

Обратите внимание на небольшую тонкость, с которой вы можете столкнуться при чтении шейдеров: чтобы быть более правильным и уважать соглашения об именах, мне следовало использовать vTextureCoordinate. st. STPQ заменяет XYZW, когда речь идет о координатах текстуры, и RGBA, когда речь идет о цветах. В любом случае, использование того или другого не изменит выполнение, только его читабельность.

Текстуры

Текстуры - это области памяти, в которых графический процессор (ГП) хранит изображения. Либо для их рендеринга, либо для записи новых значений.

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

Буферы (FBO)

F rame B uffer O bject (или FBO) накладывается на текстуру и пишет в ней. Без FBO все команды OpenGL, которые вы выполняете, будут отображаться на экране. Это было бы довольно неприятно, если бы вы добились некоторого эффекта, комбинируя некоторые другие эффекты.

Код для создания FBO можно найти здесь.

Советы

В демонстрационном проекте я добровольно поместил все в один файл. Это было сделано для облегчения чтения, потому что, когда я учился, я заметил, что попытка найти фрагмент кода, который я пытаюсь понять, остановит меня в моем отражении и не поможет мне понять.
Тем не менее, когда у вас есть основы, я могу только посоветовать вам максимально абстрагировать ваш код за некоторыми классами Java. Например, создайте класс, который будет обрабатывать создание FBO за вас, поместите вашу настройку в класс, который расширит GLSurfaceView и извлечет логику шейдеров в их собственные классы (как это делает GPUImage это так хорошо) и т. д. Это сделает код чище и удобнее для чтения.

Сбои часто, но сбои хорошо. Отладка кода OpenGL особенно сложна. Проверяйте наличие ошибок и свои состояния (например: состояние ваших FBO или ошибку компиляции шейдеров) и генерируйте исключение RuntimeException, чтобы помочь вам увидеть, что и где не так. Это поможет вам во время разработки, а также при тестировании на разных устройствах.

Наконец, протестируйте вручную на нескольких устройствах и нескольких версиях Android. Вы не сможете уловить все, но вы уже уловите множество мелких деталей. Также протестируйте флагманы, такие как серия Galaxy S или серия Galaxy Note от Samsung , но также на более скромных устройствах. Не забывайте некоторые известные китайские бренды, такие как Wiko или Xiaomi.
Если у вас не так много устройств и / или вы хотите перестраховаться, опубликуйте свое приложение на альфа- и бета-каналах в Play Маркете, чтобы получить отзывы. от ваших пользователей, прежде чем это перейдет к более широкой аудитории.

Заключение

Что ж, вы поняли, разработка для OpenGL, это много шаблонов, математики и возни. Тем не менее, будьте уверены, многие люди прошли через это до вас, и они столкнулись с той же проблемой, с которой столкнетесь вы, поэтому на этом пути будет помощь.
Надеюсь, в этой статье я смог выделить некоторые ключевые концепции, которые помогут вам понять сообщения StackOverflow, когда вы будете искать ответы на свои вопросы.

Как упоминалось ранее, руководство, предоставленное веб-сайтом разработчика Android, действительно хорошее, даже если не хватает нескольких моментов, которые, я надеюсь, были объяснены здесь. Он также поставляется с отличным образцом проекта, который может стать огромным источником вдохновения, и предлагает несколько утилит, которые вам очень помогут.