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
|
|
@ -6,6 +6,7 @@ import android.opengl.GLES11Ext
|
|||
import android.opengl.GLES20
|
||||
import android.opengl.GLSurfaceView
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.nio.FloatBuffer
|
||||
|
|
@ -38,9 +39,8 @@ class TiltShiftRenderer(
|
|||
private var surfaceTexture: SurfaceTexture? = null
|
||||
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 cameraTexCoordBuffer: FloatBuffer
|
||||
|
||||
// Fullscreen quad for blur passes (no crop, standard texcoords)
|
||||
private lateinit var fullscreenVertexBuffer: FloatBuffer
|
||||
|
|
@ -54,6 +54,9 @@ 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)
|
||||
|
||||
// Current effect parameters (updated from UI thread)
|
||||
@Volatile
|
||||
var blurParameters: BlurParameters = BlurParameters.DEFAULT
|
||||
|
|
@ -66,33 +69,14 @@ 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)
|
||||
|
||||
|
|
@ -104,12 +88,8 @@ class TiltShiftRenderer(
|
|||
cameraVertexBuffer.put(floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f))
|
||||
cameraVertexBuffer.position(0)
|
||||
|
||||
// Camera texcoord buffer (rotated for portrait)
|
||||
cameraTexCoordBuffer = allocateFloatBuffer(8)
|
||||
cameraTexCoordBuffer.put(currentTexCoords)
|
||||
cameraTexCoordBuffer.position(0)
|
||||
|
||||
// Fullscreen quad for blur passes (standard coords)
|
||||
// Fullscreen quad for blur passes (standard coords). The same buffer is reused
|
||||
// for the camera passthrough texcoords — rotation is applied via uTexMatrix.
|
||||
fullscreenVertexBuffer = allocateFloatBuffer(8)
|
||||
fullscreenVertexBuffer.put(floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f))
|
||||
fullscreenVertexBuffer.position(0)
|
||||
|
|
@ -145,20 +125,17 @@ class TiltShiftRenderer(
|
|||
}
|
||||
|
||||
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) {
|
||||
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) ---
|
||||
|
|
@ -169,10 +146,10 @@ class TiltShiftRenderer(
|
|||
)
|
||||
GLES20.glViewport(0, 0, surfaceWidth, surfaceHeight)
|
||||
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
|
||||
shader.usePassthrough(cameraTextureId)
|
||||
shader.usePassthrough(cameraTextureId, texMatrix, isFrontCamera)
|
||||
drawQuad(
|
||||
shader.passthroughPositionLoc, shader.passthroughTexCoordLoc,
|
||||
cameraVertexBuffer, cameraTexCoordBuffer
|
||||
cameraVertexBuffer, fullscreenTexCoordBuffer
|
||||
)
|
||||
|
||||
// --- Pass 2: FBO-A → FBO-B (horizontal blur) ---
|
||||
|
|
@ -203,11 +180,7 @@ class TiltShiftRenderer(
|
|||
}
|
||||
|
||||
fun setFrontCamera(front: Boolean) {
|
||||
if (isFrontCamera != front) {
|
||||
isFrontCamera = front
|
||||
currentTexCoords = if (front) texCoordsFront else texCoordsBack
|
||||
updateTexCoordBuffer = true
|
||||
}
|
||||
isFrontCamera = front
|
||||
}
|
||||
|
||||
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() {
|
||||
shader.release()
|
||||
surfaceTexture?.release()
|
||||
|
|
@ -254,16 +235,24 @@ class TiltShiftRenderer(
|
|||
/**
|
||||
* Recomputes camera vertex positions to achieve crop-to-fill.
|
||||
*
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
private fun recomputeVertices() {
|
||||
var scaleX = 1f
|
||||
var scaleY = 1f
|
||||
|
||||
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
|
||||
|
||||
if (cameraRatio > screenRatio) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue