Статьи

Как использовать Android Media Effects с OpenGL ES

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

Чтобы следовать этому уроку, вам необходимо иметь:

  • IDE, которая поддерживает разработку приложений для Android. Если у вас его нет, загрузите последнюю версию Android Studio с веб-сайта Android Developer .
  • устройство под управлением Android 4.0+ и с графическим процессором, поддерживающим OpenGL ES 2.0.
  • базовое понимание OpenGL.

Для отображения графики 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);

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));

Если вы планируете опубликовать свое приложение в Google Play, добавьте в AndroidManifest.xml следующее:

1
<uses-feature android:glEsVersion=»0x00020000″ android:required=»true» />

Это гарантирует, что ваше приложение может быть установлено только на устройствах, которые поддерживают OpenGL ES 2.0. Среда OpenGL теперь готова.

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
};

Массивы координат должны быть преобразованы в байтовые буферы, прежде чем 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);
}

Пришло время написать свои собственные шейдеры. Шейдеры — это не что иное, как простые программы на Си, которые запускаются графическим процессором для обработки каждой отдельной вершины. Для этого урока вы должны создать два шейдера, вершинный шейдер и фрагментный шейдер.

Код 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);»
        «}»;

Создайте новый метод 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 .

Создайте новый метод draw чтобы фактически нарисовать квадрат, используя вершины и шейдеры, которые мы определили ранее.

Вот что вам нужно сделать в этом методе:

  1. Используйте glBindFramebuffer чтобы создать именованный объект буфера кадра (часто называемый FBO).
  2. Используйте glUseProgram чтобы начать использовать программу, которую мы только что связали.
  3. Передайте значение GL_BLEND в glDisable чтобы отключить смешение цветов при рендеринге.
  4. Используйте glGetAttribLocation чтобы получить дескриптор переменных aPosition и aTexPosition упомянутых в коде вершинного шейдера.
  5. Используйте glGetUniformLocation чтобы получить дескриптор константы uTexture упомянутой в коде фрагмента шейдера.
  6. Используйте glVertexAttribPointer чтобы связать aPosition и aTexPosition с verticesBuffer и textureBuffer соответственно.
  7. Используйте glBindTexture чтобы связать текстуру (передаваемую в качестве аргумента методу draw ) с фрагментным шейдером.
  8. Очистите содержимое GLSurfaceView с помощью glClear .
  9. Наконец, используйте метод 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();
}

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

Но сначала давайте создадим 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 на плоскости.

Сложный код, который мы писали до сих пор, был просто предпосылкой для использования среды 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 .