package net.trippedout.android.shadercamera.gl; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.SurfaceTexture; import android.opengl.GLES11Ext; import android.opengl.GLES20; import android.opengl.GLUtils; import android.util.Log; import net.trippedout.android.shadercamera.utils.AndroidUtils; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.nio.ShortBuffer; import java.util.ArrayList; /** * Base camera rendering class. Extend this and override the fragment and vertex shaders, as well * as the methods in {@link #draw()} which set up uniforms, draw the elements, and clean up, if need * be. Check out {@link SimpleCameraRenderer} for simple usage of this. * * Original code by https://www.virag.si/ * Updated/refactored by Anthony Tripaldi */ public class CameraRenderer extends TextureSurfaceRenderer implements SurfaceTexture.OnFrameAvailableListener { private static final String TAG = CameraRenderer.class.getSimpleName(); /** * if you create new files, just override these defaults in your subclass and * don't edit the {@link #vertexShaderCode} and {@link #fragmentShaderCode} variables */ protected String DEFAULT_FRAGMENT_SHADER = "camera.frag.glsl"; protected String DEFAULT_VERTEX_SHADER = "camera.vert.glsl"; private Context ctx; /** * if you override these in ctor of subclass, loader will ignore the files listed above */ protected String vertexShaderCode; protected String fragmentShaderCode; private static float squareSize = 1.0f; private static float squareCoords[] = { -squareSize, squareSize, // 0.0f, // top left squareSize, squareSize, // 0.0f, // top right -squareSize, -squareSize, // 0.0f, // bottom left squareSize, -squareSize, // 0.0f, // bottom right }; private static short drawOrder[] = {0, 1, 2, 1, 3, 2}; // Texture to be shown in backgrund private FloatBuffer textureBuffer; private float textureCoords[] = { 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, }; //texture ints for glGenTextures public static final int MAX_TEXTURES = 8; private int[] textures = new int[MAX_TEXTURES]; protected int shaderProgram; private FloatBuffer vertexBuffer; private ShortBuffer drawListBuffer; private SurfaceTexture videoTexture; private float[] videoTextureTransform = new float[16]; protected boolean frameAvailable = false; private boolean adjustViewport = false; private int textureCoordinateHandle; private int positionHandle; //aspect ratio private float mAspectRatio = 1.0f; //textures private ArrayList<Texture> mTextureArray; public CameraRenderer(Context context, SurfaceTexture texture, int width, int height) { super(texture, width, height); this.ctx = context; init(DEFAULT_FRAGMENT_SHADER, DEFAULT_VERTEX_SHADER); } /** * Secondary constructor for passing in shaders to override the default shader. * Context, texture, width, and height are passed in automatically by {@link net.trippedout.android.shadercamera.fragments.CameraFragment.CameraTextureListener} * @param context * @param texture * @param width * @param height * @param fragPath the file name of your fragment shader, ex: "lip_service.frag" if it is top-level /assets/ folder. Add subdirectories if needed * @param vertPath the file name of your vertex shader, ex: "lip_service.vert" if it is top-level /assets/ folder. Add subdirectories if needed */ public CameraRenderer(Context context, SurfaceTexture texture, int width, int height, String fragPath, String vertPath) { super(texture, width, height); this.ctx = context; init(fragPath, vertPath); } private void init(String fragPath, String vertPath) { //load shaders if(fragmentShaderCode == null || vertexShaderCode == null) loadFromShadersFromAssets(fragPath, vertPath); //setup arrays mTextureArray = new ArrayList<>(); } private void loadFromShadersFromAssets(String pathToFragment, String pathToVertex) { try { fragmentShaderCode = AndroidUtils.getStringFromFileInAssets(ctx, pathToFragment); vertexShaderCode = AndroidUtils.getStringFromFileInAssets(ctx, pathToVertex); } catch (IOException e) { e.printStackTrace(); } } // ------------------------------------------------------------ // overrides // ------------------------------------------------------------ @Override public void onFrameAvailable(SurfaceTexture surfaceTexture) { synchronized (this) { frameAvailable = true; } } @Override protected void deinitGLComponents() { GLES20.glDeleteTextures(1, textures, 0); GLES20.glDeleteProgram(shaderProgram); videoTexture.release(); videoTexture.setOnFrameAvailableListener(null); } @Override protected void initGLComponents() { setupBlending(); setupVertexBuffer(); setupTexture(); setupShaders(); onSetupComplete(); } // ------------------------------------------------------------ // setup // ------------------------------------------------------------ private void setupBlending() { // GLES20.glEnable(GLES20.GL_BLEND); // GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); } private void setupVertexBuffer() { // Draw list buffer ByteBuffer dlb = ByteBuffer.allocateDirect(drawOrder.length * 2); dlb.order(ByteOrder.nativeOrder()); drawListBuffer = dlb.asShortBuffer(); drawListBuffer.put(drawOrder); drawListBuffer.position(0); // Initialize the texture holder ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4); bb.order(ByteOrder.nativeOrder()); vertexBuffer = bb.asFloatBuffer(); vertexBuffer.put(squareCoords); vertexBuffer.position(0); } private void setupTexture() { ByteBuffer texturebb = ByteBuffer.allocateDirect(textureCoords.length * 4); texturebb.order(ByteOrder.nativeOrder()); textureBuffer = texturebb.asFloatBuffer(); textureBuffer.put(textureCoords); textureBuffer.position(0); // Generate the max amount texture ids GLES20.glGenTextures(MAX_TEXTURES, textures, 0); checkGlError("Texture generate"); //set texture[0] to camera texture GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0]); checkGlError("Texture bind"); videoTexture = new SurfaceTexture(textures[0]); videoTexture.setOnFrameAvailableListener(this); } private void setupShaders() { int vertexShaderHandle = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER); GLES20.glShaderSource(vertexShaderHandle, vertexShaderCode); GLES20.glCompileShader(vertexShaderHandle); checkGlError("Vertex shader compile"); Log.d(TAG, "vertexShader info log:\n " + GLES20.glGetShaderInfoLog(vertexShaderHandle)); int fragmentShaderHandle = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER); GLES20.glShaderSource(fragmentShaderHandle, fragmentShaderCode); GLES20.glCompileShader(fragmentShaderHandle); checkGlError("Pixel shader compile"); Log.d(TAG, "fragmentShader info log:\n " + GLES20.glGetShaderInfoLog(fragmentShaderHandle)); shaderProgram = GLES20.glCreateProgram(); GLES20.glAttachShader(shaderProgram, vertexShaderHandle); GLES20.glAttachShader(shaderProgram, fragmentShaderHandle); GLES20.glLinkProgram(shaderProgram); checkGlError("Shader program compile"); int[] status = new int[1]; GLES20.glGetProgramiv(shaderProgram, GLES20.GL_LINK_STATUS, status, 0); if (status[0] != GLES20.GL_TRUE) { String error = GLES20.glGetProgramInfoLog(shaderProgram); Log.e("SurfaceTest", "Error while linking program:\n" + error); } } /** * called when all setup is complete on basic GL stuffs * override for adding textures and other shaders */ protected void onSetupComplete() { } // ------------------------------------------------------------ // drawing // ------------------------------------------------------------ @Override protected boolean draw() { synchronized (this) { if (frameAvailable) { videoTexture.updateTexImage(); videoTexture.getTransformMatrix(videoTextureTransform); frameAvailable = false; } else { return false; } } // if (adjustViewport) // adjustViewport(); GLES20.glClearColor(1.0f, 0.0f, 0.0f, 0.0f); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); //set shader GLES20.glUseProgram(shaderProgram); setUniformsAndAttribs(); setExtraTextures(); drawElements(); onDrawCleanup(); return true; } /** * base amount of attributes needed for rendering camera to screen */ protected void setUniformsAndAttribs() { int textureParamHandle = GLES20.glGetUniformLocation(shaderProgram, "camTexture"); int textureTranformHandle = GLES20.glGetUniformLocation(shaderProgram, "camTextureTransform"); int aspectRatioHandle = GLES20.glGetUniformLocation(shaderProgram, "aspectRatio"); textureCoordinateHandle = GLES20.glGetAttribLocation(shaderProgram, "camTexCoordinate"); positionHandle = GLES20.glGetAttribLocation(shaderProgram, "position"); GLES20.glUniform1f(aspectRatioHandle, mAspectRatio); GLES20.glEnableVertexAttribArray(positionHandle); GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 4 * 2, vertexBuffer); //camera texture GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0]); GLES20.glUniform1i(textureParamHandle, 0); GLES20.glEnableVertexAttribArray(textureCoordinateHandle); GLES20.glVertexAttribPointer(textureCoordinateHandle, 2, GLES20.GL_FLOAT, false, 4 * 2, textureBuffer); GLES20.glUniformMatrix4fv(textureTranformHandle, 1, false, videoTextureTransform, 0); } public void addTexture(int resource_id, String uniformName) { int texNum = mTextureArray.size() + 1; if(texNum >= MAX_TEXTURES) throw new IllegalStateException("Too many textures! Please don't use so many :("); Bitmap bmp = BitmapFactory.decodeResource(ctx.getResources(), resource_id); GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + texNum); checkGlError("Texture generate"); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[texNum]); checkGlError("Texture bind"); GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bmp, 0); bmp.recycle(); Texture tex = new Texture(texNum, uniformName); mTextureArray.add(tex); Log.d(TAG, "addedTexture() " + tex); } /** * override this and copy if u want to add your own textures * if u need different uv coordinates, refer to {@link #setupTexture()} * for how to create your own buffer */ protected void setExtraTextures() { for(int i = 0; i < mTextureArray.size(); i++) { Texture tex = mTextureArray.get(i); int imageParamHandle = GLES20.glGetUniformLocation(shaderProgram, tex.uniformName); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[tex.texId]); GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + tex.texId); GLES20.glUniform1i(imageParamHandle, tex.texId); } // int imageParamHandle = GLES20.glGetUniformLocation(shaderProgram, "image"); // int texture2CoordinateHandle = GLES20.glGetAttribLocation(shaderProgram, "vTex2Coordinate"); // // //extra image // GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[1]); // GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + 1); // GLES20.glUniform1i(imageParamHandle, 1); // // //reuse same coordinates from before but point them elsewhere // GLES20.glEnableVertexAttribArray(texture2CoordinateHandle); // GLES20.glVertexAttribPointer(texture2CoordinateHandle, 4, GLES20.GL_FLOAT, false, 4 * 2, textureBuffer); } private void drawElements() { GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer); } private void onDrawCleanup() { GLES20.glDisableVertexAttribArray(positionHandle); GLES20.glDisableVertexAttribArray(textureCoordinateHandle); } // private void adjustViewport() { // float surfaceAspect = height / (float) width; // float videoAspect = videoHeight / (float) videoWidth; // // Log.d(TAG, "adjustViewport() " + surfaceAspect + ", " + videoAspect); // // if (surfaceAspect > videoAspect) { // float heightRatio = height / (float) videoHeight; // int newWidth = (int) (width * heightRatio); // int xOffset = (newWidth - width) / 2; // GLES20.glViewport(-xOffset, 0, newWidth, height); // } else { // float widthRatio = width / (float) videoWidth; // int newHeight = (int) (height * widthRatio); // int yOffset = (newHeight - height) / 2; // GLES20.glViewport(0, -yOffset, width, newHeight); // } // // adjustViewport = false; // } /** * necessary to make sure that our updating of animation happens in-line with the new frames * coming in. without this, we were seeing two or three 'animationUpdates' being called * in between actual draw calls. this guarantees a draw every time you update the frame */ protected void updateFrame() { synchronized (this) { frameAvailable = true; } } public void checkGlError(String op) { int error; while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { Log.e("SurfaceTest", op + ": glError " + GLUtils.getEGLErrorString(error)); } } public SurfaceTexture getVideoTexture() { return videoTexture; } public void setAspectRatio(float aspectRatio) { Log.d(TAG, "setAspectRatio() " + mAspectRatio); mAspectRatio = aspectRatio; } /** * Internal class for storing refs to textures for rendering */ private class Texture { public int texId; public String uniformName; private Texture(int texId, String uniformName) { this.texId = texId; this.uniformName = uniformName; } @Override public String toString() { return "[Texture] textureId: " + texId + ", uniformName: " + uniformName; } } }