@aethervtt/shard-render 0.1.0 → 0.2.1
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/atmosphere-nodes.js +100 -12
- package/dist/atmosphere-shaders.js +17 -5
- package/dist/baseline/exposure.d.ts +1 -1
- package/dist/baseline/exposure.js +18 -5
- package/dist/crowd.d.ts +21 -0
- package/dist/crowd.js +121 -0
- 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.js +4 -1
- package/dist/display-nodes.js +24 -11
- 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.js +26 -2
- package/dist/graph.d.ts +34 -0
- package/dist/graph.js +139 -14
- package/dist/index.d.ts +11 -1
- package/dist/index.js +57 -0
- package/dist/overlay-registry.d.ts +3 -1
- package/dist/overlay-registry.js +3 -1
- package/dist/overlays.js +5 -2
- package/dist/perf-overlay.d.ts +9 -0
- package/dist/perf-overlay.js +125 -0
- package/dist/plugin.d.ts +11 -1
- package/dist/plugin.js +33 -2
- package/dist/post-common.d.ts +20 -2
- package/dist/post-common.js +79 -2
- package/dist/post-nodes.d.ts +5 -0
- package/dist/post-nodes.js +136 -104
- package/dist/post-scene.d.ts +130 -0
- package/dist/post-scene.js +164 -0
- package/dist/post-shaders.js +41 -63
- package/dist/post.js +11 -1
- 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/shaders.js +58 -5
- package/dist/shadows.d.ts +2 -1
- package/dist/shadows.js +3 -1
- package/dist/standard.js +6 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +6 -0
- package/dist/timer.d.ts +16 -1
- package/dist/timer.js +66 -19
- 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/package.json +19 -11
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { defineResource, defineSystem, Last, PostUpdate } from "@aethervtt/shard-core";
|
|
2
|
+
import { definePlugin, FrameDemand } from "@aethervtt/shard-runtime";
|
|
3
|
+
import { TransformSystems } from "@aethervtt/shard-transform";
|
|
4
|
+
import { Decals, describeDecals, updateDecals } from "./decals.js";
|
|
5
|
+
import { addRenderFeatures } from "./features.js";
|
|
6
|
+
import { Graph, RenderDescribers, RenderSet, Shaders } from "./plugin.js";
|
|
7
|
+
import { registerShaders } from "./shaders.js";
|
|
8
|
+
import { DecalsPath } from "./vfx.js";
|
|
9
|
+
import { VFX_SHADERS, VfxRenderer } from "./vfx-draw.js";
|
|
10
|
+
import { computeVisibility } from "./visibility.js";
|
|
11
|
+
const DecalGpu = defineResource("render/DecalGpu", {
|
|
12
|
+
init: () => ({})
|
|
13
|
+
});
|
|
14
|
+
const prune = defineSystem({
|
|
15
|
+
name: "render/decals-prune",
|
|
16
|
+
run: (_, world) => world.resource(DecalGpu).renderer?.prune(world)
|
|
17
|
+
});
|
|
18
|
+
const decalsPlugin = definePlugin({
|
|
19
|
+
name: "render/decals",
|
|
20
|
+
dependencies: ["render/forward"],
|
|
21
|
+
provides: [Decals, DecalGpu, updateDecals],
|
|
22
|
+
build(app) {
|
|
23
|
+
app.world.initResource(DecalsPath);
|
|
24
|
+
app.world.initResource(Decals);
|
|
25
|
+
app.world.initResource(DecalGpu);
|
|
26
|
+
app.addSystems(PostUpdate, updateDecals.after(TransformSystems, computeVisibility));
|
|
27
|
+
app.addSystems(Last, prune.inSet(RenderSet.Prepare));
|
|
28
|
+
},
|
|
29
|
+
ready(app) {
|
|
30
|
+
const w = app.world, renderer = new VfxRenderer(w.resource(Decals).records, true);
|
|
31
|
+
w.resource(DecalGpu).renderer = renderer;
|
|
32
|
+
registerShaders(w.resource(Shaders), VFX_SHADERS);
|
|
33
|
+
addRenderFeatures(w, {
|
|
34
|
+
name: "render/decals",
|
|
35
|
+
description: "Clipped rigid-mesh projected decals.",
|
|
36
|
+
nodes: ["decals"],
|
|
37
|
+
baseline: { strategy: "CPU projection and the same vertex/fragment passes" }
|
|
38
|
+
});
|
|
39
|
+
w.resource(Graph).addNode("decals", renderer.node());
|
|
40
|
+
w.initResource(RenderDescribers).set("decals", describeDecals);
|
|
41
|
+
},
|
|
42
|
+
dispose(app) {
|
|
43
|
+
app.world.resource(DecalGpu).renderer?.dispose();
|
|
44
|
+
const s = app.world.resource(Decals);
|
|
45
|
+
s.records.length = 0;
|
|
46
|
+
s.receivers.length = 0;
|
|
47
|
+
s.receiverByEntity.clear();
|
|
48
|
+
app.world.tryResource(FrameDemand)?.set("render/decals", false);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
export {
|
|
52
|
+
DecalGpu,
|
|
53
|
+
decalsPlugin
|
|
54
|
+
};
|
package/dist/decals.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type Entity, type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { Mesh } from '@aethervtt/shard-mesh';
|
|
3
|
+
import { type VfxRecord } from './vfx-state.js';
|
|
4
|
+
export declare const DECAL_TRIANGLE_BUDGET = 32768;
|
|
5
|
+
export interface DecalRange {
|
|
6
|
+
start: number;
|
|
7
|
+
count: number;
|
|
8
|
+
entity: Entity;
|
|
9
|
+
layers: number;
|
|
10
|
+
}
|
|
11
|
+
interface Receiver {
|
|
12
|
+
entity: Entity;
|
|
13
|
+
matrix: Float64Array;
|
|
14
|
+
mesh: Mesh | undefined;
|
|
15
|
+
version: number;
|
|
16
|
+
mask: number;
|
|
17
|
+
visible: boolean;
|
|
18
|
+
unsupported: boolean;
|
|
19
|
+
layers: number;
|
|
20
|
+
seen: number;
|
|
21
|
+
}
|
|
22
|
+
export interface DecalRecord extends VfxRecord {
|
|
23
|
+
inverse: Float64Array;
|
|
24
|
+
settings: Float64Array;
|
|
25
|
+
geometryCount: number;
|
|
26
|
+
receiverRevision: number;
|
|
27
|
+
rebuilds: number;
|
|
28
|
+
ranges: DecalRange[];
|
|
29
|
+
clipA: Float64Array;
|
|
30
|
+
clipB: Float64Array;
|
|
31
|
+
worldTriangle: Float64Array;
|
|
32
|
+
}
|
|
33
|
+
export declare const Decals: import("@aethervtt/shard-core").ResourceDef<{
|
|
34
|
+
records: DecalRecord[];
|
|
35
|
+
receivers: Receiver[];
|
|
36
|
+
receiverByEntity: Map<number, Receiver>;
|
|
37
|
+
frame: number;
|
|
38
|
+
revision: number;
|
|
39
|
+
rejected: number;
|
|
40
|
+
}>;
|
|
41
|
+
/** Clips a convex polygon against one axis-aligned half-space. Scratch fits a clipped triangle. */
|
|
42
|
+
export declare function clipPlane(input: Float64Array, count: number, output: Float64Array, axis: number, sign: number, half: number): number;
|
|
43
|
+
/** Cold projection work. Output is all-or-nothing on budget/unsupported errors. */
|
|
44
|
+
export declare function projectDecal(r: DecalRecord, receivers: readonly Receiver[]): void;
|
|
45
|
+
export declare const updateDecals: import("@aethervtt/shard-core").SystemDef<{
|
|
46
|
+
q: import("@aethervtt/shard-core").Query;
|
|
47
|
+
receivers: import("@aethervtt/shard-core").Query;
|
|
48
|
+
}>;
|
|
49
|
+
export declare function describeDecals(world: World): {
|
|
50
|
+
rejected: number;
|
|
51
|
+
error: string | null;
|
|
52
|
+
receivers: number;
|
|
53
|
+
records: {
|
|
54
|
+
entity: number;
|
|
55
|
+
kind: "beam" | "decal" | "trail";
|
|
56
|
+
vertices: number;
|
|
57
|
+
age: number;
|
|
58
|
+
opacity: number;
|
|
59
|
+
error: string | null;
|
|
60
|
+
visible: boolean;
|
|
61
|
+
texture: string;
|
|
62
|
+
playing: boolean;
|
|
63
|
+
paused: boolean;
|
|
64
|
+
cpuBytes: number;
|
|
65
|
+
rebuilds: number;
|
|
66
|
+
triangles: number;
|
|
67
|
+
}[];
|
|
68
|
+
};
|
|
69
|
+
export {};
|
package/dist/decals.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import {
|
|
2
|
+
affine,
|
|
3
|
+
defineResource,
|
|
4
|
+
defineSystem
|
|
5
|
+
} from "@aethervtt/shard-core";
|
|
6
|
+
import { FrameDemand, Time } from "@aethervtt/shard-runtime";
|
|
7
|
+
import { GlobalTransform } from "@aethervtt/shard-transform";
|
|
8
|
+
import { Materials, Meshes } from "./assets.js";
|
|
9
|
+
import { Mesh3d, MeshMaterial, MorphWeights, SkinnedMesh } from "./instances.js";
|
|
10
|
+
import { RenderLayers } from "./layers.js";
|
|
11
|
+
import { isTransparent } from "./materials.js";
|
|
12
|
+
import { Decal, DecalReceiver } from "./vfx.js";
|
|
13
|
+
import {
|
|
14
|
+
createRecord,
|
|
15
|
+
matrixChanged,
|
|
16
|
+
number,
|
|
17
|
+
point,
|
|
18
|
+
recordDescription,
|
|
19
|
+
refreshRecord,
|
|
20
|
+
VFX_STRIDE
|
|
21
|
+
} from "./vfx-state.js";
|
|
22
|
+
import { ComputedVisibility } from "./visibility.js";
|
|
23
|
+
const DECAL_TRIANGLE_BUDGET = 32768;
|
|
24
|
+
const Decals = defineResource("render/Decals", {
|
|
25
|
+
init: () => ({
|
|
26
|
+
records: [],
|
|
27
|
+
receivers: [],
|
|
28
|
+
receiverByEntity: /* @__PURE__ */ new Map(),
|
|
29
|
+
frame: 0,
|
|
30
|
+
revision: 0,
|
|
31
|
+
rejected: 0
|
|
32
|
+
})
|
|
33
|
+
});
|
|
34
|
+
function create(world, entity) {
|
|
35
|
+
const max = world.get(entity, Decal).maxVertices;
|
|
36
|
+
return {
|
|
37
|
+
...createRecord(world, entity, Decal, "decal", max),
|
|
38
|
+
inverse: new Float64Array(12),
|
|
39
|
+
settings: new Float64Array(8).fill(NaN),
|
|
40
|
+
geometryCount: 0,
|
|
41
|
+
receiverRevision: -1,
|
|
42
|
+
rebuilds: 0,
|
|
43
|
+
ranges: [],
|
|
44
|
+
clipA: new Float64Array(48),
|
|
45
|
+
clipB: new Float64Array(48),
|
|
46
|
+
worldTriangle: new Float64Array(9)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function clipPlane(input, count, output, axis, sign, half) {
|
|
50
|
+
let n = 0;
|
|
51
|
+
for (let i = 0; i < count; i++) {
|
|
52
|
+
const a = (i + count - 1) % count * 3, b = i * 3;
|
|
53
|
+
const da = half - sign * input[a + axis], db = half - sign * input[b + axis];
|
|
54
|
+
if (da >= 0 !== db >= 0) {
|
|
55
|
+
const f = da / (da - db);
|
|
56
|
+
for (let k = 0; k < 3; k++)
|
|
57
|
+
output[n * 3 + k] = input[a + k] + (input[b + k] - input[a + k]) * f;
|
|
58
|
+
n++;
|
|
59
|
+
}
|
|
60
|
+
if (db >= 0) {
|
|
61
|
+
for (let k = 0; k < 3; k++) output[n * 3 + k] = input[b + k];
|
|
62
|
+
n++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return n;
|
|
66
|
+
}
|
|
67
|
+
function intersects(r, receiver, sx, sy, sz) {
|
|
68
|
+
const bounds = receiver.mesh.bounds, scratch = r.worldTriangle;
|
|
69
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
70
|
+
for (let i = 0; i < 8; i++) {
|
|
71
|
+
point(
|
|
72
|
+
scratch,
|
|
73
|
+
0,
|
|
74
|
+
receiver.matrix,
|
|
75
|
+
bounds[i & 1 ? 3 : 0],
|
|
76
|
+
bounds[i & 2 ? 4 : 1],
|
|
77
|
+
bounds[i & 4 ? 5 : 2]
|
|
78
|
+
);
|
|
79
|
+
point(scratch, 3, r.inverse, scratch[0], scratch[1], scratch[2]);
|
|
80
|
+
minX = Math.min(minX, scratch[3]);
|
|
81
|
+
minY = Math.min(minY, scratch[4]);
|
|
82
|
+
minZ = Math.min(minZ, scratch[5]);
|
|
83
|
+
maxX = Math.max(maxX, scratch[3]);
|
|
84
|
+
maxY = Math.max(maxY, scratch[4]);
|
|
85
|
+
maxZ = Math.max(maxZ, scratch[5]);
|
|
86
|
+
}
|
|
87
|
+
return minX <= sx / 2 && maxX >= -sx / 2 && minY <= sy / 2 && maxY >= -sy / 2 && minZ <= sz / 2 && maxZ >= -sz / 2;
|
|
88
|
+
}
|
|
89
|
+
function projectDecal(r, receivers) {
|
|
90
|
+
r.count = 0;
|
|
91
|
+
r.geometryCount = 0;
|
|
92
|
+
r.ranges.length = 0;
|
|
93
|
+
r.error = null;
|
|
94
|
+
r.rebuilds++;
|
|
95
|
+
if (!affine.invert(r.inverse, r.matrix)) {
|
|
96
|
+
r.error = "render/decal-singular";
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const size = r.columns.size, so = r.row * 3;
|
|
100
|
+
const sx = size[so], sy = size[so + 1], sz = size[so + 2];
|
|
101
|
+
if (!(sx > 0 && sy > 0 && sz > 0)) {
|
|
102
|
+
r.error = "render/decal-size";
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const mask = number(r, "receiverMask"), offset = number(r, "offset"), cos = Math.cos(number(r, "maxAngle") * Math.PI / 180);
|
|
106
|
+
const zx = r.matrix[2], zy = r.matrix[6], zz = r.matrix[10], zl = Math.sqrt(zx * zx + zy * zy + zz * zz);
|
|
107
|
+
let tested = 0;
|
|
108
|
+
for (let ri = 0; ri < receivers.length; ri++) {
|
|
109
|
+
const receiver = receivers[ri], mesh = receiver.mesh;
|
|
110
|
+
if (!receiver.visible || !(receiver.mask & mask) || !mesh || !intersects(r, receiver, sx, sy, sz))
|
|
111
|
+
continue;
|
|
112
|
+
if (receiver.unsupported) {
|
|
113
|
+
r.error = "render/decal-unsupported-receiver";
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
const start = r.count, positions = mesh.positions, indices = mesh.indices;
|
|
117
|
+
const triangles = Math.floor((indices ? indices.length : positions.length / 3) / 3);
|
|
118
|
+
if (tested + triangles > DECAL_TRIANGLE_BUDGET) {
|
|
119
|
+
r.error = "render/decal-input-budget";
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
tested += triangles;
|
|
123
|
+
for (let t = 0; t < triangles; t++) {
|
|
124
|
+
for (let v = 0; v < 3; v++) {
|
|
125
|
+
const vi = (indices ? indices[t * 3 + v] : t * 3 + v) * 3;
|
|
126
|
+
point(
|
|
127
|
+
r.worldTriangle,
|
|
128
|
+
v * 3,
|
|
129
|
+
receiver.matrix,
|
|
130
|
+
positions[vi],
|
|
131
|
+
positions[vi + 1],
|
|
132
|
+
positions[vi + 2]
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const w = r.worldTriangle;
|
|
136
|
+
const ax = w[3] - w[0], ay = w[4] - w[1], az = w[5] - w[2], bx = w[6] - w[0], by = w[7] - w[1], bz = w[8] - w[2];
|
|
137
|
+
let nx = ay * bz - az * by, ny = az * bx - ax * bz, nz = ax * by - ay * bx;
|
|
138
|
+
const length = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
139
|
+
if (length < 1e-12) continue;
|
|
140
|
+
nx /= length;
|
|
141
|
+
ny /= length;
|
|
142
|
+
nz /= length;
|
|
143
|
+
if ((nx * zx + ny * zy + nz * zz) / zl < cos) continue;
|
|
144
|
+
for (let v = 0; v < 3; v++)
|
|
145
|
+
point(r.clipA, v * 3, r.inverse, w[v * 3], w[v * 3 + 1], w[v * 3 + 2]);
|
|
146
|
+
let a = r.clipA, b = r.clipB, count = 3;
|
|
147
|
+
for (let plane = 0; plane < 6 && count; plane++) {
|
|
148
|
+
const axis = plane >> 1, half = (axis === 0 ? sx : axis === 1 ? sy : sz) * 0.5;
|
|
149
|
+
count = clipPlane(a, count, b, axis, plane % 2 ? -1 : 1, half);
|
|
150
|
+
const swap = a;
|
|
151
|
+
a = b;
|
|
152
|
+
b = swap;
|
|
153
|
+
}
|
|
154
|
+
if (count < 3) continue;
|
|
155
|
+
if (r.count + (count - 2) * 3 > r.vertices.length / VFX_STRIDE) {
|
|
156
|
+
r.error = "render/decal-output-budget";
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
for (let fan = 1; fan < count - 1; fan++)
|
|
160
|
+
for (let v = 0; v < 3; v++) {
|
|
161
|
+
const at = (v === 0 ? 0 : v === 1 ? fan : fan + 1) * 3, o = r.count++ * VFX_STRIDE;
|
|
162
|
+
point(w, 0, r.matrix, a[at], a[at + 1], a[at + 2]);
|
|
163
|
+
r.vertices[o] = w[0] + nx * offset;
|
|
164
|
+
r.vertices[o + 1] = w[1] + ny * offset;
|
|
165
|
+
r.vertices[o + 2] = w[2] + nz * offset;
|
|
166
|
+
r.vertices[o + 3] = 0;
|
|
167
|
+
r.vertices[o + 4] = 0;
|
|
168
|
+
r.vertices[o + 5] = 0;
|
|
169
|
+
r.vertices[o + 6] = a[at] / sx + 0.5;
|
|
170
|
+
r.vertices[o + 7] = 0.5 - a[at + 1] / sy;
|
|
171
|
+
for (let k = 8; k < 12; k++) r.vertices[o + k] = 1;
|
|
172
|
+
r.vertices[o + 12] = 0;
|
|
173
|
+
r.vertices[o + 13] = 0;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (r.error) break;
|
|
177
|
+
if (r.count > start)
|
|
178
|
+
r.ranges.push({
|
|
179
|
+
start,
|
|
180
|
+
count: r.count - start,
|
|
181
|
+
entity: receiver.entity,
|
|
182
|
+
layers: receiver.layers
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (r.error) {
|
|
186
|
+
r.count = 0;
|
|
187
|
+
r.ranges.length = 0;
|
|
188
|
+
}
|
|
189
|
+
r.geometryCount = r.count;
|
|
190
|
+
r.version++;
|
|
191
|
+
}
|
|
192
|
+
const updateDecals = defineSystem({
|
|
193
|
+
name: "render/decals-update",
|
|
194
|
+
setup: (world) => ({
|
|
195
|
+
q: world.query({ with: [Decal, GlobalTransform] }),
|
|
196
|
+
receivers: world.query({ with: [DecalReceiver, Mesh3d, GlobalTransform] })
|
|
197
|
+
}),
|
|
198
|
+
run({ q, receivers }, world) {
|
|
199
|
+
const state = world.resource(Decals), meshes = world.resource(Meshes), delta = world.resource(Time).delta;
|
|
200
|
+
state.frame++;
|
|
201
|
+
state.rejected = 0;
|
|
202
|
+
for (let ti = 0; ti < receivers.tables.length; ti++) {
|
|
203
|
+
const table = receivers.tables[ti], mask = table.column(DecalReceiver, "mask"), refs = table.column(Mesh3d, "mesh");
|
|
204
|
+
for (let row = 0; row < table.count; row++) {
|
|
205
|
+
const entity = table.entities[row];
|
|
206
|
+
let receiver = state.receiverByEntity.get(entity);
|
|
207
|
+
if (!receiver) {
|
|
208
|
+
receiver = {
|
|
209
|
+
entity,
|
|
210
|
+
matrix: new Float64Array(12),
|
|
211
|
+
mesh: void 0,
|
|
212
|
+
version: -1,
|
|
213
|
+
mask: -1,
|
|
214
|
+
visible: false,
|
|
215
|
+
unsupported: false,
|
|
216
|
+
layers: 1,
|
|
217
|
+
seen: 0
|
|
218
|
+
};
|
|
219
|
+
state.receivers.push(receiver);
|
|
220
|
+
state.receiverByEntity.set(entity, receiver);
|
|
221
|
+
state.revision++;
|
|
222
|
+
}
|
|
223
|
+
const mesh = meshes.get(refs[row]), version = mesh?.version ?? -1;
|
|
224
|
+
const visible = !table.has(ComputedVisibility) || table.column(ComputedVisibility, "visible")[row] !== 0;
|
|
225
|
+
const layers = table.has(RenderLayers) ? table.column(RenderLayers, "mask")[row] : 1;
|
|
226
|
+
let unsupported = !!mesh && (!mesh.positions || !!mesh.joints || !!mesh.targets?.length || table.has(SkinnedMesh) || table.has(MorphWeights));
|
|
227
|
+
if (table.has(MeshMaterial)) {
|
|
228
|
+
const material = world.tryResource(Materials)?.get(table.column(MeshMaterial, "material")[row]);
|
|
229
|
+
if (material && isTransparent(material.type.blendOf(material.value))) unsupported = true;
|
|
230
|
+
}
|
|
231
|
+
if (matrixChanged(world, entity, receiver.matrix) || receiver.mesh !== mesh || receiver.version !== version || receiver.mask !== mask[row] || receiver.visible !== visible || receiver.layers !== layers || receiver.unsupported !== unsupported)
|
|
232
|
+
state.revision++;
|
|
233
|
+
receiver.mesh = mesh;
|
|
234
|
+
receiver.version = version;
|
|
235
|
+
receiver.mask = mask[row];
|
|
236
|
+
receiver.visible = visible;
|
|
237
|
+
receiver.layers = layers;
|
|
238
|
+
receiver.unsupported = unsupported;
|
|
239
|
+
receiver.seen = state.frame;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (let i = state.receivers.length - 1; i >= 0; i--)
|
|
243
|
+
if (state.receivers[i].seen !== state.frame) {
|
|
244
|
+
state.receiverByEntity.delete(state.receivers[i].entity);
|
|
245
|
+
state.receivers.splice(i, 1);
|
|
246
|
+
state.revision++;
|
|
247
|
+
}
|
|
248
|
+
let demand = false;
|
|
249
|
+
for (let ti = 0; ti < q.tables.length; ti++) {
|
|
250
|
+
const table = q.tables[ti];
|
|
251
|
+
for (let row = 0; row < table.count; row++) {
|
|
252
|
+
const entity = table.entities[row];
|
|
253
|
+
const maxVertices = table.column(Decal, "maxVertices")[row];
|
|
254
|
+
if (maxVertices < 3 || maxVertices > 65535) {
|
|
255
|
+
state.rejected++;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
let r;
|
|
259
|
+
for (let i = 0; i < state.records.length; i++)
|
|
260
|
+
if (state.records[i].entity === entity) {
|
|
261
|
+
r = state.records[i];
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
if (!r) {
|
|
265
|
+
if (state.records.length >= 64) {
|
|
266
|
+
state.rejected++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
r = create(world, entity);
|
|
270
|
+
state.records.push(r);
|
|
271
|
+
}
|
|
272
|
+
refreshRecord(world, r);
|
|
273
|
+
r.seen = state.frame;
|
|
274
|
+
let changed = matrixChanged(world, entity, r.matrix);
|
|
275
|
+
const size = r.columns.size;
|
|
276
|
+
for (let i = 0; i < 8; i++) {
|
|
277
|
+
const value = i < 3 ? size[row * 3 + i] : i === 3 ? number(r, "receiverMask") : i === 4 ? number(r, "maxAngle") : i === 5 ? number(r, "offset") : i === 6 ? number(r, "maxVertices") : 0;
|
|
278
|
+
if (r.settings[i] !== value) changed = true;
|
|
279
|
+
r.settings[i] = value;
|
|
280
|
+
}
|
|
281
|
+
const capacity = number(r, "maxVertices") * VFX_STRIDE;
|
|
282
|
+
if (r.vertices.length !== capacity) {
|
|
283
|
+
r.vertices = new Float32Array(capacity);
|
|
284
|
+
changed = true;
|
|
285
|
+
}
|
|
286
|
+
const playing = !!number(r, "playing"), paused = !!number(r, "paused"), lifetime = number(r, "lifetime");
|
|
287
|
+
if (playing && !r.wasPlaying) r.age = 0;
|
|
288
|
+
r.wasPlaying = playing;
|
|
289
|
+
if (playing && !paused) r.age += delta;
|
|
290
|
+
if (changed || r.receiverRevision !== state.revision) {
|
|
291
|
+
projectDecal(r, state.receivers);
|
|
292
|
+
r.receiverRevision = state.revision;
|
|
293
|
+
}
|
|
294
|
+
r.count = playing && (!lifetime || r.age < lifetime) ? r.geometryCount : 0;
|
|
295
|
+
if (lifetime && number(r, "fadeOut") > 0)
|
|
296
|
+
r.opacity *= Math.min(1, Math.max(0, (lifetime - r.age) / number(r, "fadeOut")));
|
|
297
|
+
if (playing && !paused && lifetime > 0 && r.age < lifetime) demand = true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
for (let i = state.records.length - 1; i >= 0; i--)
|
|
301
|
+
if (state.records[i].seen !== state.frame) state.records.splice(i, 1);
|
|
302
|
+
world.tryResource(FrameDemand)?.set("render/decals", demand);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
function describeDecals(world) {
|
|
306
|
+
const s = world.resource(Decals);
|
|
307
|
+
return {
|
|
308
|
+
rejected: s.rejected,
|
|
309
|
+
error: s.rejected ? "render/decal-limit" : null,
|
|
310
|
+
receivers: s.receivers.length,
|
|
311
|
+
records: s.records.map((r) => ({
|
|
312
|
+
...recordDescription(r),
|
|
313
|
+
cpuBytes: r.vertices.byteLength + r.matrix.byteLength + r.inverse.byteLength + r.settings.byteLength + r.clipA.byteLength + r.clipB.byteLength + r.worldTriangle.byteLength,
|
|
314
|
+
rebuilds: r.rebuilds,
|
|
315
|
+
triangles: r.geometryCount / 3
|
|
316
|
+
}))
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
export {
|
|
320
|
+
DECAL_TRIANGLE_BUDGET,
|
|
321
|
+
Decals,
|
|
322
|
+
clipPlane,
|
|
323
|
+
describeDecals,
|
|
324
|
+
projectDecal,
|
|
325
|
+
updateDecals
|
|
326
|
+
};
|
package/dist/deferred.js
CHANGED
|
@@ -2,6 +2,7 @@ import { ShardError } from "@aethervtt/shard-core";
|
|
|
2
2
|
import { GpuBuffer } from "@aethervtt/shard-gpu";
|
|
3
3
|
import { Culler } from "./culling.js";
|
|
4
4
|
import { addRenderFeatures } from "./features.js";
|
|
5
|
+
import { FoliagePath } from "./foliage-path.js";
|
|
5
6
|
import {
|
|
6
7
|
drawMaterials,
|
|
7
8
|
ForwardStateResource,
|
|
@@ -31,7 +32,9 @@ function fillGBuffer(ctx) {
|
|
|
31
32
|
const state = ctx.world.resource(ForwardStateResource);
|
|
32
33
|
const cam = cameraOf(ctx.view);
|
|
33
34
|
const pv = state.views.get(ctx.view.name);
|
|
34
|
-
if (pv)
|
|
35
|
+
if (!pv) return;
|
|
36
|
+
drawMaterials(ctx, state, pv, cam, cam.draws, PASS_GBUFFER);
|
|
37
|
+
ctx.world.tryResource(FoliagePath)?.draw(ctx, cam, PASS_GBUFFER, ctx.renderPass);
|
|
35
38
|
}
|
|
36
39
|
const gbufferNode = {
|
|
37
40
|
kind: "render",
|
package/dist/display-nodes.js
CHANGED
|
@@ -33,7 +33,8 @@ function tonemapNode() {
|
|
|
33
33
|
kind: "render",
|
|
34
34
|
phase: RenderPhase.Tonemap,
|
|
35
35
|
enabled: (view) => cameraOf(view) !== void 0,
|
|
36
|
-
|
|
36
|
+
// Bloom's chain, when the view blooms: the glow is composited here (see the 'bloom' node).
|
|
37
|
+
reads: ["post-hdr", "bloom"],
|
|
37
38
|
writes: ["ldr"],
|
|
38
39
|
color: [{ resource: "ldr", clear: { r: 0, g: 0, b: 0, a: 1 } }],
|
|
39
40
|
run: (ctx) => {
|
|
@@ -43,7 +44,7 @@ function tonemapNode() {
|
|
|
43
44
|
layoutGen = gpu.generation;
|
|
44
45
|
layout = gpu.layouts.bindGroupLayout({
|
|
45
46
|
label: "tonemap",
|
|
46
|
-
entries: [tex(0, "unfilterable-float"), uniform(1), tex(2), sampler(3)]
|
|
47
|
+
entries: [tex(0, "unfilterable-float"), uniform(1), tex(2), sampler(3), tex(4)]
|
|
47
48
|
});
|
|
48
49
|
white = gpu.device.createTexture({
|
|
49
50
|
label: "tonemap/no-lut",
|
|
@@ -55,16 +56,18 @@ function tonemapNode() {
|
|
|
55
56
|
const format = ctx.texture("ldr").format;
|
|
56
57
|
const srgb = format.endsWith("-srgb");
|
|
57
58
|
const alpha = cam.alphaOutput;
|
|
59
|
+
const bloom = (cam.post.effects & PostEffect.Bloom) !== 0;
|
|
58
60
|
const pipeline = cache.render(
|
|
59
61
|
ctx,
|
|
60
|
-
`tonemap/${format}${alpha ? "/alpha" : ""}`,
|
|
62
|
+
`tonemap/${format}${alpha ? "/alpha" : ""}${bloom ? "/bloom" : ""}`,
|
|
61
63
|
"shard::post::tonemap",
|
|
62
64
|
"fs",
|
|
63
65
|
[layout],
|
|
64
66
|
[{ format }],
|
|
65
67
|
{
|
|
66
68
|
SRGB_TARGET: srgb,
|
|
67
|
-
TRANSPARENT: alpha
|
|
69
|
+
TRANSPARENT: alpha,
|
|
70
|
+
BLOOM: bloom
|
|
68
71
|
}
|
|
69
72
|
);
|
|
70
73
|
if (!pipeline) return;
|
|
@@ -82,7 +85,7 @@ function tonemapNode() {
|
|
|
82
85
|
flags |= 4;
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
|
-
scratch.fill(0, 0,
|
|
88
|
+
scratch.fill(0, 0, 36);
|
|
86
89
|
u[0] = cam.curve;
|
|
87
90
|
u[1] = cam.dither ? 1 : 0;
|
|
88
91
|
u[2] = flags;
|
|
@@ -100,20 +103,30 @@ function tonemapNode() {
|
|
|
100
103
|
scratch[26] = cam.width / Math.max(1, cam.height);
|
|
101
104
|
scratch[28] = 1 / cam.width;
|
|
102
105
|
scratch[29] = 1 / cam.height;
|
|
103
|
-
const
|
|
104
|
-
|
|
106
|
+
const chain = bloom ? ctx.texture("bloom") : void 0;
|
|
107
|
+
if (chain) {
|
|
108
|
+
const b = post.bloom;
|
|
109
|
+
scratch[32] = b.intensity;
|
|
110
|
+
scratch[33] = chain.mipLevelCount;
|
|
111
|
+
scratch[34] = b.threshold > 0 ? 0 : 1;
|
|
112
|
+
}
|
|
113
|
+
const params = cache.buffer(gpu, `${ctx.view.name}/tonemap`, 144);
|
|
114
|
+
params.write(scratch, 0, 0, 36);
|
|
105
115
|
const hdr = ctx.texture("post-hdr");
|
|
106
|
-
const lutView = lut ?? white.createView();
|
|
107
116
|
const group = cache.group(
|
|
108
117
|
gpu,
|
|
109
118
|
`${ctx.view.name}/tonemap`,
|
|
110
|
-
`${idOf(hdr)}/${params.version}/${lut ? idOf(lut) : 0}`,
|
|
119
|
+
`${idOf(hdr)}/${params.version}/${lut ? idOf(lut) : 0}/${chain ? idOf(chain) : 0}`,
|
|
111
120
|
layout,
|
|
112
121
|
() => [
|
|
113
122
|
{ binding: 0, resource: hdr.createView() },
|
|
114
123
|
{ binding: 1, resource: { buffer: params.buffer } },
|
|
115
|
-
{ binding: 2, resource:
|
|
116
|
-
{ binding: 3, resource: cache.sampler(gpu) }
|
|
124
|
+
{ binding: 2, resource: lut ?? white.createView() },
|
|
125
|
+
{ binding: 3, resource: cache.sampler(gpu) },
|
|
126
|
+
{
|
|
127
|
+
binding: 4,
|
|
128
|
+
resource: chain ? chain.createView({ mipLevelCount: 1 }) : white.createView()
|
|
129
|
+
}
|
|
117
130
|
]
|
|
118
131
|
);
|
|
119
132
|
const pass = ctx.renderPass;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type Profiler } from '@aethervtt/shard-core';
|
|
2
|
+
/** The budget key foliage's GPU time is measured by: it covers `gpu:foliage/place`, `/cull`, `/draw`. */
|
|
3
|
+
export declare const FOLIAGE_SPAN_KEY = "gpu:foliage";
|
|
4
|
+
export interface FoliageBudgetValue {
|
|
5
|
+
/**
|
|
6
|
+
* GPU milliseconds foliage may take a frame. 0 (the default): its `gpu:foliage` slice of the
|
|
7
|
+
* app's scenario (`App.perfScenario`) on the detected machine, and with no scenario (or an
|
|
8
|
+
* unknown machine) no target: foliage draws at full detail.
|
|
9
|
+
*/
|
|
10
|
+
ms: number;
|
|
11
|
+
/** The least detail it thins to, as a share of the instances drawn at full detail. Default 0.1. */
|
|
12
|
+
minDetail: number;
|
|
13
|
+
/** Read only: the target in force (ms), 0 when none. */
|
|
14
|
+
target: number;
|
|
15
|
+
/** Read only: foliage's GPU time a frame, smoothed (ms). */
|
|
16
|
+
measuredMs: number;
|
|
17
|
+
/**
|
|
18
|
+
* Read only: the share of full detail drawn (minDetail to 1). Density is its square root and range
|
|
19
|
+
* its fourth root, so instances drawn scale with it.
|
|
20
|
+
*/
|
|
21
|
+
detail: number;
|
|
22
|
+
}
|
|
23
|
+
export declare const FoliageBudget: import("@aethervtt/shard-core").ResourceDef<FoliageBudgetValue>;
|
|
24
|
+
/**
|
|
25
|
+
* The controller: one GPU time a frame in, detail out. Smoothed (an exponential average), with
|
|
26
|
+
* hysteresis (it moves only above the target, or under 85% of it) and asymmetric steps (down 1% a
|
|
27
|
+
* sample, up 0.5%), so the lag of GPU timings (frames late) doesn't make it oscillate. Like
|
|
28
|
+
* `TerrainBudget.triangles`' LOD bias (0043), slow on purpose: density and range change gradually.
|
|
29
|
+
*/
|
|
30
|
+
export declare class FoliageController {
|
|
31
|
+
detail: number;
|
|
32
|
+
/** Smoothed GPU time (ms); 0 before the first sample. */
|
|
33
|
+
smoothed: number;
|
|
34
|
+
private primed;
|
|
35
|
+
/** One frame's foliage GPU time against `target` (0: none, back to full detail at once). */
|
|
36
|
+
sample(ms: number, target: number, minDetail: number): void;
|
|
37
|
+
/** No target: full detail, and the smoothing starts over. */
|
|
38
|
+
reset(): void;
|
|
39
|
+
/** The share of instances kept by thinning (the cull's density factor). */
|
|
40
|
+
get density(): number;
|
|
41
|
+
/** The share of each layer's range drawn. */
|
|
42
|
+
get rangeScale(): number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Reads the GPU time a budget key covers, one GPU frame at a time, without allocating: when
|
|
46
|
+
* `gpu:frame` has a new sample, the covered spans that got one with it are summed. Span names are
|
|
47
|
+
* matched by `spanCovers` as they're interned.
|
|
48
|
+
*/
|
|
49
|
+
export declare class CoveredGpuTime {
|
|
50
|
+
readonly key: string;
|
|
51
|
+
/** The latest frame's covered time (ms), after `read` returned true. */
|
|
52
|
+
ms: number;
|
|
53
|
+
private ids;
|
|
54
|
+
private seen;
|
|
55
|
+
private count;
|
|
56
|
+
private scanned;
|
|
57
|
+
private frameId;
|
|
58
|
+
private frameRuns;
|
|
59
|
+
constructor(key: string);
|
|
60
|
+
/** True when a new GPU frame's timings landed since the last call; `ms` is then its covered time. */
|
|
61
|
+
read(profiler: Profiler): boolean;
|
|
62
|
+
/** New span names since the last scan: the ones the key covers join the list. */
|
|
63
|
+
private scan;
|
|
64
|
+
}
|