Платформа Android Media Effects позволяет разработчикам легко применять множество впечатляющих визуальных эффектов к фотографиям и видео. Поскольку каркас использует графический процессор для выполнения всех своих операций по обработке изображений, он может принимать только текстуры OpenGL в качестве входных данных. В этом руководстве вы узнаете, как использовать OpenGL ES 2.0 для преобразования нарисованного ресурса в текстуру, а затем использовать среду для применения к ней различных эффектов.
Предпосылки
Чтобы следовать этому уроку, вам необходимо иметь:
- IDE, которая поддерживает разработку приложений для Android. Если у вас его нет, загрузите последнюю версию Android Studio с веб-сайта Android Developer .
- устройство под управлением Android 4.0+ и с графическим процессором, поддерживающим OpenGL ES 2.0.
- базовое понимание OpenGL.
1. Настройка среды OpenGL ES
Шаг 1: Создайте GLSurfaceView
Для отображения графики OpenGL в вашем приложении вы должны использовать объект GLSurfaceView . Как и любой другой View , вы можете добавить его в Activity или Fragment , определив его в XML-файле макета или создав его экземпляр в коде.
В этом руководстве вы будете иметь объект GLSurfaceView в качестве единственного View в вашей Activity . Поэтому создать его в коде проще. После создания передайте его методу setContentView чтобы он занимал весь экран. Метод onCreate вашей Activity должен выглядеть следующим образом:
|
1
2
3
4
5
6
|
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GLSurfaceView view = new GLSurfaceView(this);
setContentView(view);
}
|
Поскольку среда Media Effects поддерживает только OpenGL ES 2.0 или выше, передайте значение 2 методу setEGLContextClientVersion .
|
1
|
view.setEGLContextClientVersion(2);
|
Чтобы убедиться, что GLSurfaceView отображает свое содержимое только при необходимости, передайте значение RENDERMODE_WHEN_DIRTY в метод setRenderMode .
|
1
|
view.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
|
Шаг 2. Создайте рендерер
GLSurfaceView.Renderer отвечает за рисование содержимого GLSurfaceView .
Создайте новый класс, который реализует интерфейс GLSurfaceView.Renderer . Я собираюсь назвать этот класс EffectsRenderer . После добавления конструктора и переопределения всех методов интерфейса класс должен выглядеть следующим образом:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
public class EffectsRenderer implements GLSurfaceView.Renderer {
public EffectsRenderer(Context context){
super();
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
}
@Override
public void onDrawFrame(GL10 gl) {
}
}
|
Вернитесь к своей Activity и вызовите метод setRenderer чтобы GLSurfaceView использовал пользовательский рендер.
|
1
|
view.setRenderer(new EffectsRenderer(this));
|
Шаг 3: Редактировать Манифест
Если вы планируете опубликовать свое приложение в Google Play, добавьте в AndroidManifest.xml следующее:
|
1
|
<uses-feature android:glEsVersion=»0x00020000″ android:required=»true» />
|
Это гарантирует, что ваше приложение может быть установлено только на устройствах, которые поддерживают OpenGL ES 2.0. Среда OpenGL теперь готова.
2. Создание плоскости OpenGL
Шаг 1: Определить вершины
GLSurfaceView не может отображать фотографию напрямую. Фотография должна быть преобразована в текстуру и применена сначала к форме OpenGL. В этом уроке мы будем создавать 2D-плоскость с четырьмя вершинами. Ради простоты, давайте сделаем это квадратом. Создайте новый класс Square для представления квадрата.
|
1
2
3
|
public class Square {
}
|
Система координат OpenGL по умолчанию имеет свое начало в центре. В результате координаты четырех углов нашего квадрата, стороны которых имеют длину в две единицы , будут:
- нижний левый угол в (-1, -1)
- нижний правый угол в (1, -1)
- верхний правый угол в (1, 1)
- верхний левый угол в (-1, 1)
Все объекты, которые мы рисуем с использованием OpenGL, должны состоять из треугольников. Чтобы нарисовать квадрат, нам нужно два треугольника с общим краем. Это означает, что координаты треугольников будут:
треугольник 1: (-1, -1), (1, -1) и (-1, 1)
треугольник 2: (1, -1), (-1, 1) и (1, 1)
Создайте массив с float для представления этих вершин.
|
1
2
3
4
5
6
|
private float vertices[] = {
-1f, -1f,
1f, -1f,
-1f, 1f,
1f, 1f,
};
|
Чтобы отобразить текстуру на квадрат, вам необходимо указать координаты вершин текстуры. Текстуры следуют за системой координат, в которой значение y-координаты увеличивается с ростом. Создайте еще один массив для представления вершин текстуры.
|
1
2
3
4
5
6
|
private float textureVertices[] = {
0f,1f,
1f,1f,
0f,0f,
1f,0f
};
|
Шаг 2: Создание объектов буфера
Массивы координат должны быть преобразованы в байтовые буферы, прежде чем OpenGL сможет их использовать. Давайте сначала объявим эти буферы.
|
1
2
|
private FloatBuffer verticesBuffer;
private FloatBuffer textureBuffer;
|
Напишите код для инициализации этих буферов в новом методе initializeBuffers . Используйте метод ByteBuffer.allocateDirect для создания буфера. Поскольку float использует 4 байта, вам необходимо умножить размер массивов на значение 4 .
Затем используйте ByteBuffer.nativeOrder чтобы определить порядок байтов базовой нативной платформы и установить порядок буферов в это значение. Используйте метод asFloatBuffer для преобразования экземпляра ByteBuffer в FloatBuffer . После того, как FloatBuffer создан, используйте метод put чтобы загрузить массив в буфер. Наконец, используйте метод position чтобы убедиться, что буфер читается с самого начала.
Содержимое метода initializeBuffers должно выглядеть так:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
private void initializeBuffers(){
ByteBuffer buff = ByteBuffer.allocateDirect(vertices.length * 4);
buff.order(ByteOrder.nativeOrder());
verticesBuffer = buff.asFloatBuffer();
verticesBuffer.put(vertices);
verticesBuffer.position(0);
buff = ByteBuffer.allocateDirect(textureVertices.length * 4);
buff.order(ByteOrder.nativeOrder());
textureBuffer = buff.asFloatBuffer();
textureBuffer.put(textureVertices);
textureBuffer.position(0);
}
|
Шаг 3: Создание шейдеров
Пришло время написать свои собственные шейдеры. Шейдеры — это не что иное, как простые программы на Си, которые запускаются графическим процессором для обработки каждой отдельной вершины. Для этого урока вы должны создать два шейдера, вершинный шейдер и фрагментный шейдер.
Код C для вершинного шейдера:
|
1
2
3
4
5
6
7
|
attribute vec4 aPosition;
attribute vec2 aTexPosition;
varying vec2 vTexPosition;
void main() {
gl_Position = aPosition;
vTexPosition = aTexPosition;
};
|
Код C для фрагментного шейдера:
|
1
2
3
4
5
6
|
precision mediump float;
uniform sampler2D uTexture;
varying vec2 vTexPosition;
void main() {
gl_FragColor = texture2D(uTexture, vTexPosition);
};
|
Если вы уже знаете OpenGL, этот код должен быть вам знаком, потому что он распространен на всех платформах. В противном случае, чтобы понять эти программы, вы должны обратиться к документации OpenGL . Вот краткое объяснение, с чего можно начать:
- Вершинный шейдер отвечает за отрисовку отдельных вершин.
aPosition— это переменная, которая будет связана сFloatBufferкоторый содержит координаты вершин. Аналогично,aTexPosition— это переменная, которая будет привязана кFloatBufferкоторый содержит координаты текстуры.gl_Positionявляется встроенной переменной OpenGL и представляет положение каждой вершины.vTexPosition— этоvaryingпеременная, значение которой просто передается фрагментному шейдеру. - В этом уроке фрагментный шейдер отвечает за раскраску квадрата. Он выбирает цвета из текстуры, используя метод
texture2Dи назначает их фрагменту, используя встроенную переменную с именемgl_FragColor.
Код шейдера должен быть представлен как класс String в классе.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
private final String vertexShaderCode =
«attribute vec4 aPosition;»
«attribute vec2 aTexPosition;»
«varying vec2 vTexPosition;»
«void main() {» +
» gl_Position = aPosition;»
» vTexPosition = aTexPosition;»
«}»;
private final String fragmentShaderCode =
«precision mediump float;»
«uniform sampler2D uTexture;»
«varying vec2 vTexPosition;»
«void main() {» +
» gl_FragColor = texture2D(uTexture, vTexPosition);»
«}»;
|
Шаг 4: Создать программу
Создайте новый метод initializeProgram для создания программы OpenGL после компиляции и компоновки шейдеров.
Используйте glCreateShader чтобы создать объект шейдера и вернуть ссылку на него в форме типа int . Чтобы создать вершинный шейдер, передайте ему значение GL_VERTEX_SHADER . Аналогично, чтобы создать фрагментный шейдер, передайте ему значение GL_FRAGMENT_SHADER . Затем используйте glShaderSource чтобы связать соответствующий код шейдера с шейдером. Используйте glCompileShader для компиляции кода шейдера.
После компиляции обоих шейдеров создайте новую программу, используя glCreateProgram . Так же как и glCreateShader , он также возвращает int как ссылку на программу. Вызовите glAttachShader чтобы присоединить шейдеры к программе. Наконец, вызовите glLinkProgram чтобы связать программу.
Ваш метод и связанные переменные должны выглядеть так:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
private int vertexShader;
private int fragmentShader;
private int program;
private void initializeProgram(){
vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
GLES20.glShaderSource(vertexShader, vertexShaderCode);
GLES20.glCompileShader(vertexShader);
fragmentShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
GLES20.glShaderSource(fragmentShader, fragmentShaderCode);
GLES20.glCompileShader(fragmentShader);
program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
}
|
Вы могли заметить, что методы OpenGL (методы с префиксом gl ) принадлежат классу GLES20 . Это потому, что мы используем OpenGL ES 2.0. Если вы хотите использовать более высокую версию, то вам придется использовать классы GLES30 или GLES31 .
Шаг 5: Нарисуй квадрат
Создайте новый метод draw чтобы фактически нарисовать квадрат, используя вершины и шейдеры, которые мы определили ранее.
Вот что вам нужно сделать в этом методе:
- Используйте
glBindFramebufferчтобы создать именованный объект буфера кадра (часто называемый FBO). - Используйте
glUseProgramчтобы начать использовать программу, которую мы только что связали. - Передайте значение
GL_BLENDвglDisableчтобы отключить смешение цветов при рендеринге. - Используйте
glGetAttribLocationчтобы получить дескриптор переменныхaPositionиaTexPositionупомянутых в коде вершинного шейдера. - Используйте
glGetUniformLocationчтобы получить дескриптор константыuTextureупомянутой в коде фрагмента шейдера. - Используйте
glVertexAttribPointerчтобы связатьaPositionиaTexPositionсverticesBufferиtextureBufferсоответственно. - Используйте
glBindTextureчтобы связать текстуру (передаваемую в качестве аргумента методуdraw) с фрагментным шейдером. - Очистите содержимое
GLSurfaceViewс помощьюglClear. - Наконец, используйте метод
glDrawArraysчтобы фактически нарисовать два треугольника (и, следовательно, квадрат).
Код для метода draw должен выглядеть следующим образом:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public void draw(int texture){
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
GLES20.glUseProgram(program);
GLES20.glDisable(GLES20.GL_BLEND);
int positionHandle = GLES20.glGetAttribLocation(program, «aPosition»);
int textureHandle = GLES20.glGetUniformLocation(program, «uTexture»);
int texturePositionHandle = GLES20.glGetAttribLocation(program, «aTexPosition»);
GLES20.glVertexAttribPointer(texturePositionHandle, 2, GLES20.GL_FLOAT, false, 0, textureBuffer);
GLES20.glEnableVertexAttribArray(texturePositionHandle);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);
GLES20.glUniform1i(textureHandle, 0);
GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 0, verticesBuffer);
GLES20.glEnableVertexAttribArray(positionHandle);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
}
|
Добавьте конструктор к классу, чтобы инициализировать буферы и программу во время создания объекта.
|
1
2
3
4
|
public Square(){
initializeBuffers();
initializeProgram();
}
|
3. Визуализация плоскости и текстуры OpenGL
В настоящее время наш рендерер ничего не делает. Нам нужно изменить это так, чтобы он мог отображать плоскость, которую мы создали на предыдущих шагах.
Но сначала давайте создадим Bitmap . Добавьте любую фотографию в папку res / drawable вашего проекта. Файл, который я использую, называется forest.jpg . Используйте BitmapFactory для преобразования фотографии в Bitmap объект. Также сохраняйте размеры объекта Bitmap в отдельных переменных.
Измените конструктор класса EffectsRenderer чтобы он имел следующее содержимое:
|
1
2
3
4
5
6
7
8
|
private Bitmap photo;
private int photoWidth, photoHeight;
public EffectsRenderer(Context context){
super();
photo = BitmapFactory.decodeResource(context.getResources(), R.drawable.forest);
photoWidth = photo.getWidth();
photoHeight = photo.getHeight();
}
|
Создайте новый метод generateSquare для преобразования растрового изображения в текстуру и инициализации объекта Square . Вам также понадобится массив целых чисел для хранения ссылок на текстуры OpenGL. Используйте glGenTextures для инициализации массива и glBindTexture для активации текстуры с индексом 0 .
Далее, используйте glTexParameteri чтобы установить различные свойства, которые определяют, как будет отображаться текстура:
- Установите
GL_TEXTURE_MIN_FILTER(минимизирующая функция) иGL_TEXTURE_MAG_FILTER(увеличительная функция) вGL_LINEAR, чтобы текстура выглядела гладкой, даже если она растянута или сжата. - Установите
GL_TEXTURE_WRAP_SиGL_TEXTURE_WRAP_TвGL_CLAMP_TO_EDGEчтобы текстура никогда не повторялась.
Наконец, используйте метод texImage2D для сопоставления Bitmap с текстурой. Реализация метода generateSquare должна выглядеть так:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
private int textures[] = new int[2];
private Square square;
private void generateSquare(){
GLES20.glGenTextures(2, textures, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, photo, 0);
square = new Square();
}
|
Всякий раз, когда размеры GLSurfaceView изменяются, onSurfaceChanged метод onSurfaceChanged . Здесь вы должны вызвать glViewPort чтобы указать новые размеры области просмотра. Также вызовите glClearColor чтобы закрасить GLSurfaceView черный цвет. Затем вызовите generateSquare для повторной инициализации текстур и плоскости.
|
1
2
3
4
5
6
|
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0,0,width, height);
GLES20.glClearColor(0,0,0,1);
generateSquare();
}
|
Наконец, вызовите метод draw объекта Square внутри метода onDrawFrame Renderer .
|
1
2
3
4
|
@Override
public void onDrawFrame(GL10 gl) {
square.draw(textures[0]);
}
|
Теперь вы можете запустить свое приложение и увидеть выбранную вами фотографию в виде текстуры OpenGL на плоскости.

4. Использование Media Effects Framework
Сложный код, который мы писали до сих пор, был просто предпосылкой для использования среды Media Effects. Пришло время начать использовать сам фреймворк. Добавьте следующие поля в ваш класс Renderer .
|
1
2
|
private EffectContext effectContext;
private Effect effect;
|
Инициализируйте поле EffectContext.createWithCurrentGlContext с помощью EffectContext.createWithCurrentGlContext . Он отвечает за управление информацией о визуальных эффектах в контексте OpenGL. Для оптимизации производительности это следует вызывать только один раз. Добавьте следующий код в начале вашего метода onDrawFrame .
|
1
2
3
|
if(effectContext==null) {
effectContext = EffectContext.createWithCurrentGlContext();
}
|
Создать эффект очень просто. Используйте effectContext для создания EffectFactory и используйте EffectFactory для создания объекта Effect . Как только объект Effect доступен, вы можете вызвать apply и передать ему ссылку на исходную текстуру, в нашем случае это textures[0] , а также ссылку на пустой объект текстуры, в нашем случае это textures[1] . После apply метода apply textures[1] будут содержать результат Effect .
Например, чтобы создать и применить эффект оттенков серого , вот код, который вы должны написать:
|
1
2
3
4
5
|
private void grayScaleEffect(){
EffectFactory factory = effectContext.getFactory();
effect = factory.createEffect(EffectFactory.EFFECT_GRAYSCALE);
effect.apply(textures[0], photoWidth, photoHeight, textures[1]);
}
|
Вызовите этот метод в onDrawFrame и передайте textures[1] в метод draw объекта Square . Ваш метод onDrawFrame должен иметь следующий код:
|
01
02
03
04
05
06
07
08
09
10
11
|
@Override
public void onDrawFrame(GL10 gl) {
if(effectContext==null) {
effectContext = EffectContext.createWithCurrentGlContext();
}
if(effect!=null){
effect.release();
}
grayScaleEffect();
square.draw(textures[1]);
}
|
Метод release используется для освобождения всех ресурсов, находящихся в Effect . Когда вы запустите приложение, вы должны увидеть следующий результат:

Вы можете использовать тот же код для применения других эффектов. Например, вот код для применения документального эффекта:
|
1
2
3
4
5
|
private void documentaryEffect(){
EffectFactory factory = effectContext.getFactory();
effect = factory.createEffect(EffectFactory.EFFECT_DOCUMENTARY);
effect.apply(textures[0], photoWidth, photoHeight, textures[1]);
}
|
Результат выглядит так:

Некоторые эффекты принимают параметры. Например, эффект регулировки яркости имеет параметр brightness который принимает значение с float . Вы можете использовать setParameter чтобы изменить значение любого параметра. Следующий код показывает вам, как его использовать:
|
1
2
3
4
5
6
|
private void brightnessEffect(){
EffectFactory factory = effectContext.getFactory();
effect = factory.createEffect(EffectFactory.EFFECT_BRIGHTNESS);
effect.setParameter(«brightness», 2f);
effect.apply(textures[0], photoWidth, photoHeight, textures[1]);
}
|
Эффект заставит ваше приложение отобразить следующий результат:

Вывод
В этом руководстве вы узнали, как использовать среду Media Effects Framework для применения различных эффектов к вашим фотографиям. При этом вы также узнали, как рисовать плоскость с помощью OpenGL ES 2.0 и применять к ней различные текстуры.
Рамки можно применять как к фотографиям, так и к видео. В случае видео вам просто нужно применить эффект к отдельным кадрам видео в методе onDrawFrame .
Вы уже видели три эффекта в этом уроке, и у фреймворка есть еще десятки возможностей для экспериментов. Чтобы узнать о них больше, обратитесь на сайт разработчика Android .