rayzee 11.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 (34) hide show
  1. package/dist/{ArchiveImporter-BRQvLggb.js → ArchiveImporter-Hn35o6Z5.js} +1525 -680
  2. package/dist/BidirectionalIntegrator-CBw8hkeu.js +946 -0
  3. package/dist/{NeuralRetouch-BCXylThH.js → NeuralRetouch-BzNpiVHZ.js} +1 -1
  4. package/dist/{NeuralSuperRes-sZAeS142.js → NeuralSuperRes-Bk7bHg2B.js} +1 -1
  5. package/dist/{PathTracer-CQPW06Mm.js → PathTracer-72XufKAr.js} +79 -74
  6. package/dist/{PhysicalSky-BvqCw_dy.js → PhysicalSky-BgjdV88k.js} +1 -1
  7. package/dist/{RayzeeRenderer-COpwotpU.js → RayzeeRenderer-1QRO97eq.js} +3 -3
  8. package/dist/addons/archives.es.js +1 -1
  9. package/dist/addons/bidirectional.es.js +1 -1
  10. package/dist/addons/physical-sky.es.js +1 -1
  11. package/dist/{archiveFormats-BZw7TiDp.js → archiveFormats-tnAoUL1j.js} +28 -9
  12. package/dist/rayzee-core.es.js +4 -4
  13. package/dist/rayzee.es.js +11 -11
  14. package/dist/rayzee.umd.js +43 -43
  15. package/dist/{sceneCachePolicy-BSSsKY98.js → sceneCachePolicy-C67jbci-.js} +1 -1
  16. package/package.json +1 -1
  17. package/src/Processor/ArchiveCache.js +19 -6
  18. package/src/Processor/ArchiveImporter.js +9 -3
  19. package/src/Processor/ArchiveReader.js +22 -4
  20. package/src/Processor/PBRT/PBRTMaterials.js +132 -8
  21. package/src/Processor/PBRT/PBRTParser.js +20 -3
  22. package/src/Processor/PBRT/PBRTScattering.js +93 -0
  23. package/src/Processor/PBRT/PBRTSceneBuilder.js +513 -49
  24. package/src/Processor/PBRT/PBRTTextureBake.js +32 -7
  25. package/src/Processor/SceneMetadata.js +30 -5
  26. package/src/RayzeeRenderer.js +3 -2
  27. package/src/Stages/PathTracer.js +3 -5
  28. package/src/TSL/Bidirectional.js +6 -2
  29. package/src/TSL/ConnectKernel.js +31 -11
  30. package/src/TSL/LightSplatKernel.js +22 -7
  31. package/src/TSL/MergeKernel.js +13 -7
  32. package/src/TSL/ShadeKernel.js +51 -30
  33. package/src/integrators/BidirectionalIntegrator.js +6 -1
  34. package/dist/BidirectionalIntegrator-9lZQxFVH.js +0 -934
@@ -17,14 +17,14 @@
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
22
  SRGBColorSpace, NoColorSpace, AnimationClip, VectorKeyframeTrack, QuaternionKeyframeTrack,
23
23
  NumberKeyframeTrack, BooleanKeyframeTrack, DirectionalLight, PointLight, SpotLight, FrontSide, DoubleSide,
24
24
  RepeatWrapping
25
25
  } from 'three';
26
26
  import { buildMaterial, pBool, pFloat, pString, resolveSpectrum } from './PBRTMaterials.js';
27
- import { makeLayer, layerMean, bake, mixOf, srgbToLinear, linearToSRGB } from './PBRTTextureBake.js';
27
+ import { makeLayer, layerMean, bake, mixOf, srgbToLinear, linearToSRGB, hasAlpha } from './PBRTTextureBake.js';
28
28
  import { loopSubdivide } from './LoopSubdivision.js';
29
29
  import { octahedralToEquirect } from './EqualAreaOctahedral.js';
30
30
  import { tessellateCurve } from './PBRTCurves.js';
@@ -107,7 +107,7 @@ const DEFAULT_CURVE_SIDES = { flat: 1, ribbon: 1, cylinder: 2 };
107
107
  const DEFAULT_CURVE_TOLERANCE = 0.05;
108
108
 
109
109
  /** Bumped whenever the same scene files build a different graph, so a stored graph is not reused. */
110
- export const PBRT_BUILD_REVISION = 8;
110
+ export const PBRT_BUILD_REVISION = 9;
111
111
 
112
112
  function samePlacements( a, b ) {
113
113
 
@@ -244,11 +244,11 @@ function eightBit( texture, { srgb } ) {
244
244
 
245
245
  }
246
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 } ) {
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
249
 
250
250
  const texture = new DataTexture( data, width, height, RGBAFormat );
251
- texture.colorSpace = SRGBColorSpace;
251
+ texture.colorSpace = srgb ? SRGBColorSpace : NoColorSpace;
252
252
  texture.wrapS = texture.wrapT = RepeatWrapping;
253
253
  setMapping( texture, mapping );
254
254
  texture.needsUpdate = true;
@@ -256,6 +256,248 @@ function bakedTexture( { data, width, height, mapping } ) {
256
256
 
257
257
  }
258
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
+
259
501
  /**
260
502
  * Names an unreadable texture may have been derived from. Moana binds colour as
261
503
  * `"texture reflectance" "el:part_Color"` and leaves `MakeNamedMaterial "el:part"` in the
@@ -316,6 +558,7 @@ export class PBRTSceneBuilder {
316
558
  this._imageCache = new Map(); // filename -> Promise<Texture|null>, decoded once for every named texture
317
559
  this._termCache = new Map(); // texture name + channels -> Promise<{rgb}|{layer, tint?}|null>
318
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>)
319
562
 
320
563
  }
321
564
 
@@ -373,11 +616,12 @@ export class PBRTSceneBuilder {
373
616
  }
374
617
 
375
618
  const shape = ir.shapes[ i ];
376
- const [ geometry, sharedMaterial ] = await Promise.all( [
619
+ const [ geometry, surface ] = await Promise.all( [
377
620
  this._batches ? this._createGeometry( shape ) : this._buildGeometry( shape ),
378
621
  this._getMaterial( shape )
379
622
  ] );
380
623
  if ( ! geometry ) continue;
624
+ const sharedMaterial = shape.areaLight ? this._poweredMaterial( shape, geometry, surface ) : surface;
381
625
 
382
626
  // A moving shape needs a node of its own to move.
383
627
  if ( this._batches && ! shape.motion && geometry.getAttribute( 'position' ).count <= MERGE_VERTEX_LIMIT ) {
@@ -442,6 +686,7 @@ export class PBRTSceneBuilder {
442
686
 
443
687
  return {
444
688
  group, camera, environment, animations,
689
+ render: renderRequest( ir ),
445
690
  report: this.report,
446
691
  meshCount: this.reportedMeshes,
447
692
  triangleCount: this.triangleCount,
@@ -865,7 +1110,7 @@ export class PBRTSceneBuilder {
865
1110
  if ( ! material ) {
866
1111
 
867
1112
  this.warn( `${name} (${shape.type}, "${shape.material?.type || 'diffuse'}") has a texture map but no UVs — dropping map, using base color` );
868
- material = sharedMaterial.clone();
1113
+ material = cloneMaterial( sharedMaterial );
869
1114
  material.map = null;
870
1115
  this._noUVMaterials.set( sharedMaterial, material );
871
1116
 
@@ -1119,6 +1364,7 @@ export class PBRTSceneBuilder {
1119
1364
  case 'plymesh': return this._plyMesh( shape.params );
1120
1365
  case 'sphere': return this._sphere( shape.params );
1121
1366
  case 'disk': return this._disk( shape.params );
1367
+ case 'cylinder': return this._cylinder( shape.params );
1122
1368
  default:
1123
1369
  this.warn( `shape "${shape.type}" not supported — skipped` );
1124
1370
  return null;
@@ -1229,28 +1475,75 @@ export class PBRTSceneBuilder {
1229
1475
  }
1230
1476
 
1231
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
+ */
1232
1482
  _bilinearMesh( params ) {
1233
1483
 
1234
1484
  const P = params.P?.value;
1235
- const quad = params.indices?.value;
1485
+ const quad = params.indices?.value ?? ( P?.length === 12 ? [ 0, 1, 2, 3 ] : null );
1236
1486
  if ( ! P || ! quad ) {
1237
1487
 
1238
1488
  this.warn( 'bilinearmesh missing P/indices' ); return null;
1239
1489
 
1240
1490
  }
1241
1491
 
1242
- const tris = [];
1243
- 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 ) {
1244
1498
 
1245
- const [ a, b, c, d ] = [ quad[ i ], quad[ i + 1 ], quad[ i + 2 ], quad[ i + 3 ] ];
1246
- tris.push( a, b, c, a, c, d );
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 ++ ) {
1523
+
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 ) );
1247
1542
 
1248
1543
  }
1249
1544
 
1250
- const geo = new BufferGeometry();
1251
- geo.setAttribute( 'position', new Float32BufferAttribute( Float32Array.from( P ), 3 ) );
1252
- geo.setIndex( tris );
1253
- geo.computeVertexNormals();
1545
+ if ( N ) geo.userData.pbrtNormals = true;
1546
+ else geo.computeVertexNormals();
1254
1547
  return geo;
1255
1548
 
1256
1549
  }
@@ -1297,38 +1590,41 @@ export class PBRTSceneBuilder {
1297
1590
 
1298
1591
  _sphere( params ) {
1299
1592
 
1300
- const radius = pFloat( params, 'radius', 1 );
1301
- 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
+ } );
1302
1615
 
1303
1616
  }
1304
1617
 
1305
1618
  _disk( params ) {
1306
1619
 
1307
- // Approximate as a thin ring/disk in the z=height plane.
1308
- const radius = pFloat( params, 'radius', 1 );
1309
- const inner = pFloat( params, 'innerradius', 0 );
1310
- const h = pFloat( params, 'height', 0 );
1311
- const seg = 48;
1312
- const pos = [];
1313
- const idx = [];
1314
- for ( let i = 0; i < seg; i ++ ) {
1315
-
1316
- const a0 = ( i / seg ) * Math.PI * 2;
1317
- const a1 = ( ( i + 1 ) / seg ) * Math.PI * 2;
1318
- const base = pos.length / 3;
1319
- pos.push( Math.cos( a0 ) * inner, Math.sin( a0 ) * inner, h );
1320
- pos.push( Math.cos( a0 ) * radius, Math.sin( a0 ) * radius, h );
1321
- pos.push( Math.cos( a1 ) * radius, Math.sin( a1 ) * radius, h );
1322
- pos.push( Math.cos( a1 ) * inner, Math.sin( a1 ) * inner, h );
1323
- 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 ) => {
1324
1622
 
1325
- }
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 );
1326
1626
 
1327
- const geo = new BufferGeometry();
1328
- geo.setAttribute( 'position', new Float32BufferAttribute( Float32Array.from( pos ), 3 ) );
1329
- geo.setIndex( idx );
1330
- geo.computeVertexNormals();
1331
- return geo;
1627
+ } );
1332
1628
 
1333
1629
  }
1334
1630
 
@@ -1336,8 +1632,75 @@ export class PBRTSceneBuilder {
1336
1632
 
1337
1633
  async _getMaterial( shape ) {
1338
1634
 
1339
- const material = await this._surfaceMaterial( shape );
1340
- return shape.interior ? this._inMedium( material, shape.interior ) : material;
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 );
1341
1704
 
1342
1705
  }
1343
1706
 
@@ -1380,6 +1743,17 @@ export class PBRTSceneBuilder {
1380
1743
 
1381
1744
  },
1382
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
+ },
1383
1757
  resolveNormalMap: async ( filename ) => {
1384
1758
 
1385
1759
  const image = await this._image( filename );
@@ -1429,7 +1803,7 @@ export class PBRTSceneBuilder {
1429
1803
  const max = Math.max( ...sigma );
1430
1804
  if ( ! ( max > 0 ) ) return material;
1431
1805
 
1432
- const variant = material.clone();
1806
+ const variant = cloneMaterial( material );
1433
1807
  variant.attenuationDistance = 1 / max;
1434
1808
  variant.attenuationColor.setRGB( ...sigma.map( ( v ) => Math.exp( - v / max ) ) );
1435
1809
  return this._dedupeMaterial( variant );
@@ -1445,11 +1819,12 @@ export class PBRTSceneBuilder {
1445
1819
 
1446
1820
  const c = material.color, e = material.emissive;
1447
1821
  const key = `${c.r},${c.g},${c.b}|${e.r},${e.g},${e.b}|${material.emissiveIntensity}|` +
1448
- `${material.map ? material.map.uuid : '-'}|${material.roughness}|${material.metalness}|` +
1822
+ `${material.map ? material.map.uuid : '-'}|${material.roughness}|${material.roughnessMap?.uuid ?? '-'}|${material.metalness}|` +
1449
1823
  `${material.transmission}|${material.ior}|${material.thickness}|` +
1450
- `${material.clearcoat}|${material.clearcoatRoughness}|${material.opacity}|${material.side}|` +
1824
+ `${material.clearcoat}|${material.clearcoatRoughness}|${material.clearcoatRoughnessMap?.uuid ?? '-'}|${material.opacity}|${material.side}|` +
1451
1825
  `${material.normalMap ? material.normalMap.uuid : '-'}|${material.attenuationDistance}|${material.attenuationColor.toArray()}|` +
1452
- `${material.specularIntensity}|${material.diffuseTransmission ?? 0}|${material.diffuseTransmissionColor?.toArray() ?? '-'}`;
1826
+ `${material.specularIntensity}|${material.diffuseTransmission ?? 0}|${material.diffuseTransmissionColor?.toArray() ?? '-'}|` +
1827
+ `${material.subsurface ?? 0}|${material.subsurfaceColor?.toArray() ?? '-'}|${material.subsurfaceRadius ?? '-'}|${material.subsurfaceAnisotropy ?? 0}`;
1453
1828
 
1454
1829
  const existing = this._materialBySignature.get( key );
1455
1830
  if ( existing ) return existing;
@@ -1458,6 +1833,23 @@ export class PBRTSceneBuilder {
1458
1833
 
1459
1834
  }
1460
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
+
1461
1853
  async _applyAreaLight( material, areaLight, ctx ) {
1462
1854
 
1463
1855
  const rgb = await lightRGB( areaLight.params, 'L', ctx );
@@ -1494,6 +1886,10 @@ export class PBRTSceneBuilder {
1494
1886
  if ( v && v.type === 'rgb' ) result = { constant: [ v.value[ 0 ], v.value[ 1 ], v.value[ 2 ] ] };
1495
1887
  else if ( v ) result = { constant: [ v.value[ 0 ], v.value[ 0 ], v.value[ 0 ] ] };
1496
1888
 
1889
+ } else if ( def.class === 'checkerboard' || def.class === 'bilerp' ) {
1890
+
1891
+ result = await this._proceduralTexture( name, def );
1892
+
1497
1893
  } else if ( def.class === 'scale' ) {
1498
1894
 
1499
1895
  // Scale = inner_texture * scale_factor. Resolve the inner (recursively if
@@ -1523,6 +1919,52 @@ export class PBRTSceneBuilder {
1523
1919
 
1524
1920
  }
1525
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
+
1526
1968
  /** A file's image, decoded once however many named textures read it. @private */
1527
1969
  _image( filename ) {
1528
1970
 
@@ -1575,12 +2017,32 @@ export class PBRTSceneBuilder {
1575
2017
 
1576
2018
  const color = eightBit( image, { srgb: true } );
1577
2019
  const texture = color === image ? image.clone() : color;
2020
+ texture.userData.pbrtImage = filename;
1578
2021
  setMapping( texture, uvMapping( def.params ) );
1579
2022
  texture.needsUpdate = true;
1580
2023
  return scale < 1 ? { texture, constant: [ scale, scale, scale ] } : { texture };
1581
2024
 
1582
2025
  }
1583
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
+
1584
2046
  /** pbrt's `mix` texture, ( 1 − amount ) · tex1 + amount · tex2, baked when any of the three is an image. @private */
1585
2047
  async _mixTexture( name, def ) {
1586
2048
 
@@ -1617,7 +2079,7 @@ export class PBRTSceneBuilder {
1617
2079
  * A named texture as pbrt evaluates it, for baking: `{ rgb }` for a constant, `{ layer, tint? }` for an image;
1618
2080
  * null when it cannot be read in this runtime.
1619
2081
  * @param {string} name
1620
- * @param {number} channels - 1 where it is read as a float texture (its red channel), 3 as a spectrum
2082
+ * @param {number} channels - 1 where it is read as a float texture, 3 as a spectrum
1621
2083
  * @private
1622
2084
  */
1623
2085
  _bakeTerm( name, channels ) {
@@ -1798,6 +2260,8 @@ export class PBRTSceneBuilder {
1798
2260
  camera.name = 'PBRT Camera';
1799
2261
  this._poseCamera( camera, cam.cameraToWorld );
1800
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 ) );
1801
2265
  return camera;
1802
2266
 
1803
2267
  }