Add preloader to interactivity example

This commit is contained in:
Diego F. Goberna
2025-05-25 02:15:54 +02:00
parent 3332f609a9
commit 9433e6ff0b
5 changed files with 114 additions and 62 deletions
+71 -61
View File
@@ -7,7 +7,6 @@
</head> </head>
<body> <body>
<img src="loading.gif" alt="loading" id="loading">
<div id="menu"> <div id="menu">
<div class="border"> <div class="border">
<div class="border"> <div class="border">
@@ -36,6 +35,7 @@
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { FOOD_ASSETS, FOOD_URL } from './food.js'; import { FOOD_ASSETS, FOOD_URL } from './food.js';
import { getAssetFileURL } from "/examples/js/get-asset-url.js"; import { getAssetFileURL } from "/examples/js/get-asset-url.js";
import { preloadSplats } from "/examples/js/preloader.js";
// Add food items to the menu // Add food items to the menu
FOOD_ASSETS.forEach((food, i) => { FOOD_ASSETS.forEach((food, i) => {
@@ -43,7 +43,7 @@
el.textContent = food.name; el.textContent = food.name;
el.href = 'javascript:;'; el.href = 'javascript:;';
el.addEventListener('click', async function () { el.addEventListener('click', async function () {
await switchToFood(i); switchToFood(i);
}); });
document.getElementById('menu_list').appendChild(el); document.getElementById('menu_list').appendChild(el);
}); });
@@ -58,50 +58,6 @@
const camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000); const camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0.9, -1.2); camera.position.set(0, 0.9, -1.2);
// Setup lighting
const spotLight = new THREE.SpotLight(0xffcc88);
spotLight.position.set(0, 1, 0);
spotLight.castShadow = true;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.camera.near = 0.1;
spotLight.shadow.camera.far = 5;
spotLight.angle = 0.9;
spotLight.penumbra = 1;
spotLight.intensity = 3;
scene.add(spotLight);
const fillLight = new THREE.PointLight(0xffcc88, 0.2);
fillLight.position.set(0, 0, -3);
scene.add(fillLight);
// Splats don't project shadows, so we add a cylinder below the spotlight to fake one ;)
const geometry = new THREE.CylinderGeometry(0.45, 0.45, 0.04, 40, 1);
const material = new THREE.MeshPhongMaterial({ colorWrite: false, depthWrite: false });
const shadow = new THREE.Mesh(geometry, material);
shadow.visible = false;
shadow.castShadow = true;
shadow.position.set(0, 0.1, 0);
scene.add(shadow);
// Add table
const gltfLoader = new GLTFLoader();
const modelURL = await getAssetFileURL("table.glb");
const gltfTable = await gltfLoader.loadAsync(modelURL);
const table = gltfTable.scene;
// Set the table cloth to receive shadows
const tableCloth = table.children.find(item => item.name == 'cover');
tableCloth.receiveShadow = true;
scene.add(table);
// Add floor
const plane = new THREE.PlaneGeometry(10, 10);
const floormat = new THREE.MeshPhongMaterial({ color: 0x777777 });
const floor = new THREE.Mesh(plane, floormat);
floor.rotation.x = -Math.PI / 2;
floor.position.set(0, -1.397, 0);
scene.add(floor);
// Setup mouse controls to orbit the camera around // Setup mouse controls to orbit the camera around
const controls = new OrbitControls(camera, renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0.2, 0, 0); controls.target.set(0.2, 0, 0);
@@ -110,10 +66,18 @@
controls.enablePan = false; controls.enablePan = false;
controls.update(); controls.update();
// Current and next food splats // Current and next food splats
let food, nextFood; let food, nextFood;
// Other items
let table, shadow;
// Add table
const gltfLoader = new GLTFLoader();
const modelURL = await getAssetFileURL("table.glb");
const gltfTable = await gltfLoader.loadAsync(modelURL);
table = gltfTable.scene;
const loadingIcon = document.getElementById('loading');
// Transition length in frames. // Transition length in frames.
// Two transitions: one for fading out the old food, another for fading in the next one // Two transitions: one for fading out the old food, another for fading in the next one
const TRANSITION_LENGTH = 60; const TRANSITION_LENGTH = 60;
@@ -121,10 +85,64 @@
let fadeOutTime = null; let fadeOutTime = null;
let fadeInTime = null; let fadeInTime = null;
// Load first food by default // Preload all splat files
await switchToFood(0); let splats;
preloadSplats(FOOD_ASSETS.map(t => t.file)).then(loaded_splats => {
splats = loaded_splats;
init();
});
renderer.setAnimationLoop(function animate(time) { async function init() {
// show menu
document.getElementById('menu').style.display = 'block';
// Setup lighting
const spotLight = new THREE.SpotLight(0xffcc88);
spotLight.position.set(0, 1, 0);
spotLight.castShadow = true;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.camera.near = 0.1;
spotLight.shadow.camera.far = 5;
spotLight.angle = 0.9;
spotLight.penumbra = 1;
spotLight.intensity = 3;
scene.add(spotLight);
const fillLight = new THREE.PointLight(0xffcc88, 0.2);
fillLight.position.set(0, 0, -3);
scene.add(fillLight);
// Splats don't project shadows, so we add a cylinder below the spotlight to fake one ;)
const geometry = new THREE.CylinderGeometry(0.45, 0.45, 0.04, 40, 1);
const material = new THREE.MeshPhongMaterial({ colorWrite: false, depthWrite: false });
shadow = new THREE.Mesh(geometry, material);
shadow.visible = false;
shadow.castShadow = true;
shadow.position.set(0, 0.1, 0);
scene.add(shadow);
// Set the table cloth to receive shadows
const tableCloth = table.children.find(item => item.name == 'cover');
tableCloth.receiveShadow = true;
scene.add(table);
// Add floor
const plane = new THREE.PlaneGeometry(10, 10);
const floormat = new THREE.MeshPhongMaterial({ color: 0x777777 });
const floor = new THREE.Mesh(plane, floormat);
floor.rotation.x = -Math.PI / 2;
floor.position.set(0, -1.397, 0);
scene.add(floor);
// Load first food by default
switchToFood(0);
// Start render loop
renderer.setAnimationLoop(animate);
}
function animate(time) {
controls.update(); controls.update();
renderer.render(scene, camera); renderer.render(scene, camera);
const rotation = time / 10000; const rotation = time / 10000;
@@ -161,20 +179,17 @@
fadeInTime = null; fadeInTime = null;
} }
} }
};
});
// Change food from menu link // Change food from menu link
async function switchToFood(foodIndex) { function switchToFood(foodIndex) {
// already transitioning // already transitioning
if (fadeOutTime !== null || fadeInTime !== null) return; if (fadeOutTime !== null || fadeInTime !== null) return;
const foodItem = FOOD_ASSETS[foodIndex]; const foodItem = FOOD_ASSETS[foodIndex];
const splatURL = await getAssetFileURL(foodItem.file); nextFood = splats[foodItem.file];
nextFood = new SplatMesh({ url: splatURL });
nextFood.quaternion.set(1, 0, 0, 0); nextFood.quaternion.set(1, 0, 0, 0);
// Customize splat depending on the settings set for this food // Customize splat depending on the settings set for this food
@@ -188,7 +203,6 @@
// Setup shadow to the required size, and make it visible only when the splat is initialized // Setup shadow to the required size, and make it visible only when the splat is initialized
nextFood.initialized.then(() => { nextFood.initialized.then(() => {
shadow.visible = false; shadow.visible = false;
loadingIcon.style.display = 'none';
fadeOutTime = 0; // Fade in next food fadeOutTime = 0; // Fade in next food
}); });
@@ -197,10 +211,6 @@
// toggle menu item // toggle menu item
const menu_items = document.getElementById('menu_list').children; const menu_items = document.getElementById('menu_list').children;
for (let i = 0; i < menu_items.length; i++) { for (let i = 0; i < menu_items.length; i++) {
if (foodIndex == i) {
loadingIcon.style.display = 'inline';
menu_items[i].appendChild(loadingIcon);
}
menu_items[i].classList.toggle('active', foodIndex == i); menu_items[i].classList.toggle('active', foodIndex == i);
} }
} }
+2 -1
View File
@@ -2,6 +2,7 @@
body { body {
margin: 0; margin: 0;
background-color: black;
} }
h1 { h1 {
@@ -24,7 +25,7 @@ h3 {
#menu { #menu {
position: absolute; position: absolute;
left: -12rem; left: -12rem;
display: inline-block; display: none;
background-color: rgba(223, 203, 171, 0.8); background-color: rgba(223, 203, 171, 0.8);
padding: 2rem 2rem; padding: 2rem 2rem;
margin-top: 4rem; margin-top: 4rem;
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

+41
View File
@@ -0,0 +1,41 @@
import { SplatMesh } from "@forge-gfx/forge";
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
// Add loading icon in the center of the screen
function addLoader(loading_icon_color) {
const loaderIcon = new Image();
loaderIcon.src = `/examples/js/forge_loading_${loading_icon_color}.gif`;
loaderIcon.id = "_forge_loader";
loaderIcon.style.position = "absolute";
loaderIcon.style.zIndex = 99999;
loaderIcon.style.left = "50%";
loaderIcon.style.top = "50%";
loaderIcon.style.transform = "translate(-50%, -50%)";
loaderIcon.style.pointerEvents = "none";
document.body.appendChild(loaderIcon);
}
function removeLoader() {
const el = document.getElementById("_forge_loader");
el.parentNode.removeChild(el);
}
// preload splats, returning a map [filename -> SplatMesh]
export async function preloadSplats(assets, loading_icon_color = "black") {
addLoader(loading_icon_color);
const map = {};
await Promise.all(
assets.map(async (asset) => {
const splatURL = await getAssetFileURL(asset);
return new Promise((resolve) => {
map[asset] = new SplatMesh({
url: splatURL,
onLoad: () => resolve(),
});
});
}),
);
removeLoader();
return map;
}