rayzee 10.0.0 → 11.0.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 (173) hide show
  1. package/README.md +252 -44
  2. package/dist/ActiveColor-YvuQq44O.js +13 -0
  3. package/dist/ArchiveImporter-BRQvLggb.js +4300 -0
  4. package/dist/AssetConfig-DSEntE65.js +30 -0
  5. package/dist/BidirectionalIntegrator-9lZQxFVH.js +934 -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-BCXylThH.js} +8 -6
  13. package/dist/{NeuralSuperRes-PkgDLQcD.js → NeuralSuperRes-sZAeS142.js} +55 -120
  14. package/dist/PathTracer-CQPW06Mm.js +29971 -0
  15. package/dist/PhysicalSky-BvqCw_dy.js +1219 -0
  16. package/dist/RayzeeRenderer-COpwotpU.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-BZw7TiDp.js +75 -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-BSSsKY98.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 +1 -1
  52. package/src/Processor/ArchiveImporter.js +1259 -0
  53. package/src/Processor/AssetLoader.js +327 -1763
  54. package/src/Processor/AtmosphereModel.js +1 -2
  55. package/src/Processor/BVHBuilder.js +14 -126
  56. package/src/Processor/BVHLeafFold.js +4 -6
  57. package/src/Processor/BVHRefitter.js +67 -46
  58. package/src/Processor/BufferLayout.js +291 -0
  59. package/src/Processor/EmissiveTriangleBuilder.js +1 -1
  60. package/src/Processor/EnvironmentExactTable.js +187 -0
  61. package/src/Processor/EquirectHDRInfo.js +30 -223
  62. package/src/Processor/FileFormats.js +116 -0
  63. package/src/Processor/GLTFDecoders.js +85 -0
  64. package/src/Processor/GLTFDiffuseTransmission.js +63 -0
  65. package/src/Processor/GeometryExtractor.js +15 -5
  66. package/src/Processor/HostMemory.js +7 -1
  67. package/src/Processor/IESParser.js +47 -35
  68. package/src/Processor/InstanceTable.js +1 -1
  69. package/src/Processor/KernelManager.js +35 -2
  70. package/src/Processor/MaterialPacking.js +10 -8
  71. package/src/Processor/PBRT/PBRTMaterials.js +129 -41
  72. package/src/Processor/PBRT/PBRTParser.js +30 -17
  73. package/src/Processor/PBRT/PBRTSceneBuilder.js +548 -37
  74. package/src/Processor/PBRT/PBRTTextureBake.js +154 -0
  75. package/src/Processor/PBRT/PFM.js +59 -0
  76. package/src/Processor/PackedRayBuffer.js +116 -9
  77. package/src/Processor/ParallelBVHBuilder.js +3 -6
  78. package/src/Processor/PhysicalSky.js +5 -4
  79. package/src/Processor/PlatformImageLoader.js +50 -6
  80. package/src/Processor/QueueManager.js +39 -5
  81. package/src/Processor/ReinsertionOptimizer.js +2 -2
  82. package/src/Processor/SceneProcessor.js +53 -62
  83. package/src/Processor/ShaderBuilder.js +6 -16
  84. package/src/Processor/SimpleSky.js +1 -62
  85. package/src/Processor/SolarLimb.js +3 -0
  86. package/src/Processor/SplitBLAS.js +1 -1
  87. package/src/Processor/StorageTexturePool.js +22 -1
  88. package/src/Processor/TLASBuilder.js +1 -3
  89. package/src/Processor/TextureBuckets.js +237 -0
  90. package/src/Processor/TextureCreator.js +181 -8
  91. package/src/Processor/ToneMapGPU.js +64 -15
  92. package/src/Processor/Workers/BVHRefitWorker.js +5 -2
  93. package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
  94. package/src/Processor/Workers/BVHWorker.js +5 -18
  95. package/src/Processor/Workers/CDFWorker.js +6 -183
  96. package/src/Processor/Workers/TexturesWorker.js +103 -18
  97. package/src/Processor/archiveFormats.js +7 -0
  98. package/src/Processor/utils.js +2 -1
  99. package/src/RayzeeRenderer.js +3899 -0
  100. package/src/RenderSettings.js +68 -62
  101. package/src/Stages/ASVGF.js +13 -14
  102. package/src/Stages/AutoExposure.js +17 -17
  103. package/src/Stages/BilateralFilter.js +2 -1
  104. package/src/Stages/Compositor.js +8 -23
  105. package/src/Stages/DenoiserSettings.js +182 -0
  106. package/src/Stages/EdgeFilter.js +2 -1
  107. package/src/Stages/MotionVector.js +1 -1
  108. package/src/Stages/NRD.js +11 -5
  109. package/src/Stages/NormalDepth.js +12 -8
  110. package/src/Stages/PathTracer.js +338 -43
  111. package/src/Stages/PathTracerStage.js +69 -66
  112. package/src/Stages/Variance.js +1 -1
  113. package/src/Storage/BLASCache.js +1 -1
  114. package/src/Storage/CDFCache.js +18 -17
  115. package/src/Storage/DownloadCache.js +1 -1
  116. package/src/Storage/SceneGraphCodec.js +46 -3
  117. package/src/Storage/SpillStore.js +1 -1
  118. package/src/Storage/StorageManager.js +45 -9
  119. package/src/Storage/areas.js +4 -0
  120. package/src/TSL/BVHTraversal.js +50 -25
  121. package/src/TSL/Bidirectional.js +204 -0
  122. package/src/TSL/BidirectionalLamps.js +132 -0
  123. package/src/TSL/Clearcoat.js +15 -6
  124. package/src/TSL/Common.js +20 -15
  125. package/src/TSL/ConnectKernel.js +147 -0
  126. package/src/TSL/Debugger.js +1 -1
  127. package/src/TSL/Displacement.js +4 -3
  128. package/src/TSL/EmissiveSampling.js +65 -18
  129. package/src/TSL/Environment.js +77 -106
  130. package/src/TSL/EnvironmentCDF.js +85 -39
  131. package/src/TSL/ExtendKernel.js +8 -3
  132. package/src/TSL/FinalWriteKernel.js +4 -4
  133. package/src/TSL/GenerateKernel.js +18 -4
  134. package/src/TSL/HitFacet.js +35 -5
  135. package/src/TSL/LightBVHSampling.js +40 -9
  136. package/src/TSL/LightGenerateKernel.js +293 -0
  137. package/src/TSL/LightGuide.js +198 -0
  138. package/src/TSL/LightSplatKernel.js +165 -0
  139. package/src/TSL/LightVisibility.js +111 -0
  140. package/src/TSL/LightsCore.js +13 -38
  141. package/src/TSL/LightsDirect.js +28 -29
  142. package/src/TSL/LightsIndirect.js +3 -1
  143. package/src/TSL/LightsSampling.js +239 -51
  144. package/src/TSL/MaterialEvaluation.js +113 -31
  145. package/src/TSL/MaterialProperties.js +44 -17
  146. package/src/TSL/MaterialTransmission.js +52 -47
  147. package/src/TSL/MergeKernel.js +218 -0
  148. package/src/TSL/PathTracerCore.js +65 -29
  149. package/src/TSL/Random.js +5 -1
  150. package/src/TSL/SceneResources.js +51 -0
  151. package/src/TSL/ShadeKernel.js +964 -101
  152. package/src/TSL/Struct.js +10 -0
  153. package/src/TSL/Sun.js +1 -1
  154. package/src/TSL/TextureSampling.js +66 -61
  155. package/src/addons/archives.js +14 -0
  156. package/src/addons/bidirectional.js +13 -0
  157. package/src/addons/color.js +32 -0
  158. package/src/addons/formats.js +16 -0
  159. package/src/addons/physicalSky.js +14 -0
  160. package/src/addons/storage.js +19 -0
  161. package/src/core.js +31 -0
  162. package/src/index.js +13 -23
  163. package/src/integrators/BidirectionalIntegrator.js +833 -0
  164. package/src/managers/AnimationManager.js +88 -6
  165. package/src/managers/CameraManager.js +103 -46
  166. package/src/managers/DenoisingManager.js +14 -10
  167. package/src/managers/EnvironmentManager.js +125 -103
  168. package/src/managers/GoboManager.js +9 -0
  169. package/src/managers/MaterialDataManager.js +48 -37
  170. package/src/managers/UniformManager.js +32 -10
  171. package/dist/ToneMapGPU-B6XIe3DU.js +0 -544
  172. package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
  173. 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
  }
@@ -65,6 +65,13 @@ export function pString( params, name, dflt ) {
65
65
 
66
66
  }
67
67
 
68
+ export function pBool( params, name, dflt ) {
69
+
70
+ const v = params[ name ]?.value?.[ 0 ];
71
+ return v === undefined ? dflt : v === true || v === 'true';
72
+
73
+ }
74
+
68
75
  /**
69
76
  * Resolve a spectrum/color/float-valued parameter to an RGB triple and/or a
70
77
  * texture. Returns `{ rgb, texture }` — exactly one is typically non-null.
@@ -144,15 +151,74 @@ function namedMetalKey( s ) {
144
151
 
145
152
  }
146
153
 
147
- /** Roughness from `roughness` or anisotropic `uroughness`/`vroughness`. */
148
- function resolveRoughness( params, dflt ) {
154
+ // A float parameter's value, or a float texture's mean (`ctx.floatTextureMean`); null when absent or unreadable.
155
+ async function floatOrMean( params, name, ctx ) {
156
+
157
+ const p = params[ name ];
158
+ if ( ! p ) return null;
159
+ if ( typeof p.value[ 0 ] === 'number' ) return p.value[ 0 ];
160
+ if ( p.type === 'texture' ) return ( await ctx.floatTextureMean?.( p.value[ 0 ] ) ) ?? null;
161
+ return null;
162
+
163
+ }
164
+
165
+ /**
166
+ * The engine's roughness for pbrt's `roughness` or anisotropic `uroughness`/`vroughness` (their α averaged), under
167
+ * `prefix` for a layer (`conductor.`, `interface.`); a textured roughness counts as its mean. pbrt-v4 takes GGX
168
+ * α = √roughness (`remaproughness`, the default) or α = roughness; the engine's GGX takes α = roughness². Passing
169
+ * pbrt's number through made `remaproughness false` 0.1 an α of 0.01, a near mirror. `dflt` is the engine's.
170
+ */
171
+ async function resolveRoughness( params, dflt, ctx, prefix = '' ) {
172
+
173
+ const remap = params.remaproughness?.value?.[ 0 ];
174
+ const alpha = ( r ) => ( remap === false || remap === 'false' ? r : Math.sqrt( Math.max( r, 0 ) ) );
175
+ let a = null;
176
+ const r = await floatOrMean( params, `${prefix}roughness`, ctx );
177
+ if ( r !== null ) a = alpha( r );
178
+ else {
179
+
180
+ const u = await floatOrMean( params, `${prefix}uroughness`, ctx );
181
+ const v = await floatOrMean( params, `${prefix}vroughness`, ctx );
182
+ if ( u !== null ) a = v !== null ? ( alpha( u ) + alpha( v ) ) / 2 : alpha( u );
183
+
184
+ }
185
+
186
+ return a === null ? dflt : Math.sqrt( Math.max( a, 0 ) );
187
+
188
+ }
189
+
190
+ // A conductor's colour: `reflectance`, else normal-incidence Fresnel of `eta`/`k` (under `prefix` for a coated
191
+ // conductor). pbrt's default is copper.
192
+ async function applyConductorColor( mat, params, ctx, prefix, applyAlbedo, setColor ) {
193
+
194
+ const refl = await resolveSpectrum( params, 'reflectance', ctx, null );
195
+ const etaP = params[ `${prefix}eta` ], kP = params[ `${prefix}k` ];
196
+ const etaNamed = etaP && etaP.type === 'spectrum' && typeof etaP.value[ 0 ] === 'string';
197
+
198
+ if ( refl.rgb || refl.texture ) {
199
+
200
+ applyAlbedo( refl );
201
+
202
+ } else if ( etaP && kP && ! etaNamed ) {
203
+
204
+ // Normal-incidence reflectance from complex IOR: ((η-1)²+k²)/((η+1)²+k²).
205
+ const eta = ( await resolveSpectrum( params, `${prefix}eta`, ctx, [ 0.2, 0.92, 1.1 ] ) ).rgb;
206
+ const k = ( await resolveSpectrum( params, `${prefix}k`, ctx, [ 3.9, 2.45, 2.14 ] ) ).rgb;
207
+ const fr = ( n, kk ) => ( ( n - 1 ) ** 2 + kk ** 2 ) / ( ( n + 1 ) ** 2 + kk ** 2 );
208
+ setColor( [ fr( eta[ 0 ], k[ 0 ] ), fr( eta[ 1 ], k[ 1 ] ), fr( eta[ 2 ], k[ 2 ] ) ] );
209
+
210
+ } else if ( etaP ) {
149
211
 
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;
212
+ // Named conductor spectrum → metal albedo table.
213
+ setColor( ( await resolveSpectrum( params, `${prefix}eta`, ctx, DEFAULT_METAL ) ).rgb || DEFAULT_METAL );
214
+
215
+ } else {
216
+
217
+ setColor( METAL_ALBEDO.cu ); // pbrt-v4 default conductor is copper
218
+
219
+ }
220
+
221
+ mat.metalness = 1;
156
222
 
157
223
  }
158
224
 
@@ -193,9 +259,11 @@ export async function buildMaterial( def, ctx ) {
193
259
 
194
260
  case 'diffuse': {
195
261
 
262
+ // Lambertian: no dielectric specular layer, which MeshPhysicalMaterial has by default.
196
263
  applyAlbedo( await resolveSpectrum( params, 'reflectance', ctx, [ 0.5, 0.5, 0.5 ] ) );
197
264
  mat.roughness = 1;
198
265
  mat.metalness = 0;
266
+ mat.specularIntensity = 0;
199
267
  break;
200
268
 
201
269
  }
@@ -203,35 +271,19 @@ export async function buildMaterial( def, ctx ) {
203
271
  case 'conductor':
204
272
  case 'metal': {
205
273
 
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 {
274
+ await applyConductorColor( mat, params, ctx, '', applyAlbedo, setColor );
275
+ mat.roughness = await resolveRoughness( params, 0.1, ctx );
276
+ break;
228
277
 
229
- setColor( METAL_ALBEDO.cu ); // pbrt-v4 default conductor is copper
278
+ }
230
279
 
231
- }
280
+ case 'coatedconductor': {
232
281
 
233
- mat.metalness = 1;
234
- mat.roughness = resolveRoughness( params, 0.1 );
282
+ // A conductor under a dielectric coat; pbrt's roughnesses default to 0 for both.
283
+ await applyConductorColor( mat, params, ctx, 'conductor.', applyAlbedo, setColor );
284
+ mat.roughness = await resolveRoughness( params, 0, ctx, 'conductor.' );
285
+ mat.clearcoat = 1;
286
+ mat.clearcoatRoughness = await resolveRoughness( params, 0, ctx, 'interface.' );
235
287
  break;
236
288
 
237
289
  }
@@ -243,7 +295,7 @@ export async function buildMaterial( def, ctx ) {
243
295
  mat.metalness = 0;
244
296
  mat.color.setRGB( 1, 1, 1 );
245
297
  mat.ior = pFloat( params, 'eta', 1.5 );
246
- mat.roughness = resolveRoughness( params, 0 );
298
+ mat.roughness = await resolveRoughness( params, 0, ctx );
247
299
  mat.thickness = type === 'thindielectric' ? 0 : pFloat( params, 'thickness', 0 );
248
300
  break;
249
301
 
@@ -255,18 +307,30 @@ export async function buildMaterial( def, ctx ) {
255
307
  mat.roughness = 0.6;
256
308
  mat.metalness = 0;
257
309
  mat.clearcoat = 1;
258
- mat.clearcoatRoughness = resolveRoughness( params, 0 );
310
+ mat.clearcoatRoughness = await resolveRoughness( params, 0, ctx );
259
311
  break;
260
312
 
261
313
  }
262
314
 
263
315
  case 'diffusetransmission': {
264
316
 
317
+ // pbrt reflects R and transmits T, both diffusely and both × `scale`, clamped to [0, 1]; no specular layer.
318
+ // The engine splits its diffuse lobe by `diffuseTransmission`: reflecting (1 − dt) · base, transmitting
319
+ // dt · colour. dt = max(T) makes both exact whenever max(R) + max(T) ≤ 1.
320
+ const scale = pFloat( params, 'scale', 1 );
321
+ const unit = ( v ) => Math.min( 1, Math.max( 0, scale * v ) );
265
322
  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;
323
+ const T = trans.rgb ? trans.rgb.map( unit ) : [ 0.5, 0.5, 0.5 ];
324
+ const dt = Math.min( 0.99, Math.max( ...T ) );
325
+ const refl = await resolveSpectrum( params, 'reflectance', ctx, [ 0.25, 0.25, 0.25 ] );
326
+ const base = ( v ) => Math.min( 1, unit( v ) / ( 1 - dt ) );
327
+ applyAlbedo( { texture: refl.texture, rgb: refl.rgb ? refl.rgb.map( base ) : ( refl.texture ? [ 1, 1, 1 ].map( base ) : null ) } );
328
+ if ( trans.texture ) ctx.warn( 'diffusetransmission: a textured "transmittance" is not supported — using 0.5' );
329
+ mat.diffuseTransmission = dt;
330
+ mat.diffuseTransmissionColor = new Color().setRGB( ...T.map( ( v ) => ( dt > 0 ? Math.min( 1, v / dt ) : 1 ) ) );
268
331
  mat.roughness = 1;
269
- mat.ior = 1.0;
332
+ mat.metalness = 0;
333
+ mat.specularIntensity = 0;
270
334
  break;
271
335
 
272
336
  }
@@ -292,13 +356,18 @@ export async function buildMaterial( def, ctx ) {
292
356
  // instead of warning per shape. Recursive: mix-in-mix terminates as the
293
357
  // chain bottoms out at a non-mix.
294
358
  const matNames = params.materials?.value || [];
295
- const t = Math.max( 0, Math.min( 1, pFloat( params, 'amount', 0.5 ) ) );
359
+ let t = Math.max( 0, Math.min( 1, pFloat( params, 'amount', 0.5 ) ) );
296
360
  const defA = matNames[ 0 ] ? ctx.namedMaterials?.get( matNames[ 0 ] ) : null;
297
361
  const defB = matNames[ 1 ] ? ctx.namedMaterials?.get( matNames[ 1 ] ) : null;
298
362
 
299
363
  if ( defA && defB ) {
300
364
 
301
365
  const [ matA, matB ] = await Promise.all( [ buildMaterial( defA, ctx ), buildMaterial( defB, ctx ) ] );
366
+ // pbrt picks the second material where a texture amount says so; its colours are baked by that
367
+ // texture (`ctx.bakeMaterialMix`) and the rest is weighed by the texture's mean.
368
+ const amountP = params.amount;
369
+ const baked = amountP?.type === 'texture' ? await ctx.bakeMaterialMix?.( matA, matB, amountP.value[ 0 ] ) : null;
370
+ if ( baked ) t = baked.mean;
302
371
  const lerp = ( a, b ) => a * ( 1 - t ) + b * t;
303
372
  mat.color.lerpColors( matA.color, matB.color, t );
304
373
  mat.roughness = lerp( matA.roughness, matB.roughness );
@@ -309,9 +378,25 @@ export async function buildMaterial( def, ctx ) {
309
378
  mat.clearcoat = lerp( matA.clearcoat ?? 0, matB.clearcoat ?? 0 );
310
379
  mat.clearcoatRoughness = lerp( matA.clearcoatRoughness ?? 0, matB.clearcoatRoughness ?? 0 );
311
380
  mat.emissive.lerpColors( matA.emissive, matB.emissive, t );
381
+ const dtA = matA.diffuseTransmission ?? 0, dtB = matB.diffuseTransmission ?? 0;
382
+ if ( dtA > 0 || dtB > 0 ) {
383
+
384
+ mat.diffuseTransmission = lerp( dtA, dtB );
385
+ mat.diffuseTransmissionColor = new Color().lerpColors( matA.diffuseTransmissionColor ?? new Color( 1, 1, 1 ), matB.diffuseTransmissionColor ?? new Color( 1, 1, 1 ), t );
386
+
387
+ }
388
+
312
389
  mat.emissiveIntensity = lerp( matA.emissiveIntensity ?? 0, matB.emissiveIntensity ?? 0 );
313
- // Maps can't be lerped — pick the dominant side.
390
+ // Maps can't be lerped — pick the dominant side, unless the amount's own texture baked them together.
314
391
  mat.map = ( t < 0.5 ? matA.map : matB.map ) || null;
392
+ mat.normalMap = ( t < 0.5 ? matA.normalMap : matB.normalMap ) || null;
393
+ if ( baked?.texture ) {
394
+
395
+ mat.map = baked.texture;
396
+ mat.color.setRGB( 1, 1, 1 );
397
+
398
+ }
399
+
315
400
  break;
316
401
 
317
402
  }
@@ -344,6 +429,9 @@ export async function buildMaterial( def, ctx ) {
344
429
 
345
430
  }
346
431
 
432
+ const normalMap = pString( params, 'normalmap', null );
433
+ if ( normalMap && ctx.resolveNormalMap ) mat.normalMap = await ctx.resolveNormalMap( normalMap );
434
+
347
435
  mat.roughness = Math.max( 0, Math.min( 1, mat.roughness ) );
348
436
  return mat;
349
437
 
@@ -111,6 +111,7 @@ export class PBRTParser {
111
111
  instanceCount: 0,
112
112
  skippedInstances: 0,
113
113
  objects: new Map(),
114
+ media: new Map(),
114
115
  transformTimes: { start: 0, end: 1 },
115
116
  hasMotion: false,
116
117
  warnings: []
@@ -122,14 +123,13 @@ export class PBRTParser {
122
123
  this.activeTransform = BOTH;
123
124
  this._ctmSource = null;
124
125
  this._ctmValue = null;
125
- this.state = { material: null, areaLight: null, reverseOrientation: false };
126
+ this.state = { material: null, areaLight: null, reverseOrientation: false, interior: null };
126
127
  this.attributeStack = [];
127
128
  this.transformStack = [];
128
129
  this.coordSystems = new Map();
129
130
 
130
131
  // Object capture (ObjectBegin/End)
131
132
  this.currentObject = null; // name being captured, or null
132
- this.objectBeginCTM = null; // CTM frame at ObjectBegin
133
133
 
134
134
  // Directory stack for resolving nested Includes
135
135
  this.dirStack = [ '' ];
@@ -275,7 +275,8 @@ export class PBRTParser {
275
275
  activeTransform: this.activeTransform,
276
276
  material: this.state.material,
277
277
  areaLight: this.state.areaLight,
278
- reverseOrientation: this.state.reverseOrientation
278
+ reverseOrientation: this.state.reverseOrientation,
279
+ interior: this.state.interior
279
280
  } );
280
281
 
281
282
  }
@@ -287,7 +288,7 @@ export class PBRTParser {
287
288
  this.ctm = s.ctm;
288
289
  this.ctmEnd = s.ctmEnd;
289
290
  this.activeTransform = s.activeTransform;
290
- this.state = { material: s.material, areaLight: s.areaLight, reverseOrientation: s.reverseOrientation };
291
+ this.state = { material: s.material, areaLight: s.areaLight, reverseOrientation: s.reverseOrientation, interior: s.interior };
291
292
 
292
293
  }
293
294
 
@@ -624,7 +625,7 @@ export class PBRTParser {
624
625
  this.ctm = M.identity();
625
626
  this.ctmEnd = null;
626
627
  this.activeTransform = BOTH;
627
- this.state = { material: null, areaLight: null, reverseOrientation: false };
628
+ this.state = { material: null, areaLight: null, reverseOrientation: false, interior: null };
628
629
  break;
629
630
  case 'WorldEnd': break; // legacy v3
630
631
 
@@ -673,14 +674,21 @@ export class PBRTParser {
673
674
 
674
675
  case 'MediumInterface': {
675
676
 
676
- // up to two strings (inside/outside)
677
- if ( this._peek() && this._peek().type === TokenType.STRING ) this._next();
678
- if ( this._peek() && this._peek().type === TokenType.STRING ) this._next();
677
+ // "interior" "exterior"; a single name is both. Only the interior colours a shape.
678
+ const interior = this._peek()?.type === TokenType.STRING ? this._next().value : '';
679
+ if ( this._peek()?.type === TokenType.STRING ) this._next();
680
+ this.state.interior = interior || null;
679
681
  break;
680
682
 
681
683
  }
682
684
 
683
- case 'MakeNamedMedium': this._skipNamedAndParams(); break;
685
+ case 'MakeNamedMedium': {
686
+
687
+ const mediumName = this._expectString( 'MakeNamedMedium name' );
688
+ this.ir.media.set( mediumName, this._parseParams() );
689
+ break;
690
+
691
+ }
684
692
 
685
693
  // ── materials ──
686
694
  case 'Material': {
@@ -748,13 +756,18 @@ export class PBRTParser {
748
756
 
749
757
  const type = this._expectString( 'Shape type' );
750
758
  const params = this._parseParams();
759
+ // pbrt: a shape of alpha 0 is never hit, and an area light on it emits nothing.
760
+ const alpha = params.alpha;
761
+ if ( alpha?.type === 'float' && alpha.value[ 0 ] <= 0 ) break;
762
+ if ( alpha && ! ( alpha.type === 'float' && alpha.value[ 0 ] >= 1 ) ) this._warnOnce( 'alpha', 'shape "alpha" between 0 and 1, or as a texture, is not supported — drawn opaque' );
751
763
  const shape = {
752
764
  type,
753
765
  params,
754
766
  ctm: this._ctmSnapshot(),
755
767
  material: this.state.material,
756
768
  areaLight: this.state.areaLight,
757
- reverseOrientation: this.state.reverseOrientation
769
+ reverseOrientation: this.state.reverseOrientation,
770
+ interior: this.state.interior
758
771
  };
759
772
  const end = this.currentObject === null ? this._ctmEndSnapshot() : null;
760
773
  if ( end ) shape.ctmEnd = end;
@@ -776,7 +789,6 @@ export class PBRTParser {
776
789
  // pbrt implicitly pushes graphics state.
777
790
  this._pushGraphicsState();
778
791
  this.currentObject = objName;
779
- this.objectBeginCTM = this.ctm.slice();
780
792
  if ( ! this.ir.objects.has( objName ) ) this.ir.objects.set( objName, [] );
781
793
  break;
782
794
 
@@ -785,7 +797,6 @@ export class PBRTParser {
785
797
  case 'ObjectEnd': {
786
798
 
787
799
  this.currentObject = null;
788
- this.objectBeginCTM = null;
789
800
  this._popGraphicsState();
790
801
  break;
791
802
 
@@ -824,8 +835,8 @@ export class PBRTParser {
824
835
 
825
836
  if ( this.currentObject !== null ) {
826
837
 
827
- // Store relative to the ObjectBegin frame so instances can re-place it.
828
- shape.relativeCTM = M.multiply( M.invert( this.objectBeginCTM ), shape.ctm );
838
+ // pbrt keeps a template shape's whole transform and puts each placement's on top of it,
839
+ // whatever the transform was at ObjectBegin (kroken defines under one and places at Identity).
829
840
  this.ir.objects.get( this.currentObject ).push( shape );
830
841
 
831
842
  } else {
@@ -880,7 +891,7 @@ export class PBRTParser {
880
891
 
881
892
  if ( ! this.instanceIncludes || this.currentObject !== null ) return null;
882
893
  if ( this.ctmEnd !== null || this.activeTransform !== BOTH || this.state.areaLight ) return null;
883
- return `${dir}\0${path}\0${this._stateId( this.state.material )}\0${this.state.reverseOrientation}`;
894
+ return `${dir}\0${path}\0${this._stateId( this.state.material )}\0${this.state.reverseOrientation}\0${this.state.interior}`;
884
895
 
885
896
  }
886
897
 
@@ -902,7 +913,8 @@ export class PBRTParser {
902
913
  attributes: this.attributeStack.length,
903
914
  transforms: this.transformStack.length,
904
915
  material: this.state.material,
905
- reverseOrientation: this.state.reverseOrientation
916
+ reverseOrientation: this.state.reverseOrientation,
917
+ interior: this.state.interior
906
918
  };
907
919
 
908
920
  }
@@ -912,7 +924,8 @@ export class PBRTParser {
912
924
  return this._effects === before.effects && this.ctmEnd === null && this.activeTransform === BOTH &&
913
925
  sameMatrix( this.ctm, before.ctm ) && this.attributeStack.length === before.attributes &&
914
926
  this.transformStack.length === before.transforms && this.state.material === before.material &&
915
- this.state.areaLight === null && this.state.reverseOrientation === before.reverseOrientation;
927
+ this.state.areaLight === null && this.state.reverseOrientation === before.reverseOrientation &&
928
+ this.state.interior === before.interior;
916
929
 
917
930
  }
918
931