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 +712 -33
- package/dist/index.js +712 -33
- package/package.json +1 -1
- package/src/commands/doctor.ts +15 -3
- package/src/commands/scene.ts +6 -1
- package/src/commands/shared.ts +20 -1
- package/src/doctor/cardTrim.ts +2 -0
- package/src/doctor/impostor.ts +614 -0
- package/src/doctor/transcodeKtx2.ts +8 -4
- package/src/doctor/transcodeTextures.ts +6 -1
- package/src/doctor/vertexAo.ts +16 -4
- package/src/unity/gameObjects.ts +13 -4
- package/src/unity/lecodesLevel.ts +38 -5
- package/src/unity/levelTerrain.ts +16 -7
- package/src/unity/materialWriter.ts +11 -1
- package/src/unity/scene.ts +16 -2
package/dist/cli.js
CHANGED
|
@@ -4205,7 +4205,13 @@ var SLOTS, pick = (m, names) => {
|
|
|
4205
4205
|
slots.push(`base=${nameOf(baseG)}`);
|
|
4206
4206
|
}
|
|
4207
4207
|
}
|
|
4208
|
-
|
|
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
|
|
13850
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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 *
|
|
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 /
|
|
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 -
|
|
16419
|
-
const az0 = Math.max(0, Math.floor((
|
|
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
|
-
},
|
|
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 ||
|
|
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 ||
|
|
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 =
|
|
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
|
-
},
|
|
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
|
-
|
|
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 =
|
|
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("
|
|
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
|
|
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
|
|
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)}`);
|