Follow by Email

Friday, March 27, 2015

Using shaders in AndEngine


Introduction

    I'd spent quite a lot time trying to  figure out how to use custom shaders in AndEngine. There are a couple of tutorials on the Internet but all of them are incomplete or have some strange solutions like engine onDraw overloading or something like this, so i decided to write this tutorial to collect useful info in one place.

First steps

Luckily  AndEngine has logical and straight architecture and we don't need to walk through the forest of code. It already has ShaderProgram class that can be used as a basis for our shader. In this example i will show how to write simple blink shader using which you can mark sprites in your game for example. So let me show you the whole BlinkShader class and then explain how it works:
package com.syouth.heli.main.shaders;

import android.opengl.GLES20;

import org.andengine.opengl.shader.ShaderProgram;
import org.andengine.opengl.shader.constants.ShaderProgramConstants;
import org.andengine.opengl.shader.exception.ShaderProgramException;
import org.andengine.opengl.shader.exception.ShaderProgramLinkException;
import org.andengine.opengl.util.GLState;
import org.andengine.opengl.vbo.attribute.VertexBufferObjectAttributes;

/**
 * Created by syouth on 25.03.15.
 */
public class BlinkShader extends ShaderProgram {
    public static final String UNIFORM_INTENCITY = "intencity";
    public static final double START_ANGLE = 60.0;

    private static BlinkShader INSTANCE = null;
    private static final float BLINKING_SPEED = 0.09f;
    private static final float MAX_ANGLE = 90;
    private static final float MIN_ANGLE = 0;
    private static final float MIN_INTENCITY = 0.5f;
    private static final float MAX_INTENCITY = 1.5f;
    private static final float mDiv = (float) ((MAX_INTENCITY - MIN_INTENCITY) /
            (Math.cos(Math.toRadians(MIN_ANGLE)) - Math.cos(Math.toRadians(MAX_ANGLE))));
    private static int mModelViewProjectionMatrixLocation = ShaderProgramConstants.LOCATION_INVALID;
    private static int mTexture0Location = ShaderProgramConstants.LOCATION_INVALID;
    private static int mIntencityLocation = ShaderProgramConstants.LOCATION_INVALID;

    private static boolean mBlink = false;
    private static double mCurAngle = 0.0f;
    private static long mPrevTime = 0;
    private static boolean mGrow = true;

    private static final String VERTEXT_SHADER =
            "uniform mat4 " + ShaderProgramConstants.UNIFORM_MODELVIEWPROJECTIONMATRIX + ";\n" +
                    "attribute vec4 " + ShaderProgramConstants.ATTRIBUTE_POSITION + ";\n" +
                    "attribute vec2 " + ShaderProgramConstants.ATTRIBUTE_TEXTURECOORDINATES + ";\n" +
                    "attribute vec4 " + ShaderProgramConstants.ATTRIBUTE_COLOR + ";\n" +
                    "varying vec2 " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ";\n" +
                    "varying vec4 " + ShaderProgramConstants.VARYING_COLOR + ";\n" +
                    "void main() {\n" +
                    "   " + ShaderProgramConstants.VARYING_COLOR +
                    " = " + ShaderProgramConstants.ATTRIBUTE_COLOR + ";\n" +
                    "   " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + " = " +
                    ShaderProgramConstants.ATTRIBUTE_TEXTURECOORDINATES + ";\n" +
                    "   gl_Position = " + ShaderProgramConstants.UNIFORM_MODELVIEWPROJECTIONMATRIX +
                    "*" + ShaderProgramConstants.ATTRIBUTE_POSITION + ";\n" +
                    "}";

    private static final String FRAGMENT_SHADER =
            "precision lowp float;\n" +
                    "uniform float " + UNIFORM_INTENCITY + ";\n" +
                    "uniform sampler2D " + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ";\n" +
                    "varying mediump vec2 " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ";\n" +
                    "varying lowp vec4 " + ShaderProgramConstants.VARYING_COLOR + ";\n" +
                    "void main() {\n" +
                    "   vec4 inten = vec4(" + UNIFORM_INTENCITY + ", " +
                    UNIFORM_INTENCITY + ", " +
                    UNIFORM_INTENCITY + ", 1.0);\n" +
                    "   gl_FragColor = inten * " + ShaderProgramConstants.VARYING_COLOR + " * " +
                    "texture2D(" + ShaderProgramConstants.UNIFORM_TEXTURE_0 + ", " + ShaderProgramConstants.VARYING_TEXTURECOORDINATES + ");\n" +
                    "}";
    private BlinkShader() {
        super(VERTEXT_SHADER, FRAGMENT_SHADER);
    }

    public static BlinkShader getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new BlinkShader();
        }

        return INSTANCE;
    }

    @Override
    protected void link(GLState pGLState) throws ShaderProgramLinkException {
        GLES20.glBindAttribLocation(mProgramID,
                ShaderProgramConstants.ATTRIBUTE_POSITION_LOCATION,
                ShaderProgramConstants.ATTRIBUTE_POSITION);
        GLES20.glBindAttribLocation(mProgramID,
                ShaderProgramConstants.ATTRIBUTE_TEXTURECOORDINATES_LOCATION,
                ShaderProgramConstants.ATTRIBUTE_TEXTURECOORDINATES);
        GLES20.glBindAttribLocation(mProgramID,
                ShaderProgramConstants.ATTRIBUTE_COLOR_LOCATION,
                ShaderProgramConstants.ATTRIBUTE_COLOR);
        super.link(pGLState);
        mModelViewProjectionMatrixLocation =
                getUniformLocation(ShaderProgramConstants.UNIFORM_MODELVIEWPROJECTIONMATRIX);
        mTexture0Location =
                getUniformLocation(ShaderProgramConstants.UNIFORM_TEXTURE_0);
        mIntencityLocation =
                getUniformLocation(UNIFORM_INTENCITY);
    }

    @Override
    public void bind(GLState pGLState, VertexBufferObjectAttributes pVertexBufferObjectAttributes) throws ShaderProgramException {
        super.bind(pGLState, pVertexBufferObjectAttributes);
        GLES20.glUniformMatrix4fv(
                mModelViewProjectionMatrixLocation,
                1,
                false,
                pGLState.getModelViewProjectionGLMatrix(),
                0
        );
        GLES20.glUniform1i(mTexture0Location, 0);
        if (mBlink) {
            long curTime = System.currentTimeMillis();
            long timeDelta = curTime - mPrevTime;
            mPrevTime = curTime;
            float angleDelta = timeDelta * BLINKING_SPEED;
            if (mGrow) {
                mCurAngle += angleDelta;
            } else {
                mCurAngle -= angleDelta;
            }
            if (mCurAngle > MAX_ANGLE) {
                mCurAngle -= 2 * angleDelta;
                mGrow = false;
            } else if (mCurAngle < 0) {
                mCurAngle += 2 * angleDelta;
                mGrow = true;
            }
        }
        GLES20.glUniform1f(mIntencityLocation, map(mCurAngle));
    }

    private static float map(double angle) {
        return (float) (MIN_INTENCITY + mDiv * Math.cos(Math.toRadians(angle)));
    }

    public static void setmBlink(boolean mBlink) {
        mPrevTime = System.currentTimeMillis();
        mGrow = false;
        mCurAngle = START_ANGLE;
        BlinkShader.mBlink = mBlink;
    }
}


It's a bit messy so sorry for this.
The shader itself is very easy. It just changes an intensity of the sprite according to passed variable.
ShaderProgramConstants.ATTRIBUTE_POSITION, ATTRIBUTE_TEXTURECOORDINATES, ATTRIBUTE_COLOR
are used for simplicity. They represent position, texture coordinates and color for triangle respectively. The main parts are link and bind methonds.
Link is called before the shader is linked and you have to bind your attributes here before the shader is linked.
GLES20.glBindAttribLocation()
is used for this.
ShaderProgramConstants.ATTRIBUTE_POSITION_LOCATION
is a constant that represents the location AndEngine uses for position and so on. After the linkage is completed in super.link we should get location of our unifor variables.
getUniformLocation()
is used for this. The nex inportant method is
public void bind(GLState pGLState, VertexBufferObjectAttributes pVertexBufferObjectAttributes) throws ShaderProgramExceptionpublic

It's called every time sprite is being drawn, so here we pass variables to shader.
GLES20.glUniform1i(mTexture0Location, 0)
This line means that 0's texture unit will be used for sampler.

How to attach shader to sprite

Attaching shader to sprite is really simple. You just have to write 
o.setShaderProgram(BlinkShader.getInstance());
BlinkShader.getInstance().setmBlink(true);
This will attach shader to sprite and start blinking. Also you have to add your shader to shader manager to correctly manage onResume and onPause events

mActivity.getShaderProgramManager().loadShaderProgram(BlinkShader.getInstance());

The end

Thats it. As you can see its very easy to use shaders in AndEngine if you know how to do this. Later i will show how to use multitexturing and maybe so other methods.