@aethervtt/shard-render 0.0.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.
Files changed (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/dist/assets.d.ts +104 -0
  4. package/dist/assets.js +213 -0
  5. package/dist/atmosphere-model.d.ts +96 -0
  6. package/dist/atmosphere-model.js +370 -0
  7. package/dist/atmosphere-nodes.d.ts +55 -0
  8. package/dist/atmosphere-nodes.js +780 -0
  9. package/dist/atmosphere-plugin.d.ts +5 -0
  10. package/dist/atmosphere-plugin.js +42 -0
  11. package/dist/atmosphere-shaders.d.ts +7 -0
  12. package/dist/atmosphere-shaders.js +595 -0
  13. package/dist/atmosphere-state.d.ts +72 -0
  14. package/dist/atmosphere-state.js +18 -0
  15. package/dist/atmosphere.d.ts +191 -0
  16. package/dist/atmosphere.js +950 -0
  17. package/dist/camera.d.ts +83 -0
  18. package/dist/camera.js +163 -0
  19. package/dist/clusters.d.ts +68 -0
  20. package/dist/clusters.js +288 -0
  21. package/dist/culling-shaders.d.ts +2 -0
  22. package/dist/culling-shaders.js +203 -0
  23. package/dist/culling.d.ts +84 -0
  24. package/dist/culling.js +498 -0
  25. package/dist/debug-shaders.d.ts +3 -0
  26. package/dist/debug-shaders.js +207 -0
  27. package/dist/debug-views.d.ts +60 -0
  28. package/dist/debug-views.js +197 -0
  29. package/dist/deferred-plugin.d.ts +5 -0
  30. package/dist/deferred-plugin.js +20 -0
  31. package/dist/deferred-shaders.d.ts +2 -0
  32. package/dist/deferred-shaders.js +151 -0
  33. package/dist/deferred.d.ts +22 -0
  34. package/dist/deferred.js +296 -0
  35. package/dist/deform.d.ts +80 -0
  36. package/dist/deform.js +213 -0
  37. package/dist/describe-plugin.d.ts +5 -0
  38. package/dist/describe-plugin.js +19 -0
  39. package/dist/display-nodes.d.ts +5 -0
  40. package/dist/display-nodes.js +189 -0
  41. package/dist/dynamic-resolution.d.ts +47 -0
  42. package/dist/dynamic-resolution.js +191 -0
  43. package/dist/environment-plugin.d.ts +6 -0
  44. package/dist/environment-plugin.js +44 -0
  45. package/dist/environment-shaders.d.ts +2 -0
  46. package/dist/environment-shaders.js +260 -0
  47. package/dist/environment-state.d.ts +71 -0
  48. package/dist/environment-state.js +144 -0
  49. package/dist/environment.d.ts +70 -0
  50. package/dist/environment.js +529 -0
  51. package/dist/forward.d.ts +123 -0
  52. package/dist/forward.js +1216 -0
  53. package/dist/fxaa.d.ts +2 -0
  54. package/dist/fxaa.js +65 -0
  55. package/dist/gizmo-font.d.ts +21 -0
  56. package/dist/gizmo-font.js +132 -0
  57. package/dist/gizmos-plugin.d.ts +5 -0
  58. package/dist/gizmos-plugin.js +34 -0
  59. package/dist/gizmos.d.ts +156 -0
  60. package/dist/gizmos.js +772 -0
  61. package/dist/gpu-assets.d.ts +114 -0
  62. package/dist/gpu-assets.js +494 -0
  63. package/dist/graph.d.ts +204 -0
  64. package/dist/graph.js +417 -0
  65. package/dist/index.d.ts +53 -0
  66. package/dist/index.js +540 -0
  67. package/dist/instances.d.ts +349 -0
  68. package/dist/instances.js +1335 -0
  69. package/dist/large-world.d.ts +13 -0
  70. package/dist/large-world.js +20 -0
  71. package/dist/lights.d.ts +160 -0
  72. package/dist/lights.js +468 -0
  73. package/dist/material-noise.d.ts +19 -0
  74. package/dist/material-noise.js +7 -0
  75. package/dist/material-pipelines.d.ts +46 -0
  76. package/dist/material-pipelines.js +166 -0
  77. package/dist/materials.d.ts +108 -0
  78. package/dist/materials.js +219 -0
  79. package/dist/noise.d.ts +76 -0
  80. package/dist/noise.js +286 -0
  81. package/dist/overlay-registry.d.ts +18 -0
  82. package/dist/overlay-registry.js +30 -0
  83. package/dist/overlays.d.ts +35 -0
  84. package/dist/overlays.js +344 -0
  85. package/dist/paths.d.ts +20 -0
  86. package/dist/paths.js +46 -0
  87. package/dist/picking-plugin.d.ts +2 -0
  88. package/dist/picking-plugin.js +20 -0
  89. package/dist/picking.d.ts +94 -0
  90. package/dist/picking.js +456 -0
  91. package/dist/pixel-perfect-plugin.d.ts +9 -0
  92. package/dist/pixel-perfect-plugin.js +135 -0
  93. package/dist/pixel-perfect.d.ts +27 -0
  94. package/dist/pixel-perfect.js +70 -0
  95. package/dist/plugin.d.ts +111 -0
  96. package/dist/plugin.js +182 -0
  97. package/dist/pool.d.ts +19 -0
  98. package/dist/pool.js +43 -0
  99. package/dist/post-common.d.ts +51 -0
  100. package/dist/post-common.js +148 -0
  101. package/dist/post-nodes.d.ts +39 -0
  102. package/dist/post-nodes.js +849 -0
  103. package/dist/post-plugin.d.ts +5 -0
  104. package/dist/post-plugin.js +26 -0
  105. package/dist/post-shaders.d.ts +8 -0
  106. package/dist/post-shaders.js +698 -0
  107. package/dist/post.d.ts +175 -0
  108. package/dist/post.js +453 -0
  109. package/dist/readback.d.ts +4 -0
  110. package/dist/readback.js +95 -0
  111. package/dist/render-scale.d.ts +53 -0
  112. package/dist/render-scale.js +63 -0
  113. package/dist/shaders.d.ts +122 -0
  114. package/dist/shaders.js +1168 -0
  115. package/dist/shadows.d.ts +119 -0
  116. package/dist/shadows.js +562 -0
  117. package/dist/skin-asset.d.ts +26 -0
  118. package/dist/skin-asset.js +25 -0
  119. package/dist/skinning-plugin.d.ts +5 -0
  120. package/dist/skinning-plugin.js +16 -0
  121. package/dist/skinning.d.ts +19 -0
  122. package/dist/skinning.js +343 -0
  123. package/dist/standard-fields.d.ts +67 -0
  124. package/dist/standard-fields.js +73 -0
  125. package/dist/standard.d.ts +7 -0
  126. package/dist/standard.js +37 -0
  127. package/dist/stats.d.ts +25 -0
  128. package/dist/stats.js +18 -0
  129. package/dist/target.d.ts +55 -0
  130. package/dist/target.js +70 -0
  131. package/dist/testing.d.ts +34 -0
  132. package/dist/testing.js +126 -0
  133. package/dist/timer.d.ts +28 -0
  134. package/dist/timer.js +136 -0
  135. package/dist/view.d.ts +117 -0
  136. package/dist/view.js +389 -0
  137. package/dist/visibility.d.ts +10 -0
  138. package/dist/visibility.js +84 -0
  139. package/package.json +49 -0
@@ -0,0 +1,60 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ import type { GpuContext } from '@aethervtt/shard-gpu';
3
+ import type { CapturedBuffer } from './graph.js';
4
+ /** Debug views a camera can render instead of its normal image. */
5
+ export declare const DEBUG_VIEWS: {
6
+ readonly none: 0;
7
+ readonly clusters: 1;
8
+ readonly cascades: 2;
9
+ readonly lod: 3;
10
+ readonly culling: 0;
11
+ };
12
+ /** G-buffer channels, shown through the `gbuffer-debug` buffer (deferred or forward views). */
13
+ export declare const GBUFFER_VIEWS: readonly ['albedo', 'normal', 'roughness', 'metallic', 'emissive'];
14
+ export type DebugView = keyof typeof DEBUG_VIEWS | (typeof GBUFFER_VIEWS)[number];
15
+ /**
16
+ * Switches a camera's debug view; it stays until switched back to 'none'. Lighting views replace
17
+ * the image; G-buffer channels render into the `gbuffer-debug` buffer (see `captureBuffer`).
18
+ */
19
+ export declare function setDebugView(world: World, camera: Entity, view: DebugView): void;
20
+ /** The culling section of `render.describe`: per view, what was culled and what it cost. */
21
+ export declare function describeCulling(world: World): {
22
+ mode: string;
23
+ instances: number;
24
+ batches: number;
25
+ lodSets: number;
26
+ uploadedBytes: number;
27
+ cpuMs: {
28
+ total: number;
29
+ systems: Record<string, number>;
30
+ } | undefined;
31
+ gpuMs: number | undefined;
32
+ views: Record<string, unknown>;
33
+ } | undefined;
34
+ /** Copies one layer of a texture to the CPU as floats (depth in R). */
35
+ export declare function readTextureLayer(gpu: GpuContext, texture: GPUTexture, layer: number): Promise<CapturedBuffer>;
36
+ /** Reads a GPU buffer back to the CPU (tests and debugging). */
37
+ export declare function readBuffer(gpu: GpuContext, source: GPUBuffer, size: number): Promise<ArrayBuffer>;
38
+ /**
39
+ * A light's shadow map as floats (reversed-Z depth in R). Directional lights: the cascades of
40
+ * `camera` (default: the first camera view), `layer` = cascade. Spots: its map. Points: `layer` =
41
+ * face (+X, -X, +Y, -Y, +Z, -Z).
42
+ */
43
+ export declare function captureShadowMap(world: World, light: Entity, layer?: number, camera?: Entity): Promise<CapturedBuffer>;
44
+ /** The lighting section of `render.describe`: per view lights, clusters, shadows, uploads. */
45
+ export declare function describeLighting(world: World): {
46
+ directional: number;
47
+ shadowedDirectional: number | null;
48
+ pointAndSpot: number;
49
+ uploadedLights: number;
50
+ uploadedBytes: number;
51
+ shadowBudget: {
52
+ spots: string;
53
+ points: string;
54
+ overBudget: {
55
+ entity: number;
56
+ type: string;
57
+ }[];
58
+ };
59
+ views: Record<string, unknown>;
60
+ } | undefined;
@@ -0,0 +1,197 @@
1
+ import {
2
+ ProfilerResource,
3
+ ShardError
4
+ } from "@aethervtt/shard-core";
5
+ import { Culler } from "./culling.js";
6
+ import { ForwardStateResource } from "./forward.js";
7
+ import { Instances } from "./instances.js";
8
+ import { DirectionalLight, LightingSettings, Lights, PointLight, SpotLight } from "./lights.js";
9
+ import { Gpu, Views } from "./plugin.js";
10
+ import { BYTES_PER_TEXEL, toFloats } from "./readback.js";
11
+ import { ShadowsResource } from "./shadows.js";
12
+ import { Cameras, cameraOf } from "./view.js";
13
+ const DEBUG_VIEWS = { none: 0, clusters: 1, cascades: 2, lod: 3, culling: 0 };
14
+ const GBUFFER_VIEWS = ["albedo", "normal", "roughness", "metallic", "emissive"];
15
+ function setDebugView(world, camera, view) {
16
+ const cam = world.resource(Cameras).get(camera);
17
+ if (!cam) {
18
+ throw new ShardError("render/unknown-camera", `Entity ${camera} is not a rendered camera`, {
19
+ hint: "Pass a Camera3d entity that has rendered at least one frame."
20
+ });
21
+ }
22
+ const channel = GBUFFER_VIEWS.indexOf(view);
23
+ cam.gbufferDebug = channel;
24
+ cam.debug = channel >= 0 ? 0 : DEBUG_VIEWS[view];
25
+ if (view === "culling") {
26
+ cam.frozenFrustum = Float32Array.from(cam.frustum);
27
+ cam.frozenPosition = Float32Array.from(cam.position);
28
+ } else {
29
+ cam.frozenFrustum = void 0;
30
+ cam.frozenPosition = void 0;
31
+ }
32
+ }
33
+ const PREP_SYSTEMS = ["render/prepare-instances", "render/forward-queue", "render/upload-visible"];
34
+ function prepMs(profiler) {
35
+ if (!profiler) return void 0;
36
+ const systems = {};
37
+ let total = 0;
38
+ for (const name of PREP_SYSTEMS) {
39
+ const ms = profiler.timing(name)?.avg ?? 0;
40
+ systems[name] = ms;
41
+ total += ms;
42
+ }
43
+ return { total, systems };
44
+ }
45
+ function describeCulling(world) {
46
+ const store = world.tryResource(Instances);
47
+ const culler = world.tryResource(Culler);
48
+ const state = world.tryResource(ForwardStateResource);
49
+ if (!store || !culler || !state) return void 0;
50
+ const profiler = world.tryResource(ProfilerResource);
51
+ const views = {};
52
+ for (const view of world.resource(Views).list) {
53
+ const cam = cameraOf(view);
54
+ if (!cam) continue;
55
+ const lods = culler.lodCounts.get(cam.draws);
56
+ views[view.name] = {
57
+ mode: cam.draws.cullView >= 0 ? "gpu" : "cpu",
58
+ batches: cam.draws.length,
59
+ visible: cam.draws.visible + cam.forwardOnly.visible + cam.transparent.visible,
60
+ perLod: lods ? [...lods].map((n) => n ?? 0) : void 0,
61
+ frozen: cam.frozenFrustum !== void 0,
62
+ shadowViews: state.views.get(view.name)?.cascades.count ?? 0
63
+ };
64
+ }
65
+ return {
66
+ mode: culler.active ? "gpu" : "cpu",
67
+ instances: store.live,
68
+ batches: store.batches.filter((b) => b.count > 0).length,
69
+ lodSets: store.lodSets.length,
70
+ uploadedBytes: store.uploadedBytes,
71
+ cpuMs: prepMs(profiler),
72
+ gpuMs: profiler?.timing("gpu:instance-cull")?.avg,
73
+ views
74
+ };
75
+ }
76
+ async function readTextureLayer(gpu, texture, layer) {
77
+ const { width, height, format } = texture;
78
+ const bpp = BYTES_PER_TEXEL[format] ?? 4;
79
+ const bytesPerRow = Math.ceil(width * bpp / 256) * 256;
80
+ const buffer = gpu.device.createBuffer({
81
+ label: "capture/layer",
82
+ size: bytesPerRow * height,
83
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
84
+ });
85
+ const encoder = gpu.device.createCommandEncoder({ label: "capture/layer" });
86
+ encoder.copyTextureToBuffer(
87
+ {
88
+ texture,
89
+ origin: { x: 0, y: 0, z: layer },
90
+ aspect: format.startsWith("depth") ? "depth-only" : "all"
91
+ },
92
+ { buffer, bytesPerRow },
93
+ [width, height, 1]
94
+ );
95
+ gpu.device.queue.submit([encoder.finish()]);
96
+ await buffer.mapAsync(GPUMapMode.READ);
97
+ const src = buffer.getMappedRange().slice(0);
98
+ buffer.unmap();
99
+ buffer.destroy();
100
+ const data = new Float32Array(width * height * 4);
101
+ for (let y = 0; y < height; y++) {
102
+ toFloats(src.slice(y * bytesPerRow, (y + 1) * bytesPerRow), format, width, data, y * width * 4);
103
+ }
104
+ return { width, height, format, data };
105
+ }
106
+ async function readBuffer(gpu, source, size) {
107
+ const buffer = gpu.device.createBuffer({
108
+ label: "readback",
109
+ size,
110
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
111
+ });
112
+ const encoder = gpu.device.createCommandEncoder({ label: "readback" });
113
+ encoder.copyBufferToBuffer(source, 0, buffer, 0, size);
114
+ gpu.device.queue.submit([encoder.finish()]);
115
+ await buffer.mapAsync(GPUMapMode.READ);
116
+ const out = buffer.getMappedRange().slice(0);
117
+ buffer.unmap();
118
+ buffer.destroy();
119
+ return out;
120
+ }
121
+ async function captureShadowMap(world, light, layer = 0, camera) {
122
+ const gpu = world.resource(Gpu);
123
+ const lights = world.resource(Lights);
124
+ const local = world.resource(ShadowsResource).local;
125
+ const noShadow = (why) => new ShardError("render/no-shadow-map", `Light ${light} has no shadow map: ${why}`, {
126
+ hint: "Set shadows: true on the light, and check render.describe for the shadow budget."
127
+ });
128
+ if (world.isAlive(light) && world.has(light, DirectionalLight)) {
129
+ if (lights.shadowSun?.entity !== light) throw noShadow("not the shadowed directional light");
130
+ const state = world.resource(ForwardStateResource);
131
+ const viewName = camera !== void 0 ? `camera:${camera}` : world.resource(Views).list.find((v) => cameraOf(v))?.name;
132
+ const pv = viewName ? state.views.get(viewName) : void 0;
133
+ if (!pv?.cascades.texture || layer >= pv.cascades.count) throw noShadow("no cascades rendered");
134
+ return readTextureLayer(gpu, pv.cascades.texture, layer);
135
+ }
136
+ const r = lights.byEntity.get(light);
137
+ if (!r) throw noShadow("not a light");
138
+ if (r.shadowIndex < 0) throw noShadow("no shadow map this frame (budget or shadows off)");
139
+ if (r.kind === 1) return readTextureLayer(gpu, local.spotTexture, r.shadowIndex);
140
+ return readTextureLayer(gpu, local.pointTexture, r.shadowIndex * 6 + Math.min(5, layer));
141
+ }
142
+ function describeLighting(world) {
143
+ const lights = world.tryResource(Lights);
144
+ const state = world.tryResource(ForwardStateResource);
145
+ const shadows = world.tryResource(ShadowsResource);
146
+ if (!lights || !state || !shadows) return void 0;
147
+ const settings = world.resource(LightingSettings);
148
+ const local = shadows.local;
149
+ const views = {};
150
+ for (const view of world.resource(Views).list) {
151
+ const cam = cameraOf(view);
152
+ const pv = state.views.get(view.name);
153
+ if (!cam || !pv) continue;
154
+ views[view.name] = {
155
+ lights: pv.lightList.records.map((r) => ({
156
+ entity: r.entity,
157
+ type: r.kind === 1 ? "spot" : "point",
158
+ intensity: r.intensity,
159
+ range: r.range,
160
+ castsShadows: r.shadowIndex >= 0
161
+ })),
162
+ clusters: {
163
+ grid: [16, 9, 24],
164
+ maxLightsPerCluster: pv.clusters.latest.maxLightsPerCluster,
165
+ overflows: pv.clusters.latest.overflows
166
+ },
167
+ cascades: pv.cascades.count,
168
+ cascadeSplits: [...pv.cascades.splits.subarray(0, pv.cascades.count)]
169
+ };
170
+ }
171
+ return {
172
+ directional: lights.directionalCount,
173
+ shadowedDirectional: lights.shadowSun?.entity ?? null,
174
+ pointAndSpot: lights.byEntity.size,
175
+ uploadedLights: lights.uploadedLights,
176
+ uploadedBytes: lights.uploadedBytes,
177
+ shadowBudget: {
178
+ spots: `${local.spots.length}/${settings.maxShadowedSpots}`,
179
+ points: `${local.points.length}/${settings.maxShadowedPoints}`,
180
+ overBudget: local.overBudget.map((r) => ({
181
+ entity: r.entity,
182
+ type: world.has(r.entity, SpotLight) ? "spot" : world.has(r.entity, PointLight) ? "point" : "unknown"
183
+ }))
184
+ },
185
+ views
186
+ };
187
+ }
188
+ export {
189
+ DEBUG_VIEWS,
190
+ GBUFFER_VIEWS,
191
+ captureShadowMap,
192
+ describeCulling,
193
+ describeLighting,
194
+ readBuffer,
195
+ readTextureLayer,
196
+ setDebugView
197
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The deferred path (spec 0021): cameras with `RenderPath { mode: 'deferred' }` shade opaque
3
+ * geometry through a G-buffer. Without it they render forward.
4
+ */
5
+ export declare const deferredPlugin: import("@aethervtt/shard-runtime").Plugin;
@@ -0,0 +1,20 @@
1
+ import { definePlugin } from "@aethervtt/shard-runtime";
2
+ import { addDeferredNodes } from "./deferred.js";
3
+ import { DEFERRED_SHADERS } from "./deferred-shaders.js";
4
+ import { Shaders } from "./plugin.js";
5
+ import { registerShaders } from "./shaders.js";
6
+ import { DeferredPath } from "./view.js";
7
+ const deferredPlugin = definePlugin({
8
+ name: "render/deferred",
9
+ dependencies: ["render/forward"],
10
+ build(app) {
11
+ app.insertResource(DeferredPath, { installed: true });
12
+ },
13
+ ready(app) {
14
+ registerShaders(app.world.resource(Shaders), DEFERRED_SHADERS);
15
+ addDeferredNodes(app);
16
+ }
17
+ });
18
+ export {
19
+ deferredPlugin
20
+ };
@@ -0,0 +1,2 @@
1
+ /** WGSL for the deferred path: G-buffer packing, the G-buffer pass, and the lighting pass. */
2
+ export declare const DEFERRED_SHADERS: Record<string, string>;
@@ -0,0 +1,151 @@
1
+ const DEFERRED_SHADERS = {
2
+ "shard::pbr::gbuffer": `
3
+ import shard::pbr::types::PbrInput;
4
+
5
+ /**
6
+ * The G-buffer layout, in one place:
7
+ * gbuffer0 (rgba8unorm-srgb): base color, occlusion
8
+ * gbuffer1 (rgba16float): octahedral normal (xy), roughness, metallic (+2 when shadows are off)
9
+ * gbuffer2 (rg11b10ufloat): emissive, pre-exposed
10
+ */
11
+ struct GBufferOutput {
12
+ @location(0) albedo: vec4f,
13
+ @location(1) normal: vec4f,
14
+ @location(2) emissive: vec4f,
15
+ }
16
+
17
+ fn oct_wrap(v: vec2f) -> vec2f {
18
+ return (1.0 - abs(v.yx)) * select(vec2f(-1.0), vec2f(1.0), v >= vec2f(0.0));
19
+ }
20
+
21
+ fn oct_encode(n: vec3f) -> vec2f {
22
+ let p = n.xy / (abs(n.x) + abs(n.y) + abs(n.z));
23
+ return select(p, oct_wrap(p), n.z < 0.0);
24
+ }
25
+
26
+ fn oct_decode(e: vec2f) -> vec3f {
27
+ var n = vec3f(e, 1.0 - abs(e.x) - abs(e.y));
28
+ let t = max(-n.z, 0.0);
29
+ n.x += select(t, -t, n.x >= 0.0);
30
+ n.y += select(t, -t, n.y >= 0.0);
31
+ return normalize(n);
32
+ }
33
+
34
+ fn pack_gbuffer(p: PbrInput, receives_shadows: bool, exposure: f32) -> GBufferOutput {
35
+ var out: GBufferOutput;
36
+ out.albedo = vec4f(p.base_color, p.occlusion);
37
+ out.normal = vec4f(oct_encode(p.normal), p.roughness, p.metallic + select(2.0, 0.0, receives_shadows));
38
+ out.emissive = vec4f(p.emissive * exposure, 1.0);
39
+ return out;
40
+ }
41
+
42
+ struct GBufferSample {
43
+ p: PbrInput,
44
+ receives_shadows: bool,
45
+ /** Pre-exposed emissive. */
46
+ emissive: vec3f,
47
+ }
48
+
49
+ fn unpack_gbuffer(albedo: vec4f, normal: vec4f, emissive: vec4f) -> GBufferSample {
50
+ var s: GBufferSample;
51
+ s.p.base_color = albedo.rgb;
52
+ s.p.alpha = 1.0;
53
+ s.p.occlusion = albedo.a;
54
+ s.p.normal = oct_decode(normal.xy);
55
+ s.p.roughness = normal.z;
56
+ s.receives_shadows = normal.w < 1.5;
57
+ s.p.metallic = select(normal.w - 2.0, normal.w, s.receives_shadows);
58
+ s.p.emissive = vec3f(0.0);
59
+ s.emissive = emissive.rgb;
60
+ return s;
61
+ }`,
62
+ "shard::pbr::gbuffer_pass": `
63
+ import shard::view::view;
64
+ import shard::pbr::types::VertexOutput;
65
+ import shard::mesh::mesh_vertex_at;
66
+ import shard::pbr::material::pbr_input;
67
+ import shard::pbr::standard::material;
68
+ import shard::pbr::gbuffer::{ GBufferOutput, pack_gbuffer };
69
+
70
+ const FLAG_RECEIVER: u32 = 4u;
71
+
72
+ @vertex fn vs(
73
+ @builtin(instance_index) instance_index: u32,
74
+ @builtin(vertex_index) vertex_index: u32,
75
+ @location(0) position: vec3f,
76
+ @location(1) normal: vec3f,
77
+ @location(2) uv: vec2f,
78
+ @location(3) uv1: vec2f,
79
+ @location(4) tangent: vec4f,
80
+ ) -> VertexOutput {
81
+ var out = mesh_vertex_at(instance_index, vertex_index, position, normal, uv, uv1, tangent);
82
+ out.clip = view.viewProj * vec4f(out.world_position, 1.0);
83
+ return out;
84
+ }
85
+
86
+ /** The surface stage only: what the material makes of the surface, packed for lighting. */
87
+ @fragment fn fs(in: VertexOutput) -> GBufferOutput {
88
+ let p = pbr_input(in);
89
+ @if(MASK) if (p.alpha < material.alphaCutoff) { discard; }
90
+ return pack_gbuffer(p, (in.flags & FLAG_RECEIVER) != 0u, view.exposure);
91
+ }`,
92
+ "shard::pbr::deferred_lighting": `
93
+ import shard::view::view;
94
+ import shard::pbr::gbuffer::unpack_gbuffer;
95
+ import shard::pbr::lighting::apply_lighting;
96
+
97
+ @group(1) @binding(0) var gbuffer0: texture_2d<f32>;
98
+ @group(1) @binding(1) var gbuffer1: texture_2d<f32>;
99
+ @group(1) @binding(2) var gbuffer2: texture_2d<f32>;
100
+ @group(1) @binding(3) var gbuffer_depth: texture_depth_2d;
101
+
102
+ const FLAG_RECEIVER: u32 = 4u;
103
+
104
+ @vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
105
+ let xy = vec2f(f32((i << 1u) & 2u), f32(i & 2u));
106
+ return vec4f(xy * 2.0 - 1.0, 0.0, 1.0);
107
+ }
108
+
109
+ /** One lighting evaluation per pixel: the same lighting stage the forward path runs. */
110
+ @fragment fn fs(@builtin(position) frag: vec4f) -> @location(0) vec4f {
111
+ let px = vec2i(frag.xy);
112
+ let depth = textureLoad(gbuffer_depth, px, 0);
113
+ // Reversed-Z: 0 is the far background, left for the sky.
114
+ if (depth <= 0.0) { discard; }
115
+ let ndc = vec2f(frag.x * view.viewport.z * 2.0 - 1.0, 1.0 - frag.y * view.viewport.w * 2.0);
116
+ let world = view.invViewProj * vec4f(ndc, depth, 1.0);
117
+ let position = world.xyz / world.w;
118
+ let s = unpack_gbuffer(textureLoad(gbuffer0, px, 0), textureLoad(gbuffer1, px, 0), textureLoad(gbuffer2, px, 0));
119
+ let flags = select(0u, FLAG_RECEIVER, s.receives_shadows);
120
+ let color = apply_lighting(s.p, position, frag, flags) * view.exposure + s.emissive;
121
+ return vec4f(color, 1.0);
122
+ }`,
123
+ "shard::debug::gbuffer": `
124
+ import shard::pbr::gbuffer::unpack_gbuffer;
125
+
126
+ @group(0) @binding(0) var gbuffer0: texture_2d<f32>;
127
+ @group(0) @binding(1) var gbuffer1: texture_2d<f32>;
128
+ @group(0) @binding(2) var gbuffer2: texture_2d<f32>;
129
+ @group(0) @binding(3) var<uniform> channel: vec4u;
130
+
131
+ @vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
132
+ let xy = vec2f(f32((i << 1u) & 2u), f32(i & 2u));
133
+ return vec4f(xy * 2.0 - 1.0, 0.0, 1.0);
134
+ }
135
+
136
+ /** A G-buffer channel as a viewable image: albedo, normal, roughness, metallic, emissive. */
137
+ @fragment fn fs(@builtin(position) frag: vec4f) -> @location(0) vec4f {
138
+ let px = vec2i(frag.xy);
139
+ let s = unpack_gbuffer(textureLoad(gbuffer0, px, 0), textureLoad(gbuffer1, px, 0), textureLoad(gbuffer2, px, 0));
140
+ switch channel.x {
141
+ case 0u: { return vec4f(s.p.base_color, 1.0); }
142
+ case 1u: { return vec4f(s.p.normal * 0.5 + 0.5, 1.0); }
143
+ case 2u: { return vec4f(vec3f(s.p.roughness), 1.0); }
144
+ case 3u: { return vec4f(vec3f(s.p.metallic), 1.0); }
145
+ default: { return vec4f(s.emissive / (1.0 + s.emissive), 1.0); }
146
+ }
147
+ }`
148
+ };
149
+ export {
150
+ DEFERRED_SHADERS
151
+ };
@@ -0,0 +1,22 @@
1
+ import { type World } from '@aethervtt/shard-core';
2
+ import type { CapturedImage } from './graph.js';
3
+ /** G-buffer channels a capture can show. */
4
+ export declare const GBUFFER_CHANNELS: readonly ['albedo', 'normal', 'roughness', 'metallic', 'emissive'];
5
+ export type GBufferChannel = (typeof GBUFFER_CHANNELS)[number];
6
+ /**
7
+ * A G-buffer channel of a camera, as an image, after the next frame renders it. Deferred views show
8
+ * their own G-buffer; forward views fill one. The first time, set the channel with `setDebugView`
9
+ * and render until shaders compile (as `render.capture` does), or the capture may be blank.
10
+ */
11
+ export declare function captureGBuffer(world: World, camera: number, channel: GBufferChannel): Promise<CapturedImage>;
12
+ /** The deferred section of `render.describe`: path, G-buffer memory, and what drew forward. */
13
+ export declare function describeDeferred(world: World): {
14
+ views: Record<string, unknown>;
15
+ } | undefined;
16
+ /**
17
+ * The deferred rendering path's graph nodes, installed by the forward renderer: a camera chooses
18
+ * it with `RenderPath`, sharing lights, shadows, environments, and materials.
19
+ */
20
+ export declare function addDeferredNodes(app: {
21
+ world: World;
22
+ }): void;