Use a lookup table to do area calculations instead of Loop-Blinn-style

distance-to-edge for stencil analytic antialiasing.

This improves the rendering quality of stencil AAA significantly.

Additionally, this adds an approximation of Core Graphics' (macOS')
defringing filter.

Closes #73.
This commit is contained in:
Patrick Walton 2018-03-09 09:20:27 -08:00
parent e851afd5d9
commit 6e13fb171c
26 changed files with 517 additions and 303 deletions

View File

@ -4,6 +4,7 @@ members = [
"partitioner",
"path-utils",
"demo/server",
"utils/area-lut",
"utils/frontend",
"utils/gamma-lut",
]

View File

@ -51,8 +51,18 @@
<option value="xcaa">XCAA</option>
</select>
</div>
<div class="form-group row justify-content-between">
{{>partials/switch.html id="pf-subpixel-aa" title="Subpixel AA"}}
<div class="form-group">
<label for="pf-subpixel-aa-select">Subpixel AA</label>
<select id="pf-subpixel-aa-select"
class="form-control custom-select">
<option value="none">None</option>
<option value="freetype" selected>
FreeType-style
</option>
<option value="core-graphics">
Core Graphics-style
</option>
</select>
</div>
</form>
</div>

View File

@ -67,9 +67,14 @@
<option value="slight" selected>Slight</option>
</select>
</div>
<div class="form-group row justify-content-between">
{{>partials/switch.html id="pf-subpixel-aa"
title="Subpixel AA"}}
<div class="form-group">
<label for="pf-subpixel-aa-select">Subpixel AA</label>
<select id="pf-subpixel-aa-select"
class="form-control custom-select">
<option value="none">None</option>
<option value="freetype" selected>FreeType-style</option>
<option value="core-graphics">Core Graphics-style</option>
</select>
</div>
<div class="form-group row justify-content-between">
{{>partials/switch.html id="pf-gamma-correction"

View File

@ -155,11 +155,12 @@ class ThreeDController extends DemoAppController<ThreeDView> {
this.monumentPromise.then(monument => this.layoutMonument(fileData, monument));
}
protected createView(gammaLUT: HTMLImageElement,
protected createView(areaLUT: HTMLImageElement,
gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>):
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<ShaderProgramSource>) {
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<Float32Array> {
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<Float32Array> {
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;
}

View File

@ -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();
}

View File

@ -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<View extends DemoView> extends AppContro
implements Switches {
view!: Promise<View>;
subpixelAASwitchInputs: SwitchInputs | null = null;
gammaCorrectionSwitchInputs: SwitchInputs | null = null;
stemDarkeningSwitchInputs: SwitchInputs | null = null;
@ -120,6 +111,7 @@ export abstract class DemoAppController<View extends DemoView> 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<View extends DemoView> 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<View extends DemoView> 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<View extends DemoView> 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<View extends DemoView> extends AppContro
// Overridden by subclasses.
}
protected abstract createView(gammaLUT: HTMLImageElement,
protected abstract createView(areaLUT: HTMLImageElement,
gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>):
View;
@ -331,8 +341,8 @@ export abstract class DemoAppController<View extends DemoView> extends AppContro
}, false);
}
private loadGammaLUT(): Promise<HTMLImageElement> {
return window.fetch(GAMMA_LUT_URI)
private loadTexture(uri: string): Promise<HTMLImageElement> {
return window.fetch(uri)
.then(response => response.blob())
.then(blob => {
const imgElement = document.createElement('img');

View File

@ -188,19 +188,20 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
}
}
protected createView(gammaLUT: HTMLImageElement,
protected createView(areaLUT: HTMLImageElement,
gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>):
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<ShaderProgramSource>) {
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<Float32Array> {
pathTransformsForObject(objectIndex: number): PathTransformBuffers<Float32Array> {
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<void> {
return 'directCurve';
}

View File

@ -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<ReferenceTestView> {
context.putImageData(imageData, 0, 0);
}
protected createView(gammaLUT: HTMLImageElement,
protected createView(areaLUT: HTMLImageElement,
gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>):
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<ReferenceTestView> {
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<ShaderProgramSource>) {
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<Float32Array> {
pathTransformsForObject(objectIndex: number): PathTransformBuffers<Float32Array> {
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<void> {
return 'directCurve';
}

View File

@ -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<Float32Array>;
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<Float32Array>;
protected abstract directCurveProgramName(): keyof ShaderMap<void>;
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 {

View File

@ -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;

View File

@ -48,11 +48,12 @@ class SVGDemoController extends DemoAppController<SVGDemoView> {
});
}
protected createView(gammaLUT: HTMLImageElement,
protected createView(areaLUT: HTMLImageElement,
gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>):
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<ShaderProgramSource>) {
super(gammaLUT, commonShaderSource, shaderSources);
super(areaLUT, gammaLUT, commonShaderSource, shaderSources);
this.appController = appController;
this.renderer = new SVGDemoRenderer(this, {sizeToFit: true});

View File

@ -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<Float32Array> {
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<Float32Array> {
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):

View File

@ -227,11 +227,12 @@ class TextDemoController extends DemoAppController<TextDemoView> {
}
}
protected createView(gammaLUT: HTMLImageElement,
protected createView(areaLUT: HTMLImageElement,
gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>):
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<ShaderProgramSource>) {
super(gammaLUT, commonShaderSource, shaderSources);
super(areaLUT, gammaLUT, commonShaderSource, shaderSources);
this.appController = appController;
this.renderer = new TextDemoRenderer(this);

View File

@ -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<Float32Array> {
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<Float32Array> {
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);
}

View File

@ -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;

View File

@ -143,6 +143,7 @@ export abstract class DemoView extends PathfinderView implements RenderContext {
gl!: WebGLRenderingContext;
shaderPrograms: ShaderMap<PathfinderShaderProgram>;
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<ShaderProgramSource>) {
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<PathfinderShaderProgram>;
readonly areaLUT: HTMLImageElement;
readonly gammaLUT: HTMLImageElement;
readonly quadPositionsBuffer: WebGLBuffer;

View File

@ -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');
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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -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) {

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;

View File

@ -0,0 +1,9 @@
[package]
name = "area-lut"
version = "0.1.0"
authors = ["Patrick Walton <pcwalton@mimiga.net>"]
[dependencies]
clap = "2.30"
euclid = "0.17"
image = "0.18"

View File

@ -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<f32>, p1: &Point2D<f32>, y: f32) -> Point2D<f32> {
let m = (p1.y - p0.y) / (p1.x - p0.x);
Point2D::new(p0.x - (p0.y - y) / m, y)
}
fn area_tri(p0: Point2D<f32>, p1: Point2D<f32>) -> f32 {
0.5 * (p1.x - p0.x) * (p0.y - p1.y)
}
fn area_rect(p0: Point2D<f32>, p1: Point2D<f32>) -> 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();
}