@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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { IRenderBackend } from "../backends/IRenderBackend";
|
|
2
2
|
import type { ITextShader } from "../backends/ITextShader";
|
|
3
|
+
import type { WebGLBackend } from "../backends/webgl2/WebGLBackend";
|
|
4
|
+
import { WebGLFontPipeline } from "../backends/webgl2/WebGLFontPipeline";
|
|
3
5
|
import { WebGLTextShader } from "../backends/webgl2/WebGLTextShader";
|
|
4
6
|
import { FontPipeline } from "../backends/webgpu/FontPipeline";
|
|
5
7
|
import { TextureComputeShader } from "../backends/webgpu/TextureComputeShader";
|
|
@@ -19,7 +21,11 @@ import type {
|
|
|
19
21
|
TextureBundleOpts,
|
|
20
22
|
TextureWithMetadata,
|
|
21
23
|
} from "./types";
|
|
22
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
getBitmapFromUrl,
|
|
26
|
+
packBitmapsToAtlas,
|
|
27
|
+
packBitmapsToAtlasCPU,
|
|
28
|
+
} from "./util";
|
|
23
29
|
|
|
24
30
|
export type TextureId = string;
|
|
25
31
|
export type BundleId = string;
|
|
@@ -349,8 +355,14 @@ export class AssetManager {
|
|
|
349
355
|
);
|
|
350
356
|
this.#fonts.set(id, textShader);
|
|
351
357
|
} else {
|
|
352
|
-
// WebGL: font
|
|
353
|
-
const
|
|
358
|
+
// WebGL: create font pipeline and text shader for rendering
|
|
359
|
+
const webglBackend = this.#backend as WebGLBackend;
|
|
360
|
+
const fontPipeline = WebGLFontPipeline.create(
|
|
361
|
+
webglBackend.gl,
|
|
362
|
+
font,
|
|
363
|
+
limits.maxTextLength,
|
|
364
|
+
);
|
|
365
|
+
const textShader = new WebGLTextShader(webglBackend, fontPipeline);
|
|
354
366
|
this.#fonts.set(id, textShader);
|
|
355
367
|
}
|
|
356
368
|
|
|
@@ -424,41 +436,58 @@ export class AssetManager {
|
|
|
424
436
|
bundleId: BundleId,
|
|
425
437
|
opts: TextureBundleOpts,
|
|
426
438
|
) {
|
|
427
|
-
if (this.#backend.type
|
|
428
|
-
|
|
429
|
-
|
|
439
|
+
if (this.#backend.type === "webgpu") {
|
|
440
|
+
// WebGPU path: supports optional crop compute shader
|
|
441
|
+
const device = (this.#backend as WebGPUBackend).device;
|
|
442
|
+
const images = new Map<string, TextureWithMetadata>();
|
|
443
|
+
|
|
444
|
+
await Promise.all(
|
|
445
|
+
Object.entries(opts.textures).map(async ([id, url]) => {
|
|
446
|
+
const bitmap = await getBitmapFromUrl(url);
|
|
447
|
+
let textureWrapper: TextureWithMetadata = this.#wrapBitmapToTexture(
|
|
448
|
+
bitmap,
|
|
449
|
+
id,
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
if (opts.cropTransparentPixels && this.#cropComputeShader) {
|
|
453
|
+
textureWrapper =
|
|
454
|
+
await this.#cropComputeShader.processTexture(textureWrapper);
|
|
455
|
+
}
|
|
456
|
+
images.set(id, textureWrapper);
|
|
457
|
+
}),
|
|
430
458
|
);
|
|
431
|
-
}
|
|
432
459
|
|
|
433
|
-
|
|
434
|
-
|
|
460
|
+
const atlases = await packBitmapsToAtlas(
|
|
461
|
+
images,
|
|
462
|
+
this.#backend.limits.textureSize,
|
|
463
|
+
device,
|
|
464
|
+
);
|
|
435
465
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
let textureWrapper: TextureWithMetadata = this.#wrapBitmapToTexture(
|
|
443
|
-
bitmap,
|
|
444
|
-
id,
|
|
466
|
+
this.bundles.registerDynamicBundle(bundleId, atlases);
|
|
467
|
+
} else {
|
|
468
|
+
// WebGL2 path: CPU-only packing without cropping support
|
|
469
|
+
if (opts.cropTransparentPixels) {
|
|
470
|
+
console.warn(
|
|
471
|
+
"cropTransparentPixels is not supported on WebGL2 backend and will be ignored.",
|
|
445
472
|
);
|
|
473
|
+
}
|
|
446
474
|
|
|
447
|
-
|
|
448
|
-
textureWrapper =
|
|
449
|
-
await this.#cropComputeShader.processTexture(textureWrapper);
|
|
450
|
-
}
|
|
451
|
-
images.set(id, textureWrapper);
|
|
452
|
-
}),
|
|
453
|
-
);
|
|
475
|
+
const images = new Map<string, { bitmap: ImageBitmap; id: string }>();
|
|
454
476
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
477
|
+
await Promise.all(
|
|
478
|
+
Object.entries(opts.textures).map(async ([id, url]) => {
|
|
479
|
+
const bitmap = await getBitmapFromUrl(url);
|
|
480
|
+
images.set(id, { bitmap, id });
|
|
481
|
+
}),
|
|
482
|
+
);
|
|
460
483
|
|
|
461
|
-
|
|
484
|
+
const atlases = await packBitmapsToAtlasCPU(
|
|
485
|
+
images,
|
|
486
|
+
this.#backend.limits.textureSize,
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
this.bundles.registerDynamicBundle(bundleId, atlases);
|
|
490
|
+
}
|
|
462
491
|
}
|
|
463
492
|
|
|
464
493
|
async #registerBundleFromAtlases(bundleId: BundleId, opts: AtlasBundleOpts) {
|
package/src/textures/util.ts
CHANGED
|
@@ -258,3 +258,143 @@ type Rectangle = {
|
|
|
258
258
|
width: number;
|
|
259
259
|
height: number;
|
|
260
260
|
};
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* CPU-only version of packBitmapsToAtlas for WebGL2 backend.
|
|
264
|
+
* Uses OffscreenCanvas to composite packed bitmaps instead of GPU textures.
|
|
265
|
+
* Does not support transparent pixel cropping.
|
|
266
|
+
*/
|
|
267
|
+
export async function packBitmapsToAtlasCPU(
|
|
268
|
+
images: Map<string, { bitmap: ImageBitmap; id: string }>,
|
|
269
|
+
textureSize: number,
|
|
270
|
+
): Promise<CpuTextureAtlas[]> {
|
|
271
|
+
const cpuTextureAtlases: CpuTextureAtlas[] = [];
|
|
272
|
+
const packed: PackedTexture[] = [];
|
|
273
|
+
const spaces: Rectangle[] = [
|
|
274
|
+
{ x: 0, y: 0, width: textureSize, height: textureSize },
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
let atlasRegionMap = new Map<string, TextureRegion>();
|
|
278
|
+
|
|
279
|
+
for (const [id, { bitmap }] of images) {
|
|
280
|
+
// Find best fitting space using guillotine method
|
|
281
|
+
let bestSpace = -1;
|
|
282
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
283
|
+
|
|
284
|
+
for (let i = 0; i < spaces.length; i++) {
|
|
285
|
+
const space = spaces[i];
|
|
286
|
+
if (bitmap.width <= space.width && bitmap.height <= space.height) {
|
|
287
|
+
// Score based on how well it fits (smaller score is better)
|
|
288
|
+
const score = Math.abs(
|
|
289
|
+
space.width * space.height - bitmap.width * bitmap.height,
|
|
290
|
+
);
|
|
291
|
+
if (score < bestScore) {
|
|
292
|
+
bestScore = score;
|
|
293
|
+
bestSpace = i;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (bestSpace === -1) {
|
|
299
|
+
// Current atlas is full, finalize it and start a new one
|
|
300
|
+
const tex = createAtlasBitmapFromPacked(packed, textureSize);
|
|
301
|
+
cpuTextureAtlases.push({
|
|
302
|
+
texture: tex,
|
|
303
|
+
textureRegions: atlasRegionMap,
|
|
304
|
+
width: tex.width,
|
|
305
|
+
height: tex.height,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
atlasRegionMap = new Map<string, TextureRegion>();
|
|
309
|
+
packed.length = 0;
|
|
310
|
+
|
|
311
|
+
spaces.length = 0;
|
|
312
|
+
spaces.push({
|
|
313
|
+
x: 0,
|
|
314
|
+
y: 0,
|
|
315
|
+
width: textureSize,
|
|
316
|
+
height: textureSize,
|
|
317
|
+
});
|
|
318
|
+
bestSpace = 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const space = spaces[bestSpace];
|
|
322
|
+
|
|
323
|
+
// Pack the image
|
|
324
|
+
packed.push({
|
|
325
|
+
texture: bitmap,
|
|
326
|
+
x: space.x,
|
|
327
|
+
y: space.y,
|
|
328
|
+
width: bitmap.width,
|
|
329
|
+
height: bitmap.height,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Split remaining space into two new spaces
|
|
333
|
+
spaces.splice(bestSpace, 1);
|
|
334
|
+
|
|
335
|
+
if (space.width - bitmap.width > 0) {
|
|
336
|
+
spaces.push({
|
|
337
|
+
x: space.x + bitmap.width,
|
|
338
|
+
y: space.y,
|
|
339
|
+
width: space.width - bitmap.width,
|
|
340
|
+
height: bitmap.height,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (space.height - bitmap.height > 0) {
|
|
345
|
+
spaces.push({
|
|
346
|
+
x: space.x,
|
|
347
|
+
y: space.y + bitmap.height,
|
|
348
|
+
width: space.width,
|
|
349
|
+
height: space.height - bitmap.height,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Create atlas coords (no cropping for CPU path)
|
|
354
|
+
const uvScale = {
|
|
355
|
+
width: bitmap.width / textureSize,
|
|
356
|
+
height: bitmap.height / textureSize,
|
|
357
|
+
};
|
|
358
|
+
atlasRegionMap.set(id, {
|
|
359
|
+
uvOffset: {
|
|
360
|
+
x: space.x / textureSize,
|
|
361
|
+
y: space.y / textureSize,
|
|
362
|
+
},
|
|
363
|
+
uvScale,
|
|
364
|
+
uvScaleCropped: uvScale,
|
|
365
|
+
cropOffset: { x: 0, y: 0 },
|
|
366
|
+
originalSize: { width: bitmap.width, height: bitmap.height },
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Finalize the last atlas
|
|
371
|
+
const tex = createAtlasBitmapFromPacked(packed, textureSize);
|
|
372
|
+
cpuTextureAtlases.push({
|
|
373
|
+
texture: tex,
|
|
374
|
+
textureRegions: atlasRegionMap,
|
|
375
|
+
width: tex.width,
|
|
376
|
+
height: tex.height,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
return cpuTextureAtlases;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Composites packed textures onto an OffscreenCanvas and returns an ImageBitmap.
|
|
384
|
+
*/
|
|
385
|
+
function createAtlasBitmapFromPacked(
|
|
386
|
+
packed: PackedTexture[],
|
|
387
|
+
atlasSize: number,
|
|
388
|
+
): ImageBitmap {
|
|
389
|
+
const canvas = new OffscreenCanvas(atlasSize, atlasSize);
|
|
390
|
+
const ctx = canvas.getContext("2d");
|
|
391
|
+
if (!ctx) {
|
|
392
|
+
throw new Error("Failed to get 2d context from OffscreenCanvas");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
for (const texture of packed) {
|
|
396
|
+
ctx.drawImage(texture.texture, texture.x, texture.y);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return canvas.transferToImageBitmap();
|
|
400
|
+
}
|