feat(image-pipeline): expand webgl backend step coverage

This commit is contained in:
Matthias
2026-04-04 22:36:54 +02:00
parent c4ca4b001b
commit 65e96cbdf1
6 changed files with 540 additions and 22 deletions

View File

@@ -0,0 +1,45 @@
precision mediump float;
varying vec2 vUv;
uniform sampler2D uSource;
uniform float uSharpenBoost;
uniform float uClarityBoost;
uniform float uDenoiseLuma;
uniform float uDenoiseColor;
uniform float uGrainAmount;
uniform float uGrainScale;
float pseudoNoise(float seed) {
float x = sin(seed * 12.9898) * 43758.5453;
return fract(x);
}
void main() {
vec4 color = texture2D(uSource, vUv);
vec3 rgb = color.rgb * 255.0;
float luma = dot(rgb, vec3(0.2126, 0.7152, 0.0722));
rgb.r = rgb.r + (rgb.r - luma) * uSharpenBoost * 0.6;
rgb.g = rgb.g + (rgb.g - luma) * uSharpenBoost * 0.6;
rgb.b = rgb.b + (rgb.b - luma) * uSharpenBoost * 0.6;
float midtoneFactor = 1.0 - abs(luma / 255.0 - 0.5) * 2.0;
float clarityScale = 1.0 + uClarityBoost * midtoneFactor * 0.7;
rgb = (rgb - 128.0) * clarityScale + 128.0;
if (uDenoiseLuma > 0.0 || uDenoiseColor > 0.0) {
rgb = rgb * (1.0 - uDenoiseLuma * 0.2) + vec3(luma) * uDenoiseLuma * 0.2;
float average = (rgb.r + rgb.g + rgb.b) / 3.0;
rgb = rgb * (1.0 - uDenoiseColor * 0.2) + vec3(average) * uDenoiseColor * 0.2;
}
if (uGrainAmount > 0.0) {
float grainSeed = (gl_FragCoord.y * 4096.0 + gl_FragCoord.x) / max(0.5, uGrainScale);
float grain = (pseudoNoise(grainSeed) - 0.5) * uGrainAmount * 40.0;
rgb += vec3(grain);
}
gl_FragColor = vec4(clamp(rgb / 255.0, 0.0, 1.0), color.a);
}

View File

@@ -0,0 +1,52 @@
precision mediump float;
varying vec2 vUv;
uniform sampler2D uSource;
uniform float uExposureFactor;
uniform float uContrastFactor;
uniform float uBrightnessShift;
uniform float uHighlights;
uniform float uShadows;
uniform float uWhites;
uniform float uBlacks;
uniform float uVignetteAmount;
uniform float uVignetteSize;
uniform float uVignetteRoundness;
float toByte(float value) {
return clamp(floor(value + 0.5), 0.0, 255.0);
}
void main() {
vec4 color = texture2D(uSource, vUv);
vec3 rgb = color.rgb * 255.0;
rgb *= uExposureFactor;
rgb = (rgb - 128.0) * uContrastFactor + 128.0 + uBrightnessShift;
float luma = dot(rgb, vec3(0.2126, 0.7152, 0.0722));
float highlightsBoost = (luma / 255.0) * uHighlights * 40.0;
float shadowsBoost = ((255.0 - luma) / 255.0) * uShadows * 40.0;
float whitesBoost = (luma / 255.0) * uWhites * 35.0;
float blacksBoost = ((255.0 - luma) / 255.0) * uBlacks * 35.0;
float totalBoost = highlightsBoost + shadowsBoost + whitesBoost + blacksBoost;
rgb = vec3(
toByte(rgb.r + totalBoost),
toByte(rgb.g + totalBoost),
toByte(rgb.b + totalBoost)
);
if (uVignetteAmount > 0.0) {
vec2 centeredUv = (vUv - vec2(0.5)) / vec2(0.5);
float radialDistance = length(centeredUv);
float softEdge = pow(1.0 - clamp(radialDistance, 0.0, 1.0), 1.0 + uVignetteRoundness);
float strength = 1.0 - uVignetteAmount * (1.0 - softEdge) * (1.5 - uVignetteSize);
rgb = vec3(
toByte(rgb.r * strength),
toByte(rgb.g * strength),
toByte(rgb.b * strength)
);
}
gl_FragColor = vec4(clamp(rgb / 255.0, 0.0, 1.0), color.a);
}

View File

@@ -3,9 +3,15 @@ import type {
BackendStepRequest,
ImagePipelineBackend,
} from "@/lib/image-pipeline/backend/backend-types";
import {
normalizeDetailAdjustData,
normalizeLightAdjustData,
} from "@/lib/image-pipeline/adjustment-types";
import type { PipelineStep } from "@/lib/image-pipeline/contracts";
import colorAdjustFragmentShaderSource from "@/lib/image-pipeline/backend/webgl/shaders/color-adjust.frag.glsl?raw";
import curvesFragmentShaderSource from "@/lib/image-pipeline/backend/webgl/shaders/curves.frag.glsl?raw";
import detailAdjustFragmentShaderSource from "@/lib/image-pipeline/backend/webgl/shaders/detail-adjust.frag.glsl?raw";
import lightAdjustFragmentShaderSource from "@/lib/image-pipeline/backend/webgl/shaders/light-adjust.frag.glsl?raw";
const VERTEX_SHADER_SOURCE = `
attribute vec2 aPosition;
@@ -17,18 +23,22 @@ void main() {
}
`;
type SupportedPreviewStepType = "curves" | "color-adjust";
type SupportedPreviewStepType = "curves" | "color-adjust" | "light-adjust" | "detail-adjust";
type WebglBackendContext = {
gl: WebGLRenderingContext;
curvesProgram: WebGLProgram;
colorAdjustProgram: WebGLProgram;
lightAdjustProgram: WebGLProgram;
detailAdjustProgram: WebGLProgram;
quadBuffer: WebGLBuffer;
};
const SUPPORTED_PREVIEW_STEP_TYPES = new Set<SupportedPreviewStepType>([
"curves",
"color-adjust",
"light-adjust",
"detail-adjust",
]);
function assertSupportedStep(step: PipelineStep): void {
@@ -156,9 +166,127 @@ function mapColorShift(step: PipelineStep): [number, number, number] {
];
}
function applyStepUniforms(
gl: WebGLRenderingContext,
shaderProgram: WebGLProgram,
request: BackendStepRequest,
): void {
if (request.step.type === "curves") {
const gammaLocation = gl.getUniformLocation(shaderProgram, "uGamma");
if (gammaLocation) {
gl.uniform1f(gammaLocation, mapCurvesGamma(request.step));
}
return;
}
if (request.step.type === "color-adjust") {
const colorShiftLocation = gl.getUniformLocation(shaderProgram, "uColorShift");
if (colorShiftLocation) {
const [r, g, b] = mapColorShift(request.step);
gl.uniform3f(colorShiftLocation, r, g, b);
}
return;
}
if (request.step.type === "light-adjust") {
const light = normalizeLightAdjustData(request.step.params);
const exposureFactorLocation = gl.getUniformLocation(shaderProgram, "uExposureFactor");
if (exposureFactorLocation) {
gl.uniform1f(exposureFactorLocation, Math.pow(2, light.exposure / 2));
}
const contrastFactorLocation = gl.getUniformLocation(shaderProgram, "uContrastFactor");
if (contrastFactorLocation) {
gl.uniform1f(contrastFactorLocation, 1 + light.contrast / 100);
}
const brightnessShiftLocation = gl.getUniformLocation(shaderProgram, "uBrightnessShift");
if (brightnessShiftLocation) {
gl.uniform1f(brightnessShiftLocation, light.brightness * 1.8);
}
const highlightsLocation = gl.getUniformLocation(shaderProgram, "uHighlights");
if (highlightsLocation) {
gl.uniform1f(highlightsLocation, light.highlights / 100);
}
const shadowsLocation = gl.getUniformLocation(shaderProgram, "uShadows");
if (shadowsLocation) {
gl.uniform1f(shadowsLocation, light.shadows / 100);
}
const whitesLocation = gl.getUniformLocation(shaderProgram, "uWhites");
if (whitesLocation) {
gl.uniform1f(whitesLocation, light.whites / 100);
}
const blacksLocation = gl.getUniformLocation(shaderProgram, "uBlacks");
if (blacksLocation) {
gl.uniform1f(blacksLocation, light.blacks / 100);
}
const vignetteAmountLocation = gl.getUniformLocation(shaderProgram, "uVignetteAmount");
if (vignetteAmountLocation) {
gl.uniform1f(vignetteAmountLocation, light.vignette.amount);
}
const vignetteSizeLocation = gl.getUniformLocation(shaderProgram, "uVignetteSize");
if (vignetteSizeLocation) {
gl.uniform1f(vignetteSizeLocation, light.vignette.size);
}
const vignetteRoundnessLocation = gl.getUniformLocation(shaderProgram, "uVignetteRoundness");
if (vignetteRoundnessLocation) {
gl.uniform1f(vignetteRoundnessLocation, light.vignette.roundness);
}
return;
}
if (request.step.type === "detail-adjust") {
const detail = normalizeDetailAdjustData(request.step.params);
const sharpenBoostLocation = gl.getUniformLocation(shaderProgram, "uSharpenBoost");
if (sharpenBoostLocation) {
gl.uniform1f(sharpenBoostLocation, detail.sharpen.amount / 500);
}
const clarityBoostLocation = gl.getUniformLocation(shaderProgram, "uClarityBoost");
if (clarityBoostLocation) {
gl.uniform1f(clarityBoostLocation, detail.clarity / 100);
}
const denoiseLumaLocation = gl.getUniformLocation(shaderProgram, "uDenoiseLuma");
if (denoiseLumaLocation) {
gl.uniform1f(denoiseLumaLocation, detail.denoise.luminance / 100);
}
const denoiseColorLocation = gl.getUniformLocation(shaderProgram, "uDenoiseColor");
if (denoiseColorLocation) {
gl.uniform1f(denoiseColorLocation, detail.denoise.color / 100);
}
const grainAmountLocation = gl.getUniformLocation(shaderProgram, "uGrainAmount");
if (grainAmountLocation) {
gl.uniform1f(grainAmountLocation, detail.grain.amount / 100);
}
const grainScaleLocation = gl.getUniformLocation(shaderProgram, "uGrainScale");
if (grainScaleLocation) {
gl.uniform1f(grainScaleLocation, Math.max(0.5, detail.grain.size));
}
}
}
function runStepOnGpu(context: WebglBackendContext, request: BackendStepRequest): void {
const { gl } = context;
const shaderProgram = request.step.type === "curves" ? context.curvesProgram : context.colorAdjustProgram;
const shaderProgram =
request.step.type === "curves"
? context.curvesProgram
: request.step.type === "color-adjust"
? context.colorAdjustProgram
: request.step.type === "light-adjust"
? context.lightAdjustProgram
: context.detailAdjustProgram;
gl.useProgram(shaderProgram);
gl.bindBuffer(gl.ARRAY_BUFFER, context.quadBuffer);
@@ -239,18 +367,7 @@ function runStepOnGpu(context: WebglBackendContext, request: BackendStepRequest)
gl.uniform1i(sourceLocation, 0);
}
if (request.step.type === "curves") {
const gammaLocation = gl.getUniformLocation(shaderProgram, "uGamma");
if (gammaLocation) {
gl.uniform1f(gammaLocation, mapCurvesGamma(request.step));
}
} else {
const colorShiftLocation = gl.getUniformLocation(shaderProgram, "uColorShift");
if (colorShiftLocation) {
const [r, g, b] = mapColorShift(request.step);
gl.uniform3f(colorShiftLocation, r, g, b);
}
}
applyStepUniforms(gl, shaderProgram, request);
gl.viewport(0, 0, request.width, request.height);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
@@ -286,6 +403,8 @@ export function createWebglPreviewBackend(): ImagePipelineBackend {
gl,
curvesProgram: compileProgram(gl, curvesFragmentShaderSource),
colorAdjustProgram: compileProgram(gl, colorAdjustFragmentShaderSource),
lightAdjustProgram: compileProgram(gl, lightAdjustFragmentShaderSource),
detailAdjustProgram: compileProgram(gl, detailAdjustFragmentShaderSource),
quadBuffer: createQuadBuffer(gl),
};