<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>日式樱花山谷 · 蓝调时刻</title>
<!-- 使用固定版本 Three.js 及辅助库 -->
<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>
<style>
body {
margin: 0;
overflow: hidden;
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
background-color: #0a0f1a;
}
#info {
position: absolute;
top: 20px;
left: 20px;
color: rgba(255, 255, 255, 0.8);
background: rgba(10, 20, 30, 0.4);
backdrop-filter: blur(8px);
padding: 12px 20px;
border-radius: 40px;
border-left: 3px solid #ffb7c5;
font-size: 14px;
letter-spacing: 1px;
pointer-events: none;
z-index: 10;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
text-shadow: 0 1px 4px rgba(0,0,0,0.5);
}
#controls {
position: absolute;
bottom: 30px;
left: 0;
right: 0;
display: flex;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
padding: 0 16px;
z-index: 20;
pointer-events: none;
}
.btn-group {
display: flex;
gap: 8px;
background: rgba(20, 30, 40, 0.5);
backdrop-filter: blur(12px);
padding: 8px 12px;
border-radius: 60px;
border: 1px solid rgba(255, 200, 210, 0.2);
pointer-events: auto;
box-shadow: 0 8px 30px rgba(0,0,0,0.3);
flex-wrap: wrap;
justify-content: center;
}
.btn {
background: transparent;
border: none;
color: #f0e6e8;
padding: 8px 16px;
border-radius: 40px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
letter-spacing: 0.5px;
border: 1px solid transparent;
white-space: nowrap;
}
.btn:hover {
background: rgba(255, 180, 200, 0.2);
border-color: rgba(255, 180, 200, 0.4);
}
.btn.active {
background: rgba(255, 170, 190, 0.25);
border-color: #ffb0c0;
color: white;
}
.btn-icon {
font-size: 16px;
margin-right: 4px;
vertical-align: middle;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
background: rgba(0,0,0,0.5);
padding: 16px 32px;
border-radius: 60px;
backdrop-filter: blur(10px);
font-size: 16px;
z-index: 100;
border: 1px solid rgba(255,255,255,0.2);
letter-spacing: 2px;
}
#error {
position: absolute;
top: 20px;
right: 20px;
background: rgba(200, 40, 40, 0.8);
color: white;
padding: 12px 20px;
border-radius: 12px;
display: none;
z-index: 200;
font-size: 14px;
max-width: 300px;
backdrop-filter: blur(8px);
}
@media (max-width: 700px) {
#info { font-size: 12px; top: 12px; left: 12px; padding: 8px 16px; }
.btn { padding: 6px 12px; font-size: 12px; }
.btn-group { gap: 4px; padding: 6px 8px; }
}
</style>
</head>
<body>
<div id="info">🌸 樱花山谷 · 蓝调时刻</div>
<div id="controls">
<div class="btn-group">
<button class="btn" id="view1">🏔️ 全景</button>
<button class="btn" id="view2">🌊 河畔</button>
<button class="btn" id="view3">⛩️ 小径</button>
<button class="btn" id="view4">🌄 俯瞰</button>
</div>
<div class="btn-group">
<button class="btn" id="mood-dusk">🌙 蓝调</button>
<button class="btn" id="mood-morning">☀️ 清晨</button>
<button class="btn" id="mood-rain">🌧️ 雨中</button>
</div>
<div class="btn-group">
<button class="btn" id="reset">⟲ 重置</button>
<button class="btn" id="hideui">🙈 隐藏</button>
<button class="btn" id="screenshot">📸 保存</button>
</div>
</div>
<div id="loading">🌸 加载山谷中...</div>
<div id="error"></div>
<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';
// --- 初始化场景 ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a1420); // 蓝调夜空
// --- 相机 (透视) ---
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(12, 6, 18);
camera.lookAt(0, 2, 0);
// --- 渲染器 ---
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
document.body.appendChild(renderer.domElement);
// --- 后期特效: 抗锯齿 + 泛光 (克制) ---
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.6, 0.85);
bloomPass.threshold = 0.6;
bloomPass.strength = 0.6;
bloomPass.radius = 0.4;
composer.addPass(bloomPass);
// FXAA 抗锯齿
const fxaaPass = new ShaderPass(FXAAShader);
const pixelRatio = renderer.getPixelRatio();
fxaaPass.uniforms['resolution'].value.x = 1 / (window.innerWidth * pixelRatio);
fxaaPass.uniforms['resolution'].value.y = 1 / (window.innerHeight * pixelRatio);
composer.addPass(fxaaPass);
// --- 控制器 ---
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxPolarAngle = Math.PI / 2.1;
controls.minDistance = 4;
controls.maxDistance = 40;
controls.target.set(0, 2.5, 0);
// --- 光照 ---
// 环境光 (蓝调)
const ambientLight = new THREE.AmbientLight(0x404060, 0.6);
scene.add(ambientLight);
// 主光 (月光/冷色)
const moonLight = new THREE.DirectionalLight(0x9bb8ff, 0.8);
moonLight.position.set(-5, 10, 5);
moonLight.castShadow = true;
moonLight.shadow.mapSize.width = 1024;
moonLight.shadow.mapSize.height = 1024;
moonLight.shadow.camera.near = 0.5;
moonLight.shadow.camera.far = 30;
moonLight.shadow.camera.left = -15;
moonLight.shadow.camera.right = 15;
moonLight.shadow.camera.top = 15;
moonLight.shadow.camera.bottom = -15;
scene.add(moonLight);
// 暖色补光 (灯笼/窗)
const warmLight = new THREE.PointLight(0xffaa66, 0.5, 20);
warmLight.position.set(2, 1.5, 3);
scene.add(warmLight);
// 辅助背光
const backLight = new THREE.DirectionalLight(0x4466aa, 0.3);
backLight.position.set(3, 5, -8);
scene.add(backLight);
// --- 雾 (层次) ---
scene.fog = new THREE.FogExp2(0x0a1420, 0.035);
// --- 地面/地形 (程序化起伏) ---
const groundGeo = new THREE.PlaneGeometry(40, 40, 64, 64);
groundGeo.rotateX(-Math.PI / 2);
const pos = groundGeo.attributes.position;
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i);
const z = pos.getZ(i);
// 山谷起伏
let y = 0;
y += Math.sin(x * 0.3) * Math.cos(z * 0.25) * 0.8;
y += Math.sin(x * 0.8) * 0.3 + Math.cos(z * 0.6) * 0.3;
y += Math.exp(-(x*x + z*z) / 40) * 1.5; // 中央隆起
// 河流区域凹陷
const riverDist = Math.abs(z - 1.5 * Math.sin(x * 0.2) - 0.5);
if (riverDist < 1.2) y -= 0.8;
pos.setY(i, y);
}
groundGeo.computeVertexNormals();
const groundMat = new THREE.MeshStandardMaterial({
color: 0x2a3a2a,
roughness: 0.9,
metalness: 0.1,
emissive: 0x111a11,
emissiveIntensity: 0.1
});
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.receiveShadow = true;
scene.add(ground);
// --- 河流 (半透明平面) ---
const riverGeo = new THREE.PlaneGeometry(3.5, 20, 20, 20);
riverGeo.rotateX(-Math.PI / 2);
const riverPos = riverGeo.attributes.position;
for (let i = 0; i < riverPos.count; i++) {
const x = riverPos.getX(i);
const z = riverPos.getZ(i);
riverPos.setY(i, 0.05); // 略高于地面
}
riverGeo.computeVertexNormals();
const riverMat = new THREE.MeshStandardMaterial({
color: 0x3a5a7a,
transparent: true,
opacity: 0.7,
roughness: 0.2,
metalness: 0.4,
emissive: 0x112233,
emissiveIntensity: 0.2
});
const river = new THREE.Mesh(riverGeo, riverMat);
river.position.set(0, 0.1, -2);
river.rotation.y = 0.2;
river.receiveShadow = true;
scene.add(river);
// --- 红色木桥 ---
function createBridge() {
const group = new THREE.Group();
const woodMat = new THREE.MeshStandardMaterial({ color: 0x8a4a3a, roughness: 0.7 });
const railMat = new THREE.MeshStandardMaterial({ color: 0xaa5a4a, roughness: 0.6 });
// 桥面
const deck = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.2, 3.5), woodMat);
deck.position.y = 0.3;
deck.castShadow = true;
deck.receiveShadow = true;
group.add(deck);
// 栏杆
for (let i = -1; i <= 1; i+=2) {
const rail = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.4, 3.2), railMat);
rail.position.set(i * 1.0, 0.7, 0);
rail.castShadow = true;
group.add(rail);
// 立柱
for (let j = -1.5; j <= 1.5; j+=1.5) {
const pillar = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.6, 0.15), railMat);
pillar.position.set(i * 1.0, 0.5, j);
pillar.castShadow = true;
group.add(pillar);
}
}
return group;
}
const bridge = createBridge();
bridge.position.set(0.8, 0.2, -1.2);
bridge.rotation.y = 0.3;
scene.add(bridge);
// --- 樱花树 (程序化) ---
function createSakuraTree(posX, posZ, scale = 1.0) {
const group = new THREE.Group();
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x4a2a2a, roughness: 0.8 });
const leafMat = new THREE.MeshStandardMaterial({ color: 0xffb7c5, emissive: 0x552233, emissiveIntensity: 0.1, roughness: 0.6 });
// 树干 (多个圆柱体)
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.25 * scale, 0.4 * scale, 1.8 * scale, 6), trunkMat);
trunk.position.y = 0.9 * scale;
trunk.castShadow = true;
group.add(trunk);
// 分支
const branch1 = new THREE.Mesh(new THREE.CylinderGeometry(0.12 * scale, 0.2 * scale, 0.9 * scale, 5), trunkMat);
branch1.position.set(0.4 * scale, 1.6 * scale, 0.2 * scale);
branch1.rotation.z = 0.4;
branch1.rotation.x = 0.2;
branch1.castShadow = true;
group.add(branch1);
const branch2 = new THREE.Mesh(new THREE.CylinderGeometry(0.1 * scale, 0.18 * scale, 0.8 * scale, 5), trunkMat);
branch2.position.set(-0.5 * scale, 1.5 * scale, -0.3 * scale);
branch2.rotation.z = -0.5;
branch2.rotation.x = -0.1;
branch2.castShadow = true;
group.add(branch2);
// 花簇 (多个小立方体/球体)
const flowerPositions = [
[0.3, 2.0, 0.2], [-0.2, 2.1, -0.4], [0.6, 1.9, -0.1], [-0.6, 1.8, 0.3],
[0.0, 2.3, 0.5], [0.4, 2.2, -0.5], [-0.4, 2.0, 0.6], [0.1, 1.7, 0.8],
[-0.3, 2.4, 0.0], [0.7, 2.0, 0.4], [-0.7, 1.9, -0.2], [0.2, 2.5, -0.2]
];
flowerPositions.forEach(pos => {
const flower = new THREE.Mesh(new THREE.SphereGeometry(0.18 * scale, 5, 5), leafMat);
flower.position.set(pos[0] * scale, pos[1] * scale, pos[2] * scale);
flower.castShadow = true;
group.add(flower);
});
group.position.set(posX, 0, posZ);
return group;
}
// 前景大树
const bigTree = createSakuraTree(-3.5, 3.5, 1.3);
scene.add(bigTree);
// 中景几棵
scene.add(createSakuraTree(-1.5, 1.8, 0.8));
scene.add(createSakuraTree(2.8, 2.5, 0.9));
scene.add(createSakuraTree(-2.0, -2.5, 0.7));
scene.add(createSakuraTree(3.5, -1.5, 0.8));
// --- 石灯笼 ---
function createLantern(x, z) {
const group = new THREE.Group();
const stoneMat = new THREE.MeshStandardMaterial({ color: 0x6a6a6a, roughness: 0.9 });
const base = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.2, 0.4), stoneMat);
base.position.y = 0.1;
group.add(base);
const pillar = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.6, 0.2), stoneMat);
pillar.position.y = 0.5;
group.add(pillar);
const lightBox = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, 0.3), new THREE.MeshStandardMaterial({ color: 0xffaa66, emissive: 0xff8833, emissiveIntensity: 0.8 }));
lightBox.position.y = 0.9;
group.add(lightBox);
group.position.set(x, 0, z);
return group;
}
scene.add(createLantern(-2.8, 2.2));
scene.add(createLantern(2.5, -0.5));
// --- 建筑 (茶屋/神社) ---
function createHouse(x, z, color = 0x8a5a4a, size = 1.0) {
const group = new THREE.Group();
const wallMat = new THREE.MeshStandardMaterial({ color: 0x6a4a3a, roughness: 0.8 });
const roofMat = new THREE.MeshStandardMaterial({ color: 0x3a2a2a, roughness: 0.9 });
const base = new THREE.Mesh(new THREE.BoxGeometry(1.2 * size, 0.8 * size, 1.2 * size), wallMat);
base.position.y = 0.4 * size;
base.castShadow = true;
group.add(base);
// 屋顶
const roof = new THREE.Mesh(new THREE.ConeGeometry(1.0 * size, 0.5 * size, 4), roofMat);
roof.position.y = 1.0 * size;
roof.rotation.y = Math.PI/4;
roof.castShadow = true;
group.add(roof);
group.position.set(x, 0, z);
return group;
}
scene.add(createHouse(1.8, 0.8, 0x8a5a4a, 0.9));
scene.add(createHouse(-1.2, -1.0, 0x7a5a4a, 0.8));
scene.add(createHouse(0.5, -2.2, 0x9a6a5a, 0.7));
// --- 多层塔 (远景) ---
function createPagoda(x, z) {
const group = new THREE.Group();
const mat = new THREE.MeshStandardMaterial({ color: 0x5a4a3a, roughness: 0.8 });
for (let i = 0; i < 3; i++) {
const layer = new THREE.Mesh(new THREE.BoxGeometry(0.8 - i*0.2, 0.4, 0.8 - i*0.2), mat);
layer.position.y = 0.4 + i * 0.5;
layer.castShadow = true;
group.add(layer);
const roof = new THREE.Mesh(new THREE.ConeGeometry(0.7 - i*0.15, 0.3, 4), mat);
roof.position.y = 0.7 + i * 0.5;
roof.rotation.y = Math.PI/4;
roof.castShadow = true;
group.add(roof);
}
group.position.set(x, 0, z);
return group;
}
scene.add(createPagoda(-4.5, -5.0));
scene.add(createPagoda(5.0, -4.0));
// --- 远山/雪山 ---
function createMountain(x, z, scale) {
const group = new THREE.Group();
const mat = new THREE.MeshStandardMaterial({ color: 0x4a5a7a, roughness: 0.9, emissive: 0x1a2a4a, emissiveIntensity: 0.2 });
const peak = new THREE.Mesh(new THREE.ConeGeometry(1.5 * scale, 1.8 * scale, 5), mat);
peak.position.y = 0.9 * scale;
peak.castShadow = true;
group.add(peak);
const snow = new THREE.Mesh(new THREE.ConeGeometry(0.8 * scale, 0.5 * scale, 5), new THREE.MeshStandardMaterial({ color: 0xccddee, roughness: 0.4 }));
snow.position.y = 1.6 * scale;
snow.castShadow = true;
group.add(snow);
group.position.set(x, 0, z);
return group;
}
scene.add(createMountain(-8, -8, 1.5));
scene.add(createMountain(7, -9, 1.8));
scene.add(createMountain(-6, -10, 1.2));
// --- 岩石/草丛 (点缀) ---
function addDetails() {
const rockMat = new THREE.MeshStandardMaterial({ color: 0x5a5a5a, roughness: 0.9 });
for (let i = 0; i < 20; i++) {
const rock = new THREE.Mesh(new THREE.DodecahedronGeometry(0.1 + Math.random()*0.2), rockMat);
rock.position.set((Math.random()-0.5)*8, 0.1, (Math.random()-0.5)*8);
rock.castShadow = true;
scene.add(rock);
}
// 草丛 (小立方体)
const grassMat = new THREE.MeshStandardMaterial({ color: 0x3a6a3a });
for (let i = 0; i < 40; i++) {
const grass = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.15, 0.08), grassMat);
grass.position.set((Math.random()-0.5)*10, 0.08, (Math.random()-0.5)*10);
grass.castShadow = true;
scene.add(grass);
}
}
addDetails();
// --- 氛围管理 ---
const moods = {
dusk: { bg: 0x0a1420, fog: 0x0a1420, ambient: 0x404060, moon: 0x9bb8ff, exposure: 1.2 },
morning: { bg: 0x9ac0e0, fog: 0x9ac0e0, ambient: 0x8899aa, moon: 0xffccaa, exposure: 1.5 },
rain: { bg: 0x3a4a5a, fog: 0x3a4a5a, ambient: 0x556677, moon: 0x8899aa, exposure: 0.9 }
};
let currentMood = 'dusk';
function setMood(mood) {
currentMood = mood;
const m = moods[mood];
scene.background = new THREE.Color(m.bg);
scene.fog = new THREE.FogExp2(m.fog, 0.035);
ambientLight.color.setHex(m.ambient);
moonLight.color.setHex(m.moon);
renderer.toneMappingExposure = m.exposure;
// 雨: 降低亮度, 增加雾
if (mood === 'rain') {
scene.fog.density = 0.06;
} else {
scene.fog.density = 0.035;
}
}
// --- 视角预设 ---
const views = {
view1: { pos: [12, 6, 18], target: [0, 2, 0] },
view2: { pos: [3, 1.5, 5], target: [0.5, 0.5, -1] },
view3: { pos: [-3, 2, 4], target: [-1, 1, 1] },
view4: { pos: [8, 10, 5], target: [0, 2, -2] }
};
function setView(name) {
const v = views[name];
if (!v) return;
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();
}
// --- UI 绑定 ---
document.getElementById('view1').addEventListener('click', () => setView('view1'));
document.getElementById('view2').addEventListener('click', () => setView('view2'));
document.getElementById('view3').addEventListener('click', () => setView('view3'));
document.getElementById('view4').addEventListener('click', () => setView('view4'));
document.getElementById('mood-dusk').addEventListener('click', () => setMood('dusk'));
document.getElementById('mood-morning').addEventListener('click', () => setMood('morning'));
document.getElementById('mood-rain').addEventListener('click', () => setMood('rain'));
document.getElementById('reset').addEventListener('click', () => setView('view1'));
let uiVisible = true;
document.getElementById('hideui').addEventListener('click', () => {
uiVisible = !uiVisible;
document.getElementById('controls').style.display = uiVisible ? 'flex' : 'none';
document.getElementById('info').style.display = uiVisible ? 'block' : 'none';
});
document.getElementById('screenshot').addEventListener('click', () => {
renderer.render(scene, camera);
const link = document.createElement('a');
link.download = 'sakura-valley.png';
link.href = renderer.domElement.toDataURL('image/png');
link.click();
});
// --- 加载完成 ---
document.getElementById('loading').style.display = 'none';
// --- 动画循环 ---
function animate() {
requestAnimationFrame(animate);
controls.update();
// 轻微动态: 水面波纹 (通过旋转)
river.rotation.y += 0.0005;
composer.render();
}
animate();
// --- 窗口大小自适应 ---
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
fxaaPass.uniforms['resolution'].value.x = 1 / (window.innerWidth * pixelRatio);
fxaaPass.uniforms['resolution'].value.y = 1 / (window.innerHeight * pixelRatio);
});
// --- 错误处理 ---
if (!window.WebGLRenderingContext) {
document.getElementById('error').textContent = 'WebGL 不支持,请更换浏览器';
document.getElementById('error').style.display = 'block';
}
// 初始视角
setView('view1');
setMood('dusk');
// 移动端简单适配
if (/Mobi|Android/i.test(navigator.userAgent)) {
renderer.setPixelRatio(1.2);
}
</script>
</body>
</html>
Discussion
0 commentaires