web-3Dviever-glb/index.html Repository web-3Dviever-glb Original path web-3Dviever-glb/index.htmlRole SOURCE Size 53908 bytes Lines 1890 SHA-256 245ec511830c888022123d3efca5a44eded70f64927cbfaa8ab3645e1463f2beDisplayed range 1501–1890 Previous file/page · Project index · Next file/page
if (classification.kind === "mesh") {
if (!geometry.hasAttribute("normal")) {
geometry.computeVertexNormals();
}
const material = new THREE.MeshStandardMaterial({
vertexColors: hasVertexColors,
color: hasVertexColors ? 0xffffff : 0xb8b8b8,
roughness: 0.92,
metalness: 0,
side: THREE.DoubleSide
});
object = new THREE.Mesh(geometry, material);
} else {
const diagonal = geometry.boundingBox
? geometry.boundingBox.getSize(new THREE.Vector3()).length()
: 1;
const pointSize = Math.max(diagonal * 0.00125, 0.0002);
const material = makeCircularPointsMaterial({
size: pointSize,
vertexColors: hasVertexColors
});
if (!hasVertexColors) {
material.color.set(0xd0d0d0);
}
object = new THREE.Points(geometry, material);
object.frustumCulled = true;
}
applyScaniversePlyOrientation(object, parsedHeader);
scene.add(object);
loadedObject = object;
loadedKind = classification.kind;
loadedModelName = displayName;
fitCameraToObject(object, new THREE.Vector3(0, 0, -1));
const positionCount = geometry.getAttribute("position")?.count ?? 0;
if (classification.kind === "mesh") {
const triangleCount = geometry.index
? geometry.index.count / 3
: classification.faceCount;
setStatus(
`Model załadowany poprawnie.\n` +
`Tryb: klasyczny mesh PLY\n` +
`Wierzchołki: ${positionCount.toLocaleString("pl-PL")}\n` +
`Trójkąty/ściany: ${Math.round(triangleCount).toLocaleString("pl-PL")}\n` +
`Kolory wierzchołków: ${hasVertexColors ? "tak" : "nie"}`
);
} else {
setStatus(
`Model załadowany poprawnie.\n` +
`Tryb: kolorowa chmura punktów PLY\n` +
`Punkty: ${positionCount.toLocaleString("pl-PL")}\n` +
`Kolory RGB: ${hasVertexColors ? "tak" : "nie"}\n` +
`Uwaga: wersja GLB wygląda znacznie bliżej Scaniverse.`
);
}
hideStatusOverlay(4300);
}
async function loadGaussianSplat(
modelUrl,
parsedHeader,
classification,
generation,
displayName
) {
let lastDisplayedPercent = -1;
const splatMesh = new SplatMesh({
url: modelUrl,
extSplats: true,
lod: false,
onProgress: (event) => {
if (generation !== loadGeneration) {
return;
}
if (
event.lengthComputable &&
Number.isFinite(event.total) &&
event.total > 0
) {
const percent = Math.floor((event.loaded / event.total) * 100);
if (percent !== lastDisplayedPercent) {
lastDisplayedPercent = percent;
setStatus(
`Ładowanie modelu Gaussian Splatting...\n` +
`${percent}% — ${(event.loaded / 1024 / 1024).toFixed(1)} / ` +
`${(event.total / 1024 / 1024).toFixed(1)} MB`
);
}
} else {
setStatus(
`Ładowanie i dekodowanie splatów...\n` +
`${Number.isFinite(event.loaded)
? (event.loaded / 1024 / 1024).toFixed(1)
: "?"} MB`
);
}
}
});
splatMesh.maxSh = 3;
applyScaniversePlyOrientation(splatMesh, parsedHeader);
scene.add(splatMesh);
loadedObject = splatMesh;
loadedKind = "splat";
loadedModelName = displayName;
try {
await splatMesh.initialized;
if (generation !== loadGeneration) {
scene.remove(splatMesh);
splatMesh.dispose?.();
return;
}
splatMesh.updateGenerator();
homeCameraPosition.set(0, 0, -3.2);
homeCameraTarget.set(0, 0, -0.05);
loadedModelRadius = 1;
camera.near = 0.01;
camera.far = 1000;
camera.updateProjectionMatrix();
resetCamera();
const numberOfSplats =
splatMesh.numSplats ??
splatMesh.packedSplats?.numSplats ??
classification.vertexCount;
setStatus(
`Model załadowany poprawnie.\n` +
`Tryb: 3D Gaussian Splatting\n` +
`Splaty: ${Number(numberOfSplats).toLocaleString("pl-PL")}\n` +
`Spherical harmonics: do SH3`
);
hideStatusOverlay();
} catch (error) {
scene.remove(splatMesh);
splatMesh.dispose?.();
loadedObject = null;
loadedKind = null;
throw error;
}
}
function extensionOf(fileName) {
const dotIndex = fileName.lastIndexOf(".");
return dotIndex === -1 ? "" : fileName.slice(dotIndex).toLowerCase();
}
function baseNameWithoutExtension(fileName) {
const dotIndex = fileName.lastIndexOf(".");
return dotIndex === -1 ? fileName : fileName.slice(0, dotIndex);
}
function encodeInputUrl(fileName) {
return `./input/${encodeURIComponent(fileName)}`;
}
async function urlExists(url) {
try {
const response = await fetch(url, {
method: "HEAD",
cache: "no-store"
});
return response.ok;
} catch {
return false;
}
}
async function getModelList() {
/*
* GitHub Pages nie uruchamia backendu Node/Express.
* Publiczna wersja korzysta więc ze statycznego manifestu models.json.
* Jeżeli manifestu nie ma, lokalny server.js może dostarczyć listę
* dynamicznie przez /api/models.
*/
try {
const manifestResponse = await fetch("./models.json", {
cache: "no-store"
});
if (manifestResponse.ok) {
const models = await manifestResponse.json();
if (Array.isArray(models)) return models;
}
} catch {
// Fallback do lokalnego API poniżej.
}
const apiResponse = await fetch("./api/models", { cache: "no-store" });
if (!apiResponse.ok) {
throw new Error(
"Nie udało się pobrać listy modeli z models.json ani /api/models."
);
}
return apiResponse.json();
}
function modelSortValue(name) {
const extension = extensionOf(name);
const priority = extension === ".glb"
? 0
: extension === ".gltf"
? 1
: extension === ".ply"
? 2
: 9;
return `${baseNameWithoutExtension(name).toLocaleLowerCase("pl-PL")}\u0000${priority}\u0000${name}`;
}
function fillModelSelect(models) {
const sortedModels = [...models].sort((left, right) =>
modelSortValue(left).localeCompare(
modelSortValue(right),
"pl-PL",
{ numeric: true }
)
);
availableModelNames = new Set(sortedModels);
modelSelect.innerHTML = "";
for (const modelName of sortedModels) {
const option = document.createElement("option");
const extension = extensionOf(modelName);
option.value = modelName;
option.textContent = extension === ".glb"
? `${modelName} — teksturowany mesh`
: extension === ".gltf"
? `${modelName} — teksturowany mesh`
: `${modelName} — PLY`;
modelSelect.appendChild(option);
}
const firstGlb = sortedModels.find((name) =>
[".glb", ".gltf"].includes(extensionOf(name))
);
if (firstGlb) {
modelSelect.value = firstGlb;
}
}
async function loadSelectedModel() {
const selectedName = modelSelect.value;
if (!selectedName) {
return;
}
const generation = ++loadGeneration;
const extension = extensionOf(selectedName);
const modelUrl = encodeInputUrl(selectedName);
showStatusOverlay();
setLoadingState(true);
removeLoadedObject();
try {
if (extension === ".glb" || extension === ".gltf") {
await loadGlb(modelUrl, selectedName, generation, false);
return;
}
if (extension !== ".ply") {
throw new Error(`Nieobsługiwane rozszerzenie: ${extension || "brak"}`);
}
setStatus("Rozpoznawanie rodzaju pliku PLY...");
const parsedHeader = await fetchPlyHeader(modelUrl);
const classification = classifyPly(parsedHeader);
if (generation !== loadGeneration) {
return;
}
if (classification.kind === "splat") {
await loadGaussianSplat(
modelUrl,
parsedHeader,
classification,
generation,
selectedName
);
return;
}
const companionGlbName = `${baseNameWithoutExtension(selectedName)}.glb`;
const companionGlbUrl = encodeInputUrl(companionGlbName);
const companionIsHiddenByOldServer =
!availableModelNames.has(companionGlbName);
if (
PREFER_MATCHING_GLB_WHEN_HIDDEN_BY_OLD_SERVER &&
companionIsHiddenByOldServer &&
await urlExists(companionGlbUrl)
) {
await loadGlb(
companionGlbUrl,
companionGlbName,
generation,
true
);
return;
}
await loadClassicPly(
modelUrl,
parsedHeader,
classification,
generation,
selectedName
);
} catch (error) {
console.error("Nie udało się załadować modelu:", error);
setStatus(
`Błąd ładowania modelu:\n${error?.message ?? String(error)}\n\n` +
`Sprawdź plik:\n${selectedName}`,
true
);
} finally {
if (generation === loadGeneration) {
setLoadingState(false);
}
}
}
loadButton.addEventListener("click", loadSelectedModel);
try {
const models = await getModelList();
if (!Array.isArray(models) || models.length === 0) {
throw new Error(
"Katalog input jest pusty. Umieść w nim plik .glb, .gltf albo .ply."
);
}
fillModelSelect(models);
await loadSelectedModel();
} catch (error) {
console.error("Nie udało się zainicjalizować listy modeli:", error);
setStatus(error?.message ?? String(error), true);
loadButton.disabled = true;
modelSelect.disabled = true;
resetButton.disabled = true;
}
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
updateMobileMode();
});
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled promise rejection:", event.reason);
});
renderer.setAnimationLoop((now) => {
updateFlightAnimation(now);
/* OrbitControls nie może w tym czasie dołożyć ruchu orbitalnego. */
if (!inPlaceLook.suppressUntilAllButtonsReleased) {
controls.update();
}
renderer.render(scene, camera);
});
</script>
</body>
</html>