@luma.gl/shadertools 9.3.5 → 9.4.0-alpha.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 (113) hide show
  1. package/dist/dist.dev.js +6555 -2373
  2. package/dist/dist.min.js +2984 -237
  3. package/dist/index.cjs +4356 -268
  4. package/dist/index.cjs.map +4 -4
  5. package/dist/index.d.ts +13 -3
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +6 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/lib/preprocessor/preprocessor.d.ts +11 -1
  10. package/dist/lib/preprocessor/preprocessor.d.ts.map +1 -1
  11. package/dist/lib/preprocessor/preprocessor.js +47 -2
  12. package/dist/lib/preprocessor/preprocessor.js.map +1 -1
  13. package/dist/lib/shader-assembler.d.ts +10 -10
  14. package/dist/lib/shader-assembler.d.ts.map +1 -1
  15. package/dist/lib/shader-assembler.js +36 -20
  16. package/dist/lib/shader-assembler.js.map +1 -1
  17. package/dist/lib/shader-assembly/assemble-shaders.d.ts +28 -7
  18. package/dist/lib/shader-assembly/assemble-shaders.d.ts.map +1 -1
  19. package/dist/lib/shader-assembly/assemble-shaders.js +133 -15
  20. package/dist/lib/shader-assembly/assemble-shaders.js.map +1 -1
  21. package/dist/lib/shader-assembly/platform-info.d.ts +2 -0
  22. package/dist/lib/shader-assembly/platform-info.d.ts.map +1 -1
  23. package/dist/lib/shader-assembly/shader-hooks.d.ts +1 -1
  24. package/dist/lib/shader-assembly/shader-hooks.d.ts.map +1 -1
  25. package/dist/lib/shader-assembly/shader-hooks.js +6 -3
  26. package/dist/lib/shader-assembly/shader-hooks.js.map +1 -1
  27. package/dist/lib/shader-assembly/shader-injections.d.ts +4 -1
  28. package/dist/lib/shader-assembly/shader-injections.d.ts.map +1 -1
  29. package/dist/lib/shader-assembly/shader-injections.js +68 -11
  30. package/dist/lib/shader-assembly/shader-injections.js.map +1 -1
  31. package/dist/lib/shader-assembly/shader-plugin-varyings.d.ts +15 -0
  32. package/dist/lib/shader-assembly/shader-plugin-varyings.d.ts.map +1 -0
  33. package/dist/lib/shader-assembly/shader-plugin-varyings.js +412 -0
  34. package/dist/lib/shader-assembly/shader-plugin-varyings.js.map +1 -0
  35. package/dist/lib/shader-assembly/shader-plugin-vertex-inputs.d.ts +12 -0
  36. package/dist/lib/shader-assembly/shader-plugin-vertex-inputs.d.ts.map +1 -0
  37. package/dist/lib/shader-assembly/shader-plugin-vertex-inputs.js +237 -0
  38. package/dist/lib/shader-assembly/shader-plugin-vertex-inputs.js.map +1 -0
  39. package/dist/lib/shader-module/shader-module.d.ts +3 -0
  40. package/dist/lib/shader-module/shader-module.d.ts.map +1 -1
  41. package/dist/lib/shader-module/shader-module.js.map +1 -1
  42. package/dist/lib/shader-module/shader-pass-pipeline.d.ts +14 -0
  43. package/dist/lib/shader-module/shader-pass-pipeline.d.ts.map +1 -0
  44. package/dist/lib/shader-module/shader-pass-pipeline.js +5 -0
  45. package/dist/lib/shader-module/shader-pass-pipeline.js.map +1 -0
  46. package/dist/lib/shader-module/shader-pass.d.ts +21 -4
  47. package/dist/lib/shader-module/shader-pass.d.ts.map +1 -1
  48. package/dist/lib/shader-plugin.d.ts +68 -0
  49. package/dist/lib/shader-plugin.d.ts.map +1 -0
  50. package/dist/lib/shader-plugin.js +98 -0
  51. package/dist/lib/shader-plugin.js.map +1 -0
  52. package/dist/lib/utils/assert.js +1 -1
  53. package/dist/lib/utils/assert.js.map +1 -1
  54. package/dist/modules/color/float-colors.d.ts +111 -5
  55. package/dist/modules/color/float-colors.d.ts.map +1 -1
  56. package/dist/modules/color/float-colors.js +200 -34
  57. package/dist/modules/color/float-colors.js.map +1 -1
  58. package/dist/modules/engine/clip/clip.d.ts +12 -0
  59. package/dist/modules/engine/clip/clip.d.ts.map +1 -0
  60. package/dist/modules/engine/clip/clip.js +129 -0
  61. package/dist/modules/engine/clip/clip.js.map +1 -0
  62. package/dist/modules/engine/filter/filter.d.ts +12 -0
  63. package/dist/modules/engine/filter/filter.d.ts.map +1 -0
  64. package/dist/modules/engine/filter/filter.js +88 -0
  65. package/dist/modules/engine/filter/filter.js.map +1 -0
  66. package/dist/modules/geospatial/dggs/dggs-wgsl.d.ts +3 -0
  67. package/dist/modules/geospatial/dggs/dggs-wgsl.d.ts.map +1 -0
  68. package/dist/modules/geospatial/dggs/dggs-wgsl.js +1892 -0
  69. package/dist/modules/geospatial/dggs/dggs-wgsl.js.map +1 -0
  70. package/dist/modules/geospatial/dggs/dggs.d.ts +6 -0
  71. package/dist/modules/geospatial/dggs/dggs.d.ts.map +1 -0
  72. package/dist/modules/geospatial/dggs/dggs.js +10 -0
  73. package/dist/modules/geospatial/dggs/dggs.js.map +1 -0
  74. package/dist/modules/lighting/water-material/water-material.d.ts +28 -0
  75. package/dist/modules/lighting/water-material/water-material.d.ts.map +1 -0
  76. package/dist/modules/lighting/water-material/water-material.js +179 -0
  77. package/dist/modules/lighting/water-material/water-material.js.map +1 -0
  78. package/dist/modules/lighting/water-material/water-shaders-glsl.d.ts +3 -0
  79. package/dist/modules/lighting/water-material/water-shaders-glsl.d.ts.map +1 -0
  80. package/dist/modules/lighting/water-material/water-shaders-glsl.js +232 -0
  81. package/dist/modules/lighting/water-material/water-shaders-glsl.js.map +1 -0
  82. package/dist/modules/lighting/water-material/water-shaders-wgsl.d.ts +2 -0
  83. package/dist/modules/lighting/water-material/water-shaders-wgsl.d.ts.map +1 -0
  84. package/dist/modules/lighting/water-material/water-shaders-wgsl.js +230 -0
  85. package/dist/modules/lighting/water-material/water-shaders-wgsl.js.map +1 -0
  86. package/dist/modules/math/fp64/fp64-arithmetic-wgsl.d.ts +2 -1
  87. package/dist/modules/math/fp64/fp64-arithmetic-wgsl.d.ts.map +1 -1
  88. package/dist/modules/math/fp64/fp64-arithmetic-wgsl.js +268 -0
  89. package/dist/modules/math/fp64/fp64-arithmetic-wgsl.js.map +1 -1
  90. package/package.json +2 -2
  91. package/src/index.ts +50 -3
  92. package/src/lib/preprocessor/preprocessor.ts +68 -3
  93. package/src/lib/shader-assembler.ts +49 -20
  94. package/src/lib/shader-assembly/assemble-shaders.ts +242 -30
  95. package/src/lib/shader-assembly/platform-info.ts +2 -0
  96. package/src/lib/shader-assembly/shader-hooks.ts +7 -3
  97. package/src/lib/shader-assembly/shader-injections.ts +96 -11
  98. package/src/lib/shader-assembly/shader-plugin-varyings.ts +524 -0
  99. package/src/lib/shader-assembly/shader-plugin-vertex-inputs.ts +279 -0
  100. package/src/lib/shader-module/shader-module.ts +3 -0
  101. package/src/lib/shader-module/shader-pass-pipeline.ts +19 -0
  102. package/src/lib/shader-module/shader-pass.ts +29 -4
  103. package/src/lib/shader-plugin.ts +214 -0
  104. package/src/lib/utils/assert.ts +1 -1
  105. package/src/modules/color/float-colors.ts +272 -37
  106. package/src/modules/engine/clip/clip.ts +149 -0
  107. package/src/modules/engine/filter/filter.ts +107 -0
  108. package/src/modules/geospatial/dggs/dggs-wgsl.ts +1892 -0
  109. package/src/modules/geospatial/dggs/dggs.ts +12 -0
  110. package/src/modules/lighting/water-material/water-material.ts +234 -0
  111. package/src/modules/lighting/water-material/water-shaders-glsl.ts +233 -0
  112. package/src/modules/lighting/water-material/water-shaders-wgsl.ts +230 -0
  113. package/src/modules/math/fp64/fp64-arithmetic-wgsl.ts +268 -0
@@ -0,0 +1,279 @@
1
+ // luma.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {shaderTypeDecoder, type AttributeShaderType} from '@luma.gl/core';
6
+
7
+ export type ShaderPluginVertexInputAssembly = {
8
+ source: string;
9
+ declarations: string;
10
+ initialization: string;
11
+ };
12
+
13
+ /** Generate GLSL vertex input declarations contributed by shader plugins. */
14
+ export function getShaderPluginVertexInputDeclarationsGLSL(
15
+ source: string,
16
+ vertexInputs: Record<string, AttributeShaderType>
17
+ ): string {
18
+ const declarations: string[] = [];
19
+ for (const [name, type] of Object.entries(vertexInputs)) {
20
+ assertGLSLVertexInputIsNew(source, name);
21
+ declarations.push(`in ${getGLSLAttributeType(type)} ${name};`);
22
+ }
23
+ return declarations.join('\n');
24
+ }
25
+
26
+ /** Add WGSL plugin inputs to the selected vertex entry point. */
27
+ export function assembleShaderPluginVertexInputsWGSL(
28
+ source: string,
29
+ vertexEntryPoint: string,
30
+ vertexInputs: Record<string, AttributeShaderType>
31
+ ): ShaderPluginVertexInputAssembly {
32
+ const entries = Object.entries(vertexInputs);
33
+ if (entries.length === 0) {
34
+ return {source, declarations: '', initialization: ''};
35
+ }
36
+
37
+ const functionRange = getWGSLFunctionParameterRange(source, vertexEntryPoint);
38
+ const parameterSource = source.slice(
39
+ functionRange.openParenthesis + 1,
40
+ functionRange.closeParenthesis
41
+ );
42
+ const interfaceInfo = getWGSLVertexInterfaceInfo(source, parameterSource);
43
+ const usedLocations = new Set(interfaceInfo.locations);
44
+ const generatedParameters: string[] = [];
45
+ const declarations: string[] = [];
46
+ const initialization: string[] = [];
47
+
48
+ for (const [name, type] of entries) {
49
+ if (interfaceInfo.names.has(name) || hasWGSLModuleVariable(source, name)) {
50
+ throw new Error(
51
+ `ShaderPlugin vertex input "${name}" conflicts with an existing WGSL shader input or variable`
52
+ );
53
+ }
54
+ const location = getFirstUnusedLocation(usedLocations);
55
+ usedLocations.add(location);
56
+ const parameterName = `_luma_${name}`;
57
+ generatedParameters.push(`@location(${location}) ${parameterName}: ${type}`);
58
+ declarations.push(`var<private> ${name}: ${type};`);
59
+ initialization.push(`${name} = ${parameterName};`);
60
+ }
61
+
62
+ const separator = parameterSource.trim() ? ',\n ' : '\n ';
63
+ const suffix = parameterSource.trim() ? '' : '\n';
64
+ const injectedParameters = `${parameterSource}${separator}${generatedParameters.join(',\n ')}${suffix}`;
65
+ const transformedSource =
66
+ source.slice(0, functionRange.openParenthesis + 1) +
67
+ injectedParameters +
68
+ source.slice(functionRange.closeParenthesis);
69
+
70
+ return {
71
+ source: transformedSource,
72
+ declarations: declarations.join('\n'),
73
+ initialization: initialization.join('\n')
74
+ };
75
+ }
76
+
77
+ export function getGLSLAttributeType(type: AttributeShaderType): string {
78
+ const {primitiveType, components} = shaderTypeDecoder.getAttributeShaderTypeInfo(type);
79
+ const scalarType = primitiveType === 'i32' ? 'int' : primitiveType === 'u32' ? 'uint' : 'float';
80
+ if (components === 1) {
81
+ return scalarType;
82
+ }
83
+ const prefix = scalarType === 'int' ? 'i' : scalarType === 'uint' ? 'u' : '';
84
+ return `${prefix}vec${components}`;
85
+ }
86
+
87
+ function assertGLSLVertexInputIsNew(source: string, name: string): void {
88
+ const escapedName = escapeRegExp(name);
89
+ const declaration = new RegExp(
90
+ `\\b(?:in|attribute)\\s+(?:(?:lowp|mediump|highp)\\s+)?[A-Za-z_][A-Za-z0-9_]*\\s+${escapedName}\\s*(?:\\[|;)`
91
+ );
92
+ if (declaration.test(source)) {
93
+ throw new Error(`ShaderPlugin vertex input "${name}" conflicts with an existing GLSL input`);
94
+ }
95
+ }
96
+
97
+ function getWGSLFunctionParameterRange(
98
+ source: string,
99
+ entryPoint: string
100
+ ): {openParenthesis: number; closeParenthesis: number} {
101
+ const functionPattern = new RegExp(`\\bfn\\s+${escapeRegExp(entryPoint)}\\s*\\(`, 'g');
102
+ const match = functionPattern.exec(source);
103
+ if (!match) {
104
+ throw new Error(`ShaderPlugin vertex inputs require WGSL vertex entry point "${entryPoint}"`);
105
+ }
106
+ const openParenthesis = source.indexOf('(', match.index);
107
+ const closeParenthesis = findMatchingDelimiter(source, openParenthesis, '(', ')');
108
+ if (closeParenthesis < 0) {
109
+ throw new Error(`Unable to parse WGSL vertex entry point "${entryPoint}" parameters`);
110
+ }
111
+ return {openParenthesis, closeParenthesis};
112
+ }
113
+
114
+ function getWGSLVertexInterfaceInfo(
115
+ source: string,
116
+ parameterSource: string
117
+ ): {locations: number[]; names: Set<string>} {
118
+ const locations = getWGSLLocations(parameterSource);
119
+ const names = new Set(getWGSLDeclaredNames(parameterSource));
120
+ const parameterTypes = getWGSLParameterTypes(parameterSource);
121
+ for (const parameterType of parameterTypes) {
122
+ const structBody = getWGSLStructBody(source, parameterType);
123
+ if (structBody === null) {
124
+ continue;
125
+ }
126
+ locations.push(...getWGSLLocations(structBody));
127
+ for (const name of getWGSLDeclaredNames(structBody)) {
128
+ names.add(name);
129
+ }
130
+ }
131
+ return {locations, names};
132
+ }
133
+
134
+ function getWGSLLocations(source: string): number[] {
135
+ const locations: number[] = [];
136
+ const locationPattern = /@location\s*\(\s*(\d+)\s*\)/g;
137
+ let match = locationPattern.exec(source);
138
+ while (match) {
139
+ locations.push(Number(match[1]));
140
+ match = locationPattern.exec(source);
141
+ }
142
+ return locations;
143
+ }
144
+
145
+ function getWGSLDeclaredNames(source: string): string[] {
146
+ const names: string[] = [];
147
+ const namePattern = /(?:^|,)\s*(?:@[A-Za-z_][\w]*(?:\([^)]*\))?\s*)*([A-Za-z_][\w]*)\s*:/gm;
148
+ let match = namePattern.exec(source);
149
+ while (match) {
150
+ names.push(match[1]);
151
+ match = namePattern.exec(source);
152
+ }
153
+ return names;
154
+ }
155
+
156
+ function getWGSLParameterTypes(source: string): string[] {
157
+ const types: string[] = [];
158
+ const typePattern = /:\s*([A-Za-z_][\w]*)\b/g;
159
+ let match = typePattern.exec(source);
160
+ while (match) {
161
+ types.push(match[1]);
162
+ match = typePattern.exec(source);
163
+ }
164
+ return types;
165
+ }
166
+
167
+ function getWGSLStructBody(source: string, structName: string): string | null {
168
+ const structPattern = new RegExp(`\\bstruct\\s+${escapeRegExp(structName)}\\s*\\{`, 'g');
169
+ const match = structPattern.exec(source);
170
+ if (!match) {
171
+ return null;
172
+ }
173
+ const openBrace = source.indexOf('{', match.index);
174
+ const closeBrace = findMatchingDelimiter(source, openBrace, '{', '}');
175
+ return closeBrace < 0 ? null : source.slice(openBrace + 1, closeBrace);
176
+ }
177
+
178
+ function hasWGSLModuleVariable(source: string, name: string): boolean {
179
+ const escapedName = escapeRegExp(name);
180
+ const variablePattern = new RegExp(`\\b(?:var(?:<[^>]+>)?|let|const)\\s+${escapedName}\\b`, 'g');
181
+ let match = variablePattern.exec(source);
182
+ while (match) {
183
+ if (getBraceDepthAt(source, match.index) === 0) {
184
+ return true;
185
+ }
186
+ match = variablePattern.exec(source);
187
+ }
188
+ return false;
189
+ }
190
+
191
+ function getFirstUnusedLocation(usedLocations: Set<number>): number {
192
+ let location = 0;
193
+ while (usedLocations.has(location)) {
194
+ location++;
195
+ }
196
+ return location;
197
+ }
198
+
199
+ function findMatchingDelimiter(
200
+ source: string,
201
+ openIndex: number,
202
+ openCharacter: string,
203
+ closeCharacter: string
204
+ ): number {
205
+ let depth = 0;
206
+ let blockCommentDepth = 0;
207
+ let lineComment = false;
208
+ for (let index = openIndex; index < source.length; index++) {
209
+ const character = source[index];
210
+ const nextCharacter = source[index + 1];
211
+ if (lineComment) {
212
+ if (character === '\n') lineComment = false;
213
+ continue;
214
+ }
215
+ if (blockCommentDepth > 0) {
216
+ if (character === '/' && nextCharacter === '*') {
217
+ blockCommentDepth++;
218
+ index++;
219
+ } else if (character === '*' && nextCharacter === '/') {
220
+ blockCommentDepth--;
221
+ index++;
222
+ }
223
+ continue;
224
+ }
225
+ if (character === '/' && nextCharacter === '/') {
226
+ lineComment = true;
227
+ index++;
228
+ continue;
229
+ }
230
+ if (character === '/' && nextCharacter === '*') {
231
+ blockCommentDepth = 1;
232
+ index++;
233
+ continue;
234
+ }
235
+ if (character === openCharacter) depth++;
236
+ if (character === closeCharacter && --depth === 0) return index;
237
+ }
238
+ return -1;
239
+ }
240
+
241
+ function getBraceDepthAt(source: string, endIndex: number): number {
242
+ let depth = 0;
243
+ let blockCommentDepth = 0;
244
+ let lineComment = false;
245
+ for (let index = 0; index < endIndex; index++) {
246
+ const character = source[index];
247
+ const nextCharacter = source[index + 1];
248
+ if (lineComment) {
249
+ if (character === '\n') lineComment = false;
250
+ continue;
251
+ }
252
+ if (blockCommentDepth > 0) {
253
+ if (character === '/' && nextCharacter === '*') {
254
+ blockCommentDepth++;
255
+ index++;
256
+ } else if (character === '*' && nextCharacter === '/') {
257
+ blockCommentDepth--;
258
+ index++;
259
+ }
260
+ continue;
261
+ }
262
+ if (character === '/' && nextCharacter === '/') {
263
+ lineComment = true;
264
+ index++;
265
+ } else if (character === '/' && nextCharacter === '*') {
266
+ blockCommentDepth = 1;
267
+ index++;
268
+ } else if (character === '{') {
269
+ depth++;
270
+ } else if (character === '}') {
271
+ depth--;
272
+ }
273
+ }
274
+ return depth;
275
+ }
276
+
277
+ function escapeRegExp(value: string): string {
278
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
279
+ }
@@ -25,6 +25,8 @@ export type UniformInfo = {
25
25
  export type ShaderModuleBindingLayout = {
26
26
  name: string;
27
27
  group: number;
28
+ /** Which shader stages can access this binding. Uses GPUShaderStage bit flags. */
29
+ visibility?: number;
28
30
  };
29
31
 
30
32
  // Helper types
@@ -78,6 +80,7 @@ export type ShaderModule<
78
80
  prevUniforms?: UniformsT
79
81
  ) => Partial<UniformsT & BindingsT>;
80
82
 
83
+ /** Boolean or numeric preprocessor defines contributed by this shader module. */
81
84
  defines?: Record<string, boolean | number>;
82
85
  /** Injections */
83
86
  inject?: Record<string, string | {injection: string; order: number}>;
@@ -0,0 +1,19 @@
1
+ // luma.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {UniformValue} from '../utils/uniform-types';
6
+ import type {ShaderPass, ShaderPassInputSource, ShaderPassRenderTarget} from './shader-pass';
7
+
8
+ export type ShaderPassPipelineStep<TargetNameT extends string = string> = {
9
+ shaderPass: ShaderPass<any, any, any, any>;
10
+ inputs?: Record<string, ShaderPassInputSource<TargetNameT>>;
11
+ output?: 'previous' | TargetNameT;
12
+ uniforms?: Record<string, UniformValue>;
13
+ };
14
+
15
+ export type ShaderPassPipeline<TargetNameT extends string = string> = {
16
+ name: string;
17
+ renderTargets?: Record<TargetNameT, ShaderPassRenderTarget>;
18
+ steps: ShaderPassPipelineStep<TargetNameT>[];
19
+ };
@@ -2,10 +2,26 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import type {Binding} from '@luma.gl/core';
5
+ import type {Binding, TextureFormat} from '@luma.gl/core';
6
6
  import type {PickBindings, PickUniforms, ShaderModule} from './shader-module';
7
7
  import type {UniformValue} from '../utils/uniform-types';
8
8
 
9
+ export type ShaderPassRenderTarget = {
10
+ /** Width and height scale relative to the renderer output size. */
11
+ scale?: [number, number];
12
+ /** Render target format. Defaults to the device preferred color format. */
13
+ format?: TextureFormat;
14
+ /** Resource lifetime. History targets retain their last successfully rendered value. */
15
+ lifetime?: 'transient' | 'history';
16
+ /** Initial value used after construction, resize, or an explicit history reset. */
17
+ initialize?: 'original' | {clearColor: [number, number, number, number]};
18
+ };
19
+
20
+ export type ShaderPassInputSource<TargetNameT extends string = string> =
21
+ | 'original'
22
+ | 'previous'
23
+ | TargetNameT;
24
+
9
25
  /**
10
26
  * A ShaderPass is a ShaderModule that can be run "standalone" (e.g. post processing effects)
11
27
  * It adds additional information on how to run the module.
@@ -14,17 +30,20 @@ import type {UniformValue} from '../utils/uniform-types';
14
30
  export type ShaderPass<
15
31
  PropsT extends Record<string, any> = Record<string, any>,
16
32
  UniformsT extends Record<string, UniformValue> = PickUniforms<PropsT>,
17
- BindingsT extends Record<string, Binding> = PickBindings<PropsT>
33
+ BindingsT extends Record<string, Binding> = PickBindings<PropsT>,
34
+ RenderTargetNameT extends string = never
18
35
  > = ShaderModule<PropsT, UniformsT, BindingsT> & {
19
36
  /** A shader pass can run multiple sub passes */
20
- passes?: ShaderSubPass<UniformsT>[];
37
+ passes?: ShaderSubPass<UniformsT, Extract<keyof BindingsT, string>, RenderTargetNameT>[];
21
38
  // TODO better name
22
39
  // subPasses?: ShaderSubPass[];
23
40
  };
24
41
 
25
42
  /** Information on how to run a specific sub pass */
26
43
  export type ShaderSubPass<
27
- UniformsT extends Record<string, UniformValue> = Record<string, UniformValue>
44
+ UniformsT extends Record<string, UniformValue> = Record<string, UniformValue>,
45
+ BindingNameT extends string = string,
46
+ RenderTargetNameT extends string = string
28
47
  > = {
29
48
  /**
30
49
  * Action indicates whether this pass:
@@ -35,4 +54,10 @@ export type ShaderSubPass<
35
54
  sampler?: boolean;
36
55
  filter?: boolean;
37
56
  uniforms?: UniformsT;
57
+ /** Mapping from shader binding name to the logical texture source. */
58
+ inputs?: Partial<
59
+ Record<BindingNameT | 'sourceTexture', ShaderPassInputSource<RenderTargetNameT>>
60
+ >;
61
+ /** Output target for this sub pass. Defaults to the shared "previous" chain. */
62
+ output?: 'previous' | RenderTargetNameT;
38
63
  };
@@ -0,0 +1,214 @@
1
+ // luma.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {ShaderInjection} from './shader-assembly/shader-injections';
6
+ import type {ShaderModule} from './shader-module/shader-module';
7
+ import {shaderTypeDecoder, type AttributeShaderType} from '@luma.gl/core';
8
+
9
+ /** Named shader anchor or hook target accepted by ShaderPlugin injections. */
10
+ export type ShaderPluginInjectionTarget =
11
+ | 'vs:#decl'
12
+ | 'vs:#main-start'
13
+ | 'vs:#main-end'
14
+ | 'fs:#decl'
15
+ | 'fs:#main-start'
16
+ | 'fs:#main-end'
17
+ | `vs:${string}`
18
+ | `fs:${string}`;
19
+
20
+ /** Named shader source injection contributed by a ShaderPlugin. */
21
+ export type ShaderPluginInjection = {
22
+ /** Named shader anchor or hook that receives this source. */
23
+ target: ShaderPluginInjectionTarget;
24
+ /** Shader source to inject at the target. */
25
+ injection: string;
26
+ /** Relative injection order within the target. */
27
+ order?: number;
28
+ };
29
+
30
+ /** Portable interpolation modes for ShaderPlugin varyings. */
31
+ export type ShaderPluginVaryingInterpolation = 'smooth' | 'flat';
32
+
33
+ /** Cross-stage value contributed by a ShaderPlugin. */
34
+ export type ShaderPluginVarying = {
35
+ /** Portable scalar or vector shader type. */
36
+ type: AttributeShaderType;
37
+ /** Defaults to smooth for floating-point types and flat for integer types. */
38
+ interpolation?: ShaderPluginVaryingInterpolation;
39
+ };
40
+
41
+ /** Fully normalized ShaderPlugin varying declaration. */
42
+ export type ResolvedShaderPluginVarying = {
43
+ type: AttributeShaderType;
44
+ interpolation: ShaderPluginVaryingInterpolation;
45
+ };
46
+
47
+ /** Shared or backend-specific shader assembly contributions. */
48
+ export type ShaderPluginVariant = {
49
+ /** Shader modules added to shader assembly. */
50
+ modules?: ShaderModule[];
51
+ /** Boolean preprocessor defines added to shader assembly. */
52
+ defines?: Record<string, boolean>;
53
+ /** Named code injections added to shader assembly. */
54
+ injections?: ShaderPluginInjection[];
55
+ /** Shader-facing vertex inputs added by this plugin. Buffer ownership remains with the caller. */
56
+ vertexInputs?: Record<string, AttributeShaderType>;
57
+ /** Values transported from the selected vertex entry point to the fragment entry point. */
58
+ varyings?: Record<string, ShaderPluginVarying>;
59
+ };
60
+
61
+ /** Reusable cross-backend shader assembly descriptor. */
62
+ export type ShaderPlugin = ShaderPluginVariant & {
63
+ /** Stable plugin identifier. */
64
+ name: string;
65
+ /** GLSL-only shader assembly contributions. */
66
+ glsl?: ShaderPluginVariant;
67
+ /** WGSL-only shader assembly contributions. */
68
+ wgsl?: ShaderPluginVariant;
69
+ };
70
+
71
+ /** ShaderPlugin contributions resolved for one shader language. */
72
+ export type ResolvedShaderPlugins = {
73
+ /** Resolved shader modules in plugin declaration order. */
74
+ modules: ShaderModule[];
75
+ /** Resolved boolean preprocessor defines. */
76
+ defines: Record<string, boolean>;
77
+ /** Resolved named code injections grouped by target. */
78
+ injections: Record<string, ShaderInjection[]>;
79
+ /** Resolved shader-facing vertex inputs in plugin declaration order. */
80
+ vertexInputs: Record<string, AttributeShaderType>;
81
+ /** Resolved cross-stage varyings in plugin declaration order. */
82
+ varyings: Record<string, ResolvedShaderPluginVarying>;
83
+ };
84
+
85
+ const SHADER_PLUGIN_INJECTION_TARGET_REGEX =
86
+ /^(vs|fs):(?:#(?:decl|main-start|main-end)|[A-Za-z_][\w-]*)$/;
87
+
88
+ /** Resolve shared and backend-specific contributions from shader plugins. */
89
+ export function resolveShaderPlugins(
90
+ plugins: readonly ShaderPlugin[] = [],
91
+ shaderLanguage: 'glsl' | 'wgsl'
92
+ ): ResolvedShaderPlugins {
93
+ const modules: ShaderModule[] = [];
94
+ const defines: Record<string, boolean> = {};
95
+ const injections: Record<string, ShaderInjection[]> = {};
96
+ const vertexInputs: Record<string, AttributeShaderType> = {};
97
+ const varyings: Record<string, ResolvedShaderPluginVarying> = {};
98
+
99
+ for (const plugin of plugins) {
100
+ appendShaderPluginVariant({modules, defines, injections, vertexInputs, varyings}, plugin);
101
+ appendShaderPluginVariant(
102
+ {modules, defines, injections, vertexInputs, varyings},
103
+ plugin[shaderLanguage]
104
+ );
105
+ }
106
+
107
+ for (const name of Object.keys(varyings)) {
108
+ if (vertexInputs[name]) {
109
+ throw new Error(`ShaderPlugin name "${name}" cannot be both a vertex input and a varying`);
110
+ }
111
+ }
112
+
113
+ return {modules, defines, injections, vertexInputs, varyings};
114
+ }
115
+
116
+ /** Preserve explicit module inputs while appending plugin modules by first-seen name. */
117
+ export function mergeShaderPluginModules(
118
+ modules: readonly ShaderModule[] = [],
119
+ pluginModules: readonly ShaderModule[] = []
120
+ ): ShaderModule[] {
121
+ const mergedModules: ShaderModule[] = [...modules];
122
+ const seenModuleNames = new Set(mergedModules.map(module => module.name));
123
+
124
+ for (const pluginModule of pluginModules) {
125
+ if (!seenModuleNames.has(pluginModule.name)) {
126
+ mergedModules.push(pluginModule);
127
+ seenModuleNames.add(pluginModule.name);
128
+ }
129
+ }
130
+
131
+ return mergedModules;
132
+ }
133
+
134
+ function appendShaderPluginVariant(
135
+ resolved: ResolvedShaderPlugins,
136
+ variant: ShaderPluginVariant | undefined
137
+ ): void {
138
+ if (!variant) {
139
+ return;
140
+ }
141
+
142
+ if (variant.modules?.length) {
143
+ resolved.modules.push(...variant.modules);
144
+ }
145
+ if (variant.defines) {
146
+ Object.assign(resolved.defines, variant.defines);
147
+ }
148
+ for (const [name, type] of Object.entries(variant.vertexInputs || {})) {
149
+ assertShaderPluginInterfaceName(name, 'vertex input');
150
+ const existingType = resolved.vertexInputs[name];
151
+ if (existingType && existingType !== type) {
152
+ throw new Error(
153
+ `ShaderPlugin vertex input "${name}" has conflicting types "${existingType}" and "${type}"`
154
+ );
155
+ }
156
+ resolved.vertexInputs[name] = type;
157
+ }
158
+ for (const [name, varying] of Object.entries(variant.varyings || {})) {
159
+ assertShaderPluginInterfaceName(name, 'varying');
160
+ const normalizedVarying = normalizeShaderPluginVarying(name, varying);
161
+ const existingVarying = resolved.varyings[name];
162
+ if (
163
+ existingVarying &&
164
+ (existingVarying.type !== normalizedVarying.type ||
165
+ existingVarying.interpolation !== normalizedVarying.interpolation)
166
+ ) {
167
+ throw new Error(
168
+ `ShaderPlugin varying "${name}" has conflicting declarations ` +
169
+ `"${existingVarying.type}/${existingVarying.interpolation}" and ` +
170
+ `"${normalizedVarying.type}/${normalizedVarying.interpolation}"`
171
+ );
172
+ }
173
+ resolved.varyings[name] = normalizedVarying;
174
+ }
175
+
176
+ for (const injection of variant.injections || []) {
177
+ assertNamedShaderPluginInjectionTarget(injection.target);
178
+ if (!resolved.injections[injection.target]) {
179
+ resolved.injections[injection.target] = [];
180
+ }
181
+ const injections = resolved.injections[injection.target];
182
+ injections.push({
183
+ injection: injection.injection,
184
+ order: injection.order ?? 0
185
+ });
186
+ }
187
+ }
188
+
189
+ function assertShaderPluginInterfaceName(name: string, kind: 'vertex input' | 'varying'): void {
190
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || name.startsWith('_luma_')) {
191
+ throw new Error(`ShaderPlugin ${kind} "${name}" must be a valid non-reserved identifier`);
192
+ }
193
+ }
194
+
195
+ function normalizeShaderPluginVarying(
196
+ name: string,
197
+ varying: ShaderPluginVarying
198
+ ): ResolvedShaderPluginVarying {
199
+ const {primitiveType} = shaderTypeDecoder.getAttributeShaderTypeInfo(varying.type);
200
+ const integerType = primitiveType === 'i32' || primitiveType === 'u32';
201
+ const interpolation = varying.interpolation || (integerType ? 'flat' : 'smooth');
202
+ if (integerType && interpolation === 'smooth') {
203
+ throw new Error(`ShaderPlugin integer varying "${name}" must use flat interpolation`);
204
+ }
205
+ return {type: varying.type, interpolation};
206
+ }
207
+
208
+ function assertNamedShaderPluginInjectionTarget(target: ShaderPluginInjectionTarget): void {
209
+ if (!SHADER_PLUGIN_INJECTION_TARGET_REGEX.test(target)) {
210
+ throw new Error(
211
+ `ShaderPlugin injection target "${target}" must be a named shader anchor or hook`
212
+ );
213
+ }
214
+ }
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- // Recommendation is to ignore message but current test suite checks agains the
5
+ // Recommendation is to ignore message but current test suite checks against the
6
6
  // message so keep it for now.
7
7
  export function assert(condition: unknown, message?: string): void {
8
8
  if (!condition) {