rayzee 4.8.14 → 5.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 +84 -97
- package/dist/rayzee.es.js +1915 -2185
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +56 -56
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/PathTracerApp.js +402 -1567
- package/src/Pipeline/CompletionTracker.js +89 -0
- package/src/Processor/AssetLoader.js +26 -2
- package/src/Processor/SceneProcessor.js +146 -0
- package/src/Processor/TLASBuilder.js +61 -30
- package/src/RenderSettings.js +82 -4
- package/src/TSL/LightsSampling.js +6 -7
- package/src/index.js +2 -12
- package/src/managers/AnimationManager.js +18 -6
- package/src/managers/CameraManager.js +133 -15
- package/src/managers/DenoisingManager.js +289 -3
- package/src/managers/EnvironmentManager.js +94 -1
- package/src/managers/InteractionManager.js +142 -0
- package/src/managers/LightManager.js +51 -1
- package/src/managers/OverlayManager.js +97 -0
- package/src/managers/TransformManager.js +1 -0
- package/src/managers/VideoRenderManager.js +6 -6
- package/src/managers/helpers/StatsHelper.js +45 -0
- package/src/api/AnimationAPI.js +0 -87
- package/src/api/CameraAPI.js +0 -109
- package/src/api/DenoisingAPI.js +0 -243
- package/src/api/EnvironmentAPI.js +0 -106
- package/src/api/LightsAPI.js +0 -80
- package/src/api/MaterialsAPI.js +0 -73
- package/src/api/OutputAPI.js +0 -90
- package/src/api/SelectionAPI.js +0 -89
- package/src/api/TransformAPI.js +0 -49
- package/src/api/index.js +0 -16
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks render completion state, time limits, and sample limits.
|
|
3
|
+
*
|
|
4
|
+
* Owns: timeElapsed, lastResetTime, renderCompleteDispatched.
|
|
5
|
+
* Called each frame by the render loop and on reset.
|
|
6
|
+
*/
|
|
7
|
+
export class CompletionTracker {
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
|
|
11
|
+
this.timeElapsed = 0;
|
|
12
|
+
this.lastResetTime = performance.now();
|
|
13
|
+
this.renderCompleteDispatched = false;
|
|
14
|
+
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Updates elapsed time. Call each frame while rendering is active.
|
|
19
|
+
*/
|
|
20
|
+
updateTime() {
|
|
21
|
+
|
|
22
|
+
this.timeElapsed = ( performance.now() - this.lastResetTime ) / 1000;
|
|
23
|
+
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Checks whether the time-based render limit has been reached.
|
|
28
|
+
* @param {string} renderLimitMode - 'time' or 'samples'
|
|
29
|
+
* @param {number} renderTimeLimit - Time limit in seconds
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
isTimeLimitReached( renderLimitMode, renderTimeLimit ) {
|
|
33
|
+
|
|
34
|
+
return renderLimitMode === 'time' && renderTimeLimit > 0 && this.timeElapsed >= renderTimeLimit;
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Checks whether ANY render limit (time or samples) is reached.
|
|
40
|
+
* @param {Object} pathTracer - The PathTracer stage
|
|
41
|
+
* @param {string} renderLimitMode
|
|
42
|
+
* @param {number} renderTimeLimit
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
isLimitReached( pathTracer, renderLimitMode, renderTimeLimit ) {
|
|
46
|
+
|
|
47
|
+
if ( ! pathTracer ) return false;
|
|
48
|
+
|
|
49
|
+
if ( this.isTimeLimitReached( renderLimitMode, renderTimeLimit ) ) return true;
|
|
50
|
+
|
|
51
|
+
return pathTracer.frameCount >= pathTracer.completionThreshold;
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Marks render as complete and returns true if this is the first time.
|
|
57
|
+
* @returns {boolean} true if freshly completed (should trigger denoise chain)
|
|
58
|
+
*/
|
|
59
|
+
markComplete() {
|
|
60
|
+
|
|
61
|
+
if ( this.renderCompleteDispatched ) return false;
|
|
62
|
+
this.renderCompleteDispatched = true;
|
|
63
|
+
return true;
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resets all tracking state. Call on accumulation reset.
|
|
69
|
+
*/
|
|
70
|
+
reset() {
|
|
71
|
+
|
|
72
|
+
this.timeElapsed = 0;
|
|
73
|
+
this.lastResetTime = performance.now();
|
|
74
|
+
this.renderCompleteDispatched = false;
|
|
75
|
+
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Adjusts lastResetTime to account for idle time so timeElapsed
|
|
80
|
+
* continues from where it paused rather than including idle time.
|
|
81
|
+
*/
|
|
82
|
+
resumeFromPause() {
|
|
83
|
+
|
|
84
|
+
this.renderCompleteDispatched = false;
|
|
85
|
+
this.lastResetTime = performance.now() - this.timeElapsed * 1000;
|
|
86
|
+
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Box3, Vector3, RectAreaLight, Color, FloatType, LinearFilter, EquirectangularReflectionMapping, LinearMipmapLinearFilter,
|
|
2
|
-
TextureLoader, Mesh, MeshStandardMaterial, Points, PointsMaterial, LoadingManager, EventDispatcher
|
|
2
|
+
TextureLoader, Mesh, MeshStandardMaterial, MeshPhysicalMaterial, CircleGeometry, Points, PointsMaterial, LoadingManager, EventDispatcher
|
|
3
3
|
} from 'three';
|
|
4
4
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
5
5
|
import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
|
|
@@ -1214,7 +1214,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1214
1214
|
|
|
1215
1215
|
const light = new RectAreaLight(
|
|
1216
1216
|
new Color( ...userData.color ),
|
|
1217
|
-
userData.intensity / Math.PI, // Adjust intensity for better visual results
|
|
1217
|
+
userData.intensity * 0.1 / Math.PI, // Adjust intensity for better visual results
|
|
1218
1218
|
userData.width,
|
|
1219
1219
|
userData.height
|
|
1220
1220
|
);
|
|
@@ -1253,6 +1253,30 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1253
1253
|
}
|
|
1254
1254
|
|
|
1255
1255
|
// Utility methods
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* Creates and adds a floor plane to the scene.
|
|
1259
|
+
* The floor plane is used for focus raycasting and ground contact.
|
|
1260
|
+
*/
|
|
1261
|
+
createFloorPlane() {
|
|
1262
|
+
|
|
1263
|
+
this.floorPlane = new Mesh(
|
|
1264
|
+
new CircleGeometry(),
|
|
1265
|
+
new MeshPhysicalMaterial( {
|
|
1266
|
+
transparent: false,
|
|
1267
|
+
color: 0x303030,
|
|
1268
|
+
roughness: 1,
|
|
1269
|
+
metalness: 0,
|
|
1270
|
+
opacity: 0,
|
|
1271
|
+
transmission: 0,
|
|
1272
|
+
} )
|
|
1273
|
+
);
|
|
1274
|
+
this.floorPlane.name = "Ground";
|
|
1275
|
+
this.floorPlane.visible = false;
|
|
1276
|
+
this.scene.add( this.floorPlane );
|
|
1277
|
+
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1256
1280
|
setFloorPlane( floorPlane ) {
|
|
1257
1281
|
|
|
1258
1282
|
this.floorPlane = floorPlane;
|
|
@@ -1297,6 +1297,152 @@ export class SceneProcessor {
|
|
|
1297
1297
|
|
|
1298
1298
|
}
|
|
1299
1299
|
|
|
1300
|
+
/**
|
|
1301
|
+
* Computes the dirty buffer ranges for a set of affected mesh BLASes.
|
|
1302
|
+
* Used for partial GPU upload after per-mesh refit instead of full buffer copy.
|
|
1303
|
+
*
|
|
1304
|
+
* @param {number[]} affectedMeshIndices
|
|
1305
|
+
* @returns {{ triRanges: Array<{offset:number,count:number}>, bvhRanges: Array<{offset:number,count:number}> }}
|
|
1306
|
+
*/
|
|
1307
|
+
computeBLASDirtyRanges( affectedMeshIndices ) {
|
|
1308
|
+
|
|
1309
|
+
const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
|
|
1310
|
+
const FPN = 16; // FLOATS_PER_NODE — 4 × vec4 per BVH node
|
|
1311
|
+
const triRanges = [];
|
|
1312
|
+
const bvhRanges = [];
|
|
1313
|
+
|
|
1314
|
+
for ( const meshIdx of affectedMeshIndices ) {
|
|
1315
|
+
|
|
1316
|
+
const entry = this.instanceTable.entries[ meshIdx ];
|
|
1317
|
+
if ( ! entry ) continue;
|
|
1318
|
+
|
|
1319
|
+
triRanges.push( { offset: entry.triOffset * FPT, count: entry.triCount * FPT } );
|
|
1320
|
+
bvhRanges.push( { offset: entry.blasOffset * FPN, count: entry.blasNodeCount * FPN } );
|
|
1321
|
+
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Always include TLAS range (rebuilt on every refit)
|
|
1325
|
+
bvhRanges.push( { offset: 0, count: this.instanceTable.tlasNodeCount * FPN } );
|
|
1326
|
+
|
|
1327
|
+
return { triRanges, bvhRanges };
|
|
1328
|
+
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Transfers all scene data (geometry, BVH, materials, textures, emissive, lights)
|
|
1333
|
+
* from this SceneProcessor to the PathTracer stage for GPU rendering.
|
|
1334
|
+
*
|
|
1335
|
+
* @param {import('../Stages/PathTracer.js').PathTracer} pathTracer
|
|
1336
|
+
* @param {import('../managers/LightManager.js').LightManager} lightManager
|
|
1337
|
+
* @param {import('three').Scene} meshScene
|
|
1338
|
+
* @param {import('three').Texture|null} environmentTexture
|
|
1339
|
+
* @returns {boolean} false if critical data is missing
|
|
1340
|
+
*/
|
|
1341
|
+
uploadToPathTracer( pathTracer, lightManager, meshScene, environmentTexture ) {
|
|
1342
|
+
|
|
1343
|
+
if ( ! this.triangleData ) {
|
|
1344
|
+
|
|
1345
|
+
console.error( 'SceneProcessor: Failed to get triangle data' );
|
|
1346
|
+
return false;
|
|
1347
|
+
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
pathTracer.setTriangleData( this.triangleData, this.triangleCount );
|
|
1351
|
+
|
|
1352
|
+
if ( ! this.bvhData ) {
|
|
1353
|
+
|
|
1354
|
+
console.error( 'SceneProcessor: Failed to get BVH data' );
|
|
1355
|
+
return false;
|
|
1356
|
+
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
pathTracer.setBVHData( this.bvhData );
|
|
1360
|
+
|
|
1361
|
+
if ( this.materialData ) {
|
|
1362
|
+
|
|
1363
|
+
pathTracer.materialData.setMaterialData( this.materialData );
|
|
1364
|
+
|
|
1365
|
+
} else {
|
|
1366
|
+
|
|
1367
|
+
console.warn( 'SceneProcessor: No material data, using defaults' );
|
|
1368
|
+
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if ( environmentTexture ) {
|
|
1372
|
+
|
|
1373
|
+
pathTracer.environment.setEnvironmentTexture( environmentTexture );
|
|
1374
|
+
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
pathTracer.materialData.setMaterialTextures( {
|
|
1378
|
+
albedoMaps: this.albedoTextures,
|
|
1379
|
+
normalMaps: this.normalTextures,
|
|
1380
|
+
bumpMaps: this.bumpTextures,
|
|
1381
|
+
roughnessMaps: this.roughnessTextures,
|
|
1382
|
+
metalnessMaps: this.metalnessTextures,
|
|
1383
|
+
emissiveMaps: this.emissiveTextures,
|
|
1384
|
+
displacementMaps: this.displacementTextures,
|
|
1385
|
+
} );
|
|
1386
|
+
|
|
1387
|
+
if ( this.emissiveTriangleData ) {
|
|
1388
|
+
|
|
1389
|
+
pathTracer.setEmissiveTriangleData(
|
|
1390
|
+
this.emissiveTriangleData,
|
|
1391
|
+
this.emissiveTriangleCount,
|
|
1392
|
+
this.emissiveTotalPower,
|
|
1393
|
+
);
|
|
1394
|
+
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
if ( this.lightBVHNodeData ) {
|
|
1398
|
+
|
|
1399
|
+
pathTracer.setLightBVHData(
|
|
1400
|
+
this.lightBVHNodeData,
|
|
1401
|
+
this.lightBVHNodeCount,
|
|
1402
|
+
);
|
|
1403
|
+
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
lightManager.transferSceneLights( meshScene );
|
|
1407
|
+
return true;
|
|
1408
|
+
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Updates material emissive data and rebuilds emissive triangle sampling data.
|
|
1413
|
+
* Returns null if no change, or the updated emissive data for GPU upload.
|
|
1414
|
+
*
|
|
1415
|
+
* @param {number} materialIndex
|
|
1416
|
+
* @param {string} property - 'emissive' | 'emissiveIntensity' | 'visible'
|
|
1417
|
+
* @param {*} value
|
|
1418
|
+
* @returns {{ rawData: Float32Array, emissiveCount: number, totalPower: number }|null}
|
|
1419
|
+
*/
|
|
1420
|
+
updateMaterialEmissive( materialIndex, property, value ) {
|
|
1421
|
+
|
|
1422
|
+
if ( ! this.emissiveTriangleBuilder ) return null;
|
|
1423
|
+
|
|
1424
|
+
const mat = this.materials[ materialIndex ];
|
|
1425
|
+
if ( ! mat ) return null;
|
|
1426
|
+
|
|
1427
|
+
if ( property === 'emissive' ) mat.emissive = value;
|
|
1428
|
+
else if ( property === 'emissiveIntensity' ) mat.emissiveIntensity = value;
|
|
1429
|
+
else if ( property === 'visible' ) mat.visible = value;
|
|
1430
|
+
|
|
1431
|
+
const changed = this.emissiveTriangleBuilder.updateMaterialEmissive(
|
|
1432
|
+
materialIndex, mat,
|
|
1433
|
+
this.triangleData, this.materials, this.triangleCount,
|
|
1434
|
+
);
|
|
1435
|
+
|
|
1436
|
+
if ( ! changed ) return null;
|
|
1437
|
+
|
|
1438
|
+
return {
|
|
1439
|
+
rawData: this.emissiveTriangleBuilder.createEmissiveRawData(),
|
|
1440
|
+
emissiveCount: this.emissiveTriangleBuilder.emissiveCount,
|
|
1441
|
+
totalPower: this.emissiveTriangleBuilder.totalEmissivePower,
|
|
1442
|
+
};
|
|
1443
|
+
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1300
1446
|
/**
|
|
1301
1447
|
* Update triangle positions for a single mesh entry.
|
|
1302
1448
|
* Iterates in BVH order for sequential writes (cache-friendly), random reads from newPositions.
|
|
@@ -116,42 +116,48 @@ export class TLASBuilder {
|
|
|
116
116
|
let bestAxis = 0;
|
|
117
117
|
let bestSplit = 0;
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
// Only attempt SAH when surface area is finite and positive —
|
|
120
|
+
// degenerate/overflow AABBs (meshes far from origin) produce NaN costs.
|
|
121
|
+
if ( parentSA > 0 && isFinite( parentSA ) ) {
|
|
120
122
|
|
|
121
|
-
|
|
122
|
-
const sorted = indices.slice().sort( ( a, b ) => {
|
|
123
|
+
for ( let axis = 0; axis < 3; axis ++ ) {
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
const cA = this._centroid( aabbA, axis );
|
|
127
|
-
const cB = this._centroid( aabbB, axis );
|
|
128
|
-
return cA - cB;
|
|
125
|
+
// Sort indices by centroid along axis
|
|
126
|
+
const sorted = indices.slice().sort( ( a, b ) => {
|
|
129
127
|
|
|
130
|
-
|
|
128
|
+
const aabbA = entries[ a ].worldAABB;
|
|
129
|
+
const aabbB = entries[ b ].worldAABB;
|
|
130
|
+
const cA = this._centroid( aabbA, axis );
|
|
131
|
+
const cB = this._centroid( aabbB, axis );
|
|
132
|
+
return cA - cB;
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
for ( let i = 1; i < sorted.length; i ++ ) {
|
|
134
|
+
} );
|
|
134
135
|
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
// Evaluate SAH for each split position
|
|
137
|
+
for ( let i = 1; i < sorted.length; i ++ ) {
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
leftAABB.maxX, leftAABB.maxY, leftAABB.maxZ
|
|
141
|
-
);
|
|
142
|
-
const rightSA = this._surfaceArea(
|
|
143
|
-
rightAABB.minX, rightAABB.minY, rightAABB.minZ,
|
|
144
|
-
rightAABB.maxX, rightAABB.maxY, rightAABB.maxZ
|
|
145
|
-
);
|
|
139
|
+
const leftAABB = this._computeGroupAABB( entries, sorted, 0, i );
|
|
140
|
+
const rightAABB = this._computeGroupAABB( entries, sorted, i, sorted.length );
|
|
146
141
|
|
|
147
|
-
|
|
148
|
-
|
|
142
|
+
const leftSA = this._surfaceArea(
|
|
143
|
+
leftAABB.minX, leftAABB.minY, leftAABB.minZ,
|
|
144
|
+
leftAABB.maxX, leftAABB.maxY, leftAABB.maxZ
|
|
145
|
+
);
|
|
146
|
+
const rightSA = this._surfaceArea(
|
|
147
|
+
rightAABB.minX, rightAABB.minY, rightAABB.minZ,
|
|
148
|
+
rightAABB.maxX, rightAABB.maxY, rightAABB.maxZ
|
|
149
|
+
);
|
|
149
150
|
|
|
150
|
-
|
|
151
|
+
// SAH cost: traversal + (leftSA/parentSA * leftCount + rightSA/parentSA * rightCount)
|
|
152
|
+
const cost = 1.0 + ( leftSA * i + rightSA * ( sorted.length - i ) ) / parentSA;
|
|
151
153
|
|
|
152
|
-
bestCost
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
if ( cost < bestCost ) {
|
|
155
|
+
|
|
156
|
+
bestCost = cost;
|
|
157
|
+
bestAxis = axis;
|
|
158
|
+
bestSplit = i;
|
|
159
|
+
|
|
160
|
+
}
|
|
155
161
|
|
|
156
162
|
}
|
|
157
163
|
|
|
@@ -159,6 +165,19 @@ export class TLASBuilder {
|
|
|
159
165
|
|
|
160
166
|
}
|
|
161
167
|
|
|
168
|
+
// Fallback to median split when SAH fails to find a valid partition
|
|
169
|
+
// (degenerate AABB, overflow surface area, or coincident centroids).
|
|
170
|
+
if ( bestSplit <= 0 || bestSplit >= indices.length ) {
|
|
171
|
+
|
|
172
|
+
bestAxis = 0;
|
|
173
|
+
const dx = maxX - minX, dy = maxY - minY, dz = maxZ - minZ;
|
|
174
|
+
if ( dy > dx && dy > dz ) bestAxis = 1;
|
|
175
|
+
else if ( dz > dx ) bestAxis = 2;
|
|
176
|
+
|
|
177
|
+
bestSplit = indices.length >> 1;
|
|
178
|
+
|
|
179
|
+
}
|
|
180
|
+
|
|
162
181
|
// Sort along best axis and split
|
|
163
182
|
const sorted = indices.slice().sort( ( a, b ) => {
|
|
164
183
|
|
|
@@ -301,10 +320,22 @@ export class TLASBuilder {
|
|
|
301
320
|
|
|
302
321
|
}
|
|
303
322
|
|
|
304
|
-
_countNodes(
|
|
323
|
+
_countNodes( root ) {
|
|
324
|
+
|
|
325
|
+
if ( ! root ) return 0;
|
|
326
|
+
|
|
327
|
+
let count = 0;
|
|
328
|
+
const stack = [ root ];
|
|
329
|
+
while ( stack.length > 0 ) {
|
|
330
|
+
|
|
331
|
+
const node = stack.pop();
|
|
332
|
+
count ++;
|
|
333
|
+
if ( node.leftChild ) stack.push( node.leftChild );
|
|
334
|
+
if ( node.rightChild ) stack.push( node.rightChild );
|
|
335
|
+
|
|
336
|
+
}
|
|
305
337
|
|
|
306
|
-
|
|
307
|
-
return 1 + this._countNodes( node.leftChild ) + this._countNodes( node.rightChild );
|
|
338
|
+
return count;
|
|
308
339
|
|
|
309
340
|
}
|
|
310
341
|
|
package/src/RenderSettings.js
CHANGED
|
@@ -101,13 +101,91 @@ export class RenderSettings extends EventDispatcher {
|
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
103
|
* Wires internal references. Called by PathTracerApp after init().
|
|
104
|
+
*
|
|
105
|
+
* @param {Object} params
|
|
106
|
+
* @param {Object} params.stages - Pipeline stages { pathTracer, display, autoExposure, ... }
|
|
107
|
+
* @param {Function} params.resetCallback - Called to reset accumulation
|
|
108
|
+
* @param {Function} [params.reconcileCompletion] - Called when completion limits change
|
|
104
109
|
*/
|
|
105
|
-
bind( {
|
|
110
|
+
bind( { stages, resetCallback, reconcileCompletion } ) {
|
|
106
111
|
|
|
107
|
-
this._pathTracer = pathTracer;
|
|
112
|
+
this._pathTracer = stages.pathTracer;
|
|
108
113
|
this._resetCallback = resetCallback;
|
|
109
|
-
this.
|
|
110
|
-
this.
|
|
114
|
+
this._delegates = {};
|
|
115
|
+
this._handlers = this._buildHandlers( stages, reconcileCompletion );
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Builds handler functions for multi-stage settings that can't
|
|
121
|
+
* be routed with a simple uniform forward.
|
|
122
|
+
*/
|
|
123
|
+
_buildHandlers( stages, reconcileCompletion ) {
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
|
|
127
|
+
handleTransparentBackground: ( value ) => {
|
|
128
|
+
|
|
129
|
+
stages.pathTracer?.setUniform( 'transparentBackground', value );
|
|
130
|
+
stages.display?.setTransparentBackground( value );
|
|
131
|
+
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
handleExposure: ( value ) => {
|
|
135
|
+
|
|
136
|
+
if ( ! stages.autoExposure?.enabled ) {
|
|
137
|
+
|
|
138
|
+
stages.display?.setExposure( value );
|
|
139
|
+
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
handleSaturation: ( value ) => {
|
|
145
|
+
|
|
146
|
+
stages.display?.setSaturation( value );
|
|
147
|
+
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
handleRenderLimitMode: ( value ) => {
|
|
151
|
+
|
|
152
|
+
stages.pathTracer?.setRenderLimitMode?.( value );
|
|
153
|
+
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
handleMaxSamples: ( value ) => {
|
|
157
|
+
|
|
158
|
+
stages.pathTracer?.setUniform( 'maxSamples', value );
|
|
159
|
+
stages.pathTracer?.updateCompletionThreshold();
|
|
160
|
+
reconcileCompletion?.();
|
|
161
|
+
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
handleRenderTimeLimit: () => {
|
|
165
|
+
|
|
166
|
+
reconcileCompletion?.();
|
|
167
|
+
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
handleRenderMode: ( value ) => {
|
|
171
|
+
|
|
172
|
+
stages.pathTracer?.setUniform( 'renderMode', parseInt( value ) );
|
|
173
|
+
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
handleEnvironmentRotation: ( value ) => {
|
|
177
|
+
|
|
178
|
+
stages.pathTracer?.environment.setEnvironmentRotation( value );
|
|
179
|
+
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
handleInteractionModeEnabled: ( value ) => {
|
|
183
|
+
|
|
184
|
+
stages.pathTracer?.setInteractionModeEnabled( value );
|
|
185
|
+
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
};
|
|
111
189
|
|
|
112
190
|
}
|
|
113
191
|
|
|
@@ -145,7 +145,7 @@ export const sampleRectAreaLight = Fn( ( [ light, rayOrigin, ruv, lightSelection
|
|
|
145
145
|
const cosAngle = dot( direction.negate(), lightNormal ).toVar();
|
|
146
146
|
|
|
147
147
|
ls_lightType.assign( int( LIGHT_TYPE_AREA ) );
|
|
148
|
-
ls_emission.assign( light.color.mul( light.intensity )
|
|
148
|
+
ls_emission.assign( light.color.mul( light.intensity ) );
|
|
149
149
|
ls_distance.assign( dist );
|
|
150
150
|
ls_direction.assign( direction );
|
|
151
151
|
// Guard division: ensure denominator is never zero
|
|
@@ -205,7 +205,7 @@ export const sampleCircAreaLight = Fn( ( [ light, rayOrigin, ruv, lightSelection
|
|
|
205
205
|
const cosAngle = dot( direction.negate(), lightNormal ).toVar();
|
|
206
206
|
|
|
207
207
|
ls_lightType.assign( int( LIGHT_TYPE_AREA ) );
|
|
208
|
-
ls_emission.assign( light.color.mul( light.intensity )
|
|
208
|
+
ls_emission.assign( light.color.mul( light.intensity ) );
|
|
209
209
|
ls_distance.assign( dist );
|
|
210
210
|
ls_direction.assign( direction );
|
|
211
211
|
// Guard division
|
|
@@ -1062,15 +1062,14 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
1062
1062
|
const lightPdfWeighted = lightSample.pdf.mul( lightWeight ).toVar();
|
|
1063
1063
|
const brdfPdfWeighted = bPdf.mul( brdfWeight ).toVar();
|
|
1064
1064
|
|
|
1065
|
-
// Apply power heuristic for area lights
|
|
1065
|
+
// Apply power heuristic only for area lights — the BRDF path can
|
|
1066
|
+
// intersect area lights, so both strategies contribute and MIS is valid.
|
|
1067
|
+
// Point/spot/directional lights are delta or non-intersectable by the
|
|
1068
|
+
// BRDF path, so MIS would only reduce energy without compensation.
|
|
1066
1069
|
If( lightSample.lightType.equal( int( LIGHT_TYPE_AREA ) ), () => {
|
|
1067
1070
|
|
|
1068
1071
|
misW.assign( powerHeuristic( { pdf1: lightPdfWeighted, pdf2: brdfPdfWeighted } ) );
|
|
1069
1072
|
|
|
1070
|
-
} ).ElseIf( bounceIndex.equal( int( 0 ) ).and( lightSample.lightType.equal( int( LIGHT_TYPE_DIRECTIONAL ) ) ), () => {
|
|
1071
|
-
|
|
1072
|
-
misW.assign( powerHeuristic( { pdf1: lightPdfWeighted, pdf2: brdfPdfWeighted } ) );
|
|
1073
|
-
|
|
1074
1073
|
} );
|
|
1075
1074
|
|
|
1076
1075
|
} );
|
package/src/index.js
CHANGED
|
@@ -53,15 +53,5 @@ export { TransformManager } from './managers/TransformManager.js';
|
|
|
53
53
|
// Video rendering
|
|
54
54
|
export { VideoRenderManager } from './managers/VideoRenderManager.js';
|
|
55
55
|
|
|
56
|
-
//
|
|
57
|
-
export {
|
|
58
|
-
OutputAPI,
|
|
59
|
-
LightsAPI,
|
|
60
|
-
AnimationAPI,
|
|
61
|
-
SelectionAPI,
|
|
62
|
-
TransformAPI,
|
|
63
|
-
CameraAPI,
|
|
64
|
-
EnvironmentAPI,
|
|
65
|
-
MaterialsAPI,
|
|
66
|
-
DenoisingAPI,
|
|
67
|
-
} from './api/index.js';
|
|
56
|
+
// Interaction
|
|
57
|
+
export { InteractionManager } from './managers/InteractionManager.js';
|
|
@@ -6,29 +6,35 @@
|
|
|
6
6
|
* them in the format expected by PathTracerApp.refitBVH().
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { AnimationMixer, Timer, Vector3, LoopRepeat, LoopOnce } from 'three';
|
|
9
|
+
import { AnimationMixer, EventDispatcher, Timer, Vector3, LoopRepeat, LoopOnce } from 'three';
|
|
10
|
+
import { EngineEvents } from '../EngineEvents.js';
|
|
10
11
|
|
|
11
|
-
export class AnimationManager {
|
|
12
|
+
export class AnimationManager extends EventDispatcher {
|
|
12
13
|
|
|
13
14
|
constructor() {
|
|
14
15
|
|
|
16
|
+
super();
|
|
17
|
+
|
|
15
18
|
this.mixer = null;
|
|
16
19
|
this.timer = new Timer();
|
|
17
20
|
this.actions = [];
|
|
18
21
|
this.isPlaying = false;
|
|
19
22
|
|
|
20
|
-
this._scene = null;
|
|
21
|
-
this._mixerRoot = null;
|
|
23
|
+
this._scene = null; // scene root (for matrixWorld updates)
|
|
24
|
+
this._mixerRoot = null; // mixer target (GLTF model root for track resolution)
|
|
22
25
|
this._meshes = null;
|
|
23
26
|
this._meshTriRanges = null; // { start, count, uniqueVerts, indices }[]
|
|
24
|
-
this._posBuffer = null;
|
|
27
|
+
this._posBuffer = null; // Float32Array(triCount * 9) — reused each frame
|
|
25
28
|
this._tempVec = new Vector3();
|
|
26
|
-
this._skinnedCache = null;
|
|
29
|
+
this._skinnedCache = null; // per-mesh Float32Array for skinned vertex positions
|
|
27
30
|
this._totalTriangleCount = 0;
|
|
28
31
|
this._clipsCache = null;
|
|
29
32
|
this._savedTimeScale = 1;
|
|
30
33
|
this.onFinished = null; // callback when a non-looping clip ends
|
|
31
34
|
|
|
35
|
+
/** Injected by PathTracerApp — wakes the render loop after play/resume. */
|
|
36
|
+
this.wakeCallback = null;
|
|
37
|
+
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
/**
|
|
@@ -144,6 +150,8 @@ export class AnimationManager {
|
|
|
144
150
|
|
|
145
151
|
this.timer.reset();
|
|
146
152
|
this.isPlaying = true;
|
|
153
|
+
this.wakeCallback?.();
|
|
154
|
+
this.dispatchEvent( { type: EngineEvents.ANIMATION_STARTED } );
|
|
147
155
|
|
|
148
156
|
}
|
|
149
157
|
|
|
@@ -157,6 +165,7 @@ export class AnimationManager {
|
|
|
157
165
|
this.mixer.timeScale = 0;
|
|
158
166
|
this.timer.reset();
|
|
159
167
|
this.isPlaying = false;
|
|
168
|
+
this.dispatchEvent( { type: EngineEvents.ANIMATION_PAUSED } );
|
|
160
169
|
|
|
161
170
|
}
|
|
162
171
|
|
|
@@ -170,6 +179,8 @@ export class AnimationManager {
|
|
|
170
179
|
this.mixer.timeScale = this._savedTimeScale || 1;
|
|
171
180
|
this.timer.reset();
|
|
172
181
|
this.isPlaying = true;
|
|
182
|
+
this.wakeCallback?.();
|
|
183
|
+
this.dispatchEvent( { type: EngineEvents.ANIMATION_STARTED } );
|
|
173
184
|
|
|
174
185
|
}
|
|
175
186
|
|
|
@@ -184,6 +195,7 @@ export class AnimationManager {
|
|
|
184
195
|
this.mixer.timeScale = this._savedTimeScale || 1;
|
|
185
196
|
this.timer.reset();
|
|
186
197
|
this.isPlaying = false;
|
|
198
|
+
this.dispatchEvent( { type: EngineEvents.ANIMATION_STOPPED } );
|
|
187
199
|
|
|
188
200
|
}
|
|
189
201
|
|