rayzee 9.1.1 → 9.2.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.
Files changed (48) hide show
  1. package/README.md +174 -15
  2. package/dist/{NeuralRetouch-BRquqfmZ.js → NeuralRetouch-DgnfldWO.js} +1 -1
  3. package/dist/{NeuralSuperRes-CbNvQjCw.js → NeuralSuperRes-Baa2w8VU.js} +72 -40
  4. package/dist/{ToneMapGPU-DpUZDMbQ.js → ToneMapGPU-CNgexVto.js} +75 -71
  5. package/dist/rayzee.es.js +4704 -4103
  6. package/dist/rayzee.umd.js +96 -96
  7. package/package.json +3 -2
  8. package/src/AssetConfig.js +10 -5
  9. package/src/EngineEvents.js +41 -1
  10. package/src/EngineIssues.js +3 -0
  11. package/src/Headless.js +23 -7
  12. package/src/HeadlessCanvas.js +104 -0
  13. package/src/Passes/OIDNDenoiser.js +29 -4
  14. package/src/Passes/OIDNTemporalHistory.js +7 -0
  15. package/src/PathTracerApp.js +432 -119
  16. package/src/Platform.js +76 -0
  17. package/src/Processor/AssetLoader.js +66 -4
  18. package/src/Processor/BVHBuilder.js +2 -1
  19. package/src/Processor/EquirectHDRInfo.js +2 -1
  20. package/src/Processor/HostMemory.js +14 -0
  21. package/src/Processor/IESParser.js +1 -2
  22. package/src/Processor/PBRT/PBRTSceneBuilder.js +26 -0
  23. package/src/Processor/ParallelBVHBuilder.js +7 -6
  24. package/src/Processor/PlatformImageLoader.js +118 -0
  25. package/src/Processor/ResampleRGBA8.js +102 -0
  26. package/src/Processor/SceneProcessor.js +8 -8
  27. package/src/Processor/TextureCreator.js +162 -13
  28. package/src/Processor/Workers/PackWorker.js +28 -0
  29. package/src/Stages/Compositor.js +25 -6
  30. package/src/Stages/PathTracer.js +143 -26
  31. package/src/Stages/PathTracerStage.js +7 -98
  32. package/src/TSL/DebugKernel.js +1 -1
  33. package/src/TSL/EmissiveSampling.js +1 -1
  34. package/src/TSL/GenerateKernel.js +3 -7
  35. package/src/TSL/Random.js +3 -120
  36. package/src/TSL/ShadeKernel.js +6 -6
  37. package/src/index.js +2 -1
  38. package/src/managers/CameraManager.js +2 -2
  39. package/src/managers/DenoisingManager.js +77 -8
  40. package/src/managers/GoboManager.js +8 -1
  41. package/src/managers/IESManager.js +13 -2
  42. package/src/managers/InteractionManager.js +1 -1
  43. package/src/managers/LightManager.js +31 -0
  44. package/src/managers/OverlayManager.js +11 -1
  45. package/src/managers/VideoRenderManager.js +8 -0
  46. package/src/node/NodeWorker.js +144 -0
  47. package/src/node/index.js +9 -0
  48. package/src/node/nodePlatform.js +86 -0
@@ -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 ( object.name.startsWith( 'RectAreaLightPlaceholder' ) &&
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 && typeof Worker !== 'undefined' ) {
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 = new CDFWorker();
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
 
@@ -302,9 +302,8 @@ function tokenize( text ) {
302
302
  function remainderAfterTokens( text, count ) {
303
303
 
304
304
  const re = /\S+/g;
305
- let m;
306
305
  let n = 0;
307
- while ( ( m = re.exec( text ) ) !== null ) {
306
+ while ( re.exec( text ) !== null ) {
308
307
 
309
308
  n ++;
310
309
  if ( n === count ) return text.slice( re.lastIndex );
@@ -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( () => new BVHWorker() );
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( navigator.hardwareConcurrency || 4, MAX_PARALLEL_WORKERS );
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 = new BVHWorker();
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 = new BVHSubtreeWorker();
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 = new BVHWorker();
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 typeof Worker !== 'undefined'
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
+ }