rayzee 6.3.0 → 6.4.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.
- package/README.md +1 -1
- package/dist/rayzee.es.js +3273 -2008
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +50 -48
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/Processor/AssetLoader.js +151 -1
- package/src/Processor/PBRT/PBRTMaterials.js +350 -0
- package/src/Processor/PBRT/PBRTMath.js +173 -0
- package/src/Processor/PBRT/PBRTParser.js +642 -0
- package/src/Processor/PBRT/PBRTSceneBuilder.js +578 -0
- package/src/Processor/PBRT/PBRTTokenizer.js +141 -0
- package/src/Processor/PBRT/index.js +179 -0
- package/src/TSL/PathTracerCore.js +12 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser + graphics-state machine for pbrt-v4 scene files.
|
|
3
|
+
*
|
|
4
|
+
* Consumes the token stream from PBRTTokenizer and produces a plain-data
|
|
5
|
+
* intermediate representation (IR). No Three.js types — the IR is converted
|
|
6
|
+
* to a scene graph later by PBRTSceneBuilder, keeping this module unit-testable.
|
|
7
|
+
*
|
|
8
|
+
* The graphics state mirrors pbrt's: a current transformation matrix (CTM),
|
|
9
|
+
* a current material, a current area-light emission, and a reverse-orientation
|
|
10
|
+
* flag, all saved/restored by AttributeBegin/AttributeEnd.
|
|
11
|
+
*
|
|
12
|
+
* IR shape:
|
|
13
|
+
* {
|
|
14
|
+
* film: { xresolution, yresolution, filename } | null,
|
|
15
|
+
* camera: { type, params, cameraToWorld:number[16] } | null,
|
|
16
|
+
* namedMaterials: Map<name, { type, params }>,
|
|
17
|
+
* namedTextures: Map<name, { dataType, class, params }>,
|
|
18
|
+
* shapes: [ { type, params, ctm, material, areaLight, reverseOrientation } ],
|
|
19
|
+
* lights: [ { type, params, ctm } ],
|
|
20
|
+
* instances: [ { name, ctm } ],
|
|
21
|
+
* objects: Map<name, shapes[]>,
|
|
22
|
+
* warnings: string[]
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { tokenize, TokenType } from './PBRTTokenizer.js';
|
|
27
|
+
import * as M from './PBRTMath.js';
|
|
28
|
+
|
|
29
|
+
export class PBRTParser {
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {object} [opts]
|
|
33
|
+
* @param {(path:string)=>string} [opts.resolveInclude] - returns the text of
|
|
34
|
+
* an Include/Import target, resolved relative to the current file.
|
|
35
|
+
*/
|
|
36
|
+
constructor( opts = {} ) {
|
|
37
|
+
|
|
38
|
+
this.resolveInclude = opts.resolveInclude || ( () => {
|
|
39
|
+
|
|
40
|
+
throw new Error( 'PBRTParser: Include used but no resolveInclude provided' );
|
|
41
|
+
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
// IR accumulators
|
|
45
|
+
this.ir = {
|
|
46
|
+
film: null,
|
|
47
|
+
camera: null,
|
|
48
|
+
namedMaterials: new Map(),
|
|
49
|
+
namedTextures: new Map(),
|
|
50
|
+
shapes: [],
|
|
51
|
+
lights: [],
|
|
52
|
+
instances: [],
|
|
53
|
+
objects: new Map(),
|
|
54
|
+
warnings: []
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Graphics state
|
|
58
|
+
this.ctm = M.identity();
|
|
59
|
+
this.state = { material: null, areaLight: null, reverseOrientation: false };
|
|
60
|
+
this.attributeStack = [];
|
|
61
|
+
this.transformStack = [];
|
|
62
|
+
this.coordSystems = new Map();
|
|
63
|
+
|
|
64
|
+
// Object capture (ObjectBegin/End)
|
|
65
|
+
this.currentObject = null; // name being captured, or null
|
|
66
|
+
this.objectBeginCTM = null; // CTM frame at ObjectBegin
|
|
67
|
+
|
|
68
|
+
// Directory stack for resolving nested Includes
|
|
69
|
+
this.dirStack = [ '' ];
|
|
70
|
+
|
|
71
|
+
// Token cursor (swapped during Include recursion)
|
|
72
|
+
this.tokens = [];
|
|
73
|
+
this.pos = 0;
|
|
74
|
+
|
|
75
|
+
this._warnedUnknown = new Set();
|
|
76
|
+
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Parse a top-level pbrt source string.
|
|
81
|
+
* @param {string} src
|
|
82
|
+
* @param {string} [baseDir] - directory of the source file, for Include paths
|
|
83
|
+
* @returns {object} IR
|
|
84
|
+
*/
|
|
85
|
+
parse( src, baseDir = '' ) {
|
|
86
|
+
|
|
87
|
+
this.dirStack = [ baseDir ];
|
|
88
|
+
this._run( tokenize( src ) );
|
|
89
|
+
return this.ir;
|
|
90
|
+
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── token helpers ──────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
_peek() {
|
|
96
|
+
|
|
97
|
+
return this.tokens[ this.pos ];
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_next() {
|
|
102
|
+
|
|
103
|
+
return this.tokens[ this.pos ++ ];
|
|
104
|
+
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_expectNumber( what ) {
|
|
108
|
+
|
|
109
|
+
const t = this._next();
|
|
110
|
+
if ( ! t || t.type !== TokenType.NUMBER ) {
|
|
111
|
+
|
|
112
|
+
throw new Error( `PBRT parser: expected number for ${what}, got ${t ? t.value : 'EOF'}` );
|
|
113
|
+
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return t.value;
|
|
117
|
+
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_expectString( what ) {
|
|
121
|
+
|
|
122
|
+
const t = this._next();
|
|
123
|
+
if ( ! t || t.type !== TokenType.STRING ) {
|
|
124
|
+
|
|
125
|
+
throw new Error( `PBRT parser: expected string for ${what}, got ${t ? t.value : 'EOF'}` );
|
|
126
|
+
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return t.value;
|
|
130
|
+
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_readNumbers( count ) {
|
|
134
|
+
|
|
135
|
+
const out = [];
|
|
136
|
+
for ( let i = 0; i < count; i ++ ) out.push( this._expectNumber( 'matrix/transform' ) );
|
|
137
|
+
return out;
|
|
138
|
+
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Reads a `[ ... ]` bracketed list of numbers (transforms use this form,
|
|
143
|
+
* but pbrt also accepts the 16 bare numbers without brackets).
|
|
144
|
+
*/
|
|
145
|
+
_readBracketedOrBareNumbers( count ) {
|
|
146
|
+
|
|
147
|
+
if ( this._peek() && this._peek().type === TokenType.LBRACKET ) {
|
|
148
|
+
|
|
149
|
+
this._next(); // [
|
|
150
|
+
const out = [];
|
|
151
|
+
while ( this._peek() && this._peek().type !== TokenType.RBRACKET ) {
|
|
152
|
+
|
|
153
|
+
out.push( this._expectNumber( 'transform element' ) );
|
|
154
|
+
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this._next(); // ]
|
|
158
|
+
return out;
|
|
159
|
+
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return this._readNumbers( count );
|
|
163
|
+
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Parse a pbrt parameter list: a run of `"type name" value(s)` pairs.
|
|
168
|
+
* Stops when the next token is not a declarator string.
|
|
169
|
+
* @returns {Object<string, {type:string, value:Array}>}
|
|
170
|
+
*/
|
|
171
|
+
_parseParams() {
|
|
172
|
+
|
|
173
|
+
const params = {};
|
|
174
|
+
|
|
175
|
+
while ( this._peek() && this._peek().type === TokenType.STRING ) {
|
|
176
|
+
|
|
177
|
+
const decl = this._next().value.trim().split( /\s+/ );
|
|
178
|
+
const type = decl[ 0 ];
|
|
179
|
+
const name = decl[ 1 ] !== undefined ? decl[ 1 ] : decl[ 0 ];
|
|
180
|
+
|
|
181
|
+
const value = this._parseParamValue();
|
|
182
|
+
params[ name ] = { type, value };
|
|
183
|
+
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return params;
|
|
187
|
+
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Read a single parameter value: a bracketed array or one bare token. */
|
|
191
|
+
_parseParamValue() {
|
|
192
|
+
|
|
193
|
+
const out = [];
|
|
194
|
+
|
|
195
|
+
if ( this._peek() && this._peek().type === TokenType.LBRACKET ) {
|
|
196
|
+
|
|
197
|
+
this._next(); // [
|
|
198
|
+
while ( this._peek() && this._peek().type !== TokenType.RBRACKET ) {
|
|
199
|
+
|
|
200
|
+
out.push( this._coerceValueToken( this._next() ) );
|
|
201
|
+
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this._next(); // ]
|
|
205
|
+
|
|
206
|
+
} else {
|
|
207
|
+
|
|
208
|
+
out.push( this._coerceValueToken( this._next() ) );
|
|
209
|
+
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return out;
|
|
213
|
+
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
_coerceValueToken( t ) {
|
|
217
|
+
|
|
218
|
+
if ( ! t ) throw new Error( 'PBRT parser: unexpected EOF in parameter value' );
|
|
219
|
+
if ( t.type === TokenType.NUMBER ) return t.value;
|
|
220
|
+
if ( t.type === TokenType.STRING ) return t.value;
|
|
221
|
+
if ( t.type === TokenType.WORD ) {
|
|
222
|
+
|
|
223
|
+
if ( t.value === 'true' ) return true;
|
|
224
|
+
if ( t.value === 'false' ) return false;
|
|
225
|
+
return t.value;
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
throw new Error( `PBRT parser: unexpected token in parameter value: ${t.type}` );
|
|
230
|
+
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── main directive loop ────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
_run( tokens ) {
|
|
236
|
+
|
|
237
|
+
// Save/restore cursor so Include can recurse on a fresh token array.
|
|
238
|
+
const savedTokens = this.tokens;
|
|
239
|
+
const savedPos = this.pos;
|
|
240
|
+
this.tokens = tokens;
|
|
241
|
+
this.pos = 0;
|
|
242
|
+
|
|
243
|
+
while ( this.pos < this.tokens.length ) {
|
|
244
|
+
|
|
245
|
+
const t = this._next();
|
|
246
|
+
if ( t.type !== TokenType.WORD ) {
|
|
247
|
+
|
|
248
|
+
throw new Error( `PBRT parser: expected directive, got ${t.type} ${t.value ?? ''}` );
|
|
249
|
+
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this._directive( t.value );
|
|
253
|
+
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
this.tokens = savedTokens;
|
|
257
|
+
this.pos = savedPos;
|
|
258
|
+
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
_directive( name ) {
|
|
262
|
+
|
|
263
|
+
switch ( name ) {
|
|
264
|
+
|
|
265
|
+
// ── transforms ──
|
|
266
|
+
case 'Identity': this.ctm = M.identity(); break;
|
|
267
|
+
case 'Translate': {
|
|
268
|
+
|
|
269
|
+
const [ x, y, z ] = this._readNumbers( 3 );
|
|
270
|
+
this.ctm = M.multiply( this.ctm, M.translate( x, y, z ) );
|
|
271
|
+
break;
|
|
272
|
+
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
case 'Scale': {
|
|
276
|
+
|
|
277
|
+
const [ x, y, z ] = this._readNumbers( 3 );
|
|
278
|
+
this.ctm = M.multiply( this.ctm, M.scale( x, y, z ) );
|
|
279
|
+
break;
|
|
280
|
+
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
case 'Rotate': {
|
|
284
|
+
|
|
285
|
+
const [ angle, x, y, z ] = this._readNumbers( 4 );
|
|
286
|
+
this.ctm = M.multiply( this.ctm, M.rotate( angle, x, y, z ) );
|
|
287
|
+
break;
|
|
288
|
+
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
case 'LookAt': {
|
|
292
|
+
|
|
293
|
+
const v = this._readNumbers( 9 );
|
|
294
|
+
const camToWorld = M.lookAtCameraToWorld(
|
|
295
|
+
[ v[ 0 ], v[ 1 ], v[ 2 ] ], [ v[ 3 ], v[ 4 ], v[ 5 ] ], [ v[ 6 ], v[ 7 ], v[ 8 ] ]
|
|
296
|
+
);
|
|
297
|
+
// pbrt sets CTM to world-to-camera = inverse(cameraToWorld).
|
|
298
|
+
this.ctm = M.multiply( this.ctm, M.invert( camToWorld ) );
|
|
299
|
+
break;
|
|
300
|
+
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
case 'Transform': {
|
|
304
|
+
|
|
305
|
+
this.ctm = this._readBracketedOrBareNumbers( 16 );
|
|
306
|
+
break;
|
|
307
|
+
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
case 'ConcatTransform': {
|
|
311
|
+
|
|
312
|
+
const m = this._readBracketedOrBareNumbers( 16 );
|
|
313
|
+
this.ctm = M.multiply( this.ctm, m );
|
|
314
|
+
break;
|
|
315
|
+
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
case 'CoordinateSystem': this.coordSystems.set( this._expectString( 'CoordinateSystem' ), this.ctm.slice() ); break;
|
|
319
|
+
case 'CoordSysTransform': {
|
|
320
|
+
|
|
321
|
+
const cs = this.coordSystems.get( this._expectString( 'CoordSysTransform' ) );
|
|
322
|
+
if ( cs ) this.ctm = cs.slice();
|
|
323
|
+
break;
|
|
324
|
+
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── scene-wide options ──
|
|
328
|
+
case 'Camera': {
|
|
329
|
+
|
|
330
|
+
const type = this._expectString( 'Camera type' );
|
|
331
|
+
const params = this._parseParams();
|
|
332
|
+
// Camera-to-world is the inverse of the CTM at the Camera directive.
|
|
333
|
+
this.ir.camera = { type, params, cameraToWorld: M.invert( this.ctm ) };
|
|
334
|
+
break;
|
|
335
|
+
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
case 'Film': {
|
|
339
|
+
|
|
340
|
+
this._expectString( 'Film type' );
|
|
341
|
+
const params = this._parseParams();
|
|
342
|
+
this.ir.film = {
|
|
343
|
+
xresolution: this._num( params.xresolution, 1280 ),
|
|
344
|
+
yresolution: this._num( params.yresolution, 720 ),
|
|
345
|
+
filename: this._str( params.filename, null )
|
|
346
|
+
};
|
|
347
|
+
break;
|
|
348
|
+
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Consumed for completeness; not used by the engine.
|
|
352
|
+
case 'Integrator':
|
|
353
|
+
case 'Sampler':
|
|
354
|
+
case 'PixelFilter':
|
|
355
|
+
case 'Filter':
|
|
356
|
+
case 'Accelerator':
|
|
357
|
+
case 'ColorSpace':
|
|
358
|
+
case 'Option':
|
|
359
|
+
this._skipTypeAndParams();
|
|
360
|
+
break;
|
|
361
|
+
|
|
362
|
+
// ── world block ──
|
|
363
|
+
case 'WorldBegin':
|
|
364
|
+
this.ctm = M.identity();
|
|
365
|
+
this.state = { material: null, areaLight: null, reverseOrientation: false };
|
|
366
|
+
break;
|
|
367
|
+
case 'WorldEnd': break; // legacy v3
|
|
368
|
+
|
|
369
|
+
case 'AttributeBegin':
|
|
370
|
+
this.attributeStack.push( {
|
|
371
|
+
ctm: this.ctm.slice(),
|
|
372
|
+
material: this.state.material,
|
|
373
|
+
areaLight: this.state.areaLight,
|
|
374
|
+
reverseOrientation: this.state.reverseOrientation
|
|
375
|
+
} );
|
|
376
|
+
break;
|
|
377
|
+
case 'AttributeEnd': {
|
|
378
|
+
|
|
379
|
+
const s = this.attributeStack.pop();
|
|
380
|
+
if ( s ) {
|
|
381
|
+
|
|
382
|
+
this.ctm = s.ctm;
|
|
383
|
+
this.state = { material: s.material, areaLight: s.areaLight, reverseOrientation: s.reverseOrientation };
|
|
384
|
+
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
break;
|
|
388
|
+
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
case 'TransformBegin': this.transformStack.push( this.ctm.slice() ); break;
|
|
392
|
+
case 'TransformEnd': {
|
|
393
|
+
|
|
394
|
+
const m = this.transformStack.pop(); if ( m ) this.ctm = m; break;
|
|
395
|
+
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
case 'ReverseOrientation': this.state.reverseOrientation = ! this.state.reverseOrientation; break;
|
|
399
|
+
|
|
400
|
+
// `Attribute "target" params` — v4 default-setting; ignored for MVP.
|
|
401
|
+
case 'Attribute': this._expectString( 'Attribute target' ); this._parseParams(); break;
|
|
402
|
+
case 'ActiveTransform': this._next(); break; // StartTime|EndTime|All
|
|
403
|
+
case 'TransformTimes': this._readNumbers( 2 ); break;
|
|
404
|
+
case 'MediumInterface': {
|
|
405
|
+
|
|
406
|
+
// up to two strings (inside/outside)
|
|
407
|
+
if ( this._peek() && this._peek().type === TokenType.STRING ) this._next();
|
|
408
|
+
if ( this._peek() && this._peek().type === TokenType.STRING ) this._next();
|
|
409
|
+
break;
|
|
410
|
+
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
case 'MakeNamedMedium': this._skipNamedAndParams(); break;
|
|
414
|
+
|
|
415
|
+
// ── materials ──
|
|
416
|
+
case 'Material': {
|
|
417
|
+
|
|
418
|
+
const type = this._expectString( 'Material type' );
|
|
419
|
+
const params = this._parseParams();
|
|
420
|
+
this.state.material = { type, params };
|
|
421
|
+
break;
|
|
422
|
+
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
case 'MakeNamedMaterial': {
|
|
426
|
+
|
|
427
|
+
const matName = this._expectString( 'MakeNamedMaterial name' );
|
|
428
|
+
const params = this._parseParams();
|
|
429
|
+
const type = this._str( params.type, 'diffuse' );
|
|
430
|
+
this.ir.namedMaterials.set( matName, { type, params } );
|
|
431
|
+
break;
|
|
432
|
+
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
case 'NamedMaterial': {
|
|
436
|
+
|
|
437
|
+
const ref = this._expectString( 'NamedMaterial name' );
|
|
438
|
+
const def = this.ir.namedMaterials.get( ref );
|
|
439
|
+
this.state.material = def || { type: 'diffuse', params: {}, _missingRef: ref };
|
|
440
|
+
if ( ! def ) this._warn( `NamedMaterial "${ref}" referenced before definition` );
|
|
441
|
+
break;
|
|
442
|
+
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
case 'Texture': {
|
|
446
|
+
|
|
447
|
+
const texName = this._expectString( 'Texture name' );
|
|
448
|
+
const dataType = this._expectString( 'Texture data type' ); // float | spectrum
|
|
449
|
+
const texClass = this._expectString( 'Texture class' ); // imagemap | scale | ...
|
|
450
|
+
const params = this._parseParams();
|
|
451
|
+
this.ir.namedTextures.set( texName, { dataType, class: texClass, params } );
|
|
452
|
+
break;
|
|
453
|
+
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ── lights ──
|
|
457
|
+
case 'AreaLightSource': {
|
|
458
|
+
|
|
459
|
+
const type = this._expectString( 'AreaLightSource type' );
|
|
460
|
+
const params = this._parseParams();
|
|
461
|
+
this.state.areaLight = { type, params };
|
|
462
|
+
break;
|
|
463
|
+
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
case 'LightSource': {
|
|
467
|
+
|
|
468
|
+
const type = this._expectString( 'LightSource type' );
|
|
469
|
+
const params = this._parseParams();
|
|
470
|
+
this.ir.lights.push( { type, params, ctm: this.ctm.slice() } );
|
|
471
|
+
break;
|
|
472
|
+
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── geometry ──
|
|
476
|
+
case 'Shape': {
|
|
477
|
+
|
|
478
|
+
const type = this._expectString( 'Shape type' );
|
|
479
|
+
const params = this._parseParams();
|
|
480
|
+
const shape = {
|
|
481
|
+
type,
|
|
482
|
+
params,
|
|
483
|
+
ctm: this.ctm.slice(),
|
|
484
|
+
material: this.state.material,
|
|
485
|
+
areaLight: this.state.areaLight,
|
|
486
|
+
reverseOrientation: this.state.reverseOrientation
|
|
487
|
+
};
|
|
488
|
+
this._emitShape( shape );
|
|
489
|
+
break;
|
|
490
|
+
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ── instancing ──
|
|
494
|
+
case 'ObjectBegin': {
|
|
495
|
+
|
|
496
|
+
const objName = this._expectString( 'ObjectBegin name' );
|
|
497
|
+
// pbrt implicitly pushes graphics state.
|
|
498
|
+
this.attributeStack.push( {
|
|
499
|
+
ctm: this.ctm.slice(),
|
|
500
|
+
material: this.state.material,
|
|
501
|
+
areaLight: this.state.areaLight,
|
|
502
|
+
reverseOrientation: this.state.reverseOrientation
|
|
503
|
+
} );
|
|
504
|
+
this.currentObject = objName;
|
|
505
|
+
this.objectBeginCTM = this.ctm.slice();
|
|
506
|
+
if ( ! this.ir.objects.has( objName ) ) this.ir.objects.set( objName, [] );
|
|
507
|
+
break;
|
|
508
|
+
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
case 'ObjectEnd': {
|
|
512
|
+
|
|
513
|
+
this.currentObject = null;
|
|
514
|
+
this.objectBeginCTM = null;
|
|
515
|
+
const s = this.attributeStack.pop();
|
|
516
|
+
if ( s ) {
|
|
517
|
+
|
|
518
|
+
this.ctm = s.ctm;
|
|
519
|
+
this.state = { material: s.material, areaLight: s.areaLight, reverseOrientation: s.reverseOrientation };
|
|
520
|
+
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
break;
|
|
524
|
+
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
case 'ObjectInstance': {
|
|
528
|
+
|
|
529
|
+
const objName = this._expectString( 'ObjectInstance name' );
|
|
530
|
+
this.ir.instances.push( { name: objName, ctm: this.ctm.slice() } );
|
|
531
|
+
break;
|
|
532
|
+
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ── file inclusion ──
|
|
536
|
+
case 'Include':
|
|
537
|
+
case 'Import': {
|
|
538
|
+
|
|
539
|
+
const path = this._expectString( name );
|
|
540
|
+
this._include( path );
|
|
541
|
+
break;
|
|
542
|
+
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
default:
|
|
546
|
+
this._warnUnknown( name );
|
|
547
|
+
// Best effort: swallow any trailing parameter list to stay in sync.
|
|
548
|
+
this._parseParams();
|
|
549
|
+
break;
|
|
550
|
+
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ── directive support ──────────────────────────────────────────
|
|
556
|
+
|
|
557
|
+
_emitShape( shape ) {
|
|
558
|
+
|
|
559
|
+
if ( this.currentObject !== null ) {
|
|
560
|
+
|
|
561
|
+
// Store relative to the ObjectBegin frame so instances can re-place it.
|
|
562
|
+
shape.relativeCTM = M.multiply( M.invert( this.objectBeginCTM ), shape.ctm );
|
|
563
|
+
this.ir.objects.get( this.currentObject ).push( shape );
|
|
564
|
+
|
|
565
|
+
} else {
|
|
566
|
+
|
|
567
|
+
this.ir.shapes.push( shape );
|
|
568
|
+
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
_include( path ) {
|
|
574
|
+
|
|
575
|
+
const text = this.resolveInclude( path, this.dirStack[ this.dirStack.length - 1 ] );
|
|
576
|
+
if ( text == null ) {
|
|
577
|
+
|
|
578
|
+
this._warn( `Include target not found: ${path}` );
|
|
579
|
+
return;
|
|
580
|
+
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const dir = path.includes( '/' ) ? path.slice( 0, path.lastIndexOf( '/' ) ) : '';
|
|
584
|
+
this.dirStack.push( dir );
|
|
585
|
+
this._run( tokenize( text ) );
|
|
586
|
+
this.dirStack.pop();
|
|
587
|
+
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
_skipTypeAndParams() {
|
|
591
|
+
|
|
592
|
+
// "type" then params
|
|
593
|
+
if ( this._peek() && this._peek().type === TokenType.STRING ) this._next();
|
|
594
|
+
this._parseParams();
|
|
595
|
+
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
_skipNamedAndParams() {
|
|
599
|
+
|
|
600
|
+
if ( this._peek() && this._peek().type === TokenType.STRING ) this._next();
|
|
601
|
+
this._parseParams();
|
|
602
|
+
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ── param coercion helpers ─────────────────────────────────────
|
|
606
|
+
|
|
607
|
+
_num( p, dflt ) {
|
|
608
|
+
|
|
609
|
+
return p && p.value.length ? p.value[ 0 ] : dflt;
|
|
610
|
+
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
_str( p, dflt ) {
|
|
614
|
+
|
|
615
|
+
return p && p.value.length ? p.value[ 0 ] : dflt;
|
|
616
|
+
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ── diagnostics ────────────────────────────────────────────────
|
|
620
|
+
|
|
621
|
+
_warn( msg ) {
|
|
622
|
+
|
|
623
|
+
this.ir.warnings.push( msg );
|
|
624
|
+
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
_warnUnknown( name ) {
|
|
628
|
+
|
|
629
|
+
if ( this._warnedUnknown.has( name ) ) return;
|
|
630
|
+
this._warnedUnknown.add( name );
|
|
631
|
+
this._warn( `Unsupported directive ignored: ${name}` );
|
|
632
|
+
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** Convenience: parse a string into IR with no Include support. */
|
|
638
|
+
export function parsePBRT( src, opts ) {
|
|
639
|
+
|
|
640
|
+
return new PBRTParser( opts ).parse( src );
|
|
641
|
+
|
|
642
|
+
}
|