Use clean ToyCar model and lane-offset cruise route
This commit is contained in:
@@ -83,7 +83,7 @@ function normalizeAreaConfig(raw) {
|
||||
outputOverrides.cesiumPreview || path.join(areaDir, `${fileStem}-cesium-preview.html`),
|
||||
),
|
||||
vehicleRoute: path.resolve(outputOverrides.vehicleRoute || path.join(areaDir, `${fileStem}-vehicle-route.json`)),
|
||||
vehicleModel: path.resolve(outputOverrides.vehicleModel || path.join(areaDir, `${fileStem}-vehicle-car.gltf`)),
|
||||
vehicleModel: path.resolve(outputOverrides.vehicleModel || path.join(areaDir, `${fileStem}-vehicle-car.glb`)),
|
||||
pipelineDir: path.resolve(outputOverrides.pipelineDir || path.join(areaDir, "_pipeline")),
|
||||
};
|
||||
|
||||
@@ -292,9 +292,10 @@ function writeVehicleRoute(area) {
|
||||
}
|
||||
|
||||
function writeVehicleModel(area) {
|
||||
const gltf = makeVehicleGltf();
|
||||
const source = path.join(repoRoot, "assets", "vehicles", "ToyCar.glb");
|
||||
ensureFile(source, "Vehicle model asset");
|
||||
fs.mkdirSync(path.dirname(area.outputs.vehicleModel), { recursive: true });
|
||||
fs.writeFileSync(area.outputs.vehicleModel, `${JSON.stringify(gltf, null, 2)}\n`);
|
||||
fs.copyFileSync(source, area.outputs.vehicleModel);
|
||||
console.log(`Vehicle model: ${area.outputs.vehicleModel}`);
|
||||
}
|
||||
|
||||
@@ -330,13 +331,17 @@ function buildVehicleRoute(osmPath) {
|
||||
const lengthMeters = routeLength(run);
|
||||
if (lengthMeters < 20) continue;
|
||||
runIndex += 1;
|
||||
const laneOffsetMeters = 1.3;
|
||||
const shiftedRun = offsetPolylineRight(run, laneOffsetMeters);
|
||||
segments.push({
|
||||
id: runIndex === 1 ? (attrs.id || `way-${segments.length + 1}`) : `${attrs.id || "way"}-${runIndex}`,
|
||||
name: tags.name || tags.highway || "road",
|
||||
highway: tags.highway || "",
|
||||
oneWay: tags.oneway || "",
|
||||
lengthMeters,
|
||||
coordinates: run,
|
||||
laneOffsetMeters,
|
||||
coordinates: shiftedRun,
|
||||
centerlineCoordinates: run,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -394,6 +399,35 @@ function compactCoords(coords) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function offsetPolylineRight(coords, offsetMeters) {
|
||||
if (coords.length < 2 || offsetMeters === 0) return coords;
|
||||
const refLat = coords.reduce((sum, coord) => sum + coord[1], 0) / coords.length;
|
||||
const metersPerLat = 111320.0;
|
||||
const metersPerLon = 111320.0 * Math.cos(degreesToRadians(refLat));
|
||||
const points = coords.map((coord) => ({
|
||||
x: coord[0] * metersPerLon,
|
||||
y: coord[1] * metersPerLat,
|
||||
lon: coord[0],
|
||||
lat: coord[1],
|
||||
}));
|
||||
return points.map((point, index) => {
|
||||
const prev = points[Math.max(0, index - 1)];
|
||||
const next = points[Math.min(points.length - 1, index + 1)];
|
||||
let dx = next.x - prev.x;
|
||||
let dy = next.y - prev.y;
|
||||
const length = Math.hypot(dx, dy);
|
||||
if (length < 0.001) return [point.lon, point.lat];
|
||||
dx /= length;
|
||||
dy /= length;
|
||||
const rightX = dy;
|
||||
const rightY = -dx;
|
||||
return [
|
||||
(point.x + rightX * offsetMeters) / metersPerLon,
|
||||
(point.y + rightY * offsetMeters) / metersPerLat,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
function splitInBounds(coords, bounds) {
|
||||
if (!bounds) return [coords];
|
||||
const runs = [];
|
||||
@@ -444,174 +478,6 @@ function degreesToRadians(value) {
|
||||
return value * Math.PI / 180;
|
||||
}
|
||||
|
||||
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>
|
||||
<html lang="zh-CN">
|
||||
@@ -743,12 +609,12 @@ function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, a
|
||||
const positions = new Cesium.SampledPositionProperty();
|
||||
let elapsed = 0;
|
||||
let previous = segment.coordinates[0];
|
||||
positions.addSample(start, Cesium.Cartesian3.fromDegrees(previous[0], previous[1], 1.15));
|
||||
positions.addSample(start, Cesium.Cartesian3.fromDegrees(previous[0], previous[1], 0.8));
|
||||
for (let i = 1; i < segment.coordinates.length; i += 1) {
|
||||
const coord = segment.coordinates[i];
|
||||
elapsed += distanceMeters(previous, coord) / speed;
|
||||
const time = Cesium.JulianDate.addSeconds(start, elapsed, new Cesium.JulianDate());
|
||||
positions.addSample(time, Cesium.Cartesian3.fromDegrees(coord[0], coord[1], 1.15));
|
||||
positions.addSample(time, Cesium.Cartesian3.fromDegrees(coord[0], coord[1], 0.8));
|
||||
previous = coord;
|
||||
}
|
||||
positions.setInterpolationOptions({
|
||||
@@ -785,7 +651,7 @@ function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, a
|
||||
orientation: new Cesium.VelocityOrientationProperty(positions),
|
||||
model: {
|
||||
uri: "${escapeJs(vehicleModelName)}",
|
||||
scale: 1.0,
|
||||
scale: 0.006,
|
||||
minimumPixelSize: 24,
|
||||
maximumScale: 80
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user