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.
@@ -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
- enableEmissiveTriangleSampling: {
313
+ fireflyThreshold: {
326
314
  get value() {
327
315
 
328
- return self.enableEmissiveTriangleSampling.value;
316
+ return self.fireflyThreshold.value;
329
317
 
330
318
  },
331
319
  set value( v ) {
332
320
 
333
- self.enableEmissiveTriangleSampling.value = v;
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
- enableEmissiveTriangleSampling: false,
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
- if ( wasShowing ) {
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
- const stage = this._stages?.pathTracer;
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
- DirectionalLight: { position: [ 1, 1, 1 ], power: 1.0, color: '#ffffff' },
45
- PointLight: { position: [ 0, 2, 0 ], power: 1000, color: '#ffffff' },
46
- SpotLight: { position: [ 0, 1, 0 ], power: 1000, color: '#ffffff', angle: 15 },
47
- RectAreaLight: { position: [ 0, 2, 0 ], power: 100, color: '#ffffff', width: 2, height: 2 }
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, 1 );
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, 1 );
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, 1 );
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, 1, props.width, props.height );
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 ) bakeAreaLightScale( cloned );
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: lightPower( light ), // Blender Power (W); the light stores the three.js quantity
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
- maxSamples: app.settings.get( 'maxSamples' ),
279
- maxBounces: app.settings.get( 'maxBounces' ),
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
- app.settings.setMany( {
300
- maxSamples: state.maxSamples,
301
- maxBounces: state.maxBounces,
302
- transmissiveBounces: state.transmissiveBounces,
303
- }, { silent: true } );
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
- }