rayzee 11.2.1 → 11.3.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 +58 -29
- package/dist/ArchiveImporter-D735xX-4.js +7860 -0
- package/dist/BidirectionalIntegrator-M89c0AlP.js +954 -0
- package/dist/{BufferLayout-D_IBo2ci.js → BufferLayout-xPRH9jYD.js} +41 -5
- package/dist/{NeuralRetouch-Cr-otSW8.js → NeuralRetouch-DiBH63kz.js} +1 -1
- package/dist/{NeuralSuperRes-B7E3_M0t.js → NeuralSuperRes-CZNQP13W.js} +1 -1
- package/dist/{PathTracer-D6nbknj5.js → PathTracer-BXCCVrPK.js} +4371 -4612
- package/dist/{PhysicalSky-Dq5VSiCQ.js → PhysicalSky-DqmaS0lb.js} +2 -2
- package/dist/{RayzeeRenderer-DbjqO6v-.js → RayzeeRenderer-DUSMFhmc.js} +426 -363
- package/dist/SolarLimb-bZE6y63S.js +762 -0
- package/dist/addons/archives.es.js +1 -1
- package/dist/addons/bidirectional.es.js +1 -1
- package/dist/addons/physical-sky.es.js +1 -1
- package/dist/rayzee-core.es.js +7 -7
- package/dist/rayzee.es.js +472 -468
- package/dist/rayzee.umd.js +106 -105
- package/dist/sceneCachePolicy-BAib5fCp.js +1602 -0
- package/package.json +1 -1
- package/src/PathTracerApp.js +4 -3
- package/src/Processor/ArchiveImporter.js +173 -94
- package/src/Processor/ArchiveReader.js +20 -23
- package/src/Processor/AssetLoader.js +48 -1
- package/src/Processor/BVHRefitter.js +109 -13
- package/src/Processor/BufferLayout.js +115 -0
- package/src/Processor/ChunkedRecords.js +3 -2
- package/src/Processor/EmissiveTriangleBuilder.js +6 -6
- package/src/Processor/GeometryExtractor.js +189 -79
- package/src/Processor/HostMemory.js +39 -9
- package/src/Processor/InstanceTable.js +714 -42
- package/src/Processor/PBRT/PBRTCurves.js +224 -6
- package/src/Processor/PBRT/PBRTMaterials.js +3 -0
- package/src/Processor/PBRT/PBRTSceneBuilder.js +82 -25
- package/src/Processor/PackedRayBuffer.js +5 -2
- package/src/Processor/SceneProcessor.js +747 -141
- package/src/Processor/TLASBuilder.js +32 -17
- package/src/Processor/USD/USDLayer.js +1149 -0
- package/src/Processor/USD/USDScene.js +1655 -0
- package/src/Processor/USD/USDStage.js +682 -0
- package/src/Processor/USD/USDText.js +674 -0
- package/src/Processor/USD/index.js +138 -0
- package/src/Processor/Workers/BVHRefitWorker.js +3 -3
- package/src/RayzeeRenderer.js +167 -26
- package/src/Stages/NormalDepth.js +7 -12
- package/src/Stages/PathTracer.js +12 -17
- package/src/Stages/PathTracerStage.js +273 -96
- package/src/Storage/GeometrySpill.js +46 -14
- package/src/Storage/SceneGraphCodec.js +17 -3
- package/src/Storage/SpillStore.js +9 -0
- package/src/TSL/BVHTraversal.js +183 -7
- package/src/TSL/Common.js +57 -13
- package/src/TSL/ShadeKernel.js +3 -2
- package/src/TSL/TextureSampling.js +11 -8
- package/src/TSL/patches.js +84 -0
- package/src/integrators/BidirectionalIntegrator.js +20 -2
- package/src/managers/DenoisingManager.js +4 -4
- package/src/managers/InteractionManager.js +19 -6
- package/src/managers/MaterialDataManager.js +22 -5
- package/src/managers/UniformManager.js +3 -0
- package/dist/ArchiveImporter-D_x863yu.js +0 -5198
- package/dist/BidirectionalIntegrator-DmkxxEOR.js +0 -946
- package/dist/SolarLimb-CPaeZVwz.js +0 -721
- package/dist/sceneCachePolicy-C4-4qwt6.js +0 -485
|
@@ -0,0 +1,1655 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A composed USD stage read into the scene description the pbrt importer's builder takes (PBRTSceneBuilder's IR), so
|
|
3
|
+
* USD scenes get the same instancing, budgets, merging and curve tessellation: meshes and curves become shapes,
|
|
4
|
+
* instanceable prims and point instancers become templates placed once per instance, materials MeshPhysicalMaterials.
|
|
5
|
+
* Cameras, lights and the dome light are built here, as three.js objects.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Matrix4, Quaternion, Vector3, Euler, Color, MeshPhysicalMaterial, DoubleSide, PerspectiveCamera, OrthographicCamera,
|
|
10
|
+
RectAreaLight, DirectionalLight, PointLight, SpotLight, SRGBColorSpace, NoColorSpace, RepeatWrapping,
|
|
11
|
+
ClampToEdgeWrapping, MirroredRepeatWrapping, BoxGeometry, SphereGeometry, CylinderGeometry, ConeGeometry,
|
|
12
|
+
CapsuleGeometry, PlaneGeometry,
|
|
13
|
+
} from 'three';
|
|
14
|
+
import { cloneMaterial } from '../PBRT/PBRTSceneBuilder.js';
|
|
15
|
+
|
|
16
|
+
const DEG = Math.PI / 180;
|
|
17
|
+
const GRID = 1 / 255;
|
|
18
|
+
const LIGHT_TYPES = new Set( [ 'RectLight', 'DiskLight', 'SphereLight', 'DistantLight', 'DomeLight', 'CylinderLight' ] );
|
|
19
|
+
const GPRIMS = new Set( [ 'Sphere', 'Cube', 'Cylinder', 'Cone', 'Capsule', 'Plane' ] );
|
|
20
|
+
const SKIPPED = new Set( [ 'Material', 'Shader', 'NodeGraph', 'GeomSubset', 'Volume', 'OpenVDBAsset', 'Field3DAsset', 'SkelAnimation', 'BlendShape' ] );
|
|
21
|
+
const PTEX = /\.(ptx|ptex)$/i;
|
|
22
|
+
const WRAP = { repeat: RepeatWrapping, mirror: MirroredRepeatWrapping, clamp: ClampToEdgeWrapping, black: ClampToEdgeWrapping };
|
|
23
|
+
|
|
24
|
+
function emptyIR() {
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
film: null, camera: null, namedMaterials: new Map(), namedTextures: new Map(), shapes: [], lights: [],
|
|
28
|
+
instances: new Map(), instanceCount: 0, skippedInstances: 0, objects: new Map(), media: new Map(),
|
|
29
|
+
transformTimes: { start: 0, end: 1 }, hasMotion: false, warnings: [],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const param = ( value, type = 'float' ) => ( { type, value } );
|
|
35
|
+
const word = value => ( { type: 'string', value: [ value ] } );
|
|
36
|
+
|
|
37
|
+
function opMatrix( name, value ) {
|
|
38
|
+
|
|
39
|
+
const kind = name.split( ':' )[ 1 ];
|
|
40
|
+
if ( value === undefined || value === null ) return null;
|
|
41
|
+
const m = new Matrix4();
|
|
42
|
+
switch ( kind ) {
|
|
43
|
+
|
|
44
|
+
case 'translate': return m.makeTranslation( value[ 0 ], value[ 1 ], value[ 2 ] );
|
|
45
|
+
case 'scale': return typeof value === 'number' ? m.makeScale( value, value, value ) : m.makeScale( value[ 0 ], value[ 1 ], value[ 2 ] );
|
|
46
|
+
case 'rotateX': return m.makeRotationX( value * DEG );
|
|
47
|
+
case 'rotateY': return m.makeRotationY( value * DEG );
|
|
48
|
+
case 'rotateZ': return m.makeRotationZ( value * DEG );
|
|
49
|
+
case 'orient': return m.makeRotationFromQuaternion( new Quaternion( value[ 0 ], value[ 1 ], value[ 2 ], value[ 3 ] ).normalize() );
|
|
50
|
+
case 'transform': return m.fromArray( value );
|
|
51
|
+
default: {
|
|
52
|
+
|
|
53
|
+
// rotateXYZ turns about X first; three's Euler names its matrix product, so the order reverses.
|
|
54
|
+
const axes = /^rotate([XYZ]{3})$/.exec( kind );
|
|
55
|
+
if ( ! axes ) return null;
|
|
56
|
+
return m.makeRotationFromEuler( new Euler( value[ 0 ] * DEG, value[ 1 ] * DEG, value[ 2 ] * DEG, [ ...axes[ 1 ] ].reverse().join( '' ) ) );
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function localTransform( prim ) {
|
|
65
|
+
|
|
66
|
+
const matrix = new Matrix4();
|
|
67
|
+
let reset = false;
|
|
68
|
+
for ( const raw of prim.value( 'xformOpOrder' ) ?? [] ) {
|
|
69
|
+
|
|
70
|
+
if ( raw === '!resetXformStack!' ) {
|
|
71
|
+
|
|
72
|
+
reset = true;
|
|
73
|
+
matrix.identity();
|
|
74
|
+
continue;
|
|
75
|
+
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const invert = raw.startsWith( '!invert!' );
|
|
79
|
+
const name = invert ? raw.slice( 8 ) : raw;
|
|
80
|
+
const op = opMatrix( name, prim.value( name ) );
|
|
81
|
+
if ( ! op ) continue;
|
|
82
|
+
if ( invert ) op.invert();
|
|
83
|
+
matrix.multiply( op );
|
|
84
|
+
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { matrix, reset };
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function primvar( prim, name ) {
|
|
92
|
+
|
|
93
|
+
const attr = `primvars:${name}`;
|
|
94
|
+
const values = prim.value( attr );
|
|
95
|
+
if ( values === undefined || values === null || values.length === 0 ) return null;
|
|
96
|
+
const indices = prim.value( `${attr}:indices` );
|
|
97
|
+
return { values, interpolation: prim.propertyMeta( attr, 'interpolation' ) ?? 'constant', indices: indices?.length ? indices : null };
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function meanColor( pv ) {
|
|
102
|
+
|
|
103
|
+
if ( ! pv ) return null;
|
|
104
|
+
const { values } = pv;
|
|
105
|
+
const sum = [ 0, 0, 0 ];
|
|
106
|
+
const n = Math.floor( values.length / 3 );
|
|
107
|
+
if ( n === 0 ) return null;
|
|
108
|
+
for ( let i = 0; i < n; i ++ ) for ( let c = 0; c < 3; c ++ ) sum[ c ] += values[ i * 3 + c ];
|
|
109
|
+
return sum.map( v => v / n );
|
|
110
|
+
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function elementAt( pv, face, corner, point ) {
|
|
114
|
+
|
|
115
|
+
const i = pv.interpolation === 'faceVarying' ? corner : pv.interpolation === 'uniform' ? face : pv.interpolation === 'constant' ? 0 : point;
|
|
116
|
+
return pv.indices ? pv.indices[ i ] : i;
|
|
117
|
+
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const perCorner = pv => pv && ( pv.indices || pv.interpolation === 'faceVarying' || pv.interpolation === 'uniform' || pv.interpolation === 'constant' );
|
|
121
|
+
|
|
122
|
+
/** Fan triangles; a primvar that varies per face corner makes each corner a vertex of its own. */
|
|
123
|
+
function triangulate( mesh, faces ) {
|
|
124
|
+
|
|
125
|
+
const { points, counts, offsets, fvi, leftHanded, normals, uv } = mesh;
|
|
126
|
+
const corners = perCorner( normals ) || perCorner( uv );
|
|
127
|
+
let triangles = 0, cornerCount = 0;
|
|
128
|
+
for ( const f of faces ) {
|
|
129
|
+
|
|
130
|
+
if ( counts[ f ] >= 3 ) {
|
|
131
|
+
|
|
132
|
+
triangles += counts[ f ] - 2;
|
|
133
|
+
cornerCount += counts[ f ];
|
|
134
|
+
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const index = new Uint32Array( triangles * 3 );
|
|
140
|
+
let t = 0;
|
|
141
|
+
const fan = ( base, n, at ) => {
|
|
142
|
+
|
|
143
|
+
for ( let k = 1; k + 1 < n; k ++ ) {
|
|
144
|
+
|
|
145
|
+
index[ t ++ ] = at( base, 0 );
|
|
146
|
+
index[ t ++ ] = at( base, leftHanded ? k + 1 : k );
|
|
147
|
+
index[ t ++ ] = at( base, leftHanded ? k : k + 1 );
|
|
148
|
+
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if ( ! corners ) {
|
|
154
|
+
|
|
155
|
+
for ( const f of faces ) if ( counts[ f ] >= 3 ) fan( offsets[ f ], counts[ f ], ( o, k ) => fvi[ o + k ] );
|
|
156
|
+
const pointCount = points.length / 3;
|
|
157
|
+
return {
|
|
158
|
+
P: points,
|
|
159
|
+
N: normals && normals.values.length === pointCount * 3 ? normals.values : null,
|
|
160
|
+
uv: uv && uv.values.length === pointCount * 2 ? uv.values : null,
|
|
161
|
+
index,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const P = new Float32Array( cornerCount * 3 );
|
|
167
|
+
const N = normals ? new Float32Array( cornerCount * 3 ) : null;
|
|
168
|
+
const UV = uv ? new Float32Array( cornerCount * 2 ) : null;
|
|
169
|
+
let v = 0;
|
|
170
|
+
for ( const f of faces ) {
|
|
171
|
+
|
|
172
|
+
const n = counts[ f ];
|
|
173
|
+
if ( n < 3 ) continue;
|
|
174
|
+
const base = v;
|
|
175
|
+
for ( let k = 0; k < n; k ++ ) {
|
|
176
|
+
|
|
177
|
+
const c = offsets[ f ] + k;
|
|
178
|
+
const p = fvi[ c ];
|
|
179
|
+
P[ v * 3 ] = points[ p * 3 ];
|
|
180
|
+
P[ v * 3 + 1 ] = points[ p * 3 + 1 ];
|
|
181
|
+
P[ v * 3 + 2 ] = points[ p * 3 + 2 ];
|
|
182
|
+
if ( N ) {
|
|
183
|
+
|
|
184
|
+
const e = elementAt( normals, f, c, p ) * 3;
|
|
185
|
+
N[ v * 3 ] = normals.values[ e ];
|
|
186
|
+
N[ v * 3 + 1 ] = normals.values[ e + 1 ];
|
|
187
|
+
N[ v * 3 + 2 ] = normals.values[ e + 2 ];
|
|
188
|
+
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if ( UV ) {
|
|
192
|
+
|
|
193
|
+
const e = elementAt( uv, f, c, p ) * 2;
|
|
194
|
+
UV[ v * 2 ] = uv.values[ e ];
|
|
195
|
+
UV[ v * 2 + 1 ] = uv.values[ e + 1 ];
|
|
196
|
+
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
v ++;
|
|
200
|
+
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fan( base, n, ( o, k ) => o + k );
|
|
204
|
+
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { P, N, uv: UV, index };
|
|
208
|
+
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function geometryArrays( geometry, axis ) {
|
|
212
|
+
|
|
213
|
+
if ( axis === 'Z' ) geometry.rotateX( Math.PI / 2 );
|
|
214
|
+
else if ( axis === 'X' ) geometry.rotateZ( - Math.PI / 2 );
|
|
215
|
+
const P = geometry.getAttribute( 'position' ).array;
|
|
216
|
+
const N = geometry.getAttribute( 'normal' )?.array ?? null;
|
|
217
|
+
const uv = geometry.getAttribute( 'uv' )?.array ?? null;
|
|
218
|
+
const index = geometry.index ? Uint32Array.from( geometry.index.array ) : Uint32Array.from( { length: P.length / 3 }, ( _, i ) => i );
|
|
219
|
+
return { P, N, uv, index };
|
|
220
|
+
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function quantize( rgb ) {
|
|
224
|
+
|
|
225
|
+
return rgb.map( v => Math.round( Math.min( Math.max( v, 0 ), 64 ) / GRID ) * GRID );
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** A fixed pseudo-random number in [0, 1) for item `i` of the set `salt`: what thinning keeps is the same every load. */
|
|
230
|
+
function hash01( i, salt ) {
|
|
231
|
+
|
|
232
|
+
let h = Math.imul( i ^ salt, 0x9E3779B1 );
|
|
233
|
+
h = Math.imul( h ^ ( h >>> 15 ), 0x85EBCA6B );
|
|
234
|
+
h = Math.imul( h ^ ( h >>> 13 ), 0xC2B2AE35 );
|
|
235
|
+
return ( ( h ^ ( h >>> 16 ) ) >>> 0 ) / 4294967296;
|
|
236
|
+
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function saltOf( text ) {
|
|
240
|
+
|
|
241
|
+
let h = 0x811C9DC5;
|
|
242
|
+
for ( let i = 0; i < text.length; i ++ ) h = Math.imul( h ^ text.charCodeAt( i ), 0x01000193 );
|
|
243
|
+
return h;
|
|
244
|
+
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Spans of one curve as tessellateCurves draws it. */
|
|
248
|
+
function curveSpans( n, type, basis, wrap ) {
|
|
249
|
+
|
|
250
|
+
if ( type === 'linear' ) return wrap === 'periodic' ? n : n - 1;
|
|
251
|
+
if ( basis === 'bezier' ) return wrap === 'periodic' ? n / 3 : ( n - 1 ) / 3;
|
|
252
|
+
if ( wrap === 'periodic' ) return n;
|
|
253
|
+
if ( wrap === 'pinned' ) return n + 1;
|
|
254
|
+
return n - 3;
|
|
255
|
+
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Adaptive tessellation keeps about this share of the uniform strip's triangles on Moana's curves.
|
|
259
|
+
const ADAPTIVE_SHARE = 0.65;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Rates that fit `demands` (key → cost) into `room`, smallest first: what fits under an even share is kept whole, and
|
|
263
|
+
* what is left is shared evenly among the rest — fronds and flowers stay, grass and ground cover thin.
|
|
264
|
+
*/
|
|
265
|
+
function fill( demands, room ) {
|
|
266
|
+
|
|
267
|
+
const rates = new Map();
|
|
268
|
+
const sorted = [ ...demands ].sort( ( a, b ) => a[ 1 ] - b[ 1 ] );
|
|
269
|
+
let left = Math.max( 0, room );
|
|
270
|
+
for ( let i = 0; i < sorted.length; i ++ ) {
|
|
271
|
+
|
|
272
|
+
const demand = sorted[ i ][ 1 ];
|
|
273
|
+
const share = left / ( sorted.length - i );
|
|
274
|
+
if ( demand <= share ) {
|
|
275
|
+
|
|
276
|
+
left -= demand;
|
|
277
|
+
continue;
|
|
278
|
+
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for ( let k = i; k < sorted.length; k ++ ) rates.set( sorted[ k ][ 0 ], share / sorted[ k ][ 1 ] );
|
|
282
|
+
break;
|
|
283
|
+
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return rates;
|
|
287
|
+
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Primvar elements per curve under each interpolation, as tessellateCurves reads them. */
|
|
291
|
+
function curveElements( n, mode, type, basis ) {
|
|
292
|
+
|
|
293
|
+
if ( mode === 'vertex' ) return n;
|
|
294
|
+
if ( mode === 'varying' ) return Math.max( 1, Math.round( type === 'linear' ? n - 1 : basis === 'bezier' ? ( n - 1 ) / 3 : n - 3 ) + 1 );
|
|
295
|
+
return 1;
|
|
296
|
+
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Keeps each curve with chance `rate`, with its points and primvars; null when none is kept. */
|
|
300
|
+
function thinCurves( curve, rate, salt ) {
|
|
301
|
+
|
|
302
|
+
const { counts, type, basis } = curve;
|
|
303
|
+
const kept = [];
|
|
304
|
+
const quota = Math.floor( counts.length * rate );
|
|
305
|
+
for ( let c = 0; c < counts.length && kept.length < quota; c ++ ) if ( hash01( c, salt ) < rate ) kept.push( c );
|
|
306
|
+
if ( kept.length === 0 ) return null;
|
|
307
|
+
if ( kept.length === counts.length ) return curve;
|
|
308
|
+
|
|
309
|
+
const pick = ( values, size, mode ) => {
|
|
310
|
+
|
|
311
|
+
if ( ! values?.length || ! mode || mode === 'constant' ) return values;
|
|
312
|
+
const starts = new Int32Array( counts.length + 1 );
|
|
313
|
+
for ( let c = 0; c < counts.length; c ++ ) starts[ c + 1 ] = starts[ c ] + curveElements( counts[ c ], mode, type, basis );
|
|
314
|
+
let total = 0;
|
|
315
|
+
for ( const c of kept ) total += starts[ c + 1 ] - starts[ c ];
|
|
316
|
+
const out = new values.constructor( total * size );
|
|
317
|
+
let o = 0;
|
|
318
|
+
for ( const c of kept ) {
|
|
319
|
+
|
|
320
|
+
const chunk = values.subarray( starts[ c ] * size, starts[ c + 1 ] * size );
|
|
321
|
+
out.set( chunk, o );
|
|
322
|
+
o += chunk.length;
|
|
323
|
+
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return out;
|
|
327
|
+
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
...curve,
|
|
332
|
+
points: pick( curve.points, 3, 'vertex' ),
|
|
333
|
+
counts: Int32Array.from( kept, c => counts[ c ] ),
|
|
334
|
+
widths: pick( curve.widths, 1, curve.widthMode ),
|
|
335
|
+
normals: pick( curve.normals, 3, curve.normalMode ),
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export class USDSceneReader {
|
|
341
|
+
|
|
342
|
+
constructor( stage, { maxTriangles = Infinity, maxPlacements = Infinity, curveSteps = 2, resolveImage = async () => null, warn = () => {} } = {} ) {
|
|
343
|
+
|
|
344
|
+
this.stage = stage;
|
|
345
|
+
this.maxTriangles = maxTriangles;
|
|
346
|
+
this.maxPlacements = maxPlacements;
|
|
347
|
+
this.curveSteps = curveSteps;
|
|
348
|
+
this.resolveImage = resolveImage;
|
|
349
|
+
this.warn = warn;
|
|
350
|
+
this.ir = emptyIR();
|
|
351
|
+
this.cameras = [];
|
|
352
|
+
this.lights = [];
|
|
353
|
+
this.domes = [];
|
|
354
|
+
this.prototypes = new Map();
|
|
355
|
+
this._surfaces = new Map();
|
|
356
|
+
this._colored = new Map();
|
|
357
|
+
this._wrappers = new Map();
|
|
358
|
+
this._images = new Map();
|
|
359
|
+
this._handed = new WeakSet();
|
|
360
|
+
this._warned = new Set();
|
|
361
|
+
this._protoCount = 0;
|
|
362
|
+
this._geometries = new Map();
|
|
363
|
+
this._specIds = new WeakMap();
|
|
364
|
+
this._nextSpecId = 0;
|
|
365
|
+
this.dry = false;
|
|
366
|
+
this.cost = null;
|
|
367
|
+
this.placementRates = new Map();
|
|
368
|
+
this.curveRates = new Map();
|
|
369
|
+
this.fitNote = null;
|
|
370
|
+
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
warnOnce( key, message ) {
|
|
374
|
+
|
|
375
|
+
if ( this._warned.has( key ) ) return;
|
|
376
|
+
this._warned.add( key );
|
|
377
|
+
this.warn( message );
|
|
378
|
+
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async read() {
|
|
382
|
+
|
|
383
|
+
if ( Number.isFinite( this.maxTriangles ) || Number.isFinite( this.maxPlacements ) ) await this.fit();
|
|
384
|
+
await this.walk();
|
|
385
|
+
this.shareGeometry();
|
|
386
|
+
return { ir: this.ir, cameras: this.cameras, lights: this.lights, dome: this.pickDome(), cost: this.cost };
|
|
387
|
+
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async walk() {
|
|
391
|
+
|
|
392
|
+
const root = this.stage.pseudoRoot;
|
|
393
|
+
const top = this.stage.upAxis === 'Z' ? new Matrix4().makeRotationX( - Math.PI / 2 ) : new Matrix4();
|
|
394
|
+
for ( const name of root.childNames() ) {
|
|
395
|
+
|
|
396
|
+
const prim = await this.stage.child( root, name );
|
|
397
|
+
if ( prim ) await this.visit( prim, top, this.world(), { binding: null, depth: 0 } );
|
|
398
|
+
this.stage.release();
|
|
399
|
+
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Counts what the whole scene costs first, so that one too large for the budgets is thinned evenly — curves, then
|
|
406
|
+
* point instancers' copies — rather than losing whatever is read last. Meshes are never thinned.
|
|
407
|
+
*/
|
|
408
|
+
async fit() {
|
|
409
|
+
|
|
410
|
+
this.dry = true;
|
|
411
|
+
this.cost = { mesh: 0, curves: 0, placements: 0, scatter: 0, counted: new Set(), curveSets: new Map(), instancers: new Map() };
|
|
412
|
+
await this.walk();
|
|
413
|
+
this.dry = false;
|
|
414
|
+
this.prototypes.clear();
|
|
415
|
+
|
|
416
|
+
const { mesh, curves, placements, scatter, curveSets, instancers } = this.cost;
|
|
417
|
+
delete this.cost.counted;
|
|
418
|
+
delete this.cost.curveSets;
|
|
419
|
+
delete this.cost.instancers;
|
|
420
|
+
if ( placements > this.maxPlacements ) this.placementRates = fill( instancers, ( this.maxPlacements - ( placements - scatter ) ) * 0.98 );
|
|
421
|
+
if ( mesh + curves > this.maxTriangles ) this.curveRates = fill( curveSets, ( this.maxTriangles - mesh ) * 0.95 );
|
|
422
|
+
|
|
423
|
+
const m = v => `${( v / 1e6 ).toFixed( 1 )}M`;
|
|
424
|
+
const kept = ( rates, demands ) => {
|
|
425
|
+
|
|
426
|
+
let all = 0, left = 0;
|
|
427
|
+
for ( const [ key, demand ] of demands ) {
|
|
428
|
+
|
|
429
|
+
all += demand;
|
|
430
|
+
left += demand * ( rates.get( key ) ?? 1 );
|
|
431
|
+
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return `${Math.round( left / all * 100 )} %`;
|
|
435
|
+
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const notes = [];
|
|
439
|
+
if ( this.placementRates.size || this.curveRates.size ) notes.push(
|
|
440
|
+
`USD scene is ${m( mesh + curves )} triangles (${m( curves )} of curves) and ${m( placements )} placed copies, past the budgets ` +
|
|
441
|
+
`(${m( this.maxTriangles )}, ${m( this.maxPlacements )}): ${kept( this.curveRates, curveSets )} of the curves and ` +
|
|
442
|
+
`${kept( this.placementRates, instancers )} of the scattered copies kept, the largest sets thinned most`
|
|
443
|
+
);
|
|
444
|
+
if ( mesh > this.maxTriangles ) notes.push(
|
|
445
|
+
`its meshes alone are ${m( mesh )} triangles, past the ${m( this.maxTriangles )} budget: what is read last is cut. ` +
|
|
446
|
+
'Load fewer parts, or raise maxTriangles (memory spill raises it)'
|
|
447
|
+
);
|
|
448
|
+
this.fitNote = notes.join( '; ' ) || null;
|
|
449
|
+
if ( this.fitNote ) this.warn( this.fitNote );
|
|
450
|
+
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
world() {
|
|
454
|
+
|
|
455
|
+
if ( this.dry ) return { isWorld: true, shape: () => this.cost.placements ++, place: proto => this.countPrototype( proto, 1, false ), instancer: spec => this.countInstancer( spec, 1 ) };
|
|
456
|
+
return {
|
|
457
|
+
isWorld: true,
|
|
458
|
+
shape: ( shape, matrix ) => {
|
|
459
|
+
|
|
460
|
+
shape.ctm = Array.from( matrix.elements );
|
|
461
|
+
this.ir.shapes.push( shape );
|
|
462
|
+
|
|
463
|
+
},
|
|
464
|
+
place: ( proto, matrix ) => this.placePrototype( proto, matrix ),
|
|
465
|
+
instancer: ( spec, matrix ) => this.placeInstancer( spec, matrix ),
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
collector( proto ) {
|
|
471
|
+
|
|
472
|
+
if ( this.dry ) return { isWorld: false, shape: ( _, matrix ) => proto.shapes.push( matrix ? matrix.elements.join( ',' ) : '' ), place: inner => proto.nested.push( { proto: inner } ), instancer: spec => proto.nested.push( { instancer: spec } ) };
|
|
473
|
+
return {
|
|
474
|
+
isWorld: false,
|
|
475
|
+
shape: ( shape, matrix ) => {
|
|
476
|
+
|
|
477
|
+
shape.ctm = Array.from( matrix.elements );
|
|
478
|
+
proto.shapes.push( shape );
|
|
479
|
+
|
|
480
|
+
},
|
|
481
|
+
place: ( inner, matrix ) => proto.nested.push( { proto: inner, matrix: matrix.clone() } ),
|
|
482
|
+
instancer: ( spec, matrix ) => proto.nested.push( { instancer: spec, matrix: matrix.clone() } ),
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async visit( prim, parent, out, ctx ) {
|
|
488
|
+
|
|
489
|
+
if ( ! prim.isDefined || prim.meta( 'active' ) === false ) return;
|
|
490
|
+
if ( prim.value( 'visibility' ) === 'invisible' ) return;
|
|
491
|
+
const purpose = prim.value( 'purpose' );
|
|
492
|
+
if ( purpose === 'guide' || purpose === 'proxy' ) return;
|
|
493
|
+
|
|
494
|
+
const type = prim.typeName;
|
|
495
|
+
if ( SKIPPED.has( type ) ) {
|
|
496
|
+
|
|
497
|
+
if ( type === 'Volume' ) this.warnOnce( 'volume', 'USD volumes are not rendered — skipped' );
|
|
498
|
+
return;
|
|
499
|
+
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const { matrix: local, reset } = localTransform( prim );
|
|
503
|
+
const matrix = reset ? local : new Matrix4().multiplyMatrices( parent, local );
|
|
504
|
+
const binding = this.bindingOf( prim ) ?? ctx.binding;
|
|
505
|
+
|
|
506
|
+
if ( prim.meta( 'instanceable' ) === true && prim.childNames().length ) {
|
|
507
|
+
|
|
508
|
+
const proto = await this.instancePrototype( prim, binding );
|
|
509
|
+
if ( proto ) out.place( proto, matrix );
|
|
510
|
+
return;
|
|
511
|
+
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if ( type === 'Mesh' ) await this.mesh( prim, matrix, out, binding );
|
|
515
|
+
else if ( type === 'BasisCurves' ) await this.curves( prim, matrix, out, binding );
|
|
516
|
+
else if ( GPRIMS.has( type ) ) await this.gprim( prim, matrix, out, binding );
|
|
517
|
+
else if ( type === 'PointInstancer' ) {
|
|
518
|
+
|
|
519
|
+
await this.pointInstancer( prim, matrix, out, binding );
|
|
520
|
+
return;
|
|
521
|
+
|
|
522
|
+
} else if ( type === 'Camera' ) {
|
|
523
|
+
|
|
524
|
+
if ( out.isWorld && ! this.dry ) this.camera( prim, matrix );
|
|
525
|
+
|
|
526
|
+
} else if ( LIGHT_TYPES.has( type ) ) {
|
|
527
|
+
|
|
528
|
+
if ( this.dry ) return;
|
|
529
|
+
if ( out.isWorld ) this.light( prim, type, matrix );
|
|
530
|
+
else this.warnOnce( 'protoLight', 'USD lights inside instanced prims are not placed — skipped' );
|
|
531
|
+
|
|
532
|
+
} else if ( type === 'NurbsPatch' || type === 'NurbsCurves' || type === 'Points' || type === 'HermiteCurves' ) {
|
|
533
|
+
|
|
534
|
+
this.warnOnce( `type:${type}`, `USD ${type} prims are not supported — skipped` );
|
|
535
|
+
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
for ( const name of prim.childNames() ) {
|
|
539
|
+
|
|
540
|
+
const child = await this.stage.child( prim, name );
|
|
541
|
+
if ( child ) await this.visit( child, matrix, out, { binding, depth: ctx.depth + 1 } );
|
|
542
|
+
// A top prim's children are usually whole assets (Moana's elements): what one read is let go before the next.
|
|
543
|
+
if ( ctx.depth === 0 && out.isWorld ) this.stage.release();
|
|
544
|
+
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
bindingOf( prim ) {
|
|
550
|
+
|
|
551
|
+
for ( const name of [ 'material:binding:full', 'material:binding' ] ) {
|
|
552
|
+
|
|
553
|
+
const [ target ] = prim.targets( name );
|
|
554
|
+
if ( target ) return target;
|
|
555
|
+
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return null;
|
|
559
|
+
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ── instancing ───────────────────────────────────────────────
|
|
563
|
+
|
|
564
|
+
newPrototype() {
|
|
565
|
+
|
|
566
|
+
return { name: `usd_proto_${this._protoCount ++}`, shapes: [], nested: [], registered: false };
|
|
567
|
+
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
register( proto ) {
|
|
571
|
+
|
|
572
|
+
if ( proto.shapes.length && ! proto.registered && ! this.dry ) {
|
|
573
|
+
|
|
574
|
+
this.ir.objects.set( proto.name, proto.shapes );
|
|
575
|
+
proto.registered = true;
|
|
576
|
+
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return proto.shapes.length || proto.nested.length ? proto : null;
|
|
580
|
+
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async instancePrototype( prim, binding ) {
|
|
584
|
+
|
|
585
|
+
const key = `instance|${prim.instanceKey()}|${binding ?? ''}`;
|
|
586
|
+
if ( this.prototypes.has( key ) ) return this.prototypes.get( key );
|
|
587
|
+
const proto = this.newPrototype();
|
|
588
|
+
this.prototypes.set( key, proto );
|
|
589
|
+
const out = this.collector( proto );
|
|
590
|
+
for ( const name of prim.childNames() ) {
|
|
591
|
+
|
|
592
|
+
const child = await this.stage.child( prim, name );
|
|
593
|
+
if ( child ) await this.visit( child, new Matrix4(), out, { binding, depth: Infinity } );
|
|
594
|
+
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const result = this.register( proto );
|
|
598
|
+
this.prototypes.set( key, result );
|
|
599
|
+
return result;
|
|
600
|
+
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** A point instancer prototype: its root prim and everything under it, the root's own transform included. */
|
|
604
|
+
async instancerPrototype( prim, binding ) {
|
|
605
|
+
|
|
606
|
+
const key = `proto|${prim.path}`;
|
|
607
|
+
if ( this.prototypes.has( key ) ) return this.prototypes.get( key );
|
|
608
|
+
const proto = this.newPrototype();
|
|
609
|
+
this.prototypes.set( key, proto );
|
|
610
|
+
await this.visit( prim, new Matrix4(), this.collector( proto ), { binding, depth: Infinity } );
|
|
611
|
+
const result = this.register( proto );
|
|
612
|
+
this.prototypes.set( key, result );
|
|
613
|
+
return result;
|
|
614
|
+
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
addInstance( name, matrix ) {
|
|
618
|
+
|
|
619
|
+
const ir = this.ir;
|
|
620
|
+
if ( ir.instanceCount >= this.maxPlacements ) {
|
|
621
|
+
|
|
622
|
+
ir.skippedInstances ++;
|
|
623
|
+
return;
|
|
624
|
+
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
let list = ir.instances.get( name );
|
|
628
|
+
if ( ! list ) ir.instances.set( name, list = { name, count: 0, matrices: new Float32Array( 16 * 32 ), matricesEnd: null } );
|
|
629
|
+
const need = ( list.count + 1 ) * 16;
|
|
630
|
+
if ( need > list.matrices.length ) {
|
|
631
|
+
|
|
632
|
+
const grown = new Float32Array( Math.max( need, list.matrices.length * 2 ) );
|
|
633
|
+
grown.set( list.matrices );
|
|
634
|
+
list.matrices = grown;
|
|
635
|
+
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
list.matrices.set( matrix.elements, list.count * 16 );
|
|
639
|
+
list.count ++;
|
|
640
|
+
ir.instanceCount ++;
|
|
641
|
+
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// The builder places a template's shapes at one relative transform as one object, so a copy costs one placement
|
|
645
|
+
// per distinct transform among its shapes.
|
|
646
|
+
countPrototype( proto, n, scatter ) {
|
|
647
|
+
|
|
648
|
+
if ( proto.shapes.length ) {
|
|
649
|
+
|
|
650
|
+
proto.placeKeys ??= new Set( proto.shapes ).size;
|
|
651
|
+
this.cost.placements += n * proto.placeKeys;
|
|
652
|
+
if ( scatter ) this.cost.scatter += n * proto.placeKeys;
|
|
653
|
+
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
for ( const nested of proto.nested ) {
|
|
657
|
+
|
|
658
|
+
if ( nested.proto ) this.countPrototype( nested.proto, n, scatter );
|
|
659
|
+
else this.countInstancer( nested.instancer, n );
|
|
660
|
+
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
countInstancer( spec, n ) {
|
|
666
|
+
|
|
667
|
+
const before = this.cost.scatter;
|
|
668
|
+
spec.histogram.forEach( ( count, p ) => {
|
|
669
|
+
|
|
670
|
+
if ( spec.protos[ p ] && count ) this.countPrototype( spec.protos[ p ], n * count, true );
|
|
671
|
+
|
|
672
|
+
} );
|
|
673
|
+
this.cost.instancers.set( spec.path, ( this.cost.instancers.get( spec.path ) ?? 0 ) + this.cost.scatter - before );
|
|
674
|
+
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
placePrototype( proto, matrix ) {
|
|
678
|
+
|
|
679
|
+
if ( proto.shapes.length ) this.addInstance( proto.name, matrix );
|
|
680
|
+
for ( const nested of proto.nested ) {
|
|
681
|
+
|
|
682
|
+
const m = new Matrix4().multiplyMatrices( matrix, nested.matrix );
|
|
683
|
+
if ( nested.proto ) this.placePrototype( nested.proto, m );
|
|
684
|
+
else this.placeInstancer( nested.instancer, m );
|
|
685
|
+
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
placeInstancer( spec, world ) {
|
|
691
|
+
|
|
692
|
+
const { protos, protoIndices, positions, orientations, scales, hidden, salt } = spec;
|
|
693
|
+
const rate = spec.rate;
|
|
694
|
+
let quota = rate < 1 ? Math.floor( protoIndices.length * rate ) : Infinity;
|
|
695
|
+
const instance = new Matrix4(), m = new Matrix4();
|
|
696
|
+
const q = new Quaternion(), p = new Vector3(), s = new Vector3();
|
|
697
|
+
for ( let i = 0; i < protoIndices.length; i ++ ) {
|
|
698
|
+
|
|
699
|
+
if ( this.ir.instanceCount >= this.maxPlacements ) {
|
|
700
|
+
|
|
701
|
+
this.ir.skippedInstances += protoIndices.length - i;
|
|
702
|
+
return;
|
|
703
|
+
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const proto = protos[ protoIndices[ i ] ];
|
|
707
|
+
if ( ! proto || hidden?.has( i ) || ( rate < 1 && ( quota <= 0 || hash01( i, salt ) >= rate ) ) ) continue;
|
|
708
|
+
quota --;
|
|
709
|
+
p.set( positions[ i * 3 ], positions[ i * 3 + 1 ], positions[ i * 3 + 2 ] );
|
|
710
|
+
if ( orientations ) q.set( orientations[ i * 4 ], orientations[ i * 4 + 1 ], orientations[ i * 4 + 2 ], orientations[ i * 4 + 3 ] ).normalize();
|
|
711
|
+
else q.identity();
|
|
712
|
+
if ( scales ) s.set( scales[ i * 3 ], scales[ i * 3 + 1 ], scales[ i * 3 + 2 ] );
|
|
713
|
+
else s.set( 1, 1, 1 );
|
|
714
|
+
instance.compose( p, q, s );
|
|
715
|
+
this.placePrototype( proto, m.multiplyMatrices( world, instance ) );
|
|
716
|
+
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async pointInstancer( prim, matrix, out, binding ) {
|
|
722
|
+
|
|
723
|
+
const protoIndices = prim.value( 'protoIndices' );
|
|
724
|
+
if ( ! protoIndices?.length ) return;
|
|
725
|
+
const positions = this.dry ? null : prim.value( 'positions' );
|
|
726
|
+
if ( ! this.dry && ! positions?.length ) return;
|
|
727
|
+
const protos = [];
|
|
728
|
+
for ( const path of prim.targets( 'prototypes' ) ) {
|
|
729
|
+
|
|
730
|
+
const target = await this.stage.primAt( path );
|
|
731
|
+
protos.push( target ? await this.instancerPrototype( target, binding ) : null );
|
|
732
|
+
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const invisible = prim.value( 'invisibleIds' );
|
|
736
|
+
let hidden = null;
|
|
737
|
+
if ( invisible?.length ) {
|
|
738
|
+
|
|
739
|
+
const ids = prim.value( 'ids' );
|
|
740
|
+
const byId = new Map();
|
|
741
|
+
if ( ids?.length ) for ( let i = 0; i < ids.length; i ++ ) byId.set( Number( ids[ i ] ), i );
|
|
742
|
+
hidden = new Set( Array.from( invisible, id => ( ids?.length ? byId.get( Number( id ) ) : Number( id ) ) ) );
|
|
743
|
+
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if ( this.dry ) {
|
|
747
|
+
|
|
748
|
+
const histogram = new Float64Array( protos.length );
|
|
749
|
+
for ( let i = 0; i < protoIndices.length; i ++ ) if ( ! hidden?.has( i ) ) histogram[ protoIndices[ i ] ] ++;
|
|
750
|
+
out.instancer( { protos, histogram, path: prim.path } );
|
|
751
|
+
return;
|
|
752
|
+
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const orientations = prim.value( 'orientations' );
|
|
756
|
+
const scales = prim.value( 'scales' );
|
|
757
|
+
out.instancer( {
|
|
758
|
+
protos, protoIndices, positions, salt: saltOf( prim.path ), rate: this.placementRates.get( prim.path ) ?? 1,
|
|
759
|
+
orientations: orientations?.length === protoIndices.length * 4 ? orientations : null,
|
|
760
|
+
scales: scales?.length === protoIndices.length * 3 ? scales : null,
|
|
761
|
+
hidden,
|
|
762
|
+
}, matrix );
|
|
763
|
+
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// ── geometry ─────────────────────────────────────────────────
|
|
767
|
+
|
|
768
|
+
/** An array handed to the builder is its alone: it frees each one once the shape is merged. */
|
|
769
|
+
own( array ) {
|
|
770
|
+
|
|
771
|
+
if ( ! array ) return array;
|
|
772
|
+
if ( this._handed.has( array ) ) return array.slice();
|
|
773
|
+
this._handed.add( array );
|
|
774
|
+
return array;
|
|
775
|
+
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
shape( prim, type, params, material ) {
|
|
779
|
+
|
|
780
|
+
return { type, params, ctm: null, material, areaLight: null, reverseOrientation: false, interior: null, name: prim.path };
|
|
781
|
+
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
specId( prop ) {
|
|
785
|
+
|
|
786
|
+
if ( ! prop ) return 0;
|
|
787
|
+
let id = this._specIds.get( prop );
|
|
788
|
+
if ( ! id ) this._specIds.set( prop, id = ++ this._nextSpecId );
|
|
789
|
+
return id;
|
|
790
|
+
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/** The same specs give the same triangles: a mesh read again under another prim shares its geometry. */
|
|
794
|
+
geometryKey( prim, uvSet ) {
|
|
795
|
+
|
|
796
|
+
const id = name => this.specId( prim.property( name ) );
|
|
797
|
+
const uv = uvSet ?? 'st';
|
|
798
|
+
return [ id( 'points' ), id( 'faceVertexIndices' ), id( 'faceVertexCounts' ), id( 'holeIndices' ), id( 'primvars:normals' ),
|
|
799
|
+
id( 'normals' ), uv, id( `primvars:${uv}` ), id( `primvars:${uv}:indices` ), prim.value( 'orientation' ) ?? '' ].join( '|' );
|
|
800
|
+
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
countGeometry( key, kind, triangles ) {
|
|
804
|
+
|
|
805
|
+
if ( this.cost.counted.has( key ) ) return;
|
|
806
|
+
this.cost.counted.add( key );
|
|
807
|
+
this.cost[ kind ] += triangles;
|
|
808
|
+
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
meshParams( prim, faces, uvSet ) {
|
|
812
|
+
|
|
813
|
+
const points = prim.value( 'points' );
|
|
814
|
+
const counts = prim.value( 'faceVertexCounts' );
|
|
815
|
+
const fvi = prim.value( 'faceVertexIndices' );
|
|
816
|
+
if ( ! points?.length || ! counts?.length || ! fvi?.length ) return null;
|
|
817
|
+
|
|
818
|
+
const offsets = new Int32Array( counts.length );
|
|
819
|
+
for ( let f = 1; f < counts.length; f ++ ) offsets[ f ] = offsets[ f - 1 ] + counts[ f - 1 ];
|
|
820
|
+
if ( ! faces ) {
|
|
821
|
+
|
|
822
|
+
const holes = new Set( prim.value( 'holeIndices' ) ?? [] );
|
|
823
|
+
faces = [];
|
|
824
|
+
for ( let f = 0; f < counts.length; f ++ ) if ( ! holes.has( f ) ) faces.push( f );
|
|
825
|
+
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const normals = primvar( prim, 'normals' ) ?? ( prim.value( 'normals' )?.length ? { values: prim.value( 'normals' ), interpolation: prim.propertyMeta( 'normals', 'interpolation' ) ?? 'vertex', indices: null } : null );
|
|
829
|
+
const uv = primvar( prim, uvSet ) ?? ( uvSet === 'st' ? primvar( prim, 'uv' ) ?? primvar( prim, 'st0' ) ?? primvar( prim, 'UVMap' ) : null );
|
|
830
|
+
const built = triangulate( { points, counts, offsets, fvi, leftHanded: prim.value( 'orientation' ) === 'leftHanded', normals, uv }, faces );
|
|
831
|
+
if ( built.index.length === 0 ) return null;
|
|
832
|
+
const params = { P: param( this.own( built.P ), 'point3' ), indices: param( built.index, 'integer' ) };
|
|
833
|
+
if ( built.N ) params.N = param( this.own( built.N ), 'normal' );
|
|
834
|
+
if ( built.uv ) params.uv = param( this.own( built.uv ), 'point2' );
|
|
835
|
+
return params;
|
|
836
|
+
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
async mesh( prim, matrix, out, binding ) {
|
|
840
|
+
|
|
841
|
+
const subsets = [];
|
|
842
|
+
for ( const name of prim.childNames() ) {
|
|
843
|
+
|
|
844
|
+
const subset = await this.stage.child( prim, name );
|
|
845
|
+
if ( subset?.typeName !== 'GeomSubset' || ( subset.value( 'elementType' ) ?? 'face' ) !== 'face' ) continue;
|
|
846
|
+
const family = subset.value( 'familyName' );
|
|
847
|
+
const target = this.bindingOf( subset );
|
|
848
|
+
if ( target && ( ! family || family === 'materialBind' ) ) subsets.push( { subset, target } );
|
|
849
|
+
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if ( this.dry ) {
|
|
853
|
+
|
|
854
|
+
const counts = prim.value( 'faceVertexCounts' );
|
|
855
|
+
if ( ! prim.property( 'points' ) || ! counts?.length ) return;
|
|
856
|
+
const holes = new Set( prim.value( 'holeIndices' ) ?? [] );
|
|
857
|
+
let triangles = 0;
|
|
858
|
+
for ( let f = 0; f < counts.length; f ++ ) if ( counts[ f ] > 2 && ! holes.has( f ) ) triangles += counts[ f ] - 2;
|
|
859
|
+
this.countGeometry( this.geometryKey( prim, null ), 'mesh', triangles );
|
|
860
|
+
out.shape( null, matrix );
|
|
861
|
+
return;
|
|
862
|
+
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if ( subsets.length === 0 ) {
|
|
866
|
+
|
|
867
|
+
const { material, uvSet } = await this.materialFor( binding, prim );
|
|
868
|
+
const key = this.geometryKey( prim, uvSet );
|
|
869
|
+
const known = this._geometries.get( key );
|
|
870
|
+
if ( known ) {
|
|
871
|
+
|
|
872
|
+
const shape = this.shape( prim, 'trianglemesh', known.params, material );
|
|
873
|
+
known.shapes.push( shape );
|
|
874
|
+
out.shape( shape, matrix );
|
|
875
|
+
return;
|
|
876
|
+
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const params = this.meshParams( prim, null, uvSet );
|
|
880
|
+
if ( ! params ) return;
|
|
881
|
+
const shape = this.shape( prim, 'trianglemesh', params, material );
|
|
882
|
+
this._geometries.set( key, { params, shapes: [ shape ] } );
|
|
883
|
+
out.shape( shape, matrix );
|
|
884
|
+
return;
|
|
885
|
+
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const counts = prim.value( 'faceVertexCounts' );
|
|
889
|
+
if ( ! counts?.length ) return;
|
|
890
|
+
const holes = new Set( prim.value( 'holeIndices' ) ?? [] );
|
|
891
|
+
const groups = [];
|
|
892
|
+
const claimed = new Set();
|
|
893
|
+
for ( const { subset, target } of subsets ) {
|
|
894
|
+
|
|
895
|
+
const faces = Array.from( subset.value( 'indices' ) ?? [] ).filter( f => f < counts.length && ! holes.has( f ) && ! claimed.has( f ) );
|
|
896
|
+
for ( const f of faces ) claimed.add( f );
|
|
897
|
+
if ( faces.length ) groups.push( { faces, binding: target } );
|
|
898
|
+
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const rest = [];
|
|
902
|
+
for ( let f = 0; f < counts.length; f ++ ) if ( ! holes.has( f ) && ! claimed.has( f ) ) rest.push( f );
|
|
903
|
+
if ( rest.length ) groups.unshift( { faces: rest, binding } );
|
|
904
|
+
|
|
905
|
+
for ( const group of groups ) {
|
|
906
|
+
|
|
907
|
+
const { material, uvSet } = await this.materialFor( group.binding, prim );
|
|
908
|
+
const params = this.meshParams( prim, group.faces, uvSet );
|
|
909
|
+
if ( params ) out.shape( this.shape( prim, 'trianglemesh', params, material ), matrix );
|
|
910
|
+
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
async curves( prim, matrix, out, binding ) {
|
|
916
|
+
|
|
917
|
+
const counts = prim.value( 'curveVertexCounts' );
|
|
918
|
+
if ( ! counts?.length || ! prim.property( 'points' ) ) return;
|
|
919
|
+
const type = prim.value( 'type' ) ?? 'cubic';
|
|
920
|
+
const basis = prim.value( 'basis' ) ?? 'bezier';
|
|
921
|
+
const wrap = prim.value( 'wrap' ) ?? 'nonperiodic';
|
|
922
|
+
|
|
923
|
+
if ( this.dry ) {
|
|
924
|
+
|
|
925
|
+
let segments = 0;
|
|
926
|
+
for ( let c = 0; c < counts.length; c ++ ) segments += Math.max( 1, Math.round( curveSpans( counts[ c ], type, basis, wrap ) * this.curveSteps ) * ADAPTIVE_SHARE );
|
|
927
|
+
const key = `curves|${this.specId( prim.property( 'points' ) )}|${this.specId( prim.property( 'curveVertexCounts' ) )}`;
|
|
928
|
+
if ( ! this.cost.counted.has( key ) ) this.cost.curveSets.set( prim.path, segments * 2 );
|
|
929
|
+
this.countGeometry( key, 'curves', segments * 2 );
|
|
930
|
+
out.shape( null, matrix );
|
|
931
|
+
return;
|
|
932
|
+
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const widthName = prim.property( 'widths' ) ? 'widths' : 'primvars:widths';
|
|
936
|
+
const normalName = prim.property( 'normals' ) ? 'normals' : 'primvars:normals';
|
|
937
|
+
let curve = {
|
|
938
|
+
points: prim.value( 'points' ), counts, type, basis, wrap,
|
|
939
|
+
widths: prim.value( widthName ) ?? null,
|
|
940
|
+
widthMode: prim.propertyMeta( widthName, 'interpolation' ) ?? null,
|
|
941
|
+
normals: prim.value( normalName ) ?? null,
|
|
942
|
+
normalMode: prim.propertyMeta( normalName, 'interpolation' ) ?? 'vertex',
|
|
943
|
+
};
|
|
944
|
+
if ( curve.widths?.length ) curve.widthMode ??= curve.widths.length === 1 ? 'constant' : 'vertex';
|
|
945
|
+
const rate = this.curveRates.get( prim.path ) ?? 1;
|
|
946
|
+
if ( rate < 1 ) curve = thinCurves( curve, rate, saltOf( prim.path ) );
|
|
947
|
+
if ( ! curve ) return;
|
|
948
|
+
|
|
949
|
+
const params = {
|
|
950
|
+
P: param( this.own( curve.points ), 'point3' ),
|
|
951
|
+
counts: param( this.own( curve.counts ), 'integer' ),
|
|
952
|
+
type: word( type ), basis: word( basis ), wrap: word( wrap ),
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
if ( curve.widths?.length ) {
|
|
956
|
+
|
|
957
|
+
params.widths = param( this.own( curve.widths ) );
|
|
958
|
+
params.widthMode = word( curve.widthMode );
|
|
959
|
+
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if ( curve.normals?.length ) {
|
|
963
|
+
|
|
964
|
+
params.N = param( this.own( curve.normals ), 'normal' );
|
|
965
|
+
params.normalMode = word( curve.normalMode );
|
|
966
|
+
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const { material } = await this.materialFor( binding, prim );
|
|
970
|
+
out.shape( this.shape( prim, 'curves', params, material ), matrix );
|
|
971
|
+
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/** Shapes that read one mesh share its geometry; the builder keeps such a shape whole rather than merging it. */
|
|
975
|
+
shareGeometry() {
|
|
976
|
+
|
|
977
|
+
for ( const [ key, { shapes } ] of this._geometries ) {
|
|
978
|
+
|
|
979
|
+
if ( shapes.length < 2 ) continue;
|
|
980
|
+
for ( const shape of shapes ) {
|
|
981
|
+
|
|
982
|
+
shape.shared = true;
|
|
983
|
+
shape.geometryKey = key;
|
|
984
|
+
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
this._geometries.clear();
|
|
990
|
+
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
async gprim( prim, matrix, out, binding ) {
|
|
994
|
+
|
|
995
|
+
const axis = prim.value( 'axis' ) ?? 'Z';
|
|
996
|
+
const radius = prim.value( 'radius' ) ?? 1;
|
|
997
|
+
const height = prim.value( 'height' ) ?? 2;
|
|
998
|
+
let geometry;
|
|
999
|
+
switch ( prim.typeName ) {
|
|
1000
|
+
|
|
1001
|
+
case 'Sphere': geometry = new SphereGeometry( radius, 48, 24 ); break;
|
|
1002
|
+
case 'Cube': {
|
|
1003
|
+
|
|
1004
|
+
const size = prim.value( 'size' ) ?? 2;
|
|
1005
|
+
geometry = new BoxGeometry( size, size, size );
|
|
1006
|
+
break;
|
|
1007
|
+
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
case 'Cylinder': geometry = new CylinderGeometry( radius, radius, height, 48 ); break;
|
|
1011
|
+
case 'Cone': geometry = new ConeGeometry( radius, height, 48 ); break;
|
|
1012
|
+
case 'Capsule': geometry = new CapsuleGeometry( radius, height, 8, 32 ); break;
|
|
1013
|
+
default: geometry = new PlaneGeometry( prim.value( 'width' ) ?? 2, prim.value( 'length' ) ?? 2 );
|
|
1014
|
+
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const arrays = geometryArrays( geometry, prim.typeName === 'Plane' ? ( axis === 'Z' ? 'none' : axis ) : axis );
|
|
1018
|
+
geometry.dispose();
|
|
1019
|
+
if ( this.dry ) {
|
|
1020
|
+
|
|
1021
|
+
this.countGeometry( `gprim|${prim.path}`, 'mesh', arrays.index.length / 3 );
|
|
1022
|
+
out.shape( null, matrix );
|
|
1023
|
+
return;
|
|
1024
|
+
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const params = { P: param( arrays.P, 'point3' ), indices: param( arrays.index, 'integer' ) };
|
|
1028
|
+
if ( arrays.N ) params.N = param( arrays.N, 'normal' );
|
|
1029
|
+
if ( arrays.uv ) params.uv = param( arrays.uv, 'point2' );
|
|
1030
|
+
const { material } = await this.materialFor( binding, prim );
|
|
1031
|
+
out.shape( this.shape( prim, 'trianglemesh', params, material ), matrix );
|
|
1032
|
+
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// ── materials ────────────────────────────────────────────────
|
|
1036
|
+
|
|
1037
|
+
async materialFor( path, prim ) {
|
|
1038
|
+
|
|
1039
|
+
const surface = path ? await this.surface( path ) : null;
|
|
1040
|
+
let material = surface?.material ?? null;
|
|
1041
|
+
const display = primvar( prim, 'displayColor' );
|
|
1042
|
+
|
|
1043
|
+
if ( ! material ) {
|
|
1044
|
+
|
|
1045
|
+
const rgb = meanColor( display ) ?? [ 0.18, 0.18, 0.18 ];
|
|
1046
|
+
material = this.colored( null, rgb );
|
|
1047
|
+
|
|
1048
|
+
} else if ( surface.perMesh ) {
|
|
1049
|
+
|
|
1050
|
+
const mean = meanColor( primvar( prim, surface.perMesh.primvar ) );
|
|
1051
|
+
if ( mean ) material = this.colored( material, mean.map( ( v, c ) => Math.max( v, 0 ) ** ( surface.perMesh.exponent?.[ c ] ?? 1 ) ) );
|
|
1052
|
+
else this.warnOnce( 'nodisplay', `USD material "${path}" takes its colour from "${surface.perMesh.primvar}", which a mesh lacks — its fallback used` );
|
|
1053
|
+
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
let wrapper = this._wrappers.get( material );
|
|
1057
|
+
if ( ! wrapper ) this._wrappers.set( material, wrapper = { type: 'usd', material } );
|
|
1058
|
+
return { material: wrapper, uvSet: surface?.uvSet ?? 'st' };
|
|
1059
|
+
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/** One material per colour, on an 8-bit grid so near-identical meshes share it. */
|
|
1063
|
+
colored( base, rgb ) {
|
|
1064
|
+
|
|
1065
|
+
const color = quantize( rgb );
|
|
1066
|
+
const key = `${base?.uuid ?? 'plain'}|${color.join( ',' )}`;
|
|
1067
|
+
let material = this._colored.get( key );
|
|
1068
|
+
if ( ! material ) {
|
|
1069
|
+
|
|
1070
|
+
material = base ? cloneMaterial( base ) : new MeshPhysicalMaterial( { side: DoubleSide, roughness: 0.5, metalness: 0 } );
|
|
1071
|
+
material.color.setRGB( color[ 0 ], color[ 1 ], color[ 2 ] );
|
|
1072
|
+
if ( base?.userData.diffuseTransmissionFromBase ) material.diffuseTransmissionColor = material.color.clone();
|
|
1073
|
+
this._colored.set( key, material );
|
|
1074
|
+
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
return material;
|
|
1078
|
+
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
async surface( path ) {
|
|
1082
|
+
|
|
1083
|
+
if ( this._surfaces.has( path ) ) return this._surfaces.get( path );
|
|
1084
|
+
const pending = this.buildSurface( path );
|
|
1085
|
+
this._surfaces.set( path, pending );
|
|
1086
|
+
return pending;
|
|
1087
|
+
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
async buildSurface( path ) {
|
|
1091
|
+
|
|
1092
|
+
const prim = await this.stage.primAt( path );
|
|
1093
|
+
if ( ! prim ) {
|
|
1094
|
+
|
|
1095
|
+
this.warnOnce( `nomat:${path}`, `USD material "${path}" not found` );
|
|
1096
|
+
return null;
|
|
1097
|
+
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
for ( const output of [ 'outputs:ri:surface', 'outputs:surface', 'outputs:glslfx:surface' ] ) {
|
|
1101
|
+
|
|
1102
|
+
const source = await this.stage.connectionSource( prim, output );
|
|
1103
|
+
const id = source?.prim.value( 'info:id' );
|
|
1104
|
+
if ( id === 'PxrDisneyBsdf' ) return this.disney( source.prim );
|
|
1105
|
+
if ( id === 'PxrSurface' ) return this.pxrSurface( source.prim );
|
|
1106
|
+
if ( id === 'UsdPreviewSurface' ) return this.previewSurface( source.prim );
|
|
1107
|
+
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
this.warnOnce( `nosurface:${path}`, `USD material "${path}" has no surface shader read here — shaded grey` );
|
|
1111
|
+
return null;
|
|
1112
|
+
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
async term( prim, name, hops = 0 ) {
|
|
1116
|
+
|
|
1117
|
+
const attr = `inputs:${name}`;
|
|
1118
|
+
const [ source ] = prim.connections( attr );
|
|
1119
|
+
if ( source && hops < 32 ) {
|
|
1120
|
+
|
|
1121
|
+
const dot = source.lastIndexOf( '.' );
|
|
1122
|
+
const from = await this.stage.primAt( source.slice( 0, dot ) );
|
|
1123
|
+
const prop = source.slice( dot + 1 );
|
|
1124
|
+
if ( from ) {
|
|
1125
|
+
|
|
1126
|
+
const found = prop.startsWith( 'inputs:' ) ? await this.term( from, prop.slice( 7 ), hops + 1 )
|
|
1127
|
+
: prop.startsWith( 'outputs:' ) ? await this.output( from, prop.slice( 8 ), hops + 1 ) : null;
|
|
1128
|
+
if ( found ) return found;
|
|
1129
|
+
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const value = prim.value( attr );
|
|
1135
|
+
return value === undefined ? null : { value };
|
|
1136
|
+
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
async output( prim, name, hops ) {
|
|
1140
|
+
|
|
1141
|
+
const id = prim.value( 'info:id' );
|
|
1142
|
+
switch ( id ) {
|
|
1143
|
+
|
|
1144
|
+
case 'PxrColorCorrect': {
|
|
1145
|
+
|
|
1146
|
+
const input = await this.term( prim, 'inputRGB', hops + 1 );
|
|
1147
|
+
const gamma = ( await this.term( prim, 'gamma', hops + 1 ) )?.value;
|
|
1148
|
+
if ( ! input || ! Array.isArray( gamma ) ) return input;
|
|
1149
|
+
const exponent = gamma.map( g => ( g > 0 ? 1 / g : 1 ) );
|
|
1150
|
+
if ( Array.isArray( input.value ) ) return { value: input.value.map( ( v, c ) => Math.max( v, 0 ) ** exponent[ c ] ) };
|
|
1151
|
+
return { ...input, exponent };
|
|
1152
|
+
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
case 'PxrBlend':
|
|
1156
|
+
return await this.term( prim, 'bottomRGB', hops + 1 ) ?? this.term( prim, 'topRGB', hops + 1 );
|
|
1157
|
+
case 'PxrPtexture':
|
|
1158
|
+
case 'HwPtexTexture_1':
|
|
1159
|
+
// Ptex is not read here; scenes that ship it bake its colours into displayColor (Moana does).
|
|
1160
|
+
return { primvar: 'displayColor' };
|
|
1161
|
+
case 'UsdUVTexture': return this.texture( prim, name, hops );
|
|
1162
|
+
case 'UsdPrimvarReader_float':
|
|
1163
|
+
case 'UsdPrimvarReader_float2':
|
|
1164
|
+
case 'UsdPrimvarReader_float3':
|
|
1165
|
+
case 'UsdPrimvarReader_float4':
|
|
1166
|
+
case 'UsdPrimvarReader_normal':
|
|
1167
|
+
case 'UsdPrimvarReader_point':
|
|
1168
|
+
case 'UsdPrimvarReader_vector': {
|
|
1169
|
+
|
|
1170
|
+
const varname = ( await this.term( prim, 'varname', hops + 1 ) )?.value;
|
|
1171
|
+
return varname ? { primvar: varname } : await this.term( prim, 'fallback', hops + 1 );
|
|
1172
|
+
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
case 'UsdTransform2d': {
|
|
1176
|
+
|
|
1177
|
+
const input = await this.term( prim, 'in', hops + 1 );
|
|
1178
|
+
if ( ! input?.texture ) return input;
|
|
1179
|
+
return { ...input, transform: { scale: prim.value( 'inputs:scale' ), translation: prim.value( 'inputs:translation' ), rotation: prim.value( 'inputs:rotation' ) } };
|
|
1180
|
+
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
case undefined: {
|
|
1184
|
+
|
|
1185
|
+
// A node graph's output, wired to whatever inside produces it.
|
|
1186
|
+
const [ next ] = prim.connections( `outputs:${name}` );
|
|
1187
|
+
if ( ! next ) return null;
|
|
1188
|
+
const dot = next.lastIndexOf( '.' );
|
|
1189
|
+
const from = await this.stage.primAt( next.slice( 0, dot ) );
|
|
1190
|
+
const prop = next.slice( dot + 1 );
|
|
1191
|
+
if ( ! from ) return null;
|
|
1192
|
+
return prop.startsWith( 'outputs:' ) ? this.output( from, prop.slice( 8 ), hops + 1 ) : this.term( from, prop.slice( 7 ), hops + 1 );
|
|
1193
|
+
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
default:
|
|
1197
|
+
this.warnOnce( `shader:${id}`, `USD shader "${id}" is not read — its input left at its default` );
|
|
1198
|
+
return null;
|
|
1199
|
+
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
async texture( prim, channel, hops ) {
|
|
1205
|
+
|
|
1206
|
+
const file = prim.assetPath( 'inputs:file' );
|
|
1207
|
+
const fallback = prim.value( 'inputs:fallback' );
|
|
1208
|
+
if ( ! file?.path ) {
|
|
1209
|
+
|
|
1210
|
+
if ( file?.asset ) this.warnOnce( `tex:${file.asset}`, `USD texture "${file.asset}" not found` );
|
|
1211
|
+
return fallback ? { value: fallback } : null;
|
|
1212
|
+
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if ( PTEX.test( file.path ) ) return { primvar: 'displayColor' };
|
|
1216
|
+
if ( /<udim>/i.test( file.asset ) ) {
|
|
1217
|
+
|
|
1218
|
+
this.warnOnce( 'udim', 'USD UDIM textures are not supported — skipped' );
|
|
1219
|
+
return null;
|
|
1220
|
+
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const st = await this.term( prim, 'st', hops + 1 );
|
|
1224
|
+
return {
|
|
1225
|
+
texture: {
|
|
1226
|
+
path: file.path,
|
|
1227
|
+
uvSet: st?.primvar ?? 'st',
|
|
1228
|
+
wrapS: prim.value( 'inputs:wrapS' ) ?? 'repeat',
|
|
1229
|
+
wrapT: prim.value( 'inputs:wrapT' ) ?? 'repeat',
|
|
1230
|
+
colorSpace: prim.value( 'inputs:sourceColorSpace' ) ?? 'auto',
|
|
1231
|
+
scale: prim.value( 'inputs:scale' ) ?? null,
|
|
1232
|
+
},
|
|
1233
|
+
channel,
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
async image( spec, srgb ) {
|
|
1239
|
+
|
|
1240
|
+
const key = `${spec.path}|${srgb}|${spec.wrapS}|${spec.wrapT}`;
|
|
1241
|
+
if ( ! this._images.has( key ) ) this._images.set( key, ( async () => {
|
|
1242
|
+
|
|
1243
|
+
const texture = await this.resolveImage( spec.path );
|
|
1244
|
+
if ( ! texture ) {
|
|
1245
|
+
|
|
1246
|
+
this.warnOnce( `img:${spec.path}`, `USD texture "${spec.path}" could not be read` );
|
|
1247
|
+
return null;
|
|
1248
|
+
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const out = texture.clone();
|
|
1252
|
+
out.userData.usdPath = spec.path;
|
|
1253
|
+
out.colorSpace = srgb ? SRGBColorSpace : NoColorSpace;
|
|
1254
|
+
out.wrapS = WRAP[ spec.wrapS ] ?? RepeatWrapping;
|
|
1255
|
+
out.wrapT = WRAP[ spec.wrapT ] ?? RepeatWrapping;
|
|
1256
|
+
out.needsUpdate = true;
|
|
1257
|
+
return out;
|
|
1258
|
+
|
|
1259
|
+
} )() );
|
|
1260
|
+
return this._images.get( key );
|
|
1261
|
+
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
async applyColor( mat, slot, term, surface, mapSlot ) {
|
|
1265
|
+
|
|
1266
|
+
if ( ! term ) return;
|
|
1267
|
+
if ( Array.isArray( term.value ) ) mat[ slot ].setRGB( term.value[ 0 ], term.value[ 1 ], term.value[ 2 ] );
|
|
1268
|
+
else if ( typeof term.value === 'number' ) mat[ slot ].setRGB( term.value, term.value, term.value );
|
|
1269
|
+
else if ( term.primvar && slot === 'color' ) surface.perMesh = { primvar: term.primvar, exponent: term.exponent ?? null };
|
|
1270
|
+
else if ( term.texture && mapSlot ) {
|
|
1271
|
+
|
|
1272
|
+
const map = await this.image( term.texture, term.texture.colorSpace !== 'raw' );
|
|
1273
|
+
if ( map ) {
|
|
1274
|
+
|
|
1275
|
+
this.transformTexture( map, term.transform );
|
|
1276
|
+
mat[ mapSlot ] = map;
|
|
1277
|
+
const scale = term.texture.scale;
|
|
1278
|
+
mat[ slot ].setRGB( scale?.[ 0 ] ?? 1, scale?.[ 1 ] ?? 1, scale?.[ 2 ] ?? 1 );
|
|
1279
|
+
surface.uvSet = term.texture.uvSet;
|
|
1280
|
+
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/** A scalar slot from a term; a texture lands in `mapSlot` only from the channel the engine reads there. */
|
|
1288
|
+
async applyScalar( mat, slot, term, surface, mapSlot = null, channel = null ) {
|
|
1289
|
+
|
|
1290
|
+
if ( ! term ) return;
|
|
1291
|
+
if ( typeof term.value === 'number' ) mat[ slot ] = term.value;
|
|
1292
|
+
else if ( term.texture && mapSlot ) {
|
|
1293
|
+
|
|
1294
|
+
if ( term.channel !== channel ) {
|
|
1295
|
+
|
|
1296
|
+
this.warnOnce( `chan:${mapSlot}:${term.channel}`, `USD ${slot} from a texture's "${term.channel}" channel is not supported (the engine reads "${channel}") — constant used` );
|
|
1297
|
+
return;
|
|
1298
|
+
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const map = await this.image( term.texture, false );
|
|
1302
|
+
if ( map ) {
|
|
1303
|
+
|
|
1304
|
+
this.transformTexture( map, term.transform );
|
|
1305
|
+
mat[ mapSlot ] = map;
|
|
1306
|
+
mat[ slot ] = term.texture.scale?.[ 0 ] ?? 1;
|
|
1307
|
+
surface.uvSet = term.texture.uvSet;
|
|
1308
|
+
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
transformTexture( map, transform ) {
|
|
1316
|
+
|
|
1317
|
+
if ( ! transform ) return;
|
|
1318
|
+
if ( transform.scale ) map.repeat.set( transform.scale[ 0 ], transform.scale[ 1 ] );
|
|
1319
|
+
if ( transform.translation ) map.offset.set( transform.translation[ 0 ], transform.translation[ 1 ] );
|
|
1320
|
+
if ( transform.rotation ) map.rotation = transform.rotation * DEG;
|
|
1321
|
+
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/** RenderMan's PxrDisneyBsdf (Burley 2015), as pbrt-v3's Disney material reads the same parameters. */
|
|
1325
|
+
async disney( shader ) {
|
|
1326
|
+
|
|
1327
|
+
const t = name => this.term( shader, name );
|
|
1328
|
+
const num = async ( name, fallback ) => {
|
|
1329
|
+
|
|
1330
|
+
const term = await t( name );
|
|
1331
|
+
return typeof term?.value === 'number' ? term.value : fallback;
|
|
1332
|
+
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
const mat = new MeshPhysicalMaterial( { side: DoubleSide } );
|
|
1336
|
+
const surface = { material: mat, perMesh: null, uvSet: 'st' };
|
|
1337
|
+
mat.color.setRGB( 0.2, 0.5, 0.8 );
|
|
1338
|
+
await this.applyColor( mat, 'color', await t( 'baseColor' ), surface, 'map' );
|
|
1339
|
+
mat.metalness = await num( 'metallic', 0 );
|
|
1340
|
+
mat.roughness = await num( 'roughness', 0.5 );
|
|
1341
|
+
mat.ior = await num( 'ior', 1.5 );
|
|
1342
|
+
|
|
1343
|
+
const tint = surface.perMesh ? null : mat.color.clone();
|
|
1344
|
+
const hue = c => {
|
|
1345
|
+
|
|
1346
|
+
const l = 0.3 * c.r + 0.6 * c.g + 0.1 * c.b;
|
|
1347
|
+
return l > 0 ? c.clone().multiplyScalar( 1 / l ) : new Color( 1, 1, 1 );
|
|
1348
|
+
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
const specularTint = await num( 'specularTint', 0 );
|
|
1352
|
+
if ( specularTint > 0 && tint ) mat.specularColor.lerp( hue( tint ), specularTint );
|
|
1353
|
+
|
|
1354
|
+
const sheen = await num( 'sheen', 0 );
|
|
1355
|
+
if ( sheen > 0 ) {
|
|
1356
|
+
|
|
1357
|
+
const sheenTint = await num( 'sheenTint', 0.5 );
|
|
1358
|
+
mat.sheen = Math.min( 1, sheen );
|
|
1359
|
+
mat.sheenColor.set( 1, 1, 1 );
|
|
1360
|
+
if ( tint ) mat.sheenColor.lerp( hue( tint ), sheenTint );
|
|
1361
|
+
mat.sheenRoughness = 0.5;
|
|
1362
|
+
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// Burley's clear coat is a fixed-IOR GTR1 lobe at a quarter weight, its gloss blending α from 0.1 to 0.001.
|
|
1366
|
+
const clearcoat = await num( 'clearcoat', 0 );
|
|
1367
|
+
if ( clearcoat > 0 ) {
|
|
1368
|
+
|
|
1369
|
+
mat.clearcoat = 0.25 * clearcoat;
|
|
1370
|
+
mat.clearcoatRoughness = Math.sqrt( 0.1 + ( 0.001 - 0.1 ) * await num( 'clearcoatGloss', 1 ) );
|
|
1371
|
+
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
const specTrans = await num( 'specTrans', 0 );
|
|
1375
|
+
if ( specTrans > 0 ) {
|
|
1376
|
+
|
|
1377
|
+
mat.transmission = specTrans;
|
|
1378
|
+
const transColor = ( await t( 'transColor' ) )?.value;
|
|
1379
|
+
const distance = await num( 'transDistance', 0 );
|
|
1380
|
+
if ( Array.isArray( transColor ) && distance > 0 ) {
|
|
1381
|
+
|
|
1382
|
+
mat.attenuationColor.setRGB( transColor[ 0 ], transColor[ 1 ], transColor[ 2 ] );
|
|
1383
|
+
mat.attenuationDistance = distance;
|
|
1384
|
+
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// A thin surface passes half its diffuse at diffTrans 1 (pbrt-v3 divides it by two).
|
|
1390
|
+
const thin = await num( 'isThin', 0 );
|
|
1391
|
+
const diffTrans = await num( 'diffTrans', 0 );
|
|
1392
|
+
if ( thin && diffTrans > 0 ) {
|
|
1393
|
+
|
|
1394
|
+
mat.diffuseTransmission = Math.min( 1, diffTrans / 2 );
|
|
1395
|
+
mat.diffuseTransmissionColor = mat.color.clone();
|
|
1396
|
+
mat.userData.diffuseTransmissionFromBase = true;
|
|
1397
|
+
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
const opacity = await num( 'opacity', 1 );
|
|
1401
|
+
if ( opacity < 1 ) {
|
|
1402
|
+
|
|
1403
|
+
mat.opacity = opacity;
|
|
1404
|
+
mat.transparent = true;
|
|
1405
|
+
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
await this.applyColor( mat, 'emissive', await t( 'emitColor' ), surface, 'emissiveMap' );
|
|
1409
|
+
return surface;
|
|
1410
|
+
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/** RenderMan's PxrSurface, its main lobes only: diffuse, primary specular, glass and diffuse transmission. */
|
|
1414
|
+
async pxrSurface( shader ) {
|
|
1415
|
+
|
|
1416
|
+
const t = name => this.term( shader, name );
|
|
1417
|
+
const num = async ( name, fallback ) => {
|
|
1418
|
+
|
|
1419
|
+
const term = await t( name );
|
|
1420
|
+
return typeof term?.value === 'number' ? term.value : fallback;
|
|
1421
|
+
|
|
1422
|
+
};
|
|
1423
|
+
|
|
1424
|
+
const mat = new MeshPhysicalMaterial( { side: DoubleSide } );
|
|
1425
|
+
const surface = { material: mat, perMesh: null, uvSet: 'st' };
|
|
1426
|
+
await this.applyColor( mat, 'color', await t( 'diffuseColor' ), surface, 'map' );
|
|
1427
|
+
mat.color.multiplyScalar( await num( 'diffuseGain', 1 ) );
|
|
1428
|
+
mat.roughness = await num( 'specularRoughness', 0.2 );
|
|
1429
|
+
mat.metalness = 0;
|
|
1430
|
+
|
|
1431
|
+
const refraction = await num( 'refractionGain', 0 );
|
|
1432
|
+
if ( refraction > 0 ) {
|
|
1433
|
+
|
|
1434
|
+
mat.transmission = Math.min( 1, refraction );
|
|
1435
|
+
mat.ior = await num( 'glassIor', 1.5 );
|
|
1436
|
+
mat.roughness = await num( 'glassRoughness', 0.1 );
|
|
1437
|
+
mat.color.setRGB( 1, 1, 1 );
|
|
1438
|
+
|
|
1439
|
+
} else {
|
|
1440
|
+
|
|
1441
|
+
const transmit = await num( 'diffuseTransmitGain', 0 );
|
|
1442
|
+
if ( transmit > 0 ) {
|
|
1443
|
+
|
|
1444
|
+
const color = ( await t( 'diffuseTransmitColor' ) )?.value;
|
|
1445
|
+
mat.diffuseTransmission = Math.min( 1, transmit );
|
|
1446
|
+
mat.diffuseTransmissionColor = Array.isArray( color ) ? new Color().setRGB( color[ 0 ], color[ 1 ], color[ 2 ] ) : new Color( 1, 1, 1 );
|
|
1447
|
+
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const presence = await num( 'presence', 1 );
|
|
1453
|
+
if ( presence < 1 ) {
|
|
1454
|
+
|
|
1455
|
+
mat.opacity = presence;
|
|
1456
|
+
mat.transparent = true;
|
|
1457
|
+
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
return surface;
|
|
1461
|
+
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
async previewSurface( shader ) {
|
|
1465
|
+
|
|
1466
|
+
const t = name => this.term( shader, name );
|
|
1467
|
+
const mat = new MeshPhysicalMaterial( { side: DoubleSide, roughness: 0.5, metalness: 0 } );
|
|
1468
|
+
const surface = { material: mat, perMesh: null, uvSet: 'st' };
|
|
1469
|
+
mat.color.setRGB( 0.18, 0.18, 0.18 );
|
|
1470
|
+
await this.applyColor( mat, 'color', await t( 'diffuseColor' ), surface, 'map' );
|
|
1471
|
+
await this.applyColor( mat, 'emissive', await t( 'emissiveColor' ), surface, 'emissiveMap' );
|
|
1472
|
+
if ( mat.emissiveMap || mat.emissive.r + mat.emissive.g + mat.emissive.b > 0 ) mat.emissiveIntensity = 1;
|
|
1473
|
+
await this.applyScalar( mat, 'roughness', await t( 'roughness' ), surface, 'roughnessMap', 'g' );
|
|
1474
|
+
await this.applyScalar( mat, 'clearcoat', await t( 'clearcoat' ), surface );
|
|
1475
|
+
await this.applyScalar( mat, 'clearcoatRoughness', await t( 'clearcoatRoughness' ), surface );
|
|
1476
|
+
await this.applyScalar( mat, 'ior', await t( 'ior' ), surface );
|
|
1477
|
+
|
|
1478
|
+
if ( ( await t( 'useSpecularWorkflow' ) )?.value === 1 ) {
|
|
1479
|
+
|
|
1480
|
+
mat.metalness = 0;
|
|
1481
|
+
const specular = ( await t( 'specularColor' ) )?.value;
|
|
1482
|
+
if ( Array.isArray( specular ) ) mat.specularColor.setRGB( specular[ 0 ], specular[ 1 ], specular[ 2 ] );
|
|
1483
|
+
|
|
1484
|
+
} else {
|
|
1485
|
+
|
|
1486
|
+
await this.applyScalar( mat, 'metalness', await t( 'metallic' ), surface, 'metalnessMap', 'b' );
|
|
1487
|
+
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
const opacity = await t( 'opacity' );
|
|
1491
|
+
const threshold = ( await t( 'opacityThreshold' ) )?.value ?? 0;
|
|
1492
|
+
if ( typeof opacity?.value === 'number' && opacity.value < 1 ) {
|
|
1493
|
+
|
|
1494
|
+
if ( threshold > 0 ) mat.alphaTest = threshold;
|
|
1495
|
+
else mat.transmission = 1 - opacity.value;
|
|
1496
|
+
|
|
1497
|
+
} else if ( opacity?.texture ) {
|
|
1498
|
+
|
|
1499
|
+
if ( opacity.texture.path === mat.map?.userData.usdPath && opacity.channel === 'a' ) {
|
|
1500
|
+
|
|
1501
|
+
mat.transparent = true;
|
|
1502
|
+
if ( threshold > 0 ) mat.alphaTest = threshold;
|
|
1503
|
+
|
|
1504
|
+
} else {
|
|
1505
|
+
|
|
1506
|
+
this.warnOnce( 'opacitymap', 'USD opacity from a texture other than the colour map\'s alpha is not supported' );
|
|
1507
|
+
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
const normal = await t( 'normal' );
|
|
1513
|
+
if ( normal?.texture ) {
|
|
1514
|
+
|
|
1515
|
+
const map = await this.image( normal.texture, false );
|
|
1516
|
+
if ( map ) {
|
|
1517
|
+
|
|
1518
|
+
mat.normalMap = map;
|
|
1519
|
+
surface.uvSet = normal.texture.uvSet;
|
|
1520
|
+
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
return surface;
|
|
1526
|
+
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// ── cameras and lights ───────────────────────────────────────
|
|
1530
|
+
|
|
1531
|
+
camera( prim, matrix ) {
|
|
1532
|
+
|
|
1533
|
+
const hAperture = prim.value( 'horizontalAperture' ) ?? 20.955;
|
|
1534
|
+
const vAperture = prim.value( 'verticalAperture' ) ?? 15.2908;
|
|
1535
|
+
const focal = prim.value( 'focalLength' ) ?? 50;
|
|
1536
|
+
const [ near, far ] = prim.value( 'clippingRange' ) ?? [ 1, 1000000 ];
|
|
1537
|
+
const camera = prim.value( 'projection' ) === 'orthographic'
|
|
1538
|
+
// Apertures are in tenths of a scene unit.
|
|
1539
|
+
? new OrthographicCamera( - hAperture / 20, hAperture / 20, vAperture / 20, - vAperture / 20, near, far )
|
|
1540
|
+
: new PerspectiveCamera( 2 * Math.atan( vAperture / ( 2 * focal ) ) / DEG, hAperture / vAperture, near, far );
|
|
1541
|
+
camera.name = prim.name;
|
|
1542
|
+
const scale = new Vector3();
|
|
1543
|
+
matrix.decompose( camera.position, camera.quaternion, scale );
|
|
1544
|
+
camera.updateMatrixWorld( true );
|
|
1545
|
+
this.cameras.push( camera );
|
|
1546
|
+
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
light( prim, type, matrix ) {
|
|
1550
|
+
|
|
1551
|
+
const read = ( name, fallback ) => prim.value( `inputs:${name}` ) ?? prim.value( name ) ?? fallback;
|
|
1552
|
+
const color = read( 'color', [ 1, 1, 1 ] );
|
|
1553
|
+
const radiance = read( 'intensity', type === 'DistantLight' ? 50000 : 1 ) * 2 ** read( 'exposure', 0 );
|
|
1554
|
+
if ( read( 'enableColorTemperature', false ) ) this.warnOnce( 'temperature', 'USD light colour temperature is not applied' );
|
|
1555
|
+
if ( prim.propertyNames().some( name => name.startsWith( 'collection:lightLink' ) || name.startsWith( 'collection:shadowLink' ) ) && type !== 'DomeLight' ) {
|
|
1556
|
+
|
|
1557
|
+
this.warnOnce( 'linking', 'USD light linking is not supported — every light lights everything' );
|
|
1558
|
+
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
if ( type === 'DomeLight' ) {
|
|
1562
|
+
|
|
1563
|
+
const file = prim.assetPath( 'inputs:texture:file' ) ?? prim.assetPath( 'texture:file' );
|
|
1564
|
+
const excludes = prim.targets( 'collection:lightLink:excludes' );
|
|
1565
|
+
const top = this.stage.pseudoRoot.childNames().map( name => `/${name}` );
|
|
1566
|
+
this.domes.push( {
|
|
1567
|
+
name: prim.name, file: file?.path ?? null, asset: file?.asset ?? null, intensity: radiance, matrix: matrix.clone(), color,
|
|
1568
|
+
lightsScene: ! excludes.some( path => top.includes( path ) ),
|
|
1569
|
+
} );
|
|
1570
|
+
return;
|
|
1571
|
+
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
let light;
|
|
1575
|
+
if ( type === 'RectLight' || type === 'DiskLight' ) {
|
|
1576
|
+
|
|
1577
|
+
const disk = type === 'DiskLight';
|
|
1578
|
+
const radius = read( 'radius', 0.5 );
|
|
1579
|
+
light = new RectAreaLight( new Color(), radiance, disk ? radius * 2 : read( 'width', 1 ), disk ? radius * 2 : read( 'height', 1 ) );
|
|
1580
|
+
light.userData.normalize = read( 'normalize', false );
|
|
1581
|
+
if ( disk ) light.userData.shape = 'disk';
|
|
1582
|
+
|
|
1583
|
+
} else if ( type === 'SphereLight' || type === 'CylinderLight' ) {
|
|
1584
|
+
|
|
1585
|
+
const radius = read( 'radius', 0.5 );
|
|
1586
|
+
const cone = read( 'shaping:cone:angle', 180 );
|
|
1587
|
+
// The engine's lamps take 4π × the radiant intensity, which a sphere of radiance L shows as L·πr².
|
|
1588
|
+
const intensity = 4 * Math.PI * radiance * Math.PI * radius * radius;
|
|
1589
|
+
if ( cone < 90 ) {
|
|
1590
|
+
|
|
1591
|
+
light = new SpotLight( new Color(), intensity );
|
|
1592
|
+
light.angle = cone * DEG;
|
|
1593
|
+
light.penumbra = Math.min( 1, Math.max( 0, read( 'shaping:cone:softness', 0 ) ) );
|
|
1594
|
+
|
|
1595
|
+
} else {
|
|
1596
|
+
|
|
1597
|
+
light = new PointLight( new Color(), intensity );
|
|
1598
|
+
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
light.decay = 2;
|
|
1602
|
+
light.distance = 0;
|
|
1603
|
+
light.userData.__candelaConverted = true;
|
|
1604
|
+
if ( type === 'CylinderLight' ) this.warnOnce( 'cylinder', 'USD cylinder lights are placed as point lights' );
|
|
1605
|
+
|
|
1606
|
+
} else {
|
|
1607
|
+
|
|
1608
|
+
// Lux, as glTF's and three.js's are: the loader takes the luminous efficacy out.
|
|
1609
|
+
light = new DirectionalLight( new Color(), radiance );
|
|
1610
|
+
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
const peak = Math.max( color[ 0 ], color[ 1 ], color[ 2 ] );
|
|
1614
|
+
if ( ! ( peak > 0 ) || ! ( radiance > 0 ) ) return;
|
|
1615
|
+
light.color.setRGB( color[ 0 ] / peak, color[ 1 ] / peak, color[ 2 ] / peak );
|
|
1616
|
+
light.intensity *= peak;
|
|
1617
|
+
light.name = prim.name;
|
|
1618
|
+
matrix.decompose( light.position, light.quaternion, light.scale );
|
|
1619
|
+
if ( light.target ) {
|
|
1620
|
+
|
|
1621
|
+
const forward = new Vector3( 0, 0, - 1 ).transformDirection( matrix );
|
|
1622
|
+
light.target.position.copy( forward );
|
|
1623
|
+
light.add( light.target );
|
|
1624
|
+
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
this.lights.push( light );
|
|
1628
|
+
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/**
|
|
1632
|
+
* The dome light to install as the environment: the first that lights the scene. Its texture is a lat-long with +Y up
|
|
1633
|
+
* and its centre facing +Z (OpenEXR's convention); the engine's equirect centre faces +X, a quarter turn about Y.
|
|
1634
|
+
*/
|
|
1635
|
+
pickDome() {
|
|
1636
|
+
|
|
1637
|
+
const dome = this.domes.find( d => d.lightsScene && d.file ) ?? null;
|
|
1638
|
+
if ( this.domes.length > 1 ) this.warnOnce( 'domes', `${this.domes.length} USD dome lights; "${dome?.name ?? this.domes[ 0 ].name}" lights the scene` );
|
|
1639
|
+
if ( ! dome ) {
|
|
1640
|
+
|
|
1641
|
+
const named = this.domes.find( d => d.asset );
|
|
1642
|
+
if ( named ) this.warnOnce( 'domefile', `USD dome light texture "${named.asset}" not found` );
|
|
1643
|
+
return null;
|
|
1644
|
+
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
const pole = new Vector3( 0, 1, 0 ).transformDirection( dome.matrix );
|
|
1648
|
+
if ( pole.y < 0.999 ) this.warnOnce( 'domepole', 'USD dome light is tilted; only its turn about the up axis is applied' );
|
|
1649
|
+
const front = new Vector3( 0, 0, 1 ).transformDirection( dome.matrix );
|
|
1650
|
+
const yaw = Math.atan2( front.x, front.z ) / DEG;
|
|
1651
|
+
return { file: dome.file, intensity: dome.intensity * Math.max( ...dome.color ), rotation: 90 - yaw };
|
|
1652
|
+
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
}
|