msdf font renderen
This commit is contained in:
parent
d3845eb85f
commit
fdc2ab9421
8 changed files with 253 additions and 11 deletions
1
web/pw-visualizer/assets/res/fonts/roboto.json
Normal file
1
web/pw-visualizer/assets/res/fonts/roboto.json
Normal file
File diff suppressed because one or more lines are too long
BIN
web/pw-visualizer/assets/res/fonts/roboto.png
Normal file
BIN
web/pw-visualizer/assets/res/fonts/roboto.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
29
web/pw-visualizer/assets/shaders/frag/msdf.glsl
Normal file
29
web/pw-visualizer/assets/shaders/frag/msdf.glsl
Normal file
|
@ -0,0 +1,29 @@
|
|||
#extension GL_OES_standard_derivatives : enable
|
||||
#ifdef GL_ES
|
||||
precision mediump float;
|
||||
#endif
|
||||
|
||||
varying vec2 v_texCoord;
|
||||
uniform sampler2D msdf;
|
||||
uniform vec4 u_bgColor;
|
||||
uniform vec4 u_fgColor;
|
||||
|
||||
uniform float u_distanceRange;
|
||||
uniform float u_glyphSize;
|
||||
uniform vec2 u_resolution;
|
||||
uniform vec4 u_viewbox;
|
||||
|
||||
float median(float r, float g, float b) {
|
||||
return max(min(r, g), min(max(r, g), b));
|
||||
}
|
||||
|
||||
void main() {
|
||||
float scale = u_distanceRange / u_glyphSize * u_resolution.y / u_viewbox.w;
|
||||
|
||||
vec3 msd = texture2D(msdf, v_texCoord).rgb;
|
||||
float sd = median(msd.r, msd.g, msd.b);
|
||||
float screenPxRange = max(u_distanceRange, scale);
|
||||
float screenPxDistance = screenPxRange*(sd - 0.5);
|
||||
float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0);
|
||||
gl_FragColor = vec4(u_fgColor.rgb, u_fgColor.a * opacity);
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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<ShaderFactory>
|
||||
) {
|
||||
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<GameInstance> {
|
|||
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<GameInstance> {
|
|||
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<GameInstance> {
|
|||
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
|
||||
);
|
||||
|
||||
|
|
181
web/pw-visualizer/src/webgl/msdf_text.ts
Normal file
181
web/pw-visualizer/src/webgl/msdf_text.ts
Normal file
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
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]));
|
||||
|
|
|
@ -74,6 +74,7 @@ export class Resizer {
|
|||
mouse_pos = [0, 0];
|
||||
last_drag = [0, 0];
|
||||
|
||||
// x, y, w, h
|
||||
viewbox: number[];
|
||||
orig_viewbox: number[];
|
||||
|
||||
|
|
Loading…
Reference in a new issue