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/cli.js CHANGED
@@ -4205,7 +4205,13 @@ var SLOTS, pick = (m, names) => {
4205
4205
  slots.push(`base=${nameOf(baseG)}`);
4206
4206
  }
4207
4207
  }
4208
- const baseColor = um.colors._BaseColor ?? um.colors._Color;
4208
+ let baseColor = um.colors._BaseColor ?? um.colors._Color;
4209
+ const tilling = um.colors._Color_Tilling;
4210
+ if (tilling && um.floats._Tilling_Color !== undefined) {
4211
+ const c = baseColor ?? [1, 1, 1, 1];
4212
+ baseColor = [c[0] + 0.5 * tilling[0], c[1] + 0.5 * tilling[1], c[2] + 0.5 * tilling[2], c[3]];
4213
+ slots.push(`base×(_Color + _Color_Tilling/2)`);
4214
+ }
4209
4215
  if (baseColor)
4210
4216
  gm.setBaseColorFactor([baseColor[0], baseColor[1], baseColor[2], gm.getBaseColorFactor()[3]]);
4211
4217
  const live = (names) => names.map((n) => um.textures[n]).find((g) => g && (g.startsWith(PATH_GUID) || resolver.resolve(g))) ?? null;
@@ -6896,7 +6902,7 @@ async function planTexture(tex, root, maxSize = DEFAULT_MAX_SIZE, forceFmt) {
6896
6902
  } catch {
6897
6903
  return null;
6898
6904
  }
6899
- const resize = Math.max(width, height) > maxSize;
6905
+ const resize = !isImpostorAtlas(tex) && Math.max(width, height) > maxSize;
6900
6906
  let desired = forceFmt ?? (hasAlpha ? "png" : "jpeg");
6901
6907
  if (desired === "jpeg" && hasAlpha)
6902
6908
  desired = "png";
@@ -6939,7 +6945,7 @@ async function applyTexturePlan(plan, maxSize = DEFAULT_MAX_SIZE) {
6939
6945
  plan.afterBytes = out.length;
6940
6946
  }
6941
6947
  }
6942
- var DEFAULT_MAX_SIZE = 2048, HEAVY_PNG_MIN_DIM = 1024;
6948
+ var DEFAULT_MAX_SIZE = 2048, HEAVY_PNG_MIN_DIM = 1024, isImpostorAtlas = (tex) => !!tex.getExtras()?.lecodes?.impostor;
6943
6949
  var init_transcodeTextures = () => {};
6944
6950
 
6945
6951
  // src/doctor/transcodeKtx2.ts
@@ -7023,9 +7029,9 @@ async function planKtx2(tex, root, maxSize = DEFAULT_MAX_SIZE, policy = "hq") {
7023
7029
  } catch {
7024
7030
  return null;
7025
7031
  }
7026
- const codec = policy === "uastc" ? "uastc" : isNormal || isData ? "uastc" : "etc1s";
7032
+ const codec = policy === "uastc" ? "uastc" : isNormal || isData || isImpostorAtlas(tex) ? "uastc" : "etc1s";
7027
7033
  const oetf = isNormal || isData ? "linear" : "srgb";
7028
- const resize = Math.max(width, height) > maxSize;
7034
+ const resize = !isImpostorAtlas(tex) && Math.max(width, height) > maxSize;
7029
7035
  return {
7030
7036
  tex,
7031
7037
  name: tex.getName() || "?",
@@ -7083,7 +7089,7 @@ async function encodeOne(plan, enc, maxSize) {
7083
7089
  const snap4 = (n) => Math.max(4, Math.round(n / 4) * 4);
7084
7090
  let { width: tw, height: th } = plan;
7085
7091
  const m = Math.max(tw, th);
7086
- if (m > maxSize) {
7092
+ if (plan.resize && m > maxSize) {
7087
7093
  const s = maxSize / m;
7088
7094
  tw = tw * s;
7089
7095
  th = th * s;
@@ -7424,6 +7430,8 @@ function planCardTrim(doc, opts) {
7424
7430
  const mat = prim.getMaterial();
7425
7431
  if (!mat || mat.getAlphaMode() !== "MASK" || prim.getMode() !== 4)
7426
7432
  continue;
7433
+ if (mat.getExtras()?.lecodes?.impostor !== undefined)
7434
+ continue;
7427
7435
  if (!prim.getAttribute("POSITION") || !prim.getAttribute("TEXCOORD_0"))
7428
7436
  continue;
7429
7437
  const tex = mat.getBaseColorTexture();
@@ -9815,6 +9823,586 @@ var init_probe2 = __esm(() => {
9815
9823
  init_mergeSkins();
9816
9824
  });
9817
9825
 
9826
+ // src/doctor/impostor.ts
9827
+ import { Logger as Logger3, PropertyType } from "@gltf-transform/core";
9828
+ import { prune as prune3 } from "@gltf-transform/functions";
9829
+ 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) => {
9830
+ const a = u * 2 - 1, b = v * 2 - 1;
9831
+ const x = (a + b) / 2, z = (a - b) / 2, y = 1 - Math.abs(x) - Math.abs(z);
9832
+ const l = Math.hypot(x, y, z) || 1;
9833
+ return [x / l, y / l, z / l];
9834
+ }, normalEncode = (n) => {
9835
+ const s = Math.abs(n[0]) + Math.abs(n[1]) + Math.abs(n[2]) || 1;
9836
+ let px = n[0] / s, py = n[2] / s;
9837
+ if (n[1] / s < 0) {
9838
+ const sx = px >= 0 ? 1 : -1, sy = py >= 0 ? 1 : -1;
9839
+ const ox = (1 - Math.abs(py)) * sx, oy = (1 - Math.abs(px)) * sy;
9840
+ px = ox;
9841
+ py = oy;
9842
+ }
9843
+ return [px * 0.5 + 0.5, py * 0.5 + 0.5];
9844
+ }, frameBasis = (d) => {
9845
+ const ref = Math.abs(d[1]) < 0.999 ? [0, 1, 0] : [0, 0, -1];
9846
+ 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];
9847
+ const l = Math.hypot(rx, ry, rz) || 1;
9848
+ rx /= l;
9849
+ ry /= l;
9850
+ rz /= l;
9851
+ return { right: [rx, ry, rz], up: [d[1] * rz - d[2] * ry, d[2] * rx - d[0] * rz, d[0] * ry - d[1] * rx] };
9852
+ }, gridDirection = (i, k, A, E) => {
9853
+ const az = i * 2 * Math.PI / A, el = k * (Math.PI / 2) / E;
9854
+ return [Math.cos(el) * Math.sin(az), Math.sin(el), Math.cos(el) * Math.cos(az)];
9855
+ }, 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) => {
9856
+ const frames = [];
9857
+ for (let k = 0;k < E; k++)
9858
+ for (let i = 0;i < A; i++)
9859
+ frames.push(frame(i, k, gridDirection(i, k, A, E), P));
9860
+ return { cols: A, rows: E, px: P, width: A * P, height: E * P, frames };
9861
+ }, octLayout = (N, P) => {
9862
+ const frames = [];
9863
+ for (let j = 0;j < N; j++)
9864
+ for (let i = 0;i < N; i++)
9865
+ frames.push(frame(i, j, octDecode(i / (N - 1), j / (N - 1)), P));
9866
+ return { cols: N, rows: 0, px: P, width: N * P, height: N * P, frames };
9867
+ }, isLevel0 = (n) => !!n.getMesh() && !LOD_RE.test(n.getName()), impostorBounds = (doc) => {
9868
+ const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
9869
+ const v = [];
9870
+ for (const node of doc.getRoot().listNodes()) {
9871
+ if (!isLevel0(node))
9872
+ continue;
9873
+ const m = node.getWorldMatrix();
9874
+ for (const p of node.getMesh().listPrimitives()) {
9875
+ const pos = p.getAttribute("POSITION");
9876
+ if (!pos || p.getMode() !== 4)
9877
+ continue;
9878
+ for (let i = 0;i < pos.getCount(); i++) {
9879
+ pos.getElement(i, v);
9880
+ 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];
9881
+ if (x < min[0])
9882
+ min[0] = x;
9883
+ if (y < min[1])
9884
+ min[1] = y;
9885
+ if (z < min[2])
9886
+ min[2] = z;
9887
+ if (x > max[0])
9888
+ max[0] = x;
9889
+ if (y > max[1])
9890
+ max[1] = y;
9891
+ if (z > max[2])
9892
+ max[2] = z;
9893
+ }
9894
+ }
9895
+ }
9896
+ return min[0] <= max[0] ? { min, max } : null;
9897
+ }, makeSlot = async (mat, images, log) => {
9898
+ const f = mat?.getBaseColorFactor() ?? [1, 1, 1, 1];
9899
+ const mask = mat?.getAlphaMode() === "MASK";
9900
+ 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 };
9901
+ const tex = mat?.getBaseColorTexture();
9902
+ if (!mat || !tex)
9903
+ return slot;
9904
+ if ((tex.getMimeType() || "").includes("ktx2"))
9905
+ throw new Error(`impostor: the base colour of material "${mat.getName()}" is already KTX2 — bake impostors before the KTX2 encode (from the source textures)`);
9906
+ const info = mat.getBaseColorTextureInfo();
9907
+ slot.set = info?.getTexCoord() ?? 0;
9908
+ slot.wrapS = info?.getWrapS() ?? REPEAT;
9909
+ slot.wrapT = info?.getWrapT() ?? REPEAT;
9910
+ let job = images.get(tex);
9911
+ if (!job) {
9912
+ job = (async () => {
9913
+ const bytes = tex.getImage();
9914
+ const img = bytes ? await decodeImage(bytes, tex.getURI() || tex.getMimeType() || "texture.png") : null;
9915
+ return img ? { img: img.data, w: img.width, h: img.height } : null;
9916
+ })();
9917
+ images.set(tex, job);
9918
+ }
9919
+ const img = await job;
9920
+ if (!img) {
9921
+ log(` ⚠ impostor: the base colour of "${mat.getName()}" could not be decoded — drawn as its factor, opaque`);
9922
+ slot.mask = false;
9923
+ return slot;
9924
+ }
9925
+ slot.img = img.img;
9926
+ slot.w = img.w;
9927
+ slot.h = img.h;
9928
+ return slot;
9929
+ }, collectLevel0 = async (doc, log) => {
9930
+ const pos = [], nrm = [], uv = [], ao = [];
9931
+ const tri = [], triSlot = [], face = [];
9932
+ const slots = [], slotOf = new Map;
9933
+ const images = new Map;
9934
+ const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
9935
+ const trisOfNode = new Map;
9936
+ const v = [];
9937
+ for (const node of doc.getRoot().listNodes()) {
9938
+ if (!isLevel0(node))
9939
+ continue;
9940
+ const m = node.getWorldMatrix();
9941
+ 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];
9942
+ const det = ax * (by * cz - bz * cy) - bx * (ay * cz - az * cy) + cx * (ay * bz - az * by);
9943
+ const sg = det < 0 ? -1 : 1;
9944
+ const n0 = [(by * cz - bz * cy) * sg, (bz * cx - bx * cz) * sg, (bx * cy - by * cx) * sg];
9945
+ const n1 = [(cy * az - cz * ay) * sg, (cz * ax - cx * az) * sg, (cx * ay - cy * ax) * sg];
9946
+ const n2 = [(ay * bz - az * by) * sg, (az * bx - ax * bz) * sg, (ax * by - ay * bx) * sg];
9947
+ let nodeTris = 0;
9948
+ for (const p of node.getMesh().listPrimitives()) {
9949
+ const P = p.getAttribute("POSITION");
9950
+ if (!P || p.getMode() !== 4)
9951
+ continue;
9952
+ const mat = p.getMaterial();
9953
+ let si = slotOf.get(mat);
9954
+ if (si === undefined) {
9955
+ si = slots.length;
9956
+ slots.push(await makeSlot(mat, images, log));
9957
+ slotOf.set(mat, si);
9958
+ }
9959
+ const slot = slots[si];
9960
+ const N = p.getAttribute("NORMAL"), T = p.getAttribute(`TEXCOORD_${slot.set}`), C = p.getAttribute("COLOR_0");
9961
+ const base = pos.length / 3;
9962
+ const count = P.getCount();
9963
+ for (let i = 0;i < count; i++) {
9964
+ P.getElement(i, v);
9965
+ 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];
9966
+ pos.push(x, y, z);
9967
+ if (x < min[0])
9968
+ min[0] = x;
9969
+ if (y < min[1])
9970
+ min[1] = y;
9971
+ if (z < min[2])
9972
+ min[2] = z;
9973
+ if (x > max[0])
9974
+ max[0] = x;
9975
+ if (y > max[1])
9976
+ max[1] = y;
9977
+ if (z > max[2])
9978
+ max[2] = z;
9979
+ if (N) {
9980
+ N.getElement(i, v);
9981
+ 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];
9982
+ const l = Math.hypot(nx, ny, nz) || 1;
9983
+ nrm.push(nx / l, ny / l, nz / l);
9984
+ } else
9985
+ nrm.push(0, 0, 0);
9986
+ if (T) {
9987
+ T.getElement(i, v);
9988
+ uv.push(v[0], v[1]);
9989
+ } else
9990
+ uv.push(0, 0);
9991
+ if (C) {
9992
+ C.getElement(i, v);
9993
+ ao.push(v[0]);
9994
+ } else
9995
+ ao.push(1);
9996
+ }
9997
+ const idx = p.getIndices();
9998
+ const n = idx ? idx.getCount() : count;
9999
+ for (let k = 0;k + 2 < n; k += 3) {
10000
+ 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);
10001
+ tri.push(a, b, c);
10002
+ triSlot.push(si);
10003
+ 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];
10004
+ 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];
10005
+ const fx = (e1y * e2z - e1z * e2y) * sg, fy = (e1z * e2x - e1x * e2z) * sg, fz = (e1x * e2y - e1y * e2x) * sg;
10006
+ const fl = Math.hypot(fx, fy, fz) || 1;
10007
+ face.push(fx / fl, fy / fl, fz / fl);
10008
+ nodeTris++;
10009
+ }
10010
+ }
10011
+ trisOfNode.set(node, nodeTris);
10012
+ }
10013
+ if (!tri.length)
10014
+ return null;
10015
+ let root = null, best = -1;
10016
+ for (const [node, n] of trisOfNode) {
10017
+ let nested = false;
10018
+ for (let a = node.getParentNode();a; a = a.getParentNode())
10019
+ if (trisOfNode.has(a)) {
10020
+ nested = true;
10021
+ break;
10022
+ }
10023
+ if (!nested && n > best) {
10024
+ best = n;
10025
+ root = node;
10026
+ }
10027
+ }
10028
+ return {
10029
+ pos: Float32Array.from(pos),
10030
+ nrm: Float32Array.from(nrm),
10031
+ uv: Float32Array.from(uv),
10032
+ ao: Float32Array.from(ao),
10033
+ tri: Uint32Array.from(tri),
10034
+ triSlot: Uint16Array.from(triSlot),
10035
+ face: Float32Array.from(face),
10036
+ slots,
10037
+ min,
10038
+ max,
10039
+ root
10040
+ };
10041
+ }, SRGB_TO_LINEAR2, toSrgb8 = (x) => {
10042
+ const c = x <= 0 ? 0 : x >= 1 ? 1 : x <= 0.0031308 ? x * 12.92 : 1.055 * Math.pow(x, 1 / 2.4) - 0.055;
10043
+ return Math.round(c * 255);
10044
+ }, wrapI = (i, n, mode) => {
10045
+ if (mode === CLAMP)
10046
+ return i < 0 ? 0 : i >= n ? n - 1 : i;
10047
+ if (mode === MIRROR) {
10048
+ const p = 2 * n, k = (i % p + p) % p;
10049
+ return k < n ? k : p - 1 - k;
10050
+ }
10051
+ return (i % n + n) % n;
10052
+ }, tapI, tapW, taps = (s, u, v) => {
10053
+ const fx = u * s.w - 0.5, fy = v * s.h - 0.5;
10054
+ const x0 = Math.floor(fx), y0 = Math.floor(fy), tx = fx - x0, ty = fy - y0;
10055
+ 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);
10056
+ tapI[0] = (ya * s.w + xa) * 4;
10057
+ tapI[1] = (ya * s.w + xb) * 4;
10058
+ tapI[2] = (yb * s.w + xa) * 4;
10059
+ tapI[3] = (yb * s.w + xb) * 4;
10060
+ tapW[0] = (1 - tx) * (1 - ty);
10061
+ tapW[1] = tx * (1 - ty);
10062
+ tapW[2] = (1 - tx) * ty;
10063
+ tapW[3] = tx * ty;
10064
+ }, alphaAt = (s, u, v) => {
10065
+ if (!s.img)
10066
+ return s.f[3];
10067
+ taps(s, u, v);
10068
+ const d = s.img;
10069
+ 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];
10070
+ }, DILATE_PASSES = 16, dilate = (base, data, covered, W, H, frameOf, nFrames) => {
10071
+ let cur = covered.slice();
10072
+ for (let pass = 0;pass < DILATE_PASSES; pass++) {
10073
+ const next = cur.slice();
10074
+ let changed = 0;
10075
+ for (let y = 0;y < H; y++) {
10076
+ for (let x = 0;x < W; x++) {
10077
+ const i = y * W + x;
10078
+ if (cur[i])
10079
+ continue;
10080
+ const f = frameOf[i];
10081
+ let n = 0, r = 0, g = 0, b = 0, d0 = 0, d1 = 0, d2 = 0, d3 = 0;
10082
+ for (let oy = -1;oy <= 1; oy++) {
10083
+ const yy = y + oy;
10084
+ if (yy < 0 || yy >= H)
10085
+ continue;
10086
+ for (let ox = -1;ox <= 1; ox++) {
10087
+ const xx = x + ox;
10088
+ if (xx < 0 || xx >= W)
10089
+ continue;
10090
+ const k = yy * W + xx;
10091
+ if (!cur[k] || frameOf[k] !== f)
10092
+ continue;
10093
+ const o = k * 4;
10094
+ r += base[o];
10095
+ g += base[o + 1];
10096
+ b += base[o + 2];
10097
+ d0 += data[o];
10098
+ d1 += data[o + 1];
10099
+ d2 += data[o + 2];
10100
+ d3 += data[o + 3];
10101
+ n++;
10102
+ }
10103
+ }
10104
+ if (!n)
10105
+ continue;
10106
+ const o = i * 4;
10107
+ base[o] = Math.round(r / n);
10108
+ base[o + 1] = Math.round(g / n);
10109
+ base[o + 2] = Math.round(b / n);
10110
+ data[o] = Math.round(d0 / n);
10111
+ data[o + 1] = Math.round(d1 / n);
10112
+ data[o + 2] = Math.round(d2 / n);
10113
+ data[o + 3] = Math.round(d3 / n);
10114
+ next[i] = 1;
10115
+ changed++;
10116
+ }
10117
+ }
10118
+ cur = next;
10119
+ if (!changed)
10120
+ break;
10121
+ }
10122
+ const sums = new Float64Array((nFrames + 1) * 8);
10123
+ const all = nFrames * 8;
10124
+ for (let i = 0;i < W * H; i++) {
10125
+ if (!covered[i])
10126
+ continue;
10127
+ const o = i * 4;
10128
+ for (const s of frameOf[i] >= 0 ? [frameOf[i] * 8, all] : [all]) {
10129
+ sums[s] += base[o];
10130
+ sums[s + 1] += base[o + 1];
10131
+ sums[s + 2] += base[o + 2];
10132
+ sums[s + 3] += data[o];
10133
+ sums[s + 4] += data[o + 1];
10134
+ sums[s + 5] += data[o + 2];
10135
+ sums[s + 6] += data[o + 3];
10136
+ sums[s + 7]++;
10137
+ }
10138
+ }
10139
+ for (let i = 0;i < W * H; i++) {
10140
+ if (cur[i])
10141
+ continue;
10142
+ let s = frameOf[i] >= 0 ? frameOf[i] * 8 : all;
10143
+ if (!sums[s + 7])
10144
+ s = all;
10145
+ const n = sums[s + 7], o = i * 4;
10146
+ if (!n) {
10147
+ base[o] = base[o + 1] = base[o + 2] = 128;
10148
+ data[o] = data[o + 1] = data[o + 2] = 128;
10149
+ data[o + 3] = 255;
10150
+ continue;
10151
+ }
10152
+ base[o] = Math.round(sums[s] / n);
10153
+ base[o + 1] = Math.round(sums[s + 1] / n);
10154
+ base[o + 2] = Math.round(sums[s + 2] / n);
10155
+ data[o] = Math.round(sums[s + 3] / n);
10156
+ data[o + 1] = Math.round(sums[s + 4] / n);
10157
+ data[o + 2] = Math.round(sums[s + 5] / n);
10158
+ data[o + 3] = Math.round(sums[s + 6] / n);
10159
+ }
10160
+ }, invert4 = (m) => {
10161
+ const [a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23, a30, a31, a32, a33] = Array.from(m);
10162
+ const b00 = a00 * a11 - a01 * a10, b01 = a00 * a12 - a02 * a10, b02 = a00 * a13 - a03 * a10, b03 = a01 * a12 - a02 * a11;
10163
+ const b04 = a01 * a13 - a03 * a11, b05 = a02 * a13 - a03 * a12, b06 = a20 * a31 - a21 * a30, b07 = a20 * a32 - a22 * a30;
10164
+ const b08 = a20 * a33 - a23 * a30, b09 = a21 * a32 - a22 * a31, b10 = a21 * a33 - a23 * a31, b11 = a22 * a33 - a23 * a32;
10165
+ let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
10166
+ if (Math.abs(det) < 0.000000000001)
10167
+ return null;
10168
+ det = 1 / det;
10169
+ return [
10170
+ (a11 * b11 - a12 * b10 + a13 * b09) * det,
10171
+ (a02 * b10 - a01 * b11 - a03 * b09) * det,
10172
+ (a31 * b05 - a32 * b04 + a33 * b03) * det,
10173
+ (a22 * b04 - a21 * b05 - a23 * b03) * det,
10174
+ (a12 * b08 - a10 * b11 - a13 * b07) * det,
10175
+ (a00 * b11 - a02 * b08 + a03 * b07) * det,
10176
+ (a32 * b02 - a30 * b05 - a33 * b01) * det,
10177
+ (a20 * b05 - a22 * b02 + a23 * b01) * det,
10178
+ (a10 * b10 - a11 * b08 + a13 * b06) * det,
10179
+ (a01 * b08 - a00 * b10 - a03 * b06) * det,
10180
+ (a30 * b04 - a31 * b02 + a33 * b00) * det,
10181
+ (a21 * b02 - a20 * b04 - a23 * b00) * det,
10182
+ (a11 * b07 - a10 * b09 - a12 * b06) * det,
10183
+ (a00 * b09 - a01 * b07 + a02 * b06) * det,
10184
+ (a31 * b01 - a30 * b03 - a32 * b00) * det,
10185
+ (a20 * b03 - a21 * b01 + a22 * b00) * det
10186
+ ];
10187
+ }, encodePng2 = async (rgba, w, h) => {
10188
+ const sharp = (await import("sharp")).default;
10189
+ return new Uint8Array(await sharp(Buffer.from(rgba.buffer, rgba.byteOffset, rgba.byteLength), { raw: { width: w, height: h, channels: 4 } }).png().toBuffer());
10190
+ }, bakeImpostor = async (doc, opts) => {
10191
+ const t0 = performance.now();
10192
+ const log = opts.log ?? ((s) => console.log(s));
10193
+ const g = await collectLevel0(doc, log);
10194
+ if (!g)
10195
+ return null;
10196
+ const { pos, nrm, uv, ao, tri, triSlot, face, slots } = g;
10197
+ const nv = pos.length / 3, nt = tri.length / 3;
10198
+ const c = [(g.min[0] + g.max[0]) / 2, (g.min[1] + g.max[1]) / 2, (g.min[2] + g.max[2]) / 2];
10199
+ let r2 = 0;
10200
+ for (let v = 0;v < nv; v++) {
10201
+ const dx = pos[v * 3] - c[0], dy = pos[v * 3 + 1] - c[1], dz = pos[v * 3 + 2] - c[2];
10202
+ const d = dx * dx + dy * dy + dz * dz;
10203
+ if (d > r2)
10204
+ r2 = d;
10205
+ }
10206
+ const r = Math.sqrt(r2) || 0.001;
10207
+ const height = g.max[1] - g.min[1];
10208
+ const P = Math.max(4, Math.round((opts.px ?? (height >= 8 ? 256 : 128)) / 4) * 4);
10209
+ 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);
10210
+ const { width: AW, height: AH, frames } = layout;
10211
+ const crown = [0, 0.6 * height, 0];
10212
+ const frameOf = new Int32Array(AW * AH).fill(-1);
10213
+ let span = 0;
10214
+ frames.forEach((f, fi) => {
10215
+ span = Math.max(span, f.x1 - f.x0, f.y1 - f.y0);
10216
+ for (let y = f.y0;y < f.y1; y++)
10217
+ frameOf.fill(fi, y * AW + f.x0, y * AW + f.x1);
10218
+ });
10219
+ const base = new Uint8Array(AW * AH * 4), data = new Uint8Array(AW * AH * 4), covered = new Uint8Array(AW * AH);
10220
+ const samples = span * span * 4;
10221
+ const zb = new Float32Array(samples), tb = new Int32Array(samples), l1b = new Float32Array(samples), l2b = new Float32Array(samples);
10222
+ const qx = new Float32Array(nv), qy = new Float32Array(nv), qz = new Float32Array(nv);
10223
+ for (const fr of frames) {
10224
+ const d = fr.d;
10225
+ const { right, up } = frameBasis(d);
10226
+ const { x0: X0, x1: X1, y0: Y0, y1: Y1 } = fr;
10227
+ const W = (X1 - X0) * 2, H = (Y1 - Y0) * 2;
10228
+ const k = 0.5 / r;
10229
+ for (let v = 0;v < nv; v++) {
10230
+ const dx = pos[v * 3] - c[0], dy = pos[v * 3 + 1] - c[1], dz = pos[v * 3 + 2] - c[2];
10231
+ const a = 0.5 + k * (dx * right[0] + dy * right[1] + dz * right[2]);
10232
+ const b = 0.5 + k * (dx * up[0] + dy * up[1] + dz * up[2]);
10233
+ qx[v] = a * W;
10234
+ qy[v] = (1 - b) * H;
10235
+ qz[v] = dx * d[0] + dy * d[1] + dz * d[2];
10236
+ }
10237
+ zb.fill(-Infinity, 0, W * H);
10238
+ tb.fill(-1, 0, W * H);
10239
+ for (let t = 0;t < nt; t++) {
10240
+ const ia = tri[t * 3], ib = tri[t * 3 + 1], ic = tri[t * 3 + 2];
10241
+ const x0 = qx[ia], y0 = qy[ia], x1 = qx[ib], y1 = qy[ib], x2 = qx[ic], y2 = qy[ic];
10242
+ 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));
10243
+ if (minX > maxX)
10244
+ continue;
10245
+ 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));
10246
+ if (minY > maxY)
10247
+ continue;
10248
+ const area = (x1 - x0) * (y2 - y0) - (y1 - y0) * (x2 - x0);
10249
+ if (Math.abs(area) < 0.000000000001)
10250
+ continue;
10251
+ const slot = slots[triSlot[t]];
10252
+ if (slot.cull && face[t * 3] * d[0] + face[t * 3 + 1] * d[1] + face[t * 3 + 2] * d[2] < 0)
10253
+ continue;
10254
+ const inv = 1 / area;
10255
+ const z0 = qz[ia], dz1 = qz[ib] - z0, dz2 = qz[ic] - z0;
10256
+ const testAlpha = slot.mask;
10257
+ 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;
10258
+ const s1x = (y2 - y0) * inv, s2x = -(y1 - y0) * inv;
10259
+ for (let sy = minY;sy <= maxY; sy++) {
10260
+ const py = sy + 0.5 - y0, px = minX + 0.5 - x0;
10261
+ let l1 = (px * (y2 - y0) - py * (x2 - x0)) * inv;
10262
+ let l2 = ((x1 - x0) * py - (y1 - y0) * px) * inv;
10263
+ const row = sy * W;
10264
+ for (let sx = minX;sx <= maxX; sx++, l1 += s1x, l2 += s2x) {
10265
+ if (l1 < -0.0000001 || l2 < -0.0000001 || l1 + l2 > 1.0000001)
10266
+ continue;
10267
+ const z = z0 + l1 * dz1 + l2 * dz2;
10268
+ const s = row + sx;
10269
+ if (z <= zb[s])
10270
+ continue;
10271
+ if (testAlpha && alphaAt(slot, u0 + l1 * du1 + l2 * du2, v0 + l1 * dv1 + l2 * dv2) < slot.cutoff)
10272
+ continue;
10273
+ zb[s] = z;
10274
+ tb[s] = t;
10275
+ l1b[s] = l1;
10276
+ l2b[s] = l2;
10277
+ }
10278
+ }
10279
+ }
10280
+ for (let y = Y0;y < Y1; y++) {
10281
+ for (let x = X0;x < X1; x++) {
10282
+ let n = 0, cr = 0, cg = 0, cb = 0, nx = 0, ny = 0, nz = 0, aoSum = 0, zSum = 0;
10283
+ for (let q = 0;q < 4; q++) {
10284
+ const s = ((y - Y0) * 2 + (q >> 1)) * W + (x - X0) * 2 + (q & 1);
10285
+ const t = tb[s];
10286
+ if (t < 0)
10287
+ continue;
10288
+ zSum += zb[s];
10289
+ const l1 = l1b[s], l2 = l2b[s], l0 = 1 - l1 - l2;
10290
+ const ia = tri[t * 3], ib = tri[t * 3 + 1], ic = tri[t * 3 + 2];
10291
+ const mx = pos[ia * 3] * l0 + pos[ib * 3] * l1 + pos[ic * 3] * l2 - crown[0];
10292
+ const my = pos[ia * 3 + 1] * l0 + pos[ib * 3 + 1] * l1 + pos[ic * 3 + 1] * l2 - crown[1];
10293
+ const mz = pos[ia * 3 + 2] * l0 + pos[ib * 3 + 2] * l1 + pos[ic * 3 + 2] * l2 - crown[2];
10294
+ const ml = Math.hypot(mx, my, mz) || 1;
10295
+ nx += mx / ml;
10296
+ ny += my / ml;
10297
+ nz += mz / ml;
10298
+ const slot = slots[triSlot[t]];
10299
+ if (slot.img) {
10300
+ 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);
10301
+ const im = slot.img;
10302
+ let r0 = 0, g0 = 0, b0 = 0;
10303
+ for (let k4 = 0;k4 < 4; k4++) {
10304
+ const o = tapI[k4], w = tapW[k4];
10305
+ r0 += SRGB_TO_LINEAR2[im[o]] * w;
10306
+ g0 += SRGB_TO_LINEAR2[im[o + 1]] * w;
10307
+ b0 += SRGB_TO_LINEAR2[im[o + 2]] * w;
10308
+ }
10309
+ cr += r0 * slot.f[0];
10310
+ cg += g0 * slot.f[1];
10311
+ cb += b0 * slot.f[2];
10312
+ } else {
10313
+ cr += slot.f[0];
10314
+ cg += slot.f[1];
10315
+ cb += slot.f[2];
10316
+ }
10317
+ aoSum += ao[ia] * l0 + ao[ib] * l1 + ao[ic] * l2;
10318
+ n++;
10319
+ }
10320
+ if (!n)
10321
+ continue;
10322
+ const o = (y * AW + x) * 4;
10323
+ covered[y * AW + x] = 1;
10324
+ base[o] = toSrgb8(cr / n);
10325
+ base[o + 1] = toSrgb8(cg / n);
10326
+ base[o + 2] = toSrgb8(cb / n);
10327
+ base[o + 3] = Math.round(n / 4 * 255);
10328
+ const nl = Math.hypot(nx, ny, nz) || 1;
10329
+ const [eu, ev] = normalEncode([nx / nl, ny / nl, nz / nl]);
10330
+ const depth = zSum / n / r;
10331
+ data[o] = Math.round(Math.min(Math.max(eu, 0), 1) * 255);
10332
+ data[o + 1] = Math.round(Math.min(Math.max(ev, 0), 1) * 255);
10333
+ data[o + 2] = Math.round(Math.min(Math.max(depth * 0.5 + 0.5, 0), 1) * 255);
10334
+ data[o + 3] = Math.round(Math.min(Math.max(aoSum / n, 0), 1) * 255);
10335
+ }
10336
+ }
10337
+ }
10338
+ dilate(base, data, covered, AW, AH, frameOf, frames.length);
10339
+ const root = g.root;
10340
+ const rootName = root.getName() || root.getMesh()?.getName() || "impostor";
10341
+ let highest = 0;
10342
+ for (const n of doc.getRoot().listNodes()) {
10343
+ const m = LOD_RE.exec(n.getName());
10344
+ if (m)
10345
+ highest = Math.max(highest, Number(m[1]));
10346
+ }
10347
+ const level = Math.min(Math.max(1, Math.round(opts.level ?? 2)), highest + 1);
10348
+ const drop = (n) => {
10349
+ for (const ch of n.listChildren())
10350
+ drop(ch);
10351
+ n.dispose();
10352
+ };
10353
+ for (const n of doc.getRoot().listNodes()) {
10354
+ const m = LOD_RE.exec(n.getName());
10355
+ if (m && Number(m[1]) >= level && !n.isDisposed())
10356
+ drop(n);
10357
+ }
10358
+ const buffer = doc.getRoot().listBuffers()[0] ?? doc.createBuffer();
10359
+ const tag = () => ({ lecodes: { impostor: true } });
10360
+ const albedo = doc.createTexture(`${rootName}_impostor_albedo`).setMimeType("image/png").setImage(await encodePng2(base, AW, AH)).setExtras(tag());
10361
+ const normalDepth = doc.createTexture(`${rootName}_impostor_data`).setMimeType("image/png").setImage(await encodePng2(data, AW, AH)).setExtras(tag());
10362
+ 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] } });
10363
+ for (const info of [material.getBaseColorTextureInfo(), material.getMetallicRoughnessTextureInfo()])
10364
+ info?.setWrapS(CLAMP).setWrapT(CLAMP);
10365
+ const acc = (type, arr) => doc.createAccessor().setType(type).setArray(arr).setBuffer(buffer);
10366
+ const corners = new Float32Array([-1, -1, -1, 1, -1, 1, 1, 1, -1, -1, 1, 1]).map((s, k) => c[k % 3] + s * r);
10367
+ 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])));
10368
+ 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 } } });
10369
+ const parent = root.getParentNode();
10370
+ if (parent) {
10371
+ parent.addChild(node);
10372
+ const pw = parent.getWorldMatrix();
10373
+ const identity = pw.every((x, k) => Math.abs(x - (k % 5 === 0 ? 1 : 0)) < 0.000001);
10374
+ const inv = identity ? null : invert4(pw);
10375
+ if (inv)
10376
+ node.setMatrix(inv);
10377
+ } else {
10378
+ for (const s of doc.getRoot().listScenes())
10379
+ if (s.listChildren().includes(root))
10380
+ s.addChild(node);
10381
+ }
10382
+ const logger = doc.getLogger();
10383
+ doc.setLogger(new Logger3(Logger3.Verbosity.SILENT));
10384
+ await doc.transform(prune3({
10385
+ propertyTypes: [PropertyType.MESH, PropertyType.PRIMITIVE, PropertyType.MATERIAL, PropertyType.TEXTURE, PropertyType.ACCESSOR],
10386
+ keepLeaves: true,
10387
+ keepAttributes: true,
10388
+ keepIndices: true,
10389
+ keepSolidTextures: true,
10390
+ keepExtras: false
10391
+ }));
10392
+ doc.setLogger(logger);
10393
+ 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() };
10394
+ };
10395
+ var init_impostor = __esm(() => {
10396
+ init_image();
10397
+ LOD_RE = /_LOD([1-9]\d*)$/i;
10398
+ SRGB_TO_LINEAR2 = Float32Array.from({ length: 256 }, (_, i) => {
10399
+ const c = i / 255;
10400
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
10401
+ });
10402
+ tapI = new Int32Array(4);
10403
+ tapW = new Float32Array(4);
10404
+ });
10405
+
9818
10406
  // src/geometry.ts
9819
10407
  var MAX_VERTICES = 65535, mul32 = (m, x, y, z, w) => [
9820
10408
  m[0] * x + m[4] * y + m[8] * z + m[12] * w,
@@ -13840,20 +14428,33 @@ var cloneTree = (t) => {
13840
14428
  x.children.forEach(collect);
13841
14429
  };
13842
14430
  collect(g);
14431
+ const levels = c.body.m_LODs.filter((lod) => isMap(lod) && Array.isArray(lod.renderers)).length;
13843
14432
  c.body.m_LODs.forEach((lod, level) => {
13844
14433
  if (!isMap(lod) || !Array.isArray(lod.renderers))
13845
14434
  return;
13846
14435
  for (const r of lod.renderers) {
13847
14436
  const id = ref(isMap(r) ? r.renderer : undefined).fileID;
13848
14437
  const rc = byId.get(id) ?? ids?.get(id);
13849
- if (rc && "go" in rc && !out.has(rc))
13850
- out.set(rc, level);
14438
+ if (!rc || !("go" in rc))
14439
+ continue;
14440
+ if (levels <= 1) {
14441
+ if (!out.has(rc))
14442
+ out.set(rc, []);
14443
+ continue;
14444
+ }
14445
+ const have = out.get(rc);
14446
+ if (!have)
14447
+ out.set(rc, [level]);
14448
+ else if (have.length > 0 && !have.includes(level))
14449
+ have.push(level);
13851
14450
  }
13852
14451
  });
13853
14452
  }
13854
14453
  g.children.forEach(walk);
13855
14454
  };
13856
14455
  roots.forEach(walk);
14456
+ for (const v of out.values())
14457
+ v.sort((a, b) => a - b);
13857
14458
  return out;
13858
14459
  };
13859
14460
  var init_gameObjects = () => {};
@@ -14986,6 +15587,7 @@ var readCollider = (c, path, M) => {
14986
15587
  };
14987
15588
  const kindOf = (root, prefab, fallbackName, nested) => {
14988
15589
  const parts = [], colliders = [], klights = [];
15590
+ const drawn = [];
14989
15591
  let particles = 0;
14990
15592
  const lods = lodLevels([root]);
14991
15593
  const walk = (g, parentM, path, top) => {
@@ -15004,7 +15606,9 @@ var readCollider = (c, path, M) => {
15004
15606
  const mesh = ref(mf.body.m_Mesh);
15005
15607
  if (mesh.guid && mesh.guid !== "0000000000000000e000000000000000") {
15006
15608
  const mats = Array.isArray(mr.body.m_Materials) ? mr.body.m_Materials.map((x) => ref(x).guid ?? "") : [];
15007
- parts.push({ path: p, matrix: M, mesh: { guid: mesh.guid, fileID: mesh.fileID }, materials: mats, lod: lods.get(mr) ?? lodSuffix(goName(g)), castShadows: num2(mr.body.m_CastShadows, 1) !== 0 });
15609
+ const suffix = lodSuffix(goName(g));
15610
+ const levels = lods.get(mr) ?? (suffix > 0 ? [suffix] : []);
15611
+ drawn.push({ part: { path: p, matrix: M, mesh: { guid: mesh.guid, fileID: mesh.fileID }, materials: mats, castShadows: num2(mr.body.m_CastShadows, 1) !== 0 }, levels });
15008
15612
  }
15009
15613
  }
15010
15614
  for (const c of g.comps) {
@@ -15022,6 +15626,12 @@ var readCollider = (c, path, M) => {
15022
15626
  walk(ch, m, p, false);
15023
15627
  };
15024
15628
  walk(root, I42(), "", true);
15629
+ const maxLevel = drawn.reduce((m, d) => Math.max(m, ...d.levels), 0);
15630
+ for (const d of drawn) {
15631
+ const levels = d.levels.length ? d.levels : Array.from({ length: maxLevel + 1 }, (_, i) => i);
15632
+ for (const lod of levels)
15633
+ parts.push({ ...d.part, lod });
15634
+ }
15025
15635
  stats.particles += particles;
15026
15636
  stats.prefabLights += klights.length;
15027
15637
  if (!parts.length && !colliders.length)
@@ -15462,8 +16072,8 @@ var EPS2 = 0.000000001, rotate = (q, v) => {
15462
16072
  };
15463
16073
 
15464
16074
  // src/unity/triBudget.ts
15465
- import { PropertyType } from "@gltf-transform/core";
15466
- import { prune as prune3, simplifyPrimitive as simplifyPrimitive2, weldPrimitive as weldPrimitive3 } from "@gltf-transform/functions";
16075
+ import { PropertyType as PropertyType2 } from "@gltf-transform/core";
16076
+ import { prune as prune4, simplifyPrimitive as simplifyPrimitive2, weldPrimitive as weldPrimitive3 } from "@gltf-transform/functions";
15467
16077
  import { MeshoptSimplifier as MeshoptSimplifier2 } from "meshoptimizer";
15468
16078
  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) => {
15469
16079
  const prims = [...new Set(doc.getRoot().listMeshes().flatMap((m) => m.listPrimitives()))].filter((p) => p.getMode() === 4 && !p.getAttribute("JOINTS_0"));
@@ -15483,7 +16093,7 @@ var DEFAULT_MAX_TRIS = 50000, trisOf = (p) => p.getMode() !== 4 ? 0 : Math.floor
15483
16093
  smoothNormals(doc, p);
15484
16094
  }
15485
16095
  }
15486
- await doc.transform(prune3({ propertyTypes: [PropertyType.ACCESSOR] }));
16096
+ await doc.transform(prune4({ propertyTypes: [PropertyType2.ACCESSOR] }));
15487
16097
  return { before, after: prims.reduce((n, p) => n + trisOf(p), 0) };
15488
16098
  }, smoothNormals = (doc, p) => {
15489
16099
  const pos = p.getAttribute("POSITION"), idx = p.getIndices();
@@ -15804,7 +16414,7 @@ var init_triplanar = __esm(() => {
15804
16414
  import { basename as basename21, dirname as dirname12 } from "node:path";
15805
16415
  import { readFileSync as readFileSync24 } from "node:fs";
15806
16416
  import { Document as Document5 } from "@gltf-transform/core";
15807
- import { mergeDocuments as mergeDocuments2, prune as prune4 } from "@gltf-transform/functions";
16417
+ import { mergeDocuments as mergeDocuments2, prune as prune5 } from "@gltf-transform/functions";
15808
16418
 
15809
16419
  class KindGlbBuilder {
15810
16420
  ctx;
@@ -16082,7 +16692,7 @@ class KindGlbBuilder {
16082
16692
  s.dispose();
16083
16693
  for (const n of out.imported)
16084
16694
  n.dispose();
16085
- await doc.transform(prune4({ keepLeaves: false }));
16695
+ await doc.transform(prune5({ keepLeaves: false }));
16086
16696
  const buffers = doc.getRoot().listBuffers();
16087
16697
  if (buffers.length > 1) {
16088
16698
  const keep = buffers[0];
@@ -16399,24 +17009,31 @@ var planLayers = async (t) => {
16399
17009
  if (!t || !r.terrain)
16400
17010
  return null;
16401
17011
  mkdirSync5(dir, { recursive: true });
16402
- const res = t.resolution;
17012
+ const src = t.resolution;
17013
+ let step = 1;
17014
+ if (opts.terrainRes && opts.terrainRes > 1)
17015
+ while ((src - 1) / step + 1 > opts.terrainRes && (src - 1) % (step * 2) === 0)
17016
+ step *= 2;
17017
+ const res = (src - 1) / step + 1;
17018
+ if (step > 1)
17019
+ log(` \uD83C\uDFD4 terrain heightfield ${src}² → ${res}² (every ${step}th sample, --terrain-res ${opts.terrainRes})`);
16403
17020
  const heights = new Float32Array(res * res);
16404
17021
  for (let z = 0;z < res; z++)
16405
17022
  for (let x = 0;x < res; x++)
16406
- heights[z * res + x] = t.heights[z * res + (res - 1 - x)];
17023
+ heights[z * res + x] = t.heights[z * step * src + (src - 1 - x * step)];
16407
17024
  const plan = await planLayers(t);
16408
17025
  const slot = new Map;
16409
17026
  plan.keep.forEach((li, s) => slot.set(li, s));
16410
17027
  for (const [lose, win] of plan.mergeInto)
16411
17028
  slot.set(lose, slot.get(win));
16412
17029
  const ar = t.alphaResolution, control = new Uint8Array(res * res * 4);
16413
- const f = ar / res;
17030
+ const f = ar / src;
16414
17031
  for (let z = 0;z < res; z++)
16415
17032
  for (let x = 0;x < res; x++) {
16416
- const ux = res - 1 - x;
17033
+ const ux = (res - 1 - x) * step, uz = z * step;
16417
17034
  const acc = [0, 0, 0, 0];
16418
- const ax0 = Math.max(0, Math.floor((ux - 0.5) * f)), ax1 = Math.min(ar - 1, Math.ceil((ux + 0.5) * f));
16419
- const az0 = Math.max(0, Math.floor((z - 0.5) * f)), az1 = Math.min(ar - 1, Math.ceil((z + 0.5) * f));
17035
+ const ax0 = Math.max(0, Math.floor((ux - step / 2) * f)), ax1 = Math.min(ar - 1, Math.ceil((ux + step / 2) * f));
17036
+ const az0 = Math.max(0, Math.floor((uz - step / 2) * f)), az1 = Math.min(ar - 1, Math.ceil((uz + step / 2) * f));
16420
17037
  for (let li = 0;li < t.weights.length; li++) {
16421
17038
  const w = t.weights[li];
16422
17039
  let s = 0;
@@ -16549,12 +17166,15 @@ var buildBvh = (tris) => {
16549
17166
  }
16550
17167
  }
16551
17168
  return best;
16552
- }, LOD_RE, collectOccluders = (doc) => {
17169
+ }, LOD_RE2, windProfiled = (p) => {
17170
+ const extras = p.getMaterial()?.getExtras();
17171
+ return Array.isArray(extras?.lecodes?.wind);
17172
+ }, collectOccluders = (doc) => {
16553
17173
  const tris = [];
16554
17174
  const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
16555
17175
  for (const node of doc.getRoot().listNodes()) {
16556
17176
  const mesh = node.getMesh();
16557
- if (!mesh || LOD_RE.test(node.getName()))
17177
+ if (!mesh || LOD_RE2.test(node.getName()))
16558
17178
  continue;
16559
17179
  const m = node.getWorldMatrix();
16560
17180
  for (const p of mesh.listPrimitives()) {
@@ -16609,7 +17229,7 @@ var buildBvh = (tris) => {
16609
17229
  if (canopy > 0) {
16610
17230
  for (const node of doc.getRoot().listNodes()) {
16611
17231
  const mesh = node.getMesh();
16612
- if (!mesh || LOD_RE.test(node.getName()))
17232
+ if (!mesh || LOD_RE2.test(node.getName()))
16613
17233
  continue;
16614
17234
  const m = node.getWorldMatrix();
16615
17235
  for (const p of mesh.listPrimitives()) {
@@ -16649,7 +17269,7 @@ var buildBvh = (tris) => {
16649
17269
  const n = pos.getCount();
16650
17270
  const nrm = p.getAttribute("NORMAL");
16651
17271
  if (canopy > 0 && crownN && nrm && p.getMaterial()?.getAlphaMode() === "MASK") {
16652
- const inv = invert4(m);
17272
+ const inv = invert42(m);
16653
17273
  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;
16654
17274
  if (cl) {
16655
17275
  const out = new Float32Array(new ArrayBuffer(n * 12)), v = [], q = [];
@@ -16675,6 +17295,7 @@ var buildBvh = (tris) => {
16675
17295
  }
16676
17296
  const old = p.getAttribute("COLOR_0");
16677
17297
  const oldEl = old ? old.getElementSize() : 0;
17298
+ const authoredWind = windProfiled(p);
16678
17299
  const out = new Float32Array(new ArrayBuffer(n * 16));
16679
17300
  const v = [], c = [], q = [];
16680
17301
  const cavNrm = cavReach > 0 && p.getMaterial()?.getAlphaMode() !== "MASK" ? p.getAttribute("NORMAL") : null;
@@ -16710,7 +17331,7 @@ var buildBvh = (tris) => {
16710
17331
  cav = 1 - occ / cavRays.length;
16711
17332
  }
16712
17333
  let wind;
16713
- if (old && oldEl === 4) {
17334
+ if (old && oldEl === 4 && authoredWind) {
16714
17335
  old.getElement(i, c);
16715
17336
  wind = c[3];
16716
17337
  } else {
@@ -16738,7 +17359,7 @@ var buildBvh = (tris) => {
16738
17359
  q[0] = x / l;
16739
17360
  q[1] = y / l;
16740
17361
  q[2] = z / l;
16741
- }, invert4 = (m) => {
17362
+ }, invert42 = (m) => {
16742
17363
  const a = Array.from(m), r = new Array(16);
16743
17364
  const [a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23, a30, a31, a32, a33] = a;
16744
17365
  const b00 = a00 * a11 - a01 * a10, b01 = a00 * a12 - a02 * a10, b02 = a00 * a13 - a03 * a10, b03 = a01 * a12 - a02 * a11;
@@ -16767,14 +17388,14 @@ var buildBvh = (tris) => {
16767
17388
  return r;
16768
17389
  };
16769
17390
  var init_vertexAo = __esm(() => {
16770
- LOD_RE = /_LOD([1-9]\d*)$/i;
17391
+ LOD_RE2 = /_LOD([1-9]\d*)$/i;
16771
17392
  });
16772
17393
 
16773
17394
  // src/unity/lecodesLevel.ts
16774
17395
  import { createHash as createHash3 } from "node:crypto";
16775
17396
  import { existsSync as existsSync24, mkdirSync as mkdirSync6, readFileSync as readFileSync27, writeFileSync as writeFileSync12 } from "node:fs";
16776
17397
  import { dirname as dirname13, join as join12, relative, resolve as resolve6 } from "node:path";
16777
- var BUILDER_VERSION = 12, v3 = (v) => `[${v.map(r3).join(", ")}]`, ident = (s) => {
17398
+ var BUILDER_VERSION = 20, v3 = (v) => `[${v.map(r3).join(", ")}]`, ident = (s) => {
16778
17399
  const x = s.replace(/[^\w$]/g, "_");
16779
17400
  return /^\d/.test(x) ? `_${x}` : x;
16780
17401
  }, transformFields = (pos, rot, scale) => {
@@ -16852,7 +17473,7 @@ var BUILDER_VERSION = 12, v3 = (v) => `[${v.map(r3).join(", ")}]`, ident = (s) =
16852
17473
  if (foliage)
16853
17474
  model.push("foliage: true");
16854
17475
  if (noLightmap)
16855
- model.push("lightmap: false");
17476
+ model.push("static: false");
16856
17477
  if (modelAspects.length)
16857
17478
  model.push(`aspects: [${modelAspects.join(", ")}]`);
16858
17479
  return `// Generated by \`lecodes assets scene\` from ${source}${k.prefabPath ? ` (${k.prefabPath.replace(/\\/g, "/").replace(/^.*\/Assets\//, "Assets/")})` : ""}.
@@ -16880,7 +17501,7 @@ ${[` model: { ${model.join(", ")} },`, ...children].join(`
16880
17501
  const glb = join12(assetDir, `${k.name}.glb`);
16881
17502
  const maxTris = opts.maxTris ?? DEFAULT_MAX_TRIS;
16882
17503
  const foliage = decideFoliage(k, builder.materialsOf(k), opts);
16883
- const hash = createHash3("sha1").update(JSON.stringify([BUILDER_VERSION, k.key, opts.maxSize, opts.ktx2, maxTris, ...foliage ? [] : ["nocolor"]])).digest("hex").slice(0, 16);
17504
+ 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);
16884
17505
  if (!opts.force && existsSync24(glb) && manifest[k.name] === hash) {
16885
17506
  reused++;
16886
17507
  continue;
@@ -16897,6 +17518,9 @@ ${[` model: { ${model.join(", ")} },`, ...children].join(`
16897
17518
  }
16898
17519
  }
16899
17520
  if (foliage) {
17521
+ for (const m of doc.getRoot().listMaterials())
17522
+ if (m.getAlphaMode() === "MASK")
17523
+ m.setDoubleSided(true);
16900
17524
  await Promise.resolve().then(() => init_vertexAo());
16901
17525
  const ao = bakeVertexAo(doc);
16902
17526
  if (ao.vertices)
@@ -16911,6 +17535,16 @@ ${[` model: { ${model.join(", ")} },`, ...children].join(`
16911
17535
  const cut = await fitTriangleBudget(doc, maxTris);
16912
17536
  if (cut)
16913
17537
  log(` ✂ ${k.name}: ${cut.before.toLocaleString("en")} → ${cut.after.toLocaleString("en")} triangles (--max-tris ${maxTris})`);
17538
+ if (foliage && opts.impostor) {
17539
+ await Promise.resolve().then(() => init_impostor());
17540
+ const box = impostorBounds(doc);
17541
+ const height = box ? box.max[1] - box.min[1] : 0;
17542
+ if (height >= 4) {
17543
+ const imp = await bakeImpostor(doc, { ...opts.impostor, log });
17544
+ if (imp)
17545
+ log(` \uD83C\uDF32 ${k.name}: impostor ${impostorLabel(imp)} · ${imp.node} · r ${imp.radius.toFixed(2)} m · ${imp.triangles} triangles in ${imp.ms} ms`);
17546
+ }
17547
+ }
16914
17548
  await applyPlan(doc, await analyzeDoc(doc, doctor), doctor);
16915
17549
  writeFileSync12(glb, await makeIO().writeBinary(doc));
16916
17550
  manifest[k.name] = hash;
@@ -16963,7 +17597,7 @@ ${[` model: { ${model.join(", ")} },`, ...children].join(`
16963
17597
  }
16964
17598
  log(` ${describeFoliage(foliageKinds, opts)}`);
16965
17599
  if (opts.noLightmap)
16966
- 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}/)`}`);
17600
+ 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}/)`}`);
16967
17601
  }, terrainNode = (r, terrain, sdir) => {
16968
17602
  const data = r.terrainData;
16969
17603
  const layers = terrain.layers.map((f, i) => {
@@ -19461,6 +20095,28 @@ var resolveInputs = (args, ext = /\.glb$/i, skipGenerated = true) => {
19461
20095
  }
19462
20096
  return [...new Set(out)];
19463
20097
  };
20098
+ var impostorFlag = (v, level) => {
20099
+ if (v === undefined)
20100
+ return;
20101
+ if (level !== undefined && (!Number.isInteger(level) || level < 1 || level > 3))
20102
+ throw usageError(`--impostor-level wants 1, 2 or 3 — got "${level}"`);
20103
+ const s = v.trim() || "12x3";
20104
+ 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})`);
20105
+ const grid = /^(\d+)x(\d+)(?:@(\d+))?$/i.exec(s), oct = /^oct(\d+)(?:@(\d+))?$/i.exec(s);
20106
+ if (!grid && !oct)
20107
+ throw bad("not either form");
20108
+ const cols = Number(grid ? grid[1] : oct[1]), rows = grid ? Number(grid[2]) : 0;
20109
+ const pxs = grid ? grid[3] : oct[2], px = pxs ? Number(pxs) : undefined;
20110
+ if (grid && (cols < 3 || cols > 64))
20111
+ throw bad("azimuths 3..64");
20112
+ if (grid && (rows < 1 || rows > 8))
20113
+ throw bad("rows 1..8");
20114
+ if (oct && (cols < 2 || cols > 64))
20115
+ throw bad("N 2..64");
20116
+ if (px !== undefined && (px < 32 || px > 1024 || px % 4 !== 0))
20117
+ throw bad("px 32..1024, a multiple of 4");
20118
+ return { cols, rows, px, level };
20119
+ };
19464
20120
  var regexFlag = (v) => v ? new RegExp(v, "i") : undefined;
19465
20121
 
19466
20122
  // src/commands/avatar.ts
@@ -19756,7 +20412,9 @@ var doctor_default = defineCommand({
19756
20412
  dump: str("write the file's textures out instead (default: <in>-textures/)", { value: "dir", bare: "" }),
19757
20413
  ...doctorFlags,
19758
20414
  lod: str("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" }),
19759
- "lod-bones": bool("fold fingers / toes / twists onto their parents in the last level's skin")
20415
+ "lod-bones": bool("fold fingers / toes / twists onto their parents in the last level's skin"),
20416
+ impostor: str("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" }),
20417
+ "impostor-level": int("the LOD slot the quad takes (default 2: shown under 0.12 of the viewport height; 1 = under 0.30)")
19760
20418
  },
19761
20419
  examples: [
19762
20420
  "lecodes assets doctor hero.glb",
@@ -19772,7 +20430,8 @@ var doctor_default = defineCommand({
19772
20430
  await Promise.resolve().then(() => init_validateGlb());
19773
20431
  const useKtx2 = wantsKtx2(flags);
19774
20432
  const lod = (flags.lod ?? "").split(",").map(Number).filter((n) => n > 0 && n < 1);
19775
- const doFix = wantsDoctorFix(flags) || flags["fix-metallic"] || lod.length > 0;
20433
+ const impostor = impostorFlag(flags.impostor, flags["impostor-level"]);
20434
+ const doFix = wantsDoctorFix(flags) || flags["fix-metallic"] || lod.length > 0 || !!impostor;
19776
20435
  const opts = { ...doctorOptions(flags), lod, lodBones: flags["lod-bones"] };
19777
20436
  const ktxEncoder = useKtx2 ? findKtxEncoder() : null;
19778
20437
  if (useKtx2) {
@@ -19797,6 +20456,21 @@ var doctor_default = defineCommand({
19797
20456
  await dumpTextures(doc, flags.dump && files.length === 1 ? flags.dump : input.replace(/\.glb$/i, "-textures"));
19798
20457
  continue;
19799
20458
  }
20459
+ if (impostor) {
20460
+ await Promise.resolve().then(() => init_impostor());
20461
+ try {
20462
+ const imp = await bakeImpostor(doc, { ...impostor, log });
20463
+ if (imp)
20464
+ log(` impostor: ${imp.node} · ${impostorLabel(imp)} · r ${imp.radius.toFixed(2)} m · ${imp.triangles} triangles in ${imp.ms} ms`);
20465
+ else
20466
+ log(` impostor: no level-0 triangles in ${input}`);
20467
+ } catch (e) {
20468
+ console.error(`
20469
+ ✗ ${input}: ${e.message}`);
20470
+ exitCode = 1;
20471
+ continue;
20472
+ }
20473
+ }
19800
20474
  const plan = await analyzeDoc(doc, opts);
19801
20475
  const rep = reportPlan(plan);
19802
20476
  log(`
@@ -20438,6 +21112,8 @@ var scene_default = defineCommand({
20438
21112
  "no-shadows": str("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>" }),
20439
21113
  "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"),
20440
21114
  "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"),
21115
+ impostor: str("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" }),
21116
+ "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)"),
20441
21117
  "no-trim-cards": bool("skip the alpha-card trim the import otherwise runs (it can empty a kind whose decal atlas is mostly clear)"),
20442
21118
  filter: str("keep only instances whose kind name / scene group / node name matches", { value: "<re>", group: "Subset" }),
20443
21119
  "drop-at": str(`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;…>" }),
@@ -20445,6 +21121,7 @@ var scene_default = defineCommand({
20445
21121
  group: str("only instances under a scene group whose path contains this", { value: "name" }),
20446
21122
  region: str("only instances inside this Unity X/Z rectangle", { value: "x0,z0,x1,z1" }),
20447
21123
  "no-trees": bool("skip the terrain's painted trees"),
21124
+ "terrain-res": num("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>" }),
20448
21125
  "no-details": bool("skip the terrain's detail meshes"),
20449
21126
  "include-inactive": bool("import inactive GameObjects too"),
20450
21127
  handedness: str("how Unity's left-handed frame is undone (default x: −x, (x, −y, −z, w))", { value: "x|none", group: "Source" }),
@@ -20500,6 +21177,8 @@ var scene_default = defineCommand({
20500
21177
  noColliders: flags["no-colliders"],
20501
21178
  noShadows: regexFlag(flags["no-shadows"]),
20502
21179
  maxTris: flags["max-tris"],
21180
+ impostor: impostorFlag(flags.impostor, flags["impostor-level"]),
21181
+ terrainRes: flags["terrain-res"] ?? 1025,
20503
21182
  command: "lecodes assets scene " + argv.map((a) => /[\s"'|<>^&();]/.test(a) ? `"${a.replace(/"/g, "\\\"")}"` : a).join(" ")
20504
21183
  });
20505
21184
  log(`\uD83D\uDCBE ${res.scene} — ${res.kinds} kinds (${res.converted} converted, ${res.reused} up to date), ${r.instances.length} placements, ${seconds(t0)}`);