@bloopjs/toodle 0.0.104 → 0.1.2
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/Toodle.d.ts +41 -19
- package/dist/Toodle.d.ts.map +1 -1
- package/dist/backends/IBackendShader.d.ts +48 -0
- package/dist/backends/IBackendShader.d.ts.map +1 -0
- package/dist/backends/IRenderBackend.d.ts +92 -0
- package/dist/backends/IRenderBackend.d.ts.map +1 -0
- package/dist/backends/ITextureAtlas.d.ts +34 -0
- package/dist/backends/ITextureAtlas.d.ts.map +1 -0
- package/dist/backends/detection.d.ts +16 -0
- package/dist/backends/detection.d.ts.map +1 -0
- package/dist/backends/mod.d.ts +9 -0
- package/dist/backends/mod.d.ts.map +1 -0
- package/dist/backends/webgl2/WebGLBackend.d.ts +51 -0
- package/dist/backends/webgl2/WebGLBackend.d.ts.map +1 -0
- package/dist/backends/webgl2/WebGLQuadShader.d.ts +17 -0
- package/dist/backends/webgl2/WebGLQuadShader.d.ts.map +1 -0
- package/dist/backends/webgl2/glsl/quad.glsl.d.ts +12 -0
- package/dist/backends/webgl2/glsl/quad.glsl.d.ts.map +1 -0
- package/dist/backends/webgl2/mod.d.ts +3 -0
- package/dist/backends/webgl2/mod.d.ts.map +1 -0
- package/dist/backends/webgpu/ShaderDescriptor.d.ts.map +1 -0
- package/dist/{textures → backends/webgpu}/TextureComputeShader.d.ts +1 -1
- package/dist/backends/webgpu/TextureComputeShader.d.ts.map +1 -0
- package/dist/backends/webgpu/WebGPUBackend.d.ts +67 -0
- package/dist/backends/webgpu/WebGPUBackend.d.ts.map +1 -0
- package/dist/backends/webgpu/WebGPUQuadShader.d.ts +18 -0
- package/dist/backends/webgpu/WebGPUQuadShader.d.ts.map +1 -0
- package/dist/backends/webgpu/mod.d.ts +3 -0
- package/dist/backends/webgpu/mod.d.ts.map +1 -0
- package/dist/backends/webgpu/parser.d.ts.map +1 -0
- package/dist/{shaders → backends/webgpu}/postprocess/blur.d.ts +1 -1
- package/dist/backends/webgpu/postprocess/blur.d.ts.map +1 -0
- package/dist/{shaders → backends/webgpu}/postprocess/mod.d.ts +1 -1
- package/dist/backends/webgpu/postprocess/mod.d.ts.map +1 -0
- package/dist/backends/webgpu/samplers.d.ts.map +1 -0
- package/dist/backends/webgpu/wgsl/example.wgsl.d.ts.map +1 -0
- package/dist/backends/webgpu/wgsl/hello.wgsl.d.ts.map +1 -0
- package/dist/backends/webgpu/wgsl/helloInstanced.wgsl.d.ts.map +1 -0
- package/dist/backends/webgpu/wgsl/pixel-scraping.wgsl.d.ts.map +1 -0
- package/dist/backends/webgpu/wgsl/quad.wgsl.d.ts.map +1 -0
- package/dist/coreTypes/EngineUniform.d.ts.map +1 -0
- package/dist/mod.d.ts +3 -2
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +7247 -6663
- package/dist/mod.js.map +27 -22
- package/dist/scene/Batcher.d.ts +2 -2
- package/dist/scene/Batcher.d.ts.map +1 -1
- package/dist/scene/QuadNode.d.ts +3 -2
- package/dist/scene/QuadNode.d.ts.map +1 -1
- package/dist/scene/RenderComponent.d.ts +2 -2
- package/dist/scene/RenderComponent.d.ts.map +1 -1
- package/dist/text/TextShader.d.ts +8 -6
- package/dist/text/TextShader.d.ts.map +1 -1
- package/dist/textures/AssetManager.d.ts +21 -5
- package/dist/textures/AssetManager.d.ts.map +1 -1
- package/dist/textures/util.d.ts +0 -4
- package/dist/textures/util.d.ts.map +1 -1
- package/dist/utils/boilerplate.d.ts +1 -1
- package/dist/utils/boilerplate.d.ts.map +1 -1
- package/package.json +1 -2
- package/src/Toodle.ts +124 -156
- package/src/backends/IBackendShader.ts +52 -0
- package/src/backends/IRenderBackend.ts +118 -0
- package/src/backends/ITextureAtlas.ts +35 -0
- package/src/backends/detection.ts +46 -0
- package/src/backends/mod.ts +29 -0
- package/src/backends/webgl2/WebGLBackend.ts +256 -0
- package/src/backends/webgl2/WebGLQuadShader.ts +278 -0
- package/src/backends/webgl2/glsl/quad.glsl.ts +114 -0
- package/src/backends/webgl2/mod.ts +2 -0
- package/src/{textures → backends/webgpu}/TextureComputeShader.ts +2 -48
- package/src/backends/webgpu/WebGPUBackend.ts +350 -0
- package/src/{shaders/QuadShader.ts → backends/webgpu/WebGPUQuadShader.ts} +226 -170
- package/src/backends/webgpu/mod.ts +2 -0
- package/src/{shaders → backends/webgpu}/parser.ts +2 -2
- package/src/{shaders → backends/webgpu}/postprocess/blur.ts +2 -2
- package/src/{shaders → backends/webgpu}/postprocess/mod.ts +1 -1
- package/src/mod.ts +3 -2
- package/src/scene/Batcher.ts +3 -3
- package/src/scene/QuadNode.ts +6 -2
- package/src/scene/RenderComponent.ts +2 -2
- package/src/text/TextShader.ts +17 -11
- package/src/textures/AssetManager.ts +117 -93
- package/src/textures/util.ts +0 -92
- package/src/utils/boilerplate.ts +1 -1
- package/dist/shaders/EngineUniform.d.ts.map +0 -1
- package/dist/shaders/IShader.d.ts +0 -15
- package/dist/shaders/IShader.d.ts.map +0 -1
- package/dist/shaders/QuadShader.d.ts +0 -18
- package/dist/shaders/QuadShader.d.ts.map +0 -1
- package/dist/shaders/ShaderDescriptor.d.ts.map +0 -1
- package/dist/shaders/mod.d.ts +0 -6
- package/dist/shaders/mod.d.ts.map +0 -1
- package/dist/shaders/parser.d.ts.map +0 -1
- package/dist/shaders/postprocess/blur.d.ts.map +0 -1
- package/dist/shaders/postprocess/mod.d.ts.map +0 -1
- package/dist/shaders/samplers.d.ts.map +0 -1
- package/dist/shaders/wgsl/example.wgsl.d.ts.map +0 -1
- package/dist/shaders/wgsl/hello.wgsl.d.ts.map +0 -1
- package/dist/shaders/wgsl/helloInstanced.wgsl.d.ts.map +0 -1
- package/dist/shaders/wgsl/quad.wgsl.d.ts.map +0 -1
- package/dist/textures/TextureComputeShader.d.ts.map +0 -1
- package/dist/textures/pixel-scraping.wgsl.d.ts.map +0 -1
- package/src/shaders/IShader.ts +0 -20
- package/src/shaders/mod.ts +0 -5
- /package/dist/{shaders → backends/webgpu}/ShaderDescriptor.d.ts +0 -0
- /package/dist/{shaders → backends/webgpu}/parser.d.ts +0 -0
- /package/dist/{shaders → backends/webgpu}/samplers.d.ts +0 -0
- /package/dist/{shaders → backends/webgpu}/wgsl/example.wgsl.d.ts +0 -0
- /package/dist/{shaders → backends/webgpu}/wgsl/hello.wgsl.d.ts +0 -0
- /package/dist/{shaders → backends/webgpu}/wgsl/helloInstanced.wgsl.d.ts +0 -0
- /package/dist/{textures → backends/webgpu/wgsl}/pixel-scraping.wgsl.d.ts +0 -0
- /package/dist/{shaders → backends/webgpu}/wgsl/quad.wgsl.d.ts +0 -0
- /package/dist/{shaders → coreTypes}/EngineUniform.d.ts +0 -0
- /package/src/{shaders → backends/webgpu}/ShaderDescriptor.ts +0 -0
- /package/src/{shaders → backends/webgpu}/samplers.ts +0 -0
- /package/src/{shaders → backends/webgpu}/wgsl/example.wgsl.ts +0 -0
- /package/src/{shaders → backends/webgpu}/wgsl/hello.wgsl.ts +0 -0
- /package/src/{shaders → backends/webgpu}/wgsl/helloInstanced.wgsl.ts +0 -0
- /package/src/{textures → backends/webgpu/wgsl}/pixel-scraping.wgsl.ts +0 -0
- /package/src/{shaders → backends/webgpu}/wgsl/quad.wgsl.ts +0 -0
- /package/src/{shaders → coreTypes}/EngineUniform.ts +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { BackendType } from "./IRenderBackend";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect the best available rendering backend.
|
|
5
|
+
*
|
|
6
|
+
* Tries WebGPU first (better performance), falls back to WebGL 2.
|
|
7
|
+
*/
|
|
8
|
+
export async function detectBackend(): Promise<BackendType> {
|
|
9
|
+
// Check for WebGPU support
|
|
10
|
+
if (typeof navigator !== "undefined" && "gpu" in navigator) {
|
|
11
|
+
try {
|
|
12
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
13
|
+
if (adapter) {
|
|
14
|
+
return "webgpu";
|
|
15
|
+
}
|
|
16
|
+
} catch {
|
|
17
|
+
// WebGPU initialization failed, fall back to WebGL
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return "webgl2";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if WebGPU is available in the current environment.
|
|
26
|
+
*/
|
|
27
|
+
export function isWebGPUAvailable(): boolean {
|
|
28
|
+
return typeof navigator !== "undefined" && "gpu" in navigator;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if WebGL 2 is available in the current environment.
|
|
33
|
+
*/
|
|
34
|
+
export function isWebGL2Available(): boolean {
|
|
35
|
+
if (typeof document === "undefined") {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const canvas = document.createElement("canvas");
|
|
41
|
+
const gl = canvas.getContext("webgl2");
|
|
42
|
+
return gl !== null;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Backend abstraction layer
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
detectBackend,
|
|
5
|
+
isWebGL2Available,
|
|
6
|
+
isWebGPUAvailable,
|
|
7
|
+
} from "./detection";
|
|
8
|
+
export type { IBackendShader, QuadShaderCreationOpts } from "./IBackendShader";
|
|
9
|
+
export type {
|
|
10
|
+
BackendType,
|
|
11
|
+
BlendFactor,
|
|
12
|
+
BlendMode,
|
|
13
|
+
BlendOperation,
|
|
14
|
+
IRenderBackend,
|
|
15
|
+
} from "./IRenderBackend";
|
|
16
|
+
export type {
|
|
17
|
+
ITextureAtlas,
|
|
18
|
+
TextureAtlasFormat,
|
|
19
|
+
TextureAtlasOptions,
|
|
20
|
+
} from "./ITextureAtlas";
|
|
21
|
+
export { defaultFragmentShader as defaultGLSLFragmentShader } from "./webgl2/glsl/quad.glsl";
|
|
22
|
+
export { WebGLBackend } from "./webgl2/WebGLBackend";
|
|
23
|
+
// WebGPU-specific postprocess utilities
|
|
24
|
+
export {
|
|
25
|
+
type PostProcess,
|
|
26
|
+
PostProcessDefaults,
|
|
27
|
+
} from "./webgpu/postprocess/mod";
|
|
28
|
+
// Concrete backend implementations
|
|
29
|
+
export { WebGPUBackend } from "./webgpu/WebGPUBackend";
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import type { Color } from "../../coreTypes/Color";
|
|
2
|
+
import type { EngineUniform } from "../../coreTypes/EngineUniform";
|
|
3
|
+
import type { Size } from "../../coreTypes/Size";
|
|
4
|
+
import type { Limits, LimitsOptions } from "../../limits";
|
|
5
|
+
import { DEFAULT_LIMITS } from "../../limits";
|
|
6
|
+
import type { CpuTextureAtlas } from "../../textures/types";
|
|
7
|
+
import { assert } from "../../utils/assert";
|
|
8
|
+
import type { IBackendShader, QuadShaderCreationOpts } from "../IBackendShader";
|
|
9
|
+
import type { IRenderBackend } from "../IRenderBackend";
|
|
10
|
+
import type {
|
|
11
|
+
ITextureAtlas,
|
|
12
|
+
TextureAtlasFormat,
|
|
13
|
+
TextureAtlasOptions,
|
|
14
|
+
} from "../ITextureAtlas";
|
|
15
|
+
import { WebGLQuadShader } from "./WebGLQuadShader";
|
|
16
|
+
|
|
17
|
+
export type WebGLBackendOptions = {
|
|
18
|
+
limits?: LimitsOptions;
|
|
19
|
+
format?: "rgba8unorm" | "rg8unorm";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* WebGL 2 implementation of the render backend.
|
|
24
|
+
*/
|
|
25
|
+
export class WebGLBackend implements IRenderBackend {
|
|
26
|
+
readonly type = "webgl2" as const;
|
|
27
|
+
readonly limits: Limits;
|
|
28
|
+
readonly atlasSize: Size;
|
|
29
|
+
readonly defaultAtlasId = "default";
|
|
30
|
+
|
|
31
|
+
#atlases = new Map<string, ITextureAtlas>();
|
|
32
|
+
#gl: WebGL2RenderingContext;
|
|
33
|
+
#canvas: HTMLCanvasElement;
|
|
34
|
+
|
|
35
|
+
private constructor(
|
|
36
|
+
gl: WebGL2RenderingContext,
|
|
37
|
+
canvas: HTMLCanvasElement,
|
|
38
|
+
limits: Limits,
|
|
39
|
+
) {
|
|
40
|
+
this.#gl = gl;
|
|
41
|
+
this.#canvas = canvas;
|
|
42
|
+
this.limits = limits;
|
|
43
|
+
this.atlasSize = {
|
|
44
|
+
width: limits.textureSize,
|
|
45
|
+
height: limits.textureSize,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a WebGL 2 backend attached to a canvas.
|
|
51
|
+
*/
|
|
52
|
+
static async create(
|
|
53
|
+
canvas: HTMLCanvasElement,
|
|
54
|
+
options: WebGLBackendOptions = {},
|
|
55
|
+
): Promise<WebGLBackend> {
|
|
56
|
+
const gl = canvas.getContext("webgl2", {
|
|
57
|
+
alpha: true,
|
|
58
|
+
antialias: false,
|
|
59
|
+
premultipliedAlpha: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!gl) {
|
|
63
|
+
throw new Error("WebGL 2 not supported");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const limits: Limits = {
|
|
67
|
+
...DEFAULT_LIMITS,
|
|
68
|
+
...options.limits,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const backend = new WebGLBackend(gl, canvas, limits);
|
|
72
|
+
|
|
73
|
+
// Create the default texture atlas
|
|
74
|
+
backend.createTextureAtlas("default", {
|
|
75
|
+
format: options.format ?? "rgba8unorm",
|
|
76
|
+
layers: limits.textureArrayLayers,
|
|
77
|
+
size: limits.textureSize,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return backend;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
startFrame(clearColor: Color, loadOp: "clear" | "load"): void {
|
|
84
|
+
const gl = this.#gl;
|
|
85
|
+
|
|
86
|
+
// Set viewport to canvas size
|
|
87
|
+
gl.viewport(0, 0, this.#canvas.width, this.#canvas.height);
|
|
88
|
+
|
|
89
|
+
if (loadOp === "clear") {
|
|
90
|
+
gl.clearColor(clearColor.r, clearColor.g, clearColor.b, clearColor.a);
|
|
91
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Enable blending
|
|
95
|
+
gl.enable(gl.BLEND);
|
|
96
|
+
gl.blendFuncSeparate(
|
|
97
|
+
gl.SRC_ALPHA,
|
|
98
|
+
gl.ONE_MINUS_SRC_ALPHA,
|
|
99
|
+
gl.ONE,
|
|
100
|
+
gl.ONE_MINUS_SRC_ALPHA,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
endFrame(): void {
|
|
105
|
+
const gl = this.#gl;
|
|
106
|
+
gl.flush();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
updateEngineUniform(_uniform: EngineUniform): void {
|
|
110
|
+
// Uniforms are updated per-shader in WebGL, not at the backend level
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async uploadAtlas(
|
|
114
|
+
atlas: CpuTextureAtlas,
|
|
115
|
+
layerIndex: number,
|
|
116
|
+
atlasId?: string,
|
|
117
|
+
): Promise<void> {
|
|
118
|
+
const gl = this.#gl;
|
|
119
|
+
const targetAtlas = this.getTextureAtlas(atlasId ?? "default");
|
|
120
|
+
assert(targetAtlas, `Atlas "${atlasId ?? "default"}" not found`);
|
|
121
|
+
const texture = targetAtlas.handle as WebGLTexture;
|
|
122
|
+
|
|
123
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
|
|
124
|
+
|
|
125
|
+
if (atlas.rg8Bytes) {
|
|
126
|
+
// Upload raw bytes for RG8 format
|
|
127
|
+
gl.texSubImage3D(
|
|
128
|
+
gl.TEXTURE_2D_ARRAY,
|
|
129
|
+
0, // mip level
|
|
130
|
+
0,
|
|
131
|
+
0,
|
|
132
|
+
layerIndex, // x, y, z offset
|
|
133
|
+
targetAtlas.size,
|
|
134
|
+
targetAtlas.size,
|
|
135
|
+
1, // width, height, depth
|
|
136
|
+
gl.RG,
|
|
137
|
+
gl.UNSIGNED_BYTE,
|
|
138
|
+
atlas.rg8Bytes,
|
|
139
|
+
);
|
|
140
|
+
} else {
|
|
141
|
+
// Upload ImageBitmap for RGBA format
|
|
142
|
+
gl.texSubImage3D(
|
|
143
|
+
gl.TEXTURE_2D_ARRAY,
|
|
144
|
+
0,
|
|
145
|
+
0,
|
|
146
|
+
0,
|
|
147
|
+
layerIndex,
|
|
148
|
+
atlas.texture.width,
|
|
149
|
+
atlas.texture.height,
|
|
150
|
+
1,
|
|
151
|
+
gl.RGBA,
|
|
152
|
+
gl.UNSIGNED_BYTE,
|
|
153
|
+
atlas.texture,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
createQuadShader(opts: QuadShaderCreationOpts): IBackendShader {
|
|
161
|
+
return new WebGLQuadShader(
|
|
162
|
+
opts.label,
|
|
163
|
+
this,
|
|
164
|
+
opts.instanceCount,
|
|
165
|
+
opts.userCode,
|
|
166
|
+
opts.atlasId,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
createTextureAtlas(id: string, options?: TextureAtlasOptions): ITextureAtlas {
|
|
171
|
+
if (this.#atlases.has(id)) {
|
|
172
|
+
throw new Error(`Atlas "${id}" already exists`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const gl = this.#gl;
|
|
176
|
+
const format: TextureAtlasFormat = options?.format ?? "rgba8unorm";
|
|
177
|
+
const layers = options?.layers ?? this.limits.textureArrayLayers;
|
|
178
|
+
const size = options?.size ?? this.limits.textureSize;
|
|
179
|
+
|
|
180
|
+
const texture = gl.createTexture();
|
|
181
|
+
assert(texture, "Failed to create WebGL texture");
|
|
182
|
+
|
|
183
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
|
|
184
|
+
|
|
185
|
+
// Configure texture parameters
|
|
186
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
187
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
188
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
189
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
190
|
+
|
|
191
|
+
// Allocate storage for texture array
|
|
192
|
+
const internalFormat = format === "rg8unorm" ? gl.RG8 : gl.RGBA8;
|
|
193
|
+
|
|
194
|
+
gl.texStorage3D(
|
|
195
|
+
gl.TEXTURE_2D_ARRAY,
|
|
196
|
+
1, // mip levels
|
|
197
|
+
internalFormat,
|
|
198
|
+
size,
|
|
199
|
+
size,
|
|
200
|
+
layers,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
|
|
204
|
+
|
|
205
|
+
const atlas: ITextureAtlas = { id, format, layers, size, handle: texture };
|
|
206
|
+
this.#atlases.set(id, atlas);
|
|
207
|
+
return atlas;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
getTextureAtlas(id: string): ITextureAtlas | null {
|
|
211
|
+
return this.#atlases.get(id) ?? null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
destroyTextureAtlas(id: string): void {
|
|
215
|
+
const atlas = this.#atlases.get(id);
|
|
216
|
+
if (atlas) {
|
|
217
|
+
this.#gl.deleteTexture(atlas.handle as WebGLTexture);
|
|
218
|
+
this.#atlases.delete(id);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get the default texture atlas handle.
|
|
224
|
+
* @deprecated Use getTextureAtlas("default").handle instead
|
|
225
|
+
*/
|
|
226
|
+
get textureArrayHandle(): WebGLTexture {
|
|
227
|
+
return this.getTextureAtlas("default")!.handle as WebGLTexture;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
resize(_width: number, _height: number): void {
|
|
231
|
+
// Canvas resize is handled by the application
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
destroy(): void {
|
|
235
|
+
const gl = this.#gl;
|
|
236
|
+
// Destroy all atlases
|
|
237
|
+
for (const atlas of this.#atlases.values()) {
|
|
238
|
+
gl.deleteTexture(atlas.handle as WebGLTexture);
|
|
239
|
+
}
|
|
240
|
+
this.#atlases.clear();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get the WebGL 2 rendering context.
|
|
245
|
+
*/
|
|
246
|
+
get gl(): WebGL2RenderingContext {
|
|
247
|
+
return this.#gl;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get the presentation format (of the default atlas).
|
|
252
|
+
*/
|
|
253
|
+
get presentationFormat(): TextureAtlasFormat {
|
|
254
|
+
return this.getTextureAtlas("default")?.format ?? "rgba8unorm";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import type { EngineUniform } from "../../coreTypes/EngineUniform";
|
|
2
|
+
import type { SceneNode } from "../../scene/SceneNode";
|
|
3
|
+
import { assert } from "../../utils/assert";
|
|
4
|
+
import type { IBackendShader } from "../IBackendShader";
|
|
5
|
+
import type { ITextureAtlas } from "../ITextureAtlas";
|
|
6
|
+
import { fragmentShader, vertexShader } from "./glsl/quad.glsl";
|
|
7
|
+
import type { WebGLBackend } from "./WebGLBackend";
|
|
8
|
+
|
|
9
|
+
// Instance data size in floats (must match WGSL shader)
|
|
10
|
+
// model (12) + tint (4) + uvOffsetAndScale (4) + cropOffsetAndScale (4) + atlasIndex (1) + padding (3) = 28
|
|
11
|
+
const INSTANCE_FLOATS = 28;
|
|
12
|
+
const INSTANCE_BYTES = INSTANCE_FLOATS * Float32Array.BYTES_PER_ELEMENT;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* WebGL 2 implementation of quad shader for instanced rendering.
|
|
16
|
+
*/
|
|
17
|
+
export class WebGLQuadShader implements IBackendShader {
|
|
18
|
+
readonly label: string;
|
|
19
|
+
|
|
20
|
+
#backend: WebGLBackend;
|
|
21
|
+
#atlas: ITextureAtlas;
|
|
22
|
+
#program: WebGLProgram;
|
|
23
|
+
#vao: WebGLVertexArrayObject;
|
|
24
|
+
#instanceBuffer: WebGLBuffer;
|
|
25
|
+
#cpuBuffer: Float32Array;
|
|
26
|
+
#instanceCount: number;
|
|
27
|
+
#instanceIndex = 0;
|
|
28
|
+
|
|
29
|
+
// Uniform locations
|
|
30
|
+
#uViewProjection: WebGLUniformLocation | null = null;
|
|
31
|
+
#uResolution: WebGLUniformLocation | null = null;
|
|
32
|
+
#uTextureArray: WebGLUniformLocation | null = null;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
label: string,
|
|
36
|
+
backend: WebGLBackend,
|
|
37
|
+
instanceCount: number,
|
|
38
|
+
userFragmentShader?: string,
|
|
39
|
+
atlasId?: string,
|
|
40
|
+
) {
|
|
41
|
+
const atlas = backend.getTextureAtlas(atlasId ?? "default");
|
|
42
|
+
if (!atlas) {
|
|
43
|
+
throw new Error(`Atlas "${atlasId ?? "default"}" not found`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.#atlas = atlas;
|
|
47
|
+
this.label = label;
|
|
48
|
+
this.#backend = backend;
|
|
49
|
+
this.#instanceCount = instanceCount;
|
|
50
|
+
|
|
51
|
+
const gl = backend.gl;
|
|
52
|
+
|
|
53
|
+
// Compile shaders
|
|
54
|
+
const vs = this.#compileShader(gl, gl.VERTEX_SHADER, vertexShader);
|
|
55
|
+
const fs = this.#compileShader(
|
|
56
|
+
gl,
|
|
57
|
+
gl.FRAGMENT_SHADER,
|
|
58
|
+
userFragmentShader ?? fragmentShader,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Create program
|
|
62
|
+
const program = gl.createProgram();
|
|
63
|
+
assert(program, "Failed to create WebGL program");
|
|
64
|
+
gl.attachShader(program, vs);
|
|
65
|
+
gl.attachShader(program, fs);
|
|
66
|
+
gl.linkProgram(program);
|
|
67
|
+
|
|
68
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
69
|
+
const info = gl.getProgramInfoLog(program);
|
|
70
|
+
throw new Error(`Failed to link shader program: ${info}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.#program = program;
|
|
74
|
+
|
|
75
|
+
// Get uniform locations
|
|
76
|
+
this.#uViewProjection = gl.getUniformLocation(program, "u_viewProjection");
|
|
77
|
+
this.#uResolution = gl.getUniformLocation(program, "u_resolution");
|
|
78
|
+
this.#uTextureArray = gl.getUniformLocation(program, "u_textureArray");
|
|
79
|
+
|
|
80
|
+
// Create VAO
|
|
81
|
+
const vao = gl.createVertexArray();
|
|
82
|
+
assert(vao, "Failed to create WebGL VAO");
|
|
83
|
+
this.#vao = vao;
|
|
84
|
+
|
|
85
|
+
// Create instance buffer
|
|
86
|
+
const instanceBuffer = gl.createBuffer();
|
|
87
|
+
assert(instanceBuffer, "Failed to create WebGL instance buffer");
|
|
88
|
+
this.#instanceBuffer = instanceBuffer;
|
|
89
|
+
|
|
90
|
+
// Allocate CPU buffer
|
|
91
|
+
this.#cpuBuffer = new Float32Array(instanceCount * INSTANCE_FLOATS);
|
|
92
|
+
|
|
93
|
+
// Set up VAO
|
|
94
|
+
gl.bindVertexArray(vao);
|
|
95
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
|
|
96
|
+
gl.bufferData(
|
|
97
|
+
gl.ARRAY_BUFFER,
|
|
98
|
+
instanceCount * INSTANCE_BYTES,
|
|
99
|
+
gl.DYNAMIC_DRAW,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Set up instance attributes
|
|
103
|
+
// Each vec4 attribute takes up 16 bytes
|
|
104
|
+
// model0 at location 0
|
|
105
|
+
gl.enableVertexAttribArray(0);
|
|
106
|
+
gl.vertexAttribPointer(0, 4, gl.FLOAT, false, INSTANCE_BYTES, 0);
|
|
107
|
+
gl.vertexAttribDivisor(0, 1);
|
|
108
|
+
|
|
109
|
+
// model1 at location 1
|
|
110
|
+
gl.enableVertexAttribArray(1);
|
|
111
|
+
gl.vertexAttribPointer(1, 4, gl.FLOAT, false, INSTANCE_BYTES, 16);
|
|
112
|
+
gl.vertexAttribDivisor(1, 1);
|
|
113
|
+
|
|
114
|
+
// model2 at location 2
|
|
115
|
+
gl.enableVertexAttribArray(2);
|
|
116
|
+
gl.vertexAttribPointer(2, 4, gl.FLOAT, false, INSTANCE_BYTES, 32);
|
|
117
|
+
gl.vertexAttribDivisor(2, 1);
|
|
118
|
+
|
|
119
|
+
// tint at location 3
|
|
120
|
+
gl.enableVertexAttribArray(3);
|
|
121
|
+
gl.vertexAttribPointer(3, 4, gl.FLOAT, false, INSTANCE_BYTES, 48);
|
|
122
|
+
gl.vertexAttribDivisor(3, 1);
|
|
123
|
+
|
|
124
|
+
// uvOffsetAndScale at location 4
|
|
125
|
+
gl.enableVertexAttribArray(4);
|
|
126
|
+
gl.vertexAttribPointer(4, 4, gl.FLOAT, false, INSTANCE_BYTES, 64);
|
|
127
|
+
gl.vertexAttribDivisor(4, 1);
|
|
128
|
+
|
|
129
|
+
// cropOffsetAndScale at location 5
|
|
130
|
+
gl.enableVertexAttribArray(5);
|
|
131
|
+
gl.vertexAttribPointer(5, 4, gl.FLOAT, false, INSTANCE_BYTES, 80);
|
|
132
|
+
gl.vertexAttribDivisor(5, 1);
|
|
133
|
+
|
|
134
|
+
// atlasIndex at location 6 (integer attribute - use vertexAttribIPointer)
|
|
135
|
+
gl.enableVertexAttribArray(6);
|
|
136
|
+
gl.vertexAttribIPointer(6, 1, gl.UNSIGNED_INT, INSTANCE_BYTES, 96);
|
|
137
|
+
gl.vertexAttribDivisor(6, 1);
|
|
138
|
+
|
|
139
|
+
gl.bindVertexArray(null);
|
|
140
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
141
|
+
|
|
142
|
+
// Cleanup shaders (they're linked to the program now)
|
|
143
|
+
gl.deleteShader(vs);
|
|
144
|
+
gl.deleteShader(fs);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#cachedUniform: EngineUniform | null = null;
|
|
148
|
+
|
|
149
|
+
startFrame(uniform: EngineUniform): void {
|
|
150
|
+
this.#instanceIndex = 0;
|
|
151
|
+
// Cache uniform for use in processBatch - WebGL state is global,
|
|
152
|
+
// so we need to set program/uniforms/textures right before drawing
|
|
153
|
+
this.#cachedUniform = uniform;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#bindState(): void {
|
|
157
|
+
const gl = this.#backend.gl;
|
|
158
|
+
const uniform = this.#cachedUniform;
|
|
159
|
+
if (!uniform)
|
|
160
|
+
throw new Error("Tried to bind state but engine uniform is not set");
|
|
161
|
+
|
|
162
|
+
gl.useProgram(this.#program);
|
|
163
|
+
|
|
164
|
+
// Set uniforms
|
|
165
|
+
if (this.#uViewProjection) {
|
|
166
|
+
// wgpu-matrix mat3 is stored as 12 floats (3 columns × 4 floats with padding)
|
|
167
|
+
// WebGL uniformMatrix3fv expects 9 floats, so extract the relevant values
|
|
168
|
+
const m = uniform.viewProjectionMatrix;
|
|
169
|
+
const mat3x3 = new Float32Array([
|
|
170
|
+
m[0],
|
|
171
|
+
m[1],
|
|
172
|
+
m[2], // column 0
|
|
173
|
+
m[4],
|
|
174
|
+
m[5],
|
|
175
|
+
m[6], // column 1
|
|
176
|
+
m[8],
|
|
177
|
+
m[9],
|
|
178
|
+
m[10], // column 2
|
|
179
|
+
]);
|
|
180
|
+
gl.uniformMatrix3fv(this.#uViewProjection, false, mat3x3);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (this.#uResolution) {
|
|
184
|
+
gl.uniform2f(
|
|
185
|
+
this.#uResolution,
|
|
186
|
+
uniform.resolution.width,
|
|
187
|
+
uniform.resolution.height,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Bind texture array to texture unit 0
|
|
192
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
193
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.#atlas.handle as WebGLTexture);
|
|
194
|
+
if (this.#uTextureArray) {
|
|
195
|
+
gl.uniform1i(this.#uTextureArray, 0);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
processBatch(nodes: SceneNode[]): number {
|
|
200
|
+
const gl = this.#backend.gl;
|
|
201
|
+
|
|
202
|
+
// Bind program, uniforms, and textures right before drawing
|
|
203
|
+
// (WebGL state is global, so this must happen per-batch)
|
|
204
|
+
this.#bindState();
|
|
205
|
+
|
|
206
|
+
if (nodes.length > this.#instanceCount) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`ToodleInstanceCap: ${nodes.length} instances enqueued, max is ${this.#instanceCount} for ${this.label} shader`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let instanceCount = 0;
|
|
213
|
+
|
|
214
|
+
// WebGL2 doesn't support firstInstance in drawArraysInstanced,
|
|
215
|
+
// so we always write starting at offset 0 for each batch
|
|
216
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
217
|
+
const instance = nodes[i];
|
|
218
|
+
assert(instance.renderComponent, "instance has no render component");
|
|
219
|
+
const floatOffset = instanceCount * INSTANCE_FLOATS;
|
|
220
|
+
|
|
221
|
+
instanceCount += instance.renderComponent.writeInstance(
|
|
222
|
+
instance,
|
|
223
|
+
this.#cpuBuffer,
|
|
224
|
+
floatOffset,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Upload instance data to GPU starting at offset 0
|
|
229
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.#instanceBuffer);
|
|
230
|
+
|
|
231
|
+
gl.bufferSubData(
|
|
232
|
+
gl.ARRAY_BUFFER,
|
|
233
|
+
0,
|
|
234
|
+
this.#cpuBuffer,
|
|
235
|
+
0,
|
|
236
|
+
instanceCount * INSTANCE_FLOATS,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// Draw instances from offset 0
|
|
240
|
+
gl.bindVertexArray(this.#vao);
|
|
241
|
+
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instanceCount);
|
|
242
|
+
gl.bindVertexArray(null);
|
|
243
|
+
|
|
244
|
+
return 1;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
endFrame(): void {
|
|
248
|
+
// Nothing to do
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#compileShader(
|
|
252
|
+
gl: WebGL2RenderingContext,
|
|
253
|
+
type: number,
|
|
254
|
+
source: string,
|
|
255
|
+
): WebGLShader {
|
|
256
|
+
const shader = gl.createShader(type);
|
|
257
|
+
assert(shader, "Failed to create WebGL shader");
|
|
258
|
+
|
|
259
|
+
gl.shaderSource(shader, source);
|
|
260
|
+
gl.compileShader(shader);
|
|
261
|
+
|
|
262
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
263
|
+
const info = gl.getShaderInfoLog(shader);
|
|
264
|
+
const typeStr = type === gl.VERTEX_SHADER ? "vertex" : "fragment";
|
|
265
|
+
gl.deleteShader(shader);
|
|
266
|
+
throw new Error(`Failed to compile ${typeStr} shader: ${info}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return shader;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
destroy(): void {
|
|
273
|
+
const gl = this.#backend.gl;
|
|
274
|
+
gl.deleteProgram(this.#program);
|
|
275
|
+
gl.deleteVertexArray(this.#vao);
|
|
276
|
+
gl.deleteBuffer(this.#instanceBuffer);
|
|
277
|
+
}
|
|
278
|
+
}
|