@woosh/meep-engine 2.165.7 → 2.167.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 (146) hide show
  1. package/build/bundle-worker-image-decoder.js +1 -1
  2. package/package.json +1 -1
  3. package/src/CORRECTNESS_HUNT_2026_07_24.md +142 -0
  4. package/src/core/binary/BinaryBuffer.js +1 -1
  5. package/src/core/binary/EncodingBinaryBuffer.d.ts.map +1 -1
  6. package/src/core/binary/EncodingBinaryBuffer.js +3 -1
  7. package/src/core/cache/Cache.d.ts.map +1 -1
  8. package/src/core/cache/Cache.js +0 -4
  9. package/src/core/cache/CacheElement.d.ts.map +1 -1
  10. package/src/core/cache/CacheElement.js +4 -0
  11. package/src/core/cache/FrequencySketch.d.ts +0 -21
  12. package/src/core/cache/FrequencySketch.d.ts.map +1 -1
  13. package/src/core/cache/FrequencySketch.js +66 -65
  14. package/src/core/cache/wtinylfu/CacheElementWTinylfu.d.ts +17 -0
  15. package/src/core/cache/wtinylfu/CacheElementWTinylfu.d.ts.map +1 -0
  16. package/src/core/cache/wtinylfu/CacheElementWTinylfu.js +18 -0
  17. package/src/core/cache/wtinylfu/CacheRegionType.d.ts +10 -0
  18. package/src/core/cache/wtinylfu/CacheRegionType.d.ts.map +1 -0
  19. package/src/core/cache/wtinylfu/CacheRegionType.js +10 -0
  20. package/src/core/cache/wtinylfu/CacheRegionWTinylfu.d.ts +66 -0
  21. package/src/core/cache/wtinylfu/CacheRegionWTinylfu.d.ts.map +1 -0
  22. package/src/core/cache/wtinylfu/CacheRegionWTinylfu.js +183 -0
  23. package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts +279 -0
  24. package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts.map +1 -0
  25. package/src/core/cache/wtinylfu/CacheWTinylfu.js +807 -0
  26. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +243 -0
  27. package/src/core/cache/wtinylfu/wtinylfuDecisions.d.ts +80 -0
  28. package/src/core/cache/wtinylfu/wtinylfuDecisions.d.ts.map +1 -0
  29. package/src/core/cache/wtinylfu/wtinylfuDecisions.js +102 -0
  30. package/src/core/collection/RingBuffer.d.ts.map +1 -1
  31. package/src/core/collection/RingBuffer.js +15 -3
  32. package/src/core/collection/map/HashMap.d.ts +1 -1
  33. package/src/core/collection/map/HashMap.d.ts.map +1 -1
  34. package/src/core/collection/map/HashMap.js +5 -17
  35. package/src/core/geom/2d/line/line2_compute_segment_point_distance_sqr.d.ts.map +1 -1
  36. package/src/core/geom/2d/line/line2_compute_segment_point_distance_sqr.js +5 -0
  37. package/src/core/geom/ConicRay.d.ts.map +1 -1
  38. package/src/core/geom/ConicRay.js +9 -2
  39. package/src/core/geom/Quaternion.d.ts.map +1 -1
  40. package/src/core/geom/Quaternion.js +7 -3
  41. package/src/core/math/build_gaussian_kernel_1d.d.ts +2 -2
  42. package/src/core/math/build_gaussian_kernel_1d.d.ts.map +1 -1
  43. package/src/core/math/build_gaussian_kernel_1d.js +4 -2
  44. package/src/core/math/build_gaussian_kernel_2d.d.ts +2 -2
  45. package/src/core/math/build_gaussian_kernel_2d.d.ts.map +1 -1
  46. package/src/core/math/build_gaussian_kernel_2d.js +4 -2
  47. package/src/core/math/gaussian.d.ts +2 -2
  48. package/src/core/math/gaussian.js +2 -2
  49. package/src/core/math/spline/spline3_bezier_bounds.d.ts.map +1 -1
  50. package/src/core/math/spline/spline3_bezier_bounds.js +10 -3
  51. package/src/core/math/spline/spline3_bezier_to_hermite.d.ts +8 -7
  52. package/src/core/math/spline/spline3_bezier_to_hermite.d.ts.map +1 -1
  53. package/src/core/math/spline/spline3_bezier_to_hermite.js +13 -12
  54. package/src/core/math/spline/spline3_hermite_bounds.d.ts.map +1 -1
  55. package/src/core/math/spline/spline3_hermite_bounds.js +10 -3
  56. package/src/core/math/spline/spline3_hermite_bounds_t.d.ts.map +1 -1
  57. package/src/core/math/spline/spline3_hermite_bounds_t.js +115 -109
  58. package/src/core/math/spline/spline3_hermite_to_bezier.d.ts +2 -0
  59. package/src/core/math/spline/spline3_hermite_to_bezier.d.ts.map +1 -1
  60. package/src/core/math/spline/spline3_hermite_to_bezier.js +2 -0
  61. package/src/core/math/statistics/computeStatisticalMean.js +1 -1
  62. package/src/core/math/statistics/gaussian_amplitude.d.ts.map +1 -1
  63. package/src/core/math/statistics/gaussian_amplitude.js +3 -1
  64. package/src/engine/animation/clip/ecd_bind_animation_curve.d.ts.map +1 -1
  65. package/src/engine/animation/clip/ecd_bind_animation_curve.js +10 -1
  66. package/src/engine/animation/clip/hemisphere_align_quaternion_values.d.ts +20 -0
  67. package/src/engine/animation/clip/hemisphere_align_quaternion_values.d.ts.map +1 -0
  68. package/src/engine/animation/clip/hemisphere_align_quaternion_values.js +37 -0
  69. package/src/engine/animation/keyed2/AnimationTrackPlayback.d.ts.map +1 -1
  70. package/src/engine/animation/keyed2/AnimationTrackPlayback.js +5 -1
  71. package/src/engine/ecs/EntityComponentDataset.d.ts.map +1 -1
  72. package/src/engine/ecs/EntityComponentDataset.js +5 -21
  73. package/src/engine/ecs/fow/FogOfWar.d.ts +22 -0
  74. package/src/engine/ecs/fow/FogOfWar.d.ts.map +1 -1
  75. package/src/engine/ecs/fow/FogOfWar.js +202 -54
  76. package/src/engine/ecs/fow/FogOfWarEdgeMode.d.ts +16 -0
  77. package/src/engine/ecs/fow/FogOfWarEdgeMode.d.ts.map +1 -0
  78. package/src/engine/ecs/fow/FogOfWarEdgeMode.js +39 -0
  79. package/src/engine/ecs/fow/FogOfWarSystem.d.ts.map +1 -1
  80. package/src/engine/ecs/fow/FogOfWarSystem.js +1 -0
  81. package/src/engine/ecs/fow/serialization/FogOfWarSerializationAdapter.d.ts.map +1 -1
  82. package/src/engine/ecs/fow/serialization/FogOfWarSerializationAdapter.js +3 -0
  83. package/src/engine/ecs/fow/shader/FogOfWarRenderer.d.ts +7 -0
  84. package/src/engine/ecs/fow/shader/FogOfWarRenderer.d.ts.map +1 -1
  85. package/src/engine/ecs/fow/shader/FogOfWarRenderer.js +155 -145
  86. package/src/engine/ecs/fow/shader/screenSpaceFogOfWarShader.d.ts.map +1 -1
  87. package/src/engine/ecs/fow/shader/screenSpaceFogOfWarShader.js +33 -8
  88. package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.d.ts.map +1 -1
  89. package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.js +3 -1
  90. package/src/engine/graphics/GraphicsEngine.d.ts +5 -1
  91. package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
  92. package/src/engine/graphics/GraphicsEngine.js +14 -3
  93. package/src/engine/graphics/composit/CompositLayer.d.ts.map +1 -1
  94. package/src/engine/graphics/composit/CompositLayer.js +258 -255
  95. package/src/engine/graphics/composit/LayerCompositer.d.ts +12 -0
  96. package/src/engine/graphics/composit/LayerCompositer.d.ts.map +1 -1
  97. package/src/engine/graphics/composit/LayerCompositer.js +19 -10
  98. package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.d.ts +1 -1
  99. package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.d.ts.map +1 -1
  100. package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.js +10 -4
  101. package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.d.ts +6 -0
  102. package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.d.ts.map +1 -1
  103. package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.js +21 -0
  104. package/src/engine/graphics/ecs/path/ribbon/RibbonPathBuilder.d.ts.map +1 -1
  105. package/src/engine/graphics/ecs/path/ribbon/RibbonPathBuilder.js +5 -23
  106. package/src/engine/graphics/ecs/path/tube/build/estimatePathViaIterativeIntegral.d.ts.map +1 -1
  107. package/src/engine/graphics/ecs/path/tube/build/estimatePathViaIterativeIntegral.js +6 -4
  108. package/src/engine/graphics/ecs/water/WaterSystem.d.ts.map +1 -1
  109. package/src/engine/graphics/ecs/water/WaterSystem.js +9 -3
  110. package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.d.ts +12 -0
  111. package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.d.ts.map +1 -1
  112. package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.js +46 -9
  113. package/src/engine/graphics/particles/particular/engine/ParticularEngine.d.ts +5 -0
  114. package/src/engine/graphics/particles/particular/engine/ParticularEngine.d.ts.map +1 -1
  115. package/src/engine/graphics/particles/particular/engine/ParticularEngine.js +8 -0
  116. package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.d.ts +7 -0
  117. package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.d.ts.map +1 -1
  118. package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.js +8 -0
  119. package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.d.ts +10 -2
  120. package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.d.ts.map +1 -1
  121. package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.js +42 -10
  122. package/src/engine/graphics/render/buffer/FrameBuffer.d.ts.map +1 -1
  123. package/src/engine/graphics/render/buffer/FrameBuffer.js +5 -2
  124. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts +13 -1
  125. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
  126. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +40 -7
  127. package/src/engine/graphics/render/buffer/simple-fx/taa/TemporalSupersamplingRenderPlugin.d.ts.map +1 -1
  128. package/src/engine/graphics/render/buffer/simple-fx/taa/TemporalSupersamplingRenderPlugin.js +10 -4
  129. package/src/engine/graphics/render/forward_plus/LightManager.d.ts.map +1 -1
  130. package/src/engine/graphics/render/forward_plus/LightManager.js +1235 -1232
  131. package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.d.ts +5 -0
  132. package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.d.ts.map +1 -1
  133. package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.js +11 -10
  134. package/src/engine/graphics/trail/tube/TubeX.d.ts.map +1 -1
  135. package/src/engine/graphics/trail/tube/TubeX.js +557 -555
  136. package/src/engine/graphics/trail/x/RibbonXPlugin.d.ts +12 -1
  137. package/src/engine/graphics/trail/x/RibbonXPlugin.d.ts.map +1 -1
  138. package/src/engine/graphics/trail/x/RibbonXPlugin.js +15 -8
  139. package/src/engine/input/ecs/systems/InputControllerSystem.d.ts.map +1 -1
  140. package/src/engine/input/ecs/systems/InputControllerSystem.js +9 -6
  141. package/src/engine/navigation/ecs/components/Path.d.ts +5 -2
  142. package/src/engine/navigation/ecs/components/Path.d.ts.map +1 -1
  143. package/src/engine/navigation/ecs/components/Path.js +10 -4
  144. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.js +1 -1
  145. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  146. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
@@ -1,1232 +1,1235 @@
1
- import { m4_multiply } from "../../../../core/geom/3d/mat4/m4_multiply.js";
2
- import {
3
- ClampToEdgeWrapping,
4
- DataTexture,
5
- DataTexture3D,
6
- Frustum,
7
- LinearFilter,
8
- LinearMipMapLinearFilter,
9
- NearestFilter,
10
- RGBAFormat,
11
- RGBAIntegerFormat,
12
- UnsignedByteType,
13
- UnsignedShortType
14
- } from "three";
15
- import { assert } from "../../../../core/assert.js";
16
- import { computeBinaryDataTypeByPrecision } from "../../../../core/binary/type/computeBinaryDataTypeByPrecision.js";
17
- import {
18
- DataType2TypedArrayConstructorMapping
19
- } from "../../../../core/binary/type/DataType2TypedArrayConstructorMapping.js";
20
- import { BinaryUint32BVH } from "../../../../core/bvh2/binary/2/BinaryUint32BVH.js";
21
- import { BVH } from "../../../../core/bvh2/bvh3/BVH.js";
22
- import {
23
- bvh_query_user_data_intersects_frustum
24
- } from "../../../../core/bvh2/bvh3/query/bvh_query_user_data_intersects_frustum.js";
25
- import { array_copy } from "../../../../core/collection/array/array_copy.js";
26
- import { array_sort_quick } from "../../../../core/collection/array/array_sort_quick.js";
27
- import { array_swap_one } from "../../../../core/collection/array/array_swap_one.js";
28
- import { frustum3_planes_to_flat_from_three } from "../../../../core/geom/3d/frustum/frustum3_planes_to_flat_from_three.js";
29
- import { frustum3_slice_linear_to_points } from "../../../../core/geom/3d/frustum/frustum3_slice_linear_to_points.js";
30
- import { v3_morton_encode_transformed } from "../../../../core/geom/3d/morton/v3_morton_encode_transformed.js";
31
- import { v3_distance } from "../../../../core/geom/vec3/v3_distance.js";
32
- import Vector3 from "../../../../core/geom/Vector3.js";
33
- import { NumericType } from "../../../../core/math/NumericType.js";
34
- import { frustum_from_camera } from "../../ecs/camera/frustum_from_camera.js";
35
- import { CachingTextureAtlas } from "../../texture/atlas/CachingTextureAtlas.js";
36
- import { ReferencedTextureAtlas } from "../../texture/atlas/ReferencedTextureAtlas.js";
37
- import { TextureAtlas } from "../../texture/atlas/TextureAtlas.js";
38
- import {
39
- computeThreeTextureInternalFormatFromDataType
40
- } from "../../texture/computeThreeTextureInternalFormatFromDataType.js";
41
- import { computeThreeTextureTypeFromDataType } from "../../texture/computeThreeTextureTypeFromDataType.js";
42
- import { writeSample2DDataToDataTexture } from "../../texture/sampler/writeSampler2DDataToDataTexture.js";
43
- import { TextureBackedMemoryRegion } from "../../texture/TextureBackedMemoryRegion.js";
44
- import { IncrementalDeltaSet } from "../visibility/IncrementalDeltaSet.js";
45
- import { assign_cluster, LOOKUP_CACHE, scratch_corners, scratch_frustum_planes } from "./assign_cluster.js";
46
- import { compute_cluster_planes_from_points } from "./cluster/compute_cluster_planes_from_points.js";
47
- import { read_plane_pair } from "./cluster/read_plane_pair.js";
48
- import { computeFrustumCorners } from "./computeFrustumCorners.js";
49
- import { LightRenderMetadata } from "./LightRenderMetadata.js";
50
- import { Decal } from "./model/Decal.js";
51
- import { point_light_inside_volume } from "./query/point_light_inside_volume.js";
52
-
53
-
54
- /**
55
- * How many bits are used to encode light type
56
- * @type {number}
57
- */
58
- const LIGHT_ENCODING_TYPE_BIT_COUNT = 2;
59
-
60
- /**
61
- *
62
- * @param {AbstractLight|PointLight|Decal} light
63
- * @returns {number}
64
- */
65
- function encode_light_type(light) {
66
- if (light.isPointLight === true) {
67
- return 0;
68
- } else if (light.isDecal === true) {
69
- return 3;
70
- } else {
71
- throw new Error('Unsupported light type');
72
- }
73
- }
74
-
75
- /**
76
- *
77
- * @param {number} address
78
- * @param {AbstractLight} light
79
- * @returns {number}
80
- */
81
- function encode_light_descriptor(address, light) {
82
- const light_type = encode_light_type(light);
83
- return light_type | (address << 2);
84
- }
85
-
86
- /**
87
- *
88
- * @param {IncrementalDeltaSet<LightRenderMetadata>} data_set
89
- * @param {BinaryUint32BVH} bvh
90
- */
91
- function build_light_data_bvh(data_set, bvh) {
92
- const elements = data_set.elements;
93
- const element_count = data_set.size;
94
-
95
- bvh.setLeafCount(element_count);
96
- bvh.initialize_structure();
97
-
98
- for (let i = 0; i < element_count; i++) {
99
-
100
- /**
101
- *
102
- * @type {LightRenderMetadata}
103
- */
104
- const datum = elements[i];
105
-
106
- const bb = datum.bvh_leaf.bounds;
107
-
108
- const payload = encode_light_descriptor(datum.address, datum.light);
109
-
110
- bvh.setLeafData(
111
- i, payload,
112
- bb[0], bb[1], bb[2],
113
- bb[3], bb[4], bb[5],
114
- );
115
- }
116
-
117
- // vbvh.sort_morton(this.__projection_matrix);
118
- // vbvh.sort_bubble_sah();
119
-
120
- bvh.build();
121
- }
122
-
123
- /**
124
- *
125
- * @param {LightRenderMetadata} a
126
- * @param {LightRenderMetadata} b
127
- * @returns {number}
128
- */
129
- function compareLRMByLightId(a, b) {
130
- return b.light.id - a.light.id;
131
- }
132
-
133
- export class LightManager {
134
-
135
- /**
136
- * @readonly
137
- * @type {BVH}
138
- */
139
- #light_data_bvh = new BVH();
140
-
141
- /**
142
- * Mapping from a light/decal object ID internal metadata
143
- * @type {Map<number,LightRenderMetadata>}
144
- */
145
- #metadata_map = new Map();
146
-
147
-
148
- constructor() {
149
-
150
- /**
151
- * Number of cluster slices along each dimension
152
- * @type {Vector3}
153
- * @private
154
- */
155
- this.__tiles_resolution = new Vector3(32, 16, 8);
156
-
157
- this.__cluster_texture_precision = 8;
158
- this.__cluster_texture_needs_rebuild = false;
159
-
160
- /**
161
- * Texel layout:
162
- * - R: offset into lookup texture for lights
163
- * - G: light count
164
- * - B: offset into lookup texture for decals
165
- * - A: decal count
166
- * @type {DataTexture3D}
167
- * @private
168
- */
169
- this.__cluster_texture = new DataTexture3D(
170
- new Uint16Array(4),
171
- 1,
172
- 1,
173
- 1,
174
- );
175
-
176
- this.__cluster_texture.flipY = false;
177
- this.__cluster_texture.generateMipmaps = false;
178
- this.__cluster_texture.unpackAlignment = 2;
179
-
180
- this.__cluster_texture.magFilter = NearestFilter;
181
- this.__cluster_texture.minFilter = NearestFilter;
182
-
183
- this.__cluster_texture.type = UnsignedShortType;
184
- this.__cluster_texture.format = RGBAIntegerFormat;
185
- this.__cluster_texture.internalFormat = "RGBA16UI";
186
-
187
- /**
188
- *
189
- * @type {TextureBackedMemoryRegion}
190
- * @readonly
191
- * @private
192
- */
193
- this.__lookup_data = new TextureBackedMemoryRegion();
194
- this.__lookup_data.type = NumericType.Uint;
195
- this.__lookup_data.channel_count = 1;
196
- this.__lookup_data.precision = 8;
197
- this.__lookup_data.resize(1);
198
-
199
- /**
200
- *
201
- * @type {TextureBackedMemoryRegion}
202
- * @readonly
203
- * @private
204
- */
205
- this.__light_data = new TextureBackedMemoryRegion();
206
- this.__light_data.type = NumericType.Float;
207
- this.__light_data.precision = 32;
208
- this.__light_data.channel_count = 4;
209
- this.__light_data.resize(8);
210
-
211
- /**
212
- * @type {DataTexture}
213
- * @readonly
214
- * @private
215
- */
216
- this.__decal_atlas_texture = new DataTexture(new Uint8Array(4), 1, 1, RGBAFormat);
217
- this.__decal_atlas_texture.type = UnsignedByteType;
218
- this.__decal_atlas_texture.flipY = false;
219
- this.__decal_atlas_texture.wrapS = ClampToEdgeWrapping;
220
- this.__decal_atlas_texture.wrapT = ClampToEdgeWrapping;
221
- this.__decal_atlas_texture.minFilter = LinearMipMapLinearFilter;
222
- this.__decal_atlas_texture.magFilter = LinearFilter;
223
- this.__decal_atlas_texture.generateMipmaps = true;
224
- this.__decal_atlas_texture.unpackAlignment = 4;
225
- this.__decal_atlas_texture.anisotropy = 8;
226
-
227
- /**
228
- *
229
- * @type {AbstractTextureAtlas}
230
- * @readonly
231
- * @private
232
- */
233
- this.__decal_atlas = new CachingTextureAtlas({
234
- atlas: new TextureAtlas(512)
235
- });
236
-
237
- /**
238
- *
239
- * @type {ReferencedTextureAtlas}
240
- * @private
241
- */
242
- this.__decal_patch_references = new ReferencedTextureAtlas(this.__decal_atlas);
243
-
244
- /**
245
- *
246
- * @type {Map<Decal, Reference<AtlasPatch>>}
247
- * @private
248
- */
249
- this.__decal_references = new Map();
250
-
251
- /**
252
- *
253
- * @type {IncrementalDeltaSet<LightRenderMetadata>}
254
- * @readonly
255
- * @private
256
- */
257
- this.__visible_lights = new IncrementalDeltaSet(compareLRMByLightId);
258
- /**
259
- *
260
- * @type {IncrementalDeltaSet<LightRenderMetadata>}
261
- * @readonly
262
- * @private
263
- */
264
- this.__visible_decals = new IncrementalDeltaSet(compareLRMByLightId);
265
-
266
- /**
267
- *
268
- * @type {LightRenderMetadata[]}
269
- * @private
270
- */
271
- this.__sorted_visible_lights = [];
272
-
273
- /**
274
- *
275
- * @type {BinaryUint32BVH}
276
- * @private
277
- */
278
- this.__visible_bvh_lights = new BinaryUint32BVH();
279
- /**
280
- *
281
- * @type {BinaryUint32BVH}
282
- * @private
283
- */
284
- this.__visible_bvh_decals = new BinaryUint32BVH();
285
-
286
- /**
287
- *
288
- * @type {Frustum}
289
- * @private
290
- */
291
- this.__view_frustum = new Frustum();
292
-
293
- /**
294
- * Corner points of the view frustum
295
- * @type {Float32Array}
296
- * @private
297
- */
298
- this.__view_frustum_points = new Float32Array(24);
299
-
300
- /**
301
- *
302
- * @type {Float32Array|mat4|number[]}
303
- * @private
304
- */
305
- this.__projection_matrix = new Float32Array(16);
306
-
307
-
308
- /**
309
- * Accelerated data structure with pre-computed plane normals for clusters in each dimensions. Order: X, Y, Z
310
- * @type {Float32Array}
311
- * @private
312
- */
313
- this.__cluster_planes = new Float32Array(1);
314
-
315
- /**
316
- * Accelerated data structure with pre-computed corner coordinates of each cluster's frustum
317
- * @type {Float32Array}
318
- * @private
319
- */
320
- this.__cluster_frustum_points = new Float32Array(1);
321
-
322
- this.setTileMapResolution(32, 16, 16);
323
-
324
- this.__visible_lights.onAdded.add(this.__handle_visible_light_added, this);
325
- this.__visible_lights.onRemoved.add(this.__handle_visible_light_removed, this);
326
-
327
- this.__visible_decals.onAdded.add(this.__handle_visible_decal_added, this);
328
- this.__visible_decals.onRemoved.add(this.__handle_visible_decal_removed, this);
329
-
330
- /**
331
- * Data needs to be re-written into the data texture
332
- * Usually set when source lights change
333
- * @type {boolean}
334
- * @private
335
- */
336
- this.__light_data_needs_update = true;
337
-
338
- /**
339
- *
340
- * @type {boolean}
341
- * @private
342
- */
343
- this.__visible_bvh_needs_update = true;
344
-
345
- // window.light_manager = this; // DEBUG
346
- }
347
-
348
- requestDataUpdate() {
349
- this.__light_data_needs_update = true;
350
- }
351
-
352
- /**
353
- * Please set this to false if you have a lot of overlapping decals in the scene
354
- * Overlapping decals can provide filtering artifacts due to incorrect mipmap level detection by WebGL
355
- * see article: https://0fps.net/2013/07/09/texture-atlases-wrapping-and-mip-mapping/
356
- * NOTE: the technique mentioned in the article above is not implemented, as it would require significant increase in number of texture fetches
357
- * @param {boolean} v
358
- */
359
- set decal_filtering_enabled(v) {
360
- const t = this.__decal_atlas_texture;
361
-
362
- if (t.generateMipmaps === v) {
363
- return;
364
- }
365
-
366
- t.generateMipmaps = v;
367
- }
368
-
369
- /**
370
- *
371
- * @return {boolean}
372
- */
373
- get decal_filtering_enabled() {
374
- return this.__decal_atlas_texture.generateMipmaps;
375
- }
376
-
377
- __update_decal_atlas_texture() {
378
- const sampler = this.__decal_atlas.sampler;
379
-
380
- if (sampler.version !== this.__decal_atlas_texture.version) {
381
- writeSample2DDataToDataTexture(sampler, this.__decal_atlas_texture);
382
-
383
- this.__decal_atlas_texture.version = sampler.version;
384
- }
385
- }
386
-
387
- /**
388
- * Sometimes light lookup table can get quite large, to make sure we can store these addresses in the cluster texture,
389
- * we may need to switch to a larger data type, such as Uint8->Uint16 or Uint16->Uint32
390
- * @param {number} bit_count How many bits should be addressable from the cluster content
391
- * @returns {boolean} true if texture was rebuilt (underlying type change), false otherwise
392
- * @private
393
- */
394
- __set_cluster_addressable_bit_range(bit_count) {
395
- const rounded_value = Math.ceil(bit_count);
396
-
397
- this.__cluster_texture_precision = rounded_value;
398
- const dataType = computeBinaryDataTypeByPrecision(NumericType.Uint, rounded_value);
399
-
400
- const threeTextureType = computeThreeTextureTypeFromDataType(dataType);
401
-
402
- const texture = this.__cluster_texture;
403
- if (threeTextureType !== texture.type) {
404
- this.__cluster_texture_needs_rebuild = true;
405
-
406
- return true;
407
- } else {
408
- return false;
409
- }
410
- }
411
-
412
- __update_cluster_texture() {
413
- if (this.__cluster_texture_needs_rebuild) {
414
- this.__build_cluster_texture();
415
- }
416
- }
417
-
418
- __build_cluster_texture() {
419
- const dataType = computeBinaryDataTypeByPrecision(NumericType.Uint, this.__cluster_texture_precision);
420
-
421
- const threeTextureType = computeThreeTextureTypeFromDataType(dataType);
422
-
423
- const texture = this.__cluster_texture;
424
-
425
- const channelCount = 4;
426
-
427
- texture.dispose();
428
-
429
- const image = texture.image;
430
-
431
- const resolution = this.__tiles_resolution;
432
-
433
- image.width = resolution.x;
434
- image.height = resolution.y;
435
- image.depth = resolution.z;
436
-
437
- texture.type = threeTextureType;
438
- texture.internalFormat = computeThreeTextureInternalFormatFromDataType(dataType, channelCount);
439
-
440
- const ArrayConstructor = DataType2TypedArrayConstructorMapping[dataType];
441
-
442
- image.data = new ArrayConstructor(resolution.x * resolution.y * resolution.z * channelCount);
443
-
444
- texture.needsUpdate = true;
445
-
446
- this.__cluster_texture_needs_rebuild = false;
447
- }
448
-
449
- /**
450
- *
451
- * @param {number} size
452
- * @returns {boolean}
453
- * @private
454
- */
455
- __ensure_lookup_size(size) {
456
- return this.__lookup_data.resize(size);
457
- }
458
-
459
- /**
460
- *
461
- * @returns {DataTexture}
462
- */
463
- getTextureLookup() {
464
- return this.__lookup_data.getTexture();
465
- }
466
-
467
- /**
468
- *
469
- * @returns {DataTexture}
470
- */
471
- getTextureData() {
472
- return this.__light_data.getTexture();
473
- }
474
-
475
- /**
476
- * @returns {DataTexture3D}
477
- */
478
- getTextureClusters() {
479
- return this.__cluster_texture;
480
- }
481
-
482
- /**
483
- * @returns {DataTexture}
484
- */
485
- getTextureDecalAtlas() {
486
- return this.__decal_atlas_texture;
487
- }
488
-
489
- /**
490
- * NOTE: do not modify the value
491
- * @returns {Readonly<Vector3>}
492
- */
493
- getResolution() {
494
- return this.__tiles_resolution;
495
- }
496
-
497
- /**
498
- *
499
- * @private
500
- */
501
- __handle_light_dimensions_change() {
502
- // internal BVH needs to be rebuilt
503
- this.__visible_bvh_needs_update = true;
504
- // when dimensions change, typically data will need to be re-written to the GPU, things like light positions or projection matrix
505
- this.__light_data_needs_update = true;
506
- }
507
-
508
- /**
509
- *
510
- * @private
511
- */
512
- __handle_light_appearance_change() {
513
- // appearance changes (such as color or intensity) don't affect bounds, only the encoded data
514
- this.__light_data_needs_update = true;
515
- }
516
-
517
- /**
518
- *
519
- * @param {LightRenderMetadata} data
520
- * @private
521
- */
522
- __handle_visible_decal_added(data) {
523
- this.__light_data_needs_update = true;
524
-
525
- /**
526
- *
527
- * @type {Decal}
528
- */
529
- const light = data.light;
530
-
531
- light.onDimensionChanged(this.__handle_light_dimensions_change, this);
532
-
533
- const ref = this.__decal_patch_references.acquire(light.texture_diffuse);
534
-
535
- const patch = ref.getValue();
536
- const patch_uv = patch.uv;
537
-
538
- patch_uv.position.onChanged.add(light.handleUvPositionChange, light);
539
- patch_uv.size.onChanged.add(light.handleUvSizeChange, light);
540
-
541
- light.uv[0] = patch_uv.position.x;
542
- light.uv[1] = patch_uv.position.y;
543
- light.uv[2] = patch_uv.size.x;
544
- light.uv[3] = patch_uv.size.y;
545
-
546
- this.__decal_references.set(light, ref);
547
- }
548
-
549
- /**
550
- *
551
- * @param {LightRenderMetadata} data
552
- * @private
553
- */
554
- __handle_visible_decal_removed(data) {
555
- this.__light_data_needs_update = true;
556
-
557
- /**
558
- *
559
- * @type {Decal}
560
- */
561
- const light = data.light;
562
-
563
- light.offDimensionChanged(this.__handle_light_dimensions_change, this);
564
-
565
- const ref = this.__decal_references.get(light);
566
-
567
- if (ref === undefined) {
568
- // This can occur when decal changes while being in the visible list, messing with the IncrementalDeltaSet's compare order
569
- console.warn(`Decal reference not found: ${light}`);
570
- return;
571
- }
572
-
573
- const patch = ref.getValue();
574
- const patch_uv = patch.uv;
575
-
576
- // unsubscribe
577
- patch_uv.position.onChanged.remove(light.handleUvPositionChange, light);
578
- patch_uv.size.onChanged.remove(light.handleUvSizeChange, light);
579
-
580
-
581
- ref.release();
582
-
583
- this.__decal_references.delete(light);
584
- }
585
-
586
- /**
587
- *
588
- * @param {LightRenderMetadata} data
589
- * @private
590
- */
591
- __handle_visible_light_added(data) {
592
- this.__light_data_needs_update = true;
593
-
594
- const light = data.light;
595
-
596
- light.onDimensionChanged(this.__handle_light_dimensions_change, this);
597
- light.onAppearanceChanged(this.__handle_light_appearance_change, this);
598
- }
599
-
600
- /**
601
- *
602
- * @param {LightRenderMetadata} data
603
- * @private
604
- */
605
- __handle_visible_light_removed(data) {
606
- this.__light_data_needs_update = true;
607
-
608
- const light = data.light;
609
- light.offDimensionChanged(this.__handle_light_dimensions_change, this);
610
- light.offAppearanceChanged(this.__handle_light_appearance_change, this);
611
- }
612
-
613
- /**
614
- *
615
- * @param {LightRenderMetadata} light
616
- * @returns {number}
617
- * @protected
618
- */
619
- __sort_visible_light_score(light) {
620
- const center = scratch_corners;
621
-
622
- light.light.getCenter(center)
623
-
624
- const x = center[0];
625
- const y = center[1];
626
- const z = center[2];
627
-
628
- return v3_morton_encode_transformed(x, y, z, this.__projection_matrix);
629
- }
630
-
631
- /**
632
- * DEBUG method
633
- * @param {LightRenderMetadata} lights
634
- * @param {number} count
635
- * @returns {number} lower = better sorting score
636
- * @private
637
- */
638
- __assess_sorting_score(lights, count) {
639
-
640
- let r = 0;
641
-
642
- const center_a = [];
643
- const center_b = [];
644
-
645
- for (let i = 0; i < count; i++) {
646
- const a = lights[i];
647
-
648
- const score_a = this.__sort_visible_light_score(a);
649
- a.light.getCenter(center_a);
650
-
651
- for (let j = i + 1; j < count; j++) {
652
- const b = lights[j];
653
-
654
- const score_b = this.__sort_visible_light_score(b);
655
- b.light.getCenter(center_b);
656
-
657
- //
658
- const score_distance = Math.abs(score_a - score_b);
659
- const position_distance = v3_distance(
660
- center_a[0], center_a[1], center_a[2],
661
- center_b[0], center_b[1], center_b[2],
662
- );
663
-
664
- r += score_distance / position_distance;
665
- }
666
- }
667
-
668
-
669
- return r / (count * (count - 1) * 0.5);
670
- }
671
-
672
- /**
673
- * Sort lights for better data locality
674
- * @private
675
- */
676
- __sort_visible_light() {
677
- const sorted_lights = this.__sorted_visible_lights;
678
-
679
- const visible_light_set = this.__visible_lights;
680
- const visible_light_count = visible_light_set.size;
681
-
682
- const visible_decal_set = this.__visible_decals;
683
- const visible_decal_count = visible_decal_set.size;
684
-
685
- const expected_sorted_size = visible_light_count + visible_decal_count;
686
-
687
- if (expected_sorted_size > visible_light_count) {
688
- // crop array if it's too big
689
- sorted_lights.splice(visible_light_count, expected_sorted_size - visible_light_count)
690
- }
691
-
692
- array_copy(visible_light_set.elements, 0, sorted_lights, 0, visible_light_count);
693
- array_copy(visible_decal_set.elements, 0, sorted_lights, visible_light_count, visible_decal_count);
694
-
695
- array_sort_quick(sorted_lights, this.__sort_visible_light_score, this, 0, expected_sorted_size - 1);
696
- }
697
-
698
- __update_visible_bvh() {
699
- this.__visible_bvh_needs_update = false;
700
-
701
- build_light_data_bvh(
702
- this.__visible_lights,
703
- this.__visible_bvh_lights
704
- );
705
-
706
- build_light_data_bvh(
707
- this.__visible_decals,
708
- this.__visible_bvh_decals
709
- );
710
- }
711
-
712
-
713
- /**
714
- *
715
- * @param {Camera} camera
716
- * @private
717
- */
718
- __build_view_frustum(camera) {
719
- frustum_from_camera(camera, this.__view_frustum, false);
720
-
721
- array_swap_one(this.__view_frustum.planes, 4, 5);
722
-
723
- this.__build_view_frustum_points();
724
- }
725
-
726
- __build_visible_light_list() {
727
-
728
- /**
729
- *
730
- * @type {IncrementalDeltaSet<LightRenderMetadata>}
731
- */
732
- const visible_lights = this.__visible_lights;
733
-
734
- visible_lights.initializeUpdate();
735
-
736
- const visible_decals = this.__visible_decals;
737
-
738
- visible_decals.initializeUpdate();
739
-
740
- const nodes = [];
741
-
742
- frustum3_planes_to_flat_from_three(this.__view_frustum.planes, scratch_frustum_planes)
743
-
744
- /*
745
- Search is done in 2 phases:
746
- 1) broad phase using bounding boxes
747
- 2) granular phase where we use object-specific shape to check against frustum
748
- */
749
-
750
- const broad_match_count = bvh_query_user_data_intersects_frustum(nodes, 0, this.#light_data_bvh, scratch_frustum_planes);
751
-
752
- for (let i = 0; i < broad_match_count; i++) {
753
-
754
- const light_id = nodes[i];
755
-
756
- /**
757
- *
758
- * @type {LightRenderMetadata}
759
- */
760
- const light_data = this.#metadata_map.get(light_id);
761
-
762
- /**
763
- *
764
- * @type {PointLight|Decal}
765
- */
766
- const light = light_data.light;
767
-
768
- if (light.isDecal === true) {
769
-
770
-
771
- // decals go into a separate bucket
772
- visible_decals.push(light_data);
773
-
774
- } else if (light.isPointLight === true) {
775
- // perform granular check
776
-
777
- const light_position = light.position;
778
-
779
- const light_x = light_position.x;
780
- const light_y = light_position.y;
781
- const light_z = light_position.z;
782
-
783
- const radius = light.radius.getValue();
784
-
785
- if (!point_light_inside_volume(light_x, light_y, light_z, radius, scratch_frustum_planes)) {
786
- // outside of view frustum
787
- continue;
788
- }
789
-
790
-
791
- // register as visible
792
- visible_lights.push(light_data);
793
- }
794
-
795
- }
796
-
797
- visible_lights.finalizeUpdate();
798
-
799
- visible_decals.finalizeUpdate();
800
- }
801
-
802
- __write_light_data_texture() {
803
-
804
- /**
805
- *
806
- * @type {LightRenderMetadata[]}
807
- */
808
- const visible_lights = this.__sorted_visible_lights;
809
-
810
- // write light data into texture
811
- const visible_light_count = visible_lights.length;
812
-
813
- // compute amount of space required for the lights
814
- let address = 0;
815
- for (let i = 0; i < visible_light_count; i++) {
816
- const datum = visible_lights[i];
817
-
818
- /**
819
- *
820
- * @type {PointLight}
821
- */
822
- const light = datum.light;
823
-
824
- address += light.ENCODED_SLOT_COUNT;
825
- }
826
-
827
- const expected_light_data_size = address;
828
-
829
-
830
- const light_data = this.__light_data;
831
- light_data.resize(Math.ceil(address / 4));
832
-
833
- light_data.update();
834
-
835
- // update required lookup precision
836
- this.__lookup_data.precision = Math.max(
837
- 0,
838
- Math.ceil(Math.log2(light_data.size))
839
- ) + LIGHT_ENCODING_TYPE_BIT_COUNT;
840
-
841
- const tx_light = light_data.getTexture();
842
-
843
- /**
844
- *
845
- * @type {Float32Array}
846
- */
847
- const tx_light_data = tx_light.image.data;
848
-
849
- address = 0;
850
-
851
- for (let i = 0; i < visible_light_count; i++) {
852
- const datum = visible_lights[i];
853
-
854
- assert.lessThan(address, tx_light_data.length, "overflow");
855
-
856
- /**
857
- *
858
- * @type {PointLight}
859
- */
860
- const light = datum.light;
861
-
862
- datum.address = address >> 2;
863
-
864
- const written_slots = light.toArray(tx_light_data, address);
865
-
866
- address += written_slots;
867
-
868
- // align address to 4 slot boundary
869
- if ((address & 3) !== 0) {
870
- // not on 4 slot boundary
871
- address = ((address >> 2) + 1) << 2;
872
- }
873
- }
874
-
875
- if (address !== expected_light_data_size) {
876
- throw new Error(`Expected light data size is ${expected_light_data_size}, actual written data is size is ${address}`);
877
- }
878
-
879
- tx_light.needsUpdate = true;
880
-
881
- this.__visible_bvh_needs_update = true;
882
- }
883
-
884
- /**
885
- * Perform cluster assignment where each cluster is filled with overlapping lights
886
- * @private
887
- */
888
- __assign_lights_to_clusters() {
889
-
890
- const tiles_resolution = this.__tiles_resolution;
891
-
892
- const tr_z = tiles_resolution.z;
893
- const tr_y = tiles_resolution.y;
894
- const tr_x = tiles_resolution.x;
895
-
896
- const tr_x_1 = tr_x + 1;
897
- const tr_y_1 = tr_y + 1;
898
-
899
- const tr_xy_1 = tr_x_1 * tr_y_1;
900
-
901
- /**
902
- *
903
- * @type {Float32Array}
904
- */
905
- const cluster_planes = this.__cluster_planes;
906
-
907
- const bvh_lights = this.__visible_bvh_lights;
908
- const bvh_decals = this.__visible_bvh_decals;
909
-
910
-
911
- const cluster_planes_x_offset = 0;
912
- const cluster_planes_y_offset = tr_x_1 * 4;
913
- const cluster_planes_z_offset = cluster_planes_y_offset + tr_y_1 * 4;
914
-
915
- /**
916
- *
917
- * @type {number[]|Float32Array}
918
- */
919
- const light_source_data = this.__light_data.getTexture().image.data;
920
-
921
- const tile_slice_size = tr_x * tr_y;
922
-
923
- const light_cluster_texture = this.__cluster_texture;
924
- const tile_texture_data = light_cluster_texture.image.data;
925
-
926
- const light_lookup = this.__lookup_data;
927
- light_lookup.update();
928
- const light_lookup_texture = light_lookup.getTexture();
929
- const light_lookup_texture_data = light_lookup_texture.image.data;
930
-
931
- let lookup_address_offset = 0;
932
- let i_x_0, i_y_0, i_z_0;
933
-
934
- // clear cache
935
- LOOKUP_CACHE.fill(0xFFFFFFFF);
936
-
937
- // console.log('Assignment Start');
938
-
939
- for (i_z_0 = 0; i_z_0 < tr_z; i_z_0++) {
940
-
941
- // construct z planes
942
-
943
- const slice_offset = i_z_0 * tile_slice_size;
944
-
945
- const z_plane_index_offset_0 = cluster_planes_z_offset + i_z_0 * 4;
946
-
947
- read_plane_pair(cluster_planes, z_plane_index_offset_0, scratch_frustum_planes, 16);
948
-
949
- for (i_y_0 = 0; i_y_0 < tr_y; i_y_0++) {
950
-
951
- // construct y planes
952
-
953
- const y_plane_index_offset_0 = cluster_planes_y_offset + i_y_0 * 4;
954
-
955
- read_plane_pair(cluster_planes, y_plane_index_offset_0, scratch_frustum_planes, 8);
956
-
957
- for (i_x_0 = 0; i_x_0 < tr_x; i_x_0++) {
958
-
959
- // construct x planes
960
-
961
- const x_plane_index_offset_0 = cluster_planes_x_offset + i_x_0 * 4;
962
-
963
- read_plane_pair(cluster_planes, x_plane_index_offset_0, scratch_frustum_planes, 0);
964
-
965
- const tile_index = slice_offset + (i_y_0 * tr_x) + i_x_0;
966
-
967
- const tile_data_offset = tile_index * 4;
968
-
969
- //assign lights
970
- lookup_address_offset += assign_cluster(
971
- tile_data_offset,
972
- bvh_lights,
973
- lookup_address_offset,
974
- light_lookup_texture_data,
975
- tile_texture_data,
976
- light_source_data
977
- );
978
-
979
- // assign decals
980
- const decal_count = assign_cluster(
981
- tile_data_offset + 2,
982
- bvh_decals,
983
- lookup_address_offset,
984
- light_lookup_texture_data,
985
- tile_texture_data,
986
- light_source_data
987
- );
988
-
989
- /*
990
- TODO fix decal sorting artifacts
991
-
992
- if (decal_count > 1) { // only sort when there are 2 or more, sorting 1 object is pointless
993
- // decals need to be sorted for correct blending result
994
- sort_decal_data(
995
- light_lookup_texture_data,
996
- light_source_data,
997
- lookup_address_offset,
998
- decal_count
999
- );
1000
- }
1001
-
1002
- */
1003
-
1004
- lookup_address_offset += decal_count;
1005
- }
1006
- }
1007
- }
1008
-
1009
- light_cluster_texture.needsUpdate = true;
1010
- light_lookup_texture.needsUpdate = true;
1011
-
1012
- // Post-fact lookup texture resize, this frame will be wrong, but next one should be ok
1013
- const current_lookup_size = this.__lookup_data.size;
1014
-
1015
- const lookup_memory_resized = this.__ensure_lookup_size(lookup_address_offset);
1016
-
1017
- const cluster_resized = this.__set_cluster_addressable_bit_range(Math.max(0, Math.log2(lookup_address_offset)));
1018
-
1019
- if (
1020
- (lookup_memory_resized && current_lookup_size < lookup_address_offset)
1021
- || cluster_resized
1022
- ) {
1023
- // overflow, we ended up trying to write more lights into some clusters than what they could contain
1024
- this.__update_cluster_texture();
1025
-
1026
- // re-do assignment
1027
- this.__assign_lights_to_clusters();
1028
- }
1029
-
1030
- // console.log('Unique assignment count: ', assignment_count, ', Hash reuse:', hash_reuse_count);
1031
- }
1032
-
1033
- /**
1034
- *
1035
- * @private
1036
- */
1037
- __build_cluster_frustum_planes() {
1038
- const tiles_resolution = this.__tiles_resolution;
1039
-
1040
- const tr_z = tiles_resolution.z;
1041
- const tr_y = tiles_resolution.y;
1042
- const tr_x = tiles_resolution.x;
1043
-
1044
- const destination = this.__cluster_planes;
1045
-
1046
- compute_cluster_planes_from_points(destination, this.__cluster_frustum_points, tr_x, tr_y, tr_z);
1047
- }
1048
-
1049
- /**
1050
- *
1051
- * @private
1052
- */
1053
- __build_view_frustum_points() {
1054
-
1055
- const view_frustum = this.__view_frustum;
1056
-
1057
- const points = this.__view_frustum_points;
1058
-
1059
- computeFrustumCorners(points, view_frustum.planes);
1060
- }
1061
-
1062
- /**
1063
- *
1064
- * @private
1065
- */
1066
- __build_cluster_frustum_points() {
1067
- const tiles_resolution = this.__tiles_resolution;
1068
-
1069
- const tr_z = tiles_resolution.z;
1070
- const tr_y = tiles_resolution.y;
1071
- const tr_x = tiles_resolution.x;
1072
-
1073
- const view_frustum_points = this.__view_frustum_points;
1074
- const points_data = this.__cluster_frustum_points;
1075
-
1076
- frustum3_slice_linear_to_points(view_frustum_points, tr_x, tr_y, tr_z, points_data);
1077
- }
1078
-
1079
- /**
1080
- * Build light tile texture
1081
- * @param {Camera|THREE.PerspectiveCamera} camera
1082
- */
1083
- buildTiles(camera) {
1084
- // camera.updateProjectionMatrix();
1085
- // camera.updateMatrix();
1086
- // camera.updateMatrixWorld(true);
1087
-
1088
- //update world inverse matrix
1089
- camera.matrixWorldInverse.copy(camera.matrixWorld);
1090
- camera.matrixWorldInverse.invert();
1091
-
1092
- // this.__projection_matrix.set(camera.matrixWorldInverse.elements);
1093
- m4_multiply(this.__projection_matrix, camera.projectionMatrix.elements, camera.matrixWorldInverse.elements);
1094
-
1095
- // console.time('__build_view_frustum');
1096
- this.__build_view_frustum(camera);
1097
- // console.timeEnd('__build_view_frustum');
1098
-
1099
-
1100
- // console.time('__build_visible_light_list');
1101
- this.__build_visible_light_list();
1102
- // console.timeEnd('__build_visible_light_list');
1103
-
1104
- // update decal atlas
1105
- this.__decal_atlas.update();
1106
- this.__update_decal_atlas_texture();
1107
-
1108
- // console.time('__write_light_data_texture');
1109
- if (this.__light_data_needs_update) {
1110
- // sort light for better search and query locality
1111
- this.__sort_visible_light();
1112
-
1113
- this.__write_light_data_texture();
1114
-
1115
- // clear flag
1116
- this.__light_data_needs_update = false;
1117
- }
1118
- // console.timeEnd('__write_light_data_texture');
1119
-
1120
- if (this.__visible_bvh_needs_update) {
1121
- this.__update_visible_bvh();
1122
- }
1123
-
1124
- this.__build_cluster_frustum_points();
1125
- // console.time('__build_cluster_frustum_planes');
1126
- this.__build_cluster_frustum_planes();
1127
- // console.timeEnd('__build_cluster_frustum_planes');
1128
-
1129
- // console.time('__assign_lights_to_clusters');
1130
- this.__assign_lights_to_clusters();
1131
- // console.timeEnd('__assign_lights_to_clusters');
1132
- }
1133
-
1134
- dispose() {
1135
- this.__cluster_texture.dispose();
1136
- this.__light_data.dispose();
1137
- this.__lookup_data.dispose();
1138
- this.__decal_atlas_texture.dispose();
1139
- }
1140
-
1141
- /**
1142
- * Set resolution of the cluster texture, higher resolution will result in less load on the GPU, but will take up more RAM to represent and more time to build the clusters each frame
1143
- * @param {number} x
1144
- * @param {number} y
1145
- * @param {number} z
1146
- */
1147
- setTileMapResolution(x, y, z) {
1148
- assert.isNumber(x, 'x');
1149
- assert.isNonNegativeInteger(x, 'x');
1150
- assert.isFinite(x, 'x');
1151
- assert.greaterThan(x, 0, 'x must be > 0');
1152
-
1153
- assert.isNumber(y, 'y');
1154
- assert.isNonNegativeInteger(y, 'y');
1155
- assert.isFinite(y, 'y');
1156
- assert.greaterThan(y, 0, 'y must be > 0');
1157
-
1158
- assert.isNumber(z, 'z');
1159
- assert.isNonNegativeInteger(z, 'z');
1160
- assert.isFinite(z, 'z');
1161
- assert.greaterThan(z, 0, 'z must be > 0');
1162
-
1163
- const r = this.__tiles_resolution;
1164
-
1165
- if (x === r.x && y === r.y && z === r.z) {
1166
- // special case, no change
1167
- return;
1168
- }
1169
-
1170
- r.set(x, y, z);
1171
-
1172
- this.__cluster_texture_needs_rebuild = true;
1173
- this.__update_cluster_texture();
1174
-
1175
- const cluster_planes_size = ((x + 1) + (y + 1) + (z + 1)) * 4;
1176
- const cluster_frustum_points_size = (x + 1) * (y + 1) * (z + 1) * 3;
1177
-
1178
- const buffer = new ArrayBuffer((cluster_planes_size + cluster_frustum_points_size) * 4);
1179
-
1180
- this.__cluster_planes = new Float32Array(
1181
- buffer, 0,
1182
- cluster_planes_size
1183
- );
1184
- this.__cluster_frustum_points = new Float32Array(
1185
- buffer, cluster_planes_size * 4,
1186
- cluster_frustum_points_size
1187
- );
1188
- }
1189
-
1190
- /**
1191
- *
1192
- * @param {AbstractLight} light
1193
- * @returns {boolean}
1194
- */
1195
- hasLight(light) {
1196
- return this.#metadata_map.has(light.id);
1197
- }
1198
-
1199
- /**
1200
- *
1201
- * @param {AbstractLight} light
1202
- */
1203
- addLight(light) {
1204
- const lightData = new LightRenderMetadata(light);
1205
-
1206
- lightData.link(this.#light_data_bvh);
1207
-
1208
- this.#metadata_map.set(light.id, lightData);
1209
- }
1210
-
1211
- /**
1212
- *
1213
- * @param {AbstractLight} light
1214
- * @returns {boolean}
1215
- */
1216
- removeLight(light) {
1217
-
1218
- const light_id = light.id;
1219
-
1220
- const data = this.#metadata_map.get(light_id);
1221
-
1222
- if (data === undefined) {
1223
- return false;
1224
- }
1225
-
1226
- data.unlink();
1227
-
1228
- this.#metadata_map.delete(light_id);
1229
-
1230
- return true;
1231
- }
1232
- }
1
+ import { m4_multiply } from "../../../../core/geom/3d/mat4/m4_multiply.js";
2
+ import {
3
+ ClampToEdgeWrapping,
4
+ DataTexture,
5
+ DataTexture3D,
6
+ Frustum,
7
+ LinearFilter,
8
+ LinearMipMapLinearFilter,
9
+ NearestFilter,
10
+ RGBAFormat,
11
+ RGBAIntegerFormat,
12
+ UnsignedByteType,
13
+ UnsignedShortType
14
+ } from "three";
15
+ import { assert } from "../../../../core/assert.js";
16
+ import { computeBinaryDataTypeByPrecision } from "../../../../core/binary/type/computeBinaryDataTypeByPrecision.js";
17
+ import {
18
+ DataType2TypedArrayConstructorMapping
19
+ } from "../../../../core/binary/type/DataType2TypedArrayConstructorMapping.js";
20
+ import { BinaryUint32BVH } from "../../../../core/bvh2/binary/2/BinaryUint32BVH.js";
21
+ import { BVH } from "../../../../core/bvh2/bvh3/BVH.js";
22
+ import {
23
+ bvh_query_user_data_intersects_frustum
24
+ } from "../../../../core/bvh2/bvh3/query/bvh_query_user_data_intersects_frustum.js";
25
+ import { array_copy } from "../../../../core/collection/array/array_copy.js";
26
+ import { array_sort_quick } from "../../../../core/collection/array/array_sort_quick.js";
27
+ import { array_swap_one } from "../../../../core/collection/array/array_swap_one.js";
28
+ import { frustum3_planes_to_flat_from_three } from "../../../../core/geom/3d/frustum/frustum3_planes_to_flat_from_three.js";
29
+ import { frustum3_slice_linear_to_points } from "../../../../core/geom/3d/frustum/frustum3_slice_linear_to_points.js";
30
+ import { v3_morton_encode_transformed } from "../../../../core/geom/3d/morton/v3_morton_encode_transformed.js";
31
+ import { v3_distance } from "../../../../core/geom/vec3/v3_distance.js";
32
+ import Vector3 from "../../../../core/geom/Vector3.js";
33
+ import { NumericType } from "../../../../core/math/NumericType.js";
34
+ import { frustum_from_camera } from "../../ecs/camera/frustum_from_camera.js";
35
+ import { CachingTextureAtlas } from "../../texture/atlas/CachingTextureAtlas.js";
36
+ import { ReferencedTextureAtlas } from "../../texture/atlas/ReferencedTextureAtlas.js";
37
+ import { TextureAtlas } from "../../texture/atlas/TextureAtlas.js";
38
+ import {
39
+ computeThreeTextureInternalFormatFromDataType
40
+ } from "../../texture/computeThreeTextureInternalFormatFromDataType.js";
41
+ import { computeThreeTextureTypeFromDataType } from "../../texture/computeThreeTextureTypeFromDataType.js";
42
+ import { writeSample2DDataToDataTexture } from "../../texture/sampler/writeSampler2DDataToDataTexture.js";
43
+ import { TextureBackedMemoryRegion } from "../../texture/TextureBackedMemoryRegion.js";
44
+ import { IncrementalDeltaSet } from "../visibility/IncrementalDeltaSet.js";
45
+ import { assign_cluster, LOOKUP_CACHE, scratch_corners, scratch_frustum_planes } from "./assign_cluster.js";
46
+ import { compute_cluster_planes_from_points } from "./cluster/compute_cluster_planes_from_points.js";
47
+ import { read_plane_pair } from "./cluster/read_plane_pair.js";
48
+ import { computeFrustumCorners } from "./computeFrustumCorners.js";
49
+ import { LightRenderMetadata } from "./LightRenderMetadata.js";
50
+ import { Decal } from "./model/Decal.js";
51
+ import { point_light_inside_volume } from "./query/point_light_inside_volume.js";
52
+
53
+
54
+ /**
55
+ * How many bits are used to encode light type
56
+ * @type {number}
57
+ */
58
+ const LIGHT_ENCODING_TYPE_BIT_COUNT = 2;
59
+
60
+ /**
61
+ *
62
+ * @param {AbstractLight|PointLight|Decal} light
63
+ * @returns {number}
64
+ */
65
+ function encode_light_type(light) {
66
+ if (light.isPointLight === true) {
67
+ return 0;
68
+ } else if (light.isDecal === true) {
69
+ return 3;
70
+ } else {
71
+ throw new Error('Unsupported light type');
72
+ }
73
+ }
74
+
75
+ /**
76
+ *
77
+ * @param {number} address
78
+ * @param {AbstractLight} light
79
+ * @returns {number}
80
+ */
81
+ function encode_light_descriptor(address, light) {
82
+ const light_type = encode_light_type(light);
83
+ return light_type | (address << 2);
84
+ }
85
+
86
+ /**
87
+ *
88
+ * @param {IncrementalDeltaSet<LightRenderMetadata>} data_set
89
+ * @param {BinaryUint32BVH} bvh
90
+ */
91
+ function build_light_data_bvh(data_set, bvh) {
92
+ const elements = data_set.elements;
93
+ const element_count = data_set.size;
94
+
95
+ bvh.setLeafCount(element_count);
96
+ bvh.initialize_structure();
97
+
98
+ for (let i = 0; i < element_count; i++) {
99
+
100
+ /**
101
+ *
102
+ * @type {LightRenderMetadata}
103
+ */
104
+ const datum = elements[i];
105
+
106
+ const bb = datum.bvh_leaf.bounds;
107
+
108
+ const payload = encode_light_descriptor(datum.address, datum.light);
109
+
110
+ bvh.setLeafData(
111
+ i, payload,
112
+ bb[0], bb[1], bb[2],
113
+ bb[3], bb[4], bb[5],
114
+ );
115
+ }
116
+
117
+ // vbvh.sort_morton(this.__projection_matrix);
118
+ // vbvh.sort_bubble_sah();
119
+
120
+ bvh.build();
121
+ }
122
+
123
+ /**
124
+ *
125
+ * @param {LightRenderMetadata} a
126
+ * @param {LightRenderMetadata} b
127
+ * @returns {number}
128
+ */
129
+ function compareLRMByLightId(a, b) {
130
+ return b.light.id - a.light.id;
131
+ }
132
+
133
+ export class LightManager {
134
+
135
+ /**
136
+ * @readonly
137
+ * @type {BVH}
138
+ */
139
+ #light_data_bvh = new BVH();
140
+
141
+ /**
142
+ * Mapping from a light/decal object ID internal metadata
143
+ * @type {Map<number,LightRenderMetadata>}
144
+ */
145
+ #metadata_map = new Map();
146
+
147
+
148
+ constructor() {
149
+
150
+ /**
151
+ * Number of cluster slices along each dimension
152
+ * @type {Vector3}
153
+ * @private
154
+ */
155
+ this.__tiles_resolution = new Vector3(32, 16, 8);
156
+
157
+ this.__cluster_texture_precision = 8;
158
+ this.__cluster_texture_needs_rebuild = false;
159
+
160
+ /**
161
+ * Texel layout:
162
+ * - R: offset into lookup texture for lights
163
+ * - G: light count
164
+ * - B: offset into lookup texture for decals
165
+ * - A: decal count
166
+ * @type {DataTexture3D}
167
+ * @private
168
+ */
169
+ this.__cluster_texture = new DataTexture3D(
170
+ new Uint16Array(4),
171
+ 1,
172
+ 1,
173
+ 1,
174
+ );
175
+
176
+ this.__cluster_texture.flipY = false;
177
+ this.__cluster_texture.generateMipmaps = false;
178
+ this.__cluster_texture.unpackAlignment = 2;
179
+
180
+ this.__cluster_texture.magFilter = NearestFilter;
181
+ this.__cluster_texture.minFilter = NearestFilter;
182
+
183
+ this.__cluster_texture.type = UnsignedShortType;
184
+ this.__cluster_texture.format = RGBAIntegerFormat;
185
+ this.__cluster_texture.internalFormat = "RGBA16UI";
186
+
187
+ /**
188
+ *
189
+ * @type {TextureBackedMemoryRegion}
190
+ * @readonly
191
+ * @private
192
+ */
193
+ this.__lookup_data = new TextureBackedMemoryRegion();
194
+ this.__lookup_data.type = NumericType.Uint;
195
+ this.__lookup_data.channel_count = 1;
196
+ this.__lookup_data.precision = 8;
197
+ this.__lookup_data.resize(1);
198
+
199
+ /**
200
+ *
201
+ * @type {TextureBackedMemoryRegion}
202
+ * @readonly
203
+ * @private
204
+ */
205
+ this.__light_data = new TextureBackedMemoryRegion();
206
+ this.__light_data.type = NumericType.Float;
207
+ this.__light_data.precision = 32;
208
+ this.__light_data.channel_count = 4;
209
+ this.__light_data.resize(8);
210
+
211
+ /**
212
+ * @type {DataTexture}
213
+ * @readonly
214
+ * @private
215
+ */
216
+ this.__decal_atlas_texture = new DataTexture(new Uint8Array(4), 1, 1, RGBAFormat);
217
+ this.__decal_atlas_texture.type = UnsignedByteType;
218
+ this.__decal_atlas_texture.flipY = false;
219
+ this.__decal_atlas_texture.wrapS = ClampToEdgeWrapping;
220
+ this.__decal_atlas_texture.wrapT = ClampToEdgeWrapping;
221
+ this.__decal_atlas_texture.minFilter = LinearMipMapLinearFilter;
222
+ this.__decal_atlas_texture.magFilter = LinearFilter;
223
+ this.__decal_atlas_texture.generateMipmaps = true;
224
+ this.__decal_atlas_texture.unpackAlignment = 4;
225
+ this.__decal_atlas_texture.anisotropy = 8;
226
+
227
+ /**
228
+ *
229
+ * @type {AbstractTextureAtlas}
230
+ * @readonly
231
+ * @private
232
+ */
233
+ this.__decal_atlas = new CachingTextureAtlas({
234
+ atlas: new TextureAtlas(512)
235
+ });
236
+
237
+ /**
238
+ *
239
+ * @type {ReferencedTextureAtlas}
240
+ * @private
241
+ */
242
+ this.__decal_patch_references = new ReferencedTextureAtlas(this.__decal_atlas);
243
+
244
+ /**
245
+ *
246
+ * @type {Map<Decal, Reference<AtlasPatch>>}
247
+ * @private
248
+ */
249
+ this.__decal_references = new Map();
250
+
251
+ /**
252
+ *
253
+ * @type {IncrementalDeltaSet<LightRenderMetadata>}
254
+ * @readonly
255
+ * @private
256
+ */
257
+ this.__visible_lights = new IncrementalDeltaSet(compareLRMByLightId);
258
+ /**
259
+ *
260
+ * @type {IncrementalDeltaSet<LightRenderMetadata>}
261
+ * @readonly
262
+ * @private
263
+ */
264
+ this.__visible_decals = new IncrementalDeltaSet(compareLRMByLightId);
265
+
266
+ /**
267
+ *
268
+ * @type {LightRenderMetadata[]}
269
+ * @private
270
+ */
271
+ this.__sorted_visible_lights = [];
272
+
273
+ /**
274
+ *
275
+ * @type {BinaryUint32BVH}
276
+ * @private
277
+ */
278
+ this.__visible_bvh_lights = new BinaryUint32BVH();
279
+ /**
280
+ *
281
+ * @type {BinaryUint32BVH}
282
+ * @private
283
+ */
284
+ this.__visible_bvh_decals = new BinaryUint32BVH();
285
+
286
+ /**
287
+ *
288
+ * @type {Frustum}
289
+ * @private
290
+ */
291
+ this.__view_frustum = new Frustum();
292
+
293
+ /**
294
+ * Corner points of the view frustum
295
+ * @type {Float32Array}
296
+ * @private
297
+ */
298
+ this.__view_frustum_points = new Float32Array(24);
299
+
300
+ /**
301
+ *
302
+ * @type {Float32Array|mat4|number[]}
303
+ * @private
304
+ */
305
+ this.__projection_matrix = new Float32Array(16);
306
+
307
+
308
+ /**
309
+ * Accelerated data structure with pre-computed plane normals for clusters in each dimensions. Order: X, Y, Z
310
+ * @type {Float32Array}
311
+ * @private
312
+ */
313
+ this.__cluster_planes = new Float32Array(1);
314
+
315
+ /**
316
+ * Accelerated data structure with pre-computed corner coordinates of each cluster's frustum
317
+ * @type {Float32Array}
318
+ * @private
319
+ */
320
+ this.__cluster_frustum_points = new Float32Array(1);
321
+
322
+ this.setTileMapResolution(32, 16, 16);
323
+
324
+ this.__visible_lights.onAdded.add(this.__handle_visible_light_added, this);
325
+ this.__visible_lights.onRemoved.add(this.__handle_visible_light_removed, this);
326
+
327
+ this.__visible_decals.onAdded.add(this.__handle_visible_decal_added, this);
328
+ this.__visible_decals.onRemoved.add(this.__handle_visible_decal_removed, this);
329
+
330
+ /**
331
+ * Data needs to be re-written into the data texture
332
+ * Usually set when source lights change
333
+ * @type {boolean}
334
+ * @private
335
+ */
336
+ this.__light_data_needs_update = true;
337
+
338
+ /**
339
+ *
340
+ * @type {boolean}
341
+ * @private
342
+ */
343
+ this.__visible_bvh_needs_update = true;
344
+
345
+ // window.light_manager = this; // DEBUG
346
+ }
347
+
348
+ requestDataUpdate() {
349
+ this.__light_data_needs_update = true;
350
+ }
351
+
352
+ /**
353
+ * Please set this to false if you have a lot of overlapping decals in the scene
354
+ * Overlapping decals can provide filtering artifacts due to incorrect mipmap level detection by WebGL
355
+ * see article: https://0fps.net/2013/07/09/texture-atlases-wrapping-and-mip-mapping/
356
+ * NOTE: the technique mentioned in the article above is not implemented, as it would require significant increase in number of texture fetches
357
+ * @param {boolean} v
358
+ */
359
+ set decal_filtering_enabled(v) {
360
+ const t = this.__decal_atlas_texture;
361
+
362
+ if (t.generateMipmaps === v) {
363
+ return;
364
+ }
365
+
366
+ t.generateMipmaps = v;
367
+ }
368
+
369
+ /**
370
+ *
371
+ * @return {boolean}
372
+ */
373
+ get decal_filtering_enabled() {
374
+ return this.__decal_atlas_texture.generateMipmaps;
375
+ }
376
+
377
+ __update_decal_atlas_texture() {
378
+ const sampler = this.__decal_atlas.sampler;
379
+
380
+ if (sampler.version !== this.__decal_atlas_texture.version) {
381
+ writeSample2DDataToDataTexture(sampler, this.__decal_atlas_texture);
382
+
383
+ this.__decal_atlas_texture.version = sampler.version;
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Sometimes light lookup table can get quite large, to make sure we can store these addresses in the cluster texture,
389
+ * we may need to switch to a larger data type, such as Uint8->Uint16 or Uint16->Uint32
390
+ * @param {number} bit_count How many bits should be addressable from the cluster content
391
+ * @returns {boolean} true if texture was rebuilt (underlying type change), false otherwise
392
+ * @private
393
+ */
394
+ __set_cluster_addressable_bit_range(bit_count) {
395
+ const rounded_value = Math.ceil(bit_count);
396
+
397
+ this.__cluster_texture_precision = rounded_value;
398
+ const dataType = computeBinaryDataTypeByPrecision(NumericType.Uint, rounded_value);
399
+
400
+ const threeTextureType = computeThreeTextureTypeFromDataType(dataType);
401
+
402
+ const texture = this.__cluster_texture;
403
+ if (threeTextureType !== texture.type) {
404
+ this.__cluster_texture_needs_rebuild = true;
405
+
406
+ return true;
407
+ } else {
408
+ return false;
409
+ }
410
+ }
411
+
412
+ __update_cluster_texture() {
413
+ if (this.__cluster_texture_needs_rebuild) {
414
+ this.__build_cluster_texture();
415
+ }
416
+ }
417
+
418
+ __build_cluster_texture() {
419
+ const dataType = computeBinaryDataTypeByPrecision(NumericType.Uint, this.__cluster_texture_precision);
420
+
421
+ const threeTextureType = computeThreeTextureTypeFromDataType(dataType);
422
+
423
+ const texture = this.__cluster_texture;
424
+
425
+ const channelCount = 4;
426
+
427
+ texture.dispose();
428
+
429
+ const image = texture.image;
430
+
431
+ const resolution = this.__tiles_resolution;
432
+
433
+ image.width = resolution.x;
434
+ image.height = resolution.y;
435
+ image.depth = resolution.z;
436
+
437
+ texture.type = threeTextureType;
438
+ texture.internalFormat = computeThreeTextureInternalFormatFromDataType(dataType, channelCount);
439
+
440
+ const ArrayConstructor = DataType2TypedArrayConstructorMapping[dataType];
441
+
442
+ image.data = new ArrayConstructor(resolution.x * resolution.y * resolution.z * channelCount);
443
+
444
+ texture.needsUpdate = true;
445
+
446
+ this.__cluster_texture_needs_rebuild = false;
447
+ }
448
+
449
+ /**
450
+ *
451
+ * @param {number} size
452
+ * @returns {boolean}
453
+ * @private
454
+ */
455
+ __ensure_lookup_size(size) {
456
+ return this.__lookup_data.resize(size);
457
+ }
458
+
459
+ /**
460
+ *
461
+ * @returns {DataTexture}
462
+ */
463
+ getTextureLookup() {
464
+ return this.__lookup_data.getTexture();
465
+ }
466
+
467
+ /**
468
+ *
469
+ * @returns {DataTexture}
470
+ */
471
+ getTextureData() {
472
+ return this.__light_data.getTexture();
473
+ }
474
+
475
+ /**
476
+ * @returns {DataTexture3D}
477
+ */
478
+ getTextureClusters() {
479
+ return this.__cluster_texture;
480
+ }
481
+
482
+ /**
483
+ * @returns {DataTexture}
484
+ */
485
+ getTextureDecalAtlas() {
486
+ return this.__decal_atlas_texture;
487
+ }
488
+
489
+ /**
490
+ * NOTE: do not modify the value
491
+ * @returns {Readonly<Vector3>}
492
+ */
493
+ getResolution() {
494
+ return this.__tiles_resolution;
495
+ }
496
+
497
+ /**
498
+ *
499
+ * @private
500
+ */
501
+ __handle_light_dimensions_change() {
502
+ // internal BVH needs to be rebuilt
503
+ this.__visible_bvh_needs_update = true;
504
+ // when dimensions change, typically data will need to be re-written to the GPU, things like light positions or projection matrix
505
+ this.__light_data_needs_update = true;
506
+ }
507
+
508
+ /**
509
+ *
510
+ * @private
511
+ */
512
+ __handle_light_appearance_change() {
513
+ // appearance changes (such as color or intensity) don't affect bounds, only the encoded data
514
+ this.__light_data_needs_update = true;
515
+ }
516
+
517
+ /**
518
+ *
519
+ * @param {LightRenderMetadata} data
520
+ * @private
521
+ */
522
+ __handle_visible_decal_added(data) {
523
+ this.__light_data_needs_update = true;
524
+
525
+ /**
526
+ *
527
+ * @type {Decal}
528
+ */
529
+ const light = data.light;
530
+
531
+ light.onDimensionChanged(this.__handle_light_dimensions_change, this);
532
+
533
+ const ref = this.__decal_patch_references.acquire(light.texture_diffuse);
534
+
535
+ const patch = ref.getValue();
536
+ const patch_uv = patch.uv;
537
+
538
+ patch_uv.position.onChanged.add(light.handleUvPositionChange, light);
539
+ patch_uv.size.onChanged.add(light.handleUvSizeChange, light);
540
+
541
+ light.uv[0] = patch_uv.position.x;
542
+ light.uv[1] = patch_uv.position.y;
543
+ light.uv[2] = patch_uv.size.x;
544
+ light.uv[3] = patch_uv.size.y;
545
+
546
+ this.__decal_references.set(light, ref);
547
+ }
548
+
549
+ /**
550
+ *
551
+ * @param {LightRenderMetadata} data
552
+ * @private
553
+ */
554
+ __handle_visible_decal_removed(data) {
555
+ this.__light_data_needs_update = true;
556
+
557
+ /**
558
+ *
559
+ * @type {Decal}
560
+ */
561
+ const light = data.light;
562
+
563
+ light.offDimensionChanged(this.__handle_light_dimensions_change, this);
564
+
565
+ const ref = this.__decal_references.get(light);
566
+
567
+ if (ref === undefined) {
568
+ // This can occur when decal changes while being in the visible list, messing with the IncrementalDeltaSet's compare order
569
+ console.warn(`Decal reference not found: ${light}`);
570
+ return;
571
+ }
572
+
573
+ const patch = ref.getValue();
574
+ const patch_uv = patch.uv;
575
+
576
+ // unsubscribe
577
+ patch_uv.position.onChanged.remove(light.handleUvPositionChange, light);
578
+ patch_uv.size.onChanged.remove(light.handleUvSizeChange, light);
579
+
580
+
581
+ ref.release();
582
+
583
+ this.__decal_references.delete(light);
584
+ }
585
+
586
+ /**
587
+ *
588
+ * @param {LightRenderMetadata} data
589
+ * @private
590
+ */
591
+ __handle_visible_light_added(data) {
592
+ this.__light_data_needs_update = true;
593
+
594
+ const light = data.light;
595
+
596
+ light.onDimensionChanged(this.__handle_light_dimensions_change, this);
597
+ light.onAppearanceChanged(this.__handle_light_appearance_change, this);
598
+ }
599
+
600
+ /**
601
+ *
602
+ * @param {LightRenderMetadata} data
603
+ * @private
604
+ */
605
+ __handle_visible_light_removed(data) {
606
+ this.__light_data_needs_update = true;
607
+
608
+ const light = data.light;
609
+ light.offDimensionChanged(this.__handle_light_dimensions_change, this);
610
+ light.offAppearanceChanged(this.__handle_light_appearance_change, this);
611
+ }
612
+
613
+ /**
614
+ *
615
+ * @param {LightRenderMetadata} light
616
+ * @returns {number}
617
+ * @protected
618
+ */
619
+ __sort_visible_light_score(light) {
620
+ const center = scratch_corners;
621
+
622
+ light.light.getCenter(center)
623
+
624
+ const x = center[0];
625
+ const y = center[1];
626
+ const z = center[2];
627
+
628
+ return v3_morton_encode_transformed(x, y, z, this.__projection_matrix);
629
+ }
630
+
631
+ /**
632
+ * DEBUG method
633
+ * @param {LightRenderMetadata} lights
634
+ * @param {number} count
635
+ * @returns {number} lower = better sorting score
636
+ * @private
637
+ */
638
+ __assess_sorting_score(lights, count) {
639
+
640
+ let r = 0;
641
+
642
+ const center_a = [];
643
+ const center_b = [];
644
+
645
+ for (let i = 0; i < count; i++) {
646
+ const a = lights[i];
647
+
648
+ const score_a = this.__sort_visible_light_score(a);
649
+ a.light.getCenter(center_a);
650
+
651
+ for (let j = i + 1; j < count; j++) {
652
+ const b = lights[j];
653
+
654
+ const score_b = this.__sort_visible_light_score(b);
655
+ b.light.getCenter(center_b);
656
+
657
+ //
658
+ const score_distance = Math.abs(score_a - score_b);
659
+ const position_distance = v3_distance(
660
+ center_a[0], center_a[1], center_a[2],
661
+ center_b[0], center_b[1], center_b[2],
662
+ );
663
+
664
+ r += score_distance / position_distance;
665
+ }
666
+ }
667
+
668
+
669
+ return r / (count * (count - 1) * 0.5);
670
+ }
671
+
672
+ /**
673
+ * Sort lights for better data locality
674
+ * @private
675
+ */
676
+ __sort_visible_light() {
677
+ const sorted_lights = this.__sorted_visible_lights;
678
+
679
+ const visible_light_set = this.__visible_lights;
680
+ const visible_light_count = visible_light_set.size;
681
+
682
+ const visible_decal_set = this.__visible_decals;
683
+ const visible_decal_count = visible_decal_set.size;
684
+
685
+ const expected_sorted_size = visible_light_count + visible_decal_count;
686
+
687
+ if (sorted_lights.length > expected_sorted_size) {
688
+ // crop the reused array back down to the current frame's size, otherwise stale records from a
689
+ // larger earlier frame are re-encoded into the light-data texture every frame. Truncate via
690
+ // `length =` (in place, drops the tail references for GC) rather than splice, which would
691
+ // allocate a throwaway array of the removed elements.
692
+ sorted_lights.length = expected_sorted_size;
693
+ }
694
+
695
+ array_copy(visible_light_set.elements, 0, sorted_lights, 0, visible_light_count);
696
+ array_copy(visible_decal_set.elements, 0, sorted_lights, visible_light_count, visible_decal_count);
697
+
698
+ array_sort_quick(sorted_lights, this.__sort_visible_light_score, this, 0, expected_sorted_size - 1);
699
+ }
700
+
701
+ __update_visible_bvh() {
702
+ this.__visible_bvh_needs_update = false;
703
+
704
+ build_light_data_bvh(
705
+ this.__visible_lights,
706
+ this.__visible_bvh_lights
707
+ );
708
+
709
+ build_light_data_bvh(
710
+ this.__visible_decals,
711
+ this.__visible_bvh_decals
712
+ );
713
+ }
714
+
715
+
716
+ /**
717
+ *
718
+ * @param {Camera} camera
719
+ * @private
720
+ */
721
+ __build_view_frustum(camera) {
722
+ frustum_from_camera(camera, this.__view_frustum, false);
723
+
724
+ array_swap_one(this.__view_frustum.planes, 4, 5);
725
+
726
+ this.__build_view_frustum_points();
727
+ }
728
+
729
+ __build_visible_light_list() {
730
+
731
+ /**
732
+ *
733
+ * @type {IncrementalDeltaSet<LightRenderMetadata>}
734
+ */
735
+ const visible_lights = this.__visible_lights;
736
+
737
+ visible_lights.initializeUpdate();
738
+
739
+ const visible_decals = this.__visible_decals;
740
+
741
+ visible_decals.initializeUpdate();
742
+
743
+ const nodes = [];
744
+
745
+ frustum3_planes_to_flat_from_three(this.__view_frustum.planes, scratch_frustum_planes)
746
+
747
+ /*
748
+ Search is done in 2 phases:
749
+ 1) broad phase using bounding boxes
750
+ 2) granular phase where we use object-specific shape to check against frustum
751
+ */
752
+
753
+ const broad_match_count = bvh_query_user_data_intersects_frustum(nodes, 0, this.#light_data_bvh, scratch_frustum_planes);
754
+
755
+ for (let i = 0; i < broad_match_count; i++) {
756
+
757
+ const light_id = nodes[i];
758
+
759
+ /**
760
+ *
761
+ * @type {LightRenderMetadata}
762
+ */
763
+ const light_data = this.#metadata_map.get(light_id);
764
+
765
+ /**
766
+ *
767
+ * @type {PointLight|Decal}
768
+ */
769
+ const light = light_data.light;
770
+
771
+ if (light.isDecal === true) {
772
+
773
+
774
+ // decals go into a separate bucket
775
+ visible_decals.push(light_data);
776
+
777
+ } else if (light.isPointLight === true) {
778
+ // perform granular check
779
+
780
+ const light_position = light.position;
781
+
782
+ const light_x = light_position.x;
783
+ const light_y = light_position.y;
784
+ const light_z = light_position.z;
785
+
786
+ const radius = light.radius.getValue();
787
+
788
+ if (!point_light_inside_volume(light_x, light_y, light_z, radius, scratch_frustum_planes)) {
789
+ // outside of view frustum
790
+ continue;
791
+ }
792
+
793
+
794
+ // register as visible
795
+ visible_lights.push(light_data);
796
+ }
797
+
798
+ }
799
+
800
+ visible_lights.finalizeUpdate();
801
+
802
+ visible_decals.finalizeUpdate();
803
+ }
804
+
805
+ __write_light_data_texture() {
806
+
807
+ /**
808
+ *
809
+ * @type {LightRenderMetadata[]}
810
+ */
811
+ const visible_lights = this.__sorted_visible_lights;
812
+
813
+ // write light data into texture
814
+ const visible_light_count = visible_lights.length;
815
+
816
+ // compute amount of space required for the lights
817
+ let address = 0;
818
+ for (let i = 0; i < visible_light_count; i++) {
819
+ const datum = visible_lights[i];
820
+
821
+ /**
822
+ *
823
+ * @type {PointLight}
824
+ */
825
+ const light = datum.light;
826
+
827
+ address += light.ENCODED_SLOT_COUNT;
828
+ }
829
+
830
+ const expected_light_data_size = address;
831
+
832
+
833
+ const light_data = this.__light_data;
834
+ light_data.resize(Math.ceil(address / 4));
835
+
836
+ light_data.update();
837
+
838
+ // update required lookup precision
839
+ this.__lookup_data.precision = Math.max(
840
+ 0,
841
+ Math.ceil(Math.log2(light_data.size))
842
+ ) + LIGHT_ENCODING_TYPE_BIT_COUNT;
843
+
844
+ const tx_light = light_data.getTexture();
845
+
846
+ /**
847
+ *
848
+ * @type {Float32Array}
849
+ */
850
+ const tx_light_data = tx_light.image.data;
851
+
852
+ address = 0;
853
+
854
+ for (let i = 0; i < visible_light_count; i++) {
855
+ const datum = visible_lights[i];
856
+
857
+ assert.lessThan(address, tx_light_data.length, "overflow");
858
+
859
+ /**
860
+ *
861
+ * @type {PointLight}
862
+ */
863
+ const light = datum.light;
864
+
865
+ datum.address = address >> 2;
866
+
867
+ const written_slots = light.toArray(tx_light_data, address);
868
+
869
+ address += written_slots;
870
+
871
+ // align address to 4 slot boundary
872
+ if ((address & 3) !== 0) {
873
+ // not on 4 slot boundary
874
+ address = ((address >> 2) + 1) << 2;
875
+ }
876
+ }
877
+
878
+ if (address !== expected_light_data_size) {
879
+ throw new Error(`Expected light data size is ${expected_light_data_size}, actual written data is size is ${address}`);
880
+ }
881
+
882
+ tx_light.needsUpdate = true;
883
+
884
+ this.__visible_bvh_needs_update = true;
885
+ }
886
+
887
+ /**
888
+ * Perform cluster assignment where each cluster is filled with overlapping lights
889
+ * @private
890
+ */
891
+ __assign_lights_to_clusters() {
892
+
893
+ const tiles_resolution = this.__tiles_resolution;
894
+
895
+ const tr_z = tiles_resolution.z;
896
+ const tr_y = tiles_resolution.y;
897
+ const tr_x = tiles_resolution.x;
898
+
899
+ const tr_x_1 = tr_x + 1;
900
+ const tr_y_1 = tr_y + 1;
901
+
902
+ const tr_xy_1 = tr_x_1 * tr_y_1;
903
+
904
+ /**
905
+ *
906
+ * @type {Float32Array}
907
+ */
908
+ const cluster_planes = this.__cluster_planes;
909
+
910
+ const bvh_lights = this.__visible_bvh_lights;
911
+ const bvh_decals = this.__visible_bvh_decals;
912
+
913
+
914
+ const cluster_planes_x_offset = 0;
915
+ const cluster_planes_y_offset = tr_x_1 * 4;
916
+ const cluster_planes_z_offset = cluster_planes_y_offset + tr_y_1 * 4;
917
+
918
+ /**
919
+ *
920
+ * @type {number[]|Float32Array}
921
+ */
922
+ const light_source_data = this.__light_data.getTexture().image.data;
923
+
924
+ const tile_slice_size = tr_x * tr_y;
925
+
926
+ const light_cluster_texture = this.__cluster_texture;
927
+ const tile_texture_data = light_cluster_texture.image.data;
928
+
929
+ const light_lookup = this.__lookup_data;
930
+ light_lookup.update();
931
+ const light_lookup_texture = light_lookup.getTexture();
932
+ const light_lookup_texture_data = light_lookup_texture.image.data;
933
+
934
+ let lookup_address_offset = 0;
935
+ let i_x_0, i_y_0, i_z_0;
936
+
937
+ // clear cache
938
+ LOOKUP_CACHE.fill(0xFFFFFFFF);
939
+
940
+ // console.log('Assignment Start');
941
+
942
+ for (i_z_0 = 0; i_z_0 < tr_z; i_z_0++) {
943
+
944
+ // construct z planes
945
+
946
+ const slice_offset = i_z_0 * tile_slice_size;
947
+
948
+ const z_plane_index_offset_0 = cluster_planes_z_offset + i_z_0 * 4;
949
+
950
+ read_plane_pair(cluster_planes, z_plane_index_offset_0, scratch_frustum_planes, 16);
951
+
952
+ for (i_y_0 = 0; i_y_0 < tr_y; i_y_0++) {
953
+
954
+ // construct y planes
955
+
956
+ const y_plane_index_offset_0 = cluster_planes_y_offset + i_y_0 * 4;
957
+
958
+ read_plane_pair(cluster_planes, y_plane_index_offset_0, scratch_frustum_planes, 8);
959
+
960
+ for (i_x_0 = 0; i_x_0 < tr_x; i_x_0++) {
961
+
962
+ // construct x planes
963
+
964
+ const x_plane_index_offset_0 = cluster_planes_x_offset + i_x_0 * 4;
965
+
966
+ read_plane_pair(cluster_planes, x_plane_index_offset_0, scratch_frustum_planes, 0);
967
+
968
+ const tile_index = slice_offset + (i_y_0 * tr_x) + i_x_0;
969
+
970
+ const tile_data_offset = tile_index * 4;
971
+
972
+ //assign lights
973
+ lookup_address_offset += assign_cluster(
974
+ tile_data_offset,
975
+ bvh_lights,
976
+ lookup_address_offset,
977
+ light_lookup_texture_data,
978
+ tile_texture_data,
979
+ light_source_data
980
+ );
981
+
982
+ // assign decals
983
+ const decal_count = assign_cluster(
984
+ tile_data_offset + 2,
985
+ bvh_decals,
986
+ lookup_address_offset,
987
+ light_lookup_texture_data,
988
+ tile_texture_data,
989
+ light_source_data
990
+ );
991
+
992
+ /*
993
+ TODO fix decal sorting artifacts
994
+
995
+ if (decal_count > 1) { // only sort when there are 2 or more, sorting 1 object is pointless
996
+ // decals need to be sorted for correct blending result
997
+ sort_decal_data(
998
+ light_lookup_texture_data,
999
+ light_source_data,
1000
+ lookup_address_offset,
1001
+ decal_count
1002
+ );
1003
+ }
1004
+
1005
+ */
1006
+
1007
+ lookup_address_offset += decal_count;
1008
+ }
1009
+ }
1010
+ }
1011
+
1012
+ light_cluster_texture.needsUpdate = true;
1013
+ light_lookup_texture.needsUpdate = true;
1014
+
1015
+ // Post-fact lookup texture resize, this frame will be wrong, but next one should be ok
1016
+ const current_lookup_size = this.__lookup_data.size;
1017
+
1018
+ const lookup_memory_resized = this.__ensure_lookup_size(lookup_address_offset);
1019
+
1020
+ const cluster_resized = this.__set_cluster_addressable_bit_range(Math.max(0, Math.log2(lookup_address_offset)));
1021
+
1022
+ if (
1023
+ (lookup_memory_resized && current_lookup_size < lookup_address_offset)
1024
+ || cluster_resized
1025
+ ) {
1026
+ // overflow, we ended up trying to write more lights into some clusters than what they could contain
1027
+ this.__update_cluster_texture();
1028
+
1029
+ // re-do assignment
1030
+ this.__assign_lights_to_clusters();
1031
+ }
1032
+
1033
+ // console.log('Unique assignment count: ', assignment_count, ', Hash reuse:', hash_reuse_count);
1034
+ }
1035
+
1036
+ /**
1037
+ *
1038
+ * @private
1039
+ */
1040
+ __build_cluster_frustum_planes() {
1041
+ const tiles_resolution = this.__tiles_resolution;
1042
+
1043
+ const tr_z = tiles_resolution.z;
1044
+ const tr_y = tiles_resolution.y;
1045
+ const tr_x = tiles_resolution.x;
1046
+
1047
+ const destination = this.__cluster_planes;
1048
+
1049
+ compute_cluster_planes_from_points(destination, this.__cluster_frustum_points, tr_x, tr_y, tr_z);
1050
+ }
1051
+
1052
+ /**
1053
+ *
1054
+ * @private
1055
+ */
1056
+ __build_view_frustum_points() {
1057
+
1058
+ const view_frustum = this.__view_frustum;
1059
+
1060
+ const points = this.__view_frustum_points;
1061
+
1062
+ computeFrustumCorners(points, view_frustum.planes);
1063
+ }
1064
+
1065
+ /**
1066
+ *
1067
+ * @private
1068
+ */
1069
+ __build_cluster_frustum_points() {
1070
+ const tiles_resolution = this.__tiles_resolution;
1071
+
1072
+ const tr_z = tiles_resolution.z;
1073
+ const tr_y = tiles_resolution.y;
1074
+ const tr_x = tiles_resolution.x;
1075
+
1076
+ const view_frustum_points = this.__view_frustum_points;
1077
+ const points_data = this.__cluster_frustum_points;
1078
+
1079
+ frustum3_slice_linear_to_points(view_frustum_points, tr_x, tr_y, tr_z, points_data);
1080
+ }
1081
+
1082
+ /**
1083
+ * Build light tile texture
1084
+ * @param {Camera|THREE.PerspectiveCamera} camera
1085
+ */
1086
+ buildTiles(camera) {
1087
+ // camera.updateProjectionMatrix();
1088
+ // camera.updateMatrix();
1089
+ // camera.updateMatrixWorld(true);
1090
+
1091
+ //update world inverse matrix
1092
+ camera.matrixWorldInverse.copy(camera.matrixWorld);
1093
+ camera.matrixWorldInverse.invert();
1094
+
1095
+ // this.__projection_matrix.set(camera.matrixWorldInverse.elements);
1096
+ m4_multiply(this.__projection_matrix, camera.projectionMatrix.elements, camera.matrixWorldInverse.elements);
1097
+
1098
+ // console.time('__build_view_frustum');
1099
+ this.__build_view_frustum(camera);
1100
+ // console.timeEnd('__build_view_frustum');
1101
+
1102
+
1103
+ // console.time('__build_visible_light_list');
1104
+ this.__build_visible_light_list();
1105
+ // console.timeEnd('__build_visible_light_list');
1106
+
1107
+ // update decal atlas
1108
+ this.__decal_atlas.update();
1109
+ this.__update_decal_atlas_texture();
1110
+
1111
+ // console.time('__write_light_data_texture');
1112
+ if (this.__light_data_needs_update) {
1113
+ // sort light for better search and query locality
1114
+ this.__sort_visible_light();
1115
+
1116
+ this.__write_light_data_texture();
1117
+
1118
+ // clear flag
1119
+ this.__light_data_needs_update = false;
1120
+ }
1121
+ // console.timeEnd('__write_light_data_texture');
1122
+
1123
+ if (this.__visible_bvh_needs_update) {
1124
+ this.__update_visible_bvh();
1125
+ }
1126
+
1127
+ this.__build_cluster_frustum_points();
1128
+ // console.time('__build_cluster_frustum_planes');
1129
+ this.__build_cluster_frustum_planes();
1130
+ // console.timeEnd('__build_cluster_frustum_planes');
1131
+
1132
+ // console.time('__assign_lights_to_clusters');
1133
+ this.__assign_lights_to_clusters();
1134
+ // console.timeEnd('__assign_lights_to_clusters');
1135
+ }
1136
+
1137
+ dispose() {
1138
+ this.__cluster_texture.dispose();
1139
+ this.__light_data.dispose();
1140
+ this.__lookup_data.dispose();
1141
+ this.__decal_atlas_texture.dispose();
1142
+ }
1143
+
1144
+ /**
1145
+ * Set resolution of the cluster texture, higher resolution will result in less load on the GPU, but will take up more RAM to represent and more time to build the clusters each frame
1146
+ * @param {number} x
1147
+ * @param {number} y
1148
+ * @param {number} z
1149
+ */
1150
+ setTileMapResolution(x, y, z) {
1151
+ assert.isNumber(x, 'x');
1152
+ assert.isNonNegativeInteger(x, 'x');
1153
+ assert.isFinite(x, 'x');
1154
+ assert.greaterThan(x, 0, 'x must be > 0');
1155
+
1156
+ assert.isNumber(y, 'y');
1157
+ assert.isNonNegativeInteger(y, 'y');
1158
+ assert.isFinite(y, 'y');
1159
+ assert.greaterThan(y, 0, 'y must be > 0');
1160
+
1161
+ assert.isNumber(z, 'z');
1162
+ assert.isNonNegativeInteger(z, 'z');
1163
+ assert.isFinite(z, 'z');
1164
+ assert.greaterThan(z, 0, 'z must be > 0');
1165
+
1166
+ const r = this.__tiles_resolution;
1167
+
1168
+ if (x === r.x && y === r.y && z === r.z) {
1169
+ // special case, no change
1170
+ return;
1171
+ }
1172
+
1173
+ r.set(x, y, z);
1174
+
1175
+ this.__cluster_texture_needs_rebuild = true;
1176
+ this.__update_cluster_texture();
1177
+
1178
+ const cluster_planes_size = ((x + 1) + (y + 1) + (z + 1)) * 4;
1179
+ const cluster_frustum_points_size = (x + 1) * (y + 1) * (z + 1) * 3;
1180
+
1181
+ const buffer = new ArrayBuffer((cluster_planes_size + cluster_frustum_points_size) * 4);
1182
+
1183
+ this.__cluster_planes = new Float32Array(
1184
+ buffer, 0,
1185
+ cluster_planes_size
1186
+ );
1187
+ this.__cluster_frustum_points = new Float32Array(
1188
+ buffer, cluster_planes_size * 4,
1189
+ cluster_frustum_points_size
1190
+ );
1191
+ }
1192
+
1193
+ /**
1194
+ *
1195
+ * @param {AbstractLight} light
1196
+ * @returns {boolean}
1197
+ */
1198
+ hasLight(light) {
1199
+ return this.#metadata_map.has(light.id);
1200
+ }
1201
+
1202
+ /**
1203
+ *
1204
+ * @param {AbstractLight} light
1205
+ */
1206
+ addLight(light) {
1207
+ const lightData = new LightRenderMetadata(light);
1208
+
1209
+ lightData.link(this.#light_data_bvh);
1210
+
1211
+ this.#metadata_map.set(light.id, lightData);
1212
+ }
1213
+
1214
+ /**
1215
+ *
1216
+ * @param {AbstractLight} light
1217
+ * @returns {boolean}
1218
+ */
1219
+ removeLight(light) {
1220
+
1221
+ const light_id = light.id;
1222
+
1223
+ const data = this.#metadata_map.get(light_id);
1224
+
1225
+ if (data === undefined) {
1226
+ return false;
1227
+ }
1228
+
1229
+ data.unlink();
1230
+
1231
+ this.#metadata_map.delete(light_id);
1232
+
1233
+ return true;
1234
+ }
1235
+ }