Введение
Сей эпичный труд появился благодаря нескольким событиям.
Во-первых, в эмуляторе 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++)
работала неправильно. Кто бы мог подумать?..
Шейдеры и всё остальное обновлено, теперь этой проблемы не будет.