package no.naiv.tiltshift.effect import android.content.Context import android.opengl.GLES11Ext import android.opengl.GLES20 import no.naiv.tiltshift.R import java.io.BufferedReader import java.io.InputStreamReader import kotlin.math.cos import kotlin.math.sin /** * Manages OpenGL shader programs for the two-pass tilt-shift effect. * * Two programs: * - **Passthrough**: copies camera texture (external OES) to an FBO, handling the * coordinate transform via vertex/texcoord setup. * - **Blur**: applies a directional Gaussian blur with tilt-shift mask. * Used twice per frame (horizontal then vertical) via the [uBlurDirection] uniform. */ class TiltShiftShader(private val context: Context) { // --- Passthrough program (camera → FBO) --- private var passthroughProgramId: Int = 0 var passthroughPositionLoc: Int = 0 private set var passthroughTexCoordLoc: Int = 0 private set private var passthroughTextureLoc: Int = 0 // --- Blur program (FBO → FBO/screen) --- private var blurProgramId: Int = 0 var blurPositionLoc: Int = 0 private set var blurTexCoordLoc: Int = 0 private set private var blurTextureLoc: Int = 0 private var blurModeLoc: Int = 0 private var blurPositionXLoc: Int = 0 private var blurPositionYLoc: Int = 0 private var blurSizeLoc: Int = 0 private var blurAmountLoc: Int = 0 private var blurFalloffLoc: Int = 0 private var blurAspectRatioLoc: Int = 0 private var blurResolutionLoc: Int = 0 private var blurCosAngleLoc: Int = 0 private var blurSinAngleLoc: Int = 0 private var blurDirectionLoc: Int = 0 /** * Compiles and links both shader programs. * Must be called from GL thread. */ fun initialize() { val vertexSource = loadShaderSource(R.raw.tiltshift_vertex) val vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, vertexSource) // Passthrough program val passthroughFragSource = loadShaderSource(R.raw.tiltshift_passthrough_fragment) val passthroughFragShader = compileShader(GLES20.GL_FRAGMENT_SHADER, passthroughFragSource) passthroughProgramId = linkProgram(vertexShader, passthroughFragShader) GLES20.glDeleteShader(passthroughFragShader) passthroughPositionLoc = GLES20.glGetAttribLocation(passthroughProgramId, "aPosition") passthroughTexCoordLoc = GLES20.glGetAttribLocation(passthroughProgramId, "aTexCoord") passthroughTextureLoc = GLES20.glGetUniformLocation(passthroughProgramId, "uTexture") // Blur program val blurFragSource = loadShaderSource(R.raw.tiltshift_fragment) val blurFragShader = compileShader(GLES20.GL_FRAGMENT_SHADER, blurFragSource) blurProgramId = linkProgram(vertexShader, blurFragShader) GLES20.glDeleteShader(blurFragShader) blurPositionLoc = GLES20.glGetAttribLocation(blurProgramId, "aPosition") blurTexCoordLoc = GLES20.glGetAttribLocation(blurProgramId, "aTexCoord") blurTextureLoc = GLES20.glGetUniformLocation(blurProgramId, "uTexture") blurModeLoc = GLES20.glGetUniformLocation(blurProgramId, "uMode") blurPositionXLoc = GLES20.glGetUniformLocation(blurProgramId, "uPositionX") blurPositionYLoc = GLES20.glGetUniformLocation(blurProgramId, "uPositionY") blurSizeLoc = GLES20.glGetUniformLocation(blurProgramId, "uSize") blurAmountLoc = GLES20.glGetUniformLocation(blurProgramId, "uBlurAmount") blurFalloffLoc = GLES20.glGetUniformLocation(blurProgramId, "uFalloff") blurAspectRatioLoc = GLES20.glGetUniformLocation(blurProgramId, "uAspectRatio") blurResolutionLoc = GLES20.glGetUniformLocation(blurProgramId, "uResolution") blurCosAngleLoc = GLES20.glGetUniformLocation(blurProgramId, "uCosAngle") blurSinAngleLoc = GLES20.glGetUniformLocation(blurProgramId, "uSinAngle") blurDirectionLoc = GLES20.glGetUniformLocation(blurProgramId, "uBlurDirection") // Vertex shader is linked into both programs and can be freed GLES20.glDeleteShader(vertexShader) } /** * Activates the passthrough program and binds the camera texture. */ fun usePassthrough(cameraTextureId: Int) { GLES20.glUseProgram(passthroughProgramId) GLES20.glActiveTexture(GLES20.GL_TEXTURE0) GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTextureId) GLES20.glUniform1i(passthroughTextureLoc, 0) } /** * Activates the blur program and sets all uniforms for one blur pass. * * @param fboTextureId The FBO color attachment to sample from. * @param params Current blur parameters. * @param width Surface width in pixels. * @param height Surface height in pixels. * @param dirX Blur direction X component (1 for horizontal pass, 0 for vertical). * @param dirY Blur direction Y component (0 for horizontal pass, 1 for vertical). */ fun useBlurPass( fboTextureId: Int, params: BlurParameters, width: Int, height: Int, dirX: Float, dirY: Float ) { GLES20.glUseProgram(blurProgramId) GLES20.glActiveTexture(GLES20.GL_TEXTURE0) GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, fboTextureId) GLES20.glUniform1i(blurTextureLoc, 0) GLES20.glUniform1i(blurModeLoc, if (params.mode == BlurMode.RADIAL) 1 else 0) GLES20.glUniform1f(blurPositionXLoc, params.positionX) GLES20.glUniform1f(blurPositionYLoc, params.positionY) GLES20.glUniform1f(blurSizeLoc, params.size) GLES20.glUniform1f(blurAmountLoc, params.blurAmount) GLES20.glUniform1f(blurFalloffLoc, params.falloff) GLES20.glUniform1f(blurAspectRatioLoc, params.aspectRatio) GLES20.glUniform2f(blurResolutionLoc, width.toFloat(), height.toFloat()) // Raw screen-space angle (no camera rotation adjustment needed — FBO is already // in screen orientation after the passthrough pass) GLES20.glUniform1f(blurCosAngleLoc, cos(params.angle)) GLES20.glUniform1f(blurSinAngleLoc, sin(params.angle)) GLES20.glUniform2f(blurDirectionLoc, dirX, dirY) } /** * Releases both shader programs. */ fun release() { if (passthroughProgramId != 0) { GLES20.glDeleteProgram(passthroughProgramId) passthroughProgramId = 0 } if (blurProgramId != 0) { GLES20.glDeleteProgram(blurProgramId) blurProgramId = 0 } } private fun linkProgram(vertexShader: Int, fragmentShader: Int): Int { val programId = GLES20.glCreateProgram() GLES20.glAttachShader(programId, vertexShader) GLES20.glAttachShader(programId, fragmentShader) GLES20.glLinkProgram(programId) val linkStatus = IntArray(1) GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, 0) if (linkStatus[0] == 0) { val error = GLES20.glGetProgramInfoLog(programId) GLES20.glDeleteProgram(programId) throw RuntimeException("Shader program link failed: $error") } return programId } private fun loadShaderSource(resourceId: Int): String { val inputStream = context.resources.openRawResource(resourceId) val reader = BufferedReader(InputStreamReader(inputStream)) return reader.use { it.readText() } } private fun compileShader(type: Int, source: String): Int { val shader = GLES20.glCreateShader(type) GLES20.glShaderSource(shader, source) GLES20.glCompileShader(shader) val compileStatus = IntArray(1) GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0) if (compileStatus[0] == 0) { val error = GLES20.glGetShaderInfoLog(shader) GLES20.glDeleteShader(shader) val shaderType = if (type == GLES20.GL_VERTEX_SHADER) "vertex" else "fragment" throw RuntimeException("$shaderType shader compilation failed: $error") } return shader } }