package rajawali.materials;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;
import rajawali.Camera;
import rajawali.lights.ALight;
import rajawali.materials.TextureManager.TextureType;
import rajawali.math.Number3D;
import rajawali.renderer.RajawaliRenderer;
import rajawali.util.RajLog;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.opengl.GLES20;
public abstract class AMaterial {
public static final int NONE = 0;
public static final int VERTEX_ANIMATION = 1 << 0;
public static final int SKELETAL_ANIMATION = 1 << 1;
public static final int ALPHA_MASKING = 1 << 2;
protected String mUntouchedVertexShader;
protected String mUntouchedFragmentShader;
protected String mVertexShader;
protected String mFragmentShader;
protected int mProgram;
protected int mVShaderHandle;
protected int mFShaderHandle;
protected int maPositionHandle;
protected int maTextureHandle;
protected int maColorHandle;
protected int maNormalHandle;
protected int maNextFramePositionHandle;
protected int maNextFrameNormalHandle;
protected int muMVPMatrixHandle;
protected int muCameraPositionHandle;
protected int muMMatrixHandle;
protected int muVMatrixHandle;
protected int muInterpolationHandle;
protected int muAlphaMaskingThresholdHandle;
protected Stack<ALight> mLights;
protected boolean mUseColor = false;
protected boolean mUseAlphaMap = false;
protected boolean mUseNormalMap = false;
protected boolean mUseSpecMap = false;
protected int mNumTextures = 0;
protected float mAlphaMaskingThreshold = .5f;
protected float[] mModelViewMatrix;
protected float[] mViewMatrix;
protected float[] mCameraPosArray;
protected ArrayList<TextureInfo> mTextureInfoList;
/**
* The maximum number of available textures for this device.
*/
private int mMaxTextures;
private boolean mProgramCreated = false;
protected boolean mVertexAnimationEnabled;
protected boolean mSkeletalAnimationEnabled;
protected boolean mAlphaMaskingEnabled;
public AMaterial() {
mTextureInfoList = new ArrayList<TextureInfo>();
mCameraPosArray = new float[3];
mLights = new Stack<ALight>();
mMaxTextures = queryMaxTextures();
}
public AMaterial(String vertexShader, String fragmentShader, boolean vertexAnimationEnabled) {
this(vertexShader, fragmentShader, vertexAnimationEnabled ? VERTEX_ANIMATION : NONE);
}
public AMaterial(String vertexShader, String fragmentShader, int parameters) {
this();
mUntouchedVertexShader = vertexShader;
mUntouchedFragmentShader = fragmentShader;
mVertexAnimationEnabled = (parameters & VERTEX_ANIMATION) != 0;
mSkeletalAnimationEnabled = (parameters & SKELETAL_ANIMATION) != 0;
mAlphaMaskingEnabled = (parameters & ALPHA_MASKING) != 0;
}
public AMaterial(int parameters) {
this();
mVertexAnimationEnabled = (parameters & VERTEX_ANIMATION) != 0;
mSkeletalAnimationEnabled = (parameters & SKELETAL_ANIMATION) != 0;
mAlphaMaskingEnabled = (parameters & ALPHA_MASKING) != 0;
}
public AMaterial(int vertex_res, int fragment_res) {
this(vertex_res, fragment_res, false);
}
public AMaterial(int vertex_res, int fragment_res, boolean vertexAnimationEnabled) {
this(RawMaterialLoader.fetch(vertex_res), RawMaterialLoader.fetch(fragment_res), vertexAnimationEnabled);
}
public AMaterial(int vertex_res, int fragment_res, int parameters) {
this(RawMaterialLoader.fetch(vertex_res), RawMaterialLoader.fetch(fragment_res), parameters);
}
protected int queryMaxTextures() {
int numTexUnits[] = new int[1];
GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_IMAGE_UNITS, numTexUnits, 0);
return numTexUnits[0];
}
public void reload() {
setShaders(mUntouchedVertexShader, mUntouchedFragmentShader);
mNumTextures = mTextureInfoList.size();
for(int i=0; i<mNumTextures; i++) {
if(mTextureInfoList.get(i).getTexture() != null)
addTexture(mTextureInfoList.get(i), true, true);
}
}
public void setShaders() {
setShaders(mUntouchedVertexShader, mUntouchedFragmentShader);
}
public void setShaders(String vertexShader, String fragmentShader) {
mVertexShader = mVertexAnimationEnabled ? "#define VERTEX_ANIM\n" + vertexShader : vertexShader;
mVertexShader = mSkeletalAnimationEnabled ? "#define SKELETAL_ANIM\n" + mVertexShader : mVertexShader;
mVertexShader = mUseColor ? mVertexShader : "#define TEXTURED\n" + mVertexShader;
mFragmentShader = mUseColor ? fragmentShader : "#define TEXTURED\n" + fragmentShader;
mFragmentShader = mAlphaMaskingEnabled ? "#define ALPHA_MASK\n" + mFragmentShader : mFragmentShader;
mFragmentShader = mUseAlphaMap ? "#define ALPHA_MAP\n" + mFragmentShader : mFragmentShader;
mFragmentShader = mUseNormalMap ? "#define NORMAL_MAP\n" + mFragmentShader : mFragmentShader;
mFragmentShader = mUseSpecMap ? "#define SPECULAR_MAP\n" + mFragmentShader : mFragmentShader;
if(RajawaliRenderer.isFogEnabled())
{
mVertexShader = "#define FOG_ENABLED\n" + mVertexShader;
mFragmentShader = "#define FOG_ENABLED\n" + mFragmentShader;
}
mProgram = createProgram(mVertexShader, mFragmentShader);
if (mProgram == 0)
return;
maPositionHandle = getAttribLocation("aPosition");
maNormalHandle = getAttribLocation("aNormal");
maTextureHandle = getAttribLocation("aTextureCoord");
maColorHandle = getAttribLocation("aColor");
muCameraPositionHandle = getUniformLocation("uCameraPosition");
muMVPMatrixHandle = getUniformLocation("uMVPMatrix");
muMMatrixHandle = getUniformLocation("uMMatrix");
muVMatrixHandle = getUniformLocation("uVMatrix");
if(mVertexAnimationEnabled == true) {
maNextFramePositionHandle = getAttribLocation("aNextFramePosition");
maNextFrameNormalHandle = getAttribLocation("aNextFrameNormal");
muInterpolationHandle = getUniformLocation("uInterpolation");
}
if(mAlphaMaskingEnabled == true) {
muAlphaMaskingThresholdHandle = getUniformLocation("uAlphaMaskingThreshold");
}
mProgramCreated = true;
checkTextureHandles();
}
protected int loadShader(int shaderType, String source) {
int shader = GLES20.glCreateShader(shaderType);
if (shader != 0) {
GLES20.glShaderSource(shader, source);
GLES20.glCompileShader(shader);
int[] compiled = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0) {
RajLog.e("[" +getClass().getName()+ "] Could not compile " + (shaderType == GLES20.GL_FRAGMENT_SHADER ? "fragment" : "vertex") + " shader:");
RajLog.e("Shader log: " + GLES20.glGetShaderInfoLog(shader));
GLES20.glDeleteShader(shader);
shader = 0;
}
}
return shader;
}
protected int createProgram(String vertexSource, String fragmentSource) {
mVShaderHandle = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
if (mVShaderHandle == 0) {
return 0;
}
mFShaderHandle = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
if (mFShaderHandle == 0) {
return 0;
}
int program = GLES20.glCreateProgram();
if (program != 0) {
GLES20.glAttachShader(program, mVShaderHandle);
GLES20.glAttachShader(program, mFShaderHandle);
GLES20.glLinkProgram(program);
int[] linkStatus = new int[1];
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] != GLES20.GL_TRUE) {
RajLog.e("Could not link program in " + getClass().getCanonicalName() +": ");
RajLog.e(GLES20.glGetProgramInfoLog(program));
RajLog.d("-=-=-= VERTEX SHADER =-=-=-");
RajLog.d(mVertexShader);
RajLog.d("-=-=-= FRAGMENT SHADER =-=-=-");
RajLog.d(mFragmentShader);
GLES20.glDeleteProgram(program);
program = 0;
}
}
return program;
}
protected int getUniformLocation(String name) {
return GLES20.glGetUniformLocation(mProgram, name);
}
protected int getAttribLocation(String name) {
return GLES20.glGetAttribLocation(mProgram, name);
}
public void unload() {
GLES20.glDeleteShader(mVShaderHandle);
GLES20.glDeleteShader(mFShaderHandle);
GLES20.glDeleteProgram(mProgram);
}
public void destroy() {
mModelViewMatrix = null;
mViewMatrix = null;
mCameraPosArray = null;
if(mLights != null) mLights.clear();
if(mTextureInfoList != null) mTextureInfoList.clear();
unload();
}
public void useProgram() {
if(!mProgramCreated) {
mMaxTextures = queryMaxTextures();
reload();
}
GLES20.glUseProgram(mProgram);
if(checkValidHandle(muAlphaMaskingThresholdHandle, "alpha masking threshold"))
GLES20.glUniform1f(muAlphaMaskingThresholdHandle, mAlphaMaskingThreshold);
}
public void bindTextures() {
int num = mTextureInfoList.size();
for (int i = 0; i < num; i++) {
TextureInfo ti = mTextureInfoList.get(i);
int type = ti.isCubeMap() ? GLES20.GL_TEXTURE_CUBE_MAP : GLES20.GL_TEXTURE_2D;
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
GLES20.glBindTexture(type, ti.getTextureId());
GLES20.glUniform1i(ti.getUniformHandle(), i);
}
}
public void unbindTextures() {
int num = mTextureInfoList.size();
for (int i = 0; i < num; i++) {
TextureInfo ti = mTextureInfoList.get(i);
int type = ti.isCubeMap() ? GLES20.GL_TEXTURE_CUBE_MAP
: GLES20.GL_TEXTURE_2D;
GLES20.glBindTexture(type, 0);
}
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
}
public ArrayList<TextureInfo> getTextureInfoList() {
return mTextureInfoList;
}
public void setTextureInfoList(ArrayList<TextureInfo> textureInfoList) {
mTextureInfoList = textureInfoList;
}
public void addTexture(TextureInfo textureInfo) {
addTexture(textureInfo, false);
}
public void addTexture(TextureInfo textureInfo, boolean isExistingTexture) {
addTexture(textureInfo, isExistingTexture, false);
}
public void removeTexture(TextureInfo textureInfo) {
mTextureInfoList.remove(textureInfo);
}
public void addTexture(TextureInfo textureInfo, boolean isExistingTexture, boolean reload) {
// -- check if this texture is already in the list
if(mTextureInfoList.indexOf(textureInfo) > -1 && !reload) return;
if(mTextureInfoList.size() > mMaxTextures) {
RajLog.e("[" +getClass().getCanonicalName()+ "] Maximum number of textures for this material has been reached. Maximum number of textures is " + mMaxTextures + ".");
}
String textureName = "uTexture";
switch (textureInfo.getTextureType()) {
case DIFFUSE:
case VIDEO_TEXTURE:
textureName = "uDiffuseTexture";
break;
case BUMP:
textureName = "uNormalTexture";
mUseNormalMap = true;
break;
case SPECULAR:
textureName = "uSpecularTexture";
mUseSpecMap = true;
break;
case ALPHA:
textureName = "uAlphaTexture";
mUseAlphaMap = true;
break;
case FRAME_BUFFER:
textureName = "uFrameBufferTexture";
break;
case DEPTH_BUFFER:
textureName = "uDepthBufferTexture";
break;
case LOOKUP:
textureName = "uLookupTexture";
break;
case CUBE_MAP:
textureName = "uCubeMapTexture";
break;
case SPHERE_MAP:
textureName = "uSphereMapTexture";
break;
}
// -- check if there are already diffuse texture in the list
int num = mTextureInfoList.size();
int numDiffuse = 0;
for(int i=0; i<num; ++i) {
TextureInfo ti = mTextureInfoList.get(i);
if(ti.getTextureType() == TextureType.DIFFUSE)
numDiffuse++;
}
// -- if there are already diffuse textures in the list then append a
// number (ie the second texture in the list will be called
// "uDiffuseTexture1", the third "uDiffuseTexture2", etc.
if(numDiffuse > 0 && textureInfo.getTextureType() == TextureType.DIFFUSE)
textureName += numDiffuse;
if(isExistingTexture)
textureName = textureInfo.getTextureName();
if(mProgramCreated) {
int textureHandle = GLES20.glGetUniformLocation(mProgram, textureName);
if (textureHandle == -1) {
RajLog.d("Could not get attrib location for "
+ textureName + ", " + textureInfo.getTextureType());
}
textureInfo.setUniformHandle(textureHandle);
}
if(!isExistingTexture)
textureInfo.setTextureName(textureName);
if(textureInfo.getTextureType() != TextureType.SPHERE_MAP) mUseColor = false;
if(!isExistingTexture) {
mTextureInfoList.add(textureInfo);
mNumTextures++;
}
}
protected void checkTextureHandles() {
int num = mTextureInfoList.size();
for(int i=0; i<num; ++i) {
TextureInfo ti = mTextureInfoList.get(i);
if(ti.getUniformHandle() == -1) {
addTexture(ti, true, true);
}
}
}
public void setVertices(final int vertexBufferHandle) {
if(checkValidHandle(vertexBufferHandle, "vertex data")){
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexBufferHandle);
GLES20.glEnableVertexAttribArray(maPositionHandle);
GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT,
false, 0, 0);
}
}
public void setTextureCoords(int textureCoordBufferHandle) {
if(checkValidHandle(textureCoordBufferHandle, "texture coordinates"))
setTextureCoords(textureCoordBufferHandle, false);
}
public void setTextureCoords(final int textureCoordBufferHandle,
boolean hasCubemapTexture) {
if(checkValidHandle(textureCoordBufferHandle, "texture coordinates"))
{
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, textureCoordBufferHandle);
GLES20.glEnableVertexAttribArray(maTextureHandle);
GLES20.glVertexAttribPointer(maTextureHandle,
hasCubemapTexture ? 3 : 2, GLES20.GL_FLOAT, false, 0, 0);
}
}
public void setColors(final int colorBufferHandle) {
if(checkValidHandle(colorBufferHandle, "color data"))
{
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, colorBufferHandle);
GLES20.glEnableVertexAttribArray(maColorHandle);
GLES20.glVertexAttribPointer(maColorHandle, 4, GLES20.GL_FLOAT,
false, 0, 0);
}
}
public void setNormals(final int normalBufferHandle) {
if(checkValidHandle(normalBufferHandle, "normal data"))
if(checkValidHandle(maNormalHandle, null)){
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, normalBufferHandle);
GLES20.glEnableVertexAttribArray(maNormalHandle);
GLES20.glVertexAttribPointer(maNormalHandle, 3, GLES20.GL_FLOAT,
false, 0, 0);
}
}
public void setMVPMatrix(float[] mvpMatrix) {
if(checkValidHandle(muMVPMatrixHandle, "mvp matrix"))
GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mvpMatrix, 0);
}
public void setModelMatrix(float[] modelMatrix) {
mModelViewMatrix = modelMatrix;
if(checkValidHandle(muMMatrixHandle, null))
GLES20.glUniformMatrix4fv(muMMatrixHandle, 1, false, modelMatrix, 0);
}
public void setViewMatrix(float[] viewMatrix) {
mViewMatrix = viewMatrix;
if(checkValidHandle(muVMatrixHandle, null))
GLES20.glUniformMatrix4fv(muVMatrixHandle, 1, false, viewMatrix, 0);
}
public void setInterpolation(float interpolation) {
if(checkValidHandle(muInterpolationHandle, "interpolation"))
GLES20.glUniform1f(muInterpolationHandle, interpolation);
}
public void setNextFrameVertices(final int vertexBufferHandle) {
if(checkValidHandle(vertexBufferHandle, "NextFrameVertices")){
if(checkValidHandle(maNextFramePositionHandle, "maNextFramePositionHandle")){
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexBufferHandle);
GLES20.glEnableVertexAttribArray(maNextFramePositionHandle);
GLES20.glVertexAttribPointer(maNextFramePositionHandle, 3, GLES20.GL_FLOAT,
false, 0, 0);
}
}
}
public void setNextFrameNormals(final int normalBufferHandle) {
if(checkValidHandle(normalBufferHandle, "NextFrameNormals")){
if(checkValidHandle(maNextFrameNormalHandle, "maNextFrameNormalHandle")){
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, normalBufferHandle);
GLES20.glEnableVertexAttribArray(maNextFrameNormalHandle);
GLES20.glVertexAttribPointer(maNextFrameNormalHandle, 3, GLES20.GL_FLOAT,
false, 0, 0);
}
}
}
/**
* Set the threshold for alpha masking. The default value is .5f
*
*
* @param threshold Pixels with alpha values below this number will be discarded (range 0 - 1)
*/
public void setAlphaMaskingThreshold(float threshold) {
mAlphaMaskingThreshold = threshold;
}
public boolean checkValidHandle(int handle, String message){
if(handle >= 0)
return true;
if(message != null)
RajLog.e("[" +getClass().getCanonicalName()+ "] Trying to set "+message+
" without a valid handle.");
return false;
}
public void setLightParams() {
}
public void setLights(List<ALight> lights) {
if(lights == null || lights.size() == 0)
return;
for(int i=0; i<lights.size(); ++i) {
if(i>=mLights.size())
mLights.add(lights.get(i));
else
mLights.set(i, lights.get(i));
}
}
public void setCamera(Camera camera) {
Number3D camPos = camera.getPosition();
mCameraPosArray[0] = camPos.x;
mCameraPosArray[1] = camPos.y;
mCameraPosArray[2] = camPos.z;
if (muCameraPositionHandle > -1)
GLES20.glUniform3fv(muCameraPositionHandle, 1, mCameraPosArray, 0);
}
public String toString() {
StringBuffer out = new StringBuffer();
out.append("[" +getClass().getName()+ "]\n");
out.append("program: ").append(mProgram).append("\n");
out.append("vshader handle: ").append(mVShaderHandle).append("\n");
out.append("fshader handle: ").append(mFShaderHandle).append("\n");
out.append("program created: ").append(mProgramCreated).append("\n");
return out.toString();
}
/**
* Get the model-space to view-space matrix
*
* @return {@link float[]}
*/
public float[] getModelViewMatrix() {
return mModelViewMatrix;
}
public void copyTexturesTo(AMaterial shader) {
int num = mTextureInfoList.size();
for (int i = 0; i < num; ++i)
shader.addTexture(mTextureInfoList.get(i));
}
/**
* Set the material to use a color value rather than a texture
*
* @param value {@link boolean}
*/
public void setUseColor(boolean value) {
if(value != mUseColor) {
mUseColor = value;
if(mLights.size() > 0 || !(this instanceof AAdvancedMaterial))
setShaders(mUntouchedVertexShader, mUntouchedFragmentShader);
}
mUseColor = value;
}
/**
* Determine if color is used.
*
* @return {@link boolean}
*/
public boolean getUseColor() {
return mUseColor;
}
/**
* Pass the context to be used for resource loading. This should only be called internally by the renderer.
*
* This is kind of a hack but I do not see any better way to load resources for materials.
*
* @param context
*/
public static final void setLoaderContext(final Context context) {
RawMaterialLoader.mContext = new WeakReference<Context>(context);
}
/**
* Internal class for managing shader loading.
*
* This class is mostly internal unfortunately loading of resources requires context so this class has help from
* AMaterial to statically pass the context and store it as a weak reference. Unfortunately there is no way around
* this that I can see but I am open for suggestions of a better solution.
*
* @author Ian Thomas (toxicbakery aka damnusernames http://toxicbakery.com/)
*
*/
protected static final class RawMaterialLoader {
@SuppressLint("UseSparseArrays")
private static final HashMap<Integer, String> mRawMaterials = new HashMap<Integer, String>();
// Prevent memory leaks as referencing the context can be dangerous.
public static WeakReference<Context> mContext;
/**
* Read a material from the raw resources folder. Subsequent calls will return from memory.
*
* @param resID
* @return
*/
public static final String fetch(final int resID) {
if (mRawMaterials.containsKey(resID))
return mRawMaterials.get(resID);
final StringBuilder sb = new StringBuilder();
try {
final Resources res = mContext.get().getResources();
final InputStreamReader isr = new InputStreamReader(res.openRawResource(resID));
final BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null)
sb.append(line).append("\n");
mRawMaterials.put(resID, sb.toString());
isr.close();
br.close();
} catch (Exception e) {
RajLog.e("Failed to read material: " + e.getMessage());
e.printStackTrace();
}
return mRawMaterials.get(resID);
}
}
}