diff --git a/Cargo.toml b/Cargo.toml index 789129ac..e90d691d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "partitioner", "path-utils", "demo/server", + "utils/area-lut", "utils/frontend", "utils/gamma-lut", ] diff --git a/demo/client/html/benchmark.html.hbs b/demo/client/html/benchmark.html.hbs index a0e83cd6..bd5dab81 100644 --- a/demo/client/html/benchmark.html.hbs +++ b/demo/client/html/benchmark.html.hbs @@ -51,8 +51,18 @@ -
- {{>partials/switch.html id="pf-subpixel-aa" title="Subpixel AA"}} +
+ +
diff --git a/demo/client/html/text-demo.html.hbs b/demo/client/html/text-demo.html.hbs index 2e39ddc2..e1ce6155 100644 --- a/demo/client/html/text-demo.html.hbs +++ b/demo/client/html/text-demo.html.hbs @@ -67,9 +67,14 @@ -
- {{>partials/switch.html id="pf-subpixel-aa" - title="Subpixel AA"}} +
+ +
{{>partials/switch.html id="pf-gamma-correction" diff --git a/demo/client/src/3d-demo.ts b/demo/client/src/3d-demo.ts index 396cc25d..42bdcb1b 100644 --- a/demo/client/src/3d-demo.ts +++ b/demo/client/src/3d-demo.ts @@ -155,11 +155,12 @@ class ThreeDController extends DemoAppController { this.monumentPromise.then(monument => this.layoutMonument(fileData, monument)); } - protected createView(gammaLUT: HTMLImageElement, + protected createView(areaLUT: HTMLImageElement, + gammaLUT: HTMLImageElement, commonShaderSource: string, shaderSources: ShaderMap): ThreeDView { - return new ThreeDView(this, gammaLUT, commonShaderSource, shaderSources); + return new ThreeDView(this, areaLUT, gammaLUT, commonShaderSource, shaderSources); } protected get builtinFileURI(): string { @@ -343,10 +344,11 @@ class ThreeDView extends DemoView implements TextRenderContext { } constructor(appController: ThreeDController, + areaLUT: HTMLImageElement, gammaLUT: HTMLImageElement, commonShaderSource: string, shaderSources: ShaderMap) { - super(gammaLUT, commonShaderSource, shaderSources); + super(areaLUT, gammaLUT, commonShaderSource, shaderSources); this.cameraView = new ThreeDAtlasCameraView; @@ -385,6 +387,10 @@ class ThreeDRenderer extends Renderer { return this.destAllocatedSize; } + get allowSubpixelAA(): boolean { + return false; + } + get backgroundColor(): glmatrix.vec4 { return glmatrix.vec4.clone([1.0, 1.0, 1.0, 1.0]); } @@ -466,6 +472,17 @@ class ThreeDRenderer extends Renderer { this.renderContext.gl.uniform4f(uniforms.uHints, 0, 0, 0, 0); } + pathTransformsForObject(objectIndex: number): PathTransformBuffers { + const meshDescriptor = this.renderContext.appController.meshDescriptors[objectIndex]; + const pathCount = this.pathCountForObject(objectIndex); + const pathTransforms = this.createPathTransformBuffers(pathCount); + for (let pathIndex = 0; pathIndex < pathCount; pathIndex++) { + const glyphOrigin = meshDescriptor.positions[pathIndex]; + pathTransforms.st.set([1, 1, glyphOrigin[0], glyphOrigin[1]], (pathIndex + 1) * 4); + } + return pathTransforms; + } + protected clearColorForObject(objectIndex: number): glmatrix.vec4 | null { return null; } @@ -496,17 +513,6 @@ class ThreeDRenderer extends Renderer { return TEXT_COLOR; } - protected pathTransformsForObject(objectIndex: number): PathTransformBuffers { - const meshDescriptor = this.renderContext.appController.meshDescriptors[objectIndex]; - const pathCount = this.pathCountForObject(objectIndex); - const pathTransforms = this.createPathTransformBuffers(pathCount); - for (let pathIndex = 0; pathIndex < pathCount; pathIndex++) { - const glyphOrigin = meshDescriptor.positions[pathIndex]; - pathTransforms.st.set([1, 1, glyphOrigin[0], glyphOrigin[1]], (pathIndex + 1) * 4); - } - return pathTransforms; - } - protected meshInstanceCountForObject(objectIndex: number): number { return this.renderContext.appController.meshDescriptors[objectIndex].positions.length; } diff --git a/demo/client/src/aa-strategy.ts b/demo/client/src/aa-strategy.ts index 804ad3d9..b329b6d0 100644 --- a/demo/client/src/aa-strategy.ts +++ b/demo/client/src/aa-strategy.ts @@ -10,17 +10,25 @@ import * as glmatrix from 'gl-matrix'; -import {createFramebuffer, createFramebufferColorTexture} from './gl-utils'; +import {createFramebuffer, createFramebufferColorTexture, UniformMap} from './gl-utils'; import {createFramebufferDepthTexture} from './gl-utils'; import {Renderer} from './renderer'; import {unwrapNull} from './utils'; import {DemoView} from './view'; +const SUBPIXEL_AA_KERNELS: {readonly [kind in SubpixelAAType]: glmatrix.vec4} = { + // These intentionally do not precisely match what Core Graphics does (a Lanczos function), + // because we don't want any ringing artefacts. + 'core-graphics': glmatrix.vec4.clone([0.033165660, 0.102074051, 0.221434336, 0.286651906]), + 'freetype': glmatrix.vec4.clone([0.0, 0.031372549, 0.301960784, 0.337254902]), + 'none': glmatrix.vec4.clone([0.0, 0.0, 0.0, 1.0]), +}; + export type AntialiasingStrategyName = 'none' | 'ssaa' | 'xcaa'; export type DirectRenderingMode = 'none' | 'conservative' | 'color'; -export type SubpixelAAType = 'none' | 'medium'; +export type SubpixelAAType = 'none' | 'freetype' | 'core-graphics'; export type GammaCorrectionMode = 'off' | 'on'; @@ -38,6 +46,12 @@ export abstract class AntialiasingStrategy { // How many rendering passes this AA strategy requires. abstract readonly passCount: number; + protected subpixelAA: SubpixelAAType; + + constructor(subpixelAA: SubpixelAAType) { + this.subpixelAA = subpixelAA; + } + // Prepares any OpenGL data. This is only called on startup and canvas resize. init(renderer: Renderer): void { this.setFramebufferSize(renderer); @@ -83,6 +97,14 @@ export abstract class AntialiasingStrategy { // This usually blits to the real framebuffer. abstract resolve(pass: number, renderer: Renderer): void; + setSubpixelAAKernelUniform(renderer: Renderer, uniforms: UniformMap): void { + const renderContext = renderer.renderContext; + const gl = renderContext.gl; + + const kernel = SUBPIXEL_AA_KERNELS[this.subpixelAA]; + gl.uniform4f(uniforms.uKernel, kernel[0], kernel[1], kernel[2], kernel[3]); + } + worldTransformForPass(renderer: Renderer, pass: number): glmatrix.mat4 { return glmatrix.mat4.create(); } @@ -96,7 +118,7 @@ export class NoAAStrategy extends AntialiasingStrategy { } constructor(level: number, subpixelAA: SubpixelAAType) { - super(); + super(subpixelAA); this.framebufferSize = glmatrix.vec2.create(); } diff --git a/demo/client/src/app-controller.ts b/demo/client/src/app-controller.ts index b3c50107..9df5b686 100644 --- a/demo/client/src/app-controller.ts +++ b/demo/client/src/app-controller.ts @@ -15,6 +15,7 @@ import {ShaderLoader, ShaderMap, ShaderProgramSource} from './shader-loader'; import {expectNotNull, unwrapNull, unwrapUndef} from './utils'; import {DemoView, Timings, TIMINGS} from "./view"; +const AREA_LUT_URI: string = "/textures/area-lut.png"; const GAMMA_LUT_URI: string = "/textures/gamma-lut.png"; const SWITCHES: SwitchMap = { @@ -32,13 +33,6 @@ const SWITCHES: SwitchMap = { onValue: 'dark', switchInputsName: 'stemDarkeningSwitchInputs', }, - subpixelAA: { - defaultValue: 'none', - id: 'pf-subpixel-aa', - offValue: 'none', - onValue: 'medium', - switchInputsName: 'subpixelAASwitchInputs', - }, }; interface SwitchDescriptor { @@ -52,7 +46,6 @@ interface SwitchDescriptor { interface SwitchMap { gammaCorrection: SwitchDescriptor; stemDarkening: SwitchDescriptor; - subpixelAA: SwitchDescriptor; } export interface AAOptions { @@ -67,7 +60,6 @@ export interface SwitchInputs { } interface Switches { - subpixelAASwitchInputs: SwitchInputs | null; gammaCorrectionSwitchInputs: SwitchInputs | null; stemDarkeningSwitchInputs: SwitchInputs | null; } @@ -111,7 +103,6 @@ export abstract class DemoAppController extends AppContro implements Switches { view!: Promise; - subpixelAASwitchInputs: SwitchInputs | null = null; gammaCorrectionSwitchInputs: SwitchInputs | null = null; stemDarkeningSwitchInputs: SwitchInputs | null = null; @@ -120,6 +111,7 @@ export abstract class DemoAppController extends AppContro protected filePickerView: FilePickerView | null = null; protected aaLevelSelect: HTMLSelectElement | null = null; + protected subpixelAASelect: HTMLSelectElement | null = null; private fpsLabel: HTMLElement | null = null; @@ -190,11 +182,17 @@ export abstract class DemoAppController extends AppContro const shaderLoader = new ShaderLoader; shaderLoader.load(); - const gammaLUTPromise = this.loadGammaLUT(); + const areaLUTPromise = this.loadTexture(AREA_LUT_URI); + const gammaLUTPromise = this.loadTexture(GAMMA_LUT_URI); - const promises: any[] = [gammaLUTPromise, shaderLoader.common, shaderLoader.shaders]; + const promises: any[] = [ + areaLUTPromise, + gammaLUTPromise, + shaderLoader.common, + shaderLoader.shaders, + ]; this.view = Promise.all(promises).then(assets => { - return this.createView(assets[0], assets[1], assets[2]); + return this.createView(assets[0], assets[1], assets[2], assets[3]); }); this.aaLevelSelect = document.getElementById('pf-aa-level-select') as @@ -202,6 +200,11 @@ export abstract class DemoAppController extends AppContro if (this.aaLevelSelect != null) this.aaLevelSelect.addEventListener('change', () => this.updateAALevel(), false); + this.subpixelAASelect = document.getElementById('pf-subpixel-aa-select') as + (HTMLSelectElement | null); + if (this.subpixelAASelect != null) + this.subpixelAASelect.addEventListener('change', () => this.updateAALevel(), false); + // The event listeners here use `window.setTimeout()` because jQuery won't fire the "live" // 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 @@ -284,6 +287,12 @@ export abstract class DemoAppController extends AppContro else aaOptions[switchName] = switchDescriptor.offValue as any; } + if (this.subpixelAASelect != null) { + const selectedOption = this.subpixelAASelect.selectedOptions[0]; + aaOptions.subpixelAA = selectedOption.value as SubpixelAAType; + } else { + aaOptions.subpixelAA = 'none'; + } return this.view.then(view => { view.setAntialiasingOptions(aaType, aaLevel, aaOptions as AAOptions); @@ -294,7 +303,8 @@ export abstract class DemoAppController extends AppContro // Overridden by subclasses. } - protected abstract createView(gammaLUT: HTMLImageElement, + protected abstract createView(areaLUT: HTMLImageElement, + gammaLUT: HTMLImageElement, commonShaderSource: string, shaderSources: ShaderMap): View; @@ -331,8 +341,8 @@ export abstract class DemoAppController extends AppContro }, false); } - private loadGammaLUT(): Promise { - return window.fetch(GAMMA_LUT_URI) + private loadTexture(uri: string): Promise { + return window.fetch(uri) .then(response => response.blob()) .then(blob => { const imgElement = document.createElement('img'); diff --git a/demo/client/src/benchmark.ts b/demo/client/src/benchmark.ts index bb168819..6a1b9347 100644 --- a/demo/client/src/benchmark.ts +++ b/demo/client/src/benchmark.ts @@ -188,19 +188,20 @@ class BenchmarkAppController extends DemoAppController { } } - protected createView(gammaLUT: HTMLImageElement, + protected createView(areaLUT: HTMLImageElement, + gammaLUT: HTMLImageElement, commonShaderSource: string, shaderSources: ShaderMap): BenchmarkTestView { - return new BenchmarkTestView(this, gammaLUT, commonShaderSource, shaderSources); + return new BenchmarkTestView(this, areaLUT, gammaLUT, commonShaderSource, shaderSources); } private modeChanged(): void { this.loadInitialFile(this.builtinFileURI); if (this.aaLevelSelect != null) this.aaLevelSelect.selectedIndex = 0; - if (this.subpixelAASwitchInputs != null) - setSwitchInputsValue(this.subpixelAASwitchInputs, false); + if (this.subpixelAASelect != null) + this.subpixelAASelect.selectedIndex = 0; this.updateAALevel(); } @@ -381,10 +382,11 @@ class BenchmarkTestView extends DemoView { } constructor(appController: BenchmarkAppController, + areaLUT: HTMLImageElement, gammaLUT: HTMLImageElement, commonShaderSource: string, shaderSources: ShaderMap) { - super(gammaLUT, commonShaderSource, shaderSources); + super(areaLUT, gammaLUT, commonShaderSource, shaderSources); this.appController = appController; this.recreateRenderer(); this.resizeToFit(true); @@ -447,6 +449,10 @@ class BenchmarkTextRenderer extends Renderer { return this.destAllocatedSize; } + get allowSubpixelAA(): boolean { + return true; + } + get emboldenAmount(): glmatrix.vec2 { return this.stemDarkeningAmount; } @@ -541,27 +547,7 @@ class BenchmarkTextRenderer extends Renderer { this.renderContext.gl.uniform4f(uniforms.uHints, 0, 0, 0, 0); } - protected createAAStrategy(aaType: AntialiasingStrategyName, - aaLevel: number, - subpixelAA: SubpixelAAType): - AntialiasingStrategy { - return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA); - } - - protected compositeIfNecessary(): void {} - - protected updateTimings(timings: Timings): void { - // TODO(pcwalton) - } - - protected pathColorsForObject(objectIndex: number): Uint8Array { - const pathColors = new Uint8Array(4 * (STRING.length + 1)); - for (let pathIndex = 0; pathIndex < STRING.length; pathIndex++) - pathColors.set(TEXT_COLOR, (pathIndex + 1) * 4); - return pathColors; - } - - protected pathTransformsForObject(objectIndex: number): PathTransformBuffers { + pathTransformsForObject(objectIndex: number): PathTransformBuffers { const appController = this.renderContext.appController; const canvas = this.renderContext.canvas; const font = unwrapNull(appController.font); @@ -586,6 +572,26 @@ class BenchmarkTextRenderer extends Renderer { return pathTransforms; } + protected createAAStrategy(aaType: AntialiasingStrategyName, + aaLevel: number, + subpixelAA: SubpixelAAType): + AntialiasingStrategy { + return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA); + } + + protected compositeIfNecessary(): void {} + + protected updateTimings(timings: Timings): void { + // TODO(pcwalton) + } + + protected pathColorsForObject(objectIndex: number): Uint8Array { + const pathColors = new Uint8Array(4 * (STRING.length + 1)); + for (let pathIndex = 0; pathIndex < STRING.length; pathIndex++) + pathColors.set(TEXT_COLOR, (pathIndex + 1) * 4); + return pathColors; + } + protected directCurveProgramName(): keyof ShaderMap { return 'directCurve'; } diff --git a/demo/client/src/reference-test.ts b/demo/client/src/reference-test.ts index c8687454..7782946a 100644 --- a/demo/client/src/reference-test.ts +++ b/demo/client/src/reference-test.ts @@ -19,7 +19,8 @@ import {DemoAppController, setSwitchInputsValue} from "./app-controller"; import {SUBPIXEL_GRANULARITY} from './atlas'; import {OrthographicCamera} from './camera'; import {UniformMap} from './gl-utils'; -import {PathfinderMeshPack, PathfinderPackedMeshBuffers, PathfinderPackedMeshes} from './meshes'; +import {B_QUAD_UPPER_CONTROL_POINT_VERTEX_OFFSET, PathfinderMeshPack} from './meshes'; +import {PathfinderPackedMeshBuffers, PathfinderPackedMeshes} from './meshes'; import {PathTransformBuffers, Renderer} from "./renderer"; import {ShaderMap, ShaderProgramSource} from "./shader-loader"; import SSAAStrategy from './ssaa-strategy'; @@ -28,6 +29,7 @@ import {SVGRenderer} from './svg-renderer'; import {BUILTIN_FONT_URI, computeStemDarkeningAmount, ExpandedMeshData, GlyphStore} from "./text"; import {Hint} from "./text"; import {PathfinderFont, TextFrame, TextRun} from "./text"; +import {MAX_SUBPIXEL_AA_FONT_SIZE} from './text-renderer'; import {unwrapNull} from "./utils"; import {DemoView} from "./view"; import {AdaptiveStencilMeshAAAStrategy} from './xcaa-strategy'; @@ -314,11 +316,12 @@ class ReferenceTestAppController extends DemoAppController { context.putImageData(imageData, 0, 0); } - protected createView(gammaLUT: HTMLImageElement, + protected createView(areaLUT: HTMLImageElement, + gammaLUT: HTMLImageElement, commonShaderSource: string, shaderSources: ShaderMap): ReferenceTestView { - return new ReferenceTestView(this, gammaLUT, commonShaderSource, shaderSources); + return new ReferenceTestView(this, areaLUT, gammaLUT, commonShaderSource, shaderSources); } protected fileLoaded(fileData: ArrayBuffer, builtinName: string | null): void { @@ -468,7 +471,8 @@ class ReferenceTestAppController extends DemoAppController { return option.value.startsWith(currentTestCase.aaMode); }); - setSwitchInputsValue(unwrapNull(this.subpixelAASwitchInputs), currentTestCase.subpixel); + const subpixelAASelect = unwrapNull(this.subpixelAASelect); + subpixelAASelect.selectedIndex = currentTestCase.subpixel ? 1 : 0; return this.updateAALevel(); } @@ -591,10 +595,11 @@ class ReferenceTestView extends DemoView { } constructor(appController: ReferenceTestAppController, + areaLUT: HTMLImageElement, gammaLUT: HTMLImageElement, commonShaderSource: string, shaderSources: ShaderMap) { - super(gammaLUT, commonShaderSource, shaderSources); + super(areaLUT, gammaLUT, commonShaderSource, shaderSources); this.appController = appController; this.recreateRenderer(); @@ -687,6 +692,11 @@ class ReferenceTestTextRenderer extends Renderer { return this.stemDarkeningAmount; } + get allowSubpixelAA(): boolean { + const appController = this.renderContext.appController; + return appController.currentFontSize <= MAX_SUBPIXEL_AA_FONT_SIZE; + } + protected get objectCount(): number { return this.meshBuffers == null ? 0 : this.meshBuffers.length; } @@ -768,22 +778,7 @@ class ReferenceTestTextRenderer extends Renderer { return textRun.pixelRectForGlyphAt(glyphIndex); } - protected createAAStrategy(aaType: AntialiasingStrategyName, - aaLevel: number, - subpixelAA: SubpixelAAType): - AntialiasingStrategy { - return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA); - } - - protected compositeIfNecessary(): void {} - - protected pathColorsForObject(objectIndex: number): Uint8Array { - const pathColors = new Uint8Array(4 * 2); - pathColors.set(TEXT_COLOR, 1 * 4); - return pathColors; - } - - protected pathTransformsForObject(objectIndex: number): PathTransformBuffers { + pathTransformsForObject(objectIndex: number): PathTransformBuffers { const appController = this.renderContext.appController; const canvas = this.renderContext.canvas; const font = unwrapNull(appController.font); @@ -809,6 +804,21 @@ class ReferenceTestTextRenderer extends Renderer { return pathTransforms; } + protected createAAStrategy(aaType: AntialiasingStrategyName, + aaLevel: number, + subpixelAA: SubpixelAAType): + AntialiasingStrategy { + return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA); + } + + protected compositeIfNecessary(): void {} + + protected pathColorsForObject(objectIndex: number): Uint8Array { + const pathColors = new Uint8Array(4 * 2); + pathColors.set(TEXT_COLOR, 1 * 4); + return pathColors; + } + protected directCurveProgramName(): keyof ShaderMap { return 'directCurve'; } diff --git a/demo/client/src/renderer.ts b/demo/client/src/renderer.ts index 93891236..ad4620b8 100644 --- a/demo/client/src/renderer.ts +++ b/demo/client/src/renderer.ts @@ -70,6 +70,7 @@ export abstract class Renderer { abstract get isMulticolor(): boolean; abstract get needsStencil(): boolean; + abstract get allowSubpixelAA(): boolean; abstract get destFramebuffer(): WebGLFramebuffer | null; abstract get destAllocatedSize(): glmatrix.vec2; @@ -92,6 +93,7 @@ export abstract class Renderer { private implicitCoverCurveVAO: WebGLVertexArrayObjectOES | null = null; private gammaLUTTexture: WebGLTexture | null = null; + private areaLUTTexture: WebGLTexture | null = null; private instancedPathIDVBO: WebGLBuffer | null = null; private vertexIDVBO: WebGLBuffer | null = null; @@ -114,7 +116,8 @@ export abstract class Renderer { this.initInstancedPathIDVBO(); this.initVertexIDVBO(); - this.initGammaLUTTexture(); + this.initLUTTexture('gammaLUT', 'gammaLUTTexture'); + this.initLUTTexture('areaLUT', 'areaLUTTexture'); this.antialiasingStrategy = new NoAAStrategy(0, 'none'); this.antialiasingStrategy.init(this); @@ -132,6 +135,7 @@ export abstract class Renderer { abstract pathBoundingRects(objectIndex: number): Float32Array; abstract setHintsUniform(uniforms: UniformMap): void; + abstract pathTransformsForObject(objectIndex: number): PathTransformBuffers; redraw(): void { const renderContext = this.renderContext; @@ -287,20 +291,27 @@ export abstract class Renderer { transform[13]); } - setTransformAffineUniforms(uniforms: UniformMap, objectIndex: number): void { + affineTransform(objectIndex: number): glmatrix.mat2d { // FIXME(pcwalton): Lossy conversion from a 4x4 matrix to an affine matrix is ugly and // fragile. Refactor. + const transform = this.computeTransform(0, objectIndex); + return glmatrix.mat2d.fromValues(transform[0], transform[1], + transform[4], transform[5], + transform[12], transform[13]); + + } + + setTransformAffineUniforms(uniforms: UniformMap, objectIndex: number): void { const renderContext = this.renderContext; const gl = renderContext.gl; - const transform = this.computeTransform(0, objectIndex); - + const transform = this.affineTransform(objectIndex); gl.uniform4f(uniforms.uTransformST, transform[0], - transform[5], - transform[12], - transform[13]); - gl.uniform2f(uniforms.uTransformExt, transform[1], transform[4]); + transform[3], + transform[4], + transform[5]); + gl.uniform2f(uniforms.uTransformExt, transform[1], transform[2]); } uploadPathColors(objectCount: number): void { @@ -367,6 +378,15 @@ export abstract class Renderer { return new Range(1, bVertexPathRanges.length + 1); } + bindAreaLUT(textureUnit: number, uniforms: UniformMap): void { + const renderContext = this.renderContext; + const gl = renderContext.gl; + + gl.activeTexture(gl.TEXTURE0 + textureUnit); + gl.bindTexture(gl.TEXTURE_2D, this.areaLUTTexture); + gl.uniform1i(uniforms.uAreaLUT, textureUnit); + } + protected clearColorForObject(objectIndex: number): glmatrix.vec4 | null { return null; } @@ -390,8 +410,6 @@ export abstract class Renderer { AntialiasingStrategy; protected abstract compositeIfNecessary(): void; protected abstract pathColorsForObject(objectIndex: number): Uint8Array; - protected abstract pathTransformsForObject(objectIndex: number): - PathTransformBuffers; protected abstract directCurveProgramName(): keyof ShaderMap; protected abstract directInteriorProgramName(renderingMode: DirectRenderingMode): @@ -609,20 +627,23 @@ export abstract class Renderer { }, TIME_INTERVAL_DELAY); } - private initGammaLUTTexture(): void { + private initLUTTexture(imageName: 'gammaLUT' | 'areaLUT', + textureName: 'gammaLUTTexture' | 'areaLUTTexture'): + void { const renderContext = this.renderContext; const gl = renderContext.gl; - const gammaLUT = renderContext.gammaLUT; + const image = renderContext[imageName]; 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.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, gl.LUMINANCE, gl.UNSIGNED_BYTE, image); + const filter = imageName === 'gammaLUT' ? gl.NEAREST : gl.LINEAR; + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter); 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; + this[textureName] = texture; } private initImplicitCoverCurveVAO(objectIndex: number, instanceRange: Range): void { diff --git a/demo/client/src/ssaa-strategy.ts b/demo/client/src/ssaa-strategy.ts index 01ecdec0..395f6d3c 100644 --- a/demo/client/src/ssaa-strategy.ts +++ b/demo/client/src/ssaa-strategy.ts @@ -29,7 +29,6 @@ export default class SSAAStrategy extends AntialiasingStrategy { } private level: number; - private subpixelAA: SubpixelAAType; private destFramebufferSize: glmatrix.vec2; private supersampledFramebufferSize: glmatrix.vec2; @@ -38,7 +37,7 @@ export default class SSAAStrategy extends AntialiasingStrategy { private supersampledFramebuffer!: WebGLFramebuffer; constructor(level: number, subpixelAA: SubpixelAAType) { - super(); + super(subpixelAA); this.level = level; this.subpixelAA = subpixelAA; diff --git a/demo/client/src/svg-demo.ts b/demo/client/src/svg-demo.ts index dd76cf7c..e62d6c6e 100644 --- a/demo/client/src/svg-demo.ts +++ b/demo/client/src/svg-demo.ts @@ -48,11 +48,12 @@ class SVGDemoController extends DemoAppController { }); } - protected createView(gammaLUT: HTMLImageElement, + protected createView(areaLUT: HTMLImageElement, + gammaLUT: HTMLImageElement, commonShaderSource: string, shaderSources: ShaderMap): SVGDemoView { - return new SVGDemoView(this, gammaLUT, commonShaderSource, shaderSources); + return new SVGDemoView(this, areaLUT, gammaLUT, commonShaderSource, shaderSources); } protected get defaultFile(): string { @@ -76,10 +77,11 @@ class SVGDemoView extends DemoView { } constructor(appController: SVGDemoController, + areaLUT: HTMLImageElement, gammaLUT: HTMLImageElement, commonShaderSource: string, shaderSources: ShaderMap) { - super(gammaLUT, commonShaderSource, shaderSources); + super(areaLUT, gammaLUT, commonShaderSource, shaderSources); this.appController = appController; this.renderer = new SVGDemoRenderer(this, {sizeToFit: true}); diff --git a/demo/client/src/svg-renderer.ts b/demo/client/src/svg-renderer.ts index 7bf409be..1947e080 100644 --- a/demo/client/src/svg-renderer.ts +++ b/demo/client/src/svg-renderer.ts @@ -79,6 +79,10 @@ export abstract class SVGRenderer extends Renderer { return glmatrix.vec4.clone([1.0, 1.0, 1.0, 1.0]); } + get allowSubpixelAA(): boolean { + return false; + } + protected get objectCount(): number { return 1; } @@ -136,6 +140,19 @@ export abstract class SVGRenderer extends Renderer { return new Range(1, this.loader.pathInstances.length + 1); } + pathTransformsForObject(objectIndex: number): PathTransformBuffers { + const instances = this.loader.pathInstances; + const pathTransforms = this.createPathTransformBuffers(instances.length); + + for (let pathIndex = 0; pathIndex < instances.length; pathIndex++) { + // TODO(pcwalton): Set transform. + const startOffset = (pathIndex + 1) * 4; + pathTransforms.st.set([1, 1, 0, 0], startOffset); + } + + return pathTransforms; + } + protected get usedSizeFactor(): glmatrix.vec2 { return glmatrix.vec2.clone([1.0, 1.0]); } @@ -189,19 +206,6 @@ export abstract class SVGRenderer extends Renderer { return pathColors; } - protected pathTransformsForObject(objectIndex: number): PathTransformBuffers { - const instances = this.loader.pathInstances; - const pathTransforms = this.createPathTransformBuffers(instances.length); - - for (let pathIndex = 0; pathIndex < instances.length; pathIndex++) { - // TODO(pcwalton): Set transform. - const startOffset = (pathIndex + 1) * 4; - pathTransforms.st.set([1, 1, 0, 0], startOffset); - } - - return pathTransforms; - } - protected createAAStrategy(aaType: AntialiasingStrategyName, aaLevel: number, subpixelAA: SubpixelAAType): diff --git a/demo/client/src/text-demo.ts b/demo/client/src/text-demo.ts index d18a92c1..46b00953 100644 --- a/demo/client/src/text-demo.ts +++ b/demo/client/src/text-demo.ts @@ -227,11 +227,12 @@ class TextDemoController extends DemoAppController { } } - protected createView(gammaLUT: HTMLImageElement, + protected createView(areaLUT: HTMLImageElement, + gammaLUT: HTMLImageElement, commonShaderSource: string, shaderSources: ShaderMap): TextDemoView { - return new TextDemoView(this, gammaLUT, commonShaderSource, shaderSources); + return new TextDemoView(this, areaLUT, gammaLUT, commonShaderSource, shaderSources); } protected fileLoaded(fileData: ArrayBuffer, builtinName: string | null) { @@ -386,10 +387,11 @@ class TextDemoView extends DemoView implements TextRenderContext { } constructor(appController: TextDemoController, + areaLUT: HTMLImageElement, gammaLUT: HTMLImageElement, commonShaderSource: string, shaderSources: ShaderMap) { - super(gammaLUT, commonShaderSource, shaderSources); + super(areaLUT, gammaLUT, commonShaderSource, shaderSources); this.appController = appController; this.renderer = new TextDemoRenderer(this); diff --git a/demo/client/src/text-renderer.ts b/demo/client/src/text-renderer.ts index 833b115b..4f8a968a 100644 --- a/demo/client/src/text-renderer.ts +++ b/demo/client/src/text-renderer.ts @@ -25,12 +25,12 @@ import {MAX_STEM_DARKENING_PIXELS_PER_EM, PathfinderFont, SimpleTextLayout} from import {UnitMetrics} from "./text"; import {unwrapNull} from './utils'; import {RenderContext, Timings} from "./view"; -import {AdaptiveStencilMeshAAAStrategy} from './xcaa-strategy'; +import {StencilAAAStrategy} from './xcaa-strategy'; interface AntialiasingStrategyTable { none: typeof NoAAStrategy; ssaa: typeof SSAAStrategy; - xcaa: typeof AdaptiveStencilMeshAAAStrategy; + xcaa: typeof StencilAAAStrategy; } const SQRT_1_2: number = 1.0 / Math.sqrt(2.0); @@ -38,10 +38,12 @@ const SQRT_1_2: number = 1.0 / Math.sqrt(2.0); const MIN_SCALE: number = 0.0025; const MAX_SCALE: number = 0.5; +export const MAX_SUBPIXEL_AA_FONT_SIZE: number = 48.0; + const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = { none: NoAAStrategy, ssaa: SSAAStrategy, - xcaa: AdaptiveStencilMeshAAAStrategy, + xcaa: StencilAAAStrategy, }; export interface TextRenderContext extends RenderContext { @@ -103,6 +105,10 @@ export abstract class TextRenderer extends Renderer { return 0.0; } + get allowSubpixelAA(): boolean { + return this.renderContext.fontSize <= MAX_SUBPIXEL_AA_FONT_SIZE; + } + protected get pixelsPerUnit(): number { return this.renderContext.fontSize / this.renderContext.font.opentypeFont.unitsPerEm; } @@ -188,6 +194,54 @@ export abstract class TextRenderer extends Renderer { return boundingRects; } + pathTransformsForObject(objectIndex: number): PathTransformBuffers { + const pathCount = this.pathCount; + const atlasGlyphs = this.renderContext.atlasGlyphs; + const pixelsPerUnit = this.pixelsPerUnit; + const rotationAngle = this.rotationAngle; + + // FIXME(pcwalton): This is a hack that tries to preserve the vertical extents of the glyph + // after stem darkening. It's better than nothing, but we should really do better. + // + // This hack seems to produce *better* results than what macOS does on sans-serif fonts; + // the ascenders and x-heights of the glyphs are pixel snapped, while they aren't on macOS. + // But we should really figure out what macOS does… + const ascender = this.renderContext.font.opentypeFont.ascender; + const emboldenAmount = this.emboldenAmount; + const stemDarkeningYScale = (ascender + emboldenAmount[1]) / ascender; + + const stemDarkeningOffset = glmatrix.vec2.clone(emboldenAmount); + glmatrix.vec2.scale(stemDarkeningOffset, stemDarkeningOffset, pixelsPerUnit); + glmatrix.vec2.scale(stemDarkeningOffset, stemDarkeningOffset, SQRT_1_2); + glmatrix.vec2.mul(stemDarkeningOffset, stemDarkeningOffset, [1, stemDarkeningYScale]); + + const transform = glmatrix.mat2d.create(); + const transforms = this.createPathTransformBuffers(pathCount); + + for (const glyph of atlasGlyphs) { + const pathID = glyph.pathID; + const atlasOrigin = glyph.calculateSubpixelOrigin(pixelsPerUnit); + + glmatrix.mat2d.identity(transform); + glmatrix.mat2d.translate(transform, transform, atlasOrigin); + glmatrix.mat2d.translate(transform, transform, stemDarkeningOffset); + glmatrix.mat2d.rotate(transform, transform, rotationAngle); + glmatrix.mat2d.scale(transform, + transform, + [pixelsPerUnit, pixelsPerUnit * stemDarkeningYScale]); + + transforms.st[pathID * 4 + 0] = transform[0]; + transforms.st[pathID * 4 + 1] = transform[3]; + transforms.st[pathID * 4 + 2] = transform[4]; + transforms.st[pathID * 4 + 3] = transform[5]; + + transforms.ext[pathID * 2 + 0] = transform[1]; + transforms.ext[pathID * 2 + 1] = transform[2]; + } + + return transforms; + } + protected createAtlasFramebuffer(): void { const atlasColorTexture = this.renderContext.atlas.ensureTexture(this.renderContext); this.atlasDepthTexture = createFramebufferDepthTexture(this.renderContext.gl, ATLAS_SIZE); @@ -250,54 +304,6 @@ export abstract class TextRenderer extends Renderer { return pathColors; } - protected pathTransformsForObject(objectIndex: number): PathTransformBuffers { - const pathCount = this.pathCount; - const atlasGlyphs = this.renderContext.atlasGlyphs; - const pixelsPerUnit = this.pixelsPerUnit; - const rotationAngle = this.rotationAngle; - - // FIXME(pcwalton): This is a hack that tries to preserve the vertical extents of the glyph - // after stem darkening. It's better than nothing, but we should really do better. - // - // This hack seems to produce *better* results than what macOS does on sans-serif fonts; - // the ascenders and x-heights of the glyphs are pixel snapped, while they aren't on macOS. - // But we should really figure out what macOS does… - const ascender = this.renderContext.font.opentypeFont.ascender; - const emboldenAmount = this.emboldenAmount; - const stemDarkeningYScale = (ascender + emboldenAmount[1]) / ascender; - - const stemDarkeningOffset = glmatrix.vec2.clone(emboldenAmount); - glmatrix.vec2.scale(stemDarkeningOffset, stemDarkeningOffset, pixelsPerUnit); - glmatrix.vec2.scale(stemDarkeningOffset, stemDarkeningOffset, SQRT_1_2); - glmatrix.vec2.mul(stemDarkeningOffset, stemDarkeningOffset, [1, stemDarkeningYScale]); - - const transform = glmatrix.mat2d.create(); - const transforms = this.createPathTransformBuffers(pathCount); - - for (const glyph of atlasGlyphs) { - const pathID = glyph.pathID; - const atlasOrigin = glyph.calculateSubpixelOrigin(pixelsPerUnit); - - glmatrix.mat2d.identity(transform); - glmatrix.mat2d.translate(transform, transform, atlasOrigin); - glmatrix.mat2d.translate(transform, transform, stemDarkeningOffset); - glmatrix.mat2d.rotate(transform, transform, rotationAngle); - glmatrix.mat2d.scale(transform, - transform, - [pixelsPerUnit, pixelsPerUnit * stemDarkeningYScale]); - - transforms.st[pathID * 4 + 0] = transform[0]; - transforms.st[pathID * 4 + 1] = transform[3]; - transforms.st[pathID * 4 + 2] = transform[4]; - transforms.st[pathID * 4 + 3] = transform[5]; - - transforms.ext[pathID * 2 + 0] = transform[1]; - transforms.ext[pathID * 2 + 1] = transform[2]; - } - - return transforms; - } - protected newTimingsReceived(): void { this.renderContext.newTimingsReceived(this.lastTimings); } diff --git a/demo/client/src/text.ts b/demo/client/src/text.ts index 68982b4c..268a9556 100644 --- a/demo/client/src/text.ts +++ b/demo/client/src/text.ts @@ -334,6 +334,7 @@ export class Hint { private useHinting: boolean; constructor(font: PathfinderFont, pixelsPerUnit: number, useHinting: boolean) { + useHinting = false; this.useHinting = useHinting; const os2Table = font.opentypeFont.tables.os2; diff --git a/demo/client/src/view.ts b/demo/client/src/view.ts index 8267671e..509ce024 100644 --- a/demo/client/src/view.ts +++ b/demo/client/src/view.ts @@ -143,6 +143,7 @@ export abstract class DemoView extends PathfinderView implements RenderContext { gl!: WebGLRenderingContext; shaderPrograms: ShaderMap; + areaLUT: HTMLImageElement; gammaLUT: HTMLImageElement; instancedArraysExt!: ANGLE_instanced_arrays; @@ -175,7 +176,8 @@ export abstract class DemoView extends PathfinderView implements RenderContext { private wantsScreenshot: boolean; /// NB: All subclasses are responsible for creating a renderer in their constructors. - constructor(gammaLUT: HTMLImageElement, + constructor(areaLUT: HTMLImageElement, + gammaLUT: HTMLImageElement, commonShaderSource: string, shaderSources: ShaderMap) { super(); @@ -188,6 +190,7 @@ export abstract class DemoView extends PathfinderView implements RenderContext { const shaderSource = this.compileShaders(commonShaderSource, shaderSources); this.shaderPrograms = this.linkShaders(shaderSource); + this.areaLUT = areaLUT; this.gammaLUT = gammaLUT; this.wantsScreenshot = false; @@ -372,6 +375,7 @@ export interface RenderContext { readonly colorAlphaFormat: ColorAlphaFormat; readonly shaderPrograms: ShaderMap; + readonly areaLUT: HTMLImageElement; readonly gammaLUT: HTMLImageElement; readonly quadPositionsBuffer: WebGLBuffer; diff --git a/demo/client/src/xcaa-strategy.ts b/demo/client/src/xcaa-strategy.ts index e06575aa..64da4dfb 100644 --- a/demo/client/src/xcaa-strategy.ts +++ b/demo/client/src/xcaa-strategy.ts @@ -65,8 +65,6 @@ export abstract class XCAAStrategy extends AntialiasingStrategy { protected supersampledFramebufferSize: glmatrix.vec2; protected destFramebufferSize: glmatrix.vec2; - protected subpixelAA: SubpixelAAType; - protected resolveVAO: WebGLVertexArrayObject | null; protected aaAlphaTexture: WebGLTexture | null = null; @@ -76,9 +74,7 @@ export abstract class XCAAStrategy extends AntialiasingStrategy { protected abstract get mightUseAAFramebuffer(): boolean; constructor(level: number, subpixelAA: SubpixelAAType) { - super(); - - this.subpixelAA = subpixelAA; + super(subpixelAA); this.supersampledFramebufferSize = glmatrix.vec2.create(); this.destFramebufferSize = glmatrix.vec2.create(); @@ -201,6 +197,7 @@ export abstract class XCAAStrategy extends AntialiasingStrategy { if (renderer.fgColor != null) gl.uniform4fv(resolveProgram.uniforms.uFGColor, renderer.fgColor); renderer.setTransformSTAndTexScaleUniformsForDest(resolveProgram.uniforms); + this.setSubpixelAAKernelUniform(renderer, resolveProgram.uniforms); this.setAdditionalStateForResolveIfNecessary(renderer, resolveProgram, 1); gl.drawElements(renderContext.gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0); renderContext.vertexArrayObjectExt.bindVertexArrayOES(null); @@ -273,6 +270,7 @@ export abstract class XCAAStrategy extends AntialiasingStrategy { renderer.pathTransformBufferTextures[0].st.bind(gl, uniforms, 1); this.pathBoundsBufferTextures[objectIndex].bind(gl, uniforms, 2); renderer.setHintsUniform(uniforms); + renderer.bindAreaLUT(4, uniforms); } protected setDepthAndBlendModeForResolve(renderContext: RenderContext): void { @@ -410,7 +408,7 @@ export class MCAAStrategy extends XCAAStrategy { const renderContext = renderer.renderContext; if (renderer.isMulticolor) return null; - if (this.subpixelAA !== 'none') + if (this.subpixelAA !== 'none' && renderer.allowSubpixelAA) return renderContext.shaderPrograms.xcaaMonoSubpixelResolve; return renderContext.shaderPrograms.xcaaMonoResolve; } @@ -651,8 +649,11 @@ export class StencilAAAStrategy extends XCAAStrategy { // FIXME(pcwalton): Only render the appropriate instances. const count = renderer.meshes[0].count('stencilSegments'); - renderContext.instancedArraysExt - .drawElementsInstancedANGLE(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, count); + for (let side = 0; side < 2; side++) { + gl.uniform1i(uniforms.uSide, side); + renderContext.instancedArraysExt + .drawElementsInstancedANGLE(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, count); + } renderContext.vertexArrayObjectExt.bindVertexArrayOES(null); } @@ -673,7 +674,7 @@ export class StencilAAAStrategy extends XCAAStrategy { protected getResolveProgram(renderer: Renderer): PathfinderShaderProgram | null { const renderContext = renderer.renderContext; - if (this.subpixelAA !== 'none') + if (this.subpixelAA !== 'none' && renderer.allowSubpixelAA) return renderContext.shaderPrograms.xcaaMonoSubpixelResolve; return renderContext.shaderPrograms.xcaaMonoResolve; } @@ -768,8 +769,6 @@ export class StencilAAAStrategy extends XCAAStrategy { renderContext.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aToNormal, 1); renderContext.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aPathID, 1); - // TODO(pcwalton): Normals. - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, renderContext.quadElementsBuffer); renderContext.vertexArrayObjectExt.bindVertexArrayOES(null); @@ -789,7 +788,7 @@ export class StencilAAAStrategy extends XCAAStrategy { /// darkening is enabled. /// /// FIXME(pcwalton): Share textures and FBOs between the two strategies. -export class AdaptiveStencilMeshAAAStrategy implements AntialiasingStrategy { +export class AdaptiveStencilMeshAAAStrategy extends AntialiasingStrategy { private meshStrategy: MCAAStrategy; private stencilStrategy: StencilAAAStrategy; @@ -802,6 +801,7 @@ export class AdaptiveStencilMeshAAAStrategy implements AntialiasingStrategy { } constructor(level: number, subpixelAA: SubpixelAAType) { + super(subpixelAA); this.meshStrategy = new MCAAStrategy(level, subpixelAA); this.stencilStrategy = new StencilAAAStrategy(level, subpixelAA); } diff --git a/resources/textures/area-lut.png b/resources/textures/area-lut.png new file mode 100644 index 00000000..b54d669f Binary files /dev/null and b/resources/textures/area-lut.png differ diff --git a/shaders/gles2/common.inc.glsl b/shaders/gles2/common.inc.glsl index 983ea7d9..8016f25a 100644 --- a/shaders/gles2/common.inc.glsl +++ b/shaders/gles2/common.inc.glsl @@ -13,9 +13,16 @@ #extension GL_EXT_frag_depth : require #extension GL_OES_standard_derivatives : require -#define LCD_FILTER_FACTOR_0 (86.0 / 255.0) -#define LCD_FILTER_FACTOR_1 (77.0 / 255.0) -#define LCD_FILTER_FACTOR_2 (8.0 / 255.0) +#define FREETYPE_LCD_FILTER_FACTOR_0 0.337254902 +#define FREETYPE_LCD_FILTER_FACTOR_1 0.301960784 +#define FREETYPE_LCD_FILTER_FACTOR_2 0.031372549 + +// These intentionally do not precisely match what Core Graphics does (a Lanczos function), because +// we don't want any ringing artefacts. +#define CG_LCD_FILTER_FACTOR_0 0.286651906 +#define CG_LCD_FILTER_FACTOR_1 0.221434336 +#define CG_LCD_FILTER_FACTOR_2 0.102074051 +#define CG_LCD_FILTER_FACTOR_3 0.033165660 #define MAX_PATHS 65536 @@ -226,12 +233,38 @@ vec2 solveCurveT(float p0x, float p1x, float p2x, vec2 x) { /// /// The algorithm should be identical to that of FreeType: /// https://www.freetype.org/freetype2/docs/reference/ft2-lcd_filtering.html -float lcdFilter(float shadeL2, float shadeL1, float shade0, float shadeR1, float shadeR2) { - return LCD_FILTER_FACTOR_2 * shadeL2 + - LCD_FILTER_FACTOR_1 * shadeL1 + - LCD_FILTER_FACTOR_0 * shade0 + - LCD_FILTER_FACTOR_1 * shadeR1 + - LCD_FILTER_FACTOR_2 * shadeR2; +float freetypeLCDFilter(float shadeL2, float shadeL1, float shade0, float shadeR1, float shadeR2) { + return FREETYPE_LCD_FILTER_FACTOR_2 * shadeL2 + + FREETYPE_LCD_FILTER_FACTOR_1 * shadeL1 + + FREETYPE_LCD_FILTER_FACTOR_0 * shade0 + + FREETYPE_LCD_FILTER_FACTOR_1 * shadeR1 + + FREETYPE_LCD_FILTER_FACTOR_2 * shadeR2; +} + +float sample1Tap(sampler2D source, vec2 center, float offset) { + return texture2D(source, vec2(center.x + offset, center.y)).r; +} + +void sample9Tap(out vec4 outShadesL, + out float outShadeC, + out vec4 outShadesR, + sampler2D source, + vec2 center, + float onePixel, + vec4 kernel) { + outShadesL = vec4(kernel.x > 0.0 ? sample1Tap(source, center, -4.0 * onePixel) : 0.0, + sample1Tap(source, center, -3.0 * onePixel), + sample1Tap(source, center, -2.0 * onePixel), + sample1Tap(source, center, -1.0 * onePixel)); + outShadeC = sample1Tap(source, center, 0.0); + outShadesR = vec4(sample1Tap(source, center, 1.0 * onePixel), + sample1Tap(source, center, 2.0 * onePixel), + sample1Tap(source, center, 3.0 * onePixel), + kernel.x > 0.0 ? sample1Tap(source, center, 4.0 * onePixel) : 0.0); +} + +float convolve7Tap(vec4 shades0, vec3 shades1, vec4 kernel) { + return dot(shades0, kernel) + dot(shades1, kernel.zyx); } float gammaCorrectChannel(float fgColor, float bgColor, sampler2D gammaLUT) { diff --git a/shaders/gles2/ssaa-subpixel-resolve.fs.glsl b/shaders/gles2/ssaa-subpixel-resolve.fs.glsl index bbe3386b..e0e31bf4 100644 --- a/shaders/gles2/ssaa-subpixel-resolve.fs.glsl +++ b/shaders/gles2/ssaa-subpixel-resolve.fs.glsl @@ -18,27 +18,19 @@ precision mediump float; uniform sampler2D uSource; /// The dimensions of the alpha coverage texture, in texels. uniform ivec2 uSourceDimensions; +uniform vec4 uKernel; varying vec2 vTexCoord; -float sampleSource(float deltaX) { - return texture2D(uSource, vec2(vTexCoord.x + deltaX, vTexCoord.y)).r; -} - void main() { float onePixel = 1.0 / float(uSourceDimensions.x); + vec4 shadesL, shadesR; + float shadeC; + sample9Tap(shadesL, shadeC, shadesR, uSource, vTexCoord, onePixel, uKernel); - float shade0 = sampleSource(0.0); - vec3 shadeL = vec3(sampleSource(-1.0 * onePixel), - sampleSource(-2.0 * onePixel), - sampleSource(-3.0 * onePixel)); - vec3 shadeR = vec3(sampleSource(1.0 * onePixel), - sampleSource(2.0 * onePixel), - sampleSource(3.0 * onePixel)); + vec3 shades = vec3(convolve7Tap(shadesL, vec3(shadeC, shadesR.xy), uKernel), + convolve7Tap(vec4(shadesL.yzw, shadeC), shadesR.xyz, uKernel), + convolve7Tap(vec4(shadesL.zw, shadeC, shadesR.x), shadesR.yzw, uKernel)); - vec3 color = vec3(lcdFilter(shadeL.z, shadeL.y, shadeL.x, shade0, shadeR.x), - lcdFilter(shadeL.y, shadeL.x, shade0, shadeR.x, shadeR.y), - lcdFilter(shadeL.x, shade0, shadeR.x, shadeR.y, shadeR.z)); - - gl_FragColor = vec4(color, 1.0); + gl_FragColor = vec4(shades, 1.0); } diff --git a/shaders/gles2/stencil-aaa.fs.glsl b/shaders/gles2/stencil-aaa.fs.glsl index ba3a5a72..f0097163 100644 --- a/shaders/gles2/stencil-aaa.fs.glsl +++ b/shaders/gles2/stencil-aaa.fs.glsl @@ -8,42 +8,33 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -varying vec3 vUV; -varying vec3 vXDist; +precision mediump float; + +uniform sampler2D uAreaLUT; + +varying vec2 vFrom; +varying vec2 vCtrl; +varying vec2 vTo; void main() { // Unpack. - vec3 uv = vUV; - vec2 dUVDX = dFdx(uv.xy), dUVDY = dFdy(uv.xy); - vec3 xDist = vXDist; - vec2 dXDistDX = dFdx(xDist.xz); + vec2 from = vFrom, ctrl = vCtrl, to = vTo; - // Calculate X distances between endpoints (x02, x10, and x21 respectively). - vec3 vDist = xDist - xDist.zxy; + // Determine winding, and sort into a consistent order so we only need to find one root below. + bool winding = from.x < to.x; + vec2 left = winding ? from : to, right = winding ? to : from; + vec2 v0 = ctrl - left, v1 = right - ctrl; - // Compute winding number and convexity. - bool inCurve = insideCurve(uv); - float openWinding = fastSign(-vDist.x); - float convex = uv.z != 0.0 ? uv.z : fastSign(vDist.x * dUVDY.y); + // Shoot a vertical ray toward the curve. + vec2 window = clamp(vec2(from.x, to.x), -0.5, 0.5); + float offset = mix(window.x, window.y, 0.5) - left.x; + float t = offset / (v0.x + sqrt(v1.x * offset - v0.x * (offset - v0.x))); - // Compute open rect area. - vec2 areas = clamp(xDist.xz / dXDistDX, -0.5, 0.5); - float openRectArea = openWinding * (areas.y - areas.x); + // Compute position and derivative to form a line approximation. + float y = mix(mix(left.y, ctrl.y, t), mix(ctrl.y, right.y, t), t); + float d = mix(v0.y, v1.y, t) / mix(v0.x, v1.x, t); - // Compute closed rect area and winding, if necessary. - float closedRectArea = 0.0, closedWinding = 0.0; - if (inCurve && vDist.y * vDist.z < 0.0) { - closedRectArea = 0.5 - fastSign(vDist.y) * (vDist.x * vDist.y < 0.0 ? areas.y : areas.x); - closedWinding = fastSign(vDist.y * dUVDY.y); - } - - // Calculate approximate area of the curve covering this pixel square. - float curveArea = estimateArea(signedDistanceToCurve(uv.xy, dUVDX, dUVDY, inCurve)); - - // Calculate alpha. - vec2 alpha = vec2(openWinding, closedWinding) * 0.5 + convex * curveArea; - alpha *= vec2(openRectArea, closedRectArea); - - // Finish up. - gl_FragColor = vec4(alpha.x + alpha.y); + // Look up area under that line, and scale horizontally to the window size. + float dX = window.x - window.y; + gl_FragColor = vec4(texture2D(uAreaLUT, vec2(y + 8.0, abs(d * dX)) / 16.0).r * dX); } diff --git a/shaders/gles2/stencil-aaa.vs.glsl b/shaders/gles2/stencil-aaa.vs.glsl index 69046af7..b9ad03bb 100644 --- a/shaders/gles2/stencil-aaa.vs.glsl +++ b/shaders/gles2/stencil-aaa.vs.glsl @@ -20,6 +20,7 @@ uniform ivec2 uPathTransformSTDimensions; uniform sampler2D uPathTransformST; uniform ivec2 uPathTransformExtDimensions; uniform sampler2D uPathTransformExt; +uniform int uSide; attribute vec2 aTessCoord; attribute vec2 aFromPosition; @@ -30,8 +31,9 @@ attribute vec2 aCtrlNormal; attribute vec2 aToNormal; attribute float aPathID; -varying vec3 vUV; -varying vec3 vXDist; +varying vec2 vFrom; +varying vec2 vCtrl; +varying vec2 vTo; void main() { // Unpack. @@ -39,14 +41,14 @@ void main() { int pathID = int(aPathID); // Hint positions. - vec2 fromPosition = hintPosition(aFromPosition, uHints); - vec2 ctrlPosition = hintPosition(aCtrlPosition, uHints); - vec2 toPosition = hintPosition(aToPosition, uHints); + vec2 from = hintPosition(aFromPosition, uHints); + vec2 ctrl = hintPosition(aCtrlPosition, uHints); + vec2 to = hintPosition(aToPosition, uHints); // Embolden as necessary. - fromPosition -= aFromNormal * emboldenAmount; - ctrlPosition -= aCtrlNormal * emboldenAmount; - toPosition -= aToNormal * emboldenAmount; + from -= aFromNormal * emboldenAmount; + ctrl -= aCtrlNormal * emboldenAmount; + to -= aToNormal * emboldenAmount; // Fetch transform. vec2 transformExt; @@ -63,9 +65,9 @@ void main() { mat2 transformLinear = globalTransformLinear * localTransformLinear; // Perform the linear component of the transform (everything but translation). - fromPosition = quantize(transformLinear * fromPosition); - ctrlPosition = quantize(transformLinear * ctrlPosition); - toPosition = quantize(transformLinear * toPosition); + from = transformLinear * from; + ctrl = transformLinear * ctrl; + to = transformLinear * to; // Choose correct quadrant for rotation. vec4 bounds = fetchFloat4Data(uPathBounds, pathID, uPathBoundsDimensions); @@ -73,53 +75,48 @@ void main() { vec2 corner = transformLinear * vec2(fillVector.x < 0.0 ? bounds.z : bounds.x, fillVector.y < 0.0 ? bounds.y : bounds.w); - // Compute edge vectors. - vec2 v02 = toPosition - fromPosition; - vec2 v01 = ctrlPosition - fromPosition, v21 = ctrlPosition - toPosition; - - // Compute area of convex hull (w). Change from curve to line if appropriate. - float w = det2(mat2(v01, v02)); - float sqLen01 = dot(v01, v01), sqLen02 = dot(v02, v02), sqLen21 = dot(v21, v21); - float hullHeight = abs(w * inversesqrt(sqLen02)); - float minCtrlSqLen = sqLen02 * 0.01; - if (sqLen01 < minCtrlSqLen || sqLen21 < minCtrlSqLen || hullHeight < 0.0001) { - w = 0.0; - v01 = vec2(0.5, abs(v02.y) >= 0.01 ? 0.0 : 0.5) * v02.xx; + // Compute edge vectors. De Casteljau subdivide if necessary. + vec2 v01 = ctrl - from, v12 = to - ctrl; + float t = clamp(v01.x / (v01.x - v12.x), 0.0, 1.0); + vec2 ctrl0 = mix(from, ctrl, t), ctrl1 = mix(ctrl, to, t); + vec2 mid = mix(ctrl0, ctrl1, t); + if (uSide == 0) { + from = mid; + ctrl = ctrl1; + } else { + ctrl = ctrl0; + to = mid; } // Compute position and dilate. If too thin, discard to avoid artefacts. - vec2 dilation = vec2(0.0), position; + vec2 dilation, position; + bool zeroArea = abs(from.x - to.x) < 0.00001; if (aTessCoord.x < 0.5) { - position.x = min(min(fromPosition.x, toPosition.x), ctrlPosition.x); - dilation.x = -1.0; + position.x = min(min(from.x, to.x), ctrl.x); + dilation.x = zeroArea ? 0.0 : -1.0; } else { - position.x = max(max(fromPosition.x, toPosition.x), ctrlPosition.x); - dilation.x = 1.0; + position.x = max(max(from.x, to.x), ctrl.x); + dilation.x = zeroArea ? 0.0 : 1.0; } if (aTessCoord.y < 0.5) { - position.y = min(min(fromPosition.y, toPosition.y), ctrlPosition.y); - dilation.y = -1.0; + position.y = min(min(from.y, to.y), ctrl.y); + dilation.y = zeroArea ? 0.0 : -1.0; } else { position.y = corner.y; + dilation.y = 0.0; } - position += 2.0 * dilation / vec2(uFramebufferSize); - - // Compute UV using Cramer's rule. - // https://gamedev.stackexchange.com/a/63203 - vec2 v03 = position - fromPosition; - vec3 uv = vec3(0.0, det2(mat2(v01, v03)), sign(w)); - uv.x = uv.y + 0.5 * det2(mat2(v03, v02)); - uv.xy /= det2(mat2(v01, v02)); - - // Compute X distances. - vec3 xDist = position.x - vec3(fromPosition.x, ctrlPosition.x, toPosition.x); + position += dilation * 2.0 / vec2(uFramebufferSize); // Compute final position and depth. - position += uTransformST.zw + globalTransformLinear * transformST.zw; + vec2 offsetPosition = position + uTransformST.zw + globalTransformLinear * transformST.zw; float depth = convertPathIndexToViewportDepthValue(pathID); + // Compute transformed framebuffer size. + vec2 framebufferSizeVector = 0.5 * vec2(uFramebufferSize); + // Finish up. - gl_Position = vec4(position, depth, 1.0); - vUV = uv; - vXDist = xDist; + gl_Position = vec4(offsetPosition, depth, 1.0); + vFrom = (from - position) * framebufferSizeVector; + vCtrl = (ctrl - position) * framebufferSizeVector; + vTo = (to - position) * framebufferSizeVector; } diff --git a/shaders/gles2/xcaa-mono-resolve.fs.glsl b/shaders/gles2/xcaa-mono-resolve.fs.glsl index 59f65bfa..41fe70c3 100644 --- a/shaders/gles2/xcaa-mono-resolve.fs.glsl +++ b/shaders/gles2/xcaa-mono-resolve.fs.glsl @@ -22,6 +22,6 @@ uniform sampler2D uAAAlpha; varying vec2 vTexCoord; void main() { - float alpha = clamp(texture2D(uAAAlpha, vTexCoord).r, 0.0, 1.0); + float alpha = abs(texture2D(uAAAlpha, vTexCoord).r); gl_FragColor = mix(uBGColor, uFGColor, alpha); } diff --git a/shaders/gles2/xcaa-mono-subpixel-resolve.fs.glsl b/shaders/gles2/xcaa-mono-subpixel-resolve.fs.glsl index 70973020..8eed946f 100644 --- a/shaders/gles2/xcaa-mono-subpixel-resolve.fs.glsl +++ b/shaders/gles2/xcaa-mono-subpixel-resolve.fs.glsl @@ -22,27 +22,19 @@ uniform vec4 uFGColor; uniform sampler2D uAAAlpha; /// The dimensions of the alpha coverage texture, in texels. uniform ivec2 uAAAlphaDimensions; +uniform vec4 uKernel; varying vec2 vTexCoord; -float sampleSource(float deltaX) { - return abs(texture2D(uAAAlpha, vec2(vTexCoord.s + deltaX, vTexCoord.y)).r); -} - void main() { float onePixel = 1.0 / float(uAAAlphaDimensions.x); + vec4 shadesL, shadesR; + float shadeC; + sample9Tap(shadesL, shadeC, shadesR, uAAAlpha, vTexCoord, onePixel, uKernel); - float shade0 = sampleSource(0.0); - vec3 shadeL = vec3(sampleSource(-1.0 * onePixel), - sampleSource(-2.0 * onePixel), - sampleSource(-3.0 * onePixel)); - vec3 shadeR = vec3(sampleSource(1.0 * onePixel), - sampleSource(2.0 * onePixel), - sampleSource(3.0 * onePixel)); - - vec3 shades = vec3(lcdFilter(shadeL.z, shadeL.y, shadeL.x, shade0, shadeR.x), - lcdFilter(shadeL.y, shadeL.x, shade0, shadeR.x, shadeR.y), - lcdFilter(shadeL.x, shade0, shadeR.x, shadeR.y, shadeR.z)); + vec3 shades = vec3(convolve7Tap(shadesL, vec3(shadeC, shadesR.xy), uKernel), + convolve7Tap(vec4(shadesL.yzw, shadeC), shadesR.xyz, uKernel), + convolve7Tap(vec4(shadesL.zw, shadeC, shadesR.x), shadesR.yzw, uKernel)); vec3 color = mix(uBGColor.rgb, uFGColor.rgb, shades); float alpha = any(greaterThan(shades, vec3(0.0))) ? uFGColor.a : uBGColor.a; diff --git a/utils/area-lut/Cargo.toml b/utils/area-lut/Cargo.toml new file mode 100644 index 00000000..03094d96 --- /dev/null +++ b/utils/area-lut/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "area-lut" +version = "0.1.0" +authors = ["Patrick Walton "] + +[dependencies] +clap = "2.30" +euclid = "0.17" +image = "0.18" diff --git a/utils/area-lut/src/main.rs b/utils/area-lut/src/main.rs new file mode 100644 index 00000000..7e258c98 --- /dev/null +++ b/utils/area-lut/src/main.rs @@ -0,0 +1,91 @@ +// pathfinder/area-lut/src/main.rs + +extern crate clap; +extern crate euclid; +extern crate image; + +use clap::{App, Arg}; +use euclid::Point2D; +use image::{ImageBuffer, Luma}; +use std::f32; +use std::path::Path; + +const WIDTH: u32 = 256; +const HEIGHT: u32 = 256; + +fn solve_line_y(p0: &Point2D, p1: &Point2D, y: f32) -> Point2D { + let m = (p1.y - p0.y) / (p1.x - p0.x); + Point2D::new(p0.x - (p0.y - y) / m, y) +} + +fn area_tri(p0: Point2D, p1: Point2D) -> f32 { + 0.5 * (p1.x - p0.x) * (p0.y - p1.y) +} + +fn area_rect(p0: Point2D, p1: Point2D) -> f32 { + (p1.x - p0.x) * (p0.y - p1.y) +} + +fn main() { + let app = App::new("Pathfinder Area LUT Generator") + .version("0.1") + .author("The Pathfinder Project Developers") + .about("Generates area lookup tables for use with Pathfinder") + .arg(Arg::with_name("OUTPUT-PATH").help("The `.png` image to produce") + .required(true) + .index(1)); + + let matches = app.get_matches(); + let image = ImageBuffer::from_fn(WIDTH, HEIGHT, |u, v| { + if u == 0 { + return Luma([255]) + } + if u == WIDTH - 1 { + return Luma([0]) + } + + let y = ((u as f32) - (WIDTH / 2) as f32) / 16.0; + let dydx = -(v as f32) / 16.0; + + let (x_left, x_right) = (-0.5, 0.5); + let (y_left, y_right) = (dydx * x_left + y, dydx * x_right + y); + + let (p0, p1) = (Point2D::new(x_left, y_left), Point2D::new(x_right, y_right)); + let p2 = solve_line_y(&p0, &p1, -0.5); + let p3 = Point2D::new(p1.x, -0.5); + let p4 = solve_line_y(&p0, &p1, 0.5); + let p7 = Point2D::new(p1.x, 0.5); + + let alpha; + if p0.y > 0.5 { + if p1.y < -0.5 { + // Case 0 + alpha = area_tri(p0, p1) - area_tri(p2, p1) - area_rect(p0, p7) + area_tri(p0, p4); + } else if p1.y < 0.5 { + // Case 6 + alpha = area_tri(p0, p1) - area_rect(p0, p7) + area_tri(p0, p4); + } else { + // Case 3 + alpha = 0.0; + } + } else if p0.y > -0.5 { + if p1.y < -0.5 { + // Case 1 + alpha = area_tri(p0, p1) - area_tri(p2, p1) - area_rect(p0, p7); + } else { + // Case 4 + alpha = area_tri(p0, p1) - area_rect(p0, p7); + } + } else { + // Case 2 + alpha = -area_rect(p0, p7) + area_rect(p0, p3); + } + + Luma([f32::round(alpha * 255.0) as u8]) + }); + + let output_path = matches.value_of("OUTPUT-PATH").unwrap(); + let output_path = Path::new(output_path); + + image.save(&output_path).unwrap(); +}