@meimu/mutracer 0.1.3 → 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 (125) hide show
  1. package/dist/assets/bvh.worker.js +1 -1
  2. package/dist/mutracer.umd.js +4155 -4052
  3. package/docs/api.md +13 -10
  4. package/docs/materials.md +17 -26
  5. package/docs/threejs.md +35 -9
  6. package/docs/viewer.md +36 -3
  7. package/lib/Engine.js +18 -1
  8. package/lib/Engine.js.map +1 -1
  9. package/lib/main.d.ts +1 -1
  10. package/lib/main.js.map +1 -1
  11. package/lib/oidn.d.ts +1 -1
  12. package/lib/oidnBundledWeights.cdn.d.ts +2 -2
  13. package/lib/renderer/MaterialTable.d.ts +1 -0
  14. package/lib/renderer/MaterialTable.js +10 -4
  15. package/lib/renderer/MaterialTable.js.map +1 -1
  16. package/lib/renderer/WGPUBloomPass.js +18 -18
  17. package/lib/renderer/WGPUFullQuadPass.js +6 -6
  18. package/lib/renderer/WGPUJNLMDenoiser.js +69 -69
  19. package/lib/renderer/WGPURayTraceOutput.js +14 -14
  20. package/lib/renderer/WGPURayTracePass.d.ts +2 -0
  21. package/lib/renderer/WGPURayTracePass.js +17 -9
  22. package/lib/renderer/WGPURayTracePass.js.map +1 -1
  23. package/lib/renderer/WGPURenderingPipeline.js +15 -15
  24. package/lib/renderer/WebGLJNLMDenoiser.js +69 -69
  25. package/lib/renderer/WebGLRayTraceOutputPass.js +4 -4
  26. package/lib/renderer/WebGLRayTracePass.d.ts +1 -0
  27. package/lib/renderer/WebGLRayTracePass.js +8 -1
  28. package/lib/renderer/WebGLRayTracePass.js.map +1 -1
  29. package/lib/renderer/envMapCreation.d.ts +5 -1
  30. package/lib/renderer/envMapCreation.js +42 -5
  31. package/lib/renderer/envMapCreation.js.map +1 -1
  32. package/lib/renderer/geometryUpdates.js +1 -1
  33. package/lib/renderer/geometryUpdates.js.map +1 -1
  34. package/lib/renderer/glsl/chunks/cameraRay.glsl.js +12 -12
  35. package/lib/renderer/glsl/chunks/envMap.glsl.js +66 -54
  36. package/lib/renderer/glsl/chunks/envMap.glsl.js.map +1 -1
  37. package/lib/renderer/glsl/chunks/intersect.glsl.js +323 -315
  38. package/lib/renderer/glsl/chunks/intersect.glsl.js.map +1 -1
  39. package/lib/renderer/glsl/chunks/materialBuffer.glsl.js +154 -121
  40. package/lib/renderer/glsl/chunks/materialBuffer.glsl.js.map +1 -1
  41. package/lib/renderer/glsl/chunks/openPbrBsdf.glsl.js +434 -434
  42. package/lib/renderer/glsl/chunks/openPbrBsdf.glsl.js.map +1 -1
  43. package/lib/renderer/glsl/chunks/rand.glsl.js +3 -3
  44. package/lib/renderer/glsl/chunks/random.glsl.js +3 -3
  45. package/lib/renderer/glsl/chunks/rayTraceCore.glsl.js +42 -41
  46. package/lib/renderer/glsl/chunks/rayTraceCore.glsl.js.map +1 -1
  47. package/lib/renderer/glsl/chunks/sampleLight.glsl.js +69 -69
  48. package/lib/renderer/glsl/chunks/sampleMaterial.glsl.js +84 -84
  49. package/lib/renderer/glsl/chunks/sampleMaterial.glsl.js.map +1 -1
  50. package/lib/renderer/glsl/chunks/sampleShadowCatcher.glsl.js +12 -12
  51. package/lib/renderer/glsl/chunks/sampling.glsl.js +75 -75
  52. package/lib/renderer/glsl/chunks/sobol.glsl.js +61 -61
  53. package/lib/renderer/glsl/chunks/textureLinear.glsl.js +5 -5
  54. package/lib/renderer/glsl/chunks/uvProjection.glsl.js +11 -11
  55. package/lib/renderer/glsl/depthQuery.frag.js +5 -5
  56. package/lib/renderer/glsl/depthQuery.frag.js.map +1 -1
  57. package/lib/renderer/glsl/normalizeRayTraceOutput.frag.js +8 -8
  58. package/lib/renderer/glsl/rayTrace.frag.js +133 -133
  59. package/lib/renderer/glsl/rayTrace.frag.js.map +1 -1
  60. package/lib/renderer/glsl/shadowCatcher.frag.js +9 -9
  61. package/lib/renderer/glsl/shadowCatcher.frag.js.map +1 -1
  62. package/lib/renderer/glsl/webglPipeline.frag.js +52 -52
  63. package/lib/renderer/lightsUtil.js +6 -4
  64. package/lib/renderer/lightsUtil.js.map +1 -1
  65. package/lib/renderer/materialChanges.js +1 -1
  66. package/lib/renderer/materialChanges.js.map +1 -1
  67. package/lib/renderer/materialComparison.generated.js +36 -18
  68. package/lib/renderer/materialComparison.generated.js.map +1 -1
  69. package/lib/renderer/prepareMeshBVH.js +2 -2
  70. package/lib/renderer/prepareMeshBVH.js.map +1 -1
  71. package/lib/renderer/sceneChangeDetector.js +4 -1
  72. package/lib/renderer/sceneChangeDetector.js.map +1 -1
  73. package/lib/renderer/sceneLightChanges.js +3 -2
  74. package/lib/renderer/sceneLightChanges.js.map +1 -1
  75. package/lib/renderer/scratchSampling.js +18 -18
  76. package/lib/renderer/wgsl/chunks/common.wgsl.js +4 -4
  77. package/lib/renderer/wgsl/chunks/envMap.wgsl.d.ts +1 -1
  78. package/lib/renderer/wgsl/chunks/envMap.wgsl.js +39 -31
  79. package/lib/renderer/wgsl/chunks/envMap.wgsl.js.map +1 -1
  80. package/lib/renderer/wgsl/chunks/intersect.wgsl.js +476 -470
  81. package/lib/renderer/wgsl/chunks/intersect.wgsl.js.map +1 -1
  82. package/lib/renderer/wgsl/chunks/intersectionAlpha.wgsl.js +8 -8
  83. package/lib/renderer/wgsl/chunks/materialBuffer.wgsl.js +95 -60
  84. package/lib/renderer/wgsl/chunks/materialBuffer.wgsl.js.map +1 -1
  85. package/lib/renderer/wgsl/chunks/openPbrBsdf.wgsl.js +469 -469
  86. package/lib/renderer/wgsl/chunks/pdm_displacement.wgsl.d.ts +1 -1
  87. package/lib/renderer/wgsl/chunks/pdm_displacement.wgsl.js +117 -120
  88. package/lib/renderer/wgsl/chunks/pdm_displacement.wgsl.js.map +1 -1
  89. package/lib/renderer/wgsl/chunks/rand.wgsl.js +3 -3
  90. package/lib/renderer/wgsl/chunks/rayTraceCore.wgsl.js +25 -22
  91. package/lib/renderer/wgsl/chunks/rayTraceCore.wgsl.js.map +1 -1
  92. package/lib/renderer/wgsl/chunks/sampleLight.wgsl.js +127 -127
  93. package/lib/renderer/wgsl/chunks/sampleMaterial.wgsl.js +136 -136
  94. package/lib/renderer/wgsl/chunks/sampleShadowCatcher.wgsl.js +6 -6
  95. package/lib/renderer/wgsl/chunks/sampling.wgsl.js +73 -73
  96. package/lib/renderer/wgsl/chunks/sobol.wgsl.js +62 -62
  97. package/lib/renderer/wgsl/chunks/uvProjection.wgsl.js +11 -11
  98. package/lib/renderer/wgsl/compose.frag.wgsl.js +105 -105
  99. package/lib/renderer/wgsl/customDenoise.wgsl.js +10 -10
  100. package/lib/renderer/wgsl/depthQuery.wgsl.d.ts +1 -1
  101. package/lib/renderer/wgsl/depthQuery.wgsl.js +5 -5
  102. package/lib/renderer/wgsl/depthQuery.wgsl.js.map +1 -1
  103. package/lib/renderer/wgsl/offlineAdaptive.compute.js +28 -28
  104. package/lib/renderer/wgsl/rayTrace.compute.js +251 -248
  105. package/lib/renderer/wgsl/rayTrace.compute.js.map +1 -1
  106. package/lib/scene/EnvironmentLight.d.ts +9 -3
  107. package/lib/scene/EnvironmentLight.js +9 -2
  108. package/lib/scene/EnvironmentLight.js.map +1 -1
  109. package/lib/scene/OpenPBRMaterial.d.ts +1 -1
  110. package/lib/scene/OpenPBRMaterial.js +1 -1
  111. package/lib/scene/OpenPBRMaterial.js.map +1 -1
  112. package/lib/scene/Scene.d.ts +4 -0
  113. package/lib/scene/Scene.js.map +1 -1
  114. package/lib/scene/SpotLight.d.ts +2 -2
  115. package/lib/scene/SpotLight.js +1 -1
  116. package/lib/scene/SpotLight.js.map +1 -1
  117. package/lib/scene/Texture.d.ts +17 -6
  118. package/lib/scene/Texture.js +13 -4
  119. package/lib/scene/Texture.js.map +1 -1
  120. package/lib/util/fromTHREE.js +151 -29
  121. package/lib/util/fromTHREE.js.map +1 -1
  122. package/lib/version.d.ts +1 -1
  123. package/lib/version.js +1 -1
  124. package/lib/version.js.map +1 -1
  125. package/package.json +1 -1
@@ -26,11 +26,11 @@ function convertTexture(threeTexture, textureStore) {
26
26
  return;
27
27
  const state = [threeTexture.version, threeTexture.image, threeTexture.flipY,
28
28
  threeTexture.repeat.x, threeTexture.repeat.y, threeTexture.offset.x,
29
- threeTexture.offset.y, threeTexture.channel];
29
+ threeTexture.offset.y, threeTexture.channel, threeTexture.wrapS, threeTexture.wrapT, threeTexture.colorSpace];
30
30
  const cached = textureStore.get(threeTexture);
31
31
  if (cached && snapshotsEqual(cached.state, state))
32
32
  return cached.texture;
33
- const image = threeTexture.image;
33
+ const image = convertTextureImage(threeTexture);
34
34
  const texture = cached?.texture ?? new Texture(image);
35
35
  texture.image = image;
36
36
  texture.source = threeTexture;
@@ -38,6 +38,13 @@ function convertTexture(threeTexture, textureStore) {
38
38
  texture.uvScale = [threeTexture.repeat.x, threeTexture.repeat.y];
39
39
  texture.uvOffset = [threeTexture.offset.x, threeTexture.offset.y];
40
40
  texture.uvChannel = threeTexture.channel === 1 ? 1 : 0;
41
+ const wrap = (value) => value === 1001 ? 'clamp-to-edge'
42
+ : value === 1002 ? 'mirrored-repeat' : 'repeat';
43
+ texture.wrapS = wrap(threeTexture.wrapS);
44
+ texture.wrapT = wrap(threeTexture.wrapT);
45
+ // Three.js NoColorSpace also reads color channels without a transfer function.
46
+ texture.colorSpace = threeTexture.colorSpace === 'srgb' ? 'srgb'
47
+ : threeTexture.colorSpace === 'srgb-linear' || threeTexture.colorSpace === '' ? 'linear' : undefined;
41
48
  if (cached)
42
49
  texture.markUpdated();
43
50
  else
@@ -57,6 +64,49 @@ function halfFloatToFloat(value) {
57
64
  }
58
65
  return sign * Math.pow(2, exponent - 15) * (1 + fraction / 0x400);
59
66
  }
67
+ /** Decode Three.js storage into the RGBA byte/float images used by MuTracer. */
68
+ function convertTextureImage(texture) {
69
+ const image = texture.image;
70
+ if (!image?.data)
71
+ return image;
72
+ // Three.js enums are kept numeric to retain the optional, type-only peer import.
73
+ const channels = texture.format === 1028 ? 1 : texture.format === 1030 ? 2
74
+ : texture.format === 1022 ? 3 : texture.format === 1023 ? 4 : 0;
75
+ if (!channels)
76
+ throw new Error(`Unsupported Three.js data texture format: ${texture.format}`);
77
+ const data = image.data;
78
+ if (data.length !== image.width * image.height * channels) {
79
+ throw new Error('Three.js data texture length does not match its format and dimensions.');
80
+ }
81
+ const half = texture.type === 1016;
82
+ if (channels === 4 && !half && (data instanceof Float32Array || data instanceof Uint8Array ||
83
+ data instanceof Uint8ClampedArray || data instanceof Int32Array))
84
+ return image;
85
+ const bytes = data instanceof Uint8Array || data instanceof Uint8ClampedArray;
86
+ const result = bytes ? new Uint8Array(image.width * image.height * 4)
87
+ : new Float32Array(image.width * image.height * 4);
88
+ const read = (index) => {
89
+ const value = data[index];
90
+ if (half)
91
+ return halfFloatToFloat(value);
92
+ if (data instanceof Uint16Array)
93
+ return value / 65535;
94
+ if (data instanceof Uint32Array)
95
+ return value / 4294967295;
96
+ if (data instanceof Int8Array)
97
+ return Math.max(-1, value / 127);
98
+ if (data instanceof Int16Array)
99
+ return Math.max(-1, value / 32767);
100
+ return value;
101
+ };
102
+ for (let i = 0; i < image.width * image.height; i++) {
103
+ result[i * 4] = read(i * channels);
104
+ result[i * 4 + 1] = channels > 1 ? read(i * channels + 1) : 0;
105
+ result[i * 4 + 2] = channels > 2 ? read(i * channels + 2) : 0;
106
+ result[i * 4 + 3] = channels === 4 ? read(i * channels + 3) : bytes ? 255 : 1;
107
+ }
108
+ return { width: image.width, height: image.height, data: result };
109
+ }
60
110
  function convertDataTexture(threeTexture, textureStore) {
61
111
  if (!threeTexture?.image?.data || !threeTexture.image.width || !threeTexture.image.height)
62
112
  return;
@@ -64,14 +114,25 @@ function convertDataTexture(threeTexture, textureStore) {
64
114
  const texture = convertTexture(threeTexture, textureStore);
65
115
  if (previous?.state === textureStore.get(threeTexture)?.state)
66
116
  return texture;
67
- const source = threeTexture.image.data;
68
- const data = source instanceof Uint16Array
69
- ? Float32Array.from(source, halfFloatToFloat)
70
- : source instanceof Float32Array || source instanceof Uint8Array || source instanceof Int32Array
71
- ? source : Float32Array.from(source);
72
- texture.image = { data, width: threeTexture.image.width, height: threeTexture.image.height };
117
+ const data = texture.image.data;
118
+ const { width, height } = threeTexture.image;
119
+ // Three.js equirect maps put the sky at v = 1, so with flipY = false the
120
+ // first row is the nadir. MuTracer environments start at the zenith and
121
+ // ignore flipY, so reorder the rows here instead.
122
+ texture.image = { data: threeTexture.flipY ? data : flipRows(data, width, height), width, height };
123
+ texture.flipY = false;
73
124
  return texture;
74
125
  }
126
+ function flipRows(data, width, height) {
127
+ if (height <= 1)
128
+ return data;
129
+ const rowLength = data.length / height;
130
+ const flipped = new data.constructor(data.length);
131
+ for (let y = 0; y < height; y++) {
132
+ flipped.set(data.subarray((height - 1 - y) * rowLength, (height - y) * rowLength), y * rowLength);
133
+ }
134
+ return flipped;
135
+ }
75
136
  function convertMaterial(threeMaterial, textureCache) {
76
137
  const material = new OpenPBRMaterial();
77
138
  if (threeMaterial.userData.mutracerShadowCatcher === true) {
@@ -178,8 +239,12 @@ function convertMaterial(threeMaterial, textureCache) {
178
239
  material.transmissionWeight = physicalMaterial.transmission;
179
240
  material.transmissionMap = convertTexture(physicalMaterial.transmissionMap, textureCache);
180
241
  material.specularIor = physicalMaterial.ior;
181
- material.specularWeight = physicalMaterial.specularIntensity;
182
- if (physicalMaterial.specularColor) {
242
+ // glTF specular (KHR_materials_specular) only scales the dielectric
243
+ // lobe, while OpenPBR applies it to metals too. A fully metallic
244
+ // surface has no dielectric part, so keep the OpenPBR defaults there.
245
+ const fullyMetallic = physicalMaterial.metalness === 1 && !physicalMaterial.metalnessMap;
246
+ material.specularWeight = fullyMetallic ? 1 : physicalMaterial.specularIntensity;
247
+ if (physicalMaterial.specularColor && !fullyMetallic) {
183
248
  material.specularColor =
184
249
  physicalMaterial.specularColor.toArray();
185
250
  }
@@ -189,8 +254,10 @@ function convertMaterial(threeMaterial, textureCache) {
189
254
  material.coatNormalScale = physicalMaterial.clearcoatNormalScale.toArray();
190
255
  material.fuzzColorMap = convertTexture(physicalMaterial.sheenColorMap, textureCache);
191
256
  material.fuzzRoughnessMap = convertTexture(physicalMaterial.sheenRoughnessMap, textureCache);
192
- material.specularWeightMap = convertTexture(physicalMaterial.specularIntensityMap, textureCache);
193
- material.specularColorMap = convertTexture(physicalMaterial.specularColorMap, textureCache);
257
+ if (!fullyMetallic) {
258
+ material.specularWeightMap = convertTexture(physicalMaterial.specularIntensityMap, textureCache);
259
+ material.specularColorMap = convertTexture(physicalMaterial.specularColorMap, textureCache);
260
+ }
194
261
  material.coatWeight = physicalMaterial.clearcoat;
195
262
  material.coatRoughness = physicalMaterial.clearcoatRoughness;
196
263
  // MeshPhysicalMaterial's clearcoat BRDF uses the fixed dielectric F0
@@ -360,6 +427,12 @@ function convertMaterial(threeMaterial, textureCache) {
360
427
  !!standardMaterial.alphaMap
361
428
  ? 'blend'
362
429
  : 'opaque';
430
+ if (standardMaterial.side === 1) {
431
+ // BackSide normals are reversed in converted geometry. Keep the source
432
+ // displacement direction and biased base surface unchanged.
433
+ material.displacementScale = -material.displacementScale;
434
+ material.displacementBias = -material.displacementBias;
435
+ }
363
436
  return material;
364
437
  }
365
438
  function copyFloatAttribute(attribute, itemSize = attribute.itemSize) {
@@ -397,12 +470,13 @@ function copyFloatAttribute(attribute, itemSize = attribute.itemSize) {
397
470
  }
398
471
  return result;
399
472
  }
400
- function convertGeometry(threeGeometry, drawRange) {
473
+ function convertGeometry(threeGeometry, drawRange, vertexColors, backSide) {
401
474
  const positionAttrb = threeGeometry.getAttribute('position');
402
475
  const normalAttrb = threeGeometry.getAttribute('normal');
403
476
  const tangentAttrb = threeGeometry.getAttribute('tangent');
404
477
  const uvAttrb = threeGeometry.getAttribute('uv');
405
478
  const uv1Attrb = threeGeometry.getAttribute('uv1');
479
+ const colorAttrb = vertexColors && threeGeometry.getAttribute('color');
406
480
  const indexAttrb = threeGeometry.getIndex();
407
481
  const sourceCount = indexAttrb ? indexAttrb.array.length : positionAttrb.count;
408
482
  // Match Array.slice range handling without expanding the complete index stream.
@@ -427,6 +501,13 @@ function convertGeometry(threeGeometry, drawRange) {
427
501
  for (let index = 0; index < indexCount; index++)
428
502
  indices[index] = start + index;
429
503
  }
504
+ if (backSide) {
505
+ for (let i = 0; i < indices.length; i += 3) {
506
+ const tmp = indices[i + 1];
507
+ indices[i + 1] = indices[i + 2];
508
+ indices[i + 2] = tmp;
509
+ }
510
+ }
430
511
  const geometry = new Geometry({
431
512
  position: new Attribute(copyFloatAttribute(positionAttrb), positionAttrb.itemSize),
432
513
  normal: normalAttrb &&
@@ -435,21 +516,40 @@ function convertGeometry(threeGeometry, drawRange) {
435
516
  uv: uvAttrb && new Attribute(copyFloatAttribute(uvAttrb), uvAttrb.itemSize),
436
517
  uv1: uv1Attrb &&
437
518
  new Attribute(copyFloatAttribute(uv1Attrb), uv1Attrb.itemSize),
519
+ // MuTracer stores linear RGB only; omit Three.js's optional vertex alpha.
520
+ color: colorAttrb ? new Attribute(copyFloatAttribute(colorAttrb, 3), 3) : undefined,
438
521
  indices: new Attribute(indices, 1)
439
522
  });
440
- if (!geometry.normal) {
441
- generateVertexNormals(geometry);
523
+ // Generated normals already follow the reversed indices; authored normals do not.
524
+ if (backSide) {
525
+ if (normalAttrb)
526
+ negateNormals(geometry);
527
+ reverseTangentHandedness(geometry);
442
528
  }
443
529
  updateBoundingBox(geometry);
444
530
  return geometry;
445
531
  }
532
+ function negateNormals(geometry) {
533
+ const normals = geometry.normal.array;
534
+ for (let i = 0; i < normals.length; i++)
535
+ normals[i] = -normals[i];
536
+ }
537
+ function reverseTangentHandedness(geometry) {
538
+ // Three.js FLIP_SIDED reverses N but preserves T and B. Reversing W
539
+ // preserves that bitangent when the renderer reconstructs cross(N,T)*W.
540
+ const tangent = geometry.tangent;
541
+ if (tangent?.itemSize === 4)
542
+ for (let i = 3; i < tangent.array.length; i += 4)
543
+ tangent.array[i] *= -1;
544
+ }
446
545
  function createConversionStore() {
447
- return { updateId: 0, textures: new WeakMap(), materials: new WeakMap(), geometries: new WeakMap(),
546
+ return { updateId: 0, textures: new WeakMap(), equirectTextures: new WeakMap(),
547
+ materials: new WeakMap(), geometries: new WeakMap(),
448
548
  materialHooks: new WeakMap(), lightHooks: new WeakMap(),
449
549
  poseChanged: false, meshes: new WeakMap(), lights: new WeakMap(), deformedGeometries: new WeakMap(), deformations: new WeakMap(),
450
550
  instanceMeshes: new WeakMap(), projectionVariants: new Map(), warned: new WeakSet() };
451
551
  }
452
- const geometryAttributeNames = ['position', 'normal', 'tangent', 'uv', 'uv1', 'index'];
552
+ const geometryAttributeNames = ['position', 'normal', 'tangent', 'uv', 'uv1', 'color', 'index'];
453
553
  function geometryState(geometry, deformation) {
454
554
  const state = [];
455
555
  for (let i = 0; i < geometryAttributeNames.length; i++) {
@@ -703,30 +803,48 @@ function convertScene(scene, options, store) {
703
803
  ? group.count
704
804
  : (child.geometry.getIndex()?.count ??
705
805
  child.geometry.getAttribute('position').count);
706
- const geometryKey = `${group.start}:${count}`;
806
+ const sourceMaterial = threeMaterials[group.materialIndex ?? 0];
807
+ // Vertex colors and BackSide orientation are baked into geometry, so
808
+ // groups with different switches cannot share the same cached variant.
809
+ const vertexColors = sourceMaterial.vertexColors === true;
810
+ const backSide = sourceMaterial.side === 1;
811
+ const geometryKey = `${group.start}:${count}:${vertexColors}:${backSide}`;
707
812
  let geometry = geometryEntry.groups.get(geometryKey);
708
813
  const newGeometry = !geometry;
709
814
  if (!geometry) {
710
- const converted = convertGeometry(child.geometry, { start: group.start, count });
815
+ const converted = convertGeometry(child.geometry, { start: group.start, count }, vertexColors, backSide);
711
816
  if (!converted)
712
817
  return;
713
818
  geometry = converted;
714
819
  geometryEntry.groups.set(geometryKey, geometry);
715
820
  }
716
- if (deformation && (deformed || newGeometry)) {
821
+ // A retained color/side variant may predate the current pose.
822
+ if (deformation && (deformed || newGeometry || store.meshes.get(child)?.get(groupIndex)?.geometry !== geometry)) {
717
823
  geometry.dynamic = true;
718
824
  geometry.position.array.set(deformation.position);
719
- if (deformation.normal)
825
+ if (deformation.normal) {
720
826
  geometry.normal.array.set(deformation.normal);
721
- if (deformation.tangent && geometry.tangent)
827
+ if (backSide)
828
+ negateNormals(geometry);
829
+ }
830
+ else {
831
+ // Retain resident attribute storage so a pose change can refit its BVH.
832
+ const normal = geometry.normal;
833
+ generateVertexNormals(geometry);
834
+ normal.array.set(geometry.normal.array);
835
+ geometry.normal = normal;
836
+ }
837
+ if (deformation.tangent && geometry.tangent) {
722
838
  geometry.tangent.array.set(deformation.tangent);
839
+ if (backSide)
840
+ reverseTangentHandedness(geometry);
841
+ }
723
842
  if (!child.geometry.getAttribute('tangent'))
724
843
  updateDynamicTangents(geometry);
725
844
  updateBoundingBox(geometry);
726
845
  if (!newGeometry)
727
846
  markDeformedGeometry(geometry);
728
847
  }
729
- const sourceMaterial = threeMaterials[group.materialIndex ?? 0];
730
848
  let material = resolveMaterial(sourceMaterial);
731
849
  const baseMaterial = material;
732
850
  const autoUvProjection = !geometry.uv && baseMaterial.uvProjection === 'none' &&
@@ -890,14 +1008,16 @@ function convertScene(scene, options, store) {
890
1008
  }
891
1009
  });
892
1010
  const threeEnvironment = scene.environment;
893
- const environmentMap = convertDataTexture(threeEnvironment, textureCache);
1011
+ const environmentMap = convertDataTexture(threeEnvironment, store.equirectTextures);
894
1012
  const environmentLight = threeEnvironment && environmentMap
895
- ? retainLight(store, threeEnvironment, new EnvironmentLight({ map: environmentMap }))
1013
+ ? retainLight(store, threeEnvironment, new EnvironmentLight({ map: environmentMap, intensity: scene.environmentIntensity }))
896
1014
  : undefined;
897
- if (environmentLight)
1015
+ if (environmentLight) {
1016
+ environmentLight.transform = scene.matrix.clone().makeRotationFromEuler(scene.environmentRotation).elements.slice();
898
1017
  lights.push(environmentLight);
1018
+ }
899
1019
  const grounded = scene.userData.mutracerGroundedSkybox;
900
- const groundedTexture = grounded && convertDataTexture(grounded.texture, textureCache);
1020
+ const groundedTexture = grounded && convertDataTexture(grounded.texture, store.equirectTextures);
901
1021
  const hideEnvironmentBackground = scene.userData.mutracerHiddenBackground === true;
902
1022
  let background = hideEnvironmentBackground
903
1023
  ? [0, 0, 0, 0]
@@ -908,14 +1028,16 @@ function convertScene(scene, options, store) {
908
1028
  height: grounded.height,
909
1029
  radius: grounded.radius,
910
1030
  groundHeight: grounded.groundHeight,
911
- center: grounded.center
1031
+ center: grounded.center,
1032
+ rotation: grounded.rotation,
1033
+ intensity: grounded.intensity
912
1034
  }
913
1035
  : scene.background
914
1036
  ? scene.background.isColor
915
1037
  ? [...scene.background.toArray(), 1]
916
1038
  : scene.background === scene.environment && environmentLight
917
1039
  ? environmentLight.map
918
- : convertDataTexture(scene.background, textureCache)
1040
+ : convertDataTexture(scene.background, store.equirectTextures)
919
1041
  : undefined;
920
1042
  const oldBackground = store.background;
921
1043
  if (Array.isArray(background) && Array.isArray(oldBackground) && snapshotsEqual(background, oldBackground)) {