feat(image-pipeline): expand webgl backend step coverage
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -3,9 +3,15 @@ import type {
|
|||||||
BackendStepRequest,
|
BackendStepRequest,
|
||||||
ImagePipelineBackend,
|
ImagePipelineBackend,
|
||||||
} from "@/lib/image-pipeline/backend/backend-types";
|
} 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 type { PipelineStep } from "@/lib/image-pipeline/contracts";
|
||||||
import colorAdjustFragmentShaderSource from "@/lib/image-pipeline/backend/webgl/shaders/color-adjust.frag.glsl?raw";
|
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 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 = `
|
const VERTEX_SHADER_SOURCE = `
|
||||||
attribute vec2 aPosition;
|
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 = {
|
type WebglBackendContext = {
|
||||||
gl: WebGLRenderingContext;
|
gl: WebGLRenderingContext;
|
||||||
curvesProgram: WebGLProgram;
|
curvesProgram: WebGLProgram;
|
||||||
colorAdjustProgram: WebGLProgram;
|
colorAdjustProgram: WebGLProgram;
|
||||||
|
lightAdjustProgram: WebGLProgram;
|
||||||
|
detailAdjustProgram: WebGLProgram;
|
||||||
quadBuffer: WebGLBuffer;
|
quadBuffer: WebGLBuffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SUPPORTED_PREVIEW_STEP_TYPES = new Set<SupportedPreviewStepType>([
|
const SUPPORTED_PREVIEW_STEP_TYPES = new Set<SupportedPreviewStepType>([
|
||||||
"curves",
|
"curves",
|
||||||
"color-adjust",
|
"color-adjust",
|
||||||
|
"light-adjust",
|
||||||
|
"detail-adjust",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function assertSupportedStep(step: PipelineStep): void {
|
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 {
|
function runStepOnGpu(context: WebglBackendContext, request: BackendStepRequest): void {
|
||||||
const { gl } = context;
|
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.useProgram(shaderProgram);
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, context.quadBuffer);
|
gl.bindBuffer(gl.ARRAY_BUFFER, context.quadBuffer);
|
||||||
@@ -239,18 +367,7 @@ function runStepOnGpu(context: WebglBackendContext, request: BackendStepRequest)
|
|||||||
gl.uniform1i(sourceLocation, 0);
|
gl.uniform1i(sourceLocation, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.step.type === "curves") {
|
applyStepUniforms(gl, shaderProgram, request);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gl.viewport(0, 0, request.width, request.height);
|
gl.viewport(0, 0, request.width, request.height);
|
||||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||||
@@ -286,6 +403,8 @@ export function createWebglPreviewBackend(): ImagePipelineBackend {
|
|||||||
gl,
|
gl,
|
||||||
curvesProgram: compileProgram(gl, curvesFragmentShaderSource),
|
curvesProgram: compileProgram(gl, curvesFragmentShaderSource),
|
||||||
colorAdjustProgram: compileProgram(gl, colorAdjustFragmentShaderSource),
|
colorAdjustProgram: compileProgram(gl, colorAdjustFragmentShaderSource),
|
||||||
|
lightAdjustProgram: compileProgram(gl, lightAdjustFragmentShaderSource),
|
||||||
|
detailAdjustProgram: compileProgram(gl, detailAdjustFragmentShaderSource),
|
||||||
quadBuffer: createQuadBuffer(gl),
|
quadBuffer: createQuadBuffer(gl),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,47 @@ describe("cpu vs webgl parity", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps light-adjust-only pipeline within parity tolerance", () => {
|
||||||
|
const pipelines = createParityPipelines();
|
||||||
|
installParityWebglContextMock();
|
||||||
|
|
||||||
|
const metrics = evaluateCpuWebglParity(pipelines.lightAdjustOnly);
|
||||||
|
|
||||||
|
expect(metrics.maxChannelDelta).toBeLessThanOrEqual(parityTolerances.lightAdjustOnly.maxChannelDelta);
|
||||||
|
expect(metrics.histogramSimilarity).toBeGreaterThanOrEqual(
|
||||||
|
parityTolerances.lightAdjustOnly.histogramSimilarity,
|
||||||
|
);
|
||||||
|
expect(metrics.spatialRmse).toBeLessThanOrEqual(parityTolerances.lightAdjustOnly.spatialRmse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps detail-adjust-only pipeline within parity tolerance", () => {
|
||||||
|
const pipelines = createParityPipelines();
|
||||||
|
installParityWebglContextMock();
|
||||||
|
|
||||||
|
const metrics = evaluateCpuWebglParity(pipelines.detailAdjustOnly);
|
||||||
|
|
||||||
|
expect(metrics.maxChannelDelta).toBeLessThanOrEqual(parityTolerances.detailAdjustOnly.maxChannelDelta);
|
||||||
|
expect(metrics.histogramSimilarity).toBeGreaterThanOrEqual(
|
||||||
|
parityTolerances.detailAdjustOnly.histogramSimilarity,
|
||||||
|
);
|
||||||
|
expect(metrics.spatialRmse).toBeLessThanOrEqual(parityTolerances.detailAdjustOnly.spatialRmse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps curves + color-adjust + light-adjust + detail-adjust chain within parity tolerance", () => {
|
||||||
|
const pipelines = createParityPipelines();
|
||||||
|
installParityWebglContextMock();
|
||||||
|
|
||||||
|
const metrics = evaluateCpuWebglParity(pipelines.curvesColorLightDetailChain);
|
||||||
|
|
||||||
|
expect(metrics.maxChannelDelta).toBeLessThanOrEqual(
|
||||||
|
parityTolerances.curvesColorLightDetailChain.maxChannelDelta,
|
||||||
|
);
|
||||||
|
expect(metrics.histogramSimilarity).toBeGreaterThanOrEqual(
|
||||||
|
parityTolerances.curvesColorLightDetailChain.histogramSimilarity,
|
||||||
|
);
|
||||||
|
expect(metrics.spatialRmse).toBeLessThanOrEqual(parityTolerances.curvesColorLightDetailChain.spatialRmse);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps channel-specific curves pressure case within parity tolerance", () => {
|
it("keeps channel-specific curves pressure case within parity tolerance", () => {
|
||||||
const pipelines = createParityPipelines();
|
const pipelines = createParityPipelines();
|
||||||
installParityWebglContextMock();
|
installParityWebglContextMock();
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ type ParityPipelineKey =
|
|||||||
| "curvesOnly"
|
| "curvesOnly"
|
||||||
| "colorAdjustOnly"
|
| "colorAdjustOnly"
|
||||||
| "curvesPlusColorAdjust"
|
| "curvesPlusColorAdjust"
|
||||||
|
| "lightAdjustOnly"
|
||||||
|
| "detailAdjustOnly"
|
||||||
|
| "curvesColorLightDetailChain"
|
||||||
| "curvesChannelPressure"
|
| "curvesChannelPressure"
|
||||||
| "colorAdjustPressure"
|
| "colorAdjustPressure"
|
||||||
| "curvesColorPressureChain";
|
| "curvesColorPressureChain";
|
||||||
@@ -61,6 +64,21 @@ export const parityTolerances: Record<ParityPipelineKey, ParityTolerance> = {
|
|||||||
histogramSimilarity: 0.16,
|
histogramSimilarity: 0.16,
|
||||||
spatialRmse: 43.5,
|
spatialRmse: 43.5,
|
||||||
},
|
},
|
||||||
|
lightAdjustOnly: {
|
||||||
|
maxChannelDelta: 52,
|
||||||
|
histogramSimilarity: 0.5,
|
||||||
|
spatialRmse: 20.0,
|
||||||
|
},
|
||||||
|
detailAdjustOnly: {
|
||||||
|
maxChannelDelta: 2,
|
||||||
|
histogramSimilarity: 0.99,
|
||||||
|
spatialRmse: 1.2,
|
||||||
|
},
|
||||||
|
curvesColorLightDetailChain: {
|
||||||
|
maxChannelDelta: 130,
|
||||||
|
histogramSimilarity: 0.17,
|
||||||
|
spatialRmse: 57.5,
|
||||||
|
},
|
||||||
curvesChannelPressure: {
|
curvesChannelPressure: {
|
||||||
maxChannelDelta: 99,
|
maxChannelDelta: 99,
|
||||||
histogramSimilarity: 0.08,
|
histogramSimilarity: 0.08,
|
||||||
@@ -223,9 +241,59 @@ function createCurvesColorPressureChainSteps(): PipelineStep[] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createLightAdjustStep(): PipelineStep {
|
||||||
|
return {
|
||||||
|
nodeId: "light-adjust-parity",
|
||||||
|
type: "light-adjust",
|
||||||
|
params: {
|
||||||
|
brightness: 14,
|
||||||
|
contrast: 22,
|
||||||
|
exposure: 0.8,
|
||||||
|
highlights: -16,
|
||||||
|
shadows: 24,
|
||||||
|
whites: 8,
|
||||||
|
blacks: -10,
|
||||||
|
vignette: {
|
||||||
|
amount: 0.25,
|
||||||
|
size: 0.72,
|
||||||
|
roundness: 0.85,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDetailAdjustStep(): PipelineStep {
|
||||||
|
return {
|
||||||
|
nodeId: "detail-adjust-parity",
|
||||||
|
type: "detail-adjust",
|
||||||
|
params: {
|
||||||
|
sharpen: {
|
||||||
|
amount: 210,
|
||||||
|
radius: 1.4,
|
||||||
|
threshold: 6,
|
||||||
|
},
|
||||||
|
clarity: 32,
|
||||||
|
denoise: {
|
||||||
|
luminance: 18,
|
||||||
|
color: 14,
|
||||||
|
},
|
||||||
|
grain: {
|
||||||
|
amount: 12,
|
||||||
|
size: 1.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCurvesColorLightDetailChainSteps(): PipelineStep[] {
|
||||||
|
return [createCurvesStep(), createColorAdjustStep(), createLightAdjustStep(), createDetailAdjustStep()];
|
||||||
|
}
|
||||||
|
|
||||||
export function createParityPipelines(): Record<ParityPipelineKey, ParityPipeline> {
|
export function createParityPipelines(): Record<ParityPipelineKey, ParityPipeline> {
|
||||||
const curvesStep = createCurvesStep();
|
const curvesStep = createCurvesStep();
|
||||||
const colorAdjustStep = createColorAdjustStep();
|
const colorAdjustStep = createColorAdjustStep();
|
||||||
|
const lightAdjustStep = createLightAdjustStep();
|
||||||
|
const detailAdjustStep = createDetailAdjustStep();
|
||||||
const curvesChannelPressureStep = createCurvesChannelPressureStep();
|
const curvesChannelPressureStep = createCurvesChannelPressureStep();
|
||||||
const colorAdjustPressureStep = createColorAdjustPressureStep();
|
const colorAdjustPressureStep = createColorAdjustPressureStep();
|
||||||
|
|
||||||
@@ -242,6 +310,18 @@ export function createParityPipelines(): Record<ParityPipelineKey, ParityPipelin
|
|||||||
key: "curvesPlusColorAdjust",
|
key: "curvesPlusColorAdjust",
|
||||||
steps: [curvesStep, colorAdjustStep],
|
steps: [curvesStep, colorAdjustStep],
|
||||||
},
|
},
|
||||||
|
lightAdjustOnly: {
|
||||||
|
key: "lightAdjustOnly",
|
||||||
|
steps: [lightAdjustStep],
|
||||||
|
},
|
||||||
|
detailAdjustOnly: {
|
||||||
|
key: "detailAdjustOnly",
|
||||||
|
steps: [detailAdjustStep],
|
||||||
|
},
|
||||||
|
curvesColorLightDetailChain: {
|
||||||
|
key: "curvesColorLightDetailChain",
|
||||||
|
steps: createCurvesColorLightDetailChainSteps(),
|
||||||
|
},
|
||||||
curvesChannelPressure: {
|
curvesChannelPressure: {
|
||||||
key: "curvesChannelPressure",
|
key: "curvesChannelPressure",
|
||||||
steps: [curvesChannelPressureStep],
|
steps: [curvesChannelPressureStep],
|
||||||
@@ -277,7 +357,13 @@ function clonePixels(pixels: Uint8ClampedArray): Uint8ClampedArray {
|
|||||||
return new Uint8ClampedArray(pixels);
|
return new Uint8ClampedArray(pixels);
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShaderKind = "curves" | "color-adjust" | "vertex" | "unknown";
|
type ShaderKind =
|
||||||
|
| "curves"
|
||||||
|
| "color-adjust"
|
||||||
|
| "light-adjust"
|
||||||
|
| "detail-adjust"
|
||||||
|
| "vertex"
|
||||||
|
| "unknown";
|
||||||
|
|
||||||
type FakeShader = {
|
type FakeShader = {
|
||||||
type: number;
|
type: number;
|
||||||
@@ -297,6 +383,12 @@ type FakeTexture = {
|
|||||||
data: Uint8Array;
|
data: Uint8Array;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FakeTextureImageSource = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
data: Uint8Array;
|
||||||
|
};
|
||||||
|
|
||||||
type FakeFramebuffer = {
|
type FakeFramebuffer = {
|
||||||
attachment: FakeTexture | null;
|
attachment: FakeTexture | null;
|
||||||
};
|
};
|
||||||
@@ -316,6 +408,12 @@ function inferShaderKind(source: string): ShaderKind {
|
|||||||
if (source.includes("uColorShift")) {
|
if (source.includes("uColorShift")) {
|
||||||
return "color-adjust";
|
return "color-adjust";
|
||||||
}
|
}
|
||||||
|
if (source.includes("uExposureFactor")) {
|
||||||
|
return "light-adjust";
|
||||||
|
}
|
||||||
|
if (source.includes("uSharpenBoost")) {
|
||||||
|
return "detail-adjust";
|
||||||
|
}
|
||||||
if (source.includes("aPosition")) {
|
if (source.includes("aPosition")) {
|
||||||
return "vertex";
|
return "vertex";
|
||||||
}
|
}
|
||||||
@@ -364,6 +462,139 @@ function runColorAdjustShader(input: Uint8Array, shift: [number, number, number]
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pseudoNoise(seed: number): number {
|
||||||
|
const x = Math.sin(seed * 12.9898) * 43758.5453;
|
||||||
|
return x - Math.floor(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runLightAdjustShader(
|
||||||
|
input: Uint8Array,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
uniforms: Map<string, number | [number, number, number]>,
|
||||||
|
): Uint8Array {
|
||||||
|
const output = new Uint8Array(input.length);
|
||||||
|
const centerX = width / 2;
|
||||||
|
const centerY = height / 2;
|
||||||
|
|
||||||
|
const exposureFactor = Number(uniforms.get("uExposureFactor") ?? 1);
|
||||||
|
const contrastFactor = Number(uniforms.get("uContrastFactor") ?? 1);
|
||||||
|
const brightnessShift = Number(uniforms.get("uBrightnessShift") ?? 0);
|
||||||
|
const highlights = Number(uniforms.get("uHighlights") ?? 0);
|
||||||
|
const shadows = Number(uniforms.get("uShadows") ?? 0);
|
||||||
|
const whites = Number(uniforms.get("uWhites") ?? 0);
|
||||||
|
const blacks = Number(uniforms.get("uBlacks") ?? 0);
|
||||||
|
const vignetteAmount = Number(uniforms.get("uVignetteAmount") ?? 0);
|
||||||
|
const vignetteSize = Number(uniforms.get("uVignetteSize") ?? 0.5);
|
||||||
|
const vignetteRoundness = Number(uniforms.get("uVignetteRoundness") ?? 1);
|
||||||
|
|
||||||
|
for (let y = 0; y < height; y += 1) {
|
||||||
|
for (let x = 0; x < width; x += 1) {
|
||||||
|
const index = (y * width + x) * 4;
|
||||||
|
|
||||||
|
let red = input[index] ?? 0;
|
||||||
|
let green = input[index + 1] ?? 0;
|
||||||
|
let blue = input[index + 2] ?? 0;
|
||||||
|
|
||||||
|
red *= exposureFactor;
|
||||||
|
green *= exposureFactor;
|
||||||
|
blue *= exposureFactor;
|
||||||
|
|
||||||
|
red = (red - 128) * contrastFactor + 128 + brightnessShift;
|
||||||
|
green = (green - 128) * contrastFactor + 128 + brightnessShift;
|
||||||
|
blue = (blue - 128) * contrastFactor + 128 + brightnessShift;
|
||||||
|
|
||||||
|
const luma = red * 0.2126 + green * 0.7152 + blue * 0.0722;
|
||||||
|
const highlightsBoost = (luma / 255) * highlights * 40;
|
||||||
|
const shadowsBoost = ((255 - luma) / 255) * shadows * 40;
|
||||||
|
const whitesBoost = (luma / 255) * whites * 35;
|
||||||
|
const blacksBoost = ((255 - luma) / 255) * blacks * 35;
|
||||||
|
const totalBoost = highlightsBoost + shadowsBoost + whitesBoost + blacksBoost;
|
||||||
|
|
||||||
|
red += totalBoost;
|
||||||
|
green += totalBoost;
|
||||||
|
blue += totalBoost;
|
||||||
|
|
||||||
|
if (vignetteAmount > 0) {
|
||||||
|
const dx = (x - centerX) / Math.max(1, centerX);
|
||||||
|
const dy = (y - centerY) / Math.max(1, centerY);
|
||||||
|
const radialDistance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
const softEdge = Math.pow(1 - Math.max(0, Math.min(1, radialDistance)), 1 + vignetteRoundness);
|
||||||
|
const strength = 1 - vignetteAmount * (1 - softEdge) * (1.5 - vignetteSize);
|
||||||
|
|
||||||
|
red *= strength;
|
||||||
|
green *= strength;
|
||||||
|
blue *= strength;
|
||||||
|
}
|
||||||
|
|
||||||
|
output[index] = toByte(red / 255);
|
||||||
|
output[index + 1] = toByte(green / 255);
|
||||||
|
output[index + 2] = toByte(blue / 255);
|
||||||
|
output[index + 3] = input[index + 3] ?? 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runDetailAdjustShader(
|
||||||
|
input: Uint8Array,
|
||||||
|
uniforms: Map<string, number | [number, number, number]>,
|
||||||
|
): Uint8Array {
|
||||||
|
const output = new Uint8Array(input.length);
|
||||||
|
|
||||||
|
const sharpenBoost = Number(uniforms.get("uSharpenBoost") ?? 0);
|
||||||
|
const clarityBoost = Number(uniforms.get("uClarityBoost") ?? 0);
|
||||||
|
const denoiseLuma = Number(uniforms.get("uDenoiseLuma") ?? 0);
|
||||||
|
const denoiseColor = Number(uniforms.get("uDenoiseColor") ?? 0);
|
||||||
|
const grainAmount = Number(uniforms.get("uGrainAmount") ?? 0);
|
||||||
|
const grainScale = Math.max(0.5, Number(uniforms.get("uGrainScale") ?? 1));
|
||||||
|
|
||||||
|
for (let index = 0; index < input.length; index += 4) {
|
||||||
|
let red = input[index] ?? 0;
|
||||||
|
let green = input[index + 1] ?? 0;
|
||||||
|
let blue = input[index + 2] ?? 0;
|
||||||
|
|
||||||
|
const luma = red * 0.2126 + green * 0.7152 + blue * 0.0722;
|
||||||
|
|
||||||
|
red = red + (red - luma) * sharpenBoost * 0.6;
|
||||||
|
green = green + (green - luma) * sharpenBoost * 0.6;
|
||||||
|
blue = blue + (blue - luma) * sharpenBoost * 0.6;
|
||||||
|
|
||||||
|
const midtoneFactor = 1 - Math.abs(luma / 255 - 0.5) * 2;
|
||||||
|
const clarityScale = 1 + clarityBoost * midtoneFactor * 0.7;
|
||||||
|
|
||||||
|
red = (red - 128) * clarityScale + 128;
|
||||||
|
green = (green - 128) * clarityScale + 128;
|
||||||
|
blue = (blue - 128) * clarityScale + 128;
|
||||||
|
|
||||||
|
if (denoiseLuma > 0 || denoiseColor > 0) {
|
||||||
|
red = red * (1 - denoiseLuma * 0.2) + luma * denoiseLuma * 0.2;
|
||||||
|
green = green * (1 - denoiseLuma * 0.2) + luma * denoiseLuma * 0.2;
|
||||||
|
blue = blue * (1 - denoiseLuma * 0.2) + luma * denoiseLuma * 0.2;
|
||||||
|
|
||||||
|
const average = (red + green + blue) / 3;
|
||||||
|
red = red * (1 - denoiseColor * 0.2) + average * denoiseColor * 0.2;
|
||||||
|
green = green * (1 - denoiseColor * 0.2) + average * denoiseColor * 0.2;
|
||||||
|
blue = blue * (1 - denoiseColor * 0.2) + average * denoiseColor * 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grainAmount > 0) {
|
||||||
|
const grain = (pseudoNoise((index + 1) / grainScale) - 0.5) * grainAmount * 40;
|
||||||
|
red += grain;
|
||||||
|
green += grain;
|
||||||
|
blue += grain;
|
||||||
|
}
|
||||||
|
|
||||||
|
output[index] = toByte(red / 255);
|
||||||
|
output[index + 1] = toByte(green / 255);
|
||||||
|
output[index + 2] = toByte(blue / 255);
|
||||||
|
output[index + 3] = input[index + 3] ?? 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
function createParityWebglContext(): WebGLRenderingContext {
|
function createParityWebglContext(): WebGLRenderingContext {
|
||||||
const glConstants = {
|
const glConstants = {
|
||||||
VERTEX_SHADER: 0x8b31,
|
VERTEX_SHADER: 0x8b31,
|
||||||
@@ -392,6 +623,9 @@ function createParityWebglContext(): WebGLRenderingContext {
|
|||||||
let currentProgram: FakeProgram | null = null;
|
let currentProgram: FakeProgram | null = null;
|
||||||
let currentTexture: FakeTexture | null = null;
|
let currentTexture: FakeTexture | null = null;
|
||||||
let currentFramebuffer: FakeFramebuffer | null = null;
|
let currentFramebuffer: FakeFramebuffer | null = null;
|
||||||
|
let sourceTexture: FakeTexture | null = null;
|
||||||
|
let drawWidth = 1;
|
||||||
|
let drawHeight = 1;
|
||||||
|
|
||||||
const gl = {
|
const gl = {
|
||||||
...glConstants,
|
...glConstants,
|
||||||
@@ -453,6 +687,9 @@ function createParityWebglContext(): WebGLRenderingContext {
|
|||||||
},
|
},
|
||||||
bindTexture(_target: number, texture: FakeTexture | null) {
|
bindTexture(_target: number, texture: FakeTexture | null) {
|
||||||
currentTexture = texture;
|
currentTexture = texture;
|
||||||
|
if (texture) {
|
||||||
|
sourceTexture = texture;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
texParameteri() {},
|
texParameteri() {},
|
||||||
texImage2D(
|
texImage2D(
|
||||||
@@ -464,7 +701,7 @@ function createParityWebglContext(): WebGLRenderingContext {
|
|||||||
_border: number,
|
_border: number,
|
||||||
_format: number,
|
_format: number,
|
||||||
_type: number,
|
_type: number,
|
||||||
pixels: ArrayBufferView | null,
|
pixels: ArrayBufferView | FakeTextureImageSource | null,
|
||||||
) {
|
) {
|
||||||
if (!currentTexture) {
|
if (!currentTexture) {
|
||||||
return;
|
return;
|
||||||
@@ -473,7 +710,13 @@ function createParityWebglContext(): WebGLRenderingContext {
|
|||||||
if (pixels) {
|
if (pixels) {
|
||||||
currentTexture.width = width;
|
currentTexture.width = width;
|
||||||
currentTexture.height = height;
|
currentTexture.height = height;
|
||||||
currentTexture.data = new Uint8Array(pixels.buffer.slice(0));
|
if ("buffer" in pixels) {
|
||||||
|
const byteOffset = pixels.byteOffset ?? 0;
|
||||||
|
const byteLength = pixels.byteLength ?? pixels.buffer.byteLength;
|
||||||
|
currentTexture.data = new Uint8Array(pixels.buffer.slice(byteOffset, byteOffset + byteLength));
|
||||||
|
} else {
|
||||||
|
currentTexture.data = new Uint8Array(pixels.data);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,15 +763,18 @@ function createParityWebglContext(): WebGLRenderingContext {
|
|||||||
return glConstants.FRAMEBUFFER_COMPLETE;
|
return glConstants.FRAMEBUFFER_COMPLETE;
|
||||||
},
|
},
|
||||||
deleteFramebuffer() {},
|
deleteFramebuffer() {},
|
||||||
viewport() {},
|
viewport(_x: number, _y: number, width: number, height: number) {
|
||||||
|
drawWidth = width;
|
||||||
|
drawHeight = height;
|
||||||
|
},
|
||||||
drawArrays() {
|
drawArrays() {
|
||||||
if (!currentProgram || !currentTexture || !currentFramebuffer?.attachment) {
|
if (!currentProgram || !sourceTexture || !currentFramebuffer?.attachment) {
|
||||||
throw new Error("Parity WebGL mock is missing required render state.");
|
throw new Error("Parity WebGL mock is missing required render state.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentProgram.kind === "curves") {
|
if (currentProgram.kind === "curves") {
|
||||||
const gamma = Number(currentProgram.uniforms.get("uGamma") ?? 1);
|
const gamma = Number(currentProgram.uniforms.get("uGamma") ?? 1);
|
||||||
currentFramebuffer.attachment.data = runCurvesShader(currentTexture.data, gamma);
|
currentFramebuffer.attachment.data = runCurvesShader(sourceTexture.data, gamma);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,7 +783,22 @@ function createParityWebglContext(): WebGLRenderingContext {
|
|||||||
const shift: [number, number, number] = Array.isArray(colorShift)
|
const shift: [number, number, number] = Array.isArray(colorShift)
|
||||||
? [colorShift[0] ?? 0, colorShift[1] ?? 0, colorShift[2] ?? 0]
|
? [colorShift[0] ?? 0, colorShift[1] ?? 0, colorShift[2] ?? 0]
|
||||||
: [0, 0, 0];
|
: [0, 0, 0];
|
||||||
currentFramebuffer.attachment.data = runColorAdjustShader(currentTexture.data, shift);
|
currentFramebuffer.attachment.data = runColorAdjustShader(sourceTexture.data, shift);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentProgram.kind === "light-adjust") {
|
||||||
|
currentFramebuffer.attachment.data = runLightAdjustShader(
|
||||||
|
sourceTexture.data,
|
||||||
|
drawWidth,
|
||||||
|
drawHeight,
|
||||||
|
currentProgram.uniforms,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentProgram.kind === "detail-adjust") {
|
||||||
|
currentFramebuffer.attachment.data = runDetailAdjustShader(sourceTexture.data, currentProgram.uniforms);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function createColorAdjustStep(): PipelineStep {
|
|||||||
function createUnsupportedStep(): PipelineStep {
|
function createUnsupportedStep(): PipelineStep {
|
||||||
return {
|
return {
|
||||||
nodeId: "light-1",
|
nodeId: "light-1",
|
||||||
type: "light-adjust",
|
type: "unsupported-adjust" as PipelineStep["type"],
|
||||||
params: {
|
params: {
|
||||||
exposure: 0,
|
exposure: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user