gloop-sim 0.0.0-stage → 1.0.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.
Files changed (166) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -2
  3. package/dist/assert.d.ts +1 -0
  4. package/dist/assert.js +6 -0
  5. package/dist/assert.js.map +1 -0
  6. package/dist/backend/cpu.d.ts +2 -0
  7. package/dist/backend/cpu.js +23 -0
  8. package/dist/backend/cpu.js.map +1 -0
  9. package/dist/backend/gpu.d.ts +2 -0
  10. package/dist/backend/gpu.js +145 -0
  11. package/dist/backend/gpu.js.map +1 -0
  12. package/dist/backend/index.d.ts +14 -0
  13. package/dist/backend/index.js +23 -0
  14. package/dist/backend/index.js.map +1 -0
  15. package/dist/device.d.ts +11 -0
  16. package/dist/device.js +66 -0
  17. package/dist/device.js.map +1 -0
  18. package/dist/index.d.ts +18 -0
  19. package/dist/index.js +18 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/initializer/fixed.d.ts +2 -0
  22. package/dist/initializer/fixed.js +12 -0
  23. package/dist/initializer/fixed.js.map +1 -0
  24. package/dist/initializer/index.d.ts +14 -0
  25. package/dist/initializer/index.js +23 -0
  26. package/dist/initializer/index.js.map +1 -0
  27. package/dist/initializer/random.d.ts +2 -0
  28. package/dist/initializer/random.js +16 -0
  29. package/dist/initializer/random.js.map +1 -0
  30. package/dist/integrator/euler.d.ts +2 -0
  31. package/dist/integrator/euler.js +34 -0
  32. package/dist/integrator/euler.js.map +1 -0
  33. package/dist/integrator/index.d.ts +14 -0
  34. package/dist/integrator/index.js +23 -0
  35. package/dist/integrator/index.js.map +1 -0
  36. package/dist/integrator/semi-implicit-euler.d.ts +2 -0
  37. package/dist/integrator/semi-implicit-euler.js +36 -0
  38. package/dist/integrator/semi-implicit-euler.js.map +1 -0
  39. package/dist/model/elastic-network/cpu.d.ts +2 -0
  40. package/dist/model/elastic-network/cpu.js +90 -0
  41. package/dist/model/elastic-network/cpu.js.map +1 -0
  42. package/dist/model/elastic-network/gpu.d.ts +2 -0
  43. package/dist/model/elastic-network/gpu.js +114 -0
  44. package/dist/model/elastic-network/gpu.js.map +1 -0
  45. package/dist/model/elastic-network/index.d.ts +2 -0
  46. package/dist/model/elastic-network/index.js +69 -0
  47. package/dist/model/elastic-network/index.js.map +1 -0
  48. package/dist/model/index.d.ts +15 -0
  49. package/dist/model/index.js +24 -0
  50. package/dist/model/index.js.map +1 -0
  51. package/dist/model/inertial.d.ts +2 -0
  52. package/dist/model/inertial.js +17 -0
  53. package/dist/model/inertial.js.map +1 -0
  54. package/dist/render/backend/channels.d.ts +17 -0
  55. package/dist/render/backend/channels.js +35 -0
  56. package/dist/render/backend/channels.js.map +1 -0
  57. package/dist/render/backend/cloud.d.ts +77 -0
  58. package/dist/render/backend/cloud.js +29 -0
  59. package/dist/render/backend/cloud.js.map +1 -0
  60. package/dist/render/backend/compile.d.ts +31 -0
  61. package/dist/render/backend/compile.js +73 -0
  62. package/dist/render/backend/compile.js.map +1 -0
  63. package/dist/render/backend/glyph.d.ts +8 -0
  64. package/dist/render/backend/glyph.js +38 -0
  65. package/dist/render/backend/glyph.js.map +1 -0
  66. package/dist/render/backend/glyphs.d.ts +6 -0
  67. package/dist/render/backend/glyphs.js +146 -0
  68. package/dist/render/backend/glyphs.js.map +1 -0
  69. package/dist/render/backend/gpu.d.ts +12 -0
  70. package/dist/render/backend/gpu.js +278 -0
  71. package/dist/render/backend/gpu.js.map +1 -0
  72. package/dist/render/backend/material.d.ts +30 -0
  73. package/dist/render/backend/material.js +57 -0
  74. package/dist/render/backend/material.js.map +1 -0
  75. package/dist/render/backend/particles.d.ts +5 -0
  76. package/dist/render/backend/particles.js +22 -0
  77. package/dist/render/backend/particles.js.map +1 -0
  78. package/dist/render/backend/post.d.ts +29 -0
  79. package/dist/render/backend/post.js +77 -0
  80. package/dist/render/backend/post.js.map +1 -0
  81. package/dist/render/backend/solids.d.ts +10 -0
  82. package/dist/render/backend/solids.js +134 -0
  83. package/dist/render/backend/solids.js.map +1 -0
  84. package/dist/render/camera.d.ts +73 -0
  85. package/dist/render/camera.js +32 -0
  86. package/dist/render/camera.js.map +1 -0
  87. package/dist/render/channel.d.ts +25 -0
  88. package/dist/render/channel.js +9 -0
  89. package/dist/render/channel.js.map +1 -0
  90. package/dist/render/color.d.ts +34 -0
  91. package/dist/render/color.js +56 -0
  92. package/dist/render/color.js.map +1 -0
  93. package/dist/render/effect.d.ts +81 -0
  94. package/dist/render/effect.js +30 -0
  95. package/dist/render/effect.js.map +1 -0
  96. package/dist/render/frame.d.ts +60 -0
  97. package/dist/render/frame.js +52 -0
  98. package/dist/render/frame.js.map +1 -0
  99. package/dist/render/index.d.ts +12 -0
  100. package/dist/render/index.js +2 -0
  101. package/dist/render/index.js.map +1 -0
  102. package/dist/render/layer.d.ts +90 -0
  103. package/dist/render/layer.js +54 -0
  104. package/dist/render/layer.js.map +1 -0
  105. package/dist/render/ramp.d.ts +93 -0
  106. package/dist/render/ramp.js +120 -0
  107. package/dist/render/ramp.js.map +1 -0
  108. package/dist/render/sampler.d.ts +34 -0
  109. package/dist/render/sampler.js +53 -0
  110. package/dist/render/sampler.js.map +1 -0
  111. package/dist/render/signal.d.ts +66 -0
  112. package/dist/render/signal.js +51 -0
  113. package/dist/render/signal.js.map +1 -0
  114. package/dist/render/sources.d.ts +33 -0
  115. package/dist/render/sources.js +46 -0
  116. package/dist/render/sources.js.map +1 -0
  117. package/dist/render/springs/anchors.d.ts +37 -0
  118. package/dist/render/springs/anchors.js +34 -0
  119. package/dist/render/springs/anchors.js.map +1 -0
  120. package/dist/render/springs/construction.d.ts +30 -0
  121. package/dist/render/springs/construction.js +66 -0
  122. package/dist/render/springs/construction.js.map +1 -0
  123. package/dist/render/springs/impulses.d.ts +45 -0
  124. package/dist/render/springs/impulses.js +144 -0
  125. package/dist/render/springs/impulses.js.map +1 -0
  126. package/dist/render/springs/index.d.ts +9 -0
  127. package/dist/render/springs/index.js +2 -0
  128. package/dist/render/springs/index.js.map +1 -0
  129. package/dist/render/springs/link.d.ts +51 -0
  130. package/dist/render/springs/link.js +42 -0
  131. package/dist/render/springs/link.js.map +1 -0
  132. package/dist/render/springs/links.d.ts +70 -0
  133. package/dist/render/springs/links.js +210 -0
  134. package/dist/render/springs/links.js.map +1 -0
  135. package/dist/render/springs/palette.d.ts +36 -0
  136. package/dist/render/springs/palette.js +22 -0
  137. package/dist/render/springs/palette.js.map +1 -0
  138. package/dist/render/springs/preset.d.ts +65 -0
  139. package/dist/render/springs/preset.js +114 -0
  140. package/dist/render/springs/preset.js.map +1 -0
  141. package/dist/render/springs/segment.d.ts +40 -0
  142. package/dist/render/springs/segment.js +81 -0
  143. package/dist/render/springs/segment.js.map +1 -0
  144. package/dist/render/springs/source.d.ts +35 -0
  145. package/dist/render/springs/source.js +70 -0
  146. package/dist/render/springs/source.js.map +1 -0
  147. package/dist/render/springs/springs.d.ts +40 -0
  148. package/dist/render/springs/springs.js +34 -0
  149. package/dist/render/springs/springs.js.map +1 -0
  150. package/dist/render/springs/strain.d.ts +21 -0
  151. package/dist/render/springs/strain.js +39 -0
  152. package/dist/render/springs/strain.js.map +1 -0
  153. package/dist/render/stage.d.ts +96 -0
  154. package/dist/render/stage.js +2 -0
  155. package/dist/render/stage.js.map +1 -0
  156. package/dist/render/view.d.ts +63 -0
  157. package/dist/render/view.js +63 -0
  158. package/dist/render/view.js.map +1 -0
  159. package/dist/swarm.d.ts +5 -0
  160. package/dist/swarm.js +24 -0
  161. package/dist/swarm.js.map +1 -0
  162. package/dist/types.d.ts +290 -0
  163. package/dist/types.js +2 -0
  164. package/dist/types.js.map +1 -0
  165. package/docs/assets/logo.svg +112 -0
  166. package/package.json +102 -3
@@ -0,0 +1,278 @@
1
+ import { Color, Fog, Group, OrthographicCamera, PerspectiveCamera, Scene as ThreeScene, Vector3, WebGPURenderer, } from "three/webgpu";
2
+ import { assert } from "../../assert.js";
3
+ import { post } from "../../render/backend/post.js";
4
+ import { Sampler } from "../../render/sampler.js";
5
+ const DEFAULT_PIXEL_RATIO_CAP = 2;
6
+ const DEFAULT_CLEAR_COLOR = "#000000";
7
+ const FOG_NEAR = 2;
8
+ const FOG_FAR = 6;
9
+ export var backend;
10
+ (function (backend) {
11
+ /**
12
+ * Selects the GPU rendering device.
13
+ */
14
+ function gpu() {
15
+ return { kind: "gpu" };
16
+ }
17
+ backend.gpu = gpu;
18
+ })(backend || (backend = {}));
19
+ function browserPixelRatio() {
20
+ return typeof devicePixelRatio === "number" ? devicePixelRatio : 1;
21
+ }
22
+ function createCameraObject(camera, aspect) {
23
+ const object = camera.kind === "perspective" ? new PerspectiveCamera() : new OrthographicCamera();
24
+ frameCameraObject({ object, camera, aspect });
25
+ return object;
26
+ }
27
+ function frameCameraObject({ object, camera, aspect }) {
28
+ if (camera.kind === "perspective") {
29
+ assert(object instanceof PerspectiveCamera, "a perspective camera cannot be framed by an orthographic object");
30
+ object.fov = camera.fieldOfView;
31
+ object.aspect = aspect;
32
+ }
33
+ else {
34
+ assert(object instanceof OrthographicCamera, "an orthographic camera cannot be framed by a perspective object");
35
+ object.left = -camera.extent * aspect;
36
+ object.right = camera.extent * aspect;
37
+ object.top = camera.extent;
38
+ object.bottom = -camera.extent;
39
+ }
40
+ object.near = camera.near;
41
+ object.far = camera.far;
42
+ object.position.set(...camera.position);
43
+ object.lookAt(new Vector3(...camera.target));
44
+ object.updateProjectionMatrix();
45
+ }
46
+ function mount(built, order) {
47
+ const root = new Group();
48
+ root.add(built.object);
49
+ root.traverse((object) => {
50
+ object.renderOrder = order;
51
+ });
52
+ return root;
53
+ }
54
+ class GpuScene {
55
+ sampler;
56
+ threeScene = new ThreeScene();
57
+ mounted = [];
58
+ sources = [];
59
+ disposed = false;
60
+ /**
61
+ * Counts layer replacements, so the stage can tell which layer set its
62
+ * pipelines were compiled for.
63
+ */
64
+ revision = 0;
65
+ constructor(sampler) {
66
+ this.sampler = sampler;
67
+ }
68
+ layers(layers) {
69
+ assert(!this.disposed, "cannot set layers on a disposed scene");
70
+ this.clearLayers();
71
+ this.revision += 1;
72
+ const sources = [];
73
+ for (const [order, layer] of layers.entries()) {
74
+ if (!sources.includes(layer.source)) {
75
+ sources.push(layer.source);
76
+ }
77
+ const built = layer.build();
78
+ const root = mount(built, order);
79
+ this.mounted.push({ built, root });
80
+ this.threeScene.add(root);
81
+ }
82
+ this.sources = sources;
83
+ return this;
84
+ }
85
+ async advanceFrame() {
86
+ const sample = await this.sampler.sample();
87
+ for (const source of this.sources) {
88
+ source.update(sample.state, sample);
89
+ }
90
+ for (const { built } of this.mounted) {
91
+ built.update(sample);
92
+ }
93
+ }
94
+ dispose() {
95
+ if (this.disposed) {
96
+ return;
97
+ }
98
+ this.clearLayers();
99
+ this.disposed = true;
100
+ }
101
+ clearLayers() {
102
+ for (const { built, root } of this.mounted) {
103
+ this.threeScene.remove(root);
104
+ built.dispose();
105
+ }
106
+ this.mounted.length = 0;
107
+ }
108
+ }
109
+ class GpuStage {
110
+ renderer;
111
+ canvas;
112
+ pixelRatioCap;
113
+ fogColor;
114
+ scenes = new Set();
115
+ effectChain = [];
116
+ cameraDescriptor;
117
+ cameraObject;
118
+ post = null;
119
+ postScene = null;
120
+ postDirty = true;
121
+ compiledRevision = null;
122
+ width = 1;
123
+ height = 1;
124
+ fogNear = FOG_NEAR;
125
+ fogFar = FOG_FAR;
126
+ disposed = false;
127
+ constructor(renderer, canvas, pixelRatioCap, fogColor, camera) {
128
+ this.renderer = renderer;
129
+ this.canvas = canvas;
130
+ this.pixelRatioCap = pixelRatioCap;
131
+ this.fogColor = fogColor;
132
+ this.cameraDescriptor = camera;
133
+ this.cameraObject = createCameraObject(camera, 1);
134
+ this.resize();
135
+ }
136
+ observe(swarm) {
137
+ assert(!this.disposed, "cannot observe on a disposed stage");
138
+ const scene = new GpuScene(new Sampler({ swarm }));
139
+ this.applyFog(scene);
140
+ this.scenes.add(scene);
141
+ return scene;
142
+ }
143
+ fog(range) {
144
+ this.fogNear = range === null ? null : range.near;
145
+ this.fogFar = range === null ? this.fogFar : range.far;
146
+ for (const scene of this.scenes) {
147
+ // We adjust the existing fog in place, since replacing it re-dirties every material.
148
+ this.applyFog(scene);
149
+ }
150
+ }
151
+ applyFog(scene) {
152
+ if (this.fogNear === null) {
153
+ scene.threeScene.fog = null;
154
+ return;
155
+ }
156
+ const fog = scene.threeScene.fog;
157
+ if (fog instanceof Fog) {
158
+ fog.near = this.fogNear;
159
+ fog.far = this.fogFar;
160
+ return;
161
+ }
162
+ scene.threeScene.fog = new Fog(this.fogColor, this.fogNear, this.fogFar);
163
+ }
164
+ camera(camera) {
165
+ this.frameCamera(camera);
166
+ }
167
+ effects(effects) {
168
+ const next = [...effects];
169
+ // A chain with the same passes in the same order is tuned in place.
170
+ // Otherwise, the post chain is rebuilt on the next render.
171
+ if (hasSameShape(this.effectChain, next)) {
172
+ this.post?.tune(next);
173
+ }
174
+ else {
175
+ this.postDirty = true;
176
+ }
177
+ this.effectChain = next;
178
+ }
179
+ async render(scene) {
180
+ assert(!this.disposed, "cannot render on a disposed stage");
181
+ assert(scene instanceof GpuScene, "scene was not created by this stage");
182
+ await scene.advanceFrame();
183
+ if (this.postDirty || this.postScene !== scene) {
184
+ this.post = post({
185
+ renderer: this.renderer,
186
+ scene: scene.threeScene,
187
+ camera: this.cameraObject,
188
+ effects: this.effectChain,
189
+ });
190
+ this.postScene = scene;
191
+ this.postDirty = false;
192
+ this.compiledRevision = null;
193
+ }
194
+ assert(this.post !== null, "post-processing pipeline failed to build");
195
+ if (this.compiledRevision !== scene.revision) {
196
+ const post = this.post;
197
+ const revision = scene.revision;
198
+ await post.compile();
199
+ // The chain or the layers can have been replaced while compiling, in
200
+ // which case the next render compiles again.
201
+ if (this.disposed || this.post !== post) {
202
+ return;
203
+ }
204
+ this.compiledRevision = revision;
205
+ }
206
+ this.post.render();
207
+ }
208
+ resize(size) {
209
+ const width = Math.max(1, Math.floor(size?.width ?? this.canvas.clientWidth ?? this.canvas.width));
210
+ const height = Math.max(1, Math.floor(size?.height ?? this.canvas.clientHeight ?? this.canvas.height));
211
+ this.width = width;
212
+ this.height = height;
213
+ this.renderer.setPixelRatio(Math.min(browserPixelRatio(), this.pixelRatioCap));
214
+ this.renderer.setSize(width, height, false);
215
+ // The post-processing chain tracks the renderer's drawing buffer, so a
216
+ // resize must not dirty it.
217
+ this.frameCamera(this.cameraDescriptor);
218
+ }
219
+ frameCamera(camera) {
220
+ if (camera.kind === this.cameraDescriptor.kind) {
221
+ frameCameraObject({ object: this.cameraObject, camera, aspect: this.aspect() });
222
+ }
223
+ else {
224
+ this.cameraObject = createCameraObject(camera, this.aspect());
225
+ this.postDirty = true;
226
+ }
227
+ this.cameraDescriptor = camera;
228
+ }
229
+ dispose() {
230
+ if (this.disposed) {
231
+ return;
232
+ }
233
+ for (const scene of this.scenes) {
234
+ scene.dispose();
235
+ }
236
+ this.scenes.clear();
237
+ this.post = null;
238
+ this.renderer.dispose();
239
+ this.disposed = true;
240
+ }
241
+ aspect() {
242
+ return this.width / this.height;
243
+ }
244
+ }
245
+ function hasSameShape(current, next) {
246
+ if (current.length !== next.length) {
247
+ return false;
248
+ }
249
+ for (const [index, effect] of current.entries()) {
250
+ const other = next[index];
251
+ assert(other !== undefined, `effect ${index} is missing from a chain of ${next.length}`);
252
+ if (effect.type !== other.type) {
253
+ return false;
254
+ }
255
+ }
256
+ return true;
257
+ }
258
+ /**
259
+ * Creates a GPU-backed stage on `canvas`. Waits for the device to initialize
260
+ * before returning.
261
+ */
262
+ export async function createStage(options) {
263
+ assert(options.backend.kind === "gpu", `unsupported render backend "${options.backend.kind}"`);
264
+ const renderer = new WebGPURenderer({ canvas: options.canvas, antialias: true, alpha: false });
265
+ await renderer.init();
266
+ const clear = new Color(options.clear ?? DEFAULT_CLEAR_COLOR);
267
+ renderer.setClearColor(clear, 1);
268
+ const defaultCamera = {
269
+ kind: "perspective",
270
+ position: [0, 0, 3],
271
+ target: [0, 0, 0],
272
+ fieldOfView: 45,
273
+ near: 0.01,
274
+ far: 100,
275
+ };
276
+ return new GpuStage(renderer, options.canvas, options.pixelRatio ?? DEFAULT_PIXEL_RATIO_CAP, clear, options.camera ?? defaultCamera);
277
+ }
278
+ //# sourceMappingURL=gpu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gpu.js","sourceRoot":"","sources":["../../../src/render/backend/gpu.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Color,\n Fog,\n Group,\n OrthographicCamera,\n PerspectiveCamera,\n Scene as ThreeScene,\n Vector3,\n WebGPURenderer,\n} from \"three/webgpu\";\nimport { assert } from \"@/assert.js\";\nimport { type PostChain, post } from \"@/render/backend/post.js\";\nimport type { Camera } from \"@/render/camera.js\";\nimport type { Effect } from \"@/render/effect.js\";\nimport type { Layer, LayerObject } from \"@/render/layer.js\";\nimport { Sampler } from \"@/render/sampler.js\";\nimport type { Source } from \"@/render/sources.js\";\nimport type { RenderBackend, Scene, Stage, StageOptions } from \"@/render/stage.js\";\nimport type { Swarm } from \"@/types.js\";\n\nconst DEFAULT_PIXEL_RATIO_CAP = 2;\nconst DEFAULT_CLEAR_COLOR = \"#000000\";\nconst FOG_NEAR = 2;\nconst FOG_FAR = 6;\n\nexport namespace backend {\n /**\n * Selects the GPU rendering device.\n */\n export function gpu(): RenderBackend {\n return { kind: \"gpu\" };\n }\n}\n\nfunction browserPixelRatio(): number {\n return typeof devicePixelRatio === \"number\" ? devicePixelRatio : 1;\n}\n\ntype CameraObject = PerspectiveCamera | OrthographicCamera;\n\ninterface CameraFraming {\n object: CameraObject;\n camera: Camera;\n aspect: number;\n}\n\nfunction createCameraObject(camera: Camera, aspect: number): CameraObject {\n const object = camera.kind === \"perspective\" ? new PerspectiveCamera() : new OrthographicCamera();\n frameCameraObject({ object, camera, aspect });\n return object;\n}\n\nfunction frameCameraObject({ object, camera, aspect }: CameraFraming): void {\n if (camera.kind === \"perspective\") {\n assert(object instanceof PerspectiveCamera, \"a perspective camera cannot be framed by an orthographic object\");\n object.fov = camera.fieldOfView;\n object.aspect = aspect;\n } else {\n assert(object instanceof OrthographicCamera, \"an orthographic camera cannot be framed by a perspective object\");\n object.left = -camera.extent * aspect;\n object.right = camera.extent * aspect;\n object.top = camera.extent;\n object.bottom = -camera.extent;\n }\n\n object.near = camera.near;\n object.far = camera.far;\n object.position.set(...camera.position);\n object.lookAt(new Vector3(...camera.target));\n object.updateProjectionMatrix();\n}\n\ninterface MountedLayer {\n readonly built: LayerObject;\n readonly root: Group;\n}\n\nfunction mount(built: LayerObject, order: number): Group {\n const root = new Group();\n root.add(built.object);\n root.traverse((object) => {\n object.renderOrder = order;\n });\n return root;\n}\n\nclass GpuScene implements Scene {\n readonly threeScene = new ThreeScene();\n private readonly mounted: MountedLayer[] = [];\n private sources: readonly Source[] = [];\n private disposed = false;\n\n /**\n * Counts layer replacements, so the stage can tell which layer set its\n * pipelines were compiled for.\n */\n revision = 0;\n\n constructor(private readonly sampler: Sampler) {}\n\n layers(layers: readonly Layer<Source>[]): Scene {\n assert(!this.disposed, \"cannot set layers on a disposed scene\");\n this.clearLayers();\n this.revision += 1;\n const sources: Source[] = [];\n for (const [order, layer] of layers.entries()) {\n if (!sources.includes(layer.source)) {\n sources.push(layer.source);\n }\n const built = layer.build();\n const root = mount(built, order);\n this.mounted.push({ built, root });\n this.threeScene.add(root);\n }\n this.sources = sources;\n return this;\n }\n\n async advanceFrame(): Promise<void> {\n const sample = await this.sampler.sample();\n for (const source of this.sources) {\n source.update(sample.state, sample);\n }\n for (const { built } of this.mounted) {\n built.update(sample);\n }\n }\n\n dispose(): void {\n if (this.disposed) {\n return;\n }\n this.clearLayers();\n this.disposed = true;\n }\n\n private clearLayers(): void {\n for (const { built, root } of this.mounted) {\n this.threeScene.remove(root);\n built.dispose();\n }\n this.mounted.length = 0;\n }\n}\n\nclass GpuStage implements Stage {\n private readonly scenes = new Set<GpuScene>();\n private effectChain: readonly Effect[] = [];\n private cameraDescriptor: Camera;\n private cameraObject: CameraObject;\n private post: PostChain | null = null;\n private postScene: GpuScene | null = null;\n private postDirty = true;\n private compiledRevision: number | null = null;\n private width = 1;\n private height = 1;\n private fogNear: number | null = FOG_NEAR;\n private fogFar = FOG_FAR;\n private disposed = false;\n\n constructor(\n private readonly renderer: WebGPURenderer,\n private readonly canvas: HTMLCanvasElement,\n private readonly pixelRatioCap: number,\n private readonly fogColor: Color,\n camera: Camera,\n ) {\n this.cameraDescriptor = camera;\n this.cameraObject = createCameraObject(camera, 1);\n this.resize();\n }\n\n observe(swarm: Swarm): Scene {\n assert(!this.disposed, \"cannot observe on a disposed stage\");\n const scene = new GpuScene(new Sampler({ swarm }));\n this.applyFog(scene);\n this.scenes.add(scene);\n return scene;\n }\n\n fog(range: { readonly near: number; readonly far: number } | null): void {\n this.fogNear = range === null ? null : range.near;\n this.fogFar = range === null ? this.fogFar : range.far;\n for (const scene of this.scenes) {\n // We adjust the existing fog in place, since replacing it re-dirties every material.\n this.applyFog(scene);\n }\n }\n\n private applyFog(scene: GpuScene): void {\n if (this.fogNear === null) {\n scene.threeScene.fog = null;\n return;\n }\n\n const fog = scene.threeScene.fog;\n if (fog instanceof Fog) {\n fog.near = this.fogNear;\n fog.far = this.fogFar;\n return;\n }\n scene.threeScene.fog = new Fog(this.fogColor, this.fogNear, this.fogFar);\n }\n\n camera(camera: Camera): void {\n this.frameCamera(camera);\n }\n\n effects(effects: readonly Effect[]): void {\n const next = [...effects];\n\n // A chain with the same passes in the same order is tuned in place.\n // Otherwise, the post chain is rebuilt on the next render.\n if (hasSameShape(this.effectChain, next)) {\n this.post?.tune(next);\n } else {\n this.postDirty = true;\n }\n this.effectChain = next;\n }\n\n async render(scene: Scene): Promise<void> {\n assert(!this.disposed, \"cannot render on a disposed stage\");\n assert(scene instanceof GpuScene, \"scene was not created by this stage\");\n await scene.advanceFrame();\n\n if (this.postDirty || this.postScene !== scene) {\n this.post = post({\n renderer: this.renderer,\n scene: scene.threeScene,\n camera: this.cameraObject,\n effects: this.effectChain,\n });\n this.postScene = scene;\n this.postDirty = false;\n this.compiledRevision = null;\n }\n assert(this.post !== null, \"post-processing pipeline failed to build\");\n\n if (this.compiledRevision !== scene.revision) {\n const post = this.post;\n const revision = scene.revision;\n await post.compile();\n // The chain or the layers can have been replaced while compiling, in\n // which case the next render compiles again.\n if (this.disposed || this.post !== post) {\n return;\n }\n this.compiledRevision = revision;\n }\n this.post.render();\n }\n\n resize(size?: { readonly width: number; readonly height: number }): void {\n const width = Math.max(1, Math.floor(size?.width ?? this.canvas.clientWidth ?? this.canvas.width));\n const height = Math.max(1, Math.floor(size?.height ?? this.canvas.clientHeight ?? this.canvas.height));\n this.width = width;\n this.height = height;\n\n this.renderer.setPixelRatio(Math.min(browserPixelRatio(), this.pixelRatioCap));\n this.renderer.setSize(width, height, false);\n\n // The post-processing chain tracks the renderer's drawing buffer, so a\n // resize must not dirty it.\n this.frameCamera(this.cameraDescriptor);\n }\n\n private frameCamera(camera: Camera): void {\n if (camera.kind === this.cameraDescriptor.kind) {\n frameCameraObject({ object: this.cameraObject, camera, aspect: this.aspect() });\n } else {\n this.cameraObject = createCameraObject(camera, this.aspect());\n this.postDirty = true;\n }\n this.cameraDescriptor = camera;\n }\n\n dispose(): void {\n if (this.disposed) {\n return;\n }\n for (const scene of this.scenes) {\n scene.dispose();\n }\n this.scenes.clear();\n this.post = null;\n this.renderer.dispose();\n this.disposed = true;\n }\n\n private aspect(): number {\n return this.width / this.height;\n }\n}\n\nfunction hasSameShape(current: readonly Effect[], next: readonly Effect[]): boolean {\n if (current.length !== next.length) {\n return false;\n }\n for (const [index, effect] of current.entries()) {\n const other = next[index];\n assert(other !== undefined, `effect ${index} is missing from a chain of ${next.length}`);\n if (effect.type !== other.type) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * Creates a GPU-backed stage on `canvas`. Waits for the device to initialize\n * before returning.\n */\nexport async function createStage(options: StageOptions): Promise<Stage> {\n assert(options.backend.kind === \"gpu\", `unsupported render backend \"${options.backend.kind}\"`);\n\n const renderer = new WebGPURenderer({ canvas: options.canvas, antialias: true, alpha: false });\n await renderer.init();\n\n const clear = new Color(options.clear ?? DEFAULT_CLEAR_COLOR);\n renderer.setClearColor(clear, 1);\n\n const defaultCamera: Camera = {\n kind: \"perspective\",\n position: [0, 0, 3],\n target: [0, 0, 0],\n fieldOfView: 45,\n near: 0.01,\n far: 100,\n };\n\n return new GpuStage(\n renderer,\n options.canvas,\n options.pixelRatio ?? DEFAULT_PIXEL_RATIO_CAP,\n clear,\n options.camera ?? defaultCamera,\n );\n}\n"]}
@@ -0,0 +1,30 @@
1
+ import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from "three/webgpu";
2
+ import type { ColorNode, ScalarNode } from "../../render/backend/compile.js";
3
+ import type { Color } from "../../render/color.js";
4
+ export declare namespace material {
5
+ /**
6
+ * An unlit material for the flat, translucent geometry that layers draw.
7
+ * Additive materials add their color to what is behind them instead of
8
+ * blending over it.
9
+ */
10
+ function flat({ color, opacity, additive, }: {
11
+ color: ColorNode;
12
+ opacity: ScalarNode;
13
+ additive?: boolean;
14
+ }): MeshBasicNodeMaterial;
15
+ /**
16
+ * An unlit, translucent material for closed solid geometry, drawn from the
17
+ * front only.
18
+ */
19
+ function solid({ color, opacity }: {
20
+ color: ColorNode;
21
+ opacity: ScalarNode;
22
+ }): MeshBasicNodeMaterial;
23
+ /**
24
+ * An unlit, translucent material for line geometry of one constant color.
25
+ */
26
+ function line({ color, opacity }: {
27
+ color: Color;
28
+ opacity: number;
29
+ }): LineBasicNodeMaterial;
30
+ }
@@ -0,0 +1,57 @@
1
+ import { AdditiveBlending, DoubleSide, LineBasicNodeMaterial, MeshBasicNodeMaterial, Color as ThreeColor, } from "three/webgpu";
2
+ export var material;
3
+ (function (material) {
4
+ /**
5
+ * An unlit material for the flat, translucent geometry that layers draw.
6
+ * Additive materials add their color to what is behind them instead of
7
+ * blending over it.
8
+ */
9
+ function flat({ color, opacity, additive = false, }) {
10
+ const result = new MeshBasicNodeMaterial();
11
+ result.colorNode = color;
12
+ result.opacityNode = opacity;
13
+ makeTranslucent(result);
14
+ // A flat layer is seen from either side. In three's terms: double-sided,
15
+ // so a quad draws whichever way its triangles wind; and single-pass,
16
+ // because three otherwise draws transparent double-sided materials as a
17
+ // back-then-front pair, which for flat geometry that writes no depth gives
18
+ // the same image at twice the cost.
19
+ result.side = DoubleSide;
20
+ result.forceSinglePass = true;
21
+ if (additive) {
22
+ result.blending = AdditiveBlending;
23
+ }
24
+ return result;
25
+ }
26
+ material.flat = flat;
27
+ /**
28
+ * An unlit, translucent material for closed solid geometry, drawn from the
29
+ * front only.
30
+ */
31
+ function solid({ color, opacity }) {
32
+ const result = new MeshBasicNodeMaterial();
33
+ result.colorNode = color;
34
+ result.opacityNode = opacity;
35
+ makeTranslucent(result);
36
+ return result;
37
+ }
38
+ material.solid = solid;
39
+ /**
40
+ * An unlit, translucent material for line geometry of one constant color.
41
+ */
42
+ function line({ color, opacity }) {
43
+ const result = new LineBasicNodeMaterial();
44
+ result.color = new ThreeColor(color);
45
+ result.opacity = opacity;
46
+ makeTranslucent(result);
47
+ return result;
48
+ }
49
+ material.line = line;
50
+ })(material || (material = {}));
51
+ function makeTranslucent(target) {
52
+ // A layer is translucent. In three's terms: transparent without depth
53
+ // writes, so overlapping layers blend rather than occlude each other.
54
+ target.transparent = true;
55
+ target.depthWrite = false;
56
+ }
57
+ //# sourceMappingURL=material.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"material.js","sourceRoot":"","sources":["../../../src/render/backend/material.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,UAAU,EACV,qBAAqB,EAErB,qBAAqB,EACrB,KAAK,IAAI,UAAU,GACpB,MAAM,cAAc,CAAC;AAItB,MAAM,KAAW,QAAQ,CAwDxB;AAxDD,WAAiB,QAAQ;IACvB;;;;OAIG;IACH,SAAgB,IAAI,CAAC,EACnB,KAAK,EACL,OAAO,EACP,QAAQ,GAAG,KAAK,GAKjB;QACC,MAAM,MAAM,GAAG,IAAI,qBAAqB,EAAE,CAAC;QAC3C,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC;QACzB,MAAM,CAAC,WAAW,GAAG,OAAO,CAAC;QAC7B,eAAe,CAAC,MAAM,CAAC,CAAC;QAExB,yEAAyE;QACzE,qEAAqE;QACrE,wEAAwE;QACxE,2EAA2E;QAC3E,oCAAoC;QACpC,MAAM,CAAC,IAAI,GAAG,UAAU,CAAC;QACzB,MAAM,CAAC,eAAe,GAAG,IAAI,CAAC;QAE9B,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,CAAC,QAAQ,GAAG,gBAAgB,CAAC;QACrC,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IA1Be,aAAI,OA0BnB,CAAA;IAED;;;OAGG;IACH,SAAgB,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,EAA6C;QACjF,MAAM,MAAM,GAAG,IAAI,qBAAqB,EAAE,CAAC;QAC3C,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC;QACzB,MAAM,CAAC,WAAW,GAAG,OAAO,CAAC;QAC7B,eAAe,CAAC,MAAM,CAAC,CAAC;QACxB,OAAO,MAAM,CAAC;IAChB,CAAC;IANe,cAAK,QAMpB,CAAA;IAED;;OAEG;IACH,SAAgB,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAqC;QACxE,MAAM,MAAM,GAAG,IAAI,qBAAqB,EAAE,CAAC;QAC3C,MAAM,CAAC,KAAK,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;QACrC,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC;QACzB,eAAe,CAAC,MAAM,CAAC,CAAC;QACxB,OAAO,MAAM,CAAC;IAChB,CAAC;IANe,aAAI,OAMnB,CAAA;AACH,CAAC,EAxDgB,QAAQ,KAAR,QAAQ,QAwDxB;AAED,SAAS,eAAe,CAAC,MAAgB;IACvC,sEAAsE;IACtE,sEAAsE;IACtE,MAAM,CAAC,WAAW,GAAG,IAAI,CAAC;IAC1B,MAAM,CAAC,UAAU,GAAG,KAAK,CAAC;AAC5B,CAAC","sourcesContent":["import {\n AdditiveBlending,\n DoubleSide,\n LineBasicNodeMaterial,\n type Material,\n MeshBasicNodeMaterial,\n Color as ThreeColor,\n} from \"three/webgpu\";\nimport type { ColorNode, ScalarNode } from \"@/render/backend/compile.js\";\nimport type { Color } from \"@/render/color.js\";\n\nexport namespace material {\n /**\n * An unlit material for the flat, translucent geometry that layers draw.\n * Additive materials add their color to what is behind them instead of\n * blending over it.\n */\n export function flat({\n color,\n opacity,\n additive = false,\n }: {\n color: ColorNode;\n opacity: ScalarNode;\n additive?: boolean;\n }): MeshBasicNodeMaterial {\n const result = new MeshBasicNodeMaterial();\n result.colorNode = color;\n result.opacityNode = opacity;\n makeTranslucent(result);\n\n // A flat layer is seen from either side. In three's terms: double-sided,\n // so a quad draws whichever way its triangles wind; and single-pass,\n // because three otherwise draws transparent double-sided materials as a\n // back-then-front pair, which for flat geometry that writes no depth gives\n // the same image at twice the cost.\n result.side = DoubleSide;\n result.forceSinglePass = true;\n\n if (additive) {\n result.blending = AdditiveBlending;\n }\n return result;\n }\n\n /**\n * An unlit, translucent material for closed solid geometry, drawn from the\n * front only.\n */\n export function solid({ color, opacity }: { color: ColorNode; opacity: ScalarNode }): MeshBasicNodeMaterial {\n const result = new MeshBasicNodeMaterial();\n result.colorNode = color;\n result.opacityNode = opacity;\n makeTranslucent(result);\n return result;\n }\n\n /**\n * An unlit, translucent material for line geometry of one constant color.\n */\n export function line({ color, opacity }: { color: Color; opacity: number }): LineBasicNodeMaterial {\n const result = new LineBasicNodeMaterial();\n result.color = new ThreeColor(color);\n result.opacity = opacity;\n makeTranslucent(result);\n return result;\n }\n}\n\nfunction makeTranslucent(target: Material): void {\n // A layer is translucent. In three's terms: transparent without depth\n // writes, so overlapping layers blend rather than occlude each other.\n target.transparent = true;\n target.depthWrite = false;\n}\n"]}
@@ -0,0 +1,5 @@
1
+ import type { LayerObject, ParticlesLayer } from "../../render/layer.js";
2
+ /**
3
+ * Builds a particles layer. It draws one glyph per particle in the source.
4
+ */
5
+ export declare function buildParticles(layer: ParticlesLayer): LayerObject;
@@ -0,0 +1,22 @@
1
+ import { buildGlyphCloud } from "../../render/backend/glyphs.js";
2
+ /**
3
+ * Builds a particles layer. It draws one glyph per particle in the source.
4
+ */
5
+ export function buildParticles(layer) {
6
+ const cloud = buildGlyphCloud({ shape: layer.shape, style: layer, sizeScale: 1 });
7
+ let indices = [];
8
+ return {
9
+ object: cloud.mesh,
10
+ update(frame) {
11
+ const count = layer.source.count();
12
+ if (indices.length !== count) {
13
+ indices = Array.from({ length: count }, (_, particle) => particle);
14
+ }
15
+ cloud.update(frame, indices);
16
+ },
17
+ dispose() {
18
+ cloud.dispose();
19
+ },
20
+ };
21
+ }
22
+ //# sourceMappingURL=particles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"particles.js","sourceRoot":"","sources":["../../../src/render/backend/particles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAI7D;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAAqB;IAClD,MAAM,KAAK,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC;IAElF,IAAI,OAAO,GAAa,EAAE,CAAC;IAE3B,OAAO;QACL,MAAM,EAAE,KAAK,CAAC,IAAI;QAClB,MAAM,CAAC,KAAY;YACjB,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YACnC,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;gBAC7B,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC;YACrE,CAAC;YACD,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAC/B,CAAC;QACD,OAAO;YACL,KAAK,CAAC,OAAO,EAAE,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { buildGlyphCloud } from \"@/render/backend/glyphs.js\";\nimport type { Frame } from \"@/render/frame.js\";\nimport type { LayerObject, ParticlesLayer } from \"@/render/layer.js\";\n\n/**\n * Builds a particles layer. It draws one glyph per particle in the source.\n */\nexport function buildParticles(layer: ParticlesLayer): LayerObject {\n const cloud = buildGlyphCloud({ shape: layer.shape, style: layer, sizeScale: 1 });\n\n let indices: number[] = [];\n\n return {\n object: cloud.mesh,\n update(frame: Frame): void {\n const count = layer.source.count();\n if (indices.length !== count) {\n indices = Array.from({ length: count }, (_, particle) => particle);\n }\n cloud.update(frame, indices);\n },\n dispose(): void {\n cloud.dispose();\n },\n };\n}\n"]}
@@ -0,0 +1,29 @@
1
+ import type { Camera, Scene, WebGPURenderer } from "three/webgpu";
2
+ import type { Effect } from "../../render/effect.js";
3
+ export interface PostChain {
4
+ /**
5
+ * Renders the post-processing chain to the screen. Synchronous; the renderer
6
+ * must already be initialized.
7
+ */
8
+ render(): void;
9
+ /**
10
+ * Builds the scene's pipelines without blocking the GPU queue. Pipelines not
11
+ * built beforehand are compiled on first draw, stalling that frame.
12
+ */
13
+ compile(): Promise<void>;
14
+ /**
15
+ * Updates the post-processing effect chain parameters in place.
16
+ */
17
+ tune(effects: readonly Effect[]): void;
18
+ }
19
+ interface PostOptions {
20
+ renderer: WebGPURenderer;
21
+ scene: Scene;
22
+ camera: Camera;
23
+ effects: readonly Effect[];
24
+ }
25
+ /**
26
+ * Builds a post-processing chain that applies the specified `effects` in order.
27
+ */
28
+ export declare function post({ renderer, scene, camera, effects }: PostOptions): PostChain;
29
+ export {};
@@ -0,0 +1,77 @@
1
+ import { bloom } from "three/addons/tsl/display/BloomNode.js";
2
+ import { float, length, pass, screenUV, smoothstep, uniform } from "three/tsl";
3
+ import { RenderPipeline } from "three/webgpu";
4
+ import { assert } from "../../assert.js";
5
+ const build = {
6
+ bloom({ scenePass, input, effect }) {
7
+ const intensity = uniform(effect.intensity);
8
+ const radius = uniform(effect.radius);
9
+ const threshold = uniform(effect.threshold);
10
+ return {
11
+ output: input.add(bloom(scenePass, intensity, radius, threshold)),
12
+ tune(next) {
13
+ assert(next.type === "bloom", `a bloom pass cannot be tuned by a ${next.type} effect`);
14
+ intensity.value = next.intensity;
15
+ radius.value = next.radius;
16
+ threshold.value = next.threshold;
17
+ },
18
+ };
19
+ },
20
+ vignette({ input, effect }) {
21
+ const intensity = uniform(effect.intensity);
22
+ const smoothness = uniform(effect.smoothness);
23
+ const distance = length(screenUV.sub(0.5));
24
+ const inner = smoothness.mul(0.5);
25
+ const edge = smoothstep(inner, 0.75, distance).mul(intensity);
26
+ return {
27
+ output: input.mul(float(1).sub(edge)),
28
+ tune(next) {
29
+ assert(next.type === "vignette", `a vignette pass cannot be tuned by a ${next.type} effect`);
30
+ intensity.value = next.intensity;
31
+ smoothness.value = next.smoothness;
32
+ },
33
+ };
34
+ },
35
+ };
36
+ /**
37
+ * Builds a post-processing chain that applies the specified `effects` in order.
38
+ */
39
+ export function post({ renderer, scene, camera, effects }) {
40
+ if (effects.length === 0) {
41
+ return {
42
+ render: () => renderer.render(scene, camera),
43
+ compile: () => renderer.compileAsync(scene, camera),
44
+ tune() { },
45
+ };
46
+ }
47
+ const scenePass = pass(scene, camera);
48
+ let output = scenePass;
49
+ const tuners = [];
50
+ for (const effect of effects) {
51
+ const built = effect.type === "bloom"
52
+ ? build.bloom({ scenePass, input: output, effect })
53
+ : build.vignette({ scenePass, input: output, effect });
54
+ output = built.output;
55
+ tuners.push(built.tune);
56
+ }
57
+ const post = new RenderPipeline(renderer);
58
+ post.outputNode = output;
59
+ return {
60
+ render: () => post.render(),
61
+ compile: () => {
62
+ // The pass takes the renderer's sample count on its first render, so a
63
+ // compile ahead of that would build single-sampled pipelines.
64
+ scenePass.renderTarget.samples = renderer.samples;
65
+ return scenePass.compileAsync(renderer);
66
+ },
67
+ tune(next) {
68
+ assert(next.length === tuners.length, `a chain of ${tuners.length} effects cannot be tuned by ${next.length}`);
69
+ for (const [index, tune] of tuners.entries()) {
70
+ const effect = next[index];
71
+ assert(effect !== undefined, `effect ${index} is missing from a chain of ${next.length}`);
72
+ tune(effect);
73
+ }
74
+ },
75
+ };
76
+ }
77
+ //# sourceMappingURL=post.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post.js","sourceRoot":"","sources":["../../../src/render/backend/post.ts"],"names":[],"mappings":"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","sourcesContent":["import { bloom } from \"three/addons/tsl/display/BloomNode.js\";\nimport { float, length, pass, screenUV, smoothstep, uniform } from \"three/tsl\";\nimport type { Camera, Node, Scene, WebGPURenderer } from \"three/webgpu\";\nimport { RenderPipeline } from \"three/webgpu\";\nimport { assert } from \"@/assert.js\";\nimport type { BloomEffect, Effect, VignetteEffect } from \"@/render/effect.js\";\n\ntype EffectTuner = (effect: Effect) => void;\n\ninterface Pass {\n output: Node<\"vec4\">;\n tune: EffectTuner;\n}\n\ninterface PassInput<E extends Effect> {\n scenePass: Node<\"vec4\">;\n input: Node<\"vec4\">;\n effect: E;\n}\n\nexport interface PostChain {\n /**\n * Renders the post-processing chain to the screen. Synchronous; the renderer\n * must already be initialized.\n */\n render(): void;\n\n /**\n * Builds the scene's pipelines without blocking the GPU queue. Pipelines not\n * built beforehand are compiled on first draw, stalling that frame.\n */\n compile(): Promise<void>;\n\n /**\n * Updates the post-processing effect chain parameters in place.\n */\n tune(effects: readonly Effect[]): void;\n}\n\nconst build = {\n bloom({ scenePass, input, effect }: PassInput<BloomEffect>): Pass {\n const intensity = uniform(effect.intensity);\n const radius = uniform(effect.radius);\n const threshold = uniform(effect.threshold);\n return {\n output: input.add(bloom(scenePass, intensity, radius, threshold)),\n tune(next) {\n assert(next.type === \"bloom\", `a bloom pass cannot be tuned by a ${next.type} effect`);\n intensity.value = next.intensity;\n radius.value = next.radius;\n threshold.value = next.threshold;\n },\n };\n },\n\n vignette({ input, effect }: PassInput<VignetteEffect>): Pass {\n const intensity = uniform(effect.intensity);\n const smoothness = uniform(effect.smoothness);\n const distance = length(screenUV.sub(0.5));\n const inner = smoothness.mul(0.5);\n const edge = smoothstep(inner, 0.75, distance).mul(intensity);\n return {\n output: input.mul(float(1).sub(edge)),\n tune(next) {\n assert(next.type === \"vignette\", `a vignette pass cannot be tuned by a ${next.type} effect`);\n intensity.value = next.intensity;\n smoothness.value = next.smoothness;\n },\n };\n },\n};\n\ninterface PostOptions {\n renderer: WebGPURenderer;\n scene: Scene;\n camera: Camera;\n effects: readonly Effect[];\n}\n\n/**\n * Builds a post-processing chain that applies the specified `effects` in order.\n */\nexport function post({ renderer, scene, camera, effects }: PostOptions): PostChain {\n if (effects.length === 0) {\n return {\n render: () => renderer.render(scene, camera),\n compile: () => renderer.compileAsync(scene, camera),\n tune() {},\n };\n }\n\n const scenePass = pass(scene, camera);\n let output: Node<\"vec4\"> = scenePass;\n const tuners: EffectTuner[] = [];\n\n for (const effect of effects) {\n const built =\n effect.type === \"bloom\"\n ? build.bloom({ scenePass, input: output, effect })\n : build.vignette({ scenePass, input: output, effect });\n output = built.output;\n tuners.push(built.tune);\n }\n\n const post = new RenderPipeline(renderer);\n post.outputNode = output;\n\n return {\n render: () => post.render(),\n compile: () => {\n // The pass takes the renderer's sample count on its first render, so a\n // compile ahead of that would build single-sampled pipelines.\n scenePass.renderTarget.samples = renderer.samples;\n return scenePass.compileAsync(renderer);\n },\n tune(next: readonly Effect[]): void {\n assert(next.length === tuners.length, `a chain of ${tuners.length} effects cannot be tuned by ${next.length}`);\n for (const [index, tune] of tuners.entries()) {\n const effect = next[index];\n assert(effect !== undefined, `effect ${index} is missing from a chain of ${next.length}`);\n tune(effect);\n }\n },\n };\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import { type GlyphCloud, type GlyphCloudOptions } from "../../render/backend/cloud.js";
2
+ import type { ParticleGlyph, SolidGlyph } from "../../render/layer.js";
3
+ /**
4
+ * Whether `shape` is drawn as a solid.
5
+ */
6
+ export declare function isSolid(shape: ParticleGlyph): shape is SolidGlyph;
7
+ /**
8
+ * Builds a glyph cloud that draws one shaded solid per particle.
9
+ */
10
+ export declare function buildSolidCloud({ shape, style, sizeScale }: GlyphCloudOptions<SolidGlyph>): GlyphCloud;