rayzee 11.8.0 → 11.9.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 (53) hide show
  1. package/README.md +18 -6
  2. package/dist/{ArchiveImporter-_sJm5uaK.js → ArchiveImporter-BJxszU_k.js} +1549 -1232
  3. package/dist/{BidirectionalIntegrator-Dzt4Z97x.js → BidirectionalIntegrator-DWg66vAd.js} +240 -240
  4. package/dist/{BufferLayout-xPRH9jYD.js → BufferLayout-Bzgccg1o.js} +1 -0
  5. package/dist/{ColorManagement-30MfpHwQ.js → ColorManagement-B_HjAiy2.js} +1 -1
  6. package/dist/{EngineIssues-xljoXM0E.js → EngineIssues-CfJiBaNj.js} +1 -0
  7. package/dist/{NeuralRetouch-BKQCsmEs.js → NeuralRetouch-BD9E-Yht.js} +1 -1
  8. package/dist/{NeuralSuperRes-C4ymq68e.js → NeuralSuperRes-C8Mf3tJX.js} +1 -1
  9. package/dist/{PathTracer-C_to_i40.js → PathTracer-DeE9UJ1n.js} +3433 -3363
  10. package/dist/{PhysicalSky-bMHbm_7H.js → PhysicalSky-cKy75-oR.js} +45 -45
  11. package/dist/{RayzeeRenderer-B_aB6VTJ.js → RayzeeRenderer-CzLyBA3G.js} +13 -8
  12. package/dist/{SceneMetadata-CSmlJW4c.js → SceneMetadata-C17UfOEp.js} +1 -1
  13. package/dist/{SolarLimb-bZE6y63S.js → SolarLimb-DRYy8tln.js} +184 -172
  14. package/dist/addons/archives.es.js +1 -1
  15. package/dist/addons/bidirectional.es.js +1 -1
  16. package/dist/addons/color.es.js +1 -1
  17. package/dist/addons/physical-sky.es.js +1 -1
  18. package/dist/addons/storage.es.js +1 -1
  19. package/dist/{openStorage-B085D9FR.js → openStorage-CdyVjmps.js} +1 -1
  20. package/dist/rayzee-core.es.js +7 -7
  21. package/dist/rayzee.es.js +567 -559
  22. package/dist/rayzee.umd.js +106 -101
  23. package/dist/{sceneCachePolicy-D_uBSbQF.js → sceneCachePolicy-ywbGKHD3.js} +9 -6
  24. package/package.json +1 -1
  25. package/src/EngineDefaults.js +1 -0
  26. package/src/EngineIssues.js +1 -0
  27. package/src/Passes/OIDNTemporalHistory.js +1 -1
  28. package/src/Processor/ArchiveImporter.js +83 -21
  29. package/src/Processor/AssetLoader.js +7 -0
  30. package/src/Processor/BufferLayout.js +3 -3
  31. package/src/Processor/GeometryExtractor.js +12 -4
  32. package/src/Processor/MaterialPacking.js +2 -2
  33. package/src/Processor/PBRT/PBRTMaterials.js +47 -12
  34. package/src/Processor/PBRT/PBRTParser.js +144 -22
  35. package/src/Processor/PBRT/PBRTSceneBuilder.js +287 -67
  36. package/src/Processor/PBRT/PBRTTextureBake.js +191 -9
  37. package/src/Processor/PBRT/index.js +19 -4
  38. package/src/Processor/SceneProcessor.js +42 -26
  39. package/src/RayzeeRenderer.js +4 -2
  40. package/src/Stages/DenoiserSettings.js +0 -4
  41. package/src/Stages/NRD.js +53 -24
  42. package/src/Stages/NormalDepth.js +5 -3
  43. package/src/Stages/PathTracer.js +2 -0
  44. package/src/Stages/PathTracerStage.js +5 -1
  45. package/src/Storage/SceneGraphCodec.js +6 -3
  46. package/src/TSL/BVHTraversal.js +33 -20
  47. package/src/TSL/Common.js +25 -8
  48. package/src/TSL/Displacement.js +66 -113
  49. package/src/TSL/LightsDirect.js +33 -11
  50. package/src/TSL/SceneResources.js +8 -0
  51. package/src/TSL/ShadeKernel.js +2 -2
  52. package/src/TSL/Struct.js +1 -0
  53. package/src/managers/MaterialDataManager.js +11 -0
@@ -24,8 +24,9 @@ import {
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, hasAlpha } from './PBRTTextureBake.js';
27
+ import { makeLayer, layerMean, bake, mixOf, srgbToLinear, linearToSRGB, hasAlpha, generateUV, uvKey } from './PBRTTextureBake.js';
28
28
  import { loopSubdivide } from './LoopSubdivision.js';
29
+ import { approximated, param } from './PBRTParser.js';
29
30
  import { octahedralToEquirect } from './EqualAreaOctahedral.js';
30
31
  import { tessellateCurve, tessellateCurves } from './PBRTCurves.js';
31
32
  import * as M from './PBRTMath.js';
@@ -36,7 +37,7 @@ const LAMP_TYPES = new Set( [ 'distant', 'point', 'spot' ] );
36
37
  // pbrt scales a light's spectrum to luminance 1 unless it is RGB, so a blackbody of any temperature is equally bright.
37
38
  async function lightRGB( params, name, ctx ) {
38
39
 
39
- const p = params[ name ];
40
+ const p = param( params, name );
40
41
  if ( p?.type === 'spectrum' && typeof p.value[ 0 ] === 'string' && p.value[ 0 ].startsWith( 'stdillum' ) ) return [ 1, 1, 1 ];
41
42
  const rgb = ( await resolveSpectrum( params, name, ctx, [ 1, 1, 1 ] ) ).rgb || [ 1, 1, 1 ];
42
43
  if ( ! p || p.type === 'rgb' || p.type === 'color' || p.type === 'float' ) return rgb;
@@ -98,6 +99,8 @@ export { SPILL_TRIANGLE_BUDGET, SPILL_PLACEMENT_BUDGET } from '../HostMemory.js'
98
99
  const DEFAULT_MERGE_MIN_SHAPES = 256;
99
100
  const MERGE_VERTEX_LIMIT = 4096;
100
101
  const MERGE_BATCH_TRIANGLES = 500_000;
102
+ // Copies of a template shape textured by position, each stored with its own uv, up to this many triangles in all.
103
+ const MAPPED_COPY_TRIANGLES = 1_000_000;
101
104
  // Curve tessellation defaults. pbrt sweeps a spline analytically; a triangle tracer has to
102
105
  // pick a resolution, and isMountainB alone declares 5.18M curves — two samples per span and
103
106
  // a crossed pair of ribbons is 16 triangles a curve.
@@ -108,7 +111,7 @@ const DEFAULT_CURVE_SIDES = { flat: 1, ribbon: 1, cylinder: 2 };
108
111
  const DEFAULT_CURVE_TOLERANCE = 0.05;
109
112
 
110
113
  /** Bumped whenever the same scene files build a different graph, so a stored graph is not reused. */
111
- export const PBRT_BUILD_REVISION = 11;
114
+ export const PBRT_BUILD_REVISION = 13;
112
115
 
113
116
  function samePlacements( a, b ) {
114
117
 
@@ -209,10 +212,62 @@ function uvMapping( params ) {
209
212
 
210
213
  }
211
214
 
212
- function setMapping( texture, { su, sv, du, dv } ) {
215
+ const POSITION_MAPPINGS = new Set( [ 'planar', 'spherical', 'cylindrical' ] );
216
+
217
+ function pVector( params, name, dflt ) {
218
+
219
+ const v = param( params, name )?.value;
220
+ return v && v.length >= 3 ? [ v[ 0 ], v[ 1 ], v[ 2 ] ] : dflt;
221
+
222
+ }
223
+
224
+ /**
225
+ * pbrt's TextureMapping2D::Create: `uv` scales and shifts the mesh's uv; a position mapping is a uv set of its own (`uv`),
226
+ * generated per shape and read as it is. It maps from where the texture was declared (`ctm`).
227
+ */
228
+ function textureMapping( params, ctm ) {
229
+
230
+ const type = pString( params, 'mapping', 'uv' );
231
+ if ( ! POSITION_MAPPINGS.has( type ) ) return { ...uvMapping( params ), uv: null };
232
+
233
+ const uv = { type, textureFromWorld: M.invert( ctm ?? M.identity() ) };
234
+ if ( type === 'planar' ) Object.assign( uv, {
235
+ vs: pVector( params, 'v1', [ 1, 0, 0 ] ), vt: pVector( params, 'v2', [ 0, 1, 0 ] ),
236
+ ds: pFloat( params, 'udelta', 0 ), dt: pFloat( params, 'vdelta', 0 )
237
+ } );
238
+ uv.key = JSON.stringify( uv );
239
+ return { su: 1, sv: 1, du: 0, dv: 0, uv };
240
+
241
+ }
242
+
243
+ function setMapping( texture, { su, sv, du, dv, uv = null } ) {
213
244
 
214
245
  texture.repeat.set( su, sv );
215
246
  texture.offset.set( du, dv );
247
+ if ( uv ) texture.userData.pbrtUV = uv;
248
+
249
+ }
250
+
251
+ // Material maps in the order that picks a surface's one uv set: its base colour's, else the first it has.
252
+ const UV_MAPS = [ 'map', 'roughnessMap', 'clearcoatRoughnessMap', 'normalMap' ];
253
+ const MAP_SCALARS = { roughnessMap: 'roughness', clearcoatRoughnessMap: 'clearcoatRoughness' };
254
+
255
+ /** The position mapping a material's textures are read in, or null for the mesh's own uv. */
256
+ function uvSetOf( material ) {
257
+
258
+ for ( const slot of UV_MAPS ) if ( material[ slot ] ) return material[ slot ].userData.pbrtUV ?? null;
259
+ return null;
260
+
261
+ }
262
+
263
+ // An attribute with `sources`' values appended, one copy each.
264
+ function withCopies( attribute, sources ) {
265
+
266
+ const { array, itemSize, count } = attribute;
267
+ const out = new array.constructor( ( count + sources.length ) * itemSize );
268
+ out.set( array.subarray( 0, count * itemSize ) );
269
+ for ( let i = 0; i < sources.length; i ++ ) for ( let c = 0; c < itemSize; c ++ ) out[ ( count + i ) * itemSize + c ] = array[ sources[ i ] * itemSize + c ];
270
+ return new BufferAttribute( out, itemSize, attribute.normalized );
216
271
 
217
272
  }
218
273
 
@@ -464,8 +519,9 @@ function translucent( material, alpha ) {
464
519
 
465
520
  }
466
521
 
467
- const sameMapping = ( texture, { su, sv, du, dv } ) =>
468
- texture.repeat.x === su && texture.repeat.y === sv && texture.offset.x === du && texture.offset.y === dv;
522
+ const sameMapping = ( texture, { su, sv, du, dv, uv } ) =>
523
+ texture.repeat.x === su && texture.repeat.y === sv && texture.offset.x === du && texture.offset.y === dv &&
524
+ uvKey( texture.userData.pbrtUV ) === uvKey( uv );
469
525
 
470
526
  // An 8-bit image's colour with its alpha decoded as pbrt reads it; null when its alpha is opaque throughout.
471
527
  function withDecodedAlpha( pixels, decode ) {
@@ -547,6 +603,7 @@ export class PBRTSceneBuilder {
547
603
  this.resolveImage = resolvers.resolveImage || ( async () => null );
548
604
  this.resolveEnvironment = resolvers.resolveEnvironment || resolvers.resolveImage || ( async () => null );
549
605
  this.convertHandedness = resolvers.convertHandedness === true;
606
+ this._usage = resolvers.usage ?? null;
550
607
 
551
608
  this.warnings = [];
552
609
  this.report = []; // per-mesh diagnostics for debugging imports
@@ -560,6 +617,9 @@ export class PBRTSceneBuilder {
560
617
  this._termCache = new Map(); // texture name + channels -> Promise<{rgb}|{layer, tint?}|null>
561
618
  this._mediumMaterials = new Map(); // material -> Map(medium name -> material with its attenuation)
562
619
  this._alphaMaterials = new Map(); // material -> Map(alpha texture name or #value -> Promise<material>)
620
+ this._mappings = new Map(); // texture name -> its textureMapping()
621
+ this._oneUVSets = new WeakMap(); // material -> itself, or its maps read in one uv set
622
+ this._uvWarnings = new Set();
563
623
 
564
624
  }
565
625
 
@@ -576,9 +636,15 @@ export class PBRTSceneBuilder {
576
636
  async build( ir ) {
577
637
 
578
638
  this.ir = ir;
579
- // Built again for their moving placements after the static ones, so never freed early.
639
+ // Built again for their moving placements, so freed and accounted for only at the end.
580
640
  this._keepShapes = new Set();
581
- for ( const { name } of ir.animatedInstances ?? [] ) for ( const shape of ir.objects.get( name ) ?? [] ) this._keepShapes.add( shape );
641
+ for ( const { name } of ir.animatedInstances ?? [] ) for ( const shape of ir.objects.get( name ) ?? [] ) {
642
+
643
+ this._keepShapes.add( shape );
644
+ this._usage?.open( shape.params, `Shape ${shape.type}`, 'alpha' );
645
+
646
+ }
647
+
582
648
  // One decoded .ply serves every shape naming it: a merge frees it only as its last direct
583
649
  // user, and never while a template or an unmerged shape holds its geometry.
584
650
  this._plyUsers = new Map();
@@ -635,7 +701,7 @@ export class PBRTSceneBuilder {
635
701
 
636
702
  if ( shape.type === 'plymesh' ) this._plyHeld.add( pString( shape.params, 'filename', null ) );
637
703
 
638
- const mesh = this._meshFromGeometry( shape, geometry, sharedMaterial, shape.ctm, `shape_${i}` );
704
+ const mesh = this._meshFromGeometry( shape, this._mappedGeometry( geometry, sharedMaterial, shape.ctm ), sharedMaterial, shape.ctm, `shape_${i}` );
639
705
  if ( ! mesh ) continue;
640
706
  group.add( mesh );
641
707
  if ( shape.motion ) this._animate( mesh, shape.motion );
@@ -675,6 +741,7 @@ export class PBRTSceneBuilder {
675
741
 
676
742
  const animations = ir.animation ? this._buildClip( ir, camera ) : [];
677
743
 
744
+ for ( const light of ir.lights ) this._usage?.open( light.params, `LightSource ${light.type}` );
678
745
  // Infinite light → environment
679
746
  const environment = await this._buildEnvironment( ir.lights );
680
747
  await this._buildLamps( ir.lights, group );
@@ -828,16 +895,23 @@ export class PBRTSceneBuilder {
828
895
 
829
896
  }
830
897
 
831
- const [ geometry, sharedMaterial ] = await Promise.all( [
898
+ const [ built, sharedMaterial ] = await Promise.all( [
832
899
  this._buildGeometry( shape ),
833
900
  this._getMaterial( shape )
834
901
  ] );
835
- if ( ! geometry ) continue;
902
+ if ( ! built ) continue;
903
+ let geometry = built;
904
+
905
+ const rel = shape.relativeCTM || shape.ctm;
906
+ if ( uvSetOf( sharedMaterial ) ) {
836
907
 
837
- if ( batches && shape.type !== 'plymesh' && ! shape.shared && geometry.getAttribute( 'position' ).count <= MERGE_VERTEX_LIMIT ) {
908
+ if ( this._placeMapped( list, shape, geometry, sharedMaterial, rel, group, name ) ) continue;
909
+ geometry = this._mappedGeometry( geometry, sharedMaterial, M.multiplyInto( new Array( 16 ), list.matrices, 0, rel ) );
910
+
911
+ } else if ( batches && shape.type !== 'plymesh' && ! shape.shared && geometry.getAttribute( 'position' ).count <= MERGE_VERTEX_LIMIT ) {
838
912
 
839
913
  this._geometryCache.delete( shape );
840
- const full = this._appendToBatch( batches, shape, geometry, sharedMaterial, shape.relativeCTM || shape.ctm );
914
+ const full = this._appendToBatch( batches, shape, geometry, sharedMaterial, rel );
841
915
  this._releaseMerged( shape, geometry );
842
916
  placeBatch( list, full );
843
917
  continue;
@@ -846,7 +920,7 @@ export class PBRTSceneBuilder {
846
920
 
847
921
  const tris = this._accountGeometry( geometry );
848
922
  const material = this._materialForGeometry( shape, geometry, sharedMaterial, `instances_${name}` );
849
- place( list, geometry, material, shape.relativeCTM || shape.ctm, shape.type, shape.material?.type || 'diffuse', tris );
923
+ place( list, geometry, material, rel, shape.type, shape.material?.type || 'diffuse', tris );
850
924
 
851
925
  }
852
926
 
@@ -865,6 +939,34 @@ export class PBRTSceneBuilder {
865
939
 
866
940
  }
867
941
 
942
+ /**
943
+ * pbrt maps each copy of a template where it stands, so a shape textured by position is placed as a mesh a copy, each
944
+ * with its own uv set. False, placing nothing, past MAPPED_COPY_TRIANGLES: the copies then share the first one's.
945
+ * @private
946
+ */
947
+ _placeMapped( list, shape, geometry, material, rel, group, name ) {
948
+
949
+ const tris = ( geometry.index ? geometry.index.count : geometry.getAttribute( 'position' ).count ) / 3;
950
+ if ( list.count * tris > MAPPED_COPY_TRIANGLES ) {
951
+
952
+ this.warn( `ObjectInstance "${name}": its ${list.count.toLocaleString()} copies share the first one's uv from a position-mapped texture` );
953
+ return false;
954
+
955
+ }
956
+
957
+ let i = 0;
958
+ for ( ; i < list.count && ! this._overBudget(); i ++ ) {
959
+
960
+ const world = M.multiplyInto( new Array( 16 ), list.matrices, i * 16, rel );
961
+ group.add( this._meshFromGeometry( shape, this._mappedGeometry( geometry, material, world ), material, world, `instances_${name}_${i}` ) );
962
+
963
+ }
964
+
965
+ this.skippedForBudget += list.count - i;
966
+ return true;
967
+
968
+ }
969
+
868
970
  /**
869
971
  * Templates placed at exactly the same transforms, as one template. Each Pandanus tree
870
972
  * includes ten leaf files; kept apart they were ten overlapping instances a ray entered
@@ -942,7 +1044,9 @@ export class PBRTSceneBuilder {
942
1044
  const shape = template[ s ];
943
1045
  const [ geometry, sharedMaterial ] = await Promise.all( [ this._buildGeometry( shape ), this._getMaterial( shape ) ] );
944
1046
  if ( ! geometry ) continue;
945
- const mesh = this._meshFromGeometry( shape, geometry, sharedMaterial, shape.relativeCTM || shape.ctm, `${node.name}_${s}`, true );
1047
+ const rel = shape.relativeCTM || shape.ctm;
1048
+ const mapped = this._mappedGeometry( geometry, sharedMaterial, M.multiply( start, rel ) );
1049
+ const mesh = this._meshFromGeometry( shape, mapped, sharedMaterial, rel, `${node.name}_${s}`, true );
946
1050
  if ( mesh ) node.add( mesh );
947
1051
 
948
1052
  }
@@ -1145,7 +1249,7 @@ export class PBRTSceneBuilder {
1145
1249
 
1146
1250
  this.reportedMeshes ++;
1147
1251
  const world = this.convertHandedness ? M.multiply( FLIP_Z, shape.ctm ) : shape.ctm;
1148
- const full = this._appendToBatch( this._batches, shape, geometry, sharedMaterial, world );
1252
+ const full = this._appendToBatch( this._batches, shape, this._mappedGeometry( geometry, sharedMaterial, shape.ctm ), sharedMaterial, world );
1149
1253
  this._releaseMerged( shape, geometry );
1150
1254
  if ( full ) this._flushBatch( full, group );
1151
1255
 
@@ -1322,7 +1426,9 @@ export class PBRTSceneBuilder {
1322
1426
 
1323
1427
  async _createGeometry( shape ) {
1324
1428
 
1325
- const geometry = await this._shapeGeometry( shape );
1429
+ // A shape's parameters are all read before the first await; its `alpha`, by its material.
1430
+ const read = () => this._shapeGeometry( shape );
1431
+ const geometry = await ( this._usage ? this._usage.translate( shape.params, `Shape ${shape.type}`, read, 'alpha' ) : read() );
1326
1432
  return geometry && shape.areaLight && ! pBool( shape.areaLight.params, 'twosided', false ) ? this._facingEmission( shape, geometry ) : geometry;
1327
1433
 
1328
1434
  }
@@ -1395,7 +1501,7 @@ export class PBRTSceneBuilder {
1395
1501
 
1396
1502
  _triangleMesh( params ) {
1397
1503
 
1398
- const P = params.P?.value;
1504
+ const P = param( params, 'P' )?.value;
1399
1505
  if ( ! P || P.length < 9 ) {
1400
1506
 
1401
1507
  this.warn( 'trianglemesh missing P' ); return null;
@@ -1405,7 +1511,7 @@ export class PBRTSceneBuilder {
1405
1511
  const geo = new BufferGeometry();
1406
1512
  geo.setAttribute( 'position', new Float32BufferAttribute( float32( P ), 3 ) );
1407
1513
 
1408
- const N = params.N?.value;
1514
+ const N = param( params, 'N' )?.value;
1409
1515
  if ( N && N.length === P.length ) {
1410
1516
 
1411
1517
  geo.setAttribute( 'normal', new Float32BufferAttribute( float32( N ), 3 ) );
@@ -1413,10 +1519,10 @@ export class PBRTSceneBuilder {
1413
1519
 
1414
1520
  }
1415
1521
 
1416
- const uv = ( params.uv || params.st )?.value;
1522
+ const uv = param( params, params.uv ? 'uv' : 'st' )?.value;
1417
1523
  if ( uv && uv.length === ( P.length / 3 ) * 2 ) geo.setAttribute( 'uv', new Float32BufferAttribute( float32( uv ), 2 ) );
1418
1524
 
1419
- const indices = params.indices?.value;
1525
+ const indices = param( params, 'indices' )?.value;
1420
1526
  if ( indices && indices.length ) geo.setIndex( indexAttribute( indices ) );
1421
1527
 
1422
1528
  if ( ! N ) geo.computeVertexNormals();
@@ -1426,7 +1532,7 @@ export class PBRTSceneBuilder {
1426
1532
 
1427
1533
  _curve( params ) {
1428
1534
 
1429
- const P = params.P?.value;
1535
+ const P = param( params, 'P' )?.value;
1430
1536
  if ( ! P || P.length < 12 ) {
1431
1537
 
1432
1538
  this.warn( 'curve needs at least 4 control points' ); return null;
@@ -1447,7 +1553,7 @@ export class PBRTSceneBuilder {
1447
1553
  basis: pString( params, 'basis', 'bezier' ),
1448
1554
  width0: pFloat( params, 'width0', width ),
1449
1555
  width1: pFloat( params, 'width1', width ),
1450
- N: params.N?.value || null,
1556
+ N: param( params, 'N' )?.value || null,
1451
1557
  steps: this.curveSteps,
1452
1558
  sides: this.curveSides ?? DEFAULT_CURVE_SIDES[ type ] ?? 1,
1453
1559
  tolerance: this.curveTolerance
@@ -1473,11 +1579,11 @@ export class PBRTSceneBuilder {
1473
1579
  _curves( params ) {
1474
1580
 
1475
1581
  const built = tessellateCurves( {
1476
- P: params.P.value,
1477
- counts: params.counts.value,
1478
- widths: params.widths?.value ?? null,
1582
+ P: param( params, 'P' ).value,
1583
+ counts: param( params, 'counts' ).value,
1584
+ widths: param( params, 'widths' )?.value ?? null,
1479
1585
  widthMode: pString( params, 'widthMode', 'constant' ),
1480
- N: params.N?.value ?? null,
1586
+ N: param( params, 'N' )?.value ?? null,
1481
1587
  normalMode: pString( params, 'normalMode', 'vertex' ),
1482
1588
  type: pString( params, 'type', 'cubic' ),
1483
1589
  basis: pString( params, 'basis', 'bspline' ),
@@ -1506,8 +1612,8 @@ export class PBRTSceneBuilder {
1506
1612
 
1507
1613
  _loopSubdiv( params ) {
1508
1614
 
1509
- const P = params.P?.value;
1510
- const indices = params.indices?.value;
1615
+ const P = param( params, 'P' )?.value;
1616
+ const indices = param( params, 'indices' )?.value;
1511
1617
  if ( ! P || P.length < 9 || ! indices || indices.length < 3 ) {
1512
1618
 
1513
1619
  this.warn( 'loopsubdiv missing P/indices' ); return null;
@@ -1538,8 +1644,8 @@ export class PBRTSceneBuilder {
1538
1644
  */
1539
1645
  _bilinearMesh( params ) {
1540
1646
 
1541
- const P = params.P?.value;
1542
- const quad = params.indices?.value ?? ( P?.length === 12 ? [ 0, 1, 2, 3 ] : null );
1647
+ const P = param( params, 'P' )?.value;
1648
+ const quad = param( params, 'indices' )?.value ?? ( P?.length === 12 ? [ 0, 1, 2, 3 ] : null );
1543
1649
  if ( ! P || ! quad ) {
1544
1650
 
1545
1651
  this.warn( 'bilinearmesh missing P/indices' ); return null;
@@ -1547,8 +1653,8 @@ export class PBRTSceneBuilder {
1547
1653
  }
1548
1654
 
1549
1655
  const count = P.length / 3;
1550
- const uv = params.uv?.value?.length === count * 2 ? params.uv.value : null;
1551
- const N = params.N?.value?.length === count * 3 ? params.N.value : null;
1656
+ const uv = param( params, 'uv' )?.value?.length === count * 2 ? params.uv.value : null;
1657
+ const N = param( params, 'N' )?.value?.length === count * 3 ? params.N.value : null;
1552
1658
  const geo = new BufferGeometry();
1553
1659
 
1554
1660
  if ( uv ) {
@@ -1691,7 +1797,75 @@ export class PBRTSceneBuilder {
1691
1797
 
1692
1798
  let material = await this._surfaceMaterial( shape );
1693
1799
  if ( shape.interior ) material = await this._inMedium( material, shape.interior );
1694
- return shape.params.alpha ? this._withAlpha( material, shape.params.alpha ) : material;
1800
+ if ( shape.params.alpha ) material = await this._withAlpha( material, shape.params.alpha );
1801
+ return this._oneUVSet( material );
1802
+
1803
+ }
1804
+
1805
+ /**
1806
+ * The material with every map in its one uv set (`uvSetOf`): the engine reads one a triangle. A map in another
1807
+ * counts as its mean (a roughness) or is dropped, reported once.
1808
+ * @private
1809
+ */
1810
+ _oneUVSet( material ) {
1811
+
1812
+ let out = this._oneUVSets.get( material );
1813
+ if ( out ) return out;
1814
+ out = material;
1815
+ const set = uvSetOf( material );
1816
+ const what = ( uv ) => ( uv ? `a "${uv.type}" mapping` : 'the mesh\'s uv' );
1817
+
1818
+ for ( const slot of UV_MAPS ) {
1819
+
1820
+ const map = material[ slot ];
1821
+ if ( ! map || uvKey( map.userData.pbrtUV ) === uvKey( set ) ) continue;
1822
+ if ( out === material ) out = cloneMaterial( material );
1823
+ out[ slot ] = null;
1824
+ const scalar = MAP_SCALARS[ slot ];
1825
+ const layer = scalar && this._textureLayer( map, 1 );
1826
+ if ( layer ) out[ scalar ] *= layerMean( layer );
1827
+ const message = `a ${slot} in ${what( map.userData.pbrtUV )} on a surface textured in ${what( set )}: ` +
1828
+ `one uv set a surface — ${layer ? 'its mean used' : 'dropped'}`;
1829
+ if ( ! this._uvWarnings.has( message ) ) {
1830
+
1831
+ this._uvWarnings.add( message );
1832
+ this.warn( message );
1833
+
1834
+ }
1835
+
1836
+ }
1837
+
1838
+ this._oneUVSets.set( material, out );
1839
+ return out;
1840
+
1841
+ }
1842
+
1843
+ /**
1844
+ * `geometry` with the uv set its material's position mapping gives it at `world` (pbrt's, column-major); as it is
1845
+ * when the material reads the mesh's own uv. Attributes are shared unless a seam copied vertices.
1846
+ * @private
1847
+ */
1848
+ _mappedGeometry( geometry, material, world ) {
1849
+
1850
+ const mapping = uvSetOf( material );
1851
+ if ( ! mapping ) return geometry;
1852
+
1853
+ const { uv, index, extra } = generateUV(
1854
+ geometry.getAttribute( 'position' ).array, geometry.index?.array ?? null, M.multiply( mapping.textureFromWorld, world ), mapping
1855
+ );
1856
+ const out = new BufferGeometry();
1857
+ out.name = geometry.name;
1858
+ Object.assign( out.userData, geometry.userData );
1859
+ for ( const name in geometry.attributes ) {
1860
+
1861
+ if ( name !== 'uv' ) out.setAttribute( name, extra.length ? withCopies( geometry.getAttribute( name ), extra ) : geometry.getAttribute( name ) );
1862
+
1863
+ }
1864
+
1865
+ out.setAttribute( 'uv', new BufferAttribute( uv, 2 ) );
1866
+ if ( index ) out.setIndex( new Uint32BufferAttribute( index, 1 ) );
1867
+ else if ( geometry.index ) out.setIndex( geometry.index );
1868
+ return out;
1695
1869
 
1696
1870
  }
1697
1871
 
@@ -1734,17 +1908,18 @@ export class PBRTSceneBuilder {
1734
1908
  */
1735
1909
  async _alphaMap( colorMap, name ) {
1736
1910
 
1737
- const def = this.ir.namedTextures.get( name );
1911
+ const def = this._textureDef( name );
1738
1912
  const filename = def?.class === 'imagemap' ? pString( def.params, 'filename', null ) : null;
1913
+ const mapping = filename && this._textureMapping( name, def );
1739
1914
  // Most often the alpha is the colour map's own image: its colour stays as it is.
1740
- if ( filename && colorMap?.userData.pbrtImage === filename && pString( def.params, 'mapping', 'uv' ) === 'uv'
1741
- && pFloat( def.params, 'scale', 1 ) === 1 && ! pBool( def.params, 'invert', false ) && sameMapping( colorMap, uvMapping( def.params ) ) ) {
1915
+ if ( filename && colorMap?.userData.pbrtImage === filename
1916
+ && pFloat( def.params, 'scale', 1 ) === 1 && ! pBool( def.params, 'invert', false ) && sameMapping( colorMap, mapping ) ) {
1742
1917
 
1743
1918
  const pixels = texturePixels( colorMap );
1744
1919
  const own = pixels && withDecodedAlpha( pixels, decoderFor( def.params ) );
1745
1920
  if ( own ) {
1746
1921
 
1747
- setMapping( own, uvMapping( def.params ) );
1922
+ setMapping( own, mapping );
1748
1923
  return own;
1749
1924
 
1750
1925
  }
@@ -1756,7 +1931,7 @@ export class PBRTSceneBuilder {
1756
1931
  if ( ! alpha.layer ) return { constant: alpha.rgb[ 0 ] };
1757
1932
  const color = colorMap ? this._textureLayer( colorMap, 3 ) : null;
1758
1933
  if ( colorMap && ! color ) return null;
1759
- const baked = bake( [ color ? { layer: color } : { rgb: [ 1, 1, 1 ] }, alpha ], ( [ c ] ) => c, { alphaOf: ( [ , a ] ) => a[ 0 ] } );
1934
+ const baked = this._bake( `alpha "${name}"`, [ color ? { layer: color } : { rgb: [ 1, 1, 1 ] }, alpha ], ( [ c ] ) => c, { alphaOf: ( [ , a ] ) => a[ 0 ] } );
1760
1935
  return baked && bakedTexture( baked );
1761
1936
 
1762
1937
  }
@@ -1792,6 +1967,7 @@ export class PBRTSceneBuilder {
1792
1967
  return this._ctx ??= {
1793
1968
  resolveNamedTexture: ( n ) => this._resolveNamedTexture( n ),
1794
1969
  namedMaterials: this.ir.namedMaterials,
1970
+ trackParams: ( params, label, consumed ) => this._usage?.open( params, label, consumed ),
1795
1971
  warn: ( m ) => this.warn( m ),
1796
1972
  floatTextureMean: async ( n ) => {
1797
1973
 
@@ -1844,6 +2020,7 @@ export class PBRTSceneBuilder {
1844
2020
 
1845
2021
  const params = this.ir.media?.get( name );
1846
2022
  const type = params && pString( params, 'type', '' );
2023
+ this._usage?.open( params, `MakeNamedMedium ${type}`, 'type' );
1847
2024
  if ( type !== 'homogeneous' ) {
1848
2025
 
1849
2026
  this.warn( params ? `medium "${name}" (${type}) is not supported — "${name}" glass left clear` : `medium "${name}" not defined` );
@@ -1851,7 +2028,13 @@ export class PBRTSceneBuilder {
1851
2028
 
1852
2029
  }
1853
2030
 
1854
- if ( params.preset ) this.warn( `medium preset "${pString( params, 'preset', '' )}" is not supported — using sigma_a / sigma_s` );
2031
+ if ( params.preset ) {
2032
+
2033
+ this.warn( `medium preset "${pString( params, 'preset', '' )}" is not supported — using sigma_a / sigma_s` );
2034
+ approximated( params, 'preset' );
2035
+
2036
+ }
2037
+
1855
2038
  const ctx = this._materialContext();
1856
2039
  const a = ( await resolveSpectrum( params, 'sigma_a', ctx, [ 1, 1, 1 ] ) ).rgb;
1857
2040
  const s = ( await resolveSpectrum( params, 'sigma_s', ctx, [ 1, 1, 1 ] ) ).rgb;
@@ -1909,6 +2092,7 @@ export class PBRTSceneBuilder {
1909
2092
 
1910
2093
  async _applyAreaLight( material, areaLight, ctx ) {
1911
2094
 
2095
+ this._usage?.open( areaLight.params, `AreaLightSource ${areaLight.type}` );
1912
2096
  const rgb = await lightRGB( areaLight.params, 'L', ctx );
1913
2097
  const scale = pFloat( areaLight.params, 'scale', 1 );
1914
2098
  material.emissive.setRGB( rgb[ 0 ], rgb[ 1 ], rgb[ 2 ] );
@@ -1918,11 +2102,20 @@ export class PBRTSceneBuilder {
1918
2102
 
1919
2103
  }
1920
2104
 
2105
+ /** A named texture's definition, its parameters accounted from here to the end of the build. @private */
2106
+ _textureDef( name ) {
2107
+
2108
+ const def = this.ir.namedTextures.get( name );
2109
+ if ( def ) this._usage?.open( def.params, `Texture ${def.class}` );
2110
+ return def;
2111
+
2112
+ }
2113
+
1921
2114
  async _resolveNamedTexture( name ) {
1922
2115
 
1923
2116
  if ( this._textureCache.has( name ) ) return this._textureCache.get( name );
1924
2117
 
1925
- const def = this.ir.namedTextures.get( name );
2118
+ const def = this._textureDef( name );
1926
2119
  let result = null;
1927
2120
 
1928
2121
  if ( ! def ) {
@@ -1939,7 +2132,7 @@ export class PBRTSceneBuilder {
1939
2132
 
1940
2133
  } else if ( def.class === 'constant' ) {
1941
2134
 
1942
- const v = def.params.value;
2135
+ const v = param( def.params, 'value' );
1943
2136
  if ( v && v.type === 'rgb' ) result = { constant: [ v.value[ 0 ], v.value[ 1 ], v.value[ 2 ] ] };
1944
2137
  else if ( v ) result = { constant: [ v.value[ 0 ], v.value[ 0 ], v.value[ 0 ] ] };
1945
2138
 
@@ -1987,15 +2180,19 @@ export class PBRTSceneBuilder {
1987
2180
  const ctx = this._materialContext();
1988
2181
  const value = async ( key, dflt ) => {
1989
2182
 
1990
- const p = params[ key ];
1991
- if ( p?.type === 'texture' ) return ( await ctx.textureMeanRGB( p.value[ 0 ] ) ) ?? dflt;
2183
+ const p = param( params, key );
2184
+ if ( p?.type === 'texture' ) {
2185
+
2186
+ approximated( params, key );
2187
+ return ( await ctx.textureMeanRGB( p.value[ 0 ] ) ) ?? dflt;
2188
+
2189
+ }
2190
+
1992
2191
  return ( await resolveSpectrum( params, key, ctx, dflt ) ).rgb ?? dflt;
1993
2192
 
1994
2193
  };
1995
2194
 
1996
- const mapping = pString( params, 'mapping', 'uv' );
1997
- if ( mapping !== 'uv' ) this.warn( `texture "${name}": "${mapping}" mapping is not supported — using the mesh's uv` );
1998
- const { su, sv, du, dv } = uvMapping( params );
2195
+ const { su, sv, du, dv, uv } = this._textureMapping( name, def );
1999
2196
 
2000
2197
  if ( def.class === 'checkerboard' ) {
2001
2198
 
@@ -2009,7 +2206,7 @@ export class PBRTSceneBuilder {
2009
2206
  const [ a, b ] = await Promise.all( [ value( 'tex1', [ 1, 1, 1 ] ), value( 'tex2', [ 0, 0, 0 ] ) ] );
2010
2207
  const n = 2 * CHECK_TEXELS;
2011
2208
  const data = rasterize( n, n, ( s, t ) => ( ( Math.floor( 2 * s ) + Math.floor( 2 * t ) ) % 2 ? b : a ) );
2012
- return { texture: bakedTexture( { data, width: n, height: n, mapping: { su: su / 2, sv: sv / 2, du: du / 2, dv: dv / 2 } } ) };
2209
+ return { texture: bakedTexture( { data, width: n, height: n, mapping: { su: su / 2, sv: sv / 2, du: du / 2, dv: dv / 2, uv } } ) };
2013
2210
 
2014
2211
  }
2015
2212
 
@@ -2018,7 +2215,32 @@ export class PBRTSceneBuilder {
2018
2215
  ] );
2019
2216
  const data = rasterize( BILERP_TEXELS, BILERP_TEXELS, ( s, t ) => [ 0, 1, 2 ].map( ( c ) =>
2020
2217
  ( 1 - s ) * ( 1 - t ) * v00[ c ] + s * ( 1 - t ) * v10[ c ] + ( 1 - s ) * t * v01[ c ] + s * t * v11[ c ] ) );
2021
- return { texture: bakedTexture( { data, width: BILERP_TEXELS, height: BILERP_TEXELS, mapping: { su, sv, du, dv } } ) };
2218
+ return { texture: bakedTexture( { data, width: BILERP_TEXELS, height: BILERP_TEXELS, mapping: { su, sv, du, dv, uv } } ) };
2219
+
2220
+ }
2221
+
2222
+ /** A named texture's textureMapping(), once; a mapping pbrt does not know is reported and read as `uv`. @private */
2223
+ _textureMapping( name, def ) {
2224
+
2225
+ let mapping = this._mappings.get( name );
2226
+ if ( ! mapping ) {
2227
+
2228
+ const type = pString( def.params, 'mapping', 'uv' );
2229
+ if ( type !== 'uv' && ! POSITION_MAPPINGS.has( type ) ) this.warn( `texture "${name}": unknown mapping "${type}" — using the mesh's uv` );
2230
+ this._mappings.set( name, mapping = textureMapping( def.params, def.ctm ) );
2231
+
2232
+ }
2233
+
2234
+ return mapping;
2235
+
2236
+ }
2237
+
2238
+ /** bake(), reporting the inputs it had to read as their mean. @private */
2239
+ _bake( label, terms, combine, options ) {
2240
+
2241
+ const baked = bake( terms, combine, options );
2242
+ if ( baked?.averaged ) this.warn( `${label}: ${baked.averaged} input(s) in another uv set than the first image — read as their mean` );
2243
+ return baked;
2022
2244
 
2023
2245
  }
2024
2246
 
@@ -2059,14 +2281,12 @@ export class PBRTSceneBuilder {
2059
2281
 
2060
2282
  }
2061
2283
 
2062
- const mapping = pString( def.params, 'mapping', 'uv' );
2063
- if ( mapping !== 'uv' ) this.warn( `texture "${name}": "${mapping}" mapping is not supported — using the mesh's uv` );
2064
-
2284
+ const mapping = this._textureMapping( name, def );
2065
2285
  const scale = pFloat( def.params, 'scale', 1 );
2066
2286
  if ( scale > 1 || pBool( def.params, 'invert', false ) ) {
2067
2287
 
2068
2288
  const term = await this._bakeTerm( name, 3 );
2069
- const baked = term?.layer && bake( [ { ...term, clamp: true } ], ( [ v ] ) => v );
2289
+ const baked = term?.layer && this._bake( `texture "${name}"`, [ { ...term, clamp: true } ], ( [ v ] ) => v );
2070
2290
  if ( baked ) return { texture: bakedTexture( baked ) };
2071
2291
  this.warn( `texture "${name}": its scale or invert could not be baked here — ignored` );
2072
2292
 
@@ -2075,7 +2295,7 @@ export class PBRTSceneBuilder {
2075
2295
  const color = eightBit( image, { srgb: true } );
2076
2296
  const texture = color === image ? image.clone() : color;
2077
2297
  texture.userData.pbrtImage = filename;
2078
- setMapping( texture, uvMapping( def.params ) );
2298
+ setMapping( texture, mapping );
2079
2299
  texture.needsUpdate = true;
2080
2300
  return scale < 1 ? { texture, constant: [ scale, scale, scale ] } : { texture };
2081
2301
 
@@ -2090,7 +2310,7 @@ export class PBRTSceneBuilder {
2090
2310
 
2091
2311
  const terms = await Promise.all( keys.map( ( key ) => this._paramTerm( params, key, 1, [ 0, 0, 0 ] ) ) );
2092
2312
  if ( terms.some( ( term ) => ! term ) ) return null;
2093
- const baked = bake( terms, ( values ) => {
2313
+ const baked = this._bake( `"${keys.join( '", "' )}"`, terms, ( values ) => {
2094
2314
 
2095
2315
  const v = combine( values.map( ( value ) => value[ 0 ] ) );
2096
2316
  return [ v, v, v ];
@@ -2116,7 +2336,7 @@ export class PBRTSceneBuilder {
2116
2336
  }
2117
2337
 
2118
2338
  const unit = ( rgb ) => rgb.map( ( v ) => Math.min( 1, Math.max( 0, v ) ) );
2119
- const baked = bake( [ { ...a, clamp: true }, { ...b, clamp: true }, amount ], mixOf );
2339
+ const baked = this._bake( `texture "${name}"`, [ { ...a, clamp: true }, { ...b, clamp: true }, amount ], mixOf );
2120
2340
  return baked ? { texture: bakedTexture( baked ) } : { constant: mixOf( [ unit( a.rgb ), unit( b.rgb ), amount.rgb ] ) };
2121
2341
 
2122
2342
  }
@@ -2124,7 +2344,7 @@ export class PBRTSceneBuilder {
2124
2344
  /** A texture parameter as a bake term: a constant, or the texture it names. @private */
2125
2345
  async _paramTerm( params, key, channels, dflt ) {
2126
2346
 
2127
- const p = params[ key ];
2347
+ const p = param( params, key );
2128
2348
  if ( ! p ) return { rgb: dflt };
2129
2349
  if ( p.type === 'texture' ) return this._bakeTerm( p.value[ 0 ], channels );
2130
2350
  const rgb = ( await resolveSpectrum( params, key, this._materialContext(), dflt ) ).rgb;
@@ -2150,16 +2370,15 @@ export class PBRTSceneBuilder {
2150
2370
 
2151
2371
  async _createBakeTerm( name, channels ) {
2152
2372
 
2153
- const def = this.ir.namedTextures.get( name );
2373
+ const def = this._textureDef( name );
2154
2374
  if ( def?.class === 'imagemap' ) {
2155
2375
 
2156
- if ( pString( def.params, 'mapping', 'uv' ) !== 'uv' ) return null;
2157
2376
  const filename = pString( def.params, 'filename', null );
2158
2377
  const pixels = texturePixels( filename && await this._image( filename ) );
2159
2378
  if ( ! pixels ) return null;
2160
2379
  return { layer: makeLayer( pixels, {
2161
2380
  channels, decode: pixels.float ? null : decoderFor( def.params ),
2162
- scale: pFloat( def.params, 'scale', 1 ), invert: pBool( def.params, 'invert', false ), mapping: uvMapping( def.params )
2381
+ scale: pFloat( def.params, 'scale', 1 ), invert: pBool( def.params, 'invert', false ), mapping: this._textureMapping( name, def )
2163
2382
  } ) };
2164
2383
 
2165
2384
  }
@@ -2180,7 +2399,7 @@ export class PBRTSceneBuilder {
2180
2399
  const srgb = ! pixels.float && texture.colorSpace === SRGBColorSpace;
2181
2400
  return makeLayer( pixels, {
2182
2401
  channels, decode: pixels.float ? null : ( srgb ? srgbToLinear : ( v ) => v ),
2183
- mapping: { su: texture.repeat.x, sv: texture.repeat.y, du: texture.offset.x, dv: texture.offset.y }
2402
+ mapping: { su: texture.repeat.x, sv: texture.repeat.y, du: texture.offset.x, dv: texture.offset.y, uv: texture.userData.pbrtUV ?? null }
2184
2403
  } );
2185
2404
 
2186
2405
  }
@@ -2207,7 +2426,7 @@ export class PBRTSceneBuilder {
2207
2426
 
2208
2427
  const a = term( matA ), b = term( matB );
2209
2428
  if ( ! a || ! b ) return { mean, texture: null };
2210
- const baked = bake( [ a, b, amount ], mixOf );
2429
+ const baked = this._bake( `mix amount "${amountName}"`, [ a, b, amount ], mixOf );
2211
2430
  return { mean, texture: baked ? bakedTexture( baked ) : null };
2212
2431
 
2213
2432
  }
@@ -2243,7 +2462,7 @@ export class PBRTSceneBuilder {
2243
2462
  const product = ( [ a, f ] ) => [ a[ 0 ] * f[ 0 ], a[ 1 ] * f[ 0 ], a[ 2 ] * f[ 0 ] ];
2244
2463
  if ( inner && factor ) {
2245
2464
 
2246
- const baked = bake( [ { ...inner, clamp: true }, factor ], product );
2465
+ const baked = this._bake( `texture "${name}"`, [ { ...inner, clamp: true }, factor ], product );
2247
2466
  return baked ? { texture: bakedTexture( baked ) } : { constant: product( [ inner.rgb, factor.rgb ] ) };
2248
2467
 
2249
2468
  }
@@ -2252,7 +2471,7 @@ export class PBRTSceneBuilder {
2252
2471
 
2253
2472
  }
2254
2473
 
2255
- const innerP = def.params.tex;
2474
+ const innerP = param( def.params, 'tex' );
2256
2475
  let inner = null;
2257
2476
 
2258
2477
  if ( innerP?.type === 'texture' && typeof innerP.value[ 0 ] === 'string' ) {
@@ -2270,7 +2489,7 @@ export class PBRTSceneBuilder {
2270
2489
 
2271
2490
  }
2272
2491
 
2273
- const sP = def.params.scale;
2492
+ const sP = param( def.params, 'scale' );
2274
2493
  let scale = [ 1, 1, 1 ];
2275
2494
  if ( sP?.type === 'rgb' || sP?.type === 'color' ) scale = [ sP.value[ 0 ], sP.value[ 1 ], sP.value[ 2 ] ];
2276
2495
  else if ( sP?.type === 'float' ) {
@@ -2299,12 +2518,13 @@ export class PBRTSceneBuilder {
2299
2518
  _buildCamera( cam, film ) {
2300
2519
 
2301
2520
  const aspect = film && film.yresolution ? film.xresolution / film.yresolution : 16 / 9;
2521
+ this._usage?.open( cam.params, `Camera ${cam.type}` );
2302
2522
 
2303
2523
  let camera;
2304
2524
  if ( cam.type === 'orthographic' ) {
2305
2525
 
2306
2526
  // pbrt's default screen window spans [-1, 1] along the shorter image axis.
2307
- const window = cam.params?.screenwindow?.value;
2527
+ const window = param( cam.params, 'screenwindow' )?.value;
2308
2528
  const half = window?.length === 4 ? ( window[ 3 ] - window[ 2 ] ) / 2 : Math.max( 1, 1 / aspect );
2309
2529
  camera = new OrthographicCamera( - half * aspect, half * aspect, half, - half, 0.01, 10000 );
2310
2530
 
@@ -2493,7 +2713,7 @@ export class PBRTSceneBuilder {
2493
2713
  const world = new Matrix4().fromArray( this.convertHandedness ? M.multiply( FLIP_Z, l.ctm ) : l.ctm );
2494
2714
  const point = ( name, dflt ) => {
2495
2715
 
2496
- const v = l.params[ name ]?.value;
2716
+ const v = param( l.params, name )?.value;
2497
2717
  return new Vector3().fromArray( v?.length >= 3 ? v : dflt ).applyMatrix4( world );
2498
2718
 
2499
2719
  };