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>
|
<!DOCTYPE html>
|
||||||
<!--
|
<!--
|
||||||
Crédits :
|
Crédits :
|
||||||
- Modèle 3D « Airplane » par Poly by Google, licence CC-BY 3.0
|
- Modèle 3D « Airplane » par Poly by Google, CC-BY 3.0 — https://poly.pizza/m/a3XrQkLNna9
|
||||||
Source : https://poly.pizza/m/a3XrQkLNna9
|
- Modèle 3D « Parachute » par Poly by Google, CC-BY 3.0 — https://poly.pizza/m/3Z7vJ96JIEB
|
||||||
https://creativecommons.org/licenses/by/3.0/
|
- Licence : https://creativecommons.org/licenses/by/3.0/
|
||||||
-->
|
-->
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
INTRO SCENE — MVA Global Fret
|
INTRO SCENE — MVA Global Fret
|
||||||
Photo aérienne d'Antananarivo en fond, avion 3D piloté par la souris :
|
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'avion lâche périodiquement des colis parachutés qui tombent et
|
||||||
l'écran à mesure qu'elle bouge, et sort par la droite. Pas de scroll.
|
grandissent (perspective).
|
||||||
|
|
||||||
3D model credit (CC-BY 3.0) : « Airplane » by Poly by Google
|
3D model credits (CC-BY 3.0, Poly by Google) :
|
||||||
https://poly.pizza/m/a3XrQkLNna9
|
- « Airplane » → https://poly.pizza/m/a3XrQkLNna9
|
||||||
|
- « Parachute » → https://poly.pizza/m/3Z7vJ96JIEB
|
||||||
========================================================================= */
|
========================================================================= */
|
||||||
|
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
@ -65,38 +66,89 @@ loader.load(
|
|||||||
(err) => console.error('Failed to load airplane.glb:', err)
|
(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
|
À intervalle régulier (et tant que l'avion est dans le cadre), on
|
||||||
spawn un sprite « parachute + colis » à la position de l'avion.
|
clone un template « parachute + boîte ». La boîte est solidaire du
|
||||||
Chaque colis tombe lentement et grossit (effet de zoom dû à la
|
parachute (mêmes parent → même position/rotation/échelle), et placée
|
||||||
descente vers la caméra). Il s'efface en fin de course.
|
pile au niveau du harnais pour que l'attachement soit lisible.
|
||||||
*/
|
*/
|
||||||
const parcelTex = new THREE.TextureLoader().load('assets/parcel.png');
|
const PARCEL_SPAWN_EVERY = 1.4;
|
||||||
parcelTex.colorSpace = THREE.SRGBColorSpace;
|
const PARCEL_FALL_SPEED = 1.4;
|
||||||
|
const PARCEL_DRIFT = 0.45;
|
||||||
const PARCEL_SPAWN_EVERY = 1.4; // un colis toutes les ~1.4s
|
const PARCEL_INITIAL_SCALE = 0.45;
|
||||||
const PARCEL_FALL_SPEED = 1.4; // unités monde/s
|
const PARCEL_FINAL_SCALE = 2.4;
|
||||||
const PARCEL_DRIFT = 0.45; // dérive horizontale (sortie de soute)
|
const PARCEL_LIFETIME = 5.5;
|
||||||
const PARCEL_INITIAL_SCALE = 0.35;
|
|
||||||
const PARCEL_FINAL_SCALE = 2.6;
|
|
||||||
const PARCEL_LIFETIME = 5.5; // s avant de disparaître
|
|
||||||
|
|
||||||
const parcels = [];
|
const parcels = [];
|
||||||
let lastSpawn = 0;
|
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) {
|
function spawnParcel(x, y) {
|
||||||
const mat = new THREE.SpriteMaterial({
|
if (!parcelTemplate) return;
|
||||||
map: parcelTex, transparent: true, opacity: 0
|
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);
|
g.position.set(x + (Math.random() - 0.5) * 0.4, y - 0.4, 0.2);
|
||||||
sprite.scale.setScalar(PARCEL_INITIAL_SCALE);
|
g.scale.setScalar(PARCEL_INITIAL_SCALE);
|
||||||
scene.add(sprite);
|
/* 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({
|
parcels.push({
|
||||||
sprite, age: 0,
|
group: g, age: 0,
|
||||||
vx: (Math.random() - 0.5) * PARCEL_DRIFT, // dérive latérale légère
|
vx: (Math.random() - 0.5) * PARCEL_DRIFT,
|
||||||
sway: 0.5 + Math.random() * 0.6, // freq de balancement
|
sway: 0.5 + Math.random() * 0.6,
|
||||||
swayAmp: 0.08 + Math.random() * 0.08
|
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;
|
p.age += dt;
|
||||||
const life = p.age / PARCEL_LIFETIME;
|
const life = p.age / PARCEL_LIFETIME;
|
||||||
|
|
||||||
/* Trajectoire : chute douce + dérive + balancement parachute */
|
/* Chute + dérive + balancement (boîte et parachute solidaires) */
|
||||||
p.sprite.position.y -= PARCEL_FALL_SPEED * dt;
|
p.group.position.y -= PARCEL_FALL_SPEED * dt;
|
||||||
p.sprite.position.x += p.vx * dt;
|
p.group.position.x += p.vx * dt;
|
||||||
p.sprite.position.x += Math.sin(t * p.sway) * p.swayAmp * 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);
|
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
|
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;
|
let opacity = 1;
|
||||||
if (life < 0.06) opacity = life / 0.06;
|
if (life < 0.06) opacity = life / 0.06;
|
||||||
else if (life > 0.85) opacity = (1 - life) / 0.15;
|
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) {
|
if (life >= 1 || p.group.position.y < -10) {
|
||||||
scene.remove(p.sprite);
|
scene.remove(p.group);
|
||||||
p.sprite.material.dispose();
|
p.group.traverse((node) => {
|
||||||
|
if (node.isMesh && node.material) node.material.dispose();
|
||||||
|
});
|
||||||
parcels.splice(i, 1);
|
parcels.splice(i, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user