rayzee 11.7.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 (79) hide show
  1. package/README.md +31 -6
  2. package/dist/{ArchiveImporter-DYcRXeKE.js → ArchiveImporter-BJxszU_k.js} +2353 -1356
  3. package/dist/{BidirectionalIntegrator-BvjRpbTP.js → BidirectionalIntegrator-DWg66vAd.js} +302 -301
  4. package/dist/{BufferLayout-xPRH9jYD.js → BufferLayout-Bzgccg1o.js} +1 -0
  5. package/dist/{ColorManagement-oeEk7oSu.js → ColorManagement-B_HjAiy2.js} +1 -1
  6. package/dist/{EngineIssues-CIZnXHsF.js → EngineIssues-CfJiBaNj.js} +19 -8
  7. package/dist/{NeuralRetouch-ClaMiKYg.js → NeuralRetouch-BD9E-Yht.js} +2 -2
  8. package/dist/{NeuralSuperRes-WNEXun9s.js → NeuralSuperRes-C8Mf3tJX.js} +2 -2
  9. package/dist/{PathTracer-B9wZPPVL.js → PathTracer-DeE9UJ1n.js} +3530 -3402
  10. package/dist/{PhysicalSky-DcjItqKi.js → PhysicalSky-cKy75-oR.js} +45 -45
  11. package/dist/{RayzeeRenderer-p-NeE6E2.js → RayzeeRenderer-CzLyBA3G.js} +378 -374
  12. package/dist/SceneMetadata-C17UfOEp.js +183 -0
  13. package/dist/{SolarLimb-bZE6y63S.js → SolarLimb-DRYy8tln.js} +184 -172
  14. package/dist/{ToneMapGPU-sAs8wdgI.js → ToneMapGPU-4GPwoG_c.js} +13 -6
  15. package/dist/addons/archives.es.js +1 -1
  16. package/dist/addons/bidirectional.es.js +1 -1
  17. package/dist/addons/color.es.js +1 -1
  18. package/dist/addons/physical-sky.es.js +1 -1
  19. package/dist/addons/storage.es.js +1 -1
  20. package/dist/{openStorage-Fe9BoS1_.js → openStorage-CdyVjmps.js} +1 -1
  21. package/dist/rayzee-core.es.js +7 -7
  22. package/dist/rayzee.es.js +821 -794
  23. package/dist/rayzee.umd.js +112 -103
  24. package/dist/{sceneCachePolicy-B9w3BnaP.js → sceneCachePolicy-ywbGKHD3.js} +9 -6
  25. package/package.json +1 -1
  26. package/src/EngineDefaults.js +1 -0
  27. package/src/EngineIssues.js +21 -6
  28. package/src/Passes/AIUpscaler.js +4 -0
  29. package/src/Passes/OIDNDenoiser.js +8 -6
  30. package/src/Passes/OIDNTemporalHistory.js +1 -1
  31. package/src/PathTracerApp.js +10 -1
  32. package/src/Processor/ArchiveImporter.js +106 -27
  33. package/src/Processor/AssetLoader.js +22 -0
  34. package/src/Processor/BufferLayout.js +3 -3
  35. package/src/Processor/GeometryExtractor.js +12 -4
  36. package/src/Processor/LightSerializer.js +6 -2
  37. package/src/Processor/MaterialPacking.js +2 -2
  38. package/src/Processor/PBRT/PBRTMaterials.js +49 -13
  39. package/src/Processor/PBRT/PBRTParser.js +144 -22
  40. package/src/Processor/PBRT/PBRTSceneBuilder.js +287 -67
  41. package/src/Processor/PBRT/PBRTTextureBake.js +191 -9
  42. package/src/Processor/PBRT/index.js +19 -4
  43. package/src/Processor/PlatformImageLoader.js +9 -0
  44. package/src/Processor/SceneProcessor.js +42 -26
  45. package/src/Processor/TextureCreator.js +44 -4
  46. package/src/Processor/ToneMapCPU.js +34 -5
  47. package/src/Processor/ToneMapGPU.js +15 -9
  48. package/src/Processor/USD/USDScene.js +981 -17
  49. package/src/RayzeeRenderer.js +85 -28
  50. package/src/Stages/AutoExposure.js +21 -8
  51. package/src/Stages/Compositor.js +39 -1
  52. package/src/Stages/DenoiserSettings.js +0 -4
  53. package/src/Stages/LocalExposure.js +44 -18
  54. package/src/Stages/NRD.js +53 -24
  55. package/src/Stages/NormalDepth.js +12 -6
  56. package/src/Stages/PathTracer.js +27 -1
  57. package/src/Stages/PathTracerStage.js +14 -2
  58. package/src/Storage/SceneGraphCodec.js +6 -3
  59. package/src/TSL/BVHTraversal.js +33 -20
  60. package/src/TSL/CameraRay.js +12 -1
  61. package/src/TSL/Common.js +25 -8
  62. package/src/TSL/DebugKernel.js +3 -2
  63. package/src/TSL/Displacement.js +66 -113
  64. package/src/TSL/FinalWriteKernel.js +4 -7
  65. package/src/TSL/GenerateKernel.js +6 -2
  66. package/src/TSL/LightSplatKernel.js +7 -4
  67. package/src/TSL/LightsCore.js +6 -2
  68. package/src/TSL/LightsDirect.js +33 -11
  69. package/src/TSL/LightsSampling.js +45 -12
  70. package/src/TSL/SceneResources.js +19 -0
  71. package/src/TSL/ShadeKernel.js +3 -4
  72. package/src/TSL/Struct.js +1 -0
  73. package/src/integrators/BidirectionalIntegrator.js +1 -0
  74. package/src/managers/CameraManager.js +9 -0
  75. package/src/managers/DenoisingManager.js +1 -0
  76. package/src/managers/InteractionManager.js +6 -0
  77. package/src/managers/MaterialDataManager.js +11 -0
  78. package/src/managers/UniformManager.js +3 -1
  79. package/dist/SceneMetadata-D2Sh-0e-.js +0 -110
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { MeshPhysicalMaterial, Color, DoubleSide } from 'three';
12
12
  import { namedScattering, dipoleReflectance, albedoForReflectance } from './PBRTScattering.js';
13
+ import { approximated, param } from './PBRTParser.js';
13
14
 
14
15
  // Normal-incidence reflectance approximations for pbrt's named conductor spectra.
15
16
  const METAL_ALBEDO = {
@@ -54,21 +55,37 @@ function blackbodyToRGB( kelvin ) {
54
55
 
55
56
  export function pFloat( params, name, dflt ) {
56
57
 
57
- const p = params[ name ];
58
+ const p = param( params, name );
59
+ if ( p && p.type !== 'float' && p.type !== 'integer' ) approximated( params, name );
58
60
  return p && typeof p.value[ 0 ] === 'number' ? p.value[ 0 ] : dflt;
59
61
 
60
62
  }
61
63
 
64
+ /** An index of refraction: a sampled spectrum ( λ value pairs ) is read at 550 nm, linearly between its samples. */
65
+ export function pEta( params, name, dflt ) {
66
+
67
+ const p = param( params, name );
68
+ if ( p?.type !== 'spectrum' || typeof p.value[ 0 ] !== 'number' ) return pFloat( params, name, dflt );
69
+ approximated( params, name );
70
+ const v = p.value;
71
+ if ( v.length < 2 ) return dflt;
72
+ let i = 2;
73
+ while ( i < v.length - 1 && v[ i ] < 550 ) i += 2;
74
+ if ( i >= v.length - 1 || v[ i - 2 ] >= 550 ) return v[ i >= v.length - 1 ? i - 1 : 1 ];
75
+ return v[ i - 1 ] + ( v[ i + 1 ] - v[ i - 1 ] ) * ( 550 - v[ i - 2 ] ) / ( v[ i ] - v[ i - 2 ] );
76
+
77
+ }
78
+
62
79
  export function pString( params, name, dflt ) {
63
80
 
64
- const p = params[ name ];
81
+ const p = param( params, name );
65
82
  return p && typeof p.value[ 0 ] === 'string' ? p.value[ 0 ] : dflt;
66
83
 
67
84
  }
68
85
 
69
86
  export function pBool( params, name, dflt ) {
70
87
 
71
- const v = params[ name ]?.value?.[ 0 ];
88
+ const v = param( params, name )?.value?.[ 0 ];
72
89
  return v === undefined ? dflt : v === true || v === 'true';
73
90
 
74
91
  }
@@ -80,7 +97,7 @@ export function pBool( params, name, dflt ) {
80
97
  */
81
98
  export async function resolveSpectrum( params, name, ctx, dfltRGB = null ) {
82
99
 
83
- const p = params[ name ];
100
+ const p = param( params, name );
84
101
  if ( ! p ) return { rgb: dfltRGB, texture: null };
85
102
 
86
103
  // Reference to a named texture.
@@ -91,6 +108,7 @@ export async function resolveSpectrum( params, name, ctx, dfltRGB = null ) {
91
108
  if ( tex && tex.texture ) return { rgb: tex.constant ?? null, texture: tex.texture };
92
109
  if ( tex && tex.constant ) return { rgb: tex.constant, texture: null };
93
110
  ctx.warn( `texture "${texName}" could not be resolved` );
111
+ approximated( params, name );
94
112
  return { rgb: dfltRGB, texture: null };
95
113
 
96
114
  }
@@ -110,6 +128,7 @@ export async function resolveSpectrum( params, name, ctx, dfltRGB = null ) {
110
128
 
111
129
  if ( p.type === 'blackbody' ) {
112
130
 
131
+ approximated( params, name );
113
132
  return { rgb: blackbodyToRGB( p.value[ 0 ] ), texture: null };
114
133
 
115
134
  }
@@ -117,6 +136,7 @@ export async function resolveSpectrum( params, name, ctx, dfltRGB = null ) {
117
136
  if ( p.type === 'spectrum' ) {
118
137
 
119
138
  // Named spectrum (e.g. "metal-Au-eta") or sampled [lambda val ...].
139
+ approximated( params, name );
120
140
  if ( typeof p.value[ 0 ] === 'string' ) {
121
141
 
122
142
  const key = namedMetalKey( p.value[ 0 ] );
@@ -139,6 +159,7 @@ export async function resolveSpectrum( params, name, ctx, dfltRGB = null ) {
139
159
 
140
160
  }
141
161
 
162
+ approximated( params, name );
142
163
  return { rgb: dfltRGB, texture: null };
143
164
 
144
165
  }
@@ -155,9 +176,10 @@ function namedMetalKey( s ) {
155
176
  // A float parameter's value, or a float texture's mean (`ctx.floatTextureMean`); null when absent or unreadable.
156
177
  async function floatOrMean( params, name, ctx ) {
157
178
 
158
- const p = params[ name ];
179
+ const p = param( params, name );
159
180
  if ( ! p ) return null;
160
181
  if ( typeof p.value[ 0 ] === 'number' ) return p.value[ 0 ];
182
+ approximated( params, name );
161
183
  if ( p.type === 'texture' ) return ( await ctx.floatTextureMean?.( p.value[ 0 ] ) ) ?? null;
162
184
  return null;
163
185
 
@@ -171,7 +193,7 @@ async function floatOrMean( params, name, ctx ) {
171
193
  */
172
194
  const remapsRoughness = ( params ) => {
173
195
 
174
- const remap = params.remaproughness?.value?.[ 0 ];
196
+ const remap = param( params, 'remaproughness' )?.value?.[ 0 ];
175
197
  return remap !== false && remap !== 'false';
176
198
 
177
199
  };
@@ -225,8 +247,14 @@ async function applyRoughness( mat, target, params, dflt, ctx, prefix = '' ) {
225
247
  // A spectrum parameter as one RGB; a texture counts as its mean (`ctx.textureMeanRGB`).
226
248
  async function spectrumMean( params, key, ctx, dflt ) {
227
249
 
228
- const p = params[ key ];
229
- if ( p?.type === 'texture' ) return ( await ctx.textureMeanRGB?.( p.value[ 0 ] ) ) ?? dflt;
250
+ const p = param( params, key );
251
+ if ( p?.type === 'texture' ) {
252
+
253
+ approximated( params, key );
254
+ return ( await ctx.textureMeanRGB?.( p.value[ 0 ] ) ) ?? dflt;
255
+
256
+ }
257
+
230
258
  return ( await resolveSpectrum( params, key, ctx, dflt ) ).rgb ?? dflt;
231
259
 
232
260
  }
@@ -243,7 +271,7 @@ const SUBSURFACE_SIGMA_S = [ 2.55, 3.21, 3.77 ];
243
271
  */
244
272
  async function applySubsurface( mat, params, ctx ) {
245
273
 
246
- const eta = pFloat( params, 'eta', 1.33 );
274
+ const eta = pEta( params, 'eta', 1.33 );
247
275
  const scale = pFloat( params, 'scale', 1 );
248
276
  const scaled = ( rgb ) => rgb.map( ( v ) => Math.max( 0, v * scale ) );
249
277
  let g = pFloat( params, 'g', 0 );
@@ -353,6 +381,7 @@ export async function buildMaterial( def, ctx ) {
353
381
 
354
382
  const type = def?.type || 'diffuse';
355
383
  const params = def?.params || {};
384
+ ctx.trackParams?.( params, `Material ${type}`, 'type' );
356
385
  const mat = new MeshPhysicalMaterial( { side: DoubleSide, roughness: 1, metalness: 0 } );
357
386
 
358
387
  const setColor = ( rgb ) => {
@@ -393,7 +422,8 @@ export async function buildMaterial( def, ctx ) {
393
422
  case 'metal': {
394
423
 
395
424
  await applyConductorColor( mat, params, ctx, '', applyAlbedo, setColor );
396
- await applyRoughness( mat, 'roughness', params, 0.1, ctx );
425
+ // pbrt's roughness defaults to 0, a mirror.
426
+ await applyRoughness( mat, 'roughness', params, 0, ctx );
397
427
  break;
398
428
 
399
429
  }
@@ -415,7 +445,7 @@ export async function buildMaterial( def, ctx ) {
415
445
  mat.transmission = 1;
416
446
  mat.metalness = 0;
417
447
  mat.color.setRGB( 1, 1, 1 );
418
- mat.ior = pFloat( params, 'eta', 1.5 );
448
+ mat.ior = pEta( params, 'eta', 1.5 );
419
449
  await applyRoughness( mat, 'roughness', params, 0, ctx );
420
450
  mat.thickness = type === 'thindielectric' ? 0 : pFloat( params, 'thickness', 0 );
421
451
  break;
@@ -446,7 +476,13 @@ export async function buildMaterial( def, ctx ) {
446
476
  const refl = await resolveSpectrum( params, 'reflectance', ctx, [ 0.25, 0.25, 0.25 ] );
447
477
  const base = ( v ) => Math.min( 1, unit( v ) / ( 1 - dt ) );
448
478
  applyAlbedo( { texture: refl.texture, rgb: refl.rgb ? refl.rgb.map( base ) : ( refl.texture ? [ 1, 1, 1 ].map( base ) : null ) } );
449
- if ( trans.texture ) ctx.warn( 'diffusetransmission: a textured "transmittance" is not supported — using 0.5' );
479
+ if ( trans.texture ) {
480
+
481
+ ctx.warn( 'diffusetransmission: a textured "transmittance" is not supported — using 0.5' );
482
+ approximated( params, 'transmittance' );
483
+
484
+ }
485
+
450
486
  mat.diffuseTransmission = dt;
451
487
  mat.diffuseTransmissionColor = new Color().setRGB( ...T.map( ( v ) => ( dt > 0 ? Math.min( 1, v / dt ) : 1 ) ) );
452
488
  mat.roughness = 1;
@@ -476,7 +512,7 @@ export async function buildMaterial( def, ctx ) {
476
512
  // — physically loose but visually faithful, and crucially silent on success
477
513
  // instead of warning per shape. Recursive: mix-in-mix terminates as the
478
514
  // chain bottoms out at a non-mix.
479
- const matNames = params.materials?.value || [];
515
+ const matNames = param( params, 'materials' )?.value || [];
480
516
  let t = Math.max( 0, Math.min( 1, pFloat( params, 'amount', 0.5 ) ) );
481
517
  const defA = matNames[ 0 ] ? ctx.namedMaterials?.get( matNames[ 0 ] ) : null;
482
518
  const defB = matNames[ 1 ] ? ctx.namedMaterials?.get( matNames[ 1 ] ) : null;
@@ -14,7 +14,7 @@
14
14
  * film: { xresolution, yresolution, filename } | null,
15
15
  * camera: { type, params, cameraToWorld:number[16] } | null,
16
16
  * namedMaterials: Map<name, { type, params }>,
17
- * namedTextures: Map<name, { dataType, class, params }>,
17
+ * namedTextures: Map<name, { dataType, class, params, ctm? }>, (ctm: where a position mapping was declared)
18
18
  * shapes: [ { type, params, ctm, ctmEnd?, material, areaLight, reverseOrientation } ],
19
19
  * lights: [ { type, params, ctm } ],
20
20
  * instances: Map<name, { name, count, matrices, matricesEnd? }>,
@@ -68,6 +68,101 @@ const NUMERIC_STORAGE = {
68
68
  rgb: Float32Array
69
69
  };
70
70
 
71
+ // The directive `ParamUsage.translate` is in, and each directive open until `ParamUsage.finish`.
72
+ let current = null;
73
+ const scopes = new Map();
74
+
75
+ /** A directive's parameter, counted as used: every translator reads through this, as pbrt-v4's lookups mark theirs. */
76
+ export function param( params, name ) {
77
+
78
+ if ( current !== null && current.params === params ) current.read.push( name );
79
+ else if ( scopes.size !== 0 ) scopes.get( params )?.read.push( name );
80
+ return params?.[ name ];
81
+
82
+ }
83
+
84
+ /**
85
+ * A parameter read but not honoured exactly: a texture's mean where the engine takes one value,
86
+ * a default for a type it cannot read.
87
+ */
88
+ export function approximated( params, name ) {
89
+
90
+ const scope = current !== null && current.params === params ? current : scopes.get( params );
91
+ if ( scope ) ( scope.approximated ??= new Set() ).add( name );
92
+
93
+ }
94
+
95
+ /**
96
+ * What one load did not honour, as pbrt's ReportUnused: each translated directive's parameters that nothing looked up,
97
+ * or that were approximated, counted per `directive type: name`. Holds nothing per directive once it is accounted for.
98
+ */
99
+ export class ParamUsage {
100
+
101
+ constructor() {
102
+
103
+ this.unused = new Map();
104
+ this.approximated = new Map();
105
+
106
+ }
107
+
108
+ /**
109
+ * Runs `read`, which looks up every parameter it uses before it returns, and accounts for the directive at once.
110
+ * `consumed` names one read before, where nothing counted it. A directive left open is accounted for by `finish`.
111
+ */
112
+ translate( params, label, read, consumed = null ) {
113
+
114
+ if ( scopes.has( params ) ) return read();
115
+ const outer = current;
116
+ const scope = current = { params, label, approximated: null, read: [ consumed ] };
117
+ try {
118
+
119
+ return read();
120
+
121
+ } finally {
122
+
123
+ current = outer;
124
+ for ( const name in params ) this._count( scope, name, ! scope.read.includes( name ) );
125
+
126
+ }
127
+
128
+ }
129
+
130
+ /** Accounts for a directive read across awaits, from here to `finish`. */
131
+ open( params, label, consumed = null ) {
132
+
133
+ if ( params && ! scopes.has( params ) ) scopes.set( params, { usage: this, label, approximated: null, read: [ consumed ] } );
134
+
135
+ }
136
+
137
+ /** @returns {{unused: Object<string, number>, approximated: Object<string, number>}} */
138
+ finish() {
139
+
140
+ for ( const [ params, scope ] of scopes ) {
141
+
142
+ if ( scope.usage !== this ) continue;
143
+ scopes.delete( params );
144
+ for ( const name in params ) this._count( scope, name, ! scope.read.includes( name ) );
145
+
146
+ }
147
+
148
+ const flat = ( counts ) => [ ...counts ].flatMap( ( [ label, byName ] ) => [ ...byName ].map( ( [ name, n ] ) => [ `${label}: ${name}`, n ] ) );
149
+ const sorted = ( counts ) => Object.fromEntries( flat( counts ).sort( ( a, b ) => ( a[ 0 ] < b[ 0 ] ? - 1 : 1 ) ) );
150
+ return { unused: sorted( this.unused ), approximated: sorted( this.approximated ) };
151
+
152
+ }
153
+
154
+ _count( scope, name, unused ) {
155
+
156
+ const counts = scope.approximated?.has( name ) ? this.approximated : unused ? this.unused : null;
157
+ if ( ! counts ) return;
158
+ let byName = counts.get( scope.label );
159
+ if ( ! byName ) counts.set( scope.label, byName = new Map() );
160
+ byName.set( name, ( byName.get( name ) ?? 0 ) + 1 );
161
+
162
+ }
163
+
164
+ }
165
+
71
166
  export class PBRTParser {
72
167
 
73
168
  /**
@@ -90,6 +185,7 @@ export class PBRTParser {
90
185
  // is the parse itself, so a limit applied later saves nothing.
91
186
  this.maxPlacements = opts.maxPlacements ?? Infinity;
92
187
  this.instanceIncludes = opts.instanceIncludes !== false;
188
+ this.usage = opts.usage ?? null;
93
189
  this._includes = new Map();
94
190
  this._stateIds = new WeakMap();
95
191
  this._nextStateId = 1;
@@ -598,14 +694,18 @@ export class PBRTParser {
598
694
 
599
695
  case 'Film': {
600
696
 
601
- this._expectString( 'Film type' );
697
+ const type = this._expectString( 'Film type' );
602
698
  const params = this._parseParams();
603
- this.ir.film = {
604
- xresolution: this._num( params.xresolution, 1280 ),
605
- yresolution: this._num( params.yresolution, 720 ),
606
- resolutionGiven: Boolean( params.xresolution || params.yresolution ),
607
- filename: this._str( params.filename, null )
608
- };
699
+ this._translate( params, `${name} ${type}`, () => {
700
+
701
+ this.ir.film = {
702
+ xresolution: this._num( param( params, 'xresolution' ), 1280 ),
703
+ yresolution: this._num( param( params, 'yresolution' ), 720 ),
704
+ resolutionGiven: Boolean( params.xresolution || params.yresolution ),
705
+ filename: this._str( param( params, 'filename' ), null )
706
+ };
707
+
708
+ } );
609
709
  break;
610
710
 
611
711
  }
@@ -613,18 +713,26 @@ export class PBRTParser {
613
713
  // What the scene asks the renderer for; reported, not applied (PBRTSceneBuilder renderRequest).
614
714
  case 'Integrator': {
615
715
 
616
- this._expectString( 'Integrator type' );
716
+ const type = this._expectString( 'Integrator type' );
617
717
  const params = this._parseParams();
618
- if ( params.maxdepth ) this.ir.integrator = { maxdepth: this._num( params.maxdepth, 5 ) };
718
+ this._translate( params, `${name} ${type}`, () => {
719
+
720
+ if ( params.maxdepth ) this.ir.integrator = { maxdepth: this._num( param( params, 'maxdepth' ), 5 ) };
721
+
722
+ } );
619
723
  break;
620
724
 
621
725
  }
622
726
 
623
727
  case 'Sampler': {
624
728
 
625
- this._expectString( 'Sampler type' );
729
+ const type = this._expectString( 'Sampler type' );
626
730
  const params = this._parseParams();
627
- if ( params.pixelsamples ) this.ir.sampler = { pixelsamples: this._num( params.pixelsamples, 16 ) };
731
+ this._translate( params, `${name} ${type}`, () => {
732
+
733
+ if ( params.pixelsamples ) this.ir.sampler = { pixelsamples: this._num( param( params, 'pixelsamples' ), 16 ) };
734
+
735
+ } );
628
736
  break;
629
737
 
630
738
  }
@@ -633,11 +741,17 @@ export class PBRTParser {
633
741
  case 'PixelFilter':
634
742
  case 'Filter':
635
743
  case 'Accelerator':
636
- case 'ColorSpace':
637
- case 'Option':
638
- this._skipTypeAndParams();
744
+ case 'ColorSpace': {
745
+
746
+ const type = this._peek()?.type === TokenType.STRING ? this._next().value : '';
747
+ this._translate( this._parseParams(), `${name} ${type}` );
639
748
  break;
640
749
 
750
+ }
751
+
752
+ // `Option "type name" value`: one parameter, no type before it.
753
+ case 'Option': this._translate( this._parseParams(), name ); break;
754
+
641
755
  // ── world block ──
642
756
  case 'WorldBegin':
643
757
  this.ctm = M.identity();
@@ -669,7 +783,14 @@ export class PBRTParser {
669
783
  case 'ReverseOrientation': this.state.reverseOrientation = ! this.state.reverseOrientation; break;
670
784
 
671
785
  // `Attribute "target" params` — v4 default-setting; ignored for MVP.
672
- case 'Attribute': this._expectString( 'Attribute target' ); this._parseParams(); break;
786
+ case 'Attribute': {
787
+
788
+ const target = this._expectString( 'Attribute target' );
789
+ this._translate( this._parseParams(), `${name} ${target}` );
790
+ break;
791
+
792
+ }
793
+
673
794
  case 'ActiveTransform': {
674
795
 
675
796
  const t = this._next();
@@ -744,7 +865,9 @@ export class PBRTParser {
744
865
  const dataType = this._expectString( 'Texture data type' ); // float | spectrum
745
866
  const texClass = this._expectString( 'Texture class' ); // imagemap | scale | ...
746
867
  const params = this._parseParams();
747
- this.ir.namedTextures.set( texName, { dataType, class: texClass, params } );
868
+ const def = { dataType, class: texClass, params };
869
+ if ( this._str( params.mapping, 'uv' ) !== 'uv' ) def.ctm = this._ctmSnapshot();
870
+ this.ir.namedTextures.set( texName, def );
748
871
  break;
749
872
 
750
873
  }
@@ -839,7 +962,7 @@ export class PBRTParser {
839
962
  default:
840
963
  this._warnUnknown( name );
841
964
  // Best effort: swallow any trailing parameter list to stay in sync.
842
- this._parseParams();
965
+ this._translate( this._parseParams(), name );
843
966
  break;
844
967
 
845
968
  }
@@ -992,11 +1115,10 @@ export class PBRTParser {
992
1115
 
993
1116
  }
994
1117
 
995
- _skipTypeAndParams() {
1118
+ /** Runs `read` over a directive's parameters, and counts what it leaves unread: all of them by default. */
1119
+ _translate( params, label, read = () => {} ) {
996
1120
 
997
- // "type" then params
998
- if ( this._peek() && this._peek().type === TokenType.STRING ) this._next();
999
- this._parseParams();
1121
+ return this.usage ? this.usage.translate( params, label, read ) : read();
1000
1122
 
1001
1123
  }
1002
1124