@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.
- package/dist/ablation.d.ts +74 -0
- package/dist/ablation.js +117 -0
- package/dist/atmosphere-nodes.js +100 -12
- package/dist/atmosphere-shaders.js +17 -5
- package/dist/baseline/exposure.d.ts +1 -1
- package/dist/baseline/exposure.js +18 -5
- package/dist/crowd.d.ts +21 -0
- package/dist/crowd.js +121 -0
- package/dist/decals-plugin.d.ts +5 -0
- package/dist/decals-plugin.js +54 -0
- package/dist/decals.d.ts +69 -0
- package/dist/decals.js +326 -0
- package/dist/deferred.js +4 -1
- package/dist/display-nodes.js +24 -11
- package/dist/foliage-budget.d.ts +64 -0
- package/dist/foliage-budget.js +108 -0
- package/dist/foliage-path.d.ts +19 -0
- package/dist/foliage-path.js +7 -0
- package/dist/foliage-shaders.d.ts +11 -0
- package/dist/foliage-shaders.js +504 -0
- package/dist/foliage.d.ts +194 -0
- package/dist/foliage.js +1027 -0
- package/dist/forward.js +26 -2
- package/dist/graph.d.ts +34 -0
- package/dist/graph.js +139 -14
- package/dist/index.d.ts +11 -1
- package/dist/index.js +57 -0
- package/dist/overlay-registry.d.ts +3 -1
- package/dist/overlay-registry.js +3 -1
- package/dist/overlays.js +5 -2
- package/dist/perf-overlay.d.ts +9 -0
- package/dist/perf-overlay.js +125 -0
- package/dist/plugin.d.ts +11 -1
- package/dist/plugin.js +33 -2
- package/dist/post-common.d.ts +20 -2
- package/dist/post-common.js +79 -2
- package/dist/post-nodes.d.ts +5 -0
- package/dist/post-nodes.js +136 -104
- package/dist/post-scene.d.ts +130 -0
- package/dist/post-scene.js +164 -0
- package/dist/post-shaders.js +41 -63
- package/dist/post.js +11 -1
- package/dist/ribbons-plugin.d.ts +5 -0
- package/dist/ribbons-plugin.js +59 -0
- package/dist/ribbons.d.ts +44 -0
- package/dist/ribbons.js +288 -0
- package/dist/scenario.d.ts +70 -0
- package/dist/scenario.js +125 -0
- package/dist/shaders.js +58 -5
- package/dist/shadows.d.ts +2 -1
- package/dist/shadows.js +3 -1
- package/dist/standard.js +6 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +6 -0
- package/dist/timer.d.ts +16 -1
- package/dist/timer.js +66 -19
- package/dist/vfx-draw.d.ts +37 -0
- package/dist/vfx-draw.js +291 -0
- package/dist/vfx-state.d.ts +42 -0
- package/dist/vfx-state.js +81 -0
- package/dist/vfx.d.ts +69 -0
- package/dist/vfx.js +103 -0
- 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
|
+
};
|
package/dist/post-shaders.js
CHANGED
|
@@ -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::
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
515
|
-
|
|
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
|
-
|
|
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,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
|
+
};
|