@volter/threejs-runtime 0.5.82 → 0.5.83

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@volter/threejs-runtime",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.5.82",
5
+ "version": "0.5.83",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
@@ -43,7 +43,7 @@
43
43
  "@dimforge/rapier3d-compat": "^0.14.0",
44
44
  "@recast-navigation/three": "^0.43.0",
45
45
  "@sparkjsdev/spark": "^2.1.0",
46
- "@volter/editor-project": "0.5.82",
46
+ "@volter/editor-project": "0.5.83",
47
47
  "n8ao": "^1.10.1",
48
48
  "postprocessing": "^6.38.0",
49
49
  "quarks.core": "0.16.0",
@@ -103,9 +103,11 @@ export interface IblOverrideOptions {
103
103
  /**
104
104
  * The GLSL that replaces `#include <envmap_physical_pars_fragment>`. Read at
105
105
  * COMPILE time, not at install time, so a caller whose probe count is still
106
- * settling does not have to reinstall to change it.
106
+ * settling does not have to reinstall to change it. `null` leaves this
107
+ * program as three's own: nothing is spliced and `onCompile` is not called
108
+ * (a caller with no probes to light, say).
107
109
  */
108
- readonly fragment: () => string;
110
+ readonly fragment: () => string | null;
109
111
  /**
110
112
  * Appended to the material's own `customProgramCacheKey`, after a `|`. Must
111
113
  * distinguish every shape of {@link IblOverrideOptions.fragment} the caller
@@ -129,8 +131,14 @@ export interface IblOverride {
129
131
  * `shader.uniforms`.
130
132
  */
131
133
  readonly shader: IblOverrideShader | null;
132
- /** Put the material's own `onBeforeCompile`/`customProgramCacheKey` back. */
133
- restore(): void;
134
+ /**
135
+ * Put the material's own `onBeforeCompile`/`customProgramCacheKey` back, if
136
+ * they are still this override's, and return `true`. Once something else has
137
+ * installed its hooks over it, restoring would remove theirs too, so the
138
+ * override stays and this returns `false`: the caller then still owns an
139
+ * override that is live.
140
+ */
141
+ restore(): boolean;
134
142
  }
135
143
 
136
144
  /**
@@ -139,7 +147,10 @@ export interface IblOverride {
139
147
  * (in particular this never assigns `envMap`, which stays the caller's).
140
148
  *
141
149
  * The caller's own `onBeforeCompile` runs FIRST and its result is what gets
142
- * spliced, so an override composes over a material that already had one.
150
+ * spliced, so an override composes over a material that already had one. If
151
+ * that hook has already consumed a chunk the splice needs, the program keeps
152
+ * three's own lighting and the material warns once, rather than failing the
153
+ * draw.
143
154
  */
144
155
  export function overrideMaterialIbl(
145
156
  material: MeshStandardMaterial,
@@ -148,11 +159,23 @@ export function overrideMaterialIbl(
148
159
  const previousCompile = material.onBeforeCompile;
149
160
  const previousCacheKey = material.customProgramCacheKey;
150
161
  let compiled: IblOverrideShader | null = null;
162
+ let warned = false;
151
163
 
152
- material.onBeforeCompile = (shader, renderer) => {
164
+ const compile: MeshStandardMaterial['onBeforeCompile'] = (shader, renderer) => {
153
165
  previousCompile.call(material, shader, renderer);
154
- requireChunk(shader.vertexShader, '#include <worldpos_vertex>');
155
- requireChunk(shader.fragmentShader, '#include <envmap_physical_pars_fragment>');
166
+ const fragment = options.fragment();
167
+ if (fragment === null) return;
168
+ const missing = missingChunk(shader);
169
+ if (missing !== null) {
170
+ if (!warned) {
171
+ warned = true;
172
+ // biome-ignore lint/suspicious/noConsole: a hook before this one took a chunk the splice needs; say why the override is not drawn.
173
+ console.warn(
174
+ `[overrideMaterialIbl] material "${material.name}" keeps three's own lighting: its program no longer contains ${missing}, which an earlier onBeforeCompile replaced.`,
175
+ );
176
+ }
177
+ return;
178
+ }
156
179
  shader.vertexShader = `varying vec3 ${IBL_WORLD_POSITION};\n${shader.vertexShader.replace(
157
180
  '#include <worldpos_vertex>',
158
181
  `#include <worldpos_vertex>
@@ -162,12 +185,14 @@ export function overrideMaterialIbl(
162
185
  )}`;
163
186
  shader.fragmentShader = `varying vec3 ${IBL_WORLD_POSITION};\n${shader.fragmentShader.replace(
164
187
  '#include <envmap_physical_pars_fragment>',
165
- options.fragment(),
188
+ fragment,
166
189
  )}`;
167
190
  compiled = shader;
168
191
  options.onCompile?.(shader);
169
192
  };
170
- material.customProgramCacheKey = () => `${previousCacheKey.call(material)}|${options.cacheKey()}`;
193
+ const cacheKey = () => `${previousCacheKey.call(material)}|${options.cacheKey()}`;
194
+ material.onBeforeCompile = compile;
195
+ material.customProgramCacheKey = cacheKey;
171
196
  material.needsUpdate = true;
172
197
 
173
198
  return {
@@ -176,27 +201,31 @@ export function overrideMaterialIbl(
176
201
  return compiled;
177
202
  },
178
203
  restore() {
204
+ // A hook installed after this one wraps it: putting the earlier hooks back
205
+ // would remove that one too, so the override then stays.
206
+ if (material.onBeforeCompile !== compile || material.customProgramCacheKey !== cacheKey) return false;
179
207
  material.onBeforeCompile = previousCompile;
180
208
  material.customProgramCacheKey = previousCacheKey;
181
209
  material.needsUpdate = true;
182
210
  compiled = null;
211
+ return true;
183
212
  },
184
213
  };
185
214
  }
186
215
 
187
216
  /**
188
- * A missing chunk is a NAMED error rather than a silent no-op.
217
+ * The chunk the splice needs that `shader` no longer contains, or null.
189
218
  *
190
219
  * `String.replace` with no match returns the string unchanged, which would
191
220
  * leave the varying declared and never assigned — a shader that links and
192
- * renders the wrong thing. The only way to reach this today is a preceding
221
+ * renders the wrong thing. The way to reach this is a preceding
193
222
  * `onBeforeCompile` in the same chain having already replaced the chunk (two
194
- * IBL overrides stacked on one material), and that is worth a message naming
195
- * the chunk instead of a black frame.
223
+ * IBL overrides stacked on one material).
196
224
  */
197
- function requireChunk(source: string, chunk: string): void {
198
- if (source.includes(chunk)) return;
199
- throw new Error(
200
- `overrideMaterialIbl needs three's ${chunk} shader chunk, and this material's program no longer contains it.`,
201
- );
225
+ function missingChunk(shader: IblOverrideShader): string | null {
226
+ if (!shader.vertexShader.includes('#include <worldpos_vertex>')) return '#include <worldpos_vertex>';
227
+ if (!shader.fragmentShader.includes('#include <envmap_physical_pars_fragment>')) {
228
+ return '#include <envmap_physical_pars_fragment>';
229
+ }
230
+ return null;
202
231
  }