rayzee 10.0.0 → 11.1.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 (176) hide show
  1. package/README.md +252 -44
  2. package/dist/ActiveColor-YvuQq44O.js +13 -0
  3. package/dist/ArchiveImporter-Hn35o6Z5.js +5145 -0
  4. package/dist/AssetConfig-DSEntE65.js +30 -0
  5. package/dist/BidirectionalIntegrator-CBw8hkeu.js +946 -0
  6. package/dist/BufferLayout-D_IBo2ci.js +150 -0
  7. package/dist/ColorManagement-oeEk7oSu.js +1488 -0
  8. package/dist/EngineEvents-Ci8dVWCP.js +73 -0
  9. package/dist/EngineIssues-CIZnXHsF.js +72 -0
  10. package/dist/FileFormats-sPGJeCRc.js +102 -0
  11. package/dist/Logger-dDDGj9Av.js +97 -0
  12. package/dist/{NeuralRetouch-CYgkQY8Z.js → NeuralRetouch-BzNpiVHZ.js} +8 -6
  13. package/dist/{NeuralSuperRes-PkgDLQcD.js → NeuralSuperRes-Bk7bHg2B.js} +55 -120
  14. package/dist/PathTracer-72XufKAr.js +29976 -0
  15. package/dist/PhysicalSky-BgjdV88k.js +1219 -0
  16. package/dist/RayzeeRenderer-1QRO97eq.js +3551 -0
  17. package/dist/SolarLimb-CPaeZVwz.js +721 -0
  18. package/dist/ToneMapGPU-CjU3osDd.js +177 -0
  19. package/dist/ViewTransforms-CYN7bq2w.js +255 -0
  20. package/dist/WorkingMatrix-C6y233hT.js +27 -0
  21. package/dist/ZipReader-gDSWr0I5.js +98 -0
  22. package/dist/addons/archives.es.js +3 -0
  23. package/dist/addons/bidirectional.es.js +2 -0
  24. package/dist/addons/color.es.js +3 -0
  25. package/dist/addons/formats.es.js +2 -0
  26. package/dist/addons/physical-sky.es.js +2 -0
  27. package/dist/addons/storage.es.js +3 -0
  28. package/dist/archiveFormats-tnAoUL1j.js +94 -0
  29. package/dist/identity-DrQsGT2o.js +39 -0
  30. package/dist/openStorage-DwcBRUj7.js +820 -0
  31. package/dist/rayzee-core.es.js +12 -0
  32. package/dist/rayzee.es.js +2551 -44024
  33. package/dist/rayzee.umd.js +528 -487
  34. package/dist/sceneCachePolicy-C67jbci-.js +443 -0
  35. package/dist/shared-BGBcSxBQ.js +17 -0
  36. package/package.json +23 -2
  37. package/src/Color/ActiveColor.js +34 -0
  38. package/src/Color/BasicColor.js +147 -0
  39. package/src/Color/ColorManagement.js +7 -31
  40. package/src/Color/ViewTransforms.js +5 -1
  41. package/src/EngineDefaults.js +62 -1029
  42. package/src/EngineEvents.js +2 -0
  43. package/src/EngineIssues.js +2 -0
  44. package/src/Headless.js +6 -5
  45. package/src/Passes/AIUpscaler.js +58 -141
  46. package/src/Passes/OIDNDenoiser.js +92 -65
  47. package/src/Passes/OIDNTemporalHistory.js +1 -1
  48. package/src/PathTracerApp.js +620 -4026
  49. package/src/Pipeline/PipelineContext.js +0 -4
  50. package/src/Platform.js +36 -0
  51. package/src/Processor/ArchiveCache.js +20 -7
  52. package/src/Processor/ArchiveImporter.js +1265 -0
  53. package/src/Processor/ArchiveReader.js +22 -4
  54. package/src/Processor/AssetLoader.js +327 -1763
  55. package/src/Processor/AtmosphereModel.js +1 -2
  56. package/src/Processor/BVHBuilder.js +14 -126
  57. package/src/Processor/BVHLeafFold.js +4 -6
  58. package/src/Processor/BVHRefitter.js +67 -46
  59. package/src/Processor/BufferLayout.js +291 -0
  60. package/src/Processor/EmissiveTriangleBuilder.js +1 -1
  61. package/src/Processor/EnvironmentExactTable.js +187 -0
  62. package/src/Processor/EquirectHDRInfo.js +30 -223
  63. package/src/Processor/FileFormats.js +116 -0
  64. package/src/Processor/GLTFDecoders.js +85 -0
  65. package/src/Processor/GLTFDiffuseTransmission.js +63 -0
  66. package/src/Processor/GeometryExtractor.js +15 -5
  67. package/src/Processor/HostMemory.js +7 -1
  68. package/src/Processor/IESParser.js +47 -35
  69. package/src/Processor/InstanceTable.js +1 -1
  70. package/src/Processor/KernelManager.js +35 -2
  71. package/src/Processor/MaterialPacking.js +10 -8
  72. package/src/Processor/PBRT/PBRTMaterials.js +254 -42
  73. package/src/Processor/PBRT/PBRTParser.js +49 -19
  74. package/src/Processor/PBRT/PBRTScattering.js +93 -0
  75. package/src/Processor/PBRT/PBRTSceneBuilder.js +1050 -75
  76. package/src/Processor/PBRT/PBRTTextureBake.js +179 -0
  77. package/src/Processor/PBRT/PFM.js +59 -0
  78. package/src/Processor/PackedRayBuffer.js +116 -9
  79. package/src/Processor/ParallelBVHBuilder.js +3 -6
  80. package/src/Processor/PhysicalSky.js +5 -4
  81. package/src/Processor/PlatformImageLoader.js +50 -6
  82. package/src/Processor/QueueManager.js +39 -5
  83. package/src/Processor/ReinsertionOptimizer.js +2 -2
  84. package/src/Processor/SceneMetadata.js +30 -5
  85. package/src/Processor/SceneProcessor.js +53 -62
  86. package/src/Processor/ShaderBuilder.js +6 -16
  87. package/src/Processor/SimpleSky.js +1 -62
  88. package/src/Processor/SolarLimb.js +3 -0
  89. package/src/Processor/SplitBLAS.js +1 -1
  90. package/src/Processor/StorageTexturePool.js +22 -1
  91. package/src/Processor/TLASBuilder.js +1 -3
  92. package/src/Processor/TextureBuckets.js +237 -0
  93. package/src/Processor/TextureCreator.js +181 -8
  94. package/src/Processor/ToneMapGPU.js +64 -15
  95. package/src/Processor/Workers/BVHRefitWorker.js +5 -2
  96. package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
  97. package/src/Processor/Workers/BVHWorker.js +5 -18
  98. package/src/Processor/Workers/CDFWorker.js +6 -183
  99. package/src/Processor/Workers/TexturesWorker.js +103 -18
  100. package/src/Processor/archiveFormats.js +7 -0
  101. package/src/Processor/utils.js +2 -1
  102. package/src/RayzeeRenderer.js +3900 -0
  103. package/src/RenderSettings.js +68 -62
  104. package/src/Stages/ASVGF.js +13 -14
  105. package/src/Stages/AutoExposure.js +17 -17
  106. package/src/Stages/BilateralFilter.js +2 -1
  107. package/src/Stages/Compositor.js +8 -23
  108. package/src/Stages/DenoiserSettings.js +182 -0
  109. package/src/Stages/EdgeFilter.js +2 -1
  110. package/src/Stages/MotionVector.js +1 -1
  111. package/src/Stages/NRD.js +11 -5
  112. package/src/Stages/NormalDepth.js +12 -8
  113. package/src/Stages/PathTracer.js +336 -43
  114. package/src/Stages/PathTracerStage.js +69 -66
  115. package/src/Stages/Variance.js +1 -1
  116. package/src/Storage/BLASCache.js +1 -1
  117. package/src/Storage/CDFCache.js +18 -17
  118. package/src/Storage/DownloadCache.js +1 -1
  119. package/src/Storage/SceneGraphCodec.js +46 -3
  120. package/src/Storage/SpillStore.js +1 -1
  121. package/src/Storage/StorageManager.js +45 -9
  122. package/src/Storage/areas.js +4 -0
  123. package/src/TSL/BVHTraversal.js +50 -25
  124. package/src/TSL/Bidirectional.js +208 -0
  125. package/src/TSL/BidirectionalLamps.js +132 -0
  126. package/src/TSL/Clearcoat.js +15 -6
  127. package/src/TSL/Common.js +20 -15
  128. package/src/TSL/ConnectKernel.js +167 -0
  129. package/src/TSL/Debugger.js +1 -1
  130. package/src/TSL/Displacement.js +4 -3
  131. package/src/TSL/EmissiveSampling.js +65 -18
  132. package/src/TSL/Environment.js +77 -106
  133. package/src/TSL/EnvironmentCDF.js +85 -39
  134. package/src/TSL/ExtendKernel.js +8 -3
  135. package/src/TSL/FinalWriteKernel.js +4 -4
  136. package/src/TSL/GenerateKernel.js +18 -4
  137. package/src/TSL/HitFacet.js +35 -5
  138. package/src/TSL/LightBVHSampling.js +40 -9
  139. package/src/TSL/LightGenerateKernel.js +293 -0
  140. package/src/TSL/LightGuide.js +198 -0
  141. package/src/TSL/LightSplatKernel.js +180 -0
  142. package/src/TSL/LightVisibility.js +111 -0
  143. package/src/TSL/LightsCore.js +13 -38
  144. package/src/TSL/LightsDirect.js +28 -29
  145. package/src/TSL/LightsIndirect.js +3 -1
  146. package/src/TSL/LightsSampling.js +239 -51
  147. package/src/TSL/MaterialEvaluation.js +113 -31
  148. package/src/TSL/MaterialProperties.js +44 -17
  149. package/src/TSL/MaterialTransmission.js +52 -47
  150. package/src/TSL/MergeKernel.js +224 -0
  151. package/src/TSL/PathTracerCore.js +65 -29
  152. package/src/TSL/Random.js +5 -1
  153. package/src/TSL/SceneResources.js +51 -0
  154. package/src/TSL/ShadeKernel.js +985 -101
  155. package/src/TSL/Struct.js +10 -0
  156. package/src/TSL/Sun.js +1 -1
  157. package/src/TSL/TextureSampling.js +66 -61
  158. package/src/addons/archives.js +14 -0
  159. package/src/addons/bidirectional.js +13 -0
  160. package/src/addons/color.js +32 -0
  161. package/src/addons/formats.js +16 -0
  162. package/src/addons/physicalSky.js +14 -0
  163. package/src/addons/storage.js +19 -0
  164. package/src/core.js +31 -0
  165. package/src/index.js +13 -23
  166. package/src/integrators/BidirectionalIntegrator.js +838 -0
  167. package/src/managers/AnimationManager.js +88 -6
  168. package/src/managers/CameraManager.js +103 -46
  169. package/src/managers/DenoisingManager.js +14 -10
  170. package/src/managers/EnvironmentManager.js +125 -103
  171. package/src/managers/GoboManager.js +9 -0
  172. package/src/managers/MaterialDataManager.js +48 -37
  173. package/src/managers/UniformManager.js +32 -10
  174. package/dist/ToneMapGPU-B6XIe3DU.js +0 -544
  175. package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
  176. package/src/Processor/TreeletOptimizer.js +0 -574
@@ -125,19 +125,39 @@ export function parseIES( text, name = 'ies' ) {
125
125
 
126
126
  }
127
127
 
128
+ /**
129
+ * The horizontal angle a type C profile stores for `hDeg` in [0, 360), by the symmetry its range implies
130
+ * (LM-63): one angle — the same all round; 0–90 — mirrored about the 0–180 and 90–270 planes; 0–180 — about the
131
+ * 0–180 plane; 90–270 — about the 90–270 plane; 0–360 — none.
132
+ */
133
+ function storedHorizontalAngle( hDeg, hMin, hMax ) {
134
+
135
+ if ( hMin === 0 && hMax === 90 ) {
136
+
137
+ const half = hDeg % 180;
138
+ return half > 90 ? 180 - half : half;
139
+
140
+ }
141
+
142
+ if ( hMin === 0 && hMax === 180 ) return hDeg > 180 ? 360 - hDeg : hDeg;
143
+ if ( hMin === 90 && hMax === 270 ) return hDeg >= 90 && hDeg <= 270 ? hDeg : ( ( 540 - hDeg ) % 360 );
144
+ return hDeg;
145
+
146
+ }
147
+
128
148
  /**
129
149
  * Resample an IESProfile onto a fixed-size 2D grid suitable for a
130
150
  * DataArrayTexture layer. Values are normalised to [0,1] by `maxCandela`
131
151
  * so the shader gets a pure shape multiplier; absolute intensity scaling
132
152
  * stays in `light.intensity`.
133
153
  *
134
- * UV convention:
154
+ * UV convention (TSL/LightsCore.js sampleIESProfile reads it so):
135
155
  * U = horizontal angle / 360 (0..1)
136
156
  * V = vertical angle / 180 (0..1, 0 = bulb axis, 1 = opposite axis)
137
157
  *
138
- * Profiles that are rotationally symmetric (single horizontal sample, or all
139
- * H angles identical) replicate the row across the U axis so the shader can
140
- * sample uniformly without a symmetry flag.
158
+ * The grid always spans the whole sphere: a downlight's 0–90° fills the lower half and leaves the upper dark, and
159
+ * a horizontal range with a symmetry is mirrored round (storedHorizontalAngle). Angles outside the file's range
160
+ * emit nothing. Type A and B profiles, whose angles mean something else, are stretched over the grid as before.
141
161
  *
142
162
  * @param {IESProfile} profile
143
163
  * @param {number} width - texture width in samples (horizontal/U axis)
@@ -147,20 +167,34 @@ export function parseIES( text, name = 'ies' ) {
147
167
  export function resampleIESToGrid( profile, width, height ) {
148
168
 
149
169
  const data = new Uint8Array( width * height );
150
- const { verticalAngles: vA, horizontalAngles: hA, candela, maxCandela } = profile;
170
+ const { verticalAngles: vA, horizontalAngles: hA, candela, maxCandela, photometricType } = profile;
151
171
 
152
172
  if ( maxCandela <= 0 ) return data; // dead profile → zeros
153
173
 
154
- const vMaxDeg = vA[ vA.length - 1 ];
155
- const hMaxDeg = hA[ hA.length - 1 ];
174
+ const typeC = photometricType === undefined || photometricType === 1;
175
+ const vMin = vA[ 0 ];
176
+ const vMax = vA[ vA.length - 1 ];
156
177
  const hMin = hA[ 0 ];
157
- const rotationallySymmetric = hA.length === 1 || hMaxDeg === hMin;
178
+ const hMax = hA[ hA.length - 1 ];
179
+ const rotationallySymmetric = hA.length === 1 || hMax === hMin;
180
+
181
+ const at = ( v0, v1, vt, hDeg ) => {
182
+
183
+ if ( rotationallySymmetric ) return lerp( candela[ v0 ][ 0 ], candela[ v1 ][ 0 ], vt );
184
+ if ( hDeg < hMin || hDeg > hMax ) return 0;
185
+ const h0 = lowerBoundIdx( hA, hDeg );
186
+ const h1 = Math.min( h0 + 1, hA.length - 1 );
187
+ const hSpan = hA[ h1 ] - hA[ h0 ];
188
+ const ht = hSpan > 0 ? ( hDeg - hA[ h0 ] ) / hSpan : 0;
189
+ return lerp( lerp( candela[ v0 ][ h0 ], candela[ v0 ][ h1 ], ht ), lerp( candela[ v1 ][ h0 ], candela[ v1 ][ h1 ], ht ), vt );
190
+
191
+ };
158
192
 
159
193
  for ( let py = 0; py < height; py ++ ) {
160
194
 
161
- // V coordinate → vertical angle in degrees. Texture covers [0, vMaxDeg].
162
195
  const tV = ( py + 0.5 ) / height;
163
- const vDeg = tV * vMaxDeg;
196
+ const vDeg = typeC ? tV * 180 : vMin + tV * ( vMax - vMin );
197
+ if ( vDeg < vMin || vDeg > vMax ) continue;
164
198
  const v0 = lowerBoundIdx( vA, vDeg );
165
199
  const v1 = Math.min( v0 + 1, vA.length - 1 );
166
200
  const vSpan = vA[ v1 ] - vA[ v0 ];
@@ -168,31 +202,9 @@ export function resampleIESToGrid( profile, width, height ) {
168
202
 
169
203
  for ( let px = 0; px < width; px ++ ) {
170
204
 
171
- let cd;
172
-
173
- if ( rotationallySymmetric ) {
174
-
175
- // Single column → just bilerp on V axis.
176
- cd = lerp( candela[ v0 ][ 0 ], candela[ v1 ][ 0 ], vt );
177
-
178
- } else {
179
-
180
- const tH = ( px + 0.5 ) / width;
181
- const hDeg = hMin + tH * ( hMaxDeg - hMin );
182
- const h0 = lowerBoundIdx( hA, hDeg );
183
- const h1 = Math.min( h0 + 1, hA.length - 1 );
184
- const hSpan = hA[ h1 ] - hA[ h0 ];
185
- const ht = hSpan > 0 ? ( hDeg - hA[ h0 ] ) / hSpan : 0;
186
-
187
- const c00 = candela[ v0 ][ h0 ];
188
- const c10 = candela[ v0 ][ h1 ];
189
- const c01 = candela[ v1 ][ h0 ];
190
- const c11 = candela[ v1 ][ h1 ];
191
- cd = lerp( lerp( c00, c10, ht ), lerp( c01, c11, ht ), vt );
192
-
193
- }
194
-
195
- const norm = Math.min( 1, Math.max( 0, cd / maxCandela ) );
205
+ const tH = ( px + 0.5 ) / width;
206
+ const hDeg = typeC ? storedHorizontalAngle( tH * 360, hMin, hMax ) : hMin + tH * ( hMax - hMin );
207
+ const norm = Math.min( 1, Math.max( 0, at( v0, v1, vt, hDeg ) / maxCandela ) );
196
208
  data[ py * width + px ] = Math.round( norm * 255 );
197
209
 
198
210
  }
@@ -9,7 +9,7 @@
9
9
  * denormalised one. World bounds and the inverse transform are derived on demand.
10
10
  */
11
11
 
12
- import { BVH_LEAF_MARKERS, TRIANGLE_DATA_LAYOUT, assertBVHIndexFits, bvhIndexView } from '../EngineDefaults.js';
12
+ import { TRIANGLE_DATA_LAYOUT, BVH_LEAF_MARKERS, assertBVHIndexFits, bvhIndexView } from './BufferLayout.js';
13
13
 
14
14
  const IDENTITY = Float64Array.from( [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] );
15
15
 
@@ -95,11 +95,44 @@ export class KernelManager {
95
95
  * Register a pre-built compute node.
96
96
  * @param {string} name - Kernel name (e.g. 'generate', 'extend')
97
97
  * @param {ComputeNode} computeNode - Built via `Fn().compute([dx,dy,dz], [wgx,wgy,wgz])`
98
+ * @param {{eager?: boolean}} [options] - `eager: false` leaves it out of compile(): it compiles at its first dispatch
98
99
  */
99
- register( name, computeNode ) {
100
+ register( name, computeNode, { eager = true } = {} ) {
100
101
 
101
102
  this.kernels.set( name, computeNode );
102
- this.timing.set( name, { compiledOnce: false, lastDispatchMs: 0 } );
103
+ this.timing.set( name, { compiledOnce: false, lastDispatchMs: 0, eager } );
104
+
105
+ }
106
+
107
+ /**
108
+ * Whether compile() can run here: three yields between build steps through scheduler.yield or requestAnimationFrame,
109
+ * and Node has neither (nor a page to keep responsive).
110
+ */
111
+ static get canCompileInBackground() {
112
+
113
+ return typeof globalThis.requestAnimationFrame === 'function' || typeof globalThis.scheduler?.yield === 'function';
114
+
115
+ }
116
+
117
+ /**
118
+ * Compiles the eager kernels without blocking the page: three.js generates their WGSL in steps that yield and
119
+ * creates the GPU pipelines asynchronously. A dispatch after it compiles nothing.
120
+ * @returns {Promise<void>}
121
+ */
122
+ async compile() {
123
+
124
+ const eager = [ ...this.kernels ].filter( ( [ name ] ) => this.timing.get( name )?.eager );
125
+ const t0 = performance.now();
126
+ // One call per kernel, all at once: the GPU compiles them side by side rather than each after the last.
127
+ await Promise.all( eager.map( ( [ , node ] ) => this.renderer.compileComputeAsync( node ) ) );
128
+ for ( const [ name ] of eager ) {
129
+
130
+ const entry = this.timing.get( name );
131
+ if ( entry ) entry.compiledOnce = true;
132
+
133
+ }
134
+
135
+ log.info( `${eager.length} kernels compiled in the background in ${fmt.ms( performance.now() - t0 )}` );
103
136
 
104
137
  }
105
138
 
@@ -1,4 +1,5 @@
1
- import { MATERIAL_DATA_LAYOUT as M, MATERIAL_DEFAULTS as D, normalizeAttenuationDistance } from '../EngineDefaults.js';
1
+ import { MATERIAL_DEFAULTS as D } from '../EngineDefaults.js';
2
+ import { MATERIAL_DATA_LAYOUT as M, normalizeAttenuationDistance } from './BufferLayout.js';
2
3
 
3
4
  // The first 8 Matrix3 elements; the GPU rebuilds the 9th (always 1).
4
5
  const IDENTITY_UV = [ 1, 0, 0, 0, 1, 0, 0, 0 ];
@@ -15,8 +16,7 @@ const TRANSFORMS = [
15
16
 
16
17
  /** Floats no field writes; the packer gives them fixed values. */
17
18
  export const MATERIAL_RESERVED_FLOATS = Object.freeze( [
18
- M.VISIBLE, M.SHEEN_COLOR + 3, M.DISPLACEMENT_MAP_INDEX + 1,
19
- M.SPECULAR_COLOR_MAP_INDEX + 1, M.SPECULAR_COLOR_MAP_INDEX + 2, M.SPECULAR_COLOR_MAP_INDEX + 3,
19
+ M.VISIBLE, M.DISPLACEMENT_MAP_INDEX + 1, M.DIFFUSE_TRANSMISSION_COLOR_MAP_INDEX + 1, M.DIFFUSE_TRANSMISSION_COLOR_MAP_INDEX + 2,
20
20
  ] );
21
21
 
22
22
  /**
@@ -25,7 +25,7 @@ export const MATERIAL_RESERVED_FLOATS = Object.freeze( [
25
25
  */
26
26
  export const UNIT_RANGE_PROPERTIES = Object.freeze( new Set( [
27
27
  'metalness', 'roughness', 'transmission', 'opacity', 'clearcoat', 'clearcoatRoughness',
28
- 'sheen', 'sheenRoughness', 'specularIntensity', 'iridescence', 'subsurface', 'anisotropy',
28
+ 'sheen', 'sheenRoughness', 'specularIntensity', 'iridescence', 'subsurface', 'anisotropy', 'diffuseTransmission',
29
29
  ] ) );
30
30
 
31
31
  export const clampUnit = ( value ) => Math.min( Math.max( value, 0 ), 1 );
@@ -116,6 +116,7 @@ export function packMaterial( data, base, mat ) {
116
116
  u( M.SHEEN_ROUGHNESS, mat.sheenRoughness, D.sheenRoughness );
117
117
 
118
118
  writeTriple( data, base + M.SHEEN_COLOR, mat.sheenColor, D.sheenColor );
119
+ u( M.DIFFUSE_TRANSMISSION, mat.diffuseTransmission, D.diffuseTransmission );
119
120
 
120
121
  u( M.SPECULAR_INTENSITY, mat.specularIntensity, D.specularIntensity );
121
122
  writeTriple( data, base + M.SPECULAR_COLOR, mat.specularColor, D.specularColor );
@@ -157,12 +158,13 @@ export function packMaterial( data, base, mat ) {
157
158
  data[ base + M.SPECULAR_INTENSITY_MAP_INDEX ] = mapIndex( mat.specularIntensityMap );
158
159
 
159
160
  data[ base + M.SPECULAR_COLOR_MAP_INDEX ] = mapIndex( mat.specularColorMap );
161
+ writeTriple( data, base + M.DIFFUSE_TRANSMISSION_COLOR, mat.diffuseTransmissionColor, D.diffuseTransmissionColor );
162
+ data[ base + M.DIFFUSE_TRANSMISSION_MAP_INDEX ] = mapIndex( mat.diffuseTransmissionMap );
163
+ data[ base + M.DIFFUSE_TRANSMISSION_COLOR_MAP_INDEX ] = mapIndex( mat.diffuseTransmissionColorMap );
160
164
 
161
165
  data[ base + M.VISIBLE ] = 1;
162
- data[ base + M.SHEEN_COLOR + 3 ] = 1;
163
166
  data[ base + M.DISPLACEMENT_MAP_INDEX + 1 ] = 0;
164
- data[ base + M.SPECULAR_COLOR_MAP_INDEX + 1 ] = 0;
165
- data[ base + M.SPECULAR_COLOR_MAP_INDEX + 2 ] = 0;
166
- data[ base + M.SPECULAR_COLOR_MAP_INDEX + 3 ] = 0;
167
+ data[ base + M.DIFFUSE_TRANSMISSION_COLOR_MAP_INDEX + 1 ] = 0;
168
+ data[ base + M.DIFFUSE_TRANSMISSION_COLOR_MAP_INDEX + 2 ] = 0;
167
169
 
168
170
  }
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { MeshPhysicalMaterial, Color, DoubleSide } from 'three';
12
+ import { namedScattering, dipoleReflectance, albedoForReflectance } from './PBRTScattering.js';
12
13
 
13
14
  // Normal-incidence reflectance approximations for pbrt's named conductor spectra.
14
15
  const METAL_ALBEDO = {
@@ -65,6 +66,13 @@ export function pString( params, name, dflt ) {
65
66
 
66
67
  }
67
68
 
69
+ export function pBool( params, name, dflt ) {
70
+
71
+ const v = params[ name ]?.value?.[ 0 ];
72
+ return v === undefined ? dflt : v === true || v === 'true';
73
+
74
+ }
75
+
68
76
  /**
69
77
  * Resolve a spectrum/color/float-valued parameter to an RGB triple and/or a
70
78
  * texture. Returns `{ rgb, texture }` — exactly one is typically non-null.
@@ -144,15 +152,191 @@ function namedMetalKey( s ) {
144
152
 
145
153
  }
146
154
 
147
- /** Roughness from `roughness` or anisotropic `uroughness`/`vroughness`. */
148
- function resolveRoughness( params, dflt ) {
155
+ // A float parameter's value, or a float texture's mean (`ctx.floatTextureMean`); null when absent or unreadable.
156
+ async function floatOrMean( params, name, ctx ) {
157
+
158
+ const p = params[ name ];
159
+ if ( ! p ) return null;
160
+ if ( typeof p.value[ 0 ] === 'number' ) return p.value[ 0 ];
161
+ if ( p.type === 'texture' ) return ( await ctx.floatTextureMean?.( p.value[ 0 ] ) ) ?? null;
162
+ return null;
163
+
164
+ }
165
+
166
+ /**
167
+ * The engine's roughness for pbrt's `roughness` or anisotropic `uroughness`/`vroughness` (their α averaged), under
168
+ * `prefix` for a layer (`conductor.`, `interface.`); a textured roughness counts as its mean. pbrt-v4 takes GGX
169
+ * α = √roughness (`remaproughness`, the default) or α = roughness; the engine's GGX takes α = roughness². Passing
170
+ * pbrt's number through made `remaproughness false` 0.1 an α of 0.01, a near mirror. `dflt` is the engine's.
171
+ */
172
+ const remapsRoughness = ( params ) => {
173
+
174
+ const remap = params.remaproughness?.value?.[ 0 ];
175
+ return remap !== false && remap !== 'false';
176
+
177
+ };
178
+
179
+ async function resolveRoughness( params, dflt, ctx, prefix = '' ) {
180
+
181
+ const remap = remapsRoughness( params );
182
+ const alpha = ( r ) => ( remap ? Math.sqrt( Math.max( r, 0 ) ) : r );
183
+ let a = null;
184
+ const r = await floatOrMean( params, `${prefix}roughness`, ctx );
185
+ if ( r !== null ) a = alpha( r );
186
+ else {
187
+
188
+ const u = await floatOrMean( params, `${prefix}uroughness`, ctx );
189
+ const v = await floatOrMean( params, `${prefix}vroughness`, ctx );
190
+ if ( u !== null ) a = v !== null ? ( alpha( u ) + alpha( v ) ) / 2 : alpha( u );
191
+
192
+ }
193
+
194
+ return a === null ? dflt : Math.sqrt( Math.max( a, 0 ) );
195
+
196
+ }
197
+
198
+ /**
199
+ * Sets `target` (`roughness` or `clearcoatRoughness`) as resolveRoughness does, except that a textured roughness becomes
200
+ * a roughness map with the remap baked in (`ctx.bakeFloatMap`), the value 1 multiplying it.
201
+ */
202
+ async function applyRoughness( mat, target, params, dflt, ctx, prefix = '' ) {
203
+
204
+ const single = params[ `${prefix}roughness` ];
205
+ const keys = single ? [ `${prefix}roughness` ] : [ `${prefix}uroughness`, `${prefix}vroughness` ].filter( ( key ) => params[ key ] );
206
+ if ( ctx.bakeFloatMap && keys.some( ( key ) => params[ key ].type === 'texture' ) ) {
207
+
208
+ const remap = remapsRoughness( params );
209
+ const alpha = ( r ) => ( remap ? Math.sqrt( Math.max( r, 0 ) ) : Math.max( r, 0 ) );
210
+ const map = await ctx.bakeFloatMap( params, keys, ( values ) => Math.sqrt( values.reduce( ( sum, r ) => sum + alpha( r ), 0 ) / values.length ) );
211
+ if ( map ) {
212
+
213
+ mat[ `${target}Map` ] = map;
214
+ mat[ target ] = 1;
215
+ return;
216
+
217
+ }
218
+
219
+ }
220
+
221
+ mat[ target ] = await resolveRoughness( params, dflt, ctx, prefix );
222
+
223
+ }
224
+
225
+ // A spectrum parameter as one RGB; a texture counts as its mean (`ctx.textureMeanRGB`).
226
+ async function spectrumMean( params, key, ctx, dflt ) {
149
227
 
150
- if ( params.roughness && typeof params.roughness.value[ 0 ] === 'number' ) return params.roughness.value[ 0 ];
151
- const u = pFloat( params, 'uroughness', null );
152
- const v = pFloat( params, 'vroughness', null );
153
- if ( u !== null && v !== null ) return ( u + v ) / 2;
154
- if ( u !== null ) return u;
155
- return dflt;
228
+ const p = params[ key ];
229
+ if ( p?.type === 'texture' ) return ( await ctx.textureMeanRGB?.( p.value[ 0 ] ) ) ?? dflt;
230
+ return ( await resolveSpectrum( params, key, ctx, dflt ) ).rgb ?? dflt;
231
+
232
+ }
233
+
234
+ // pbrt's default subsurface medium (materials.cpp), mm⁻¹.
235
+ const SUBSURFACE_SIGMA_A = [ 0.0011, 0.0024, 0.014 ];
236
+ const SUBSURFACE_SIGMA_S = [ 2.55, 3.21, 3.77 ];
237
+
238
+ /**
239
+ * pbrt's `subsurface` as the engine's random walk under a dielectric boundary of `eta`: σt = σa + σs (times `scale`),
240
+ * single-scattering albedo σs / σt, mean free path 1 / σt. The coefficients come as pbrt takes them — a named medium
241
+ * (g 0), `sigma_a` and `sigma_s`, or a `reflectance` and `mfp` (scaled) inverted through the dipole — and default to
242
+ * pbrt's. The base colour is the reflectance the dipole gives, for what reads it as an albedo.
243
+ */
244
+ async function applySubsurface( mat, params, ctx ) {
245
+
246
+ const eta = pFloat( params, 'eta', 1.33 );
247
+ const scale = pFloat( params, 'scale', 1 );
248
+ const scaled = ( rgb ) => rgb.map( ( v ) => Math.max( 0, v * scale ) );
249
+ let g = pFloat( params, 'g', 0 );
250
+ let sigmaA, sigmaS;
251
+
252
+ const name = pString( params, 'name', '' );
253
+ const named = name ? namedScattering( name ) : null;
254
+ if ( name && ! named ) ctx.warn( `subsurface: no medium named "${name}" — using pbrt's default` );
255
+
256
+ if ( named ) {
257
+
258
+ sigmaA = scaled( named.sigmaA );
259
+ sigmaS = scaled( named.sigmaS );
260
+ g = 0;
261
+
262
+ } else if ( params.sigma_a && params.sigma_s ) {
263
+
264
+ sigmaA = scaled( await spectrumMean( params, 'sigma_a', ctx, SUBSURFACE_SIGMA_A ) );
265
+ sigmaS = scaled( await spectrumMean( params, 'sigma_s', ctx, SUBSURFACE_SIGMA_S ) );
266
+
267
+ } else if ( params.reflectance && ! name ) {
268
+
269
+ const reflectance = await spectrumMean( params, 'reflectance', ctx, [ 0.5, 0.5, 0.5 ] );
270
+ const mfp = scaled( params.mfp ? await spectrumMean( params, 'mfp', ctx, [ 1, 1, 1 ] ) : [ 1, 1, 1 ] );
271
+ sigmaA = [];
272
+ sigmaS = [];
273
+ for ( let c = 0; c < 3; c ++ ) {
274
+
275
+ const sigmaT = 1 / Math.max( mfp[ c ], 1e-9 );
276
+ // The reduced albedo σ′s / ( σa + σ′s ), σ′s = ( 1 − g ) σs, back to σs at this σt.
277
+ const reduced = albedoForReflectance( reflectance[ c ], eta );
278
+ sigmaS[ c ] = reduced * sigmaT / ( 1 - g + reduced * g );
279
+ sigmaA[ c ] = sigmaT - sigmaS[ c ];
280
+
281
+ }
282
+
283
+ } else {
284
+
285
+ sigmaA = scaled( SUBSURFACE_SIGMA_A );
286
+ sigmaS = scaled( SUBSURFACE_SIGMA_S );
287
+
288
+ }
289
+
290
+ const sigmaT = sigmaA.map( ( a, c ) => a + sigmaS[ c ] );
291
+ mat.subsurface = 1;
292
+ mat.subsurfaceColor = new Color( ...sigmaT.map( ( t, c ) => ( t > 0 ? sigmaS[ c ] / t : 0 ) ) );
293
+ mat.subsurfaceRadius = sigmaT.map( ( t ) => ( t > 0 ? 1 / t : 1e6 ) );
294
+ mat.subsurfaceRadiusScale = 1;
295
+ mat.subsurfaceAnisotropy = Math.max( - 0.99, Math.min( 0.99, g ) );
296
+ mat.ior = eta;
297
+ mat.metalness = 0;
298
+ mat.color.setRGB( ...sigmaT.map( ( t, c ) => {
299
+
300
+ const reducedS = ( 1 - g ) * sigmaS[ c ];
301
+ return t > 0 ? dipoleReflectance( reducedS / ( sigmaA[ c ] + reducedS ), eta ) : 0;
302
+
303
+ } ) );
304
+ await applyRoughness( mat, 'roughness', params, 0, ctx );
305
+
306
+ }
307
+
308
+ // A conductor's colour: `reflectance`, else normal-incidence Fresnel of `eta`/`k` (under `prefix` for a coated
309
+ // conductor). pbrt's default is copper.
310
+ async function applyConductorColor( mat, params, ctx, prefix, applyAlbedo, setColor ) {
311
+
312
+ const refl = await resolveSpectrum( params, 'reflectance', ctx, null );
313
+ const etaP = params[ `${prefix}eta` ], kP = params[ `${prefix}k` ];
314
+ const etaNamed = etaP && etaP.type === 'spectrum' && typeof etaP.value[ 0 ] === 'string';
315
+
316
+ if ( refl.rgb || refl.texture ) {
317
+
318
+ applyAlbedo( refl );
319
+
320
+ } else if ( etaP && kP && ! etaNamed ) {
321
+
322
+ // Normal-incidence reflectance from complex IOR: ((η-1)²+k²)/((η+1)²+k²).
323
+ const eta = ( await resolveSpectrum( params, `${prefix}eta`, ctx, [ 0.2, 0.92, 1.1 ] ) ).rgb;
324
+ const k = ( await resolveSpectrum( params, `${prefix}k`, ctx, [ 3.9, 2.45, 2.14 ] ) ).rgb;
325
+ const fr = ( n, kk ) => ( ( n - 1 ) ** 2 + kk ** 2 ) / ( ( n + 1 ) ** 2 + kk ** 2 );
326
+ setColor( [ fr( eta[ 0 ], k[ 0 ] ), fr( eta[ 1 ], k[ 1 ] ), fr( eta[ 2 ], k[ 2 ] ) ] );
327
+
328
+ } else if ( etaP ) {
329
+
330
+ // Named conductor spectrum → metal albedo table.
331
+ setColor( ( await resolveSpectrum( params, `${prefix}eta`, ctx, DEFAULT_METAL ) ).rgb || DEFAULT_METAL );
332
+
333
+ } else {
334
+
335
+ setColor( METAL_ALBEDO.cu ); // pbrt-v4 default conductor is copper
336
+
337
+ }
338
+
339
+ mat.metalness = 1;
156
340
 
157
341
  }
158
342
 
@@ -193,9 +377,11 @@ export async function buildMaterial( def, ctx ) {
193
377
 
194
378
  case 'diffuse': {
195
379
 
380
+ // Lambertian: no dielectric specular layer, which MeshPhysicalMaterial has by default.
196
381
  applyAlbedo( await resolveSpectrum( params, 'reflectance', ctx, [ 0.5, 0.5, 0.5 ] ) );
197
382
  mat.roughness = 1;
198
383
  mat.metalness = 0;
384
+ mat.specularIntensity = 0;
199
385
  break;
200
386
 
201
387
  }
@@ -203,35 +389,19 @@ export async function buildMaterial( def, ctx ) {
203
389
  case 'conductor':
204
390
  case 'metal': {
205
391
 
206
- const refl = await resolveSpectrum( params, 'reflectance', ctx, null );
207
- const etaP = params.eta, kP = params.k;
208
- const etaNamed = etaP && etaP.type === 'spectrum' && typeof etaP.value[ 0 ] === 'string';
209
-
210
- if ( refl.rgb || refl.texture ) {
211
-
212
- applyAlbedo( refl );
213
-
214
- } else if ( etaP && kP && ! etaNamed ) {
215
-
216
- // Normal-incidence reflectance from complex IOR: ((η-1)²+k²)/((η+1)²+k²).
217
- const eta = ( await resolveSpectrum( params, 'eta', ctx, [ 0.2, 0.92, 1.1 ] ) ).rgb;
218
- const k = ( await resolveSpectrum( params, 'k', ctx, [ 3.9, 2.45, 2.14 ] ) ).rgb;
219
- const fr = ( n, kk ) => ( ( n - 1 ) ** 2 + kk ** 2 ) / ( ( n + 1 ) ** 2 + kk ** 2 );
220
- setColor( [ fr( eta[ 0 ], k[ 0 ] ), fr( eta[ 1 ], k[ 1 ] ), fr( eta[ 2 ], k[ 2 ] ) ] );
221
-
222
- } else if ( etaP ) {
223
-
224
- // Named conductor spectrum → metal albedo table.
225
- setColor( ( await resolveSpectrum( params, 'eta', ctx, DEFAULT_METAL ) ).rgb || DEFAULT_METAL );
226
-
227
- } else {
392
+ await applyConductorColor( mat, params, ctx, '', applyAlbedo, setColor );
393
+ await applyRoughness( mat, 'roughness', params, 0.1, ctx );
394
+ break;
228
395
 
229
- setColor( METAL_ALBEDO.cu ); // pbrt-v4 default conductor is copper
396
+ }
230
397
 
231
- }
398
+ case 'coatedconductor': {
232
399
 
233
- mat.metalness = 1;
234
- mat.roughness = resolveRoughness( params, 0.1 );
400
+ // A conductor under a dielectric coat; pbrt's roughnesses default to 0 for both.
401
+ await applyConductorColor( mat, params, ctx, 'conductor.', applyAlbedo, setColor );
402
+ await applyRoughness( mat, 'roughness', params, 0, ctx, 'conductor.' );
403
+ mat.clearcoat = 1;
404
+ await applyRoughness( mat, 'clearcoatRoughness', params, 0, ctx, 'interface.' );
235
405
  break;
236
406
 
237
407
  }
@@ -243,7 +413,7 @@ export async function buildMaterial( def, ctx ) {
243
413
  mat.metalness = 0;
244
414
  mat.color.setRGB( 1, 1, 1 );
245
415
  mat.ior = pFloat( params, 'eta', 1.5 );
246
- mat.roughness = resolveRoughness( params, 0 );
416
+ await applyRoughness( mat, 'roughness', params, 0, ctx );
247
417
  mat.thickness = type === 'thindielectric' ? 0 : pFloat( params, 'thickness', 0 );
248
418
  break;
249
419
 
@@ -255,18 +425,30 @@ export async function buildMaterial( def, ctx ) {
255
425
  mat.roughness = 0.6;
256
426
  mat.metalness = 0;
257
427
  mat.clearcoat = 1;
258
- mat.clearcoatRoughness = resolveRoughness( params, 0 );
428
+ await applyRoughness( mat, 'clearcoatRoughness', params, 0, ctx );
259
429
  break;
260
430
 
261
431
  }
262
432
 
263
433
  case 'diffusetransmission': {
264
434
 
435
+ // pbrt reflects R and transmits T, both diffusely and both × `scale`, clamped to [0, 1]; no specular layer.
436
+ // The engine splits its diffuse lobe by `diffuseTransmission`: reflecting (1 − dt) · base, transmitting
437
+ // dt · colour. dt = max(T) makes both exact whenever max(R) + max(T) ≤ 1.
438
+ const scale = pFloat( params, 'scale', 1 );
439
+ const unit = ( v ) => Math.min( 1, Math.max( 0, scale * v ) );
265
440
  const trans = await resolveSpectrum( params, 'transmittance', ctx, [ 0.25, 0.25, 0.25 ] );
266
- applyAlbedo( await resolveSpectrum( params, 'reflectance', ctx, [ 0.25, 0.25, 0.25 ] ) );
267
- mat.transmission = trans.rgb ? ( trans.rgb[ 0 ] + trans.rgb[ 1 ] + trans.rgb[ 2 ] ) / 3 : 0.5;
441
+ const T = trans.rgb ? trans.rgb.map( unit ) : [ 0.5, 0.5, 0.5 ];
442
+ const dt = Math.min( 0.99, Math.max( ...T ) );
443
+ const refl = await resolveSpectrum( params, 'reflectance', ctx, [ 0.25, 0.25, 0.25 ] );
444
+ const base = ( v ) => Math.min( 1, unit( v ) / ( 1 - dt ) );
445
+ applyAlbedo( { texture: refl.texture, rgb: refl.rgb ? refl.rgb.map( base ) : ( refl.texture ? [ 1, 1, 1 ].map( base ) : null ) } );
446
+ if ( trans.texture ) ctx.warn( 'diffusetransmission: a textured "transmittance" is not supported — using 0.5' );
447
+ mat.diffuseTransmission = dt;
448
+ mat.diffuseTransmissionColor = new Color().setRGB( ...T.map( ( v ) => ( dt > 0 ? Math.min( 1, v / dt ) : 1 ) ) );
268
449
  mat.roughness = 1;
269
- mat.ior = 1.0;
450
+ mat.metalness = 0;
451
+ mat.specularIntensity = 0;
270
452
  break;
271
453
 
272
454
  }
@@ -292,13 +474,18 @@ export async function buildMaterial( def, ctx ) {
292
474
  // instead of warning per shape. Recursive: mix-in-mix terminates as the
293
475
  // chain bottoms out at a non-mix.
294
476
  const matNames = params.materials?.value || [];
295
- const t = Math.max( 0, Math.min( 1, pFloat( params, 'amount', 0.5 ) ) );
477
+ let t = Math.max( 0, Math.min( 1, pFloat( params, 'amount', 0.5 ) ) );
296
478
  const defA = matNames[ 0 ] ? ctx.namedMaterials?.get( matNames[ 0 ] ) : null;
297
479
  const defB = matNames[ 1 ] ? ctx.namedMaterials?.get( matNames[ 1 ] ) : null;
298
480
 
299
481
  if ( defA && defB ) {
300
482
 
301
483
  const [ matA, matB ] = await Promise.all( [ buildMaterial( defA, ctx ), buildMaterial( defB, ctx ) ] );
484
+ // pbrt picks the second material where a texture amount says so; its colours are baked by that
485
+ // texture (`ctx.bakeMaterialMix`) and the rest is weighed by the texture's mean.
486
+ const amountP = params.amount;
487
+ const baked = amountP?.type === 'texture' ? await ctx.bakeMaterialMix?.( matA, matB, amountP.value[ 0 ] ) : null;
488
+ if ( baked ) t = baked.mean;
302
489
  const lerp = ( a, b ) => a * ( 1 - t ) + b * t;
303
490
  mat.color.lerpColors( matA.color, matB.color, t );
304
491
  mat.roughness = lerp( matA.roughness, matB.roughness );
@@ -309,9 +496,25 @@ export async function buildMaterial( def, ctx ) {
309
496
  mat.clearcoat = lerp( matA.clearcoat ?? 0, matB.clearcoat ?? 0 );
310
497
  mat.clearcoatRoughness = lerp( matA.clearcoatRoughness ?? 0, matB.clearcoatRoughness ?? 0 );
311
498
  mat.emissive.lerpColors( matA.emissive, matB.emissive, t );
499
+ const dtA = matA.diffuseTransmission ?? 0, dtB = matB.diffuseTransmission ?? 0;
500
+ if ( dtA > 0 || dtB > 0 ) {
501
+
502
+ mat.diffuseTransmission = lerp( dtA, dtB );
503
+ mat.diffuseTransmissionColor = new Color().lerpColors( matA.diffuseTransmissionColor ?? new Color( 1, 1, 1 ), matB.diffuseTransmissionColor ?? new Color( 1, 1, 1 ), t );
504
+
505
+ }
506
+
312
507
  mat.emissiveIntensity = lerp( matA.emissiveIntensity ?? 0, matB.emissiveIntensity ?? 0 );
313
- // Maps can't be lerped — pick the dominant side.
508
+ // Maps can't be lerped — pick the dominant side, unless the amount's own texture baked them together.
314
509
  mat.map = ( t < 0.5 ? matA.map : matB.map ) || null;
510
+ mat.normalMap = ( t < 0.5 ? matA.normalMap : matB.normalMap ) || null;
511
+ if ( baked?.texture ) {
512
+
513
+ mat.map = baked.texture;
514
+ mat.color.setRGB( 1, 1, 1 );
515
+
516
+ }
517
+
315
518
  break;
316
519
 
317
520
  }
@@ -322,7 +525,13 @@ export async function buildMaterial( def, ctx ) {
322
525
 
323
526
  }
324
527
 
325
- case 'subsurface':
528
+ case 'subsurface': {
529
+
530
+ await applySubsurface( mat, params, ctx );
531
+ break;
532
+
533
+ }
534
+
326
535
  case 'hair':
327
536
  case 'measured': {
328
537
 
@@ -344,6 +553,9 @@ export async function buildMaterial( def, ctx ) {
344
553
 
345
554
  }
346
555
 
556
+ const normalMap = pString( params, 'normalmap', null );
557
+ if ( normalMap && ctx.resolveNormalMap ) mat.normalMap = await ctx.resolveNormalMap( normalMap );
558
+
347
559
  mat.roughness = Math.max( 0, Math.min( 1, mat.roughness ) );
348
560
  return mat;
349
561