msdf font renderen

This commit is contained in:
Ilion Beyst 2022-11-06 17:43:00 +01:00
parent d3845eb85f
commit fdc2ab9421
8 changed files with 253 additions and 11 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View 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);
}

View file

@ -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 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 imageFragmentShader} from "../assets/shaders/frag/image.glsl?url";
export {default as maskedImageFragmentShader} from "../assets/shaders/frag/masked_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 simpleFragmentShader} from "../assets/shaders/frag/simple.glsl?url";
export {default as vorFragmentShader} from "../assets/shaders/frag/vor.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 imageVertexShader} from "../assets/shaders/vert/image.glsl?url";
export {default as simpleVertexShader} from "../assets/shaders/vert/simple.glsl?url"; export {default as simpleVertexShader} from "../assets/shaders/vert/simple.glsl?url";

View file

@ -25,6 +25,7 @@ import { defaultLabelFactory, LabelFactory, Align, Label } from "./webgl/text";
import { VoronoiBuilder } from "./voronoi/voronoi"; import { VoronoiBuilder } from "./voronoi/voronoi";
import * as assets from "./assets"; import * as assets from "./assets";
import { loadImage, Texture } from "./webgl/texture"; import { loadImage, Texture } from "./webgl/texture";
import { defaultMsdfLabelFactory, MsdfLabelFactory, Label as MsdfLabel } from "./webgl/msdf_text";
function to_bbox(box: number[]): BBox { function to_bbox(box: number[]): BBox {
@ -84,7 +85,7 @@ export function init() {
ms_per_frame = parseInt(ELEMENTS["speed"].value); ms_per_frame = parseInt(ELEMENTS["speed"].value);
GL = CANVAS.getContext("webgl"); GL = CANVAS.getContext("webgl", { antialias: true });
GL.clearColor(0, 0, 0, 1); GL.clearColor(0, 0, 0, 1);
GL.clear(GL.COLOR_BUFFER_BIT); GL.clear(GL.COLOR_BUFFER_BIT);
@ -124,9 +125,12 @@ export class GameInstance {
image_shader: Shader; image_shader: Shader;
masked_image_shader: Shader; masked_image_shader: Shader;
msdf_shader: Shader;
text_factory: LabelFactory; text_factory: LabelFactory;
planet_labels: Label[]; msdf_text_factory: MsdfLabelFactory;
ship_labels: Label[]; planet_labels: MsdfLabel[];
ship_labels: MsdfLabel[];
ship_ibo: IndexBuffer; ship_ibo: IndexBuffer;
ship_vao: VertexArray; ship_vao: VertexArray;
@ -153,6 +157,7 @@ export class GameInstance {
planets_textures: Texture[], planets_textures: Texture[],
ship_texture: Texture, ship_texture: Texture,
font_texture: Texture, font_texture: Texture,
robotoMsdfTexture: Texture,
shaders: Dictionary<ShaderFactory> shaders: Dictionary<ShaderFactory>
) { ) {
this.game = game; this.game = game;
@ -168,7 +173,9 @@ export class GameInstance {
}); });
this.masked_image_shader = shaders["masked_image"].create_shader(GL); 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.text_factory = defaultLabelFactory(GL, font_texture, this.image_shader);
this.msdf_text_factory = defaultMsdfLabelFactory(GL, robotoMsdfTexture, this.msdf_shader);
this.planet_labels = []; this.planet_labels = [];
this.ship_labels = []; this.ship_labels = [];
@ -278,11 +285,11 @@ export class GameInstance {
1, 1,
0, 0,
-planets[i * 3], -planets[i * 3],
-planets[i * 3 + 1] - 1.2, -planets[i * 3 + 1] - 1.171875,
1, 1,
]); ]);
const label = this.text_factory.build(GL, transform); const label = this.msdf_text_factory.build(GL, transform);
this.planet_labels.push(label); this.planet_labels.push(label);
this.renderer.addRenderable(label.getRenderable(), LAYERS.planet_label); this.renderer.addRenderable(label.getRenderable(), LAYERS.planet_label);
} }
@ -330,7 +337,7 @@ export class GameInstance {
this.planet_labels[i].setText( this.planet_labels[i].setText(
GL, GL,
"*" + planet_ships[i], "" + planet_ships[i],
Align.Middle, Align.Middle,
Align.Begin Align.Begin
); );
@ -375,7 +382,7 @@ export class GameInstance {
const renderable = new DefaultRenderable(ib, vao, this.masked_image_shader, [this.ship_texture], {}); const renderable = new DefaultRenderable(ib, vao, this.masked_image_shader, [this.ship_texture], {});
this.renderer.addRenderable(renderable, LAYERS.ship); 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.ship_labels.push(label);
this.renderer.addRenderable(label.getRenderable(), LAYERS.ship_label); this.renderer.addRenderable(label.getRenderable(), LAYERS.ship_label);
@ -451,10 +458,11 @@ export class GameInstance {
this.shader, this.shader,
this.image_shader, this.image_shader,
this.masked_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) { if (!this.playing) {
this.last_time = time; this.last_time = time;
@ -595,6 +603,7 @@ export async function set_instance(source: string): Promise<GameInstance> {
loadImage(assets.fontPng), loadImage(assets.fontPng),
loadImage(assets.shipPng), loadImage(assets.shipPng),
loadImage(assets.earthPng), loadImage(assets.earthPng),
loadImage(assets.robotoMsdfPng),
]; ];
const shader_promies = [ const shader_promies = [
@ -630,7 +639,14 @@ export async function set_instance(source: string): Promise<GameInstance> {
assets.simpleVertexShader, assets.simpleVertexShader,
), ),
])(), ])(),
(async () =>
<[string, ShaderFactory]>[
"msdf",
await ShaderFactory.create_factory(
assets.msdfFragmentShader,
assets.simpleVertexShader,
),
])(),
]; ];
let shaders_array: [string, ShaderFactory][]; let shaders_array: [string, ShaderFactory][];
[texture_images, shaders_array] = await Promise.all([ [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 fontTexture = Texture.fromImage(GL, texture_images[0], "font");
const shipTexture = Texture.fromImage(GL, texture_images[1], "ship"); const shipTexture = Texture.fromImage(GL, texture_images[1], "ship");
const earthTexture = Texture.fromImage(GL, texture_images[2], "earth"); const earthTexture = Texture.fromImage(GL, texture_images[2], "earth");
const robotoMsdfTexture = Texture.fromImage(GL, texture_images[3], "robotoMsdf");
game_instance = new GameInstance( game_instance = new GameInstance(
Game.new(source), Game.new(source),
[earthTexture], [earthTexture],
shipTexture, shipTexture,
fontTexture, fontTexture,
robotoMsdfTexture,
shaders shaders
); );

View 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);
}

View file

@ -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_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_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_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_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.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA,
gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255])); gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255]));

View file

@ -74,6 +74,7 @@ export class Resizer {
mouse_pos = [0, 0]; mouse_pos = [0, 0];
last_drag = [0, 0]; last_drag = [0, 0];
// x, y, w, h
viewbox: number[]; viewbox: number[];
orig_viewbox: number[]; orig_viewbox: number[];