rayzee 9.1.2 → 9.2.1
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 +176 -16
- package/dist/{NeuralRetouch-BRquqfmZ.js → NeuralRetouch-CYgkQY8Z.js} +1 -1
- package/dist/{NeuralSuperRes-CbNvQjCw.js → NeuralSuperRes-PkgDLQcD.js} +72 -40
- package/dist/{ToneMapGPU-DpUZDMbQ.js → ToneMapGPU-B6XIe3DU.js} +102 -92
- package/dist/rayzee.es.js +6209 -5571
- package/dist/rayzee.umd.js +137 -126
- package/package.json +3 -2
- package/src/AssetConfig.js +10 -5
- package/src/EngineEvents.js +41 -1
- package/src/EngineIssues.js +3 -0
- package/src/Headless.js +23 -7
- package/src/HeadlessCanvas.js +104 -0
- package/src/Passes/OIDNDenoiser.js +29 -4
- package/src/Passes/OIDNTemporalHistory.js +7 -0
- package/src/PathTracerApp.js +511 -136
- package/src/Platform.js +76 -0
- package/src/Processor/AssetLoader.js +66 -4
- package/src/Processor/BVHBuilder.js +2 -1
- package/src/Processor/EquirectHDRInfo.js +2 -1
- package/src/Processor/HostMemory.js +14 -0
- package/src/Processor/PBRT/PBRTSceneBuilder.js +26 -0
- package/src/Processor/ParallelBVHBuilder.js +7 -6
- package/src/Processor/PlatformImageLoader.js +118 -0
- package/src/Processor/ResampleRGBA8.js +102 -0
- package/src/Processor/SceneProcessor.js +7 -6
- package/src/Processor/TextureCreator.js +162 -13
- package/src/Processor/TextureReadback.js +26 -10
- package/src/Processor/ToneMapGPU.js +45 -15
- package/src/Processor/Workers/PackWorker.js +28 -0
- package/src/Stages/Compositor.js +25 -6
- package/src/Stages/PathTracer.js +142 -25
- package/src/Stages/PathTracerStage.js +6 -96
- package/src/Storage/DownloadCache.js +30 -7
- package/src/TSL/DebugKernel.js +1 -1
- package/src/TSL/GenerateKernel.js +2 -5
- package/src/TSL/Random.js +3 -120
- package/src/TSL/ShadeKernel.js +3 -3
- package/src/index.js +2 -1
- package/src/managers/CameraManager.js +2 -2
- package/src/managers/DenoisingManager.js +76 -7
- package/src/managers/GoboManager.js +8 -1
- package/src/managers/IESManager.js +13 -2
- package/src/managers/InteractionManager.js +1 -1
- package/src/managers/LightManager.js +31 -0
- package/src/managers/OverlayManager.js +11 -1
- package/src/managers/VideoRenderManager.js +8 -0
- package/src/node/NodeWorker.js +144 -0
- package/src/node/index.js +9 -0
- package/src/node/nodePlatform.js +86 -0
package/src/Platform.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the engine needs from its host that a browser provides by itself. A browser host configures
|
|
3
|
+
* nothing. A host without one (Node, Deno) supplies each service here rather than patching globals,
|
|
4
|
+
* so a new browser dependency shows up as a missing service instead of a ReferenceError.
|
|
5
|
+
*
|
|
6
|
+
* import { configurePlatform } from 'rayzee';
|
|
7
|
+
* import { nodePlatform } from 'rayzee/node';
|
|
8
|
+
* configurePlatform( nodePlatform( { decodeImage } ) ); // { Worker: NodeWorker, decodeImage }
|
|
9
|
+
*
|
|
10
|
+
* Call before constructing PathTracerApp.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const platform = {
|
|
14
|
+
/** A class with the Web Worker API. The engine's bundled workers start through it. */
|
|
15
|
+
Worker: null,
|
|
16
|
+
/**
|
|
17
|
+
* `( bytes: Uint8Array, mimeType: string ) => Promise<{ data: Uint8Array | Uint8ClampedArray, width, height }>`:
|
|
18
|
+
* an image a model carries (PNG, JPEG, WebP) to RGBA8 pixels, top row first. Where set, it decodes
|
|
19
|
+
* every glTF image in place of the browser.
|
|
20
|
+
*/
|
|
21
|
+
decodeImage: null,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** @param {Partial<typeof platform>} overrides - only the keys given are replaced */
|
|
25
|
+
export function configurePlatform( overrides ) {
|
|
26
|
+
|
|
27
|
+
if ( overrides ) Object.assign( platform, overrides );
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getPlatform() {
|
|
32
|
+
|
|
33
|
+
return platform;
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Logical cores, as the browser reports them; 4 where nothing does. */
|
|
38
|
+
export function hardwareThreads() {
|
|
39
|
+
|
|
40
|
+
return ( typeof navigator !== 'undefined' && navigator.hardwareConcurrency ) || 4;
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether a worker can be started at all: the browser's or the host's. */
|
|
45
|
+
export function hasWorkers() {
|
|
46
|
+
|
|
47
|
+
return !! platform.Worker || typeof Worker !== 'undefined';
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Starts one of the engine's bundled workers. Their bundled constructors call the global `Worker`
|
|
53
|
+
* themselves, so the host's class stands in for it while one is constructed.
|
|
54
|
+
* @param {new ( options?: Object ) => Worker} WorkerClass
|
|
55
|
+
*/
|
|
56
|
+
export function createWorker( WorkerClass, options ) {
|
|
57
|
+
|
|
58
|
+
const Host = platform.Worker;
|
|
59
|
+
if ( ! Host ) return new WorkerClass( options );
|
|
60
|
+
|
|
61
|
+
const had = Object.prototype.hasOwnProperty.call( globalThis, 'Worker' );
|
|
62
|
+
const previous = globalThis.Worker;
|
|
63
|
+
globalThis.Worker = Host;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
|
|
67
|
+
return new WorkerClass( options );
|
|
68
|
+
|
|
69
|
+
} finally {
|
|
70
|
+
|
|
71
|
+
if ( had ) globalThis.Worker = previous;
|
|
72
|
+
else delete globalThis.Worker;
|
|
73
|
+
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
}
|
|
@@ -25,6 +25,8 @@ import { VERSION } from '../version.js';
|
|
|
25
25
|
import { disposeEngineOwnedResources, disposeObjectFromMemory, updateLoading } from './utils';
|
|
26
26
|
import { BuildTimer } from './BuildTimer.js';
|
|
27
27
|
import { getAssetConfig } from '../AssetConfig.js';
|
|
28
|
+
import { getPlatform } from '../Platform.js';
|
|
29
|
+
import { loadPlatformImage, platformImagesPlugin } from './PlatformImageLoader.js';
|
|
28
30
|
import { loadPBRTScene, pickEntryPath, VirtualFS, PBRT_BUILD_REVISION } from './PBRT/index.js';
|
|
29
31
|
import { extractSceneMetadata } from './SceneMetadata.js';
|
|
30
32
|
import { ISSUE_CODES, ISSUE_SEVERITY } from '../EngineIssues.js';
|
|
@@ -627,6 +629,11 @@ export class AssetLoader extends EventDispatcher {
|
|
|
627
629
|
: ( this.loaderCache.exr || ( this.loaderCache.exr = new EXRLoader( this._loadingManager ).setDataType( FloatType ) ) );
|
|
628
630
|
texture = await loader.loadAsync( source.url, onProgress );
|
|
629
631
|
|
|
632
|
+
} else if ( getPlatform().decodeImage ) {
|
|
633
|
+
|
|
634
|
+
texture = await loadPlatformImage( source.url, `image/${extension === 'jpg' ? 'jpeg' : extension}` );
|
|
635
|
+
texture.colorSpace = SRGBColorSpace;
|
|
636
|
+
|
|
630
637
|
} else {
|
|
631
638
|
|
|
632
639
|
if ( ! this.loaderCache.texture ) this.loaderCache.texture = new TextureLoader( this._loadingManager );
|
|
@@ -1578,6 +1585,17 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1578
1585
|
loader.parse( gltfContent, '',
|
|
1579
1586
|
gltf => {
|
|
1580
1587
|
|
|
1588
|
+
try {
|
|
1589
|
+
|
|
1590
|
+
this._throwDeferred();
|
|
1591
|
+
|
|
1592
|
+
} catch ( error ) {
|
|
1593
|
+
|
|
1594
|
+
reject( error );
|
|
1595
|
+
return;
|
|
1596
|
+
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1581
1599
|
this.releaseTargetModel();
|
|
1582
1600
|
this.targetModel = gltf.scene;
|
|
1583
1601
|
this.sceneMetadata = extractSceneMetadata( gltf );
|
|
@@ -1889,11 +1907,40 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1889
1907
|
loader.setDRACOLoader( dracoLoader );
|
|
1890
1908
|
loader.setKTX2Loader( ktx2Loader );
|
|
1891
1909
|
loader.setMeshoptDecoder( MeshoptDecoder );
|
|
1910
|
+
if ( getPlatform().decodeImage ) loader.register( ( parser ) => platformImagesPlugin( parser, ( where, error ) => this._reportImageFailure( where, error ) ) );
|
|
1892
1911
|
|
|
1893
1912
|
return loader;
|
|
1894
1913
|
|
|
1895
1914
|
}
|
|
1896
1915
|
|
|
1916
|
+
// GLTFLoader swallows the failure and loads the model untextured, so a strict host's throw is kept
|
|
1917
|
+
// for the end of the load (_throwDeferred).
|
|
1918
|
+
_reportImageFailure( where, error ) {
|
|
1919
|
+
|
|
1920
|
+
try {
|
|
1921
|
+
|
|
1922
|
+
this._issues?.record(
|
|
1923
|
+
ISSUE_CODES.TEXTURE_BUILD_FAILED,
|
|
1924
|
+
`image ${where} could not be decoded — the surfaces using it render untextured`,
|
|
1925
|
+
{ image: where, cause: String( error?.message ?? error ) }
|
|
1926
|
+
);
|
|
1927
|
+
|
|
1928
|
+
} catch ( strictError ) {
|
|
1929
|
+
|
|
1930
|
+
this._deferredError ??= strictError;
|
|
1931
|
+
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
_throwDeferred() {
|
|
1937
|
+
|
|
1938
|
+
const error = this._deferredError;
|
|
1939
|
+
this._deferredError = null;
|
|
1940
|
+
if ( error ) throw error;
|
|
1941
|
+
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1897
1944
|
_disposeGLTFLoader( loader ) {
|
|
1898
1945
|
|
|
1899
1946
|
if ( ! loader ) return;
|
|
@@ -1934,6 +1981,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1934
1981
|
if ( source.cached ) loader.setResourcePath( LoaderUtils.extractUrlBase( modelUrl ) );
|
|
1935
1982
|
const data = await loader.loadAsync( source.url, source.cached ? undefined : this._downloadProgress( "Downloading Model...", cancelable ) );
|
|
1936
1983
|
this._downloadComplete();
|
|
1984
|
+
this._throwDeferred();
|
|
1937
1985
|
|
|
1938
1986
|
this.releaseTargetModel();
|
|
1939
1987
|
this._sourceKey = cancelable ? this._keyed( 'url', options.cacheKey ?? modelUrl ) : null;
|
|
@@ -1996,6 +2044,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1996
2044
|
if ( source.cached ) loader.setResourcePath( LoaderUtils.extractUrlBase( url ) );
|
|
1997
2045
|
const data = await loader.loadAsync( source.url, source.cached ? undefined : this._downloadProgress( "Downloading Model...", cancelable ) );
|
|
1998
2046
|
this._downloadComplete();
|
|
2047
|
+
this._throwDeferred();
|
|
1999
2048
|
this._appended = true;
|
|
2000
2049
|
this._processAndParent( data.scene );
|
|
2001
2050
|
return { root: data.scene, animations: data.animations || [] };
|
|
@@ -2055,6 +2104,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
2055
2104
|
await new Promise( r => setTimeout( r, 0 ) );
|
|
2056
2105
|
|
|
2057
2106
|
const data = await loader.parseAsync( arrayBuffer, '' );
|
|
2107
|
+
this._throwDeferred();
|
|
2058
2108
|
|
|
2059
2109
|
this.releaseTargetModel();
|
|
2060
2110
|
|
|
@@ -2520,6 +2570,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
2520
2570
|
// Split after the walk: traverse() caches children.length, so splitting in place
|
|
2521
2571
|
// shifts later siblings down a slot and skips one.
|
|
2522
2572
|
const multiMaterialMeshes = [];
|
|
2573
|
+
const skippedPlaceholders = [];
|
|
2523
2574
|
model.traverse( ( object ) => {
|
|
2524
2575
|
|
|
2525
2576
|
const userData = object.userData;
|
|
@@ -2551,11 +2602,12 @@ export class AssetLoader extends EventDispatcher {
|
|
|
2551
2602
|
|
|
2552
2603
|
}
|
|
2553
2604
|
|
|
2605
|
+
const placeholder = object.name.startsWith( 'RectAreaLightPlaceholder' );
|
|
2606
|
+
if ( placeholder && ! ( userData.name && userData.type === 'RectAreaLight' )
|
|
2607
|
+
&& ! object.parent?.name?.startsWith( 'RectAreaLightPlaceholder' ) ) skippedPlaceholders.push( object.name );
|
|
2608
|
+
|
|
2554
2609
|
// Process ceiling lights
|
|
2555
|
-
if (
|
|
2556
|
-
userData.name
|
|
2557
|
-
// && userData.name.includes( "ceilingLight" )
|
|
2558
|
-
) {
|
|
2610
|
+
if ( placeholder && userData.name ) {
|
|
2559
2611
|
|
|
2560
2612
|
if ( userData.type === 'RectAreaLight' ) {
|
|
2561
2613
|
|
|
@@ -2588,6 +2640,16 @@ export class AssetLoader extends EventDispatcher {
|
|
|
2588
2640
|
|
|
2589
2641
|
} );
|
|
2590
2642
|
|
|
2643
|
+
if ( skippedPlaceholders.length ) {
|
|
2644
|
+
|
|
2645
|
+
this._issues?.record(
|
|
2646
|
+
ISSUE_CODES.LIGHT_PLACEHOLDER_SKIPPED,
|
|
2647
|
+
`${skippedPlaceholders.length} area-light placeholder(s) lack userData.name or userData.type 'RectAreaLight' — no light was made for them`,
|
|
2648
|
+
{ count: skippedPlaceholders.length, names: skippedPlaceholders.slice( 0, 10 ) }
|
|
2649
|
+
);
|
|
2650
|
+
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2591
2653
|
const shared = model.userData.__rayzeeExternal === true;
|
|
2592
2654
|
|
|
2593
2655
|
for ( const object of multiMaterialMeshes ) {
|
|
@@ -3,6 +3,7 @@ import { ReinsertionOptimizer } from './ReinsertionOptimizer.js';
|
|
|
3
3
|
// Logger is worker-safe (globalThis only, storage access guarded), unlike Constants.js below.
|
|
4
4
|
import { createLogger, fmt } from '../utils/Logger.js';
|
|
5
5
|
import { foldLeaves } from './BVHLeafFold.js';
|
|
6
|
+
import { hasWorkers } from '../Platform.js';
|
|
6
7
|
|
|
7
8
|
const log = createLogger( 'bvh' );
|
|
8
9
|
|
|
@@ -423,7 +424,7 @@ export class BVHBuilder {
|
|
|
423
424
|
this.processedTriangles = 0;
|
|
424
425
|
this.lastProgressUpdate = performance.now();
|
|
425
426
|
|
|
426
|
-
if ( this.useWorker && createBVHWorker &&
|
|
427
|
+
if ( this.useWorker && createBVHWorker && hasWorkers() ) {
|
|
427
428
|
|
|
428
429
|
return new Promise( ( resolve, reject ) => {
|
|
429
430
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DataUtils, HalfFloatType, FloatType, SRGBColorSpace } from 'three';
|
|
2
2
|
import CDFWorker from './Workers/CDFWorker.js?worker&inline';
|
|
3
|
+
import { createWorker } from '../Platform.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Binary search to find the closest index
|
|
@@ -222,7 +223,7 @@ export class EquirectHDRInfo {
|
|
|
222
223
|
const { floatData, width, height } = extractFloatData( hdr );
|
|
223
224
|
|
|
224
225
|
// Fresh worker per call — terminated in finally to avoid ~30 MB residency.
|
|
225
|
-
this._worker =
|
|
226
|
+
this._worker = createWorker( CDFWorker );
|
|
226
227
|
|
|
227
228
|
try {
|
|
228
229
|
|
|
@@ -59,6 +59,20 @@ export const SAFE_SCENE_BYTES = 7040 * 1024 * 1024;
|
|
|
59
59
|
*/
|
|
60
60
|
export const MAX_SCENE_BYTES = 9216 * 1024 * 1024;
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The host's memory in GB: the host's own figure when it gives one, else Chrome's
|
|
64
|
+
* `navigator.deviceMemory`, else an assumed 4 (every other runtime lacks it).
|
|
65
|
+
* @param {number} [stated]
|
|
66
|
+
* @returns {{gb: number, assumed: boolean}}
|
|
67
|
+
*/
|
|
68
|
+
export function deviceMemoryGB( stated ) {
|
|
69
|
+
|
|
70
|
+
if ( stated > 0 ) return { gb: stated, assumed: false };
|
|
71
|
+
const reported = typeof navigator !== 'undefined' ? navigator.deviceMemory : undefined;
|
|
72
|
+
return reported > 0 ? { gb: reported, assumed: false } : { gb: 4, assumed: true };
|
|
73
|
+
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
/** Headroom a single build step wants over its own size before it is called safe. */
|
|
63
77
|
export const PREFLIGHT_SAFETY = 1.15;
|
|
64
78
|
|
|
@@ -211,6 +211,18 @@ export class PBRTSceneBuilder {
|
|
|
211
211
|
// Built again for their moving placements after the static ones, so never freed early.
|
|
212
212
|
this._keepShapes = new Set();
|
|
213
213
|
for ( const { name } of ir.animatedInstances ?? [] ) for ( const shape of ir.objects.get( name ) ?? [] ) this._keepShapes.add( shape );
|
|
214
|
+
// One decoded .ply serves every shape naming it: a merge frees it only as its last direct
|
|
215
|
+
// user, and never while a template or an unmerged shape holds its geometry.
|
|
216
|
+
this._plyUsers = new Map();
|
|
217
|
+
this._plyHeld = new Set();
|
|
218
|
+
for ( const shape of ir.shapes ) if ( shape?.type === 'plymesh' ) {
|
|
219
|
+
|
|
220
|
+
const file = pString( shape.params, 'filename', null );
|
|
221
|
+
this._plyUsers.set( file, ( this._plyUsers.get( file ) ?? 0 ) + 1 );
|
|
222
|
+
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for ( const template of ir.objects.values() ) for ( const shape of template ) if ( shape.type === 'plymesh' ) this._plyHeld.add( pString( shape.params, 'filename', null ) );
|
|
214
226
|
this._recoveredColors = 0;
|
|
215
227
|
this.reportedMeshes = 0;
|
|
216
228
|
this.triangleCount = 0; // stored triangles — a shared geometry counts once
|
|
@@ -252,6 +264,8 @@ export class PBRTSceneBuilder {
|
|
|
252
264
|
|
|
253
265
|
}
|
|
254
266
|
|
|
267
|
+
if ( shape.type === 'plymesh' ) this._plyHeld.add( pString( shape.params, 'filename', null ) );
|
|
268
|
+
|
|
255
269
|
const mesh = this._meshFromGeometry( shape, geometry, sharedMaterial, shape.ctm, `shape_${i}` );
|
|
256
270
|
if ( ! mesh ) continue;
|
|
257
271
|
group.add( mesh );
|
|
@@ -759,12 +773,24 @@ export class PBRTSceneBuilder {
|
|
|
759
773
|
_releaseMerged( shape, geometry ) {
|
|
760
774
|
|
|
761
775
|
if ( this._keepShapes?.has( shape ) ) return;
|
|
776
|
+
if ( shape.type === 'plymesh' && ! this._lastPlyUse( pString( shape.params, 'filename', null ) ) ) return;
|
|
762
777
|
for ( const name in geometry.attributes ) freeNow( geometry.attributes[ name ].array );
|
|
763
778
|
freeNow( geometry.index?.array );
|
|
764
779
|
for ( const name in shape.params ) if ( ArrayBuffer.isView( shape.params[ name ]?.value ) ) freeNow( shape.params[ name ].value );
|
|
765
780
|
|
|
766
781
|
}
|
|
767
782
|
|
|
783
|
+
/** Whether this merge was the last use of a .ply, which then leaves the cache. @private */
|
|
784
|
+
_lastPlyUse( file ) {
|
|
785
|
+
|
|
786
|
+
const left = ( this._plyUsers.get( file ) ?? 1 ) - 1;
|
|
787
|
+
this._plyUsers.set( file, left );
|
|
788
|
+
if ( left > 0 || this._plyHeld.has( file ) ) return false;
|
|
789
|
+
this._plyCache.delete( file );
|
|
790
|
+
return true;
|
|
791
|
+
|
|
792
|
+
}
|
|
793
|
+
|
|
768
794
|
/**
|
|
769
795
|
* Copy one shape's triangles, moved by `transform`, into the batch for its material.
|
|
770
796
|
* @returns {object|null} the batch, taken out of `batches`, once it is full
|
|
@@ -11,8 +11,9 @@ import BVHWorker from './Workers/BVHWorker.js?worker&inline';
|
|
|
11
11
|
import BVHSubtreeWorker from './Workers/BVHSubtreeWorker.js?worker&inline';
|
|
12
12
|
import { setBVHWorkerFactory } from './BVHBuilder.js';
|
|
13
13
|
import { createLogger, fmt } from '../utils/Logger.js';
|
|
14
|
+
import { createWorker, hardwareThreads, hasWorkers } from '../Platform.js';
|
|
14
15
|
|
|
15
|
-
setBVHWorkerFactory( () =>
|
|
16
|
+
setBVHWorkerFactory( () => createWorker( BVHWorker ) );
|
|
16
17
|
|
|
17
18
|
const log = createLogger( 'bvh' );
|
|
18
19
|
|
|
@@ -38,7 +39,7 @@ const MAX_PARALLEL_WORKERS = 8;
|
|
|
38
39
|
export function buildBVHParallel( triangles, depth, progressCallback, config ) {
|
|
39
40
|
|
|
40
41
|
const triangleCount = triangles.byteLength / ( FPT * 4 );
|
|
41
|
-
const numWorkers = Math.min(
|
|
42
|
+
const numWorkers = Math.min( hardwareThreads(), MAX_PARALLEL_WORKERS );
|
|
42
43
|
const parallelDepth = Math.ceil( Math.log2( numWorkers * 2.5 + 1 ) );
|
|
43
44
|
|
|
44
45
|
log.debug( `parallel build ${fmt.n( triangleCount )} triangles · ${numWorkers} workers · parallelDepth ${parallelDepth}` );
|
|
@@ -63,7 +64,7 @@ export function buildBVHParallel( triangles, depth, progressCallback, config ) {
|
|
|
63
64
|
const sharedReorderBuffer = null;
|
|
64
65
|
|
|
65
66
|
// Phase 1: Coordinator worker
|
|
66
|
-
const coordinatorWorker =
|
|
67
|
+
const coordinatorWorker = createWorker( BVHWorker );
|
|
67
68
|
|
|
68
69
|
let phase1Stats = null;
|
|
69
70
|
const allWorkers = [ coordinatorWorker ];
|
|
@@ -326,7 +327,7 @@ async function handlePhase2(
|
|
|
326
327
|
const bucket = workerTaskBuckets[ w ];
|
|
327
328
|
if ( bucket.length === 0 ) continue;
|
|
328
329
|
|
|
329
|
-
const subtreeWorker =
|
|
330
|
+
const subtreeWorker = createWorker( BVHSubtreeWorker );
|
|
330
331
|
|
|
331
332
|
allWorkers.push( subtreeWorker );
|
|
332
333
|
|
|
@@ -431,7 +432,7 @@ function buildSingleWorker( triangles, depth, progressCallback, config ) {
|
|
|
431
432
|
|
|
432
433
|
( async () => {
|
|
433
434
|
|
|
434
|
-
const worker =
|
|
435
|
+
const worker = createWorker( BVHWorker );
|
|
435
436
|
|
|
436
437
|
const triangleCount = triangles.byteLength / ( FPT * 4 );
|
|
437
438
|
const useShared = typeof SharedArrayBuffer !== 'undefined';
|
|
@@ -506,7 +507,7 @@ function buildSingleWorker( triangles, depth, progressCallback, config ) {
|
|
|
506
507
|
*/
|
|
507
508
|
export function shouldUseParallelBuild( triangleCount ) {
|
|
508
509
|
|
|
509
|
-
return
|
|
510
|
+
return hasWorkers()
|
|
510
511
|
&& typeof SharedArrayBuffer !== 'undefined'
|
|
511
512
|
&& triangleCount >= PARALLEL_THRESHOLD;
|
|
512
513
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { DataTexture, LoaderUtils, RGBAFormat, UnsignedByteType } from 'three';
|
|
2
|
+
import { getPlatform } from '../Platform.js';
|
|
3
|
+
|
|
4
|
+
const MIME_BY_EXTENSION = [[ /\.jpe?g(\?|#|$)/i, 'image/jpeg' ], [ /\.webp(\?|#|$)/i, 'image/webp' ], [ /\.avif(\?|#|$)/i, 'image/avif' ]];
|
|
5
|
+
|
|
6
|
+
function mimeTypeOf( def ) {
|
|
7
|
+
|
|
8
|
+
if ( def.mimeType ) return def.mimeType;
|
|
9
|
+
if ( def.uri?.startsWith( 'data:' ) ) return def.uri.slice( 5, def.uri.indexOf( ';' ) );
|
|
10
|
+
return MIME_BY_EXTENSION.find( ( [ pattern ] ) => pattern.test( def.uri ?? '' ) )?.[ 1 ] ?? 'image/png';
|
|
11
|
+
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Browsers ignore bytes after IEND and exporters leave them there; strict decoders reject the file.
|
|
15
|
+
function trimPNG( bytes ) {
|
|
16
|
+
|
|
17
|
+
for ( let i = 8; i + 12 <= bytes.length; ) {
|
|
18
|
+
|
|
19
|
+
const length = ( ( bytes[ i ] << 24 ) | ( bytes[ i + 1 ] << 16 ) | ( bytes[ i + 2 ] << 8 ) | bytes[ i + 3 ] ) >>> 0;
|
|
20
|
+
const end = i + 12 + length;
|
|
21
|
+
if ( bytes[ i + 4 ] === 0x49 && bytes[ i + 5 ] === 0x45 && bytes[ i + 6 ] === 0x4E && bytes[ i + 7 ] === 0x44 ) return bytes.subarray( 0, end );
|
|
22
|
+
i = end;
|
|
23
|
+
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return bytes;
|
|
27
|
+
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// A model's images go to the texture pack worker, which reads a SharedArrayBuffer where they lie.
|
|
31
|
+
function pixelTexture( { data, width, height }, { shared = false } = {} ) {
|
|
32
|
+
|
|
33
|
+
const view = new Uint8Array( data.buffer, data.byteOffset, width * height * 4 );
|
|
34
|
+
let pixels = view;
|
|
35
|
+
if ( shared && typeof SharedArrayBuffer !== 'undefined' && ! ( view.buffer instanceof SharedArrayBuffer ) ) {
|
|
36
|
+
|
|
37
|
+
pixels = new Uint8Array( new SharedArrayBuffer( view.byteLength ) );
|
|
38
|
+
pixels.set( view );
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const texture = new DataTexture( pixels, width, height, RGBAFormat, UnsignedByteType );
|
|
43
|
+
texture.needsUpdate = true;
|
|
44
|
+
return texture;
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function decodeBytes( bytes, mimeType ) {
|
|
49
|
+
|
|
50
|
+
return getPlatform().decodeImage( mimeType === 'image/png' ? trimPNG( bytes ) : bytes, mimeType );
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function fetchBytes( url ) {
|
|
55
|
+
|
|
56
|
+
const response = await fetch( url );
|
|
57
|
+
if ( ! response.ok ) throw new Error( `HTTP ${response.status}` );
|
|
58
|
+
return new Uint8Array( await response.arrayBuffer() );
|
|
59
|
+
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* An image file as a texture, decoded by the platform: the counterpart of TextureLoader for hosts
|
|
64
|
+
* with no DOM. Rows stay top first with `flipY` on, exactly as TextureLoader leaves an image.
|
|
65
|
+
*/
|
|
66
|
+
export async function loadPlatformImage( url, mimeType = mimeTypeOf( { uri: url } ) ) {
|
|
67
|
+
|
|
68
|
+
const texture = pixelTexture( await decodeBytes( await fetchBytes( url ), mimeType ) );
|
|
69
|
+
texture.flipY = true;
|
|
70
|
+
return texture;
|
|
71
|
+
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A GLTFLoader plugin for hosts with the platform's `decodeImage`: every image of the model, embedded
|
|
76
|
+
* or not, is decoded by the host and becomes a DataTexture of its pixels, which the texture packer
|
|
77
|
+
* copies without a canvas. three.js would otherwise need the DOM or createImageBitmap, and `self.URL`
|
|
78
|
+
* for the blob URL it makes of an embedded image.
|
|
79
|
+
*
|
|
80
|
+
* GLTFLoader turns a texture that fails into no texture at all, silently; `onFailure( where, error )`
|
|
81
|
+
* is how that is heard.
|
|
82
|
+
*/
|
|
83
|
+
export function platformImagesPlugin( parser, onFailure = null ) {
|
|
84
|
+
|
|
85
|
+
parser.loadImageSource = function ( sourceIndex ) {
|
|
86
|
+
|
|
87
|
+
if ( this.sourceCache[ sourceIndex ] !== undefined ) return this.sourceCache[ sourceIndex ].then( ( texture ) => texture.clone() );
|
|
88
|
+
|
|
89
|
+
const def = this.json.images[ sourceIndex ];
|
|
90
|
+
const mimeType = mimeTypeOf( def );
|
|
91
|
+
const where = def.uri ?? `bufferView ${def.bufferView}`;
|
|
92
|
+
|
|
93
|
+
const bytes = def.bufferView !== undefined
|
|
94
|
+
? this.getDependency( 'bufferView', def.bufferView ).then( ( buffer ) => new Uint8Array( buffer ) )
|
|
95
|
+
: fetchBytes( this.options.manager.resolveURL( LoaderUtils.resolveURL( def.uri, this.options.path ) ) );
|
|
96
|
+
|
|
97
|
+
const promise = bytes.then( ( data ) => decodeBytes( data, mimeType ) ).then( ( pixels ) => {
|
|
98
|
+
|
|
99
|
+
const texture = pixelTexture( pixels, { shared: true } );
|
|
100
|
+
if ( def.extras && typeof def.extras === 'object' ) Object.assign( texture.userData, def.extras );
|
|
101
|
+
texture.userData.mimeType = mimeType;
|
|
102
|
+
return texture;
|
|
103
|
+
|
|
104
|
+
} ).catch( ( error ) => {
|
|
105
|
+
|
|
106
|
+
onFailure?.( where, error );
|
|
107
|
+
throw error;
|
|
108
|
+
|
|
109
|
+
} );
|
|
110
|
+
|
|
111
|
+
this.sourceCache[ sourceIndex ] = promise;
|
|
112
|
+
return promise;
|
|
113
|
+
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return { name: 'RAYZEE_platform_images' };
|
|
117
|
+
|
|
118
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One RGBA8 layer into its slot of a packed array: bilinear with texel centres aligned, a plain copy
|
|
3
|
+
* when the sizes match. flipY puts the last row first. Shared by the main thread and PackWorker.
|
|
4
|
+
*/
|
|
5
|
+
export function resampleRGBA8( { data: src, width: sw, height: sh, flipY }, dst, offset, dw, dh ) {
|
|
6
|
+
|
|
7
|
+
const rowBytes = dw * 4;
|
|
8
|
+
|
|
9
|
+
if ( sw === dw && sh === dh ) {
|
|
10
|
+
|
|
11
|
+
if ( ! flipY ) {
|
|
12
|
+
|
|
13
|
+
dst.set( src.subarray( 0, dh * rowBytes ), offset );
|
|
14
|
+
return;
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
for ( let y = 0; y < dh; y ++ ) dst.set( src.subarray( y * rowBytes, ( y + 1 ) * rowBytes ), offset + ( dh - 1 - y ) * rowBytes );
|
|
19
|
+
return;
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Every row samples the same columns. Enlarged, neighbouring rows also share source rows, so each
|
|
24
|
+
// source row is blended across once, and the slot holding the other row of a pair is never evicted.
|
|
25
|
+
const left = new Int32Array( dw ), right = new Int32Array( dw ), weight = new Float64Array( dw );
|
|
26
|
+
|
|
27
|
+
for ( let x = 0; x < dw; x ++ ) {
|
|
28
|
+
|
|
29
|
+
const fx = Math.min( Math.max( ( x + 0.5 ) * sw / dw - 0.5, 0 ), sw - 1 );
|
|
30
|
+
const x0 = Math.floor( fx );
|
|
31
|
+
left[ x ] = x0 * 4;
|
|
32
|
+
right[ x ] = Math.min( x0 + 1, sw - 1 ) * 4;
|
|
33
|
+
weight[ x ] = fx - x0;
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if ( dh <= sh ) {
|
|
38
|
+
|
|
39
|
+
for ( let y = 0; y < dh; y ++ ) {
|
|
40
|
+
|
|
41
|
+
const fy = Math.min( Math.max( ( y + 0.5 ) * sh / dh - 0.5, 0 ), sh - 1 );
|
|
42
|
+
const y0 = Math.floor( fy ), y1 = Math.min( y0 + 1, sh - 1 ), ty = fy - y0;
|
|
43
|
+
const top = y0 * sw * 4, bottom = y1 * sw * 4;
|
|
44
|
+
let out = offset + ( flipY ? dh - 1 - y : y ) * rowBytes;
|
|
45
|
+
|
|
46
|
+
for ( let x = 0; x < dw; x ++, out += 4 ) {
|
|
47
|
+
|
|
48
|
+
const a = top + left[ x ], b = top + right[ x ], c = bottom + left[ x ], d = bottom + right[ x ], tx = weight[ x ];
|
|
49
|
+
|
|
50
|
+
for ( let ch = 0; ch < 4; ch ++ ) {
|
|
51
|
+
|
|
52
|
+
const t = src[ a + ch ] + ( src[ b + ch ] - src[ a + ch ] ) * tx;
|
|
53
|
+
const u = src[ c + ch ] + ( src[ d + ch ] - src[ c + ch ] ) * tx;
|
|
54
|
+
dst[ out + ch ] = Math.round( t + ( u - t ) * ty );
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return;
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const slots = [ { row: - 1, data: new Float64Array( rowBytes ) }, { row: - 1, data: new Float64Array( rowBytes ) } ];
|
|
67
|
+
|
|
68
|
+
const across = ( sy, keep ) => {
|
|
69
|
+
|
|
70
|
+
for ( const slot of slots ) if ( slot.row === sy ) return slot.data;
|
|
71
|
+
|
|
72
|
+
const slot = slots[ 0 ].row === keep ? slots[ 1 ] : slots[ 0 ];
|
|
73
|
+
const base = sy * sw * 4, row = slot.data;
|
|
74
|
+
|
|
75
|
+
for ( let x = 0, o = 0; x < dw; x ++, o += 4 ) {
|
|
76
|
+
|
|
77
|
+
const a = base + left[ x ], b = base + right[ x ], tx = weight[ x ];
|
|
78
|
+
row[ o ] = src[ a ] + ( src[ b ] - src[ a ] ) * tx;
|
|
79
|
+
row[ o + 1 ] = src[ a + 1 ] + ( src[ b + 1 ] - src[ a + 1 ] ) * tx;
|
|
80
|
+
row[ o + 2 ] = src[ a + 2 ] + ( src[ b + 2 ] - src[ a + 2 ] ) * tx;
|
|
81
|
+
row[ o + 3 ] = src[ a + 3 ] + ( src[ b + 3 ] - src[ a + 3 ] ) * tx;
|
|
82
|
+
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
slot.row = sy;
|
|
86
|
+
return row;
|
|
87
|
+
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
for ( let y = 0; y < dh; y ++ ) {
|
|
91
|
+
|
|
92
|
+
const fy = Math.min( Math.max( ( y + 0.5 ) * sh / dh - 0.5, 0 ), sh - 1 );
|
|
93
|
+
const y0 = Math.floor( fy ), y1 = Math.min( y0 + 1, sh - 1 ), ty = fy - y0;
|
|
94
|
+
const top = across( y0, y1 );
|
|
95
|
+
const bottom = y1 === y0 ? top : across( y1, y0 );
|
|
96
|
+
const out = offset + ( flipY ? dh - 1 - y : y ) * rowBytes;
|
|
97
|
+
|
|
98
|
+
for ( let i = 0; i < rowBytes; i ++ ) dst[ out + i ] = Math.round( top[ i ] + ( bottom[ i ] - top[ i ] ) * ty );
|
|
99
|
+
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
}
|
|
@@ -32,6 +32,7 @@ import { SCENE_CACHE_MIN_BUILD_MS } from '../Storage/sceneCachePolicy.js';
|
|
|
32
32
|
import BVHWorker from './Workers/BVHWorker.js?worker&inline';
|
|
33
33
|
import BVHRefitWorker from './Workers/BVHRefitWorker.js?worker&inline';
|
|
34
34
|
import TLASWorker from './Workers/TLASWorker.js?worker&inline';
|
|
35
|
+
import { createWorker, hardwareThreads } from '../Platform.js';
|
|
35
36
|
|
|
36
37
|
// Under this the TLAS build is a few ms; the worker round trip would cost more than it saves.
|
|
37
38
|
const TLAS_WORKER_MIN_ENTRIES = 50_000;
|
|
@@ -113,7 +114,7 @@ export class SceneProcessor {
|
|
|
113
114
|
textureQuality: 'adaptive', // 'low', 'medium', 'high', 'adaptive'
|
|
114
115
|
maxTextureSize: TEXTURE_CONSTANTS.DEFAULT_MAX_TEXTURE_SIZE, // longest-edge cap for material textures
|
|
115
116
|
enableTextureCache: true,
|
|
116
|
-
maxConcurrentTextureTasks: Math.min(
|
|
117
|
+
maxConcurrentTextureTasks: Math.min( hardwareThreads(), 6 ),
|
|
117
118
|
maxSceneBytes: MAX_SCENE_BYTES,
|
|
118
119
|
// Treelet optimization configuration
|
|
119
120
|
// Keep: `_buildBVH` sends `enabled: value !== false`, so undefined re-enables treelets.
|
|
@@ -1294,7 +1295,7 @@ export class SceneProcessor {
|
|
|
1294
1295
|
|
|
1295
1296
|
const tri = this.triangles;
|
|
1296
1297
|
const { order, pieces, tree } = await partitionRange( tri, range.start, range.count, SPLIT_PIECE_TRIANGLES, { pause: () => new Promise( r => setTimeout( r, 0 ) ) } );
|
|
1297
|
-
const pool = this._blasPool( workerOpts, null, null, Math.min(
|
|
1298
|
+
const pool = this._blasPool( workerOpts, null, null, Math.min( hardwareThreads(), 8 ) );
|
|
1298
1299
|
pieces.forEach( ( p, k ) => pool.submit( { m: k, range: { start: range.start + p.start, count: p.count } } ) );
|
|
1299
1300
|
|
|
1300
1301
|
const built = [];
|
|
@@ -1844,7 +1845,7 @@ export class SceneProcessor {
|
|
|
1844
1845
|
|
|
1845
1846
|
const spawn = () => {
|
|
1846
1847
|
|
|
1847
|
-
const worker =
|
|
1848
|
+
const worker = createWorker( BVHWorker );
|
|
1848
1849
|
worker.onmessage = ( e ) => onWorkerMessage( worker, e );
|
|
1849
1850
|
worker.onerror = ( err ) => fail( err );
|
|
1850
1851
|
workers.push( worker );
|
|
@@ -2611,7 +2612,7 @@ export class SceneProcessor {
|
|
|
2611
2612
|
// Lazy-create worker
|
|
2612
2613
|
if ( ! this._refitWorker ) {
|
|
2613
2614
|
|
|
2614
|
-
this._refitWorker =
|
|
2615
|
+
this._refitWorker = createWorker( BVHRefitWorker );
|
|
2615
2616
|
|
|
2616
2617
|
}
|
|
2617
2618
|
|
|
@@ -3319,7 +3320,7 @@ export class SceneProcessor {
|
|
|
3319
3320
|
/** @private */
|
|
3320
3321
|
_runTLASWorker( aabbs, count ) {
|
|
3321
3322
|
|
|
3322
|
-
if ( ! this._tlasWorker ) this._tlasWorker =
|
|
3323
|
+
if ( ! this._tlasWorker ) this._tlasWorker = createWorker( TLASWorker );
|
|
3323
3324
|
const worker = this._tlasWorker;
|
|
3324
3325
|
|
|
3325
3326
|
return new Promise( ( resolve, reject ) => {
|
|
@@ -3705,7 +3706,7 @@ export class SceneProcessor {
|
|
|
3705
3706
|
const existing = this._pendingRebuilds.get( placement );
|
|
3706
3707
|
if ( existing ) existing.terminate();
|
|
3707
3708
|
|
|
3708
|
-
dispatchRebuild( placement, entry,
|
|
3709
|
+
dispatchRebuild( placement, entry, createWorker( BVHWorker ) );
|
|
3709
3710
|
|
|
3710
3711
|
}
|
|
3711
3712
|
|