feat(preview): add low poly vehicle library
This commit is contained in:
@@ -12,7 +12,7 @@ const {
|
||||
} = require("./lib/scene-layers");
|
||||
const { digest: glbDigest } = require("./glb-digest");
|
||||
const { buildVehicleRoute: buildPreviewVehicleRoute } = require("./lib/vehicle-route");
|
||||
const { makeVehicleGltf: makePreviewVehicleGltf } = require("./lib/vehicle-model");
|
||||
const { writePreviewVehicleLibrary } = require("./lib/vehicle-library");
|
||||
const {
|
||||
cesiumPreviewHtml,
|
||||
previewSummary,
|
||||
@@ -468,13 +468,13 @@ function writeCesiumPreview(area) {
|
||||
const startedAt = new Date(started).toISOString();
|
||||
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
|
||||
writeVehicleRoute(area);
|
||||
writeVehicleModel(area);
|
||||
const vehicleModelNames = writeVehicleModel(area);
|
||||
writeCesiumPreviewSupportFiles(path.dirname(htmlPath));
|
||||
const glbName = path.basename(area.outputs.glb);
|
||||
const metadataName = path.basename(area.outputs.metadata);
|
||||
const routeName = path.basename(area.outputs.vehicleRoute);
|
||||
const vehicleModelName = path.basename(area.outputs.vehicleModel);
|
||||
fs.writeFileSync(htmlPath, cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, area.id));
|
||||
fs.writeFileSync(htmlPath, cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, area.id, vehicleModelNames));
|
||||
console.log(`Cesium preview: ${htmlPath}`);
|
||||
const finished = Date.now();
|
||||
writeStageManifest(area, {
|
||||
@@ -510,10 +510,14 @@ function writeVehicleRoute(area) {
|
||||
}
|
||||
|
||||
function writeVehicleModel(area) {
|
||||
const gltf = makePreviewVehicleGltf();
|
||||
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}`);
|
||||
const outDir = path.dirname(area.outputs.vehicleModel);
|
||||
const fileStem = path.basename(area.outputs.vehicleModel, ".gltf").replace(/-vehicle-car$/, "");
|
||||
const models = writePreviewVehicleLibrary(outDir, fileStem);
|
||||
// Preserve the existing output/manifest contract for old preview HTML.
|
||||
fs.copyFileSync(path.join(outDir, models[0]), area.outputs.vehicleModel);
|
||||
console.log(`Vehicle models: ${models.length} candidates in ${path.dirname(area.outputs.vehicleModel)}`);
|
||||
return models;
|
||||
}
|
||||
|
||||
function sceneGeojsonRecords(area) {
|
||||
|
||||
@@ -13,13 +13,14 @@ function writeCesiumPreviewSupportFiles(outDir) {
|
||||
}
|
||||
}
|
||||
|
||||
function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, areaId) {
|
||||
function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, areaId, vehicleModelNames = []) {
|
||||
const previewConfig = {
|
||||
areaId,
|
||||
glbName,
|
||||
metadataName,
|
||||
routeName,
|
||||
vehicleModelName,
|
||||
vehicleModelNames,
|
||||
};
|
||||
return `<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
const viewer = createViewer();
|
||||
setLoadingMessage("Loading model", config.glbName || "");
|
||||
const assets = await loadSceneAssets(viewer, metadata, placement);
|
||||
const cruise = addVehicleCruises(viewer, routeData, config.vehicleModelName);
|
||||
const cruise = addVehicleCruises(viewer, routeData, config.vehicleModelNames, config.vehicleModelName);
|
||||
const cameras = createCameraPresets(viewer, metadata, placement, cruise);
|
||||
|
||||
buildAssetToggles(assets);
|
||||
@@ -422,7 +422,7 @@
|
||||
return stopFollow;
|
||||
}
|
||||
|
||||
function addVehicleCruises(viewer, routeData, vehicleModelName) {
|
||||
function addVehicleCruises(viewer, routeData, vehicleModelNames, fallbackVehicleModelName) {
|
||||
const segments = ((routeData && (routeData.routes || routeData.segments)) || [])
|
||||
.filter((segment) => segment.coordinates && segment.coordinates.length >= 2)
|
||||
.slice(0, 5);
|
||||
@@ -436,7 +436,8 @@
|
||||
viewer.clock.shouldAnimate = segments.length > 0;
|
||||
|
||||
const vehicles = segments.map((segment, index) => {
|
||||
const vehicle = addCruiseVehicle(viewer, segment, index, start, speed, vehicleModelName);
|
||||
const vehicle = addCruiseVehicle(viewer, segment, index, start, speed,
|
||||
selectedVehicleModelName(vehicleModelNames, fallbackVehicleModelName));
|
||||
const option = document.createElement("option");
|
||||
option.value = String(index);
|
||||
option.textContent = routeLabel(segment, index);
|
||||
@@ -450,6 +451,14 @@
|
||||
};
|
||||
}
|
||||
|
||||
function selectedVehicleModelName(modelNames, fallbackModelName) {
|
||||
const choices = Array.isArray(modelNames) && modelNames.length
|
||||
? modelNames
|
||||
: [fallbackModelName];
|
||||
const usable = choices.filter(Boolean);
|
||||
return usable[Math.floor(Math.random() * usable.length)] || "";
|
||||
}
|
||||
|
||||
function addCruiseVehicle(viewer, segment, index, start, speed, vehicleModelName) {
|
||||
const route = prepareRoute(segment);
|
||||
const positions = new Cesium.CallbackProperty((time, result) => {
|
||||
@@ -482,9 +491,10 @@
|
||||
orientation: routeOrientation(route, start, speed, 0.0),
|
||||
model: {
|
||||
uri: vehicleModelName,
|
||||
scale: 1.0,
|
||||
minimumPixelSize: 24,
|
||||
maximumScale: 80
|
||||
// Keep the library's world scale visible. A minimum screen size made
|
||||
// long trucks balloon in overview and at intersections.
|
||||
scale: 0.72,
|
||||
minimumPixelSize: 0
|
||||
}
|
||||
});
|
||||
return {
|
||||
|
||||
49
scripts/lib/vehicle-library.js
Normal file
49
scripts/lib/vehicle-library.js
Normal file
@@ -0,0 +1,49 @@
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const VEHICLE_IDS = [
|
||||
"car_a01_002",
|
||||
"car_a02_002",
|
||||
"car_a03_001",
|
||||
"truck_a01_001",
|
||||
"truck_a02_001",
|
||||
"truck_a03_001",
|
||||
];
|
||||
const LIBRARY_ROOT = path.resolve(__dirname, "..", "..", "assets", "models", "custom", "lowpoly_cars");
|
||||
const TEXTURE_NAME = "color_512x512.jpg";
|
||||
const REVERSED_MODEL_IDS = new Set(["truck_a03_001"]);
|
||||
|
||||
function writePreviewVehicleLibrary(outDir, fileStem) {
|
||||
const textureOutput = `${fileStem}-vehicle-texture.jpg`;
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
fs.copyFileSync(path.join(LIBRARY_ROOT, "textures", TEXTURE_NAME), path.join(outDir, textureOutput));
|
||||
return VEHICLE_IDS.map((id) => {
|
||||
const modelName = `${fileStem}-vehicle-${id}.gltf`;
|
||||
const binName = `${fileStem}-vehicle-${id}.bin`;
|
||||
const sourceDir = path.join(LIBRARY_ROOT, id);
|
||||
const gltf = JSON.parse(fs.readFileSync(path.join(sourceDir, "model.gltf"), "utf8"));
|
||||
for (const image of gltf.images || []) image.uri = textureOutput;
|
||||
for (const buffer of gltf.buffers || []) buffer.uri = binName;
|
||||
if (REVERSED_MODEL_IDS.has(id)) {
|
||||
// The source's longitudinal PCA axis points rearward for this truck.
|
||||
for (const node of gltf.nodes || []) node.rotation = [0, 1, 0, 0];
|
||||
}
|
||||
for (const material of gltf.materials || []) {
|
||||
const pbr = material.pbrMetallicRoughness || (material.pbrMetallicRoughness = {});
|
||||
// Match the Cesium foliage profile: lift albedo first, then use the
|
||||
// texture itself as a modest shadow-side floor.
|
||||
pbr.baseColorFactor = [2.1, 2.1, 2.1, 1];
|
||||
if (pbr.baseColorTexture) {
|
||||
material.emissiveTexture = { ...pbr.baseColorTexture };
|
||||
material.emissiveFactor = [0.25, 0.25, 0.25];
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(path.join(outDir, modelName), `${JSON.stringify(gltf, null, 2)}\n`);
|
||||
fs.copyFileSync(path.join(sourceDir, "model.bin"), path.join(outDir, binName));
|
||||
return modelName;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { VEHICLE_IDS, REVERSED_MODEL_IDS, writePreviewVehicleLibrary };
|
||||
@@ -7,6 +7,7 @@ const os = require("os");
|
||||
const path = require("path");
|
||||
const { cesiumPreviewHtml } = require("./lib/area-preview");
|
||||
const { makeVehicleGltf } = require("./lib/vehicle-model");
|
||||
const { VEHICLE_IDS, REVERSED_MODEL_IDS, writePreviewVehicleLibrary } = require("./lib/vehicle-library");
|
||||
const { allowedTurns, buildVehicleRoute, classifyConnection } = require("./lib/vehicle-route");
|
||||
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "preview-assets-"));
|
||||
@@ -85,17 +86,39 @@ assert.equal(
|
||||
vehicle.buffers[0].byteLength,
|
||||
);
|
||||
|
||||
const vehicleLibraryRoot = path.join(__dirname, "..", "assets", "models", "custom", "lowpoly_cars");
|
||||
if (fs.existsSync(vehicleLibraryRoot)) {
|
||||
const libraryDir = path.join(tempDir, "vehicle-library");
|
||||
const models = writePreviewVehicleLibrary(libraryDir, "fixture");
|
||||
assert.deepEqual(models, VEHICLE_IDS.map((id) => `fixture-vehicle-${id}.gltf`));
|
||||
assert.equal(models.length, 6);
|
||||
assert.ok(fs.existsSync(path.join(libraryDir, "fixture-vehicle-texture.jpg")));
|
||||
for (const model of models) {
|
||||
const gltf = JSON.parse(fs.readFileSync(path.join(libraryDir, model), "utf8"));
|
||||
assert.deepEqual(gltf.images.map((image) => image.uri), ["fixture-vehicle-texture.jpg"]);
|
||||
assert.equal(gltf.buffers[0].uri, model.replace(/\.gltf$/, ".bin"));
|
||||
assert.deepEqual(gltf.materials[0].pbrMetallicRoughness.baseColorFactor, [2.1, 2.1, 2.1, 1]);
|
||||
assert.deepEqual(gltf.materials[0].emissiveFactor, [0.25, 0.25, 0.25]);
|
||||
assert.equal(gltf.materials[0].emissiveTexture.index, gltf.materials[0].pbrMetallicRoughness.baseColorTexture.index);
|
||||
const reversed = model.includes("truck_a03_001");
|
||||
assert.equal(reversed, REVERSED_MODEL_IDS.has(model.match(/vehicle-(.+)\.gltf$/)[1]));
|
||||
assert.equal(gltf.nodes.every((node) => JSON.stringify(node.rotation || []) === JSON.stringify([0, 1, 0, 0])), reversed);
|
||||
}
|
||||
}
|
||||
|
||||
const html = cesiumPreviewHtml(
|
||||
"scene<&>.glb",
|
||||
"scene.json",
|
||||
"route.json",
|
||||
"vehicle.gltf",
|
||||
"north<&>\u2028valley",
|
||||
["car-a.gltf", "truck-a.gltf"],
|
||||
);
|
||||
assert.match(html, /<title>north<&>\u2028valley Cesium Preview<\/title>/);
|
||||
assert.match(html, /Loading scene<&>\.glb/);
|
||||
assert.match(html, /"areaId":"north\\u003c\\u0026\\u003e\\u2028valley"/);
|
||||
assert.match(html, /"glbName":"scene\\u003c\\u0026\\u003e\.glb"/);
|
||||
assert.match(html, /"vehicleModelNames":\["car-a\.gltf","truck-a\.gltf"\]/);
|
||||
assert.match(html, /id="viewMode"/);
|
||||
assert.match(html, /data-view-mode="inspect"/);
|
||||
assert.match(html, /id="semanticToggles" class="control-subgroup hidden"/);
|
||||
|
||||
Reference in New Issue
Block a user