rayzee 6.5.0 → 7.0.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.
Files changed (51) hide show
  1. package/README.md +24 -5
  2. package/dist/rayzee.es.js +7554 -7014
  3. package/dist/rayzee.es.js.map +1 -1
  4. package/dist/rayzee.umd.js +157 -236
  5. package/dist/rayzee.umd.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/EngineDefaults.js +12 -9
  8. package/src/PathTracerApp.js +118 -26
  9. package/src/Pipeline/PipelineContext.js +1 -2
  10. package/src/Pipeline/RenderPipeline.js +1 -1
  11. package/src/Pipeline/RenderStage.js +1 -1
  12. package/src/Processor/CameraOptimizer.js +0 -5
  13. package/src/Processor/GeometryExtractor.js +6 -0
  14. package/src/Processor/KernelManager.js +277 -0
  15. package/src/Processor/PackedRayBuffer.js +265 -0
  16. package/src/Processor/QueueManager.js +173 -0
  17. package/src/Processor/SceneProcessor.js +1 -0
  18. package/src/Processor/ShaderBuilder.js +11 -317
  19. package/src/Processor/StorageTexturePool.js +29 -15
  20. package/src/Processor/VRAMTracker.js +169 -0
  21. package/src/Processor/utils.js +11 -110
  22. package/src/RenderSettings.js +0 -3
  23. package/src/Stages/ASVGF.js +76 -20
  24. package/src/Stages/BilateralFilter.js +34 -10
  25. package/src/Stages/EdgeFilter.js +2 -3
  26. package/src/Stages/MotionVector.js +16 -9
  27. package/src/Stages/NormalDepth.js +17 -5
  28. package/src/Stages/PathTracer.js +671 -1456
  29. package/src/Stages/PathTracerStage.js +1451 -0
  30. package/src/Stages/SSRC.js +32 -15
  31. package/src/Stages/Variance.js +35 -12
  32. package/src/TSL/CompactKernel.js +110 -0
  33. package/src/TSL/DebugKernel.js +98 -0
  34. package/src/TSL/Environment.js +13 -11
  35. package/src/TSL/ExtendKernel.js +75 -0
  36. package/src/TSL/FinalWriteKernel.js +121 -0
  37. package/src/TSL/GenerateKernel.js +109 -0
  38. package/src/TSL/LightsSampling.js +2 -2
  39. package/src/TSL/PathTracerCore.js +43 -1039
  40. package/src/TSL/ShadeKernel.js +873 -0
  41. package/src/TSL/patches.js +81 -4
  42. package/src/index.js +3 -0
  43. package/src/managers/CameraManager.js +1 -1
  44. package/src/managers/DenoisingManager.js +40 -75
  45. package/src/managers/EnvironmentManager.js +30 -39
  46. package/src/managers/OverlayManager.js +7 -22
  47. package/src/managers/UniformManager.js +0 -3
  48. package/src/managers/helpers/TileHelper.js +2 -2
  49. package/src/Stages/AdaptiveSampling.js +0 -483
  50. package/src/TSL/PathTracer.js +0 -384
  51. package/src/managers/TileManager.js +0 -298
@@ -1,384 +0,0 @@
1
- /**
2
- * PathTracer.js - Main Path Tracer Entry Point (Compute Shader)
3
- *
4
- * Contains:
5
- * - dithering — anti-banding dither pattern
6
- * - computeNDCDepth — world position to NDC depth [0,1]
7
- * - getRequiredSamples — adaptive sampling sample count
8
- * - pathTracerMain — main entry (sample loop, accumulation, textureStore output)
9
- *
10
- * Compute Outputs (via textureStore):
11
- * - writeColorTex: RGB + alpha (transparent bg: per-sample hit/miss alpha, opaque: 1.0)
12
- * - writeNDTex: Normal(RGB) + depth(A)
13
- * - writeAlbedoTex: Albedo(RGB) for OIDN denoiser
14
- */
15
-
16
- import {
17
- Fn,
18
- wgslFn,
19
- float,
20
- vec2,
21
- vec3,
22
- vec4,
23
- int,
24
- uint,
25
- uvec2,
26
- clamp,
27
- mix,
28
- normalize,
29
- floor,
30
- If,
31
- Loop,
32
- Break,
33
- texture,
34
- textureStore,
35
- select,
36
- } from 'three/tsl';
37
-
38
- import {
39
- RandomValue,
40
- getDecorrelatedSeed,
41
- getStratifiedSample,
42
- pcgHash,
43
- } from './Random.js';
44
-
45
- import { generateRayFromCamera } from './BVHTraversal.js';
46
- import { Trace, TraceResult } from './PathTracerCore.js';
47
- import { TraceDebugMode } from './Debugger.js';
48
- import { Ray } from './Struct.js';
49
-
50
- // =============================================================================
51
- // Helper Functions
52
- // =============================================================================
53
-
54
- // Dithering to prevent banding in 8-bit output
55
- export const dithering = Fn( ( [ color, seed ] ) => {
56
-
57
- const gridPosition = RandomValue( seed );
58
- const ditherShiftRGB = vec3( 0.25 / 255.0, - 0.25 / 255.0, 0.25 / 255.0 ).toVar();
59
-
60
- ditherShiftRGB.assign(
61
- mix( ditherShiftRGB.mul( 2.0 ), ditherShiftRGB.mul( - 2.0 ), gridPosition ),
62
- );
63
-
64
- return color.add( ditherShiftRGB );
65
-
66
- } );
67
-
68
- // Compute NDC depth from world position for motion vector reprojection
69
- export const computeNDCDepth = /*@__PURE__*/ wgslFn( `
70
- fn computeNDCDepth( worldPos: vec3f, cameraProjectionMatrix: mat4x4f, cameraViewMatrix: mat4x4f ) -> f32 {
71
- let clipPos = cameraProjectionMatrix * cameraViewMatrix * vec4f( worldPos, 1.0f );
72
- let ndcDepth = clipPos.z / clipPos.w * 0.5f + 0.5f;
73
- return clamp( ndcDepth, 0.0f, 1.0f );
74
- }
75
- ` );
76
-
77
- // NaN/Inf detector for debug mode 11. x != x catches NaN; the abs threshold catches Inf.
78
- const nanInfToRed = /*@__PURE__*/ wgslFn( `
79
- fn nanInfToRed( c: vec3f ) -> vec3f {
80
- let isNan = c.x != c.x || c.y != c.y || c.z != c.z;
81
- let isInf = abs( c.x ) > 1e30f || abs( c.y ) > 1e30f || abs( c.z ) > 1e30f;
82
- if ( isNan || isInf ) { return vec3f( 1.0f, 0.0f, 0.0f ); }
83
- return vec3f( 0.0f );
84
- }
85
- ` );
86
-
87
- // Get required samples from adaptive sampling texture
88
- export const getRequiredSamples = Fn( ( [
89
- pixelCoord, resolution,
90
- adaptiveSamplingTexture, adaptiveSamplingMin, adaptiveSamplingMax,
91
- ] ) => {
92
-
93
- const texCoord = pixelCoord.div( resolution );
94
- const samplingData = texture( adaptiveSamplingTexture, texCoord, 0 );
95
-
96
- const result = int( 0 ).toVar();
97
-
98
- // Early exit for converged pixels
99
- If( samplingData.b.greaterThan( 0.5 ), () => {
100
-
101
- result.assign( 0 );
102
-
103
- } ).Else( () => {
104
-
105
- const normalizedSamples = samplingData.r;
106
- const targetSamples = normalizedSamples.mul( float( adaptiveSamplingMax ) );
107
- const samples = int( floor( targetSamples.add( 0.5 ) ) );
108
- result.assign( clamp( samples, adaptiveSamplingMin, adaptiveSamplingMax ) );
109
-
110
- } );
111
-
112
- return result;
113
-
114
- } );
115
-
116
- // =============================================================================
117
- // Main Path Tracer Implementation (Compute Shader)
118
- // =============================================================================
119
-
120
- /**
121
- * Generates TSL nodes for the path tracer compute kernel body.
122
- * Must be called from within a compute Fn() scope (e.g., in ShaderBuilder).
123
- *
124
- * Previous-frame reads use texture() sampling from MRT RenderTarget textures
125
- * (populated by copyTextureToTexture after each dispatch).
126
- *
127
- * @param {Object} params - All uniforms, textures, and output StorageTextures
128
- */
129
- export const pathTracerMain = ( params ) => {
130
-
131
- const {
132
- // Compute pixel coordinate (vec2 with 0.5 offset for pixel center)
133
- pixelCoord,
134
- // Output StorageTextures
135
- writeColorTex, writeNDTex, writeAlbedoTex,
136
- // Frame / resolution
137
- resolution, frame,
138
- samplesPerPixel: numRaysPerPixel,
139
- visMode,
140
- cameraWorldMatrix, cameraProjectionMatrixInverse, cameraViewMatrix, cameraProjectionMatrix,
141
- bvhBuffer, triangleBuffer, materialBuffer,
142
- albedoMaps, normalMaps, bumpMaps,
143
- metalnessMaps, roughnessMaps, emissiveMaps,
144
- displacementMaps,
145
- directionalLightsBuffer, numDirectionalLights,
146
- areaLightsBuffer, numAreaLights,
147
- pointLightsBuffer, numPointLights,
148
- spotLightsBuffer, numSpotLights,
149
- envTexture, environmentIntensity, envMatrix,
150
- envCDFBuffer,
151
- envTotalSum, envCompensationDelta, envResolution,
152
- enableEnvironmentLight, useEnvMapIS,
153
- groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
154
- maxBounceCount, transmissiveBounces, maxSubsurfaceSteps,
155
- showBackground, transparentBackground, backgroundIntensity,
156
- fireflyThreshold, globalIlluminationIntensity,
157
- enableEmissiveTriangleSampling,
158
- emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower, emissiveBoost,
159
- lightBVHBuffer, lightBVHNodeCount,
160
- debugVisScale,
161
- enableAccumulation, hasPreviousAccumulated,
162
- prevAccumTexture, prevNormalDepthTexture, prevAlbedoTexture,
163
- accumulationAlpha, cameraIsMoving,
164
- useAdaptiveSampling, adaptiveSamplingTexture, adaptiveSamplingMin, adaptiveSamplingMax,
165
- enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
166
- } = params;
167
-
168
- // Integer coordinates for textureStore writes
169
- const uintCoord = uvec2( uint( int( pixelCoord.x ) ), uint( int( pixelCoord.y ) ) );
170
-
171
- // UV for sampling previous-frame textures from MRT RenderTarget
172
- const prevUV = pixelCoord.div( resolution );
173
-
174
- // Screen position in NDC [-1, 1]
175
- // Negate Y for NDC convention (bottom-up)
176
- const screenPosition = pixelCoord.div( resolution ).mul( 2.0 ).sub( 1.0 ).toVar();
177
- screenPosition.y.assign( screenPosition.y.negate() );
178
-
179
- // Initialize pixel accumulator
180
- const pixelColor = vec4( 0.0 ).toVar();
181
- const pixelSamples = int( 0 ).toVar();
182
-
183
- const baseSeed = getDecorrelatedSeed( { pixelCoord, rayIndex: int( 0 ), frame } ).toVar();
184
-
185
- // MRT — linearDepth is ray distance (sky/miss = 1e6), the convention
186
- // MotionVector + ASVGF expect downstream.
187
- const worldNormal = vec3( 0.0, 0.0, 1.0 ).toVar();
188
- const linearDepth = float( 1e6 ).toVar();
189
-
190
- // Accumulate per-sample alpha for transparent background (0.0 = env, 1.0 = geometry)
191
- const pixelAlpha = float( 0.0 ).toVar();
192
-
193
- const samplesCount = int( numRaysPerPixel ).toVar();
194
-
195
- // Adaptive sampling
196
- If( frame.greaterThan( uint( 2 ) ).and( useAdaptiveSampling ), () => {
197
-
198
- const adaptiveSamples = getRequiredSamples(
199
- pixelCoord, resolution,
200
- adaptiveSamplingTexture, adaptiveSamplingMin, adaptiveSamplingMax,
201
- );
202
- samplesCount.assign( adaptiveSamples );
203
-
204
- // Handle converged pixels — carry forward all data from previous frame
205
- If( samplesCount.equal( int( 0 ) ), () => {
206
-
207
- If( enableAccumulation.and( hasPreviousAccumulated ), () => {
208
-
209
- const prevSample = texture( prevAccumTexture, prevUV, 0 );
210
- pixelColor.assign( prevSample );
211
- pixelAlpha.assign( prevSample.w );
212
-
213
- // Carry forward MRT data so accumulation blend preserves them
214
- const prevND = texture( prevNormalDepthTexture, prevUV, 0 );
215
- worldNormal.assign( prevND.xyz.mul( 2.0 ).sub( 1.0 ) );
216
- linearDepth.assign( prevND.w );
217
-
218
- } ).Else( () => {
219
-
220
- samplesCount.assign( 1 );
221
-
222
- } );
223
-
224
- } );
225
-
226
- } );
227
-
228
- // First-hit data for MRT
229
- const objectNormal = vec3( 0.0 ).toVar();
230
- const objectColor = vec3( 0.0 ).toVar();
231
- const objectID = float( - 1000.0 ).toVar();
232
-
233
- // Pre-compute loop-invariant jitter scale
234
- const jitterScale = vec2( 2.0 ).div( resolution ).toVar();
235
-
236
- // Main sample loop
237
- Loop( { start: int( 0 ), end: samplesCount, type: 'int', condition: '<' }, ( { i: rayIndex } ) => {
238
-
239
- const seed = pcgHash( { state: baseSeed.add( uint( rayIndex ) ) } ).toVar();
240
-
241
- const stratifiedJitter = getStratifiedSample(
242
- pixelCoord, rayIndex, samplesCount, seed, resolution, frame,
243
- ).toVar();
244
-
245
- // Debug mode 9: Visualize stratified samples
246
- If( visMode.equal( int( 9 ) ), () => {
247
-
248
- pixelColor.assign( vec4( stratifiedJitter, 1.0, 1.0 ) );
249
- pixelSamples.assign( 1 );
250
- Break();
251
-
252
- } );
253
-
254
- const jitter = stratifiedJitter.sub( 0.5 ).mul( jitterScale );
255
- const jitteredScreenPosition = screenPosition.add( jitter );
256
-
257
- const ray = Ray.wrap( generateRayFromCamera(
258
- jitteredScreenPosition, seed,
259
- cameraWorldMatrix, cameraProjectionMatrixInverse,
260
- enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
261
- ) );
262
-
263
- const sampleColor = vec4( 0.0 ).toVar();
264
-
265
- // Debug or normal trace (mode 11 runs the full path tracer so we can sniff NaN at the end)
266
- If( visMode.greaterThan( int( 0 ) ).and( visMode.notEqual( int( 11 ) ) ), () => {
267
-
268
- sampleColor.assign( TraceDebugMode(
269
- ray.origin, ray.direction,
270
- bvhBuffer,
271
- triangleBuffer,
272
- materialBuffer,
273
- envTexture, envMatrix, environmentIntensity, enableEnvironmentLight,
274
- visMode, debugVisScale,
275
- pixelCoord, resolution,
276
- albedoMaps, normalMaps, bumpMaps,
277
- metalnessMaps, roughnessMaps, emissiveMaps,
278
- cameraProjectionMatrix, cameraViewMatrix,
279
- frame,
280
- ) );
281
-
282
- } ).Else( () => {
283
-
284
- // Normal path tracing
285
- const traceResult = TraceResult.wrap( Trace(
286
- ray, seed, rayIndex,
287
- bvhBuffer,
288
- triangleBuffer,
289
- materialBuffer,
290
- albedoMaps, normalMaps, bumpMaps,
291
- metalnessMaps, roughnessMaps, emissiveMaps,
292
- displacementMaps,
293
- directionalLightsBuffer, numDirectionalLights,
294
- areaLightsBuffer, numAreaLights,
295
- pointLightsBuffer, numPointLights,
296
- spotLightsBuffer, numSpotLights,
297
- envTexture, environmentIntensity, envMatrix,
298
- envCDFBuffer,
299
- envTotalSum, envCompensationDelta, envResolution,
300
- enableEnvironmentLight, useEnvMapIS,
301
- groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
302
- maxBounceCount, transmissiveBounces, maxSubsurfaceSteps,
303
- backgroundIntensity, showBackground, transparentBackground,
304
- fireflyThreshold, globalIlluminationIntensity,
305
- enableEmissiveTriangleSampling,
306
- emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower, emissiveBoost,
307
- lightBVHBuffer, lightBVHNodeCount,
308
- pixelCoord, resolution, frame,
309
- ) );
310
-
311
- sampleColor.assign( traceResult.radiance );
312
-
313
- // Accumulate first-hit data from primary rays
314
- If( rayIndex.equal( int( 0 ) ), () => {
315
-
316
- objectNormal.assign( traceResult.objectNormal );
317
- objectColor.assign( traceResult.objectColor );
318
- objectID.assign( traceResult.objectID );
319
-
320
- // Set MRT data from first hit (only for geometry hits — miss rays have zero normal,
321
- // and normalize(vec3(0)) = NaN which would corrupt the OIDN denoiser input)
322
- If( traceResult.firstHitDistance.lessThan( 1e9 ), () => {
323
-
324
- worldNormal.assign( normalize( traceResult.objectNormal ) );
325
- linearDepth.assign( traceResult.firstHitDistance );
326
-
327
- } );
328
-
329
- } );
330
-
331
- } );
332
-
333
- pixelColor.addAssign( sampleColor );
334
- pixelAlpha.addAssign( sampleColor.w );
335
- pixelSamples.addAssign( 1 );
336
-
337
- } );
338
-
339
- // Average samples
340
- If( pixelSamples.greaterThan( int( 0 ) ), () => {
341
-
342
- pixelColor.divAssign( float( pixelSamples ) );
343
- pixelAlpha.divAssign( float( pixelSamples ) );
344
-
345
- } );
346
-
347
- // Temporal accumulation
348
- const finalColor = pixelColor.xyz.toVar();
349
- const finalNormalDepth = vec4( worldNormal.mul( 0.5 ).add( 0.5 ), linearDepth ).toVar();
350
- const finalAlbedo = vec3( objectColor ).toVar();
351
-
352
- // Output alpha: accumulated per-sample alpha when transparent, otherwise 1.0
353
- const outputAlpha = select( transparentBackground, pixelAlpha, float( 1.0 ) ).toVar();
354
-
355
- If( enableAccumulation.and( cameraIsMoving.not() ).and( frame.greaterThan( uint( 0 ) ) ).and( hasPreviousAccumulated ).and( visMode.notEqual( int( 11 ) ) ), () => {
356
-
357
- const prevAccumSample = texture( prevAccumTexture, prevUV, 0 ).toVar();
358
-
359
- finalColor.assign( mix( prevAccumSample.xyz, pixelColor.xyz, accumulationAlpha ) );
360
- finalNormalDepth.assign( mix( texture( prevNormalDepthTexture, prevUV, 0 ), finalNormalDepth, accumulationAlpha ) );
361
- finalAlbedo.assign( mix( texture( prevAlbedoTexture, prevUV, 0 ).xyz, finalAlbedo, accumulationAlpha ) );
362
-
363
- // Temporally accumulate alpha from previous frame's gColor.w
364
- If( transparentBackground, () => {
365
-
366
- outputAlpha.assign( mix( prevAccumSample.w, pixelAlpha, accumulationAlpha ) );
367
-
368
- } );
369
-
370
- } );
371
-
372
- // NaN/Inf debug: red where the path tracer output isn't finite, black otherwise.
373
- If( visMode.equal( int( 11 ) ), () => {
374
-
375
- finalColor.assign( nanInfToRed( finalColor ) );
376
-
377
- } );
378
-
379
- // Write outputs to StorageTextures
380
- textureStore( writeColorTex, uintCoord, vec4( finalColor.xyz, outputAlpha ) ).toWriteOnly();
381
- textureStore( writeNDTex, uintCoord, finalNormalDepth ).toWriteOnly();
382
- textureStore( writeAlbedoTex, uintCoord, vec4( finalAlbedo, 1.0 ) ).toWriteOnly();
383
-
384
- };
@@ -1,298 +0,0 @@
1
- /**
2
- * TileManager.js
3
- * Handles all tile-based rendering logic including tile bounds calculation,
4
- * spiral order generation, and tile bounds calculation.
5
- */
6
-
7
- export class TileManager {
8
-
9
- constructor( width, height, tiles ) {
10
-
11
- this.width = width;
12
- this.height = height;
13
- this.tiles = tiles;
14
- this.tileIndex = 0;
15
-
16
- // Performance caches
17
- this.tileBoundsCache = new Map();
18
- this.totalTilesCache = tiles * tiles;
19
-
20
- // Tile rendering state
21
- this.currentTileBounds = null;
22
-
23
- // Generate initial spiral order
24
- this.spiralOrder = this.generateSpiralOrder( tiles );
25
-
26
- }
27
-
28
- /**
29
- * Calculate the scissor bounds for a given tile
30
- * @param {number} tileIndex - The index of the tile
31
- * @param {number} totalTiles - Total number of tiles per row/column
32
- * @param {number} width - Render target width
33
- * @param {number} height - Render target height
34
- * @returns {Object} - Scissor bounds {x, y, width, height}
35
- */
36
- calculateTileBounds( tileIndex, totalTiles, width, height ) {
37
-
38
- // Use cache to avoid recalculation
39
- const cacheKey = `${tileIndex}-${totalTiles}-${width}-${height}`;
40
- if ( this.tileBoundsCache.has( cacheKey ) ) {
41
-
42
- return this.tileBoundsCache.get( cacheKey );
43
-
44
- }
45
-
46
- // Calculate tile size using ceiling division to ensure all pixels are covered
47
- const tileWidth = Math.ceil( width / totalTiles );
48
- const tileHeight = Math.ceil( height / totalTiles );
49
-
50
- // Calculate tile coordinates
51
- const tileX = tileIndex % totalTiles;
52
- const tileY = Math.floor( tileIndex / totalTiles );
53
-
54
- // Calculate pixel bounds
55
- const x = tileX * tileWidth;
56
- const y = tileY * tileHeight;
57
-
58
- // Clamp to actual render target bounds
59
- const clampedWidth = Math.min( tileWidth, width - x );
60
- const clampedHeight = Math.min( tileHeight, height - y );
61
-
62
- const bounds = {
63
- x: x,
64
- y: y,
65
- width: clampedWidth,
66
- height: clampedHeight
67
- };
68
-
69
- // Cache the result
70
- this.tileBoundsCache.set( cacheKey, bounds );
71
- return bounds;
72
-
73
- }
74
-
75
- /**
76
- * Set up scissor testing for tile rendering
77
- * @param {Object} renderer - The Three.js renderer
78
- * @param {Object} bounds - Scissor bounds {x, y, width, height}
79
- */
80
-
81
- /**
82
- * Generate spiral order for tile rendering (center-out pattern)
83
- * @param {number} tiles - Number of tiles per row/column
84
- * @returns {Array<number>} - Array of tile indices in spiral order
85
- */
86
- generateSpiralOrder( tiles ) {
87
-
88
- const totalTiles = tiles * tiles;
89
- const tilePositions = [];
90
-
91
- // Create array of tile positions with their distances from center
92
- for ( let i = 0; i < totalTiles; i ++ ) {
93
-
94
- const x = i % tiles;
95
- const y = Math.floor( i / tiles );
96
-
97
- // Improved distance calculation for better ordering with any tile count
98
- const center = ( tiles - 1 ) / 2;
99
- const dx = x - center;
100
- const dy = y - center;
101
-
102
- // Use Manhattan distance as primary sort key for more predictable ordering
103
- const manhattanDistance = Math.abs( dx ) + Math.abs( dy );
104
-
105
- // Use Euclidean distance as secondary sort key for smooth transitions
106
- const euclideanDistance = Math.sqrt( dx * dx + dy * dy );
107
-
108
- // Calculate angle with better precision for spiral ordering
109
- let angle = Math.atan( dy, dx );
110
- // Normalize angle to 0-2π range
111
- if ( angle < 0 ) angle += 2 * Math.PI;
112
-
113
- // Add small offset based on position to ensure deterministic ordering
114
- const positionOffset = ( x + y * tiles ) * 0.001;
115
-
116
- tilePositions.push( {
117
- index: i,
118
- x,
119
- y,
120
- manhattanDistance,
121
- euclideanDistance,
122
- angle,
123
- positionOffset
124
- } );
125
-
126
- }
127
-
128
- // Improved sorting: Manhattan distance first, then Euclidean, then angle
129
- tilePositions.sort( ( a, b ) => {
130
-
131
- // Primary: Manhattan distance (creates more predictable rings)
132
- const manhattanDiff = a.manhattanDistance - b.manhattanDistance;
133
- if ( Math.abs( manhattanDiff ) > 0.01 ) {
134
-
135
- return manhattanDiff;
136
-
137
- }
138
-
139
- // Secondary: Euclidean distance (smooth transitions within rings)
140
- const euclideanDiff = a.euclideanDistance - b.euclideanDistance;
141
- if ( Math.abs( euclideanDiff ) > 0.01 ) {
142
-
143
- return euclideanDiff;
144
-
145
- }
146
-
147
- // Tertiary: Angle for spiral effect within same distance
148
- const angleDiff = a.angle - b.angle;
149
- if ( Math.abs( angleDiff ) > 0.01 ) {
150
-
151
- return angleDiff;
152
-
153
- }
154
-
155
- // Final: Position offset for deterministic ordering
156
- return a.positionOffset - b.positionOffset;
157
-
158
- } );
159
-
160
- return tilePositions.map( pos => pos.index );
161
-
162
- }
163
-
164
- /**
165
- * Handle tile rendering logic for a given frame and render mode
166
- * @param {Object} renderer - The Three.js renderer
167
- * @param {number} renderMode - Current render mode (0 = full, 1 = tiled)
168
- * @param {number} frameValue - Current frame number
169
- * @returns {Object} - Tile rendering info {tileIndex, tileBounds, shouldSwapTargets, isCompleteCycle}
170
- */
171
- handleTileRendering( renderer, renderMode, frameValue ) {
172
-
173
- let shouldSwapTargets = true;
174
- let currentTileIndex = - 1;
175
- let tileBounds = null;
176
- let isCompleteCycle = true;
177
-
178
- if ( renderMode === 1 ) {
179
-
180
- if ( frameValue === 0 ) {
181
-
182
- currentTileIndex = - 1;
183
- isCompleteCycle = true;
184
-
185
- } else {
186
-
187
- const linearTileIndex = ( frameValue - 1 ) % this.totalTilesCache;
188
- currentTileIndex = this.spiralOrder[ linearTileIndex ];
189
- tileBounds = this.calculateTileBounds( currentTileIndex, this.tiles, this.width, this.height );
190
- isCompleteCycle = ( linearTileIndex === this.totalTilesCache - 1 );
191
- shouldSwapTargets = isCompleteCycle;
192
-
193
- }
194
-
195
- } else {
196
-
197
- currentTileIndex = - 1;
198
- isCompleteCycle = true;
199
-
200
- }
201
-
202
- this.tileIndex = currentTileIndex;
203
-
204
- return {
205
- tileIndex: currentTileIndex,
206
- tileBounds,
207
- shouldSwapTargets,
208
- isCompleteCycle
209
- };
210
-
211
- }
212
-
213
- /**
214
- * Set the number of tiles and regenerate order
215
- * @param {number} newTileCount - New tile count per row/column
216
- */
217
- setTileCount( newTileCount ) {
218
-
219
- // Validate tile count and provide warnings
220
- if ( newTileCount < 1 ) {
221
-
222
- console.warn( 'TileManager: Tile count must be at least 1, clamping to 1' );
223
- newTileCount = 1;
224
-
225
- }
226
-
227
- if ( newTileCount > 10 ) {
228
-
229
- console.warn( 'TileManager: Tile count > 10 may cause performance issues' );
230
-
231
- }
232
-
233
- if ( newTileCount > 6 ) {
234
-
235
- const totalTiles = newTileCount * newTileCount;
236
- console.warn( `TileManager: ${newTileCount}x${newTileCount} = ${totalTiles} tiles may impact performance and memory usage` );
237
-
238
- }
239
-
240
-
241
- this.tiles = newTileCount;
242
- this.totalTilesCache = newTileCount * newTileCount;
243
- this.tileIndex = 0;
244
- this.spiralOrder = this.generateSpiralOrder( newTileCount );
245
- this.tileBoundsCache.clear(); // Clear cache when tile count changes
246
-
247
- }
248
-
249
- /**
250
- * Update dimensions when render target size changes
251
- * @param {number} width - New width
252
- * @param {number} height - New height
253
- */
254
- setSize( width, height ) {
255
-
256
- this.width = width;
257
- this.height = height;
258
- this.tileBoundsCache.clear(); // Clear cache when size changes
259
-
260
- }
261
-
262
- /**
263
- * Calculate completion threshold for tiled rendering
264
- * @param {number} maxFrames - Maximum frames to render
265
- * @returns {number} - Total frames needed for completion
266
- */
267
- calculateCompletionThreshold( maxFrames ) {
268
-
269
- return this.totalTilesCache * maxFrames;
270
-
271
- }
272
-
273
- /**
274
- * Get current tile information
275
- * @returns {Object} - Current tile state
276
- */
277
- getCurrentTileInfo() {
278
-
279
- return {
280
- tileIndex: this.tileIndex,
281
- tiles: this.tiles,
282
- totalTiles: this.totalTilesCache,
283
- currentBounds: this.currentTileBounds
284
- };
285
-
286
- }
287
-
288
- /**
289
- * Clean up resources
290
- */
291
- dispose() {
292
-
293
- this.tileBoundsCache.clear();
294
- this.currentTileBounds = null;
295
-
296
- }
297
-
298
- }