@bloopjs/toodle 0.1.2 → 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.
Files changed (55) hide show
  1. package/dist/Toodle.d.ts +1 -1
  2. package/dist/Toodle.d.ts.map +1 -1
  3. package/dist/backends/ITextShader.d.ts +15 -0
  4. package/dist/backends/ITextShader.d.ts.map +1 -0
  5. package/dist/backends/mod.d.ts +1 -0
  6. package/dist/backends/mod.d.ts.map +1 -1
  7. package/dist/backends/webgl2/WebGLFontPipeline.d.ts +26 -0
  8. package/dist/backends/webgl2/WebGLFontPipeline.d.ts.map +1 -0
  9. package/dist/backends/webgl2/WebGLTextShader.d.ts +24 -0
  10. package/dist/backends/webgl2/WebGLTextShader.d.ts.map +1 -0
  11. package/dist/backends/webgl2/glsl/text.glsl.d.ts +12 -0
  12. package/dist/backends/webgl2/glsl/text.glsl.d.ts.map +1 -0
  13. package/dist/backends/webgl2/mod.d.ts +2 -0
  14. package/dist/backends/webgl2/mod.d.ts.map +1 -1
  15. package/dist/{text → backends/webgpu}/FontPipeline.d.ts +1 -1
  16. package/dist/backends/webgpu/FontPipeline.d.ts.map +1 -0
  17. package/dist/{text/TextShader.d.ts → backends/webgpu/WebGPUTextShader.d.ts} +7 -7
  18. package/dist/backends/webgpu/WebGPUTextShader.d.ts.map +1 -0
  19. package/dist/backends/webgpu/mod.d.ts +2 -0
  20. package/dist/backends/webgpu/mod.d.ts.map +1 -1
  21. package/dist/backends/webgpu/wgsl/text.wgsl.d.ts.map +1 -0
  22. package/dist/mod.d.ts +1 -1
  23. package/dist/mod.d.ts.map +1 -1
  24. package/dist/mod.js +1832 -1460
  25. package/dist/mod.js.map +17 -14
  26. package/dist/{text → scene}/TextNode.d.ts +5 -5
  27. package/dist/scene/TextNode.d.ts.map +1 -0
  28. package/dist/scene/mod.d.ts +1 -0
  29. package/dist/scene/mod.d.ts.map +1 -1
  30. package/dist/text/mod.d.ts +1 -3
  31. package/dist/text/mod.d.ts.map +1 -1
  32. package/dist/textures/AssetManager.d.ts +9 -6
  33. package/dist/textures/AssetManager.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/Toodle.ts +1 -1
  36. package/src/backends/ITextShader.ts +15 -0
  37. package/src/backends/mod.ts +1 -0
  38. package/src/backends/webgl2/WebGLFontPipeline.ts +173 -0
  39. package/src/backends/webgl2/WebGLTextShader.ts +275 -0
  40. package/src/backends/webgl2/glsl/text.glsl.ts +132 -0
  41. package/src/backends/webgl2/mod.ts +2 -0
  42. package/src/{text → backends/webgpu}/FontPipeline.ts +2 -2
  43. package/src/{text/TextShader.ts → backends/webgpu/WebGPUTextShader.ts} +14 -10
  44. package/src/backends/webgpu/mod.ts +2 -0
  45. package/src/mod.ts +1 -1
  46. package/src/{text → scene}/TextNode.ts +6 -6
  47. package/src/scene/mod.ts +1 -0
  48. package/src/text/mod.ts +1 -4
  49. package/src/textures/AssetManager.ts +46 -31
  50. package/dist/text/FontPipeline.d.ts.map +0 -1
  51. package/dist/text/TextNode.d.ts.map +0 -1
  52. package/dist/text/TextShader.d.ts.map +0 -1
  53. package/dist/text/text.wgsl.d.ts.map +0 -1
  54. /package/dist/{text → backends/webgpu/wgsl}/text.wgsl.d.ts +0 -0
  55. /package/src/{text → backends/webgpu/wgsl}/text.wgsl.ts +0 -0
@@ -1,12 +1,12 @@
1
+ import type { ITextShader } from "../backends/ITextShader";
1
2
  import type { Color } from "../coreTypes/Color";
2
- import { type NodeOptions, SceneNode } from "../scene/SceneNode";
3
- import type { MsdfFont } from "./MsdfFont";
4
- import type { TextFormatting } from "./TextFormatting";
5
- import type { TextShader } from "./TextShader";
3
+ import type { MsdfFont } from "../text/MsdfFont";
4
+ import type { TextFormatting } from "../text/TextFormatting";
5
+ import { type NodeOptions, SceneNode } from "./SceneNode";
6
6
  export declare const DEFAULT_FONT_SIZE = 14;
7
7
  export declare class TextNode extends SceneNode {
8
8
  #private;
9
- constructor(shader: TextShader, text: string, opts?: TextOptions);
9
+ constructor(shader: ITextShader, text: string, opts?: TextOptions);
10
10
  get text(): string;
11
11
  get formatting(): TextFormatting;
12
12
  get font(): MsdfFont;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TextNode.d.ts","sourceRoot":"","sources":["../../src/scene/TextNode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,KAAK,WAAW,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE1D,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,qBAAa,QAAS,SAAQ,SAAS;;gBAKzB,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB;IAgCrE,IAAI,IAAI,IAYO,MAAM,CAVpB;IAED,IAAI,UAAU,IAyBa,cAAc,CAvBxC;IAED,IAAI,IAAI,aAEP;IAED,IAAI,IAAI,CAAC,IAAI,EAAE,MAAM,EAMpB;IAED,IAAI,IAAI,IAIO,KAAK,CAFnB;IAED,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAGnB;IAED,IAAI,UAAU,CAAC,UAAU,EAAE,cAAc,EAGxC;CACF;AAED,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,cAAc,CAAC"}
@@ -2,4 +2,5 @@ export * from "./Camera";
2
2
  export * from "./QuadNode";
3
3
  export * from "./RenderComponent";
4
4
  export * from "./SceneNode";
5
+ export * from "./TextNode";
5
6
  //# sourceMappingURL=mod.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../src/scene/mod.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../src/scene/mod.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
@@ -1,4 +1,2 @@
1
- export type { TextOptions } from "./TextNode";
2
- export { TextNode } from "./TextNode";
3
- export { TextShader } from "./TextShader";
1
+ export { WebGPUTextShader as TextShader } from "../backends/webgpu/WebGPUTextShader";
4
2
  //# sourceMappingURL=mod.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../src/text/mod.ts"],"names":[],"mappings":"AAKA,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../src/text/mod.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,gBAAgB,IAAI,UAAU,EAAE,MAAM,qCAAqC,CAAC"}
@@ -1,9 +1,9 @@
1
1
  import type { IRenderBackend } from "../backends/IRenderBackend";
2
+ import type { ITextShader } from "../backends/ITextShader";
2
3
  import type { Size } from "../coreTypes/Size";
3
4
  import type { Vec2 } from "../coreTypes/Vec2";
4
5
  import { QuadNode } from "../scene/QuadNode";
5
6
  import type { SceneNode } from "../scene/SceneNode";
6
- import { TextShader } from "../text/TextShader";
7
7
  import { Bundles } from "./Bundles";
8
8
  import type { AtlasBundleOpts, AtlasCoords, CpuTextureAtlas, TextureBundleOpts } from "./types";
9
9
  export type TextureId = string;
@@ -119,16 +119,19 @@ export declare class AssetManager {
119
119
  */
120
120
  unloadBundle(bundleId: BundleId): Promise<void>;
121
121
  /**
122
- * Load a font to the gpu
122
+ * Load a font for text rendering (WebGPU) or measurement only (WebGL).
123
123
  *
124
124
  * @param id - The id of the font to load
125
- * @param url - The url of the font to load
126
- * @param fallbackCharacter - The character to use as a fallback if the font does not contain a character to be rendererd
125
+ * @param url - The url of the font JSON to load
126
+ * @param fallbackCharacter - The character to use as a fallback if the font does not contain a character to be rendered
127
127
  *
128
- * @throws Error if using WebGL backend (fonts not supported in WebGL mode)
128
+ * @remarks
129
+ * On WebGPU backend, loads the full font for rendering.
130
+ * On WebGL backend, loads the font for text measurement only. Attempting to
131
+ * render text on WebGL will throw an error.
129
132
  */
130
133
  loadFont(id: string, url: URL, fallbackCharacter?: string): Promise<string>;
131
- getFont(id: string): TextShader;
134
+ getFont(id: string): ITextShader;
132
135
  validateTextureReference(node: SceneNode | QuadNode): void;
133
136
  /**
134
137
  * Advanced and niche features
@@ -1 +1 @@
1
- {"version":3,"file":"AssetManager.d.ts","sourceRoot":"","sources":["../../src/textures/AssetManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAGjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAE9C,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAGpD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,EACV,eAAe,EACf,WAAW,EACX,eAAe,EACf,iBAAiB,EAElB,MAAM,SAAS,CAAC;AAGjB,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC;AAC/B,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC;AAC9B,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC;AAE5B,MAAM,MAAM,mBAAmB,GAAG;IAChC,oGAAoG;IACpG,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,qBAAa,YAAY;;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;gBAOd,OAAO,EAAE,cAAc,EAAE,OAAO,GAAE,mBAAwB;IAsBtE;;OAEG;IACH,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED;;;OAGG;IACH,IAAI,YAAY,IAAI,UAAU,CAE7B;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,IAAI,CAGpB;IAED;;;;;OAKG;IACH,OAAO,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI;IAU5B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI;IAYnC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO;IAUjC;;OAEG;IACH,IAAI,QAAQ,uCAEX;IAED;;OAEG;IACH,IAAI,UAAU,aAEb;IAED;;;;;;;;;;;;;;;;OAgBG;IACG,YAAY,CAAC,IAAI,EAAE,iBAAiB,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAMtE;;;;;;;;;;;OAWG;IACG,WAAW,CACf,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,GAAG,GAAG,WAAW,EACtB,OAAO,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC;;;;IA+CtC;;;;;;;;OAQG;IACG,cAAc,CAClB,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,iBAAiB,GAAG,eAAe,GACxC,OAAO,CAAC,QAAQ,CAAC;IAcpB;;;;OAIG;IACG,UAAU,CAAC,QAAQ,EAAE,QAAQ;IAsBnC;;;;;OAKG;IACG,YAAY,CAAC,QAAQ,EAAE,QAAQ;IAkBrC;;;;;;;;OAQG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB,SAAM;IAgC5D,OAAO,CAAC,EAAE,EAAE,MAAM;IASlB,wBAAwB,CAAC,IAAI,EAAE,SAAS,GAAG,QAAQ;IAoGnD;;OAEG;IACH,KAAK;sCAEyB,MAAM,EAAE;kCAKZ,MAAM,EAAE;QAIhC;;;;;;;WAOG;6BACkB,SAAS,UAAU,WAAW;QAInD;;;;;WAKG;6BACkB,SAAS,KAAG,WAAW,EAAE;QAI9C;;;;;WAKG;+BACoB,SAAS,KAAG,IAAI;QAIvC;;;;WAIG;;YAKC;;;eAGG;;YAEH;;eAEG;;YAEH;;eAEG;;;QAKP;;;WAGG;;QAaH;;;;;WAKG;2BACsB,eAAe;QAYxC;;;;WAIG;kCAC6B,MAAM;MAItC;IAqCF;;OAEG;IACH,OAAO;CAGR"}
1
+ {"version":3,"file":"AssetManager.d.ts","sourceRoot":"","sources":["../../src/textures/AssetManager.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloopjs/toodle",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
package/src/Toodle.ts CHANGED
@@ -21,8 +21,8 @@ import { Camera } from "./scene/Camera";
21
21
  import { JumboQuadNode, type JumboQuadOptions } from "./scene/JumboQuadNode";
22
22
  import { QuadNode, type QuadOptions } from "./scene/QuadNode";
23
23
  import { type NodeOptions, SceneNode } from "./scene/SceneNode";
24
+ import { TextNode, type TextOptions } from "./scene/TextNode";
24
25
  import type { Resolution } from "./screen/resolution";
25
- import { TextNode, type TextOptions } from "./text/TextNode";
26
26
  import { AssetManager, type TextureId } from "./textures/AssetManager";
27
27
  import { Pool } from "./utils/mod";
28
28
 
@@ -0,0 +1,15 @@
1
+ import type { MsdfFont } from "../text/MsdfFont";
2
+ import type { IBackendShader } from "./IBackendShader";
3
+
4
+ /**
5
+ * Backend-agnostic text shader interface.
6
+ *
7
+ * Extends IBackendShader with text-specific properties.
8
+ * Each backend provides its own implementation.
9
+ */
10
+ export interface ITextShader extends IBackendShader {
11
+ /** The font used by this text shader */
12
+ readonly font: MsdfFont;
13
+ /** Maximum number of characters that can be rendered per text node */
14
+ readonly maxCharCount: number;
15
+ }
@@ -13,6 +13,7 @@ export type {
13
13
  BlendOperation,
14
14
  IRenderBackend,
15
15
  } from "./IRenderBackend";
16
+ export type { ITextShader } from "./ITextShader";
16
17
  export type {
17
18
  ITextureAtlas,
18
19
  TextureAtlasFormat,
@@ -0,0 +1,173 @@
1
+ import type { MsdfFont } from "../../text/MsdfFont";
2
+ import { assert } from "../../utils/assert";
3
+
4
+ /**
5
+ * Manages WebGL font resources for MSDF text rendering.
6
+ *
7
+ * Creates and manages:
8
+ * - Font atlas texture (MSDF image)
9
+ * - Character data texture (metrics as RGBA32F)
10
+ * - Text buffer texture (per-glyph positions)
11
+ */
12
+ export class WebGLFontPipeline {
13
+ readonly font: MsdfFont;
14
+ readonly fontTexture: WebGLTexture;
15
+ readonly charDataTexture: WebGLTexture;
16
+ readonly textBufferTexture: WebGLTexture;
17
+ readonly maxCharCount: number;
18
+ readonly lineHeight: number;
19
+
20
+ #gl: WebGL2RenderingContext;
21
+
22
+ private constructor(
23
+ gl: WebGL2RenderingContext,
24
+ font: MsdfFont,
25
+ fontTexture: WebGLTexture,
26
+ charDataTexture: WebGLTexture,
27
+ textBufferTexture: WebGLTexture,
28
+ maxCharCount: number,
29
+ ) {
30
+ this.#gl = gl;
31
+ this.font = font;
32
+ this.fontTexture = fontTexture;
33
+ this.charDataTexture = charDataTexture;
34
+ this.textBufferTexture = textBufferTexture;
35
+ this.maxCharCount = maxCharCount;
36
+ this.lineHeight = font.lineHeight;
37
+ }
38
+
39
+ static create(
40
+ gl: WebGL2RenderingContext,
41
+ font: MsdfFont,
42
+ maxCharCount: number,
43
+ ): WebGLFontPipeline {
44
+ // Create font atlas texture
45
+ const fontTexture = gl.createTexture();
46
+ assert(fontTexture, "Failed to create font texture");
47
+
48
+ gl.bindTexture(gl.TEXTURE_2D, fontTexture);
49
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
50
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
51
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
52
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
53
+
54
+ // Upload the MSDF font image
55
+ gl.texImage2D(
56
+ gl.TEXTURE_2D,
57
+ 0,
58
+ gl.RGBA,
59
+ gl.RGBA,
60
+ gl.UNSIGNED_BYTE,
61
+ font.imageBitmap,
62
+ );
63
+
64
+ // Create character data texture (RGBA32F)
65
+ // Each character needs 8 floats = 2 RGBA texels
66
+ // charBuffer layout per char: texOffset.x, texOffset.y, texExtent.x, texExtent.y, size.x, size.y, offset.x, offset.y
67
+ const charDataTexture = gl.createTexture();
68
+ assert(charDataTexture, "Failed to create char data texture");
69
+
70
+ gl.bindTexture(gl.TEXTURE_2D, charDataTexture);
71
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
72
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
73
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
74
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
75
+
76
+ // Convert charBuffer to texture format (2 texels per character)
77
+ const charCount = font.charCount;
78
+ const charTextureWidth = charCount * 2; // 2 texels per char
79
+ const charTextureData = new Float32Array(charTextureWidth * 4); // 4 components per texel
80
+
81
+ for (let i = 0; i < charCount; i++) {
82
+ const srcOffset = i * 8;
83
+ const dstOffset0 = i * 2 * 4; // First texel for this char
84
+ const dstOffset1 = (i * 2 + 1) * 4; // Second texel for this char
85
+
86
+ // Texel 0: texOffset.xy, texExtent.xy
87
+ charTextureData[dstOffset0] = font.charBuffer[srcOffset]; // texOffset.x
88
+ charTextureData[dstOffset0 + 1] = font.charBuffer[srcOffset + 1]; // texOffset.y
89
+ charTextureData[dstOffset0 + 2] = font.charBuffer[srcOffset + 2]; // texExtent.x
90
+ charTextureData[dstOffset0 + 3] = font.charBuffer[srcOffset + 3]; // texExtent.y
91
+
92
+ // Texel 1: size.xy, offset.xy
93
+ charTextureData[dstOffset1] = font.charBuffer[srcOffset + 4]; // size.x
94
+ charTextureData[dstOffset1 + 1] = font.charBuffer[srcOffset + 5]; // size.y
95
+ charTextureData[dstOffset1 + 2] = font.charBuffer[srcOffset + 6]; // offset.x
96
+ charTextureData[dstOffset1 + 3] = font.charBuffer[srcOffset + 7]; // offset.y
97
+ }
98
+
99
+ gl.texImage2D(
100
+ gl.TEXTURE_2D,
101
+ 0,
102
+ gl.RGBA32F,
103
+ charTextureWidth,
104
+ 1,
105
+ 0,
106
+ gl.RGBA,
107
+ gl.FLOAT,
108
+ charTextureData,
109
+ );
110
+
111
+ // Create text buffer texture (RGBA32F)
112
+ // Each glyph needs 1 texel: xy = position, z = charIndex, w = unused
113
+ const textBufferTexture = gl.createTexture();
114
+ assert(textBufferTexture, "Failed to create text buffer texture");
115
+
116
+ gl.bindTexture(gl.TEXTURE_2D, textBufferTexture);
117
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
118
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
119
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
120
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
121
+
122
+ // Allocate texture storage (will be filled per-frame)
123
+ gl.texImage2D(
124
+ gl.TEXTURE_2D,
125
+ 0,
126
+ gl.RGBA32F,
127
+ maxCharCount,
128
+ 1,
129
+ 0,
130
+ gl.RGBA,
131
+ gl.FLOAT,
132
+ null,
133
+ );
134
+
135
+ gl.bindTexture(gl.TEXTURE_2D, null);
136
+
137
+ return new WebGLFontPipeline(
138
+ gl,
139
+ font,
140
+ fontTexture,
141
+ charDataTexture,
142
+ textBufferTexture,
143
+ maxCharCount,
144
+ );
145
+ }
146
+
147
+ /**
148
+ * Update the text buffer texture with glyph data.
149
+ */
150
+ updateTextBuffer(data: Float32Array, glyphCount: number): void {
151
+ const gl = this.#gl;
152
+ gl.bindTexture(gl.TEXTURE_2D, this.textBufferTexture);
153
+ gl.texSubImage2D(
154
+ gl.TEXTURE_2D,
155
+ 0,
156
+ 0,
157
+ 0,
158
+ glyphCount,
159
+ 1,
160
+ gl.RGBA,
161
+ gl.FLOAT,
162
+ data,
163
+ );
164
+ // Note: don't unbind here - the texture needs to stay bound for rendering
165
+ }
166
+
167
+ destroy(): void {
168
+ const gl = this.#gl;
169
+ gl.deleteTexture(this.fontTexture);
170
+ gl.deleteTexture(this.charDataTexture);
171
+ gl.deleteTexture(this.textBufferTexture);
172
+ }
173
+ }
@@ -0,0 +1,275 @@
1
+ import type { EngineUniform } from "../../coreTypes/EngineUniform";
2
+ import type { SceneNode } from "../../scene/SceneNode";
3
+ import { DEFAULT_FONT_SIZE, TextNode } from "../../scene/TextNode";
4
+ import type { MsdfFont } from "../../text/MsdfFont";
5
+ import {
6
+ findLargestFontSize,
7
+ measureText,
8
+ shapeText,
9
+ } from "../../text/shaping";
10
+ import { assert } from "../../utils/assert";
11
+ import type { ITextShader } from "../ITextShader";
12
+ import { fragmentShader, vertexShader } from "./glsl/text.glsl";
13
+ import type { WebGLBackend } from "./WebGLBackend";
14
+ import type { WebGLFontPipeline } from "./WebGLFontPipeline";
15
+
16
+ /**
17
+ * WebGL 2 text shader for MSDF font rendering.
18
+ *
19
+ * Unlike WebGPU which batches all text into storage buffers, WebGL renders
20
+ * each TextNode separately since WebGL2 doesn't support firstInstance.
21
+ */
22
+ export class WebGLTextShader implements ITextShader {
23
+ readonly label = "text";
24
+ readonly font: MsdfFont;
25
+ readonly maxCharCount: number;
26
+
27
+ #backend: WebGLBackend;
28
+ #pipeline: WebGLFontPipeline;
29
+ #program: WebGLProgram;
30
+ #vao: WebGLVertexArrayObject;
31
+ #cpuTextBuffer: Float32Array;
32
+ #cachedUniform: EngineUniform | null = null;
33
+
34
+ // Uniform locations
35
+ #uViewProjection: WebGLUniformLocation | null = null;
36
+ #uTextTransform: WebGLUniformLocation | null = null;
37
+ #uTextColor: WebGLUniformLocation | null = null;
38
+ #uFontSize: WebGLUniformLocation | null = null;
39
+ #uBlockWidth: WebGLUniformLocation | null = null;
40
+ #uBlockHeight: WebGLUniformLocation | null = null;
41
+ #uLineHeight: WebGLUniformLocation | null = null;
42
+ #uCharData: WebGLUniformLocation | null = null;
43
+ #uTextBuffer: WebGLUniformLocation | null = null;
44
+ #uFontTexture: WebGLUniformLocation | null = null;
45
+
46
+ constructor(backend: WebGLBackend, pipeline: WebGLFontPipeline) {
47
+ this.#backend = backend;
48
+ this.#pipeline = pipeline;
49
+ this.font = pipeline.font;
50
+ this.maxCharCount = pipeline.maxCharCount;
51
+
52
+ const gl = backend.gl;
53
+
54
+ // Compile shaders
55
+ const vs = this.#compileShader(gl, gl.VERTEX_SHADER, vertexShader);
56
+ const fs = this.#compileShader(gl, gl.FRAGMENT_SHADER, fragmentShader);
57
+
58
+ // Create program
59
+ const program = gl.createProgram();
60
+ assert(program, "Failed to create WebGL program");
61
+ gl.attachShader(program, vs);
62
+ gl.attachShader(program, fs);
63
+ gl.linkProgram(program);
64
+
65
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
66
+ const info = gl.getProgramInfoLog(program);
67
+ throw new Error(`Failed to link text shader program: ${info}`);
68
+ }
69
+
70
+ this.#program = program;
71
+
72
+ // Get uniform locations
73
+ this.#uViewProjection = gl.getUniformLocation(program, "u_viewProjection");
74
+ this.#uTextTransform = gl.getUniformLocation(program, "u_textTransform");
75
+ this.#uTextColor = gl.getUniformLocation(program, "u_textColor");
76
+ this.#uFontSize = gl.getUniformLocation(program, "u_fontSize");
77
+ this.#uBlockWidth = gl.getUniformLocation(program, "u_blockWidth");
78
+ this.#uBlockHeight = gl.getUniformLocation(program, "u_blockHeight");
79
+ this.#uLineHeight = gl.getUniformLocation(program, "u_lineHeight");
80
+ this.#uCharData = gl.getUniformLocation(program, "u_charData");
81
+ this.#uTextBuffer = gl.getUniformLocation(program, "u_textBuffer");
82
+ this.#uFontTexture = gl.getUniformLocation(program, "u_fontTexture");
83
+
84
+ // Create VAO (required for WebGL2 draw calls, even without vertex attributes)
85
+ const vao = gl.createVertexArray();
86
+ assert(vao, "Failed to create WebGL VAO");
87
+ this.#vao = vao;
88
+
89
+ // Allocate CPU buffer for text shaping
90
+ this.#cpuTextBuffer = new Float32Array(this.maxCharCount * 4);
91
+
92
+ // Cleanup shaders
93
+ gl.deleteShader(vs);
94
+ gl.deleteShader(fs);
95
+ }
96
+
97
+ startFrame(uniform: EngineUniform): void {
98
+ this.#cachedUniform = uniform;
99
+ }
100
+
101
+ processBatch(nodes: SceneNode[]): number {
102
+ if (nodes.length === 0) return 0;
103
+
104
+ const gl = this.#backend.gl;
105
+ const uniform = this.#cachedUniform;
106
+ if (!uniform) {
107
+ throw new Error("Tried to process batch but engine uniform is not set");
108
+ }
109
+
110
+ gl.useProgram(this.#program);
111
+ gl.bindVertexArray(this.#vao);
112
+
113
+ // Set view projection matrix (extract 9 floats from padded mat3)
114
+ if (this.#uViewProjection) {
115
+ const m = uniform.viewProjectionMatrix;
116
+ const mat3x3 = new Float32Array([
117
+ m[0],
118
+ m[1],
119
+ m[2], // column 0
120
+ m[4],
121
+ m[5],
122
+ m[6], // column 1
123
+ m[8],
124
+ m[9],
125
+ m[10], // column 2
126
+ ]);
127
+ gl.uniformMatrix3fv(this.#uViewProjection, false, mat3x3);
128
+ }
129
+
130
+ // Set line height uniform
131
+ if (this.#uLineHeight) {
132
+ gl.uniform1f(this.#uLineHeight, this.#pipeline.lineHeight);
133
+ }
134
+
135
+ // Bind textures
136
+ // Texture unit 0: font atlas
137
+ gl.activeTexture(gl.TEXTURE0);
138
+ gl.bindTexture(gl.TEXTURE_2D, this.#pipeline.fontTexture);
139
+ if (this.#uFontTexture) {
140
+ gl.uniform1i(this.#uFontTexture, 0);
141
+ }
142
+
143
+ // Texture unit 1: character data
144
+ gl.activeTexture(gl.TEXTURE1);
145
+ gl.bindTexture(gl.TEXTURE_2D, this.#pipeline.charDataTexture);
146
+ if (this.#uCharData) {
147
+ gl.uniform1i(this.#uCharData, 1);
148
+ }
149
+
150
+ // Texture unit 2: text buffer
151
+ gl.activeTexture(gl.TEXTURE2);
152
+ gl.bindTexture(gl.TEXTURE_2D, this.#pipeline.textBufferTexture);
153
+ if (this.#uTextBuffer) {
154
+ gl.uniform1i(this.#uTextBuffer, 2);
155
+ }
156
+
157
+ // Render each TextNode separately
158
+ for (const node of nodes) {
159
+ if (!(node instanceof TextNode)) {
160
+ console.error(node);
161
+ throw new Error(
162
+ `Tried to use WebGLTextShader on something that isn't a TextNode: ${node}`,
163
+ );
164
+ }
165
+
166
+ const text = node.text;
167
+ const formatting = node.formatting;
168
+ const measurements = measureText(this.font, text, formatting.wordWrap);
169
+
170
+ // Calculate font size
171
+ const size = node.size ?? measurements;
172
+ const fontSize = formatting.shrinkToFit
173
+ ? findLargestFontSize(this.font, text, size, formatting)
174
+ : formatting.fontSize;
175
+ const actualFontSize = fontSize || DEFAULT_FONT_SIZE;
176
+
177
+ // Shape text into buffer
178
+ shapeText(
179
+ this.font,
180
+ text,
181
+ size,
182
+ actualFontSize,
183
+ formatting,
184
+ this.#cpuTextBuffer,
185
+ 0,
186
+ );
187
+
188
+ // Upload glyph data to text buffer texture
189
+ this.#pipeline.updateTextBuffer(
190
+ this.#cpuTextBuffer,
191
+ measurements.printedCharCount,
192
+ );
193
+
194
+ // Set per-text uniforms
195
+ if (this.#uTextTransform) {
196
+ const m = node.matrix;
197
+ const mat3x3 = new Float32Array([
198
+ m[0],
199
+ m[1],
200
+ m[2], // column 0
201
+ m[4],
202
+ m[5],
203
+ m[6], // column 1
204
+ m[8],
205
+ m[9],
206
+ m[10], // column 2
207
+ ]);
208
+ gl.uniformMatrix3fv(this.#uTextTransform, false, mat3x3);
209
+ }
210
+
211
+ if (this.#uTextColor) {
212
+ const tint = node.tint;
213
+ gl.uniform4f(this.#uTextColor, tint.r, tint.g, tint.b, tint.a);
214
+ }
215
+
216
+ if (this.#uFontSize) {
217
+ gl.uniform1f(this.#uFontSize, actualFontSize);
218
+ }
219
+
220
+ if (this.#uBlockWidth) {
221
+ gl.uniform1f(
222
+ this.#uBlockWidth,
223
+ formatting.align === "center" ? 0 : measurements.width,
224
+ );
225
+ }
226
+
227
+ if (this.#uBlockHeight) {
228
+ gl.uniform1f(this.#uBlockHeight, measurements.height);
229
+ }
230
+
231
+ // Draw instanced: 4 vertices per glyph, one instance per character
232
+ gl.drawArraysInstanced(
233
+ gl.TRIANGLE_STRIP,
234
+ 0,
235
+ 4,
236
+ measurements.printedCharCount,
237
+ );
238
+ }
239
+
240
+ gl.bindVertexArray(null);
241
+ return nodes.length;
242
+ }
243
+
244
+ endFrame(): void {
245
+ // No cleanup needed
246
+ }
247
+
248
+ #compileShader(
249
+ gl: WebGL2RenderingContext,
250
+ type: number,
251
+ source: string,
252
+ ): WebGLShader {
253
+ const shader = gl.createShader(type);
254
+ assert(shader, "Failed to create WebGL shader");
255
+
256
+ gl.shaderSource(shader, source);
257
+ gl.compileShader(shader);
258
+
259
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
260
+ const info = gl.getShaderInfoLog(shader);
261
+ const typeStr = type === gl.VERTEX_SHADER ? "vertex" : "fragment";
262
+ gl.deleteShader(shader);
263
+ throw new Error(`Failed to compile ${typeStr} shader: ${info}`);
264
+ }
265
+
266
+ return shader;
267
+ }
268
+
269
+ destroy(): void {
270
+ const gl = this.#backend.gl;
271
+ gl.deleteProgram(this.#program);
272
+ gl.deleteVertexArray(this.#vao);
273
+ this.#pipeline.destroy();
274
+ }
275
+ }