Implement two-pass separable Gaussian blur for camera preview
Replace the single-pass 9-tap directional blur with a three-pass pipeline: passthrough (camera→FBO), horizontal blur (FBO-A→FBO-B), vertical blur (FBO-B→screen). This produces a true 2D Gaussian with a 13-tap kernel per pass, eliminating the visible banding/streaking of the old approach. Key changes: - TiltShiftRenderer: FBO ping-pong with two color textures, separate fullscreen quad for blur passes (no crop-to-fill), drawQuad helper - TiltShiftShader: manages two programs (passthrough + blur), blur program uses raw screen-space angle (no camera rotation adjustment) - tiltshift_fragment.glsl: rewritten for sampler2D in screen space, aspect correction on X axis (height-normalized), uBlurDirection uniform for H/V selection, wider falloff (3x multiplier) - New tiltshift_passthrough_fragment.glsl for camera→FBO copy - TiltShiftOverlay: shrink PINCH_SIZE zone (1.3x, was 2.0x) so pinch-to-zoom is reachable over more of the screen - CameraManager: optimistic zoom update fixes pinch-to-zoom stalling (stale zoomRatio base prevented delta accumulation) Bump version to 1.1.3. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aab1ff38a4
commit
f3baa723be
7 changed files with 397 additions and 298 deletions
|
|
@ -1,71 +1,58 @@
|
|||
#extension GL_OES_EGL_image_external : require
|
||||
|
||||
// Fragment shader for tilt-shift effect
|
||||
// Supports both linear and radial blur modes
|
||||
// Fragment shader for tilt-shift blur pass (two-pass separable Gaussian)
|
||||
// Reads from a sampler2D (FBO texture already in screen orientation).
|
||||
// Used twice: once for horizontal blur, once for vertical blur.
|
||||
|
||||
precision mediump float;
|
||||
|
||||
// Camera texture (external texture for camera preview)
|
||||
uniform samplerExternalOES uTexture;
|
||||
uniform sampler2D uTexture;
|
||||
|
||||
// Effect parameters
|
||||
uniform int uMode; // 0 = linear, 1 = radial
|
||||
uniform int uIsFrontCamera; // 0 = back camera, 1 = front camera
|
||||
uniform float uAngle; // Rotation angle in radians
|
||||
uniform float uPositionX; // Horizontal center of focus (0-1)
|
||||
uniform float uPositionY; // Vertical center of focus (0-1)
|
||||
uniform float uPositionX; // Horizontal center of focus (0-1, screen space)
|
||||
uniform float uPositionY; // Vertical center of focus (0-1, screen space, 0 = top)
|
||||
uniform float uSize; // Size of in-focus region (0-1)
|
||||
uniform float uBlurAmount; // Maximum blur intensity (0-1)
|
||||
uniform float uFalloff; // Transition sharpness (0-1, higher = more gradual)
|
||||
uniform float uAspectRatio; // Ellipse aspect ratio for radial mode
|
||||
uniform vec2 uResolution; // Texture resolution for proper sampling
|
||||
uniform vec2 uResolution; // Surface resolution for proper sampling
|
||||
|
||||
// Precomputed trig for the adjusted angle (avoids per-fragment cos/sin calls)
|
||||
// Precomputed trig for the raw screen-space angle
|
||||
uniform float uCosAngle;
|
||||
uniform float uSinAngle;
|
||||
|
||||
// Blur direction: (1,0) for horizontal pass, (0,1) for vertical pass
|
||||
uniform vec2 uBlurDirection;
|
||||
|
||||
varying vec2 vTexCoord;
|
||||
|
||||
// Calculate signed distance from the focus region for LINEAR mode
|
||||
float linearFocusDistance(vec2 uv) {
|
||||
// Center point of the focus region
|
||||
// Transform from screen coordinates to texture coordinates
|
||||
// Back camera: Screen (x,y) -> Texture (y, 1-x)
|
||||
// Front camera: Screen (x,y) -> Texture (1-y, 1-x) (additional X flip for mirror)
|
||||
vec2 center;
|
||||
if (uIsFrontCamera == 1) {
|
||||
center = vec2(1.0 - uPositionY, 1.0 - uPositionX);
|
||||
} else {
|
||||
center = vec2(uPositionY, 1.0 - uPositionX);
|
||||
}
|
||||
vec2 offset = uv - center;
|
||||
// Calculate distance from the focus region for LINEAR mode
|
||||
// Works in screen space: X right (0-1), Y down (0-1)
|
||||
// Distances are normalized to the Y axis (height) to match the overlay,
|
||||
// which defines focus size as a fraction of screen height.
|
||||
float linearFocusDistance(vec2 screenPos) {
|
||||
vec2 center = vec2(uPositionX, uPositionY);
|
||||
vec2 offset = screenPos - center;
|
||||
|
||||
// Correct for screen aspect ratio to make coordinate space square
|
||||
// Scale X into the same physical units as Y (height-normalized)
|
||||
float screenAspect = uResolution.x / uResolution.y;
|
||||
offset.y *= screenAspect;
|
||||
offset.x *= screenAspect;
|
||||
|
||||
// Use precomputed cos/sin for the adjusted angle
|
||||
// Perpendicular distance to the rotated focus line
|
||||
float rotatedY = -offset.x * uSinAngle + offset.y * uCosAngle;
|
||||
|
||||
return abs(rotatedY);
|
||||
}
|
||||
|
||||
// Calculate signed distance from the focus region for RADIAL mode
|
||||
float radialFocusDistance(vec2 uv) {
|
||||
// Center point of the focus region
|
||||
vec2 center;
|
||||
if (uIsFrontCamera == 1) {
|
||||
center = vec2(1.0 - uPositionY, 1.0 - uPositionX);
|
||||
} else {
|
||||
center = vec2(uPositionY, 1.0 - uPositionX);
|
||||
}
|
||||
vec2 offset = uv - center;
|
||||
// Calculate distance from the focus region for RADIAL mode
|
||||
float radialFocusDistance(vec2 screenPos) {
|
||||
vec2 center = vec2(uPositionX, uPositionY);
|
||||
vec2 offset = screenPos - center;
|
||||
|
||||
// Correct for screen aspect ratio
|
||||
// Scale X into the same physical units as Y (height-normalized)
|
||||
float screenAspect = uResolution.x / uResolution.y;
|
||||
offset.y *= screenAspect;
|
||||
offset.x *= screenAspect;
|
||||
|
||||
// Use precomputed cos/sin for rotation
|
||||
// Rotate offset
|
||||
vec2 rotated = vec2(
|
||||
offset.x * uCosAngle - offset.y * uSinAngle,
|
||||
offset.x * uSinAngle + offset.y * uCosAngle
|
||||
|
|
@ -74,83 +61,59 @@ float radialFocusDistance(vec2 uv) {
|
|||
// Apply ellipse aspect ratio
|
||||
rotated.x /= uAspectRatio;
|
||||
|
||||
// Distance from center (elliptical)
|
||||
return length(rotated);
|
||||
}
|
||||
|
||||
// Calculate blur factor based on distance from focus
|
||||
float blurFactor(float dist) {
|
||||
float halfSize = uSize * 0.5;
|
||||
// Falloff range scales with the falloff parameter
|
||||
float transitionSize = halfSize * uFalloff;
|
||||
float transitionSize = halfSize * uFalloff * 3.0;
|
||||
|
||||
if (dist < halfSize) {
|
||||
return 0.0; // In focus region
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Smooth falloff using smoothstep
|
||||
float normalizedDist = (dist - halfSize) / max(transitionSize, 0.001);
|
||||
return smoothstep(0.0, 1.0, normalizedDist) * uBlurAmount;
|
||||
}
|
||||
|
||||
// Sample with Gaussian blur (9-tap, sigma ~= 2.0, unrolled for GLSL ES 1.00 compatibility)
|
||||
vec4 sampleBlurred(vec2 uv, float blur) {
|
||||
if (blur < 0.01) {
|
||||
return texture2D(uTexture, uv);
|
||||
}
|
||||
|
||||
vec2 texelSize = 1.0 / uResolution;
|
||||
|
||||
// For radial mode, blur in radial direction from center
|
||||
// For linear mode, blur perpendicular to focus line
|
||||
vec2 blurDir;
|
||||
if (uMode == 1) {
|
||||
// Radial: blur away from center
|
||||
vec2 center;
|
||||
if (uIsFrontCamera == 1) {
|
||||
center = vec2(1.0 - uPositionY, 1.0 - uPositionX);
|
||||
} else {
|
||||
center = vec2(uPositionY, 1.0 - uPositionX);
|
||||
}
|
||||
vec2 toCenter = uv - center;
|
||||
float len = length(toCenter);
|
||||
if (len > 0.001) {
|
||||
blurDir = toCenter / len;
|
||||
} else {
|
||||
blurDir = vec2(1.0, 0.0);
|
||||
}
|
||||
} else {
|
||||
// Linear: blur perpendicular to focus line using precomputed trig
|
||||
blurDir = vec2(uCosAngle, uSinAngle);
|
||||
}
|
||||
|
||||
// Scale blur radius by blur amount
|
||||
float radius = blur * 20.0;
|
||||
vec2 step = blurDir * texelSize * radius;
|
||||
|
||||
// Unrolled 9-tap Gaussian blur (avoids integer-branched weight lookup)
|
||||
vec4 color = vec4(0.0);
|
||||
color += texture2D(uTexture, uv + step * -4.0) * 0.0162;
|
||||
color += texture2D(uTexture, uv + step * -3.0) * 0.0540;
|
||||
color += texture2D(uTexture, uv + step * -2.0) * 0.1216;
|
||||
color += texture2D(uTexture, uv + step * -1.0) * 0.1933;
|
||||
color += texture2D(uTexture, uv) * 0.2258;
|
||||
color += texture2D(uTexture, uv + step * 1.0) * 0.1933;
|
||||
color += texture2D(uTexture, uv + step * 2.0) * 0.1216;
|
||||
color += texture2D(uTexture, uv + step * 3.0) * 0.0540;
|
||||
color += texture2D(uTexture, uv + step * 4.0) * 0.0162;
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Convert FBO texture coords to screen space (flip Y: GL bottom-up → screen top-down)
|
||||
vec2 screenPos = vec2(vTexCoord.x, 1.0 - vTexCoord.y);
|
||||
|
||||
float dist;
|
||||
if (uMode == 1) {
|
||||
dist = radialFocusDistance(vTexCoord);
|
||||
dist = radialFocusDistance(screenPos);
|
||||
} else {
|
||||
dist = linearFocusDistance(vTexCoord);
|
||||
dist = linearFocusDistance(screenPos);
|
||||
}
|
||||
float blur = blurFactor(dist);
|
||||
|
||||
gl_FragColor = sampleBlurred(vTexCoord, blur);
|
||||
if (blur < 0.01) {
|
||||
gl_FragColor = texture2D(uTexture, vTexCoord);
|
||||
return;
|
||||
}
|
||||
|
||||
// 13-tap separable Gaussian (sigma ~= 2.5)
|
||||
// Each pass blurs in one direction; combined gives a full 2D Gaussian.
|
||||
vec2 texelSize = 1.0 / uResolution;
|
||||
float radius = blur * 20.0;
|
||||
vec2 step = uBlurDirection * texelSize * radius;
|
||||
|
||||
vec4 color = vec4(0.0);
|
||||
color += texture2D(uTexture, vTexCoord + step * -6.0) * 0.0090;
|
||||
color += texture2D(uTexture, vTexCoord + step * -5.0) * 0.0218;
|
||||
color += texture2D(uTexture, vTexCoord + step * -4.0) * 0.0448;
|
||||
color += texture2D(uTexture, vTexCoord + step * -3.0) * 0.0784;
|
||||
color += texture2D(uTexture, vTexCoord + step * -2.0) * 0.1169;
|
||||
color += texture2D(uTexture, vTexCoord + step * -1.0) * 0.1486;
|
||||
color += texture2D(uTexture, vTexCoord) * 0.1610;
|
||||
color += texture2D(uTexture, vTexCoord + step * 1.0) * 0.1486;
|
||||
color += texture2D(uTexture, vTexCoord + step * 2.0) * 0.1169;
|
||||
color += texture2D(uTexture, vTexCoord + step * 3.0) * 0.0784;
|
||||
color += texture2D(uTexture, vTexCoord + step * 4.0) * 0.0448;
|
||||
color += texture2D(uTexture, vTexCoord + step * 5.0) * 0.0218;
|
||||
color += texture2D(uTexture, vTexCoord + step * 6.0) * 0.0090;
|
||||
|
||||
gl_FragColor = color;
|
||||
}
|
||||
|
|
|
|||
15
app/src/main/res/raw/tiltshift_passthrough_fragment.glsl
Normal file
15
app/src/main/res/raw/tiltshift_passthrough_fragment.glsl
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#extension GL_OES_EGL_image_external : require
|
||||
|
||||
// Passthrough fragment shader: copies camera texture to FBO
|
||||
// This separates the camera coordinate transform (handled by vertex/texcoord setup)
|
||||
// from the blur passes, which then work entirely in screen space.
|
||||
|
||||
precision mediump float;
|
||||
|
||||
uniform samplerExternalOES uTexture;
|
||||
|
||||
varying vec2 vTexCoord;
|
||||
|
||||
void main() {
|
||||
gl_FragColor = texture2D(uTexture, vTexCoord);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue