<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Vale das Cerejeiras · Paisagem 3D</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background-color: #0b1a2a;
color: #e0e9f0;
}
#info {
position: absolute;
top: 16px;
left: 16px;
background: rgba(10, 20, 30, 0.6);
backdrop-filter: blur(8px);
padding: 8px 16px;
border-radius: 30px;
font-size: 14px;
letter-spacing: 1px;
border: 1px solid rgba(255, 255, 255, 0.15);
pointer-events: none;
z-index: 10;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
#controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
background: rgba(10, 20, 30, 0.5);
backdrop-filter: blur(10px);
padding: 10px 18px;
border-radius: 50px;
border: 1px solid rgba(255, 255, 255, 0.15);
z-index: 20;
max-width: 90vw;
box-shadow: 0 8px 20px rgba(0,0,0,0.3);
}
.btn {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #e0e9f0;
padding: 6px 14px;
border-radius: 30px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
backdrop-filter: blur(4px);
font-weight: 400;
letter-spacing: 0.3px;
}
.btn:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.4);
transform: scale(1.02);
}
.btn.active {
background: #d4a0b0;
color: #1a1a2e;
border-color: #e0b0c0;
font-weight: 500;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #ccddee;
font-size: 18px;
background: rgba(0,0,0,0.5);
padding: 16px 32px;
border-radius: 40px;
backdrop-filter: blur(6px);
z-index: 100;
border: 1px solid rgba(255,255,255,0.2);
letter-spacing: 1px;
}
#error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #3a1a2a;
color: #ffb0b0;
padding: 20px 30px;
border-radius: 20px;
display: none;
z-index: 200;
border: 1px solid #ff8080;
text-align: center;
max-width: 80vw;
}
@media (max-width: 600px) {
#controls { bottom: 12px; padding: 8px 12px; gap: 6px; }
.btn { font-size: 11px; padding: 5px 10px; }
#info { font-size: 12px; top: 10px; left: 10px; }
}
</style>
</head>
<body>
<div id="info">🌸 Vale das Cerejeiras · 3D</div>
<div id="controls">
<button class="btn" id="view1">Vista Panorâmica</button>
<button class="btn" id="view2">Margem do Rio</button>
<button class="btn" id="view3">Trilha do Templo</button>
<button class="btn" id="view4">Colina</button>
<span style="width:6px;"></span>
<button class="btn" id="mood1">🌆 Crepúsculo</button>
<button class="btn" id="mood2">🌄 Manhã</button>
<button class="btn" id="mood3">🌧️ Chuva</button>
<span style="width:6px;"></span>
<button class="btn" id="resetView">⟲ Centralizar</button>
<button class="btn" id="toggleUI">Ocultar UI</button>
<button class="btn" id="saveImg">📸 Capturar</button>
</div>
<div id="loading">Carregando paisagem...</div>
<div id="error">⚠️ WebGL não suportado ou erro de renderização.<br>Tente outro navegador.</div>
<!-- Three.js e dependências via CDN -->
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.128.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.128.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
import { FXAAShader } from 'three/addons/shaders/FXAAShader.js';
// --- Verificação WebGL ---
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
if (!gl) {
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'block';
throw new Error('WebGL não disponível');
}
// --- Cena, câmera, renderer ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a2b3c);
scene.fog = new THREE.FogExp2(0x1a2b3c, 0.008);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(18, 12, 22);
camera.lookAt(0, 2, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.1;
document.body.appendChild(renderer.domElement);
// --- Controles orbitais ---
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxPolarAngle = Math.PI / 2.2;
controls.minDistance = 5;
controls.maxDistance = 40;
controls.target.set(0, 3, 0);
// --- Composer com FXAA e Bloom sutil ---
const composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.3, 0.4, 0.85);
composer.addPass(bloomPass);
const effectFXAA = new ShaderPass(FXAAShader);
effectFXAA.uniforms['resolution'].value.set(1 / window.innerWidth, 1 / window.innerHeight);
composer.addPass(effectFXAA);
// --- Luzes ---
const ambient = new THREE.AmbientLight(0x404060, 0.5);
scene.add(ambient);
const moonLight = new THREE.DirectionalLight(0xccddff, 0.8);
moonLight.position.set(-10, 20, 10);
moonLight.castShadow = true;
moonLight.shadow.mapSize.width = 1024;
moonLight.shadow.mapSize.height = 1024;
scene.add(moonLight);
const warmLight = new THREE.PointLight(0xffaa66, 0.6, 30);
warmLight.position.set(2, 2, 3);
scene.add(warmLight);
// --- Grupo principal da paisagem ---
const world = new THREE.Group();
scene.add(world);
// --- Materiais ---
const matGround = new THREE.MeshStandardMaterial({ color: 0x3a4a3a, roughness: 0.9, metalness: 0.1 });
const matRock = new THREE.MeshStandardMaterial({ color: 0x5a5a5a, roughness: 0.8 });
const matWood = new THREE.MeshStandardMaterial({ color: 0x8a5a3a, roughness: 0.7 });
const matRoof = new THREE.MeshStandardMaterial({ color: 0x4a2a2a, roughness: 0.6 });
const matLeaf = new THREE.MeshStandardMaterial({ color: 0xffb0c0, roughness: 0.4, emissive: 0x331020, emissiveIntensity: 0.1 });
const matWater = new THREE.MeshStandardMaterial({ color: 0x2a4a5a, roughness: 0.1, metalness: 0.4, transparent: true, opacity: 0.85 });
const matStone = new THREE.MeshStandardMaterial({ color: 0x7a7a7a, roughness: 0.9 });
// --- Terreno (colinas suaves) ---
const terrainGeo = new THREE.CylinderGeometry(14, 14, 2, 32, 8);
const terrain = new THREE.Mesh(terrainGeo, matGround);
terrain.position.y = -1;
terrain.receiveShadow = true;
world.add(terrain);
// --- Rio (faixa plana) ---
const riverGeo = new THREE.BoxGeometry(2.5, 0.1, 12);
const river = new THREE.Mesh(riverGeo, matWater);
river.position.set(1.5, 0.1, 2);
river.rotation.y = 0.3;
river.receiveShadow = true;
world.add(river);
// --- Ponte vermelha ---
const bridgeGroup = new THREE.Group();
const bridgePlank = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.1, 2.5), new THREE.MeshStandardMaterial({ color: 0xcc4444, roughness: 0.6 }));
bridgePlank.position.set(0, 0.2, 0);
bridgePlank.castShadow = true;
bridgeGroup.add(bridgePlank);
const rail1 = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.4, 2.5), matWood);
rail1.position.set(0.4, 0.5, 0);
rail1.castShadow = true;
bridgeGroup.add(rail1);
const rail2 = rail1.clone();
rail2.position.x = -0.4;
bridgeGroup.add(rail2);
bridgeGroup.position.set(1.5, 0.2, 2);
bridgeGroup.rotation.y = 0.3;
world.add(bridgeGroup);
// --- Casa (chaya) ---
function createHouse(x, z, scale = 1) {
const house = new THREE.Group();
const base = new THREE.Mesh(new THREE.BoxGeometry(1.8*scale, 0.8*scale, 1.8*scale), matWood);
base.position.y = 0.4*scale;
base.castShadow = true;
house.add(base);
const roof = new THREE.Mesh(new THREE.ConeGeometry(1.6*scale, 0.8*scale, 4), matRoof);
roof.position.y = 1.0*scale;
roof.rotation.y = Math.PI/4;
roof.castShadow = true;
house.add(roof);
house.position.set(x, 0.2, z);
return house;
}
world.add(createHouse(-2, 0, 1.0));
world.add(createHouse(-3.5, 2.5, 0.8));
world.add(createHouse(3.8, -1.5, 0.9));
// --- Templo (pagode) ---
const pagoda = new THREE.Group();
const basePag = new THREE.Mesh(new THREE.BoxGeometry(1.6, 0.6, 1.6), matStone);
basePag.position.y = 0.3;
basePag.castShadow = true;
pagoda.add(basePag);
for (let i = 0; i < 3; i++) {
const floor = new THREE.Mesh(new THREE.BoxGeometry(1.2 - i*0.2, 0.4, 1.2 - i*0.2), matWood);
floor.position.y = 0.8 + i*0.6;
floor.castShadow = true;
pagoda.add(floor);
const roof = new THREE.Mesh(new THREE.ConeGeometry(1.0 - i*0.15, 0.5, 4), matRoof);
roof.position.y = 1.2 + i*0.6;
roof.rotation.y = Math.PI/4;
roof.castShadow = true;
pagoda.add(roof);
}
pagoda.position.set(-4, 0.2, -2);
world.add(pagoda);
// --- Árvore de cerejeira (procedural) ---
const treeGroup = new THREE.Group();
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x5a3a2a, roughness: 0.9 });
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.7, 2.5, 6), trunkMat);
trunk.position.y = 1.2;
trunk.castShadow = true;
treeGroup.add(trunk);
// galhos
const branch1 = new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.25, 1.2, 5), trunkMat);
branch1.position.set(0.5, 2.0, 0.2);
branch1.rotation.z = 0.4;
branch1.castShadow = true;
treeGroup.add(branch1);
const branch2 = branch1.clone();
branch2.position.set(-0.5, 2.1, -0.2);
branch2.rotation.z = -0.5;
treeGroup.add(branch2);
// copa (flocos)
const leafMat = new THREE.MeshStandardMaterial({ color: 0xffaacc, roughness: 0.3, emissive: 0x552233, emissiveIntensity: 0.2 });
for (let i = 0; i < 30; i++) {
const size = 0.3 + Math.random()*0.4;
const leaf = new THREE.Mesh(new THREE.SphereGeometry(size, 5, 5), leafMat);
const angle = Math.random() * Math.PI * 2;
const rad = 0.8 + Math.random() * 1.5;
leaf.position.set(Math.cos(angle)*rad, 2.2 + Math.random()*1.2, Math.sin(angle)*rad);
leaf.castShadow = true;
treeGroup.add(leaf);
}
treeGroup.position.set(3.5, 0.2, -2.5);
world.add(treeGroup);
// --- Pedras, lanternas, arbustos ---
const lantern = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.8, 0.4), matStone);
lantern.position.set(0.8, 0.4, -1.2);
lantern.castShadow = true;
world.add(lantern);
const lanternLight = new THREE.PointLight(0xffaa77, 0.5, 4);
lanternLight.position.set(0.8, 1.0, -1.2);
world.add(lanternLight);
for (let i = 0; i < 12; i++) {
const rock = new THREE.Mesh(new THREE.DodecahedronGeometry(0.2 + Math.random()*0.3), matRock);
rock.position.set((Math.random()-0.5)*8, 0.1, (Math.random()-0.5)*8);
rock.rotation.set(Math.random(), Math.random(), Math.random());
rock.castShadow = true;
world.add(rock);
}
// --- Montanhas ao fundo ---
const mountMat = new THREE.MeshStandardMaterial({ color: 0x6a7a8a, roughness: 1.0 });
for (let i = 0; i < 8; i++) {
const mtn = new THREE.Mesh(new THREE.ConeGeometry(2 + Math.random()*3, 3 + Math.random()*3, 5), mountMat);
mtn.position.set((i-4)*4, -0.5, -12 - Math.random()*4);
mtn.rotation.y = Math.random();
mtn.castShadow = false;
world.add(mtn);
}
// --- Partículas: pétalas e luzes ---
const petalGeo = new THREE.BufferGeometry();
const petalCount = 60;
const positions = new Float32Array(petalCount * 3);
for (let i = 0; i < petalCount; i++) {
positions[i*3] = (Math.random()-0.5)*20;
positions[i*3+1] = 1 + Math.random()*6;
positions[i*3+2] = (Math.random()-0.5)*20;
}
petalGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const petalMat = new THREE.PointsMaterial({ color: 0xffc0d0, size: 0.15, transparent: true, opacity: 0.7 });
const petals = new THREE.Points(petalGeo, petalMat);
world.add(petals);
// --- Modos de atmosfera ---
const moods = {
twilight: { bg: 0x1a2b3c, fog: 0x1a2b3c, ambient: 0x404060, moon: 0xccddff, exposure: 1.1 },
morning: { bg: 0x9ac0d0, fog: 0x9ac0d0, ambient: 0xccddee, moon: 0xffeedd, exposure: 1.3 },
rain: { bg: 0x3a4a5a, fog: 0x3a4a5a, ambient: 0x556677, moon: 0x8899aa, exposure: 0.9 }
};
function setMood(mood) {
const m = moods[mood];
scene.background.setHex(m.bg);
scene.fog.color.setHex(m.fog);
ambient.color.setHex(m.ambient);
moonLight.color.setHex(m.moon);
renderer.toneMappingExposure = m.exposure;
// atualizar botões
document.querySelectorAll('.btn[id^="mood"]').forEach(b => b.classList.remove('active'));
document.getElementById('mood' + (mood === 'twilight' ? 1 : mood === 'morning' ? 2 : 3)).classList.add('active');
}
// --- Views predefinidas ---
const views = [
{ pos: [18, 12, 22], target: [0, 2, 0] },
{ pos: [4, 1.5, 8], target: [1.5, 0.5, 2] },
{ pos: [-6, 3, 4], target: [-3, 1, -1] },
{ pos: [10, 15, -8], target: [0, 2, -2] }
];
function setView(index) {
const v = views[index];
camera.position.set(v.pos[0], v.pos[1], v.pos[2]);
controls.target.set(v.target[0], v.target[1], v.target[2]);
controls.update();
}
// --- Eventos UI ---
document.getElementById('view1').addEventListener('click', () => setView(0));
document.getElementById('view2').addEventListener('click', () => setView(1));
document.getElementById('view3').addEventListener('click', () => setView(2));
document.getElementById('view4').addEventListener('click', () => setView(3));
document.getElementById('resetView').addEventListener('click', () => setView(0));
document.getElementById('mood1').addEventListener('click', () => setMood('twilight'));
document.getElementById('mood2').addEventListener('click', () => setMood('morning'));
document.getElementById('mood3').addEventListener('click', () => setMood('rain'));
document.getElementById('toggleUI').addEventListener('click', () => {
const ui = document.getElementById('controls');
ui.style.display = ui.style.display === 'none' ? 'flex' : 'none';
});
document.getElementById('saveImg').addEventListener('click', () => {
renderer.render(scene, camera);
const link = document.createElement('a');
link.download = 'vale_cerejeiras.png';
link.href = renderer.domElement.toDataURL('image/png');
link.click();
});
// --- Animações ---
let time = 0;
function animate() {
requestAnimationFrame(animate);
time += 0.01;
// pétalas flutuantes
petals.rotation.y += 0.0005;
// luzes suaves
lanternLight.intensity = 0.5 + Math.sin(time*2)*0.1;
controls.update();
composer.render();
}
// --- Resize ---
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
effectFXAA.uniforms['resolution'].value.set(1 / window.innerWidth, 1 / window.innerHeight);
});
// --- Inicialização ---
document.getElementById('loading').style.display = 'none';
setMood('twilight');
setView(0);
animate();
// --- Sistema de partículas de chuva (simples) ---
const rainGeo = new THREE.BufferGeometry();
const rainCount = 200;
const rainPos = new Float32Array(rainCount * 3);
for (let i = 0; i < rainCount; i++) {
rainPos[i*3] = (Math.random()-0.5)*30;
rainPos[i*3+1] = Math.random()*10;
rainPos[i*3+2] = (Math.random()-0.5)*30;
}
rainGeo.setAttribute('position', new THREE.BufferAttribute(rainPos, 3));
const rainMat = new THREE.PointsMaterial({ color: 0x88aacc, size: 0.08, transparent: true, opacity: 0.4 });
const rain = new THREE.Points(rainGeo, rainMat);
rain.visible = false;
world.add(rain);
// ativar chuva no modo chuva
const originalMood = setMood;
setMood = function(mood) {
originalMood(mood);
rain.visible = (mood === 'rain');
};
// reaplicar
setMood('twilight');
// --- Preferência de movimento reduzido (não aplicado por simplicidade) ---
// --- Fim ---
</script>
</body>
</html>
Discussão
0 comentários