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.
- package/README.md +66 -4
- package/dist/animation-label.d.ts +8 -0
- package/dist/animation-label.js +47 -0
- package/dist/animation.d.ts +2 -0
- package/dist/animations/charged-orb.d.ts +55 -0
- package/dist/animations/charged-orb.js +229 -0
- package/dist/animations/grid-assembly.d.ts +67 -0
- package/dist/animations/grid-assembly.js +268 -0
- package/dist/animations/object-motion.d.ts +19 -7
- package/dist/animations/object-motion.js +38 -32
- package/dist/animations/particles.d.ts +110 -0
- package/dist/animations/particles.js +209 -0
- package/dist/cjs/animations/charged-orb.cjs +1334 -0
- package/dist/cjs/animations/grid-assembly.cjs +1498 -0
- package/dist/cjs/animations/object-motion.cjs +124 -43
- package/dist/cjs/animations/particles.cjs +1940 -0
- package/dist/cjs/animations/spin.cjs +4 -3
- package/dist/cjs/composite-animation.cjs +58 -0
- package/dist/cjs/engines/little-3d-engine/little-3d-engine.cjs +131 -22
- package/dist/cjs/engines/little-3d-engine/loaders/obj.cjs +30 -1
- package/dist/cjs/engines/little-3d-engine/renderers/canvas2d-textured.cjs +345 -0
- package/dist/cjs/engines/little-3d-engine/renderers/webgl-textured.cjs +528 -0
- package/dist/cjs/engines/little-3d-engine/renderers/webgpu-textured.cjs +712 -0
- package/dist/cjs/prefabs/prefabs.cjs +3450 -0
- package/dist/composite-animation.d.ts +18 -0
- package/dist/composite-animation.js +39 -0
- package/dist/engines/little-3d-engine/little-3d-engine.d.ts +17 -12
- package/dist/engines/little-3d-engine/little-3d-engine.js +13 -8
- package/dist/engines/little-3d-engine/loaders/obj.d.ts +12 -4
- package/dist/engines/little-3d-engine/loaders/obj.js +44 -5
- package/dist/engines/little-3d-engine/renderer.d.ts +7 -1
- package/dist/engines/little-3d-engine/renderer.js +2 -0
- package/dist/engines/little-3d-engine/renderers/canvas2d-textured.d.ts +20 -0
- package/dist/engines/little-3d-engine/renderers/canvas2d-textured.js +135 -0
- package/dist/engines/little-3d-engine/renderers/textured-helpers.d.ts +9 -0
- package/dist/engines/little-3d-engine/renderers/textured-helpers.js +35 -0
- package/dist/engines/little-3d-engine/renderers/webgl-textured.d.ts +43 -0
- package/dist/engines/little-3d-engine/renderers/webgl-textured.js +211 -0
- package/dist/engines/little-3d-engine/renderers/webgpu-textured.d.ts +45 -0
- package/dist/engines/little-3d-engine/renderers/webgpu-textured.js +300 -0
- package/dist/engines/little-3d-engine/renderers/webgpu.d.ts +11 -6
- package/dist/engines/little-3d-engine/shapes/complex/plane.d.ts +3 -0
- package/dist/engines/little-3d-engine/shapes/complex/plane.js +33 -0
- package/dist/engines/little-3d-engine/shapes/{cube.d.ts → primitives/cube.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{octahedron.d.ts → primitives/octahedron.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{pyramid.d.ts → primitives/pyramid.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/primitives/quad.d.ts +9 -0
- package/dist/engines/little-3d-engine/shapes/primitives/quad.js +18 -0
- package/dist/engines/little-3d-engine/shapes/{cube-sphere.d.ts → primitives/spheres/cube-sphere.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{icosphere.d.ts → primitives/spheres/icosphere.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{icosphere.js → primitives/spheres/icosphere.js} +1 -1
- package/dist/engines/little-3d-engine/shapes/{octa-sphere.d.ts → primitives/spheres/octa-sphere.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{octa-sphere.js → primitives/spheres/octa-sphere.js} +1 -1
- package/dist/engines/little-3d-engine/shapes/{uv-sphere.d.ts → primitives/spheres/uv-sphere.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{tetrahedron.d.ts → primitives/tetrahedron.d.ts} +1 -1
- package/dist/engines/little-3d-engine/textures/dynamic/canvas-texture.d.ts +1 -0
- package/dist/engines/little-3d-engine/textures/dynamic/canvas-texture.js +8 -0
- package/dist/engines/little-3d-engine/textures/dynamic/shine.d.ts +2 -0
- package/dist/engines/little-3d-engine/textures/dynamic/shine.js +14 -0
- package/dist/engines/little-3d-engine/textures/dynamic/star.d.ts +2 -0
- package/dist/engines/little-3d-engine/textures/dynamic/star.js +16 -0
- package/dist/engines/little-3d-engine/textures/dynamic/streak.d.ts +5 -0
- package/dist/engines/little-3d-engine/textures/dynamic/streak.js +20 -0
- package/dist/index.d.ts +2 -1
- package/dist/prefabs/charged-orb.d.ts +17 -0
- package/dist/prefabs/charged-orb.js +37 -0
- package/dist/prefabs/crystal-comet.d.ts +4 -0
- package/dist/prefabs/crystal-comet.js +41 -0
- package/dist/prefabs/ghost-train.d.ts +9 -0
- package/dist/prefabs/ghost-train.js +44 -0
- package/dist/prefabs/grid-assembly.d.ts +13 -0
- package/dist/prefabs/grid-assembly.js +15 -0
- package/dist/prefabs/monochrome-streak.d.ts +4 -0
- package/dist/prefabs/monochrome-streak.js +25 -0
- package/dist/prefabs/plane-star-trail.d.ts +4 -0
- package/dist/prefabs/plane-star-trail.js +40 -0
- package/dist/prefabs/prefabs.d.ts +10 -0
- package/dist/prefabs/prefabs.js +9 -0
- package/dist/prefabs/pulsing-starfield.d.ts +4 -0
- package/dist/prefabs/pulsing-starfield.js +27 -0
- package/dist/prefabs/rocket-launch.d.ts +8 -0
- package/dist/prefabs/rocket-launch.js +59 -0
- package/dist/prefabs/spinner.d.ts +7 -0
- package/dist/prefabs/spinner.js +17 -0
- package/dist/prefabs/star-swarm.d.ts +4 -0
- package/dist/prefabs/star-swarm.js +27 -0
- package/dist/prefabs/types.d.ts +46 -0
- package/dist/prefabs/types.js +1 -0
- package/dist/umd/spinner.global.js +3055 -1251
- package/dist/umd/spinner.global.min.js +60 -5
- package/package.json +42 -1
- /package/dist/engines/little-3d-engine/shapes/{cube.js → primitives/cube.js} +0 -0
- /package/dist/engines/little-3d-engine/shapes/{octahedron.js → primitives/octahedron.js} +0 -0
- /package/dist/engines/little-3d-engine/shapes/{pyramid.js → primitives/pyramid.js} +0 -0
- /package/dist/engines/little-3d-engine/shapes/{cube-sphere.js → primitives/spheres/cube-sphere.js} +0 -0
- /package/dist/engines/little-3d-engine/shapes/{uv-sphere.js → primitives/spheres/uv-sphere.js} +0 -0
- /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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
12
|
+
protected depthTexture: any;
|
|
13
13
|
private depthSize;
|
|
14
|
-
|
|
14
|
+
protected destroyed: boolean;
|
|
15
15
|
private readonly cache;
|
|
16
|
-
|
|
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
|
-
|
|
26
|
+
protected ensureDepth(): void;
|
|
22
27
|
private buffers;
|
|
23
28
|
private ensureUniformCapacity;
|
|
24
29
|
render(frame: RenderFrame): void;
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|