rayzee 8.4.1 → 8.5.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 +1 -5
- package/dist/rayzee.es.js +1975 -1981
- package/dist/rayzee.umd.js +64 -64
- package/package.json +1 -1
- package/src/EngineDefaults.js +29 -1
- package/src/EngineIssues.js +0 -1
- package/src/PathTracerApp.js +91 -28
- package/src/Processor/AssetLoader.js +46 -53
- package/src/Processor/CameraOptimizer.js +22 -6
- package/src/Processor/LightSerializer.js +9 -17
- package/src/RenderSettings.js +9 -1
- package/src/Stages/PathTracer.js +19 -4
- package/src/Stages/PathTracerStage.js +10 -17
- package/src/index.js +0 -1
- package/src/managers/DenoisingManager.js +3 -7
- package/src/managers/LightManager.js +17 -15
- package/src/managers/VideoRenderManager.js +9 -9
- package/src/LightUnits.js +0 -122
|
@@ -298,18 +298,6 @@ export class PathTracerStage extends RenderStage {
|
|
|
298
298
|
const self = this;
|
|
299
299
|
const materialInterface = {
|
|
300
300
|
uniforms: {
|
|
301
|
-
maxBounceCount: {
|
|
302
|
-
get value() {
|
|
303
|
-
|
|
304
|
-
return self.maxBounces.value;
|
|
305
|
-
|
|
306
|
-
},
|
|
307
|
-
set value( v ) {
|
|
308
|
-
|
|
309
|
-
self.maxBounces.value = v;
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
},
|
|
313
301
|
enableAccumulation: {
|
|
314
302
|
get value() {
|
|
315
303
|
|
|
@@ -322,15 +310,15 @@ export class PathTracerStage extends RenderStage {
|
|
|
322
310
|
|
|
323
311
|
}
|
|
324
312
|
},
|
|
325
|
-
|
|
313
|
+
fireflyThreshold: {
|
|
326
314
|
get value() {
|
|
327
315
|
|
|
328
|
-
return self.
|
|
316
|
+
return self.fireflyThreshold.value;
|
|
329
317
|
|
|
330
318
|
},
|
|
331
319
|
set value( v ) {
|
|
332
320
|
|
|
333
|
-
self.
|
|
321
|
+
self.fireflyThreshold.value = v;
|
|
334
322
|
|
|
335
323
|
}
|
|
336
324
|
},
|
|
@@ -351,11 +339,16 @@ export class PathTracerStage extends RenderStage {
|
|
|
351
339
|
|
|
352
340
|
this.cameraOptimizer = new CameraOptimizer( this.renderer, materialInterface, {
|
|
353
341
|
enabled: DEFAULT_STATE.interactionModeEnabled,
|
|
342
|
+
// Nothing that costs light: the app lowers the resolution instead (PathTracerApp._applyRenderScale).
|
|
343
|
+
// The firefly limit grows with sqrt(frame + 1), and every moving frame is frame 0, where it clipped
|
|
344
|
+
// ~10% of an emissive-lit interior's light. ×8 is the limit a still image reaches at 64 samples:
|
|
345
|
+
// lowest large-area error of the values measured, and half the extra grain of turning it off.
|
|
354
346
|
qualitySettings: {
|
|
355
|
-
maxBounceCount: 1,
|
|
356
347
|
enableAccumulation: false,
|
|
357
|
-
|
|
348
|
+
fireflyThreshold: ( threshold ) => threshold * 8,
|
|
358
349
|
},
|
|
350
|
+
onEnter: () => this.emit( 'pathtracer:interactionStart' ),
|
|
351
|
+
onExit: () => this.emit( 'pathtracer:interactionEnd' ),
|
|
359
352
|
onReset: () => {
|
|
360
353
|
|
|
361
354
|
this.reset();
|
package/src/index.js
CHANGED
|
@@ -18,7 +18,6 @@ export { EngineEvents } from './EngineEvents.js';
|
|
|
18
18
|
export { renderHeadless, openHeadless, captureHeadless } from './Headless.js';
|
|
19
19
|
|
|
20
20
|
// Degradation contract — what the engine survived instead of failing on
|
|
21
|
-
export { LUMENS_PER_WATT, areaLightArea, bakeAreaLightScale, lightPower, preserveLightPower, setLightPower } from './LightUnits.js';
|
|
22
21
|
export { ISSUE_CODES, ISSUE_SEVERITY, IssueLog, EngineIssueError } from './EngineIssues.js';
|
|
23
22
|
|
|
24
23
|
// Settings provenance + viewer-vs-physical tuning
|
|
@@ -836,14 +836,10 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
836
836
|
if ( this.upscalerCanvas ) this.upscalerCanvas.style.display = 'none';
|
|
837
837
|
if ( this.mainCanvas ) this.mainCanvas.style.opacity = '1';
|
|
838
838
|
|
|
839
|
-
|
|
839
|
+
// The nominal size, not the path tracer's: that one is smaller while the camera moves.
|
|
840
|
+
if ( wasShowing && this._lastRenderWidth && this._lastRenderHeight ) {
|
|
840
841
|
|
|
841
|
-
|
|
842
|
-
if ( stage?.width && stage?.height ) {
|
|
843
|
-
|
|
844
|
-
this.dispatchEvent( { type: 'resolution_changed', width: stage.width, height: stage.height } );
|
|
845
|
-
|
|
846
|
-
}
|
|
842
|
+
this.dispatchEvent( { type: 'resolution_changed', width: this._lastRenderWidth, height: this._lastRenderHeight } );
|
|
847
843
|
|
|
848
844
|
}
|
|
849
845
|
|
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
EventDispatcher, DirectionalLight, PointLight, SpotLight, RectAreaLight,
|
|
3
3
|
Object3D, MathUtils
|
|
4
4
|
} from 'three';
|
|
5
|
-
import { bakeAreaLightScale, lightPower, setLightPower } from '../LightUnits.js';
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Manages scene lights: add, remove, transfer from mesh scene to WebGPU
|
|
@@ -38,13 +37,12 @@ export class LightManager extends EventDispatcher {
|
|
|
38
37
|
*/
|
|
39
38
|
addLight( type ) {
|
|
40
39
|
|
|
41
|
-
// Authored as Blender Power in Watts (the Sun as W/m²), converted to the stored
|
|
42
|
-
// quantity by setLightPower() once the light exists — see LightUnits.js.
|
|
43
40
|
const defaults = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
// Power in Watts (Blender-style) for point/spot/area; Sun is W/m² strength.
|
|
42
|
+
DirectionalLight: { position: [ 1, 1, 1 ], intensity: 1.0, color: '#ffffff' },
|
|
43
|
+
PointLight: { position: [ 0, 2, 0 ], intensity: 1000, color: '#ffffff' },
|
|
44
|
+
SpotLight: { position: [ 0, 1, 0 ], intensity: 1000, color: '#ffffff', angle: 15 },
|
|
45
|
+
RectAreaLight: { position: [ 0, 2, 0 ], intensity: 100, color: '#ffffff', width: 2, height: 2 }
|
|
48
46
|
};
|
|
49
47
|
|
|
50
48
|
const props = defaults[ type ];
|
|
@@ -54,17 +52,17 @@ export class LightManager extends EventDispatcher {
|
|
|
54
52
|
|
|
55
53
|
if ( type === 'DirectionalLight' ) {
|
|
56
54
|
|
|
57
|
-
light = new DirectionalLight( props.color,
|
|
55
|
+
light = new DirectionalLight( props.color, props.intensity );
|
|
58
56
|
light.position.fromArray( props.position );
|
|
59
57
|
|
|
60
58
|
} else if ( type === 'PointLight' ) {
|
|
61
59
|
|
|
62
|
-
light = new PointLight( props.color,
|
|
60
|
+
light = new PointLight( props.color, props.intensity );
|
|
63
61
|
light.position.fromArray( props.position );
|
|
64
62
|
|
|
65
63
|
} else if ( type === 'SpotLight' ) {
|
|
66
64
|
|
|
67
|
-
light = new SpotLight( props.color,
|
|
65
|
+
light = new SpotLight( props.color, props.intensity );
|
|
68
66
|
light.position.fromArray( props.position );
|
|
69
67
|
light.angle = MathUtils.degToRad( props.angle );
|
|
70
68
|
const target = new Object3D();
|
|
@@ -73,7 +71,7 @@ export class LightManager extends EventDispatcher {
|
|
|
73
71
|
|
|
74
72
|
} else if ( type === 'RectAreaLight' ) {
|
|
75
73
|
|
|
76
|
-
light = new RectAreaLight( props.color,
|
|
74
|
+
light = new RectAreaLight( props.color, props.intensity, props.width, props.height );
|
|
77
75
|
light.position.fromArray( props.position );
|
|
78
76
|
light.lookAt( 0, 0, 0 );
|
|
79
77
|
// Blender-style emission defaults: power-normalized, full Lambertian
|
|
@@ -84,8 +82,6 @@ export class LightManager extends EventDispatcher {
|
|
|
84
82
|
|
|
85
83
|
}
|
|
86
84
|
|
|
87
|
-
setLightPower( light, props.power );
|
|
88
|
-
|
|
89
85
|
// Blender-style emission controls common to every light type.
|
|
90
86
|
light.userData.temperature = 6500;
|
|
91
87
|
light.userData.useTemperature = false;
|
|
@@ -182,7 +178,13 @@ export class LightManager extends EventDispatcher {
|
|
|
182
178
|
light.getWorldQuaternion( cloned.quaternion );
|
|
183
179
|
light.getWorldScale( cloned.scale );
|
|
184
180
|
|
|
185
|
-
if ( cloned.isRectAreaLight )
|
|
181
|
+
if ( cloned.isRectAreaLight ) {
|
|
182
|
+
|
|
183
|
+
cloned.width *= cloned.scale.x;
|
|
184
|
+
cloned.height *= cloned.scale.y;
|
|
185
|
+
cloned.scale.set( 1, 1, 1 );
|
|
186
|
+
|
|
187
|
+
}
|
|
186
188
|
|
|
187
189
|
if ( ( light.isSpotLight || light.isDirectionalLight ) && light.target ) {
|
|
188
190
|
|
|
@@ -336,7 +338,7 @@ export class LightManager extends EventDispatcher {
|
|
|
336
338
|
name: light.name,
|
|
337
339
|
type: light.type,
|
|
338
340
|
visible: light.visible,
|
|
339
|
-
intensity:
|
|
341
|
+
intensity: light.intensity,
|
|
340
342
|
color: `#${light.color.getHexString()}`,
|
|
341
343
|
position: [ light.position.x, light.position.y, light.position.z ],
|
|
342
344
|
angle,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { EngineEvents } from '../EngineEvents.js';
|
|
10
|
-
import { PRODUCTION_RENDER_CONFIG } from '../EngineDefaults.js';
|
|
10
|
+
import { PRODUCTION_RENDER_CONFIG, modePresetSettings } from '../EngineDefaults.js';
|
|
11
11
|
import { updateStats, getDisplaySamples } from '../Processor/utils.js';
|
|
12
12
|
|
|
13
13
|
export class VideoRenderManager {
|
|
@@ -273,11 +273,11 @@ export class VideoRenderManager {
|
|
|
273
273
|
_saveState() {
|
|
274
274
|
|
|
275
275
|
const app = this._app;
|
|
276
|
+
const effective = app.settings.getEffective();
|
|
276
277
|
|
|
277
278
|
return {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
transmissiveBounces: app.settings.get( 'transmissiveBounces' ),
|
|
279
|
+
// Everything configureForMode( 'production' ) overwrites, with who set it.
|
|
280
|
+
settings: Object.keys( modePresetSettings( PRODUCTION_RENDER_CONFIG ) ).map( key => [ key, effective[ key ] ] ),
|
|
281
281
|
renderMode: app.stages.pathTracer?.renderMode?.value,
|
|
282
282
|
controlsEnabled: app.cameraManager.controls?.enabled,
|
|
283
283
|
oidnEnabled: app.denoisingManager?.finalDenoise,
|
|
@@ -296,11 +296,11 @@ export class VideoRenderManager {
|
|
|
296
296
|
|
|
297
297
|
const app = this._app;
|
|
298
298
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
299
|
+
for ( const [ key, saved ] of state.settings ) {
|
|
300
|
+
|
|
301
|
+
if ( saved ) app.settings.set( key, saved.value, { silent: true, reset: false, source: saved.source } );
|
|
302
|
+
|
|
303
|
+
}
|
|
304
304
|
|
|
305
305
|
if ( app.stages.pathTracer && state.renderMode !== undefined ) {
|
|
306
306
|
|
package/src/LightUnits.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The engine's light-unit rule, stated once.
|
|
3
|
-
*
|
|
4
|
-
* Every light in the scene graph is a **valid three.js light**: it carries the quantity three.js
|
|
5
|
-
* expects for its type, measured in radiometric (watt-based) units rather than photometric ones.
|
|
6
|
-
*
|
|
7
|
-
* point / spot → radiant intensity W/sr
|
|
8
|
-
* directional → irradiance W/m²
|
|
9
|
-
* rect area → radiance W/(sr·m²), `width`/`height` in world metres, unit scale
|
|
10
|
-
*
|
|
11
|
-
* three.js' shading maths is unit-agnostic — it divides by distance² and integrates a form factor,
|
|
12
|
-
* never caring whether the number it was handed is candela or W/sr. Feeding it radiometric values
|
|
13
|
-
* therefore makes the raster fallback, the light helpers and any host reading `sceneModel` correct
|
|
14
|
-
* without one of them knowing anything about the path tracer.
|
|
15
|
-
*
|
|
16
|
-
* Blender-style **Power in Watts** — what the Lights panel edits — is a view of that, converted
|
|
17
|
-
* here and nowhere else. `LightSerializer` converts the other way when it fills the GPU buffer.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/** Luminous efficacy that glTF, three.js and Blender's exporter all assume (lm/W). */
|
|
21
|
-
export const LUMENS_PER_WATT = 683;
|
|
22
|
-
|
|
23
|
-
/** Emitting area in m², including the ellipse/disk shape factor. Assumes unit world scale. */
|
|
24
|
-
export function areaLightArea( light ) {
|
|
25
|
-
|
|
26
|
-
const shape = light.userData?.shape;
|
|
27
|
-
const shapeFactor = shape === 'ellipse' || shape === 'disk' ? Math.PI / 4 : 1;
|
|
28
|
-
const area = Math.abs( light.width * light.height ) * shapeFactor;
|
|
29
|
-
return Number.isFinite( area ) && area > 0 ? area : 0;
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Blender-style Power for the Lights panel: radiant power in W for point, spot and area lights,
|
|
35
|
-
* and the Sun's strength in W/m² unchanged.
|
|
36
|
-
*/
|
|
37
|
-
export function lightPower( light ) {
|
|
38
|
-
|
|
39
|
-
if ( light.isRectAreaLight ) {
|
|
40
|
-
|
|
41
|
-
const normalize = light.userData?.normalize ?? true;
|
|
42
|
-
return light.intensity * Math.PI * ( normalize ? areaLightArea( light ) : 1 );
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if ( light.isPointLight || light.isSpotLight ) return light.intensity * 4 * Math.PI;
|
|
47
|
-
|
|
48
|
-
return light.intensity;
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Inverse of {@link lightPower} — what the panel writes back. */
|
|
53
|
-
export function setLightPower( light, watts ) {
|
|
54
|
-
|
|
55
|
-
if ( light.isRectAreaLight ) {
|
|
56
|
-
|
|
57
|
-
const normalize = light.userData?.normalize ?? true;
|
|
58
|
-
const denominator = Math.PI * ( normalize ? areaLightArea( light ) : 1 );
|
|
59
|
-
light.intensity = denominator > 0 ? watts / denominator : 0;
|
|
60
|
-
return;
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if ( light.isPointLight || light.isSpotLight ) {
|
|
65
|
-
|
|
66
|
-
light.intensity = watts / ( 4 * Math.PI );
|
|
67
|
-
return;
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
light.intensity = watts;
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Runs `mutate` with the light's Power held constant.
|
|
77
|
-
*
|
|
78
|
-
* Resizing a normalized area light is meant to keep its total power and change its radiance —
|
|
79
|
-
* Blender's Normalize toggle. Since the stored quantity is radiance, that only happens if the
|
|
80
|
-
* edit puts the Power back afterwards. With Normalize off, Power does not depend on the area,
|
|
81
|
-
* so this is a no-op and the same call is still correct.
|
|
82
|
-
*/
|
|
83
|
-
export function preserveLightPower( light, mutate ) {
|
|
84
|
-
|
|
85
|
-
const watts = lightPower( light );
|
|
86
|
-
mutate();
|
|
87
|
-
setLightPower( light, watts );
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Puts an area light's rectangle in world metres with unit scale, leaving its world size and
|
|
93
|
-
* placement untouched.
|
|
94
|
-
*
|
|
95
|
-
* ⚠️ three.js' rasteriser reads `width`/`height` as WORLD dimensions and drops any scale inherited
|
|
96
|
-
* from parent nodes — `RectAreaLightNode` builds the light's basis with `extractRotation`, which
|
|
97
|
-
* normalises the scale away. A light authored as 70 units under a 0.01 node scale therefore rasters
|
|
98
|
-
* as a **70 metre** panel while the path tracer, which uses the world matrix, renders the 0.7 m one
|
|
99
|
-
* it should. Measured on 24001884.glb: 58.7x too bright in the raster view. Baking the scale in and
|
|
100
|
-
* cancelling it on the light itself makes every reader agree.
|
|
101
|
-
*
|
|
102
|
-
* Idempotent: a light already at unit scale is left alone, so re-processing a tree is safe.
|
|
103
|
-
*/
|
|
104
|
-
export function bakeAreaLightScale( light ) {
|
|
105
|
-
|
|
106
|
-
light.updateWorldMatrix( true, false );
|
|
107
|
-
|
|
108
|
-
// Column lengths of the world matrix, read directly so this module needs no three.js import.
|
|
109
|
-
const e = light.matrixWorld.elements;
|
|
110
|
-
const sx = Math.hypot( e[ 0 ], e[ 1 ], e[ 2 ] );
|
|
111
|
-
const sy = Math.hypot( e[ 4 ], e[ 5 ], e[ 6 ] );
|
|
112
|
-
const sz = Math.hypot( e[ 8 ], e[ 9 ], e[ 10 ] );
|
|
113
|
-
|
|
114
|
-
if ( ! Number.isFinite( sx ) || ! Number.isFinite( sy ) || sx === 0 || sy === 0 ) return;
|
|
115
|
-
if ( Math.abs( sx - 1 ) < 1e-9 && Math.abs( sy - 1 ) < 1e-9 ) return;
|
|
116
|
-
|
|
117
|
-
light.width = Math.abs( light.width * sx );
|
|
118
|
-
light.height = Math.abs( light.height * sy );
|
|
119
|
-
light.scale.set( light.scale.x / sx, light.scale.y / sy, sz === 0 ? light.scale.z : light.scale.z / sz );
|
|
120
|
-
light.updateMatrixWorld( true );
|
|
121
|
-
|
|
122
|
-
}
|