Switch parcel sprites to a real 3D parachute GLB with bound cargo
Drops the parcel.png sprite (which baked the parachute and the box into a single image) for a real 3D model: assets/parachute.glb is the CC-BY 3.0 « Parachute » by Poly by Google, decompressed from poly.pizza's static.poly.pizza CDN. The parcel template now stacks four meshes inside one Group so they move as a unit: - the loaded parachute (centered + scaled to ~1.6 world units max) - a 0.46×0.36×0.46 brown box with metal-low MeshStandardMaterial, positioned at y = -0.96 — right below the parachute's harness point — so the model's strings appear to terminate on it - two thin black straps wrapping the box (0.04-thick boxes, one horizontal one vertical) for visual reinforcement that the cargo is tied down spawnParcel clones the template (deep), per-instance clones every material so opacity can be modulated independently per parcel, and adds a slight pendulum sway + slow Y spin. Falling/scale-up/fade logic adapted from the sprite version. Cleanup disposes the cloned materials when a parcel exits. HTML credit comment extended to attribute both the airplane and the new parachute under CC-BY 3.0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3fc8f26c2e
commit
6b0f8d9afb
BIN
assets/parachute.glb
Normal file
BIN
assets/parachute.glb
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 219 KiB |
@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
Crédits :
|
||||
- Modèle 3D « Airplane » par Poly by Google, licence CC-BY 3.0
|
||||
Source : https://poly.pizza/m/a3XrQkLNna9
|
||||
https://creativecommons.org/licenses/by/3.0/
|
||||
- Modèle 3D « Airplane » par Poly by Google, CC-BY 3.0 — https://poly.pizza/m/a3XrQkLNna9
|
||||
- Modèle 3D « Parachute » par Poly by Google, CC-BY 3.0 — https://poly.pizza/m/3Z7vJ96JIEB
|
||||
- Licence : https://creativecommons.org/licenses/by/3.0/
|
||||
-->
|
||||
<html lang="fr">
|
||||
<head>
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
/* =========================================================================
|
||||
INTRO SCENE — MVA Global Fret
|
||||
Photo aérienne d'Antananarivo en fond, avion 3D piloté par la souris :
|
||||
il entre par le haut-gauche quand la souris est à gauche, traverse
|
||||
l'écran à mesure qu'elle bouge, et sort par la droite. Pas de scroll.
|
||||
Photo aérienne d'Antananarivo en fond, avion 3D piloté par la souris.
|
||||
L'avion lâche périodiquement des colis parachutés qui tombent et
|
||||
grandissent (perspective).
|
||||
|
||||
3D model credit (CC-BY 3.0) : « Airplane » by Poly by Google
|
||||
https://poly.pizza/m/a3XrQkLNna9
|
||||
3D model credits (CC-BY 3.0, Poly by Google) :
|
||||
- « Airplane » → https://poly.pizza/m/a3XrQkLNna9
|
||||
- « Parachute » → https://poly.pizza/m/3Z7vJ96JIEB
|
||||
========================================================================= */
|
||||
|
||||
import * as THREE from 'three';
|
||||
@ -65,38 +66,89 @@ loader.load(
|
||||
(err) => console.error('Failed to load airplane.glb:', err)
|
||||
);
|
||||
|
||||
/* ── Colis parachutés ──────────────────────────────────────────────────
|
||||
/* ── Colis parachutés (modèle 3D Poly by Google + boîte cartonnée) ──────
|
||||
À intervalle régulier (et tant que l'avion est dans le cadre), on
|
||||
spawn un sprite « parachute + colis » à la position de l'avion.
|
||||
Chaque colis tombe lentement et grossit (effet de zoom dû à la
|
||||
descente vers la caméra). Il s'efface en fin de course.
|
||||
clone un template « parachute + boîte ». La boîte est solidaire du
|
||||
parachute (mêmes parent → même position/rotation/échelle), et placée
|
||||
pile au niveau du harnais pour que l'attachement soit lisible.
|
||||
*/
|
||||
const parcelTex = new THREE.TextureLoader().load('assets/parcel.png');
|
||||
parcelTex.colorSpace = THREE.SRGBColorSpace;
|
||||
|
||||
const PARCEL_SPAWN_EVERY = 1.4; // un colis toutes les ~1.4s
|
||||
const PARCEL_FALL_SPEED = 1.4; // unités monde/s
|
||||
const PARCEL_DRIFT = 0.45; // dérive horizontale (sortie de soute)
|
||||
const PARCEL_INITIAL_SCALE = 0.35;
|
||||
const PARCEL_FINAL_SCALE = 2.6;
|
||||
const PARCEL_LIFETIME = 5.5; // s avant de disparaître
|
||||
const PARCEL_SPAWN_EVERY = 1.4;
|
||||
const PARCEL_FALL_SPEED = 1.4;
|
||||
const PARCEL_DRIFT = 0.45;
|
||||
const PARCEL_INITIAL_SCALE = 0.45;
|
||||
const PARCEL_FINAL_SCALE = 2.4;
|
||||
const PARCEL_LIFETIME = 5.5;
|
||||
|
||||
const parcels = [];
|
||||
let lastSpawn = 0;
|
||||
|
||||
let parcelTemplate = null;
|
||||
|
||||
const parachuteLoader = new GLTFLoader();
|
||||
parachuteLoader.load(
|
||||
'assets/parachute.glb',
|
||||
(gltf) => {
|
||||
const para = gltf.scene;
|
||||
|
||||
/* Centrer + normaliser le parachute à ~1.6 unités max */
|
||||
const box = new THREE.Box3().setFromObject(para);
|
||||
const size = box.getSize(new THREE.Vector3());
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
para.position.sub(center);
|
||||
const baseScale = 1.6 / Math.max(size.x, size.y, size.z);
|
||||
para.scale.setScalar(baseScale);
|
||||
|
||||
/* Boîte cartonnée brune, dimensionnée pour ressembler à un colis
|
||||
réaliste, attachée juste au-dessous du harnais (bottom of bbox
|
||||
≈ y -0.8 après centrage + scaling à 1.6). */
|
||||
const cargo = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(0.46, 0.36, 0.46),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: 0xb98859, roughness: 0.85, metalness: 0.05
|
||||
})
|
||||
);
|
||||
cargo.position.y = -0.96; // pendu juste sous le harnais
|
||||
|
||||
/* Petites « sangles » noires sur la boîte (deux fines bandes
|
||||
perpendiculaires) → renforce visuellement l'idée d'attachement. */
|
||||
const strapMat = new THREE.MeshStandardMaterial({ color: 0x2a2118, roughness: 0.6 });
|
||||
const strapH = new THREE.Mesh(new THREE.BoxGeometry(0.48, 0.04, 0.48), strapMat);
|
||||
strapH.position.copy(cargo.position);
|
||||
const strapV = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.38, 0.48), strapMat);
|
||||
strapV.position.copy(cargo.position);
|
||||
|
||||
parcelTemplate = new THREE.Group();
|
||||
parcelTemplate.add(para);
|
||||
parcelTemplate.add(cargo);
|
||||
parcelTemplate.add(strapH);
|
||||
parcelTemplate.add(strapV);
|
||||
},
|
||||
undefined,
|
||||
(err) => console.error('Failed to load parachute.glb:', err)
|
||||
);
|
||||
|
||||
function spawnParcel(x, y) {
|
||||
const mat = new THREE.SpriteMaterial({
|
||||
map: parcelTex, transparent: true, opacity: 0
|
||||
if (!parcelTemplate) return;
|
||||
const g = parcelTemplate.clone(true);
|
||||
/* On clone aussi les matériaux pour pouvoir gérer le fade par instance */
|
||||
g.traverse((node) => {
|
||||
if (node.isMesh && node.material) {
|
||||
node.material = node.material.clone();
|
||||
node.material.transparent = true;
|
||||
}
|
||||
});
|
||||
const sprite = new THREE.Sprite(mat);
|
||||
sprite.position.set(x + (Math.random() - 0.5) * 0.4, y - 0.2, 0.2);
|
||||
sprite.scale.setScalar(PARCEL_INITIAL_SCALE);
|
||||
scene.add(sprite);
|
||||
|
||||
g.position.set(x + (Math.random() - 0.5) * 0.4, y - 0.4, 0.2);
|
||||
g.scale.setScalar(PARCEL_INITIAL_SCALE);
|
||||
/* Petit angle initial différent pour chaque colis pour la variété */
|
||||
g.rotation.y = Math.random() * Math.PI * 2;
|
||||
scene.add(g);
|
||||
parcels.push({
|
||||
sprite, age: 0,
|
||||
vx: (Math.random() - 0.5) * PARCEL_DRIFT, // dérive latérale légère
|
||||
sway: 0.5 + Math.random() * 0.6, // freq de balancement
|
||||
swayAmp: 0.08 + Math.random() * 0.08
|
||||
group: g, age: 0,
|
||||
vx: (Math.random() - 0.5) * PARCEL_DRIFT,
|
||||
sway: 0.5 + Math.random() * 0.6,
|
||||
swayAmp: 0.08 + Math.random() * 0.08,
|
||||
spinSpeed: (Math.random() - 0.5) * 0.4
|
||||
});
|
||||
}
|
||||
|
||||
@ -106,26 +158,34 @@ function updateParcels(dt, t) {
|
||||
p.age += dt;
|
||||
const life = p.age / PARCEL_LIFETIME;
|
||||
|
||||
/* Trajectoire : chute douce + dérive + balancement parachute */
|
||||
p.sprite.position.y -= PARCEL_FALL_SPEED * dt;
|
||||
p.sprite.position.x += p.vx * dt;
|
||||
p.sprite.position.x += Math.sin(t * p.sway) * p.swayAmp * dt;
|
||||
/* Chute + dérive + balancement (boîte et parachute solidaires) */
|
||||
p.group.position.y -= PARCEL_FALL_SPEED * dt;
|
||||
p.group.position.x += p.vx * dt;
|
||||
p.group.position.x += Math.sin(t * p.sway) * p.swayAmp * dt;
|
||||
/* Léger basculement — pendulum naturel */
|
||||
p.group.rotation.z = Math.sin(t * p.sway + p.age) * 0.10;
|
||||
p.group.rotation.y += p.spinSpeed * dt;
|
||||
|
||||
/* Grandit en tombant — perspective */
|
||||
/* Grandit (perspective) */
|
||||
const k = Math.min(1, life * 1.1);
|
||||
p.sprite.scale.setScalar(
|
||||
p.group.scale.setScalar(
|
||||
PARCEL_INITIAL_SCALE + (PARCEL_FINAL_SCALE - PARCEL_INITIAL_SCALE) * k
|
||||
);
|
||||
|
||||
/* Fade-in rapide au spawn, fade-out lent à la fin */
|
||||
/* Fade-in rapide / fade-out lent — propagé sur tous les meshes */
|
||||
let opacity = 1;
|
||||
if (life < 0.06) opacity = life / 0.06;
|
||||
else if (life > 0.85) opacity = (1 - life) / 0.15;
|
||||
p.sprite.material.opacity = Math.max(0, Math.min(1, opacity));
|
||||
opacity = Math.max(0, Math.min(1, opacity));
|
||||
p.group.traverse((node) => {
|
||||
if (node.isMesh && node.material) node.material.opacity = opacity;
|
||||
});
|
||||
|
||||
if (life >= 1 || p.sprite.position.y < -10) {
|
||||
scene.remove(p.sprite);
|
||||
p.sprite.material.dispose();
|
||||
if (life >= 1 || p.group.position.y < -10) {
|
||||
scene.remove(p.group);
|
||||
p.group.traverse((node) => {
|
||||
if (node.isMesh && node.material) node.material.dispose();
|
||||
});
|
||||
parcels.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user