rayzee 8.4.1 → 8.6.0

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 (65) hide show
  1. package/README.md +61 -9
  2. package/dist/{DLSSNeural-BNSJ8V3z.js → DLSSNeural-BhBCBgKf.js} +5 -5
  3. package/dist/{DLSSSuperRes-BoEW89Cl.js → DLSSSuperRes-BCA7F_k5.js} +5 -5
  4. package/dist/ToneMapGPU-BzcLheoR.js +533 -0
  5. package/dist/rayzee.es.js +27278 -9133
  6. package/dist/rayzee.umd.js +255 -187
  7. package/package.json +1 -1
  8. package/src/AssetConfig.js +15 -2
  9. package/src/Color/BakedViews.js +134 -0
  10. package/src/Color/BuiltinViews.js +261 -0
  11. package/src/Color/ColorManagement.js +1130 -0
  12. package/src/Color/ColorSpaces.js +432 -0
  13. package/src/Color/Displays.js +19 -0
  14. package/src/Color/InputColorSpaces.js +243 -0
  15. package/src/Color/LutBake.js +371 -0
  16. package/src/Color/OcioRuntime.js +338 -0
  17. package/src/Color/OcioViews.js +419 -0
  18. package/src/Color/ViewTransforms.js +323 -0
  19. package/src/Color/WorkingMatrix.js +86 -0
  20. package/src/EngineDefaults.js +84 -4
  21. package/src/EngineIssues.js +3 -1
  22. package/src/Passes/AIUpscaler.js +16 -7
  23. package/src/PathTracerApp.js +318 -35
  24. package/src/Processor/AssetLoader.js +47 -55
  25. package/src/Processor/CameraOptimizer.js +22 -6
  26. package/src/Processor/EmissiveTriangleBuilder.js +34 -2
  27. package/src/Processor/GeometryExtractor.js +151 -138
  28. package/src/Processor/LightSerializer.js +17 -20
  29. package/src/Processor/MaterialPacking.js +168 -0
  30. package/src/Processor/PackedRayBuffer.js +6 -2
  31. package/src/Processor/ProceduralSky.js +4 -0
  32. package/src/Processor/SceneProcessor.js +25 -1
  33. package/src/Processor/SimpleSky.js +8 -0
  34. package/src/Processor/TextureCreator.js +137 -91
  35. package/src/Processor/TextureReadback.js +92 -0
  36. package/src/Processor/ToneMapCPU.js +80 -165
  37. package/src/Processor/ToneMapGPU.js +122 -151
  38. package/src/RenderSettings.js +10 -1
  39. package/src/Stages/Compositor.js +11 -2
  40. package/src/Stages/PathTracer.js +21 -4
  41. package/src/Stages/PathTracerStage.js +11 -18
  42. package/src/TSL/Common.js +15 -0
  43. package/src/TSL/Debugger.js +2 -3
  44. package/src/TSL/EmissiveSampling.js +6 -8
  45. package/src/TSL/ExtendKernel.js +9 -0
  46. package/src/TSL/Fresnel.js +23 -1
  47. package/src/TSL/HitFacet.js +102 -0
  48. package/src/TSL/LightsDirect.js +12 -33
  49. package/src/TSL/LightsSampling.js +15 -7
  50. package/src/TSL/MaterialEvaluation.js +26 -19
  51. package/src/TSL/MaterialProperties.js +74 -37
  52. package/src/TSL/MaterialTransmission.js +2 -3
  53. package/src/TSL/ShadeKernel.js +34 -21
  54. package/src/TSL/ShadowTerminator.js +125 -0
  55. package/src/TSL/Struct.js +9 -0
  56. package/src/TSL/Subsurface.js +2 -3
  57. package/src/index.js +29 -2
  58. package/src/managers/DenoisingManager.js +3 -7
  59. package/src/managers/EnvironmentManager.js +20 -0
  60. package/src/managers/LightManager.js +17 -15
  61. package/src/managers/MaterialDataManager.js +118 -208
  62. package/src/managers/UniformManager.js +1 -0
  63. package/src/managers/VideoRenderManager.js +9 -9
  64. package/dist/ToneMapGPU-DzqSyzTx.js +0 -341
  65. package/src/LightUnits.js +0 -122
@@ -19,12 +19,11 @@ export class CameraOptimizer {
19
19
  this.interactionMode = false;
20
20
  this.interactionTimeout = null;
21
21
  this.originalValues = {};
22
+ this.appliedValues = {};
22
23
  this.wasAccumulationEnabled = true; // Track original accumulation state
23
24
 
24
25
  // Enhanced interaction mode settings for reduced quality during interaction
25
26
  this.interactionQualitySettings = {
26
- maxBounceCount: 1,
27
- // pixelRatio: 0.25,
28
27
  enableAccumulation: false,
29
28
  ...settings.qualitySettings
30
29
  };
@@ -54,6 +53,7 @@ export class CameraOptimizer {
54
53
  // Enter interaction mode and save original values
55
54
  this.interactionMode = true;
56
55
  this.originalValues = {}; // Reset stored values
56
+ this.appliedValues = {};
57
57
 
58
58
  // Store original accumulation state before any changes
59
59
  if ( this.material.uniforms.enableAccumulation ) {
@@ -80,7 +80,8 @@ export class CameraOptimizer {
80
80
 
81
81
  // Handle material uniforms
82
82
  this.originalValues[ key ] = this.material.uniforms[ key ].value;
83
- this.material.uniforms[ key ].value = this.interactionQualitySettings[ key ];
83
+ this.material.uniforms[ key ].value = this._resolveQualityValue( key, this.interactionQualitySettings[ key ] );
84
+ this.appliedValues[ key ] = this.material.uniforms[ key ].value;
84
85
 
85
86
  }
86
87
 
@@ -136,8 +137,13 @@ export class CameraOptimizer {
136
137
 
137
138
  } else if ( this.material.uniforms[ key ] ) {
138
139
 
139
- // Restore material uniforms
140
- this.material.uniforms[ key ].value = this.originalValues[ key ];
140
+ // A value written while we held it (a settings change) is newer than the one we saved.
141
+ const applied = this.appliedValues[ key ];
142
+ if ( applied === undefined || this.material.uniforms[ key ].value === applied ) {
143
+
144
+ this.material.uniforms[ key ].value = this.originalValues[ key ];
145
+
146
+ }
141
147
 
142
148
  }
143
149
 
@@ -145,6 +151,7 @@ export class CameraOptimizer {
145
151
 
146
152
  this.interactionMode = false;
147
153
  this.originalValues = {}; // Clear stored values
154
+ this.appliedValues = {};
148
155
 
149
156
  // Call exit callback first (this may trigger a reset)
150
157
  if ( this.onExitCallback ) {
@@ -213,7 +220,8 @@ export class CameraOptimizer {
213
220
 
214
221
  } else if ( this.material.uniforms[ key ] ) {
215
222
 
216
- this.material.uniforms[ key ].value = settings[ key ];
223
+ this.material.uniforms[ key ].value = this._resolveQualityValue( key, settings[ key ] );
224
+ this.appliedValues[ key ] = this.material.uniforms[ key ].value;
217
225
 
218
226
  }
219
227
 
@@ -223,6 +231,14 @@ export class CameraOptimizer {
223
231
 
224
232
  }
225
233
 
234
+ // A function derives the interaction value from the one it replaces.
235
+ _resolveQualityValue( key, target ) {
236
+
237
+ if ( typeof target !== 'function' ) return target;
238
+ return target( this.originalValues[ key ] ?? this.material.uniforms[ key ].value );
239
+
240
+ }
241
+
226
242
  /**
227
243
  * Set the interaction delay
228
244
  * @param {number} delay - Delay in milliseconds
@@ -10,6 +10,28 @@ import { DataTexture, RGBAFormat, FloatType, NearestFilter } from 'three';
10
10
  import { TRIANGLE_DATA_LAYOUT, TRI_MATERIAL_MASK } from '../EngineDefaults.js';
11
11
  import { LightBVHBuilder } from './LightBVHBuilder.js';
12
12
  import { createLogger, fmt } from '../utils/Logger.js';
13
+ import { convertLinearTriple, getWorkingMatrix } from '../Color/WorkingMatrix.js';
14
+
15
+ const NO_EMISSION = Object.freeze( { r: 0, g: 0, b: 0 } );
16
+
17
+ /**
18
+ * A material's emission in the working space.
19
+ *
20
+ * The emitter seen directly reads the material buffer, which is already converted, and so does the
21
+ * shader's pick probability. This buffer is the other copy of the same colour — the one next-event
22
+ * estimation lights the scene with — so it has to move with them, or a working space adopted from a
23
+ * config lights everything in the wrong primaries and weights the pick by a colour nothing renders.
24
+ */
25
+ function workingEmissive( emissive ) {
26
+
27
+ if ( ! emissive ) return NO_EMISSION;
28
+ if ( ! getWorkingMatrix() ) return emissive;
29
+
30
+ const rgb = [ emissive.r, emissive.g, emissive.b ];
31
+ convertLinearTriple( rgb );
32
+ return { r: rgb[ 0 ], g: rgb[ 1 ], b: rgb[ 2 ] };
33
+
34
+ }
13
35
 
14
36
  const log = createLogger( 'emissive' );
15
37
 
@@ -62,6 +84,9 @@ export class EmissiveTriangleBuilder {
62
84
  const flatU = chunked ? null : triangleData;
63
85
  const flatF = chunked ? null : new Float32Array( triangleData.buffer, triangleData.byteOffset, triangleData.length );
64
86
 
87
+ // Once per material, not once per triangle: this loop runs over every triangle in the scene.
88
+ const emissiveOf = new Map();
89
+
65
90
  for ( let i = 0; i < triangleCount; i ++ ) {
66
91
 
67
92
  const triU = chunked ? chunked.chunkFor( i ) : flatU;
@@ -75,7 +100,14 @@ export class EmissiveTriangleBuilder {
75
100
  if ( ! material ) continue;
76
101
 
77
102
  // Check if emissive
78
- const emissive = material.emissive || { r: 0, g: 0, b: 0 };
103
+ let emissive = emissiveOf.get( material );
104
+ if ( emissive === undefined ) {
105
+
106
+ emissive = workingEmissive( material.emissive );
107
+ emissiveOf.set( material, emissive );
108
+
109
+ }
110
+
79
111
  const emissiveIntensity = material.emissiveIntensity || 0;
80
112
 
81
113
  const isEmissive = emissiveIntensity > 0 && (
@@ -461,7 +493,7 @@ export class EmissiveTriangleBuilder {
461
493
  */
462
494
  updateMaterialEmissive( materialIndex, material, triangleData, materials, triangleCount ) {
463
495
 
464
- const emissive = material.emissive || { r: 0, g: 0, b: 0 };
496
+ const emissive = workingEmissive( material.emissive );
465
497
  const emissiveIntensity = material.emissiveIntensity || 0;
466
498
  const isNowEmissive = emissiveIntensity > 0 && ( emissive.r > 0 || emissive.g > 0 || emissive.b > 0 );
467
499
 
@@ -1,6 +1,6 @@
1
1
  import { BufferAttribute, Vector3, Vector2, Color, Matrix3, Matrix4, FrontSide, BackSide, DoubleSide, RGBAFormat } from "three";
2
2
  import {
3
- TEXTURE_CONSTANTS, TRIANGLE_DATA_LAYOUT, packNormalOct, packTriangleFlags
3
+ TEXTURE_CONSTANTS, TRIANGLE_DATA_LAYOUT, MATERIAL_DEFAULTS, packNormalOct, packTriangleFlags
4
4
  } from '../EngineDefaults.js';
5
5
  import { ISSUE_CODES } from '../EngineIssues.js';
6
6
  import { ChunkedRecords, SHARED_MEMORY_AVAILABLE } from './ChunkedRecords.js';
@@ -10,6 +10,20 @@ const log = createLogger( 'geometry' );
10
10
 
11
11
  const MAX_TEXTURES_LIMIT = TEXTURE_CONSTANTS.MAX_TEXTURES_LIMIT;
12
12
 
13
+ const COLOR_DEFAULTS = new Set( [ 'color', 'emissive', 'attenuationColor', 'sheenColor', 'specularColor', 'subsurfaceColor' ] );
14
+
15
+ /**
16
+ * Where a packed material value came from: the three.js material carried it, a legacy-type
17
+ * conversion (Basic/Lambert/Phong/Toon) produced it, the engine filled it from
18
+ * MATERIAL_DEFAULTS, or a host set it at runtime through setMaterialProperty.
19
+ */
20
+ export const MATERIAL_VALUE_SOURCE = Object.freeze( {
21
+ MATERIAL: 'material',
22
+ MAPPED: 'mapped',
23
+ DEFAULT: 'default',
24
+ HOST: 'host',
25
+ } );
26
+
13
27
  const IDENTITY_ELEMENTS = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ];
14
28
 
15
29
  function isIdentityElements( e ) {
@@ -19,6 +33,36 @@ function isIdentityElements( e ) {
19
33
 
20
34
  }
21
35
 
36
+ /**
37
+ * The texture that feeds each GPU map slot. Re-packing texture indices without this loses the
38
+ * unlit routing (a MeshBasicMaterial's map is also its emission map).
39
+ * @param {import('three').Material} material
40
+ * @returns {Object<string, import('three').Texture|undefined>}
41
+ */
42
+ export function resolveMaterialTextures( material ) {
43
+
44
+ return {
45
+ map: material.map,
46
+ normalMap: material.normalMap,
47
+ bumpMap: material.bumpMap,
48
+ roughnessMap: material.roughnessMap,
49
+ metalnessMap: material.metalnessMap,
50
+ emissiveMap: material.emissiveMap ?? ( material.isMeshBasicMaterial ? material.map : undefined ),
51
+ displacementMap: material.displacementMap,
52
+ anisotropyMap: material.anisotropyMap,
53
+ clearcoatMap: material.clearcoatMap,
54
+ clearcoatRoughnessMap: material.clearcoatRoughnessMap,
55
+ transmissionMap: material.transmissionMap,
56
+ sheenColorMap: material.sheenColorMap,
57
+ sheenRoughnessMap: material.sheenRoughnessMap,
58
+ specularIntensityMap: material.specularIntensityMap,
59
+ specularColorMap: material.specularColorMap,
60
+ iridescenceMap: material.iridescenceMap,
61
+ iridescenceThicknessMap: material.iridescenceThicknessMap,
62
+ };
63
+
64
+ }
65
+
22
66
  /**
23
67
  * glTF 2.0 alphaMode for a three.js material: 0 OPAQUE, 1 MASK, 2 BLEND.
24
68
  *
@@ -734,43 +778,15 @@ export class GeometryExtractor {
734
778
 
735
779
  getPhysicalDefaults() {
736
780
 
737
- // Defaults optimized for physically-based path tracing
738
- return {
739
- emissive: new Color( 0, 0, 0 ),
740
- emissiveIntensity: 1.0,
741
- roughness: 1.0,
742
- metalness: 0.0,
743
- ior: 1.5, // Common dielectric IOR (glass, plastic)
744
- opacity: 1.0,
745
- transmission: 0.0,
746
- thickness: 0.1,
747
- attenuationColor: new Color( 0xffffff ),
748
- attenuationDistance: Infinity, // No attenuation by default
749
- dispersion: 0.0,
750
- sheen: 0.0,
751
- sheenRoughness: 1.0,
752
- sheenColor: new Color( 0x000000 ),
753
- specularIntensity: 1.0,
754
- specularColor: new Color( 0xffffff ),
755
- clearcoat: 0.0,
756
- clearcoatRoughness: 0.0,
757
- iridescence: 0.0,
758
- iridescenceIOR: 1.3,
759
- iridescenceThicknessRange: [ 100, 400 ],
760
- normalScale: { x: 1, y: 1 },
761
- bumpScale: 1.0,
762
- displacementScale: 1.0,
763
- alphaTest: 0.0,
764
- // Subsurface scattering (no native MeshPhysicalMaterial equivalent)
765
- subsurface: 0.0,
766
- subsurfaceColor: new Color( 0xffffff ),
767
- subsurfaceRadius: [ 1.0, 0.2, 0.1 ], // skin-like: red travels furthest
768
- subsurfaceRadiusScale: 1.0,
769
- subsurfaceAnisotropy: 0.0,
770
- // Surface specular anisotropy (native MeshPhysicalMaterial / KHR_materials_anisotropy)
771
- anisotropy: 0.0,
772
- anisotropyRotation: 0.0
773
- };
781
+ const defaults = {};
782
+ for ( const [ key, value ] of Object.entries( MATERIAL_DEFAULTS ) ) {
783
+
784
+ defaults[ key ] = COLOR_DEFAULTS.has( key ) ? new Color().fromArray( value ) : Array.isArray( value ) ? [ ...value ] : value;
785
+
786
+ }
787
+
788
+ defaults.normalScale = { x: MATERIAL_DEFAULTS.normalScale[ 0 ], y: MATERIAL_DEFAULTS.normalScale[ 1 ] };
789
+ return defaults;
774
790
 
775
791
  }
776
792
 
@@ -782,12 +798,13 @@ export class GeometryExtractor {
782
798
  switch ( materialType ) {
783
799
 
784
800
  case 'basic':
785
- // MeshBasicMaterial -> Unlit/Emissive material
801
+ // Unlit (GLTFLoader's KHR_materials_unlit): colour and texture are emission, nothing reflects.
786
802
  mapped.emissive = material.color.clone();
787
803
  mapped.emissiveIntensity = 1.0;
788
- mapped.color = new Color( 0x000000 ); // No diffuse reflection
804
+ mapped.color = new Color( 0x000000 );
789
805
  mapped.roughness = 1.0;
790
806
  mapped.metalness = 0.0;
807
+ mapped.specularIntensity = 0.0;
791
808
  break;
792
809
 
793
810
  case 'lambert':
@@ -807,14 +824,14 @@ export class GeometryExtractor {
807
824
 
808
825
  }
809
826
 
810
- // Convert specular color to specular intensity
827
+ // Strength in specularIntensity, hue alone in specularColor; carrying it in both squared it.
811
828
  if ( material.specular ) {
812
829
 
813
- const specularLuminance = material.specular.r * 0.299 +
814
- material.specular.g * 0.587 +
815
- material.specular.b * 0.114;
830
+ const s = material.specular;
831
+ const specularLuminance = s.r * 0.299 + s.g * 0.587 + s.b * 0.114;
832
+ const peak = Math.max( s.r, s.g, s.b );
816
833
  mapped.specularIntensity = Math.min( specularLuminance * 2.0, 1.0 );
817
- mapped.specularColor = material.specular.clone();
834
+ mapped.specularColor = peak > 0 ? s.clone().multiplyScalar( 1 / peak ) : new Color( 1, 1, 1 );
818
835
 
819
836
  }
820
837
 
@@ -842,121 +859,117 @@ export class GeometryExtractor {
842
859
  const defaults = this.getPhysicalDefaults();
843
860
  const materialType = this.getMaterialType( material );
844
861
  const legacyMapping = this.mapLegacyMaterialToPhysical( material, materialType );
862
+ const textures = resolveMaterialTextures( material );
863
+ const sources = {};
845
864
 
846
- // A transmissive surface is a dielectric interface whose IOR drives refraction, so it wins
847
- // over the metal Fresnel hack: glTF defaults metallicFactor to 1, which otherwise stamps
848
- // 2.5 onto water/glass authored as MeshStandardMaterial.
849
- const isTransmissive = ( material.transmission ?? 0.0 ) > 0.0;
850
- const isMetallic = ( material.metalness ?? legacyMapping.metalness ?? 0.0 ) > 0.1;
851
- const defaultIOR = ( isMetallic && ! isTransmissive ) ? 2.5 : defaults.ior;
865
+ const pick = ( key ) => {
852
866
 
853
- // Handle color conversion for different material types
854
- let baseColor = material.color || new Color( 0xffffff );
855
- if ( materialType === 'basic' && ! material.map ) {
867
+ if ( legacyMapping[ key ] !== undefined ) {
856
868
 
857
- // For basic materials without textures, treat color as emissive
858
- baseColor = new Color( 0x000000 );
869
+ sources[ key ] = MATERIAL_VALUE_SOURCE.MAPPED;
870
+ return legacyMapping[ key ];
859
871
 
860
- }
872
+ }
873
+
874
+ if ( material[ key ] !== undefined && material[ key ] !== null ) {
875
+
876
+ sources[ key ] = MATERIAL_VALUE_SOURCE.MATERIAL;
877
+ return material[ key ];
878
+
879
+ }
880
+
881
+ sources[ key ] = MATERIAL_VALUE_SOURCE.DEFAULT;
882
+ return defaults[ key ];
883
+
884
+ };
885
+
886
+ for ( const key of [ 'transparent', 'alphaMode', 'side', 'depthWrite' ] ) sources[ key ] = MATERIAL_VALUE_SOURCE.MATERIAL;
861
887
 
862
888
  return {
863
889
  uuid: material.uuid,
864
890
 
865
- // Base material properties
866
- color: baseColor,
867
- emissive: legacyMapping.emissive ?? material.emissive ?? defaults.emissive,
868
- emissiveIntensity: legacyMapping.emissiveIntensity ?? material.emissiveIntensity ?? defaults.emissiveIntensity,
891
+ color: pick( 'color' ),
892
+ emissive: pick( 'emissive' ),
893
+ emissiveIntensity: pick( 'emissiveIntensity' ),
869
894
 
870
- // Surface properties
871
895
  // Floor at 0.02 (the sampler's own VNDF-PDF clamp, MaterialProperties.js) rather than
872
896
  // 0.05, so near-mirror metals stay sharp without entering a new firefly regime.
873
- roughness: Math.max( 0.02, legacyMapping.roughness ?? material.roughness ?? defaults.roughness ),
874
- metalness: legacyMapping.metalness ?? material.metalness ?? defaults.metalness,
875
-
876
- // Optical properties
877
- ior: material.ior ?? defaultIOR,
878
- opacity: material.opacity ?? defaults.opacity,
879
-
880
- // Transmission properties (MeshPhysicalMaterial only)
881
- transmission: material.transmission ?? defaults.transmission,
882
- thickness: material.thickness ?? defaults.thickness,
883
- attenuationColor: material.attenuationColor ?? defaults.attenuationColor,
884
- attenuationDistance: material.attenuationDistance ?? defaults.attenuationDistance,
885
-
886
- // Advanced properties (MeshPhysicalMaterial only)
887
- dispersion: material.dispersion ?? defaults.dispersion,
888
- sheen: material.sheen ?? defaults.sheen,
889
- sheenRoughness: material.sheenRoughness ?? defaults.sheenRoughness,
890
- sheenColor: material.sheenColor ?? defaults.sheenColor,
891
- clearcoat: material.clearcoat ?? defaults.clearcoat,
892
- clearcoatRoughness: material.clearcoatRoughness ?? defaults.clearcoatRoughness,
893
- iridescence: material.iridescence ?? defaults.iridescence,
894
- iridescenceIOR: material.iridescenceIOR ?? defaults.iridescenceIOR,
895
- iridescenceThicknessRange: material.iridescenceThicknessRange ?? defaults.iridescenceThicknessRange,
896
-
897
- // Subsurface scattering (custom props; MeshPhysicalMaterial has none)
898
- subsurface: material.subsurface ?? defaults.subsurface,
899
- subsurfaceColor: material.subsurfaceColor ?? defaults.subsurfaceColor,
900
- subsurfaceRadius: material.subsurfaceRadius ?? defaults.subsurfaceRadius,
901
- subsurfaceRadiusScale: material.subsurfaceRadiusScale ?? defaults.subsurfaceRadiusScale,
902
- subsurfaceAnisotropy: material.subsurfaceAnisotropy ?? defaults.subsurfaceAnisotropy,
903
-
904
- // Surface specular anisotropy (native MeshPhysicalMaterial / KHR_materials_anisotropy)
905
- anisotropy: material.anisotropy ?? defaults.anisotropy,
906
- anisotropyRotation: material.anisotropyRotation ?? defaults.anisotropyRotation,
907
-
908
- // Specular properties (for compatibility)
909
- specularIntensity: legacyMapping.specularIntensity ?? material.specularIntensity ?? defaults.specularIntensity,
910
- specularColor: legacyMapping.specularColor ?? material.specularColor ?? defaults.specularColor,
911
-
912
- // Surface detail properties
913
- normalScale: material.normalScale ?? defaults.normalScale,
914
- bumpScale: material.bumpScale ?? defaults.bumpScale,
915
- displacementScale: material.displacementScale ?? defaults.displacementScale,
916
-
917
- // Transparency and alpha
897
+ roughness: Math.max( 0.02, pick( 'roughness' ) ),
898
+ metalness: pick( 'metalness' ),
899
+
900
+ ior: pick( 'ior' ),
901
+ opacity: pick( 'opacity' ),
902
+
903
+ transmission: pick( 'transmission' ),
904
+ thickness: pick( 'thickness' ),
905
+ attenuationColor: pick( 'attenuationColor' ),
906
+ attenuationDistance: pick( 'attenuationDistance' ),
907
+
908
+ dispersion: pick( 'dispersion' ),
909
+ sheen: pick( 'sheen' ),
910
+ sheenRoughness: pick( 'sheenRoughness' ),
911
+ sheenColor: pick( 'sheenColor' ),
912
+ clearcoat: pick( 'clearcoat' ),
913
+ clearcoatRoughness: pick( 'clearcoatRoughness' ),
914
+ iridescence: pick( 'iridescence' ),
915
+ iridescenceIOR: pick( 'iridescenceIOR' ),
916
+ iridescenceThicknessRange: pick( 'iridescenceThicknessRange' ),
917
+
918
+ subsurface: pick( 'subsurface' ),
919
+ subsurfaceColor: pick( 'subsurfaceColor' ),
920
+ subsurfaceRadius: pick( 'subsurfaceRadius' ),
921
+ subsurfaceRadiusScale: pick( 'subsurfaceRadiusScale' ),
922
+ subsurfaceAnisotropy: pick( 'subsurfaceAnisotropy' ),
923
+
924
+ anisotropy: pick( 'anisotropy' ),
925
+ anisotropyRotation: pick( 'anisotropyRotation' ),
926
+
927
+ specularIntensity: pick( 'specularIntensity' ),
928
+ specularColor: pick( 'specularColor' ),
929
+
930
+ normalScale: pick( 'normalScale' ),
931
+ bumpScale: pick( 'bumpScale' ),
932
+ displacementScale: pick( 'displacementScale' ),
933
+
918
934
  transparent: material.transparent ? 1 : 0,
919
- alphaTest: material.alphaTest ?? defaults.alphaTest,
935
+ alphaTest: pick( 'alphaTest' ),
920
936
  alphaMode: deriveAlphaMode( material ),
921
937
 
922
- // Rendering properties
923
938
  side: this.getMaterialSide( material ),
924
939
  depthWrite: material.depthWrite ?? true ? 1 : 0,
925
940
 
926
- // Texture processing
927
- map: this.processTexture( material.map, this.maps ),
928
- normalMap: this.processTexture( material.normalMap, this.normalMaps ),
929
- bumpMap: this.processTexture( material.bumpMap, this.bumpMaps ),
930
- roughnessMap: this.processTexture( material.roughnessMap, this.roughnessMaps ),
931
- metalnessMap: this.processTexture( material.metalnessMap, this.metalnessMaps ),
932
- emissiveMap: this.processTexture( material.emissiveMap, this.emissiveMaps ),
933
- displacementMap: this.processTexture( material.displacementMap, this.displacementMaps ),
934
- anisotropyMap: this.processTexture( material.anisotropyMap, this.anisotropyMaps ),
941
+ map: this.processTexture( textures.map, this.maps ),
942
+ normalMap: this.processTexture( textures.normalMap, this.normalMaps ),
943
+ bumpMap: this.processTexture( textures.bumpMap, this.bumpMaps ),
944
+ roughnessMap: this.processTexture( textures.roughnessMap, this.roughnessMaps ),
945
+ metalnessMap: this.processTexture( textures.metalnessMap, this.metalnessMaps ),
946
+ emissiveMap: this.processTexture( textures.emissiveMap, this.emissiveMaps ),
947
+ displacementMap: this.processTexture( textures.displacementMap, this.displacementMaps ),
948
+ anisotropyMap: this.processTexture( textures.anisotropyMap, this.anisotropyMaps ),
935
949
 
936
950
  // Advanced texture maps (MeshPhysicalMaterial only). Folded into their scalar factors
937
951
  // in ShadeKernel (applyExtensionMaps). thicknessMap stays dropped — thickness has no
938
952
  // render effect yet (see gap-plan Phase 4.4).
939
- clearcoatMap: this.processTexture( material.clearcoatMap, this.clearcoatMaps ),
940
- clearcoatRoughnessMap: this.processTexture( material.clearcoatRoughnessMap, this.clearcoatRoughnessMaps ),
941
- transmissionMap: this.processTexture( material.transmissionMap, this.transmissionMaps ),
953
+ clearcoatMap: this.processTexture( textures.clearcoatMap, this.clearcoatMaps ),
954
+ clearcoatRoughnessMap: this.processTexture( textures.clearcoatRoughnessMap, this.clearcoatRoughnessMaps ),
955
+ transmissionMap: this.processTexture( textures.transmissionMap, this.transmissionMaps ),
942
956
  thicknessMap: this.processTexture( material.thicknessMap, [] ),
943
- sheenColorMap: this.processTexture( material.sheenColorMap, this.sheenColorMaps ),
944
- sheenRoughnessMap: this.processTexture( material.sheenRoughnessMap, this.sheenRoughnessMaps ),
945
- specularIntensityMap: this.processTexture( material.specularIntensityMap, this.specularIntensityMaps ),
946
- specularColorMap: this.processTexture( material.specularColorMap, this.specularColorMaps ),
947
- iridescenceMap: this.processTexture( material.iridescenceMap, this.iridescenceMaps ),
948
- iridescenceThicknessMap: this.processTexture( material.iridescenceThicknessMap, this.iridescenceThicknessMaps ),
949
-
950
- // Texture transformation matrices
951
- mapMatrix: this.getTextureMatrix( material.map ),
952
- normalMapMatrices: this.getTextureMatrix( material.normalMap ),
953
- bumpMapMatrices: this.getTextureMatrix( material.bumpMap ),
954
- roughnessMapMatrices: this.getTextureMatrix( material.roughnessMap ),
955
- metalnessMapMatrices: this.getTextureMatrix( material.metalnessMap ),
956
- emissiveMapMatrices: this.getTextureMatrix( material.emissiveMap ),
957
- displacementMapMatrices: this.getTextureMatrix( material.displacementMap ),
958
-
959
- // Material type for debugging/optimization
957
+ sheenColorMap: this.processTexture( textures.sheenColorMap, this.sheenColorMaps ),
958
+ sheenRoughnessMap: this.processTexture( textures.sheenRoughnessMap, this.sheenRoughnessMaps ),
959
+ specularIntensityMap: this.processTexture( textures.specularIntensityMap, this.specularIntensityMaps ),
960
+ specularColorMap: this.processTexture( textures.specularColorMap, this.specularColorMaps ),
961
+ iridescenceMap: this.processTexture( textures.iridescenceMap, this.iridescenceMaps ),
962
+ iridescenceThicknessMap: this.processTexture( textures.iridescenceThicknessMap, this.iridescenceThicknessMaps ),
963
+
964
+ mapMatrix: this.getTextureMatrix( textures.map ),
965
+ normalMapMatrices: this.getTextureMatrix( textures.normalMap ),
966
+ bumpMapMatrices: this.getTextureMatrix( textures.bumpMap ),
967
+ roughnessMapMatrices: this.getTextureMatrix( textures.roughnessMap ),
968
+ metalnessMapMatrices: this.getTextureMatrix( textures.metalnessMap ),
969
+ emissiveMapMatrices: this.getTextureMatrix( textures.emissiveMap ),
970
+ displacementMapMatrices: this.getTextureMatrix( textures.displacementMap ),
971
+
972
+ sources,
960
973
  originalType: materialType
961
974
  };
962
975
 
@@ -1,11 +1,11 @@
1
1
  import { Vector3, Quaternion } from 'three';
2
2
  import { blackbodyToLinearRGB } from './blackbody.js';
3
- import { lightPower } from '../LightUnits.js';
3
+ import { convertLinearTriple } from '../Color/WorkingMatrix.js';
4
4
 
5
- // Scene lights hold the three.js quantity in radiometric units (see LightUnits.js): point and
6
- // spot in W/sr, the Sun in W/m², area lights in radiance. The GPU buffer wants radiant intensity
7
- // for point/spot — already what they hold — and radiant POWER for area lights, which the shader
8
- // turns back into radiance with its own π·area. This is where that adaptation happens.
5
+ // Point & spot lights specify Power in Watts; the shader uses radiant intensity
6
+ // (W/sr) as `intensity / dist²`, so convert with I = P / 4π (isotropic emitter),
7
+ // matching Blender. Area lights are already radiant power; the Sun is irradiance.
8
+ const INV_4PI = 1 / ( 4 * Math.PI );
9
9
 
10
10
  // Effective emission colour + intensity after per-light Exposure (EV stops) and
11
11
  // optional blackbody Temperature tint (Blender-style). Both fold into the values
@@ -16,15 +16,19 @@ function effectiveEmission( light ) {
16
16
  const exposure = Number.isFinite( ud.exposure ) ? ud.exposure : 0;
17
17
  const intensity = light.intensity * Math.pow( 2, exposure );
18
18
 
19
- let r = light.color.r, g = light.color.g, b = light.color.b;
19
+ const rgb = [ light.color.r, light.color.g, light.color.b ];
20
20
  if ( ud.useTemperature ) {
21
21
 
22
22
  const [ tr, tg, tb ] = blackbodyToLinearRGB( ud.temperature ?? 6500 );
23
- r *= tr; g *= tg; b *= tb;
23
+ rgb[ 0 ] *= tr; rgb[ 1 ] *= tg; rgb[ 2 ] *= tb;
24
24
 
25
25
  }
26
26
 
27
- return { r, g, b, intensity };
27
+ // A light colour is authored against sRGB primaries, like everything else three.js hands over.
28
+ // Rendering in a wider working space without converting it tints every surface it touches.
29
+ convertLinearTriple( rgb );
30
+
31
+ return { r: rgb[ 0 ], g: rgb[ 1 ], b: rgb[ 2 ], intensity };
28
32
 
29
33
  }
30
34
 
@@ -62,10 +66,9 @@ export class LightSerializer {
62
66
 
63
67
  calculateLightImportance( light, type = 'directional' ) {
64
68
 
65
- // Ranked on Power, so the ordering does not shift when a light type's stored quantity
66
- // differs from what the panel shows.
69
+ // Calculate luminance-weighted importance
67
70
  const luminance = 0.2126 * light.color.r + 0.7152 * light.color.g + 0.0722 * light.color.b;
68
- let importance = lightPower( light ) * luminance;
71
+ let importance = light.intensity * luminance;
69
72
 
70
73
  // Area lights get additional importance based on size
71
74
  if ( type === 'area' ) {
@@ -174,12 +177,6 @@ export class LightSerializer {
174
177
 
175
178
  const em = effectiveEmission( light );
176
179
 
177
- // The shader recovers radiance as power/(π·area), so hand it the power that inverts to
178
- // the radiance the light holds. Area is measured off u/v exactly as the shader does it.
179
- const rectArea = 4 * u.clone().cross( v ).length();
180
- const emittingArea = shape > 0.5 ? rectArea * Math.PI / 4 : rectArea;
181
- const power = em.intensity * Math.PI * ( normalize > 0.5 ? emittingArea : 1 );
182
-
183
180
  // Store in cache with importance (16 floats, vec4-aligned)
184
181
  this.areaLightCache.push( {
185
182
  data: [
@@ -187,7 +184,7 @@ export class LightSerializer {
187
184
  u.x, u.y, u.z, // u half-vector (3)
188
185
  v.x, v.y, v.z, // v half-vector (3)
189
186
  em.r, em.g, em.b, // color (3) — incl. temperature tint
190
- power, // radiant power in Watts (1) — incl. exposure
187
+ em.intensity, // radiant power in Watts (1) — incl. exposure
191
188
  normalize, // power-normalize flag (1)
192
189
  spread, // emission spread in radians (1)
193
190
  shape, // 0 = rect, 1 = disk/ellipse (1)
@@ -217,7 +214,7 @@ export class LightSerializer {
217
214
  data: [
218
215
  position.x, position.y, position.z, // position (3)
219
216
  em.r, em.g, em.b, // color (3) — incl. temperature tint
220
- em.intensity, // radiant intensity W/sr, incl. exposure (1)
217
+ em.intensity * INV_4PI, // radiant intensity W/sr = power(W)/4π, incl. exposure (1)
221
218
  light.distance || 0.0, // cutoff distance (0 = infinite) (1)
222
219
  light.decay !== undefined ? light.decay : 2.0 // decay exponent (1)
223
220
  ],
@@ -260,7 +257,7 @@ export class LightSerializer {
260
257
  position.x, position.y, position.z, // position (3)
261
258
  direction.x, direction.y, direction.z, // direction (3)
262
259
  em.r, em.g, em.b, // color (3) — incl. temperature tint
263
- em.intensity, // radiant intensity W/sr, incl. exposure (1)
260
+ em.intensity * INV_4PI, // radiant intensity W/sr = power(W)/4π, incl. exposure (1)
264
261
  light.angle || Math.PI / 4, // cone half-angle in radians (1)
265
262
  light.penumbra || 0.0, // penumbra [0,1] (1)
266
263
  light.distance || 0.0, // cutoff distance (0 = infinite) (1)