<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>日式樱花山谷 · 蓝调时刻</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
background-color: #0a0f1a;
}
#info {
position: absolute;
top: 16px;
left: 16px;
color: rgba(255, 255, 255, 0.8);
background: rgba(10, 15, 30, 0.5);
backdrop-filter: blur(6px);
padding: 8px 16px;
border-radius: 40px;
font-size: 14px;
letter-spacing: 1px;
border: 1px solid rgba(255, 255, 255, 0.15);
pointer-events: none;
z-index: 10;
text-shadow: 0 1px 4px rgba(0,0,0,0.3);
}
#controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
background: rgba(10, 15, 30, 0.6);
backdrop-filter: blur(8px);
padding: 10px 18px;
border-radius: 60px;
border: 1px solid rgba(255, 255, 255, 0.15);
z-index: 20;
flex-wrap: wrap;
justify-content: center;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
pointer-events: auto;
}
.btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #f0e6d8;
padding: 6px 14px;
border-radius: 40px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
backdrop-filter: blur(4px);
letter-spacing: 0.5px;
font-weight: 400;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.btn:hover {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 200, 150, 0.5);
color: #fff;
}
.btn.active {
background: rgba(220, 180, 140, 0.3);
border-color: #ffb98a;
color: #fff;
}
#ui-toggle {
position: absolute;
bottom: 20px;
right: 20px;
z-index: 30;
background: rgba(10, 15, 30, 0.5);
backdrop-filter: blur(6px);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 40px;
padding: 6px 12px;
color: #ddd;
font-size: 12px;
cursor: pointer;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #ccc;
font-size: 18px;
background: rgba(0,0,0,0.5);
padding: 12px 24px;
border-radius: 40px;
z-index: 100;
letter-spacing: 2px;
backdrop-filter: blur(4px);
border: 1px solid rgba(255,255,255,0.2);
transition: opacity 0.5s;
}
#error {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
color: #ffaa88;
background: rgba(30, 10, 10, 0.7);
padding: 8px 20px;
border-radius: 30px;
font-size: 14px;
z-index: 200;
display: none;
border: 1px solid #ff9966;
backdrop-filter: blur(4px);
}
@media (max-width: 600px) {
#controls {
bottom: 12px;
padding: 8px 12px;
gap: 6px;
width: 95%;
overflow-x: auto;
justify-content: flex-start;
flex-wrap: nowrap;
scrollbar-width: none;
}
#controls::-webkit-scrollbar {
display: none;
}
.btn {
font-size: 12px;
padding: 5px 10px;
flex-shrink: 0;
}
#info {
font-size: 12px;
top: 10px;
left: 10px;
padding: 4px 12px;
}
}
canvas {
display: block;
}
</style>
</head>
<body>
<div id="info">🌸 日式樱花山谷 · 蓝调时刻</div>
<div id="controls">
<button class="btn" id="view1">🏔️ 山谷全景</button>
<button class="btn" id="view2">🌊 河边低机位</button>
<button class="btn" id="view3">⛩️ 寺庙小径</button>
<button class="btn" id="view4">⛰️ 山坡俯瞰</button>
<button class="btn" id="reset">↺ 重置视角</button>
<button class="btn" id="toggleUI">🙈 隐藏界面</button>
<button class="btn" id="save">📸 保存画面</button>
<button class="btn" id="mood1">🌙 蓝调</button>
<button class="btn" id="mood2">🌅 清晨</button>
<button class="btn" id="mood3">🌧️ 雨中</button>
</div>
<div id="ui-toggle">🙈</div>
<div id="loading">🌸 加载山谷中...</div>
<div id="error"></div>
<!-- 引入 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>
<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(0x0a1220);
scene.fog = new THREE.FogExp2(0x0a1220, 0.006);
// --- 相机 (透视) ---
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(18, 8, 22);
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);
// --- 后期特效: 泛光 + FXAA ---
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);
bloomPass.threshold = 0.6;
bloomPass.strength = 0.5;
bloomPass.radius = 0.2;
composer.addPass(bloomPass);
// FXAA 抗锯齿
const fxaaPass = new ShaderPass(FXAAShader);
fxaaPass.uniforms['resolution'].value.set(1 / window.innerWidth, 1 / window.innerHeight);
composer.addPass(fxaaPass);
// --- 控制器 ---
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);
// --- 光照 ---
// 环境光 (蓝调)
const ambientLight = new THREE.AmbientLight(0x404060, 0.6);
scene.add(ambientLight);
// 主光 (冷月光)
const dirLight = new THREE.DirectionalLight(0xc8d0ff, 0.8);
dirLight.position.set(-10, 20, 10);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 1024;
dirLight.shadow.mapSize.height = 1024;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 50;
dirLight.shadow.camera.left = -20;
dirLight.shadow.camera.right = 20;
dirLight.shadow.camera.top = 20;
dirLight.shadow.camera.bottom = -20;
scene.add(dirLight);
// 暖色补光 (灯笼/窗光)
const warmLight = new THREE.PointLight(0xffaa66, 0.5, 20);
warmLight.position.set(5, 2, 3);
scene.add(warmLight);
// 辅助背光
const backLight = new THREE.PointLight(0x8899ff, 0.3, 30);
backLight.position.set(-5, 5, -10);
scene.add(backLight);
// --- 地面 / 地形 (程序化起伏) ---
const groundGeo = new THREE.PlaneGeometry(60, 60, 64, 64);
groundGeo.rotateX(-Math.PI / 2);
const posAttr = groundGeo.attributes.position;
for (let i = 0; i < posAttr.count; i++) {
const x = posAttr.getX(i);
const z = posAttr.getZ(i);
// 山谷起伏
let y = 0;
y += Math.sin(x * 0.2) * 0.8;
y += Math.cos(z * 0.25) * 0.8;
y += Math.sin(x * 0.5 + z * 0.3) * 0.4;
// 河流区域凹陷
const distToRiver = Math.abs(z - 2 * Math.sin(x * 0.15) - 1);
if (distToRiver < 2.5) {
y -= 1.2 * (1 - distToRiver / 2.5);
}
// 山坡
if (x > 8) y += (x - 8) * 0.3;
if (x < -10) y += (-10 - x) * 0.2;
posAttr.setY(i, y);
}
groundGeo.computeVertexNormals();
const groundMat = new THREE.MeshStandardMaterial({
color: 0x3a4a3a,
roughness: 0.8,
metalness: 0.1,
emissive: new THREE.Color(0x111122)
});
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.receiveShadow = true;
scene.add(ground);
// --- 河流 (半透明平面) ---
const riverGeo = new THREE.PlaneGeometry(4, 30);
riverGeo.rotateX(-Math.PI / 2);
const riverMat = new THREE.MeshStandardMaterial({
color: 0x2a4a5a,
transparent: true,
opacity: 0.7,
roughness: 0.2,
metalness: 0.1,
emissive: new THREE.Color(0x112233)
});
const river = new THREE.Mesh(riverGeo, riverMat);
river.position.set(0, 0.1, 2);
river.rotation.z = 0.2;
river.receiveShadow = true;
scene.add(river);
// --- 红色木桥 ---
const bridgeGroup = new THREE.Group();
const bridgeMat = new THREE.MeshStandardMaterial({ color: 0xaa4444, roughness: 0.7 });
const bridgePlank = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.1, 1.2), bridgeMat);
bridgePlank.position.set(0, 0.2, 0);
bridgePlank.castShadow = true;
bridgeGroup.add(bridgePlank);
for (let i = -2; i <= 2; i++) {
const plank = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.05, 0.8), bridgeMat);
plank.position.set(i * 0.4, 0.35, 0);
plank.castShadow = true;
bridgeGroup.add(plank);
}
// 桥柱
const pillarMat = new THREE.MeshStandardMaterial({ color: 0x8a5a3a });
for (let i = -1; i <= 1; i+=2) {
const pillar = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.8, 0.2), pillarMat);
pillar.position.set(i * 1.2, 0.4, 0.6);
pillar.castShadow = true;
bridgeGroup.add(pillar);
const pillar2 = pillar.clone();
pillar2.position.z = -0.6;
bridgeGroup.add(pillar2);
}
bridgeGroup.position.set(0.5, 0.2, 1.5);
bridgeGroup.rotation.y = 0.3;
scene.add(bridgeGroup);
// --- 樱花树 (程序化) ---
function createSakuraTree(pos, scale = 1) {
const treeGroup = new THREE.Group();
// 树干
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x5a3a2a, roughness: 0.9 });
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.3 * scale, 0.5 * scale, 2.5 * scale, 6), trunkMat);
trunk.position.y = 1.2 * scale;
trunk.castShadow = true;
treeGroup.add(trunk);
// 分支
const branchMat = new THREE.MeshStandardMaterial({ color: 0x4a2a1a });
for (let i = 0; i < 5; i++) {
const branch = new THREE.Mesh(new THREE.CylinderGeometry(0.1 * scale, 0.2 * scale, 1.2 * scale, 4), branchMat);
branch.position.y = 2.0 * scale + i * 0.2;
branch.rotation.z = (i - 2) * 0.3;
branch.rotation.x = (i % 2 === 0 ? 0.2 : -0.2);
branch.castShadow = true;
treeGroup.add(branch);
}
// 花簇 (多个小球)
const flowerMat = new THREE.MeshStandardMaterial({
color: 0xffaacc,
emissive: new THREE.Color(0x552233),
roughness: 0.4,
emissiveIntensity: 0.2
});
for (let i = 0; i < 30; i++) {
const flower = new THREE.Mesh(new THREE.SphereGeometry(0.2 * scale + Math.random()*0.1, 5), flowerMat);
const angle = Math.random() * Math.PI * 2;
const radius = 0.8 * scale + Math.random() * 0.5;
const height = 2.0 * scale + Math.random() * 1.5;
flower.position.set(Math.cos(angle) * radius, height, Math.sin(angle) * radius);
flower.castShadow = true;
treeGroup.add(flower);
}
treeGroup.position.set(pos.x, pos.y, pos.z);
return treeGroup;
}
// 前景大樱花树
const bigTree = createSakuraTree(new THREE.Vector3(-4, 0.2, -2), 1.5);
scene.add(bigTree);
// 中景几棵
scene.add(createSakuraTree(new THREE.Vector3(3, 0.1, 4), 1.0));
scene.add(createSakuraTree(new THREE.Vector3(-2, 0.2, 6), 1.2));
scene.add(createSakuraTree(new THREE.Vector3(6, 0.3, -1), 0.9));
// --- 石灯笼 ---
const stoneMat = new THREE.MeshStandardMaterial({ color: 0x6a6a6a, roughness: 0.8 });
const lanternBase = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.4, 0.5, 6), stoneMat);
lanternBase.position.set(-3, 0.25, 1);
lanternBase.castShadow = true;
scene.add(lanternBase);
const lanternTop = new THREE.Mesh(new THREE.SphereGeometry(0.25, 6), stoneMat);
lanternTop.position.set(-3, 0.7, 1);
lanternTop.castShadow = true;
scene.add(lanternTop);
// 暖光
const lanternLight = new THREE.PointLight(0xffaa66, 0.8, 4);
lanternLight.position.set(-3, 0.8, 1);
scene.add(lanternLight);
// --- 建筑 (茶屋/神社) ---
function createHouse(pos, scale = 1, color = 0x8a6a4a) {
const house = new THREE.Group();
const baseMat = new THREE.MeshStandardMaterial({ color: color, roughness: 0.7 });
const roofMat = new THREE.MeshStandardMaterial({ color: 0x4a3a3a, roughness: 0.6 });
// 主体
const base = new THREE.Mesh(new THREE.BoxGeometry(1.2 * scale, 0.8 * scale, 1.2 * scale), baseMat);
base.position.y = 0.4 * scale;
base.castShadow = true;
house.add(base);
// 屋顶
const roof = new THREE.Mesh(new THREE.ConeGeometry(1.0 * scale, 0.5 * scale, 4), roofMat);
roof.position.y = 1.0 * scale;
roof.rotation.y = Math.PI/4;
roof.castShadow = true;
house.add(roof);
// 窗光
const windowLight = new THREE.PointLight(0xffaa66, 0.4, 3);
windowLight.position.set(0.3 * scale, 0.5 * scale, 0.5 * scale);
house.add(windowLight);
house.position.set(pos.x, pos.y, pos.z);
return house;
}
scene.add(createHouse(new THREE.Vector3(2, 0.2, 3), 1.0));
scene.add(createHouse(new THREE.Vector3(-1.5, 0.3, 5), 0.8, 0x7a5a3a));
scene.add(createHouse(new THREE.Vector3(4, 0.4, -2), 1.2, 0x9a7a5a));
// --- 多层塔 (远景) ---
function createPagoda(pos, scale = 1) {
const pagoda = new THREE.Group();
const mat = new THREE.MeshStandardMaterial({ color: 0x6a4a3a, roughness: 0.7 });
const roofMat = new THREE.MeshStandardMaterial({ color: 0x3a2a2a });
for (let i = 0; i < 3; i++) {
const base = new THREE.Mesh(new THREE.BoxGeometry(0.8 * scale, 0.4 * scale, 0.8 * scale), mat);
base.position.y = i * 0.6 * scale + 0.2 * scale;
base.castShadow = true;
pagoda.add(base);
const roof = new THREE.Mesh(new THREE.ConeGeometry(0.7 * scale, 0.3 * scale, 4), roofMat);
roof.position.y = i * 0.6 * scale + 0.5 * scale;
roof.rotation.y = Math.PI/4;
roof.castShadow = true;
pagoda.add(roof);
}
pagoda.position.set(pos.x, pos.y, pos.z);
return pagoda;
}
scene.add(createPagoda(new THREE.Vector3(8, 0.8, -5), 1.5));
scene.add(createPagoda(new THREE.Vector3(-7, 0.6, -3), 1.2));
// --- 远山/雪山 ---
const mountainMat = new THREE.MeshStandardMaterial({ color: 0x8a9aaa, roughness: 0.9, emissive: new THREE.Color(0x223344) });
const m1 = new THREE.Mesh(new THREE.ConeGeometry(4, 3, 5), mountainMat);
m1.position.set(-12, 1.5, -15);
m1.castShadow = true;
scene.add(m1);
const m2 = new THREE.Mesh(new THREE.ConeGeometry(5, 4, 5), mountainMat);
m2.position.set(10, 2, -18);
m2.castShadow = true;
scene.add(m2);
const m3 = new THREE.Mesh(new THREE.ConeGeometry(3, 2.5, 5), mountainMat);
m3.position.set(0, 1.2, -20);
m3.castShadow = true;
scene.add(m3);
// --- 岩石/草丛 (点缀) ---
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.2 + Math.random()*0.3), rockMat);
rock.position.set(Math.random() * 20 - 10, 0.1, Math.random() * 20 - 10);
rock.castShadow = true;
scene.add(rock);
}
// --- 落花粒子 ---
const petalCount = 80;
const petalGeo = new THREE.SphereGeometry(0.05, 3);
const petalMat = new THREE.MeshStandardMaterial({ color: 0xffaacc, emissive: new THREE.Color(0x552233), emissiveIntensity: 0.3 });
const petals = new THREE.InstancedMesh(petalGeo, petalMat, petalCount);
const dummy = new THREE.Object3D();
for (let i = 0; i < petalCount; i++) {
dummy.position.set(Math.random() * 20 - 10, Math.random() * 5 + 1, Math.random() * 20 - 10);
dummy.scale.set(1, 1, 1);
dummy.updateMatrix();
petals.setMatrixAt(i, dummy.matrix);
}
scene.add(petals);
// --- 萤火虫 (远处) ---
const fireflyMat = new THREE.MeshStandardMaterial({ color: 0xffdd88, emissive: new THREE.Color(0xffaa44), emissiveIntensity: 0.8 });
const fireflies = new THREE.InstancedMesh(new THREE.SphereGeometry(0.06, 4), fireflyMat, 30);
for (let i = 0; i < 30; i++) {
dummy.position.set(Math.random() * 20 - 10, Math.random() * 4 + 0.5, Math.random() * 20 - 10);
dummy.updateMatrix();
fireflies.setMatrixAt(i, dummy.matrix);
}
scene.add(fireflies);
// --- 氛围切换 ---
function setMood(mood) {
if (mood === 'blue') {
scene.background = new THREE.Color(0x0a1220);
scene.fog.color.set(0x0a1220);
dirLight.color.set(0xc8d0ff);
dirLight.intensity = 0.8;
ambientLight.color.set(0x404060);
ambientLight.intensity = 0.6;
bloomPass.strength = 0.5;
} else if (mood === 'morning') {
scene.background = new THREE.Color(0x8a9aaa);
scene.fog.color.set(0x8a9aaa);
dirLight.color.set(0xffccaa);
dirLight.intensity = 1.2;
ambientLight.color.set(0x8899aa);
ambientLight.intensity = 0.8;
bloomPass.strength = 0.3;
} else if (mood === 'rain') {
scene.background = new THREE.Color(0x2a3a4a);
scene.fog.color.set(0x2a3a4a);
dirLight.color.set(0x8899aa);
dirLight.intensity = 0.4;
ambientLight.color.set(0x556677);
ambientLight.intensity = 0.5;
bloomPass.strength = 0.2;
}
}
// --- 镜头预设 ---
const views = {
view1: { pos: new THREE.Vector3(18, 8, 22), target: new THREE.Vector3(0, 2, 0) },
view2: { pos: new THREE.Vector3(2, 1.5, 5), target: new THREE.Vector3(0, 0.5, 2) },
view3: { pos: new THREE.Vector3(-3, 2, 6), target: new THREE.Vector3(0, 1, 3) },
view4: { pos: new THREE.Vector3(10, 12, -5), target: new THREE.Vector3(0, 2, 0) }
};
// --- UI 事件绑定 ---
document.getElementById('view1').addEventListener('click', () => {
camera.position.copy(views.view1.pos);
controls.target.copy(views.view1.target);
});
document.getElementById('view2').addEventListener('click', () => {
camera.position.copy(views.view2.pos);
controls.target.copy(views.view2.target);
});
document.getElementById('view3').addEventListener('click', () => {
camera.position.copy(views.view3.pos);
controls.target.copy(views.view3.target);
});
document.getElementById('view4').addEventListener('click', () => {
camera.position.copy(views.view4.pos);
controls.target.copy(views.view4.target);
});
document.getElementById('reset').addEventListener('click', () => {
camera.position.set(18, 8, 22);
controls.target.set(0, 2, 0);
});
document.getElementById('toggleUI').addEventListener('click', () => {
const controlsEl = document.getElementById('controls');
controlsEl.style.display = controlsEl.style.display === 'none' ? 'flex' : 'none';
});
document.getElementById('save').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('mood1').addEventListener('click', () => setMood('blue'));
document.getElementById('mood2').addEventListener('click', () => setMood('morning'));
document.getElementById('mood3').addEventListener('click', () => setMood('rain'));
// 隐藏加载
setTimeout(() => {
document.getElementById('loading').style.opacity = '0';
setTimeout(() => document.getElementById('loading').style.display = 'none', 500);
}, 1000);
// --- 动画循环 ---
let time = 0;
function animate() {
requestAnimationFrame(animate);
time += 0.01;
// 落花飘动
petals.rotation.y += 0.0005;
// 萤火虫闪烁
fireflies.rotation.y += 0.001;
controls.update();
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.set(1 / window.innerWidth, 1 / window.innerHeight);
});
// 错误处理
window.addEventListener('error', (e) => {
document.getElementById('error').textContent = '渲染错误: ' + e.message;
document.getElementById('error').style.display = 'block';
});
// 移动端提示
if (/Mobi|Android/i.test(navigator.userAgent)) {
document.getElementById('info').textContent = '🌸 樱花山谷 · 拖动/双指缩放';
}
</script>
</body>
</html>
Discusión
0 comentarios