Reapply "Revert orientation tracking on the camera image"

This reverts commit c0bab85d63.
This commit is contained in:
Ole-Morten Duesund 2026-05-11 15:57:32 +02:00
commit b0691adfa3
7 changed files with 55 additions and 171 deletions

View file

@ -5,9 +5,7 @@ import android.graphics.SurfaceTexture
import android.opengl.GLES11Ext
import android.opengl.GLES20
import android.opengl.GLSurfaceView
import android.opengl.Matrix
import android.util.Log
import android.view.Surface
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
@ -40,8 +38,9 @@ class TiltShiftRenderer(
private var surfaceTexture: SurfaceTexture? = null
private var cameraTextureId: Int = 0
// Camera quad: crop-to-fill vertices, standard texcoords (rotation comes from texMatrix)
// Camera quad: crop-to-fill vertices + rotated texcoords (pass 1 only)
private lateinit var cameraVertexBuffer: FloatBuffer
private lateinit var cameraTexCoordBuffer: FloatBuffer
// Fullscreen quad for blur passes (no crop, standard texcoords)
private lateinit var fullscreenVertexBuffer: FloatBuffer
@ -55,11 +54,6 @@ class TiltShiftRenderer(
private var fboTexA: Int = 0
private var fboTexB: Int = 0
// SurfaceTexture transform matrix, refreshed each frame on the GL thread.
private val texMatrix = FloatArray(16)
// Sensor-to-buffer matrix from SurfaceTexture before display-rotation correction.
private val sensorMatrix = FloatArray(16)
// Current effect parameters (updated from UI thread)
@Volatile
var blurParameters: BlurParameters = BlurParameters.DEFAULT
@ -72,14 +66,33 @@ class TiltShiftRenderer(
private var cameraWidth: Int = 0
@Volatile
private var cameraHeight: Int = 0
/** Display rotation as a Surface.ROTATION_* constant; affects effective aspect. */
@Volatile
private var displayRotation: Int = Surface.ROTATION_0
@Volatile
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?) {
GLES20.glClearColor(0f, 0f, 0f, 1f)
@ -91,8 +104,12 @@ class TiltShiftRenderer(
cameraVertexBuffer.put(floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f))
cameraVertexBuffer.position(0)
// Fullscreen quad for blur passes (standard coords). The same buffer is reused
// for the camera passthrough texcoords — rotation is applied via uTexMatrix.
// Camera texcoord buffer (rotated for portrait)
cameraTexCoordBuffer = allocateFloatBuffer(8)
cameraTexCoordBuffer.put(currentTexCoords)
cameraTexCoordBuffer.position(0)
// Fullscreen quad for blur passes (standard coords)
fullscreenVertexBuffer = allocateFloatBuffer(8)
fullscreenVertexBuffer.put(floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f))
fullscreenVertexBuffer.position(0)
@ -128,19 +145,20 @@ class TiltShiftRenderer(
}
override fun onDrawFrame(gl: GL10?) {
val st = surfaceTexture
st?.updateTexImage()
// SurfaceTexture's transform matrix only handles the sensor-to-buffer
// orientation; for a custom SurfaceProvider it does NOT vary with
// Preview.targetRotation. Apply the display-rotation correction here.
st?.getTransformMatrix(sensorMatrix)
composeTexMatrix()
surfaceTexture?.updateTexImage()
if (vertexBufferDirty) {
recomputeVertices()
vertexBufferDirty = false
}
if (updateTexCoordBuffer) {
cameraTexCoordBuffer.clear()
cameraTexCoordBuffer.put(currentTexCoords)
cameraTexCoordBuffer.position(0)
updateTexCoordBuffer = false
}
val params = blurParameters
// --- Pass 1: Camera → FBO-A (passthrough with crop-to-fill) ---
@ -151,10 +169,10 @@ class TiltShiftRenderer(
)
GLES20.glViewport(0, 0, surfaceWidth, surfaceHeight)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
shader.usePassthrough(cameraTextureId, texMatrix, isFrontCamera)
shader.usePassthrough(cameraTextureId)
drawQuad(
shader.passthroughPositionLoc, shader.passthroughTexCoordLoc,
cameraVertexBuffer, fullscreenTexCoordBuffer
cameraVertexBuffer, cameraTexCoordBuffer
)
// --- Pass 2: FBO-A → FBO-B (horizontal blur) ---
@ -185,7 +203,11 @@ class TiltShiftRenderer(
}
fun setFrontCamera(front: Boolean) {
isFrontCamera = front
if (isFrontCamera != front) {
isFrontCamera = front
currentTexCoords = if (front) texCoordsFront else texCoordsBack
updateTexCoordBuffer = true
}
}
fun setCameraResolution(width: Int, height: Int) {
@ -196,49 +218,6 @@ 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
}
}
/**
* Combines the sensor-to-buffer matrix with a rotation that compensates for
* the activity's current rotation. The activity rotates with the device
* (screenOrientation="fullSensor"), so the GL clip-space "up" direction
* tracks the device rather than the world. To keep world-up at screen-up
* regardless of orientation, rotate the texcoord sampling pattern by the
* inverse of the activity rotation. Without this correction, the two
* landscape orientations would render the same matrix and one would appear
* upside-down on a real device.
*/
private fun composeTexMatrix() {
// Inverse of the activity rotation: rotating the sampling pattern by
// -activityAngle puts the world-aligned point originally at screen P
// at the same screen P after the activity has rotated.
val angle = when (displayRotation) {
Surface.ROTATION_90 -> -90f
Surface.ROTATION_180 -> 180f
Surface.ROTATION_270 -> 90f
else -> 0f
}
if (angle == 0f) {
System.arraycopy(sensorMatrix, 0, texMatrix, 0, 16)
return
}
// Build rotation around the (0.5, 0.5) texcoord center.
Matrix.setIdentityM(texMatrix, 0)
Matrix.translateM(texMatrix, 0, 0.5f, 0.5f, 0f)
Matrix.rotateM(texMatrix, 0, angle, 0f, 0f, 1f)
Matrix.translateM(texMatrix, 0, -0.5f, -0.5f, 0f)
// texMatrix = sensorMatrix * rotation (rotation is applied to the
// texcoord first, then the sensor-to-buffer transform).
val rot = texMatrix.copyOf()
Matrix.multiplyMM(texMatrix, 0, sensorMatrix, 0, rot, 0)
}
fun release() {
shader.release()
surfaceTexture?.release()
@ -275,24 +254,16 @@ class TiltShiftRenderer(
/**
* Recomputes camera vertex positions to achieve crop-to-fill.
*
* The camera buffer is the sensor's native landscape resolution. The texMatrix
* rotates it to match the display, so the *effective* displayed dimensions
* 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.
* The camera sensor is landscape; after the 90° rotation applied via texture coordinates,
* the effective portrait dimensions are (cameraHeight × cameraWidth). We scale the vertex
* quad so the camera frame fills the surface without stretching the GPU clips the overflow.
*/
private fun recomputeVertices() {
var scaleX = 1f
var scaleY = 1f
if (cameraWidth > 0 && cameraHeight > 0 && surfaceWidth > 0 && surfaceHeight > 0) {
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 cameraRatio = cameraHeight.toFloat() / cameraWidth
val screenRatio = surfaceWidth.toFloat() / surfaceHeight
if (cameraRatio > screenRatio) {