@call-me-sensei/toonlab 0.4.21 → 0.4.22
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/NPM-LIBRARY.md +51 -2
- package/README.md +4 -3
- package/agents/PROMPTS.md +1 -1
- package/database/apply-one-catalog-seed.mjs +63 -0
- package/database/seeds/catalog/0005_2026-08-c8-first12.sql +1101 -0
- package/database/seeds/catalog/0006_2026-09-c8-first100.sql +14245 -0
- package/database/seeds/catalog/0007_2026-09-c8-first100-primary-model.sql +14845 -0
- package/package.json +4 -2
- package/src/catalog/officialCatalog.js +1 -0
- package/src/catalog/officialCatalogAssetRuntime.js +98 -1
- package/src/catalog/officialCatalogLod.js +160 -16
- package/src/catalog/officialCatalogPlacement.js +52 -12
- package/src/catalog/officialCatalogProvider.js +79 -8
- package/src/catalog/officialCatalogRockPackage.js +172 -0
- package/src/rock-shader/rockMaterial.js +205 -28
- package/src/rock-shader/rockShaderRuntime.js +17 -1
- package/src/rock-shader/rockShaderSettings.js +16 -0
- package/src/rockgen/lod/index.js +1 -0
- package/src/rockgen/lod/rockDenseFieldPolicy.js +143 -0
- package/src/rockgen/rockDocument.js +604 -26
- package/src/version.js +1 -1
- package/types/catalog/officialCatalog.d.ts +1 -0
- package/types/catalog/officialCatalogPlacement.d.ts +4 -0
- package/types/catalog/officialCatalogRockPackage.d.ts +68 -0
- package/types/index.d.ts +24 -0
- package/types/rock-shader/rockMaterial.d.ts +3 -0
- package/types/rock-shader/rockShaderSettings.d.ts +2 -0
- package/types/rockgen/lod/index.d.ts +1 -0
- package/types/rockgen/lod/rockDenseFieldPolicy.d.ts +181 -0
- package/types/rockgen/rockDocument.d.ts +579 -68
- package/types/version.d.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@call-me-sensei/toonlab",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.22",
|
|
4
4
|
"description": "Anime-style game toolkit for Three.js with cel shading, vegetation, rocks, water, sky, clouds, procedural textures, and portable Lab artifacts",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hyperbond Studio PTE. LTD. (Call Me Sensei)",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"src/catalog/officialCatalog.js",
|
|
62
62
|
"src/catalog/officialCatalogLod.js",
|
|
63
63
|
"src/catalog/officialCatalogPlacement.js",
|
|
64
|
+
"src/catalog/officialCatalogRockPackage.js",
|
|
64
65
|
"src/core",
|
|
65
66
|
"src/environment",
|
|
66
67
|
"src/ground-shader",
|
|
@@ -278,6 +279,7 @@
|
|
|
278
279
|
"golden:capture": "node scripts/capture-golden-fixtures.mjs",
|
|
279
280
|
"golden:compare": "node scripts/compare-golden-captures.mjs",
|
|
280
281
|
"verify:rockgen": "node scripts/verify-rockgen.mjs",
|
|
282
|
+
"verify:rock-dense-field-lod": "node scripts/verify-rock-dense-field-lod.mjs && node scripts/verify-official-catalog-lod.mjs",
|
|
281
283
|
"verify:rock-generation-lab": "node scripts/verify-rock-generation-lab.mjs",
|
|
282
284
|
"verify:rock-c7-library": "node scripts/verify-rock-c7-surface-library.mjs",
|
|
283
285
|
"verify:rock-lab-lifecycle": "node scripts/verify-rock-lab-lifecycle.mjs",
|
|
@@ -362,7 +364,7 @@
|
|
|
362
364
|
"verify:style-contract": "node scripts/verify-style-contract.mjs",
|
|
363
365
|
"verify:style-labels-package": "node scripts/verify-packaged-style-labels.mjs",
|
|
364
366
|
"verify:collision-package": "node scripts/verify-packaged-world-collision.mjs",
|
|
365
|
-
"verify:official-catalog": "node scripts/verify-official-catalog-provider.mjs && node scripts/verify-official-catalog-runtime.mjs && node scripts/verify-official-catalog-lod.mjs && node scripts/verify-official-catalog-placement.mjs && node scripts/verify-playground-official-catalog.mjs && node scripts/verify-packaged-official-catalog.mjs",
|
|
367
|
+
"verify:official-catalog": "node scripts/verify-official-catalog-provider.mjs && node scripts/verify-official-catalog-rock-package.mjs && node scripts/verify-official-catalog-runtime.mjs && node scripts/verify-official-catalog-lod.mjs && node scripts/verify-official-catalog-placement.mjs && node scripts/verify-playground-official-catalog.mjs && node scripts/verify-packaged-official-catalog.mjs",
|
|
366
368
|
"verify:system-style": "node scripts/verify-system-style-policy.mjs",
|
|
367
369
|
"verify:release": "npm run verify:system-style && npm run verify:style-contract && npm run verify:style-labels-package && npm run verify:collision-package && npm run verify:scene-collision && npm run verify:official-catalog && npm run verify:public-labs && npm run verify:live-labs && npm run verify:rock-shader && npm run verify:rock-generation-lab && npm run verify:rock-lab-lifecycle && npm run verify:ground-shader && npm run verify:manufactured-materials && npm run verify:material-labeling:agent && npm run verify:material-labeling:mcp && npm run verify:water-lab && npm run verify:water && npm run verify:vegetation-labs && npm run verify:texgen && npm run verify:cloud-shader && npm run verify:sky-cloud-lab && npm run verify:world-quality && npm run verify:scatter && npm run verify:branch-tree && npm run verify:grass-package && npm run verify:character-package && npm run verify:locomotion-state && npm run verify:ground-stabilizer && npm run verify:water-interaction && npm run verify:walkable-character && npm run verify:playground-walkable && npm run verify:renderer-configuration && npm run verify:scene-scheduler && npm run verify:scene-surface && npm run verify:shared-shadow-receivers && npm run verify:scene-quality && npm run verify:quality-integration && npm run verify:lab-asset-provenance && npm run verify:api && npm run verify:types && npm run verify:mcp && npm run verify:skills && npm run verify:docs && npm run verify:local-database && npm run verify:package",
|
|
368
370
|
"verify:generation": "node scripts/verify-generation.mjs",
|
|
@@ -14,6 +14,10 @@ import {
|
|
|
14
14
|
rockSemanticMaterialTextures,
|
|
15
15
|
} from '../rock-shader/rockSemanticMaterialRuntime.js';
|
|
16
16
|
import { OFFICIAL_CATALOG_ASSET_VERSION } from './officialCatalogProvider.js';
|
|
17
|
+
import {
|
|
18
|
+
loadOfficialCatalogRockPackage,
|
|
19
|
+
loadOfficialCatalogRockShaderInput,
|
|
20
|
+
} from './officialCatalogRockPackage.js';
|
|
17
21
|
|
|
18
22
|
const rendererPools = new WeakMap();
|
|
19
23
|
const headlessPools = new Map();
|
|
@@ -141,18 +145,25 @@ async function defaultPrepareTextures(root, asset) {
|
|
|
141
145
|
export function createOfficialCatalogAssetRuntime({
|
|
142
146
|
decoderBasePath = '/',
|
|
143
147
|
loadModel = loadModelAsset,
|
|
148
|
+
loadRockPackage = loadOfficialCatalogRockPackage,
|
|
149
|
+
loadRockShaderInput = loadOfficialCatalogRockShaderInput,
|
|
144
150
|
prepareTextures = defaultPrepareTextures,
|
|
145
151
|
provider,
|
|
146
152
|
renderer = null,
|
|
153
|
+
rockTextureLoader = null,
|
|
147
154
|
transcodersFactory = createModelAssetTranscoders,
|
|
148
155
|
} = {}) {
|
|
149
156
|
if (!provider || typeof provider.getAsset !== 'function') {
|
|
150
157
|
throw new TypeError('Official catalog asset runtime requires a provider with getAsset().');
|
|
151
158
|
}
|
|
152
159
|
if (typeof loadModel !== 'function') throw new TypeError('loadModel must be a function.');
|
|
160
|
+
if (typeof loadRockPackage !== 'function') throw new TypeError('loadRockPackage must be a function.');
|
|
161
|
+
if (typeof loadRockShaderInput !== 'function') throw new TypeError('loadRockShaderInput must be a function.');
|
|
153
162
|
if (typeof prepareTextures !== 'function') throw new TypeError('prepareTextures must be a function.');
|
|
154
163
|
|
|
155
164
|
const assetPromises = new Map();
|
|
165
|
+
const rockPackagePromises = new Map();
|
|
166
|
+
const rockShaderInputPromises = new Map();
|
|
156
167
|
const sourcePromises = new Map();
|
|
157
168
|
const sourceEntries = new Map();
|
|
158
169
|
const handles = new Set();
|
|
@@ -190,6 +201,53 @@ export function createOfficialCatalogAssetRuntime({
|
|
|
190
201
|
return assetPromises.get(id);
|
|
191
202
|
}
|
|
192
203
|
|
|
204
|
+
async function getAsset(assetOrId) {
|
|
205
|
+
assertActive();
|
|
206
|
+
return resolveAsset(assetOrId);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function getRockPackage(assetOrId, options = {}) {
|
|
210
|
+
assertActive();
|
|
211
|
+
const asset = await resolveAsset(assetOrId);
|
|
212
|
+
assertActive();
|
|
213
|
+
const includeManifest = options.includeManifest === true;
|
|
214
|
+
const includeNatureProvenance = options.includeNatureProvenance === true;
|
|
215
|
+
const key = `${asset.identity}|manifest:${includeManifest}|nature:${includeNatureProvenance}`;
|
|
216
|
+
if (!rockPackagePromises.has(key)) {
|
|
217
|
+
const promise = Promise.resolve(loadRockPackage(asset, options)).catch((error) => {
|
|
218
|
+
if (rockPackagePromises.get(key) === promise) rockPackagePromises.delete(key);
|
|
219
|
+
throw error;
|
|
220
|
+
});
|
|
221
|
+
rockPackagePromises.set(key, promise);
|
|
222
|
+
}
|
|
223
|
+
return rockPackagePromises.get(key);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function getRockShaderInput(assetOrId) {
|
|
227
|
+
assertActive();
|
|
228
|
+
const asset = await resolveAsset(assetOrId);
|
|
229
|
+
assertActive();
|
|
230
|
+
if (!asset.packageFiles?.materialConfigUrl) return null;
|
|
231
|
+
const key = asset.identity;
|
|
232
|
+
if (!rockShaderInputPromises.has(key)) {
|
|
233
|
+
const promise = (async () => {
|
|
234
|
+
const rockPackage = await getRockPackage(asset);
|
|
235
|
+
const options = rockTextureLoader ? { textureLoader: rockTextureLoader } : {};
|
|
236
|
+
const input = await loadRockShaderInput(rockPackage, options);
|
|
237
|
+
if (disposed) {
|
|
238
|
+
Object.values(input?.textures ?? {}).forEach((texture) => texture.dispose?.());
|
|
239
|
+
throw new Error('Official catalog asset runtime was disposed while loading rock textures.');
|
|
240
|
+
}
|
|
241
|
+
return input;
|
|
242
|
+
})().catch((error) => {
|
|
243
|
+
if (rockShaderInputPromises.get(key) === promise) rockShaderInputPromises.delete(key);
|
|
244
|
+
throw error;
|
|
245
|
+
});
|
|
246
|
+
rockShaderInputPromises.set(key, promise);
|
|
247
|
+
}
|
|
248
|
+
return rockShaderInputPromises.get(key);
|
|
249
|
+
}
|
|
250
|
+
|
|
193
251
|
async function sourceFor(asset) {
|
|
194
252
|
const key = asset.identity;
|
|
195
253
|
if (!sourcePromises.has(key)) {
|
|
@@ -273,7 +331,26 @@ export function createOfficialCatalogAssetRuntime({
|
|
|
273
331
|
|
|
274
332
|
function evict(assetOrId, { force = false } = {}) {
|
|
275
333
|
const entry = findEntry(assetOrId);
|
|
276
|
-
|
|
334
|
+
const identity = entry?.asset.identity ?? String(assetOrId?.identity ?? '');
|
|
335
|
+
const id = entry?.asset.id ?? String(assetOrId?.id ?? assetOrId ?? '');
|
|
336
|
+
const packageKeys = [...rockPackagePromises.keys()].filter((key) => (
|
|
337
|
+
(identity && key.startsWith(`${identity}|`)) || (!identity && key.includes(`:${id}@`))
|
|
338
|
+
));
|
|
339
|
+
const shaderKeys = [...rockShaderInputPromises.keys()].filter((key) => (
|
|
340
|
+
key === identity || (!identity && key.includes(`:${id}@`))
|
|
341
|
+
));
|
|
342
|
+
if (!entry && packageKeys.length === 0 && shaderKeys.length === 0) return false;
|
|
343
|
+
if (!entry) {
|
|
344
|
+
packageKeys.forEach((key) => rockPackagePromises.delete(key));
|
|
345
|
+
for (const key of shaderKeys) {
|
|
346
|
+
rockShaderInputPromises.get(key)?.then((input) => {
|
|
347
|
+
Object.values(input?.textures ?? {}).forEach((texture) => texture.dispose?.());
|
|
348
|
+
}).catch(() => {});
|
|
349
|
+
rockShaderInputPromises.delete(key);
|
|
350
|
+
}
|
|
351
|
+
assetPromises.delete(id);
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
277
354
|
if (entry.refs > 0 && !force) return false;
|
|
278
355
|
if (force) {
|
|
279
356
|
for (const handle of [...handles]) {
|
|
@@ -284,6 +361,13 @@ export function createOfficialCatalogAssetRuntime({
|
|
|
284
361
|
sourceEntries.delete(entry.asset.identity);
|
|
285
362
|
sourcePromises.delete(entry.asset.identity);
|
|
286
363
|
assetPromises.delete(entry.asset.id);
|
|
364
|
+
packageKeys.forEach((key) => rockPackagePromises.delete(key));
|
|
365
|
+
for (const key of shaderKeys) {
|
|
366
|
+
rockShaderInputPromises.get(key)?.then((input) => {
|
|
367
|
+
Object.values(input?.textures ?? {}).forEach((texture) => texture.dispose?.());
|
|
368
|
+
}).catch(() => {});
|
|
369
|
+
rockShaderInputPromises.delete(key);
|
|
370
|
+
}
|
|
287
371
|
return true;
|
|
288
372
|
}
|
|
289
373
|
|
|
@@ -297,6 +381,8 @@ export function createOfficialCatalogAssetRuntime({
|
|
|
297
381
|
refs: entry.refs,
|
|
298
382
|
}))),
|
|
299
383
|
pendingAssets: sourcePromises.size - sourceEntries.size,
|
|
384
|
+
pendingRockPackages: rockPackagePromises.size,
|
|
385
|
+
cachedRockShaderInputs: rockShaderInputPromises.size,
|
|
300
386
|
});
|
|
301
387
|
}
|
|
302
388
|
|
|
@@ -305,10 +391,18 @@ export function createOfficialCatalogAssetRuntime({
|
|
|
305
391
|
disposed = true;
|
|
306
392
|
for (const handle of [...handles]) handle.release();
|
|
307
393
|
await Promise.allSettled(sourcePromises.values());
|
|
394
|
+
const shaderInputs = await Promise.allSettled(rockShaderInputPromises.values());
|
|
395
|
+
for (const result of shaderInputs) {
|
|
396
|
+
if (result.status === 'fulfilled') {
|
|
397
|
+
Object.values(result.value?.textures ?? {}).forEach((texture) => texture.dispose?.());
|
|
398
|
+
}
|
|
399
|
+
}
|
|
308
400
|
for (const entry of sourceEntries.values()) disposeResources(entry.resources);
|
|
309
401
|
sourceEntries.clear();
|
|
310
402
|
sourcePromises.clear();
|
|
311
403
|
assetPromises.clear();
|
|
404
|
+
rockPackagePromises.clear();
|
|
405
|
+
rockShaderInputPromises.clear();
|
|
312
406
|
transcoderLease?.release();
|
|
313
407
|
transcoderLease = null;
|
|
314
408
|
}
|
|
@@ -317,6 +411,9 @@ export function createOfficialCatalogAssetRuntime({
|
|
|
317
411
|
acquireAsset,
|
|
318
412
|
dispose,
|
|
319
413
|
evict,
|
|
414
|
+
getAsset,
|
|
415
|
+
getRockPackage,
|
|
416
|
+
getRockShaderInput,
|
|
320
417
|
get disposed() { return disposed; },
|
|
321
418
|
provider,
|
|
322
419
|
stats,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Vector3 } from 'three';
|
|
1
|
+
import { Box3, Sphere, Vector3 } from 'three';
|
|
2
2
|
|
|
3
3
|
const DEFAULT_LOD_NAME_PATTERN = /(?:^|[_\s.-])LOD[_\s.-]?(\d+)(?:$|[_\s.-])/iu;
|
|
4
4
|
|
|
@@ -97,16 +97,118 @@ export function selectCatalogLodLevel({
|
|
|
97
97
|
return selected;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function normalizePixelThresholds(thresholds, levelCount) {
|
|
101
|
+
const count = Math.max(Math.round(Number(levelCount) || 1), 1);
|
|
102
|
+
const source = Array.isArray(thresholds) ? thresholds : [];
|
|
103
|
+
const normalized = [];
|
|
104
|
+
for (let level = 0; level < count; level += 1) {
|
|
105
|
+
const fallback = level === 0 ? 240 : Math.max((normalized[level - 1] ?? 240) * 0.45, 1);
|
|
106
|
+
const value = Number(source[level]);
|
|
107
|
+
normalized.push(Number.isFinite(value) && value > 0 ? value : fallback);
|
|
108
|
+
}
|
|
109
|
+
for (let level = 1; level < normalized.length; level += 1) {
|
|
110
|
+
normalized[level] = Math.min(normalized[level], normalized[level - 1] - 0.001);
|
|
111
|
+
}
|
|
112
|
+
return Object.freeze(normalized);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resolve an authored LOD from its projected on-screen diameter. A null result
|
|
117
|
+
* is an intentional cull decision, not a missing-asset failure.
|
|
118
|
+
*/
|
|
119
|
+
export function selectCatalogLodLevelByProjectedPixels({
|
|
120
|
+
availableLevels,
|
|
121
|
+
cullBelowPixels = 0,
|
|
122
|
+
currentLevel = null,
|
|
123
|
+
hysteresis = 0,
|
|
124
|
+
pixelThresholds,
|
|
125
|
+
projectedPixels,
|
|
126
|
+
} = {}) {
|
|
127
|
+
const available = [...new Set((availableLevels ?? []).map(finiteLevel).filter((v) => v !== null))]
|
|
128
|
+
.sort((left, right) => left - right);
|
|
129
|
+
if (available.length === 0) return null;
|
|
130
|
+
const pixels = Number(projectedPixels);
|
|
131
|
+
if (!Number.isFinite(pixels) || pixels < 0) {
|
|
132
|
+
throw new RangeError('Projected LOD pixels must be a finite non-negative number.');
|
|
133
|
+
}
|
|
134
|
+
const margin = Number(hysteresis);
|
|
135
|
+
if (!Number.isFinite(margin) || margin < 0 || margin >= 0.5) {
|
|
136
|
+
throw new RangeError('Catalog LOD hysteresis must be a finite ratio in [0, 0.5).');
|
|
137
|
+
}
|
|
138
|
+
const cull = Math.max(Number(cullBelowPixels) || 0, 0);
|
|
139
|
+
const thresholds = normalizePixelThresholds(pixelThresholds, available.at(-1) + 1);
|
|
140
|
+
const current = finiteLevel(currentLevel);
|
|
141
|
+
|
|
142
|
+
if (pixels < cull) {
|
|
143
|
+
if (current !== null && available.includes(current) && margin > 0
|
|
144
|
+
&& pixels >= cull * (1 - margin)) return current;
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let requested = thresholds.length - 1;
|
|
149
|
+
for (let level = 0; level < thresholds.length; level += 1) {
|
|
150
|
+
if (pixels >= thresholds[level]) {
|
|
151
|
+
requested = level;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const selected = available.filter((level) => level <= requested).at(-1) ?? available[0];
|
|
156
|
+
if (current === null || !available.includes(current) || margin === 0 || selected === current) {
|
|
157
|
+
return selected;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (selected > current) {
|
|
161
|
+
const boundary = thresholds[current] ?? cull;
|
|
162
|
+
if (pixels >= boundary * (1 - margin)) return current;
|
|
163
|
+
} else {
|
|
164
|
+
const boundary = thresholds[selected] ?? thresholds[0];
|
|
165
|
+
if (pixels <= boundary * (1 + margin)) return current;
|
|
166
|
+
}
|
|
167
|
+
return selected;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Estimate projected object diameter for perspective and orthographic cameras. */
|
|
171
|
+
export function estimateCatalogProjectedPixels({
|
|
172
|
+
camera,
|
|
173
|
+
distance,
|
|
174
|
+
referenceDiameter,
|
|
175
|
+
viewportHeight,
|
|
176
|
+
} = {}) {
|
|
177
|
+
const pixelsHigh = Number(viewportHeight);
|
|
178
|
+
const diameter = Number(referenceDiameter);
|
|
179
|
+
if (!camera?.isCamera) throw new TypeError('Projected LOD estimation requires a Three.js camera.');
|
|
180
|
+
if (!Number.isFinite(pixelsHigh) || pixelsHigh <= 0) {
|
|
181
|
+
throw new RangeError('Projected LOD estimation requires a positive viewportHeight.');
|
|
182
|
+
}
|
|
183
|
+
if (!Number.isFinite(diameter) || diameter <= 0) {
|
|
184
|
+
throw new RangeError('Projected LOD estimation requires a positive referenceDiameter.');
|
|
185
|
+
}
|
|
186
|
+
if (camera.isOrthographicCamera) {
|
|
187
|
+
const span = Math.abs(Number(camera.top) - Number(camera.bottom)) / Math.max(Number(camera.zoom) || 1, 0.001);
|
|
188
|
+
return span > 0 ? diameter / span * pixelsHigh : Number.POSITIVE_INFINITY;
|
|
189
|
+
}
|
|
190
|
+
if (!camera.isPerspectiveCamera) {
|
|
191
|
+
throw new TypeError('Projected LOD estimation supports perspective and orthographic cameras.');
|
|
192
|
+
}
|
|
193
|
+
const range = Math.max(Number(distance) || 0, 0.0001);
|
|
194
|
+
const zoom = Math.max(Number(camera.zoom) || 1, 0.001);
|
|
195
|
+
const halfFov = Math.atan(Math.tan((Number(camera.fov) || 50) * Math.PI / 360) / zoom);
|
|
196
|
+
return diameter / (2 * range * Math.tan(halfFov)) * pixelsHigh;
|
|
197
|
+
}
|
|
198
|
+
|
|
100
199
|
/**
|
|
101
200
|
* Authored, renderer-independent LOD controller. The host calls update() with
|
|
102
201
|
* either a direct distance or a camera. World scale is removed from camera
|
|
103
202
|
* distance so a uniformly enlarged landmark changes LOD at proportional range.
|
|
104
203
|
*/
|
|
105
204
|
export function createCatalogLodRuntime(root, {
|
|
205
|
+
cullBelowPixels = 0,
|
|
106
206
|
distances = [0, 45, 120],
|
|
107
207
|
hysteresis = 0,
|
|
108
208
|
maxLevel = Number.POSITIVE_INFINITY,
|
|
109
209
|
namePattern = DEFAULT_LOD_NAME_PATTERN,
|
|
210
|
+
pixelThresholds = null,
|
|
211
|
+
referenceDiameter = null,
|
|
110
212
|
} = {}) {
|
|
111
213
|
const bindings = collectCatalogLodBindings(root, { namePattern });
|
|
112
214
|
const availableLevels = Object.freeze(
|
|
@@ -119,27 +221,41 @@ export function createCatalogLodRuntime(root, {
|
|
|
119
221
|
}
|
|
120
222
|
const worldPosition = new Vector3();
|
|
121
223
|
const worldScale = new Vector3();
|
|
224
|
+
const bounds = new Box3();
|
|
225
|
+
const sphere = new Sphere();
|
|
226
|
+
const usesScreenSpace = Array.isArray(pixelThresholds);
|
|
227
|
+
let localReferenceDiameter = Number(referenceDiameter);
|
|
228
|
+
if (!Number.isFinite(localReferenceDiameter) || localReferenceDiameter <= 0) {
|
|
229
|
+
root.updateWorldMatrix?.(true, true);
|
|
230
|
+
bounds.setFromObject(root, true).getBoundingSphere(sphere);
|
|
231
|
+
root.getWorldScale(worldScale);
|
|
232
|
+
const scale = Math.max(Math.abs(worldScale.x), Math.abs(worldScale.y), Math.abs(worldScale.z), 0.001);
|
|
233
|
+
localReferenceDiameter = Math.max(sphere.radius * 2 / scale, 0.001);
|
|
234
|
+
}
|
|
235
|
+
const normalizedPixelThresholds = usesScreenSpace
|
|
236
|
+
? normalizePixelThresholds(pixelThresholds, availableLevels.at(-1) + 1)
|
|
237
|
+
: null;
|
|
122
238
|
let currentLevel = null;
|
|
123
239
|
let disposed = false;
|
|
124
240
|
|
|
125
241
|
function setLevel(level) {
|
|
126
|
-
if (disposed || level
|
|
242
|
+
if (disposed || (level !== null && !availableLevels.includes(level))) return false;
|
|
127
243
|
if (currentLevel === level) return false;
|
|
128
244
|
bindings.forEach((binding) => {
|
|
129
|
-
binding.mesh.visible = binding.level === level;
|
|
245
|
+
binding.mesh.visible = level !== null && binding.level === level;
|
|
130
246
|
});
|
|
131
247
|
currentLevel = level;
|
|
132
248
|
return true;
|
|
133
249
|
}
|
|
134
250
|
|
|
135
251
|
function resolveDistance({ camera = null, distance = null } = {}) {
|
|
252
|
+
root.updateWorldMatrix?.(true, false);
|
|
253
|
+
root.getWorldPosition(worldPosition);
|
|
254
|
+
root.getWorldScale(worldScale);
|
|
136
255
|
if (Number.isFinite(distance)) return Math.max(distance, 0);
|
|
137
256
|
if (!camera?.getWorldPosition && !camera?.position) {
|
|
138
257
|
throw new TypeError('Catalog LOD update requires distance or camera.');
|
|
139
258
|
}
|
|
140
|
-
root.updateWorldMatrix?.(true, false);
|
|
141
|
-
root.getWorldPosition(worldPosition);
|
|
142
|
-
root.getWorldScale(worldScale);
|
|
143
259
|
const cameraPosition = camera.getWorldPosition
|
|
144
260
|
? camera.getWorldPosition(new Vector3())
|
|
145
261
|
: camera.position;
|
|
@@ -149,17 +265,42 @@ export function createCatalogLodRuntime(root, {
|
|
|
149
265
|
|
|
150
266
|
function update(options = {}) {
|
|
151
267
|
if (disposed || availableLevels.length === 0) return null;
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
268
|
+
const hasDirectPixels = Number.isFinite(Number(options.projectedPixels));
|
|
269
|
+
const distance = hasDirectPixels && !Number.isFinite(options.distance) && !options.camera
|
|
270
|
+
? null
|
|
271
|
+
: resolveDistance(options);
|
|
272
|
+
const projectedPixels = hasDirectPixels
|
|
273
|
+
? Math.max(Number(options.projectedPixels), 0)
|
|
274
|
+
: usesScreenSpace
|
|
275
|
+
? estimateCatalogProjectedPixels({
|
|
276
|
+
camera: options.camera,
|
|
277
|
+
distance,
|
|
278
|
+
referenceDiameter: localReferenceDiameter * Math.max(
|
|
279
|
+
Math.abs(worldScale.x), Math.abs(worldScale.y), Math.abs(worldScale.z), 0.001,
|
|
280
|
+
),
|
|
281
|
+
viewportHeight: options.viewportHeight,
|
|
282
|
+
})
|
|
283
|
+
: null;
|
|
284
|
+
let level = usesScreenSpace
|
|
285
|
+
? selectCatalogLodLevelByProjectedPixels({
|
|
286
|
+
availableLevels,
|
|
287
|
+
cullBelowPixels,
|
|
288
|
+
currentLevel,
|
|
289
|
+
hysteresis: hysteresisRatio,
|
|
290
|
+
pixelThresholds: normalizedPixelThresholds,
|
|
291
|
+
projectedPixels,
|
|
292
|
+
})
|
|
293
|
+
: selectCatalogLodLevel({
|
|
294
|
+
availableLevels,
|
|
295
|
+
distance,
|
|
296
|
+
distances: thresholds,
|
|
297
|
+
currentLevel,
|
|
298
|
+
hysteresis: hysteresisRatio,
|
|
299
|
+
maxLevel,
|
|
300
|
+
});
|
|
301
|
+
if (level !== null) level = Math.min(level, Math.max(Number(maxLevel) || 0, 0));
|
|
161
302
|
const changed = setLevel(level);
|
|
162
|
-
return Object.freeze({ changed, distance, level });
|
|
303
|
+
return Object.freeze({ changed, culled: level === null, distance, level, projectedPixels });
|
|
163
304
|
}
|
|
164
305
|
|
|
165
306
|
if (availableLevels.length > 0) setLevel(availableLevels[0]);
|
|
@@ -176,6 +317,9 @@ export function createCatalogLodRuntime(root, {
|
|
|
176
317
|
get disposed() { return disposed; },
|
|
177
318
|
get level() { return currentLevel; },
|
|
178
319
|
setLevel,
|
|
320
|
+
cullBelowPixels: Math.max(Number(cullBelowPixels) || 0, 0),
|
|
321
|
+
pixelThresholds: normalizedPixelThresholds,
|
|
322
|
+
referenceDiameter: localReferenceDiameter,
|
|
179
323
|
thresholds,
|
|
180
324
|
hysteresis: hysteresisRatio,
|
|
181
325
|
update,
|
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
resolveCatalogQualityOptions,
|
|
21
21
|
resolveSceneQualityProfile,
|
|
22
22
|
} from '../styles/sceneQualityProfiles.js';
|
|
23
|
-
import {
|
|
23
|
+
import { createDenseFieldRockLodRuntime } from '../rockgen/lod/rockDenseFieldPolicy.js';
|
|
24
|
+
import { collectCatalogLodBindings, createCatalogLodRuntime } from './officialCatalogLod.js';
|
|
24
25
|
|
|
25
26
|
function vector3(value, fallback = [0, 0, 0]) {
|
|
26
27
|
if (Array.isArray(value)) {
|
|
@@ -39,6 +40,17 @@ function rotation3(value) {
|
|
|
39
40
|
return [0, Number(value) || 0, 0];
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
function mergeSettings(base, patch) {
|
|
44
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) return base;
|
|
45
|
+
const output = { ...(base ?? {}) };
|
|
46
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
47
|
+
output[key] = value && typeof value === 'object' && !Array.isArray(value)
|
|
48
|
+
? mergeSettings(output[key], value)
|
|
49
|
+
: value;
|
|
50
|
+
}
|
|
51
|
+
return output;
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
/**
|
|
43
55
|
* Validates caller-supplied style textures without reclassifying the asset's
|
|
44
56
|
* own UV-space maps as triplanar detail maps. applyRockShader reads the full
|
|
@@ -54,13 +66,13 @@ function catalogRockTextures(overrides = {}) {
|
|
|
54
66
|
};
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
function styleAdapterFor(asset, root, { textures: overrides, variation }) {
|
|
69
|
+
function styleAdapterFor(asset, root, { settings: assetSettings, textures: overrides, variation }) {
|
|
58
70
|
if (asset.domain !== 'natural.rock') return null;
|
|
59
71
|
const resolved = catalogRockTextures(overrides);
|
|
60
72
|
const report = { rejectedTextures: resolved.rejected };
|
|
61
73
|
return Object.freeze({
|
|
62
74
|
apply(subject, settings) {
|
|
63
|
-
const result = applyRockShader(subject, settings, {
|
|
75
|
+
const result = applyRockShader(subject, mergeSettings(settings, assetSettings), {
|
|
64
76
|
name: `ToonLab · ${asset.label}`,
|
|
65
77
|
textures: resolved.textures,
|
|
66
78
|
variation,
|
|
@@ -124,6 +136,7 @@ export async function loadOfficialCatalogAsset({
|
|
|
124
136
|
styleBundle,
|
|
125
137
|
targetId = null,
|
|
126
138
|
textures = {},
|
|
139
|
+
usePublishedRockSurface = true,
|
|
127
140
|
variation = 0,
|
|
128
141
|
} = {}) {
|
|
129
142
|
if (!assetRuntime?.acquireAsset) {
|
|
@@ -153,6 +166,7 @@ export async function loadOfficialCatalogAsset({
|
|
|
153
166
|
let lod = null;
|
|
154
167
|
let collisionRegistration = null;
|
|
155
168
|
let inspectorRegistration = null;
|
|
169
|
+
let publishedRockSurface = null;
|
|
156
170
|
try {
|
|
157
171
|
metadata = collisionMetadataFor(asset, collision, collisionAdapter);
|
|
158
172
|
labelStyleTarget(root, createStyleTargetLabel(asset.domain, {
|
|
@@ -160,17 +174,33 @@ export async function loadOfficialCatalogAsset({
|
|
|
160
174
|
collision: metadata,
|
|
161
175
|
targetId: id,
|
|
162
176
|
}));
|
|
163
|
-
|
|
177
|
+
if (usePublishedRockSurface
|
|
178
|
+
&& asset.domain === 'natural.rock'
|
|
179
|
+
&& typeof assetRuntime.getRockShaderInput === 'function') {
|
|
180
|
+
publishedRockSurface = await assetRuntime.getRockShaderInput(asset);
|
|
181
|
+
}
|
|
182
|
+
adapter = styleAdapterFor(asset, root, {
|
|
183
|
+
settings: publishedRockSurface?.settings,
|
|
184
|
+
textures: { ...(publishedRockSurface?.textures ?? {}), ...textures },
|
|
185
|
+
variation,
|
|
186
|
+
});
|
|
164
187
|
style = await applyStyleBundle(styleBundle, {
|
|
165
188
|
targets: [createStyleTarget(id, asset.domain, root, { adapter })],
|
|
166
189
|
});
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
190
|
+
const authoredLodLevels = [...new Set(
|
|
191
|
+
collectCatalogLodBindings(root).map(({ level }) => level),
|
|
192
|
+
)];
|
|
193
|
+
const usesDenseFieldRockLods = asset.domain === 'natural.rock'
|
|
194
|
+
&& authoredLodLevels.includes(4);
|
|
195
|
+
lod = usesDenseFieldRockLods
|
|
196
|
+
? createDenseFieldRockLodRuntime(root, { maxLevel: maxLodLevel })
|
|
197
|
+
: createCatalogLodRuntime(root, {
|
|
198
|
+
distances: resolveCatalogLodDistancesForQuality(
|
|
199
|
+
qualityProfile,
|
|
200
|
+
asset.lod?.distances,
|
|
201
|
+
),
|
|
202
|
+
maxLevel: maxLodLevel,
|
|
203
|
+
});
|
|
174
204
|
if (metadata.kind !== 'none') {
|
|
175
205
|
collisionRegistration = await registerCollisionTarget({
|
|
176
206
|
adapter: collisionAdapter,
|
|
@@ -191,6 +221,8 @@ export async function loadOfficialCatalogAsset({
|
|
|
191
221
|
lod: {
|
|
192
222
|
availableLevels: lod.availableLevels,
|
|
193
223
|
enabled: true,
|
|
224
|
+
cullBelowPixels: lod.cullBelowPixels,
|
|
225
|
+
pixelThresholds: lod.pixelThresholds,
|
|
194
226
|
thresholds: lod.thresholds,
|
|
195
227
|
},
|
|
196
228
|
},
|
|
@@ -221,6 +253,7 @@ export async function loadOfficialCatalogAsset({
|
|
|
221
253
|
lod,
|
|
222
254
|
normalization,
|
|
223
255
|
object: root,
|
|
256
|
+
publishedRockSurface,
|
|
224
257
|
quality: qualityProfile,
|
|
225
258
|
/** Detail maps the artifact shipped that were too small to be usable. */
|
|
226
259
|
get rejectedTextures() { return adapter?.report?.rejectedTextures ?? []; },
|
|
@@ -238,6 +271,13 @@ export async function loadOfficialCatalogAsset({
|
|
|
238
271
|
get released() { return released; },
|
|
239
272
|
style,
|
|
240
273
|
targetId: id,
|
|
241
|
-
updateLod(options
|
|
274
|
+
updateLod(options = {}) {
|
|
275
|
+
if (!lod.pixelThresholds || options.projectedPixels !== undefined) return lod.update(options);
|
|
276
|
+
return lod.update({
|
|
277
|
+
...options,
|
|
278
|
+
viewportHeight: options.viewportHeight
|
|
279
|
+
?? (typeof globalThis.innerHeight === 'number' ? globalThis.innerHeight : 1080),
|
|
280
|
+
});
|
|
281
|
+
},
|
|
242
282
|
});
|
|
243
283
|
}
|