@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/outline.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineComponent, defineResource, onAdd, t } from "@aethervtt/shard-core";
|
|
2
|
+
import { warnFeatureMissing } from "./features.js";
|
|
3
|
+
const OUTLINE_OCCLUSION = ["hide", "show", "dim"];
|
|
4
|
+
const Outline = defineComponent(
|
|
5
|
+
"render/Outline",
|
|
6
|
+
{
|
|
7
|
+
color: t.color({
|
|
8
|
+
default: [1, 0.78, 0.2, 1],
|
|
9
|
+
description: "Outline color (linear), with alpha."
|
|
10
|
+
}),
|
|
11
|
+
width: t.f32({
|
|
12
|
+
default: 2,
|
|
13
|
+
min: 0,
|
|
14
|
+
max: 32,
|
|
15
|
+
unit: "px",
|
|
16
|
+
description: "Width in CSS pixels, outside the silhouette, at any zoom and display density."
|
|
17
|
+
}),
|
|
18
|
+
occluded: t.enum(OUTLINE_OCCLUSION, {
|
|
19
|
+
description: "Where something hides the outlined object: hide the outline there, show it anyway, or dim it."
|
|
20
|
+
})
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
description: "Outlines a renderable, or every renderable under this entity. Selection and hover are two Outlines with different colors; the host sets and clears them."
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
const OutlinePath = defineResource("render/OutlinePath", {
|
|
27
|
+
description: "Present when the outline pass is installed."
|
|
28
|
+
});
|
|
29
|
+
const MAX_OUTLINE_STYLES = 16;
|
|
30
|
+
function observeOutlinesWithoutPass(world) {
|
|
31
|
+
world.observe(onAdd(Outline), ({ world: w }) => {
|
|
32
|
+
if (!w.hasResource(OutlinePath))
|
|
33
|
+
warnFeatureMissing(w, "An entity has Outline", "outlinePlugin", "it draws no outline");
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
MAX_OUTLINE_STYLES,
|
|
38
|
+
OUTLINE_OCCLUSION,
|
|
39
|
+
Outline,
|
|
40
|
+
OutlinePath,
|
|
41
|
+
observeOutlinesWithoutPass
|
|
42
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Entity, World } from '@aethervtt/shard-core';
|
|
2
2
|
import type { GizmoStore } from './gizmos.js';
|
|
3
|
-
export declare const OVERLAYS: readonly ['bounds', 'lights', 'cameras', 'cascades', 'normals', 'axes', 'labels'
|
|
3
|
+
export declare const OVERLAYS: readonly ['bounds', 'lights', 'cameras', 'cascades', 'normals', 'axes', 'labels',
|
|
4
|
+
/** Frame time, the top 8 spans, and the GPU frame (0074). */
|
|
5
|
+
'perf'];
|
|
4
6
|
export type Overlay = (typeof OVERLAYS)[number];
|
|
5
7
|
/** An overlay another package draws (physics colliders, the navmesh, UI rects). */
|
|
6
8
|
export interface OverlayDef {
|
package/dist/overlay-registry.js
CHANGED
package/dist/overlays.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { DirectionalLight, Lights } from "./lights.js";
|
|
16
16
|
import { defineOverlay, OVERLAYS, registeredOverlays } from "./overlay-registry.js";
|
|
17
17
|
const extraOverlays = registeredOverlays();
|
|
18
|
+
import { drawPerfOverlay } from "./perf-overlay.js";
|
|
18
19
|
import { Views } from "./plugin.js";
|
|
19
20
|
import { Cameras, cameraOf } from "./view.js";
|
|
20
21
|
import {
|
|
@@ -34,7 +35,8 @@ function scenePath(world, entity) {
|
|
|
34
35
|
return v?.path || void 0;
|
|
35
36
|
}
|
|
36
37
|
const DebugOverlays = defineResource("render/DebugOverlays", {
|
|
37
|
-
|
|
38
|
+
hostWritable: true,
|
|
39
|
+
description: "Built-in debug drawings, each drawn through Gizmos: bounds, light volumes, camera frustums, shadow cascades, normals, transform axes, entity labels, and perf (frame time, top spans, GPU frame).",
|
|
38
40
|
init: () => ({
|
|
39
41
|
bounds: false,
|
|
40
42
|
lights: false,
|
|
@@ -43,6 +45,7 @@ const DebugOverlays = defineResource("render/DebugOverlays", {
|
|
|
43
45
|
normals: false,
|
|
44
46
|
axes: false,
|
|
45
47
|
labels: false,
|
|
48
|
+
perf: false,
|
|
46
49
|
extra: {},
|
|
47
50
|
filter: { components: [], path: "" },
|
|
48
51
|
name: scenePath
|
|
@@ -226,7 +229,7 @@ const drawOverlays = defineSystem({
|
|
|
226
229
|
g.overlay.clear();
|
|
227
230
|
let extra = false;
|
|
228
231
|
for (const name in o.extra) if (o.extra[name] && extraOverlays.has(name)) extra = true;
|
|
229
|
-
if (!(o.bounds || o.lights || o.cameras || o.cascades || o.normals || o.axes || o.labels || extra))
|
|
232
|
+
if (!(o.bounds || o.lights || o.cameras || o.cascades || o.normals || o.axes || o.labels || o.perf || extra))
|
|
230
233
|
return;
|
|
231
234
|
g.beginOverlays();
|
|
232
235
|
try {
|
|
@@ -249,6 +252,7 @@ function draw(world, o, g) {
|
|
|
249
252
|
}
|
|
250
253
|
if (o.lights) drawLights(world, o, g, defs);
|
|
251
254
|
const primary = primaryCamera(world);
|
|
255
|
+
if (o.perf) drawPerfOverlay(world, g, primary);
|
|
252
256
|
if (o.cameras) {
|
|
253
257
|
for (const [entity, cam] of world.resource(Cameras)) {
|
|
254
258
|
if (entity === primary || !passes(world, o, entity, defs)) continue;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Entity, type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { GizmoStore } from './gizmos.js';
|
|
3
|
+
/** The overlay's text and colours: refreshed four times a second, so it's readable and cheap. */
|
|
4
|
+
export declare function perfOverlayLines(world: World): {
|
|
5
|
+
lines: string[];
|
|
6
|
+
colors: number[][];
|
|
7
|
+
};
|
|
8
|
+
/** Draws the perf overlay for the primary camera. Gizmo labels center on their anchor, above it. */
|
|
9
|
+
export declare function drawPerfOverlay(world: World, g: GizmoStore, camera: Entity | undefined): void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { ProfilerResource, spanStats } from "@aethervtt/shard-core";
|
|
2
|
+
import {
|
|
3
|
+
gpuPassOverlap,
|
|
4
|
+
isGpuPass,
|
|
5
|
+
measureSlices,
|
|
6
|
+
PerfBudgets,
|
|
7
|
+
PerfScenario,
|
|
8
|
+
perfBudgetState,
|
|
9
|
+
Time
|
|
10
|
+
} from "@aethervtt/shard-runtime";
|
|
11
|
+
import { PassCosts } from "./ablation.js";
|
|
12
|
+
import { LABEL_FONT, labelWidth } from "./gizmo-font.js";
|
|
13
|
+
import { Cameras } from "./view.js";
|
|
14
|
+
const LINES = 8;
|
|
15
|
+
const PASS_LINES = 4;
|
|
16
|
+
const REFRESH_S = 0.25;
|
|
17
|
+
const MARGIN = 8;
|
|
18
|
+
const COLOR = [1, 1, 1, 1];
|
|
19
|
+
const HEADER = [0.55, 0.9, 1, 1];
|
|
20
|
+
const GREY = [0.6, 0.6, 0.6, 1];
|
|
21
|
+
const RED = [1, 0.35, 0.3, 1];
|
|
22
|
+
const LABEL_OPTIONS = { depthTest: false };
|
|
23
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
24
|
+
const pad = (ms) => ms.toFixed(2).padStart(6);
|
|
25
|
+
const percent = (share) => `${Math.round(share * 100)}%`.padStart(4);
|
|
26
|
+
function perfOverlayLines(world) {
|
|
27
|
+
const now = world.tryResource(Time)?.elapsed ?? 0;
|
|
28
|
+
let state = states.get(world);
|
|
29
|
+
if (state && now - state.at < REFRESH_S && now >= state.at) return state;
|
|
30
|
+
const profiler = world.tryResource(ProfilerResource);
|
|
31
|
+
const lines = [];
|
|
32
|
+
const colors = [];
|
|
33
|
+
const line = (text, color) => {
|
|
34
|
+
lines.push(text);
|
|
35
|
+
colors.push(color);
|
|
36
|
+
};
|
|
37
|
+
if (profiler) {
|
|
38
|
+
const frame = spanStats(profiler, "frame");
|
|
39
|
+
const gpu = spanStats(profiler, "gpu:frame");
|
|
40
|
+
line(
|
|
41
|
+
`frame ${frame ? pad(frame.avg) : " -"} ms p95 ${frame ? pad(frame.p95) : " -"}` + (profiler.gpu.status === "unavailable" ? " gpu n/a" : ` gpu ${gpu ? pad(gpu.avg) : " -"} ms`),
|
|
42
|
+
HEADER
|
|
43
|
+
);
|
|
44
|
+
const overlapping = gpuPassOverlap(profiler)?.overlapping ?? false;
|
|
45
|
+
const spans = [];
|
|
46
|
+
const passes = [];
|
|
47
|
+
for (const name2 of profiler.names()) {
|
|
48
|
+
if (name2 === "frame" || name2 === "gpu:frame" || name2.startsWith("schedule/") || name2.startsWith("commands/"))
|
|
49
|
+
continue;
|
|
50
|
+
const entry = { name: name2, avg: profiler.timing(name2).avg };
|
|
51
|
+
if (overlapping && name2.startsWith("gpu:")) {
|
|
52
|
+
if (isGpuPass(name2)) passes.push(entry);
|
|
53
|
+
} else spans.push(entry);
|
|
54
|
+
}
|
|
55
|
+
spans.sort((a, b) => b.avg - a.avg || (a.name < b.name ? -1 : 1));
|
|
56
|
+
for (let i = 0; i < Math.min(LINES, spans.length); i++) {
|
|
57
|
+
line(`${pad(spans[i].avg)} ms ${spans[i].name}`, COLOR);
|
|
58
|
+
}
|
|
59
|
+
if (overlapping) {
|
|
60
|
+
const ablation = world.tryResource(PassCosts)?.latest;
|
|
61
|
+
if (ablation && ablation.passes.length > 0) {
|
|
62
|
+
line("gpu passes by ablation (timestamps overlap)", GREY);
|
|
63
|
+
const ranked = [...ablation.passes].sort((a, b) => b.ms - a.ms);
|
|
64
|
+
for (let i = 0; i < Math.min(PASS_LINES, ranked.length); i++) {
|
|
65
|
+
line(`${pad(ranked[i].ms)} ms gpu:${ranked[i].pass} ablated`, COLOR);
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
line("gpu passes overlapping: perf.ablate ranks them", GREY);
|
|
69
|
+
passes.sort((a, b) => a.name < b.name ? -1 : 1);
|
|
70
|
+
for (let i = 0; i < Math.min(PASS_LINES, passes.length); i++) {
|
|
71
|
+
line(`${pad(passes[i].avg)} ms ${passes[i].name} overlapping`, GREY);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const name = world.tryResource(PerfScenario)?.name;
|
|
76
|
+
const scenario = name ? world.tryResource(PerfBudgets)?.budgets.scenarios[name] : void 0;
|
|
77
|
+
if (name && scenario) {
|
|
78
|
+
const machine = perfBudgetState(world)?.detection.machine ?? null;
|
|
79
|
+
line(`scenario ${name}${machine ? ` on ${machine}` : ""}: share of frame / budget`, HEADER);
|
|
80
|
+
for (const s of measureSlices(profiler, scenario, machine).slices) {
|
|
81
|
+
const measured = s.measuredShare === null ? " -" : percent(s.measuredShare);
|
|
82
|
+
line(
|
|
83
|
+
`${s.track} ${measured} / ${percent(s.share)} ${s.slice}`,
|
|
84
|
+
s.verdict === "over" ? RED : s.verdict === "unmeasured" ? GREY : COLOR
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
state = { at: now, lines, colors };
|
|
90
|
+
states.set(world, state);
|
|
91
|
+
return state;
|
|
92
|
+
}
|
|
93
|
+
const point = new Float32Array(3);
|
|
94
|
+
function unproject(inv, width, height, px, py) {
|
|
95
|
+
const x = px / width * 2 - 1;
|
|
96
|
+
const y = 1 - py / height * 2;
|
|
97
|
+
const z = 0.5;
|
|
98
|
+
const w = inv[3] * x + inv[7] * y + inv[11] * z + inv[15];
|
|
99
|
+
point[0] = (inv[0] * x + inv[4] * y + inv[8] * z + inv[12]) / w;
|
|
100
|
+
point[1] = (inv[1] * x + inv[5] * y + inv[9] * z + inv[13]) / w;
|
|
101
|
+
point[2] = (inv[2] * x + inv[6] * y + inv[10] * z + inv[14]) / w;
|
|
102
|
+
return point;
|
|
103
|
+
}
|
|
104
|
+
function drawPerfOverlay(world, g, camera) {
|
|
105
|
+
if (camera === void 0) return;
|
|
106
|
+
const cam = world.resource(Cameras).get(camera);
|
|
107
|
+
if (!cam || cam.width <= 0 || cam.height <= 0) return;
|
|
108
|
+
const { lines, colors } = perfOverlayLines(world);
|
|
109
|
+
const step = LABEL_FONT.cellHeight + 4;
|
|
110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
111
|
+
const text = lines[i];
|
|
112
|
+
const px = MARGIN + labelWidth(text) / 2;
|
|
113
|
+
const py = MARGIN + (i + 1) * step;
|
|
114
|
+
g.label(
|
|
115
|
+
unproject(cam.invViewProj, cam.width, cam.height, px, py),
|
|
116
|
+
text,
|
|
117
|
+
colors[i],
|
|
118
|
+
LABEL_OPTIONS
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export {
|
|
123
|
+
drawPerfOverlay,
|
|
124
|
+
perfOverlayLines
|
|
125
|
+
};
|
package/dist/picking.d.ts
CHANGED
|
@@ -12,7 +12,22 @@ export interface PickHit {
|
|
|
12
12
|
normal: [number, number, number];
|
|
13
13
|
/** Meters from the camera (from the ray origin, for raycasts). */
|
|
14
14
|
distance: number;
|
|
15
|
+
/**
|
|
16
|
+
* What within the entity was hit, when something knows (0059): a tilemap gives
|
|
17
|
+
* `{ kind: 'tile', layer, x, y, tile }`, the tile named as its palette names it.
|
|
18
|
+
*/
|
|
19
|
+
detail?: PickDetail;
|
|
15
20
|
}
|
|
21
|
+
/** Detail of a pick within an entity: `kind` says what the rest means. */
|
|
22
|
+
export interface PickDetail {
|
|
23
|
+
kind: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Turns a hit into detail about what within the entity it hit (a tile of a tilemap), or
|
|
28
|
+
* undefined when the hit isn't its kind. Runs for GPU picks and raycasts alike.
|
|
29
|
+
*/
|
|
30
|
+
export type PickDetailer = (world: World, hit: PickHit) => PickDetail | undefined;
|
|
16
31
|
interface PickRequest {
|
|
17
32
|
x: number;
|
|
18
33
|
y: number;
|
|
@@ -35,6 +50,8 @@ export interface PickingState {
|
|
|
35
50
|
pending: Map<string, PickRequest[]>;
|
|
36
51
|
drawers: Map<string, PickDrawer>;
|
|
37
52
|
blockers: Map<string, PickBlocker>;
|
|
53
|
+
/** Detail for hits, by who adds it (tilemaps). */
|
|
54
|
+
detailers: Map<string, PickDetailer>;
|
|
38
55
|
/** Views whose pick pass skipped a draw (a pipeline still compiling): their picks wait a frame. */
|
|
39
56
|
incomplete: Set<string>;
|
|
40
57
|
}
|
package/dist/picking.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { defineResource, mat4, ShardError } from "@aethervtt/shard-core";
|
|
2
2
|
import { GlobalTransform } from "@aethervtt/shard-transform";
|
|
3
|
-
import { Meshes } from "./assets.js";
|
|
4
|
-
import {
|
|
3
|
+
import { Materials, Meshes } from "./assets.js";
|
|
4
|
+
import { addRenderFeatures } from "./features.js";
|
|
5
|
+
import { drawMaterials, ForwardStateResource, PASS_PICK, PASS_PICK_GROUND } from "./forward.js";
|
|
5
6
|
import { RenderPhase } from "./graph.js";
|
|
6
|
-
import { Lod, Mesh3d } from "./instances.js";
|
|
7
|
+
import { Lod, Mesh3d, MeshMaterial } from "./instances.js";
|
|
7
8
|
import { DebugOverlays } from "./overlays.js";
|
|
8
9
|
import { Graph, Views } from "./plugin.js";
|
|
9
10
|
import { cameraOf } from "./view.js";
|
|
@@ -14,6 +15,7 @@ const Picking = defineResource("render/Picking", {
|
|
|
14
15
|
pending: /* @__PURE__ */ new Map(),
|
|
15
16
|
drawers: /* @__PURE__ */ new Map(),
|
|
16
17
|
blockers: /* @__PURE__ */ new Map(),
|
|
18
|
+
detailers: /* @__PURE__ */ new Map(),
|
|
17
19
|
incomplete: /* @__PURE__ */ new Set()
|
|
18
20
|
})
|
|
19
21
|
});
|
|
@@ -69,6 +71,8 @@ function pickNode(world) {
|
|
|
69
71
|
drawMaterials(ctx, state, pv, cam, cam.draws, PASS_PICK);
|
|
70
72
|
if (cam.deferred) drawMaterials(ctx, state, pv, cam, cam.forwardOnly, PASS_PICK);
|
|
71
73
|
drawMaterials(ctx, state, pv, cam, cam.transparent, PASS_PICK);
|
|
74
|
+
drawMaterials(ctx, state, pv, cam, cam.ground, PASS_PICK_GROUND);
|
|
75
|
+
drawMaterials(ctx, state, pv, cam, cam.overlay, PASS_PICK_GROUND);
|
|
72
76
|
for (const draw of picking.drawers.values()) draw(ctx, cam);
|
|
73
77
|
if (ctx.gpu.pipelines.skipped > skipped) picking.incomplete.add(ctx.view.name);
|
|
74
78
|
}
|
|
@@ -147,13 +151,15 @@ function pickReadbackNode(world) {
|
|
|
147
151
|
const dx = position[0] - eye[0];
|
|
148
152
|
const dy = position[1] - eye[1];
|
|
149
153
|
const dz = position[2] - eye[2];
|
|
150
|
-
r.resolve(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
r.resolve(
|
|
155
|
+
withDetail(w, {
|
|
156
|
+
entity,
|
|
157
|
+
path: w.isAlive(entity) ? name(w, entity) : void 0,
|
|
158
|
+
position,
|
|
159
|
+
normal: [n[0], n[1], n[2]],
|
|
160
|
+
distance: Math.sqrt(dx * dx + dy * dy + dz * dz)
|
|
161
|
+
})
|
|
162
|
+
);
|
|
157
163
|
});
|
|
158
164
|
buffer.unmap();
|
|
159
165
|
buffer.destroy();
|
|
@@ -173,6 +179,12 @@ function addPickNodes(world) {
|
|
|
173
179
|
graph.declare({ name: "pick-id", format: "r32uint", usage });
|
|
174
180
|
graph.declare({ name: "pick-normal", format: "rgba32float", usage });
|
|
175
181
|
graph.declare({ name: "pick-depth", format: "depth32float" });
|
|
182
|
+
addRenderFeatures(world, {
|
|
183
|
+
name: "render/picking",
|
|
184
|
+
description: "Entity picking from id and normal targets, read back per request.",
|
|
185
|
+
nodes: ["picking", "picking/readback"],
|
|
186
|
+
baseline: { strategy: "The same targets, read back with copies" }
|
|
187
|
+
});
|
|
176
188
|
graph.addNode("picking", pickNode(world));
|
|
177
189
|
graph.addNode("picking/readback", pickReadbackNode(world));
|
|
178
190
|
}
|
|
@@ -235,11 +247,14 @@ function worldBox(bb, m, out) {
|
|
|
235
247
|
}
|
|
236
248
|
function buildBvh(world, tick, tables) {
|
|
237
249
|
const items = [];
|
|
250
|
+
const materials = world.tryResource(Materials);
|
|
238
251
|
for (const table of world.query({ with: [Mesh3d, GlobalTransform, ComputedVisibility] }).tables) {
|
|
239
252
|
const g = table.column(GlobalTransform, "matrix");
|
|
240
253
|
const vis = table.column(ComputedVisibility, "visible");
|
|
254
|
+
const refs = table.has(MeshMaterial) ? table.column(MeshMaterial, "material") : void 0;
|
|
241
255
|
for (let i = 0; i < table.count; i++) {
|
|
242
256
|
if (!vis[i]) continue;
|
|
257
|
+
if (refs && materials?.get(refs[i])?.type.pickable === false) continue;
|
|
243
258
|
const entity = table.entities[i];
|
|
244
259
|
const mesh = meshOf(world, entity);
|
|
245
260
|
if (!mesh) continue;
|
|
@@ -442,7 +457,21 @@ function raycast(world, origin, direction, options = {}) {
|
|
|
442
457
|
}
|
|
443
458
|
}
|
|
444
459
|
hits.sort((a, b) => a.distance - b.distance);
|
|
445
|
-
|
|
460
|
+
const out = options.all ? hits : hits.slice(0, 1);
|
|
461
|
+
for (const hit of out) withDetail(world, hit);
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
function withDetail(world, hit) {
|
|
465
|
+
const detailers = world.tryResource(Picking)?.detailers;
|
|
466
|
+
if (!detailers || detailers.size === 0 || !world.isAlive(hit.entity)) return hit;
|
|
467
|
+
for (const detailer of detailers.values()) {
|
|
468
|
+
const detail = detailer(world, hit);
|
|
469
|
+
if (detail) {
|
|
470
|
+
hit.detail = detail;
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return hit;
|
|
446
475
|
}
|
|
447
476
|
export {
|
|
448
477
|
BvhResource,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { definePlugin } from "@aethervtt/shard-runtime";
|
|
2
|
+
import { addRenderFeatures } from "./features.js";
|
|
2
3
|
import { RenderPhase } from "./graph.js";
|
|
3
4
|
import { PixelPerfectPath } from "./pixel-perfect.js";
|
|
4
5
|
import { Graph, Shaders } from "./plugin.js";
|
|
@@ -6,7 +7,7 @@ import { registerShaders } from "./shaders.js";
|
|
|
6
7
|
const PIXEL_PERFECT_SHADERS = {
|
|
7
8
|
"shard::post::pixel_upscale": `
|
|
8
9
|
struct Upscale {
|
|
9
|
-
/** offset (xy), scale, 0. */
|
|
10
|
+
/** offset (xy), scale, the bars' alpha (0 for a camera clearing to alpha, 0052). */
|
|
10
11
|
params: vec4f,
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -17,7 +18,7 @@ struct Upscale {
|
|
|
17
18
|
@fragment fn fs(@builtin(position) frag: vec4f) -> @location(0) vec4f {
|
|
18
19
|
let p = (frag.xy - upscale.params.xy) / upscale.params.z;
|
|
19
20
|
let size = vec2f(textureDimensions(source));
|
|
20
|
-
if (any(p < vec2f(0.0)) || any(p >= size)) { return vec4f(0.0, 0.0, 0.0,
|
|
21
|
+
if (any(p < vec2f(0.0)) || any(p >= size)) { return vec4f(0.0, 0.0, 0.0, upscale.params.w); }
|
|
21
22
|
return textureLoad(source, vec2i(floor(p)), 0);
|
|
22
23
|
}`
|
|
23
24
|
};
|
|
@@ -92,6 +93,7 @@ function pixelUpscaleNode() {
|
|
|
92
93
|
params[0] = layout.offsetX;
|
|
93
94
|
params[1] = layout.offsetY;
|
|
94
95
|
params[2] = layout.scale;
|
|
96
|
+
params[3] = ctx.view.data.alphaOutput === true ? 0 : 1;
|
|
95
97
|
gpu.device.queue.writeBuffer(buffer, 0, params);
|
|
96
98
|
const source = layout.target.texture();
|
|
97
99
|
let cached = groups.get(ctx.view.name);
|
|
@@ -125,6 +127,12 @@ const pixelPerfectPlugin = definePlugin({
|
|
|
125
127
|
},
|
|
126
128
|
ready(app) {
|
|
127
129
|
registerShaders(app.world.resource(Shaders), PIXEL_PERFECT_SHADERS);
|
|
130
|
+
addRenderFeatures(app.world, {
|
|
131
|
+
name: "render/pixel-perfect",
|
|
132
|
+
description: "Pixel-art cameras: low-resolution render, integer upscale.",
|
|
133
|
+
nodes: ["pixel-upscale"],
|
|
134
|
+
baseline: { strategy: "The same fragment pass" }
|
|
135
|
+
});
|
|
128
136
|
app.world.resource(Graph).addNode("pixel-upscale", pixelUpscaleNode());
|
|
129
137
|
}
|
|
130
138
|
});
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type World } from '@aethervtt/shard-core';
|
|
2
|
-
import { type GpuContext } from '@aethervtt/shard-gpu';
|
|
2
|
+
import { type GpuBackendChoice, type GpuContext, type Surface, type SurfaceAlpha, type Webgl2ContextOptions } from '@aethervtt/shard-gpu';
|
|
3
3
|
import { type Plugin } from '@aethervtt/shard-runtime';
|
|
4
4
|
import { type ShaderBake, ShaderLibrary } from '@aethervtt/shard-shader';
|
|
5
5
|
import { type CapturedBuffer, type CapturedImage, RenderGraph, type RenderView } from './graph.js';
|
|
6
|
-
import {
|
|
6
|
+
import type { RenderTarget } from './target.js';
|
|
7
7
|
export declare const Gpu: import("@aethervtt/shard-core").ResourceDef<GpuContext>;
|
|
8
8
|
export declare const Graph: import("@aethervtt/shard-core").ResourceDef<RenderGraph>;
|
|
9
9
|
export declare const Shaders: import("@aethervtt/shard-core").ResourceDef<ShaderLibrary>;
|
|
@@ -29,16 +29,43 @@ export declare const RenderSet: {
|
|
|
29
29
|
readonly Graph: import("@aethervtt/shard-core").SystemSetDef;
|
|
30
30
|
};
|
|
31
31
|
export interface RenderPluginOptions {
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Render into this canvas: the plugin adds it as a surface of `gpu` (or of a device it makes).
|
|
34
|
+
* Omit for headless (views use offscreen targets).
|
|
35
|
+
*/
|
|
33
36
|
canvas?: HTMLCanvasElement | OffscreenCanvas;
|
|
34
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* With `canvas`: 'premultiplied' composites the canvas over the page, where cameras clearing to
|
|
39
|
+
* alpha 0 show what's under it (0052). Default 'opaque'.
|
|
40
|
+
*/
|
|
41
|
+
alpha?: SurfaceAlpha;
|
|
42
|
+
/** Render into a surface already on `gpu` (`gpu.addSurface`). Disposing the app removes it. */
|
|
43
|
+
surface?: Surface;
|
|
44
|
+
/**
|
|
45
|
+
* An existing GPU context (e.g. Dawn in Node, or one shared with other apps). Otherwise one is
|
|
46
|
+
* created in `ready` and destroyed with the app.
|
|
47
|
+
*/
|
|
35
48
|
gpu?: GpuContext;
|
|
49
|
+
/** The name this app's GPU objects count under in `gpu.stats(owner)`. Default `render:<n>`. */
|
|
50
|
+
owner?: string;
|
|
36
51
|
/**
|
|
37
52
|
* Headless stand-in for the window: cameras without a target render here. Used by the CLI for
|
|
38
|
-
* screenshots. Ignored when there's a canvas.
|
|
53
|
+
* screenshots. Ignored when there's a canvas or surface.
|
|
39
54
|
*/
|
|
40
55
|
target?: RenderTarget;
|
|
41
56
|
features?: GPUFeatureName[];
|
|
57
|
+
/**
|
|
58
|
+
* The graphics API, when the plugin makes the device (0064). 'auto' (default): WebGPU where it
|
|
59
|
+
* works, else WebGL2 on the baseline tier. 'webgpu' or 'webgl2' insist on one.
|
|
60
|
+
*/
|
|
61
|
+
backend?: GpuBackendChoice;
|
|
62
|
+
/** WebGL2 settings, for tests and tools (the 'minimum' profile, GL error checks). */
|
|
63
|
+
webgl2?: Webgl2ContextOptions;
|
|
64
|
+
/**
|
|
65
|
+
* 'baseline' runs the baseline tier even where the full one would (on WebGPU, a
|
|
66
|
+
* compatibility-mode device), when the plugin makes the device.
|
|
67
|
+
*/
|
|
68
|
+
tier?: 'baseline';
|
|
42
69
|
/** An existing shader library to share (e.g. a preview rendering the game's own shaders). */
|
|
43
70
|
shaders?: ShaderLibrary;
|
|
44
71
|
/**
|
|
@@ -57,8 +84,15 @@ export interface RenderPluginOptions {
|
|
|
57
84
|
* so the renderer's description grows with what's installed.
|
|
58
85
|
*/
|
|
59
86
|
export declare const RenderDescribers: import("@aethervtt/shard-core").ResourceDef<Map<string, (world: World) => unknown>>;
|
|
60
|
-
export
|
|
87
|
+
export interface RenderOptionsValue {
|
|
88
|
+
windowView: boolean;
|
|
89
|
+
/** What this app's GPU objects count under in `gpu.stats` (0052). */
|
|
90
|
+
owner: string;
|
|
91
|
+
}
|
|
92
|
+
export declare const RenderOptions: import("@aethervtt/shard-core").ResourceDef<RenderOptionsValue>;
|
|
61
93
|
export declare function renderPlugin(options?: RenderPluginOptions): Plugin;
|
|
94
|
+
/** The name this app's GPU objects count under in `gpu.stats` (0052). */
|
|
95
|
+
export declare function renderOwner(world: World): string;
|
|
62
96
|
/** A screenshot of a view (default: the window) after the next frame renders it. */
|
|
63
97
|
export declare function captureView(world: World, view?: string): Promise<CapturedImage>;
|
|
64
98
|
/**
|
|
@@ -67,6 +101,11 @@ export declare function captureView(world: World, view?: string): Promise<Captur
|
|
|
67
101
|
* perspective, fog, bloom…), `depth`, G-buffer channels, effect buffers.
|
|
68
102
|
*/
|
|
69
103
|
export declare function captureBuffer(world: World, view: string, buffer: string): Promise<CapturedBuffer>;
|
|
104
|
+
/** Every buffer and texture on the device by category: the ledger's total (0055, 0074). */
|
|
105
|
+
export declare function gpuMemory(world: World): {
|
|
106
|
+
bytes: number;
|
|
107
|
+
byCategory: Record<string, number>;
|
|
108
|
+
};
|
|
70
109
|
/** What the renderer is doing: graph order, culled nodes, views, pending pipelines. For agents. */
|
|
71
110
|
export declare function describeRender(world: World): {
|
|
72
111
|
order: {
|
|
@@ -100,7 +139,12 @@ export declare function describeRender(world: World): {
|
|
|
100
139
|
taaResets: number;
|
|
101
140
|
originShifts: number;
|
|
102
141
|
};
|
|
103
|
-
memory:
|
|
142
|
+
memory: {
|
|
143
|
+
textures: number;
|
|
144
|
+
textureBytes: number;
|
|
145
|
+
bytes: number;
|
|
146
|
+
byCategory: Record<string, number>;
|
|
147
|
+
};
|
|
104
148
|
recentErrors: {
|
|
105
149
|
code: string;
|
|
106
150
|
message: string;
|
|
@@ -108,4 +152,22 @@ export declare function describeRender(world: World): {
|
|
|
108
152
|
path?: string;
|
|
109
153
|
details?: unknown[];
|
|
110
154
|
}[];
|
|
155
|
+
features: {
|
|
156
|
+
name: string;
|
|
157
|
+
nodes: string[];
|
|
158
|
+
baseline: string;
|
|
159
|
+
}[];
|
|
160
|
+
health: import("./health.js").RenderHealthValue;
|
|
161
|
+
backend: import("@aethervtt/shard-gpu").GpuBackendName;
|
|
162
|
+
tier: import("@aethervtt/shard-gpu").GpuTier;
|
|
163
|
+
capabilities: import("@aethervtt/shard-gpu").GpuCapabilities;
|
|
164
|
+
shaderCache: import("@aethervtt/shard-gpu").ShaderCacheStats | undefined;
|
|
165
|
+
limits: {
|
|
166
|
+
maxTextureDimension2D: number;
|
|
167
|
+
maxColorAttachments: number;
|
|
168
|
+
maxSampledTexturesPerShaderStage: number;
|
|
169
|
+
maxInterStageShaderVariables: number;
|
|
170
|
+
maxUniformBufferBindingSize: number;
|
|
171
|
+
};
|
|
172
|
+
reasons: readonly import("@aethervtt/shard-gpu").GraphicsReason[];
|
|
111
173
|
} & Record<string, unknown>;
|