rayzee 7.31.0 → 8.0.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 +164 -0
- package/dist/rayzee.es.js +6547 -4334
- package/dist/rayzee.umd.js +122 -73
- package/package.json +1 -1
- package/src/EngineDefaults.js +168 -13
- package/src/EngineIssues.js +6 -0
- package/src/PathTracerApp.js +128 -20
- package/src/Processor/ArchiveReader.js +538 -0
- package/src/Processor/AssetLoader.js +357 -22
- package/src/Processor/BVHBuilder.js +48 -32
- package/src/Processor/BVHRefitter.js +244 -99
- package/src/Processor/ChunkedRecords.js +302 -0
- package/src/Processor/EmissiveTriangleBuilder.js +48 -21
- package/src/Processor/GeometryExtractor.js +577 -92
- package/src/Processor/HostMemory.js +222 -0
- package/src/Processor/InstanceTable.js +548 -97
- package/src/Processor/PBRT/EqualAreaOctahedral.js +173 -0
- package/src/Processor/PBRT/LoopSubdivision.js +240 -0
- package/src/Processor/PBRT/PBRTCurves.js +249 -0
- package/src/Processor/PBRT/PBRTMath.js +28 -0
- package/src/Processor/PBRT/PBRTParser.js +187 -41
- package/src/Processor/PBRT/PBRTSceneBuilder.js +659 -34
- package/src/Processor/PBRT/PBRTTokenizer.js +205 -65
- package/src/Processor/PBRT/index.js +306 -45
- package/src/Processor/PackedRayBuffer.js +29 -18
- package/src/Processor/ParallelBVHBuilder.js +9 -8
- package/src/Processor/QueueManager.js +5 -3
- package/src/Processor/SceneProcessor.js +1063 -366
- package/src/Processor/TLASBuilder.js +546 -201
- package/src/Processor/TextureCreator.js +11 -9
- package/src/Processor/VRAMTracker.js +12 -3
- package/src/Processor/Workers/BVHRefitWorker.js +16 -11
- package/src/Processor/Workers/BVHWorker.js +12 -12
- package/src/Processor/Workers/TLASWorker.js +37 -0
- package/src/Stages/NormalDepth.js +6 -2
- package/src/Stages/PathTracer.js +5 -5
- package/src/Stages/PathTracerStage.js +163 -41
- package/src/TSL/BVHTraversal.js +208 -90
- package/src/TSL/Common.js +135 -2
- package/src/TSL/Displacement.js +21 -11
- package/src/TSL/EmissiveSampling.js +49 -25
- package/src/TSL/ExtendKernel.js +3 -2
- package/src/TSL/LightBVHSampling.js +7 -4
- package/src/TSL/LightsDirect.js +30 -29
- package/src/TSL/ShadeKernel.js +8 -6
- package/src/TSL/Struct.js +3 -0
- package/src/TSL/TextureSampling.js +24 -9
- package/src/TSL/patches.js +128 -3
- package/src/index.js +3 -0
- package/src/managers/AnimationManager.js +86 -76
- package/src/managers/CameraManager.js +11 -0
- package/src/managers/MaterialDataManager.js +29 -23
- package/src/managers/TransformManager.js +9 -192
|
@@ -14,14 +14,133 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
|
-
Group, Mesh, PerspectiveCamera, Matrix4, Vector3,
|
|
18
|
-
BufferGeometry, Float32BufferAttribute, SphereGeometry,
|
|
19
|
-
DataTexture, FloatType, RGBAFormat, LinearFilter, EquirectangularReflectionMapping
|
|
17
|
+
Group, Mesh, InstancedMesh, PerspectiveCamera, Matrix4, Vector3,
|
|
18
|
+
BufferGeometry, Float32BufferAttribute, Uint32BufferAttribute, SphereGeometry,
|
|
19
|
+
DataTexture, FloatType, RGBAFormat, LinearFilter, EquirectangularReflectionMapping,
|
|
20
|
+
SRGBColorSpace
|
|
20
21
|
} from 'three';
|
|
21
22
|
import { buildMaterial, pFloat, pString, resolveSpectrum } from './PBRTMaterials.js';
|
|
23
|
+
import { loopSubdivide } from './LoopSubdivision.js';
|
|
24
|
+
import { octahedralToEquirect } from './EqualAreaOctahedral.js';
|
|
25
|
+
import { tessellateCurve } from './PBRTCurves.js';
|
|
22
26
|
import * as M from './PBRTMath.js';
|
|
23
27
|
|
|
24
28
|
const FLIP_Z = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, 1 ];
|
|
29
|
+
const MAX_SKY_READBACK = 4096;
|
|
30
|
+
|
|
31
|
+
// setIndex() only wraps a plain Array; hand it a typed one and it lands on the geometry
|
|
32
|
+
// raw, with no .count, and every consumer downstream reads NaN.
|
|
33
|
+
function indexAttribute( indices ) {
|
|
34
|
+
|
|
35
|
+
if ( indices instanceof Uint32Array ) return new Uint32BufferAttribute( indices, 1 );
|
|
36
|
+
if ( ArrayBuffer.isView( indices ) && indices.BYTES_PER_ELEMENT === 4 ) {
|
|
37
|
+
|
|
38
|
+
return new Uint32BufferAttribute( new Uint32Array( indices.buffer, indices.byteOffset, indices.length ), 1 );
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new Uint32BufferAttribute( Uint32Array.from( indices ), 1 );
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function grow( array, needed ) {
|
|
47
|
+
|
|
48
|
+
if ( needed <= array.length ) return array;
|
|
49
|
+
let length = array.length || 1;
|
|
50
|
+
while ( length < needed ) length *= 2;
|
|
51
|
+
const out = new array.constructor( length );
|
|
52
|
+
out.set( array );
|
|
53
|
+
return out;
|
|
54
|
+
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Reuse a Float32Array parameter as-is; the IR is discarded once the scene is built. */
|
|
58
|
+
function float32( values ) {
|
|
59
|
+
|
|
60
|
+
return values instanceof Float32Array ? values : Float32Array.from( values );
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The per-mesh table is a debug aid; an instanced scene reaches tens of millions of rows.
|
|
65
|
+
const MAX_REPORT_ROWS = 1000;
|
|
66
|
+
// Triangles are chunked across several arrays now, so the 2 GB V8 cap no longer bounds them at
|
|
67
|
+
// 26.8M, and the GPU ceiling is higher still: one storage buffer of 4,096 MB at 80 B a triangle
|
|
68
|
+
// is 53.7M. What actually binds is CPU address space, measured on Moana: 40M (7.3 GB) and 45M
|
|
69
|
+
// (8.5 GB) load and render, 50M dies at 9.4 GB resident with nothing thrown and nothing logged.
|
|
70
|
+
// So the budget sits on the highest rung that survived — past it placements are skipped and the
|
|
71
|
+
// scene is reported as truncated, which beats taking the tab down. Raise `maxTriangles` per load
|
|
72
|
+
// to go further, on a freshly started browser.
|
|
73
|
+
const DEFAULT_TRIANGLE_BUDGET = 45_000_000;
|
|
74
|
+
// Placements are their own budget: geometry is shared, but each costs a TLAS leaf and an
|
|
75
|
+
// instance record. isCoastline's 5.09M loaded and rendered at 23 fps.
|
|
76
|
+
const DEFAULT_PLACEMENT_BUDGET = 6_000_000;
|
|
77
|
+
// Non-instanced shapes merge into a few large meshes: isPalmRig declares 173,338 of them,
|
|
78
|
+
// and one BufferGeometry + Mesh each measured 3.3 GB — the loader OOM'd before tracing.
|
|
79
|
+
const DEFAULT_MERGE_MIN_SHAPES = 256;
|
|
80
|
+
const MERGE_VERTEX_LIMIT = 4096;
|
|
81
|
+
const MERGE_BATCH_TRIANGLES = 500_000;
|
|
82
|
+
// Curve tessellation defaults. pbrt sweeps a spline analytically; a triangle tracer has to
|
|
83
|
+
// pick a resolution, and isMountainB alone declares 5.18M curves — two samples per span and
|
|
84
|
+
// a crossed pair of ribbons is 16 triangles a curve.
|
|
85
|
+
const DEFAULT_CURVE_STEPS = 2;
|
|
86
|
+
const DEFAULT_CURVE_SIDES = { flat: 1, ribbon: 1, cylinder: 2 };
|
|
87
|
+
|
|
88
|
+
/** Pixels out of an HTMLImageElement / ImageBitmap, which expose none of their own. */
|
|
89
|
+
function pixelsFromDrawable( image, srgb ) {
|
|
90
|
+
|
|
91
|
+
const size = Math.min( image.width, MAX_SKY_READBACK );
|
|
92
|
+
let canvas = null;
|
|
93
|
+
|
|
94
|
+
if ( typeof OffscreenCanvas !== 'undefined' ) canvas = new OffscreenCanvas( size, size );
|
|
95
|
+
else if ( typeof document !== 'undefined' ) {
|
|
96
|
+
|
|
97
|
+
canvas = document.createElement( 'canvas' );
|
|
98
|
+
canvas.width = canvas.height = size;
|
|
99
|
+
|
|
100
|
+
} else return null;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
|
|
104
|
+
const ctx = canvas.getContext( '2d', { willReadFrequently: true } );
|
|
105
|
+
if ( ! ctx ) return null;
|
|
106
|
+
ctx.drawImage( image, 0, 0, size, size );
|
|
107
|
+
const { data } = ctx.getImageData( 0, 0, size, size );
|
|
108
|
+
return { data, width: size, height: size, channels: 4, bottomUp: false, srgb };
|
|
109
|
+
|
|
110
|
+
} catch {
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Names an unreadable texture may have been derived from. Moana binds colour as
|
|
120
|
+
* `"texture reflectance" "el:part_Color"` and leaves `MakeNamedMaterial "el:part"` in the
|
|
121
|
+
* shared library, so the colour survives the .ptx files being a separate download.
|
|
122
|
+
*/
|
|
123
|
+
function materialNameCandidates( texName ) {
|
|
124
|
+
|
|
125
|
+
const out = [ texName ];
|
|
126
|
+
let n = texName;
|
|
127
|
+
const strips = [
|
|
128
|
+
v => v.replace( /-renamed-\d+$/, '' ),
|
|
129
|
+
v => v.replace( /\d+$/, '' ),
|
|
130
|
+
v => v.replace( /_Color$/i, '' ),
|
|
131
|
+
v => v.replace( /\d+$/, '' )
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
for ( const strip of strips ) {
|
|
135
|
+
|
|
136
|
+
const next = strip( n );
|
|
137
|
+
if ( next !== n && next ) out.push( n = next );
|
|
138
|
+
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return out;
|
|
142
|
+
|
|
143
|
+
}
|
|
25
144
|
|
|
26
145
|
export class PBRTSceneBuilder {
|
|
27
146
|
|
|
@@ -34,6 +153,11 @@ export class PBRTSceneBuilder {
|
|
|
34
153
|
*/
|
|
35
154
|
constructor( resolvers = {} ) {
|
|
36
155
|
|
|
156
|
+
this.maxTriangles = resolvers.maxTriangles ?? DEFAULT_TRIANGLE_BUDGET;
|
|
157
|
+
this.maxPlacements = resolvers.maxPlacements ?? DEFAULT_PLACEMENT_BUDGET;
|
|
158
|
+
this.mergeShapesAbove = resolvers.mergeShapesAbove ?? DEFAULT_MERGE_MIN_SHAPES;
|
|
159
|
+
this.curveSteps = resolvers.curveSteps ?? DEFAULT_CURVE_STEPS;
|
|
160
|
+
this.curveSides = resolvers.curveSides ?? null;
|
|
37
161
|
this.resolvePLY = resolvers.resolvePLY || ( async () => null );
|
|
38
162
|
this.resolveImage = resolvers.resolveImage || ( async () => null );
|
|
39
163
|
this.resolveEnvironment = resolvers.resolveEnvironment || resolvers.resolveImage || ( async () => null );
|
|
@@ -43,6 +167,10 @@ export class PBRTSceneBuilder {
|
|
|
43
167
|
this.report = []; // per-mesh diagnostics for debugging imports
|
|
44
168
|
this._materialCache = new Map(); // material obj -> Map(areaLight obj -> MeshPhysicalMaterial)
|
|
45
169
|
this._textureCache = new Map(); // texName -> { texture } | { constant }
|
|
170
|
+
this._geometryCache = new Map(); // shape object -> Promise<BufferGeometry|null>
|
|
171
|
+
this._materialBySignature = new Map(); // visual signature -> shared MeshPhysicalMaterial
|
|
172
|
+
this._noUVMaterials = new Map(); // textured material -> its map-less clone
|
|
173
|
+
this._plyCache = new Map(); // filename -> Promise<BufferGeometry|null>
|
|
46
174
|
|
|
47
175
|
}
|
|
48
176
|
|
|
@@ -59,19 +187,71 @@ export class PBRTSceneBuilder {
|
|
|
59
187
|
async build( ir ) {
|
|
60
188
|
|
|
61
189
|
this.ir = ir;
|
|
190
|
+
this._recoveredColors = 0;
|
|
191
|
+
this.reportedMeshes = 0;
|
|
192
|
+
this.triangleCount = 0; // stored triangles — a shared geometry counts once
|
|
193
|
+
this.placementCount = 0;
|
|
194
|
+
this.skippedForBudget = 0;
|
|
195
|
+
this._countedGeometries = new WeakSet();
|
|
62
196
|
const group = new Group();
|
|
63
197
|
group.name = 'PBRTScene';
|
|
64
198
|
|
|
199
|
+
this._batches = ir.shapes.length >= this.mergeShapesAbove ? new Map() : null;
|
|
200
|
+
this._mergedBatches = 0;
|
|
201
|
+
this.mergedShapes = 0;
|
|
202
|
+
this.droppedNoTemplate = 0;
|
|
203
|
+
|
|
65
204
|
// Shapes (direct + instanced)
|
|
66
205
|
for ( let i = 0; i < ir.shapes.length; i ++ ) {
|
|
67
206
|
|
|
68
|
-
|
|
207
|
+
if ( this._overBudget() ) {
|
|
208
|
+
|
|
209
|
+
this.skippedForBudget += ir.shapes.length - i;
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const shape = ir.shapes[ i ];
|
|
215
|
+
const [ geometry, sharedMaterial ] = await Promise.all( [
|
|
216
|
+
this._batches ? this._createGeometry( shape ) : this._buildGeometry( shape ),
|
|
217
|
+
this._getMaterial( shape )
|
|
218
|
+
] );
|
|
219
|
+
if ( ! geometry ) continue;
|
|
220
|
+
|
|
221
|
+
if ( this._batches && geometry.getAttribute( 'position' ).count <= MERGE_VERTEX_LIMIT ) {
|
|
222
|
+
|
|
223
|
+
this._mergeShape( shape, geometry, sharedMaterial, group );
|
|
224
|
+
ir.shapes[ i ] = null; // its parsed arrays are copied out; let them go
|
|
225
|
+
continue;
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const mesh = this._meshFromGeometry( shape, geometry, sharedMaterial, shape.ctm, `shape_${i}` );
|
|
69
230
|
if ( mesh ) group.add( mesh );
|
|
70
231
|
|
|
71
232
|
}
|
|
72
233
|
|
|
234
|
+
if ( this._batches ) {
|
|
235
|
+
|
|
236
|
+
for ( const slot of this._batches.values() ) {
|
|
237
|
+
|
|
238
|
+
this._flushBatch( slot[ 0 ], group );
|
|
239
|
+
this._flushBatch( slot[ 1 ], group );
|
|
240
|
+
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this._batches = null;
|
|
244
|
+
|
|
245
|
+
}
|
|
246
|
+
|
|
73
247
|
await this._buildInstances( ir, group );
|
|
74
248
|
|
|
249
|
+
if ( this.skippedForBudget > 0 ) this.warn(
|
|
250
|
+
`stopped at ${this.triangleCount.toLocaleString()} stored triangles ` +
|
|
251
|
+
`(budget ${this.maxTriangles.toLocaleString()}) and ${this.placementCount.toLocaleString()} placements ` +
|
|
252
|
+
`(budget ${this.maxPlacements.toLocaleString()}); ${this.skippedForBudget.toLocaleString()} placement(s) skipped`
|
|
253
|
+
);
|
|
254
|
+
|
|
75
255
|
// Camera
|
|
76
256
|
let camera = null;
|
|
77
257
|
if ( ir.camera ) {
|
|
@@ -86,9 +266,19 @@ export class PBRTSceneBuilder {
|
|
|
86
266
|
|
|
87
267
|
this._reportUnsupportedLights( ir.lights );
|
|
88
268
|
|
|
269
|
+
if ( this._recoveredColors > 0 ) this.warn(
|
|
270
|
+
`${this._recoveredColors} unreadable texture(s) fell back to the like-named material's colour`
|
|
271
|
+
);
|
|
272
|
+
|
|
89
273
|
return {
|
|
90
274
|
group, camera, environment,
|
|
91
275
|
report: this.report,
|
|
276
|
+
meshCount: this.reportedMeshes,
|
|
277
|
+
triangleCount: this.triangleCount,
|
|
278
|
+
placementCount: this.placementCount,
|
|
279
|
+
mergedShapes: this.mergedShapes,
|
|
280
|
+
droppedNoTemplate: this.droppedNoTemplate,
|
|
281
|
+
skippedForBudget: this.skippedForBudget,
|
|
92
282
|
warnings: this.warnings.concat( ir.warnings || [] )
|
|
93
283
|
};
|
|
94
284
|
|
|
@@ -96,24 +286,108 @@ export class PBRTSceneBuilder {
|
|
|
96
286
|
|
|
97
287
|
// ── shapes ─────────────────────────────────────────────────────
|
|
98
288
|
|
|
289
|
+
_overBudget() {
|
|
290
|
+
|
|
291
|
+
return this.triangleCount >= this.maxTriangles || this.placementCount >= this.maxPlacements;
|
|
292
|
+
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Charge a geometry's triangles to the storage budget once, however many placements use
|
|
297
|
+
* it. Returns its triangle count either way.
|
|
298
|
+
* @private
|
|
299
|
+
*/
|
|
300
|
+
_accountGeometry( geometry ) {
|
|
301
|
+
|
|
302
|
+
const tris = geometry.index ? geometry.index.count / 3 : geometry.getAttribute( 'position' ).count / 3;
|
|
303
|
+
if ( ! this._countedGeometries.has( geometry ) ) {
|
|
304
|
+
|
|
305
|
+
this._countedGeometries.add( geometry );
|
|
306
|
+
this.triangleCount += tris;
|
|
307
|
+
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return tris;
|
|
311
|
+
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Placements become InstancedMesh batches: one object per template shape, carrying a
|
|
316
|
+
* matrix per placement. Moana's beach ground cover alone is 21 million placements, and a
|
|
317
|
+
* Three.js object each would be the whole budget before a triangle is traced.
|
|
318
|
+
*/
|
|
99
319
|
async _buildInstances( ir, group ) {
|
|
100
320
|
|
|
101
321
|
let n = 0;
|
|
102
|
-
|
|
322
|
+
if ( ir.skippedInstances > 0 ) this.skippedForBudget += ir.skippedInstances;
|
|
323
|
+
|
|
324
|
+
const scratch = new Float64Array( 16 );
|
|
325
|
+
const matrix = new Matrix4();
|
|
103
326
|
|
|
104
|
-
|
|
327
|
+
for ( const [ name, list ] of ir.instances ) {
|
|
328
|
+
|
|
329
|
+
const template = ir.objects.get( name );
|
|
105
330
|
if ( ! template ) {
|
|
106
331
|
|
|
107
|
-
|
|
332
|
+
// Counted, not just warned: these are placements that silently leave the scene,
|
|
333
|
+
// and a caller reading skippedForBudget would otherwise be told nothing was lost.
|
|
334
|
+
this.droppedNoTemplate += list.count;
|
|
335
|
+
this.warn( `ObjectInstance "${name}" has no template — ${list.count.toLocaleString()} placement(s) dropped` );
|
|
336
|
+
continue;
|
|
108
337
|
|
|
109
338
|
}
|
|
110
339
|
|
|
111
340
|
for ( const shape of template ) {
|
|
112
341
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
342
|
+
if ( this._overBudget() ) {
|
|
343
|
+
|
|
344
|
+
this.skippedForBudget += list.count;
|
|
345
|
+
continue;
|
|
346
|
+
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const [ geometry, sharedMaterial ] = await Promise.all( [
|
|
350
|
+
this._buildGeometry( shape ),
|
|
351
|
+
this._getMaterial( shape )
|
|
352
|
+
] );
|
|
353
|
+
if ( ! geometry ) continue;
|
|
354
|
+
|
|
355
|
+
const tris = this._accountGeometry( geometry );
|
|
356
|
+
const affordable = Math.max( 0, this.maxPlacements - this.placementCount );
|
|
357
|
+
const count = Math.min( list.count, affordable );
|
|
358
|
+
if ( count < list.count ) this.skippedForBudget += list.count - count;
|
|
359
|
+
if ( count === 0 ) continue;
|
|
360
|
+
|
|
361
|
+
const material = this._materialForGeometry( shape, geometry, sharedMaterial, `instances_${name}` );
|
|
362
|
+
const mesh = new InstancedMesh( geometry, material, count );
|
|
363
|
+
mesh.name = `instance_${n ++}`;
|
|
364
|
+
mesh.frustumCulled = false;
|
|
365
|
+
|
|
366
|
+
const rel = shape.relativeCTM || shape.ctm;
|
|
367
|
+
for ( let i = 0; i < count; i ++ ) {
|
|
368
|
+
|
|
369
|
+
M.multiplyInto( scratch, list.matrices, i * 16, rel );
|
|
370
|
+
mesh.setMatrixAt( i, matrix.fromArray( this.convertHandedness ? M.multiply( FLIP_Z, scratch ) : scratch ) );
|
|
371
|
+
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
375
|
+
group.add( mesh );
|
|
376
|
+
|
|
377
|
+
this.placementCount += count;
|
|
378
|
+
this.reportedMeshes += count;
|
|
379
|
+
if ( this.report.length < MAX_REPORT_ROWS ) this.report.push( {
|
|
380
|
+
mesh: `${mesh.name} ×${count}`,
|
|
381
|
+
shape: shape.type,
|
|
382
|
+
material: shape.material?.type || 'diffuse',
|
|
383
|
+
color: '#' + material.color.getHexString(),
|
|
384
|
+
map: material.map ? 'yes' : '-',
|
|
385
|
+
uv: geometry.getAttribute( 'uv' ) ? 'yes' : 'NO',
|
|
386
|
+
normals: geometry.getAttribute( 'normal' ) ? 'yes' : 'NO',
|
|
387
|
+
emissive: material.emissiveIntensity > 0 ? `#${material.emissive.getHexString()}×${material.emissiveIntensity}` : '-',
|
|
388
|
+
size: `instanced`,
|
|
389
|
+
tris,
|
|
390
|
+
} );
|
|
117
391
|
|
|
118
392
|
}
|
|
119
393
|
|
|
@@ -129,21 +403,14 @@ export class PBRTSceneBuilder {
|
|
|
129
403
|
this._getMaterial( shape )
|
|
130
404
|
] );
|
|
131
405
|
if ( ! geometry ) return null;
|
|
406
|
+
return this._meshFromGeometry( shape, geometry, sharedMaterial, ctm, name );
|
|
132
407
|
|
|
133
|
-
|
|
134
|
-
// usual cause of "black"/wrong meshes on import. Drop the map, but on a CLONE:
|
|
135
|
-
// _getMaterial caches and shares one instance across every shape using the
|
|
136
|
-
// same NamedMaterial, so mutating it would strip the texture from sibling
|
|
137
|
-
// meshes that DO have UVs.
|
|
138
|
-
const hasUV = !! geometry.getAttribute( 'uv' );
|
|
139
|
-
let material = sharedMaterial;
|
|
140
|
-
if ( sharedMaterial.map && ! hasUV ) {
|
|
408
|
+
}
|
|
141
409
|
|
|
142
|
-
|
|
143
|
-
material = sharedMaterial.clone();
|
|
144
|
-
material.map = null;
|
|
410
|
+
_meshFromGeometry( shape, geometry, sharedMaterial, ctm, name ) {
|
|
145
411
|
|
|
146
|
-
|
|
412
|
+
const hasUV = !! geometry.getAttribute( 'uv' );
|
|
413
|
+
const material = this._materialForGeometry( shape, geometry, sharedMaterial, name );
|
|
147
414
|
|
|
148
415
|
const mesh = new Mesh( geometry, material );
|
|
149
416
|
mesh.name = name;
|
|
@@ -159,12 +426,15 @@ export class PBRTSceneBuilder {
|
|
|
159
426
|
|
|
160
427
|
// World-space dimensions (object bbox transformed by the baked matrix) —
|
|
161
428
|
// surfaces an oversized/under-scaled mesh at a glance.
|
|
162
|
-
geometry.computeBoundingBox();
|
|
429
|
+
if ( ! geometry.boundingBox ) geometry.computeBoundingBox();
|
|
163
430
|
const worldSize = geometry.boundingBox
|
|
164
431
|
? geometry.boundingBox.clone().applyMatrix4( mesh.matrix ).getSize( new Vector3() )
|
|
165
432
|
: new Vector3();
|
|
166
433
|
|
|
167
|
-
this.
|
|
434
|
+
const tris = this._accountGeometry( geometry );
|
|
435
|
+
this.placementCount ++;
|
|
436
|
+
this.reportedMeshes ++;
|
|
437
|
+
if ( this.report.length < MAX_REPORT_ROWS ) this.report.push( {
|
|
168
438
|
mesh: name,
|
|
169
439
|
shape: shape.type,
|
|
170
440
|
material: shape.material?.type || 'diffuse',
|
|
@@ -174,19 +444,181 @@ export class PBRTSceneBuilder {
|
|
|
174
444
|
normals: geometry.getAttribute( 'normal' ) ? 'yes' : 'NO',
|
|
175
445
|
emissive: material.emissiveIntensity > 0 ? `#${material.emissive.getHexString()}×${material.emissiveIntensity}` : '-',
|
|
176
446
|
size: `${worldSize.x.toFixed( 2 )}×${worldSize.y.toFixed( 2 )}×${worldSize.z.toFixed( 2 )}`,
|
|
177
|
-
tris
|
|
447
|
+
tris
|
|
178
448
|
} );
|
|
179
449
|
|
|
180
450
|
return mesh;
|
|
181
451
|
|
|
182
452
|
}
|
|
183
453
|
|
|
184
|
-
|
|
454
|
+
/**
|
|
455
|
+
* A textured material on geometry with no UVs samples a single texel — the usual cause of
|
|
456
|
+
* "black"/wrong meshes on import. Drop the map, but on a CLONE: _getMaterial shares one
|
|
457
|
+
* instance across every shape using the same NamedMaterial, so mutating it would strip
|
|
458
|
+
* the texture from siblings that DO have UVs.
|
|
459
|
+
* @private
|
|
460
|
+
*/
|
|
461
|
+
_materialForGeometry( shape, geometry, sharedMaterial, name ) {
|
|
462
|
+
|
|
463
|
+
if ( ! sharedMaterial.map || geometry.getAttribute( 'uv' ) ) return sharedMaterial;
|
|
464
|
+
|
|
465
|
+
// One clone per source material, not per shape — else 173,000 warnings and 173,000
|
|
466
|
+
// materials, defeating every downstream cache that keys on material identity.
|
|
467
|
+
let material = this._noUVMaterials.get( sharedMaterial );
|
|
468
|
+
if ( ! material ) {
|
|
469
|
+
|
|
470
|
+
this.warn( `${name} (${shape.type}, "${shape.material?.type || 'diffuse'}") has a texture map but no UVs — dropping map, using base color` );
|
|
471
|
+
material = sharedMaterial.clone();
|
|
472
|
+
material.map = null;
|
|
473
|
+
this._noUVMaterials.set( sharedMaterial, material );
|
|
474
|
+
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return material;
|
|
478
|
+
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Copy one shape's triangles into a world-space batch, keyed by material and UV presence.
|
|
483
|
+
* Nothing references the source geometry afterwards, so it is collectable on return.
|
|
484
|
+
* @private
|
|
485
|
+
*/
|
|
486
|
+
_mergeShape( shape, geometry, sharedMaterial, group ) {
|
|
487
|
+
|
|
488
|
+
const material = this._materialForGeometry( shape, geometry, sharedMaterial, 'merged shape' );
|
|
489
|
+
const position = geometry.getAttribute( 'position' );
|
|
490
|
+
const normal = geometry.getAttribute( 'normal' );
|
|
491
|
+
const uv = geometry.getAttribute( 'uv' );
|
|
492
|
+
const index = geometry.index;
|
|
493
|
+
const vertices = position.count;
|
|
494
|
+
|
|
495
|
+
// Charged directly: a merged shape stores its own copy even when two share one .ply.
|
|
496
|
+
this.triangleCount += ( index ? index.count : vertices ) / 3;
|
|
497
|
+
this.reportedMeshes ++;
|
|
498
|
+
this.mergedShapes ++;
|
|
499
|
+
|
|
500
|
+
let slot = this._batches.get( material );
|
|
501
|
+
if ( ! slot ) this._batches.set( material, slot = [ null, null ] );
|
|
502
|
+
const k = uv ? 1 : 0;
|
|
503
|
+
let batch = slot[ k ];
|
|
504
|
+
if ( ! batch ) slot[ k ] = batch = {
|
|
505
|
+
material,
|
|
506
|
+
positions: new Float32Array( 3072 ),
|
|
507
|
+
normals: new Float32Array( 3072 ),
|
|
508
|
+
uvs: uv ? new Float32Array( 2048 ) : null,
|
|
509
|
+
indices: new Uint32Array( 3072 ),
|
|
510
|
+
vertexCount: 0, indexCount: 0, triangles: 0
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const world = this.convertHandedness ? M.multiply( FLIP_Z, shape.ctm ) : shape.ctm;
|
|
514
|
+
const inv = M.invert( world );
|
|
515
|
+
const base = batch.vertexCount;
|
|
516
|
+
|
|
517
|
+
batch.positions = grow( batch.positions, ( base + vertices ) * 3 );
|
|
518
|
+
batch.normals = grow( batch.normals, ( base + vertices ) * 3 );
|
|
519
|
+
if ( uv ) batch.uvs = grow( batch.uvs, ( base + vertices ) * 2 );
|
|
520
|
+
|
|
521
|
+
const src = position.array, dst = batch.positions;
|
|
522
|
+
const nsrc = normal ? normal.array : null, ndst = batch.normals;
|
|
523
|
+
for ( let i = 0; i < vertices; i ++ ) {
|
|
524
|
+
|
|
525
|
+
const s = i * 3, d = ( base + i ) * 3;
|
|
526
|
+
const x = src[ s ], y = src[ s + 1 ], z = src[ s + 2 ];
|
|
527
|
+
dst[ d ] = world[ 0 ] * x + world[ 4 ] * y + world[ 8 ] * z + world[ 12 ];
|
|
528
|
+
dst[ d + 1 ] = world[ 1 ] * x + world[ 5 ] * y + world[ 9 ] * z + world[ 13 ];
|
|
529
|
+
dst[ d + 2 ] = world[ 2 ] * x + world[ 6 ] * y + world[ 10 ] * z + world[ 14 ];
|
|
530
|
+
|
|
531
|
+
if ( ! nsrc ) continue;
|
|
532
|
+
// Inverse transpose, which in column-major order is the inverse read by rows.
|
|
533
|
+
const nx = nsrc[ s ], ny = nsrc[ s + 1 ], nz = nsrc[ s + 2 ];
|
|
534
|
+
let a = inv[ 0 ] * nx + inv[ 1 ] * ny + inv[ 2 ] * nz;
|
|
535
|
+
let b = inv[ 4 ] * nx + inv[ 5 ] * ny + inv[ 6 ] * nz;
|
|
536
|
+
let c = inv[ 8 ] * nx + inv[ 9 ] * ny + inv[ 10 ] * nz;
|
|
537
|
+
const len = Math.hypot( a, b, c );
|
|
538
|
+
if ( len > 0 ) {
|
|
539
|
+
|
|
540
|
+
a /= len; b /= len; c /= len;
|
|
541
|
+
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
ndst[ d ] = a; ndst[ d + 1 ] = b; ndst[ d + 2 ] = c;
|
|
545
|
+
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if ( uv ) batch.uvs.set( uv.array.subarray( 0, vertices * 2 ), base * 2 );
|
|
549
|
+
|
|
550
|
+
const count = index ? index.count : vertices;
|
|
551
|
+
batch.indices = grow( batch.indices, batch.indexCount + count );
|
|
552
|
+
const idst = batch.indices;
|
|
553
|
+
const isrc = index ? index.array : null;
|
|
554
|
+
for ( let i = 0; i < count; i ++ ) idst[ batch.indexCount + i ] = base + ( isrc ? isrc[ i ] : i );
|
|
555
|
+
|
|
556
|
+
batch.vertexCount += vertices;
|
|
557
|
+
batch.indexCount += count;
|
|
558
|
+
batch.triangles += count / 3;
|
|
559
|
+
|
|
560
|
+
if ( batch.triangles >= MERGE_BATCH_TRIANGLES ) {
|
|
561
|
+
|
|
562
|
+
this._flushBatch( batch, group );
|
|
563
|
+
slot[ k ] = null;
|
|
564
|
+
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Turn one accumulated batch into a single Mesh at the scene origin. @private */
|
|
570
|
+
_flushBatch( batch, group ) {
|
|
571
|
+
|
|
572
|
+
if ( ! batch || batch.triangles === 0 ) return;
|
|
573
|
+
|
|
574
|
+
const geometry = new BufferGeometry();
|
|
575
|
+
geometry.setAttribute( 'position', new Float32BufferAttribute( batch.positions.slice( 0, batch.vertexCount * 3 ), 3 ) );
|
|
576
|
+
geometry.setAttribute( 'normal', new Float32BufferAttribute( batch.normals.slice( 0, batch.vertexCount * 3 ), 3 ) );
|
|
577
|
+
if ( batch.uvs ) geometry.setAttribute( 'uv', new Float32BufferAttribute( batch.uvs.slice( 0, batch.vertexCount * 2 ), 2 ) );
|
|
578
|
+
geometry.setIndex( new Uint32BufferAttribute( batch.indices.slice( 0, batch.indexCount ), 1 ) );
|
|
579
|
+
|
|
580
|
+
const mesh = new Mesh( geometry, batch.material );
|
|
581
|
+
mesh.name = `merged_${this._mergedBatches ++}`;
|
|
582
|
+
mesh.frustumCulled = false;
|
|
583
|
+
group.add( mesh );
|
|
584
|
+
|
|
585
|
+
this.placementCount ++;
|
|
586
|
+
if ( this.report.length < MAX_REPORT_ROWS ) this.report.push( {
|
|
587
|
+
mesh: mesh.name,
|
|
588
|
+
shape: 'merged',
|
|
589
|
+
material: '-',
|
|
590
|
+
color: '#' + batch.material.color.getHexString(),
|
|
591
|
+
map: batch.material.map ? 'yes' : '-',
|
|
592
|
+
uv: batch.uvs ? 'yes' : 'NO',
|
|
593
|
+
normals: 'yes',
|
|
594
|
+
emissive: batch.material.emissiveIntensity > 0 ? `#${batch.material.emissive.getHexString()}×${batch.material.emissiveIntensity}` : '-',
|
|
595
|
+
size: 'merged',
|
|
596
|
+
tris: batch.triangles
|
|
597
|
+
} );
|
|
598
|
+
|
|
599
|
+
batch.positions = batch.normals = batch.uvs = batch.indices = null;
|
|
600
|
+
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
_buildGeometry( shape ) {
|
|
604
|
+
|
|
605
|
+
// An ObjectInstance template is built once per placement, and the Moana palm debris
|
|
606
|
+
// alone places one 208-triangle leaf 2.25 million times. three.js is happy to share
|
|
607
|
+
// one BufferGeometry across meshes; only the per-mesh matrix differs.
|
|
608
|
+
let pending = this._geometryCache.get( shape );
|
|
609
|
+
if ( ! pending ) this._geometryCache.set( shape, pending = this._createGeometry( shape ) );
|
|
610
|
+
return pending;
|
|
611
|
+
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async _createGeometry( shape ) {
|
|
185
615
|
|
|
186
616
|
switch ( shape.type ) {
|
|
187
617
|
|
|
188
618
|
case 'trianglemesh': return this._triangleMesh( shape.params );
|
|
189
619
|
case 'bilinearmesh': return this._bilinearMesh( shape.params );
|
|
620
|
+
case 'loopsubdiv': return this._loopSubdiv( shape.params );
|
|
621
|
+
case 'curve': return this._curve( shape.params );
|
|
190
622
|
case 'plymesh': return this._plyMesh( shape.params );
|
|
191
623
|
case 'sphere': return this._sphere( shape.params );
|
|
192
624
|
case 'disk': return this._disk( shape.params );
|
|
@@ -208,22 +640,91 @@ export class PBRTSceneBuilder {
|
|
|
208
640
|
}
|
|
209
641
|
|
|
210
642
|
const geo = new BufferGeometry();
|
|
211
|
-
geo.setAttribute( 'position', new Float32BufferAttribute(
|
|
643
|
+
geo.setAttribute( 'position', new Float32BufferAttribute( float32( P ), 3 ) );
|
|
212
644
|
|
|
213
645
|
const N = params.N?.value;
|
|
214
|
-
if ( N && N.length === P.length ) geo.setAttribute( 'normal', new Float32BufferAttribute(
|
|
646
|
+
if ( N && N.length === P.length ) geo.setAttribute( 'normal', new Float32BufferAttribute( float32( N ), 3 ) );
|
|
215
647
|
|
|
216
648
|
const uv = ( params.uv || params.st )?.value;
|
|
217
|
-
if ( uv && uv.length === ( P.length / 3 ) * 2 ) geo.setAttribute( 'uv', new Float32BufferAttribute(
|
|
649
|
+
if ( uv && uv.length === ( P.length / 3 ) * 2 ) geo.setAttribute( 'uv', new Float32BufferAttribute( float32( uv ), 2 ) );
|
|
218
650
|
|
|
219
651
|
const indices = params.indices?.value;
|
|
220
|
-
if ( indices && indices.length ) geo.setIndex( indices );
|
|
652
|
+
if ( indices && indices.length ) geo.setIndex( indexAttribute( indices ) );
|
|
221
653
|
|
|
222
654
|
if ( ! N ) geo.computeVertexNormals();
|
|
223
655
|
return geo;
|
|
224
656
|
|
|
225
657
|
}
|
|
226
658
|
|
|
659
|
+
_curve( params ) {
|
|
660
|
+
|
|
661
|
+
const P = params.P?.value;
|
|
662
|
+
if ( ! P || P.length < 12 ) {
|
|
663
|
+
|
|
664
|
+
this.warn( 'curve needs at least 4 control points' ); return null;
|
|
665
|
+
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const degree = Math.round( pFloat( params, 'degree', 3 ) );
|
|
669
|
+
if ( degree !== 3 ) {
|
|
670
|
+
|
|
671
|
+
this.warn( `curve degree ${degree} not supported — only cubic` ); return null;
|
|
672
|
+
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const type = pString( params, 'type', 'flat' );
|
|
676
|
+
const width = pFloat( params, 'width', 1 );
|
|
677
|
+
const built = tessellateCurve( {
|
|
678
|
+
P,
|
|
679
|
+
basis: pString( params, 'basis', 'bezier' ),
|
|
680
|
+
width0: pFloat( params, 'width0', width ),
|
|
681
|
+
width1: pFloat( params, 'width1', width ),
|
|
682
|
+
N: params.N?.value || null,
|
|
683
|
+
steps: this.curveSteps,
|
|
684
|
+
sides: this.curveSides ?? DEFAULT_CURVE_SIDES[ type ] ?? 1
|
|
685
|
+
} );
|
|
686
|
+
|
|
687
|
+
if ( ! built ) {
|
|
688
|
+
|
|
689
|
+
this.warn( 'curve could not be tessellated' ); return null;
|
|
690
|
+
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const geo = new BufferGeometry();
|
|
694
|
+
geo.setAttribute( 'position', new Float32BufferAttribute( built.positions, 3 ) );
|
|
695
|
+
geo.setIndex( new Uint32BufferAttribute( built.indices, 1 ) );
|
|
696
|
+
geo.computeVertexNormals();
|
|
697
|
+
return geo;
|
|
698
|
+
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
_loopSubdiv( params ) {
|
|
702
|
+
|
|
703
|
+
const P = params.P?.value;
|
|
704
|
+
const indices = params.indices?.value;
|
|
705
|
+
if ( ! P || P.length < 9 || ! indices || indices.length < 3 ) {
|
|
706
|
+
|
|
707
|
+
this.warn( 'loopsubdiv missing P/indices' ); return null;
|
|
708
|
+
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// pbrt's default is 3; each level quadruples the face count, so a deep level on
|
|
712
|
+
// a dense cage is worth refusing rather than stalling the import.
|
|
713
|
+
const requested = Math.max( 0, Math.min( 6, Math.round( pFloat( params, 'levels', 3 ) ) ) );
|
|
714
|
+
const { positions, indices: faces, levels } = loopSubdivide( P, indices, requested );
|
|
715
|
+
|
|
716
|
+
if ( levels < requested ) this.warn(
|
|
717
|
+
`loopsubdiv refined ${levels}/${requested} levels — the control mesh is too dense for the triangle budget`
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
const geo = new BufferGeometry();
|
|
721
|
+
geo.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );
|
|
722
|
+
geo.setIndex( new Uint32BufferAttribute( faces, 1 ) );
|
|
723
|
+
geo.computeVertexNormals();
|
|
724
|
+
return geo;
|
|
725
|
+
|
|
726
|
+
}
|
|
727
|
+
|
|
227
728
|
// Bilinear patch mesh → triangulate each quad (P + indices in quads of 4).
|
|
228
729
|
_bilinearMesh( params ) {
|
|
229
730
|
|
|
@@ -251,7 +752,7 @@ export class PBRTSceneBuilder {
|
|
|
251
752
|
|
|
252
753
|
}
|
|
253
754
|
|
|
254
|
-
|
|
755
|
+
_plyMesh( params ) {
|
|
255
756
|
|
|
256
757
|
const filename = pString( params, 'filename', null );
|
|
257
758
|
if ( ! filename ) {
|
|
@@ -260,6 +761,15 @@ export class PBRTSceneBuilder {
|
|
|
260
761
|
|
|
261
762
|
}
|
|
262
763
|
|
|
764
|
+
// Two shapes can name the same .ply, and decoding it twice costs the full parse.
|
|
765
|
+
let pending = this._plyCache.get( filename );
|
|
766
|
+
if ( ! pending ) this._plyCache.set( filename, pending = this._decodePly( filename ) );
|
|
767
|
+
return pending;
|
|
768
|
+
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
async _decodePly( filename ) {
|
|
772
|
+
|
|
263
773
|
try {
|
|
264
774
|
|
|
265
775
|
const geo = await this.resolvePLY( filename );
|
|
@@ -344,7 +854,28 @@ export class PBRTSceneBuilder {
|
|
|
344
854
|
|
|
345
855
|
if ( shape.areaLight ) await this._applyAreaLight( material, shape.areaLight, ctx );
|
|
346
856
|
|
|
347
|
-
|
|
857
|
+
const shared = this._dedupeMaterial( material );
|
|
858
|
+
byLight.set( shape.areaLight, shared );
|
|
859
|
+
return shared;
|
|
860
|
+
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Collapse materials that render identically onto one instance — an inline `Material` per
|
|
865
|
+
* shape is a distinct object every time, so every batch would hold exactly one shape.
|
|
866
|
+
* @private
|
|
867
|
+
*/
|
|
868
|
+
_dedupeMaterial( material ) {
|
|
869
|
+
|
|
870
|
+
const c = material.color, e = material.emissive;
|
|
871
|
+
const key = `${c.r},${c.g},${c.b}|${e.r},${e.g},${e.b}|${material.emissiveIntensity}|` +
|
|
872
|
+
`${material.map ? material.map.uuid : '-'}|${material.roughness}|${material.metalness}|` +
|
|
873
|
+
`${material.transmission}|${material.ior}|${material.thickness}|` +
|
|
874
|
+
`${material.clearcoat}|${material.clearcoatRoughness}|${material.opacity}|${material.side}`;
|
|
875
|
+
|
|
876
|
+
const existing = this._materialBySignature.get( key );
|
|
877
|
+
if ( existing ) return existing;
|
|
878
|
+
this._materialBySignature.set( key, material );
|
|
348
879
|
return material;
|
|
349
880
|
|
|
350
881
|
}
|
|
@@ -405,7 +936,17 @@ export class PBRTSceneBuilder {
|
|
|
405
936
|
|
|
406
937
|
} else {
|
|
407
938
|
|
|
408
|
-
|
|
939
|
+
const recovered = this._colorFromLikeNamedMaterial( name );
|
|
940
|
+
if ( recovered ) {
|
|
941
|
+
|
|
942
|
+
result = { constant: recovered.rgb };
|
|
943
|
+
this._recoveredColors ++;
|
|
944
|
+
|
|
945
|
+
} else {
|
|
946
|
+
|
|
947
|
+
this.warn( `texture class "${def.class}" not supported (texture "${name}")` );
|
|
948
|
+
|
|
949
|
+
}
|
|
409
950
|
|
|
410
951
|
}
|
|
411
952
|
|
|
@@ -414,6 +955,24 @@ export class PBRTSceneBuilder {
|
|
|
414
955
|
|
|
415
956
|
}
|
|
416
957
|
|
|
958
|
+
_colorFromLikeNamedMaterial( texName ) {
|
|
959
|
+
|
|
960
|
+
for ( const candidate of materialNameCandidates( texName ) ) {
|
|
961
|
+
|
|
962
|
+
const mat = this.ir?.namedMaterials?.get( candidate );
|
|
963
|
+
const p = mat?.params?.reflectance;
|
|
964
|
+
if ( p && ( p.type === 'rgb' || p.type === 'color' ) ) {
|
|
965
|
+
|
|
966
|
+
return { name: candidate, rgb: [ p.value[ 0 ], p.value[ 1 ], p.value[ 2 ] ] };
|
|
967
|
+
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
return null;
|
|
973
|
+
|
|
974
|
+
}
|
|
975
|
+
|
|
417
976
|
async _resolveScaleTexture( name, def ) {
|
|
418
977
|
|
|
419
978
|
// pbrt-v4 uses "tex" (the inner texture or constant) and "scale" (the multiplier).
|
|
@@ -487,6 +1046,16 @@ export class PBRTSceneBuilder {
|
|
|
487
1046
|
camera.up.copy( up.normalize() );
|
|
488
1047
|
camera.position.copy( eye );
|
|
489
1048
|
camera.lookAt( target );
|
|
1049
|
+
|
|
1050
|
+
// pbrt's camera space is left-handed: image-right is cameraToWorld's first column,
|
|
1051
|
+
// = cross(up, dir). three's lookAt() builds cross(up, -dir) — the opposite — so a
|
|
1052
|
+
// plain pbrt scene imports left-right flipped against pbrt's own render. An
|
|
1053
|
+
// exported scene's `Scale -1 1 1` already negates that column (det < 0), and then
|
|
1054
|
+
// lookAt() happens to agree and no correction is wanted. Hence: mirror exactly when
|
|
1055
|
+
// the scene does NOT. Verified against the reference images for contemporary-bathroom
|
|
1056
|
+
// (has the Scale) and killeroos/killeroo-simple (does not).
|
|
1057
|
+
if ( ( M.determinant3( cam.cameraToWorld ) > 0 ) !== this.convertHandedness ) camera.scale.x = - 1;
|
|
1058
|
+
|
|
490
1059
|
camera.updateMatrixWorld( true );
|
|
491
1060
|
return camera;
|
|
492
1061
|
|
|
@@ -506,6 +1075,59 @@ export class PBRTSceneBuilder {
|
|
|
506
1075
|
|
|
507
1076
|
// ── lights / environment ───────────────────────────────────────
|
|
508
1077
|
|
|
1078
|
+
/**
|
|
1079
|
+
* A square infinite-light image is pbrt's equal-area octahedral layout, not a
|
|
1080
|
+
* lat-long panorama — resample it, folding in the light's transform and `scale`.
|
|
1081
|
+
* Returns null when the image is not square (nothing to convert) or its pixels are
|
|
1082
|
+
* not readable, leaving the caller's equirectangular path in place.
|
|
1083
|
+
*/
|
|
1084
|
+
_equirectFromInfiniteLight( tex, inf, scale, filename ) {
|
|
1085
|
+
|
|
1086
|
+
const image = tex.image;
|
|
1087
|
+
if ( ! image?.width || image.width !== image.height ) {
|
|
1088
|
+
|
|
1089
|
+
if ( image?.width !== image?.height ) this.warn(
|
|
1090
|
+
`infinite-light image "${filename}" is ${image?.width}x${image?.height}, not square — ` +
|
|
1091
|
+
'read as equirectangular, but pbrt would read it as equal-area octahedral'
|
|
1092
|
+
);
|
|
1093
|
+
return null;
|
|
1094
|
+
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
let src = null;
|
|
1098
|
+
|
|
1099
|
+
if ( image.data ) {
|
|
1100
|
+
|
|
1101
|
+
const channels = image.data.length / ( image.width * image.height );
|
|
1102
|
+
if ( ! Number.isInteger( channels ) || channels < 3 ) return null;
|
|
1103
|
+
// three's EXR/HDR loaders fill DataTexture rows bottom-up; pbrt reads the square top-down.
|
|
1104
|
+
src = { data: image.data, width: image.width, height: image.height, channels, bottomUp: true };
|
|
1105
|
+
|
|
1106
|
+
} else {
|
|
1107
|
+
|
|
1108
|
+
// A PNG sky has no readable pixels of its own; without these the octahedral map
|
|
1109
|
+
// used to fall through and be sampled as a lat-long panorama.
|
|
1110
|
+
src = pixelsFromDrawable( image, tex.colorSpace === SRGBColorSpace );
|
|
1111
|
+
if ( ! src ) {
|
|
1112
|
+
|
|
1113
|
+
this.warn( `infinite-light image "${filename}" could not be read back for octahedral conversion` );
|
|
1114
|
+
return null;
|
|
1115
|
+
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const { data: pixels, width, height } = octahedralToEquirect( src, inf.ctm, scale );
|
|
1121
|
+
|
|
1122
|
+
const out = new DataTexture( pixels, width, height, RGBAFormat, FloatType );
|
|
1123
|
+
out.mapping = EquirectangularReflectionMapping;
|
|
1124
|
+
out.minFilter = LinearFilter;
|
|
1125
|
+
out.magFilter = LinearFilter;
|
|
1126
|
+
out.needsUpdate = true;
|
|
1127
|
+
return out;
|
|
1128
|
+
|
|
1129
|
+
}
|
|
1130
|
+
|
|
509
1131
|
async _buildEnvironment( lights ) {
|
|
510
1132
|
|
|
511
1133
|
const inf = lights.find( l => l.type === 'infinite' );
|
|
@@ -521,6 +1143,9 @@ export class PBRTSceneBuilder {
|
|
|
521
1143
|
const tex = await this.resolveEnvironment( filename );
|
|
522
1144
|
if ( tex ) {
|
|
523
1145
|
|
|
1146
|
+
const converted = this._equirectFromInfiniteLight( tex, inf, scale, filename );
|
|
1147
|
+
if ( converted ) return { texture: converted };
|
|
1148
|
+
|
|
524
1149
|
tex.mapping = EquirectangularReflectionMapping;
|
|
525
1150
|
return { texture: tex };
|
|
526
1151
|
|