@aethervtt/shard-render 0.1.0 → 0.2.1

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 (63) hide show
  1. package/dist/ablation.d.ts +74 -0
  2. package/dist/ablation.js +117 -0
  3. package/dist/atmosphere-nodes.js +100 -12
  4. package/dist/atmosphere-shaders.js +17 -5
  5. package/dist/baseline/exposure.d.ts +1 -1
  6. package/dist/baseline/exposure.js +18 -5
  7. package/dist/crowd.d.ts +21 -0
  8. package/dist/crowd.js +121 -0
  9. package/dist/decals-plugin.d.ts +5 -0
  10. package/dist/decals-plugin.js +54 -0
  11. package/dist/decals.d.ts +69 -0
  12. package/dist/decals.js +326 -0
  13. package/dist/deferred.js +4 -1
  14. package/dist/display-nodes.js +24 -11
  15. package/dist/foliage-budget.d.ts +64 -0
  16. package/dist/foliage-budget.js +108 -0
  17. package/dist/foliage-path.d.ts +19 -0
  18. package/dist/foliage-path.js +7 -0
  19. package/dist/foliage-shaders.d.ts +11 -0
  20. package/dist/foliage-shaders.js +504 -0
  21. package/dist/foliage.d.ts +194 -0
  22. package/dist/foliage.js +1027 -0
  23. package/dist/forward.js +26 -2
  24. package/dist/graph.d.ts +34 -0
  25. package/dist/graph.js +139 -14
  26. package/dist/index.d.ts +11 -1
  27. package/dist/index.js +57 -0
  28. package/dist/overlay-registry.d.ts +3 -1
  29. package/dist/overlay-registry.js +3 -1
  30. package/dist/overlays.js +5 -2
  31. package/dist/perf-overlay.d.ts +9 -0
  32. package/dist/perf-overlay.js +125 -0
  33. package/dist/plugin.d.ts +11 -1
  34. package/dist/plugin.js +33 -2
  35. package/dist/post-common.d.ts +20 -2
  36. package/dist/post-common.js +79 -2
  37. package/dist/post-nodes.d.ts +5 -0
  38. package/dist/post-nodes.js +136 -104
  39. package/dist/post-scene.d.ts +130 -0
  40. package/dist/post-scene.js +164 -0
  41. package/dist/post-shaders.js +41 -63
  42. package/dist/post.js +11 -1
  43. package/dist/ribbons-plugin.d.ts +5 -0
  44. package/dist/ribbons-plugin.js +59 -0
  45. package/dist/ribbons.d.ts +44 -0
  46. package/dist/ribbons.js +288 -0
  47. package/dist/scenario.d.ts +70 -0
  48. package/dist/scenario.js +125 -0
  49. package/dist/shaders.js +58 -5
  50. package/dist/shadows.d.ts +2 -1
  51. package/dist/shadows.js +3 -1
  52. package/dist/standard.js +6 -0
  53. package/dist/testing.d.ts +1 -0
  54. package/dist/testing.js +6 -0
  55. package/dist/timer.d.ts +16 -1
  56. package/dist/timer.js +66 -19
  57. package/dist/vfx-draw.d.ts +37 -0
  58. package/dist/vfx-draw.js +291 -0
  59. package/dist/vfx-state.d.ts +42 -0
  60. package/dist/vfx-state.js +81 -0
  61. package/dist/vfx.d.ts +69 -0
  62. package/dist/vfx.js +103 -0
  63. package/package.json +19 -11
@@ -0,0 +1,164 @@
1
+ import {
2
+ defineComponent,
3
+ defineSystem,
4
+ quat,
5
+ Rng,
6
+ t,
7
+ Update
8
+ } from "@aethervtt/shard-core";
9
+ import { cube, plane, sphere } from "@aethervtt/shard-mesh";
10
+ import { definePlugin, Time } from "@aethervtt/shard-runtime";
11
+ import { Transform } from "@aethervtt/shard-transform";
12
+ import { MaterialAsset, Materials, Meshes } from "./assets.js";
13
+ import { PhysicalCamera } from "./camera.js";
14
+ import { DefaultEnvironment } from "./environment.js";
15
+ import { Mesh3d, MeshMaterial } from "./instances.js";
16
+ import { DirectionalLight } from "./lights.js";
17
+ import {
18
+ Antialiasing,
19
+ AutoExposure,
20
+ Bloom,
21
+ ColorGrading,
22
+ DepthOfField,
23
+ Fog,
24
+ MotionBlur,
25
+ Ssao,
26
+ Vignette
27
+ } from "./post.js";
28
+ const POST_EFFECTS = {
29
+ bloom: [Bloom, { intensity: 0.12 }],
30
+ exposure: [AutoExposure, { compensation: 0.5 }],
31
+ dof: [DepthOfField, { focusDistance: 9, maxBlur: 0.012 }],
32
+ "motion-blur": [MotionBlur, {}],
33
+ taa: [Antialiasing, { mode: "taa" }],
34
+ fxaa: [Antialiasing, { mode: "fxaa" }],
35
+ noaa: [Antialiasing, { mode: "none" }],
36
+ ssao: [Ssao, { radius: 0.6 }],
37
+ fog: [Fog, { density: 0.015, heightFalloff: 0.08, sunScattering: 0.6 }],
38
+ grading: [ColorGrading, { temperature: 0.1, saturation: 1.1, contrast: 1.05 }],
39
+ vignette: [Vignette, { intensity: 0.25 }]
40
+ };
41
+ const ALL_POST_EFFECTS = Object.keys(POST_EFFECTS).filter((n) => n !== "fxaa" && n !== "noaa");
42
+ function postEffects(names) {
43
+ return names.map((n) => POST_EFFECTS[n]).filter((e) => e !== void 0);
44
+ }
45
+ const PostSpin = defineComponent("render/PostSpin", {
46
+ radius: t.f32,
47
+ speed: t.f32,
48
+ height: t.f32
49
+ });
50
+ const postSpin = defineSystem({
51
+ name: "render/post-spin",
52
+ setup: (world) => ({ q: world.query({ with: [PostSpin, Transform] }) }),
53
+ run: ({ q }, world) => {
54
+ const time = world.resource(Time).elapsed;
55
+ for (const table of q.tables) {
56
+ const r = table.column(PostSpin, "radius");
57
+ const s = table.column(PostSpin, "speed");
58
+ const h = table.column(PostSpin, "height");
59
+ const tr = table.column(Transform, "translation");
60
+ for (let i = 0; i < table.count; i++) {
61
+ const a = time * s[i];
62
+ tr[i * 3] = Math.cos(a) * r[i];
63
+ tr[i * 3 + 1] = h[i];
64
+ tr[i * 3 + 2] = Math.sin(a) * r[i] - 4;
65
+ }
66
+ table.markChanged(Transform);
67
+ }
68
+ }
69
+ });
70
+ const postScenePlugin = definePlugin({
71
+ name: "render/post-scene",
72
+ dependencies: ["render"],
73
+ provides: [PostSpin],
74
+ build(app) {
75
+ app.addSystems(Update, postSpin);
76
+ }
77
+ });
78
+ const POST_EYE = [0, 2.2, 9];
79
+ const POST_TARGET = [0, 1.2, -2];
80
+ function postCamera(seconds) {
81
+ const [tx, ty, tz] = POST_TARGET;
82
+ const dx = POST_EYE[0] - tx;
83
+ const dy = POST_EYE[1] - ty;
84
+ const dz = POST_EYE[2] - tz;
85
+ const flat = Math.sqrt(dx * dx + dz * dz);
86
+ const yaw = Math.atan2(dx, dz) + Math.PI / 6 * Math.sin(seconds * 2 * Math.PI / 20);
87
+ const distance = flat + 1.5 * Math.sin(seconds * 2 * Math.PI / 13);
88
+ return {
89
+ eye: [tx + Math.sin(yaw) * distance, ty + dy, tz + Math.cos(yaw) * distance],
90
+ target: [tx, ty, tz]
91
+ };
92
+ }
93
+ const POST_LENS = [
94
+ PhysicalCamera,
95
+ { aperture: 5.6, shutterSpeed: 1 / 1e3, iso: 100 }
96
+ ];
97
+ function spawnPostScene(world) {
98
+ world.resource(DefaultEnvironment).sky = { turbidity: 2.5 };
99
+ world.spawn(
100
+ [DirectionalLight, { illuminance: 1e5, shadows: true }],
101
+ [Transform, { rotation: quat.fromEuler([0, 0, 0, 1], -0.5, 0.9, 0) }]
102
+ );
103
+ const meshes = world.resource(Meshes);
104
+ const materials = world.resource(Materials);
105
+ const mat = (v) => materials.add(new MaterialAsset(v));
106
+ world.spawn(
107
+ [Mesh3d, { mesh: meshes.add(plane({ size: 400 })) }],
108
+ [MeshMaterial, { material: mat({ baseColor: [0.35, 0.33, 0.3, 1], roughness: 0.9 }) }],
109
+ Transform
110
+ );
111
+ const block = meshes.add(cube({ size: 1 }));
112
+ const ball = meshes.add(sphere({ radius: 0.5, segments: 40 }));
113
+ const rng = new Rng(4);
114
+ const stone = [
115
+ mat({ baseColor: [0.7, 0.68, 0.64, 1], roughness: 0.7 }),
116
+ mat({ baseColor: [0.55, 0.3, 0.22, 1], roughness: 0.8 }),
117
+ mat({ baseColor: [0.9, 0.8, 0.55, 1], metallic: 1, roughness: 0.3 })
118
+ ];
119
+ for (let i = 0; i < 60; i++) {
120
+ const size = rng.range(0.6, 2.2);
121
+ world.spawn(
122
+ [Mesh3d, { mesh: i % 3 === 2 ? ball : block }],
123
+ [MeshMaterial, { material: stone[i % 3] }],
124
+ [
125
+ Transform,
126
+ {
127
+ translation: [rng.range(-14, 14), size / 2, rng.range(-30, 2)],
128
+ rotation: quat.fromEuler([0, 0, 0, 1], 0, rng.float() * 6, 0),
129
+ scale: [size, size * rng.range(0.6, 2.4), size]
130
+ }
131
+ ]
132
+ );
133
+ }
134
+ const neon = (c) => mat({ baseColor: [0, 0, 0, 1], emissive: c, emissiveLuminance: 4e5 });
135
+ const panels = [neon([1, 0.2, 0.6, 1]), neon([0.2, 0.8, 1, 1]), neon([1, 0.7, 0.2, 1])];
136
+ for (let i = 0; i < 3; i++) {
137
+ world.spawn(
138
+ [Mesh3d, { mesh: block }],
139
+ [MeshMaterial, { material: panels[i] }],
140
+ [Transform, { translation: [(i - 1) * 3, 2.5, -5], scale: [2, 0.15, 0.1] }]
141
+ );
142
+ }
143
+ for (let i = 0; i < 4; i++) {
144
+ world.spawn(
145
+ [Mesh3d, { mesh: ball }],
146
+ [MeshMaterial, { material: stone[2] }],
147
+ [PostSpin, { radius: 3 + i, speed: 1.2 - i * 0.2, height: 0.6 + i * 0.3 }],
148
+ Transform
149
+ );
150
+ }
151
+ }
152
+ export {
153
+ ALL_POST_EFFECTS,
154
+ POST_EFFECTS,
155
+ POST_EYE,
156
+ POST_LENS,
157
+ POST_TARGET,
158
+ PostSpin,
159
+ postCamera,
160
+ postEffects,
161
+ postScenePlugin,
162
+ postSpin,
163
+ spawnPostScene
164
+ };
@@ -122,11 +122,11 @@ import shard::prepass::common::{ PrepassVertex, PrepassOutput, prepass_vertex, p
122
122
  @if(BASELINE) @fragment fn fs_depth(in: PrepassVertex) {
123
123
  @if(CUTAWAY) cutaway_clip(in.world_position, in.clip.xy, in.flags);
124
124
  }`,
125
- "shard::post::fog": `
125
+ "shard::post::fog::common": `
126
126
  import shard::view::view;
127
127
  import shard::pbr::lights::directional;
128
128
  import shard::pbr::environment::env_irradiance;
129
- import shard::post::common::{ uv_of, world_at };
129
+ import shard::post::common::world_at;
130
130
 
131
131
  struct FogParams {
132
132
  color: vec4f,
@@ -136,31 +136,27 @@ struct FogParams {
136
136
  sun: f32,
137
137
  }
138
138
 
139
- @group(1) @binding(0) var input: texture_2d<f32>;
140
- @group(1) @binding(1) var depth_texture: texture_depth_2d;
141
- @group(1) @binding(2) var<uniform> fog: FogParams;
142
-
143
139
  const PI: f32 = 3.14159265;
144
140
 
145
141
  /** Optical depth of exponential height fog along the ray from t0 to t1 (meters). */
146
- fn optical_depth(y0: f32, dir_y: f32, t0: f32, t1: f32) -> f32 {
142
+ fn optical_depth(fog: FogParams, y0: f32, dir_y: f32, t0: f32, t1: f32) -> f32 {
147
143
  let base = fog.density * exp(-fog.falloff * y0);
148
144
  let k = fog.falloff * dir_y;
149
145
  if (abs(k) < 1e-5) { return base * (t1 - t0); }
150
146
  return base * (exp(-k * t0) - exp(-k * t1)) / k;
151
147
  }
152
148
 
153
- @fragment fn fs(@builtin(position) frag: vec4f) -> @location(0) vec4f {
154
- let px = vec2i(frag.xy);
155
- let c = textureLoad(input, px, 0);
156
- let depth = textureLoad(depth_texture, px, 0);
157
- let uv = uv_of(frag.xy);
149
+ /**
150
+ * Height fog over color \`c\` at a pixel (\`uv\`, its depth): the fog pass, and the atmosphere's
151
+ * aerial perspective when a view has both (one pass for the two).
152
+ */
153
+ fn fog_apply(c: vec4f, uv: vec2f, depth: f32, fog: FogParams) -> vec4f {
158
154
  // The background is far away, not infinitely: a floor keeps the ray finite.
159
155
  let world = world_at(uv, max(depth, 1e-7));
160
156
  let to = world - view.cameraPosition;
161
157
  let dist = length(to);
162
158
  let dir = to / max(dist, 1e-6);
163
- let tau = optical_depth(view.cameraPosition.y, dir.y, fog.start, max(dist, fog.start));
159
+ let tau = optical_depth(fog, view.cameraPosition.y, dir.y, fog.start, max(dist, fog.start));
164
160
  let transmittance = exp(-tau);
165
161
  // In-scattering: the sky (environment or ambient), plus the sun through a forward-peaked phase.
166
162
  var sky = view.ambient;
@@ -178,6 +174,18 @@ fn optical_depth(y0: f32, dir_y: f32, t0: f32, t1: f32) -> f32 {
178
174
  @if(!TRANSPARENT) return vec4f(color, c.a);
179
175
  // Premultiplied (0052): the fog is a layer of its own opacity over what's behind it.
180
176
  @if(TRANSPARENT) return vec4f(color, c.a * transmittance + (1.0 - transmittance));
177
+ }`,
178
+ "shard::post::fog": `
179
+ import shard::post::common::{ uv_of, post_out };
180
+ import shard::post::fog::common::{ FogParams, fog_apply };
181
+
182
+ @group(1) @binding(0) var input: texture_2d<f32>;
183
+ @group(1) @binding(1) var depth_texture: texture_depth_2d;
184
+ @group(1) @binding(2) var<uniform> fog: FogParams;
185
+
186
+ @fragment fn fs(@builtin(position) frag: vec4f) -> @location(0) vec4f {
187
+ let px = vec2i(frag.xy);
188
+ return post_out(fog_apply(textureLoad(input, px, 0), uv_of(frag.xy), textureLoad(depth_texture, px, 0), fog));
181
189
  }`,
182
190
  "shard::post::bloom": `
183
191
  import shard::color::luminance;
@@ -192,7 +200,6 @@ struct BloomParams {
192
200
  @group(0) @binding(0) var source: texture_2d<f32>;
193
201
  @group(0) @binding(1) var source_sampler: sampler;
194
202
  @group(0) @binding(2) var<uniform> params: BloomParams;
195
- @group(0) @binding(3) var scene: texture_2d<f32>;
196
203
 
197
204
  fn tap(uv: vec2f, dx: f32, dy: f32) -> vec3f {
198
205
  return textureSampleLevel(source, source_sampler, uv + vec2f(dx, dy) * params.threshold.zw, 0.0).rgb;
@@ -243,27 +250,24 @@ struct FullscreenInput { @builtin(position) clip: vec4f, @location(0) uv: vec2f
243
250
  + (tap(in.uv, -1.0, 0.0) + tap(in.uv, 1.0, 0.0) + tap(in.uv, 0.0, -1.0) + tap(in.uv, 0.0, 1.0)) * 2.0
244
251
  + tap(in.uv, -1.0, -1.0) + tap(in.uv, 1.0, -1.0) + tap(in.uv, -1.0, 1.0) + tap(in.uv, 1.0, 1.0);
245
252
  return vec4f(c / 16.0, 1.0);
246
- }
247
-
248
- /** The scene plus the glow: mixed (energy-conserving) without a threshold, added with one. */
249
- @fragment fn composite(in: FullscreenInput) -> @location(0) vec4f {
250
- let s = textureLoad(scene, vec2i(in.clip.xy), 0);
251
- let glow = textureSampleLevel(source, source_sampler, in.uv, 0.0).rgb / params.mix.y;
252
- let c = select(s.rgb + glow * params.mix.x, mix(s.rgb, glow, params.mix.x), params.mix.z > 0.5);
253
- return vec4f(c, s.a);
254
253
  }`,
255
254
  "shard::post::exposure": `
256
255
  import shard::view::view;
257
256
  import shard::color::luminance;
257
+ @if(BLOOM) import shard::post::glow::add_glow;
258
258
 
259
259
  struct MeterParams {
260
260
  /** EV of bin 0, bins per EV, metering mode, 0. */
261
261
  range: vec4f,
262
+ /** Bloom, which the tonemap adds to the image: intensity, level count, mix (1) or add (0). */
263
+ glow: vec4f,
262
264
  }
263
265
 
264
266
  @group(0) @binding(1) var input: texture_2d<f32>;
265
267
  @group(0) @binding(2) var<storage, read_write> histogram: array<atomic<u32>, 256>;
266
268
  @group(0) @binding(3) var<uniform> params: MeterParams;
269
+ @if(BLOOM) @group(0) @binding(4) var bloom: texture_2d<f32>;
270
+ @if(BLOOM) @group(0) @binding(5) var bloom_sampler: sampler;
267
271
 
268
272
  var<workgroup> local: array<atomic<u32>, 256>;
269
273
 
@@ -279,7 +283,12 @@ fn meter(@builtin(global_invocation_id) id: vec3u, @builtin(local_invocation_ind
279
283
  let size = textureDimensions(input);
280
284
  let px = id.xy * 4u + 2u;
281
285
  if (px.x < size.x && px.y < size.y) {
282
- let c = textureLoad(input, vec2i(px), 0).rgb;
286
+ var c = textureLoad(input, vec2i(px), 0).rgb;
287
+ // What the tonemap shows: the image with bloom's glow over it.
288
+ @if(BLOOM) {
289
+ let uv = (vec2f(px) + 0.5) / vec2f(size);
290
+ c = add_glow(c, textureSampleLevel(bloom, bloom_sampler, uv, 0.0).rgb, params.glow);
291
+ }
283
292
  let l = luminance(c) / max(view.exposure, 1e-20);
284
293
  let ev = log2(max(l, 1e-10) * 8.0);
285
294
  let bin = u32(clamp((ev - params.range.x) * params.range.y, 0.0, 255.0));
@@ -296,7 +305,7 @@ fn meter(@builtin(global_invocation_id) id: vec3u, @builtin(local_invocation_ind
296
305
  }`,
297
306
  "shard::post::dof": `
298
307
  import shard::view::view;
299
- import shard::post::common::{ uv_of, world_at, view_depth };
308
+ import shard::post::common::{ uv_of, world_at, view_depth, post_out };
300
309
  import shard::color::luminance;
301
310
 
302
311
  struct DofParams {
@@ -377,11 +386,11 @@ const TAPS: i32 = 48;
377
386
  let sharp = textureLoad(input, px, 0);
378
387
  let blurred = textureSampleLevel(half_texture, linear_sampler, uv_of(frag.xy), 0.0);
379
388
  let t = smoothstep(0.5, 1.5, abs(coc_at(px)));
380
- return vec4f(mix(sharp.rgb, blurred.rgb, t), sharp.a);
389
+ return post_out(vec4f(mix(sharp.rgb, blurred.rgb, t), sharp.a));
381
390
  }`,
382
391
  "shard::post::motion_blur": `
383
392
  import shard::view::view;
384
- import shard::post::common::{ uv_of, motion };
393
+ import shard::post::common::{ uv_of, motion, post_out };
385
394
 
386
395
  struct MotionParams {
387
396
  /** Shutter fraction of a frame, max blur in pixels, samples, 0. */
@@ -403,14 +412,14 @@ struct MotionParams {
403
412
  let cap = motion_params.params.y;
404
413
  if (pixels > cap) { v *= cap / pixels; }
405
414
  let center = textureLoad(input, px, 0);
406
- if (pixels < 0.5) { return center; }
415
+ if (pixels < 0.5) { return post_out(center); }
407
416
  let n = i32(motion_params.params.z);
408
417
  var sum = vec3f(0.0);
409
418
  for (var i = 0; i < n; i++) {
410
419
  let t = (f32(i) + 0.5) / f32(n) - 0.5;
411
420
  sum += textureSampleLevel(input, linear_sampler, uv + v * t, 0.0).rgb;
412
421
  }
413
- return vec4f(sum / f32(n), center.a);
422
+ return post_out(vec4f(sum / f32(n), center.a));
414
423
  }`,
415
424
  "shard::post::taa": `
416
425
  import shard::view::view;
@@ -429,11 +438,6 @@ struct TaaParams {
429
438
  @group(0) @binding(5) var linear_sampler: sampler;
430
439
  @group(0) @binding(6) var<uniform> taa: TaaParams;
431
440
 
432
- struct TaaOutput {
433
- @location(0) color: vec4f,
434
- @location(1) history: vec4f,
435
- }
436
-
437
441
  fn rgb_to_ycocg(c: vec3f) -> vec3f {
438
442
  return vec3f(0.25 * c.r + 0.5 * c.g + 0.25 * c.b, 0.5 * c.r - 0.5 * c.b, -0.25 * c.r + 0.5 * c.g - 0.25 * c.b);
439
443
  }
@@ -469,7 +473,7 @@ fn sample_history(uv: vec2f) -> vec3f {
469
473
  return max(c / w, vec3f(0.0));
470
474
  }
471
475
 
472
- @fragment fn fs(@builtin(position) frag: vec4f) -> TaaOutput {
476
+ @fragment fn fs(@builtin(position) frag: vec4f) -> @location(0) vec4f {
473
477
  let px = vec2i(frag.xy);
474
478
  let size = vec2i(textureDimensions(input));
475
479
  let uv = uv_of(frag.xy);
@@ -511,17 +515,12 @@ fn sample_history(uv: vec2f) -> vec3f {
511
515
  if (m > 1.0) { h = center + offset / m; }
512
516
  let blended = mix(ycocg_to_rgb(h), current, alpha);
513
517
  let color = expand(max(blended, vec3f(0.0)));
514
- var out: TaaOutput;
515
- @if(!TRANSPARENT) {
516
- out.color = vec4f(color, 1.0);
517
- out.history = vec4f(color, 1.0);
518
- }
518
+ // The output is also next frame's history.
519
+ var out = vec4f(color, 1.0);
519
520
  @if(TRANSPARENT) {
520
521
  // Coverage resolves like color: history clamped to the neighborhood, blended at the same rate.
521
522
  let past = clamp(textureSampleLevel(history, linear_sampler, prev_uv, 0.0).a, alpha_lo, alpha_hi);
522
- let coverage = mix(past, textureLoad(input, px, 0).a, alpha);
523
- out.color = vec4f(color, coverage);
524
- out.history = vec4f(color, coverage);
523
+ out.a = mix(past, textureLoad(input, px, 0).a, alpha);
525
524
  }
526
525
  return out;
527
526
  }`,
@@ -542,7 +541,6 @@ struct SsaoParams {
542
541
  @group(0) @binding(1) var depth_texture: texture_depth_2d;
543
542
  @group(0) @binding(2) var normal_texture: texture_2d<f32>;
544
543
  @group(0) @binding(3) var<uniform> ssao: SsaoParams;
545
- @group(0) @binding(4) var half_ao: texture_2d<f32>;
546
544
 
547
545
  const PI: f32 = 3.14159265;
548
546
 
@@ -609,26 +607,6 @@ fn view_position(px: vec2i) -> vec3f {
609
607
  }
610
608
  let ao = clamp(visibility / f32(dirs), 0.0, 1.0);
611
609
  return vec4f(pow(ao, ssao.params.y), -p.z, 0.0, 1.0);
612
- }
613
-
614
- /** Back to full resolution: the four nearest half-resolution texels, weighted by depth match. */
615
- @fragment fn upsample(@builtin(position) frag: vec4f) -> @location(0) vec4f {
616
- let px = vec2i(frag.xy);
617
- let depth = textureLoad(depth_texture, px, 0);
618
- if (depth <= 0.0) { return vec4f(1.0); }
619
- let z = -view_position(px).z;
620
- let half_size = vec2i(textureDimensions(half_ao));
621
- let base = vec2i(floor((frag.xy - 1.0) * 0.5));
622
- var sum = 0.0;
623
- var weight = 0.0;
624
- for (var k = 0; k < 4; k++) {
625
- let q = clamp(base + vec2i(k & 1, k >> 1), vec2i(0), half_size - 1);
626
- let s = textureLoad(half_ao, q, 0);
627
- let w = 1.0 / (1e-3 + abs(s.y - z) / max(z, 1e-3));
628
- sum += s.x * w;
629
- weight += w;
630
- }
631
- return vec4f(sum / max(weight, 1e-6), 0.0, 0.0, 1.0);
632
610
  }`
633
611
  };
634
612
  const FXAA_SHADERS = {
package/dist/post.js CHANGED
@@ -403,9 +403,19 @@ function postAliases(cam, base) {
403
403
  let flip = false;
404
404
  for (const [name, bit] of HDR_CHAIN) {
405
405
  if ((effects & bit) === 0) continue;
406
+ if (name === "fog" && effects & PostEffect.Atmosphere) continue;
407
+ if (name === "bloom") {
408
+ out["bloom-in"] = current;
409
+ continue;
410
+ }
411
+ out[`${name}-in`] = current;
412
+ if (name === "taa") {
413
+ out["taa-out"] = "taa-history";
414
+ current = "taa-history";
415
+ continue;
416
+ }
406
417
  const next = flip ? "post-b" : "post-a";
407
418
  flip = !flip;
408
- out[`${name}-in`] = current;
409
419
  out[`${name}-out`] = next;
410
420
  current = next;
411
421
  }
@@ -0,0 +1,5 @@
1
+ import { VfxRenderer } from './vfx-draw.js';
2
+ export declare const RibbonGpu: import("@aethervtt/shard-core").ResourceDef<{
3
+ renderer?: VfxRenderer;
4
+ }>;
5
+ export declare const ribbonsPlugin: import("@aethervtt/shard-runtime").Plugin;
@@ -0,0 +1,59 @@
1
+ import { defineResource, defineSystem, Last, PostUpdate } from "@aethervtt/shard-core";
2
+ import { definePlugin, FrameDemand } from "@aethervtt/shard-runtime";
3
+ import { OriginShift, TransformSystems } from "@aethervtt/shard-transform";
4
+ import { addRenderFeatures } from "./features.js";
5
+ import { Graph, RenderDescribers, RenderSet, Shaders } from "./plugin.js";
6
+ import { describeRibbons, Ribbons, shiftRibbons, updateRibbons } from "./ribbons.js";
7
+ import { registerShaders } from "./shaders.js";
8
+ import { RibbonsPath } from "./vfx.js";
9
+ import { VFX_SHADERS, VfxRenderer } from "./vfx-draw.js";
10
+ import { computeVisibility } from "./visibility.js";
11
+ const RibbonGpu = defineResource("render/RibbonGpu", {
12
+ init: () => ({})
13
+ });
14
+ const prune = defineSystem({
15
+ name: "render/ribbons-prune",
16
+ run: (_, world) => world.resource(RibbonGpu).renderer?.prune(world)
17
+ });
18
+ const ribbonsPlugin = definePlugin({
19
+ name: "render/ribbons",
20
+ dependencies: ["render/forward"],
21
+ provides: [Ribbons, RibbonGpu, updateRibbons],
22
+ build(app) {
23
+ const w = app.world;
24
+ w.initResource(RibbonsPath);
25
+ w.initResource(Ribbons);
26
+ w.initResource(RibbonGpu);
27
+ w.observe(
28
+ OriginShift,
29
+ ({ data }) => shiftRibbons(w, data.offset[0], data.offset[1], data.offset[2])
30
+ );
31
+ app.addSystems(PostUpdate, updateRibbons.after(TransformSystems, computeVisibility));
32
+ app.addSystems(Last, prune.inSet(RenderSet.Prepare));
33
+ },
34
+ ready(app) {
35
+ const w = app.world, renderer = new VfxRenderer(w.resource(Ribbons).records, false);
36
+ w.resource(RibbonGpu).renderer = renderer;
37
+ registerShaders(w.resource(Shaders), VFX_SHADERS);
38
+ addRenderFeatures(w, {
39
+ name: "render/ribbons",
40
+ description: "Bounded trails and beams.",
41
+ nodes: ["ribbons"],
42
+ baseline: { strategy: "The same vertex attributes, uniform buffers and fragment passes" }
43
+ });
44
+ w.resource(Graph).addNode("ribbons", renderer.node());
45
+ w.initResource(RenderDescribers).set("ribbons", describeRibbons);
46
+ },
47
+ dispose(app) {
48
+ app.world.resource(RibbonGpu).renderer?.dispose();
49
+ const s = app.world.resource(Ribbons);
50
+ s.records.length = 0;
51
+ s.trails.clear();
52
+ s.beams.clear();
53
+ app.world.tryResource(FrameDemand)?.set("render/ribbons", false);
54
+ }
55
+ });
56
+ export {
57
+ RibbonGpu,
58
+ ribbonsPlugin
59
+ };
@@ -0,0 +1,44 @@
1
+ import { type World } from '@aethervtt/shard-core';
2
+ import { type VfxRecord } from './vfx-state.js';
3
+ export interface RibbonRecord extends VfxRecord {
4
+ points: Float64Array;
5
+ capacity: number;
6
+ head: number;
7
+ pointsCount: number;
8
+ distance: number;
9
+ targetMatrix: Float64Array;
10
+ scratch: Float64Array;
11
+ }
12
+ export declare const Ribbons: import("@aethervtt/shard-core").ResourceDef<{
13
+ records: RibbonRecord[];
14
+ trails: Map<number, RibbonRecord>;
15
+ beams: Map<number, RibbonRecord>;
16
+ frame: number;
17
+ rejected: number;
18
+ }>;
19
+ /** Geometry is camera-independent; the vertex shader extrudes shared point tangents per view. */
20
+ export declare function ribbonGeometry(r: RibbonRecord): void;
21
+ export declare const updateRibbons: import("@aethervtt/shard-core").SystemDef<{
22
+ trails: import("@aethervtt/shard-core").Query;
23
+ beams: import("@aethervtt/shard-core").Query;
24
+ }>;
25
+ export declare function shiftRibbons(world: World, x: number, y: number, z: number): void;
26
+ export declare function describeRibbons(world: World): {
27
+ rejected: number;
28
+ error: string | null;
29
+ records: {
30
+ entity: number;
31
+ kind: "beam" | "decal" | "trail";
32
+ vertices: number;
33
+ age: number;
34
+ opacity: number;
35
+ error: string | null;
36
+ visible: boolean;
37
+ texture: string;
38
+ playing: boolean;
39
+ paused: boolean;
40
+ cpuBytes: number;
41
+ points: number;
42
+ capacity: number;
43
+ }[];
44
+ };