package no.naiv.tiltshift.effect import android.content.Context import android.opengl.GLES11Ext import android.opengl.GLES20 import no.naiv.tiltshift.R import kotlin.math.cos import kotlin.math.sin import java.io.BufferedReader import java.io.InputStreamReader /** * Manages OpenGL shader programs for the tilt-shift effect. */ class TiltShiftShader(private val context: Context) { var programId: Int = 0 private set // Attribute locations var aPositionLocation: Int = 0 private set var aTexCoordLocation: Int = 0 private set // Uniform locations private var uTextureLocation: Int = 0 private var uModeLocation: Int = 0 private var uIsFrontCameraLocation: Int = 0 private var uAngleLocation: Int = 0 private var uPositionXLocation: Int = 0 private var uPositionYLocation: Int = 0 private var uSizeLocation: Int = 0 private var uBlurAmountLocation: Int = 0 private var uFalloffLocation: Int = 0 private var uAspectRatioLocation: Int = 0 private var uResolutionLocation: Int = 0 private var uCosAngleLocation: Int = 0 private var uSinAngleLocation: Int = 0 /** * Compiles and links the shader program. * Must be called from GL thread. */ fun initialize() { val vertexSource = loadShaderSource(R.raw.tiltshift_vertex) val fragmentSource = loadShaderSource(R.raw.tiltshift_fragment) val vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, vertexSource) val fragmentShader = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource) programId = GLES20.glCreateProgram() GLES20.glAttachShader(programId, vertexShader) GLES20.glAttachShader(programId, fragmentShader) GLES20.glLinkProgram(programId) // Check for link errors 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") } // Get attribute locations aPositionLocation = GLES20.glGetAttribLocation(programId, "aPosition") aTexCoordLocation = GLES20.glGetAttribLocation(programId, "aTexCoord") // Get uniform locations uTextureLocation = GLES20.glGetUniformLocation(programId, "uTexture") uModeLocation = GLES20.glGetUniformLocation(programId, "uMode") uIsFrontCameraLocation = GLES20.glGetUniformLocation(programId, "uIsFrontCamera") uAngleLocation = GLES20.glGetUniformLocation(programId, "uAngle") uPositionXLocation = GLES20.glGetUniformLocation(programId, "uPositionX") uPositionYLocation = GLES20.glGetUniformLocation(programId, "uPositionY") uSizeLocation = GLES20.glGetUniformLocation(programId, "uSize") uBlurAmountLocation = GLES20.glGetUniformLocation(programId, "uBlurAmount") uFalloffLocation = GLES20.glGetUniformLocation(programId, "uFalloff") uAspectRatioLocation = GLES20.glGetUniformLocation(programId, "uAspectRatio") uResolutionLocation = GLES20.glGetUniformLocation(programId, "uResolution") uCosAngleLocation = GLES20.glGetUniformLocation(programId, "uCosAngle") uSinAngleLocation = GLES20.glGetUniformLocation(programId, "uSinAngle") // Clean up shaders (they're linked into program now) GLES20.glDeleteShader(vertexShader) GLES20.glDeleteShader(fragmentShader) } /** * Uses the shader program and sets uniforms. */ fun use(textureId: Int, params: BlurParameters, width: Int, height: Int, isFrontCamera: Boolean = false) { GLES20.glUseProgram(programId) // Bind camera texture GLES20.glActiveTexture(GLES20.GL_TEXTURE0) GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId) GLES20.glUniform1i(uTextureLocation, 0) // Set effect parameters GLES20.glUniform1i(uModeLocation, if (params.mode == BlurMode.RADIAL) 1 else 0) GLES20.glUniform1i(uIsFrontCameraLocation, if (isFrontCamera) 1 else 0) GLES20.glUniform1f(uAngleLocation, params.angle) GLES20.glUniform1f(uPositionXLocation, params.positionX) GLES20.glUniform1f(uPositionYLocation, params.positionY) GLES20.glUniform1f(uSizeLocation, params.size) GLES20.glUniform1f(uBlurAmountLocation, params.blurAmount) GLES20.glUniform1f(uFalloffLocation, params.falloff) GLES20.glUniform1f(uAspectRatioLocation, params.aspectRatio) GLES20.glUniform2f(uResolutionLocation, width.toFloat(), height.toFloat()) // Precompute angle trig on CPU to avoid per-fragment transcendental calls. // The adjusted angle accounts for the 90deg coordinate transform. val adjustedAngle = if (isFrontCamera) { -params.angle - (Math.PI / 2).toFloat() } else { params.angle + (Math.PI / 2).toFloat() } GLES20.glUniform1f(uCosAngleLocation, cos(adjustedAngle)) GLES20.glUniform1f(uSinAngleLocation, sin(adjustedAngle)) } /** * Releases shader resources. */ fun release() { if (programId != 0) { GLES20.glDeleteProgram(programId) programId = 0 } } 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) // Check for compile errors 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 } }