@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/scenario.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { spanCovers, spanName } from "@aethervtt/shard-core";
|
|
2
|
+
import {
|
|
3
|
+
budget,
|
|
4
|
+
passTiming,
|
|
5
|
+
scenarioSlices,
|
|
6
|
+
timingMode
|
|
7
|
+
} from "@aethervtt/shard-core/test-env";
|
|
8
|
+
import { capturePerf } from "@aethervtt/shard-runtime";
|
|
9
|
+
import { ablatePasses } from "./ablation.js";
|
|
10
|
+
import { Graph } from "./plugin.js";
|
|
11
|
+
function p95Of(values) {
|
|
12
|
+
const sorted = Array.from(values).sort((a, b) => a - b);
|
|
13
|
+
if (sorted.length === 0) return 0;
|
|
14
|
+
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))];
|
|
15
|
+
}
|
|
16
|
+
async function measureScenario(world, name, options) {
|
|
17
|
+
const keys = scenarioSlices(name);
|
|
18
|
+
let i = 0;
|
|
19
|
+
const { summary, capture } = await capturePerf(world, {
|
|
20
|
+
frames: options.frames,
|
|
21
|
+
write: false,
|
|
22
|
+
trace: false,
|
|
23
|
+
keep: true,
|
|
24
|
+
step: async () => {
|
|
25
|
+
await options.step(Math.min(i, options.frames - 1));
|
|
26
|
+
i++;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
if (!capture) throw new Error("capturePerf kept no capture");
|
|
30
|
+
const seen = /* @__PURE__ */ new Set();
|
|
31
|
+
for (let e = 0; e < capture.eventCount; e++) seen.add(capture.events.ids[e]);
|
|
32
|
+
const spans = [...seen].map(spanName).sort();
|
|
33
|
+
const gpuFrame = capture.spanTime("gpu:frame");
|
|
34
|
+
const timedFrames = [];
|
|
35
|
+
for (let f = 0; f < gpuFrame.length; f++) if (gpuFrame[f] > 0) timedFrames.push(f);
|
|
36
|
+
const gpuTimed = timedFrames.length > 0;
|
|
37
|
+
const slices = [];
|
|
38
|
+
for (const track of ["gpu", "cpu"]) {
|
|
39
|
+
for (const key of keys[track]) {
|
|
40
|
+
const perFrame = capture.spanTime(key);
|
|
41
|
+
const values = track === "gpu" ? timedFrames.map((f) => perFrame[f]) : perFrame;
|
|
42
|
+
slices.push({
|
|
43
|
+
key,
|
|
44
|
+
track,
|
|
45
|
+
covers: spans.filter((s) => spanCovers(key, s)),
|
|
46
|
+
perFrame,
|
|
47
|
+
p95: p95Of(values),
|
|
48
|
+
measuredBy: "capture"
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if ((options.ablation?.force || passTiming() === "ablation") && gpuTimed) {
|
|
53
|
+
const graph = world.resource(Graph);
|
|
54
|
+
const nodes = graph.nodeNames();
|
|
55
|
+
const groups = {};
|
|
56
|
+
for (const s of slices) {
|
|
57
|
+
if (s.track !== "gpu") continue;
|
|
58
|
+
const covered = nodes.filter((n) => spanCovers(s.key, `gpu:${n}`));
|
|
59
|
+
if (covered.length > 0) groups[s.key] = covered;
|
|
60
|
+
}
|
|
61
|
+
const passes = Object.keys(groups);
|
|
62
|
+
if (passes.length > 0) {
|
|
63
|
+
const result = await ablatePasses(
|
|
64
|
+
world,
|
|
65
|
+
{
|
|
66
|
+
passes,
|
|
67
|
+
groups,
|
|
68
|
+
frames: options.ablation?.frames ?? 60,
|
|
69
|
+
rounds: options.ablation?.rounds ?? 3
|
|
70
|
+
},
|
|
71
|
+
() => options.step(options.frames - 1)
|
|
72
|
+
);
|
|
73
|
+
for (const cost of result.passes) {
|
|
74
|
+
const s = slices.find((x) => x.track === "gpu" && x.key === cost.pass);
|
|
75
|
+
s.p95 = Math.max(0, cost.ms);
|
|
76
|
+
s.measuredBy = "ablation";
|
|
77
|
+
s.noise = (Math.max(...cost.rounds) - Math.min(...cost.rounds)) / 2;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
name,
|
|
83
|
+
summary,
|
|
84
|
+
capture,
|
|
85
|
+
spans,
|
|
86
|
+
frame: {
|
|
87
|
+
cpu: capture.frames.cpu,
|
|
88
|
+
gpu: gpuFrame,
|
|
89
|
+
cpuP95: p95Of(capture.frames.cpu),
|
|
90
|
+
gpuP95: p95Of(timedFrames.map((f) => gpuFrame[f]))
|
|
91
|
+
},
|
|
92
|
+
gpuTimed,
|
|
93
|
+
slices
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function expectScenario(run, expect) {
|
|
97
|
+
expect(run.summary.frames.count).toBe(run.capture.frameCount);
|
|
98
|
+
expect(run.capture.frameCount).toBeGreaterThan(0);
|
|
99
|
+
for (const s of run.slices) {
|
|
100
|
+
if (s.track === "gpu" && !run.gpuTimed) continue;
|
|
101
|
+
expect(s.covers, `${run.name}: slice "${s.key}" covers no span that ran`).not.toEqual([]);
|
|
102
|
+
}
|
|
103
|
+
if (timingMode !== "bench") return;
|
|
104
|
+
const soft = expect.soft;
|
|
105
|
+
soft(run.frame.cpuP95, `${run.name}: CPU frame p95`).toBeLessThan(
|
|
106
|
+
budget(`${run.name}`, { track: "cpu" })
|
|
107
|
+
);
|
|
108
|
+
if (run.gpuTimed) {
|
|
109
|
+
soft(run.frame.gpuP95, `${run.name}: GPU frame p95`).toBeLessThan(
|
|
110
|
+
budget(`${run.name}`, { track: "gpu" })
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
for (const s of run.slices) {
|
|
114
|
+
if (s.track === "gpu" && !run.gpuTimed) continue;
|
|
115
|
+
const measured = s.noise === void 0 ? s.p95 : Math.max(0, s.p95 - s.noise);
|
|
116
|
+
const how = s.noise === void 0 ? s.measuredBy : `ablation, ${s.p95.toFixed(3)} \xB1 ${s.noise.toFixed(3)} ms`;
|
|
117
|
+
soft(measured, `${run.name}: ${s.track} slice ${s.key} p95 (${how})`).toBeLessThan(
|
|
118
|
+
budget(`${run.name}`, { slice: s.key, track: s.track })
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export {
|
|
123
|
+
expectScenario,
|
|
124
|
+
measureScenario
|
|
125
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type Entity, type World } from '@aethervtt/shard-core';
|
|
2
|
+
/** Effects `ScreenEffects` holds at once. */
|
|
3
|
+
export declare const MAX_SCREEN_EFFECTS = 8;
|
|
4
|
+
/** Keys an effect's params may have. */
|
|
5
|
+
export declare const MAX_SCREEN_EFFECT_PARAMS = 8;
|
|
6
|
+
export type ScreenEffectParams = Record<string, number | string | boolean>;
|
|
7
|
+
/** An effect another app's view should draw at a screen point (0065). */
|
|
8
|
+
export interface ScreenEffect {
|
|
9
|
+
/** What to draw: `fire`, or a kind the consuming app registered. */
|
|
10
|
+
kind: string;
|
|
11
|
+
/** Center, in the target's CSS pixels. */
|
|
12
|
+
screen: [number, number];
|
|
13
|
+
/** In CSS pixels. */
|
|
14
|
+
radius: number;
|
|
15
|
+
/** Ms it has left: counts down each frame, and refreshing sets it again. At 0 it drops. */
|
|
16
|
+
ttlMs: number;
|
|
17
|
+
/** What published it (a die); one effect per (source, kind). */
|
|
18
|
+
source: Entity;
|
|
19
|
+
params: ScreenEffectParams;
|
|
20
|
+
}
|
|
21
|
+
export declare const ScreenEffects: import("@aethervtt/shard-core").ResourceDef<{
|
|
22
|
+
effects: ScreenEffect[];
|
|
23
|
+
}>;
|
|
24
|
+
/** What an app draws for one kind of screen effect. */
|
|
25
|
+
export interface ScreenEffectHandler {
|
|
26
|
+
/** An effect of this kind appeared (a new source): spawn what draws it. */
|
|
27
|
+
start(world: World, effect: ScreenEffect): Entity[] | undefined;
|
|
28
|
+
/** Each frame it stays live (refreshed), with the frame's seconds. */
|
|
29
|
+
update?(world: World, effect: ScreenEffect, entities: readonly Entity[], dt: number): void;
|
|
30
|
+
/** It expired or was cleared. Default: despawn what `start` returned. */
|
|
31
|
+
end?(world: World, effect: ScreenEffect, entities: readonly Entity[]): void;
|
|
32
|
+
}
|
|
33
|
+
interface LiveEffect {
|
|
34
|
+
source: Entity;
|
|
35
|
+
kind: string;
|
|
36
|
+
handler: ScreenEffectHandler;
|
|
37
|
+
entities: Entity[];
|
|
38
|
+
/** The effect as last seen, for `end`. */
|
|
39
|
+
last: ScreenEffect;
|
|
40
|
+
seen: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** This app's handlers, the effects they're drawing, and the unhandled kinds already reported. */
|
|
43
|
+
export declare const ScreenEffectHandlers: import("@aethervtt/shard-core").ResourceDef<{
|
|
44
|
+
kinds: Map<string, ScreenEffectHandler>;
|
|
45
|
+
live: LiveEffect[];
|
|
46
|
+
reported: Set<string>;
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* Publishes or refreshes `source`'s effect of this kind. Returns false when `MAX_SCREEN_EFFECTS`
|
|
50
|
+
* others are live. Throws `render/invalid-screen-effect` for an effect without a kind, a negative
|
|
51
|
+
* radius, or more than 8 params.
|
|
52
|
+
*/
|
|
53
|
+
export declare function publishScreenEffect(world: World, effect: ScreenEffect): boolean;
|
|
54
|
+
/** Drops `source`'s effects, or every effect. Publishers call it on dismissal and teardown. */
|
|
55
|
+
export declare function clearScreenEffects(world: World, source?: Entity): void;
|
|
56
|
+
/**
|
|
57
|
+
* Copies `from`'s effects into `to` (another app on the device, 0052), for the host to call each
|
|
58
|
+
* frame, as `forwardLensFields`: copies, each app counting its own down; `offset` is where `to`'s
|
|
59
|
+
* target sits in `from`'s CSS pixels, subtracted from each center.
|
|
60
|
+
*/
|
|
61
|
+
export declare function forwardScreenEffects(from: World, to: World, offset?: readonly [number, number]): void;
|
|
62
|
+
/**
|
|
63
|
+
* Registers what this app draws for a kind of screen effect: `start` when one appears, `update`
|
|
64
|
+
* while it's refreshed, `end` when it expires (default: despawn what `start` returned).
|
|
65
|
+
*/
|
|
66
|
+
export declare function onScreenEffect(world: World, kind: string, handler: ScreenEffectHandler): void;
|
|
67
|
+
/** The frame demand live effects hold, so they expire on time and the app idles after (0052). */
|
|
68
|
+
export declare const SCREEN_EFFECTS_DEMAND = "render/screen-effects";
|
|
69
|
+
export declare const expireScreenEffects: import("@aethervtt/shard-core").SystemDef<undefined>;
|
|
70
|
+
/**
|
|
71
|
+
* Starts, updates and ends this app's handlers as effects appear, stay and go. An effect of a kind
|
|
72
|
+
* nobody registered is dropped and logged once per kind (`render/unhandled-screen-effect`).
|
|
73
|
+
* Allocates only when an effect starts.
|
|
74
|
+
*/
|
|
75
|
+
export declare const runScreenEffects: import("@aethervtt/shard-core").SystemDef<undefined>;
|
|
76
|
+
/** For render.describe: live effects, and which kinds this app draws. */
|
|
77
|
+
export declare function describeScreenEffects(world: World): {
|
|
78
|
+
effects: {
|
|
79
|
+
/** What to draw: `fire`, or a kind the consuming app registered. */
|
|
80
|
+
kind: string;
|
|
81
|
+
/** In CSS pixels. */
|
|
82
|
+
radius: number;
|
|
83
|
+
/** Ms it has left: counts down each frame, and refreshing sets it again. At 0 it drops. */
|
|
84
|
+
ttlMs: number;
|
|
85
|
+
/** What published it (a die); one effect per (source, kind). */
|
|
86
|
+
source: Entity;
|
|
87
|
+
params: ScreenEffectParams;
|
|
88
|
+
screen: number[];
|
|
89
|
+
}[];
|
|
90
|
+
kinds: string[];
|
|
91
|
+
drawing: {
|
|
92
|
+
kind: string;
|
|
93
|
+
source: number;
|
|
94
|
+
entities: number;
|
|
95
|
+
}[];
|
|
96
|
+
};
|
|
97
|
+
export {};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineResource,
|
|
3
|
+
defineSystem,
|
|
4
|
+
ShardError
|
|
5
|
+
} from "@aethervtt/shard-core";
|
|
6
|
+
import { FrameDemand, LogResource, Time } from "@aethervtt/shard-runtime";
|
|
7
|
+
const MAX_SCREEN_EFFECTS = 8;
|
|
8
|
+
const MAX_SCREEN_EFFECT_PARAMS = 8;
|
|
9
|
+
const ScreenEffects = defineResource("render/ScreenEffects", {
|
|
10
|
+
hostWritable: true,
|
|
11
|
+
description: "Typed effects published at a screen point (at most 8): kind, center and radius in CSS pixels, ms to live, source entity, params. The app whose view they are in draws them through handlers registered with onScreenEffect.",
|
|
12
|
+
init: () => ({ effects: [] })
|
|
13
|
+
});
|
|
14
|
+
const ScreenEffectHandlers = defineResource("render/ScreenEffectHandlers", {
|
|
15
|
+
description: "Handlers by kind (onScreenEffect) and the screen effects they are drawing.",
|
|
16
|
+
init: () => ({ kinds: /* @__PURE__ */ new Map(), live: [], reported: /* @__PURE__ */ new Set() })
|
|
17
|
+
});
|
|
18
|
+
function invalid(message, path) {
|
|
19
|
+
return new ShardError("render/invalid-screen-effect", message, {
|
|
20
|
+
path,
|
|
21
|
+
hint: "A screen effect has a kind, a radius of at least 0, and at most 8 params."
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function publishScreenEffect(world, effect) {
|
|
25
|
+
if (!effect.kind) throw invalid("A screen effect needs a kind", "kind");
|
|
26
|
+
if (!(effect.radius >= 0)) throw invalid(`Radius ${effect.radius} is below 0`, "radius");
|
|
27
|
+
const keys = Object.keys(effect.params);
|
|
28
|
+
if (keys.length > MAX_SCREEN_EFFECT_PARAMS) {
|
|
29
|
+
throw invalid(`${keys.length} params; at most ${MAX_SCREEN_EFFECT_PARAMS}`, "params");
|
|
30
|
+
}
|
|
31
|
+
const effects = world.initResource(ScreenEffects).effects;
|
|
32
|
+
let i = 0;
|
|
33
|
+
while (i < effects.length && (effects[i].source !== effect.source || effects[i].kind !== effect.kind))
|
|
34
|
+
i++;
|
|
35
|
+
if (i === MAX_SCREEN_EFFECTS) return false;
|
|
36
|
+
const e = effects[i];
|
|
37
|
+
if (e) {
|
|
38
|
+
e.screen[0] = effect.screen[0];
|
|
39
|
+
e.screen[1] = effect.screen[1];
|
|
40
|
+
e.radius = effect.radius;
|
|
41
|
+
e.ttlMs = effect.ttlMs;
|
|
42
|
+
e.params = effect.params;
|
|
43
|
+
} else effects.push({ ...effect, screen: [effect.screen[0], effect.screen[1]] });
|
|
44
|
+
world.touchResource(ScreenEffects);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
function clearScreenEffects(world, source) {
|
|
48
|
+
const value = world.tryResource(ScreenEffects);
|
|
49
|
+
if (!value || value.effects.length === 0) return;
|
|
50
|
+
if (source === void 0) value.effects.length = 0;
|
|
51
|
+
else value.effects = value.effects.filter((e) => e.source !== source);
|
|
52
|
+
world.touchResource(ScreenEffects);
|
|
53
|
+
}
|
|
54
|
+
const NO_OFFSET = [0, 0];
|
|
55
|
+
function forwardScreenEffects(from, to, offset = NO_OFFSET) {
|
|
56
|
+
const source = from.tryResource(ScreenEffects)?.effects ?? [];
|
|
57
|
+
const out = to.initResource(ScreenEffects).effects;
|
|
58
|
+
const n = Math.min(source.length, MAX_SCREEN_EFFECTS);
|
|
59
|
+
for (let i = 0; i < n; i++) {
|
|
60
|
+
const s = source[i];
|
|
61
|
+
const d = out[i];
|
|
62
|
+
if (d) {
|
|
63
|
+
d.kind = s.kind;
|
|
64
|
+
d.radius = s.radius;
|
|
65
|
+
d.ttlMs = s.ttlMs;
|
|
66
|
+
d.source = s.source;
|
|
67
|
+
d.params = s.params;
|
|
68
|
+
} else out.push({ ...s, screen: [0, 0] });
|
|
69
|
+
const e = out[i];
|
|
70
|
+
e.screen[0] = s.screen[0] - offset[0];
|
|
71
|
+
e.screen[1] = s.screen[1] - offset[1];
|
|
72
|
+
}
|
|
73
|
+
if (out.length === 0 && n === 0) return;
|
|
74
|
+
out.length = n;
|
|
75
|
+
to.touchResource(ScreenEffects);
|
|
76
|
+
}
|
|
77
|
+
function onScreenEffect(world, kind, handler) {
|
|
78
|
+
world.initResource(ScreenEffectHandlers).kinds.set(kind, handler);
|
|
79
|
+
}
|
|
80
|
+
const SCREEN_EFFECTS_DEMAND = "render/screen-effects";
|
|
81
|
+
const expireScreenEffects = defineSystem({
|
|
82
|
+
name: "render/expire-screen-effects",
|
|
83
|
+
description: "Counts ScreenEffects down by the frame time and drops expired ones; holds frames while any live.",
|
|
84
|
+
run: (_, world) => {
|
|
85
|
+
const effects = world.tryResource(ScreenEffects)?.effects;
|
|
86
|
+
let n = 0;
|
|
87
|
+
if (effects && effects.length > 0) {
|
|
88
|
+
const ms = world.resource(Time).delta * 1e3;
|
|
89
|
+
for (let i = 0; i < effects.length; i++) {
|
|
90
|
+
const e = effects[i];
|
|
91
|
+
e.ttlMs -= ms;
|
|
92
|
+
if (e.ttlMs > 0 && n < MAX_SCREEN_EFFECTS) effects[n++] = e;
|
|
93
|
+
}
|
|
94
|
+
effects.length = n;
|
|
95
|
+
}
|
|
96
|
+
const live = world.tryResource(ScreenEffectHandlers)?.live.length ?? 0;
|
|
97
|
+
world.tryResource(FrameDemand)?.set(SCREEN_EFFECTS_DEMAND, n > 0 || live > 0);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
function despawnAll(world, entities) {
|
|
101
|
+
for (const e of entities) if (world.isAlive(e)) world.despawn(e);
|
|
102
|
+
}
|
|
103
|
+
const runScreenEffects = defineSystem({
|
|
104
|
+
name: "render/screen-effects",
|
|
105
|
+
description: "Runs the handlers registered with onScreenEffect for live ScreenEffects.",
|
|
106
|
+
run: (_, world) => {
|
|
107
|
+
const handlers = world.tryResource(ScreenEffectHandlers);
|
|
108
|
+
const effects = world.tryResource(ScreenEffects)?.effects;
|
|
109
|
+
if (!handlers || handlers.live.length === 0 && (!effects || effects.length === 0)) return;
|
|
110
|
+
const live = handlers.live;
|
|
111
|
+
for (let i = 0; i < live.length; i++) live[i].seen = false;
|
|
112
|
+
const dt = world.resource(Time).delta;
|
|
113
|
+
if (effects) {
|
|
114
|
+
for (let i = 0; i < effects.length; i++) {
|
|
115
|
+
const e = effects[i];
|
|
116
|
+
let found;
|
|
117
|
+
for (let j = 0; j < live.length; j++) {
|
|
118
|
+
const l = live[j];
|
|
119
|
+
if (l.source === e.source && l.kind === e.kind) {
|
|
120
|
+
found = l;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (found) {
|
|
125
|
+
found.seen = true;
|
|
126
|
+
found.last = e;
|
|
127
|
+
found.handler.update?.(world, e, found.entities, dt);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const handler = handlers.kinds.get(e.kind);
|
|
131
|
+
if (!handler) {
|
|
132
|
+
if (!handlers.reported.has(e.kind)) {
|
|
133
|
+
handlers.reported.add(e.kind);
|
|
134
|
+
world.tryResource(LogResource)?.log("warn", `No handler draws "${e.kind}" effects`, {
|
|
135
|
+
code: "render/unhandled-screen-effect",
|
|
136
|
+
hint: "Register one in this app with onScreenEffect(world, kind, handler)."
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
live.push({
|
|
142
|
+
source: e.source,
|
|
143
|
+
kind: e.kind,
|
|
144
|
+
handler,
|
|
145
|
+
entities: handler.start(world, e) ?? [],
|
|
146
|
+
last: e,
|
|
147
|
+
seen: true
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
let kept = 0;
|
|
152
|
+
for (let i = 0; i < live.length; i++) {
|
|
153
|
+
const l = live[i];
|
|
154
|
+
if (l.seen) {
|
|
155
|
+
live[kept++] = l;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (l.handler.end) l.handler.end(world, l.last, l.entities);
|
|
159
|
+
else despawnAll(world, l.entities);
|
|
160
|
+
}
|
|
161
|
+
live.length = kept;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
function describeScreenEffects(world) {
|
|
165
|
+
const handlers = world.tryResource(ScreenEffectHandlers);
|
|
166
|
+
return {
|
|
167
|
+
effects: (world.tryResource(ScreenEffects)?.effects ?? []).map((e) => ({
|
|
168
|
+
...e,
|
|
169
|
+
screen: [...e.screen]
|
|
170
|
+
})),
|
|
171
|
+
kinds: handlers ? [...handlers.kinds.keys()].sort() : [],
|
|
172
|
+
drawing: (handlers?.live ?? []).map((l) => ({
|
|
173
|
+
kind: l.kind,
|
|
174
|
+
source: l.source,
|
|
175
|
+
entities: l.entities.length
|
|
176
|
+
}))
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export {
|
|
180
|
+
MAX_SCREEN_EFFECTS,
|
|
181
|
+
MAX_SCREEN_EFFECT_PARAMS,
|
|
182
|
+
SCREEN_EFFECTS_DEMAND,
|
|
183
|
+
ScreenEffectHandlers,
|
|
184
|
+
ScreenEffects,
|
|
185
|
+
clearScreenEffects,
|
|
186
|
+
describeScreenEffects,
|
|
187
|
+
expireScreenEffects,
|
|
188
|
+
forwardScreenEffects,
|
|
189
|
+
onScreenEffect,
|
|
190
|
+
publishScreenEffect,
|
|
191
|
+
runScreenEffects
|
|
192
|
+
};
|
package/dist/shaders.d.ts
CHANGED
|
@@ -25,6 +25,12 @@ export declare const ViewUniform: import("@aethervtt/shard-core").ComponentDef<{
|
|
|
25
25
|
* that sun, applied to its light (spec 0044). All ones without an atmosphere.
|
|
26
26
|
*/
|
|
27
27
|
readonly sunTransmittance: import("@aethervtt/shard-core").FieldType<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number], "f32"> & ((options?: import("@aethervtt/shard-core").FieldOptions<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]> | undefined) => import("@aethervtt/shard-core").FieldType<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number], "f32">);
|
|
28
|
+
/**
|
|
29
|
+
* Screen scale (0057): render pixels per CSS pixel (x); world units per render pixel at one
|
|
30
|
+
* unit of view depth, or everywhere when orthographic (y); 1 when orthographic (z). A CSS-pixel
|
|
31
|
+
* width w at view depth d is w · x · y · d world units (w · x · y orthographic).
|
|
32
|
+
*/
|
|
33
|
+
readonly pixelScale: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec4, "f32"> & ((options?: import("@aethervtt/shard-core").FieldOptions<import("@aethervtt/shard-core").Vec4> | undefined) => import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec4, "f32">);
|
|
28
34
|
}>;
|
|
29
35
|
export declare const viewLayout: import("@aethervtt/shard-shader").WgslLayout<{
|
|
30
36
|
readonly viewProj: import("@aethervtt/shard-core").FieldType<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number], "f32"> & ((options?: import("@aethervtt/shard-core").FieldOptions<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]> | undefined) => import("@aethervtt/shard-core").FieldType<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number], "f32">);
|
|
@@ -51,6 +57,12 @@ export declare const viewLayout: import("@aethervtt/shard-shader").WgslLayout<{
|
|
|
51
57
|
* that sun, applied to its light (spec 0044). All ones without an atmosphere.
|
|
52
58
|
*/
|
|
53
59
|
readonly sunTransmittance: import("@aethervtt/shard-core").FieldType<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number], "f32"> & ((options?: import("@aethervtt/shard-core").FieldOptions<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]> | undefined) => import("@aethervtt/shard-core").FieldType<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number], "f32">);
|
|
60
|
+
/**
|
|
61
|
+
* Screen scale (0057): render pixels per CSS pixel (x); world units per render pixel at one
|
|
62
|
+
* unit of view depth, or everywhere when orthographic (y); 1 when orthographic (z). A CSS-pixel
|
|
63
|
+
* width w at view depth d is w · x · y · d world units (w · x · y orthographic).
|
|
64
|
+
*/
|
|
65
|
+
readonly pixelScale: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec4, "f32"> & ((options?: import("@aethervtt/shard-core").FieldOptions<import("@aethervtt/shard-core").Vec4> | undefined) => import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec4, "f32">);
|
|
54
66
|
}>;
|
|
55
67
|
export declare const materialLayout: import("@aethervtt/shard-shader").WgslLayout<{
|
|
56
68
|
baseColor: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
|
|
@@ -59,6 +71,7 @@ export declare const materialLayout: import("@aethervtt/shard-shader").WgslLayou
|
|
|
59
71
|
emissive: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
|
|
60
72
|
emissiveLuminance: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
61
73
|
doubleSided: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
|
|
74
|
+
deferUntilReady: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
|
|
62
75
|
alphaMode: import("@aethervtt/shard-core").FieldType<"additive" | "alpha" | "mask" | "opaque" | "premultiplied", "u8">;
|
|
63
76
|
alphaCutoff: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
64
77
|
normalScale: import("@aethervtt/shard-core").FieldType<number, "f32">;
|