@bloopjs/toodle 0.1.3 → 0.1.5
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 +919 -459
- package/dist/mod.js.map +10 -8
- package/dist/textures/AssetManager.d.ts.map +1 -1
- package/dist/textures/util.d.ts +9 -0
- package/dist/textures/util.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 +60 -31
- package/src/textures/util.ts +140 -0
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
|
}
|
|
@@ -18218,82 +17952,700 @@ class SceneNode {
|
|
|
18218
17952
|
};
|
|
18219
17953
|
}
|
|
18220
17954
|
}
|
|
18221
|
-
function reviver(key, value) {
|
|
18222
|
-
if (key === "kids") {
|
|
18223
|
-
return value.map((kid) => new SceneNode(kid));
|
|
18224
|
-
}
|
|
18225
|
-
if (Array.isArray(value) && value.every((v3) => typeof v3 === "number")) {
|
|
18226
|
-
if (value.length === 2) {
|
|
18227
|
-
return value;
|
|
18228
|
-
}
|
|
18229
|
-
if (value.length === 3) {
|
|
18230
|
-
return value;
|
|
18231
|
-
}
|
|
18232
|
-
if (value.length === 4) {
|
|
18233
|
-
return value;
|
|
17955
|
+
function reviver(key, value) {
|
|
17956
|
+
if (key === "kids") {
|
|
17957
|
+
return value.map((kid) => new SceneNode(kid));
|
|
17958
|
+
}
|
|
17959
|
+
if (Array.isArray(value) && value.every((v3) => typeof v3 === "number")) {
|
|
17960
|
+
if (value.length === 2) {
|
|
17961
|
+
return value;
|
|
17962
|
+
}
|
|
17963
|
+
if (value.length === 3) {
|
|
17964
|
+
return value;
|
|
17965
|
+
}
|
|
17966
|
+
if (value.length === 4) {
|
|
17967
|
+
return value;
|
|
17968
|
+
}
|
|
17969
|
+
}
|
|
17970
|
+
return value;
|
|
17971
|
+
}
|
|
17972
|
+
|
|
17973
|
+
// src/scene/TextNode.ts
|
|
17974
|
+
var DEFAULT_FONT_SIZE = 14;
|
|
17975
|
+
|
|
17976
|
+
class TextNode extends SceneNode {
|
|
17977
|
+
#text;
|
|
17978
|
+
#formatting;
|
|
17979
|
+
#font;
|
|
17980
|
+
constructor(shader, text, opts = {}) {
|
|
17981
|
+
const { width, height } = measureText(shader.font, text, opts.wordWrap);
|
|
17982
|
+
if (text.length > shader.maxCharCount) {
|
|
17983
|
+
throw new Error(`Text: ${text} exceeds ${shader.maxCharCount} characters. Try using fewer characters or increase the limit in Toodle.attach.`);
|
|
17984
|
+
}
|
|
17985
|
+
const em2px = shader.font.lineHeight / (opts.fontSize ?? DEFAULT_FONT_SIZE);
|
|
17986
|
+
if (!opts.shrinkToFit && !opts.size) {
|
|
17987
|
+
opts.size = { width: width / em2px, height: height / em2px };
|
|
17988
|
+
}
|
|
17989
|
+
super({
|
|
17990
|
+
...opts,
|
|
17991
|
+
render: {
|
|
17992
|
+
shader,
|
|
17993
|
+
writeInstance: (_node, _array, _offset) => {
|
|
17994
|
+
throw new Error("not implemented - needs access to text uniform buffer, dimensions and a model matrix");
|
|
17995
|
+
}
|
|
17996
|
+
}
|
|
17997
|
+
});
|
|
17998
|
+
this.#font = shader.font;
|
|
17999
|
+
this.#text = text;
|
|
18000
|
+
this.#formatting = opts;
|
|
18001
|
+
}
|
|
18002
|
+
get text() {
|
|
18003
|
+
return this.#text;
|
|
18004
|
+
}
|
|
18005
|
+
get formatting() {
|
|
18006
|
+
return this.#formatting;
|
|
18007
|
+
}
|
|
18008
|
+
get font() {
|
|
18009
|
+
return this.#font;
|
|
18010
|
+
}
|
|
18011
|
+
set text(text) {
|
|
18012
|
+
if (!text) {
|
|
18013
|
+
throw new Error("text cannot be empty");
|
|
18014
|
+
}
|
|
18015
|
+
this.#text = text;
|
|
18016
|
+
this.setDirty();
|
|
18017
|
+
}
|
|
18018
|
+
get tint() {
|
|
18019
|
+
return this.#formatting.color || { r: 1, g: 1, b: 1, a: 1 };
|
|
18020
|
+
}
|
|
18021
|
+
set tint(tint) {
|
|
18022
|
+
this.#formatting.color = tint;
|
|
18023
|
+
this.setDirty();
|
|
18024
|
+
}
|
|
18025
|
+
set formatting(formatting) {
|
|
18026
|
+
this.#formatting = formatting;
|
|
18027
|
+
this.setDirty();
|
|
18028
|
+
}
|
|
18029
|
+
}
|
|
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"
|
|
18234
18582
|
}
|
|
18235
|
-
}
|
|
18236
|
-
return value;
|
|
18583
|
+
});
|
|
18237
18584
|
}
|
|
18238
|
-
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
|
|
18242
|
-
|
|
18243
|
-
|
|
18244
|
-
|
|
18245
|
-
|
|
18246
|
-
|
|
18247
|
-
|
|
18248
|
-
|
|
18249
|
-
|
|
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: {}
|
|
18250
18614
|
}
|
|
18251
|
-
|
|
18252
|
-
|
|
18253
|
-
|
|
18615
|
+
]
|
|
18616
|
+
};
|
|
18617
|
+
var engineUniformBindGroupLayout = {
|
|
18618
|
+
label: "Uniform bind group",
|
|
18619
|
+
entries: [
|
|
18620
|
+
{
|
|
18621
|
+
binding: 0,
|
|
18622
|
+
visibility: GPUShaderStage.VERTEX,
|
|
18623
|
+
buffer: {}
|
|
18254
18624
|
}
|
|
18255
|
-
|
|
18256
|
-
|
|
18257
|
-
|
|
18258
|
-
|
|
18259
|
-
|
|
18260
|
-
|
|
18261
|
-
|
|
18262
|
-
|
|
18263
|
-
|
|
18264
|
-
|
|
18265
|
-
|
|
18266
|
-
|
|
18267
|
-
|
|
18268
|
-
|
|
18269
|
-
|
|
18270
|
-
|
|
18271
|
-
|
|
18272
|
-
|
|
18273
|
-
|
|
18274
|
-
|
|
18275
|
-
|
|
18276
|
-
}
|
|
18277
|
-
set text(text) {
|
|
18278
|
-
if (!text) {
|
|
18279
|
-
throw new Error("text cannot be empty");
|
|
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" }
|
|
18280
18646
|
}
|
|
18281
|
-
|
|
18282
|
-
|
|
18283
|
-
}
|
|
18284
|
-
get tint() {
|
|
18285
|
-
return this.#formatting.color || { r: 1, g: 1, b: 1, a: 1 };
|
|
18286
|
-
}
|
|
18287
|
-
set tint(tint) {
|
|
18288
|
-
this.#formatting.color = tint;
|
|
18289
|
-
this.setDirty();
|
|
18290
|
-
}
|
|
18291
|
-
set formatting(formatting) {
|
|
18292
|
-
this.#formatting = formatting;
|
|
18293
|
-
this.setDirty();
|
|
18294
|
-
}
|
|
18295
|
-
}
|
|
18296
|
-
|
|
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");
|
|
@@ -19574,6 +19926,105 @@ async function textureToBitmap(device, texture, width, height) {
|
|
|
19574
19926
|
readBuffer.unmap();
|
|
19575
19927
|
return bitmap;
|
|
19576
19928
|
}
|
|
19929
|
+
async function packBitmapsToAtlasCPU(images, textureSize) {
|
|
19930
|
+
const cpuTextureAtlases = [];
|
|
19931
|
+
const packed = [];
|
|
19932
|
+
const spaces = [
|
|
19933
|
+
{ x: 0, y: 0, width: textureSize, height: textureSize }
|
|
19934
|
+
];
|
|
19935
|
+
let atlasRegionMap = new Map;
|
|
19936
|
+
for (const [id, { bitmap }] of images) {
|
|
19937
|
+
let bestSpace = -1;
|
|
19938
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
19939
|
+
for (let i3 = 0;i3 < spaces.length; i3++) {
|
|
19940
|
+
const space2 = spaces[i3];
|
|
19941
|
+
if (bitmap.width <= space2.width && bitmap.height <= space2.height) {
|
|
19942
|
+
const score = Math.abs(space2.width * space2.height - bitmap.width * bitmap.height);
|
|
19943
|
+
if (score < bestScore) {
|
|
19944
|
+
bestScore = score;
|
|
19945
|
+
bestSpace = i3;
|
|
19946
|
+
}
|
|
19947
|
+
}
|
|
19948
|
+
}
|
|
19949
|
+
if (bestSpace === -1) {
|
|
19950
|
+
const tex2 = createAtlasBitmapFromPacked(packed, textureSize);
|
|
19951
|
+
cpuTextureAtlases.push({
|
|
19952
|
+
texture: tex2,
|
|
19953
|
+
textureRegions: atlasRegionMap,
|
|
19954
|
+
width: tex2.width,
|
|
19955
|
+
height: tex2.height
|
|
19956
|
+
});
|
|
19957
|
+
atlasRegionMap = new Map;
|
|
19958
|
+
packed.length = 0;
|
|
19959
|
+
spaces.length = 0;
|
|
19960
|
+
spaces.push({
|
|
19961
|
+
x: 0,
|
|
19962
|
+
y: 0,
|
|
19963
|
+
width: textureSize,
|
|
19964
|
+
height: textureSize
|
|
19965
|
+
});
|
|
19966
|
+
bestSpace = 0;
|
|
19967
|
+
}
|
|
19968
|
+
const space = spaces[bestSpace];
|
|
19969
|
+
packed.push({
|
|
19970
|
+
texture: bitmap,
|
|
19971
|
+
x: space.x,
|
|
19972
|
+
y: space.y,
|
|
19973
|
+
width: bitmap.width,
|
|
19974
|
+
height: bitmap.height
|
|
19975
|
+
});
|
|
19976
|
+
spaces.splice(bestSpace, 1);
|
|
19977
|
+
if (space.width - bitmap.width > 0) {
|
|
19978
|
+
spaces.push({
|
|
19979
|
+
x: space.x + bitmap.width,
|
|
19980
|
+
y: space.y,
|
|
19981
|
+
width: space.width - bitmap.width,
|
|
19982
|
+
height: bitmap.height
|
|
19983
|
+
});
|
|
19984
|
+
}
|
|
19985
|
+
if (space.height - bitmap.height > 0) {
|
|
19986
|
+
spaces.push({
|
|
19987
|
+
x: space.x,
|
|
19988
|
+
y: space.y + bitmap.height,
|
|
19989
|
+
width: space.width,
|
|
19990
|
+
height: space.height - bitmap.height
|
|
19991
|
+
});
|
|
19992
|
+
}
|
|
19993
|
+
const uvScale = {
|
|
19994
|
+
width: bitmap.width / textureSize,
|
|
19995
|
+
height: bitmap.height / textureSize
|
|
19996
|
+
};
|
|
19997
|
+
atlasRegionMap.set(id, {
|
|
19998
|
+
uvOffset: {
|
|
19999
|
+
x: space.x / textureSize,
|
|
20000
|
+
y: space.y / textureSize
|
|
20001
|
+
},
|
|
20002
|
+
uvScale,
|
|
20003
|
+
uvScaleCropped: uvScale,
|
|
20004
|
+
cropOffset: { x: 0, y: 0 },
|
|
20005
|
+
originalSize: { width: bitmap.width, height: bitmap.height }
|
|
20006
|
+
});
|
|
20007
|
+
}
|
|
20008
|
+
const tex = createAtlasBitmapFromPacked(packed, textureSize);
|
|
20009
|
+
cpuTextureAtlases.push({
|
|
20010
|
+
texture: tex,
|
|
20011
|
+
textureRegions: atlasRegionMap,
|
|
20012
|
+
width: tex.width,
|
|
20013
|
+
height: tex.height
|
|
20014
|
+
});
|
|
20015
|
+
return cpuTextureAtlases;
|
|
20016
|
+
}
|
|
20017
|
+
function createAtlasBitmapFromPacked(packed, atlasSize) {
|
|
20018
|
+
const canvas = new OffscreenCanvas(atlasSize, atlasSize);
|
|
20019
|
+
const ctx = canvas.getContext("2d");
|
|
20020
|
+
if (!ctx) {
|
|
20021
|
+
throw new Error("Failed to get 2d context from OffscreenCanvas");
|
|
20022
|
+
}
|
|
20023
|
+
for (const texture of packed) {
|
|
20024
|
+
ctx.drawImage(texture.texture, texture.x, texture.y);
|
|
20025
|
+
}
|
|
20026
|
+
return canvas.transferToImageBitmap();
|
|
20027
|
+
}
|
|
19577
20028
|
|
|
19578
20029
|
// src/textures/AssetManager.ts
|
|
19579
20030
|
class AssetManager {
|
|
@@ -19723,7 +20174,9 @@ class AssetManager {
|
|
|
19723
20174
|
const textShader = new WebGPUTextShader(webgpuBackend, fontPipeline, font, presentationFormat, limits.instanceCount);
|
|
19724
20175
|
this.#fonts.set(id, textShader);
|
|
19725
20176
|
} else {
|
|
19726
|
-
const
|
|
20177
|
+
const webglBackend = this.#backend;
|
|
20178
|
+
const fontPipeline = WebGLFontPipeline.create(webglBackend.gl, font, limits.maxTextLength);
|
|
20179
|
+
const textShader = new WebGLTextShader(webglBackend, fontPipeline);
|
|
19727
20180
|
this.#fonts.set(id, textShader);
|
|
19728
20181
|
}
|
|
19729
20182
|
return id;
|
|
@@ -19761,24 +20214,31 @@ class AssetManager {
|
|
|
19761
20214
|
return texture;
|
|
19762
20215
|
}
|
|
19763
20216
|
async#registerBundleFromTextures(bundleId, opts) {
|
|
19764
|
-
if (this.#backend.type
|
|
19765
|
-
|
|
19766
|
-
|
|
19767
|
-
|
|
19768
|
-
|
|
19769
|
-
|
|
19770
|
-
|
|
19771
|
-
|
|
19772
|
-
|
|
19773
|
-
|
|
19774
|
-
|
|
19775
|
-
|
|
19776
|
-
|
|
20217
|
+
if (this.#backend.type === "webgpu") {
|
|
20218
|
+
const device = this.#backend.device;
|
|
20219
|
+
const images = new Map;
|
|
20220
|
+
await Promise.all(Object.entries(opts.textures).map(async ([id, url]) => {
|
|
20221
|
+
const bitmap = await getBitmapFromUrl(url);
|
|
20222
|
+
let textureWrapper = this.#wrapBitmapToTexture(bitmap, id);
|
|
20223
|
+
if (opts.cropTransparentPixels && this.#cropComputeShader) {
|
|
20224
|
+
textureWrapper = await this.#cropComputeShader.processTexture(textureWrapper);
|
|
20225
|
+
}
|
|
20226
|
+
images.set(id, textureWrapper);
|
|
20227
|
+
}));
|
|
20228
|
+
const atlases = await packBitmapsToAtlas(images, this.#backend.limits.textureSize, device);
|
|
20229
|
+
this.bundles.registerDynamicBundle(bundleId, atlases);
|
|
20230
|
+
} else {
|
|
20231
|
+
if (opts.cropTransparentPixels) {
|
|
20232
|
+
console.warn("cropTransparentPixels is not supported on WebGL2 backend and will be ignored.");
|
|
19777
20233
|
}
|
|
19778
|
-
images
|
|
19779
|
-
|
|
19780
|
-
|
|
19781
|
-
|
|
20234
|
+
const images = new Map;
|
|
20235
|
+
await Promise.all(Object.entries(opts.textures).map(async ([id, url]) => {
|
|
20236
|
+
const bitmap = await getBitmapFromUrl(url);
|
|
20237
|
+
images.set(id, { bitmap, id });
|
|
20238
|
+
}));
|
|
20239
|
+
const atlases = await packBitmapsToAtlasCPU(images, this.#backend.limits.textureSize);
|
|
20240
|
+
this.bundles.registerDynamicBundle(bundleId, atlases);
|
|
20241
|
+
}
|
|
19782
20242
|
}
|
|
19783
20243
|
async#registerBundleFromAtlases(bundleId, opts) {
|
|
19784
20244
|
await this.bundles.registerAtlasBundle(bundleId, opts);
|
|
@@ -20444,5 +20904,5 @@ export {
|
|
|
20444
20904
|
AssetManager
|
|
20445
20905
|
};
|
|
20446
20906
|
|
|
20447
|
-
//# debugId=
|
|
20907
|
+
//# debugId=6E0EAD9B5EBDC37664756E2164756E21
|
|
20448
20908
|
//# sourceMappingURL=mod.js.map
|