lecodes-assets 2.4.10 → 2.5.1
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/dist/cli.js +712 -33
- package/dist/index.js +712 -33
- package/package.json +1 -1
- package/src/commands/doctor.ts +15 -3
- package/src/commands/scene.ts +6 -1
- package/src/commands/shared.ts +20 -1
- package/src/doctor/cardTrim.ts +2 -0
- package/src/doctor/impostor.ts +614 -0
- package/src/doctor/transcodeKtx2.ts +8 -4
- package/src/doctor/transcodeTextures.ts +6 -1
- package/src/doctor/vertexAo.ts +16 -4
- package/src/unity/gameObjects.ts +13 -4
- package/src/unity/lecodesLevel.ts +38 -5
- package/src/unity/levelTerrain.ts +16 -7
- package/src/unity/materialWriter.ts +11 -1
- package/src/unity/scene.ts +16 -2
package/dist/index.js
CHANGED
|
@@ -3743,7 +3743,13 @@ var SLOTS, pick = (m, names) => {
|
|
|
3743
3743
|
slots.push(`base=${nameOf(baseG)}`);
|
|
3744
3744
|
}
|
|
3745
3745
|
}
|
|
3746
|
-
|
|
3746
|
+
let baseColor = um.colors._BaseColor ?? um.colors._Color;
|
|
3747
|
+
const tilling = um.colors._Color_Tilling;
|
|
3748
|
+
if (tilling && um.floats._Tilling_Color !== undefined) {
|
|
3749
|
+
const c = baseColor ?? [1, 1, 1, 1];
|
|
3750
|
+
baseColor = [c[0] + 0.5 * tilling[0], c[1] + 0.5 * tilling[1], c[2] + 0.5 * tilling[2], c[3]];
|
|
3751
|
+
slots.push(`base×(_Color + _Color_Tilling/2)`);
|
|
3752
|
+
}
|
|
3747
3753
|
if (baseColor)
|
|
3748
3754
|
gm.setBaseColorFactor([baseColor[0], baseColor[1], baseColor[2], gm.getBaseColorFactor()[3]]);
|
|
3749
3755
|
const live = (names) => names.map((n) => um.textures[n]).find((g) => g && (g.startsWith(PATH_GUID) || resolver.resolve(g))) ?? null;
|
|
@@ -6358,7 +6364,7 @@ async function planTexture(tex, root, maxSize = DEFAULT_MAX_SIZE, forceFmt) {
|
|
|
6358
6364
|
} catch {
|
|
6359
6365
|
return null;
|
|
6360
6366
|
}
|
|
6361
|
-
const resize = Math.max(width, height) > maxSize;
|
|
6367
|
+
const resize = !isImpostorAtlas(tex) && Math.max(width, height) > maxSize;
|
|
6362
6368
|
let desired = forceFmt ?? (hasAlpha ? "png" : "jpeg");
|
|
6363
6369
|
if (desired === "jpeg" && hasAlpha)
|
|
6364
6370
|
desired = "png";
|
|
@@ -6401,7 +6407,7 @@ async function applyTexturePlan(plan, maxSize = DEFAULT_MAX_SIZE) {
|
|
|
6401
6407
|
plan.afterBytes = out.length;
|
|
6402
6408
|
}
|
|
6403
6409
|
}
|
|
6404
|
-
var DEFAULT_MAX_SIZE = 2048, HEAVY_PNG_MIN_DIM = 1024;
|
|
6410
|
+
var DEFAULT_MAX_SIZE = 2048, HEAVY_PNG_MIN_DIM = 1024, isImpostorAtlas = (tex) => !!tex.getExtras()?.lecodes?.impostor;
|
|
6405
6411
|
var init_transcodeTextures = () => {};
|
|
6406
6412
|
|
|
6407
6413
|
// src/doctor/transcodeKtx2.ts
|
|
@@ -6485,9 +6491,9 @@ async function planKtx2(tex, root, maxSize = DEFAULT_MAX_SIZE, policy = "hq") {
|
|
|
6485
6491
|
} catch {
|
|
6486
6492
|
return null;
|
|
6487
6493
|
}
|
|
6488
|
-
const codec = policy === "uastc" ? "uastc" : isNormal || isData ? "uastc" : "etc1s";
|
|
6494
|
+
const codec = policy === "uastc" ? "uastc" : isNormal || isData || isImpostorAtlas(tex) ? "uastc" : "etc1s";
|
|
6489
6495
|
const oetf = isNormal || isData ? "linear" : "srgb";
|
|
6490
|
-
const resize = Math.max(width, height) > maxSize;
|
|
6496
|
+
const resize = !isImpostorAtlas(tex) && Math.max(width, height) > maxSize;
|
|
6491
6497
|
return {
|
|
6492
6498
|
tex,
|
|
6493
6499
|
name: tex.getName() || "?",
|
|
@@ -6545,7 +6551,7 @@ async function encodeOne(plan, enc, maxSize) {
|
|
|
6545
6551
|
const snap4 = (n) => Math.max(4, Math.round(n / 4) * 4);
|
|
6546
6552
|
let { width: tw, height: th } = plan;
|
|
6547
6553
|
const m = Math.max(tw, th);
|
|
6548
|
-
if (m > maxSize) {
|
|
6554
|
+
if (plan.resize && m > maxSize) {
|
|
6549
6555
|
const s = maxSize / m;
|
|
6550
6556
|
tw = tw * s;
|
|
6551
6557
|
th = th * s;
|
|
@@ -6686,6 +6692,8 @@ function planCardTrim(doc, opts) {
|
|
|
6686
6692
|
const mat = prim.getMaterial();
|
|
6687
6693
|
if (!mat || mat.getAlphaMode() !== "MASK" || prim.getMode() !== 4)
|
|
6688
6694
|
continue;
|
|
6695
|
+
if (mat.getExtras()?.lecodes?.impostor !== undefined)
|
|
6696
|
+
continue;
|
|
6689
6697
|
if (!prim.getAttribute("POSITION") || !prim.getAttribute("TEXCOORD_0"))
|
|
6690
6698
|
continue;
|
|
6691
6699
|
const tex = mat.getBaseColorTexture();
|
|
@@ -14684,6 +14692,586 @@ var init_props = __esm(() => {
|
|
|
14684
14692
|
IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
14685
14693
|
});
|
|
14686
14694
|
|
|
14695
|
+
// src/doctor/impostor.ts
|
|
14696
|
+
import { Logger as Logger3, PropertyType } from "@gltf-transform/core";
|
|
14697
|
+
import { prune as prune3 } from "@gltf-transform/functions";
|
|
14698
|
+
var impostorLabel = (r) => `${r.rows ? `${r.cols}×${r.rows}` : `oct ${r.cols}×${r.cols}`} frames · ${r.px} px (${r.width}×${r.height})`, LOD_RE, REPEAT = 10497, CLAMP = 33071, MIRROR = 33648, octDecode = (u, v) => {
|
|
14699
|
+
const a = u * 2 - 1, b = v * 2 - 1;
|
|
14700
|
+
const x = (a + b) / 2, z = (a - b) / 2, y = 1 - Math.abs(x) - Math.abs(z);
|
|
14701
|
+
const l = Math.hypot(x, y, z) || 1;
|
|
14702
|
+
return [x / l, y / l, z / l];
|
|
14703
|
+
}, normalEncode = (n) => {
|
|
14704
|
+
const s = Math.abs(n[0]) + Math.abs(n[1]) + Math.abs(n[2]) || 1;
|
|
14705
|
+
let px = n[0] / s, py = n[2] / s;
|
|
14706
|
+
if (n[1] / s < 0) {
|
|
14707
|
+
const sx = px >= 0 ? 1 : -1, sy = py >= 0 ? 1 : -1;
|
|
14708
|
+
const ox = (1 - Math.abs(py)) * sx, oy = (1 - Math.abs(px)) * sy;
|
|
14709
|
+
px = ox;
|
|
14710
|
+
py = oy;
|
|
14711
|
+
}
|
|
14712
|
+
return [px * 0.5 + 0.5, py * 0.5 + 0.5];
|
|
14713
|
+
}, frameBasis = (d) => {
|
|
14714
|
+
const ref = Math.abs(d[1]) < 0.999 ? [0, 1, 0] : [0, 0, -1];
|
|
14715
|
+
let rx = ref[1] * d[2] - ref[2] * d[1], ry = ref[2] * d[0] - ref[0] * d[2], rz = ref[0] * d[1] - ref[1] * d[0];
|
|
14716
|
+
const l = Math.hypot(rx, ry, rz) || 1;
|
|
14717
|
+
rx /= l;
|
|
14718
|
+
ry /= l;
|
|
14719
|
+
rz /= l;
|
|
14720
|
+
return { right: [rx, ry, rz], up: [d[1] * rz - d[2] * ry, d[2] * rx - d[0] * rz, d[0] * ry - d[1] * rx] };
|
|
14721
|
+
}, gridDirection = (i, k, A, E) => {
|
|
14722
|
+
const az = i * 2 * Math.PI / A, el = k * (Math.PI / 2) / E;
|
|
14723
|
+
return [Math.cos(el) * Math.sin(az), Math.sin(el), Math.cos(el) * Math.cos(az)];
|
|
14724
|
+
}, frame = (i, k, d, P) => ({ i, k, d, x0: i * P, x1: (i + 1) * P, y0: k * P, y1: (k + 1) * P }), gridLayout = (A, E, P) => {
|
|
14725
|
+
const frames = [];
|
|
14726
|
+
for (let k = 0;k < E; k++)
|
|
14727
|
+
for (let i = 0;i < A; i++)
|
|
14728
|
+
frames.push(frame(i, k, gridDirection(i, k, A, E), P));
|
|
14729
|
+
return { cols: A, rows: E, px: P, width: A * P, height: E * P, frames };
|
|
14730
|
+
}, octLayout = (N, P) => {
|
|
14731
|
+
const frames = [];
|
|
14732
|
+
for (let j = 0;j < N; j++)
|
|
14733
|
+
for (let i = 0;i < N; i++)
|
|
14734
|
+
frames.push(frame(i, j, octDecode(i / (N - 1), j / (N - 1)), P));
|
|
14735
|
+
return { cols: N, rows: 0, px: P, width: N * P, height: N * P, frames };
|
|
14736
|
+
}, isLevel0 = (n) => !!n.getMesh() && !LOD_RE.test(n.getName()), impostorBounds = (doc) => {
|
|
14737
|
+
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
14738
|
+
const v = [];
|
|
14739
|
+
for (const node of doc.getRoot().listNodes()) {
|
|
14740
|
+
if (!isLevel0(node))
|
|
14741
|
+
continue;
|
|
14742
|
+
const m = node.getWorldMatrix();
|
|
14743
|
+
for (const p of node.getMesh().listPrimitives()) {
|
|
14744
|
+
const pos = p.getAttribute("POSITION");
|
|
14745
|
+
if (!pos || p.getMode() !== 4)
|
|
14746
|
+
continue;
|
|
14747
|
+
for (let i = 0;i < pos.getCount(); i++) {
|
|
14748
|
+
pos.getElement(i, v);
|
|
14749
|
+
const x = m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12], y = m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13], z = m[2] * v[0] + m[6] * v[1] + m[10] * v[2] + m[14];
|
|
14750
|
+
if (x < min[0])
|
|
14751
|
+
min[0] = x;
|
|
14752
|
+
if (y < min[1])
|
|
14753
|
+
min[1] = y;
|
|
14754
|
+
if (z < min[2])
|
|
14755
|
+
min[2] = z;
|
|
14756
|
+
if (x > max[0])
|
|
14757
|
+
max[0] = x;
|
|
14758
|
+
if (y > max[1])
|
|
14759
|
+
max[1] = y;
|
|
14760
|
+
if (z > max[2])
|
|
14761
|
+
max[2] = z;
|
|
14762
|
+
}
|
|
14763
|
+
}
|
|
14764
|
+
}
|
|
14765
|
+
return min[0] <= max[0] ? { min, max } : null;
|
|
14766
|
+
}, makeSlot = async (mat, images, log) => {
|
|
14767
|
+
const f = mat?.getBaseColorFactor() ?? [1, 1, 1, 1];
|
|
14768
|
+
const mask = mat?.getAlphaMode() === "MASK";
|
|
14769
|
+
const slot = { mask, cull: !mask && !mat?.getDoubleSided(), cutoff: mat ? mat.getAlphaCutoff() : 0.5, f: [f[0], f[1], f[2], f[3]], img: null, w: 0, h: 0, wrapS: REPEAT, wrapT: REPEAT, set: 0 };
|
|
14770
|
+
const tex = mat?.getBaseColorTexture();
|
|
14771
|
+
if (!mat || !tex)
|
|
14772
|
+
return slot;
|
|
14773
|
+
if ((tex.getMimeType() || "").includes("ktx2"))
|
|
14774
|
+
throw new Error(`impostor: the base colour of material "${mat.getName()}" is already KTX2 — bake impostors before the KTX2 encode (from the source textures)`);
|
|
14775
|
+
const info = mat.getBaseColorTextureInfo();
|
|
14776
|
+
slot.set = info?.getTexCoord() ?? 0;
|
|
14777
|
+
slot.wrapS = info?.getWrapS() ?? REPEAT;
|
|
14778
|
+
slot.wrapT = info?.getWrapT() ?? REPEAT;
|
|
14779
|
+
let job = images.get(tex);
|
|
14780
|
+
if (!job) {
|
|
14781
|
+
job = (async () => {
|
|
14782
|
+
const bytes = tex.getImage();
|
|
14783
|
+
const img = bytes ? await decodeImage(bytes, tex.getURI() || tex.getMimeType() || "texture.png") : null;
|
|
14784
|
+
return img ? { img: img.data, w: img.width, h: img.height } : null;
|
|
14785
|
+
})();
|
|
14786
|
+
images.set(tex, job);
|
|
14787
|
+
}
|
|
14788
|
+
const img = await job;
|
|
14789
|
+
if (!img) {
|
|
14790
|
+
log(` ⚠ impostor: the base colour of "${mat.getName()}" could not be decoded — drawn as its factor, opaque`);
|
|
14791
|
+
slot.mask = false;
|
|
14792
|
+
return slot;
|
|
14793
|
+
}
|
|
14794
|
+
slot.img = img.img;
|
|
14795
|
+
slot.w = img.w;
|
|
14796
|
+
slot.h = img.h;
|
|
14797
|
+
return slot;
|
|
14798
|
+
}, collectLevel0 = async (doc, log) => {
|
|
14799
|
+
const pos = [], nrm = [], uv = [], ao = [];
|
|
14800
|
+
const tri = [], triSlot = [], face = [];
|
|
14801
|
+
const slots = [], slotOf = new Map;
|
|
14802
|
+
const images = new Map;
|
|
14803
|
+
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
14804
|
+
const trisOfNode = new Map;
|
|
14805
|
+
const v = [];
|
|
14806
|
+
for (const node of doc.getRoot().listNodes()) {
|
|
14807
|
+
if (!isLevel0(node))
|
|
14808
|
+
continue;
|
|
14809
|
+
const m = node.getWorldMatrix();
|
|
14810
|
+
const ax = m[0], ay = m[1], az = m[2], bx = m[4], by = m[5], bz = m[6], cx = m[8], cy = m[9], cz = m[10];
|
|
14811
|
+
const det = ax * (by * cz - bz * cy) - bx * (ay * cz - az * cy) + cx * (ay * bz - az * by);
|
|
14812
|
+
const sg = det < 0 ? -1 : 1;
|
|
14813
|
+
const n0 = [(by * cz - bz * cy) * sg, (bz * cx - bx * cz) * sg, (bx * cy - by * cx) * sg];
|
|
14814
|
+
const n1 = [(cy * az - cz * ay) * sg, (cz * ax - cx * az) * sg, (cx * ay - cy * ax) * sg];
|
|
14815
|
+
const n2 = [(ay * bz - az * by) * sg, (az * bx - ax * bz) * sg, (ax * by - ay * bx) * sg];
|
|
14816
|
+
let nodeTris = 0;
|
|
14817
|
+
for (const p of node.getMesh().listPrimitives()) {
|
|
14818
|
+
const P = p.getAttribute("POSITION");
|
|
14819
|
+
if (!P || p.getMode() !== 4)
|
|
14820
|
+
continue;
|
|
14821
|
+
const mat = p.getMaterial();
|
|
14822
|
+
let si = slotOf.get(mat);
|
|
14823
|
+
if (si === undefined) {
|
|
14824
|
+
si = slots.length;
|
|
14825
|
+
slots.push(await makeSlot(mat, images, log));
|
|
14826
|
+
slotOf.set(mat, si);
|
|
14827
|
+
}
|
|
14828
|
+
const slot = slots[si];
|
|
14829
|
+
const N = p.getAttribute("NORMAL"), T = p.getAttribute(`TEXCOORD_${slot.set}`), C = p.getAttribute("COLOR_0");
|
|
14830
|
+
const base = pos.length / 3;
|
|
14831
|
+
const count = P.getCount();
|
|
14832
|
+
for (let i = 0;i < count; i++) {
|
|
14833
|
+
P.getElement(i, v);
|
|
14834
|
+
const x = m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12], y = m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13], z = m[2] * v[0] + m[6] * v[1] + m[10] * v[2] + m[14];
|
|
14835
|
+
pos.push(x, y, z);
|
|
14836
|
+
if (x < min[0])
|
|
14837
|
+
min[0] = x;
|
|
14838
|
+
if (y < min[1])
|
|
14839
|
+
min[1] = y;
|
|
14840
|
+
if (z < min[2])
|
|
14841
|
+
min[2] = z;
|
|
14842
|
+
if (x > max[0])
|
|
14843
|
+
max[0] = x;
|
|
14844
|
+
if (y > max[1])
|
|
14845
|
+
max[1] = y;
|
|
14846
|
+
if (z > max[2])
|
|
14847
|
+
max[2] = z;
|
|
14848
|
+
if (N) {
|
|
14849
|
+
N.getElement(i, v);
|
|
14850
|
+
const nx = n0[0] * v[0] + n1[0] * v[1] + n2[0] * v[2], ny = n0[1] * v[0] + n1[1] * v[1] + n2[1] * v[2], nz = n0[2] * v[0] + n1[2] * v[1] + n2[2] * v[2];
|
|
14851
|
+
const l = Math.hypot(nx, ny, nz) || 1;
|
|
14852
|
+
nrm.push(nx / l, ny / l, nz / l);
|
|
14853
|
+
} else
|
|
14854
|
+
nrm.push(0, 0, 0);
|
|
14855
|
+
if (T) {
|
|
14856
|
+
T.getElement(i, v);
|
|
14857
|
+
uv.push(v[0], v[1]);
|
|
14858
|
+
} else
|
|
14859
|
+
uv.push(0, 0);
|
|
14860
|
+
if (C) {
|
|
14861
|
+
C.getElement(i, v);
|
|
14862
|
+
ao.push(v[0]);
|
|
14863
|
+
} else
|
|
14864
|
+
ao.push(1);
|
|
14865
|
+
}
|
|
14866
|
+
const idx = p.getIndices();
|
|
14867
|
+
const n = idx ? idx.getCount() : count;
|
|
14868
|
+
for (let k = 0;k + 2 < n; k += 3) {
|
|
14869
|
+
const a = base + (idx ? idx.getScalar(k) : k), b = base + (idx ? idx.getScalar(k + 1) : k + 1), c = base + (idx ? idx.getScalar(k + 2) : k + 2);
|
|
14870
|
+
tri.push(a, b, c);
|
|
14871
|
+
triSlot.push(si);
|
|
14872
|
+
const e1x = pos[b * 3] - pos[a * 3], e1y = pos[b * 3 + 1] - pos[a * 3 + 1], e1z = pos[b * 3 + 2] - pos[a * 3 + 2];
|
|
14873
|
+
const e2x = pos[c * 3] - pos[a * 3], e2y = pos[c * 3 + 1] - pos[a * 3 + 1], e2z = pos[c * 3 + 2] - pos[a * 3 + 2];
|
|
14874
|
+
const fx = (e1y * e2z - e1z * e2y) * sg, fy = (e1z * e2x - e1x * e2z) * sg, fz = (e1x * e2y - e1y * e2x) * sg;
|
|
14875
|
+
const fl = Math.hypot(fx, fy, fz) || 1;
|
|
14876
|
+
face.push(fx / fl, fy / fl, fz / fl);
|
|
14877
|
+
nodeTris++;
|
|
14878
|
+
}
|
|
14879
|
+
}
|
|
14880
|
+
trisOfNode.set(node, nodeTris);
|
|
14881
|
+
}
|
|
14882
|
+
if (!tri.length)
|
|
14883
|
+
return null;
|
|
14884
|
+
let root = null, best = -1;
|
|
14885
|
+
for (const [node, n] of trisOfNode) {
|
|
14886
|
+
let nested = false;
|
|
14887
|
+
for (let a = node.getParentNode();a; a = a.getParentNode())
|
|
14888
|
+
if (trisOfNode.has(a)) {
|
|
14889
|
+
nested = true;
|
|
14890
|
+
break;
|
|
14891
|
+
}
|
|
14892
|
+
if (!nested && n > best) {
|
|
14893
|
+
best = n;
|
|
14894
|
+
root = node;
|
|
14895
|
+
}
|
|
14896
|
+
}
|
|
14897
|
+
return {
|
|
14898
|
+
pos: Float32Array.from(pos),
|
|
14899
|
+
nrm: Float32Array.from(nrm),
|
|
14900
|
+
uv: Float32Array.from(uv),
|
|
14901
|
+
ao: Float32Array.from(ao),
|
|
14902
|
+
tri: Uint32Array.from(tri),
|
|
14903
|
+
triSlot: Uint16Array.from(triSlot),
|
|
14904
|
+
face: Float32Array.from(face),
|
|
14905
|
+
slots,
|
|
14906
|
+
min,
|
|
14907
|
+
max,
|
|
14908
|
+
root
|
|
14909
|
+
};
|
|
14910
|
+
}, SRGB_TO_LINEAR2, toSrgb8 = (x) => {
|
|
14911
|
+
const c = x <= 0 ? 0 : x >= 1 ? 1 : x <= 0.0031308 ? x * 12.92 : 1.055 * Math.pow(x, 1 / 2.4) - 0.055;
|
|
14912
|
+
return Math.round(c * 255);
|
|
14913
|
+
}, wrapI = (i, n, mode) => {
|
|
14914
|
+
if (mode === CLAMP)
|
|
14915
|
+
return i < 0 ? 0 : i >= n ? n - 1 : i;
|
|
14916
|
+
if (mode === MIRROR) {
|
|
14917
|
+
const p = 2 * n, k = (i % p + p) % p;
|
|
14918
|
+
return k < n ? k : p - 1 - k;
|
|
14919
|
+
}
|
|
14920
|
+
return (i % n + n) % n;
|
|
14921
|
+
}, tapI, tapW, taps = (s, u, v) => {
|
|
14922
|
+
const fx = u * s.w - 0.5, fy = v * s.h - 0.5;
|
|
14923
|
+
const x0 = Math.floor(fx), y0 = Math.floor(fy), tx = fx - x0, ty = fy - y0;
|
|
14924
|
+
const xa = wrapI(x0, s.w, s.wrapS), xb = wrapI(x0 + 1, s.w, s.wrapS), ya = wrapI(y0, s.h, s.wrapT), yb = wrapI(y0 + 1, s.h, s.wrapT);
|
|
14925
|
+
tapI[0] = (ya * s.w + xa) * 4;
|
|
14926
|
+
tapI[1] = (ya * s.w + xb) * 4;
|
|
14927
|
+
tapI[2] = (yb * s.w + xa) * 4;
|
|
14928
|
+
tapI[3] = (yb * s.w + xb) * 4;
|
|
14929
|
+
tapW[0] = (1 - tx) * (1 - ty);
|
|
14930
|
+
tapW[1] = tx * (1 - ty);
|
|
14931
|
+
tapW[2] = (1 - tx) * ty;
|
|
14932
|
+
tapW[3] = tx * ty;
|
|
14933
|
+
}, alphaAt = (s, u, v) => {
|
|
14934
|
+
if (!s.img)
|
|
14935
|
+
return s.f[3];
|
|
14936
|
+
taps(s, u, v);
|
|
14937
|
+
const d = s.img;
|
|
14938
|
+
return (d[tapI[0] + 3] * tapW[0] + d[tapI[1] + 3] * tapW[1] + d[tapI[2] + 3] * tapW[2] + d[tapI[3] + 3] * tapW[3]) / 255 * s.f[3];
|
|
14939
|
+
}, DILATE_PASSES = 16, dilate = (base, data, covered, W, H, frameOf, nFrames) => {
|
|
14940
|
+
let cur = covered.slice();
|
|
14941
|
+
for (let pass = 0;pass < DILATE_PASSES; pass++) {
|
|
14942
|
+
const next = cur.slice();
|
|
14943
|
+
let changed = 0;
|
|
14944
|
+
for (let y = 0;y < H; y++) {
|
|
14945
|
+
for (let x = 0;x < W; x++) {
|
|
14946
|
+
const i = y * W + x;
|
|
14947
|
+
if (cur[i])
|
|
14948
|
+
continue;
|
|
14949
|
+
const f = frameOf[i];
|
|
14950
|
+
let n = 0, r = 0, g = 0, b = 0, d0 = 0, d1 = 0, d2 = 0, d3 = 0;
|
|
14951
|
+
for (let oy = -1;oy <= 1; oy++) {
|
|
14952
|
+
const yy = y + oy;
|
|
14953
|
+
if (yy < 0 || yy >= H)
|
|
14954
|
+
continue;
|
|
14955
|
+
for (let ox = -1;ox <= 1; ox++) {
|
|
14956
|
+
const xx = x + ox;
|
|
14957
|
+
if (xx < 0 || xx >= W)
|
|
14958
|
+
continue;
|
|
14959
|
+
const k = yy * W + xx;
|
|
14960
|
+
if (!cur[k] || frameOf[k] !== f)
|
|
14961
|
+
continue;
|
|
14962
|
+
const o = k * 4;
|
|
14963
|
+
r += base[o];
|
|
14964
|
+
g += base[o + 1];
|
|
14965
|
+
b += base[o + 2];
|
|
14966
|
+
d0 += data[o];
|
|
14967
|
+
d1 += data[o + 1];
|
|
14968
|
+
d2 += data[o + 2];
|
|
14969
|
+
d3 += data[o + 3];
|
|
14970
|
+
n++;
|
|
14971
|
+
}
|
|
14972
|
+
}
|
|
14973
|
+
if (!n)
|
|
14974
|
+
continue;
|
|
14975
|
+
const o = i * 4;
|
|
14976
|
+
base[o] = Math.round(r / n);
|
|
14977
|
+
base[o + 1] = Math.round(g / n);
|
|
14978
|
+
base[o + 2] = Math.round(b / n);
|
|
14979
|
+
data[o] = Math.round(d0 / n);
|
|
14980
|
+
data[o + 1] = Math.round(d1 / n);
|
|
14981
|
+
data[o + 2] = Math.round(d2 / n);
|
|
14982
|
+
data[o + 3] = Math.round(d3 / n);
|
|
14983
|
+
next[i] = 1;
|
|
14984
|
+
changed++;
|
|
14985
|
+
}
|
|
14986
|
+
}
|
|
14987
|
+
cur = next;
|
|
14988
|
+
if (!changed)
|
|
14989
|
+
break;
|
|
14990
|
+
}
|
|
14991
|
+
const sums = new Float64Array((nFrames + 1) * 8);
|
|
14992
|
+
const all = nFrames * 8;
|
|
14993
|
+
for (let i = 0;i < W * H; i++) {
|
|
14994
|
+
if (!covered[i])
|
|
14995
|
+
continue;
|
|
14996
|
+
const o = i * 4;
|
|
14997
|
+
for (const s of frameOf[i] >= 0 ? [frameOf[i] * 8, all] : [all]) {
|
|
14998
|
+
sums[s] += base[o];
|
|
14999
|
+
sums[s + 1] += base[o + 1];
|
|
15000
|
+
sums[s + 2] += base[o + 2];
|
|
15001
|
+
sums[s + 3] += data[o];
|
|
15002
|
+
sums[s + 4] += data[o + 1];
|
|
15003
|
+
sums[s + 5] += data[o + 2];
|
|
15004
|
+
sums[s + 6] += data[o + 3];
|
|
15005
|
+
sums[s + 7]++;
|
|
15006
|
+
}
|
|
15007
|
+
}
|
|
15008
|
+
for (let i = 0;i < W * H; i++) {
|
|
15009
|
+
if (cur[i])
|
|
15010
|
+
continue;
|
|
15011
|
+
let s = frameOf[i] >= 0 ? frameOf[i] * 8 : all;
|
|
15012
|
+
if (!sums[s + 7])
|
|
15013
|
+
s = all;
|
|
15014
|
+
const n = sums[s + 7], o = i * 4;
|
|
15015
|
+
if (!n) {
|
|
15016
|
+
base[o] = base[o + 1] = base[o + 2] = 128;
|
|
15017
|
+
data[o] = data[o + 1] = data[o + 2] = 128;
|
|
15018
|
+
data[o + 3] = 255;
|
|
15019
|
+
continue;
|
|
15020
|
+
}
|
|
15021
|
+
base[o] = Math.round(sums[s] / n);
|
|
15022
|
+
base[o + 1] = Math.round(sums[s + 1] / n);
|
|
15023
|
+
base[o + 2] = Math.round(sums[s + 2] / n);
|
|
15024
|
+
data[o] = Math.round(sums[s + 3] / n);
|
|
15025
|
+
data[o + 1] = Math.round(sums[s + 4] / n);
|
|
15026
|
+
data[o + 2] = Math.round(sums[s + 5] / n);
|
|
15027
|
+
data[o + 3] = Math.round(sums[s + 6] / n);
|
|
15028
|
+
}
|
|
15029
|
+
}, invert4 = (m) => {
|
|
15030
|
+
const [a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23, a30, a31, a32, a33] = Array.from(m);
|
|
15031
|
+
const b00 = a00 * a11 - a01 * a10, b01 = a00 * a12 - a02 * a10, b02 = a00 * a13 - a03 * a10, b03 = a01 * a12 - a02 * a11;
|
|
15032
|
+
const b04 = a01 * a13 - a03 * a11, b05 = a02 * a13 - a03 * a12, b06 = a20 * a31 - a21 * a30, b07 = a20 * a32 - a22 * a30;
|
|
15033
|
+
const b08 = a20 * a33 - a23 * a30, b09 = a21 * a32 - a22 * a31, b10 = a21 * a33 - a23 * a31, b11 = a22 * a33 - a23 * a32;
|
|
15034
|
+
let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
|
|
15035
|
+
if (Math.abs(det) < 0.000000000001)
|
|
15036
|
+
return null;
|
|
15037
|
+
det = 1 / det;
|
|
15038
|
+
return [
|
|
15039
|
+
(a11 * b11 - a12 * b10 + a13 * b09) * det,
|
|
15040
|
+
(a02 * b10 - a01 * b11 - a03 * b09) * det,
|
|
15041
|
+
(a31 * b05 - a32 * b04 + a33 * b03) * det,
|
|
15042
|
+
(a22 * b04 - a21 * b05 - a23 * b03) * det,
|
|
15043
|
+
(a12 * b08 - a10 * b11 - a13 * b07) * det,
|
|
15044
|
+
(a00 * b11 - a02 * b08 + a03 * b07) * det,
|
|
15045
|
+
(a32 * b02 - a30 * b05 - a33 * b01) * det,
|
|
15046
|
+
(a20 * b05 - a22 * b02 + a23 * b01) * det,
|
|
15047
|
+
(a10 * b10 - a11 * b08 + a13 * b06) * det,
|
|
15048
|
+
(a01 * b08 - a00 * b10 - a03 * b06) * det,
|
|
15049
|
+
(a30 * b04 - a31 * b02 + a33 * b00) * det,
|
|
15050
|
+
(a21 * b02 - a20 * b04 - a23 * b00) * det,
|
|
15051
|
+
(a11 * b07 - a10 * b09 - a12 * b06) * det,
|
|
15052
|
+
(a00 * b09 - a01 * b07 + a02 * b06) * det,
|
|
15053
|
+
(a31 * b01 - a30 * b03 - a32 * b00) * det,
|
|
15054
|
+
(a20 * b03 - a21 * b01 + a22 * b00) * det
|
|
15055
|
+
];
|
|
15056
|
+
}, encodePng2 = async (rgba, w, h) => {
|
|
15057
|
+
const sharp = (await import("sharp")).default;
|
|
15058
|
+
return new Uint8Array(await sharp(Buffer.from(rgba.buffer, rgba.byteOffset, rgba.byteLength), { raw: { width: w, height: h, channels: 4 } }).png().toBuffer());
|
|
15059
|
+
}, bakeImpostor = async (doc, opts) => {
|
|
15060
|
+
const t0 = performance.now();
|
|
15061
|
+
const log = opts.log ?? ((s) => console.log(s));
|
|
15062
|
+
const g = await collectLevel0(doc, log);
|
|
15063
|
+
if (!g)
|
|
15064
|
+
return null;
|
|
15065
|
+
const { pos, nrm, uv, ao, tri, triSlot, face, slots } = g;
|
|
15066
|
+
const nv = pos.length / 3, nt = tri.length / 3;
|
|
15067
|
+
const c = [(g.min[0] + g.max[0]) / 2, (g.min[1] + g.max[1]) / 2, (g.min[2] + g.max[2]) / 2];
|
|
15068
|
+
let r2 = 0;
|
|
15069
|
+
for (let v = 0;v < nv; v++) {
|
|
15070
|
+
const dx = pos[v * 3] - c[0], dy = pos[v * 3 + 1] - c[1], dz = pos[v * 3 + 2] - c[2];
|
|
15071
|
+
const d = dx * dx + dy * dy + dz * dz;
|
|
15072
|
+
if (d > r2)
|
|
15073
|
+
r2 = d;
|
|
15074
|
+
}
|
|
15075
|
+
const r = Math.sqrt(r2) || 0.001;
|
|
15076
|
+
const height = g.max[1] - g.min[1];
|
|
15077
|
+
const P = Math.max(4, Math.round((opts.px ?? (height >= 8 ? 256 : 128)) / 4) * 4);
|
|
15078
|
+
const layout = opts.rows > 0 ? gridLayout(Math.max(3, Math.round(opts.cols)), Math.max(1, Math.round(opts.rows)), P) : octLayout(Math.max(2, Math.round(opts.cols)), P);
|
|
15079
|
+
const { width: AW, height: AH, frames } = layout;
|
|
15080
|
+
const crown = [0, 0.6 * height, 0];
|
|
15081
|
+
const frameOf = new Int32Array(AW * AH).fill(-1);
|
|
15082
|
+
let span = 0;
|
|
15083
|
+
frames.forEach((f, fi) => {
|
|
15084
|
+
span = Math.max(span, f.x1 - f.x0, f.y1 - f.y0);
|
|
15085
|
+
for (let y = f.y0;y < f.y1; y++)
|
|
15086
|
+
frameOf.fill(fi, y * AW + f.x0, y * AW + f.x1);
|
|
15087
|
+
});
|
|
15088
|
+
const base = new Uint8Array(AW * AH * 4), data = new Uint8Array(AW * AH * 4), covered = new Uint8Array(AW * AH);
|
|
15089
|
+
const samples = span * span * 4;
|
|
15090
|
+
const zb = new Float32Array(samples), tb = new Int32Array(samples), l1b = new Float32Array(samples), l2b = new Float32Array(samples);
|
|
15091
|
+
const qx = new Float32Array(nv), qy = new Float32Array(nv), qz = new Float32Array(nv);
|
|
15092
|
+
for (const fr of frames) {
|
|
15093
|
+
const d = fr.d;
|
|
15094
|
+
const { right, up } = frameBasis(d);
|
|
15095
|
+
const { x0: X0, x1: X1, y0: Y0, y1: Y1 } = fr;
|
|
15096
|
+
const W = (X1 - X0) * 2, H = (Y1 - Y0) * 2;
|
|
15097
|
+
const k = 0.5 / r;
|
|
15098
|
+
for (let v = 0;v < nv; v++) {
|
|
15099
|
+
const dx = pos[v * 3] - c[0], dy = pos[v * 3 + 1] - c[1], dz = pos[v * 3 + 2] - c[2];
|
|
15100
|
+
const a = 0.5 + k * (dx * right[0] + dy * right[1] + dz * right[2]);
|
|
15101
|
+
const b = 0.5 + k * (dx * up[0] + dy * up[1] + dz * up[2]);
|
|
15102
|
+
qx[v] = a * W;
|
|
15103
|
+
qy[v] = (1 - b) * H;
|
|
15104
|
+
qz[v] = dx * d[0] + dy * d[1] + dz * d[2];
|
|
15105
|
+
}
|
|
15106
|
+
zb.fill(-Infinity, 0, W * H);
|
|
15107
|
+
tb.fill(-1, 0, W * H);
|
|
15108
|
+
for (let t = 0;t < nt; t++) {
|
|
15109
|
+
const ia = tri[t * 3], ib = tri[t * 3 + 1], ic = tri[t * 3 + 2];
|
|
15110
|
+
const x0 = qx[ia], y0 = qy[ia], x1 = qx[ib], y1 = qy[ib], x2 = qx[ic], y2 = qy[ic];
|
|
15111
|
+
const minX = Math.max(0, Math.ceil(Math.min(x0, x1, x2) - 0.5)), maxX = Math.min(W - 1, Math.floor(Math.max(x0, x1, x2) - 0.5));
|
|
15112
|
+
if (minX > maxX)
|
|
15113
|
+
continue;
|
|
15114
|
+
const minY = Math.max(0, Math.ceil(Math.min(y0, y1, y2) - 0.5)), maxY = Math.min(H - 1, Math.floor(Math.max(y0, y1, y2) - 0.5));
|
|
15115
|
+
if (minY > maxY)
|
|
15116
|
+
continue;
|
|
15117
|
+
const area = (x1 - x0) * (y2 - y0) - (y1 - y0) * (x2 - x0);
|
|
15118
|
+
if (Math.abs(area) < 0.000000000001)
|
|
15119
|
+
continue;
|
|
15120
|
+
const slot = slots[triSlot[t]];
|
|
15121
|
+
if (slot.cull && face[t * 3] * d[0] + face[t * 3 + 1] * d[1] + face[t * 3 + 2] * d[2] < 0)
|
|
15122
|
+
continue;
|
|
15123
|
+
const inv = 1 / area;
|
|
15124
|
+
const z0 = qz[ia], dz1 = qz[ib] - z0, dz2 = qz[ic] - z0;
|
|
15125
|
+
const testAlpha = slot.mask;
|
|
15126
|
+
const u0 = uv[ia * 2], v0 = uv[ia * 2 + 1], du1 = uv[ib * 2] - u0, dv1 = uv[ib * 2 + 1] - v0, du2 = uv[ic * 2] - u0, dv2 = uv[ic * 2 + 1] - v0;
|
|
15127
|
+
const s1x = (y2 - y0) * inv, s2x = -(y1 - y0) * inv;
|
|
15128
|
+
for (let sy = minY;sy <= maxY; sy++) {
|
|
15129
|
+
const py = sy + 0.5 - y0, px = minX + 0.5 - x0;
|
|
15130
|
+
let l1 = (px * (y2 - y0) - py * (x2 - x0)) * inv;
|
|
15131
|
+
let l2 = ((x1 - x0) * py - (y1 - y0) * px) * inv;
|
|
15132
|
+
const row = sy * W;
|
|
15133
|
+
for (let sx = minX;sx <= maxX; sx++, l1 += s1x, l2 += s2x) {
|
|
15134
|
+
if (l1 < -0.0000001 || l2 < -0.0000001 || l1 + l2 > 1.0000001)
|
|
15135
|
+
continue;
|
|
15136
|
+
const z = z0 + l1 * dz1 + l2 * dz2;
|
|
15137
|
+
const s = row + sx;
|
|
15138
|
+
if (z <= zb[s])
|
|
15139
|
+
continue;
|
|
15140
|
+
if (testAlpha && alphaAt(slot, u0 + l1 * du1 + l2 * du2, v0 + l1 * dv1 + l2 * dv2) < slot.cutoff)
|
|
15141
|
+
continue;
|
|
15142
|
+
zb[s] = z;
|
|
15143
|
+
tb[s] = t;
|
|
15144
|
+
l1b[s] = l1;
|
|
15145
|
+
l2b[s] = l2;
|
|
15146
|
+
}
|
|
15147
|
+
}
|
|
15148
|
+
}
|
|
15149
|
+
for (let y = Y0;y < Y1; y++) {
|
|
15150
|
+
for (let x = X0;x < X1; x++) {
|
|
15151
|
+
let n = 0, cr = 0, cg = 0, cb = 0, nx = 0, ny = 0, nz = 0, aoSum = 0, zSum = 0;
|
|
15152
|
+
for (let q = 0;q < 4; q++) {
|
|
15153
|
+
const s = ((y - Y0) * 2 + (q >> 1)) * W + (x - X0) * 2 + (q & 1);
|
|
15154
|
+
const t = tb[s];
|
|
15155
|
+
if (t < 0)
|
|
15156
|
+
continue;
|
|
15157
|
+
zSum += zb[s];
|
|
15158
|
+
const l1 = l1b[s], l2 = l2b[s], l0 = 1 - l1 - l2;
|
|
15159
|
+
const ia = tri[t * 3], ib = tri[t * 3 + 1], ic = tri[t * 3 + 2];
|
|
15160
|
+
const mx = pos[ia * 3] * l0 + pos[ib * 3] * l1 + pos[ic * 3] * l2 - crown[0];
|
|
15161
|
+
const my = pos[ia * 3 + 1] * l0 + pos[ib * 3 + 1] * l1 + pos[ic * 3 + 1] * l2 - crown[1];
|
|
15162
|
+
const mz = pos[ia * 3 + 2] * l0 + pos[ib * 3 + 2] * l1 + pos[ic * 3 + 2] * l2 - crown[2];
|
|
15163
|
+
const ml = Math.hypot(mx, my, mz) || 1;
|
|
15164
|
+
nx += mx / ml;
|
|
15165
|
+
ny += my / ml;
|
|
15166
|
+
nz += mz / ml;
|
|
15167
|
+
const slot = slots[triSlot[t]];
|
|
15168
|
+
if (slot.img) {
|
|
15169
|
+
taps(slot, uv[ia * 2] * l0 + uv[ib * 2] * l1 + uv[ic * 2] * l2, uv[ia * 2 + 1] * l0 + uv[ib * 2 + 1] * l1 + uv[ic * 2 + 1] * l2);
|
|
15170
|
+
const im = slot.img;
|
|
15171
|
+
let r0 = 0, g0 = 0, b0 = 0;
|
|
15172
|
+
for (let k4 = 0;k4 < 4; k4++) {
|
|
15173
|
+
const o = tapI[k4], w = tapW[k4];
|
|
15174
|
+
r0 += SRGB_TO_LINEAR2[im[o]] * w;
|
|
15175
|
+
g0 += SRGB_TO_LINEAR2[im[o + 1]] * w;
|
|
15176
|
+
b0 += SRGB_TO_LINEAR2[im[o + 2]] * w;
|
|
15177
|
+
}
|
|
15178
|
+
cr += r0 * slot.f[0];
|
|
15179
|
+
cg += g0 * slot.f[1];
|
|
15180
|
+
cb += b0 * slot.f[2];
|
|
15181
|
+
} else {
|
|
15182
|
+
cr += slot.f[0];
|
|
15183
|
+
cg += slot.f[1];
|
|
15184
|
+
cb += slot.f[2];
|
|
15185
|
+
}
|
|
15186
|
+
aoSum += ao[ia] * l0 + ao[ib] * l1 + ao[ic] * l2;
|
|
15187
|
+
n++;
|
|
15188
|
+
}
|
|
15189
|
+
if (!n)
|
|
15190
|
+
continue;
|
|
15191
|
+
const o = (y * AW + x) * 4;
|
|
15192
|
+
covered[y * AW + x] = 1;
|
|
15193
|
+
base[o] = toSrgb8(cr / n);
|
|
15194
|
+
base[o + 1] = toSrgb8(cg / n);
|
|
15195
|
+
base[o + 2] = toSrgb8(cb / n);
|
|
15196
|
+
base[o + 3] = Math.round(n / 4 * 255);
|
|
15197
|
+
const nl = Math.hypot(nx, ny, nz) || 1;
|
|
15198
|
+
const [eu, ev] = normalEncode([nx / nl, ny / nl, nz / nl]);
|
|
15199
|
+
const depth = zSum / n / r;
|
|
15200
|
+
data[o] = Math.round(Math.min(Math.max(eu, 0), 1) * 255);
|
|
15201
|
+
data[o + 1] = Math.round(Math.min(Math.max(ev, 0), 1) * 255);
|
|
15202
|
+
data[o + 2] = Math.round(Math.min(Math.max(depth * 0.5 + 0.5, 0), 1) * 255);
|
|
15203
|
+
data[o + 3] = Math.round(Math.min(Math.max(aoSum / n, 0), 1) * 255);
|
|
15204
|
+
}
|
|
15205
|
+
}
|
|
15206
|
+
}
|
|
15207
|
+
dilate(base, data, covered, AW, AH, frameOf, frames.length);
|
|
15208
|
+
const root = g.root;
|
|
15209
|
+
const rootName = root.getName() || root.getMesh()?.getName() || "impostor";
|
|
15210
|
+
let highest = 0;
|
|
15211
|
+
for (const n of doc.getRoot().listNodes()) {
|
|
15212
|
+
const m = LOD_RE.exec(n.getName());
|
|
15213
|
+
if (m)
|
|
15214
|
+
highest = Math.max(highest, Number(m[1]));
|
|
15215
|
+
}
|
|
15216
|
+
const level = Math.min(Math.max(1, Math.round(opts.level ?? 2)), highest + 1);
|
|
15217
|
+
const drop = (n) => {
|
|
15218
|
+
for (const ch of n.listChildren())
|
|
15219
|
+
drop(ch);
|
|
15220
|
+
n.dispose();
|
|
15221
|
+
};
|
|
15222
|
+
for (const n of doc.getRoot().listNodes()) {
|
|
15223
|
+
const m = LOD_RE.exec(n.getName());
|
|
15224
|
+
if (m && Number(m[1]) >= level && !n.isDisposed())
|
|
15225
|
+
drop(n);
|
|
15226
|
+
}
|
|
15227
|
+
const buffer = doc.getRoot().listBuffers()[0] ?? doc.createBuffer();
|
|
15228
|
+
const tag = () => ({ lecodes: { impostor: true } });
|
|
15229
|
+
const albedo = doc.createTexture(`${rootName}_impostor_albedo`).setMimeType("image/png").setImage(await encodePng2(base, AW, AH)).setExtras(tag());
|
|
15230
|
+
const normalDepth = doc.createTexture(`${rootName}_impostor_data`).setMimeType("image/png").setImage(await encodePng2(data, AW, AH)).setExtras(tag());
|
|
15231
|
+
const material = doc.createMaterial(`${rootName}_impostor`).setAlphaMode("MASK").setAlphaCutoff(0.5).setDoubleSided(true).setMetallicFactor(0).setRoughnessFactor(1).setBaseColorFactor([1, 1, 1, 1]).setBaseColorTexture(albedo).setMetallicRoughnessTexture(normalDepth).setExtras({ lecodes: { impostor: [layout.cols, c[0], c[1], c[2], r, layout.rows] } });
|
|
15232
|
+
for (const info of [material.getBaseColorTextureInfo(), material.getMetallicRoughnessTextureInfo()])
|
|
15233
|
+
info?.setWrapS(CLAMP).setWrapT(CLAMP);
|
|
15234
|
+
const acc = (type, arr) => doc.createAccessor().setType(type).setArray(arr).setBuffer(buffer);
|
|
15235
|
+
const corners = new Float32Array([-1, -1, -1, 1, -1, 1, 1, 1, -1, -1, 1, 1]).map((s, k) => c[k % 3] + s * r);
|
|
15236
|
+
const prim = doc.createPrimitive().setMaterial(material).setAttribute("POSITION", acc("VEC3", corners)).setAttribute("NORMAL", acc("VEC3", new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]))).setAttribute("TEXCOORD_0", acc("VEC2", new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]))).setAttribute("COLOR_0", acc("VEC4", new Float32Array(16).fill(1))).setIndices(acc("SCALAR", new Uint16Array([0, 1, 2, 0, 2, 3])));
|
|
15237
|
+
const node = doc.createNode(`${rootName}_LOD${level}`).setMesh(doc.createMesh(`${rootName}_impostor`).addPrimitive(prim)).setExtras({ lecodes: { impostor: { cols: layout.cols, rows: layout.rows, px: P } } });
|
|
15238
|
+
const parent = root.getParentNode();
|
|
15239
|
+
if (parent) {
|
|
15240
|
+
parent.addChild(node);
|
|
15241
|
+
const pw = parent.getWorldMatrix();
|
|
15242
|
+
const identity = pw.every((x, k) => Math.abs(x - (k % 5 === 0 ? 1 : 0)) < 0.000001);
|
|
15243
|
+
const inv = identity ? null : invert4(pw);
|
|
15244
|
+
if (inv)
|
|
15245
|
+
node.setMatrix(inv);
|
|
15246
|
+
} else {
|
|
15247
|
+
for (const s of doc.getRoot().listScenes())
|
|
15248
|
+
if (s.listChildren().includes(root))
|
|
15249
|
+
s.addChild(node);
|
|
15250
|
+
}
|
|
15251
|
+
const logger = doc.getLogger();
|
|
15252
|
+
doc.setLogger(new Logger3(Logger3.Verbosity.SILENT));
|
|
15253
|
+
await doc.transform(prune3({
|
|
15254
|
+
propertyTypes: [PropertyType.MESH, PropertyType.PRIMITIVE, PropertyType.MATERIAL, PropertyType.TEXTURE, PropertyType.ACCESSOR],
|
|
15255
|
+
keepLeaves: true,
|
|
15256
|
+
keepAttributes: true,
|
|
15257
|
+
keepIndices: true,
|
|
15258
|
+
keepSolidTextures: true,
|
|
15259
|
+
keepExtras: false
|
|
15260
|
+
}));
|
|
15261
|
+
doc.setLogger(logger);
|
|
15262
|
+
return { cols: layout.cols, rows: layout.rows, px: P, width: AW, height: AH, radius: r, center: c, level, triangles: nt, ms: Math.round(performance.now() - t0), node: node.getName() };
|
|
15263
|
+
};
|
|
15264
|
+
var init_impostor = __esm(() => {
|
|
15265
|
+
init_image();
|
|
15266
|
+
LOD_RE = /_LOD([1-9]\d*)$/i;
|
|
15267
|
+
SRGB_TO_LINEAR2 = Float32Array.from({ length: 256 }, (_, i) => {
|
|
15268
|
+
const c = i / 255;
|
|
15269
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
15270
|
+
});
|
|
15271
|
+
tapI = new Int32Array(4);
|
|
15272
|
+
tapW = new Float32Array(4);
|
|
15273
|
+
});
|
|
15274
|
+
|
|
14687
15275
|
// src/geometry.ts
|
|
14688
15276
|
var MAX_VERTICES = 65535, mul32 = (m, x, y, z, w) => [
|
|
14689
15277
|
m[0] * x + m[4] * y + m[8] * z + m[12] * w,
|
|
@@ -15401,20 +15989,33 @@ var cloneTree = (t) => {
|
|
|
15401
15989
|
x.children.forEach(collect);
|
|
15402
15990
|
};
|
|
15403
15991
|
collect(g);
|
|
15992
|
+
const levels = c.body.m_LODs.filter((lod) => isMap(lod) && Array.isArray(lod.renderers)).length;
|
|
15404
15993
|
c.body.m_LODs.forEach((lod, level) => {
|
|
15405
15994
|
if (!isMap(lod) || !Array.isArray(lod.renderers))
|
|
15406
15995
|
return;
|
|
15407
15996
|
for (const r of lod.renderers) {
|
|
15408
15997
|
const id = ref(isMap(r) ? r.renderer : undefined).fileID;
|
|
15409
15998
|
const rc = byId.get(id) ?? ids?.get(id);
|
|
15410
|
-
if (rc
|
|
15411
|
-
|
|
15999
|
+
if (!rc || !("go" in rc))
|
|
16000
|
+
continue;
|
|
16001
|
+
if (levels <= 1) {
|
|
16002
|
+
if (!out.has(rc))
|
|
16003
|
+
out.set(rc, []);
|
|
16004
|
+
continue;
|
|
16005
|
+
}
|
|
16006
|
+
const have = out.get(rc);
|
|
16007
|
+
if (!have)
|
|
16008
|
+
out.set(rc, [level]);
|
|
16009
|
+
else if (have.length > 0 && !have.includes(level))
|
|
16010
|
+
have.push(level);
|
|
15412
16011
|
}
|
|
15413
16012
|
});
|
|
15414
16013
|
}
|
|
15415
16014
|
g.children.forEach(walk);
|
|
15416
16015
|
};
|
|
15417
16016
|
roots.forEach(walk);
|
|
16017
|
+
for (const v of out.values())
|
|
16018
|
+
v.sort((a, b) => a - b);
|
|
15418
16019
|
return out;
|
|
15419
16020
|
};
|
|
15420
16021
|
var init_gameObjects = () => {};
|
|
@@ -16547,6 +17148,7 @@ var readCollider = (c, path, M) => {
|
|
|
16547
17148
|
};
|
|
16548
17149
|
const kindOf = (root, prefab, fallbackName, nested) => {
|
|
16549
17150
|
const parts = [], colliders = [], klights = [];
|
|
17151
|
+
const drawn = [];
|
|
16550
17152
|
let particles = 0;
|
|
16551
17153
|
const lods = lodLevels([root]);
|
|
16552
17154
|
const walk = (g, parentM, path, top) => {
|
|
@@ -16565,7 +17167,9 @@ var readCollider = (c, path, M) => {
|
|
|
16565
17167
|
const mesh = ref(mf.body.m_Mesh);
|
|
16566
17168
|
if (mesh.guid && mesh.guid !== "0000000000000000e000000000000000") {
|
|
16567
17169
|
const mats = Array.isArray(mr.body.m_Materials) ? mr.body.m_Materials.map((x) => ref(x).guid ?? "") : [];
|
|
16568
|
-
|
|
17170
|
+
const suffix = lodSuffix(goName(g));
|
|
17171
|
+
const levels = lods.get(mr) ?? (suffix > 0 ? [suffix] : []);
|
|
17172
|
+
drawn.push({ part: { path: p, matrix: M, mesh: { guid: mesh.guid, fileID: mesh.fileID }, materials: mats, castShadows: num(mr.body.m_CastShadows, 1) !== 0 }, levels });
|
|
16569
17173
|
}
|
|
16570
17174
|
}
|
|
16571
17175
|
for (const c of g.comps) {
|
|
@@ -16583,6 +17187,12 @@ var readCollider = (c, path, M) => {
|
|
|
16583
17187
|
walk(ch, m, p, false);
|
|
16584
17188
|
};
|
|
16585
17189
|
walk(root, I42(), "", true);
|
|
17190
|
+
const maxLevel = drawn.reduce((m, d) => Math.max(m, ...d.levels), 0);
|
|
17191
|
+
for (const d of drawn) {
|
|
17192
|
+
const levels = d.levels.length ? d.levels : Array.from({ length: maxLevel + 1 }, (_, i) => i);
|
|
17193
|
+
for (const lod of levels)
|
|
17194
|
+
parts.push({ ...d.part, lod });
|
|
17195
|
+
}
|
|
16586
17196
|
stats.particles += particles;
|
|
16587
17197
|
stats.prefabLights += klights.length;
|
|
16588
17198
|
if (!parts.length && !colliders.length)
|
|
@@ -17023,8 +17633,8 @@ var EPS2 = 0.000000001, rotate = (q, v) => {
|
|
|
17023
17633
|
};
|
|
17024
17634
|
|
|
17025
17635
|
// src/unity/triBudget.ts
|
|
17026
|
-
import { PropertyType } from "@gltf-transform/core";
|
|
17027
|
-
import { prune as
|
|
17636
|
+
import { PropertyType as PropertyType2 } from "@gltf-transform/core";
|
|
17637
|
+
import { prune as prune4, simplifyPrimitive as simplifyPrimitive2, weldPrimitive as weldPrimitive3 } from "@gltf-transform/functions";
|
|
17028
17638
|
import { MeshoptSimplifier as MeshoptSimplifier2 } from "meshoptimizer";
|
|
17029
17639
|
var DEFAULT_MAX_TRIS = 50000, trisOf = (p) => p.getMode() !== 4 ? 0 : Math.floor((p.getIndices()?.getCount() ?? p.getAttribute("POSITION")?.getCount() ?? 0) / 3), fitTriangleBudget = async (doc, maxTris) => {
|
|
17030
17640
|
const prims = [...new Set(doc.getRoot().listMeshes().flatMap((m) => m.listPrimitives()))].filter((p) => p.getMode() === 4 && !p.getAttribute("JOINTS_0"));
|
|
@@ -17044,7 +17654,7 @@ var DEFAULT_MAX_TRIS = 50000, trisOf = (p) => p.getMode() !== 4 ? 0 : Math.floor
|
|
|
17044
17654
|
smoothNormals(doc, p);
|
|
17045
17655
|
}
|
|
17046
17656
|
}
|
|
17047
|
-
await doc.transform(
|
|
17657
|
+
await doc.transform(prune4({ propertyTypes: [PropertyType2.ACCESSOR] }));
|
|
17048
17658
|
return { before, after: prims.reduce((n, p) => n + trisOf(p), 0) };
|
|
17049
17659
|
}, smoothNormals = (doc, p) => {
|
|
17050
17660
|
const pos = p.getAttribute("POSITION"), idx = p.getIndices();
|
|
@@ -17365,7 +17975,7 @@ var init_triplanar = __esm(() => {
|
|
|
17365
17975
|
import { basename as basename23, dirname as dirname13 } from "node:path";
|
|
17366
17976
|
import { readFileSync as readFileSync25 } from "node:fs";
|
|
17367
17977
|
import { Document as Document5 } from "@gltf-transform/core";
|
|
17368
|
-
import { mergeDocuments as mergeDocuments2, prune as
|
|
17978
|
+
import { mergeDocuments as mergeDocuments2, prune as prune5 } from "@gltf-transform/functions";
|
|
17369
17979
|
|
|
17370
17980
|
class KindGlbBuilder {
|
|
17371
17981
|
ctx;
|
|
@@ -17643,7 +18253,7 @@ class KindGlbBuilder {
|
|
|
17643
18253
|
s.dispose();
|
|
17644
18254
|
for (const n of out.imported)
|
|
17645
18255
|
n.dispose();
|
|
17646
|
-
await doc.transform(
|
|
18256
|
+
await doc.transform(prune5({ keepLeaves: false }));
|
|
17647
18257
|
const buffers = doc.getRoot().listBuffers();
|
|
17648
18258
|
if (buffers.length > 1) {
|
|
17649
18259
|
const keep = buffers[0];
|
|
@@ -17785,24 +18395,31 @@ var planLayers = async (t) => {
|
|
|
17785
18395
|
if (!t || !r.terrain)
|
|
17786
18396
|
return null;
|
|
17787
18397
|
mkdirSync6(dir, { recursive: true });
|
|
17788
|
-
const
|
|
18398
|
+
const src = t.resolution;
|
|
18399
|
+
let step = 1;
|
|
18400
|
+
if (opts.terrainRes && opts.terrainRes > 1)
|
|
18401
|
+
while ((src - 1) / step + 1 > opts.terrainRes && (src - 1) % (step * 2) === 0)
|
|
18402
|
+
step *= 2;
|
|
18403
|
+
const res = (src - 1) / step + 1;
|
|
18404
|
+
if (step > 1)
|
|
18405
|
+
log(` \uD83C\uDFD4 terrain heightfield ${src}² → ${res}² (every ${step}th sample, --terrain-res ${opts.terrainRes})`);
|
|
17789
18406
|
const heights = new Float32Array(res * res);
|
|
17790
18407
|
for (let z = 0;z < res; z++)
|
|
17791
18408
|
for (let x = 0;x < res; x++)
|
|
17792
|
-
heights[z * res + x] = t.heights[z *
|
|
18409
|
+
heights[z * res + x] = t.heights[z * step * src + (src - 1 - x * step)];
|
|
17793
18410
|
const plan = await planLayers(t);
|
|
17794
18411
|
const slot = new Map;
|
|
17795
18412
|
plan.keep.forEach((li, s) => slot.set(li, s));
|
|
17796
18413
|
for (const [lose, win] of plan.mergeInto)
|
|
17797
18414
|
slot.set(lose, slot.get(win));
|
|
17798
18415
|
const ar = t.alphaResolution, control = new Uint8Array(res * res * 4);
|
|
17799
|
-
const f = ar /
|
|
18416
|
+
const f = ar / src;
|
|
17800
18417
|
for (let z = 0;z < res; z++)
|
|
17801
18418
|
for (let x = 0;x < res; x++) {
|
|
17802
|
-
const ux = res - 1 - x;
|
|
18419
|
+
const ux = (res - 1 - x) * step, uz = z * step;
|
|
17803
18420
|
const acc = [0, 0, 0, 0];
|
|
17804
|
-
const ax0 = Math.max(0, Math.floor((ux -
|
|
17805
|
-
const az0 = Math.max(0, Math.floor((
|
|
18421
|
+
const ax0 = Math.max(0, Math.floor((ux - step / 2) * f)), ax1 = Math.min(ar - 1, Math.ceil((ux + step / 2) * f));
|
|
18422
|
+
const az0 = Math.max(0, Math.floor((uz - step / 2) * f)), az1 = Math.min(ar - 1, Math.ceil((uz + step / 2) * f));
|
|
17806
18423
|
for (let li = 0;li < t.weights.length; li++) {
|
|
17807
18424
|
const w = t.weights[li];
|
|
17808
18425
|
let s = 0;
|
|
@@ -17935,12 +18552,15 @@ var buildBvh = (tris) => {
|
|
|
17935
18552
|
}
|
|
17936
18553
|
}
|
|
17937
18554
|
return best;
|
|
17938
|
-
},
|
|
18555
|
+
}, LOD_RE2, windProfiled = (p) => {
|
|
18556
|
+
const extras = p.getMaterial()?.getExtras();
|
|
18557
|
+
return Array.isArray(extras?.lecodes?.wind);
|
|
18558
|
+
}, collectOccluders = (doc) => {
|
|
17939
18559
|
const tris = [];
|
|
17940
18560
|
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
17941
18561
|
for (const node of doc.getRoot().listNodes()) {
|
|
17942
18562
|
const mesh = node.getMesh();
|
|
17943
|
-
if (!mesh ||
|
|
18563
|
+
if (!mesh || LOD_RE2.test(node.getName()))
|
|
17944
18564
|
continue;
|
|
17945
18565
|
const m = node.getWorldMatrix();
|
|
17946
18566
|
for (const p of mesh.listPrimitives()) {
|
|
@@ -17995,7 +18615,7 @@ var buildBvh = (tris) => {
|
|
|
17995
18615
|
if (canopy > 0) {
|
|
17996
18616
|
for (const node of doc.getRoot().listNodes()) {
|
|
17997
18617
|
const mesh = node.getMesh();
|
|
17998
|
-
if (!mesh ||
|
|
18618
|
+
if (!mesh || LOD_RE2.test(node.getName()))
|
|
17999
18619
|
continue;
|
|
18000
18620
|
const m = node.getWorldMatrix();
|
|
18001
18621
|
for (const p of mesh.listPrimitives()) {
|
|
@@ -18035,7 +18655,7 @@ var buildBvh = (tris) => {
|
|
|
18035
18655
|
const n = pos.getCount();
|
|
18036
18656
|
const nrm = p.getAttribute("NORMAL");
|
|
18037
18657
|
if (canopy > 0 && crownN && nrm && p.getMaterial()?.getAlphaMode() === "MASK") {
|
|
18038
|
-
const inv =
|
|
18658
|
+
const inv = invert42(m);
|
|
18039
18659
|
const cl = inv ? [inv[0] * crown[0] + inv[4] * crown[1] + inv[8] * crown[2] + inv[12], inv[1] * crown[0] + inv[5] * crown[1] + inv[9] * crown[2] + inv[13], inv[2] * crown[0] + inv[6] * crown[1] + inv[10] * crown[2] + inv[14]] : null;
|
|
18040
18660
|
if (cl) {
|
|
18041
18661
|
const out = new Float32Array(new ArrayBuffer(n * 12)), v = [], q = [];
|
|
@@ -18061,6 +18681,7 @@ var buildBvh = (tris) => {
|
|
|
18061
18681
|
}
|
|
18062
18682
|
const old = p.getAttribute("COLOR_0");
|
|
18063
18683
|
const oldEl = old ? old.getElementSize() : 0;
|
|
18684
|
+
const authoredWind = windProfiled(p);
|
|
18064
18685
|
const out = new Float32Array(new ArrayBuffer(n * 16));
|
|
18065
18686
|
const v = [], c = [], q = [];
|
|
18066
18687
|
const cavNrm = cavReach > 0 && p.getMaterial()?.getAlphaMode() !== "MASK" ? p.getAttribute("NORMAL") : null;
|
|
@@ -18096,7 +18717,7 @@ var buildBvh = (tris) => {
|
|
|
18096
18717
|
cav = 1 - occ / cavRays.length;
|
|
18097
18718
|
}
|
|
18098
18719
|
let wind;
|
|
18099
|
-
if (old && oldEl === 4) {
|
|
18720
|
+
if (old && oldEl === 4 && authoredWind) {
|
|
18100
18721
|
old.getElement(i, c);
|
|
18101
18722
|
wind = c[3];
|
|
18102
18723
|
} else {
|
|
@@ -18124,7 +18745,7 @@ var buildBvh = (tris) => {
|
|
|
18124
18745
|
q[0] = x / l;
|
|
18125
18746
|
q[1] = y / l;
|
|
18126
18747
|
q[2] = z / l;
|
|
18127
|
-
},
|
|
18748
|
+
}, invert42 = (m) => {
|
|
18128
18749
|
const a = Array.from(m), r = new Array(16);
|
|
18129
18750
|
const [a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23, a30, a31, a32, a33] = a;
|
|
18130
18751
|
const b00 = a00 * a11 - a01 * a10, b01 = a00 * a12 - a02 * a10, b02 = a00 * a13 - a03 * a10, b03 = a01 * a12 - a02 * a11;
|
|
@@ -18153,14 +18774,14 @@ var buildBvh = (tris) => {
|
|
|
18153
18774
|
return r;
|
|
18154
18775
|
};
|
|
18155
18776
|
var init_vertexAo = __esm(() => {
|
|
18156
|
-
|
|
18777
|
+
LOD_RE2 = /_LOD([1-9]\d*)$/i;
|
|
18157
18778
|
});
|
|
18158
18779
|
|
|
18159
18780
|
// src/unity/lecodesLevel.ts
|
|
18160
18781
|
import { createHash as createHash3 } from "node:crypto";
|
|
18161
18782
|
import { existsSync as existsSync25, mkdirSync as mkdirSync7, readFileSync as readFileSync27, writeFileSync as writeFileSync13 } from "node:fs";
|
|
18162
18783
|
import { dirname as dirname14, join as join13, relative, resolve as resolve6 } from "node:path";
|
|
18163
|
-
var BUILDER_VERSION =
|
|
18784
|
+
var BUILDER_VERSION = 20, v3 = (v) => `[${v.map(r3).join(", ")}]`, ident = (s) => {
|
|
18164
18785
|
const x = s.replace(/[^\w$]/g, "_");
|
|
18165
18786
|
return /^\d/.test(x) ? `_${x}` : x;
|
|
18166
18787
|
}, transformFields = (pos, rot, scale) => {
|
|
@@ -18238,7 +18859,7 @@ var BUILDER_VERSION = 12, v3 = (v) => `[${v.map(r3).join(", ")}]`, ident = (s) =
|
|
|
18238
18859
|
if (foliage)
|
|
18239
18860
|
model.push("foliage: true");
|
|
18240
18861
|
if (noLightmap)
|
|
18241
|
-
model.push("
|
|
18862
|
+
model.push("static: false");
|
|
18242
18863
|
if (modelAspects.length)
|
|
18243
18864
|
model.push(`aspects: [${modelAspects.join(", ")}]`);
|
|
18244
18865
|
return `// Generated by \`lecodes assets scene\` from ${source}${k.prefabPath ? ` (${k.prefabPath.replace(/\\/g, "/").replace(/^.*\/Assets\//, "Assets/")})` : ""}.
|
|
@@ -18266,7 +18887,7 @@ ${[` model: { ${model.join(", ")} },`, ...children].join(`
|
|
|
18266
18887
|
const glb = join13(assetDir, `${k.name}.glb`);
|
|
18267
18888
|
const maxTris = opts.maxTris ?? DEFAULT_MAX_TRIS;
|
|
18268
18889
|
const foliage = decideFoliage(k, builder.materialsOf(k), opts);
|
|
18269
|
-
const hash = createHash3("sha1").update(JSON.stringify([BUILDER_VERSION, k.key, opts.maxSize, opts.ktx2, maxTris, ...foliage ? [] : ["nocolor"]])).digest("hex").slice(0, 16);
|
|
18890
|
+
const hash = createHash3("sha1").update(JSON.stringify([BUILDER_VERSION, k.key, opts.maxSize, opts.ktx2, maxTris, ...foliage ? [] : ["nocolor"], ...foliage && opts.impostor ? ["impostor", opts.impostor.cols, opts.impostor.rows, opts.impostor.px ?? 0, opts.impostor.level ?? 2] : []])).digest("hex").slice(0, 16);
|
|
18270
18891
|
if (!opts.force && existsSync25(glb) && manifest[k.name] === hash) {
|
|
18271
18892
|
reused++;
|
|
18272
18893
|
continue;
|
|
@@ -18283,6 +18904,9 @@ ${[` model: { ${model.join(", ")} },`, ...children].join(`
|
|
|
18283
18904
|
}
|
|
18284
18905
|
}
|
|
18285
18906
|
if (foliage) {
|
|
18907
|
+
for (const m of doc.getRoot().listMaterials())
|
|
18908
|
+
if (m.getAlphaMode() === "MASK")
|
|
18909
|
+
m.setDoubleSided(true);
|
|
18286
18910
|
await Promise.resolve().then(() => init_vertexAo());
|
|
18287
18911
|
const ao = bakeVertexAo(doc);
|
|
18288
18912
|
if (ao.vertices)
|
|
@@ -18297,6 +18921,16 @@ ${[` model: { ${model.join(", ")} },`, ...children].join(`
|
|
|
18297
18921
|
const cut = await fitTriangleBudget(doc, maxTris);
|
|
18298
18922
|
if (cut)
|
|
18299
18923
|
log(` ✂ ${k.name}: ${cut.before.toLocaleString("en")} → ${cut.after.toLocaleString("en")} triangles (--max-tris ${maxTris})`);
|
|
18924
|
+
if (foliage && opts.impostor) {
|
|
18925
|
+
await Promise.resolve().then(() => init_impostor());
|
|
18926
|
+
const box = impostorBounds(doc);
|
|
18927
|
+
const height = box ? box.max[1] - box.min[1] : 0;
|
|
18928
|
+
if (height >= 4) {
|
|
18929
|
+
const imp = await bakeImpostor(doc, { ...opts.impostor, log });
|
|
18930
|
+
if (imp)
|
|
18931
|
+
log(` \uD83C\uDF32 ${k.name}: impostor ${impostorLabel(imp)} · ${imp.node} · r ${imp.radius.toFixed(2)} m · ${imp.triangles} triangles in ${imp.ms} ms`);
|
|
18932
|
+
}
|
|
18933
|
+
}
|
|
18300
18934
|
await applyPlan(doc, await analyzeDoc(doc, doctor), doctor);
|
|
18301
18935
|
writeFileSync13(glb, await makeIO().writeBinary(doc));
|
|
18302
18936
|
manifest[k.name] = hash;
|
|
@@ -18349,7 +18983,7 @@ ${[` model: { ${model.join(", ")} },`, ...children].join(`
|
|
|
18349
18983
|
}
|
|
18350
18984
|
log(` ${describeFoliage(foliageKinds, opts)}`);
|
|
18351
18985
|
if (opts.noLightmap)
|
|
18352
|
-
log(` \uD83D\uDD26
|
|
18986
|
+
log(` \uD83D\uDD26 static: false on ${unbakedKinds.length} kind(s)${unbakedKinds.length ? `: ${unbakedKinds.slice(0, 8).join(", ")}${unbakedKinds.length > 8 ? " …" : ""}` : ` (nothing matched /${opts.noLightmap.source}/)`}`);
|
|
18353
18987
|
}, terrainNode = (r, terrain, sdir) => {
|
|
18354
18988
|
const data = r.terrainData;
|
|
18355
18989
|
const layers = terrain.layers.map((f, i) => {
|
|
@@ -19577,6 +20211,28 @@ var resolveInputs = (args, ext = /\.glb$/i, skipGenerated = true) => {
|
|
|
19577
20211
|
}
|
|
19578
20212
|
return [...new Set(out)];
|
|
19579
20213
|
};
|
|
20214
|
+
var impostorFlag = (v, level) => {
|
|
20215
|
+
if (v === undefined)
|
|
20216
|
+
return;
|
|
20217
|
+
if (level !== undefined && (!Number.isInteger(level) || level < 1 || level > 3))
|
|
20218
|
+
throw usageError(`--impostor-level wants 1, 2 or 3 — got "${level}"`);
|
|
20219
|
+
const s = v.trim() || "12x3";
|
|
20220
|
+
const bad = (why) => usageError(`--impostor wants <A>x<E>[@<px>] (azimuths ≥ 3, rows 1..8) or oct<N>[@<px>] (N 2..64), px 32..1024 a multiple of 4 — e.g. 12x3, 8x2@256, oct8@128 — got "${v}" (${why})`);
|
|
20221
|
+
const grid = /^(\d+)x(\d+)(?:@(\d+))?$/i.exec(s), oct = /^oct(\d+)(?:@(\d+))?$/i.exec(s);
|
|
20222
|
+
if (!grid && !oct)
|
|
20223
|
+
throw bad("not either form");
|
|
20224
|
+
const cols = Number(grid ? grid[1] : oct[1]), rows = grid ? Number(grid[2]) : 0;
|
|
20225
|
+
const pxs = grid ? grid[3] : oct[2], px = pxs ? Number(pxs) : undefined;
|
|
20226
|
+
if (grid && (cols < 3 || cols > 64))
|
|
20227
|
+
throw bad("azimuths 3..64");
|
|
20228
|
+
if (grid && (rows < 1 || rows > 8))
|
|
20229
|
+
throw bad("rows 1..8");
|
|
20230
|
+
if (oct && (cols < 2 || cols > 64))
|
|
20231
|
+
throw bad("N 2..64");
|
|
20232
|
+
if (px !== undefined && (px < 32 || px > 1024 || px % 4 !== 0))
|
|
20233
|
+
throw bad("px 32..1024, a multiple of 4");
|
|
20234
|
+
return { cols, rows, px, level };
|
|
20235
|
+
};
|
|
19580
20236
|
var regexFlag = (v) => v ? new RegExp(v, "i") : undefined;
|
|
19581
20237
|
|
|
19582
20238
|
// src/commands/avatar.ts
|
|
@@ -19872,7 +20528,9 @@ var doctor_default = defineCommand({
|
|
|
19872
20528
|
dump: str2("write the file's textures out instead (default: <in>-textures/)", { value: "dir", bare: "" }),
|
|
19873
20529
|
...doctorFlags,
|
|
19874
20530
|
lod: str2("add mesh LOD levels (<name>_LOD1… siblings at these triangle ratios, same skin; >= 2000-tri meshes only) — creator-gl shows one by screen size (docs/lod-plan.md)", { value: "0.5,0.2", group: "LOD" }),
|
|
19875
|
-
"lod-bones": bool("fold fingers / toes / twists onto their parents in the last level's skin")
|
|
20531
|
+
"lod-bones": bool("fold fingers / toes / twists onto their parents in the last level's skin"),
|
|
20532
|
+
impostor: str2("bake the level-0 geometry into an impostor atlas (albedo + hull normal / AO) and add it as a LOD level, one camera-facing quad (<name>_LOD<k>, k = --impostor-level, never past the first missing level; the levels from k up are replaced). PNG / JPEG base colours only — run it with --ktx2, not after it; the atlases skip --max-size. <A>x<E>[@<px>] = A azimuths × E elevation rows (0°, 90°/E, …; 8x2@256), oct<N>[@<px>] = the N×N hemi-octahedron (steep views too); bare = 12x3; px = a frame's pixels (default 256 for a plant ≥ 8 m, 128 under)", { value: "<A>x<E>[@px]|oct<N>[@px]", bare: "12x3" }),
|
|
20533
|
+
"impostor-level": int("the LOD slot the quad takes (default 2: shown under 0.12 of the viewport height; 1 = under 0.30)")
|
|
19876
20534
|
},
|
|
19877
20535
|
examples: [
|
|
19878
20536
|
"lecodes assets doctor hero.glb",
|
|
@@ -19888,7 +20546,8 @@ var doctor_default = defineCommand({
|
|
|
19888
20546
|
await Promise.resolve().then(() => init_validateGlb());
|
|
19889
20547
|
const useKtx2 = wantsKtx2(flags);
|
|
19890
20548
|
const lod = (flags.lod ?? "").split(",").map(Number).filter((n) => n > 0 && n < 1);
|
|
19891
|
-
const
|
|
20549
|
+
const impostor = impostorFlag(flags.impostor, flags["impostor-level"]);
|
|
20550
|
+
const doFix = wantsDoctorFix(flags) || flags["fix-metallic"] || lod.length > 0 || !!impostor;
|
|
19892
20551
|
const opts = { ...doctorOptions(flags), lod, lodBones: flags["lod-bones"] };
|
|
19893
20552
|
const ktxEncoder = useKtx2 ? findKtxEncoder() : null;
|
|
19894
20553
|
if (useKtx2) {
|
|
@@ -19913,6 +20572,21 @@ var doctor_default = defineCommand({
|
|
|
19913
20572
|
await dumpTextures(doc, flags.dump && files.length === 1 ? flags.dump : input.replace(/\.glb$/i, "-textures"));
|
|
19914
20573
|
continue;
|
|
19915
20574
|
}
|
|
20575
|
+
if (impostor) {
|
|
20576
|
+
await Promise.resolve().then(() => init_impostor());
|
|
20577
|
+
try {
|
|
20578
|
+
const imp = await bakeImpostor(doc, { ...impostor, log });
|
|
20579
|
+
if (imp)
|
|
20580
|
+
log(` impostor: ${imp.node} · ${impostorLabel(imp)} · r ${imp.radius.toFixed(2)} m · ${imp.triangles} triangles in ${imp.ms} ms`);
|
|
20581
|
+
else
|
|
20582
|
+
log(` impostor: no level-0 triangles in ${input}`);
|
|
20583
|
+
} catch (e) {
|
|
20584
|
+
console.error(`
|
|
20585
|
+
✗ ${input}: ${e.message}`);
|
|
20586
|
+
exitCode = 1;
|
|
20587
|
+
continue;
|
|
20588
|
+
}
|
|
20589
|
+
}
|
|
19916
20590
|
const plan = await analyzeDoc(doc, opts);
|
|
19917
20591
|
const rep = reportPlan(plan);
|
|
19918
20592
|
log(`
|
|
@@ -20554,6 +21228,8 @@ var scene_default = defineCommand({
|
|
|
20554
21228
|
"no-shadows": str2("kinds whose name matches take 'castShadows: false': a pack's BACKGROUND MOUNTAINS are 2 km meshes a kilometre out that Unity's shadow distance never reaches, and here their shadow lies across the whole map", { value: "<re>" }),
|
|
20555
21229
|
"no-colliders": bool("write NO colliders: the kinds are pure visuals, nothing gets a Physics body. For a viewer, or a level whose 78 000 placements would otherwise be 20 000 Jolt bodies before the game adds one of its own"),
|
|
20556
21230
|
"max-tris": int("triangles a kind may have before the import simplifies it (default 50000, 0 = no cap): a photogrammetry scan otherwise arrives as a million-triangle prop AND mesh collider"),
|
|
21231
|
+
impostor: str2("foliage kinds ≥ 4 m tall get their far LOD level as ONE camera-facing quad over an atlas of views (albedo + hull normal / AO), replacing the pack's billboard; the atlases skip --max-size. <A>x<E>[@<px>] = A azimuths × E elevation rows (0°, 90°/E, …; 8x2@256), oct<N>[@<px>] = the N×N hemi-octahedron (steep views too); bare = 12x3; px = a frame's pixels (default 256 for a plant ≥ 8 m, 128 under)", { value: "<A>x<E>[@px]|oct<N>[@px]", bare: "12x3" }),
|
|
21232
|
+
"impostor-level": int("the LOD slot the quad takes (default 2: shown under 0.12 of the viewport height; 1 = under 0.30, the pack's LOD1 goes; never past the first missing level)"),
|
|
20557
21233
|
"no-trim-cards": bool("skip the alpha-card trim the import otherwise runs (it can empty a kind whose decal atlas is mostly clear)"),
|
|
20558
21234
|
filter: str2("keep only instances whose kind name / scene group / node name matches", { value: "<re>", group: "Subset" }),
|
|
20559
21235
|
"drop-at": str2(`leave OUT the placements standing at these points ("x,y,z;x,y,z", the lecodes frame the scene file prints, within 10 cm): the one instance of a kind the game moves while its twins stay put — the range's five lane targets, which Unity names like the sixth on a shelf`, { value: "<x,y,z;…>" }),
|
|
@@ -20561,6 +21237,7 @@ var scene_default = defineCommand({
|
|
|
20561
21237
|
group: str2("only instances under a scene group whose path contains this", { value: "name" }),
|
|
20562
21238
|
region: str2("only instances inside this Unity X/Z rectangle", { value: "x0,z0,x1,z1" }),
|
|
20563
21239
|
"no-trees": bool("skip the terrain's painted trees"),
|
|
21240
|
+
"terrain-res": num2("the heightfield's largest resolution, samples a side — a finer Unity heightmap is read every k-th sample (Chernobyl's 4097² over 500 m: 12 cm cells, 117 MB, 33 M navmesh triangles; 1025 = half-metre cells; default 1025)", { value: "<n>" }),
|
|
20564
21241
|
"no-details": bool("skip the terrain's detail meshes"),
|
|
20565
21242
|
"include-inactive": bool("import inactive GameObjects too"),
|
|
20566
21243
|
handedness: str2("how Unity's left-handed frame is undone (default x: −x, (x, −y, −z, w))", { value: "x|none", group: "Source" }),
|
|
@@ -20616,6 +21293,8 @@ var scene_default = defineCommand({
|
|
|
20616
21293
|
noColliders: flags["no-colliders"],
|
|
20617
21294
|
noShadows: regexFlag(flags["no-shadows"]),
|
|
20618
21295
|
maxTris: flags["max-tris"],
|
|
21296
|
+
impostor: impostorFlag(flags.impostor, flags["impostor-level"]),
|
|
21297
|
+
terrainRes: flags["terrain-res"] ?? 1025,
|
|
20619
21298
|
command: "lecodes assets scene " + argv.map((a) => /[\s"'|<>^&();]/.test(a) ? `"${a.replace(/"/g, "\\\"")}"` : a).join(" ")
|
|
20620
21299
|
});
|
|
20621
21300
|
log(`\uD83D\uDCBE ${res.scene} — ${res.kinds} kinds (${res.converted} converted, ${res.reused} up to date), ${r.instances.length} placements, ${seconds(t0)}`);
|