rayzee 8.3.0 → 8.4.1

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.
@@ -0,0 +1,11 @@
1
+ //#region \0rolldown/runtime.js
2
+ var e = Object.defineProperty, t = (t, n) => {
3
+ let r = {};
4
+ for (var i in t) e(r, i, {
5
+ get: t[i],
6
+ enumerable: !0
7
+ });
8
+ return n || e(r, Symbol.toStringTag, { value: "Module" }), r;
9
+ };
10
+ //#endregion
11
+ export { t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "8.3.0",
3
+ "version": "8.4.1",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -32,6 +32,13 @@ const config = {
32
32
  // AI upscaler ONNX model base URL. Quality presets resolve relative paths against this.
33
33
  upscalerModelBaseUrl: 'https://huggingface.co/notaneimu/onnx-image-models/resolve/main/',
34
34
 
35
+ // DLSS runtime bundle. A plain script the host serves, not a module: it installs
36
+ // `globalThis.DLSSRuntime`, so it cannot be imported and the host must publish it somewhere.
37
+ dlssRuntimeUrl: '/dlss/dlss-runtime.js',
38
+
39
+ // Base for the DLSS weight manifests the runtime fetches (`generated/...` beneath this).
40
+ dlssAssetBaseUrl: 'https://pub-6e048ff014374f11bef33d76b6c2b5ef.r2.dev/v1',
41
+
35
42
  // Prefix used when the engine writes to client-side stores (IndexedDB, etc).
36
43
  // Set to a unique value to avoid collisions when multiple apps embed the engine on the same origin.
37
44
  cacheNamespace: 'rayzee',
@@ -10,13 +10,11 @@
10
10
  */
11
11
  export const RENDER_PROFILES = Object.freeze( {
12
12
  viewer: Object.freeze( {
13
- areaLightIntensityScale: 0.1, // scales glTF placeholder area-light power (viewer tuning)
14
13
  environmentRotation: 270.0, // degrees
15
14
  toneMapping: 4, // ACESFilmicToneMapping
16
15
  saturation: 1.2, // compensates for ACES desaturation — see Compositor
17
16
  } ),
18
17
  physical: Object.freeze( {
19
- areaLightIntensityScale: 1.0,
20
18
  environmentRotation: 0.0,
21
19
  // AgX, matching Blender/Cycles' default view transform. ACES crushes shadows on this
22
20
  // engine's own corpus (shade 1.48 vs AgX 3.01 from identical radiance).
@@ -27,7 +25,7 @@ export const RENDER_PROFILES = Object.freeze( {
27
25
 
28
26
  /**
29
27
  * @param {string} [name] - a RENDER_PROFILES key
30
- * @returns {{areaLightIntensityScale: number, environmentRotation: number}}
28
+ * @returns {{environmentRotation: number, toneMapping: number, saturation: number}}
31
29
  * @throws {Error} on an unknown name — a typo must not silently select viewer tuning
32
30
  */
33
31
  export function getRenderProfile( name = 'viewer' ) {
@@ -1062,7 +1060,8 @@ export const DEFAULT_TEXTURE_MATRIX = [ 0, 0, 1, 1, 0, 0, 0, 1 ];
1062
1060
  export const PRODUCTION_RENDER_CONFIG = {
1063
1061
  // maxSamples is a CEILING: adaptive sampling retires the frame once adaptiveStopFraction of pixels converge,
1064
1062
  // so easy scenes finish well under it while hard GI scenes use the full budget.
1065
- maxSamples: 150, bounces: 20, transmissiveBounces: 8, maxSubsurfaceSteps: 64,
1063
+ // Below 24 a ray that spends its transmissive budget is shaded opaque — black pixels, not dim glass.
1064
+ maxSamples: 30, bounces: 20, transmissiveBounces: 24, maxSubsurfaceSteps: 64,
1066
1065
  renderMode: 1, enableAlphaShadows: true,
1067
1066
  // 'high' is the only tier that reaches OIDN's _large weights (calb_cnrm); ~2x denoise cost.
1068
1067
  enableOIDN: true, oidnQuality: 'high',
@@ -1077,9 +1076,14 @@ export const PRODUCTION_RENDER_CONFIG = {
1077
1076
  export const INTERACTIVE_RENDER_CONFIG = {
1078
1077
  maxSamples: ENGINE_DEFAULTS.maxSamples, bounces: ENGINE_DEFAULTS.bounces,
1079
1078
  renderMode: ENGINE_DEFAULTS.renderMode, enableAlphaShadows: ENGINE_DEFAULTS.enableAlphaShadows,
1080
- transmissiveBounces: ENGINE_DEFAULTS.transmissiveBounces,
1079
+ // 12, not 5: a spent budget leaves black glass and costs MORE — the ray then bounces diffusely.
1080
+ transmissiveBounces: 12,
1081
1081
  maxSubsurfaceSteps: ENGINE_DEFAULTS.maxSubsurfaceSteps,
1082
- enableOIDN: false, oidnQuality: 'fast',
1082
+ // On, like production. The final pass costs one cheap denoise when the preview settles, and both
1083
+ // neural passes need a denoised frame to be worth running — on Monte-Carlo noise the upscaler
1084
+ // measured worse than a plain resize. ⚠️ `ENGINE_DEFAULTS.enableOIDN` stays false: the bench
1085
+ // renders against it and every quality golden would move.
1086
+ enableOIDN: true, oidnQuality: 'fast',
1083
1087
  interactionModeEnabled: true,
1084
1088
  useAdaptiveSampling: true, // idle refine stops early when converged; frozen during motion
1085
1089
  noiseThreshold: 0.1, // loose: preview wants a fast settle, not a clean one
@@ -19,6 +19,7 @@ export const ISSUE_CODES = Object.freeze( {
19
19
  STAGE_RENDER_FAILED: 'stage.render_failed',
20
20
  RENDER_RESERVE_CAPPED: 'render.reserve_capped',
21
21
  SCENE_MEMORY_BUDGET: 'scene.memory_budget',
22
+ LIGHT_PLACEHOLDER_INVALID: 'light.placeholder_invalid',
22
23
  EMISSIVE_INSTANCES_COLLAPSED: 'emissive.instances_collapsed',
23
24
  REFIT_SHARED_GEOMETRY: 'refit.shared_geometry',
24
25
  } );
@@ -0,0 +1,122 @@
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
+ }
@@ -764,8 +764,9 @@ export class PathTracerApp extends EventDispatcher {
764
764
  * by reference, and any ancestor transform is baked in. Later edits to `object3d` do not
765
765
  * reach the render — mutate {@link sceneModel}, then {@link refitBVH}/{@link refitBLASes}.
766
766
  *
767
- * Lights keep three.js units — `RectAreaLight.intensity` in nits, point/spot in candela — and are
768
- * converted to radiant power on the copy. `areaLightIntensityScale` does not apply here.
767
+ * Lights are read as three.js lights in RADIOMETRIC units — W/sr for point and spot, W/m² for
768
+ * directional, radiance for area — and are left as they are; only an area light's rectangle is
769
+ * put into world metres on the copy. See `LightUnits.js`.
769
770
  *
770
771
  * @param {import('three').Object3D} object3d - The Object3D to render; left untouched.
771
772
  * @param {string} [name='object3d'] - Display name for the object
@@ -2463,11 +2464,13 @@ export class PathTracerApp extends EventDispatcher {
2463
2464
 
2464
2465
  if ( ! this.renderer?.domElement ) return null;
2465
2466
 
2467
+ // Whatever is on the overlay is what the viewport shows, so it is also what a save must
2468
+ // write. Gating on `upscaler.enabled` instead missed the neural-rendering pass, which puts
2469
+ // a picture there without the ONNX upscaler being on at all — saves silently wrote the
2470
+ // un-enhanced render.
2466
2471
  const dm = this.denoisingManager;
2467
- const upscaled = dm?.upscaler?.enabled && dm?.upscalerCanvas
2468
- && dm.upscalerCanvas.style.display !== 'none';
2469
-
2470
- if ( upscaled ) return dm.upscalerCanvas;
2472
+ const overlay = dm?.upscalerCanvas;
2473
+ if ( overlay && overlay.style.display !== 'none' ) return overlay;
2471
2474
 
2472
2475
  // A presented WebGPU canvas only reads back what was drawn immediately before, so draw.
2473
2476
  // This also puts the denoised picture on it: the Compositor prefers it over the raw render.
@@ -3104,6 +3107,7 @@ export class PathTracerApp extends EventDispatcher {
3104
3107
  this.denoisingManager.setOverlayManager( this.overlayManager );
3105
3108
  this.denoisingManager.setResetCallback( () => this.reset() );
3106
3109
  this.denoisingManager.setPostProcessRefreshCallback( () => this.requestPostProcessRefresh() );
3110
+ this.denoisingManager.setDisplayRefreshCallback( () => this.refreshFrame() );
3107
3111
  this.denoisingManager.setSettings( this.settings );
3108
3112
 
3109
3113
  // Expose environment manager (lives on pathTracer stage)
@@ -19,6 +19,7 @@ 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';
22
23
  import { getRenderProfile } from '../EngineDefaults.js';
23
24
 
24
25
  // Define supported file formats
@@ -67,16 +68,43 @@ function standInForSplit( source ) {
67
68
 
68
69
  }
69
70
 
70
- // three.js nits → the engine's radiant power, inverting areaLightRadiance.
71
- function areaLightPowerFactor( node, width, height, userData ) {
71
+ const PLACEHOLDER_SHAPES = new Set( [ 'rectangle', 'square', 'disk', 'ellipse' ] );
72
72
 
73
- if ( ! ( userData.normalize ?? true ) ) return Math.PI;
73
+ /**
74
+ * Build the light a placeholder node stands for. glTF has no area light, so a three.js host's
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 ) {
74
88
 
75
- const shapeFactor = userData.shape === 'ellipse' || userData.shape === 'disk' ? Math.PI / 4 : 1;
76
- const scale = node.getWorldScale( new Vector3() );
77
- // abs: a mirrored ancestor decomposes negative; the serializer takes the area as |u × v|.
78
- const factor = Math.PI * shapeFactor * Math.abs( width * scale.x * height * scale.y );
79
- return Number.isFinite( factor ) ? factor : Math.PI;
89
+ const width = Number( record.width );
90
+ const height = Number( record.height );
91
+ const intensity = Number( record.intensity );
92
+ if ( ! ( width > 0 ) || ! ( height > 0 ) || ! Number.isFinite( intensity ) ) return null;
93
+
94
+ const shape = PLACEHOLDER_SHAPES.has( record.shape ) ? record.shape : 'rectangle';
95
+ const color = Array.isArray( record.color ) && record.color.length >= 3 ? record.color : [ 1, 1, 1 ];
96
+
97
+ const light = new RectAreaLight( new Color( ...color ), intensity, width, height );
98
+ light.name = record.name || 'RectAreaLight';
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;
80
108
 
81
109
  }
82
110
 
@@ -2081,50 +2109,40 @@ export class AssetLoader extends EventDispatcher {
2081
2109
 
2082
2110
  const userData = object.userData;
2083
2111
 
2084
- // An adopted light carries three.js units; convert as point/spot are below.
2085
- if ( object.isRectAreaLight && ! userData.__radianceConverted ) {
2112
+ // three.js states RectAreaLight.intensity as radiance, which is what the engine
2113
+ // stores, so only the rectangle needs putting into world metres.
2114
+ if ( object.isRectAreaLight ) bakeAreaLightScale( object );
2086
2115
 
2087
- object.intensity *= areaLightPowerFactor( object, object.width, object.height, userData );
2088
- userData.__radianceConverted = true;
2116
+ // Punctual lights arrive photometric: glTF (and three.js) state point/spot in
2117
+ // candela and directional in lux. The engine keeps the same QUANTITY in radiometric
2118
+ // units — W/sr and W/m² — so only the luminous efficacy is divided back out, which
2119
+ // is what Blender's own glTF importer does. Skipping it rendered every Blender lamp
2120
+ // 683x too bright.
2121
+ if ( ( object.isPointLight || object.isSpotLight ) && ! userData.__candelaConverted ) {
2122
+
2123
+ object.intensity /= LUMENS_PER_WATT;
2124
+ userData.__candelaConverted = true;
2089
2125
 
2090
2126
  }
2091
2127
 
2092
- // glTF punctual point/spot intensity is candela (W/sr); the engine's light
2093
- // model is Blender-style Power (W) that LightSerializer divides by 4π. Convert
2094
- // glTF candela → Power once at import (×4π) so it nets to the correct I/d².
2095
- // Directional (lux ≈ irradiance) is used directly and needs no conversion.
2096
- if ( ( object.isPointLight || object.isSpotLight ) && ! userData.__candelaConverted ) {
2128
+ if ( object.isDirectionalLight && ! userData.__luxConverted ) {
2097
2129
 
2098
- object.intensity *= 4 * Math.PI;
2099
- userData.__candelaConverted = true;
2130
+ object.intensity /= LUMENS_PER_WATT;
2131
+ userData.__luxConverted = true;
2100
2132
 
2101
2133
  }
2102
2134
 
2103
- // Process ceiling lights
2104
- if ( object.name.startsWith( 'RectAreaLightPlaceholder' ) &&
2105
- userData.name
2106
- // && userData.name.includes( "ceilingLight" )
2107
- ) {
2108
-
2109
- if ( userData.type === 'RectAreaLight' ) {
2110
-
2111
- const normalize = userData.normalize ?? true;
2112
- const shape = userData.shape ?? 'rectangle';
2113
- const power = userData.intensity * areaLightPowerFactor( object, userData.width, userData.height, userData );
2114
- const light = new RectAreaLight(
2115
- new Color( ...userData.color ),
2116
- power * this._profile.areaLightIntensityScale,
2117
- userData.width,
2118
- userData.height
2119
- );
2120
- light.userData.normalize = normalize;
2121
- light.userData.spread = Number.isFinite( userData.spread ) ? userData.spread : Math.PI;
2122
- light.userData.shape = shape;
2123
- light.userData.__radianceConverted = true; // already power, and traverse() reaches it
2124
- light.name = userData.name;
2125
- object.add( light );
2135
+ // An area light serialized into a placeholder node. Keyed on the record's own type
2136
+ // rather than the node's name, so a renamed placeholder still lights the scene.
2137
+ if ( userData.type === 'RectAreaLight' ) {
2126
2138
 
2127
- }
2139
+ const light = areaLightFromPlaceholder( object, userData );
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
+ );
2128
2146
 
2129
2147
  }
2130
2148
 
@@ -1,10 +1,11 @@
1
1
  import { Vector3, Quaternion } from 'three';
2
2
  import { blackbodyToLinearRGB } from './blackbody.js';
3
+ import { lightPower } from '../LightUnits.js';
3
4
 
4
- // Point & spot lights specify Power in Watts; the shader uses radiant intensity
5
- // (W/sr) as `intensity / dist²`, so convert with I = P / 4π (isotropic emitter),
6
- // matching Blender. Area lights are already radiant power; the Sun is irradiance.
7
- const INV_4PI = 1 / ( 4 * Math.PI );
5
+ // Scene lights hold the three.js quantity in radiometric units (see LightUnits.js): point and
6
+ // spot in W/sr, the Sun in W/m², area lights in radiance. The GPU buffer wants radiant intensity
7
+ // for point/spot — already what they hold — and radiant POWER for area lights, which the shader
8
+ // turns back into radiance with its own π·area. This is where that adaptation happens.
8
9
 
9
10
  // Effective emission colour + intensity after per-light Exposure (EV stops) and
10
11
  // optional blackbody Temperature tint (Blender-style). Both fold into the values
@@ -61,9 +62,10 @@ export class LightSerializer {
61
62
 
62
63
  calculateLightImportance( light, type = 'directional' ) {
63
64
 
64
- // Calculate luminance-weighted importance
65
+ // Ranked on Power, so the ordering does not shift when a light type's stored quantity
66
+ // differs from what the panel shows.
65
67
  const luminance = 0.2126 * light.color.r + 0.7152 * light.color.g + 0.0722 * light.color.b;
66
- let importance = light.intensity * luminance;
68
+ let importance = lightPower( light ) * luminance;
67
69
 
68
70
  // Area lights get additional importance based on size
69
71
  if ( type === 'area' ) {
@@ -172,6 +174,12 @@ export class LightSerializer {
172
174
 
173
175
  const em = effectiveEmission( light );
174
176
 
177
+ // The shader recovers radiance as power/(π·area), so hand it the power that inverts to
178
+ // the radiance the light holds. Area is measured off u/v exactly as the shader does it.
179
+ const rectArea = 4 * u.clone().cross( v ).length();
180
+ const emittingArea = shape > 0.5 ? rectArea * Math.PI / 4 : rectArea;
181
+ const power = em.intensity * Math.PI * ( normalize > 0.5 ? emittingArea : 1 );
182
+
175
183
  // Store in cache with importance (16 floats, vec4-aligned)
176
184
  this.areaLightCache.push( {
177
185
  data: [
@@ -179,7 +187,7 @@ export class LightSerializer {
179
187
  u.x, u.y, u.z, // u half-vector (3)
180
188
  v.x, v.y, v.z, // v half-vector (3)
181
189
  em.r, em.g, em.b, // color (3) — incl. temperature tint
182
- em.intensity, // radiant power in Watts (1) — incl. exposure
190
+ power, // radiant power in Watts (1) — incl. exposure
183
191
  normalize, // power-normalize flag (1)
184
192
  spread, // emission spread in radians (1)
185
193
  shape, // 0 = rect, 1 = disk/ellipse (1)
@@ -209,7 +217,7 @@ export class LightSerializer {
209
217
  data: [
210
218
  position.x, position.y, position.z, // position (3)
211
219
  em.r, em.g, em.b, // color (3) — incl. temperature tint
212
- em.intensity * INV_4PI, // radiant intensity W/sr = power(W)/4π, incl. exposure (1)
220
+ em.intensity, // radiant intensity W/sr, incl. exposure (1)
213
221
  light.distance || 0.0, // cutoff distance (0 = infinite) (1)
214
222
  light.decay !== undefined ? light.decay : 2.0 // decay exponent (1)
215
223
  ],
@@ -252,7 +260,7 @@ export class LightSerializer {
252
260
  position.x, position.y, position.z, // position (3)
253
261
  direction.x, direction.y, direction.z, // direction (3)
254
262
  em.r, em.g, em.b, // color (3) — incl. temperature tint
255
- em.intensity * INV_4PI, // radiant intensity W/sr = power(W)/4π, incl. exposure (1)
263
+ em.intensity, // radiant intensity W/sr, incl. exposure (1)
256
264
  light.angle || Math.PI / 4, // cone half-angle in radians (1)
257
265
  light.penumbra || 0.0, // penumbra [0,1] (1)
258
266
  light.distance || 0.0, // cutoff distance (0 = infinite) (1)