3d-spinner 0.9.2 → 0.9.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.
Files changed (97) hide show
  1. package/README.md +66 -4
  2. package/dist/animation-label.d.ts +8 -0
  3. package/dist/animation-label.js +47 -0
  4. package/dist/animation.d.ts +2 -0
  5. package/dist/animations/charged-orb.d.ts +55 -0
  6. package/dist/animations/charged-orb.js +229 -0
  7. package/dist/animations/grid-assembly.d.ts +67 -0
  8. package/dist/animations/grid-assembly.js +268 -0
  9. package/dist/animations/object-motion.d.ts +19 -7
  10. package/dist/animations/object-motion.js +38 -32
  11. package/dist/animations/particles.d.ts +110 -0
  12. package/dist/animations/particles.js +209 -0
  13. package/dist/cjs/animations/charged-orb.cjs +1334 -0
  14. package/dist/cjs/animations/grid-assembly.cjs +1498 -0
  15. package/dist/cjs/animations/object-motion.cjs +124 -43
  16. package/dist/cjs/animations/particles.cjs +1940 -0
  17. package/dist/cjs/animations/spin.cjs +4 -3
  18. package/dist/cjs/composite-animation.cjs +58 -0
  19. package/dist/cjs/engines/little-3d-engine/little-3d-engine.cjs +131 -22
  20. package/dist/cjs/engines/little-3d-engine/loaders/obj.cjs +30 -1
  21. package/dist/cjs/engines/little-3d-engine/renderers/canvas2d-textured.cjs +345 -0
  22. package/dist/cjs/engines/little-3d-engine/renderers/webgl-textured.cjs +528 -0
  23. package/dist/cjs/engines/little-3d-engine/renderers/webgpu-textured.cjs +712 -0
  24. package/dist/cjs/prefabs/prefabs.cjs +3450 -0
  25. package/dist/composite-animation.d.ts +18 -0
  26. package/dist/composite-animation.js +39 -0
  27. package/dist/engines/little-3d-engine/little-3d-engine.d.ts +17 -12
  28. package/dist/engines/little-3d-engine/little-3d-engine.js +13 -8
  29. package/dist/engines/little-3d-engine/loaders/obj.d.ts +12 -4
  30. package/dist/engines/little-3d-engine/loaders/obj.js +44 -5
  31. package/dist/engines/little-3d-engine/renderer.d.ts +7 -1
  32. package/dist/engines/little-3d-engine/renderer.js +2 -0
  33. package/dist/engines/little-3d-engine/renderers/canvas2d-textured.d.ts +20 -0
  34. package/dist/engines/little-3d-engine/renderers/canvas2d-textured.js +135 -0
  35. package/dist/engines/little-3d-engine/renderers/textured-helpers.d.ts +9 -0
  36. package/dist/engines/little-3d-engine/renderers/textured-helpers.js +35 -0
  37. package/dist/engines/little-3d-engine/renderers/webgl-textured.d.ts +43 -0
  38. package/dist/engines/little-3d-engine/renderers/webgl-textured.js +211 -0
  39. package/dist/engines/little-3d-engine/renderers/webgpu-textured.d.ts +45 -0
  40. package/dist/engines/little-3d-engine/renderers/webgpu-textured.js +300 -0
  41. package/dist/engines/little-3d-engine/renderers/webgpu.d.ts +11 -6
  42. package/dist/engines/little-3d-engine/shapes/complex/plane.d.ts +3 -0
  43. package/dist/engines/little-3d-engine/shapes/complex/plane.js +33 -0
  44. package/dist/engines/little-3d-engine/shapes/{cube.d.ts → primitives/cube.d.ts} +1 -1
  45. package/dist/engines/little-3d-engine/shapes/{octahedron.d.ts → primitives/octahedron.d.ts} +1 -1
  46. package/dist/engines/little-3d-engine/shapes/{pyramid.d.ts → primitives/pyramid.d.ts} +1 -1
  47. package/dist/engines/little-3d-engine/shapes/primitives/quad.d.ts +9 -0
  48. package/dist/engines/little-3d-engine/shapes/primitives/quad.js +18 -0
  49. package/dist/engines/little-3d-engine/shapes/{cube-sphere.d.ts → primitives/spheres/cube-sphere.d.ts} +1 -1
  50. package/dist/engines/little-3d-engine/shapes/{icosphere.d.ts → primitives/spheres/icosphere.d.ts} +1 -1
  51. package/dist/engines/little-3d-engine/shapes/{icosphere.js → primitives/spheres/icosphere.js} +1 -1
  52. package/dist/engines/little-3d-engine/shapes/{octa-sphere.d.ts → primitives/spheres/octa-sphere.d.ts} +1 -1
  53. package/dist/engines/little-3d-engine/shapes/{octa-sphere.js → primitives/spheres/octa-sphere.js} +1 -1
  54. package/dist/engines/little-3d-engine/shapes/{uv-sphere.d.ts → primitives/spheres/uv-sphere.d.ts} +1 -1
  55. package/dist/engines/little-3d-engine/shapes/{tetrahedron.d.ts → primitives/tetrahedron.d.ts} +1 -1
  56. package/dist/engines/little-3d-engine/textures/dynamic/canvas-texture.d.ts +1 -0
  57. package/dist/engines/little-3d-engine/textures/dynamic/canvas-texture.js +8 -0
  58. package/dist/engines/little-3d-engine/textures/dynamic/shine.d.ts +2 -0
  59. package/dist/engines/little-3d-engine/textures/dynamic/shine.js +14 -0
  60. package/dist/engines/little-3d-engine/textures/dynamic/star.d.ts +2 -0
  61. package/dist/engines/little-3d-engine/textures/dynamic/star.js +16 -0
  62. package/dist/engines/little-3d-engine/textures/dynamic/streak.d.ts +5 -0
  63. package/dist/engines/little-3d-engine/textures/dynamic/streak.js +20 -0
  64. package/dist/index.d.ts +2 -1
  65. package/dist/prefabs/charged-orb.d.ts +17 -0
  66. package/dist/prefabs/charged-orb.js +37 -0
  67. package/dist/prefabs/crystal-comet.d.ts +4 -0
  68. package/dist/prefabs/crystal-comet.js +41 -0
  69. package/dist/prefabs/ghost-train.d.ts +9 -0
  70. package/dist/prefabs/ghost-train.js +44 -0
  71. package/dist/prefabs/grid-assembly.d.ts +13 -0
  72. package/dist/prefabs/grid-assembly.js +15 -0
  73. package/dist/prefabs/monochrome-streak.d.ts +4 -0
  74. package/dist/prefabs/monochrome-streak.js +25 -0
  75. package/dist/prefabs/plane-star-trail.d.ts +4 -0
  76. package/dist/prefabs/plane-star-trail.js +40 -0
  77. package/dist/prefabs/prefabs.d.ts +10 -0
  78. package/dist/prefabs/prefabs.js +9 -0
  79. package/dist/prefabs/pulsing-starfield.d.ts +4 -0
  80. package/dist/prefabs/pulsing-starfield.js +27 -0
  81. package/dist/prefabs/rocket-launch.d.ts +8 -0
  82. package/dist/prefabs/rocket-launch.js +59 -0
  83. package/dist/prefabs/spinner.d.ts +7 -0
  84. package/dist/prefabs/spinner.js +17 -0
  85. package/dist/prefabs/star-swarm.d.ts +4 -0
  86. package/dist/prefabs/star-swarm.js +27 -0
  87. package/dist/prefabs/types.d.ts +46 -0
  88. package/dist/prefabs/types.js +1 -0
  89. package/dist/umd/spinner.global.js +3055 -1251
  90. package/dist/umd/spinner.global.min.js +60 -5
  91. package/package.json +42 -1
  92. /package/dist/engines/little-3d-engine/shapes/{cube.js → primitives/cube.js} +0 -0
  93. /package/dist/engines/little-3d-engine/shapes/{octahedron.js → primitives/octahedron.js} +0 -0
  94. /package/dist/engines/little-3d-engine/shapes/{pyramid.js → primitives/pyramid.js} +0 -0
  95. /package/dist/engines/little-3d-engine/shapes/{cube-sphere.js → primitives/spheres/cube-sphere.js} +0 -0
  96. /package/dist/engines/little-3d-engine/shapes/{uv-sphere.js → primitives/spheres/uv-sphere.js} +0 -0
  97. /package/dist/engines/little-3d-engine/shapes/{tetrahedron.js → primitives/tetrahedron.js} +0 -0
@@ -0,0 +1,211 @@
1
+ import { expandToTriangles } from "../core/geometry.js";
2
+ import { DEFAULT_ONE_SIDED_OPACITY, opacity, resolveTwoSidedOpacity, } from "../renderer.js";
3
+ import { planarUVs } from "./textured-helpers.js";
4
+ import { WebGLRenderer } from "./webgl.js";
5
+ const VERTEX_SHADER = `#version 300 es
6
+ in vec3 aPos;
7
+ in vec2 aUV;
8
+ in vec3 aColor;
9
+ uniform mat4 uViewProj;
10
+ uniform mat4 uModel;
11
+ out vec2 vUV;
12
+ out vec3 vColor;
13
+ void main() {
14
+ vUV = aUV;
15
+ vColor = aColor;
16
+ gl_Position = uViewProj * uModel * vec4(aPos, 1.0);
17
+ }`;
18
+ const FRAGMENT_SHADER = `#version 300 es
19
+ precision mediump float;
20
+ in vec2 vUV;
21
+ in vec3 vColor;
22
+ uniform sampler2D uTexture;
23
+ uniform float uOpacity;
24
+ out vec4 fragColor;
25
+ void main() {
26
+ vec4 t = texture(uTexture, vUV);
27
+ fragColor = vec4(t.rgb * vColor, t.a * uOpacity);
28
+ }`;
29
+ function compile(gl, type, source) {
30
+ const shader = gl.createShader(type);
31
+ gl.shaderSource(shader, source);
32
+ gl.compileShader(shader);
33
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
34
+ throw new Error(`3d-spinner: shader compile failed: ${gl.getShaderInfoLog(shader)}`);
35
+ }
36
+ return shader;
37
+ }
38
+ function link(gl) {
39
+ const program = gl.createProgram();
40
+ gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER));
41
+ gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER));
42
+ gl.linkProgram(program);
43
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
44
+ throw new Error(`3d-spinner: program link failed: ${gl.getProgramInfoLog(program)}`);
45
+ }
46
+ return program;
47
+ }
48
+ function itemOpacity(transparency) {
49
+ if (!transparency)
50
+ return 1;
51
+ if (transparency.mode === "two-sided")
52
+ return resolveTwoSidedOpacity(transparency).front;
53
+ return opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY);
54
+ }
55
+ /**
56
+ * WebGL renderer with image textures on registered meshes; unregistered
57
+ * meshes render exactly as in the plain WebGL backend. Made for billboard
58
+ * quads: UVs are a planar projection of the mesh's XY extent, textured faces
59
+ * render unlit with the texture tinted by the face color, and textured
60
+ * instances draw after the other transparent instances. Until an image URL
61
+ * finishes loading its mesh renders with a plain white texture.
62
+ *
63
+ * Pass it to the engine (or an animation) as a `backend` factory so the
64
+ * module is only fetched when used:
65
+ *
66
+ * ```js
67
+ * backend: async (options) => {
68
+ * const { WebGLTexturedRenderer } =
69
+ * await import("3d-spinner/engines/little-3d-engine/renderers/webgl-textured");
70
+ * const renderer = new WebGLTexturedRenderer(options);
71
+ * renderer.setTexture(mesh, "/sprite.png");
72
+ * return renderer;
73
+ * }
74
+ * ```
75
+ */
76
+ export class WebGLTexturedRenderer {
77
+ constructor(options = {}) {
78
+ this.sources = new Map();
79
+ this.textures = new Map();
80
+ this.buffers = new Map();
81
+ this.inner = new WebGLRenderer(options);
82
+ }
83
+ /** Texture every instance of `mesh` with `source`. Call any time, also before init. */
84
+ setTexture(mesh, source) {
85
+ this.sources.set(mesh, source);
86
+ }
87
+ init(canvas) {
88
+ this.inner.init(canvas);
89
+ const gl = canvas.getContext("webgl2");
90
+ this.gl = gl;
91
+ this.program = link(gl);
92
+ this.locations = {
93
+ aPos: gl.getAttribLocation(this.program, "aPos"),
94
+ aUV: gl.getAttribLocation(this.program, "aUV"),
95
+ aColor: gl.getAttribLocation(this.program, "aColor"),
96
+ uViewProj: gl.getUniformLocation(this.program, "uViewProj"),
97
+ uModel: gl.getUniformLocation(this.program, "uModel"),
98
+ uTexture: gl.getUniformLocation(this.program, "uTexture"),
99
+ uOpacity: gl.getUniformLocation(this.program, "uOpacity"),
100
+ };
101
+ }
102
+ resize() {
103
+ this.inner.resize();
104
+ }
105
+ textureFor(mesh) {
106
+ const cached = this.textures.get(mesh);
107
+ if (cached)
108
+ return cached;
109
+ const gl = this.gl;
110
+ const texture = gl.createTexture();
111
+ gl.bindTexture(gl.TEXTURE_2D, texture);
112
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 255, 255, 255]));
113
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
114
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
115
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
116
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
117
+ this.textures.set(mesh, texture);
118
+ const upload = (image) => {
119
+ if (!this.gl || this.textures.get(mesh) !== texture)
120
+ return;
121
+ this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
122
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, image);
123
+ };
124
+ const source = this.sources.get(mesh);
125
+ if (typeof source === "string") {
126
+ const image = new Image();
127
+ image.onload = () => upload(image);
128
+ image.src = source;
129
+ }
130
+ else {
131
+ upload(source);
132
+ }
133
+ return texture;
134
+ }
135
+ buffersFor(mesh) {
136
+ const cached = this.buffers.get(mesh);
137
+ if (cached)
138
+ return cached;
139
+ const gl = this.gl;
140
+ const loc = this.locations;
141
+ const data = expandToTriangles(mesh);
142
+ const vao = gl.createVertexArray();
143
+ gl.bindVertexArray(vao);
144
+ const attribute = (location, array, size) => {
145
+ if (location < 0)
146
+ return;
147
+ const buffer = gl.createBuffer();
148
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
149
+ gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
150
+ gl.enableVertexAttribArray(location);
151
+ gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
152
+ };
153
+ attribute(loc.aPos, data.positions, 3);
154
+ attribute(loc.aColor, data.colors, 3);
155
+ attribute(loc.aUV, planarUVs(mesh), 2);
156
+ gl.bindVertexArray(null);
157
+ const result = { vao, count: data.count };
158
+ this.buffers.set(mesh, result);
159
+ return result;
160
+ }
161
+ render(frame) {
162
+ const plain = [];
163
+ const textured = [];
164
+ for (const item of frame.items) {
165
+ (this.sources.has(item.mesh) ? textured : plain).push(item);
166
+ }
167
+ this.inner.render(textured.length ? { ...frame, items: plain } : frame);
168
+ if (!textured.length)
169
+ return;
170
+ const gl = this.gl;
171
+ const loc = this.locations;
172
+ if (!gl || !this.program || !loc)
173
+ return;
174
+ gl.useProgram(this.program);
175
+ gl.uniformMatrix4fv(loc.uViewProj, false, new Float32Array(frame.viewProjection));
176
+ gl.uniform1i(loc.uTexture, 0);
177
+ gl.activeTexture(gl.TEXTURE0);
178
+ gl.enable(gl.BLEND);
179
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
180
+ gl.depthMask(false);
181
+ for (const item of textured) {
182
+ const buffers = this.buffersFor(item.mesh);
183
+ gl.bindTexture(gl.TEXTURE_2D, this.textureFor(item.mesh));
184
+ gl.uniformMatrix4fv(loc.uModel, false, new Float32Array(item.model));
185
+ gl.uniform1f(loc.uOpacity, itemOpacity(item.transparency));
186
+ gl.bindVertexArray(buffers.vao);
187
+ gl.drawArrays(gl.TRIANGLES, 0, buffers.count);
188
+ }
189
+ gl.depthMask(true);
190
+ gl.disable(gl.BLEND);
191
+ gl.bindVertexArray(null);
192
+ }
193
+ destroy() {
194
+ const gl = this.gl;
195
+ if (gl) {
196
+ for (const texture of this.textures.values())
197
+ gl.deleteTexture(texture);
198
+ for (const buffers of this.buffers.values())
199
+ gl.deleteVertexArray(buffers.vao);
200
+ if (this.program)
201
+ gl.deleteProgram(this.program);
202
+ }
203
+ this.textures.clear();
204
+ this.buffers.clear();
205
+ this.sources.clear();
206
+ this.gl = undefined;
207
+ this.program = undefined;
208
+ this.locations = undefined;
209
+ this.inner.destroy();
210
+ }
211
+ }
@@ -0,0 +1,45 @@
1
+ import type { Mesh } from "../core/mesh.js";
2
+ import { type RenderFrame } from "../renderer.js";
3
+ import { type TextureSource } from "./textured-helpers.js";
4
+ import { WebGPURenderer } from "./webgpu.js";
5
+ export type { TextureSource } from "./textured-helpers.js";
6
+ /**
7
+ * WebGPU renderer with image textures on registered meshes; unregistered
8
+ * meshes render exactly as in the plain WebGPU backend. Made for billboard
9
+ * quads: UVs are a planar projection of the mesh's XY extent, textured faces
10
+ * render unlit with the texture tinted by the face color, and textured
11
+ * instances draw after the other transparent instances. Until an image URL
12
+ * finishes loading its mesh renders with a plain white texture.
13
+ *
14
+ * Pass it to the engine (or an animation) as a `backend` factory so the
15
+ * module is only fetched when used:
16
+ *
17
+ * ```js
18
+ * backend: async (options) => {
19
+ * const { WebGPUTexturedRenderer } =
20
+ * await import("3d-spinner/engines/little-3d-engine/renderers/webgpu-textured");
21
+ * const renderer = new WebGPUTexturedRenderer(options);
22
+ * renderer.setTexture(mesh, "/sprite.png");
23
+ * return renderer;
24
+ * }
25
+ * ```
26
+ */
27
+ export declare class WebGPUTexturedRenderer extends WebGPURenderer {
28
+ private texturedPipeline;
29
+ private sampler;
30
+ private texturedUniforms;
31
+ private texturedCapacity;
32
+ private readonly sources;
33
+ private readonly textures;
34
+ private readonly retired;
35
+ private readonly texturedBuffers;
36
+ private readonly bindGroups;
37
+ /** Texture every instance of `mesh` with `source`. Call any time, also before init. */
38
+ setTexture(mesh: Mesh, source: TextureSource): void;
39
+ init(canvas: HTMLCanvasElement): Promise<void>;
40
+ private textureFor;
41
+ private buffersFor;
42
+ private bindGroupFor;
43
+ render(frame: RenderFrame): void;
44
+ destroy(): void;
45
+ }
@@ -0,0 +1,300 @@
1
+ import { expandToTriangles } from "../core/geometry.js";
2
+ import { multiply } from "../core/math.js";
3
+ import { DEFAULT_ONE_SIDED_OPACITY, opacity, resolveTwoSidedOpacity, } from "../renderer.js";
4
+ import { planarUVs } from "./textured-helpers.js";
5
+ import { WebGPURenderer } from "./webgpu.js";
6
+ const WGSL = `
7
+ struct Uniforms {
8
+ viewProj: mat4x4<f32>,
9
+ model: mat4x4<f32>,
10
+ params: vec4<f32>,
11
+ };
12
+ @group(0) @binding(0) var<uniform> u: Uniforms;
13
+ @group(0) @binding(1) var tex: texture_2d<f32>;
14
+ @group(0) @binding(2) var samp: sampler;
15
+
16
+ struct VSOut {
17
+ @builtin(position) position: vec4<f32>,
18
+ @location(0) uv: vec2<f32>,
19
+ @location(1) color: vec3<f32>,
20
+ };
21
+
22
+ @vertex
23
+ fn vs(@location(0) pos: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) color: vec3<f32>) -> VSOut {
24
+ var out: VSOut;
25
+ out.uv = uv;
26
+ out.color = color;
27
+ out.position = u.viewProj * u.model * vec4<f32>(pos, 1.0);
28
+ return out;
29
+ }
30
+
31
+ @fragment
32
+ fn fs(in: VSOut) -> @location(0) vec4<f32> {
33
+ let t = textureSample(tex, samp, in.uv);
34
+ return vec4<f32>(t.rgb * in.color, t.a * u.params.x);
35
+ }
36
+ `;
37
+ // Maps OpenGL clip space (z in -1..1) to WebGPU clip space (z in 0..1).
38
+ const CLIP_Z_FIX = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1];
39
+ const UNIFORM_STRIDE = 256;
40
+ function itemOpacity(transparency) {
41
+ if (!transparency)
42
+ return 1;
43
+ if (transparency.mode === "two-sided")
44
+ return resolveTwoSidedOpacity(transparency).front;
45
+ return opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY);
46
+ }
47
+ /**
48
+ * WebGPU renderer with image textures on registered meshes; unregistered
49
+ * meshes render exactly as in the plain WebGPU backend. Made for billboard
50
+ * quads: UVs are a planar projection of the mesh's XY extent, textured faces
51
+ * render unlit with the texture tinted by the face color, and textured
52
+ * instances draw after the other transparent instances. Until an image URL
53
+ * finishes loading its mesh renders with a plain white texture.
54
+ *
55
+ * Pass it to the engine (or an animation) as a `backend` factory so the
56
+ * module is only fetched when used:
57
+ *
58
+ * ```js
59
+ * backend: async (options) => {
60
+ * const { WebGPUTexturedRenderer } =
61
+ * await import("3d-spinner/engines/little-3d-engine/renderers/webgpu-textured");
62
+ * const renderer = new WebGPUTexturedRenderer(options);
63
+ * renderer.setTexture(mesh, "/sprite.png");
64
+ * return renderer;
65
+ * }
66
+ * ```
67
+ */
68
+ export class WebGPUTexturedRenderer extends WebGPURenderer {
69
+ constructor() {
70
+ super(...arguments);
71
+ this.texturedCapacity = 0;
72
+ this.sources = new Map();
73
+ this.textures = new Map();
74
+ this.retired = [];
75
+ this.texturedBuffers = new Map();
76
+ this.bindGroups = new Map();
77
+ }
78
+ /** Texture every instance of `mesh` with `source`. Call any time, also before init. */
79
+ setTexture(mesh, source) {
80
+ this.sources.set(mesh, source);
81
+ }
82
+ async init(canvas) {
83
+ await super.init(canvas);
84
+ const device = this.device;
85
+ if (!device)
86
+ return;
87
+ const format = navigator.gpu.getPreferredCanvasFormat();
88
+ const module = device.createShaderModule({ code: WGSL });
89
+ const stage = globalThis.GPUShaderStage;
90
+ const layout = device.createBindGroupLayout({
91
+ entries: [
92
+ {
93
+ binding: 0,
94
+ visibility: stage.VERTEX | stage.FRAGMENT,
95
+ buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 144 },
96
+ },
97
+ { binding: 1, visibility: stage.FRAGMENT, texture: {} },
98
+ { binding: 2, visibility: stage.FRAGMENT, sampler: {} },
99
+ ],
100
+ });
101
+ const vertexBuffer = (location, components) => ({
102
+ arrayStride: components * 4,
103
+ attributes: [{ shaderLocation: location, offset: 0, format: `float32x${components}` }],
104
+ });
105
+ this.texturedPipeline = device.createRenderPipeline({
106
+ layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
107
+ vertex: {
108
+ module,
109
+ entryPoint: "vs",
110
+ buffers: [vertexBuffer(0, 3), vertexBuffer(1, 2), vertexBuffer(2, 3)],
111
+ },
112
+ fragment: {
113
+ module,
114
+ entryPoint: "fs",
115
+ targets: [
116
+ {
117
+ format,
118
+ blend: {
119
+ color: {
120
+ srcFactor: "src-alpha",
121
+ dstFactor: "one-minus-src-alpha",
122
+ operation: "add",
123
+ },
124
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
125
+ },
126
+ },
127
+ ],
128
+ },
129
+ primitive: { topology: "triangle-list", cullMode: "back", frontFace: "ccw" },
130
+ depthStencil: {
131
+ format: "depth24plus",
132
+ depthWriteEnabled: false,
133
+ depthCompare: "less",
134
+ },
135
+ });
136
+ this.sampler = device.createSampler({ magFilter: "linear", minFilter: "linear" });
137
+ }
138
+ textureFor(mesh) {
139
+ const cached = this.textures.get(mesh);
140
+ if (cached)
141
+ return cached;
142
+ const device = this.device;
143
+ const usage = globalThis.GPUTextureUsage;
144
+ const white = device.createTexture({
145
+ size: { width: 1, height: 1 },
146
+ format: "rgba8unorm",
147
+ usage: usage.TEXTURE_BINDING | usage.COPY_DST,
148
+ });
149
+ device.queue.writeTexture({ texture: white }, new Uint8Array([255, 255, 255, 255]), {}, { width: 1, height: 1 });
150
+ this.textures.set(mesh, white);
151
+ const upload = async (source) => {
152
+ const image = source instanceof HTMLImageElement ? await createImageBitmap(source) : source;
153
+ if (this.destroyed || !this.device || this.textures.get(mesh) !== white)
154
+ return;
155
+ const width = image.width || 1;
156
+ const height = image.height || 1;
157
+ const texture = this.device.createTexture({
158
+ size: { width, height },
159
+ format: "rgba8unorm",
160
+ usage: usage.TEXTURE_BINDING | usage.COPY_DST | usage.RENDER_ATTACHMENT,
161
+ });
162
+ this.device.queue.copyExternalImageToTexture({ source: image }, { texture }, { width, height });
163
+ // The placeholder may still be referenced by an unsubmitted command
164
+ // buffer, so it is retired here and destroyed with the renderer.
165
+ this.retired.push(white);
166
+ this.textures.set(mesh, texture);
167
+ this.bindGroups.delete(mesh);
168
+ };
169
+ const source = this.sources.get(mesh);
170
+ if (typeof source === "string") {
171
+ const image = new Image();
172
+ image.onload = () => void upload(image);
173
+ image.src = source;
174
+ }
175
+ else {
176
+ void upload(source);
177
+ }
178
+ return this.textures.get(mesh);
179
+ }
180
+ buffersFor(mesh) {
181
+ const cached = this.texturedBuffers.get(mesh);
182
+ if (cached)
183
+ return cached;
184
+ const data = expandToTriangles(mesh);
185
+ const usage = globalThis.GPUBufferUsage.VERTEX | globalThis.GPUBufferUsage.COPY_DST;
186
+ const upload = (array) => {
187
+ const buffer = this.device.createBuffer({ size: array.byteLength, usage });
188
+ this.device.queue.writeBuffer(buffer, 0, array);
189
+ return buffer;
190
+ };
191
+ const result = {
192
+ position: upload(data.positions),
193
+ uv: upload(planarUVs(mesh)),
194
+ color: upload(data.colors),
195
+ count: data.count,
196
+ };
197
+ this.texturedBuffers.set(mesh, result);
198
+ return result;
199
+ }
200
+ bindGroupFor(mesh) {
201
+ const texture = this.textureFor(mesh);
202
+ const cached = this.bindGroups.get(mesh);
203
+ if (cached && cached.buffer === this.texturedUniforms && cached.texture === texture) {
204
+ return cached.group;
205
+ }
206
+ const group = this.device.createBindGroup({
207
+ layout: this.texturedPipeline.getBindGroupLayout(0),
208
+ entries: [
209
+ { binding: 0, resource: { buffer: this.texturedUniforms, offset: 0, size: 144 } },
210
+ { binding: 1, resource: texture.createView() },
211
+ { binding: 2, resource: this.sampler },
212
+ ],
213
+ });
214
+ this.bindGroups.set(mesh, { group, buffer: this.texturedUniforms, texture });
215
+ return group;
216
+ }
217
+ render(frame) {
218
+ const plain = [];
219
+ const textured = [];
220
+ for (const item of frame.items) {
221
+ (this.sources.has(item.mesh) ? textured : plain).push(item);
222
+ }
223
+ super.render(textured.length ? { ...frame, items: plain } : frame);
224
+ if (!textured.length)
225
+ return;
226
+ if (this.destroyed || !this.device || !this.context || !this.texturedPipeline)
227
+ return;
228
+ if (frame.width === 0 || frame.height === 0)
229
+ return;
230
+ this.ensureDepth();
231
+ if (textured.length > this.texturedCapacity || !this.texturedUniforms) {
232
+ this.texturedUniforms?.destroy?.();
233
+ this.texturedUniforms = this.device.createBuffer({
234
+ size: textured.length * UNIFORM_STRIDE,
235
+ usage: globalThis.GPUBufferUsage.UNIFORM |
236
+ globalThis.GPUBufferUsage.COPY_DST,
237
+ });
238
+ this.texturedCapacity = textured.length;
239
+ }
240
+ const viewProj = multiply(CLIP_Z_FIX, frame.viewProjection);
241
+ textured.forEach((item, i) => {
242
+ const data = new Float32Array(UNIFORM_STRIDE / 4);
243
+ data.set(viewProj, 0);
244
+ data.set(item.model, 16);
245
+ data.set([itemOpacity(item.transparency), 0, 0, 0], 32);
246
+ this.device.queue.writeBuffer(this.texturedUniforms, i * UNIFORM_STRIDE, data);
247
+ });
248
+ // The base pass skips entirely when it has no items, so this pass clears
249
+ // the attachments in that case instead of loading them.
250
+ const cleared = plain.length > 0;
251
+ const encoder = this.device.createCommandEncoder();
252
+ const pass = encoder.beginRenderPass({
253
+ colorAttachments: [
254
+ {
255
+ view: this.context.getCurrentTexture().createView(),
256
+ clearValue: this.clearValue,
257
+ loadOp: cleared ? "load" : "clear",
258
+ storeOp: "store",
259
+ },
260
+ ],
261
+ depthStencilAttachment: {
262
+ view: this.depthTexture.createView(),
263
+ depthClearValue: 1,
264
+ depthLoadOp: cleared ? "load" : "clear",
265
+ depthStoreOp: "store",
266
+ },
267
+ });
268
+ pass.setPipeline(this.texturedPipeline);
269
+ textured.forEach((item, i) => {
270
+ const mesh = this.buffersFor(item.mesh);
271
+ pass.setBindGroup(0, this.bindGroupFor(item.mesh), [i * UNIFORM_STRIDE]);
272
+ pass.setVertexBuffer(0, mesh.position);
273
+ pass.setVertexBuffer(1, mesh.uv);
274
+ pass.setVertexBuffer(2, mesh.color);
275
+ pass.draw(mesh.count);
276
+ });
277
+ pass.end();
278
+ this.device.queue.submit([encoder.finish()]);
279
+ }
280
+ destroy() {
281
+ for (const texture of this.textures.values())
282
+ texture.destroy?.();
283
+ for (const texture of this.retired.splice(0))
284
+ texture.destroy?.();
285
+ for (const buffers of this.texturedBuffers.values()) {
286
+ buffers.position.destroy?.();
287
+ buffers.uv.destroy?.();
288
+ buffers.color.destroy?.();
289
+ }
290
+ this.textures.clear();
291
+ this.texturedBuffers.clear();
292
+ this.bindGroups.clear();
293
+ this.sources.clear();
294
+ this.texturedUniforms?.destroy?.();
295
+ this.texturedUniforms = undefined;
296
+ this.texturedPipeline = undefined;
297
+ this.sampler = undefined;
298
+ super.destroy();
299
+ }
300
+ }
@@ -2,23 +2,28 @@ import { type Renderer, type RenderFrame, type RendererOptions } from "../render
2
2
  /** Hardware renderer using WebGPU: GPU transforms with a real depth buffer. */
3
3
  export declare class WebGPURenderer implements Renderer {
4
4
  private canvas?;
5
- private device;
6
- private context;
5
+ protected device: any;
6
+ protected context: any;
7
7
  private pipeline;
8
8
  private transparentBackPipeline;
9
9
  private transparentFrontPipeline;
10
10
  private uniformBuffer;
11
11
  private uniformCapacity;
12
- private depthTexture;
12
+ protected depthTexture: any;
13
13
  private depthSize;
14
- private destroyed;
14
+ protected destroyed: boolean;
15
15
  private readonly cache;
16
- private readonly clearValue;
16
+ protected readonly clearValue: {
17
+ r: number;
18
+ g: number;
19
+ b: number;
20
+ a: number;
21
+ };
17
22
  private readonly alphaMode;
18
23
  constructor(options?: RendererOptions);
19
24
  init(canvas: HTMLCanvasElement): Promise<void>;
20
25
  resize(): void;
21
- private ensureDepth;
26
+ protected ensureDepth(): void;
22
27
  private buffers;
23
28
  private ensureUniformCapacity;
24
29
  render(frame: RenderFrame): void;
@@ -0,0 +1,3 @@
1
+ import type { Mesh } from "../../core/mesh.js";
2
+ /** Build a low-poly plane mesh pointing along the positive X axis. */
3
+ export declare function planeMesh(colors?: string[]): Mesh;
@@ -0,0 +1,33 @@
1
+ const DEFAULT_COLORS = ["#e0f2fe", "#7dd3fc", "#38bdf8", "#f8fafc"];
2
+ /** Build a low-poly plane mesh pointing along the positive X axis. */
3
+ export function planeMesh(colors = DEFAULT_COLORS) {
4
+ return {
5
+ vertices: [
6
+ { x: 0.9, y: 0, z: 0 },
7
+ { x: -0.2, y: 0, z: 0.82 },
8
+ { x: -0.55, y: 0, z: 0.16 },
9
+ { x: -0.72, y: 0, z: 0 },
10
+ { x: -0.55, y: 0, z: -0.16 },
11
+ { x: -0.2, y: 0, z: -0.82 },
12
+ { x: -0.08, y: 0.12, z: 0 },
13
+ { x: -0.08, y: -0.1, z: 0 },
14
+ { x: -0.52, y: 0.38, z: 0 },
15
+ ],
16
+ faces: [
17
+ { indices: [6, 1, 0], color: colors[0] ?? DEFAULT_COLORS[0] },
18
+ { indices: [6, 2, 1], color: colors[3] ?? DEFAULT_COLORS[3] },
19
+ { indices: [6, 3, 2], color: colors[1] ?? DEFAULT_COLORS[1] },
20
+ { indices: [6, 4, 3], color: colors[2] ?? DEFAULT_COLORS[2] },
21
+ { indices: [6, 5, 4], color: colors[3] ?? DEFAULT_COLORS[3] },
22
+ { indices: [6, 0, 5], color: colors[0] ?? DEFAULT_COLORS[0] },
23
+ { indices: [7, 0, 1], color: colors[1] ?? DEFAULT_COLORS[1] },
24
+ { indices: [7, 1, 2], color: colors[2] ?? DEFAULT_COLORS[2] },
25
+ { indices: [7, 2, 3], color: colors[1] ?? DEFAULT_COLORS[1] },
26
+ { indices: [7, 3, 4], color: colors[2] ?? DEFAULT_COLORS[2] },
27
+ { indices: [7, 4, 5], color: colors[1] ?? DEFAULT_COLORS[1] },
28
+ { indices: [7, 5, 0], color: colors[2] ?? DEFAULT_COLORS[2] },
29
+ { indices: [3, 6, 8], color: colors[0] ?? DEFAULT_COLORS[0] },
30
+ { indices: [3, 8, 6], color: colors[1] ?? DEFAULT_COLORS[1] },
31
+ ],
32
+ };
33
+ }
@@ -1,4 +1,4 @@
1
- import type { Mesh } from "../core/mesh.js";
1
+ import type { Mesh } from "../../core/mesh.js";
2
2
  /**
3
3
  * Build a cube mesh centered on the origin.
4
4
  *
@@ -1,4 +1,4 @@
1
- import type { Mesh } from "../core/mesh.js";
1
+ import type { Mesh } from "../../core/mesh.js";
2
2
  /**
3
3
  * Build a regular octahedron mesh centered on the origin.
4
4
  *
@@ -1,4 +1,4 @@
1
- import type { Mesh } from "../core/mesh.js";
1
+ import type { Mesh } from "../../core/mesh.js";
2
2
  /**
3
3
  * Build a square pyramid mesh centered on the origin, base down.
4
4
  *
@@ -0,0 +1,9 @@
1
+ import type { Mesh } from "../../core/mesh.js";
2
+ /**
3
+ * Build a flat square in the XY plane, facing +Z, as a single four-vertex
4
+ * face. Useful as a billboard when kept facing the camera.
5
+ *
6
+ * @param size Edge length. Defaults to `1`.
7
+ * @param colors CSS color for the face. Defaults to a built-in blue.
8
+ */
9
+ export declare function quad(size?: number, colors?: string[]): Mesh;
@@ -0,0 +1,18 @@
1
+ const DEFAULT_COLORS = ["#3b82f6"];
2
+ /**
3
+ * Build a flat square in the XY plane, facing +Z, as a single four-vertex
4
+ * face. Useful as a billboard when kept facing the camera.
5
+ *
6
+ * @param size Edge length. Defaults to `1`.
7
+ * @param colors CSS color for the face. Defaults to a built-in blue.
8
+ */
9
+ export function quad(size = 1, colors = DEFAULT_COLORS) {
10
+ const s = size / 2;
11
+ const vertices = [
12
+ { x: -s, y: -s, z: 0 },
13
+ { x: s, y: -s, z: 0 },
14
+ { x: s, y: s, z: 0 },
15
+ { x: -s, y: s, z: 0 },
16
+ ];
17
+ return { vertices, faces: [{ indices: [0, 1, 2, 3], color: colors[0] }] };
18
+ }