@aethervtt/shard-render 0.1.0 → 0.2.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.
- 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,74 @@
|
|
|
1
|
+
import { type World } from '@aethervtt/shard-core';
|
|
2
|
+
/**
|
|
3
|
+
* Per-pass GPU cost by ablation (0075). On tile-based GPUs passes overlap and a pass's timestamps
|
|
4
|
+
* include waiting for earlier work, so per-pass times add up to more than the frame. `gpu:frame`
|
|
5
|
+
* (first pass start to last pass end) stays true, so a pass's cost is how much `gpu:frame` drops
|
|
6
|
+
* with the pass disabled.
|
|
7
|
+
*/
|
|
8
|
+
export interface AblationOptions {
|
|
9
|
+
/** Render-graph nodes to measure, each on its own. */
|
|
10
|
+
passes: readonly string[];
|
|
11
|
+
/** Frames measured per pass and round (the median is kept). Default 120. */
|
|
12
|
+
frames?: number;
|
|
13
|
+
/** Rounds, each measuring every pass and the baseline; the median over rounds is kept. Default 5. */
|
|
14
|
+
rounds?: number;
|
|
15
|
+
/** Frames dropped after each change, while timings of the previous state land. Default 4. */
|
|
16
|
+
settle?: number;
|
|
17
|
+
/** Also measure every pass disabled at once. */
|
|
18
|
+
together?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Names in `passes` that stand for several nodes, disabled together (0075's scenario tests ablate
|
|
21
|
+
* a slice: every node whose `gpu:<node>` its key covers).
|
|
22
|
+
*/
|
|
23
|
+
groups?: Readonly<Record<string, readonly string[]>>;
|
|
24
|
+
}
|
|
25
|
+
export interface PassCost {
|
|
26
|
+
/** The node, or every node joined with '+' for `together`. */
|
|
27
|
+
pass: string;
|
|
28
|
+
/** Median over rounds of baseline `gpu:frame` minus `gpu:frame` with the pass disabled, in ms. */
|
|
29
|
+
ms: number;
|
|
30
|
+
/** Each round's difference. */
|
|
31
|
+
rounds: number[];
|
|
32
|
+
}
|
|
33
|
+
export interface AblationResult {
|
|
34
|
+
/** Median `gpu:frame` with every pass enabled, in ms. */
|
|
35
|
+
frameMs: number;
|
|
36
|
+
passes: PassCost[];
|
|
37
|
+
/** Every pass disabled at once, with `together`. */
|
|
38
|
+
together?: PassCost;
|
|
39
|
+
frames: number;
|
|
40
|
+
rounds: number;
|
|
41
|
+
/** GPU frame timings read, all states together. */
|
|
42
|
+
samples: number;
|
|
43
|
+
}
|
|
44
|
+
export interface PassCostsData {
|
|
45
|
+
/**
|
|
46
|
+
* The latest ablation in this app, and when it finished (`Time.elapsed`, s). The `perf` overlay
|
|
47
|
+
* and the playground HUD rank passes by it where their timestamps overlap (0075).
|
|
48
|
+
*/
|
|
49
|
+
latest: (AblationResult & {
|
|
50
|
+
at: number;
|
|
51
|
+
}) | undefined;
|
|
52
|
+
}
|
|
53
|
+
/** The latest `perf.ablate` result, where the overlay, the HUD and `perf.describe` read it. */
|
|
54
|
+
export declare const PassCosts: import("@aethervtt/shard-core").ResourceDef<PassCostsData>;
|
|
55
|
+
/** How a measurement drives the app: what to disable, and one frame's `gpu:frame`. */
|
|
56
|
+
export interface AblationDriver {
|
|
57
|
+
disable(passes: readonly string[]): void;
|
|
58
|
+
/** Renders a frame and resolves its `gpu:frame` in ms, or undefined when none landed. */
|
|
59
|
+
frame(): Promise<number | undefined>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The measurement, apart from any app: each round measures the baseline, every pass disabled in
|
|
63
|
+
* turn (in reverse order on odd rounds), and the baseline again; a pass's difference in a round is
|
|
64
|
+
* the mean of the two baselines minus its median. Interleaving rounds cancels thermal drift, as
|
|
65
|
+
* `structure/src/cutaway.test.ts` does. Restores every pass before it returns or throws.
|
|
66
|
+
*/
|
|
67
|
+
export declare function runAblation(driver: AblationDriver, options: AblationOptions): Promise<AblationResult>;
|
|
68
|
+
/**
|
|
69
|
+
* Ablates render-graph passes in a running app: `frame` produces one frame (rendering without
|
|
70
|
+
* advancing time, or waiting for the loop). Reads `gpu:frame` from the graph's `GpuTimer`, so the
|
|
71
|
+
* device needs `timestamp-query` (`render/gpu-timing-unavailable` otherwise). Unknown passes throw
|
|
72
|
+
* `render/unknown-node`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function ablatePasses(world: World, options: AblationOptions, frame: () => Promise<void> | void): Promise<AblationResult>;
|
package/dist/ablation.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { defineResource, ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
import { PerfProviders, Time } from "@aethervtt/shard-runtime";
|
|
3
|
+
import { Gpu, Graph } from "./plugin.js";
|
|
4
|
+
const PassCosts = defineResource("render/PassCosts", {
|
|
5
|
+
description: "The latest ablation's per-pass GPU costs (0075): what ranks passes where their timestamps overlap.",
|
|
6
|
+
init: () => ({ latest: void 0 })
|
|
7
|
+
});
|
|
8
|
+
function keep(world, result) {
|
|
9
|
+
const costs = world.initResource(PassCosts);
|
|
10
|
+
costs.latest = { ...result, at: world.tryResource(Time)?.elapsed ?? 0 };
|
|
11
|
+
const sections = world.initResource(PerfProviders).sections;
|
|
12
|
+
if (!sections.has("ablation")) {
|
|
13
|
+
sections.set("ablation", (w) => {
|
|
14
|
+
const latest = w.tryResource(PassCosts)?.latest;
|
|
15
|
+
if (!latest) return void 0;
|
|
16
|
+
return {
|
|
17
|
+
at: latest.at,
|
|
18
|
+
frameMs: latest.frameMs,
|
|
19
|
+
passes: [...latest.passes].sort((a, b) => b.ms - a.ms).map((p) => ({ pass: p.pass, ms: p.ms })),
|
|
20
|
+
...latest.together ? { together: latest.together.ms } : {}
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const median = (values) => {
|
|
26
|
+
if (values.length === 0) return Number.NaN;
|
|
27
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
28
|
+
const mid = sorted.length >> 1;
|
|
29
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
30
|
+
};
|
|
31
|
+
async function runAblation(driver, options) {
|
|
32
|
+
const frames = options.frames ?? 120;
|
|
33
|
+
const rounds = options.rounds ?? 5;
|
|
34
|
+
const settle = options.settle ?? 4;
|
|
35
|
+
const sets = options.passes.map((p) => [p]);
|
|
36
|
+
if (options.together && options.passes.length > 1) sets.push(options.passes);
|
|
37
|
+
let samples = 0;
|
|
38
|
+
const measure = async (disabled) => {
|
|
39
|
+
driver.disable(disabled);
|
|
40
|
+
for (let i = 0; i < settle; i++) await driver.frame();
|
|
41
|
+
const times = [];
|
|
42
|
+
for (let i = 0; i < frames; i++) {
|
|
43
|
+
const ms = await driver.frame();
|
|
44
|
+
if (ms !== void 0) times.push(ms);
|
|
45
|
+
}
|
|
46
|
+
samples += times.length;
|
|
47
|
+
return median(times);
|
|
48
|
+
};
|
|
49
|
+
const diffs = sets.map(() => []);
|
|
50
|
+
const baselines = [];
|
|
51
|
+
try {
|
|
52
|
+
for (let round = 0; round < rounds; round++) {
|
|
53
|
+
const before = await measure([]);
|
|
54
|
+
const order = sets.map((_, i) => i);
|
|
55
|
+
if (round % 2 === 1) order.reverse();
|
|
56
|
+
const off = /* @__PURE__ */ new Map();
|
|
57
|
+
for (const i of order) off.set(i, await measure(sets[i]));
|
|
58
|
+
const after = await measure([]);
|
|
59
|
+
const base = (before + after) / 2;
|
|
60
|
+
baselines.push(base);
|
|
61
|
+
for (let i = 0; i < sets.length; i++) diffs[i].push(base - off.get(i));
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
driver.disable([]);
|
|
65
|
+
}
|
|
66
|
+
const costs = sets.map(
|
|
67
|
+
(set, i) => ({ pass: set.join("+"), ms: median(diffs[i]), rounds: diffs[i] })
|
|
68
|
+
);
|
|
69
|
+
const together = options.together && options.passes.length > 1 ? costs.pop() : void 0;
|
|
70
|
+
return {
|
|
71
|
+
frameMs: median(baselines),
|
|
72
|
+
passes: costs,
|
|
73
|
+
...together ? { together } : {},
|
|
74
|
+
frames,
|
|
75
|
+
rounds,
|
|
76
|
+
samples
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async function ablatePasses(world, options, frame) {
|
|
80
|
+
const graph = world.resource(Graph);
|
|
81
|
+
const gpu = world.resource(Gpu);
|
|
82
|
+
if (!graph.timer.enabled) {
|
|
83
|
+
throw new ShardError("render/gpu-timing-unavailable", "This device can't time GPU passes", {
|
|
84
|
+
hint: "Ablation reads gpu:frame, which needs the 'timestamp-query' feature."
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (options.passes.length === 0) {
|
|
88
|
+
throw new ShardError("render/unknown-node", "No passes to ablate", {
|
|
89
|
+
hint: `Name render graph nodes: ${graph.nodeNames().join(", ")}.`
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const groups = options.groups;
|
|
93
|
+
const nodesOf = (passes) => groups ? passes.flatMap((p) => groups[p] ?? [p]) : passes;
|
|
94
|
+
graph.ablate(nodesOf(options.passes));
|
|
95
|
+
graph.ablate([]);
|
|
96
|
+
const timer = graph.timer;
|
|
97
|
+
const driver = {
|
|
98
|
+
disable: (passes) => graph.ablate(nodesOf(passes)),
|
|
99
|
+
async frame() {
|
|
100
|
+
const seen = timer.frameSamples;
|
|
101
|
+
await frame();
|
|
102
|
+
for (let i = 0; i < 8 && timer.frameSamples === seen; i++) {
|
|
103
|
+
await gpu.device.queue.onSubmittedWorkDone();
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
105
|
+
}
|
|
106
|
+
return timer.frameSamples === seen ? void 0 : timer.frameMs;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const result = await runAblation(driver, options);
|
|
110
|
+
keep(world, result);
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
export {
|
|
114
|
+
PassCosts,
|
|
115
|
+
ablatePasses,
|
|
116
|
+
runAblation
|
|
117
|
+
};
|
package/dist/atmosphere-nodes.js
CHANGED
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
environmentBakers
|
|
8
8
|
} from "./environment.js";
|
|
9
9
|
import { addRenderFeatures } from "./features.js";
|
|
10
|
-
import { ForwardStateResource, sceneColor } from "./forward.js";
|
|
10
|
+
import { ForwardStateResource, sceneColor, viewBindGroup } from "./forward.js";
|
|
11
11
|
import { RenderPhase } from "./graph.js";
|
|
12
12
|
import { Gpu, Graph, Shaders, Views } from "./plugin.js";
|
|
13
13
|
import { hasEffect, PostEffect } from "./post.js";
|
|
14
|
+
import { roundsRg11, targetsFor } from "./post-common.js";
|
|
14
15
|
import { bindingDimension, depthReadEntry } from "./tier.js";
|
|
15
16
|
import { cameraOf } from "./view.js";
|
|
16
17
|
const LUT_LAYERS = 8;
|
|
@@ -371,6 +372,21 @@ function layouts(gpu, s) {
|
|
|
371
372
|
{ binding: 7, visibility: F, texture: { sampleType: "float", viewDimension: "3d" } }
|
|
372
373
|
]
|
|
373
374
|
}),
|
|
375
|
+
// The composite with fog folded in: fog's parameters after the haze's bindings.
|
|
376
|
+
compositeFog: gpu.layouts.bindGroupLayout({
|
|
377
|
+
label: "atmosphere/composite-fog",
|
|
378
|
+
entries: [
|
|
379
|
+
{ binding: 0, visibility: F, texture: { sampleType: "unfilterable-float" } },
|
|
380
|
+
depthReadEntry(gpu, 1, F),
|
|
381
|
+
uniform(2, F),
|
|
382
|
+
array(3, F),
|
|
383
|
+
array(4, F),
|
|
384
|
+
filtering(5, F),
|
|
385
|
+
{ binding: 6, visibility: F, texture: { sampleType: "float", viewDimension: "3d" } },
|
|
386
|
+
{ binding: 7, visibility: F, texture: { sampleType: "float", viewDimension: "3d" } },
|
|
387
|
+
uniform(8, F)
|
|
388
|
+
]
|
|
389
|
+
}),
|
|
374
390
|
viewOnly: gpu.layouts.bindGroupLayout({
|
|
375
391
|
label: "atmosphere/view",
|
|
376
392
|
entries: [uniform(0, GPUShaderStage.VERTEX | F)]
|
|
@@ -692,12 +708,31 @@ function skyNode() {
|
|
|
692
708
|
}
|
|
693
709
|
};
|
|
694
710
|
}
|
|
711
|
+
const fogScratch = new Float32Array(8);
|
|
712
|
+
const FOG_COMPOSITE_KEYS = [
|
|
713
|
+
"composite-fog",
|
|
714
|
+
"composite-fog/alpha",
|
|
715
|
+
"composite-fog/interior",
|
|
716
|
+
"composite-fog/alpha/interior",
|
|
717
|
+
"composite-fog/rg11",
|
|
718
|
+
"composite-fog/alpha/rg11",
|
|
719
|
+
"composite-fog/interior/rg11",
|
|
720
|
+
"composite-fog/alpha/interior/rg11"
|
|
721
|
+
];
|
|
695
722
|
function compositeNode() {
|
|
723
|
+
const fogBuffers = /* @__PURE__ */ new Map();
|
|
696
724
|
return {
|
|
697
725
|
kind: "render",
|
|
698
726
|
phase: RenderPhase.Post,
|
|
699
727
|
enabled: hasEffect(PostEffect.Atmosphere),
|
|
700
|
-
reads: [
|
|
728
|
+
reads: [
|
|
729
|
+
"atmosphere-in",
|
|
730
|
+
"depth",
|
|
731
|
+
"atmosphere-luts",
|
|
732
|
+
"atmosphere-view",
|
|
733
|
+
"environment",
|
|
734
|
+
"clusters"
|
|
735
|
+
],
|
|
701
736
|
writes: ["atmosphere-out"],
|
|
702
737
|
color: [{ resource: "atmosphere-out", clear: { r: 0, g: 0, b: 0, a: 1 } }],
|
|
703
738
|
run: (ctx) => {
|
|
@@ -705,30 +740,43 @@ function compositeNode() {
|
|
|
705
740
|
const ca = ctx.view.data.atmosphere;
|
|
706
741
|
const s = ctx.world.resource(AtmosphereGpuResource);
|
|
707
742
|
const c = cam ? s.cameras.get(cam.entity) : void 0;
|
|
708
|
-
const
|
|
743
|
+
const forward = ctx.world.resource(ForwardStateResource);
|
|
744
|
+
const pv = forward.views.get(ctx.view.name);
|
|
709
745
|
if (!cam || !ca || !c || !pv) return;
|
|
710
746
|
const gpu = ctx.gpu;
|
|
711
747
|
const l = layouts(gpu, s);
|
|
712
|
-
|
|
748
|
+
const fog = (cam.post.effects & PostEffect.Fog) !== 0;
|
|
749
|
+
const alpha = cam.alphaOutput;
|
|
750
|
+
const out = ctx.texture("atmosphere-out");
|
|
751
|
+
const rg11 = out.format === "rg11b10ufloat";
|
|
752
|
+
const name = fog ? FOG_COMPOSITE_KEYS[(alpha ? 1 : 0) + (forward.interiorMode !== 0 ? 2 : 0) + (rg11 ? 4 : 0)] : rg11 ? "composite/rg11" : "composite";
|
|
753
|
+
let pipeline = s.pipelines.get(name);
|
|
713
754
|
if (!pipeline) {
|
|
714
755
|
const shaders = ctx.world.resource(Shaders);
|
|
715
|
-
const fs = shaders.module(gpu, {
|
|
756
|
+
const fs = shaders.module(gpu, {
|
|
757
|
+
root: "shard::atmosphere::composite",
|
|
758
|
+
defines: { FOG: fog, TRANSPARENT: fog && alpha, RG11_ROUND: roundsRg11(gpu, out) }
|
|
759
|
+
});
|
|
716
760
|
const vs = shaders.module(gpu, { root: "shard::fullscreen" });
|
|
717
761
|
if (!fs || !vs) {
|
|
718
762
|
gpu.pipelines.skipped++;
|
|
719
763
|
return;
|
|
720
764
|
}
|
|
721
765
|
pipeline = gpu.pipelines.render({
|
|
722
|
-
label:
|
|
766
|
+
label: `atmosphere/${name}`,
|
|
723
767
|
layout: gpu.layouts.pipelineLayout({
|
|
724
|
-
label:
|
|
725
|
-
bindGroupLayouts: [l.viewOnly, l.composite]
|
|
768
|
+
label: `atmosphere/${name}`,
|
|
769
|
+
bindGroupLayouts: fog ? [forward.layouts.view, l.compositeFog] : [l.viewOnly, l.composite]
|
|
726
770
|
}),
|
|
727
771
|
vertex: { module: vs, entryPoint: "vs" },
|
|
728
|
-
fragment: {
|
|
772
|
+
fragment: {
|
|
773
|
+
module: fs,
|
|
774
|
+
entryPoint: fog ? "fs_fog" : "fs",
|
|
775
|
+
targets: targetsFor(out)
|
|
776
|
+
}
|
|
729
777
|
});
|
|
730
778
|
if (!pipeline) return;
|
|
731
|
-
s.pipelines.set(
|
|
779
|
+
s.pipelines.set(name, pipeline);
|
|
732
780
|
}
|
|
733
781
|
const input = ctx.texture("atmosphere-in");
|
|
734
782
|
const depth = ctx.texture("depth");
|
|
@@ -736,6 +784,48 @@ function compositeNode() {
|
|
|
736
784
|
const inside = ca.inside && c.computed === s.frame;
|
|
737
785
|
const scatter = inside && c.scatter ? c.scatter : e.d3;
|
|
738
786
|
const trans = inside && c.transmittance ? c.transmittance : e.d3;
|
|
787
|
+
const pass = ctx.renderPass;
|
|
788
|
+
pass.setPipeline(pipeline);
|
|
789
|
+
if (fog) {
|
|
790
|
+
let buffer = fogBuffers.get(cam.entity);
|
|
791
|
+
if (!buffer) {
|
|
792
|
+
buffer = gpu.device.createBuffer({
|
|
793
|
+
label: `${ctx.view.name}/composite-fog`,
|
|
794
|
+
size: 32,
|
|
795
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
796
|
+
});
|
|
797
|
+
fogBuffers.set(cam.entity, buffer);
|
|
798
|
+
}
|
|
799
|
+
const f = cam.post.fog;
|
|
800
|
+
fogScratch.set(f.color, 0);
|
|
801
|
+
fogScratch[4] = f.density;
|
|
802
|
+
fogScratch[5] = f.heightFalloff;
|
|
803
|
+
fogScratch[6] = f.start;
|
|
804
|
+
fogScratch[7] = f.sunScattering;
|
|
805
|
+
gpu.device.queue.writeBuffer(buffer, 0, fogScratch);
|
|
806
|
+
const fogBuffer = buffer;
|
|
807
|
+
const group2 = s.group(
|
|
808
|
+
gpu,
|
|
809
|
+
`${cam.entity}/composite-fog`,
|
|
810
|
+
`${gpu.generation}/${idOf(input)}/${idOf(depth)}/${c.uniform.version}/${idOf(scatter)}/${idOf(trans)}/${idOf(s.transmittance)}/${idOf(fogBuffer)}`,
|
|
811
|
+
l.compositeFog,
|
|
812
|
+
() => [
|
|
813
|
+
{ binding: 0, resource: input.createView() },
|
|
814
|
+
{ binding: 1, resource: depth.createView() },
|
|
815
|
+
{ binding: 2, resource: { buffer: c.uniform.buffer } },
|
|
816
|
+
{ binding: 3, resource: s.transmittanceArray },
|
|
817
|
+
{ binding: 4, resource: s.multiscatterArray },
|
|
818
|
+
{ binding: 5, resource: s.clamp },
|
|
819
|
+
{ binding: 6, resource: scatter.createView() },
|
|
820
|
+
{ binding: 7, resource: trans.createView() },
|
|
821
|
+
{ binding: 8, resource: { buffer: fogBuffer } }
|
|
822
|
+
]
|
|
823
|
+
);
|
|
824
|
+
pass.setBindGroup(0, viewBindGroup(gpu, ctx.world, pv, cam));
|
|
825
|
+
pass.setBindGroup(1, group2);
|
|
826
|
+
pass.draw(3);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
739
829
|
const group = s.group(
|
|
740
830
|
gpu,
|
|
741
831
|
`${cam.entity}/composite`,
|
|
@@ -759,8 +849,6 @@ function compositeNode() {
|
|
|
759
849
|
l.viewOnly,
|
|
760
850
|
() => [{ binding: 0, resource: { buffer: pv.uniform.buffer } }]
|
|
761
851
|
);
|
|
762
|
-
const pass = ctx.renderPass;
|
|
763
|
-
pass.setPipeline(pipeline);
|
|
764
852
|
pass.setBindGroup(0, viewGroup);
|
|
765
853
|
pass.setBindGroup(1, group);
|
|
766
854
|
pass.draw(3);
|
|
@@ -518,8 +518,9 @@ struct SkyOutput {
|
|
|
518
518
|
}`,
|
|
519
519
|
"shard::atmosphere::composite": `
|
|
520
520
|
import shard::view::view;
|
|
521
|
-
import shard::post::common::uv_of;
|
|
521
|
+
import shard::post::common::{ uv_of, post_out };
|
|
522
522
|
import shard::atmosphere::{ AtmosphereView, KCD, integrate, froxel_slice, relative_at };
|
|
523
|
+
@if(FOG) import shard::post::fog::common::{ FogParams, fog_apply };
|
|
523
524
|
|
|
524
525
|
@group(1) @binding(0) var input: texture_2d<f32>;
|
|
525
526
|
@group(1) @binding(1) var depth_texture: texture_depth_2d;
|
|
@@ -529,16 +530,15 @@ import shard::atmosphere::{ AtmosphereView, KCD, integrate, froxel_slice, relati
|
|
|
529
530
|
@group(1) @binding(5) var lut_sampler: sampler;
|
|
530
531
|
@group(1) @binding(6) var ap_scatter: texture_3d<f32>;
|
|
531
532
|
@group(1) @binding(7) var ap_transmittance: texture_3d<f32>;
|
|
533
|
+
/** A view with fog too: fog follows in the same pass (\`fs_fog\`). */
|
|
534
|
+
@if(FOG) @group(1) @binding(8) var<uniform> fog: FogParams;
|
|
532
535
|
|
|
533
536
|
/**
|
|
534
537
|
* Aerial perspective on geometry: color \xD7 transmittance + in-scattering for every atmosphere the
|
|
535
538
|
* view ray crosses before the surface, far to near. The primary reads its froxels within their
|
|
536
539
|
* range; beyond it (terrain seen from orbit), and for the others, the ray is marched.
|
|
537
540
|
*/
|
|
538
|
-
|
|
539
|
-
let px = vec2i(frag.xy);
|
|
540
|
-
let c = textureLoad(input, px, 0);
|
|
541
|
-
let depth = textureLoad(depth_texture, px, 0);
|
|
541
|
+
fn haze(frag: vec4f, c: vec4f, depth: f32) -> vec4f {
|
|
542
542
|
let count = u32(atmo.info.x);
|
|
543
543
|
if (depth <= 0.0 || count == 0u) { return c; }
|
|
544
544
|
let uv = uv_of(frag.xy);
|
|
@@ -568,6 +568,18 @@ import shard::atmosphere::{ AtmosphereView, KCD, integrate, froxel_slice, relati
|
|
|
568
568
|
}
|
|
569
569
|
}
|
|
570
570
|
return vec4f(min(color, vec3f(60000.0)), c.a);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
@fragment fn fs(@builtin(position) frag: vec4f) -> @location(0) vec4f {
|
|
574
|
+
let px = vec2i(frag.xy);
|
|
575
|
+
return post_out(haze(frag, textureLoad(input, px, 0), textureLoad(depth_texture, px, 0)));
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** Aerial perspective, then height fog: the view's fog pass folded into this one. */
|
|
579
|
+
@if(FOG) @fragment fn fs_fog(@builtin(position) frag: vec4f) -> @location(0) vec4f {
|
|
580
|
+
let px = vec2i(frag.xy);
|
|
581
|
+
let depth = textureLoad(depth_texture, px, 0);
|
|
582
|
+
return post_out(fog_apply(haze(frag, textureLoad(input, px, 0), depth), uv_of(frag.xy), depth, fog));
|
|
571
583
|
}`,
|
|
572
584
|
"shard::atmosphere::bake": `
|
|
573
585
|
import shard::env::common::cube_dir;
|
|
@@ -20,6 +20,6 @@ export declare function meterLayout(gpu: GpuContext): GPUBindGroupLayout;
|
|
|
20
20
|
* Renders the meter image and, when a readback is free, copies it out. Returns the readback that
|
|
21
21
|
* will hold it, or undefined when all are still mapping (this frame isn't metered).
|
|
22
22
|
*/
|
|
23
|
-
export declare function renderMeter(ctx: NodeContext, m: MeterView, pipeline: GPURenderPipeline, group: GPUBindGroup, range: Float32Array): number | undefined;
|
|
23
|
+
export declare function renderMeter(ctx: NodeContext, m: MeterView, pipeline: GPURenderPipeline, group: GPUBindGroup, range: Float32Array, glow: Float32Array): number | undefined;
|
|
24
24
|
/** The meter image's samples binned into the full tier's histogram: weight into its EV bin. */
|
|
25
25
|
export declare function binMeter(bytes: ArrayBuffer, minEv: number, binsPerEv: number, bins: Uint32Array): Uint32Array;
|
|
@@ -5,22 +5,32 @@ const BASELINE_EXPOSURE_SHADERS = {
|
|
|
5
5
|
"shard::post::baseline::meter": `
|
|
6
6
|
import shard::view::view;
|
|
7
7
|
import shard::color::luminance;
|
|
8
|
+
@if(BLOOM) import shard::post::glow::add_glow;
|
|
8
9
|
|
|
9
10
|
struct Meter {
|
|
10
11
|
/** EV of bin 0, bins per EV, metering mode, 0. */
|
|
11
12
|
range: vec4f,
|
|
12
13
|
/** The meter image's size. */
|
|
13
14
|
size: vec4f,
|
|
15
|
+
/** Bloom, which the tonemap adds to the image: intensity, level count, mix (1) or add (0). */
|
|
16
|
+
glow: vec4f,
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
@group(0) @binding(1) var input: texture_2d<f32>;
|
|
17
20
|
@group(0) @binding(2) var<uniform> meter: Meter;
|
|
21
|
+
@if(BLOOM) @group(0) @binding(3) var bloom: texture_2d<f32>;
|
|
22
|
+
@if(BLOOM) @group(0) @binding(4) var bloom_sampler: sampler;
|
|
18
23
|
|
|
19
24
|
/** One sample: the EV100 of the input pixel under it, and its metering weight (of 16). */
|
|
20
25
|
@fragment fn main(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
21
26
|
let size = textureDimensions(input);
|
|
22
27
|
let px = min(vec2u((floor(p.xy) + 0.5) / meter.size.xy * vec2f(size)), size - 1u);
|
|
23
|
-
|
|
28
|
+
var c = textureLoad(input, vec2i(px), 0).rgb;
|
|
29
|
+
// What the tonemap shows: the image with bloom's glow over it.
|
|
30
|
+
@if(BLOOM) {
|
|
31
|
+
let uv = (vec2f(px) + 0.5) / vec2f(size);
|
|
32
|
+
c = add_glow(c, textureSampleLevel(bloom, bloom_sampler, uv, 0.0).rgb, meter.glow);
|
|
33
|
+
}
|
|
24
34
|
let l = luminance(c) / max(view.exposure, 1e-20);
|
|
25
35
|
let ev = log2(max(l, 1e-10) * 8.0);
|
|
26
36
|
let q = vec2f(px) / vec2f(size) * 2.0 - 1.0;
|
|
@@ -44,7 +54,7 @@ function meterView(gpu, name, readbacks) {
|
|
|
44
54
|
view: target.createView(),
|
|
45
55
|
params: gpu.device.createBuffer({
|
|
46
56
|
label: `${name}/exposure-meter`,
|
|
47
|
-
size:
|
|
57
|
+
size: 48,
|
|
48
58
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
49
59
|
}),
|
|
50
60
|
readbacks: Array.from(
|
|
@@ -68,15 +78,18 @@ function meterLayout(gpu) {
|
|
|
68
78
|
entries: [
|
|
69
79
|
{ binding: 0, visibility: F, buffer: { type: "uniform" } },
|
|
70
80
|
{ binding: 1, visibility: F, texture: { sampleType: "unfilterable-float" } },
|
|
71
|
-
{ binding: 2, visibility: F, buffer: { type: "uniform" } }
|
|
81
|
+
{ binding: 2, visibility: F, buffer: { type: "uniform" } },
|
|
82
|
+
{ binding: 3, visibility: F, texture: { sampleType: "float" } },
|
|
83
|
+
{ binding: 4, visibility: F, sampler: { type: "filtering" } }
|
|
72
84
|
]
|
|
73
85
|
});
|
|
74
86
|
}
|
|
75
|
-
const params = new Float32Array(
|
|
76
|
-
function renderMeter(ctx, m, pipeline, group, range) {
|
|
87
|
+
const params = new Float32Array(12);
|
|
88
|
+
function renderMeter(ctx, m, pipeline, group, range, glow) {
|
|
77
89
|
params.set(range, 0);
|
|
78
90
|
params[4] = METER_WIDTH;
|
|
79
91
|
params[5] = METER_HEIGHT;
|
|
92
|
+
params.set(glow, 8);
|
|
80
93
|
ctx.gpu.device.queue.writeBuffer(m.params, 0, params);
|
|
81
94
|
const pass = ctx.encoder.beginRenderPass({
|
|
82
95
|
label: `${ctx.view.name}/exposure`,
|
package/dist/crowd.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type World } from '@aethervtt/shard-core';
|
|
2
|
+
export interface CrowdOptions {
|
|
3
|
+
/** Instances (default 200 000). */
|
|
4
|
+
count?: number;
|
|
5
|
+
/** Every instance at full detail, no LOD (the playground's `?nolod`). */
|
|
6
|
+
flat?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The crowd's turntable: 160 m out and 40 m up, looking at the field's centre, `angle` radians
|
|
10
|
+
* round (the playground turns it 2.86° a second).
|
|
11
|
+
*/
|
|
12
|
+
export declare function crowdCamera(angle: number): {
|
|
13
|
+
eye: [number, number, number];
|
|
14
|
+
target: [number, number, number];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* 200k static instances of 20 mesh types over a 600 m field, under a sun with 4 shadow cascades.
|
|
18
|
+
* Most have 3 LOD levels; every fifth is a small prop hidden past 120 m. The camera is the
|
|
19
|
+
* caller's (`crowdCamera` places the turntable).
|
|
20
|
+
*/
|
|
21
|
+
export declare function spawnCrowd(world: World, options?: CrowdOptions): void;
|
package/dist/crowd.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { quat, Rng } from "@aethervtt/shard-core";
|
|
2
|
+
import {
|
|
3
|
+
box,
|
|
4
|
+
capsule,
|
|
5
|
+
cone,
|
|
6
|
+
cylinder,
|
|
7
|
+
plane,
|
|
8
|
+
sphere,
|
|
9
|
+
torus
|
|
10
|
+
} from "@aethervtt/shard-mesh";
|
|
11
|
+
import { Transform } from "@aethervtt/shard-transform";
|
|
12
|
+
import { MaterialAsset, Materials, Meshes } from "./assets.js";
|
|
13
|
+
import { Lod, Mesh3d, MeshMaterial, VisibilityRange } from "./instances.js";
|
|
14
|
+
import { DirectionalLight } from "./lights.js";
|
|
15
|
+
function kinds() {
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const segments of [8, 12, 16, 24]) {
|
|
18
|
+
out.push([sphere({ radius: 0.5, segments }), sphere({ radius: 0.5, segments: 6 })]);
|
|
19
|
+
}
|
|
20
|
+
for (const y of [0.5, 0.8, 1.2]) {
|
|
21
|
+
const m = box({ x: 0.8, y, z: 0.8 });
|
|
22
|
+
out.push([m, m]);
|
|
23
|
+
}
|
|
24
|
+
for (const segments of [6, 12, 24]) {
|
|
25
|
+
out.push([
|
|
26
|
+
cylinder({ radius: 0.4, height: 1, segments }),
|
|
27
|
+
cylinder({ radius: 0.4, height: 1, segments: 5 })
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
for (const segments of [6, 16]) {
|
|
31
|
+
out.push([
|
|
32
|
+
cone({ radius: 0.5, height: 1.2, segments }),
|
|
33
|
+
cone({ radius: 0.5, height: 1.2, segments: 4 })
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
for (const segments of [8, 16]) {
|
|
37
|
+
out.push([
|
|
38
|
+
capsule({ radius: 0.3, height: 1.2, segments }),
|
|
39
|
+
capsule({ radius: 0.3, height: 1.2, segments: 4, rings: 2 })
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
for (const tube of [0.2, 0.1]) {
|
|
43
|
+
out.push([
|
|
44
|
+
torus({ radius: 0.4, tube }),
|
|
45
|
+
torus({ radius: 0.4, tube, radialSegments: 4, tubularSegments: 8 })
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
for (const y of [0.3, 0.6, 1]) {
|
|
49
|
+
const m = box({ x: 0.5, y, z: 1.2 });
|
|
50
|
+
out.push([m, m]);
|
|
51
|
+
}
|
|
52
|
+
const beam = box({ x: 1.5, y: 0.3, z: 0.3 });
|
|
53
|
+
out.push([beam, beam]);
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
function crowdCamera(angle) {
|
|
57
|
+
return { eye: [Math.sin(angle) * 160, 40, Math.cos(angle) * 160], target: [0, 0, 0] };
|
|
58
|
+
}
|
|
59
|
+
function spawnCrowd(world, options = {}) {
|
|
60
|
+
const count = options.count ?? 2e5;
|
|
61
|
+
const flat = options.flat ?? false;
|
|
62
|
+
world.spawn(
|
|
63
|
+
[DirectionalLight, { illuminance: 6e4, shadows: true }],
|
|
64
|
+
[Transform, { rotation: quat.fromEuler([0, 0, 0, 1], -0.8, 0.7, 0) }]
|
|
65
|
+
);
|
|
66
|
+
const meshes = world.resource(Meshes);
|
|
67
|
+
const materials = world.resource(Materials);
|
|
68
|
+
const rng = new Rng(11);
|
|
69
|
+
const stand = meshes.add(box({ x: 0.8, y: 0.8, z: 0.8 }));
|
|
70
|
+
const types = kinds().map(([hi, lo]) => {
|
|
71
|
+
const mesh = meshes.add(hi);
|
|
72
|
+
return {
|
|
73
|
+
mesh,
|
|
74
|
+
levels: [
|
|
75
|
+
{ mesh, screenSize: 0.04 },
|
|
76
|
+
{ mesh: meshes.add(lo), screenSize: 0.01 },
|
|
77
|
+
{ mesh: stand, screenSize: 2e-3 }
|
|
78
|
+
]
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
const looks = Array.from(
|
|
82
|
+
{ length: 8 },
|
|
83
|
+
() => materials.add(
|
|
84
|
+
new MaterialAsset({
|
|
85
|
+
baseColor: [rng.range(0.2, 0.9), rng.range(0.2, 0.9), rng.range(0.2, 0.9), 1],
|
|
86
|
+
roughness: rng.range(0.2, 0.9)
|
|
87
|
+
})
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
world.spawn(
|
|
91
|
+
[Mesh3d, { mesh: meshes.add(plane({ size: 700 })) }],
|
|
92
|
+
[MeshMaterial, { material: looks[0] }],
|
|
93
|
+
Transform
|
|
94
|
+
);
|
|
95
|
+
for (let i = 0; i < count; i++) {
|
|
96
|
+
const s = rng.range(0.6, 1.8);
|
|
97
|
+
const kind = types[i % types.length];
|
|
98
|
+
const common = [
|
|
99
|
+
[MeshMaterial, { material: looks[i % looks.length] }],
|
|
100
|
+
[
|
|
101
|
+
Transform,
|
|
102
|
+
{
|
|
103
|
+
translation: [rng.range(-300, 300), s * 0.5, rng.range(-300, 300)],
|
|
104
|
+
rotation: quat.fromEuler([0, 0, 0, 1], 0, rng.float() * 6.28, 0),
|
|
105
|
+
scale: [s, s, s]
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
];
|
|
109
|
+
if (flat) {
|
|
110
|
+
world.spawn([Mesh3d, { mesh: kind.mesh }], ...common);
|
|
111
|
+
} else if (i % 5 === 0) {
|
|
112
|
+
world.spawn([Mesh3d, { mesh: kind.mesh }], [VisibilityRange, { end: 120 }], ...common);
|
|
113
|
+
} else {
|
|
114
|
+
world.spawn([Mesh3d, { mesh: kind.mesh }], [Lod, { levels: kind.levels }], ...common);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export {
|
|
119
|
+
crowdCamera,
|
|
120
|
+
spawnCrowd
|
|
121
|
+
};
|