@bloopjs/toodle 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backends/webgl2/WebGLFontPipeline.d.ts +26 -0
- package/dist/backends/webgl2/WebGLFontPipeline.d.ts.map +1 -0
- package/dist/backends/webgl2/WebGLTextShader.d.ts +10 -6
- package/dist/backends/webgl2/WebGLTextShader.d.ts.map +1 -1
- package/dist/backends/webgl2/glsl/text.glsl.d.ts +12 -0
- package/dist/backends/webgl2/glsl/text.glsl.d.ts.map +1 -0
- package/dist/backends/webgl2/mod.d.ts +1 -0
- package/dist/backends/webgl2/mod.d.ts.map +1 -1
- package/dist/mod.js +725 -371
- package/dist/mod.js.map +9 -7
- package/dist/textures/AssetManager.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/backends/webgl2/WebGLFontPipeline.ts +173 -0
- package/src/backends/webgl2/WebGLTextShader.ts +253 -13
- package/src/backends/webgl2/glsl/text.glsl.ts +132 -0
- package/src/backends/webgl2/mod.ts +1 -0
- package/src/textures/AssetManager.ts +10 -2
package/dist/mod.js
CHANGED
|
@@ -17219,386 +17219,120 @@ class WebGPUBackend {
|
|
|
17219
17219
|
return this.#renderPass;
|
|
17220
17220
|
}
|
|
17221
17221
|
}
|
|
17222
|
-
// src/backends/webgl2/
|
|
17223
|
-
class
|
|
17224
|
-
label = "text";
|
|
17222
|
+
// src/backends/webgl2/WebGLFontPipeline.ts
|
|
17223
|
+
class WebGLFontPipeline {
|
|
17225
17224
|
font;
|
|
17225
|
+
fontTexture;
|
|
17226
|
+
charDataTexture;
|
|
17227
|
+
textBufferTexture;
|
|
17226
17228
|
maxCharCount;
|
|
17227
|
-
|
|
17229
|
+
lineHeight;
|
|
17230
|
+
#gl;
|
|
17231
|
+
constructor(gl, font, fontTexture, charDataTexture, textBufferTexture, maxCharCount) {
|
|
17232
|
+
this.#gl = gl;
|
|
17228
17233
|
this.font = font;
|
|
17234
|
+
this.fontTexture = fontTexture;
|
|
17235
|
+
this.charDataTexture = charDataTexture;
|
|
17236
|
+
this.textBufferTexture = textBufferTexture;
|
|
17229
17237
|
this.maxCharCount = maxCharCount;
|
|
17238
|
+
this.lineHeight = font.lineHeight;
|
|
17239
|
+
}
|
|
17240
|
+
static create(gl, font, maxCharCount) {
|
|
17241
|
+
const fontTexture = gl.createTexture();
|
|
17242
|
+
assert(fontTexture, "Failed to create font texture");
|
|
17243
|
+
gl.bindTexture(gl.TEXTURE_2D, fontTexture);
|
|
17244
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
17245
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
17246
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
17247
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
17248
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, font.imageBitmap);
|
|
17249
|
+
const charDataTexture = gl.createTexture();
|
|
17250
|
+
assert(charDataTexture, "Failed to create char data texture");
|
|
17251
|
+
gl.bindTexture(gl.TEXTURE_2D, charDataTexture);
|
|
17252
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
17253
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
17254
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
17255
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
17256
|
+
const charCount = font.charCount;
|
|
17257
|
+
const charTextureWidth = charCount * 2;
|
|
17258
|
+
const charTextureData = new Float32Array(charTextureWidth * 4);
|
|
17259
|
+
for (let i3 = 0;i3 < charCount; i3++) {
|
|
17260
|
+
const srcOffset = i3 * 8;
|
|
17261
|
+
const dstOffset0 = i3 * 2 * 4;
|
|
17262
|
+
const dstOffset1 = (i3 * 2 + 1) * 4;
|
|
17263
|
+
charTextureData[dstOffset0] = font.charBuffer[srcOffset];
|
|
17264
|
+
charTextureData[dstOffset0 + 1] = font.charBuffer[srcOffset + 1];
|
|
17265
|
+
charTextureData[dstOffset0 + 2] = font.charBuffer[srcOffset + 2];
|
|
17266
|
+
charTextureData[dstOffset0 + 3] = font.charBuffer[srcOffset + 3];
|
|
17267
|
+
charTextureData[dstOffset1] = font.charBuffer[srcOffset + 4];
|
|
17268
|
+
charTextureData[dstOffset1 + 1] = font.charBuffer[srcOffset + 5];
|
|
17269
|
+
charTextureData[dstOffset1 + 2] = font.charBuffer[srcOffset + 6];
|
|
17270
|
+
charTextureData[dstOffset1 + 3] = font.charBuffer[srcOffset + 7];
|
|
17271
|
+
}
|
|
17272
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, charTextureWidth, 1, 0, gl.RGBA, gl.FLOAT, charTextureData);
|
|
17273
|
+
const textBufferTexture = gl.createTexture();
|
|
17274
|
+
assert(textBufferTexture, "Failed to create text buffer texture");
|
|
17275
|
+
gl.bindTexture(gl.TEXTURE_2D, textBufferTexture);
|
|
17276
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
17277
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
17278
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
17279
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
17280
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, maxCharCount, 1, 0, gl.RGBA, gl.FLOAT, null);
|
|
17281
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
17282
|
+
return new WebGLFontPipeline(gl, font, fontTexture, charDataTexture, textBufferTexture, maxCharCount);
|
|
17283
|
+
}
|
|
17284
|
+
updateTextBuffer(data, glyphCount) {
|
|
17285
|
+
const gl = this.#gl;
|
|
17286
|
+
gl.bindTexture(gl.TEXTURE_2D, this.textBufferTexture);
|
|
17287
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, glyphCount, 1, gl.RGBA, gl.FLOAT, data);
|
|
17230
17288
|
}
|
|
17231
|
-
|
|
17232
|
-
|
|
17233
|
-
|
|
17289
|
+
destroy() {
|
|
17290
|
+
const gl = this.#gl;
|
|
17291
|
+
gl.deleteTexture(this.fontTexture);
|
|
17292
|
+
gl.deleteTexture(this.charDataTexture);
|
|
17293
|
+
gl.deleteTexture(this.textBufferTexture);
|
|
17234
17294
|
}
|
|
17235
|
-
endFrame() {}
|
|
17236
|
-
}
|
|
17237
|
-
|
|
17238
|
-
// src/backends/webgpu/wgsl/text.wgsl.ts
|
|
17239
|
-
var text_wgsl_default = `
|
|
17240
|
-
// Adapted from: https://webgpu.github.io/webgpu-samples/?sample=textRenderingMsdf
|
|
17241
|
-
|
|
17242
|
-
// Quad vertex positions for a character
|
|
17243
|
-
const pos = array(
|
|
17244
|
-
vec2f(0, -1),
|
|
17245
|
-
vec2f(1, -1),
|
|
17246
|
-
vec2f(0, 0),
|
|
17247
|
-
vec2f(1, 0),
|
|
17248
|
-
);
|
|
17249
|
-
|
|
17250
|
-
// Debug colors for visualization
|
|
17251
|
-
const debugColors = array(
|
|
17252
|
-
vec4f(1, 0, 0, 1),
|
|
17253
|
-
vec4f(0, 1, 0, 1),
|
|
17254
|
-
vec4f(0, 0, 1, 1),
|
|
17255
|
-
vec4f(1, 1, 1, 1),
|
|
17256
|
-
);
|
|
17257
|
-
|
|
17258
|
-
// Vertex input from GPU
|
|
17259
|
-
struct VertexInput {
|
|
17260
|
-
@builtin(vertex_index) vertex: u32,
|
|
17261
|
-
@builtin(instance_index) instance: u32,
|
|
17262
|
-
};
|
|
17263
|
-
|
|
17264
|
-
// Output from vertex shader to fragment shader
|
|
17265
|
-
struct VertexOutput {
|
|
17266
|
-
@builtin(position) position: vec4f,
|
|
17267
|
-
@location(0) texcoord: vec2f,
|
|
17268
|
-
@location(1) debugColor: vec4f,
|
|
17269
|
-
@location(2) @interpolate(flat) instanceIndex: u32,
|
|
17270
|
-
};
|
|
17271
|
-
|
|
17272
|
-
// Metadata for a single character glyph
|
|
17273
|
-
struct Char {
|
|
17274
|
-
texOffset: vec2f, // Offset to top-left in MSDF texture (pixels)
|
|
17275
|
-
texExtent: vec2f, // Size in texture (pixels)
|
|
17276
|
-
size: vec2f, // Glyph size in ems
|
|
17277
|
-
offset: vec2f, // Position offset in ems
|
|
17278
|
-
};
|
|
17279
|
-
|
|
17280
|
-
// Metadata for a text block
|
|
17281
|
-
struct TextBlockDescriptor {
|
|
17282
|
-
transform: mat3x3f, // Text transform matrix (model matrix)
|
|
17283
|
-
color: vec4f, // Text color
|
|
17284
|
-
fontSize: f32, // Font size
|
|
17285
|
-
blockWidth: f32, // Total width of text block
|
|
17286
|
-
blockHeight: f32, // Total height of text block
|
|
17287
|
-
bufferPosition: f32 // Index and length in textBuffer
|
|
17288
|
-
};
|
|
17289
|
-
|
|
17290
|
-
// Font bindings
|
|
17291
|
-
@group(0) @binding(0) var fontTexture: texture_2d<f32>;
|
|
17292
|
-
@group(0) @binding(1) var fontSampler: sampler;
|
|
17293
|
-
@group(0) @binding(2) var<storage> chars: array<Char>;
|
|
17294
|
-
@group(0) @binding(3) var<uniform> fontData: vec4f; // Contains line height (x)
|
|
17295
|
-
|
|
17296
|
-
// Text bindings
|
|
17297
|
-
@group(1) @binding(0) var<storage> texts: array<TextBlockDescriptor>;
|
|
17298
|
-
@group(1) @binding(1) var<storage> textBuffer: array<vec4f>; // Each vec4: xy = glyph pos, z = char index
|
|
17299
|
-
|
|
17300
|
-
// Global uniforms
|
|
17301
|
-
@group(2) @binding(0) var<uniform> viewProjectionMatrix: mat3x3f;
|
|
17302
|
-
|
|
17303
|
-
// Vertex shader
|
|
17304
|
-
@vertex
|
|
17305
|
-
fn vertexMain(input: VertexInput) -> VertexOutput {
|
|
17306
|
-
// Because the instance index is used for character indexing, we are
|
|
17307
|
-
// overloading the vertex index to store the instance of the text metadata.
|
|
17308
|
-
//
|
|
17309
|
-
// I.e...
|
|
17310
|
-
// Vertex 0-4 = Instance 0, Vertex 0-4
|
|
17311
|
-
// Vertex 4-8 = Instance 1, Vertex 0-4
|
|
17312
|
-
// Vertex 8-12 = Instance 2, Vertex 0-4
|
|
17313
|
-
let vertexIndex = input.vertex % 4;
|
|
17314
|
-
let textIndex = input.vertex / 4;
|
|
17315
|
-
|
|
17316
|
-
let text = texts[textIndex];
|
|
17317
|
-
let textElement = textBuffer[u32(text.bufferPosition) + input.instance];
|
|
17318
|
-
let char = chars[u32(textElement.z)];
|
|
17319
|
-
|
|
17320
|
-
let lineHeight = fontData.x;
|
|
17321
|
-
let textWidth = text.blockWidth;
|
|
17322
|
-
let textHeight = text.blockHeight;
|
|
17323
|
-
|
|
17324
|
-
// Center text vertically; origin is mid-height
|
|
17325
|
-
let offset = vec2f(0, -textHeight / 2);
|
|
17326
|
-
|
|
17327
|
-
// Glyph position in ems (quad pos * size + per-char offset)
|
|
17328
|
-
let emPos = pos[vertexIndex] * char.size + char.offset + textElement.xy - offset;
|
|
17329
|
-
let charPos = emPos * (text.fontSize / lineHeight);
|
|
17330
|
-
|
|
17331
|
-
var output: VertexOutput;
|
|
17332
|
-
let transformedPosition = viewProjectionMatrix * text.transform * vec3f(charPos, 1);
|
|
17333
|
-
|
|
17334
|
-
output.position = vec4f(transformedPosition, 1);
|
|
17335
|
-
output.texcoord = pos[vertexIndex] * vec2f(1, -1);
|
|
17336
|
-
output.texcoord *= char.texExtent;
|
|
17337
|
-
output.texcoord += char.texOffset;
|
|
17338
|
-
output.debugColor = debugColors[vertexIndex];
|
|
17339
|
-
output.instanceIndex = textIndex;
|
|
17340
|
-
return output;
|
|
17341
|
-
|
|
17342
|
-
// To debug - hardcode quad in bottom right quarter of the screen:
|
|
17343
|
-
// output.position = vec4f(pos[input.vertex], 0, 1);
|
|
17344
|
-
}
|
|
17345
|
-
|
|
17346
|
-
// Signed distance function sampling for MSDF font rendering
|
|
17347
|
-
fn sampleMsdf(texcoord: vec2f) -> f32 {
|
|
17348
|
-
let c = textureSample(fontTexture, fontSampler, texcoord);
|
|
17349
|
-
return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
|
|
17350
17295
|
}
|
|
17351
17296
|
|
|
17352
|
-
//
|
|
17353
|
-
|
|
17354
|
-
|
|
17355
|
-
|
|
17356
|
-
|
|
17357
|
-
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
|
|
17358
|
-
let text = texts[input.instanceIndex];
|
|
17359
|
-
|
|
17360
|
-
// pxRange (AKA distanceRange) comes from the msdfgen tool.
|
|
17361
|
-
let pxRange = 4.0;
|
|
17362
|
-
let texSize = vec2f(textureDimensions(fontTexture, 0));
|
|
17363
|
-
|
|
17364
|
-
let dx = texSize.x * length(vec2f(dpdxFine(input.texcoord.x), dpdyFine(input.texcoord.x)));
|
|
17365
|
-
let dy = texSize.y * length(vec2f(dpdxFine(input.texcoord.y), dpdyFine(input.texcoord.y)));
|
|
17366
|
-
|
|
17367
|
-
let toPixels = pxRange * inverseSqrt(dx * dx + dy * dy);
|
|
17368
|
-
let sigDist = sampleMsdf(input.texcoord) - 0.5;
|
|
17369
|
-
let pxDist = sigDist * toPixels;
|
|
17370
|
-
|
|
17371
|
-
let edgeWidth = 0.5;
|
|
17372
|
-
let alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
|
|
17373
|
-
|
|
17374
|
-
if (alpha < 0.001) {
|
|
17375
|
-
discard;
|
|
17297
|
+
// src/utils/error.ts
|
|
17298
|
+
var warnings = new Map;
|
|
17299
|
+
function warnOnce(key, msg) {
|
|
17300
|
+
if (warnings.has(key)) {
|
|
17301
|
+
return;
|
|
17376
17302
|
}
|
|
17377
|
-
|
|
17378
|
-
|
|
17379
|
-
return msdfColor;
|
|
17380
|
-
|
|
17381
|
-
// Debug options:
|
|
17382
|
-
// return text.color;
|
|
17383
|
-
// return input.debugColor;
|
|
17384
|
-
// return vec4f(1, 0, 1, 1); // hardcoded magenta
|
|
17385
|
-
// return textureSample(fontTexture, fontSampler, input.texcoord);
|
|
17303
|
+
warnings.set(key, true);
|
|
17304
|
+
console.warn(msg ?? key);
|
|
17386
17305
|
}
|
|
17387
|
-
`;
|
|
17388
17306
|
|
|
17389
|
-
// src/
|
|
17390
|
-
class
|
|
17391
|
-
|
|
17392
|
-
|
|
17393
|
-
|
|
17394
|
-
|
|
17395
|
-
|
|
17396
|
-
|
|
17397
|
-
|
|
17398
|
-
|
|
17399
|
-
|
|
17400
|
-
|
|
17401
|
-
|
|
17402
|
-
|
|
17403
|
-
|
|
17404
|
-
|
|
17405
|
-
|
|
17406
|
-
|
|
17407
|
-
|
|
17408
|
-
|
|
17409
|
-
|
|
17410
|
-
|
|
17411
|
-
|
|
17412
|
-
|
|
17413
|
-
|
|
17414
|
-
|
|
17415
|
-
|
|
17416
|
-
|
|
17417
|
-
|
|
17418
|
-
charsGpuBuffer.unmap();
|
|
17419
|
-
const fontDataBuffer = device.createBuffer({
|
|
17420
|
-
label: `MSDF font ${font.name} metadata buffer`,
|
|
17421
|
-
size: Float32Array.BYTES_PER_ELEMENT * 4,
|
|
17422
|
-
usage: GPUBufferUsage.UNIFORM,
|
|
17423
|
-
mappedAtCreation: true
|
|
17424
|
-
});
|
|
17425
|
-
const fontDataArray = new Float32Array(fontDataBuffer.getMappedRange());
|
|
17426
|
-
fontDataArray[0] = font.lineHeight;
|
|
17427
|
-
fontDataBuffer.unmap();
|
|
17428
|
-
const fontBindGroup = device.createBindGroup({
|
|
17429
|
-
layout: pipeline.getBindGroupLayout(0),
|
|
17430
|
-
entries: [
|
|
17431
|
-
{
|
|
17432
|
-
binding: 0,
|
|
17433
|
-
resource: texture.createView()
|
|
17434
|
-
},
|
|
17435
|
-
{
|
|
17436
|
-
binding: 1,
|
|
17437
|
-
resource: device.createSampler(sampler)
|
|
17438
|
-
},
|
|
17439
|
-
{
|
|
17440
|
-
binding: 2,
|
|
17441
|
-
resource: {
|
|
17442
|
-
buffer: charsGpuBuffer
|
|
17443
|
-
}
|
|
17444
|
-
},
|
|
17445
|
-
{
|
|
17446
|
-
binding: 3,
|
|
17447
|
-
resource: {
|
|
17448
|
-
buffer: fontDataBuffer
|
|
17449
|
-
}
|
|
17450
|
-
}
|
|
17451
|
-
]
|
|
17452
|
-
});
|
|
17453
|
-
return new FontPipeline(pipeline, font, fontBindGroup, maxCharCount);
|
|
17454
|
-
}
|
|
17455
|
-
}
|
|
17456
|
-
function pipelinePromise(device, colorFormat, label) {
|
|
17457
|
-
const shader = device.createShaderModule({
|
|
17458
|
-
label: `${label} shader`,
|
|
17459
|
-
code: text_wgsl_default
|
|
17460
|
-
});
|
|
17461
|
-
return device.createRenderPipelineAsync({
|
|
17462
|
-
label: `${label} pipeline`,
|
|
17463
|
-
layout: device.createPipelineLayout({
|
|
17464
|
-
bindGroupLayouts: [
|
|
17465
|
-
device.createBindGroupLayout(fontBindGroupLayout),
|
|
17466
|
-
device.createBindGroupLayout(textUniformBindGroupLayout),
|
|
17467
|
-
device.createBindGroupLayout(engineUniformBindGroupLayout)
|
|
17468
|
-
]
|
|
17469
|
-
}),
|
|
17470
|
-
vertex: {
|
|
17471
|
-
module: shader,
|
|
17472
|
-
entryPoint: "vertexMain"
|
|
17473
|
-
},
|
|
17474
|
-
fragment: {
|
|
17475
|
-
module: shader,
|
|
17476
|
-
entryPoint: "fragmentMain",
|
|
17477
|
-
targets: [
|
|
17478
|
-
{
|
|
17479
|
-
format: colorFormat,
|
|
17480
|
-
blend: {
|
|
17481
|
-
color: {
|
|
17482
|
-
srcFactor: "src-alpha",
|
|
17483
|
-
dstFactor: "one-minus-src-alpha"
|
|
17484
|
-
},
|
|
17485
|
-
alpha: {
|
|
17486
|
-
srcFactor: "one",
|
|
17487
|
-
dstFactor: "one"
|
|
17488
|
-
}
|
|
17489
|
-
}
|
|
17490
|
-
}
|
|
17491
|
-
]
|
|
17492
|
-
},
|
|
17493
|
-
primitive: {
|
|
17494
|
-
topology: "triangle-strip",
|
|
17495
|
-
stripIndexFormat: "uint32"
|
|
17496
|
-
}
|
|
17497
|
-
});
|
|
17498
|
-
}
|
|
17499
|
-
if (typeof GPUShaderStage === "undefined") {
|
|
17500
|
-
globalThis.GPUShaderStage = {
|
|
17501
|
-
VERTEX: 1,
|
|
17502
|
-
FRAGMENT: 2,
|
|
17503
|
-
COMPUTE: 4
|
|
17504
|
-
};
|
|
17505
|
-
}
|
|
17506
|
-
var fontBindGroupLayout = {
|
|
17507
|
-
label: "MSDF font group layout",
|
|
17508
|
-
entries: [
|
|
17509
|
-
{
|
|
17510
|
-
binding: 0,
|
|
17511
|
-
visibility: GPUShaderStage.FRAGMENT,
|
|
17512
|
-
texture: {}
|
|
17513
|
-
},
|
|
17514
|
-
{
|
|
17515
|
-
binding: 1,
|
|
17516
|
-
visibility: GPUShaderStage.FRAGMENT,
|
|
17517
|
-
sampler: {}
|
|
17518
|
-
},
|
|
17519
|
-
{
|
|
17520
|
-
binding: 2,
|
|
17521
|
-
visibility: GPUShaderStage.VERTEX,
|
|
17522
|
-
buffer: { type: "read-only-storage" }
|
|
17523
|
-
},
|
|
17524
|
-
{
|
|
17525
|
-
binding: 3,
|
|
17526
|
-
visibility: GPUShaderStage.VERTEX,
|
|
17527
|
-
buffer: {}
|
|
17528
|
-
}
|
|
17529
|
-
]
|
|
17530
|
-
};
|
|
17531
|
-
var engineUniformBindGroupLayout = {
|
|
17532
|
-
label: "Uniform bind group",
|
|
17533
|
-
entries: [
|
|
17534
|
-
{
|
|
17535
|
-
binding: 0,
|
|
17536
|
-
visibility: GPUShaderStage.VERTEX,
|
|
17537
|
-
buffer: {}
|
|
17538
|
-
}
|
|
17539
|
-
]
|
|
17540
|
-
};
|
|
17541
|
-
var sampler = {
|
|
17542
|
-
label: "MSDF text sampler",
|
|
17543
|
-
minFilter: "linear",
|
|
17544
|
-
magFilter: "linear",
|
|
17545
|
-
mipmapFilter: "linear",
|
|
17546
|
-
maxAnisotropy: 16
|
|
17547
|
-
};
|
|
17548
|
-
var textUniformBindGroupLayout = {
|
|
17549
|
-
label: "MSDF text block uniform",
|
|
17550
|
-
entries: [
|
|
17551
|
-
{
|
|
17552
|
-
binding: 0,
|
|
17553
|
-
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
17554
|
-
buffer: { type: "read-only-storage" }
|
|
17555
|
-
},
|
|
17556
|
-
{
|
|
17557
|
-
binding: 1,
|
|
17558
|
-
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
17559
|
-
buffer: { type: "read-only-storage" }
|
|
17560
|
-
}
|
|
17561
|
-
]
|
|
17562
|
-
};
|
|
17563
|
-
// src/utils/error.ts
|
|
17564
|
-
var warnings = new Map;
|
|
17565
|
-
function warnOnce(key, msg) {
|
|
17566
|
-
if (warnings.has(key)) {
|
|
17567
|
-
return;
|
|
17568
|
-
}
|
|
17569
|
-
warnings.set(key, true);
|
|
17570
|
-
console.warn(msg ?? key);
|
|
17571
|
-
}
|
|
17572
|
-
|
|
17573
|
-
// src/text/MsdfFont.ts
|
|
17574
|
-
class MsdfFont {
|
|
17575
|
-
id;
|
|
17576
|
-
json;
|
|
17577
|
-
imageBitmap;
|
|
17578
|
-
name;
|
|
17579
|
-
charset;
|
|
17580
|
-
charCount;
|
|
17581
|
-
lineHeight;
|
|
17582
|
-
charBuffer;
|
|
17583
|
-
#kernings;
|
|
17584
|
-
#chars;
|
|
17585
|
-
#fallbackCharCode;
|
|
17586
|
-
constructor(id, json, imageBitmap) {
|
|
17587
|
-
this.id = id;
|
|
17588
|
-
this.json = json;
|
|
17589
|
-
this.imageBitmap = imageBitmap;
|
|
17590
|
-
const charArray = Object.values(json.chars);
|
|
17591
|
-
this.charCount = charArray.length;
|
|
17592
|
-
this.lineHeight = json.common.lineHeight;
|
|
17593
|
-
this.charset = json.info.charset;
|
|
17594
|
-
this.name = json.info.face;
|
|
17595
|
-
this.#kernings = new Map;
|
|
17596
|
-
if (json.kernings) {
|
|
17597
|
-
for (const kearning of json.kernings) {
|
|
17598
|
-
let charKerning = this.#kernings.get(kearning.first);
|
|
17599
|
-
if (!charKerning) {
|
|
17600
|
-
charKerning = new Map;
|
|
17601
|
-
this.#kernings.set(kearning.first, charKerning);
|
|
17307
|
+
// src/text/MsdfFont.ts
|
|
17308
|
+
class MsdfFont {
|
|
17309
|
+
id;
|
|
17310
|
+
json;
|
|
17311
|
+
imageBitmap;
|
|
17312
|
+
name;
|
|
17313
|
+
charset;
|
|
17314
|
+
charCount;
|
|
17315
|
+
lineHeight;
|
|
17316
|
+
charBuffer;
|
|
17317
|
+
#kernings;
|
|
17318
|
+
#chars;
|
|
17319
|
+
#fallbackCharCode;
|
|
17320
|
+
constructor(id, json, imageBitmap) {
|
|
17321
|
+
this.id = id;
|
|
17322
|
+
this.json = json;
|
|
17323
|
+
this.imageBitmap = imageBitmap;
|
|
17324
|
+
const charArray = Object.values(json.chars);
|
|
17325
|
+
this.charCount = charArray.length;
|
|
17326
|
+
this.lineHeight = json.common.lineHeight;
|
|
17327
|
+
this.charset = json.info.charset;
|
|
17328
|
+
this.name = json.info.face;
|
|
17329
|
+
this.#kernings = new Map;
|
|
17330
|
+
if (json.kernings) {
|
|
17331
|
+
for (const kearning of json.kernings) {
|
|
17332
|
+
let charKerning = this.#kernings.get(kearning.first);
|
|
17333
|
+
if (!charKerning) {
|
|
17334
|
+
charKerning = new Map;
|
|
17335
|
+
this.#kernings.set(kearning.first, charKerning);
|
|
17602
17336
|
}
|
|
17603
17337
|
charKerning.set(kearning.second, kearning.amount);
|
|
17604
17338
|
}
|
|
@@ -18294,6 +18028,624 @@ class TextNode extends SceneNode {
|
|
|
18294
18028
|
}
|
|
18295
18029
|
}
|
|
18296
18030
|
|
|
18031
|
+
// src/backends/webgl2/glsl/text.glsl.ts
|
|
18032
|
+
var vertexShader2 = `#version 300 es
|
|
18033
|
+
precision highp float;
|
|
18034
|
+
|
|
18035
|
+
// Engine uniforms
|
|
18036
|
+
uniform mat3 u_viewProjection;
|
|
18037
|
+
|
|
18038
|
+
// Per-text-block uniforms
|
|
18039
|
+
uniform mat3 u_textTransform;
|
|
18040
|
+
uniform vec4 u_textColor;
|
|
18041
|
+
uniform float u_fontSize;
|
|
18042
|
+
uniform float u_blockWidth;
|
|
18043
|
+
uniform float u_blockHeight;
|
|
18044
|
+
uniform float u_lineHeight;
|
|
18045
|
+
|
|
18046
|
+
// Character data texture (RGBA32F, 2 texels per character)
|
|
18047
|
+
// Texel 0: texOffset.xy, texExtent.xy
|
|
18048
|
+
// Texel 1: size.xy, offset.xy
|
|
18049
|
+
uniform sampler2D u_charData;
|
|
18050
|
+
|
|
18051
|
+
// Text buffer texture (RGBA32F, 1 texel per glyph)
|
|
18052
|
+
// Each texel: xy = glyph position, z = char index
|
|
18053
|
+
uniform sampler2D u_textBuffer;
|
|
18054
|
+
|
|
18055
|
+
// Outputs to fragment shader
|
|
18056
|
+
out vec2 v_texcoord;
|
|
18057
|
+
|
|
18058
|
+
// Quad vertex positions for a character (matches WGSL)
|
|
18059
|
+
const vec2 pos[4] = vec2[4](
|
|
18060
|
+
vec2(0.0, -1.0),
|
|
18061
|
+
vec2(1.0, -1.0),
|
|
18062
|
+
vec2(0.0, 0.0),
|
|
18063
|
+
vec2(1.0, 0.0)
|
|
18064
|
+
);
|
|
18065
|
+
|
|
18066
|
+
void main() {
|
|
18067
|
+
// gl_VertexID gives us 0-3 for the quad vertices
|
|
18068
|
+
// gl_InstanceID gives us which glyph we're rendering
|
|
18069
|
+
int vertexIndex = gl_VertexID;
|
|
18070
|
+
int glyphIndex = gl_InstanceID;
|
|
18071
|
+
|
|
18072
|
+
// Fetch glyph data from text buffer texture
|
|
18073
|
+
vec4 glyphData = texelFetch(u_textBuffer, ivec2(glyphIndex, 0), 0);
|
|
18074
|
+
vec2 glyphPos = glyphData.xy;
|
|
18075
|
+
int charIndex = int(glyphData.z);
|
|
18076
|
+
|
|
18077
|
+
// Fetch character metrics (2 texels per char)
|
|
18078
|
+
// Texel 0: texOffset.x, texOffset.y, texExtent.x, texExtent.y
|
|
18079
|
+
// Texel 1: size.x, size.y, offset.x, offset.y
|
|
18080
|
+
vec4 charData0 = texelFetch(u_charData, ivec2(charIndex * 2, 0), 0);
|
|
18081
|
+
vec4 charData1 = texelFetch(u_charData, ivec2(charIndex * 2 + 1, 0), 0);
|
|
18082
|
+
|
|
18083
|
+
vec2 texOffset = charData0.xy;
|
|
18084
|
+
vec2 texExtent = charData0.zw;
|
|
18085
|
+
vec2 charSize = charData1.xy;
|
|
18086
|
+
vec2 charOffset = charData1.zw;
|
|
18087
|
+
|
|
18088
|
+
// Center text vertically; origin is mid-height
|
|
18089
|
+
vec2 offset = vec2(0.0, -u_blockHeight / 2.0);
|
|
18090
|
+
|
|
18091
|
+
// Glyph position in ems (quad pos * size + per-char offset)
|
|
18092
|
+
vec2 emPos = pos[vertexIndex] * charSize + charOffset + glyphPos - offset;
|
|
18093
|
+
vec2 charPos = emPos * (u_fontSize / u_lineHeight);
|
|
18094
|
+
|
|
18095
|
+
// Transform position through model and view-projection matrices
|
|
18096
|
+
vec3 worldPos = u_textTransform * vec3(charPos, 1.0);
|
|
18097
|
+
vec3 clipPos = u_viewProjection * worldPos;
|
|
18098
|
+
|
|
18099
|
+
gl_Position = vec4(clipPos.xy, 0.0, 1.0);
|
|
18100
|
+
|
|
18101
|
+
// Calculate texture coordinates
|
|
18102
|
+
v_texcoord = pos[vertexIndex] * vec2(1.0, -1.0);
|
|
18103
|
+
v_texcoord *= texExtent;
|
|
18104
|
+
v_texcoord += texOffset;
|
|
18105
|
+
}
|
|
18106
|
+
`;
|
|
18107
|
+
var fragmentShader2 = `#version 300 es
|
|
18108
|
+
precision highp float;
|
|
18109
|
+
|
|
18110
|
+
// Font texture (MSDF atlas)
|
|
18111
|
+
uniform sampler2D u_fontTexture;
|
|
18112
|
+
|
|
18113
|
+
// Text color
|
|
18114
|
+
uniform vec4 u_textColor;
|
|
18115
|
+
|
|
18116
|
+
// Input from vertex shader
|
|
18117
|
+
in vec2 v_texcoord;
|
|
18118
|
+
|
|
18119
|
+
// Output color
|
|
18120
|
+
out vec4 fragColor;
|
|
18121
|
+
|
|
18122
|
+
// Signed distance function sampling for MSDF font rendering
|
|
18123
|
+
// Median of three: max(min(r,g), min(max(r,g), b))
|
|
18124
|
+
float sampleMsdf(vec2 texcoord) {
|
|
18125
|
+
vec4 c = texture(u_fontTexture, texcoord);
|
|
18126
|
+
return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
|
|
18127
|
+
}
|
|
18128
|
+
|
|
18129
|
+
void main() {
|
|
18130
|
+
// pxRange (AKA distanceRange) comes from the msdfgen tool
|
|
18131
|
+
float pxRange = 4.0;
|
|
18132
|
+
vec2 texSize = vec2(textureSize(u_fontTexture, 0));
|
|
18133
|
+
|
|
18134
|
+
// Anti-aliasing technique by Paul Houx
|
|
18135
|
+
// https://github.com/Chlumsky/msdfgen/issues/22#issuecomment-234958005
|
|
18136
|
+
float dx = texSize.x * length(vec2(dFdx(v_texcoord.x), dFdy(v_texcoord.x)));
|
|
18137
|
+
float dy = texSize.y * length(vec2(dFdx(v_texcoord.y), dFdy(v_texcoord.y)));
|
|
18138
|
+
|
|
18139
|
+
float toPixels = pxRange * inversesqrt(dx * dx + dy * dy);
|
|
18140
|
+
float sigDist = sampleMsdf(v_texcoord) - 0.5;
|
|
18141
|
+
float pxDist = sigDist * toPixels;
|
|
18142
|
+
|
|
18143
|
+
float edgeWidth = 0.5;
|
|
18144
|
+
float alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
|
|
18145
|
+
|
|
18146
|
+
if (alpha < 0.001) {
|
|
18147
|
+
discard;
|
|
18148
|
+
}
|
|
18149
|
+
|
|
18150
|
+
fragColor = vec4(u_textColor.rgb, u_textColor.a * alpha);
|
|
18151
|
+
}
|
|
18152
|
+
`;
|
|
18153
|
+
|
|
18154
|
+
// src/backends/webgl2/WebGLTextShader.ts
|
|
18155
|
+
class WebGLTextShader {
|
|
18156
|
+
label = "text";
|
|
18157
|
+
font;
|
|
18158
|
+
maxCharCount;
|
|
18159
|
+
#backend;
|
|
18160
|
+
#pipeline;
|
|
18161
|
+
#program;
|
|
18162
|
+
#vao;
|
|
18163
|
+
#cpuTextBuffer;
|
|
18164
|
+
#cachedUniform = null;
|
|
18165
|
+
#uViewProjection = null;
|
|
18166
|
+
#uTextTransform = null;
|
|
18167
|
+
#uTextColor = null;
|
|
18168
|
+
#uFontSize = null;
|
|
18169
|
+
#uBlockWidth = null;
|
|
18170
|
+
#uBlockHeight = null;
|
|
18171
|
+
#uLineHeight = null;
|
|
18172
|
+
#uCharData = null;
|
|
18173
|
+
#uTextBuffer = null;
|
|
18174
|
+
#uFontTexture = null;
|
|
18175
|
+
constructor(backend, pipeline) {
|
|
18176
|
+
this.#backend = backend;
|
|
18177
|
+
this.#pipeline = pipeline;
|
|
18178
|
+
this.font = pipeline.font;
|
|
18179
|
+
this.maxCharCount = pipeline.maxCharCount;
|
|
18180
|
+
const gl = backend.gl;
|
|
18181
|
+
const vs = this.#compileShader(gl, gl.VERTEX_SHADER, vertexShader2);
|
|
18182
|
+
const fs = this.#compileShader(gl, gl.FRAGMENT_SHADER, fragmentShader2);
|
|
18183
|
+
const program = gl.createProgram();
|
|
18184
|
+
assert(program, "Failed to create WebGL program");
|
|
18185
|
+
gl.attachShader(program, vs);
|
|
18186
|
+
gl.attachShader(program, fs);
|
|
18187
|
+
gl.linkProgram(program);
|
|
18188
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
18189
|
+
const info = gl.getProgramInfoLog(program);
|
|
18190
|
+
throw new Error(`Failed to link text shader program: ${info}`);
|
|
18191
|
+
}
|
|
18192
|
+
this.#program = program;
|
|
18193
|
+
this.#uViewProjection = gl.getUniformLocation(program, "u_viewProjection");
|
|
18194
|
+
this.#uTextTransform = gl.getUniformLocation(program, "u_textTransform");
|
|
18195
|
+
this.#uTextColor = gl.getUniformLocation(program, "u_textColor");
|
|
18196
|
+
this.#uFontSize = gl.getUniformLocation(program, "u_fontSize");
|
|
18197
|
+
this.#uBlockWidth = gl.getUniformLocation(program, "u_blockWidth");
|
|
18198
|
+
this.#uBlockHeight = gl.getUniformLocation(program, "u_blockHeight");
|
|
18199
|
+
this.#uLineHeight = gl.getUniformLocation(program, "u_lineHeight");
|
|
18200
|
+
this.#uCharData = gl.getUniformLocation(program, "u_charData");
|
|
18201
|
+
this.#uTextBuffer = gl.getUniformLocation(program, "u_textBuffer");
|
|
18202
|
+
this.#uFontTexture = gl.getUniformLocation(program, "u_fontTexture");
|
|
18203
|
+
const vao = gl.createVertexArray();
|
|
18204
|
+
assert(vao, "Failed to create WebGL VAO");
|
|
18205
|
+
this.#vao = vao;
|
|
18206
|
+
this.#cpuTextBuffer = new Float32Array(this.maxCharCount * 4);
|
|
18207
|
+
gl.deleteShader(vs);
|
|
18208
|
+
gl.deleteShader(fs);
|
|
18209
|
+
}
|
|
18210
|
+
startFrame(uniform) {
|
|
18211
|
+
this.#cachedUniform = uniform;
|
|
18212
|
+
}
|
|
18213
|
+
processBatch(nodes) {
|
|
18214
|
+
if (nodes.length === 0)
|
|
18215
|
+
return 0;
|
|
18216
|
+
const gl = this.#backend.gl;
|
|
18217
|
+
const uniform = this.#cachedUniform;
|
|
18218
|
+
if (!uniform) {
|
|
18219
|
+
throw new Error("Tried to process batch but engine uniform is not set");
|
|
18220
|
+
}
|
|
18221
|
+
gl.useProgram(this.#program);
|
|
18222
|
+
gl.bindVertexArray(this.#vao);
|
|
18223
|
+
if (this.#uViewProjection) {
|
|
18224
|
+
const m3 = uniform.viewProjectionMatrix;
|
|
18225
|
+
const mat3x3 = new Float32Array([
|
|
18226
|
+
m3[0],
|
|
18227
|
+
m3[1],
|
|
18228
|
+
m3[2],
|
|
18229
|
+
m3[4],
|
|
18230
|
+
m3[5],
|
|
18231
|
+
m3[6],
|
|
18232
|
+
m3[8],
|
|
18233
|
+
m3[9],
|
|
18234
|
+
m3[10]
|
|
18235
|
+
]);
|
|
18236
|
+
gl.uniformMatrix3fv(this.#uViewProjection, false, mat3x3);
|
|
18237
|
+
}
|
|
18238
|
+
if (this.#uLineHeight) {
|
|
18239
|
+
gl.uniform1f(this.#uLineHeight, this.#pipeline.lineHeight);
|
|
18240
|
+
}
|
|
18241
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
18242
|
+
gl.bindTexture(gl.TEXTURE_2D, this.#pipeline.fontTexture);
|
|
18243
|
+
if (this.#uFontTexture) {
|
|
18244
|
+
gl.uniform1i(this.#uFontTexture, 0);
|
|
18245
|
+
}
|
|
18246
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
18247
|
+
gl.bindTexture(gl.TEXTURE_2D, this.#pipeline.charDataTexture);
|
|
18248
|
+
if (this.#uCharData) {
|
|
18249
|
+
gl.uniform1i(this.#uCharData, 1);
|
|
18250
|
+
}
|
|
18251
|
+
gl.activeTexture(gl.TEXTURE2);
|
|
18252
|
+
gl.bindTexture(gl.TEXTURE_2D, this.#pipeline.textBufferTexture);
|
|
18253
|
+
if (this.#uTextBuffer) {
|
|
18254
|
+
gl.uniform1i(this.#uTextBuffer, 2);
|
|
18255
|
+
}
|
|
18256
|
+
for (const node of nodes) {
|
|
18257
|
+
if (!(node instanceof TextNode)) {
|
|
18258
|
+
console.error(node);
|
|
18259
|
+
throw new Error(`Tried to use WebGLTextShader on something that isn't a TextNode: ${node}`);
|
|
18260
|
+
}
|
|
18261
|
+
const text = node.text;
|
|
18262
|
+
const formatting = node.formatting;
|
|
18263
|
+
const measurements = measureText(this.font, text, formatting.wordWrap);
|
|
18264
|
+
const size = node.size ?? measurements;
|
|
18265
|
+
const fontSize = formatting.shrinkToFit ? findLargestFontSize(this.font, text, size, formatting) : formatting.fontSize;
|
|
18266
|
+
const actualFontSize = fontSize || DEFAULT_FONT_SIZE;
|
|
18267
|
+
shapeText(this.font, text, size, actualFontSize, formatting, this.#cpuTextBuffer, 0);
|
|
18268
|
+
this.#pipeline.updateTextBuffer(this.#cpuTextBuffer, measurements.printedCharCount);
|
|
18269
|
+
if (this.#uTextTransform) {
|
|
18270
|
+
const m3 = node.matrix;
|
|
18271
|
+
const mat3x3 = new Float32Array([
|
|
18272
|
+
m3[0],
|
|
18273
|
+
m3[1],
|
|
18274
|
+
m3[2],
|
|
18275
|
+
m3[4],
|
|
18276
|
+
m3[5],
|
|
18277
|
+
m3[6],
|
|
18278
|
+
m3[8],
|
|
18279
|
+
m3[9],
|
|
18280
|
+
m3[10]
|
|
18281
|
+
]);
|
|
18282
|
+
gl.uniformMatrix3fv(this.#uTextTransform, false, mat3x3);
|
|
18283
|
+
}
|
|
18284
|
+
if (this.#uTextColor) {
|
|
18285
|
+
const tint = node.tint;
|
|
18286
|
+
gl.uniform4f(this.#uTextColor, tint.r, tint.g, tint.b, tint.a);
|
|
18287
|
+
}
|
|
18288
|
+
if (this.#uFontSize) {
|
|
18289
|
+
gl.uniform1f(this.#uFontSize, actualFontSize);
|
|
18290
|
+
}
|
|
18291
|
+
if (this.#uBlockWidth) {
|
|
18292
|
+
gl.uniform1f(this.#uBlockWidth, formatting.align === "center" ? 0 : measurements.width);
|
|
18293
|
+
}
|
|
18294
|
+
if (this.#uBlockHeight) {
|
|
18295
|
+
gl.uniform1f(this.#uBlockHeight, measurements.height);
|
|
18296
|
+
}
|
|
18297
|
+
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, measurements.printedCharCount);
|
|
18298
|
+
}
|
|
18299
|
+
gl.bindVertexArray(null);
|
|
18300
|
+
return nodes.length;
|
|
18301
|
+
}
|
|
18302
|
+
endFrame() {}
|
|
18303
|
+
#compileShader(gl, type, source) {
|
|
18304
|
+
const shader = gl.createShader(type);
|
|
18305
|
+
assert(shader, "Failed to create WebGL shader");
|
|
18306
|
+
gl.shaderSource(shader, source);
|
|
18307
|
+
gl.compileShader(shader);
|
|
18308
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
18309
|
+
const info = gl.getShaderInfoLog(shader);
|
|
18310
|
+
const typeStr = type === gl.VERTEX_SHADER ? "vertex" : "fragment";
|
|
18311
|
+
gl.deleteShader(shader);
|
|
18312
|
+
throw new Error(`Failed to compile ${typeStr} shader: ${info}`);
|
|
18313
|
+
}
|
|
18314
|
+
return shader;
|
|
18315
|
+
}
|
|
18316
|
+
destroy() {
|
|
18317
|
+
const gl = this.#backend.gl;
|
|
18318
|
+
gl.deleteProgram(this.#program);
|
|
18319
|
+
gl.deleteVertexArray(this.#vao);
|
|
18320
|
+
this.#pipeline.destroy();
|
|
18321
|
+
}
|
|
18322
|
+
}
|
|
18323
|
+
|
|
18324
|
+
// src/backends/webgpu/wgsl/text.wgsl.ts
|
|
18325
|
+
var text_wgsl_default = `
|
|
18326
|
+
// Adapted from: https://webgpu.github.io/webgpu-samples/?sample=textRenderingMsdf
|
|
18327
|
+
|
|
18328
|
+
// Quad vertex positions for a character
|
|
18329
|
+
const pos = array(
|
|
18330
|
+
vec2f(0, -1),
|
|
18331
|
+
vec2f(1, -1),
|
|
18332
|
+
vec2f(0, 0),
|
|
18333
|
+
vec2f(1, 0),
|
|
18334
|
+
);
|
|
18335
|
+
|
|
18336
|
+
// Debug colors for visualization
|
|
18337
|
+
const debugColors = array(
|
|
18338
|
+
vec4f(1, 0, 0, 1),
|
|
18339
|
+
vec4f(0, 1, 0, 1),
|
|
18340
|
+
vec4f(0, 0, 1, 1),
|
|
18341
|
+
vec4f(1, 1, 1, 1),
|
|
18342
|
+
);
|
|
18343
|
+
|
|
18344
|
+
// Vertex input from GPU
|
|
18345
|
+
struct VertexInput {
|
|
18346
|
+
@builtin(vertex_index) vertex: u32,
|
|
18347
|
+
@builtin(instance_index) instance: u32,
|
|
18348
|
+
};
|
|
18349
|
+
|
|
18350
|
+
// Output from vertex shader to fragment shader
|
|
18351
|
+
struct VertexOutput {
|
|
18352
|
+
@builtin(position) position: vec4f,
|
|
18353
|
+
@location(0) texcoord: vec2f,
|
|
18354
|
+
@location(1) debugColor: vec4f,
|
|
18355
|
+
@location(2) @interpolate(flat) instanceIndex: u32,
|
|
18356
|
+
};
|
|
18357
|
+
|
|
18358
|
+
// Metadata for a single character glyph
|
|
18359
|
+
struct Char {
|
|
18360
|
+
texOffset: vec2f, // Offset to top-left in MSDF texture (pixels)
|
|
18361
|
+
texExtent: vec2f, // Size in texture (pixels)
|
|
18362
|
+
size: vec2f, // Glyph size in ems
|
|
18363
|
+
offset: vec2f, // Position offset in ems
|
|
18364
|
+
};
|
|
18365
|
+
|
|
18366
|
+
// Metadata for a text block
|
|
18367
|
+
struct TextBlockDescriptor {
|
|
18368
|
+
transform: mat3x3f, // Text transform matrix (model matrix)
|
|
18369
|
+
color: vec4f, // Text color
|
|
18370
|
+
fontSize: f32, // Font size
|
|
18371
|
+
blockWidth: f32, // Total width of text block
|
|
18372
|
+
blockHeight: f32, // Total height of text block
|
|
18373
|
+
bufferPosition: f32 // Index and length in textBuffer
|
|
18374
|
+
};
|
|
18375
|
+
|
|
18376
|
+
// Font bindings
|
|
18377
|
+
@group(0) @binding(0) var fontTexture: texture_2d<f32>;
|
|
18378
|
+
@group(0) @binding(1) var fontSampler: sampler;
|
|
18379
|
+
@group(0) @binding(2) var<storage> chars: array<Char>;
|
|
18380
|
+
@group(0) @binding(3) var<uniform> fontData: vec4f; // Contains line height (x)
|
|
18381
|
+
|
|
18382
|
+
// Text bindings
|
|
18383
|
+
@group(1) @binding(0) var<storage> texts: array<TextBlockDescriptor>;
|
|
18384
|
+
@group(1) @binding(1) var<storage> textBuffer: array<vec4f>; // Each vec4: xy = glyph pos, z = char index
|
|
18385
|
+
|
|
18386
|
+
// Global uniforms
|
|
18387
|
+
@group(2) @binding(0) var<uniform> viewProjectionMatrix: mat3x3f;
|
|
18388
|
+
|
|
18389
|
+
// Vertex shader
|
|
18390
|
+
@vertex
|
|
18391
|
+
fn vertexMain(input: VertexInput) -> VertexOutput {
|
|
18392
|
+
// Because the instance index is used for character indexing, we are
|
|
18393
|
+
// overloading the vertex index to store the instance of the text metadata.
|
|
18394
|
+
//
|
|
18395
|
+
// I.e...
|
|
18396
|
+
// Vertex 0-4 = Instance 0, Vertex 0-4
|
|
18397
|
+
// Vertex 4-8 = Instance 1, Vertex 0-4
|
|
18398
|
+
// Vertex 8-12 = Instance 2, Vertex 0-4
|
|
18399
|
+
let vertexIndex = input.vertex % 4;
|
|
18400
|
+
let textIndex = input.vertex / 4;
|
|
18401
|
+
|
|
18402
|
+
let text = texts[textIndex];
|
|
18403
|
+
let textElement = textBuffer[u32(text.bufferPosition) + input.instance];
|
|
18404
|
+
let char = chars[u32(textElement.z)];
|
|
18405
|
+
|
|
18406
|
+
let lineHeight = fontData.x;
|
|
18407
|
+
let textWidth = text.blockWidth;
|
|
18408
|
+
let textHeight = text.blockHeight;
|
|
18409
|
+
|
|
18410
|
+
// Center text vertically; origin is mid-height
|
|
18411
|
+
let offset = vec2f(0, -textHeight / 2);
|
|
18412
|
+
|
|
18413
|
+
// Glyph position in ems (quad pos * size + per-char offset)
|
|
18414
|
+
let emPos = pos[vertexIndex] * char.size + char.offset + textElement.xy - offset;
|
|
18415
|
+
let charPos = emPos * (text.fontSize / lineHeight);
|
|
18416
|
+
|
|
18417
|
+
var output: VertexOutput;
|
|
18418
|
+
let transformedPosition = viewProjectionMatrix * text.transform * vec3f(charPos, 1);
|
|
18419
|
+
|
|
18420
|
+
output.position = vec4f(transformedPosition, 1);
|
|
18421
|
+
output.texcoord = pos[vertexIndex] * vec2f(1, -1);
|
|
18422
|
+
output.texcoord *= char.texExtent;
|
|
18423
|
+
output.texcoord += char.texOffset;
|
|
18424
|
+
output.debugColor = debugColors[vertexIndex];
|
|
18425
|
+
output.instanceIndex = textIndex;
|
|
18426
|
+
return output;
|
|
18427
|
+
|
|
18428
|
+
// To debug - hardcode quad in bottom right quarter of the screen:
|
|
18429
|
+
// output.position = vec4f(pos[input.vertex], 0, 1);
|
|
18430
|
+
}
|
|
18431
|
+
|
|
18432
|
+
// Signed distance function sampling for MSDF font rendering
|
|
18433
|
+
fn sampleMsdf(texcoord: vec2f) -> f32 {
|
|
18434
|
+
let c = textureSample(fontTexture, fontSampler, texcoord);
|
|
18435
|
+
return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
|
|
18436
|
+
}
|
|
18437
|
+
|
|
18438
|
+
// Fragment shader
|
|
18439
|
+
// Anti-aliasing technique by Paul Houx
|
|
18440
|
+
// more details here:
|
|
18441
|
+
// https://github.com/Chlumsky/msdfgen/issues/22#issuecomment-234958005
|
|
18442
|
+
@fragment
|
|
18443
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
|
|
18444
|
+
let text = texts[input.instanceIndex];
|
|
18445
|
+
|
|
18446
|
+
// pxRange (AKA distanceRange) comes from the msdfgen tool.
|
|
18447
|
+
let pxRange = 4.0;
|
|
18448
|
+
let texSize = vec2f(textureDimensions(fontTexture, 0));
|
|
18449
|
+
|
|
18450
|
+
let dx = texSize.x * length(vec2f(dpdxFine(input.texcoord.x), dpdyFine(input.texcoord.x)));
|
|
18451
|
+
let dy = texSize.y * length(vec2f(dpdxFine(input.texcoord.y), dpdyFine(input.texcoord.y)));
|
|
18452
|
+
|
|
18453
|
+
let toPixels = pxRange * inverseSqrt(dx * dx + dy * dy);
|
|
18454
|
+
let sigDist = sampleMsdf(input.texcoord) - 0.5;
|
|
18455
|
+
let pxDist = sigDist * toPixels;
|
|
18456
|
+
|
|
18457
|
+
let edgeWidth = 0.5;
|
|
18458
|
+
let alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
|
|
18459
|
+
|
|
18460
|
+
if (alpha < 0.001) {
|
|
18461
|
+
discard;
|
|
18462
|
+
}
|
|
18463
|
+
|
|
18464
|
+
let msdfColor = vec4f(text.color.rgb, text.color.a * alpha);
|
|
18465
|
+
return msdfColor;
|
|
18466
|
+
|
|
18467
|
+
// Debug options:
|
|
18468
|
+
// return text.color;
|
|
18469
|
+
// return input.debugColor;
|
|
18470
|
+
// return vec4f(1, 0, 1, 1); // hardcoded magenta
|
|
18471
|
+
// return textureSample(fontTexture, fontSampler, input.texcoord);
|
|
18472
|
+
}
|
|
18473
|
+
`;
|
|
18474
|
+
|
|
18475
|
+
// src/backends/webgpu/FontPipeline.ts
|
|
18476
|
+
class FontPipeline {
|
|
18477
|
+
pipeline;
|
|
18478
|
+
font;
|
|
18479
|
+
fontBindGroup;
|
|
18480
|
+
maxCharCount;
|
|
18481
|
+
constructor(pipeline, font, fontBindGroup, maxCharCount) {
|
|
18482
|
+
this.pipeline = pipeline;
|
|
18483
|
+
this.font = font;
|
|
18484
|
+
this.fontBindGroup = fontBindGroup;
|
|
18485
|
+
this.maxCharCount = maxCharCount;
|
|
18486
|
+
}
|
|
18487
|
+
static async create(device, font, colorFormat, maxCharCount) {
|
|
18488
|
+
const pipeline = await pipelinePromise(device, colorFormat, font.name);
|
|
18489
|
+
const texture = device.createTexture({
|
|
18490
|
+
label: `MSDF font ${font.name}`,
|
|
18491
|
+
size: [font.imageBitmap.width, font.imageBitmap.height, 1],
|
|
18492
|
+
format: "rgba8unorm",
|
|
18493
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
|
18494
|
+
});
|
|
18495
|
+
device.queue.copyExternalImageToTexture({ source: font.imageBitmap }, { texture }, [font.imageBitmap.width, font.imageBitmap.height]);
|
|
18496
|
+
const charsGpuBuffer = device.createBuffer({
|
|
18497
|
+
label: `MSDF font ${font.name} character layout buffer`,
|
|
18498
|
+
size: font.charCount * Float32Array.BYTES_PER_ELEMENT * 8,
|
|
18499
|
+
usage: GPUBufferUsage.STORAGE,
|
|
18500
|
+
mappedAtCreation: true
|
|
18501
|
+
});
|
|
18502
|
+
const charsArray = new Float32Array(charsGpuBuffer.getMappedRange());
|
|
18503
|
+
charsArray.set(font.charBuffer, 0);
|
|
18504
|
+
charsGpuBuffer.unmap();
|
|
18505
|
+
const fontDataBuffer = device.createBuffer({
|
|
18506
|
+
label: `MSDF font ${font.name} metadata buffer`,
|
|
18507
|
+
size: Float32Array.BYTES_PER_ELEMENT * 4,
|
|
18508
|
+
usage: GPUBufferUsage.UNIFORM,
|
|
18509
|
+
mappedAtCreation: true
|
|
18510
|
+
});
|
|
18511
|
+
const fontDataArray = new Float32Array(fontDataBuffer.getMappedRange());
|
|
18512
|
+
fontDataArray[0] = font.lineHeight;
|
|
18513
|
+
fontDataBuffer.unmap();
|
|
18514
|
+
const fontBindGroup = device.createBindGroup({
|
|
18515
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
18516
|
+
entries: [
|
|
18517
|
+
{
|
|
18518
|
+
binding: 0,
|
|
18519
|
+
resource: texture.createView()
|
|
18520
|
+
},
|
|
18521
|
+
{
|
|
18522
|
+
binding: 1,
|
|
18523
|
+
resource: device.createSampler(sampler)
|
|
18524
|
+
},
|
|
18525
|
+
{
|
|
18526
|
+
binding: 2,
|
|
18527
|
+
resource: {
|
|
18528
|
+
buffer: charsGpuBuffer
|
|
18529
|
+
}
|
|
18530
|
+
},
|
|
18531
|
+
{
|
|
18532
|
+
binding: 3,
|
|
18533
|
+
resource: {
|
|
18534
|
+
buffer: fontDataBuffer
|
|
18535
|
+
}
|
|
18536
|
+
}
|
|
18537
|
+
]
|
|
18538
|
+
});
|
|
18539
|
+
return new FontPipeline(pipeline, font, fontBindGroup, maxCharCount);
|
|
18540
|
+
}
|
|
18541
|
+
}
|
|
18542
|
+
function pipelinePromise(device, colorFormat, label) {
|
|
18543
|
+
const shader = device.createShaderModule({
|
|
18544
|
+
label: `${label} shader`,
|
|
18545
|
+
code: text_wgsl_default
|
|
18546
|
+
});
|
|
18547
|
+
return device.createRenderPipelineAsync({
|
|
18548
|
+
label: `${label} pipeline`,
|
|
18549
|
+
layout: device.createPipelineLayout({
|
|
18550
|
+
bindGroupLayouts: [
|
|
18551
|
+
device.createBindGroupLayout(fontBindGroupLayout),
|
|
18552
|
+
device.createBindGroupLayout(textUniformBindGroupLayout),
|
|
18553
|
+
device.createBindGroupLayout(engineUniformBindGroupLayout)
|
|
18554
|
+
]
|
|
18555
|
+
}),
|
|
18556
|
+
vertex: {
|
|
18557
|
+
module: shader,
|
|
18558
|
+
entryPoint: "vertexMain"
|
|
18559
|
+
},
|
|
18560
|
+
fragment: {
|
|
18561
|
+
module: shader,
|
|
18562
|
+
entryPoint: "fragmentMain",
|
|
18563
|
+
targets: [
|
|
18564
|
+
{
|
|
18565
|
+
format: colorFormat,
|
|
18566
|
+
blend: {
|
|
18567
|
+
color: {
|
|
18568
|
+
srcFactor: "src-alpha",
|
|
18569
|
+
dstFactor: "one-minus-src-alpha"
|
|
18570
|
+
},
|
|
18571
|
+
alpha: {
|
|
18572
|
+
srcFactor: "one",
|
|
18573
|
+
dstFactor: "one"
|
|
18574
|
+
}
|
|
18575
|
+
}
|
|
18576
|
+
}
|
|
18577
|
+
]
|
|
18578
|
+
},
|
|
18579
|
+
primitive: {
|
|
18580
|
+
topology: "triangle-strip",
|
|
18581
|
+
stripIndexFormat: "uint32"
|
|
18582
|
+
}
|
|
18583
|
+
});
|
|
18584
|
+
}
|
|
18585
|
+
if (typeof GPUShaderStage === "undefined") {
|
|
18586
|
+
globalThis.GPUShaderStage = {
|
|
18587
|
+
VERTEX: 1,
|
|
18588
|
+
FRAGMENT: 2,
|
|
18589
|
+
COMPUTE: 4
|
|
18590
|
+
};
|
|
18591
|
+
}
|
|
18592
|
+
var fontBindGroupLayout = {
|
|
18593
|
+
label: "MSDF font group layout",
|
|
18594
|
+
entries: [
|
|
18595
|
+
{
|
|
18596
|
+
binding: 0,
|
|
18597
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
18598
|
+
texture: {}
|
|
18599
|
+
},
|
|
18600
|
+
{
|
|
18601
|
+
binding: 1,
|
|
18602
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
18603
|
+
sampler: {}
|
|
18604
|
+
},
|
|
18605
|
+
{
|
|
18606
|
+
binding: 2,
|
|
18607
|
+
visibility: GPUShaderStage.VERTEX,
|
|
18608
|
+
buffer: { type: "read-only-storage" }
|
|
18609
|
+
},
|
|
18610
|
+
{
|
|
18611
|
+
binding: 3,
|
|
18612
|
+
visibility: GPUShaderStage.VERTEX,
|
|
18613
|
+
buffer: {}
|
|
18614
|
+
}
|
|
18615
|
+
]
|
|
18616
|
+
};
|
|
18617
|
+
var engineUniformBindGroupLayout = {
|
|
18618
|
+
label: "Uniform bind group",
|
|
18619
|
+
entries: [
|
|
18620
|
+
{
|
|
18621
|
+
binding: 0,
|
|
18622
|
+
visibility: GPUShaderStage.VERTEX,
|
|
18623
|
+
buffer: {}
|
|
18624
|
+
}
|
|
18625
|
+
]
|
|
18626
|
+
};
|
|
18627
|
+
var sampler = {
|
|
18628
|
+
label: "MSDF text sampler",
|
|
18629
|
+
minFilter: "linear",
|
|
18630
|
+
magFilter: "linear",
|
|
18631
|
+
mipmapFilter: "linear",
|
|
18632
|
+
maxAnisotropy: 16
|
|
18633
|
+
};
|
|
18634
|
+
var textUniformBindGroupLayout = {
|
|
18635
|
+
label: "MSDF text block uniform",
|
|
18636
|
+
entries: [
|
|
18637
|
+
{
|
|
18638
|
+
binding: 0,
|
|
18639
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
18640
|
+
buffer: { type: "read-only-storage" }
|
|
18641
|
+
},
|
|
18642
|
+
{
|
|
18643
|
+
binding: 1,
|
|
18644
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
18645
|
+
buffer: { type: "read-only-storage" }
|
|
18646
|
+
}
|
|
18647
|
+
]
|
|
18648
|
+
};
|
|
18297
18649
|
// src/backends/webgpu/WebGPUTextShader.ts
|
|
18298
18650
|
var deets = new _t2(text_wgsl_default);
|
|
18299
18651
|
var struct = deets.structs.find((s3) => s3.name === "TextBlockDescriptor");
|
|
@@ -19723,7 +20075,9 @@ class AssetManager {
|
|
|
19723
20075
|
const textShader = new WebGPUTextShader(webgpuBackend, fontPipeline, font, presentationFormat, limits.instanceCount);
|
|
19724
20076
|
this.#fonts.set(id, textShader);
|
|
19725
20077
|
} else {
|
|
19726
|
-
const
|
|
20078
|
+
const webglBackend = this.#backend;
|
|
20079
|
+
const fontPipeline = WebGLFontPipeline.create(webglBackend.gl, font, limits.maxTextLength);
|
|
20080
|
+
const textShader = new WebGLTextShader(webglBackend, fontPipeline);
|
|
19727
20081
|
this.#fonts.set(id, textShader);
|
|
19728
20082
|
}
|
|
19729
20083
|
return id;
|
|
@@ -20444,5 +20798,5 @@ export {
|
|
|
20444
20798
|
AssetManager
|
|
20445
20799
|
};
|
|
20446
20800
|
|
|
20447
|
-
//# debugId=
|
|
20801
|
+
//# debugId=4A88BA8B6CA05C3D64756E2164756E21
|
|
20448
20802
|
//# sourceMappingURL=mod.js.map
|