@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,46 @@
1
+ import { type World } from '@aethervtt/shard-core';
2
+ import type { GpuContext } from '@aethervtt/shard-gpu';
3
+ import type { ShaderLibrary } from '@aethervtt/shard-shader';
4
+ import type { MaterialAsset } from './assets.js';
5
+ import { type BlendMode, type MaterialType } from './materials.js';
6
+ /** A small stable number per material type, for sorting and pipeline keys. */
7
+ export declare function typeOrdinal(type: MaterialType): number;
8
+ /** What distinguishes a pipeline within a pass: blend mode and culling. */
9
+ export declare function materialVariant(material: MaterialAsset): number;
10
+ export declare function variantBlend(variant: number): BlendMode;
11
+ export declare function variantCull(variant: number): GPUCullMode;
12
+ /** Color blend state for a blend mode (HDR target). */
13
+ export declare function blendState(blend: BlendMode): GPUBlendState | undefined;
14
+ /**
15
+ * Makes sure a type's generated module is in the library at its current version, with `noise` (its
16
+ * noise slots' wrappers and their key) when it has any.
17
+ */
18
+ export declare function registerMaterialModule(library: ShaderLibrary, type: MaterialType, noise?: {
19
+ source: string;
20
+ key: string;
21
+ }): void;
22
+ /**
23
+ * Shader modules and pipelines per material type, looked up per draw without allocating: modules
24
+ * resolve once per (type, root, defines) per frame, pipelines are cached under a numeric key.
25
+ */
26
+ export declare class MaterialPipelines {
27
+ private frame;
28
+ private readonly modules;
29
+ private readonly pipelines;
30
+ beginFrame(frame: number): void;
31
+ /**
32
+ * The module for a type linked from `root` with its shader's overrides. `slot` distinguishes
33
+ * roots and define sets (a small integer chosen by the caller).
34
+ */
35
+ module(world: World, gpu: GpuContext, type: MaterialType, slot: number, root: string, defines: Readonly<Record<string, boolean>> | undefined): GPUShaderModule | undefined;
36
+ /** A compiled pipeline under `key` (unique per pass, type, and variant), if there is one. */
37
+ cached(key: number): GPURenderPipeline | undefined;
38
+ /** The last pipeline that compiled under each key, with the layout it was made for. */
39
+ private readonly previous;
40
+ /**
41
+ * Asks the cache for a pipeline on a miss; compiled ones are kept for the rest of the frame.
42
+ * While a replacement compiles (an edited shader or noise graph), draws keep the previous one
43
+ * for the key if its layout is the same object (the bind groups still fit), instead of vanishing.
44
+ */
45
+ create(gpu: GpuContext, key: number, descriptor: GPURenderPipelineDescriptor): GPURenderPipeline | undefined;
46
+ }
@@ -0,0 +1,166 @@
1
+ import { assetServer } from "@aethervtt/shard-assets";
2
+ import { ShardError } from "@aethervtt/shard-core";
3
+ import { LogResource } from "@aethervtt/shard-runtime";
4
+ import { MaterialNoise } from "./material-noise.js";
5
+ import { allMaterialTypes, BLEND_MODES } from "./materials.js";
6
+ import { Shaders } from "./plugin.js";
7
+ const ordinals = /* @__PURE__ */ new WeakMap();
8
+ let nextOrdinal = 0;
9
+ function typeOrdinal(type) {
10
+ let n = ordinals.get(type);
11
+ if (n === void 0) {
12
+ n = nextOrdinal++;
13
+ ordinals.set(type, n);
14
+ }
15
+ return n;
16
+ }
17
+ function materialVariant(material) {
18
+ const blend = material.type.blendOf(material.value);
19
+ const cull = material.value.doubleSided ? 1 : 0;
20
+ return BLEND_MODES.indexOf(blend) * 2 + cull;
21
+ }
22
+ function variantBlend(variant) {
23
+ return BLEND_MODES[variant >> 1];
24
+ }
25
+ function variantCull(variant) {
26
+ return variant & 1 ? "none" : "back";
27
+ }
28
+ function blendState(blend) {
29
+ switch (blend) {
30
+ case "alpha":
31
+ return {
32
+ color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha" },
33
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha" }
34
+ };
35
+ case "premultiplied":
36
+ return {
37
+ color: { srcFactor: "one", dstFactor: "one-minus-src-alpha" },
38
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha" }
39
+ };
40
+ case "additive":
41
+ return {
42
+ color: { srcFactor: "src-alpha", dstFactor: "one" },
43
+ alpha: { srcFactor: "zero", dstFactor: "one" }
44
+ };
45
+ default:
46
+ return void 0;
47
+ }
48
+ }
49
+ const registered = /* @__PURE__ */ new WeakMap();
50
+ function registerMaterialModule(library, type, noise) {
51
+ let list = registered.get(type);
52
+ if (!list) {
53
+ list = [];
54
+ registered.set(type, list);
55
+ }
56
+ const entry = list.find((r) => r.library === library);
57
+ const key = noise?.key ?? "";
58
+ if (entry && entry.version === type.version && entry.noise === key) return;
59
+ library.register(type.modulePath, type.moduleSource(noise?.source), `material:${type.name}`);
60
+ if (entry) {
61
+ entry.version = type.version;
62
+ entry.noise = key;
63
+ } else list.push({ version: type.version, noise: key, library });
64
+ }
65
+ const watching = /* @__PURE__ */ new WeakSet();
66
+ function watchNoiseGraphs(world, library) {
67
+ if (watching.has(world)) return;
68
+ watching.add(world);
69
+ assetServer(world).onEvent((event) => {
70
+ if (event.kind !== "modified" && event.kind !== "loaded") return;
71
+ const support = world.tryResource(MaterialNoise);
72
+ if (!support) return;
73
+ for (const type of allMaterialTypes()) {
74
+ if (!type.noise.some((slot) => slot.path === event.path)) continue;
75
+ const noise = support.wrappers(world, library, type);
76
+ if (noise) registerMaterialModule(library, type, noise);
77
+ }
78
+ });
79
+ }
80
+ const missingNoise = /* @__PURE__ */ new WeakSet();
81
+ class MaterialPipelines {
82
+ frame = -1;
83
+ modules = /* @__PURE__ */ new Map();
84
+ pipelines = /* @__PURE__ */ new Map();
85
+ beginFrame(frame) {
86
+ if (frame === this.frame) return;
87
+ this.frame = frame;
88
+ this.modules.clear();
89
+ this.pipelines.clear();
90
+ }
91
+ /**
92
+ * The module for a type linked from `root` with its shader's overrides. `slot` distinguishes
93
+ * roots and define sets (a small integer chosen by the caller).
94
+ */
95
+ module(world, gpu, type, slot, root, defines) {
96
+ const key = typeOrdinal(type) * 64 + slot;
97
+ if (this.modules.has(key)) return this.modules.get(key);
98
+ const library = world.resource(Shaders);
99
+ let noise;
100
+ if (type.noise.length > 0) {
101
+ const support = world.tryResource(MaterialNoise);
102
+ if (!support) {
103
+ if (!missingNoise.has(type)) {
104
+ missingNoise.add(type);
105
+ world.tryResource(LogResource)?.error(
106
+ new ShardError(
107
+ "render/feature-missing",
108
+ `Material ${type.name} has noise slots, but materialNoisePlugin isn't installed`,
109
+ { hint: "Add materialNoisePlugin from '@aethervtt/shard-render/noise'." }
110
+ )
111
+ );
112
+ }
113
+ this.modules.set(key, void 0);
114
+ return void 0;
115
+ }
116
+ watchNoiseGraphs(world, library);
117
+ noise = support.wrappers(world, library, type);
118
+ if (!noise) {
119
+ this.modules.set(key, void 0);
120
+ return void 0;
121
+ }
122
+ }
123
+ registerMaterialModule(library, type, noise);
124
+ const module = library.module(gpu, {
125
+ root,
126
+ defines,
127
+ overrides: type.shader ? [type.shader] : void 0,
128
+ label: type.name === "render/StandardMaterial" ? void 0 : `material ${type.name}`
129
+ });
130
+ this.modules.set(key, module);
131
+ return module;
132
+ }
133
+ /** A compiled pipeline under `key` (unique per pass, type, and variant), if there is one. */
134
+ cached(key) {
135
+ return this.pipelines.get(key);
136
+ }
137
+ /** The last pipeline that compiled under each key, with the layout it was made for. */
138
+ previous = /* @__PURE__ */ new Map();
139
+ /**
140
+ * Asks the cache for a pipeline on a miss; compiled ones are kept for the rest of the frame.
141
+ * While a replacement compiles (an edited shader or noise graph), draws keep the previous one
142
+ * for the key if its layout is the same object (the bind groups still fit), instead of vanishing.
143
+ */
144
+ create(gpu, key, descriptor) {
145
+ const pipeline = gpu.pipelines.render(descriptor);
146
+ if (pipeline) {
147
+ this.pipelines.set(key, pipeline);
148
+ this.previous.set(key, { pipeline, layout: descriptor.layout, generation: gpu.generation });
149
+ return pipeline;
150
+ }
151
+ const last = this.previous.get(key);
152
+ if (last && last.layout === descriptor.layout && last.generation === gpu.generation) {
153
+ return last.pipeline;
154
+ }
155
+ return void 0;
156
+ }
157
+ }
158
+ export {
159
+ MaterialPipelines,
160
+ blendState,
161
+ materialVariant,
162
+ registerMaterialModule,
163
+ typeOrdinal,
164
+ variantBlend,
165
+ variantCull
166
+ };
@@ -0,0 +1,108 @@
1
+ import { type AnyField, type ComponentDef, type Fields } from '@aethervtt/shard-core';
2
+ import type { GpuContext } from '@aethervtt/shard-gpu';
3
+ import { type WgslLayout } from '@aethervtt/shard-shader';
4
+ export declare const BLEND_MODES: readonly ['opaque', 'mask', 'alpha', 'additive', 'premultiplied'];
5
+ export type BlendMode = (typeof BLEND_MODES)[number];
6
+ /** Blend modes drawn after opaque geometry, sorted back to front, without depth writes. */
7
+ export declare const isTransparent: (blend: BlendMode) => blend is "additive" | "alpha" | "premultiplied";
8
+ export interface MaterialTypeOptions<F extends Fields> {
9
+ /**
10
+ * 'standard' keeps every StandardMaterial field and its lighting; hooks change the surface.
11
+ * 'none' starts from nothing: a `shade` hook computes the final color, and it renders forward.
12
+ */
13
+ extends?: 'standard' | 'none';
14
+ /** The type's own parameters. Texture handles (`t.handle('Texture')`) become bindings. */
15
+ fields?: F;
16
+ /** Fixed blend mode. Omitted, standard extensions follow the material's `alphaMode`. */
17
+ blend?: BlendMode;
18
+ /** The shader module with the hook overrides, e.g. `project::lava` (`shaders/lava.wesl`). */
19
+ shader?: string;
20
+ /**
21
+ * Noise graphs the shader calls, by name: `{ detail: 'assets/noise/rock.noise.json' }` makes
22
+ * `noise_detail(p: vec3f, seed: u32) -> f32` available in `material::<name>`. The graph is
23
+ * code, so it's chosen per type; editing the file relinks the material.
24
+ */
25
+ noise?: Readonly<Record<string, string>>;
26
+ /**
27
+ * Texture fields bound as `texture_2d_array<f32>` (a `*.texarray.json`, or any texture as one
28
+ * layer). Arrays of color textures sample through the sRGB view.
29
+ */
30
+ arrays?: readonly string[];
31
+ /**
32
+ * Standard extensions only: false leaves the standard material's five texture slots (and their
33
+ * samplers) out of the layout, for types that build their own surface and need the room (a stage
34
+ * holds 16 sampled textures). Its shader must then not call `standard_input`.
35
+ */
36
+ standardTextures?: boolean;
37
+ description?: string;
38
+ }
39
+ /** A material type's noise slot: `noise_<name>` calls the graph at `path`. */
40
+ export interface MaterialNoiseSlot {
41
+ readonly name: string;
42
+ readonly path: string;
43
+ }
44
+ /** A PascalName's shader module path: `my-game/Lava` → `material::lava`. */
45
+ export declare function materialModulePath(name: string): string;
46
+ /**
47
+ * A material type: a schema (validation, JSON Schema, inspector data) and, from it, a generated
48
+ * WGSL module with the uniform struct and texture bindings. Its shader overrides hooks.
49
+ */
50
+ export declare class MaterialType {
51
+ readonly name: string;
52
+ extends: 'standard' | 'none';
53
+ /** Binds the standard texture slots (standard extensions; see MaterialTypeOptions). */
54
+ standardTextures: boolean;
55
+ /** Every field of the material asset (standard fields too, for standard extensions). */
56
+ schema: ComponentDef;
57
+ /** The type's own numeric fields, packed into its uniform. Undefined if it has none. */
58
+ layout: WgslLayout<Fields> | undefined;
59
+ /** The type's own texture fields, in binding order. */
60
+ textures: string[];
61
+ /** Texture fields bound as 2D arrays. */
62
+ arrays: Set<string>;
63
+ blend: BlendMode | undefined;
64
+ shader: string | undefined;
65
+ /** Noise graphs the module wraps as `noise_<name>`. */
66
+ noise: MaterialNoiseSlot[];
67
+ description: string;
68
+ /** Increments on every redefinition, so GPU state and pipelines rebuild. */
69
+ version: number;
70
+ readonly modulePath: string;
71
+ /** WGSL identifier of the uniform (the PascalName). */
72
+ readonly varName: string;
73
+ private layouts;
74
+ constructor(name: string, options: MaterialTypeOptions<Fields>, schema: ComponentDef);
75
+ /** (Re)applies a definition: redefinitions keep the object, so assets keep pointing at it. */
76
+ assign(options: MaterialTypeOptions<Fields>, schema: ComponentDef): void;
77
+ get standard(): boolean;
78
+ /** First group-1 binding of the type's own data. */
79
+ get bindingBase(): number;
80
+ /** The blend mode for a material of this type. */
81
+ blendOf(value: {
82
+ alphaMode?: string;
83
+ }): BlendMode;
84
+ /** Deferred shading only takes standard lighting, and never transparency. */
85
+ deferrable(value: {
86
+ alphaMode?: string;
87
+ }): boolean;
88
+ /**
89
+ * The generated `material::<name>` module: uniform struct and bindings, then `noise` (the noise
90
+ * slots' wrappers, once their graphs load).
91
+ */
92
+ moduleSource(noise?: string): string;
93
+ /** The group-1 bind group layout: standard bindings (if extended), own uniform, own textures. */
94
+ bindGroupLayout(gpu: GpuContext, standard: GPUBindGroupLayoutEntry[]): GPUBindGroupLayout;
95
+ }
96
+ /**
97
+ * Declares a material type. Engine types use their package namespace; project code uses
98
+ * `project.material(name, ...)`, which namespaces it. Defining an existing name again (hot reload)
99
+ * updates the type in place: existing assets revalidate and gain new fields' defaults.
100
+ */
101
+ export declare function defineMaterial<const F extends Fields>(name: string, options: MaterialTypeOptions<F>, standardFields?: Fields): MaterialType;
102
+ export declare function findMaterialType(name: string): MaterialType | undefined;
103
+ export declare function allMaterialTypes(): MaterialType[];
104
+ /** Called with a type whenever it's redefined. Returns an unsubscribe function. */
105
+ export declare function onMaterialTypeChange(fn: (type: MaterialType) => void): () => void;
106
+ /** Registers a type as the standard one (it keeps `render/StandardMaterial`'s schema). */
107
+ export declare function registerStandardType(schema: ComponentDef): MaterialType;
108
+ export type { AnyField };
@@ -0,0 +1,219 @@
1
+ import {
2
+ defineComponent,
3
+ defineSchema,
4
+ ShardError
5
+ } from "@aethervtt/shard-core";
6
+ import { wgslLayout } from "@aethervtt/shard-shader";
7
+ import { STANDARD_FIELDS } from "./standard-fields.js";
8
+ const BLEND_MODES = ["opaque", "mask", "alpha", "additive", "premultiplied"];
9
+ const isTransparent = (blend) => blend === "alpha" || blend === "additive" || blend === "premultiplied";
10
+ const STANDARD_BINDINGS = 12;
11
+ function isTextureHandle(field) {
12
+ if (field.kind !== "handle") return false;
13
+ const schema = field.jsonSchema();
14
+ return schema.anyOf?.some((s) => s["x-asset-type"] === "Texture") ?? false;
15
+ }
16
+ function materialModulePath(name) {
17
+ const pascal = name.slice(name.indexOf("/") + 1);
18
+ return `material::${pascal.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase()}`;
19
+ }
20
+ class MaterialType {
21
+ name;
22
+ extends;
23
+ /** Binds the standard texture slots (standard extensions; see MaterialTypeOptions). */
24
+ standardTextures = true;
25
+ /** Every field of the material asset (standard fields too, for standard extensions). */
26
+ schema;
27
+ /** The type's own numeric fields, packed into its uniform. Undefined if it has none. */
28
+ layout;
29
+ /** The type's own texture fields, in binding order. */
30
+ textures;
31
+ /** Texture fields bound as 2D arrays. */
32
+ arrays;
33
+ blend;
34
+ shader;
35
+ /** Noise graphs the module wraps as `noise_<name>`. */
36
+ noise;
37
+ description;
38
+ /** Increments on every redefinition, so GPU state and pipelines rebuild. */
39
+ version = 0;
40
+ modulePath;
41
+ /** WGSL identifier of the uniform (the PascalName). */
42
+ varName;
43
+ layouts = /* @__PURE__ */ new WeakMap();
44
+ constructor(name, options, schema) {
45
+ this.name = name;
46
+ this.modulePath = materialModulePath(name);
47
+ this.varName = name.slice(name.indexOf("/") + 1);
48
+ this.extends = options.extends ?? "standard";
49
+ this.schema = schema;
50
+ this.layout = void 0;
51
+ this.textures = [];
52
+ this.arrays = /* @__PURE__ */ new Set();
53
+ this.blend = options.blend;
54
+ this.shader = options.shader;
55
+ this.noise = [];
56
+ this.description = options.description ?? "";
57
+ this.assign(options, schema);
58
+ }
59
+ /** (Re)applies a definition: redefinitions keep the object, so assets keep pointing at it. */
60
+ assign(options, schema) {
61
+ const fields = options.fields ?? {};
62
+ const numeric = {};
63
+ const textures = [];
64
+ for (const [key, field] of Object.entries(fields)) {
65
+ if (isTextureHandle(field)) textures.push(key);
66
+ else if (field.storage !== "object") numeric[key] = field;
67
+ }
68
+ this.extends = options.extends ?? "standard";
69
+ this.standardTextures = options.standardTextures ?? true;
70
+ this.schema = schema;
71
+ this.textures = textures;
72
+ this.arrays = new Set(options.arrays ?? []);
73
+ this.blend = options.blend;
74
+ this.shader = options.shader;
75
+ this.noise = Object.entries(options.noise ?? {}).map(([name, path]) => {
76
+ if (!/^[a-z_][a-z0-9_]*$/.test(name)) {
77
+ throw new ShardError(
78
+ "render/material-noise-name",
79
+ `Noise slot "${name}" of ${this.name} isn't a WGSL name`,
80
+ {
81
+ hint: 'Use lowercase letters, digits, and underscores: { detail: "assets/noise/rock.noise.json" }.'
82
+ }
83
+ );
84
+ }
85
+ return { name, path };
86
+ });
87
+ this.description = options.description ?? "";
88
+ this.layout = Object.keys(numeric).length > 0 ? wgslLayout(defineSchema(`${this.name}Params`, numeric)) : void 0;
89
+ this.version++;
90
+ this.layouts = /* @__PURE__ */ new WeakMap();
91
+ }
92
+ get standard() {
93
+ return this.extends === "standard";
94
+ }
95
+ /** First group-1 binding of the type's own data. */
96
+ get bindingBase() {
97
+ return this.standard ? STANDARD_BINDINGS : 0;
98
+ }
99
+ /** The blend mode for a material of this type. */
100
+ blendOf(value) {
101
+ if (this.blend) return this.blend;
102
+ const mode = value.alphaMode;
103
+ return mode === "mask" || mode === "alpha" || mode === "premultiplied" || mode === "additive" ? mode : "opaque";
104
+ }
105
+ /** Deferred shading only takes standard lighting, and never transparency. */
106
+ deferrable(value) {
107
+ return this.standard && !isTransparent(this.blendOf(value));
108
+ }
109
+ /**
110
+ * The generated `material::<name>` module: uniform struct and bindings, then `noise` (the noise
111
+ * slots' wrappers, once their graphs load).
112
+ */
113
+ moduleSource(noise = "") {
114
+ const lines = [];
115
+ let binding = this.bindingBase;
116
+ if (this.layout) {
117
+ lines.push(this.layout.wgsl);
118
+ lines.push(
119
+ `@group(1) @binding(${binding++}) var<uniform> ${this.varName}: ${this.layout.structName};`
120
+ );
121
+ } else {
122
+ binding++;
123
+ }
124
+ for (const tex of this.textures) {
125
+ const kind = this.arrays.has(tex) ? "texture_2d_array<f32>" : "texture_2d<f32>";
126
+ lines.push(`@group(1) @binding(${binding++}) var ${this.varName}_${tex}: ${kind};`);
127
+ lines.push(`@group(1) @binding(${binding++}) var ${this.varName}_${tex}_sampler: sampler;`);
128
+ }
129
+ if (!this.layout && this.textures.length === 0) {
130
+ lines.push(`const ${this.varName}_fields: u32 = 0u;`);
131
+ }
132
+ if (noise) lines.push(noise);
133
+ return `// Generated from the ${this.name} schema. Don't edit: change the material definition.
134
+ ${lines.join("\n")}
135
+ `;
136
+ }
137
+ /** The group-1 bind group layout: standard bindings (if extended), own uniform, own textures. */
138
+ bindGroupLayout(gpu, standard) {
139
+ const cached = this.layouts.get(gpu);
140
+ if (cached && cached.generation === gpu.generation) return cached.layout;
141
+ const visibility = GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT;
142
+ const entries = !this.standard ? [] : this.standardTextures ? [...standard] : standard.filter((e) => e.binding < 2);
143
+ let binding = this.bindingBase;
144
+ if (this.layout) entries.push({ binding, visibility, buffer: { type: "uniform" } });
145
+ binding++;
146
+ for (const tex of this.textures) {
147
+ entries.push({
148
+ binding: binding++,
149
+ visibility,
150
+ texture: {
151
+ sampleType: "float",
152
+ viewDimension: this.arrays.has(tex) ? "2d-array" : "2d"
153
+ }
154
+ });
155
+ entries.push({ binding: binding++, visibility, sampler: { type: "filtering" } });
156
+ }
157
+ const layout = gpu.layouts.bindGroupLayout({ label: `material/${this.name}`, entries });
158
+ this.layouts.set(gpu, { generation: gpu.generation, layout });
159
+ return layout;
160
+ }
161
+ }
162
+ const types = /* @__PURE__ */ new Map();
163
+ const listeners = /* @__PURE__ */ new Set();
164
+ function defineMaterial(name, options, standardFields) {
165
+ const ext = options.extends ?? "standard";
166
+ const standard = standardFields ?? STANDARD_FIELDS;
167
+ for (const key of Object.keys(options.fields ?? {})) {
168
+ if (ext === "standard" && standard[key]) {
169
+ throw new ShardError(
170
+ "render/material-field-clash",
171
+ `Material ${name} redefines the standard field "${key}"`,
172
+ { hint: 'Give the field another name, or use extends: "none".' }
173
+ );
174
+ }
175
+ }
176
+ const fields = {
177
+ ...ext === "standard" ? standard : {},
178
+ ...options.fields ?? {}
179
+ };
180
+ const existing = types.get(name);
181
+ const schema = defineComponent(name, fields, {
182
+ description: options.description ?? `Material type ${name}${ext === "standard" ? " (extends the standard material)" : ""}.`,
183
+ serialize: true
184
+ });
185
+ if (existing) {
186
+ existing.assign(options, schema);
187
+ for (const listener of listeners) listener(existing);
188
+ return existing;
189
+ }
190
+ const type = new MaterialType(name, options, schema);
191
+ types.set(name, type);
192
+ return type;
193
+ }
194
+ function findMaterialType(name) {
195
+ return types.get(name);
196
+ }
197
+ function allMaterialTypes() {
198
+ return [...types.values()];
199
+ }
200
+ function onMaterialTypeChange(fn) {
201
+ listeners.add(fn);
202
+ return () => listeners.delete(fn);
203
+ }
204
+ function registerStandardType(schema) {
205
+ const type = new MaterialType("render/StandardMaterial", { extends: "standard" }, schema);
206
+ types.set(type.name, type);
207
+ return type;
208
+ }
209
+ export {
210
+ BLEND_MODES,
211
+ MaterialType,
212
+ allMaterialTypes,
213
+ defineMaterial,
214
+ findMaterialType,
215
+ isTransparent,
216
+ materialModulePath,
217
+ onMaterialTypeChange,
218
+ registerStandardType
219
+ };
@@ -0,0 +1,76 @@
1
+ import { type World } from '@aethervtt/shard-core';
2
+ import { type GpuContext } from '@aethervtt/shard-gpu';
3
+ import { type GridParams, type NoiseGraph, type SpherePatch } from '@aethervtt/shard-noise';
4
+ import type { ShaderLibrary } from '@aethervtt/shard-shader';
5
+ import type { NodeDescriptor } from './graph.js';
6
+ import type { MaterialType } from './materials.js';
7
+ /**
8
+ * The WGSL a material type's noise slots add to its module: `noise_<slot>(p, seed)` for each,
9
+ * calling its graph's function. Registers each graph's module (a changed graph relinks the
10
+ * material). Undefined while a graph is still loading; this starts the load.
11
+ */
12
+ export declare function materialNoise(world: World, library: ShaderLibrary, type: MaterialType): {
13
+ source: string;
14
+ key: string;
15
+ } | undefined;
16
+ export type NoiseComputeDomain = 'grid2d' | 'sphere-patch';
17
+ export interface NoiseComputeRequest {
18
+ readonly seed: number;
19
+ /** The grid (grid2d) or cube-sphere patch (sphere-patch) to fill. */
20
+ readonly area: GridParams | SpherePatch;
21
+ /** Receives one f32 per sample, row by row (the same order as `sampleGrid2d` and `sampleSpherePatch`). */
22
+ readonly out: GPUBuffer;
23
+ }
24
+ /**
25
+ * Fills a storage buffer with a graph's values over a grid or a cube-sphere patch, on the GPU,
26
+ * with origin-offset sampling: the CPU computes the origin records in f64 (and, for sphere patches,
27
+ * the same local points `sampleSpherePatch` uses), so the values match the CPU kernel within its
28
+ * tolerance at any planet size. 0043 dispatches one per terrain chunk.
29
+ */
30
+ export declare class NoiseCompute {
31
+ readonly gpu: GpuContext;
32
+ readonly domain: NoiseComputeDomain;
33
+ graph: NoiseGraph;
34
+ private readonly params;
35
+ private readonly origins;
36
+ private readonly points;
37
+ private readonly scratch;
38
+ private readonly f32;
39
+ private readonly u32;
40
+ private local;
41
+ private records;
42
+ private readonly origin64;
43
+ private bindGroups;
44
+ constructor(gpu: GpuContext, graph: NoiseGraph, domain: NoiseComputeDomain);
45
+ /** The root module path of this domain's compute shader for the graph. */
46
+ get modulePath(): string;
47
+ /** The compute shader: one invocation per sample, calling `noise_<name>_at`. */
48
+ shaderSource(): string;
49
+ private pipeline;
50
+ /**
51
+ * Writes this request's parameters and records a dispatch into `pass`. Returns false while the
52
+ * shader compiles (nothing is recorded).
53
+ */
54
+ encode(library: ShaderLibrary, pass: GPUComputePassEncoder, request: NoiseComputeRequest): boolean;
55
+ private bindGroup;
56
+ destroy(): void;
57
+ }
58
+ export interface NoiseComputeNodeOptions {
59
+ readonly graph: NoiseGraph;
60
+ readonly domain: NoiseComputeDomain;
61
+ /** What to fill this frame, or undefined for nothing. Called once per frame. */
62
+ next(): NoiseComputeRequest | undefined;
63
+ /** Called after the frame's commands are submitted with the request that ran. */
64
+ done?(request: NoiseComputeRequest): void;
65
+ }
66
+ /**
67
+ * A render graph node (setup phase, once per frame) that fills a storage buffer from a noise graph
68
+ * on the GPU whenever `next` returns a request. Add it with `world.resource(Graph).addNode(name, …)`.
69
+ */
70
+ export declare function noiseComputeNode(options: NoiseComputeNodeOptions): NodeDescriptor;
71
+ /**
72
+ * Noise slots in materials: `noise: { detail: 'assets/noise/rock.noise.json' }` makes
73
+ * `noise_detail(p, seed)` callable from the material's shader (spec 0041). Separate from core render
74
+ * so apps without such materials carry no noise code.
75
+ */
76
+ export declare const materialNoisePlugin: import("@aethervtt/shard-runtime").Plugin;