Support landscape orientation
Replace hardcoded portrait-only texture coordinate rotation with SurfaceTexture.getTransformMatrix(), so the camera preview and capture re-orient correctly when the device rotates. Also drive Preview/ImageCapture targetRotation from the live display rotation, fix the crop-to-fill aspect math to swap effective camera dimensions between portrait and landscape, and make the slider control panel scroll if it doesn't fit the shorter landscape height. Bump to 1.1.6. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2633f261ef
commit
d321f07973
7 changed files with 127 additions and 55 deletions
|
|
@ -2,8 +2,10 @@ package no.naiv.tiltshift.camera
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.SurfaceTexture
|
import android.graphics.SurfaceTexture
|
||||||
|
import android.hardware.display.DisplayManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
|
import android.view.Display
|
||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
import androidx.camera.core.Camera
|
import androidx.camera.core.Camera
|
||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
|
|
@ -70,6 +72,14 @@ class CameraManager(private val context: Context) {
|
||||||
/** Weak reference to avoid preventing Activity GC across config changes. */
|
/** Weak reference to avoid preventing Activity GC across config changes. */
|
||||||
private var lifecycleOwnerRef: WeakReference<LifecycleOwner>? = null
|
private var lifecycleOwnerRef: WeakReference<LifecycleOwner>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Target rotation passed to CameraX use cases. Drives the SurfaceTexture
|
||||||
|
* transform matrix and the rotation metadata on captured images.
|
||||||
|
* Initialized to the display rotation when the camera binds; updated by
|
||||||
|
* [setTargetRotation] when the device orientation changes.
|
||||||
|
*/
|
||||||
|
private var targetRotation: Int = Surface.ROTATION_0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the camera with the given lifecycle owner.
|
* Starts the camera with the given lifecycle owner.
|
||||||
* The surfaceTextureProvider should return the SurfaceTexture from the GL renderer.
|
* The surfaceTextureProvider should return the SurfaceTexture from the GL renderer.
|
||||||
|
|
@ -80,6 +90,13 @@ class CameraManager(private val context: Context) {
|
||||||
) {
|
) {
|
||||||
this.surfaceTextureProvider = surfaceTextureProvider
|
this.surfaceTextureProvider = surfaceTextureProvider
|
||||||
this.lifecycleOwnerRef = WeakReference(lifecycleOwner)
|
this.lifecycleOwnerRef = WeakReference(lifecycleOwner)
|
||||||
|
// Capture initial display rotation so the very first frame is oriented correctly,
|
||||||
|
// before the OrientationEventListener has had a chance to fire.
|
||||||
|
// Note: Context.getDisplay() throws on Application contexts; DisplayManager works
|
||||||
|
// for any context type and returns the default display.
|
||||||
|
targetRotation = (context.getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager)
|
||||||
|
?.getDisplay(Display.DEFAULT_DISPLAY)?.rotation
|
||||||
|
?: Surface.ROTATION_0
|
||||||
|
|
||||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||||
cameraProviderFuture.addListener({
|
cameraProviderFuture.addListener({
|
||||||
|
|
@ -110,11 +127,13 @@ class CameraManager(private val context: Context) {
|
||||||
|
|
||||||
preview = Preview.Builder()
|
preview = Preview.Builder()
|
||||||
.setResolutionSelector(resolutionSelector)
|
.setResolutionSelector(resolutionSelector)
|
||||||
|
.setTargetRotation(targetRotation)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// Image capture use case
|
// Image capture use case
|
||||||
val captureBuilder = ImageCapture.Builder()
|
val captureBuilder = ImageCapture.Builder()
|
||||||
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
|
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
|
||||||
|
.setTargetRotation(targetRotation)
|
||||||
|
|
||||||
imageCapture = captureBuilder.build()
|
imageCapture = captureBuilder.build()
|
||||||
|
|
||||||
|
|
@ -173,6 +192,19 @@ class CameraManager(private val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the target rotation for Preview and ImageCapture use cases.
|
||||||
|
* Call when the device rotates: this rotates the SurfaceTexture transform matrix
|
||||||
|
* (so the GL preview stays upright) and tags captures with the right orientation.
|
||||||
|
* Safe to call on the main thread; CameraX permits live target-rotation updates.
|
||||||
|
*/
|
||||||
|
fun setTargetRotation(rotation: Int) {
|
||||||
|
if (targetRotation == rotation) return
|
||||||
|
targetRotation = rotation
|
||||||
|
preview?.targetRotation = rotation
|
||||||
|
imageCapture?.targetRotation = rotation
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the zoom ratio. Updates UI state immediately so that rapid pinch-to-zoom
|
* Sets the zoom ratio. Updates UI state immediately so that rapid pinch-to-zoom
|
||||||
* gestures accumulate correctly (each frame uses the latest ratio as its base).
|
* gestures accumulate correctly (each frame uses the latest ratio as its base).
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import android.opengl.GLES11Ext
|
||||||
import android.opengl.GLES20
|
import android.opengl.GLES20
|
||||||
import android.opengl.GLSurfaceView
|
import android.opengl.GLSurfaceView
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.Surface
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
import java.nio.FloatBuffer
|
import java.nio.FloatBuffer
|
||||||
|
|
@ -38,9 +39,8 @@ class TiltShiftRenderer(
|
||||||
private var surfaceTexture: SurfaceTexture? = null
|
private var surfaceTexture: SurfaceTexture? = null
|
||||||
private var cameraTextureId: Int = 0
|
private var cameraTextureId: Int = 0
|
||||||
|
|
||||||
// Camera quad: crop-to-fill vertices + rotated texcoords (pass 1 only)
|
// Camera quad: crop-to-fill vertices, standard texcoords (rotation comes from texMatrix)
|
||||||
private lateinit var cameraVertexBuffer: FloatBuffer
|
private lateinit var cameraVertexBuffer: FloatBuffer
|
||||||
private lateinit var cameraTexCoordBuffer: FloatBuffer
|
|
||||||
|
|
||||||
// Fullscreen quad for blur passes (no crop, standard texcoords)
|
// Fullscreen quad for blur passes (no crop, standard texcoords)
|
||||||
private lateinit var fullscreenVertexBuffer: FloatBuffer
|
private lateinit var fullscreenVertexBuffer: FloatBuffer
|
||||||
|
|
@ -54,6 +54,9 @@ class TiltShiftRenderer(
|
||||||
private var fboTexA: Int = 0
|
private var fboTexA: Int = 0
|
||||||
private var fboTexB: Int = 0
|
private var fboTexB: Int = 0
|
||||||
|
|
||||||
|
// SurfaceTexture transform matrix, refreshed each frame on the GL thread.
|
||||||
|
private val texMatrix = FloatArray(16)
|
||||||
|
|
||||||
// Current effect parameters (updated from UI thread)
|
// Current effect parameters (updated from UI thread)
|
||||||
@Volatile
|
@Volatile
|
||||||
var blurParameters: BlurParameters = BlurParameters.DEFAULT
|
var blurParameters: BlurParameters = BlurParameters.DEFAULT
|
||||||
|
|
@ -66,33 +69,14 @@ class TiltShiftRenderer(
|
||||||
private var cameraWidth: Int = 0
|
private var cameraWidth: Int = 0
|
||||||
@Volatile
|
@Volatile
|
||||||
private var cameraHeight: Int = 0
|
private var cameraHeight: Int = 0
|
||||||
|
|
||||||
|
/** Display rotation as a Surface.ROTATION_* constant; affects effective aspect. */
|
||||||
|
@Volatile
|
||||||
|
private var displayRotation: Int = Surface.ROTATION_0
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var vertexBufferDirty: Boolean = false
|
private var vertexBufferDirty: Boolean = false
|
||||||
|
|
||||||
// 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
|
|
||||||
)
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
private var currentTexCoords = texCoordsBack
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
private var updateTexCoordBuffer = false
|
|
||||||
|
|
||||||
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
|
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
|
||||||
GLES20.glClearColor(0f, 0f, 0f, 1f)
|
GLES20.glClearColor(0f, 0f, 0f, 1f)
|
||||||
|
|
||||||
|
|
@ -104,12 +88,8 @@ class TiltShiftRenderer(
|
||||||
cameraVertexBuffer.put(floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f))
|
cameraVertexBuffer.put(floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f))
|
||||||
cameraVertexBuffer.position(0)
|
cameraVertexBuffer.position(0)
|
||||||
|
|
||||||
// Camera texcoord buffer (rotated for portrait)
|
// Fullscreen quad for blur passes (standard coords). The same buffer is reused
|
||||||
cameraTexCoordBuffer = allocateFloatBuffer(8)
|
// for the camera passthrough texcoords — rotation is applied via uTexMatrix.
|
||||||
cameraTexCoordBuffer.put(currentTexCoords)
|
|
||||||
cameraTexCoordBuffer.position(0)
|
|
||||||
|
|
||||||
// Fullscreen quad for blur passes (standard coords)
|
|
||||||
fullscreenVertexBuffer = allocateFloatBuffer(8)
|
fullscreenVertexBuffer = allocateFloatBuffer(8)
|
||||||
fullscreenVertexBuffer.put(floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f))
|
fullscreenVertexBuffer.put(floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f))
|
||||||
fullscreenVertexBuffer.position(0)
|
fullscreenVertexBuffer.position(0)
|
||||||
|
|
@ -145,20 +125,17 @@ class TiltShiftRenderer(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDrawFrame(gl: GL10?) {
|
override fun onDrawFrame(gl: GL10?) {
|
||||||
surfaceTexture?.updateTexImage()
|
val st = surfaceTexture
|
||||||
|
st?.updateTexImage()
|
||||||
|
// Pull the latest sensor-to-display transform; updated by SurfaceTexture each frame
|
||||||
|
// and reflects whatever rotation CameraX requested via Preview.targetRotation.
|
||||||
|
st?.getTransformMatrix(texMatrix)
|
||||||
|
|
||||||
if (vertexBufferDirty) {
|
if (vertexBufferDirty) {
|
||||||
recomputeVertices()
|
recomputeVertices()
|
||||||
vertexBufferDirty = false
|
vertexBufferDirty = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateTexCoordBuffer) {
|
|
||||||
cameraTexCoordBuffer.clear()
|
|
||||||
cameraTexCoordBuffer.put(currentTexCoords)
|
|
||||||
cameraTexCoordBuffer.position(0)
|
|
||||||
updateTexCoordBuffer = false
|
|
||||||
}
|
|
||||||
|
|
||||||
val params = blurParameters
|
val params = blurParameters
|
||||||
|
|
||||||
// --- Pass 1: Camera → FBO-A (passthrough with crop-to-fill) ---
|
// --- Pass 1: Camera → FBO-A (passthrough with crop-to-fill) ---
|
||||||
|
|
@ -169,10 +146,10 @@ class TiltShiftRenderer(
|
||||||
)
|
)
|
||||||
GLES20.glViewport(0, 0, surfaceWidth, surfaceHeight)
|
GLES20.glViewport(0, 0, surfaceWidth, surfaceHeight)
|
||||||
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
|
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
|
||||||
shader.usePassthrough(cameraTextureId)
|
shader.usePassthrough(cameraTextureId, texMatrix, isFrontCamera)
|
||||||
drawQuad(
|
drawQuad(
|
||||||
shader.passthroughPositionLoc, shader.passthroughTexCoordLoc,
|
shader.passthroughPositionLoc, shader.passthroughTexCoordLoc,
|
||||||
cameraVertexBuffer, cameraTexCoordBuffer
|
cameraVertexBuffer, fullscreenTexCoordBuffer
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Pass 2: FBO-A → FBO-B (horizontal blur) ---
|
// --- Pass 2: FBO-A → FBO-B (horizontal blur) ---
|
||||||
|
|
@ -203,11 +180,7 @@ class TiltShiftRenderer(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setFrontCamera(front: Boolean) {
|
fun setFrontCamera(front: Boolean) {
|
||||||
if (isFrontCamera != front) {
|
|
||||||
isFrontCamera = front
|
isFrontCamera = front
|
||||||
currentTexCoords = if (front) texCoordsFront else texCoordsBack
|
|
||||||
updateTexCoordBuffer = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCameraResolution(width: Int, height: Int) {
|
fun setCameraResolution(width: Int, height: Int) {
|
||||||
|
|
@ -218,6 +191,14 @@ class TiltShiftRenderer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Updates the display rotation so crop-to-fill picks the right effective aspect. */
|
||||||
|
fun setDisplayRotation(rotation: Int) {
|
||||||
|
if (displayRotation != rotation) {
|
||||||
|
displayRotation = rotation
|
||||||
|
vertexBufferDirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun release() {
|
fun release() {
|
||||||
shader.release()
|
shader.release()
|
||||||
surfaceTexture?.release()
|
surfaceTexture?.release()
|
||||||
|
|
@ -254,16 +235,24 @@ class TiltShiftRenderer(
|
||||||
/**
|
/**
|
||||||
* Recomputes camera vertex positions to achieve crop-to-fill.
|
* Recomputes camera vertex positions to achieve crop-to-fill.
|
||||||
*
|
*
|
||||||
* The camera sensor is landscape; after the 90° rotation applied via texture coordinates,
|
* The camera buffer is the sensor's native landscape resolution. The texMatrix
|
||||||
* the effective portrait dimensions are (cameraHeight × cameraWidth). We scale the vertex
|
* rotates it to match the display, so the *effective* displayed dimensions
|
||||||
* quad so the camera frame fills the surface without stretching — the GPU clips the overflow.
|
* depend on the display rotation: in portrait the buffer is rotated 90°
|
||||||
|
* (effective width = cameraHeight), in landscape it is unrotated.
|
||||||
|
* We scale the vertex quad so the frame fills the surface without stretching —
|
||||||
|
* the GPU clips the overflow.
|
||||||
*/
|
*/
|
||||||
private fun recomputeVertices() {
|
private fun recomputeVertices() {
|
||||||
var scaleX = 1f
|
var scaleX = 1f
|
||||||
var scaleY = 1f
|
var scaleY = 1f
|
||||||
|
|
||||||
if (cameraWidth > 0 && cameraHeight > 0 && surfaceWidth > 0 && surfaceHeight > 0) {
|
if (cameraWidth > 0 && cameraHeight > 0 && surfaceWidth > 0 && surfaceHeight > 0) {
|
||||||
val cameraRatio = cameraHeight.toFloat() / cameraWidth
|
val isPortrait = displayRotation == Surface.ROTATION_0 ||
|
||||||
|
displayRotation == Surface.ROTATION_180
|
||||||
|
val effectiveW = if (isPortrait) cameraHeight else cameraWidth
|
||||||
|
val effectiveH = if (isPortrait) cameraWidth else cameraHeight
|
||||||
|
|
||||||
|
val cameraRatio = effectiveW.toFloat() / effectiveH
|
||||||
val screenRatio = surfaceWidth.toFloat() / surfaceHeight
|
val screenRatio = surfaceWidth.toFloat() / surfaceHeight
|
||||||
|
|
||||||
if (cameraRatio > screenRatio) {
|
if (cameraRatio > screenRatio) {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,15 @@ import kotlin.math.sin
|
||||||
*/
|
*/
|
||||||
class TiltShiftShader(private val context: Context) {
|
class TiltShiftShader(private val context: Context) {
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val IDENTITY_MATRIX = floatArrayOf(
|
||||||
|
1f, 0f, 0f, 0f,
|
||||||
|
0f, 1f, 0f, 0f,
|
||||||
|
0f, 0f, 1f, 0f,
|
||||||
|
0f, 0f, 0f, 1f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// --- Passthrough program (camera → FBO) ---
|
// --- Passthrough program (camera → FBO) ---
|
||||||
|
|
||||||
private var passthroughProgramId: Int = 0
|
private var passthroughProgramId: Int = 0
|
||||||
|
|
@ -29,6 +38,8 @@ class TiltShiftShader(private val context: Context) {
|
||||||
var passthroughTexCoordLoc: Int = 0
|
var passthroughTexCoordLoc: Int = 0
|
||||||
private set
|
private set
|
||||||
private var passthroughTextureLoc: Int = 0
|
private var passthroughTextureLoc: Int = 0
|
||||||
|
private var passthroughTexMatrixLoc: Int = 0
|
||||||
|
private var passthroughMirrorLoc: Int = 0
|
||||||
|
|
||||||
// --- Blur program (FBO → FBO/screen) ---
|
// --- Blur program (FBO → FBO/screen) ---
|
||||||
|
|
||||||
|
|
@ -39,6 +50,8 @@ class TiltShiftShader(private val context: Context) {
|
||||||
var blurTexCoordLoc: Int = 0
|
var blurTexCoordLoc: Int = 0
|
||||||
private set
|
private set
|
||||||
private var blurTextureLoc: Int = 0
|
private var blurTextureLoc: Int = 0
|
||||||
|
private var blurTexMatrixLoc: Int = 0
|
||||||
|
private var blurMirrorLoc: Int = 0
|
||||||
private var blurModeLoc: Int = 0
|
private var blurModeLoc: Int = 0
|
||||||
private var blurPositionXLoc: Int = 0
|
private var blurPositionXLoc: Int = 0
|
||||||
private var blurPositionYLoc: Int = 0
|
private var blurPositionYLoc: Int = 0
|
||||||
|
|
@ -68,6 +81,8 @@ class TiltShiftShader(private val context: Context) {
|
||||||
passthroughPositionLoc = GLES20.glGetAttribLocation(passthroughProgramId, "aPosition")
|
passthroughPositionLoc = GLES20.glGetAttribLocation(passthroughProgramId, "aPosition")
|
||||||
passthroughTexCoordLoc = GLES20.glGetAttribLocation(passthroughProgramId, "aTexCoord")
|
passthroughTexCoordLoc = GLES20.glGetAttribLocation(passthroughProgramId, "aTexCoord")
|
||||||
passthroughTextureLoc = GLES20.glGetUniformLocation(passthroughProgramId, "uTexture")
|
passthroughTextureLoc = GLES20.glGetUniformLocation(passthroughProgramId, "uTexture")
|
||||||
|
passthroughTexMatrixLoc = GLES20.glGetUniformLocation(passthroughProgramId, "uTexMatrix")
|
||||||
|
passthroughMirrorLoc = GLES20.glGetUniformLocation(passthroughProgramId, "uMirrorX")
|
||||||
|
|
||||||
// Blur program
|
// Blur program
|
||||||
val blurFragSource = loadShaderSource(R.raw.tiltshift_fragment)
|
val blurFragSource = loadShaderSource(R.raw.tiltshift_fragment)
|
||||||
|
|
@ -78,6 +93,8 @@ class TiltShiftShader(private val context: Context) {
|
||||||
blurPositionLoc = GLES20.glGetAttribLocation(blurProgramId, "aPosition")
|
blurPositionLoc = GLES20.glGetAttribLocation(blurProgramId, "aPosition")
|
||||||
blurTexCoordLoc = GLES20.glGetAttribLocation(blurProgramId, "aTexCoord")
|
blurTexCoordLoc = GLES20.glGetAttribLocation(blurProgramId, "aTexCoord")
|
||||||
blurTextureLoc = GLES20.glGetUniformLocation(blurProgramId, "uTexture")
|
blurTextureLoc = GLES20.glGetUniformLocation(blurProgramId, "uTexture")
|
||||||
|
blurTexMatrixLoc = GLES20.glGetUniformLocation(blurProgramId, "uTexMatrix")
|
||||||
|
blurMirrorLoc = GLES20.glGetUniformLocation(blurProgramId, "uMirrorX")
|
||||||
blurModeLoc = GLES20.glGetUniformLocation(blurProgramId, "uMode")
|
blurModeLoc = GLES20.glGetUniformLocation(blurProgramId, "uMode")
|
||||||
blurPositionXLoc = GLES20.glGetUniformLocation(blurProgramId, "uPositionX")
|
blurPositionXLoc = GLES20.glGetUniformLocation(blurProgramId, "uPositionX")
|
||||||
blurPositionYLoc = GLES20.glGetUniformLocation(blurProgramId, "uPositionY")
|
blurPositionYLoc = GLES20.glGetUniformLocation(blurProgramId, "uPositionY")
|
||||||
|
|
@ -96,12 +113,19 @@ class TiltShiftShader(private val context: Context) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activates the passthrough program and binds the camera texture.
|
* Activates the passthrough program and binds the camera texture.
|
||||||
|
*
|
||||||
|
* @param cameraTextureId The OES texture receiving camera frames.
|
||||||
|
* @param texMatrix 4x4 transform from SurfaceTexture.getTransformMatrix() —
|
||||||
|
* encodes sensor-to-display rotation and Y-flip.
|
||||||
|
* @param mirrorX true to horizontally mirror (front camera selfie view).
|
||||||
*/
|
*/
|
||||||
fun usePassthrough(cameraTextureId: Int) {
|
fun usePassthrough(cameraTextureId: Int, texMatrix: FloatArray, mirrorX: Boolean) {
|
||||||
GLES20.glUseProgram(passthroughProgramId)
|
GLES20.glUseProgram(passthroughProgramId)
|
||||||
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
|
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
|
||||||
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTextureId)
|
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTextureId)
|
||||||
GLES20.glUniform1i(passthroughTextureLoc, 0)
|
GLES20.glUniform1i(passthroughTextureLoc, 0)
|
||||||
|
GLES20.glUniformMatrix4fv(passthroughTexMatrixLoc, 1, false, texMatrix, 0)
|
||||||
|
GLES20.glUniform1f(passthroughMirrorLoc, if (mirrorX) 1f else 0f)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -128,6 +152,10 @@ class TiltShiftShader(private val context: Context) {
|
||||||
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, fboTextureId)
|
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, fboTextureId)
|
||||||
GLES20.glUniform1i(blurTextureLoc, 0)
|
GLES20.glUniform1i(blurTextureLoc, 0)
|
||||||
|
|
||||||
|
// FBO content is already in display orientation — pass identity matrix and no mirror.
|
||||||
|
GLES20.glUniformMatrix4fv(blurTexMatrixLoc, 1, false, IDENTITY_MATRIX, 0)
|
||||||
|
GLES20.glUniform1f(blurMirrorLoc, 0f)
|
||||||
|
|
||||||
GLES20.glUniform1i(blurModeLoc, if (params.mode == BlurMode.RADIAL) 1 else 0)
|
GLES20.glUniform1i(blurModeLoc, if (params.mode == BlurMode.RADIAL) 1 else 0)
|
||||||
GLES20.glUniform1f(blurPositionXLoc, params.positionX)
|
GLES20.glUniform1f(blurPositionXLoc, params.positionX)
|
||||||
GLES20.glUniform1f(blurPositionYLoc, params.positionY)
|
GLES20.glUniform1f(blurPositionYLoc, params.positionY)
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ fun CameraScreen(
|
||||||
val isFrontCamera by viewModel.cameraManager.isFrontCamera.collectAsState()
|
val isFrontCamera by viewModel.cameraManager.isFrontCamera.collectAsState()
|
||||||
val previewResolution by viewModel.cameraManager.previewResolution.collectAsState()
|
val previewResolution by viewModel.cameraManager.previewResolution.collectAsState()
|
||||||
val cameraError by viewModel.cameraManager.error.collectAsState()
|
val cameraError by viewModel.cameraManager.error.collectAsState()
|
||||||
|
val currentRotation by viewModel.currentRotation.collectAsState()
|
||||||
|
|
||||||
// Gallery picker
|
// Gallery picker
|
||||||
val galleryLauncher = rememberLauncherForActivityResult(
|
val galleryLauncher = rememberLauncherForActivityResult(
|
||||||
|
|
@ -164,6 +165,13 @@ fun CameraScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Forward device rotation to renderer (aspect math) and CameraX (target rotation)
|
||||||
|
LaunchedEffect(currentRotation, renderer) {
|
||||||
|
renderer?.setDisplayRotation(currentRotation)
|
||||||
|
viewModel.cameraManager.setTargetRotation(currentRotation)
|
||||||
|
glSurfaceView?.requestRender()
|
||||||
|
}
|
||||||
|
|
||||||
// Start camera when surface texture is available
|
// Start camera when surface texture is available
|
||||||
LaunchedEffect(surfaceTexture) {
|
LaunchedEffect(surfaceTexture) {
|
||||||
surfaceTexture?.let {
|
surfaceTexture?.let {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,10 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.RestartAlt
|
import androidx.compose.material.icons.filled.RestartAlt
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
|
@ -110,8 +113,10 @@ fun ControlPanel(
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.width(200.dp)
|
.width(200.dp)
|
||||||
|
.wrapContentHeight()
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.background(AppColors.OverlayDarker)
|
.background(AppColors.OverlayDarker)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
// Vertex shader for tilt-shift effect
|
// Vertex shader for tilt-shift effect.
|
||||||
// Passes through position and calculates texture coordinates
|
//
|
||||||
|
// uTexMatrix: applied to texcoords. For the passthrough pass it carries the
|
||||||
|
// SurfaceTexture transform (sensor → display rotation, plus Y-flip). For the
|
||||||
|
// blur passes it is identity.
|
||||||
|
// uMirrorX: 1.0 to horizontally mirror texcoords (front-camera selfie view),
|
||||||
|
// 0.0 otherwise. Applied AFTER uTexMatrix.
|
||||||
|
|
||||||
attribute vec4 aPosition;
|
attribute vec4 aPosition;
|
||||||
attribute vec2 aTexCoord;
|
attribute vec2 aTexCoord;
|
||||||
|
|
||||||
|
uniform mat4 uTexMatrix;
|
||||||
|
uniform float uMirrorX;
|
||||||
|
|
||||||
varying vec2 vTexCoord;
|
varying vec2 vTexCoord;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
gl_Position = aPosition;
|
gl_Position = aPosition;
|
||||||
vTexCoord = aTexCoord;
|
vec2 tc = (uTexMatrix * vec4(aTexCoord, 0.0, 1.0)).xy;
|
||||||
|
if (uMirrorX > 0.5) tc.x = 1.0 - tc.x;
|
||||||
|
vTexCoord = tc;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
versionMajor=1
|
versionMajor=1
|
||||||
versionMinor=1
|
versionMinor=1
|
||||||
versionPatch=5
|
versionPatch=6
|
||||||
versionCode=7
|
versionCode=8
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue