wgsl-renderer 0.0.1 → 0.0.3
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.md +8 -8
- package/README.md +294 -200
- package/README.zh-CN.md +417 -0
- package/dist/cjs/index.js +147 -157
- package/dist/esm/index.js +147 -157
- package/dist/types/index.d.ts +64 -55
- package/dist/types/index.js +147 -157
- package/package.json +4 -2
package/dist/cjs/index.js
CHANGED
|
@@ -7,8 +7,12 @@ var RenderPass = class {
|
|
|
7
7
|
vertexBuffer;
|
|
8
8
|
clearColor;
|
|
9
9
|
blendMode;
|
|
10
|
-
|
|
10
|
+
view;
|
|
11
|
+
format;
|
|
12
|
+
passResources = [];
|
|
13
|
+
device;
|
|
11
14
|
constructor(descriptor, device, format, layout) {
|
|
15
|
+
this.device = device;
|
|
12
16
|
this.name = descriptor.name;
|
|
13
17
|
this.clearColor = descriptor.clearColor || {
|
|
14
18
|
r: 0,
|
|
@@ -16,9 +20,12 @@ var RenderPass = class {
|
|
|
16
20
|
b: 0,
|
|
17
21
|
a: 1
|
|
18
22
|
};
|
|
19
|
-
this.blendMode = descriptor.blendMode || "
|
|
20
|
-
|
|
21
|
-
this.
|
|
23
|
+
this.blendMode = descriptor.blendMode || "none";
|
|
24
|
+
this.view = descriptor.view;
|
|
25
|
+
this.format = descriptor.format;
|
|
26
|
+
const actualFormat = descriptor.format || format;
|
|
27
|
+
const module$1 = this.device.createShaderModule({ code: descriptor.shaderCode });
|
|
28
|
+
this.vertexBuffer = this.device.createBuffer({
|
|
22
29
|
size: 36,
|
|
23
30
|
usage: GPUBufferUsage.VERTEX,
|
|
24
31
|
mappedAtCreation: true
|
|
@@ -35,11 +42,13 @@ var RenderPass = class {
|
|
|
35
42
|
0
|
|
36
43
|
]);
|
|
37
44
|
this.vertexBuffer.unmap();
|
|
38
|
-
|
|
45
|
+
const vertexEntryPoint = descriptor.entryPoints?.vertex || "vs_main";
|
|
46
|
+
const fragmentEntryPoint = descriptor.entryPoints?.fragment || "fs_main";
|
|
47
|
+
this.pipeline = this.device.createRenderPipeline({
|
|
39
48
|
layout,
|
|
40
49
|
vertex: {
|
|
41
50
|
module: module$1,
|
|
42
|
-
entryPoint:
|
|
51
|
+
entryPoint: vertexEntryPoint,
|
|
43
52
|
buffers: [{
|
|
44
53
|
arrayStride: 12,
|
|
45
54
|
attributes: [{
|
|
@@ -51,18 +60,24 @@ var RenderPass = class {
|
|
|
51
60
|
},
|
|
52
61
|
fragment: {
|
|
53
62
|
module: module$1,
|
|
54
|
-
entryPoint:
|
|
63
|
+
entryPoint: fragmentEntryPoint,
|
|
55
64
|
targets: [{
|
|
56
|
-
format,
|
|
65
|
+
format: actualFormat,
|
|
57
66
|
blend: this.getBlendState()
|
|
58
67
|
}]
|
|
59
68
|
},
|
|
60
69
|
primitive: { topology: "triangle-list" }
|
|
61
70
|
});
|
|
71
|
+
this.bindGroup = null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Update bind group with new entries (e.g., after texture resize)
|
|
75
|
+
*/
|
|
76
|
+
updateBindGroup(newEntries) {
|
|
62
77
|
const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
|
|
63
|
-
this.bindGroup = device.createBindGroup({
|
|
78
|
+
this.bindGroup = this.device.createBindGroup({
|
|
64
79
|
layout: bindGroupLayout,
|
|
65
|
-
entries:
|
|
80
|
+
entries: newEntries
|
|
66
81
|
});
|
|
67
82
|
}
|
|
68
83
|
getBlendState() {
|
|
@@ -126,7 +141,7 @@ var TextureManager = class {
|
|
|
126
141
|
const texture = this.device.createTexture({
|
|
127
142
|
size: [this.width, this.height],
|
|
128
143
|
format: format || "bgra8unorm",
|
|
129
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
|
|
144
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
|
|
130
145
|
});
|
|
131
146
|
this.textures.set(name, texture);
|
|
132
147
|
return texture;
|
|
@@ -136,10 +151,12 @@ var TextureManager = class {
|
|
|
136
151
|
}
|
|
137
152
|
resize(width, height) {
|
|
138
153
|
if (width === this.width && height === this.height) return;
|
|
139
|
-
this.textures.forEach((texture) => texture.destroy());
|
|
140
|
-
this.textures.clear();
|
|
141
154
|
this.width = width;
|
|
142
155
|
this.height = height;
|
|
156
|
+
this.textures.forEach((texture) => {
|
|
157
|
+
texture.destroy();
|
|
158
|
+
});
|
|
159
|
+
this.textures.clear();
|
|
143
160
|
}
|
|
144
161
|
destroy() {
|
|
145
162
|
this.textures.forEach((texture) => texture.destroy());
|
|
@@ -153,6 +170,30 @@ var TextureManager = class {
|
|
|
153
170
|
}
|
|
154
171
|
};
|
|
155
172
|
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/PassTextureRef.ts
|
|
175
|
+
const PASS_TEXTURE_REF_SYMBOL = Symbol("PassTextureRef");
|
|
176
|
+
var PassTextureRef = class PassTextureRef {
|
|
177
|
+
[PASS_TEXTURE_REF_SYMBOL] = true;
|
|
178
|
+
passName;
|
|
179
|
+
constructor(passName) {
|
|
180
|
+
this.passName = passName;
|
|
181
|
+
}
|
|
182
|
+
static is(obj) {
|
|
183
|
+
return obj && typeof obj === "object" && PASS_TEXTURE_REF_SYMBOL in obj;
|
|
184
|
+
}
|
|
185
|
+
static fromGPUBindingResource(resource) {
|
|
186
|
+
if (this.is(resource)) return resource;
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
static create(passName) {
|
|
190
|
+
return new PassTextureRef(passName);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
function isPassTextureRef(obj) {
|
|
194
|
+
return PassTextureRef.is(obj);
|
|
195
|
+
}
|
|
196
|
+
|
|
156
197
|
//#endregion
|
|
157
198
|
//#region src/index.ts
|
|
158
199
|
var WGSLRenderer = class {
|
|
@@ -161,145 +202,122 @@ var WGSLRenderer = class {
|
|
|
161
202
|
format;
|
|
162
203
|
passes = [];
|
|
163
204
|
textureManager;
|
|
164
|
-
backgroundPassAdded = false;
|
|
165
|
-
backgroundColor = {
|
|
166
|
-
r: .1,
|
|
167
|
-
g: .1,
|
|
168
|
-
b: .1,
|
|
169
|
-
a: 1
|
|
170
|
-
};
|
|
171
|
-
uniforms = /* @__PURE__ */ new Map();
|
|
172
205
|
animationFrameId = null;
|
|
206
|
+
isResizing = false;
|
|
173
207
|
constructor(canvas, options) {
|
|
174
208
|
this.canvas = canvas;
|
|
209
|
+
this.options = options;
|
|
175
210
|
if (!navigator.gpu) throw new Error("WebGPU is not supported in this browser.");
|
|
176
211
|
this.ctx = canvas.getContext("webgpu");
|
|
177
|
-
switch (typeof options?.backgroundColor) {
|
|
178
|
-
case "number":
|
|
179
|
-
const hex = options.backgroundColor;
|
|
180
|
-
this.backgroundColor = {
|
|
181
|
-
r: (hex >> 16 & 255) / 255,
|
|
182
|
-
g: (hex >> 8 & 255) / 255,
|
|
183
|
-
b: (hex & 255) / 255,
|
|
184
|
-
a: 1
|
|
185
|
-
};
|
|
186
|
-
break;
|
|
187
|
-
case "string":
|
|
188
|
-
const m = options.backgroundColor.match(/^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i);
|
|
189
|
-
if (m) this.backgroundColor = {
|
|
190
|
-
r: Number.parseInt(m[1], 16) / 255,
|
|
191
|
-
g: Number.parseInt(m[2], 16) / 255,
|
|
192
|
-
b: Number.parseInt(m[3], 16) / 255,
|
|
193
|
-
a: 1
|
|
194
|
-
};
|
|
195
|
-
break;
|
|
196
|
-
case "object":
|
|
197
|
-
Object.assign(this.backgroundColor, options.backgroundColor);
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
212
|
}
|
|
201
213
|
async init() {
|
|
202
214
|
this.device = await (await navigator.gpu.requestAdapter()).requestDevice();
|
|
203
215
|
this.format = navigator.gpu.getPreferredCanvasFormat();
|
|
204
|
-
|
|
216
|
+
const config = Object.assign({
|
|
205
217
|
device: this.device,
|
|
206
218
|
format: this.format,
|
|
207
219
|
alphaMode: "opaque"
|
|
208
|
-
});
|
|
220
|
+
}, this.options?.config);
|
|
221
|
+
this.ctx.configure(config);
|
|
209
222
|
const canvasWidth = this.canvas.width || this.canvas.clientWidth;
|
|
210
223
|
const canvasHeight = this.canvas.height || this.canvas.clientHeight;
|
|
211
224
|
this.textureManager = new TextureManager(this.device, canvasWidth, canvasHeight);
|
|
212
|
-
|
|
225
|
+
}
|
|
226
|
+
async resize(width, height) {
|
|
227
|
+
if (this.isResizing) return;
|
|
228
|
+
if (this.canvas.width === width && this.canvas.height === height) return;
|
|
229
|
+
this.isResizing = true;
|
|
230
|
+
this.canvas.width = width;
|
|
231
|
+
this.canvas.height = height;
|
|
232
|
+
const future = this.device.queue.onSubmittedWorkDone();
|
|
233
|
+
future.catch(() => {
|
|
234
|
+
console.warn("GPU work submission failed during resize.");
|
|
235
|
+
this.isResizing = false;
|
|
236
|
+
});
|
|
237
|
+
await future;
|
|
238
|
+
this.textureManager.resize(width, height);
|
|
239
|
+
this.isResizing = false;
|
|
240
|
+
}
|
|
241
|
+
getContext() {
|
|
242
|
+
return this.ctx;
|
|
243
|
+
}
|
|
244
|
+
getDevice() {
|
|
245
|
+
return this.device;
|
|
213
246
|
}
|
|
214
247
|
/**
|
|
215
|
-
*
|
|
248
|
+
* Get texture reference by pass name
|
|
249
|
+
* Returns a PassTextureRef that will resolve to the actual texture at render time
|
|
216
250
|
*/
|
|
217
|
-
|
|
218
|
-
if (!this.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
this.backgroundPassAdded = true;
|
|
238
|
-
this.textureManager.createTexture("pass_0_output", this.format);
|
|
239
|
-
}
|
|
251
|
+
getPassTexture(passName) {
|
|
252
|
+
if (!this.passes.find((pass) => pass.name === passName)) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
|
|
253
|
+
return PassTextureRef.create(passName);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Resolve a PassTextureRef to actual GPUTextureView with validation
|
|
257
|
+
*/
|
|
258
|
+
resolveTextureRef(ref) {
|
|
259
|
+
const targetPassIndex = this.passes.findIndex((pass) => pass.name === ref.passName);
|
|
260
|
+
if (targetPassIndex === -1) throw new Error(`Cannot find pass named '${ref.passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
|
|
261
|
+
const textureName = `pass_${targetPassIndex}_output`;
|
|
262
|
+
let texture = this.textureManager.getTexture(textureName);
|
|
263
|
+
if (!texture) texture = this.textureManager.createTexture(textureName, this.format);
|
|
264
|
+
return texture.createView();
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Get pass by name
|
|
268
|
+
*/
|
|
269
|
+
getPassByName(passName) {
|
|
270
|
+
return this.passes.find((pass) => pass.name === passName);
|
|
240
271
|
}
|
|
241
272
|
/**
|
|
242
273
|
* Add a render pass to the multi-pass pipeline
|
|
243
274
|
*/
|
|
244
275
|
addPass(descriptor) {
|
|
245
276
|
const finalBindGroupEntries = [];
|
|
246
|
-
|
|
247
|
-
const previousOutput = this.getPassOutput(this.passes.length - 1);
|
|
248
|
-
if (previousOutput) finalBindGroupEntries.push({
|
|
249
|
-
binding: 0,
|
|
250
|
-
resource: previousOutput.createView()
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
descriptor.resources.forEach((resource, index) => {
|
|
277
|
+
descriptor.resources?.forEach((resource, index) => {
|
|
254
278
|
finalBindGroupEntries.push({
|
|
255
|
-
binding: index
|
|
279
|
+
binding: index,
|
|
256
280
|
resource
|
|
257
281
|
});
|
|
258
282
|
});
|
|
259
|
-
const
|
|
283
|
+
const internalDescriptor = {
|
|
260
284
|
name: descriptor.name,
|
|
261
285
|
shaderCode: descriptor.shaderCode,
|
|
286
|
+
entryPoints: descriptor.entryPoints,
|
|
262
287
|
clearColor: descriptor.clearColor,
|
|
263
288
|
blendMode: descriptor.blendMode,
|
|
264
|
-
bindGroupEntries: finalBindGroupEntries
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const currentPassIndex = this.passes.length - 1;
|
|
268
|
-
const textureName = `pass_${currentPassIndex}_output`;
|
|
269
|
-
this.textureManager.createTexture(textureName, this.format);
|
|
270
|
-
this.passes[currentPassIndex].hasOutputTexture = true;
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Set background color
|
|
274
|
-
*/
|
|
275
|
-
setBackgroundColor(r, g, b, a = 1) {
|
|
276
|
-
this.backgroundColor = {
|
|
277
|
-
r,
|
|
278
|
-
g,
|
|
279
|
-
b,
|
|
280
|
-
a
|
|
289
|
+
bindGroupEntries: finalBindGroupEntries,
|
|
290
|
+
view: descriptor.view,
|
|
291
|
+
format: descriptor.format
|
|
281
292
|
};
|
|
293
|
+
const pipelineFormat = descriptor.format || this.format;
|
|
294
|
+
const pass = new RenderPass(internalDescriptor, this.device, pipelineFormat, "auto");
|
|
295
|
+
pass.passResources = descriptor.resources ?? [];
|
|
296
|
+
this.passes.push(pass);
|
|
282
297
|
}
|
|
283
298
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
299
|
+
* Resolve resource to actual GPU binding resource
|
|
300
|
+
* Handles PassTextureRef by getting the current texture view with validation
|
|
286
301
|
*/
|
|
287
|
-
|
|
288
|
-
if (
|
|
289
|
-
|
|
290
|
-
const textureName = `pass_${passIndex}_output`;
|
|
291
|
-
return this.textureManager.createTexture(textureName, this.format);
|
|
302
|
+
resolveResource(resource) {
|
|
303
|
+
if (isPassTextureRef(resource)) return this.resolveTextureRef(resource);
|
|
304
|
+
return resource;
|
|
292
305
|
}
|
|
293
306
|
/**
|
|
294
|
-
*
|
|
307
|
+
* Update bind groups to resolve current texture references
|
|
308
|
+
* Call this before rendering to ensure all PassTextureRef are resolved
|
|
295
309
|
*/
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
310
|
+
updateBindGroups() {
|
|
311
|
+
this.passes.forEach((pass) => {
|
|
312
|
+
const finalBindGroupEntries = [];
|
|
313
|
+
pass.passResources.forEach((resource, index) => {
|
|
314
|
+
finalBindGroupEntries.push({
|
|
315
|
+
binding: index,
|
|
316
|
+
resource: this.resolveResource(resource)
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
pass.updateBindGroup(finalBindGroupEntries);
|
|
320
|
+
});
|
|
303
321
|
}
|
|
304
322
|
/**
|
|
305
323
|
* Create a uniforms
|
|
@@ -312,20 +330,13 @@ var WGSLRenderer = class {
|
|
|
312
330
|
size: values.byteLength,
|
|
313
331
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
314
332
|
});
|
|
315
|
-
|
|
316
|
-
const uniforms = {
|
|
317
|
-
id: uniformID,
|
|
333
|
+
return {
|
|
318
334
|
values,
|
|
319
335
|
apply: () => {
|
|
320
336
|
this.device.queue.writeBuffer(buffer, 0, values.buffer, values.byteOffset, values.byteLength);
|
|
321
337
|
},
|
|
322
338
|
getBuffer: () => buffer
|
|
323
339
|
};
|
|
324
|
-
this.uniforms.set(uniformID, uniforms);
|
|
325
|
-
return uniforms;
|
|
326
|
-
}
|
|
327
|
-
getUniformsByID(id) {
|
|
328
|
-
return this.uniforms.get(id);
|
|
329
340
|
}
|
|
330
341
|
/**
|
|
331
342
|
* Create a sampler
|
|
@@ -338,25 +349,7 @@ var WGSLRenderer = class {
|
|
|
338
349
|
addressModeV: "clamp-to-edge"
|
|
339
350
|
}, options));
|
|
340
351
|
}
|
|
341
|
-
|
|
342
|
-
* Create a bind group entry for texture
|
|
343
|
-
*/
|
|
344
|
-
createTextureBinding(texture) {
|
|
345
|
-
return texture.createView();
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Configure multi-pass rendering
|
|
349
|
-
*/
|
|
350
|
-
setupMultiPass(descriptor) {
|
|
351
|
-
this.passes = [];
|
|
352
|
-
this.textureManager.destroy();
|
|
353
|
-
const canvasWidth = this.canvas.width || this.canvas.clientWidth;
|
|
354
|
-
const canvasHeight = this.canvas.height || this.canvas.clientHeight;
|
|
355
|
-
this.textureManager = new TextureManager(this.device, canvasWidth, canvasHeight);
|
|
356
|
-
descriptor.passes.forEach((passDesc) => this.addPass(passDesc));
|
|
357
|
-
if (descriptor.output?.texture && !descriptor.output.writeToCanvas) this.textureManager.createTexture("final_output");
|
|
358
|
-
}
|
|
359
|
-
async loadTexture(url) {
|
|
352
|
+
async loadImageTexture(url) {
|
|
360
353
|
const resp = fetch(url);
|
|
361
354
|
resp.catch((err) => {
|
|
362
355
|
console.error("Failed to load texture:", err);
|
|
@@ -381,25 +374,25 @@ var WGSLRenderer = class {
|
|
|
381
374
|
}
|
|
382
375
|
renderFrame() {
|
|
383
376
|
if (this.passes.length === 0) return;
|
|
377
|
+
this.updateBindGroups();
|
|
384
378
|
const commandEncoder = this.device.createCommandEncoder();
|
|
385
|
-
const canvasWidth = this.canvas.width || this.canvas.clientWidth;
|
|
386
|
-
const canvasHeight = this.canvas.height || this.canvas.clientHeight;
|
|
387
|
-
this.textureManager.resize(canvasWidth, canvasHeight);
|
|
388
379
|
for (let i = 0; i < this.passes.length; i++) {
|
|
389
380
|
const pass = this.passes[i];
|
|
381
|
+
let loadOp = "load";
|
|
382
|
+
const isLast = i === this.passes.length - 1;
|
|
383
|
+
if (isLast) loadOp = "clear";
|
|
390
384
|
let renderTarget;
|
|
391
|
-
|
|
392
|
-
if (
|
|
385
|
+
if (pass.view) renderTarget = pass.view;
|
|
386
|
+
else if (isLast) renderTarget = this.ctx.getCurrentTexture().createView();
|
|
393
387
|
else {
|
|
394
388
|
const textureName = `pass_${i}_output`;
|
|
395
|
-
|
|
396
|
-
if (!texture)
|
|
389
|
+
let texture = this.textureManager.getTexture(textureName);
|
|
390
|
+
if (!texture) texture = this.textureManager.createTexture(textureName, "rgba16float");
|
|
397
391
|
renderTarget = texture.createView();
|
|
398
|
-
loadOp = "load";
|
|
399
392
|
}
|
|
400
393
|
const renderPass = commandEncoder.beginRenderPass({ colorAttachments: [{
|
|
401
394
|
view: renderTarget,
|
|
402
|
-
loadOp
|
|
395
|
+
loadOp,
|
|
403
396
|
storeOp: "store",
|
|
404
397
|
clearValue: pass.clearColor
|
|
405
398
|
}] });
|
|
@@ -412,9 +405,11 @@ var WGSLRenderer = class {
|
|
|
412
405
|
this.device.queue.submit([commandEncoder.finish()]);
|
|
413
406
|
}
|
|
414
407
|
loopRender(cb) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
408
|
+
this.animationFrameId = requestAnimationFrame((t) => {
|
|
409
|
+
cb?.(t);
|
|
410
|
+
this.renderFrame();
|
|
411
|
+
this.loopRender(cb);
|
|
412
|
+
});
|
|
418
413
|
}
|
|
419
414
|
stopLoop() {
|
|
420
415
|
if (this.animationFrameId !== null) {
|
|
@@ -422,11 +417,6 @@ var WGSLRenderer = class {
|
|
|
422
417
|
this.animationFrameId = null;
|
|
423
418
|
}
|
|
424
419
|
}
|
|
425
|
-
resize(width, height) {
|
|
426
|
-
this.canvas.width = width;
|
|
427
|
-
this.canvas.height = height;
|
|
428
|
-
this.textureManager.resize(width, height);
|
|
429
|
-
}
|
|
430
420
|
};
|
|
431
421
|
async function createWGSLRenderer(cvs, options) {
|
|
432
422
|
const renderer = new WGSLRenderer(cvs, options);
|