2026-01-28 15:26:41 +01:00
|
|
|
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
|
|
|
|
|
|
2026-01-29 17:03:26 +01:00
|
|
|
@Volatile
|
|
|
|
|
private var isFrontCamera: Boolean = false
|
|
|
|
|
|
2026-01-28 15:26:41 +01:00
|
|
|
// Quad vertices (full screen)
|
|
|
|
|
private val vertices = floatArrayOf(
|
|
|
|
|
-1f, -1f, // Bottom left
|
|
|
|
|
1f, -1f, // Bottom right
|
|
|
|
|
-1f, 1f, // Top left
|
|
|
|
|
1f, 1f // Top right
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-29 17:03:26 +01:00
|
|
|
// Texture coordinates rotated 90° for portrait mode (back camera)
|
2026-01-28 15:32:31 +01:00
|
|
|
// (Camera sensors are landscape-oriented, we rotate to portrait)
|
2026-01-29 17:03:26 +01:00
|
|
|
private val texCoordsBack = floatArrayOf(
|
2026-01-28 15:32:31 +01:00
|
|
|
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
|
2026-01-28 15:26:41 +01:00
|
|
|
)
|
|
|
|
|
|
2026-01-29 17:03:26 +01:00
|
|
|
// 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
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-05 13:44:12 +01:00
|
|
|
@Volatile
|
2026-01-29 17:03:26 +01:00
|
|
|
private var currentTexCoords = texCoordsBack
|
|
|
|
|
|
2026-01-28 15:26:41 +01:00
|
|
|
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
|
2026-01-29 17:03:26 +01:00
|
|
|
texCoordBuffer = ByteBuffer.allocateDirect(currentTexCoords.size * 4)
|
2026-01-28 15:26:41 +01:00
|
|
|
.order(ByteOrder.nativeOrder())
|
|
|
|
|
.asFloatBuffer()
|
2026-01-29 17:03:26 +01:00
|
|
|
.put(currentTexCoords)
|
2026-01-28 15:26:41 +01:00
|
|
|
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()
|
|
|
|
|
|
2026-01-29 17:03:26 +01:00
|
|
|
// Update texture coordinate buffer if camera changed
|
|
|
|
|
if (updateTexCoordBuffer) {
|
|
|
|
|
texCoordBuffer.clear()
|
|
|
|
|
texCoordBuffer.put(currentTexCoords)
|
|
|
|
|
texCoordBuffer.position(0)
|
|
|
|
|
updateTexCoordBuffer = false
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 15:26:41 +01:00
|
|
|
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
|
|
|
|
|
|
|
|
|
|
// Use shader and set parameters
|
2026-01-29 17:07:44 +01:00
|
|
|
shader.use(cameraTextureId, blurParameters, surfaceWidth, surfaceHeight, isFrontCamera)
|
2026-01-28 15:26:41 +01:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 17:03:26 +01:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
|
2026-01-28 15:26:41 +01:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|