@woosh/meep-engine 2.44.7 → 2.44.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/cache/Cache.js +1 -1
- package/core/localization/LanguageMetadata.js +28 -0
- package/core/localization/Localization.js +244 -0
- package/core/model/node-graph/DataType.js +1 -1
- package/core/model/node-graph/node/Port.js +4 -0
- package/editor/view/ecs/components/items/ItemContainerController.stories.js +1 -1
- package/engine/Engine.js +1 -1
- package/engine/asset/AssetManager.js +4 -0
- package/engine/ecs/speaker/VoiceSystem.js +1 -1
- package/engine/graphics/ecs/decal/v2/prototypeDecalSystem.js +17 -11
- package/engine/graphics/render/forward_plus/LightManager.js +20 -122
- package/engine/graphics/render/forward_plus/SPECIFICATION.md +155 -0
- package/engine/graphics/render/forward_plus/assign_cluster.js +125 -0
- package/engine/graphics/render/forward_plus/model/Decal.js +6 -2
- package/engine/graphics/render/forward_plus/prototype/prototypeLightManager.js +3 -3
- package/engine/graphics/render/forward_plus/query/query_bvh_frustum_from_texture.js +2 -2
- package/engine/graphics/render/forward_plus/sort_decal_data.js +102 -0
- package/engine/graphics/sh3/path_tracer/prototypePathTracer.js +1 -1
- package/package.json +1 -1
- package/core/Localization.js +0 -199
package/core/cache/Cache.js
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { assert } from "../assert.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
export class LanguageMetadata {
|
|
7
|
+
/**
|
|
8
|
+
* Measured in characters per second
|
|
9
|
+
* @type {number}
|
|
10
|
+
*/
|
|
11
|
+
reading_speed = 10
|
|
12
|
+
|
|
13
|
+
fromJSON({ reading_speed = 10 }) {
|
|
14
|
+
|
|
15
|
+
assert.isNumber(reading_speed, 'reading_speed');
|
|
16
|
+
assert.greaterThan(reading_speed, 0, 'reading_speed');
|
|
17
|
+
|
|
18
|
+
this.reading_speed = reading_speed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static fromJSON(j) {
|
|
22
|
+
const r = new LanguageMetadata();
|
|
23
|
+
|
|
24
|
+
r.fromJSON(j);
|
|
25
|
+
|
|
26
|
+
return r;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import levenshtein from "fast-levenshtein";
|
|
2
|
+
import { parseTooltipString } from "../../view/tooltip/gml/parser/parseTooltipString.js";
|
|
3
|
+
import { assert } from "../assert.js";
|
|
4
|
+
import ObservedString from "../model/ObservedString.js";
|
|
5
|
+
import { seedVariablesIntoTemplateString } from "../parser/seedVariablesIntoTemplateString.js";
|
|
6
|
+
import { STRING_TEMPLATE_VARIABLE_REGEX } from "../parser/STRING_TEMPLATE_VARIABLE_REGEX.js";
|
|
7
|
+
import { Cache } from "../cache/Cache.js";
|
|
8
|
+
import { computeStringHash } from "../primitives/strings/computeStringHash.js";
|
|
9
|
+
import { computeHashArray } from "../collection/array/computeHashArray.js";
|
|
10
|
+
import { LanguageMetadata } from "./LanguageMetadata.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validation utility method
|
|
14
|
+
* @param {string} template
|
|
15
|
+
* @return {string}
|
|
16
|
+
*/
|
|
17
|
+
function validationMockSeed(template) {
|
|
18
|
+
|
|
19
|
+
const result = template.replace(STRING_TEMPLATE_VARIABLE_REGEX, function (match, varName) {
|
|
20
|
+
return "0";
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const EMPTY_SEED = Object.freeze({});
|
|
27
|
+
|
|
28
|
+
const DEFAULT_LANGUAGE_METADATA = new LanguageMetadata();
|
|
29
|
+
|
|
30
|
+
export class Localization {
|
|
31
|
+
constructor() {
|
|
32
|
+
/**
|
|
33
|
+
*
|
|
34
|
+
* @type {AssetManager|null}
|
|
35
|
+
*/
|
|
36
|
+
this.assetManager = null;
|
|
37
|
+
|
|
38
|
+
this.json = {};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @protected
|
|
42
|
+
* @type {LanguageMetadata}
|
|
43
|
+
*/
|
|
44
|
+
this.language_metadata = DEFAULT_LANGUAGE_METADATA;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
*
|
|
48
|
+
* @type {ObservedString}
|
|
49
|
+
*/
|
|
50
|
+
this.locale = new ObservedString('');
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* In debug mode error messages are a lot more verbose and helpful
|
|
54
|
+
* @type {boolean}
|
|
55
|
+
*/
|
|
56
|
+
this.debug = true;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @type {Cache<string[], string>}
|
|
60
|
+
* @private
|
|
61
|
+
*/
|
|
62
|
+
this.__failure_cache = new Cache({
|
|
63
|
+
maxWeight: 1024,
|
|
64
|
+
keyHashFunction: (key) => computeHashArray(key, computeStringHash)
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Measured in characters per second
|
|
70
|
+
* @deprecated use 'language_metadata' directly instead
|
|
71
|
+
* @return {number}
|
|
72
|
+
*/
|
|
73
|
+
get reading_speed() {
|
|
74
|
+
return this.language_metadata.reading_speed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Time required to read a piece of text, in seconds
|
|
79
|
+
* @param {string} text
|
|
80
|
+
* @returns {number} time in seconds
|
|
81
|
+
*/
|
|
82
|
+
estimateReadingTime(text) {
|
|
83
|
+
assert.isString(text, 'text');
|
|
84
|
+
|
|
85
|
+
return text.length / this.reading_speed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
*
|
|
90
|
+
* @param {AssetManager} am
|
|
91
|
+
*/
|
|
92
|
+
setAssetManager(am) {
|
|
93
|
+
this.assetManager = am;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
* @param {function(key:string, error:*, original: string)} errorConsumer
|
|
99
|
+
*/
|
|
100
|
+
validate(errorConsumer) {
|
|
101
|
+
let result = true;
|
|
102
|
+
|
|
103
|
+
for (let key in this.json) {
|
|
104
|
+
const value = this.json[key];
|
|
105
|
+
|
|
106
|
+
const seededValue = validationMockSeed(value);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
parseTooltipString(seededValue);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
result = false;
|
|
112
|
+
errorConsumer(key, e, value);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
*
|
|
121
|
+
* @param {string} locale
|
|
122
|
+
* @param {string} path where to look for localization data
|
|
123
|
+
* @returns {Promise}
|
|
124
|
+
*/
|
|
125
|
+
loadLocale(locale, path = 'data/database/text') {
|
|
126
|
+
assert.isString(locale, 'locale');
|
|
127
|
+
assert.isString(path, 'path');
|
|
128
|
+
|
|
129
|
+
const am = this.assetManager;
|
|
130
|
+
|
|
131
|
+
if (am === null) {
|
|
132
|
+
throw new Error('AssetManager is not set');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const pLoadData = am.promise(`${path}/${locale}.json`, "json")
|
|
136
|
+
.then(asset => {
|
|
137
|
+
const json = asset.create();
|
|
138
|
+
|
|
139
|
+
this.json = json;
|
|
140
|
+
|
|
141
|
+
this.locale.set(locale);
|
|
142
|
+
}, reason => {
|
|
143
|
+
|
|
144
|
+
console.error(`Failed to load locale data for locale '${locale}' : ${reason}`);
|
|
145
|
+
|
|
146
|
+
// reset data
|
|
147
|
+
this.json = {};
|
|
148
|
+
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const pLoadMetadata = am.promise(`${path}/languages.json`, "json")
|
|
152
|
+
.then(asset => {
|
|
153
|
+
const languages_metadata = asset.create();
|
|
154
|
+
|
|
155
|
+
this.language_metadata = LanguageMetadata.fromJSON(languages_metadata[locale]);
|
|
156
|
+
}, reason => {
|
|
157
|
+
console.error(`Failed to load language metadata: ${reason}`);
|
|
158
|
+
|
|
159
|
+
this.language_metadata = DEFAULT_LANGUAGE_METADATA;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return Promise.all([pLoadData, pLoadMetadata]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
*
|
|
167
|
+
* @param {String} id
|
|
168
|
+
* @return {boolean}
|
|
169
|
+
*/
|
|
170
|
+
hasString(id) {
|
|
171
|
+
return this.json[id] !== undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
*
|
|
176
|
+
* @param {number} value
|
|
177
|
+
*/
|
|
178
|
+
formatIntegerByThousands(value) {
|
|
179
|
+
const formatter = new Intl.NumberFormat(this.locale.getValue(), { useGrouping: true });
|
|
180
|
+
|
|
181
|
+
return formatter.format(value);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
*
|
|
186
|
+
* @param {string} id
|
|
187
|
+
* @param {Object} seed
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
__debugMissingKey(id, seed) {
|
|
191
|
+
const locale = this.locale.getValue();
|
|
192
|
+
|
|
193
|
+
const seed_string = JSON.stringify(seed);
|
|
194
|
+
|
|
195
|
+
const message = this.__failure_cache.getOrCompute([locale, id, seed_string], () => {
|
|
196
|
+
|
|
197
|
+
//try to find similar keys
|
|
198
|
+
const similarities = Object.keys(this.json).map(function (key) {
|
|
199
|
+
const distance = levenshtein.get(key, id);
|
|
200
|
+
return {
|
|
201
|
+
key,
|
|
202
|
+
distance
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
similarities.sort(function (a, b) {
|
|
207
|
+
return a.distance - b.distance;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const suggestions = similarities.slice(0, 3).map(p => p.key);
|
|
211
|
+
|
|
212
|
+
return `No localization value for id='${id}', seed=${seed_string}, approximate matches: ${suggestions.join(', ')}`;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
console.warn(message);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
*
|
|
220
|
+
* @param {string} id
|
|
221
|
+
* @param {object} [seed]
|
|
222
|
+
*
|
|
223
|
+
* @returns {string}
|
|
224
|
+
*/
|
|
225
|
+
getString(id, seed = EMPTY_SEED) {
|
|
226
|
+
assert.isString(id, 'id');
|
|
227
|
+
|
|
228
|
+
const value = this.json[id];
|
|
229
|
+
|
|
230
|
+
if (value === undefined) {
|
|
231
|
+
|
|
232
|
+
if (this.debug) {
|
|
233
|
+
this.__debugMissingKey(id, seed)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
//no value, provide substitute
|
|
237
|
+
return `@${id}`;
|
|
238
|
+
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
//value needs to be seeded
|
|
242
|
+
return seedVariablesIntoTemplateString(value, seed);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { storiesOf } from "@storybook/html/dist/client/preview";
|
|
2
|
-
import { Localization } from "../../../../../core/Localization.js";
|
|
2
|
+
import { Localization } from "../../../../../core/localization/Localization.js";
|
|
3
3
|
import { ItemContainerController } from "./ItemContainerController.js";
|
|
4
4
|
import ItemContainer from "../../../../../../model/game/ecs/component/item_container/ItemContainer.js";
|
|
5
5
|
import Item from "../../../../../../model/game/ecs/component/Item.js";
|
package/engine/Engine.js
CHANGED
|
@@ -23,7 +23,7 @@ import EmptyView from "../view/elements/EmptyView.js";
|
|
|
23
23
|
import { assert } from "../core/assert.js";
|
|
24
24
|
import { StaticKnowledgeDatabase } from "./knowledge/database/StaticKnowledgeDatabase.js";
|
|
25
25
|
import Ticker from "./simulation/Ticker.js";
|
|
26
|
-
import { Localization } from "../core/Localization.js";
|
|
26
|
+
import { Localization } from "../core/localization/Localization.js";
|
|
27
27
|
import { ModuleRegistry } from "../core/model/ModuleRegistry.js";
|
|
28
28
|
import { BinarySerializationRegistry } from "./ecs/storage/binary/BinarySerializationRegistry.js";
|
|
29
29
|
import { EnginePluginManager } from "./plugin/EnginePluginManager.js";
|
|
@@ -326,6 +326,10 @@ export class AssetManager {
|
|
|
326
326
|
throw new Error("Path must be string. Path = " + JSON.stringify(path));
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
if(typeof type !== "string"){
|
|
330
|
+
throw new TypeError(`type must be a string, instead was '${typeof type}'`);
|
|
331
|
+
}
|
|
332
|
+
|
|
329
333
|
const assetDescription = new AssetDescription(path, type);
|
|
330
334
|
|
|
331
335
|
const asset = this.assets.get(assetDescription);
|
|
@@ -373,7 +373,7 @@ export class VoiceSystem extends AbstractContextSystem {
|
|
|
373
373
|
// localized line may contain reference tags, the user will not see/read those, so we also compile line as pure text for estimating reading time
|
|
374
374
|
const line_pure_text = gml.compileAsText(localized_line);
|
|
375
375
|
|
|
376
|
-
const display_time_raw = localiation.
|
|
376
|
+
const display_time_raw = localiation.estimateReadingTime(line_pure_text);
|
|
377
377
|
|
|
378
378
|
const display_time = max2(TIMING_MINIMUM_READ_TIME, display_time_raw * line.displayDuration) + TIMING_NOTICE_DELAY;
|
|
379
379
|
|
|
@@ -15,7 +15,7 @@ import { GameAssetType } from "../../../../asset/GameAssetType.js";
|
|
|
15
15
|
import { GLTFAssetLoader } from "../../../../asset/loaders/GLTFAssetLoader.js";
|
|
16
16
|
import '../../../../../../../../css/game.scss';
|
|
17
17
|
import { countTask } from "../../../../../core/process/task/util/countTask.js";
|
|
18
|
-
import Vector2 from "../../../../../core/geom/Vector2.js";
|
|
18
|
+
import Vector2, { v2_distance } from "../../../../../core/geom/Vector2.js";
|
|
19
19
|
import { RotationBehavior } from "../../../../intelligence/behavior/util/RotationBehavior.js";
|
|
20
20
|
import { BehaviorComponent } from "../../../../intelligence/behavior/ecs/BehaviorComponent.js";
|
|
21
21
|
import Vector3 from "../../../../../core/geom/Vector3.js";
|
|
@@ -270,14 +270,17 @@ function grid(ecd, offset_x, offset_y, x, y, size_x, size_y, spacing, textures)
|
|
|
270
270
|
function makeNormalTestGridDecal(ecd, root_transform = new Transform()) {
|
|
271
271
|
const uv = new OctahedralUvEncoder();
|
|
272
272
|
|
|
273
|
-
const GRID_SIZE =
|
|
273
|
+
const GRID_SIZE = 16;
|
|
274
274
|
|
|
275
275
|
const CELL_SIZE = 1;
|
|
276
276
|
|
|
277
|
-
const SPACING = CELL_SIZE*1.2;
|
|
277
|
+
const SPACING = CELL_SIZE * 1.2;
|
|
278
|
+
// const SPACING = CELL_SIZE * 0.6;
|
|
278
279
|
|
|
279
280
|
const DEBUG_BOX = makeHelperBoxGeometry();
|
|
280
281
|
|
|
282
|
+
const random = seededRandom(7);
|
|
283
|
+
|
|
281
284
|
for (let i = 0; i < GRID_SIZE; i++) {
|
|
282
285
|
for (let j = 0; j < GRID_SIZE; j++) {
|
|
283
286
|
|
|
@@ -290,7 +293,7 @@ function makeNormalTestGridDecal(ecd, root_transform = new Transform()) {
|
|
|
290
293
|
const decal = new Decal();
|
|
291
294
|
|
|
292
295
|
// decal.uri = decal_urls[1];
|
|
293
|
-
decal.uri ='moicon/ISO 7010 - Safety Signs (3)/ISO 7010 - Safety Signs/Emergency/400px/E001 – Emergency exit (left hand).png';
|
|
296
|
+
decal.uri = 'moicon/ISO 7010 - Safety Signs (3)/ISO 7010 - Safety Signs/Emergency/400px/E001 – Emergency exit (left hand).png';
|
|
294
297
|
|
|
295
298
|
const entity = new EntityBuilder();
|
|
296
299
|
|
|
@@ -303,7 +306,7 @@ function makeNormalTestGridDecal(ecd, root_transform = new Transform()) {
|
|
|
303
306
|
0,
|
|
304
307
|
);
|
|
305
308
|
transform.scale.setScalar(CELL_SIZE);
|
|
306
|
-
transform.rotation._lookRotation(direction[0],direction[2], direction[1]
|
|
309
|
+
transform.rotation._lookRotation(direction[0], direction[2], direction[1], 0, 1, 0);
|
|
307
310
|
|
|
308
311
|
transform.multiplyTransforms(root_transform, transform);
|
|
309
312
|
|
|
@@ -314,9 +317,12 @@ function makeNormalTestGridDecal(ecd, root_transform = new Transform()) {
|
|
|
314
317
|
.add(decal)
|
|
315
318
|
.build(ecd);
|
|
316
319
|
|
|
317
|
-
setInterval(()=>{
|
|
318
|
-
|
|
319
|
-
},7000);
|
|
320
|
+
// setInterval(()=>{
|
|
321
|
+
// decal.priority = Math.random();
|
|
322
|
+
// },7000);
|
|
323
|
+
|
|
324
|
+
// decal.priority = randomIntegerBetween(random,0,4096);
|
|
325
|
+
decal.priority = v2_distance(i, j, GRID_SIZE * 0.5, GRID_SIZE * 0.5);
|
|
320
326
|
|
|
321
327
|
|
|
322
328
|
// make an arrow helper
|
|
@@ -330,7 +336,7 @@ function makeNormalTestGridDecal(ecd, root_transform = new Transform()) {
|
|
|
330
336
|
side: DoubleSide
|
|
331
337
|
}), DrawMode.Triangles))
|
|
332
338
|
.add(arrow_transform)
|
|
333
|
-
|
|
339
|
+
.build(ecd);
|
|
334
340
|
}
|
|
335
341
|
}
|
|
336
342
|
}
|
|
@@ -448,9 +454,9 @@ async function main(engine) {
|
|
|
448
454
|
new EntityBuilder()
|
|
449
455
|
.add(SGMesh.fromURL('data/models/snaps/cube_white.gltf'))
|
|
450
456
|
.add(Transform.fromJSON({
|
|
451
|
-
|
|
457
|
+
position: new Vector3(14, 1, 4.8),
|
|
452
458
|
rotation: t_forward_grid.rotation,
|
|
453
|
-
scale: new Vector3(25,25,0.1)
|
|
459
|
+
scale: new Vector3(25, 25, 0.1)
|
|
454
460
|
}))
|
|
455
461
|
.build(ecd);
|
|
456
462
|
|
|
@@ -17,7 +17,6 @@ import Vector3 from "../../../../core/geom/Vector3.js";
|
|
|
17
17
|
import { query_bvh_frustum_from_objects } from "./query/query_bvh_frustum_from_objects.js";
|
|
18
18
|
import { IncrementalDeltaSet } from "../visibility/IncrementalDeltaSet.js";
|
|
19
19
|
import { computeFrustumCorners } from "./computeFrustumCorners.js";
|
|
20
|
-
import { compute_light_data_hash_0 } from "./cluster/compute_light_data_hash_0.js";
|
|
21
20
|
import { read_plane_pair } from "./cluster/read_plane_pair.js";
|
|
22
21
|
import { read_frustum_planes_to_array } from "../../../../core/geom/3d/frustum/read_frustum_planes_to_array.js";
|
|
23
22
|
import { compute_cluster_planes_from_points } from "./cluster/compute_cluster_planes_from_points.js";
|
|
@@ -30,9 +29,7 @@ import { NumericType } from "./data/NumericType.js";
|
|
|
30
29
|
import { computeDataType } from "./data/computeDataType.js";
|
|
31
30
|
import { computeThreeTextureTypeFromDataType } from "./data/computeThreeTextureTypeFromDataType.js";
|
|
32
31
|
import { computeThreeTextureInternalFormatFromDataType } from "./data/computeThreeTextureInternalFormatFromDataType.js";
|
|
33
|
-
import { testClusterEquality } from "./testClusterEquality.js";
|
|
34
32
|
import { BinaryUint32BVH } from "../../../../core/bvh2/binary/2/BinaryUint32BVH.js";
|
|
35
|
-
import { query_bvh_frustum_from_texture } from "./query/query_bvh_frustum_from_texture.js";
|
|
36
33
|
import { mat4 } from "gl-matrix";
|
|
37
34
|
import { TextureAtlas } from "../../texture/atlas/TextureAtlas.js";
|
|
38
35
|
import { CachingTextureAtlas } from "../../texture/atlas/CachingTextureAtlas.js";
|
|
@@ -48,46 +45,8 @@ import { arraySwapElements } from "../../../../core/collection/array/arraySwapEl
|
|
|
48
45
|
import { compareObjectsByNumericId } from "../../../../core/model/object/compareObjectsByNumericId.js";
|
|
49
46
|
import { slice_frustum_linear_to_points } from "../../../../core/geom/3d/frustum/slice_frustum_linear_to_points.js";
|
|
50
47
|
import { read_cluster_frustum_corners } from "../../../../core/geom/3d/frustum/read_cluster_frustum_corners.js";
|
|
48
|
+
import { assign_cluster, LOOKUP_CACHE, scratch_corners, scratch_frustum_planes } from "./assign_cluster.js";
|
|
51
49
|
|
|
52
|
-
const LOOKUP_CACHE_SIZE = 1024; // must be power of two
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Using continuous buffer for related attributes to get better cache utilization
|
|
56
|
-
* @type {ArrayBuffer}
|
|
57
|
-
* @private
|
|
58
|
-
*/
|
|
59
|
-
const _scratch_array_buffer = new ArrayBuffer(
|
|
60
|
-
24 * 4
|
|
61
|
-
+ 24 * 4
|
|
62
|
-
+ LOOKUP_CACHE_SIZE * 4
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
*
|
|
67
|
-
* @type {number[]|Float32Array|Float64Array}
|
|
68
|
-
*/
|
|
69
|
-
const scratch_corners = new Float32Array(_scratch_array_buffer, 0, 24);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Linear access data structure with parameters of each plane of the frustum
|
|
74
|
-
* 6 planes, 4 values per plane
|
|
75
|
-
* Layout: [normal.x, normal.y, normal.z, constant]
|
|
76
|
-
* @type {number[]|Float32Array|Float64Array}
|
|
77
|
-
*/
|
|
78
|
-
const scratch_frustum_planes = new Float32Array(_scratch_array_buffer, 24 * 4, 24);
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Cache used to identify duplicate cells. Helps reduce memory usage and just point to equivalent existing cells instead of allocating new ones.
|
|
82
|
-
* @type {Uint16Array|Uint32Array}
|
|
83
|
-
*/
|
|
84
|
-
const LOOKUP_CACHE = new Uint32Array(_scratch_array_buffer, (24 + 24) * 4, LOOKUP_CACHE_SIZE);
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
*
|
|
88
|
-
* @type {number[]}
|
|
89
|
-
*/
|
|
90
|
-
const scratch_light_nodes = [];
|
|
91
50
|
|
|
92
51
|
/**
|
|
93
52
|
* How many bits are used to encode light type
|
|
@@ -121,84 +80,6 @@ function encode_light_descriptor(address, light) {
|
|
|
121
80
|
return light_type | (address << 2);
|
|
122
81
|
}
|
|
123
82
|
|
|
124
|
-
function assign_cluster(
|
|
125
|
-
tile_texture_data_offset,
|
|
126
|
-
light_bvh,
|
|
127
|
-
lookup_address_offset,
|
|
128
|
-
light_lookup_texture_data,
|
|
129
|
-
tile_texture_data,
|
|
130
|
-
light_source_data
|
|
131
|
-
) {
|
|
132
|
-
|
|
133
|
-
// at this point we have a fully constructed cluster, we can query lights within the cluster
|
|
134
|
-
const match_count = query_bvh_frustum_from_texture(
|
|
135
|
-
scratch_light_nodes,
|
|
136
|
-
0,
|
|
137
|
-
light_bvh,
|
|
138
|
-
light_source_data,
|
|
139
|
-
scratch_frustum_planes,
|
|
140
|
-
scratch_corners
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
// construct tile data
|
|
144
|
-
|
|
145
|
-
// write number of light entries
|
|
146
|
-
tile_texture_data[tile_texture_data_offset + 1] = match_count;
|
|
147
|
-
|
|
148
|
-
if (match_count === 0) {
|
|
149
|
-
// don't waste time on assignment, there are no lights in the cluster
|
|
150
|
-
return 0;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/*
|
|
154
|
-
We can sort lights by index to get higher cache utilization, but it turns out quite costly.
|
|
155
|
-
Through experimentation, sorting was taking around 20% of the whole assignment time
|
|
156
|
-
while improving number of cache hits by around 40% (8000 without sorting -> 11000 with sorting)
|
|
157
|
-
*/
|
|
158
|
-
const cluster_data_hash = compute_light_data_hash_0(scratch_light_nodes, match_count) & (LOOKUP_CACHE_SIZE - 1);
|
|
159
|
-
|
|
160
|
-
const existing_lookup_address = LOOKUP_CACHE[cluster_data_hash];
|
|
161
|
-
|
|
162
|
-
if (existing_lookup_address !== 0xFFFFFFFF) {
|
|
163
|
-
// found a hash match, validate equality
|
|
164
|
-
const existing_lookup_entry_match = testClusterEquality(
|
|
165
|
-
scratch_light_nodes,
|
|
166
|
-
match_count,
|
|
167
|
-
light_lookup_texture_data,
|
|
168
|
-
existing_lookup_address
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
if (existing_lookup_entry_match) {
|
|
172
|
-
tile_texture_data[tile_texture_data_offset] = existing_lookup_address;
|
|
173
|
-
|
|
174
|
-
//hash_reuse_count++;
|
|
175
|
-
|
|
176
|
-
return 0;
|
|
177
|
-
}
|
|
178
|
-
} else {
|
|
179
|
-
// no hash entry exists, create one
|
|
180
|
-
LOOKUP_CACHE[cluster_data_hash] = lookup_address_offset;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// if we get to this point, that means no hash match was found, or equality check was failed
|
|
184
|
-
|
|
185
|
-
// write address of the lookup table
|
|
186
|
-
tile_texture_data[tile_texture_data_offset] = lookup_address_offset;
|
|
187
|
-
|
|
188
|
-
for (let i = 0; i < match_count; i++) {
|
|
189
|
-
/**
|
|
190
|
-
*
|
|
191
|
-
* @type {number}
|
|
192
|
-
*/
|
|
193
|
-
const light_descriptor = scratch_light_nodes[i];
|
|
194
|
-
|
|
195
|
-
// construct lookup table
|
|
196
|
-
light_lookup_texture_data[lookup_address_offset + i] = light_descriptor;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return match_count;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
83
|
/**
|
|
203
84
|
*
|
|
204
85
|
* @param {IncrementalDeltaSet<PointLightData>} data_set
|
|
@@ -968,7 +849,7 @@ export class LightManager {
|
|
|
968
849
|
|
|
969
850
|
/**
|
|
970
851
|
*
|
|
971
|
-
* @type {number[]|
|
|
852
|
+
* @type {number[]|Float32Array}
|
|
972
853
|
*/
|
|
973
854
|
const light_source_data = this.__light_data.getTexture().image.data;
|
|
974
855
|
|
|
@@ -1033,7 +914,7 @@ export class LightManager {
|
|
|
1033
914
|
light_source_data
|
|
1034
915
|
);
|
|
1035
916
|
|
|
1036
|
-
|
|
917
|
+
const decal_count = assign_cluster(
|
|
1037
918
|
tile_data_offset + 2,
|
|
1038
919
|
bvh_decals,
|
|
1039
920
|
lookup_address_offset,
|
|
@@ -1041,6 +922,23 @@ export class LightManager {
|
|
|
1041
922
|
tile_texture_data,
|
|
1042
923
|
light_source_data
|
|
1043
924
|
);
|
|
925
|
+
|
|
926
|
+
/*
|
|
927
|
+
TODO fix decal sorting artifacts
|
|
928
|
+
|
|
929
|
+
if (decal_count > 1) { // only sort when there are 2 or more, sorting 1 object is pointless
|
|
930
|
+
// decals need to be sorted for correct blending result
|
|
931
|
+
sort_decal_data(
|
|
932
|
+
light_lookup_texture_data,
|
|
933
|
+
light_source_data,
|
|
934
|
+
lookup_address_offset,
|
|
935
|
+
decal_count
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
*/
|
|
940
|
+
|
|
941
|
+
lookup_address_offset += decal_count;
|
|
1044
942
|
}
|
|
1045
943
|
}
|
|
1046
944
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Clustered Lighting Specification
|
|
2
|
+
|
|
3
|
+
## 1. GPU Data structures
|
|
4
|
+
|
|
5
|
+
Data is organized into 3 main textures:
|
|
6
|
+
|
|
7
|
+
- Clusters. `type: UINT, format: RGBA, dimension: 3D, filtering: nearest(none)`
|
|
8
|
+
- R - Offset into lookup texture for lights
|
|
9
|
+
- G - number of lights
|
|
10
|
+
- B - Offset into lookup texture for decals
|
|
11
|
+
- A - number of decals
|
|
12
|
+
- Lookup. `type: UINT, format: R, dimensions: 2D, filtering: nearest(none)`
|
|
13
|
+
- R - encoded reference into data texture, where actual lighting information is stored
|
|
14
|
+
- Data. `type: FLOAT, format: RGBA, dimensions: 2D, filtering: nearest(none)`
|
|
15
|
+
- RGBA - single `word` of data, you can think of it as a collection of `VEC4`s
|
|
16
|
+
|
|
17
|
+
Below is a glsl snippet with initialization of uniforms for these textures:
|
|
18
|
+
```glsl
|
|
19
|
+
uniform usampler3D fp_t_light_clusters;
|
|
20
|
+
uniform usampler2D fp_t_light_lookup;
|
|
21
|
+
uniform sampler2D fp_t_light_data;
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 2. Light Data Format
|
|
25
|
+
|
|
26
|
+
Each light is represented as a collection of `VEC4`s inside the `Data` texture.
|
|
27
|
+
|
|
28
|
+
- Point Light:
|
|
29
|
+
- `[ position.x, position.y, position.z, radius ]`
|
|
30
|
+
- `[ color.r, color.g, color.b, intensity ]`
|
|
31
|
+
- Directional Light:
|
|
32
|
+
- `[ direction.x, direction.y, direction.z, UNUSED ]`
|
|
33
|
+
- `[ color.r, color.g, color.b, intensity ]`
|
|
34
|
+
|
|
35
|
+
## 3. Light reference encoding
|
|
36
|
+
|
|
37
|
+
Lookup texture contains integers that encode both the address of light data, and the type of light found at that address, this is done by encoding light type into lower 2 bits of a UINT32
|
|
38
|
+
|
|
39
|
+
Light type codes:
|
|
40
|
+
- 0 - PointLight
|
|
41
|
+
- 1 - Directional Light
|
|
42
|
+
- 2 - Spotlight
|
|
43
|
+
|
|
44
|
+
Encoding snippet in JavaScript:
|
|
45
|
+
```js
|
|
46
|
+
/**
|
|
47
|
+
*
|
|
48
|
+
* @param {number} address
|
|
49
|
+
* @param {AbstractLight} light
|
|
50
|
+
* @returns {number}
|
|
51
|
+
*/
|
|
52
|
+
function encode_light_descriptor(address, light) {
|
|
53
|
+
const light_type = encode_light_type(light);
|
|
54
|
+
return light_type | (address << 2);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Decoding snippet in GLSL
|
|
59
|
+
```glsl
|
|
60
|
+
ivec2 address_to_data_texture_coordinates(uint address){
|
|
61
|
+
// Lookup texture has 128 width
|
|
62
|
+
uint lookup_index_x = address % 128u;
|
|
63
|
+
uint lookup_index_y = address >> 7;
|
|
64
|
+
|
|
65
|
+
return ivec2(int(lookup_index_x),int(lookup_index_y));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ...
|
|
69
|
+
uint light_descriptor = texelFetch(fp_t_light_lookup, address_to_data_texture_coordinates(lookup_index), 0 ).r;
|
|
70
|
+
|
|
71
|
+
uint type = (light_descriptor) & 0x3u;
|
|
72
|
+
uint light_address = light_descriptor >> 2;
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 4. Auxiliary material
|
|
76
|
+
|
|
77
|
+
Convert projection-space depth to linear space
|
|
78
|
+
```glsl
|
|
79
|
+
float convert_depth_to_linear(in float d){
|
|
80
|
+
float d_n = 2.0*d - 1.0;
|
|
81
|
+
|
|
82
|
+
float f = fp_f_camera_far;
|
|
83
|
+
float n = fp_f_camera_near;
|
|
84
|
+
|
|
85
|
+
float fn = f*n;
|
|
86
|
+
|
|
87
|
+
float z_diff = f - n;
|
|
88
|
+
|
|
89
|
+
float denominator = (f + n - d_n * z_diff );
|
|
90
|
+
|
|
91
|
+
float z_view = (2.0*fn) / denominator;
|
|
92
|
+
|
|
93
|
+
return (z_view - n) / z_diff;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Fetching cluster information from fragment shader inside a forward pass
|
|
98
|
+
```glsl
|
|
99
|
+
// v3_cluster_resolution is resolution of our cluster 3d texture
|
|
100
|
+
ivec3 v3_cluster_resolution = textureSize(fp_t_light_tiles, 0);
|
|
101
|
+
ivec3 v3_cluster_position = ivec3( clip_v.x * float(v3_cluster_resolution.x), clip_v.y*float(v3_cluster_resolution.y), (clip_v.z)*float(v3_cluster_resolution.z) );
|
|
102
|
+
|
|
103
|
+
uvec4 fp_cluster_metadata = texelFetch( fp_t_light_tiles, v3_cluster_position, 0 ).rgba;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Perform lighting (point-light only)
|
|
107
|
+
```glsl
|
|
108
|
+
// read light data
|
|
109
|
+
for(uint i=0u; i < fp_cluster_metadata.y; i++){
|
|
110
|
+
uint lookup_index = fp_cluster_metadata.x + i;
|
|
111
|
+
|
|
112
|
+
uint light_descriptor = texelFetch(fp_t_light_lookup, address_to_data_texture_coordinates(lookup_index), 0 ).r;
|
|
113
|
+
|
|
114
|
+
uint type = (light_descriptor) & 0x3u;
|
|
115
|
+
uint light_address = light_descriptor >> 2;
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if(type == 0u){
|
|
119
|
+
// point light
|
|
120
|
+
|
|
121
|
+
vec4 light_data_0 = texelFetch(fp_t_light_data, address_to_data_texture_coordinates(light_address), 0);
|
|
122
|
+
vec4 light_data_1 = texelFetch(fp_t_light_data, address_to_data_texture_coordinates(light_address+1u), 0);
|
|
123
|
+
|
|
124
|
+
vec3 light_position = light_data_0.xyz;
|
|
125
|
+
float light_radius = light_data_0.w;
|
|
126
|
+
|
|
127
|
+
vec3 light_color = light_data_1.xyz;
|
|
128
|
+
float light_intensity = light_data_1.w;
|
|
129
|
+
|
|
130
|
+
PointLight pointlight;
|
|
131
|
+
|
|
132
|
+
pointlight.position = light_position;
|
|
133
|
+
pointlight.distance = light_radius;
|
|
134
|
+
pointlight.color = light_color*light_intensity;
|
|
135
|
+
pointlight.decay = 1.0;
|
|
136
|
+
|
|
137
|
+
fp_getPointDirectLightIrradiance( pointlight, directLight, vViewPosition );
|
|
138
|
+
RE_Direct( directLight, geometry, material, reflectedLight );
|
|
139
|
+
|
|
140
|
+
} else if(type == 1u){
|
|
141
|
+
// directional light
|
|
142
|
+
|
|
143
|
+
// ...
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## References:
|
|
149
|
+
- [Dr.Strangelight or: How I learned to Stop Worrying and Love the Cluster](https://discourse.threejs.org/t/dr-strangelight-or-how-i-learned-to-stop-worrying-and-love-the-cluster/23104)
|
|
150
|
+
- [Practical Clustered Shading by Emil Persson](http://www.humus.name/Articles/PracticalClusteredShading.pdf)
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
Author: Alexander Goldring, Company Named Limited
|
|
154
|
+
|
|
155
|
+
Private and confidential, copyright (c) Company Named Limited, 2022
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { query_bvh_frustum_from_texture } from "./query/query_bvh_frustum_from_texture.js";
|
|
2
|
+
import { compute_light_data_hash_0 } from "./cluster/compute_light_data_hash_0.js";
|
|
3
|
+
import { testClusterEquality } from "./testClusterEquality.js";
|
|
4
|
+
|
|
5
|
+
const LOOKUP_CACHE_SIZE = 1024; // must be power of two
|
|
6
|
+
/**
|
|
7
|
+
* Using continuous buffer for related attributes to get better cache utilization
|
|
8
|
+
* @type {ArrayBuffer}
|
|
9
|
+
* @private
|
|
10
|
+
*/
|
|
11
|
+
const _scratch_array_buffer = new ArrayBuffer(
|
|
12
|
+
24 * 4
|
|
13
|
+
+ 24 * 4
|
|
14
|
+
+ LOOKUP_CACHE_SIZE * 4
|
|
15
|
+
);
|
|
16
|
+
/**
|
|
17
|
+
*
|
|
18
|
+
* @type {number[]|Float32Array|Float64Array}
|
|
19
|
+
*/
|
|
20
|
+
export const scratch_corners = new Float32Array(_scratch_array_buffer, 0, 24);
|
|
21
|
+
/**
|
|
22
|
+
* Linear access data structure with parameters of each plane of the frustum
|
|
23
|
+
* 6 planes, 4 values per plane
|
|
24
|
+
* Layout: [normal.x, normal.y, normal.z, constant]
|
|
25
|
+
* @type {number[]|Float32Array|Float64Array}
|
|
26
|
+
*/
|
|
27
|
+
export const scratch_frustum_planes = new Float32Array(_scratch_array_buffer, 24 * 4, 24);
|
|
28
|
+
/**
|
|
29
|
+
* Cache used to identify duplicate cells. Helps reduce memory usage and just point to equivalent existing cells instead of allocating new ones.
|
|
30
|
+
* @type {Uint16Array|Uint32Array}
|
|
31
|
+
*/
|
|
32
|
+
export const LOOKUP_CACHE = new Uint32Array(_scratch_array_buffer, (24 + 24) * 4, LOOKUP_CACHE_SIZE);
|
|
33
|
+
/**
|
|
34
|
+
*
|
|
35
|
+
* @type {number[]}
|
|
36
|
+
*/
|
|
37
|
+
const scratch_light_nodes = [];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
*
|
|
41
|
+
* @param {number} tile_texture_data_offset
|
|
42
|
+
* @param {BinaryUint32BVH} bvh
|
|
43
|
+
* @param {number} lookup_address_offset
|
|
44
|
+
* @param {number[]|ArrayLike<number>|Uint8ClampedArray|Uint8Array|Uint16Array|Uint32Array} lookup_data
|
|
45
|
+
* @param {number[]|ArrayLike<number>|Uint8Array|Uint8ClampedArray|Uint16Array|Uint32Array} cluster_data
|
|
46
|
+
* @param {number[]|ArrayLike<number>|Float32Array} source_data Light/Decal source information is stored in here, such as position, color etc.
|
|
47
|
+
* @return {number}
|
|
48
|
+
*/
|
|
49
|
+
export function assign_cluster(
|
|
50
|
+
tile_texture_data_offset,
|
|
51
|
+
bvh,
|
|
52
|
+
lookup_address_offset,
|
|
53
|
+
lookup_data,
|
|
54
|
+
cluster_data,
|
|
55
|
+
source_data
|
|
56
|
+
) {
|
|
57
|
+
|
|
58
|
+
// at this point we have a fully constructed cluster, we can query lights within the cluster
|
|
59
|
+
const match_count = query_bvh_frustum_from_texture(
|
|
60
|
+
scratch_light_nodes,
|
|
61
|
+
0,
|
|
62
|
+
bvh,
|
|
63
|
+
source_data,
|
|
64
|
+
scratch_frustum_planes,
|
|
65
|
+
scratch_corners
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// construct tile data
|
|
69
|
+
|
|
70
|
+
// write number of light entries
|
|
71
|
+
cluster_data[tile_texture_data_offset + 1] = match_count;
|
|
72
|
+
|
|
73
|
+
if (match_count === 0) {
|
|
74
|
+
// don't waste time on assignment, there are no lights in the cluster
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
We can sort lights by index to get higher cache utilization, but it turns out quite costly.
|
|
80
|
+
Through experimentation, sorting was taking around 20% of the whole assignment time
|
|
81
|
+
while improving number of cache hits by around 40% (8000 without sorting -> 11000 with sorting)
|
|
82
|
+
*/
|
|
83
|
+
const cluster_data_hash = compute_light_data_hash_0(scratch_light_nodes, match_count) & (LOOKUP_CACHE_SIZE - 1);
|
|
84
|
+
|
|
85
|
+
const existing_lookup_address = LOOKUP_CACHE[cluster_data_hash];
|
|
86
|
+
|
|
87
|
+
if (existing_lookup_address !== 0xFFFFFFFF) {
|
|
88
|
+
// found a hash match, validate equality
|
|
89
|
+
const existing_lookup_entry_match = testClusterEquality(
|
|
90
|
+
scratch_light_nodes,
|
|
91
|
+
match_count,
|
|
92
|
+
lookup_data,
|
|
93
|
+
existing_lookup_address
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (existing_lookup_entry_match) {
|
|
97
|
+
cluster_data[tile_texture_data_offset] = existing_lookup_address;
|
|
98
|
+
|
|
99
|
+
//hash_reuse_count++;
|
|
100
|
+
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
// no hash entry exists, create one
|
|
105
|
+
LOOKUP_CACHE[cluster_data_hash] = lookup_address_offset;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// if we get to this point, that means no hash match was found, or equality check was failed
|
|
109
|
+
|
|
110
|
+
// write address of the lookup table
|
|
111
|
+
cluster_data[tile_texture_data_offset] = lookup_address_offset;
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < match_count; i++) {
|
|
114
|
+
/**
|
|
115
|
+
*
|
|
116
|
+
* @type {number}
|
|
117
|
+
*/
|
|
118
|
+
const light_descriptor = scratch_light_nodes[i];
|
|
119
|
+
|
|
120
|
+
// construct lookup table
|
|
121
|
+
lookup_data[lookup_address_offset + i] = light_descriptor;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return match_count;
|
|
125
|
+
}
|
|
@@ -100,7 +100,11 @@ export class Decal extends AbstractLight {
|
|
|
100
100
|
|
|
101
101
|
array_copy(this.uv, 0, destination, address + 16, 4);
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
// required for sorting
|
|
104
|
+
destination[address + 20] = this.draw_priority;
|
|
105
|
+
destination[address + 21] = this.id;
|
|
106
|
+
|
|
107
|
+
return 24;
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
110
|
|
|
@@ -110,4 +114,4 @@ export class Decal extends AbstractLight {
|
|
|
110
114
|
*/
|
|
111
115
|
Decal.prototype.isDecal = true;
|
|
112
116
|
|
|
113
|
-
Decal.prototype.ENCODED_SLOT_COUNT =
|
|
117
|
+
Decal.prototype.ENCODED_SLOT_COUNT = 24;
|
|
@@ -62,7 +62,7 @@ import { TooltipManager } from "../../../../../view/tooltip/TooltipManager.js";
|
|
|
62
62
|
import { GMLEngine } from "../../../../../view/tooltip/gml/GMLEngine.js";
|
|
63
63
|
import { PointerDevice } from "../../../../input/devices/PointerDevice.js";
|
|
64
64
|
import { StaticKnowledgeDatabase } from "../../../../knowledge/database/StaticKnowledgeDatabase.js";
|
|
65
|
-
import { Localization } from "../../../../../core/Localization.js";
|
|
65
|
+
import { Localization } from "../../../../../core/localization/Localization.js";
|
|
66
66
|
import '../../../../../../../../css/game.scss'
|
|
67
67
|
import { randomFromArray } from "../../../../../core/math/random/randomFromArray.js";
|
|
68
68
|
import { debugAtlas } from "../../../particles/particular/engine/shader/debugAtlas.js";
|
|
@@ -1558,8 +1558,8 @@ function draw_camera_view_planes() {
|
|
|
1558
1558
|
});
|
|
1559
1559
|
}
|
|
1560
1560
|
|
|
1561
|
-
prepare_scene_2();
|
|
1562
|
-
|
|
1561
|
+
// prepare_scene_2();
|
|
1562
|
+
prepare_scene_decal_0();
|
|
1563
1563
|
// prepare_scene_decal_1();
|
|
1564
1564
|
// prepare_scene_decal_2();
|
|
1565
1565
|
// prepare_scene_9();
|
|
@@ -125,9 +125,9 @@ export function query_bvh_frustum_from_texture(
|
|
|
125
125
|
const light_descriptor = data_view.getUint32(node_address + 24);
|
|
126
126
|
|
|
127
127
|
const light_type = light_descriptor & 3;
|
|
128
|
-
const light_texel_index = light_descriptor >> 2;
|
|
129
128
|
|
|
130
|
-
|
|
129
|
+
// mask out type bits
|
|
130
|
+
const light_address = light_descriptor & 0xFFFFFFF8;
|
|
131
131
|
|
|
132
132
|
if (light_type === 0) {
|
|
133
133
|
// Point light
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const _scratch_array = [];
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* @param {number} a
|
|
6
|
+
* @param {number} b
|
|
7
|
+
* @param {number[]} lookup
|
|
8
|
+
* @param {number[]} data
|
|
9
|
+
* @returns {number}
|
|
10
|
+
*/
|
|
11
|
+
function compare_decals(a, b, lookup, data) {
|
|
12
|
+
if (a === b) {
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const a_encoded = lookup[a];
|
|
17
|
+
const b_encoded = lookup[b];
|
|
18
|
+
|
|
19
|
+
// mask out bottom 2 type bits
|
|
20
|
+
const a_address = a_encoded & 0xFFFFFFF8;
|
|
21
|
+
const b_address = b_encoded & 0xFFFFFFF8;
|
|
22
|
+
|
|
23
|
+
const a_priority = data[a_address + 20];
|
|
24
|
+
const b_priority = data[b_address + 20];
|
|
25
|
+
|
|
26
|
+
let diff = b_priority - a_priority;
|
|
27
|
+
|
|
28
|
+
if (diff !== 0) {
|
|
29
|
+
return diff;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
const a_id = data[a_address + 21];
|
|
34
|
+
const b_id = data[b_address + 21];
|
|
35
|
+
|
|
36
|
+
// data order is explicit and stable, we use that as a secondary comparison criteria
|
|
37
|
+
return b_id - a_id;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Decals need to be drawn in a specific order to achieve correct blending, for this they need to be sorted
|
|
42
|
+
* @param {number[]|Uint8ClampedArray|Uint8Array|Uint32Array} lookup
|
|
43
|
+
* @param {number[]|Float32Array} data
|
|
44
|
+
* @param {number} offset
|
|
45
|
+
* @param {number} count
|
|
46
|
+
*/
|
|
47
|
+
export function sort_decal_data(lookup, data, offset, count) {
|
|
48
|
+
const stack = _scratch_array;
|
|
49
|
+
|
|
50
|
+
stack[0] = offset;
|
|
51
|
+
stack[1] = offset + count - 1;
|
|
52
|
+
|
|
53
|
+
let stackPointer = 2;
|
|
54
|
+
let i, j;
|
|
55
|
+
|
|
56
|
+
while (stackPointer > 0) {
|
|
57
|
+
stackPointer -= 2;
|
|
58
|
+
|
|
59
|
+
const right = stack[stackPointer + 1];
|
|
60
|
+
const left = stack[stackPointer];
|
|
61
|
+
|
|
62
|
+
i = left;
|
|
63
|
+
j = right;
|
|
64
|
+
|
|
65
|
+
const pivotIndex = (left + right) >> 1;
|
|
66
|
+
|
|
67
|
+
/* partition */
|
|
68
|
+
while (i <= j) {
|
|
69
|
+
|
|
70
|
+
while (compare_decals(i, pivotIndex, lookup, data) > 0) {
|
|
71
|
+
i++;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
while (compare_decals(j, pivotIndex, lookup, data) < 0) {
|
|
75
|
+
j--;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (i <= j) {
|
|
79
|
+
|
|
80
|
+
if (i !== j) {
|
|
81
|
+
//do swap
|
|
82
|
+
const swap = lookup[i];
|
|
83
|
+
lookup[i] = lookup[j]
|
|
84
|
+
lookup[j] = swap;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
i++;
|
|
88
|
+
j--;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* recursion */
|
|
93
|
+
if (left < j) {
|
|
94
|
+
stack[stackPointer++] = left;
|
|
95
|
+
stack[stackPointer++] = j;
|
|
96
|
+
}
|
|
97
|
+
if (i < right) {
|
|
98
|
+
stack[stackPointer++] = i;
|
|
99
|
+
stack[stackPointer++] = right;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -27,7 +27,7 @@ import Task from "../../../../core/process/task/Task.js";
|
|
|
27
27
|
import { TaskSignal } from "../../../../core/process/task/TaskSignal.js";
|
|
28
28
|
import Quaternion from "../../../../core/geom/Quaternion.js";
|
|
29
29
|
import TaskProgressView from "../../../../view/task/TaskProgressView.js";
|
|
30
|
-
import { Localization } from "../../../../core/Localization.js";
|
|
30
|
+
import { Localization } from "../../../../core/localization/Localization.js";
|
|
31
31
|
|
|
32
32
|
import '../../../../../../../css/game.scss';
|
|
33
33
|
import { MouseEvents } from "../../../input/devices/events/MouseEvents.js";
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"productName": "Meep",
|
|
6
6
|
"description": "production-ready JavaScript game engine based on Entity Component System Architecture",
|
|
7
7
|
"author": "Alexander Goldring",
|
|
8
|
-
"version": "2.44.
|
|
8
|
+
"version": "2.44.8",
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"gl-matrix": "3.4.3",
|
|
11
11
|
"fast-levenshtein": "2.0.6",
|
package/core/Localization.js
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import levenshtein from "fast-levenshtein";
|
|
2
|
-
import { GameAssetType } from "../engine/asset/GameAssetType.js";
|
|
3
|
-
import { parseTooltipString } from "../view/tooltip/gml/parser/parseTooltipString.js";
|
|
4
|
-
import { assert } from "./assert.js";
|
|
5
|
-
import ObservedString from "./model/ObservedString.js";
|
|
6
|
-
import { seedVariablesIntoTemplateString } from "./parser/seedVariablesIntoTemplateString.js";
|
|
7
|
-
import { STRING_TEMPLATE_VARIABLE_REGEX } from "./parser/STRING_TEMPLATE_VARIABLE_REGEX.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Validation utility method
|
|
11
|
-
* @param {string} template
|
|
12
|
-
* @return {string}
|
|
13
|
-
*/
|
|
14
|
-
function validationMockSeed(template) {
|
|
15
|
-
|
|
16
|
-
const result = template.replace(STRING_TEMPLATE_VARIABLE_REGEX, function (match, varName) {
|
|
17
|
-
return "0";
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
return result;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const FAILURE_MESSAGE_CACHE = {};
|
|
24
|
-
|
|
25
|
-
export class Localization {
|
|
26
|
-
constructor() {
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
29
|
-
* @type {AssetManager|null}
|
|
30
|
-
*/
|
|
31
|
-
this.assetManager = null;
|
|
32
|
-
|
|
33
|
-
this.json = {};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Measured in characters per second
|
|
37
|
-
* @type {number}
|
|
38
|
-
*/
|
|
39
|
-
this.reading_speed = 10;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
*
|
|
43
|
-
* @type {ObservedString}
|
|
44
|
-
*/
|
|
45
|
-
this.locale = new ObservedString('');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
*
|
|
50
|
-
* @param {string} text
|
|
51
|
-
* @returns {number}
|
|
52
|
-
*/
|
|
53
|
-
computeReadingTime(text) {
|
|
54
|
-
assert.typeOf(text, 'string', 'text');
|
|
55
|
-
|
|
56
|
-
return text.length / this.reading_speed;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
*
|
|
61
|
-
* @param {AssetManager} am
|
|
62
|
-
*/
|
|
63
|
-
setAssetManager(am) {
|
|
64
|
-
this.assetManager = am;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* @returns {boolean}
|
|
69
|
-
* @param {function(key:string, error:*, original: string)} errorConsumer
|
|
70
|
-
*/
|
|
71
|
-
validate(errorConsumer) {
|
|
72
|
-
let result = true;
|
|
73
|
-
|
|
74
|
-
for (let key in this.json) {
|
|
75
|
-
const value = this.json[key];
|
|
76
|
-
|
|
77
|
-
const seededValue = validationMockSeed(value);
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
parseTooltipString(seededValue);
|
|
81
|
-
} catch (e) {
|
|
82
|
-
result = false;
|
|
83
|
-
errorConsumer(key, e, value);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return result;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
*
|
|
92
|
-
* @param {string} locale
|
|
93
|
-
* @returns {Promise}
|
|
94
|
-
*/
|
|
95
|
-
loadLocale(locale) {
|
|
96
|
-
const am = this.assetManager;
|
|
97
|
-
|
|
98
|
-
const pLoadData = am.promise(`data/database/text/${locale}.json`, GameAssetType.JSON)
|
|
99
|
-
.then(asset => {
|
|
100
|
-
const json = asset.create();
|
|
101
|
-
|
|
102
|
-
this.json = json;
|
|
103
|
-
|
|
104
|
-
this.locale.set(locale);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const pLoadMetadata = am.promise(`data/database/text/languages.json`, GameAssetType.JSON)
|
|
108
|
-
.then(asset => {
|
|
109
|
-
const languages_metadata = asset.create();
|
|
110
|
-
|
|
111
|
-
const { reading_speed = 10 } = languages_metadata[locale];
|
|
112
|
-
|
|
113
|
-
assert.isNumber(reading_speed, 'reading_speed');
|
|
114
|
-
assert.greaterThan(reading_speed, 0, 'reading_speed');
|
|
115
|
-
|
|
116
|
-
this.reading_speed = reading_speed;
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
return Promise.all([pLoadData, pLoadMetadata]);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
*
|
|
124
|
-
* @param {String} id
|
|
125
|
-
* @return {boolean}
|
|
126
|
-
*/
|
|
127
|
-
hasString(id) {
|
|
128
|
-
return this.json[id] !== undefined;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
*
|
|
133
|
-
* @param {number} value
|
|
134
|
-
*/
|
|
135
|
-
formatIntegerByThousands(value) {
|
|
136
|
-
const formatter = new Intl.NumberFormat(this.locale.getValue(), { useGrouping: true });
|
|
137
|
-
|
|
138
|
-
return formatter.format(value);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
*
|
|
143
|
-
* @param {string} id
|
|
144
|
-
* @param {object} [seed]
|
|
145
|
-
*
|
|
146
|
-
* @returns {string}
|
|
147
|
-
*/
|
|
148
|
-
getString(id, seed = {}) {
|
|
149
|
-
assert.typeOf(id, 'string', 'id');
|
|
150
|
-
|
|
151
|
-
const value = this.json[id];
|
|
152
|
-
|
|
153
|
-
if (value === undefined) {
|
|
154
|
-
|
|
155
|
-
if (!ENV_PRODUCTION) {
|
|
156
|
-
const locale = this.locale.getValue();
|
|
157
|
-
|
|
158
|
-
if (FAILURE_MESSAGE_CACHE[locale] === undefined) {
|
|
159
|
-
FAILURE_MESSAGE_CACHE[locale] = {};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (FAILURE_MESSAGE_CACHE[locale][id] === undefined) {
|
|
163
|
-
|
|
164
|
-
//try to find similar keys
|
|
165
|
-
const similarities = Object.keys(this.json).map(function (key) {
|
|
166
|
-
const distance = levenshtein.get(key, id);
|
|
167
|
-
return {
|
|
168
|
-
key,
|
|
169
|
-
distance
|
|
170
|
-
};
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
similarities.sort(function (a, b) {
|
|
174
|
-
return a.distance - b.distance;
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
const suggestions = similarities.slice(0, 3).map(p => p.key);
|
|
178
|
-
|
|
179
|
-
const message = `No localization value for id='${id}', seed=${JSON.stringify(seed)}, approximate matches: ${suggestions.join(', ')}`;
|
|
180
|
-
|
|
181
|
-
FAILURE_MESSAGE_CACHE[locale][id] = message;
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
console.warn(FAILURE_MESSAGE_CACHE[locale][id]);
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
//no value
|
|
190
|
-
return `@${id}`;
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
//value needs to be seeded
|
|
195
|
-
const seededValue = seedVariablesIntoTemplateString(value, seed);
|
|
196
|
-
|
|
197
|
-
return seededValue;
|
|
198
|
-
}
|
|
199
|
-
}
|