qwen3.8-flash 24GB显存 本地部署教程及测试

推理引擎strata

部署教程如下

download file and Extract

conda create -n strata-build -c conda-forge python=3.12 cmake ninja -y

conda activate strata-build
cd ~/Strata-main
./setup.sh

根据本地硬件实际情况选择,我这里全拉满

追求质量选择原版+4(ipq3_s)

选择64k

image

复杂推理选8bit

如遇代理问题可手动下载

Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00001-of-00002.gguf

Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00002-of-00002.gguf

cd ~/Strata-main
./run-iq3_s.sh

可接入codex或者cc

OpenAI base URL

http://127.0.0.1:8080/v1

Anthropic base URL

http://127.0.0.1:8080

配置~/.claude/settings.json

{
  "env": {
    "ANTHROPIC_API_KEY": "sk-local",
    "ANTHROPIC_BASE_URL": "http://127.0.0.1:8080",
    "HTTPS_PROXY": "http://127.0.0.1:10808",
    "HTTP_PROXY": "http://127.0.0.1:10808",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL": "qwen3.8-flash-next-iq3_s",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME": "qwen3.8-flash-next-iq3_s",
    "ANTHROPIC_DEFAULT_OPUS_MODEL": "qwen3.8-flash-next-iq3_s",
    "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME": "qwen3.8-flash-next-iq3_s",
    "ANTHROPIC_DEFAULT_SONNET_MODEL": "qwen3.8-flash-next-iq3_s",
    "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "qwen3.8-flash-next-iq3_s",
    "CLAUDE_CODE_ATTRIBUTION_HEADER": "0",
    "CLAUDE_CODE_EFFORT_LEVEL": "max",
    "DISABLE_AUTOUPDATER": "1",
    "ENABLE_TOOL_SEARCH": "true"
  },
  "model": "qwen3.8-flash-next-iq3_s",
  "theme": "light-daltonized"
}

鹈鹕测试

一只鹈鹕踩着单车掠过黄昏的乡间公路:喉囊里还藏着一条鱼,围巾被风扯成两面旗。整张图只有一个 .svg 文件——车轮、脚踏、腿、围巾、视差层、眨眼与扬尘,全部用 SMIL 在矢量里跑。

有一个水杯配对游戏。共有 4 种不同颜色的水杯,每种颜色各有两个。将同色的两个水杯分别放在上下两层,因此上下两层各有 4 个水杯。下层 4 个水杯按某个未知顺序排列,挑战者无法看到它们;上层水杯的颜色和位置则完全可见。游戏开始后,挑战者可以反复进行以下操作:1. 向裁判询问当前有多少个位置满足“上下两个水杯颜色相同”。裁判只回答匹配位置的总数,不透露具体是哪些位置。2. 根据目前获得的所有信息,挑战者可以选择交换上层任意两个相邻位置的水杯,注意只能是相邻,不能是任意两个。当 4 个位置全部匹配时,游戏结束。问题:挑战者应采用何种策略,才能保证对于下层水杯的任意排列都能完成配对?所有能保证成功的策略中,最坏情况所需的交换次数最少是多少?回答时请不要进行联网搜索,也不要写代码来辅助计算(包括思考过程中)。
假设答案是 x ,你需要给出严格的证明,为什么 x 可行,为什么小于 x 不可行。


请用纯原生 HTML 5 和 JavaScript(包含在一个自包含的单一 HTML 文件中,严禁引入任何外部库或框架),实现一个经典 Wolfenstein 3 D 风格的光线投射(Raycasting)伪 3 D 迷宫走廊。
要求:

  1. 画布上半部为天花板,下半部为地板,中间是渲染出的 3 D 墙壁立面。
  2. 必须包含正确的 DDA (Digital Differential Analysis) 光线投射算法与垂直切片渲染。
  3. 必须包含鱼眼失真校正(Fish-eye correction),让墙面平直不弯曲。
  4. 提供键盘 WASD 控制玩家前后移动与左右转向,并在左上角绘制一个小型的 2 D 俯视雷达小地图展示当前玩家位置与视锥。
  5. 只返回完整、干净、保存为 .html 即可双击在浏览器中直接键盘游玩的代码。


请生成一个可直接渲染的 SVG 光路图,展示白光通过三棱镜发生色散的现象。
要求:

  • 画一个等边三棱镜,顶角朝上,底边水平。
  • 一束白光从左侧面斜射入,经过两次折射后从右侧面射出。
  • 画出两条法线:左侧面入射点法线、右侧面出射点法线。
  • 画出红光和紫光的光路,用不同颜色区分。
  • 右侧画一个竖直光屏,光屏上画出七色光带,从上到下依次标注:红、橙、黄、绿、蓝、靛、紫。
  • 标注文字:白光、法线、红光、紫光、光屏。
  • 光从空气进入玻璃时,折射角小于入射角;从玻璃进入>空气时,折射角大于入射角。
  • 紫光比红光偏折更大。
  • SVG 尺寸约 600×400,代码放在 svg 代码块中。
    最后输出一个HTML文件用于展示这个SVG

在这里输入要转换的内容在单个 HTML 文件中,使用纯原生 WebGL 或 Three.js 创建一个精美细致的体素艺术环境,其中包含一座坐落于生机勃勃花园中的华丽宝塔。在这个 3D 环境中,请包含:
• 建筑: 一座 3 到 5 层的传统宝塔,具有明显的飞檐、栏杆和发光的入口。
• 自然: 多样化的生态系统,有方块状的绿树、盛开的粉色樱花、瀑布和锦鲤池塘。
• 互动: 流畅的相机旋转和缩放控制。一个昼夜切换按钮,可将场景从明亮柔和的日光转化为由宝塔屋檐下悬挂的发光纸灯笼照亮的魔幻夜景,并在流动的水面上产生倒影。
• 性能: 必须使用 InstancedMesh 对程式码进行高度优化,以便在桌面浏览器中无缝处理高密度的体素方块。

qwen3.8-flash-next-iq3_s
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>浮屠園 · Voxel Pagoda Garden</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Zen+Antique&family=Noto+Serif+SC:wght@700;900&display=swap" rel="stylesheet">
<style>
:root{
  --ink:#0a0c11; --paper:#f3ebdd; --gold:#e6b667; --gold-d:#8f7340;
  --verm:#d8503a; --jade:#74c08d; --line:rgba(230,182,103,.20);
  --disp:'Zen Antique','Noto Serif SC',serif;
  --body:'DM Sans','Noto Sans SC',system-ui,sans-serif;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;overflow:hidden;background:#0a0c11}
body{font-family:var(--body);color:var(--paper);-webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility}
canvas{display:block;position:fixed;inset:0;touch-action:none}

/* ── 颗粒 + 暗角 ── */
#grain{position:fixed;inset:0;z-index:9;pointer-events:none;mix-blend-mode:overlay;opacity:.16;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/></filter><rect width='140' height='140' filter='url(%23n)' opacity='.55'/></svg>")}
#vig{position:fixed;inset:0;z-index:8;pointer-events:none;
  background:radial-gradient(120% 90% at 50% 45%,transparent 45%,rgba(0,0,0,.55) 100%)}

#ui{position:fixed;inset:0;z-index:12;pointer-events:none}
#ui>*{pointer-events:auto}

.panel{
  background:linear-gradient(178deg,rgba(11,14,20,.90),rgba(11,14,20,.70));
  border:1px solid var(--line);border-radius:3px;
  backdrop-filter:blur(10px) saturate(1.15);-webkit-backdrop-filter:blur(10px) saturate(1.15);
  box-shadow:0 22px 60px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,255,255,.07);
}

/* ── 品牌区 ── */
.brand{position:absolute;left:26px;top:22px;display:flex;gap:15px;align-items:flex-start;user-select:none}
.seal{width:54px;height:54px;flex:none;display:grid;place-items:center;border-radius:3px;
  background:linear-gradient(155deg,#d2503a,#8f2a1c);color:#fff3e6;font-family:var(--disp);font-size:27px;
  box-shadow:0 10px 26px rgba(160,40,20,.4),inset 0 0 0 1px rgba(255,255,255,.14);
  transform:rotate(-4deg);transition:transform .6s cubic-bezier(.2,.95,.2,1)}
.brand:hover .seal{transform:rotate(4deg) scale(1.07)}
.brand h1{font-family:var(--disp);font-size:clamp(30px,3.6vw,44px);line-height:.95;letter-spacing:.06em;
  text-shadow:0 3px 26px rgba(0,0,0,.75)}
.brand h1 em{font-style:normal;color:var(--gold)}
.brand .sub{margin-top:9px;font-size:10px;letter-spacing:.44em;color:rgba(230,182,103,.85);text-transform:uppercase}
.brand .cn{margin-top:5px;font-size:11px;letter-spacing:.2em;color:rgba(243,235,221,.42)}

/* ── 竖排和铭 ── */
.tate{position:absolute;right:296px;top:34px;writing-mode:vertical-rl;letter-spacing:.42em;
  font-family:var(--disp);font-size:13px;color:rgba(243,235,221,.30);user-select:none}
@media (max-width:1080px){.tate{display:none}}

/* ── 控制台 ── */
.ctl{position:absolute;right:22px;top:50%;transform:translateY(-50%);width:252px;padding:16px 16px 12px}
.sec{margin-bottom:15px}
.lbl{display:flex;align-items:center;gap:9px;font-size:9.5px;letter-spacing:.3em;text-transform:uppercase;
  color:rgba(243,235,221,.44);margin-bottom:9px}
.lbl::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--line),transparent)}

.dn{width:100%;display:flex;align-items:center;gap:12px;padding:9px 11px;cursor:pointer;
  border:1px solid var(--line);border-radius:3px;background:rgba(255,255,255,.03);
  transition:border-color .35s,background .35s,transform .25s}
.dn:hover{border-color:rgba(230,182,103,.5);background:rgba(255,255,255,.06);transform:translateY(-1px)}
.dn-track{position:relative;width:62px;height:30px;flex:none;border-radius:3px;overflow:hidden;
  background:linear-gradient(90deg,#7fb2e8 0%,#cfe6f5 45%,#1a2740 55%,#0b1120 100%);
  background-size:200% 100%;background-position:0 0;transition:background-position .8s cubic-bezier(.4,0,.2,1)}
.dn[data-night="1"] .dn-track{background-position:100% 0}
.dn-knob{position:absolute;top:3px;left:3px;width:24px;height:24px;border-radius:2px;
  background:radial-gradient(circle at 35% 32%,#fff6dd,#e8b451);
  box-shadow:0 2px 10px rgba(0,0,0,.5);transition:transform .55s cubic-bezier(.34,1.5,.4,1),background .5s}
.dn[data-night="1"] .dn-knob{transform:translateX(32px);background:radial-gradient(circle at 62% 34%,#f4f9ff,#9fc0e8)}
.dn-tx b{display:block;font-family:var(--disp);font-size:17px;letter-spacing:.12em}
.dn-tx i{display:block;font-style:normal;font-size:9px;letter-spacing:.28em;color:rgba(230,182,103,.7);margin-top:3px}

.grid{display:grid;grid-template-columns:1fr 1fr;gap:6px}
.b{position:relative;overflow:hidden;padding:8px 6px;font-size:11.5px;letter-spacing:.1em;cursor:pointer;
  color:rgba(243,235,221,.8);border:1px solid rgba(255,255,255,.08);border-radius:2px;
  background:rgba(255,255,255,.035);transition:color .3s,border-color .3s,transform .2s}
.b::before{content:"";position:absolute;inset:0;background:linear-gradient(120deg,rgba(230,182,103,.22),rgba(216,80,58,.14));
  transform:translateY(101%);transition:transform .38s cubic-bezier(.3,.9,.2,1)}
.b:hover{color:#fff;border-color:rgba(230,182,103,.42);transform:translateY(-1px)}
.b:hover::before{transform:translateY(0)}
.b.on{border-color:rgba(116,192,141,.55);color:#eafff2}
.b.on::before{transform:translateY(0);background:linear-gradient(120deg,rgba(116,192,141,.24),rgba(116,192,141,.08))}
.b span{position:relative}

.stats{display:flex;gap:14px;padding-top:11px;border-top:1px solid rgba(255,255,255,.07)}
.st{flex:1}
.st u{display:block;text-decoration:none;font-size:8.5px;letter-spacing:.22em;color:rgba(243,235,221,.35);margin-bottom:3px}
.st b{font-family:var(--disp);font-size:19px;letter-spacing:.02em;font-variant-numeric:tabular-nums}
.st b i{font-style:normal;font-size:10px;color:rgba(243,235,221,.45);margin-left:2px}

/* ── 图例 ── */
.legend{position:absolute;left:26px;bottom:22px;display:flex;gap:7px;flex-wrap:wrap;padding:9px 11px;max-width:60vw}
.legend span{display:flex;align-items:center;gap:6px;font-size:11px;color:rgba(243,235,221,.62)}
kbd{font-family:var(--body);font-size:10px;letter-spacing:.06em;padding:3px 7px;border-radius:2px;
  background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.12);color:var(--gold)}

/* ── 载入 ── */
#loader{position:fixed;inset:0;z-index:60;background:
  radial-gradient(110% 80% at 18% 8%,#1c2333 0%,#080b12 62%);
  display:flex;flex-direction:column;justify-content:flex-end;padding:42px;gap:26px;
  transition:opacity 1s ease,visibility 1s}
#loader.gone{opacity:0;visibility:hidden;pointer-events:none}
.lrow{display:flex;justify-content:space-between;align-items:flex-end;gap:30px;flex-wrap:wrap}
.ltitle{font-family:var(--disp);font-size:clamp(46px,9vw,104px);line-height:.9;letter-spacing:.04em}
.ltitle em{font-style:normal;color:var(--verm)}
.lmeta{font-size:11px;letter-spacing:.34em;color:rgba(230,182,103,.8);text-transform:uppercase;margin-top:14px}
.lnum{font-family:var(--disp);font-size:clamp(52px,11vw,132px);line-height:.85;color:var(--gold);
  font-variant-numeric:tabular-nums}
.lnum span{font-size:.28em;letter-spacing:.2em;color:rgba(243,235,221,.5);margin-left:6px}
.lbar{position:absolute;left:0;right:0;bottom:0;height:2px;background:rgba(255,255,255,.08)}
.lbar i{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--verm),var(--gold));
  transition:width .45s cubic-bezier(.3,.9,.2,1);box-shadow:0 0 18px rgba(230,182,103,.7)}

@media (max-width:860px){
  .ctl{right:12px;left:12px;width:auto;top:auto;bottom:88px;transform:none}
  .legend{left:12px;bottom:12px;padding:7px 9px}
  .brand{left:14px;top:14px}.brand h1{font-size:26px}.seal{width:42px;height:42px;font-size:21px}
}
</style>
</head>
<body>
<canvas id="scene"></canvas>
<div id="vig"></div><div id="grain"></div>

<div id="ui">
  <div class="brand">
    <div class="seal">塔</div>
    <div>
      <h1>浮屠<em>園</em></h1>
      <div class="sub">Voxel Pagoda Garden</div>
      <div class="cn">五重塔 · 錦鯉 · 落櫻 · 瀑泉</div>
    </div>
  </div>

  <div class="tate">檐下燈火 · 水面倒影</div>

  <div class="ctl panel">
    <div class="sec">
      <div class="lbl">時刻 / Hour</div>
      <button class="dn" id="dn" data-night="0">
        <span class="dn-track"><span class="dn-knob"></span></span>
        <span class="dn-tx"><b id="dnB">白晝</b><i id="dnI">Daylight</i></span>
      </button>
    </div>
    <div class="sec">
      <div class="lbl">視角 / View</div>
      <div class="grid" id="views">
        <button class="b" data-v="0"><span>全景</span></button>
        <button class="b" data-v="1"><span>正門</span></button>
        <button class="b" data-v="2"><span>俯瞰</span></button>
        <button class="b" data-v="3"><span>池畔</span></button>
      </div>
    </div>
    <div class="sec">
      <div class="lbl">生態 / Life</div>
      <div class="grid">
        <button class="b on" data-t="spin"><span>自動旋轉</span></button>
        <button class="b on" data-t="petal"><span>落櫻</span></button>
        <button class="b on" data-t="fire"><span>螢火</span></button>
        <button class="b on" data-t="refl"><span>倒影</span></button>
      </div>
    </div>
    <div class="stats">
      <div class="st"><u>FPS</u><b id="sFps">–</b></div>
      <div class="st"><u>VOXELS</u><b id="sVox">–</b></div>
      <div class="st"><u>DRAWS</u><b id="sDc">–</b></div>
    </div>
  </div>

  <div class="legend panel">
    <span><kbd>拖曳</kbd>旋轉</span>
    <span><kbd>滾輪</kbd>縮放</span>
    <span><kbd>右鍵</kbd>平移</span>
    <span><kbd>L</kbd>晝夜</span>
    <span><kbd>R</kbd>復位</span>
  </div>
</div>

<div id="loader">
  <div class="lrow">
    <div>
      <div class="ltitle">浮屠<em>園</em></div>
      <div class="lmeta" id="lStage">Initialising…</div>
    </div>
    <div class="lnum"><i id="lNum">0</i><span>%</span></div>
  </div>
  <div class="lbar"><i id="lBar"></i></div>
</div>

<script type="importmap">
{"imports":{
  "three":"https://cdn.jsdelivr.net/npm/three@0.161.0/build/three.module.js",
  "three/addons/":"https://cdn.jsdelivr.net/npm/three@0.161.0/examples/jsm/"
}}
</script>

<script type="module">
import * as THREE from 'three';

/* ═══════════════════════ 0 · 工具 ═══════════════════════ */
const mul32=a=>()=>{a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296};
const rnd=mul32(0x5eed1234);
const hash2=(x,z)=>{const s=Math.sin(x*127.1+z*311.7)*43758.5453;return s-Math.floor(s)};
function vnoise(x,z){const xi=Math.floor(x),zi=Math.floor(z),xf=x-xi,zf=z-zi,
  u=xf*xf*(3-2*xf),v=zf*zf*(3-2*zf);
  const a=hash2(xi,zi),b=hash2(xi+1,zi),c=hash2(xi,zi+1),d=hash2(xi+1,zi+1);
  return (a*(1-u)+b*u)*(1-v)+(c*(1-u)+d*u)*v}
const fbm=(x,z)=>vnoise(x,z)*.6+vnoise(x*2.1+5,z*2.1-3)*.3+vnoise(x*4.7-11,z*4.7+7)*.1;
const ss=(a,b,x)=>{const t=Math.min(1,Math.max(0,(x-a)/(b-a)));return t*t*(3-2*t)};
const lerp=(a,b,t)=>a+(b-a)*t;
const clamp=(v,a,b)=>v<a?a:v>b?b:v;

/* ═══════════════════════ 1 · 调色板 ═══════════════════════ */
const _cc=new Map();
const C=h=>{if(!_cc.has(h))_cc.set(h,new THREE.Color(h));return _cc.get(h)};
const jit=(c,a=.07)=>{const n=c.clone();n.offsetHSL(0,0,(rnd()-.5)*a);return n};
const pick=l=>jit(C(l[(rnd()*l.length)|0]),.06);

const P={
  grass :['#6fae4f','#5d9c43','#7cbb5c','#548f3e'],
  dry   :['#8fa954','#7d9848'],
  dirt  :['#7b5a3c','#6b4c33','#8a6845'],
  stone :['#8d9199','#7b8087','#9aa0a6','#6e737a'],
  pave  :['#a89f8f','#988f7f','#b3aa99'],
  path  :['#b9a888','#a8977a','#c4b494'],
  sand  :['#c9b58c','#bda87f','#d3c19b'],
  rock  :['#6a6f77','#585d65','#787e86','#4e535a'],
  wDark :['#4c3122','#3e271b'],
  wood  :['#8a5a34','#7a4d2c','#96663d'],
  verm  :['#b9432f','#a53a28','#c85038'],
  plas  :['#e8e0d2','#ddd4c4','#f0eadd'],
  tile  :['#3a4a63','#33425a','#414f6b'],
  ridge :['#59697f','#4c5d78','#657592'],
  gold  :['#e6b455','#f0c96e','#d3a03f'],
  leaf  :['#4f9146','#5fa852','#3f7a3c','#6cb85c'],
  leafD :['#356b39','#2f5c33','#3d7640'],
  maple :['#d2603a','#e07a3c','#b8452f','#e8944a'],
  cherry:['#f6a9c6','#ffc0d6','#ef93b8','#ffd8e4'],
  bamboo:['#7fae5a','#94c06b','#6b9a4c'],
  water :['#4fb4d8','#3ea3cb','#63c4e2'],
  deep  :['#2c7fa8'],
  paper :['#ffe3b0','#ffd48a'],
  goldG :['#ffd07a'],
  koiO  :['#f2793a'], koiW:['#f5f1e6'], koiR:['#d8402f'],
};

/* ═══════════════════════ 2 · 体素存储 ═══════════════════════ */
const OFF=80,SPAN=176;
const key=(x,y,z)=>((x+OFF)*SPAN+(y+OFF))*SPAN+(z+OFF);
const occ=new Set(),occSolid=new Set();
const B={solid:[],glow:[],water:[]};
function put(x,y,z,c,b='solid'){
  const k=key(x,y,z); if(occ.has(k))return false;
  occ.add(k); if(b==='solid')occSolid.add(k);
  B[b].push({x,y,z,c}); return true;
}

/* ═══════════════════════ 3 · 地形参数 ═══════════════════════ */
const R=46, WATER_Y=3, WTOP=WATER_Y+1;
const PC={x:-4,z:2};                       // 宝塔中心
const T={x0:-12,x1:4,z0:-6,z1:10};         // 台基
const MESA={cx:15,cz:-14,rx:8,rz:6,top:14};// 瀑源台地
const PA={x:15,z:3,r:11.5}, PB={x:5,z:15,r:5.2}, PCv={x:0,z:16,r:3.6};
const FALL={x0:11,x1:17,z:-7,y0:3,y1:14};
const HN=2*R+1, Hmap=new Int16Array(HN*HN);
const hi=(x,z)=>(x+R)+ (z+R)*HN;
const ground=(x,z)=> (x<-R||z<-R||x>R||z>R)?-1:Hmap[hi(x,z)];

/* 园路(提前生成,决定地表材质) */
const PATH=[[-4,13],[-4,16],[4,16],[9,21],[17,23],[24,15],[23,3],[18,-4]];
const pathCells=new Set(), lantPos=[];
(function buildPath(){
  let total=0; const segs=[];
  for(let i=0;i<PATH.length-1;i++){
    const a=PATH[i],b=PATH[i+1],L=Math.hypot(b[0]-a[0],b[1]-a[1]);
    segs.push({a,b,L}); total+=L;
  }
  for(const s of segs){
    const n=Math.ceil(s.L/0.5);
    for(let j=0;j<=n;j++){
      const t=j/n,x=s.a[0]+(s.b[0]-s.a[0])*t,z=s.a[1]+(s.b[1]-s.a[1])*t;
      const dx=(s.b[0]-s.a[0])/s.L,dz=(s.b[1]-s.a[1])/s.L;
      for(let w=-1;w<=1;w++) pathCells.add(((Math.round(x-dz*w))+200)*1000+(Math.round(z+dx*w)+200));
    }
  }
  let acc=0;
  for(const s of segs){
    const ux=(s.b[0]-s.a[0])/s.L,uz=(s.b[1]-s.a[1])/s.L;
    for(let d=4; d<s.L; d+=8){
      acc++;
      const side=(acc%2)?1:-1;
      lantPos.push([Math.round(s.a[0]+ux*d-dz2(uz)*2.6*side),
                    Math.round(s.a[1]+uz*2*0+uz*d-dx2(ux)*0)]);
    }
  }
  function dz2(uz){return uz} function dx2(ux){return ux}
})();
lantPos.length=0;
for(const s of (function(){const o=[];let L=0;for(let i=0;i<PATH.length-1;i++){const a=PATH[i],b=PATH[i+1];const l=Math.hypot(b[0]-a[0],b[1]-a[1]);o.push({a,b,l,L});L+=l}return o})()){
  const ux=(s.b[0]-s.a[0])/s.l,uz=(s.b[1]-s.a[1])/s.l;
  for(let d=5;d<s.l-1;d+=9){
    const side=(lantPos.length%2)?1:-1;
    lantPos.push([Math.round(s.a[0]+ux*d-uz*2.6*side), Math.round(s.a[1]+uz*d+ux*2.6*side)]);
  }
}
const onPath=(x,z)=>pathCells.has((x+200)*1000+(z+200));

function pondDip(x,z){
  let d=0;
  for(const c of [PA,PB,PCv]){
    const r=c.r+(fbm(x*.21+c.x,z*.21+c.z)-.5)*2.4;
    const dd=Math.hypot(x-c.x,z-c.z);
    if(dd<r) d=Math.max(d,Math.pow(1-dd/r,.9));
  }
  return d;
}
function mesaF(x,z){
  const dx=Math.abs(x-MESA.cx)-MESA.rx, dz=Math.abs(z-MESA.cz)-MESA.rz;
  return Math.max(dx,dz)+ (fbm(x*.16+3,z*.16-2)-.5)*2.6;
}
function heightAt(x,z){
  const d=Math.hypot(x,z);
  let h=4.4+fbm(x*.055,z*.055)*2.8-1.0;
  h+=Math.max(0,3.4-Math.hypot(x+25,z+9)*.115);
  h+=Math.max(0,2.8-Math.hypot(x-26,z-20)*.13);
  h-=ss(31,44,d)*13;
  const tf=ss(-1,3.2,Math.max(Math.abs(x),Math.abs(z))-11.6);
  const dip=pondDip(x,z);
  if(dip>0) h-=dip*6.4*tf;
  const m=mesaF(x,z);
  if(m<0) h=Math.max(h,MESA.top+m*.55+fbm(x*.22,z*.22)*1.1);
  if(x>=FALL.x0-1&&x<=FALL.x1+2&&z>=-12&&z<=-8) h=Math.min(h,12);  // 引水槽
  if(Math.max(Math.abs(x-PC.x),Math.abs(z-PC.z))<=12.5) h=Math.min(h,0); // 台基下平整
  return Math.round(h);
}
function surfCol(x,z,h){
  if(onPath(x,z)&&h>WATER_Y) return pick(P.path);
  if(h<=WATER_Y+1&&pondDip(x,z)>.05) return pick(P.sand);
  if(h<=WATER_Y+1) return pick(P.sand);
  if(h>=11) return pick(P.grass);
  if(pondDip(x,z)>.02&&pondDip(x,z)<.22) return pick(P.sand);
  return pick(h>7?P.dry:P.grass);
}
function buildTerrain(){
  for(let x=-R;x<=R;x++)for(let z=-R;z<=R;z++) Hmap[hi(x,z)]=heightAt(x,z);
  for(let x=-R;x<=R;x++)for(let z=-R;z<=R;z++){
    const h=Hmap[hi(x,z)]; if(h<0)continue;
    let mn=h;
    for(const[dx,dz]of[[1,0],[-1,0],[0,1],[0,-1]]){
      const n=(x+dx<-R||z+dz<-R||x+dx>R||z+dz>R)?-1:Hmap[hi(x+dx,z+dz)];
      if(n<mn)mn=n;
    }
    const bot=Math.max(-1,Math.min(h-1,mn-1));
    for(let y=h;y>=bot;y--){
      const dep=h-y; let c;
      if(dep===0)c=surfCol(x,z,h);
      else if(dep<=2&&h<9)c=pick(P.dirt);
      else c=pick(P.stone);
      if(y<=1)c=pick(P.rock);
      put(x,y,z,c);
    }
  }
}

/* ═══════════════════════ 4 · 台基 / 阶梯 / 栏楯 ═══════════════════════ */
function buildTerrace(){
  for(let x=T.x0;x<=T.x1;x++)for(let z=T.z0;z<=T.z1;z++)
    for(let y=1;y<=6;y++){
      const top=y===6, edge=(x===T.x0||x===T.x1||z===T.z0||z===T.z1);
      let c;
      if(top) c=((x+z)&1)?C('#b0a795'):C('#9c9382');
      else if(edge) c=pick(P.stone);
      else c=pick(P.rock);
      put(x,y,z,c);
    }
  // 石阶(南)
  for(let x=-6;x<=-2;x++){put(x,5,T.z1+1,pick(P.stone));put(x,4,T.z1+2,pick(P.stone));}
  for(let x=-6;x<=-2;x++)put(x,6,T.z1+1,pick(P.pave));
  // 栏楯
  const rail=(x,z)=>{put(x,7,z,pick(P.stone));put(x,8,z,C('#8f8676'))};
  for(let x=T.x0;x<=T.x1;x++)for(let z=T.z0;z<=T.z1;z++){
    const e=(x===T.x0||x===T.x1||z===T.z0||z===T.z1); if(!e)continue;
    if(((x*T.z1)%2===0)||(z*T.x1)%2===0){}
    if(z===T.z1&&x>=-7&&x<=-1)continue;
    if(((x+z)&1)===0) rail(x,z);
  }
  // 朱漆小门
  const gx=-4,gz=T.z1+3;
  for(let y=3;y<=8;y++){put(gx-2,y,gz,pick(P.verm));put(gx+2,y,gz,pick(P.verm));}
  for(let x=gx-3;x<=gx+3;x++){put(x,9,gz,C('#c8503a'));put(x,10,gz,C('#3a4a63'))}
  put(gx,11,gz,C('#e6b455'));
}

/* ═══════════════════════ 5 · 五重塔 ═══════════════════════ */
const lanternPts=[];
function hangLantern(x,y,z,light){
  put(x,y,z,C('#2a2118'),'glow');
  put(x,y-1,z,pick(P.paper),'glow');
  put(x,y-2,z,pick(P.paper),'glow');
  put(x,y-3,z,C('#3a2a20'),'glow');
  if(light)lanternPts.push([x,y-2,z,1]);
}
function story(i,w,y,wh){
  const X=PC.x,Z=PC.z;
  if(i>0)for(let dx=-w;dx<=w;dx++)for(let dz=-w;dz<=w;dz++)
    put(X+dx,y-1,Z+dz,pick(P.wood));
  for(let yy=y;yy<y+wh;yy++){
    for(let dx=-w;dx<=w;dx++)for(let dz=-w;dz<=w;dz++){
      const ax=Math.abs(dx),az=Math.abs(dz);
      if(Math.max(ax,az)!==w)continue;
      const along=ax>=az?dz:dx, corner=ax===w&&az===w;
      if(Math.abs(along)<=1&&yy<y+(i===0?6:3))continue;     // 门洞
      if(corner){put(X+dx,yy,Z+dz,pick(P.wDark));continue}
      if(yy===y+wh-1){put(X+dx,yy,Z+dz,pick(P.wDark));continue}
      if(Math.abs(along)%4===0&&(yy-y)%2===0){put(X+dx,yy,Z+dz,pick(P.wood));continue}
      if(yy>=y+2&&yy<=y+3&&Math.abs(along)%3===1&&yy<y+wh-2){put(X+dx,yy,Z+dz,pick(P.paper),'glow');continue}
      put(X+dx,yy,Z+dz,pick(P.plas));
    }
  }
  // 门内辉光
  const gd=i===0?5:3;
  for(let a=-1;a<=1;a++)for(let yy=y;yy<y+gd;yy++){
    put(X+a, y, Z+w-1, pick(P.goldG),'glow');
    put(X+a, yy, Z+w-1, pick(P.goldG),'glow');
    put(X+a, yy, Z-w+1, pick(P.goldG),'glow');
    put(X+w-1,yy,Z+a, pick(P.goldG),'glow');
    put(X-w+1,yy,Z+a, pick(P.goldG),'glow');
  }
  // 平座 + 勾栏
  const vy=y-1;
  for(let r=w+1;r<=w+2;r++)for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){
    if(Math.max(Math.abs(dx),Math.abs(dz))!==r)continue;
    put(X+dx,vy,Z+dz,pick(P.wood));
  }
  for(let r=w+2;r<=w+2;r++){
    for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){
      if(Math.max(Math.abs(dx),Math.abs(dz))!==r)continue;
      const along=Math.abs(dx)>=Math.abs(dz)?dz:dx;
      if(i===0&&dz===r&&Math.abs(along)<=2)continue;
      if(((dx+dz)&1)===0){put(X+dx,y,Z+dz,pick(P.verm));put(X+dx,y+1,Z+dz,pick(P.verm))}
      put(X+dx,y+2,Z+dz,C('#d8a24e'));
    }
  }
  // 铺作(檐下天花)
  for(let r=w+1;r<=w+3;r++)for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){
    if(Math.max(Math.abs(dx),Math.abs(dz))!==r)continue;
    put(X+dx,y+wh-1,Z+dz,pick(P.wDark));
  }
}
function roof(i,w,baseY,Rr,rInner,rise){
  const X=PC.x,Z=PC.z;
  for(let r=rInner;r<=Rr;r++){
    const yy=baseY+rise(r);
    for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){
      const ax=Math.abs(dx),az=Math.abs(dz);
      if(Math.max(ax,az)!==r)continue;
      const t=r===0?0:Math.min(ax,az)/r;
      const outer=(r-rInner)/Math.max(1,Rr-rInner);
      const lift=Math.round(Math.pow(t,2.2)*(0.5+1.7*outer));
      const along=ax>=az?az:ax;
      put(X+dx,yy+lift,Z+dz, along%3===0?pick(P.ridge):pick(P.tile));
    }
  }
  for(const sx of[-1,1])for(const sz of[-1,1]){
    put(X+sx*(Rr+1),baseY+2,Z+sz*(Rr+1),pick(P.ridge));
    put(X+sx*(Rr+2),baseY+3,Z+sz*(Rr+2),pick(P.ridge));
    put(X+sx*(Rr+2),baseY+2,Z+sz*(Rr+2),pick(P.tile));
    hangLantern(X+sx*(Rr+1),baseY+1,Z+sz*(Rr+1),i===0);
  }
  if(i<2)for(const s of[[Rr,0],[-Rr,0],[0,Rr],[0,-Rr]])
    hangLantern(X+s[0],baseY-1,Z+s[1],i===0);
}
function buildPagoda(){
  const W=[8,7,6,5,4]; let y=7;
  for(let i=0;i<5;i++){
    const w=W[i], wh=i===0?8:5, Rr=w+3, rIn=i<4?W[i+1]:0;
    const pk=Math.round(Rr*.78);
    const rise=r=>Math.round(Math.pow((Rr-r)/Rr,1.35)*pk);
    story(i,w,y,wh);
    roof(i,w,y+wh,Rr,rIn,rise);
    y=y+wh+rise(i<4?W[i+1]:0)+1;
  }
  for(let i=0;i<7;i++)put(PC.x,y+i,PC.z,pick(P.gold));
  for(let k=1;k<=3;k++){const yy=y+k*2;
    for(const s of[[1,0],[-1,0],[0,1],[0,-1]])put(PC.x+s[0],yy,PC.z+s[1],C('#d3a03f'));}
  put(PC.x,y+7,PC.z,pick(P.goldG),'glow');
  put(PC.x,y+8,PC.z,pick(P.goldG),'glow');
  lanternPts.push([PC.x,y+7,PC.z,1.4]);
  lanternPts.push([PC.x,10,PC.z+9,1.2]);
}

/* ═══════════════════════ 6 · 瀑布 · 池塘 ═══════════════════════ */
function buildWater(){
  for(let x=-R;x<=R;x++)for(let z=-R;z<=R;z++){
    const h=Hmap[hi(x,z)];
    if(h<0||h>=WATER_Y)continue;
    if(pondDip(x,z)<.06)continue;
    put(x,WATER_Y,z,pick(P.water),'water');
  }
  for(let x=FALL.x0;x<=FALL.x1;x++)
    for(let y=FALL.y0;y<=FALL.y1;y++)
      put(x,y,FALL.z,pick(P.water),'water');
  for(let x=FALL.x0;x<=FALL.x1;x++)for(let z=-11;z<=-8;z++)for(let y=13;y<=14;y++)
    put(x,y,z,pick(P.water),'water');
  // 崖底岩块
  for(let i=0;i<26;i++){
    const x=FALL.x0-2+((rnd()*12)|0), z=-6+((rnd()*5)|0), y=WATER_Y-((rnd()*2)|0);
    blob(x,y,z,1+((rnd()*1.4)|0),P.rock,.4,.75);
  }
  for(let i=0;i<40;i++){ // 湖中石
    const a=rnd()*6.283,d=4+rnd()*8;
    const x=Math.round(PA.x+Math.cos(a)*d), z=Math.round(PA.z+Math.sin(a)*d);
    if(ground(x,z)>WATER_Y)continue;
    blob(x,WATER_Y,z,1+((rnd()*1.2)|0),P.rock,.35,.7);
  }
}
function blob(cx,cy,cz,r,cols,holes=0.18,sq=1){
  const ry=Math.max(1,Math.round(r*sq));
  for(let x=-r;x<=r;x++)for(let y=-ry;y<=ry;y++)for(let z=-r;z<=r;z++){
    const d=Math.sqrt(x*x+y*y+z*z);
    if(d>r+.35)continue;
    if(d>r-1.15&&rnd()<holes)continue;
    put(cx+x,cy+y,cz+z,pick(cols));
  }
}

/* ═══════════════════════ 7 · 花木 ═══════════════════════ */
const cherries=[];
function pine(x,y,z){
  const h=6+((rnd()*4)|0);
  for(let i=0;i<h;i++)put(x,y+i,z,pick(P.wDark));
  for(let l=0;l<4;l++){
    const r=Math.max(1,3.3-l*.75);
    blob(x,y+h-l*2,z,r,P.leafD,.28,.62);
  }
}
function oak(x,y,z,maple){
  const h=3+((rnd()*3)|0);
  for(let i=0;i<h;i++)put(x,y+i,z,pick(P.wood));
  const cols=maple?P.maple:P.leaf;
  blob(x,y+h+1,z,2.4+((rnd()*1.2)|0),cols,.2,.85);
  blob(x+((rnd()*3)|0)-1,y+h+2,z+((rnd()*3)|0)-1,1.6+((rnd()*1)|0),cols,.3,.8);
}
function cherry(x,y,z){
  const h=3+((rnd()*2)|0);
  for(let i=0;i<h;i++)put(x,y+i,z,pick(P.wDark));
  if(rnd()<.6)put(x+(rnd()<.5?1:-1),y+h,z,pick(P.wDark));
  for(let i=0;i<3;i++)
    blob(x+((rnd()*5)|0)-2,y+h+1+((rnd()*2)|0),z+((rnd()*5)|0)-2,
      2+((rnd()*1.6)|0),P.cherry,.26,.8);
  cherries.push([x,y+h+3,z]);
}
function bamboo(x,y,z){
  const h=8+((rnd()*7)|0);
  for(let i=0;i<h;i++)put(x,y+i,z,i%3===0?C('#6b9a4c'):pick(P.bamboo));
  blob(x,y+h,z,1.4,P.bamboo,.45,.9);
}
function bush(x,y,z){
  blob(x,y,z,1+((rnd()*1.2)|0),rnd()<.75?P.leaf:P.maple,.35,.8);
  if(rnd()<.45){
    const fx=x+((rnd()*3)|0)-1,fz=z+((rnd()*3)|0)-1;
    put(fx,y+2,fz,rnd()<.5?C('#f5b7cd'):C('#f2e07a'));
  }
}
function buildFlora(){
  // 竹林
  for(let i=0;i<70;i++){
    const x=-24+((rnd()*9)|0), z=4+((rnd()*10)|0), y=ground(x,z);
    if(y>WATER_Y) bamboo(x,y+1,z);
  }
  const tries=2600;
  for(let i=0;i<tries;i++){
    const a=rnd()*6.283, d=Math.sqrt(rnd())*40;
    const x=Math.round(Math.cos(a)*d), z=Math.round(Math.sin(a)*d);
    const y=ground(x,z);
    if(y<=WATER_Y)continue;
    if(Math.max(Math.abs(x-PC.x),Math.abs(z-PC.z))<15)continue;
    let near=false;
    for(let ox=-2;ox<=2&&!near;ox++)for(let oz=-2;oz<=2&&!near;oz++)if(onPath(x+ox,z+oz))near=true;
    if(near)continue;
    let mn=y,mx=y;
    for(const[dx,dz]of[[2,0],[-2,0],[0,2],[0,-2]]){const n=ground(x+dx,z+dz);if(n<mn)mn=n;if(n>mx)mx=n}
    if(mx-mn>4)continue;
    if(mesaF(x,z)>-2.5)continue;
    let tooClose=false;
    for(let ox=-2;ox<=2&&!tooClose;ox++)for(let oz=-2;oz<=2&&!oz;oz++){
      if(occ.has(key(x+ox,y+1,z+oz)))tooClose=true;
    }
    if(tooClose)continue;
    const inMesa=mesaF(x,z)<0;
    const nearPond=pondDip(x,z)>0&&pondDip(x,z)<.35;
    const r=rnd();
    if(inMesa) pine(x,y+1,z);
    else if(nearPond&&r<.62) cherry(x,y+1,z);
    else if(r<.24) cherry(x,y+1,z);
    else if(r<.36) oak(x,y+1,z,true);
    else if(r<.62) pine(x,y+1,z);
    else oak(x,y+1,z,false);
  }
  for(let i=0;i<420;i++){
    const a=rnd()*6.283,d=Math.sqrt(rnd())*41;
    const x=Math.round(Math.cos(a)*d),z=Math.round(Math.sin(a)*d),y=ground(x,z);
    if(y<=WATER_Y+1||Math.max(Math.abs(x-PC.x),Math.abs(z-PC.z))<13)continue;
    if(onPath(x,z)||onPath(x+1,z)||onPath(x,z+1))continue;
    if(occ.has(key(x,y+1,z)))continue;
    bush(x,y+1,z);
  }
  for(let i=0;i<180;i++){ // 岩块
    const a=rnd()*6.283,d=6+Math.sqrt(rnd())*36;
    const x=Math.round(Math.cos(a)*d),z=Math.round(Math.sin(a)*d),y=ground(x,z);
    if(y<0||occ.has(key(x,y+1,z)))continue;
    blob(x,y+1,z,1+((rnd()*1.5)|0),P.rock,.4,.7);
  }
}

/* ═══════════════════════ 8 · 石灯 · 桥 ═══════════════════════ */
function stoneLantern(x,y,z){
  put(x,y,z,pick(P.rock));put(x,y+1,z,pick(P.rock));
  put(x,y+2,z,pick(P.stone));put(x,y+3,z,pick(P.stone));
  put(x,y+4,z,pick(P.paper),'glow');
  for(const s of[[1,0],[-1,0],[0,1],[0,-1]])put(x+s[0],y+4,z+s[1],pick(P.rock));
  for(let dx=-1;dx<=1;dx++)for(let dz=-1;dz<=1;dz++)put(x+dx,y+5,z+dz,pick(P.stone));
  put(x,y+6,z,C('#9aa0a6'));
  lanternPts.push([x,y+4,z,.75]);
}
function poleLantern(x,y,z){
  for(let i=0;i<5;i++)put(x,y+i,z,pick(P.wDark));
  put(x,y+5,z,pick(P.paper),'glow');
  put(x,y+6,z,pick(P.paper),'glow');
  put(x,y+7,z,C('#b9432f'));
  lanternPts.push([x,y+5,z,.9]);
}
function bridge(){
  const z0=16,x0=-4,x1=4;
  for(let x=x0;x<=x1;x++){
    const t=(x-x0)/(x1-x0), y=5+Math.round(Math.sin(t*Math.PI)*2.4);
    put(x,y,z0,pick(P.verm));put(x,y,z0+1,pick(P.verm));
    if((x-x0)%2===0){put(x,y+1,z0-1,C('#c8503a'));put(x,y+1,z0+2,C('#c8503a'));}
    put(x,y+2,z0-1,C('#d8503a'));put(x,y+2,z0+2,C('#d8503a'));
    const g=ground(x,z0);
    if(g>=0&&g<y-1)for(let yy=g+1;yy<y;yy++){put(x,yy,z0,pick(P.wDark));put(x,yy,z0+1,pick(P.wDark));}
  }
  lanternPts.push([0,9,16,.8]);
}
function buildProps(){
  for(const p of lantPos){
    const y=ground(p[0],p[1]);
    if(y>WATER_Y) stoneLantern(p[0],y+1,p[1]);
  }
  for(const s of[[-9,-5],[5,-5],[-13,11],[5,11]]){
    const y=ground(s[0],s[1]); if(y>0) stoneLantern(s[0],y+1,s[1]);
  }
  bridge();
  for(const p of[[6,20],[19,19],[22,7],[-2,19],[14,-6]]){
    const y=ground(p[0],p[1]); if(y>WATER_Y) poleLantern(p[0],y+1,p[1]);
  }
}

/* ═══════════════════════ 9 · 渲染器 / 场景 ═══════════════════════ */
const canvas=document.getElementById('scene');
const renderer=new THREE.WebGLRenderer({canvas,antialias:true,powerPreference:'high-performance'});
renderer.setPixelRatio(Math.min(devicePixelRatio,1.6));
renderer.setSize(innerWidth,innerHeight);
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.localClippingEnabled=true;

const scene=new THREE.Scene();
scene.fog=new THREE.Fog(0xcfe3f2,80,360);
const camera=new THREE.PerspectiveCamera(52,innerWidth/innerHeight,.5,1600);

const hemi=new THREE.HemisphereLight(0xcfe6ff,0x6b7a4e,.95); scene.add(hemi);
const sun=new THREE.DirectionalLight(0xfff2d6,2.45);
sun.castShadow=true; sun.shadow.mapSize.set(2048,2048);
sun.shadow.camera.left=-52;sun.shadow.camera.right=52;
sun.shadow.camera.top=52;sun.shadow.camera.bottom=-52;
sun.shadow.camera.near=1;sun.shadow.camera.far=260;
sun.shadow.bias=-0.0012; sun.shadow.normalBias=.06;
scene.add(sun); scene.add(sun.target); sun.target.position.set(0,12,0);
const amb=new THREE.AmbientLight(0xffffff,.14); scene.add(amb);

/* 天空 */
const skyU={topC:{value:new THREE.Color('#4a86c9')},botC:{value:new THREE.Color('#cfe6f5')},
  sunDir:{value:new THREE.Vector3(0,.8,.4)},sunC:{value:new THREE.Color('#fff0d4')},glow:{value:1}};
const sky=new THREE.Mesh(new THREE.SphereGeometry(700,32,18),new THREE.ShaderMaterial({
  side:THREE.BackSide,depthWrite:false,uniforms:skyU,
  vertexShader:`varying vec3 vD;void main(){vD=position;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.);}`,
  fragmentShader:`varying vec3 vD;uniform vec3 topC,botC,sunC,sunDir;uniform float glow;
   void main(){vec3 d=normalize(vD);float h=clamp(d.y*.5+.5,0.,1.);
   vec3 c=mix(botC,topC,pow(h,.8));
   float sd=max(dot(d,normalize(sunDir)),0.);
   c+=sunC*pow(sd,26.)*1.3*glow; c+=sunC*pow(sd,4.)*.16*glow;
   c+=vec3(.10,.16,.28)*pow(1.-abs(d.y),8.)*(1.-glow);
   gl_FragColor=vec4(c,1.);}`
}));
sky.renderOrder=-20; scene.add(sky);

/* 星 */
const starGeo=new THREE.BufferGeometry();
{const n=1400,a=new Float32Array(n*3);
 for(let i=0;i<n;i++){const u=rnd()*6.283,v=Math.acos(rnd()*.95+.02),r=640;
  a[i*3]=Math.sin(v)*Math.cos(u)*r;a[i*3+1]=Math.cos(v)*r*.9+40;a[i*3+2]=Math.sin(v)*Math.sin(u)*r;}
 starGeo.setAttribute('position',new THREE.BufferAttribute(a,3));}
const starMat=new THREE.PointsMaterial({size:2.4,sizeAttenuation:false,transparent:true,opacity:0,
  color:0xdfe8ff,depthWrite:false});
const stars=new THREE.Points(starGeo,starMat); stars.renderOrder=-15; scene.add(stars);

/* 日月盘 */
function disc(col,r){const m=new THREE.Mesh(new THREE.CircleGeometry(r,26),
  new THREE.MeshBasicMaterial({color:col,fog:false,transparent:true,depthWrite:false}));
  m.renderOrder=-14;scene.add(m);return m}
const sunDisc=disc(0xfff2c4,17), moonDisc=disc(0xdfe9ff,11);
moonDisc.material.transparent=true;

/* 海平面 */
const wU={uTime:{value:0},uNight:{value:0}};
function waterMat(op,foam,spec){
  const m=new THREE.MeshPhongMaterial({color:0xffffff,transparent:true,opacity:op,
    shininess:88,specular:new THREE.Color(spec),depthWrite:false});
  m.onBeforeCompile=sh=>{
    Object.assign(sh.uniforms,wU,{uFoam:{value:foam}});
    sh.vertexShader='varying vec3 vWP;\n'+sh.vertexShader.replace('#include <project_vertex>',
      `{vec4 wp=vec4(transformed,1.);
        #ifdef USE_INSTANCING
         wp=instanceMatrix*wp;
        #endif
        vWP=(modelMatrix*wp).xyz;}
       #include <project_vertex>`);
    sh.fragmentShader='varying vec3 vWP;uniform float uTime,uNight,uFoam;\n'+sh.fragmentShader
     .replace('#include <color_fragment>',`#include <color_fragment>
       vec2 p=vWP.xz;
       float rip=sin(p.x*1.05+uTime*1.5)*.45+sin(p.y*1.25-uTime*1.15)*.35+sin((p.x+p.y)*2.2+uTime*2.4)*.2;
       float fall=smoothstep(4.6,5.8,vWP.y);
       float stripe=.5+.5*sin(vWP.y*3.2-uTime*13.+sin(p.x*1.3)*1.4);
       diffuseColor.rgb*=mix(1.+rip*.12,.86+stripe*.42,fall);
       float foam=uFoam*fall*smoothstep(7.2,4.,vWP.y)*(.35+.65*stripe);
       foam+=uFoam*.22*smoothstep(.4,0.,abs(vWP.y-4.))*(.4+.6*sin(p.x*.8+p.y*.7+uTime*2.));`)
     .replace('#include <emissivemap_fragment>',`#include <emissivemap_fragment>
       totalEmissiveRadiance+=vec3(.55,.78,.95)*foam*(.35+.55*uNight);`);
  };
  return m;
}
const oceanMat=waterMat(.80,.15,0x8fc8ee);
oceanMat.color=C('#1f6d9c');
const ocean=new THREE.Mesh(new THREE.PlaneGeometry(1500,1500),oceanMat);
ocean.rotation.x=-Math.PI/2; ocean.position.y=2.6; ocean.renderOrder=-4; scene.add(ocean);

/* ═══════════════════════ 10 · 实例化 ═══════════════════════ */
const box=new THREE.BoxGeometry(1,1,1);
const DIRS=[[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]];
let solidMat,glowMat,pondMat,fallMat,reflSolidMat,reflGlowMat,stencilMat;
let mSolid,mGlow,mPond,mFall,mReflS,mReflG;

function mkInst(list,mat,cast,recv,order){
  const m=new THREE.InstancedMesh(box,mat,list.length);
  const M=new THREE.Matrix4();
  for(let i=0;i<list.length;i++){const b=list[i];M.makeTranslation(b.x+.5,b.y+.5,b.z+.5);
    m.setMatrixAt(i,M);m.setColorAt(i,b.c);}
  m.castShadow=!!cast;m.receiveShadow=!!recv;
  if(order!==undefined)m.renderOrder=order;
  m.frustumCulled=false;
  scene.add(m);return m;
}
function finalize(){
  const n0=B.solid.length,out=[];
  for(const b of B.solid){
    let hid=true;
    for(const d of DIRS){if(!occSolid.has(key(b.x+d[0],b.y+d[1],b.z+d[2]))){hid=false;break}}
    if(!hid)out.push(b);
  }
  B.solid=out;
  console.log('体素:生成 %d → 剔除后 %d',n0,out.length);

  solidMat=new THREE.MeshLambertMaterial({});
  glowMat =new THREE.MeshBasicMaterial({toneMapped:false});
  pondMat =waterMat(.70,1.0,0xb6e6ff); pondMat.color=C('#5cc0e0');
  fallMat =waterMat(.62,1.6,0xd6f2ff); fallMat.color=C('#7fd4ee');

  const refl=[],reflG=[];
  for(const b of B.solid){const dx=b.x-15,dz=b.z-3;
    if(b.y>WATER_Y&&dx*dx+dz*dz<980)refl.push(b)}
  for(const b of B.glow){const dx=b.x-15,dz=b.z-3;
    if(b.y>WATER_Y&&dx*dx+dz*dz<980)reflG.push(b)}

  const clipBelow=new THREE.Plane(new THREE.Vector3(0,-1,0),WTOP);
  reflSolidMat=new THREE.MeshBasicMaterial({transparent:true,opacity:.30,depthTest:false,
    depthWrite:false,side:THREE.DoubleSide,fog:false});
  reflSolidMat.clippingPlanes=[clipBelow];
  reflGlowMat=new THREE.MeshBasicMaterial({transparent:true,opacity:.55,depthTest:false,
    depthWrite:false,side:THREE.DoubleSide,toneMapped:false,fog:false});
  reflGlowMat.clippingPlanes=[clipBelow];
  stencilMat=new THREE.MeshBasicMaterial({colorWrite:false,depthWrite:false,transparent:true,opacity:0});
  stencilMat.stencil={test:false,write:true,func:THREE.AlwaysStencilFunc,ref:1,
    readMask:0xff,writeMask:0xff,fail:THREE.KeepStencilOp,zfail:THREE.KeepStencilOp,
    zpass:THREE.ReplaceStencilOp};
  const reflTest={test:true,write:false,func:THREE.EqualStencilFunc,ref:1,readMask:0xff,
    writeMask:0xff,fail:THREE.KeepStencilOp,zfail:THREE.KeepStencilOp,zpass:THREE.KeepStencilOp};
  reflSolidMat.stencil=reflTest; reflGlowMat.stencil=reflTest;

  mSolid=mkInst(B.solid,solidMat,true,true);
  mGlow =mkInst(B.glow,glowMat,false,false);
  const pond=[],fall=[];
  for(const b of B.water)(b.z===FALL.z&&b.y>FALL.y0?fall:pond).push(b);
  mPond=mkInst(pond,pondMat,false,false,3);
  mFall=mkInst(fall,fallMat,false,false,3);
  const mStencil=new THREE.InstancedMesh(box,stencilMat,pond.length);
  {const M=new THREE.Matrix4();for(let i=0;i<pond.length;i++){const b=pond[i];
    M.makeTranslation(b.x+.5,b.y+.5,b.z+.5);mStencil.setMatrixAt(i,M);} }
  mStencil.renderOrder=1;mStencil.frustumCulled=false;scene.add(mStencil);

  mReflS=mkInst(refl,reflSolidMat,false,false,2);
  mReflG=mkInst(reflG,reflGlowMat,false,false,2);
  for(const m of[mReflS,mReflG]){m.scale.set(1,-1,1);m.position.set(0,2*WTOP,0);}

  window.__vox=(B.solid.length+B.glow.length+B.water.length);

  /* 灯笼点光 + 光晕贴片 */
  const cvs=document.createElement('canvas');cvs.width=cvs.height=64;
  const g2=cvs.getContext('2d'),grd=g2.createRadialGradient(32,32,0,32,32,32);
  grd.addColorStop(0,'rgba(255,225,170,.95)');grd.addColorStop(.35,'rgba(255,190,120,.35)');
  grd.addColorStop(1,'rgba(255,180,90,0)');g2.fillStyle=grd;g2.fillRect(0,0,64,64);
  const haloTex=new THREE.CanvasTexture(cvs);
  const chosen=lanternPts.filter((_,i)=>i%2===0).slice(0,9);
  for(const p of lanternPts){
    const s=new THREE.Sprite(new THREE.SpriteMaterial({map:haloTex,transparent:true,opacity:0,
      depthWrite:false,blending:THREE.AdditiveBlending,fog:false}));
    s.position.set(p[0],p[1],p[2]);s.scale.setScalar(7*p[3]);s.renderOrder=6;scene.add(s);
    haloList.push(s);
  }
  for(const p of chosen){
    const L=new THREE.PointLight(0xffc878,0,34,2);
    L.position.set(p[0],p[1],p[2]);scene.add(L);
    lampLights.push({l:L,b:150*p[3],ph:rnd()*9});
  }
}
const lampLights=[],haloList=[];

/* ═══════════════════════ 11 · 活物:锦鲤·落樱·萤火·云 ═══════════════════════ */
let koiMesh,petalMesh,fireMesh,cloudMesh;
const kois=[],petals=[],fires=[],clouds=[];
function initLife(){
  // 锦鲤
  for(let i=0;i<16;i++){
    const c=rnd()<.55?C('#f2793a'):rnd()<.5?C('#f5f1e6'):C('#d8402f');
    kois.push({cx:rnd()<.75?PA.x:PB.x,cz:rnd()<.75?PA.z:PB.z,r:2+rnd()*7,
      a:rnd()*6.28,s:.12+rnd()*.28,ph:rnd()*6,c});
  }
  koiMesh=mkInstList(48,new THREE.MeshLambertMaterial());
  // 花瓣
  for(let i=0;i<420;i++)petals.push(newPetal(true));
  petalMesh=mkInstList(420,new THREE.MeshLambertMaterial());
  // 萤火
  for(let i=0;i<170;i++){
    const a=rnd()*6.28,d=Math.sqrt(rnd())*40;
    const x=Math.cos(a)*d,z=Math.sin(a)*d;
    fires.push({x,z,y:Math.max(5,ground(Math.round(x),Math.round(z))+2+rnd()*4),
      ph:rnd()*6.28,r:1.5+rnd()*3.5,s:.4+rnd()*.9});
  }
  fireMesh=mkInstList(170,new THREE.MeshBasicMaterial({toneMapped:false,transparent:true,
    opacity:0,blending:THREE.AdditiveBlending,depthWrite:false}));
  fireMesh.renderOrder=5;
  // 云
  const cl=[];
  for(let i=0;i<30;i++){
    const cx=(rnd()-.5)*300,cy=46+rnd()*16,cz=(rnd()-.5)*300,parts=[];
    for(let j=0;j<5;j++)parts.push([cx+(rnd()-.5)*16,cy+(rnd()-.5)*3,cz+(rnd()-.5)*13,
      6+rnd()*11,1.6+rnd()*2,5+rnd()*8]);
    clouds.push({parts,vx:.35+rnd()*.5});
    for(const p of parts)cl.push(p);
  }
  cloudMesh=new THREE.InstancedMesh(box,new THREE.MeshLambertMaterial(),cl.length);
  cloudMesh.frustumCulled=false;
  const M=new THREE.Matrix4();
  cloudMesh.instanceColor=new THREE.InstancedBufferAttribute(new Float32Array(cl.length*3),3);
  for(let i=0;i<cl.length;i++){cloudMesh.setColorAt(i,C('#ffffff'));M.identity();cloudMesh.setMatrixAt(i,M);}
  scene.add(cloudMesh);
  window.__clouds=cl;
}
function mkInstList(n,mat){
  const m=new THREE.InstancedMesh(box,mat,n);
  m.instanceColor=new THREE.InstancedBufferAttribute(new Float32Array(n*3),3);
  m.frustumCulled=false;scene.add(m);return m;
}
function newPetal(init){
  const t=cherries.length?cherries[(rnd()*cherries.length)|0]:[-4,26,2];
  return{x:t[0]+(rnd()-.5)*11,y:t[1]+rnd()*7,z:t[2]+(rnd()-.5)*11,
    vy:1.1+rnd()*1.4,ph:rnd()*6.28,sp:.6+rnd()*1.2,
    c:rnd()<.5?C('#ffc0d6'):rnd()<.5?C('#f6a9c6'):C('#ffd8e4'),
    rx:rnd()*6.28,rz:rnd()*6.28};
}
const _m4=new THREE.Matrix4(),_q=new THREE.Quaternion(),_v=new THREE.Vector3(),_s=new THREE.Vector3();
function updLife(t,dt,night){
  // 锦鲤
  {let i=0;
   for(const k of kois){
     k.a+=k.s*dt;
     const wig=Math.sin(t*4+k.ph)*.5;
     const px=k.cx+Math.cos(k.a)*k.r, pz=k.cz+Math.sin(k.a)*k.r;
     const hx=-Math.sin(k.a),hz=Math.cos(k.a);
     for(let s=0;s<3;s++){
       const off=(1-s)*.75;
       _v.set(px-hx*off+wig*hz*.5, 3.35+Math.sin(t*2+k.ph)*0.06, pz-hz*off-wig*hx*.5);
       _q.setFromAxisAngle(_up,k.a+Math.PI/2);
       _s.set(s===2?.55:.85,.42,s===0?.75:.6);
       _m4.compose(_v,_q,_s);koiMesh.setMatrixAt(i,_m4);koiMesh.setColorAt(i,s===2?C('#f5f1e6'):k.c);i++;
     }
   }
   koiMesh.count=i;koiMesh.instanceMatrix.needsUpdate=true;koiMesh.instanceColor.needsUpdate=true;}
  // 花瓣
  if(flags.petal){
    for(let i=0;i<petals.length;i++){
      const p=petals[i];
      p.y-=p.vy*dt; p.ph+=p.sp*dt;
      p.x+=Math.sin(p.ph)*.55*dt+windx*dt; p.z+=Math.cos(p.ph*.7)*.45*dt+windz*dt;
      p.rx+=dt*1.6;p.rz+=dt*1.1;
      if(p.y<2)Object.assign(p,newPetal(false));
      _v.set(p.x,p.y,p.z);
      _q.setFromEuler(new THREE.Euler(p.rx,p.ph,p.rz));
      _s.set(.3,.06,.3);
      _m4.compose(_v,_q,_s);petalMesh.setMatrixAt(i,_m4);petalMesh.setColorAt(i,p.c);
    }
    petalMesh.visible=true;
  }else petalMesh.visible=false;
  petalMesh.instanceMatrix.needsUpdate=true;petalMesh.instanceColor.needsUpdate=true;
  // 萤火
  const fv=flags.fire?ss(.25,.8,night):0;
  fireMesh.visible=fv>0.01;
  if(fireMesh.visible){
    fireMesh.material.opacity=fv;
    for(let i=0;i<fires.length;i++){
      const f=fires[i];f.ph+=dt*f.s;
      _v.set(f.x+Math.cos(f.ph)*f.r, f.y+Math.sin(f.ph*1.7)*1.4, f.z+Math.sin(f.ph)*f.r);
      const sc=.34+.14*Math.sin(t*9+i);
      _s.set(sc,sc,sc);_q.identity();
      _m4.compose(_v,_q,_s);fireMesh.setMatrixAt(i,_m4);
      fireMesh.setColorAt(i,C(i%7===0?'#c8ff9e':'#ffe08a'));
    }
    fireMesh.instanceMatrix.needsUpdate=true;fireMesh.instanceColor.needsUpdate=true;
  }
  // 云
  const cl=window.__clouds;let ci=0;
  for(const c of clouds){
    for(const p of c.parts){
      p[0]+=c.vx*dt; if(p[0]>190)p[0]-=380;
      _v.set(p[0],p[1],p[2]);_q.identity();_s.set(p[3],p[4],p[5]);
      _m4.compose(_v,_q,_s);cloudMesh.setMatrixAt(ci++,_m4);
    }
  }
  cloudMesh.instanceMatrix.needsUpdate=true;
}
const _up=new THREE.Vector3(0,1,0);
let windx=1.4,windz=.6;

/* ═══════════════════════ 12 · 昼夜环境 ═══════════════════════ */
const ENV={
  day :{top:'#4a86c9',bot:'#cfe6f5',sun:'#fff2d6',sunI:2.5,hs:'#cfe6ff',hg:'#6b7a4e',hi:.95,
        fog:'#cfe3f2',fn:85,ff:380,glow:.62,pond:.66,fall:.58,refl:.16,reflG:.22,bloom:.22,
        ocean:'#2a86b8',star:0,sglow:1,water1:'#5cc0e0',water2:'#7fd4ee'},
  night:{top:'#05091a',bot:'#16233c',sun:'#9fc0ff',sunI:.42,hs:'#2b3f66',hg:'#17202e',hi:.38,
        fog:'#101a2c',fn:45,ff:300,glow:1.0,pond:.78,fall:.66,refl:.34,reflG:.62,bloom:.95,
        ocean:'#0d3555',star:1,sglow:.15,water1:'#2a6d99',water2:'#3f8fb8'}
};
const _ca=new THREE.Color(),_cb=new THREE.Color();
function mixc(a,b,t,out){out.copy(_ca.set(a)).lerp(_cb.set(b),t);return out}
function applyEnv(n){
  const e=ENV.day,f=ENV.night;
  mixc(e.top,f.top,n,skyU.topC.value);
  mixc(e.bot,f.bot,n,skyU.botC.value);
  mixc(e.sun,f.sun,n,skyU.sunC.value);
  skyU.glow.value=lerp(e.sglow,f.sglow,n);
  mixc(e.fog,f.fog,n,scene.fog.color);
  scene.fog.near=lerp(e.fn,f.fn,n); scene.fog.far=lerp(e.ff,f.ff,n);
  sun.intensity=lerp(e.sunI,f.sunI,n);
  mixc(e.sun,f.sun,n,sun.color);
  hemi.intensity=lerp(e.hi,f.hi,n);
  mixc(e.hs,f.hs,n,hemi.color); mixc(e.hg,f.hg,n,hemi.groundColor);
  amb.intensity=lerp(.14,.06,n);
  glowMat.color.setScalar(lerp(e.glow,f.glow,n));
  pondMat.opacity=lerp(e.pond,f.pond,n); fallMat.opacity=lerp(e.fall,f.fall,n);
  mixc(e.water1,f.water1,n,pondMat.color); mixc(e.water2,f.water2,n,fallMat.color);
  mixc(e.ocean,f.ocean,n,oceanMat.color);
  reflSolidMat.opacity=lerp(e.refl,f.refl,n); reflGlowMat.opacity=lerp(e.reflG,f.reflG,n);
  starMat.opacity=lerp(e.star,f.star,n)*.95;
  if(bloomPass)bloomPass.strength=lerp(e.bloom,f.bloom,n);
  sunDir.set(lerp(.42,-.5,n),lerp(.78,.55,n),lerp(.34,-.55,n)).normalize();
  sun.position.copy(sunDir).multiplyScalar(140);
  sunDisc.position.copy(sunDir).multiplyScalar(520);
  moonDisc.position.copy(sunDir).multiplyScalar(-520);
  sunDisc.material.opacity=clamp(1-n*1.6,0,1);
  moonDisc.material.opacity=clamp(n*1.6-.2,0,1);
  sunDisc.lookAt(0,0,0);moonDisc.lookAt(0,0,0);
  for(const h of haloList)h.material.opacity=clamp(n*1.1-.05,0,1);
  wU.uNight.value=n;
}
const sunDir=new THREE.Vector3();

/* ═══════════════════════ 13 · 相机控制 ═══════════════════════ */
const goal={th:-.78,ph:1.03,r:104,tx:-1,ty:19,tz:2};
const cur={...goal};
const VIEWS=[
 {th:-.78,ph:1.03,r:104,tx:-1,ty:19,tz:2},
 {th:.02, ph:1.16,r:66, tx:-4,ty:18,tz:2},
 {th:-.5, ph:.34,r:78, tx:-4,ty:26,tz:2},
 {th:.55, ph:1.2, r:44, tx:15,ty:7, tz:3},
];
let dragging=false,panning=false,lastX=0,lastY=0,idle=0,tween=null;
const ptrs=new Map();
canvas.addEventListener('pointerdown',e=>{
  ptrs.set(e.pointerId,{x:e.clientX,y:e.clientY});
  if(ptrs.size===1){dragging=true;panning=(e.button===2||e.shiftKey);lastX=e.clientX;lastY=e.clientY}
  idle=0;canvas.setPointerCapture(e.pointerId);
});
canvas.addEventListener('pointermove',e=>{
  if(!ptrs.has(e.pointerId))return;
  ptrs.set(e.pointerId,{x:e.clientX,y:e.clientY});
  if(ptrs.size>=2){
    const a=[...ptrs.values()];
    const d=Math.hypot(a[0].x-a[1].x,a[0].y-a[1].y);
    if(ptrs.__pd)goal.r=clamp(goal.r*(ptrs.__pd/d),16,230);
    ptrs.__pd=d;idle=0;return;
  }
  if(!dragging)return;
  const dx=e.clientX-lastX,dy=e.clientY-lastY;lastX=e.clientX;lastY=e.clientY;idle=0;
  if(panning){
    const k=cur.r*0.0016;
    const rt=new THREE.Vector3().setFromMatrixColumn(camera.matrix,0);
    const up=new THREE.Vector3().setFromMatrixColumn(camera.matrix,1);
    goal.tx+=(-rt.x*dx+up.x*dy)*k; goal.ty+=(-rt.y*dx+up.y*dy)*k;
    goal.tz+=(-rt.z*dx+up.z*dy)*k;
    goal.tx=clamp(goal.tx,-40,40);goal.tz=clamp(goal.tz,-40,40);goal.ty=clamp(goal.ty,2,60);
  }else{
    goal.th-=dx*0.0062; goal.ph=clamp(goal.ph-dy*0.005,.10,1.48);
  }
});
const endPtr=e=>{ptrs.delete(e.pointerId);if(ptrs.size<2)ptrs.__pd=null;if(!ptrs.size)dragging=panning=false};
canvas.addEventListener('pointerup',endPtr);canvas.addEventListener('pointercancel',endPtr);
canvas.addEventListener('contextmenu',e=>e.preventDefault());
canvas.addEventListener('wheel',e=>{e.preventDefault();idle=0;
  goal.r=clamp(goal.r*Math.exp(e.deltaY*0.0011),16,230)},{passive:false});
addEventListener('keydown',e=>{
  const k=e.key.toLowerCase();
  if(k==='l')setNight(!nightOn);
  if(k==='r'){setView(0)}
  if(k==='a'){flags.spin=!flags.spin;syncToggle('spin')}
});

/* ═══════════════════════ 14 · UI ═══════════════════════ */
const flags={spin:true,petal:true,fire:true,refl:true};
let nightOn=false,night=0;
const dn=document.getElementById('dn');
function setNight(v){
  nightOn=v;dn.dataset.night=v?'1':'0';
  document.getElementById('dnB').textContent=v?'夜':'白晝';
  document.getElementById('dnI').textContent=v?'Lantern Hour':'Daylight';
}
dn.addEventListener('click',()=>setNight(!nightOn));
document.getElementById('views').addEventListener('click',e=>{
  const b=e.target.closest('.b');if(!b)return;setView(+b.dataset.v);
});
function setView(i){const v=VIEWS[i];tween={t:0,from:{...goal},to:v};idle=0}
document.querySelectorAll('[data-t]').forEach(b=>{
  b.addEventListener('click',()=>{const k=b.dataset.t;flags[k]=!flags[k];syncToggle(k)});
});
function syncToggle(k){document.querySelector(`[data-t="${k}"]`).classList.toggle('on',flags[k])}

/* ═══════════════════════ 15 · 主循环 ═══════════════════════ */
let composer=null,bloomPass=null;
try{
  const [{EffectComposer},{RenderPass},{UnrealBloomPass},{OutputPass}]=await Promise.all([
    import('three/addons/postprocessing/EffectComposer.js'),
    import('three/addons/postprocessing/RenderPass.js'),
    import('three/addons/postprocessing/UnrealBloomPass.js'),
    import('three/addons/postprocessing/OutputPass.js')]);
  composer=new EffectComposer(renderer);
  composer.addPass(new RenderPass(scene,camera));
  bloomPass=new UnrealBloomPass(new THREE.Vector2(innerWidth,innerHeight),.4,.75,.84);
  composer.addPass(bloomPass);
  composer.addPass(new OutputPass());
  composer.setSize(innerWidth,innerHeight);
}catch(err){composer=null}

addEventListener('resize',()=>{
  camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();
  renderer.setSize(innerWidth,innerHeight);
  if(composer)composer.setSize(innerWidth,innerHeight);
});

let prev=performance.now(),fAcc=0,fCnt=0,fT=0;
function loop(now){
  const dt=Math.min(.05,(now-prev)/1000);prev=now;
  const t=now/1000;
  wU.uTime.value=t;
  night+= ((nightOn?1:0)-night)*(1-Math.exp(-dt*2.1));
  applyEnv(night);

  idle+=dt;
  if(flags.spin&&idle>3.5&&!tween)goal.th+=dt*0.055;
  if(tween){
    tween.t=Math.min(1,tween.t+dt/1.1);
    const e=tween.t<.5?4*tween.t**3:1-Math.pow(-2*tween.t+2,3)/2;
    for(const k of['th','ph','r','tx','ty','tz'])goal[k]=lerp(tween.from[k],tween.to[k],e);
    if(tween.t>=1)tween=null;
  }
  const k=1-Math.exp(-dt*9);
  for(const p of['th','ph','r','tx','ty','tz'])cur[p]+=(goal[p]-cur[p])*k;
  const sp=Math.sin(cur.ph),cp=Math.cos(cur.ph);
  camera.position.set(cur.tx+cur.r*sp*Math.sin(cur.th),cur.ty+cur.r*cp,cur.tz+cur.r*sp*Math.cos(cur.th));
  camera.lookAt(cur.tx,cur.ty,cur.tz);
  sky.position.copy(camera.position);stars.position.copy(camera.position);

  for(const L of lampLights)
    L.l.intensity=L.b*night*(.9+.08*Math.sin(t*7+L.ph)+.05*Math.sin(t*23+L.ph*3));

  windx=1.2+Math.sin(t*.23)*.9; windz=.5+Math.cos(t*.19)*.8;
  updLife(t,dt,night);
  mReflS.visible=mReflG.visible=flags.refl;

  if(composer)composer.render();else renderer.render(scene,camera);

  fAcc+=dt;fCnt++;
  if(fAcc>.5){
    document.getElementById('sFps').textContent=Math.round(fCnt/fAcc);
    document.getElementById('sDc').textContent=renderer.info.render.calls;
    fAcc=0;fCnt=0;
  }
  requestAnimationFrame(loop);
}

/* ═══════════════════════ 16 · 建造流程(带进度) ═══════════════════════ */
const lBar=document.getElementById('lBar'),lNum=document.getElementById('lNum'),lStage=document.getElementById('lStage');
const nextFrame=()=>new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)));
const STAGES=[
  ['测绘岛屿 · 山脊与池底',buildTerrain],
  ['堆砌台基 · 石阶 · 栏楯',buildTerrace],
  ['五重塔 · 飞檐 · 悬灯',buildPagoda],
  ['引水槽 · 瀑布 · 锦鲤池',buildWater],
  ['林木 · 落樱 · 竹径',buildFlora],
  ['虹桥 · 石灯笼',buildProps],
  ['剔除隐藏面 · 实例化合并',finalize],
  ['注入生命:锦鲤·花瓣·萤火',initLife],
];
(async function build(){
  for(let i=0;i<STAGES.length;i++){
    lStage.textContent=STAGES[i][0];
    const p=Math.round(i/STAGES.length*100);
    lBar.style.width=p+'%';lNum.textContent=p;
    await nextFrame();
    STAGES[i][1]();
  }
  lBar.style.width='100%';lNum.textContent='100';lStage.textContent='完成 · 拖曳以环视';
  document.getElementById('sVox').innerHTML=window.__vox.toLocaleString()+'<i>blocks</i>';
  setNight(false);applyEnv(0);
  await nextFrame();await nextFrame();
  document.getElementById('loader').classList.add('gone');
  prev=performance.now();
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>
13 Likes

macbook 可以跑吗

apple silicon可以,intel不行;

支持了mac,但是最少32g

1 Like

谢谢回复 等新的mac月底上市了 还要玩minimax 越狱版