Use the LUT to gamma correct text, and fix stem darkening math

This commit is contained in:
Patrick Walton 2017-11-07 17:24:19 -08:00
parent 82fd214a76
commit e5b76726d9
12 changed files with 180 additions and 67 deletions

View File

@ -70,6 +70,10 @@
{{>partials/switch.html id="pf-subpixel-aa"
title="Subpixel AA"}}
</div>
<div class="form-group row justify-content-between">
{{>partials/switch.html id="pf-gamma-correction"
title="Gamma Correction"}}
</div>
<div class="form-group row justify-content-between">
{{>partials/switch.html id="pf-stem-darkening"
title="Stem Darkening"}}

View File

@ -22,6 +22,8 @@ export type DirectRenderingMode = 'none' | 'color' | 'color-depth';
export type SubpixelAAType = 'none' | 'medium';
export type GammaCorrectionMode = 'off' | 'on';
export type StemDarkeningMode = 'none' | 'dark';
export abstract class AntialiasingStrategy {

View File

@ -8,7 +8,8 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.
import {AntialiasingStrategyName, StemDarkeningMode, SubpixelAAType} from "./aa-strategy";
import {AntialiasingStrategyName, GammaCorrectionMode, StemDarkeningMode} from "./aa-strategy";
import {SubpixelAAType} from "./aa-strategy";
import {FilePickerView} from "./file-picker";
import {ShaderLoader, ShaderMap, ShaderProgramSource} from './shader-loader';
import {expectNotNull, unwrapNull, unwrapUndef} from './utils';
@ -16,6 +17,52 @@ import {DemoView, Timings, TIMINGS} from "./view";
const GAMMA_LUT_URI: string = "/textures/gamma-lut.png";
const SWITCHES: SwitchMap = {
gammaCorrection: {
id: 'pf-gamma-correction',
offValue: 'off',
onValue: 'on',
radioButtonName: 'gammaCorrectionRadioButton',
},
stemDarkening: {
id: 'pf-stem-darkening',
offValue: 'none',
onValue: 'dark',
radioButtonName: 'stemDarkeningRadioButton',
},
subpixelAA: {
id: 'pf-subpixel-aa',
offValue: 'none',
onValue: 'medium',
radioButtonName: 'subpixelAARadioButton',
},
};
interface SwitchDescriptor {
id: string;
radioButtonName: keyof Switches;
onValue: string;
offValue: string;
}
interface SwitchMap {
gammaCorrection: SwitchDescriptor;
stemDarkening: SwitchDescriptor;
subpixelAA: SwitchDescriptor;
}
export interface AAOptions {
gammaCorrection: GammaCorrectionMode;
stemDarkening: StemDarkeningMode;
subpixelAA: SubpixelAAType;
}
interface Switches {
subpixelAARadioButton: HTMLInputElement | null;
gammaCorrectionRadioButton: HTMLInputElement | null;
stemDarkeningRadioButton: HTMLInputElement | null;
}
export abstract class AppController {
protected canvas: HTMLCanvasElement;
@ -47,9 +94,14 @@ export abstract class AppController {
protected abstract get defaultFile(): string;
}
export abstract class DemoAppController<View extends DemoView> extends AppController {
export abstract class DemoAppController<View extends DemoView> extends AppController
implements Switches {
view: Promise<View>;
subpixelAARadioButton: HTMLInputElement | null;
gammaCorrectionRadioButton: HTMLInputElement | null;
stemDarkeningRadioButton: HTMLInputElement | null;
protected abstract readonly builtinFileURI: string;
protected filePickerView: FilePickerView | null;
@ -59,8 +111,6 @@ export abstract class DemoAppController<View extends DemoView> extends AppContro
protected gammaLUT: HTMLImageElement;
private aaLevelSelect: HTMLSelectElement | null;
private subpixelAARadioButton: HTMLInputElement | null;
private stemDarkeningRadioButton: HTMLInputElement | null;
private fpsLabel: HTMLElement | null;
constructor() {
@ -169,22 +219,15 @@ export abstract class DemoAppController<View extends DemoView> extends AppContro
// click listener that Bootstrap sets up until the event bubbles up to the document. This
// click listener is what toggles the `checked` attribute, so we have to wait until it
// fires before updating the antialiasing settings.
this.subpixelAARadioButton =
document.getElementById('pf-subpixel-aa-select-on') as HTMLInputElement | null;
const subpixelAAButtons =
document.getElementById('pf-subpixel-aa-buttons') as HTMLElement | null;
if (subpixelAAButtons != null) {
subpixelAAButtons.addEventListener('click', () => {
window.setTimeout(() => this.updateAALevel(), 0);
}, false);
}
this.stemDarkeningRadioButton =
document.getElementById('pf-stem-darkening-select-on') as HTMLInputElement | null;
const stemDarkeningButtons =
document.getElementById('pf-stem-darkening-buttons') as HTMLElement | null;
if (stemDarkeningButtons != null) {
stemDarkeningButtons.addEventListener('click', () => {
for (const switchName of Object.keys(SWITCHES) as Array<keyof SwitchMap>) {
const radioButtonName = SWITCHES[switchName].radioButtonName;
const switchID = SWITCHES[switchName].id;
this[radioButtonName] = document.getElementById(`${switchID}-select-on`) as
HTMLInputElement | null;
const buttons = document.getElementById(`${switchID}-buttons`) as HTMLElement | null;
if (buttons == null)
continue;
buttons.addEventListener('click', () => {
window.setTimeout(() => this.updateAALevel(), 0);
}, false);
}
@ -242,20 +285,19 @@ export abstract class DemoAppController<View extends DemoView> extends AppContro
aaLevel = 0;
}
let subpixelAA: SubpixelAAType;
if (this.subpixelAARadioButton != null && this.subpixelAARadioButton.checked)
subpixelAA = 'medium';
else
subpixelAA = 'none';
let stemDarkening: StemDarkeningMode;
if (this.stemDarkeningRadioButton != null && this.stemDarkeningRadioButton.checked)
stemDarkening = 'dark';
else
stemDarkening = 'none';
const aaOptions: Partial<AAOptions> = {};
for (const switchName of Object.keys(SWITCHES) as Array<keyof SwitchMap>) {
const switchDescriptor = SWITCHES[switchName];
const radioButtonName = switchDescriptor.radioButtonName;
const radioButton = this[radioButtonName];
if (radioButton != null && radioButton.checked)
aaOptions[switchName] = switchDescriptor.onValue as any;
else
aaOptions[switchName] = switchDescriptor.offValue as any;
}
this.view.then(view => {
view.setAntialiasingOptions(aaType, aaLevel, subpixelAA, stemDarkening);
view.setAntialiasingOptions(aaType, aaLevel, aaOptions as AAOptions);
});
}

View File

@ -11,8 +11,9 @@
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from './aa-strategy';
import {StemDarkeningMode, SubpixelAAType} from './aa-strategy';
import {AntialiasingStrategy, AntialiasingStrategyName, GammaCorrectionMode} from './aa-strategy';
import {NoAAStrategy, StemDarkeningMode, SubpixelAAType} from './aa-strategy';
import {AAOptions} from './app-controller';
import PathfinderBufferTexture from "./buffer-texture";
import {UniformMap} from './gl-utils';
import {PathfinderMeshBuffers, PathfinderMeshData} from "./meshes";
@ -42,8 +43,8 @@ export abstract class Renderer {
return glmatrix.vec2.create();
}
get bgColor(): glmatrix.vec4 | null {
return null;
get bgColor(): glmatrix.vec4 {
return glmatrix.vec4.clone([1.0, 1.0, 1.0, 1.0]);
}
get fgColor(): glmatrix.vec4 | null {
@ -66,6 +67,8 @@ export abstract class Renderer {
protected lastTimings: Timings;
protected pathColorsBufferTextures: PathfinderBufferTexture[];
protected gammaCorrectionMode: GammaCorrectionMode;
protected get pathIDsAreInstanced(): boolean {
return false;
}
@ -87,6 +90,8 @@ export abstract class Renderer {
this.lastTimings = { rendering: 0, compositing: 0 };
this.gammaCorrectionMode = 'on';
this.pathTransformBufferTextures = [];
this.pathColorsBufferTextures = [];
@ -154,12 +159,14 @@ export abstract class Renderer {
setAntialiasingOptions(aaType: AntialiasingStrategyName,
aaLevel: number,
subpixelAA: SubpixelAAType,
stemDarkening: StemDarkeningMode) {
aaOptions: AAOptions):
void {
this.gammaCorrectionMode = aaOptions.gammaCorrection;
this.antialiasingStrategy = this.createAAStrategy(aaType,
aaLevel,
subpixelAA,
stemDarkening);
aaOptions.subpixelAA,
aaOptions.stemDarkening);
this.antialiasingStrategy.init(this);
if (this.meshData != null)
@ -168,12 +175,12 @@ export abstract class Renderer {
this.renderContext.setDirty();
}
canvasResized() {
canvasResized(): void {
if (this.antialiasingStrategy != null)
this.antialiasingStrategy.init(this);
}
setFramebufferSizeUniform(uniforms: UniformMap) {
setFramebufferSizeUniform(uniforms: UniformMap): void {
const gl = this.renderContext.gl;
gl.uniform2i(uniforms.uFramebufferSize,
this.destAllocatedSize[0],
@ -201,7 +208,7 @@ export abstract class Renderer {
gl.uniform2f(uniforms.uTexScale, usedSize[0], usedSize[1]);
}
setTransformSTUniform(uniforms: UniformMap, objectIndex: number) {
setTransformSTUniform(uniforms: UniformMap, objectIndex: number): void {
// FIXME(pcwalton): Lossy conversion from a 4x4 matrix to an ST matrix is ugly and fragile.
// Refactor.
const renderContext = this.renderContext;
@ -219,7 +226,7 @@ export abstract class Renderer {
transform[13]);
}
uploadPathColors(objectCount: number) {
uploadPathColors(objectCount: number): void {
const renderContext = this.renderContext;
for (let objectIndex = 0; objectIndex < objectCount; objectIndex++) {
const pathColors = this.pathColorsForObject(objectIndex);
@ -237,7 +244,7 @@ export abstract class Renderer {
}
}
uploadPathTransforms(objectCount: number) {
uploadPathTransforms(objectCount: number): void {
const renderContext = this.renderContext;
for (let objectIndex = 0; objectIndex < objectCount; objectIndex++) {
const pathTransforms = this.pathTransformsForObject(objectIndex);
@ -273,6 +280,18 @@ export abstract class Renderer {
return glmatrix.vec4.create();
}
protected bindGammaLUT(bgColor: glmatrix.vec3, textureUnit: number, uniforms: UniformMap):
void {
const renderContext = this.renderContext;
const gl = renderContext.gl;
gl.activeTexture(gl.TEXTURE0 + textureUnit);
gl.bindTexture(gl.TEXTURE_2D, this.gammaLUTTexture);
gl.uniform1i(uniforms.uGammaLUT, textureUnit);
gl.uniform3f(uniforms.uBGColor, bgColor[0], bgColor[1], bgColor[2]);
}
protected abstract createAAStrategy(aaType: AntialiasingStrategyName,
aaLevel: number,
subpixelAA: SubpixelAAType,
@ -515,6 +534,12 @@ export abstract class Renderer {
const texture = unwrapNull(gl.createTexture());
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, gl.LUMINANCE, gl.UNSIGNED_BYTE, gammaLUT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
this.gammaLUTTexture = texture;
}
private initImplicitCoverCurveVAO(objectIndex: number, instanceRange: Range): void {

View File

@ -12,7 +12,8 @@ import {AttributeMap, UniformMap} from './gl-utils';
import {expectNotNull, PathfinderError, unwrapNull} from './utils';
export interface ShaderMap<T> {
blit: T;
blitLinear: T;
blitGamma: T;
compositeAlphaMask: T;
demo3DDistantGlyph: T;
demo3DMonument: T;
@ -43,7 +44,8 @@ export interface UnlinkedShaderProgram {
const COMMON_SHADER_URL: string = '/glsl/gles2/common.inc.glsl';
export const SHADER_NAMES: Array<keyof ShaderMap<void>> = [
'blit',
'blitLinear',
'blitGamma',
'compositeAlphaMask',
'directCurve',
'directInterior',
@ -67,8 +69,12 @@ export const SHADER_NAMES: Array<keyof ShaderMap<void>> = [
];
const SHADER_URLS: ShaderMap<ShaderProgramURLs> = {
blit: {
fragment: "/glsl/gles2/blit.fs.glsl",
blitGamma: {
fragment: "/glsl/gles2/blit-gamma.fs.glsl",
vertex: "/glsl/gles2/blit.vs.glsl",
},
blitLinear: {
fragment: "/glsl/gles2/blit-linear.fs.glsl",
vertex: "/glsl/gles2/blit.vs.glsl",
},
compositeAlphaMask: {

View File

@ -176,7 +176,7 @@ export default class SSAAStrategy extends AntialiasingStrategy {
if (this.subpixelAA !== 'none')
resolveProgram = renderContext.shaderPrograms.ssaaSubpixelResolve;
else
resolveProgram = renderContext.shaderPrograms.blit;
resolveProgram = renderContext.shaderPrograms.blitLinear;
gl.useProgram(resolveProgram.program);
renderContext.initQuadVAO(resolveProgram.attributes);

View File

@ -16,7 +16,7 @@ import * as opentype from 'opentype.js';
import {Metrics} from 'opentype.js';
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy";
import {StemDarkeningMode, SubpixelAAType} from './aa-strategy';
import {DemoAppController} from './app-controller';
import {AAOptions, DemoAppController} from './app-controller';
import {Atlas, ATLAS_SIZE, AtlasGlyph, GlyphKey, SUBPIXEL_GRANULARITY} from './atlas';
import PathfinderBufferTexture from './buffer-texture';
import {CameraView, OrthographicCamera} from "./camera";
@ -379,10 +379,9 @@ class TextDemoRenderer extends TextRenderer {
setAntialiasingOptions(aaType: AntialiasingStrategyName,
aaLevel: number,
subpixelAA: SubpixelAAType,
stemDarkening: StemDarkeningMode):
aaOptions: AAOptions):
void {
super.setAntialiasingOptions(aaType, aaLevel, subpixelAA, stemDarkening);
super.setAntialiasingOptions(aaType, aaLevel, aaOptions);
// Need to relayout because changing AA options can cause font dilation to change...
this.layoutText();
@ -425,8 +424,11 @@ class TextDemoRenderer extends TextRenderer {
gl.clearColor(1.0, 1.0, 1.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// Set the appropriate program.
const programName = this.gammaCorrectionMode === 'off' ? 'blitLinear' : 'blitGamma';
const blitProgram = this.renderContext.shaderPrograms[programName];
// Set up the composite VAO.
const blitProgram = this.renderContext.shaderPrograms.blit;
const attributes = blitProgram.attributes;
gl.useProgram(blitProgram.program);
gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphPositionsBuffer);
@ -458,6 +460,7 @@ class TextDemoRenderer extends TextRenderer {
gl.bindTexture(gl.TEXTURE_2D, destTexture);
gl.uniform1i(blitProgram.uniforms.uSource, 0);
this.setIdentityTexScaleUniform(blitProgram.uniforms);
this.bindGammaLUT(glmatrix.vec3.clone([1.0, 1.0, 1.0]), 1, blitProgram.uniforms);
const totalGlyphCount = this.layout.textFrame.totalGlyphCount;
gl.drawElements(gl.TRIANGLES, totalGlyphCount * 6, gl.UNSIGNED_INT, 0);
}

View File

@ -386,10 +386,7 @@ export function computeStemDarkeningAmount(pixelsPerEm: number, pixelsPerUnit: n
return amount;
glmatrix.vec2.scale(amount, STEM_DARKENING_FACTORS, pixelsPerEm);
glmatrix.vec2.min(amount, amount, MIN_STEM_DARKENING_AMOUNT);
// Convert diameter to radius.
glmatrix.vec2.scale(amount, amount, 0.5);
glmatrix.vec2.max(amount, amount, MIN_STEM_DARKENING_AMOUNT);
glmatrix.vec2.scale(amount, amount, 1.0 / pixelsPerUnit);
return amount;
}

View File

@ -12,6 +12,7 @@ import * as glmatrix from 'gl-matrix';
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy";
import {StemDarkeningMode, SubpixelAAType} from "./aa-strategy";
import {AAOptions} from './app-controller';
import PathfinderBufferTexture from './buffer-texture';
import {Camera} from "./camera";
import {EXTDisjointTimerQuery, QUAD_ELEMENTS, UniformMap} from './gl-utils';
@ -153,15 +154,13 @@ export abstract class DemoView extends PathfinderView implements RenderContext {
meshData: PathfinderMeshData[];
get colorAlphaFormat(): GLenum {
return this.sRGBExt == null ? this.gl.RGBA : this.sRGBExt.SRGB_ALPHA_EXT;
return this.gl.RGBA;
}
get renderContext(): RenderContext {
return this;
}
protected sRGBExt: EXTsRGB;
protected colorBufferHalfFloatExt: any;
private wantsScreenshot: boolean;
@ -204,9 +203,9 @@ export abstract class DemoView extends PathfinderView implements RenderContext {
setAntialiasingOptions(aaType: AntialiasingStrategyName,
aaLevel: number,
subpixelAA: SubpixelAAType,
stemDarkening: StemDarkeningMode) {
this.renderer.setAntialiasingOptions(aaType, aaLevel, subpixelAA, stemDarkening);
aaOptions: AAOptions):
void {
this.renderer.setAntialiasingOptions(aaType, aaLevel, aaOptions);
}
protected resized(): void {
@ -220,7 +219,6 @@ export abstract class DemoView extends PathfinderView implements RenderContext {
"Failed to initialize WebGL! Check that your browser supports it.");
this.colorBufferHalfFloatExt = this.gl.getExtension('EXT_color_buffer_half_float');
this.instancedArraysExt = this.gl.getExtension('ANGLE_instanced_arrays');
this.sRGBExt = this.gl.getExtension('EXT_sRGB');
this.textureHalfFloatExt = this.gl.getExtension('OES_texture_half_float');
this.timerQueryExt = this.gl.getExtension('EXT_disjoint_timer_query');
this.vertexArrayObjectExt = this.gl.getExtension('OES_vertex_array_object');

View File

@ -0,0 +1,25 @@
// pathfinder/shaders/gles2/blit-gamma.fs.glsl
//
// Copyright (c) 2017 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
/// Blits a texture, applying gamma correction.
precision mediump float;
uniform sampler2D uSource;
uniform vec3 uBGColor;
uniform sampler2D uGammaLUT;
varying vec2 vTexCoord;
void main() {
vec4 source = texture2D(uSource, vTexCoord);
gl_FragColor = vec4(gammaCorrect(source.rgb, uBGColor, uGammaLUT), source.a);
}

View File

@ -1,4 +1,4 @@
// pathfinder/shaders/gles2/blit.fs.glsl
// pathfinder/shaders/gles2/blit-linear.fs.glsl
//
// Copyright (c) 2017 The Pathfinder Project Developers.
//

View File

@ -306,6 +306,17 @@ float lcdFilter(float shadeL2, float shadeL1, float shade0, float shadeR1, float
LCD_FILTER_FACTOR_2 * shadeR2;
}
float gammaCorrectChannel(float fgColor, float bgColor, sampler2D gammaLUT) {
return texture2D(gammaLUT, vec2(fgColor, 1.0 - bgColor)).r;
}
// `fgColor` is in linear space.
vec3 gammaCorrect(vec3 fgColor, vec3 bgColor, sampler2D gammaLUT) {
return vec3(gammaCorrectChannel(fgColor.r, bgColor.r, gammaLUT),
gammaCorrectChannel(fgColor.g, bgColor.g, gammaLUT),
gammaCorrectChannel(fgColor.b, bgColor.b, gammaLUT));
}
int unpackUInt16(vec2 packedValue) {
ivec2 valueBytes = ivec2(floor(packedValue * 255.0));
return valueBytes.y * 256 + valueBytes.x;