@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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { GpuContext } from '@aethervtt/shard-gpu';
|
|
3
|
+
import { type InteriorSupport } from './interior.js';
|
|
4
|
+
/** Level table entries; entry `INTERIOR_GROUND` is the ground level's (pieces with no Level). */
|
|
5
|
+
export declare const INTERIOR_LEVELS = 8;
|
|
6
|
+
export declare const INTERIOR_GROUND = 8;
|
|
7
|
+
/** A field texel's cover code: 0 uncovered, else 32768 + 64 × (cover top y − the layer's base). */
|
|
8
|
+
export declare const COVER_SCALE = 64;
|
|
9
|
+
export declare const COVER_ZERO = 32768;
|
|
10
|
+
/** Visibility, in a field texel's low 16 bits above the two link bits. */
|
|
11
|
+
export declare const VISIBILITY_MAX = 16383;
|
|
12
|
+
/** A field texel's link bits: flux to the +x and +z neighbours is blocked. */
|
|
13
|
+
export declare const LINK_X = 1;
|
|
14
|
+
export declare const LINK_Z = 2;
|
|
15
|
+
/** A row bin: distance (low 16 bits) and the barrier's top relative to the light (high), in 1/256 m. */
|
|
16
|
+
export declare const ROW_SCALE = 256;
|
|
17
|
+
export declare const ROW_ZERO = 32768;
|
|
18
|
+
/** A bin with no barrier: the low half all ones (256 m, past any range). */
|
|
19
|
+
export declare const ROW_CLEAR = 2147549183;
|
|
20
|
+
export declare const INTERIOR_SHADERS: Record<string, string>;
|
|
21
|
+
/**
|
|
22
|
+
* Interior lighting's GPU side: what providers write, and what forward binds. One r32uint texture
|
|
23
|
+
* array holds both parts (a stage has 16 sampled textures, and the baseline tier's standard
|
|
24
|
+
* material already uses 15): the field's layers, then the rows, `height` to a layer. Providers
|
|
25
|
+
* (structure) size and fill it, write the table, and switch the parts on; with both off nothing
|
|
26
|
+
* is bound at all.
|
|
27
|
+
*/
|
|
28
|
+
export declare class InteriorLighting implements InteriorSupport {
|
|
29
|
+
sky: boolean;
|
|
30
|
+
blocked: boolean;
|
|
31
|
+
provided: boolean;
|
|
32
|
+
version: number;
|
|
33
|
+
/**
|
|
34
|
+
* Set when the texture is made again (a size changed, or the device was lost): the provider
|
|
35
|
+
* writes the field and every row again, then clears it.
|
|
36
|
+
*/
|
|
37
|
+
stale: boolean;
|
|
38
|
+
/** The field's size in texels and layers, and the rows' bins and count (0: none). */
|
|
39
|
+
fieldWidth: number;
|
|
40
|
+
fieldHeight: number;
|
|
41
|
+
fieldLayers: number;
|
|
42
|
+
rowBins: number;
|
|
43
|
+
rowCount: number;
|
|
44
|
+
/** The texture's size: rows sit `height` to a layer after the field's layers. */
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
layers: number;
|
|
48
|
+
/** The table as written; `writeTable` uploads it when it changed. */
|
|
49
|
+
readonly table: Float32Array<ArrayBuffer>;
|
|
50
|
+
private readonly sent;
|
|
51
|
+
private tableStale;
|
|
52
|
+
private texture;
|
|
53
|
+
private generation;
|
|
54
|
+
private placeholder;
|
|
55
|
+
private buffer;
|
|
56
|
+
private readonly gpu;
|
|
57
|
+
private bound;
|
|
58
|
+
constructor(gpu: GpuContext);
|
|
59
|
+
get mode(): number;
|
|
60
|
+
/**
|
|
61
|
+
* Sizes the texture for a field of `fieldLayers` layers (0: none) and `rows` rows of `bins` bins
|
|
62
|
+
* (0: none). Made again, and `stale`, only when a size changes or the device was lost.
|
|
63
|
+
*/
|
|
64
|
+
configure(fieldWidth: number, fieldHeight: number, fieldLayers: number, bins: number, rows: number): void;
|
|
65
|
+
/**
|
|
66
|
+
* Whether the device was lost since the texture was made: makes it again (`stale`), so the
|
|
67
|
+
* provider writes everything once more. Providers check it each frame they're on.
|
|
68
|
+
*/
|
|
69
|
+
lost(): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Writes a rectangle of field layer `layer` from `data`, a whole layer's texels (fieldWidth ×
|
|
72
|
+
* fieldHeight, row by row): only the rectangle uploads.
|
|
73
|
+
*/
|
|
74
|
+
writeField(layer: number, x: number, z: number, w: number, h: number, data: Uint32Array): void;
|
|
75
|
+
/** Writes row `row` from `data` (`rowBins` bins). */
|
|
76
|
+
writeRow(row: number, data: Uint32Array): void;
|
|
77
|
+
/**
|
|
78
|
+
* Sets the table's field and size: origin (x, z), texel (m), field size, bins per row, filter
|
|
79
|
+
* taps. `writeTable` uploads.
|
|
80
|
+
*/
|
|
81
|
+
setGrid(originX: number, originZ: number, texel: number, width: number, height: number, bins: number, taps: number): void;
|
|
82
|
+
/**
|
|
83
|
+
* Sets level `i`'s entry (`INTERIOR_GROUND` for the ground level): its [floor, top) y, field
|
|
84
|
+
* layer, the y its cover heights count from, its interior ambient (rgb, cd/m²) and fill share.
|
|
85
|
+
*/
|
|
86
|
+
setLevel(i: number, floor: number, top: number, layer: number, base: number, ambient: ArrayLike<number>, fill: number): void;
|
|
87
|
+
/** How many levels (besides the ground's) the table holds. */
|
|
88
|
+
setLevelCount(count: number): void;
|
|
89
|
+
/** Uploads the table if it changed since the last upload. */
|
|
90
|
+
writeTable(): void;
|
|
91
|
+
/** GPU bytes the texture holds. */
|
|
92
|
+
get bytes(): number;
|
|
93
|
+
entries(gpu: GpuContext): readonly GPUBindGroupEntry[];
|
|
94
|
+
/** Frees the texture (both parts off). */
|
|
95
|
+
free(): void;
|
|
96
|
+
/** Frees everything (the plugin's dispose). */
|
|
97
|
+
destroy(): void;
|
|
98
|
+
}
|
|
99
|
+
export declare const InteriorLightingResource: import("@aethervtt/shard-core").ResourceDef<InteriorLighting>;
|
|
100
|
+
/** render.describe's interior section. */
|
|
101
|
+
export declare function describeInterior(world: World): {
|
|
102
|
+
sky: boolean;
|
|
103
|
+
blockedLights: boolean;
|
|
104
|
+
provided: boolean;
|
|
105
|
+
field: {
|
|
106
|
+
width: number;
|
|
107
|
+
height: number;
|
|
108
|
+
layers: number;
|
|
109
|
+
} | null;
|
|
110
|
+
rows: {
|
|
111
|
+
bins: number;
|
|
112
|
+
rows: number;
|
|
113
|
+
} | null;
|
|
114
|
+
bytes: number;
|
|
115
|
+
} | undefined;
|
|
116
|
+
/**
|
|
117
|
+
* Interior lighting (0069): the lighting stage scales sky ambient by a plan's sky visibility and
|
|
118
|
+
* occludes wall-blocked lights through polar rows, once a provider (structure's
|
|
119
|
+
* interiorLightingPlugin) turns a part on. Until then nothing links or binds differently.
|
|
120
|
+
*/
|
|
121
|
+
export declare const interiorPlugin: import("@aethervtt/shard-runtime").Plugin;
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { defineResource } from "@aethervtt/shard-core";
|
|
2
|
+
import { GpuBuffer } from "@aethervtt/shard-gpu";
|
|
3
|
+
import { definePlugin } from "@aethervtt/shard-runtime";
|
|
4
|
+
import { addRenderFeatures } from "./features.js";
|
|
5
|
+
import {
|
|
6
|
+
INTERIOR_BINDINGS,
|
|
7
|
+
INTERIOR_BLOCKED,
|
|
8
|
+
INTERIOR_SKY,
|
|
9
|
+
InteriorPath
|
|
10
|
+
} from "./interior.js";
|
|
11
|
+
import { Gpu, RenderDescribers, Shaders } from "./plugin.js";
|
|
12
|
+
import { registerShaders } from "./shaders.js";
|
|
13
|
+
import { bindingDimension } from "./tier.js";
|
|
14
|
+
const INTERIOR_LEVELS = 8;
|
|
15
|
+
const INTERIOR_GROUND = INTERIOR_LEVELS;
|
|
16
|
+
const TABLE_FLOATS = 12 + (INTERIOR_LEVELS + 1) * 8;
|
|
17
|
+
const LEVELS_AT = 12;
|
|
18
|
+
const AMBIENT_AT = 12 + (INTERIOR_LEVELS + 1) * 4;
|
|
19
|
+
const COVER_SCALE = 64;
|
|
20
|
+
const COVER_ZERO = 32768;
|
|
21
|
+
const VISIBILITY_MAX = 16383;
|
|
22
|
+
const LINK_X = 1;
|
|
23
|
+
const LINK_Z = 2;
|
|
24
|
+
const ROW_SCALE = 256;
|
|
25
|
+
const ROW_ZERO = 32768;
|
|
26
|
+
const ROW_CLEAR = 2147549183;
|
|
27
|
+
const INTERIOR_SHADERS = {
|
|
28
|
+
"shard::interior": `
|
|
29
|
+
import shard::pbr::lights::{ Light, LIGHT_SPOT };
|
|
30
|
+
|
|
31
|
+
struct Interior {
|
|
32
|
+
/** The field's origin (x, z, m), 1 / texel, and how many levels the table holds. */
|
|
33
|
+
field: vec4f,
|
|
34
|
+
/** The field's size (texels x, z), bins per light row, filter taps. */
|
|
35
|
+
size: vec4f,
|
|
36
|
+
/** Where rows start: after the field's layers (x), \`y\` rows to a layer. */
|
|
37
|
+
rows: vec4f,
|
|
38
|
+
/**
|
|
39
|
+
* Levels by elevation: floor y, top y, field layer, the y cover heights count from. Entry 8 is
|
|
40
|
+
* the ground level's, for any y no level holds.
|
|
41
|
+
*/
|
|
42
|
+
levels: array<vec4f, 9>,
|
|
43
|
+
/** Per entry: interior ambient (rgb, cd/m\xB2), and the share of the sky's ambient kept indoors. */
|
|
44
|
+
ambient: array<vec4f, 9>,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The field's layers, then the light rows. */
|
|
48
|
+
@group(0) @binding(16) var interior_data: texture_2d_array<u32>;
|
|
49
|
+
@group(0) @binding(17) var<uniform> interior: Interior;
|
|
50
|
+
|
|
51
|
+
const INTERIOR_GROUND: u32 = 8u;
|
|
52
|
+
/** How far a blocked light's receiver moves off its surface, so each face reads its own side. */
|
|
53
|
+
const INTERIOR_OFFSET: f32 = 0.05;
|
|
54
|
+
|
|
55
|
+
/** The table entry whose [floor, top) holds y, or the ground level's. */
|
|
56
|
+
fn interior_level(y: f32) -> u32 {
|
|
57
|
+
let count = u32(interior.field.w);
|
|
58
|
+
for (var i = 0u; i < count; i++) {
|
|
59
|
+
let e = interior.levels[i];
|
|
60
|
+
if (y >= e.x && y < e.y) { return i; }
|
|
61
|
+
}
|
|
62
|
+
return INTERIOR_GROUND;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn interior_visibility(texel: u32) -> f32 {
|
|
66
|
+
return f32((texel & 0xffffu) >> 2u) / ${VISIBILITY_MAX}.0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Sky visibility at a point on a surface: 1 outdoors (outside the field, on an uncovered texel, or
|
|
71
|
+
* above its cover), else the field, filtered between the texel centres the point reaches without
|
|
72
|
+
* crossing a barrier, so light never bleeds through a wall. The point moves a texel along the
|
|
73
|
+
* normal first: a wall's inside face reads the room, not the outdoor texel next to it.
|
|
74
|
+
*/
|
|
75
|
+
fn sky_visibility(world: vec3f, n: vec3f, level: u32) -> f32 {
|
|
76
|
+
let p = world + n / interior.field.z;
|
|
77
|
+
let entry = interior.levels[level];
|
|
78
|
+
let size = vec2i(interior.size.xy);
|
|
79
|
+
let t = (p.xz - interior.field.xy) * interior.field.z - 0.5;
|
|
80
|
+
let home = vec2i(floor(t + 0.5));
|
|
81
|
+
if (any(home < vec2i(0)) || any(home >= size)) { return 1.0; }
|
|
82
|
+
let layer = i32(entry.z);
|
|
83
|
+
let own = textureLoad(interior_data, home, layer, 0).r;
|
|
84
|
+
let cover = own >> 16u;
|
|
85
|
+
if (cover == 0u) { return 1.0; }
|
|
86
|
+
if (p.y >= entry.w + (f32(cover) - ${COVER_ZERO}.0) / ${COVER_SCALE}.0) { return 1.0; }
|
|
87
|
+
// One filter tap (quality low): the texel alone.
|
|
88
|
+
if (interior.size.w < 1.5) { return interior_visibility(own); }
|
|
89
|
+
let f = t - floor(t);
|
|
90
|
+
let i0 = clamp(vec2i(floor(t)), vec2i(0), size - 1);
|
|
91
|
+
let i1 = min(i0 + 1, size - 1);
|
|
92
|
+
let a = textureLoad(interior_data, i0, layer, 0).r;
|
|
93
|
+
let b = textureLoad(interior_data, vec2i(i1.x, i0.y), layer, 0).r;
|
|
94
|
+
let c = textureLoad(interior_data, vec2i(i0.x, i1.y), layer, 0).r;
|
|
95
|
+
let d = textureLoad(interior_data, i1, layer, 0).r;
|
|
96
|
+
let hx = f.x >= 0.5;
|
|
97
|
+
let hz = f.y >= 0.5;
|
|
98
|
+
// Open links: along x on rows 0 and 1, along z on columns 0 and 1.
|
|
99
|
+
let ab = (a & ${LINK_X}u) == 0u;
|
|
100
|
+
let cd = (c & ${LINK_X}u) == 0u;
|
|
101
|
+
let ac = (a & ${LINK_Z}u) == 0u;
|
|
102
|
+
let bd = (b & ${LINK_Z}u) == 0u;
|
|
103
|
+
let x_home = select(ab, cd, hz);
|
|
104
|
+
let x_other = select(cd, ab, hz);
|
|
105
|
+
let z_home = select(ac, bd, hx);
|
|
106
|
+
let z_other = select(bd, ac, hx);
|
|
107
|
+
let across = select(0.0, 1.0, x_home);
|
|
108
|
+
let along = select(0.0, 1.0, z_home);
|
|
109
|
+
let diagonal = select(0.0, 1.0, (x_home && z_other) || (z_home && x_other));
|
|
110
|
+
// Weights of the home texel, its row neighbour, its column neighbour, and the diagonal one.
|
|
111
|
+
let wx = select(f.x, 1.0 - f.x, hx);
|
|
112
|
+
let wz = select(f.y, 1.0 - f.y, hz);
|
|
113
|
+
let w_home = (1.0 - wx) * (1.0 - wz);
|
|
114
|
+
let w_row = wx * (1.0 - wz) * across;
|
|
115
|
+
let w_col = (1.0 - wx) * wz * along;
|
|
116
|
+
let w_diag = wx * wz * diagonal;
|
|
117
|
+
let row = select(select(b, a, hx), select(d, c, hx), hz);
|
|
118
|
+
let col = select(select(c, d, hx), select(a, b, hx), hz);
|
|
119
|
+
let diag = select(select(d, c, hx), select(b, a, hx), hz);
|
|
120
|
+
let sum = w_home * interior_visibility(own) + w_row * interior_visibility(row)
|
|
121
|
+
+ w_col * interior_visibility(col) + w_diag * interior_visibility(diag);
|
|
122
|
+
return sum / (w_home + w_row + w_col + w_diag);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Ambient light under the plan's cover: \`sky\` (the uniform ambient or the environment's light,
|
|
127
|
+
* already shaded) scaled by sky visibility s, plus the level's interior ambient on \`albedo\`
|
|
128
|
+
* (diffuse colour times occlusion) where the sky doesn't reach. Indoors the sky keeps the level's
|
|
129
|
+
* fill share: mix(fill \xD7 sky + interior, sky, s).
|
|
130
|
+
*/
|
|
131
|
+
fn interior_ambient(sky: vec3f, albedo: vec3f, world: vec3f, n: vec3f) -> vec3f {
|
|
132
|
+
let level = interior_level(world.y + n.y / interior.field.z);
|
|
133
|
+
let s = sky_visibility(world, n, level);
|
|
134
|
+
let a = interior.ambient[level];
|
|
135
|
+
return sky * (s + (1.0 - s) * a.w) + albedo * a.rgb * (1.0 - s);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A direction's place around the circle, 0 to 1 from \u2212x counter-clockwise (+x at \xBD): a pseudo-angle
|
|
140
|
+
* (one division, not an atan2), monotonic in the angle. Rows bin by it (interior.ts's binOf).
|
|
141
|
+
*/
|
|
142
|
+
fn interior_bin(v: vec2f) -> f32 {
|
|
143
|
+
let t = v.y / (abs(v.x) + abs(v.y));
|
|
144
|
+
// 0 to 4 counter-clockwise from +x: t in the first quadrant, 2 \u2212 t left of the z axis, 4 + t
|
|
145
|
+
// in the fourth quadrant.
|
|
146
|
+
let p = select(select(4.0 + t, t, t >= 0.0), 2.0 - t, v.x < 0.0);
|
|
147
|
+
return fract(p * 0.25 + 0.5);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Where wall-blocked lights (0069) test a fragment: moved off its surface along the normal (xyz),
|
|
152
|
+
* and its level (w). Once per fragment, before the light loop.
|
|
153
|
+
*/
|
|
154
|
+
fn interior_receiver(world: vec3f, n: vec3f) -> vec4f {
|
|
155
|
+
let p = world + n * INTERIOR_OFFSET;
|
|
156
|
+
return vec4f(p, f32(interior_level(p.y)));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* How much of a wall-blocked light reaches a receiver (\`interior_receiver\`): 0 on another
|
|
161
|
+
* level, else the share of the bins around its direction whose barrier it isn't behind, or whose
|
|
162
|
+
* top the light passes over. A light without a row (its row word is 0) is unblocked.
|
|
163
|
+
*/
|
|
164
|
+
fn wall_visibility(light: Light, receiver: vec4f) -> f32 {
|
|
165
|
+
// Row + 1, in a word this kind never reads (LIGHT_ROW_WORD): a point's spot scale, a spot's bright.
|
|
166
|
+
let row = u32(select(light.spot_scale, light.bright, light.kind == LIGHT_SPOT));
|
|
167
|
+
if (row == 0u) { return 1.0; }
|
|
168
|
+
let p = receiver.xyz;
|
|
169
|
+
if (u32(receiver.w) != interior_level(light.position.y)) { return 0.0; }
|
|
170
|
+
let to = p.xz - light.position.xz;
|
|
171
|
+
let d = length(to);
|
|
172
|
+
if (d < 1e-4) { return 1.0; }
|
|
173
|
+
let bins = i32(interior.size.z);
|
|
174
|
+
let taps = i32(interior.size.w);
|
|
175
|
+
let at = i32(floor(interior_bin(to) * f32(bins)));
|
|
176
|
+
let r = i32(row) - 1;
|
|
177
|
+
let h = i32(interior.rows.y);
|
|
178
|
+
let first = i32(interior.rows.x);
|
|
179
|
+
var lit = 0.0;
|
|
180
|
+
for (var k = -(taps / 2); k <= taps / 2; k++) {
|
|
181
|
+
let v = textureLoad(interior_data, vec2i((at + k + bins) % bins, r % h), first + r / h, 0).r;
|
|
182
|
+
let r = f32(v & 0xffffu) / ${ROW_SCALE}.0;
|
|
183
|
+
let top = light.position.y + (f32(v >> 16u) - ${ROW_ZERO}.0) / ${ROW_SCALE}.0;
|
|
184
|
+
// Behind the barrier, unless the line from the light clears its top there.
|
|
185
|
+
let y_at = light.position.y + (p.y - light.position.y) * r / d;
|
|
186
|
+
lit += select(1.0, 0.0, d > r && y_at <= top);
|
|
187
|
+
}
|
|
188
|
+
return lit / f32(taps);
|
|
189
|
+
}`
|
|
190
|
+
};
|
|
191
|
+
class InteriorLighting {
|
|
192
|
+
sky = false;
|
|
193
|
+
blocked = false;
|
|
194
|
+
provided = false;
|
|
195
|
+
version = 0;
|
|
196
|
+
/**
|
|
197
|
+
* Set when the texture is made again (a size changed, or the device was lost): the provider
|
|
198
|
+
* writes the field and every row again, then clears it.
|
|
199
|
+
*/
|
|
200
|
+
stale = false;
|
|
201
|
+
/** The field's size in texels and layers, and the rows' bins and count (0: none). */
|
|
202
|
+
fieldWidth = 0;
|
|
203
|
+
fieldHeight = 0;
|
|
204
|
+
fieldLayers = 0;
|
|
205
|
+
rowBins = 0;
|
|
206
|
+
rowCount = 0;
|
|
207
|
+
/** The texture's size: rows sit `height` to a layer after the field's layers. */
|
|
208
|
+
width = 0;
|
|
209
|
+
height = 0;
|
|
210
|
+
layers = 0;
|
|
211
|
+
/** The table as written; `writeTable` uploads it when it changed. */
|
|
212
|
+
table = new Float32Array(TABLE_FLOATS);
|
|
213
|
+
sent = new Float32Array(TABLE_FLOATS);
|
|
214
|
+
tableStale = true;
|
|
215
|
+
texture;
|
|
216
|
+
generation = -1;
|
|
217
|
+
placeholder;
|
|
218
|
+
buffer;
|
|
219
|
+
gpu;
|
|
220
|
+
bound;
|
|
221
|
+
constructor(gpu) {
|
|
222
|
+
this.gpu = gpu;
|
|
223
|
+
}
|
|
224
|
+
get mode() {
|
|
225
|
+
return (this.sky ? INTERIOR_SKY : 0) | (this.blocked ? INTERIOR_BLOCKED : 0);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Sizes the texture for a field of `fieldLayers` layers (0: none) and `rows` rows of `bins` bins
|
|
229
|
+
* (0: none). Made again, and `stale`, only when a size changes or the device was lost.
|
|
230
|
+
*/
|
|
231
|
+
configure(fieldWidth, fieldHeight, fieldLayers, bins, rows) {
|
|
232
|
+
const lost = this.texture !== void 0 && this.generation !== this.gpu.generation;
|
|
233
|
+
if (!lost && fieldWidth === this.fieldWidth && fieldHeight === this.fieldHeight && fieldLayers === this.fieldLayers && bins === this.rowBins && rows === this.rowCount)
|
|
234
|
+
return;
|
|
235
|
+
this.texture?.destroy();
|
|
236
|
+
this.texture = void 0;
|
|
237
|
+
this.fieldWidth = fieldWidth;
|
|
238
|
+
this.fieldHeight = fieldHeight;
|
|
239
|
+
this.fieldLayers = fieldLayers;
|
|
240
|
+
this.rowBins = bins;
|
|
241
|
+
this.rowCount = rows;
|
|
242
|
+
const height = fieldLayers > 0 ? Math.max(fieldHeight, Math.ceil(rows / 64)) : Math.min(rows, 2048);
|
|
243
|
+
this.width = Math.max(1, fieldWidth, bins);
|
|
244
|
+
this.height = Math.max(1, height);
|
|
245
|
+
this.layers = fieldLayers + (rows > 0 ? Math.ceil(rows / this.height) : 0);
|
|
246
|
+
const t = this.table;
|
|
247
|
+
t[8] = fieldLayers;
|
|
248
|
+
t[9] = this.height;
|
|
249
|
+
this.version++;
|
|
250
|
+
if (this.layers === 0) return;
|
|
251
|
+
this.texture = this.gpu.device.createTexture({
|
|
252
|
+
label: "interior/data",
|
|
253
|
+
size: { width: this.width, height: this.height, depthOrArrayLayers: this.layers },
|
|
254
|
+
format: "r32uint",
|
|
255
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
256
|
+
...bindingDimension(this.gpu, "2d-array")
|
|
257
|
+
});
|
|
258
|
+
this.generation = this.gpu.generation;
|
|
259
|
+
this.stale = true;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Whether the device was lost since the texture was made: makes it again (`stale`), so the
|
|
263
|
+
* provider writes everything once more. Providers check it each frame they're on.
|
|
264
|
+
*/
|
|
265
|
+
lost() {
|
|
266
|
+
if (this.texture === void 0 || this.generation === this.gpu.generation) return false;
|
|
267
|
+
const { fieldWidth, fieldHeight, fieldLayers, rowBins, rowCount } = this;
|
|
268
|
+
this.configure(fieldWidth, fieldHeight, fieldLayers, rowBins, rowCount);
|
|
269
|
+
this.tableStale = true;
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Writes a rectangle of field layer `layer` from `data`, a whole layer's texels (fieldWidth ×
|
|
274
|
+
* fieldHeight, row by row): only the rectangle uploads.
|
|
275
|
+
*/
|
|
276
|
+
writeField(layer, x, z, w, h, data) {
|
|
277
|
+
if (!this.texture || w <= 0 || h <= 0 || layer >= this.fieldLayers) return;
|
|
278
|
+
this.gpu.device.queue.writeTexture(
|
|
279
|
+
{ texture: this.texture, origin: { x, y: z, z: layer } },
|
|
280
|
+
data,
|
|
281
|
+
{
|
|
282
|
+
// Relative to the view's start, as WebGPU reads a typed array.
|
|
283
|
+
offset: (z * this.fieldWidth + x) * 4,
|
|
284
|
+
bytesPerRow: this.fieldWidth * 4,
|
|
285
|
+
rowsPerImage: h
|
|
286
|
+
},
|
|
287
|
+
{ width: w, height: h, depthOrArrayLayers: 1 }
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
/** Writes row `row` from `data` (`rowBins` bins). */
|
|
291
|
+
writeRow(row, data) {
|
|
292
|
+
if (!this.texture || row >= this.rowCount) return;
|
|
293
|
+
this.gpu.device.queue.writeTexture(
|
|
294
|
+
{
|
|
295
|
+
texture: this.texture,
|
|
296
|
+
origin: { x: 0, y: row % this.height, z: this.fieldLayers + Math.floor(row / this.height) }
|
|
297
|
+
},
|
|
298
|
+
data,
|
|
299
|
+
{ offset: 0, bytesPerRow: this.rowBins * 4 },
|
|
300
|
+
{ width: this.rowBins, height: 1 }
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Sets the table's field and size: origin (x, z), texel (m), field size, bins per row, filter
|
|
305
|
+
* taps. `writeTable` uploads.
|
|
306
|
+
*/
|
|
307
|
+
setGrid(originX, originZ, texel, width, height, bins, taps) {
|
|
308
|
+
const t = this.table;
|
|
309
|
+
t[0] = originX;
|
|
310
|
+
t[1] = originZ;
|
|
311
|
+
t[2] = 1 / texel;
|
|
312
|
+
t[4] = width;
|
|
313
|
+
t[5] = height;
|
|
314
|
+
t[6] = bins;
|
|
315
|
+
t[7] = taps;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Sets level `i`'s entry (`INTERIOR_GROUND` for the ground level): its [floor, top) y, field
|
|
319
|
+
* layer, the y its cover heights count from, its interior ambient (rgb, cd/m²) and fill share.
|
|
320
|
+
*/
|
|
321
|
+
setLevel(i, floor, top, layer, base, ambient, fill) {
|
|
322
|
+
const t = this.table;
|
|
323
|
+
const l = LEVELS_AT + i * 4;
|
|
324
|
+
t[l] = floor;
|
|
325
|
+
t[l + 1] = top;
|
|
326
|
+
t[l + 2] = layer;
|
|
327
|
+
t[l + 3] = base;
|
|
328
|
+
const a = AMBIENT_AT + i * 4;
|
|
329
|
+
t[a] = ambient[0];
|
|
330
|
+
t[a + 1] = ambient[1];
|
|
331
|
+
t[a + 2] = ambient[2];
|
|
332
|
+
t[a + 3] = fill;
|
|
333
|
+
}
|
|
334
|
+
/** How many levels (besides the ground's) the table holds. */
|
|
335
|
+
setLevelCount(count) {
|
|
336
|
+
this.table[3] = Math.min(count, INTERIOR_LEVELS);
|
|
337
|
+
}
|
|
338
|
+
/** Uploads the table if it changed since the last upload. */
|
|
339
|
+
writeTable() {
|
|
340
|
+
const t = this.table;
|
|
341
|
+
let same = !this.tableStale && this.buffer !== void 0;
|
|
342
|
+
for (let i = 0; same && i < t.length; i++) if (!Object.is(t[i], this.sent[i])) same = false;
|
|
343
|
+
if (same) return;
|
|
344
|
+
if (!this.buffer) {
|
|
345
|
+
this.buffer = new GpuBuffer(this.gpu, {
|
|
346
|
+
label: "interior/table",
|
|
347
|
+
usage: GPUBufferUsage.UNIFORM,
|
|
348
|
+
size: TABLE_FLOATS * 4
|
|
349
|
+
});
|
|
350
|
+
this.version++;
|
|
351
|
+
}
|
|
352
|
+
this.buffer.write(t);
|
|
353
|
+
this.sent.set(t);
|
|
354
|
+
this.tableStale = false;
|
|
355
|
+
}
|
|
356
|
+
/** GPU bytes the texture holds. */
|
|
357
|
+
get bytes() {
|
|
358
|
+
return this.texture ? this.width * this.height * this.layers * 4 : 0;
|
|
359
|
+
}
|
|
360
|
+
entries(gpu) {
|
|
361
|
+
if (!this.buffer) this.writeTable();
|
|
362
|
+
const buffer = this.buffer;
|
|
363
|
+
const gpuBuffer = buffer.buffer;
|
|
364
|
+
const version = this.version * 4096 + buffer.version;
|
|
365
|
+
if (this.bound?.version === version) return this.bound.entries;
|
|
366
|
+
let texture = this.texture;
|
|
367
|
+
if (!texture || this.generation !== gpu.generation) {
|
|
368
|
+
if (!this.placeholder || this.placeholder.generation !== gpu.generation) {
|
|
369
|
+
this.placeholder = {
|
|
370
|
+
texture: gpu.device.createTexture({
|
|
371
|
+
label: "interior/placeholder",
|
|
372
|
+
size: { width: 1, height: 1, depthOrArrayLayers: 1 },
|
|
373
|
+
format: "r32uint",
|
|
374
|
+
usage: GPUTextureUsage.TEXTURE_BINDING,
|
|
375
|
+
...bindingDimension(gpu, "2d-array")
|
|
376
|
+
}),
|
|
377
|
+
generation: gpu.generation
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
texture = this.placeholder.texture;
|
|
381
|
+
}
|
|
382
|
+
const entries = [
|
|
383
|
+
{ binding: INTERIOR_BINDINGS.data, resource: texture.createView({ dimension: "2d-array" }) },
|
|
384
|
+
{ binding: INTERIOR_BINDINGS.table, resource: { buffer: gpuBuffer } }
|
|
385
|
+
];
|
|
386
|
+
this.bound = { version, entries };
|
|
387
|
+
return entries;
|
|
388
|
+
}
|
|
389
|
+
/** Frees the texture (both parts off). */
|
|
390
|
+
free() {
|
|
391
|
+
if (!this.texture) return;
|
|
392
|
+
this.texture.destroy();
|
|
393
|
+
this.texture = void 0;
|
|
394
|
+
this.fieldWidth = this.fieldHeight = this.fieldLayers = this.rowBins = this.rowCount = 0;
|
|
395
|
+
this.width = this.height = this.layers = 0;
|
|
396
|
+
this.version++;
|
|
397
|
+
}
|
|
398
|
+
/** Frees everything (the plugin's dispose). */
|
|
399
|
+
destroy() {
|
|
400
|
+
this.free();
|
|
401
|
+
this.placeholder?.texture.destroy();
|
|
402
|
+
this.buffer?.destroy();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const InteriorLightingResource = defineResource(
|
|
406
|
+
"render/InteriorLighting",
|
|
407
|
+
{
|
|
408
|
+
description: "Interior lighting's textures and level table (0069): sky visibility field, wall-blocked lights' rows. Structure's interiorLightingPlugin fills them."
|
|
409
|
+
}
|
|
410
|
+
);
|
|
411
|
+
function describeInterior(world) {
|
|
412
|
+
const s = world.tryResource(InteriorLightingResource);
|
|
413
|
+
if (!s) return void 0;
|
|
414
|
+
return {
|
|
415
|
+
sky: s.sky,
|
|
416
|
+
blockedLights: s.blocked,
|
|
417
|
+
provided: s.provided,
|
|
418
|
+
field: s.fieldLayers > 0 ? { width: s.fieldWidth, height: s.fieldHeight, layers: s.fieldLayers } : null,
|
|
419
|
+
rows: s.rowCount > 0 ? { bins: s.rowBins, rows: s.rowCount } : null,
|
|
420
|
+
bytes: s.bytes
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const interiorPlugin = definePlugin({
|
|
424
|
+
name: "render/interior",
|
|
425
|
+
dependencies: ["render/forward"],
|
|
426
|
+
provides: [InteriorLightingResource],
|
|
427
|
+
build() {
|
|
428
|
+
},
|
|
429
|
+
ready(app) {
|
|
430
|
+
const world = app.world;
|
|
431
|
+
const state = new InteriorLighting(world.resource(Gpu));
|
|
432
|
+
app.insertResource(InteriorLightingResource, state);
|
|
433
|
+
app.insertResource(InteriorPath, state);
|
|
434
|
+
registerShaders(world.resource(Shaders), INTERIOR_SHADERS);
|
|
435
|
+
world.initResource(RenderDescribers).set("interior", describeInterior);
|
|
436
|
+
addRenderFeatures(world, {
|
|
437
|
+
name: "render/interior",
|
|
438
|
+
description: "Sky visibility and wall-blocked lights from a structure plan.",
|
|
439
|
+
nodes: [],
|
|
440
|
+
baseline: {
|
|
441
|
+
strategy: "The same integer texture loads (texelFetch); the table a uniform block"
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
},
|
|
445
|
+
dispose(app) {
|
|
446
|
+
app.world.tryResource(InteriorLightingResource)?.destroy();
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
export {
|
|
450
|
+
COVER_SCALE,
|
|
451
|
+
COVER_ZERO,
|
|
452
|
+
INTERIOR_GROUND,
|
|
453
|
+
INTERIOR_LEVELS,
|
|
454
|
+
INTERIOR_SHADERS,
|
|
455
|
+
InteriorLighting,
|
|
456
|
+
InteriorLightingResource,
|
|
457
|
+
LINK_X,
|
|
458
|
+
LINK_Z,
|
|
459
|
+
ROW_CLEAR,
|
|
460
|
+
ROW_SCALE,
|
|
461
|
+
ROW_ZERO,
|
|
462
|
+
VISIBILITY_MAX,
|
|
463
|
+
describeInterior,
|
|
464
|
+
interiorPlugin
|
|
465
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { GpuContext } from '@aethervtt/shard-gpu';
|
|
2
|
+
/** `InteriorSupport.mode` bits. */
|
|
3
|
+
export declare const INTERIOR_SKY = 1;
|
|
4
|
+
export declare const INTERIOR_BLOCKED = 2;
|
|
5
|
+
/** The view group's bindings while a mode bit is on: the field and rows (one texture), the table. */
|
|
6
|
+
export declare const INTERIOR_BINDINGS: {
|
|
7
|
+
readonly data: 16;
|
|
8
|
+
readonly table: 17;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Set by `interiorPlugin`: what forward needs to link and bind the interior variants. With mode 0
|
|
12
|
+
* the view group and every shader are as they are without the plugin.
|
|
13
|
+
*/
|
|
14
|
+
export interface InteriorSupport {
|
|
15
|
+
/** INTERIOR_SKY | INTERIOR_BLOCKED this frame. */
|
|
16
|
+
readonly mode: number;
|
|
17
|
+
/** Whether something fills the field and rows (structure's interiorLightingPlugin). */
|
|
18
|
+
readonly provided: boolean;
|
|
19
|
+
/** Bumps when an entry's resource changes (a texture grows): the view bind group is rebuilt. */
|
|
20
|
+
readonly version: number;
|
|
21
|
+
/** The view group's extra entries (INTERIOR_BINDINGS): a placeholder until there's data. */
|
|
22
|
+
entries(gpu: GpuContext): readonly GPUBindGroupEntry[];
|
|
23
|
+
}
|
|
24
|
+
export declare const InteriorPath: import("@aethervtt/shard-core").ResourceDef<InteriorSupport>;
|
package/dist/interior.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineResource } from "@aethervtt/shard-core";
|
|
2
|
+
const INTERIOR_SKY = 1;
|
|
3
|
+
const INTERIOR_BLOCKED = 2;
|
|
4
|
+
const INTERIOR_BINDINGS = { data: 16, table: 17 };
|
|
5
|
+
const InteriorPath = defineResource("render/InteriorPath", {
|
|
6
|
+
description: "Present when interior lighting (interiorPlugin) is installed."
|
|
7
|
+
});
|
|
8
|
+
export {
|
|
9
|
+
INTERIOR_BINDINGS,
|
|
10
|
+
INTERIOR_BLOCKED,
|
|
11
|
+
INTERIOR_SKY,
|
|
12
|
+
InteriorPath
|
|
13
|
+
};
|
package/dist/layers.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which cameras draw this renderable: it draws in a view when `mask & Camera3d.layers` is
|
|
3
|
+
* nonzero. Absent means layer 1. Culling tests it first, so a hidden visual costs no draw.
|
|
4
|
+
*/
|
|
5
|
+
export declare const RenderLayers: import("@aethervtt/shard-core").ComponentDef<{
|
|
6
|
+
readonly mask: import("@aethervtt/shard-core").FieldType<number, "u16">;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* The default ground bands, bottom to top. A host may renumber them; only the order matters.
|
|
10
|
+
* Floors are opaque, write depth, and sit under every band.
|
|
11
|
+
*/
|
|
12
|
+
export declare const GROUND_BANDS: {
|
|
13
|
+
readonly tiles: 10;
|
|
14
|
+
readonly grid: 20;
|
|
15
|
+
readonly drawings: 30;
|
|
16
|
+
readonly 'tokens-flat': 40;
|
|
17
|
+
readonly fog: 50;
|
|
18
|
+
readonly overlay: 60;
|
|
19
|
+
};
|
|
20
|
+
export type GroundBand = keyof typeof GROUND_BANDS;
|
|
21
|
+
/**
|
|
22
|
+
* Bands from this one up draw after the transparent phase and projected fog (0058), so fog never
|
|
23
|
+
* covers them: selection rings, pings, measurement templates.
|
|
24
|
+
*/
|
|
25
|
+
export declare const OVERLAY_BAND: 60;
|
|
26
|
+
/**
|
|
27
|
+
* Coplanar content on a floor, drawn after opaque geometry in `(band, order)` order with the depth
|
|
28
|
+
* test on and depth writes off: walls and props in front hide it, and bands never fight each other.
|
|
29
|
+
* Ground renderables cast no shadows.
|
|
30
|
+
*/
|
|
31
|
+
export declare const GroundLayer: import("@aethervtt/shard-core").ComponentDef<{
|
|
32
|
+
readonly band: import("@aethervtt/shard-core").FieldType<number, "i16">;
|
|
33
|
+
readonly order: import("@aethervtt/shard-core").FieldType<number, "i32">;
|
|
34
|
+
readonly level: import("@aethervtt/shard-core").FieldType<number, "i8">;
|
|
35
|
+
}>;
|