@bloopjs/toodle 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"AssetManager.d.ts","sourceRoot":"","sources":["../../src/textures/AssetManager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AssetManager.d.ts","sourceRoot":"","sources":["../../src/textures/AssetManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAQ3D,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;AAEpD,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;;;;;;;;;;;OAWG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB,SAAM;IAwC5D,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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloopjs/toodle",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -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
+ }
@@ -1,35 +1,275 @@
1
1
  import type { EngineUniform } from "../../coreTypes/EngineUniform";
2
2
  import type { SceneNode } from "../../scene/SceneNode";
3
+ import { DEFAULT_FONT_SIZE, TextNode } from "../../scene/TextNode";
3
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";
4
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";
5
15
 
6
16
  /**
7
- * WebGL text shader that supports font loading for measurement but not rendering.
17
+ * WebGL 2 text shader for MSDF font rendering.
8
18
  *
9
- * This allows text measurement (via MsdfFont) on WebGL backend, but throws
10
- * an error if text rendering is attempted. For text rendering, use the WebGPU backend.
19
+ * Unlike WebGPU which batches all text into storage buffers, WebGL renders
20
+ * each TextNode separately since WebGL2 doesn't support firstInstance.
11
21
  */
12
22
  export class WebGLTextShader implements ITextShader {
13
23
  readonly label = "text";
14
24
  readonly font: MsdfFont;
15
25
  readonly maxCharCount: number;
16
26
 
17
- constructor(font: MsdfFont, maxCharCount: number) {
18
- this.font = font;
19
- this.maxCharCount = maxCharCount;
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);
20
95
  }
21
96
 
22
- startFrame(_uniform: EngineUniform): void {
23
- // No-op for measurement-only shader
97
+ startFrame(uniform: EngineUniform): void {
98
+ this.#cachedUniform = uniform;
24
99
  }
25
100
 
26
- processBatch(_nodes: SceneNode[]): number {
27
- throw new Error(
28
- "Text rendering is not supported in WebGL mode. Use WebGPU backend for text rendering.",
29
- );
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;
30
242
  }
31
243
 
32
244
  endFrame(): void {
33
- // No-op for measurement-only shader
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();
34
274
  }
35
275
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * GLSL ES 3.0 port of the WGSL text shader for MSDF font rendering.
3
+ *
4
+ * Key differences from WebGPU version:
5
+ * - Uses texelFetch() to read from data textures instead of storage buffers
6
+ * - Character metrics stored in RGBA32F texture (8 floats per char = 2 texels)
7
+ * - Per-glyph positions stored in RGBA32F texture (vec4: xy = pos, z = charIndex)
8
+ * - Each TextNode is rendered separately with uniforms (no firstInstance)
9
+ */
10
+
11
+ export const vertexShader = /*glsl*/ `#version 300 es
12
+ precision highp float;
13
+
14
+ // Engine uniforms
15
+ uniform mat3 u_viewProjection;
16
+
17
+ // Per-text-block uniforms
18
+ uniform mat3 u_textTransform;
19
+ uniform vec4 u_textColor;
20
+ uniform float u_fontSize;
21
+ uniform float u_blockWidth;
22
+ uniform float u_blockHeight;
23
+ uniform float u_lineHeight;
24
+
25
+ // Character data texture (RGBA32F, 2 texels per character)
26
+ // Texel 0: texOffset.xy, texExtent.xy
27
+ // Texel 1: size.xy, offset.xy
28
+ uniform sampler2D u_charData;
29
+
30
+ // Text buffer texture (RGBA32F, 1 texel per glyph)
31
+ // Each texel: xy = glyph position, z = char index
32
+ uniform sampler2D u_textBuffer;
33
+
34
+ // Outputs to fragment shader
35
+ out vec2 v_texcoord;
36
+
37
+ // Quad vertex positions for a character (matches WGSL)
38
+ const vec2 pos[4] = vec2[4](
39
+ vec2(0.0, -1.0),
40
+ vec2(1.0, -1.0),
41
+ vec2(0.0, 0.0),
42
+ vec2(1.0, 0.0)
43
+ );
44
+
45
+ void main() {
46
+ // gl_VertexID gives us 0-3 for the quad vertices
47
+ // gl_InstanceID gives us which glyph we're rendering
48
+ int vertexIndex = gl_VertexID;
49
+ int glyphIndex = gl_InstanceID;
50
+
51
+ // Fetch glyph data from text buffer texture
52
+ vec4 glyphData = texelFetch(u_textBuffer, ivec2(glyphIndex, 0), 0);
53
+ vec2 glyphPos = glyphData.xy;
54
+ int charIndex = int(glyphData.z);
55
+
56
+ // Fetch character metrics (2 texels per char)
57
+ // Texel 0: texOffset.x, texOffset.y, texExtent.x, texExtent.y
58
+ // Texel 1: size.x, size.y, offset.x, offset.y
59
+ vec4 charData0 = texelFetch(u_charData, ivec2(charIndex * 2, 0), 0);
60
+ vec4 charData1 = texelFetch(u_charData, ivec2(charIndex * 2 + 1, 0), 0);
61
+
62
+ vec2 texOffset = charData0.xy;
63
+ vec2 texExtent = charData0.zw;
64
+ vec2 charSize = charData1.xy;
65
+ vec2 charOffset = charData1.zw;
66
+
67
+ // Center text vertically; origin is mid-height
68
+ vec2 offset = vec2(0.0, -u_blockHeight / 2.0);
69
+
70
+ // Glyph position in ems (quad pos * size + per-char offset)
71
+ vec2 emPos = pos[vertexIndex] * charSize + charOffset + glyphPos - offset;
72
+ vec2 charPos = emPos * (u_fontSize / u_lineHeight);
73
+
74
+ // Transform position through model and view-projection matrices
75
+ vec3 worldPos = u_textTransform * vec3(charPos, 1.0);
76
+ vec3 clipPos = u_viewProjection * worldPos;
77
+
78
+ gl_Position = vec4(clipPos.xy, 0.0, 1.0);
79
+
80
+ // Calculate texture coordinates
81
+ v_texcoord = pos[vertexIndex] * vec2(1.0, -1.0);
82
+ v_texcoord *= texExtent;
83
+ v_texcoord += texOffset;
84
+ }
85
+ `;
86
+
87
+ export const fragmentShader = /*glsl*/ `#version 300 es
88
+ precision highp float;
89
+
90
+ // Font texture (MSDF atlas)
91
+ uniform sampler2D u_fontTexture;
92
+
93
+ // Text color
94
+ uniform vec4 u_textColor;
95
+
96
+ // Input from vertex shader
97
+ in vec2 v_texcoord;
98
+
99
+ // Output color
100
+ out vec4 fragColor;
101
+
102
+ // Signed distance function sampling for MSDF font rendering
103
+ // Median of three: max(min(r,g), min(max(r,g), b))
104
+ float sampleMsdf(vec2 texcoord) {
105
+ vec4 c = texture(u_fontTexture, texcoord);
106
+ return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
107
+ }
108
+
109
+ void main() {
110
+ // pxRange (AKA distanceRange) comes from the msdfgen tool
111
+ float pxRange = 4.0;
112
+ vec2 texSize = vec2(textureSize(u_fontTexture, 0));
113
+
114
+ // Anti-aliasing technique by Paul Houx
115
+ // https://github.com/Chlumsky/msdfgen/issues/22#issuecomment-234958005
116
+ float dx = texSize.x * length(vec2(dFdx(v_texcoord.x), dFdy(v_texcoord.x)));
117
+ float dy = texSize.y * length(vec2(dFdx(v_texcoord.y), dFdy(v_texcoord.y)));
118
+
119
+ float toPixels = pxRange * inversesqrt(dx * dx + dy * dy);
120
+ float sigDist = sampleMsdf(v_texcoord) - 0.5;
121
+ float pxDist = sigDist * toPixels;
122
+
123
+ float edgeWidth = 0.5;
124
+ float alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);
125
+
126
+ if (alpha < 0.001) {
127
+ discard;
128
+ }
129
+
130
+ fragColor = vec4(u_textColor.rgb, u_textColor.a * alpha);
131
+ }
132
+ `;
@@ -1,3 +1,4 @@
1
1
  export { WebGLBackend, type WebGLBackendOptions } from "./WebGLBackend";
2
+ export { WebGLFontPipeline } from "./WebGLFontPipeline";
2
3
  export { WebGLQuadShader } from "./WebGLQuadShader";
3
4
  export { WebGLTextShader } from "./WebGLTextShader";
@@ -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";
@@ -349,8 +351,14 @@ export class AssetManager {
349
351
  );
350
352
  this.#fonts.set(id, textShader);
351
353
  } else {
352
- // WebGL: font loaded for measurement, but rendering will throw
353
- const textShader = new WebGLTextShader(font, limits.maxTextLength);
354
+ // WebGL: create font pipeline and text shader for rendering
355
+ const webglBackend = this.#backend as WebGLBackend;
356
+ const fontPipeline = WebGLFontPipeline.create(
357
+ webglBackend.gl,
358
+ font,
359
+ limits.maxTextLength,
360
+ );
361
+ const textShader = new WebGLTextShader(webglBackend, fontPipeline);
354
362
  this.#fonts.set(id, textShader);
355
363
  }
356
364