Введение
Сей эпичный труд появился благодаря нескольким событиям.Во-первых, в эмуляторе Android появилась поддержка аппаратного видеоускорения, что позволяет с полной скоростью работать не только интерфейсу, но и тестировать программы, использующие OpenGL ES 2.0.
Во-вторых, близится день рождения любимой супруги, а лучшим дополнением к новому смартфону или планшету будет собственноручно написанная программа-открытка для него.
Сказано — сделано: создаём канву проекта по андроидному Tutorial'у, достаём с полки старые Direct3D-проекты с использованием загрузки файлов .3ds, рендера-в-текстуру и пачки шейдеров, переписываем на Java и OpenGL ES 2.0, получаем то, что на картинке. Текст поздравления и тому подобное добавим потом.
Вся информация по использованию OpenGL ES 2.0 на Android оказалась сильно разрозненной, знания собирались по крупицам… Надеюсь, этот пост поможет тем, кто в будущем столкнётся с теми же трудностями, что и я.
А теперь подробнее.
Подготовка
Первое, что надо сделать — включить аппаратное ускорение в эмуляторе. Это делается либо через AVD Manager (см. скриншот; не забываем установить значение в «yes»), либо добавлением в файл ".android/avd/<имя_вашего_эмулятора>.avd/config.ini" строчки «hw.gpu.enabled=yes». Тут есть одна тонкость: аппаратное ускорение несовместимо со Snapshot'ами. Соответственно, эту галку мы снимаем (либо пишем в .ini-файле «snapshot.present=false»).

Далее следуем упомянутому выше Tutorial'у, чтобы создать всё необходимое, в частности наследника класса Renderer.
Модель
Под словом «модель» я здесь подразумеваю вот эту самую розочку. В сущности, можно использовать любой объект или целую сцену, это не важно.
Подразумевается, что весь дальнейший код расположен в том самом классе, унаследованном от класса Renderer.
Загрузка модели
Код загрузки файла .3ds я здесь приводить не буду: длинно, да и пост не о том (в принципе, это достойно отдельного поста), однако код отрисовки модели приведу, т.к., во-первых, очень уж много я грабель собрал по пути, во-вторых, он почти весь состоит из вызовов gl*, в-третьих, некоторые функции понадобятся ниже. Однако, если интересна только реализация эффекта, этот раздел можно пропустить. Итак, в итоге все данные модели уложились в такие структуры:
class Light3D { public float[] pos; public float[] color; } class Material3D { public float[] ambient; public float[] diffuse; } class FaceMat { public Material3D material; public int faces; public short[] indexBuffer; public int bufOffset; } class Object3D { public ArrayList<FaceMat> faceMats; public int vertCount; public int indCount; public int glVertices; public int glIndices; public float[] vertexBuffer; } public class Scene3D { public ArrayList<Material3D> materials; public ArrayList<Object3D> objects; public ArrayList<Light3D> lights; public float[] ambient; }
Здесь нарочно убраны такие тонкости, как блики (specular) и направленные источники света: сцена и так будет достаточно тяжёлой для отрисовки. Массив вершин объекта содержит 6*(кол-во вершин) вещественных чисел: координаты вершин и нормали, записанные подряд.
Отрисовка из массивов float/short оказалась небыстрой, а вот из буферов — вполне сносной (в зависимости от драйвера и видеоядра, эти данные могут сразу распологаться в видеопамяти). Конвертируем из одного в другое, отдельно вершинный, отдельно индексный. Не забываем после заполнения буфера закончить работу с ним, указав 0 в качестве активного буфера.
int[] genbuf = new int[1]; private int createBuffer(float[] buffer) { FloatBuffer floatBuf = ByteBuffer.allocateDirect(buffer.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); floatBuf.put(buffer); floatBuf.position(0); GLES20.glGenBuffers(1, genbuf, 0); int glBuf = genbuf[0]; GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, glBuf); GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, buffer.length * 4, floatBuf, GLES20.GL_STATIC_DRAW); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); return glBuf; } ... int i, num = scene.objects.size(); for (i = 0; i < num; i++) { Object3D obj = scene.objects.get(i); obj.glVertices = createBuffer(obj.vertexBuffer); GLES20.glGenBuffers(1, genbuf, 0); obj.glIndices = genbuf[0]; GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, obj.glIndices); GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, obj.indCount * 2, null, GLES20.GL_STATIC_DRAW); int k, mats = obj.faceMats.size(); for (k = 0; k < mats; k++) { FaceMat mat = obj.faceMats.get(k); ShortBuffer indBuf = ByteBuffer.allocateDirect(mat.indexBuffer.length * 2).order(ByteOrder.nativeOrder()).asShortBuffer(); indBuf.put(mat.indexBuffer); indBuf.position(0); GLES20.glBufferSubData(GLES20.GL_ELEMENT_ARRAY_BUFFER, mat.bufOffset * 2, mat.indexBuffer.length * 2, indBuf); } GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); }
Генерация вертексного буфера вынесена в отдельную функцию для того, чтобы использовать её же при создании квада.
Шейдеры для модели
Создаём шейдеры для отрисовки сцены:
private final String vertexShaderCode = "precision mediump float;\n" + "uniform mat4 uMVPMatrix;\n" + "uniform mat4 uMVMatrix;\n" + "uniform mat3 uNMatrix;\n" + "uniform vec4 uAmbient;\n" + "uniform vec4 uDiffuse;\n" + "const int MaxLights = 8;\n" + "struct LightSourceParameters {\n" + " bool enabled;\n" + " vec4 color;\n" + " vec3 position;\n" + "};\n" + "uniform LightSourceParameters uLight[MaxLights];\n" + "attribute vec4 vPosition;\n" + "attribute vec3 vNormal;\n" + "varying vec4 FrontColor;\n" + "vec4 light_point_view_local(vec3 epos, vec3 normal, int idx);\n" + "void main() {\n" + " gl_Position = uMVPMatrix * vPosition;\n" + " vec4 epos = uMVMatrix * vPosition;\n" + " vec3 normal =uNMatrix * vNormal;\n" + " vec4 vcolor = uAmbient;\n" + " int i;\n" + " for (i = 0; i < MaxLights; i++) {\n" + " if (uLight[i].enabled) {\n" + " vcolor += light_point_view_local(epos.xyz, normal, i);\n" + " }\n" + " }\n" + " FrontColor = clamp(vcolor, 0.0, 1.0);\n" + "}\n" + "vec4 light_point_view_local(vec3 epos, vec3 normal, int idx) {\n" + " vec3 vert2light = uLight[idx].position - epos;\n" + " vec3 ldir = normalize(vert2light);\n" + " float NdotL = dot(normal, ldir);\n" + " vec4 outCol = vec4(0.0, 0.0, 0.0, 1.0);\n" + " if (NdotL > 0.0) {\n" + " outCol = uLight[idx].color * uDiffuse * NdotL;\n" + " }\n" + " return outCol;\n" + "}\n"; private final String fragmentShaderCode = "precision mediump float;\n" + "varying vec4 FrontColor;\n" + "void main() {\n" + " gl_FragColor = FrontColor;\n" + "}\n"; private int mProgram; private int maPosition; private int maNormal; private int muMVPMatrix; private int muMVMatrix; private int muNMatrix; private int muAmbient; private int muDiffuse; private int[] muLightOn = new int[8]; private int[] muLightPos = new int[8]; private int[] muLightCol = new int[8];
Компилируем их, определяем расположение аттрибутов:
private int loadShader(int type, String shaderCode) { int shader = GLES20.glCreateShader(type); GLES20.glShaderSource(shader, shaderCode); GLES20.glCompileShader(shader); Log.i("Shader", GLES20.glGetShaderInfoLog(shader)); return shader; } private int Compile(String vs, String fs) { int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vs); int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fs); int prog = GLES20.glCreateProgram(); // create empty OpenGL Program GLES20.glAttachShader(prog, vertexShader); // add the vertex shader to program GLES20.glAttachShader(prog, fragmentShader); // add the fragment shader to program GLES20.glLinkProgram(prog); // creates OpenGL program executables return prog; } ... mProgram = Compile(vertexShaderCode, fragmentShaderCode); // get handle to the vertex shader's vPosition member maPosition = GLES20.glGetAttribLocation(mProgram, "vPosition"); maNormal = GLES20.glGetAttribLocation(mProgram, "vNormal"); muMVPMatrix = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); muMVMatrix = GLES20.glGetUniformLocation(mProgram, "uMVMatrix"); muNMatrix = GLES20.glGetUniformLocation(mProgram, "uNMatrix"); muAmbient = GLES20.glGetUniformLocation(mProgram, "uAmbient"); muDiffuse = GLES20.glGetUniformLocation(mProgram, "uDiffuse"); int i; for (i = 0; i < 8; i++) { muLightOn[i] = GLES20.glGetUniformLocation(mProgram, String.format("uLight[%d].enabled", i)); muLightPos[i] = GLES20.glGetUniformLocation(mProgram, String.format("uLight[%d].position", i)); muLightCol[i] = GLES20.glGetUniformLocation(mProgram, String.format("uLight[%d].color", i)); }
Подробно на работе этих шейдеров останавливаться не буду: фрагментный и так тривиален, а вершинный реализует обычную работу со всенаправленными источниками света.
Отрисовка модели
Предполагается, что у нас уже готовы матрицы преобразования Model, View и Projection (у меня, например, розочка плавно поворачивается). Из произведения Model-View выделяем только поворот, это нужно для работы с нормалями.
Отрисовка проста и приятна: берём созданные ранее буферы, назначаем аттрибуты, рисуем. Особо стоит отметить, что координаты источников света передаются в eye-space, для этого они домножаются на View-матрицу.
private void DrawScene() { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); GLES20.glUseProgram(mProgram); GLES20.glEnable(GLES20.GL_CULL_FACE); GLES20.glEnable(GLES20.GL_DEPTH_TEST); Matrix.multiplyMM(mMVMatrix, 0, mVMatrix, 0, mMMatrix, 0); Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mMVMatrix, 0); // Apply a ModelView Projection transformation GLES20.glUniformMatrix4fv(muMVPMatrix, 1, false, mMVPMatrix, 0); GLES20.glUniformMatrix4fv(muMVMatrix, 1, false, mMVMatrix, 0); int i, j, num; for (i = 0; i < 3; i++) for (j = 0; j < 3; j++) mNMatrix[i*3 + j] = mMVMatrix[i*4 + j]; GLES20.glUniformMatrix3fv(muNMatrix, 1, false, mNMatrix, 0); num = min(scene.lights.size(), 8); float[] eyepos = new float[3]; for (i = 0; i < num; i++) { Light3D light = scene.lights.get(i); for (j = 0; j < 3; j++) { eyepos[j] = mVMatrix[4*3 + j]; for (k = 0; k < 3; k++) eyepos[j] += light.pos[k] * mVMatrix[k*4 + j]; } GLES20.glUniform1i(muLightOn[i], 1); GLES20.glUniform3fv(muLightPos[i], 1, eyepos, 0); GLES20.glUniform4fv(muLightCol[i], 1, light.color, 0); } for (i = num; i < 8; i++) GLES20.glUniform1i(muLightOn[i], 0); // Prepare the triangle data GLES20.glEnableVertexAttribArray(maPosition); GLES20.glEnableVertexAttribArray(maNormal); num = scene.objects.size(); for (i = 0; i < num; i++) { Object3D obj = scene.objects.get(i); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, obj.glVertices); GLES20.glVertexAttribPointer(maPosition, 3, GLES20.GL_FLOAT, false, 24, 0); GLES20.glVertexAttribPointer(maNormal, 3, GLES20.GL_FLOAT, false, 24, 12); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, obj.glIndices); int mats = obj.faceMats.size(); for (j = 0; j < mats; j++) { FaceMat mat = obj.faceMats.get(j); for (int k = 0; k < 3; k++) mAmbient[k] = mat.material.ambient[k] * scene.ambient[k]; GLES20.glUniform4fv(muAmbient, 1, mAmbient, 0); GLES20.glUniform4fv(muDiffuse, 1, mat.material.diffuse, 0); GLES20.glDrawElements(GLES20.GL_TRIANGLES, mat.indexBuffer.length, GLES20.GL_UNSIGNED_SHORT, mat.bufOffset * 2); } GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); } GLES20.glDisableVertexAttribArray(maPosition); GLES20.glDisableVertexAttribArray(maNormal); }

В результате получили функцию рендера сцены без текстур, зато с материалами и восемью источниками света. Собственно, конкретно этой сцене, да ещё на таком небольшом экране, текстуры уже и не к чему, и так неплохо выглядит. Но я всё же хочу свечения!
Теперь — самое интересное: отрендерим эту сцену на текстуру.
Квад
Квад нам понадобится, во-первых, для применения гауссового размытия к отрендерённой заранее сцене, а во-вторых, для наложения финального «свечения» на сцену.
Создание квада
Готовим вершины и текстурные координаты, создаём буфер, компилируем шейдер — всё так же, как для модели, разве что шейдеры стали ещё проще:
private final String quadVS = "precision mediump float;\n" + "attribute vec4 vPosition;\n" + "attribute vec4 vTexCoord0;\n" + "varying vec4 TexCoord0;\n" + "void main() {\n" + " gl_Position = vPosition;\n" + " TexCoord0 = vTexCoord0;\n" + "}\n"; private final String quadFS = "precision mediump float;\n" + "uniform sampler2D uTexture0;\n" + "varying vec4 TexCoord0;\n" + "void main() {\n" + " gl_FragColor = texture2D(uTexture0, TexCoord0.xy);\n" + "}\n"; private int mQProgram; private int maQPosition; private int maQTexCoord; private int muQTexture; private int glQuadVB; ... final float quadv[] = { -1, 1, 0, 0, 1, -1, -1, 0, 0, 0, 1, 1, 0, 1, 1, 1, -1, 0, 1, 0 }; glQuadVB = createBuffer(quadv); mQProgram = Compile(quadVS, quadFS); maQPosition = GLES20.glGetAttribLocation(mQProgram, "vPosition"); maQTexCoord = GLES20.glGetAttribLocation(mQProgram, "vTexCoord0"); muQTexture = GLES20.glGetUniformLocation(mQProgram, "uTexture0");
Подготовка текстурных буферов
Создаём сразу два буфера размером 256х256, делать это следует в функции onSurfaceChanged.
private int filterBuf1; private int filterBuf2; private int renderTex1; private int renderTex2; public int scrWidth; public int scrHeight; public int texWidth; public int texHeight; ... private int makeRenderTarget(int width, int height, int[] handles) { GLES20.glGenTextures(1, genbuf, 0); int renderTex = genbuf[0]; GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, renderTex); 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); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); IntBuffer texBuffer = ByteBuffer.allocateDirect(width * height * 4).order(ByteOrder.nativeOrder()).asIntBuffer(); GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, texBuffer); GLES20.glGenRenderbuffers(1, genbuf, 0); int depthBuf = genbuf[0]; GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, depthBuf); GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width, height); GLES20.glGenFramebuffers(1, genbuf, 0); int frameBuf = genbuf[0]; GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuf); GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, renderTex, 0); GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, GLES20.GL_RENDERBUFFER, depthBuf); int res = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER); GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); handles[0] = frameBuf; handles[1] = renderTex; return res; } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { ratio = (float) width / height; int[] handles = new int[2]; scrWidth = width; scrHeight = height; texWidth = 256; texHeight = 256; makeRenderTarget(texWidth, texHeight, handles); filterBuf1 = handles[0]; renderTex1 = handles[1]; makeRenderTarget(texWidth, texHeight, handles); filterBuf2 = handles[0]; renderTex2 = handles[1]; }
С каждым текстурным буфером связано две переменных: собственно текстура и кадровый буфер (прошу прощения за тавтологию).
Отрисовка квада из текстуры / в текстуру
Буквально пара функций: одна задаёт текущие источник и цель отрисовки (0 — это наш экран, остальное — созданные ранее кадровые буферы), вторая рисует квад.
private void setRenderTexture(int frameBuf, int texture) { if (frameBuf == 0) GLES20.glViewport(0, 0, scrWidth, scrHeight); else GLES20.glViewport(0, 0, texWidth, texHeight); GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuf); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture); } private void DrawQuad() { GLES20.glUseProgram(mQProgram); GLES20.glDisable(GLES20.GL_DEPTH_TEST); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, glQuadVB); GLES20.glEnableVertexAttribArray(maQPosition); GLES20.glVertexAttribPointer(maQPosition, 3, GLES20.GL_FLOAT, false, 20, 0); GLES20.glEnableVertexAttribArray(maQTexCoord); GLES20.glVertexAttribPointer(maQTexCoord, 2, GLES20.GL_FLOAT, false, 20, 12); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); GLES20.glUniform1i(muQTexture, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); GLES20.glDisableVertexAttribArray(maQPosition); GLES20.glDisableVertexAttribArray(maQTexCoord); }
Поскольку эффект будет накладываться в несколько проходов, то из соображений производительности текстура у нас невысокого разрешения, так что результат её отрисовки на кваде будет подобен чему-то вроде изображённого на картинке.Уф! Осталось совсем чуть-чуть: ещё пара шейдеров и несколько блоков кода!
Размытие по Гауссу
Одномерное размытие реализуется здесь следующим образом: для каждой точки нашего кадра берётся 44 соседних пикселя, и их цвета складываются с разными весами, в соответствии с гауссовским распределением. Делать это мы будем в 11 проходов, на каждом из которых к результирующему изображению будем прибавлять по 4 пикселя исходной сцены. Соотвественно, шейдеры устроены так, чтобы накладывать по четыре текстуры за один проход, с разными смещениями. Конечно, можно сделать и меньшее число проходов, тут уж надо смотреть по вкусу и по тому, сколько сможет с приемлемой производительностью вытянуть то железо, на котором планируется запускать программу.
Эффект применяется в два этапа: по горизонтали и по вертикали. Нам понадобится посчитать кое-что заранее.
Вспомогательные данные Гаусса
class FilterKernelElement { public float du; public float dv; public float coef; } ... float mOffsets[] = new float[4]; private float[] pix_mult = new float[4]; private FilterKernelElement[] mvGaussian1D = new FilterKernelElement[44]; private float mfPerTexelWidth; private float mfPerTexelHeight; ... float cent = (mvGaussian1D.length - 1.0f) / 2.0f, radi; for (int u = 0; u < mvGaussian1D.length; u++) { FilterKernelElement el = mvGaussian1D[u] = new FilterKernelElement(); el.du = ((float)u) - cent - 0.1f; el.dv = 0.0f; radi = (el.du * el.du) / (cent * cent); el.coef = (float)((0.24/Math.exp(radi*0.18)) + 0.41/Math.exp(radi*4.5)); } float rr = texWidth / (float) texHeight; float rs = rr / ratio; mfPerTexelWidth = rs / texWidth; mfPerTexelHeight = 1.0f / texHeight;
Посчитанные в конце константы нужны для корректировки aspect ratio: экран, в отличие от текстурного буфера, не квадратный, поэтому без такой корректировки свечение будет несколько сплющено.
Шейдеры Гаусса
private final String gaussVS = "precision mediump float;\n" + "attribute vec4 vPosition;\n" + "attribute vec4 vTexCoord0;\n" + "uniform vec4 uTexOffset0;\n" + "uniform vec4 uTexOffset1;\n" + "uniform vec4 uTexOffset2;\n" + "uniform vec4 uTexOffset3;\n" + "varying vec4 TexCoord0;\n" + "varying vec4 TexCoord1;\n" + "varying vec4 TexCoord2;\n" + "varying vec4 TexCoord3;\n" + "void main() {\n" + " gl_Position = vPosition;\n" + " TexCoord0 = vTexCoord0 + uTexOffset0;\n" + " TexCoord1 = vTexCoord0 + uTexOffset1;\n" + " TexCoord2 = vTexCoord0 + uTexOffset2;\n" + " TexCoord3 = vTexCoord0 + uTexOffset3;\n" + "}\n"; private final String gaussFS = "precision mediump float;\n" + "uniform sampler2D uTexture0;\n" + "uniform vec4 uTexCoef0;\n" + "uniform vec4 uTexCoef1;\n" + "uniform vec4 uTexCoef2;\n" + "uniform vec4 uTexCoef3;\n" + "varying vec4 TexCoord0;\n" + "varying vec4 TexCoord1;\n" + "varying vec4 TexCoord2;\n" + "varying vec4 TexCoord3;\n" + "void main() {\n" + " vec4 c0 = texture2D(uTexture0, TexCoord0.xy);\n" + " vec4 c1 = texture2D(uTexture0, TexCoord1.xy);\n" + " vec4 c2 = texture2D(uTexture0, TexCoord2.xy);\n" + " vec4 c3 = texture2D(uTexture0, TexCoord3.xy);\n" + " gl_FragColor = uTexCoef0 * c0 + uTexCoef1 * c1 + uTexCoef2 * c2 + uTexCoef3 * c3;\n" + "}\n"; private int mGProgram; private int maGPosition; private int maGTexCoord; private int muGTexture; private int[] muGTexCoef = new int[4]; private int[] muGTexOffset = new int[4]; ... mGProgram = Compile(gaussVS, gaussFS); maGPosition = GLES20.glGetAttribLocation(mGProgram, "vPosition"); maGTexCoord = GLES20.glGetAttribLocation(mGProgram, "vTexCoord0"); muGTexture = GLES20.glGetUniformLocation(mGProgram, "uTexture0"); for (i = 0; i < 4; i++) { muGTexOffset[i] = GLES20.glGetUniformLocation(mGProgram, String.format("uTexOffset%d", i)); muGTexCoef[i] = GLES20.glGetUniformLocation(mGProgram, String.format("uTexCoef%d", i)); }
Отрисовка Гаусса
Данная функция будет осуществлять размытие по одной оси: либо по горизонтали, либо по вертикали, причём в несколько проходов. Именно поэтому было нужно две текстуры!
private void DrawGauss(boolean invert) { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLES20.glUseProgram(mGProgram); GLES20.glDisable(GLES20.GL_DEPTH_TEST); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, glQuadVB); GLES20.glEnableVertexAttribArray(maGPosition); GLES20.glVertexAttribPointer(maGPosition, 3, GLES20.GL_FLOAT, false, 20, 0); GLES20.glEnableVertexAttribArray(maGTexCoord); GLES20.glVertexAttribPointer(maGTexCoord, 2, GLES20.GL_FLOAT, false, 20, 12); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); GLES20.glUniform1i(muGTexture, 0); int i, n, k; for (i = 0; i < mvGaussian1D.length; i += 4) { for (n = 0; n < 4; n++) { FilterKernelElement pE = mvGaussian1D[i + n]; for (k = 0; k < 4; k++) pix_mult[k] = pE.coef * 0.10f; GLES20.glUniform4fv(muGTexCoef[n], 1, pix_mult, 0); mOffsets[0] = mfPerTexelWidth * (invert ? pE.dv : pE.du); mOffsets[1] = mfPerTexelHeight * (invert ? pE.du : pE.dv); GLES20.glUniform4fv(muGTexOffset[n], 1, mOffsets, 0); } GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); } GLES20.glDisableVertexAttribArray(maGPosition); GLES20.glDisableVertexAttribArray(maGTexCoord); }
Так будет выглядеть результат после горизонтального и вертикального размытия соответственно:

Собираем всё вместе
Последний шаг: процедура отрисовки всего кадра, со всеми эффектами. Надо ещё раз отрендерить модель, после чего наложить на неё сияние.
@Override public void onDrawFrame(GL10 arg0) { setRenderTexture(filterBuf1, 0); DrawScene(); GLES20.glEnable(GLES20.GL_BLEND); GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE); setRenderTexture(filterBuf2, renderTex1); DrawGauss(false); setRenderTexture(filterBuf1, renderTex2); DrawGauss(true); GLES20.glDisable(GLES20.GL_BLEND); setRenderTexture(0, 0); DrawScene(); GLES20.glEnable(GLES20.GL_BLEND); GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE); setRenderTexture(0, renderTex1); DrawQuad(); GLES20.glDisable(GLES20.GL_BLEND); }
Итого:
1. Рисуем сцену в первую текстуру;
2. Для первой текстуры делаем горизонтальное размытие, сохраняем результат во вторую;
3. Вторую текстуру размываем вертикально, сохраняем в первую;
4. Рисуем сцену в обычном режиме;
5. Накладываем первую текстуру поверх сцены с помощью квада;
6. Готово!
На самом деле, сцену можно рисовать и один раз, только на текстуру, сразу скопировав её в самом начале в экранный буфер с помощью квада. Однако, для обеспечения достойного качества картинки это потребует текстуры такого же разрешения, как сам экран, а это уже существенно замедлит её размытие.
Кстати, полученная программа служит неплохим бенчмарком, хотя и упирается в основном в fillrate. Единственное «но»: она не очень хорошо работает на Qualcomm'овских процессорах (какая-то проблема с шейдером исходной сцены), причём я так и не смог выяснить причину, т.к. у меня нет ни одного устройства от HTC, чтобы отладить до конца, зато всё прекрасно отрисовывается на PowerVR 540 (на стареньком Galaxy S), Mali 400 (S2, Tab 7.7, Note) и в эмуляторе.
Update: С момента публикации нашлось несколько ошибок, поэтому статья была немного обновлена. Изменился код шейдера модели (vertexShaderCode, убрано несколько строк), код функции DrawScene (добавлено преобразование координат источников света в eye-space) и финальная отрисовка (onDrawFrame), обновлены скриншоты (на них исчез пересвет). Остальное осталось прежним.
Update 2: Пост о загрузке .3ds готов.
Update 3: Проблема отрисовки на квалкомах решена: оказалось, что вот эта строчка в шейдере
for (i = 0; i < uLights; i++)
работала неправильно. Кто бы мог подумать?..
Шейдеры и всё остальное обновлено, теперь этой проблемы не будет.