rayzee 8.4.1 → 8.6.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 +61 -9
- package/dist/{DLSSNeural-BNSJ8V3z.js → DLSSNeural-BhBCBgKf.js} +5 -5
- package/dist/{DLSSSuperRes-BoEW89Cl.js → DLSSSuperRes-BCA7F_k5.js} +5 -5
- package/dist/ToneMapGPU-BzcLheoR.js +533 -0
- package/dist/rayzee.es.js +27278 -9133
- package/dist/rayzee.umd.js +255 -187
- package/package.json +1 -1
- package/src/AssetConfig.js +15 -2
- package/src/Color/BakedViews.js +134 -0
- package/src/Color/BuiltinViews.js +261 -0
- package/src/Color/ColorManagement.js +1130 -0
- package/src/Color/ColorSpaces.js +432 -0
- package/src/Color/Displays.js +19 -0
- package/src/Color/InputColorSpaces.js +243 -0
- package/src/Color/LutBake.js +371 -0
- package/src/Color/OcioRuntime.js +338 -0
- package/src/Color/OcioViews.js +419 -0
- package/src/Color/ViewTransforms.js +323 -0
- package/src/Color/WorkingMatrix.js +86 -0
- package/src/EngineDefaults.js +84 -4
- package/src/EngineIssues.js +3 -1
- package/src/Passes/AIUpscaler.js +16 -7
- package/src/PathTracerApp.js +318 -35
- package/src/Processor/AssetLoader.js +47 -55
- package/src/Processor/CameraOptimizer.js +22 -6
- package/src/Processor/EmissiveTriangleBuilder.js +34 -2
- package/src/Processor/GeometryExtractor.js +151 -138
- package/src/Processor/LightSerializer.js +17 -20
- package/src/Processor/MaterialPacking.js +168 -0
- package/src/Processor/PackedRayBuffer.js +6 -2
- package/src/Processor/ProceduralSky.js +4 -0
- package/src/Processor/SceneProcessor.js +25 -1
- package/src/Processor/SimpleSky.js +8 -0
- package/src/Processor/TextureCreator.js +137 -91
- package/src/Processor/TextureReadback.js +92 -0
- package/src/Processor/ToneMapCPU.js +80 -165
- package/src/Processor/ToneMapGPU.js +122 -151
- package/src/RenderSettings.js +10 -1
- package/src/Stages/Compositor.js +11 -2
- package/src/Stages/PathTracer.js +21 -4
- package/src/Stages/PathTracerStage.js +11 -18
- package/src/TSL/Common.js +15 -0
- package/src/TSL/Debugger.js +2 -3
- package/src/TSL/EmissiveSampling.js +6 -8
- package/src/TSL/ExtendKernel.js +9 -0
- package/src/TSL/Fresnel.js +23 -1
- package/src/TSL/HitFacet.js +102 -0
- package/src/TSL/LightsDirect.js +12 -33
- package/src/TSL/LightsSampling.js +15 -7
- package/src/TSL/MaterialEvaluation.js +26 -19
- package/src/TSL/MaterialProperties.js +74 -37
- package/src/TSL/MaterialTransmission.js +2 -3
- package/src/TSL/ShadeKernel.js +34 -21
- package/src/TSL/ShadowTerminator.js +125 -0
- package/src/TSL/Struct.js +9 -0
- package/src/TSL/Subsurface.js +2 -3
- package/src/index.js +29 -2
- package/src/managers/DenoisingManager.js +3 -7
- package/src/managers/EnvironmentManager.js +20 -0
- package/src/managers/LightManager.js +17 -15
- package/src/managers/MaterialDataManager.js +118 -208
- package/src/managers/UniformManager.js +1 -0
- package/src/managers/VideoRenderManager.js +9 -9
- package/dist/ToneMapGPU-DzqSyzTx.js +0 -341
- package/src/LightUnits.js +0 -122
package/src/PathTracerApp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WebGPURenderer, RectAreaLightNode, SRGBColorSpace } from 'three/webgpu';
|
|
1
|
+
import { WebGPURenderer, RectAreaLightNode, SRGBColorSpace, LinearSRGBColorSpace } from 'three/webgpu';
|
|
2
2
|
import { texture as _tslTexture, cubeTexture as _tslCubeTexture } from 'three/tsl';
|
|
3
3
|
import {
|
|
4
4
|
Scene, EventDispatcher, Box3
|
|
@@ -17,15 +17,18 @@ import { AutoExposure } from './Stages/AutoExposure.js';
|
|
|
17
17
|
import { Compositor } from './Stages/Compositor.js';
|
|
18
18
|
import { RenderPipeline } from './Pipeline/RenderPipeline.js';
|
|
19
19
|
import { CompletionTracker } from './Pipeline/CompletionTracker.js';
|
|
20
|
-
import { ENGINE_DEFAULTS as DEFAULT_STATE, PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, MAX_STORAGE_TEXTURE_SIZE, MAX_RESERVABLE_RENDER_SIZE, setReservedRenderSize, getRenderProfile } from './EngineDefaults.js';
|
|
20
|
+
import { ENGINE_DEFAULTS as DEFAULT_STATE, PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, modePresetSettings, MAX_STORAGE_TEXTURE_SIZE, MAX_RESERVABLE_RENDER_SIZE, setReservedRenderSize, getRenderProfile } from './EngineDefaults.js';
|
|
21
21
|
import { updateStats, updateLoading, resetLoading, setStatusCallback, getDisplaySamples, disposeObjectFromMemory, disposeRenderer } from './Processor/utils.js';
|
|
22
22
|
import { BuildTimer } from './Processor/BuildTimer.js';
|
|
23
|
+
import { TextureReadback } from './Processor/TextureReadback.js';
|
|
23
24
|
import { createLogger, fmt } from './utils/Logger.js';
|
|
24
25
|
import { InteractionManager } from './managers/InteractionManager.js';
|
|
25
26
|
import { EngineEvents } from './EngineEvents.js';
|
|
26
27
|
import { IssueLog, ISSUE_CODES } from './EngineIssues.js';
|
|
27
28
|
import { SETTING_SOURCE } from './RenderSettings.js';
|
|
28
29
|
import { toneMapToRGBA8 } from './Processor/ToneMapCPU.js';
|
|
30
|
+
import { ColorManagement, setActiveColorManagement } from './Color/ColorManagement.js';
|
|
31
|
+
import { getViewTransform } from './Color/ViewTransforms.js';
|
|
29
32
|
import { AssetLoader } from './Processor/AssetLoader.js';
|
|
30
33
|
import { SceneProcessor } from './Processor/SceneProcessor.js';
|
|
31
34
|
|
|
@@ -163,6 +166,15 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
163
166
|
onIssue: ( issue ) => this.dispatchEvent( { type: EngineEvents.ISSUE, issue } ),
|
|
164
167
|
} );
|
|
165
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Colour management: what the engine renders in, shows it as, and hands out.
|
|
171
|
+
*
|
|
172
|
+
* Inert until a host loads an OCIO config — until then the working space is linear
|
|
173
|
+
* Rec.709 and the view transforms are three.js's own seven, exactly as before.
|
|
174
|
+
*/
|
|
175
|
+
this.color = new ColorManagement( { issues: this._issues } );
|
|
176
|
+
setActiveColorManagement( this.color );
|
|
177
|
+
|
|
166
178
|
// ── Settings (single source of truth for all render parameters) ──
|
|
167
179
|
this.settings = new RenderSettings(
|
|
168
180
|
{
|
|
@@ -239,6 +251,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
239
251
|
|
|
240
252
|
// Resolution state
|
|
241
253
|
this._resizeDebounceTimer = null;
|
|
254
|
+
// The size the host asked for. The canvas backing store is this × _renderScale, which drops
|
|
255
|
+
// below 1 only while the camera moves.
|
|
256
|
+
this._displayWidth = 0;
|
|
257
|
+
this._displayHeight = 0;
|
|
258
|
+
this._renderScale = 1;
|
|
259
|
+
this._pendingRenderScale = null;
|
|
242
260
|
|
|
243
261
|
// Tracked listeners for clean dispose()
|
|
244
262
|
this._trackedListeners = [];
|
|
@@ -342,6 +360,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
342
360
|
|
|
343
361
|
if ( this.cameraManager.controls ) this.cameraManager.controls.update();
|
|
344
362
|
|
|
363
|
+
this._applyPendingRenderScale();
|
|
364
|
+
|
|
345
365
|
// Animation playback: compute skinned positions and refit BVH.
|
|
346
366
|
// Guard prevents overlapping async refits (fire-and-forget with 1-frame latency).
|
|
347
367
|
if ( this.animationManager?.isPlaying && ! this._animRefitInFlight ) {
|
|
@@ -590,6 +610,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
590
610
|
|
|
591
611
|
this._issues.detach(); // onIssue captures `this`; see IssueLog.detach()
|
|
592
612
|
|
|
613
|
+
// Holds the renderer and the issue log, both of which outlive it otherwise.
|
|
614
|
+
this.color?.dispose();
|
|
615
|
+
this.color = null;
|
|
616
|
+
this._textureReadback?.dispose();
|
|
617
|
+
this._textureReadback = null;
|
|
618
|
+
|
|
593
619
|
this.interactionManager?.deselect?.();
|
|
594
620
|
this.transformManager?.detach?.();
|
|
595
621
|
|
|
@@ -764,9 +790,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
764
790
|
* by reference, and any ancestor transform is baked in. Later edits to `object3d` do not
|
|
765
791
|
* reach the render — mutate {@link sceneModel}, then {@link refitBVH}/{@link refitBLASes}.
|
|
766
792
|
*
|
|
767
|
-
* Lights
|
|
768
|
-
*
|
|
769
|
-
* put into world metres on the copy. See `LightUnits.js`.
|
|
793
|
+
* Lights keep three.js units — `RectAreaLight.intensity` in nits, point/spot in candela — and are
|
|
794
|
+
* converted to radiant power on the copy. `areaLightIntensityScale` does not apply here.
|
|
770
795
|
*
|
|
771
796
|
* @param {import('three').Object3D} object3d - The Object3D to render; left untouched.
|
|
772
797
|
* @param {string} [name='object3d'] - Display name for the object
|
|
@@ -1813,9 +1838,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1813
1838
|
if ( ! this._isRenderSizeSupported( width, height ) ) return;
|
|
1814
1839
|
|
|
1815
1840
|
this.renderer.setPixelRatio( 1.0 );
|
|
1816
|
-
this.
|
|
1817
|
-
this.cameraManager.camera.aspect = width / height;
|
|
1818
|
-
this.cameraManager.camera.updateProjectionMatrix();
|
|
1841
|
+
this._setDisplaySize( width, height );
|
|
1819
1842
|
|
|
1820
1843
|
const lastW = this.denoisingManager?._lastRenderWidth ?? 0;
|
|
1821
1844
|
const lastH = this.denoisingManager?._lastRenderHeight ?? 0;
|
|
@@ -1834,7 +1857,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1834
1857
|
|
|
1835
1858
|
if ( ! this._isRenderSizeSupported( renderWidth, renderHeight ) ) return;
|
|
1836
1859
|
|
|
1837
|
-
this.pipeline?.setSize( renderWidth, renderHeight );
|
|
1860
|
+
this.pipeline?.setSize( this._scaled( renderWidth ), this._scaled( renderHeight ) );
|
|
1861
|
+
// Full size: the denoiser only runs once the camera has stopped.
|
|
1838
1862
|
this.denoisingManager?.setRenderSize( renderWidth, renderHeight );
|
|
1839
1863
|
this.needsReset = true;
|
|
1840
1864
|
|
|
@@ -1857,9 +1881,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1857
1881
|
if ( ! this._isRenderSizeSupported( width, height ) ) return null;
|
|
1858
1882
|
|
|
1859
1883
|
this.renderer.setPixelRatio( 1.0 );
|
|
1860
|
-
this.
|
|
1861
|
-
this.cameraManager.camera.aspect = width / height;
|
|
1862
|
-
this.cameraManager.camera.updateProjectionMatrix();
|
|
1884
|
+
this._setDisplaySize( width, height );
|
|
1863
1885
|
|
|
1864
1886
|
clearTimeout( this._resizeDebounceTimer );
|
|
1865
1887
|
this._applyRenderResize( width, height );
|
|
@@ -1868,6 +1890,66 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1868
1890
|
|
|
1869
1891
|
}
|
|
1870
1892
|
|
|
1893
|
+
_scaled( size ) {
|
|
1894
|
+
|
|
1895
|
+
return Math.max( 1, Math.round( size * this._renderScale ) );
|
|
1896
|
+
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// The wavefront takes its resolution from the canvas backing store, so that is what has to shrink.
|
|
1900
|
+
// updateStyle=false keeps the CSS size, and the browser stretches the smaller frame over it.
|
|
1901
|
+
_setDisplaySize( width, height ) {
|
|
1902
|
+
|
|
1903
|
+
this._displayWidth = width;
|
|
1904
|
+
this._displayHeight = height;
|
|
1905
|
+
|
|
1906
|
+
this.renderer.setSize( this._scaled( width ), this._scaled( height ), false );
|
|
1907
|
+
this.cameraManager.camera.aspect = width / height;
|
|
1908
|
+
this.cameraManager.camera.updateProjectionMatrix();
|
|
1909
|
+
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// OIDN as the live denoiser rebuilds its network on every size change, so it keeps full size.
|
|
1913
|
+
_interactionRenderScale() {
|
|
1914
|
+
|
|
1915
|
+
if ( this.denoisingManager?.continuousDenoise ) return 1;
|
|
1916
|
+
const scale = Number( this.settings.get( 'interactionRenderScale' ) );
|
|
1917
|
+
return scale > 0 ? Math.min( 1, Math.max( 0.125, scale ) ) : 1;
|
|
1918
|
+
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// Interaction can start inside PathTracer.render(), where resizing would pull textures out from
|
|
1922
|
+
// under the frame, so the change waits for the next frame boundary. No wake(): the move that
|
|
1923
|
+
// started it already woke the loop, and waking from inside render() would re-enter it.
|
|
1924
|
+
_requestRenderScale( scale ) {
|
|
1925
|
+
|
|
1926
|
+
this._pendingRenderScale = scale;
|
|
1927
|
+
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
_applyPendingRenderScale() {
|
|
1931
|
+
|
|
1932
|
+
if ( this._pendingRenderScale !== null ) this._applyRenderScale( this._pendingRenderScale );
|
|
1933
|
+
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
_applyRenderScale( scale ) {
|
|
1937
|
+
|
|
1938
|
+
this._pendingRenderScale = null;
|
|
1939
|
+
if ( this._disposed || scale === this._renderScale ) return;
|
|
1940
|
+
|
|
1941
|
+
this._renderScale = scale;
|
|
1942
|
+
if ( ! this._displayWidth || ! this._displayHeight ) return;
|
|
1943
|
+
|
|
1944
|
+
this._setDisplaySize( this._displayWidth, this._displayHeight );
|
|
1945
|
+
this.pipeline?.setSize( this._scaled( this._displayWidth ), this._scaled( this._displayHeight ) );
|
|
1946
|
+
// History from the other size would reproject garbage.
|
|
1947
|
+
this.pipeline?.eventBus.emit( 'asvgf:reset' );
|
|
1948
|
+
this.pipeline?.eventBus.emit( 'denoiser:reset' );
|
|
1949
|
+
this.needsReset = true;
|
|
1950
|
+
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1871
1953
|
// ═══════════════════════════════════════════════════════════════
|
|
1872
1954
|
// Mode Configuration
|
|
1873
1955
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1890,22 +1972,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1890
1972
|
|
|
1891
1973
|
// Anything with a SETTING_ROUTES entry must go through settings, not setUniform: set() early-returns on
|
|
1892
1974
|
// `prev === value`, so a uniform written behind the map leaves it stale and the next set() silently no-ops.
|
|
1893
|
-
this.settings.setMany( {
|
|
1894
|
-
maxSamples: config.maxSamples,
|
|
1895
|
-
maxBounces: config.bounces,
|
|
1896
|
-
transmissiveBounces: config.transmissiveBounces,
|
|
1897
|
-
maxSubsurfaceSteps: config.maxSubsurfaceSteps,
|
|
1898
|
-
enableAlphaShadows: config.enableAlphaShadows ?? false,
|
|
1899
|
-
// Tier-1 convergence early-stop
|
|
1900
|
-
useAdaptiveSampling: config.useAdaptiveSampling ?? false,
|
|
1901
|
-
noiseThreshold: config.noiseThreshold ?? DEFAULT_STATE.noiseThreshold,
|
|
1902
|
-
adaptiveStopFraction: config.adaptiveStopFraction ?? DEFAULT_STATE.adaptiveStopFraction,
|
|
1903
|
-
adaptiveMinSamples: config.adaptiveMinSamples ?? DEFAULT_STATE.adaptiveMinSamples,
|
|
1904
|
-
// Tier-2 per-pixel freeze
|
|
1905
|
-
usePixelFreeze: config.usePixelFreeze ?? false,
|
|
1906
|
-
pixelFreezeThreshold: config.pixelFreezeThreshold ?? DEFAULT_STATE.pixelFreezeThreshold,
|
|
1907
|
-
pixelFreezeStability: config.pixelFreezeStability ?? DEFAULT_STATE.pixelFreezeStability,
|
|
1908
|
-
}, { silent: true, source: SETTING_SOURCE.MODE_PRESET } );
|
|
1975
|
+
this.settings.setMany( modePresetSettings( config ), { silent: true, source: SETTING_SOURCE.MODE_PRESET } );
|
|
1909
1976
|
|
|
1910
1977
|
// renderMode has no SETTING_ROUTES entry
|
|
1911
1978
|
this.stages.pathTracer?.setUniform( 'renderMode', parseInt( config.renderMode ) );
|
|
@@ -2015,7 +2082,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2015
2082
|
* - `_bounceEarlyExitThreshold` / `_useDynamicDispatch` both consume the async
|
|
2016
2083
|
* survivor curve. Kernels bind on ENTERING_COUNT, so an under-sized grid silently
|
|
2017
2084
|
* drops rays, and the frame a readback lands on is GPU-scheduled.
|
|
2018
|
-
* - `interactionModeEnabled` is a 100 ms timer that
|
|
2085
|
+
* - `interactionModeEnabled` is a 100 ms timer that lowers the render resolution, disables
|
|
2019
2086
|
* accumulation and freezes frameCount; it engages on the very first frame.
|
|
2020
2087
|
* - auto-focus raycasts per frame and auto-exposure adapts off `performance.now()`.
|
|
2021
2088
|
*
|
|
@@ -2263,15 +2330,23 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2263
2330
|
* the viewport shows.
|
|
2264
2331
|
*
|
|
2265
2332
|
* @param {Object} [options]
|
|
2266
|
-
* @param {
|
|
2333
|
+
* @param {string} [options.colorSpace='srgb'] - `'srgb'` for display bytes through the active
|
|
2334
|
+
* view transform, `'linear'` for the raw working-space accumulation, or the name of a colour
|
|
2335
|
+
* space in the loaded OCIO config for a delivery buffer (float, e.g. `'ACES2065-1'`)
|
|
2267
2336
|
* @param {boolean} [options.preserveAlpha=false] - srgb only
|
|
2337
|
+
* @param {'accumulation'|'display'} [options.source='accumulation'] - `'display'` reads what the
|
|
2338
|
+
* viewport is showing — denoised when a denoiser has run, without bloom — instead of the raw
|
|
2339
|
+
* accumulation
|
|
2268
2340
|
* @returns {Promise<{data: Float32Array|Uint8ClampedArray, width: number, height: number, colorSpace: string}>}
|
|
2269
2341
|
*/
|
|
2270
|
-
async renderToBuffer( { colorSpace = 'srgb', preserveAlpha = false } = {} ) {
|
|
2342
|
+
async renderToBuffer( { colorSpace = 'srgb', preserveAlpha = false, source = 'accumulation' } = {} ) {
|
|
2271
2343
|
|
|
2272
|
-
|
|
2344
|
+
const named = colorSpace !== 'linear' && colorSpace !== 'srgb';
|
|
2345
|
+
if ( named && ! this.color?.hasConfig ) {
|
|
2273
2346
|
|
|
2274
|
-
throw new Error(
|
|
2347
|
+
throw new Error(
|
|
2348
|
+
`renderToBuffer: colorSpace must be 'linear' or 'srgb' without a colour config loaded, got "${colorSpace}"`
|
|
2349
|
+
);
|
|
2275
2350
|
|
|
2276
2351
|
}
|
|
2277
2352
|
|
|
@@ -2282,9 +2357,27 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2282
2357
|
// The pool over-allocates to the reserve, so the texture is larger than the frame.
|
|
2283
2358
|
const { width, height } = stage;
|
|
2284
2359
|
|
|
2285
|
-
const linear =
|
|
2360
|
+
const linear = source === 'display'
|
|
2361
|
+
? await this._readDisplaySource( target, width, height )
|
|
2362
|
+
: await this.renderer.readRenderTargetPixelsAsync( target, 0, 0, width, height, 0 );
|
|
2286
2363
|
|
|
2287
|
-
|
|
2364
|
+
// `colorSpace` stays 'linear' — callers branch on it. `workingSpace` is the new, additive
|
|
2365
|
+
// answer to "linear in what primaries", which only means something once a config is loaded.
|
|
2366
|
+
if ( colorSpace === 'linear' ) {
|
|
2367
|
+
|
|
2368
|
+
return { data: linear, width, height, colorSpace, workingSpace: this.color?.workingSpace ?? null };
|
|
2369
|
+
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
// A named space is a delivery buffer, not a picture: scene-referred float in whatever the
|
|
2373
|
+
// config calls that space. Saving an EXR for a compositor and grading through an sRGB view
|
|
2374
|
+
// on screen are different questions, and this is the one that answers the first.
|
|
2375
|
+
if ( named ) {
|
|
2376
|
+
|
|
2377
|
+
const { rgba, colorSpace: got } = this.color.exportPixels( linear, colorSpace );
|
|
2378
|
+
return { data: rgba, width, height, colorSpace: got };
|
|
2379
|
+
|
|
2380
|
+
}
|
|
2288
2381
|
|
|
2289
2382
|
return {
|
|
2290
2383
|
data: toneMapToRGBA8( linear, {
|
|
@@ -2716,6 +2809,20 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2716
2809
|
|
|
2717
2810
|
}
|
|
2718
2811
|
|
|
2812
|
+
/**
|
|
2813
|
+
* Where a material property's value came from: `'material'` (the three.js material carried
|
|
2814
|
+
* it), `'mapped'` (converted from a Basic/Lambert/Phong/Toon material), `'default'` (the model
|
|
2815
|
+
* never said; filled from MATERIAL_DEFAULTS) or `'host'` (set through setMaterialProperty).
|
|
2816
|
+
* @param {number} materialIndex
|
|
2817
|
+
* @param {string} property
|
|
2818
|
+
* @returns {string|undefined}
|
|
2819
|
+
*/
|
|
2820
|
+
getMaterialPropertySource( materialIndex, property ) {
|
|
2821
|
+
|
|
2822
|
+
return this.stages.pathTracer?.materialData.getMaterialPropertySource( materialIndex, property );
|
|
2823
|
+
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2719
2826
|
setMaterialProperty( materialIndex, property, value ) {
|
|
2720
2827
|
|
|
2721
2828
|
this.stages.pathTracer?.materialData.updateMaterialProperty( materialIndex, property, value );
|
|
@@ -2868,6 +2975,163 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2868
2975
|
|
|
2869
2976
|
}
|
|
2870
2977
|
|
|
2978
|
+
/**
|
|
2979
|
+
* What the Compositor is drawing from, as linear float. Falls back to the accumulation when no
|
|
2980
|
+
* denoiser has published anything, which is also what the viewport shows then.
|
|
2981
|
+
* @private
|
|
2982
|
+
*/
|
|
2983
|
+
async _readDisplaySource( accumulation, width, height ) {
|
|
2984
|
+
|
|
2985
|
+
const context = this.pipeline?.context;
|
|
2986
|
+
const texture = context && this.stages.compositor?.resolveLightTexture( context );
|
|
2987
|
+
if ( ! texture || texture === accumulation.texture ) {
|
|
2988
|
+
|
|
2989
|
+
return await this.renderer.readRenderTargetPixelsAsync( accumulation, 0, 0, width, height, 0 );
|
|
2990
|
+
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
this._textureReadback ??= new TextureReadback( this.renderer );
|
|
2994
|
+
return await this._textureReadback.read( texture, width, height );
|
|
2995
|
+
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
/**
|
|
2999
|
+
* Set what colour space one texture is in, then rebuild so it takes effect.
|
|
3000
|
+
*
|
|
3001
|
+
* The per-image colour space every OCIO application offers. Three kinds of answer:
|
|
3002
|
+
* - `null` — automatic: tag, override, the config's file rules, then three.js
|
|
3003
|
+
* - `'srgb'`/`'linear'` — three.js's own two, which work with no config at all
|
|
3004
|
+
* - a config space — anything the loaded config names, including log and camera spaces
|
|
3005
|
+
*
|
|
3006
|
+
* A config space is carried as `userData.ocioColorSpace`, and the texture's three.js colour space
|
|
3007
|
+
* is set to match the colour pool while it is, so harmonization leaves the stored bytes as the
|
|
3008
|
+
* author wrote them — the named transform has to read them raw. The original is kept and put
|
|
3009
|
+
* back when the choice is cleared.
|
|
3010
|
+
*
|
|
3011
|
+
* @param {import('three').Texture} texture
|
|
3012
|
+
* @param {?string} choice
|
|
3013
|
+
*/
|
|
3014
|
+
async setTextureColorSpace( texture, choice ) {
|
|
3015
|
+
|
|
3016
|
+
if ( ! texture?.isTexture ) throw new Error( 'setTextureColorSpace wants a three.js texture' );
|
|
3017
|
+
|
|
3018
|
+
const ud = texture.userData ?? ( texture.userData = {} );
|
|
3019
|
+
if ( ud.__rayzeeOriginalColorSpace === undefined ) ud.__rayzeeOriginalColorSpace = texture.colorSpace;
|
|
3020
|
+
const original = ud.__rayzeeOriginalColorSpace;
|
|
3021
|
+
|
|
3022
|
+
delete ud.ocioColorSpace;
|
|
3023
|
+
delete ud.__rayzeeColorSpaceChoice;
|
|
3024
|
+
|
|
3025
|
+
if ( choice === null || choice === undefined ) {
|
|
3026
|
+
|
|
3027
|
+
texture.colorSpace = original;
|
|
3028
|
+
delete ud.__rayzeeOriginalColorSpace;
|
|
3029
|
+
|
|
3030
|
+
} else if ( choice === 'srgb' || choice === 'linear' ) {
|
|
3031
|
+
|
|
3032
|
+
texture.colorSpace = choice === 'srgb' ? SRGBColorSpace : LinearSRGBColorSpace;
|
|
3033
|
+
ud.__rayzeeColorSpaceChoice = choice;
|
|
3034
|
+
|
|
3035
|
+
} else {
|
|
3036
|
+
|
|
3037
|
+
if ( ! this.color?.hasConfig ) throw new Error( `"${choice}" needs a colour config loaded` );
|
|
3038
|
+
ud.ocioColorSpace = choice;
|
|
3039
|
+
texture.colorSpace = SRGBColorSpace;
|
|
3040
|
+
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
texture.needsUpdate = true;
|
|
3044
|
+
if ( this.stages.pathTracer?.sdfs ) await this.rebuildMaterials();
|
|
3045
|
+
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
/**
|
|
3049
|
+
* Load a colour config, keeping the scene consistent with it.
|
|
3050
|
+
*
|
|
3051
|
+
* Prefer this to `app.color.loadConfig()` whenever a scene is loaded. A working space adopted
|
|
3052
|
+
* under the previous config is undone first, *while that config is still loaded* — the
|
|
3053
|
+
* environment is converted in place, and only the config that converted it can convert it
|
|
3054
|
+
* back. With `adoptWorkingSpace` the scene is rebuilt in the new space afterwards.
|
|
3055
|
+
*
|
|
3056
|
+
* @param {Object} options - as `ColorManagement.loadConfig`
|
|
3057
|
+
* @returns {Promise<Object>} the config description
|
|
3058
|
+
*/
|
|
3059
|
+
async loadColorConfig( options = {} ) {
|
|
3060
|
+
|
|
3061
|
+
await this._leaveColorWorkingSpace();
|
|
3062
|
+
const shown = getViewTransform( this.renderer?.toneMapping );
|
|
3063
|
+
const described = await this.color.loadConfig( options );
|
|
3064
|
+
if ( this.color.workingSpaceAdopted ) await this.applyColorWorkingSpace();
|
|
3065
|
+
// A baked view the config kept is still what is on screen.
|
|
3066
|
+
const kept = shown?.source === 'ocio' && getViewTransform( this.renderer?.toneMapping ) === shown;
|
|
3067
|
+
if ( ! kept || this.color.workingSpaceAdopted ) this.reset();
|
|
3068
|
+
return described;
|
|
3069
|
+
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
/** Unload the colour config, putting the scene back into linear Rec.709 first. */
|
|
3073
|
+
async unloadColorConfig() {
|
|
3074
|
+
|
|
3075
|
+
await this._leaveColorWorkingSpace();
|
|
3076
|
+
this.color.unloadConfig();
|
|
3077
|
+
this.reset();
|
|
3078
|
+
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
/** @private */
|
|
3082
|
+
async _leaveColorWorkingSpace() {
|
|
3083
|
+
|
|
3084
|
+
if ( ! this.color?.workingSpaceAdopted ) return;
|
|
3085
|
+
this.color.setWorkingSpace( null );
|
|
3086
|
+
await this.applyColorWorkingSpace();
|
|
3087
|
+
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
/**
|
|
3091
|
+
* Rebuild everything the working space touches, after it has been changed.
|
|
3092
|
+
*
|
|
3093
|
+
* Changing what the engine renders in is not a display setting — it changes what every texture,
|
|
3094
|
+
* tint and light colour already loaded *means*. Textures and materials are re-packed from their
|
|
3095
|
+
* pristine three.js sources, so they convert cleanly; the environment is converted where it
|
|
3096
|
+
* lies, from whichever space it currently holds, and its importance-sampling table is rebuilt
|
|
3097
|
+
* because its pixels moved.
|
|
3098
|
+
*
|
|
3099
|
+
* Call this after `app.color.setWorkingSpace()`. Doing nothing instead leaves a scene half in
|
|
3100
|
+
* one space and half in another, which reads as a colour cast with no obvious cause.
|
|
3101
|
+
*/
|
|
3102
|
+
async applyColorWorkingSpace() {
|
|
3103
|
+
|
|
3104
|
+
const env = this.environmentManager?.getEnvironmentTexture?.();
|
|
3105
|
+
let envChanged = false;
|
|
3106
|
+
|
|
3107
|
+
if ( env && this.color?.hasConfig ) {
|
|
3108
|
+
|
|
3109
|
+
try {
|
|
3110
|
+
|
|
3111
|
+
envChanged = this.color.convertTexturePixels( env );
|
|
3112
|
+
|
|
3113
|
+
} catch ( error ) {
|
|
3114
|
+
|
|
3115
|
+
log.warn( `environment colour conversion failed: ${error.message}` );
|
|
3116
|
+
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
// Nothing to rebuild before a scene exists; anything loaded later converts on its way in.
|
|
3122
|
+
if ( this.stages.pathTracer?.sdfs ) {
|
|
3123
|
+
|
|
3124
|
+
await this.rebuildMaterials();
|
|
3125
|
+
this._uploadEmissivePayload( this._sdf?.rebuildEmissiveColors?.() ?? null );
|
|
3126
|
+
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
if ( envChanged ) await this.environmentManager?.buildEnvironmentCDF?.();
|
|
3130
|
+
|
|
3131
|
+
this.reset();
|
|
3132
|
+
|
|
3133
|
+
}
|
|
3134
|
+
|
|
2871
3135
|
/**
|
|
2872
3136
|
* Full material rebuild (required after texture changes).
|
|
2873
3137
|
* @param {import('three').Scene} [scene]
|
|
@@ -2995,6 +3259,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2995
3259
|
this.renderer.toneMappingExposure = 1.0;
|
|
2996
3260
|
this.renderer.setPixelRatio( 1.0 );
|
|
2997
3261
|
|
|
3262
|
+
// Hands every registered view transform to the renderer and keeps `outputColorSpace` in
|
|
3263
|
+
// step with whichever is selected — an OCIO view already encoded for its display must not
|
|
3264
|
+
// be encoded a second time by the output pass.
|
|
3265
|
+
this.color.attachRenderer( this.renderer );
|
|
3266
|
+
|
|
2998
3267
|
}
|
|
2999
3268
|
|
|
3000
3269
|
_initCameraManager() {
|
|
@@ -3028,6 +3297,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3028
3297
|
this._addTrackedListener( this.cameraManager.controls, 'change', () => {
|
|
3029
3298
|
|
|
3030
3299
|
this.needsReset = true;
|
|
3300
|
+
// Here rather than in render(), so the first frame of the move is already at the lower resolution.
|
|
3301
|
+
this.stages.pathTracer?.enterInteractionMode();
|
|
3031
3302
|
this.wake();
|
|
3032
3303
|
|
|
3033
3304
|
} );
|
|
@@ -3054,8 +3325,15 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3054
3325
|
|
|
3055
3326
|
const initRenderW = this.canvas.clientWidth || 1;
|
|
3056
3327
|
const initRenderH = this.canvas.clientHeight || 1;
|
|
3328
|
+
this._displayWidth = initRenderW;
|
|
3329
|
+
this._displayHeight = initRenderH;
|
|
3057
3330
|
this.pipeline.setSize( initRenderW, initRenderH );
|
|
3058
3331
|
|
|
3332
|
+
this.pipeline.eventBus.on( 'pathtracer:interactionStart', () => this._requestRenderScale( this._interactionRenderScale() ) );
|
|
3333
|
+
// Never fires inside a frame (a timer, or interaction mode being switched off), so it applies at once —
|
|
3334
|
+
// renderFrames() and the video renderer drive pipeline.render() without passing through animate().
|
|
3335
|
+
this.pipeline.eventBus.on( 'pathtracer:interactionEnd', () => this._applyRenderScale( 1 ) );
|
|
3336
|
+
|
|
3059
3337
|
}
|
|
3060
3338
|
|
|
3061
3339
|
async _initManagers() {
|
|
@@ -3176,6 +3454,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3176
3454
|
reconcileCompletion: () => this._reconcileCompletion(),
|
|
3177
3455
|
denoisingManager: this.denoisingManager,
|
|
3178
3456
|
cameraManager: this.cameraManager,
|
|
3457
|
+
onInteractionRenderScale: () => {
|
|
3458
|
+
|
|
3459
|
+
if ( this.stages.pathTracer?.interactionMode ) this._requestRenderScale( this._interactionRenderScale() );
|
|
3460
|
+
|
|
3461
|
+
},
|
|
3179
3462
|
} );
|
|
3180
3463
|
|
|
3181
3464
|
this.renderer.toneMappingExposure = this.settings.get( 'exposure' ) ?? 1.0;
|
|
@@ -19,7 +19,6 @@ import { getAssetConfig } from '../AssetConfig.js';
|
|
|
19
19
|
import { loadPBRTScene, pickEntryPath } from './PBRT/index.js';
|
|
20
20
|
import { extractSceneMetadata } from './SceneMetadata.js';
|
|
21
21
|
import { ISSUE_CODES, ISSUE_SEVERITY } from '../EngineIssues.js';
|
|
22
|
-
import { bakeAreaLightScale, LUMENS_PER_WATT } from '../LightUnits.js';
|
|
23
22
|
import { getRenderProfile } from '../EngineDefaults.js';
|
|
24
23
|
|
|
25
24
|
// Define supported file formats
|
|
@@ -68,43 +67,19 @@ function standInForSplit( source ) {
|
|
|
68
67
|
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
|
|
70
|
+
// Luminous efficacy glTF, three.js and Blender's exporter all assume (PBR_WATTS_TO_LUMENS).
|
|
71
|
+
const LUMENS_PER_WATT = 683;
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* patched GLTFExporter writes the RectAreaLight into the node's `extras` and leaves the node
|
|
76
|
-
* itself carrying the transform. `intensity` is three.js radiance, which is what the engine
|
|
77
|
-
* stores, so the number is used as authored; only the rectangle needs normalising to world
|
|
78
|
-
* metres (see `bakeAreaLightScale`).
|
|
79
|
-
*
|
|
80
|
-
* `power`, `position`, `rotation` and `direction` are present in the record and deliberately
|
|
81
|
-
* unused. The node's matrix already carries the transform, `direction` is not even normalised,
|
|
82
|
-
* and `power` was written from the light's UNSCALED dimensions — on 24155522.glb's window light,
|
|
83
|
-
* whose ancestor scale is non-uniform (1.6 × 1.2 m in world, not 1.2 × 1.2), it is 25 % low.
|
|
84
|
-
*
|
|
85
|
-
* @returns {RectAreaLight|null} null when the record carries no usable size or intensity.
|
|
86
|
-
*/
|
|
87
|
-
function areaLightFromPlaceholder( node, record ) {
|
|
73
|
+
// three.js nits → the engine's radiant power, inverting areaLightRadiance.
|
|
74
|
+
function areaLightPowerFactor( node, width, height, userData ) {
|
|
88
75
|
|
|
89
|
-
|
|
90
|
-
const height = Number( record.height );
|
|
91
|
-
const intensity = Number( record.intensity );
|
|
92
|
-
if ( ! ( width > 0 ) || ! ( height > 0 ) || ! Number.isFinite( intensity ) ) return null;
|
|
76
|
+
if ( ! ( userData.normalize ?? true ) ) return Math.PI;
|
|
93
77
|
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
light.userData.normalize = record.normalize ?? true;
|
|
100
|
-
light.userData.spread = Number.isFinite( record.spread ) ? record.spread : Math.PI;
|
|
101
|
-
light.userData.shape = shape;
|
|
102
|
-
|
|
103
|
-
// Parented before baking: the bake needs the node's world scale, and on the append path
|
|
104
|
-
// nothing has refreshed the tree yet.
|
|
105
|
-
node.add( light );
|
|
106
|
-
bakeAreaLightScale( light );
|
|
107
|
-
return light;
|
|
78
|
+
const shapeFactor = userData.shape === 'ellipse' || userData.shape === 'disk' ? Math.PI / 4 : 1;
|
|
79
|
+
const scale = node.getWorldScale( new Vector3() );
|
|
80
|
+
// abs: a mirrored ancestor decomposes negative; the serializer takes the area as |u × v|.
|
|
81
|
+
const factor = Math.PI * shapeFactor * Math.abs( width * scale.x * height * scale.y );
|
|
82
|
+
return Number.isFinite( factor ) ? factor : Math.PI;
|
|
108
83
|
|
|
109
84
|
}
|
|
110
85
|
|
|
@@ -876,8 +851,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
876
851
|
this.targetModel = group;
|
|
877
852
|
|
|
878
853
|
// The light's own orientation and `scale` are already baked into the texture, so the
|
|
879
|
-
// scene is only correct at rotation 0 / intensity 1
|
|
880
|
-
// default 270° rotation lands on top and the sky sits 90° from where pbrt puts it.
|
|
854
|
+
// scene is only correct at rotation 0 / intensity 1 — pinned, whatever the host's defaults.
|
|
881
855
|
if ( environment?.texture ) this.sceneMetadata = { environment: { rotation: 0, intensity: 1 } };
|
|
882
856
|
|
|
883
857
|
updateLoading( { isLoading: true, status: 'Processing PBRT geometry...', progress: 10 } );
|
|
@@ -2109,18 +2083,22 @@ export class AssetLoader extends EventDispatcher {
|
|
|
2109
2083
|
|
|
2110
2084
|
const userData = object.userData;
|
|
2111
2085
|
|
|
2112
|
-
// three.js
|
|
2113
|
-
|
|
2114
|
-
|
|
2086
|
+
// An adopted light carries three.js units; convert as point/spot are below.
|
|
2087
|
+
if ( object.isRectAreaLight && ! userData.__radianceConverted ) {
|
|
2088
|
+
|
|
2089
|
+
object.intensity *= areaLightPowerFactor( object, object.width, object.height, userData );
|
|
2090
|
+
userData.__radianceConverted = true;
|
|
2091
|
+
|
|
2092
|
+
}
|
|
2115
2093
|
|
|
2116
2094
|
// Punctual lights arrive photometric: glTF (and three.js) state point/spot in
|
|
2117
|
-
// candela and directional in lux. The engine
|
|
2118
|
-
//
|
|
2119
|
-
//
|
|
2120
|
-
// 683x too bright.
|
|
2095
|
+
// candela and directional in lux. The engine is radiometric Blender Watts, which
|
|
2096
|
+
// LightSerializer turns into W/sr with ÷4π, so the luminous efficacy has to be
|
|
2097
|
+
// divided back out — Blender's own glTF importer does exactly this. Skipping it
|
|
2098
|
+
// rendered every Blender lamp 683x too bright.
|
|
2121
2099
|
if ( ( object.isPointLight || object.isSpotLight ) && ! userData.__candelaConverted ) {
|
|
2122
2100
|
|
|
2123
|
-
object.intensity
|
|
2101
|
+
object.intensity *= 4 * Math.PI / LUMENS_PER_WATT;
|
|
2124
2102
|
userData.__candelaConverted = true;
|
|
2125
2103
|
|
|
2126
2104
|
}
|
|
@@ -2132,17 +2110,31 @@ export class AssetLoader extends EventDispatcher {
|
|
|
2132
2110
|
|
|
2133
2111
|
}
|
|
2134
2112
|
|
|
2135
|
-
//
|
|
2136
|
-
|
|
2137
|
-
|
|
2113
|
+
// Process ceiling lights
|
|
2114
|
+
if ( object.name.startsWith( 'RectAreaLightPlaceholder' ) &&
|
|
2115
|
+
userData.name
|
|
2116
|
+
// && userData.name.includes( "ceilingLight" )
|
|
2117
|
+
) {
|
|
2118
|
+
|
|
2119
|
+
if ( userData.type === 'RectAreaLight' ) {
|
|
2120
|
+
|
|
2121
|
+
const normalize = userData.normalize ?? true;
|
|
2122
|
+
const shape = userData.shape ?? 'rectangle';
|
|
2123
|
+
const power = userData.intensity * areaLightPowerFactor( object, userData.width, userData.height, userData );
|
|
2124
|
+
const light = new RectAreaLight(
|
|
2125
|
+
new Color( ...userData.color ),
|
|
2126
|
+
power * this._profile.areaLightIntensityScale,
|
|
2127
|
+
userData.width,
|
|
2128
|
+
userData.height
|
|
2129
|
+
);
|
|
2130
|
+
light.userData.normalize = normalize;
|
|
2131
|
+
light.userData.spread = Number.isFinite( userData.spread ) ? userData.spread : Math.PI;
|
|
2132
|
+
light.userData.shape = shape;
|
|
2133
|
+
light.userData.__radianceConverted = true; // already power, and traverse() reaches it
|
|
2134
|
+
light.name = userData.name;
|
|
2135
|
+
object.add( light );
|
|
2138
2136
|
|
|
2139
|
-
|
|
2140
|
-
if ( ! light ) this._issues?.record(
|
|
2141
|
-
ISSUE_CODES.LIGHT_PLACEHOLDER_INVALID,
|
|
2142
|
-
`area-light placeholder "${object.name}" carries no usable size or intensity`,
|
|
2143
|
-
{ node: object.name, record: userData },
|
|
2144
|
-
ISSUE_SEVERITY.WARNING,
|
|
2145
|
-
);
|
|
2137
|
+
}
|
|
2146
2138
|
|
|
2147
2139
|
}
|
|
2148
2140
|
|