Pull to refresh
1302.6
OTUS
Цифровые навыки от ведущих экспертов

Как можно использовать шейдеры в Android View и как Android View может использовать шейдеры

Reading time17 min
Views5.4K
Original author: Alexey Glukharev

Перевод статьи подготовлен в преддверии старта курсов "Android Developer. Basic" и "Android Developer. Professional".


Для полноценной работы с динамическими элементами пользовательского интерфейса, которые используют тени, фильтры в реальном времени для фото или видео, или адаптивный пользовательский интерфейс и освещение, недостаточно использовать только Canvas. Было бы куда лучше, если бы у нас в распоряжении было что-то помощнее. Раньше мы могли использовать RenderScript, но поддерживается ли он сейчас?

В этой статье я расскажу как использовать стандартные GLSL шейдеры OpenGL в вашем пользовательском view, которое является наследником класса Android View (android.view.View). Я предлагаю вам использовать это решение, если вы работаете над чем-нибудь из нижеперечисленного:

  • Шейдеры или коррекция цвета в реальном времени для видеопотоков.

  • Динамические тени и освещение для кастомных элементов пользовательского интерфейса.

  • Продвинутая попиксельная анимация.

  • Какие-либо эффекты пользовательского интерфейса, наподобие размытия (blurring), искажения (distortion), пикселизации и т. д.

  • Если вы создаете новый нейроморфный адаптивный пользовательский интерфейс.

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

Идея

Нам нужно, чтобы в нашем стандартном лэйауте лежал класс, который ведет себя так же, как Android View (android.view.View), и мы cможем использовать фрагментный шейдер OpenGL для визуализации его содержимого.

Демо

Демо-приложение с несколькими ShaderViews. Динамический свет и видео фильтры.

Как это работает на абстрактном примере

Предположим, мы хотим заказать у одного известного художника новую картину, написанную волшебными красками, и повесить ее на стену. Что мы имеем в нашей ситуации:

  • Волшебные краски — GLSL шейдеры OpenGL.

  • Холст — четырехугольник, который заполнит все пространство нашего кастомного view.

  • Известный художник — класс, реализующий интерфейс Render. Этот художник, в свою очередь, использует волшебные краски, чтобы нарисовать картину на холсте.

  • Картина — кастомный view-класс, который задействует художника с его/ее холстом и волшебными красками.

  • Стена — Activity или Fragment android.

Как это работает с технической точки зрения

  1. Давайте выберем родительский view для нашего кастомного view-класса (кстати, мы назовем наш view-класс ShaderView). Тут у нас есть два варианта: SurfaceView и TextureView. Я вернусь к разнице между ними через пару мгновений.

  2. Создадим класс Render, который будет отображать view с использованием шейдеров.

  3. Создадим 3D-модель четырехугольника (quadrangle), который заполнит все пространство view (3D, поскольку OpenGL был создан для 3D-сцен). Не беспокойтесь об этом; это стандартное решение, и с ним не связано никаких трудностей.

Четырехугольник OpenGL внутри TextureView.
Четырехугольник OpenGL внутри TextureView.

SurfaceView или TextureView

SurfaceView и TextureView оба наследуются от класса Android View, но между ними есть некоторые различия.

На сегодняшний день SurfaceView имеет класс наследник, который отлично работает с OpenGL и обеспечивает отличную производительность. Этот класс называется GLSurfaceView. Но главная проблема этого класса в том, что мы не можем перекрывать один GLSurfaceView другим. Следовательно, мы не можем использовать его в нашей иерархии лейаутов, и мы не можем преобразовывать, анимировать или масштабировать view таким образом.

TextureView ведет себя как обычный android.view.View, и вы можете анимировать, преобразовывать, масштабировать или даже наслаивать его с другими экземплярами. Но это преимущество дается на ценой потребления большего количества памяти, чем SurfaceView, и вы теряете в производительности (в среднем 1–3 кадра).

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

Следующая проблема для нас заключается в том, что нет встроенного класса, который использует OpenGL render и TextureView. Но не спешите расстраиваться — GLSurfaceView подходит как раз для того, что нам нужно, но только с SurfaceView, поэтому давайте поразмыслим о том, как мы можем использовать этот класс для нашего собственного GLTextureView.

Создание GLTextureView

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

  1. Создайте новый класс GLTextureView.kt, который наследуется от TextureView и расширяет TextureView.SurfaceTextureListener и View.OnLayoutChangeListener. Добавьте конструкторы.

open class GLTextureView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) :
    TextureView(context, attrs, defStyleAttr),
    TextureView.SurfaceTextureListener,
    View.OnLayoutChangeListener {
}

GLTextureView.kt на GitHub

5. Обновите метод  finalize() до стандарта Kotlin. (Если у вас есть лучшее решение, напишите в комментариях).

6. Замените SurfaceHolder на SurfaceTexture.

7. Замените все упоминания GLSurfaceView на GLTextureView.

8. Обновите импорты, исключая использование GLSurfaceView. Также проверьте оставшиеся импорты и удалите все, что связано с GLSurfaceView.

9. Устранение проблемы с допустимостью нулевых значений после автоматического преобразования кода Java в Kotlin. В моем случае мне пришлось обновить методы переопределения и некоторые параметры, допускающие значение NULL (например, egl: EGL10 должно быть egl: EGL10?).

10. Переместите константы в объект-компаньон или на верхний уровень.

11. Удалите неподдерживаемые аннотации.

12. Добавьте методы интерфейса SurfaceTextureListener.

 override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
        surfaceCreated(surface)
        surfaceChanged(surface, 0, width, height)
    }

    override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
        surfaceChanged(surface, 0, width, height)
    }

    override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
        surfaceDestroyed(surface)
        return true
    }

    override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
    }

GLTextureView.kt на GitHub

13. В createSurface() вы наткнетесь на неработающую строчку, замените view.holder на view.surfaceTexture.

14. Переопределите onLayoutChange.

 override fun onLayoutChange(
        v: View?, left: Int, top: Int,
        right: Int,
        bottom: Int,
        oldLeft: Int,
        oldTop: Int,
        oldRight: Int,
        oldBottom: Int
    ) {
        surfaceChanged(surfaceTexture, 0, right - left, bottom - top)
    }

GLTextureView.kt на GitHub

В результате у вас получится что-то вроде этого.

Расширения

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

fun Resources.getRawTextFile(@RawRes resource: Int): String =
   openRawResource(resource).bufferedReader().use { it.readText() }

extensions.kt на GitHub

Код шейдеров

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

Вершинный шейдер (Vertex Shader)

Для наших целей нам достаточно простого вершинного шейдера для рендеринга нашего четырехугольника (мы не потратим кучу времени на его код).

#version 300 es

uniform mat4 uMVPMatrix;
uniform mat4 uSTMatrix;

in vec3 inPosition;
in vec2 inTextureCoord;

out vec2 textureCoord;

void main() {
   gl_Position = uMVPMatrix * vec4(inPosition.xyz, 1);
   textureCoord = (uSTMatrix * vec4(inTextureCoord.xy, 0, 0)).xy;
}

vertex.vsh на GitHub

Фрагментный/пиксельный шейдер (Fragment Shader)

Код довольно прост, но давайте посмотрим, что у нас здесь есть.

Прежде всего, мы определяем версию GLSL.

#version 300 es

Затем мы определяем пользовательские параметры, которые мы собираемся отправить шейдеру.

uniform vec4 uMyUniform;

Определяем параметры ввода и вывода для нашего фрагментного шейдера. In — что мы получаем от вершинного шейдера (в нашем случае координаты текстуры), а out — что отправляем в результате (цвет пикселя).

in vec2 textureCoord;
out vec4 fragColor;

Теперь напишем функцию, которая будет выполняться для каждого пикселя нашего Android View и возвращать его цвет.

void main() {
   fragColor = vec4(textureCoord.x, textureCoord.y, 1.0, 1.0) * uMyUniform;
}

В результате мы получим следующее:

#version 300 es

precision mediump float;

uniform vec4 uMyUniform;

in vec2 textureCoord;
out vec4 fragColor;

void main() {
    fragColor = vec4(textureCoord.x, textureCoord.y, 1.0, 1.0) * uMyUniform;
}

fragment_shader.fsh на GitHub

QuadRender

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

Четырехугольник OpenGL в проекции камеры. Камера — это точка зрения пользователя, который смотрит на устройство.

Наш класс должен расширить интерфейс GLTextureView.Renderer тремя методами:

onSurfaceCreated() — Создает программу шейдера, связывает некоторые параметры формы (uniform) и отправляет атрибуты в вершинный шейдер.

onDrawFrame() — Обновление на каждом кадре. В этом методе мы отрисовываем четырехугольник экрана и при необходимости обновляем параметры формы.

onSurfaceChanged() — Обновляет вьюпорт.

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

Определите константы.

private const val FLOAT_SIZE_BYTES = 4 // размер Float
private const val TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES // 5 float’ов для каждой вершины (3 float’а на позицию и 2 на координаты текстуры)
private const val TRIANGLE_VERTICES_DATA_POS_OFFSET = 0 // позиция начинается с начала массива каждой вершины
private const val TRIANGLE_VERTICES_DATA_UV_OFFSET = 3 // координаты текстуры начиная с 3-го float’а (4-й и 5-й float’ы)

// атрибуты вершинного шейдера

const val VERTEX_SHADER_IN_POSITION = "inPosition"
const val VERTEX_SHADER_IN_TEXTURE_COORD = "inTextureCoord"
const val VERTEX_SHADER_UNIFORM_MATRIX_MVP = "uMVPMatrix"
const val VERTEX_SHADER_UNIFORM_MATRIX_STM = "uSTMatrix"

const val FRAGMENT_SHADER_UNIFORM_MY_UNIFORM = "uMyUniform"

private const val UNKNOWN_PROGRAM = -1
private const val UNKNOWN_ATTRIBUTE = -1

QuadRender.kt на GitHub

Две переменные, которые будут содержать исходный код наших вершинного и фрагментного шейдеров.

private var vertexShaderSource : String, // исходный код вершинного шейдера
private var fragmentShaderSource : String, // исходный код фрагментного шейдера
QuadRender.kt на GitHub

Определите список вершин для буфера вершин.

private val quadVertices: FloatBuffer

init {
  // задаем массив вершин четырехугольника
   val quadVerticesData = floatArrayOf(
       // [x,y,z, U,V]
       -1.0f, -1.0f, 0f, 0f, 1f,
       1.0f, -1.0f, 0f, 1f, 1f,
       -1.0f, 1.0f, 0f, 0f, 0f,
       1.0f, 1.0f, 0f, 1f, 0f
   )

   quadVertices = ByteBuffer
       .allocateDirect(quadVerticesData.size * FLOAT_SIZE_BYTES)
       .order(ByteOrder.nativeOrder())
       .asFloatBuffer()
       .apply {
           put(quadVerticesData).position(0)
       }
}

QuadRender.kt на GitHub

Определите матрицы.

private val matrixMVP = FloatArray(16)
private val matrixSTM = FloatArray(16)

QuadRender.kt на GitHub

И добавить инициализацию в init{} блок.

init {
 // код, который мы добавили ранее

  Matrix.setIdentityM(matrixSTM, 0)
}

QuadRender.kt на GitHub

Вершинный шейдер, атрибуты вершин и расположение матриц.

private var inPositionHandle = UNKNOWN_ATTRIBUTE
private var inTextureHandle = UNKNOWN_ATTRIBUTE
private var uMVPMatrixHandle = UNKNOWN_ATTRIBUTE
private var uSTMatrixHandle = UNKNOWN_ATTRIBUTE

private var uMyUniform = UNKNOWN_ATTRIBUTE

QuadRender.kt на GitHub

Локатор программы шейдера.

private var program = UNKNOWN_PROGRAM

QuadRender.kt на GitHub

Отлично, мы закончили с инициализацией. Теперь давайте напишем метод onSurfaceCreated(). Мы загрузим и инициализируем наши шейдеры и получим указатели для атрибутов, включая параметр формы uMyUniform, который мы будем использовать для отправки некоторых пользовательских векторных данных во фрагментный шейдер.

override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
  //создаем программу шейдера из исходного кода
createProgram(vertexShaderSource, fragmentShaderSource)

  // связываем вектор атрибутов шейдера
   inPositionHandle = GLES20.glGetAttribLocation(program, VERTEX_SHADER_IN_POSITION)
   checkGlError("glGetAttribLocation $VERTEX_SHADER_IN_POSITION")
   if (inPositionHandle == UNKNOWN_ATTRIBUTE) throw RuntimeException("Could not get attrib location for $VERTEX_SHADER_IN_POSITION")
   inTextureHandle = GLES20.glGetAttribLocation(program, VERTEX_SHADER_IN_TEXTURE_COORD)
   checkGlError("glGetAttribLocation $VERTEX_SHADER_IN_TEXTURE_COORD")
   if (inTextureHandle == UNKNOWN_ATTRIBUTE) throw RuntimeException("Could not get attrib location for $VERTEX_SHADER_IN_TEXTURE_COORD")

   uMVPMatrixHandle = GLES20.glGetUniformLocation(program, VERTEX_SHADER_UNIFORM_MATRIX_MVP)
   checkGlError("glGetUniformLocation $VERTEX_SHADER_UNIFORM_MATRIX_MVP")
   if (uMVPMatrixHandle == UNKNOWN_ATTRIBUTE) throw RuntimeException("Could not get uniform location for $VERTEX_SHADER_UNIFORM_MATRIX_MVP")

   uSTMatrixHandle = GLES20.glGetUniformLocation(program, VERTEX_SHADER_UNIFORM_MATRIX_STM)
   checkGlError("glGetUniformLocation $VERTEX_SHADER_UNIFORM_MATRIX_STM")
   if (uSTMatrixHandle == UNKNOWN_ATTRIBUTE) throw RuntimeException("Could not get uniform location for $VERTEX_SHADER_UNIFORM_MATRIX_STM")

  // (!) связываем атрибуты фрагментного шейдера

   uMyUniform = GLES30.glGetUniformLocation(program, FRAGMENT_SHADER_UNIFORM_MY_UNIFORM)
   checkGlError("glGetUniformLocation $FRAGMENT_SHADER_UNIFORM_MY_UNIFORM")
   if (uMyUniform == UNKNOWN_ATTRIBUTE) throw RuntimeException("Could not get uniform location for $FRAGMENT_SHADER_UNIFORM_MY_UNIFORM")
}

QuadRender.kt на GitHub

Обратите внимание на последние три строки, где мы получаем расположение нашей кастомной формы (uMyUniform) для фрагментного шейдера. Для более сложных шейдеров нам придется добавить больше таких параметров.

В onSurfaceCreated() мы использовали специальные методы для создания и связывания программы.

/**
* Создаем программу шейдера из исходного кода вершинного и фрагментного шейдера
*/
private fun createProgram(vertexSource: String, fragmentSource: String): Boolean {
   if (program != UNKNOWN_PROGRAM) {
      // удаляем программу
       GLES30.glDeleteProgram(program)
       program = UNKNOWN_PROGRAM
   }
  // загружаем вершинный шейдер
   val vertexShader = loadShader(GLES30.GL_VERTEX_SHADER, vertexSource)
   if (vertexShader == UNKNOWN_PROGRAM) {
       return false
   }
   // загружаем фрагментный шейдер
   val pixelShader = loadShader(GLES30.GL_FRAGMENT_SHADER, fragmentSource)
   if (pixelShader == UNKNOWN_PROGRAM) {
       return false
   }
   program = GLES30.glCreateProgram()
   if (program != UNKNOWN_PROGRAM) {
       GLES30.glAttachShader(program, vertexShader)
       checkGlError("glAttachShader: vertex")
       GLES30.glAttachShader(program, pixelShader)
       checkGlError("glAttachShader: pixel")
       return linkProgram()
   }
   return true
}

private fun linkProgram(): Boolean {
   if (program == UNKNOWN_PROGRAM) {
       return false
   }
   GLES30.glLinkProgram(program)
   val linkStatus = IntArray(1)
   GLES30.glGetProgramiv(program, GLES30.GL_LINK_STATUS, linkStatus, 0)
   if (linkStatus[0] != GLES30.GL_TRUE) {
       Log.e(TAG, "Could not link program: ")
       Log.e(TAG, GLES30.glGetProgramInfoLog(program))
       GLES30.glDeleteProgram(program)
       program = UNKNOWN_PROGRAM
       return false
   }
   return true
}

private fun loadShader(shaderType: Int, source: String): Int {
   var shader = GLES30.glCreateShader(shaderType)
   if (shader != UNKNOWN_PROGRAM) {
       GLES30.glShaderSource(shader, source)
       GLES30.glCompileShader(shader)
       val compiled = IntArray(1)
       GLES30.glGetShaderiv(shader, GLES30.GL_COMPILE_STATUS, compiled, 0)
       if (compiled[0] == UNKNOWN_PROGRAM) {
           Log.e(TAG, "Could not compile shader $shaderType:")
           Log.e(TAG, GLES30.glGetShaderInfoLog(shader))
           GLES30.glDeleteShader(shader)
           shader = UNKNOWN_PROGRAM
       }
   }
   return shader
}

private fun checkGlError(op: String) {
   var error: Int
   while (GLES30.glGetError().also { error = it } != GLES30.GL_NO_ERROR) {
       Log.e(TAG, "$op: glError $error")
       throw RuntimeException("$op: glError $error")
   }

QuadRender.kt на GitHub

Следующий метод, который мы должны реализовать, — это onDrawFrame().

override fun onDrawFrame(gl: GL10?) {
   // очищаем наш "экран"
   GLES30.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
   GLES30.glClear(GLES30.GL_DEPTH_BUFFER_BIT or GLES30.GL_COLOR_BUFFER_BIT)

   // используем программу
   GLES30.glUseProgram(program)

   // устанавливаем ввод шейдера (встроенные атрибуты)
   setAttribute(inPositionHandle, VERTEX_SHADER_IN_POSITION, 3, TRIANGLE_VERTICES_DATA_POS_OFFSET) // 3 потому что 3 float’а на позицию
   setAttribute(inTextureHandle, VERTEX_SHADER_IN_TEXTURE_COORD, 2, TRIANGLE_VERTICES_DATA_UV_OFFSET) // 2 потому что 2 float’а на координаты текстуры

  // обновляем матрицу
   Matrix.setIdentityM(matrixMVP, 0)
   GLES30.glUniformMatrix4fv(uMVPMatrixHandle, 1, false, matrixMVP, 0)
   GLES30.glUniformMatrix4fv(uSTMatrixHandle, 1, false, matrixSTM, 0)

  // (!) обновляем формы для фрагментного шейдера
   val uMyUniformValue = floatArrayOf(1.0f, 0.75f, 0.95f, 1f) // некоторые значения, которые мы собираемся передать фрагментному шейдеру
   GLES30.glUniform4fv(uMyUniform, 1, uMyUniformValue, 0)

   // активируем смешивание текстур (для поддержки прозрачности)
   GLES30.glBlendFunc(GLES30.GL_SRC_ALPHA, GLES30.GL_ONE_MINUS_SRC_ALPHA)
   GLES30.glEnable(GLES20.GL_BLEND)

  // отрисовываем наши четырехугольники
   GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4)
   checkGlError("glDrawArrays")

   GLES30.glFinish()
}

QuadRender.kt на GitHub

Обратите внимание на строки, в которых мы отправляем кастомное значение (uMyUniformValue) в форму (uMyUniform) во фрагментный шейдер.

И последнее, surfaceChange() — довольно простой метод.

override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
   GLES30.glViewport(0, 0, width, height)
}

QuadRender.kt на GitHub

Полный код этого класса вы можете найти здесь.

ShaderView

Отлично, все, что нам нужно для нашего ShaderView, готово. Теперь мы можем использовать мощь фрагментного шейдера для рендеринга его содержимого! Создадим ShaderView.

private const val OPENGL_VERSION = 3

class ShaderView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) :
   GLTextureView(context, attrs, defStyleAttr) {

   init {
      // определяем версию OpenGL
       setEGLContextClientVersion(OPENGL_VERSION)

      // загружаем исходный код шейдеров из файлов 
       val vsh = context.resources.getRawTextFile(R.raw.vertex_shader)
       val fsh = context.resources.getRawTextFile(R.raw.fragment_shader)

      // устанавливаем рендерер
       setRenderer(QuadRender(vertexShaderSource = vsh, fragmentShaderSource = fsh))

       // устанавливаем режим рендеринга RENDERMODE_WHEN_DIRTY или RENDERMODE_CONTINUOUSLY
       setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY) // или GLSurfaceView.RENDERMODE_CONTINUOUSLY если нужно обновлять его на каждом кадре
   }
}

ShaderView.kt на GitHub

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

Приятно, что мы научились общаться и работать с фрагментными шейдерами, но во многих случаях вам приходится иметь дело с текстурами (например, для размытия или других видеоэффектов).

Вам нужно определить форму во фрагментном шейдере как sampler2D и получить текущий пиксель текстуры по координатам текстуры с помощью метода texture() из GLSL.

Вот полный код шейдера.

#version 300 es
precision mediump float;

uniform sampler2D uTexture;

in vec2 textureCoord;
out vec4 fragColor;

void main() {
   fragColor = texture(uTexture, textureCoord);
}

fragment_texture_shader.fsh на GitHub

Затем нам понадобятся два расширения для загрузки и использования растрового изображения в качестве текстур OpenGL.

fun Resources.loadBitmapForTexture(@DrawableRes drawableRes: Int): Bitmap {
   val options = BitmapFactory.Options()
   options.inScaled = false // по умолчанию true. false, если нам нужно масштабируемое изображение

  // загрузка из ресурсов
   return BitmapFactory.decodeResource(this, drawableRes, options)
}

/**
* Загрузка текстуры из Bitmap и запись ее в видеопамять
* @needToRecycle - нужно ли нам повторно использовать текущий Bitmap, когда пишем это GPI?
*/
@Throws(RuntimeException::class)
fun Bitmap.toGlTexture(needToRecycle: Boolean = true, textureSlot: Int = GLES30.GL_TEXTURE0): Int {
  // инициализация текстуры
   val textureIds = IntArray(1)
   GLES30.glGenTextures(1, textureIds, 0) // генерируем ID для текстуры
   if (textureIds[0] == 0) {
       throw java.lang.RuntimeException("It's not possible to generate ID for texture")
   }
   
   GLES30.glActiveTexture(textureSlot) // активируем слот #0 для текстуры
   GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureIds[0]) // привязываем текстуру по ID к активному слоту

  // фильтры текстуры
   GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR)
   GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR)

  // записываем растровое изображение в GPU
   GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, this, 0)
  // нам больше не нужно это растровое изображение
   if (needToRecycle) {
       this.recycle()
   }

  // отвязываем текстуру от слота
   GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0)

   return textureIds[0]
}

extensions.kt на GitHub

Теперь мы готовы загрузить текстуру из каталога ресурсов в виде растрового изображения (bitmap), используя loadBitmapForTexture(), а затем метод QuadRender.onSurfaceCreated(). Мы привяжем текстуру к слоту текстуры OpenGL (доступны слоты от GL_TEXTURE0 до GL_TEXTURE31).

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

uTextureHandle = GLES30.glGetUniformLocation(program, FRAGMENT_SHADER_UNIFORM_TEXTURE)
uTextureId = textureBitmap.toGlTexture(needToRecycle = true, GLES30.GL_TEXTURE0)

QuadRender.kt на GitHub

После этого, мы устанавливаем эту текстуру в качестве активной и видимой для фрагментного шейдера в QuadRender.onDrawFrame().

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

GLES30.glUniform1i(uTextureHandle, 0) // 0 as far as it's slot number 0// 0 если номер слота 0
GLES30.glActiveTexture(GLES30.GL_TEXTURE0) // тот же слот текстуры, который мы использовали при инициализации
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, uTextureId)

QuadRender.kt на GitHub

Ссылки

Исходный код этой статьи можно найти в моем репозитории.

С библиотекой ShaderView с помощью дружественного высокоуровневого API можно познакомиться здесь.


Узнать подробнее о курсах: "Android Developer. Basic" / "Android Developer. Professional".

Также предлагаем посмотреть вебинары:

1)
Рисуем свой график котировок в Android:
- Рассмотрим основные инструменты для рисования
- Изучим возможности классов Canvas, Path, Paint
- Нарисуем кастомизируемый график котировок и добавим в него анимаций

2)
Крестики-нолики на минималках — Игра на Android менее чем за 2 часа, использующийся язык — Kotlin.

Tags:
Hubs:
Total votes 8: ↑8 and ↓0+8
Comments0

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS