Jogos De Bolas Coloridas 360
Como criar jogos de bolas coloridas 360 com Three.js
Eu comecei a mexer com esse tipo de coisa há uns cinco anos, quando ainda era novo em WebGL. A ideia básica é simples: você monta uma cena esférica onde o usuário pode olhar ao redor e interagir com esferas coloridas flutuando no espaço. O problema é que a primeira vez que você tenta fazer isso funcionar direito, gasta uma tarde inteira lidando com problemas de performance e projeção. Vou mostrar como eu faço atualmente, com um setup que roda fluido na maioria dos computadores sem travar.
O que você realmente precisa entender sobre jogos de bolas coloridas 360
Muita gente pensa que criar um ambiente 360° com esferas coloridas envolve apenas colocar objetos na cena e aplicar TexturasEquirectangulares. Na prática, o desafio real está em como você gerencia a interpolação de cores durante o movimento do usuário e como lida com o sistema de partículas quando o número de esferas sobe acima de 50. O engine que recomendo é o Three.js. Ele abstraí boa parte da complexidade do WebGL puro e te deixa focar na lógica do jogo em si. Você vai precisar também de um bundler como Vite para organizar os arquivos, mas isso é padrão hoje em dia. Um detalhe que os tutoriais geralmente ignoram: o mapeamento de cores em ambiente 360° sofre de interferência gamma quando você mistura esferas com emissive materials. Sem corrigir isso, as cores ficam lavadas ou saturadas demais dependendo do ângulo de visão. A solução é aplicar toneMapping (ACES ou Reinhard) no renderer e garantir que todas as cores passem por sRGBEncoding antes de serem exibidas.
Setup inicial do projeto
Crie uma pasta e rode esses comandos: npm create vite@latest jogo-bolas-360 --template vanilla Entre na pasta e instale o Three.js:

cd jogo-bolas-360 && npm install three Agora o arquivo principal. No main.js, a estrutura básica fica assim:
import * as THREE from 'three';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);
A câmera inicia em z=-5, que é a posição padrão para visualizar o centro da cena. O loop de animação é o coração do sistema:
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
Adicionando as esferas coloridas
Para criar várias bolas coloridas, eu uso um loop que gera esferas distribuídas aleatoriamente dentro de um raio específico. Aqui está a função que eu uso:
const ballCount = 80;
const balls = [];
const colors = [0xff4466, 0x44ff88, 0x4488ff, 0xffaa44, 0xaa44ff, 0x44ffdd];
for (let i = 0; i < ballCount; i++) {
const geometry = new THREE.SphereGeometry(0.3, 32, 32);
const material = new THREE.MeshStandardMaterial({
color: colors[Math.floor(Math.random() * colors.length)],
roughness: 0.3,
metalness: 0.1,
emissive: colors[Math.floor(Math.random() * colors.length)],
emissiveIntensity: 0.15
});
const ball = new THREE.Mesh(geometry, material);
ball.position.set(
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 20
);
ball.userData = {
velocity: new THREE.Vector3(
(Math.random() - 0.5) * 0.02,
(Math.random() - 0.5) * 0.02,
(Math.random() - 0.5) * 0.02
),
rotSpeed: Math.random() * 0.02 + 0.005
};
scene.add(ball);
balls.push(ball);
}
Essa abordagem usa geometria de alta resolução (32 segmentos). Para 80 esferas, isso funciona bem, mas se você subir para mais de 200 bolas, precisa reduzir para 16 segmentos ou usar instancing para manter 60fps.

Iluminação é outro ponto onde muita gente erra. Sem luz adequada, as esferas parecem chapadas. Eu adiciono duas luzes:
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 7);
scene.add(directionalLight);
Movimento e colisão
Cada bola tem uma velocidade e uma posição. No loop de animação, você atualiza a posição a cada frame e faz as bolas rebate nas bordas de um box imaginário:
const boundary = 10;
function updateBalls() {
balls.forEach(ball => {
ball.position.add(ball.userData.velocity);
ball.rotation.x += ball.userData.rotSpeed;
ball.rotation.y += ball.userData.rotSpeed;
if (Math.abs(ball.position.x) > boundary) {
ball.userData.velocity.x *= -1;
}
if (Math.abs(ball.position.y) > boundary) {
ball.userData.velocity.y *= -1;
}
if (Math.abs(ball.position.z) > boundary) {
ball.userData.velocity.z *= -1;
}
});
}
Um problema que eu encontrei na prática: quando o usuário gira a câmera rapidamente com o mouse, as bolas parecem oscilar de forma estranha porque o movimento da câmera não é suavizado. A correção é usar um damping no controle de órbita.
Para o controle de câmera, adicione o OrbitControls:
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.rotateSpeed = 0.5;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.5;
O autoRotate é opcional. Se quiser interação pura com o mouse, remova essa linha. Mas em experiências 360°, a rotação automática suave costuma funcionar melhor para imersão.

Interação com clique
Uma funcionalidade comum é clicar numa bola para interagir com ela. Isso requer raycasting:
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
window.addEventListener('click', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(balls);
if (intersects.length > 0) {
const hit = intersects[0].object;
hit.material.emissiveIntensity = 0.8;
setTimeout(() => { hit.material.emissiveIntensity = 0.15; }, 300);
}
});
Quando você clica, a bola pisca aumentando o emissiveIntensity. O setTimeout volta ao valor normal em 300ms. Simples, mas funciona.
Responsividade
Não esqueça de ajustar a câmera e o renderer quando a janela muda de tamanho:
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
Limitações que ninguém conta
Esse tipo de projeto tem um limite prático. Com mais de 150 esferas em resolutions altas (1920x1080), a experiência começa a travar em hardware médio. A causa geralmente não é a geometria, mas sim o overdraw — quando muitas esferas ficam sobrepostas na tela ao mesmo tempo, o GPU gasta tempo pintando pixels que vão ser cobertos depois. Se você precisa de muitos mais objetos, a alternativa é usar instancing com InstancedMesh. Isso reduz drasticamente o número de draw calls e permite centenas de esferas sem perda de performance. No entanto, o InstancedMesh não suporta materiais individualizados facilmente — todas as cópias compartilham o mesmo material, então você perde a variação de cor por bola. Outro problema prático: o controle de auto-rotação pode causar motion sickness em parte dos usuários. Alguns ficam tontos depois de alguns segundos. Sempre inclua uma opção para desligar o auto-rotate e permitir que o usuário controle completamente a câmera.

Deploy
Para rodar localmente: npm run dev O Vite sobe um servidor em localhost na porta 5173 por padrão. Para publicar, você pode usar o Vercel, Netlify, ou GitHub Pages — todos aceitam projetos estáticos gerados pelo Vite sem configuração adicional.
O código completo fica num único arquivo se você não se importar com organização. Para projetos maiores, divida em módulos: cena, bolas, controles, e UI separadamente. A diferença na manutenibilidade é significativa depois de algumas semanas.
Galeria
-
Jogos de Bolas Coloridas no Jogos 360