Add opaque tiles

This commit is contained in:
Patrick Walton 2018-12-03 16:19:10 -08:00
parent 0929a98e2a
commit b51028568b
5 changed files with 242 additions and 47 deletions

21
demo2/opaque.fs.glsl Normal file
View File

@ -0,0 +1,21 @@
#version 300 es
// pathfinder/demo2/opaque.fs.glsl
//
// Copyright © 2018 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
precision highp float;
in vec4 vColor;
out vec4 oFragColor;
void main() {
oFragColor = vColor;
}

29
demo2/opaque.vs.glsl Normal file
View File

@ -0,0 +1,29 @@
#version 300 es
// pathfinder/demo2/opaque.vs.glsl
//
// Copyright © 2018 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
precision highp float;
uniform vec2 uFramebufferSize;
uniform vec2 uTileSize;
in vec2 aTessCoord;
in vec2 aTileOrigin;
in vec4 aColor;
out vec4 vColor;
void main() {
vec2 position = aTileOrigin + uTileSize * aTessCoord;
vColor = aColor;
gl_Position = vec4((position / uFramebufferSize * 2.0 - 1.0) * vec2(1.0, -1.0), 0.0, 1.0);
}

View File

@ -11,11 +11,44 @@
"commander": "^2.15.1"
}
},
"@types/bezier-js": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/bezier-js/-/bezier-js-0.0.6.tgz",
"integrity": "sha1-DZdtaBY8SVUzLveYohoLPWOB0Ss="
},
"@types/node": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.2.tgz",
"integrity": "sha512-RO4ig5taKmcrU4Rex8ojG1gpwFkjddzug9iPQSDvbewHN9vDpcFewevkaOK+KT+w1LeZnxbgOyfXwV4pxsQ4GQ=="
},
"@types/opentype.js": {
"version": "0.0.0",
"resolved": "http://registry.npmjs.org/@types/opentype.js/-/opentype.js-0.0.0.tgz",
"integrity": "sha1-AvZD18Y8Pr9PZG/GBa5Gg/hJrxs="
},
"@types/pdfkit": {
"version": "0.7.36",
"resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.7.36.tgz",
"integrity": "sha512-9eRA6MuW+n78yU3HhoIrDxjyAX2++B5MpLDYqHOnaRTquCw+5sYXT+QN8E1eSaxvNUwlRfU3tOm4UzTeGWmBqg==",
"requires": {
"@types/node": "*"
}
},
"@types/webgl2": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.4.tgz",
"integrity": "sha512-PACt1xdErJbMUOUweSrbVM7gSIYm1vTncW2hF6Os/EeWi6TXYAYMPp+8v6rzHmypE5gHrxaxZNXgMkJVIdZpHw=="
},
"bezier-js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-2.3.1.tgz",
"integrity": "sha512-nFpFL9tuayvlHfWh6xM7OHeTZvwr74+6KnzO3eNZMt0BC0cqb9lCTc9C8OVzrHBvbrNwriTw7XaF2SBsWQJLZA=="
},
"clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
},
"color-convert": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz",
@ -180,6 +213,11 @@
"integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
"dev": true
},
"graham_scan": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/graham_scan/-/graham_scan-1.0.4.tgz",
"integrity": "sha1-OZZR3R+DU+GID1nqjl+Uud9Mkoo="
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
@ -192,6 +230,20 @@
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true
},
"makerjs": {
"version": "0.9.93",
"resolved": "https://registry.npmjs.org/makerjs/-/makerjs-0.9.93.tgz",
"integrity": "sha512-XztS2tJEpL01dsZ5P11kGO2MitrD6MsSvkNDS3uUDVN9SVDJn33Q2j9DFruJsiRtqNVRseLK52vkkbWKtLB/tA==",
"requires": {
"@types/bezier-js": "^0.0.6",
"@types/node": "^7.0.5",
"@types/opentype.js": "^0.0.0",
"@types/pdfkit": "^0.7.34",
"bezier-js": "^2.1.0",
"clone": "^1.0.2",
"graham_scan": "^1.0.4"
}
},
"map-limit": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/map-limit/-/map-limit-0.0.1.tgz",
@ -263,6 +315,14 @@
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
"dev": true
},
"svg-path-outline": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/svg-path-outline/-/svg-path-outline-1.0.1.tgz",
"integrity": "sha1-w5Zk922IdGW4voXER3jIg0Jomas=",
"requires": {
"makerjs": "^0.9.39"
}
},
"svgpath": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/svgpath/-/svgpath-2.2.1.tgz",

View File

@ -7,6 +7,7 @@
"dependencies": {
"@types/webgl2": "0.0.4",
"parse-color": "^1.0.0",
"svg-path-outline": "^1.0.1",
"svgpath": "^2.2.1"
}
}

View File

@ -10,6 +10,8 @@
import COVER_VERTEX_SHADER_SOURCE from "./cover.vs.glsl";
import COVER_FRAGMENT_SHADER_SOURCE from "./cover.fs.glsl";
import OPAQUE_VERTEX_SHADER_SOURCE from "./opaque.vs.glsl";
import OPAQUE_FRAGMENT_SHADER_SOURCE from "./opaque.fs.glsl";
import STENCIL_VERTEX_SHADER_SOURCE from "./stencil.vs.glsl";
import STENCIL_FRAGMENT_SHADER_SOURCE from "./stencil.fs.glsl";
import SVG from "../resources/svg/Ghostscript_Tiger.svg";
@ -20,6 +22,7 @@ import {staticCast, unwrapNull} from "./util";
const SVGPath: (path: string) => SVGPath = require('svgpath');
const parseColor: (color: string) => any = require('parse-color');
const svgPathOutline: any = require('svg-path-outline');
const SVG_NS: string = "http://www.w3.org/2000/svg";
@ -58,6 +61,8 @@ class App {
private stencilFramebuffer: WebGLFramebuffer;
private stencilProgram: Program<'FramebufferSize' | 'TileSize' | 'AreaLUT',
'TessCoord' | 'From' | 'Ctrl' | 'To' | 'TileIndex'>;
private opaqueProgram: Program<'FramebufferSize' | 'TileSize',
'TessCoord' | 'TileOrigin' | 'Color'>;
private coverProgram:
Program<'FramebufferSize' | 'TileSize' | 'StencilTexture' | 'StencilTextureSize',
'TessCoord' | 'TileOrigin' | 'TileIndex' | 'Color'>;
@ -65,12 +70,15 @@ class App {
private stencilVertexPositionsBuffer: WebGLBuffer;
private stencilVertexTileIndicesBuffer: WebGLBuffer;
private stencilVertexArray: WebGLVertexArrayObject;
private opaqueVertexBuffer: WebGLBuffer;
private opaqueVertexArray: WebGLVertexArrayObject;
private coverVertexBuffer: WebGLBuffer;
private coverVertexArray: WebGLVertexArrayObject;
private scene: Scene | null;
private primitiveCount: number;
private tileCount: number;
private opaqueTileCount: number;
constructor(svg: XMLDocument, areaLUT: HTMLImageElement) {
const canvas = staticCast(document.getElementById('canvas'), HTMLCanvasElement);
@ -135,6 +143,13 @@ class App {
['TessCoord', 'TileOrigin', 'TileIndex', 'Color']);
this.coverProgram = coverProgram;
const opaqueProgram = new Program(gl,
OPAQUE_VERTEX_SHADER_SOURCE,
OPAQUE_FRAGMENT_SHADER_SOURCE,
['FramebufferSize', 'TileSize'],
['TessCoord', 'TileOrigin', 'Color']);
this.opaqueProgram = opaqueProgram;
const stencilProgram = new Program(gl,
STENCIL_VERTEX_SHADER_SOURCE,
STENCIL_FRAGMENT_SHADER_SOURCE,
@ -182,6 +197,29 @@ class App {
gl.enableVertexAttribArray(stencilProgram.attributes.To);
gl.enableVertexAttribArray(stencilProgram.attributes.TileIndex);
// Initialize opaque VBO.
this.opaqueVertexBuffer = unwrapNull(gl.createBuffer());
// Initialize opaque VAO.
this.opaqueVertexArray = unwrapNull(gl.createVertexArray());
gl.bindVertexArray(this.opaqueVertexArray);
gl.useProgram(this.opaqueProgram.program);
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVertexBuffer);
gl.vertexAttribPointer(opaqueProgram.attributes.TessCoord,
2,
gl.UNSIGNED_BYTE,
false,
0,
0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.opaqueVertexBuffer);
gl.vertexAttribPointer(opaqueProgram.attributes.TileOrigin, 2, gl.SHORT, false, 10, 0);
gl.vertexAttribDivisor(opaqueProgram.attributes.TileOrigin, 1);
gl.vertexAttribPointer(opaqueProgram.attributes.Color, 4, gl.UNSIGNED_BYTE, true, 10, 6);
gl.vertexAttribDivisor(opaqueProgram.attributes.Color, 1);
gl.enableVertexAttribArray(opaqueProgram.attributes.TessCoord);
gl.enableVertexAttribArray(opaqueProgram.attributes.TileOrigin);
gl.enableVertexAttribArray(opaqueProgram.attributes.Color);
// Initialize cover VBO.
this.coverVertexBuffer = unwrapNull(gl.createBuffer());
@ -214,6 +252,7 @@ class App {
this.scene = null;
this.primitiveCount = 0;
this.tileCount = 0;
this.opaqueTileCount = 0;
}
redraw(): void {
@ -276,13 +315,23 @@ class App {
//console.log(stencilData);
*/
// Cover.
// Draw opaque tiles.
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
const framebufferSize = {width: canvas.width, height: canvas.height};
gl.viewport(0, 0, framebufferSize.width, framebufferSize.height);
gl.clearColor(1.0, 1.0, 1.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.bindVertexArray(this.opaqueVertexArray);
gl.useProgram(this.opaqueProgram.program);
gl.uniform2f(this.opaqueProgram.uniforms.FramebufferSize,
framebufferSize.width,
framebufferSize.height);
gl.uniform2f(this.opaqueProgram.uniforms.TileSize, TILE_SIZE.width, TILE_SIZE.height);
gl.disable(gl.BLEND);
gl.drawArraysInstanced(gl.TRIANGLE_FAN, 0, 4, this.opaqueTileCount);
// Cover.
gl.bindVertexArray(this.coverVertexArray);
gl.useProgram(this.coverProgram.program);
gl.uniform2f(this.coverProgram.uniforms.FramebufferSize,
@ -315,6 +364,35 @@ class App {
prepare(): void {
const gl = this.gl, scene = unwrapNull(this.scene);
// Construct opaque tile VBOs.
this.opaqueTileCount = 0;
const opaqueVertexData: number[] = [];
const opaqueTiles: number[][] = [];
for (let pathIndex = scene.pathTileStrips.length - 1; pathIndex >= 0; pathIndex--) {
const pathTileStrips = scene.pathTileStrips[pathIndex];
for (const tileStrip of pathTileStrips) {
for (const tile of tileStrip.tiles) {
// TODO(pcwalton)
const color = scene.pathColors[pathIndex];
if (!tile.isFilled())
continue;
if (opaqueTiles[tile.tileLeft] == null)
opaqueTiles[tile.tileLeft] = [];
if (opaqueTiles[tile.tileLeft][tileStrip.tileTop] != null)
continue;
opaqueTiles[tile.tileLeft][tileStrip.tileTop] = pathIndex;
opaqueVertexData.push(Math.floor(tile.tileLeft),
Math.floor(tileStrip.tileTop),
0,
color.r | (color.g << 8),
color.b | (color.a << 8));
this.opaqueTileCount++;
}
}
}
// Construct stencil and cover VBOs.
this.tileCount = 0;
let primitiveCount = 0;
@ -324,10 +402,16 @@ class App {
const pathTileStrips = scene.pathTileStrips[pathIndex];
for (const tileStrip of pathTileStrips) {
for (const tile of tileStrip.tiles) {
// TODO(pcwalton)
const color = scene.pathColors[pathIndex];
if (tile.isFilled())
continue;
if (opaqueTiles[tile.tileLeft] != null &&
opaqueTiles[tile.tileLeft][tileStrip.tileTop] != null &&
pathIndex <= opaqueTiles[tile.tileLeft][tileStrip.tileTop]) {
continue;
}
for (const edge of tile.edges) {
let ctrl;
if (edge.ctrl == null)
@ -341,7 +425,6 @@ class App {
primitiveCount++;
}
const color = scene.pathColors[pathIndex];
coverVertexData.push(Math.floor(tile.tileLeft),
Math.floor(tileStrip.tileTop),
this.tileCount,
@ -359,6 +442,10 @@ class App {
gl.bindBuffer(gl.ARRAY_BUFFER, this.stencilVertexTileIndicesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Uint16Array(stencilVertexTileIndices), gl.STATIC_DRAW);
// Populate the opaque VBO.
gl.bindBuffer(gl.ARRAY_BUFFER, this.opaqueVertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Int16Array(opaqueVertexData), gl.DYNAMIC_DRAW);
// Populate the cover VBO.
gl.bindBuffer(gl.ARRAY_BUFFER, this.coverVertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Int16Array(coverVertexData), gl.DYNAMIC_DRAW);
@ -382,46 +469,34 @@ class Scene {
document.body.appendChild(svgElement);
const pathElements = Array.from(document.getElementsByTagName('path'));
const pathColors = [];
const pathColors: any[] = [];
this.pathTileStrips = [];
//const tileDebugger = new TileDebugger(document);
const paths = [];
const paths: SVGPath[] = [];
for (let pathElementIndex = 0;
pathElementIndex < pathElements.length;
pathElementIndex++) {
const pathElement = pathElements[pathElementIndex];
const pathString = unwrapNull(pathElement.getAttribute('d'));
const style = window.getComputedStyle(pathElement);
let paint: string;
if (style.fill != null && style.fill !== 'none') {
paint = style.fill;
/*} else if (style.stroke != null && style.stroke !== 'none') {
paint = style.stroke;*/
} else {
continue;
this.addPath(paths, pathColors, style.fill, pathString);
}
if (style.stroke != null && style.stroke !== 'none') {
/*
const strokeWidth =
style.strokeWidth == null ? 1.0 : parseFloat(style.strokeWidth);
console.log("stroking path:", pathString, strokeWidth);
try {
const strokedPathString = svgPathOutline(pathString, strokeWidth, {joints: 1});
this.addPath(paths, pathColors, style.stroke, strokedPathString);
} catch (e) {}
*/
}
const color = parseColor(paint).rgba;
pathColors.push({
r: color[0],
g: color[1],
b: color[2],
a: Math.round(color[3] * 255.),
});
let path = SVGPath(unwrapNull(pathElement.getAttribute('d')));
path = path.matrix([
GLOBAL_TRANSFORM.a, GLOBAL_TRANSFORM.b,
GLOBAL_TRANSFORM.c, GLOBAL_TRANSFORM.d,
GLOBAL_TRANSFORM.tx, GLOBAL_TRANSFORM.ty,
]);
path = flattenPath(path);
path = canonicalizePath(path);
paths.push(path);
}
const startTime = window.performance.now();
@ -474,6 +549,27 @@ class Scene {
this.pathColors = pathColors;
}
private addPath(paths: SVGPath[], pathColors: any[], paint: string, pathString: string): void {
const color = parseColor(paint).rgba;
pathColors.push({
r: color[0],
g: color[1],
b: color[2],
a: Math.round(color[3] * 255.),
});
let path = SVGPath(pathString);
path = path.matrix([
GLOBAL_TRANSFORM.a, GLOBAL_TRANSFORM.b,
GLOBAL_TRANSFORM.c, GLOBAL_TRANSFORM.d,
GLOBAL_TRANSFORM.tx, GLOBAL_TRANSFORM.ty,
]);
path = flattenPath(path);
path = canonicalizePath(path);
paths.push(path);
}
}
class Program<U extends string, A extends string> {
@ -540,6 +636,11 @@ function flattenPath(path: SVGPath): SVGPath {
const ctrl = new Point2D(0.5 * (ctrl0.x + ctrl1.x), 0.5 * (ctrl0.y + ctrl1.y));
return [['Q', "" + ctrl.x, "" + ctrl.y, "" + to.x, "" + to.y]];
}
if (segment[0] === 'A') {
const to = new Point2D(parseFloat(segment[segment.length - 2]),
parseFloat(segment[segment.length - 1]));
return [['L', "" + to.x, "" + to.y]];
}
return [segment];
});
}
@ -566,23 +667,6 @@ function waitForQuery(gl: WebGL2RenderingContext, disjointTimerQueryExt: any, qu
console.log(elapsed + "ms elapsed");
}
function pathIsRect(path: SVGPath, rectSize: Size2D): boolean {
let result = true;
path.iterate((segment, index) => {
if (segment.length < 3)
return;
const point = new Point2D(parseFloat(segment[1]), parseFloat(segment[2]));
result = result &&
(approxEq(point.x, 0.0) || approxEq(point.x, rectSize.width)) &&
(approxEq(point.y, 0.0) || approxEq(point.y, rectSize.height));
});
return result;
}
function sampleBezier(from: Point2D, ctrl: Point2D, to: Point2D, t: number): Point2D {
return from.lerp(ctrl, t).lerp(ctrl.lerp(to, t), t);
}
function main(): void {
window.fetch(SVG).then(svg => {
svg.text().then(svgText => {