@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
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { mat4 } from "@aethervtt/shard-core";
|
|
2
|
+
import { Cameras } from "./view.js";
|
|
3
|
+
const scratchInv = mat4.create();
|
|
4
|
+
const scratchKey = mat4.create();
|
|
5
|
+
let invValid = false;
|
|
6
|
+
function inverse(viewProj) {
|
|
7
|
+
let same = invValid;
|
|
8
|
+
for (let i = 0; same && i < 16; i++) if (scratchKey[i] !== viewProj[i]) same = false;
|
|
9
|
+
if (!same) {
|
|
10
|
+
mat4.copy(scratchKey, viewProj);
|
|
11
|
+
invValid = mat4.invert(scratchInv, viewProj) !== null;
|
|
12
|
+
}
|
|
13
|
+
return scratchInv;
|
|
14
|
+
}
|
|
15
|
+
function worldToScreen(world, camera, point, out) {
|
|
16
|
+
const cam = world.tryResource(Cameras)?.get(camera);
|
|
17
|
+
if (!cam) return false;
|
|
18
|
+
const m = cam.viewProjNoJitter;
|
|
19
|
+
const x = point[0];
|
|
20
|
+
const y = point[1];
|
|
21
|
+
const z = point[2];
|
|
22
|
+
const cx = m[0] * x + m[4] * y + m[8] * z + m[12];
|
|
23
|
+
const cy = m[1] * x + m[5] * y + m[9] * z + m[13];
|
|
24
|
+
const cw = m[3] * x + m[7] * y + m[11] * z + m[15];
|
|
25
|
+
if (cw <= 1e-9) return false;
|
|
26
|
+
const width = cam.displayWidth / cam.pixelRatio;
|
|
27
|
+
const height = cam.displayHeight / cam.pixelRatio;
|
|
28
|
+
out[0] = (cx / cw * 0.5 + 0.5) * width;
|
|
29
|
+
out[1] = (0.5 - cy / cw * 0.5) * height;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
function screenToRay(world, camera, x, y, outOrigin, outDir) {
|
|
33
|
+
const cam = world.tryResource(Cameras)?.get(camera);
|
|
34
|
+
if (!cam) {
|
|
35
|
+
outOrigin[0] = outOrigin[1] = outOrigin[2] = 0;
|
|
36
|
+
outDir[0] = outDir[1] = 0;
|
|
37
|
+
outDir[2] = -1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const inv = inverse(cam.viewProjNoJitter);
|
|
41
|
+
const nx = x / (cam.displayWidth / cam.pixelRatio) * 2 - 1;
|
|
42
|
+
const ny = 1 - y / (cam.displayHeight / cam.pixelRatio) * 2;
|
|
43
|
+
let px = 0;
|
|
44
|
+
let py = 0;
|
|
45
|
+
let pz = 0;
|
|
46
|
+
for (let k = 0; k < 2; k++) {
|
|
47
|
+
const d = k === 0 ? 1 : 0.5;
|
|
48
|
+
const wx = inv[0] * nx + inv[4] * ny + inv[8] * d + inv[12];
|
|
49
|
+
const wy = inv[1] * nx + inv[5] * ny + inv[9] * d + inv[13];
|
|
50
|
+
const wz = inv[2] * nx + inv[6] * ny + inv[10] * d + inv[14];
|
|
51
|
+
const ww = inv[3] * nx + inv[7] * ny + inv[11] * d + inv[15];
|
|
52
|
+
if (k === 0) {
|
|
53
|
+
px = wx / ww;
|
|
54
|
+
py = wy / ww;
|
|
55
|
+
pz = wz / ww;
|
|
56
|
+
} else {
|
|
57
|
+
const dx = wx / ww - px;
|
|
58
|
+
const dy = wy / ww - py;
|
|
59
|
+
const dz = wz / ww - pz;
|
|
60
|
+
const len = Math.sqrt(dx * dx + dy * dy + dz * dz) || 1;
|
|
61
|
+
outDir[0] = dx / len;
|
|
62
|
+
outDir[1] = dy / len;
|
|
63
|
+
outDir[2] = dz / len;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
outOrigin[0] = px;
|
|
67
|
+
outOrigin[1] = py;
|
|
68
|
+
outOrigin[2] = pz;
|
|
69
|
+
}
|
|
70
|
+
const rayOrigin = new Float64Array(3);
|
|
71
|
+
const rayDir = new Float64Array(3);
|
|
72
|
+
function screenToPlane(world, camera, x, y, planeY, out) {
|
|
73
|
+
screenToRay(world, camera, x, y, rayOrigin, rayDir);
|
|
74
|
+
const dy = rayDir[1];
|
|
75
|
+
if (Math.abs(dy) < 1e-9) return false;
|
|
76
|
+
const t = (planeY - rayOrigin[1]) / dy;
|
|
77
|
+
if (t < 0) return false;
|
|
78
|
+
out[0] = rayOrigin[0] + rayDir[0] * t;
|
|
79
|
+
out[1] = planeY;
|
|
80
|
+
out[2] = rayOrigin[2] + rayDir[2] * t;
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
screenToPlane,
|
|
85
|
+
screenToRay,
|
|
86
|
+
worldToScreen
|
|
87
|
+
};
|
package/dist/readback.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Readback helpers: bytes per texel and conversion of any readable format to floats. */
|
|
2
2
|
export declare const BYTES_PER_TEXEL: Partial<Record<GPUTextureFormat, number>>;
|
|
3
|
+
export declare function halfToFloat(h: number): number;
|
|
3
4
|
/** Unpacks a readback row of any supported format into 4 floats per pixel. */
|
|
4
5
|
export declare function toFloats(src: ArrayBuffer, format: GPUTextureFormat, count: number, out: Float32Array, o: number): void;
|
package/dist/readback.js
CHANGED
package/dist/render-scale.js
CHANGED
|
@@ -2,6 +2,7 @@ import { defineResource } from "@aethervtt/shard-core";
|
|
|
2
2
|
import { DisplayRate } from "@aethervtt/shard-runtime";
|
|
3
3
|
import { Views } from "./plugin.js";
|
|
4
4
|
const RenderScale = defineResource("render/RenderScale", {
|
|
5
|
+
hostWritable: true,
|
|
5
6
|
description: "Render resolution of window cameras (0051): the scene renders at scale \xD7 the window size and is upscaled, UI stays native. auto moves the scale between min and max to hold the frame budget: targetMs, or one display refresh (DisplayRate) when it is 0.",
|
|
6
7
|
init: () => ({
|
|
7
8
|
mode: "auto",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { defineResource, defineSystem, Last, PostUpdate } from "@aethervtt/shard-core";
|
|
2
|
+
import { definePlugin, FrameDemand } from "@aethervtt/shard-runtime";
|
|
3
|
+
import { OriginShift, TransformSystems } from "@aethervtt/shard-transform";
|
|
4
|
+
import { addRenderFeatures } from "./features.js";
|
|
5
|
+
import { Graph, RenderDescribers, RenderSet, Shaders } from "./plugin.js";
|
|
6
|
+
import { describeRibbons, Ribbons, shiftRibbons, updateRibbons } from "./ribbons.js";
|
|
7
|
+
import { registerShaders } from "./shaders.js";
|
|
8
|
+
import { RibbonsPath } from "./vfx.js";
|
|
9
|
+
import { VFX_SHADERS, VfxRenderer } from "./vfx-draw.js";
|
|
10
|
+
import { computeVisibility } from "./visibility.js";
|
|
11
|
+
const RibbonGpu = defineResource("render/RibbonGpu", {
|
|
12
|
+
init: () => ({})
|
|
13
|
+
});
|
|
14
|
+
const prune = defineSystem({
|
|
15
|
+
name: "render/ribbons-prune",
|
|
16
|
+
run: (_, world) => world.resource(RibbonGpu).renderer?.prune(world)
|
|
17
|
+
});
|
|
18
|
+
const ribbonsPlugin = definePlugin({
|
|
19
|
+
name: "render/ribbons",
|
|
20
|
+
dependencies: ["render/forward"],
|
|
21
|
+
provides: [Ribbons, RibbonGpu, updateRibbons],
|
|
22
|
+
build(app) {
|
|
23
|
+
const w = app.world;
|
|
24
|
+
w.initResource(RibbonsPath);
|
|
25
|
+
w.initResource(Ribbons);
|
|
26
|
+
w.initResource(RibbonGpu);
|
|
27
|
+
w.observe(
|
|
28
|
+
OriginShift,
|
|
29
|
+
({ data }) => shiftRibbons(w, data.offset[0], data.offset[1], data.offset[2])
|
|
30
|
+
);
|
|
31
|
+
app.addSystems(PostUpdate, updateRibbons.after(TransformSystems, computeVisibility));
|
|
32
|
+
app.addSystems(Last, prune.inSet(RenderSet.Prepare));
|
|
33
|
+
},
|
|
34
|
+
ready(app) {
|
|
35
|
+
const w = app.world, renderer = new VfxRenderer(w.resource(Ribbons).records, false);
|
|
36
|
+
w.resource(RibbonGpu).renderer = renderer;
|
|
37
|
+
registerShaders(w.resource(Shaders), VFX_SHADERS);
|
|
38
|
+
addRenderFeatures(w, {
|
|
39
|
+
name: "render/ribbons",
|
|
40
|
+
description: "Bounded trails and beams.",
|
|
41
|
+
nodes: ["ribbons"],
|
|
42
|
+
baseline: { strategy: "The same vertex attributes, uniform buffers and fragment passes" }
|
|
43
|
+
});
|
|
44
|
+
w.resource(Graph).addNode("ribbons", renderer.node());
|
|
45
|
+
w.initResource(RenderDescribers).set("ribbons", describeRibbons);
|
|
46
|
+
},
|
|
47
|
+
dispose(app) {
|
|
48
|
+
app.world.resource(RibbonGpu).renderer?.dispose();
|
|
49
|
+
const s = app.world.resource(Ribbons);
|
|
50
|
+
s.records.length = 0;
|
|
51
|
+
s.trails.clear();
|
|
52
|
+
s.beams.clear();
|
|
53
|
+
app.world.tryResource(FrameDemand)?.set("render/ribbons", false);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
export {
|
|
57
|
+
RibbonGpu,
|
|
58
|
+
ribbonsPlugin
|
|
59
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type World } from '@aethervtt/shard-core';
|
|
2
|
+
import { type VfxRecord } from './vfx-state.js';
|
|
3
|
+
export interface RibbonRecord extends VfxRecord {
|
|
4
|
+
points: Float64Array;
|
|
5
|
+
capacity: number;
|
|
6
|
+
head: number;
|
|
7
|
+
pointsCount: number;
|
|
8
|
+
distance: number;
|
|
9
|
+
targetMatrix: Float64Array;
|
|
10
|
+
scratch: Float64Array;
|
|
11
|
+
}
|
|
12
|
+
export declare const Ribbons: import("@aethervtt/shard-core").ResourceDef<{
|
|
13
|
+
records: RibbonRecord[];
|
|
14
|
+
trails: Map<number, RibbonRecord>;
|
|
15
|
+
beams: Map<number, RibbonRecord>;
|
|
16
|
+
frame: number;
|
|
17
|
+
rejected: number;
|
|
18
|
+
}>;
|
|
19
|
+
/** Geometry is camera-independent; the vertex shader extrudes shared point tangents per view. */
|
|
20
|
+
export declare function ribbonGeometry(r: RibbonRecord): void;
|
|
21
|
+
export declare const updateRibbons: import("@aethervtt/shard-core").SystemDef<{
|
|
22
|
+
trails: import("@aethervtt/shard-core").Query;
|
|
23
|
+
beams: import("@aethervtt/shard-core").Query;
|
|
24
|
+
}>;
|
|
25
|
+
export declare function shiftRibbons(world: World, x: number, y: number, z: number): void;
|
|
26
|
+
export declare function describeRibbons(world: World): {
|
|
27
|
+
rejected: number;
|
|
28
|
+
error: string | null;
|
|
29
|
+
records: {
|
|
30
|
+
entity: number;
|
|
31
|
+
kind: "beam" | "decal" | "trail";
|
|
32
|
+
vertices: number;
|
|
33
|
+
age: number;
|
|
34
|
+
opacity: number;
|
|
35
|
+
error: string | null;
|
|
36
|
+
visible: boolean;
|
|
37
|
+
texture: string;
|
|
38
|
+
playing: boolean;
|
|
39
|
+
paused: boolean;
|
|
40
|
+
cpuBytes: number;
|
|
41
|
+
points: number;
|
|
42
|
+
capacity: number;
|
|
43
|
+
}[];
|
|
44
|
+
};
|
package/dist/ribbons.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { defineResource, defineSystem } from "@aethervtt/shard-core";
|
|
2
|
+
import { FrameDemand, Time } from "@aethervtt/shard-runtime";
|
|
3
|
+
import { GlobalTransform } from "@aethervtt/shard-transform";
|
|
4
|
+
import { Beam, Trail } from "./vfx.js";
|
|
5
|
+
import {
|
|
6
|
+
createRecord,
|
|
7
|
+
matrixChanged,
|
|
8
|
+
number,
|
|
9
|
+
point,
|
|
10
|
+
recordDescription,
|
|
11
|
+
refreshRecord,
|
|
12
|
+
VFX_STRIDE
|
|
13
|
+
} from "./vfx-state.js";
|
|
14
|
+
const Ribbons = defineResource("render/Ribbons", {
|
|
15
|
+
init: () => ({
|
|
16
|
+
records: [],
|
|
17
|
+
trails: /* @__PURE__ */ new Map(),
|
|
18
|
+
beams: /* @__PURE__ */ new Map(),
|
|
19
|
+
frame: 0,
|
|
20
|
+
rejected: 0
|
|
21
|
+
})
|
|
22
|
+
});
|
|
23
|
+
function create(world, entity, beam2) {
|
|
24
|
+
const capacity = beam2 ? 256 : world.get(entity, Trail).capacity;
|
|
25
|
+
return {
|
|
26
|
+
...createRecord(
|
|
27
|
+
world,
|
|
28
|
+
entity,
|
|
29
|
+
beam2 ? Beam : Trail,
|
|
30
|
+
beam2 ? "beam" : "trail",
|
|
31
|
+
(capacity - 1) * 6
|
|
32
|
+
),
|
|
33
|
+
points: new Float64Array(capacity * 5),
|
|
34
|
+
capacity,
|
|
35
|
+
head: 0,
|
|
36
|
+
pointsCount: 0,
|
|
37
|
+
distance: 0,
|
|
38
|
+
targetMatrix: new Float64Array(12),
|
|
39
|
+
scratch: new Float64Array(6)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function noise(seed, index) {
|
|
43
|
+
let x = (seed ^ Math.imul(index + 1, 2654435769)) >>> 0;
|
|
44
|
+
x = Math.imul(x ^ x >>> 16, 569420461);
|
|
45
|
+
x = Math.imul(x ^ x >>> 15, 1935289751);
|
|
46
|
+
return ((x ^ x >>> 15) >>> 0) / 4294967295 * 2 - 1;
|
|
47
|
+
}
|
|
48
|
+
function trail(r, playing, paused) {
|
|
49
|
+
const life = number(r, "lifetime"), p = r.points;
|
|
50
|
+
while (r.pointsCount && r.age - p[r.head * 5 + 3] >= life) {
|
|
51
|
+
r.head = (r.head + 1) % r.capacity;
|
|
52
|
+
r.pointsCount--;
|
|
53
|
+
}
|
|
54
|
+
if (!playing || paused) return;
|
|
55
|
+
const offset = r.columns.offset, o = r.row * 3;
|
|
56
|
+
point(r.scratch, 0, r.matrix, offset[o], offset[o + 1], offset[o + 2]);
|
|
57
|
+
let distance = 0;
|
|
58
|
+
if (r.pointsCount) {
|
|
59
|
+
const last = (r.head + r.pointsCount - 1) % r.capacity * 5;
|
|
60
|
+
const dx = r.scratch[0] - p[last], dy = r.scratch[1] - p[last + 1], dz = r.scratch[2] - p[last + 2];
|
|
61
|
+
distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
62
|
+
if (distance < number(r, "minDistance")) return;
|
|
63
|
+
if (distance > number(r, "breakDistance")) {
|
|
64
|
+
r.head = 0;
|
|
65
|
+
r.pointsCount = 0;
|
|
66
|
+
r.distance = 0;
|
|
67
|
+
distance = 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (r.pointsCount === r.capacity) {
|
|
71
|
+
r.head = (r.head + 1) % r.capacity;
|
|
72
|
+
r.pointsCount--;
|
|
73
|
+
}
|
|
74
|
+
const at = (r.head + r.pointsCount++) % r.capacity * 5;
|
|
75
|
+
p[at] = r.scratch[0];
|
|
76
|
+
p[at + 1] = r.scratch[1];
|
|
77
|
+
p[at + 2] = r.scratch[2];
|
|
78
|
+
p[at + 3] = r.age;
|
|
79
|
+
p[at + 4] = r.distance += distance;
|
|
80
|
+
}
|
|
81
|
+
function beam(world, r) {
|
|
82
|
+
const authored = r.columns.points[r.row];
|
|
83
|
+
if (authored.length > 256 || authored.length === 1) {
|
|
84
|
+
r.error = "render/beam-points";
|
|
85
|
+
r.pointsCount = 0;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const p = r.points;
|
|
89
|
+
r.head = 0;
|
|
90
|
+
if (authored.length) {
|
|
91
|
+
r.pointsCount = authored.length;
|
|
92
|
+
for (let i = 0; i < authored.length; i++) {
|
|
93
|
+
const v = authored[i];
|
|
94
|
+
point(p, i * 5, r.matrix, v[0], v[1], v[2]);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
const target = number(r, "target"), start = r.columns.start, end = r.columns.end, o = r.row * 3;
|
|
98
|
+
if (target >= 0 && (!world.isAlive(target) || !world.has(target, GlobalTransform))) {
|
|
99
|
+
r.error = "render/beam-target";
|
|
100
|
+
r.pointsCount = 0;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const m = target >= 0 ? r.targetMatrix : r.matrix;
|
|
104
|
+
if (target >= 0) matrixChanged(world, target, m);
|
|
105
|
+
point(r.scratch, 0, r.matrix, start[o], start[o + 1], start[o + 2]);
|
|
106
|
+
point(r.scratch, 3, m, end[o], end[o + 1], end[o + 2]);
|
|
107
|
+
const segments = number(r, "segments"), strength = number(r, "noise"), seed = number(r, "seed");
|
|
108
|
+
r.pointsCount = segments + 1;
|
|
109
|
+
for (let i = 0; i <= segments; i++) {
|
|
110
|
+
const f = i / segments, amplitude = i === 0 || i === segments ? 0 : strength;
|
|
111
|
+
for (let k = 0; k < 3; k++)
|
|
112
|
+
p[i * 5 + k] = r.scratch[k] + (r.scratch[k + 3] - r.scratch[k]) * f + noise(seed, i * 3 + k) * amplitude;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
let distance = 0;
|
|
116
|
+
for (let i = 0; i < r.pointsCount; i++) {
|
|
117
|
+
if (i) {
|
|
118
|
+
const dx = p[i * 5] - p[(i - 1) * 5], dy = p[i * 5 + 1] - p[(i - 1) * 5 + 1], dz = p[i * 5 + 2] - p[(i - 1) * 5 + 2];
|
|
119
|
+
distance += Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
120
|
+
}
|
|
121
|
+
p[i * 5 + 4] = distance;
|
|
122
|
+
}
|
|
123
|
+
r.distance = distance;
|
|
124
|
+
}
|
|
125
|
+
function ribbonGeometry(r) {
|
|
126
|
+
r.count = 0;
|
|
127
|
+
const curve = r.columns.widthCurve[r.row];
|
|
128
|
+
if (curve.length > 16) {
|
|
129
|
+
r.error = "render/ribbon-curve";
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
for (let i = 0; i < curve.length; i++)
|
|
133
|
+
if (!Number.isFinite(curve[i].at) || !Number.isFinite(curve[i].value) || curve[i].at < 0 || curve[i].at > 1 || curve[i].value < 0 || curve[i].value > 16 || i > 0 && curve[i].at <= curve[i - 1].at) {
|
|
134
|
+
r.error = "render/ribbon-curve";
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const p = r.points, out = r.vertices, c = r.columns.color, e = r.columns.endColor;
|
|
138
|
+
const width = number(r, "width"), repeat = number(r, "repeat"), scroll = number(r, "scroll") * r.age;
|
|
139
|
+
const life = r.kind === "trail" ? number(r, "lifetime") : 1;
|
|
140
|
+
for (let segment = 0; segment < r.pointsCount - 1; segment++) {
|
|
141
|
+
const a = (r.head + segment) % r.capacity * 5, b = (r.head + segment + 1) % r.capacity * 5;
|
|
142
|
+
const dx = p[b] - p[a], dy = p[b + 1] - p[a + 1], dz = p[b + 2] - p[a + 2];
|
|
143
|
+
if (dx * dx + dy * dy + dz * dz < 1e-16) continue;
|
|
144
|
+
for (let v = 0; v < 6; v++) {
|
|
145
|
+
const next = v === 1 || v === 2 || v === 4, side = v === 2 || v === 4 || v === 5 ? 1 : -1;
|
|
146
|
+
const index = segment + (next ? 1 : 0), at = next ? b : a;
|
|
147
|
+
const prev = (r.head + Math.max(0, index - 1)) % r.capacity * 5, after = (r.head + Math.min(r.pointsCount - 1, index + 1)) % r.capacity * 5;
|
|
148
|
+
const f = Math.min(
|
|
149
|
+
1,
|
|
150
|
+
Math.max(
|
|
151
|
+
0,
|
|
152
|
+
r.kind === "trail" ? (r.age - p[at + 3]) / life : p[at + 4] / Math.max(r.distance, 1e-9)
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
let scale = 1;
|
|
156
|
+
if (curve.length) {
|
|
157
|
+
scale = curve[0].value;
|
|
158
|
+
for (let k = 1; k < curve.length; k++) {
|
|
159
|
+
const x = curve[k - 1], y = curve[k];
|
|
160
|
+
if (f >= y.at) scale = y.value;
|
|
161
|
+
else if (f > x.at) {
|
|
162
|
+
scale = x.value + (y.value - x.value) * (f - x.at) / (y.at - x.at);
|
|
163
|
+
break;
|
|
164
|
+
} else break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const o = r.count++ * VFX_STRIDE;
|
|
168
|
+
out[o] = p[at];
|
|
169
|
+
out[o + 1] = p[at + 1];
|
|
170
|
+
out[o + 2] = p[at + 2];
|
|
171
|
+
out[o + 3] = p[after] - p[prev];
|
|
172
|
+
out[o + 4] = p[after + 1] - p[prev + 1];
|
|
173
|
+
out[o + 5] = p[after + 2] - p[prev + 2];
|
|
174
|
+
out[o + 6] = p[at + 4] / repeat + scroll;
|
|
175
|
+
out[o + 7] = (side + 1) * 0.5;
|
|
176
|
+
for (let k = 0; k < 4; k++)
|
|
177
|
+
out[o + 8 + k] = c[r.row * 4 + k] + (e[r.row * 4 + k] - c[r.row * 4 + k]) * f;
|
|
178
|
+
out[o + 12] = width * scale * 0.5;
|
|
179
|
+
out[o + 13] = side;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
r.version++;
|
|
183
|
+
}
|
|
184
|
+
const updateRibbons = defineSystem({
|
|
185
|
+
name: "render/ribbons-update",
|
|
186
|
+
setup: (world) => ({
|
|
187
|
+
trails: world.query({ with: [Trail, GlobalTransform] }),
|
|
188
|
+
beams: world.query({ with: [Beam, GlobalTransform] })
|
|
189
|
+
}),
|
|
190
|
+
run({ trails, beams }, world) {
|
|
191
|
+
const state = world.resource(Ribbons), delta = world.resource(Time).delta;
|
|
192
|
+
state.frame++;
|
|
193
|
+
state.rejected = 0;
|
|
194
|
+
let demand = false;
|
|
195
|
+
for (let kind = 0; kind < 2; kind++) {
|
|
196
|
+
const q = kind ? beams : trails;
|
|
197
|
+
for (let t = 0; t < q.tables.length; t++) {
|
|
198
|
+
const table = q.tables[t];
|
|
199
|
+
for (let row = 0; row < table.count; row++) {
|
|
200
|
+
const entity = table.entities[row];
|
|
201
|
+
const limit = kind ? table.column(Beam, "segments")[row] : table.column(Trail, "capacity")[row];
|
|
202
|
+
if (limit < (kind ? 1 : 2) || limit > (kind ? 255 : 256)) {
|
|
203
|
+
state.rejected++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const lookup = kind ? state.beams : state.trails;
|
|
207
|
+
let r = lookup.get(entity);
|
|
208
|
+
if (!r) {
|
|
209
|
+
if (state.records.length >= 256) {
|
|
210
|
+
state.rejected++;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
r = create(world, entity, kind === 1);
|
|
214
|
+
state.records.push(r);
|
|
215
|
+
lookup.set(entity, r);
|
|
216
|
+
}
|
|
217
|
+
refreshRecord(world, r);
|
|
218
|
+
matrixChanged(world, entity, r.matrix);
|
|
219
|
+
r.seen = state.frame;
|
|
220
|
+
r.error = null;
|
|
221
|
+
const playing = !!number(r, "playing"), paused = !!number(r, "paused");
|
|
222
|
+
if (r.kind === "trail" && r.capacity !== number(r, "capacity")) {
|
|
223
|
+
r.capacity = number(r, "capacity");
|
|
224
|
+
r.points = new Float64Array(r.capacity * 5);
|
|
225
|
+
r.vertices = new Float32Array((r.capacity - 1) * 6 * VFX_STRIDE);
|
|
226
|
+
r.head = 0;
|
|
227
|
+
r.pointsCount = 0;
|
|
228
|
+
}
|
|
229
|
+
if (playing && !r.wasPlaying && r.kind === "beam") r.age = 0;
|
|
230
|
+
r.wasPlaying = playing;
|
|
231
|
+
if (!paused) r.age += delta;
|
|
232
|
+
if (kind) {
|
|
233
|
+
if (playing && (!paused || r.pointsCount === 0)) beam(world, r);
|
|
234
|
+
else if (!playing) r.pointsCount = 0;
|
|
235
|
+
else if (r.columns.points[r.row].length === 0) {
|
|
236
|
+
const target = number(r, "target");
|
|
237
|
+
if (target >= 0 && (!world.isAlive(target) || !world.has(target, GlobalTransform))) {
|
|
238
|
+
r.error = "render/beam-target";
|
|
239
|
+
r.pointsCount = 0;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} else trail(r, playing, paused);
|
|
243
|
+
ribbonGeometry(r);
|
|
244
|
+
if (!paused && (playing || r.pointsCount > 0)) demand = true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (let i = state.records.length - 1; i >= 0; i--)
|
|
249
|
+
if (state.records[i].seen !== state.frame) {
|
|
250
|
+
const r = state.records[i];
|
|
251
|
+
(r.kind === "beam" ? state.beams : state.trails).delete(r.entity);
|
|
252
|
+
state.records.splice(i, 1);
|
|
253
|
+
}
|
|
254
|
+
world.tryResource(FrameDemand)?.set("render/ribbons", demand);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
function shiftRibbons(world, x, y, z) {
|
|
258
|
+
const records = world.resource(Ribbons).records;
|
|
259
|
+
for (let i = 0; i < records.length; i++) {
|
|
260
|
+
const r = records[i];
|
|
261
|
+
for (let j = 0; j < r.pointsCount; j++) {
|
|
262
|
+
const at = (r.head + j) % r.capacity * 5;
|
|
263
|
+
r.points[at] = r.points[at] + x;
|
|
264
|
+
r.points[at + 1] = r.points[at + 1] + y;
|
|
265
|
+
r.points[at + 2] = r.points[at + 2] + z;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function describeRibbons(world) {
|
|
270
|
+
const s = world.resource(Ribbons);
|
|
271
|
+
return {
|
|
272
|
+
rejected: s.rejected,
|
|
273
|
+
error: s.rejected ? "render/ribbon-limit" : null,
|
|
274
|
+
records: s.records.map((r) => ({
|
|
275
|
+
...recordDescription(r),
|
|
276
|
+
cpuBytes: r.vertices.byteLength + r.points.byteLength + r.matrix.byteLength + r.targetMatrix.byteLength + r.scratch.byteLength,
|
|
277
|
+
points: r.pointsCount,
|
|
278
|
+
capacity: r.capacity
|
|
279
|
+
}))
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
export {
|
|
283
|
+
Ribbons,
|
|
284
|
+
describeRibbons,
|
|
285
|
+
ribbonGeometry,
|
|
286
|
+
shiftRibbons,
|
|
287
|
+
updateRibbons
|
|
288
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scenario tests (0075), a test helper exported from `@aethervtt/shard-render/testing`: a fixture's
|
|
3
|
+
* camera path runs through a 0074 capture, and each slice of the scenario (`bench/perf/budgets.json`)
|
|
4
|
+
* is read from it with the covers rule (`Capture.spanTime`), so tests, `shard profile` and the
|
|
5
|
+
* `perf` overlay measure the same thing. On a machine whose GPU passes overlap (machines.json's
|
|
6
|
+
* `passTiming: "ablation"`), GPU slices are measured by ablation under `pnpm bench` instead.
|
|
7
|
+
*/
|
|
8
|
+
import { type World } from '@aethervtt/shard-core';
|
|
9
|
+
import type { Capture, CaptureSummary } from '@aethervtt/shard-core/capture';
|
|
10
|
+
import { type Track } from '@aethervtt/shard-core/test-env';
|
|
11
|
+
import type { expect as vitestExpect } from 'vitest';
|
|
12
|
+
export interface ScenarioSlice {
|
|
13
|
+
key: string;
|
|
14
|
+
track: Track;
|
|
15
|
+
/** Spans that ran in the capture and that the key covers. */
|
|
16
|
+
covers: string[];
|
|
17
|
+
/** Time the key covered in each captured frame (ms). */
|
|
18
|
+
perFrame: Float64Array;
|
|
19
|
+
/** The p95 of it: frames whose GPU timings landed, for a GPU slice. Ablation's cost instead. */
|
|
20
|
+
p95: number;
|
|
21
|
+
measuredBy: 'capture' | 'ablation';
|
|
22
|
+
/**
|
|
23
|
+
* Ablation only: half the range of the cost over rounds, in ms. A pass's cost is the difference
|
|
24
|
+
* of two frame times that vary by milliseconds, so a sub-millisecond pass can't be resolved
|
|
25
|
+
* below this; the budget check allows it.
|
|
26
|
+
*/
|
|
27
|
+
noise?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface ScenarioRun {
|
|
30
|
+
name: string;
|
|
31
|
+
summary: CaptureSummary;
|
|
32
|
+
capture: Capture;
|
|
33
|
+
/** Every span that ran during the capture, by name. */
|
|
34
|
+
spans: string[];
|
|
35
|
+
/** Each frame's CPU time (`frame`) and GPU time (`gpu:frame`; 0 where none landed). */
|
|
36
|
+
frame: {
|
|
37
|
+
cpu: Float64Array;
|
|
38
|
+
gpu: Float64Array;
|
|
39
|
+
cpuP95: number;
|
|
40
|
+
gpuP95: number;
|
|
41
|
+
};
|
|
42
|
+
/** Whether GPU passes were timed (the device has `timestamp-query`). */
|
|
43
|
+
gpuTimed: boolean;
|
|
44
|
+
slices: ScenarioSlice[];
|
|
45
|
+
}
|
|
46
|
+
export interface ScenarioOptions {
|
|
47
|
+
/** Frames along the camera path, each run through the capture. */
|
|
48
|
+
frames: number;
|
|
49
|
+
/** Puts the camera at path frame `i`, runs one app frame, and waits for the GPU. */
|
|
50
|
+
step: (i: number) => void | Promise<void>;
|
|
51
|
+
/** Ablation under `pnpm bench` on a machine that needs it: frames per measurement, rounds. */
|
|
52
|
+
ablation?: {
|
|
53
|
+
frames?: number;
|
|
54
|
+
rounds?: number; /** Ablate whatever the machine (tests of this helper). */
|
|
55
|
+
force?: boolean;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Runs a scenario's camera path through a capture and measures its frame and slices (0075). The
|
|
60
|
+
* slices are the scenario's keys in budgets.json, `headroom` aside.
|
|
61
|
+
*/
|
|
62
|
+
export declare function measureScenario(world: World, name: string, options: ScenarioOptions): Promise<ScenarioRun>;
|
|
63
|
+
/**
|
|
64
|
+
* The checks of a scenario test. In `pnpm test`: the capture ran every frame, and every slice key
|
|
65
|
+
* covers at least one span that ran (GPU ones where the device times passes). Under `pnpm bench`:
|
|
66
|
+
* p95 of the frame and of each slice against the scenario's budgets, soft, so every one is
|
|
67
|
+
* recorded for `bench/perf/report.json`. A slice measured by ablation fails only when it's over by
|
|
68
|
+
* more than its noise: what's checked (and reported) is its cost minus the noise.
|
|
69
|
+
*/
|
|
70
|
+
export declare function expectScenario(run: ScenarioRun, expect: typeof vitestExpect): void;
|