Skip to content

Commit 821a7ae

Browse files
authored
Merge pull request #1 from forge-gfx/loader
Add preloader to interactivity example
2 parents 3332f60 + 9433e6f commit 821a7ae

5 files changed

Lines changed: 114 additions & 62 deletions

File tree

examples/interactivity/index.html

Lines changed: 71 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
</head>
88

99
<body>
10-
<img src="loading.gif" alt="loading" id="loading">
1110
<div id="menu">
1211
<div class="border">
1312
<div class="border">
@@ -36,14 +35,15 @@ <h2>Food scans by <a href="https://x.com/tipatat" target="_blank">Tipatat</a></h
3635
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
3736
import { FOOD_ASSETS, FOOD_URL } from './food.js';
3837
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
38+
import { preloadSplats } from "/examples/js/preloader.js";
3939

4040
// Add food items to the menu
4141
FOOD_ASSETS.forEach((food, i) => {
4242
const el = document.createElement("a");
4343
el.textContent = food.name;
4444
el.href = 'javascript:;';
4545
el.addEventListener('click', async function () {
46-
await switchToFood(i);
46+
switchToFood(i);
4747
});
4848
document.getElementById('menu_list').appendChild(el);
4949
});
@@ -58,50 +58,6 @@ <h2>Food scans by <a href="https://x.com/tipatat" target="_blank">Tipatat</a></h
5858
const camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
5959
camera.position.set(0, 0.9, -1.2);
6060

61-
// Setup lighting
62-
const spotLight = new THREE.SpotLight(0xffcc88);
63-
spotLight.position.set(0, 1, 0);
64-
spotLight.castShadow = true;
65-
spotLight.shadow.mapSize.width = 1024;
66-
spotLight.shadow.mapSize.height = 1024;
67-
spotLight.shadow.camera.near = 0.1;
68-
spotLight.shadow.camera.far = 5;
69-
spotLight.angle = 0.9;
70-
spotLight.penumbra = 1;
71-
spotLight.intensity = 3;
72-
scene.add(spotLight);
73-
74-
const fillLight = new THREE.PointLight(0xffcc88, 0.2);
75-
fillLight.position.set(0, 0, -3);
76-
scene.add(fillLight);
77-
78-
// Splats don't project shadows, so we add a cylinder below the spotlight to fake one ;)
79-
const geometry = new THREE.CylinderGeometry(0.45, 0.45, 0.04, 40, 1);
80-
const material = new THREE.MeshPhongMaterial({ colorWrite: false, depthWrite: false });
81-
const shadow = new THREE.Mesh(geometry, material);
82-
shadow.visible = false;
83-
shadow.castShadow = true;
84-
shadow.position.set(0, 0.1, 0);
85-
scene.add(shadow);
86-
87-
// Add table
88-
const gltfLoader = new GLTFLoader();
89-
const modelURL = await getAssetFileURL("table.glb");
90-
const gltfTable = await gltfLoader.loadAsync(modelURL);
91-
const table = gltfTable.scene;
92-
// Set the table cloth to receive shadows
93-
const tableCloth = table.children.find(item => item.name == 'cover');
94-
tableCloth.receiveShadow = true;
95-
scene.add(table);
96-
97-
// Add floor
98-
const plane = new THREE.PlaneGeometry(10, 10);
99-
const floormat = new THREE.MeshPhongMaterial({ color: 0x777777 });
100-
const floor = new THREE.Mesh(plane, floormat);
101-
floor.rotation.x = -Math.PI / 2;
102-
floor.position.set(0, -1.397, 0);
103-
scene.add(floor);
104-
10561
// Setup mouse controls to orbit the camera around
10662
const controls = new OrbitControls(camera, renderer.domElement);
10763
controls.target.set(0.2, 0, 0);
@@ -110,21 +66,83 @@ <h2>Food scans by <a href="https://x.com/tipatat" target="_blank">Tipatat</a></h
11066
controls.enablePan = false;
11167
controls.update();
11268

69+
11370
// Current and next food splats
11471
let food, nextFood;
72+
// Other items
73+
let table, shadow;
74+
75+
// Add table
76+
const gltfLoader = new GLTFLoader();
77+
const modelURL = await getAssetFileURL("table.glb");
78+
const gltfTable = await gltfLoader.loadAsync(modelURL);
79+
table = gltfTable.scene;
11580

116-
const loadingIcon = document.getElementById('loading');
11781
// Transition length in frames.
11882
// Two transitions: one for fading out the old food, another for fading in the next one
11983
const TRANSITION_LENGTH = 60;
12084
// Transition timers. `null` if transition is not active
12185
let fadeOutTime = null;
12286
let fadeInTime = null;
12387

124-
// Load first food by default
125-
await switchToFood(0);
88+
// Preload all splat files
89+
let splats;
90+
preloadSplats(FOOD_ASSETS.map(t => t.file)).then(loaded_splats => {
91+
splats = loaded_splats;
92+
init();
93+
});
94+
95+
async function init() {
96+
// show menu
97+
document.getElementById('menu').style.display = 'block';
98+
99+
// Setup lighting
100+
const spotLight = new THREE.SpotLight(0xffcc88);
101+
spotLight.position.set(0, 1, 0);
102+
spotLight.castShadow = true;
103+
spotLight.shadow.mapSize.width = 1024;
104+
spotLight.shadow.mapSize.height = 1024;
105+
spotLight.shadow.camera.near = 0.1;
106+
spotLight.shadow.camera.far = 5;
107+
spotLight.angle = 0.9;
108+
spotLight.penumbra = 1;
109+
spotLight.intensity = 3;
110+
scene.add(spotLight);
111+
112+
const fillLight = new THREE.PointLight(0xffcc88, 0.2);
113+
fillLight.position.set(0, 0, -3);
114+
scene.add(fillLight);
115+
116+
// Splats don't project shadows, so we add a cylinder below the spotlight to fake one ;)
117+
const geometry = new THREE.CylinderGeometry(0.45, 0.45, 0.04, 40, 1);
118+
const material = new THREE.MeshPhongMaterial({ colorWrite: false, depthWrite: false });
119+
shadow = new THREE.Mesh(geometry, material);
120+
shadow.visible = false;
121+
shadow.castShadow = true;
122+
shadow.position.set(0, 0.1, 0);
123+
scene.add(shadow);
124+
125+
// Set the table cloth to receive shadows
126+
const tableCloth = table.children.find(item => item.name == 'cover');
127+
tableCloth.receiveShadow = true;
128+
scene.add(table);
129+
130+
// Add floor
131+
const plane = new THREE.PlaneGeometry(10, 10);
132+
const floormat = new THREE.MeshPhongMaterial({ color: 0x777777 });
133+
const floor = new THREE.Mesh(plane, floormat);
134+
floor.rotation.x = -Math.PI / 2;
135+
floor.position.set(0, -1.397, 0);
136+
scene.add(floor);
137+
138+
// Load first food by default
139+
switchToFood(0);
140+
141+
// Start render loop
142+
renderer.setAnimationLoop(animate);
143+
}
126144

127-
renderer.setAnimationLoop(function animate(time) {
145+
function animate(time) {
128146
controls.update();
129147
renderer.render(scene, camera);
130148
const rotation = time / 10000;
@@ -161,20 +179,17 @@ <h2>Food scans by <a href="https://x.com/tipatat" target="_blank">Tipatat</a></h
161179
fadeInTime = null;
162180
}
163181
}
164-
165-
166-
});
182+
};
167183

168184
// Change food from menu link
169-
async function switchToFood(foodIndex) {
185+
function switchToFood(foodIndex) {
170186

171187
// already transitioning
172188
if (fadeOutTime !== null || fadeInTime !== null) return;
173189

174190
const foodItem = FOOD_ASSETS[foodIndex];
175191

176-
const splatURL = await getAssetFileURL(foodItem.file);
177-
nextFood = new SplatMesh({ url: splatURL });
192+
nextFood = splats[foodItem.file];
178193
nextFood.quaternion.set(1, 0, 0, 0);
179194

180195
// Customize splat depending on the settings set for this food
@@ -188,7 +203,6 @@ <h2>Food scans by <a href="https://x.com/tipatat" target="_blank">Tipatat</a></h
188203
// Setup shadow to the required size, and make it visible only when the splat is initialized
189204
nextFood.initialized.then(() => {
190205
shadow.visible = false;
191-
loadingIcon.style.display = 'none';
192206
fadeOutTime = 0; // Fade in next food
193207
});
194208

@@ -197,10 +211,6 @@ <h2>Food scans by <a href="https://x.com/tipatat" target="_blank">Tipatat</a></h
197211
// toggle menu item
198212
const menu_items = document.getElementById('menu_list').children;
199213
for (let i = 0; i < menu_items.length; i++) {
200-
if (foodIndex == i) {
201-
loadingIcon.style.display = 'inline';
202-
menu_items[i].appendChild(loadingIcon);
203-
}
204214
menu_items[i].classList.toggle('active', foodIndex == i);
205215
}
206216
}

examples/interactivity/style.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
body {
44
margin: 0;
5+
background-color: black;
56
}
67

78
h1 {
@@ -24,7 +25,7 @@ h3 {
2425
#menu {
2526
position: absolute;
2627
left: -12rem;
27-
display: inline-block;
28+
display: none;
2829
background-color: rgba(223, 203, 171, 0.8);
2930
padding: 2rem 2rem;
3031
margin-top: 4rem;
9.02 KB
Loading
9.15 KB
Loading

examples/js/preloader.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { SplatMesh } from "@forge-gfx/forge";
2+
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
3+
4+
// Add loading icon in the center of the screen
5+
function addLoader(loading_icon_color) {
6+
const loaderIcon = new Image();
7+
loaderIcon.src = `/examples/js/forge_loading_${loading_icon_color}.gif`;
8+
loaderIcon.id = "_forge_loader";
9+
loaderIcon.style.position = "absolute";
10+
loaderIcon.style.zIndex = 99999;
11+
loaderIcon.style.left = "50%";
12+
loaderIcon.style.top = "50%";
13+
loaderIcon.style.transform = "translate(-50%, -50%)";
14+
loaderIcon.style.pointerEvents = "none";
15+
document.body.appendChild(loaderIcon);
16+
}
17+
18+
function removeLoader() {
19+
const el = document.getElementById("_forge_loader");
20+
el.parentNode.removeChild(el);
21+
}
22+
23+
// preload splats, returning a map [filename -> SplatMesh]
24+
export async function preloadSplats(assets, loading_icon_color = "black") {
25+
addLoader(loading_icon_color);
26+
const map = {};
27+
await Promise.all(
28+
assets.map(async (asset) => {
29+
const splatURL = await getAssetFileURL(asset);
30+
return new Promise((resolve) => {
31+
map[asset] = new SplatMesh({
32+
url: splatURL,
33+
onLoad: () => resolve(),
34+
});
35+
});
36+
}),
37+
);
38+
39+
removeLoader();
40+
return map;
41+
}

0 commit comments

Comments
 (0)