lecodes-assets 2.4.10 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
- const baseColor = um.colors._BaseColor ?? um.colors._Color;
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 && "go" in rc && !out.has(rc))
15411
- out.set(rc, level);
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
- parts.push({ path: p, matrix: M, mesh: { guid: mesh.guid, fileID: mesh.fileID }, materials: mats, lod: lods.get(mr) ?? lodSuffix(goName(g)), castShadows: num(mr.body.m_CastShadows, 1) !== 0 });
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 prune3, simplifyPrimitive as simplifyPrimitive2, weldPrimitive as weldPrimitive3 } from "@gltf-transform/functions";
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(prune3({ propertyTypes: [PropertyType.ACCESSOR] }));
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 prune4 } from "@gltf-transform/functions";
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(prune4({ keepLeaves: false }));
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 res = t.resolution;
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 * res + (res - 1 - x)];
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 / res;
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 - 0.5) * f)), ax1 = Math.min(ar - 1, Math.ceil((ux + 0.5) * f));
17805
- const az0 = Math.max(0, Math.floor((z - 0.5) * f)), az1 = Math.min(ar - 1, Math.ceil((z + 0.5) * f));
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
- }, LOD_RE, collectOccluders = (doc) => {
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 || LOD_RE.test(node.getName()))
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 || LOD_RE.test(node.getName()))
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 = invert4(m);
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
- }, invert4 = (m) => {
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
- LOD_RE = /_LOD([1-9]\d*)$/i;
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 = 12, v3 = (v) => `[${v.map(r3).join(", ")}]`, ident = (s) => {
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("lightmap: false");
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 lightmap: false on ${unbakedKinds.length} kind(s)${unbakedKinds.length ? `: ${unbakedKinds.slice(0, 8).join(", ")}${unbakedKinds.length > 8 ? " …" : ""}` : ` (nothing matched /${opts.noLightmap.source}/)`}`);
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 doFix = wantsDoctorFix(flags) || flags["fix-metallic"] || lod.length > 0;
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)}`);