rayzee 7.31.1 → 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 +6366 -4361
- package/dist/rayzee.umd.js +122 -73
- package/package.json +1 -1
- package/src/EngineDefaults.js +168 -13
- package/src/EngineIssues.js +5 -0
- package/src/PathTracerApp.js +120 -19
- package/src/Processor/ArchiveReader.js +538 -0
- package/src/Processor/AssetLoader.js +265 -23
- 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 +32 -8
- package/src/Processor/PBRT/PBRTCurves.js +249 -0
- package/src/Processor/PBRT/PBRTMath.js +19 -0
- package/src/Processor/PBRT/PBRTParser.js +187 -41
- package/src/Processor/PBRT/PBRTSceneBuilder.js +586 -42
- package/src/Processor/PBRT/PBRTTokenizer.js +205 -65
- package/src/Processor/PBRT/index.js +257 -50
- 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 +1046 -365
- 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/MaterialDataManager.js +29 -23
- package/src/managers/TransformManager.js +9 -192
|
@@ -14,16 +14,133 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
|
-
Group, Mesh, PerspectiveCamera, Matrix4, Vector3,
|
|
17
|
+
Group, Mesh, InstancedMesh, PerspectiveCamera, Matrix4, Vector3,
|
|
18
18
|
BufferGeometry, Float32BufferAttribute, Uint32BufferAttribute, SphereGeometry,
|
|
19
|
-
DataTexture, FloatType, RGBAFormat, LinearFilter, EquirectangularReflectionMapping
|
|
19
|
+
DataTexture, FloatType, RGBAFormat, LinearFilter, EquirectangularReflectionMapping,
|
|
20
|
+
SRGBColorSpace
|
|
20
21
|
} from 'three';
|
|
21
22
|
import { buildMaterial, pFloat, pString, resolveSpectrum } from './PBRTMaterials.js';
|
|
22
23
|
import { loopSubdivide } from './LoopSubdivision.js';
|
|
23
24
|
import { octahedralToEquirect } from './EqualAreaOctahedral.js';
|
|
25
|
+
import { tessellateCurve } from './PBRTCurves.js';
|
|
24
26
|
import * as M from './PBRTMath.js';
|
|
25
27
|
|
|
26
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
|
+
}
|
|
27
144
|
|
|
28
145
|
export class PBRTSceneBuilder {
|
|
29
146
|
|
|
@@ -36,6 +153,11 @@ export class PBRTSceneBuilder {
|
|
|
36
153
|
*/
|
|
37
154
|
constructor( resolvers = {} ) {
|
|
38
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;
|
|
39
161
|
this.resolvePLY = resolvers.resolvePLY || ( async () => null );
|
|
40
162
|
this.resolveImage = resolvers.resolveImage || ( async () => null );
|
|
41
163
|
this.resolveEnvironment = resolvers.resolveEnvironment || resolvers.resolveImage || ( async () => null );
|
|
@@ -45,6 +167,10 @@ export class PBRTSceneBuilder {
|
|
|
45
167
|
this.report = []; // per-mesh diagnostics for debugging imports
|
|
46
168
|
this._materialCache = new Map(); // material obj -> Map(areaLight obj -> MeshPhysicalMaterial)
|
|
47
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>
|
|
48
174
|
|
|
49
175
|
}
|
|
50
176
|
|
|
@@ -61,19 +187,71 @@ export class PBRTSceneBuilder {
|
|
|
61
187
|
async build( ir ) {
|
|
62
188
|
|
|
63
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();
|
|
64
196
|
const group = new Group();
|
|
65
197
|
group.name = 'PBRTScene';
|
|
66
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
|
+
|
|
67
204
|
// Shapes (direct + instanced)
|
|
68
205
|
for ( let i = 0; i < ir.shapes.length; i ++ ) {
|
|
69
206
|
|
|
70
|
-
|
|
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}` );
|
|
71
230
|
if ( mesh ) group.add( mesh );
|
|
72
231
|
|
|
73
232
|
}
|
|
74
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
|
+
|
|
75
247
|
await this._buildInstances( ir, group );
|
|
76
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
|
+
|
|
77
255
|
// Camera
|
|
78
256
|
let camera = null;
|
|
79
257
|
if ( ir.camera ) {
|
|
@@ -88,9 +266,19 @@ export class PBRTSceneBuilder {
|
|
|
88
266
|
|
|
89
267
|
this._reportUnsupportedLights( ir.lights );
|
|
90
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
|
+
|
|
91
273
|
return {
|
|
92
274
|
group, camera, environment,
|
|
93
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,
|
|
94
282
|
warnings: this.warnings.concat( ir.warnings || [] )
|
|
95
283
|
};
|
|
96
284
|
|
|
@@ -98,24 +286,108 @@ export class PBRTSceneBuilder {
|
|
|
98
286
|
|
|
99
287
|
// ── shapes ─────────────────────────────────────────────────────
|
|
100
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
|
+
*/
|
|
101
319
|
async _buildInstances( ir, group ) {
|
|
102
320
|
|
|
103
321
|
let n = 0;
|
|
104
|
-
|
|
322
|
+
if ( ir.skippedInstances > 0 ) this.skippedForBudget += ir.skippedInstances;
|
|
323
|
+
|
|
324
|
+
const scratch = new Float64Array( 16 );
|
|
325
|
+
const matrix = new Matrix4();
|
|
326
|
+
|
|
327
|
+
for ( const [ name, list ] of ir.instances ) {
|
|
105
328
|
|
|
106
|
-
const template = ir.objects.get(
|
|
329
|
+
const template = ir.objects.get( name );
|
|
107
330
|
if ( ! template ) {
|
|
108
331
|
|
|
109
|
-
|
|
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;
|
|
110
337
|
|
|
111
338
|
}
|
|
112
339
|
|
|
113
340
|
for ( const shape of template ) {
|
|
114
341
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
} );
|
|
119
391
|
|
|
120
392
|
}
|
|
121
393
|
|
|
@@ -131,21 +403,14 @@ export class PBRTSceneBuilder {
|
|
|
131
403
|
this._getMaterial( shape )
|
|
132
404
|
] );
|
|
133
405
|
if ( ! geometry ) return null;
|
|
406
|
+
return this._meshFromGeometry( shape, geometry, sharedMaterial, ctm, name );
|
|
134
407
|
|
|
135
|
-
|
|
136
|
-
// usual cause of "black"/wrong meshes on import. Drop the map, but on a CLONE:
|
|
137
|
-
// _getMaterial caches and shares one instance across every shape using the
|
|
138
|
-
// same NamedMaterial, so mutating it would strip the texture from sibling
|
|
139
|
-
// meshes that DO have UVs.
|
|
140
|
-
const hasUV = !! geometry.getAttribute( 'uv' );
|
|
141
|
-
let material = sharedMaterial;
|
|
142
|
-
if ( sharedMaterial.map && ! hasUV ) {
|
|
408
|
+
}
|
|
143
409
|
|
|
144
|
-
|
|
145
|
-
material = sharedMaterial.clone();
|
|
146
|
-
material.map = null;
|
|
410
|
+
_meshFromGeometry( shape, geometry, sharedMaterial, ctm, name ) {
|
|
147
411
|
|
|
148
|
-
|
|
412
|
+
const hasUV = !! geometry.getAttribute( 'uv' );
|
|
413
|
+
const material = this._materialForGeometry( shape, geometry, sharedMaterial, name );
|
|
149
414
|
|
|
150
415
|
const mesh = new Mesh( geometry, material );
|
|
151
416
|
mesh.name = name;
|
|
@@ -161,12 +426,15 @@ export class PBRTSceneBuilder {
|
|
|
161
426
|
|
|
162
427
|
// World-space dimensions (object bbox transformed by the baked matrix) —
|
|
163
428
|
// surfaces an oversized/under-scaled mesh at a glance.
|
|
164
|
-
geometry.computeBoundingBox();
|
|
429
|
+
if ( ! geometry.boundingBox ) geometry.computeBoundingBox();
|
|
165
430
|
const worldSize = geometry.boundingBox
|
|
166
431
|
? geometry.boundingBox.clone().applyMatrix4( mesh.matrix ).getSize( new Vector3() )
|
|
167
432
|
: new Vector3();
|
|
168
433
|
|
|
169
|
-
this.
|
|
434
|
+
const tris = this._accountGeometry( geometry );
|
|
435
|
+
this.placementCount ++;
|
|
436
|
+
this.reportedMeshes ++;
|
|
437
|
+
if ( this.report.length < MAX_REPORT_ROWS ) this.report.push( {
|
|
170
438
|
mesh: name,
|
|
171
439
|
shape: shape.type,
|
|
172
440
|
material: shape.material?.type || 'diffuse',
|
|
@@ -176,20 +444,181 @@ export class PBRTSceneBuilder {
|
|
|
176
444
|
normals: geometry.getAttribute( 'normal' ) ? 'yes' : 'NO',
|
|
177
445
|
emissive: material.emissiveIntensity > 0 ? `#${material.emissive.getHexString()}×${material.emissiveIntensity}` : '-',
|
|
178
446
|
size: `${worldSize.x.toFixed( 2 )}×${worldSize.y.toFixed( 2 )}×${worldSize.z.toFixed( 2 )}`,
|
|
179
|
-
tris
|
|
447
|
+
tris
|
|
180
448
|
} );
|
|
181
449
|
|
|
182
450
|
return mesh;
|
|
183
451
|
|
|
184
452
|
}
|
|
185
453
|
|
|
186
|
-
|
|
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 ) {
|
|
187
615
|
|
|
188
616
|
switch ( shape.type ) {
|
|
189
617
|
|
|
190
618
|
case 'trianglemesh': return this._triangleMesh( shape.params );
|
|
191
619
|
case 'bilinearmesh': return this._bilinearMesh( shape.params );
|
|
192
620
|
case 'loopsubdiv': return this._loopSubdiv( shape.params );
|
|
621
|
+
case 'curve': return this._curve( shape.params );
|
|
193
622
|
case 'plymesh': return this._plyMesh( shape.params );
|
|
194
623
|
case 'sphere': return this._sphere( shape.params );
|
|
195
624
|
case 'disk': return this._disk( shape.params );
|
|
@@ -211,22 +640,64 @@ export class PBRTSceneBuilder {
|
|
|
211
640
|
}
|
|
212
641
|
|
|
213
642
|
const geo = new BufferGeometry();
|
|
214
|
-
geo.setAttribute( 'position', new Float32BufferAttribute(
|
|
643
|
+
geo.setAttribute( 'position', new Float32BufferAttribute( float32( P ), 3 ) );
|
|
215
644
|
|
|
216
645
|
const N = params.N?.value;
|
|
217
|
-
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 ) );
|
|
218
647
|
|
|
219
648
|
const uv = ( params.uv || params.st )?.value;
|
|
220
|
-
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 ) );
|
|
221
650
|
|
|
222
651
|
const indices = params.indices?.value;
|
|
223
|
-
if ( indices && indices.length ) geo.setIndex( indices );
|
|
652
|
+
if ( indices && indices.length ) geo.setIndex( indexAttribute( indices ) );
|
|
224
653
|
|
|
225
654
|
if ( ! N ) geo.computeVertexNormals();
|
|
226
655
|
return geo;
|
|
227
656
|
|
|
228
657
|
}
|
|
229
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
|
+
|
|
230
701
|
_loopSubdiv( params ) {
|
|
231
702
|
|
|
232
703
|
const P = params.P?.value;
|
|
@@ -281,7 +752,7 @@ export class PBRTSceneBuilder {
|
|
|
281
752
|
|
|
282
753
|
}
|
|
283
754
|
|
|
284
|
-
|
|
755
|
+
_plyMesh( params ) {
|
|
285
756
|
|
|
286
757
|
const filename = pString( params, 'filename', null );
|
|
287
758
|
if ( ! filename ) {
|
|
@@ -290,6 +761,15 @@ export class PBRTSceneBuilder {
|
|
|
290
761
|
|
|
291
762
|
}
|
|
292
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
|
+
|
|
293
773
|
try {
|
|
294
774
|
|
|
295
775
|
const geo = await this.resolvePLY( filename );
|
|
@@ -374,7 +854,28 @@ export class PBRTSceneBuilder {
|
|
|
374
854
|
|
|
375
855
|
if ( shape.areaLight ) await this._applyAreaLight( material, shape.areaLight, ctx );
|
|
376
856
|
|
|
377
|
-
|
|
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 );
|
|
378
879
|
return material;
|
|
379
880
|
|
|
380
881
|
}
|
|
@@ -435,7 +936,17 @@ export class PBRTSceneBuilder {
|
|
|
435
936
|
|
|
436
937
|
} else {
|
|
437
938
|
|
|
438
|
-
|
|
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
|
+
}
|
|
439
950
|
|
|
440
951
|
}
|
|
441
952
|
|
|
@@ -444,6 +955,24 @@ export class PBRTSceneBuilder {
|
|
|
444
955
|
|
|
445
956
|
}
|
|
446
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
|
+
|
|
447
976
|
async _resolveScaleTexture( name, def ) {
|
|
448
977
|
|
|
449
978
|
// pbrt-v4 uses "tex" (the inner texture or constant) and "scale" (the multiplier).
|
|
@@ -555,8 +1084,7 @@ export class PBRTSceneBuilder {
|
|
|
555
1084
|
_equirectFromInfiniteLight( tex, inf, scale, filename ) {
|
|
556
1085
|
|
|
557
1086
|
const image = tex.image;
|
|
558
|
-
|
|
559
|
-
if ( ! data || ! image.width || image.width !== image.height ) {
|
|
1087
|
+
if ( ! image?.width || image.width !== image.height ) {
|
|
560
1088
|
|
|
561
1089
|
if ( image?.width !== image?.height ) this.warn(
|
|
562
1090
|
`infinite-light image "${filename}" is ${image?.width}x${image?.height}, not square — ` +
|
|
@@ -566,14 +1094,30 @@ export class PBRTSceneBuilder {
|
|
|
566
1094
|
|
|
567
1095
|
}
|
|
568
1096
|
|
|
569
|
-
|
|
570
|
-
if ( ! Number.isInteger( channels ) || channels < 3 ) return null;
|
|
1097
|
+
let src = null;
|
|
571
1098
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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 );
|
|
577
1121
|
|
|
578
1122
|
const out = new DataTexture( pixels, width, height, RGBAFormat, FloatType );
|
|
579
1123
|
out.mapping = EquirectangularReflectionMapping;
|