From fdc2ab9421c6a38c6bbd9b621c4da4f2c147a773 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Sun, 6 Nov 2022 17:43:00 +0100 Subject: msdf font renderen --- web/pw-visualizer/src/assets.ts | 5 + web/pw-visualizer/src/index.ts | 37 +++++-- web/pw-visualizer/src/webgl/msdf_text.ts | 181 +++++++++++++++++++++++++++++++ web/pw-visualizer/src/webgl/texture.ts | 10 +- web/pw-visualizer/src/webgl/util.ts | 1 + 5 files changed, 223 insertions(+), 11 deletions(-) create mode 100644 web/pw-visualizer/src/webgl/msdf_text.ts (limited to 'web/pw-visualizer/src') diff --git a/web/pw-visualizer/src/assets.ts b/web/pw-visualizer/src/assets.ts index 5fa5215..abd17e4 100644 --- a/web/pw-visualizer/src/assets.ts +++ b/web/pw-visualizer/src/assets.ts @@ -9,11 +9,16 @@ export {default as shipPng} from "../assets/res/ship.png"; export {default as fontPng} from "../assets/res/font.png"; +export {default as robotoMsdfPng} from "../assets/res/fonts/roboto.png"; +export {default as robotoMsdfJson} from "../assets/res/fonts/roboto.json"; + export {default as imageFragmentShader} from "../assets/shaders/frag/image.glsl?url"; export {default as maskedImageFragmentShader} from "../assets/shaders/frag/masked_image.glsl?url"; export {default as simpleFragmentShader} from "../assets/shaders/frag/simple.glsl?url"; export {default as vorFragmentShader} from "../assets/shaders/frag/vor.glsl?url"; +export {default as msdfFragmentShader} from "../assets/shaders/frag/msdf.glsl?url"; + export {default as imageVertexShader} from "../assets/shaders/vert/image.glsl?url"; export {default as simpleVertexShader} from "../assets/shaders/vert/simple.glsl?url"; diff --git a/web/pw-visualizer/src/index.ts b/web/pw-visualizer/src/index.ts index 4231707..97bdb3d 100644 --- a/web/pw-visualizer/src/index.ts +++ b/web/pw-visualizer/src/index.ts @@ -25,6 +25,7 @@ import { defaultLabelFactory, LabelFactory, Align, Label } from "./webgl/text"; import { VoronoiBuilder } from "./voronoi/voronoi"; import * as assets from "./assets"; import { loadImage, Texture } from "./webgl/texture"; +import { defaultMsdfLabelFactory, MsdfLabelFactory, Label as MsdfLabel } from "./webgl/msdf_text"; function to_bbox(box: number[]): BBox { @@ -84,7 +85,7 @@ export function init() { ms_per_frame = parseInt(ELEMENTS["speed"].value); - GL = CANVAS.getContext("webgl"); + GL = CANVAS.getContext("webgl", { antialias: true }); GL.clearColor(0, 0, 0, 1); GL.clear(GL.COLOR_BUFFER_BIT); @@ -124,9 +125,12 @@ export class GameInstance { image_shader: Shader; masked_image_shader: Shader; + msdf_shader: Shader; + text_factory: LabelFactory; - planet_labels: Label[]; - ship_labels: Label[]; + msdf_text_factory: MsdfLabelFactory; + planet_labels: MsdfLabel[]; + ship_labels: MsdfLabel[]; ship_ibo: IndexBuffer; ship_vao: VertexArray; @@ -153,6 +157,7 @@ export class GameInstance { planets_textures: Texture[], ship_texture: Texture, font_texture: Texture, + robotoMsdfTexture: Texture, shaders: Dictionary ) { this.game = game; @@ -168,7 +173,9 @@ export class GameInstance { }); this.masked_image_shader = shaders["masked_image"].create_shader(GL); + this.msdf_shader = shaders["msdf"].create_shader(GL); this.text_factory = defaultLabelFactory(GL, font_texture, this.image_shader); + this.msdf_text_factory = defaultMsdfLabelFactory(GL, robotoMsdfTexture, this.msdf_shader); this.planet_labels = []; this.ship_labels = []; @@ -278,11 +285,11 @@ export class GameInstance { 1, 0, -planets[i * 3], - -planets[i * 3 + 1] - 1.2, + -planets[i * 3 + 1] - 1.171875, 1, ]); - const label = this.text_factory.build(GL, transform); + const label = this.msdf_text_factory.build(GL, transform); this.planet_labels.push(label); this.renderer.addRenderable(label.getRenderable(), LAYERS.planet_label); } @@ -330,7 +337,7 @@ export class GameInstance { this.planet_labels[i].setText( GL, - "*" + planet_ships[i], + "" + planet_ships[i], Align.Middle, Align.Begin ); @@ -375,7 +382,7 @@ export class GameInstance { const renderable = new DefaultRenderable(ib, vao, this.masked_image_shader, [this.ship_texture], {}); this.renderer.addRenderable(renderable, LAYERS.ship); - const label = this.text_factory.build(GL); + const label = this.msdf_text_factory.build(GL); this.ship_labels.push(label); this.renderer.addRenderable(label.getRenderable(), LAYERS.ship_label); @@ -451,10 +458,11 @@ export class GameInstance { this.shader, this.image_shader, this.masked_image_shader, + this.msdf_shader, ]; - // If not playing, still reder with different viewbox, so people can still pan etc. + // If not playing, still render with different viewbox, so that panning is still possible if (!this.playing) { this.last_time = time; @@ -595,6 +603,7 @@ export async function set_instance(source: string): Promise { loadImage(assets.fontPng), loadImage(assets.shipPng), loadImage(assets.earthPng), + loadImage(assets.robotoMsdfPng), ]; const shader_promies = [ @@ -630,7 +639,14 @@ export async function set_instance(source: string): Promise { assets.simpleVertexShader, ), ])(), - + (async () => + <[string, ShaderFactory]>[ + "msdf", + await ShaderFactory.create_factory( + assets.msdfFragmentShader, + assets.simpleVertexShader, + ), + ])(), ]; let shaders_array: [string, ShaderFactory][]; [texture_images, shaders_array] = await Promise.all([ @@ -646,12 +662,15 @@ export async function set_instance(source: string): Promise { const fontTexture = Texture.fromImage(GL, texture_images[0], "font"); const shipTexture = Texture.fromImage(GL, texture_images[1], "ship"); const earthTexture = Texture.fromImage(GL, texture_images[2], "earth"); + const robotoMsdfTexture = Texture.fromImage(GL, texture_images[3], "robotoMsdf"); + game_instance = new GameInstance( Game.new(source), [earthTexture], shipTexture, fontTexture, + robotoMsdfTexture, shaders ); diff --git a/web/pw-visualizer/src/webgl/msdf_text.ts b/web/pw-visualizer/src/webgl/msdf_text.ts new file mode 100644 index 0000000..5bbac20 --- /dev/null +++ b/web/pw-visualizer/src/webgl/msdf_text.ts @@ -0,0 +1,181 @@ +import { Shader, Uniform1f, Uniform4f, UniformMatrix3fv } from "./shader"; +import { Texture } from "./texture"; +import { DefaultRenderable } from "./renderer"; +import { IndexBuffer, VertexBuffer } from "./buffer"; +import { VertexBufferLayout, VertexArray } from "./vertexBufferLayout"; +import { robotoMsdfJson } from "../assets"; +import { GlypInfo } from "./text"; + + +export enum Align { + Begin, + End, + Middle, +} + +export type FontAtlas = { + atlas: AtlasMeta, + metrics: Metrics, + glyphs: Glyph[], +} + +export type AtlasMeta = { + type: string, + distanceRange: number, + size: number, + width: number, + height: number, + yOrigin: string, +} + +export type Metrics = { + emSize: number, + lineHeight: number, + ascender: number, + descender: number, + underlineY: number, + underlineThickness: number, +} + + +export type Glyph = { + unicode: number, + advance: number, + planeBounds?: Bounds, + atlasBounds?: Bounds, +} + +export type Bounds = { + left: number, + bottom: number, + right: number, + top: number, +} + + +export class MsdfLabelFactory { + texture: Texture; + font: FontAtlas; + shader: Shader; + + constructor(gl: WebGLRenderingContext, fontTexture: Texture, font: FontAtlas, shader: Shader) { + this.texture = fontTexture; + this.font = font; + this.shader = shader; + } + + build(gl: WebGLRenderingContext, transform?: UniformMatrix3fv): Label { + return new Label(gl, this.shader, this.texture, this.font, transform); + } +} + +export class Label { + inner: DefaultRenderable; + + font: FontAtlas; + charAtlas: {[unicodeNumber: number]: Glyph}; + + constructor(gl: WebGLRenderingContext, shader: Shader, tex: Texture, font: FontAtlas, transform: UniformMatrix3fv) { + this.font = font; + this.charAtlas = {} + this.font.glyphs.forEach((glyph) => { + this.charAtlas[glyph.unicode] = glyph; + }); + + const uniforms = { + "u_trans": transform, + "u_trans_next": transform, + "u_fgColor": new Uniform4f([1.0, 1.0, 1.0, 1.0]), + "u_bgColor": new Uniform4f([0.0, 0.0, 0.0, 1.0]), + "u_distanceRange": new Uniform1f(font.atlas.distanceRange), + "u_glyphSize": new Uniform1f(font.atlas.size), + }; + const ib = new IndexBuffer(gl, []); + const vb_pos = new VertexBuffer(gl, []); + const vb_tex = new VertexBuffer(gl, []); + + const layout_pos = new VertexBufferLayout(); + layout_pos.push(gl.FLOAT, 2, 4, "a_position"); + + const layout_tex = new VertexBufferLayout(); + layout_tex.push(gl.FLOAT, 2, 4, "a_texCoord"); + + const vao = new VertexArray(); + vao.addBuffer(vb_pos, layout_pos); + vao.addBuffer(vb_tex, layout_tex); + + this.inner = new DefaultRenderable(ib, vao, shader, [tex], uniforms); + } + + getRenderable(): DefaultRenderable { + return this.inner; + } + + setText(gl: WebGLRenderingContext, text: string, h_align = Align.Begin, v_align = Align.Begin) { + const idxs = []; + const verts_pos = []; + const verts_tex = []; + + let xPos = 0; + let yPos = 0; + switch (v_align) { + case Align.Begin: + yPos = -1; + break; + case Align.End: + yPos = 0; + break; + case Align.Middle: + yPos = -0.5; + break; + } + + // track position in the index buffer + let bufPos = 0; + for (let charIndex = 0; charIndex < text.length; charIndex++) { + let char = this.charAtlas[text.charCodeAt(charIndex)] + if (char.atlasBounds && char.planeBounds) { + verts_pos.push(xPos + char.planeBounds.left, yPos-char.planeBounds.top); + verts_pos.push(xPos + char.planeBounds.right, yPos-char.planeBounds.top); + verts_pos.push(xPos + char.planeBounds.left, yPos-char.planeBounds.bottom); + verts_pos.push(xPos + char.planeBounds.right, yPos-char.planeBounds.bottom); + + const atlasWidth = this.font.atlas.width; + const atlasHeight = this.font.atlas.height; + + verts_tex.push(char.atlasBounds.left / atlasWidth, char.atlasBounds.top / atlasHeight); + verts_tex.push(char.atlasBounds.right / atlasWidth, char.atlasBounds.top / atlasHeight); + verts_tex.push(char.atlasBounds.left / atlasWidth, char.atlasBounds.bottom / atlasHeight); + verts_tex.push(char.atlasBounds.right / atlasWidth, char.atlasBounds.bottom / atlasHeight); + + idxs.push(bufPos+0, bufPos+1, bufPos+2); + idxs.push(bufPos+1, bufPos+2, bufPos+3); + bufPos += 4; + } + xPos += char.advance; + } + + let shift = 0; + switch (h_align) { + case Align.End: + shift = xPos; + break; + case Align.Middle: + shift = xPos / 2; + break; + } + + for (let i = 0; i < verts_pos.length; i += 2) { + verts_pos[i] -= shift; + } + + + this.inner.updateIndexBuffer(gl, idxs); + this.inner.updateVAOBuffer(gl, 0, verts_pos); + this.inner.updateVAOBuffer(gl, 1, verts_tex); + } +} + +export function defaultMsdfLabelFactory(gl: WebGLRenderingContext, fontTexture: Texture, shader: Shader): MsdfLabelFactory { + return new MsdfLabelFactory(gl, fontTexture, robotoMsdfJson, shader); +} diff --git a/web/pw-visualizer/src/webgl/texture.ts b/web/pw-visualizer/src/webgl/texture.ts index 9624489..9ebd5c0 100644 --- a/web/pw-visualizer/src/webgl/texture.ts +++ b/web/pw-visualizer/src/webgl/texture.ts @@ -67,8 +67,14 @@ export class Texture { gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + if (name == "font") { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + } else { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + + } gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255])); diff --git a/web/pw-visualizer/src/webgl/util.ts b/web/pw-visualizer/src/webgl/util.ts index 7b55d19..922b016 100644 --- a/web/pw-visualizer/src/webgl/util.ts +++ b/web/pw-visualizer/src/webgl/util.ts @@ -74,6 +74,7 @@ export class Resizer { mouse_pos = [0, 0]; last_drag = [0, 0]; + // x, y, w, h viewbox: number[]; orig_viewbox: number[]; -- cgit v1.2.3