@aethervtt/shard-render 0.0.1 → 0.1.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/assets.d.ts +3 -0
- package/dist/assets.js +43 -2
- package/dist/atmosphere-nodes.d.ts +9 -0
- package/dist/atmosphere-nodes.js +57 -18
- package/dist/atmosphere-plugin.js +8 -2
- 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 +120 -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/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/deferred-shaders.js +2 -0
- package/dist/deferred.js +24 -3
- 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 +15 -4
- 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/forward.d.ts +38 -2
- package/dist/forward.js +582 -184
- 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 +11 -0
- package/dist/graph.js +30 -5
- package/dist/health.d.ts +58 -0
- package/dist/health.js +112 -0
- package/dist/index.d.ts +30 -11
- package/dist/index.js +286 -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/overlays.js +1 -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 +58 -6
- package/dist/plugin.js +191 -24
- package/dist/post-common.d.ts +2 -0
- package/dist/post-common.js +3 -0
- package/dist/post-nodes.d.ts +2 -1
- package/dist/post-nodes.js +111 -19
- package/dist/post-plugin.js +5 -3
- package/dist/post-shaders.js +51 -6
- 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/screen-effects.d.ts +97 -0
- package/dist/screen-effects.js +192 -0
- package/dist/shaders.d.ts +13 -0
- package/dist/shaders.js +131 -29
- package/dist/shadow-catcher.d.ts +9 -0
- package/dist/shadow-catcher.js +133 -0
- package/dist/shadows.d.ts +25 -1
- package/dist/shadows.js +97 -19
- package/dist/standard-fields.d.ts +1 -0
- package/dist/standard-fields.js +5 -0
- package/dist/standard.js +12 -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 +10 -0
- package/dist/testing.js +33 -1
- package/dist/tier.d.ts +13 -0
- package/dist/tier.js +14 -0
- package/dist/timer.d.ts +2 -0
- package/dist/timer.js +7 -1
- package/dist/variants.d.ts +37 -0
- package/dist/variants.js +104 -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 +15 -11
package/dist/target.js
CHANGED
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
class WindowTarget {
|
|
2
|
-
label = "window";
|
|
3
|
-
renderScale = true;
|
|
4
|
-
gpu;
|
|
5
|
-
constructor(gpu) {
|
|
6
|
-
this.gpu = gpu;
|
|
7
|
-
}
|
|
8
|
-
get format() {
|
|
9
|
-
return this.gpu.format;
|
|
10
|
-
}
|
|
11
|
-
get width() {
|
|
12
|
-
return this.gpu.canvas?.width ?? 1;
|
|
13
|
-
}
|
|
14
|
-
get pixelRatio() {
|
|
15
|
-
return this.gpu.pixelRatio;
|
|
16
|
-
}
|
|
17
|
-
get height() {
|
|
18
|
-
return this.gpu.canvas?.height ?? 1;
|
|
19
|
-
}
|
|
20
|
-
texture() {
|
|
21
|
-
this.gpu.resize();
|
|
22
|
-
return this.gpu.context.getCurrentTexture();
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
1
|
class OffscreenTarget {
|
|
26
2
|
label;
|
|
27
3
|
format;
|
|
@@ -65,6 +41,5 @@ class OffscreenTarget {
|
|
|
65
41
|
}
|
|
66
42
|
}
|
|
67
43
|
export {
|
|
68
|
-
OffscreenTarget
|
|
69
|
-
WindowTarget
|
|
44
|
+
OffscreenTarget
|
|
70
45
|
};
|
package/dist/testing.d.ts
CHANGED
|
@@ -32,3 +32,13 @@ export declare function compareGolden(dir: string, name: string, image: {
|
|
|
32
32
|
height: number;
|
|
33
33
|
data: Uint8Array;
|
|
34
34
|
}): GoldenResult;
|
|
35
|
+
/**
|
|
36
|
+
* Watches a device for what the baseline tier (0064) must never ask for: compute passes, and
|
|
37
|
+
* storage buffers visible to the vertex or fragment stage. Counts from now on.
|
|
38
|
+
*/
|
|
39
|
+
export declare function watchBaseline(gpu: import('@aethervtt/shard-gpu').GpuContext): {
|
|
40
|
+
computePasses: number;
|
|
41
|
+
renderStorage: string[];
|
|
42
|
+
};
|
|
43
|
+
/** Mean absolute difference of two images' bytes. */
|
|
44
|
+
export declare function meanDifference(a: ArrayLike<number>, b: ArrayLike<number>): number;
|
package/dist/testing.js
CHANGED
|
@@ -116,11 +116,43 @@ function compareGolden(dir, name, image) {
|
|
|
116
116
|
}
|
|
117
117
|
return { mean: sum / expected.length, max, written: false };
|
|
118
118
|
}
|
|
119
|
+
function watchBaseline(gpu) {
|
|
120
|
+
const found = { computePasses: 0, renderStorage: [] };
|
|
121
|
+
const device = gpu.device;
|
|
122
|
+
const createEncoder = device.createCommandEncoder.bind(device);
|
|
123
|
+
device.createCommandEncoder = (descriptor) => {
|
|
124
|
+
const encoder = createEncoder(descriptor);
|
|
125
|
+
const begin = encoder.beginComputePass.bind(encoder);
|
|
126
|
+
encoder.beginComputePass = (d) => {
|
|
127
|
+
found.computePasses++;
|
|
128
|
+
return begin(d);
|
|
129
|
+
};
|
|
130
|
+
return encoder;
|
|
131
|
+
};
|
|
132
|
+
const createLayout = device.createBindGroupLayout.bind(device);
|
|
133
|
+
device.createBindGroupLayout = (descriptor) => {
|
|
134
|
+
for (const e of descriptor.entries) {
|
|
135
|
+
const render = e.visibility & (GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT);
|
|
136
|
+
const t = e.buffer?.type;
|
|
137
|
+
if (render && (t === "storage" || t === "read-only-storage"))
|
|
138
|
+
found.renderStorage.push(`${descriptor.label ?? "?"}#${e.binding}`);
|
|
139
|
+
}
|
|
140
|
+
return createLayout(descriptor);
|
|
141
|
+
};
|
|
142
|
+
return found;
|
|
143
|
+
}
|
|
144
|
+
function meanDifference(a, b) {
|
|
145
|
+
let sum = 0;
|
|
146
|
+
for (let i = 0; i < a.length; i++) sum += Math.abs(a[i] - b[i]);
|
|
147
|
+
return sum / a.length;
|
|
148
|
+
}
|
|
119
149
|
export {
|
|
120
150
|
compareGolden,
|
|
121
151
|
comparePng,
|
|
152
|
+
meanDifference,
|
|
122
153
|
pixel,
|
|
123
154
|
pngBytes,
|
|
124
155
|
renderView,
|
|
125
|
-
settle
|
|
156
|
+
settle,
|
|
157
|
+
watchBaseline
|
|
126
158
|
};
|
package/dist/tier.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GpuContext } from '@aethervtt/shard-gpu';
|
|
2
|
+
/**
|
|
3
|
+
* A texture's binding dimension, fixed at creation on baseline (compatibility mode and WebGL2):
|
|
4
|
+
* spread into the texture descriptor. Nothing on the full tier.
|
|
5
|
+
*/
|
|
6
|
+
export declare function bindingDimension(gpu: GpuContext, dimension: GPUTextureViewDimension): {
|
|
7
|
+
textureBindingViewDimension?: GPUTextureViewDimension;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The layout entry for a depth texture read without comparison. On baseline the shader rewrite
|
|
11
|
+
* reads it as a float texture (naga can't load from depth), so it binds as unfilterable float.
|
|
12
|
+
*/
|
|
13
|
+
export declare function depthReadEntry(gpu: GpuContext, binding: number, visibility: GPUShaderStageFlags, viewDimension?: GPUTextureViewDimension): GPUBindGroupLayoutEntry;
|
package/dist/tier.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function bindingDimension(gpu, dimension) {
|
|
2
|
+
return gpu.tier === "baseline" ? { textureBindingViewDimension: dimension } : {};
|
|
3
|
+
}
|
|
4
|
+
function depthReadEntry(gpu, binding, visibility, viewDimension = "2d") {
|
|
5
|
+
return {
|
|
6
|
+
binding,
|
|
7
|
+
visibility,
|
|
8
|
+
texture: gpu.tier === "baseline" ? { sampleType: "unfilterable-float", viewDimension } : viewDimension === "2d" ? { sampleType: "depth" } : { sampleType: "depth", viewDimension }
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
bindingDimension,
|
|
13
|
+
depthReadEntry
|
|
14
|
+
};
|
package/dist/timer.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export declare class GpuTimer {
|
|
|
18
18
|
private generation;
|
|
19
19
|
private readonly gpu;
|
|
20
20
|
constructor(gpu: GpuContext);
|
|
21
|
+
/** Destroys the query set; buffers are released with the rest of the app's (0052). */
|
|
22
|
+
destroy(): void;
|
|
21
23
|
beginFrame(): void;
|
|
22
24
|
/** Timestamp writes for the next pass, or undefined when timing is off or full. */
|
|
23
25
|
allocate(name: string): GPURenderPassTimestampWrites | undefined;
|
package/dist/timer.js
CHANGED
|
@@ -22,6 +22,12 @@ class GpuTimer {
|
|
|
22
22
|
this.gpu = gpu;
|
|
23
23
|
this.enabled = gpu.features.has("timestamp-query");
|
|
24
24
|
}
|
|
25
|
+
/** Destroys the query set; buffers are released with the rest of the app's (0052). */
|
|
26
|
+
destroy() {
|
|
27
|
+
this.querySet?.destroy();
|
|
28
|
+
this.querySet = void 0;
|
|
29
|
+
this.generation = this.gpu.generation;
|
|
30
|
+
}
|
|
25
31
|
beginFrame() {
|
|
26
32
|
this.names = [];
|
|
27
33
|
if (!this.enabled || this.generation === this.gpu.generation) return;
|
|
@@ -117,7 +123,7 @@ class GpuTimer {
|
|
|
117
123
|
}
|
|
118
124
|
if (b > a) profiler?.record(label, Number(b - a) / 1e6);
|
|
119
125
|
}
|
|
120
|
-
if (
|
|
126
|
+
if (first !== 0n) {
|
|
121
127
|
this.frameMs = Number(last - first) / 1e6;
|
|
122
128
|
this.frameSamples++;
|
|
123
129
|
profiler?.record("gpu:frame", this.frameMs);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type Entity, type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { App } from '@aethervtt/shard-runtime';
|
|
3
|
+
/** A kind of manifest entry, and how to show the variants it names. */
|
|
4
|
+
export interface ShaderVariantSource {
|
|
5
|
+
/** The entry's key: `{ "dice": … }` is run by the source named `dice`. */
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly description: string;
|
|
8
|
+
/**
|
|
9
|
+
* Shows every variant `entry` names for at least a frame, stepping the app until their pipelines
|
|
10
|
+
* are made (`app.update`, then `Shaders.whenIdle` and `gpu.pipelines.whenIdle`). Returns how many
|
|
11
|
+
* variants it showed.
|
|
12
|
+
*/
|
|
13
|
+
show(app: App, entry: unknown): Promise<number>;
|
|
14
|
+
}
|
|
15
|
+
export declare const ShaderVariantSources: import("@aethervtt/shard-core").ResourceDef<Map<string, ShaderVariantSource>>;
|
|
16
|
+
/** Registers a manifest entry kind. Plugins call it in `build`. */
|
|
17
|
+
export declare function addShaderVariantSource(world: World, source: ShaderVariantSource): void;
|
|
18
|
+
/** The manifest's shape. */
|
|
19
|
+
export interface ShaderVariantManifest {
|
|
20
|
+
variants: Record<string, unknown>[];
|
|
21
|
+
}
|
|
22
|
+
/** Runs one manifest entry through its source. Throws `render/unknown-variant-source` for a key no plugin registered. */
|
|
23
|
+
export declare function showVariants(app: App, entry: Record<string, unknown>): Promise<number>;
|
|
24
|
+
/** A material asset's path (or guid) and parameter overrides. */
|
|
25
|
+
export interface MaterialVariantEntry {
|
|
26
|
+
path?: string;
|
|
27
|
+
guid?: string;
|
|
28
|
+
params?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Shows what `spawn` makes to a camera of its own (an offscreen target, a shadowed sun), stepping
|
|
32
|
+
* the app until every pipeline it asked for is made, then takes it all away again. For variant
|
|
33
|
+
* sources: what's drawn is what a bake records.
|
|
34
|
+
*/
|
|
35
|
+
export declare function showToCamera(app: App, spawn: (at: [number, number, number]) => Entity[]): Promise<void>;
|
|
36
|
+
/** `{ "material": { "path": …, "params": … } }`: a material asset, with its parameters set, on a cube. */
|
|
37
|
+
export declare const materialVariants: ShaderVariantSource;
|
package/dist/variants.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { assetServer } from "@aethervtt/shard-assets";
|
|
2
|
+
import {
|
|
3
|
+
defineResource,
|
|
4
|
+
ShardError
|
|
5
|
+
} from "@aethervtt/shard-core";
|
|
6
|
+
import { cube } from "@aethervtt/shard-mesh";
|
|
7
|
+
import { lookAt, Transform } from "@aethervtt/shard-transform";
|
|
8
|
+
import { Materials, Meshes, RenderTargets } from "./assets.js";
|
|
9
|
+
import { Camera3d } from "./camera.js";
|
|
10
|
+
import { Mesh3d, MeshMaterial } from "./instances.js";
|
|
11
|
+
import { DirectionalLight } from "./lights.js";
|
|
12
|
+
import { Gpu, Shaders } from "./plugin.js";
|
|
13
|
+
import { OffscreenTarget } from "./target.js";
|
|
14
|
+
const ShaderVariantSources = defineResource(
|
|
15
|
+
"render/ShaderVariantSources",
|
|
16
|
+
{
|
|
17
|
+
description: "Kinds of shaders.variants.json entries (0064) and how to show their variants, for `shard shaders bake`.",
|
|
18
|
+
init: () => /* @__PURE__ */ new Map()
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
function addShaderVariantSource(world, source) {
|
|
22
|
+
world.initResource(ShaderVariantSources).set(source.name, source);
|
|
23
|
+
}
|
|
24
|
+
function showVariants(app, entry) {
|
|
25
|
+
const keys = Object.keys(entry);
|
|
26
|
+
if (keys.length !== 1) {
|
|
27
|
+
throw new ShardError(
|
|
28
|
+
"render/variant-entry",
|
|
29
|
+
`A shaders.variants.json entry has one key, its source: got ${keys.length ? keys.join(", ") : "none"}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const name = keys[0];
|
|
33
|
+
const source = app.world.tryResource(ShaderVariantSources)?.get(name);
|
|
34
|
+
if (!source) {
|
|
35
|
+
const known = [...app.world.tryResource(ShaderVariantSources)?.keys() ?? []];
|
|
36
|
+
throw new ShardError("render/unknown-variant-source", `No shader variant source "${name}"`, {
|
|
37
|
+
hint: `Known: ${known.join(", ") || "none"}. Plugins register theirs with addShaderVariantSource.`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return source.show(app, entry[name]);
|
|
41
|
+
}
|
|
42
|
+
const STAGE = [5e4, 0, 5e4];
|
|
43
|
+
async function showToCamera(app, spawn) {
|
|
44
|
+
const world = app.world;
|
|
45
|
+
const gpu = world.resource(Gpu);
|
|
46
|
+
const target = new OffscreenTarget(gpu, { label: "shader-variants", width: 64, height: 64 });
|
|
47
|
+
const ref = world.resource(RenderTargets).add(target, `render:variants/${STAGE.join(",")}`);
|
|
48
|
+
const eye = [STAGE[0], STAGE[1] + 2, STAGE[2] + 4];
|
|
49
|
+
const made = [
|
|
50
|
+
world.spawn(
|
|
51
|
+
[Camera3d, { target: ref, order: 90 }],
|
|
52
|
+
[Transform, { translation: eye, rotation: lookAt(eye, STAGE) }]
|
|
53
|
+
),
|
|
54
|
+
world.spawn(
|
|
55
|
+
[DirectionalLight, { shadows: true }],
|
|
56
|
+
[Transform, { translation: STAGE, rotation: lookAt([0, 0, 0], [-0.4, -1, -0.3]) }]
|
|
57
|
+
),
|
|
58
|
+
...spawn(STAGE)
|
|
59
|
+
];
|
|
60
|
+
try {
|
|
61
|
+
for (let i = 0; i < 8; i++) {
|
|
62
|
+
app.update(1 / 60);
|
|
63
|
+
await world.resource(Shaders).whenIdle();
|
|
64
|
+
await gpu.pipelines.whenIdle();
|
|
65
|
+
}
|
|
66
|
+
} finally {
|
|
67
|
+
for (const e of made) if (world.isAlive(e)) world.despawn(e);
|
|
68
|
+
app.update(1 / 60);
|
|
69
|
+
target.destroy?.();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const materialVariants = {
|
|
73
|
+
name: "material",
|
|
74
|
+
description: "A material asset by path or guid, with parameter overrides: { path, params? }.",
|
|
75
|
+
async show(app, entry) {
|
|
76
|
+
const list = Array.isArray(entry) ? entry : [entry];
|
|
77
|
+
const world = app.world;
|
|
78
|
+
for (const m of list) {
|
|
79
|
+
const ref = m.guid ? { guid: m.guid } : { path: m.path };
|
|
80
|
+
await assetServer(world).load(ref);
|
|
81
|
+
const material = world.resource(Materials).get(ref);
|
|
82
|
+
if (!material) {
|
|
83
|
+
throw new ShardError("render/variant-material", `No material ${m.guid ?? m.path}`);
|
|
84
|
+
}
|
|
85
|
+
if (m.params) material.set(m.params);
|
|
86
|
+
const mesh = world.resource(Meshes).add(cube({ size: 1 }));
|
|
87
|
+
await showToCamera(app, (at) => [
|
|
88
|
+
world.spawn(
|
|
89
|
+
[Mesh3d, { mesh }],
|
|
90
|
+
[MeshMaterial, { material: ref }],
|
|
91
|
+
[Transform, { translation: at }]
|
|
92
|
+
)
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
return list.length;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
export {
|
|
99
|
+
ShaderVariantSources,
|
|
100
|
+
addShaderVariantSource,
|
|
101
|
+
materialVariants,
|
|
102
|
+
showToCamera,
|
|
103
|
+
showVariants
|
|
104
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type Entity, type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { GpuBuffer } from '@aethervtt/shard-gpu';
|
|
3
|
+
import { type HiddenSet } from './view-visibility.js';
|
|
4
|
+
/**
|
|
5
|
+
* Every camera's hidden set, one range of words each. The GPU culler reads them from a region of
|
|
6
|
+
* its LOD state buffer; only words that changed upload there.
|
|
7
|
+
*/
|
|
8
|
+
export declare class HiddenSets {
|
|
9
|
+
/** Words per set: one bit per instance slot of the store's capacity. */
|
|
10
|
+
words: number;
|
|
11
|
+
data: Uint32Array<ArrayBuffer>;
|
|
12
|
+
readonly sets: Map<number, HiddenSet>;
|
|
13
|
+
/** Free set indices, reused before growing. */
|
|
14
|
+
private readonly free;
|
|
15
|
+
private count;
|
|
16
|
+
private dirty;
|
|
17
|
+
private dirtyLo;
|
|
18
|
+
private dirtyHi;
|
|
19
|
+
/** Every word uploads next time (a new layout, buffer, region or device). */
|
|
20
|
+
private all;
|
|
21
|
+
/** Where the last upload went: a buffer version (it bumps when replaced) and byte offset. */
|
|
22
|
+
private target;
|
|
23
|
+
private targetVersion;
|
|
24
|
+
private targetOffset;
|
|
25
|
+
/** Bytes uploaded by the last `upload`. */
|
|
26
|
+
uploadedBytes: number;
|
|
27
|
+
/** Resolves since start (tests: a still frame resolves nothing). */
|
|
28
|
+
resolves: number;
|
|
29
|
+
/** The set of `camera`, made on first use. */
|
|
30
|
+
setOf(camera: Entity): HiddenSet;
|
|
31
|
+
/** Frees a set: its words clear (and upload, so a reused index starts empty). */
|
|
32
|
+
drop(set: HiddenSet): void;
|
|
33
|
+
/** Makes room for `capacity` slots per set; a new width re-lays every set and uploads all. */
|
|
34
|
+
ensureSlots(capacity: number): void;
|
|
35
|
+
private layout;
|
|
36
|
+
private write;
|
|
37
|
+
/**
|
|
38
|
+
* Writes a set's new bits (from `next`, `words` long), marking only words that differ. Returns
|
|
39
|
+
* whether any did.
|
|
40
|
+
*/
|
|
41
|
+
assign(set: HiddenSet, next: Uint32Array): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Uploads changed words in runs to `buffer` at `byteOffset`; everything when that's not where
|
|
44
|
+
* the last upload went, or the buffer was replaced (grown, or a new device).
|
|
45
|
+
*/
|
|
46
|
+
upload(buffer: GpuBuffer, byteOffset: number): number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolves each camera's ViewVisibility into its hidden set, after the instance slots are current.
|
|
50
|
+
* Lists resolve again only when they change, a slot is allocated or freed, or a ChildOf changes.
|
|
51
|
+
*/
|
|
52
|
+
export declare const resolveViewVisibility: import("@aethervtt/shard-core").SystemDef<{
|
|
53
|
+
q: import("@aethervtt/shard-core").Query;
|
|
54
|
+
hierarchy: import("@aethervtt/shard-core").Query;
|
|
55
|
+
epoch: number;
|
|
56
|
+
frame: number;
|
|
57
|
+
}>;
|
|
58
|
+
/** render.describe's viewVisibility section: per camera, what it hides and the slots it resolves to. */
|
|
59
|
+
export declare function describeViewVisibility(world: World): Record<string, unknown>;
|
|
60
|
+
/** Per-view hiding (0070): cameras' ViewVisibility lists, resolved for their culls. */
|
|
61
|
+
export declare const viewVisibilityPlugin: import("@aethervtt/shard-runtime").Plugin;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChildOf,
|
|
3
|
+
Children,
|
|
4
|
+
defineSystem,
|
|
5
|
+
Last
|
|
6
|
+
} from "@aethervtt/shard-core";
|
|
7
|
+
import { definePlugin } from "@aethervtt/shard-runtime";
|
|
8
|
+
import { Camera3d } from "./camera.js";
|
|
9
|
+
import { addRenderFeatures } from "./features.js";
|
|
10
|
+
import { InstanceSlot, Instances, prepareInstances } from "./instances.js";
|
|
11
|
+
import { RenderDescribers, RenderSet } from "./plugin.js";
|
|
12
|
+
import { Cameras } from "./view.js";
|
|
13
|
+
import { HiddenSetsResource, ViewVisibility } from "./view-visibility.js";
|
|
14
|
+
class HiddenSets {
|
|
15
|
+
/** Words per set: one bit per instance slot of the store's capacity. */
|
|
16
|
+
words = 0;
|
|
17
|
+
data = new Uint32Array(0);
|
|
18
|
+
sets = /* @__PURE__ */ new Map();
|
|
19
|
+
/** Free set indices, reused before growing. */
|
|
20
|
+
free = [];
|
|
21
|
+
count = 0;
|
|
22
|
+
dirty = new Uint8Array(0);
|
|
23
|
+
dirtyLo = Number.POSITIVE_INFINITY;
|
|
24
|
+
dirtyHi = -1;
|
|
25
|
+
/** Every word uploads next time (a new layout, buffer, region or device). */
|
|
26
|
+
all = true;
|
|
27
|
+
/** Where the last upload went: a buffer version (it bumps when replaced) and byte offset. */
|
|
28
|
+
target;
|
|
29
|
+
targetVersion = -1;
|
|
30
|
+
targetOffset = -1;
|
|
31
|
+
/** Bytes uploaded by the last `upload`. */
|
|
32
|
+
uploadedBytes = 0;
|
|
33
|
+
/** Resolves since start (tests: a still frame resolves nothing). */
|
|
34
|
+
resolves = 0;
|
|
35
|
+
/** The set of `camera`, made on first use. */
|
|
36
|
+
setOf(camera) {
|
|
37
|
+
let set = this.sets.get(camera);
|
|
38
|
+
if (set) return set;
|
|
39
|
+
const index = this.free.pop() ?? this.count++;
|
|
40
|
+
if (index * this.words + this.words > this.data.length) this.layout(this.words, index + 1);
|
|
41
|
+
const base = index * this.words;
|
|
42
|
+
set = {
|
|
43
|
+
camera,
|
|
44
|
+
base,
|
|
45
|
+
bits: this.data.subarray(base, base + this.words),
|
|
46
|
+
slots: 0,
|
|
47
|
+
hide: [],
|
|
48
|
+
shadows: false,
|
|
49
|
+
frame: -1
|
|
50
|
+
};
|
|
51
|
+
this.sets.set(camera, set);
|
|
52
|
+
return set;
|
|
53
|
+
}
|
|
54
|
+
/** Frees a set: its words clear (and upload, so a reused index starts empty). */
|
|
55
|
+
drop(set) {
|
|
56
|
+
for (let w = 0; w < set.bits.length; w++) if (set.bits[w] !== 0) this.write(set.base + w, 0);
|
|
57
|
+
this.sets.delete(set.camera);
|
|
58
|
+
this.free.push(set.base / Math.max(1, this.words));
|
|
59
|
+
}
|
|
60
|
+
/** Makes room for `capacity` slots per set; a new width re-lays every set and uploads all. */
|
|
61
|
+
ensureSlots(capacity) {
|
|
62
|
+
const words = Math.ceil(capacity / 32);
|
|
63
|
+
if (words > this.words) this.layout(words, this.count);
|
|
64
|
+
}
|
|
65
|
+
layout(words, sets) {
|
|
66
|
+
const data = new Uint32Array(words * Math.max(1, sets * 2));
|
|
67
|
+
for (const set of this.sets.values()) {
|
|
68
|
+
const index = set.base / Math.max(1, this.words);
|
|
69
|
+
data.set(set.bits, index * words);
|
|
70
|
+
set.base = index * words;
|
|
71
|
+
}
|
|
72
|
+
this.data = data;
|
|
73
|
+
for (const set of this.sets.values()) set.bits = data.subarray(set.base, set.base + words);
|
|
74
|
+
this.words = words;
|
|
75
|
+
this.dirty = new Uint8Array(data.length);
|
|
76
|
+
this.all = true;
|
|
77
|
+
}
|
|
78
|
+
write(word, value) {
|
|
79
|
+
if (this.data[word] === value) return;
|
|
80
|
+
this.data[word] = value;
|
|
81
|
+
if (this.dirty[word]) return;
|
|
82
|
+
this.dirty[word] = 1;
|
|
83
|
+
if (word < this.dirtyLo) this.dirtyLo = word;
|
|
84
|
+
if (word > this.dirtyHi) this.dirtyHi = word;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Writes a set's new bits (from `next`, `words` long), marking only words that differ. Returns
|
|
88
|
+
* whether any did.
|
|
89
|
+
*/
|
|
90
|
+
assign(set, next2) {
|
|
91
|
+
let changed = false;
|
|
92
|
+
for (let w = 0; w < set.bits.length; w++) {
|
|
93
|
+
const value = next2[w];
|
|
94
|
+
if (set.bits[w] !== value) {
|
|
95
|
+
this.write(set.base + w, value);
|
|
96
|
+
changed = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return changed;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Uploads changed words in runs to `buffer` at `byteOffset`; everything when that's not where
|
|
103
|
+
* the last upload went, or the buffer was replaced (grown, or a new device).
|
|
104
|
+
*/
|
|
105
|
+
upload(buffer, byteOffset) {
|
|
106
|
+
buffer.ensureCapacity(byteOffset + this.data.byteLength);
|
|
107
|
+
void buffer.buffer;
|
|
108
|
+
if (buffer !== this.target || buffer.version !== this.targetVersion || byteOffset !== this.targetOffset) {
|
|
109
|
+
this.target = buffer;
|
|
110
|
+
this.targetVersion = buffer.version;
|
|
111
|
+
this.targetOffset = byteOffset;
|
|
112
|
+
this.all = true;
|
|
113
|
+
}
|
|
114
|
+
let bytes = 0;
|
|
115
|
+
if (this.all) {
|
|
116
|
+
this.all = false;
|
|
117
|
+
this.dirty.fill(0);
|
|
118
|
+
this.dirtyLo = Number.POSITIVE_INFINITY;
|
|
119
|
+
this.dirtyHi = -1;
|
|
120
|
+
if (this.data.length === 0) return 0;
|
|
121
|
+
buffer.write(this.data, byteOffset);
|
|
122
|
+
this.uploadedBytes = this.data.byteLength;
|
|
123
|
+
return this.uploadedBytes;
|
|
124
|
+
}
|
|
125
|
+
let start = -1;
|
|
126
|
+
for (let w = this.dirtyLo; w <= this.dirtyHi + 1; w++) {
|
|
127
|
+
if (w <= this.dirtyHi && this.dirty[w] === 1) {
|
|
128
|
+
this.dirty[w] = 0;
|
|
129
|
+
if (start < 0) start = w;
|
|
130
|
+
} else if (start >= 0) {
|
|
131
|
+
buffer.write(this.data, byteOffset + start * 4, start, w - start);
|
|
132
|
+
bytes += (w - start) * 4;
|
|
133
|
+
start = -1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
this.dirtyLo = Number.POSITIVE_INFINITY;
|
|
137
|
+
this.dirtyHi = -1;
|
|
138
|
+
this.uploadedBytes = bytes;
|
|
139
|
+
return bytes;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
let next = new Uint32Array(0);
|
|
143
|
+
const stack = [];
|
|
144
|
+
function resolve(world, roots, out) {
|
|
145
|
+
out.fill(0);
|
|
146
|
+
let slots = 0;
|
|
147
|
+
stack.length = 0;
|
|
148
|
+
for (const e of roots) if (e !== null && e !== void 0) stack.push(e);
|
|
149
|
+
while (stack.length > 0) {
|
|
150
|
+
const e = stack.pop();
|
|
151
|
+
if (!world.isAlive(e)) continue;
|
|
152
|
+
const table = world.entityTable(e);
|
|
153
|
+
const row = world.entityRow(e);
|
|
154
|
+
if (table.has(InstanceSlot)) {
|
|
155
|
+
const slot = table.column(InstanceSlot, "slot")[row] - 1;
|
|
156
|
+
if (slot >= 0 && (out[slot >>> 5] & 1 << (slot & 31)) === 0) {
|
|
157
|
+
out[slot >>> 5] = out[slot >>> 5] | 1 << (slot & 31);
|
|
158
|
+
slots++;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (table.has(Children)) {
|
|
162
|
+
const children = table.column(Children, "entities")[row];
|
|
163
|
+
if (children) {
|
|
164
|
+
for (const c of children) if (c !== null) stack.push(c);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return slots;
|
|
169
|
+
}
|
|
170
|
+
const resolveViewVisibility = defineSystem({
|
|
171
|
+
name: "render/resolve-view-visibility",
|
|
172
|
+
description: "Resolves cameras' ViewVisibility lists to hidden slot bitsets (0070).",
|
|
173
|
+
setup: (world) => ({
|
|
174
|
+
q: world.query({ with: [Camera3d, ViewVisibility] }),
|
|
175
|
+
hierarchy: world.query({ with: [ChildOf] }),
|
|
176
|
+
epoch: -1,
|
|
177
|
+
frame: 0
|
|
178
|
+
}),
|
|
179
|
+
run: (state, world, ctx) => {
|
|
180
|
+
const sets = world.tryResource(HiddenSetsResource);
|
|
181
|
+
if (!sets) return;
|
|
182
|
+
const cameras = world.resource(Cameras);
|
|
183
|
+
const store = world.resource(Instances);
|
|
184
|
+
const frame = ++state.frame;
|
|
185
|
+
let listed = 0;
|
|
186
|
+
for (const table of state.q.tables) listed += table.count;
|
|
187
|
+
if (listed === 0 && sets.sets.size === 0) return;
|
|
188
|
+
sets.ensureSlots(store.capacity);
|
|
189
|
+
if (next.length < sets.words) next = new Uint32Array(sets.words);
|
|
190
|
+
const since = ctx.lastRunTick;
|
|
191
|
+
let moved = store.slotEpoch !== state.epoch;
|
|
192
|
+
state.epoch = store.slotEpoch;
|
|
193
|
+
if (!moved) {
|
|
194
|
+
for (const table of state.hierarchy.tables) {
|
|
195
|
+
if (table.lastStructural > since || table.lastChanged(ChildOf) > since) {
|
|
196
|
+
moved = true;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const table of state.q.tables) {
|
|
202
|
+
const hide = table.column(ViewVisibility, "hide");
|
|
203
|
+
const shadows = table.column(ViewVisibility, "shadows");
|
|
204
|
+
const changed = table.changedTicks(ViewVisibility);
|
|
205
|
+
for (let i = 0; i < table.count; i++) {
|
|
206
|
+
const camera = table.entities[i];
|
|
207
|
+
const fresh = !sets.sets.has(camera);
|
|
208
|
+
const set = sets.setOf(camera);
|
|
209
|
+
set.frame = frame;
|
|
210
|
+
const cam = cameras.get(camera);
|
|
211
|
+
if (cam) cam.hidden = set;
|
|
212
|
+
const hideShadows = shadows[i] === 1;
|
|
213
|
+
if (!fresh && !moved && changed[i] <= since && set.shadows === hideShadows) continue;
|
|
214
|
+
const list = hide[i] ?? [];
|
|
215
|
+
const words = next.subarray(0, sets.words);
|
|
216
|
+
set.slots = resolve(world, list, words);
|
|
217
|
+
set.hide = list.filter((e) => e !== null);
|
|
218
|
+
sets.resolves++;
|
|
219
|
+
if (sets.assign(set, words) && hideShadows || set.shadows !== hideShadows)
|
|
220
|
+
store.shadowEpoch++;
|
|
221
|
+
set.shadows = hideShadows;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (sets.sets.size > listed) {
|
|
225
|
+
for (const set of sets.sets.values()) {
|
|
226
|
+
if (set.frame === frame) continue;
|
|
227
|
+
const cam = cameras.get(set.camera);
|
|
228
|
+
if (cam?.hidden === set) cam.hidden = void 0;
|
|
229
|
+
if (set.shadows) store.shadowEpoch++;
|
|
230
|
+
sets.drop(set);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
function describeViewVisibility(world) {
|
|
236
|
+
const out = {};
|
|
237
|
+
for (const set of world.tryResource(HiddenSetsResource)?.sets.values() ?? [])
|
|
238
|
+
out[`camera:${set.camera}`] = {
|
|
239
|
+
hide: [...set.hide],
|
|
240
|
+
slots: set.slots,
|
|
241
|
+
shadows: set.shadows ? "hide" : "keep"
|
|
242
|
+
};
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
const viewVisibilityPlugin = definePlugin({
|
|
246
|
+
name: "render/view-visibility",
|
|
247
|
+
dependencies: ["render/forward"],
|
|
248
|
+
build(app) {
|
|
249
|
+
app.insertResource(HiddenSetsResource, new HiddenSets());
|
|
250
|
+
app.addSystems(Last, resolveViewVisibility.inSet(RenderSet.Prepare).after(prepareInstances));
|
|
251
|
+
},
|
|
252
|
+
ready(app) {
|
|
253
|
+
app.world.initResource(RenderDescribers).set("viewVisibility", describeViewVisibility);
|
|
254
|
+
addRenderFeatures(app.world, {
|
|
255
|
+
name: "render/view-visibility",
|
|
256
|
+
description: "Cameras' hidden lists (ViewVisibility).",
|
|
257
|
+
nodes: [],
|
|
258
|
+
baseline: { strategy: "CPU culls read the same bits" }
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
export {
|
|
263
|
+
HiddenSets,
|
|
264
|
+
describeViewVisibility,
|
|
265
|
+
resolveViewVisibility,
|
|
266
|
+
viewVisibilityPlugin
|
|
267
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Entity, type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { HiddenSets } from './view-visibility-plugin.js';
|
|
3
|
+
export declare const VIEW_SHADOWS: readonly ['keep', 'hide'];
|
|
4
|
+
export declare const ViewVisibility: import("@aethervtt/shard-core").ComponentDef<{
|
|
5
|
+
readonly hide: import("@aethervtt/shard-core").FieldType<(number | null)[], "object">;
|
|
6
|
+
readonly shadows: import("@aethervtt/shard-core").FieldType<"hide" | "keep", "u8">;
|
|
7
|
+
}>;
|
|
8
|
+
/** `hiddenBase` of a view with no hidden set. */
|
|
9
|
+
export declare const NO_HIDDEN = 4294967295;
|
|
10
|
+
/** One camera's hidden set: a bit per instance slot, in its range of `HiddenSets.data`. */
|
|
11
|
+
export interface HiddenSet {
|
|
12
|
+
camera: Entity;
|
|
13
|
+
/** Its first word in `HiddenSets.data` (and the GPU buffer). */
|
|
14
|
+
base: number;
|
|
15
|
+
/** Its words: bit `s & 31` of word `s >> 5` is slot `s`. */
|
|
16
|
+
bits: Uint32Array;
|
|
17
|
+
/** Slots it resolves to. */
|
|
18
|
+
slots: number;
|
|
19
|
+
/** The entities listed, as last resolved. */
|
|
20
|
+
hide: Entity[];
|
|
21
|
+
/** Its camera's cascades leave the slots out too. */
|
|
22
|
+
shadows: boolean;
|
|
23
|
+
/** The frame it was last seen on (sets of cameras that lose the component are freed). */
|
|
24
|
+
frame: number;
|
|
25
|
+
}
|
|
26
|
+
export declare const HiddenSetsResource: import("@aethervtt/shard-core").ResourceDef<HiddenSets>;
|
|
27
|
+
/** Warns once per world about a ViewVisibility without its plugin (render/feature-missing). */
|
|
28
|
+
export declare function observeViewVisibilityWithoutPlugin(world: World): void;
|