rayzee 6.0.1 → 6.1.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 +5 -0
- package/dist/assets/TexturesWorker-DBqGmVdR.js.map +1 -1
- package/dist/rayzee.es.js +2419 -2078
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +48 -52
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +2 -2
- package/src/EngineDefaults.js +3 -0
- package/src/PathTracerApp.js +18 -8
- package/src/Pipeline/RenderStage.js +3 -0
- package/src/Processor/IESParser.js +340 -0
- package/src/Processor/LightSerializer.js +32 -4
- package/src/Processor/SceneProcessor.js +0 -1
- package/src/Processor/ShaderBuilder.js +40 -1
- package/src/Processor/Workers/TexturesWorker.js +1 -1
- package/src/RenderSettings.js +3 -0
- package/src/Stages/NormalDepth.js +3 -19
- package/src/Stages/PathTracer.js +15 -9
- package/src/TSL/BVHTraversal.js +4 -6
- package/src/TSL/Common.js +1 -1
- package/src/TSL/Debugger.js +0 -2
- package/src/TSL/EmissiveSampling.js +20 -22
- package/src/TSL/Environment.js +60 -14
- package/src/TSL/Fresnel.js +13 -4
- package/src/TSL/LightsCore.js +238 -5
- package/src/TSL/LightsDirect.js +16 -5
- package/src/TSL/LightsIndirect.js +4 -37
- package/src/TSL/LightsSampling.js +119 -185
- package/src/TSL/MaterialEvaluation.js +25 -14
- package/src/TSL/MaterialProperties.js +14 -34
- package/src/TSL/MaterialTransmission.js +18 -37
- package/src/TSL/PathTracer.js +5 -4
- package/src/TSL/PathTracerCore.js +144 -139
- package/src/TSL/Struct.js +7 -1
- package/src/TSL/TextureSampling.js +2 -2
- package/src/index.js +2 -0
- package/src/managers/AnimationManager.js +3 -6
- package/src/managers/DenoisingManager.js +1 -1
- package/src/managers/GoboManager.js +277 -0
- package/src/managers/IESManager.js +268 -0
- package/src/managers/LightManager.js +33 -1
- package/src/managers/TransformManager.js +3 -3
- package/src/managers/UniformManager.js +5 -5
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { DataArrayTexture, LinearFilter, RGBAFormat, UnsignedByteType } from 'three';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manages projection masks ("gobos" / "cookies") for spot lights.
|
|
5
|
+
*
|
|
6
|
+
* Loads a library of grayscale images into a single GPU `DataArrayTexture`;
|
|
7
|
+
* spot lights reference layers by index via `light.userData.gobo`. The TSL
|
|
8
|
+
* shader projects the surface direction onto the light's local plane and
|
|
9
|
+
* multiplies emission by the mask, producing classic "light through a window"
|
|
10
|
+
* or dappled-foliage shadows without any extra geometry.
|
|
11
|
+
*
|
|
12
|
+
* Library layers must all share the same square resolution (default 256×256).
|
|
13
|
+
* Images are rasterised through a 2D canvas so non-square sources scale to fit.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* ```js
|
|
17
|
+
* const entries = await app.goboManager.loadLibrary( [
|
|
18
|
+
* { name: 'window', url: '/lightmasks/window_a.png' },
|
|
19
|
+
* { name: 'leaves', url: '/lightmasks/foliage_canopy_a.png' },
|
|
20
|
+
* ] );
|
|
21
|
+
* app.goboManager.setSpotLightGobo( spotLight.uuid, 'window', 1.0 );
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export class GoboManager {
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {import('../Stages/PathTracer.js').PathTracer} pathTracer
|
|
28
|
+
* @param {Object} [options]
|
|
29
|
+
* @param {Function} [options.onReset] - Called after gobo state changes so the host can reset accumulation
|
|
30
|
+
*/
|
|
31
|
+
constructor( pathTracer, options = {} ) {
|
|
32
|
+
|
|
33
|
+
this.pathTracer = pathTracer;
|
|
34
|
+
this._onReset = options.onReset || null;
|
|
35
|
+
|
|
36
|
+
/** @type {DataArrayTexture | null} */
|
|
37
|
+
this.texture = null;
|
|
38
|
+
|
|
39
|
+
/** @type {Array<{ name: string, index: number }>} */
|
|
40
|
+
this.entries = [];
|
|
41
|
+
|
|
42
|
+
this._size = 256;
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Load a list of gobo images into a single DataArrayTexture.
|
|
48
|
+
* Replaces any previously loaded library.
|
|
49
|
+
*
|
|
50
|
+
* @param {Array<{ name: string, url: string }>} items
|
|
51
|
+
* @param {Object} [options]
|
|
52
|
+
* @param {number} [options.size=256] - Per-layer square resolution
|
|
53
|
+
* @returns {Promise<Array<{ name: string, index: number }>>}
|
|
54
|
+
*/
|
|
55
|
+
async loadLibrary( items, { size = 256 } = {} ) {
|
|
56
|
+
|
|
57
|
+
if ( ! Array.isArray( items ) || items.length === 0 ) return [];
|
|
58
|
+
|
|
59
|
+
this._size = size;
|
|
60
|
+
const images = await Promise.all( items.map( it => loadImage( it.url ) ) );
|
|
61
|
+
|
|
62
|
+
const width = size;
|
|
63
|
+
const height = size;
|
|
64
|
+
const depth = images.length;
|
|
65
|
+
const data = new Uint8Array( width * height * depth * 4 );
|
|
66
|
+
|
|
67
|
+
const cnv = document.createElement( 'canvas' );
|
|
68
|
+
cnv.width = width;
|
|
69
|
+
cnv.height = height;
|
|
70
|
+
const ctx = cnv.getContext( '2d', { willReadFrequently: true } );
|
|
71
|
+
|
|
72
|
+
for ( let i = 0; i < depth; i ++ ) {
|
|
73
|
+
|
|
74
|
+
ctx.clearRect( 0, 0, width, height );
|
|
75
|
+
ctx.drawImage( images[ i ], 0, 0, width, height );
|
|
76
|
+
const img = ctx.getImageData( 0, 0, width, height );
|
|
77
|
+
data.set( img.data, i * width * height * 4 );
|
|
78
|
+
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const tex = new DataArrayTexture( data, width, height, depth );
|
|
82
|
+
tex.minFilter = LinearFilter;
|
|
83
|
+
tex.magFilter = LinearFilter;
|
|
84
|
+
tex.format = RGBAFormat;
|
|
85
|
+
tex.type = UnsignedByteType;
|
|
86
|
+
tex.generateMipmaps = false;
|
|
87
|
+
tex.needsUpdate = true;
|
|
88
|
+
|
|
89
|
+
const old = this.texture;
|
|
90
|
+
this.texture = tex;
|
|
91
|
+
this.entries = items.map( ( it, i ) => ( { name: it.name, index: i } ) );
|
|
92
|
+
|
|
93
|
+
// Hand the new texture to the shader graph and refresh light data.
|
|
94
|
+
this.pathTracer.goboMaps = tex;
|
|
95
|
+
this.pathTracer.shaderBuilder?.updateGoboMaps?.( tex );
|
|
96
|
+
|
|
97
|
+
// Free old GPU memory after the new one is bound.
|
|
98
|
+
old?.dispose?.();
|
|
99
|
+
|
|
100
|
+
return this.entries;
|
|
101
|
+
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Returns the loaded library entries (name → index lookup).
|
|
106
|
+
* @returns {Array<{ name: string, index: number }>}
|
|
107
|
+
*/
|
|
108
|
+
getEntries() {
|
|
109
|
+
|
|
110
|
+
return this.entries.slice();
|
|
111
|
+
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Assign a gobo to a spot or directional light by uuid.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} uuid - Light's uuid
|
|
118
|
+
* @param {string | null} name - Gobo entry name (or null to clear)
|
|
119
|
+
* @param {Object} [opts]
|
|
120
|
+
* @param {number} [opts.intensity=1.0] - Mask strength [0,1]
|
|
121
|
+
* @param {boolean} [opts.inverted=false] - If true, sample (1 - mask)
|
|
122
|
+
* @param {number} [opts.scale=5.0] - World units per gobo tile (directional only)
|
|
123
|
+
* @returns {boolean} True if the light was found and updated
|
|
124
|
+
*/
|
|
125
|
+
setLightGobo( uuid, name, opts = {} ) {
|
|
126
|
+
|
|
127
|
+
const { intensity = 1.0, inverted = false, scale = 5.0 } = opts;
|
|
128
|
+
const light = this._findGoboLight( uuid );
|
|
129
|
+
if ( ! light ) return false;
|
|
130
|
+
|
|
131
|
+
light.userData = light.userData || {};
|
|
132
|
+
|
|
133
|
+
if ( name == null ) {
|
|
134
|
+
|
|
135
|
+
delete light.userData.gobo;
|
|
136
|
+
|
|
137
|
+
} else {
|
|
138
|
+
|
|
139
|
+
const entry = this.entries.find( e => e.name === name );
|
|
140
|
+
if ( ! entry ) {
|
|
141
|
+
|
|
142
|
+
console.warn( `GoboManager: unknown gobo "${name}"` );
|
|
143
|
+
return false;
|
|
144
|
+
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
light.userData.gobo = {
|
|
148
|
+
name: entry.name,
|
|
149
|
+
index: entry.index,
|
|
150
|
+
intensity: clamp01( intensity ),
|
|
151
|
+
inverted: !! inverted,
|
|
152
|
+
scale: Math.max( 1e-4, scale ),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.pathTracer.updateLights();
|
|
158
|
+
this._onReset?.();
|
|
159
|
+
return true;
|
|
160
|
+
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Toggle inversion on an existing gobo assignment without losing
|
|
165
|
+
* the chosen mask or intensity.
|
|
166
|
+
* @param {string} uuid
|
|
167
|
+
* @param {boolean} inverted
|
|
168
|
+
* @returns {boolean}
|
|
169
|
+
*/
|
|
170
|
+
setLightGoboInverted( uuid, inverted ) {
|
|
171
|
+
|
|
172
|
+
const light = this._findGoboLight( uuid );
|
|
173
|
+
if ( ! light?.userData?.gobo ) return false;
|
|
174
|
+
|
|
175
|
+
light.userData.gobo.inverted = !! inverted;
|
|
176
|
+
this.pathTracer.updateLights();
|
|
177
|
+
this._onReset?.();
|
|
178
|
+
return true;
|
|
179
|
+
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Update the gobo projection scale for a directional light without
|
|
184
|
+
* touching the chosen mask, intensity, or inverted flag.
|
|
185
|
+
* @param {string} uuid
|
|
186
|
+
* @param {number} scale
|
|
187
|
+
* @returns {boolean}
|
|
188
|
+
*/
|
|
189
|
+
setLightGoboScale( uuid, scale ) {
|
|
190
|
+
|
|
191
|
+
const light = this._findGoboLight( uuid );
|
|
192
|
+
if ( ! light?.userData?.gobo ) return false;
|
|
193
|
+
|
|
194
|
+
light.userData.gobo.scale = Math.max( 1e-4, scale );
|
|
195
|
+
this.pathTracer.updateLights();
|
|
196
|
+
this._onReset?.();
|
|
197
|
+
return true;
|
|
198
|
+
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Returns the gobo descriptor currently assigned to a light, or null.
|
|
203
|
+
* @param {string} uuid
|
|
204
|
+
*/
|
|
205
|
+
getLightGobo( uuid ) {
|
|
206
|
+
|
|
207
|
+
const light = this._findGoboLight( uuid );
|
|
208
|
+
return light?.userData?.gobo || null;
|
|
209
|
+
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Back-compat thin wrappers ─────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
setSpotLightGobo( uuid, name, intensity = 1.0, inverted = false ) {
|
|
215
|
+
|
|
216
|
+
return this.setLightGobo( uuid, name, { intensity, inverted } );
|
|
217
|
+
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
setSpotLightGoboInverted( uuid, inverted ) {
|
|
221
|
+
|
|
222
|
+
return this.setLightGoboInverted( uuid, inverted );
|
|
223
|
+
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
getSpotLightGobo( uuid ) {
|
|
227
|
+
|
|
228
|
+
return this.getLightGobo( uuid );
|
|
229
|
+
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
dispose() {
|
|
233
|
+
|
|
234
|
+
this.texture?.dispose?.();
|
|
235
|
+
this.texture = null;
|
|
236
|
+
this.entries = [];
|
|
237
|
+
this.pathTracer = null;
|
|
238
|
+
this._onReset = null;
|
|
239
|
+
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
_findGoboLight( uuid ) {
|
|
243
|
+
|
|
244
|
+
const obj = this.pathTracer?.scene?.getObjectByProperty?.( 'uuid', uuid );
|
|
245
|
+
if ( ! obj ) return null;
|
|
246
|
+
return ( obj.isSpotLight || obj.isDirectionalLight ) ? obj : null;
|
|
247
|
+
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_findSpotLight( uuid ) {
|
|
251
|
+
|
|
252
|
+
const obj = this.pathTracer?.scene?.getObjectByProperty?.( 'uuid', uuid );
|
|
253
|
+
return obj && obj.isSpotLight ? obj : null;
|
|
254
|
+
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function loadImage( url ) {
|
|
260
|
+
|
|
261
|
+
return new Promise( ( resolve, reject ) => {
|
|
262
|
+
|
|
263
|
+
const img = new Image();
|
|
264
|
+
img.crossOrigin = 'anonymous';
|
|
265
|
+
img.onload = () => resolve( img );
|
|
266
|
+
img.onerror = () => reject( new Error( `Failed to load gobo image: ${url}` ) );
|
|
267
|
+
img.src = url;
|
|
268
|
+
|
|
269
|
+
} );
|
|
270
|
+
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function clamp01( v ) {
|
|
274
|
+
|
|
275
|
+
return Math.max( 0, Math.min( 1, v ) );
|
|
276
|
+
|
|
277
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { DataArrayTexture, LinearFilter, RGBAFormat, UnsignedByteType } from 'three';
|
|
2
|
+
import { parseIES, resampleIESToGrid, deriveIESBeamAngle, deriveIESPenumbra } from '../Processor/IESParser.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Manages IES photometric profiles for spot lights.
|
|
6
|
+
*
|
|
7
|
+
* Pulls .ies files over HTTP, parses each into an angular candela grid,
|
|
8
|
+
* resamples to a fixed-size 2D texture (U = horizontal angle, V = vertical
|
|
9
|
+
* angle), and stacks all profiles into a single `DataArrayTexture` referenced
|
|
10
|
+
* per-light through `light.userData.ies = { name, index, intensity }`.
|
|
11
|
+
*
|
|
12
|
+
* Shader sampling math lives in `LightsCore.js (sampleIESProfile)`.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* ```js
|
|
16
|
+
* const entries = await app.iesManager.loadLibrary([
|
|
17
|
+
* { name: 'parallel-beam', url: '/iesprofiles/parallel-beam.ies' },
|
|
18
|
+
* ]);
|
|
19
|
+
* app.iesManager.setSpotLightProfile(spotLight.uuid, 'parallel-beam', 1.0);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export class IESManager {
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {import('../Stages/PathTracer.js').PathTracer} pathTracer
|
|
26
|
+
* @param {Object} [options]
|
|
27
|
+
* @param {Function} [options.onReset] - reset accumulation after assignment changes
|
|
28
|
+
*/
|
|
29
|
+
constructor( pathTracer, options = {} ) {
|
|
30
|
+
|
|
31
|
+
this.pathTracer = pathTracer;
|
|
32
|
+
this._onReset = options.onReset || null;
|
|
33
|
+
|
|
34
|
+
/** @type {DataArrayTexture | null} */
|
|
35
|
+
this.texture = null;
|
|
36
|
+
|
|
37
|
+
/** @type {Array<{ name: string, index: number, maxCandela: number, photometricType: number }>} */
|
|
38
|
+
this.entries = [];
|
|
39
|
+
|
|
40
|
+
this._gridWidth = 128; // horizontal samples (U axis)
|
|
41
|
+
this._gridHeight = 128; // vertical samples (V axis)
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load a list of IES profiles. Replaces any previously loaded library.
|
|
47
|
+
*
|
|
48
|
+
* @param {Array<{ name: string, url: string }>} items
|
|
49
|
+
* @param {Object} [options]
|
|
50
|
+
* @param {number} [options.gridWidth=128]
|
|
51
|
+
* @param {number} [options.gridHeight=128]
|
|
52
|
+
* @returns {Promise<Array<{ name: string, index: number }>>}
|
|
53
|
+
*/
|
|
54
|
+
async loadLibrary( items, { gridWidth = 128, gridHeight = 128 } = {} ) {
|
|
55
|
+
|
|
56
|
+
if ( ! Array.isArray( items ) || items.length === 0 ) return [];
|
|
57
|
+
|
|
58
|
+
this._gridWidth = gridWidth;
|
|
59
|
+
this._gridHeight = gridHeight;
|
|
60
|
+
|
|
61
|
+
// Fetch + parse in parallel; tolerate individual failures.
|
|
62
|
+
const results = await Promise.all( items.map( async ( it ) => {
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
|
|
66
|
+
const res = await fetch( it.url );
|
|
67
|
+
if ( ! res.ok ) throw new Error( `HTTP ${res.status}` );
|
|
68
|
+
const text = await res.text();
|
|
69
|
+
const profile = parseIES( text, it.name );
|
|
70
|
+
const grid = resampleIESToGrid( profile, gridWidth, gridHeight );
|
|
71
|
+
return { it, profile, grid };
|
|
72
|
+
|
|
73
|
+
} catch ( err ) {
|
|
74
|
+
|
|
75
|
+
console.warn( `IESManager: failed to load "${it.name}": ${err.message}` );
|
|
76
|
+
return null;
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
} ) );
|
|
81
|
+
|
|
82
|
+
const okResults = results.filter( r => r !== null );
|
|
83
|
+
if ( okResults.length === 0 ) return [];
|
|
84
|
+
|
|
85
|
+
const depth = okResults.length;
|
|
86
|
+
const pixelsPerLayer = gridWidth * gridHeight;
|
|
87
|
+
// Expand single-channel grid to RGBA so the texture format matches the
|
|
88
|
+
// placeholder bound at shader-compile time (DataArrayTexture rebinds
|
|
89
|
+
// require matching format). R channel carries the value; G/B copy R for
|
|
90
|
+
// safer linear filtering, A=255.
|
|
91
|
+
const data = new Uint8Array( pixelsPerLayer * 4 * depth );
|
|
92
|
+
|
|
93
|
+
const entries = [];
|
|
94
|
+
for ( let i = 0; i < depth; i ++ ) {
|
|
95
|
+
|
|
96
|
+
const grid = okResults[ i ].grid;
|
|
97
|
+
const dst = i * pixelsPerLayer * 4;
|
|
98
|
+
for ( let p = 0; p < pixelsPerLayer; p ++ ) {
|
|
99
|
+
|
|
100
|
+
const v = grid[ p ];
|
|
101
|
+
data[ dst + p * 4 + 0 ] = v;
|
|
102
|
+
data[ dst + p * 4 + 1 ] = v;
|
|
103
|
+
data[ dst + p * 4 + 2 ] = v;
|
|
104
|
+
data[ dst + p * 4 + 3 ] = 255;
|
|
105
|
+
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const profile = okResults[ i ].profile;
|
|
109
|
+
const suggestedAngle = deriveIESBeamAngle( profile );
|
|
110
|
+
entries.push( {
|
|
111
|
+
name: okResults[ i ].it.name,
|
|
112
|
+
index: i,
|
|
113
|
+
maxCandela: profile.maxCandela,
|
|
114
|
+
photometricType: profile.photometricType,
|
|
115
|
+
suggestedAngle,
|
|
116
|
+
suggestedPenumbra: deriveIESPenumbra( profile, suggestedAngle ),
|
|
117
|
+
lumens: profile.lumens,
|
|
118
|
+
} );
|
|
119
|
+
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const tex = new DataArrayTexture( data, gridWidth, gridHeight, depth );
|
|
123
|
+
tex.format = RGBAFormat;
|
|
124
|
+
tex.type = UnsignedByteType;
|
|
125
|
+
tex.minFilter = LinearFilter;
|
|
126
|
+
tex.magFilter = LinearFilter;
|
|
127
|
+
tex.generateMipmaps = false;
|
|
128
|
+
tex.needsUpdate = true;
|
|
129
|
+
|
|
130
|
+
const old = this.texture;
|
|
131
|
+
this.texture = tex;
|
|
132
|
+
this.entries = entries;
|
|
133
|
+
|
|
134
|
+
// Hand the texture to the path tracer's shader graph.
|
|
135
|
+
this.pathTracer.iesProfiles = tex;
|
|
136
|
+
this.pathTracer.shaderBuilder?.updateIESProfiles?.( tex );
|
|
137
|
+
|
|
138
|
+
old?.dispose?.();
|
|
139
|
+
|
|
140
|
+
return this.entries.map( ( { name, index } ) => ( { name, index } ) );
|
|
141
|
+
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Returns the loaded library entries.
|
|
146
|
+
*/
|
|
147
|
+
getEntries() {
|
|
148
|
+
|
|
149
|
+
return this.entries.slice();
|
|
150
|
+
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Assign or clear an IES profile on a spot light.
|
|
155
|
+
*
|
|
156
|
+
* When `applyAutoCone` is true (default), the spot light's photometrically
|
|
157
|
+
* meaningful parameters are derived from the profile and applied:
|
|
158
|
+
* - cone half-angle (snug clip outside the IES emission)
|
|
159
|
+
* - penumbra (transition band matching the IES soft edge)
|
|
160
|
+
* - decay (forced to 2 — physically correct inverse-square)
|
|
161
|
+
*
|
|
162
|
+
* @param {string} uuid
|
|
163
|
+
* @param {string | null} name - profile name (or null to clear)
|
|
164
|
+
* @param {number} [intensity=1.0] - blend [0,1] between flat (0) and full profile (1)
|
|
165
|
+
* @param {Object} [opts]
|
|
166
|
+
* @param {boolean} [opts.applyAutoCone=true]
|
|
167
|
+
* @returns {{ applied: boolean, suggestedAngle: number | null, suggestedPenumbra: number | null, suggestedDecay: number | null, fixtureLumens: number | null }}
|
|
168
|
+
* host can mirror the suggested values into UI state.
|
|
169
|
+
*/
|
|
170
|
+
setSpotLightProfile( uuid, name, intensity = 1.0, { applyAutoCone = true } = {} ) {
|
|
171
|
+
|
|
172
|
+
const light = this._findSpotLight( uuid );
|
|
173
|
+
const empty = { applied: false, suggestedAngle: null, suggestedPenumbra: null, suggestedDecay: null, fixtureLumens: null };
|
|
174
|
+
if ( ! light ) return empty;
|
|
175
|
+
|
|
176
|
+
light.userData = light.userData || {};
|
|
177
|
+
let suggestedAngle = null;
|
|
178
|
+
let suggestedPenumbra = null;
|
|
179
|
+
let suggestedDecay = null;
|
|
180
|
+
let fixtureLumens = null;
|
|
181
|
+
|
|
182
|
+
if ( name == null ) {
|
|
183
|
+
|
|
184
|
+
delete light.userData.ies;
|
|
185
|
+
|
|
186
|
+
} else {
|
|
187
|
+
|
|
188
|
+
const entry = this.entries.find( e => e.name === name );
|
|
189
|
+
if ( ! entry ) {
|
|
190
|
+
|
|
191
|
+
console.warn( `IESManager: unknown profile "${name}"` );
|
|
192
|
+
return empty;
|
|
193
|
+
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fixtureLumens = Number.isFinite( entry.lumens ) ? entry.lumens : null;
|
|
197
|
+
|
|
198
|
+
light.userData.ies = {
|
|
199
|
+
name: entry.name,
|
|
200
|
+
index: entry.index,
|
|
201
|
+
intensity: clamp01( intensity ),
|
|
202
|
+
fixtureLumens,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
if ( applyAutoCone ) {
|
|
206
|
+
|
|
207
|
+
if ( Number.isFinite( entry.suggestedAngle ) ) {
|
|
208
|
+
|
|
209
|
+
light.angle = entry.suggestedAngle;
|
|
210
|
+
suggestedAngle = entry.suggestedAngle;
|
|
211
|
+
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if ( Number.isFinite( entry.suggestedPenumbra ) ) {
|
|
215
|
+
|
|
216
|
+
light.penumbra = entry.suggestedPenumbra;
|
|
217
|
+
suggestedPenumbra = entry.suggestedPenumbra;
|
|
218
|
+
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// IES is a photometric measurement that assumes inverse-square falloff.
|
|
222
|
+
light.decay = 2;
|
|
223
|
+
suggestedDecay = 2;
|
|
224
|
+
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.pathTracer.updateLights();
|
|
230
|
+
this._onReset?.();
|
|
231
|
+
return { applied: true, suggestedAngle, suggestedPenumbra, suggestedDecay, fixtureLumens };
|
|
232
|
+
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Returns the current IES descriptor on a spot light (or null).
|
|
237
|
+
*/
|
|
238
|
+
getSpotLightProfile( uuid ) {
|
|
239
|
+
|
|
240
|
+
const light = this._findSpotLight( uuid );
|
|
241
|
+
return light?.userData?.ies || null;
|
|
242
|
+
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
dispose() {
|
|
246
|
+
|
|
247
|
+
this.texture?.dispose?.();
|
|
248
|
+
this.texture = null;
|
|
249
|
+
this.entries = [];
|
|
250
|
+
this.pathTracer = null;
|
|
251
|
+
this._onReset = null;
|
|
252
|
+
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_findSpotLight( uuid ) {
|
|
256
|
+
|
|
257
|
+
const obj = this.pathTracer?.scene?.getObjectByProperty?.( 'uuid', uuid );
|
|
258
|
+
return obj && obj.isSpotLight ? obj : null;
|
|
259
|
+
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function clamp01( v ) {
|
|
265
|
+
|
|
266
|
+
return Math.max( 0, Math.min( 1, v ) );
|
|
267
|
+
|
|
268
|
+
}
|
|
@@ -129,11 +129,14 @@ export class LightManager extends EventDispatcher {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
|
-
* Reprocesses all scene lights and updates the path tracer uniform buffers
|
|
132
|
+
* Reprocesses all scene lights and updates the path tracer uniform buffers,
|
|
133
|
+
* and refreshes any visible helper gizmos so they reflect parameter changes
|
|
134
|
+
* (intensity, cone angle, position, target, distance) without rebuilding.
|
|
133
135
|
*/
|
|
134
136
|
updateLights() {
|
|
135
137
|
|
|
136
138
|
this.pathTracer?.updateLights();
|
|
139
|
+
if ( this.sceneHelpers?.visible ) this.sceneHelpers.update();
|
|
137
140
|
|
|
138
141
|
}
|
|
139
142
|
|
|
@@ -332,6 +335,35 @@ export class LightManager extends EventDispatcher {
|
|
|
332
335
|
} else if ( light.type === 'SpotLight' && light.target ) {
|
|
333
336
|
|
|
334
337
|
descriptor.target = [ light.target.position.x, light.target.position.y, light.target.position.z ];
|
|
338
|
+
descriptor.distance = light.distance ?? 0;
|
|
339
|
+
descriptor.penumbra = light.penumbra ?? 0;
|
|
340
|
+
descriptor.decay = light.decay ?? 2;
|
|
341
|
+
|
|
342
|
+
} else if ( light.type === 'PointLight' ) {
|
|
343
|
+
|
|
344
|
+
descriptor.distance = light.distance ?? 0;
|
|
345
|
+
descriptor.decay = light.decay ?? 2;
|
|
346
|
+
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if ( ( light.type === 'SpotLight' || light.type === 'DirectionalLight' ) && light.userData?.gobo ) {
|
|
350
|
+
|
|
351
|
+
descriptor.gobo = light.userData.gobo.name;
|
|
352
|
+
descriptor.goboIntensity = light.userData.gobo.intensity;
|
|
353
|
+
descriptor.goboInverted = !! light.userData.gobo.inverted;
|
|
354
|
+
if ( light.type === 'DirectionalLight' ) {
|
|
355
|
+
|
|
356
|
+
descriptor.goboScale = light.userData.gobo.scale ?? 5.0;
|
|
357
|
+
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if ( light.type === 'SpotLight' && light.userData?.ies ) {
|
|
363
|
+
|
|
364
|
+
descriptor.ies = light.userData.ies.name;
|
|
365
|
+
descriptor.iesIntensity = light.userData.ies.intensity ?? 1.0;
|
|
366
|
+
descriptor.fixtureLumens = light.userData.ies.fixtureLumens ?? null;
|
|
335
367
|
|
|
336
368
|
}
|
|
337
369
|
|
|
@@ -49,7 +49,7 @@ export class TransformManager {
|
|
|
49
49
|
* Provide mesh data from SceneProcessor after scene load.
|
|
50
50
|
* Required for position extraction during BVH refit.
|
|
51
51
|
*/
|
|
52
|
-
setMeshData( meshes
|
|
52
|
+
setMeshData( meshes ) {
|
|
53
53
|
|
|
54
54
|
this._meshes = meshes;
|
|
55
55
|
this._meshTriRanges = [];
|
|
@@ -72,8 +72,8 @@ export class TransformManager {
|
|
|
72
72
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
this._posBuffer = new Float32Array(
|
|
76
|
-
this._normalBuffer = new Float32Array(
|
|
75
|
+
this._posBuffer = new Float32Array( offset * 9 );
|
|
76
|
+
this._normalBuffer = new Float32Array( offset * 9 );
|
|
77
77
|
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -184,6 +184,9 @@ export class UniformManager {
|
|
|
184
184
|
u( 'envTotalSum', 0.0, 'float' );
|
|
185
185
|
u( 'envCompensationDelta', 0.0, 'float' );
|
|
186
186
|
u( 'envResolution', new Vector2( 1, 1 ), 'vec2' );
|
|
187
|
+
ub( 'groundProjectionEnabled', DEFAULT_STATE.groundProjectionEnabled );
|
|
188
|
+
u( 'groundProjectionRadius', DEFAULT_STATE.groundProjectionRadius, 'float' );
|
|
189
|
+
u( 'groundProjectionHeight', DEFAULT_STATE.groundProjectionHeight, 'float' );
|
|
187
190
|
|
|
188
191
|
// Sun parameters
|
|
189
192
|
u( 'sunDirection', new Vector3( 0, 1, 0 ), 'vec3' );
|
|
@@ -202,10 +205,10 @@ export class UniformManager {
|
|
|
202
205
|
|
|
203
206
|
// Light buffer nodes - pre-allocate for up to 16 lights per type (shader hard cap)
|
|
204
207
|
this._lightBuffers = {
|
|
205
|
-
directional: uniformArray( new Float32Array(
|
|
208
|
+
directional: uniformArray( new Float32Array( 12 * 16 ), 'float' ),
|
|
206
209
|
area: uniformArray( new Float32Array( 13 * 16 ), 'float' ),
|
|
207
210
|
point: uniformArray( new Float32Array( 9 * 16 ), 'float' ),
|
|
208
|
-
spot: uniformArray( new Float32Array(
|
|
211
|
+
spot: uniformArray( new Float32Array( 20 * 16 ), 'float' ),
|
|
209
212
|
};
|
|
210
213
|
|
|
211
214
|
// Camera matrices
|
|
@@ -249,9 +252,6 @@ export class UniformManager {
|
|
|
249
252
|
// Resolution (for RNG seeding)
|
|
250
253
|
u( 'resolution', new Vector2( width, height ), 'vec2' );
|
|
251
254
|
|
|
252
|
-
// Scene data
|
|
253
|
-
u( 'totalTriangleCount', 0, 'int' );
|
|
254
|
-
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
/**
|