DeepSeek-Flash, GLM-5.3-Flash, 小米 MiMo-V2.6-Flash & Space Bunny & MiniMax M3.1 Flash 对比
对比主流模型的物理世界理解
社交平台直接发布;GitHub/README 使用 Markdown 或 Badge。
[](/zh-CN/r/O643gCkkMe)
黄色显示思考TPS,绿色显示正文输出TPS;进行中为实时估算,结束后与卡片「输出TPS」一致。速度不代表回答质量。
历史结果 · 2026/9/28 07:53:39
Here's a complete, single-file HTML document that renders an elaborate voxel art scene. It features a five-tiered pagoda, blossoming cherry trees, a lively village, and a reflective pond—all built from colorful voxels in an interactive Three.js environment.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Voxel Pagoda Garden</title>
<style>
html, body { margin:0; padding:0; height:100%; overflow:hidden; background:#0b1220; }
canvas { display:block; }
#hud {
position:fixed; left:18px; bottom:16px; color:#eaf4ff; z-index:10;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size:12px; letter-spacing:.06em; line-height:1.6;
text-shadow: 0 2px 8px rgba(0,0,0,.8);
pointer-events:none; opacity:.85;
}
#hud b { color:#ffd6e6; font-weight:600; letter-spacing:.14em; }
#title {
position:fixed; left:18px; top:14px; color:#fff; z-index:10;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size:13px; letter-spacing:.3em; text-transform:uppercase;
text-shadow: 0 2px 12px rgba(0,0,0,.9); opacity:.75;
pointer-events:none;
}
#loader {
position:fixed; inset:0; display:grid; place-items:center; z-index:50;
background:#0b1220; color:#cfe6ff; transition:opacity .6s ease;
font-family: ui-monospace, Menlo, monospace; letter-spacing:.2em; font-size:13px;
}
#loader.hide { opacity:0; pointer-events:none; }
</style>
</head>
<body>
<div id="title">Voxel Pagoda Garden</div>
<div id="hud">
<b>DRAG</b> orbit · <b>WHEEL</b> zoom · <b>RIGHT-DRAG</b> pan<br>
voxels: <span id="vcount">…</span>
</div>
<div id="loader">BUILDING VOXEL WORLD…</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
/* ==========================================================================
0. MATH HELPERS
========================================================================== */
const clamp = (v,a,b)=> v<a?a:(v>b?b:v);
function smoothstep(e0,e1,x){
const t = clamp((x-e0)/(e1-e0), 0, 1);
return t*t*(3-2*t);
}
function hashSin(x,y){
const n = Math.sin(x*127.1 + y*311.7) * 43758.5453123;
return n - Math.floor(n);
}
function vnoise(x,y){
const xi = Math.floor(x), yi = Math.floor(y);
const xf = x-xi, yf = y-yi;
const u = xf*xf*(3-2*xf), v = yf*yf*(3-2*yf);
const a = hashSin(xi,yi), b = hashSin(xi+1,yi);
const c = hashSin(xi,yi+1), d = hashSin(xi+1,yi+1);
return a*(1-u)*(1-v) + b*u*(1-v) + c*(1-u)*v + d*u*v;
}
function fbm(x,y,oct){
oct = oct || 4;
let s=0, amp=0.5, f=1, norm=0;
for (let i=0;i<oct;i++){ s += amp*vnoise(x*f, y*f); norm += amp; f*=2; amp*=0.5; }
return s/norm;
}
// fast deterministic integer hash -> [0,1)
function ihash(x,y,z){
let h = Math.imul(x|0, 374761393) ^ Math.imul(y|0, 668265263) ^ Math.imul(z|0, 2147483647);
h = Math.imul(h ^ (h >>> 13), 1274126177);
h ^= h >>> 16;
return (h >>> 0) / 4294967296;
}
const rngState = { s: 987654321 };
function rnd(){
rngState.s = (Math.imul(rngState.s, 1664525) + 1013904223) >>> 0;
return rngState.s / 4294967296;
}
function rr(a,b){ return a + rnd()*(b-a); }
function ri(a,b){ return Math.floor(rr(a,b+1)); }
function pickFrom(arr, x, y, z){
return arr[Math.floor(ihash(x,y,z)*arr.length) % arr.length];
}
/* ==========================================================================
1. PALETTE
========================================================================== */
const C = {
grass: [0x5aa832, 0x66b73c, 0x4f9a2c, 0x71c247, 0x60ad36, 0x6bbe40],
grassHi: [0x86c94f, 0x94d45c, 0x7cc047],
dirt: [0x7d5233, 0x8b5e3a, 0x6d4629, 0x875839],
rock: [0x767b82, 0x676c73, 0x828790, 0x6f747c],
rockTop: [0x8d9299, 0x7e838a, 0x999ea6],
sand: [0xdfd0a5, 0xd2c294, 0xe8dcb6, 0xd8c89c],
gravelL: 0xe6dcbf,
gravelD: 0xc9bb98,
snow: [0xeef3f7, 0xdde6ee],
woodDark: 0x53341f,
woodMid: 0x7a4b2a,
woodLight: 0xa97442,
woodPale: 0xc79a63,
woodRed: 0x8d4a2c,
plaster: 0xf0e9da,
plaster2: 0xdfd6c3,
window: 0x2d2b33,
roofTile: [0x35505f, 0x2c4453, 0x3e5c6d, 0x2a4150],
roofEdge: 0x1f333f,
roofRed: [0xa8433a, 0x93382f, 0xb84d43],
thatch: [0xb99a5f, 0xa98b52, 0xc7a86b, 0xb0904e],
vermilion: 0xcf3a2a,
vermilion2: 0xa82d20,
gold: 0xe9b93f,
gold2: 0xc99a26,
cherry: [0xffc3d9, 0xffb0cc, 0xffd7e6, 0xf79dbf, 0xffe3ee, 0xffbdd6],
cherryD: [0xe884ab, 0xd9739c, 0xef93b6],
pine: [0x2c5f36, 0x24512c, 0x35703f, 0x2a5a32],
leaf: [0x4a9b3a, 0x3f8a33, 0x57ab45, 0x46a03a],
leafD: [0x357a2c, 0x2c6b26],
bamboo: [0x87b544, 0x74a238, 0x93c24c],
bambooL: [0x6d9436],
stoneLight: 0xbdb6a8,
stoneMid: 0x968e80,
stoneDark: 0x6e675c,
water: 0x3fa0d2,
waterDeep: 0x2b7ba8,
waterShal: 0x63bde6,
paper: 0xfff0d0,
lantern: 0xff6b4a,
lantern2:0xffd27a,
cloth: [0x2f7fbf, 0xd94f4f, 0xf0c04a, 0x62b36a, 0x9a6bc4, 0xe98a3a, 0xffffff],
skin: [0xf2c8a0, 0xe0b088, 0xd6a074, 0xf7d7b5],
hair: [0x2a1f18, 0x3d2a1a, 0x1a1410, 0x50351f],
crop: [0x7fbf3f, 0x6fae35, 0x8fcf4a],
flower: [0xff6f9c, 0xffd93d, 0xffffff, 0xc47fff, 0xff9a3d, 0x7fd8ff],
koi: [0xff8a3d, 0xff5a2d, 0xffe0c0],
lily: 0x3f8f4a,
lotus: [0xff9ec4, 0xffd0e2]
};
/* ==========================================================================
2. VOXEL STORAGE
========================================================================== */
const VOX = new Map(); // solid voxels
const WAT = new Map(); // water voxels
const GLOW = new Map(); // emissive voxels
function voxKey(x,y,z){ return (x+512) + (z+512)*1024 + (y+512)*1048576; }
function setVox(map,x,y,z,c){ map.set(voxKey(x,y,z), c); }
const _colCache = new Map();
function colorOf(hex){
let c = _colCache.get(hex);
if (!c){ c = new THREE.Color(hex); _colCache.set(hex,c); }
return c;
}
/* ==========================================================================
3. WORLD CONSTANTS + HEIGHTMAP
========================================================================== */
const SEA_LEVEL = -2;
const ISLAND_R = 58;
const PLAZA = { x: 0, z: -8 };
const VILLAGE = { x: 33, z: 8 };
const POND = { x: -30, z: 20, rx: 12, rz: 9 };
const SIZE = 129, OFF = 64;
const Hmap = new Int16Array(SIZE*SIZE);
const TopType = new Uint8Array(SIZE*SIZE); // 0 = natural, 1 = painted
function hi(x,z){ return (x+OFF) + (z+OFF)*SIZE; }
function inside(x,z){ return x>=-64 && x<=64 && z>=-64 && z<=64; }
function terrainHeight(x,z){
const d = Math.hypot(x,z);
const m = smoothstep(ISLAND_R+10, ISLAND_R-14, d);
const n1 = fbm(x*0.045+11.3, z*0.045-7.7, 4);
const n2 = fbm(x*0.130-3.2, z*0.130+5.1, 3);
const rim = smoothstep(12, 46, d);
let h = rim*(5 + n1*17) + (n2-0.5)*1.7;
h = h*m + (1-m)*(-6);
return h;
}
function surfaceHeight(x,z){
let h = terrainHeight(x,z);
// flatten the pagoda plaza
const pd = Math.hypot(x-PLAZA.x, z-PLAZA.z);
const pt = smoothstep(26, 10, pd);
h += (0 - h) * pt * 0.96;
// flatten the village
const vd = Math.hypot(x-VILLAGE.x, z-VILLAGE.z);
const vt = smoothstep(30, 14, vd);
h += (1.1 - h) * vt * 0.92;
// dig the pond
const qd = Math.hypot((x-POND.x)/POND.rx, (z-POND.z)/POND.rz);
if (qd < 1.40){
const t = smoothstep(1.40, 0.50, qd);
h += (-4.6 - h) * t;
}
return h;
}
/* ---------- generate heightmap ---------- */
for (let x=-64;x<=64;x++){
for (let z=-64;z<=64;z++){
Hmap[hi(x,z)] = Math.round(surfaceHeight(x,z));
}
}
/* ---------- fill terrain volume ---------- */
for (let x=-64;x<=64;x++){
for (let z=-64;z<=64;z++){
if (Math.hypot(x,z) > 74) continue;
const h = Hmap[hi(x,z)];
const deep = h < SEA_LEVEL;
const depth = deep ? 2 : 5;
for (let k=0;k<=depth;k++){
const y = h - k;
if (y < -9) break;
let c;
if (k === 0){
if (h <= 0) c = pickFrom(C.sand, x, y, z);
else if (h >= 16) c = pickFrom(C.rock, x, y, z);
else if (h >= 12) c = pickFrom(C.rockTop, x, y, z);
else c = pickFrom(C.grass, x, y, z);
} else if (k <= 2){
c = pickFrom(C.dirt, x, y, z);
} else {
c = pickFrom(C.rock, x, y, z);
}
setVox(VOX, x, y, z, c);
}
}
}
/* ---------- pond water volume ---------- */
for (let x = POND.x-POND.rx-3; x <= POND.x+POND.rx+3; x++){
for (let z = POND.z-POND.rz-3; z <= POND.z+POND.rz+3; z++){
if (!inside(x,z)) continue;
const qd = Math.hypot((x-POND.x)/POND.rx, (z-POND.z)/POND.rz);
if (qd > 1.02) continue;
const g = Hmap[hi(x,z)];
for (let y = g+1; y <= SEA_LEVEL; y++){
setVox(WAT, x, y, z, y <= SEA_LEVEL-1 ? C.waterDeep : C.water);
}
}
}
/* ---------- helper: paint the top terrain voxel ---------- */
function paintGround(x,z,c){
if (!inside(x,z)) return;
const y = Hmap[hi(x,z)];
if (y < 0) return;
setVox(VOX, x, y, z, c);
TopType[hi(x,z)] = 1;
}
/* ==========================================================================
4. GARDEN GROUND : PLAZA, RAKED GRAVEL, PATHS
========================================================================== */
// --- raked gravel plaza ---
for (let x=-30;x<=30;x++){
for (let z=-38;z<=22;z++){
if (!inside(x,z)) continue;
const r = Math.hypot(x-PLAZA.x, z-PLAZA.z);
if (r > 11.6 && r < 25){
const ring = Math.floor(r*0.95) % 2;
let c = ring ? C.gravelL : C.gravelD;
if (ihash(x,1,z) > 0.94) c = C.stoneMid; // pebbles
if (ihash(x,2,z) > 0.985) c = pickFrom(C.grass, x, 0, z); // moss patch
paintGround(x,z,c);
}
}
}
// --- moss ring right around the pagoda platform ---
for (let x=-18;x<=18;x++){
for (let z=-26;z<=10;z++){
const r = Math.hypot(x-PLAZA.x, z-PLAZA.z);
if (r >= 11.6 && r < 14.5 && ihash(x,9,z) > 0.3){
paintGround(x,z, pickFrom(C.grassHi, x, 5, z));
}
}
}
// --- paths ---
function drawPath(pts, radius, colors){
for (let i=0;i<pts.length-1;i++){
const a = pts[i], b = pts[i+1];
const len = Math.hypot(b[0]-a[0], b[1]-a[1]);
const steps = Math.max(1, Math.ceil(len*2));
for (let s=0;s<=steps;s++){
const t = s/steps;
const cx = a[0] + (b[0]-a[0])*t;
const cz = a[1] + (b[1]-a[1])*t;
for (let dx=-radius-1; dx<=radius+1; dx++){
for (let dz=-radius-1; dz<=radius+1; dz++){
if (dx*dx+dz*dz > radius*radius) continue;
const x = Math.round(cx+dx), z = Math.round(cz+dz);
if (!inside(x,z)) continue;
if (Hmap[hi(x,z)] < 0) continue;
paintGround(x,z, pickFrom(colors, x, 4, z));
}
}
}
}
}
const PATH_COLORS = [0xbfb49a, 0xb2a68c, 0xc9bea4, 0xa89d84, 0xd0c5ab];
const STONE_PATH = [0x9f9788, 0x8d8577, 0xb0a898, 0x968e80];
// main approach from the south
drawPath([[0,14],[0,30],[6,38],[16,42],[26,40],[32,32],[34,20],[34,8]], 2.0, PATH_COLORS);
// branch west to the pond
drawPath([[0,20],[-10,24],[-18,26],[-22,24]], 1.4, PATH_COLORS);
// north loop around the pagoda
drawPath([[-14,-26],[-2,-32],[12,-30],[20,-20],[22,-10]], 1.4, PATH_COLORS);
// east branch to the village
drawPath([[20,-6],[28,-2],[30,4]], 1.4, PATH_COLORS);
// small trail to the bamboo grove
drawPath([[-20,-14],[-30,-24],[-40,-32]], 1.2, PATH_COLORS);
// --- stone stepping stones from the plaza to the pagoda stairs ---
for (let i=0;i<6;i++){
const z = 12 + i*2;
const x = ((i%2)?1:-1);
paintGround(x, z, pickFrom(STONE_PATH, x, 6, z));
paintGround(x+1, z, pickFrom(STONE_PATH, x+1, 6, z));
}
/* ==========================================================================
5. THE PAGODA
========================================================================== */
function buildStory(cx, cz, y0, w, tier){
for (let x=-w;x<=w;x++){
for (let z=-w;z<=w;z++){
const ax = Math.abs(x), az = Math.abs(z);
const onX = ax === w, onZ = az === w;
if (!onX && !onZ) continue;
for (let hh=0; hh<3; hh++){
const yy = y0 + hh;
const corner = onX && onZ;
const band = (hh === 0 || hh === 2);
let c;
if (corner || band){
c = C.woodDark;
} else {
const winX = onX && !onZ && az <= 1;
const winZ = onZ && !onX && ax <= 1;
c = (winX || winZ) ? C.window : C.plaster;
}
setVox(VOX, cx+x, yy, cz+z, c);
}
}
}
// ground floor doorway (front = +z)
if (tier === 0){
for (let x=-1;x<=1;x++) for (let hh=0;hh<2;hh++)
setVox(VOX, cx+x, y0+hh, cz+w, C.window);
}
// corner posts slightly darker, plus a vermilion accent on the middle of each face
for (const s of [-1,1]){
setVox(VOX, cx+s*w, y0, cz+w, C.woodMid);
setVox(VOX, cx+s*w, y0, cz-w, C.woodMid);
setVox(VOX, cx+w, y0, cz+s*w, C.woodMid);
setVox(VOX, cx-w, y0, cz+s*w, C.woodMid);
}
}
function buildRoof(cx, cz, ry, w, tier){
const e0 = w + 3;
// bottom slab
for (let x=-e0;x<=e0;x++){
for (let z=-e0;z<=e0;z++){
const edge = Math.max(Math.abs(x), Math.abs(z)) === e0;
setVox(VOX, cx+x, ry, cz+z, edge ? C.roofEdge : pickFrom(C.roofTile, x, ry, z));
}
}
// upper slab
const e1 = w + 1;
for (let x=-e1;x<=e1;x++){
for (let z=-e1;z<=e1;z++){
const edge = Math.max(Math.abs(x), Math.abs(z)) === e1;
setVox(VOX, cx+x, ry+1, cz+z, edge ? C.roofEdge : pickFrom(C.roofTile, x, ry+3, z));
}
}
// upturned corner eaves
for (const sx of [-1,1]){
for (const sz of [-1,1]){
for (let a=0;a<=3;a++){
setVox(VOX, cx+sx*(e0-a), ry+1, cz+sz*e0, C.roofEdge);
setVox(VOX, cx+sx*e0, ry+1, cz+sz*(e0-a), C.roofEdge);
}
setVox(VOX, cx+sx*e0, ry+2, cz+sz*e0, C.gold);
setVox(VOX, cx+sx*(e0-1), ry+2, cz+sz*e0, C.gold2);
setVox(VOX, cx+sx*e0, ry+2, cz+sz*(e0-1), C.gold2);
}
}
// hanging lanterns on the front corners of the lower tiers
if (tier < 2){
for (const sx of [-1,1]){
const lx = cx + sx*e1;
const lz = cz + e1;
setVox(GLOW, lx, ry, lz, C.lantern);
setVox(GLOW, lx, ry-1, lz, C.lantern2);
}
}
}
function buildPagoda(cx, cz){
const gy = 1;
// --- stone platform ---
for (let x=-11;x<=11;x++){
for (let z=-11;z<=11;z++){
const edge = Math.max(Math.abs(x), Math.abs(z));
let c = (edge >= 11) ? C.stoneDark : C.stoneMid;
if (edge < 11 && ihash(x,3,z) > 0.9) c = C.stoneLight;
setVox(VOX, cx+x, gy, cz+z, c);
}
}
for (let x=-9;x<=9;x++){
for (let z=-9;z<=9;z++){
const edge = Math.max(Math.abs(x), Math.abs(z));
let c = (edge >= 9) ? C.stoneMid : C.stoneLight;
if (ihash(x,5,z) > 0.92) c = C.stoneMid;
setVox(VOX, cx+x, gy+1, cz+z, c);
}
}
// --- front stairs ---
for (let s=0;s<2;s++){
const y = 2 - s;
const zz = 10 + s;
for (let x=-4;x<=4;x++){
setVox(VOX, cx+x, y, cz+zz, C.stoneLight);
for (let yy=y-1; yy>=-0; yy--) setVox(VOX, cx+x, yy, cz+zz, C.stoneMid);
}
// side rails
setVox(VOX, cx-5, y, cz+zz, C.stoneDark);
setVox(VOX, cx+5, y, cz+zz, C.stoneDark);
}
// --- five tiers ---
const halfs = [6,5,4,3,2];
let y = 3;
for (let t=0;t<5;t++){
buildStory(cx, cz, y, halfs[t], t);
buildRoof(cx, cz, y+3, halfs[t], t);
y += 5;
}
// --- golden finial (sorin) ---
const fy = y; // 28
for (let k=0;k<8;k++) setVox(VOX, cx, fy+k, cz, C.gold);
for (const k of [1,3,5]){
setVox(VOX, cx-1, fy+k, cz, C.gold2);
setVox(VOX, cx+1, fy+k, cz, C.gold2);
setVox(VOX, cx, fy+k, cz-1, C.gold2);
setVox(VOX, cx, fy+k, cz+1, C.gold2);
}
setVox(VOX, cx, fy+8, cz, C.gold);
setVox(VOX, cx, fy+9, cz, C.gold);
setVox(GLOW, cx, fy+10, cz, C.gold);
}
buildPagoda(PLAZA.x, PLAZA.z);
/* ==========================================================================
6. STONE LANTERNS, TORII, SHRINE BITS
========================================================================== */
function stoneLantern(x, y, z){
for (let dx=-1;dx<=1;dx++) for (let dz=-1;dz<=1;dz++){
if (Math.abs(dx)+Math.abs(dz) === 2) continue;
setVox(VOX, x+dx, y, z+dz, C.stoneDark);
}
setVox(VOX, x, y+1, z, C.stoneMid);
setVox(VOX, x, y+2, z, C.stoneMid);
for (let dx=-1;dx<=1;dx++) for (let dz=-1;dz<=1;dz++){
if (Math.abs(dx)+Math.abs(dz) === 2) continue;
setVox(VOX, x+dx, y+3, z+dz, C.stoneMid);
}
setVox(GLOW, x, y+4, z, C.lantern2);
setVox(GLOW, x-1, y+4, z, C.lantern);
setVox(GLOW, x+1, y+4, z, C.lantern);
setVox(GLOW, x, y+4, z-1, C.lantern);
setVox(GLOW, x, y+4, z+1, C.lantern);
for (let dx=-1;dx<=1;dx++) for (let dz=-1;dz<=1;dz++){
if (Math.abs(dx)===1 && Math.abs(dz)===1) continue;
setVox(VOX, x+dx, y+5, z+dz, C.stoneDark);
}
setVox(VOX, x, y+6, z, C.stoneDark);
setVox(VOX, x, y+7, z, C.stoneLight);
}
function placeLanternOnGround(x,z){
if (!inside(x,z)) return;
const y = Hmap[hi(x,z)] + 1;
stoneLantern(x, y, z);
}
// line the approach
for (let i=0;i<6;i++){
const z = 16 + i*5;
placeLanternOnGround(-4, z);
placeLanternOnGround( 4, z);
}
// around the plaza
for (let a=0;a<10;a++){
const ang = a/10*Math.PI*2;
const x = Math.round(PLAZA.x + Math.cos(ang)*17);
const z = Math.round(PLAZA.z + Math.sin(ang)*17);
placeLanternOnGround(x,z);
}
// pond side
placeLanternOnGround(-18, 24);
placeLanternOnGround(-40, 16);
placeLanternOnGround(-24, 33);
/* ---------- torii gate ---------- */
function torii(x, z, big){
const gy = Hmap[hi(x,z)] + 1;
const H = big ? 9 : 7;
const halfSpan = big ? 6 : 5;
// pillars
for (const s of [-1,1]){
for (let y=0;y<H;y++){
setVox(VOX, x + s*halfSpan, gy+y, z, C.vermilion);
if (y < H-2) setVox(VOX, x + s*halfSpan, gy+y, z+ (s>0?1:-1)*0 , C.vermilion);
}
// base stones
setVox(VOX, x+s*halfSpan, gy-1, z, C.stoneDark);
setVox(VOX, x+s*halfSpan, gy-1, z+1, C.stoneDark);
setVox(VOX, x+s*halfSpan, gy-1, z-1, C.stoneDark);
}
// top beam (kasagi) with upward-curving ends
for (let dx=-halfSpan-2; dx<=halfSpan+2; dx++){
const lift = (Math.abs(dx) >= halfSpan) ? 2 : ((Math.abs(dx) >= halfSpan-1) ? 1 : 0);
setVox(VOX, x+dx, gy+H+lift, z, C.vermilion2);
setVox(VOX, x+dx, gy+H+lift-1, z, C.vermilion);
if (Math.abs(dx) <= halfSpan) setVox(VOX, x+dx, gy+H-2, z, C.vermilion);
}
// second beam (nuki)
for (let dx=-halfSpan-1; dx<=halfSpan+1; dx++){
setVox(VOX, x+dx, gy+H-4, z, C.vermilion);
}
// small plaque
setVox(VOX, x, gy+H-3, z, C.gold);
setVox(VOX, x-1, gy+H-3, z, C.gold2);
setVox(VOX, x+1, gy+H-3, z, C.gold2);
}
torii(0, 14, true);
torii(-24, 31, false);
torii(34, 20, false);
/* ==========================================================================
7. TREES
========================================================================== */
function trunk(x, y, z, h, thick, colors){
for (let i=0;i<h;i++){
for (let dx=0; dx<thick; dx++){
for (let dz=0; dz<thick; dz++){
setVox(VOX, x+dx, y+i, z+dz, pickFrom(colors, x+dx, y+i, z+dz));
}
}
}
}
function blob(cx, cy, cz, rx, ry, rz, colors, density){
for (let i=-rx;i<=rx;i++){
for (let j=-ry;j<=ry;j++){
for (let k=-rz;k<=rz;k++){
const d = (i*i)/(rx*rx) + (j*j)/(ry*ry) + (k*k)/(rz*rz);
if (d > 1.0) continue;
if (d > 0.45 && ihash(cx+i*7+1, cy+j*13+3, cz+k*5+7) > density) continue;
setVox(VOX, cx+i, cy+j, cz+k, pickFrom(colors, cx+i, cy+j, cz+k));
}
}
}
}
function cherryTree(x, z, size){
const y = Hmap[hi(x,z)] + 1;
const h = 5 + Math.round(size*1.6);
const thick = size >= 2 ? 2 : 1;
trunk(x, y, z, h, thick, [C.woodDark, C.woodMid, 0x4a2e1b]);
// branches reaching outward
const branches = 5 + Math.floor(size);
for (let b=0;b<branches;b++){
const ang = (b/branches)*Math.PI*2 + size*0.7;
const len = 2 + Math.round(size*1.3);
let bx = x + (thick>1?0:0), bz = z, by = y + h - 2;
const dirx = Math.cos(ang), dirz = Math.sin(ang);
for (let s=1;s<=len;s++){
bx = Math.round(x + dirx*s*1.1);
bz = Math.round(z + dirz*s*1.1);
by = y + h - 2 + Math.round(s*0.55);
setVox(VOX, bx, by, bz, C.woodMid);
setVox(VOX, bx, by+1, bz, C.woodDark);
}
// canopy at branch tip
blob(bx, by+2, bz, 3+Math.round(size*0.5), 2+Math.round(size*0.4), 3+Math.round(size*0.5),
ihash(b,1,2) > 0.5 ? C.cherry : C.cherryD, 0.78);
}
// main canopy
blob(x, y+h+1, z, 4+Math.round(size*1.2), 3+Math.round(size*0.7), 4+Math.round(size*1.2),
C.cherry, 0.8);
// deep shading underneath
blob(x, y+h-1, z, 3+Math.round(size), 1, 3+Math.round(size), C.cherryD, 0.55);
}
function pineTree(x, z, size){
const y = Hmap[hi(x,z)] + 1;
const h = 4 + Math.round(size*2);
trunk(x, y, z, h, 1, [C.woodDark, 0x4a2e1b]);
const layers = 4 + Math.floor(size);
for (let l=0;l<layers;l++){
const t = l/(layers-1);
const r = Math.max(1, Math.round((1-t)*(2.6+size*1.5)));
const ly = y + 2 + Math.round(t*(h-1));
for (let i=-r;i<=r;i++){
for (let k=-r;k<=r;k++){
const d = Math.hypot(i,k);
if (d > r + 0.35) continue;
if (d > r - 0.4 && ihash(x+i, ly, z+k) > 0.7) continue;
setVox(VOX, x+i, ly, z+k, pickFrom(C.pine, x+i, ly, z+k));
}
}
}
setVox(VOX, x, y+h+1, z, C.pine[2]);
setVox(VOX, x, y+h+2, z, C.pine[1]);
}
function leafTree(x, z, size){
const y = Hmap[hi(x,z)] + 1;
const h = 3 + Math.round(size*1.8);
trunk(x, y, z, h, size>=2?2:1, [C.woodDark, C.woodMid]);
blob(x, y+h+1, z, 3+Math.round(size*1.1), 3+Math.round(size*0.8), 3+Math.round(size*1.1),
C.leaf, 0.75);
blob(x, y+h, z, 2+Math.round(size*0.8), 1, 2+Math.round(size*0.8), C.leafD, 0.6);
}
function bambooStalk(x, z, h){
const y = Hmap[hi(x,z)] + 1;
for (let i=0;i<h;i++){
const c = (i % 4 === 3) ? C.bambooL : pickFrom(C.bamboo, x, i, z);
setVox(VOX, x, y+i, z, c);
}
const top = y + h;
for (let a=0;a<5;a++){
const ang = a*1.3 + x*0.4;
const dx = Math.round(Math.cos(ang)*2);
const dz = Math.round(Math.sin(ang)*2);
setVox(VOX, x+dx, top-1, z+dz, C.bamboo[1]);
setVox(VOX, x+dx, top, z+dz, C.bamboo[0]);
setVox(VOX, x+Math.round(dx*1.6), top, z+Math.round(dz*1.6), C.bamboo[2]);
}
}
/* --- placement --- */
function freeSpot(x, z, pad){
pad = pad || 0;
if (!inside(x,z)) return false;
if (Math.hypot(x-PLAZA.x, z-PLAZA.z) < 25 + pad) return false;
if (Math.hypot(x-VILLAGE.x, z-VILLAGE.z) < 17 + pad) return false;
const qd = Math.hypot((x-POND.x)/POND.rx, (z-POND.z)/POND.rz);
if (qd < 1.28) return false;
const h = Hmap[hi(x,z)];
if (h < 1 || h > 15) return false;
return true;
}
const CHERRY_SPOTS = [
[-26,-28,2], [24,-30,2], [-32,-8,1.5], [30,-12,1.5],
[-12,26,2], [12,26,1.5], [-44,2,2], [40,-14,1.5],
[-42,-34,2], [34,-36,2], [-6,38,2], [52,20,1.5],
[6,-44,2], [-52,18,1.5], [16,-38,1], [-36,28,1.5],
[46,30,1.5], [18,34,1], [-8,-18,1.2], [8,-20,1.2]
];
for (const s of CHERRY_SPOTS){
if (freeSpot(s[0], s[1], 1)) cherryTree(s[0], s[1], s[2]);
}
// scattered pines on the hills
for (let i=0;i<90;i++){
const ang = rnd()*Math.PI*2;
const rad = 40 + rnd()*18;
const x = Math.round(Math.cos(ang)*rad);
const z = Math.round(Math.sin(ang)*rad);
if (!freeSpot(x, z, 1)) continue;
const h = Hmap[hi(x,z)];
if (h < 4) continue;
if (rnd() > 0.55) pineTree(x, z, rr(0.6, 1.6));
else leafTree(x, z, rr(0.6, 1.4));
}
// mid-ring decorative trees
for (let i=0;i<90;i++){
const ang = rnd()*Math.PI*2;
const rad = 26 + rnd()*16;
const x = Math.round(Math.cos(ang)*rad);
const z = Math.round(Math.sin(ang)*rad);
if (!freeSpot(x, z, 2)) continue;
const r = rnd();
if (r < 0.35) cherryTree(x, z, rr(0.8, 1.6));
else if (r < 0.62) leafTree(x, z, rr(0.7, 1.4));
else if (r < 0.82) pineTree(x, z, rr(0.7, 1.3));
else { /* leave gaps */ }
}
// bamboo grove
for (let i=0;i<70;i++){
const x = Math.round(-46 + rr(-7, 7));
const z = Math.round(-34 + rr(-7, 7));
if (!inside(x,z)) continue;
const h = Hmap[hi(x,z)];
if (h < 1 || h > 14) continue;
bambooStalk(x, z, ri(8, 16));
}
/* ==========================================================================
8. THE POND DETAIL (lilies, koi, boat, stones)
========================================================================== */
for (let i=0;i<70;i++){
const ang = rnd()*Math.PI*2;
const rad = Math.sqrt(rnd())*0.85;
const x = Math.round(POND.x + Math.cos(ang)*rad*POND.rx);
const z = Math.round(POND.z + Math.sin(ang)*rad*POND.rz);
if (!inside(x,z)) continue;
if (Hmap[hi(x,z)] > SEA_LEVEL-1) continue;
// lily pad
setVox(VOX, x, SEA_LEVEL+1, z, C.lily);
if (ihash(x,1,z) > 0.75){
setVox(VOX, x, SEA_LEVEL+2, z, pickFrom(C.lotus, x, 2, z));
setVox(GLOW, x, SEA_LEVEL+3, z, C.lotus[0]);
}
}
// koi fish just under the surface
for (let i=0;i<26;i++){
const ang = rnd()*Math.PI*2;
const rad = Math.sqrt(rnd())*0.7;
const x = Math.round(POND.x + Math.cos(ang)*rad*POND.rx);
const z = Math.round(POND.z + Math.sin(ang)*rad*POND.rz);
if (!inside(x,z)) continue;
const c = pickFrom(C.koi, x, 3, z);
setVox(VOX, x, SEA_LEVEL, z, c);
setVox(VOX, x+1, SEA_LEVEL, z, c);
}
// stepping stones across the pond
for (let i=0;i<9;i++){
const t = i/8;
const x = Math.round(POND.x - POND.rx*0.95 + t*POND.rx*1.9);
const z = Math.round(POND.z + Math.sin(t*Math.PI)*3.0 - 1.5);
if (!inside(x,z)) continue;
setVox(VOX, x, SEA_LEVEL+1, z, C.stoneMid);
setVox(VOX, x+1, SEA_LEVEL+1, z, C.stoneLight);
}
// little wooden boat
(function boat(){
const bx = POND.x + 5, bz = POND.z - 5;
for (let i=-3;i<=3;i++){
setVox(VOX, bx+i, SEA_LEVEL+1, bz-1, C.woodMid);
setVox(VOX, bx+i, SEA_LEVEL+1, bz+1, C.woodMid);
}
setVox(VOX, bx-3, SEA_LEVEL+2, bz-1, C.woodLight);
setVox(VOX, bx-3, SEA_LEVEL+2, bz+1, C.woodLight);
setVox(VOX, bx+3, SEA_LEVEL+2, bz-1, C.woodLight);
setVox(VOX, bx+3, SEA_LEVEL+2, bz+1, C.woodLight);
setVox(VOX, bx, SEA_LEVEL+1, bz, C.woodDark);
// mast + lantern
setVox(VOX, bx, SEA_LEVEL+2, bz, C.woodDark);
setVox(VOX, bx, SEA_LEVEL+3, bz, C.woodDark);
setVox(GLOW, bx, SEA_LEVEL+4, bz, C.lantern);
})();
/* ==========================================================================
9. VILLAGE
========================================================================== */
function house(cx, cz, w, d, hh, roofColors, wallColor, rot){
rot = rot || 0;
const gy = Hmap[hi(cx,cz)] + 1;
const T = (x,z)=>{
if (rot === 1) return [ z, -x];
if (rot === 2) return [-x, -z];
if (rot === 3) return [-z, x];
return [x, z];
};
// floor
for (let x=-w;x<=w;x++) for (let z=-d;z<=d;z++){
const p = T(x,z);
setVox(VOX, cx+p[0], gy-0, cz+p[1], C.woodMid);
}
// walls
for (let x=-w;x<=w;x++){
for (let z=-d;z<=d;z++){
const ax = Math.abs(x), az = Math.abs(z);
const onX = ax === w, onZ = az === d;
if (!onX && !onZ) continue;
for (let y=0;y<hh;y++){
const yy = gy + y;
const corner = onX && onZ;
let c;
if (corner) c = C.woodDark;
else if (y === 0) c = C.stoneMid;
else {
const win = (y === 1) && ((onZ && !onX && ax <= 1) || (onX && !onZ && az <= 1));
if (win) c = (ihash(x,y,z) > 0.5) ? C.paper : C.window;
else c = (ihash(x, y*3, z) > 0.88) ? C.plaster2 : wallColor;
}
// door on +z face
if (onZ && az === d && ax <= 0 && y < 2) c = C.window;
const p = T(x,z);
setVox(VOX, cx+p[0], yy, cz+p[1], c);
}
}
}
// gable roof
const rh = gy + hh;
const levels = w + 2;
for (let k=0;k<levels;k++){
const e = (w+1) - k;
if (e < 0) break;
for (let x=-e;x<=e;x++){
for (let z=-d-1;z<=d+1;z++){
let c;
if (x === 0) c = C.roofEdge;
else if (Math.abs(x) === e) c = C.roofEdge;
else c = pickFrom(roofColors, x, rh+k, z);
const p = T(x,z);
setVox(VOX, cx+p[0], rh+k, cz+p[1], c);
}
}
}
// chimney
if (ihash(cx, 17, cz) > 0.5){
const p = T(w-1, -d+1);
for (let y=0;y<2;y++) setVox(VOX, cx+p[0], rh+levels+y, cz+p[1], C.stoneDark);
setVox(VOX, cx+p[0], rh+levels+2, cz+p[1], C.stoneMid);
}
}
const houseDefs = [
[21, 2, 4, 3, 3, C.roofTile, C.plaster, 0],
[33, -2, 5, 4, 4, C.thatch, C.plaster, 0],
[44, 4, 4, 3, 3, C.roofTile, C.plaster2, 1],
[20, 14, 5, 3, 3, C.thatch, C.plaster, 0],
[44, 16, 4, 4, 4, C.roofTile, C.plaster, 2],
[33, 18, 3, 3, 2, C.thatch, C.plaster2, 0],
[24, -12, 4, 3, 3, C.roofRed, C.plaster, 1],
[43, -8, 3, 3, 2, C.thatch, C.plaster, 0]
];
for (const h of houseDefs) house(h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]);
/* ---------- village square: well, market stalls, fences ---------- */
(function well(){
const x = VILLAGE.x, z = VILLAGE.z;
const gy = Hmap[hi(x,z)] + 1;
for (let dx=-2;dx<=2;dx++) for (let dz=-2;dz<=2;dz++){
const d = Math.hypot(dx,dz);
if (d > 2.2) continue;
if (d < 1.1) { setVox(WAT, x+dx, gy, z+dz, C.waterDeep); continue; }
setVox(VOX, x+dx, gy, z+dz, (Math.abs(dx)===2||Math.abs(dz)===2) ? C.stoneDark : C.stoneMid);
setVox(VOX, x+dx, gy+1, z+dz, C.stoneMid);
}
for (const s of [[-2,-2],[2,-2],[-2,2],[2,2]]){
for (let y=2;y<5;y++) setVox(VOX, x+s[0], gy+y, z+s[1], C.woodMid);
}
for (let dx=-2;dx<=2;dx++) for (let dz=-2;dz<=2;dz++){
if (Math.abs(dx)===2 || Math.abs(dz)===2 || (Math.abs(dx)<=1 && Math.abs(dz)<=1))
setVox(VOX, x+dx, gy+5, z+dz, C.thatch[0]);
}
setVox(VOX, x, gy+6, z, C.woodDark);
})();
function marketStall(x, z, rot){
const gy = Hmap[hi(x,z)] + 1;
const T = (a,b)=> rot ? [b,-a] : [a,b];
// posts
for (const p of [[-2,-1],[2,-1],[-2,1],[2,1]]){
const q = T(p[0],p[1]);
for (let y=0;y<4;y++) setVox(VOX, x+q[0], gy+y, z+q[1], C.woodMid);
}
// counter
for (let a=-2;a<=2;a++){
const q = T(a,1);
setVox(VOX, x+q[0], gy+1, z+q[1], C.woodLight);
}
// awning
const col = pickFrom(C.cloth, x, 5, z);
for (let a=-2;a<=2;a++){
for (let b=-1;b<=1;b++){
const q = T(a,b);
setVox(VOX, x+q[0], gy+4, z+q[1], (a+b)%2===0 ? col : 0xffffff);
}
}
// goods
setVox(VOX, x + (rot?1:0), gy+2, z + (rot?0:1), C.lantern);
setVox(VOX, x + (rot?-1:0), gy+2, z + (rot?0:-1), C.gold);
}
marketStall(27, 4, false);
marketStall(39, 13, true);
marketStall(30, 14, false);
/* ---------- fences around crop fields ---------- */
function fenceLine(x0,z0,x1,z1){
const len = Math.max(Math.abs(x1-x0), Math.abs(z1-z0));
for (let i=0;i<=len;i++){
const t = len ? i/len : 0;
const x = Math.round(x0 + (x1-x0)*t);
const z = Math.round(z0 + (z1-z0)*t);
if (!inside(x,z)) continue;
const gy = Hmap[hi(x,z)] + 1;
setVox(VOX, x, gy, z, C.woodDark);
setVox(VOX, x, gy+1, z, C.woodMid);
if (i % 2 === 0) setVox(VOX, x, gy+2, z, C.woodPale);
}
}
// crop field south-east of the village
const FX = 40, FZ = 30;
for (let x=-6;x<=6;x++){
for (let z=-5;z<=5;z++){
const gy = Hmap[hi(FX+x, FZ+z)];
const row = Math.abs(z) % 3 === 0;
const c = row ? C.dirt[0] : pickFrom(C.crop, FX+x, gy, FZ+z);
paintGround(FX+x, FZ+z, c);
if (!row && ihash(FX+x, 1, FZ+z) > 0.6){
setVox(VOX, FX+x, gy+1, FZ+z, pickFrom(C.crop, FX+x, 2, FZ+z));
if (ihash(FX+x, 3, FZ+z) > 0.85)
setVox(GLOW, FX+x, gy+2, FZ+z, pickFrom(C.flower, FX+x, 4, FZ+z));
}
}
}
fenceLine(FX-7, FZ-6, FX+7, FZ-6);
fenceLine(FX-7, FZ+6, FX+7, FZ+6);
fenceLine(FX-7, FZ-6, FX-7, FZ+6);
fenceLine(FX+7, FZ-6, FX+7, FZ+6);
/* ---------- red paper lanterns on posts along the village path ---------- */
function lanternPost(x, z){
const gy = Hmap[hi(x,z)] + 1;
setVox(VOX, x, gy, z, C.woodDark);
setVox(VOX, x, gy+1, z, C.woodDark);
setVox(VOX, x, gy+2, z, C.woodDark);
setVox(VOX, x, gy+3, z, C.woodMid);
setVox(GLOW, x+1, gy+3, z, C.lantern);
setVox(GLOW, x-1, gy+3, z, C.lantern);
setVox(GLOW, x, gy+3, z+1, C.lantern);
setVox(GLOW, x, gy+4, z, C.lantern2);
}
for (let i=0;i<7;i++){
const t = i/6;
const x = Math.round(6 + (34-6)*t);
const z = Math.round(34 + (18-34)*t + Math.sin(t*3)*3);
lanternPost(x+4, z);
lanternPost(x-4, z+2);
}
/* ==========================================================================
10. PEOPLE
========================================================================== */
function person(x, y, z, shirt, pants, skin, hair, hat){
setVox(VOX, x, y, z, pants);
setVox(VOX, x, y+1, z, shirt);
setVox(VOX, x, y+2, z, skin);
setVox(VOX, x, y+3, z, hat ? hat : hair);
if (ihash(x,1,z) > 0.55){
setVox(VOX, x+1, y+1, z, shirt);
setVox(VOX, x+1, y+2, z, skin);
} else {
setVox(VOX, x-1, y+1, z, shirt);
setVox(VOX, x-1, y+2, z, skin);
}
}
function personAt(x, z, opts){
if (!inside(x,z)) return;
const gy = Hmap[hi(x,z)] + 1;
const shirt = opts && opts.shirt ? opts.shirt : pickFrom(C.cloth, x, 1, z);
const pants = opts && opts.pants ? opts.pants : [0x3a4a6b, 0x4a3a2a, 0x2f2f38, 0x6b4a3a][Math.floor(ihash(x,2,z)*4)%4];
const skin = pickFrom(C.skin, x, 3, z);
const hair = pickFrom(C.hair, x, 4, z);
const hat = (ihash(x,5,z) > 0.55) ? C.woodPale : null;
person(x, gy, z, shirt, pants, skin, hair, hat);
}
// villagers along the main path and in the village
const peopleSpots = [
[2,22],[ -1,26],[4,34],[9,38],[15,41],[21,41],[27,37],[31,30],[33,24],
[26,6],[30,10],[36,6],[38,12],[29,16],[35,2],[42,8],[23,10],[46,12],
[-10,22],[-16,25],[-21,23],[-27,26],
[-3,-24],[7,-27],[14,-24],[20,-16],[22,-8],
[40,30],[44,32],[38,34]
];
for (const p of peopleSpots) personAt(p[0], p[1]);
/* ==========================================================================
11. FLOWERS SCATTERED ON GRASS
========================================================================== */
for (let x=-64;x<=64;x++){
for (let z=-64;z<=64;z++){
if (!inside(x,z)) continue;
const id = hi(x,z);
if (TopType[id] !== 0) continue;
const h = Hmap[id];
if (h < 1 || h > 13) continue;
if (ihash(x, 77, z) > 0.9915){
setVox(VOX, x, h+1, z, pickFrom(C.flower, x, 9, z));
} else if (ihash(x, 91, z) > 0.988){
setVox(VOX, x, h+1, z, pickFrom(C.grassHi, x, 11, z));
}
}
}
/* ==========================================================================
12. THREE.JS SCENE
========================================================================== */
const renderer = new THREE.WebGLRenderer({ antialias:true, powerPreference:'high-performance' });
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;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.06;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0xcfe3f4, 190, 470);
const camera = new THREE.PerspectiveCamera(46, window.innerWidth/window.innerHeight, 0.5, 4000);
camera.position.set(96, 78, 122);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 12, -2);
controls.enableDamping = true;
controls.dampingFactor = 0.055;
controls.minDistance = 32;
controls.maxDistance = 300;
controls.maxPolarAngle = Math.PI * 0.487;
controls.minPolarAngle = 0.10;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.28;
controls.update();
/* ---------- sky dome ---------- */
(function sky(){
const geo = new THREE.SphereGeometry(1400, 40, 26);
const pos = geo.attributes.position;
const cols = new Float32Array(pos.count*3);
const cTop = new THREE.Color(0x2b6cb8);
const cMid = new THREE.Color(0x8fc4ea);
const cBot = new THREE.Color(0xe6f2fb);
const tmp = new THREE.Color();
for (let i=0;i<pos.count;i++){
const t = clamp(pos.getY(i)/1400 * 0.5 + 0.5, 0, 1);
if (t < 0.5) tmp.copy(cBot).lerp(cMid, t*2);
else tmp.copy(cMid).lerp(cTop, (t-0.5)*2);
cols[i*3] = tmp.r; cols[i*3+1] = tmp.g; cols[i*3+2] = tmp.b;
}
geo.setAttribute('color', new THREE.BufferAttribute(cols, 3));
const mat = new THREE.MeshBasicMaterial({ vertexColors:true, side:THREE.BackSide, fog:false, depthWrite:false });
const m = new THREE.Mesh(geo, mat);
m.renderOrder = -1000;
scene.add(m);
})();
/* ---------- lights ---------- */
scene.add(new THREE.HemisphereLight(0xcfe6ff, 0x5a7a44, 1.05));
scene.add(new THREE.AmbientLight(0xffffff, 0.18));
const sun = new THREE.DirectionalLight(0xfff0cf, 2.35);
sun.position.set(102, 152, 74);
sun.castShadow = true;
sun.shadow.mapSize.set(4096, 4096);
sun.shadow.camera.left = -92;
sun.shadow.camera.right = 92;
sun.shadow.camera.top = 92;
sun.shadow.camera.bottom = -92;
sun.shadow.camera.near = 40;
sun.shadow.camera.far = 340;
sun.shadow.bias = -0.0004;
sun.shadow.normalBias = 0.09;
scene.add(sun);
scene.add(sun.target);
sun.target.position.set(0, 0, 0);
// soft bounce light from the water
const bounce = new THREE.DirectionalLight(0x9fd0ff, 0.32);
bounce.position.set(-80, 30, -60);
scene.add(bounce);
/* ---------- ocean plane ---------- */
(function ocean(){
const geo = new THREE.RingGeometry(48, 900, 128, 4);
geo.rotateX(-Math.PI/2);
const mat = new THREE.MeshStandardMaterial({
color: 0x2b87bd,
transparent: true,
opacity: 0.88,
roughness: 0.12,
metalness: 0.15,
side: THREE.DoubleSide
});
const m = new THREE.Mesh(geo, mat);
m.position.y = SEA_LEVEL - 0.42;
m.receiveShadow = false;
scene.add(m);
})();
/* ==========================================================================
13. BUILD INSTANCED MESHES
========================================================================== */
const boxGeo = new THREE.BoxGeometry(0.985, 0.985, 0.985);
function buildInstanced(map, material, castShadow, receiveShadow){
const count = map.size;
if (!count) return null;
const mesh = new THREE.InstancedMesh(boxGeo, material, count);
const mat4 = new THREE.Matrix4();
let i = 0;
for (const [key, hex] of map){
const x = (key % 1024) - 512;
const z = (Math.floor(key / 1024) % 1024) - 512;
const y = Math.floor(key / 1048576) - 512;
mat4.makeTranslation(x, y, z);
mesh.setMatrixAt(i, mat4);
mesh.setColorAt(i, colorOf(hex));
i++;
}
mesh.instanceMatrix.needsUpdate = true;
if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
mesh.castShadow = castShadow;
mesh.receiveShadow = receiveShadow;
mesh.frustumCulled = false;
scene.add(mesh);
return mesh;
}
const solidMat = new THREE.MeshLambertMaterial({ color: 0xffffff });
buildInstanced(VOX, solidMat, true, true);
const glowMat = new THREE.MeshBasicMaterial({ color: 0xffffff, toneMapped: false });
buildInstanced(GLOW, glowMat, false, false);
const waterMat = new THREE.MeshStandardMaterial({
color: 0xffffff, transparent: true, opacity: 0.74,
roughness: 0.08, metalness: 0.05, depthWrite: false
});
buildInstanced(WAT, waterMat, false, false);
const totalVoxels = VOX.size + GLOW.size + WAT.size;
document.getElementById('vcount').textContent = totalVoxels.toLocaleString() + ' (solid ' +
VOX.size.toLocaleString() + ' / water ' + WAT.size.toLocaleString() + ' / glow ' + GLOW.size.toLocaleString() + ')';
/* ==========================================================================
14. FALLING CHERRY PETALS
========================================================================== */
const PETALS = 1100;
const petalPos = new Float32Array(PETALS*3);
const petalSpd = new Float32Array(PETALS);
const petalPhase = new Float32Array(PETALS);
for (let i=0;i<PETALS;i++){
const a = Math.random()*Math.PI*2;
const r = Math.sqrt(Math.random())*62;
petalPos[i*3] = Math.cos(a)*r;
petalPos[i*3+1] = Math.random()*48;
petalPos[i*3+2] = Math.sin(a)*r;
petalSpd[i] = 0.4 + Math.random()*1.1;
petalPhase[i] = Math.random()*Math.PI*2;
}
const petalGeo = new THREE.BufferGeometry();
petalGeo.setAttribute('position', new THREE.BufferAttribute(petalPos, 3));
const petalTex = (function(){
const c = document.createElement('canvas');
c.width = c.height = 32;
const ctx = c.getContext('2d');
const g = ctx.createRadialGradient(16,16,0,16,16,16);
g.addColorStop(0.0, 'rgba(255,235,244,1)');
g.addColorStop(0.45,'rgba(255,190,215,0.95)');
g.addColorStop(1.0, 'rgba(255,170,205,0)');
ctx.fillStyle = g;
ctx.fillRect(0,0,32,32);
const t = new THREE.CanvasTexture(c);
t.colorSpace = THREE.SRGBColorSpace;
return t;
})();
const petalMat = new THREE.PointsMaterial({
size: 1.15, map: petalTex, transparent: true, depthWrite: false,
blending: THREE.NormalBlending, sizeAttenuation: true, color: 0xffc6dc, opacity: 0.95
});
const petals = new THREE.Points(petalGeo, petalMat);
petals.frustumCulled = false;
scene.add(petals);
/* ==========================================================================
15. ANIMATE
========================================================================== */
const clock = new THREE.Clock();
let tAcc = 0;
function animate(){
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.05);
tAcc += dt;
// petals drift
const arr = petalGeo.attributes.position.array;
for (let i=0;i<PETALS;i++){
const i3 = i*3;
arr[i3+1] -= (0.9 + petalSpd[i]) * dt * 2.6;
arr[i3] += Math.sin(tAcc*0.85 + petalPhase[i]) * dt * 2.2;
arr[i3+2] += Math.cos(tAcc*0.65 + petalPhase[i]*1.3) * dt * 2.2;
if (arr[i3+1] < 0.5){
const a = Math.random()*Math.PI*2;
const r = Math.sqrt(Math.random())*60;
arr[i3] = Math.cos(a)*r;
arr[i3+1] = 40 + Math.random()*14;
arr[i3+2] = Math.sin(a)*r;
}
}
petalGeo.attributes.position.needsUpdate = true;
// gentle water shimmer
waterMat.opacity = 0.70 + Math.sin(tAcc*0.9)*0.05;
controls.update();
renderer.render(scene, camera);
}
/* ==========================================================================
16. RESIZE + BOOT
========================================================================== */
window.addEventListener('resize', ()=>{
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
renderer.domElement.addEventListener('pointerdown', ()=>{ controls.autoRotate = false; });
// warm up shaders then reveal
requestAnimationFrame(()=>{
renderer.compile(scene, camera);
animate();
setTimeout(()=>{
const l = document.getElementById('loader');
l.classList.add('hide');
setTimeout(()=> l.remove(), 700);
}, 220);
});
</script>
</body>
</html>
Scene Composition & Interactive Exploration
The scene is built around three layered zones—a central pagoda plaza, a village, and a pond—each with distinct voxel details and interactive behavior.
- Pagoda & Stone Lanterns: A five-tier pagoda with golden finial anchors the garden. Stone lanterns with glowing interiors line the main path, and torii gates mark key entry points.
- Nature & Cherry Blossoms: Cherry trees, pine trees, and bamboo groves populate the terrain. Falling cherry petals drift continuously, with randomized speeds and paths, adding gentle movement.
- Village Life: Houses with varied roofs, a well, market stalls, and fenced crop fields create a lived-in village. Dozens of small voxel people with different clothing colors are placed along paths and communal spaces.
- Interactive Controls: Drag to orbit the camera, use the mouse wheel to zoom, and right-drag to pan. An auto-rotate mode slowly circles the scene until you click, giving a sweeping overview of the layout.
The HUD in the bottom-left shows the total voxel count, and a loading overlay hides the scene until shaders are ready.
Optimization Tip: You can easily tune the world by adjusting the PETALS constant at the top of section 14 to change the number of cherry petals, or modify the holes array in section 9 to reposition buildings. The autoRotateSpeed and controls.maxDistance in section 12 let you control camera behavior.
计时详情
- 实际等待(客户端)
- 0.95s
- 客户端请求准备
- 0.01s
- 本站代理准备
- 0.00s
- 上游响应头到达
- 0.38s
- 上游首字节到达
- 0.38s
- 上游首 Token 到达
- 0.70s
- 上游重试次数
- 0
实际等待使用客户端时钟;上游计时从本站准备完成后开始,各项为累计耗时,不能直接相加。上游等待包含网络、调度、计算及缓冲,不能仅凭这些数值断定厂商排队。
诊断记录
{
"attempts": 1,
"startedAt": "2026-09-28T07:30:37.286Z",
"transport": "railway",
"ttftSource": "proxy",
"clientTtftMs": 952.5,
"completionId": "dfd3f1c4-e4be-4fde-b6ef-9072c86ffb40",
"streamResumes": 4,
"proxyPrepareMs": 1,
"proxyRequestId": "goODPRCzQous3syAWUN5dQ",
"upstreamStatus": 200,
"upstreamTtftMs": 698,
"clientHeadersMs": 296.2999997138977,
"clientPrepareMs": 12.699999809265137,
"upstreamHeadersMs": 383,
"upstreamFirstByteMs": 384,
"clientFirstContentMs": 150376.2999997139
}历史结果 · 2026/9/28 07:53:39
Here's my take on this: a hand-built voxel diorama I call 「花守の里」— The Blossom Keeper's Village. A five-tier pagoda on a stone terrace, a cherry-blossom garden with a torii gate and an arched bridge, a pond with koi and ducks, and a living village with walking villagers, smoking chimneys, market stalls and rice paddies — every voxel placed by procedural builders, rendered as a single instanced mesh.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>花守の里 — The Blossom Keeper's Village</title>
<link href="https://fonts.googleapis.com/css2?family=Shippori+Mincho:wght@500;700;800&family=Zen+Kaku+Gothic+New:wght@400;500&display=swap" rel="stylesheet">
<style>
* { margin:0; padding:0; box-sizing:border-box; }
html,body { width:100%; height:100%; overflow:hidden; background:#14100e; }
canvas { display:block; }
.vignette { position:fixed; inset:0; pointer-events:none; box-shadow: inset 0 0 180px rgba(15,10,8,.38); z-index:5; }
#ui { position:fixed; inset:0; pointer-events:none; z-index:10; font-family:'Zen Kaku Gothic New',sans-serif; color:#f2e9d6; }
.panel { position:absolute; pointer-events:auto; background:rgba(22,17,14,.78); border:1px solid rgba(224,168,90,.32); backdrop-filter: blur(6px); }
#title { top:22px; left:24px; padding:16px 22px 14px; }
#title h1 { font-family:'Shippori Mincho',serif; font-weight:800; font-size:30px; letter-spacing:.14em; color:#f6ecd7; line-height:1.1; }
#title h1 span { color:#e0a85a; }
#title .en { font-family:'Shippori Mincho',serif; font-size:12px; letter-spacing:.24em; color:#c9b691; margin-top:5px; text-transform:uppercase; }
#title .sub { font-size:11px; color:#9a8c74; margin-top:7px; letter-spacing:.05em; }
#stats { bottom:22px; left:24px; padding:9px 16px; font-size:11.5px; letter-spacing:.08em; color:#c9b691; }
#stats b { color:#f2e9d6; font-weight:500; }
#hint { bottom:22px; right:24px; padding:9px 16px; font-size:11px; letter-spacing:.06em; color:#9a8c74; }
#btns { top:22px; right:24px; display:flex; gap:8px; }
.btn { pointer-events:auto; display:flex; align-items:center; gap:7px; cursor:pointer; user-select:none;
background:rgba(22,17,14,.78); border:1px solid rgba(224,168,90,.32); color:#d8c8a4;
font-family:'Zen Kaku Gothic New',sans-serif; font-size:11px; letter-spacing:.12em; padding:9px 14px;
transition:all .18s ease; backdrop-filter: blur(6px); }
.btn:hover { border-color:rgba(224,168,90,.7); color:#f6ecd7; }
.btn.on { color:#f0b95e; border-color:rgba(224,168,90,.75); }
.btn svg { width:13px; height:13px; stroke:currentColor; fill:none; stroke-width:1.8; stroke-linecap:round; stroke-linejoin:round; }
#loader { position:fixed; inset:0; z-index:50; background:#14100e; display:flex; flex-direction:column;
align-items:center; justify-content:center; gap:18px; transition:opacity 1.1s ease; }
#loader.hide { opacity:0; pointer-events:none; }
#loader .kanji { font-family:'Shippori Mincho',serif; font-size:52px; font-weight:700; letter-spacing:.3em; color:#e8d9b8; }
#loader .line { width:120px; height:1px; background:rgba(224,168,90,.5); }
#loader .msg { font-size:11px; letter-spacing:.3em; color:#8d7f66; text-transform:uppercase; }
</style>
</head>
<body>
<div id="loader">
<div class="kanji">花守の里</div>
<div class="line"></div>
<div class="msg">placing voxels</div>
</div>
<div class="vignette"></div>
<div id="ui">
<div class="panel" id="title">
<h1>花守の<span>里</span></h1>
<div class="en">The Blossom Keeper's Village</div>
<div class="sub">a voxel diorama — pagoda · gardens · village life</div>
</div>
<div class="panel" id="stats"></div>
<div id="btns">
<div class="btn on" id="bOrbit"><svg viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-3-6.7"/><path d="M21 3v5h-5"/></svg>ORBIT</div>
<div class="btn on" id="bPetal"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="2.4"/><circle cx="12" cy="5.5" r="3"/><circle cx="18" cy="10" r="3"/><circle cx="16" cy="17" r="3"/><circle cx="8" cy="17" r="3"/><circle cx="6" cy="10" r="3"/></svg>PETALS</div>
<div class="btn" id="bReset"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4"/></svg>RESET VIEW</div>
</div>
<div class="panel" id="hint">drag · orbit scroll · zoom right-drag · pan</div>
</div>
<script src="https://unpkg.com/three@0.128.0/build/three.min.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script>
/* =========================================================================
花守の里 — The Blossom Keeper's Village
A fully procedural voxel diorama. Every structure is built from code.
========================================================================= */
// ------------------------- renderer / scene ------------------------------
const renderer = new THREE.WebGLRenderer({ antialias:true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.02;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0xdcebee, 170, 420);
// sky — painted vertical gradient
(function(){
const cv = document.createElement('canvas'); cv.width = 2; cv.height = 512;
const g = cv.getContext('2d');
const gr = g.createLinearGradient(0,0,0,512);
gr.addColorStop(0.0, '#6fb1e8');
gr.addColorStop(0.45,'#a8d2ef');
gr.addColorStop(0.78,'#e3eff1');
gr.addColorStop(0.92,'#f6e7d4');
gr.addColorStop(1.0, '#f0d9c2');
g.fillStyle = gr; g.fillRect(0,0,2,512);
const tex = new THREE.CanvasTexture(cv);
scene.background = tex;
})();
const camera = new THREE.PerspectiveCamera(50, innerWidth/innerHeight, 0.5, 900);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; controls.dampingFactor = 0.06;
controls.minDistance = 18; controls.maxDistance = 340;
controls.maxPolarAngle = Math.PI * 0.495;
controls.autoRotate = true; controls.autoRotateSpeed = 0.35;
controls.target.set(-6, 14, -8);
const CAM_START = new THREE.Vector3(165, 105, 185);
const CAM_END = new THREE.Vector3(72, 46, 96);
camera.position.copy(CAM_START);
// lights
const hemi = new THREE.HemisphereLight(0xcfe2ff, 0x9c8a66, 0.62);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xffeecf, 1.25);
sun.position.set(95, 140, 60);
sun.castShadow = true;
sun.shadow.mapSize.set(4096, 4096);
sun.shadow.camera.left = -135; sun.shadow.camera.right = 135;
sun.shadow.camera.top = 135; sun.shadow.camera.bottom = -135;
sun.shadow.camera.near = 20; sun.shadow.camera.far = 460;
sun.shadow.bias = -0.0004; sun.shadow.normalBias = 0.6;
scene.add(sun);
const fill = new THREE.DirectionalLight(0xa8c0dd, 0.28);
fill.position.set(-80, 45, -70);
scene.add(fill);
// ------------------------- utility ---------------------------------------
const clamp = (v,a,b)=>Math.max(a,Math.min(b,v));
const lerp = (a,b,t)=>a+(b-a)*t;
const smooth= t=>{ t=clamp(t,0,1); return t*t*(3-2*t); };
function hash2(ix,iz){
let n = (Math.imul(ix,374761393) + Math.imul(iz,668265263))|0;
n = Math.imul(n ^ (n>>>13), 1274126177); n ^= n>>>16;
return (n>>>0)/4294967296;
}
function vnoise(x,z){
const ix=Math.floor(x), iz=Math.floor(z), fx=x-ix, fz=z-iz;
const a=hash2(ix,iz), b=hash2(ix+1,iz), c=hash2(ix,iz+1), d=hash2(ix+1,iz+1);
const u=fx*fx*(3-2*fx), v=fz*fz*(3-2*fz);
return a+(b-a)*u+(c-a)*v+(a-b-c+d)*u*v;
}
const fbm = (x,z)=> vnoise(x,z)*0.55 + vnoise(x*2.13+13.7, z*2.13+7.1)*0.28 + vnoise(x*4.31+31.4, z*4.31+17.9)*0.17;
function distSeg(px,pz, ax,az, bx,bz){
const dx=bx-ax, dz=bz-az, l2=dx*dx+dz*dz;
let t = l2? ((px-ax)*dx+(pz-az)*dz)/l2 : 0; t = clamp(t,0,1);
const x=ax+dx*t, z=az+dz*t;
return Math.hypot(px-x, pz-z);
}
// ------------------------- terrain height field ---------------------------
const OFF=100, N=201;
const H = new Int16Array(N*N); // top solid voxel y per column
const pathM = new Uint8Array(N*N); // path mask
const idx = (x,z)=>(x+OFF)+(z+OFF)*N;
const inB = (x,z)=> x>=-OFF && x<=OFF && z>=-OFF && z<=OFF;
const gAt = (x,z)=>{ x=clamp(Math.round(x),-OFF,OFF); z=clamp(Math.round(z),-OFF,OFF); return H[idx(x,z)]; };
// stream centreline (starts inside the pond, exits south)
const STREAM = [[26,4],[22,10],[14,17],[5,24],[0,30],[-4,42],[-6,58],[-6,76],[-8,100]];
function streamDist(x,z){
let m=1e9;
for(let i=0;i<STREAM.length-1;i++)
m = Math.min(m, distSeg(x,z, STREAM[i][0],STREAM[i][1], STREAM[i+1][0],STREAM[i+1][1]));
return m;
}
const POND = {x:30, z:2};
const TER = {x:-18, z:-34}; // pagoda terrace centre
for(let z=-OFF; z<=OFF; z++) for(let x=-OFF; x<=OFF; x++){
let h = 3.5 + fbm(x*0.024, z*0.024)*6; // rolling base
const dEdge = Math.hypot(x,z);
h += smooth((dEdge-55)/38)*16; // rim mountains
// village plateau (flat building ground)
const dxv = Math.max(-55-x, x-45, 0), dzv = Math.max(34-z, z-82, 0);
h = lerp(h, 4, smooth(1 - Math.max(dxv,dzv)/9));
// pond bowl
const dP = Math.hypot(x-POND.x, z-POND.z);
const tP = 1 - smooth((dP-8.5)/4.5);
if(tP>0) h = Math.min(h, lerp(h, 2, tP));
// stream carve
const dS = streamDist(x,z);
if(dS<4.5){
const tS = dS<2.2 ? 1 : 1-(dS-2.2)/2.3;
h = Math.min(h, lerp(h, 2, tS));
}
// pagoda terrace hill + plateau
const dT = Math.hypot(x-TER.x, z-TER.z);
const tT = 1 - smooth((dT-16)/8);
if(tT>0) h = Math.max(h, lerp(h, 10, smooth(tT)));
H[idx(x,z)] = Math.round(h);
}
// stepped stairway carved into the terrace skirt
for(let z=-14; z<=-19; z++){
const top = z>=-18 ? 10 : (z===-17||z===-16 ? 9 : 8);
for(let x=-22; x<=-15; x++) H[idx(x,z)] = top;
}
// rice paddy terraces (channels flooded, mud ridges between)
const PADDY = {x0:28, x1:44, z0:62, z1:78};
for(let z=PADDY.z0; z<=PADDY.z1; z++) for(let x=PADDY.x0; x<=PADDY.x1; x++){
const edge = x===PADDY.x0||x===PADDY.x1||z===PADDY.z0||z===PADDY.z1;
const chan = !edge && ((z-PADDY.z0)%4===1 || (z-PADDY.z0)%4===2);
H[idx(x,z)] = edge ? 4 : (chan ? 2 : 3);
}
// path network
const PATHS = [
[[0,84],[0,40]],[[0,40],[0,24]], // main street
[[0,12],[0,0]],[[0,0],[-8,-8]],[[-8,-8],[-16,-14]],[[-16,-14],[-18,-17]], // to garden
[[-18,-17],[-18,-24]], // across terrace
[[-42,52],[32,52]], // cross street
[[12,44],[12,58]],[[-20,46],[-16,50]], // lanes
[[16,2],[24,-8],[36,-8],[42,2],[38,10],[24,10],[16,2]], // pond loop
[[6,40],[20,40]],[[0,66],[24,66]], // east lanes
];
for(const pts of PATHS) for(let i=0;i<pts.length-1;i++){
const [ax,az]=pts[i], [bx,bz]=pts[i+1];
const steps = Math.ceil(Math.max(Math.abs(bx-ax),Math.abs(bz-az)))*2;
for(let s=0;s<=steps;s++){
const px = Math.round(lerp(ax,bx,s/steps)), pz = Math.round(lerp(az,bz,s/steps));
for(let dx=-1; dx<=1; dx++) for(let dz=-1; dz<=1; dz++){
const x=px+dx, z=pz+dz;
if(!inB(x,z)) continue;
if(Math.abs(dx)+Math.abs(dz)===2 && hash2(x*3,z*3)<0.5) continue;
if(gAt(x,z)>=3) pathM[idx(x,z)] = 1; // never stamp open water
}
}
}
// ------------------------- voxel store ------------------------------------
const V = new Map();
const KEY = (x,y,z)=>((x+128)*256+(z+128))*256+(y+64);
function jitC(hex, amt){
let r=hex>>16&255, g=hex>>8&255, b=hex&255;
const d=(Math.random()-0.5)*2*amt;
r+=d; g+=d; b+=d;
r+=(Math.random()-0.5)*5; g+=(Math.random()-0.5)*5; b+=(Math.random()-0.5)*5;
return (clamp(r|0,0,255)<<16)|(clamp(g|0,0,255)<<8)|clamp(b|0,0,255);
}
function set(x,y,z,c,j=4){ if(y<0||y>120||!inB(x,z)) return; V.set(KEY(x,y,z), jitC(c,j)); }
function box(x0,y0,z0,x1,y1,z1,c,j){
for(let y=y0;y<=y1;y++) for(let z=z0;z<=z1;z++) for(let x=x0;x<=x1;x++) set(x,y,z,c,j);
}
function ring(cx,cz,y,h,c,j){ for(let i=-h;i<=h;i++){ set(cx+i,y,cz-h,c,j); set(cx+i,y,cz+h,c,j); set(cx-h,y,cz+i,c,j); set(cx+h,y,cz+i,c,j); } }
function sq(cx,cz,y,h,c,j){ for(let z=-h;z<=h;z++) for(let x=-h;x<=h;x++) set(cx+x,y,cz+z,c,j); }
function disc(cx,cz,y,r,c,j){ for(let z=-r;z<=r;z++) for(let x=-r;x<=r;x++) if(x*x+z*z<=r*r+r*0.5) set(cx+x,y,cz+z,c,j); }
function blob(cx,cy,cz,r,c,j){
const R=Math.ceil(r);
for(let dy=-R;dy<=R;dy++) for(let dz=-R;dz<=R;dz++) for(let dx=-R;dx<=R;dx++){
const d2 = dx*dx+dy*dy+dz*dz;
if(d2 <= r*r*(0.82+hash2(cx+dx*7+dy*3, cz+dz*5+dy)*0.34)) set(cx+dx, cy+dy, cz+dz, c, j);
}
}
// ------------------------- palette ----------------------------------------
const C = {
grass1:0x5fae4a, grass2:0x7ac25a, grassDry:0x9db35a, dirt:0x7c5433, stone:0x8b8d93,
sand:0xd6c493, pebble:0x9d9176, gravel:0xccc09b, gravel2:0xa89a80,
water:0x2f8fd6,
red:0xc8402f, cream:0xf2e7cf, roofI:0x3b556e, roofEdge:0x57809e, gold:0xe8b23a,
woodD:0x6e4527, wood:0x8a6440, woodL:0xa87b4e,
pink1:0xf7a8c4, pink2:0xf39ac0, pink3:0xfbc9dc, pinkW:0xfadfe8,
pine1:0x2e6e46, pine2:0x3c8a55, leaf1:0x4c9e4c, leaf2:0x6fbd52, maple:0xd96a2b, maple2:0xe08a3a,
glow:0xffd489, win:0x46352c, door:0x4a2f1d,
};
// ------------------------- terrain voxels ---------------------------------
const waterCells = [];
for(let z=-OFF; z<=OFF; z++) for(let x=-OFF; x<=OFF; x++){
const h = H[idx(x,z)];
for(let y=0; y<=h; y++){
let c;
if(y===0) c = 0x54545b;
else if(y===h){
const onTerrace = Math.abs(x-TER.x)<=15 && Math.abs(z-TER.z)<=15;
if(onTerrace){ // stone plaza, checker slabs
c = ((x+z)&1) ? 0x9aa1a8 : 0x8a9198;
if(Math.random()<0.05) c = 0x6f9459;
}
else if(h<=2) c = Math.random()<0.5 ? C.sand : C.pebble; // pond / stream bed
else if(pathM[idx(x,z)]) c = Math.random()<0.13 ? C.gravel2 : C.gravel;
else if(h>=20) c = 0xedf1f4; // snow caps
else if(h>=14) c = Math.random()<0.32 ? 0x7fae62 : 0x8f939a; // rocky slopes
else if(h<=3) c = C.sand; // shorelines
else if(h>=11) c = Math.random()<0.25 ? C.grassDry : 0x4f9440;
else {
const n = fbm(x*0.15+40, z*0.15+9);
c = n>0.55 ? C.grass2 : (n<0.42 ? C.grass1 : 0x6cb850);
}
set(x,y,z,c, pathM[idx(x,z)]?7:10);
}
else if(y > h-3) set(x,y,z,C.dirt,9);
else set(x,y,z,C.stone,8);
}
if(h<=2){ waterCells.push(x,3,z); set(x,h,z,C.sand,5); } // register water + sandy bed
}
// rice sprouts standing in the paddies
for(let z=PADDY.z0; z<=PADDY.z1; z++) for(let x=PADDY.x0; x<=PADDY.x1; x++){
if(gAt(x,z)===2 && x%2===0) set(x,4,z,0x569b3f,10);
}
// ------------------------- the pagoda -------------------------------------
const PX=TER.x, PZ=TER.z;
sq(PX,PZ,11,12,C.woodD,4); // timber floor of tier 1
const HB=[11,9,8,6,5]; // body half-widths per tier
function pagodaRoof(cx,cz,y,hb){
const e=hb+3;
ring(cx,cz,y, e, C.roofEdge,5); ring(cx,cz,y, e-1,C.roofI,5); // eaves
sq (cx,cz,y+1, e-1, C.roofI,5); ring(cx,cz,y+1,e-1,C.roofEdge,5);
sq (cx,cz,y+2, e-2, C.roofI,5); ring(cx,cz,y+2,e-2,C.roofEdge,5);
sq (cx,cz,y+3, hb, C.roofI,5); ring(cx,cz,y+3,hb, C.roofEdge,5);
for(const sx of [-1,1]) for(const sz of [-1,1]){ // upswept corner tips
set(cx+sx*e, y, cz+sz*(e+1), C.roofEdge,4);
set(cx+sx*(e+1), y, cz+sz*e, C.roofEdge,4);
set(cx+sx*(e+1), y+1, cz+sz*(e+1), C.gold,4); // golden tip
set(cx+sx*(e+1), y+2, cz+sz*(e+1), C.red,4); // bell
}
}
function pagodaBody(cx,cz,y0,hb,hgt,tier){
for(let y=y0;y<y0+hgt;y++){
for(let i=-hb;i<=hb;i++){
for(const [wx,wz] of [[cx+i,cz-hb],[cx+i,cz+hb],[cx-hb,cz+i],[cx+hb,cz+i]]){
const onX = (wz===cz-hb||wz===cz+hb);
const a = onX ? wx-cx : wz-cz;
let c = C.cream, j = 5;
if(Math.abs(a)===hb){ c=C.red; j=6; } // corner posts
else if(y===y0+hgt-1){ c=C.woodD; j=3; } // head beam
else if(tier===1 && !onX && Math.abs(a)<=1 && y<y0+4){ c=0x6b1d12; j=3; } // door
else if(y>y0+1 && y<y0+5 && Math.abs(a)<=1){
c = (a===0 && y===y0+3) ? C.glow : C.win; j=3; // windows
}
set(wx,y,wz,c,j);
}
}
}
// balcony just outside the walls, on the roof slab below
ring(cx,cz,y0-1,hb+1,C.woodD,4);
for(let i=-hb-1;i<=hb+1;i+=2){
set(cx+i, y0, cz-hb-1, C.woodD,3); set(cx+i, y0, cz+hb+1, C.woodD,3);
set(cx-hb-1, y0, cz+i, C.woodD,3); set(cx+hb-0+1, y0, cz+i, C.woodD,3);
}
}
let y0 = 12;
for(let t=0; t<5; t++){
pagodaBody(PX,PZ,y0,HB[t],6,t+1);
pagodaRoof(PX,PZ,y0+6,HB[t]);
y0 += 10;
}
// sacred spire
for(let y=62;y<=70;y++) set(PX,y,PZ,C.gold,6);
for(const ry of [63,65,67]) sq(PX,PZ,ry,1,C.gold,4);
blob(PX,71,PZ,1.6,0xf5cf4f,5);
// terrace parapet with a gap at the stairway
for(let i=-15;i<=15;i++){
for(const [px2,pz2] of [[PX+i,PZ-15],[PX+i,PZ+15],[PX-15,PZ+i],[PX+15,PZ+i]]){
if(pz2===PZ+15 && Math.abs(px2-PX)<=4) continue;
if(i%2===0) set(px2,11,pz2,0x9aa1a8,5);
}
}
// sand garden with raked lines, on a corner of the plaza
for(let z=-45;z<=-39;z++) for(let x=-30;x<=-24;x++)
set(x,10,z, z%2===0 ? 0xe8e6df : 0xdcd9d0, 3);
box(-29,11,-44,-29,11,-44,0x777c83,4); box(-28,11,-44,-28,11,-44,0x8a8f96,4);
box(-26,11,-42,-26,12,-42,0x6e737a,4);
// ------------------------- torii gate -------------------------------------
(function(){
const zx=6, zc=10, g=gAt(-5,zc);
for(const sx of [-1,1]){
const px2 = sx*5;
box(px2,g+1,zc,px2+sx,g+9,zc,C.red,6); // pillars (2 wide)
set(px2,g,zc,0x2b2320,3); set(px2+sx,g,zc,0x2b2320,3); // stone bases
}
box(-7,g+6,zc,7,g+6,zc,C.red,5); // nuki beam
box(-9,g+9,zc,9,g+9,zc,C.red,5); // kasagi
box(-9,g+10,zc,-7,g+10,zc,0x2b2320,3); box(7,g+10,zc,9,g+10,zc,0x2b2320,3);
box(0,g+7,zc,0,g+8,zc,0x2b2320,3); set(0,g+8,zc,C.gold,4); // central plaque
})();
// ------------------------- stone lanterns & benches -----------------------
function lantern(x,z){
const g=gAt(x,z);
box(x,g+1,z,x+1,g+1,z+1,0x9aa0a6,5);
set(x,g+2,z,0x8a9096,4); set(x,g+3,z,0x8a9096,4);
sq(x,g+4,z,1,0x9aa0a6,4);
sq(x,g+5,z,1,0x55575c,3); set(x,g+5,z,C.glow,3);
sq(x,g+6,z,1,0x55575c,3); set(x,g+7,z,0x55575c,3);
}
[[4,4],[-4,4],[4,-8],[-4,-8],[22,2],[38,6],[PX-15,PZ+13],[PX+15,PZ+13],[4,56],[4,48]]
.forEach(p=>lantern(p[0],p[1]));
function bench(x,z){
const g=gAt(x,z);
set(x,g+1,z,C.wood,4); set(x+1,g+1,z,C.wood,4);
set(x,g+2,z,C.woodD,3); set(x+1,g+2,z,C.woodD,3);
}
bench(18,6); bench(3,38); bench(-20,4);
// ------------------------- bridges ----------------------------------------
// arched bridge over the stream on the garden approach
for(let z=24; z<=35; z++){
const t=(z-24)/11, y = 5 + Math.round(Math.sin(Math.PI*t)*2.6);
for(let x=-2; x<=2; x++){
set(x,y,z,C.woodL,6);
const g=gAt(x,z);
for(let yy=g+1; yy<y; yy++) set(x,yy,z,0x77726c,6); // stone footings
if(x===-2||x===2){
set(x,y+1,z,C.woodD,4);
if(z%3===0) set(x,y+2,z,C.woodD,4);
}
}
}
// flat plank bridge on the village cross street
for(let x=-8; x<=-4; x++) for(let z=51; z<=53; z++) set(x,4,z,C.woodL,6);
[[-8,51],[-8,53],[-4,51],[-4,53]].forEach(p=>set(p[0],5,p[1],C.woodD,4));
// ------------------------- fences ------------------------------------------
function fence(x0,z0,x1,z1){
const dx=Math.sign(x1-x0), dz=Math.sign(z1-z0), len=Math.max(Math.abs(x1-x0),Math.abs(z1-z0));
for(let i=0;i<=len;i++){
const x=x0+dx*i, z=z0+dz*i, g=gAt(x,z);
set(x,g+2,z,C.woodD,4);
if(i%3===0) set(x,g+1,z,C.woodD,3);
}
}
fence(6,-2,6,-14); fence(-27,-13,-23,-13); fence(-14,-13,-9,-13);
fence(18,38,26,38); fence(-24,64,-24,72); fence(8,80,18,80);
// ------------------------- village houses ----------------------------------
const reserved = [];
const chimneys = [];
function house(cx,cz,w,d,wall,roofC,opts={}){
const hw=w>>1, hd=d>>1;
const g=gAt(cx,cz), y0h=g+1, wh=opts.wh||4;
const x0=cx-hw, x1=cx+hw, z0=cz-hd, z1=cz+hd;
reserved.push([x0-2,z0-2,x1+2,z1+2]);
for(let y=y0h-2; y<y0h+wh; y++){
const top = y===y0h+wh-1;
for(let z=z0; z<=z1; z++) for(let x=x0; x<=x1; x++){
if(x!==x0 && x!==x1 && z!==z0 && z!==z1) continue;
const corner = (x===x0||x===x1)&&(z===z0||z===z1);
let c = wall, j=5;
if(corner || (x-x0)%4===0&&(z===z0||z===z1) || (z-z0)%4===0&&(x===x0||x===x1)){ c=C.woodD; j=4; }
if(top && !corner){ c=C.woodD; j=3; }
set(x,y,z,c,j);
}
}
// gabled roof, ridge along the longer axis, one-voxel eaves overhang
const yR=y0h+wh, alongX = w>=d;
let ridgeEnds;
if(alongX){
let i=0;
for(let zh=hd+1; zh>=1; zh--,i++){
for(let x=x0-1; x<=x1+1; x++) for(let z=cz-zh; z<=cz+zh; z++)
set(x,yR+i,z, (z===cz-zh||z===cz+zh)? C.roofEdge : roofC, 5);
}
for(let x=x0; x<=x1; x++){ set(x,yR+hd+1,cz,0x3f2e20,4); }
ridgeEnds=[[x0-1,yR-1,z0],[x1+1,yR-1,z1]];
} else {
let i=0;
for(let xh=hw+1; xh>=1; xh--,i++){
for(let z=z0-1; z<=z1+1; z++) for(let x=cx-xh; x<=cx+xh; x++)
set(x,yR+i,z, (x===cx-xh||x===cx+xh)? C.roofEdge : roofC, 5);
}
for(let z=z0; z<=z1; z++){ set(cx,yR+hw+1,z,0x3f2e20,4); }
}
// door + windows (door faces the main street by default)
const side = opts.door||'W';
const dz2 = side==='N'?z0 : side==='S'?z1 : 0;
const dx2 = side==='W'?x0 : side==='E'?x1 : 0;
if(side==='N'||side==='S'){
set(cx,y0h,dz2,C.door,3); set(cx,y0h+1,dz2,C.door,3);
set(cx-1,y0h+2,dz2,0xd94430,5); set(cx+1,y0h+2,dz2,0xd94430,5); // hanging lanterns
for(const off of [-3,3]) if(cx+off>x0&&cx+off<x1){
set(cx+off,y0h+1,dz2,C.glow,4); set(cx+off,y0h+2,dz2,C.win,3);
}
} else {
set(dx2,y0h,cz,C.door,3); set(dx2,y0h+1,cz,C.door,3);
set(dx2,y0h+2,cz-1,0xd94430,5); set(dx2,y0h+2,cz+1,0xd94430,5);
for(const off of [-3,3]) if(cz+off>z0&&cz+off<z1){
set(dx2,y0h+1,cz+off,C.glow,4); set(dx2,y0h+2,cz+off,C.win,3);
}
}
for(const zz of [z0,z1]) for(const off of [-3,3]) if(cx+off>x0&&cx+off<x1){
set(cx+off,y0h+1,zz,C.glow,4);
}
if(opts.wh===7){ // two-storey band + upper windows
for(let z=z0;z<=z1;z++) for(let x=x0;x<=x1;x++)
if(x===x0||x===x1||z===z0||z===z1) set(x,y0h+3,z,C.woodD,3);
for(const zz of [z0,z1]) for(const off of [-3,3]) set(cx+off,y0h+5,zz,C.glow,4);
}
if(opts.chimney){
const chx = x1-1;
for(let y=yR; y<=yR+hd+2; y++) set(chx,y,cz,0x8b8f96,5);
set(chx,yR+hd+2,cz,0x55575c,3);
chimneys.push([chx, yR+hd+3, cz]);
}
// dirt path stub from the door toward the nearest street
let tx=0, tz=0;
if(side==='W'){ tx=-1; } else if(side==='E'){ tx=1; } else { tz = side==='N'?-1:1; }
let px3 = dx2||cx, pz3 = dz2||cz;
for(let i=0;i<6;i++){
px3+=tx*2; pz3+=tz*2;
if(!inB(px3,pz3) || gAt(px3,pz3)<3) break;
pathM[idx(px3,pz3)]=1; pathM[idx(px3+ (tz?1:0), pz3+(tx?1:0))]=1;
}
}
house(-11,41, 8,7, 0xf1e8d2, 0xb3592f, {door:'E', chimney:true});
house( 11,43, 9,8, 0xece4cf, 0x64707d, {door:'W'});
house(-16,58, 8,8, 0xf1e8d2, 0x5e7d54, {door:'E'});
house( 14,62,10,9, 0xe7dcc0, 0xb3592f, {door:'W', wh:7, chimney:true}); // the inn
house(-32,46, 8,7, 0xece4cf, 0x64707d, {door:'E'});
house( 30,44, 9,7, 0xf1e8d2, 0x5e7d54, {door:'W', chimney:true});
house(-34,68, 9,8, 0xece4cf, 0xb3592f, {door:'E', chimney:true});
house( 20,74, 8,7, 0xf1e8d2, 0x64707d, {door:'W'});
house(-14,74, 8,7, 0xf1e8d2, 0x64707d, {door:'E'});
house( 36,56, 7,6, 0x8a6440, 0x7a5230, {door:'W'}); // farmer's hut
// ------------------------- well, stalls, clutter ---------------------------
(function well(x,z){
const g=gAt(x,z);
for(let dz=-1;dz<=1;dz++) for(let dx=-1;dx<=1;dx++){
if(dx||dz){ set(x+dx,g+1,z+dz,0x8a9096,5); set(x+dx,g+2,z+dz,0x8a9096,5); }
else set(x,g+1,z,0x2f8fd6,4);
}
set(x,g+3,z-1,C.woodD,3); set(x,g+3,z+1,C.woodD,3);
set(x,g+4,z-1,C.woodD,3); set(x,g+4,z+1,C.woodD,3);
for(let dz=-2;dz<=2;dz++) for(let dx=-1;dx<=1;dx++)
set(x+dx,g+5,z+dz,C.woodD,4);
set(x,g+4,z,0x9a8c74,4); set(x,g+3,z,0x6e4527,4); // rope & bucket
})(5,55);
function stall(cx,cz,c1,c2){
const g=gAt(cx,cz);
for(const [dx,dz] of [[-2,-1],[2,-1],[-2,1],[2,1]])
for(let y=g+1;y<=g+3;y++) set(cx+dx,y,cz+dz,C.woodD,3);
for(let dx=-3;dx<=3;dx++) for(let dz=-2;dz<=2;dz++)
set(cx+dx,g+4,cz+dz, (dx+9)%2? c1:c2, 4);
for(let dx=-3;dx<=3;dx++) for(let dz=-1;dz<=1;dz++) set(cx+dx,g+2,cz+dz,C.woodL,4);
const goods=[0xff8c2e,0xe84a4a,0xf5d442];
for(let i=0;i<3;i++){
const gx=cx-2+i*2, gz=cz+(i%2?1:-1);
set(gx,g+3,gz,goods[i],6); set(gx+ (i%2?-1:1),g+3,gz,goods[i],6);
}
}
stall(-1,47, 0xd94430, 0xf2e7cf);
stall( 6,58, 0x2e6e46, 0xf2e7cf);
// crates, barrels, clothesline
box(9,5,53,9,5,53,0xb08a4a,6); box(10,5,53,10,6,53,0xa37c3f,6); box(9,5,54,9,5,54,0xb08a4a,6);
set(-3,5,62,0x8a5a30,5); set(-3,6,62,0x6e4527,4); set(-2,5,62,0x8a5a30,5);
set(24,5,58,0x8a5a30,5); set(25,5,58,0x8a5a30,5); set(25,6,58,0x6e4527,4);
(function clothes(x0,z,x1){
const g=gAt(x0,z);
set(x0,g+1,z,C.woodD,3); set(x0,g+2,z,C.woodD,3); set(x0,g+3,z,C.woodD,3);
set(x1,g+1,z,C.woodD,3); set(x1,g+2,z,C.woodD,3); set(x1,g+3,z,C.woodD,3);
const cloth=[0xe8b4c8,0x9ec4e0,0xf2ede0,0xc9a3d6];
for(let x=x0+1;x<x1;x++){
set(x,g+3,z,0x8a8378,4);
if(x%2===0){ set(x,g+2,z,cloth[(x>>1)%4],6); }
}
})(8,37,14);
// ------------------------- trees -------------------------------------------
const treeSpots = [];
function farEnough(x,z,d){ for(const p of treeSpots) if(Math.hypot(p[0]-x,p[1]-z)<d) return false; return true; }
function nearPath(x,z){ for(let dz=-2;dz<=2;dz++) for(let dx=-2;dx<=2;dx++){ const ax=x+dx,az=z+dz; if(inB(ax,az)&&pathM[idx(ax,az)]) return true; } return false; }
function isReserved(x,z){ for(const r of reserved) if(x>=r[0]&&x<=r[2]&&z>=r[1]&&z<=r[3]) return true; return false; }
function cherry(x,z,white){
const g=gAt(x,z); if(g<4) return;
const th=4+Math.floor(Math.random()*3);
for(let y=g+1;y<=g+th;y++) set(x,y,z,0x6b4a34,7);
const p1=white?C.pinkW:C.pink1, p2=white?C.pink3:C.pink2, p3=C.pink3;
blob(x,g+th+2,z, 3.1+Math.random(), p1, 9);
blob(x+2,g+th+1,z+1, 2.1, p2, 9);
blob(x-2,g+th+2,z-1, 2.2, p3, 9);
if(Math.random()<0.6) blob(x+1,g+th+3,z-2, 1.8, p2, 9);
for(let i=0;i<14;i++){ // fallen petals on the grass
const px4=x+Math.round((Math.random()-0.5)*8), pz4=z+Math.round((Math.random()-0.5)*8);
if(inB(px4,pz4)&&gAt(px4,pz4)>=3&&!pathM[idx(px4,pz4)]) set(px4,gAt(px4,pz4)+1,pz4, C.pink3, 8);
}
treeSpots.push([x,z]);
}
function greenTree(x,z){
const g=gAt(x,z); if(g<4) return;
const th=3+Math.floor(Math.random()*2);
for(let y=g+1;y<=g+th;y++) set(x,y,z,0x6b4a34,7);
blob(x,g+th+2,z, 3.0, C.leaf1, 12);
blob(x+2,g+th+1,z+2, 2.0, C.leaf2, 12);
blob(x-2,g+th+3,z-1, 2.0, C.leaf2, 12);
if(Math.random()<0.35) blob(x-1,g+th+1,z+2, 1.5, 0xe14b4b, 8); // fruit
treeSpots.push([x,z]);
}
function pine(x,z){
const g=gAt(x,z); if(g<4) return;
const th=3+Math.floor(Math.random()*3);
for(let y=g+1;y<=g+th;y++) set(x,y,z,0x5c3f2b,6);
let y=g+th+1;
for(const r of [3,3,2,2,1,1,0]){
if(r===0){ set(x,y,z,C.pine1,8); break; }
disc(x,z,y,r, Math.random()<0.5?C.pine1:C.pine2, 10);
y++;
}
treeSpots.push([x,z]);
}
function maple(x,z){
const g=gAt(x,z); if(g<4) return;
const th=3+Math.floor(Math.random()*2);
for(let y=g+1;y<=g+th;y++) set(x,y,z,0x5c3f2b,6);
blob(x,g+th+2,z, 2.8, C.maple, 12);
blob(x+2,g+th+3,z, 1.8, C.maple2, 12);
treeSpots.push([x,z]);
}
function bush(x,z){
const g=gAt(x,z); if(g<3) return;
blob(x,g+1,z, 1.6, C.leaf1, 12);
if(Math.random()<0.5) set(x+1,g+2,z, 0xf5d442, 8);
}
function flowerPatch(x,z){
const cols=[0xff5b5b,0xffd93d,0xffffff,0xff9ff3,0xf39c3d];
for(let i=0;i<10;i++){
const fx=x+Math.round((Math.random()-0.5)*6), fz=z+Math.round((Math.random()-0.5)*6);
if(!inB(fx,fz)) continue;
const g=gAt(fx,fz);
if(g>=4 && !pathM[idx(fx,fz)]){ set(fx,g+1,fz,cols[Math.floor(Math.random()*5)],10); set(fx,g,fz+0,0,0); set(fx,g,fz, undefined); }
}
}
// hand-placed cherries: avenue along the approach + pond ring
for(const zz of [16,22,28]){ cherry(-6,zz); cherry(6,zz); }
for(const zz of [-2,-7,-12]){ cherry(7,zz); cherry(-7,zz,false); }
[[16,-8],[42,4],[38,-8],[22,-10],[45,-2]].forEach(p=>cherry(p[0],p[1], Math.random()<0.3));
cherry(-24,0); cherry(-12,4,true); cherry(-2,-16);
maple(-30,-22); maple(-6,-22); maple(46,14); maple(-40,20);
// scattered woodland
let tries=0;
while(treeSpots.length<115 && tries<5000){
tries++;
const x = -97+Math.floor(Math.random()*194), z = -97+Math.floor(Math.random()*194);
const h = gAt(x,z);
if(h<4||h>13) continue;
if(Math.hypot(x-TER.x,z-TER.z)<26) continue;
if(Math.hypot(x-POND.x,z-POND.z)<15) continue;
if(isReserved(x,z) || nearPath(x,z) || !farEnough(x,z,5.5)) continue;
if(Math.abs(x)<5 && z>32 && z<86) continue;
if(Math.abs(z-52)<5 && x>-44 && x<34) continue;
const dP=Math.hypot(x-POND.x,z-POND.z);
const r=Math.random();
if(dP<30) r<0.6?cherry(x,z):r<0.85?greenTree(x,z):bush(x,z);
else if(h>=10) r<0.75?pine(x,z):greenTree(x,z);
else if(z>-30&&z<30&&Math.abs(x)<45) r<0.4?cherry(x,z):r<0.75?greenTree(x,z):r<0.9?bush(x,z):maple(x,z);
else r<0.55?greenTree(x,z):r<0.85?pine(x,z):bush(x,z);
}
// flower patches + rocks
for(const p of [[4,-2],[-10,2],[20,12],[34,-6],[2,34],[-8,44],[16,68],[34,50]])
flowerPatch(p[0],p[1]);
for(const p of [[20,-2],[40,8],[24,12],[-45,-20],[50,30],[-52,10]]){
const g=gAt(p[0],p[1]); if(g<3) continue;
blob(p[0],g,p[1],1.4+Math.random(), 0x8a8f96, 8);
}
// ------------------------- villagers (static, in the voxel map) ------------
const SKIN=[0xf2c99b,0xdda577,0xb57a4a], HAIR=[0x2b2b2b,0x191410,0x4a3220,0x66503a];
const SHIRT=[0xd94f3d,0x3d7dd9,0xe9a13b,0x7a4fd9,0x2fa38a,0x40495a,0xc25b8a];
const PANT=[0x4a4038,0x39465c,0x6e5233];
function pick(a){ return a[Math.floor(Math.random()*a.length)]; }
function person(x,z,pose){
const g=gAt(x,z); if(g<3) return;
const sk=pick(SKIN), sh=pick(SHIRT), pa=pick(PANT), hr=pick(HAIR);
if(pose==='sit'){
set(x,g+1,z,pa,5); set(x,g+2,z,pa,5);
set(x,g+3,z,sh,5); set(x,g+4,z,sk,5); set(x,g+5,z,hr,5);
} else {
set(x,g+1,z,pa,5); set(x,g+2,z,sh,5); set(x,g+3,z,sh,5);
set(x,g+4,z,sk,5); set(x,g+5,z,hr,5);
if(pose==='wave'){ set(x+1,g+3,z,sh,5); set(x+1,g+4,z,sk,5); }
if(Math.random()<0.3) set(x,g+5,z,0xd9b45a,6); // straw hat over hair
}
}
person(6,54,'stand'); person(3,52,'sit'); person(-4,49,'wave');
person(8,45,'stand'); person(-12,45,'wave'); person(-6,-2,'stand'); person(-9,-5,'stand');
person(5,2,'stand'); person(-24,-22,'stand'); person(-10,-22,'stand');
person(22,12,'sit'); person(26,64,'stand'); person(36,70,'wave'); person(0,60,'stand');
person(-30,60,'sit'); person(12,52,'stand');
// ------------------------- build the main instanced mesh -------------------
const solid = (x,y,z)=> y<0 ? true : (inB(x,z)&&y<=120 ? V.has(KEY(x,y,z)) : false);
const keepKeys = [];
for(const [k] of V){
const y = (k%256)-64, rest=(k-y-64)/256;
const z = (rest%256)-128, x = (rest-z-128)/256;
if(z===128) continue;
if(!solid(x+1,y,z)||!solid(x-1,y,z)||!solid(x,y,z+1)||!solid(x,y,z-1)||!solid(x,y+1,z)||!solid(x,y-1,z))
keepKeys.push(k);
}
const _col = new THREE.Color();
const voxelGeo = new THREE.BoxGeometry(1,1,1);
const voxelMat = new THREE.MeshLambertMaterial({ color:0xffffff });
const world = new THREE.InstancedMesh(voxelGeo, voxelMat, keepKeys.length);
world.castShadow = world.receiveShadow = true;
world.frustumCulled = false;
const _m = new THREE.Matrix4();
for(let i=0;i<keepKeys.length;i++){
const k=keepKeys[i];
const y=(k%256)-64, rest=(k-y-64)/256;
const z=(rest%256)-128, x=(rest-z-128)/256;
_m.makeTranslation(x,y,z);
world.setMatrixAt(i,_m);
world.setColorAt(i, _col.setHex(V.get(k)).convertSRGBToLinear());
}
scene.add(world);
// ------------------------- water (animated, transparent) -------------------
const waterGeo = new THREE.BoxGeometry(1,0.9,1);
const waterMat = new THREE.MeshLambertMaterial({ color:0xffffff, transparent:true, opacity:0.62 });
const water = new THREE.InstancedMesh(waterGeo, waterMat, waterCells.length/3);
water.receiveShadow = true; water.frustumCulled = false;
water.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
const wBase = new Float32Array(waterCells.length);
for(let i=0;i<waterCells.length;i+=3){
const x=waterCells[i], y=waterCells[i+1], z=waterCells[i+2];
wBase[i]=x; wBase[i+1]=y; wBase[i+2]=z;
water.setColorAt(i/3, _col.setHex(jitC(C.water,12)).convertSRGBToLinear());
}
scene.add(water);
// ------------------------- petals ------------------------------------------
const PET_N = 650;
const petGeo = new THREE.BoxGeometry(0.42,0.1,0.42);
const petMat = new THREE.MeshLambertMaterial({ color:0xffffff });
const petals = new THREE.InstancedMesh(petGeo, petMat, PET_N);
petals.frustumCulled = false;
petals.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
const pet = { x:new Float32Array(PET_N), y:new Float32Array(PET_N), z:new Float32Array(PET_N),
s:new Float32Array(PET_N), ph:new Float32Array(PET_N), r1:new Float32Array(PET_N), r2:new Float32Array(PET_N) };
const PINKS=[0xf7a8c4,0xf39ac0,0xfbc9dc,0xfadfe8];
function petalSpawn(i, top){
pet.x[i] = -45+Math.random()*100; pet.z[i] = -35+Math.random()*62;
pet.y[i] = top ? 26+Math.random()*16 : pet.y[i];
pet.s[i] = 0.9+Math.random()*1.6; pet.ph[i] = Math.random()*Math.PI*2;
pet.r1[i]= (Math.random()-0.5)*3; pet.r2[i]= (Math.random()-0.5)*3;
}
for(let i=0;i<PET_N;i++){ petalSpawn(i,true); pet.y[i]=Math.random()*40+2;
petals.setColorAt(i,_col.setHex(PINKS[i%4]).convertSRGBToLinear()); }
scene.add(petals);
// ------------------------- chimney smoke -----------------------------------
const SM_N = chimneys.length*5;
const smokeGeo = new THREE.BoxGeometry(1,1,1);
const smokeMat = new THREE.MeshLambertMaterial({ color:0xffffff });
const smoke = new THREE.InstancedMesh(smokeGeo, smokeMat, Math.max(SM_N,1));
smoke.frustumCulled = false;
smoke.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
for(let i=0;i<SM_N;i++) smoke.setColorAt(i,_col.setHex(0xf2f2f2).convertSRGBToLinear());
scene.add(smoke);
const smokeA = new THREE.Color(0xf2f2f2).convertSRGBToLinear();
const smokeB = new THREE.Color(0xdcebee).convertSRGBToLinear();
// ------------------------- clouds ------------------------------------------
const clouds = [];
for(let c=0;c<5;c++){
const arr=[];
const cx=-120+c*60+Math.random()*20, cy=58+Math.random()*16, cz=-90+Math.random()*160;
const nb=2+Math.floor(Math.random()*2);
for(let b=0;b<nb;b++){
const bx=cx+(Math.random()-0.5)*22, bz=cz+(Math.random()-0.5)*14, r=4+Math.random()*4;
const R=Math.ceil(r);
for(let dy=-3;dy<=3;dy++) for(let dz=-R;dz<=R;dz++) for(let dx=-R;dx<=R;dx++)
if(dx*dx+(dy/0.42)*(dy/0.42)+dz*dz <= r*r) arr.push([Math.round(bx+dx), Math.round(cy+dy*0.5), Math.round(bz+dz)]);
}
const m = new THREE.InstancedMesh(voxelGeo, new THREE.MeshLambertMaterial({color:0xfbfdfg||0xfbfdfe}), arr.length);
m.castShadow = true; m.frustumCulled = false;
arr.forEach((p,i)=>{ _m.makeTranslation(p[0],p[1],p[2]); m.setMatrixAt(i,_m);
m.setColorAt(i,_col.setHex(jitC(0xfbfdfe,4)).convertSRGBToLinear()); });
m.position.set(0,0,0);
scene.add(m);
clouds.push({ mesh:m, base:0, vx:1.1+Math.random()*0.9, y:cy });
}
// ------------------------- actors: walkers, birds, koi, ducks --------------
function lam(hex){ return new THREE.MeshLambertMaterial({ color:new THREE.Color(hex).convertSRGBToLinear() }); }
function mkBox(w,h,d,mat,x,y,z,parent){
const m=new THREE.Mesh(new THREE.BoxGeometry(w,h,d),mat);
m.position.set(x,y,z); m.castShadow=true; parent.add(m); return m;
}
const walkers=[];
function makeWalker(path, loop, hat){
const g=new THREE.Group();
const sh=pick(SHIRT), pa=pick(PANT), sk=pick(SKIN), hr=pick(HAIR);
const mSh=lam(sh), mPa=lam(pa), mSk=lam(sk), mHr=lam(hr);
const legG=new THREE.BoxGeometry(0.32,0.8,0.32); legG.translate(0,-0.4,0);
const legL=new THREE.Mesh(legG,mPa); legL.position.set(-0.18,0.8,0); legL.castShadow=true; g.add(legL);
const legR=new THREE.Mesh(legG,mPa); legR.position.set(0.18,0.8,0); legR.castShadow=true; g.add(legR);
mkBox(0.78,0.85,0.44,mSh, 0,1.22,0, g);
const armG=new THREE.BoxGeometry(0.16,0.66,0.16); armG.translate(0,-0.28,0);
const armL=new THREE.Mesh(armG,mSh); armL.position.set(-0.47,1.6,0); g.add(armL);
const armR=new THREE.Mesh(armG,mSh); armR.position.set(0.47,1.6,0); g.add(armR);
mkBox(0.6,0.58,0.56,mSk, 0,1.95,0, g);
mkBox(0.64,0.16,0.6,mHr, 0,2.3,0, g);
mkBox(0.64,0.28,0.14,mHr, 0,2.06,-0.26, g);
mkBox(0.09,0.12,0.05,lam(0x241c16), -0.15,1.98,0.29, g);
mkBox(0.09,0.12,0.05,lam(0x241c16), 0.15,1.98,0.29, g);
if(hat){ mkBox(0.72,0.08,0.72,lam(0xd9b45a),0,2.42,0,g); mkBox(0.44,0.16,0.44,lam(0xd9b45a),0,2.5,0,g); }
scene.add(g);
walkers.push({ g, path, loop, i:0, dir:1, ph:Math.random()*9,
sp:1.4+Math.random()*0.7,
x:path[0][0], z:path[0][1] });
}
makeWalker([[2,78],[2,44],[-2,44],[-2,78]], true, false);
makeWalker([[2,8],[2,-2],[-6,-8],[-14,-13]], false, true);
makeWalker([[-3,49],[8,49],[8,56],[-3,56]], true, false);
makeWalker([[18,0],[28,-12],[40,-2],[36,10],[22,10]], true, true);
makeWalker([[16,58],[24,58],[24,74],[16,74]], true, false);
const birds=[];
for(let i=0;i<4;i++){
const g=new THREE.Group();
mkBox(0.36,0.26,0.85, lam(0x33302b), 0,0,0, g);
mkBox(0.26,0.24,0.3, lam(0x33302b), 0,0.12,0.5, g);
const wgl=new THREE.BoxGeometry(1.25,0.06,0.42); wgl.translate(-0.66,0,0);
const wgr=new THREE.BoxGeometry(1.25,0.06,0.42); wgr.translate(0.66,0,0);
const wl=new THREE.Mesh(wgl,lam(0x44403a)); wl.castShadow=true; g.add(wl);
const wr=new THREE.Mesh(wgr,lam(0x44403a)); wr.castShadow=true; g.add(wr);
scene.add(g);
birds.push({ g, wl, wr, cx: i<2?TER.x:(i<3?10:20), cz: i<2?TER.z:(i<3?40:55),
r: 20+i*6, y: 52+i*5, a: Math.random()*7, sp: 0.25+Math.random()*0.2 });
}
const koi=[];
for(let i=0;i<4;i++){
const g=new THREE.Group();
mkBox(0.5,0.26,1.0, lam(i%2?0xff7a26:0xf5f2ea), 0,0,0, g);
mkBox(0.32,0.18,0.4, lam(i%2?0xf5f2ea:0xff7a26), 0,0.02,-0.65, g);
scene.add(g);
koi.push({ g, a:Math.random()*7, r:3.5+i*1.6, sp:0.4+Math.random()*0.3, ph:Math.random()*7 });
}
const ducks=[];
for(let i=0;i<2;i++){
const g=new THREE.Group();
mkBox(0.55,0.3,0.75, lam(0xf5f2ea), 0,0,0, g);
mkBox(0.3,0.3,0.3, lam(0xf5f2ea), 0,0.28,0.42, g);
mkBox(0.12,0.08,0.18, lam(0xe8973a), 0,0.26,0.62, g);
scene.add(g);
ducks.push({ g, a:Math.random()*7, ph:Math.random()*7 });
}
// ------------------------- UI wiring ---------------------------------------
const bOrbit=document.getElementById('bOrbit'), bPetal=document.getElementById('bPetal'), bReset=document.getElementById('bReset');
bOrbit.onclick = ()=>{ controls.autoRotate=!controls.autoRotate; bOrbit.classList.toggle('on',controls.autoRotate); };
bPetal.onclick = ()=>{ petals.visible=!petals.visible; bPetal.classList.toggle('on',petals.visible); };
bReset.onclick = ()=>{ camera.position.copy(CAM_END); controls.target.set(-6,14,-8); };
document.getElementById('stats').innerHTML =
`<b>${keepKeys.length.toLocaleString()}</b> voxels · <b>${waterCells.length/3}</b> water · <b>${PET_N}</b> petals`;
// ------------------------- animation loop ----------------------------------
const _q=new THREE.Quaternion(), _e=new THREE.Euler(), _s=new THREE.Vector3(1,1,1);
let intro = 0, introDone = false;
renderer.domElement.addEventListener('pointerdown', ()=>{ if(!introDone){ intro=1; } });
const clock = new THREE.Clock();
function animate(){
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.05);
const t = clock.elapsedTime;
// camera fly-in
if(!introDone){
intro = Math.min(1, intro + dt/4.2);
const e = 1-Math.pow(1-intro,3);
camera.position.lerpVectors(CAM_START, CAM_END, e);
camera.lookAt(controls.target);
if(intro>=1) introDone=true;
} else controls.update();
// water shimmer
for(let i=0;i<waterCells.length;i+=3){
const x=wBase[i], z=wBase[i+2];
_m.makeTranslation(x, wBase[i+1] + Math.sin(t*1.7 + x*0.35 + z*0.27)*0.1, z);
water.setMatrixAt(i/3,_m);
}
water.instanceMatrix.needsUpdate = true;
// petals
if(petals.visible){
for(let i=0;i<PET_N;i++){
pet.y[i] -= pet.s[i]*dt*2.1;
pet.x[i] += (Math.sin(t*1.3+pet.ph[i])*0.5 + 0.85)*dt*1.6;
pet.z[i] += Math.cos(t*1.1+pet.ph[i])*dt*0.7;
const gy = gAt(pet.x[i], pet.z[i]) + 0.6;
if(pet.y[i] < gy){ petalSpawn(i,true); pet.y[i] = 26+Math.random()*16; }
_p.set(pet.x[i],pet.y[i],pet.z[i]);
_e.set(t*pet.r1[i]+pet.ph[i], t*pet.r2[i], pet.ph[i]);
_q.setFromEuler(_e);
_m.compose(_p,_q,_s.set(1,1,1));
petals.setMatrixAt(i,_m);
}
petals.instanceMatrix.needsUpdate = true;
}
// chimney smoke
for(let i=0;i<SM_N;i++){
const ch = chimneys[(i/5)|0];
const ph = (t*0.14 + (i%5)*0.2) % 1;
const x = ch[0] + Math.sin(ph*5+i)*0.6 + ph*1.4, y = ch[1] + ph*9, z = ch[2] + Math.cos(ph*4+i)*0.4;
const sc = 0.55 + ph*2.3;
_p.set(x,y,z); _q.setFromEuler(_e.set(0,ph*3,0)); _s.set(sc,sc,sc);
_m.compose(_p,_q,_s);
smoke.setMatrixAt(i,_m);
smoke.setColorAt(i, _c2.copy(smokeA).lerp(smokeB, ph*ph));
}
smoke.instanceMatrix.needsUpdate = true;
if(smoke.instanceColor) smoke.instanceColor.needsUpdate = true;
// clouds
for(const c of clouds){
c.base += c.vx*dt;
if(c.base > 240) c.base = -240;
c.mesh.position.set(c.base, 0, 0);
}
// walkers
for(const w of walkers){
const tgt = w.path[w.i];
const dx = tgt[0]-w.x, dz = tgt[1]-w.z, d = Math.hypot(dx,dz);
if(d<0.4){
if(w.loop) w.i = (w.i+1)%w.path.length;
else { if(w.i+ w.dir >= w.path.length || w.i+w.dir < 0) w.dir*=-1; w.i += w.dir; }
} else {
w.x += dx/d*w.sp*dt; w.z += dz/d*w.sp*dt;
w.g.rotation.y = Math.atan2(dx,dz);
}
w.ph += dt*9;
const gy = gAt(Math.round(w.x), Math.round(w.z)) + 0.5;
w.g.position.set(w.x, gy + Math.abs(Math.sin(w.ph))*0.05, w.z);
w.g.rotation.z = Math.sin(w.ph)*0.045;
w.g.children[0].rotation.x = Math.sin(w.ph)*0.6;
w.g.children[1].rotation.x = -Math.sin(w.ph)*0.6;
w.g.children[3].rotation.x = -Math.sin(w.ph)*0.45;
w.g.children[4].rotation.x = Math.sin(w.ph)*0.45;
}
// birds
for(const b of birds){
b.a += b.sp*dt;
const x=b.cx+Math.cos(b.a)*b.r, z=b.cz+Math.sin(b.a)*b.r;
b.g.position.set(x, b.y + Math.sin(t*2+b.r)*1.2, z);
b.g.rotation.y = -b.a;
b.g.rotation.z = 0.25;
const f = Math.sin(t*9+b.r);
b.wl.rotation.z = f*0.55; b.wr.rotation.z = -f*0.55;
}
// koi gliding beneath the pond surface
for(const k of koi){
k.a += k.sp*dt;
k.g.position.set(POND.x+Math.cos(k.a)*k.r, 2.75 + Math.sin(t*1.5+k.ph)*0.08, POND.z+Math.sin(k.a)*k.r);
k.g.rotation.y = -k.a + Math.PI/2;
k.g.rotation.z = Math.sin(t*4+k.ph)*0.1;
}
// ducks
ducks.forEach((d,i)=>{
d.a += dt*0.12;
d.g.position.set(POND.x-5+Math.cos(d.a)*3+i*1.5, 3.55+Math.sin(t*1.6+d.ph)*0.09, POND.z+4+Math.sin(d.a)*3);
d.g.rotation.y = -d.a;
});
renderer.render(scene, camera);
}
const _p=new THREE.Vector3(), _c2=new THREE.Color();
animate();
addEventListener('resize', ()=>{
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
// fade the loader once the first frames are in
requestAnimationFrame(()=>requestAnimationFrame(()=>{
document.getElementById('loader').classList.add('hide');
}));
</script>
</body>
</html>
How it's built:
- Everything is procedural voxels. A ~200×200 height field (value-noise terrain with a rim of mountains, snow caps, a carved stream, pond bowl, flattened village plateau and the pagoda hill) is filled column by column with color-graded strata — grass, dirt, stone, sand beds — then passed through a hidden-face culling pass so only visible voxels reach the GPU. The whole world renders as one
InstancedMesh(with per-instance color and slight per-voxel luminance jitter for that hand-placed look), so the scene stays at a handful of draw calls. - The pagoda is a real structure: five tiers with vermillion posts, cream walls, lattice-lit windows, balconies, upswept corner tips with golden finials and bells, and a ringed golden spire — on a checkered stone plaza with parapet, carved stairway, and a raked sand garden in one corner.
- The garden has a red torii gate, an arched footbridge over the stream, stone lanterns, fences, flower patches, ponds ringed by a gravel path, and ~20 cherry trees (hand-placed avenues plus scattered ones) that continuously shed 650 animated petals on the wind.
- The village lives: ten varied houses with gabled roofs, glowing lattice windows, red door lanterns and smoking chimneys (animated voxel smoke); a well, market stalls with striped awnings, crates, barrels, a clothesline, rice paddies with sprouts, and five villagers actually walking their routes (with swinging legs and arms), plus static folk chatting, waving and sitting around.
- Ambient life everywhere: shimmering transparent water with koi gliding beneath it, bobbing ducks, four birds circling the pagoda, and drifting voxel clouds that cast real moving shadows across the valley.
- Interaction: a cinematic fly-in on load, then full orbit/zoom/pan with damping and slow auto-orbit, plus toggles for petals, auto-rotate and a view reset.
Just save it as an .html file and open it — no build step, everything runs locally.
计时详情
- 实际等待(客户端)
- 6.62s
- 客户端请求准备
- 0.01s
- 本站代理准备
- 0.00s
- 上游响应头到达
- 6.36s
- 上游首字节到达
- 6.36s
- 上游首 Token 到达
- 6.36s
- 上游重试次数
- 0
实际等待使用客户端时钟;上游计时从本站准备完成后开始,各项为累计耗时,不能直接相加。上游等待包含网络、调度、计算及缓冲,不能仅凭这些数值断定厂商排队。
诊断记录
{
"attempts": 1,
"startedAt": "2026-09-28T07:30:37.299Z",
"transport": "railway",
"ttftSource": "proxy",
"clientTtftMs": 6617.299999713898,
"completionId": "20260928153037f5d4127c428f4644",
"streamResumes": 25,
"proxyPrepareMs": 0,
"proxyRequestId": "etiZaQlYTeO8kWxw9o6EoQ",
"upstreamStatus": 200,
"upstreamTtftMs": 6359,
"clientHeadersMs": 296.69999980926514,
"clientPrepareMs": 13.099999904632568,
"upstreamHeadersMs": 6358,
"upstreamFirstByteMs": 6359,
"clientFirstContentMs": 890490
}历史结果 · 2026/9/28 07:53:39
Here's a single HTML file with an elaborate voxel art scene featuring a pagoda, garden, cherry blossoms, village, and more:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Voxel Pagoda Garden</title>
<style>
*{margin:0;padding:0}body{overflow:hidden;background:#1a1a2e}canvas{display:block}
#ui{position:absolute;top:18px;left:50%;transform:translateX(-50%);color:rgba(255,255,255,.7);font:600 15px/1.5 'Segoe UI',sans-serif;text-shadow:0 2px 8px rgba(0,0,0,.6);pointer-events:none;letter-spacing:3px;white-space:nowrap}
#hint{position:absolute;bottom:18px;left:50%;transform:translateX(-50%);color:rgba(255,255,255,.3);font:12px sans-serif;text-shadow:0 1px 4px rgba(0,0,0,.6);pointer-events:none}
</style>
</head>
<body>
<div id="ui">⛩ VOXEL PAGODA GARDEN ⛩</div>
<div id="hint">Drag to rotate • Scroll to zoom • Right-click to pan</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
const scene=new THREE.Scene;
scene.background=new THREE.Color(0x87CEEB);
scene.fog=new THREE.FogExp2(0x94B8D4,.0018);
const camera=new THREE.PerspectiveCamera(55,innerWidth/innerHeight,.1,600);
const renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth,innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0x667799,.45));
scene.add(new THREE.HemisphereLight(0x88BBFF,0x446633,.35));
const sun=new THREE.DirectionalLight(0xFFF4D6,1.3);
sun.position.set(80,150,60);
sun.castShadow=true;
sun.shadow.mapSize.set(4096,4096);
const sc=sun.shadow.camera;sc.left=-140;sc.right=140;sc.top=140;sc.bottom=-140;sc.near=1;sc.far=400;
sun.shadow.bias=-.001;scene.add(sun);
const fill=new THREE.DirectionalLight(0x8899BB,.3);fill.position.set(-50,80,-40);scene.add(fill);
let theta=.7,phi=.42,radius=140;
const target=new THREE.Vector3(0,18,0);
let mL=false,mR=false,lX=0,lY=0;
function updateCam(){camera.position.set(target.x+radius*Math.cos(phi)*Math.sin(theta),target.y+radius*Math.sin(phi),target.z+radius*Math.cos(phi)*Math.cos(theta));camera.lookAt(target)}
updateCam();
renderer.domElement.onmousedown=e=>{if(e.button==0)mL=true;if(e.button==2)mR=true;lX=e.clientX;lY=e.clientY};
addEventListener('mouseup',()=>{mL=mR=false});
addEventListener('mousemove',e=>{const dx=e.clientX-lX,dy=e.clientY-lY;if(mL){theta-=dx*.005;phi=Math.max(.05,Math.min(1.4,phi+dy*.005));updateCam()}if(mR){const fw=new THREE.Vector3().subVectors(target,camera.position).normalize();const rt=new THREE.Vector3().crossVectors(fw,camera.up).normalize();target.addScaledVector(rt,dx*.12).addScaledVector(camera.up,dy*.12);updateCam()}lX=e.clientX;lY=e.clientY});
renderer.domElement.onwheel=e=>{radius=Math.max(30,Math.min(350,radius+e.deltaY*.08));updateCam()};
renderer.domElement.oncontextmenu=e=>e.preventDefault();
let td=0;
renderer.domElement.ontouchstart=e=>{if(e.touches.length==1){lX=e.touches[0].clientX;lY=e.touches[0].clientY}else if(e.touches.length==2)td=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY)};
renderer.domElement.ontouchmove=e=>{e.preventDefault();if(e.touches.length==1){const dx=e.touches[0].clientX-lX,dy=e.touches[0].clientY-lY;theta-=dx*.005;phi=Math.max(.05,Math.min(1.4,phi+dy*.005));lX=e.touches[0].clientX;lY=e.touches[0].clientY;updateCam()}else if(e.touches.length==2){const d=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);radius=Math.max(30,Math.min(350,radius-(d-td)*.4));td=d;updateCam()}},{passive:false});
const C={
pw:[.7,.12,.12],pw2:[.88,.84,.75],pr:[.15,.2,.26],pg:[.85,.7,.15],pgb:[1,.82,.28],
ps:[.55,.52,.48],pw3:[.92,.78,.35],pdoor:[.35,.2,.1],
tk:[.35,.2,.1],lf:[.08,.42,.1],lf2:[.15,.55,.15],lf3:[.22,.65,.18],lf4:[.3,.72,.15],
ck:[.3,.18,.1],cp1:[1,.65,.75],cp2:[1,.8,.86],cp3:[.95,.5,.65],cp4:[1,.72,.8],
hw:[.82,.75,.55],hwd:[.5,.35,.2],hdd:[.35,.22,.12],
hr1:[.3,.3,.35],hr2:[.45,.28,.15],hr3:[.22,.28,.35],
hd:[.3,.2,.1],hwin:[.92,.82,.35],
g1:[.18,.6,.18],g2:[.22,.66,.2],g3:[.15,.55,.15],g4:[.26,.7,.22],
dt:[.48,.35,.2],st:[.5,.48,.45],st2:[.58,.56,.52],pt:[.62,.55,.42],
w1:[.2,.5,.85],w2:[.15,.4,.75],w3:[.3,.6,.95],w4:[.4,.7,.92],
sk:[.9,.72,.55],hb:[.1,.08,.08],
ln:[.38,.38,.4],ll:[1,.9,.5],
f1:[.9,.15,.2],f2:[.95,.9,.15],f3:[.55,.2,.7],f4:[.3,.35,.9],f5:[.9,.5,.1],fs:[.2,.5,.1],
rk:[.48,.48,.5],rk2:[.55,.55,.57],
br:[.58,.35,.15],brd:[.48,.28,.1],
tr:[.78,.18,.12],bm:[.3,.6,.2],bm2:[.38,.7,.25],fc:[.5,.4,.28],
mt:[.32,.38,.32],mt2:[.28,.34,.28],snow:[.9,.92,.95],cl:[.95,.95,.98],
s1:[.85,.15,.15],s2:[.15,.3,.8],s3:[.15,.65,.2],s4:[.9,.8,.15],
s5:[.5,.2,.7],s6:[.9,.5,.1],s7:[.9,.4,.6],s8:[.9,.9,.88],
p1:[.2,.2,.25],p2:[.2,.3,.5],p3:[.5,.5,.5],p4:[.45,.3,.2]
};
const pos=[],nor=[],col=[];
function addBox(x,y,z,w,h,d,c){
const cR=c[0],cG=c[1],cB=c[2];
const x0=x,x1=x+w,y0=y,y1=y+h,z0=z,z1=z+d;
function t(a,b,c2,d2,e,f,g,h2,i,nx,ny,nz){
pos.push(a,b,c2,d2,e,f,g,h2,i);nor.push(nx,ny,nz,nx,ny,nz,nx,ny,nz);
col.push(cR,cG,cB,cR,cG,cB,cR,cG,cB);
}
t(x0,y0,z1,x1,y0,z1,x1,y1,z1,0,0,1);t(x0,y0,z1,x1,y1,z1,x0,y1,z1,0,0,1);
t(x1,y0,z0,x0,y0,z0,x0,y1,z0,0,0,-1);t(x1,y0,z0,x0,y1,z0,x1,y1,z0,0,0,-1);
t(x1,y0,z1,x1,y0,z0,x1,y1,z0,1,0,0);t(x1,y0,z1,x1,y1,z0,x1,y1,z1,1,0,0);
t(x0,y0,z0,x0,y0,z1,x0,y1,z1,-1,0,0);t(x0,y0,z0,x0,y1,z1,x0,y1,z0,-1,0,0);
t(x0,y1,z1,x1,y1,z1,x1,y1,z0,0,1,0);t(x0,y1,z1,x1,y1,z0,x0,y1,z0,0,1,0);
t(x0,y0,z0,x1,y0,z0,x1,y0,z1,0,-1,0);t(x0,y0,z0,x1,y0,z1,x0,y0,z1,0,-1,0);
}
function V(x,y,z,c){addBox(x,y,z,1,1,1,c)}
// ---- GROUND ----
for(let gx=-130;gx<130;gx+=10)for(let gz=-130;gz<130;gz+=10){
const ci=Math.floor(Math.random()*4);
addBox(gx,-3,gz,10,3,10,[C.g1,C.g2,C.g3,C.g4][ci]);
}
// ---- PAGODA ----
function pagodaRoof(cx,cz,y,sz,trim,rc){
const x=cx-Math.floor(sz/2),z=cz-Math.floor(sz/2),w=sz,d=sz;
addBox(x,y,z,w,3,d,rc);
addBox(x,y+3,z,w,1,1,trim);addBox(x,y+3,z+d-1,w,1,1,trim);
addBox(x,y+3,z,1,1,d,trim);addBox(x+w-1,y+3,z,1,1,d,trim);
const cs=[[x,z,-1,-1],[x+w-1,z,1,-1],[x,z+d-1,-1,1],[x+w-1,z+d-1,1,1]];
for(const[ex,ez,dx,dz]of cs)for(let h=1;h<=4;h++)addBox(ex+dx*h,y+3+h,ez+dz*h,1,1,1,trim);
}
function pagodaLevel(cx,cz,y,sz,h,wc,pic,trim){
const x=cx-Math.floor(sz/2),z=cz-Math.floor(sz/2),cs=sz-1;
addBox(x,y,z,sz,1,sz,pic);
addBox(x,y+1,z,1,h-1,1,pic);addBox(x+cs,y+1,z,1,h-1,1,pic);
addBox(x,y+1,z+cs,1,h-1,1,pic);addBox(x+cs,y+1,z+cs,1,h-1,1,pic);
addBox(x+1,y+1,z,sz-2,h-1,1,wc);addBox(x+1,y+1,z+cs,sz-2,h-1,1,wc);
addBox(x,y+1,z+1,1,h-1,sz-2,wc);addBox(x+cs,y+1,z+1,1,h-1,sz-2,wc);
if(sz>=8){const wy=y+Math.floor(h/2)-1,wh=Math.max(2,h-4),sp=Math.floor(sz/4);
for(let i=1;i<4;i++){const wx=x+Math.floor(i*sz/4)-1;
if(wx>x&&wx+2<x+cs){addBox(wx,wy,z,2,wh,1,C.pw3);addBox(wx,wy,z+cs,2,wh,1,C.pw3)}
const wz=z+Math.floor(i*sz/4)-1;
if(wz>z&&wz+2<z+cs){addBox(x,wy,wz,1,wh,2,C.pw3);addBox(x+cs,wy,wz,1,wh,2,C.pw3)}
}
}
if(sz>=15)addBox(cx-2,y+1,z+cs,4,h-2,1,C.pdoor);
const ov=Math.max(2,Math.floor(sz/5));
pagodaRoof(cx,cz,y+h,sz+ov*2,trim,C.pr);
}
(function buildPagoda(){
const cx=0,cz=0;
addBox(-18,-4,-18,36,5,36,C.st);
addBox(-18,1,-18,36,1,1,C.st2);addBox(-18,1,17,36,1,1,C.st2);
addBox(-18,1,-18,1,1,36,C.st2);addBox(17,1,-18,1,1,36,C.st2);
for(let s=0;s<3;s++)addBox(-5,s,19+s,10,1,1,C.st2);
let y=1;
pagodaLevel(cx,cz,y,20,9,C.pw,C.pw,C.pg);y+=13;
pagodaLevel(cx,cz,y,15,7,C.pw2,C.pw,C.pg);y+=11;
pagodaLevel(cx,cz,y,11,6,C.pw,C.pw,C.pg);y+=10;
pagodaLevel(cx,cz,y,8,5,C.pw2,C.pw,C.pg);y+=9;
pagodaLevel(cx,cz,y,6,4,C.pw,C.pw,C.pg);y+=8;
for(let i=0;i<8;i++)V(cx,y+i,cz,i%2?C.pg:C.pgb);
addBox(cx-1,y+2,cz-1,3,1,3,C.pgb);addBox(cx-1,y+5,cz-1,3,1,3,C.pgb);
})();
// ---- POND ----
(function buildPond(){
const px=-14,pz=22,pw=26,pd=18;
addBox(px-1,-1,pz-1,pw+2,1,1,C.dt);addBox(px-1,-1,pz+pd,pw+2,1,1,C.dt);
addBox(px-1,-1,pz,1,1,pd,C.dt);addBox(px+pw,-1,pz,1,1,pd,C.dt);
for(let i=px;i<px+pw;i++)for(let j=pz;j<pz+pd;j++)V(i,-2,j,[C.w1,C.w3,C.w4,C.w2][Math.floor(Math.random()*4)]);
for(let i=0;i<6;i++)V(px+3+Math.floor(Math.random()*(pw-6)),-1,pz+3+Math.floor(Math.random()*(pd-6)),C.lf3);
for(let i=0;i<8;i++)V(px+3+i*3,0,pz+Math.floor(pd/2)-1+Math.floor(Math.random()*3),C.st2);
V(px+5,-1,pz+5,C.f5);V(px+6,-1,pz+5,C.f5);V(px+5,-1,pz+6,C.wh);
V(px+16,-1,pz+8,C.wh);V(px+17,-1,pz+8,C.f1);
})();
// ---- BRIDGE ----
(function buildBridge(){
const bx=-4,bz=20,bw=10,bd=4;
for(let i=0;i<bw;i++){
const ah=Math.round(Math.sin(i/(bw-1)*Math.PI)*2);
addBox(bx+i,3+ah,bz,1,1,bd,C.br);addBox(bx+i,2+ah,bz,1,1,bd,C.brd);
addBox(bx+i,4+ah,bz,1,2,1,C.brd);addBox(bx+i,4+ah,bz+bd-1,1,2,1,C.brd);
}
for(let i=0;i<bw;i+=3){const ah=Math.round(Math.sin(i/(bw-1)*Math.PI)*2);addBox(bx+i,6+ah,bz,1,1,1,C.brd)}
})();
// ---- TORII ----
(function(){
const tx=0,tz=44;
addBox(tx-6,0,tz,2,16,2,C.tr);addBox(tx+4,0,tz,2,16,2,C.tr);
addBox(tx-8,16,tz-1,18,2,4,C.tr);addBox(tx-6,12,tz-1,14,2,3,C.tr);
addBox(tx-1,12,tz,3,4,2,C.tr);
})();
// ---- PATHS ----
(function(){
for(let z=18;z<44;z++)addBox(-2,0,z,5,1,1,C.pt);
for(let x=18;x<50;x++)addBox(x,0,-3,1,1,5,C.pt);
for(let x=-22;x<22;x++){addBox(x,0,-22,1,1,4,C.pt);addBox(x,0,22,1,1,4,C.pt)}
for(let z=-22;z<22;z++){addBox(-22,0,z,4,1,1,C.pt);addBox(18,0,z,4,1,1,C.pt)}
for(let i=0;i<14;i++)V(-16+i*3,0,16+Math.floor(Math.sin(i*.8)*2),C.st2);
})();
// ---- LANTERNS ----
function lantern(x,z){addBox(x-1,0,z-1,3,2,3,C.ln);addBox(x,2,z,1,4,1,C.ln);addBox(x-1,6,z-1,3,2,3,C.ll);addBox(x-1,8,z-1,3,1,3,C.ln);addBox(x,9,z,1,2,1,C.ln);V(x,11,z,C.ln)}
[[0,38],[0,50],[-5,26],[10,26],[20,-3],[38,-3],[-20,-16],[15,16],[-10,16],[18,10],[-16,-22],[25,16],[-26,0],[30,-16],[8,30],[-8,32],[14,-18],[42,14],[56,-8],[-34,12]].forEach(([lx,lz])=>lantern(lx,lz));
// ---- FENCE ----
(function(){
const r=32;
for(let i=-r;i<r;i+=5){
if(Math.abs(i)>4){V(i,0,r,C.fc);V(i,1,r,C.fc);V(i,2,r,C.fc);if(i+4<r)addBox(i+1,1,r,4,1,1,C.fc)}
V(i,0,-r,C.fc);V(i,1,-r,C.fc);V(i,2,-r,C.fc);if(i+4<r)addBox(i+1,1,-r,4,1,1,C.fc);
V(-r,0,i,C.fc);V(-r,1,i,C.fc);V(-r,2,i,C.fc);if(i+4<r)addBox(-r,1,i+1,1,1,4,C.fc);
V(r,0,i,C.fc);V(r,1,i,C.fc);V(r,2,i,C.fc);if(i+4<r)addBox(r,1,i+1,1,1,4,C.fc);
}
})();
// ---- CHERRY TREES ----
function cherryTree(x,z,h){
for(let i=0;i<h;i++)addBox(x,i,z,2,1,2,C.ck);
for(let b=0;b<Math.floor(Math.random()*3)+2;b++){const dirs=[[-1,0],[1,0],[0,-1],[0,1]][b%4];
for(let i=1;i<=Math.floor(Math.random()*3)+2;i++)V(x+dirs[0]*i,h-2+Math.floor(i*.5),z+dirs[1]*i,C.ck)}
const cy=h,cr=Math.floor(h/2)+3;
for(let dx=-cr;dx<=cr;dx++)for(let dz=-cr;dz<=cr;dz++)for(let dy=0;dy<=cr;dy++){
const dist=Math.sqrt(dx*dx+dz*dz+dy*dy*1.5);if(dist<cr){
const sd=cr-dist,prob=sd<2?.65:.12;if(Math.random()<prob)
V(x+dx,cy+dy,z+dz,[C.cp1,C.cp2,C.cp3,C.cp4][Math.floor(Math.random()*4)]);
}}
}
[[-16,-10],[15,-14],[-12,20],[18,18],[-8,34],[10,36],[-25,-6],[22,-8],[-30,10],[28,-18]].forEach(([tx,tz])=>cherryTree(tx,tz,9+Math.floor(Math.random()*5)));
// ---- GREEN TREES ----
function greenTree(x,z,h){
for(let i=0;i<h;i++)addBox(x,i,z,2,1,2,C.tk);
for(let b=0;b<Math.floor(Math.random()*3)+2;b++){const dirs=[[-1,0],[1,0],[0,-1],[0,1]][b%4];
for(let i=1;i<=Math.floor(Math.random()*3)+2;i++)V(x+dirs[0]*i,h-2+Math.floor(i*.5),z+dirs[1]*i,C.tk)}
const cy=h,cr=Math.floor(h/2)+2;
for(let dx=-cr;dx<=cr;dx++)for(let dz=-cr;dz<=cr;dz++)for(let dy=-1;dy<=cr;dy++){
const dist=Math.sqrt(dx*dx+dz*dz+dy*dy*1.4);if(dist<cr){
const sd=cr-dist,prob=sd<2?.6:.1;if(Math.random()<prob)
V(x+dx,cy+dy,z+dz,[C.lf,C.lf2,C.lf3,C.lf4][Math.floor(Math.random()*4)]);
}}
}
[[-42,-25],[44,-30],[-46,32],[48,36],[-55,5],[52,10],[-16,-52],[16,-48],[60,-8],[-60,18],[36,50],[-36,46],[0,-58],[66,25],[-66,-10]].forEach(([tx,tz])=>greenTree(tx,tz,12+Math.floor(Math.random()*7)));
// ---- BAMBOO ----
(function(){
const gx=-48,gz=2;
for(let i=0;i<18;i++){const bx=gx+Math.floor(Math.random()*8),bz=gz+Math.floor(Math.random()*18);
const h=13+Math.floor(Math.random()*10);
for(let j=0;j<h;j++)V(bx,j,bz,j%3?C.bm:C.bm2);
V(bx,h,bz,C.bm);V(bx+1,h-1,bz,C.bm);V(bx-1,h-1,bz,C.bm);V(bx,h,bz+1,C.bm);V(bx,h,bz-1,C.bm);
V(bx+1,h,bz+1,C.bm);V(bx-1,h,bz-1,C.bm);
}
})();
// ---- VILLAGE HOUSES ----
function house(x,z,w,d,h,rc,wc){
addBox(x,0,z,w,h,1,wc);addBox(x,0,z+d-1,w,h,1,wc);
addBox(x,0,z+1,1,h,d-2,wc);addBox(x+w-1,0,z+1,1,h,d-2,wc);
addBox(x,0,z,w,1,d,C.hdd);
const rY=h,rH=Math.floor(d/2)+2;
for(let rz=0;rz<d;rz++){const layer=Math.min(rz,d-1-rz);if(layer<rH)addBox(x-1,rY+layer,rz+z,w+2,1,1,rc)}
addBox(x-1,rY+rH,z+Math.floor(d/2)-1,w+2,1,2,rc);
const dx2=x+Math.floor(w/2)-1;addBox(dx2,1,z+d-1,3,h-1,1,C.hd);
if(w>=6){addBox(x+1,Math.floor(h/3),z+d-1,2,2,1,C.hwin);addBox(x+w-3,Math.floor(h/3),z+d-1,2,2,1,C.hwin);
addBox(x-1,Math.floor(h/3),z+Math.floor(d/3),1,2,2,C.hwin);addBox(x+w,Math.floor(h/3),z+Math.floor(d/3),1,2,2,C.hwin)}
addBox(dx2,0,z+d,3,1,1,C.dt);
}
[[32,-16,10,8,6,C.hr1,C.hw],[48,-12,8,10,5,C.hr2,C.hwd],[36,4,12,8,7,C.hr3,C.hw],
[54,4,8,8,5,C.hr1,C.hw],[42,20,10,10,6,C.hr2,C.hwd],[58,-22,8,6,5,C.hr3,C.hw],
[62,12,9,8,6,C.hr1,C.hw],[34,-32,8,7,5,C.hr2,C.hw]].forEach(h2=>house(...h2));
// ---- PEOPLE ----
function person(x,z,shirt,pants){
V(x,5,z,C.sk);V(x,6,z,C.hb);
V(x,4,z,shirt);V(x,3,z,shirt);
V(x-1,4,z,shirt);V(x+1,4,z,shirt);V(x-1,3,z,C.sk);V(x+1,3,z,C.sk);
V(x,2,z,pants);V(x,1,z,pants);V(x,0,z,pants);
}
[[5,30,C.s1,C.p1],[-3,38,C.s2,C.p2],[18,-3,C.s3,C.p3],[30,-3,C.s4,C.p1],
[38,-12,C.s5,C.p3],[48,-12,C.s6,C.p2],[-8,18,C.s7,C.p1],[10,12,C.s8,C.p3],
[-22,15,C.s1,C.p4],[5,-28,C.s2,C.p1],[44,16,C.s3,C.p3],[52,0,C.s4,C.p2],
[-12,-20,C.s5,C.p1],[2,-20,C.s7,C.p3],[-20,-26,C.s6,C.p4],[28,10,C.s1,C.p2],
[0,40,C.s2,C.p1],[50,20,C.s8,C.p3],[-5,8,C.s3,C.p4],[15,-8,C.s4,C.p1],
[56,-16,C.s5,C.p2],[34,0,C.s7,C.p3],[-26,-8,C.s1,C.p1],[8,36,C.s2,C.p4],
[-16,0,C.s6,C.p3],[48,-28,C.s3,C.p1],[64,0,C.s4,C.p2],[40,22,C.s5,C.p4],
[62,-6,C.s7,C.p1],[12,-38,C.s8,C.p3]].forEach(([px,pz,sh,pn])=>person(px,pz,sh,pn));
// ---- FLOWERS ----
function flower(x,z,c){V(x,0,z,C.fs);V(x,1,z,c);V(x,1,z+1,c);V(x+1,1,z,c);V(x-1,0,z,C.lf2);V(x+1,0,z,C.lf3)}
[[-14,16,C.f1],[-8,16,C.f2],[0,16,C.f3],[8,16,C.f4],[14,16,C.f5],
[-14,-16,C.f2],[-6,-16,C.f1],[6,-16,C.f4],[14,-16,C.f3],
[22,18,C.f5],[26,18,C.f2],[-22,18,C.f1],[-26,18,C.f4],
[20,-14,C.f3],[24,-20,C.f5],[-20,-14,C.f2],[-24,-20,C.f1],
[32,14,C.f4],[40,-3,C.f5],[48,-3,C.f2],[6,38,C.f1],[-6,38,C.f3],
[36,26,C.f4],[44,24,C.f5],[52,14,C.f2],[-38,0,C.f3],[0,-42,C.f1]
].forEach(([fx,fz,fc])=>flower(fx,fz,fc));
// ---- ROCKS ----
[[-10,22,2],[16,20,3],[-26,24,2],[22,-16,3],[-32,-20,2],[10,-24,2],
[-16,-32,3],[26,26,2],[-5,27,1],[8,30,2],[-22,12,2],[20,14,1],
[-30,6,2],[34,-12,2],[-38,16,3],[42,26,2],[5,-42,2],[-8,-44,3],
[14,32,1],[-14,34,2],[36,32,3],[-36,32,2],[-16,26,1],[28,-6,2]
].forEach(([rx,rz,sz])=>{const c=Math.random()>.5?C.rk:C.rk2;
for(let i=0;i<sz;i++)for(let j=0;j<sz;j++)for(let k=0;k<sz;k++)if(Math.random()>.3||k===0)V(rx+i,k,rz+j,c)});
// ---- MOUNTAINS ----
[[-70,-80,35,45],[-35,-85,42,50],[0,-82,52,55],[35,-85,45,50],[70,-80,38,45],
[-110,-75,32,40],[110,-75,35,40],[-55,-70,28,35],[55,-70,30,38],
[0,-95,58,62],[-95,-90,40,48],[95,-90,42,46],[-135,-85,28,35],[135,-85,30,36]
].forEach(([mx,mz,mh,mw])=>{
const nl=Math.floor(mh/3);
for(let L=0;L<nl;L++){const t2=L/nl,hw=Math.floor(mw*(1-t2*.9));if(hw<=0)continue;
const y=L*3-3,snowy=t2>.65,co=L%2;
addBox(mx-hw,y,mz-hw,hw*2,3,hw*2,snowy?C.snow:(co?C.mt:C.mt2));
for(let e=0;e<Math.min(hw/3,6);e++){const a=(e/6)*Math.PI*2;
V(mx+Math.floor(Math.cos(a)*hw),y,mz+Math.floor(Math.sin(a)*hw),snowy?C.snow:(co?C.mt2:C.mt));
V(mx+Math.floor(Math.cos(a)*hw),y+2,mz+Math.floor(Math.sin(a)*hw),snowy?C.snow:(co?C.mt2:C.mt))
}}
});
// ---- CLOUDS ----
[[-30,55,-40,15],[40,60,-30,12],[0,65,-50,18],[-50,58,20,10],[30,55,30,14],
[-20,62,45,16],[60,50,-60,11],[-70,55,-10,13],[15,68,-70,15],[-45,64,-65,12],
[50,62,50,17],[-60,58,45,14],[80,56,10,12],[-80,60,30,10],[0,72,60,14]
].forEach(([cx,cy,cz,s])=>{
for(let dx=-s;dx<=s;dx++)for(let dz=-Math.floor(s/2);dz<=Math.floor(s/2);dz++){
const dist=(dx*dx)/(s*s)+(dz*dz)/((s/2)*(s/2));
if(dist<=1&&Math.random()>.2){addBox(cx+dx,cy,cz+dz,1,1,1,C.cl);
if(Math.random()>.5)addBox(cx+dx,cy+1,cz+dz,1,1,1,C.cl)}
}
});
// ---- ZEN GARDEN ----
(function(){
const gx=-30,gz=-12,w=16,d=12;
addBox(gx,0,gz,w,1,d,[.85,.8,.65]);
for(let z2=1;z2<d-1;z2+=2)addBox(gx+1,1,gz+z2,w-2,0.1,1,[.9,.85,.7]);
[[3,3],[4,3],[3,4]].forEach(([dx,dz])=>{V(gx+dx,1,gz+dz,C.rk)});
[[8,5],[9,5],[9,4]].forEach(([dx,dz])=>{V(gx+dx,1,gz+dz,C.rk2)});
[[13,2],[13,3]].forEach(([dx,dz])=>{V(gx+dx,1,gz+dz,C.rk)});
V(gx+1,1,gz+9,C.lf2);V(gx+2,1,gz+9,C.lf3);V(gx+1,1,gz+10,C.lf);
V(gx+14,1,gz+10,C.lf3);V(gx+13,1,gz+10,C.lf);
})();
// ---- WELL ----
(function(){
const wx=44,wz=14;
addBox(wx-2,0,wz-2,5,3,5,C.rk);
addBox(wx,1,wz,1,1,1,C.w1);addBox(wx+1,1,wz,1,1,1,C.w2);
addBox(wx,1,wz+1,1,1,1,C.w2);addBox(wx+1,1,wz+1,1,1,1,C.w1);
addBox(wx-2,3,wz-2,1,4,1,C.hdd);addBox(wx+3,3,wz+3,1,4,1,C.hdd);
addBox(wx-3,7,wz-3,7,1,7,C.hr2);
addBox(wx,5,wz+3,1,3,1,[.7,.6,.4]);V(wx,4,wz+3,C.hdd);
})();
// ---- MARKET STALL ----
(function(){
const mx=54,mz=-4;
addBox(mx,0,mz,1,5,1,C.hdd);addBox(mx+5,0,mz,1,5,1,C.hdd);
addBox(mx,0,mz+4,1,5,1,C.hdd);addBox(mx+5,0,mz+4,1,5,1,C.hdd);
addBox(mx-1,5,mz-1,8,1,7,C.s1);addBox(mx-1,6,mz-1,8,1,7,[.9,.9,.88]);
addBox(mx+1,0,mz+2,4,3,1,C.hwd);
V(mx+1,3,mz+2,C.f1);V(mx+2,3,mz+2,C.f2);V(mx+3,3,mz+2,C.f5);V(mx+4,3,mz+2,C.f3);
})();
// ---- SMALL SHRINE ----
(function(){
const sx=34,sz=-28;
addBox(sx-3,0,sz-3,7,4,7,C.hw);
addBox(sx-3,0,sz-3,1,5,1,C.tr);addBox(sx+3,0,sz-3,1,5,1,C.tr);
addBox(sx-3,0,sz+3,1,5,1,C.tr);addBox(sx+3,0,sz+3,1,5,1,C.tr);
for(let r=0;r<4;r++)addBox(sx-3-r,4+r,sz-3-r,7+r*2,1,7+r*2,C.hr2);
addBox(sx,0,sz+4,2,2,1,C.hdd);
})();
// ---- ASSEMBLE MESH ----
const geo=new THREE.BufferGeometry();
geo.setAttribute('position',new THREE.Float32BufferAttribute(pos,3));
geo.setAttribute('normal',new THREE.Float32BufferAttribute(nor,3));
geo.setAttribute('color',new THREE.Float32BufferAttribute(col,3));
geo.computeBoundingSphere();
const mat=new THREE.MeshPhongMaterial({vertexColors:true,shininess:15,specular:new THREE.Color(0x333333)});
const mesh=new THREE.Mesh(geo,mat);
mesh.castShadow=true;mesh.receiveShadow=true;
scene.add(mesh);
// ---- CHERRY BLOSSOM PARTICLES ----
const pCount=400;
const pGeo=new THREE.BufferGeometry();
const pPos=new Float32Array(pCount*3);
const pVel=[];
for(let i=0;i<pCount;i++){
pPos[i*3]=(Math.random()-.5)*90;
pPos[i*3+1]=Math.random()*55;
pPos[i*3+2]=(Math.random()-.5)*90;
pVel.push({x:(Math.random()-.5)*.02,y:-(.015+Math.random()*.04),z:(Math.random()-.5)*.02});
}
pGeo.setAttribute('position',new THREE.BufferAttribute(pPos,3));
const pMat=new THREE.PointsMaterial({color:0xffb7c5,size:.5,transparent:true,opacity:.75,sizeAttenuation:true});
const petals=new THREE.Points(pGeo,pMat);
scene.add(petals);
// ---- ANIMATION ----
const clock=new THREE.Clock();
function animate(){
requestAnimationFrame(animate);
const t=clock.getElapsedTime();
const pA=pGeo.attributes.position.array;
for(let i=0;i<pCount;i++){
pA[i*3]+=pVel[i].x+Math.sin(t*1.5+i*.7)*.008;
pA[i*3+1]+=pVel[i].y;
pA[i*3+2]+=pVel[i].z+Math.cos(t*1.2+i*.5)*.008;
if(pA[i*3+1]<0){pA[i*3]=(Math.random()-.5)*90;pA[i*3+1]=48+Math.random()*12;pA[i*3+2]=(Math.random()-.5)*90}
}
pGeo.attributes.position.needsUpdate=true;
renderer.render(scene,camera);
}
animate();
addEventListener('resize',()=>{
camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
});
</script>
</body>
</html>
计时详情
- 实际等待(客户端)
- 1.92s
- 客户端请求准备
- 0.01s
- 本站代理准备
- 0.00s
- 上游响应头到达
- 1.63s
- 上游首字节到达
- 1.63s
- 上游首 Token 到达
- 1.71s
- 上游重试次数
- 0
实际等待使用客户端时钟;上游计时从本站准备完成后开始,各项为累计耗时,不能直接相加。上游等待包含网络、调度、计算及缓冲,不能仅凭这些数值断定厂商排队。
诊断记录
{
"attempts": 1,
"startedAt": "2026-09-28T07:30:37.293Z",
"transport": "railway",
"ttftSource": "proxy",
"clientTtftMs": 1917.0999999046326,
"completionId": "1f39913e-9fdd-4c2b-acff-5becf46f9abb_70d7789b8a394ddb8bb37000f0f00697",
"streamResumes": 30,
"proxyPrepareMs": 0,
"proxyRequestId": "gxXHPAtrT-qpVbQA0_TJvA",
"upstreamStatus": 200,
"upstreamTtftMs": 1707,
"clientHeadersMs": 296.40000009536743,
"clientPrepareMs": 13.099999904632568,
"upstreamHeadersMs": 1629,
"upstreamFirstByteMs": 1630,
"clientFirstContentMs": 1285671.5999999046
}历史结果 · 2026/9/28 07:53:39
计时详情
- 客户端请求准备
- 0.01s
- 本站代理准备
- 0.00s
- 上游响应头到达
- 0.18s
- 上游重试次数
- 0
实际等待使用客户端时钟;上游计时从本站准备完成后开始,各项为累计耗时,不能直接相加。上游等待包含网络、调度、计算及缓冲,不能仅凭这些数值断定厂商排队。
诊断记录
{
"attempts": 1,
"startedAt": "2026-09-28T07:30:37.305Z",
"transport": "railway",
"proxyPrepareMs": 0,
"proxyRequestId": "vw6mhQiFRLW3gBmyxtoGcA",
"upstreamStatus": 429,
"clientHeadersMs": 427.30000019073486,
"clientPrepareMs": 13.199999809265137,
"upstreamHeadersMs": 182
}新增测试 · 2026/9/30 07:13:42
Save this as index.html and open it in a modern browser. It is a complete single-file scene; Three.js is loaded from a CDN.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#102a32" />
<title>Sakura Sanctuary — Voxel Garden Village</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Fraunces:opsz,wght@9..144,600;9..144,700&display=swap');
:root {
--ink: #17363a;
--cream: #fff8e8;
--peach: #ff937d;
--pink: #f5a6bb;
--gold: #ffd77a;
--glass: rgba(15, 42, 45, 0.73);
--line: rgba(255, 255, 255, 0.16);
}
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #b8dce5;
color: var(--cream);
font-family: "DM Sans", system-ui, sans-serif;
}
button {
font: inherit;
}
#scene {
position: fixed;
inset: 0;
display: block;
width: 100%;
height: 100%;
outline: none;
cursor: grab;
}
#scene:active {
cursor: grabbing;
}
.vignette {
position: fixed;
inset: 0;
pointer-events: none;
background:
radial-gradient(circle at 50% 44%, transparent 47%, rgba(8, 32, 36, 0.15) 100%),
linear-gradient(180deg, rgba(6, 25, 30, 0.05), transparent 28%, transparent 70%, rgba(6, 25, 30, 0.12));
z-index: 2;
}
.hud {
position: fixed;
inset: 0;
z-index: 4;
pointer-events: none;
}
.brand {
position: absolute;
top: 28px;
left: 30px;
width: min(440px, calc(100vw - 60px));
filter: drop-shadow(0 3px 16px rgba(6, 29, 32, 0.2));
}
.eyebrow {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
color: rgba(255, 248, 232, 0.82);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
}
.eyebrow::before {
content: "";
width: 27px;
height: 2px;
background: var(--gold);
box-shadow: 0 0 10px rgba(255, 215, 122, 0.5);
}
h1 {
margin: 0;
color: #fff9e9;
font-family: "Fraunces", Georgia, serif;
font-size: clamp(34px, 4.6vw, 66px);
line-height: 0.9;
letter-spacing: -0.045em;
text-shadow: 0 4px 24px rgba(6, 32, 34, 0.28);
}
h1 span {
color: #ffd1cf;
font-style: italic;
}
.brand p {
max-width: 390px;
margin: 14px 0 0;
color: rgba(255, 249, 232, 0.76);
font-size: 12px;
line-height: 1.65;
}
.status-card {
position: absolute;
top: 28px;
right: 30px;
min-width: 210px;
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: 16px;
background: var(--glass);
box-shadow: 0 14px 45px rgba(9, 33, 35, 0.18);
backdrop-filter: blur(16px) saturate(1.25);
}
.status-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
margin-bottom: 10px;
color: rgba(255, 255, 255, 0.58);
font-size: 9px;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.live-dot {
display: inline-block;
width: 7px;
height: 7px;
margin-right: 6px;
border-radius: 50%;
background: #aee889;
box-shadow: 0 0 11px #aee889;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
50% { opacity: 0.45; transform: scale(0.8); }
}
.status-number {
color: var(--gold);
font-family: "Fraunces", Georgia, serif;
font-size: 27px;
line-height: 1;
}
.status-sub {
margin-top: 7px;
color: rgba(255, 255, 255, 0.5);
font-size: 10px;
}
.controls {
position: absolute;
left: 50%;
bottom: 25px;
display: flex;
align-items: center;
gap: 5px;
padding: 7px;
border: 1px solid var(--line);
border-radius: 17px;
background: var(--glass);
box-shadow: 0 18px 50px rgba(7, 27, 30, 0.28);
backdrop-filter: blur(18px) saturate(1.3);
transform: translateX(-50%);
pointer-events: auto;
}
.control {
position: relative;
min-width: 44px;
height: 42px;
padding: 0 13px;
border: 0;
border-radius: 11px;
color: rgba(255, 255, 255, 0.69);
background: transparent;
cursor: pointer;
transition: 160ms ease;
font-size: 11px;
font-weight: 600;
}
.control:hover {
color: white;
background: rgba(255, 255, 255, 0.09);
transform: translateY(-1px);
}
.control.active {
color: #17383b;
background: var(--gold);
box-shadow: 0 5px 18px rgba(255, 197, 95, 0.19);
}
.control svg {
width: 17px;
height: 17px;
vertical-align: middle;
}
.divider {
width: 1px;
height: 25px;
margin: 0 3px;
background: rgba(255, 255, 255, 0.12);
}
.hint {
position: absolute;
right: 30px;
bottom: 29px;
display: flex;
align-items: center;
gap: 10px;
color: rgba(255, 255, 255, 0.58);
font-size: 10px;
letter-spacing: 0.04em;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
}
.mouse {
position: relative;
width: 16px;
height: 24px;
border: 1px solid rgba(255, 255, 255, 0.58);
border-radius: 10px;
}
.mouse::before {
content: "";
position: absolute;
top: 5px;
left: 6px;
width: 2px;
height: 5px;
border-radius: 2px;
background: rgba(255, 255, 255, 0.65);
}
.label-layer {
position: fixed;
inset: 0;
z-index: 3;
pointer-events: none;
overflow: hidden;
}
.world-label {
position: absolute;
min-width: 110px;
padding: 7px 10px 7px 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: white;
background: rgba(17, 47, 49, 0.76);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.18);
backdrop-filter: blur(10px);
font-size: 9px;
font-weight: 700;
letter-spacing: 0.12em;
text-align: left;
text-transform: uppercase;
transform: translate(-50%, -100%);
transition: opacity 180ms ease;
}
.world-label::after {
content: "";
position: absolute;
left: 50%;
bottom: -18px;
width: 1px;
height: 18px;
background: linear-gradient(rgba(255, 255, 255, 0.6), transparent);
}
.world-label small {
display: block;
margin-top: 2px;
color: var(--gold);
font-size: 7px;
font-weight: 600;
letter-spacing: 0.15em;
}
body.night .brand,
body.night .status-card,
body.night .controls,
body.night .hint {
color: white;
}
#loading {
position: fixed;
inset: 0;
z-index: 20;
display: grid;
place-items: center;
background: #123b40;
transition: opacity 600ms ease, visibility 600ms ease;
}
#loading.hidden {
opacity: 0;
visibility: hidden;
}
.loader-content {
width: min(360px, 75vw);
text-align: center;
}
.loader-mark {
display: grid;
place-items: center;
width: 66px;
height: 66px;
margin: 0 auto 20px;
color: var(--gold);
font-size: 32px;
border: 2px solid rgba(255, 215, 122, 0.4);
box-shadow:
inset 0 0 0 8px rgba(255, 215, 122, 0.06),
0 0 40px rgba(255, 215, 122, 0.12);
transform: rotate(45deg);
}
.loader-mark span {
transform: rotate(-45deg);
}
.loader-content h2 {
margin: 0 0 8px;
color: #fff4dd;
font-family: "Fraunces", Georgia, serif;
font-size: 27px;
}
.loader-content p {
margin: 0 0 18px;
color: rgba(255, 255, 255, 0.55);
font-size: 11px;
}
.loader-track {
height: 3px;
overflow: hidden;
border-radius: 4px;
background: rgba(255, 255, 255, 0.1);
}
.loader-track div {
width: 40%;
height: 100%;
background: var(--gold);
animation: loading 1.2s ease-in-out infinite;
}
@keyframes loading {
0% { transform: translateX(-110%); }
100% { transform: translateX(350%); }
}
@media (max-width: 760px) {
.brand {
top: 19px;
left: 20px;
}
h1 {
font-size: 38px;
}
.brand p {
display: none;
}
.status-card {
top: 18px;
right: 18px;
min-width: 0;
padding: 10px 12px;
}
.status-sub,
.status-top > span {
display: none;
}
.status-number {
font-size: 21px;
}
.controls {
bottom: 16px;
max-width: calc(100vw - 24px);
overflow-x: auto;
}
.control {
min-width: 41px;
padding: 0 10px;
white-space: nowrap;
}
.control .control-text {
display: none;
}
.hint {
display: none;
}
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.164.1/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.164.1/examples/jsm/"
}
}
</script>
</head>
<body>
<canvas id="scene" aria-label="Interactive voxel pagoda garden and village"></canvas>
<div class="vignette"></div>
<div class="label-layer" id="labels"></div>
<main class="hud">
<header class="brand">
<div class="eyebrow">A living voxel miniature</div>
<h1>Sakura <span>Sanctuary</span></h1>
<p>
A five-tier pagoda, lantern-lit village, blossom gardens, koi ponds and
hundreds of tiny stories—all built from colorful cubes.
</p>
</header>
<aside class="status-card">
<div class="status-top">
<span><i class="live-dot"></i>World online</span>
<span id="time-label">Golden daylight</span>
</div>
<div class="status-number" id="voxel-count">0 voxels</div>
<div class="status-sub" id="scene-status">5 tiers · 18 villagers · 1 peaceful world</div>
</aside>
<nav class="controls" aria-label="Scene controls">
<button class="control active" id="themeBtn" title="Toggle day and night">
<span id="themeIcon">☾</span>
<span class="control-text" id="themeText"> Night</span>
</button>
<button class="control active" id="petalBtn" title="Toggle falling petals">
✿ <span class="control-text">Petals</span>
</button>
<button class="control" id="labelBtn" title="Toggle location labels">
⌖ <span class="control-text">Labels</span>
</button>
<button class="control" id="cinemaBtn" title="Toggle automatic camera orbit">
◉ <span class="control-text">Cinematic</span>
</button>
<div class="divider"></div>
<button class="control" id="resetBtn" title="Reset camera">
↺ <span class="control-text">Reset view</span>
</button>
</nav>
<div class="hint">
<span class="mouse"></span>
<span>Drag to orbit · Scroll to zoom · Right-drag to pan</span>
</div>
</main>
<div id="loading">
<div class="loader-content">
<div class="loader-mark"><span>❀</span></div>
<h2>Growing the sanctuary</h2>
<p>Planting trees, stacking pagodas & waking the villagers…</p>
<div class="loader-track"><div></div></div>
</div>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// -----------------------------------------------------------------------
// Scene setup
// -----------------------------------------------------------------------
const canvas = document.querySelector('#scene');
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
powerPreference: 'high-performance'
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.65));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.08;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xb7dce4);
scene.fog = new THREE.Fog(0xb7dce4, 68, 150);
const camera = new THREE.PerspectiveCamera(
41,
window.innerWidth / window.innerHeight,
0.1,
240
);
camera.position.set(54, 43, 60);
const controls = new OrbitControls(camera, canvas);
controls.target.set(0, 4, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.055;
controls.minDistance = 28;
controls.maxDistance = 105;
controls.minPolarAngle = 0.28;
controls.maxPolarAngle = Math.PI * 0.47;
controls.autoRotateSpeed = 0.34;
controls.update();
const hemi = new THREE.HemisphereLight(0xdff7ff, 0x756044, 2.15);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xfff1c5, 3.6);
sun.position.set(-25, 48, 28);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
sun.shadow.camera.left = -42;
sun.shadow.camera.right = 42;
sun.shadow.camera.top = 40;
sun.shadow.camera.bottom = -40;
sun.shadow.camera.near = 1;
sun.shadow.camera.far = 115;
sun.shadow.bias = -0.0004;
sun.shadow.normalBias = 0.025;
scene.add(sun);
const rim = new THREE.DirectionalLight(0x7fc7e3, 0.85);
rim.position.set(35, 18, -35);
scene.add(rim);
// -----------------------------------------------------------------------
// Deterministic helpers
// -----------------------------------------------------------------------
function mulberry32(seed) {
return function() {
let t = seed += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
const random = mulberry32(84127);
const rand = (a, b) => a + (b - a) * random();
const pick = arr => arr[Math.floor(random() * arr.length)];
const clamp = THREE.MathUtils.clamp;
const lerp = THREE.MathUtils.lerp;
const voxels = [];
const glowVoxels = [];
const waterCells = [];
const labelAnchors = [];
function addV(x, y, z, sx, sy, sz, color) {
voxels.push({ x, y, z, sx, sy, sz, color });
}
function addGlow(x, y, z, sx, sy, sz, color = 0xffc85d) {
glowVoxels.push({ x, y, z, sx, sy, sz, color });
}
function addVBatch(list, fn) {
for (const item of list) fn(item);
}
function addBoxMesh(parent, sx, sy, sz, color, x = 0, y = 0, z = 0) {
const material = new THREE.MeshStandardMaterial({
color,
roughness: 0.82,
metalness: 0
});
const mesh = new THREE.Mesh(new THREE.BoxGeometry(sx, sy, sz), material);
mesh.position.set(x, y, z);
mesh.castShadow = true;
mesh.receiveShadow = true;
parent.add(mesh);
return mesh;
}
// -----------------------------------------------------------------------
// Island, terrain, water channels, and path network
// -----------------------------------------------------------------------
function isWaterCell(x, z) {
const pond =
Math.pow((x - 14) / 8.5, 2) +
Math.pow((z - 10.2) / 6.7, 2);
const irregular =
1 +
Math.sin(x * 1.61 + z * 0.34) * 0.08 +
Math.cos(z * 1.27 - x * 0.18) * 0.07;
const stream =
x > 0 && x < 8 &&
Math.abs(z - (8.1 + Math.sin(x * 0.44) * 0.8)) < 0.75;
return (pond < irregular && x < 24) || stream;
}
function isPath(x, z) {
const mainRoad = z >= 2 && z <= 4 && x > -25 && x < 22;
const pagodaRoad = x >= 3 && x <= 5 && z > -18 && z < 8;
const villageLane = x >= -24 && x <= -5 && Math.abs(z + 1) < 1.1;
const gardenWalk = z >= -16 && z <= -14 && x > -25 && x < 6;
const pondWalk = z >= 16 && z <= 17 && x > 2 && x < 23;
const teaWalk = Math.abs(z - 6) < 1 && x > -5 && x < 5;
return (
mainRoad || pagodaRoad || villageLane ||
gardenWalk || pondWalk || teaWalk
);
}
function nearStructure(x, z) {
if (x > 4 && x < 20 && z > -19 && z < -2) return true; // pagoda
if (x > -25 && x < -5 && z > -5 && z < 16) return true; // village
if (x > 1 && x < 8 && z > 3 && z < 10) return true; // tea house
return false;
}
// Deep rock foundation.
addV(0, -2.38, 0, 56, 3.15, 48, '#53666b');
addV(0, -3.5, 0, 53, 0.55, 45, '#3b5158');
const grassColors = ['#65a85f', '#6eb266', '#76b96b', '#5f9f5a', '#80bd70'];
const dirtColors = ['#b98c63', '#c79b70', '#aa7b58', '#d0a77d'];
const stoneColors = ['#849494', '#728889', '#9aa6a0', '#61797c'];
for (let x = -28; x < 28; x++) {
for (let z = -24; z < 24; z++) {
if (isWaterCell(x, z)) {
waterCells.push({ x, z });
addV(x, -0.72, z, 0.99, 0.12, 0.99, pick(['#506d68', '#49655f', '#5b756c']));
continue;
}
const color = isPath(x, z) ? pick(dirtColors) : pick(grassColors);
addV(x, -0.32, z, 0.99, 0.86, 0.99, color);
// Small stone fragments embedded in the ground.
if (isPath(x, z) && random() < 0.075) {
addV(
x + rand(-0.2, 0.2),
0.15,
z + rand(-0.2, 0.2),
rand(0.12, 0.3),
rand(0.08, 0.18),
rand(0.12, 0.3),
pick(['#b3a38a', '#8c8173', '#d0b894'])
);
}
}
}
// Break the rectangular edge with layered cliff voxels.
for (let x = -28; x < 28; x++) {
for (const z of [-24, 23]) {
const h = rand(0.6, 2.7);
addV(x, -1.65 + h * 0.15, z + (z < 0 ? -0.12 : 0.12),
1.03, h, rand(0.7, 1.35), pick(stoneColors));
if (random() < 0.24) {
addV(x + rand(-0.25, 0.25), -2.8 - rand(0, 0.8), z,
rand(0.45, 0.9), rand(0.5, 1.2), rand(0.5, 0.9), pick(stoneColors));
}
}
}
for (let z = -23; z < 24; z++) {
for (const x of [-28, 27]) {
const h = rand(0.7, 2.6);
addV(x + (x < 0 ? -0.1 : 0.1), -1.6 + h * 0.16, z,
rand(0.7, 1.3), h, 1.03, pick(stoneColors));
}
}
// Ocean under the floating diorama.
const oceanMaterial = new THREE.MeshPhysicalMaterial({
color: 0x4b9db4,
roughness: 0.3,
metalness: 0.05,
transparent: true,
opacity: 0.84,
clearcoat: 0.7
});
const ocean = new THREE.Mesh(new THREE.BoxGeometry(160, 0.25, 160), oceanMaterial);
ocean.position.y = -4.35;
ocean.receiveShadow = true;
scene.add(ocean);
// -----------------------------------------------------------------------
// Grass, flowers, rocks, lanterns and tiny garden details
// -----------------------------------------------------------------------
const flowerColors = ['#fff2b3', '#ff9eae', '#e993c1', '#f5f7dd', '#a9d5ff'];
for (let i = 0; i < 2350; i++) {
const x = Math.floor(rand(-27, 27));
const z = Math.floor(rand(-23, 23));
if (isWaterCell(x, z) || isPath(x, z) || nearStructure(x, z)) continue;
const bladeColor = pick(['#3f8c50', '#4e9d57', '#67ae5c', '#397b49']);
const h = rand(0.17, 0.45);
addV(x + rand(-0.33, 0.33), 0.12 + h * 0.5, z + rand(-0.33, 0.33),
rand(0.06, 0.1), h, rand(0.06, 0.1), bladeColor);
if (random() < 0.12) {
addV(x, 0.48, z, 0.16, 0.16, 0.16, pick(flowerColors));
}
}
// Moss and grass on selected rocks.
function addRock(x, z, scale = 1, baseY = 0.12) {
const layers = 3 + Math.floor(random() * 3);
for (let i = 0; i < layers; i++) {
const w = scale * rand(1.1, 2.2) * (1 - i * 0.12);
addV(
x + rand(-0.18, 0.18),
baseY + i * scale * 0.38,
z + rand(-0.18, 0.18),
w,
scale * 0.5,
w * rand(0.65, 1.05),
pick(stoneColors)
);
}
if (random() < 0.65) {
addV(x, baseY + layers * scale * 0.38, z,
scale * rand(0.45, 0.9), 0.15, scale * rand(0.35, 0.7), '#639a56');
}
}
[
[-25, -20, 1.1], [-19, -19, 0.75], [-5, -19, 0.85],
[23, -20, 1.2], [24, 6, 0.9], [24, 15, 1.1],
[1, 17, 0.8], [-24, 19, 1.1], [-4, -7, 0.65]
].forEach(r => addRock(r[0], r[1], r[2]));
function stoneLantern(x, z, scale = 1) {
addV(x, 0.2, z, 0.8 * scale, 0.35 * scale, 0.8 * scale, '#687a79');
addV(x, 0.62 * scale, z, 0.34 * scale, 0.75 * scale, 0.34 * scale, '#8d9991');
addV(x, 1.08 * scale, z, 0.78 * scale, 0.28 * scale, 0.78 * scale, '#a0a79b');
addGlow(x, 1.35 * scale, z, 0.42 * scale, 0.48 * scale, 0.42 * scale, 0xffbf5e);
addV(x, 1.7 * scale, z, 0.95 * scale, 0.23 * scale, 0.95 * scale, '#75837f');
addV(x, 1.9 * scale, z, 0.4 * scale, 0.25 * scale, 0.4 * scale, '#87928a');
}
[
[-5, 4.8], [-5, -6], [-12, 4.7], [-20, 4.7],
[3.5, 2], [3.5, -6], [3.5, -14], [8, 16.5],
[18, 16.5], [22, 16.5], [1, 5.6]
].forEach(([x, z], i) => stoneLantern(x, z, i % 3 === 0 ? 0.85 : 1));
// -----------------------------------------------------------------------
// Pagoda
// -----------------------------------------------------------------------
function pagodaRoof(cx, y, cz, outer, rise) {
for (let layer = 0; layer < 5; layer++) {
const half = outer - layer * (outer * 0.16);
const yy = y + layer * rise / 4;
const tileColors = ['#24575b', '#2b6668', '#316e70', '#397777', '#24575b'];
for (let x = -Math.ceil(half); x <= Math.ceil(half); x++) {
for (let z = -Math.ceil(half); z <= Math.ceil(half); z++) {
if (Math.abs(x) > half + 0.05 || Math.abs(z) > half + 0.05) continue;
addV(
cx + x,
yy,
cz + z,
0.96,
0.31,
0.96,
tileColors[layer]
);
}
}
}
// Gold eave edging.
const edge = Math.round(outer);
for (let i = -edge; i <= edge; i++) {
addV(cx + i, y - 0.08, cz - edge, 0.95, 0.22, 0.32, '#d7ad56');
addV(cx + i, y - 0.08, cz + edge, 0.95, 0.22, 0.32, '#d7ad56');
addV(cx - edge, y - 0.08, cz + i, 0.32, 0.22, 0.95, '#d7ad56');
addV(cx + edge, y - 0.08, cz + i, 0.32, 0.22, 0.95, '#d7ad56');
}
// Upturned corners.
for (const sx of [-1, 1]) {
for (const sz of [-1, 1]) {
for (let i = 0; i < 4; i++) {
addV(
cx + sx * (outer - i * 0.18),
y + 0.12 + i * 0.27,
cz + sz * (outer - i * 0.18),
0.55,
0.36,
0.55,
i === 3 ? '#f1c65f' : '#24575b'
);
}
}
}
}
function buildPagoda(cx, cz) {
const levels = [
{ y: 0.35, w: 10.0 },
{ y: 3.95, w: 8.5 },
{ y: 7.15, w: 7.0 },
{ y: 9.95, w: 5.7 },
{ y: 12.35, w: 4.35 }
];
// Stone terrace and stairway.
addV(cx, 0.03, cz, 13.2, 0.6, 13.2, '#6f7d75');
addV(cx, 0.32, cz, 12.4, 0.22, 12.4, '#a3a18a');
for (let i = 0; i < 5; i++) {
addV(cx, 0.5 - i * 0.08, cz + 5.1 + i * 0.62,
3.2 + i * 0.18, 0.2, 0.65, '#c1b596');
}
for (let li = 0; li < levels.length; li++) {
const { y, w } = levels[li];
const d = w;
const bodyH = 2.55 - li * 0.08;
// Balcony/floor slab.
addV(cx, y, cz, w + 0.85, 0.38, d + 0.85, '#a83e37');
addV(cx, y + 0.28, cz, w + 0.45, 0.18, d + 0.45, '#d07154');
// Plastered main body.
addV(cx, y + bodyH * 0.58, cz, w, bodyH, d, '#eadbb8');
// Dark wood lower and upper bands.
addV(cx, y + 0.48, cz, w + 0.06, 0.32, d + 0.06, '#59352f');
addV(cx, y + bodyH, cz, w + 0.08, 0.32, d + 0.08, '#713b34');
// Corner columns.
for (const sx of [-1, 1]) {
for (const sz of [-1, 1]) {
addV(cx + sx * w * 0.44, y + bodyH * 0.55, cz + sz * d * 0.44,
0.36, bodyH + 0.3, 0.36, '#b84136');
}
}
// Windows and doors on all four sides.
for (let side = 0; side < 4; side++) {
const alongX = side % 2 === 0;
const sign = side < 2 ? -1 : 1;
const panels = li < 3 ? 3 : 2;
for (let p = 0; p < panels; p++) {
const offset = (p - (panels - 1) / 2) * (w / (panels + 0.45));
const panelW = w / (panels + 1.5);
if (alongX) {
const px = cx + offset;
const pz = cz + sign * (d * 0.505);
addV(px, y + 1.32, pz, panelW, 1.18, 0.14, '#294c4b');
if ((li + p) % 3 !== 0) {
addGlow(px, y + 1.32, pz + sign * 0.09,
panelW * 0.67, 0.78, 0.08, 0xffc25a);
}
addV(px, y + 1.32, pz + sign * 0.17,
0.11, 1.25, 0.11, '#733c32');
} else {
const px = cx + sign * (w * 0.505);
const pz = cz + offset;
addV(px, y + 1.32, pz, 0.14, 1.18, panelW, '#294c4b');
if ((li + p) % 3 !== 0) {
addGlow(px + sign * 0.09, y + 1.32, pz,
0.08, 0.78, panelW * 0.67, 0xffc25a);
}
addV(px + sign * 0.17, y + 1.32, pz,
0.11, 1.25, 0.11, '#733c32');
}
}
}
// Railings.
for (const sz of [-1, 1]) {
for (let i = -Math.floor(w / 2); i <= Math.floor(w / 2); i += 1) {
addV(cx + i, y + 0.63, cz + sz * (w * 0.5 + 0.33),
0.12, 0.58, 0.12, '#7f302b');
}
addV(cx, y + 0.92, cz + sz * (w * 0.5 + 0.33),
w + 0.4, 0.14, 0.18, '#d36448');
}
pagodaRoof(cx, y + bodyH + 0.18, cz, w * 0.5 + 1.55, 1.75);
}
// Upper crown and finial.
const topY = 17.0;
addV(cx, topY, cz, 2.3, 0.65, 2.3, '#d5a64e');
addV(cx, topY + 0.9, cz, 0.42, 1.7, 0.42, '#e7bd61');
for (let i = 0; i < 4; i++) {
addV(cx, topY + 1.5 + i * 0.42, cz,
1.65 - i * 0.28, 0.18, 1.65 - i * 0.28, '#f0ca6c');
}
addV(cx, topY + 3.4, cz, 0.2, 1.1, 0.2, '#f3d982');
labelAnchors.push({
element: makeLabel('Five-Tier Pagoda', 'Harumi shrine'),
position: new THREE.Vector3(cx, 20, cz)
});
}
buildPagoda(12, -10);
// -----------------------------------------------------------------------
// Village houses
// -----------------------------------------------------------------------
function gableRoof(cx, y, cz, w, d, roofColor) {
const steps = Math.ceil((d * 0.5 + 0.75) / 0.62);
for (let s = 0; s < steps; s++) {
const offset = d * 0.5 + 0.75 - s * 0.62;
const yy = y + s * 0.39;
for (const side of [-1, 1]) {
addV(cx, yy, cz + side * offset,
w + 1.1, 0.34, 0.68, s % 2 ? roofColor : '#31535a');
}
// Thin raised tile seams.
for (let x = -Math.floor(w / 2); x <= Math.floor(w / 2); x += 2) {
addV(cx + x, yy + 0.22, cz + offset,
0.12, 0.16, 0.72, '#55727a');
}
}
addV(cx, y + steps * 0.39 + 0.15, cz,
w + 1.25, 0.32, 0.58, '#24484e');
for (const side of [-1, 1]) {
addV(cx, y - 0.08, cz + side * (d * 0.5 + 0.68),
w + 1.35, 0.23, 0.24, '#1f4147');
}
}
function house(x, z, w, d, h, wallColor, roofColor, doorColor) {
addV(x, 0.18, z, w + 0.55, 0.35, d + 0.55, '#817e6d');
addV(x, h * 0.56, z, w, h, d, wallColor);
// Exposed timber framing.
for (const sx of [-1, 1]) {
for (const sz of [-1, 1]) {
addV(x + sx * (w * 0.5 - 0.16), h * 0.54, z + sz * (d * 0.5 + 0.03),
0.24, h, 0.22, '#684338');
}
}
addV(x, h - 0.17, z, w + 0.12, 0.25, d + 0.12, '#5c3931');
addV(x, h * 0.52, z + d * 0.5 + 0.03, w, 0.2, 0.2, '#684338');
// Door and glowing paper windows.
addV(x, h * 0.43, z + d * 0.5 + 0.13, 1.05, h * 0.78, 0.18, doorColor);
addV(x, h * 0.43, z + d * 0.5 + 0.24, 0.12, h * 0.78, 0.1, '#422e2b');
addV(x, h * 0.43, z + d * 0.5 + 0.25, 0.16, 0.12, 0.12, '#e6bd66');
for (const sx of [-1, 1]) {
addV(x + sx * w * 0.29, h * 0.57, z + d * 0.5 + 0.15,
w * 0.22, h * 0.36, 0.14, '#304e4d');
addGlow(x + sx * w * 0.29, h * 0.57, z + d * 0.5 + 0.24,
w * 0.16, h * 0.25, 0.08, 0xffd37a);
}
// Side window.
addGlow(x + w * 0.5 + 0.12, h * 0.58, z,
0.08, h * 0.32, d * 0.3, 0xffca68);
gableRoof(x, h + 0.18, z, w, d, roofColor);
// Tiny chimney and smoke source.
if (random() > 0.45) {
addV(x + w * 0.28, h + 1.2, z - d * 0.17,
0.48, 1.25, 0.48, '#64544a');
}
}
house(-20, 10, 5.0, 4.2, 3.1, '#e7d6ae', '#315761', '#8f4335');
house(-12.6, 11.3, 5.6, 4.5, 3.35, '#d9c59b', '#3d6265', '#324e4c');
house(-20.2, 0.0, 4.4, 4.2, 2.8, '#d2b990', '#2d5259', '#a34e3c');
house(-10.2, -0.1, 4.8, 4.0, 3.05, '#eadbb6', '#405f5d', '#84382f');
house(-15.7, 16.3, 5.8, 4.0, 2.9, '#cfb38d', '#2a4e55', '#425b52');
// Tea house beside the pond.
house(4.3, 6.5, 4.3, 3.8, 2.7, '#ead9b5', '#31575a', '#6d4036');
labelAnchors.push({
element: makeLabel('Mochi Lane Village', '18 residents'),
position: new THREE.Vector3(-16, 7.5, 9)
});
labelAnchors.push({
element: makeLabel('Koi Garden', 'Still water'),
position: new THREE.Vector3(15, 2.6, 11)
});
// -----------------------------------------------------------------------
// Torii gate, market stalls, bridge, boats and well
// -----------------------------------------------------------------------
function torii(x, z) {
for (const sx of [-1, 1]) {
addV(x + sx * 2.45, 1.65, z, 0.5, 3.2, 0.55, '#b63e36');
addV(x + sx * 2.45, 0.2, z, 0.8, 0.4, 0.85, '#69726a');
addV(x + sx * 2.45, 2.8, z, 0.68, 0.25, 0.7, '#d65b43');
}
addV(x, 3.25, z, 5.9, 0.35, 0.6, '#c34838');
addV(x, 3.7, z, 6.7, 0.32, 0.85, '#233f45');
addV(x, 3.94, z, 6.0, 0.18, 0.6, '#d35b43');
addV(x, 2.55, z, 5.1, 0.22, 0.28, '#a93632');
addV(x, 2.3, z, 0.35, 0.65, 0.22, '#f1d599');
}
torii(-3.6, 3);
function marketStall(x, z, colorA, colorB) {
for (const sx of [-1, 1]) {
for (const sz of [-1, 1]) {
addV(x + sx * 1.5, 1.15, z + sz * 0.9, 0.14, 2.3, 0.14, '#644438');
}
}
for (let i = 0; i < 8; i++) {
addV(x - 1.75 + i * 0.5, 2.25, z,
0.5, 0.2, 2.25, i % 2 ? colorA : colorB);
addV(x - 1.75 + i * 0.5, 2.08, z - 1.15,
0.5, 0.45, 0.16, i % 2 ? colorA : colorB);
}
addV(x, 0.86, z, 2.8, 0.25, 1.25, '#8b6041');
for (let i = 0; i < 6; i++) {
addV(x - 1 + (i % 3) * 0.7, 1.07, z - 0.3 + Math.floor(i / 3) * 0.55,
0.35, 0.25, 0.3, pick(['#ef9a57', '#f2c65f', '#c84f45', '#8ebd68']));
}
}
marketStall(-16.2, 4.7, '#df6957', '#f3d6a0');
marketStall(-10.8, 4.7, '#4f8790', '#f2d6a0');
marketStall(2.1, 3.8, '#db6a65', '#f5dfb6');
// Red arched bridge over the inlet.
for (let i = 0; i < 9; i++) {
const t = (i - 4) / 4;
const y = 0.18 + (1 - t * t) * 1.15;
const z = 8.05 + Math.sin(t * 0.5) * 0.5;
addV(4.2, y, z, 0.73, 0.25, 2.2, i % 2 ? '#9c4035' : '#ba4c38');
if (i % 2 === 0) {
for (const side of [-1, 1]) {
addV(4.2 + side * 0.94, y + 0.52, z,
0.14, 0.9, 0.14, '#b64335');
}
}
}
for (const side of [-1, 1]) {
for (let i = 0; i < 9; i++) {
const t = (i - 4) / 4;
const y = 0.78 + (1 - t * t) * 1.15;
addV(4.2 + side * 0.94, y, 8.05 + Math.sin(t * 0.5) * 0.5,
0.19, 0.2, 2.3, '#ce5740');
}
}
// Village well.
addV(-7.4, 0.52, 6.2, 1.6, 0.75, 1.6, '#6c7770');
addV(-7.4, 0.92, 6.2, 1.0, 0.18, 1.0, '#263f43');
for (const sx of [-1, 1]) {
addV(-7.4 + sx * 0.72, 1.45, 6.2, 0.15, 1.2, 0.15, '#614236');
}
addV(-7.4, 2.05, 6.2, 1.9, 0.22, 1.25, '#3c5556');
// Small wooden boat beside the bridge.
const boat = new THREE.Group();
addBoxMesh(boat, 2.8, 0.35, 1.0, '#8f5838', 0, 0, 0);
addBoxMesh(boat, 1.9, 0.25, 0.75, '#d08a45', 0, 0.27, 0);
addBoxMesh(boat, 0.14, 0.16, 2.0, '#634435', 0.2, 0.36, 0);
boat.position.set(6.4, 0.03, 10.3);
boat.rotation.y = -0.3;
scene.add(boat);
// -----------------------------------------------------------------------
// Trees and groves
// -----------------------------------------------------------------------
function branchSteps(x1, y1, z1, x2, y2, z2, width, color) {
const steps = Math.max(2, Math.ceil(Math.hypot(x2 - x1, y2 - y1, z2 - z1) * 1.5));
for (let i = 0; i <= steps; i++) {
const t = i / steps;
addV(
lerp(x1, x2, t),
lerp(y1, y2, t),
lerp(z1, z2, t),
width * (1 - t * 0.32),
width * 0.8,
width * (1 - t * 0.32),
color
);
}
}
const blossomColors = ['#f6a7b8', '#f8b9c5', '#f19bae', '#ffd0d5', '#e98aa7', '#f6c0b4'];
function cherryTree(x, z, height = 5.8, spread = 3.2) {
addV(x, height * 0.42, z, 0.75, height * 0.82, 0.75, '#694337');
addV(x + 0.18, height * 0.83, z, 0.5, height * 0.34, 0.52, '#79503b');
for (let b = 0; b < 7; b++) {
const a = (b / 7) * Math.PI * 2 + rand(-0.25, 0.25);
const ex = x + Math.cos(a) * spread * rand(0.7, 1.0);
const ez = z + Math.sin(a) * spread * rand(0.7, 1.0);
const ey = height * rand(0.72, 1.0);
branchSteps(x, height * 0.62, z, ex, ey, ez, 0.38, '#704637');
}
const count = 210 + Math.floor(random() * 65);
for (let i = 0; i < count; i++) {
const a = random() * Math.PI * 2;
const r = Math.pow(random(), 0.58);
const px = x + Math.cos(a) * spread * r + rand(-0.2, 0.2);
const pz = z + Math.sin(a) * spread * r + rand(-0.2, 0.2);
const py = height * rand(0.84, 1.22) + Math.sin(a * 3) * 0.32;
const size = rand(0.38, 0.67);
addV(px, py, pz, size, size * rand(0.65, 1), size, pick(blossomColors));
}
}
[
[-24, -15, 5.5, 3.2], [-18, -17.5, 6.2, 3.5],
[-11, -17, 5.7, 3.1], [-23, -6, 5.2, 2.9],
[-15, -8, 5.0, 2.7], [-3.1, -17.5, 5.8, 3.2],
[22, -17.5, 5.2, 2.8]
].forEach(t => cherryTree(...t));
function pineTree(x, z, height = 7, width = 2.3) {
addV(x, height * 0.38, z, 0.65, height * 0.78, 0.65, '#644132');
for (let layer = 0; layer < 6; layer++) {
const t = layer / 5;
const w = width * (1 - t * 0.64);
const y = height * (0.42 + t * 0.52);
const color = pick(['#285b4b', '#326b55', '#3a765a', '#2f6550']);
addV(x, y, z, w, 0.65, w, color);
addV(x, y + 0.22, z, w * 0.78, 0.35, w * 0.78,
pick(['#4c805e', '#396f52', '#5a8d62']));
}
addV(x, height * 1.03, z, 0.7, 1.3, 0.7, '#356b51');
}
[
[-26, -20, 6.3, 2], [-20, -21, 7.1, 2.2],
[-5, -21, 6.8, 2.2], [25, -18, 7.4, 2.4],
[25, -3, 6.1, 2.0], [-26, -1, 6.8, 2.1]
].forEach(t => pineTree(...t));
function mapleTree(x, z) {
branchSteps(x, 0.1, z, x + 0.2, 3.6, z, 0.6, '#714735');
for (let i = 0; i < 5; i++) {
const a = i / 5 * Math.PI * 2;
branchSteps(x, 2.7, z, x + Math.cos(a) * 1.8, 4.3, z + Math.sin(a) * 1.8,
0.3, '#764637');
}
const colors = ['#d9673f', '#ec8b45', '#bb4c38', '#f0a34d'];
for (let i = 0; i < 130; i++) {
const a = random() * Math.PI * 2;
const r = Math.pow(random(), 0.55) * 2.1;
addV(
x + Math.cos(a) * r,
rand(3.1, 5.1),
z + Math.sin(a) * r,
rand(0.35, 0.65), rand(0.35, 0.65), rand(0.35, 0.65),
pick(colors)
);
}
}
mapleTree(18, 18.5);
mapleTree(-2, 19);
function bambooGrove(x0, z0) {
for (let i = 0; i < 28; i++) {
const x = x0 + rand(-3.6, 3.6);
const z = z0 + rand(-2.7, 2.7);
const h = rand(4.0, 6.8);
const color = pick(['#47844f', '#5d9855', '#376f47', '#6aa45b']);
for (let s = 0; s < 7; s++) {
addV(x, 0.35 + s * (h / 7), z, 0.22, h / 7 - 0.08, 0.22, color);
}
for (let leaf = 0; leaf < 5; leaf++) {
addV(
x + rand(-0.65, 0.65),
h * rand(0.66, 1.05),
z + rand(-0.65, 0.65),
rand(0.35, 0.8), 0.12, rand(0.25, 0.6),
pick(['#4b8a4d', '#6b9d53', '#397447'])
);
}
}
}
bambooGrove(-25.5, -11.5);
bambooGrove(24, -11);
labelAnchors.push({
element: makeLabel('Moon Blossom Grove', '7 ancient trees'),
position: new THREE.Vector3(-15, 10, -16)
});
// Pond border stones and lilies.
const waterSet = new Set(waterCells.map(c => `${c.x},${c.z}`));
for (const c of waterCells) {
for (const [dx, dz] of [[1,0],[-1,0],[0,1],[0,-1]]) {
if (!waterSet.has(`${c.x + dx},${c.z + dz}`) && random() < 0.32) {
addRock(c.x + dx * 0.75, c.z + dz * 0.75, rand(0.28, 0.52));
}
}
}
for (let i = 0; i < 24; i++) {
const x = rand(8, 21);
const z = rand(6, 14.5);
if (!isWaterCell(Math.floor(x), Math.floor(z))) continue;
addV(x, -0.39, z, rand(0.3, 0.62), 0.07, rand(0.3, 0.62), '#5b9b58');
if (random() < 0.4) {
addV(x + 0.1, -0.3, z, 0.13, 0.17, 0.13, pick(['#f3a4b0', '#f7d5de']));
}
}
// Reeds at the pond edge.
for (let i = 0; i < 75; i++) {
const x = rand(7, 22);
const z = rand(5, 15);
if (isWaterCell(Math.floor(x), Math.floor(z))) continue;
addV(x, rand(0.45, 0.9), z, 0.08, rand(0.65, 1.25), 0.08, '#567c46');
if (random() < 0.25) {
addV(x, 1.2, z, 0.15, 0.3, 0.15, '#79523a');
}
}
// -----------------------------------------------------------------------
// Commit static voxels to a high-performance instanced mesh
// -----------------------------------------------------------------------
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
const voxelMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 0.88,
metalness: 0.01
});
const voxelMesh = new THREE.InstancedMesh(cubeGeometry, voxelMaterial, voxels.length);
const dummy = new THREE.Object3D();
voxels.forEach((v, i) => {
dummy.position.set(v.x, v.y, v.z);
dummy.scale.set(v.sx, v.sy, v.sz);
dummy.rotation.set(0, 0, 0);
dummy.updateMatrix();
voxelMesh.setMatrixAt(i, dummy.matrix);
voxelMesh.setColorAt(i, new THREE.Color(v.color));
});
voxelMesh.instanceMatrix.needsUpdate = true;
voxelMesh.instanceColor.needsUpdate = true;
voxelMesh.castShadow = true;
voxelMesh.receiveShadow = true;
voxelMesh.computeBoundingSphere();
scene.add(voxelMesh);
const glowMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
emissive: 0xff9a3d,
emissiveIntensity: 0.38,
roughness: 0.45
});
const glowMesh = new THREE.InstancedMesh(cubeGeometry, glowMaterial, glowVoxels.length);
glowVoxels.forEach((v, i) => {
dummy.position.set(v.x, v.y, v.z);
dummy.scale.set(v.sx, v.sy, v.sz);
dummy.updateMatrix();
glowMesh.setMatrixAt(i, dummy.matrix);
glowMesh.setColorAt(i, new THREE.Color(v.color));
});
glowMesh.instanceMatrix.needsUpdate = true;
glowMesh.instanceColor.needsUpdate = true;
scene.add(glowMesh);
// Water is also voxel shaped, matching every water tile.
const waterMaterial = new THREE.MeshPhysicalMaterial({
color: 0x56aabd,
emissive: 0x123b46,
emissiveIntensity: 0.15,
roughness: 0.22,
metalness: 0.08,
transparent: true,
opacity: 0.82,
clearcoat: 0.9,
clearcoatRoughness: 0.18
});
const waterMesh = new THREE.InstancedMesh(cubeGeometry, waterMaterial, waterCells.length);
waterCells.forEach((c, i) => {
dummy.position.set(c.x, -0.52, c.z);
dummy.scale.set(0.99, 0.13, 0.99);
dummy.updateMatrix();
waterMesh.setMatrixAt(i, dummy.matrix);
});
waterMesh.instanceMatrix.needsUpdate = true;
waterMesh.receiveShadow = true;
waterMesh.computeBoundingSphere();
scene.add(waterMesh);
document.querySelector('#voxel-count').textContent =
`${(voxels.length + glowVoxels.length).toLocaleString()} voxels`;
// -----------------------------------------------------------------------
// Living villagers
// -----------------------------------------------------------------------
const people = [];
const sharedPersonGeometry = new THREE.BoxGeometry(1, 1, 1);
const personMaterials = new Map();
function personMaterial(color) {
if (!personMaterials.has(color)) {
personMaterials.set(color, new THREE.MeshStandardMaterial({
color,
roughness: 0.85
}));
}
return personMaterials.get(color);
}
function personPart(group, sx, sy, sz, color, x, y, z) {
const mesh = new THREE.Mesh(sharedPersonGeometry, personMaterial(color));
mesh.scale.set(sx, sy, sz);
mesh.position.set(x, y, z);
mesh.castShadow = true;
mesh.receiveShadow = true;
group.add(mesh);
return mesh;
}
function makePerson(robe, hair = '#342c2c', skin = '#e6af7e', hat = false) {
const group = new THREE.Group();
const leftLeg = personPart(group, 0.23, 0.58, 0.25, '#39454a', -0.16, 0.35, 0);
const rightLeg = personPart(group, 0.23, 0.58, 0.25, '#39454a', 0.16, 0.35, 0);
personPart(group, 0.68, 0.85, 0.4, robe, 0, 1.04, 0);
personPart(group, 0.19, 0.63, 0.22, robe, -0.45, 1.0, 0);
personPart(group, 0.19, 0.63, 0.22, robe, 0.45, 1.0, 0);
personPart(group, 0.49, 0.49, 0.46, skin, 0, 1.72, 0);
personPart(group, 0.53, 0.18, 0.49, hair, 0, 1.91, -0.015);
if (hat) {
personPart(group, 0.9, 0.11, 0.85, '#d3b36b', 0, 2.06, 0);
personPart(group, 0.55, 0.35, 0.52, '#d3b36b', 0, 2.23, 0);
}
return { group, leftLeg, rightLeg };
}
const walkPaths = [
[[-23,3.2,0.12],[-5,3.2,0.12]],
[[-22,-1,0.12],[-6,-1,0.12]],
[[4.4,-16,0.12],[4.4,7,0.12]],
[[-18,17,0.12],[-8,17,0.12]],
[[-24,5,0.12],[-2,5,0.12]],
[[4,16.8,0.12],[21,16.8,0.12]],
[[-7,6,0.12],[2,6,0.12]]
];
const robes = ['#d96453','#497e88','#d6a952','#795d8a','#5c9568','#b75b73','#d17b49'];
for (let i = 0; i < 18; i++) {
const person = makePerson(
pick(robes),
pick(['#30282a','#47322a','#241f23']),
pick(['#efbd8e','#c98962','#e5a675','#8c5d45']),
i % 5 === 0
);
person.group.position.set(...walkPaths[i % walkPaths.length][0]);
scene.add(person.group);
people.push({
...person,
path: walkPaths[i % walkPaths.length],
offset: random(),
speed: rand(0.006, 0.012),
bobPhase: random() * Math.PI * 2
});
}
// Vendors remain at their stalls.
[
[-17.2, 4.0, '#c34e44', true],
[-11.8, 4.0, '#5b7f72', true],
[1.1, 3.2, '#c07a3f', false],
[-7.3, 7.5, '#6e8491', false],
[18.5, 2.2, '#a45163', true]
].forEach(([x,z,color,hat]) => {
const p = makePerson(color, '#342b2a', '#dca276', hat);
p.group.position.set(x, 0.12, z);
p.group.rotation.y = Math.PI;
scene.add(p.group);
});
function samplePath(points, t) {
const lengths = [];
let total = 0;
for (let i = 0; i < points.length - 1; i++) {
const length = Math.hypot(
points[i + 1][0] - points[i][0],
points[i + 1][2] - points[i][2]
);
lengths.push(length);
total += length;
}
let distance = ((t % 1) + 1) % 1 * total;
for (let i = 0; i < lengths.length; i++) {
if (distance <= lengths[i]) {
const f = distance / lengths[i];
const a = points[i];
const b = points[i + 1];
return {
x: lerp(a[0], b[0], f),
y: lerp(a[1], b[1], f),
z: lerp(a[2], b[2], f),
angle: Math.atan2(b[0] - a[0], b[2] - a[2])
};
}
distance -= lengths[i];
}
return { x: points[0][0], y: points[0][1], z: points[0][2], angle: 0 };
}
// -----------------------------------------------------------------------
// Koi, waterfall, petals, fireflies, smoke, birds and clouds
// -----------------------------------------------------------------------
const koi = [];
for (let i = 0; i < 6; i++) {
const group = new THREE.Group();
personPart(group, 0.55, 0.16, 0.2, i % 2 ? '#f18b55' : '#f5d6b3', 0, 0, 0);
personPart(group, 0.22, 0.14, 0.18, '#f5eee0', -0.34, 0, 0);
personPart(group, 0.25, 0.12, 0.2, i % 2 ? '#e65f43' : '#df7751', 0.38, 0, 0);
group.position.set(14, -0.34, 10);
scene.add(group);
koi.push({
group,
rx: rand(2.2, 6.2),
rz: rand(1.3, 4.3),
speed: rand(0.1, 0.18) * (i % 2 ? 1 : -1),
phase: random() * Math.PI * 2,
cx: 14.5,
cz: 10.2
});
}
// Waterfall voxels on the western cliff.
const waterfallData = [];
for (let i = 0; i < 72; i++) {
waterfallData.push({
x: -27.75 + rand(-0.7, 0.7),
z: -10 + rand(-1.5, 1.5),
y: rand(-4, 0.7),
speed: rand(0.025, 0.05),
phase: random()
});
}
const waterfallMat = new THREE.MeshBasicMaterial({
color: 0xa7e8f2,
transparent: true,
opacity: 0.66
});
const waterfallMesh = new THREE.InstancedMesh(
cubeGeometry,
waterfallMat,
waterfallData.length
);
waterfallMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
scene.add(waterfallMesh);
// Falling sakura petals.
const petals = [];
const petalMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 0.7,
emissive: 0x5b172b,
emissiveIntensity: 0.12,
side: THREE.DoubleSide
});
for (let i = 0; i < 150; i++) {
petals.push({
x: rand(-32, 32),
y: rand(0, 23),
z: rand(-28, 28),
phase: random() * Math.PI * 2,
speed: rand(0.55, 1.15),
size: rand(0.11, 0.24)
});
}
const petalMesh = new THREE.InstancedMesh(cubeGeometry, petalMaterial, petals.length);
petalMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
petals.forEach((p, i) => petalMesh.setColorAt(i, new THREE.Color(pick(blossomColors))));
petalMesh.instanceColor.needsUpdate = true;
scene.add(petalMesh);
// Fireflies use tiny emissive voxel cubes.
const fireflies = [];
for (let i = 0; i < 48; i++) {
fireflies.push({
x: rand(-24, 23),
y: rand(1, 7.5),
z: rand(-21, 20),
phase: random() * Math.PI * 2,
speed: rand(0.3, 0.8)
});
}
const fireflyMat = new THREE.MeshBasicMaterial({
color: 0xffe58a,
transparent: true,
opacity: 0.9
});
const fireflyMesh = new THREE.InstancedMesh(cubeGeometry, fireflyMat, fireflies.length);
fireflyMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
scene.add(fireflyMesh);
// Chimney smoke.
const smokeSources = [
new THREE.Vector3(-18.6, 5.0, 9.2),
new THREE.Vector3(-11.2, 5.4, 10.5),
new THREE.Vector3(5.5, 4.5, 5.9)
];
const smokeData = [];
smokeSources.forEach(source => {
for (let i = 0; i < 11; i++) {
smokeData.push({ source, i, phase: random() * 6 });
}
});
const smokeMat = new THREE.MeshBasicMaterial({
color: 0xd9e1de,
transparent: true,
opacity: 0.22,
depthWrite: false
});
const smokeMesh = new THREE.InstancedMesh(cubeGeometry, smokeMat, smokeData.length);
smokeMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
scene.add(smokeMesh);
// Voxel birds.
const birds = [];
for (let i = 0; i < 7; i++) {
const group = new THREE.Group();
addBoxMesh(group, 0.7, 0.13, 0.25, '#34494d', 0, 0, 0);
const left = addBoxMesh(group, 0.8, 0.12, 0.18, '#34494d', -0.5, 0, 0);
const right = addBoxMesh(group, 0.8, 0.12, 0.18, '#34494d', 0.5, 0, 0);
scene.add(group);
birds.push({ group, left, right, offset: i * 1.7, height: rand(13, 19) });
}
// Chunky clouds.
const clouds = [];
for (let i = 0; i < 5; i++) {
const group = new THREE.Group();
const pieces = 9 + Math.floor(random() * 7);
for (let p = 0; p < pieces; p++) {
addBoxMesh(
group,
rand(2.0, 4.2),
rand(0.7, 1.35),
rand(1.6, 3.5),
pick(['#eef4ed', '#e4ece7', '#f5f1e6']),
rand(-4, 4),
rand(-0.3, 0.5),
rand(-1.5, 1.5)
);
}
group.position.set(rand(-55, 45), rand(23, 31), rand(-55, -30));
group.scale.setScalar(rand(0.7, 1.15));
scene.add(group);
clouds.push({ group, speed: rand(0.12, 0.25) });
}
// -----------------------------------------------------------------------
// Sky stars and voxel sun/moon
// -----------------------------------------------------------------------
const stars = [];
for (let i = 0; i < 190; i++) {
const angle = random() * Math.PI * 2;
const radius = rand(65, 95);
stars.push({
x: Math.cos(angle) * radius,
y: rand(15, 65),
z: Math.sin(angle) * radius,
size: rand(0.08, 0.22)
});
}
const starMaterial = new THREE.MeshBasicMaterial({
color: 0xdff6ff,
transparent: true,
opacity: 0
});
const starMesh = new THREE.InstancedMesh(cubeGeometry, starMaterial, stars.length);
stars.forEach((s, i) => {
dummy.position.set(s.x, s.y, s.z);
dummy.scale.set(s.size, s.size, s.size);
dummy.updateMatrix();
starMesh.setMatrixAt(i, dummy.matrix);
});
scene.add(starMesh);
function createSkyOrb(x, y, z, color) {
const group = new THREE.Group();
const mat = new THREE.MeshBasicMaterial({ color, transparent: true });
const cells = [
[-1,1],[0,1],[1,1],[-2,0],[-1,0],[0,0],[1,0],[2,0],
[-2,-1],[-1,-1],[0,-1],[1,-1],[2,-1],[-1,-2],[0,-2],[1,-2]
];
for (const [cx, cy] of cells) {
const mesh = new THREE.Mesh(new THREE.BoxGeometry(0.78, 0.78, 0.3), mat);
mesh.position.set(cx * 0.74, cy * 0.74, 0);
group.add(mesh);
}
group.position.set(x, y, z);
scene.add(group);
return group;
}
const sunOrb = createSkyOrb(-45, 37, -68, 0xffd27a);
const moonOrb = createSkyOrb(43, 35, -67, 0xd9efff);
moonOrb.visible = false;
// -----------------------------------------------------------------------
// Labels
// -----------------------------------------------------------------------
function makeLabel(title, subtitle) {
const el = document.createElement('div');
el.className = 'world-label';
el.innerHTML = `${title}<small>${subtitle}</small>`;
el.style.display = 'none';
document.querySelector('#labels').appendChild(el);
return el;
}
// The pagoda label was requested before its DOM function was defined.
// Rebuilding the small list is unnecessary: createLabel below is hoisted as
// a function declaration, so the original calls above work correctly.
let labelsVisible = false;
const projected = new THREE.Vector3();
function updateLabels() {
for (const label of labelAnchors) {
if (!labelsVisible) {
label.element.style.display = 'none';
continue;
}
projected.copy(label.position).project(camera);
const visible =
projected.z > -1 && projected.z < 1 &&
projected.x > -1.15 && projected.x < 1.15 &&
projected.y > -1.15 && projected.y < 1.15;
if (!visible) {
label.element.style.display = 'none';
continue;
}
label.element.style.display = 'block';
label.element.style.left = `${(projected.x * 0.5 + 0.5) * innerWidth}px`;
label.element.style.top = `${(-projected.y * 0.5 + 0.5) * innerHeight}px`;
label.element.style.opacity = String(
clamp(1 - Math.abs(projected.z - 0.7), 0.25, 1)
);
}
}
// -----------------------------------------------------------------------
// UI behavior
// -----------------------------------------------------------------------
let night = false;
let nightBlend = 0;
let petalsEnabled = true;
let cinematic = false;
const themeBtn = document.querySelector('#themeBtn');
const themeIcon = document.querySelector('#themeIcon');
const themeText = document.querySelector('#themeText');
const timeLabel = document.querySelector('#time-label');
const petalBtn = document.querySelector('#petalBtn');
const labelBtn = document.querySelector('#labelBtn');
const cinemaBtn = document.querySelector('#cinemaBtn');
const resetBtn = document.querySelector('#resetBtn');
function toggleTheme() {
night = !night;
document.body.classList.toggle('night', night);
themeBtn.classList.toggle('active', !night);
themeIcon.textContent = night ? '☀' : '☾';
themeText.textContent = night ? ' Day' : ' Night';
timeLabel.textContent = night ? 'Moonlit evening' : 'Golden daylight';
moonOrb.visible = night;
}
themeBtn.addEventListener('click', toggleTheme);
petalBtn.addEventListener('click', () => {
petalsEnabled = !petalsEnabled;
petalBtn.classList.toggle('active', petalsEnabled);
petalMesh.visible = petalsEnabled;
});
labelBtn.addEventListener('click', () => {
labelsVisible = !labelsVisible;
labelBtn.classList.toggle('active', labelsVisible);
});
cinemaBtn.addEventListener('click', () => {
cinematic = !cinematic;
controls.autoRotate = cinematic;
cinemaBtn.classList.toggle('active', cinematic);
});
resetBtn.addEventListener('click', () => {
camera.position.set(54, 43, 60);
controls.target.set(0, 4, 0);
controls.update();
});
window.addEventListener('keydown', event => {
if (event.key.toLowerCase() === 'n') toggleTheme();
if (event.key.toLowerCase() === 'l') labelBtn.click();
if (event.key.toLowerCase() === 'p') petalBtn.click();
if (event.key.toLowerCase() === 'c') cinemaBtn.click();
if (event.key.toLowerCase() === 'r') resetBtn.click();
});
canvas.addEventListener('dblclick', resetBtn.click);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.65));
});
// -----------------------------------------------------------------------
// Animation
// -----------------------------------------------------------------------
const clock = new THREE.Clock();
const dayBackground = new THREE.Color(0xb7dce4);
const nightBackground = new THREE.Color(0x102d46);
const dayFog = new THREE.Color(0xb7dce4);
const nightFog = new THREE.Color(0x17374d);
const daySun = new THREE.Color(0xfff1c5);
const nightSun = new THREE.Color(0xa9caff);
const dayHemi = new THREE.Color(0xdff7ff);
const nightHemi = new THREE.Color(0x7ea6dc);
function updateDayNight(delta) {
nightBlend = lerp(nightBlend, night ? 1 : 0, 1 - Math.pow(0.025, delta));
scene.background.copy(dayBackground).lerp(nightBackground, nightBlend);
scene.fog.color.copy(dayFog).lerp(nightFog, nightBlend);
sun.color.copy(daySun).lerp(nightSun, nightBlend);
sun.intensity = lerp(3.6, 1.15, nightBlend);
sun.position.x = lerp(-25, 32, nightBlend);
hemi.color.copy(dayHemi).lerp(nightHemi, nightBlend);
hemi.intensity = lerp(2.15, 0.8, nightBlend);
rim.intensity = lerp(0.85, 1.2, nightBlend);
glowMaterial.emissiveIntensity = lerp(0.38, 2.2, nightBlend);
oceanMaterial.color.set(0x4b9db4).lerp(new THREE.Color(0x244a65), nightBlend);
waterMaterial.color.set(0x56aabd).lerp(new THREE.Color(0x285c78), nightBlend);
fireflyMat.opacity = nightBlend * 0.95;
starMaterial.opacity = nightBlend * 0.9;
renderer.toneMappingExposure = lerp(1.08, 0.9, nightBlend);
}
function updatePeople(time) {
for (const person of people) {
const t = person.offset + time * person.speed;
const pos = samplePath(person.path, t);
person.group.position.set(pos.x, pos.y + Math.abs(Math.sin(time * 5 + person.bobPhase)) * 0.035, pos.z);
person.group.rotation.y = pos.angle;
const stride = Math.sin(time * 5.2 + person.bobPhase) * 0.48;
person.leftLeg.rotation.x = stride;
person.rightLeg.rotation.x = -stride;
}
}
function updateKoi(time) {
for (const fish of koi) {
const a = time * fish.speed + fish.phase;
const x = fish.cx + Math.cos(a) * fish.rx;
const z = fish.cz + Math.sin(a) * fish.rz;
fish.group.position.set(x, -0.34 + Math.sin(time * 2 + fish.phase) * 0.025, z);
fish.group.rotation.y = -a + (fish.speed > 0 ? Math.PI / 2 : -Math.PI / 2);
}
}
function updateWaterfall(time) {
waterfallData.forEach((p, i) => {
const y = 0.8 - (((time * p.speed + p.phase) % 1) * 5.2);
dummy.position.set(p.x, y, p.z);
dummy.scale.set(0.22, 0.42, 0.22);
dummy.rotation.set(0, 0, 0);
dummy.updateMatrix();
waterfallMesh.setMatrixAt(i, dummy.matrix);
});
waterfallMesh.instanceMatrix.needsUpdate = true;
}
function updatePetals(time) {
if (!petalsEnabled) return;
petals.forEach((p, i) => {
const y = ((p.y - time * p.speed + 30) % 26) + 0.2;
dummy.position.set(
p.x + Math.sin(time * 0.65 + p.phase) * 2.1,
y,
p.z + Math.cos(time * 0.4 + p.phase) * 1.1
);
dummy.rotation.set(time * 0.7 + p.phase, time + p.phase, time * 0.4);
dummy.scale.set(p.size, p.size * 0.35, p.size * 0.65);
dummy.updateMatrix();
petalMesh.setMatrixAt(i, dummy.matrix);
});
petalMesh.instanceMatrix.needsUpdate = true;
}
function updateFireflies(time) {
fireflies.forEach((f, i) => {
dummy.position.set(
f.x + Math.sin(time * f.speed + f.phase) * 1.2,
f.y + Math.sin(time * f.speed * 1.7 + f.phase) * 0.45,
f.z + Math.cos(time * f.speed + f.phase) * 1.1
);
const pulse = 0.65 + Math.sin(time * 3.2 + f.phase) * 0.35;
dummy.scale.setScalar(0.09 * pulse);
dummy.rotation.set(0, time + f.phase, 0);
dummy.updateMatrix();
fireflyMesh.setMatrixAt(i, dummy.matrix);
});
fireflyMesh.instanceMatrix.needsUpdate = true;
}
function updateSmoke(time) {
smokeData.forEach((p, i) => {
const t = (time * 0.16 + p.phase) % 1;
dummy.position.set(
p.source.x + Math.sin(t * 5 + p.i) * t * 0.8,
p.source.y + t * 3.2,
p.source.z + t * 0.55
);
dummy.scale.setScalar(0.18 + t * 0.62);
dummy.rotation.set(t, t * 0.4, 0);
dummy.updateMatrix();
smokeMesh.setMatrixAt(i, dummy.matrix);
});
smokeMesh.instanceMatrix.needsUpdate = true;
}
function updateBirds(time) {
for (const bird of birds) {
const x = ((time * 1.25 + bird.offset + 60) % 120) - 60;
bird.group.position.set(x, bird.height + Math.sin(time + bird.offset), -8);
bird.group.rotation.y = -0.15;
const flap = Math.sin(time * 6 + bird.offset) * 0.35;
bird.left.rotation.z = flap;
bird.right.rotation.z = -flap;
}
}
function updateClouds(time, delta) {
for (const cloud of clouds) {
cloud.group.position.x += cloud.speed * delta;
if (cloud.group.position.x > 68) cloud.group.position.x = -68;
}
}
function animate() {
const delta = Math.min(clock.getDelta(), 0.05);
const time = clock.elapsedTime;
controls.update();
updateDayNight(delta);
updatePeople(time);
updateKoi(time);
updateWaterfall(time);
updatePetals(time);
updateFireflies(time);
updateSmoke(time);
updateBirds(time);
updateClouds(time, delta);
sunOrb.rotation.y = Math.sin(time * 0.04) * 0.08;
moonOrb.rotation.y = -Math.sin(time * 0.035) * 0.08;
boat.position.y = -0.01 + Math.sin(time * 1.25) * 0.035;
boat.rotation.z = Math.sin(time * 0.8) * 0.012;
waterMaterial.emissiveIntensity = lerp(0.12, 0.28, Math.sin(time * 0.7) * 0.5 + 0.5);
updateLabels();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
// Let the first frame render before removing the loader.
requestAnimationFrame(() => {
renderer.render(scene, camera);
setTimeout(() => {
document.querySelector('#loading').classList.add('hidden');
}, 420);
animate();
});
</script>
</body>
</html>
计时详情
- 实际等待(客户端)
- 1.02s
- 客户端请求准备
- 0.01s
- 本站代理准备
- 0.00s
- 上游响应头到达
- 0.76s
- 上游首字节到达
- 0.76s
- 上游首 Token 到达
- 0.76s
- 上游重试次数
- 0
实际等待使用客户端时钟;上游计时从本站准备完成后开始,各项为累计耗时,不能直接相加。上游等待包含网络、调度、计算及缓冲,不能仅凭这些数值断定厂商排队。
诊断记录
{
"attempts": 1,
"startedAt": "2026-09-30T07:13:42.460Z",
"transport": "railway",
"ttftSource": "proxy",
"clientTtftMs": 1016.0999999046326,
"completionId": "070be9a6fb8afade4ca7f8feaefd1509",
"streamResumes": 4,
"proxyPrepareMs": 1,
"proxyRequestId": "IfQQ59i6Rh2OaraRn6XIxQ",
"upstreamStatus": 200,
"upstreamTtftMs": 758,
"clientHeadersMs": 251.40000009536743,
"clientPrepareMs": 11.5,
"upstreamHeadersMs": 756,
"upstreamFirstByteMs": 758,
"clientFirstContentMs": 99669.2000002861
}新增测试 · 2026/9/30 07:12:47
计时详情
- 实际等待(客户端)
- 2.21s
- 客户端请求准备
- 0.24s
- 本站代理准备
- 0.84s
- 上游响应头到达
- 0.88s
- 上游重试次数
- 0
实际等待使用客户端时钟;上游计时从本站准备完成后开始,各项为累计耗时,不能直接相加。上游等待包含网络、调度、计算及缓冲,不能仅凭这些数值断定厂商排队。
诊断记录
{
"attempts": 1,
"startedAt": "2026-09-30T07:12:48.374Z",
"transport": "railway",
"ttftSource": "proxy",
"clientTtftMs": 2206.5,
"streamResumes": 1,
"proxyPrepareMs": 843,
"proxyRequestId": "8j2EG0ryRJq--gMIwUFZXw",
"upstreamStatus": 200,
"clientHeadersMs": 1377.3999996185303,
"clientPrepareMs": 238,
"upstreamHeadersMs": 881
}