🚌 Estacionar Ônibus 3D Real
by DriftPenguin96273 lines12.7 KB🛠️ Three.js (3D graphics)
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>🚌 Estacionar Ônibus 3D Real</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.158.0/examples/js/loaders/GLTFLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.158.0/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/cannon-es@0.20.0/dist/cannon-es.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; font-family: Arial, sans-serif; }
body { background: #f0f4f8; overflow: hidden; }
canvas { display: block; }
.ui { position: absolute; top: 15px; left: 15px; z-index: 10; background: rgba(255,255,255,0.92); padding: 12px 16px; border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.2); }
.controles { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); display: grid; grid-template-columns: repeat(3, 85px); gap: 10px; z-index: 10; }
button { padding: 12px; font-size: 22px; border: none; border-radius: 8px; background: #0066cc; color: white; box-shadow: 0 2px 5px rgba(0,0,0,0.3); touch-action: manipulation; }
button:active { background: #004c99; transform: scale(0.97); }
.mensagem { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(255,255,255,0.96); padding: 25px 35px; border-radius: 12px; font-size: 28px; font-weight: bold; display: none; z-index: 20; box-shadow: 0 5px 20px rgba(0,0,0,0.3); }
</style>
</head>
<body>
<div class="ui">
<h3>Fase: <span id="fase">1</span></h3>
<p id="descricao">Vaga reta ampla</p>
</div>
<div class="mensagem" id="msg"></div>
<div class="controles">
<div></div>
<button onclick="mover('frente')">↑</button>
<div></div>
<button onclick="virar('esq')">←</button>
<button onclick="reiniciar()">🔄</button>
<button onclick="virar('dir')">→</button>
<div></div>
<button onclick="mover('tras')">↓</button>
<div></div>
</div>
<script>
let cena, camera, renderizador, controles, mundoFisica;
let onibus = { modelo: null, corpo: null };
let faseAtual = 0;
const velocidade = 1.8;
const rotacao = 0.035;
// ✅ Modelo GLB de ônibus real gratuito e hospedado
const modeloOnibusURL = "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Bus/glTF/Bus.glb";[[__LINK_ICON]](https://threejs.org/manual/zh/loading-3d-models.html?f_link_type=f_linkinlinenote&flow_extra=eyJpbmxpbmVfZGlzcGxheV9wb3NpdGlvbiI6MCwiZG9jX3Bvc2l0aW9uIjowLCJkb2NfaWQiOiIwNGI1MDViODFiYTZjY2U2LWY0YWVlODQ4OTZlMjA0ZjMifQ%3D%3D "[__LINK_ICON]")
// 🎯 Fases progressivas
const fases = [
{
desc: "Fase 1 — Vaga reta ampla",
vaga: { pos: [0, 0, -22], ang: 0, tolDist: 2.2, tolAng: 0.22 },
obstaculos: []
},
{
desc: "Fase 2 — Vaga levemente inclinada",
vaga: { pos: [0.4, 0, -23], ang: Math.PI/12, tolDist: 2.0, tolAng: 0.20 },
obstaculos: []
},
{
desc: "Fase 3 — Vaga com poste lateral",
vaga: { pos: [0, 0, -22], ang: 0, tolDist: 1.9, tolAng: 0.19 },
obstaculos: [{ tipo: "cilindro", pos: [-2.8, 0, -21], raio: 0.35, alt: 2.2 }]
},
{
desc: "Fase 4 — Vaga estreita inclinada",
vaga: { pos: [0.6, 0, -24], ang: -Math.PI/10, tolDist: 1.7, tolAng: 0.17 },
obstaculos: [{ tipo: "caixa", pos: [-2.2, 0, -20], dim: [1.1, 1.6, 0.8] }]
},
{
desc: "Fase 5 — Desafio final apertado",
vaga: { pos: [0.2, 0, -23], ang: Math.PI/18, tolDist: 1.5, tolAng: 0.15 },
obstaculos: [
{ tipo: "cilindro", pos: [-2.6, 0, -22], raio: 0.35, alt: 2.2 },
{ tipo: "caixa", pos: [2.4, 0, -21], dim: [1.2, 1.8, 0.9] }
]
}
];
function iniciarJogo() {
cena = new THREE.Scene();
cena.background = new THREE.Color(0x87ceeb);
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.5, 120);
camera.position.set(6, 7, 14);
camera.lookAt(0, 1, 0);
renderizador = new THREE.WebGLRenderer({ antialias: true });
renderizador.setSize(window.innerWidth, window.innerHeight);
renderizador.shadowMap.enabled = true;
document.body.appendChild(renderizador.domElement);
controles = new THREE.OrbitControls(camera, renderizador.domElement);
controles.target.set(0, 1, 0);
controles.minDistance = 6;
controles.maxDistance = 22;
adicionarIluminacao();
criarChaoEVaga();
iniciarFisica();
carregarOnibusReal();
iniciarFase();
animar();
}
function adicionarIluminacao() {
const luzAmb = new THREE.AmbientLight(0xffffff, 0.7);
cena.add(luzAmb);
const luzDir = new THREE.DirectionalLight(0xffffff, 0.9);
luzDir.position.set(8, 15, 6);
luzDir.castShadow = true;
luzDir.shadow.mapSize.set(2048, 2048);
cena.add(luzDir);
}
function criarChaoEVaga() {
const chaoGeo = new THREE.PlaneGeometry(60, 60);
const chaoMat = new THREE.ShadowMaterial({ opacity: 0.4 });
const chao = new THREE.Mesh(chaoGeo, chaoMat);
chao.rotation.x = -Math.PI / 2;
chao.receiveShadow = true;
cena.add(chao);
const vagaGeo = new THREE.PlaneGeometry(9, 2.6);
const vagaMat = new THREE.MeshStandardMaterial({ color: 0xcfe2f3, transparent: true, opacity: 0.7 });
cena.userData.vagaMesh = new THREE.Mesh(vagaGeo, vagaMat);
cena.userData.vagaMesh.receiveShadow = true;
cena.add(cena.userData.vagaMesh);
}
function iniciarFisica() {
mundoFisica = new CANNON.World();
mundoFisica.gravity.set(0, -9.82, 0);
mundoFisica.broadphase = new CANNON.NaiveBroadphase();
}
function carregarOnibusReal() {
const carregador = new THREE.GLTFLoader();[[__LINK_ICON]](https://threejs.org/manual/zh/loading-3d-models.html?f_link_type=f_linkinlinenote&flow_extra=eyJpbmxpbmVfZGlzcGxheV9wb3NpdGlvbiI6MCwiZG9jX3Bvc2l0aW9uIjowLCJkb2NfaWQiOiIwNGI1MDViODFiYTZjY2U2LWY0YWVlODQ4OTZlMjA0ZjMifQ%3D%3D "[__LINK_ICON]")
carregador.load(modeloOnibusURL, (gltf) => {
onibus.modelo = gltf.scene;
onibus.modelo.scale.set(1.7, 1.7, 1.7);
onibus.modelo.position.set(0, 0.25, 6);
onibus.modelo.rotation.y = Math.PI;
onibus.modelo.traverse(obj => { if (obj.isMesh) { obj.castShadow = true; obj.receiveShadow = true; } });
cena.add(onibus.modelo);
const formaOnibus = new CANNON.Box(new CANNON.Vec3(3.7, 1.6, 9.2));
onibus.corpo = new CANNON.Body({ mass: 280, shape: formaOnibus, position: new CANNON.Vec3(0, 0.3, 6) });
onibus.corpo.linearDamping = 0.94;
onibus.corpo.angularDamping = 0.94;
mundoFisica.addBody(onibus.corpo);
}, undefined, (erro) => console.error("Erro ao carregar ônibus:", erro));
}
function iniciarFase() {
const dados = fases[faseAtual];
document.getElementById("fase").textContent = faseAtual + 1;
document.getElementById("descricao").textContent = dados.desc;
cena.userData.vagaMesh.position.set(dados.vaga.pos[0], 0.02, dados.vaga.pos[2]);
cena.userData.vagaMesh.rotation.y = dados.vaga.ang;
cena.userData.obstaculos = [];
dados.obstaculos.forEach(def => {
let malha, corpo;
if (def.tipo === "cilindro") {
const geo = new THREE.CylinderGeometry(def.raio, def.raio, def.alt, 24);
const mat = new THREE.MeshStandardMaterial({ color: 0x555555 });
malha = new THREE.Mesh(geo, mat);
malha.position.set(def.pos[0], def.alt/2, def.pos[2]);
malha.castShadow = true; malha.receiveShadow = true;
corpo = new CANNON.Body({ mass: 0, shape: new CANNON.Cylinder(def.raio, def.raio, def.alt, 24) });
corpo.position.set(def.pos[0], def.alt/2, def.pos[2]);
} else {
const geo = new THREE.BoxGeometry(...def.dim);
const mat = new THREE.MeshStandardMaterial({ color: 0x8b5a2b });
malha = new THREE.Mesh(geo, mat);
malha.position.set(def.pos[0], def.dim[1]/2, def.pos[2]);
malha.castShadow = true; malha.receiveShadow = true;
corpo = new CANNON.Body({ mass: 0, shape: new CANNON.Box(new CANNON.Vec3(...def.dim.map(v => v/2))) });
corpo.position.set(def.pos[0], def.dim[1]/2, def.pos[2]);
}
cena.add(malha);
mundoFisica.addBody(corpo);
cena.userData.obstaculos.push({ malha, corpo });
});
}
function mover(direcao) {
if (!onibus.corpo) return;
const sinal = direcao === "frente" ? -1 : 1;
const dir = new THREE.Vector3(0, 0, sinal).applyQuaternion(onibus.modelo.quaternion);
onibus.corpo.velocity.set(dir.x * velocidade, 0, dir.z * velocidade);
}
function virar(lado) {
if (!onibus.corpo) return;
const rot = lado === "dir" ? -rotacao : rotacao;
onibus.corpo.angularVelocity.y = rot;
}
function verificarEstacionamento() {
if (!onibus.modelo) return;
const dados = fases[faseAtual];
const centro = onibus.modelo.position;
const angulo = onibus.modelo.rotation.y;
const dist = Math.sqrt(
Math.pow(centro.x - dados.vaga.pos[0], 2) +
Math.pow(centro.z - dados.vaga.pos[2], 2)
);
const difAng = Math.abs(THREE.MathUtils.euclideanModulo(angulo - dados.vaga.ang, Math.PI * 2));
const bateu = onibus.corpo.collisionResponse === "collide";
if (bateu) {
mostrarMensagem("⚠️ Cuidado! Bateu", "#dc2626");
return;
}
if (dist < dados.vaga.tolDist && difAng < dados.vaga.tolAng) {
mostrarMensagem("✅ Estacionado! Próxima fase", "#16a34a");
setTimeout(() => {
if (faseAtual < fases.length - 1) {
faseAtual++;
reiniciar();
} else {
mostrarMensagem("🏆 Parabéns! Todas concluídas", "#2563eb");
}
}, 2200);
}
}
function mostrarMensagem(texto, cor) {
const el = document.getElementById("msg");
el.textContent = texto;
el.style.color = cor;
el.style.display = "block";
setTimeout(() => el.style.display = "none", 2000);
}
function reiniciar() {
if (onibus.corpo) {
onibus.corpo.position.set(0, 0.3, 6);
onibus.corpo.velocity.set(0, 0, 0);
onibus.corpo.angularVelocity.set(0, 0, 0);
onibus.corpo.quaternion.set(0, 0, 0, 1);
}
cena.userData.obstaculos.forEach(obj => { cena.remove(obj.malha); mundoFisica.removeBody(obj.corpo); });
iniciarFase();
}
function animar() {
requestAnimationFrame(animar);
mundoFisica.step(1/60);
if (onibus.modelo && onibus.corpo) {
onibus.modelo.position.copy(onibus.corpo.position);
onibus.modelo.quaternion.copy(onibus.corpo.quaternion);
}
controles.update();
verificarEstacionamento();
renderizador.render(cena, camera);
}
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderizador.setSize(window.innerWidth, window.innerHeight);
});
iniciarJogo();
</script>
</body>
</html>
Game Source: 🚌 Estacionar Ônibus 3D Real
Creator: DriftPenguin96
Libraries: three
Complexity: complex (273 lines, 12.7 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: estacionar-nibus-3d-real-driftpenguin96" to link back to the original. Then publish at arcadelab.ai/publish.