From e5b76726d99bd4b1d37a298e945d5945950822f1 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Tue, 7 Nov 2017 17:24:19 -0800 Subject: [PATCH] Use the LUT to gamma correct text, and fix stem darkening math --- demo/client/html/text-demo.html.hbs | 4 + demo/client/src/aa-strategy.ts | 2 + demo/client/src/app-controller.ts | 106 ++++++++++++------ demo/client/src/renderer.ts | 51 ++++++--- demo/client/src/shader-loader.ts | 14 ++- demo/client/src/ssaa-strategy.ts | 2 +- demo/client/src/text-demo.ts | 13 ++- demo/client/src/text.ts | 5 +- demo/client/src/view.ts | 12 +- shaders/gles2/blit-gamma.fs.glsl | 25 +++++ .../{blit.fs.glsl => blit-linear.fs.glsl} | 2 +- shaders/gles2/common.inc.glsl | 11 ++ 12 files changed, 180 insertions(+), 67 deletions(-) create mode 100644 shaders/gles2/blit-gamma.fs.glsl rename shaders/gles2/{blit.fs.glsl => blit-linear.fs.glsl} (92%) diff --git a/demo/client/html/text-demo.html.hbs b/demo/client/html/text-demo.html.hbs index f9bd76ca..14e73fea 100644 --- a/demo/client/html/text-demo.html.hbs +++ b/demo/client/html/text-demo.html.hbs @@ -70,6 +70,10 @@ {{>partials/switch.html id="pf-subpixel-aa" title="Subpixel AA"}} +
+ {{>partials/switch.html id="pf-gamma-correction" + title="Gamma Correction"}} +
{{>partials/switch.html id="pf-stem-darkening" title="Stem Darkening"}} diff --git a/demo/client/src/aa-strategy.ts b/demo/client/src/aa-strategy.ts index 21af70bf..fe9042da 100644 --- a/demo/client/src/aa-strategy.ts +++ b/demo/client/src/aa-strategy.ts @@ -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 { diff --git a/demo/client/src/app-controller.ts b/demo/client/src/app-controller.ts index 41cac9e0..c8bb86b7 100644 --- a/demo/client/src/app-controller.ts +++ b/demo/client/src/app-controller.ts @@ -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 extends AppController { +export abstract class DemoAppController extends AppController + implements Switches { view: Promise; + 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 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 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) { + 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 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 = {}; + for (const switchName of Object.keys(SWITCHES) as Array) { + 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); }); } diff --git a/demo/client/src/renderer.ts b/demo/client/src/renderer.ts index 4eec6ab4..3ed8b8e0 100644 --- a/demo/client/src/renderer.ts +++ b/demo/client/src/renderer.ts @@ -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 { diff --git a/demo/client/src/shader-loader.ts b/demo/client/src/shader-loader.ts index bf224553..0aa84f3f 100644 --- a/demo/client/src/shader-loader.ts +++ b/demo/client/src/shader-loader.ts @@ -12,7 +12,8 @@ import {AttributeMap, UniformMap} from './gl-utils'; import {expectNotNull, PathfinderError, unwrapNull} from './utils'; export interface ShaderMap { - 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> = [ - 'blit', + 'blitLinear', + 'blitGamma', 'compositeAlphaMask', 'directCurve', 'directInterior', @@ -67,8 +69,12 @@ export const SHADER_NAMES: Array> = [ ]; const SHADER_URLS: ShaderMap = { - 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: { diff --git a/demo/client/src/ssaa-strategy.ts b/demo/client/src/ssaa-strategy.ts index 73bdc62b..a9e5496f 100644 --- a/demo/client/src/ssaa-strategy.ts +++ b/demo/client/src/ssaa-strategy.ts @@ -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); diff --git a/demo/client/src/text-demo.ts b/demo/client/src/text-demo.ts index 37821647..cf455b07 100644 --- a/demo/client/src/text-demo.ts +++ b/demo/client/src/text-demo.ts @@ -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); } diff --git a/demo/client/src/text.ts b/demo/client/src/text.ts index 5b052220..ac3659c6 100644 --- a/demo/client/src/text.ts +++ b/demo/client/src/text.ts @@ -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; } diff --git a/demo/client/src/view.ts b/demo/client/src/view.ts index 490007f3..c12b5804 100644 --- a/demo/client/src/view.ts +++ b/demo/client/src/view.ts @@ -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'); diff --git a/shaders/gles2/blit-gamma.fs.glsl b/shaders/gles2/blit-gamma.fs.glsl new file mode 100644 index 00000000..6562cc8d --- /dev/null +++ b/shaders/gles2/blit-gamma.fs.glsl @@ -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 or the MIT license +// , 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); +} diff --git a/shaders/gles2/blit.fs.glsl b/shaders/gles2/blit-linear.fs.glsl similarity index 92% rename from shaders/gles2/blit.fs.glsl rename to shaders/gles2/blit-linear.fs.glsl index 03667e4d..9693c8e8 100644 --- a/shaders/gles2/blit.fs.glsl +++ b/shaders/gles2/blit-linear.fs.glsl @@ -1,4 +1,4 @@ -// pathfinder/shaders/gles2/blit.fs.glsl +// pathfinder/shaders/gles2/blit-linear.fs.glsl // // Copyright (c) 2017 The Pathfinder Project Developers. // diff --git a/shaders/gles2/common.inc.glsl b/shaders/gles2/common.inc.glsl index bb4684b9..675a9cff 100644 --- a/shaders/gles2/common.inc.glsl +++ b/shaders/gles2/common.inc.glsl @@ -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;