Use lightweight glTF car for cruise preview
This commit is contained in:
@@ -83,6 +83,7 @@ function normalizeAreaConfig(raw) {
|
|||||||
outputOverrides.cesiumPreview || path.join(areaDir, `${fileStem}-cesium-preview.html`),
|
outputOverrides.cesiumPreview || path.join(areaDir, `${fileStem}-cesium-preview.html`),
|
||||||
),
|
),
|
||||||
vehicleRoute: path.resolve(outputOverrides.vehicleRoute || path.join(areaDir, `${fileStem}-vehicle-route.json`)),
|
vehicleRoute: path.resolve(outputOverrides.vehicleRoute || path.join(areaDir, `${fileStem}-vehicle-route.json`)),
|
||||||
|
vehicleModel: path.resolve(outputOverrides.vehicleModel || path.join(areaDir, `${fileStem}-vehicle-car.gltf`)),
|
||||||
pipelineDir: path.resolve(outputOverrides.pipelineDir || path.join(areaDir, "_pipeline")),
|
pipelineDir: path.resolve(outputOverrides.pipelineDir || path.join(areaDir, "_pipeline")),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -274,10 +275,12 @@ function writeCesiumPreview(area) {
|
|||||||
const htmlPath = area.outputs.cesiumPreview;
|
const htmlPath = area.outputs.cesiumPreview;
|
||||||
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
|
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
|
||||||
writeVehicleRoute(area);
|
writeVehicleRoute(area);
|
||||||
|
writeVehicleModel(area);
|
||||||
const glbName = path.basename(area.outputs.glb);
|
const glbName = path.basename(area.outputs.glb);
|
||||||
const metadataName = path.basename(area.outputs.metadata);
|
const metadataName = path.basename(area.outputs.metadata);
|
||||||
const routeName = path.basename(area.outputs.vehicleRoute);
|
const routeName = path.basename(area.outputs.vehicleRoute);
|
||||||
fs.writeFileSync(htmlPath, cesiumPreviewHtml(glbName, metadataName, routeName, area.id));
|
const vehicleModelName = path.basename(area.outputs.vehicleModel);
|
||||||
|
fs.writeFileSync(htmlPath, cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, area.id));
|
||||||
console.log(`Cesium preview: ${htmlPath}`);
|
console.log(`Cesium preview: ${htmlPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +291,13 @@ function writeVehicleRoute(area) {
|
|||||||
console.log(`Vehicle route: ${area.outputs.vehicleRoute}`);
|
console.log(`Vehicle route: ${area.outputs.vehicleRoute}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeVehicleModel(area) {
|
||||||
|
const gltf = makeVehicleGltf();
|
||||||
|
fs.mkdirSync(path.dirname(area.outputs.vehicleModel), { recursive: true });
|
||||||
|
fs.writeFileSync(area.outputs.vehicleModel, `${JSON.stringify(gltf, null, 2)}\n`);
|
||||||
|
console.log(`Vehicle model: ${area.outputs.vehicleModel}`);
|
||||||
|
}
|
||||||
|
|
||||||
function buildVehicleRoute(osmPath) {
|
function buildVehicleRoute(osmPath) {
|
||||||
const xml = fs.readFileSync(osmPath, "utf8");
|
const xml = fs.readFileSync(osmPath, "utf8");
|
||||||
const bounds = osmBounds(xml);
|
const bounds = osmBounds(xml);
|
||||||
@@ -434,7 +444,175 @@ function degreesToRadians(value) {
|
|||||||
return value * Math.PI / 180;
|
return value * Math.PI / 180;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cesiumPreviewHtml(glbName, metadataName, routeName, areaId) {
|
function makeVehicleGltf() {
|
||||||
|
const meshes = [];
|
||||||
|
const nodes = [];
|
||||||
|
const bufferParts = [];
|
||||||
|
const bufferViews = [];
|
||||||
|
const accessors = [];
|
||||||
|
|
||||||
|
function align4(bytes) {
|
||||||
|
while (bytes.length % 4 !== 0) bytes.push(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addBufferView(bytes, target) {
|
||||||
|
align4(bufferParts);
|
||||||
|
const offset = bufferParts.length;
|
||||||
|
bufferParts.push(...bytes);
|
||||||
|
const view = { buffer: 0, byteOffset: offset, byteLength: bytes.length };
|
||||||
|
if (target) view.target = target;
|
||||||
|
bufferViews.push(view);
|
||||||
|
return bufferViews.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function floatBytes(values) {
|
||||||
|
const buffer = Buffer.alloc(values.length * 4);
|
||||||
|
values.forEach((value, index) => buffer.writeFloatLE(value, index * 4));
|
||||||
|
return Array.from(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ushortBytes(values) {
|
||||||
|
const buffer = Buffer.alloc(values.length * 2);
|
||||||
|
values.forEach((value, index) => buffer.writeUInt16LE(value, index * 2));
|
||||||
|
return Array.from(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAccessor(bufferView, componentType, count, type, min, max) {
|
||||||
|
const accessor = { bufferView, componentType, count, type };
|
||||||
|
if (min) accessor.min = min;
|
||||||
|
if (max) accessor.max = max;
|
||||||
|
accessors.push(accessor);
|
||||||
|
return accessors.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMesh(name, geometry, material) {
|
||||||
|
const positionView = addBufferView(floatBytes(geometry.positions), 34962);
|
||||||
|
const indexView = addBufferView(ushortBytes(geometry.indices), 34963);
|
||||||
|
const positionAccessor = addAccessor(
|
||||||
|
positionView,
|
||||||
|
5126,
|
||||||
|
geometry.positions.length / 3,
|
||||||
|
"VEC3",
|
||||||
|
geometry.min,
|
||||||
|
geometry.max,
|
||||||
|
);
|
||||||
|
const indexAccessor = addAccessor(indexView, 5123, geometry.indices.length, "SCALAR");
|
||||||
|
meshes.push({
|
||||||
|
name,
|
||||||
|
primitives: [{
|
||||||
|
attributes: { POSITION: positionAccessor },
|
||||||
|
indices: indexAccessor,
|
||||||
|
material,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
nodes.push({ name, mesh: meshes.length - 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
addMesh("body", cuboid(0, 0, 0.72, 4.6, 1.9, 0.9), 0);
|
||||||
|
addMesh("hood", cuboid(1.35, 0, 1.1, 1.25, 1.74, 0.38), 0);
|
||||||
|
addMesh("cabin", cuboid(-0.55, 0, 1.42, 1.75, 1.55, 0.82), 1);
|
||||||
|
addMesh("rear", cuboid(-1.65, 0, 1.08, 0.95, 1.78, 0.42), 0);
|
||||||
|
addMesh("front_windshield", cuboid(0.28, 0, 1.58, 0.12, 1.42, 0.58), 2);
|
||||||
|
addMesh("left_window", cuboid(-0.55, -0.82, 1.52, 1.25, 0.08, 0.48), 2);
|
||||||
|
addMesh("right_window", cuboid(-0.55, 0.82, 1.52, 1.25, 0.08, 0.48), 2);
|
||||||
|
for (const x of [-1.55, 1.45]) {
|
||||||
|
for (const y of [-1.02, 1.02]) {
|
||||||
|
addMesh(`wheel_${x}_${y}`, cylinderY(x, y, 0.46, 0.38, 0.32, 16), 3);
|
||||||
|
addMesh(`hub_${x}_${y}`, cylinderY(x, y, 0.46, 0.2, 0.34, 12), 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addMesh("left_headlight", cuboid(2.36, -0.48, 0.9, 0.08, 0.32, 0.16), 5);
|
||||||
|
addMesh("right_headlight", cuboid(2.36, 0.48, 0.9, 0.08, 0.32, 0.16), 5);
|
||||||
|
addMesh("left_tail", cuboid(-2.36, -0.55, 0.9, 0.08, 0.28, 0.16), 6);
|
||||||
|
addMesh("right_tail", cuboid(-2.36, 0.55, 0.9, 0.08, 0.28, 0.16), 6);
|
||||||
|
|
||||||
|
const buffer = Buffer.from(bufferParts);
|
||||||
|
return {
|
||||||
|
asset: { version: "2.0", generator: "osm-asset-pipeline vehicle preview" },
|
||||||
|
scene: 0,
|
||||||
|
scenes: [{ nodes: nodes.map((_, index) => index) }],
|
||||||
|
nodes,
|
||||||
|
meshes,
|
||||||
|
buffers: [{
|
||||||
|
byteLength: buffer.length,
|
||||||
|
uri: `data:application/octet-stream;base64,${buffer.toString("base64")}`,
|
||||||
|
}],
|
||||||
|
bufferViews,
|
||||||
|
accessors,
|
||||||
|
materials: [
|
||||||
|
material("paint red", [0.82, 0.05, 0.035, 1], 0.55, 0.25),
|
||||||
|
material("dark roof", [0.08, 0.08, 0.085, 1], 0.45, 0.35),
|
||||||
|
material("glass", [0.04, 0.12, 0.16, 0.82], 0.18, 0.08),
|
||||||
|
material("tire", [0.015, 0.014, 0.013, 1], 0.75, 0.65),
|
||||||
|
material("wheel hub", [0.72, 0.72, 0.68, 1], 0.35, 0.85),
|
||||||
|
material("headlight", [1.0, 0.92, 0.62, 1], 0.12, 0.0),
|
||||||
|
material("tail light", [0.95, 0.03, 0.03, 1], 0.25, 0.0),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function material(name, color, roughness, metallic) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
pbrMetallicRoughness: {
|
||||||
|
baseColorFactor: color,
|
||||||
|
roughnessFactor: roughness,
|
||||||
|
metallicFactor: metallic,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cuboid(cx, cy, cz, sx, sy, sz) {
|
||||||
|
const x0 = cx - sx / 2;
|
||||||
|
const x1 = cx + sx / 2;
|
||||||
|
const y0 = cy - sy / 2;
|
||||||
|
const y1 = cy + sy / 2;
|
||||||
|
const z0 = cz - sz / 2;
|
||||||
|
const z1 = cz + sz / 2;
|
||||||
|
const positions = [
|
||||||
|
x0, y0, z0, x1, y0, z0, x1, y1, z0, x0, y1, z0,
|
||||||
|
x0, y0, z1, x1, y0, z1, x1, y1, z1, x0, y1, z1,
|
||||||
|
];
|
||||||
|
const indices = [
|
||||||
|
0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6,
|
||||||
|
0, 4, 5, 0, 5, 1, 1, 5, 6, 1, 6, 2,
|
||||||
|
2, 6, 7, 2, 7, 3, 3, 7, 4, 3, 4, 0,
|
||||||
|
];
|
||||||
|
return { positions, indices, min: [x0, y0, z0], max: [x1, y1, z1] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function cylinderY(cx, cy, cz, radius, width, segments) {
|
||||||
|
const positions = [];
|
||||||
|
const indices = [];
|
||||||
|
const y0 = cy - width / 2;
|
||||||
|
const y1 = cy + width / 2;
|
||||||
|
for (const y of [y0, y1]) {
|
||||||
|
positions.push(cx, y, cz);
|
||||||
|
for (let i = 0; i < segments; i += 1) {
|
||||||
|
const angle = 2 * Math.PI * i / segments;
|
||||||
|
positions.push(cx + Math.cos(angle) * radius, y, cz + Math.sin(angle) * radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const center0 = 0;
|
||||||
|
const center1 = segments + 1;
|
||||||
|
for (let i = 0; i < segments; i += 1) {
|
||||||
|
const a0 = center0 + 1 + i;
|
||||||
|
const b0 = center0 + 1 + ((i + 1) % segments);
|
||||||
|
const a1 = center1 + 1 + i;
|
||||||
|
const b1 = center1 + 1 + ((i + 1) % segments);
|
||||||
|
indices.push(center0, b0, a0);
|
||||||
|
indices.push(center1, a1, b1);
|
||||||
|
indices.push(a0, b0, b1, a0, b1, a1);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
positions,
|
||||||
|
indices,
|
||||||
|
min: [cx - radius, y0, cz - radius],
|
||||||
|
max: [cx + radius, y1, cz + radius],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, areaId) {
|
||||||
return `<!doctype html>
|
return `<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
@@ -605,11 +783,11 @@ function cesiumPreviewHtml(glbName, metadataName, routeName, areaId) {
|
|||||||
]),
|
]),
|
||||||
position: positions,
|
position: positions,
|
||||||
orientation: new Cesium.VelocityOrientationProperty(positions),
|
orientation: new Cesium.VelocityOrientationProperty(positions),
|
||||||
box: {
|
model: {
|
||||||
dimensions: new Cesium.Cartesian3(4.6, 1.9, 1.5),
|
uri: "${escapeJs(vehicleModelName)}",
|
||||||
material: Cesium.Color.ORANGERED,
|
scale: 1.0,
|
||||||
outline: true,
|
minimumPixelSize: 24,
|
||||||
outlineColor: Cesium.Color.WHITE
|
maximumScale: 80
|
||||||
},
|
},
|
||||||
path: {
|
path: {
|
||||||
resolution: 1,
|
resolution: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user