@driftengine/ui2d 3.61.0
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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +228 -0
- package/dist/camera2d.d.ts +47 -0
- package/dist/camera2d.js +46 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +21 -0
- package/dist/shaders/generated/sprite.wgsl.d.ts +54 -0
- package/dist/shaders/generated/sprite.wgsl.js +60 -0
- package/dist/shaders/sprite.d.ts +3 -0
- package/dist/shaders/sprite.js +98 -0
- package/dist/spriteBatch.d.ts +78 -0
- package/dist/spriteBatch.js +93 -0
- package/dist/spriteGl.d.ts +26 -0
- package/dist/spriteGl.js +207 -0
- package/dist/spriteGpu.d.ts +31 -0
- package/dist/spriteGpu.js +201 -0
- package/dist/spritePass.d.ts +54 -0
- package/dist/spritePass.js +142 -0
- package/dist/spriteSheet.d.ts +64 -0
- package/dist/spriteSheet.js +84 -0
- package/dist/spriteTexture.d.ts +36 -0
- package/dist/spriteTexture.js +5 -0
- package/dist/tilemap.d.ts +47 -0
- package/dist/tilemap.js +70 -0
- package/dist/uiDraw.d.ts +28 -0
- package/dist/uiDraw.js +41 -0
- package/dist/uiFocus.d.ts +36 -0
- package/dist/uiFocus.js +78 -0
- package/dist/uiInput.d.ts +51 -0
- package/dist/uiInput.js +85 -0
- package/dist/uiLayout.d.ts +23 -0
- package/dist/uiLayout.js +144 -0
- package/dist/uiNode.d.ts +105 -0
- package/dist/uiNode.js +79 -0
- package/package.json +57 -0
- package/src/camera2d.ts +88 -0
- package/src/index.ts +55 -0
- package/src/shaders/generated/sprite.wgsl.ts +63 -0
- package/src/shaders/sprite.ts +102 -0
- package/src/spriteBatch.ts +169 -0
- package/src/spriteGl.ts +270 -0
- package/src/spriteGpu.ts +271 -0
- package/src/spritePass.ts +231 -0
- package/src/spriteSheet.ts +147 -0
- package/src/spriteTexture.ts +42 -0
- package/src/tilemap.ts +114 -0
- package/src/uiDraw.ts +63 -0
- package/src/uiFocus.ts +80 -0
- package/src/uiInput.ts +115 -0
- package/src/uiLayout.ts +157 -0
- package/src/uiNode.ts +186 -0
package/src/spriteGl.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/** The WebGL2 half of the sprite pass: one program, one instance buffer, one texture slot table. */
|
|
2
|
+
|
|
3
|
+
import { SPRITE_FRAG, SPRITE_VERT } from './shaders/sprite.ts';
|
|
4
|
+
import { SPRITE_FLOATS } from './spriteBatch.ts';
|
|
5
|
+
import type { SpriteBatch } from './spriteBatch.ts';
|
|
6
|
+
import type { SpriteTextureOptions } from './spriteTexture.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The unit this pass borrows for the length of one draw.
|
|
10
|
+
*
|
|
11
|
+
* Fourteen is above every unit `lightBudget.ts` assigns — `COOKIE_ATLAS_TEXTURE_UNIT` is the
|
|
12
|
+
* highest at thirteen — and below the sixteen WebGL2 guarantees. The pass releases it before it
|
|
13
|
+
* returns, because a texture left bound to a unit a later pass attaches is a feedback loop rather
|
|
14
|
+
* than a wrong colour.
|
|
15
|
+
*/
|
|
16
|
+
const SPRITE_UNIT = 14;
|
|
17
|
+
|
|
18
|
+
/** Bytes per instance. Fourteen floats; see `SPRITE_FLOATS`. */
|
|
19
|
+
const STRIDE = SPRITE_FLOATS * 4;
|
|
20
|
+
|
|
21
|
+
export interface Webgl2Sprites {
|
|
22
|
+
readonly program: WebGLProgram;
|
|
23
|
+
readonly vao: WebGLVertexArrayObject;
|
|
24
|
+
readonly instances: WebGLBuffer;
|
|
25
|
+
readonly uniforms: Readonly<Record<string, WebGLUniformLocation | null>>;
|
|
26
|
+
/** One per slot, `null` until the caller sets it. A run naming an empty slot draws nothing. */
|
|
27
|
+
readonly textures: (WebGLTexture | null)[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function compile(
|
|
31
|
+
gl: WebGL2RenderingContext,
|
|
32
|
+
kind: number,
|
|
33
|
+
source: string,
|
|
34
|
+
label: string,
|
|
35
|
+
): WebGLShader {
|
|
36
|
+
const shader = gl.createShader(kind);
|
|
37
|
+
if (shader === null) throw new Error(`${label}: createShader failed`);
|
|
38
|
+
gl.shaderSource(shader, source);
|
|
39
|
+
gl.compileShader(shader);
|
|
40
|
+
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS) !== true) {
|
|
41
|
+
const log = gl.getShaderInfoLog(shader) ?? '';
|
|
42
|
+
gl.deleteShader(shader);
|
|
43
|
+
throw new Error(`${label}: ${log}`);
|
|
44
|
+
}
|
|
45
|
+
return shader;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const UNIFORM_NAMES = [
|
|
49
|
+
'uToNdc0',
|
|
50
|
+
'uToNdc1',
|
|
51
|
+
'uClipCorrection',
|
|
52
|
+
'uSpriteTexture',
|
|
53
|
+
'uOutputTransform',
|
|
54
|
+
'uOutputExposure',
|
|
55
|
+
] as const;
|
|
56
|
+
|
|
57
|
+
export function createWebgl2Sprites(
|
|
58
|
+
gl: WebGL2RenderingContext,
|
|
59
|
+
capacity: number,
|
|
60
|
+
slots: number,
|
|
61
|
+
label: string,
|
|
62
|
+
): Webgl2Sprites {
|
|
63
|
+
const program = gl.createProgram();
|
|
64
|
+
if (program === null) throw new Error(`${label}: createProgram failed`);
|
|
65
|
+
const vert = compile(gl, gl.VERTEX_SHADER, SPRITE_VERT, `${label} vertex`);
|
|
66
|
+
const frag = compile(gl, gl.FRAGMENT_SHADER, SPRITE_FRAG, `${label} fragment`);
|
|
67
|
+
gl.attachShader(program, vert);
|
|
68
|
+
gl.attachShader(program, frag);
|
|
69
|
+
gl.linkProgram(program);
|
|
70
|
+
gl.deleteShader(vert);
|
|
71
|
+
gl.deleteShader(frag);
|
|
72
|
+
if (gl.getProgramParameter(program, gl.LINK_STATUS) !== true) {
|
|
73
|
+
const log = gl.getProgramInfoLog(program) ?? '';
|
|
74
|
+
gl.deleteProgram(program);
|
|
75
|
+
throw new Error(`${label}: ${log}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const uniforms: Record<string, WebGLUniformLocation | null> = {};
|
|
79
|
+
for (const name of UNIFORM_NAMES) uniforms[name] = gl.getUniformLocation(program, name);
|
|
80
|
+
|
|
81
|
+
const vao = gl.createVertexArray();
|
|
82
|
+
const instances = gl.createBuffer();
|
|
83
|
+
if (vao === null || instances === null) throw new Error(`${label}: buffer allocation failed`);
|
|
84
|
+
gl.bindVertexArray(vao);
|
|
85
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, instances);
|
|
86
|
+
gl.bufferData(gl.ARRAY_BUFFER, capacity * STRIDE, gl.DYNAMIC_DRAW);
|
|
87
|
+
/*
|
|
88
|
+
* Four attributes, every one of them per instance: the quad's own corners come from
|
|
89
|
+
* `gl_VertexID` and cost no buffer at all. Offsets are re-pointed per run, which is how a run
|
|
90
|
+
* starts partway into the buffer without `baseInstance` — WebGL2 has no such call.
|
|
91
|
+
*/
|
|
92
|
+
bindAttributes(gl, 0);
|
|
93
|
+
gl.bindVertexArray(null);
|
|
94
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
95
|
+
|
|
96
|
+
return { program, vao, instances, uniforms, textures: new Array<null>(slots).fill(null) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Point the four instance attributes at instance `first`. The buffer must be bound. */
|
|
100
|
+
function bindAttributes(gl: WebGL2RenderingContext, first: number): void {
|
|
101
|
+
const base = first * STRIDE;
|
|
102
|
+
gl.enableVertexAttribArray(0);
|
|
103
|
+
gl.vertexAttribPointer(0, 4, gl.FLOAT, false, STRIDE, base);
|
|
104
|
+
gl.vertexAttribDivisor(0, 1);
|
|
105
|
+
gl.enableVertexAttribArray(1);
|
|
106
|
+
gl.vertexAttribPointer(1, 4, gl.FLOAT, false, STRIDE, base + 16);
|
|
107
|
+
gl.vertexAttribDivisor(1, 1);
|
|
108
|
+
gl.enableVertexAttribArray(2);
|
|
109
|
+
gl.vertexAttribPointer(2, 4, gl.FLOAT, false, STRIDE, base + 32);
|
|
110
|
+
gl.vertexAttribDivisor(2, 1);
|
|
111
|
+
gl.enableVertexAttribArray(3);
|
|
112
|
+
gl.vertexAttribPointer(3, 2, gl.FLOAT, false, STRIDE, base + 48);
|
|
113
|
+
gl.vertexAttribDivisor(3, 1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Replace a texture slot. The previous texture in that slot is deleted. */
|
|
117
|
+
export function setWebgl2SpriteTexture(
|
|
118
|
+
gl: WebGL2RenderingContext,
|
|
119
|
+
sprites: Webgl2Sprites,
|
|
120
|
+
slot: number,
|
|
121
|
+
source: TexImageSource,
|
|
122
|
+
options: SpriteTextureOptions,
|
|
123
|
+
): void {
|
|
124
|
+
const previous = sprites.textures[slot];
|
|
125
|
+
if (previous !== null && previous !== undefined) gl.deleteTexture(previous);
|
|
126
|
+
const texture = gl.createTexture();
|
|
127
|
+
if (texture === null) throw new Error('ui2d: createTexture failed');
|
|
128
|
+
gl.activeTexture(gl.TEXTURE0 + SPRITE_UNIT);
|
|
129
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
130
|
+
const internal = options.colorSpace === 'linear' ? gl.RGBA : gl.SRGB8_ALPHA8;
|
|
131
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, internal, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
132
|
+
const filter = options.filter === 'linear' ? gl.LINEAR : gl.NEAREST;
|
|
133
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
|
|
134
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
|
|
135
|
+
/*
|
|
136
|
+
* Clamped on both axes, and it is a correctness rule rather than a default. A sheet frame's
|
|
137
|
+
* edge texel is adjacent to the *next* frame's, so a repeating wrap bleeds one sprite into
|
|
138
|
+
* another at exactly the seam a caller cannot see in the atlas.
|
|
139
|
+
*/
|
|
140
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
141
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
142
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
143
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
144
|
+
sprites.textures[slot] = texture;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Fill a slot with one opaque white texel, from an array rather than an image.
|
|
149
|
+
*
|
|
150
|
+
* **White is the identity of the multiply this shader does**, so a quad on this slot draws exactly
|
|
151
|
+
* its tint — which is what a solid background is. Built from four bytes rather than from a canvas
|
|
152
|
+
* because the pass has no DOM to reach for and should not need one: `texImage2D`'s pixel overload
|
|
153
|
+
* takes the texel directly, and `queue.writeTexture` is its WebGPU twin.
|
|
154
|
+
*/
|
|
155
|
+
export function setWebgl2WhiteTexture(
|
|
156
|
+
gl: WebGL2RenderingContext,
|
|
157
|
+
sprites: Webgl2Sprites,
|
|
158
|
+
slot: number,
|
|
159
|
+
): void {
|
|
160
|
+
const texture = gl.createTexture();
|
|
161
|
+
if (texture === null) throw new Error('ui2d: createTexture failed');
|
|
162
|
+
gl.activeTexture(gl.TEXTURE0 + SPRITE_UNIT);
|
|
163
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
164
|
+
gl.texImage2D(
|
|
165
|
+
gl.TEXTURE_2D,
|
|
166
|
+
0,
|
|
167
|
+
gl.RGBA,
|
|
168
|
+
1,
|
|
169
|
+
1,
|
|
170
|
+
0,
|
|
171
|
+
gl.RGBA,
|
|
172
|
+
gl.UNSIGNED_BYTE,
|
|
173
|
+
new Uint8Array([255, 255, 255, 255]),
|
|
174
|
+
);
|
|
175
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
176
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
177
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
178
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
179
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
180
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
181
|
+
sprites.textures[slot] = texture;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function uploadWebgl2Instances(
|
|
185
|
+
gl: WebGL2RenderingContext,
|
|
186
|
+
sprites: Webgl2Sprites,
|
|
187
|
+
batch: SpriteBatch,
|
|
188
|
+
): void {
|
|
189
|
+
if (batch.count === 0) return;
|
|
190
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, sprites.instances);
|
|
191
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, batch.instances, 0, batch.count * SPRITE_FLOATS);
|
|
192
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function drawWebgl2Sprites(
|
|
196
|
+
gl: WebGL2RenderingContext,
|
|
197
|
+
sprites: Webgl2Sprites,
|
|
198
|
+
batch: SpriteBatch,
|
|
199
|
+
toNdc: Float32Array,
|
|
200
|
+
clipCorrection: Float32Array,
|
|
201
|
+
outputTransform: number,
|
|
202
|
+
outputExposure: number,
|
|
203
|
+
): void {
|
|
204
|
+
if (batch.count === 0) return;
|
|
205
|
+
gl.useProgram(sprites.program);
|
|
206
|
+
gl.bindVertexArray(sprites.vao);
|
|
207
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, sprites.instances);
|
|
208
|
+
|
|
209
|
+
const u = sprites.uniforms;
|
|
210
|
+
gl.uniform4f(
|
|
211
|
+
u['uToNdc0'] ?? null,
|
|
212
|
+
toNdc[0] as number,
|
|
213
|
+
toNdc[1] as number,
|
|
214
|
+
toNdc[2] as number,
|
|
215
|
+
toNdc[3] as number,
|
|
216
|
+
);
|
|
217
|
+
gl.uniform4f(u['uToNdc1'] ?? null, toNdc[4] as number, toNdc[5] as number, 0, 0);
|
|
218
|
+
gl.uniformMatrix4fv(u['uClipCorrection'] ?? null, false, clipCorrection);
|
|
219
|
+
gl.uniform1i(u['uOutputTransform'] ?? null, outputTransform);
|
|
220
|
+
gl.uniform1f(u['uOutputExposure'] ?? null, outputExposure);
|
|
221
|
+
gl.uniform1i(u['uSpriteTexture'] ?? null, SPRITE_UNIT);
|
|
222
|
+
|
|
223
|
+
const blendWas = gl.getParameter(gl.BLEND) as boolean;
|
|
224
|
+
const depthTestWas = gl.getParameter(gl.DEPTH_TEST) as boolean;
|
|
225
|
+
const depthMaskWas = gl.getParameter(gl.DEPTH_WRITEMASK) as boolean;
|
|
226
|
+
const cullWas = gl.getParameter(gl.CULL_FACE) as boolean;
|
|
227
|
+
/*
|
|
228
|
+
* **Culling off, and the WebGPU pipeline says `cullMode: 'none'` for the same reason**: a sprite
|
|
229
|
+
* is mirrored by giving it a negative width, which reverses its winding, so a cull mode drops
|
|
230
|
+
* every flipped sprite — which is what a character facing left is.
|
|
231
|
+
*
|
|
232
|
+
* This line is here because the frame that was missing it drew *nothing at all* on WebGL2 while
|
|
233
|
+
* WebGPU was pixel-perfect, and nothing reported an error. A contributed pass inherits whatever
|
|
234
|
+
* cull state the last scene draw left on, and the screen-space quad's winding is not the scene's;
|
|
235
|
+
* WebGPU has no such inheritance, because a pipeline states its own. Every backend difference
|
|
236
|
+
* this seam has is of that shape: one API carries state between draws and the other does not.
|
|
237
|
+
*/
|
|
238
|
+
gl.disable(gl.CULL_FACE);
|
|
239
|
+
gl.enable(gl.BLEND);
|
|
240
|
+
/* Premultiplied `over`: the fragment stage folds alpha in, so this composes onto opaque pixels. */
|
|
241
|
+
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
242
|
+
gl.disable(gl.DEPTH_TEST);
|
|
243
|
+
gl.depthMask(false);
|
|
244
|
+
gl.activeTexture(gl.TEXTURE0 + SPRITE_UNIT);
|
|
245
|
+
|
|
246
|
+
for (let run = 0; run < batch.runCount; run += 1) {
|
|
247
|
+
const at = run * 3;
|
|
248
|
+
const texture = sprites.textures[batch.runs[at] as number];
|
|
249
|
+
if (texture === null || texture === undefined) continue;
|
|
250
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
251
|
+
bindAttributes(gl, batch.runs[at + 1] as number);
|
|
252
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, batch.runs[at + 2] as number);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
256
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
257
|
+
gl.depthMask(depthMaskWas);
|
|
258
|
+
if (cullWas) gl.enable(gl.CULL_FACE);
|
|
259
|
+
if (depthTestWas) gl.enable(gl.DEPTH_TEST);
|
|
260
|
+
if (!blendWas) gl.disable(gl.BLEND);
|
|
261
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
262
|
+
gl.bindVertexArray(null);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function disposeWebgl2Sprites(gl: WebGL2RenderingContext, sprites: Webgl2Sprites): void {
|
|
266
|
+
gl.deleteProgram(sprites.program);
|
|
267
|
+
gl.deleteVertexArray(sprites.vao);
|
|
268
|
+
gl.deleteBuffer(sprites.instances);
|
|
269
|
+
for (const texture of sprites.textures) if (texture !== null) gl.deleteTexture(texture);
|
|
270
|
+
}
|
package/src/spriteGpu.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/** The WebGPU half of the sprite pass: one pipeline, one instance buffer, a bind group per slot. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
SPRITE_BINDINGS,
|
|
5
|
+
SPRITE_FRAG_WGSL,
|
|
6
|
+
SPRITE_VERT_WGSL,
|
|
7
|
+
} from './shaders/generated/sprite.wgsl.ts';
|
|
8
|
+
import { SPRITE_FLOATS } from './spriteBatch.ts';
|
|
9
|
+
import type { SpriteBatch } from './spriteBatch.ts';
|
|
10
|
+
import type { SpriteImage, SpriteTextureOptions } from './spriteTexture.ts';
|
|
11
|
+
|
|
12
|
+
const VERT = SPRITE_BINDINGS.SPRITE_VERT;
|
|
13
|
+
const FRAG = SPRITE_BINDINGS.SPRITE_FRAG;
|
|
14
|
+
|
|
15
|
+
const STRIDE = SPRITE_FLOATS * 4;
|
|
16
|
+
const STAGE_VERTEX = 1; // GPUShaderStage.VERTEX
|
|
17
|
+
const STAGE_FRAGMENT = 2; // GPUShaderStage.FRAGMENT
|
|
18
|
+
const UNIFORM_COPY_DST = 64 | 8; // GPUBufferUsage.UNIFORM | COPY_DST
|
|
19
|
+
const VERTEX_COPY_DST = 32 | 8; // GPUBufferUsage.VERTEX | COPY_DST
|
|
20
|
+
const TEXTURE_USAGE = 4 | 2 | 16; // TEXTURE_BINDING | COPY_DST | RENDER_ATTACHMENT
|
|
21
|
+
|
|
22
|
+
export interface GpuSpriteSlot {
|
|
23
|
+
readonly texture: GPUTexture;
|
|
24
|
+
readonly bindGroup: GPUBindGroup;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GpuSprites {
|
|
28
|
+
readonly pipeline: GPURenderPipeline;
|
|
29
|
+
readonly layout: GPUBindGroupLayout;
|
|
30
|
+
readonly vertexUniforms: GPUBuffer;
|
|
31
|
+
readonly fragmentUniforms: GPUBuffer;
|
|
32
|
+
readonly instances: GPUBuffer;
|
|
33
|
+
readonly samplers: { readonly nearest: GPUSampler; readonly linear: GPUSampler };
|
|
34
|
+
readonly slots: (GpuSpriteSlot | null)[];
|
|
35
|
+
readonly vertexScratch: ArrayBuffer;
|
|
36
|
+
readonly vertexFloats: Float32Array;
|
|
37
|
+
readonly fragmentScratch: ArrayBuffer;
|
|
38
|
+
readonly fragmentFloats: Float32Array;
|
|
39
|
+
readonly fragmentInts: Int32Array;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createGpuSprites(
|
|
43
|
+
device: GPUDevice,
|
|
44
|
+
format: GPUTextureFormat,
|
|
45
|
+
depthFormat: GPUTextureFormat,
|
|
46
|
+
samples: number,
|
|
47
|
+
capacity: number,
|
|
48
|
+
slots: number,
|
|
49
|
+
label: string,
|
|
50
|
+
): GpuSprites {
|
|
51
|
+
const layout = device.createBindGroupLayout({
|
|
52
|
+
label: `${label}.layout`,
|
|
53
|
+
entries: [
|
|
54
|
+
{ binding: VERT.uniforms, visibility: STAGE_VERTEX, buffer: { type: 'uniform' } },
|
|
55
|
+
{ binding: FRAG.uniforms, visibility: STAGE_FRAGMENT, buffer: { type: 'uniform' } },
|
|
56
|
+
{
|
|
57
|
+
binding: FRAG.textures.uSpriteTexture.texture,
|
|
58
|
+
visibility: STAGE_FRAGMENT,
|
|
59
|
+
texture: { sampleType: 'float' },
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
binding: FRAG.textures.uSpriteTexture.sampler,
|
|
63
|
+
visibility: STAGE_FRAGMENT,
|
|
64
|
+
sampler: { type: 'filtering' },
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const pipeline = device.createRenderPipeline({
|
|
70
|
+
label,
|
|
71
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
|
|
72
|
+
vertex: {
|
|
73
|
+
module: device.createShaderModule({ label: `${label}.vert`, code: SPRITE_VERT_WGSL }),
|
|
74
|
+
entryPoint: 'main',
|
|
75
|
+
buffers: [
|
|
76
|
+
{
|
|
77
|
+
arrayStride: STRIDE,
|
|
78
|
+
stepMode: 'instance',
|
|
79
|
+
attributes: [
|
|
80
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x4' },
|
|
81
|
+
{ shaderLocation: 1, offset: 16, format: 'float32x4' },
|
|
82
|
+
{ shaderLocation: 2, offset: 32, format: 'float32x4' },
|
|
83
|
+
{ shaderLocation: 3, offset: 48, format: 'float32x2' },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
fragment: {
|
|
89
|
+
module: device.createShaderModule({ label: `${label}.frag`, code: SPRITE_FRAG_WGSL }),
|
|
90
|
+
entryPoint: 'main',
|
|
91
|
+
targets: [
|
|
92
|
+
{
|
|
93
|
+
format,
|
|
94
|
+
/* Premultiplied `over`, matching the WebGL2 half exactly. */
|
|
95
|
+
blend: {
|
|
96
|
+
color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
|
|
97
|
+
alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
/*
|
|
103
|
+
* Nothing is culled: a sprite is mirrored by giving it a negative width, which reverses its
|
|
104
|
+
* winding, and a character facing left is exactly that.
|
|
105
|
+
*/
|
|
106
|
+
primitive: { topology: 'triangle-list', cullMode: 'none' },
|
|
107
|
+
multisample: { count: samples },
|
|
108
|
+
/*
|
|
109
|
+
* Neither tested nor written. The 2D layer's order *is* its layering — there is no z to sort
|
|
110
|
+
* by — and a pass that depth-tested would let whatever 3D geometry is in the frame punch holes
|
|
111
|
+
* in an overlay drawn over it. Declared all the same, because a pipeline in a render pass that
|
|
112
|
+
* has a depth attachment must name its format.
|
|
113
|
+
*/
|
|
114
|
+
depthStencil: { format: depthFormat, depthWriteEnabled: false, depthCompare: 'always' },
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const vertexUniforms = device.createBuffer({
|
|
118
|
+
label: `${label}.vertexUniforms`,
|
|
119
|
+
size: VERT.uniformSize,
|
|
120
|
+
usage: UNIFORM_COPY_DST,
|
|
121
|
+
});
|
|
122
|
+
const fragmentUniforms = device.createBuffer({
|
|
123
|
+
label: `${label}.fragmentUniforms`,
|
|
124
|
+
size: FRAG.uniformSize,
|
|
125
|
+
usage: UNIFORM_COPY_DST,
|
|
126
|
+
});
|
|
127
|
+
const instances = device.createBuffer({
|
|
128
|
+
label: `${label}.instances`,
|
|
129
|
+
size: capacity * STRIDE,
|
|
130
|
+
usage: VERTEX_COPY_DST,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const vertexScratch = new ArrayBuffer(VERT.uniformSize);
|
|
134
|
+
const fragmentScratch = new ArrayBuffer(FRAG.uniformSize);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
pipeline,
|
|
138
|
+
layout,
|
|
139
|
+
vertexUniforms,
|
|
140
|
+
fragmentUniforms,
|
|
141
|
+
instances,
|
|
142
|
+
samplers: {
|
|
143
|
+
nearest: device.createSampler({ label: `${label}.nearest` }),
|
|
144
|
+
linear: device.createSampler({
|
|
145
|
+
label: `${label}.linear`,
|
|
146
|
+
magFilter: 'linear',
|
|
147
|
+
minFilter: 'linear',
|
|
148
|
+
}),
|
|
149
|
+
},
|
|
150
|
+
slots: new Array<null>(slots).fill(null),
|
|
151
|
+
vertexScratch,
|
|
152
|
+
vertexFloats: new Float32Array(vertexScratch),
|
|
153
|
+
fragmentScratch,
|
|
154
|
+
fragmentFloats: new Float32Array(fragmentScratch),
|
|
155
|
+
fragmentInts: new Int32Array(fragmentScratch),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function setGpuSpriteTexture(
|
|
160
|
+
device: GPUDevice,
|
|
161
|
+
sprites: GpuSprites,
|
|
162
|
+
slot: number,
|
|
163
|
+
source: SpriteImage,
|
|
164
|
+
options: SpriteTextureOptions,
|
|
165
|
+
): void {
|
|
166
|
+
const previous = sprites.slots[slot];
|
|
167
|
+
if (previous !== null && previous !== undefined) previous.texture.destroy();
|
|
168
|
+
const texture = device.createTexture({
|
|
169
|
+
label: `ui2d.sprite.${slot}`,
|
|
170
|
+
size: [source.width, source.height],
|
|
171
|
+
format: options.colorSpace === 'linear' ? 'rgba8unorm' : 'rgba8unorm-srgb',
|
|
172
|
+
usage: TEXTURE_USAGE,
|
|
173
|
+
});
|
|
174
|
+
/*
|
|
175
|
+
* **No flip, matching WebGL2** — `surfaceTexture.ts` carries the whole argument and the bug it
|
|
176
|
+
* came from: `UNPACK_FLIP_Y_WEBGL` is ignored for an `ImageBitmap` and honoured for a canvas, so
|
|
177
|
+
* a `flipY: true` here would be self-consistent and disagree with the other backend on exactly
|
|
178
|
+
* the source type most callers use.
|
|
179
|
+
*/
|
|
180
|
+
device.queue.copyExternalImageToTexture(
|
|
181
|
+
{ source: source as GPUCopyExternalImageSource, flipY: false },
|
|
182
|
+
{ texture },
|
|
183
|
+
[source.width, source.height],
|
|
184
|
+
);
|
|
185
|
+
const sampler = options.filter === 'linear' ? sprites.samplers.linear : sprites.samplers.nearest;
|
|
186
|
+
const bindGroup = device.createBindGroup({
|
|
187
|
+
label: `ui2d.sprite.${slot}.bindGroup`,
|
|
188
|
+
layout: sprites.layout,
|
|
189
|
+
entries: [
|
|
190
|
+
{ binding: VERT.uniforms, resource: { buffer: sprites.vertexUniforms } },
|
|
191
|
+
{ binding: FRAG.uniforms, resource: { buffer: sprites.fragmentUniforms } },
|
|
192
|
+
{ binding: FRAG.textures.uSpriteTexture.texture, resource: texture.createView() },
|
|
193
|
+
{ binding: FRAG.textures.uSpriteTexture.sampler, resource: sampler },
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
sprites.slots[slot] = { texture, bindGroup };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The same one white texel, written straight into a texture. See the WebGL2 half for why. */
|
|
200
|
+
export function setGpuWhiteTexture(device: GPUDevice, sprites: GpuSprites, slot: number): void {
|
|
201
|
+
const texture = device.createTexture({
|
|
202
|
+
label: 'ui2d.sprite.white',
|
|
203
|
+
size: [1, 1],
|
|
204
|
+
/* Not `-srgb`: 255 is 255 either way, and a linear format says so without a decode. */
|
|
205
|
+
format: 'rgba8unorm',
|
|
206
|
+
usage: TEXTURE_USAGE,
|
|
207
|
+
});
|
|
208
|
+
device.queue.writeTexture(
|
|
209
|
+
{ texture },
|
|
210
|
+
new Uint8Array([255, 255, 255, 255]),
|
|
211
|
+
{ bytesPerRow: 4 },
|
|
212
|
+
[1, 1],
|
|
213
|
+
);
|
|
214
|
+
const bindGroup = device.createBindGroup({
|
|
215
|
+
label: 'ui2d.sprite.white.bindGroup',
|
|
216
|
+
layout: sprites.layout,
|
|
217
|
+
entries: [
|
|
218
|
+
{ binding: VERT.uniforms, resource: { buffer: sprites.vertexUniforms } },
|
|
219
|
+
{ binding: FRAG.uniforms, resource: { buffer: sprites.fragmentUniforms } },
|
|
220
|
+
{ binding: FRAG.textures.uSpriteTexture.texture, resource: texture.createView() },
|
|
221
|
+
{ binding: FRAG.textures.uSpriteTexture.sampler, resource: sprites.samplers.nearest },
|
|
222
|
+
],
|
|
223
|
+
});
|
|
224
|
+
sprites.slots[slot] = { texture, bindGroup };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function uploadGpuInstances(
|
|
228
|
+
device: GPUDevice,
|
|
229
|
+
sprites: GpuSprites,
|
|
230
|
+
batch: SpriteBatch,
|
|
231
|
+
): void {
|
|
232
|
+
if (batch.count === 0) return;
|
|
233
|
+
device.queue.writeBuffer(
|
|
234
|
+
sprites.instances,
|
|
235
|
+
0,
|
|
236
|
+
batch.instances.buffer,
|
|
237
|
+
batch.instances.byteOffset,
|
|
238
|
+
batch.count * STRIDE,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function drawGpuSprites(
|
|
243
|
+
pass: GPURenderPassEncoder,
|
|
244
|
+
sprites: GpuSprites,
|
|
245
|
+
batch: SpriteBatch,
|
|
246
|
+
): void {
|
|
247
|
+
if (batch.count === 0) return;
|
|
248
|
+
pass.setPipeline(sprites.pipeline);
|
|
249
|
+
for (let run = 0; run < batch.runCount; run += 1) {
|
|
250
|
+
const at = run * 3;
|
|
251
|
+
const slot = sprites.slots[batch.runs[at] as number];
|
|
252
|
+
if (slot === null || slot === undefined) continue;
|
|
253
|
+
const first = batch.runs[at + 1] as number;
|
|
254
|
+
const count = batch.runs[at + 2] as number;
|
|
255
|
+
pass.setBindGroup(0, slot.bindGroup);
|
|
256
|
+
/*
|
|
257
|
+
* The run's offset goes into the vertex buffer binding rather than into `firstInstance`,
|
|
258
|
+
* which is the one place the two backends can be made to do the same arithmetic: WebGL2 has
|
|
259
|
+
* no base-instance call at all and re-points its attributes the same way.
|
|
260
|
+
*/
|
|
261
|
+
pass.setVertexBuffer(0, sprites.instances, first * STRIDE, count * STRIDE);
|
|
262
|
+
pass.draw(6, count);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function disposeGpuSprites(sprites: GpuSprites): void {
|
|
267
|
+
sprites.vertexUniforms.destroy();
|
|
268
|
+
sprites.fragmentUniforms.destroy();
|
|
269
|
+
sprites.instances.destroy();
|
|
270
|
+
for (const slot of sprites.slots) if (slot !== null) slot.texture.destroy();
|
|
271
|
+
}
|