diff --git a/demo/client/src/3d-demo.ts b/demo/client/src/3d-demo.ts index 7a9abfe0..ced28a7d 100644 --- a/demo/client/src/3d-demo.ts +++ b/demo/client/src/3d-demo.ts @@ -20,6 +20,7 @@ import PathfinderBufferTexture from "./buffer-texture"; import {PerspectiveCamera} from "./camera"; import {UniformMap} from './gl-utils'; import {PathfinderMeshData} from "./meshes"; +import {Renderer} from './renderer'; import {ShaderMap, ShaderProgramSource} from "./shader-loader"; import SSAAStrategy from "./ssaa-strategy"; import {BUILTIN_FONT_URI, ExpandedMeshData} from "./text"; @@ -252,78 +253,171 @@ class ThreeDController extends DemoAppController { return this.baseMeshes.expand([glyphIndex + 1]); }); - this.view.then(view => { - view.uploadPathColors(this.expandedMeshes.length); - view.uploadPathTransforms(this.expandedMeshes.length); - view.attachMeshes(this.expandedMeshes); - }); + this.view.then(view => view.attachMeshes(this.expandedMeshes)); }); } } class ThreeDView extends DemoView { - destFramebuffer: WebGLFramebuffer | null = null; + renderer: ThreeDRenderer; - camera: PerspectiveCamera; + appController: ThreeDController; - protected usedSizeFactor: glmatrix.vec2 = glmatrix.vec2.clone([1.0, 1.0]); - - protected directCurveProgramName: keyof ShaderMap = 'direct3DCurve'; - protected directInteriorProgramName: keyof ShaderMap = 'direct3DInterior'; - - protected depthFunction: number = this.gl.LESS; - - protected get pathIDsAreInstanced(): boolean { - return true; + protected get camera(): PerspectiveCamera { + return this.renderer.camera; } - private _scale: number; - - private appController: ThreeDController; - - private cubeVertexPositionBuffer: WebGLBuffer; - private cubeIndexBuffer: WebGLBuffer; - constructor(appController: ThreeDController, commonShaderSource: string, shaderSources: ShaderMap) { super(commonShaderSource, shaderSources); this.appController = appController; + this.renderer = new ThreeDRenderer(this); - this.camera = new PerspectiveCamera(this.canvas, { + this.resizeToFit(true); + } +} + +class ThreeDRenderer extends Renderer { + renderContext: ThreeDView; + + camera: PerspectiveCamera; + + get destFramebuffer(): WebGLFramebuffer | null { + return null; + } + + get destAllocatedSize(): glmatrix.vec2 { + return glmatrix.vec2.clone([ + this.renderContext.canvas.width, + this.renderContext.canvas.height, + ]); + } + + get destUsedSize(): glmatrix.vec2 { + return this.destAllocatedSize; + } + + protected get directCurveProgramName(): keyof ShaderMap { + return 'direct3DCurve'; + } + + protected get directInteriorProgramName(): keyof ShaderMap { + return 'direct3DInterior'; + } + + protected get depthFunction(): number { + return this.renderContext.gl.LESS; + } + + protected get pathIDsAreInstanced(): boolean { + return true; + } + + protected get worldTransform() { + return this.calculateWorldTransform(glmatrix.vec3.create(), TEXT_SCALE); + } + + protected get usedSizeFactor(): glmatrix.vec2 { + return glmatrix.vec2.clone([1.0, 1.0]); + } + + private cubeVertexPositionBuffer: WebGLBuffer; + private cubeIndexBuffer: WebGLBuffer; + + constructor(renderContext: ThreeDView) { + super(renderContext); + + this.camera = new PerspectiveCamera(renderContext.canvas, { innerCollisionExtent: MONUMENT_SCALE[0], }); - this.camera.onChange = () => this.setDirty(); + this.camera.onChange = () => renderContext.setDirty(); - this.cubeVertexPositionBuffer = unwrapNull(this.gl.createBuffer()); - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.cubeVertexPositionBuffer); - this.gl.bufferData(this.gl.ARRAY_BUFFER, CUBE_VERTEX_POSITIONS, this.gl.STATIC_DRAW); + this.cubeVertexPositionBuffer = unwrapNull(renderContext.gl.createBuffer()); + renderContext.gl.bindBuffer(renderContext.gl.ARRAY_BUFFER, this.cubeVertexPositionBuffer); + renderContext.gl.bufferData(renderContext.gl.ARRAY_BUFFER, + CUBE_VERTEX_POSITIONS, + renderContext.gl.STATIC_DRAW); - this.cubeIndexBuffer = unwrapNull(this.gl.createBuffer()); - this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.cubeIndexBuffer); - this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, CUBE_INDICES, this.gl.STATIC_DRAW); + this.cubeIndexBuffer = unwrapNull(renderContext.gl.createBuffer()); + renderContext.gl.bindBuffer(renderContext.gl.ELEMENT_ARRAY_BUFFER, this.cubeIndexBuffer); + renderContext.gl.bufferData(renderContext.gl.ELEMENT_ARRAY_BUFFER, + CUBE_INDICES, + renderContext.gl.STATIC_DRAW); } - setHintsUniform(uniforms: UniformMap): void { - this.gl.uniform4f(uniforms.uHints, 0, 0, 0, 0); - } + attachMeshes(expandedMeshes: PathfinderMeshData[]) { + super.attachMeshes(expandedMeshes); - pathBoundingRects(objectIndex: number): Float32Array { - panic("ThreeDView.pathBoundingRects(): TODO"); - return glmatrix.vec4.create(); + this.uploadPathColors(expandedMeshes.length); + this.uploadPathTransforms(expandedMeshes.length); } pathCountForObject(objectIndex: number): number { - return this.appController.meshDescriptors[objectIndex].positions.length; + return this.renderContext.appController.meshDescriptors[objectIndex].positions.length; } + pathBoundingRects(objectIndex: number): Float32Array { + panic("ThreeDRenderer.pathBoundingRects(): TODO"); + return glmatrix.vec4.create(); + } + + setHintsUniform(uniforms: UniformMap): void { + this.renderContext.gl.uniform4f(uniforms.uHints, 0, 0, 0, 0); + } + + protected drawSceneryIfNecessary(): void { + // Set up the cube VBO. + const shaderProgram = this.renderContext.shaderPrograms.demo3DMonument; + this.renderContext.gl.useProgram(shaderProgram.program); + this.renderContext.gl.bindBuffer(this.renderContext.gl.ARRAY_BUFFER, + this.cubeVertexPositionBuffer); + this.renderContext.gl.bindBuffer(this.renderContext.gl.ELEMENT_ARRAY_BUFFER, + this.cubeIndexBuffer); + this.renderContext.gl.vertexAttribPointer(shaderProgram.attributes.aPosition, + 3, + this.renderContext.gl.FLOAT, + false, + 0, + 0); + this.renderContext.gl.enableVertexAttribArray(shaderProgram.attributes.aPosition); + + // Set uniforms for the monument. + const transform = this.calculateWorldTransform(MONUMENT_TRANSLATION, MONUMENT_SCALE); + this.renderContext.gl.uniformMatrix4fv(shaderProgram.uniforms.uTransform, + false, + transform); + this.renderContext.gl.uniform4f(shaderProgram.uniforms.uColor, + MONUMENT_COLOR[0], + MONUMENT_COLOR[1], + MONUMENT_COLOR[2], + 1.0); + + // Set state for the monument. + this.renderContext.gl.enable(this.renderContext.gl.DEPTH_TEST); + this.renderContext.gl.depthMask(true); + this.renderContext.gl.disable(this.renderContext.gl.SCISSOR_TEST); + + // Draw the monument! + this.renderContext.gl.drawElements(this.renderContext.gl.TRIANGLES, + CUBE_INDICES.length, + this.renderContext.gl.UNSIGNED_SHORT, + 0); + + // Clear to avoid Z-fighting. + this.renderContext.gl.clearDepth(1.0); + this.renderContext.gl.clear(this.renderContext.gl.DEPTH_BUFFER_BIT); + } + + protected compositeIfNecessary(): void {} + protected pathColorsForObject(objectIndex: number): Uint8Array { return TEXT_COLOR; } protected pathTransformsForObject(objectIndex: number): Float32Array { - const meshDescriptor = this.appController.meshDescriptors[objectIndex]; + const meshDescriptor = this.renderContext.appController.meshDescriptors[objectIndex]; const pathCount = this.pathCountForObject(objectIndex); const pathTransforms = new Float32Array(4 * (pathCount + 1)); for (let pathIndex = 0; pathIndex < pathCount; pathIndex++) { @@ -333,6 +427,10 @@ class ThreeDView extends DemoView { return pathTransforms; } + protected meshInstanceCountForObject(objectIndex: number): number { + return this.renderContext.appController.meshDescriptors[objectIndex].positions.length; + } + protected createAAStrategy(aaType: AntialiasingStrategyName, aaLevel: number, subpixelAA: SubpixelAAType): @@ -342,57 +440,20 @@ class ThreeDView extends DemoView { throw new PathfinderError("Unsupported antialiasing type!"); } - protected drawSceneryIfNecessary(): void { - // Set up the cube VBO. - const shaderProgram = this.shaderPrograms.demo3DMonument; - this.gl.useProgram(shaderProgram.program); - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.cubeVertexPositionBuffer); - this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.cubeIndexBuffer); - this.gl.vertexAttribPointer(shaderProgram.attributes.aPosition, - 3, - this.gl.FLOAT, - false, - 0, - 0); - this.gl.enableVertexAttribArray(shaderProgram.attributes.aPosition); - - // Set uniforms for the monument. - const transform = this.calculateWorldTransform(MONUMENT_TRANSLATION, MONUMENT_SCALE); - this.gl.uniformMatrix4fv(shaderProgram.uniforms.uTransform, false, transform); - this.gl.uniform4f(shaderProgram.uniforms.uColor, - MONUMENT_COLOR[0], - MONUMENT_COLOR[1], - MONUMENT_COLOR[2], - 1.0); - - // Set state for the monument. - this.gl.enable(this.gl.DEPTH_TEST); - this.gl.depthMask(true); - this.gl.disable(this.gl.SCISSOR_TEST); - - // Draw the monument! - this.gl.drawElements(this.gl.TRIANGLES, CUBE_INDICES.length, this.gl.UNSIGNED_SHORT, 0); - - // Clear to avoid Z-fighting. - this.gl.clearDepth(1.0); - this.gl.clear(this.gl.DEPTH_BUFFER_BIT); - } - - protected compositeIfNecessary(): void {} - - protected newTimingsReceived() { - this.appController.newTimingsReceived(_.pick(this.lastTimings, ['rendering'])); - } - protected clearForDirectRendering(): void { - this.gl.clearColor(1.0, 1.0, 1.0, 1.0); - this.gl.clearDepth(1.0); - this.gl.depthMask(true); - this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); + this.renderContext.gl.clearColor(1.0, 1.0, 1.0, 1.0); + this.renderContext.gl.clearDepth(1.0); + this.renderContext.gl.depthMask(true); + this.renderContext.gl.clear(this.renderContext.gl.COLOR_BUFFER_BIT | + this.renderContext.gl.DEPTH_BUFFER_BIT); } protected getModelviewTransform(objectIndex: number): glmatrix.mat4 { - const textFrameIndex = this.appController.meshDescriptors[objectIndex].textFrameIndex; + const textFrameIndex = this.renderContext + .appController + .meshDescriptors[objectIndex] + .textFrameIndex; + const transform = glmatrix.mat4.create(); glmatrix.mat4.rotateY(transform, transform, Math.PI / 2.0 * textFrameIndex); glmatrix.mat4.translate(transform, transform, TEXT_TRANSLATION); @@ -401,7 +462,11 @@ class ThreeDView extends DemoView { // Cheap but effective backface culling. protected shouldRenderObject(objectIndex: number): boolean { - const textFrameIndex = this.appController.meshDescriptors[objectIndex].textFrameIndex; + const textFrameIndex = this.renderContext + .appController + .meshDescriptors[objectIndex] + .textFrameIndex; + const translation = this.camera.translation; const extent = TEXT_TRANSLATION[2] * TEXT_SCALE[2]; switch (textFrameIndex) { @@ -411,26 +476,19 @@ class ThreeDView extends DemoView { default: return translation[0] > extent; } } - - protected meshInstanceCountForObject(objectIndex: number): number { - return this.appController.meshDescriptors[objectIndex].positions.length; - } - - get destAllocatedSize(): glmatrix.vec2 { - return glmatrix.vec2.fromValues(this.canvas.width, this.canvas.height); - } - - get destUsedSize(): glmatrix.vec2 { - return this.destAllocatedSize; + protected newTimingsReceived() { + const newTimings: Timings = _.pick(this.lastTimings, ['rendering']); + this.renderContext.appController.newTimingsReceived(newTimings); } private calculateWorldTransform(modelviewTranslation: glmatrix.vec3, modelviewScale: glmatrix.vec3): glmatrix.mat4 { + const canvas = this.renderContext.canvas; const projection = glmatrix.mat4.create(); glmatrix.mat4.perspective(projection, FOV / 180.0 * Math.PI, - this.canvas.width / this.canvas.height, + canvas.width / canvas.height, NEAR_CLIP_PLANE, FAR_CLIP_PLANE); @@ -444,10 +502,6 @@ class ThreeDView extends DemoView { glmatrix.mat4.mul(transform, projection, modelview); return transform; } - - protected get worldTransform() { - return this.calculateWorldTransform(glmatrix.vec3.create(), TEXT_SCALE); - } } function main() { diff --git a/demo/client/src/aa-strategy.ts b/demo/client/src/aa-strategy.ts index 6b1cc39c..a63f3a4e 100644 --- a/demo/client/src/aa-strategy.ts +++ b/demo/client/src/aa-strategy.ts @@ -10,7 +10,8 @@ import * as glmatrix from 'gl-matrix'; -import {DemoView, Renderer} from './view'; +import {Renderer} from './renderer'; +import {DemoView} from './view'; export type AntialiasingStrategyName = 'none' | 'ssaa' | 'xcaa'; diff --git a/demo/client/src/benchmark.ts b/demo/client/src/benchmark.ts index e278b6ba..5cf4cc11 100644 --- a/demo/client/src/benchmark.ts +++ b/demo/client/src/benchmark.ts @@ -19,6 +19,7 @@ import PathfinderBufferTexture from './buffer-texture'; import {OrthographicCamera} from './camera'; import {UniformMap} from './gl-utils'; import {PathfinderMeshData} from "./meshes"; +import {Renderer} from './renderer'; import {ShaderMap, ShaderProgramSource} from "./shader-loader"; import SSAAStrategy from './ssaa-strategy'; import {BUILTIN_FONT_URI, ExpandedMeshData, GlyphStore, PathfinderFont, TextFrame} from "./text"; @@ -127,8 +128,6 @@ class BenchmarkAppController extends DemoAppController { this.expandedMeshes = expandedMeshes; this.view.then(view => { - view.uploadPathColors(1); - view.uploadPathTransforms(1); view.attachMeshes([expandedMeshes.meshes]); }); }); @@ -237,10 +236,44 @@ class BenchmarkAppController extends DemoAppController { } class BenchmarkTestView extends DemoView { - destFramebuffer: WebGLFramebuffer | null = null; + readonly renderer: BenchmarkRenderer; + readonly appController: BenchmarkAppController; + + get camera(): OrthographicCamera { + return this.renderer.camera; + } + + set pixelsPerEm(newPPEM: number) { + this.renderer.pixelsPerEm = newPPEM; + } + + set renderingPromiseCallback(newCallback: (time: number) => void) { + this.renderer.renderingPromiseCallback = newCallback; + } + + constructor(appController: BenchmarkAppController, + commonShaderSource: string, + shaderSources: ShaderMap) { + super(commonShaderSource, shaderSources); + + this.appController = appController; + this.renderer = new BenchmarkRenderer(this); + + this.resizeToFit(true); + } +} + +class BenchmarkRenderer extends Renderer { + renderContext: BenchmarkTestView; + + camera: OrthographicCamera; renderingPromiseCallback: ((time: number) => void) | null; + get destFramebuffer(): WebGLFramebuffer | null { + return null; + } + get bgColor(): glmatrix.vec4 { return glmatrix.vec4.clone([1.0, 1.0, 1.0, 0.0]); } @@ -249,50 +282,99 @@ class BenchmarkTestView extends DemoView { return glmatrix.vec4.clone([0.0, 0.0, 0.0, 1.0]); } - protected usedSizeFactor: glmatrix.vec2 = glmatrix.vec2.clone([1.0, 1.0]); + get destAllocatedSize(): glmatrix.vec2 { + const canvas = this.renderContext.canvas; + return glmatrix.vec2.clone([canvas.width, canvas.height]); + } - protected directCurveProgramName: keyof ShaderMap = 'directCurve'; - protected directInteriorProgramName: keyof ShaderMap = 'directInterior'; - - protected depthFunction: number = this.gl.GREATER; - - protected camera: OrthographicCamera; - - private _pixelsPerEm: number = 32.0; - - private readonly appController: BenchmarkAppController; + get destUsedSize(): glmatrix.vec2 { + return this.destAllocatedSize; + } get emboldenAmount(): glmatrix.vec2 { return this.stemDarkeningAmount; } + get pixelsPerEm(): number { + return this._pixelsPerEm; + } + + set pixelsPerEm(newPixelsPerEm: number) { + this._pixelsPerEm = newPixelsPerEm; + this.uploadPathTransforms(1); + this.renderContext.setDirty(); + } + + protected get usedSizeFactor(): glmatrix.vec2 { + return glmatrix.vec2.clone([1.0, 1.0]); + } + + protected get directCurveProgramName(): keyof ShaderMap { + return 'directCurve'; + } + + protected get directInteriorProgramName(): keyof ShaderMap { + return 'directInterior'; + } + + protected get depthFunction(): number { + return this.renderContext.gl.GREATER; + } + + protected get worldTransform() { + const canvas = this.renderContext.canvas; + + const transform = glmatrix.mat4.create(); + const translation = this.camera.translation; + glmatrix.mat4.translate(transform, transform, [-1.0, -1.0, 0.0]); + glmatrix.mat4.scale(transform, transform, [2.0 / canvas.width, 2.0 / canvas.height, 1.0]); + glmatrix.mat4.translate(transform, transform, [translation[0], translation[1], 0]); + glmatrix.mat4.scale(transform, transform, [this.camera.scale, this.camera.scale, 1.0]); + + const pixelsPerUnit = this.pixelsPerUnit; + glmatrix.mat4.scale(transform, transform, [pixelsPerUnit, pixelsPerUnit, 1.0]); + + return transform; + } + + private _pixelsPerEm: number = 32.0; + + private get pixelsPerUnit(): number { + const font = unwrapNull(this.renderContext.appController.font); + return this._pixelsPerEm / font.opentypeFont.unitsPerEm; + } + private get stemDarkeningAmount(): glmatrix.vec2 { return computeStemDarkeningAmount(this._pixelsPerEm, this.pixelsPerUnit); } - constructor(appController: BenchmarkAppController, - commonShaderSource: string, - shaderSources: ShaderMap) { - super(commonShaderSource, shaderSources); + constructor(renderContext: BenchmarkTestView) { + super(renderContext); - this.appController = appController; - - this.camera = new OrthographicCamera(this.canvas); - this.camera.onPan = () => this.setDirty(); - this.camera.onZoom = () => this.setDirty(); + this.camera = new OrthographicCamera(renderContext.canvas); + this.camera.onPan = () => renderContext.setDirty(); + this.camera.onZoom = () => renderContext.setDirty(); } - setHintsUniform(uniforms: UniformMap): void { - this.gl.uniform4f(uniforms.uHints, 0, 0, 0, 0); + attachMeshes(meshes: PathfinderMeshData[]): void { + super.attachMeshes(meshes); + + this.uploadPathColors(1); + this.uploadPathTransforms(1); + } + + pathCountForObject(objectIndex: number): number { + return STRING.length; } pathBoundingRects(objectIndex: number): Float32Array { - const font = unwrapNull(this.appController.font); + const appController = this.renderContext.appController; + const font = unwrapNull(appController.font); const boundingRects = new Float32Array((STRING.length + 1) * 4); for (let glyphIndex = 0; glyphIndex < STRING.length; glyphIndex++) { - const glyphID = unwrapNull(this.appController.textRun).glyphIDs[glyphIndex]; + const glyphID = unwrapNull(appController.textRun).glyphIDs[glyphIndex]; const metrics = font.metricsForGlyph(glyphID); if (metrics == null) @@ -307,8 +389,8 @@ class BenchmarkTestView extends DemoView { return boundingRects; } - pathCountForObject(objectIndex: number): number { - return STRING.length; + setHintsUniform(uniforms: UniformMap): void { + this.renderContext.gl.uniform4f(uniforms.uHints, 0, 0, 0, 0); } protected createAAStrategy(aaType: AntialiasingStrategyName, @@ -324,6 +406,14 @@ class BenchmarkTestView extends DemoView { // TODO(pcwalton) } + protected renderingFinished(): void { + if (this.renderingPromiseCallback == null) + return; + const glyphCount = unwrapNull(this.renderContext.appController.textRun).glyphIDs.length; + const usPerGlyph = this.lastTimings.rendering * 1000.0 / glyphCount; + this.renderingPromiseCallback(usPerGlyph); + } + protected pathColorsForObject(objectIndex: number): Uint8Array { const pathColors = new Uint8Array(4 * (STRING.length + 1)); for (let pathIndex = 0; pathIndex < STRING.length; pathIndex++) @@ -332,20 +422,21 @@ class BenchmarkTestView extends DemoView { } protected pathTransformsForObject(objectIndex: number): Float32Array { + const appController = this.renderContext.appController; + const canvas = this.renderContext.canvas; + const font = unwrapNull(appController.font); + const pathTransforms = new Float32Array(4 * (STRING.length + 1)); let currentX = 0, currentY = 0; - const availableWidth = this.canvas.width / this.pixelsPerUnit; - const lineHeight = unwrapNull(this.appController.font).opentypeFont.lineHeight(); + const availableWidth = canvas.width / this.pixelsPerUnit; + const lineHeight = font.opentypeFont.lineHeight(); for (let glyphIndex = 0; glyphIndex < STRING.length; glyphIndex++) { - const glyphID = unwrapNull(this.appController.textRun).glyphIDs[glyphIndex]; + const glyphID = unwrapNull(appController.textRun).glyphIDs[glyphIndex]; pathTransforms.set([1, 1, currentX, currentY], (glyphIndex + 1) * 4); - currentX += unwrapNull(this.appController.font).opentypeFont - .glyphs - .get(glyphID) - .advanceWidth; + currentX += font.opentypeFont.glyphs.get(glyphID).advanceWidth; if (currentX > availableWidth) { currentX = 0; currentY += lineHeight; @@ -354,52 +445,6 @@ class BenchmarkTestView extends DemoView { return pathTransforms; } - - protected renderingFinished(): void { - if (this.renderingPromiseCallback != null) { - const glyphCount = unwrapNull(this.appController.textRun).glyphIDs.length; - const usPerGlyph = this.lastTimings.rendering * 1000.0 / glyphCount; - this.renderingPromiseCallback(usPerGlyph); - } - } - - get destAllocatedSize(): glmatrix.vec2 { - return glmatrix.vec2.clone([this.canvas.width, this.canvas.height]); - } - - get destUsedSize(): glmatrix.vec2 { - return this.destAllocatedSize; - } - - protected get worldTransform() { - const transform = glmatrix.mat4.create(); - const translation = this.camera.translation; - glmatrix.mat4.translate(transform, transform, [-1.0, -1.0, 0.0]); - glmatrix.mat4.scale(transform, - transform, - [2.0 / this.canvas.width, 2.0 / this.canvas.height, 1.0]); - glmatrix.mat4.translate(transform, transform, [translation[0], translation[1], 0]); - glmatrix.mat4.scale(transform, transform, [this.camera.scale, this.camera.scale, 1.0]); - - const pixelsPerUnit = this.pixelsPerUnit; - glmatrix.mat4.scale(transform, transform, [pixelsPerUnit, pixelsPerUnit, 1.0]); - - return transform; - } - - private get pixelsPerUnit(): number { - return this._pixelsPerEm / unwrapNull(this.appController.font).opentypeFont.unitsPerEm; - } - - get pixelsPerEm(): number { - return this._pixelsPerEm; - } - - set pixelsPerEm(newPixelsPerEm: number) { - this._pixelsPerEm = newPixelsPerEm; - this.uploadPathTransforms(1); - this.setDirty(); - } } function computeMedian(values: number[]): number | null { diff --git a/demo/client/src/mesh-debugger.ts b/demo/client/src/mesh-debugger.ts index 5c973e91..d9cbfa82 100644 --- a/demo/client/src/mesh-debugger.ts +++ b/demo/client/src/mesh-debugger.ts @@ -216,6 +216,8 @@ class MeshDebuggerView extends PathfinderView { this.camera.onPan = () => this.setDirty(); this.camera.onZoom = () => this.setDirty(); + + this.resizeToFit(true); } attachMeshes() { @@ -277,11 +279,6 @@ class MeshDebuggerView extends PathfinderView { const lowerRightPosition = unwrapNull(getPosition(positions, lowerRightIndex)); const lowerControlPointPosition = getPosition(positions, lowerControlPointIndex); - /*const upperCurve = upperControlPointPosition != null; - const lowerCurve = lowerControlPointPosition != null; - const upperNormals = getNormals(normals, normalIndices, upperCurve, 'upper'); - const lowerNormals = getNormals(normals, normalIndices, lowerCurve, 'lower');*/ - drawVertexIfNecessary(context, drawnVertices, upperLeftIndex, @@ -303,28 +300,6 @@ class MeshDebuggerView extends PathfinderView { lowerRightPosition, invScaleFactor); - /* - drawNormalIfNecessary(context, - drawnNormals, - upperLeftPosition, - upperNormals.left, - invScaleFactor); - drawNormalIfNecessary(context, - drawnNormals, - upperRightPosition, - upperNormals.right, - invScaleFactor); - drawNormalIfNecessary(context, - drawnNormals, - lowerLeftPosition, - lowerNormals.left, - invScaleFactor); - drawNormalIfNecessary(context, - drawnNormals, - lowerRightPosition, - lowerNormals.right, - invScaleFactor);*/ - context.beginPath(); context.moveTo(upperLeftPosition[0], upperLeftPosition[1]); if (upperControlPointPosition != null) { diff --git a/demo/client/src/renderer.ts b/demo/client/src/renderer.ts new file mode 100644 index 00000000..8f2cd993 --- /dev/null +++ b/demo/client/src/renderer.ts @@ -0,0 +1,522 @@ +// pathfinder/client/src/renderer.ts +// +// Copyright © 2017 The Pathfinder Project Developers. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +import * as glmatrix from 'gl-matrix'; + +import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from './aa-strategy'; +import {StemDarkeningMode, SubpixelAAType} from './aa-strategy'; +import PathfinderBufferTexture from "./buffer-texture"; +import {UniformMap} from './gl-utils'; +import {PathfinderMeshBuffers, PathfinderMeshData} from "./meshes"; +import {ShaderMap} from './shader-loader'; +import {UINT32_SIZE, unwrapNull} from './utils'; +import {RenderContext, Timings} from "./view"; + +const MAX_PATHS: number = 65535; + +const TIME_INTERVAL_DELAY: number = 32; + +const B_LOOP_BLINN_DATA_SIZE: number = 4; +const B_LOOP_BLINN_DATA_TEX_COORD_OFFSET: number = 0; +const B_LOOP_BLINN_DATA_SIGN_OFFSET: number = 2; + +export abstract class Renderer { + readonly renderContext: RenderContext; + + readonly pathTransformBufferTextures: PathfinderBufferTexture[]; + + meshes: PathfinderMeshBuffers[]; + meshData: PathfinderMeshData[]; + + get emboldenAmount(): glmatrix.vec2 { + return glmatrix.vec2.create(); + } + + get bgColor(): glmatrix.vec4 | null { + return null; + } + + get fgColor(): glmatrix.vec4 | null { + return null; + } + + abstract get destFramebuffer(): WebGLFramebuffer | null; + abstract get destAllocatedSize(): glmatrix.vec2; + abstract get destUsedSize(): glmatrix.vec2; + + protected antialiasingStrategy: AntialiasingStrategy | null; + protected lastTimings: Timings; + protected pathColorsBufferTextures: PathfinderBufferTexture[]; + + protected get pathIDsAreInstanced(): boolean { + return false; + } + + protected abstract get depthFunction(): GLenum; + protected abstract get directCurveProgramName(): keyof ShaderMap; + protected abstract get directInteriorProgramName(): keyof ShaderMap; + protected abstract get usedSizeFactor(): glmatrix.vec2; + protected abstract get worldTransform(): glmatrix.mat4; + + private instancedPathIDVBO: WebGLBuffer | null; + private timerQueryPollInterval: number | null; + + constructor(renderContext: RenderContext) { + this.renderContext = renderContext; + + this.lastTimings = { rendering: 0, compositing: 0 }; + + this.pathTransformBufferTextures = []; + this.pathColorsBufferTextures = []; + + if (this.pathIDsAreInstanced) + this.initInstancedPathIDVBO(); + + this.antialiasingStrategy = new NoAAStrategy(0, 'none'); + this.antialiasingStrategy.init(this); + } + + attachMeshes(meshes: PathfinderMeshData[]): void { + const renderContext = this.renderContext; + this.meshData = meshes; + this.meshes = meshes.map(meshes => new PathfinderMeshBuffers(renderContext.gl, meshes)); + unwrapNull(this.antialiasingStrategy).attachMeshes(this); + } + + abstract pathBoundingRects(objectIndex: number): Float32Array; + abstract setHintsUniform(uniforms: UniformMap): void; + + redraw(): void { + const renderContext = this.renderContext; + + if (this.meshes == null) + return; + + // Start timing rendering. + if (this.timerQueryPollInterval == null) { + renderContext.timerQueryExt.beginQueryEXT(renderContext.timerQueryExt.TIME_ELAPSED_EXT, + renderContext.atlasRenderingTimerQuery); + } + + // Prepare for direct rendering. + const antialiasingStrategy = unwrapNull(this.antialiasingStrategy); + antialiasingStrategy.prepare(this); + + // Clear. + this.clearForDirectRendering(); + + // Draw "scenery" (used in the 3D view). + this.drawSceneryIfNecessary(); + + // Perform direct rendering (Loop-Blinn). + if (antialiasingStrategy.shouldRenderDirect) + this.renderDirect(); + + // Antialias. + antialiasingStrategy.antialias(this); + + // End the timer, and start a new one. + if (this.timerQueryPollInterval == null) { + renderContext.timerQueryExt.endQueryEXT(renderContext.timerQueryExt.TIME_ELAPSED_EXT); + renderContext.timerQueryExt.beginQueryEXT(renderContext.timerQueryExt.TIME_ELAPSED_EXT, + renderContext.compositingTimerQuery); + } + + antialiasingStrategy.resolve(this); + + // Draw the glyphs with the resolved atlas to the default framebuffer. + this.compositeIfNecessary(); + + // Finish timing. + this.finishTiming(); + } + + setAntialiasingOptions(aaType: AntialiasingStrategyName, + aaLevel: number, + subpixelAA: SubpixelAAType, + stemDarkening: StemDarkeningMode) { + this.antialiasingStrategy = this.createAAStrategy(aaType, + aaLevel, + subpixelAA, + stemDarkening); + + this.antialiasingStrategy.init(this); + if (this.meshData != null) + this.antialiasingStrategy.attachMeshes(this); + + this.renderContext.setDirty(); + } + + canvasResized() { + if (this.antialiasingStrategy != null) + this.antialiasingStrategy.init(this); + } + + setFramebufferSizeUniform(uniforms: UniformMap) { + const renderContext = this.renderContext; + const currentViewport = renderContext.gl.getParameter(renderContext.gl.VIEWPORT); + renderContext.gl.uniform2i(uniforms.uFramebufferSize, + currentViewport[2], + currentViewport[3]); + } + + setTransformAndTexScaleUniformsForDest(uniforms: UniformMap): void { + const renderContext = this.renderContext; + const usedSize = this.usedSizeFactor; + + const transform = glmatrix.mat4.create(); + glmatrix.mat4.fromTranslation(transform, [-1.0, -1.0, 0.0]); + glmatrix.mat4.scale(transform, transform, [2.0 * usedSize[0], 2.0 * usedSize[1], 1.0]); + renderContext.gl.uniformMatrix4fv(uniforms.uTransform, false, transform); + + renderContext.gl.uniform2f(uniforms.uTexScale, usedSize[0], usedSize[1]); + } + + setTransformSTAndTexScaleUniformsForDest(uniforms: UniformMap): void { + const renderContext = this.renderContext; + const usedSize = this.usedSizeFactor; + renderContext.gl.uniform4f(uniforms.uTransformST, + 2.0 * usedSize[0], + 2.0 * usedSize[1], + -1.0, + -1.0); + renderContext.gl.uniform2f(uniforms.uTexScale, usedSize[0], usedSize[1]); + } + + setTransformSTUniform(uniforms: UniformMap, objectIndex: number) { + // FIXME(pcwalton): Lossy conversion from a 4x4 matrix to an ST matrix is ugly and fragile. + // Refactor. + const renderContext = this.renderContext; + const transform = glmatrix.mat4.clone(this.worldTransform); + glmatrix.mat4.mul(transform, transform, this.getModelviewTransform(objectIndex)); + + const translation = glmatrix.vec4.clone([transform[12], transform[13], 0.0, 1.0]); + + renderContext.gl.uniform4f(uniforms.uTransformST, + transform[0], + transform[5], + transform[12], + transform[13]); + } + + uploadPathColors(objectCount: number) { + const renderContext = this.renderContext; + + this.pathColorsBufferTextures = []; + + for (let objectIndex = 0; objectIndex < objectCount; objectIndex++) { + const pathColorsBufferTexture = new PathfinderBufferTexture(renderContext.gl, + 'uPathColors'); + const pathColors = this.pathColorsForObject(objectIndex); + pathColorsBufferTexture.upload(renderContext.gl, pathColors); + this.pathColorsBufferTextures.push(pathColorsBufferTexture); + } + } + + uploadPathTransforms(objectCount: number) { + const renderContext = this.renderContext; + + this.pathTransformBufferTextures.splice(0); + for (let objectIndex = 0; objectIndex < objectCount; objectIndex++) { + const pathTransformBufferTexture = new PathfinderBufferTexture(renderContext.gl, + 'uPathTransform'); + + const pathTransforms = this.pathTransformsForObject(objectIndex); + pathTransformBufferTexture.upload(renderContext.gl, pathTransforms); + this.pathTransformBufferTextures.push(pathTransformBufferTexture); + } + } + + protected abstract createAAStrategy(aaType: AntialiasingStrategyName, + aaLevel: number, + subpixelAA: SubpixelAAType, + stemDarkening: StemDarkeningMode): + AntialiasingStrategy; + protected abstract compositeIfNecessary(): void; + protected abstract pathColorsForObject(objectIndex: number): Uint8Array; + protected abstract pathTransformsForObject(objectIndex: number): Float32Array; + + protected drawSceneryIfNecessary(): void {} + + protected clearForDirectRendering(): void { + const renderContext = this.renderContext; + renderContext.gl.clearColor(1.0, 1.0, 1.0, 1.0); + renderContext.gl.clearDepth(0.0); + renderContext.gl.depthMask(true); + renderContext.gl.clear(renderContext.gl.COLOR_BUFFER_BIT | + renderContext.gl.DEPTH_BUFFER_BIT); + } + + protected getModelviewTransform(pathIndex: number): glmatrix.mat4 { + return glmatrix.mat4.create(); + } + + protected meshInstanceCountForObject(objectIndex: number): number { + return 1; + } + + // FIXME(pcwalton): Merge with `meshInstanceCountForObject`? + protected shouldRenderObject(objectIndex: number): boolean { + return true; + } + + /// Called whenever new GPU timing statistics are available. + protected newTimingsReceived() {} + + private renderDirect() { + const renderContext = this.renderContext; + for (let objectIndex = 0; objectIndex < this.meshes.length; objectIndex++) { + if (!this.shouldRenderObject(objectIndex)) + continue; + + const meshes = this.meshes[objectIndex]; + + let instanceCount: number | null; + if (!this.pathIDsAreInstanced) + instanceCount = null; + else + instanceCount = this.meshInstanceCountForObject(objectIndex); + + // Set up implicit cover state. + renderContext.gl.depthFunc(this.depthFunction); + renderContext.gl.depthMask(true); + renderContext.gl.enable(renderContext.gl.DEPTH_TEST); + renderContext.gl.disable(renderContext.gl.BLEND); + + // Set up the implicit cover interior VAO. + // + // TODO(pcwalton): Cache these. + const directInteriorProgram = + renderContext.shaderPrograms[this.directInteriorProgramName]; + const implicitCoverInteriorVAO = renderContext.vertexArrayObjectExt.createVertexArrayOES(); + renderContext.vertexArrayObjectExt.bindVertexArrayOES(implicitCoverInteriorVAO); + this.initImplicitCoverInteriorVAO(objectIndex); + + // Draw direct interior parts. + this.setTransformUniform(directInteriorProgram.uniforms, objectIndex); + this.setFramebufferSizeUniform(directInteriorProgram.uniforms); + this.setHintsUniform(directInteriorProgram.uniforms); + this.pathColorsBufferTextures[objectIndex].bind(renderContext.gl, + directInteriorProgram.uniforms, + 0); + this.pathTransformBufferTextures[objectIndex].bind(renderContext.gl, + directInteriorProgram.uniforms, + 1); + let indexCount = + renderContext.gl.getBufferParameter(renderContext.gl.ELEMENT_ARRAY_BUFFER, + renderContext.gl.BUFFER_SIZE) / UINT32_SIZE; + if (instanceCount == null) { + renderContext.gl.drawElements(renderContext.gl.TRIANGLES, + indexCount, + renderContext.gl.UNSIGNED_INT, + 0); + } else { + renderContext.instancedArraysExt + .drawElementsInstancedANGLE(renderContext.gl.TRIANGLES, + indexCount, + renderContext.gl.UNSIGNED_INT, + 0, + instanceCount); + } + + // Set up direct curve state. + renderContext.gl.depthMask(false); + renderContext.gl.enable(renderContext.gl.BLEND); + renderContext.gl.blendEquation(renderContext.gl.FUNC_ADD); + renderContext.gl.blendFuncSeparate(renderContext.gl.SRC_ALPHA, + renderContext.gl.ONE_MINUS_SRC_ALPHA, + renderContext.gl.ONE, + renderContext.gl.ONE); + + // Set up the direct curve VAO. + // + // TODO(pcwalton): Cache these. + const directCurveProgram = renderContext.shaderPrograms[this.directCurveProgramName]; + const implicitCoverCurveVAO = renderContext.vertexArrayObjectExt.createVertexArrayOES(); + renderContext.vertexArrayObjectExt.bindVertexArrayOES(implicitCoverCurveVAO); + this.initImplicitCoverCurveVAO(objectIndex); + + // Draw direct curve parts. + this.setTransformUniform(directCurveProgram.uniforms, objectIndex); + this.setFramebufferSizeUniform(directCurveProgram.uniforms); + this.setHintsUniform(directInteriorProgram.uniforms); + this.pathColorsBufferTextures[objectIndex].bind(renderContext.gl, + directCurveProgram.uniforms, + 0); + this.pathTransformBufferTextures[objectIndex].bind(renderContext.gl, + directCurveProgram.uniforms, + 1); + indexCount = + renderContext.gl.getBufferParameter(renderContext.gl.ELEMENT_ARRAY_BUFFER, + renderContext.gl.BUFFER_SIZE) / UINT32_SIZE; + if (instanceCount == null) { + renderContext.gl.drawElements(renderContext.gl.TRIANGLES, + indexCount, + renderContext.gl.UNSIGNED_INT, + 0); + } else { + renderContext.instancedArraysExt + .drawElementsInstancedANGLE(renderContext.gl.TRIANGLES, + indexCount, + renderContext.gl.UNSIGNED_INT, + 0, + instanceCount); + } + + renderContext.vertexArrayObjectExt.bindVertexArrayOES(null); + } + } + + private finishTiming() { + const renderContext = this.renderContext; + + if (this.timerQueryPollInterval != null) + return; + + renderContext.timerQueryExt.endQueryEXT(renderContext.timerQueryExt.TIME_ELAPSED_EXT); + + this.timerQueryPollInterval = window.setInterval(() => { + for (const queryName of ['atlasRenderingTimerQuery', 'compositingTimerQuery'] as + Array<'atlasRenderingTimerQuery' | 'compositingTimerQuery'>) { + if (renderContext.timerQueryExt + .getQueryObjectEXT(renderContext[queryName], + renderContext.timerQueryExt + .QUERY_RESULT_AVAILABLE_EXT) === + 0) { + return; + } + } + + const atlasRenderingTime = + renderContext.timerQueryExt + .getQueryObjectEXT(renderContext.atlasRenderingTimerQuery, + renderContext.timerQueryExt.QUERY_RESULT_EXT); + const compositingTime = + renderContext.timerQueryExt + .getQueryObjectEXT(renderContext.compositingTimerQuery, + renderContext.timerQueryExt.QUERY_RESULT_EXT); + this.lastTimings = { + compositing: compositingTime / 1000000.0, + rendering: atlasRenderingTime / 1000000.0, + }; + + this.newTimingsReceived(); + + window.clearInterval(this.timerQueryPollInterval!); + this.timerQueryPollInterval = null; + }, TIME_INTERVAL_DELAY); + } + + private initImplicitCoverCurveVAO(objectIndex: number): void { + const renderContext = this.renderContext; + const meshes = this.meshes[objectIndex]; + + const directCurveProgram = renderContext.shaderPrograms[this.directCurveProgramName]; + renderContext.gl.useProgram(directCurveProgram.program); + renderContext.gl.bindBuffer(renderContext.gl.ARRAY_BUFFER, meshes.bVertexPositions); + renderContext.gl.vertexAttribPointer(directCurveProgram.attributes.aPosition, + 2, + renderContext.gl.FLOAT, + false, + 0, + 0); + + if (this.pathIDsAreInstanced) + renderContext.gl.bindBuffer(renderContext.gl.ARRAY_BUFFER, this.instancedPathIDVBO); + else + renderContext.gl.bindBuffer(renderContext.gl.ARRAY_BUFFER, meshes.bVertexPathIDs); + renderContext.gl.vertexAttribPointer(directCurveProgram.attributes.aPathID, + 1, + renderContext.gl.UNSIGNED_SHORT, + false, + 0, + 0); + if (this.pathIDsAreInstanced) { + renderContext.instancedArraysExt + .vertexAttribDivisorANGLE(directCurveProgram.attributes.aPathID, 1); + } + + renderContext.gl.bindBuffer(renderContext.gl.ARRAY_BUFFER, meshes.bVertexLoopBlinnData); + renderContext.gl.vertexAttribPointer(directCurveProgram.attributes.aTexCoord, + 2, + renderContext.gl.UNSIGNED_BYTE, + false, + B_LOOP_BLINN_DATA_SIZE, + B_LOOP_BLINN_DATA_TEX_COORD_OFFSET); + renderContext.gl.vertexAttribPointer(directCurveProgram.attributes.aSign, + 1, + renderContext.gl.BYTE, + false, + B_LOOP_BLINN_DATA_SIZE, + B_LOOP_BLINN_DATA_SIGN_OFFSET); + renderContext.gl.enableVertexAttribArray(directCurveProgram.attributes.aPosition); + renderContext.gl.enableVertexAttribArray(directCurveProgram.attributes.aTexCoord); + renderContext.gl.enableVertexAttribArray(directCurveProgram.attributes.aPathID); + renderContext.gl.enableVertexAttribArray(directCurveProgram.attributes.aSign); + renderContext.gl.bindBuffer(renderContext.gl.ELEMENT_ARRAY_BUFFER, + meshes.coverCurveIndices); + } + + private initImplicitCoverInteriorVAO(objectIndex: number): void { + const renderContext = this.renderContext; + const meshes = this.meshes[objectIndex]; + + const directInteriorProgram = renderContext.shaderPrograms[this.directInteriorProgramName]; + renderContext.gl.useProgram(directInteriorProgram.program); + renderContext.gl.bindBuffer(renderContext.gl.ARRAY_BUFFER, meshes.bVertexPositions); + renderContext.gl.vertexAttribPointer(directInteriorProgram.attributes.aPosition, + 2, + renderContext.gl.FLOAT, + false, + 0, + 0); + + if (this.pathIDsAreInstanced) + renderContext.gl.bindBuffer(renderContext.gl.ARRAY_BUFFER, this.instancedPathIDVBO); + else + renderContext.gl.bindBuffer(renderContext.gl.ARRAY_BUFFER, meshes.bVertexPathIDs); + renderContext.gl.vertexAttribPointer(directInteriorProgram.attributes.aPathID, + 1, + renderContext.gl.UNSIGNED_SHORT, + false, + 0, + 0); + if (this.pathIDsAreInstanced) { + renderContext.instancedArraysExt + .vertexAttribDivisorANGLE(directInteriorProgram.attributes.aPathID, 1); + } + + renderContext.gl.enableVertexAttribArray(directInteriorProgram.attributes.aPosition); + renderContext.gl.enableVertexAttribArray(directInteriorProgram.attributes.aPathID); + renderContext.gl.bindBuffer(renderContext.gl.ELEMENT_ARRAY_BUFFER, + meshes.coverInteriorIndices); + } + + private initInstancedPathIDVBO(): void { + const renderContext = this.renderContext; + + const pathIDs = new Uint16Array(MAX_PATHS); + for (let pathIndex = 0; pathIndex < MAX_PATHS; pathIndex++) + pathIDs[pathIndex] = pathIndex + 1; + + this.instancedPathIDVBO = renderContext.gl.createBuffer(); + renderContext.gl.bindBuffer(renderContext.gl.ARRAY_BUFFER, this.instancedPathIDVBO); + renderContext.gl.bufferData(renderContext.gl.ARRAY_BUFFER, + pathIDs, + renderContext.gl.STATIC_DRAW); + renderContext.gl.bindBuffer(renderContext.gl.ARRAY_BUFFER, null); + } + + private setTransformUniform(uniforms: UniformMap, objectIndex: number) { + const transform = glmatrix.mat4.clone(this.worldTransform); + glmatrix.mat4.mul(transform, transform, this.getModelviewTransform(objectIndex)); + this.renderContext.gl.uniformMatrix4fv(uniforms.uTransform, false, transform); + } +} diff --git a/demo/client/src/ssaa-strategy.ts b/demo/client/src/ssaa-strategy.ts index 5684d829..5a0156c1 100644 --- a/demo/client/src/ssaa-strategy.ts +++ b/demo/client/src/ssaa-strategy.ts @@ -12,8 +12,9 @@ import * as glmatrix from 'gl-matrix'; import {AntialiasingStrategy, SubpixelAAType} from './aa-strategy'; import {createFramebuffer, createFramebufferDepthTexture, setTextureParameters} from './gl-utils'; +import {Renderer} from './renderer'; import {unwrapNull} from './utils'; -import {DemoView, Renderer} from './view'; +import {DemoView} from './view'; export default class SSAAStrategy extends AntialiasingStrategy { private level: number; @@ -50,11 +51,11 @@ export default class SSAAStrategy extends AntialiasingStrategy { renderContext.gl.bindTexture(renderContext.gl.TEXTURE_2D, this.supersampledColorTexture); renderContext.gl.texImage2D(renderContext.gl.TEXTURE_2D, 0, - renderer.colorAlphaFormat, + renderContext.colorAlphaFormat, this.supersampledFramebufferSize[0], this.supersampledFramebufferSize[1], 0, - renderer.colorAlphaFormat, + renderContext.colorAlphaFormat, renderContext.gl.UNSIGNED_BYTE, null); setTextureParameters(renderContext.gl, renderContext.gl.LINEAR); diff --git a/demo/client/src/svg-demo.ts b/demo/client/src/svg-demo.ts index 639137ea..a9ec8001 100644 --- a/demo/client/src/svg-demo.ts +++ b/demo/client/src/svg-demo.ts @@ -18,6 +18,7 @@ import PathfinderBufferTexture from "./buffer-texture"; import {OrthographicCamera} from "./camera"; import {UniformMap} from './gl-utils'; import {PathfinderMeshData} from "./meshes"; +import {Renderer} from './renderer'; import {ShaderMap, ShaderProgramSource} from './shader-loader'; import SSAAStrategy from "./ssaa-strategy"; import {BUILTIN_SVG_URI, SVGLoader} from './svg-loader'; @@ -78,24 +79,19 @@ class SVGDemoController extends DemoAppController { private meshesReceived(): void { this.view.then(view => { - view.uploadPathColors(1); - view.uploadPathTransforms(1); view.attachMeshes([this.meshes]); - - view.camera.bounds = this.loader.bounds; - view.camera.zoomToFit(); + view.initCameraBounds(this.loader.bounds); }); } } class SVGDemoView extends DemoView { - camera: OrthographicCamera; + renderer: SVGDemoRenderer; + appController: SVGDemoController; - protected depthFunction: number = this.gl.GREATER; - - protected usedSizeFactor: glmatrix.vec2 = glmatrix.vec2.fromValues(1.0, 1.0); - - private appController: SVGDemoController; + get camera(): OrthographicCamera { + return this.renderer.camera; + } constructor(appController: SVGDemoController, commonShaderSource: string, @@ -103,14 +99,26 @@ class SVGDemoView extends DemoView { super(commonShaderSource, shaderSources); this.appController = appController; + this.renderer = new SVGDemoRenderer(this); - this.camera = new OrthographicCamera(this.canvas, { scaleBounds: true }); - this.camera.onPan = () => this.setDirty(); - this.camera.onZoom = () => this.setDirty(); + this.resizeToFit(true); } + initCameraBounds(bounds: glmatrix.vec4): void { + this.renderer.initCameraBounds(bounds); + } +} + +class SVGDemoRenderer extends Renderer { + renderContext: SVGDemoView; + + camera: OrthographicCamera; + get destAllocatedSize(): glmatrix.vec2 { - return glmatrix.vec2.fromValues(this.canvas.width, this.canvas.height); + return glmatrix.vec2.clone([ + this.renderContext.canvas.width, + this.renderContext.canvas.height, + ]); } get destFramebuffer(): WebGLFramebuffer | null { @@ -121,21 +129,70 @@ class SVGDemoView extends DemoView { return this.destAllocatedSize; } + constructor(renderContext: SVGDemoView) { + super(renderContext); + + this.camera = new OrthographicCamera(renderContext.canvas, { scaleBounds: true }); + this.camera.onPan = () => this.renderContext.setDirty(); + this.camera.onZoom = () => this.renderContext.setDirty(); + } + setHintsUniform(uniforms: UniformMap): void { - this.gl.uniform4f(uniforms.uHints, 0, 0, 0, 0); + this.renderContext.gl.uniform4f(uniforms.uHints, 0, 0, 0, 0); } pathBoundingRects(objectIndex: number): Float32Array { - panic("SVGDemoView.pathBoundingRects(): TODO"); + panic("SVGDemoRenderer.pathBoundingRects(): TODO"); return glmatrix.vec4.create(); } - pathCountForObject(objectIndex: number): number { - return this.appController.loader.pathInstances.length; + attachMeshes(meshes: PathfinderMeshData[]): void { + super.attachMeshes(meshes); + this.uploadPathColors(1); + this.uploadPathTransforms(1); + } + + initCameraBounds(bounds: glmatrix.vec4): void { + this.camera.bounds = bounds; + this.camera.zoomToFit(); + } + + protected get depthFunction(): number { + return this.renderContext.gl.GREATER; + } + + protected get usedSizeFactor(): glmatrix.vec2 { + return glmatrix.vec2.clone([1.0, 1.0]); + } + + protected get worldTransform(): glmatrix.mat4 { + const transform = glmatrix.mat4.create(); + const translation = this.camera.translation; + glmatrix.mat4.translate(transform, transform, [-1.0, -1.0, 0.0]); + glmatrix.mat4.scale(transform, transform, [ + 2.0 / this.renderContext.canvas.width, + 2.0 / this.renderContext.canvas.height, + 1.0, + ]); + glmatrix.mat4.translate(transform, transform, [translation[0], translation[1], 0]); + glmatrix.mat4.scale(transform, transform, [this.camera.scale, this.camera.scale, 1.0]); + return transform; + } + + protected get directCurveProgramName(): keyof ShaderMap { + return 'directCurve'; + } + + protected get directInteriorProgramName(): keyof ShaderMap { + return 'directInterior'; + } + + protected newTimingsReceived(): void { + this.renderContext.appController.newTimingsReceived(_.pick(this.lastTimings, ['rendering'])); } protected pathColorsForObject(objectIndex: number): Uint8Array { - const instances = this.appController.loader.pathInstances; + const instances = this.renderContext.appController.loader.pathInstances; const pathColors = new Uint8Array(4 * (instances.length + 1)); for (let pathIndex = 0; pathIndex < instances.length; pathIndex++) { @@ -154,7 +211,7 @@ class SVGDemoView extends DemoView { } protected pathTransformsForObject(objectIndex: number): Float32Array { - const instances = this.appController.loader.pathInstances; + const instances = this.renderContext.appController.loader.pathInstances; const pathTransforms = new Float32Array(4 * (instances.length + 1)); for (let pathIndex = 0; pathIndex < instances.length; pathIndex++) { @@ -174,30 +231,6 @@ class SVGDemoView extends DemoView { } protected compositeIfNecessary(): void {} - - protected newTimingsReceived() { - this.appController.newTimingsReceived(_.pick(this.lastTimings, ['rendering'])); - } - - protected get worldTransform() { - const transform = glmatrix.mat4.create(); - const translation = this.camera.translation; - glmatrix.mat4.translate(transform, transform, [-1.0, -1.0, 0.0]); - glmatrix.mat4.scale(transform, - transform, - [2.0 / this.canvas.width, 2.0 / this.canvas.height, 1.0]); - glmatrix.mat4.translate(transform, transform, [translation[0], translation[1], 0]); - glmatrix.mat4.scale(transform, transform, [this.camera.scale, this.camera.scale, 1.0]); - return transform; - } - - protected get directCurveProgramName(): keyof ShaderMap { - return 'directCurve'; - } - - protected get directInteriorProgramName(): keyof ShaderMap { - return 'directInterior'; - } } function main() { diff --git a/demo/client/src/text-demo.ts b/demo/client/src/text-demo.ts index 53d99473..5640b74c 100644 --- a/demo/client/src/text-demo.ts +++ b/demo/client/src/text-demo.ts @@ -23,6 +23,7 @@ import {createFramebuffer, createFramebufferColorTexture} from './gl-utils'; import {createFramebufferDepthTexture, QUAD_ELEMENTS, setTextureParameters} from './gl-utils'; import {UniformMap} from './gl-utils'; import {PathfinderMeshBuffers, PathfinderMeshData} from './meshes'; +import {Renderer} from './renderer'; import {PathfinderShaderProgram, ShaderMap, ShaderProgramSource} from './shader-loader'; import SSAAStrategy from './ssaa-strategy'; import {calculatePixelDescent, calculatePixelRectForGlyph, PathfinderFont} from "./text"; @@ -186,7 +187,7 @@ class TextDemoController extends DemoAppController { } private hintingChanged(): void { - this.view.then(view => view.updateHinting()); + this.view.then(view => view.renderer.updateHinting()); } private updateText(): void { @@ -213,7 +214,7 @@ class TextDemoController extends DemoAppController { this.meshes = meshes; view.attachText(); - view.uploadPathColors(1); + view.renderer.uploadPathColors(1); view.attachMeshes([this.meshes]); }); }); @@ -240,7 +241,7 @@ class TextDemoController extends DemoAppController { /// The font size in pixels per em. set fontSize(newFontSize: number) { this._fontSize = newFontSize; - this.view.then(view => view.relayoutText()); + this.view.then(view => view.renderer.relayoutText()); } get layoutPixelsPerUnit(): number { @@ -265,6 +266,62 @@ class TextDemoController extends DemoAppController { } class TextDemoView extends DemoView { + renderer: TextDemoRenderer; + + appController: TextDemoController; + + protected get camera(): OrthographicCamera { + return this.renderer.camera; + } + + constructor(appController: TextDemoController, + commonShaderSource: string, + shaderSources: ShaderMap) { + super(commonShaderSource, shaderSources); + + this.appController = appController; + this.renderer = new TextDemoRenderer(this); + + this.canvas.addEventListener('dblclick', () => this.appController.showTextEditor(), false); + + this.resizeToFit(true); + } + + attachText() { + this.panZoomEventsEnabled = false; + this.renderer.prepareToAttachText(); + this.renderer.camera.zoomToFit(); + this.appController.fontSize = this.renderer.camera.scale * + this.appController.font.opentypeFont.unitsPerEm; + this.renderer.finishAttachingText(); + this.panZoomEventsEnabled = true; + } + + protected onPan() { + this.renderer.viewPanned(); + } + + protected onZoom() { + this.appController.fontSize = this.renderer.camera.scale * + this.appController.font.opentypeFont.unitsPerEm; + } + + private set panZoomEventsEnabled(flag: boolean) { + if (flag) { + this.renderer.camera.onPan = () => this.onPan(); + this.renderer.camera.onZoom = () => this.onZoom(); + } else { + this.renderer.camera.onPan = null; + this.renderer.camera.onZoom = null; + } + } +} + +class TextDemoRenderer extends Renderer { + renderContext: TextDemoView; + + camera: OrthographicCamera; + atlasFramebuffer: WebGLFramebuffer; atlasDepthTexture: WebGLTexture; @@ -272,22 +329,22 @@ class TextDemoView extends DemoView { glyphTexCoordsBuffer: WebGLBuffer; glyphElementsBuffer: WebGLBuffer; - appController: TextDemoController; + get destFramebuffer(): WebGLFramebuffer { + return this.atlasFramebuffer; + } - camera: OrthographicCamera; + get destAllocatedSize(): glmatrix.vec2 { + return ATLAS_SIZE; + } + + get destUsedSize(): glmatrix.vec2 { + return this.renderContext.appController.atlas.usedSize; + } get emboldenAmount(): glmatrix.vec2 { return this.stemDarkeningAmount; } - private get stemDarkeningAmount(): glmatrix.vec2 { - if (this.stemDarkening === 'dark') { - return computeStemDarkeningAmount(this.appController.fontSize, - this.appController.layoutPixelsPerUnit); - } - return glmatrix.vec2.create(); - } - get bgColor(): glmatrix.vec4 { return glmatrix.vec4.fromValues(1.0, 1.0, 1.0, 0.0); } @@ -296,55 +353,55 @@ class TextDemoView extends DemoView { return glmatrix.vec4.fromValues(0.0, 0.0, 0.0, 1.0); } - protected depthFunction: number = this.gl.GREATER; + protected get worldTransform(): glmatrix.mat4 { + const transform = glmatrix.mat4.create(); + glmatrix.mat4.translate(transform, transform, [-1.0, -1.0, 0.0]); + glmatrix.mat4.scale(transform, transform, [2.0 / ATLAS_SIZE[0], 2.0 / ATLAS_SIZE[1], 1.0]); + return transform; + } - private subpixelAA: SubpixelAAType; - private stemDarkening: StemDarkeningMode; + protected get directCurveProgramName(): keyof ShaderMap { + return 'directCurve'; + } + + protected get directInteriorProgramName(): keyof ShaderMap { + return 'directInterior'; + } + + protected get depthFunction(): number { + return this.renderContext.gl.GREATER; + } + + protected get usedSizeFactor(): glmatrix.vec2 { + const usedSize = glmatrix.vec2.create(); + glmatrix.vec2.div(usedSize, this.renderContext.appController.atlas.usedSize, ATLAS_SIZE); + return usedSize; + } + + private get stemDarkeningAmount(): glmatrix.vec2 { + const appController = this.renderContext.appController; + if (this.stemDarkening === 'dark') { + return computeStemDarkeningAmount(appController.fontSize, + appController.layoutPixelsPerUnit); + } + return glmatrix.vec2.create(); + } private glyphBounds: Float32Array; + private stemDarkening: StemDarkeningMode; + private subpixelAA: SubpixelAAType; - constructor(appController: TextDemoController, - commonShaderSource: string, - shaderSources: ShaderMap) { - super(commonShaderSource, shaderSources); + private get displayPixelsPerUnit(): number { + return this.renderContext.appController.layoutPixelsPerUnit; + } - this.appController = appController; + constructor(renderContext: TextDemoView) { + super(renderContext); - this.camera = new OrthographicCamera(this.canvas, { + this.camera = new OrthographicCamera(this.renderContext.canvas, { maxScale: MAX_SCALE, minScale: MIN_SCALE, }); - - this.canvas.addEventListener('dblclick', () => this.appController.showTextEditor(), false); - } - - attachText() { - this.panZoomEventsEnabled = false; - - if (this.atlasFramebuffer == null) - this.createAtlasFramebuffer(); - - this.layoutText(); - this.camera.zoomToFit(); - this.appController.fontSize = this.camera.scale * - this.appController.font.opentypeFont.unitsPerEm; - this.buildAtlasGlyphs(); - this.setDirty(); - - this.panZoomEventsEnabled = true; - } - - relayoutText() { - this.layoutText(); - this.buildAtlasGlyphs(); - this.setDirty(); - } - - updateHinting(): void { - // Need to relayout the text because the pixel bounds of the glyphs can change from this... - this.layoutText(); - this.buildAtlasGlyphs(); - this.setDirty(); } setAntialiasingOptions(aaType: AntialiasingStrategyName, @@ -356,23 +413,53 @@ class TextDemoView extends DemoView { // Need to relayout because changing AA options can cause font dilation to change... this.layoutText(); this.buildAtlasGlyphs(); - this.setDirty(); + this.renderContext.setDirty(); } setHintsUniform(uniforms: UniformMap): void { const hint = this.createHint(); - this.gl.uniform4f(uniforms.uHints, - hint.xHeight, - hint.hintedXHeight, - hint.stemHeight, - hint.hintedStemHeight); + this.renderContext.gl.uniform4f(uniforms.uHints, + hint.xHeight, + hint.hintedXHeight, + hint.stemHeight, + hint.hintedStemHeight); + } + + prepareToAttachText(): void { + if (this.atlasFramebuffer == null) + this.createAtlasFramebuffer(); + + this.layoutText(); + } + + finishAttachingText(): void { + this.buildAtlasGlyphs(); + this.renderContext.setDirty(); + } + + relayoutText(): void { + this.layoutText(); + this.buildAtlasGlyphs(); + this.renderContext.setDirty(); + } + + updateHinting(): void { + // Need to relayout the text because the pixel bounds of the glyphs can change from this... + this.layoutText(); + this.buildAtlasGlyphs(); + this.renderContext.setDirty(); + } + + viewPanned(): void { + this.buildAtlasGlyphs(); + this.renderContext.setDirty(); } pathBoundingRects(objectIndex: number): Float32Array { - const pathCount = this.appController.pathCount; - const atlasGlyphs = this.appController.atlasGlyphs; + const pathCount = this.renderContext.appController.pathCount; + const atlasGlyphs = this.renderContext.appController.atlasGlyphs; const pixelsPerUnit = this.displayPixelsPerUnit; - const font = this.appController.font; + const font = this.renderContext.appController.font; const hint = this.createHint(); const boundingRects = new Float32Array((pathCount + 1) * 4); @@ -393,16 +480,97 @@ class TextDemoView extends DemoView { return boundingRects; } - pathCountForObject(objectIndex: number): number { - return this.appController.pathCount; + protected createAAStrategy(aaType: AntialiasingStrategyName, + aaLevel: number, + subpixelAA: SubpixelAAType, + stemDarkening: StemDarkeningMode): + AntialiasingStrategy { + this.subpixelAA = subpixelAA; + this.stemDarkening = stemDarkening; + return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA); } - protected initContext() { - super.initContext(); + protected clearForDirectRendering(): void { + this.renderContext.gl.clearColor(0.0, 0.0, 0.0, 0.0); + this.renderContext.gl.clearDepth(0.0); + this.renderContext.gl.depthMask(true); + this.renderContext.gl.clear(this.renderContext.gl.COLOR_BUFFER_BIT | + this.renderContext.gl.DEPTH_BUFFER_BIT); + } + + protected compositeIfNecessary() { + // Set up composite state. + this.renderContext.gl.bindFramebuffer(this.renderContext.gl.FRAMEBUFFER, null); + this.renderContext.gl.viewport(0, 0, this.renderContext.canvas.width, this.renderContext.canvas.height); + this.renderContext.gl.disable(this.renderContext.gl.DEPTH_TEST); + this.renderContext.gl.disable(this.renderContext.gl.SCISSOR_TEST); + this.renderContext.gl.blendEquation(this.renderContext.gl.FUNC_ADD); + this.renderContext.gl.blendFuncSeparate(this.renderContext.gl.SRC_ALPHA, + this.renderContext.gl.ONE_MINUS_SRC_ALPHA, + this.renderContext.gl.ONE, + this.renderContext.gl.ONE); + this.renderContext.gl.enable(this.renderContext.gl.BLEND); + + // Clear. + this.renderContext.gl.clearColor(1.0, 1.0, 1.0, 1.0); + this.renderContext.gl.clear(this.renderContext.gl.COLOR_BUFFER_BIT); + + // Set up the composite VAO. + const blitProgram = this.renderContext.shaderPrograms.blit; + const attributes = blitProgram.attributes; + this.renderContext.gl.useProgram(blitProgram.program); + this.renderContext.gl.bindBuffer(this.renderContext.gl.ARRAY_BUFFER, + this.glyphPositionsBuffer); + this.renderContext.gl.vertexAttribPointer(attributes.aPosition, + 2, + this.renderContext.gl.FLOAT, + false, + 0, + 0); + this.renderContext.gl.bindBuffer(this.renderContext.gl.ARRAY_BUFFER, + this.glyphTexCoordsBuffer); + this.renderContext.gl.vertexAttribPointer(attributes.aTexCoord, + 2, + this.renderContext.gl.FLOAT, + false, + 0, + 0); + this.renderContext.gl.enableVertexAttribArray(attributes.aPosition); + this.renderContext.gl.enableVertexAttribArray(attributes.aTexCoord); + this.renderContext.gl.bindBuffer(this.renderContext.gl.ELEMENT_ARRAY_BUFFER, + this.glyphElementsBuffer); + + // Create the transform. + const transform = glmatrix.mat4.create(); + glmatrix.mat4.fromTranslation(transform, [-1.0, -1.0, 0.0]); + glmatrix.mat4.scale(transform, transform, [ + 2.0 / this.renderContext.canvas.width, + 2.0 / this.renderContext.canvas.height, + 1.0, + ]); + glmatrix.mat4.translate(transform, + transform, + [this.camera.translation[0], this.camera.translation[1], 0.0]); + + // Blit. + this.renderContext.gl.uniformMatrix4fv(blitProgram.uniforms.uTransform, false, transform); + this.renderContext.gl.activeTexture(this.renderContext.gl.TEXTURE0); + const destTexture = this.renderContext + .appController + .atlas + .ensureTexture(this.renderContext); + this.renderContext.gl.bindTexture(this.renderContext.gl.TEXTURE_2D, destTexture); + this.renderContext.gl.uniform1i(blitProgram.uniforms.uSource, 0); + this.setIdentityTexScaleUniform(blitProgram.uniforms); + const totalGlyphCount = this.renderContext.appController.layout.textFrame.totalGlyphCount; + this.renderContext.gl.drawElements(this.renderContext.gl.TRIANGLES, + totalGlyphCount * 6, + this.renderContext.gl.UNSIGNED_INT, + 0); } protected pathColorsForObject(objectIndex: number): Uint8Array { - const pathCount = this.appController.pathCount; + const pathCount = this.renderContext.appController.pathCount; const pathColors = new Uint8Array(4 * (pathCount + 1)); @@ -416,8 +584,8 @@ class TextDemoView extends DemoView { } protected pathTransformsForObject(objectIndex: number): Float32Array { - const pathCount = this.appController.pathCount; - const atlasGlyphs = this.appController.atlasGlyphs; + const pathCount = this.renderContext.appController.pathCount; + const atlasGlyphs = this.renderContext.appController.atlasGlyphs; const pixelsPerUnit = this.displayPixelsPerUnit; const transforms = new Float32Array((pathCount + 1) * 4); @@ -435,97 +603,33 @@ class TextDemoView extends DemoView { return transforms; } - protected onPan() { - this.buildAtlasGlyphs(); - this.setDirty(); - } - - protected onZoom() { - this.appController.fontSize = this.camera.scale * - this.appController.font.opentypeFont.unitsPerEm; - } - - protected compositeIfNecessary() { - // Set up composite state. - this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); - this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); - this.gl.disable(this.gl.DEPTH_TEST); - this.gl.disable(this.gl.SCISSOR_TEST); - this.gl.blendEquation(this.gl.FUNC_ADD); - this.gl.blendFuncSeparate(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA, - this.gl.ONE, this.gl.ONE); - this.gl.enable(this.gl.BLEND); - - // Clear. - this.gl.clearColor(1.0, 1.0, 1.0, 1.0); - this.gl.clear(this.gl.COLOR_BUFFER_BIT); - - // Set up the composite VAO. - const blitProgram = this.shaderPrograms.blit; - const attributes = blitProgram.attributes; - this.gl.useProgram(blitProgram.program); - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.glyphPositionsBuffer); - this.gl.vertexAttribPointer(attributes.aPosition, 2, this.gl.FLOAT, false, 0, 0); - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.glyphTexCoordsBuffer); - this.gl.vertexAttribPointer(attributes.aTexCoord, 2, this.gl.FLOAT, false, 0, 0); - this.gl.enableVertexAttribArray(attributes.aPosition); - this.gl.enableVertexAttribArray(attributes.aTexCoord); - this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.glyphElementsBuffer); - - // Create the transform. - const transform = glmatrix.mat4.create(); - glmatrix.mat4.fromTranslation(transform, [-1.0, -1.0, 0.0]); - glmatrix.mat4.scale(transform, - transform, - [2.0 / this.canvas.width, 2.0 / this.canvas.height, 1.0]); - glmatrix.mat4.translate(transform, - transform, - [this.camera.translation[0], - this.camera.translation[1], - 0.0]); - - // Blit. - this.gl.uniformMatrix4fv(blitProgram.uniforms.uTransform, false, transform); - this.gl.activeTexture(this.gl.TEXTURE0); - this.gl.bindTexture(this.gl.TEXTURE_2D, this.appController.atlas.ensureTexture(this)); - this.gl.uniform1i(blitProgram.uniforms.uSource, 0); - this.setIdentityTexScaleUniform(blitProgram.uniforms); - this.gl.drawElements(this.gl.TRIANGLES, - this.appController.layout.textFrame.totalGlyphCount * 6, - this.gl.UNSIGNED_INT, - 0); - } - - protected clearForDirectRendering(): void { - this.gl.clearColor(0.0, 0.0, 0.0, 0.0); - this.gl.clearDepth(0.0); - this.gl.depthMask(true); - this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); - } - - protected createAAStrategy(aaType: AntialiasingStrategyName, - aaLevel: number, - subpixelAA: SubpixelAAType, - stemDarkening: StemDarkeningMode): - AntialiasingStrategy { - this.subpixelAA = subpixelAA; - this.stemDarkening = stemDarkening; - return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA); - } - protected newTimingsReceived() { - this.appController.newTimingsReceived(this.lastTimings); + this.renderContext.appController.newTimingsReceived(this.lastTimings); + } + + private createAtlasFramebuffer() { + const appController = this.renderContext.appController; + + const atlasColorTexture = appController.atlas.ensureTexture(this.renderContext); + this.atlasDepthTexture = createFramebufferDepthTexture(this.renderContext.gl, ATLAS_SIZE); + this.atlasFramebuffer = createFramebuffer(this.renderContext.gl, + this.renderContext.drawBuffersExt, + [atlasColorTexture], + this.atlasDepthTexture); + + // Allow the antialiasing strategy to set up framebuffers as necessary. + if (this.antialiasingStrategy != null) + this.antialiasingStrategy.setFramebufferSize(this); } private createHint(): Hint { - return new Hint(this.appController.font, + return new Hint(this.renderContext.appController.font, this.displayPixelsPerUnit, - this.appController.useHinting); + this.renderContext.appController.useHinting); } - /// Lays out glyphs on the canvas. private layoutText() { - const layout = this.appController.layout; + const layout = this.renderContext.appController.layout; layout.layoutRuns(); const textBounds = layout.textFrame.bounds; @@ -537,7 +641,7 @@ class TextDemoView extends DemoView { const hint = this.createHint(); const displayPixelsPerUnit = this.displayPixelsPerUnit; - const layoutPixelsPerUnit = this.appController.layoutPixelsPerUnit; + const layoutPixelsPerUnit = this.renderContext.appController.layoutPixelsPerUnit; let globalGlyphIndex = 0; for (const run of layout.textFrame.runs) { @@ -566,29 +670,38 @@ class TextDemoView extends DemoView { } } - this.glyphPositionsBuffer = unwrapNull(this.gl.createBuffer()); - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.glyphPositionsBuffer); - this.gl.bufferData(this.gl.ARRAY_BUFFER, glyphPositions, this.gl.STATIC_DRAW); - this.glyphElementsBuffer = unwrapNull(this.gl.createBuffer()); - this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.glyphElementsBuffer); - this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, glyphIndices, this.gl.STATIC_DRAW); + this.glyphPositionsBuffer = unwrapNull(this.renderContext.gl.createBuffer()); + this.renderContext.gl.bindBuffer(this.renderContext.gl.ARRAY_BUFFER, + this.glyphPositionsBuffer); + this.renderContext.gl.bufferData(this.renderContext.gl.ARRAY_BUFFER, + glyphPositions, + this.renderContext.gl.STATIC_DRAW); + this.glyphElementsBuffer = unwrapNull(this.renderContext.gl.createBuffer()); + this.renderContext.gl.bindBuffer(this.renderContext.gl.ELEMENT_ARRAY_BUFFER, + this.glyphElementsBuffer); + this.renderContext.gl.bufferData(this.renderContext.gl.ELEMENT_ARRAY_BUFFER, + glyphIndices, + this.renderContext.gl.STATIC_DRAW); } private buildAtlasGlyphs() { - const font = this.appController.font; - const glyphStore = this.appController.glyphStore; - const layoutPixelsPerUnit = this.appController.layoutPixelsPerUnit; + const appController = this.renderContext.appController; + const font = appController.font; + const glyphStore = appController.glyphStore; + const layoutPixelsPerUnit = appController.layoutPixelsPerUnit; const displayPixelsPerUnit = this.displayPixelsPerUnit; - const textFrame = this.appController.layout.textFrame; + const textFrame = appController.layout.textFrame; const hint = this.createHint(); // Only build glyphs in view. const translation = this.camera.translation; - const canvasRect = glmatrix.vec4.fromValues(-translation[0], - -translation[1], - -translation[0] + this.canvas.width, - -translation[1] + this.canvas.height); + const canvasRect = glmatrix.vec4.clone([ + -translation[0], + -translation[1], + -translation[0] + this.renderContext.canvas.width, + -translation[1] + this.renderContext.canvas.height, + ]); let atlasGlyphs = []; for (const run of textFrame.runs) { @@ -621,12 +734,12 @@ class TextDemoView extends DemoView { if (atlasGlyphs.length === 0) return; - this.appController.atlasGlyphs = atlasGlyphs; - this.appController.atlas.layoutGlyphs(atlasGlyphs, - font, - displayPixelsPerUnit, - hint, - this.stemDarkeningAmount); + appController.atlasGlyphs = atlasGlyphs; + appController.atlas.layoutGlyphs(atlasGlyphs, + font, + displayPixelsPerUnit, + hint, + this.stemDarkeningAmount); this.uploadPathTransforms(1); @@ -635,26 +748,14 @@ class TextDemoView extends DemoView { this.setGlyphTexCoords(); } - private createAtlasFramebuffer() { - const atlasColorTexture = this.appController.atlas.ensureTexture(this); - this.atlasDepthTexture = createFramebufferDepthTexture(this.gl, ATLAS_SIZE); - this.atlasFramebuffer = createFramebuffer(this.gl, - this.drawBuffersExt, - [atlasColorTexture], - this.atlasDepthTexture); - - // Allow the antialiasing strategy to set up framebuffers as necessary. - if (this.antialiasingStrategy != null) - this.antialiasingStrategy.setFramebufferSize(this); - } - private setGlyphTexCoords() { - const textFrame = this.appController.layout.textFrame; - const font = this.appController.font; - const atlasGlyphs = this.appController.atlasGlyphs; + const appController = this.renderContext.appController; + const textFrame = appController.layout.textFrame; + const font = appController.font; + const atlasGlyphs = appController.atlasGlyphs; const hint = this.createHint(); - const layoutPixelsPerUnit = this.appController.layoutPixelsPerUnit; + const layoutPixelsPerUnit = appController.layoutPixelsPerUnit; const displayPixelsPerUnit = this.displayPixelsPerUnit; const atlasGlyphKeys = atlasGlyphs.map(atlasGlyph => atlasGlyph.glyphKey.sortKey); @@ -707,61 +808,16 @@ class TextDemoView extends DemoView { } } - this.glyphTexCoordsBuffer = unwrapNull(this.gl.createBuffer()); - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.glyphTexCoordsBuffer); - this.gl.bufferData(this.gl.ARRAY_BUFFER, this.glyphBounds, this.gl.STATIC_DRAW); + this.glyphTexCoordsBuffer = unwrapNull(this.renderContext.gl.createBuffer()); + this.renderContext.gl.bindBuffer(this.renderContext.gl.ARRAY_BUFFER, + this.glyphTexCoordsBuffer); + this.renderContext.gl.bufferData(this.renderContext.gl.ARRAY_BUFFER, + this.glyphBounds, + this.renderContext.gl.STATIC_DRAW); } private setIdentityTexScaleUniform(uniforms: UniformMap) { - this.gl.uniform2f(uniforms.uTexScale, 1.0, 1.0); - } - - protected get usedSizeFactor(): glmatrix.vec2 { - const usedSize = glmatrix.vec2.create(); - glmatrix.vec2.div(usedSize, this.appController.atlas.usedSize, ATLAS_SIZE); - return usedSize; - } - - private set panZoomEventsEnabled(flag: boolean) { - if (flag) { - this.camera.onPan = () => this.onPan(); - this.camera.onZoom = () => this.onZoom(); - } else { - this.camera.onPan = null; - this.camera.onZoom = null; - } - } - - get destFramebuffer(): WebGLFramebuffer { - return this.atlasFramebuffer; - } - - get destAllocatedSize(): glmatrix.vec2 { - return ATLAS_SIZE; - } - - get destUsedSize(): glmatrix.vec2 { - return this.appController.atlas.usedSize; - } - - protected get worldTransform(): glmatrix.mat4 { - const transform = glmatrix.mat4.create(); - glmatrix.mat4.translate(transform, transform, [-1.0, -1.0, 0.0]); - glmatrix.mat4.scale(transform, transform, [2.0 / ATLAS_SIZE[0], 2.0 / ATLAS_SIZE[1], 1.0]); - return transform; - } - - protected get directCurveProgramName(): keyof ShaderMap { - return 'directCurve'; - } - - protected get directInteriorProgramName(): keyof ShaderMap { - return 'directInterior'; - } - - /// Pixels per unit, including dilation. - private get displayPixelsPerUnit(): number { - return this.appController.layoutPixelsPerUnit; + this.renderContext.gl.uniform2f(uniforms.uTexScale, 1.0, 1.0); } } diff --git a/demo/client/src/view.ts b/demo/client/src/view.ts index 5f0dd4fc..fa56358d 100644 --- a/demo/client/src/view.ts +++ b/demo/client/src/view.ts @@ -8,9 +8,6 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -// FIXME(pcwalton): This is turning into a fragile inheritance hierarchy. See if we can refactor to -// use composition more. - import * as glmatrix from 'gl-matrix'; import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy"; @@ -19,18 +16,11 @@ import PathfinderBufferTexture from './buffer-texture'; import {Camera} from "./camera"; import {EXTDisjointTimerQuery, QUAD_ELEMENTS, UniformMap} from './gl-utils'; import {PathfinderMeshBuffers, PathfinderMeshData} from './meshes'; +import {Renderer} from './renderer'; import {PathfinderShaderProgram, SHADER_NAMES, ShaderMap} from './shader-loader'; import {ShaderProgramSource, UnlinkedShaderProgram} from './shader-loader'; import {expectNotNull, PathfinderError, UINT32_SIZE, unwrapNull} from './utils'; -const MAX_PATHS: number = 65535; - -const TIME_INTERVAL_DELAY: number = 32; - -const B_LOOP_BLINN_DATA_SIZE: number = 4; -const B_LOOP_BLINN_DATA_TEX_COORD_OFFSET: number = 0; -const B_LOOP_BLINN_DATA_SIGN_OFFSET: number = 2; - const QUAD_POSITIONS: Float32Array = new Float32Array([ 0.0, 1.0, 1.0, 1.0, @@ -58,19 +48,23 @@ export interface Timings { declare class WebGLQuery {} export abstract class PathfinderView { - protected canvas: HTMLCanvasElement; + canvas: HTMLCanvasElement; - protected camera: Camera; + protected abstract get camera(): Camera; private dirty: boolean; constructor() { this.dirty = false; - this.canvas = unwrapNull(document.getElementById('pf-canvas')) as HTMLCanvasElement; - window.addEventListener('resize', () => this.resizeToFit(false), false); - this.resizeToFit(true); + } + + setDirty() { + if (this.dirty) + return; + this.dirty = true; + window.requestAnimationFrame(() => this.redraw()); } zoomIn(): void { @@ -85,18 +79,11 @@ export abstract class PathfinderView { this.setDirty(); } - protected setDirty() { - if (this.dirty) - return; - this.dirty = true; - window.requestAnimationFrame(() => this.redraw()); - } - protected redraw() { this.dirty = false; } - private resizeToFit(initialSize: boolean) { + protected resizeToFit(initialSize: boolean) { const width = window.innerWidth; let height = window.scrollY + window.innerHeight - this.canvas.getBoundingClientRect().top; @@ -121,7 +108,9 @@ export abstract class PathfinderView { } } -export abstract class DemoView extends PathfinderView implements Renderer, RenderContext { +export abstract class DemoView extends PathfinderView implements RenderContext { + readonly renderer: Renderer; + gl: WebGLRenderingContext; shaderPrograms: ShaderMap; @@ -129,29 +118,19 @@ export abstract class DemoView extends PathfinderView implements Renderer, Rende drawBuffersExt: WebGLDrawBuffers; instancedArraysExt: ANGLEInstancedArrays; textureHalfFloatExt: OESTextureHalfFloat; + timerQueryExt: EXTDisjointTimerQuery; vertexArrayObjectExt: OESVertexArrayObject; quadPositionsBuffer: WebGLBuffer; quadTexCoordsBuffer: WebGLBuffer; quadElementsBuffer: WebGLBuffer; + atlasRenderingTimerQuery: WebGLQuery; + compositingTimerQuery: WebGLQuery; + meshes: PathfinderMeshBuffers[]; meshData: PathfinderMeshData[]; - pathTransformBufferTextures: PathfinderBufferTexture[]; - - get bgColor(): glmatrix.vec4 | null { - return null; - } - - get fgColor(): glmatrix.vec4 | null { - return null; - } - - get emboldenAmount(): glmatrix.vec2 { - return glmatrix.vec2.create(); - } - get colorAlphaFormat(): GLenum { return this.sRGBExt == null ? this.gl.RGBA : this.sRGBExt.SRGB_ALPHA_EXT; } @@ -161,70 +140,24 @@ export abstract class DemoView extends PathfinderView implements Renderer, Rende } protected sRGBExt: EXTsRGB; - protected timerQueryExt: EXTDisjointTimerQuery; - protected antialiasingStrategy: AntialiasingStrategy | null; protected colorBufferHalfFloatExt: any; - protected pathColorsBufferTextures: PathfinderBufferTexture[]; - - protected lastTimings: Timings; - - protected get pathIDsAreInstanced(): boolean { - return false; - } - - private instancedPathIDVBO: WebGLBuffer | null; - - private atlasRenderingTimerQuery: WebGLQuery; - private compositingTimerQuery: WebGLQuery; - private timerQueryPollInterval: number | null; - private wantsScreenshot: boolean; + /// NB: All subclasses are responsible for creating a renderer in their constructors. constructor(commonShaderSource: string, shaderSources: ShaderMap) { super(); this.initContext(); - this.lastTimings = { rendering: 0, compositing: 0 }; - const shaderSource = this.compileShaders(commonShaderSource, shaderSources); this.shaderPrograms = this.linkShaders(shaderSource); - this.pathTransformBufferTextures = []; - this.pathColorsBufferTextures = []; - - if (this.pathIDsAreInstanced) - this.initInstancedPathIDVBO(); - this.wantsScreenshot = false; - - this.antialiasingStrategy = new NoAAStrategy(0, 'none'); - this.antialiasingStrategy.init(this); } - setAntialiasingOptions(aaType: AntialiasingStrategyName, - aaLevel: number, - subpixelAA: SubpixelAAType, - stemDarkening: StemDarkeningMode) { - this.antialiasingStrategy = this.createAAStrategy(aaType, - aaLevel, - subpixelAA, - stemDarkening); - - const canvas = this.canvas; - this.antialiasingStrategy.init(this); - if (this.meshData != null) - this.antialiasingStrategy.attachMeshes(this); - - this.setDirty(); - } - - attachMeshes(meshes: PathfinderMeshData[]) { - this.meshData = meshes; - this.meshes = meshes.map(meshes => new PathfinderMeshBuffers(this.gl, meshes)); - unwrapNull(this.antialiasingStrategy).attachMeshes(this); - + attachMeshes(meshes: PathfinderMeshData[]): void { + this.renderer.attachMeshes(meshes); this.setDirty(); } @@ -238,81 +171,21 @@ export abstract class DemoView extends PathfinderView implements Renderer, Rende this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.quadElementsBuffer); } - setFramebufferSizeUniform(uniforms: UniformMap) { - const currentViewport = this.gl.getParameter(this.gl.VIEWPORT); - this.gl.uniform2i(uniforms.uFramebufferSize, currentViewport[2], currentViewport[3]); - } - - setTransformSTUniform(uniforms: UniformMap, objectIndex: number) { - // FIXME(pcwalton): Lossy conversion from a 4x4 matrix to an ST matrix is ugly and fragile. - // Refactor. - const transform = glmatrix.mat4.clone(this.worldTransform); - glmatrix.mat4.mul(transform, transform, this.getModelviewTransform(objectIndex)); - - const translation = glmatrix.vec4.clone([transform[12], transform[13], 0.0, 1.0]); - - this.gl.uniform4f(uniforms.uTransformST, - transform[0], - transform[5], - transform[12], - transform[13]); - } - - setTransformSTAndTexScaleUniformsForDest(uniforms: UniformMap): void { - const usedSize = this.usedSizeFactor; - this.gl.uniform4f(uniforms.uTransformST, 2.0 * usedSize[0], 2.0 * usedSize[1], -1.0, -1.0); - this.gl.uniform2f(uniforms.uTexScale, usedSize[0], usedSize[1]); - } - - setTransformAndTexScaleUniformsForDest(uniforms: UniformMap): void { - const usedSize = this.usedSizeFactor; - - const transform = glmatrix.mat4.create(); - glmatrix.mat4.fromTranslation(transform, [-1.0, -1.0, 0.0]); - glmatrix.mat4.scale(transform, transform, [2.0 * usedSize[0], 2.0 * usedSize[1], 1.0]); - this.gl.uniformMatrix4fv(uniforms.uTransform, false, transform); - - this.gl.uniform2f(uniforms.uTexScale, usedSize[0], usedSize[1]); - } - queueScreenshot() { this.wantsScreenshot = true; this.setDirty(); } - uploadPathColors(objectCount: number) { - this.pathColorsBufferTextures = []; - - for (let objectIndex = 0; objectIndex < objectCount; objectIndex++) { - const pathColorsBufferTexture = new PathfinderBufferTexture(this.gl, 'uPathColors'); - const pathColors = this.pathColorsForObject(objectIndex); - pathColorsBufferTexture.upload(this.gl, pathColors); - this.pathColorsBufferTextures.push(pathColorsBufferTexture); - } + setAntialiasingOptions(aaType: AntialiasingStrategyName, + aaLevel: number, + subpixelAA: SubpixelAAType, + stemDarkening: StemDarkeningMode) { + this.renderer.setAntialiasingOptions(aaType, aaLevel, subpixelAA, stemDarkening); } - uploadPathTransforms(objectCount: number) { - this.pathTransformBufferTextures = []; - - for (let objectIndex = 0; objectIndex < objectCount; objectIndex++) { - const pathTransformBufferTexture = new PathfinderBufferTexture(this.gl, - 'uPathTransform'); - - const pathTransforms = this.pathTransformsForObject(objectIndex); - pathTransformBufferTexture.upload(this.gl, pathTransforms); - this.pathTransformBufferTextures.push(pathTransformBufferTexture); - } - } - - abstract setHintsUniform(uniforms: UniformMap): void; - abstract pathBoundingRects(objectIndex: number): Float32Array; - abstract pathCountForObject(objectIndex: number): number; - protected resized(): void { super.resized(); - - if (this.antialiasingStrategy != null) - this.antialiasingStrategy.init(this); + this.renderer.canvasResized(); } protected initContext() { @@ -350,46 +223,7 @@ export abstract class DemoView extends PathfinderView implements Renderer, Rende protected redraw() { super.redraw(); - if (this.meshes == null) - return; - - // Start timing rendering. - if (this.timerQueryPollInterval == null) { - this.timerQueryExt.beginQueryEXT(this.timerQueryExt.TIME_ELAPSED_EXT, - this.atlasRenderingTimerQuery); - } - - // Prepare for direct rendering. - const antialiasingStrategy = unwrapNull(this.antialiasingStrategy); - antialiasingStrategy.prepare(this); - - // Clear. - this.clearForDirectRendering(); - - // Draw "scenery" (used in the 3D view). - this.drawSceneryIfNecessary(); - - // Perform direct rendering (Loop-Blinn). - if (antialiasingStrategy.shouldRenderDirect) - this.renderDirect(); - - // Antialias. - antialiasingStrategy.antialias(this); - - // End the timer, and start a new one. - if (this.timerQueryPollInterval == null) { - this.timerQueryExt.endQueryEXT(this.timerQueryExt.TIME_ELAPSED_EXT); - this.timerQueryExt.beginQueryEXT(this.timerQueryExt.TIME_ELAPSED_EXT, - this.compositingTimerQuery); - } - - antialiasingStrategy.resolve(this); - - // Draw the glyphs with the resolved atlas to the default framebuffer. - this.compositeIfNecessary(); - - // Finish timing. - this.finishTiming(); + this.renderer.redraw(); // Invoke the post-render hook. this.renderingFinished(); @@ -403,42 +237,6 @@ export abstract class DemoView extends PathfinderView implements Renderer, Rende protected renderingFinished(): void {} - protected getModelviewTransform(pathIndex: number): glmatrix.mat4 { - return glmatrix.mat4.create(); - } - - protected drawSceneryIfNecessary(): void {} - - protected clearForDirectRendering(): void { - this.gl.clearColor(1.0, 1.0, 1.0, 1.0); - this.gl.clearDepth(0.0); - this.gl.depthMask(true); - this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); - } - - protected shouldRenderObject(objectIndex: number): boolean { - return true; - } - - protected newTimingsReceived() {} - - protected abstract pathColorsForObject(objectIndex: number): Uint8Array; - protected abstract pathTransformsForObject(objectIndex: number): Float32Array; - - protected abstract get depthFunction(): number; - - protected abstract createAAStrategy(aaType: AntialiasingStrategyName, - aaLevel: number, - subpixelAA: SubpixelAAType, - stemDarkening: StemDarkeningMode): - AntialiasingStrategy; - - protected abstract compositeIfNecessary(): void; - - protected meshInstanceCountForObject(objectIndex: number): number { - return 1; - } - private compileShaders(commonSource: string, shaderSources: ShaderMap): ShaderMap { const shaders: Partial>> = {}; @@ -483,228 +281,6 @@ export abstract class DemoView extends PathfinderView implements Renderer, Rende return shaderProgramMap as ShaderMap; } - private initInstancedPathIDVBO(): void { - const pathIDs = new Uint16Array(MAX_PATHS); - for (let pathIndex = 0; pathIndex < MAX_PATHS; pathIndex++) - pathIDs[pathIndex] = pathIndex + 1; - - this.instancedPathIDVBO = this.gl.createBuffer(); - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.instancedPathIDVBO); - this.gl.bufferData(this.gl.ARRAY_BUFFER, pathIDs, this.gl.STATIC_DRAW); - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null); - } - - private setTransformUniform(uniforms: UniformMap, objectIndex: number) { - const transform = glmatrix.mat4.clone(this.worldTransform); - glmatrix.mat4.mul(transform, transform, this.getModelviewTransform(objectIndex)); - this.gl.uniformMatrix4fv(uniforms.uTransform, false, transform); - } - - private renderDirect() { - for (let objectIndex = 0; objectIndex < this.meshes.length; objectIndex++) { - if (!this.shouldRenderObject(objectIndex)) - continue; - - const meshes = this.meshes[objectIndex]; - - let instanceCount: number | null; - if (!this.pathIDsAreInstanced) - instanceCount = null; - else - instanceCount = this.meshInstanceCountForObject(objectIndex); - - // Set up implicit cover state. - this.gl.depthFunc(this.depthFunction); - this.gl.depthMask(true); - this.gl.enable(this.gl.DEPTH_TEST); - this.gl.disable(this.gl.BLEND); - - // Set up the implicit cover interior VAO. - // - // TODO(pcwalton): Cache these. - const directInteriorProgram = this.shaderPrograms[this.directInteriorProgramName]; - const implicitCoverInteriorVAO = this.vertexArrayObjectExt.createVertexArrayOES(); - this.vertexArrayObjectExt.bindVertexArrayOES(implicitCoverInteriorVAO); - this.initImplicitCoverInteriorVAO(objectIndex); - - // Draw direct interior parts. - this.setTransformUniform(directInteriorProgram.uniforms, objectIndex); - this.setFramebufferSizeUniform(directInteriorProgram.uniforms); - this.setHintsUniform(directInteriorProgram.uniforms); - this.pathColorsBufferTextures[objectIndex].bind(this.gl, - directInteriorProgram.uniforms, - 0); - this.pathTransformBufferTextures[objectIndex].bind(this.gl, - directInteriorProgram.uniforms, - 1); - let indexCount = this.gl.getBufferParameter(this.gl.ELEMENT_ARRAY_BUFFER, - this.gl.BUFFER_SIZE) / UINT32_SIZE; - if (instanceCount == null) { - this.gl.drawElements(this.gl.TRIANGLES, indexCount, this.gl.UNSIGNED_INT, 0); - } else { - this.instancedArraysExt.drawElementsInstancedANGLE(this.gl.TRIANGLES, - indexCount, - this.gl.UNSIGNED_INT, - 0, - instanceCount); - } - - // Set up direct curve state. - this.gl.depthMask(false); - this.gl.enable(this.gl.BLEND); - this.gl.blendEquation(this.gl.FUNC_ADD); - this.gl.blendFuncSeparate(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA, - this.gl.ONE, this.gl.ONE); - - // Set up the direct curve VAO. - // - // TODO(pcwalton): Cache these. - const directCurveProgram = this.shaderPrograms[this.directCurveProgramName]; - const implicitCoverCurveVAO = this.vertexArrayObjectExt.createVertexArrayOES(); - this.vertexArrayObjectExt.bindVertexArrayOES(implicitCoverCurveVAO); - this.initImplicitCoverCurveVAO(objectIndex); - - // Draw direct curve parts. - this.setTransformUniform(directCurveProgram.uniforms, objectIndex); - this.setFramebufferSizeUniform(directCurveProgram.uniforms); - this.setHintsUniform(directInteriorProgram.uniforms); - this.pathColorsBufferTextures[objectIndex].bind(this.gl, - directCurveProgram.uniforms, - 0); - this.pathTransformBufferTextures[objectIndex].bind(this.gl, - directCurveProgram.uniforms, - 1); - indexCount = this.gl.getBufferParameter(this.gl.ELEMENT_ARRAY_BUFFER, - this.gl.BUFFER_SIZE) / UINT32_SIZE; - if (instanceCount == null) { - this.gl.drawElements(this.gl.TRIANGLES, indexCount, this.gl.UNSIGNED_INT, 0); - } else { - this.instancedArraysExt.drawElementsInstancedANGLE(this.gl.TRIANGLES, - indexCount, - this.gl.UNSIGNED_INT, - 0, - instanceCount); - } - - this.vertexArrayObjectExt.bindVertexArrayOES(null); - } - } - - private initImplicitCoverInteriorVAO(objectIndex: number): void { - const meshes = this.meshes[objectIndex]; - - const directInteriorProgram = this.shaderPrograms[this.directInteriorProgramName]; - this.gl.useProgram(directInteriorProgram.program); - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, meshes.bVertexPositions); - this.gl.vertexAttribPointer(directInteriorProgram.attributes.aPosition, - 2, - this.gl.FLOAT, - false, - 0, - 0); - - if (this.pathIDsAreInstanced) - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.instancedPathIDVBO); - else - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, meshes.bVertexPathIDs); - this.gl.vertexAttribPointer(directInteriorProgram.attributes.aPathID, - 1, - this.gl.UNSIGNED_SHORT, - false, - 0, - 0); - if (this.pathIDsAreInstanced) { - this.instancedArraysExt - .vertexAttribDivisorANGLE(directInteriorProgram.attributes.aPathID, 1); - } - - this.gl.enableVertexAttribArray(directInteriorProgram.attributes.aPosition); - this.gl.enableVertexAttribArray(directInteriorProgram.attributes.aPathID); - this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, meshes.coverInteriorIndices); - } - - private initImplicitCoverCurveVAO(objectIndex: number): void { - const meshes = this.meshes[objectIndex]; - - const directCurveProgram = this.shaderPrograms[this.directCurveProgramName]; - this.gl.useProgram(directCurveProgram.program); - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, meshes.bVertexPositions); - this.gl.vertexAttribPointer(directCurveProgram.attributes.aPosition, - 2, - this.gl.FLOAT, - false, - 0, - 0); - - if (this.pathIDsAreInstanced) - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.instancedPathIDVBO); - else - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, meshes.bVertexPathIDs); - this.gl.vertexAttribPointer(directCurveProgram.attributes.aPathID, - 1, - this.gl.UNSIGNED_SHORT, - false, - 0, - 0); - if (this.pathIDsAreInstanced) { - this.instancedArraysExt - .vertexAttribDivisorANGLE(directCurveProgram.attributes.aPathID, 1); - } - - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, meshes.bVertexLoopBlinnData); - this.gl.vertexAttribPointer(directCurveProgram.attributes.aTexCoord, - 2, - this.gl.UNSIGNED_BYTE, - false, - B_LOOP_BLINN_DATA_SIZE, - B_LOOP_BLINN_DATA_TEX_COORD_OFFSET); - this.gl.vertexAttribPointer(directCurveProgram.attributes.aSign, - 1, - this.gl.BYTE, - false, - B_LOOP_BLINN_DATA_SIZE, - B_LOOP_BLINN_DATA_SIGN_OFFSET); - this.gl.enableVertexAttribArray(directCurveProgram.attributes.aPosition); - this.gl.enableVertexAttribArray(directCurveProgram.attributes.aTexCoord); - this.gl.enableVertexAttribArray(directCurveProgram.attributes.aPathID); - this.gl.enableVertexAttribArray(directCurveProgram.attributes.aSign); - this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, meshes.coverCurveIndices); - } - - private finishTiming() { - if (this.timerQueryPollInterval != null) - return; - - this.timerQueryExt.endQueryEXT(this.timerQueryExt.TIME_ELAPSED_EXT); - - this.timerQueryPollInterval = window.setInterval(() => { - for (const queryName of ['atlasRenderingTimerQuery', 'compositingTimerQuery'] as - Array<'atlasRenderingTimerQuery' | 'compositingTimerQuery'>) { - if (this.timerQueryExt.getQueryObjectEXT(this[queryName], - this.timerQueryExt - .QUERY_RESULT_AVAILABLE_EXT) === 0) { - return; - } - } - - const atlasRenderingTime = - this.timerQueryExt.getQueryObjectEXT(this.atlasRenderingTimerQuery, - this.timerQueryExt.QUERY_RESULT_EXT); - const compositingTime = - this.timerQueryExt.getQueryObjectEXT(this.compositingTimerQuery, - this.timerQueryExt.QUERY_RESULT_EXT); - this.lastTimings = { - compositing: compositingTime / 1000000.0, - rendering: atlasRenderingTime / 1000000.0, - }; - - this.newTimingsReceived(); - - window.clearInterval(this.timerQueryPollInterval!); - this.timerQueryPollInterval = null; - }, TIME_INTERVAL_DELAY); - } - private takeScreenshot() { const width = this.canvas.width, height = this.canvas.height; const scratchCanvas = document.createElement('canvas'); @@ -721,18 +297,6 @@ export abstract class DemoView extends PathfinderView implements Renderer, Rende scratchLink.click(); document.body.removeChild(scratchLink); } - - abstract get destFramebuffer(): WebGLFramebuffer | null; - - abstract get destAllocatedSize(): glmatrix.vec2; - abstract get destUsedSize(): glmatrix.vec2; - - protected abstract get usedSizeFactor(): glmatrix.vec2; - - protected abstract get worldTransform(): glmatrix.mat4; - - protected abstract get directCurveProgramName(): keyof ShaderMap; - protected abstract get directInteriorProgramName(): keyof ShaderMap; } export interface RenderContext { @@ -743,40 +307,19 @@ export interface RenderContext { readonly drawBuffersExt: WebGLDrawBuffers; readonly instancedArraysExt: ANGLEInstancedArrays; readonly textureHalfFloatExt: OESTextureHalfFloat; + readonly timerQueryExt: EXTDisjointTimerQuery; readonly vertexArrayObjectExt: OESVertexArrayObject; + readonly colorAlphaFormat: GLenum; + readonly shaderPrograms: ShaderMap; readonly quadPositionsBuffer: WebGLBuffer; readonly quadElementsBuffer: WebGLBuffer; + readonly atlasRenderingTimerQuery: WebGLQuery; + readonly compositingTimerQuery: WebGLQuery; + initQuadVAO(attributes: any): void; -} - -export interface Renderer { - readonly renderContext: RenderContext; - - readonly destFramebuffer: WebGLFramebuffer | null; - readonly pathTransformBufferTextures: PathfinderBufferTexture[]; - - readonly meshes: PathfinderMeshBuffers[]; - readonly meshData: PathfinderMeshData[]; - - readonly colorAlphaFormat: GLenum; - - readonly destAllocatedSize: glmatrix.vec2; - readonly destUsedSize: glmatrix.vec2; - - readonly emboldenAmount: glmatrix.vec2; - - readonly bgColor: glmatrix.vec4 | null; - readonly fgColor: glmatrix.vec4 | null; - - pathBoundingRects(objectIndex: number): Float32Array; - - setFramebufferSizeUniform(uniforms: UniformMap): void; - setHintsUniform(uniforms: UniformMap): void; - setTransformAndTexScaleUniformsForDest(uniforms: UniformMap): void; - setTransformSTAndTexScaleUniformsForDest(uniforms: UniformMap): void; - setTransformSTUniform(uniforms: UniformMap, objectIndex: number): void; + setDirty(): void; } diff --git a/demo/client/src/xcaa-strategy.ts b/demo/client/src/xcaa-strategy.ts index f8ca694d..a6a9bee6 100644 --- a/demo/client/src/xcaa-strategy.ts +++ b/demo/client/src/xcaa-strategy.ts @@ -17,10 +17,11 @@ import {createFramebuffer, createFramebufferColorTexture} from './gl-utils'; import {createFramebufferDepthTexture, setTextureParameters, UniformMap} from './gl-utils'; import {WebGLVertexArrayObject} from './gl-utils'; import {B_QUAD_LOWER_INDICES_OFFSET, B_QUAD_SIZE, B_QUAD_UPPER_INDICES_OFFSET} from './meshes'; +import {Renderer} from './renderer'; import {PathfinderShaderProgram} from './shader-loader'; import {computeStemDarkeningAmount} from './text'; import {FLOAT32_SIZE, lerp, UINT32_SIZE, unwrapNull} from './utils'; -import {RenderContext, Renderer} from './view'; +import {RenderContext} from './view'; interface FastEdgeVAOs { upper: WebGLVertexArrayObject; @@ -145,10 +146,10 @@ export abstract class XCAAStrategy extends AntialiasingStrategy { const renderContext = renderer.renderContext; this.directColorTexture = createFramebufferColorTexture(renderContext.gl, this.destFramebufferSize, - renderer.colorAlphaFormat); + renderContext.colorAlphaFormat); this.directPathIDTexture = createFramebufferColorTexture(renderContext.gl, this.destFramebufferSize, - renderer.colorAlphaFormat); + renderContext.colorAlphaFormat); this.directFramebuffer = createFramebuffer(renderContext.gl, renderContext.drawBuffersExt, @@ -1049,10 +1050,10 @@ export class MCAAMulticolorStrategy extends MCAAStrategy { const renderContext = renderer.renderContext; this.bgColorTexture = createFramebufferColorTexture(renderContext.gl, this.supersampledFramebufferSize, - renderer.colorAlphaFormat); + renderContext.colorAlphaFormat); this.fgColorTexture = createFramebufferColorTexture(renderContext.gl, this.supersampledFramebufferSize, - renderer.colorAlphaFormat); + renderContext.colorAlphaFormat); this.edgeDetectFramebuffer = createFramebuffer(renderContext.gl, renderContext.drawBuffersExt, [this.bgColorTexture, this.fgColorTexture],