@meimu/mutracer 0.1.4 → 0.1.5

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 (67) hide show
  1. package/dist/assets/bvh.worker.js +1 -1
  2. package/dist/mutracer.umd.js +1425 -1421
  3. package/docs/api.md +2 -1
  4. package/docs/materials.md +17 -26
  5. package/docs/threejs.md +5 -5
  6. package/docs/viewer.md +36 -3
  7. package/lib/oidn.d.ts +1 -1
  8. package/lib/oidnBundledWeights.cdn.d.ts +2 -2
  9. package/lib/renderer/WGPUFullQuadPass.js +3 -3
  10. package/lib/renderer/WGPUJNLMDenoiser.js +35 -35
  11. package/lib/renderer/WGPURayTraceOutput.js +4 -4
  12. package/lib/renderer/WGPURayTracePass.js +5 -3
  13. package/lib/renderer/WGPURayTracePass.js.map +1 -1
  14. package/lib/renderer/WGPURenderingPipeline.js +7 -7
  15. package/lib/renderer/WebGLJNLMDenoiser.js +36 -36
  16. package/lib/renderer/WebGLRayTracePass.js +1 -0
  17. package/lib/renderer/WebGLRayTracePass.js.map +1 -1
  18. package/lib/renderer/glsl/chunks/cameraRay.glsl.js +8 -8
  19. package/lib/renderer/glsl/chunks/envMap.glsl.js +26 -26
  20. package/lib/renderer/glsl/chunks/intersect.glsl.js +141 -139
  21. package/lib/renderer/glsl/chunks/intersect.glsl.js.map +1 -1
  22. package/lib/renderer/glsl/chunks/materialBuffer.glsl.js +56 -55
  23. package/lib/renderer/glsl/chunks/materialBuffer.glsl.js.map +1 -1
  24. package/lib/renderer/glsl/chunks/openPbrBsdf.glsl.js +144 -144
  25. package/lib/renderer/glsl/chunks/openPbrBsdf.glsl.js.map +1 -1
  26. package/lib/renderer/glsl/chunks/random.glsl.js +1 -1
  27. package/lib/renderer/glsl/chunks/rayTraceCore.glsl.js +14 -14
  28. package/lib/renderer/glsl/chunks/rayTraceCore.glsl.js.map +1 -1
  29. package/lib/renderer/glsl/chunks/sampleLight.glsl.js +19 -19
  30. package/lib/renderer/glsl/chunks/sampleMaterial.glsl.js +46 -46
  31. package/lib/renderer/glsl/chunks/sampleMaterial.glsl.js.map +1 -1
  32. package/lib/renderer/glsl/chunks/sampleShadowCatcher.glsl.js +6 -6
  33. package/lib/renderer/glsl/chunks/sampling.glsl.js +33 -33
  34. package/lib/renderer/glsl/chunks/sobol.glsl.js +7 -7
  35. package/lib/renderer/glsl/chunks/uvProjection.glsl.js +1 -1
  36. package/lib/renderer/glsl/depthQuery.frag.js +2 -2
  37. package/lib/renderer/glsl/rayTrace.frag.js +67 -67
  38. package/lib/renderer/glsl/shadowCatcher.frag.js +3 -3
  39. package/lib/renderer/glsl/webglPipeline.frag.js +26 -26
  40. package/lib/renderer/scratchSampling.js +16 -16
  41. package/lib/renderer/wgsl/chunks/envMap.wgsl.js +14 -14
  42. package/lib/renderer/wgsl/chunks/intersect.wgsl.js +178 -178
  43. package/lib/renderer/wgsl/chunks/intersect.wgsl.js.map +1 -1
  44. package/lib/renderer/wgsl/chunks/intersectionAlpha.wgsl.js +3 -3
  45. package/lib/renderer/wgsl/chunks/materialBuffer.wgsl.js +24 -23
  46. package/lib/renderer/wgsl/chunks/materialBuffer.wgsl.js.map +1 -1
  47. package/lib/renderer/wgsl/chunks/openPbrBsdf.wgsl.js +160 -160
  48. package/lib/renderer/wgsl/chunks/pdm_displacement.wgsl.js +12 -12
  49. package/lib/renderer/wgsl/chunks/rayTraceCore.wgsl.js +5 -5
  50. package/lib/renderer/wgsl/chunks/sampleLight.wgsl.js +44 -44
  51. package/lib/renderer/wgsl/chunks/sampleMaterial.wgsl.js +69 -69
  52. package/lib/renderer/wgsl/chunks/sampleShadowCatcher.wgsl.js +3 -3
  53. package/lib/renderer/wgsl/chunks/sampling.wgsl.js +33 -33
  54. package/lib/renderer/wgsl/chunks/sobol.wgsl.js +7 -7
  55. package/lib/renderer/wgsl/chunks/uvProjection.wgsl.js +1 -1
  56. package/lib/renderer/wgsl/compose.frag.wgsl.js +54 -54
  57. package/lib/renderer/wgsl/customDenoise.wgsl.js +6 -6
  58. package/lib/renderer/wgsl/depthQuery.wgsl.js +2 -2
  59. package/lib/renderer/wgsl/offlineAdaptive.compute.js +5 -5
  60. package/lib/renderer/wgsl/rayTrace.compute.js +103 -103
  61. package/lib/scene/OpenPBRMaterial.d.ts +1 -1
  62. package/lib/scene/OpenPBRMaterial.js +1 -1
  63. package/lib/scene/OpenPBRMaterial.js.map +1 -1
  64. package/lib/version.d.ts +1 -1
  65. package/lib/version.js +1 -1
  66. package/lib/version.js.map +1 -1
  67. package/package.json +1 -1
package/docs/api.md CHANGED
@@ -333,7 +333,8 @@ one. See [thin-film textures](./materials.md#textures) for range behavior.
333
333
  `undefined`. Channels and color decoding are listed in [material textures](./materials.md#textures).
334
334
  They follow the existing material/texture update mechanism.
335
335
 
336
- `coatNormalMap` defaults to `undefined`, retaining the shared shading normal. With a map, linear RGB supplies an independent coat normal; `coatNormalScale` defaults to `[1, 1]` and scales its decoded XY. See [coat normals](./materials.md#coat-normals) for frame, sampling and update behavior.
336
+ `coatNormalMap` defaults to `undefined`; `coatNormalScale` defaults to `[1, 1]`.
337
+ See [coat normals](./materials.md#coat-normals) for independent base and coat normal mapping.
337
338
 
338
339
  Raw texture sources contain exactly `width * height * 4` RGBA values, with
339
340
  rows ordered from top to bottom. `Uint8Array` and `Uint8ClampedArray` store
package/docs/materials.md CHANGED
@@ -260,41 +260,32 @@ with coat, and the finite path-event budget can truncate deep glass paths.
260
260
  ## Coat normals
261
261
 
262
262
  Set `coatNormalMap` to give the coat a tangent-space normal independently of
263
- `normalMap`. The renderer decodes linear RGB as `2 * RGB - 1`, multiplies XY
264
- by `coatNormalScale`, then normalizes. The scale defaults to `[1, 1]` and can
265
- be negative or zero. A missing map (the default), zero coat weight or zero sampled coat coverage retains the existing shared
266
- shading normal, including the base normal map.
267
-
268
- The coat TBN starts from the interpolated geometry normal before base normal
269
- mapping. It uses the coat texture's UV0/UV1 selection and geometric
270
- tangent handedness; generated tangents use UV1 differentials when that set is
271
- selected. Degenerate frames and zero decoded vectors use stable fallbacks.
272
- The texture's scale, offset and `flipY` follow the ordinary atlas sampling
273
- rules. As with other slots, sharing an image never changes its color space.
274
-
275
- Base lobes keep their base frame. Coat reflection, its sampling PDF, directional
276
- albedo and passage absorption use the coat frame; the renderer combines their
277
- responses and densities for the same world direction. Each frame is corrected
278
- against the geometric/view hemisphere independently. Triangle normals still
279
- control reflection versus transmission, medium boundaries and ray offsets.
280
- A coat reflection can contribute below the base shading hemisphere when it
281
- remains above the coat and geometric hemispheres. As before, solid interior
282
- hits omit coat reflection; thin surfaces are treated as exterior hits.
263
+ `normalMap`. Without a coat map (the default), the coat uses the interpolated
264
+ surface normal before base normal mapping. A bumpy base can therefore have a
265
+ smooth coat. Assign the same texture to both maps to perturb both layers.
283
266
 
284
- Strongly tilted shading normals lose some lobe support when clipped to the
285
- geometric hemisphere. A unit white environment can therefore render below
286
- unit radiance; the renderer does not renormalize the clipped lobes.
267
+ The map decodes linear RGB as `2 * RGB - 1`, multiplies XY by
268
+ `coatNormalScale`, then normalizes. The scale defaults to `[1, 1]` and can be
269
+ negative or zero. UV0/UV1 selection, scale, offset and `flipY` follow the
270
+ ordinary texture rules. Zero coat weight or zero sampled coat coverage
271
+ disables the coat without changing the base normal.
287
272
 
288
273
  Replacing the map or editing its scale is detected by `engine.update()`. Call
289
274
  `texture.markUpdated()` after pixel or UV-array element edits; see [scene updates](./updates.md).
290
- These edits update material parameters/textures without rebuilding geometry. Normal maps do not alter
291
- emission's existing normal-incidence absorption or light-selection proposals.
275
+
276
+ Coat reflections can remain visible where the base normal points away from
277
+ the light. Solid interior hits omit coat reflection; thin surfaces are
278
+ treated as exterior hits.
279
+
280
+ Strongly tilted shading normals lose some lobe support when clipped to the
281
+ geometric hemisphere. A unit white environment can therefore render below
282
+ unit radiance; the renderer does not renormalize the clipped lobes.
292
283
 
293
284
  ## Anisotropy directions
294
285
 
295
286
  Set `specularAnisotropyRotation` or provide `anisotropyMap` to orient the base specular lobe. The map reads linear `2 * RG - 1`, normalizes the direction, multiplies strength by B, and then applies the rotation. A decoded squared length at or below `1e-4` falls back to `(1, 0)` before rotation (including the two central RGBA8 codes). Without a map the direction is `(1, 0)` and the strength multiplier is one. The rotation defaults to zero; the map defaults to undefined.
296
287
 
297
- `coatAnisotropyRotation` defaults to zero and uses the geometry tangent independently of the base map. Base and coat evaluation, sampling and PDF use their respective directions. The coat uses the shared shading normal unless `coatNormalMap` selects an [independent normal](#coat-normals). Zero strength ignores the new direction and retains the original isotropic path.
288
+ `coatAnisotropyRotation` defaults to zero and uses the geometry tangent independently of the base map. Base and coat use their respective directions and [normals](#coat-normals). Zero anisotropy strength ignores the direction.
298
289
 
299
290
  Provided tangent XYZ and optional W handedness are preserved. For XYZ tangents the sign comes from triangle UV derivatives. Missing or degenerate tangents use UV derivatives and then a stable orthonormal fallback; missing tangents produce one diagnostic per geometry. The direction texture's selected UV set is used for derivative fallback, including UV1. A provided XYZW tangent remains authoritative. Directions are reorthogonalized against the final shading normal, including normal maps and back faces, and instance transforms preserve mirrored handedness. UV scale, offset and flipY affect texel lookup; they do not rotate the encoded direction.
300
291
 
package/docs/threejs.md CHANGED
@@ -108,12 +108,12 @@ or a source `THREE.Texture`. `THREESync` converts parameter edits without
108
108
  rewriting texture pixels. The iridescence strength texture is not supported;
109
109
  `thinFilmMap` represents thickness only.
110
110
 
111
- The six layer maps reuse the texture conversion cache, source UV channel,
112
- scale, offset and flipY. `mutracerOpenPBR` can override them with a MuTracer
111
+ The six layer maps preserve the source UV channel, scale, offset and `flipY`.
112
+ `mutracerOpenPBR` can override them with a MuTracer
113
113
  `Texture` or `THREE.Texture`; explicit `null` clears an imported map, while
114
- unknown JSON objects are ignored. `THREESync` follows texture edits without
115
- copying pixels. General `KHR_texture_transform` rotation remains unsupported.
116
- Clearcoat normals are deferred to the independent coat-frame implementation.
114
+ unknown JSON objects are ignored. `THREESync` follows texture edits.
115
+ General `KHR_texture_transform` rotation remains unsupported.
116
+ See [coat normal conversion](#coat-normal-conversion) for clearcoat normal maps.
117
117
  Anisotropy direction and source-model mapping are described [below](#anisotropy-source-conversion). The conversion preserves OpenPBR layering: glTF sheen
118
118
  layer order differs, and specular weight/color also affect metals in OpenPBR;
119
119
  these imports do not imply identical glTF BRDFs. A fully metallic material
package/docs/viewer.md CHANGED
@@ -1,4 +1,37 @@
1
- # Sharing viewer settings
1
+ # Using the viewer
2
+
3
+ Open **Examples → MuTracer collection** for models curated for this viewer, or
4
+ **Examples → glTF Research Scenes** for the independently maintained
5
+ [glTF Research Scenes](https://github.com/ErfanMo77/gltf-research-scenes) collection.
6
+ Scenes download on demand from GitHub; cards show approximate download sizes
7
+ and link to each scene's source and license. Large scenes need more memory.
8
+
9
+ glTF/GLB imports include directional, point and spot lights and emissive meshes.
10
+ Benchmark scenes also load their sunlight and available environment maps.
11
+ Under **Lighting → Environment**, **Scene lighting** is available only when the
12
+ model provides lights or environment lighting. Choose it to use the imported
13
+ lighting alone, or select an HDRI or Studio to add lighting. Emissive materials
14
+ still work with HDRI and Studio. Environment maps add download time and are
15
+ resampled to at most 2K width. If a source map is missing, the model still opens
16
+ and the Lighting panel explains which map is unavailable; choose an environment
17
+ there to replace it.
18
+
19
+ When an imported glTF/GLB contains cameras, a toast offers **Apply camera** to
20
+ use its first camera.
21
+ Dismiss the toast to keep the Orbit view. You can select any imported camera
22
+ in the separate camera toolbar at the bottom right; it appears only for models
23
+ with cameras. Drag, pan or zoom after selecting a camera to continue in **Orbit**
24
+ from that view.
25
+ Hover over a camera in the scene tree and click its camera action to apply it
26
+ directly. **Fit to model** in the bottom toolbar (or **F**) returns to Orbit
27
+ and frames the model.
28
+ Reloading the current model restores your saved camera selection without
29
+ showing the prompt again.
30
+
31
+ Picking an object in the viewport reveals it in the scene tree and scrolls it
32
+ into view. Material rows start collapsed; expand a mesh to see its materials.
33
+
34
+ ## Sharing viewer settings
2
35
 
3
36
  Add settings to a viewer link to override the recipient's saved preferences.
4
37
  For example, open the viewer with HDR output enabled:
@@ -17,7 +50,7 @@ The viewer updates the URL as lighting, render and display settings change.
17
50
  Copy the address bar to share those settings and the selected example model.
18
51
  Settings also work in the hash, such as `#model=dragon&hdrOutputEnabled=true`.
19
52
 
20
- ## Options
53
+ ### Options
21
54
 
22
55
  Parameter names match the viewer settings and are case-sensitive:
23
56
 
@@ -42,7 +75,7 @@ can override a recipient's different preference. When the viewer updates the
42
75
  address, settings move into the hash; unrelated query parameters, including
43
76
  `?webgl`, are preserved.
44
77
 
45
- ## Caveats
78
+ ### Caveats
46
79
 
47
80
  HDR output requires WebGPU, an HDR display and a compatible browser. Unsupported
48
81
  devices fall back to SDR. `?webgl` forces WebGL 2, which does not support HDR output.
package/lib/oidn.d.ts CHANGED
@@ -40,6 +40,6 @@ export declare const DEFAULT_OIDN_WEIGHTS_PACKAGE_BASE_URL = "https://cdn.jsdeli
40
40
  * Fallback for bundled standard/small weights when application assets fail.
41
41
  * Defaults to jsDelivr's `@meimu/mutracer@VERSION/oidn/weights/` directory.
42
42
  */
43
- export declare const DEFAULT_OIDN_WEIGHTS_BASE_URL = "https://cdn.jsdelivr.net/npm/@meimu/mutracer@0.1.4/oidn/weights/";
43
+ export declare const DEFAULT_OIDN_WEIGHTS_BASE_URL = "https://cdn.jsdelivr.net/npm/@meimu/mutracer@0.1.5/oidn/weights/";
44
44
  export declare function resolveOIDNModelUrl(model: OIDNModel, weightsBaseUrl?: string | URL): string;
45
45
  export declare function resolveOIDNAuxiliaryModelUrls(weightsBaseUrl?: string | URL): OIDNAuxiliaryModelUrls;
@@ -1,2 +1,2 @@
1
- export declare const standardWeightsUrl = "https://cdn.jsdelivr.net/npm/@meimu/mutracer@0.1.4/oidn/weights/rt_hdr_alb_nrm.tza";
2
- export declare const smallWeightsUrl = "https://cdn.jsdelivr.net/npm/@meimu/mutracer@0.1.4/oidn/weights/rt_hdr_alb_nrm_small.tza";
1
+ export declare const standardWeightsUrl = "https://cdn.jsdelivr.net/npm/@meimu/mutracer@0.1.5/oidn/weights/rt_hdr_alb_nrm.tza";
2
+ export declare const smallWeightsUrl = "https://cdn.jsdelivr.net/npm/@meimu/mutracer@0.1.5/oidn/weights/rt_hdr_alb_nrm_small.tza";
@@ -282,10 +282,10 @@ ${scratchWGSLFunctions(this._reserveCapacity ? this._regionNames : inputs.filter
282
282
  ${this._fsDefine ?? ''}
283
283
  @fragment
284
284
  fn main(
285
- @builtin(position) fG: vec4f
285
+ @builtin(position) fH: vec4f
286
286
  ) -> pd {
287
- let uv = vec2i(floor(fG.xy));
288
- let uv2 = fG.xy / viewportSize;
287
+ let uv = vec2i(floor(fH.xy));
288
+ let uv2 = fH.xy / viewportSize;
289
289
  var output: pd;
290
290
  ${this._fsMain}
291
291
  return output;
@@ -55,13 +55,13 @@ if (id.x >= size.x || id.y >= size.y) { return; }
55
55
  let center = rawGuide[id.x + id.y * size.x];
56
56
  var lu = 0.0;
57
57
  var jc = 0.0;
58
- var mZ = 0.0;
58
+ var mY = 0.0;
59
59
  var iX = 0.0;
60
60
  for (var dy = -1; dy <= 1; dy++) {
61
61
  for (var dx = -1; dx <= 1; dx++) {
62
62
  let q = clamp(vec2i(id.xy) + vec2i(dx, dy), vec2i(0), vec2i(size) - 1);
63
63
  let value = rawGuide[u32(q.x) + u32(q.y) * size.x];
64
- mZ += value.x;
64
+ mY += value.x;
65
65
  iX += value.x * value.x;
66
66
  if (value.y >= 0.0) {
67
67
  lu += value.y;
@@ -73,7 +73,7 @@ var variance: f32;
73
73
  if (center.y >= 0.0) {
74
74
  variance = lu / jc;
75
75
  } else {
76
- let lt = mZ / 9.0;
76
+ let lt = mY / 9.0;
77
77
  variance = max(iX / 9.0 - lt * lt, 0.0) * 9.0 / 8.0;
78
78
  }
79
79
  guide[id.x + id.y * size.x] = vec2f(center.x, variance);
@@ -122,19 +122,19 @@ const INV_NORMAL_BANDWIDTH2 = ${(1 / normalBandwidth ** 2).toFixed(6)};
122
122
 
123
123
  const CENTER_WEIGHT_MIN = 0.01;
124
124
 
125
- fn he(p: vec2i) -> u32 {
125
+ fn hg(p: vec2i) -> u32 {
126
126
  let q = clamp(p, vec2i(0), vec2i(params.size) - 1);
127
127
  return u32(q.x) + u32(q.y) * params.size.x;
128
128
  }
129
129
 
130
- fn lA(q: vec2i, bs: vec4f, gS: vec3f) -> f32 {
130
+ fn lw(q: vec2i, bs: vec4f, gT: vec3f) -> f32 {
131
131
  let albedo = textureLoad(albedoTex, q, 0);
132
132
 
133
133
 
134
134
  let pf = max((albedo.rgb + bs.rgb) * 0.5, vec3f(${JNLM_ALBEDO_FLOOR}));
135
135
  let ld = (albedo.rgb - bs.rgb) / pf;
136
136
  let iU = albedo.a - bs.a;
137
- let lp = textureLoad(normalTex, q, 0).rgb - gS;
137
+ let lp = textureLoad(normalTex, q, 0).rgb - gT;
138
138
  return max(
139
139
  dot(ld, ld) * INV_ALBEDO_BANDWIDTH2
140
140
  + iU * iU * INV_COVERAGE_BANDWIDTH2,
@@ -153,12 +153,12 @@ let p = vec2i(id.xy);
153
153
  if (p.x >= size.x || p.y >= size.y) { return; }
154
154
  let step = i32(params.dilation);
155
155
  let bs = textureLoad(albedoTex, p, 0);
156
- let fC = textureLoad(normalTex, p, 0);
157
- let gS = fC.rgb;
158
- let index = he(p);
156
+ let fD = textureLoad(normalTex, p, 0);
157
+ let gT = fD.rgb;
158
+ let index = hg(p);
159
159
 
160
160
 
161
- if (fC.a <= 0.0 && textureLoad(shadowCatcherTex, p, 0).r <= 0.0) {
161
+ if (fD.a <= 0.0 && textureLoad(shadowCatcherTex, p, 0).r <= 0.0) {
162
162
  outColor[index] = color[index];
163
163
  outGuide[index] = guide[index];
164
164
  return;
@@ -168,20 +168,20 @@ var lf: array<vec2f, PATCH_SIZE>;
168
168
  var k = 0;
169
169
  for (var dy = -PATCH_RADIUS; dy <= PATCH_RADIUS; dy++) {
170
170
  for (var dx = -PATCH_RADIUS; dx <= PATCH_RADIUS; dx++) {
171
- lf[k] = guide[he(p + vec2i(dx, dy) * step)];
171
+ lf[k] = guide[hg(p + vec2i(dx, dy) * step)];
172
172
  k++;
173
173
  }
174
174
  }
175
175
  let k2 = params.colorSensitivity * params.colorSensitivity;
176
176
 
177
177
  var sum = vec4f(0.0);
178
- let gV = max(bs.rgb, vec3f(${JNLM_ALBEDO_FLOOR}));
179
- var mq = vec3f(0.0);
180
- var gx = vec3f(0.0);
178
+ let gW = max(bs.rgb, vec3f(${JNLM_ALBEDO_FLOOR}));
179
+ var mp = vec3f(0.0);
180
+ var gy = vec3f(0.0);
181
181
  var hs = vec3f(0.0);
182
182
  var ps = 0.0;
183
- var gz = 0.0;
184
- var nV = 0.0;
183
+ var gA = 0.0;
184
+ var nU = 0.0;
185
185
  var kM = 0.0;
186
186
  for (var sy = -SEARCH_RADIUS; sy <= SEARCH_RADIUS; sy++) {
187
187
  for (var sx = -SEARCH_RADIUS; sx <= SEARCH_RADIUS; sx++) {
@@ -191,38 +191,38 @@ if (
191
191
  q.x < 0 || q.y < 0 || q.x >= size.x || q.y >= size.y
192
192
  ) { continue; }
193
193
 
194
- let features = lA(q, bs, gS);
194
+ let features = lw(q, bs, gT);
195
195
  if (features > 16.0) { continue; }
196
196
 
197
- var fX = 0.0;
197
+ var fY = 0.0;
198
198
  k = 0;
199
199
  for (var dy = -PATCH_RADIUS; dy <= PATCH_RADIUS; dy++) {
200
200
  for (var dx = -PATCH_RADIUS; dx <= PATCH_RADIUS; dx++) {
201
201
  let a = lf[k];
202
- let b = guide[he(q + vec2i(dx, dy) * step)];
202
+ let b = guide[hg(q + vec2i(dx, dy) * step)];
203
203
  let qU = a.x - b.x;
204
- fX += (qU * qU
204
+ fY += (qU * qU
205
205
  - VARIANCE_CANCELLATION * (a.y + min(a.y, b.y)))
206
206
  / (1e-10 + k2 * (a.y + b.y));
207
207
  k++;
208
208
  }
209
209
  }
210
- fX /= f32(PATCH_SIZE);
210
+ fY /= f32(PATCH_SIZE);
211
211
 
212
- let weight = exp(-max(max(fX, 0.0), features));
213
- let index = he(q);
212
+ let weight = exp(-max(max(fY, 0.0), features));
213
+ let index = hg(q);
214
214
  let c = color[index];
215
215
  let g = guide[index];
216
216
 
217
- let a = (textureLoad(albedoTex, q, 0).rgb - bs.rgb) / gV;
217
+ let a = (textureLoad(albedoTex, q, 0).rgb - bs.rgb) / gW;
218
218
  let value = premultiplied(c);
219
219
  sum += value * weight;
220
- mq += a * weight;
221
- gx += a * a * weight;
220
+ mp += a * weight;
221
+ gy += a * a * weight;
222
222
  hs += a * value.rgb * weight;
223
223
  ps += g.x * weight;
224
- gz += g.y * weight * weight;
225
- nV += weight;
224
+ gA += g.y * weight * weight;
225
+ nU += weight;
226
226
  kM = max(kM, weight);
227
227
  }
228
228
  }
@@ -232,20 +232,20 @@ kM = max(kM, weight);
232
232
  let center = color[index];
233
233
  let le = guide[index];
234
234
  let dG = max(kM, CENTER_WEIGHT_MIN);
235
- let totalWeight = nV + dG;
235
+ let totalWeight = nU + dG;
236
236
  let tM = premultiplied(center);
237
237
  let mean = (sum + tM * dG) / totalWeight;
238
238
 
239
239
 
240
- let ez = mq / totalWeight;
241
- let lB = max(gx / totalWeight - ez * ez, vec3f(0.0));
240
+ let ez = mp / totalWeight;
241
+ let lA = max(gy / totalWeight - ez * ez, vec3f(0.0));
242
242
  let qu = hs / totalWeight - ez * mean.rgb;
243
243
 
244
244
 
245
245
 
246
- let qE = bs.rgb + ez * gV;
247
- let oA = mean.rgb * gV / max(qE, vec3f(${JNLM_ALBEDO_FLOOR}));
248
- let vB = clamp(qu / (lB + vec3f(${JNLM_ALBEDO_REGULARIZATION})), vec3f(0.0), oA);
246
+ let qE = bs.rgb + ez * gW;
247
+ let oz = mean.rgb * gW / max(qE, vec3f(${JNLM_ALBEDO_FLOOR}));
248
+ let vB = clamp(qu / (lA + vec3f(${JNLM_ALBEDO_REGULARIZATION})), vec3f(0.0), oz);
249
249
  let rD = mean.rgb - vB * ez;
250
250
  let filtered = vec4f(max(rD, vec3f(0.0)), max(mean.a, 0.0));
251
251
  let rgb = select(
@@ -256,7 +256,7 @@ filtered.a > 1e-6
256
256
  outColor[index] = vec4f(rgb, filtered.a);
257
257
  outGuide[index] = vec2f(
258
258
  (ps + le.x * dG) / totalWeight,
259
- (gz + le.y * dG * dG)
259
+ (gA + le.y * dG * dG)
260
260
  / (totalWeight * totalWeight)
261
261
  );
262
262
  }
@@ -23,10 +23,10 @@ ${layers ? `@location(0) foreground: vec4f,
23
23
  @location(2) albedo: vec4f${catcher ? ',\n @location(3) shadowCatcher: vec4f' : ''}`}
24
24
  }
25
25
  @fragment
26
- fn main(@builtin(position) fG: vec4f) -> pd {
26
+ fn main(@builtin(position) fH: vec4f) -> pd {
27
27
  let size = vec2u(u32(outputWidth), u32(outputHeight));
28
- let fP = vec2u(u32(fG.x), size.y - u32(fG.y) - 1u);
29
- var idx = fP.x + fP.y * size.x;
28
+ let fQ = vec2u(u32(fH.x), size.y - u32(fH.y) - 1u);
29
+ var idx = fQ.x + fQ.y * size.x;
30
30
 
31
31
 
32
32
 
@@ -34,7 +34,7 @@ var idx = fP.x + fP.y * size.x;
34
34
  if (sampleCount < 1.0 && normalBuffer[idx].a <= 0.0) {
35
35
  let vc = max(sampleTileGrid, vec2u(1u));
36
36
  let uM = min(
37
- ((fP + vc / 2u) / vc) * vc,
37
+ ((fQ + vc / 2u) / vc) * vc,
38
38
  ((size - vec2u(1u)) / vc) * vc
39
39
  );
40
40
  let sH = uM.x + uM.y * size.x;
@@ -1916,6 +1916,9 @@ ${rayTraceCompute({
1916
1916
  // Retain the atlas uniform capacity when a stage/material is removed.
1917
1917
  // Shrinking the array would rebuild every pipeline again on the next edit.
1918
1918
  const NUM_MAPS = Math.max(Math.ceil(+materialData.defines.NUM_MAPS / 10) * 10, Number(this._constants.NUM_MAPS ?? 0), 20);
1919
+ // Either layer's map requires independent normals, including a smooth
1920
+ // coat over a mapped base. Unmapped materials retain the shared path.
1921
+ const USE_COAT_NORMAL_FRAMES = materials.some(m => m.coatWeight > 0 && (!!m.normalMap || !!m.coatNormalMap));
1919
1922
  Object.assign(this._constants, {
1920
1923
  // Match the atlas UV selector. Keep the second set when any texture
1921
1924
  // needs it; material/texture edits already rebuild these constants.
@@ -1924,9 +1927,8 @@ ${rayTraceCompute({
1924
1927
  // triangle UV differentials rather than this vertex tangent.
1925
1928
  USE_TANGENT: mergedGeometry.tangent != null &&
1926
1929
  materials.some(material => material.specularAnisotropy > 0 || material.coatAnisotropy > 0 || (!!material.coatNormalMap && material.coatWeight > 0)),
1927
- USE_COAT_NORMAL_FRAMES: materials.some(m => !!m.coatNormalMap && m.coatWeight > 0),
1928
- USE_ANISOTROPY_DIRECTIONS: materials.some(m => (!!m.coatNormalMap && m.coatWeight > 0) ||
1929
- m.specularAnisotropy > 0 && (m.specularAnisotropyModel === 'gltf' || !!m.anisotropyMap || m.specularAnisotropyRotation !== 0) ||
1930
+ USE_COAT_NORMAL_FRAMES,
1931
+ USE_ANISOTROPY_DIRECTIONS: USE_COAT_NORMAL_FRAMES || materials.some(m => m.specularAnisotropy > 0 && (m.specularAnisotropyModel === 'gltf' || !!m.anisotropyMap || m.specularAnisotropyRotation !== 0) ||
1930
1932
  m.coatAnisotropy > 0 && m.coatAnisotropyRotation !== 0),
1931
1933
  HAS_MEDIUM,
1932
1934
  HAS_SHADOW_CATCHER: this._hasShadowCatcher,