Перевод статьи подготовлен в преддверии старта курсов "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.
Как это работает с технической точки зрения
Давайте выберем родительский view для нашего кастомного view-класса (кстати, мы назовем наш view-класс
ShaderView
). Тут у нас есть два варианта:SurfaceView
иTextureView
. Я вернусь к разнице между ними через пару мгновений.Создадим класс
Render
, который будет отображатьview
с использованием шейдеров.Создадим 3D-модель четырехугольника (quadrangle), который заполнит все пространство view (3D, поскольку OpenGL был создан для 3D-сцен). Не беспокойтесь об этом; это стандартное решение, и с ним не связано никаких трудностей.
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. Нам всего лишь нужно скопировать пригодный код в наш класс и внести некоторые изменения.
Создайте новый класс
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 {
}
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) {
}
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)
}
В результате у вас получится что-то вроде этого.
Расширения
Чтобы облегчить себе работу, мы создадим расширения, которые загружают исходный код шейдера из каталога ресурсов.
fun Resources.getRawTextFile(@RawRes resource: Int): String =
openRawResource(resource).bufferedReader().use { it.readText() }
Код шейдеров
В этой статье мы будем использовать самые простые шейдеры, чтобы сделать наш код менее сложным. Если вас интересуют более сложные шейдеры, вы можете найти их здесь или здесь.
Вершинный шейдер (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;
}
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
Две переменные, которые будут содержать исходный код наших вершинного и фрагментного шейдеров.
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)
}
}
Определите матрицы.
private val matrixMVP = FloatArray(16)
private val matrixSTM = FloatArray(16)
И добавить инициализацию в init{}
блок.
init {
// код, который мы добавили ранее
Matrix.setIdentityM(matrixSTM, 0)
}
Вершинный шейдер, атрибуты вершин и расположение матриц.
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
Локатор программы шейдера.
private var program = UNKNOWN_PROGRAM
Отлично, мы закончили с инициализацией. Теперь давайте напишем метод 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")
}
Обратите внимание на последние три строки, где мы получаем расположение нашей кастомной формы (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")
}
Следующий метод, который мы должны реализовать, — это 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()
}
Обратите внимание на строки, в которых мы отправляем кастомное значение (uMyUniformValue
) в форму (uMyUniform
) во фрагментный шейдер.
И последнее, surfaceChange()
— довольно простой метод.
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
GLES30.glViewport(0, 0, width, height)
}
Полный код этого класса вы можете найти здесь.
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 если нужно обновлять его на каждом кадре
}
}
Дополнительно: Использование текстур в фрагментных шейдерах
Приятно, что мы научились общаться и работать с фрагментными шейдерами, но во многих случаях вам приходится иметь дело с текстурами (например, для размытия или других видеоэффектов).
Вам нужно определить форму во фрагментном шейдере как 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]
}
Теперь мы готовы загрузить текстуру из каталога ресурсов в виде растрового изображения (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.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)
Ссылки
Исходный код этой статьи можно найти в моем репозитории.
С библиотекой ShaderView с помощью дружественного высокоуровневого API можно познакомиться здесь.
Узнать подробнее о курсах: "Android Developer. Basic" / "Android Developer. Professional".
Также предлагаем посмотреть вебинары:
1) Рисуем свой график котировок в Android:
- Рассмотрим основные инструменты для рисования
- Изучим возможности классов Canvas, Path, Paint
- Нарисуем кастомизируемый график котировок и добавим в него анимаций
2) Крестики-нолики на минималках — Игра на Android менее чем за 2 часа, использующийся язык — Kotlin.