Use a fixed atlas size and reuse textures/FBOs more aggressively
This commit is contained in:
parent
52afa359b4
commit
ed0288906c
|
@ -33,6 +33,8 @@ const B_QUAD_SIZE: number = 4 * 8;
|
|||
const B_QUAD_UPPER_INDICES_OFFSET: number = 0;
|
||||
const B_QUAD_LOWER_INDICES_OFFSET: number = 4 * 4;
|
||||
|
||||
const ATLAS_SIZE: glmatrix.vec2 = glmatrix.vec2.fromValues(2048, 2048);
|
||||
|
||||
const SHADER_URLS: ShaderMap<ShaderProgramURLs> = {
|
||||
blit: {
|
||||
vertex: "/glsl/gles2/blit.vs.glsl",
|
||||
|
@ -393,7 +395,9 @@ class PathfinderMeshBuffers implements Meshes<WebGLBuffer> {
|
|||
}
|
||||
|
||||
class AppController {
|
||||
constructor() {}
|
||||
constructor() {
|
||||
this._atlas = new Atlas;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.fontSize = INITIAL_FONT_SIZE;
|
||||
|
@ -470,7 +474,7 @@ class AppController {
|
|||
}
|
||||
|
||||
meshesReceived() {
|
||||
this.layoutGlyphs();
|
||||
this.rebuildAtlas();
|
||||
this.view.then(view => {
|
||||
view.uploadPathData(this.atlasGlyphs.length);
|
||||
view.attachMeshes(this.meshes);
|
||||
|
@ -484,42 +488,28 @@ class AppController {
|
|||
setFontSize(newPixelsPerEm: number) {
|
||||
this.fontSize = newPixelsPerEm;
|
||||
|
||||
this.layoutGlyphs();
|
||||
this.rebuildAtlas();
|
||||
}
|
||||
|
||||
updateTiming(newTime: number) {
|
||||
this.fpsLabel.innerHTML = `${newTime} ms`;
|
||||
}
|
||||
|
||||
layoutGlyphs() {
|
||||
const pixelsPerUnit = this.fontSize / this.font.unitsPerEm;
|
||||
|
||||
this.atlasSize = new Float32Array([1, 1]) as Size2D;
|
||||
for (const glyph of this.atlasGlyphs) {
|
||||
const metrics = glyph.metrics();
|
||||
const width = Math.ceil((metrics.xMax - metrics.xMin) * pixelsPerUnit);
|
||||
const height = Math.ceil((metrics.yMax - metrics.yMin) * pixelsPerUnit);
|
||||
this.atlasSize[1] = Math.max(this.atlasSize[1], height + 2);
|
||||
const newAtlasWidth = this.atlasSize[0] + width + 1;
|
||||
const glyphRect = new Float32Array([
|
||||
this.atlasSize[0],
|
||||
1,
|
||||
newAtlasWidth - 1,
|
||||
height + 1
|
||||
]) as glmatrix.vec4;
|
||||
glyph.setAtlasRect(glyphRect);
|
||||
this.atlasSize[0] = newAtlasWidth;
|
||||
}
|
||||
private rebuildAtlas() {
|
||||
this._atlas.layoutGlyphs(this.atlasGlyphs, this.fontSize, this.font.unitsPerEm);
|
||||
|
||||
this.view.then(view => {
|
||||
view.attachText(this.textGlyphs,
|
||||
this.atlasGlyphs,
|
||||
this.fontSize,
|
||||
this.font.unitsPerEm,
|
||||
this.atlasSize);
|
||||
this.font.unitsPerEm);
|
||||
});
|
||||
}
|
||||
|
||||
get atlas(): Atlas {
|
||||
return this._atlas;
|
||||
}
|
||||
|
||||
view: Promise<PathfinderView>;
|
||||
loadFontButton: HTMLInputElement;
|
||||
aaLevelSelect: HTMLSelectElement;
|
||||
|
@ -528,14 +518,14 @@ class AppController {
|
|||
fontData: ArrayBuffer;
|
||||
font: opentype.Font;
|
||||
textGlyphs: opentype.Glyph[];
|
||||
|
||||
private _atlas: Atlas;
|
||||
atlasGlyphs: PathfinderGlyph[];
|
||||
|
||||
meshes: PathfinderMeshData;
|
||||
|
||||
/// The font size in pixels per em.
|
||||
fontSize: number;
|
||||
|
||||
atlasSize: Size2D;
|
||||
}
|
||||
|
||||
class PathfinderShaderLoader {
|
||||
|
@ -582,6 +572,9 @@ class PathfinderView {
|
|||
const shaderSource = this.compileShaders(commonShaderSource, shaderSources);
|
||||
this.shaderPrograms = this.linkShaders(shaderSource);
|
||||
|
||||
this.atlasTransformBuffer = new PathfinderBufferTexture(this.gl, 'uPathTransform');
|
||||
this.pathColorsBufferTexture = new PathfinderBufferTexture(this.gl, 'uPathColors');
|
||||
|
||||
window.addEventListener('resize', () => this.resizeToFit(), false);
|
||||
this.resizeToFit();
|
||||
}
|
||||
|
@ -591,7 +584,7 @@ class PathfinderView {
|
|||
|
||||
let canvas = this.canvas;
|
||||
this.antialiasingStrategy.init(this);
|
||||
this.antialiasingStrategy.setFramebufferSize(this, this.atlasSize);
|
||||
this.antialiasingStrategy.setFramebufferSize(this, ATLAS_SIZE);
|
||||
if (this.meshData != null)
|
||||
this.antialiasingStrategy.attachMeshes(this);
|
||||
|
||||
|
@ -680,9 +673,7 @@ class PathfinderView {
|
|||
pathColors[pathIndex * 4 + 3] = 0xff; // alpha
|
||||
}
|
||||
|
||||
this.pathColorsBufferTexture = new PathfinderBufferTexture(this.gl,
|
||||
pathColors,
|
||||
'uPathColors');
|
||||
this.pathColorsBufferTexture.upload(this.gl, pathColors);
|
||||
}
|
||||
|
||||
attachMeshes(meshes: PathfinderMeshData) {
|
||||
|
@ -725,8 +716,8 @@ class PathfinderView {
|
|||
const atlasGlyphRect = atlasGlyph.getAtlasRect();
|
||||
const atlasGlyphBL = atlasGlyphRect.slice(0, 2) as glmatrix.vec2;
|
||||
const atlasGlyphTR = atlasGlyphRect.slice(2, 4) as glmatrix.vec2;
|
||||
glmatrix.vec2.div(atlasGlyphBL, atlasGlyphBL, this.atlasSize);
|
||||
glmatrix.vec2.div(atlasGlyphTR, atlasGlyphTR, this.atlasSize);
|
||||
glmatrix.vec2.div(atlasGlyphBL, atlasGlyphBL, ATLAS_SIZE);
|
||||
glmatrix.vec2.div(atlasGlyphTR, atlasGlyphTR, ATLAS_SIZE);
|
||||
|
||||
glyphTexCoords.set([
|
||||
atlasGlyphBL[0], atlasGlyphTR[1],
|
||||
|
@ -759,10 +750,8 @@ class PathfinderView {
|
|||
attachText(textGlyphs: opentype.Glyph[],
|
||||
atlasGlyphs: PathfinderGlyph[],
|
||||
fontSize: number,
|
||||
unitsPerEm: number,
|
||||
atlasSize: Size2D) {
|
||||
unitsPerEm: number) {
|
||||
this.pixelsPerUnit = fontSize / unitsPerEm;
|
||||
this.atlasSize = atlasSize;
|
||||
|
||||
const transforms = new Float32Array(_.concat([0, 0, 0, 0],
|
||||
_.flatMap(atlasGlyphs, glyph => {
|
||||
|
@ -778,20 +767,20 @@ class PathfinderView {
|
|||
];
|
||||
})));
|
||||
|
||||
this.atlasTransformBuffer = new PathfinderBufferTexture(this.gl,
|
||||
transforms,
|
||||
'uPathTransform');
|
||||
this.atlasTransformBuffer.upload(this.gl, transforms);
|
||||
|
||||
// Create the atlas framebuffer.
|
||||
this.atlasColorTexture = createFramebufferColorTexture(this.gl, atlasSize);
|
||||
this.atlasDepthTexture = createFramebufferDepthTexture(this.gl, atlasSize);
|
||||
// Create the atlas framebuffer if necessary.
|
||||
if (this.atlasFramebuffer == null) {
|
||||
const atlasColorTexture = this.appController.atlas.ensureTexture(this.gl);
|
||||
this.atlasDepthTexture = createFramebufferDepthTexture(this.gl, ATLAS_SIZE);
|
||||
this.atlasFramebuffer = createFramebuffer(this.gl,
|
||||
this.drawBuffersExt,
|
||||
[this.atlasColorTexture],
|
||||
[atlasColorTexture],
|
||||
this.atlasDepthTexture);
|
||||
|
||||
// Allow the antialiasing strategy to set up framebuffers as necessary.
|
||||
this.antialiasingStrategy.setFramebufferSize(this, atlasSize);
|
||||
this.antialiasingStrategy.setFramebufferSize(this, ATLAS_SIZE);
|
||||
}
|
||||
|
||||
this.createTextBuffers(textGlyphs, atlasGlyphs);
|
||||
}
|
||||
|
@ -1027,7 +1016,7 @@ class PathfinderView {
|
|||
// Blit.
|
||||
this.gl.uniformMatrix4fv(blitProgram.uniforms.uTransform, false, transform);
|
||||
this.gl.activeTexture(this.gl.TEXTURE0);
|
||||
this.gl.bindTexture(this.gl.TEXTURE_2D, this.atlasColorTexture);
|
||||
this.gl.bindTexture(this.gl.TEXTURE_2D, this.appController.atlas.ensureTexture(this.gl));
|
||||
this.gl.uniform1i(blitProgram.uniforms.uSource, 0);
|
||||
this.gl.drawElements(this.gl.TRIANGLES, this.textGlyphCount * 6, this.gl.UNSIGNED_INT, 0);
|
||||
}
|
||||
|
@ -1062,9 +1051,7 @@ class PathfinderView {
|
|||
translation: glmatrix.vec2;
|
||||
|
||||
atlasFramebuffer: WebGLFramebuffer;
|
||||
atlasColorTexture: WebGLTexture;
|
||||
atlasDepthTexture: WebGLTexture;
|
||||
atlasSize: Size2D;
|
||||
|
||||
pixelsPerUnit: number;
|
||||
textGlyphCount: number;
|
||||
|
@ -1120,46 +1107,99 @@ class PathfinderShaderProgram {
|
|||
}
|
||||
|
||||
class PathfinderBufferTexture {
|
||||
constructor(gl: WebGLRenderingContext,
|
||||
data: Float32Array | Uint8Array,
|
||||
uniformName: string) {
|
||||
const pixelCount = Math.ceil(data.length / 4);
|
||||
const width = Math.ceil(Math.sqrt(pixelCount));
|
||||
const height = Math.ceil(pixelCount / width);
|
||||
this.size = new Float32Array([width, height]) as glmatrix.vec2;
|
||||
|
||||
constructor(gl: WebGLRenderingContext, uniformName: string) {
|
||||
this.texture = expectNotNull(gl.createTexture(), "Failed to create buffer texture!");
|
||||
this.size = glmatrix.vec2.create();
|
||||
this.capacity = glmatrix.vec2.create();
|
||||
this.uniformName = uniformName;
|
||||
|
||||
// Pad out with zeroes as necessary.
|
||||
//
|
||||
// FIXME(pcwalton): Do this earlier to save a copy here.
|
||||
const elementCount = width * height * 4;
|
||||
if (data.length != elementCount) {
|
||||
// Wow, this is evil.
|
||||
const newData = new (data.constructor as any)(elementCount);
|
||||
newData.set(data);
|
||||
data = newData;
|
||||
this.glType = 0;
|
||||
}
|
||||
|
||||
this.texture = expectNotNull(gl.createTexture(), "Failed to create texture!");
|
||||
upload(gl: WebGLRenderingContext, data: Float32Array | Uint8Array) {
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
||||
const glType = data instanceof Float32Array ? gl.FLOAT : gl.UNSIGNED_BYTE;
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, glType, data);
|
||||
|
||||
const glType = data instanceof Float32Array ? gl.FLOAT : gl.UNSIGNED_BYTE;
|
||||
const area = Math.ceil(data.length / 4);
|
||||
if (glType != this.glType || area > this.capacityArea) {
|
||||
const width = Math.ceil(Math.sqrt(area));
|
||||
const height = Math.ceil(area / width);
|
||||
this.size = glmatrix.vec2.fromValues(width, height);
|
||||
this.capacity = this.size;
|
||||
this.glType = glType;
|
||||
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, glType, null);
|
||||
setTextureParameters(gl, gl.NEAREST);
|
||||
}
|
||||
|
||||
const mainDimensions = glmatrix.vec4.fromValues(0,
|
||||
0,
|
||||
this.capacity[0],
|
||||
Math.floor(area / this.capacity[0]));
|
||||
const remainderDimensions = glmatrix.vec4.fromValues(0,
|
||||
mainDimensions[3],
|
||||
area % this.capacity[0],
|
||||
1);
|
||||
const splitIndex = mainDimensions[2] * mainDimensions[3] * 4;
|
||||
|
||||
if (mainDimensions[2] > 0 && mainDimensions[3] > 0) {
|
||||
gl.texSubImage2D(gl.TEXTURE_2D,
|
||||
0,
|
||||
mainDimensions[0],
|
||||
mainDimensions[1],
|
||||
mainDimensions[2],
|
||||
mainDimensions[3],
|
||||
gl.RGBA,
|
||||
this.glType,
|
||||
data.slice(0, splitIndex));
|
||||
}
|
||||
|
||||
if (remainderDimensions[2] > 0) {
|
||||
// Round data up to a multiple of 4 if necessary.
|
||||
let remainderLength = data.length - splitIndex;
|
||||
let remainder: Float32Array | Uint8Array;
|
||||
if (remainderLength % 4 == 0) {
|
||||
remainder = data.slice(splitIndex);
|
||||
} else {
|
||||
remainderLength += 4 - remainderLength % 4;
|
||||
remainder = new (data.constructor as any)(remainderLength);
|
||||
remainder.set(data.slice(splitIndex));
|
||||
}
|
||||
|
||||
gl.texSubImage2D(gl.TEXTURE_2D,
|
||||
0,
|
||||
remainderDimensions[0],
|
||||
remainderDimensions[1],
|
||||
remainderDimensions[2],
|
||||
remainderDimensions[3],
|
||||
gl.RGBA,
|
||||
this.glType,
|
||||
remainder);
|
||||
}
|
||||
}
|
||||
|
||||
bind(gl: WebGLRenderingContext, uniforms: UniformMap, textureUnit: number) {
|
||||
gl.activeTexture(gl.TEXTURE0 + textureUnit);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
||||
gl.uniform2i(uniforms[`${this.uniformName}Dimensions`], this.size[0], this.size[1]);
|
||||
gl.uniform2i(uniforms[`${this.uniformName}Dimensions`],
|
||||
this.capacity[0],
|
||||
this.capacity[1]);
|
||||
gl.uniform1i(uniforms[this.uniformName], textureUnit);
|
||||
}
|
||||
|
||||
private get area() {
|
||||
return this.size[0] * this.size[1];
|
||||
}
|
||||
|
||||
private get capacityArea() {
|
||||
return this.capacity[0] * this.capacity[1];
|
||||
}
|
||||
|
||||
readonly texture: WebGLTexture;
|
||||
readonly size: Size2D;
|
||||
readonly uniformName: string;
|
||||
private size: Size2D;
|
||||
private capacity: Size2D;
|
||||
private glType: number;
|
||||
}
|
||||
|
||||
class NoAAStrategy implements AntialiasingStrategy {
|
||||
|
@ -1261,7 +1301,7 @@ class SSAAStrategy implements AntialiasingStrategy {
|
|||
|
||||
resolve(view: PathfinderView) {
|
||||
view.gl.bindFramebuffer(view.gl.FRAMEBUFFER, view.atlasFramebuffer);
|
||||
view.gl.viewport(0, 0, view.atlasSize[0], view.atlasSize[1]);
|
||||
view.gl.viewport(0, 0, ATLAS_SIZE[0], ATLAS_SIZE[1]);
|
||||
view.gl.disable(view.gl.DEPTH_TEST);
|
||||
|
||||
// Set up the blit program VAO.
|
||||
|
@ -1291,17 +1331,17 @@ class ECAAStrategy implements AntialiasingStrategy {
|
|||
this.framebufferSize = new Float32Array([0, 0]) as Size2D;
|
||||
}
|
||||
|
||||
init(view: PathfinderView) {}
|
||||
init(view: PathfinderView) {
|
||||
this.bVertexPositionBufferTexture = new PathfinderBufferTexture(view.gl,
|
||||
'uBVertexPosition');
|
||||
this.bVertexPathIDBufferTexture = new PathfinderBufferTexture(view.gl, 'uBVertexPathID');
|
||||
}
|
||||
|
||||
attachMeshes(view: PathfinderView) {
|
||||
const bVertexPositions = new Float32Array(view.meshData.bVertexPositions);
|
||||
const bVertexPathIDs = new Uint8Array(view.meshData.bVertexPathIDs);
|
||||
this.bVertexPositionBufferTexture = new PathfinderBufferTexture(view.gl,
|
||||
bVertexPositions,
|
||||
'uBVertexPosition');
|
||||
this.bVertexPathIDBufferTexture = new PathfinderBufferTexture(view.gl,
|
||||
bVertexPathIDs,
|
||||
'uBVertexPathID');
|
||||
this.bVertexPositionBufferTexture.upload(view.gl, bVertexPositions);
|
||||
this.bVertexPathIDBufferTexture.upload(view.gl, bVertexPathIDs);
|
||||
|
||||
this.createEdgeDetectVAO(view);
|
||||
this.createCoverVAO(view);
|
||||
|
@ -1764,6 +1804,74 @@ class PathfinderGlyph {
|
|||
private atlasRect: Rect;
|
||||
}
|
||||
|
||||
class Atlas {
|
||||
constructor() {
|
||||
this._texture = null;
|
||||
this._usedSize = glmatrix.vec2.create();
|
||||
}
|
||||
|
||||
layoutGlyphs(glyphs: PathfinderGlyph[], fontSize: number, unitsPerEm: number) {
|
||||
const pixelsPerUnit = fontSize / unitsPerEm;
|
||||
|
||||
let nextOrigin = glmatrix.vec2.create();
|
||||
let shelfBottom = 0.0;
|
||||
|
||||
for (const glyph of glyphs) {
|
||||
const metrics = glyph.metrics();
|
||||
const glyphSize = glmatrix.vec2.fromValues(metrics.xMax - metrics.xMin,
|
||||
metrics.yMax - metrics.yMin);
|
||||
glmatrix.vec2.scale(glyphSize, glyphSize, pixelsPerUnit);
|
||||
glmatrix.vec2.ceil(glyphSize, glyphSize);
|
||||
|
||||
// Make a new shelf if necessary.
|
||||
const initialGlyphRight = nextOrigin[0] + glyphSize[0] + 2;
|
||||
if (initialGlyphRight > ATLAS_SIZE[0])
|
||||
nextOrigin = glmatrix.vec2.fromValues(0.0, shelfBottom);
|
||||
|
||||
const glyphRect = glmatrix.vec4.fromValues(nextOrigin[0] + 1,
|
||||
nextOrigin[1] + 1,
|
||||
nextOrigin[0] + glyphSize[0] + 2,
|
||||
nextOrigin[1] + glyphSize[1] + 2);
|
||||
|
||||
glyph.setAtlasRect(glyphRect);
|
||||
|
||||
nextOrigin[0] = glyphRect[2];
|
||||
shelfBottom = Math.max(shelfBottom, glyphRect[3]);
|
||||
}
|
||||
|
||||
// FIXME(pcwalton): Could be more precise if we don't have a full row.
|
||||
this._usedSize = glmatrix.vec2.fromValues(ATLAS_SIZE[0], shelfBottom);
|
||||
}
|
||||
|
||||
ensureTexture(gl: WebGLRenderingContext): WebGLTexture {
|
||||
if (this._texture != null)
|
||||
return this._texture;
|
||||
|
||||
const texture = unwrapNull(gl.createTexture());
|
||||
this._texture = texture;
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.RGBA,
|
||||
ATLAS_SIZE[0],
|
||||
ATLAS_SIZE[1],
|
||||
0,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
null);
|
||||
setTextureParameters(gl, gl.NEAREST);
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
get usedSize(): glmatrix.vec2 {
|
||||
return this._usedSize;
|
||||
}
|
||||
|
||||
private _texture: WebGLTexture | null;
|
||||
private _usedSize: Size2D;
|
||||
}
|
||||
|
||||
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
|
||||
none: NoAAStrategy,
|
||||
ssaa: SSAAStrategy,
|
||||
|
|
Loading…
Reference in New Issue