package no.naiv.tiltshift.effect import android.content.Context import android.graphics.SurfaceTexture import android.opengl.GLES11Ext import android.opengl.GLES20 import android.opengl.GLSurfaceView import java.nio.ByteBuffer import java.nio.ByteOrder import java.nio.FloatBuffer import javax.microedition.khronos.egl.EGLConfig import javax.microedition.khronos.opengles.GL10 /** * OpenGL renderer for applying tilt-shift effect to camera preview. * * This renderer receives camera frames via SurfaceTexture and applies * the tilt-shift blur effect using GLSL shaders. */ class TiltShiftRenderer( private val context: Context, private val onSurfaceTextureAvailable: (SurfaceTexture) -> Unit ) : GLSurfaceView.Renderer { private lateinit var shader: TiltShiftShader private var surfaceTexture: SurfaceTexture? = null private var cameraTextureId: Int = 0 private lateinit var vertexBuffer: FloatBuffer private lateinit var texCoordBuffer: FloatBuffer private var surfaceWidth: Int = 0 private var surfaceHeight: Int = 0 // Current effect parameters (updated from UI thread) @Volatile var blurParameters: BlurParameters = BlurParameters.DEFAULT @Volatile private var isFrontCamera: Boolean = false // Quad vertices (full screen) private val vertices = floatArrayOf( -1f, -1f, // Bottom left 1f, -1f, // Bottom right -1f, 1f, // Top left 1f, 1f // Top right ) // Texture coordinates rotated 90° for portrait mode (back camera) // (Camera sensors are landscape-oriented, we rotate to portrait) private val texCoordsBack = floatArrayOf( 1f, 1f, // Bottom left of screen -> bottom right of texture 1f, 0f, // Bottom right of screen -> top right of texture 0f, 1f, // Top left of screen -> bottom left of texture 0f, 0f // Top right of screen -> top left of texture ) // Texture coordinates for front camera (mirrored + rotated) // Front camera needs horizontal mirror for natural selfie view private val texCoordsFront = floatArrayOf( 0f, 1f, // Bottom left of screen 0f, 0f, // Bottom right of screen 1f, 1f, // Top left of screen 1f, 0f // Top right of screen ) private var currentTexCoords = texCoordsBack override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) { GLES20.glClearColor(0f, 0f, 0f, 1f) // Initialize shader shader = TiltShiftShader(context) shader.initialize() // Create vertex buffer vertexBuffer = ByteBuffer.allocateDirect(vertices.size * 4) .order(ByteOrder.nativeOrder()) .asFloatBuffer() .put(vertices) vertexBuffer.position(0) // Create texture coordinate buffer texCoordBuffer = ByteBuffer.allocateDirect(currentTexCoords.size * 4) .order(ByteOrder.nativeOrder()) .asFloatBuffer() .put(currentTexCoords) texCoordBuffer.position(0) // Create camera texture val textures = IntArray(1) GLES20.glGenTextures(1, textures, 0) cameraTextureId = textures[0] GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTextureId) GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR) GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR) GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE) GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE) // Create SurfaceTexture for camera frames surfaceTexture = SurfaceTexture(cameraTextureId).also { onSurfaceTextureAvailable(it) } } override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) { GLES20.glViewport(0, 0, width, height) surfaceWidth = width surfaceHeight = height } override fun onDrawFrame(gl: GL10?) { // Update texture with latest camera frame surfaceTexture?.updateTexImage() // Update texture coordinate buffer if camera changed if (updateTexCoordBuffer) { texCoordBuffer.clear() texCoordBuffer.put(currentTexCoords) texCoordBuffer.position(0) updateTexCoordBuffer = false } GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) // Use shader and set parameters shader.use(cameraTextureId, blurParameters, surfaceWidth, surfaceHeight, isFrontCamera) // Set vertex positions GLES20.glEnableVertexAttribArray(shader.aPositionLocation) GLES20.glVertexAttribPointer( shader.aPositionLocation, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer ) // Set texture coordinates GLES20.glEnableVertexAttribArray(shader.aTexCoordLocation) GLES20.glVertexAttribPointer( shader.aTexCoordLocation, 2, GLES20.GL_FLOAT, false, 0, texCoordBuffer ) // Draw quad GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4) // Cleanup GLES20.glDisableVertexAttribArray(shader.aPositionLocation) GLES20.glDisableVertexAttribArray(shader.aTexCoordLocation) } /** * Updates blur parameters. Thread-safe. */ fun updateParameters(params: BlurParameters) { blurParameters = params } /** * Sets whether using front camera. Updates texture coordinates accordingly. * Thread-safe - actual buffer update happens on next frame. */ fun setFrontCamera(front: Boolean) { if (isFrontCamera != front) { isFrontCamera = front currentTexCoords = if (front) texCoordsFront else texCoordsBack // Buffer will be updated on next draw updateTexCoordBuffer = true } } @Volatile private var updateTexCoordBuffer = false /** * Releases OpenGL resources. * Must be called from GL thread. */ fun release() { shader.release() surfaceTexture?.release() surfaceTexture = null if (cameraTextureId != 0) { GLES20.glDeleteTextures(1, intArrayOf(cameraTextureId), 0) cameraTextureId = 0 } } }