rayzee 9.4.2 → 9.5.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 (144) hide show
  1. package/README.md +223 -21
  2. package/dist/ActiveColor-YvuQq44O.js +13 -0
  3. package/dist/ArchiveImporter-xuUJiklP.js +4301 -0
  4. package/dist/AssetConfig-DSEntE65.js +30 -0
  5. package/dist/BidirectionalIntegrator-C5PKjJoT.js +934 -0
  6. package/dist/ColorManagement-CheBCPaW.js +1488 -0
  7. package/dist/EngineDefaults-DH3bI5t-.js +844 -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-BnHghprG.js +97 -0
  12. package/dist/{NeuralRetouch-vkdXXyK2.js → NeuralRetouch-CW11yLZ_.js} +8 -6
  13. package/dist/{NeuralSuperRes-FVqRJKQV.js → NeuralSuperRes-BekaecYg.js} +55 -120
  14. package/dist/PathTracer-Ccl9KOuF.js +29729 -0
  15. package/dist/PhysicalSky-Dv2PMFGW.js +1219 -0
  16. package/dist/RayzeeRenderer-BQxnzCTD.js +3542 -0
  17. package/dist/SolarLimb-ken-JbDK.js +721 -0
  18. package/dist/ToneMapGPU-BHi5JzIq.js +177 -0
  19. package/dist/ViewTransforms-D0rTLBsX.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 +2266 -44079
  33. package/dist/rayzee.umd.js +485 -473
  34. package/dist/sceneCachePolicy-BmwYOYDu.js +183 -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/EngineDefaults.js +22 -8
  41. package/src/EngineEvents.js +2 -0
  42. package/src/EngineIssues.js +1 -0
  43. package/src/PathTracerApp.js +617 -4016
  44. package/src/Pipeline/PipelineContext.js +0 -4
  45. package/src/Platform.js +36 -0
  46. package/src/Processor/ArchiveCache.js +1 -1
  47. package/src/Processor/ArchiveImporter.js +1259 -0
  48. package/src/Processor/AssetLoader.js +322 -1746
  49. package/src/Processor/AtmosphereModel.js +1 -2
  50. package/src/Processor/BVHBuilder.js +4 -95
  51. package/src/Processor/EnvironmentExactTable.js +187 -0
  52. package/src/Processor/EquirectHDRInfo.js +30 -223
  53. package/src/Processor/FileFormats.js +116 -0
  54. package/src/Processor/GLTFDecoders.js +85 -0
  55. package/src/Processor/GLTFDiffuseTransmission.js +63 -0
  56. package/src/Processor/GeometryExtractor.js +12 -1
  57. package/src/Processor/IESParser.js +47 -35
  58. package/src/Processor/KernelManager.js +35 -2
  59. package/src/Processor/MaterialPacking.js +8 -7
  60. package/src/Processor/PBRT/PBRTMaterials.js +129 -41
  61. package/src/Processor/PBRT/PBRTParser.js +30 -17
  62. package/src/Processor/PBRT/PBRTSceneBuilder.js +548 -37
  63. package/src/Processor/PBRT/PBRTTextureBake.js +154 -0
  64. package/src/Processor/PBRT/PFM.js +59 -0
  65. package/src/Processor/PackedRayBuffer.js +116 -9
  66. package/src/Processor/ParallelBVHBuilder.js +3 -6
  67. package/src/Processor/PhysicalSky.js +5 -4
  68. package/src/Processor/PlatformImageLoader.js +50 -6
  69. package/src/Processor/QueueManager.js +12 -3
  70. package/src/Processor/ReinsertionOptimizer.js +2 -2
  71. package/src/Processor/SceneProcessor.js +15 -55
  72. package/src/Processor/ShaderBuilder.js +6 -16
  73. package/src/Processor/SolarLimb.js +3 -0
  74. package/src/Processor/TextureCreator.js +1 -1
  75. package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
  76. package/src/Processor/Workers/BVHWorker.js +5 -18
  77. package/src/Processor/Workers/CDFWorker.js +6 -183
  78. package/src/Processor/archiveFormats.js +7 -0
  79. package/src/Processor/utils.js +2 -1
  80. package/src/RayzeeRenderer.js +3902 -0
  81. package/src/RenderSettings.js +49 -41
  82. package/src/Stages/ASVGF.js +5 -7
  83. package/src/Stages/AutoExposure.js +3 -12
  84. package/src/Stages/Compositor.js +8 -23
  85. package/src/Stages/NRD.js +9 -2
  86. package/src/Stages/NormalDepth.js +9 -6
  87. package/src/Stages/PathTracer.js +254 -38
  88. package/src/Stages/PathTracerStage.js +11 -16
  89. package/src/Storage/BLASCache.js +1 -1
  90. package/src/Storage/CDFCache.js +18 -17
  91. package/src/Storage/DownloadCache.js +1 -1
  92. package/src/Storage/SceneGraphCodec.js +46 -3
  93. package/src/Storage/SpillStore.js +1 -1
  94. package/src/Storage/StorageManager.js +2 -4
  95. package/src/Storage/areas.js +4 -0
  96. package/src/TSL/BVHTraversal.js +62 -24
  97. package/src/TSL/Bidirectional.js +204 -0
  98. package/src/TSL/BidirectionalLamps.js +132 -0
  99. package/src/TSL/Clearcoat.js +15 -6
  100. package/src/TSL/Common.js +19 -14
  101. package/src/TSL/ConnectKernel.js +147 -0
  102. package/src/TSL/Debugger.js +1 -1
  103. package/src/TSL/Displacement.js +4 -3
  104. package/src/TSL/EmissiveSampling.js +64 -17
  105. package/src/TSL/Environment.js +77 -106
  106. package/src/TSL/EnvironmentCDF.js +85 -39
  107. package/src/TSL/ExtendKernel.js +8 -3
  108. package/src/TSL/FinalWriteKernel.js +4 -4
  109. package/src/TSL/GenerateKernel.js +18 -4
  110. package/src/TSL/HitFacet.js +35 -5
  111. package/src/TSL/LightBVHSampling.js +40 -9
  112. package/src/TSL/LightGenerateKernel.js +293 -0
  113. package/src/TSL/LightGuide.js +198 -0
  114. package/src/TSL/LightSplatKernel.js +165 -0
  115. package/src/TSL/LightsCore.js +13 -38
  116. package/src/TSL/LightsDirect.js +28 -29
  117. package/src/TSL/LightsIndirect.js +3 -1
  118. package/src/TSL/LightsSampling.js +90 -38
  119. package/src/TSL/MaterialEvaluation.js +113 -31
  120. package/src/TSL/MaterialProperties.js +44 -17
  121. package/src/TSL/MaterialTransmission.js +52 -47
  122. package/src/TSL/MergeKernel.js +218 -0
  123. package/src/TSL/PathTracerCore.js +65 -29
  124. package/src/TSL/Random.js +4 -0
  125. package/src/TSL/SceneResources.js +51 -0
  126. package/src/TSL/ShadeKernel.js +881 -103
  127. package/src/TSL/Struct.js +10 -0
  128. package/src/TSL/Sun.js +1 -1
  129. package/src/TSL/TextureSampling.js +65 -60
  130. package/src/addons/archives.js +14 -0
  131. package/src/addons/bidirectional.js +13 -0
  132. package/src/addons/color.js +32 -0
  133. package/src/addons/formats.js +16 -0
  134. package/src/addons/physicalSky.js +14 -0
  135. package/src/addons/storage.js +19 -0
  136. package/src/core.js +35 -0
  137. package/src/index.js +3 -1
  138. package/src/integrators/BidirectionalIntegrator.js +832 -0
  139. package/src/managers/EnvironmentManager.js +85 -33
  140. package/src/managers/MaterialDataManager.js +42 -0
  141. package/src/managers/UniformManager.js +0 -1
  142. package/dist/ToneMapGPU-DKQM1pMd.js +0 -552
  143. package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
  144. package/src/Processor/TreeletOptimizer.js +0 -574
@@ -19,15 +19,31 @@ import {
19
19
  Group, Mesh, InstancedMesh, PerspectiveCamera, OrthographicCamera, Matrix4, Vector3, Quaternion,
20
20
  BufferGeometry, Float32BufferAttribute, Uint32BufferAttribute, SphereGeometry,
21
21
  DataTexture, FloatType, RGBAFormat, LinearFilter, EquirectangularReflectionMapping,
22
- SRGBColorSpace, AnimationClip, VectorKeyframeTrack, QuaternionKeyframeTrack,
23
- NumberKeyframeTrack, BooleanKeyframeTrack
22
+ SRGBColorSpace, NoColorSpace, AnimationClip, VectorKeyframeTrack, QuaternionKeyframeTrack,
23
+ NumberKeyframeTrack, BooleanKeyframeTrack, DirectionalLight, PointLight, SpotLight, FrontSide, DoubleSide,
24
+ RepeatWrapping
24
25
  } from 'three';
25
- import { buildMaterial, pFloat, pString, resolveSpectrum } from './PBRTMaterials.js';
26
+ import { buildMaterial, pBool, pFloat, pString, resolveSpectrum } from './PBRTMaterials.js';
27
+ import { makeLayer, layerMean, bake, mixOf, srgbToLinear, linearToSRGB } from './PBRTTextureBake.js';
26
28
  import { loopSubdivide } from './LoopSubdivision.js';
27
29
  import { octahedralToEquirect } from './EqualAreaOctahedral.js';
28
30
  import { tessellateCurve } from './PBRTCurves.js';
29
31
  import * as M from './PBRTMath.js';
30
32
 
33
+ const LAMP_TYPES = new Set( [ 'distant', 'point', 'spot' ] );
34
+
35
+ // pbrt scales a light's spectrum to luminance 1 unless it is RGB, so a blackbody of any temperature is equally bright.
36
+ async function lightRGB( params, name, ctx ) {
37
+
38
+ const p = params[ name ];
39
+ if ( p?.type === 'spectrum' && typeof p.value[ 0 ] === 'string' && p.value[ 0 ].startsWith( 'stdillum' ) ) return [ 1, 1, 1 ];
40
+ const rgb = ( await resolveSpectrum( params, name, ctx, [ 1, 1, 1 ] ) ).rgb || [ 1, 1, 1 ];
41
+ if ( ! p || p.type === 'rgb' || p.type === 'color' || p.type === 'float' ) return rgb;
42
+ const y = 0.2126 * rgb[ 0 ] + 0.7152 * rgb[ 1 ] + 0.0722 * rgb[ 2 ];
43
+ return y > 0 ? rgb.map( v => v / y ) : rgb;
44
+
45
+ }
46
+
31
47
  const FLIP_Z = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, 1 ];
32
48
  const MAX_SKY_READBACK = 4096;
33
49
 
@@ -75,11 +91,7 @@ const DEFAULT_TRIANGLE_BUDGET = 45_000_000;
75
91
  // Placements are their own budget: geometry is shared, but each costs a TLAS leaf and an
76
92
  // instance record. isCoastline's 5.09M loaded and rendered at 23 fps.
77
93
  const DEFAULT_PLACEMENT_BUDGET = 6_000_000;
78
- // With the memory spill the triangles and BVH leave the heap as the build goes, so the host
79
- // passes these instead. The whole 15-part Moana subset (55.7M / 7.0M) loads cold under them, at
80
- // an 8.1 GB preflight estimate; the preflight, not the budget, refuses anything larger.
81
- export const SPILL_TRIANGLE_BUDGET = 60_000_000;
82
- export const SPILL_PLACEMENT_BUDGET = 8_000_000;
94
+ export { SPILL_TRIANGLE_BUDGET, SPILL_PLACEMENT_BUDGET } from '../../EngineDefaults.js';
83
95
  // Non-instanced shapes merge into a few large meshes: isPalmRig declares 173,338 of them,
84
96
  // and one BufferGeometry + Mesh each measured 3.3 GB — the loader OOM'd before tracing.
85
97
  const DEFAULT_MERGE_MIN_SHAPES = 256;
@@ -95,7 +107,7 @@ const DEFAULT_CURVE_SIDES = { flat: 1, ribbon: 1, cylinder: 2 };
95
107
  const DEFAULT_CURVE_TOLERANCE = 0.05;
96
108
 
97
109
  /** Bumped whenever the same scene files build a different graph, so a stored graph is not reused. */
98
- export const PBRT_BUILD_REVISION = 4;
110
+ export const PBRT_BUILD_REVISION = 8;
99
111
 
100
112
  function samePlacements( a, b ) {
101
113
 
@@ -135,6 +147,115 @@ function pixelsFromDrawable( image, srgb ) {
135
147
 
136
148
  }
137
149
 
150
+ /**
151
+ * A texture's pixels, for baking: `{ data, width, height, channels: 4, topDown, float }`, or null where this runtime
152
+ * cannot read them. A DataTexture's rows run bottom up unless it says flipY; a drawable's top down.
153
+ */
154
+ function texturePixels( texture ) {
155
+
156
+ const image = texture?.image;
157
+ if ( ! image ) return null;
158
+
159
+ if ( ArrayBuffer.isView( image.data ) ) {
160
+
161
+ return { data: image.data, width: image.width, height: image.height, channels: 4, topDown: texture.flipY === true, float: ! ( image.data instanceof Uint8Array || image.data instanceof Uint8ClampedArray ) };
162
+
163
+ }
164
+
165
+ const { width, height } = image;
166
+ if ( ! width || ! height ) return null;
167
+ let canvas = null;
168
+ if ( typeof OffscreenCanvas !== 'undefined' ) canvas = new OffscreenCanvas( width, height );
169
+ else if ( typeof document !== 'undefined' ) {
170
+
171
+ canvas = document.createElement( 'canvas' );
172
+ canvas.width = width;
173
+ canvas.height = height;
174
+
175
+ } else return null;
176
+
177
+ try {
178
+
179
+ const ctx = canvas.getContext( '2d', { willReadFrequently: true } );
180
+ if ( ! ctx ) return null;
181
+ ctx.drawImage( image, 0, 0 );
182
+ return { data: ctx.getImageData( 0, 0, width, height ).data, width, height, channels: 4, topDown: true, float: false };
183
+
184
+ } catch {
185
+
186
+ return null;
187
+
188
+ }
189
+
190
+ }
191
+
192
+ // pbrt's `encoding`: an 8-bit image is sRGB unless it says "linear" (or "gamma g").
193
+ function decoderFor( params ) {
194
+
195
+ const encoding = pString( params, 'encoding', 'sRGB' );
196
+ if ( encoding === 'linear' ) return ( v ) => v;
197
+ const gamma = /^gamma\s+([\d.]+)$/.exec( encoding );
198
+ return gamma ? ( v ) => v ** Number( gamma[ 1 ] ) : srgbToLinear;
199
+
200
+ }
201
+
202
+ function uvMapping( params ) {
203
+
204
+ return {
205
+ su: pFloat( params, 'uscale', 1 ), sv: pFloat( params, 'vscale', 1 ),
206
+ du: pFloat( params, 'udelta', 0 ), dv: pFloat( params, 'vdelta', 0 )
207
+ };
208
+
209
+ }
210
+
211
+ function setMapping( texture, { su, sv, du, dv } ) {
212
+
213
+ texture.repeat.set( su, sv );
214
+ texture.offset.set( du, dv );
215
+
216
+ }
217
+
218
+ /**
219
+ * A float image (EXR, HDR, PFM) as 8-bit, the only texel a material map takes here: sRGB-encoded for a colour,
220
+ * linear for data. Any other texture comes back as it is.
221
+ */
222
+ function eightBit( texture, { srgb } ) {
223
+
224
+ const data = texture?.image?.data;
225
+ if ( ! ArrayBuffer.isView( data ) || data instanceof Uint8Array || data instanceof Uint8ClampedArray ) return texture;
226
+
227
+ const { width, height } = texture.image;
228
+ const bytes = new Uint8Array( width * height * 4 );
229
+ const channels = data.length / ( width * height );
230
+ for ( let i = 0; i < width * height; i ++ ) for ( let c = 0; c < 4; c ++ ) {
231
+
232
+ const v = c < channels ? Math.min( 1, Math.max( 0, data[ i * channels + c ] ) ) : 1;
233
+ bytes[ i * 4 + c ] = Math.round( 255 * ( srgb && c < 3 ? linearToSRGB( v ) : v ) );
234
+
235
+ }
236
+
237
+ const out = new DataTexture( bytes, width, height, RGBAFormat );
238
+ out.flipY = texture.flipY;
239
+ out.colorSpace = srgb ? SRGBColorSpace : NoColorSpace;
240
+ out.wrapS = out.wrapT = RepeatWrapping;
241
+ // No archive path: a stored scene keeps these bytes rather than decoding the float file again.
242
+ out.needsUpdate = true;
243
+ return out;
244
+
245
+ }
246
+
247
+ // A baked image as a texture: sRGB bytes, bottom row first, in the uv mapping it was baked in.
248
+ function bakedTexture( { data, width, height, mapping } ) {
249
+
250
+ const texture = new DataTexture( data, width, height, RGBAFormat );
251
+ texture.colorSpace = SRGBColorSpace;
252
+ texture.wrapS = texture.wrapT = RepeatWrapping;
253
+ setMapping( texture, mapping );
254
+ texture.needsUpdate = true;
255
+ return texture;
256
+
257
+ }
258
+
138
259
  /**
139
260
  * Names an unreadable texture may have been derived from. Moana binds colour as
140
261
  * `"texture reflectance" "el:part_Color"` and leaves `MakeNamedMaterial "el:part"` in the
@@ -192,6 +313,9 @@ export class PBRTSceneBuilder {
192
313
  this._materialBySignature = new Map(); // visual signature -> shared MeshPhysicalMaterial
193
314
  this._noUVMaterials = new Map(); // textured material -> its map-less clone
194
315
  this._plyCache = new Map(); // filename -> Promise<BufferGeometry|null>
316
+ this._imageCache = new Map(); // filename -> Promise<Texture|null>, decoded once for every named texture
317
+ this._termCache = new Map(); // texture name + channels -> Promise<{rgb}|{layer, tint?}|null>
318
+ this._mediumMaterials = new Map(); // material -> Map(medium name -> material with its attenuation)
195
319
 
196
320
  }
197
321
 
@@ -308,6 +432,7 @@ export class PBRTSceneBuilder {
308
432
 
309
433
  // Infinite light → environment
310
434
  const environment = await this._buildEnvironment( ir.lights );
435
+ await this._buildLamps( ir.lights, group );
311
436
 
312
437
  this._reportUnsupportedLights( ir.lights );
313
438
 
@@ -933,6 +1058,58 @@ export class PBRTSceneBuilder {
933
1058
 
934
1059
  async _createGeometry( shape ) {
935
1060
 
1061
+ const geometry = await this._shapeGeometry( shape );
1062
+ return geometry && shape.areaLight && ! pBool( shape.areaLight.params, 'twosided', false ) ? this._facingEmission( shape, geometry ) : geometry;
1063
+
1064
+ }
1065
+
1066
+ /**
1067
+ * A one-sided area light emits on the side pbrt's surface normal faces: towards the vertex normals where the shape
1068
+ * has them, else its winding's side, turned over by ReverseOrientation. The engine emits from a triangle's front,
1069
+ * so a triangle facing the other way is rewound, on an index of its own: the geometry may be shared.
1070
+ * @private
1071
+ */
1072
+ _facingEmission( shape, geometry ) {
1073
+
1074
+ const normal = geometry.userData.pbrtNormals ? geometry.getAttribute( 'normal' ) : null;
1075
+ if ( ! normal && ! shape.reverseOrientation ) return geometry;
1076
+
1077
+ const p = geometry.getAttribute( 'position' ).array, n = normal?.array;
1078
+ const src = geometry.index?.array ?? null;
1079
+ const count = src ? src.length : geometry.getAttribute( 'position' ).count;
1080
+ const index = new Uint32Array( count );
1081
+ let flipped = 0;
1082
+
1083
+ for ( let t = 0; t + 2 < count; t += 3 ) {
1084
+
1085
+ const a = src ? src[ t ] : t, b = src ? src[ t + 1 ] : t + 1, c = src ? src[ t + 2 ] : t + 2;
1086
+ let flip = shape.reverseOrientation;
1087
+ if ( n ) {
1088
+
1089
+ const ux = p[ b * 3 ] - p[ a * 3 ], uy = p[ b * 3 + 1 ] - p[ a * 3 + 1 ], uz = p[ b * 3 + 2 ] - p[ a * 3 + 2 ];
1090
+ const vx = p[ c * 3 ] - p[ a * 3 ], vy = p[ c * 3 + 1 ] - p[ a * 3 + 1 ], vz = p[ c * 3 + 2 ] - p[ a * 3 + 2 ];
1091
+ const sx = n[ a * 3 ] + n[ b * 3 ] + n[ c * 3 ], sy = n[ a * 3 + 1 ] + n[ b * 3 + 1 ] + n[ c * 3 + 1 ], sz = n[ a * 3 + 2 ] + n[ b * 3 + 2 ] + n[ c * 3 + 2 ];
1092
+ flip = ( uy * vz - uz * vy ) * sx + ( uz * vx - ux * vz ) * sy + ( ux * vy - uy * vx ) * sz < 0;
1093
+
1094
+ }
1095
+
1096
+ index[ t ] = a;
1097
+ index[ t + 1 ] = flip ? c : b;
1098
+ index[ t + 2 ] = flip ? b : c;
1099
+ if ( flip ) flipped ++;
1100
+
1101
+ }
1102
+
1103
+ if ( flipped === 0 ) return geometry;
1104
+ const oriented = new BufferGeometry();
1105
+ for ( const name in geometry.attributes ) oriented.setAttribute( name, geometry.getAttribute( name ) );
1106
+ oriented.setIndex( new Uint32BufferAttribute( index, 1 ) );
1107
+ return oriented;
1108
+
1109
+ }
1110
+
1111
+ _shapeGeometry( shape ) {
1112
+
936
1113
  switch ( shape.type ) {
937
1114
 
938
1115
  case 'trianglemesh': return this._triangleMesh( shape.params );
@@ -963,7 +1140,12 @@ export class PBRTSceneBuilder {
963
1140
  geo.setAttribute( 'position', new Float32BufferAttribute( float32( P ), 3 ) );
964
1141
 
965
1142
  const N = params.N?.value;
966
- if ( N && N.length === P.length ) geo.setAttribute( 'normal', new Float32BufferAttribute( float32( N ), 3 ) );
1143
+ if ( N && N.length === P.length ) {
1144
+
1145
+ geo.setAttribute( 'normal', new Float32BufferAttribute( float32( N ), 3 ) );
1146
+ geo.userData.pbrtNormals = true;
1147
+
1148
+ }
967
1149
 
968
1150
  const uv = ( params.uv || params.st )?.value;
969
1151
  if ( uv && uv.length === ( P.length / 3 ) * 2 ) geo.setAttribute( 'uv', new Float32BufferAttribute( float32( uv ), 2 ) );
@@ -1100,7 +1282,8 @@ export class PBRTSceneBuilder {
1100
1282
 
1101
1283
  }
1102
1284
 
1103
- if ( ! geo.getAttribute( 'normal' ) ) geo.computeVertexNormals();
1285
+ if ( geo.getAttribute( 'normal' ) ) geo.userData.pbrtNormals = true;
1286
+ else geo.computeVertexNormals();
1104
1287
  return geo;
1105
1288
 
1106
1289
  } catch ( e ) {
@@ -1153,6 +1336,13 @@ export class PBRTSceneBuilder {
1153
1336
 
1154
1337
  async _getMaterial( shape ) {
1155
1338
 
1339
+ const material = await this._surfaceMaterial( shape );
1340
+ return shape.interior ? this._inMedium( material, shape.interior ) : material;
1341
+
1342
+ }
1343
+
1344
+ async _surfaceMaterial( shape ) {
1345
+
1156
1346
  // Cache by (material, areaLight) object identity — many shapes share a
1157
1347
  // NamedMaterial, so this dedupes the build + texture-decode work. Nested
1158
1348
  // Map keys on the object refs directly (null is a valid key).
@@ -1166,11 +1356,7 @@ export class PBRTSceneBuilder {
1166
1356
 
1167
1357
  if ( byLight.has( shape.areaLight ) ) return byLight.get( shape.areaLight );
1168
1358
 
1169
- const ctx = {
1170
- resolveNamedTexture: ( n ) => this._resolveNamedTexture( n ),
1171
- namedMaterials: this.ir.namedMaterials,
1172
- warn: ( m ) => this.warn( m )
1173
- };
1359
+ const ctx = this._materialContext();
1174
1360
  const material = await buildMaterial( shape.material, ctx );
1175
1361
 
1176
1362
  if ( shape.areaLight ) await this._applyAreaLight( material, shape.areaLight, ctx );
@@ -1181,6 +1367,75 @@ export class PBRTSceneBuilder {
1181
1367
 
1182
1368
  }
1183
1369
 
1370
+ _materialContext() {
1371
+
1372
+ return this._ctx ??= {
1373
+ resolveNamedTexture: ( n ) => this._resolveNamedTexture( n ),
1374
+ namedMaterials: this.ir.namedMaterials,
1375
+ warn: ( m ) => this.warn( m ),
1376
+ floatTextureMean: async ( n ) => {
1377
+
1378
+ const term = await this._bakeTerm( n, 1 );
1379
+ return term ? ( term.layer ? layerMean( term.layer ) : term.rgb[ 0 ] ) : null;
1380
+
1381
+ },
1382
+ bakeMaterialMix: ( a, b, amountName ) => this._bakeMaterialMix( a, b, amountName ),
1383
+ resolveNormalMap: async ( filename ) => {
1384
+
1385
+ const image = await this._image( filename );
1386
+ if ( ! image ) return null;
1387
+ const texture = eightBit( image, { srgb: false } );
1388
+ const normalMap = texture === image ? image.clone() : texture;
1389
+ normalMap.colorSpace = NoColorSpace;
1390
+ return normalMap;
1391
+
1392
+ }
1393
+ };
1394
+
1395
+ }
1396
+
1397
+ /**
1398
+ * Glass filled with a homogeneous medium: what passes straight through it (σt = σa + σs) becomes Beer–Lambert
1399
+ * attenuation, since the engine traces no scattering inside a solid. A medium in an opaque shape changes nothing.
1400
+ * @private
1401
+ */
1402
+ async _inMedium( material, name ) {
1403
+
1404
+ if ( ! ( material.transmission > 0 ) ) return material;
1405
+ let byName = this._mediumMaterials.get( material );
1406
+ if ( ! byName ) this._mediumMaterials.set( material, byName = new Map() );
1407
+ if ( ! byName.has( name ) ) byName.set( name, this._attenuated( material, name ) );
1408
+ return byName.get( name );
1409
+
1410
+ }
1411
+
1412
+ async _attenuated( material, name ) {
1413
+
1414
+ const params = this.ir.media?.get( name );
1415
+ const type = params && pString( params, 'type', '' );
1416
+ if ( type !== 'homogeneous' ) {
1417
+
1418
+ this.warn( params ? `medium "${name}" (${type}) is not supported — "${name}" glass left clear` : `medium "${name}" not defined` );
1419
+ return material;
1420
+
1421
+ }
1422
+
1423
+ if ( params.preset ) this.warn( `medium preset "${pString( params, 'preset', '' )}" is not supported — using sigma_a / sigma_s` );
1424
+ const ctx = this._materialContext();
1425
+ const a = ( await resolveSpectrum( params, 'sigma_a', ctx, [ 1, 1, 1 ] ) ).rgb;
1426
+ const s = ( await resolveSpectrum( params, 'sigma_s', ctx, [ 1, 1, 1 ] ) ).rgb;
1427
+ const scale = pFloat( params, 'scale', 1 );
1428
+ const sigma = [ 0, 1, 2 ].map( ( c ) => ( a[ c ] + s[ c ] ) * scale );
1429
+ const max = Math.max( ...sigma );
1430
+ if ( ! ( max > 0 ) ) return material;
1431
+
1432
+ const variant = material.clone();
1433
+ variant.attenuationDistance = 1 / max;
1434
+ variant.attenuationColor.setRGB( ...sigma.map( ( v ) => Math.exp( - v / max ) ) );
1435
+ return this._dedupeMaterial( variant );
1436
+
1437
+ }
1438
+
1184
1439
  /**
1185
1440
  * Collapse materials that render identically onto one instance — an inline `Material` per
1186
1441
  * shape is a distinct object every time, so every batch would hold exactly one shape.
@@ -1192,7 +1447,9 @@ export class PBRTSceneBuilder {
1192
1447
  const key = `${c.r},${c.g},${c.b}|${e.r},${e.g},${e.b}|${material.emissiveIntensity}|` +
1193
1448
  `${material.map ? material.map.uuid : '-'}|${material.roughness}|${material.metalness}|` +
1194
1449
  `${material.transmission}|${material.ior}|${material.thickness}|` +
1195
- `${material.clearcoat}|${material.clearcoatRoughness}|${material.opacity}|${material.side}`;
1450
+ `${material.clearcoat}|${material.clearcoatRoughness}|${material.opacity}|${material.side}|` +
1451
+ `${material.normalMap ? material.normalMap.uuid : '-'}|${material.attenuationDistance}|${material.attenuationColor.toArray()}|` +
1452
+ `${material.specularIntensity}|${material.diffuseTransmission ?? 0}|${material.diffuseTransmissionColor?.toArray() ?? '-'}`;
1196
1453
 
1197
1454
  const existing = this._materialBySignature.get( key );
1198
1455
  if ( existing ) return existing;
@@ -1203,11 +1460,12 @@ export class PBRTSceneBuilder {
1203
1460
 
1204
1461
  async _applyAreaLight( material, areaLight, ctx ) {
1205
1462
 
1206
- const L = await resolveSpectrum( areaLight.params, 'L', ctx, [ 1, 1, 1 ] );
1463
+ const rgb = await lightRGB( areaLight.params, 'L', ctx );
1207
1464
  const scale = pFloat( areaLight.params, 'scale', 1 );
1208
- const rgb = L.rgb || [ 1, 1, 1 ];
1209
1465
  material.emissive.setRGB( rgb[ 0 ], rgb[ 1 ], rgb[ 2 ] );
1210
1466
  material.emissiveIntensity = scale;
1467
+ // pbrt's diffuse area light is one-sided unless "twosided" (_facingEmission turns the triangles).
1468
+ material.side = pBool( areaLight.params, 'twosided', false ) ? DoubleSide : FrontSide;
1211
1469
 
1212
1470
  }
1213
1471
 
@@ -1224,22 +1482,11 @@ export class PBRTSceneBuilder {
1224
1482
 
1225
1483
  } else if ( def.class === 'imagemap' ) {
1226
1484
 
1227
- const filename = pString( def.params, 'filename', null );
1228
- if ( filename ) {
1229
-
1230
- try {
1485
+ result = await this._imageMapTexture( name, def );
1231
1486
 
1232
- const tex = await this.resolveImage( filename );
1233
- if ( tex ) result = { texture: tex };
1234
- else this.warn( `image not found for texture "${name}": ${filename}` );
1487
+ } else if ( def.class === 'mix' ) {
1235
1488
 
1236
- } catch ( e ) {
1237
-
1238
- this.warn( `failed to load texture "${name}" (${filename}): ${e.message}` );
1239
-
1240
- }
1241
-
1242
- }
1489
+ result = await this._mixTexture( name, def );
1243
1490
 
1244
1491
  } else if ( def.class === 'constant' ) {
1245
1492
 
@@ -1276,6 +1523,176 @@ export class PBRTSceneBuilder {
1276
1523
 
1277
1524
  }
1278
1525
 
1526
+ /** A file's image, decoded once however many named textures read it. @private */
1527
+ _image( filename ) {
1528
+
1529
+ let pending = this._imageCache.get( filename );
1530
+ if ( ! pending ) {
1531
+
1532
+ pending = Promise.resolve().then( () => this.resolveImage( filename ) ).catch( ( e ) => {
1533
+
1534
+ this.warn( `failed to load image ${filename}: ${e.message}` );
1535
+ return null;
1536
+
1537
+ } );
1538
+ this._imageCache.set( filename, pending );
1539
+
1540
+ }
1541
+
1542
+ return pending;
1543
+
1544
+ }
1545
+
1546
+ /**
1547
+ * An image texture with pbrt's uv mapping; its `scale` becomes the material's tint, except that a scale above 1 or
1548
+ * `invert` is baked (pbrt clamps an albedo per texel, which a tint cannot).
1549
+ * @private
1550
+ */
1551
+ async _imageMapTexture( name, def ) {
1552
+
1553
+ const filename = pString( def.params, 'filename', null );
1554
+ if ( ! filename ) return null;
1555
+ const image = await this._image( filename );
1556
+ if ( ! image ) {
1557
+
1558
+ this.warn( `image not found for texture "${name}": ${filename}` );
1559
+ return null;
1560
+
1561
+ }
1562
+
1563
+ const mapping = pString( def.params, 'mapping', 'uv' );
1564
+ if ( mapping !== 'uv' ) this.warn( `texture "${name}": "${mapping}" mapping is not supported — using the mesh's uv` );
1565
+
1566
+ const scale = pFloat( def.params, 'scale', 1 );
1567
+ if ( scale > 1 || pBool( def.params, 'invert', false ) ) {
1568
+
1569
+ const term = await this._bakeTerm( name, 3 );
1570
+ const baked = term?.layer && bake( [ { ...term, clamp: true } ], ( [ v ] ) => v );
1571
+ if ( baked ) return { texture: bakedTexture( baked ) };
1572
+ this.warn( `texture "${name}": its scale or invert could not be baked here — ignored` );
1573
+
1574
+ }
1575
+
1576
+ const color = eightBit( image, { srgb: true } );
1577
+ const texture = color === image ? image.clone() : color;
1578
+ setMapping( texture, uvMapping( def.params ) );
1579
+ texture.needsUpdate = true;
1580
+ return scale < 1 ? { texture, constant: [ scale, scale, scale ] } : { texture };
1581
+
1582
+ }
1583
+
1584
+ /** pbrt's `mix` texture, ( 1 − amount ) · tex1 + amount · tex2, baked when any of the three is an image. @private */
1585
+ async _mixTexture( name, def ) {
1586
+
1587
+ const [ a, b, amount ] = await Promise.all( [
1588
+ this._paramTerm( def.params, 'tex1', 3, [ 0, 0, 0 ] ),
1589
+ this._paramTerm( def.params, 'tex2', 3, [ 1, 1, 1 ] ),
1590
+ this._paramTerm( def.params, 'amount', 1, [ 0.5, 0.5, 0.5 ] )
1591
+ ] );
1592
+ if ( ! a || ! b || ! amount ) {
1593
+
1594
+ this.warn( `texture "${name}": a "mix" input could not be read here` );
1595
+ return null;
1596
+
1597
+ }
1598
+
1599
+ const unit = ( rgb ) => rgb.map( ( v ) => Math.min( 1, Math.max( 0, v ) ) );
1600
+ const baked = bake( [ { ...a, clamp: true }, { ...b, clamp: true }, amount ], mixOf );
1601
+ return baked ? { texture: bakedTexture( baked ) } : { constant: mixOf( [ unit( a.rgb ), unit( b.rgb ), amount.rgb ] ) };
1602
+
1603
+ }
1604
+
1605
+ /** A texture parameter as a bake term: a constant, or the texture it names. @private */
1606
+ async _paramTerm( params, key, channels, dflt ) {
1607
+
1608
+ const p = params[ key ];
1609
+ if ( ! p ) return { rgb: dflt };
1610
+ if ( p.type === 'texture' ) return this._bakeTerm( p.value[ 0 ], channels );
1611
+ const rgb = ( await resolveSpectrum( params, key, this._materialContext(), dflt ) ).rgb;
1612
+ return rgb ? { rgb } : null;
1613
+
1614
+ }
1615
+
1616
+ /**
1617
+ * A named texture as pbrt evaluates it, for baking: `{ rgb }` for a constant, `{ layer, tint? }` for an image;
1618
+ * null when it cannot be read in this runtime.
1619
+ * @param {string} name
1620
+ * @param {number} channels - 1 where it is read as a float texture (its red channel), 3 as a spectrum
1621
+ * @private
1622
+ */
1623
+ _bakeTerm( name, channels ) {
1624
+
1625
+ const key = `${name}\0${channels}`;
1626
+ let pending = this._termCache.get( key );
1627
+ if ( ! pending ) this._termCache.set( key, pending = this._createBakeTerm( name, channels ) );
1628
+ return pending;
1629
+
1630
+ }
1631
+
1632
+ async _createBakeTerm( name, channels ) {
1633
+
1634
+ const def = this.ir.namedTextures.get( name );
1635
+ if ( def?.class === 'imagemap' ) {
1636
+
1637
+ if ( pString( def.params, 'mapping', 'uv' ) !== 'uv' ) return null;
1638
+ const filename = pString( def.params, 'filename', null );
1639
+ const pixels = texturePixels( filename && await this._image( filename ) );
1640
+ if ( ! pixels ) return null;
1641
+ return { layer: makeLayer( pixels, {
1642
+ channels, decode: pixels.float ? null : decoderFor( def.params ),
1643
+ scale: pFloat( def.params, 'scale', 1 ), invert: pBool( def.params, 'invert', false ), mapping: uvMapping( def.params )
1644
+ } ) };
1645
+
1646
+ }
1647
+
1648
+ const resolved = await this._resolveNamedTexture( name );
1649
+ if ( ! resolved ) return null;
1650
+ if ( ! resolved.texture ) return resolved.constant ? { rgb: resolved.constant } : null;
1651
+ const layer = this._textureLayer( resolved.texture, channels );
1652
+ return layer ? { layer, tint: resolved.constant ?? null } : null;
1653
+
1654
+ }
1655
+
1656
+ // A texture already built for a material (an image or a bake) as a layer, in the mapping it carries.
1657
+ _textureLayer( texture, channels ) {
1658
+
1659
+ const pixels = texturePixels( texture );
1660
+ if ( ! pixels ) return null;
1661
+ const srgb = ! pixels.float && texture.colorSpace === SRGBColorSpace;
1662
+ return makeLayer( pixels, {
1663
+ channels, decode: pixels.float ? null : ( srgb ? srgbToLinear : ( v ) => v ),
1664
+ mapping: { su: texture.repeat.x, sv: texture.repeat.y, du: texture.offset.x, dv: texture.offset.y }
1665
+ } );
1666
+
1667
+ }
1668
+
1669
+ /**
1670
+ * A mix material whose amount is a texture: its two materials' colours (map × colour) baked by that texture, and
1671
+ * the texture's mean to weigh everything else. Null when nothing here can read it.
1672
+ * @private
1673
+ */
1674
+ async _bakeMaterialMix( matA, matB, amountName ) {
1675
+
1676
+ const amount = await this._bakeTerm( amountName, 1 );
1677
+ if ( ! amount ) return null;
1678
+ if ( ! amount.layer ) return { mean: amount.rgb[ 0 ], texture: null };
1679
+
1680
+ const mean = layerMean( amount.layer );
1681
+ const term = ( m ) => {
1682
+
1683
+ if ( ! m.map ) return { rgb: m.color.toArray(), clamp: true };
1684
+ const layer = this._textureLayer( m.map, 3 );
1685
+ return layer && { layer, tint: m.color.toArray(), clamp: true };
1686
+
1687
+ };
1688
+
1689
+ const a = term( matA ), b = term( matB );
1690
+ if ( ! a || ! b ) return { mean, texture: null };
1691
+ const baked = bake( [ a, b, amount ], mixOf );
1692
+ return { mean, texture: baked ? bakedTexture( baked ) : null };
1693
+
1694
+ }
1695
+
1279
1696
  _colorFromLikeNamedMaterial( texName ) {
1280
1697
 
1281
1698
  for ( const candidate of materialNameCandidates( texName ) ) {
@@ -1297,6 +1714,25 @@ export class PBRTSceneBuilder {
1297
1714
  async _resolveScaleTexture( name, def ) {
1298
1715
 
1299
1716
  // pbrt-v4 uses "tex" (the inner texture or constant) and "scale" (the multiplier).
1717
+ if ( def.params.scale?.type === 'texture' ) {
1718
+
1719
+ // A texture multiplier (kroken's bricks: colour × dirt) is baked.
1720
+ const [ inner, factor ] = await Promise.all( [
1721
+ this._paramTerm( def.params, 'tex', 3, [ 1, 1, 1 ] ),
1722
+ this._paramTerm( def.params, 'scale', 1, [ 1, 1, 1 ] )
1723
+ ] );
1724
+ const product = ( [ a, f ] ) => [ a[ 0 ] * f[ 0 ], a[ 1 ] * f[ 0 ], a[ 2 ] * f[ 0 ] ];
1725
+ if ( inner && factor ) {
1726
+
1727
+ const baked = bake( [ { ...inner, clamp: true }, factor ], product );
1728
+ return baked ? { texture: bakedTexture( baked ) } : { constant: product( [ inner.rgb, factor.rgb ] ) };
1729
+
1730
+ }
1731
+
1732
+ this.warn( `texture "${name}": its "scale" texture could not be read here — ignored` );
1733
+
1734
+ }
1735
+
1300
1736
  const innerP = def.params.tex;
1301
1737
  let inner = null;
1302
1738
 
@@ -1500,8 +1936,7 @@ export class PBRTSceneBuilder {
1500
1936
 
1501
1937
  // Constant-radiance infinite light → tiny float texture (CDF-buildable).
1502
1938
  const ctx = { resolveNamedTexture: async () => null, warn: ( m ) => this.warn( m ) };
1503
- const L = await resolveSpectrum( inf.params, 'L', ctx, [ 1, 1, 1 ] );
1504
- const rgb = ( L.rgb || [ 1, 1, 1 ] ).map( v => v * scale );
1939
+ const rgb = ( await lightRGB( inf.params, 'L', ctx ) ).map( v => v * scale );
1505
1940
 
1506
1941
  const w = 2, h = 1;
1507
1942
  const data = new Float32Array( w * h * 4 );
@@ -1523,13 +1958,89 @@ export class PBRTSceneBuilder {
1523
1958
 
1524
1959
  }
1525
1960
 
1961
+ /**
1962
+ * `distant`, `point` and `spot` lights as three.js lamps, in the engine's units: a directional lamp's
1963
+ * intensity is the irradiance it gives (pbrt's L), a point or spot lamp's is 4π × its radiant intensity.
1964
+ */
1965
+ async _buildLamps( lights, group ) {
1966
+
1967
+ const ctx = { resolveNamedTexture: async () => null, warn: ( m ) => this.warn( m ) };
1968
+ for ( const l of lights ) {
1969
+
1970
+ if ( ! LAMP_TYPES.has( l.type ) ) continue;
1971
+
1972
+ const world = new Matrix4().fromArray( this.convertHandedness ? M.multiply( FLIP_Z, l.ctm ) : l.ctm );
1973
+ const point = ( name, dflt ) => {
1974
+
1975
+ const v = l.params[ name ]?.value;
1976
+ return new Vector3().fromArray( v?.length >= 3 ? v : dflt ).applyMatrix4( world );
1977
+
1978
+ };
1979
+
1980
+ const rgb = await lightRGB( l.params, l.type === 'distant' ? 'L' : 'I', ctx );
1981
+ let scale = pFloat( l.params, 'scale', 1 );
1982
+ const from = point( 'from', [ 0, 0, 0 ] );
1983
+ const to = point( 'to', [ 0, 0, 1 ] );
1984
+ let light;
1985
+ if ( l.type === 'distant' ) {
1986
+
1987
+ const illuminance = pFloat( l.params, 'illuminance', - 1 );
1988
+ if ( illuminance > 0 ) scale *= illuminance;
1989
+ light = new DirectionalLight();
1990
+ light.userData.__luxConverted = true;
1991
+
1992
+ } else if ( l.type === 'point' ) {
1993
+
1994
+ // `power` sets the radiant intensity to power / 4π.
1995
+ const power = pFloat( l.params, 'power', - 1 );
1996
+ scale *= power > 0 ? power : 4 * Math.PI;
1997
+ light = new PointLight();
1998
+ light.userData.__candelaConverted = true;
1999
+
2000
+ } else {
2001
+
2002
+ // pbrt's falloff is smoothstep( cos cone, cos( cone − delta ), cos θ ); the engine's blend is that width over 1 − cos cone.
2003
+ const cone = Math.min( Math.max( pFloat( l.params, 'coneangle', 30 ), 0 ), 90 ) * Math.PI / 180;
2004
+ const delta = Math.min( Math.max( pFloat( l.params, 'conedeltaangle', 5 ) * Math.PI / 180, 0 ), cone );
2005
+ const cosEnd = Math.cos( cone ), cosStart = Math.cos( cone - delta );
2006
+ const power = pFloat( l.params, 'power', - 1 );
2007
+ if ( power > 0 ) scale *= power / ( 2 * Math.PI * ( ( 1 - cosStart ) + ( cosStart - cosEnd ) / 2 ) );
2008
+ scale *= 4 * Math.PI;
2009
+ light = new SpotLight();
2010
+ light.angle = cone;
2011
+ light.penumbra = cosEnd < 1 ? Math.min( ( cosStart - cosEnd ) / ( 1 - cosEnd ), 1 ) : 0;
2012
+ light.userData.__candelaConverted = true;
2013
+
2014
+ }
2015
+
2016
+ const peak = Math.max( rgb[ 0 ], rgb[ 1 ], rgb[ 2 ] );
2017
+ if ( ! ( peak > 0 ) || ! ( scale > 0 ) ) continue;
2018
+ light.color.setRGB( rgb[ 0 ] / peak, rgb[ 1 ] / peak, rgb[ 2 ] / peak );
2019
+ light.intensity = peak * scale;
2020
+ light.name = `pbrt ${l.type}`;
2021
+ light.position.copy( from );
2022
+ light.decay = 2;
2023
+ light.distance = 0;
2024
+ if ( light.target ) {
2025
+
2026
+ light.target.position.subVectors( to, from );
2027
+ light.add( light.target );
2028
+
2029
+ }
2030
+
2031
+ group.add( light );
2032
+
2033
+ }
2034
+
2035
+ }
2036
+
1526
2037
  _reportUnsupportedLights( lights ) {
1527
2038
 
1528
2039
  for ( const l of lights ) {
1529
2040
 
1530
- if ( l.type !== 'infinite' ) {
2041
+ if ( l.type !== 'infinite' && ! LAMP_TYPES.has( l.type ) ) {
1531
2042
 
1532
- this.warn( `light "${l.type}" not supported (only infinite lights and emissive area lights are mapped)` );
2043
+ this.warn( `light "${l.type}" not supported (only infinite, distant, point and spot lights and emissive area lights are mapped)` );
1533
2044
 
1534
2045
  }
1535
2046