Платформа 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 .