@tileflow/dev 0.1.0-alpha.37 → 0.1.0-alpha.38
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/README.md +2 -0
- package/dist/artifacts.js +2 -2
- package/dist/{chunk-5RT4CHSK.js → chunk-54VM4SNV.js} +1108 -304
- package/dist/{chunk-UKWAQKLH.js → chunk-MRFNAWWR.js} +1 -1
- package/dist/icons.js +1 -1
- package/dist/server.js +2 -2
- package/docs/building-models.md +80 -41
- package/package.json +3 -3
|
@@ -958,82 +958,38 @@ import {
|
|
|
958
958
|
|
|
959
959
|
// src/building-model-mesh.ts
|
|
960
960
|
import {
|
|
961
|
-
|
|
961
|
+
isTileflowModelPartName,
|
|
962
|
+
tileflowModelDefaultLightingRole,
|
|
962
963
|
tileflowModelLightingRoles,
|
|
963
964
|
tileflowModelLimits,
|
|
964
|
-
|
|
965
|
+
tileflowModelSurfaceLimits as tileflowModelSurfaceLimits2,
|
|
966
|
+
tileflowModelTextureChannels as tileflowModelTextureChannels2
|
|
965
967
|
} from "@tileflow/core";
|
|
966
968
|
|
|
967
|
-
// src/building-model-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
for (const primitive of primitives) {
|
|
983
|
-
const count = primitive.positions.length / 3;
|
|
984
|
-
const surface = primitive.surface;
|
|
985
|
-
if (surface) {
|
|
986
|
-
checkTileflowModelSurface(surface, count, primitive.indices.length);
|
|
987
|
-
for (const value of surface.attributes) attributes.push(value);
|
|
988
|
-
emitters.push(...surface.emitters ?? []);
|
|
989
|
-
} else {
|
|
990
|
-
for (let vertex = 0; vertex < count; vertex++) attributes.push(0, 0, 0, 0, 1, 1, 1, 1);
|
|
991
|
-
}
|
|
992
|
-
const remap = (surface?.materials ?? [{}]).map((material) => {
|
|
993
|
-
const copy = { ...material };
|
|
994
|
-
for (const channel of tileflowModelTextureChannels) {
|
|
995
|
-
const ref = copy[channel];
|
|
996
|
-
if (!ref) continue;
|
|
997
|
-
const texture = surface.textures[ref.texture];
|
|
998
|
-
let id2 = textureIds.get(texture);
|
|
999
|
-
if (id2 === void 0) {
|
|
1000
|
-
id2 = textures.length;
|
|
1001
|
-
textures.push(texture);
|
|
1002
|
-
textureIds.set(texture, id2);
|
|
1003
|
-
}
|
|
1004
|
-
copy[channel] = { ...ref, texture: id2 };
|
|
1005
|
-
}
|
|
1006
|
-
const key = JSON.stringify(copy);
|
|
1007
|
-
let id = materialIds.get(key);
|
|
1008
|
-
if (id === void 0) {
|
|
1009
|
-
id = materials.length;
|
|
1010
|
-
materials.push(copy);
|
|
1011
|
-
materialIds.set(key, id);
|
|
1012
|
-
}
|
|
1013
|
-
return id;
|
|
1014
|
-
});
|
|
1015
|
-
const sourceRanges = surface?.ranges ?? [
|
|
1016
|
-
{ from: 0, count: primitive.indices.length, material: 0 }
|
|
1017
|
-
];
|
|
1018
|
-
for (const range of sourceRanges) {
|
|
1019
|
-
const material = remap[range.material];
|
|
1020
|
-
const previous = ranges.at(-1);
|
|
1021
|
-
if (previous?.material === material && JSON.stringify(previous.emissiveBounds) === JSON.stringify(range.emissiveBounds))
|
|
1022
|
-
ranges[ranges.length - 1] = { ...previous, count: previous.count + range.count };
|
|
1023
|
-
else ranges.push({ ...range, from: indexCount + range.from, material });
|
|
969
|
+
// src/building-model-occlusion.ts
|
|
970
|
+
function buildingModelAmbientVisibility(mesh, options = {}) {
|
|
971
|
+
const sampler = buildingModelAmbientSampler(mesh, options);
|
|
972
|
+
const { positions, normals } = mesh;
|
|
973
|
+
const values = new Array(positions.length / 3);
|
|
974
|
+
const cache = /* @__PURE__ */ new Map();
|
|
975
|
+
for (let vertex = 0; vertex < values.length; vertex += 1) {
|
|
976
|
+
const at = vertex * 3;
|
|
977
|
+
const point3 = [positions[at], positions[at + 1], positions[at + 2]];
|
|
978
|
+
const normal = [normals[at], normals[at + 1], normals[at + 2]];
|
|
979
|
+
const key = [...point3, ...normal].map((v) => Math.round(v * 1e5)).join(",");
|
|
980
|
+
let value = cache.get(key);
|
|
981
|
+
if (value === void 0) {
|
|
982
|
+
value = sampler.sky(point3, normal);
|
|
983
|
+
cache.set(key, value);
|
|
1024
984
|
}
|
|
1025
|
-
|
|
985
|
+
values[vertex] = value;
|
|
1026
986
|
}
|
|
1027
|
-
|
|
1028
|
-
checkTileflowModelSurface(result, attributes.length / 8, indexCount);
|
|
1029
|
-
return result;
|
|
987
|
+
return values;
|
|
1030
988
|
}
|
|
1031
|
-
|
|
1032
|
-
// src/building-model-occlusion.ts
|
|
1033
|
-
function buildingModelAmbientVisibility(mesh, { radius = 2, samples = 48 } = {}) {
|
|
989
|
+
function buildingModelAmbientSampler(mesh, { radius = 2, samples = 48 } = {}) {
|
|
1034
990
|
if (!(radius > 0 && Number.isFinite(radius)) || !Number.isInteger(samples) || samples < 1 || samples > 256)
|
|
1035
991
|
throw new RangeError("Model ambient visibility needs a positive radius and 1 to 256 samples.");
|
|
1036
|
-
const { positions,
|
|
992
|
+
const { positions, indices } = mesh;
|
|
1037
993
|
const count = indices.length / 3;
|
|
1038
994
|
const triangles2 = new Float64Array(count * 9);
|
|
1039
995
|
const bounds = new Float64Array(count * 6);
|
|
@@ -1077,17 +1033,26 @@ function buildingModelAmbientVisibility(mesh, { radius = 2, samples = 48 } = {})
|
|
|
1077
1033
|
}
|
|
1078
1034
|
return index;
|
|
1079
1035
|
};
|
|
1080
|
-
build(0, count);
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1036
|
+
if (count > 0) build(0, count);
|
|
1037
|
+
const hemispheres = /* @__PURE__ */ new Map();
|
|
1038
|
+
const hemisphere = (count2) => {
|
|
1039
|
+
let pattern = hemispheres.get(count2);
|
|
1040
|
+
if (!pattern) {
|
|
1041
|
+
pattern = Array.from({ length: count2 }, (_, i) => {
|
|
1042
|
+
const r = Math.sqrt((i + 0.5) / count2), angle = i * Math.PI * (3 - Math.sqrt(5));
|
|
1043
|
+
return [r * Math.cos(angle), r * Math.sin(angle), Math.sqrt(1 - r * r)];
|
|
1044
|
+
});
|
|
1045
|
+
hemispheres.set(count2, pattern);
|
|
1046
|
+
}
|
|
1047
|
+
return pattern;
|
|
1048
|
+
};
|
|
1085
1049
|
const bias = Math.min(2e-3, radius * 1e-3);
|
|
1086
1050
|
const stack = new Int32Array(64);
|
|
1087
1051
|
const origin = [0, 0, 0];
|
|
1088
1052
|
const direction = [0, 0, 0];
|
|
1089
1053
|
const nearest = () => {
|
|
1090
1054
|
let best = radius, pending = 1;
|
|
1055
|
+
if (nodes.length === 0) return best;
|
|
1091
1056
|
stack[0] = 0;
|
|
1092
1057
|
while (pending > 0) {
|
|
1093
1058
|
const node = nodes[stack[--pending]];
|
|
@@ -1120,43 +1085,40 @@ function buildingModelAmbientVisibility(mesh, { radius = 2, samples = 48 } = {})
|
|
|
1120
1085
|
}
|
|
1121
1086
|
return best;
|
|
1122
1087
|
};
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1088
|
+
return {
|
|
1089
|
+
radius,
|
|
1090
|
+
sky(point3, normal, rays = samples, turn = 0) {
|
|
1091
|
+
const [nx, ny, nz] = normal;
|
|
1092
|
+
const flat = Math.abs(nz) < 0.9 ? [-ny, nx, 0] : [nz, 0, -nx];
|
|
1093
|
+
const length = Math.hypot(...flat);
|
|
1094
|
+
if (length === 0) return 1;
|
|
1095
|
+
const [c, s] = [Math.cos(turn), Math.sin(turn)];
|
|
1096
|
+
const base = flat.map((value) => value / length);
|
|
1097
|
+
const cross4 = [
|
|
1098
|
+
ny * base[2] - nz * base[1],
|
|
1099
|
+
nz * base[0] - nx * base[2],
|
|
1100
|
+
nx * base[1] - ny * base[0]
|
|
1101
|
+
];
|
|
1102
|
+
const tangent = base.map((value, axis) => value * c + cross4[axis] * s);
|
|
1103
|
+
const bitangent = cross4.map((value, axis) => value * c - base[axis] * s);
|
|
1104
|
+
for (let axis = 0; axis < 3; axis += 1) origin[axis] = point3[axis] + normal[axis] * bias;
|
|
1105
|
+
let visibility = 0;
|
|
1106
|
+
const pattern = hemisphere(rays);
|
|
1107
|
+
for (const sample of pattern) {
|
|
1108
|
+
for (let axis = 0; axis < 3; axis += 1)
|
|
1109
|
+
direction[axis] = sample[0] * tangent[axis] + sample[1] * bitangent[axis] + sample[2] * normal[axis];
|
|
1110
|
+
visibility += nearest() / radius;
|
|
1111
|
+
}
|
|
1112
|
+
return visibility / pattern.length;
|
|
1113
|
+
},
|
|
1114
|
+
along(point3, towards) {
|
|
1115
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
1116
|
+
origin[axis] = point3[axis] + towards[axis] * bias;
|
|
1117
|
+
direction[axis] = towards[axis];
|
|
1118
|
+
}
|
|
1119
|
+
return nearest();
|
|
1155
1120
|
}
|
|
1156
|
-
|
|
1157
|
-
cache.set(key, values[vertex]);
|
|
1158
|
-
}
|
|
1159
|
-
return values;
|
|
1121
|
+
};
|
|
1160
1122
|
}
|
|
1161
1123
|
function hitsBox(box, origin, direction, limit) {
|
|
1162
1124
|
let low = 0, high = limit;
|
|
@@ -1174,6 +1136,460 @@ function hitsBox(box, origin, direction, limit) {
|
|
|
1174
1136
|
return true;
|
|
1175
1137
|
}
|
|
1176
1138
|
|
|
1139
|
+
// src/building-model-lightmap.ts
|
|
1140
|
+
var paletteSide = 16;
|
|
1141
|
+
var maximumSide = 4096;
|
|
1142
|
+
function bakeBuildingModelLightmap(mesh, { texels: budget, radius = 2, ...options }) {
|
|
1143
|
+
if (options.points !== void 0 && !(options.points >= 1 && options.points <= 4 && Number.isInteger(options.points)))
|
|
1144
|
+
throw new RangeError("A lightmap samples one to four points along each side of a texel.");
|
|
1145
|
+
const { positions, indices } = mesh;
|
|
1146
|
+
const point3 = (vertex) => [
|
|
1147
|
+
positions[vertex * 3],
|
|
1148
|
+
positions[vertex * 3 + 1],
|
|
1149
|
+
positions[vertex * 3 + 2]
|
|
1150
|
+
];
|
|
1151
|
+
const charts = chartsOf(indices, point3);
|
|
1152
|
+
const palette = {
|
|
1153
|
+
triangles: [],
|
|
1154
|
+
normal: [0, 0, 1],
|
|
1155
|
+
u: [1, 0, 0],
|
|
1156
|
+
v: [0, 1, 0],
|
|
1157
|
+
min: [0, 0],
|
|
1158
|
+
extent: [0, 0],
|
|
1159
|
+
scale: [0, 0],
|
|
1160
|
+
w: 0,
|
|
1161
|
+
h: 0,
|
|
1162
|
+
x: 0,
|
|
1163
|
+
y: 0,
|
|
1164
|
+
palette: true
|
|
1165
|
+
};
|
|
1166
|
+
charts.push(palette);
|
|
1167
|
+
let density = densityFor(charts, budget);
|
|
1168
|
+
if (density === void 0) return void 0;
|
|
1169
|
+
let layout = pack(charts, density);
|
|
1170
|
+
for (let attempt = 0; attempt < 12 && !(layout && layout.width * layout.height <= budget); attempt++) {
|
|
1171
|
+
density *= 0.9;
|
|
1172
|
+
layout = pack(charts, density);
|
|
1173
|
+
}
|
|
1174
|
+
if (!layout || layout.width * layout.height > budget) return void 0;
|
|
1175
|
+
const { width, height } = layout;
|
|
1176
|
+
const points = options.points ?? Math.min(4, Math.max(2, Math.ceil(1 / (density * 0.35))));
|
|
1177
|
+
const rays = options.rays ?? Math.max(4, Math.round(48 / (points * points)));
|
|
1178
|
+
const sampler = buildingModelAmbientSampler(mesh, { radius });
|
|
1179
|
+
const sums = new Float64Array(width * height);
|
|
1180
|
+
const counts = new Uint16Array(width * height);
|
|
1181
|
+
const taken = new Uint16Array(width * height);
|
|
1182
|
+
const hidden = Math.min(0.05, radius * 0.025);
|
|
1183
|
+
for (const chart of charts) {
|
|
1184
|
+
const local2 = (vertex) => {
|
|
1185
|
+
const p = point3(vertex);
|
|
1186
|
+
return [
|
|
1187
|
+
(dot(chart.u, p) - chart.min[0]) * chart.scale[0] + 0.5,
|
|
1188
|
+
(dot(chart.v, p) - chart.min[1]) * chart.scale[1] + 0.5
|
|
1189
|
+
];
|
|
1190
|
+
};
|
|
1191
|
+
if (chart.tiny || chart.palette) continue;
|
|
1192
|
+
for (const triangle of chart.triangles) {
|
|
1193
|
+
const corners = [0, 1, 2].map((k) => local2(indices[triangle * 3 + k]));
|
|
1194
|
+
const [a, b, c] = corners;
|
|
1195
|
+
const area = cross2(a, b, c);
|
|
1196
|
+
if (Math.abs(area) < 1e-12) continue;
|
|
1197
|
+
const [A, B, C] = [0, 1, 2].map((k) => point3(indices[triangle * 3 + k]));
|
|
1198
|
+
const rawNormal = crossOf(sub(B, A), sub(C, A));
|
|
1199
|
+
const facing = rawNormal.map((value) => value / (Math.hypot(...rawNormal) || 1));
|
|
1200
|
+
const x0 = Math.max(0, Math.floor(Math.min(a[0], b[0], c[0])));
|
|
1201
|
+
const x1 = Math.min(chart.w - 1, Math.floor(Math.max(a[0], b[0], c[0])));
|
|
1202
|
+
const y0 = Math.max(0, Math.floor(Math.min(a[1], b[1], c[1])));
|
|
1203
|
+
const y1 = Math.min(chart.h - 1, Math.floor(Math.max(a[1], b[1], c[1])));
|
|
1204
|
+
for (let y = y0; y <= y1; y++)
|
|
1205
|
+
for (let x = x0; x <= x1; x++) {
|
|
1206
|
+
const texel = (chart.y + y) * width + chart.x + x;
|
|
1207
|
+
for (let j = 0; j < points; j++)
|
|
1208
|
+
for (let i = 0; i < points; i++) {
|
|
1209
|
+
const bit = 1 << j * points + i;
|
|
1210
|
+
if (taken[texel] & bit) continue;
|
|
1211
|
+
const p = [x + (i + 0.5) / points, y + (j + 0.5) / points];
|
|
1212
|
+
const w0 = cross2(b, c, p) / area, w1 = cross2(c, a, p) / area, w2 = 1 - w0 - w1;
|
|
1213
|
+
if (w0 < -1e-9 || w1 < -1e-9 || w2 < -1e-9) continue;
|
|
1214
|
+
taken[texel] |= bit;
|
|
1215
|
+
const world = [0, 1, 2].map((axis) => w0 * A[axis] + w1 * B[axis] + w2 * C[axis]);
|
|
1216
|
+
if (sampler.along(world, facing) < hidden) continue;
|
|
1217
|
+
const turn = ((j * points + i) * 2.399963 + (x * 0.618034 + y * 0.381966) * 6.283185) % 6.283185;
|
|
1218
|
+
sums[texel] += sampler.sky(world, facing, rays, turn);
|
|
1219
|
+
counts[texel] += 1;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
const values = new Float32Array(width * height).fill(-1);
|
|
1225
|
+
for (let texel = 0; texel < values.length; texel++)
|
|
1226
|
+
if (counts[texel]) values[texel] = sums[texel] / counts[texel];
|
|
1227
|
+
for (let level = 0; level < paletteSide * paletteSide; level++) {
|
|
1228
|
+
const x = palette.x + level % paletteSide, y = palette.y + Math.floor(level / paletteSide);
|
|
1229
|
+
values[y * width + x] = level / 255;
|
|
1230
|
+
}
|
|
1231
|
+
const levels = /* @__PURE__ */ new Map();
|
|
1232
|
+
for (const chart of charts) {
|
|
1233
|
+
if (!chart.tiny) continue;
|
|
1234
|
+
let sum = 0, weight = 0;
|
|
1235
|
+
for (const triangle of chart.triangles) {
|
|
1236
|
+
const [a, b, c] = [0, 1, 2].map((k) => point3(indices[triangle * 3 + k]));
|
|
1237
|
+
const raw = crossOf(sub(b, a), sub(c, a));
|
|
1238
|
+
const area = Math.hypot(...raw) / 2;
|
|
1239
|
+
if (!(area > 0)) continue;
|
|
1240
|
+
const facing = raw.map((value) => value / (2 * area));
|
|
1241
|
+
const centre = [0, 1, 2].map((axis) => (a[axis] + b[axis] + c[axis]) / 3);
|
|
1242
|
+
if (sampler.along(centre, facing) < hidden) continue;
|
|
1243
|
+
sum += area * sampler.sky(centre, facing, rays * points * points);
|
|
1244
|
+
weight += area;
|
|
1245
|
+
}
|
|
1246
|
+
levels.set(chart, Math.round((weight > 0 ? sum / weight : 1) * 255));
|
|
1247
|
+
}
|
|
1248
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
1249
|
+
const previous = values.slice();
|
|
1250
|
+
for (let y = 0; y < height; y++)
|
|
1251
|
+
for (let x = 0; x < width; x++) {
|
|
1252
|
+
if (previous[y * width + x] >= 0) continue;
|
|
1253
|
+
let sum = 0, count = 0;
|
|
1254
|
+
for (const [dx, dy] of [
|
|
1255
|
+
[1, 0],
|
|
1256
|
+
[-1, 0],
|
|
1257
|
+
[0, 1],
|
|
1258
|
+
[0, -1]
|
|
1259
|
+
]) {
|
|
1260
|
+
const xx = x + dx, yy = y + dy;
|
|
1261
|
+
if (xx < 0 || yy < 0 || xx >= width || yy >= height) continue;
|
|
1262
|
+
const value = previous[yy * width + xx];
|
|
1263
|
+
if (value >= 0) {
|
|
1264
|
+
sum += value;
|
|
1265
|
+
count++;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
if (count) values[y * width + x] = sum / count;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
const bytes = new Uint8Array(width * height);
|
|
1272
|
+
for (let texel = 0; texel < bytes.length; texel++)
|
|
1273
|
+
bytes[texel] = Math.round(
|
|
1274
|
+
Math.min(1, Math.max(0, values[texel] < 0 ? 1 : values[texel])) * 255
|
|
1275
|
+
);
|
|
1276
|
+
const sources = [];
|
|
1277
|
+
const uvs = [];
|
|
1278
|
+
const out = new Array(indices.length);
|
|
1279
|
+
for (const chart of charts) {
|
|
1280
|
+
const own = /* @__PURE__ */ new Map();
|
|
1281
|
+
for (const triangle of chart.triangles)
|
|
1282
|
+
for (let k = 0; k < 3; k++) {
|
|
1283
|
+
const vertex = indices[triangle * 3 + k];
|
|
1284
|
+
let index = own.get(vertex);
|
|
1285
|
+
if (index === void 0) {
|
|
1286
|
+
index = sources.length;
|
|
1287
|
+
own.set(vertex, index);
|
|
1288
|
+
sources.push(vertex);
|
|
1289
|
+
const level = levels.get(chart);
|
|
1290
|
+
if (level !== void 0)
|
|
1291
|
+
uvs.push(
|
|
1292
|
+
(palette.x + level % paletteSide + 0.5) / width,
|
|
1293
|
+
(palette.y + Math.floor(level / paletteSide) + 0.5) / height
|
|
1294
|
+
);
|
|
1295
|
+
else {
|
|
1296
|
+
const p = point3(vertex);
|
|
1297
|
+
uvs.push(
|
|
1298
|
+
(chart.x + 0.5 + (dot(chart.u, p) - chart.min[0]) * chart.scale[0]) / width,
|
|
1299
|
+
(chart.y + 0.5 + (dot(chart.v, p) - chart.min[1]) * chart.scale[1]) / height
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
out[triangle * 3 + k] = index;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
for (let at = 0; at < uvs.length; at++) uvs[at] = Math.round(uvs[at] * 65536) / 65536;
|
|
1307
|
+
return {
|
|
1308
|
+
sources,
|
|
1309
|
+
indices: out,
|
|
1310
|
+
uvs,
|
|
1311
|
+
width,
|
|
1312
|
+
height,
|
|
1313
|
+
density,
|
|
1314
|
+
texels: bytes
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
var chartCone = Math.cos(20 * Math.PI / 180);
|
|
1318
|
+
function chartsOf(indices, point3) {
|
|
1319
|
+
const key = (p) => p.map((value) => Math.round(value * 1e4)).join(",");
|
|
1320
|
+
const count = indices.length / 3;
|
|
1321
|
+
const normals = [];
|
|
1322
|
+
const areas = [];
|
|
1323
|
+
const edges = /* @__PURE__ */ new Map();
|
|
1324
|
+
const edgeKeys = [];
|
|
1325
|
+
for (let triangle = 0; triangle < count; triangle++) {
|
|
1326
|
+
const [a, b, c] = [0, 1, 2].map((k) => point3(indices[triangle * 3 + k]));
|
|
1327
|
+
const normal = crossOf(sub(b, a), sub(c, a));
|
|
1328
|
+
const length = Math.hypot(...normal);
|
|
1329
|
+
areas.push(length / 2);
|
|
1330
|
+
normals.push(length > 1e-12 ? normal.map((value) => value / length) : [0, 0, 1]);
|
|
1331
|
+
for (const [from, to] of [
|
|
1332
|
+
[a, b],
|
|
1333
|
+
[b, c],
|
|
1334
|
+
[c, a]
|
|
1335
|
+
]) {
|
|
1336
|
+
const [u, v] = [key(from), key(to)];
|
|
1337
|
+
const edge2 = u < v ? `${u}|${v}` : `${v}|${u}`;
|
|
1338
|
+
edgeKeys.push(edge2);
|
|
1339
|
+
const users = edges.get(edge2);
|
|
1340
|
+
if (users) users.push(triangle);
|
|
1341
|
+
else edges.set(edge2, [triangle]);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
const owner = new Int32Array(count).fill(-1);
|
|
1345
|
+
const order = Array.from({ length: count }, (_, i) => i).sort(
|
|
1346
|
+
(a, b) => areas[b] - areas[a] || a - b
|
|
1347
|
+
);
|
|
1348
|
+
const charts = [];
|
|
1349
|
+
for (const seed of order) {
|
|
1350
|
+
if (owner[seed] !== -1) continue;
|
|
1351
|
+
const normal = normals[seed];
|
|
1352
|
+
const group = [seed];
|
|
1353
|
+
owner[seed] = seed;
|
|
1354
|
+
for (let at = 0; at < group.length; at++)
|
|
1355
|
+
for (let e = 0; e < 3; e++)
|
|
1356
|
+
for (const next of edges.get(edgeKeys[group[at] * 3 + e]))
|
|
1357
|
+
if (owner[next] === -1 && dot(normals[next], normal) >= chartCone) {
|
|
1358
|
+
owner[next] = seed;
|
|
1359
|
+
group.push(next);
|
|
1360
|
+
}
|
|
1361
|
+
charts.push(...flatCharts(group, normal, indices, point3));
|
|
1362
|
+
}
|
|
1363
|
+
return charts;
|
|
1364
|
+
}
|
|
1365
|
+
function flatCharts(group, normal, indices, point3, depth = 0) {
|
|
1366
|
+
let longest = 0;
|
|
1367
|
+
let u = Math.abs(normal[2]) < 0.9 ? [-normal[1], normal[0], 0] : [normal[2], 0, -normal[0]];
|
|
1368
|
+
let area = 0;
|
|
1369
|
+
for (const triangle of group) {
|
|
1370
|
+
const [a, b, c] = [0, 1, 2].map((k) => point3(indices[triangle * 3 + k]));
|
|
1371
|
+
area += Math.hypot(...crossOf(sub(b, a), sub(c, a))) / 2;
|
|
1372
|
+
for (const [from, to] of [
|
|
1373
|
+
[a, b],
|
|
1374
|
+
[b, c],
|
|
1375
|
+
[c, a]
|
|
1376
|
+
]) {
|
|
1377
|
+
const d = sub(to, from);
|
|
1378
|
+
const along = d.map((value, axis) => value - normal[axis] * dot(d, normal));
|
|
1379
|
+
const length = Math.hypot(...along);
|
|
1380
|
+
if (length > longest) {
|
|
1381
|
+
longest = length;
|
|
1382
|
+
u = along;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
const size = Math.hypot(...u) || 1;
|
|
1387
|
+
u = u.map((value) => value / size);
|
|
1388
|
+
const v = crossOf(normal, u);
|
|
1389
|
+
let [minU, minV, maxU, maxV] = [Infinity, Infinity, -Infinity, -Infinity];
|
|
1390
|
+
for (const triangle of group)
|
|
1391
|
+
for (let k = 0; k < 3; k++) {
|
|
1392
|
+
const p = point3(indices[triangle * 3 + k]);
|
|
1393
|
+
minU = Math.min(minU, dot(u, p));
|
|
1394
|
+
maxU = Math.max(maxU, dot(u, p));
|
|
1395
|
+
minV = Math.min(minV, dot(v, p));
|
|
1396
|
+
maxV = Math.max(maxV, dot(v, p));
|
|
1397
|
+
}
|
|
1398
|
+
const flip = maxV - minV > maxU - minU;
|
|
1399
|
+
if (group.length > 1 && depth < 16 && area < 0.5 * (maxU - minU) * (maxV - minV)) {
|
|
1400
|
+
const axis = flip ? v : u;
|
|
1401
|
+
const centre = (triangle) => [0, 1, 2].reduce((sum, k) => sum + dot(axis, point3(indices[triangle * 3 + k])), 0) / 3;
|
|
1402
|
+
const sorted = [...group].sort((a, b) => centre(a) - centre(b) || a - b);
|
|
1403
|
+
const half = sorted.length >> 1;
|
|
1404
|
+
return [
|
|
1405
|
+
...flatCharts(sorted.slice(0, half), normal, indices, point3, depth + 1),
|
|
1406
|
+
...flatCharts(sorted.slice(half), normal, indices, point3, depth + 1)
|
|
1407
|
+
];
|
|
1408
|
+
}
|
|
1409
|
+
return [
|
|
1410
|
+
{
|
|
1411
|
+
triangles: group,
|
|
1412
|
+
normal,
|
|
1413
|
+
u: flip ? v : u,
|
|
1414
|
+
v: flip ? u : v,
|
|
1415
|
+
min: flip ? [minV, minU] : [minU, minV],
|
|
1416
|
+
extent: flip ? [maxV - minV, maxU - minU] : [maxU - minU, maxV - minV],
|
|
1417
|
+
scale: [0, 0],
|
|
1418
|
+
w: 0,
|
|
1419
|
+
h: 0,
|
|
1420
|
+
x: 0,
|
|
1421
|
+
y: 0
|
|
1422
|
+
}
|
|
1423
|
+
];
|
|
1424
|
+
}
|
|
1425
|
+
function sizeAt(chart, density) {
|
|
1426
|
+
if (chart.palette) return [paletteSide, paletteSide];
|
|
1427
|
+
chart.tiny = chart.extent[0] * density < 2 && chart.extent[1] * density < 2;
|
|
1428
|
+
if (chart.tiny) return [0, 0];
|
|
1429
|
+
const size = chart.extent.map((extent) => Math.max(2, Math.round(extent * density) + 1));
|
|
1430
|
+
chart.scale = [0, 1].map(
|
|
1431
|
+
(axis) => chart.extent[axis] > 0 ? (size[axis] - 1) / chart.extent[axis] : 0
|
|
1432
|
+
);
|
|
1433
|
+
return size;
|
|
1434
|
+
}
|
|
1435
|
+
function densityFor(charts, budget) {
|
|
1436
|
+
const area = (density) => charts.reduce((sum, chart) => {
|
|
1437
|
+
const [w, h] = sizeAt(chart, density);
|
|
1438
|
+
return sum + w * h;
|
|
1439
|
+
}, 0);
|
|
1440
|
+
let low = 1 / 16, high = 64;
|
|
1441
|
+
if (area(low) / 0.8 > budget) return void 0;
|
|
1442
|
+
for (let step = 0; step < 24; step++) {
|
|
1443
|
+
const middle = Math.sqrt(low * high);
|
|
1444
|
+
if (area(middle) / 0.8 <= budget) low = middle;
|
|
1445
|
+
else high = middle;
|
|
1446
|
+
}
|
|
1447
|
+
return low;
|
|
1448
|
+
}
|
|
1449
|
+
function pack(charts, density) {
|
|
1450
|
+
for (const chart of charts) [chart.w, chart.h] = sizeAt(chart, density);
|
|
1451
|
+
const order = charts.map((_, i) => i).sort((a, b) => charts[b].h - charts[a].h || charts[b].w - charts[a].w || a - b);
|
|
1452
|
+
let best;
|
|
1453
|
+
for (let width = 64; width <= maximumSide; width *= 2) {
|
|
1454
|
+
let line = [{ x: 0, y: 0, w: width }];
|
|
1455
|
+
const places = new Array(charts.length);
|
|
1456
|
+
let fits = true;
|
|
1457
|
+
for (const index of order) {
|
|
1458
|
+
const chart = charts[index];
|
|
1459
|
+
if (chart.w === 0) {
|
|
1460
|
+
places[index] = [0, 0];
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
if (chart.w > width) {
|
|
1464
|
+
fits = false;
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
let place;
|
|
1468
|
+
for (let i = 0; i < line.length; i++) {
|
|
1469
|
+
const x = line[i].x;
|
|
1470
|
+
if (x + chart.w > width) break;
|
|
1471
|
+
let y = 0, covered = 0;
|
|
1472
|
+
for (let j = i; j < line.length && covered < chart.w; j++) {
|
|
1473
|
+
y = Math.max(y, line[j].y);
|
|
1474
|
+
covered += line[j].w;
|
|
1475
|
+
}
|
|
1476
|
+
if (covered < chart.w) continue;
|
|
1477
|
+
if (!place || y < place.y) place = { x, y };
|
|
1478
|
+
}
|
|
1479
|
+
if (!place || place.y + chart.h > maximumSide) {
|
|
1480
|
+
fits = false;
|
|
1481
|
+
break;
|
|
1482
|
+
}
|
|
1483
|
+
places[index] = [place.x, place.y];
|
|
1484
|
+
const next = [];
|
|
1485
|
+
for (const segment of line) {
|
|
1486
|
+
const end = segment.x + segment.w;
|
|
1487
|
+
if (end <= place.x || segment.x >= place.x + chart.w) next.push(segment);
|
|
1488
|
+
else {
|
|
1489
|
+
if (segment.x < place.x) next.push({ x: segment.x, y: segment.y, w: place.x - segment.x });
|
|
1490
|
+
if (end > place.x + chart.w)
|
|
1491
|
+
next.push({ x: place.x + chart.w, y: segment.y, w: end - place.x - chart.w });
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
next.push({ x: place.x, y: place.y + chart.h, w: chart.w });
|
|
1495
|
+
next.sort((a, b) => a.x - b.x);
|
|
1496
|
+
line = [];
|
|
1497
|
+
for (const segment of next) {
|
|
1498
|
+
const last = line.at(-1);
|
|
1499
|
+
if (last && last.y === segment.y && last.x + last.w === segment.x) last.w += segment.w;
|
|
1500
|
+
else line.push({ ...segment });
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
if (!fits) continue;
|
|
1504
|
+
const height = Math.max(...line.map((segment) => segment.y));
|
|
1505
|
+
if (!best || width * height < best.width * best.height) best = { width, height, places };
|
|
1506
|
+
}
|
|
1507
|
+
if (!best) return void 0;
|
|
1508
|
+
charts.forEach((chart, i) => [chart.x, chart.y] = best.places[i]);
|
|
1509
|
+
return { width: best.width, height: best.height };
|
|
1510
|
+
}
|
|
1511
|
+
function dot(a, b) {
|
|
1512
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
1513
|
+
}
|
|
1514
|
+
function sub(a, b) {
|
|
1515
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
1516
|
+
}
|
|
1517
|
+
function crossOf(a, b) {
|
|
1518
|
+
return [
|
|
1519
|
+
a[1] * b[2] - a[2] * b[1],
|
|
1520
|
+
a[2] * b[0] - a[0] * b[2],
|
|
1521
|
+
a[0] * b[1] - a[1] * b[0]
|
|
1522
|
+
];
|
|
1523
|
+
}
|
|
1524
|
+
function cross2(a, b, p) {
|
|
1525
|
+
return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// src/building-model-materials.ts
|
|
1529
|
+
import {
|
|
1530
|
+
checkTileflowModelSurface,
|
|
1531
|
+
tileflowModelTextureChannels
|
|
1532
|
+
} from "@tileflow/core";
|
|
1533
|
+
function buildingModelSurface(primitives) {
|
|
1534
|
+
if (!primitives.some((primitive) => primitive.surface)) return void 0;
|
|
1535
|
+
const attributes = [];
|
|
1536
|
+
const materials = [];
|
|
1537
|
+
const materialIds = /* @__PURE__ */ new Map();
|
|
1538
|
+
const textures = [];
|
|
1539
|
+
const textureIds = /* @__PURE__ */ new Map();
|
|
1540
|
+
const ranges = [];
|
|
1541
|
+
const emitters = [];
|
|
1542
|
+
let indexCount = 0;
|
|
1543
|
+
for (const primitive of primitives) {
|
|
1544
|
+
const count = primitive.positions.length / 3;
|
|
1545
|
+
const surface = primitive.surface;
|
|
1546
|
+
if (surface) {
|
|
1547
|
+
checkTileflowModelSurface(surface, count, primitive.indices.length);
|
|
1548
|
+
for (const value of surface.attributes) attributes.push(value);
|
|
1549
|
+
emitters.push(...surface.emitters ?? []);
|
|
1550
|
+
} else {
|
|
1551
|
+
for (let vertex = 0; vertex < count; vertex++) attributes.push(0, 0, 0, 0, 1, 1, 1, 1);
|
|
1552
|
+
}
|
|
1553
|
+
const remap = (surface?.materials ?? [{}]).map((material) => {
|
|
1554
|
+
const copy = { ...material };
|
|
1555
|
+
for (const channel of tileflowModelTextureChannels) {
|
|
1556
|
+
const ref = copy[channel];
|
|
1557
|
+
if (!ref) continue;
|
|
1558
|
+
const texture = surface.textures[ref.texture];
|
|
1559
|
+
let id2 = textureIds.get(texture);
|
|
1560
|
+
if (id2 === void 0) {
|
|
1561
|
+
id2 = textures.length;
|
|
1562
|
+
textures.push(texture);
|
|
1563
|
+
textureIds.set(texture, id2);
|
|
1564
|
+
}
|
|
1565
|
+
copy[channel] = { ...ref, texture: id2 };
|
|
1566
|
+
}
|
|
1567
|
+
const key = JSON.stringify(copy);
|
|
1568
|
+
let id = materialIds.get(key);
|
|
1569
|
+
if (id === void 0) {
|
|
1570
|
+
id = materials.length;
|
|
1571
|
+
materials.push(copy);
|
|
1572
|
+
materialIds.set(key, id);
|
|
1573
|
+
}
|
|
1574
|
+
return id;
|
|
1575
|
+
});
|
|
1576
|
+
const sourceRanges = surface?.ranges ?? [
|
|
1577
|
+
{ from: 0, count: primitive.indices.length, material: 0 }
|
|
1578
|
+
];
|
|
1579
|
+
for (const range of sourceRanges) {
|
|
1580
|
+
const material = remap[range.material];
|
|
1581
|
+
const previous = ranges.at(-1);
|
|
1582
|
+
if (previous?.material === material && JSON.stringify(previous.emissiveBounds) === JSON.stringify(range.emissiveBounds))
|
|
1583
|
+
ranges[ranges.length - 1] = { ...previous, count: previous.count + range.count };
|
|
1584
|
+
else ranges.push({ ...range, from: indexCount + range.from, material });
|
|
1585
|
+
}
|
|
1586
|
+
indexCount += primitive.indices.length;
|
|
1587
|
+
}
|
|
1588
|
+
const result = { attributes, materials, textures, ranges, ...emitters.length ? { emitters } : {} };
|
|
1589
|
+
checkTileflowModelSurface(result, attributes.length / 8, indexCount);
|
|
1590
|
+
return result;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1177
1593
|
// src/gltf-materials.ts
|
|
1178
1594
|
import {
|
|
1179
1595
|
checkTileflowModelSurface as checkTileflowModelSurface2,
|
|
@@ -1282,6 +1698,8 @@ function createGltfMaterialReader(json, bin, accessor2) {
|
|
|
1282
1698
|
return value;
|
|
1283
1699
|
};
|
|
1284
1700
|
const uv0 = read("TEXCOORD_0", [2]), uv1 = read("TEXCOORD_1", [2]);
|
|
1701
|
+
if (uv1?.values.some((value) => !(value >= 0 && value <= 1)))
|
|
1702
|
+
throw new RangeError("TEXCOORD_1 must lie between zero and one.");
|
|
1285
1703
|
const colour = read("COLOR_0", [3, 4]);
|
|
1286
1704
|
for (const key of [
|
|
1287
1705
|
"baseColorTexture",
|
|
@@ -1681,41 +2099,486 @@ function vector(value, fallback) {
|
|
|
1681
2099
|
return list2.length === fallback.length && list2.every(Number.isFinite) ? list2 : fallback;
|
|
1682
2100
|
}
|
|
1683
2101
|
|
|
1684
|
-
// src/
|
|
1685
|
-
var
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
2102
|
+
// src/grey-jpeg.ts
|
|
2103
|
+
var zigzag = [
|
|
2104
|
+
0,
|
|
2105
|
+
1,
|
|
2106
|
+
8,
|
|
2107
|
+
16,
|
|
2108
|
+
9,
|
|
2109
|
+
2,
|
|
2110
|
+
3,
|
|
2111
|
+
10,
|
|
2112
|
+
17,
|
|
2113
|
+
24,
|
|
2114
|
+
32,
|
|
2115
|
+
25,
|
|
2116
|
+
18,
|
|
2117
|
+
11,
|
|
2118
|
+
4,
|
|
2119
|
+
5,
|
|
2120
|
+
12,
|
|
2121
|
+
19,
|
|
2122
|
+
26,
|
|
2123
|
+
33,
|
|
2124
|
+
40,
|
|
2125
|
+
48,
|
|
2126
|
+
41,
|
|
2127
|
+
34,
|
|
2128
|
+
27,
|
|
2129
|
+
20,
|
|
2130
|
+
13,
|
|
2131
|
+
6,
|
|
2132
|
+
7,
|
|
2133
|
+
14,
|
|
2134
|
+
21,
|
|
2135
|
+
28,
|
|
2136
|
+
35,
|
|
2137
|
+
42,
|
|
2138
|
+
49,
|
|
2139
|
+
56,
|
|
2140
|
+
57,
|
|
2141
|
+
50,
|
|
2142
|
+
43,
|
|
2143
|
+
36,
|
|
2144
|
+
29,
|
|
2145
|
+
22,
|
|
2146
|
+
15,
|
|
2147
|
+
23,
|
|
2148
|
+
30,
|
|
2149
|
+
37,
|
|
2150
|
+
44,
|
|
2151
|
+
51,
|
|
2152
|
+
58,
|
|
2153
|
+
59,
|
|
2154
|
+
52,
|
|
2155
|
+
45,
|
|
2156
|
+
38,
|
|
2157
|
+
31,
|
|
2158
|
+
39,
|
|
2159
|
+
46,
|
|
2160
|
+
53,
|
|
2161
|
+
60,
|
|
2162
|
+
61,
|
|
2163
|
+
54,
|
|
2164
|
+
47,
|
|
2165
|
+
55,
|
|
2166
|
+
62,
|
|
2167
|
+
63
|
|
2168
|
+
];
|
|
2169
|
+
var luminance = [
|
|
2170
|
+
16,
|
|
2171
|
+
11,
|
|
2172
|
+
10,
|
|
2173
|
+
16,
|
|
2174
|
+
24,
|
|
2175
|
+
40,
|
|
2176
|
+
51,
|
|
2177
|
+
61,
|
|
2178
|
+
12,
|
|
2179
|
+
12,
|
|
2180
|
+
14,
|
|
2181
|
+
19,
|
|
2182
|
+
26,
|
|
2183
|
+
58,
|
|
2184
|
+
60,
|
|
2185
|
+
55,
|
|
2186
|
+
14,
|
|
2187
|
+
13,
|
|
2188
|
+
16,
|
|
2189
|
+
24,
|
|
2190
|
+
40,
|
|
2191
|
+
57,
|
|
2192
|
+
69,
|
|
2193
|
+
56,
|
|
2194
|
+
14,
|
|
2195
|
+
17,
|
|
2196
|
+
22,
|
|
2197
|
+
29,
|
|
2198
|
+
51,
|
|
2199
|
+
87,
|
|
2200
|
+
80,
|
|
2201
|
+
62,
|
|
2202
|
+
18,
|
|
2203
|
+
22,
|
|
2204
|
+
37,
|
|
2205
|
+
56,
|
|
2206
|
+
68,
|
|
2207
|
+
109,
|
|
2208
|
+
103,
|
|
2209
|
+
77,
|
|
2210
|
+
24,
|
|
2211
|
+
35,
|
|
2212
|
+
55,
|
|
2213
|
+
64,
|
|
2214
|
+
81,
|
|
2215
|
+
104,
|
|
2216
|
+
113,
|
|
2217
|
+
92,
|
|
2218
|
+
49,
|
|
2219
|
+
64,
|
|
2220
|
+
78,
|
|
2221
|
+
87,
|
|
2222
|
+
103,
|
|
2223
|
+
121,
|
|
2224
|
+
120,
|
|
2225
|
+
101,
|
|
2226
|
+
72,
|
|
2227
|
+
92,
|
|
2228
|
+
95,
|
|
2229
|
+
98,
|
|
2230
|
+
112,
|
|
2231
|
+
100,
|
|
2232
|
+
103,
|
|
2233
|
+
99
|
|
2234
|
+
];
|
|
2235
|
+
function huffman(counts, symbols) {
|
|
2236
|
+
const out = { code: [], size: [] };
|
|
2237
|
+
let code = 0, k = 0;
|
|
2238
|
+
for (let length = 1; length <= 16; length++) {
|
|
2239
|
+
for (let i = 0; i < counts[length - 1]; i++) {
|
|
2240
|
+
out.code[symbols[k]] = code;
|
|
2241
|
+
out.size[symbols[k]] = length;
|
|
2242
|
+
code++;
|
|
2243
|
+
k++;
|
|
2244
|
+
}
|
|
2245
|
+
code <<= 1;
|
|
1698
2246
|
}
|
|
1699
|
-
return
|
|
2247
|
+
return out;
|
|
1700
2248
|
}
|
|
1701
|
-
|
|
1702
|
-
const
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
const
|
|
1710
|
-
const
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
2249
|
+
var basis = Array.from({ length: 64 }, (_, i) => {
|
|
2250
|
+
const u = i >> 3, x = i & 7;
|
|
2251
|
+
return (u === 0 ? Math.SQRT1_2 : 1) * 0.5 * Math.cos((2 * x + 1) * u * Math.PI / 16);
|
|
2252
|
+
});
|
|
2253
|
+
function encodeGreyJpeg(pixels, width, height, quality) {
|
|
2254
|
+
if (!(width > 0 && height > 0 && width <= 65535 && height <= 65535))
|
|
2255
|
+
throw new RangeError("A JPEG is 1 to 65,535 pixels on each side.");
|
|
2256
|
+
if (pixels.length !== width * height) throw new RangeError("One byte a pixel is required.");
|
|
2257
|
+
const q = Math.min(100, Math.max(1, Math.round(quality)));
|
|
2258
|
+
const scale = q < 50 ? 5e3 / q : 200 - 2 * q;
|
|
2259
|
+
const table = luminance.map(
|
|
2260
|
+
(value) => Math.min(255, Math.max(1, Math.floor((value * scale + 50) / 100)))
|
|
2261
|
+
);
|
|
2262
|
+
const symbols = [];
|
|
2263
|
+
const extra = [];
|
|
2264
|
+
const frequencies = [new Array(257).fill(0), new Array(257).fill(0)];
|
|
2265
|
+
const emit = (table2, symbol, value = 0, size = 0) => {
|
|
2266
|
+
symbols.push(table2 << 8 | symbol);
|
|
2267
|
+
extra.push(size ? (value < 0 ? value + (1 << size) - 1 : value) << 5 | size : 0);
|
|
2268
|
+
frequencies[table2][symbol] += 1;
|
|
2269
|
+
};
|
|
2270
|
+
const magnitude = (value) => {
|
|
2271
|
+
let size = 0;
|
|
2272
|
+
for (let v = Math.abs(value); v > 0; v >>= 1) size++;
|
|
2273
|
+
return size;
|
|
2274
|
+
};
|
|
2275
|
+
const block = new Float64Array(64);
|
|
2276
|
+
const row = new Float64Array(64);
|
|
2277
|
+
const coefficients = new Int32Array(64);
|
|
2278
|
+
let previous = 0;
|
|
2279
|
+
for (let by = 0; by < height; by += 8)
|
|
2280
|
+
for (let bx = 0; bx < width; bx += 8) {
|
|
2281
|
+
for (let y = 0; y < 8; y++)
|
|
2282
|
+
for (let x = 0; x < 8; x++)
|
|
2283
|
+
block[y * 8 + x] = pixels[Math.min(height - 1, by + y) * width + Math.min(width - 1, bx + x)] - 128;
|
|
2284
|
+
for (let y = 0; y < 8; y++)
|
|
2285
|
+
for (let u = 0; u < 8; u++) {
|
|
2286
|
+
let sum = 0;
|
|
2287
|
+
for (let x = 0; x < 8; x++) sum += block[y * 8 + x] * basis[u * 8 + x];
|
|
2288
|
+
row[y * 8 + u] = sum;
|
|
2289
|
+
}
|
|
2290
|
+
for (let v = 0; v < 8; v++)
|
|
2291
|
+
for (let u = 0; u < 8; u++) {
|
|
2292
|
+
let sum = 0;
|
|
2293
|
+
for (let y = 0; y < 8; y++) sum += row[y * 8 + u] * basis[v * 8 + y];
|
|
2294
|
+
coefficients[v * 8 + u] = Math.round(sum / table[v * 8 + u]);
|
|
2295
|
+
}
|
|
2296
|
+
const dc = coefficients[0] - previous;
|
|
2297
|
+
previous = coefficients[0];
|
|
2298
|
+
const dcSize = magnitude(dc);
|
|
2299
|
+
emit(0, dcSize, dc, dcSize);
|
|
2300
|
+
let zeros = 0;
|
|
2301
|
+
for (let k = 1; k < 64; k++) {
|
|
2302
|
+
const value = coefficients[zigzag[k]];
|
|
2303
|
+
if (value === 0) {
|
|
2304
|
+
zeros++;
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
for (; zeros > 15; zeros -= 16) emit(1, 240);
|
|
2308
|
+
const size = magnitude(value);
|
|
2309
|
+
emit(1, zeros << 4 | size, value, size);
|
|
2310
|
+
zeros = 0;
|
|
2311
|
+
}
|
|
2312
|
+
if (zeros > 0) emit(1, 0);
|
|
2313
|
+
}
|
|
2314
|
+
const tables = frequencies.map(optimalTable);
|
|
2315
|
+
const codes = tables.map(({ counts, values }) => huffman(counts, values));
|
|
2316
|
+
const out = [];
|
|
2317
|
+
const word = (value) => out.push(value >> 8 & 255, value & 255);
|
|
2318
|
+
word(65496);
|
|
2319
|
+
word(65499);
|
|
2320
|
+
word(67);
|
|
2321
|
+
out.push(0, ...zigzag.map((at) => table[at]));
|
|
2322
|
+
word(65472);
|
|
2323
|
+
word(11);
|
|
2324
|
+
out.push(8);
|
|
2325
|
+
word(height);
|
|
2326
|
+
word(width);
|
|
2327
|
+
out.push(1, 1, 17, 0);
|
|
2328
|
+
for (const [index, { counts, values }] of tables.entries()) {
|
|
2329
|
+
word(65476);
|
|
2330
|
+
word(3 + 16 + values.length);
|
|
2331
|
+
out.push(index << 4, ...counts, ...values);
|
|
2332
|
+
}
|
|
2333
|
+
word(65498);
|
|
2334
|
+
word(8);
|
|
2335
|
+
out.push(1, 1, 0, 0, 63, 0);
|
|
2336
|
+
let bits = 0, count = 0;
|
|
2337
|
+
const put = (code, size) => {
|
|
2338
|
+
bits = bits << size | code & (1 << size) - 1;
|
|
2339
|
+
count += size;
|
|
2340
|
+
while (count >= 8) {
|
|
2341
|
+
const byte = bits >> count - 8 & 255;
|
|
2342
|
+
out.push(byte);
|
|
2343
|
+
if (byte === 255) out.push(0);
|
|
2344
|
+
count -= 8;
|
|
2345
|
+
}
|
|
2346
|
+
bits &= (1 << count) - 1;
|
|
2347
|
+
};
|
|
2348
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
2349
|
+
const own = codes[symbols[i] >> 8];
|
|
2350
|
+
const symbol = symbols[i] & 255;
|
|
2351
|
+
put(own.code[symbol], own.size[symbol]);
|
|
2352
|
+
const size = extra[i] & 31;
|
|
2353
|
+
if (size) put(extra[i] >> 5, size);
|
|
2354
|
+
}
|
|
2355
|
+
if (count > 0) put(127, 8 - count);
|
|
2356
|
+
word(65497);
|
|
2357
|
+
return Uint8Array.from(out);
|
|
2358
|
+
}
|
|
2359
|
+
function optimalTable(frequencies) {
|
|
2360
|
+
const freq = [...frequencies];
|
|
2361
|
+
freq[256] = 1;
|
|
2362
|
+
const codesize = new Array(257).fill(0);
|
|
2363
|
+
const others = new Array(257).fill(-1);
|
|
2364
|
+
for (; ; ) {
|
|
2365
|
+
let v1 = -1, v2 = -1;
|
|
2366
|
+
for (let i = 0; i <= 256; i++) if (freq[i] > 0 && (v1 < 0 || freq[i] <= freq[v1])) v1 = i;
|
|
2367
|
+
for (let i = 0; i <= 256; i++)
|
|
2368
|
+
if (freq[i] > 0 && i !== v1 && (v2 < 0 || freq[i] <= freq[v2])) v2 = i;
|
|
2369
|
+
if (v2 < 0) break;
|
|
2370
|
+
freq[v1] += freq[v2];
|
|
2371
|
+
freq[v2] = 0;
|
|
2372
|
+
for (codesize[v1]++; others[v1] >= 0; codesize[v1]++) v1 = others[v1];
|
|
2373
|
+
others[v1] = v2;
|
|
2374
|
+
for (codesize[v2]++; others[v2] >= 0; codesize[v2]++) v2 = others[v2];
|
|
2375
|
+
}
|
|
2376
|
+
const bits = new Array(33).fill(0);
|
|
2377
|
+
for (let i = 0; i <= 256; i++) if (codesize[i]) bits[codesize[i]]++;
|
|
2378
|
+
for (let i = 32; i > 16; i--)
|
|
2379
|
+
while (bits[i] > 0) {
|
|
2380
|
+
let j = i - 2;
|
|
2381
|
+
while (bits[j] === 0) j--;
|
|
2382
|
+
bits[i] -= 2;
|
|
2383
|
+
bits[i - 1]++;
|
|
2384
|
+
bits[j + 1] += 2;
|
|
2385
|
+
bits[j]--;
|
|
2386
|
+
}
|
|
2387
|
+
let last = 16;
|
|
2388
|
+
while (bits[last] === 0) last--;
|
|
2389
|
+
bits[last]--;
|
|
2390
|
+
const values = [];
|
|
2391
|
+
for (let length = 1; length <= 32; length++)
|
|
2392
|
+
for (let symbol = 0; symbol < 256; symbol++)
|
|
2393
|
+
if (codesize[symbol] === length) values.push(symbol);
|
|
2394
|
+
return { counts: bits.slice(1, 17), values };
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// src/tree-model-mesh.ts
|
|
2398
|
+
import {
|
|
2399
|
+
tileflowTreeModelLimits,
|
|
2400
|
+
tileflowTreeParts
|
|
2401
|
+
} from "@tileflow/core";
|
|
2402
|
+
var tileflowTreeModelPartNames = ["bark", "leaves", "fronds", "stem"];
|
|
2403
|
+
function prepareTileflowTreeModelMesh(bytes, level) {
|
|
2404
|
+
const primitives = readTileflowGltfTriangles(bytes);
|
|
2405
|
+
const positions = [];
|
|
2406
|
+
const normals = [];
|
|
2407
|
+
const parts = [];
|
|
2408
|
+
const luminance2 = [];
|
|
2409
|
+
const indices = [];
|
|
2410
|
+
for (const primitive of primitives) {
|
|
2411
|
+
const part = partOf(primitive.material);
|
|
2412
|
+
const offset = positions.length / 3;
|
|
2413
|
+
const count2 = primitive.positions.length / 3;
|
|
2414
|
+
positions.push(...primitive.positions);
|
|
2415
|
+
for (let vertex = 0; vertex < count2; vertex += 1) {
|
|
2416
|
+
parts.push(part);
|
|
2417
|
+
normals.push(primitive.normals?.slice(vertex * 3, vertex * 3 + 3));
|
|
2418
|
+
const [r, g, b] = primitive.colours.slice(vertex * 3, vertex * 3 + 3);
|
|
2419
|
+
luminance2.push(0.2126 * r + 0.7152 * g + 0.0722 * b);
|
|
2420
|
+
}
|
|
2421
|
+
for (const index of primitive.indices) indices.push(offset + index);
|
|
2422
|
+
}
|
|
2423
|
+
const count = positions.length / 3;
|
|
2424
|
+
const triangleCount = indices.length / 3;
|
|
2425
|
+
const limit = level === "near" ? tileflowTreeModelLimits.nearTriangles : tileflowTreeModelLimits.farTriangles;
|
|
2426
|
+
if (triangleCount > limit)
|
|
2427
|
+
throw new TileflowGltfError(
|
|
2428
|
+
`has ${triangleCount.toLocaleString("en")} triangles; a ${level} tree model takes at most ${limit.toLocaleString("en")}. Simplify it (for example with Blender's Decimate modifier) and export it again`
|
|
2429
|
+
);
|
|
2430
|
+
if (count > tileflowTreeModelLimits.vertices)
|
|
2431
|
+
throw new TileflowGltfError(
|
|
2432
|
+
`has ${count.toLocaleString("en")} vertices; a tree model takes at most ${tileflowTreeModelLimits.vertices.toLocaleString("en")}. Merge vertices by distance and export it again`
|
|
2433
|
+
);
|
|
2434
|
+
let height = 0;
|
|
2435
|
+
let reach = 0;
|
|
2436
|
+
const crowned = parts.some((part) => isCrown(part));
|
|
2437
|
+
for (let vertex = 0; vertex < count; vertex += 1) {
|
|
2438
|
+
height = Math.max(height, positions[vertex * 3 + 2]);
|
|
2439
|
+
if (!crowned || isCrown(parts[vertex]))
|
|
2440
|
+
reach = Math.max(reach, Math.hypot(positions[vertex * 3], positions[vertex * 3 + 1]));
|
|
2441
|
+
}
|
|
2442
|
+
if (!(height > 0))
|
|
2443
|
+
throw new TileflowGltfError(
|
|
2444
|
+
"stands below its origin; put the origin where the trunk meets the ground, with the tree above it"
|
|
2445
|
+
);
|
|
2446
|
+
if (!(reach > 0)) throw new TileflowGltfError("has no width round its origin");
|
|
2447
|
+
const across = 2 * reach;
|
|
2448
|
+
const unit = [];
|
|
2449
|
+
for (let vertex = 0; vertex < count; vertex += 1)
|
|
2450
|
+
unit.push(
|
|
2451
|
+
positions[vertex * 3] / across,
|
|
2452
|
+
positions[vertex * 3 + 1] / across,
|
|
2453
|
+
positions[vertex * 3 + 2] / height
|
|
2454
|
+
);
|
|
2455
|
+
const faceSums = new Array(count * 3).fill(0);
|
|
2456
|
+
for (let at = 0; at + 2 < indices.length; at += 3) {
|
|
2457
|
+
const [a, b, c] = [indices[at], indices[at + 1], indices[at + 2]];
|
|
2458
|
+
const n = cross(sub2(point(unit, b), point(unit, a)), sub2(point(unit, c), point(unit, a)));
|
|
2459
|
+
for (const vertex of [a, b, c])
|
|
2460
|
+
for (let axis = 0; axis < 3; axis += 1) faceSums[vertex * 3 + axis] += n[axis];
|
|
2461
|
+
}
|
|
2462
|
+
const outNormals = [];
|
|
2463
|
+
for (let vertex = 0; vertex < count; vertex += 1) {
|
|
2464
|
+
const own = normals[vertex];
|
|
2465
|
+
const n = own ? [own[0] * across, own[1] * across, own[2] * height] : faceSums.slice(vertex * 3, vertex * 3 + 3);
|
|
2466
|
+
const size = Math.hypot(n[0], n[1], n[2]) || 1;
|
|
2467
|
+
outNormals.push(n[0] / size, n[1] / size, n[2] / size);
|
|
2468
|
+
}
|
|
2469
|
+
const shades = new Array(count).fill(0);
|
|
2470
|
+
for (const part of new Set(parts)) {
|
|
2471
|
+
const members = parts.flatMap((value, vertex) => value === part ? [vertex] : []);
|
|
2472
|
+
const mean = members.reduce((sum, vertex) => sum + luminance2[vertex], 0) / members.length;
|
|
2473
|
+
const spread = Math.max(0.06, mean * 0.6);
|
|
2474
|
+
for (const vertex of members)
|
|
2475
|
+
shades[vertex] = clamp((luminance2[vertex] - mean) / spread, -1, 1);
|
|
2476
|
+
}
|
|
2477
|
+
let crownLow = 1;
|
|
2478
|
+
let crownHigh = 0;
|
|
2479
|
+
for (let vertex = 0; vertex < count; vertex += 1)
|
|
2480
|
+
if (isCrown(parts[vertex])) {
|
|
2481
|
+
crownLow = Math.min(crownLow, unit[vertex * 3 + 2]);
|
|
2482
|
+
crownHigh = Math.max(crownHigh, unit[vertex * 3 + 2]);
|
|
2483
|
+
}
|
|
2484
|
+
const occlusion = [];
|
|
2485
|
+
for (let vertex = 0; vertex < count; vertex += 1) {
|
|
2486
|
+
const z = unit[vertex * 3 + 2];
|
|
2487
|
+
if (!isCrown(parts[vertex])) {
|
|
2488
|
+
occlusion.push(z > crownLow ? 0.55 : 0.8);
|
|
2489
|
+
continue;
|
|
2490
|
+
}
|
|
2491
|
+
const up = crownHigh > crownLow ? (z - crownLow) / (crownHigh - crownLow) : 1;
|
|
2492
|
+
const out = Math.min(1, Math.hypot(unit[vertex * 3], unit[vertex * 3 + 1]) / 0.5);
|
|
2493
|
+
occlusion.push(clamp(0.4 + 0.35 * up + 0.3 * out, 0.35, 1));
|
|
2494
|
+
}
|
|
2495
|
+
return {
|
|
2496
|
+
// The author's proportions: the trees it stands keep them around the sizes the data gives.
|
|
2497
|
+
proportion: Math.min(3, Math.max(0.1, across / height)),
|
|
2498
|
+
positions: unit,
|
|
2499
|
+
normals: outNormals,
|
|
2500
|
+
parts,
|
|
2501
|
+
shades,
|
|
2502
|
+
occlusion,
|
|
2503
|
+
indices
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
function partOf(material) {
|
|
2507
|
+
const name = material?.trim().toLowerCase().replace(/\.\d+$/u, "");
|
|
2508
|
+
const part = tileflowTreeModelPartNames.find((candidate) => candidate === name);
|
|
2509
|
+
if (part) return tileflowTreeParts[part];
|
|
2510
|
+
throw new TileflowGltfError(
|
|
2511
|
+
material === void 0 ? "has a mesh without a material; name each material bark, leaves, fronds or stem" : `has a material "${material}" that names no part of a tree; name each material bark, leaves, fronds or stem`
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2514
|
+
function isCrown(part) {
|
|
2515
|
+
return part === tileflowTreeParts.leaves || part === tileflowTreeParts.fronds;
|
|
2516
|
+
}
|
|
2517
|
+
function point(list2, vertex) {
|
|
2518
|
+
return [list2[vertex * 3], list2[vertex * 3 + 1], list2[vertex * 3 + 2]];
|
|
2519
|
+
}
|
|
2520
|
+
function sub2(a, b) {
|
|
2521
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
2522
|
+
}
|
|
2523
|
+
function cross(a, b) {
|
|
2524
|
+
return [
|
|
2525
|
+
a[1] * b[2] - a[2] * b[1],
|
|
2526
|
+
a[2] * b[0] - a[0] * b[2],
|
|
2527
|
+
a[0] * b[1] - a[1] * b[0]
|
|
2528
|
+
];
|
|
2529
|
+
}
|
|
2530
|
+
function clamp(value, low, high) {
|
|
2531
|
+
return Math.min(high, Math.max(low, value));
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
// src/building-model-mesh.ts
|
|
2535
|
+
function isTileflowTreeModel(primitives) {
|
|
2536
|
+
const drawn = primitives.filter((primitive) => !isFootprint(primitive));
|
|
2537
|
+
return drawn.length > 0 && drawn.every(
|
|
2538
|
+
(primitive) => tileflowTreeModelPartNames.includes(
|
|
2539
|
+
partOf2(primitive.material)
|
|
2540
|
+
)
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
function tileflowBuildingModelDeclaredFootprint(primitives) {
|
|
2544
|
+
const positions = [];
|
|
2545
|
+
const indices = [];
|
|
2546
|
+
for (const primitive of primitives) {
|
|
2547
|
+
if (!isFootprint(primitive)) continue;
|
|
2548
|
+
const offset = positions.length / 3;
|
|
2549
|
+
positions.push(...primitive.positions);
|
|
2550
|
+
for (const index of primitive.indices) indices.push(offset + index);
|
|
2551
|
+
}
|
|
2552
|
+
return indices.length > 0 ? { positions, indices } : void 0;
|
|
2553
|
+
}
|
|
2554
|
+
function prepareTileflowBuildingModelMesh(bytes, {
|
|
2555
|
+
ambientOcclusion = true,
|
|
2556
|
+
lightmapTexels = 0
|
|
2557
|
+
} = {}) {
|
|
2558
|
+
const primitives = bytes instanceof Uint8Array ? readTileflowGltfTriangles(bytes, { materials: true }) : bytes;
|
|
2559
|
+
const drawn = primitives.filter((primitive) => !isFootprint(primitive));
|
|
2560
|
+
const surface = buildingModelSurface(drawn);
|
|
2561
|
+
const positions = [];
|
|
2562
|
+
const normals = [];
|
|
2563
|
+
const partNames = [];
|
|
2564
|
+
const parts = [];
|
|
2565
|
+
const lightingRoles = [];
|
|
2566
|
+
const luminance2 = [];
|
|
2567
|
+
const indices = [];
|
|
2568
|
+
for (const primitive of drawn) {
|
|
2569
|
+
const name = partOf2(primitive.material);
|
|
2570
|
+
if (name === void 0)
|
|
2571
|
+
throw new TileflowGltfError(
|
|
2572
|
+
primitive.material === void 0 ? "has a mesh without a material; name each material after its part, such as surface, glass, roof or water" : `has a material "${primitive.material}" that names no part; name each material after its part in lower-case words joined by hyphens, such as surface, glass, roof or water`
|
|
2573
|
+
);
|
|
2574
|
+
if (!partNames.includes(name)) partNames.push(name);
|
|
2575
|
+
if (partNames.length > tileflowModelLimits.parts)
|
|
2576
|
+
throw new TileflowGltfError(
|
|
2577
|
+
`names ${partNames.length} parts in its materials; a building model takes at most ${tileflowModelLimits.parts} a level. Give materials of the same part the same name`
|
|
2578
|
+
);
|
|
2579
|
+
const part = partNames.indexOf(name);
|
|
2580
|
+
const offset = positions.length / 3;
|
|
2581
|
+
const role = primitive.lightingRole === void 0 ? tileflowModelDefaultLightingRole(name) : typeof primitive.lightingRole === "string" && Object.hasOwn(tileflowModelLightingRoles, primitive.lightingRole) ? tileflowModelLightingRoles[primitive.lightingRole] : void 0;
|
|
1719
2582
|
if (role === void 0)
|
|
1720
2583
|
throw new TileflowGltfError(
|
|
1721
2584
|
"has an unknown lighting role; use unclassified, wall, door, roof, window, lamp or logo"
|
|
@@ -1727,7 +2590,7 @@ function prepareTileflowBuildingModelMesh(bytes, { ambientOcclusion = true } = {
|
|
|
1727
2590
|
lightingRoles.push(role);
|
|
1728
2591
|
normals.push(primitive.normals?.slice(vertex * 3, vertex * 3 + 3));
|
|
1729
2592
|
const [r, g, b] = primitive.colours.slice(vertex * 3, vertex * 3 + 3);
|
|
1730
|
-
|
|
2593
|
+
luminance2.push(0.2126 * r + 0.7152 * g + 0.0722 * b);
|
|
1731
2594
|
}
|
|
1732
2595
|
for (const index of primitive.indices) indices.push(offset + index);
|
|
1733
2596
|
}
|
|
@@ -1755,9 +2618,9 @@ function prepareTileflowBuildingModelMesh(bytes, { ambientOcclusion = true } = {
|
|
|
1755
2618
|
const sums = new Array(count * 3).fill(0);
|
|
1756
2619
|
for (let at = 0; at + 2 < indices.length; at += 3) {
|
|
1757
2620
|
const [a, b, c] = [indices[at], indices[at + 1], indices[at + 2]];
|
|
1758
|
-
const n =
|
|
1759
|
-
|
|
1760
|
-
|
|
2621
|
+
const n = cross3(
|
|
2622
|
+
sub3(point2(positions, b), point2(positions, a)),
|
|
2623
|
+
sub3(point2(positions, c), point2(positions, a))
|
|
1761
2624
|
);
|
|
1762
2625
|
for (const vertex of [a, b, c])
|
|
1763
2626
|
for (let axis = 0; axis < 3; axis += 1) sums[vertex * 3 + axis] += n[axis];
|
|
@@ -1771,22 +2634,84 @@ function prepareTileflowBuildingModelMesh(bytes, { ambientOcclusion = true } = {
|
|
|
1771
2634
|
const shades = new Array(count).fill(0);
|
|
1772
2635
|
for (const part of new Set(parts)) {
|
|
1773
2636
|
const members = parts.flatMap((value, vertex) => value === part ? [vertex] : []);
|
|
1774
|
-
const mean = members.reduce((sum, vertex) => sum +
|
|
2637
|
+
const mean = members.reduce((sum, vertex) => sum + luminance2[vertex], 0) / members.length;
|
|
1775
2638
|
const spread = Math.max(0.06, mean * 0.6);
|
|
1776
2639
|
for (const vertex of members)
|
|
1777
|
-
shades[vertex] =
|
|
2640
|
+
shades[vertex] = clamp2((luminance2[vertex] - mean) / spread, -1, 1);
|
|
1778
2641
|
}
|
|
2642
|
+
const texels = typeof lightmapTexels === "function" ? lightmapTexels(surfaceArea(positions, indices)) : lightmapTexels;
|
|
2643
|
+
const lightmap = ambientOcclusion && texels > 0 && surface && lightmapFits(surface) ? bakeBuildingModelLightmap({ positions, normals: outNormals, indices }, { texels }) : void 0;
|
|
2644
|
+
if (!lightmap)
|
|
2645
|
+
return {
|
|
2646
|
+
positions,
|
|
2647
|
+
normals: outNormals,
|
|
2648
|
+
parts,
|
|
2649
|
+
partNames,
|
|
2650
|
+
lightingRoles,
|
|
2651
|
+
shades,
|
|
2652
|
+
...surface ? { surface } : {},
|
|
2653
|
+
occlusion: ambientOcclusion ? buildingModelAmbientVisibility({ positions, normals: outNormals, indices }) : new Array(count).fill(1),
|
|
2654
|
+
indices
|
|
2655
|
+
};
|
|
2656
|
+
const { sources } = lightmap;
|
|
2657
|
+
const from = (values, width = 1) => sources.flatMap((source) => values.slice(source * width, source * width + width));
|
|
2658
|
+
const attributes = from([...surface.attributes], 8);
|
|
2659
|
+
for (let vertex = 0; vertex < sources.length; vertex++) {
|
|
2660
|
+
attributes[vertex * 8 + 2] = lightmap.uvs[vertex * 2];
|
|
2661
|
+
attributes[vertex * 8 + 3] = lightmap.uvs[vertex * 2 + 1];
|
|
2662
|
+
}
|
|
2663
|
+
const texture = surface.textures.length;
|
|
1779
2664
|
return {
|
|
1780
|
-
positions,
|
|
1781
|
-
normals: outNormals,
|
|
1782
|
-
parts,
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
2665
|
+
positions: from(positions, 3),
|
|
2666
|
+
normals: from(outNormals, 3),
|
|
2667
|
+
parts: from(parts),
|
|
2668
|
+
partNames,
|
|
2669
|
+
lightingRoles: from(lightingRoles),
|
|
2670
|
+
shades: from(shades),
|
|
2671
|
+
surface: {
|
|
2672
|
+
...surface,
|
|
2673
|
+
attributes,
|
|
2674
|
+
materials: surface.materials.map((material) => ({
|
|
2675
|
+
...material,
|
|
2676
|
+
occlusionTexture: { texture, texCoord: 1 }
|
|
2677
|
+
})),
|
|
2678
|
+
textures: [
|
|
2679
|
+
...surface.textures,
|
|
2680
|
+
{
|
|
2681
|
+
mimeType: "image/jpeg",
|
|
2682
|
+
bytes: encodeGreyJpeg(lightmap.texels, lightmap.width, lightmap.height, lightmapQuality),
|
|
2683
|
+
width: lightmap.width,
|
|
2684
|
+
height: lightmap.height,
|
|
2685
|
+
wrapS: 33071,
|
|
2686
|
+
wrapT: 33071,
|
|
2687
|
+
minFilter: 9987,
|
|
2688
|
+
magFilter: 9729
|
|
2689
|
+
}
|
|
2690
|
+
]
|
|
2691
|
+
},
|
|
2692
|
+
occlusion: new Array(sources.length).fill(1),
|
|
2693
|
+
indices: lightmap.indices
|
|
1788
2694
|
};
|
|
1789
2695
|
}
|
|
2696
|
+
var lightmapQuality = 35;
|
|
2697
|
+
function surfaceArea(positions, indices) {
|
|
2698
|
+
let area = 0;
|
|
2699
|
+
for (let at = 0; at + 2 < indices.length; at += 3) {
|
|
2700
|
+
const a = point2(positions, indices[at]);
|
|
2701
|
+
area += Math.hypot(
|
|
2702
|
+
...cross3(
|
|
2703
|
+
sub3(point2(positions, indices[at + 1]), a),
|
|
2704
|
+
sub3(point2(positions, indices[at + 2]), a)
|
|
2705
|
+
)
|
|
2706
|
+
) / 2;
|
|
2707
|
+
}
|
|
2708
|
+
return area;
|
|
2709
|
+
}
|
|
2710
|
+
function lightmapFits(surface) {
|
|
2711
|
+
return surface.textures.length < tileflowModelSurfaceLimits2.textures && surface.materials.every(
|
|
2712
|
+
(material) => !material.occlusionTexture && tileflowModelTextureChannels2.every((channel) => material[channel]?.texCoord !== 1)
|
|
2713
|
+
);
|
|
2714
|
+
}
|
|
1790
2715
|
function tileflowBuildingModelFootprint(mesh) {
|
|
1791
2716
|
const { positions, indices } = mesh;
|
|
1792
2717
|
let [minX, minY, maxX, maxY] = [Infinity, Infinity, -Infinity, -Infinity];
|
|
@@ -1903,10 +2828,9 @@ function tileflowBuildingModelFootprint(mesh) {
|
|
|
1903
2828
|
function isFootprint(primitive) {
|
|
1904
2829
|
return primitive.material?.trim().toLowerCase().replace(/\.\d+$/u, "") === "footprint";
|
|
1905
2830
|
}
|
|
1906
|
-
function
|
|
2831
|
+
function partOf2(material) {
|
|
1907
2832
|
const name = material?.trim().toLowerCase().replace(/\.\d+$/u, "");
|
|
1908
|
-
|
|
1909
|
-
return part === void 0 ? void 0 : tileflowModelParts[part];
|
|
2833
|
+
return isTileflowModelPartName(name) ? name : void 0;
|
|
1910
2834
|
}
|
|
1911
2835
|
function simplify(ring, tolerance) {
|
|
1912
2836
|
if (ring.length <= 4) return [...ring];
|
|
@@ -1943,7 +2867,7 @@ function segmentDistance(p, a, b) {
|
|
|
1943
2867
|
const dx = b[0] - a[0];
|
|
1944
2868
|
const dy = b[1] - a[1];
|
|
1945
2869
|
const length = dx * dx + dy * dy;
|
|
1946
|
-
const t = length > 0 ?
|
|
2870
|
+
const t = length > 0 ? clamp2(((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / length, 0, 1) : 0;
|
|
1947
2871
|
return Math.hypot(p[0] - (a[0] + t * dx), p[1] - (a[1] + t * dy));
|
|
1948
2872
|
}
|
|
1949
2873
|
function ringArea(ring) {
|
|
@@ -1955,150 +2879,13 @@ function ringArea(ring) {
|
|
|
1955
2879
|
function edge(a, b, p) {
|
|
1956
2880
|
return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
|
|
1957
2881
|
}
|
|
1958
|
-
function point(list2, vertex) {
|
|
1959
|
-
return [list2[vertex * 3], list2[vertex * 3 + 1], list2[vertex * 3 + 2]];
|
|
1960
|
-
}
|
|
1961
|
-
function sub(a, b) {
|
|
1962
|
-
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
1963
|
-
}
|
|
1964
|
-
function cross(a, b) {
|
|
1965
|
-
return [
|
|
1966
|
-
a[1] * b[2] - a[2] * b[1],
|
|
1967
|
-
a[2] * b[0] - a[0] * b[2],
|
|
1968
|
-
a[0] * b[1] - a[1] * b[0]
|
|
1969
|
-
];
|
|
1970
|
-
}
|
|
1971
|
-
function clamp(value, low, high) {
|
|
1972
|
-
return Math.min(high, Math.max(low, value));
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
// src/tree-model-mesh.ts
|
|
1976
|
-
import {
|
|
1977
|
-
tileflowTreeModelLimits,
|
|
1978
|
-
tileflowTreeParts
|
|
1979
|
-
} from "@tileflow/core";
|
|
1980
|
-
var tileflowTreeModelPartNames = ["bark", "leaves", "fronds", "stem"];
|
|
1981
|
-
function prepareTileflowTreeModelMesh(bytes, level) {
|
|
1982
|
-
const primitives = readTileflowGltfTriangles(bytes);
|
|
1983
|
-
const positions = [];
|
|
1984
|
-
const normals = [];
|
|
1985
|
-
const parts = [];
|
|
1986
|
-
const luminance = [];
|
|
1987
|
-
const indices = [];
|
|
1988
|
-
for (const primitive of primitives) {
|
|
1989
|
-
const part = partOf2(primitive.material);
|
|
1990
|
-
const offset = positions.length / 3;
|
|
1991
|
-
const count2 = primitive.positions.length / 3;
|
|
1992
|
-
positions.push(...primitive.positions);
|
|
1993
|
-
for (let vertex = 0; vertex < count2; vertex += 1) {
|
|
1994
|
-
parts.push(part);
|
|
1995
|
-
normals.push(primitive.normals?.slice(vertex * 3, vertex * 3 + 3));
|
|
1996
|
-
const [r, g, b] = primitive.colours.slice(vertex * 3, vertex * 3 + 3);
|
|
1997
|
-
luminance.push(0.2126 * r + 0.7152 * g + 0.0722 * b);
|
|
1998
|
-
}
|
|
1999
|
-
for (const index of primitive.indices) indices.push(offset + index);
|
|
2000
|
-
}
|
|
2001
|
-
const count = positions.length / 3;
|
|
2002
|
-
const triangleCount = indices.length / 3;
|
|
2003
|
-
const limit = level === "near" ? tileflowTreeModelLimits.nearTriangles : tileflowTreeModelLimits.farTriangles;
|
|
2004
|
-
if (triangleCount > limit)
|
|
2005
|
-
throw new TileflowGltfError(
|
|
2006
|
-
`has ${triangleCount.toLocaleString("en")} triangles; a ${level} tree model takes at most ${limit.toLocaleString("en")}. Simplify it (for example with Blender's Decimate modifier) and export it again`
|
|
2007
|
-
);
|
|
2008
|
-
if (count > tileflowTreeModelLimits.vertices)
|
|
2009
|
-
throw new TileflowGltfError(
|
|
2010
|
-
`has ${count.toLocaleString("en")} vertices; a tree model takes at most ${tileflowTreeModelLimits.vertices.toLocaleString("en")}. Merge vertices by distance and export it again`
|
|
2011
|
-
);
|
|
2012
|
-
let height = 0;
|
|
2013
|
-
let reach = 0;
|
|
2014
|
-
const crowned = parts.some((part) => isCrown(part));
|
|
2015
|
-
for (let vertex = 0; vertex < count; vertex += 1) {
|
|
2016
|
-
height = Math.max(height, positions[vertex * 3 + 2]);
|
|
2017
|
-
if (!crowned || isCrown(parts[vertex]))
|
|
2018
|
-
reach = Math.max(reach, Math.hypot(positions[vertex * 3], positions[vertex * 3 + 1]));
|
|
2019
|
-
}
|
|
2020
|
-
if (!(height > 0))
|
|
2021
|
-
throw new TileflowGltfError(
|
|
2022
|
-
"stands below its origin; put the origin where the trunk meets the ground, with the tree above it"
|
|
2023
|
-
);
|
|
2024
|
-
if (!(reach > 0)) throw new TileflowGltfError("has no width round its origin");
|
|
2025
|
-
const across = 2 * reach;
|
|
2026
|
-
const unit = [];
|
|
2027
|
-
for (let vertex = 0; vertex < count; vertex += 1)
|
|
2028
|
-
unit.push(
|
|
2029
|
-
positions[vertex * 3] / across,
|
|
2030
|
-
positions[vertex * 3 + 1] / across,
|
|
2031
|
-
positions[vertex * 3 + 2] / height
|
|
2032
|
-
);
|
|
2033
|
-
const faceSums = new Array(count * 3).fill(0);
|
|
2034
|
-
for (let at = 0; at + 2 < indices.length; at += 3) {
|
|
2035
|
-
const [a, b, c] = [indices[at], indices[at + 1], indices[at + 2]];
|
|
2036
|
-
const n = cross2(sub2(point2(unit, b), point2(unit, a)), sub2(point2(unit, c), point2(unit, a)));
|
|
2037
|
-
for (const vertex of [a, b, c])
|
|
2038
|
-
for (let axis = 0; axis < 3; axis += 1) faceSums[vertex * 3 + axis] += n[axis];
|
|
2039
|
-
}
|
|
2040
|
-
const outNormals = [];
|
|
2041
|
-
for (let vertex = 0; vertex < count; vertex += 1) {
|
|
2042
|
-
const own = normals[vertex];
|
|
2043
|
-
const n = own ? [own[0] * across, own[1] * across, own[2] * height] : faceSums.slice(vertex * 3, vertex * 3 + 3);
|
|
2044
|
-
const size = Math.hypot(n[0], n[1], n[2]) || 1;
|
|
2045
|
-
outNormals.push(n[0] / size, n[1] / size, n[2] / size);
|
|
2046
|
-
}
|
|
2047
|
-
const shades = new Array(count).fill(0);
|
|
2048
|
-
for (const part of new Set(parts)) {
|
|
2049
|
-
const members = parts.flatMap((value, vertex) => value === part ? [vertex] : []);
|
|
2050
|
-
const mean = members.reduce((sum, vertex) => sum + luminance[vertex], 0) / members.length;
|
|
2051
|
-
const spread = Math.max(0.06, mean * 0.6);
|
|
2052
|
-
for (const vertex of members)
|
|
2053
|
-
shades[vertex] = clamp2((luminance[vertex] - mean) / spread, -1, 1);
|
|
2054
|
-
}
|
|
2055
|
-
let crownLow = 1;
|
|
2056
|
-
let crownHigh = 0;
|
|
2057
|
-
for (let vertex = 0; vertex < count; vertex += 1)
|
|
2058
|
-
if (isCrown(parts[vertex])) {
|
|
2059
|
-
crownLow = Math.min(crownLow, unit[vertex * 3 + 2]);
|
|
2060
|
-
crownHigh = Math.max(crownHigh, unit[vertex * 3 + 2]);
|
|
2061
|
-
}
|
|
2062
|
-
const occlusion = [];
|
|
2063
|
-
for (let vertex = 0; vertex < count; vertex += 1) {
|
|
2064
|
-
const z = unit[vertex * 3 + 2];
|
|
2065
|
-
if (!isCrown(parts[vertex])) {
|
|
2066
|
-
occlusion.push(z > crownLow ? 0.55 : 0.8);
|
|
2067
|
-
continue;
|
|
2068
|
-
}
|
|
2069
|
-
const up = crownHigh > crownLow ? (z - crownLow) / (crownHigh - crownLow) : 1;
|
|
2070
|
-
const out = Math.min(1, Math.hypot(unit[vertex * 3], unit[vertex * 3 + 1]) / 0.5);
|
|
2071
|
-
occlusion.push(clamp2(0.4 + 0.35 * up + 0.3 * out, 0.35, 1));
|
|
2072
|
-
}
|
|
2073
|
-
return {
|
|
2074
|
-
// The author's proportions: the trees it stands keep them around the sizes the data gives.
|
|
2075
|
-
proportion: Math.min(3, Math.max(0.1, across / height)),
|
|
2076
|
-
positions: unit,
|
|
2077
|
-
normals: outNormals,
|
|
2078
|
-
parts,
|
|
2079
|
-
shades,
|
|
2080
|
-
occlusion,
|
|
2081
|
-
indices
|
|
2082
|
-
};
|
|
2083
|
-
}
|
|
2084
|
-
function partOf2(material) {
|
|
2085
|
-
const name = material?.trim().toLowerCase().replace(/\.\d+$/u, "");
|
|
2086
|
-
const part = tileflowTreeModelPartNames.find((candidate) => candidate === name);
|
|
2087
|
-
if (part) return tileflowTreeParts[part];
|
|
2088
|
-
throw new TileflowGltfError(
|
|
2089
|
-
material === void 0 ? "has a mesh without a material; name each material bark, leaves, fronds or stem" : `has a material "${material}" that names no part of a tree; name each material bark, leaves, fronds or stem`
|
|
2090
|
-
);
|
|
2091
|
-
}
|
|
2092
|
-
function isCrown(part) {
|
|
2093
|
-
return part === tileflowTreeParts.leaves || part === tileflowTreeParts.fronds;
|
|
2094
|
-
}
|
|
2095
2882
|
function point2(list2, vertex) {
|
|
2096
2883
|
return [list2[vertex * 3], list2[vertex * 3 + 1], list2[vertex * 3 + 2]];
|
|
2097
2884
|
}
|
|
2098
|
-
function
|
|
2885
|
+
function sub3(a, b) {
|
|
2099
2886
|
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
2100
2887
|
}
|
|
2101
|
-
function
|
|
2888
|
+
function cross3(a, b) {
|
|
2102
2889
|
return [
|
|
2103
2890
|
a[1] * b[2] - a[2] * b[1],
|
|
2104
2891
|
a[2] * b[0] - a[0] * b[2],
|
|
@@ -2170,15 +2957,32 @@ function encodeTileflowLandmarkRegion(region) {
|
|
|
2170
2957
|
const bytes = serializeTileflowLandmarkRegion(region);
|
|
2171
2958
|
return { bytes, sha256: sha256Of(bytes) };
|
|
2172
2959
|
}
|
|
2960
|
+
var nearLightmapTexels = (area) => Math.min(
|
|
2961
|
+
524288,
|
|
2962
|
+
Math.max(65536, Math.round(89 / 0.6 * area ** 0.54), Math.round(1.3 ** 2 * area / 0.6))
|
|
2963
|
+
);
|
|
2964
|
+
var buildingModelLightmapTexels = {
|
|
2965
|
+
near: nearLightmapTexels,
|
|
2966
|
+
mid: (area) => nearLightmapTexels(area) / 4
|
|
2967
|
+
};
|
|
2173
2968
|
function buildingModelEntry(id, levels) {
|
|
2174
2969
|
const primitives = readTileflowGltfTriangles(levels.near, { materials: true });
|
|
2175
|
-
if (
|
|
2970
|
+
if (isTileflowTreeModel(primitives))
|
|
2176
2971
|
throw new TileflowGltfError(
|
|
2177
|
-
"is not a building model; name
|
|
2972
|
+
"is not a building model; its materials name only the parts of a tree (bark, leaves, fronds and stem)"
|
|
2973
|
+
);
|
|
2974
|
+
const meshes = [
|
|
2975
|
+
prepareTileflowBuildingModelMesh(primitives, {
|
|
2976
|
+
lightmapTexels: buildingModelLightmapTexels.near
|
|
2977
|
+
})
|
|
2978
|
+
];
|
|
2979
|
+
if (levels.mid)
|
|
2980
|
+
meshes.push(
|
|
2981
|
+
prepareTileflowBuildingModelMesh(levels.mid, {
|
|
2982
|
+
lightmapTexels: buildingModelLightmapTexels.mid
|
|
2983
|
+
})
|
|
2178
2984
|
);
|
|
2179
|
-
|
|
2180
|
-
for (const level of [levels.mid, levels.far])
|
|
2181
|
-
if (level) meshes.push(prepareTileflowBuildingModelMesh(level));
|
|
2985
|
+
if (levels.far) meshes.push(prepareTileflowBuildingModelMesh(levels.far));
|
|
2182
2986
|
return {
|
|
2183
2987
|
id,
|
|
2184
2988
|
levels: meshes,
|
|
@@ -2265,7 +3069,7 @@ async function prepareTileflowMapTreeModels(directories, options) {
|
|
|
2265
3069
|
});
|
|
2266
3070
|
continue;
|
|
2267
3071
|
}
|
|
2268
|
-
if (
|
|
3072
|
+
if (!isTileflowTreeModel(primitives)) {
|
|
2269
3073
|
const files2 = { near: nearBytes };
|
|
2270
3074
|
const sources = [];
|
|
2271
3075
|
let failing = entry.near;
|