2026-01-28 15:26:41 +01:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
2026-01-29 11:13:31 +01:00
|
|
|
private var uModeLocation: Int = 0
|
2026-01-28 15:26:41 +01:00
|
|
|
private var uAngleLocation: Int = 0
|
2026-01-28 15:55:17 +01:00
|
|
|
private var uPositionXLocation: Int = 0
|
|
|
|
|
private var uPositionYLocation: Int = 0
|
2026-01-28 15:26:41 +01:00
|
|
|
private var uSizeLocation: Int = 0
|
|
|
|
|
private var uBlurAmountLocation: Int = 0
|
2026-01-29 11:13:31 +01:00
|
|
|
private var uFalloffLocation: Int = 0
|
|
|
|
|
private var uAspectRatioLocation: Int = 0
|
2026-01-28 15:26:41 +01:00
|
|
|
private var uResolutionLocation: 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")
|
2026-01-29 11:13:31 +01:00
|
|
|
uModeLocation = GLES20.glGetUniformLocation(programId, "uMode")
|
2026-01-28 15:26:41 +01:00
|
|
|
uAngleLocation = GLES20.glGetUniformLocation(programId, "uAngle")
|
2026-01-28 15:55:17 +01:00
|
|
|
uPositionXLocation = GLES20.glGetUniformLocation(programId, "uPositionX")
|
|
|
|
|
uPositionYLocation = GLES20.glGetUniformLocation(programId, "uPositionY")
|
2026-01-28 15:26:41 +01:00
|
|
|
uSizeLocation = GLES20.glGetUniformLocation(programId, "uSize")
|
|
|
|
|
uBlurAmountLocation = GLES20.glGetUniformLocation(programId, "uBlurAmount")
|
2026-01-29 11:13:31 +01:00
|
|
|
uFalloffLocation = GLES20.glGetUniformLocation(programId, "uFalloff")
|
|
|
|
|
uAspectRatioLocation = GLES20.glGetUniformLocation(programId, "uAspectRatio")
|
2026-01-28 15:26:41 +01:00
|
|
|
uResolutionLocation = GLES20.glGetUniformLocation(programId, "uResolution")
|
|
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
|
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
|
2026-01-29 11:13:31 +01:00
|
|
|
GLES20.glUniform1i(uModeLocation, if (params.mode == BlurMode.RADIAL) 1 else 0)
|
2026-01-28 15:26:41 +01:00
|
|
|
GLES20.glUniform1f(uAngleLocation, params.angle)
|
2026-01-28 15:55:17 +01:00
|
|
|
GLES20.glUniform1f(uPositionXLocation, params.positionX)
|
|
|
|
|
GLES20.glUniform1f(uPositionYLocation, params.positionY)
|
2026-01-28 15:26:41 +01:00
|
|
|
GLES20.glUniform1f(uSizeLocation, params.size)
|
|
|
|
|
GLES20.glUniform1f(uBlurAmountLocation, params.blurAmount)
|
2026-01-29 11:13:31 +01:00
|
|
|
GLES20.glUniform1f(uFalloffLocation, params.falloff)
|
|
|
|
|
GLES20.glUniform1f(uAspectRatioLocation, params.aspectRatio)
|
2026-01-28 15:26:41 +01:00
|
|
|
GLES20.glUniform2f(uResolutionLocation, width.toFloat(), height.toFloat())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
}
|
|
|
|
|
}
|