<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Voxel Japanese Pagoda Garden</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: sans-serif;
}
canvas {
display: block;
}
#info {
position: fixed;
bottom: 16px;
left: 16px;
color: #fff;
background: rgba(0, 0, 0, 0.35);
padding: 8px 14px;
border-radius: 12px;
font-size: 14px;
pointer-events: none;
z-index: 10;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
user-select: none;
}
</style>
</head>
<body>
<div id="info">🌸 Japanese Pagoda Garden — drag to orbit · scroll to zoom</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.Fog(0x87CEEB, 30, 80);
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(14, 10, 16);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 3, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 5;
controls.maxDistance = 35;
controls.maxPolarAngle = Math.PI / 2.1;
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient);
const hemi = new THREE.HemisphereLight(0x87CEEB, 0x6daa3d, 0.6);
scene.add(hemi);
const dirLight = new THREE.DirectionalLight(0xfff5e6, 1.2);
dirLight.position.set(10, 20, 5);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 50;
dirLight.shadow.camera.left = -15;
dirLight.shadow.camera.right = 15;
dirLight.shadow.camera.top = 15;
dirLight.shadow.camera.bottom = -15;
scene.add(dirLight);
const grassMat = new THREE.MeshStandardMaterial({ color: 0x7cb74a });
const stoneMat = new THREE.MeshStandardMaterial({ color: 0x9a9a9a });
const woodMat = new THREE.MeshStandardMaterial({ color: 0x8b3a3a });
const roofMat = new THREE.MeshStandardMaterial({ color: 0x2d2d2d });
const waterMat = new THREE.MeshStandardMaterial({ color: 0x2e8bcc, transparent: true, opacity: 0.8 });
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x6b4226 });
const lanternLightMat = new THREE.MeshStandardMaterial({ color: 0xffdd99, emissive: 0xffaa55, emissiveIntensity: 0.6 });
const ground = new THREE.Mesh(new THREE.PlaneGeometry(40, 40), grassMat);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
function createPagoda() {
const group = new THREE.Group();
const base = new THREE.Mesh(new THREE.BoxGeometry(8, 1.5, 8), stoneMat);
base.position.y = 0.75;
base.castShadow = true;
base.receiveShadow = true;
group.add(base);
for (let i = 0; i < 3; i++) {
const step = new THREE.Mesh(new THREE.BoxGeometry(2.5 - i * 0.4, 0.25, 1.0), stoneMat);
step.position.set(0, 0.125 + i * 0.25, 4.5 + i * 0.5);
step.castShadow = true;
step.receiveShadow = true;
group.add(step);
}
let y = 1.5;
for (let i = 0; i < 5; i++) {
const bodyW = 5.0 - i * 0.6;
const bodyH = 1.8;
const body = new THREE.Mesh(new THREE.BoxGeometry(bodyW, bodyH, bodyW), woodMat);
body.position.y = y + bodyH / 2;
body.castShadow = true;
body.receiveShadow = true;
group.add(body);
const roofW = bodyW + 1.6;
const roofH = 0.5;
const roof = new THREE.Mesh(new THREE.BoxGeometry(roofW, roofH, roofW), roofMat);
roof.position.y = y + bodyH + roofH / 2;
roof.castShadow = true;
roof.receiveShadow = true;
group.add(roof);
const cornerSize = 0.5;
const corners = [[-1, -1], [-1, 1], [1, -1], [1, 1]];
for (const [sx, sz] of corners) {
const corner = new THREE.Mesh(new THREE.BoxGeometry(cornerSize, 0.4, cornerSize), roofMat);
corner.position.set(sx * roofW / 2, roof.position.y + roofH / 2 + 0.2, sz * roofW / 2);
corner.castShadow = true;
group.add(corner);
}
y = roof.position.y + roofH / 2;
}
const spireMat = new THREE.MeshStandardMaterial({ color: 0xffd700, emissive: 0xffaa00, emissiveIntensity: 0.3 });
const spireBase = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.6, 0.6), spireMat);
spireBase.position.y = y + 0.3;
group.add(spireBase);
const spire = new THREE.Mesh(new THREE.BoxGeometry(0.3, 1.8, 0.3), spireMat);
spire.position.y = y + 1.2;
group.add(spire);
const spireTop = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.2, 0.8), spireMat);
spireTop.position.y = y + 2.1;
group.add(spireTop);
return group;
}
scene.add(createPagoda());
function createCherryTree(x, z, scale) {
const group = new THREE.Group();
const trunk = new THREE.Mesh(new THREE.BoxGeometry(0.5 * scale, 1.6 * scale, 0.5 * scale), trunkMat);
trunk.position.y = 0.8 * scale;
trunk.castShadow = true;
group.add(trunk);
const foliage = new THREE.Group();
foliage.position.y = 1.6 * scale;
const pinkMats = [
new THREE.MeshStandardMaterial({ color: 0xffb7c5 }),
new THREE.MeshStandardMaterial({ color: 0xff9bb5 }),
new THREE.MeshStandardMaterial({ color: 0xffc0cb }),
new THREE.MeshStandardMaterial({ color: 0xffa6c9 })
];
for (let i = 0; i < 14; i++) {
const angle = (i / 14) * Math.PI * 2;
const r = 1.0 + Math.random() * 0.8;
const dx = Math.cos(angle) * r;
const dz = Math.sin(angle) * r;
const dy = Math.random() * 1.6;
const cube = new THREE.Mesh(
new THREE.BoxGeometry(0.8 * scale, 0.8 * scale, 0.8 * scale),
pinkMats[Math.floor(Math.random() * pinkMats.length)]
);
cube.position.set(dx, dy, dz);
cube.castShadow = true;
foliage.add(cube);
}
group.add(foliage);
group.position.set(x, 0, z);
return group;
}
scene.add(createCherryTree(4, 4, 1.1));
scene.add(createCherryTree(-5, 3, 0.9));
scene.add(createCherryTree(3, -5, 1.0));
scene.add(createCherryTree(-4, -4, 1.2));
scene.add(createCherryTree(6, -2, 0.8));
scene.add(createCherryTree(-6, -1, 1.0));
function createLantern(x, z) {
const group = new THREE.Group();
const base = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.3, 0.9), stoneMat);
base.position.y = 0.15;
base.castShadow = true;
group.add(base);
const pillar = new THREE.Mesh(new THREE.BoxGeometry(0.3, 1.2, 0.3), stoneMat);
pillar.position.y = 0.9;
pillar.castShadow = true;
group.add(pillar);
const light = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.7, 0.7), lanternLightMat);
light.position.y = 1.85;
light.castShadow = true;
group.add(light);
const roof = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.3, 1.2), roofMat);
roof.position.y = 2.35;
roof.castShadow = true;
group.add(roof);
const top = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.2, 0.4), stoneMat);
top.position.y = 2.6;
top.castShadow = true;
group.add(top);
const glow = new THREE.PointLight(0xffaa55, 0.4, 6);
glow.position.y = 2;
group.add(glow);
group.position.set(x, 0, z);
return group;
}
scene.add(createLantern(2.0, 2.0));
scene.add(createLantern(2.0, 7.8));
scene.add(createLantern(7.8, 2.0));
scene.add(createLantern(9.8, 7.8));
function createPond() {
const group = new THREE.Group();
const water = new THREE.Mesh(new THREE.BoxGeometry(7, 0.15, 5), waterMat);
water.position.set(6, 0.075, 5);
water.receiveShadow = true;
group.add(water);
const stone = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.3, 0.5), stoneMat);
const positions = [];
for (let x = 2.5; x <= 9.5; x += 0.7) {
positions.push([x, 0.25, 2.5], [x, 0.25, 7.5]);
}
for (let z = 3; z <= 7; z += 0.7) {
positions.push([2.5, 0.25, z], [9.5, 0.25, z]);
}
for (const [px, py, pz] of positions) {
const s = stone.clone();
s.position.set(px, py, pz);
s.castShadow = true;
s.receiveShadow = true;
group.add(s);
}
return group;
}
scene.add(createPond());
function createPathStone(x, z) {
const s = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.08, 0.8), stoneMat);
s.position.set(x, 0.04, z);
s.castShadow = true;
s.receiveShadow = true;
scene.add(s);
}
createPathStone(1.0, 4.5);
createPathStone(1.8, 4.8);
createPathStone(2.5, 5.2);
const petals = [];
function createPetals() {
const petalGeo = new THREE.BoxGeometry(0.15, 0.15, 0.15);
const petalMat = new THREE.MeshStandardMaterial({ color: 0xffb7c5 });
for (let i = 0; i < 180; i++) {
const mesh = new THREE.Mesh(petalGeo, petalMat);
mesh.position.set(
(Math.random() - 0.5) * 20,
Math.random() * 8 + 2,
(Math.random() - 0.5) * 20
);
mesh.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
petals.push({
mesh,
speed: 0.5 + Math.random() * 0.8,
phase: Math.random() * Math.PI * 2,
rotSpeed: new THREE.Vector3(
1 + Math.random() * 2,
1 + Math.random() * 2,
1 + Math.random() * 2
)
});
scene.add(mesh);
}
}
createPetals();
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener('resize', onResize);
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = Math.min(clock.getDelta(), 0.05);
const elapsed = clock.getElapsedTime();
for (const petal of petals) {
petal.mesh.position.y -= petal.speed * delta;
petal.mesh.position.x += Math.sin(petal.phase + elapsed) * 0.02 * delta;
petal.mesh.position.z += Math.cos(petal.phase + elapsed * 0.7) * 0.02 * delta;
petal.mesh.rotation.x += petal.rotSpeed.x * delta;
petal.mesh.rotation.y += petal.rotSpeed.y * delta;
petal.mesh.rotation.z += petal.rotSpeed.z * delta;
if (petal.mesh.position.y < 0) {
petal.mesh.position.y = 6 + Math.random() * 4;
petal.mesh.position.x = (Math.random() - 0.5) * 20;
petal.mesh.position.z = (Math.random() - 0.5) * 20;
}
}
controls.update();
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>