Pull to refresh

Эффект Glow гауссовым размытием на Android

Reading time 16 min
Views 16K

Введение


Результат Сей эпичный труд появился благодаря нескольким событиям.

Во-первых, в эмуляторе 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++)

работала неправильно. Кто бы мог подумать?..
Шейдеры и всё остальное обновлено, теперь этой проблемы не будет.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+48
Comments 16
Comments Comments 16

Articles