@aethervtt/shard-render 0.0.1 → 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/assets.d.ts +3 -0
- package/dist/assets.js +43 -2
- package/dist/atmosphere-nodes.d.ts +9 -0
- package/dist/atmosphere-nodes.js +157 -30
- package/dist/atmosphere-plugin.js +8 -2
- package/dist/atmosphere-shaders.js +17 -5
- package/dist/baseline/atmosphere.d.ts +16 -0
- package/dist/baseline/atmosphere.js +301 -0
- package/dist/baseline/data-texture.d.ts +36 -0
- package/dist/baseline/data-texture.js +181 -0
- package/dist/baseline/environment.d.ts +14 -0
- package/dist/baseline/environment.js +368 -0
- package/dist/baseline/exposure.d.ts +25 -0
- package/dist/baseline/exposure.js +133 -0
- package/dist/baseline/lights.d.ts +33 -0
- package/dist/baseline/lights.js +133 -0
- package/dist/baseline/textures.d.ts +97 -0
- package/dist/baseline/textures.js +203 -0
- package/dist/camera.d.ts +2 -0
- package/dist/camera.js +4 -0
- package/dist/crowd.d.ts +21 -0
- package/dist/crowd.js +121 -0
- package/dist/culling-shaders.js +21 -2
- package/dist/culling.d.ts +11 -2
- package/dist/culling.js +44 -18
- package/dist/cutaway-plugin.d.ts +48 -0
- package/dist/cutaway-plugin.js +364 -0
- package/dist/cutaway.d.ts +27 -0
- package/dist/cutaway.js +49 -0
- package/dist/data-store.d.ts +69 -0
- package/dist/data-store.js +105 -0
- package/dist/debug-shaders.js +17 -3
- package/dist/debug-views.js +4 -2
- 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-shaders.js +2 -0
- package/dist/deferred.js +28 -4
- package/dist/deform.d.ts +5 -4
- package/dist/deform.js +4 -9
- package/dist/describe-plugin.d.ts +4 -2
- package/dist/describe-plugin.js +75 -3
- package/dist/display-nodes.js +37 -13
- package/dist/dynamic-resolution.d.ts +2 -0
- package/dist/dynamic-resolution.js +6 -1
- package/dist/environment-plugin.js +17 -2
- package/dist/environment-state.d.ts +7 -3
- package/dist/environment-state.js +20 -9
- package/dist/environment.js +7 -1
- package/dist/features.d.ts +49 -0
- package/dist/features.js +106 -0
- 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.d.ts +38 -2
- package/dist/forward.js +608 -186
- package/dist/fxaa.js +12 -2
- package/dist/gizmos-plugin.js +7 -0
- package/dist/gizmos.d.ts +3 -3
- package/dist/gizmos.js +11 -13
- package/dist/gpu-assets.d.ts +58 -4
- package/dist/gpu-assets.js +191 -8
- package/dist/graph.d.ts +45 -0
- package/dist/graph.js +169 -19
- package/dist/health.d.ts +58 -0
- package/dist/health.js +112 -0
- package/dist/index.d.ts +40 -11
- package/dist/index.js +343 -9
- package/dist/instances.d.ts +119 -8
- package/dist/instances.js +387 -78
- package/dist/interior-plugin.d.ts +121 -0
- package/dist/interior-plugin.js +465 -0
- package/dist/interior.d.ts +24 -0
- package/dist/interior.js +13 -0
- package/dist/layers.d.ts +35 -0
- package/dist/layers.js +49 -0
- package/dist/lens-plugin.d.ts +43 -0
- package/dist/lens-plugin.js +304 -0
- package/dist/lens.d.ts +41 -0
- package/dist/lens.js +99 -0
- package/dist/lights.d.ts +50 -5
- package/dist/lights.js +101 -22
- package/dist/material-pipelines.d.ts +37 -4
- package/dist/material-pipelines.js +102 -5
- package/dist/materials.d.ts +23 -0
- package/dist/materials.js +44 -1
- package/dist/outline-plugin.d.ts +48 -0
- package/dist/outline-plugin.js +529 -0
- package/dist/outline.d.ts +15 -0
- package/dist/outline.js +42 -0
- package/dist/overlay-registry.d.ts +3 -1
- package/dist/overlay-registry.js +3 -1
- package/dist/overlays.js +6 -2
- package/dist/perf-overlay.d.ts +9 -0
- package/dist/perf-overlay.js +125 -0
- package/dist/picking.d.ts +17 -0
- package/dist/picking.js +40 -11
- package/dist/pixel-perfect-plugin.js +10 -2
- package/dist/plugin.d.ts +69 -7
- package/dist/plugin.js +223 -25
- package/dist/post-common.d.ts +22 -2
- package/dist/post-common.js +82 -2
- package/dist/post-nodes.d.ts +7 -1
- package/dist/post-nodes.js +235 -111
- package/dist/post-plugin.js +5 -3
- package/dist/post-scene.d.ts +130 -0
- package/dist/post-scene.js +164 -0
- package/dist/post-shaders.js +85 -62
- package/dist/post.js +11 -1
- package/dist/projection.d.ts +24 -0
- package/dist/projection.js +87 -0
- package/dist/readback.d.ts +1 -0
- package/dist/readback.js +1 -0
- package/dist/render-scale.js +1 -0
- 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/screen-effects.d.ts +97 -0
- package/dist/screen-effects.js +192 -0
- package/dist/shaders.d.ts +13 -0
- package/dist/shaders.js +189 -34
- package/dist/shadow-catcher.d.ts +9 -0
- package/dist/shadow-catcher.js +133 -0
- package/dist/shadows.d.ts +27 -2
- package/dist/shadows.js +100 -20
- package/dist/standard-fields.d.ts +1 -0
- package/dist/standard-fields.js +5 -0
- package/dist/standard.js +18 -0
- package/dist/stats.d.ts +45 -1
- package/dist/stats.js +91 -3
- package/dist/surface-model.d.ts +57 -0
- package/dist/surface-model.js +289 -0
- package/dist/surface-shaders.d.ts +3 -0
- package/dist/surface-shaders.js +160 -0
- package/dist/surface.d.ts +40 -0
- package/dist/surface.js +86 -0
- package/dist/target.d.ts +1 -13
- package/dist/target.js +1 -26
- package/dist/testing.d.ts +11 -0
- package/dist/testing.js +39 -1
- package/dist/tier.d.ts +13 -0
- package/dist/tier.js +14 -0
- package/dist/timer.d.ts +18 -1
- package/dist/timer.js +73 -20
- package/dist/variants.d.ts +37 -0
- package/dist/variants.js +104 -0
- 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/dist/view-visibility-plugin.d.ts +61 -0
- package/dist/view-visibility-plugin.js +267 -0
- package/dist/view-visibility.d.ts +28 -0
- package/dist/view-visibility.js +46 -0
- package/dist/view.d.ts +29 -1
- package/dist/view.js +57 -34
- package/package.json +23 -11
package/dist/display-nodes.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { addRenderFeatures } from "./features.js";
|
|
1
2
|
import { Textures } from "@aethervtt/shard-texture";
|
|
2
3
|
import { GpuAssetsResource } from "./gpu-assets.js";
|
|
3
4
|
import { RenderPhase } from "./graph.js";
|
|
@@ -32,7 +33,8 @@ function tonemapNode() {
|
|
|
32
33
|
kind: "render",
|
|
33
34
|
phase: RenderPhase.Tonemap,
|
|
34
35
|
enabled: (view) => cameraOf(view) !== void 0,
|
|
35
|
-
|
|
36
|
+
// Bloom's chain, when the view blooms: the glow is composited here (see the 'bloom' node).
|
|
37
|
+
reads: ["post-hdr", "bloom"],
|
|
36
38
|
writes: ["ldr"],
|
|
37
39
|
color: [{ resource: "ldr", clear: { r: 0, g: 0, b: 0, a: 1 } }],
|
|
38
40
|
run: (ctx) => {
|
|
@@ -42,7 +44,7 @@ function tonemapNode() {
|
|
|
42
44
|
layoutGen = gpu.generation;
|
|
43
45
|
layout = gpu.layouts.bindGroupLayout({
|
|
44
46
|
label: "tonemap",
|
|
45
|
-
entries: [tex(0, "unfilterable-float"), uniform(1), tex(2), sampler(3)]
|
|
47
|
+
entries: [tex(0, "unfilterable-float"), uniform(1), tex(2), sampler(3), tex(4)]
|
|
46
48
|
});
|
|
47
49
|
white = gpu.device.createTexture({
|
|
48
50
|
label: "tonemap/no-lut",
|
|
@@ -53,15 +55,19 @@ function tonemapNode() {
|
|
|
53
55
|
}
|
|
54
56
|
const format = ctx.texture("ldr").format;
|
|
55
57
|
const srgb = format.endsWith("-srgb");
|
|
58
|
+
const alpha = cam.alphaOutput;
|
|
59
|
+
const bloom = (cam.post.effects & PostEffect.Bloom) !== 0;
|
|
56
60
|
const pipeline = cache.render(
|
|
57
61
|
ctx,
|
|
58
|
-
`tonemap/${format}`,
|
|
62
|
+
`tonemap/${format}${alpha ? "/alpha" : ""}${bloom ? "/bloom" : ""}`,
|
|
59
63
|
"shard::post::tonemap",
|
|
60
64
|
"fs",
|
|
61
65
|
[layout],
|
|
62
66
|
[{ format }],
|
|
63
67
|
{
|
|
64
|
-
SRGB_TARGET: srgb
|
|
68
|
+
SRGB_TARGET: srgb,
|
|
69
|
+
TRANSPARENT: alpha,
|
|
70
|
+
BLOOM: bloom
|
|
65
71
|
}
|
|
66
72
|
);
|
|
67
73
|
if (!pipeline) return;
|
|
@@ -79,7 +85,7 @@ function tonemapNode() {
|
|
|
79
85
|
flags |= 4;
|
|
80
86
|
}
|
|
81
87
|
}
|
|
82
|
-
scratch.fill(0, 0,
|
|
88
|
+
scratch.fill(0, 0, 36);
|
|
83
89
|
u[0] = cam.curve;
|
|
84
90
|
u[1] = cam.dither ? 1 : 0;
|
|
85
91
|
u[2] = flags;
|
|
@@ -97,20 +103,30 @@ function tonemapNode() {
|
|
|
97
103
|
scratch[26] = cam.width / Math.max(1, cam.height);
|
|
98
104
|
scratch[28] = 1 / cam.width;
|
|
99
105
|
scratch[29] = 1 / cam.height;
|
|
100
|
-
const
|
|
101
|
-
|
|
106
|
+
const chain = bloom ? ctx.texture("bloom") : void 0;
|
|
107
|
+
if (chain) {
|
|
108
|
+
const b = post.bloom;
|
|
109
|
+
scratch[32] = b.intensity;
|
|
110
|
+
scratch[33] = chain.mipLevelCount;
|
|
111
|
+
scratch[34] = b.threshold > 0 ? 0 : 1;
|
|
112
|
+
}
|
|
113
|
+
const params = cache.buffer(gpu, `${ctx.view.name}/tonemap`, 144);
|
|
114
|
+
params.write(scratch, 0, 0, 36);
|
|
102
115
|
const hdr = ctx.texture("post-hdr");
|
|
103
|
-
const lutView = lut ?? white.createView();
|
|
104
116
|
const group = cache.group(
|
|
105
117
|
gpu,
|
|
106
118
|
`${ctx.view.name}/tonemap`,
|
|
107
|
-
`${idOf(hdr)}/${params.version}/${lut ? idOf(lut) : 0}`,
|
|
119
|
+
`${idOf(hdr)}/${params.version}/${lut ? idOf(lut) : 0}/${chain ? idOf(chain) : 0}`,
|
|
108
120
|
layout,
|
|
109
121
|
() => [
|
|
110
122
|
{ binding: 0, resource: hdr.createView() },
|
|
111
123
|
{ binding: 1, resource: { buffer: params.buffer } },
|
|
112
|
-
{ binding: 2, resource:
|
|
113
|
-
{ binding: 3, resource: cache.sampler(gpu) }
|
|
124
|
+
{ binding: 2, resource: lut ?? white.createView() },
|
|
125
|
+
{ binding: 3, resource: cache.sampler(gpu) },
|
|
126
|
+
{
|
|
127
|
+
binding: 4,
|
|
128
|
+
resource: chain ? chain.createView({ mipLevelCount: 1 }) : white.createView()
|
|
129
|
+
}
|
|
114
130
|
]
|
|
115
131
|
);
|
|
116
132
|
const pass = ctx.renderPass;
|
|
@@ -144,13 +160,15 @@ function upscaleNode() {
|
|
|
144
160
|
const input = ctx.texture("display");
|
|
145
161
|
const target = ctx.texture("view-target");
|
|
146
162
|
const format = target.format;
|
|
163
|
+
const alpha = cameraOf(ctx.view).alphaOutput;
|
|
147
164
|
const pipeline = cache.render(
|
|
148
165
|
ctx,
|
|
149
|
-
`upscale/${format}`,
|
|
166
|
+
`upscale/${format}${alpha ? "/alpha" : ""}`,
|
|
150
167
|
"shard::post::upscale",
|
|
151
168
|
"fs",
|
|
152
169
|
[layout],
|
|
153
|
-
[{ format }]
|
|
170
|
+
[{ format }],
|
|
171
|
+
{ TRANSPARENT: alpha }
|
|
154
172
|
);
|
|
155
173
|
if (!pipeline) return;
|
|
156
174
|
const settings = ctx.world.tryResource(RenderScale);
|
|
@@ -180,6 +198,12 @@ function upscaleNode() {
|
|
|
180
198
|
function addDisplayNodes(world) {
|
|
181
199
|
const graph = world.resource(Graph);
|
|
182
200
|
graph.declare({ name: "display", format: "view" });
|
|
201
|
+
addRenderFeatures(world, {
|
|
202
|
+
name: "render/display",
|
|
203
|
+
description: "Tonemapping and the upscale to the display.",
|
|
204
|
+
nodes: ["tonemap", "post/upscale"],
|
|
205
|
+
baseline: { strategy: "The same fragment passes" }
|
|
206
|
+
});
|
|
183
207
|
graph.addNode("tonemap", tonemapNode());
|
|
184
208
|
graph.addNode("post/upscale", upscaleNode());
|
|
185
209
|
}
|
|
@@ -27,6 +27,8 @@ export declare class RenderScaleController {
|
|
|
27
27
|
private probeAge;
|
|
28
28
|
private probeWait;
|
|
29
29
|
private hold;
|
|
30
|
+
/** Verifying an upward probe: it needs frames to measure, so on-demand apps keep rendering. */
|
|
31
|
+
get probing(): boolean;
|
|
30
32
|
/**
|
|
31
33
|
* One frame. `skip`: pipelines are compiling, so the frame says nothing about steady cost.
|
|
32
34
|
* `displayMs`: one refresh of the display, the budget when `targetMs` is 0.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineSystem, Last } from "@aethervtt/shard-core";
|
|
2
|
-
import { DisplayRate, definePlugin, Time } from "@aethervtt/shard-runtime";
|
|
2
|
+
import { DisplayRate, definePlugin, FrameDemand, Time } from "@aethervtt/shard-runtime";
|
|
3
3
|
import { Gpu, Graph, RenderSet } from "./plugin.js";
|
|
4
4
|
import {
|
|
5
5
|
HIGHEST,
|
|
@@ -30,6 +30,10 @@ class RenderScaleController {
|
|
|
30
30
|
probeAge = 0;
|
|
31
31
|
probeWait = 3;
|
|
32
32
|
hold = 0;
|
|
33
|
+
/** Verifying an upward probe: it needs frames to measure, so on-demand apps keep rendering. */
|
|
34
|
+
get probing() {
|
|
35
|
+
return this.probeFrom > 0;
|
|
36
|
+
}
|
|
33
37
|
/**
|
|
34
38
|
* One frame. `skip`: pipelines are compiling, so the frame says nothing about steady cost.
|
|
35
39
|
* `displayMs`: one refresh of the display, the budget when `targetMs` is 0.
|
|
@@ -175,6 +179,7 @@ const updateRenderScale = defineSystem({
|
|
|
175
179
|
gpu.pipelines.pending > 0,
|
|
176
180
|
display
|
|
177
181
|
);
|
|
182
|
+
world.tryResource(FrameDemand)?.set("render/render-scale", controller.probing);
|
|
178
183
|
}
|
|
179
184
|
});
|
|
180
185
|
const dynamicResolutionPlugin = definePlugin({
|
|
@@ -4,15 +4,17 @@ import {
|
|
|
4
4
|
DefaultEnvironment,
|
|
5
5
|
describeEnvironment,
|
|
6
6
|
EnvironmentMap,
|
|
7
|
+
Environments,
|
|
7
8
|
ProceduralSky,
|
|
8
9
|
prepareEnvironments,
|
|
9
10
|
runEnvironmentWork,
|
|
10
11
|
Skybox
|
|
11
12
|
} from "./environment.js";
|
|
12
13
|
import { ENVIRONMENT_SHADERS } from "./environment-shaders.js";
|
|
14
|
+
import { addRenderFeatures } from "./features.js";
|
|
13
15
|
import { ForwardStateResource, isCamera, skyNode } from "./forward.js";
|
|
14
16
|
import { RenderPhase } from "./graph.js";
|
|
15
|
-
import { Graph, RenderDescribers, RenderSet, Shaders } from "./plugin.js";
|
|
17
|
+
import { Gpu, Graph, RenderDescribers, RenderSet, Shaders } from "./plugin.js";
|
|
16
18
|
import { registerShaders } from "./shaders.js";
|
|
17
19
|
const environmentPlugin = definePlugin({
|
|
18
20
|
name: "render/environment",
|
|
@@ -22,11 +24,24 @@ const environmentPlugin = definePlugin({
|
|
|
22
24
|
app.world.initResource(DefaultEnvironment);
|
|
23
25
|
app.addSystems(Last, prepareEnvironments.inSet(RenderSet.Prepare));
|
|
24
26
|
},
|
|
25
|
-
ready(app) {
|
|
27
|
+
async ready(app) {
|
|
26
28
|
const world = app.world;
|
|
29
|
+
const baseline = world.resource(Gpu).tier === "baseline" ? await import("./baseline/environment.js") : void 0;
|
|
27
30
|
registerShaders(world.resource(Shaders), ENVIRONMENT_SHADERS);
|
|
31
|
+
if (baseline) {
|
|
32
|
+
registerShaders(world.resource(Shaders), baseline.BASELINE_ENVIRONMENT_SHADERS);
|
|
33
|
+
world.resource(Environments).baseline = baseline;
|
|
34
|
+
}
|
|
28
35
|
world.initResource(RenderDescribers).set("environment", (w) => describeEnvironment(w));
|
|
29
36
|
const graph = world.resource(Graph);
|
|
37
|
+
addRenderFeatures(world, {
|
|
38
|
+
name: "render/environment",
|
|
39
|
+
description: "Image-based lighting: cube conversion, specular prefilter, SH irradiance, BRDF LUT; and the sky.",
|
|
40
|
+
nodes: ["environment", "sky"],
|
|
41
|
+
baseline: {
|
|
42
|
+
strategy: "Prefilter, SH and BRDF LUT as fragment passes into the cube faces and mips"
|
|
43
|
+
}
|
|
44
|
+
});
|
|
30
45
|
graph.addNode("environment", {
|
|
31
46
|
kind: "raw",
|
|
32
47
|
phase: RenderPhase.Setup,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { GpuContext } from '@aethervtt/shard-gpu';
|
|
2
2
|
import type { Texture } from '@aethervtt/shard-texture';
|
|
3
3
|
import type { CameraAtmosphere } from './atmosphere-state.js';
|
|
4
|
+
import { DataStore } from './data-store.js';
|
|
4
5
|
import type { CameraData } from './view.js';
|
|
5
6
|
/** Frames between rebakes of a moving camera's atmosphere environment. */
|
|
6
7
|
export declare const REBAKE_FRAMES = 8;
|
|
@@ -19,7 +20,8 @@ export declare class Environment {
|
|
|
19
20
|
specular: GPUTexture;
|
|
20
21
|
specularView: GPUTextureView;
|
|
21
22
|
sourceView: GPUTextureView;
|
|
22
|
-
|
|
23
|
+
/** SH9 of the diffuse irradiance: `@data(uniform)` in the forward shader. */
|
|
24
|
+
readonly sh: DataStore;
|
|
23
25
|
/** 'pending' until the GPU has prefiltered the current key. */
|
|
24
26
|
state: 'pending' | 'ready';
|
|
25
27
|
/** How many times it's been (re)baked or prefiltered. */
|
|
@@ -59,12 +61,14 @@ export declare class EnvironmentStore {
|
|
|
59
61
|
lutReady: boolean;
|
|
60
62
|
emptyCube: GPUTexture | undefined;
|
|
61
63
|
emptyView: GPUTextureView | undefined;
|
|
62
|
-
emptySh:
|
|
64
|
+
emptySh: DataStore | undefined;
|
|
63
65
|
sampler: GPUSampler | undefined;
|
|
64
66
|
generation: number;
|
|
65
67
|
frame: number;
|
|
66
68
|
/** The frame the prefilter work last ran (it runs once per frame, for all views). */
|
|
67
69
|
ranFrame: number;
|
|
70
|
+
/** The baseline tier's prefilter (0064): fragment passes, loaded only on a baseline device. */
|
|
71
|
+
baseline: typeof import('./baseline/environment.js') | undefined;
|
|
68
72
|
ensure(gpu: GpuContext): void;
|
|
69
73
|
}
|
|
70
74
|
export declare const Environments: import("@aethervtt/shard-core").ResourceDef<EnvironmentStore>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineResource } from "@aethervtt/shard-core";
|
|
2
|
-
import {
|
|
2
|
+
import { DataStore } from "./data-store.js";
|
|
3
|
+
import { bindingDimension } from "./tier.js";
|
|
3
4
|
const REBAKE_FRAMES = 8;
|
|
4
5
|
const SOURCE_SIZE = 512;
|
|
5
6
|
const SKY_SIZE = 256;
|
|
@@ -15,6 +16,7 @@ class Environment {
|
|
|
15
16
|
specular;
|
|
16
17
|
specularView;
|
|
17
18
|
sourceView;
|
|
19
|
+
/** SH9 of the diffuse irradiance: `@data(uniform)` in the forward shader. */
|
|
18
20
|
sh;
|
|
19
21
|
/** 'pending' until the GPU has prefiltered the current key. */
|
|
20
22
|
state = "pending";
|
|
@@ -37,20 +39,25 @@ class Environment {
|
|
|
37
39
|
size: [this.size, this.size, 6],
|
|
38
40
|
format: "rgba16float",
|
|
39
41
|
mipLevelCount: mips,
|
|
40
|
-
|
|
42
|
+
// Written by compute on the full tier; rendered face by face on baseline (0064).
|
|
43
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | (gpu.tier === "full" ? GPUTextureUsage.STORAGE_BINDING : GPUTextureUsage.RENDER_ATTACHMENT) | GPUTextureUsage.COPY_SRC,
|
|
44
|
+
...bindingDimension(gpu, "cube")
|
|
41
45
|
});
|
|
42
46
|
this.specular = gpu.device.createTexture({
|
|
43
47
|
label: `environment/${kind}/specular`,
|
|
44
48
|
size: [SPECULAR_SIZE, SPECULAR_SIZE, 6],
|
|
45
49
|
format: "rgba16float",
|
|
46
50
|
mipLevelCount: SPECULAR_MIPS,
|
|
47
|
-
|
|
51
|
+
// Written by compute on the full tier; rendered face by face on baseline (0064).
|
|
52
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | (gpu.tier === "full" ? GPUTextureUsage.STORAGE_BINDING : GPUTextureUsage.RENDER_ATTACHMENT) | GPUTextureUsage.COPY_SRC,
|
|
53
|
+
...bindingDimension(gpu, "cube")
|
|
48
54
|
});
|
|
49
55
|
this.sourceView = this.source.createView({ dimension: "cube" });
|
|
50
56
|
this.specularView = this.specular.createView({ dimension: "cube" });
|
|
51
|
-
this.sh = new
|
|
57
|
+
this.sh = new DataStore(gpu, {
|
|
52
58
|
label: `environment/${kind}/sh`,
|
|
53
|
-
|
|
59
|
+
kind: "uniform",
|
|
60
|
+
usage: GPUBufferUsage.COPY_SRC,
|
|
54
61
|
size: 9 * 16
|
|
55
62
|
});
|
|
56
63
|
}
|
|
@@ -74,6 +81,8 @@ class EnvironmentStore {
|
|
|
74
81
|
frame = 0;
|
|
75
82
|
/** The frame the prefilter work last ran (it runs once per frame, for all views). */
|
|
76
83
|
ranFrame = -1;
|
|
84
|
+
/** The baseline tier's prefilter (0064): fragment passes, loaded only on a baseline device. */
|
|
85
|
+
baseline;
|
|
77
86
|
ensure(gpu) {
|
|
78
87
|
if (this.generation === gpu.generation && this.lut) return;
|
|
79
88
|
this.generation = gpu.generation;
|
|
@@ -84,19 +93,21 @@ class EnvironmentStore {
|
|
|
84
93
|
label: "environment/brdf-lut",
|
|
85
94
|
size: [LUT_SIZE, LUT_SIZE],
|
|
86
95
|
format: "rgba16float",
|
|
87
|
-
|
|
96
|
+
// Computed on the full tier; drawn by a fragment pass on baseline (0064).
|
|
97
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | (gpu.tier === "full" ? GPUTextureUsage.STORAGE_BINDING : GPUTextureUsage.RENDER_ATTACHMENT)
|
|
88
98
|
});
|
|
89
99
|
this.lutReady = false;
|
|
90
100
|
this.emptyCube = gpu.device.createTexture({
|
|
91
101
|
label: "environment/empty",
|
|
92
102
|
size: [1, 1, 6],
|
|
93
103
|
format: "rgba16float",
|
|
94
|
-
usage: GPUTextureUsage.TEXTURE_BINDING
|
|
104
|
+
usage: GPUTextureUsage.TEXTURE_BINDING,
|
|
105
|
+
...bindingDimension(gpu, "cube")
|
|
95
106
|
});
|
|
96
107
|
this.emptyView = this.emptyCube.createView({ dimension: "cube" });
|
|
97
|
-
this.emptySh = new
|
|
108
|
+
this.emptySh = new DataStore(gpu, {
|
|
98
109
|
label: "environment/empty-sh",
|
|
99
|
-
|
|
110
|
+
kind: "uniform",
|
|
100
111
|
size: 9 * 16
|
|
101
112
|
});
|
|
102
113
|
this.sampler = gpu.device.createSampler({
|
package/dist/environment.js
CHANGED
|
@@ -90,6 +90,7 @@ const ProceduralSky = defineComponent(
|
|
|
90
90
|
const DefaultEnvironment = defineResource(
|
|
91
91
|
"render/DefaultEnvironment",
|
|
92
92
|
{
|
|
93
|
+
hostWritable: true,
|
|
93
94
|
description: "The environment (map or procedural sky) for cameras that have none. AmbientLight is the fallback when neither is set.",
|
|
94
95
|
init: () => ({ map: null, sky: null, background: true })
|
|
95
96
|
}
|
|
@@ -194,6 +195,7 @@ const prepareEnvironments = defineSystem({
|
|
|
194
195
|
} else if (map?.texture) useMap(map, skybox);
|
|
195
196
|
else if (defaults.map?.texture)
|
|
196
197
|
useMap(defaults.map, defaults.background ? Math.max(skybox, 1) : skybox);
|
|
198
|
+
if (cam.alphaOutput) entry.background = -1;
|
|
197
199
|
store.cameras.set(e, entry);
|
|
198
200
|
view.data.environment = entry;
|
|
199
201
|
}
|
|
@@ -312,6 +314,10 @@ function runEnvironmentWork(ctx) {
|
|
|
312
314
|
const gpu = ctx.gpu;
|
|
313
315
|
const store = world.resource(Environments);
|
|
314
316
|
if (store.ranFrame === store.frame) return;
|
|
317
|
+
if (gpu.tier === "baseline") {
|
|
318
|
+
store.baseline?.runBaselineEnvironment(ctx, store);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
315
321
|
if (store.pending.length === 0 && store.lutReady) {
|
|
316
322
|
store.ranFrame = store.frame;
|
|
317
323
|
return;
|
|
@@ -422,7 +428,7 @@ function runEnvironmentWork(ctx) {
|
|
|
422
428
|
mipLevelCount: 1
|
|
423
429
|
})
|
|
424
430
|
},
|
|
425
|
-
{ binding: 1, resource: { buffer: env.sh.
|
|
431
|
+
{ binding: 1, resource: { buffer: env.sh.gpuBuffer } }
|
|
426
432
|
]
|
|
427
433
|
})
|
|
428
434
|
);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type ComponentDef, type World } from '@aethervtt/shard-core';
|
|
2
|
+
/**
|
|
3
|
+
* Logs `render/feature-missing` once per world and plugin: `what` asked for a feature whose
|
|
4
|
+
* `plugin` (one forwardPlugin includes) isn't installed, and `outcome` is what happens instead.
|
|
5
|
+
*/
|
|
6
|
+
export declare function warnFeatureMissing(world: World, what: string, plugin: string, outcome: string): void;
|
|
7
|
+
/** How a feature runs on the baseline tier: a short account of its strategy there. */
|
|
8
|
+
export interface BaselineStrategy {
|
|
9
|
+
strategy: string;
|
|
10
|
+
}
|
|
11
|
+
export interface RenderFeature {
|
|
12
|
+
/** `package/feature`, e.g. `render/bloom`. */
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly description: string;
|
|
15
|
+
/** The graph nodes it adds. */
|
|
16
|
+
readonly nodes: readonly string[];
|
|
17
|
+
/** What it does on the baseline tier, or `'unsupported'`. */
|
|
18
|
+
readonly baseline: BaselineStrategy | 'unsupported';
|
|
19
|
+
/** Components that mean a scene uses it (for `render/feature-unsupported` on baseline). */
|
|
20
|
+
readonly components?: readonly ComponentDef[];
|
|
21
|
+
}
|
|
22
|
+
export declare const RenderFeatures: import("@aethervtt/shard-core").ResourceDef<Map<string, RenderFeature>>;
|
|
23
|
+
/** Registers features a plugin installs. Plugins call it where they add their graph nodes. */
|
|
24
|
+
export declare function addRenderFeatures(world: World, ...features: RenderFeature[]): void;
|
|
25
|
+
/** The feature a graph node belongs to, if one registered it. */
|
|
26
|
+
export declare function featureOfNode(world: World, node: string): RenderFeature | undefined;
|
|
27
|
+
/** `render.describe → features`: each installed feature, its nodes, and its baseline strategy. */
|
|
28
|
+
export declare function describeFeatures(world: World): {
|
|
29
|
+
name: string;
|
|
30
|
+
nodes: string[];
|
|
31
|
+
baseline: string;
|
|
32
|
+
}[];
|
|
33
|
+
/**
|
|
34
|
+
* The graph nodes of features the baseline tier doesn't run (`baseline: 'unsupported'`): the
|
|
35
|
+
* graph skips them on a baseline device, so they never ask for what it lacks.
|
|
36
|
+
*/
|
|
37
|
+
export declare function unsupportedNodes(world: World): ReadonlySet<string>;
|
|
38
|
+
/**
|
|
39
|
+
* Raises `render/feature-unsupported` for `feature` (a feature, or a part of one) that a scene uses
|
|
40
|
+
* on the baseline tier: RenderHealth turns degraded until `clearUnsupported`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function reportUnsupported(world: World, feature: string, message: string): void;
|
|
43
|
+
export declare function clearUnsupported(world: World, feature: string): void;
|
|
44
|
+
/**
|
|
45
|
+
* On the baseline tier: reports each unsupported feature whose components are in the world, and
|
|
46
|
+
* clears the report once they're gone. A query per component, made once; issues change only when
|
|
47
|
+
* a feature's use does.
|
|
48
|
+
*/
|
|
49
|
+
export declare function checkUnsupportedFeatures(world: World): void;
|
package/dist/features.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { defineResource } from "@aethervtt/shard-core";
|
|
2
|
+
import { LogResource } from "@aethervtt/shard-runtime";
|
|
3
|
+
import { clearHealthIssue, raiseHealthIssue } from "./health.js";
|
|
4
|
+
const missingPlugins = /* @__PURE__ */ new WeakMap();
|
|
5
|
+
function warnFeatureMissing(world, what, plugin, outcome) {
|
|
6
|
+
let seen = missingPlugins.get(world);
|
|
7
|
+
if (!seen) {
|
|
8
|
+
seen = /* @__PURE__ */ new Set();
|
|
9
|
+
missingPlugins.set(world, seen);
|
|
10
|
+
}
|
|
11
|
+
if (seen.has(plugin)) return;
|
|
12
|
+
seen.add(plugin);
|
|
13
|
+
world.tryResource(LogResource)?.log("warn", `${what}, but ${plugin} isn't installed; ${outcome}`, {
|
|
14
|
+
code: "render/feature-missing",
|
|
15
|
+
hint: `Add ${plugin} from '@aethervtt/shard-render' (forwardPlugin includes it).`
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const RenderFeatures = defineResource("render/Features", {
|
|
19
|
+
description: "Render features installed, the graph nodes each adds, and its baseline strategy.",
|
|
20
|
+
init: () => /* @__PURE__ */ new Map()
|
|
21
|
+
});
|
|
22
|
+
function addRenderFeatures(world, ...features) {
|
|
23
|
+
const registry = world.initResource(RenderFeatures);
|
|
24
|
+
for (const feature of features) registry.set(feature.name, feature);
|
|
25
|
+
}
|
|
26
|
+
function featureOfNode(world, node) {
|
|
27
|
+
for (const feature of world.tryResource(RenderFeatures)?.values() ?? []) {
|
|
28
|
+
if (feature.nodes.includes(node)) return feature;
|
|
29
|
+
}
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
function describeFeatures(world) {
|
|
33
|
+
return [...world.tryResource(RenderFeatures)?.values() ?? []].map((f) => ({
|
|
34
|
+
name: f.name,
|
|
35
|
+
nodes: [...f.nodes],
|
|
36
|
+
baseline: f.baseline === "unsupported" ? "unsupported" : f.baseline.strategy
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
const unsupported = /* @__PURE__ */ new WeakMap();
|
|
40
|
+
function unsupportedNodes(world) {
|
|
41
|
+
const registry = world.initResource(RenderFeatures);
|
|
42
|
+
let cached = unsupported.get(registry);
|
|
43
|
+
if (!cached || cached.size !== registry.size) {
|
|
44
|
+
const nodes = /* @__PURE__ */ new Set();
|
|
45
|
+
for (const f of registry.values())
|
|
46
|
+
if (f.baseline === "unsupported") for (const n of f.nodes) nodes.add(n);
|
|
47
|
+
cached = { size: registry.size, nodes };
|
|
48
|
+
unsupported.set(registry, cached);
|
|
49
|
+
}
|
|
50
|
+
return cached.nodes;
|
|
51
|
+
}
|
|
52
|
+
function reportUnsupported(world, feature, message) {
|
|
53
|
+
raiseHealthIssue(world, {
|
|
54
|
+
code: "render/feature-unsupported",
|
|
55
|
+
severity: "degraded",
|
|
56
|
+
ref: feature,
|
|
57
|
+
message
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function clearUnsupported(world, feature) {
|
|
61
|
+
clearHealthIssue(world, "render/feature-unsupported", feature);
|
|
62
|
+
}
|
|
63
|
+
const usage = /* @__PURE__ */ new WeakMap();
|
|
64
|
+
function checkUnsupportedFeatures(world) {
|
|
65
|
+
let state = usage.get(world);
|
|
66
|
+
if (!state) {
|
|
67
|
+
state = { queries: /* @__PURE__ */ new Map(), reported: /* @__PURE__ */ new Set() };
|
|
68
|
+
usage.set(world, state);
|
|
69
|
+
}
|
|
70
|
+
const queries = state.queries;
|
|
71
|
+
for (const f of world.initResource(RenderFeatures).values()) {
|
|
72
|
+
if (f.baseline !== "unsupported" || !f.components) continue;
|
|
73
|
+
let used = false;
|
|
74
|
+
for (const c of f.components) {
|
|
75
|
+
let q = queries.get(c);
|
|
76
|
+
if (!q) {
|
|
77
|
+
q = world.query({ with: [c] });
|
|
78
|
+
queries.set(c, q);
|
|
79
|
+
}
|
|
80
|
+
for (const table of q.tables) if (table.count > 0) used = true;
|
|
81
|
+
}
|
|
82
|
+
if (used === state.reported.has(f.name)) continue;
|
|
83
|
+
if (used) {
|
|
84
|
+
state.reported.add(f.name);
|
|
85
|
+
reportUnsupported(
|
|
86
|
+
world,
|
|
87
|
+
f.name,
|
|
88
|
+
`${f.name} (${f.description}) runs on the full tier only (WebGPU with compute and storage in shaders): it draws nothing here`
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
state.reported.delete(f.name);
|
|
92
|
+
clearUnsupported(world, f.name);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export {
|
|
97
|
+
RenderFeatures,
|
|
98
|
+
addRenderFeatures,
|
|
99
|
+
checkUnsupportedFeatures,
|
|
100
|
+
clearUnsupported,
|
|
101
|
+
describeFeatures,
|
|
102
|
+
featureOfNode,
|
|
103
|
+
reportUnsupported,
|
|
104
|
+
unsupportedNodes,
|
|
105
|
+
warnFeatureMissing
|
|
106
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type Profiler } from '@aethervtt/shard-core';
|
|
2
|
+
/** The budget key foliage's GPU time is measured by: it covers `gpu:foliage/place`, `/cull`, `/draw`. */
|
|
3
|
+
export declare const FOLIAGE_SPAN_KEY = "gpu:foliage";
|
|
4
|
+
export interface FoliageBudgetValue {
|
|
5
|
+
/**
|
|
6
|
+
* GPU milliseconds foliage may take a frame. 0 (the default): its `gpu:foliage` slice of the
|
|
7
|
+
* app's scenario (`App.perfScenario`) on the detected machine, and with no scenario (or an
|
|
8
|
+
* unknown machine) no target: foliage draws at full detail.
|
|
9
|
+
*/
|
|
10
|
+
ms: number;
|
|
11
|
+
/** The least detail it thins to, as a share of the instances drawn at full detail. Default 0.1. */
|
|
12
|
+
minDetail: number;
|
|
13
|
+
/** Read only: the target in force (ms), 0 when none. */
|
|
14
|
+
target: number;
|
|
15
|
+
/** Read only: foliage's GPU time a frame, smoothed (ms). */
|
|
16
|
+
measuredMs: number;
|
|
17
|
+
/**
|
|
18
|
+
* Read only: the share of full detail drawn (minDetail to 1). Density is its square root and range
|
|
19
|
+
* its fourth root, so instances drawn scale with it.
|
|
20
|
+
*/
|
|
21
|
+
detail: number;
|
|
22
|
+
}
|
|
23
|
+
export declare const FoliageBudget: import("@aethervtt/shard-core").ResourceDef<FoliageBudgetValue>;
|
|
24
|
+
/**
|
|
25
|
+
* The controller: one GPU time a frame in, detail out. Smoothed (an exponential average), with
|
|
26
|
+
* hysteresis (it moves only above the target, or under 85% of it) and asymmetric steps (down 1% a
|
|
27
|
+
* sample, up 0.5%), so the lag of GPU timings (frames late) doesn't make it oscillate. Like
|
|
28
|
+
* `TerrainBudget.triangles`' LOD bias (0043), slow on purpose: density and range change gradually.
|
|
29
|
+
*/
|
|
30
|
+
export declare class FoliageController {
|
|
31
|
+
detail: number;
|
|
32
|
+
/** Smoothed GPU time (ms); 0 before the first sample. */
|
|
33
|
+
smoothed: number;
|
|
34
|
+
private primed;
|
|
35
|
+
/** One frame's foliage GPU time against `target` (0: none, back to full detail at once). */
|
|
36
|
+
sample(ms: number, target: number, minDetail: number): void;
|
|
37
|
+
/** No target: full detail, and the smoothing starts over. */
|
|
38
|
+
reset(): void;
|
|
39
|
+
/** The share of instances kept by thinning (the cull's density factor). */
|
|
40
|
+
get density(): number;
|
|
41
|
+
/** The share of each layer's range drawn. */
|
|
42
|
+
get rangeScale(): number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Reads the GPU time a budget key covers, one GPU frame at a time, without allocating: when
|
|
46
|
+
* `gpu:frame` has a new sample, the covered spans that got one with it are summed. Span names are
|
|
47
|
+
* matched by `spanCovers` as they're interned.
|
|
48
|
+
*/
|
|
49
|
+
export declare class CoveredGpuTime {
|
|
50
|
+
readonly key: string;
|
|
51
|
+
/** The latest frame's covered time (ms), after `read` returned true. */
|
|
52
|
+
ms: number;
|
|
53
|
+
private ids;
|
|
54
|
+
private seen;
|
|
55
|
+
private count;
|
|
56
|
+
private scanned;
|
|
57
|
+
private frameId;
|
|
58
|
+
private frameRuns;
|
|
59
|
+
constructor(key: string);
|
|
60
|
+
/** True when a new GPU frame's timings landed since the last call; `ms` is then its covered time. */
|
|
61
|
+
read(profiler: Profiler): boolean;
|
|
62
|
+
/** New span names since the last scan: the ones the key covers join the list. */
|
|
63
|
+
private scan;
|
|
64
|
+
}
|