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
@@ -17,17 +17,33 @@
17
17
  import { freeNow, resized } from './buffers.js';
18
18
  import {
19
19
  Group, Mesh, InstancedMesh, PerspectiveCamera, OrthographicCamera, Matrix4, Vector3, Quaternion,
20
- BufferGeometry, Float32BufferAttribute, Uint32BufferAttribute, SphereGeometry,
20
+ BufferGeometry, Float32BufferAttribute, Uint32BufferAttribute,
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, hasAlpha } 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 '../HostMemory.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 = 9;
99
111
 
100
112
  function samePlacements( a, b ) {
101
113
 
@@ -135,6 +147,357 @@ 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: bytes bottom row first, sRGB unless a data map, in the uv mapping it was baked in.
248
+ function bakedTexture( { data, width, height, mapping }, { srgb = true } = {} ) {
249
+
250
+ const texture = new DataTexture( data, width, height, RGBAFormat );
251
+ texture.colorSpace = srgb ? SRGBColorSpace : NoColorSpace;
252
+ texture.wrapS = texture.wrapT = RepeatWrapping;
253
+ setMapping( texture, mapping );
254
+ texture.needsUpdate = true;
255
+ return texture;
256
+
257
+ }
258
+
259
+ /**
260
+ * pbrt's thin lens as the camera's own effects (CameraManager applies them when the camera is chosen): the same
261
+ * aperture radius in either DOF mode — `dofBlur` for 'look', a full-frame lens and its f-number for 'physical' at one
262
+ * scene unit a metre — focused by hand at pbrt's distance.
263
+ */
264
+ function lensEffects( camera, radius, focusDistance ) {
265
+
266
+ const ortho = camera.isOrthographicCamera === true;
267
+ const tanHalf = ortho ? 1 : Math.tan( camera.fov * Math.PI / 360 );
268
+ const focalLength = ortho ? 50 : 12 / tanHalf;
269
+ return {
270
+ enableDOF: true,
271
+ focusDistance,
272
+ aperture: focalLength * 0.001 / ( 2 * radius ),
273
+ focalLength,
274
+ apertureScale: 1,
275
+ anamorphicRatio: 1,
276
+ dofBlur: radius / ( focusDistance * tanHalf ),
277
+ autoFocusMode: 'manual',
278
+ afScreenPoint: { x: 0.5, y: 0.5 },
279
+ orthoHeight: ortho ? ( camera.top - camera.bottom ) / camera.zoom : null,
280
+ };
281
+
282
+ }
283
+
284
+ // What the scene asks the renderer for, where it says: the Integrator's maxdepth, the Sampler's pixelsamples, the Film's
285
+ // resolution — scene metadata's `render`.
286
+ function renderRequest( ir ) {
287
+
288
+ const render = {};
289
+ if ( ir.integrator?.maxdepth > 0 ) render.maxBounces = Math.round( ir.integrator.maxdepth );
290
+ if ( ir.sampler?.pixelsamples > 0 ) render.samples = Math.round( ir.sampler.pixelsamples );
291
+ if ( ir.film?.resolutionGiven ) {
292
+
293
+ render.width = ir.film.xresolution;
294
+ render.height = ir.film.yresolution;
295
+
296
+ }
297
+
298
+ return Object.keys( render ).length ? render : null;
299
+
300
+ }
301
+
302
+ const QUADRIC_SEGMENTS = 48; // around a whole turn
303
+
304
+ // pbrt's quadric parameters, clamped as pbrt clamps them (shapes.h).
305
+ function sphereParams( params ) {
306
+
307
+ const radius = pFloat( params, 'radius', 1 );
308
+ const z0 = pFloat( params, 'zmin', - radius ), z1 = pFloat( params, 'zmax', radius );
309
+ const clamp = ( z ) => Math.min( radius, Math.max( - radius, z ) );
310
+ return { radius, zMin: clamp( Math.min( z0, z1 ) ), zMax: clamp( Math.max( z0, z1 ) ), phiMax: phiMaxOf( params ) };
311
+
312
+ }
313
+
314
+ function cylinderParams( params ) {
315
+
316
+ const z0 = pFloat( params, 'zmin', - 1 ), z1 = pFloat( params, 'zmax', 1 );
317
+ return { radius: pFloat( params, 'radius', 1 ), zMin: Math.min( z0, z1 ), zMax: Math.max( z0, z1 ), phiMax: phiMaxOf( params ) };
318
+
319
+ }
320
+
321
+ function diskParams( params ) {
322
+
323
+ return {
324
+ radius: pFloat( params, 'radius', 1 ), inner: pFloat( params, 'innerradius', 0 ),
325
+ height: pFloat( params, 'height', 0 ), phiMax: phiMaxOf( params )
326
+ };
327
+
328
+ }
329
+
330
+ const phiMaxOf = ( params ) => Math.min( 360, Math.max( 0, pFloat( params, 'phimax', 360 ) ) ) * Math.PI / 180;
331
+
332
+ /**
333
+ * A quadric as pbrt parameterises it: `at( u, v, p, n )` over [0, 1]², u around the z axis to `phiMax`; the uv is
334
+ * pbrt's own. Triangles face ∂p/∂u × ∂p/∂v, pbrt's normal; a cell edge of no length (a pole, a centre) drops its
335
+ * triangle.
336
+ */
337
+ function quadric( phiMax, vSegments, at ) {
338
+
339
+ const uSegments = Math.max( 3, Math.ceil( QUADRIC_SEGMENTS * phiMax / ( 2 * Math.PI ) ) );
340
+ const row = uSegments + 1;
341
+ const position = new Float32Array( row * ( vSegments + 1 ) * 3 );
342
+ const normal = new Float32Array( position.length );
343
+ const uv = new Float32Array( row * ( vSegments + 1 ) * 2 );
344
+ const p = new Vector3(), n = new Vector3();
345
+
346
+ for ( let j = 0; j <= vSegments; j ++ ) for ( let i = 0; i <= uSegments; i ++ ) {
347
+
348
+ const k = j * row + i;
349
+ at( i / uSegments, j / vSegments, p, n );
350
+ p.toArray( position, k * 3 );
351
+ n.toArray( normal, k * 3 );
352
+ uv[ k * 2 ] = i / uSegments;
353
+ uv[ k * 2 + 1 ] = j / vSegments;
354
+
355
+ }
356
+
357
+ let extent = 0;
358
+ for ( let k = 0; k < position.length; k ++ ) extent = Math.max( extent, Math.abs( position[ k ] ) );
359
+ const apart = ( a, b ) => Math.hypot( position[ a * 3 ] - position[ b * 3 ], position[ a * 3 + 1 ] - position[ b * 3 + 1 ], position[ a * 3 + 2 ] - position[ b * 3 + 2 ] ) > extent * 1e-6;
360
+ const index = [];
361
+ for ( let j = 0; j < vSegments; j ++ ) for ( let i = 0; i < uSegments; i ++ ) {
362
+
363
+ const a = j * row + i, b = a + 1, c = a + row, d = c + 1;
364
+ if ( apart( a, b ) ) index.push( a, b, d );
365
+ if ( apart( c, d ) ) index.push( a, d, c );
366
+
367
+ }
368
+
369
+ const geometry = new BufferGeometry();
370
+ geometry.setAttribute( 'position', new Float32BufferAttribute( position, 3 ) );
371
+ geometry.setAttribute( 'normal', new Float32BufferAttribute( normal, 3 ) );
372
+ geometry.setAttribute( 'uv', new Float32BufferAttribute( uv, 2 ) );
373
+ geometry.setIndex( new Uint32BufferAttribute( new Uint32Array( index ), 1 ) );
374
+ return geometry;
375
+
376
+ }
377
+
378
+ // What pbrt calls a shape's area when it scales an area light's `power`: a quadric's in its own space, a mesh's in the
379
+ // scene's (pbrt keeps triangles transformed).
380
+ function shapeArea( shape, geometry ) {
381
+
382
+ const params = shape.params;
383
+ switch ( shape.type ) {
384
+
385
+ case 'sphere': {
386
+
387
+ const { radius, zMin, zMax, phiMax } = sphereParams( params );
388
+ return phiMax * radius * ( zMax - zMin );
389
+
390
+ }
391
+
392
+ case 'cylinder': {
393
+
394
+ const { radius, zMin, zMax, phiMax } = cylinderParams( params );
395
+ return phiMax * radius * ( zMax - zMin );
396
+
397
+ }
398
+
399
+ case 'disk': {
400
+
401
+ const { radius, inner, phiMax } = diskParams( params );
402
+ return phiMax * 0.5 * ( radius * radius - inner * inner );
403
+
404
+ }
405
+
406
+ }
407
+
408
+ const world = new Matrix4().fromArray( shape.ctm );
409
+ const position = geometry.getAttribute( 'position' );
410
+ const index = geometry.index;
411
+ const count = index ? index.count : position.count;
412
+ const a = new Vector3(), b = new Vector3(), c = new Vector3();
413
+ let area = 0;
414
+ for ( let t = 0; t + 2 < count; t += 3 ) {
415
+
416
+ a.fromBufferAttribute( position, index ? index.getX( t ) : t ).applyMatrix4( world );
417
+ b.fromBufferAttribute( position, index ? index.getX( t + 1 ) : t + 1 ).applyMatrix4( world );
418
+ c.fromBufferAttribute( position, index ? index.getX( t + 2 ) : t + 2 ).applyMatrix4( world );
419
+ area += b.sub( a ).cross( c.sub( a ) ).length() / 2;
420
+
421
+ }
422
+
423
+ return area;
424
+
425
+ }
426
+
427
+ const CHECK_TEXELS = 64; // a baked checkerboard's texels along one check
428
+ const BILERP_TEXELS = 64;
429
+
430
+ // `colorAt( s, t )` (linear RGB) over a w × h grid as sRGB bytes, bottom row first, clamped as pbrt clamps an albedo.
431
+ function rasterize( width, height, colorAt ) {
432
+
433
+ const data = new Uint8Array( width * height * 4 );
434
+ for ( let j = 0; j < height; j ++ ) for ( let i = 0; i < width; i ++ ) {
435
+
436
+ const rgb = colorAt( ( i + 0.5 ) / width, ( j + 0.5 ) / height );
437
+ const o = ( j * width + i ) * 4;
438
+ for ( let c = 0; c < 3; c ++ ) data[ o + c ] = Math.round( 255 * linearToSRGB( Math.min( 1, Math.max( 0, rgb[ c ] ) ) ) );
439
+ data[ o + 3 ] = 255;
440
+
441
+ }
442
+
443
+ return data;
444
+
445
+ }
446
+
447
+ // A clone that keeps the engine's own material properties, which three.js's copy() does not know.
448
+ function cloneMaterial( material ) {
449
+
450
+ const out = material.clone();
451
+ for ( const key of Object.keys( material ) ) if ( ! ( key in out ) ) out[ key ] = material[ key ];
452
+ return out;
453
+
454
+ }
455
+
456
+ // pbrt's shape alpha as the engine's blend mode: a ray passes with chance 1 − α, shadow rays too.
457
+ function translucent( material, alpha ) {
458
+
459
+ const out = cloneMaterial( material );
460
+ out.transparent = true;
461
+ out.opacity = material.opacity * alpha;
462
+ return out;
463
+
464
+ }
465
+
466
+ const sameMapping = ( texture, { su, sv, du, dv } ) =>
467
+ texture.repeat.x === su && texture.repeat.y === sv && texture.offset.x === du && texture.offset.y === dv;
468
+
469
+ // An 8-bit image's colour with its alpha decoded as pbrt reads it; null when its alpha is opaque throughout.
470
+ function withDecodedAlpha( pixels, decode ) {
471
+
472
+ if ( pixels.float || ! hasAlpha( pixels ) ) return null;
473
+ const { data, width, height } = pixels;
474
+ const lut = new Uint8Array( 256 );
475
+ for ( let v = 0; v < 256; v ++ ) lut[ v ] = Math.round( 255 * Math.min( 1, Math.max( 0, decode( v / 255 ) ) ) );
476
+
477
+ const out = new Uint8Array( width * height * 4 );
478
+ const rowBytes = width * 4;
479
+ for ( let y = 0; y < height; y ++ ) {
480
+
481
+ const src = ( pixels.topDown ? height - 1 - y : y ) * rowBytes, dst = y * rowBytes;
482
+ for ( let x = 0; x < rowBytes; x += 4 ) {
483
+
484
+ out[ dst + x ] = data[ src + x ];
485
+ out[ dst + x + 1 ] = data[ src + x + 1 ];
486
+ out[ dst + x + 2 ] = data[ src + x + 2 ];
487
+ out[ dst + x + 3 ] = lut[ data[ src + x + 3 ] ];
488
+
489
+ }
490
+
491
+ }
492
+
493
+ const texture = new DataTexture( out, width, height, RGBAFormat );
494
+ texture.colorSpace = SRGBColorSpace;
495
+ texture.wrapS = texture.wrapT = RepeatWrapping;
496
+ texture.needsUpdate = true;
497
+ return texture;
498
+
499
+ }
500
+
138
501
  /**
139
502
  * Names an unreadable texture may have been derived from. Moana binds colour as
140
503
  * `"texture reflectance" "el:part_Color"` and leaves `MakeNamedMaterial "el:part"` in the
@@ -192,6 +555,10 @@ export class PBRTSceneBuilder {
192
555
  this._materialBySignature = new Map(); // visual signature -> shared MeshPhysicalMaterial
193
556
  this._noUVMaterials = new Map(); // textured material -> its map-less clone
194
557
  this._plyCache = new Map(); // filename -> Promise<BufferGeometry|null>
558
+ this._imageCache = new Map(); // filename -> Promise<Texture|null>, decoded once for every named texture
559
+ this._termCache = new Map(); // texture name + channels -> Promise<{rgb}|{layer, tint?}|null>
560
+ this._mediumMaterials = new Map(); // material -> Map(medium name -> material with its attenuation)
561
+ this._alphaMaterials = new Map(); // material -> Map(alpha texture name or #value -> Promise<material>)
195
562
 
196
563
  }
197
564
 
@@ -249,11 +616,12 @@ export class PBRTSceneBuilder {
249
616
  }
250
617
 
251
618
  const shape = ir.shapes[ i ];
252
- const [ geometry, sharedMaterial ] = await Promise.all( [
619
+ const [ geometry, surface ] = await Promise.all( [
253
620
  this._batches ? this._createGeometry( shape ) : this._buildGeometry( shape ),
254
621
  this._getMaterial( shape )
255
622
  ] );
256
623
  if ( ! geometry ) continue;
624
+ const sharedMaterial = shape.areaLight ? this._poweredMaterial( shape, geometry, surface ) : surface;
257
625
 
258
626
  // A moving shape needs a node of its own to move.
259
627
  if ( this._batches && ! shape.motion && geometry.getAttribute( 'position' ).count <= MERGE_VERTEX_LIMIT ) {
@@ -308,6 +676,7 @@ export class PBRTSceneBuilder {
308
676
 
309
677
  // Infinite light → environment
310
678
  const environment = await this._buildEnvironment( ir.lights );
679
+ await this._buildLamps( ir.lights, group );
311
680
 
312
681
  this._reportUnsupportedLights( ir.lights );
313
682
 
@@ -317,6 +686,7 @@ export class PBRTSceneBuilder {
317
686
 
318
687
  return {
319
688
  group, camera, environment, animations,
689
+ render: renderRequest( ir ),
320
690
  report: this.report,
321
691
  meshCount: this.reportedMeshes,
322
692
  triangleCount: this.triangleCount,
@@ -740,7 +1110,7 @@ export class PBRTSceneBuilder {
740
1110
  if ( ! material ) {
741
1111
 
742
1112
  this.warn( `${name} (${shape.type}, "${shape.material?.type || 'diffuse'}") has a texture map but no UVs — dropping map, using base color` );
743
- material = sharedMaterial.clone();
1113
+ material = cloneMaterial( sharedMaterial );
744
1114
  material.map = null;
745
1115
  this._noUVMaterials.set( sharedMaterial, material );
746
1116
 
@@ -933,6 +1303,58 @@ export class PBRTSceneBuilder {
933
1303
 
934
1304
  async _createGeometry( shape ) {
935
1305
 
1306
+ const geometry = await this._shapeGeometry( shape );
1307
+ return geometry && shape.areaLight && ! pBool( shape.areaLight.params, 'twosided', false ) ? this._facingEmission( shape, geometry ) : geometry;
1308
+
1309
+ }
1310
+
1311
+ /**
1312
+ * A one-sided area light emits on the side pbrt's surface normal faces: towards the vertex normals where the shape
1313
+ * has them, else its winding's side, turned over by ReverseOrientation. The engine emits from a triangle's front,
1314
+ * so a triangle facing the other way is rewound, on an index of its own: the geometry may be shared.
1315
+ * @private
1316
+ */
1317
+ _facingEmission( shape, geometry ) {
1318
+
1319
+ const normal = geometry.userData.pbrtNormals ? geometry.getAttribute( 'normal' ) : null;
1320
+ if ( ! normal && ! shape.reverseOrientation ) return geometry;
1321
+
1322
+ const p = geometry.getAttribute( 'position' ).array, n = normal?.array;
1323
+ const src = geometry.index?.array ?? null;
1324
+ const count = src ? src.length : geometry.getAttribute( 'position' ).count;
1325
+ const index = new Uint32Array( count );
1326
+ let flipped = 0;
1327
+
1328
+ for ( let t = 0; t + 2 < count; t += 3 ) {
1329
+
1330
+ const a = src ? src[ t ] : t, b = src ? src[ t + 1 ] : t + 1, c = src ? src[ t + 2 ] : t + 2;
1331
+ let flip = shape.reverseOrientation;
1332
+ if ( n ) {
1333
+
1334
+ 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 ];
1335
+ 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 ];
1336
+ 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 ];
1337
+ flip = ( uy * vz - uz * vy ) * sx + ( uz * vx - ux * vz ) * sy + ( ux * vy - uy * vx ) * sz < 0;
1338
+
1339
+ }
1340
+
1341
+ index[ t ] = a;
1342
+ index[ t + 1 ] = flip ? c : b;
1343
+ index[ t + 2 ] = flip ? b : c;
1344
+ if ( flip ) flipped ++;
1345
+
1346
+ }
1347
+
1348
+ if ( flipped === 0 ) return geometry;
1349
+ const oriented = new BufferGeometry();
1350
+ for ( const name in geometry.attributes ) oriented.setAttribute( name, geometry.getAttribute( name ) );
1351
+ oriented.setIndex( new Uint32BufferAttribute( index, 1 ) );
1352
+ return oriented;
1353
+
1354
+ }
1355
+
1356
+ _shapeGeometry( shape ) {
1357
+
936
1358
  switch ( shape.type ) {
937
1359
 
938
1360
  case 'trianglemesh': return this._triangleMesh( shape.params );
@@ -942,6 +1364,7 @@ export class PBRTSceneBuilder {
942
1364
  case 'plymesh': return this._plyMesh( shape.params );
943
1365
  case 'sphere': return this._sphere( shape.params );
944
1366
  case 'disk': return this._disk( shape.params );
1367
+ case 'cylinder': return this._cylinder( shape.params );
945
1368
  default:
946
1369
  this.warn( `shape "${shape.type}" not supported — skipped` );
947
1370
  return null;
@@ -963,7 +1386,12 @@ export class PBRTSceneBuilder {
963
1386
  geo.setAttribute( 'position', new Float32BufferAttribute( float32( P ), 3 ) );
964
1387
 
965
1388
  const N = params.N?.value;
966
- if ( N && N.length === P.length ) geo.setAttribute( 'normal', new Float32BufferAttribute( float32( N ), 3 ) );
1389
+ if ( N && N.length === P.length ) {
1390
+
1391
+ geo.setAttribute( 'normal', new Float32BufferAttribute( float32( N ), 3 ) );
1392
+ geo.userData.pbrtNormals = true;
1393
+
1394
+ }
967
1395
 
968
1396
  const uv = ( params.uv || params.st )?.value;
969
1397
  if ( uv && uv.length === ( P.length / 3 ) * 2 ) geo.setAttribute( 'uv', new Float32BufferAttribute( float32( uv ), 2 ) );
@@ -1047,28 +1475,75 @@ export class PBRTSceneBuilder {
1047
1475
  }
1048
1476
 
1049
1477
  // Bilinear patch mesh → triangulate each quad (P + indices in quads of 4).
1478
+ /**
1479
+ * pbrt's bilinear patches as two triangles each: corners p00, p10, p01, p11 (not a ring), facing ∂p/∂u × ∂p/∂v. One
1480
+ * patch of four points needs no indices. Without uvs a patch takes its own (u, v), so its corners are not shared.
1481
+ */
1050
1482
  _bilinearMesh( params ) {
1051
1483
 
1052
1484
  const P = params.P?.value;
1053
- const quad = params.indices?.value;
1485
+ const quad = params.indices?.value ?? ( P?.length === 12 ? [ 0, 1, 2, 3 ] : null );
1054
1486
  if ( ! P || ! quad ) {
1055
1487
 
1056
1488
  this.warn( 'bilinearmesh missing P/indices' ); return null;
1057
1489
 
1058
1490
  }
1059
1491
 
1060
- const tris = [];
1061
- for ( let i = 0; i + 3 < quad.length; i += 4 ) {
1492
+ const count = P.length / 3;
1493
+ const uv = params.uv?.value?.length === count * 2 ? params.uv.value : null;
1494
+ const N = params.N?.value?.length === count * 3 ? params.N.value : null;
1495
+ const geo = new BufferGeometry();
1496
+
1497
+ if ( uv ) {
1498
+
1499
+ const index = [];
1500
+ for ( let i = 0; i + 3 < quad.length; i += 4 ) {
1501
+
1502
+ const [ a, b, c, d ] = [ quad[ i ], quad[ i + 1 ], quad[ i + 2 ], quad[ i + 3 ] ];
1503
+ index.push( a, b, d, a, d, c );
1504
+
1505
+ }
1506
+
1507
+ geo.setAttribute( 'position', new Float32BufferAttribute( Float32Array.from( P ), 3 ) );
1508
+ geo.setAttribute( 'uv', new Float32BufferAttribute( Float32Array.from( uv ), 2 ) );
1509
+ if ( N ) geo.setAttribute( 'normal', new Float32BufferAttribute( Float32Array.from( N ), 3 ) );
1510
+ geo.setIndex( new Uint32BufferAttribute( new Uint32Array( index ), 1 ) );
1511
+
1512
+ } else {
1513
+
1514
+ const patches = Math.floor( quad.length / 4 );
1515
+ const position = new Float32Array( patches * 12 ), normal = N ? new Float32Array( patches * 12 ) : null;
1516
+ const corners = new Float32Array( patches * 8 ), index = new Uint32Array( patches * 6 );
1517
+ for ( let i = 0; i < patches; i ++ ) {
1518
+
1519
+ for ( let k = 0; k < 4; k ++ ) {
1520
+
1521
+ const v = quad[ i * 4 + k ];
1522
+ for ( let c = 0; c < 3; c ++ ) {
1062
1523
 
1063
- const [ a, b, c, d ] = [ quad[ i ], quad[ i + 1 ], quad[ i + 2 ], quad[ i + 3 ] ];
1064
- tris.push( a, b, c, a, c, d );
1524
+ position[ i * 12 + k * 3 + c ] = P[ v * 3 + c ];
1525
+ if ( normal ) normal[ i * 12 + k * 3 + c ] = N[ v * 3 + c ];
1526
+
1527
+ }
1528
+
1529
+ corners[ i * 8 + k * 2 ] = k & 1;
1530
+ corners[ i * 8 + k * 2 + 1 ] = k >> 1;
1531
+
1532
+ }
1533
+
1534
+ index.set( [ 0, 1, 3, 0, 3, 2 ].map( ( k ) => i * 4 + k ), i * 6 );
1535
+
1536
+ }
1537
+
1538
+ geo.setAttribute( 'position', new Float32BufferAttribute( position, 3 ) );
1539
+ geo.setAttribute( 'uv', new Float32BufferAttribute( corners, 2 ) );
1540
+ if ( normal ) geo.setAttribute( 'normal', new Float32BufferAttribute( normal, 3 ) );
1541
+ geo.setIndex( new Uint32BufferAttribute( index, 1 ) );
1065
1542
 
1066
1543
  }
1067
1544
 
1068
- const geo = new BufferGeometry();
1069
- geo.setAttribute( 'position', new Float32BufferAttribute( Float32Array.from( P ), 3 ) );
1070
- geo.setIndex( tris );
1071
- geo.computeVertexNormals();
1545
+ if ( N ) geo.userData.pbrtNormals = true;
1546
+ else geo.computeVertexNormals();
1072
1547
  return geo;
1073
1548
 
1074
1549
  }
@@ -1100,7 +1575,8 @@ export class PBRTSceneBuilder {
1100
1575
 
1101
1576
  }
1102
1577
 
1103
- if ( ! geo.getAttribute( 'normal' ) ) geo.computeVertexNormals();
1578
+ if ( geo.getAttribute( 'normal' ) ) geo.userData.pbrtNormals = true;
1579
+ else geo.computeVertexNormals();
1104
1580
  return geo;
1105
1581
 
1106
1582
  } catch ( e ) {
@@ -1114,38 +1590,41 @@ export class PBRTSceneBuilder {
1114
1590
 
1115
1591
  _sphere( params ) {
1116
1592
 
1117
- const radius = pFloat( params, 'radius', 1 );
1118
- return new SphereGeometry( radius, 48, 32 );
1593
+ const { radius, zMin, zMax, phiMax } = sphereParams( params );
1594
+ const thetaMin = Math.acos( zMin / radius ), thetaMax = Math.acos( zMax / radius );
1595
+ return quadric( phiMax, Math.max( 1, Math.ceil( 32 * ( thetaMin - thetaMax ) / Math.PI ) ), ( u, v, p, n ) => {
1596
+
1597
+ const phi = u * phiMax, theta = thetaMin + v * ( thetaMax - thetaMin );
1598
+ n.set( Math.sin( theta ) * Math.cos( phi ), Math.sin( theta ) * Math.sin( phi ), Math.cos( theta ) );
1599
+ p.copy( n ).multiplyScalar( radius );
1600
+
1601
+ } );
1602
+
1603
+ }
1604
+
1605
+ _cylinder( params ) {
1606
+
1607
+ const { radius, zMin, zMax, phiMax } = cylinderParams( params );
1608
+ return quadric( phiMax, 1, ( u, v, p, n ) => {
1609
+
1610
+ const phi = u * phiMax;
1611
+ n.set( Math.cos( phi ), Math.sin( phi ), 0 );
1612
+ p.set( radius * n.x, radius * n.y, zMin + v * ( zMax - zMin ) );
1613
+
1614
+ } );
1119
1615
 
1120
1616
  }
1121
1617
 
1122
1618
  _disk( params ) {
1123
1619
 
1124
- // Approximate as a thin ring/disk in the z=height plane.
1125
- const radius = pFloat( params, 'radius', 1 );
1126
- const inner = pFloat( params, 'innerradius', 0 );
1127
- const h = pFloat( params, 'height', 0 );
1128
- const seg = 48;
1129
- const pos = [];
1130
- const idx = [];
1131
- for ( let i = 0; i < seg; i ++ ) {
1132
-
1133
- const a0 = ( i / seg ) * Math.PI * 2;
1134
- const a1 = ( ( i + 1 ) / seg ) * Math.PI * 2;
1135
- const base = pos.length / 3;
1136
- pos.push( Math.cos( a0 ) * inner, Math.sin( a0 ) * inner, h );
1137
- pos.push( Math.cos( a0 ) * radius, Math.sin( a0 ) * radius, h );
1138
- pos.push( Math.cos( a1 ) * radius, Math.sin( a1 ) * radius, h );
1139
- pos.push( Math.cos( a1 ) * inner, Math.sin( a1 ) * inner, h );
1140
- idx.push( base, base + 1, base + 2, base, base + 2, base + 3 );
1620
+ const { radius, inner, height, phiMax } = diskParams( params );
1621
+ return quadric( phiMax, 1, ( u, v, p, n ) => {
1141
1622
 
1142
- }
1623
+ const phi = u * phiMax, r = radius - v * ( radius - inner );
1624
+ p.set( r * Math.cos( phi ), r * Math.sin( phi ), height );
1625
+ n.set( 0, 0, 1 );
1143
1626
 
1144
- const geo = new BufferGeometry();
1145
- geo.setAttribute( 'position', new Float32BufferAttribute( Float32Array.from( pos ), 3 ) );
1146
- geo.setIndex( idx );
1147
- geo.computeVertexNormals();
1148
- return geo;
1627
+ } );
1149
1628
 
1150
1629
  }
1151
1630
 
@@ -1153,6 +1632,80 @@ export class PBRTSceneBuilder {
1153
1632
 
1154
1633
  async _getMaterial( shape ) {
1155
1634
 
1635
+ let material = await this._surfaceMaterial( shape );
1636
+ if ( shape.interior ) material = await this._inMedium( material, shape.interior );
1637
+ return shape.params.alpha ? this._withAlpha( material, shape.params.alpha ) : material;
1638
+
1639
+ }
1640
+
1641
+ /** The material under a shape's `alpha`, one per material and alpha. @private */
1642
+ _withAlpha( material, alpha ) {
1643
+
1644
+ const key = alpha.type === 'texture' ? alpha.value[ 0 ] : `#${alpha.value[ 0 ]}`;
1645
+ let byAlpha = this._alphaMaterials.get( material );
1646
+ if ( ! byAlpha ) this._alphaMaterials.set( material, byAlpha = new Map() );
1647
+ if ( ! byAlpha.has( key ) ) byAlpha.set( key, this._createAlphaMaterial( material, alpha ) );
1648
+ return byAlpha.get( key );
1649
+
1650
+ }
1651
+
1652
+ async _createAlphaMaterial( material, alpha ) {
1653
+
1654
+ if ( alpha.type !== 'texture' ) return alpha.value[ 0 ] < 1 ? translucent( material, alpha.value[ 0 ] ) : material;
1655
+
1656
+ const name = alpha.value[ 0 ];
1657
+ const map = await this._alphaMap( material.map, name );
1658
+ if ( ! map ) {
1659
+
1660
+ this.warn( `shape alpha "${name}" could not be read here — drawn opaque` );
1661
+ return material;
1662
+
1663
+ }
1664
+
1665
+ if ( map.constant !== undefined ) return map.constant < 1 ? translucent( material, map.constant ) : material;
1666
+ const out = cloneMaterial( material );
1667
+ out.map = map;
1668
+ out.transparent = true;
1669
+ return out;
1670
+
1671
+ }
1672
+
1673
+ /**
1674
+ * The colour map with the float texture `name` in its alpha channel, `{ constant }` when that texture is one value,
1675
+ * or null when it cannot be read here.
1676
+ * @private
1677
+ */
1678
+ async _alphaMap( colorMap, name ) {
1679
+
1680
+ const def = this.ir.namedTextures.get( name );
1681
+ const filename = def?.class === 'imagemap' ? pString( def.params, 'filename', null ) : null;
1682
+ // Most often the alpha is the colour map's own image: its colour stays as it is.
1683
+ if ( filename && colorMap?.userData.pbrtImage === filename && pString( def.params, 'mapping', 'uv' ) === 'uv'
1684
+ && pFloat( def.params, 'scale', 1 ) === 1 && ! pBool( def.params, 'invert', false ) && sameMapping( colorMap, uvMapping( def.params ) ) ) {
1685
+
1686
+ const pixels = texturePixels( colorMap );
1687
+ const own = pixels && withDecodedAlpha( pixels, decoderFor( def.params ) );
1688
+ if ( own ) {
1689
+
1690
+ setMapping( own, uvMapping( def.params ) );
1691
+ return own;
1692
+
1693
+ }
1694
+
1695
+ }
1696
+
1697
+ const alpha = await this._bakeTerm( name, 1 );
1698
+ if ( ! alpha ) return null;
1699
+ if ( ! alpha.layer ) return { constant: alpha.rgb[ 0 ] };
1700
+ const color = colorMap ? this._textureLayer( colorMap, 3 ) : null;
1701
+ if ( colorMap && ! color ) return null;
1702
+ const baked = bake( [ color ? { layer: color } : { rgb: [ 1, 1, 1 ] }, alpha ], ( [ c ] ) => c, { alphaOf: ( [ , a ] ) => a[ 0 ] } );
1703
+ return baked && bakedTexture( baked );
1704
+
1705
+ }
1706
+
1707
+ async _surfaceMaterial( shape ) {
1708
+
1156
1709
  // Cache by (material, areaLight) object identity — many shapes share a
1157
1710
  // NamedMaterial, so this dedupes the build + texture-decode work. Nested
1158
1711
  // Map keys on the object refs directly (null is a valid key).
@@ -1166,11 +1719,7 @@ export class PBRTSceneBuilder {
1166
1719
 
1167
1720
  if ( byLight.has( shape.areaLight ) ) return byLight.get( shape.areaLight );
1168
1721
 
1169
- const ctx = {
1170
- resolveNamedTexture: ( n ) => this._resolveNamedTexture( n ),
1171
- namedMaterials: this.ir.namedMaterials,
1172
- warn: ( m ) => this.warn( m )
1173
- };
1722
+ const ctx = this._materialContext();
1174
1723
  const material = await buildMaterial( shape.material, ctx );
1175
1724
 
1176
1725
  if ( shape.areaLight ) await this._applyAreaLight( material, shape.areaLight, ctx );
@@ -1181,6 +1730,86 @@ export class PBRTSceneBuilder {
1181
1730
 
1182
1731
  }
1183
1732
 
1733
+ _materialContext() {
1734
+
1735
+ return this._ctx ??= {
1736
+ resolveNamedTexture: ( n ) => this._resolveNamedTexture( n ),
1737
+ namedMaterials: this.ir.namedMaterials,
1738
+ warn: ( m ) => this.warn( m ),
1739
+ floatTextureMean: async ( n ) => {
1740
+
1741
+ const term = await this._bakeTerm( n, 1 );
1742
+ return term ? ( term.layer ? layerMean( term.layer ) : term.rgb[ 0 ] ) : null;
1743
+
1744
+ },
1745
+ bakeMaterialMix: ( a, b, amountName ) => this._bakeMaterialMix( a, b, amountName ),
1746
+ bakeFloatMap: ( params, keys, combine ) => this._floatMap( params, keys, combine ),
1747
+ textureMeanRGB: async ( n ) => {
1748
+
1749
+ const term = await this._bakeTerm( n, 3 );
1750
+ if ( ! term?.layer ) return term?.rgb ?? null;
1751
+ const { data } = term.layer;
1752
+ const sum = [ 0, 0, 0 ];
1753
+ for ( let i = 0; i < data.length; i ++ ) sum[ i % 3 ] += data[ i ];
1754
+ return sum.map( ( v, c ) => v / ( data.length / 3 ) * ( term.tint?.[ c ] ?? 1 ) );
1755
+
1756
+ },
1757
+ resolveNormalMap: async ( filename ) => {
1758
+
1759
+ const image = await this._image( filename );
1760
+ if ( ! image ) return null;
1761
+ const texture = eightBit( image, { srgb: false } );
1762
+ const normalMap = texture === image ? image.clone() : texture;
1763
+ normalMap.colorSpace = NoColorSpace;
1764
+ return normalMap;
1765
+
1766
+ }
1767
+ };
1768
+
1769
+ }
1770
+
1771
+ /**
1772
+ * Glass filled with a homogeneous medium: what passes straight through it (σt = σa + σs) becomes Beer–Lambert
1773
+ * attenuation, since the engine traces no scattering inside a solid. A medium in an opaque shape changes nothing.
1774
+ * @private
1775
+ */
1776
+ async _inMedium( material, name ) {
1777
+
1778
+ if ( ! ( material.transmission > 0 ) ) return material;
1779
+ let byName = this._mediumMaterials.get( material );
1780
+ if ( ! byName ) this._mediumMaterials.set( material, byName = new Map() );
1781
+ if ( ! byName.has( name ) ) byName.set( name, this._attenuated( material, name ) );
1782
+ return byName.get( name );
1783
+
1784
+ }
1785
+
1786
+ async _attenuated( material, name ) {
1787
+
1788
+ const params = this.ir.media?.get( name );
1789
+ const type = params && pString( params, 'type', '' );
1790
+ if ( type !== 'homogeneous' ) {
1791
+
1792
+ this.warn( params ? `medium "${name}" (${type}) is not supported — "${name}" glass left clear` : `medium "${name}" not defined` );
1793
+ return material;
1794
+
1795
+ }
1796
+
1797
+ if ( params.preset ) this.warn( `medium preset "${pString( params, 'preset', '' )}" is not supported — using sigma_a / sigma_s` );
1798
+ const ctx = this._materialContext();
1799
+ const a = ( await resolveSpectrum( params, 'sigma_a', ctx, [ 1, 1, 1 ] ) ).rgb;
1800
+ const s = ( await resolveSpectrum( params, 'sigma_s', ctx, [ 1, 1, 1 ] ) ).rgb;
1801
+ const scale = pFloat( params, 'scale', 1 );
1802
+ const sigma = [ 0, 1, 2 ].map( ( c ) => ( a[ c ] + s[ c ] ) * scale );
1803
+ const max = Math.max( ...sigma );
1804
+ if ( ! ( max > 0 ) ) return material;
1805
+
1806
+ const variant = cloneMaterial( material );
1807
+ variant.attenuationDistance = 1 / max;
1808
+ variant.attenuationColor.setRGB( ...sigma.map( ( v ) => Math.exp( - v / max ) ) );
1809
+ return this._dedupeMaterial( variant );
1810
+
1811
+ }
1812
+
1184
1813
  /**
1185
1814
  * Collapse materials that render identically onto one instance — an inline `Material` per
1186
1815
  * shape is a distinct object every time, so every batch would hold exactly one shape.
@@ -1190,9 +1819,12 @@ export class PBRTSceneBuilder {
1190
1819
 
1191
1820
  const c = material.color, e = material.emissive;
1192
1821
  const key = `${c.r},${c.g},${c.b}|${e.r},${e.g},${e.b}|${material.emissiveIntensity}|` +
1193
- `${material.map ? material.map.uuid : '-'}|${material.roughness}|${material.metalness}|` +
1822
+ `${material.map ? material.map.uuid : '-'}|${material.roughness}|${material.roughnessMap?.uuid ?? '-'}|${material.metalness}|` +
1194
1823
  `${material.transmission}|${material.ior}|${material.thickness}|` +
1195
- `${material.clearcoat}|${material.clearcoatRoughness}|${material.opacity}|${material.side}`;
1824
+ `${material.clearcoat}|${material.clearcoatRoughness}|${material.clearcoatRoughnessMap?.uuid ?? '-'}|${material.opacity}|${material.side}|` +
1825
+ `${material.normalMap ? material.normalMap.uuid : '-'}|${material.attenuationDistance}|${material.attenuationColor.toArray()}|` +
1826
+ `${material.specularIntensity}|${material.diffuseTransmission ?? 0}|${material.diffuseTransmissionColor?.toArray() ?? '-'}|` +
1827
+ `${material.subsurface ?? 0}|${material.subsurfaceColor?.toArray() ?? '-'}|${material.subsurfaceRadius ?? '-'}|${material.subsurfaceAnisotropy ?? 0}`;
1196
1828
 
1197
1829
  const existing = this._materialBySignature.get( key );
1198
1830
  if ( existing ) return existing;
@@ -1201,13 +1833,31 @@ export class PBRTSceneBuilder {
1201
1833
 
1202
1834
  }
1203
1835
 
1836
+ /**
1837
+ * An area light given as `power`: pbrt scales its radiance to that power over this shape's area (both sides of a
1838
+ * twosided one); an image's mean is not in it, as an emitting image is not supported.
1839
+ * @private
1840
+ */
1841
+ _poweredMaterial( shape, geometry, material ) {
1842
+
1843
+ const power = pFloat( shape.areaLight.params, 'power', - 1 );
1844
+ if ( ! ( power > 0 ) ) return material;
1845
+ const area = shapeArea( shape, geometry ) * ( pBool( shape.areaLight.params, 'twosided', false ) ? 2 : 1 );
1846
+ if ( ! ( area > 0 ) ) return material;
1847
+ const out = cloneMaterial( material );
1848
+ out.emissiveIntensity = material.emissiveIntensity * power / ( Math.PI * area );
1849
+ return out;
1850
+
1851
+ }
1852
+
1204
1853
  async _applyAreaLight( material, areaLight, ctx ) {
1205
1854
 
1206
- const L = await resolveSpectrum( areaLight.params, 'L', ctx, [ 1, 1, 1 ] );
1855
+ const rgb = await lightRGB( areaLight.params, 'L', ctx );
1207
1856
  const scale = pFloat( areaLight.params, 'scale', 1 );
1208
- const rgb = L.rgb || [ 1, 1, 1 ];
1209
1857
  material.emissive.setRGB( rgb[ 0 ], rgb[ 1 ], rgb[ 2 ] );
1210
1858
  material.emissiveIntensity = scale;
1859
+ // pbrt's diffuse area light is one-sided unless "twosided" (_facingEmission turns the triangles).
1860
+ material.side = pBool( areaLight.params, 'twosided', false ) ? DoubleSide : FrontSide;
1211
1861
 
1212
1862
  }
1213
1863
 
@@ -1224,22 +1874,11 @@ export class PBRTSceneBuilder {
1224
1874
 
1225
1875
  } else if ( def.class === 'imagemap' ) {
1226
1876
 
1227
- const filename = pString( def.params, 'filename', null );
1228
- if ( filename ) {
1229
-
1230
- try {
1231
-
1232
- const tex = await this.resolveImage( filename );
1233
- if ( tex ) result = { texture: tex };
1234
- else this.warn( `image not found for texture "${name}": ${filename}` );
1235
-
1236
- } catch ( e ) {
1877
+ result = await this._imageMapTexture( name, def );
1237
1878
 
1238
- this.warn( `failed to load texture "${name}" (${filename}): ${e.message}` );
1879
+ } else if ( def.class === 'mix' ) {
1239
1880
 
1240
- }
1241
-
1242
- }
1881
+ result = await this._mixTexture( name, def );
1243
1882
 
1244
1883
  } else if ( def.class === 'constant' ) {
1245
1884
 
@@ -1247,6 +1886,10 @@ export class PBRTSceneBuilder {
1247
1886
  if ( v && v.type === 'rgb' ) result = { constant: [ v.value[ 0 ], v.value[ 1 ], v.value[ 2 ] ] };
1248
1887
  else if ( v ) result = { constant: [ v.value[ 0 ], v.value[ 0 ], v.value[ 0 ] ] };
1249
1888
 
1889
+ } else if ( def.class === 'checkerboard' || def.class === 'bilerp' ) {
1890
+
1891
+ result = await this._proceduralTexture( name, def );
1892
+
1250
1893
  } else if ( def.class === 'scale' ) {
1251
1894
 
1252
1895
  // Scale = inner_texture * scale_factor. Resolve the inner (recursively if
@@ -1276,6 +1919,242 @@ export class PBRTSceneBuilder {
1276
1919
 
1277
1920
  }
1278
1921
 
1922
+ /**
1923
+ * pbrt's 2D `checkerboard` and `bilerp`, baked: one period of the checks (two each way), or the bilerp over the unit
1924
+ * square, in the texture's uv mapping. A textured input counts as its mean.
1925
+ * @private
1926
+ */
1927
+ async _proceduralTexture( name, def ) {
1928
+
1929
+ const params = def.params;
1930
+ const ctx = this._materialContext();
1931
+ const value = async ( key, dflt ) => {
1932
+
1933
+ const p = params[ key ];
1934
+ if ( p?.type === 'texture' ) return ( await ctx.textureMeanRGB( p.value[ 0 ] ) ) ?? dflt;
1935
+ return ( await resolveSpectrum( params, key, ctx, dflt ) ).rgb ?? dflt;
1936
+
1937
+ };
1938
+
1939
+ const mapping = pString( params, 'mapping', 'uv' );
1940
+ if ( mapping !== 'uv' ) this.warn( `texture "${name}": "${mapping}" mapping is not supported — using the mesh's uv` );
1941
+ const { su, sv, du, dv } = uvMapping( params );
1942
+
1943
+ if ( def.class === 'checkerboard' ) {
1944
+
1945
+ if ( pFloat( params, 'dimension', 2 ) !== 2 ) {
1946
+
1947
+ this.warn( `texture "${name}": a 3D checkerboard is not supported` );
1948
+ return null;
1949
+
1950
+ }
1951
+
1952
+ const [ a, b ] = await Promise.all( [ value( 'tex1', [ 1, 1, 1 ] ), value( 'tex2', [ 0, 0, 0 ] ) ] );
1953
+ const n = 2 * CHECK_TEXELS;
1954
+ const data = rasterize( n, n, ( s, t ) => ( ( Math.floor( 2 * s ) + Math.floor( 2 * t ) ) % 2 ? b : a ) );
1955
+ return { texture: bakedTexture( { data, width: n, height: n, mapping: { su: su / 2, sv: sv / 2, du: du / 2, dv: dv / 2 } } ) };
1956
+
1957
+ }
1958
+
1959
+ const [ v00, v01, v10, v11 ] = await Promise.all( [
1960
+ value( 'v00', [ 0, 0, 0 ] ), value( 'v01', [ 1, 1, 1 ] ), value( 'v10', [ 0, 0, 0 ] ), value( 'v11', [ 1, 1, 1 ] )
1961
+ ] );
1962
+ const data = rasterize( BILERP_TEXELS, BILERP_TEXELS, ( s, t ) => [ 0, 1, 2 ].map( ( c ) =>
1963
+ ( 1 - s ) * ( 1 - t ) * v00[ c ] + s * ( 1 - t ) * v10[ c ] + ( 1 - s ) * t * v01[ c ] + s * t * v11[ c ] ) );
1964
+ return { texture: bakedTexture( { data, width: BILERP_TEXELS, height: BILERP_TEXELS, mapping: { su, sv, du, dv } } ) };
1965
+
1966
+ }
1967
+
1968
+ /** A file's image, decoded once however many named textures read it. @private */
1969
+ _image( filename ) {
1970
+
1971
+ let pending = this._imageCache.get( filename );
1972
+ if ( ! pending ) {
1973
+
1974
+ pending = Promise.resolve().then( () => this.resolveImage( filename ) ).catch( ( e ) => {
1975
+
1976
+ this.warn( `failed to load image ${filename}: ${e.message}` );
1977
+ return null;
1978
+
1979
+ } );
1980
+ this._imageCache.set( filename, pending );
1981
+
1982
+ }
1983
+
1984
+ return pending;
1985
+
1986
+ }
1987
+
1988
+ /**
1989
+ * An image texture with pbrt's uv mapping; its `scale` becomes the material's tint, except that a scale above 1 or
1990
+ * `invert` is baked (pbrt clamps an albedo per texel, which a tint cannot).
1991
+ * @private
1992
+ */
1993
+ async _imageMapTexture( name, def ) {
1994
+
1995
+ const filename = pString( def.params, 'filename', null );
1996
+ if ( ! filename ) return null;
1997
+ const image = await this._image( filename );
1998
+ if ( ! image ) {
1999
+
2000
+ this.warn( `image not found for texture "${name}": ${filename}` );
2001
+ return null;
2002
+
2003
+ }
2004
+
2005
+ const mapping = pString( def.params, 'mapping', 'uv' );
2006
+ if ( mapping !== 'uv' ) this.warn( `texture "${name}": "${mapping}" mapping is not supported — using the mesh's uv` );
2007
+
2008
+ const scale = pFloat( def.params, 'scale', 1 );
2009
+ if ( scale > 1 || pBool( def.params, 'invert', false ) ) {
2010
+
2011
+ const term = await this._bakeTerm( name, 3 );
2012
+ const baked = term?.layer && bake( [ { ...term, clamp: true } ], ( [ v ] ) => v );
2013
+ if ( baked ) return { texture: bakedTexture( baked ) };
2014
+ this.warn( `texture "${name}": its scale or invert could not be baked here — ignored` );
2015
+
2016
+ }
2017
+
2018
+ const color = eightBit( image, { srgb: true } );
2019
+ const texture = color === image ? image.clone() : color;
2020
+ texture.userData.pbrtImage = filename;
2021
+ setMapping( texture, uvMapping( def.params ) );
2022
+ texture.needsUpdate = true;
2023
+ return scale < 1 ? { texture, constant: [ scale, scale, scale ] } : { texture };
2024
+
2025
+ }
2026
+
2027
+ /**
2028
+ * A data map (linear, in every channel) of `combine` over float parameters `keys`, constants or textures; null when
2029
+ * one cannot be read here or all are constants.
2030
+ * @private
2031
+ */
2032
+ async _floatMap( params, keys, combine ) {
2033
+
2034
+ const terms = await Promise.all( keys.map( ( key ) => this._paramTerm( params, key, 1, [ 0, 0, 0 ] ) ) );
2035
+ if ( terms.some( ( term ) => ! term ) ) return null;
2036
+ const baked = bake( terms, ( values ) => {
2037
+
2038
+ const v = combine( values.map( ( value ) => value[ 0 ] ) );
2039
+ return [ v, v, v ];
2040
+
2041
+ }, { linear: true } );
2042
+ return baked && bakedTexture( baked, { srgb: false } );
2043
+
2044
+ }
2045
+
2046
+ /** pbrt's `mix` texture, ( 1 − amount ) · tex1 + amount · tex2, baked when any of the three is an image. @private */
2047
+ async _mixTexture( name, def ) {
2048
+
2049
+ const [ a, b, amount ] = await Promise.all( [
2050
+ this._paramTerm( def.params, 'tex1', 3, [ 0, 0, 0 ] ),
2051
+ this._paramTerm( def.params, 'tex2', 3, [ 1, 1, 1 ] ),
2052
+ this._paramTerm( def.params, 'amount', 1, [ 0.5, 0.5, 0.5 ] )
2053
+ ] );
2054
+ if ( ! a || ! b || ! amount ) {
2055
+
2056
+ this.warn( `texture "${name}": a "mix" input could not be read here` );
2057
+ return null;
2058
+
2059
+ }
2060
+
2061
+ const unit = ( rgb ) => rgb.map( ( v ) => Math.min( 1, Math.max( 0, v ) ) );
2062
+ const baked = bake( [ { ...a, clamp: true }, { ...b, clamp: true }, amount ], mixOf );
2063
+ return baked ? { texture: bakedTexture( baked ) } : { constant: mixOf( [ unit( a.rgb ), unit( b.rgb ), amount.rgb ] ) };
2064
+
2065
+ }
2066
+
2067
+ /** A texture parameter as a bake term: a constant, or the texture it names. @private */
2068
+ async _paramTerm( params, key, channels, dflt ) {
2069
+
2070
+ const p = params[ key ];
2071
+ if ( ! p ) return { rgb: dflt };
2072
+ if ( p.type === 'texture' ) return this._bakeTerm( p.value[ 0 ], channels );
2073
+ const rgb = ( await resolveSpectrum( params, key, this._materialContext(), dflt ) ).rgb;
2074
+ return rgb ? { rgb } : null;
2075
+
2076
+ }
2077
+
2078
+ /**
2079
+ * A named texture as pbrt evaluates it, for baking: `{ rgb }` for a constant, `{ layer, tint? }` for an image;
2080
+ * null when it cannot be read in this runtime.
2081
+ * @param {string} name
2082
+ * @param {number} channels - 1 where it is read as a float texture, 3 as a spectrum
2083
+ * @private
2084
+ */
2085
+ _bakeTerm( name, channels ) {
2086
+
2087
+ const key = `${name}\0${channels}`;
2088
+ let pending = this._termCache.get( key );
2089
+ if ( ! pending ) this._termCache.set( key, pending = this._createBakeTerm( name, channels ) );
2090
+ return pending;
2091
+
2092
+ }
2093
+
2094
+ async _createBakeTerm( name, channels ) {
2095
+
2096
+ const def = this.ir.namedTextures.get( name );
2097
+ if ( def?.class === 'imagemap' ) {
2098
+
2099
+ if ( pString( def.params, 'mapping', 'uv' ) !== 'uv' ) return null;
2100
+ const filename = pString( def.params, 'filename', null );
2101
+ const pixels = texturePixels( filename && await this._image( filename ) );
2102
+ if ( ! pixels ) return null;
2103
+ return { layer: makeLayer( pixels, {
2104
+ channels, decode: pixels.float ? null : decoderFor( def.params ),
2105
+ scale: pFloat( def.params, 'scale', 1 ), invert: pBool( def.params, 'invert', false ), mapping: uvMapping( def.params )
2106
+ } ) };
2107
+
2108
+ }
2109
+
2110
+ const resolved = await this._resolveNamedTexture( name );
2111
+ if ( ! resolved ) return null;
2112
+ if ( ! resolved.texture ) return resolved.constant ? { rgb: resolved.constant } : null;
2113
+ const layer = this._textureLayer( resolved.texture, channels );
2114
+ return layer ? { layer, tint: resolved.constant ?? null } : null;
2115
+
2116
+ }
2117
+
2118
+ // A texture already built for a material (an image or a bake) as a layer, in the mapping it carries.
2119
+ _textureLayer( texture, channels ) {
2120
+
2121
+ const pixels = texturePixels( texture );
2122
+ if ( ! pixels ) return null;
2123
+ const srgb = ! pixels.float && texture.colorSpace === SRGBColorSpace;
2124
+ return makeLayer( pixels, {
2125
+ channels, decode: pixels.float ? null : ( srgb ? srgbToLinear : ( v ) => v ),
2126
+ mapping: { su: texture.repeat.x, sv: texture.repeat.y, du: texture.offset.x, dv: texture.offset.y }
2127
+ } );
2128
+
2129
+ }
2130
+
2131
+ /**
2132
+ * A mix material whose amount is a texture: its two materials' colours (map × colour) baked by that texture, and
2133
+ * the texture's mean to weigh everything else. Null when nothing here can read it.
2134
+ * @private
2135
+ */
2136
+ async _bakeMaterialMix( matA, matB, amountName ) {
2137
+
2138
+ const amount = await this._bakeTerm( amountName, 1 );
2139
+ if ( ! amount ) return null;
2140
+ if ( ! amount.layer ) return { mean: amount.rgb[ 0 ], texture: null };
2141
+
2142
+ const mean = layerMean( amount.layer );
2143
+ const term = ( m ) => {
2144
+
2145
+ if ( ! m.map ) return { rgb: m.color.toArray(), clamp: true };
2146
+ const layer = this._textureLayer( m.map, 3 );
2147
+ return layer && { layer, tint: m.color.toArray(), clamp: true };
2148
+
2149
+ };
2150
+
2151
+ const a = term( matA ), b = term( matB );
2152
+ if ( ! a || ! b ) return { mean, texture: null };
2153
+ const baked = bake( [ a, b, amount ], mixOf );
2154
+ return { mean, texture: baked ? bakedTexture( baked ) : null };
2155
+
2156
+ }
2157
+
1279
2158
  _colorFromLikeNamedMaterial( texName ) {
1280
2159
 
1281
2160
  for ( const candidate of materialNameCandidates( texName ) ) {
@@ -1297,6 +2176,25 @@ export class PBRTSceneBuilder {
1297
2176
  async _resolveScaleTexture( name, def ) {
1298
2177
 
1299
2178
  // pbrt-v4 uses "tex" (the inner texture or constant) and "scale" (the multiplier).
2179
+ if ( def.params.scale?.type === 'texture' ) {
2180
+
2181
+ // A texture multiplier (kroken's bricks: colour × dirt) is baked.
2182
+ const [ inner, factor ] = await Promise.all( [
2183
+ this._paramTerm( def.params, 'tex', 3, [ 1, 1, 1 ] ),
2184
+ this._paramTerm( def.params, 'scale', 1, [ 1, 1, 1 ] )
2185
+ ] );
2186
+ const product = ( [ a, f ] ) => [ a[ 0 ] * f[ 0 ], a[ 1 ] * f[ 0 ], a[ 2 ] * f[ 0 ] ];
2187
+ if ( inner && factor ) {
2188
+
2189
+ const baked = bake( [ { ...inner, clamp: true }, factor ], product );
2190
+ return baked ? { texture: bakedTexture( baked ) } : { constant: product( [ inner.rgb, factor.rgb ] ) };
2191
+
2192
+ }
2193
+
2194
+ this.warn( `texture "${name}": its "scale" texture could not be read here — ignored` );
2195
+
2196
+ }
2197
+
1300
2198
  const innerP = def.params.tex;
1301
2199
  let inner = null;
1302
2200
 
@@ -1362,6 +2260,8 @@ export class PBRTSceneBuilder {
1362
2260
  camera.name = 'PBRT Camera';
1363
2261
  this._poseCamera( camera, cam.cameraToWorld );
1364
2262
  camera.updateMatrixWorld( true );
2263
+ const lensRadius = pFloat( cam.params, 'lensradius', 0 );
2264
+ if ( lensRadius > 0 ) camera.userData.__rayzeeEffects = lensEffects( camera, lensRadius, pFloat( cam.params, 'focaldistance', 1e6 ) );
1365
2265
  return camera;
1366
2266
 
1367
2267
  }
@@ -1500,8 +2400,7 @@ export class PBRTSceneBuilder {
1500
2400
 
1501
2401
  // Constant-radiance infinite light → tiny float texture (CDF-buildable).
1502
2402
  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 );
2403
+ const rgb = ( await lightRGB( inf.params, 'L', ctx ) ).map( v => v * scale );
1505
2404
 
1506
2405
  const w = 2, h = 1;
1507
2406
  const data = new Float32Array( w * h * 4 );
@@ -1523,13 +2422,89 @@ export class PBRTSceneBuilder {
1523
2422
 
1524
2423
  }
1525
2424
 
2425
+ /**
2426
+ * `distant`, `point` and `spot` lights as three.js lamps, in the engine's units: a directional lamp's
2427
+ * intensity is the irradiance it gives (pbrt's L), a point or spot lamp's is 4π × its radiant intensity.
2428
+ */
2429
+ async _buildLamps( lights, group ) {
2430
+
2431
+ const ctx = { resolveNamedTexture: async () => null, warn: ( m ) => this.warn( m ) };
2432
+ for ( const l of lights ) {
2433
+
2434
+ if ( ! LAMP_TYPES.has( l.type ) ) continue;
2435
+
2436
+ const world = new Matrix4().fromArray( this.convertHandedness ? M.multiply( FLIP_Z, l.ctm ) : l.ctm );
2437
+ const point = ( name, dflt ) => {
2438
+
2439
+ const v = l.params[ name ]?.value;
2440
+ return new Vector3().fromArray( v?.length >= 3 ? v : dflt ).applyMatrix4( world );
2441
+
2442
+ };
2443
+
2444
+ const rgb = await lightRGB( l.params, l.type === 'distant' ? 'L' : 'I', ctx );
2445
+ let scale = pFloat( l.params, 'scale', 1 );
2446
+ const from = point( 'from', [ 0, 0, 0 ] );
2447
+ const to = point( 'to', [ 0, 0, 1 ] );
2448
+ let light;
2449
+ if ( l.type === 'distant' ) {
2450
+
2451
+ const illuminance = pFloat( l.params, 'illuminance', - 1 );
2452
+ if ( illuminance > 0 ) scale *= illuminance;
2453
+ light = new DirectionalLight();
2454
+ light.userData.__luxConverted = true;
2455
+
2456
+ } else if ( l.type === 'point' ) {
2457
+
2458
+ // `power` sets the radiant intensity to power / 4π.
2459
+ const power = pFloat( l.params, 'power', - 1 );
2460
+ scale *= power > 0 ? power : 4 * Math.PI;
2461
+ light = new PointLight();
2462
+ light.userData.__candelaConverted = true;
2463
+
2464
+ } else {
2465
+
2466
+ // pbrt's falloff is smoothstep( cos cone, cos( cone − delta ), cos θ ); the engine's blend is that width over 1 − cos cone.
2467
+ const cone = Math.min( Math.max( pFloat( l.params, 'coneangle', 30 ), 0 ), 90 ) * Math.PI / 180;
2468
+ const delta = Math.min( Math.max( pFloat( l.params, 'conedeltaangle', 5 ) * Math.PI / 180, 0 ), cone );
2469
+ const cosEnd = Math.cos( cone ), cosStart = Math.cos( cone - delta );
2470
+ const power = pFloat( l.params, 'power', - 1 );
2471
+ if ( power > 0 ) scale *= power / ( 2 * Math.PI * ( ( 1 - cosStart ) + ( cosStart - cosEnd ) / 2 ) );
2472
+ scale *= 4 * Math.PI;
2473
+ light = new SpotLight();
2474
+ light.angle = cone;
2475
+ light.penumbra = cosEnd < 1 ? Math.min( ( cosStart - cosEnd ) / ( 1 - cosEnd ), 1 ) : 0;
2476
+ light.userData.__candelaConverted = true;
2477
+
2478
+ }
2479
+
2480
+ const peak = Math.max( rgb[ 0 ], rgb[ 1 ], rgb[ 2 ] );
2481
+ if ( ! ( peak > 0 ) || ! ( scale > 0 ) ) continue;
2482
+ light.color.setRGB( rgb[ 0 ] / peak, rgb[ 1 ] / peak, rgb[ 2 ] / peak );
2483
+ light.intensity = peak * scale;
2484
+ light.name = `pbrt ${l.type}`;
2485
+ light.position.copy( from );
2486
+ light.decay = 2;
2487
+ light.distance = 0;
2488
+ if ( light.target ) {
2489
+
2490
+ light.target.position.subVectors( to, from );
2491
+ light.add( light.target );
2492
+
2493
+ }
2494
+
2495
+ group.add( light );
2496
+
2497
+ }
2498
+
2499
+ }
2500
+
1526
2501
  _reportUnsupportedLights( lights ) {
1527
2502
 
1528
2503
  for ( const l of lights ) {
1529
2504
 
1530
- if ( l.type !== 'infinite' ) {
2505
+ if ( l.type !== 'infinite' && ! LAMP_TYPES.has( l.type ) ) {
1531
2506
 
1532
- this.warn( `light "${l.type}" not supported (only infinite lights and emissive area lights are mapped)` );
2507
+ this.warn( `light "${l.type}" not supported (only infinite, distant, point and spot lights and emissive area lights are mapped)` );
1533
2508
 
1534
2509
  }
1535
2510