rayzee 11.7.0 → 11.8.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 +13 -0
- package/dist/{ArchiveImporter-DYcRXeKE.js → ArchiveImporter-_sJm5uaK.js} +1923 -1243
- package/dist/{BidirectionalIntegrator-BvjRpbTP.js → BidirectionalIntegrator-Dzt4Z97x.js} +290 -289
- package/dist/{ColorManagement-oeEk7oSu.js → ColorManagement-30MfpHwQ.js} +1 -1
- package/dist/{EngineIssues-CIZnXHsF.js → EngineIssues-xljoXM0E.js} +18 -8
- package/dist/{NeuralRetouch-ClaMiKYg.js → NeuralRetouch-BKQCsmEs.js} +2 -2
- package/dist/{NeuralSuperRes-WNEXun9s.js → NeuralSuperRes-C4ymq68e.js} +2 -2
- package/dist/{PathTracer-B9wZPPVL.js → PathTracer-C_to_i40.js} +3156 -3098
- package/dist/{PhysicalSky-DcjItqKi.js → PhysicalSky-bMHbm_7H.js} +1 -1
- package/dist/{RayzeeRenderer-p-NeE6E2.js → RayzeeRenderer-B_aB6VTJ.js} +370 -371
- package/dist/SceneMetadata-CSmlJW4c.js +183 -0
- package/dist/{ToneMapGPU-sAs8wdgI.js → ToneMapGPU-4GPwoG_c.js} +13 -6
- package/dist/addons/archives.es.js +1 -1
- package/dist/addons/bidirectional.es.js +1 -1
- package/dist/addons/color.es.js +1 -1
- package/dist/addons/physical-sky.es.js +1 -1
- package/dist/addons/storage.es.js +1 -1
- package/dist/{openStorage-Fe9BoS1_.js → openStorage-B085D9FR.js} +1 -1
- package/dist/rayzee-core.es.js +5 -5
- package/dist/rayzee.es.js +815 -796
- package/dist/rayzee.umd.js +106 -102
- package/dist/{sceneCachePolicy-B9w3BnaP.js → sceneCachePolicy-D_uBSbQF.js} +2 -2
- package/package.json +1 -1
- package/src/EngineIssues.js +20 -6
- package/src/Passes/AIUpscaler.js +4 -0
- package/src/Passes/OIDNDenoiser.js +8 -6
- package/src/PathTracerApp.js +10 -1
- package/src/Processor/ArchiveImporter.js +23 -6
- package/src/Processor/AssetLoader.js +15 -0
- package/src/Processor/LightSerializer.js +6 -2
- package/src/Processor/PBRT/PBRTMaterials.js +2 -1
- package/src/Processor/PBRT/PBRTSceneBuilder.js +1 -1
- package/src/Processor/PlatformImageLoader.js +9 -0
- package/src/Processor/TextureCreator.js +44 -4
- package/src/Processor/ToneMapCPU.js +34 -5
- package/src/Processor/ToneMapGPU.js +15 -9
- package/src/Processor/USD/USDScene.js +981 -17
- package/src/RayzeeRenderer.js +81 -26
- package/src/Stages/AutoExposure.js +21 -8
- package/src/Stages/Compositor.js +39 -1
- package/src/Stages/LocalExposure.js +44 -18
- package/src/Stages/NormalDepth.js +7 -3
- package/src/Stages/PathTracer.js +25 -1
- package/src/Stages/PathTracerStage.js +9 -1
- package/src/TSL/CameraRay.js +12 -1
- package/src/TSL/DebugKernel.js +3 -2
- package/src/TSL/FinalWriteKernel.js +4 -7
- package/src/TSL/GenerateKernel.js +6 -2
- package/src/TSL/LightSplatKernel.js +7 -4
- package/src/TSL/LightsCore.js +6 -2
- package/src/TSL/LightsSampling.js +45 -12
- package/src/TSL/SceneResources.js +11 -0
- package/src/TSL/ShadeKernel.js +1 -2
- package/src/integrators/BidirectionalIntegrator.js +1 -0
- package/src/managers/CameraManager.js +9 -0
- package/src/managers/DenoisingManager.js +1 -0
- package/src/managers/InteractionManager.js +6 -0
- package/src/managers/UniformManager.js +3 -1
- package/dist/SceneMetadata-D2Sh-0e-.js +0 -110
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as e } from "./EngineEvents-CLckhYbL.js";
|
|
2
2
|
import { a as t, i as n, r } from "./Logger-dDDGj9Av.js";
|
|
3
3
|
import { f as i, v as a, y as o } from "./BufferLayout-xPRH9jYD.js";
|
|
4
|
-
import { n as s } from "./EngineIssues-
|
|
4
|
+
import { n as s } from "./EngineIssues-xljoXM0E.js";
|
|
5
5
|
import { n as c, r as l } from "./shared-BGBcSxBQ.js";
|
|
6
6
|
import { n as u } from "./AssetConfig-DSEntE65.js";
|
|
7
7
|
import { BackSide as d, BufferAttribute as f, Color as p, DoubleSide as m, FrontSide as h, Matrix3 as g, Matrix4 as _, RGBAFormat as v, Vector2 as y, Vector3 as b } from "three";
|
|
@@ -1594,7 +1594,7 @@ var vt = class {
|
|
|
1594
1594
|
samples: this.samples.slice()
|
|
1595
1595
|
};
|
|
1596
1596
|
}
|
|
1597
|
-
}, yt = "11.
|
|
1597
|
+
}, yt = "11.8.0", $ = 1e4, bt = 15e5;
|
|
1598
1598
|
function xt(e, t, n = $) {
|
|
1599
1599
|
return e >= n && t / bt < e / 3;
|
|
1600
1600
|
}
|
package/package.json
CHANGED
package/src/EngineIssues.js
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export const ISSUE_CODES = Object.freeze( {
|
|
8
8
|
ADAPTER_SOFTWARE: 'adapter.software',
|
|
9
|
+
ADAPTER_UNAVAILABLE: 'adapter.unavailable',
|
|
10
|
+
DEVICE_UNAVAILABLE: 'device.unavailable',
|
|
11
|
+
WEBGPU_UNAVAILABLE: 'webgpu.unavailable',
|
|
9
12
|
ASSET_UNREACHABLE: 'asset.unreachable',
|
|
10
13
|
ASSET_AMBIGUOUS_ENTRY: 'asset.ambiguous_entry',
|
|
11
14
|
ASSET_ARCHIVE_TOO_LARGE: 'asset.archive_too_large',
|
|
@@ -42,9 +45,9 @@ export const ISSUE_SEVERITY = Object.freeze( { ERROR: 'error', WARNING: 'warning
|
|
|
42
45
|
|
|
43
46
|
export class EngineIssueError extends Error {
|
|
44
47
|
|
|
45
|
-
constructor( issue ) {
|
|
48
|
+
constructor( issue, options ) {
|
|
46
49
|
|
|
47
|
-
super( `[${issue.code}] ${issue.message}
|
|
50
|
+
super( `[${issue.code}] ${issue.message}`, options );
|
|
48
51
|
this.name = 'EngineIssueError';
|
|
49
52
|
this.code = issue.code;
|
|
50
53
|
this.detail = issue.detail;
|
|
@@ -67,13 +70,24 @@ export class IssueLog {
|
|
|
67
70
|
/** @throws {EngineIssueError} when strict and severity is 'error' */
|
|
68
71
|
record( code, message, detail = {}, severity = ISSUE_SEVERITY.ERROR ) {
|
|
69
72
|
|
|
70
|
-
const issue =
|
|
71
|
-
this.
|
|
73
|
+
const issue = this._add( code, message, detail, severity );
|
|
74
|
+
if ( this.strict && severity === ISSUE_SEVERITY.ERROR ) throw new EngineIssueError( issue );
|
|
75
|
+
return issue;
|
|
72
76
|
|
|
73
|
-
|
|
77
|
+
}
|
|
74
78
|
|
|
75
|
-
|
|
79
|
+
/** For what nothing can continue past: records an error and throws it, strict or not. */
|
|
80
|
+
fail( code, message, detail = {}, cause ) {
|
|
76
81
|
|
|
82
|
+
throw new EngineIssueError( this._add( code, message, detail, ISSUE_SEVERITY.ERROR ), cause === undefined ? undefined : { cause } );
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_add( code, message, detail, severity ) {
|
|
87
|
+
|
|
88
|
+
const issue = Object.freeze( { code, message, detail, severity, at: Date.now() } );
|
|
89
|
+
this._entries.push( issue );
|
|
90
|
+
this._onIssue?.( issue );
|
|
77
91
|
return issue;
|
|
78
92
|
|
|
79
93
|
}
|
package/src/Passes/AIUpscaler.js
CHANGED
|
@@ -78,6 +78,7 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
78
78
|
* @param {Function} [options.getGPUTextures] - Returns { color: GPUTexture } for HDR path
|
|
79
79
|
* @param {Function} [options.getExposure] - Returns current exposure multiplier
|
|
80
80
|
* @param {Function} [options.getToneMapping] - Returns Three.js ToneMapping constant
|
|
81
|
+
* @param {Function} [options.getTransparentBackground] - Whether the HDR path keeps the render's coverage as alpha
|
|
81
82
|
* @param {number} [options.tileSize] - Override default tile size
|
|
82
83
|
*/
|
|
83
84
|
constructor( output, renderer, options = {} ) {
|
|
@@ -101,6 +102,7 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
101
102
|
this.getToneMapping = options.getToneMapping || ( () => ACESFilmicToneMapping );
|
|
102
103
|
this.getSaturation = options.getSaturation || ( () => 1.0 );
|
|
103
104
|
this.getDisplayGain = options.getDisplayGain || ( () => null );
|
|
105
|
+
this.getTransparentBackground = options.getTransparentBackground || ( () => false );
|
|
104
106
|
|
|
105
107
|
// Configuration
|
|
106
108
|
this.enabled = false;
|
|
@@ -506,6 +508,8 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
506
508
|
gain: this.getDisplayGain(),
|
|
507
509
|
} );
|
|
508
510
|
this._toneMapper.release();
|
|
511
|
+
// The render's alpha is coverage; the canvas shows an opaque backdrop opaque, and so does the upscale.
|
|
512
|
+
if ( ! this.getTransparentBackground() ) planes.fill( 1, 3 * width * height );
|
|
509
513
|
|
|
510
514
|
return { planes, width, height, isHDR: true };
|
|
511
515
|
|
|
@@ -443,10 +443,10 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
443
443
|
|
|
444
444
|
} while ( this._reloadPending );
|
|
445
445
|
|
|
446
|
-
// Fire a start() that arrived mid-load
|
|
447
|
-
//
|
|
448
|
-
// no-op reload still releases it — it used to be stranded by the early return below.
|
|
449
|
-
if ( this._pendingStart && this.
|
|
446
|
+
// Fire a start() that arrived mid-load, unless it was disabled meanwhile. After a failed
|
|
447
|
+
// load too: it denoises nothing but still sends 'end', which its caller waits for. Outside
|
|
448
|
+
// the loop, so a no-op reload still releases it — it used to be stranded by the early return below.
|
|
449
|
+
if ( this._pendingStart && this.enabled ) {
|
|
450
450
|
|
|
451
451
|
const continuous = this._pendingStartContinuous;
|
|
452
452
|
this._pendingStart = false;
|
|
@@ -617,8 +617,6 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
617
617
|
|
|
618
618
|
async execute( continuous = false ) {
|
|
619
619
|
|
|
620
|
-
if ( ! this.enabled || ! this.unet ) return false;
|
|
621
|
-
|
|
622
620
|
// Create abort controller for this execution
|
|
623
621
|
const runId = ++ this._runId;
|
|
624
622
|
this.state.abortController = new AbortController();
|
|
@@ -626,6 +624,10 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
626
624
|
|
|
627
625
|
try {
|
|
628
626
|
|
|
627
|
+
// Inside the try, so the finally ends what start() announced: with no weights (a failed
|
|
628
|
+
// load) the Denoising badge, the closing upscaler and a video export waited forever.
|
|
629
|
+
if ( ! this.enabled || ! this.unet ) return false;
|
|
630
|
+
|
|
629
631
|
// The GPU path bails out early — no textures yet, no device — by returning false, and
|
|
630
632
|
// a caller that read that as success published a picture nothing had written to.
|
|
631
633
|
return await this._executeUNetGPU( continuous ) !== false;
|
package/src/PathTracerApp.js
CHANGED
|
@@ -5,6 +5,7 @@ import { MotionVector } from './Stages/MotionVector.js';
|
|
|
5
5
|
import { NRD } from './Stages/NRD.js';
|
|
6
6
|
import { AutoExposure, AUTO_EXPOSURE_DEFAULTS } from './Stages/AutoExposure.js';
|
|
7
7
|
import { LocalExposure, LOCAL_EXPOSURE_DEFAULTS } from './Stages/LocalExposure.js';
|
|
8
|
+
import { backdropGain } from './Processor/ToneMapCPU.js';
|
|
8
9
|
import { PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, modePresetSettings } from './EngineDefaults.js';
|
|
9
10
|
import { createLogger } from './utils/Logger.js';
|
|
10
11
|
import { EngineEvents } from './EngineEvents.js';
|
|
@@ -323,6 +324,7 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
323
324
|
|
|
324
325
|
if ( this.stages.autoExposure ) this.stages.autoExposure.setCompensation( value );
|
|
325
326
|
else core.applyExposure( value );
|
|
327
|
+
this.stages.compositor?.setBackdropExposure( value );
|
|
326
328
|
|
|
327
329
|
},
|
|
328
330
|
onCameraProjection: ( value ) => {
|
|
@@ -423,7 +425,12 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
423
425
|
const le = this.stages.localExposure;
|
|
424
426
|
// A readback outside the loop (headless) can come before the loop would have built it from this image.
|
|
425
427
|
if ( le?.wantsBuild ) le.build( this.pipeline.context );
|
|
426
|
-
|
|
428
|
+
const local = le?.toneGain() ?? null;
|
|
429
|
+
if ( this.pipeline?.context.getState( 'pathtracer:backdrop' ) !== 'color' ) return local;
|
|
430
|
+
|
|
431
|
+
// As the compositor shows it: a solid backdrop at the manual exposure, untouched by auto and local exposure.
|
|
432
|
+
const { toneMappingExposure, toneMapping } = this.renderer;
|
|
433
|
+
return { ...local, backdrop: backdropGain( this.settings.get( 'exposure' ) ?? 1, toneMappingExposure, toneMapping ) };
|
|
427
434
|
|
|
428
435
|
}
|
|
429
436
|
|
|
@@ -1130,6 +1137,8 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
1130
1137
|
if ( ! ae ) return;
|
|
1131
1138
|
|
|
1132
1139
|
ae.setCompensation( this.settings.get( 'exposure' ) ?? 1 );
|
|
1140
|
+
// A solid backdrop stays at the manual exposure while auto exposure moves the picture.
|
|
1141
|
+
this.stages.compositor.setBackdropExposure( this.settings.get( 'exposure' ) ?? 1 );
|
|
1133
1142
|
if ( this.stages.localExposure ) this.stages.localExposure.onChange = () => this._refreshFinished();
|
|
1134
1143
|
ae.onChange = () => this._refreshFinished();
|
|
1135
1144
|
|
|
@@ -26,9 +26,10 @@ import { loadPBRTScene, pickEntryPath, VirtualFS, PBRT_BUILD_REVISION } from './
|
|
|
26
26
|
import { loadUSDScene, listUSDRootLayers, listUSDParts, USDFiles, USDStage, USD_LAYER } from './USD/index.js';
|
|
27
27
|
import { pfmTexture } from './PBRT/PFM.js';
|
|
28
28
|
import { extractSceneMetadata } from './SceneMetadata.js';
|
|
29
|
-
import { ISSUE_CODES, ISSUE_SEVERITY } from '../EngineIssues.js';
|
|
29
|
+
import { ISSUE_CODES, ISSUE_SEVERITY, EngineIssueError } from '../EngineIssues.js';
|
|
30
30
|
import { ARCHIVE_FORMATS } from './archiveFormats.js';
|
|
31
|
-
import { withHostWorker } from '../Platform.js';
|
|
31
|
+
import { withHostWorker, getPlatform } from '../Platform.js';
|
|
32
|
+
import { platformImageFromBytes } from './PlatformImageLoader.js';
|
|
32
33
|
|
|
33
34
|
const MTL_TEXTURE_TIMEOUT_MS = 30000;
|
|
34
35
|
const MAIN_MODEL_FILES = [ 'scene.gltf', 'scene.glb', 'model.gltf', 'model.glb', 'main.gltf', 'main.glb', 'asset.gltf', 'asset.glb' ];
|
|
@@ -646,6 +647,7 @@ export class ArchiveImporter {
|
|
|
646
647
|
|
|
647
648
|
const stored = graphKey ? await this._loadStoredGraph( graphKey, source, { imageFromBytes, envFromBytes } ) : null;
|
|
648
649
|
const built = stored ?? await loadPBRTScene( { vfs: zip, source, entryPath, plyParser, imageFromBytes, envFromBytes, ...shape } );
|
|
650
|
+
this.loader._throwDeferred();
|
|
649
651
|
|
|
650
652
|
const { group, environment, animations, report, warnings, meshCount, entryPath: loadedEntry, candidates,
|
|
651
653
|
frames, parseMs, buildMs, triangleCount, placementCount, mergedShapes, skippedForBudget,
|
|
@@ -729,7 +731,20 @@ export class ArchiveImporter {
|
|
|
729
731
|
/** A texture map from an archive, tagged with its path so a stored scene can read it back. @private */
|
|
730
732
|
async _archiveImage( bytes, fname ) {
|
|
731
733
|
|
|
732
|
-
|
|
734
|
+
let texture;
|
|
735
|
+
try {
|
|
736
|
+
|
|
737
|
+
texture = await this._pbrtTextureFromBytes( bytes, fname );
|
|
738
|
+
|
|
739
|
+
} catch ( error ) {
|
|
740
|
+
|
|
741
|
+
// As a glTF's image: its surfaces render untextured, and a strict host hears of it at the end of the load.
|
|
742
|
+
if ( error instanceof EngineIssueError ) throw error;
|
|
743
|
+
this.loader._reportImageFailure( fname, error );
|
|
744
|
+
return null;
|
|
745
|
+
|
|
746
|
+
}
|
|
747
|
+
|
|
733
748
|
if ( texture ) texture.userData[ ARCHIVE_PATH ] = fname;
|
|
734
749
|
return texture;
|
|
735
750
|
|
|
@@ -803,6 +818,7 @@ export class ArchiveImporter {
|
|
|
803
818
|
maxTriangles: options.maxTriangles, maxPlacements: options.maxPlacements, mergeShapesAbove: options.mergeShapesAbove,
|
|
804
819
|
curveSteps: options.curveSteps, curveSides: options.curveSides, curveTolerance: options.curveTolerance,
|
|
805
820
|
} );
|
|
821
|
+
this.loader._throwDeferred();
|
|
806
822
|
|
|
807
823
|
const { group, environment, warnings, triangleCount, placementCount, skippedForBudget, meshCount, parseMs, buildMs, fitNote } = built;
|
|
808
824
|
this.lastUSDStats = { parseMs, buildMs, loaderMs: performance.now() - start, triangleCount, placementCount, skippedForBudget, meshCount, fitNote };
|
|
@@ -915,9 +931,10 @@ export class ArchiveImporter {
|
|
|
915
931
|
|
|
916
932
|
} else {
|
|
917
933
|
|
|
918
|
-
// png / jpg / webp / gif / bmp
|
|
919
|
-
|
|
920
|
-
tex = new Texture(
|
|
934
|
+
// png / jpg / webp / gif / bmp: the host's decoder where it has one (Node), else the browser's.
|
|
935
|
+
if ( getPlatform().decodeImage ) tex = await platformImageFromBytes( bytes, fname );
|
|
936
|
+
else if ( typeof createImageBitmap === 'function' ) tex = new Texture( await createImageBitmap( new Blob( [ bytes ] ) ) );
|
|
937
|
+
else throw new Error( 'no image decoder in this runtime: configurePlatform( nodePlatform( { decodeImage } ) ) — see "Running in Node"' );
|
|
921
938
|
tex.colorSpace = SRGBColorSpace;
|
|
922
939
|
|
|
923
940
|
}
|
|
@@ -52,6 +52,20 @@ function standInForSplit( source ) {
|
|
|
52
52
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// glTF's znear is a camera's near clipping plane (Blender writes its clip start there): the path tracer honours it.
|
|
56
|
+
export const cameraClipPlugin = {
|
|
57
|
+
name: 'RAYZEE_camera_clip',
|
|
58
|
+
afterRoot( result ) {
|
|
59
|
+
|
|
60
|
+
for ( const scene of result.scenes ) scene.traverse( ( object ) => {
|
|
61
|
+
|
|
62
|
+
if ( object.isCamera && object.near > 0 ) object.userData.clipStart = object.near;
|
|
63
|
+
|
|
64
|
+
} );
|
|
65
|
+
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
55
69
|
// Luminous efficacy glTF, three.js and Blender's exporter all assume (PBR_WATTS_TO_LUMENS).
|
|
56
70
|
const LUMENS_PER_WATT = 683;
|
|
57
71
|
|
|
@@ -768,6 +782,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
768
782
|
if ( getPlatform().decodeImage ) loader.register( ( parser ) => platformImagesPlugin( parser, onImageFailure ) );
|
|
769
783
|
else if ( ! hasImageDecoder() ) loader.register( ( parser ) => missingImageDecoderPlugin( parser, onImageFailure ) );
|
|
770
784
|
loader.register( ( parser ) => diffuseTransmissionPlugin( parser ) );
|
|
785
|
+
loader.register( () => cameraClipPlugin );
|
|
771
786
|
|
|
772
787
|
return loader;
|
|
773
788
|
|
|
@@ -32,6 +32,9 @@ function effectiveEmission( light ) {
|
|
|
32
32
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// A point or spot light's radius (Blender's Radius, USD's SphereLight radius): `userData.radius`, in scene units.
|
|
36
|
+
const lightRadius = ( light ) => Math.max( 0, Number( light.userData?.radius ) || 0 );
|
|
37
|
+
|
|
35
38
|
export class LightSerializer {
|
|
36
39
|
|
|
37
40
|
constructor() {
|
|
@@ -216,7 +219,8 @@ export class LightSerializer {
|
|
|
216
219
|
em.r, em.g, em.b, // color (3) — incl. temperature tint
|
|
217
220
|
em.intensity * INV_4PI, // radiant intensity W/sr = power(W)/4π, incl. exposure (1)
|
|
218
221
|
light.distance || 0.0, // cutoff distance (0 = infinite) (1)
|
|
219
|
-
light.decay !== undefined ? light.decay : 2.0 // decay exponent (1)
|
|
222
|
+
light.decay !== undefined ? light.decay : 2.0, // decay exponent (1)
|
|
223
|
+
lightRadius( light ), // radius: a disc this size facing the lit point, 0 = a point (1)
|
|
220
224
|
],
|
|
221
225
|
importance: importance,
|
|
222
226
|
light: light
|
|
@@ -266,7 +270,7 @@ export class LightSerializer {
|
|
|
266
270
|
goboIntensity, // signed gobo strength (1)
|
|
267
271
|
iesIndex, // IES profile index, -1 = none (1)
|
|
268
272
|
iesIntensity, // IES blend [0,1] (1)
|
|
269
|
-
|
|
273
|
+
lightRadius( light ), // radius, as a point light's (1)
|
|
270
274
|
0.0, // reserved (1) — keeps stride at 20 (vec4 aligned)
|
|
271
275
|
],
|
|
272
276
|
importance: importance,
|
|
@@ -393,7 +393,8 @@ export async function buildMaterial( def, ctx ) {
|
|
|
393
393
|
case 'metal': {
|
|
394
394
|
|
|
395
395
|
await applyConductorColor( mat, params, ctx, '', applyAlbedo, setColor );
|
|
396
|
-
|
|
396
|
+
// pbrt's roughness defaults to 0, a mirror.
|
|
397
|
+
await applyRoughness( mat, 'roughness', params, 0, ctx );
|
|
397
398
|
break;
|
|
398
399
|
|
|
399
400
|
}
|
|
@@ -108,7 +108,7 @@ const DEFAULT_CURVE_SIDES = { flat: 1, ribbon: 1, cylinder: 2 };
|
|
|
108
108
|
const DEFAULT_CURVE_TOLERANCE = 0.05;
|
|
109
109
|
|
|
110
110
|
/** Bumped whenever the same scene files build a different graph, so a stored graph is not reused. */
|
|
111
|
-
export const PBRT_BUILD_REVISION =
|
|
111
|
+
export const PBRT_BUILD_REVISION = 11;
|
|
112
112
|
|
|
113
113
|
function samePlacements( a, b ) {
|
|
114
114
|
|
|
@@ -97,6 +97,15 @@ export async function loadPlatformImage( url, mimeType = mimeTypeOf( { uri: url
|
|
|
97
97
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/** An image file's bytes as a texture, decoded by the platform: {@link loadPlatformImage} without the fetch. */
|
|
101
|
+
export async function platformImageFromBytes( bytes, filename ) {
|
|
102
|
+
|
|
103
|
+
const texture = pixelTexture( await decodeBytes( bytes, mimeTypeOf( { uri: filename } ) ), { shared: true } );
|
|
104
|
+
texture.flipY = true;
|
|
105
|
+
return texture;
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
|
|
100
109
|
/**
|
|
101
110
|
* A GLTFLoader plugin for a runtime with no way to decode an image (Node without `decodeImage`): each image fails with
|
|
102
111
|
* what to configure, where three.js's own path threw `self is not defined` and took the whole load down.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColorSpace, LinearSRGBColorSpace, RepeatWrapping, FloatType, HalfFloatType } from "three";
|
|
1
|
+
import { DataArrayTexture, RGBAFormat, RGFormat, RedFormat, LinearFilter, UnsignedByteType, UnsignedShortType, SRGBColorSpace, LinearSRGBColorSpace, RepeatWrapping, FloatType, HalfFloatType, DataUtils } from "three";
|
|
2
2
|
import { MATERIAL_DATA_LAYOUT, BVH_LEAF_MARKERS, assertBVHIndexFits, bvhIndexView } from './BufferLayout.js';
|
|
3
3
|
import { alignBucketWidth, TEXTURE_CONSTANTS } from './TextureBuckets.js';
|
|
4
4
|
import { packMaterial } from './MaterialPacking.js';
|
|
@@ -1260,7 +1260,7 @@ export class TextureCreator {
|
|
|
1260
1260
|
|
|
1261
1261
|
const image = texture?.image;
|
|
1262
1262
|
const pixels = texture?.isCompressedTexture ? texture.mipmaps?.[ 0 ] : image;
|
|
1263
|
-
const data = pixels?.data;
|
|
1263
|
+
const data = texture?.isCompressedTexture ? pixels?.data : rgba8Pixels( texture );
|
|
1264
1264
|
if ( data && pixels.width > 0 && pixels.height > 0 && data.byteLength >= pixels.width * pixels.height * 4 ) {
|
|
1265
1265
|
|
|
1266
1266
|
return { data: new Uint8Array( data.buffer, data.byteOffset, pixels.width * pixels.height * 4 ), width: pixels.width, height: pixels.height, flipY: texture.flipY !== false };
|
|
@@ -1817,7 +1817,8 @@ export class TextureCreator {
|
|
|
1817
1817
|
|
|
1818
1818
|
const idx = normalized.length;
|
|
1819
1819
|
normalized.push( null );
|
|
1820
|
-
|
|
1820
|
+
const pixels = rgba8Pixels( tex );
|
|
1821
|
+
bitmapJobs.push( { index: idx, flipY: tex.flipY, promise: pixels ? _rawPixelsToBitmap( pixels, tex.image.width, tex.image.height ) : Promise.reject( new Error( 'pixels in a layout the texture arrays cannot take' ) ) } );
|
|
1821
1822
|
continue;
|
|
1822
1823
|
|
|
1823
1824
|
}
|
|
@@ -1900,10 +1901,49 @@ export class TextureCreator {
|
|
|
1900
1901
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
1901
1902
|
|
|
1902
1903
|
/** Convert raw RGBA pixel data to an ImageBitmap (zero-copy Uint8ClampedArray view). */
|
|
1903
|
-
|
|
1904
|
+
// Async, so a bad layer rejects into its placeholder instead of throwing through the whole bucket.
|
|
1905
|
+
async function _rawPixelsToBitmap( data, width, height ) {
|
|
1904
1906
|
|
|
1905
1907
|
const clamped = new Uint8ClampedArray( data.buffer, data.byteOffset, data.byteLength );
|
|
1906
1908
|
return createImageBitmap( new ImageData( clamped, width, height ) );
|
|
1907
1909
|
|
|
1908
1910
|
}
|
|
1909
1911
|
|
|
1912
|
+
/**
|
|
1913
|
+
* A data texture's pixels as RGBA8 (the arrays are 8-bit), or null. Float and half-float maps — an archive's EXR, HDR or
|
|
1914
|
+
* PFM — clamp to [0, 1]; one channel fills grey. Read as bytes, a float buffer was noise in Node and failed the bucket in
|
|
1915
|
+
* the browser.
|
|
1916
|
+
*/
|
|
1917
|
+
export function rgba8Pixels( texture ) {
|
|
1918
|
+
|
|
1919
|
+
const image = texture?.image;
|
|
1920
|
+
const data = image?.data;
|
|
1921
|
+
if ( ! data || ! ( image.width > 0 ) || ! ( image.height > 0 ) ) return null;
|
|
1922
|
+
const count = image.width * image.height;
|
|
1923
|
+
if ( data instanceof Uint8Array || data instanceof Uint8ClampedArray ) {
|
|
1924
|
+
|
|
1925
|
+
return data.byteLength >= count * 4 ? new Uint8Array( data.buffer, data.byteOffset, count * 4 ) : null;
|
|
1926
|
+
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
const channels = { [ RGBAFormat ]: 4, [ RGFormat ]: 2, [ RedFormat ]: 1 }[ texture.format ] ?? Math.round( data.length / count );
|
|
1930
|
+
if ( ! ( channels >= 1 && channels <= 4 ) || data.length < count * channels ) return null;
|
|
1931
|
+
const half = texture.type === HalfFloatType;
|
|
1932
|
+
const scale = texture.type === UnsignedShortType ? 1 / 65535 : 1;
|
|
1933
|
+
const out = new Uint8Array( count * 4 );
|
|
1934
|
+
for ( let i = 0; i < count; i ++ ) {
|
|
1935
|
+
|
|
1936
|
+
for ( let c = 0; c < 4; c ++ ) {
|
|
1937
|
+
|
|
1938
|
+
const raw = c < channels ? data[ i * channels + c ] : null;
|
|
1939
|
+
const v = raw === null ? ( c === 3 ? 1 : out[ i * 4 ] / 255 ) : ( half ? DataUtils.fromHalfFloat( raw ) : raw * scale );
|
|
1940
|
+
out[ i * 4 + c ] = Math.max( 0, Math.min( 255, Math.round( v * 255 ) ) );
|
|
1941
|
+
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
return out;
|
|
1947
|
+
|
|
1948
|
+
}
|
|
1949
|
+
|
|
@@ -98,6 +98,26 @@ export function effectiveExposure( exposure, toneMapping ) {
|
|
|
98
98
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The gain that shows a pixel of a solid backdrop at `heldAt` while the picture is shown at `exposure`: auto
|
|
103
|
+
* exposure does not move the backdrop. 1 under a view that applies no exposure.
|
|
104
|
+
*/
|
|
105
|
+
export function backdropGain( heldAt, exposure, toneMapping ) {
|
|
106
|
+
|
|
107
|
+
return effectiveExposure( heldAt, toneMapping ) / effectiveExposure( exposure, toneMapping );
|
|
108
|
+
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A pixel's gain once the backdrop is held: `backdrop` where the camera saw only the backdrop (coverage 0), the
|
|
113
|
+
* scene's own gain where it saw the scene, blended by coverage between. The GPU pass computes the same.
|
|
114
|
+
*/
|
|
115
|
+
export function heldGain( gain, backdrop, coverage ) {
|
|
116
|
+
|
|
117
|
+
return gain + ( backdrop - gain ) * ( 1 - Math.min( Math.max( coverage, 0 ), 1 ) );
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
|
101
121
|
/** Rec.709 luminance coefficients (same as Display / Common.js). */
|
|
102
122
|
const LUM_R = 0.2126, LUM_G = 0.7152, LUM_B = 0.0722;
|
|
103
123
|
|
|
@@ -128,8 +148,9 @@ export function applySaturation( out, saturation ) {
|
|
|
128
148
|
* @param {number} options.toneMapping - a registered view transform id
|
|
129
149
|
* @param {number} [options.saturation=1]
|
|
130
150
|
* @param {boolean} [options.preserveAlpha=false]
|
|
131
|
-
* @param {{fn
|
|
132
|
-
* the twin of the GPU pass's `gain`: `fn( u, v, luminance, log2 exposure )` per pixel, before the curve
|
|
151
|
+
* @param {{fn?: function(number, number, number, number): number, width: number, height: number, backdrop?: number}} [options.pixelGain] -
|
|
152
|
+
* the twin of the GPU pass's `gain`: `fn( u, v, luminance, log2 exposure )` per pixel, before the curve, and
|
|
153
|
+
* `backdrop` the gain a solid backdrop is held at (`heldGain`)
|
|
133
154
|
* @returns {Uint8ClampedArray} RGBA bytes
|
|
134
155
|
*/
|
|
135
156
|
export function toneMapToRGBA8( linear, { exposure, toneMapping, saturation = 1, preserveAlpha = false, pixelGain = null } ) {
|
|
@@ -146,9 +167,17 @@ export function toneMapToRGBA8( linear, { exposure, toneMapping, saturation = 1,
|
|
|
146
167
|
let g = gain;
|
|
147
168
|
if ( pixelGain ) {
|
|
148
169
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
170
|
+
let pg = 1;
|
|
171
|
+
if ( pixelGain.fn ) {
|
|
172
|
+
|
|
173
|
+
const p = i >> 2, x = p % pixelGain.width, y = ( p - x ) / pixelGain.width;
|
|
174
|
+
const lum = linear[ i ] * 0.2126 + linear[ i + 1 ] * 0.7152 + linear[ i + 2 ] * 0.0722;
|
|
175
|
+
pg = pixelGain.fn( ( x + 0.5 ) / pixelGain.width, ( y + 0.5 ) / pixelGain.height, lum, logExposure );
|
|
176
|
+
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if ( pixelGain.backdrop > 0 ) pg = heldGain( pg, pixelGain.backdrop, linear[ i + 3 ] );
|
|
180
|
+
g *= pg;
|
|
152
181
|
|
|
153
182
|
}
|
|
154
183
|
|
|
@@ -97,6 +97,7 @@ struct Params {
|
|
|
97
97
|
exposure: f32,
|
|
98
98
|
saturation: f32,
|
|
99
99
|
alpha: u32,
|
|
100
|
+
backdrop: f32,
|
|
100
101
|
};
|
|
101
102
|
|
|
102
103
|
@group(0) @binding(2) var<uniform> params: Params;
|
|
@@ -115,12 +116,15 @@ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
|
|
|
115
116
|
|
|
116
117
|
let linear = rayzee_source( gid.x, srcY );
|
|
117
118
|
let uv = vec2<f32>( ( f32( gid.x ) + 0.5 ) / f32( params.width ), ( f32( gid.y ) + 0.5 ) / f32( params.height ) );
|
|
118
|
-
|
|
119
|
+
var g = ${gainWGSL ? 'rayzee_gain( uv, linear.rgb, params.exposure )' : '1.0'};
|
|
120
|
+
// A solid backdrop is held at its own gain, blended by the coverage in alpha (ToneMapCPU's heldGain).
|
|
121
|
+
if ( params.backdrop > 0.0 ) { g = g + ( params.backdrop - g ) * ( 1.0 - clamp( linear.a, 0.0, 1.0 ) ); }
|
|
122
|
+
let rgb = linear.rgb * g;
|
|
119
123
|
rayzee_store( gid.y * params.width + gid.x, linear, rayzee_tone_map( rgb, params.mode, params.exposure, params.saturation ) );
|
|
120
124
|
}
|
|
121
125
|
`;
|
|
122
126
|
|
|
123
|
-
/** Uniform blocks round up to 16 bytes; the struct above is
|
|
127
|
+
/** Uniform blocks round up to 16 bytes; the struct above is 32. */
|
|
124
128
|
const PARAMS_BYTES = 32;
|
|
125
129
|
|
|
126
130
|
/**
|
|
@@ -171,7 +175,7 @@ export class PackedToneMapper {
|
|
|
171
175
|
|
|
172
176
|
}
|
|
173
177
|
|
|
174
|
-
const key = gain?.key
|
|
178
|
+
const key = gain?.wgsl ? gain.key : '';
|
|
175
179
|
let pipeline = this._pipelines.get( key );
|
|
176
180
|
if ( pipeline ) return pipeline;
|
|
177
181
|
|
|
@@ -275,9 +279,10 @@ export class PackedToneMapper {
|
|
|
275
279
|
* @param {number} [tone.saturation=1]
|
|
276
280
|
* @param {boolean} [tone.flipY=false]
|
|
277
281
|
* @param {boolean} [tone.preserveAlpha=false] - the source's alpha, rounded as the CPU rounds it; else 255
|
|
278
|
-
* @param {{key
|
|
279
|
-
* per-pixel gain before the curve: WGSL defining `rayzee_gain( uv, linear, exposure ) -> f32` over bind group 1
|
|
280
|
-
*
|
|
282
|
+
* @param {{key?: string, wgsl?: string, entries?: GPUBindGroupEntry[], device?: GPUDevice, copyTo?: Function, backdrop?: number}} [tone.gain] - a
|
|
283
|
+
* per-pixel gain before the curve: WGSL defining `rayzee_gain( uv, linear, exposure ) -> f32` over bind group 1,
|
|
284
|
+
* and/or `backdrop`, the gain a solid backdrop is held at (coverage in alpha). One on another device is copied
|
|
285
|
+
* to this one for the run (`copyTo`).
|
|
281
286
|
* @returns {Promise<Uint8ClampedArray>} RGBA bytes, `width * height * 4`
|
|
282
287
|
*/
|
|
283
288
|
async toRGBA8( src, tone ) {
|
|
@@ -302,12 +307,12 @@ export class PackedToneMapper {
|
|
|
302
307
|
async _run( src, tone = {} ) {
|
|
303
308
|
|
|
304
309
|
const gain = tone.gain;
|
|
305
|
-
if ( ! gain?.device || gain.device === this.device ) return this._dispatch( src, tone );
|
|
310
|
+
if ( ! gain?.wgsl || ! gain.device || gain.device === this.device ) return this._dispatch( src, tone );
|
|
306
311
|
|
|
307
312
|
const copy = await gain.copyTo( this.device );
|
|
308
313
|
try {
|
|
309
314
|
|
|
310
|
-
return await this._dispatch( src, { ...tone, gain: copy } );
|
|
315
|
+
return await this._dispatch( src, { ...tone, gain: { ...copy, backdrop: gain.backdrop } } );
|
|
311
316
|
|
|
312
317
|
} finally {
|
|
313
318
|
|
|
@@ -337,6 +342,7 @@ export class PackedToneMapper {
|
|
|
337
342
|
this._paramF32[ 4 ] = exposure;
|
|
338
343
|
this._paramF32[ 5 ] = saturation;
|
|
339
344
|
this._paramU32[ 6 ] = preserveAlpha ? 1 : 0;
|
|
345
|
+
this._paramF32[ 7 ] = gain?.backdrop > 0 ? gain.backdrop : 0;
|
|
340
346
|
this.device.queue.writeBuffer( this._params, 0, this._paramData );
|
|
341
347
|
|
|
342
348
|
const tableEntries = [ ...this._tables.entries() ]
|
|
@@ -356,7 +362,7 @@ export class PackedToneMapper {
|
|
|
356
362
|
const pass = encoder.beginComputePass();
|
|
357
363
|
pass.setPipeline( pipeline );
|
|
358
364
|
pass.setBindGroup( 0, group );
|
|
359
|
-
if ( gain ) pass.setBindGroup( 1, this.device.createBindGroup( { layout: pipeline.getBindGroupLayout( 1 ), entries: gain.entries } ) );
|
|
365
|
+
if ( gain?.wgsl ) pass.setBindGroup( 1, this.device.createBindGroup( { layout: pipeline.getBindGroupLayout( 1 ), entries: gain.entries } ) );
|
|
360
366
|
pass.dispatchWorkgroups( Math.ceil( width / 8 ), Math.ceil( height / 8 ) );
|
|
361
367
|
pass.end();
|
|
362
368
|
encoder.copyBufferToBuffer( this._storage, 0, this._map, 0, bytes );
|