DeepSeek-Flash, GLM-5.3-Flash, 小米 MiMo-V2.6-Flash & Space Bunny 对比

对比主流模型的物理世界理解

5 models · AI拯救打工人 · share snapshot
Share and embed

Post to social channels, or use Markdown and badges for GitHub/README.

XFacebook微博LinkedIn
[![DeepSeek-Flash, GLM-5.3-Flash, 小米 MiMo-V2.6-Flash & Space Bunny 对比 speed result on TOKRACE](/api/badge/share/URalDjtp2G?locale=en)](/en/r/URalDjtp2G)
请创建一个 可直接运行的单文件 HTML Web3D 作品,主题为 Space Bunny。 你需要创作一个梦幻、诗意、带童话感的 3D 场景: 一只可爱又有点酷的 太空兔子(Space Bunny),像小飞行员和太空旅行者的结合体,拥有柔软长耳朵、圆润身体、轻盈的弹跳动作,以及简洁的未来感装备。 场景设定在一个 梦境般的太空海岸: - 天空是深蓝到紫色的渐变 - 漂浮着具有体积感的 紫色积云 - 下方是有明显反光的海面 - 一架 白、绿、红涂装的未来战斗机 是场景的重要主体 Space Bunny 在场景中轻快地蹦跳,可以站在机翼、海边平台、或靠近镜头的前景区域。 战斗机、海面、云层、兔子、镜头运动要共同构成一个具有故事感的画面,而不是几个元素的简单堆叠。 请特别注意以下几点: 1. 角色设计 Space Bunny 要有鲜明辨识度,不只是普通兔子;要体现“太空兔”的设定与性格。 2. 动画表现 兔子需要有真实蹦跳、耳朵晃动、轻微身体弹性;战斗机可以有轻微悬停或掠海动作;整体场景要有持续生命力。 3. 材质与氛围 海面反光、云层层次、机体涂装、天空渐变、光影氛围都需要认真处理。 4. 镜头语言 请采用温柔、带动画电影感的运镜:缓慢推进、轻微环绕、低机位观察、适度景深感,让画面有“会呼吸”的节奏。 5. 文案融合 在画面中自然加入文字:“just breathe”。 它应该成为场景的一部分,例如发光的 3D 字、天空标语、漂浮装置、或海面倒影中的标题。 6. 工程要求 - 交付为一个单独 HTML 文件 - 打开即可运行 - 尽量不依赖复杂外部资源 - 保持较好的可读性和结构 请把它做成一个有氛围、有主角、有细节、有镜头感的 Web3D 小作品。 我希望看到的不是一个简单的技术 demo,而是一段让人一眼记住的 Space Bunny 场景。
Rerun this testFull rerun carries title, notes, Prompt and models; Prompt rerun only reuses the task text.
Visible
🏁 Live raceCurrent phase token/s · fastest firstReasoning / content · tokensTTFT wait · shorter is better

Amber: Thinking TPS. Green: Output TPS. Live estimates while running; final values match the card’s Output TPS. Speed is not answer quality.

0500 token/s
DeepSeek-Flash
⚡378 token/s
55,631 token
🚦0.46s
MiniMax-M3.1-Flash-Preview
180 token/s
28,526 token
TTFT 0.69s
小米 MiMo-V2.6 Flash
154 token/s
83,369 token
TTFT 3.14s
OpenRouter
151 token/s
61,194 token
TTFT 1.23s
智谱 GLM-5.3-flash
73 token/s
49,286 token
TTFT 0.77s
🏁 Verdict
🏆 Fastest
DeepSeek-Flash
378 tok/s
✓ Task quality
Not evaluated
DeepSeek-Flash
deepseek-flash
Done

Historical result · 9/29/2026, 04:01:50

用户想要一个单独的 HTML 文件,是个 Web3D 场景,用 Three.js(通过 CDN 引入),主题是太空兔子。让我仔细把它搭出来。 关键元素: - 天空渐变色(深蓝到紫色)——可以用自定义 shader 做一个巨大的球体,或者用渐变的背景。 - 体积感的紫色积云——用 sprite/billboard 加柔和的噪声纹理(程序化 canvas 纹理),分层摆放。 - 带反射的海面——用反射式平面。可选方案:用 three 的 Reflector(在 examples 里)——但从 CDN 加载 examples 要小心。可以自己用简单的 shader 做假的反射,或者用动态 cube camera。更简单:用一个带自定义 shader 的平面,用噪声和屏幕空间做反射……其实,最好还是从 CDN 引入 Reflector 吧?用 importmap 配合 three 的模块 CDN(unpkg/jsdelivr)。那就用 ES modules,配 importmap: ```html <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> ``` 然后引入 `import * as THREE from 'three'`,再从 `'three/addons/objects/Reflector.js'` 引入 `Reflector`。应该没问题,这仍然是“尽量少的外部依赖”。 不过要是 CDN 加载失败……也还好,这很常见。也许为了稳妥起见,还是自己写一套反射水面 shader?Reflector 依赖同一个 CDN,风险是同样存在的。 另一种更炫的做法:把海面做成自定义 shader 平面,用噪声波纹做假反射,同时引入一个用于反射的 cube camera……太复杂了。 还是用 Reflector 加波浪法线扰动来做吧?其实 Reflector 本身不支持波浪,但可以基于 time uniform 给它的纹理坐标加偏移——这需要改 shader。更简单的思路是:用 Reflector 得到镜面反射,再叠一层半透明的波浪平面放在上面,用加法混合描出波峰线。这样就够了。 其实,可能更简单更优雅的做法是:用自定义 ShaderMaterial 做海面,输入来自反射相机纹理。那就是重新实现 Reflector 了。嗯。 干脆就用 `three/addons/objects/Reflector.js`。它会给出一块镜子,然后用一个稍微透明的波浪平面叠在上面,带发光线条,再让反射在视觉上被搅散一些。可以。 嗯,Reflector 会有性能开销(每帧多渲染一遍)。应该没问题。 其实,让我再考虑一下:也许我该自己写水面 shader,用一套便宜的程序化“伪反射”来做——根据视线角度在反射里做出天空渐变和云层的颜色,再加上太阳高光,然后让“just breathe”这几个字的倒影通过另外的 sprite 镜像到下面来实现。嗯,Reflector 能给我那种真正的镜面感,包含战斗机、兔子,还有 3D 文字都能倒映出来。这个会很加分。就决定用 Reflector 了。 现在来规划一下场景部分: 1. 渲染器:通过 EffectComposer 加 ACESFilmic 色调映射 + 泛光?泛光会真正增加那种梦幻感。这需要再导入几个后处理模块。如果 importmap 没问题的话,其实还好。但风险也上来了。也许还是跳过泛光,用 sprite 和一个光晕来伪造发光吧。嗯,但泛光对“发光文字”和“发光线条”帮助很大。 还是用 UnrealBloomPass 吧——它让整个场景瞬间就有电影感了。从 addons 导入 EffectComposer、RenderPass、UnrealBloomPass、OutputPass。有点担心版本兼容性。用 three 0.160.0 吧,OutputPass 在这个版本里是有的。好。 其实让我再确认一下 0.160 里各个 addon 的路径: - `three/addons/postprocessing/EffectComposer.js` - `three/addons/postprocessing/RenderPass.js` - `three/addons/postprocessing/UnrealBloomPass.js` - `three/addons/postprocessing/OutputPass.js` - `three/addons/objects/Reflector.js` 都没问题。 注意:如果用 EffectComposer 并且用 OutputPass 处理色调映射,那渲染器的 toneMapping 要设成 ACESFilmic,outputColorSpace 用 sRGB。OutputPass 会负责处理色调映射和色彩空间转换。嗯,标准做法其实是:renderer.toneMapping = ACESFilmic,renderer.outputColorSpace 默认就是 sRGB;在 composer 里用 OutputPass 来做转换。这是常规做法。 嗯,有个小细节:先加 UnrealBloomPass 再加 OutputPass,应该是没问题的。 2. 天空: - 用一个巨大的球体,配 ShaderMaterial,加一个从深蓝 → 紫色 → 洋红的渐变,再加一条地平线光晕。要设成 BackSide 渲染。 - 也许再加几颗星星作为 Points 分布在四周。 - 一个柔和的月亮 / 行星圆盘。 3. 云: 用程序化生成柔和的径向渐变“云团”纹理,配上一堆 billboard 精灵,染成紫色/紫罗兰色,分布在不同高度、大小不一,缓慢漂移。每朵云本身也是由多个 sprite 聚成的。这样能做出体积感。要不要用加法混合?深色背景上的紫色云,用普通混合加透明度可能更好;或者用加法做发光效果也行。既然背景是深蓝色,那加色叠加的紫色云会自然地表现出“发光”的质感。我最后打算用普通混合、透明度适当的紫色纹理来做出那种厚实感……嗯,还是直接用加法叠加吧,更省事。 其实要做出那种厚重、有体积感的积云,我准备把每一朵云都做成一组 6 到 12 个 sprite,大小不同、微微错开,每个 sprite 都用一张柔和的团状纹理,并稍微带一点紫色的色调变化。 4. 战斗机:用基本几何体来搭——机身(拉伸出来的或类似胶囊体)、机头锥、后掠三角翼、垂直尾翼、座舱盖,再加上发动机辉光。涂装:白、绿、红三色条纹——这个灵感来自意大利三色旗,或者有点像二战意大利空军的涂装。用深绿、红色条纹横过机身。在机头/机尾加上类似圆形螺旋桨或发光环的细节。未来感处理:用带发光色的透明材质做座舱。 颜色方面,主体用白色/浅灰色平面着色,配绿色和红色条纹。 飞机的动作:沿着海面缓慢地悬停/滑翔,轻微俯仰、滚转,可能还会绕一个大圈飞。 5. 太空兔子: - 身体:用压扁的球体(大概 C 形)——我会用球体缩放来组合。 - 头部:球体。 - 耳朵:两个拉长的胶囊/锥体,再做一点轻微弯曲——可以用车削几何体或一系列缩放后的球体——最简单的方式:用 LatheGeometry 或者胶囊体再缩放。更简单的做法:用锥体做轻微弯曲?耳朵用 CapsuleGeometry 然后加一个弯曲变形?或者干脆用圆角长方体?这里我想用 LatheGeometry 沿着曲线做耳朵,或者直接用两个很薄的球体缩放成耳朵、微微旋转,再加上一点软软的晃动。 用 THREE.CapsuleGeometry(0.159 版本可用)——用半径和高度参数生成耳朵,压扁一点,再绕基准点加一点弯曲,就能得到很圆润的耳朵。 其实更好看一点:用自定义曲线做耳朵,用 TubeGeometry 或者 LatheGeometry。不过没关系,胶囊体压扁加旋转就足够可爱了。 - 眼睛:黑色小球,带高光点;可以加一个发光的护目镜(透明青色圆顶)——这样会有“太空兔”的感觉。不过为了可爱,眼睛还是保持可见,也许用青色/紫色的发光环代替护目镜。那就用一张半透明护目镜盖在额头位置,别遮住眼睛。 - 耳朵做成软管或者竖起来,带轻微摆动。 - 太空装备:一个小的喷气背包,带绿色/红色发光的推进器、一个胸前面板、一条围巾?围巾会飘动,很有诗意。再加一条小围巾吧。 - 一个透明的头盔?太空兔嘛——也许头盔是打开的,或者干脆没有。 - 小圆尾巴。 - 太空服:身体上包裹着颜色略有不同的盔甲板,白色/浅灰,带绿色和红色点缀,呼应飞机涂装。 动画:跳跃循环——y 轴用弹跳曲线,前倾,耳朵延迟摆动,挤压/拉伸。跳一下落到机翼上、平台,或者前景里。为了简单但好看,我打算让兔子做一个原地循环的跳跃,同时在垂直方向移动,偶尔在平台和机翼之间做一次大跳。简单地做个循环:每个周期:蹲下、起跳、带弧线飞行、落地、再蹲下。 简单点的话:y 取 sin 的正值,加上挤压和拉伸,在起跳/落地瞬间 x 轴带一点前倾。耳朵用带弹簧感的振荡来摆动。 6. 文字“just breathe”: - 用 TextGeometry?那要加载字体(JSON)——算是一个外部依赖。替代方案:用 canvas 把文字渲染成纹理,再贴到平面上。这样不错,而且是自包含的。用 canvas 画一个带辉光的字体,然后做成一个面向摄像机的平面,带透明度,采用加法混合。这样效果也好,还省掉了字体加载。 - 或者也可以做一个镜面反射效果到下面的水面里;但 Reflector 本来就能自动给出反射(如果文字平面在反射机位可见的话)。但要注意:Reflector 的反射机位会镜像场景,所以只要文字是场景物体,就能看到它的倒影。而 reflector 的裁剪——three 的 Reflector 默认的裁剪是 `new THREE.Plane(...)`,还带 `clipBias`,它是用裁剪平面把水面以下的东西剔除掉。严格说,在镜面平面上、靠近水面顶部的文字是会被正确反射出来的。 那把“just breathe”做成水面上方一点的一排发光 3D 文字平面?还是做成天空里的标语?我本来想把它放在前景附近的一个平面,带一点微弱的浮空感,再稍微朝向摄像机。嗯,但这样可能会显得很扁平、很违和。 更好的做法可能是:在天空中做一组发光的文字,位置在偏上方,用 canvas 纹理,让光晕柔和一点,再加一点缓慢的漂浮感,用 additive blending,还要做出景深感——它其实挺远的,所以反射里也能隐约看到,而且会被云层稍微挡住一点。 其实还有个更巧妙的方案:让文字像海边的霓虹灯牌一样,浮在水面上方,倒影就落在反射里。不过因为反射是完整镜像的,所以它也能反射出来。 嗯,我先想想,如果文字平面放在水面上方大概 3 个单位、稍微倾斜、离摄像机有点距离,那它就会出现在反射里。这个效果应该不错。 或者干脆用一整组 3D 文字——还是算了,canvas 纹理就挺好。 我打算把文字做成一个平面,大小约 12x4,放在 (0, 4.5, -12) 附近,稍微转一下让它朝向镜头,用加法混合再加一点轻微脉动。最好还带上柔和的边缘发光。 嗯,不过如果它在反射里,同时也在天空里,可能会显得重复。也行吧,要的就是那种诗意感。 其实,最稳妥又好看的做法是:把文字放在前景附近、低一点的位置,靠近水面但朝上倾斜,让镜头能把它连着倒影一起看到。不过算了,我还是把它浮在水面以上约 3 的位置;这样反射出来的文字看起来会像水里的倒影,视觉上很自然。 7. 相机: 用一条缓慢的近似轨迹动画:让相机轻轻绕场景运动,高度做得低一点(接近海面高度),慢慢推近,带着轻微浮动。要不要用类似样条曲线的路径?其实直接用正弦函数组合就行——摄像机位置用几个频率很慢的正弦参数,然后每帧看向一个会轻微漂移的目标点。持续但非常缓慢,永不循环重置,保持呼吸感和流动性。 加一点轻微的焦点呼吸感?那景深?散景(bokeh)成本太高了。跳过真正的景深吧,也许可以靠雾来给一点伪景深。 加上雾效(指数雾)来增强纵深感:颜色用天空的地平线色调。 8. 光照: - 用半球光做环境光,天空紫色、地面深蓝。 - 主平行光从后面/侧面来,稍微带点青色或暖调,用来在主对象上产生边缘光。 - 在兔子或飞机附近加一个偏点光源的辉光效果。 9. 平台:海面上有一个小平台/栈桥——就是一个从水面升起的圆柱,或者一块有纹理细节的浮板。兔子偶尔会落在上面。 现在来想一下整体构图: - 水面从 y=0 往下?还是让水面定在 y=0 作为基准面吧。 - 相机在 (0, 1.6, 9),看向 (0, 1.8, -4)。 - 飞机在大约 (-6, 3.5, -14) 的位置悬浮、缓慢巡航,稍微带一点倾斜。 - 兔子在平台或者前景区域的跳跃循环里,大概在 (0.5, 0, 2) 附近 → 但这样离相机会很近,看起来又大又靠前,效果不错。 - 其实“站在机翼上”这个设定更好。不过让兔子跳来跳去太复杂了,不如兔子就在前景的平台/岩石上跳,飞机在背景里飞——或者飞机也可以环绕飞行。 简化一下:兔子在一个小平台/岩石上跳,这个平台大约在 (1.2, 0, 1),离相机很近。飞机在海面上方、稍微靠后的位置,缓慢地巡航,带一点悬停感。 嗯,但“兔子站在机翼上”这个想法也很诱人。还是让兔子在前景平台吧,更简单——而且景深层次也出来了:兔子(前景)→ 飞机(中景)→ 云层(背景)→ 星星/月亮。 其实!可以结合一下:让飞机飞到一个极低点,然后兔子……不行。保留这个想法:兔子在漂浮的小岛/平台上跳,飞机在周围飞。没问题。 等等,但其实更酷的做法是:兔子的跳跃带点弧线,正好让飞机从下面飞过去。我会给它们做相位偏移,让画面看起来有互动感。 就这么定下来: - 平台:两个漂浮的岩石/岛屿,用 IcosahedronGeometry 加顶点位移来做,纯色带青色边缘光,稍微漂浮晃动。 - 兔子在平台之间跳。 嗯,判断时机太麻烦。还是让兔子一直在同一个地方跳吧。 现在开始写代码。 结构: ```html <!DOCTYPE html> <html lang="zh"> <head> <meta charset="utf-8"> <title>Space Bunny — just breathe</title> <style> ... </style> </head> <body> <div id="app"></div> <div class="vignette"></div> <div class="hud">...maybe a loading text / title</div> <script type="importmap">...</script> <script type="module"> // all code </script> </body> </html> ``` 现在写代码各个部分。 **常用工具** ```js // soft radial texture for clouds function makeSoftTexture(size=256, inner='rgba(255,255,255,1)', outer='rgba(255,255,255,0)') { const c = document.createElement('canvas'); c.width=c.height=size; const ctx = c.getContext('2d'); const g = ctx.createRadialGradient(size/2,size/2,0,size/2,size/2,size/2); g.addColorStop(0, inner); g.addColorStop(0.4, 'rgba(255,255,255,0.45)'); g.addColorStop(1, outer); ctx.fillStyle = g; ctx.fillRect(0,0,size,size); const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace; return t; } ``` 云层的话,我想要那种蓬松带点噪声的感觉,所以可以再多画几个错开的毛边渐变团。 让 canvas 里的云团随机叠几坨就行,不过每朵云的贴图最好统一一下,所以我就做一张尺寸大一点的 256 画布贴图,画三四团偏移的柔边圆,这样边缘就不会太规整。 **天空** ```js const skyGeo = new THREE.SphereGeometry(400, 64, 32); const skyMat = new THREE.ShaderMaterial({ side: THREE.BackSide, depthWrite: false, uniforms: { topColor: {value: new THREE.Color('#0a0a2e')}, midColor: {value: new THREE.Color('#3a1a6e')}, botColor: {value: new THREE.Color('#6b2a7a')}, // horizon purple-pink ... }, vertexShader: `varying vec3 vWorldPos; ... `, fragmentShader: `...` }); ``` 渐变用归一化高度来决定: ```glsl float h = normalize(vWorldPos).y; vec3 col = mix(botColor, midColor, smoothstep(-0.05, 0.35, h)); col = mix(col, topColor, smoothstep(0.25, 0.9, h)); ``` 再加一条地平线光晕带:`col += glowColor * exp(-abs(h)*10.0) * 0.4;` 也可以再加些细微的星云噪点?可以用正弦波拼点简单的哈希噪声,成本低也安全。 **星星**:用 `Points` 生成位置,分布在远处半径的大球面上,只保留 `y > -0.1` 的区域。用尺寸随机的小发光纹理并做透明度衰减。 **月亮**:在远处放一个大球体,用自发光平面着色,再加一圈淡淡的辉光 sprite。 其实月亮和星星可能有点杂,但“梦境太空海岸”这个主题会需要它们。保留一个柔和的大星球/月亮。 **云层** 写一个创建云团

这是可直接运行的单文件 HTML Web3D 作品,呈现 Space Bunny 在梦幻太空海岸蹦跳、战斗机掠海悬停的温暖场景,打开即可沉浸欣赏。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Space Bunny · just breathe</title>
<style>
  html, body {
    margin: 0; padding: 0; width: 100%; height: 100%;
    overflow: hidden; background: #05041a;
  }
  canvas { display: block; }
  /* 轻微暗角,让画面更有电影感 */
  #vignette {
    position: fixed; inset: 0; pointer-events: none; z-index: 10;
    background: radial-gradient(ellipse at 50% 45%,
      rgba(0,0,0,0) 40%,
      rgba(8,4,26,0.35) 78%,
      rgba(4,2,16,0.72) 100%);
  }
  #loading {
    position: fixed; inset: 0; z-index: 20; display: flex;
    align-items: center; justify-content: center;
    background: #05041a; color: rgba(190,210,255,0.65);
    font-family: "Helvetica Neue", Arial, "PingFang SC", sans-serif;
    font-size: 12px; letter-spacing: 0.5em; text-transform: uppercase;
    transition: opacity 1.2s ease;
  }
  #loading.hide { opacity: 0; pointer-events: none; }
</style>
</head>
<body>
<div id="vignette"></div>
<div id="loading">space bunny</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 { Reflector }    from 'three/addons/objects/Reflector.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass }   from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { OutputPass }   from 'three/addons/postprocessing/OutputPass.js';

/* =========================================================
   0. 基础环境
   ========================================================= */
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x1b1238, 0.0055);

const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 3000);
camera.position.set(0.8, 2.2, 9.2);

const clock = new THREE.Clock();
let time = 0;

/* =========================================================
   1. 程序化贴图工具
   ========================================================= */
function makeGlowTexture(size = 128) {
  const c = document.createElement('canvas');
  c.width = c.height = size;
  const ctx = c.getContext('2d');
  const g = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
  g.addColorStop(0.0, 'rgba(255,255,255,1)');
  g.addColorStop(0.35, 'rgba(190,215,255,0.42)');
  g.addColorStop(1.0, 'rgba(120,150,255,0)');
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, size, size);
  const t = new THREE.CanvasTexture(c);
  t.colorSpace = THREE.SRGBColorSpace;
  return t;
}

function makeCloudTexture() {
  const size = 256;
  const c = document.createElement('canvas');
  c.width = c.height = size;
  const ctx = c.getContext('2d');
  for (let i = 0; i < 8; i++) {
    const x = size * 0.5 + (Math.random() - 0.5) * size * 0.52;
    const y = size * 0.5 + (Math.random() - 0.5) * size * 0.34;
    const r = size * (0.15 + Math.random() * 0.17);
    const g = ctx.createRadialGradient(x, y, 0, x, y, r);
    g.addColorStop(0.0, 'rgba(255,255,255,0.70)');
    g.addColorStop(0.5, 'rgba(255,255,255,0.26)');
    g.addColorStop(1.0, 'rgba(255,255,255,0)');
    ctx.fillStyle = g;
    ctx.beginPath();
    ctx.arc(x, y, r, 0, Math.PI * 2);
    ctx.fill();
  }
  const t = new THREE.CanvasTexture(c);
  t.colorSpace = THREE.SRGBColorSpace;
  return t;
}

function makeTextTexture(text) {
  const W = 1024, H = 256;
  const c = document.createElement('canvas');
  c.width = W; c.height = H;
  const ctx = c.getContext('2d');
  ctx.clearRect(0, 0, W, H);
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.font = '300 108px "Helvetica Neue", Helvetica, Arial, sans-serif';

  // 多层辉光
  ctx.shadowColor = 'rgba(140,205,255,0.95)';
  ctx.shadowBlur = 60;
  ctx.fillStyle = 'rgba(200,230,255,0.55)';
  ctx.fillText(text, W / 2, H / 2);
  ctx.fillText(text, W / 2, H / 2);

  ctx.shadowBlur = 26;
  ctx.fillStyle = 'rgba(235,245,255,0.9)';
  ctx.fillText(text, W / 2, H / 2);

  // 实心白
  ctx.shadowBlur = 0;
  ctx.fillStyle = '#ffffff';
  ctx.fillText(text, W / 2, H / 2);

  const t = new THREE.CanvasTexture(c);
  t.colorSpace = THREE.SRGBColorSpace;
  t.anisotropy = 4;
  return t;
}

const glowTex  = makeGlowTexture();
const cloudTex = makeCloudTexture();

/* =========================================================
   2. 天空(深蓝 → 紫 渐变球)
   ========================================================= */
const skyMaterial = new THREE.ShaderMaterial({
  side: THREE.BackSide,
  depthWrite: false,
  fog: false,
  uniforms: {
    uTop:  { value: new THREE.Color('#080a2e') },
    uMid:  { value: new THREE.Color('#2d1f6e') },
    uBot:  { value: new THREE.Color('#6b2f7d') },
    uGlow: { value: new THREE.Color('#a855c8') }
  },
  vertexShader: /* glsl */`
    varying vec3 vDir;
    void main() {
      vDir = position;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: /* glsl */`
    uniform vec3 uTop;
    uniform vec3 uMid;
    uniform vec3 uBot;
    uniform vec3 uGlow;
    varying vec3 vDir;
    void main() {
      vec3 d = normalize(vDir);
      float h = d.y;

      vec3 col = mix(uBot, uMid, smoothstep(-0.28, 0.26, h));
      col = mix(col, uTop, smoothstep(0.20, 0.88, h));

      // 地平线紫粉色光晕
      col += uGlow * exp(-abs(h) * 6.5) * 0.42;
      // 贴近地平线的冷青色薄雾
      col += vec3(0.10, 0.22, 0.46) * exp(-abs(h) * 20.0) * 0.85;

      // 极轻微抖动,消除色带
      float n = fract(sin(dot(d.xy * 91.7, vec2(12.9898, 78.233))) * 43758.5453);
      col += (n - 0.5) * 0.014;

      gl_FragColor = vec4(col, 1.0);
    }
  `
});

const sky = new THREE.Mesh(new THREE.SphereGeometry(800, 48, 32), skyMaterial);
scene.add(sky);

/* =========================================================
   3. 星星
   ========================================================= */
(function buildStars() {
  const COUNT = 1100;
  const positions = new Float32Array(COUNT * 3);
  for (let i = 0; i < COUNT; i++) {
    const dir = new THREE.Vector3(
      Math.random() * 2 - 1,
      Math.random() * 1.05 - 0.02,
      Math.random() * 2 - 1
    ).normalize();
    const r = 420 + Math.random() * 280;
    positions[i * 3 + 0] = dir.x * r;
    positions[i * 3 + 1] = dir.y * r;
    positions[i * 3 + 2] = dir.z * r;
  }
  const geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));

  const mat = new THREE.PointsMaterial({
    size: 2.4,
    map: glowTex,
    transparent: true,
    depthWrite: false,
    blending: THREE.AdditiveBlending,
    sizeAttenuation: false,
    fog: false,
    color: 0xcfe0ff,
    opacity: 0.85
  });
  scene.add(new THREE.Points(geo, mat));
})();

/* =========================================================
   4. 远处的星球 / 月亮
   ========================================================= */
(function buildMoon() {
  const moonMat = new THREE.MeshBasicMaterial({ color: 0xd9c9ff, fog: false });
  const moon = new THREE.Mesh(new THREE.SphereGeometry(16, 32, 24), moonMat);
  moon.position.set(-118, 74, -300);
  scene.add(moon);

  const halo = new THREE.Sprite(new THREE.SpriteMaterial({
    map: glowTex,
    color: 0x9c7bff,
    transparent: true,
    opacity: 0.55,
    blending: THREE.AdditiveBlending,
    depthWrite: false,
    fog: false
  }));
  halo.position.copy(moon.position);
  halo.scale.set(160, 160, 1);
  scene.add(halo);
})();

/* =========================================================
   5. 紫色积云
   ========================================================= */
const clouds = [];
(function buildClouds() {
  const palette = [
    { color: '#7a56d6', opacity: 0.42 },
    { color: '#5b3cae', opacity: 0.50 },
    { color: '#a98cff', opacity: 0.34 },
    { color: '#4a2f96', opacity: 0.55 },
    { color: '#c48ae0', opacity: 0.30 }
  ];

  function createCloud(cfg) {
    const group = new THREE.Group();
    const puffs = cfg.puffs;
    for (let i = 0; i < puffs; i++) {
      const mat = new THREE.SpriteMaterial({
        map: cloudTex,
        color: new THREE.Color(cfg.color),
        transparent: true,
        opacity: cfg.opacity * (0.55 + Math.random() * 0.7),
        depthWrite: false,
        fog: true
      });
      const sprite = new THREE.Sprite(mat);
      const a = Math.random() * Math.PI * 2;
      const rad = Math.random() * cfg.spread;
      sprite.position.set(
        Math.cos(a) * rad,
        (Math.random() - 0.28) * cfg.spread * 0.42,
        Math.sin(a) * rad * 0.72
      );
      const s = cfg.size * (0.55 + Math.random() * 0.75);
      sprite.scale.set(s, s * 0.62, 1);
      group.add(sprite);
    }
    group.position.copy(cfg.pos);
    group.userData.speed = cfg.speed;
    return group;
  }

  for (let i = 0; i < 18; i++) {
    const pal = palette[(Math.random() * palette.length) | 0];
    const angle = Math.random() * Math.PI * 2;
    const radius = 26 + Math.random() * 95;
    const isLow = Math.random() < 0.4;

    const cloud = createCloud({
      color: pal.color,
      opacity: pal.opacity,
      puffs: 5 + ((Math.random() * 5) | 0),
      spread: 1.0,
      size: 1.0,
      speed: 0.35 + Math.random() * 0.65,
      pos: new THREE.Vector3(
        Math.cos(angle) * radius,
        isLow ? 4 + Math.random() * 6 : 12 + Math.random() * 30,
        Math.sin(angle) * radius - 18
      )
    });

    const overall = 3.6 + Math.random() * 7.5;
    cloud.scale.setScalar(overall);
    clouds.push(cloud);
    scene.add(cloud);
  }
})();

/* =========================================================
   6. 反光海面(Reflector + 程序化波光层)
   ========================================================= */
const reflector = new Reflector(new THREE.PlaneGeometry(700, 700), {
  clipBias: 0.004,
  textureWidth: 1024,
  textureHeight: 1024,
  color: 0x8fa2cc
});
reflector.rotation.x = -Math.PI / 2;
reflector.position.y = 0;
scene.add(reflector);

// 波光层
const waveUniforms = {
  uTime: { value: 0 },
  uColor: { value: new THREE.Color('#9dc8ff') }
};
const waveMaterial = new THREE.ShaderMaterial({
  transparent: true,
  blending: THREE.AdditiveBlending,
  depthWrite: false,
  uniforms: waveUniforms,
  vertexShader: /* glsl */`
    uniform float uTime;
    varying vec2 vP;
    void main() {
      vec4 wp = modelMatrix * vec4(position, 1.0);
      float w = sin(wp.x * 0.22 + uTime * 0.75) * 0.5
              + cos(wp.z * 0.27 - uTime * 0.55) * 0.5;
      wp.y += w * 0.13;
      vP = wp.xz;
      gl_Position = projectionMatrix * viewMatrix * wp;
    }
  `,
  fragmentShader: /* glsl */`
    uniform float uTime;
    uniform vec3  uColor;
    varying vec2 vP;
    void main() {
      float a = sin(vP.x * 0.34 + uTime * 0.90);
      float b = sin(vP.y * 0.27 - uTime * 0.68);
      float c = sin((vP.x + vP.y) * 0.12 + uTime * 0.42);
      float m = a * b + c * 0.5;

      float crest = smoothstep(0.42, 1.10, m);
      float d = length(vP - cameraPosition.xz);
      float fade = exp(-d * 0.018) * 0.85 + 0.12;

      vec3 col = uColor * crest * fade;
      gl_FragColor = vec4(col, 1.0);
    }
  `
});
const waveMesh = new THREE.Mesh(new THREE.PlaneGeometry(700, 700, 96, 96), waveMaterial);
waveMesh.rotation.x = -Math.PI / 2;
waveMesh.position.y = 0.03;
scene.add(waveMesh);

/* =========================================================
   7. 灯光
   ========================================================= */
scene.add(new THREE.HemisphereLight(0x6f82ff, 0x2a1044, 1.15));

const keyLight = new THREE.DirectionalLight(0xd9c6ff, 2.1);
keyLight.position.set(-7, 11, 9);
scene.add(keyLight);

const rimLight = new THREE.DirectionalLight(0x5fe6ff, 1.9);
rimLight.position.set(9, 5, -14);
scene.add(rimLight);

const pinkLight = new THREE.DirectionalLight(0xff8fc4, 0.65);
pinkLight.position.set(5, -4, -8);
scene.add(pinkLight);

/* =========================================================
   8. Space Bunny
   ========================================================= */
function createBunny() {
  const bunny = new THREE.Group();  // 原点位于脚底

  const furMat = new THREE.MeshStandardMaterial({ color: 0xf7f1ea, roughness: 0.92, metalness: 0.0 });
  const innerEarMat = new THREE.MeshStandardMaterial({
    color: 0xefa8cd, roughness: 0.85,
    emissive: 0x5c2340, emissiveIntensity: 0.35
  });
  const suitMat = new THREE.MeshStandardMaterial({ color: 0xdde6f5, roughness: 0.34, metalness: 0.38 });
  const darkMat = new THREE.MeshStandardMaterial({ color: 0x1b2036, roughness: 0.38, metalness: 0.55 });
  const visorMat = new THREE.MeshStandardMaterial({
    color: 0x2b3d63, roughness: 0.18, metalness: 0.72,
    emissive: 0x1b6f8a, emissiveIntensity: 0.55
  });
  const greenMat = new THREE.MeshStandardMaterial({
    color: 0x2ec27e, roughness: 0.35, metalness: 0.35,
    emissive: 0x0d5c38, emissiveIntensity: 0.6
  });
  const redMat = new THREE.MeshStandardMaterial({
    color: 0xe8484c, roughness: 0.35, metalness: 0.35,
    emissive: 0x6b1416, emissiveIntensity: 0.6
  });
  const glowCyan = new THREE.MeshBasicMaterial({ color: 0x9ff8ff });
  const glowPink = new THREE.MeshBasicMaterial({ color: 0xff6ad5 });

  /* --- 身体 --- */
  const body = new THREE.Mesh(new THREE.SphereGeometry(0.5, 32, 24), furMat);
  body.scale.set(0.95, 0.92, 0.86);
  body.position.y = 0.60;
  bunny.add(body);

  /* --- 头 --- */
  const head = new THREE.Mesh(new THREE.SphereGeometry(0.37, 32, 24), furMat);
  head.position.set(0, 1.30, 0.05);
  head.scale.set(1, 0.96, 0.93);
  bunny.add(head);

  /* --- 耳朵(带枢轴,方便晃动) --- */
  const earPivots = [];
  for (const side of [-1, 1]) {
    const pivot = new THREE.Group();
    pivot.position.set(side * 0.145, 1.56, 0.02);
    pivot.rotation.z = side * 0.20;
    pivot.rotation.x = -0.10;

    const outer = new THREE.Mesh(new THREE.CapsuleGeometry(0.078, 0.46, 8, 16), furMat);
    outer.position.y = 0.32;
    outer.scale.set(1, 1, 0.55);
    pivot.add(outer);

    const inner = new THREE.Mesh(new THREE.CapsuleGeometry(0.046, 0.36, 8, 14), innerEarMat);
    inner.position.set(0, 0.32, 0.028);
    inner.scale.set(1, 1, 0.5);
    pivot.add(inner);

    bunny.add(pivot);
    earPivots.push(pivot);
  }

  /* --- 眼睛 --- */
  for (const side of [-1, 1]) {
    const eye = new THREE.Mesh(
      new THREE.SphereGeometry(0.074, 18, 14),
      new THREE.MeshStandardMaterial({ color: 0x14101f, roughness: 0.18, metalness: 0.2 })
    );
    eye.position.set(side * 0.155, 1.335, 0.315);
    bunny.add(eye);

    const spark = new THREE.Mesh(new THREE.SphereGeometry(0.024, 8, 8), new THREE.MeshBasicMaterial({ color: 0xffffff }));
    spark.position.set(side * 0.155 + 0.028, 1.362, 0.368);
    bunny.add(spark);
  }

  /* --- 鼻子 --- */
  const nose = new THREE.Mesh(
    new THREE.SphereGeometry(0.032, 10, 8),
    new THREE.MeshStandardMaterial({ color: 0xf5a8c4, roughness: 0.6, emissive: 0x5c2333, emissiveIntensity: 0.4 })
  );
  nose.position.set(0, 1.235, 0.355);
  bunny.add(nose);

  /* --- 酷酷的护目镜(推到额头上) --- */
  const visor = new THREE.Mesh(new THREE.CapsuleGeometry(0.062, 0.30, 8, 16), visorMat);
  visor.rotation.z = Math.PI / 2;
  visor.position.set(0, 1.505, 0.245);
  visor.scale.set(1, 1, 0.75);
  bunny.add(visor);

  const visorGlow = new THREE.Mesh(
    new THREE.TorusGeometry(0.062, 0.008, 6, 18),
    glowCyan
  );
  visorGlow.rotation.y = Math.PI / 2;
  visorGlow.position.set(0.155, 1.505, 0.245);
  bunny.add(visorGlow);

  /* --- 天线 --- */
  const antenna = new THREE.Mesh(new THREE.CylinderGeometry(0.011, 0.011, 0.32, 6), darkMat);
  antenna.position.set(0.16, 1.74, -0.06);
  antenna.rotation.z = -0.28;
  bunny.add(antenna);

  const antennaTip = new THREE.Mesh(new THREE.SphereGeometry(0.045, 12, 10), glowPink);
  antennaTip.position.set(0.215, 1.90, -0.06);
  bunny.add(antennaTip);

  /* --- 喷气背包 --- */
  const pack = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.44, 0.20), suitMat);
  pack.position.set(0, 0.80, -0.40);
  bunny.add(pack);

  const packGlows = [];
  for (const side of [-1, 1]) {
    const nozzle = new THREE.Mesh(new THREE.CylinderGeometry(0.058, 0.075, 0.20, 12), darkMat);
    nozzle.rotation.x = Math.PI / 2;
    nozzle.position.set(side * 0.14, 0.60, -0.47);
    bunny.add(nozzle);

    const jet = new THREE.Sprite(new THREE.SpriteMaterial({
      map: glowTex,
      color: 0x7fe9ff,
      transparent: true,
      opacity: 0.85,
      blending: THREE.AdditiveBlending,
      depthWrite: false,
      fog: false
    }));
    jet.position.set(side * 0.14, 0.60, -0.62);
    jet.scale.set(0.42, 0.42, 1);
    bunny.add(jet);
    packGlows.push(jet);
  }

  /* --- 胸前控制面板 --- */
  const panel = new THREE.Mesh(new THREE.BoxGeometry(0.30, 0.22, 0.07), darkMat);
  panel.position.set(0, 0.72, 0.37);
  bunny.add(panel);

  const ledG = new THREE.Mesh(new THREE.SphereGeometry(0.028, 8, 8), glowCyan);
  ledG.position.set(-0.075, 0.745, 0.412);
  bunny.add(ledG);

  const ledR = new THREE.Mesh(new THREE.SphereGeometry(0.028, 8, 8), glowPink);
  ledR.position.set(0.075, 0.745, 0.412);
  bunny.add(ledR);

  /* --- 围巾 --- */
  const scarfRing = new THREE.Mesh(
    new THREE.TorusGeometry(0.255, 0.058, 8, 24),
    new THREE.MeshStandardMaterial({ color: 0xe0433f, roughness: 0.85, emissive: 0x4a1010, emissiveIntensity: 0.4 })
  );
  scarfRing.rotation.x = Math.PI / 2;
  scarfRing.position.set(0, 1.02, 0.02);
  bunny.add(scarfRing);

  const scarfGeo = new THREE.PlaneGeometry(0.15, 0.9, 2, 10);
  scarfGeo.translate(0, -0.45, 0);
  const scarfTail = new THREE.Mesh(scarfGeo, new THREE.MeshStandardMaterial({
    color: 0xe0433f, roughness: 0.85, side: THREE.DoubleSide,
    emissive: 0x4a1010, emissiveIntensity: 0.35
  }));
  scarfTail.position.set(-0.20, 1.05, -0.22);
  scarfTail.rotation.set(0.25, 0.3, 0.15);
  bunny.add(scarfTail);

  const scarfBase = Float32Array.from(scarfGeo.attributes.position.array);

  /* --- 爪子 --- */
  for (const side of [-1, 1]) {
    const paw = new THREE.Mesh(new THREE.SphereGeometry(0.115, 16, 12), furMat);
    paw.scale.set(0.82, 0.9, 1.15);
    paw.position.set(side * 0.33, 0.50, 0.20);
    bunny.add(paw);
  }

  /* --- 脚 --- */
  for (const side of [-1, 1]) {
    const foot = new THREE.Mesh(new THREE.SphereGeometry(0.17, 16, 12), furMat);
    foot.scale.set(0.85, 0.58, 1.5);
    foot.position.set(side * 0.24, 0.095, 0.14);
    bunny.add(foot);
  }

  /* --- 尾巴 --- */
  const tail = new THREE.Mesh(new THREE.SphereGeometry(0.15, 16, 12),
    new THREE.MeshStandardMaterial({ color: 0xfffaf4, roughness: 0.95 }));
  tail.position.set(0, 0.50, -0.42);
  bunny.add(tail);

  return { group: bunny, earPivots, scarfTail, scarfBase, packGlows };
}

const bunnyRig = createBunny();
const bunny = bunnyRig.group;

/* 浮空平台 */
const platformGroup = new THREE.Group();
platformGroup.position.set(0.9, 0, 1.25);
scene.add(platformGroup);

const platformMat = new THREE.MeshStandardMaterial({
  color: 0x2b2340, roughness: 0.9, metalness: 0.15
});
const platform = new THREE.Mesh(new THREE.CylinderGeometry(1.45, 1.05, 0.62, 28, 1), platformMat);
platform.position.y = -0.02;
platformGroup.add(platform);

const platformRim = new THREE.Mesh(
  new THREE.TorusGeometry(1.44, 0.026, 6, 48),
  new THREE.MeshBasicMaterial({ color: 0x63e8ff, transparent: true, opacity: 0.85 })
);
platformRim.rotation.x = Math.PI / 2;
platformRim.position.y = 0.29;
platformGroup.add(platformRim);

const platformGlow = new THREE.PointLight(0x63e8ff, 8, 6, 2);
platformGlow.position.set(0, 0.5, 0);
platformGroup.add(platformGlow);

// 兔子站在平台顶面
bunny.scale.setScalar(1.15);
bunny.position.set(0, 0.30, 0);
bunny.rotation.y = -0.35;
platformGroup.add(bunny);

/* =========================================================
   9. 未来战斗机(白 / 绿 / 红 涂装)
   ========================================================= */
function createJet() {
  const jet = new THREE.Group();       // 用于位置与朝向
  const model = new THREE.Group();     // 模型自身机头朝 -Z
  model.rotation.y = Math.PI;          // 让机头在父空间中指向 +Z(配合 lookAt)
  jet.add(model);

  const whiteMat = new THREE.MeshStandardMaterial({ color: 0xeef3f9, roughness: 0.30, metalness: 0.48 });
  const greenMat = new THREE.MeshStandardMaterial({
    color: 0x1fa860, roughness: 0.34, metalness: 0.40,
    emissive: 0x0a3a22, emissiveIntensity: 0.45
  });
  const redMat = new THREE.MeshStandardMaterial({
    color: 0xd93a3e, roughness: 0.34, metalness: 0.40,
    emissive: 0x5a1416, emissiveIntensity: 0.45
  });
  const darkMat = new THREE.MeshStandardMaterial({ color: 0x2b3145, roughness: 0.42, metalness: 0.72 });
  const glassMat = new THREE.MeshStandardMaterial({
    color: 0x8fe9ff, transparent: true, opacity: 0.42,
    roughness: 0.06, metalness: 0.25,
    emissive: 0x0e3d52, emissiveIntensity: 0.9,
    side: THREE.DoubleSide
  });

  /* 机身 */
  const fusGeo = new THREE.CapsuleGeometry(0.42, 2.0, 12, 24);
  fusGeo.rotateX(Math.PI / 2);
  model.add(new THREE.Mesh(fusGeo, whiteMat));

  /* 机头锥 */
  const noseGeo = new THREE.ConeGeometry(0.42, 1.1, 24, 1, true);
  noseGeo.rotateX(-Math.PI / 2);
  const nose = new THREE.Mesh(noseGeo, whiteMat);
  nose.position.z = -1.85;
  model.add(nose);

  /* 座舱盖 */
  const canGeo = new THREE.SphereGeometry(0.30, 24, 16, 0, Math.PI * 2, 0, Math.PI * 0.5);
  canGeo.scale(1.0, 0.85, 2.1);
  const canopy = new THREE.Mesh(canGeo, glassMat);
  canopy.position.set(0, 0.30, -0.55);
  model.add(canopy);

  /* 主翼 */
  const wingShape = new THREE.Shape();
  wingShape.moveTo(0.18, -0.85);
  wingShape.lineTo(2.35, 0.05);
  wingShape.lineTo(2.35, 0.45);
  wingShape.lineTo(0.18, 1.25);
  wingShape.closePath();

  const wingGeo = new THREE.ExtrudeGeometry(wingShape, {
    depth: 0.10, bevelEnabled: true,
    bevelSize: 0.025, bevelThickness: 0.025, bevelSegments: 2
  });
  wingGeo.rotateX(Math.PI / 2);
  wingGeo.translate(0, 0.05, 0);

  const wingR = new THREE.Mesh(wingGeo, whiteMat);
  model.add(wingR);

  const wingL = new THREE.Mesh(wingGeo, whiteMat);
  wingL.scale.x = -1;
  model.add(wingL);

  /* 翼尖航行灯 */
  const tipR = new THREE.Mesh(new THREE.SphereGeometry(0.062, 12, 10),
    new THREE.MeshBasicMaterial({ color: 0x45ffa2 }));
  tipR.position.set(2.30, 0.06, 0.24);
  model.add(tipR);

  const tipL = new THREE.Mesh(new THREE.SphereGeometry(0.062, 12, 10),
    new THREE.MeshBasicMaterial({ color: 0xff4d55 }));
  tipL.position.set(-2.30, 0.06, 0.24);
  model.add(tipL);

  /* 垂尾 */
  const finShape = new THREE.Shape();
  finShape.moveTo(-0.45, 0);
  finShape.lineTo(0.50, 0);
  finShape.lineTo(0.42, 0.95);
  finShape.lineTo(-0.05, 0.95);
  finShape.closePath();

  const finGeo = new THREE.ExtrudeGeometry(finShape, { depth: 0.08, bevelEnabled: false });
  finGeo.rotateY(-Math.PI / 2);
  finGeo.translate(0.04, 0, 0);

  const fin = new THREE.Mesh(finGeo, whiteMat);
  fin.position.set(0, 0.26, 0.85);
  model.add(fin);

  /* 水平尾翼 */
  const stabShape = new THREE.Shape();
  stabShape.moveTo(0.12, -0.36);
  stabShape.lineTo(1.05, 0.02);
  stabShape.lineTo(1.05, 0.24);
  stabShape.lineTo(0.12, 0.56);
  stabShape.closePath();

  const stabGeo = new THREE.ExtrudeGeometry(stabShape, { depth: 0.07, bevelEnabled: false });
  stabGeo.rotateX(Math.PI / 2);

  const stabR = new THREE.Mesh(stabGeo, whiteMat);
  stabR.position.set(0, 0.02, 1.05);
  model.add(stabR);

  const stabL = new THREE.Mesh(stabGeo, whiteMat);
  stabL.scale.x = -1;
  stabL.position.set(0, 0.02, 1.05);
  model.add(stabL);

  /* 涂装环带 */
  const bandGreen = new THREE.Mesh(
    new THREE.CylinderGeometry(0.435, 0.435, 0.30, 28, 1, true), greenMat);
  bandGreen.rotation.x = Math.PI / 2;
  bandGreen.position.z = -0.72;
  model.add(bandGreen);

  const bandRed = new THREE.Mesh(
    new THREE.CylinderGeometry(0.435, 0.435, 0.30, 28, 1, true), redMat);
  bandRed.rotation.x = Math.PI / 2;
  bandRed.position.z = -0.30;
  model.add(bandRed);

  /* 尾喷口 */
  const nozzle = new THREE.Mesh(
    new THREE.CylinderGeometry(0.30, 0.36, 0.55, 24, 1, true), darkMat);
  nozzle.rotation.x = Math.PI / 2;
  nozzle.position.z = 1.55;
  model.add(nozzle);

  /* 尾焰 */
  const flameMat = new THREE.MeshBasicMaterial({
    color: 0x8fe8ff, transparent: true, opacity: 0.7,
    blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide
  });
  const flameGeo = new THREE.ConeGeometry(0.26, 1.2, 20, 1, true);
  flameGeo.rotateX(Math.PI / 2);
  flameGeo.translate(0, 0, 0.6);
  const flame = new THREE.Mesh(flameGeo, flameMat);
  flame.position.z = 1.85;
  model.add(flame);

  const engineLight = new THREE.PointLight(0x66e6ff, 26, 16, 2);
  engineLight.position.set(0, 0, 2.2);
  model.add(engineLight);

  /* 机头灯 */
  const noseLight = new THREE.Mesh(new THREE.SphereGeometry(0.05, 10, 8),
    new THREE.MeshBasicMaterial({ color: 0xfff3c4 }));
  noseLight.position.set(0, 0, -2.36);
  model.add(noseLight);

  return { group: jet, flame, model };
}

const jetRig = createJet();
const jet = jetRig.group;
scene.add(jet);

/* =========================================================
   10. “just breathe” 发光文字
   ========================================================= */
const textGroup = new THREE.Group();
textGroup.position.set(0, 4.7, -11.5);
scene.add(textGroup);

const textTex = makeTextTexture('just breathe');
const textMesh = new THREE.Mesh(
  new THREE.PlaneGeometry(8.4, 2.1),
  new THREE.MeshBasicMaterial({
    map: textTex,
    transparent: true,
    blending: THREE.AdditiveBlending,
    depthWrite: false,
    depthTest: true,
    fog: false,
    opacity: 0.95,
    toneMapped: false
  })
);
textGroup.add(textMesh);

const textHalo = new THREE.Sprite(new THREE.SpriteMaterial({
  map: glowTex,
  color: 0x7ec8ff,
  transparent: true,
  opacity: 0.35,
  blending: THREE.AdditiveBlending,
  depthWrite: false,
  fog: false
}));
textHalo.scale.set(13, 4.5, 1);
textHalo.position.z = -0.6;
textGroup.add(textHalo);

/* =========================================================
   11. 后处理
   ========================================================= */
const composer = new EffectComposer(renderer);
composer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
composer.setSize(window.innerWidth, window.innerHeight);

composer.addPass(new RenderPass(scene, camera));

const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  0.85,  // strength
  0.70,  // radius
  0.72   // threshold
);
composer.addPass(bloomPass);

composer.addPass(new OutputPass());

/* =========================================================
   12. 动画更新
   ========================================================= */
const lookTarget = new THREE.Vector3(0, 1.7, -2.6);
let lastBunnyY = 0;
let earLag = 0;

function updateCamera() {
  const t = time;
  camera.position.set(
    0.8 + Math.sin(t * 0.062) * 2.6,
    2.15 + Math.sin(t * 0.091 + 0.7) * 0.38,
    9.2 + Math.cos(t * 0.045) * 1.4
  );
  lookTarget.set(
    Math.sin(t * 0.071) * 0.9,
    1.72 + Math.sin(t * 0.11 + 1.3) * 0.26,
    -2.6 + Math.cos(t * 0.053) * 1.2
  );
  camera.lookAt(lookTarget);
}

function updateBunny(dt) {
  const P = 1.75;
  const c = (time % P) / P;

  let y = 0, squash = 1, tilt = 0;

  if (c < 0.30) {
    // 蓄力下蹲
    const k = c / 0.30;
    const s = Math.sin(k * Math.PI);
    y = -0.09 * s;
    squash = 1 - 0.16 * s;
    tilt = 0.10 * s;
  } else if (c < 0.78) {
    // 腾空
    const k = (c - 0.30) / 0.48;
    const s = Math.sin(k * Math.PI);
    y = s * 0.85;
    squash = 1 + 0.14 * s;
    tilt = -0.28 * s;
  } else {
    // 落地缓冲
    const k = (c - 0.78) / 0.22;
    const s = Math.sin(k * Math.PI);
    y = 0;
    squash = 1 - 0.22 * s;
    tilt = 0.14 * s;
  }

  const vel = (y - lastBunnyY) / Math.max(dt, 0.001);
  lastBunnyY = y;
  earLag += (-vel * 0.16 - earLag) * Math.min(1, dt * 8);

  bunny.position.y = 0.30 + y;
  bunny.rotation.x = tilt;
  const sxz = 1 / Math.sqrt(squash);
  bunny.scale.set(1.15 * sxz, 1.15 * squash, 1.15 * sxz);

  // 耳朵晃动
  bunnyRig.earPivots.forEach((pivot, i) => {
    const phase = i * 1.1;
    const flutter = Math.sin(time * 3.1 + phase) * 0.07;
    const bounce = Math.sin(time * 6.2 + phase) * 0.035 * Math.abs(vel);
    pivot.rotation.x = -0.10 + earLag * 0.9 + flutter + bounce;
  });

  // 围巾飘动
  const pos = bunnyRig.scarfTail.geometry.attributes.position;
  const base = bunnyRig.scarfBase;
  for (let i = 0; i < pos.count; i++) {
    const bx = base[i * 3];
    const by = base[i * 3 + 1];
    const bz = base[i * 3 + 2];
    const k = Math.min(1, Math.abs(by) / 0.9);
    pos.setX(i, bx + Math.sin(time * 2.6 + k * 4.2) * 0.09 * k - vel * 0.05 * k);
    pos.setZ(i, bz + Math.sin(time * 2.0 + k * 3.1) * 0.11 * k - k * 0.12 - vel * 0.4 * k);
  }
  pos.needsUpdate = true;

  // 背包喷口的呼吸感
  bunnyRig.packGlows.forEach((g, i) => {
    const s = 0.34 + Math.abs(Math.sin(time * 5 + i * 1.7)) * 0.18;
    g.scale.set(s, s, 1);
  });
}

const jetLookAt = new THREE.Vector3();

function updateJet(dt) {
  const u = time * 0.075;
  const R = 12.0;

  const cx = Math.sin(u) * R;
  const cz = -14 + Math.cos(u) * R * 0.85;
  const cy = 6.6 + Math.sin(u * 2.3) * 1.15;

  jet.position.set(cx, cy, cz);

  // 切向速度方向
  const vx = Math.cos(u) * R;
  const vz = -Math.sin(u) * R * 0.85;
  jetLookAt.set(cx + vx, cy + Math.sin(u * 2.3 + 0.4) * 0.35, cz + vz);
  jet.lookAt(jetLookAt);

  // 侧倾(转弯 banking)
  jet.rotateZ(0.22 + Math.sin(time * 0.4) * 0.05);

  // 尾焰脉动
  const pulse = 0.85 + Math.sin(time * 12) * 0.12 + Math.sin(time * 5.3) * 0.06;
  jetRig.flame.scale.set(0.9, 0.9 * pulse, pulse);
  jetRig.flame.material.opacity = 0.55 + Math.sin(time * 9) * 0.12;
}

function updateClouds(dt) {
  for (const c of clouds) {
    c.position.x += c.userData.speed * dt * 0.45;
    if (c.position.x > 130) c.position.x = -130;
    c.rotation.y += dt * 0.02;
  }
}

/* =========================================================
   13. 主循环
   ========================================================= */
function animate() {
  requestAnimationFrame(animate);

  const dt = Math.min(clock.getDelta(), 0.05);
  time += dt;

  updateCamera();
  updateBunny(dt);
  updateJet(dt);
  updateClouds(dt);

  // 海面波动
  waveUniforms.uTime.value = time;

  // 平台轻微浮动
  platformGroup.position.y = Math.sin(time * 0.72) * 0.045;

  // 文字始终面向镜头 + 缓慢呼吸漂浮
  textGroup.quaternion.copy(camera.quaternion);
  textGroup.position.y = 4.7 + Math.sin(time * 0.55) * 0.16;
  textGroup.position.x = Math.sin(time * 0.31) * 0.35;
  textMesh.material.opacity = 0.82 + Math.sin(time * 1.1) * 0.13;

  composer.render();
}

/* =========================================================
   14. 响应式
   ========================================================= */
function onResize() {
  const w = window.innerWidth;
  const h = window.innerHeight;
  camera.aspect = w / h;
  camera.updateProjectionMatrix();
  renderer.setSize(w, h);
  composer.setSize(w, h);
  bloomPass.resolution.set(w, h);
}
window.addEventListener('resize', onResize);

/* =========================================================
   15. 启动
   ========================================================= */
animate();

setTimeout(() => {
  const el = document.getElementById('loading');
  if (el) el.classList.add('hide');
}, 600);
</script>
</body>
</html>

梦幻太空海岸的视觉与叙事解析

画面把深蓝到紫色的渐变天空、体积感紫色积云、镜面海面和未来战斗机拢在一起,镜头缓慢环绕。

  • 主角与动作:Space Bunny 有长耳朵、圆润身体、护目镜和喷气背包,蹦跳时带出下蹲、腾空、落地的弹性,耳朵和围巾也会跟着晃动。
  • 场景元素:战斗机沿空中轨迹巡航,海面用 Reflector 做实时倒影,波光层添了细微波动,云朵缓慢漂移。
  • 镜头与文字:镜头持续低角度缓慢环绕,发光文字“just breathe”始终面向观众,像漂浮在天空中的温柔提醒。

优化建议: 如果想调整整体氛围,可以修改 animate 函数中 uTime 的系数来改变海面波动速度;记得在浏览器控制台查看加载状态,确保 CDN 资源可用。

0.46s
TTFT
Actual wait 0.71s
228
Reasoning TPS
194s · 44,398 tok
378
Output TPS
29.8s · 11,233 tok
55,631
Total Tokens (official)
Input 501
Timing details
Actual wait (client)
0.71s
Client preparation
0.04s
Proxy preparation
0.00s
Upstream response headers
0.34s
Upstream first byte
0.34s
Upstream first token
0.46s
Upstream retries
0

Actual wait uses the client clock. Upstream timings start after proxy preparation and are cumulative; do not add these values. Upstream delay includes network, scheduling, processing and buffering; it does not prove provider queueing.

Diagnostic receipt
{
  "attempts": 1,
  "startedAt": "2026-09-29T03:43:55.799Z",
  "transport": "railway",
  "ttftSource": "proxy",
  "clientTtftMs": 710.5,
  "completionId": "752b3d6b-b846-42f5-aa6c-8b61ae2a0c2e",
  "streamResumes": 4,
  "proxyPrepareMs": 0,
  "proxyRequestId": "HE-CGXgzQ-e_z4zHpHNmDw",
  "upstreamStatus": 200,
  "upstreamTtftMs": 464,
  "clientHeadersMs": 251,
  "clientPrepareMs": 44.40000009536743,
  "upstreamHeadersMs": 335,
  "upstreamFirstByteMs": 336,
  "clientFirstContentMs": 195086.10000038147
}
Elapsed 225s · Average 248 tok/s · Peak 434 tok/s
智谱 GLM-5.3-flash
glm-5.3-flash
Done

Historical result · 9/29/2026, 04:01:50

这是一个非常有创作空间的任务。我要做一个"梦境太空海岸 + 太空兔 + 未来战机"的单文件 Web3D 作品。让我认真规划。 **技术选型**:Three.js 通过 CDN(unpkg 或 cdnjs 的固定版本),使用全局构建版 three.min.js(r160+ 已弃用非 module 版本,但 r128 之类的经典版本仍有全局 THREE)。为了稳妥,我用 `https://cdnjs.cloudflare.com/ajax/libs/three.js/0.160.0/three.min.js`——等等,0.160 在 cdnjs 上有 three.min.js 吗?cdnjs 上 0.150+ 只有 module 版本。安全做法:用 `https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js`,这是最经典最稳定的全局版本。r128 足够我做所有事情(雾、shader、组、骨骼式动画用代码控制)。 不过 r128 的 outputEncoding 是 sRGBEncoding,色彩管理较老但可控。我用 r128。 **场景构成**: 1. **天空**:大球体 + 自定义 shader,做深蓝→紫的渐变,靠近海平线处微微泛一点暖(像梦境黄昏)。加程序化星星(shader 里 hash 噪声闪烁)。也可以加一个低垂的大月亮/行星,增加梦境感。 2. **海面**:大平面 + 自定义 shader:法线扰动(多组正弦波叠加)、反射感用伪反射——因为真反射太贵,我用 shader 混合天空色 + 月亮/战机灯光的高光条 + Fresnel。让海面有明显反光:一条从远处月亮延伸到近处的镜面高光带,加上战机掠过时的微光。海面 shader 里可以做流动的波纹和 sparkle。 3. **积云**:用多个半透明球体组成的体积感云团(sphere cluster + MeshStandard/Lambert + 层次透明度),或者用 shader 做 fake volume。做法:每朵云是 5-10 个变形球(icosahedron + noise 顶点位移或多个 sphere 叠加),材质用 MeshStandardMaterial + emissive 紫色,flatShading 关掉。为了体积感,用多层同心球 + BackSide 外壳也行,但简单做法:一堆融合感球 + 顶点噪声起伏 + 柔和光照 + 底部略暗。云缓慢漂移、轻微起伏。 4. **Space Bunny**:纯 Three 几何体拼装: - 身体:圆润球体(scale 成蛋形) - 头:大球(比例偏 Q 版,头大身小) - 长耳朵:两根拉长的胶囊状(用 CapsuleGeometry?r128 没有 Capsule——r128 没有 CapsuleGeometry,是 r142+ 加入的。用拉长的 SphereGeometry(scale y)或者 CylinderGeometry + 球端。我用 SphereGeometry scale(0.35, 1.4, 0.5) 之类,耳朵内层粉色薄片)。 - 耳朵动画:swing(旋转 z/x 随 sin 摆动,跳起时往后飘)。 - 眼睛:黑色小球 + 白色高光点;或带一点"酷"——加一个小的护目镜/头盔面罩:透明玻璃半球罩在脸上,像宇航头盔,这很"太空兔"。我决定:兔子戴一个透明的球形头盔(前半透明玻璃 + 金属环),瞬间太空感拉满。眼睛在头盔里。 - 鼻子:粉色小球。 - 胡须:细线(可选)。 - 手手脚脚:小椭球。 - 太空装备:背后的迷你喷气背包(小盒子 + 两个圆柱喷口 + 粒子尾焰/光点),胸前的控制小面板,一根天线(细圆柱 + 顶端发光小球)——天线小球闪烁,很有辨识度。 - 涂装/性格:"有点酷":头盔上有反光条,背包白绿配色呼应战机。 - 身体弹性:scale squash & stretch(跳起拉长、落地压扁),用弹簧式插值。 5. **蹦跳动画**:参数化跳跃:兔子在几个"落脚点"之间循环跳跃——机翼上 → 前景平台 → 海边平台 → 回机翼?跳跃路径要有故事感。设计一条循环路线: - 点 A:战机左机翼(战机悬停在低空) - 点 B:海边平台(一个发光的圆形停机坪/码头,伸入海面) - 点 C:前景靠近镜头的一块浮岩/平台 蹦跳:抛物线插值(sin 曲线),带 squash-stretch,耳朵拖尾摆动,落地时平台微光涟漪。跳跃之间有短暂 idle(呼吸起伏、耳朵轻摆、偶尔看看镜头)。 具体节奏:jump(0.9s) → idle(1.2s) → jump → idle……时间轴驱动。 6. **未来战斗机**:白、绿、红涂装。用几何体拼: - 机身:拉长纺锤(用 CylinderGeometry 变形或 LatheGeometry)。我用几个部件:机头锥、机身圆柱(scale)、座舱罩(半球、玻璃材质、微发光青色)、主翼(薄盒子 shape 挤出或 BoxGeometry 斜切——用 shape+extrude 更好看:三角翼/后掠翼)、垂尾、引擎喷口(圆柱 + 内部发光盘 + 尾焰光锥 shader/加性材质)、进气道。 - 涂装:白色主体,绿色条纹(用单独的薄几何贴在机身上或机翼上的色带 box),红色点缀(翼尖、垂尾尖端红色)。这个用简单几何色块实现,干净未来感。 - 悬停动画:整体轻微上下浮动 + 缓慢的 pitch/roll 微摆;周期性地"掠海巡飞":沿着一条低空路径缓慢移动(绕一个大圈或沿岸往返),喷口尾焰随速度变化,海面上留下反光。悬停位机翼就是兔子的落脚点之一——所以兔子跳到机翼上时战机要保持在附近的悬停位。设计:战机大部分时间在两个悬停点之间缓慢往返掠海(路径平滑),兔子跳跃目标点绑定在战机机翼的局部坐标?那会更精彩——兔子站在机翼上随机机一起移动! 这是个亮点:**兔子跳上机翼后,跟随战机一起掠海飞行一段**,然后再跳到海上平台。实现:定义路径点序列,其中"机翼"落点是相对战机的局部偏移,每帧把世界位置算出来。跳跃目标在起跳瞬间锁定目标点对象(scene 或 jet),每帧用当时的实时目标位置作为抛物线终点,落地后把兔子 attach 到对应父级(或每帧同步位置)。 简化实现:兔子的"状态机": - state: perchedOnJet / perchedOnPlatformA / perchedOnPlatformB / hopping - perched 时:如果 perch 父级是 jet,兔子的位置 = jet.localToWorld(offset);做 idle 动画。 - hopping 时:记录 startWorldPos(固定)和 targetGetter(函数,每帧返回目标世界坐标),插值位置 + 抛物线高度。 - 到达后设置新的 perch 父级。 路线循环:Platform(前景浮台) → Jet右机翼(随机掠海一段)→ 海上发光圆台 → Jet左机翼 → 回前景浮台…… 我设计 3~4 站循环。 7. **镜头语言**:电影感运镜: - 缓慢的自动运镜:低机位 + 缓慢推拉(dolly in/out 的呼吸)+ 轻微环绕+ 视线注视点在兔子和战机之间柔和过渡。 - 用多个预设"镜头段"循环:比如 (a) 低机位海面视角看战机与月,(b) 环绕前景浮台看兔子特写,(c) 侧面跟拍掠海的战机,(d) 远景全貌。镜头之间用平滑插值过渡(ease),FOV 微变制造景深感(宽 FOV 近景/长焦远景)。 - 我用"相机轨道参数关键帧"方式:定义 shots = [{pos, lookAt, fov, duration}],插值时用 smoothstep + 位置 catmull 或 lerp。为避免生硬,镜头切换时做交叉的缓动(当前 shot 参数向下一个 shot 参数 ease,各 shot 持续时间不同)。另外叠加手持般的极轻微噪声晃动(幅度极小)让画面"呼吸"。 - 也允许鼠标微视差(不做成全 orbit 控制器,避免破坏电影感;加一点点鼠标偏移即可)。滚轮/拖动要不要?用户强调镜头语言,我保留轻微鼠标视差即可,另提供空格或点击切换镜头?点击画面跳到下一个镜头,加个提示。简单起见:自动运镜 + 鼠标视差 + 点击切换机位。 8. **文案 "just breathe"**: - 做成发光 3D 文字:不能用字体文件(避免外部资源复杂化)——用 canvas 生成文字纹理,贴在一个平面上放在天空/月亮旁,微微漂浮发光;同时在海面 shader 里……海面倒影里出现文字比较难。折中方案: - 一块发光漂浮牌/浮标装置在海上,屏幕 canvas 纹理写 "just breathe"(手写风格字体?系统字体里选 serif italic + letterspacing,用 canvas 绘制发光效果),双面 + 加性发光层,缓慢上下浮动,海面上放一个拉长的模糊倒影平面(同纹理,透明度低、压扁、翻转)。 - 也可以在天空远远地放一行淡淡的发光字,像星座排布。我选:**漂浮在前景海面上方的一块小光牌**(像宇航浮标),文字 "just breathe" 用 canvas 纹理 + UnrealBloom 风格的自发光(不上后处理 bloom,r128 加 bloom 要 EffectComposer 额外脚本……可以引 examples 的 js?增加外部依赖风险。我用"加性材质 + 光晕 sprite"手动模拟发光,足够好看)。再加天空远处一行淡淡的星文字(同纹理,透明度低、随呼吸闪烁)。 手动 bloom 替代:给发光体加一个 sprite(径向渐变 canvas 纹理,AdditiveBlending),简单有效。 9. **粒子**:漂浮的微光尘埃(Points,缓慢上升/漂移),兔子跳落时的光尘涟漪,战机尾焰粒子。适度即可。 10. **UI/文案**:左下角小字标题 "SPACE BUNNY · 梦境海岸" 之类 + 右下角提示(点击切换镜头)。加载即运行。加一个 2~3 秒的开场淡入和标题浮现,电影感。 **性能与结构**:全部程序化几何,无外部模型。canvas 纹理只有文字和光晕。代码组织成清晰模块:createSky, createSea, createClouds, createJet, createBunny, particles, camera director, main loop。 现在开始细化关键实现。 --- **天空 shader**: ```glsl varying vec3 vWorldPos; // fragment: vec3 dir = normalize(vWorldPos); float h = dir.y; // 深蓝(顶) → 紫(中) → 海平线暖紫粉 vec3 top = vec3(0.02,0.03,0.12); vec3 mid = vec3(0.18,0.08,0.30); vec3 hor = vec3(0.45,0.22,0.42); // 海平线偏亮 ``` 渐变用 smoothstep 组合。星星:把方向映射到网格 hash,只在 h>0.05 处,闪烁 `sin(time*speed+hash*6.28)`。加银河感?可选:一条斜向的淡色带(用 dir 与某轴的距离)+ 噪声,让天空不那么平。加一个大月亮:在天空 shader 里画一个圆盘(dir·moonDir),带柔光边缘(smoothstep),月亮给淡金色/冷白色,海面的高光带对准月亮方向。月亮放在画面构图的重要位置(三分线上)。 **海面 shader**: 平面(大,比如 400x400,分段 128x128 或直接在 fragment 里做 normal 扰动而不位移顶点——位移顶点做起伏 + fragment 法线细节)。 ```glsl // vertex: 轻微 sin 波位移(大浪),传 world pos // fragment: // 用多组方向不同的 sin/cos 波计算 normal 微扰 // 基础色 = 天空反射色(近似:根据反射向量的 y 采样一个简单渐变函数,同天空配色) // 高光:specular 对准"月亮方向"的 GGX 近似 → 拉出一条月光滑带 // Fresnel:掠射角更亮更镜像 // sparkle:noise 阈值闪点 ``` 海面反射战机:真反射贵。技巧:在战机下方海面处放一个"假的倒影光斑"——一个跟随战机 xz、半透明的发光 sprite 拉长竖条,模拟倒影。成本低效果好。同样月亮在海面的高光带在 shader 里做。倒影兔子也可以加一个小光斑(跟随兔子)。这些倒影 sprite 放在略高于海面处、blend 加性、透明度低。 **云**:makeCloud:组内多个 sphere(MeshStandardMaterial,颜色淡紫白,emissive 紫微光,roughness 高),球体顶点随机偏移制造不规则。为体积感:外层再包 1~2 个 scale 稍大、透明度 0.15~0.25 的壳球(side: FrontSide, transparent, depthWrite false)。云 6~9 朵,分布在不同高度和深度,缓慢漂移 + 各自 sin 起伏。云底加一点较暗紫。用 hemisphere light + directional(月光)照亮云。 灯光: - HemisphereLight(天空紫蓝, 海面深紫, 0.5) - DirectionalLight 月光(冷白微金,从月亮方向,强度 0.9),产生阴影?阴影能提升质感:战机和兔子投影到平台/机翼。开 shadowMap,让 dir light 覆盖主要区域。海面是 shader 材质不接收阴影(自定义 shader 不接收,没关系)。平台、机翼用标准材质可接收阴影。开阴影,PCFSoft。 - 一个跟随兔子的 PointLight(淡青/淡粉,弱),让兔子始终有局部光照,非常提气。 - 战机引擎 PointLight(青绿或暖橙)照亮机尾和海面附近。 **战机建模**(组 jet): - 机身:`LatheGeometry`?简单点:CylinderGeometry(0.5, 0.8, 5, 24) 旋转至 z 轴 + 前端 ConeGeometry 机头 + 后端收口。或者用 SphereGeometry 拉长 scale(0.8, 0.8, 2.6) 作为机身主体(圆润未来感),机头再接一个锥。我用: - fuselage: SphereGeometry(0.8) scale z 3 → 圆润纺锤 - nose: ConeGeometry(0.45, 1.6) 指向 -z,接在前端 - cockpit: SphereGeometry scale(0.5, 0.45, 0.9),位于机身上方前部,材质玻璃(MeshPhysicalMaterial r128 有吗?MeshPhysicalMaterial 存在于 r128。用 transmission?r128 有 transmission 但效果一般;用 MeshPhongMaterial 半透明 + 高光 + envMap 感。简单:MeshStandardMaterial color 深青,metalness 0.9 roughness 0.1, emissive 微青,opacity 0.9)。座舱里放一个小座椅剪影?可省。 - 主翼:Shape 三角后掠 → ExtrudeGeometry(薄)。左右各一或一体穿过机身。翼尖红色小条。 - 垂尾:两片外倾 V 尾(更未来)或单片大垂尾,尖端红色。 - 绿色条纹:细长 box 贴机身两侧/机背,或环形条纹(Torus 环贴机身)。做:机背一条绿色长条 box(薄,微凸),翼根处绿色带,翼尖红,垂尾红——"白绿红" = 意大利国旗配色,挺帅。 - 引擎:机尾 1~2 个喷口,Torus + 内部发光圆盘(emissive 青绿),尾焰:ConeGeometry 加性半透明 shader 或双层 cone(内白外青)+ 闪烁 scale。加 PointLight。 - 悬停反重力光:机腹下一个发光圆盘 + 光环,淡淡青光,呼应"未来感"。 - 起落/天线小细节、翼下小挂载。 战机动画:沿一条平缓路径:两点往返(P1 在前景左侧低空、P2 在右侧远处),用 easing 的 ping-pong + 垂直/俯仰随速度微调,roll 转弯倾斜。速度慢(掠海巡航感)。悬停时段:在端点停留数秒(轻微上下浮动)。实现:位置 = f(t) 分段:巡航段 lerp + sin 浮动;端点 hover 段。用平滑函数:`u = pingpong`;朝向:lookAt(前进方向)。roll:根据横向速度/转向量倾斜。 兔子跳上机翼时战机正好处于可跳阶段——因为 perch 是动态绑定的,无需协调时刻,任何时刻跳上机翼都随战机移动。但要防止战机掠到远处时兔子跟着跑到很远——没关系,其实这正是故事感:兔子搭机飞行!路线设计:前景浮台 → 右机翼(搭飞 ~12s 跨越大半路径)→ 跳向海上圆台(此时圆台固定,需要判断战机接近圆台时才跳,否则跳过远)。为控制节奏,我把跳跃目标改为"固定点"与"移动点"交替,并在战机路线上让机翼 perch 停留固定时长(兔子的 idle 时长 + 一次跳跃刚好发生在战机靠近海上圆台的时刻附近——即使不完美,跳跃是动态追踪目标位置的,距离不会太夸张;再限制跳跃时长固定,落点=当时目标位置,只要圆台够大/瞄准时给提前量)。简单做法:兔子在机翼上的停留时间随机 8~14s,跳向海上圆台时以当前战机位置为起点,目标固定圆台——距离可能 5~30 单位,跳跃时间随距离伸缩(hopDuration = clamp(dist*0.12, 0.7, 1.6)),跳跃高度随距离,这样无论何时跳都自然。好。 兔子路线循环(站点): 1. `pad`(前景发光圆台,浮在近岸水面,会轻晃) 2. `wingR`(战机右翼外段 perch) 3. `rock`(岸边礁石平台,低机位镜头喜欢的地方) 4. `wingL`(战机左翼) 循环:pad → wingR → rock → wingL → pad … 跳跃时的小细节:起跳蹲一下(squash),空中 stretch + 耳朵后摆,落地 squash + 涟漪光圈(在落点放一个扩散的 ring mesh,fade out)+ 少量光尘粒子。 兔子 idle:身体 scale 呼吸,耳朵缓慢摆,

我来完成这个作品。我的构思是:把「梦境海岸」做成一个真正有叙事闭环的小世界——白色小战机沿着海岸低空往返巡飞,兔子在浮台、礁石与机翼之间循环蹦跳,等战机掠过头顶时一跃登机、搭飞一段再跳回岸边;月光在海面拉出一条碎金光带,"just breathe" 做成一座漂浮在海上的发光信标,远处天空还有一行呼吸般明灭的同款文字。镜头是自动的电影化运镜(五组机位循环:入梦 / 低语海面 / 翼上同行 / 掠海 / 浮台特写),点击画面可以切换。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Space Bunny · 梦境海岸</title>
<style>
  html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#05030f;}
  canvas{display:block;}
  /* ---------- 电影暗角与颗粒 ---------- */
  #vig{position:fixed;inset:0;pointer-events:none;z-index:5;
    background:radial-gradient(ellipse at 50% 42%, transparent 52%, rgba(4,2,16,.62) 100%);}
  #grain{position:fixed;inset:0;pointer-events:none;z-index:6;opacity:.05;
    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="0.85" numOctaves="2"/></filter><rect width="140" height="140" filter="url(%23n)" opacity="0.55"/></svg>');}
  /* ---------- 常驻 UI ---------- */
  .serif{font-family:Georgia,'Times New Roman','Songti SC',serif;}
  #brand{position:fixed;left:30px;bottom:26px;z-index:10;color:#d8cdf2;pointer-events:none;user-select:none;}
  #brand h1{margin:0;font-size:15px;font-weight:600;letter-spacing:.5em;text-shadow:0 0 18px rgba(140,110,220,.6);}
  #brand p{margin:6px 0 0;font-size:11px;letter-spacing:.22em;opacity:.62;font-style:italic;}
  #hint{position:fixed;right:30px;bottom:28px;z-index:10;color:#cfc3ee;font-size:11px;
    letter-spacing:.3em;pointer-events:none;user-select:none;animation:dim 2s ease 9s forwards;}
  @keyframes dim{to{opacity:.4}}
  #shotname{position:fixed;left:30px;bottom:86px;z-index:10;color:#efe9ff;font-size:12px;
    letter-spacing:.55em;opacity:0;pointer-events:none;}
  #shotname.show{animation:shotIn 3.4s ease forwards;}
  @keyframes shotIn{0%{opacity:0;transform:translateY(10px)}14%{opacity:.92;transform:none}68%{opacity:.92}100%{opacity:0}}
  /* ---------- 开场 ---------- */
  #fade{position:fixed;inset:0;background:#04020d;z-index:20;pointer-events:none;
    animation:fadeAway 3.2s ease .4s forwards;}
  @keyframes fadeAway{to{opacity:0;visibility:hidden}}
  #title{position:fixed;inset:0;display:flex;flex-direction:column;justify-content:center;align-items:center;
    z-index:21;pointer-events:none;opacity:0;color:#eae2ff;text-align:center;
    animation:titleSeq 10s ease 1.6s forwards;}
  #title h1{margin:0;font-size:clamp(30px,5.4vw,58px);font-weight:600;letter-spacing:.55em;text-indent:.55em;
    text-shadow:0 0 34px rgba(150,120,255,.75),0 0 90px rgba(120,90,220,.4);}
  #title p{margin:22px 0 0;font-size:clamp(12px,1.4vw,16px);letter-spacing:.4em;font-style:italic;opacity:.8;
    color:#bfeee2;text-shadow:0 0 22px rgba(110,240,215,.5);}
  @keyframes titleSeq{0%{opacity:0;transform:translateY(14px)}18%{opacity:1;transform:none}74%{opacity:1}100%{opacity:0;visibility:hidden}}
</style>
</head>
<body>

<div id="vig"></div>
<div id="grain"></div>
<div id="brand" class="serif"><h1>SPACE BUNNY</h1><p>梦境海岸 · The Dreaming Shore</p></div>
<div id="hint" class="serif">轻点画面 · 切换镜头</div>
<div id="shotname" class="serif"></div>
<div id="fade"></div>
<div id="title" class="serif"><h1>SPACE BUNNY</h1><p>—— 梦境海岸 · just breathe ——</p></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
'use strict';
/* ============================================================
   SPACE BUNNY · 梦境海岸
   天空 / 海面 / 积云 / 未来战机 / 太空兔 / 发光信标 / 电影运镜
   ============================================================ */

/* ---------------- 基础 ---------------- */
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x1a0f33, 0.0075);

const camera = new THREE.PerspectiveCamera(46, innerWidth/innerHeight, 0.1, 700);
camera.position.set(-28, 9.5, 34);

/* 程序化环境立方体贴图:给金属件一点紫夜的环境反射 */
function gradFace(top, bottom){
  const c = document.createElement('canvas'); c.width = c.height = 32;
  const x = c.getContext('2d');
  const g = x.createLinearGradient(0,0,0,32);
  g.addColorStop(0, top); g.addColorStop(1, bottom);
  x.fillStyle = g; x.fillRect(0,0,32,32);
  return c;
}
const envTex = new THREE.CubeTexture([
  gradFace('#241a4a','#0d0a24'), gradFace('#241a4a','#0d0a24'),
  gradFace('#7a58b2','#241a4a'), gradFace('#0a0620','#140b30'),
  gradFace('#3a2a6a','#0d0a24'), gradFace('#3a2a6a','#0d0a24')
]);
envTex.needsUpdate = true;
scene.environment = envTex;

/* ---------------- 灯光 ---------------- */
scene.add(new THREE.AmbientLight(0x241a3e, 0.6));
scene.add(new THREE.HemisphereLight(0x8a6ad0, 0x1a0f33, 0.5));

const moonDir = new THREE.Vector3(-0.35, 0.34, -1).normalize();
const moonLight = new THREE.DirectionalLight(0xf0e0c0, 0.9);
moonLight.position.copy(moonDir).multiplyScalar(70);
moonLight.castShadow = true;
moonLight.shadow.mapSize.set(2048, 2048);
moonLight.shadow.bias = -0.0005;
const sc = moonLight.shadow.camera;
sc.left = -38; sc.right = 38; sc.top = 38; sc.bottom = -38; sc.near = 10; sc.far = 170;
scene.add(moonLight);

/* ---------------- 小工具 ---------------- */
const V3 = (x,y,z)=>new THREE.Vector3(x,y,z);
const clamp = THREE.MathUtils.clamp;
const rand = (a,b)=>a + Math.random()*(b-a);
function lerpAngle(a,b,k){
  let d = ((b - a + Math.PI) % (Math.PI*2) + Math.PI*2) % (Math.PI*2) - Math.PI;
  return a + d*k;
}
/* 柔光贴图(用于光晕 sprite)*/
function glowTexture(inner, outer){
  const c = document.createElement('canvas'); c.width = c.height = 128;
  const x = c.getContext('2d');
  const g = x.createRadialGradient(64,64,0,64,64,64);
  g.addColorStop(0, inner); g.addColorStop(0.35, outer); g.addColorStop(1, 'rgba(0,0,0,0)');
  x.fillStyle = g; x.fillRect(0,0,128,128);
  return new THREE.CanvasTexture(c);
}
const softGlow = glowTexture('rgba(255,255,255,1)','rgba(255,255,255,0.28)');
function makeGlowSprite(color, w, h, opacity){
  const m = new THREE.SpriteMaterial({map:softGlow, color:color, transparent:true,
    blending:THREE.AdditiveBlending, depthWrite:false, opacity:opacity});
  const s = new THREE.Sprite(m); s.scale.set(w, h, 1); return s;
}
/* 发光文字纹理(逐字符加字距 + 多层柔光)*/
function makeTextTexture(withChrome){
  const c = document.createElement('canvas'); c.width = 1024; c.height = 320;
  const g = c.getContext('2d');
  const text = 'just breathe';
  g.font = 'italic 600 128px Georgia, "Times New Roman", serif';
  g.textBaseline = 'middle'; g.textAlign = 'center';
  const ls = 10;
  const widths = [...text].map(ch => g.measureText(ch).width);
  const total = widths.reduce((a,b)=>a+b,0) + ls*(text.length-1);
  function pass(blur, fill){
    g.shadowColor = 'rgba(126,240,220,0.85)'; g.shadowBlur = blur; g.fillStyle = fill;
    let x = (1024-total)/2;
    for(let i=0;i<text.length;i++){ g.fillText(text[i], x+widths[i]/2, 148); x += widths[i]+ls; }
  }
  if(withChrome){
    pass(42,'rgba(126,240,220,0.26)'); pass(20,'rgba(126,240,220,0.5)');
    pass(8,'rgba(180,255,240,0.85)');  pass(0,'rgba(255,255,255,0.98)');
    g.shadowBlur = 0;
    g.strokeStyle = 'rgba(160,244,226,0.55)'; g.lineWidth = 2;
    g.beginPath(); g.moveTo(180,256); g.lineTo(844,256); g.stroke();
    g.beginPath(); g.moveTo(236,34);  g.lineTo(788,34);  g.stroke();
    const dia = (cx,cy,r)=>{g.beginPath();g.moveTo(cx,cy-r);g.lineTo(cx+r,cy);g.lineTo(cx,cy+r);g.lineTo(cx-r,cy);g.closePath();g.fill();};
    g.fillStyle = 'rgba(160,244,226,0.8)';
    dia(160,256,5); dia(864,256,5); dia(216,34,4); dia(808,34,4);
    g.font = '22px Georgia, serif'; g.textAlign = 'center'; g.fillStyle = 'rgba(190,236,226,0.55)';
    g.fillText('L U N A · 7    D E E P - S H O R E   B E A C O N', 512, 292);
  }else{
    pass(30,'rgba(200,235,255,0.55)'); pass(10,'rgba(235,245,255,0.8)');
  }
  return new THREE.CanvasTexture(c);
}

/* ---------------- 天空 ---------------- */
const skyMat = new THREE.ShaderMaterial({
  side: THREE.BackSide, depthWrite: false, fog: false,
  uniforms: { uTime:{value:0}, uMoonDir:{value:moonDir} },
  vertexShader: `
    varying vec3 vDir;
    void main(){ vDir = position; gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0); }`,
  fragmentShader: `
    uniform float uTime; uniform vec3 uMoonDir; varying vec3 vDir;
    float hash21(vec2 p){ p=fract(p*vec2(127.1,311.7)); p+=dot(p,p+34.5); return fract(p.x*p.y); }
    void main(){
      vec3 d = normalize(vDir);
      float h = d.y;
      /* 深蓝 → 紫 → 海平线暖紫 */
      vec3 col = mix(vec3(0.40,0.20,0.44), vec3(0.15,0.07,0.30), smoothstep(-0.02,0.42,h));
      col = mix(col, vec3(0.015,0.03,0.10), smoothstep(0.30,0.85,h));
      col = mix(vec3(0.10,0.05,0.18), col, smoothstep(-0.25,0.0,h));
      /* 一抹斜向银河 */
      float band = exp(-pow(dot(d, normalize(vec3(0.55,0.30,-0.75))),2.0)*7.0);
      col += vec3(0.09,0.05,0.14)*band*smoothstep(-0.05,0.3,h);
      /* 星星(网格 hash + 闪烁)*/
      vec2 sp = vec2(atan(d.z,d.x)*10.0, d.y*36.0);
      vec2 cell = floor(sp); vec2 f = fract(sp)-0.5;
      float rnd = hash21(cell);
      vec2 off = vec2(hash21(cell+7.3), hash21(cell+3.1))-0.5;
      float star = smoothstep(0.10,0.0,length(f-off*0.7))*step(0.82,rnd);
      star *= 0.5+0.5*sin(uTime*(1.0+rnd*4.0)+rnd*40.0);
      col += vec3(0.9,0.92,1.0)*star*smoothstep(0.02,0.25,h)*0.9;
      /* 梦境大月亮 */
      float m = dot(d, uMoonDir);
      float disk = smoothstep(0.99955,0.99985,m);
      vec2 mp = vec2(atan(d.x,d.z)*40.0, d.y*40.0);
      float mn = hash21(floor(mp));
      vec3 moonCol = vec3(0.98,0.90,0.72)*mix(1.0,0.86,step(0.72,mn)*0.6);
      col = mix(col, moonCol, disk);
      col += vec3(0.45,0.34,0.30)*pow(max(m,0.0),180.0)*0.55;
      gl_FragColor = vec4(col,1.0);
    }`
});
const sky = new THREE.Mesh(new THREE.SphereGeometry(300, 48, 32), skyMat);
sky.renderOrder = -1;
scene.add(sky);

/* ---------------- 海面 ---------------- */
const seaMat = new THREE.ShaderMaterial({
  fog: false,
  uniforms: { uTime:{value:0}, uMoonDir:{value:moonDir} },
  vertexShader: `
    uniform float uTime; varying vec3 vWorldPos;
    void main(){
      vec3 pos = position;
      vec2 wp = (modelMatrix*vec4(position,1.0)).xz;
      pos.z += sin(wp.x*0.10 + uTime*0.7)*0.45 + sin(dot(wp,vec2(0.07,0.09)) - uTime*0.5)*0.40;
      vec4 world = modelMatrix*vec4(pos,1.0);
      vWorldPos = world.xyz;
      gl_Position = projectionMatrix*viewMatrix*world;
    }`,
  fragmentShader: `
    uniform float uTime; uniform vec3 uMoonDir; varying vec3 vWorldPos;
    float hash21(vec2 p){ p=fract(p*vec2(127.1,311.7)); p+=dot(p,p+34.5); return fract(p.x*p.y); }
    float waveH(vec2 p, float t, float att){
      float h = sin(p.x*0.10 + t*0.7)*0.45 + sin(dot(p,vec2(0.07,0.09)) - t*0.5)*0.40;
      h += att*( sin(dot(p,vec2(0.8,0.6)) + t*1.1)*0.22
               + sin(dot(p,vec2(-0.6,0.8))*1.7 - t*1.6)*0.15
               + sin(dot(p,vec2(0.25,-0.95))*3.1 + t*2.3)*0.09 );
      return h;
    }
    vec3 skyGrad(vec3 d){
      float h = max(d.y, 0.0);
      vec3 c = mix(vec3(0.40,0.20,0.44), vec3(0.15,0.07,0.30), smoothstep(0.0,0.45,h));
      return mix(c, vec3(0.015,0.03,0.10), smoothstep(0.30,0.9,h));
    }
    void main(){
      vec2 p = vWorldPos.xz; float t = uTime;
      float att = exp(-length(p)*0.012);
      float e = 0.35;
      float hC = waveH(p,t,att), hX = waveH(p+vec2(e,0.0),t,att), hZ = waveH(p+vec2(0.0,e),t,att);
      vec3 n = normalize(vec3(-(hX-hC)/e*0.55, 1.0, -(hZ-hC)/e*0.55));
      vec3 V = normalize(cameraPosition - vWorldPos);
      vec3 R = reflect(-V, n); R.y = abs(R.y)*0.6 + 0.02;
      vec3 sky = skyGrad(normalize(R));
      float fres = pow(1.0 - max(dot(n,V),0.0), 3.0);
      vec3 col = mix(vec3(0.05,0.025,0.13), sky, 0.28 + 0.72*fres);
      /* 云影斑驳 */
      float cl = sin(p.x*0.10+1.7)*sin(p.y*0.12-0.6) + sin(dot(p,vec2(0.05,0.08))*2.0 + t*0.15);
      col += vec3(0.08,0.045,0.13)*max(cl,0.0)*0.35;
      /* 月光高光带 */
      vec3 H = normalize(uMoonDir + V);
      float doth = max(dot(n,H),0.0);
      col += vec3(1.0,0.87,0.62)*(pow(doth,420.0)*1.6 + pow(doth,18.0)*0.20);
      /* 碎金闪点 */
      vec2 scell = floor(p*3.0);
      float sr = hash21(scell + vec2(floor(t*2.0)*17.3, floor(t*2.0)*29.1));
      col += vec3(1.0,0.92,0.72)*step(0.993,sr)*pow(doth,30.0)*1.2;
      /* 与雾色衔接 */
      float d = distance(cameraPosition, vWorldPos);
      float fg = 1.0 - exp(-pow(d*0.0075, 2.0));
      col = mix(col, vec3(0.102,0.059,0.20), fg);
      gl_FragColor = vec4(col, 1.0);
    }`
});
const sea = new THREE.Mesh(new THREE.PlaneGeometry(360, 360, 110, 110), seaMat);
sea.rotation.x = -Math.PI/2;
scene.add(sea);

/* ---------------- 材质库 ---------------- */
const M = {
  white:   new THREE.MeshStandardMaterial({color:0xf2f4f5, metalness:0.35, roughness:0.35}),
  green:   new THREE.MeshStandardMaterial({color:0x2fbf71, metalness:0.3,  roughness:0.4}),
  red:     new THREE.MeshStandardMaterial({color:0xe8433f, metalness:0.3,  roughness:0.4}),
  dark:    new THREE.MeshStandardMaterial({color:0x232030, metalness:0.6,  roughness:0.5}),
  glass:   new THREE.MeshStandardMaterial({color:0x0e2a3a, metalness:0.9,  roughness:0.12, emissive:0x0a3038, emissiveIntensity:0.7}),
  fur:     new THREE.MeshStandardMaterial({color:0xf7f0e8, metalness:0,    roughness:0.95}),
  pink:    new THREE.MeshStandardMaterial({color:0xf0a2b4, metalness:0,    roughness:0.9}),
  darkEye: new THREE.MeshStandardMaterial({color:0x1c1826, metalness:0.2,  roughness:0.35}),
  cloud:   new THREE.MeshStandardMaterial({color:0xd8c8f0, metalness:0,    roughness:1, emissive:0x2a1850, emissiveIntensity:0.65}),
  cloudShell: new THREE.MeshStandardMaterial({color:0xcdb8ee, metalness:0, roughness:1, transparent:true, opacity:0.16, depthWrite:false}),
  rock:    new THREE.MeshStandardMaterial({color:0x2e2444, metalness:0.05, roughness:1}),
  glowTeal:new THREE.MeshBasicMaterial({color:0x8ef2dd}),
  flameIn: new THREE.MeshBasicMaterial({color:0xd8fff4, transparent:true, opacity:0.85, blending:THREE.AdditiveBlending, depthWrite:false, side:THREE.DoubleSide}),
  flameOut:new THREE.MeshBasicMaterial({color:0x5fe8cc, transparent:true, opacity:0.30, blending:THREE.AdditiveBlending, depthWrite:false, side:THREE.DoubleSide})
};

/* ---------------- 积云 ---------------- */
const clouds = [];
function lumpySphere(r, seed){
  const geo = new THREE.SphereGeometry(r, 22, 16);
  const pos = geo.attributes.position;
  const v = new THREE.Vector3();
  for(let i=0;i<pos.count;i++){
    v.fromBufferAttribute(pos, i);
    const nz = Math.sin(v.x*2.6+seed) + Math.sin(v.y*3.1+seed*1.7) + Math.sin(v.z*2.2+seed*0.6);
    v.multiplyScalar(1 + 0.13*Math.sin(nz*2.0));
    pos.setXYZ(i, v.x, v.y, v.z);
  }
  geo.computeVertexNormals();
  return geo;
}
function makeCloud(x,y,z, scale){
  const g = new THREE.Group();
  const n = 5 + Math.floor(Math.random()*4);
  for(let i=0;i<n;i++){
    const r = rand(1.4, 3.0);
    const m = new THREE.Mesh(lumpySphere(r, Math.random()*10), M.cloud);
    m.position.set(rand(-3.5,3.5), rand(-0.7,1.2)*r*0.4, rand(-1.8,1.8));
    m.scale.y = 0.72;
    g.add(m);
    const shell = new THREE.Mesh(m.geometry, M.cloudShell);
    shell.position.copy(m.position); shell.scale.set(1.25, 1.25*0.72, 1.25);
    shell.renderOrder = 2;
    g.add(shell);
  }
  g.position.set(x,y,z);
  g.scale.setScalar(scale);
  g.userData = {drift: rand(0.15,0.45)* (Math.random()<0.5?1:0.4), ph: Math.random()*10, baseY:y};
  scene.add(g); clouds.push(g);
  return g;
}
[[-30,15,-55,1.6],[18,20,-70,2.1],[45,12,-40,1.3],[-55,20,-30,1.8],[8,25,-90,2.6],
 [-15,10,-25,0.9],[60,18,-75,1.9],[-70,14,-70,2.2],[30,9,-18,0.7]].forEach(a=>makeCloud(...a));

/* ---------------- 反重力浮台 ---------------- */
function makePad(radius){
  const g = new THREE.Group();
  const top = new THREE.Mesh(new THREE.CylinderGeometry(radius, radius, 0.18, 48),
    new THREE.MeshStandardMaterial({color:0xdfe4ea, metalness:0.3, roughness:0.4}));
  top.receiveShadow = true; top.castShadow = true;
  g.add(top);
  const rim = new THREE.Mesh(new THREE.TorusGeometry(radius, 0.055, 10, 64), M.glowTeal);
  rim.rotation.x = Math.PI/2; rim.position.y = 0.02;
  g.add(rim);
  const under = new THREE.Mesh(new THREE.CylinderGeometry(radius*0.66, radius*0.22, 0.5, 32), M.dark);
  under.position.y = -0.33; g.add(under);
  const disc = new THREE.Mesh(new THREE.CircleGeometry(radius*0.2, 24), M.glowTeal);
  disc.rotation.x = Math.PI/2; disc.position.y = -0.6; g.add(disc);
  const halo = makeGlowSprite(0x6fe8d0, radius*2.6, radius*1.2, 0.22);
  halo.position.y = -0.75; g.add(halo);
  const perch = new THREE.Object3D(); perch.position.y = 0.09; g.add(perch);
  g.userData.perch = perch; g.userData.rim = rim; g.userData.halo = halo;
  scene.add(g);
  return g;
}
const pad = makePad(1.7); pad.position.set(9, 0.9, 9);

/* ---------------- 浮岩礁石 ---------------- */
function rockBlob(r, seed){
  const geo = new THREE.IcosahedronGeometry(r, 1);
  const pos = geo.attributes.position; const v = new THREE.Vector3();
  for(let i=0;i<pos.count;i++){
    v.fromBufferAttribute(pos,i);
    const nz = Math.sin(v.x*3.3+seed)*Math.sin(v.y*2.7+seed)*Math.sin(v.z*3.9+seed);
    v.multiplyScalar(1 + 0.22*nz);
    pos.setXYZ(i,v.x,v.y,v.z);
  }
  geo.computeVertexNormals();
  const m = new THREE.Mesh(geo, M.rock);
  m.castShadow = true; m.receiveShadow = true;
  return m;
}
const rock = new THREE.Group();
[[0,0,0,1.5],[1.1,-0.3,0.4,1.0],[-0.9,-0.5,-0.3,0.9],[0.2,-0.9,0.7,0.8]].forEach(a=>{
  const b = rockBlob(a[3], Math.random()*10);
  b.position.set(a[0],a[1],a[2]); b.scale.y = 0.7;
  rock.add(b);
});
rock.position.set(-11, 1.0, 3);
scene.add(rock);
const rockPerch = new THREE.Object3D(); rockPerch.position.set(-11, 1.55, 3.1); scene.add(rockPerch);
[[ -10.4, 0.7, 3.6 ],[ -11.6, 0.4, 2.4 ]].forEach(p=>{
  const sp = makeGlowSprite(0x6fe8d0, 0.9, 0.9, 0.35); sp.position.set(...p); scene.add(sp);
});

/* 远处礁石剪影 */
[[-42,0.6,-62,6],[38,0.9,-78,9],[55,0.5,-45,4]].forEach(a=>{
  const b = rockBlob(a[3], Math.random()*10);
  b.position.set(a[0],a[1],a[2]); b.scale.y = 0.4;
  b.material = new THREE.MeshStandardMaterial({color:0x151030, roughness:1});
  scene.add(b);
});

/* ---------------- "just breathe" 信标 ---------------- */
const beacon = new THREE.Group();
const textTex = makeTextTexture(true);
const panel = new THREE.Mesh(new THREE.PlaneGeometry(5.6, 1.75),
  new THREE.MeshBasicMaterial({map:textTex, transparent:true, blending:THREE.AdditiveBlending,
    depthWrite:false, side:THREE.DoubleSide}));
panel.renderOrder = 3;
beacon.add(panel);
[[-0.78,1],[0.78,1]].forEach(a=>{
  const bar = new THREE.Mesh(new THREE.BoxGeometry(5.9, 0.05, 0.05), M.glowTeal);
  bar.position.y = a[0]; beacon.add(bar);
});
[[-3.0],[3.0]].forEach(a=>{
  const ball = new THREE.Mesh(new THREE.SphereGeometry(0.09, 12, 10), M.glowTeal);
  ball.position.set(a[0], 0.1, 0); beacon.add(ball);
  const rod = new THREE.Mesh(new THREE.CylinderGeometry(0.015, 0.015, 0.9, 6),
    new THREE.MeshBasicMaterial({color:0x9adfcc, transparent:true, opacity:0.5}));
  rod.position.set(a[0]*0.86, 0.55, 0);
  rod.rotation.z = a[0] > 0 ? -0.5 : 0.5;
  beacon.add(rod);
});
const floatLine = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.012, 1.5, 6),
  new THREE.MeshBasicMaterial({color:0x9adfcc, transparent:true, opacity:0.4}));
floatLine.position.y = -1.1; beacon.add(floatLine);
const bob = new THREE.Mesh(new THREE.SphereGeometry(0.13, 14, 12), M.glowTeal);
bob.position.y = -1.9; beacon.add(bob);
const beaconGlow = makeGlowSprite(0x7df0dc, 9, 3.2, 0.16); beacon.add(beaconGlow);
const beaconLight = new THREE.PointLight(0x7df0dc, 0.7, 12);
beaconLight.position.y = -0.5; beacon.add(beaconLight);
beacon.position.set(4, 2.6, 10);
scene.add(beacon);
const beaconRefl = makeGlowSprite(0x5fe8cc, 5.5, 1.1, 0.18); scene.add(beaconRefl);

/* 天空深处的那行小字 */
const skyTextMat = new THREE.MeshBasicMaterial({map:makeTextTexture(false), transparent:true,
  blending:THREE.AdditiveBlending, depthWrite:false, opacity:0.08, side:THREE.DoubleSide});
const skyText = new THREE.Mesh(new THREE.PlaneGeometry(26, 8.1), skyTextMat);
skyTextMat.map.repeat.set(0.86, 0.72); skyTextMat.map.offset.set(0.07, 0.14);
skyText.position.set(-26, 19, -92);
skyText.rotation.y = 0.28;
skyText.renderOrder = 2;
scene.add(skyText);

/* ---------------- 未来战斗机(白 · 绿 · 红)---------------- */
const jet = new THREE.Group();
{
  const body = new THREE.Mesh(new THREE.SphereGeometry(0.78, 28, 20), M.white);
  body.scale.set(0.85, 0.8, 2.3); body.position.z = 0.2;
  jet.add(body);
  const nose = new THREE.Mesh(new THREE.ConeGeometry(0.45, 1.5, 24), M.white);
  nose.geometry.rotateX(Math.PI/2); nose.position.z = 2.35;
  jet.add(nose);
  const tip = new THREE.Mesh(new THREE.ConeGeometry(0.2, 0.5, 20), M.red);
  tip.geometry.rotateX(Math.PI/2); tip.position.z = 3.05;
  jet.add(tip);
  const spine = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.06, 2.6), M.green);
  spine.position.set(0, 0.63, 0.1); jet.add(spine);
  const canopy = new THREE.Mesh(new THREE.SphereGeometry(0.4, 24, 18), M.glass);
  canopy.scale.set(0.7, 0.55, 1.15); canopy.position.set(0, 0.42, 1.0);
  jet.add(canopy);
  [[0.68],[-0.68]].forEach(a=>{
    const intake = new THREE.Mesh(new THREE.BoxGeometry(0.28, 0.3, 1.1), M.dark);
    intake.position.set(a[0], -0.12, 0.5); jet.add(intake);
  });
}
/* 机翼(含绿前缘 / 红翼尖),做成组便于镜像 */
function buildWing(){
  const w = new THREE.Group();
  const shape = new THREE.Shape();
  shape.moveTo(0.35, 0.8); shape.lineTo(3.6, -0.9); shape.lineTo(3.9, -1.5); shape.lineTo(0.5, -1.5);
  shape.closePath();
  const geo = new THREE.ExtrudeGeometry(shape, {depth:0.1, bevelEnabled:true, bevelThickness:0.03, bevelSize:0.03, bevelSegments:1});
  geo.rotateX(Math.PI/2);
  const mat = M.white.clone(); mat.side = THREE.DoubleSide;
  const mesh = new THREE.Mesh(geo, mat);
  mesh.castShadow = true; mesh.receiveShadow = true;
  w.add(mesh);
  const led = new THREE.Mesh(new THREE.BoxGeometry(3.4, 0.06, 0.2), M.green);
  led.position.set(1.97, 0.06, -0.05); led.rotation.y = 0.48;
  w.add(led);
  const tipRed = new THREE.Mesh(new THREE.BoxGeometry(0.55, 0.06, 0.75), M.red);
  tipRed.position.set(3.6, 0.06, -1.25); tipRed.rotation.y = 0.48;
  w.add(tipRed);
  return w;
}
const wingR = buildWing(); jet.add(wingR);
const wingL = buildWing(); wingL.scale.x = -1; jet.add(wingL);
/* V 形垂尾(红尖)*/
[[-0.45,-0.5],[0.45,0.5]].forEach(a=>{
  const fin = new THREE.Group();
  const shape = new THREE.Shape();
  shape.moveTo(0,0); shape.lineTo(-1.1,0.15); shape.lineTo(-1.35,0.9); shape.lineTo(-0.8,0.95);
  shape.closePath();
  const geo = new THREE.ExtrudeGeometry(shape, {depth:0.06, bevelEnabled:false});
  const mat = M.white.clone(); mat.side = THREE.DoubleSide;
  const m = new THREE.Mesh(geo, mat);
  m.castShadow = true;
  fin.add(m);
  const red = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.2, 0.075), M.red);
  red.position.set(-1.12, 0.85, 0.03);
  fin.add(red);
  fin.position.set(a[0], 0.32, -1.45);
  fin.rotation.z = a[1];
  jet.add(fin);
});
/* 引擎与尾焰 */
const flames = [];
[[-0.36],[0.36]].forEach(a=>{
  const nac = new THREE.Mesh(new THREE.CylinderGeometry(0.24, 0.28, 0.9, 18), M.dark);
  nac.geometry.rotateX(Math.PI/2); nac.position.set(a[0], -0.05, -2.15);
  jet.add(nac);
  const ring = new THREE.Mesh(new THREE.TorusGeometry(0.22, 0.05, 10, 24), M.dark);
  ring.position.set(a[0], -0.05, -2.6); jet.add(ring);
  const disc = new THREE.Mesh(new THREE.CircleGeometry(0.19, 20), M.glowTeal);
  disc.rotation.y = Math.PI; disc.position.set(a[0], -0.05, -2.58);
  jet.add(disc);
  const fIn = new THREE.Mesh(new THREE.ConeGeometry(0.17, 1.5, 14, 1, true), M.flameIn);
  fIn.geometry.rotateX(-Math.PI/2); fIn.position.set(a[0], -0.05, -3.3);
  const fOut = new THREE.Mesh(new THREE.ConeGeometry(0.24, 2.3, 14, 1, true), M.flameOut);
  fOut.geometry.rotateX(-Math.PI/2); fOut.position.set(a[0], -0.05, -3.6);
  jet.add(fIn); jet.add(fOut);
  flames.push(fIn, fOut);
  const gl = makeGlowSprite(0x7df0dc, 1.6, 1.6, 0.5);
  gl.position.set(a[0], -0.05, -2.8); jet.add(gl);
});
/* 反重力腹环 */
const hoverRing = new THREE.Mesh(new THREE.TorusGeometry(0.5, 0.045, 10, 36), M.glowTeal);
hoverRing.rotation.x = Math.PI/2; hoverRing.position.set(0, -0.62, 0.3);
jet.add(hoverRing);
const hoverDisc = new THREE.Mesh(new THREE.CircleGeometry(0.42, 24), M.glowTeal);
hoverDisc.rotation.x = Math.PI/2; hoverDisc.position.set(0, -0.6, 0.3);
jet.add(hoverDisc);
const flameLight = new THREE.PointLight(0x7df0dc, 1.1, 12);
flameLight.position.set(0, 0, -3.2); jet.add(flameLight);
/* 机翼上的两个落脚点 */
const wingPerchR = new THREE.Object3D(); wingPerchR.position.set(2.55, 0.18, 1.35); jet.add(wingPerchR);
const wingPerchL = new THREE.Object3D(); wingPerchL.position.set(-2.55, 0.18, 1.35); jet.add(wingPerchL);
scene.add(jet);
const jetRefl = makeGlowSprite(0x8ef0e0, 7, 1.4, 0.25); scene.add(jetRefl);

/* 战机航线:沿岸往返,端点自然减速形成悬停感 */
const jetA = V3(-22, 4.2, -20), jetB = V3(14, 3.6, -4);
const jetPeriod = 40;
function jetPosAt(t, out){
  const u = 0.5 - 0.5*Math.cos(t*Math.PI*2/jetPeriod);
  out.lerpVectors(jetA, jetB, u);
  out.y += Math.sin(t*1.1)*0.35;
  out.x += Math.sin(t*0.5)*0.8;
  out.z += Math.cos(t*0.7)*0.8;
  return out;
}

/* ---------------- 太空兔 ---------------- */
const bunny = new THREE.Group();
const head = new THREE.Group();
const earL = new THREE.Group(), earR = new THREE.Group();
const armL = new THREE.Group(), armR = new THREE.Group();
const legL = new THREE.Group(), legR = new THREE.Group();
let eyeL, eyeR, antennaGlow, helmetLamp;
{
  const body = new THREE.Mesh(new THREE.SphereGeometry(0.55, 26, 20), M.fur);
  body.scale.set(1, 1.02, 0.92); body.position.y = 0.62;
  bunny.add(body);
  const tail = new THREE.Mesh(new THREE.SphereGeometry(0.15, 14, 12), M.fur);
  tail.position.set(0, 0.72, -0.5);
  bunny.add(tail);
  /* 头 */
  head.position.set(0, 1.3, 0.1);
  const skull = new THREE.Mesh(new THREE.SphereGeometry(0.42, 26, 20), M.fur);
  head.add(skull);
  const nose = new THREE.Mesh(new THREE.SphereGeometry(0.05, 10, 8), M.pink);
  nose.position.set(0, -0.02, 0.42); head.add(nose);
  eyeL = new THREE.Mesh(new THREE.SphereGeometry(0.058, 12, 10), M.darkEye);
  eyeL.position.set(-0.16, 0.08, 0.37); head.add(eyeL);
  eyeR = eyeL.clone(); eyeR.position.x = 0.16; head.add(eyeR);
  const hl = new THREE.Mesh(new THREE.SphereGeometry(0.018, 8, 6),
    new THREE.MeshBasicMaterial({color:0xffffff}));
  hl.position.set(-0.14, 0.12, 0.42); head.add(hl);
  const hr = hl.clone(); hr.position.x = 0.18; head.add(hr);
  /* 微斜的"酷"眉 */
  [[-0.16, 0.1],[0.16, -0.1]].forEach(a=>{
    const brow = new THREE.Mesh(new THREE.BoxGeometry(0.11, 0.022, 0.02), M.darkEye);
    brow.position.set(a[0], 0.2, 0.38); brow.rotation.z = a[1];
    head.add(brow);
  });
  /* 腮红 */
  [[-0.27],[0.27]].forEach(a=>{
    const ch = new THREE.Mesh(new THREE.SphereGeometry(0.055, 10, 8), M.pink);
    ch.scale.set(1, 0.7, 0.4); ch.position.set(a[0], -0.06, 0.3);
    ch.material = M.pink.clone(); ch.material.transparent = true; ch.material.opacity = 0.6;
    head.add(ch);
  });
  /* 长耳朵(带粉色内耳)*/
  [[earL, -0.16],[earR, 0.16]].forEach(a=>{
    const g = a[0];
    g.position.set(a[1], 0.3, -0.02);
    const outer = new THREE.Mesh(new THREE.SphereGeometry(0.28, 16, 14), M.fur);
    outer.scale.set(0.5, 1.6, 0.34); outer.position.y = 0.42;
    outer.castShadow = true;
    g.add(outer);
    const inner = new THREE.Mesh(new THREE.SphereGeometry(0.26, 14, 12), M.pink);
    inner.scale.set(0.3, 1.25, 0.1); inner.position.set(0, 0.42, 0.085);
    g.add(inner);
    head.add(g);
  });
  /* 透明宇航头盔 */
  const glass = new THREE.Mesh(new THREE.SphereGeometry(0.53, 28, 22),
    new THREE.MeshPhongMaterial({color:0xaaccdd, transparent:true, opacity:0.14,
      shininess:120, specular:0xffffff, depthWrite:false}));
  glass.renderOrder = 5;
  head.add(glass);
  const ring = new THREE.Mesh(new THREE.TorusGeometry(0.53, 0.028, 8, 40),
    new THREE.MeshStandardMaterial({color:0xe8ecef, metalness:0.8, roughness:0.25}));
  ring.rotation.x = Math.PI/2; ring.position.y = -0.02;
  head.add(ring);
  helmetLamp = new THREE.Mesh(new THREE.SphereGeometry(0.045, 10, 8), M.glowTeal);
  helmetLamp.position.set(0.34, 0.42, 0.12); head.add(helmetLamp);
  const lampGlow = makeGlowSprite(0x8ef2dd, 0.5, 0.5, 0.6);
  lampGlow.position.copy(helmetLamp.position); head.add(lampGlow);
  bunny.add(head);
  /* 手手脚脚 */
  [[armL, -0.45],[armR, 0.45]].forEach(a=>{
    const arm = new THREE.Mesh(new THREE.SphereGeometry(0.1, 12, 10), M.fur);
    arm.scale.set(0.8, 1.5, 0.8); arm.position.y = -0.12;
    a[0].add(arm);
    a[0].position.set(a[1], 0.85, 0.05);
    bunny.add(a[0]);
  });
  [[legL, -0.2],[legR, 0.2]].forEach(a=>{
    const leg = new THREE.Mesh(new THREE.SphereGeometry(0.13, 12, 10), M.fur);
    leg.scale.set(0.85, 1.1, 0.9);
    a[0].add(leg);
    a[0].position.set(a[1], 0.16, 0.02);
    bunny.add(a[0]);
  });
  /* 迷你喷气背包(白 / 绿 + 青焰口 + 闪烁天线)*/
  const pack = new THREE.Group();
  const pBody = new THREE.Mesh(new THREE.BoxGeometry(0.34, 0.4, 0.18), M.white);
  pack.add(pBody);
  const pCap = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.13, 0.02), M.green);
  pCap.position.set(0, 0.08, -0.1); pack.add(pCap);
  [[-0.1],[0.1]].forEach(a=>{
    const noz = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.065, 0.14, 10), M.dark);
    noz.position.set(a[0], -0.26, -0.02); noz.rotation.x = 0.35;
    pack.add(noz);
    const fl = makeGlowSprite(0x8ef2dd, 0.3, 0.5, 0.5);
    fl.position.set(a[0], -0.38, 0.05); pack.add(fl);
  });
  const ant = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.012, 0.5, 6), M.dark);
  ant.position.set(0.16, 0.42, 0); pack.add(ant);
  const bulb = new THREE.Mesh(new THREE.SphereGeometry(0.032, 10, 8), M.glowTeal);
  bulb.position.set(0.16, 0.68, 0); pack.add(bulb);
  antennaGlow = makeGlowSprite(0x8ef2dd, 0.55, 0.55, 0.7);
  antennaGlow.position.set(0.16, 0.68, 0); pack.add(antennaGlow);
  pack.position.set(0, 0.88, -0.44);
  bunny.add(pack);
  /* 胸前小面板 */
  const chest = new THREE.Mesh(new THREE.BoxGeometry(0.16, 0.1, 0.03), M.dark);
  chest.position.set(0, 0.78, 0.48); bunny.add(chest);
  const dot1 = new THREE.Mesh(new THREE.SphereGeometry(0.018, 8, 6), M.glowTeal);
  dot1.position.set(-0.04, 0.78, 0.5); bunny.add(dot1);
  const dot2 = new THREE.Mesh(new THREE.SphereGeometry(0.018, 8, 6), M.red);
  dot2.material = M.red; dot2.position.set(0.04, 0.78, 0.5); bunny.add(dot2);
  /* 兔子的暖光 */
  const lamp = new THREE.PointLight(0xffe2c4, 0.75, 7);
  lamp.position.set(0, 2.1, 0.6);
  bunny.add(lamp);
  bunny.traverse(o=>{ if(o.isMesh && o.material !== glass.material) o.castShadow = true; });
}
scene.add(bunny);

/* 落点(perch)登记 */
const perches = {
  pad:   Object.assign(pad.userData.perch, {isWing:false, yawOff:0}),
  rock:  Object.assign(rockPerch,            {isWing:false, yawOff:0}),
  wingR: Object.assign(wingPerchR,           {isWing:true,  yawOff:-0.45}),
  wingL: Object.assign(wingPerchL,           {isWing:true,  yawOff: 0.45})
};
const route = ['pad', 'wingR', 'rock', 'wingL'];

/* ---------------- 兔子状态机 ---------------- */
const bs = {
  perch: perches.pad, idx: 1,
  mode: 'idle', t: 0, idleDur: 2.4,
  hopFrom: new THREE.Vector3(), hopT: 0, hopDur: 1, hopH: 1.2,
  squash: 1, squashV: 0,
  yaw: 0, vy: 0, prevY: 0,
  earAng: 0, lift: 0,
  headTimer: 2, headYawT: 0, blinkT: 2.5, blinkPhase: 0
};
const tmpA = new THREE.Vector3(), tmpB = new THREE.Vector3(), tmpC = new THREE.Vector3();
const MAX_HOP_DIST = 18;

function perchIsWing(){ return bs.perch.isWing; }

function updateBunny(dt, t, jetRoll){
  if(bs.mode === 'idle'){
    bs.t += dt;
    bs.perch.getWorldPosition(tmpA);
    bunny.position.copy(tmpA);
    /* 若下一站太远(战机还没掠过来),兔子就多等一会儿,顺便张望 */
    const target = perches[route[bs.idx]];
    target.getWorldPosition(tmpB);
    const dist = tmpA.distanceTo(tmpB);
    if(bs.t > bs.idleDur){
      if(dist < MAX_HOP_DIST){
        /* —— 起跳 —— */
        bs.mode = 'hop'; bs.hopT = 0;
        bs.hopFrom.copy(tmpA);
        bs.hopDur = clamp(dist*0.16, 0.7, 1.9);
        bs.hopH = clamp(dist*0.24, 0.9, 2.6);
        bs.squash = 1.16; bs.squashV = 0;
      }else{
        bs.idleDur = bs.t + 1.0;
      }
    }
  }else{
    /* —— 滞空 —— */
    bs.hopT += dt;
    const k = Math.min(bs.hopT/bs.hopDur, 1);
    const ke = k*k*(3-2*k);
    const target = perches[route[bs.idx]];
    target.getWorldPosition(tmpB);
    tmpC.lerpVectors(bs.hopFrom, tmpB, ke);
    tmpC.y += Math.sin(Math.PI*k)*bs.hopH;
    bunny.position.copy(tmpC);
    /* 朝向与俯仰 */
    const dir = tmpA.subVectors(tmpB, bs.hopFrom);
    bs.yaw = lerpAngle(bs.yaw, Math.atan2(dir.x, dir.z), Math.min(8*dt, 1));
    bunny.rotation.y = bs.yaw;
    bunny.rotation.x = clamp(-bs.vy*0.03, -0.3, 0.3);
    if(k >= 1){
      /* —— 落地 —— */
      bs.mode = 'idle'; bs.t = 0;
      bs.perch = target;
      bs.idx = (bs.idx+1) % route.length;
      bs.idleDur = perchIsWing() ? rand(6, 11) : rand(1.6, 3.0);
      bs.squash = 0.7; bs.squashV = 0;
      bunny.rotation.x = 0;
      spawnRipple(bunny.position);
    }
  }
  /* 站立时的朝向(等战机时望向战机,平时望向镜头方向)*/
  if(bs.mode === 'idle'){
    let want;
    const nextTarget = perches[route[bs.idx]];
    nextTarget.getWorldPosition(tmpB);
    const dist = bunny.position.distanceTo(tmpB);
    const toCam = Math.atan2(camera.position.x - bunny.position.x, camera.position.z - bunny.position.z);
    if(nextTarget.isWing && dist > 10){
      const toJet = Math.atan2(jet.position.x - bunny.position.x, jet.position.z - bunny.position.z);
      want = toJet*0.65 + toCam*0.35 + Math.sin(t*0.6)*0.1;
    }else if(perchIsWing()){
      want = jet.rotation.y + bs.perch.yawOff;
    }else{
      want = toCam*0.3 + Math.sin(t*0.35)*0.5;
    }
    bs.yaw = lerpAngle(bs.yaw, want, Math.min(2.5*dt, 1));
    bunny.rotation.y = bs.yaw;
    /* 站在翼上时随 roll 微微平衡 */
    const wantZ = perchIsWing() ? -jetRoll*0.55 : 0;
    bunny.rotation.z += (wantZ - bunny.rotation.z)*Math.min(3*dt, 1);
  }
  /* 落点高度对齐(战机移动由 perch 世界坐标自动带出)*/
  /* 弹性(squash & stretch)*/
  bs.vy = (bunny.position.y - bs.prevY)/Math.max(dt, 1e-4);
  bs.prevY = bunny.position.y;
  bs.squashV += (1 - bs.squash)*60*dt;
  bs.squashV *= Math.exp(-9*dt);
  bs.squash += bs.squashV*dt;
  const air = bs.mode === 'hop' ? 1 + clamp(bs.vy*0.045, -0.12, 0.22) : 1;
  const sy = bs.squash*air, sxz = 1/Math.sqrt(sy);
  bunny.scale.set(sxz, sy, sxz);
  /* 耳朵弹簧 */
  const earT = Math.sin(t*2.4 + bs.yaw)*0.10 + (bs.mode==='hop' ? clamp(-bs.vy*0.09, -0.55, 0.55) : 0);
  bs.earAng += (earT - bs.earAng)*Math.min(9*dt, 1);
  earL.rotation.z =  0.16 + bs.earAng;
  earR.rotation.z = -0.16 - bs.earAng;
  earL.rotation.x = earR.rotation.x = -bs.earAng*0.7;
  /* 手脚的腾空收展 */
  const liftT = bs.mode === 'hop' ? Math.sin(Math.PI*Math.min(bs.hopT/bs.hopDur,1)) : 0;
  bs.lift += (liftT - bs.lift)*Math.min(10*dt, 1);
  armL.rotation.z =  0.3 + bs.lift*1.15;
  armR.rotation.z = -0.3 - bs.lift*1.15;
  legL.position.y = legR.position.y = 0.16 - bs.lift*0.07;
  legL.scale.y = legR.scale.y = 1 - bs.lift*0.35;
  /* 头部:偶尔看向镜头 */
  bs.headTimer -= dt;
  if(bs.headTimer < 0){
    bs.headTimer = rand(2.5, 6);
    if(Math.random() < 0.55){
      const camYaw = Math.atan2(camera.position.x - bunny.position.x, camera.position.z - bunny.position.z);
      bs.headYawT = clamp(((camYaw - bs.yaw + Math.PI*3) % (Math.PI*2)) - Math.PI, -0.7, 0.7);
    }else bs.headYawT = rand(-0.5, 0.5);
  }
  head.rotation.y += (bs.headYawT - head.rotation.y)*Math.min(4*dt, 1);
  head.rotation.x = Math.sin(t*1.3)*0.04;
  /* 眨眼 */
  bs.blinkT -= dt;
  if(bs.blinkT < 0){ bs.blinkT = rand(2, 5); bs.blinkPhase = 0.15; }
  if(bs.blinkPhase > 0) bs.blinkPhase -= dt;
  const es = bs.blinkPhase > 0 ? 0.12 : 1;
  eyeL.scale.y = eyeR.scale.y = es;
  /* 信号灯 */
  antennaGlow.material.opacity = 0.35 + 0.45*(0.5 + 0.5*Math.sin(t*4.5));
  helmetLamp.scale.setScalar(1 + 0.15*Math.sin(t*4.5));
}

/* ---------------- 落地涟漪(对象池) ---------------- */
const ripples = [];
for(let i=0;i<4;i++){
  const m = new THREE.Mesh(new THREE.RingGeometry(0.82, 1, 48),
    new THREE.MeshBasicMaterial({color:0xaef5e6, transparent:true, opacity:0,
      blending:THREE.AdditiveBlending, depthWrite:false, side:THREE.DoubleSide}));
  m.rotation.x = -Math.PI/2; m.visible = false; m.renderOrder = 4;
  scene.add(m);
  ripples.push({mesh:m, life:-1});
}
function spawnRipple(pos){
  const r = ripples.find(r=>r.life < 0) || ripples[0];
  r.life = 0;
  r.mesh.visible = true;
  r.mesh.position.set(pos.x, pos.y + 0.06, pos.z);
}
function updateRipples(dt){
  ripples.forEach(r=>{
    if(r.life < 0) return;
    r.life += dt;
    const k = r.life/0.9;
    if(k >= 1){ r.life = -1; r.mesh.visible = false; return; }
    r.mesh.scale.setScalar(0.35 + k*2.4);
    r.mesh.material.opacity = (1-k)*0.55;
  });
}

/* ---------------- 漂浮微尘 ---------------- */
function makeDust(count, color, size, opacity, range){
  const geo = new THREE.BufferGeometry();
  const arr = new Float32Array(count*3);
  const meta = [];
  for(let i=0;i<count;i++){
    arr[i*3]   = rand(-range, range);
    arr[i*3+1] = rand(0, 24);
    arr[i*3+2] = rand(-range*1.4, range*0.7);
    meta.push({b:arr[i*3+1], s:rand(0.15,0.5), p:rand(0,20)});
  }
  geo.setAttribute('position', new THREE.BufferAttribute(arr, 3));
  const mat = new THREE.PointsMaterial({color:color, size:size, map:softGlow, transparent:true,
    opacity:opacity, blending:THREE.AdditiveBlending, depthWrite:false});
  const pts = new THREE.Points(geo, mat);
  scene.add(pts);
  return {pts, meta, arr};
}
const dustA = makeDust(260, 0xcbb6ff, 0.5, 0.5, 55);
const dustB = makeDust(70, 0x8ef2dd, 0.3, 0.7, 40);
function updateDust(d, t){
  for(let i=0;i<d.meta.length;i++){
    const m = d.meta[i];
    d.arr[i*3+1] = 1 + ((m.b + t*m.s*0.6) % 23);
  }
  d.pts.geometry.attributes.position.needsUpdate = true;
  d.pts.material.opacity = 0.5*(0.65 + 0.35*Math.sin(t*0.7));
}

/* ---------------- 镜头导演 ---------------- */
const shotNames = ['入梦', '低语海面', '翼上同行', '掠海', '浮台特写'];
const shots = [
  { dur:15, fov:[50,43],
    update(t, T, o){
      o.pos.set(-28 + t*0.38, 9.5 - t*0.12, 34 - t*0.55);
      o.look.set(2, 3.2, -8);
    }},
  { dur:11, fov:[40,40],
    update(t, T, o){
      pad.getWorldPosition(tmpA);
      o.pos.set(tmpA.x - 4.4 + Math.sin(t*0.3)*0.5, tmpA.y + 1.4 + Math.sin(t*0.45)*0.2, tmpA.z + 4.6);
      o.look.copy(bunny.position).y += 0.5;
    }},
  { dur:12, fov:[44,44],
    update(t, T, o){
      const a = t*0.07 + 2.5;
      o.pos.set(jet.position.x + Math.sin(a)*7.5, jet.position.y + 1.7 + Math.sin(t*0.4)*0.3, jet.position.z + Math.cos(a)*7.5);
      if(perchIsWing() || bs.mode==='hop') o.look.copy(bunny.position);
      else o.look.copy(jet.position);
    }},
  { dur:10, fov:[36,33],
    update(t, T, o){
      o.pos.set(12 + Math.sin(t*0.2)*1.5, 1.7, 17 - t*0.25);
      o.look.copy(jet.position); o.look.y += 0.4;
    }},
  { dur:11, fov:[40,40],
    update(t, T, o){
      const a = t*0.10 + 0.8;
      o.pos.set(bunny.position.x + Math.sin(a)*3.6, bunny.position.y + 0.85 + Math.sin(t*0.5)*0.15, bunny.position.z + Math.cos(a)*3.6);
      o.look.copy(bunny.position); o.look.y += 0.55;
    }}
];
let shotIdx = 0, shotT = 0, camFov = 50, shotCooldown = 0;
const camPos = V3(-28, 9.5, 34), camLook = V3(2, 3.2, -8);
const desPos = new THREE.Vector3(), desLook = new THREE.Vector3();
let mouseNX = 0, mouseNY = 0;
const _right = new THREE.Vector3(), _up = new THREE.Vector3(0,1,0);

const shotEl = document.getElementById('shotname');
function showShotName(){
  shotEl.textContent = shotNames[shotIdx];
  shotEl.classList.remove('show');
  void shotEl.offsetWidth;
  shotEl.classList.add('show');
}
function nextShot(){
  if(shotCooldown > 0) return;
  shotCooldown = 1.2;
  shotIdx = (shotIdx + 1) % shots.length;
  shotT = 0;
  showShotName();
}
showShotName();

function updateCamera(dt, t){
  shotCooldown = Math.max(0, shotCooldown - dt);
  shotT += dt;
  const s = shots[shotIdx];
  if(shotT > s.dur) nextShot();
  const tt = Math.min(shotT, s.dur);
  s.update(tt, s.dur, {pos:desPos, look:desLook});
  /* 呼吸漂移 */
  desPos.x += Math.cos(t*0.3)*0.3; desPos.y += Math.sin(t*0.42)*0.25;
  desLook.y += Math.sin(t*0.35)*0.08;
  /* 阻尼追踪(镜头切换时自然滑移)*/
  camPos.lerp(desPos, 1 - Math.exp(-1.6*dt));
  camLook.lerp(desLook, 1 - Math.exp(-2.2*dt));
  camFov += (s.fov[0] + (s.fov[1]-s.fov[0])*(tt/s.dur) - camFov)*(1 - Math.exp(-1.8*dt));
  camera.fov = camFov;
  camera.updateProjectionMatrix();
  camera.position.copy(camPos);
  camera.lookAt(camLook);
  /* 鼠标视差 + 极轻微手持呼吸 */
  _right.setFromMatrixColumn(camera.matrix, 0);
  camera.position.addScaledVector(_right, mouseNX*0.5).addScaledVector(_up, -mouseNY*0.3);
  camera.rotation.z += Math.sin(t*0.23)*0.006 + mouseNX*0.01;
}

/* ---------------- 交互 ---------------- */
addEventListener('pointermove', e=>{
  mouseNX = (e.clientX/innerWidth)*2 - 1;
  mouseNY = (e.clientY/innerHeight)*2 - 1;
});
addEventListener('pointerdown', nextShot);
addEventListener('resize', ()=>{
  camera.aspect = innerWidth/innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});

/* ---------------- 主循环 ---------------- */
let last = performance.now(), T = 0;
const prevJetPos = jetPosAt(0, new THREE.Vector3());
let jetYaw = 0, jetRoll = 0, jetSpeed = 0;
const jetFuture = new THREE.Vector3(), jetCur = new THREE.Vector3();

function tick(){
  requestAnimationFrame(tick);
  const now = performance.now();
  const dt = Math.min((now - last)/1000, 0.05);
  last = now; T += dt;
  const t = T;

  skyMat.uniforms.uTime.value = t;
  seaMat.uniforms.uTime.value = t;

  /* 云 */
  clouds.forEach(c=>{
    c.position.x += c.userData.drift*dt;
    if(c.position.x > 95) c.position.x = -95;
    c.position.y = c.userData.baseY + Math.sin(t*0.24 + c.userData.ph)*0.6;
  });

  /* 浮台呼吸 */
  pad.position.y = 0.9 + Math.sin(t*0.8)*0.12;
  pad.userData.rim.scale.setScalar(1 + 0.04*Math.sin(t*1.6));
  pad.userData.halo.material.opacity = 0.18 + 0.08*Math.sin(t*1.6);
  rock.position.y = 1.0 + Math.sin(t*0.6 + 2)*0.09;

  /* 信标 */
  beacon.position.y = 2.6 + Math.sin(t*0.6)*0.18;
  beacon.rotation.y = Math.sin(t*0.23)*0.3;
  beaconGlow.material.opacity = 0.12 + 0.07*(0.5 + 0.5*Math.sin(t*0.9));
  beaconLight.intensity = 0.55 + 0.25*(0.5 + 0.5*Math.sin(t*0.9));
  beaconRefl.position.set(beacon.position.x, 0.4, beacon.position.z);
  beaconRefl.material.opacity = 0.13 + 0.06*Math.sin(t*0.9);
  skyTextMat.opacity = 0.05 + 0.06*(0.5 + 0.5*Math.sin(t*0.4));

  /* 战机 */
  jetPosAt(t, jetCur);
  jetPosAt(t + 0.35, jetFuture);
  jet.position.copy(jetCur);
  jet.lookAt(jetFuture);
  jetSpeed = jetCur.distanceTo(prevJetPos)/Math.max(dt, 1e-4);
  prevJetPos.copy(jetCur);
  const targetYaw = Math.atan2(jetFuture.x - jetCur.x, jetFuture.z - jetCur.z);
  let yawDelta = ((targetYaw - jetYaw + Math.PI*3) % (Math.PI*2)) - Math.PI;
  jetRoll += (clamp(yawDelta*6, -0.55, 0.55) + Math.sin(t*0.9)*0.05 - jetRoll)*Math.min(3*dt, 1);
  jet.rotateZ(-jetRoll);
  jetYaw = targetYaw;
  const flameLen = 0.75 + jetSpeed*0.09 + Math.sin(t*30)*0.1;
  flames.forEach((f,i)=>{ f.scale.set(1, (i%2 ? 1.15 : 1)*flameLen, 1); });
  flameLight.intensity = 0.9 + jetSpeed*0.06 + Math.sin(t*25)*0.15;
  hoverRing.scale.setScalar(1 + 0.06*Math.sin(t*2.2));

  /* 兔子 */
  updateBunny(dt, t, jetRoll);

  /* 倒影光斑 */
  jetRefl.position.set(jet.position.x, 0.35, jet.position.z);
  jetRefl.material.opacity = clamp(0.4 - jet.position.y*0.045, 0.08, 0.35)*(0.8 + 0.2*Math.sin(t*3));
  jetRefl.scale.set(5 + jetSpeed*0.4, 1.3, 1);
  if(bs.mode !== 'hop' || bs.hopT > 0.2){
    /* 落点处的水面微光跟随兔子 */
  }

  /* 粒子与涟漪 */
  updateDust(dustA, t);
  updateDust(dustB, t*1.2);
  updateRipples(dt);

  /* 镜头 */
  updateCamera(dt, t);

  renderer.render(scene, camera);
}
tick();
</script>
</body>
</html>

实现说明(要点):

  • 角色:太空兔由纯几何体拼装——Q 版头身比、长耳带粉色内耳、斜眉与腮红给性格,外面罩一层低透明度宇航头盔(金属环 + 顶部信号灯),背上是白绿配色迷你喷气背包 + 闪烁天线。耳朵、手臂、squash & stretch 全部用弹簧插值驱动:起跳蹬伸、滞空拉伸、落地压扁回弹,耳朵随垂直速度后掠;它还会眨眼、转头看镜头、在浮台上等战机掠过时张望——这些小动作是"性格"的来源。
  • 叙事循环:兔子在 浮台 → 右翼 → 礁石 → 左翼 之间循环蹦跳。落点是实时追踪的世界坐标,所以跳上机翼后会随机枪一起掠海巡航一段再跳回岸边;目标太远时它会原地等待,跳跃距离自适应时长与弧高,落地有扩散涟漪。
  • 战机:白 / 绿 / 红涂装(绿背脊与翼前缘、红鼻锥翼尖垂尾尖),双发尾焰随航速伸缩,沿一条余弦往返航线沿岸巡飞,端点自然减速成悬停,转弯带横滚,机腹有反重力光环。
  • 氛围:天空、海面均为自定义 shader(渐变 + 星野 + 大月亮银河带;海面有菲涅耳天空反射、月光高光带、碎金闪点与手动雾),积云为噪声变形球 + 半透明外壳的伪体积云,全部发光体用加性光晕模拟辉光,避免后处理依赖。
  • "just breathe":canvas 手工绘制发光字(逐字符字距 + 多层柔光),做成立在海上、系在浮标上的"LUNA-7 信标",缓缓摇摆呼吸;远处天空还有一行明灭的同款小字。
  • 镜头:五组电影机位(入梦 / 低语海面 / 窗上同行 / 掠海 / 浮台特写)自动轮换,位置、注视点、FOV 全部阻尼插值,叠加呼吸漂移、极轻微手持感与鼠标视差;点击画面可切换机位。

直接保存为 .html 双击打开即可运行,仅依赖一个 Three.js CDN。

SVG from model output 1
0.77s
TTFT
Actual wait 1.05s
38
Reasoning TPS
839s · 32,142 tok
73
Output TPS
234s · 17,144 tok
49,286
Total Tokens (official)
Input 500
Timing details
Actual wait (client)
1.05s
Client preparation
0.04s
Proxy preparation
0.00s
Upstream response headers
0.77s
Upstream first byte
0.77s
Upstream first token
0.77s
Upstream retries
0

Actual wait uses the client clock. Upstream timings start after proxy preparation and are cumulative; do not add these values. Upstream delay includes network, scheduling, processing and buffering; it does not prove provider queueing.

Diagnostic receipt
{
  "attempts": 1,
  "startedAt": "2026-09-29T03:43:55.804Z",
  "transport": "railway",
  "ttftSource": "proxy",
  "clientTtftMs": 1048.6999998092651,
  "completionId": "2026092911435594adafc27dce451f",
  "streamResumes": 23,
  "proxyPrepareMs": 0,
  "proxyRequestId": "wToovwptSyiO9BVVlt7tkg",
  "upstreamStatus": 200,
  "upstreamTtftMs": 773,
  "clientHeadersMs": 251.5,
  "clientPrepareMs": 45,
  "upstreamHeadersMs": 773,
  "upstreamFirstByteMs": 773,
  "clientFirstContentMs": 840194.2999997139
}
Elapsed 1074s · Average 46 tok/s · Peak 132 tok/s
小米 MiMo-V2.6 Flash
mimo-v2.6-flash
Done

Historical result · 9/29/2026, 04:01:50

The user wants a single-file HTML Web3D piece featuring a "Space Bunny" in a dreamy space coast scene. Let me build this with Three.js from CDN. Let me plan the scene: 1. **Sky**: Deep blue to purple gradient — use a custom shader on a large sphere or use a gradient background shader. 2. **Purple volumetric clouds**: Use sprite-based or mesh-based clouds with soft materials, multiple layers for depth. 3. **Reflective sea**: Use a plane with a custom shader or MeshStandardMaterial with high metalness, or use a simple reflection approach with a cube camera or planar reflection. For simplicity and performance, I'll use a custom water shader with normals and specular highlights, or use THREE.Reflector. 4. **Fighter jet**: White, green, red painted futuristic fighter — build from primitive geometries with good material colors. 5. **Space Bunny**: A cute bunny character with long ears, round body, some futuristic gear (helmet, small backpack, maybe a visor). 6. **Bouncing animation**: Hop cycle with squash/stretch, ear wobble, body elasticity. 7. **Camera**: Slow push-in, slight orbit, low angle observation, cinematic. 8. **"just breathe" text**: Glowing 3D text floating in the scene. Let me structure the code: - Use Three.js r158+ from CDN (unpkg or cdnjs) - OrbitControls for optional interaction - Post-processing for bloom (UnrealBloomPass) to make things glow Actually, for a single file that runs directly, I should use ES modules with importmap for Three.js. Let me think about the bunny design: - Body: sphere (slightly scaled) - Head: smaller sphere - Ears: two elongated shapes (scaled spheres or capsules) with inner ear detail - Eyes: dark spheres with highlights - Nose: small pink triangle/sphere - Helmet/visor: a semi-transparent dome over the head - Space suit details: maybe a collar ring, small backpack (jetpack) - Feet: small rounded shapes For the bunny animation: - Hopping: vertical position with parabolic arcs, squash on landing, stretch in air - Ear rotation: lag behind body motion (secondary animation) - Body bounce: subtle secondary motion For the fighter jet: - Fuselage: elongated shape - Wings: swept back delta wings - Canopy: bubble shape - Engine: glowing exhaust - Paint scheme: white body, green accents, red accents For clouds: - Multiple soft blob shapes (groups of spheres with transparent material) - Purple/violet tint- Slowly drifting For the sea: - A large plane with animated wave normals - Reflection of sky (using shader that blends with sky color) - Specular highlights for moon/light reflection For "just breathe": - Use TextGeometry with a loaded font, or create it as a canvas texture on a plane, or use simple geometry letters. Since loading fonts requires external resources, I'll use a canvas texture approach or create text with extruded shapes. Actually, for the glowing text, I could: 1. Use a canvas texture with text on a plane (with emissive material + bloom) 2. Use troika-three-text library3. Create simple 3D text using THREE.TextGeometry (needs font loader) The simplest approach that looks great: Create text on a canvas, apply it as texture to a plane with transparent material and emissive glow, place it in the scene. With bloom post-processing, it will glow beautifully. Let me also add: - Star field (points) - Particles floating (like space dust) - Moon or light source Let me write the code now. Actually, let me think about the structure more carefully: ```html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Space Bunny</title> <style> /* full screen canvas, loading overlay */ </style> </head> <body> <div id="info">Space Bunny — just breathe</div> <canvas id="canvas"></canvas> <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'; import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; // ... scene setup, object creation, animation loop </script> </body> </html> ``` Let me flesh this out completely now. For the water/sea reflection, I'll create a custom shader that: - Has animated normal maps (procedural) - Reflects the sky gradient - Has specular highlights from a light source - ShimmersActually, a simpler and effective approach: use MeshStandardMaterial with: - High metalness for reflections - Low roughness for sharp reflections - An environment map (generated from the sky) - Animated normal-like displacement Or even better, use a custom ShaderMaterial for the water that combines a sky reflection gradient with animated wave patterns. Let me go with a custom water shader approach for the best visual effect. For the sky, I'll create a gradient using a ShaderMaterial on a large sphere (inverted). For clouds, I'll create groups of spheres with a custom material that gives them a volumetric feel - using MeshStandardMaterial with transparency and some emissive purple tinting. Let me code this up: ```javascript // Sky shader const skyVertexShader = ` varying vec3 vWorldPosition; void main() { vec4 worldPosition = modelMatrix * vec4(position, 1.0); vWorldPosition = worldPosition.xyz; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; const skyFragmentShader = ` uniform vec3 topColor; uniform vec3 bottomColor; uniform float exponent; varying vec3 vWorldPosition; void main() { float h = normalize(vWorldPosition).y; float t = max(pow(max(h, 0.0), exponent), 0.0); gl_FragColor = vec4(mix(bottomColor, topColor, t), 1.0); } `; ``` For the water shader, something like: ```javascript const waterVertexShader = ` varying vec2 vUv; varying vec3 vWorldPosition; varying vec3 vNormal; void main() { vUv = uv; vec4 worldPosition = modelMatrix * vec4(position, 1.0); vWorldPosition = worldPosition.xyz; vNormal = normalize(normalMatrix * normal); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; const waterFragmentShader = ` uniform float time; uniform vec3 skyTopColor; uniform vec3 skyBottomColor; uniform vec3 lightDir; uniform vec3 lightColor; varying vec2 vUv; varying vec3 vWorldPosition; varying vec3 vNormal; // Simple noise function // ... void main() { // Animated wave pattern // Sky reflection based on view angle // Specular highlights // Fresnel effect } `; ``` This is getting complex. Let me simplify but still make it look good. Actually, for the sea, let me use a combination approach: 1. A base plane with a custom shader that creates wave-like patterns 2. Reflection of sky color using Fresnel 3. Specular highlights for the "reflection" look 4. Some particle effects on the surface Let me write more practical code now. For the fighter jet, let me design it: ```javascript function createFighterJet() { const jet = new THREE.Group(); // Fuselage - elongated shape const fuselageGeom = new THREE.CylinderGeometry(0.3, 0.5, 6, 8); fuselageGeom.rotateZ(Math.PI / 2); // Align along Z or X // Actually, let's make it more sleek // Nose cone // Wings // Canopy // Tail // Engine exhaust (glowing) return jet; } ``` Let me think about scale. If the bunny is about 1 unit tall, the jet should be maybe 5-8 units long. The sea should be hug
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Space Bunny — just breathe</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#060620}
canvas{display:block}
#loader{position:fixed;inset:0;z-index:200;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#060620;color:#a78bfa;font-family:Georgia,serif;transition:opacity 1.5s ease}
#loader.done{opacity:0;pointer-events:none}
#loader .txt{font-size:15px;letter-spacing:6px;animation:pl 2s ease-in-out infinite}
@keyframes pl{0%,100%{opacity:.3}50%{opacity:1}}
#hud{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);color:rgba(167,139,250,.25);font-size:10px;letter-spacing:3px;font-family:Georgia,serif;pointer-events:none;z-index:50}
</style>
</head>
<body>
<div id="loader"><div class="txt">space bunny is dreaming…</div></div>
<div id="hud">drag to orbit · scroll to zoom</div>
<canvas id="c"></canvas>

<script>
window.addEventListener('error',function(e){
    var l=document.getElementById('loader');
    if(l&&!l.classList.contains('done')){
        var t=l.querySelector('.txt');
        if(t)t.textContent='WebGL not supported :(';
    }
});
</script>

<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">
/* ══════════════════════════════════════════════════════════════════
   SPACE BUNNY — "just breathe"
   A dreamy Web3D scene: a cute astronaut bunny hops over a shimmering
   sea beneath purple clouds, alongside a white / green / red fighter jet.
   ══════════════════════════════════════════════════════════════════ */
import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js';
import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
import {UnrealBloomPass} from 'three/addons/postprocessing/UnrealBloomPass.js';
import {ShaderPass} from 'three/addons/postprocessing/ShaderPass.js';

/* ─── RENDERER / SCENE / CAMERA ────────────────────────────────── */
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0c0630, 0.005);

const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 500);
camera.position.set(30, 18, 40);

const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.target.set(0, 4, -3);
controls.minDistance = 8;
controls.maxDistance = 60;
controls.maxPolarAngle = Math.PI * 0.47;
controls.enabled = false;

let userInteracting = false;
canvas.addEventListener('pointerdown', () => {
    userInteracting = true;
    controls.enabled = true;
});

/* ─── POST-PROCESSING ──────────────────────────────────────────── */
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloom = new UnrealBloomPass(new THREE.Vector2(innerWidth, innerHeight), 0.7, 0.5, 0.82);
composer.addPass(bloom);
composer.addPass(new ShaderPass({
    uniforms: { tDiffuse: { value: null }, strength: { value: 0.45 } },
    vertexShader: `varying vec2 vUv;void main(){vUv=uv;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.);}`,
    fragmentShader: `uniform sampler2D tDiffuse;uniform float strength;varying vec2 vUv;
        void main(){vec4 c=texture2D(tDiffuse,vUv);float d=distance(vUv,vec2(.5));
        c.rgb*=1.-smoothstep(.35,.75,d)*strength;gl_FragColor=c;}`
}));

/* ─── LIGHTS ───────────────────────────────────────────────────── */
scene.add(new THREE.HemisphereLight(0x4433aa, 0x0a0a2e, 0.7));

const moonLight = new THREE.DirectionalLight(0xccddff, 1.2);
moonLight.position.set(30, 50, -40);
moonLight.castShadow = true;
moonLight.shadow.mapSize.set(2048, 2048);
const sc = moonLight.shadow.camera;
sc.near = 10; sc.far = 120;
sc.left = -35; sc.right = 35; sc.top = 35; sc.bottom = -35;
scene.add(moonLight);

const rimLight = new THREE.DirectionalLight(0x8866ff, 0.5);
rimLight.position.set(-20, 8, -15);
scene.add(rimLight);

const warmFill = new THREE.DirectionalLight(0xffaa66, 0.25);
warmFill.position.set(5, 3, 15);
scene.add(warmFill);

/* ─── SKY DOME ─────────────────────────────────────────────────── */
const skyMat = new THREE.ShaderMaterial({
    side: THREE.BackSide,
    uniforms: {
        top:    { value: new THREE.Color(0x04041a) },
        mid:    { value: new THREE.Color(0x120838) },
        low:    { value: new THREE.Color(0x2a1560) },
        horizon:{ value: new THREE.Color(0x4a2580) }
    },
    vertexShader: `varying vec3 vD;void main(){vD=normalize(position);gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.);}`,
    fragmentShader: `uniform vec3 top,mid,low,horizon;varying vec3 vD;
    void main(){float h=vD.y;vec3 c;
    if(h>.4)c=mix(mid,top,smoothstep(.4,1.,h));
    else if(h>0.)c=mix(horizon,mid,smoothstep(0.,.4,h));
    else c=mix(horizon,low,smoothstep(0.,-.3,h));
    gl_FragColor=vec4(c,1.);}`
});
scene.add(new THREE.Mesh(new THREE.SphereGeometry(250, 32, 32), skyMat));

/* ─── MOON ─────────────────────────────────────────────────────── */
const moon = new THREE.Mesh(new THREE.SphereGeometry(4, 24, 24), new THREE.MeshBasicMaterial({ color: 0xffffee }));
moon.position.set(50, 65, -80);
scene.add(moon);
const moonGlow = new THREE.Mesh(
    new THREE.SphereGeometry(8, 16, 16),
    new THREE.MeshBasicMaterial({ color: 0xffeedd, transparent: true, opacity: 0.08, side: THREE.BackSide })
);
moonGlow.position.copy(moon.position);
scene.add(moonGlow);

/* ─── STARS ────────────────────────────────────────────────────── */
const STAR_COUNT = 2000;
const starGeo = new THREE.BufferGeometry();
{
    const pos = new Float32Array(STAR_COUNT * 3);
    const sz  = new Float32Array(STAR_COUNT);
    for (let i = 0; i < STAR_COUNT; i++) {
        const th = Math.random() * Math.PI * 2;
        const ph = Math.acos(Math.random() * 2 - 1);
        const r  = 190 + Math.random() * 50;
        pos[i*3]   = r * Math.sin(ph) * Math.cos(th);
        pos[i*3+1] = r * Math.sin(ph) * Math.sin(th);
        pos[i*3+2] = r * Math.cos(ph);
        sz[i] = 0.5 + Math.random() * 2.5;
    }
    starGeo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
    starGeo.setAttribute('size', new THREE.BufferAttribute(sz, 1));
}
scene.add(new THREE.Points(starGeo, new THREE.ShaderMaterial({
    transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
    vertexShader: `attribute float size;void main(){vec4 mv=modelViewMatrix*vec4(position,1.);
        gl_PointSize=size*(500./-mv.z);gl_Position=projectionMatrix*mv;}`,
    fragmentShader: `void main(){float d=length(gl_PointCoord-.5);if(d>.5)discard;
        gl_FragColor=vec4(.9,.85,1.,smoothstep(.5,0.,d)*.7);}`
})));

/* ─── WATER ────────────────────────────────────────────────────── */
const waterGeo = new THREE.PlaneGeometry(500, 500, 256, 256);
waterGeo.rotateX(-Math.PI / 2);

const waterMat = new THREE.ShaderMaterial({
    uniforms: {
        uTime:  { value: 0 },
        uCam:   { value: new THREE.Vector3() },
        uSkyTop:{ value: new THREE.Color(0x120838) },
        uSkyHor:{ value: new THREE.Color(0x4a2580) },
        uMoonD: { value: new THREE.Vector3(30, 50, -40).normalize() },
        uMoonC: { value: new THREE.Color(0xffeedd) }
    },
    vertexShader: `
        uniform float uTime;
        varying vec3 vWP; varying vec3 vN;
        float w(vec2 p,float t){
            return sin(p.x*.35+t*.7)*.12+sin(p.y*.28+t*.5)*.10
                 + sin((p.x+p.y)*.22+t*.35)*.15+sin(p.x*.6-t*.8)*.05
                 + sin(p.y*.5+t*.6)*.04;
        }
        void main(){
            vec3 pos=position; float h=w(pos.xz,uTime); pos.y+=h;
            float e=.4;
            float hx=w(pos.xz+vec2(e,0),uTime)-h;
            float hz=w(pos.xz+vec2(0,e),uTime)-h;
            vN=normalize(vec3(-hx/e,1.,-hz/e));
            vec4 wp=modelMatrix*vec4(pos,1.); vWP=wp.xyz;
            gl_Position=projectionMatrix*viewMatrix*wp;
        }`,
    fragmentShader: `
        uniform float uTime;
        uniform vec3 uCam,uSkyTop,uSkyHor,uMoonD,uMoonC;
        varying vec3 vWP; varying vec3 vN;
        void main(){
            vec3 V=normalize(uCam-vWP); vec3 N=normalize(vN);
            float fres=pow(1.-max(dot(V,N),0.),3.5); fres=clamp(fres,0.,1.);
            vec3 deep=vec3(.008,.015,.06); vec3 shall=vec3(.02,.04,.12);
            vec3 refD=reflect(-V,N);
            vec3 skyRef=mix(uSkyHor,uSkyTop,clamp(refD.y,0.,1.));
            vec3 col=mix(deep,shall,fres);
            col=mix(col,skyRef,fres*.85);
            vec3 H=normalize(uMoonD+V);
            col+=uMoonC*pow(max(dot(N,H),0.),400.)*4.;
            col+=uMoonC*pow(max(dot(N,H),0.),60.)*.4;
            float sp=sin(vWP.x*2.5+uTime*1.3)*sin(vWP.z*3.-uTime*.9);
            sp+=sin(vWP.x*6.-uTime*1.8)*sin(vWP.z*7.+uTime*1.4);
            col+=vec3(.3,.25,.5)*pow(max(sp*.5,0.),6.)*.4*fres;
            float dist=length(uCam-vWP);
            col=mix(col,vec3(.06,.03,.15),smoothstep(100.,350.,dist));
            gl_FragColor=vec4(col,1.);
        }`
});
const water = new THREE.Mesh(waterGeo, waterMat);
scene.add(water);

/* ─── CLOUDS ───────────────────────────────────────────────────── */
const cloudGroup = new THREE.Group();
scene.add(cloudGroup);

function makeCloud(cx, cy, cz, scale, hue) {
    const g = new THREE.Group();
    const c = new THREE.Color().setHSL(hue, 0.45, 0.42);
    const mat = new THREE.MeshStandardMaterial({
        color: c, transparent: true, opacity: 0.5,
        roughness: 1, metalness: 0, emissive: c, emissiveIntensity: 0.12
    });
    const n = 5 + Math.floor(Math.random() * 5);
    for (let i = 0; i < n; i++) {
        const r = (0.8 + Math.random() * 1.5) * scale;
        const puff = new THREE.Mesh(new THREE.SphereGeometry(r, 8, 6), mat);
        puff.position.set(
            (Math.random() - 0.5) * 5 * scale,
            (Math.random() - 0.3) * 1.2 * scale,
            (Math.random() - 0.5) * 3.5 * scale
        );
        puff.scale.y = 0.5 + Math.random() * 0.25;
        g.add(puff);
    }
    g.position.set(cx, cy, cz);
    g.userData = { drift: (Math.random() - 0.5) * 0.4, baseY: cy, phase: Math.random() * 6.28 };
    return g;
}
[[-35,22,-60,3,.72],[30,18,-70,4,.75],[-45,25,-25,2.5,.7],
 [40,28,-35,3.5,.74],[-25,30,-80,5,.73],[45,20,-90,3,.76],
 [0,26,-100,4.5,.72],[-55,15,-45,3,.75],[55,24,-60,3.5,.71],
 [-18,32,-50,4,.73],[35,35,-70,5,.74],[-40,20,-18,2.5,.76],
 [22,24,-45,3,.73],[-28,16,10,1.8,.74],[15,18,15,2,.72]
].forEach(([x,y,z,s,h]) => cloudGroup.add(makeCloud(x,y,z,s,h)));

/* ─── FIGHTER JET ──────────────────────────────────────────────── */
function createFighterJet() {
    const jet = new THREE.Group();
    const mW = new THREE.MeshStandardMaterial({ color: 0xeeeeee, metalness: .2, roughness: .45, side: THREE.DoubleSide });
    const mG = new THREE.MeshStandardMaterial({ color: 0x22bb66, metalness: .4, roughness: .3, side: THREE.DoubleSide });
    const mR = new THREE.MeshStandardMaterial({ color: 0xcc3344, metalness: .4, roughness: .3, side: THREE.DoubleSide });
    const mD = new THREE.MeshStandardMaterial({ color: 0x1a1a30, metalness: .7, roughness: .25 });
    const mGlass = new THREE.MeshPhysicalMaterial({ color: 0x66ddff, transparent: true, opacity: .4, roughness: .05, clearcoat: 1 });
    const mGlow = new THREE.MeshBasicMaterial({ color: 0x44ddff, transparent: true, opacity: .85 });

    const bg = new THREE.SphereGeometry(1, 16, 12); bg.scale(3.2, .7, .6);
    const body = new THREE.Mesh(bg, mW); body.castShadow = true; jet.add(body);

    const ng = new THREE.ConeGeometry(.25, 1.5, 8); ng.rotateZ(-Math.PI / 2);
    const nose = new THREE.Mesh(ng, mW); nose.position.set(3.5, 0, 0); jet.add(nose);

    function wingGeom(sign) {
        const s = new THREE.Shape();
        if (sign > 0) { s.moveTo(1.5,0); s.lineTo(-2.5,4.5); s.lineTo(-3,4.5); s.lineTo(-1,0); }
        else { s.moveTo(1.5,0); s.lineTo(-1,0); s.lineTo(-3,-4.5); s.lineTo(-2.5,-4.5); }
        s.closePath();
        const g = new THREE.ExtrudeGeometry(s, { depth: .07, bevelEnabled: false });
        g.rotateX(Math.PI / 2); g.translate(0, .035, 0);
        return g;
    }
    const lW = new THREE.Mesh(wingGeom(1), mG); lW.castShadow = true; jet.add(lW);
    const rW = new THREE.Mesh(wingGeom(-1), mG); rW.castShadow = true; jet.add(rW);

    const ts = new THREE.Shape();
    ts.moveTo(-1.5,0); ts.lineTo(-2,2.5); ts.lineTo(-2.8,2.5); ts.lineTo(-2.8,0); ts.closePath();
    const tail = new THREE.Mesh(new THREE.ExtrudeGeometry(ts,{depth:.07,bevelEnabled:false}), mR);
    tail.position.set(0,.5,-.035); jet.add(tail);

    const ls = new THREE.Mesh(new THREE.BoxGeometry(1.2,.06,1.5), mR);
    ls.position.set(-2.5,.1,1); ls.rotation.y=.15; jet.add(ls);
    const rs = new THREE.Mesh(new THREE.BoxGeometry(1.2,.06,1.5), mR);
    rs.position.set(-2.5,.1,-1); rs.rotation.y=-.15; jet.add(rs);

    const cg = new THREE.SphereGeometry(.4,12,8,0,Math.PI*2,0,Math.PI/2);
    const canopy = new THREE.Mesh(cg, mGlass);
    canopy.position.set(1.5,.5,0); canopy.scale.set(1.6,1,1); jet.add(canopy);

    const eg = new THREE.CylinderGeometry(.18,.22,1.2,8); eg.rotateZ(Math.PI/2);
    const eL = new THREE.Mesh(eg,mD); eL.position.set(-3,-.1,.35); jet.add(eL);
    const eR = new THREE.Mesh(eg.clone(),mD); eR.position.set(-3,-.1,-.35); jet.add(eR);

    const gg = new THREE.SphereGeometry(.16,8,8);
    const gL = new THREE.Mesh(gg,mGlow); gL.position.set(-3.7,-.1,.35); jet.add(gL);
    const gR = new THREE.Mesh(gg.clone(),mGlow); gR.position.set(-3.7,-.1,-.35); jet.add(gR);
    const eLight = new THREE.PointLight(0x44ddff,3,10,2); eLight.position.set(-4,-.1,0); jet.add(eLight);

    const sr = new THREE.Mesh(new THREE.BoxGeometry(2.5,.04,.06), mR); sr.position.set(.3,.4,0); jet.add(sr);
    const sg = new THREE.Mesh(new THREE.BoxGeometry(2.5,.04,.06), mG); sg.position.set(.3,-.3,0); jet.add(sg);

    const tl1 = new THREE.Mesh(new THREE.SphereGeometry(.06,6,6), new THREE.MeshBasicMaterial({color:0xff3344}));
    tl1.position.set(-2.75,0,4.5); jet.add(tl1);
    const tl2 = new THREE.Mesh(new THREE.SphereGeometry(.06,6,6), new THREE.MeshBasicMaterial({color:0x33ff66}));
    tl2.position.set(-2.75,0,-4.5); jet.add(tl2);

    const belly = new THREE.Mesh(new THREE.BoxGeometry(2,.2,.8), mD);
    belly.position.set(0,-.6,0); jet.add(belly);
    return jet;
}
const jet = createFighterJet();
jet.position.set(-5, 5, -8);
jet.rotation.y = 0.4;
scene.add(jet);

/* ─── SPACE BUNNY ──────────────────────────────────────────────── */
function createSpaceBunny() {
    const bunny = new THREE.Group();
    const mFur   = new THREE.MeshStandardMaterial({ color:0xf5f0ff, metalness:0, roughness:.8 });
    const mSuit  = new THREE.MeshStandardMaterial({ color:0xe8e0f0, metalness:.25, roughness:.5 });
    const mAcc   = new THREE.MeshStandardMaterial({ color:0x33bb66, metalness:.5, roughness:.3 });
    const mRed   = new THREE.MeshStandardMaterial({ color:0xcc3344, metalness:.4, roughness:.4 });
    const mEye   = new THREE.MeshStandardMaterial({ color:0x111133, metalness:.3, roughness:.2 });
    const mHL    = new THREE.MeshBasicMaterial({ color:0xffffff });
    const mPink  = new THREE.MeshStandardMaterial({ color:0xff88aa, metalness:0, roughness:.6 });
    const mVisor = new THREE.MeshPhysicalMaterial({ color:0x88ccff, transparent:true, opacity:.3, roughness:.05, clearcoat:1, side:THREE.DoubleSide });
    const mGlow  = new THREE.MeshBasicMaterial({ color:0x88ccff });
    const mWPack = new THREE.MeshStandardMaterial({ color:0xdddddd, metalness:.4, roughness:.4 });

    // Body
    const bg = new THREE.SphereGeometry(.5,16,12); bg.scale(.9,1.05,.85);
    const body = new THREE.Mesh(bg,mSuit); body.position.y=.55; body.castShadow=true; bunny.add(body);

    const bStripe = new THREE.Mesh(new THREE.TorusGeometry(.4,.03,8,16),mAcc);
    bStripe.position.y=.6; bStripe.rotation.x=Math.PI/2; bunny.add(bStripe);

    // Chest star
    const sShape = new THREE.Shape();
    for (let i=0;i<5;i++) {
        const a=(i*4*Math.PI)/5-Math.PI/2, r=.08;
        const x=r*Math.cos(a), y=r*Math.sin(a);
        i===0?sShape.moveTo(x,y):sShape.lineTo(x,y);
    }
    sShape.closePath();
    const starM = new THREE.Mesh(
        new THREE.ExtrudeGeometry(sShape,{depth:.02,bevelEnabled:false}),
        new THREE.MeshBasicMaterial({color:0xffdd44})
    );
    starM.position.set(0,.7,.42); bunny.add(starM);

    // Head
    const headG = new THREE.Group(); headG.position.set(0,1.15,.05); bunny.add(headG);
    const hg = new THREE.SphereGeometry(.38,16,12); hg.scale(.95,1,.95);
    headG.add(new THREE.Mesh(hg,mFur));

    const muzzle = new THREE.Mesh(new THREE.SphereGeometry(.18,10,8),mFur);
    muzzle.position.set(0,-.1,.3); muzzle.scale.set(1,.8,1); headG.add(muzzle);
    const nose = new THREE.Mesh(new THREE.SphereGeometry(.055,8,6),mPink);
    nose.position.set(0,-.05,.44); headG.add(nose);

    function makeEye(x){
        const g=new THREE.Group();
        g.add(new THREE.Mesh(new THREE.SphereGeometry(.1,10,8),mEye));
        const h1=new THREE.Mesh(new THREE.SphereGeometry(.035,6,6),mHL); h1.position.set(.03,.03,.08); g.add(h1);
        const h2=new THREE.Mesh(new THREE.SphereGeometry(.02,6,6),mHL);  h2.position.set(-.02,-.02,.08); g.add(h2);
        g.position.set(x,.05,.28); return g;
    }
    headG.add(makeEye(.18)); headG.add(makeEye(-.18));

    // Ears
    function makeEar(x,dir){
        const g=new THREE.Group();
        const og=new THREE.SphereGeometry(.15,8,8); og.scale(.5,2.8,1.0);
        const outer=new THREE.Mesh(og,mFur); outer.position.y=.4; outer.castShadow=true; g.add(outer);
        const ig=new THREE.SphereGeometry(.1,8,8); ig.scale(.3,2.5,.6);
        const inner=new THREE.Mesh(ig,mPink); inner.position.set(0,.4,.04); g.add(inner);
        const tip=new THREE.Mesh(new THREE.SphereGeometry(.04,6,6),mGlow); tip.position.y=1.05; g.add(tip);
        g.position.set(x,.3,0); g.rotation.z=dir*.15; return g;
    }
    const earL=makeEar(.15,-1); headG.add(earL);
    const earR=makeEar(-.15,1);  headG.add(earR);

    // Visor & helmet rim
    const vGeom = new THREE.SphereGeometry(.45,16,12,0,Math.PI*2,0,Math.PI*.55);
    const visor = new THREE.Mesh(vGeom,mVisor); visor.position.set(0,.08,.05); headG.add(visor);
    const rGeom = new THREE.TorusGeometry(.42,.02,8,24); rGeom.rotateX(Math.PI/2);
    const hRim = new THREE.Mesh(rGeom,mAcc); hRim.position.set(0,.08,.05); headG.add(hRim);

    // Collar
    const cGeom = new THREE.TorusGeometry(.3,.06,8,20); cGeom.rotateX(Math.PI/2);
    const collar = new THREE.Mesh(cGeom,mAcc); collar.position.y=.95; bunny.add(collar);

    // Jetpack
    const pack = new THREE.Mesh(new THREE.CapsuleGeometry(.12,.3,4,8),mWPack);
    pack.position.set(0,.6,-.45); bunny.add(pack);
    const thruster = new THREE.Mesh(new THREE.SphereGeometry(.06,6,6),mGlow);
    thruster.position.set(0,.3,-.45); bunny.add(thruster);
    const thrLight = new THREE.PointLight(0x88ccff,1.5,3,2); thrLight.position.set(0,.25,-.45); bunny.add(thrLight);

    // Arms
    function makeArm(side){
        const g=new THREE.Group();
        const am=new THREE.Mesh(new THREE.CapsuleGeometry(.07,.25,4,8),mSuit); am.position.y=-.15; g.add(am);
        const hand=new THREE.Mesh(new THREE.SphereGeometry(.08,8,6),mFur); hand.position.y=-.35; g.add(hand);
        g.position.set(side*.45,.7,.1); g.rotation.z=-side*.3; return g;
    }
    const armL=makeArm(1);  bunny.add(armL);
    const armR=makeArm(-1); bunny.add(armR);

    // Legs
    function makeLeg(side){
        const g=new THREE.Group();
        const lm=new THREE.Mesh(new THREE.CapsuleGeometry(.1,.2,4,8),mSuit); lm.position.y=-.15; g.add(lm);
        const fg=new THREE.SphereGeometry(.12,8,6); fg.scale(.8,.5,1.3);
        const foot=new THREE.Mesh(fg,mRed); foot.position.set(0,-.3,.05); g.add(foot);
        g.position.set(side*.2,.35,0); return g;
    }
    const legL=makeLeg(1);  bunny.add(legL);
    const legR=makeLeg(-1); bunny.add(legR);

    return { group:bunny, earL, earR, head:headG, body, armL, armR, legL, legR, thruster, thrLight };
}
const bunny = createSpaceBunny();
scene.add(bunny.group);
bunny.group.position.set(5, 4, 5);
bunny.group.scale.set(1.5, 1.5, 1.5);

/* ─── FLOATING PLATFORMS ───────────────────────────────────────── */
function makePlatform(x, y, z, r) {
    const p = new THREE.Mesh(
        new THREE.CylinderGeometry(r, r*.75, r*.2, 6),
        new THREE.MeshStandardMaterial({ color:0x2a2a4a, metalness:.3, roughness:.6 })
    );
    p.position.set(x, y, z); p.castShadow = true; scene.add(p);
    const ring = new THREE.Mesh(
        new THREE.TorusGeometry(r*1.05,.04,6,24),
        new THREE.MeshBasicMaterial({ color:0x66aaff })
    );
    ring.rotation.x = Math.PI/2;
    ring.position.set(x, y + r*.1, z);
    scene.add(ring);
}
makePlatform(5, 2.8, 5, 1.6);
makePlatform(-3, 1.2, 6, 1.0);

/* ─── "just breathe" TEXT ──────────────────────────────────────── */
const txtCanvas = document.createElement('canvas');
txtCanvas.width = 1024; txtCanvas.height = 256;
const txtCtx = txtCanvas.getContext('2d');

(function drawText() {
    txtCtx.clearRect(0,0,1024,256);
    txtCtx.font = 'italic 88px Georgia, serif';
    txtCtx.textAlign = 'center'; txtCtx.textBaseline = 'middle';
    txtCtx.shadowColor = '#8b6cff'; txtCtx.shadowBlur = 50;
    txtCtx.fillStyle = 'rgba(140,100,255,0.4)';
    txtCtx.fillText('just breathe',512,128);
    txtCtx.fillText('just breathe',512,128);
    txtCtx.shadowBlur = 25;
    txtCtx.fillStyle = 'rgba(180,160,255,0.7)';
    txtCtx.fillText('just breathe',512,128);
    txtCtx.shadowBlur = 0;
    txtCtx.fillStyle = '#e8ddff';
    txtCtx.fillText('just breathe',512,128);
})();

const txtTex = new THREE.CanvasTexture(txtCanvas);
txtTex.minFilter = THREE.LinearFilter; txtTex.magFilter = THREE.LinearFilter;

const textMesh = new THREE.Mesh(
    new THREE.PlaneGeometry(10, 2.5),
    new THREE.MeshBasicMaterial({
        map:txtTex, transparent:true, opacity:.9, side:THREE.DoubleSide,
        depthWrite:false, blending:THREE.AdditiveBlending
    })
);
textMesh.position.set(0, 14, -35);
scene.add(textMesh);

// Light pillar below text
const pillar = new THREE.Mesh(
    new THREE.CylinderGeometry(2, 0.5, 14, 8, 1, true),
    new THREE.MeshBasicMaterial({
        color:0x8b6cff, transparent:true, opacity:.04,
        side:THREE.DoubleSide, depthWrite:false, blending:THREE.AdditiveBlending
    })
);
pillar.position.set(0, 7, -35);
scene.add(pillar);

/* ─── PARTICLES (space dust) ───────────────────────────────────── */
const PART_COUNT = 400;
const pGeo = new THREE.BufferGeometry();
{
    const pp=new Float32Array(PART_COUNT*3), ps=new Float32Array(PART_COUNT), pph=new Float32Array(PART_COUNT);
    for(let i=0;i<PART_COUNT;i++){
        pp[i*3]=(Math.random()-.5)*80; pp[i*3+1]=Math.random()*30+1; pp[i*3+2]=(Math.random()-.5)*80;
        ps[i]=1+Math.random()*3; pph[i]=Math.random()*6.28;
    }
    pGeo.setAttribute('position',new THREE.BufferAttribute(pp,3));
    pGeo.setAttribute('size',new THREE.BufferAttribute(ps,1));
    pGeo.setAttribute('phase',new THREE.BufferAttribute(pph,1));
}
const pMat = new THREE.ShaderMaterial({
    transparent:true, depthWrite:false, blending:THREE.AdditiveBlending,
    uniforms:{uT:{value:0}},
    vertexShader:`attribute float size;attribute float phase;uniform float uT;varying float vA;
    void main(){vec3 p=position;p.y+=sin(uT*.5+phase)*.5;p.x+=cos(uT*.3+phase)*.3;
    vec4 mv=modelViewMatrix*vec4(p,1.);gl_PointSize=size*(180./-mv.z);gl_Position=projectionMatrix*mv;
    vA=.3+.3*sin(uT*.8+phase);}`,
    fragmentShader:`varying float vA;void main(){float d=length(gl_PointCoord-.5);if(d>.5)discard;
    gl_FragColor=vec4(.7,.6,1.,smoothstep(.5,0.,d)*vA);}`
});
scene.add(new THREE.Points(pGeo, pMat));

/* ─── CAMERA ANIMATION ─────────────────────────────────────────── */
function updateCamera(t) {
    if (userInteracting) return;
    const introEnd = 8;
    if (t < introEnd) {
        const p = t / introEnd;
        const e = 1 - Math.pow(1 - p, 3);
        const ang = 0.65 - e * 0.2;
        const dist = 48 - e * 26;
        const hgt = 15 - e * 8;
        camera.position.x = Math.sin(ang) * dist;
        camera.position.y = hgt;
        camera.position.z = Math.cos(ang) * dist;
        camera.lookAt(0, 5 - e * 2, -6 + e * 6);
    } else {
        const el = t - introEnd;
        const ang = 0.45 + Math.sin(el * 0.03) * 0.35;
        const dist = 22 + Math.sin(el * 0.1) * 3;
        const hgt = 7 + Math.sin(el * 0.08) * 2;
        camera.position.x = Math.sin(ang) * dist;
        camera.position.y = hgt;
        camera.position.z = Math.cos(ang) * dist;
        camera.lookAt(
            Math.sin(el * 0.06) * 2,
            4.5 + Math.sin(el * 0.1) * 1.2,
            -2 + Math.cos(el * 0.07) * 2
        );
    }
    camera.position.y += Math.sin(t * 0.35) * 0.3;
}

/* ─── BUNNY ANIMATION ──────────────────────────────────────────── */
const HOP_DUR = 2.0, HOP_H = 1.8;
let hopT = 0, hopIdx = 0;
const path = [
    { x: 5,  z: 5,  y: 3.8 },
    { x: -3, z: 5,  y: 0.5 },
    { x: -2, z: -3, y: 6 },
    { x: 4,  z: -1, y: 4.5 },
    { x: 6,  z: 4,  y: 3.5 },
];

function animBunny(dt, t) {
    hopT += dt / HOP_DUR;
    if (hopT >= 1) { hopT = 0; hopIdx = (hopIdx + 1) % path.length; }
    const from = path[hopIdx];
    const to   = path[(hopIdx + 1) % path.length];
    const p = hopT, e = p * p * (3 - 2 * p);

    const x = from.x + (to.x - from.x) * e;
    const z = from.z + (to.z - from.z) * e;
    const baseY = from.y + (to.y - from.y) * e;
    const y = baseY + Math.sin(p * Math.PI) * HOP_H;

    // Squash & stretch
    let sy=1, sxz=1;
    if (p<.12) { const s=p/.12; sy=1-.3*(1-s); sxz=1+.3*(1-s); }
    else if (p<.28) { const s=(p-.12)/.16; sy=1+.22*s; sxz=1-.16*s; }
    else if (p>.75) { const s=(p-.75)/.25; sy=1-.08*s; sxz=1+.08*s; }

    bunny.group.position.set(x, y, z);
    bunny.group.scale.set(1.5*sxz, 1.5*sy, 1.5*sxz);

    // Face movement
    const dx=to.x-from.x, dz=to.z-from.z;
    if(Math.abs(dx)>.01||Math.abs(dz)>.01){
        const target=Math.atan2(dx,dz);
        let diff=target-bunny.group.rotation.y;
        while(diff>Math.PI)diff-=Math.PI*2;
        while(diff<-Math.PI)diff+=Math.PI*2;
        bunny.group.rotation.y+=diff*Math.min(dt*4,1);
    }

    // Ear wobble (secondary animation)
    const ep=t*3, swing=Math.sin(ep)*.15+Math.sin(ep*1.3)*.05, hopSwing=Math.sin(p*Math.PI*2)*.2;
    if(bunny.earL){ bunny.earL.rotation.z=-.15+swing+hopSwing; bunny.earL.rotation.x=Math.sin(ep*.8)*.1; }
    if(bunny.earR){ bunny.earR.rotation.z=.15-swing-hopSwing; bunny.earR.rotation.x=Math.sin(ep*.8+.5)*.1; }

    // Arms
    if(bunny.armL){ bunny.armL.rotation.x=Math.sin(ep*1.5)*.3; bunny.armR.rotation.x=Math.sin(ep*1.5+Math.PI)*.3; }

    // Legs (tuck in air)
    if(bunny.legL){
        if(p>.15&&p<.85){ const a=(p-.15)/.7, tuck=Math.sin(a*Math.PI)*.6; bunny.legL.rotation.x=-tuck; bunny.legR.rotation.x=-tuck; }
        else { bunny.legL.rotation.x*=.9; bunny.legR.rotation.x*=.9; }
    }

    // Thruster glow pulse
    const tp=.5+.5*Math.sin(t*4);
    bunny.thruster.material.opacity=.4+.4*tp;
    if(bunny.thrLight) bunny.thrLight.intensity=1+tp*1.5;

    // Head idle motion
    if(bunny.head){
        bunny.head.position.y=1.15+Math.sin(t*2)*.03;
        bunny.head.rotation.z=Math.sin(t*1.5)*.03;
        bunny.head.rotation.y=Math.sin(t*.7)*.1;
    }
}

/* ─── JET / CLOUD ANIMATIONS ───────────────────────────────────── */
function animJet(dt, t) {
    jet.position.set(
        -5 + Math.sin(t*.3)*.5,
         5 + Math.sin(t*.5)*.4,
        -8 + Math.cos(t*.2)*.3
    );
    jet.rotation.z = Math.sin(t*.4)*.05;
    jet.rotation.x = Math.sin(t*.3+1)*.03;
    jet.rotation.y = 0.4 + Math.sin(t*.15)*.1;
}

function animClouds(dt, t) {
    cloudGroup.children.forEach(c => {
        c.position.x += c.userData.drift * dt;
        c.position.y  = c.userData.baseY + Math.sin(t*.15 + c.userData.phase) * 1;
        if (c.position.x > 70)  c.position.x = -70;
        if (c.position.x < -70) c.position.x = 70;
    });
}

/* ─── MAIN LOOP ────────────────────────────────────────────────── */
const clock = new THREE.Clock();
let elapsed = 0;

function animate() {
    requestAnimationFrame(animate);
    const dt = Math.min(clock.getDelta(), 0.05);
    elapsed += dt;

    waterMat.uniforms.uTime.value = elapsed;
    waterMat.uniforms.uCam.value.copy(camera.position);
    pMat.uniforms.uT.value = elapsed;
    bloom.strength = 0.7 + Math.sin(elapsed * 0.4) * 0.1;

    animBunny(dt, elapsed);
    animJet(dt, elapsed);
    animClouds(dt, elapsed);
    updateCamera(elapsed);

    // Billboard text toward camera (yaw only)
    const dxT = camera.position.x - textMesh.position.x;
    const dzT = camera.position.z - textMesh.position.z;
    textMesh.rotation.y = Math.atan2(dxT, dzT);

    if (controls.enabled) controls.update();
    composer.render();
}

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

/* ─── START ────────────────────────────────────────────────────── */
animate();
setTimeout(() => document.getElementById('loader').classList.add('done'), 1200);
</script>
</body>
</html>
3.14s
TTFT
Actual wait 3.40s
89
Reasoning TPS
825s · 73,286 tok
154
Output TPS
65.3s · 10,083 tok
83,369
Total Tokens (official)
Input 525
Timing details
Actual wait (client)
3.40s
Client preparation
0.05s
Proxy preparation
0.00s
Upstream response headers
3.06s
Upstream first byte
3.06s
Upstream first token
3.14s
Upstream retries
0

Actual wait uses the client clock. Upstream timings start after proxy preparation and are cumulative; do not add these values. Upstream delay includes network, scheduling, processing and buffering; it does not prove provider queueing.

Diagnostic receipt
{
  "attempts": 1,
  "startedAt": "2026-09-29T03:43:55.809Z",
  "transport": "railway",
  "ttftSource": "proxy",
  "clientTtftMs": 3400,
  "completionId": "fec2f283-d652-486f-bc90-d7978740a3dc_04858599025247119dde133f5791a086",
  "streamResumes": 19,
  "proxyPrepareMs": 0,
  "proxyRequestId": "cbeFE6DvSROY-BA0ljLL4A",
  "upstreamStatus": 200,
  "upstreamTtftMs": 3139,
  "clientHeadersMs": 259.2999997138977,
  "clientPrepareMs": 45.09999990463257,
  "upstreamHeadersMs": 3058,
  "upstreamFirstByteMs": 3058,
  "clientFirstContentMs": 828133.0999999046
}
Elapsed 894s · Average 94 tok/s · Peak 199 tok/s
OpenRouter
stealth/space-bunny-alpha
Done

Historical result · 9/29/2026, 04:01:50

We need answer Chinese, need create directly runnable single HTML Web3D artwork. We need likely provide code. Need decide libraries external? User says single HTML, open run, minimize complex external resources. Could use Three.js CDN but opening requires network. "尽量不依赖复杂外部资源" perhaps single HTML imports Three.js from CDN acceptable? Directly runnable but network. Better maybe pure WebGL no dependencies, single HTML, but 3D modeling/render complexity. Could use CSS? Request Web3D, can build native Canvas 2D pseudo-3D but not truly. Could deliver single HTML using Three.js from esm CDN. But opening file directly modules with HTTPS CORS works? import from CDN on file likely yes, module CORS. Yet they asked open and run, dependency. Could embed minified three huge impossible. Maybe use `<script src=https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js>`; r160 has UMD removed? Three min.js possibly until r161? r160 includes build/three.min.js maybe deprecated. Global script works file. Could use three 0.152.2. Also custom post-processing maybe no. Need high quality scene and detailed Space Bunny + fighter. We can code 3D with Three.js, procedural geometry/materials, reflection. Need ensure visually compelling and animation. Need no OrbitControls. Need use custom shaders for sky/ocean/clouds? Need all in one file except CDN. Could create robust HTML. Need likely respond with complete code fenced, save as index.html. Maybe mention internet required first load. But user asks create; code is answer. Need aim desired oververbosity 5, likely provide code substantial. Need ensure no bugs. We need design meticulously. Let's formulate single HTML. Use Three.js via CDN, perhaps script classic. The file should be directly openable, no local server. Three.js 0.160.0 `build/three.min.js` global? Let's verify. Three build files: since r161, build/three.js removed? In r160 build/three.cjs, three.module.js, three.module.min.js, three.min.js? Three.js usage `<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>`, warning deprecated but works, global THREE. I think yes. Better use 0.158.0 unpkg. Need postprocessing not included in min build? examples load. Avoid. Use renderer, WebGLRenderer antialias alpha false; color management, toneMapping ACES, shadows PCFSoft, pixel ratio max 1.75. FogExp2. Coordinate system: Sea y=0 plane 300x300, shader material reflective/fake. Sky sphere or fullscreen background shader. Since plane, custom shader with vertex waves + fragment colors, Fresnel, moon streak, stars? Reflective water can use CubeCamera for actual reflection but expensive; could use Reflector from examples external which has another dependency and import issue. Implement sea ShaderMaterial with procedural waves, Fresnel, horizon, reflected sky and luminous fighter perhaps fake elongated specular. Real reflection requirement "明显反光". Could use actual planar reflection render target manually: render scene reflected in camera every 2 frames? Complex but feasible. Use a `Reflector` implementation custom with reflection camera and texture matrix. Or use `Reflector.js` via separate external script from CDN, but no complex resources. Could inline a lightweight planar reflector code. We can create ocean as plane with `MeshPhysicalMaterial` high metalness, roughness 0.15, environment? Scene.environment via PMREM generated from a custom procedural sky? But plane no accurate object reflection. Can render to cube map at a static point once, with dynamic reflection maybe. `CubeCamera` near sea position captures fighter/bunny/clouds; use as envMap on plane but expensive. Better use planar reflection with `WebGLRenderTarget`, clipping plane. Implement basic. Or use custom shader with ocean movement and mirror silhouette approximated. "明显反光" can be stylized enough if sky moon glow reflects and fighter highlights. But scene should look good. Clouds: volumetric purple cumulus. Use groups of sphere meshes with MeshStandardMaterial, transparent/opacity, maybe custom `onBeforeCompile`? Each cloud made 12-20 low-poly spheres, with random scales, varying purple. Fog. To convey volume, multiple icospheres and shell? Could create metaball-ish cluster of spheres with gradient shaders. We can use `MeshPhysicalMaterial` with color and emissive, roughness. Use spheres at different sizes, flatten bottom. Three cloud groups at background positions and one near side. Animated slow drift and rotation. With point lights. Purple cloud clusters. Space Bunny: Distinctive: - body round white suit; maybe rabbit head exposed, not ordinary. body with flight jacket / small spacesuit with green trim and red chest patch. - long ears with inner pink/purple, maybe one ear bends using segments or bone-like chain? Use hierarchy pivot; create capsules from `CapsuleGeometry` (r160 has CapsuleGeometry likely). White outer ears as capsules or custom bent tapered tube. Animate ears with springs. - head sphere, muzzle, eyes visor? Cool with translucent cyan eye visor maybe two eyes. Small nose and mouth. - suit torso maybe capsule/ellipsoid, white/ivory, green collar, red belt/patch, gray boots. A jetpack? compact backpack with mint glowing thrusters, clear "space traveler." - limbs: upper arms and legs capsules, joints; use hierarchy, animate squash/stretch. - tail: fluffy sphere. - posed running/jumping, stand on wing or foreground. Need story with fighter. Perhaps main bunny anchored following a trajectory along right wing, with two ghost? User says can bounce. We can have main bunny on right/left wing at initial; jump arc and land. Could make bunny's root in world and update y, z etc, orient. Fighter flies slowly from right to left, bunny on foreground independent. Composition maybe bunny on wing, fighter passes close. Story. Coordinate setup: camera initial maybe (10, 5, 18) lookAt (0,2,-1). Fighter main around x=0, y=3, z=-1, length aligned X, nose +X? Build nose facing +X. Wings. Sea y=0. Platform foreground maybe at x=-5,z=5? Bunny jumps in foreground. To integrate, let fighter hover center and bunny rides/jumps on its left wing. Camera low. We can have bunny periodically leap from wing to a small circular launch platform, then back; complex path. Easier make fighter fixed in approximate hover, bunny root follows path based on cycle: first 6 seconds bunny on wing x=-1? Fighter local. Then 3 sec jump to foreground platform (-5,0.6,6), pause, back. But story transitions should be smooth. Yet root path and physics. Could be continuous hopping on a floating platform in foreground while fighter behind, not necessarily on wing. User allows. We can make platform at x=-3,z=5 and y=0.5; bunny bounces there; fighter hovers behind. Strong protagonist foreground. Add an energy arc / floating stepping stones from platform to wing maybe. But all elements composition not pile. Need "can stand on wing, platform, or foreground" not must all. We can place bunny on wing and platform perhaps animated route. Could create two-stage route: - fighter at (2, 3.2, -2), length ~9 x. - platform at (-3.6,0.65,4.8) - Bunny starts on platform, hops 3 hops; every 12 sec an energy leap to right/left wing, lands, then return. But root needs orientation based on local. This can be procedurally path with phases: cycle 16s: t 0-4: 3 hops on platform around x -3.8 z 4.6, y around 0.95 t 4-7.5: leap from platform to wing local (-1.0, y wing top, z around -1.2) t 7.5-11.5: stands/2 small wing hops t 11.5-15.5: leap back. But interpolation and vertical arc, orientation. Need make root follow and not look like floating. At landing squash. Could simplify route always around platform, and bunny is visually on small connected holographic stepping pad near fighter. But explicit composition. Maybe bunny always on wing with jumps in wing local. Fighter moves subtly; bunny root child of fighter wing transform. Then seamless. Main bunny stands on left wing at local x=-0.8? If fighter nose +X, wing extends z. Bun

下面是完整的单文件作品。保存为 space-bunny.html 后直接用浏览器打开即可。

  • 所有模型、纹理、云层、粒子、UI 均为程序生成。
  • 使用真实平面倒影、程序化海浪、动态云层、角色弹簧式耳朵与蹦跳动画。
  • 镜头会自动进行缓慢推进和轻微环绕;鼠标移动会带来很轻的视差。
  • 唯一外部依赖是通过 CDN 加载的 Three.js,因此首次打开需要联网。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta name="theme-color" content="#090b22" />
  <title>Space Bunny — just breathe</title>

  <style>
    :root {
      color-scheme: dark;
      --cyan: #bffcff;
      --mint: #75f2c0;
      --violet: #b39aff;
    }

    * {
      box-sizing: border-box;
    }

    html,
    body {
      width: 100%;
      height: 100%;
      margin: 0;
      overflow: hidden;
      background: #07091c;
      font-family:
        Inter, ui-sans-serif, system-ui, -apple-system,
        BlinkMacSystemFont, "Segoe UI", sans-serif;
    }

    #stage,
    #stage canvas {
      position: fixed;
      inset: 0;
      width: 100%;
      height: 100%;
    }

    #stage canvas {
      display: block;
      outline: none;
    }

    #vignette,
    #grain {
      position: fixed;
      inset: 0;
      z-index: 3;
      pointer-events: none;
    }

    #vignette {
      background:
        radial-gradient(
          ellipse at 50% 45%,
          transparent 40%,
          rgba(4, 5, 20, 0.12) 68%,
          rgba(3, 4, 17, 0.62) 100%
        ),
        linear-gradient(
          to bottom,
          rgba(4, 5, 18, 0.2),
          transparent 17%,
          transparent 78%,
          rgba(3, 5, 18, 0.38)
        );
    }

    #grain {
      opacity: 0.055;
      mix-blend-mode: soft-light;
      background-image:
        radial-gradient(circle, rgba(255,255,255,.7) 0.5px, transparent 0.8px),
        radial-gradient(circle, rgba(120,190,255,.5) 0.5px, transparent 0.8px);
      background-position: 0 0, 2px 3px;
      background-size: 4px 4px, 5px 5px;
    }

    .hud {
      position: fixed;
      inset: 0;
      z-index: 5;
      pointer-events: none;
      color: rgba(226, 246, 255, 0.78);
    }

    .brand {
      position: absolute;
      top: 28px;
      left: 32px;
      display: flex;
      align-items: center;
      gap: 13px;
      letter-spacing: 0.16em;
      text-transform: uppercase;
      user-select: none;
    }

    .emblem {
      position: relative;
      width: 34px;
      height: 34px;
      border: 1px solid rgba(185, 255, 242, 0.48);
      border-radius: 50%;
      box-shadow:
        0 0 22px rgba(111, 240, 208, 0.12),
        inset 0 0 12px rgba(140, 218, 255, 0.08);
    }

    .emblem::before,
    .emblem::after {
      content: "";
      position: absolute;
      top: -7px;
      width: 7px;
      height: 17px;
      border: 1px solid rgba(185, 255, 242, 0.68);
      border-radius: 60% 60% 35% 35%;
      background: rgba(92, 128, 178, 0.14);
    }

    .emblem::before {
      left: 8px;
      transform: rotate(-8deg);
    }

    .emblem::after {
      right: 8px;
      transform: rotate(8deg);
    }

    .brand strong {
      display: block;
      font-size: 11px;
      font-weight: 600;
    }

    .brand small {
      display: block;
      margin-top: 5px;
      color: rgba(184, 211, 230, 0.48);
      font-size: 8px;
      letter-spacing: 0.22em;
    }

    .telemetry {
      position: absolute;
      right: 31px;
      bottom: 27px;
      display: flex;
      align-items: center;
      gap: 11px;
      font-size: 9px;
      letter-spacing: 0.17em;
      text-transform: uppercase;
    }

    .telemetry .signal {
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: var(--mint);
      box-shadow: 0 0 12px var(--mint);
      animation: breathe 3.8s ease-in-out infinite;
    }

    .caption {
      position: absolute;
      left: 32px;
      bottom: 27px;
      max-width: 260px;
      color: rgba(204, 225, 239, 0.42);
      font-size: 8px;
      line-height: 1.7;
      letter-spacing: 0.18em;
      text-transform: uppercase;
    }

    #pauseButton {
      position: absolute;
      top: 26px;
      right: 30px;
      min-width: 78px;
      padding: 9px 13px;
      border: 1px solid rgba(191, 234, 255, 0.18);
      border-radius: 999px;
      outline: none;
      color: rgba(218, 243, 255, 0.68);
      background: rgba(10, 14, 37, 0.22);
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
      font: inherit;
      font-size: 8px;
      letter-spacing: 0.17em;
      text-transform: uppercase;
      cursor: pointer;
      pointer-events: auto;
      transition:
        border-color 180ms ease,
        color 180ms ease,
        background 180ms ease;
    }

    #pauseButton:hover,
    #pauseButton:focus-visible {
      color: #dfffff;
      border-color: rgba(132, 255, 224, 0.55);
      background: rgba(29, 58, 69, 0.32);
    }

    #loading {
      position: fixed;
      inset: 0;
      z-index: 20;
      display: grid;
      place-items: center;
      color: rgba(222, 244, 255, 0.86);
      background:
        radial-gradient(circle at 50% 45%, #242154 0%, #0a0c22 48%, #050617 100%);
      transition:
        opacity 1.2s cubic-bezier(.2,.8,.2,1),
        visibility 1.2s;
    }

    #loading .loader {
      position: relative;
      width: 76px;
      height: 76px;
      margin: 0 auto 23px;
      border: 1px solid rgba(174, 245, 255, 0.2);
      border-radius: 50%;
      box-shadow: inset 0 0 35px rgba(104, 205, 255, 0.06);
      animation: orbit 5s linear infinite;
    }

    #loading .loader::before,
    #loading .loader::after {
      content: "";
      position: absolute;
      top: 13px;
      width: 12px;
      height: 31px;
      border: 1px solid rgba(202, 255, 247, 0.65);
      border-radius: 70% 70% 35% 35%;
      background: rgba(118, 172, 224, 0.1);
    }

    #loading .loader::before {
      left: 18px;
      transform: rotate(-8deg);
    }

    #loading .loader::after {
      right: 18px;
      transform: rotate(8deg);
    }

    #loading .loader i {
      position: absolute;
      inset: 26px;
      border-radius: 50%;
      background: rgba(135, 255, 223, 0.75);
      box-shadow: 0 0 18px rgba(100, 255, 216, 0.7);
    }

    #loading strong {
      display: block;
      font-size: 10px;
      font-weight: 500;
      letter-spacing: 0.32em;
      text-align: center;
      text-transform: uppercase;
    }

    #loading small {
      display: block;
      margin-top: 10px;
      color: rgba(184, 210, 231, 0.42);
      font-size: 8px;
      letter-spacing: 0.2em;
      text-align: center;
    }

    body.loaded #loading {
      opacity: 0;
      visibility: hidden;
    }

    @keyframes orbit {
      to { transform: rotate(360deg); }
    }

    @keyframes breathe {
      0%, 100% {
        opacity: 0.45;
        transform: scale(0.8);
      }
      50% {
        opacity: 1;
        transform: scale(1.2);
      }
    }

    @media (max-width: 700px) {
      .brand {
        top: 20px;
        left: 20px;
      }

      #pauseButton {
        top: 18px;
        right: 18px;
      }

      .caption {
        left: 20px;
        bottom: 20px;
        max-width: 180px;
      }

      .telemetry {
        right: 20px;
        bottom: 20px;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .telemetry .signal,
      #loading .loader {
        animation-duration: 10s;
      }
    }
  </style>
</head>

<body>
  <div id="stage"></div>
  <div id="vignette"></div>
  <div id="grain"></div>

  <div class="hud">
    <div class="brand">
      <div class="emblem"></div>
      <div>
        <strong>Space Bunny</strong>
        <small>Dream Coast / Flight 07</small>
      </div>
    </div>

    <div class="caption">
      Low-orbit living unit<br />
      Move gently. The sky is listening.
    </div>

    <div class="telemetry">
      <span class="signal"></span>
      <span id="timecode">T+ 00:00</span>
    </div>

    <button id="pauseButton" type="button" aria-label="暂停动画">
      Pause Ⅱ
    </button>
  </div>

  <div id="loading">
    <div>
      <div class="loader"><i></i></div>
      <strong id="loadingText">Entering the dream coast</strong>
      <small>All destinations are made of breathing</small>
    </div>
  </div>

  <!-- 唯一外部依赖:Three.js;其余内容全部在当前文件内生成。 -->
  <script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>

  <script>
  (() => {
    "use strict";

    const loading = document.getElementById("loading");
    const loadingText = document.getElementById("loadingText");

    if (!window.THREE) {
      loadingText.textContent = "Three.js could not be loaded";
      return;
    }

    const stage = document.getElementById("stage");
    const pauseButton = document.getElementById("pauseButton");
    const timecode = document.getElementById("timecode");

    const TAU = Math.PI * 2;
    const V2 = THREE.Vector2;
    const V3 = THREE.Vector3;
    const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;

    // ------------------------------------------------------------
    // 渲染器与基础场景
    // ------------------------------------------------------------

    let renderer;

    try {
      renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: false,
        powerPreference: "high-performance"
      });
    } catch (error) {
      loadingText.textContent = "WebGL is not available in this browser";
      return;
    }

    renderer.setPixelRatio(
      Math.min(devicePixelRatio, innerWidth < 700 ? 1.4 : 1.75)
    );
    renderer.setSize(innerWidth, innerHeight);
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1.13;
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.setClearColor(0x07091c, 1);

    renderer.domElement.setAttribute("aria-label", "Space Bunny 3D artwork");
    stage.appendChild(renderer.domElement);

    const scene = new THREE.Scene();
    scene.fog = new THREE.FogExp2(0x171731, 0.0125);

    const camera = new THREE.PerspectiveCamera(
      innerWidth < 700 ? 51 : 43,
      innerWidth / innerHeight,
      0.1,
      220
    );

    // 第 1 层用于只出现在主镜头里的近景散景。
    camera.layers.enable(1);
    camera.position.set(5.5, 4.5, 16.4);

    const cameraLook = new THREE.Vector3(-0.35, 2.45, -0.9);
    const desiredCamera = new THREE.Vector3();
    const desiredLook = new THREE.Vector3();

    let seed = 74291;

    function random() {
      seed |= 0;
      seed = (seed + 0x6D2B79F5) | 0;
      let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
      t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    }

    function clamp01(v) {
      return Math.max(0, Math.min(1, v));
    }

    function smooth(v) {
      v = clamp01(v);
      return v * v * (3 - 2 * v);
    }

    function addMesh(
      parent,
      geometry,
      material,
      position = [0, 0, 0],
      scale = [1, 1, 1],
      castShadow = true
    ) {
      const mesh = new THREE.Mesh(geometry, material);
      mesh.position.set(...position);
      mesh.scale.set(...scale);
      mesh.castShadow = castShadow;
      mesh.receiveShadow = true;
      parent.add(mesh);
      return mesh;
    }

    function makeGlowTexture() {
      const canvas = document.createElement("canvas");
      canvas.width = canvas.height = 128;

      const ctx = canvas.getContext("2d");
      const gradient = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);

      gradient.addColorStop(0, "rgba(255,255,255,1)");
      gradient.addColorStop(0.13, "rgba(255,255,255,.82)");
      gradient.addColorStop(0.42, "rgba(255,255,255,.23)");
      gradient.addColorStop(1, "rgba(255,255,255,0)");

      ctx.fillStyle = gradient;
      ctx.fillRect(0, 0, 128, 128);

      const texture = new THREE.CanvasTexture(canvas);
      texture.colorSpace = THREE.SRGBColorSpace;
      return texture;
    }

    function makeTextTexture(text, subtitle = "") {
      const canvas = document.createElement("canvas");
      canvas.width = 1024;
      canvas.height = 256;

      const ctx = canvas.getContext("2d");
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";

      ctx.font = "italic 500 76px Georgia, serif";
      ctx.shadowColor = "rgba(120,255,235,.95)";
      ctx.shadowBlur = 24;
      ctx.fillStyle = "#d9ffff";
      ctx.fillText(text, 512, 110);

      ctx.shadowBlur = 8;
      ctx.fillStyle = "rgba(240,255,255,.92)";
      ctx.fillText(text, 512, 110);

      if (subtitle) {
        ctx.shadowBlur = 0;
        ctx.font = "500 18px Arial, sans-serif";
        ctx.letterSpacing = "5px";
        ctx.fillStyle = "rgba(180,225,235,.62)";
        ctx.fillText(subtitle, 512, 183);
      }

      const texture = new THREE.CanvasTexture(canvas);
      texture.colorSpace = THREE.SRGBColorSpace;
      texture.minFilter = THREE.LinearFilter;
      texture.magFilter = THREE.LinearFilter;
      texture.generateMipmaps = false;
      return texture;
    }

    const glowTexture = makeGlowTexture();

    // ------------------------------------------------------------
    // 深蓝到紫色的渐变天空
    // ------------------------------------------------------------

    const sky = new THREE.Mesh(
      new THREE.SphereGeometry(110, 48, 28),
      new THREE.ShaderMaterial({
        side: THREE.BackSide,
        depthWrite: false,
        uniforms: {
          uTime: { value: 0 }
        },
        vertexShader: `
          varying vec3 vDirection;

          void main() {
            vDirection = position;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          }
        `,
        fragmentShader: `
          precision highp float;

          uniform float uTime;
          varying vec3 vDirection;

          float hash(vec2 p) {
            return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
          }

          void main() {
            vec3 d = normalize(vDirection);
            float h = clamp(d.y * 0.5 + 0.5, 0.0, 1.0);

            vec3 horizon = vec3(0.050, 0.065, 0.205);
            vec3 middle  = vec3(0.075, 0.040, 0.210);
            vec3 zenith  = vec3(0.010, 0.008, 0.070);

            vec3 color = mix(horizon, middle, smoothstep(0.20, 0.54, h));
            color = mix(color, zenith, smoothstep(0.55, 1.0, h));

            float horizonGlow = pow(1.0 - abs(d.y), 5.0);
            color += vec3(0.16, 0.10, 0.25) * horizonGlow * 0.5;

            float veil =
              exp(-pow((h - 0.46) * 7.0, 2.0)) *
              (0.5 + 0.5 * sin(d.x * 17.0 + d.z * 9.0 + uTime * 0.025));
            color += vec3(0.13, 0.035, 0.19) * veil * 0.22;

            float grain = hash(gl_FragCoord.xy) - 0.5;
            color += grain * 0.004;

            gl_FragColor = vec4(color, 1.0);
            #include <tonemapping_fragment>
            #include <colorspace_fragment>
          }
        `
      })
    );

    sky.renderOrder = -20;
    scene.add(sky);

    // ------------------------------------------------------------
    // 星星、星云与远方珍珠月
    // ------------------------------------------------------------

    const starCount = innerWidth < 700 ? 650 : 1050;
    const starPositions = new Float32Array(starCount * 3);
    const starSizes = new Float32Array(starCount);
    const starPhases = new Float32Array(starCount);

    for (let i = 0; i < starCount; i++) {
      const theta = random() * TAU;
      const y = 0.03 + random() * 0.94;
      const radius = 73 + random() * 27;
      const horizontal = Math.sqrt(1 - y * y);

      starPositions[i * 3] = Math.cos(theta) * horizontal * radius;
      starPositions[i * 3 + 1] = y * radius;
      starPositions[i * 3 + 2] = Math.sin(theta) * horizontal * radius;
      starSizes[i] = 0.75 + random() * 2.0;
      starPhases[i] = random() * TAU;
    }

    const starGeometry = new THREE.BufferGeometry();
    starGeometry.setAttribute("position", new THREE.BufferAttribute(starPositions, 3));
    starGeometry.setAttribute("aSize", new THREE.BufferAttribute(starSizes, 1));
    starGeometry.setAttribute("aPhase", new THREE.BufferAttribute(starPhases, 1));

    const stars = new THREE.Points(
      starGeometry,
      new THREE.ShaderMaterial({
        transparent: true,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        uniforms: {
          uTime: { value: 0 }
        },
        vertexShader: `
          uniform float uTime;
          attribute float aSize;
          attribute float aPhase;
          varying float vAlpha;

          void main() {
            float twinkle = 0.62 + 0.38 * sin(uTime * 0.8 + aPhase);
            vAlpha = twinkle;
            gl_PointSize = aSize * (0.86 + twinkle * 0.24);
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          }
        `,
        fragmentShader: `
          varying float vAlpha;

          void main() {
            vec2 p = gl_PointCoord - 0.5;
            float d = length(p);
            float alpha = smoothstep(0.5, 0.0, d) * vAlpha;
            vec3 color = mix(vec3(0.50,0.69,1.0), vec3(1.0,0.88,0.75), vAlpha);
            gl_FragColor = vec4(color, alpha * 0.85);
            #include <tonemapping_fragment>
            #include <colorspace_fragment>
          }
        `
      })
    );

    stars.renderOrder = -10;
    scene.add(stars);

    function addNebula(position, scale, color, opacity) {
      const sprite = new THREE.Sprite(
        new THREE.SpriteMaterial({
          map: glowTexture,
          color,
          transparent: true,
          opacity,
          depthWrite: false,
          blending: THREE.AdditiveBlending,
          fog: false
        })
      );

      sprite.position.set(...position);
      sprite.scale.set(scale, scale * 0.62, 1);
      sprite.renderOrder = -8;
      scene.add(sprite);
      return sprite;
    }

    addNebula([-21, 15, -49], 34, 0x8758ff, 0.16);
    addNebula([25, 18, -57], 40, 0x537fff, 0.10);
    addNebula([2, 5, -65], 48, 0xc772ff, 0.08);

    const moon = new THREE.Mesh(
      new THREE.SphereGeometry(3.55, 48, 32),
      new THREE.ShaderMaterial({
        uniforms: {
          uTime: { value: 0 }
        },
        vertexShader: `
          varying vec3 vNormal;
          varying vec3 vPoint;

          void main() {
            vNormal = normalize(normal);
            vPoint = position;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          }
        `,
        fragmentShader: `
          precision highp float;
          uniform float uTime;
          varying vec3 vNormal;
          varying vec3 vPoint;

          void main() {
            vec3 n = normalize(vNormal);
            vec3 lightDirection = normalize(vec3(-0.5, 0.65, 0.8));
            float light = 0.32 + 0.68 * max(dot(n, lightDirection), 0.0);

            float terrain =
              sin(vPoint.x * 7.0 + sin(vPoint.z * 3.0)) *
              sin(vPoint.y * 9.0 - vPoint.x * 2.0);

            float craters =
              smoothstep(0.67, 0.96, sin(vPoint.x * 19.0) * sin(vPoint.z * 17.0));

            vec3 base = mix(
              vec3(0.46, 0.51, 0.80),
              vec3(0.80, 0.84, 1.0),
              light
            );

            base -= terrain * 0.035;
            base -= craters * 0.06;

            float rim = pow(1.0 - max(dot(n, vec3(0.0, 0.0, 1.0)), 0.0), 3.0);
            base += vec3(0.22, 0.18, 0.40) * rim * 0.16;

            gl_FragColor = vec4(base, 1.0);
            #include <tonemapping_fragment>
            #include <colorspace_fragment>
          }
        `
      })
    );

    moon.position.set(12, 12.3, -47);
    scene.add(moon);

    const moonHalo = new THREE.Sprite(
      new THREE.SpriteMaterial({
        map: glowTexture,
        color: 0x9aa6ff,
        transparent: true,
        opacity: 0.30,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        fog: false
      })
    );

    moonHalo.position.copy(moon.position);
    moonHalo.scale.set(15, 15, 1);
    scene.add(moonHalo);

    // ------------------------------------------------------------
    // 灯光:珍珠月光、冷色主光与紫色轮廓光
    // ------------------------------------------------------------

    scene.add(new THREE.HemisphereLight(0x9ebcff, 0x17122d, 1.35));

    const keyLight = new THREE.DirectionalLight(0xc7dcff, 2.8);
    keyLight.position.set(7, 14, 10);
    keyLight.castShadow = true;
    keyLight.shadow.mapSize.set(1024, 1024);
    keyLight.shadow.camera.left = -14;
    keyLight.shadow.camera.right = 14;
    keyLight.shadow.camera.top = 12;
    keyLight.shadow.camera.bottom = -8;
    keyLight.shadow.camera.near = 1;
    keyLight.shadow.camera.far = 50;
    keyLight.shadow.bias = -0.0005;
    scene.add(keyLight);
    scene.add(keyLight.target);

    const rimLight = new THREE.DirectionalLight(0xc178ff, 1.55);
    rimLight.position.set(-11, 6, -12);
    scene.add(rimLight);

    const cyanFill = new THREE.DirectionalLight(0x74ffe0, 0.72);
    cyanFill.position.set(-8, 4, 12);
    scene.add(cyanFill);

    // ------------------------------------------------------------
    // 具有体积感的紫色积云
    // ------------------------------------------------------------

    const cloudGeometry = new THREE.SphereGeometry(1, 18, 13);

    const cloudMaterials = [
      new THREE.MeshStandardMaterial({
        color: 0x9c78dc,
        roughness: 0.94,
        metalness: 0,
        emissive: 0x24104a,
        emissiveIntensity: 0.34
      }),
      new THREE.MeshStandardMaterial({
        color: 0xb091eb,
        roughness: 0.96,
        metalness: 0,
        emissive: 0x35205c,
        emissiveIntensity: 0.28
      }),
      new THREE.MeshStandardMaterial({
        color: 0x7657b8,
        roughness: 0.98,
        metalness: 0,
        emissive: 0x180b3c,
        emissiveIntensity: 0.38
      })
    ];

    const clouds = [];

    function createCloud(position, scale, phase) {
      const group = new THREE.Group();
      group.position.set(...position);
      group.scale.set(scale, scale * 0.72, scale);
      group.userData.base = group.position.clone();
      group.userData.phase = phase;
      group.userData.speed = 0.025 + random() * 0.035;

      const lobeCount = 9;

      for (let i = 0; i < lobeCount; i++) {
        const angle = (i / lobeCount) * TAU + random() * 0.3;
        const radius = random() * 1.15;
        const x = Math.cos(angle) * radius;
        const z = Math.sin(angle) * radius * 0.55;
        const y = random() * 0.65;

        const sx = 1.05 + random() * 1.25;
        const sy = 0.65 + random() * 0.8;
        const sz = 0.8 + random() * 0.95;

        const mat = i === 0
          ? cloudMaterials[2]
          : i % 3 === 0
            ? cloudMaterials[1]
            : cloudMaterials[0];

        addMesh(
          group,
          cloudGeometry,
          mat,
          [x, y, z],
          [sx, sy, sz],
          false
        );
      }

      addMesh(
        group,
        cloudGeometry,
        cloudMaterials[2],
        [0, -0.22, 0],
        [2.7, 0.65, 1.4],
        false
      );

      scene.add(group);
      clouds.push(group);
      return group;
    }

    createCloud([-20, 10, -31], 5.6, 0.1);
    createCloud([-7, 16, -48], 7.3, 1.4);
    createCloud([19, 13, -38], 6.5, 2.2);
    createCloud([27, 7.3, -13], 4.3, 3.3);
    createCloud([-15, 7.3, -7], 3.4, 4.0);
    createCloud([1, 19, -66], 8.4, 5.2);

    // 远处的漂浮海岸岩层。
    const rockGeometry = new THREE.DodecahedronGeometry(1, 0);
    const rockMaterial = new THREE.MeshStandardMaterial({
      color: 0x30264e,
      roughness: 0.92,
      metalness: 0.06,
      emissive: 0x120c28,
      emissiveIntensity: 0.32
    });

    const coastRockGroups = [];

    function createOutcrop(position, scale, phase) {
      const group = new THREE.Group();
      group.position.set(...position);
      group.scale.setScalar(scale);
      group.userData.base = group.position.clone();
      group.userData.phase = phase;

      for (let i = 0; i < 6; i++) {
        const angle = random() * TAU;
        const radius = random() * 1.45;

        addMesh(
          group,
          rockGeometry,
          rockMaterial,
          [
            Math.cos(angle) * radius,
            -0.1 + random() * 0.45,
            Math.sin(angle) * radius * 0.55
          ],
          [
            0.8 + random(),
            0.45 + random() * 0.8,
            0.7 + random()
          ],
          false
        );
      }

      const halo = addMesh(
        group,
        new THREE.TorusGeometry(1.75, 0.025, 6, 64),
        new THREE.MeshBasicMaterial({
          color: 0x7a6aff,
          transparent: true,
          opacity: 0.24,
          blending: THREE.AdditiveBlending,
          depthWrite: false
        }),
        [0, 0.22, 0],
        [1, 1, 1],
        false
      );

      halo.rotation.x = Math.PI / 2;
      scene.add(group);
      coastRockGroups.push(group);
    }

    createOutcrop([-15, 0.15, -14], 2.1, 0.2);
    createOutcrop([19, 0.05, -23], 1.6, 2.1);
    createOutcrop([-27, -0.05, -35], 2.8, 4.2);

    // ------------------------------------------------------------
    // 主体:白、绿、红未来战斗机
    // ------------------------------------------------------------

    const paintWhite = new THREE.MeshPhysicalMaterial({
      color: 0xf1f3ff,
      roughness: 0.27,
      metalness: 0.12,
      clearcoat: 0.72,
      clearcoatRoughness: 0.18
    });

    const paintGreen = new THREE.MeshPhysicalMaterial({
      color: 0x52dba1,
      roughness: 0.32,
      metalness: 0.18,
      clearcoat: 0.7
    });

    const paintRed = new THREE.MeshPhysicalMaterial({
      color: 0xf24b63,
      roughness: 0.34,
      metalness: 0.12,
      clearcoat: 0.62
    });

    const paintDark = new THREE.MeshStandardMaterial({
      color: 0x11172c,
      roughness: 0.31,
      metalness: 0.72
    });

    const cockpitMaterial = new THREE.MeshPhysicalMaterial({
      color: 0x132546,
      roughness: 0.08,
      metalness: 0.38,
      transparent: true,
      opacity: 0.9,
      clearcoat: 1,
      clearcoatRoughness: 0.05
    });

    const cyanGlow = new THREE.MeshBasicMaterial({
      color: 0x9efff0,
      toneMapped: false
    });

    const wingMaterial = paintWhite.clone();
    wingMaterial.side = THREE.DoubleSide;

    const wingGreenMaterial = paintGreen.clone();
    wingGreenMaterial.side = THREE.DoubleSide;

    const wingRedMaterial = paintRed.clone();
    wingRedMaterial.side = THREE.DoubleSide;

    const fighter = new THREE.Group();
    fighter.position.set(1.15, 3.25, -2.65);
    fighter.rotation.set(-0.015, -0.08, 0.012);
    scene.add(fighter);

    const fuselageProfile = [
      new V2(0.00, -3.30),
      new V2(0.25, -3.08),
      new V2(0.43, -2.72),
      new V2(0.58, -2.10),
      new V2(0.65, -0.80),
      new V2(0.66, 0.45),
      new V2(0.59, 1.35),
      new V2(0.48, 2.15),
      new V2(0.28, 2.65),
      new V2(0.00, 2.88)
    ];

    const fuselageGeometry = new THREE.LatheGeometry(fuselageProfile, 56);
    fuselageGeometry.rotateZ(-Math.PI / 2);
    addMesh(fighter, fuselageGeometry, paintWhite);

    addMesh(
      fighter,
      new THREE.SphereGeometry(1, 32, 20),
      paintGreen,
      [-0.15, -0.43, 0],
      [2.75, 0.32, 0.57]
    );

    addMesh(
      fighter,
      new THREE.BoxGeometry(2.6, 0.055, 0.12),
      paintRed,
      [-0.25, 0.62, 0]
    );

    function makeShape(points) {
      const shape = new THREE.Shape();
      shape.moveTo(points[0][0], points[0][1]);

      for (let i = 1; i < points.length; i++) {
        shape.lineTo(points[i][0], points[i][1]);
      }

      shape.closePath();
      return shape;
    }

    function horizontalExtrusion(points, depth, bevel = 0.025) {
      const geometry = new THREE.ExtrudeGeometry(makeShape(points), {
        depth,
        steps: 1,
        bevelEnabled: bevel > 0,
        bevelSegments: 2,
        bevelSize: bevel,
        bevelThickness: bevel
      });

      // Shape 的第二维作为世界 Z,厚度成为世界 Y。
      geometry.rotateX(Math.PI / 2);
      return geometry;
    }

    function makeWing(side) {
      const wingPoints = [
        [1.72, 0.24],
        [-0.16, 0.94],
        [-2.62, 3.00],
        [-2.23, 3.30],
        [0.18, 1.27],
        [-1.78, 0.23]
      ].map(([x, z]) => [x, z * side]);

      const group = new THREE.Group();

      addMesh(
        group,
        horizontalExtrusion(wingPoints, 0.13, 0.035),
        wingMaterial,
        [0, 0.085, 0]
      );

      const panelPoints = [
        [0.95, 0.43],
        [-0.10, 0.98],
        [-1.72, 2.45],
        [-0.90, 1.30],
        [-0.65, 0.54]
      ].map(([x, z]) => [x, z * side]);

      addMesh(
        group,
        horizontalExtrusion(panelPoints, 0.026, 0.012),
        wingGreenMaterial,
        [0, 0.122, 0],
        [1, 1, 1],
        false
      );

      const tipPoints = [
        [-1.95, 2.65],
        [-2.62, 3.00],
        [-2.23, 3.30],
        [-1.73, 3.08]
      ].map(([x, z]) => [x, z * side]);

      addMesh(
        group,
        horizontalExtrusion(tipPoints, 0.032, 0.01),
        wingRedMaterial,
        [0, 0.128, 0],
        [1, 1, 1],
        false
      );

      return group;
    }

    fighter.add(makeWing(1));
    fighter.add(makeWing(-1));

    // 座舱。
    addMesh(
      fighter,
      new THREE.SphereGeometry(1, 36, 22),
      cockpitMaterial,
      [1.18, 0.48, 0],
      [0.91, 0.43, 0.47]
    );

    addMesh(
      fighter,
      new THREE.SphereGeometry(1, 24, 14),
      new THREE.MeshBasicMaterial({
        color: 0x8ebaff,
        transparent: true,
        opacity: 0.25,
        depthWrite: false
      }),
      [1.45, 0.70, 0.20],
      [0.42, 0.045, 0.11],
      false
    );

    const frameRing = addMesh(
      fighter,
      new THREE.TorusGeometry(0.49, 0.026, 8, 40),
      paintDark,
      [1.12, 0.48, 0]
    );
    frameRing.rotation.y = Math.PI / 2;
    frameRing.scale.set(1, 0.80, 1);

    // 机身分段环。
    [-2.1, -0.65, 0.35].forEach((x, index) => {
      const ring = addMesh(
        fighter,
        new THREE.TorusGeometry(0.62, index === 1 ? 0.045 : 0.018, 8, 48),
        index === 1 ? paintRed : paintDark,
        [x, 0, 0]
      );
      ring.rotation.y = Math.PI / 2;
    });

    // 垂直尾翼。
    const finShape = new THREE.Shape();
    finShape.moveTo(-1.80, 0.12);
    finShape.lineTo(-2.55, 1.40);
    finShape.lineTo(-3.12, 1.48);
    finShape.lineTo(-3.34, 0.16);
    finShape.closePath();

    const fin = addMesh(
      fighter,
      new THREE.ExtrudeGeometry(finShape, {
        depth: 0.12,
        bevelEnabled: true,
        bevelSize: 0.025,
        bevelThickness: 0.02,
        bevelSegments: 2
      }),
      wingMaterial,
      [0, 0, -0.06]
    );
    fin.material.side = THREE.DoubleSide;

    const finPanel = new THREE.Shape();
    finPanel.moveTo(-2.37, 0.47);
    finPanel.lineTo(-2.72, 1.16);
    finPanel.lineTo(-3.02, 1.15);
    finPanel.lineTo(-2.95, 0.45);
    finPanel.closePath();

    addMesh(
      fighter,
      new THREE.ShapeGeometry(finPanel),
      wingGreenMaterial,
      [0, 0, 0.086],
      [1, 1, 1],
      false
    );

    // 水平尾翼。
    [-1, 1].forEach(side => {
      const points = [
        [-2.35, 0.17],
        [-3.13, 0.80],
        [-3.48, 0.75],
        [-3.28, 0.14]
      ].map(([x, z]) => [x, z * side]);

      addMesh(
        fighter,
        horizontalExtrusion(points, 0.075, 0.018),
        wingMaterial,
        [0, 0.03, 0]
      );
    });

    // 机腹炮与进气口。
    [-0.29, 0.29].forEach(z => {
      const cannon = addMesh(
        fighter,
        new THREE.CylinderGeometry(0.065, 0.085, 1.35, 14),
        paintDark,
        [2.15, -0.36, z]
      );
      cannon.rotation.z = Math.PI / 2;

      const intake = addMesh(
        fighter,
        new THREE.CylinderGeometry(0.23, 0.30, 1.05, 20),
        paintDark,
        [-2.12, -0.06, z]
      );
      intake.rotation.z = Math.PI / 2;
    });

    const fighterDecal = new THREE.Mesh(
      new THREE.PlaneGeometry(1.18, 0.31),
      new THREE.MeshBasicMaterial({
        map: makeTextTexture("SB—01", "DREAMER"),
        transparent: true,
        depthWrite: false,
        polygonOffset: true,
        polygonOffsetFactor: -2
      })
    );

    fighterDecal.position.set(-0.22, 0.17, 0.657);
    fighter.add(fighterDecal);

    // 发动机、尾焰与航行灯。
    const fighterFlames = [];
    const fighterThrusterLights = [];

    [-0.38, 0.38].forEach(z => {
      const engine = addMesh(
        fighter,
        new THREE.CylinderGeometry(0.24, 0.31, 0.95, 24),
        paintDark,
        [-3.13, -0.03, z]
      );
      engine.rotation.z = Math.PI / 2;

      const rim = addMesh(
        fighter,
        new THREE.TorusGeometry(0.265, 0.035, 8, 28),
        paintGreen,
        [-3.58, -0.03, z]
      );
      rim.rotation.y = Math.PI / 2;

      const glowDisc = addMesh(
        fighter,
        new THREE.CircleGeometry(0.20, 28),
        cyanGlow,
        [-3.61, -0.03, z],
        [1, 1, 1],
        false
      );
      glowDisc.rotation.y = -Math.PI / 2;
      glowDisc.userData.noShadow = true;

      const flameMaterial = new THREE.MeshBasicMaterial({
        color: z > 0 ? 0x8efff0 : 0xc2a8ff,
        transparent: true,
        opacity: 0.44,
        blending: THREE.AdditiveBlending,
        depthWrite: false,
        side: THREE.DoubleSide,
        toneMapped: false
      });

      const flame = addMesh(
        fighter,
        new THREE.ConeGeometry(0.17, 1.35, 18, 1, true),
        flameMaterial,
        [-4.18, -0.03, z],
        [1, 1, 1],
        false
      );

      flame.rotation.z = Math.PI / 2;
      flame.userData.noShadow = true;
      fighterFlames.push(flame);

      const light = new THREE.PointLight(0x7dffe5, 7, 7, 2);
      light.position.set(-3.8, 0, z);
      fighter.add(light);
      fighterThrusterLights.push(light);
    });

    const navGreen = new THREE.MeshBasicMaterial({ color: 0x75ffbc, toneMapped: false });
    const navRed = new THREE.MeshBasicMaterial({ color: 0xff5872, toneMapped: false });

    addMesh(
      fighter,
      new THREE.SphereGeometry(0.085, 12, 8),
      navRed,
      [-2.38, 0.14, -3.15],
      [1, 1, 1],
      false
    );

    addMesh(
      fighter,
      new THREE.SphereGeometry(0.085, 12, 8),
      navGreen,
      [-2.38, 0.14, 3.15],
      [1, 1, 1],
      false
    );

    // 发动机尾迹粒子。
    const exhaustCount = 90;
    const exhaustPositions = new Float32Array(exhaustCount * 3);
    const exhaustData = [];

    for (let i = 0; i < exhaustCount; i++) {
      exhaustData.push({
        seed: random(),
        lane: i % 2 === 0 ? -0.38 : 0.38,
        drift: random() - 0.5
      });
    }

    const exhaustGeometry = new THREE.BufferGeometry();
    exhaustGeometry.setAttribute(
      "position",
      new THREE.BufferAttribute(exhaustPositions, 3).setUsage(THREE.DynamicDrawUsage)
    );

    const exhaust = new THREE.Points(
      exhaustGeometry,
      new THREE.PointsMaterial({
        color: 0x8fffea,
        size: 0.085,
        transparent: true,
        opacity: 0.68,
        blending: THREE.AdditiveBlending,
        depthWrite: false,
        sizeAttenuation: true
      })
    );

    fighter.add(exhaust);

    // ------------------------------------------------------------
    // Space Bunny 主角
    // 白色兔头 + 飞行夹克 + 红色围巾 + 飞行目镜 +
    // 薄荷绿装备 + 小型喷气背包。
    // ------------------------------------------------------------

    const furMaterial = new THREE.MeshStandardMaterial({
      color: 0xf5f2ff,
      roughness: 0.88,
      metalness: 0,
      emissive: 0x17152b,
      emissiveIntensity: 0.08
    });

    const suitMaterial = new THREE.MeshPhysicalMaterial({
      color: 0xe9ecfb,
      roughness: 0.38,
      metalness: 0.08,
      clearcoat: 0.55
    });

    const bunnyPink = new THREE.MeshStandardMaterial({
      color: 0xf0a4cb,
      roughness: 0.86,
      emissive: 0x441a39,
      emissiveIntensity: 0.10
    });

    const scarfMaterial = new THREE.MeshPhysicalMaterial({
      color: 0xf04464,
      roughness: 0.62,
      clearcoat: 0.25,
      side: THREE.DoubleSide
    });

    const visorMaterial = new THREE.MeshPhysicalMaterial({
      color: 0x101b36,
      roughness: 0.08,
      metalness: 0.55,
      clearcoat: 1,
      clearcoatRoughness: 0.04
    });

    const eyeMaterial = new THREE.MeshBasicMaterial({
      color: 0xa8fff0,
      toneMapped: false
    });

    const unitSphere = new THREE.SphereGeometry(1, 24, 16);

    function addSphere(parent, material, position, scale, shadow = true) {
      return addMesh(parent, unitSphere, material, position, scale, shadow);
    }

    const bunnyRoot = new THREE.Group();
    bunnyRoot.scale.setScalar(0.82);
    scene.add(bunnyRoot);

    const bunnyModel = new THREE.Group();
    bunnyRoot.add(bunnyModel);

    const bunnyUpper = new THREE.Group();
    bunnyModel.add(bunnyUpper);

    addSphere(
      bunnyUpper,
      suitMaterial,
      [0, 1.18, 0],
      [0.51, 0.61, 0.43]
    );

    addSphere(
      bunnyUpper,
      furMaterial,
      [0.40, 1.16, 0],
      [0.11, 0.40, 0.31]
    );

    addSphere(
      bunnyUpper,
      paintGreen,
      [-0.38, 1.20, 0.16],
      [0.19, 0.46, 0.19]
    );

    addSphere(
      bunnyUpper,
      paintGreen,
      [-0.38, 1.20, -0.16],
      [0.19, 0.46, 0.19]
    );

    const collar = addMesh(
      bunnyUpper,
      new THREE.TorusGeometry(0.34, 0.065, 8, 28),
      paintGreen,
      [0, 1.61, 0]
    );
    collar.rotation.x = Math.PI / 2;

    const belt = addMesh(
      bunnyUpper,
      new THREE.TorusGeometry(0.42, 0.045, 8, 28),
      paintRed,
      [0, 0.95, 0]
    );
    belt.rotation.x = Math.PI / 2;
    belt.scale.set(1.0, 1.0, 0.84);

    const chestBadge = addMesh(
      bunnyUpper,
      new THREE.CylinderGeometry(0.105, 0.105, 0.025, 20),
      paintRed,
      [0.49, 1.40, 0.10]
    );
    chestBadge.rotation.z = Math.PI / 2;

    addMesh(
      bunnyUpper,
      new THREE.BoxGeometry(0.02, 0.54, 0.025),
      paintDark,
      [0.51, 1.16, -0.07]
    );

    addSphere(
      bunnyUpper,
      furMaterial,
      [-0.53, 0.98, 0],
      [0.22, 0.22, 0.22]
    );

    const head = new THREE.Group();
    head.position.set(0, 2.02, 0);
    bunnyUpper.add(head);

    addSphere(head, furMaterial, [0, 0, 0], [0.59, 0.55, 0.52]);
    addSphere(head, furMaterial, [0.45, -0.09, 0], [0.25, 0.20, 0.31]);
    addSphere(head, bunnyPink, [0.67, -0.06, 0], [0.10, 0.075, 0.095]);

    // 未来飞行目镜。
    const goggleBand = addMesh(
      head,
      new THREE.TorusGeometry(0.515, 0.028, 8, 48),
      visorMaterial,
      [-0.015, 0.12, 0]
    );
    goggleBand.rotation.y = Math.PI / 2;
    goggleBand.scale.set(1, 0.88, 1);

    [-1, 1].forEach(side => {
      addSphere(
        head,
        visorMaterial,
        [0.45, 0.14, side * 0.19],
        [0.13, 0.16, 0.18]
      );

      addSphere(
        head,
        eyeMaterial,
        [0.555, 0.15, side * 0.19],
        [0.035, 0.085, 0.068],
        false
      );

      addSphere(
        head,
        new THREE.MeshBasicMaterial({ color: 0xffffff, toneMapped: false }),
        [0.582, 0.19, side * 0.19 - 0.018],
        [0.012, 0.025, 0.018],
        false
      );

      addSphere(
        head,
        bunnyPink,
        [0.48, -0.11, side * 0.31],
        [0.055, 0.034, 0.07],
        false
      );
    });

    const mouthCurve = new THREE.CatmullRomCurve3([
      new V3(0.64, -0.16, 0),
      new V3(0.60, -0.205, 0),
      new V3(0.54, -0.17, 0)
    ]);

    addMesh(
      head,
      new THREE.TubeGeometry(mouthCurve, 12, 0.014, 6, false),
      paintDark,
      [0, 0, 0],
      [1, 1, 1],
      false
    );

    const whiskerMaterial = new THREE.LineBasicMaterial({
      color: 0xb7bed4,
      transparent: true,
      opacity: 0.46
    });

    [-1, 1].forEach(side => {
      for (let i = 0; i < 2; i++) {
        const points = [
          new V3(0.56, -0.08 + i * 0.045, side * 0.27),
          new V3(0.79, -0.09 + i * 0.07, side * 0.39),
          new V3(0.95, -0.06 + i * 0.08, side * 0.48)
        ];

        const line = new THREE.Line(
          new THREE.BufferGeometry().setFromPoints(points),
          whiskerMaterial
        );
        head.add(line);
      }
    });

    // 额前的小绒毛。
    addSphere(head, furMaterial, [-0.02, 0.52, -0.15], [0.14, 0.20, 0.12]);
    addSphere(head, furMaterial, [-0.07, 0.50, 0.10], [0.12, 0.19, 0.11]);

    // 长耳朵由两段可摆动的软体关节组成。
    const ears = [];

    function createEar(side) {
      const base = new THREE.Group();
      base.position.set(-0.04, 0.40, side * 0.27);
      head.add(base);

      const lower = new THREE.Group();
      base.add(lower);

      addSphere(lower, furMaterial, [0, 0.43, 0], [0.18, 0.54, 0.21]);
      addSphere(lower, bunnyPink, [0.12, 0.44, 0], [0.065, 0.39, 0.13], false);

      const upper = new THREE.Group();
      upper.position.set(0, 0.81, 0);
      lower.add(upper);

      addSphere(upper, furMaterial, [0, 0.40, 0], [0.15, 0.49, 0.18]);
      addSphere(upper, bunnyPink, [0.105, 0.40, 0], [0.055, 0.35, 0.11], false);

      addSphere(
        upper,
        side > 0 ? paintGreen : paintRed,
        [0, 0.86, 0],
        [0.065, 0.09, 0.075],
        false
      );

      ears.push({
        base,
        lower,
        upper,
        side,
        phase: side > 0 ? 0.9 : 2.4
      });
    }

    createEar(-1);
    createEar(1);

    // 小型喷气背包。
    addSphere(
      bunnyUpper,
      paintDark,
      [-0.46, 1.33, 0],
      [0.27, 0.43, 0.34]
    );

    addSphere(
      bunnyUpper,
      paintGreen,
      [-0.59, 1.42, 0],
      [0.10, 0.25, 0.26]
    );

    const bunnyFlames = [];
    const bunnyThrusterMaterial = new THREE.MeshBasicMaterial({
      color: 0x8affdf,
      transparent: true,
      opacity: 0.28,
      blending: THREE.AdditiveBlending,
      depthWrite: false,
      side: THREE.DoubleSide,
      toneMapped: false
    });

    [-0.18, 0.18].forEach(z => {
      const nozzle = addMesh(
        bunnyUpper,
        new THREE.CylinderGeometry(0.09, 0.12, 0.28, 14),
        paintDark,
        [-0.68, 1.22, z]
      );
      nozzle.rotation.z = Math.PI / 2;

      const glow = addMesh(
        bunnyUpper,
        new THREE.CircleGeometry(0.08, 16),
        cyanGlow,
        [-0.825, 1.22, z],
        [1, 1, 1],
        false
      );
      glow.rotation.y = -Math.PI / 2;

      const flame = addMesh(
        bunnyUpper,
        new THREE.ConeGeometry(0.07, 0.48, 12, 1, true),
        bunnyThrusterMaterial,
        [-0.98, 1.22, z],
        [1, 0.5, 1],
        false
      );

      flame.rotation.z = Math.PI / 2;
      flame.userData.noShadow = true;
      bunnyFlames.push(flame);
    });

    const bunnyThrusterLight = new THREE.PointLight(0x7fffe4, 1.4, 3.2, 2);
    bunnyThrusterLight.position.set(-0.88, 1.22, 0);
    bunnyUpper.add(bunnyThrusterLight);

    // 红色围巾。
    const scarfNeck = addMesh(
      bunnyUpper,
      new THREE.TorusGeometry(0.35, 0.065, 8, 28),
      scarfMaterial,
      [0, 1.55, 0]
    );
    scarfNeck.rotation.x = Math.PI / 2;

    const scarfSegments = [];

    const scarfOne = new THREE.Group();
    scarfOne.position.set(-0.23, 1.54, 0.04);
    bunnyUpper.add(scarfOne);

    addMesh(
      scarfOne,
      new THREE.BoxGeometry(0.55, 0.10, 0.035),
      scarfMaterial,
      [-0.25, -0.015, 0]
    );

    const scarfTwo = new THREE.Group();
    scarfTwo.position.set(-0.52, 0, 0);
    scarfOne.add(scarfTwo);

    addMesh(
      scarfTwo,
      new THREE.BoxGeometry(0.46, 0.075, 0.03),
      scarfMaterial,
      [-0.21, -0.04, 0]
    );

    scarfSegments.push(scarfOne, scarfTwo);

    // 手臂。
    const bunnyArms = [];

    [-1, 1].forEach(side => {
      const arm = new THREE.Group();
      arm.position.set(0, 1.43, side * 0.45);
      bunnyUpper.add(arm);

      addSphere(arm, suitMaterial, [0, 0, 0], [0.17, 0.19, 0.17]);

      addMesh(
        arm,
        new THREE.CapsuleGeometry(0.125, 0.31, 5, 10),
        suitMaterial,
        [0, -0.25, 0]
      );

      const cuff = addMesh(
        arm,
        new THREE.TorusGeometry(0.13, 0.032, 7, 18),
        paintGreen,
        [0, -0.43, 0]
      );
      cuff.rotation.x = Math.PI / 2;

      addSphere(arm, furMaterial, [0.02, -0.56, 0], [0.15, 0.15, 0.14]);

      bunnyArms.push({ arm, side });
    });

    // 腿部与未来感软靴。
    const bunnyLegs = [];

    [-1, 1].forEach(side => {
      const leg = new THREE.Group();
      leg.position.set(0, 0.72, side * 0.23);
      bunnyModel.add(leg);

      addMesh(
        leg,
        new THREE.CapsuleGeometry(0.14, 0.31, 5, 10),
        suitMaterial,
        [0, -0.20, 0]
      );

      addSphere(
        leg,
        paintGreen,
        [0.06, -0.51, 0],
        [0.30, 0.17, 0.20]
      );

      addMesh(
        leg,
        new THREE.BoxGeometry(0.37, 0.045, 0.24),
        paintDark,
        [0.09, -0.655, 0]
      );

      const lightStrip = addMesh(
        leg,
        new THREE.BoxGeometry(0.18, 0.018, 0.08),
        cyanGlow,
        [0.12, -0.51, 0.18],
        [1, 1, 1],
        false
      );
      lightStrip.rotation.x = -0.1;

      bunnyLegs.push({ leg, side });
    });

    bunnyModel.traverse(object => {
      if (object.isMesh && !object.userData.noShadow) {
        object.castShadow = true;
        object.receiveShadow = true;
      }
    });

    // ------------------------------------------------------------
    // 海边休息平台
    // ------------------------------------------------------------

    const platformCenter = new V3(-4.24, 0, 4.58);
    const P0 = new V3(-4.83, 0.635, 4.68);
    const P1 = new V3(-3.62, 0.635, 4.50);

    const platform = new THREE.Group();
    platform.position.set(platformCenter.x, 0, platformCenter.z);
    scene.add(platform);

    addMesh(
      platform,
      new THREE.CylinderGeometry(1.62, 1.48, 0.22, 48),
      paintWhite,
      [0, 0.50, 0]
    );

    addMesh(
      platform,
      new THREE.CylinderGeometry(1.12, 1.50, 0.32, 40),
      paintDark,
      [0, 0.24, 0]
    );

    const platformRing = addMesh(
      platform,
      new THREE.TorusGeometry(1.42, 0.055, 8, 64),
      cyanGlow,
      [0, 0.62, 0],
      [1, 1, 1],
      false
    );
    platformRing.rotation.x = Math.PI / 2;

    const innerRing = addMesh(
      platform,
      new THREE.TorusGeometry(0.77, 0.018, 6, 56),
      new THREE.MeshBasicMaterial({
        color: 0x9f7cff,
        transparent: true,
        opacity: 0.52,
        blending: THREE.AdditiveBlending,
        depthWrite: false
      }),
      [0, 0.628, 0],
      [1, 1, 1],
      false
    );
    innerRing.rotation.x = Math.PI / 2;

    for (let i = 0; i < 6; i++) {
      const angle = (i / 6) * TAU;
      addMesh(
        platform,
        new THREE.BoxGeometry(0.43, 0.018, 0.035),
        paintGreen,
        [Math.cos(angle) * 1.15, 0.626, Math.sin(angle) * 1.15],
        [1, 1, 1],
        false
      ).rotation.y = -angle;
    }

    const platformLabel = new THREE.Mesh(
      new THREE.PlaneGeometry(0.73, 0.18),
      new THREE.MeshBasicMaterial({
        map: makeTextTexture("REST / 01", ""),
        transparent: true,
        depthWrite: false
      })
    );

    platformLabel.position.set(0, 0.638, 0.20);
    platformLabel.rotation.x = -Math.PI / 2;
    platform.add(platformLabel);

    [-1, 1].forEach(side => {
      const antenna = addMesh(
        platform,
        new THREE.CylinderGeometry(0.018, 0.028, 0.68, 8),
        paintDark,
        [side * 1.22, 0.91, -0.35]
      );

      addSphere(
        platform,
        side < 0 ? navRed : navGreen,
        [side * 1.22, 1.25, -0.35],
        [0.045, 0.045, 0.045],
        false
      );
    });

    // ------------------------------------------------------------
    // 漂浮标语:just breathe
    // ------------------------------------------------------------

    const titleGroup = new THREE.Group();
    titleGroup.position.set(-3.8, 9.0, -27.5);
    titleGroup.rotation.y = 0.035;
    scene.add(titleGroup);

    const titleTexture = makeTextTexture("just breathe", "A SMALL PLACE BEYOND THE SKY");

    const titlePlane = new THREE.Mesh(
      new THREE.PlaneGeometry(10.4, 2.6),
      new THREE.MeshBasicMaterial({
        map: titleTexture,
        transparent: true,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        toneMapped: false,
        fog: false
      })
    );
    titleGroup.add(titlePlane);

    const titleHalo = new THREE.Mesh(
      new THREE.PlaneGeometry(11.5, 3.15),
      new THREE.MeshBasicMaterial({
        map: titleTexture,
        color: 0x6fffe0,
        transparent: true,
        opacity: 0.09,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        toneMapped: false,
        fog: false
      })
    );
    titleHalo.position.z = -0.02;
    titleGroup.add(titleHalo);

    const titleOrbit = addMesh(
      titleGroup,
      new THREE.TorusGeometry(4.65, 0.015, 6, 96),
      new THREE.MeshBasicMaterial({
        color: 0x93d9ff,
        transparent: true,
        opacity: 0.25,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        toneMapped: false,
        fog: false
      }),
      [0, 0, -0.08],
      [1, 1, 1],
      false
    );

    for (let i = 0; i < 3; i++) {
      const angle = i * TAU / 3 + 0.4;
      addSphere(
        titleGroup,
        cyanGlow,
        [Math.cos(angle) * 4.65, Math.sin(angle) * 4.65, -0.08],
        [0.035, 0.035, 0.035],
        false
      );
    }

    // ------------------------------------------------------------
    // 平台与机翼之间的“梦境航线”
    // ------------------------------------------------------------

    fighter.updateMatrixWorld(true);

    function wingPoint(x = 0, z = 1.5) {
      return new V3(x, 0.145, z).applyMatrix4(fighter.matrixWorld);
    }

    const initialW0 = wingPoint(-0.55, 1.50);
    const initialW1 = wingPoint(0.45, 1.50);

    const staticRouteOut = new THREE.CubicBezierCurve3(
      P1.clone(),
      P1.clone().add(new V3(0.45, 2.8, -0.15)),
      initialW0.clone().add(new V3(-0.70, 1.35, 0.80)),
      initialW0.clone()
    );

    const staticRouteBack = new THREE.CubicBezierCurve3(
      initialW1.clone(),
      initialW1.clone().add(new V3(-0.8, 1.4, -0.4)),
      P1.clone().add(new V3(0.9, 2.4, 0.9)),
      P1.clone()
    );

    const routeLineMaterial = new THREE.LineBasicMaterial({
      color: 0x79ffe2,
      transparent: true,
      opacity: 0.15,
      depthWrite: false,
      blending: THREE.AdditiveBlending
    });

    function makeRouteLine(curve, opacity) {
      const points = curve.getPoints(64);
      const geometry = new THREE.BufferGeometry().setFromPoints(points);
      const material = routeLineMaterial.clone();
      material.opacity = opacity;

      const line = new THREE.Line(geometry, material);
      scene.add(line);
      return line;
    }

    makeRouteLine(staticRouteOut, 0.16);
    makeRouteLine(staticRouteBack, 0.09);

    const routeParticleCount = 34;
    const routeParticlePositions = new Float32Array(routeParticleCount * 3);
    const routeParticleGeometry = new THREE.BufferGeometry();
    routeParticleGeometry.setAttribute(
      "position",
      new THREE.BufferAttribute(routeParticlePositions, 3).setUsage(THREE.DynamicDrawUsage)
    );

    const routeParticles = new THREE.Points(
      routeParticleGeometry,
      new THREE.PointsMaterial({
        color: 0xa4ffe8,
        size: 0.075,
        transparent: true,
        opacity: 0.82,
        blending: THREE.AdditiveBlending,
        depthWrite: false,
        sizeAttenuation: true
      })
    );
    scene.add(routeParticles);

    // ------------------------------------------------------------
    // 近景柔光散景与空气尘埃
    // ------------------------------------------------------------

    const bokehSprites = [];

    for (let i = 0; i < 14; i++) {
      const sprite = new THREE.Sprite(
        new THREE.SpriteMaterial({
          map: glowTexture,
          color: i % 2 === 0 ? 0x9c8cff : 0x85ffe2,
          transparent: true,
          opacity: 0.035 + random() * 0.045,
          depthWrite: false,
          blending: THREE.AdditiveBlending
        })
      );

      sprite.position.set(
        (random() - 0.5) * 22,
        1 + random() * 11,
        5 + random() * 10
      );

      const size = 0.2 + random() * 0.65;
      sprite.scale.set(size, size, 1);
      sprite.userData.base = sprite.position.clone();
      sprite.userData.phase = random() * TAU;
      sprite.layers.set(1);

      scene.add(sprite);
      bokehSprites.push(sprite);
    }

    const dustCount = 130;
    const dustPositions = new Float32Array(dustCount * 3);

    for (let i = 0; i < dustCount; i++) {
      dustPositions[i * 3] = (random() - 0.5) * 28;
      dustPositions[i * 3 + 1] = 0.6 + random() * 13;
      dustPositions[i * 3 + 2] = -20 + random() * 36;
    }

    const dustGeometry = new THREE.BufferGeometry();
    dustGeometry.setAttribute("position", new THREE.BufferAttribute(dustPositions, 3));

    const dust = new THREE.Points(
      dustGeometry,
      new THREE.PointsMaterial({
        color: 0xc8bcff,
        size: 0.035,
        transparent: true,
        opacity: 0.36,
        blending: THREE.AdditiveBlending,
        depthWrite: false
      })
    );

    dust.layers.set(1);
    scene.add(dust);

    // ------------------------------------------------------------
    // 角色接触阴影与落地光圈
    // ------------------------------------------------------------

    const shadowCanvas = document.createElement("canvas");
    shadowCanvas.width = shadowCanvas.height = 128;
    const shadowContext = shadowCanvas.getContext("2d");
    const shadowGradient = shadowContext.createRadialGradient(64, 64, 0, 64, 64, 64);
    shadowGradient.addColorStop(0, "rgba(255,255,255,.9)");
    shadowGradient.addColorStop(0.40, "rgba(255,255,255,.45)");
    shadowGradient.addColorStop(1, "rgba(255,255,255,0)");
    shadowContext.fillStyle = shadowGradient;
    shadowContext.fillRect(0, 0, 128, 128);

    const bunnyShadowTexture = new THREE.CanvasTexture(shadowCanvas);

    const bunnyShadow = new THREE.Mesh(
      new THREE.PlaneGeometry(1.35, 0.95),
      new THREE.MeshBasicMaterial({
        map: bunnyShadowTexture,
        color: 0x080919,
        transparent: true,
        opacity: 0.30,
        depthWrite: false
      })
    );

    bunnyShadow.rotation.x = -Math.PI / 2;
    bunnyShadow.renderOrder = 2;
    scene.add(bunnyShadow);

    const landingRings = [];

    for (let i = 0; i < 5; i++) {
      const ring = new THREE.Mesh(
        new THREE.RingGeometry(0.42, 0.47, 48),
        new THREE.MeshBasicMaterial({
          color: 0x9effe6,
          transparent: true,
          opacity: 0,
          side: THREE.DoubleSide,
          depthWrite: false,
          blending: THREE.AdditiveBlending,
          toneMapped: false
        })
      );

      ring.rotation.x = -Math.PI / 2;
      ring.userData.age = 10;
      scene.add(ring);
      landingRings.push(ring);
    }

    function spawnLanding(position) {
      const ring = landingRings.find(item => item.userData.age > 0.8) || landingRings[0];
      ring.position.copy(position);
      ring.position.y += 0.025;
      ring.scale.setScalar(0.65);
      ring.material.opacity = 0.75;
      ring.userData.age = 0;
    }

    // ------------------------------------------------------------
    // 真实平面反射海面
    // ------------------------------------------------------------

    const isMobile = innerWidth < 700;
    const reflectionTarget = new THREE.WebGLRenderTarget(
      isMobile ? 512 : 768,
      isMobile ? 384 : 576,
      {
        minFilter: THREE.LinearFilter,
        magFilter: THREE.LinearFilter,
        format: THREE.RGBAFormat,
        depthBuffer: true
      }
    );

    reflectionTarget.texture.generateMipmaps = false;

    const mirrorCamera = new THREE.PerspectiveCamera();
    mirrorCamera.layers.set(0);
    scene.add(mirrorCamera);

    const reflectionTextureMatrix = new THREE.Matrix4();

    const waterMaterial = new THREE.ShaderMaterial({
      uniforms: {
        uTime: { value: 0 },
        uReflection: { value: reflectionTarget.texture },
        uTextureMatrix: { value: reflectionTextureMatrix },
        uPlatform: { value: new V2(platformCenter.x, platformCenter.z) }
      },
      vertexShader: `
        precision highp float;

        uniform float uTime;
        uniform mat4 uTextureMatrix;

        varying vec4 vReflectionUv;
        varying vec3 vWorldPosition;

        void main() {
          vec3 p = position;

          float waveA =
            sin(p.x * 0.29 + p.y * 0.13 + uTime * 0.68) * 0.065;
          float waveB =
            sin(p.x * 0.57 - p.y * 0.31 - uTime * 0.91) * 0.036;
          float waveC =
            sin(p.x * 1.11 + p.y * 0.72 + uTime * 1.24) * 0.016;

          p.z += waveA + waveB + waveC;

          vec4 worldPosition = modelMatrix * vec4(p, 1.0);
          vWorldPosition = worldPosition.xyz;
          vReflectionUv = uTextureMatrix * vec4(p, 1.0);

          gl_Position = projectionMatrix * viewMatrix * worldPosition;
        }
      `,
      fragmentShader: `
        precision highp float;

        uniform sampler2D uReflection;
        uniform float uTime;
        uniform vec2 uPlatform;

        varying vec4 vReflectionUv;
        varying vec3 vWorldPosition;

        void main() {
          vec2 uv = vReflectionUv.xy / max(vReflectionUv.w, 0.0001);

          float rippleA =
            sin(vWorldPosition.x * 0.62 + vWorldPosition.z * 0.31 + uTime * 0.78);
          float rippleB =
            sin(vWorldPosition.x * 1.24 - vWorldPosition.z * 0.73 - uTime * 0.96);

          uv += vec2(rippleA, rippleB) * 0.0038;

          vec3 reflection = texture2D(
            uReflection,
            clamp(uv, vec2(0.002), vec2(0.998))
          ).rgb;

          vec3 viewDirection = normalize(cameraPosition - vWorldPosition);
          float fresnel = pow(
            1.0 - clamp(viewDirection.y, 0.0, 1.0),
            2.15
          );

          vec3 deepWater = vec3(0.008, 0.024, 0.082);
          vec3 color = deepWater * 0.58;
          color += reflection * (0.27 + fresnel * 0.78);

          float glint = pow(
            max(0.0, rippleA * rippleB),
            4.0
          );
          color += vec3(0.25, 0.82, 0.77) * glint * 0.20;

          float platformDistance = length(
            vWorldPosition.xz - uPlatform
          );
          float platformRipple =
            sin(platformDistance * 9.0 - uTime * 2.3) *
            exp(-platformDistance * 1.65);
          color += vec3(0.10, 0.65, 0.61) *
            max(0.0, platformRipple) * 0.20;

          float distanceFog = 1.0 - exp(-length(vWorldPosition.xz) * 0.009);
          color = mix(
            color,
            vec3(0.045, 0.048, 0.145),
            distanceFog * 0.68
          );

          gl_FragColor = vec4(color, 1.0);
          #include <tonemapping_fragment>
          #include <colorspace_fragment>
        }
      `
    });

    const water = new THREE.Mesh(
      new THREE.PlaneGeometry(300, 300, 128, 128),
      waterMaterial
    );

    water.rotation.x = -Math.PI / 2;
    water.position.y = 0;
    water.receiveShadow = false;
    scene.add(water);

    // ------------------------------------------------------------
    // 动画工具
    // ------------------------------------------------------------

    function cubicBezier(out, a, b, c, d, t) {
      const u = 1 - t;
      const u2 = u * u;
      const t2 = t * t;

      out.set(
        u2 * u * a.x + 3 * u2 * t * b.x + 3 * u * t2 * c.x + t2 * t * d.x,
        u2 * u * a.y + 3 * u2 * t * b.y + 3 * u * t2 * c.y + t2 * t * d.y,
        u2 * u * a.z + 3 * u2 * t * b.z + 3 * u * t2 * c.z + t2 * t * d.z
      );

      return out;
    }

    function hopSample(f) {
      const u = (f - 0.13) / 0.74;

      if (u <= 0 || u >= 1) {
        return { h: 0, u: 0 };
      }

      return {
        h: Math.sin(Math.PI * u) * 0.78,
        u
      };
    }

    const pose = {
      position: new V3(),
      ground: new V3(),
      stage: "wing",
      surface: "wing",
      height: 0,
      hopU: 0,
      air: false,
      thrust: false,
      yaw: 0,
      pitch: 0,
      bank: 0
    };

    function getBunnyPose(time) {
      // 从 20 秒循环的第 10 秒开始,让首屏先看到机翼上的 Bunny。
      const t = (time + 10) % 20;
      const hopDuration = 5 / 3;

      if (t < 5) {
        const progress = t / 5;
        const f = (t / hopDuration) % 1;
        const hop = hopSample(f);

        pose.position.set(
          THREE.MathUtils.lerp(P0.x, P1.x, progress),
          P0.y + hop.h,
          THREE.MathUtils.lerp(P0.z, P1.z, progress) +
            Math.sin(progress * Math.PI) * 0.10
        );

        pose.ground.set(pose.position.x, P0.y, pose.position.z);
        pose.stage = "platform";
        pose.surface = "platform";
        pose.height = hop.h;
        pose.hopU = hop.u;
        pose.air = hop.h > 0.015;
        pose.thrust = false;
        pose.yaw = -0.32;
        pose.pitch = -0.02;
        pose.bank = Math.sin(progress * Math.PI * 2) * 0.035;
      } else if (t < 9) {
        const q = (t - 5) / 4;
        const wing = wingPoint(-0.55, 1.50);
        const c1 = P1.clone().add(new V3(0.45, 2.8, -0.15));
        const c2 = wing.clone().add(new V3(-0.70, 1.35, 0.80));

        cubicBezier(pose.position, P1, c1, c2, wing, smooth(q));
        pose.ground.set(0, 0, 0);
        pose.stage = "transfer";
        pose.surface = null;
        pose.height = 0;
        pose.hopU = q;
        pose.air = true;
        pose.thrust = true;
        pose.yaw = THREE.MathUtils.lerp(-0.32, fighter.rotation.y + 0.02, smooth(q));
        pose.pitch = Math.sin(q * TAU) * 0.14;
        pose.bank = Math.sin(q * Math.PI) * 0.12;
      } else if (t < 14) {
        const progress = (t - 9) / 5;
        const f = ((t - 9) / hopDuration) % 1;
        const hop = hopSample(f);

        const localX = THREE.MathUtils.lerp(-0.55, 0.45, progress);
        const localZ = 1.50 + Math.sin(progress * Math.PI) * 0.12;

        pose.ground.copy(wingPoint(localX, localZ));
        pose.position.copy(pose.ground);
        pose.position.y += hop.h;

        pose.stage = "wing";
        pose.surface = "wing";
        pose.height = hop.h;
        pose.hopU = hop.u;
        pose.air = hop.h > 0.015;
        pose.thrust = false;
        pose.yaw = fighter.rotation.y + 0.02;
        pose.pitch = fighter.rotation.x;
        pose.bank = fighter.rotation.z * 0.55;
      } else if (t < 18) {
        const q = (t - 14) / 4;
        const wing = wingPoint(0.45, 1.50);
        const c1 = wing.clone().add(new V3(-0.8, 1.4, -0.4));
        const c2 = P1.clone().add(new V3(0.9, 2.4, 0.9));

        cubicBezier(pose.position, wing, c1, c2, P1, smooth(q));
        pose.ground.set(0, 0, 0);
        pose.stage = "return";
        pose.surface = null;
        pose.height = 0;
        pose.hopU = q;
        pose.air = true;
        pose.thrust = true;
        pose.yaw = THREE.MathUtils.lerp(fighter.rotation.y + 0.02, -0.32, smooth(q));
        pose.pitch = -Math.sin(q * TAU) * 0.14;
        pose.bank = -Math.sin(q * Math.PI) * 0.12;
      } else {
        pose.position.copy(P1);
        pose.ground.copy(P1);
        pose.stage = "platform";
        pose.surface = "platform";
        pose.height = 0;
        pose.hopU = 0;
        pose.air = false;
        pose.thrust = false;
        pose.yaw = -0.32;
        pose.pitch = 0;
        pose.bank = 0;
      }

      return pose;
    }

    let previousBunnyPose = null;
    let previousBunnyY = 0;
    let currentStretch = 0;
    let bunnyThrust = 0;

    function updateFighter(time) {
      const motion = reducedMotion ? 0.35 : 1;

      fighter.position.set(
        1.15 + Math.sin(time * 0.29) * 0.18 * motion,
        3.24 + Math.sin(time * 0.73) * 0.15 * motion,
        -2.65 + Math.cos(time * 0.31) * 0.13 * motion
      );

      fighter.rotation.set(
        -0.018 + Math.sin(time * 0.43) * 0.018 * motion,
        -0.08 + Math.sin(time * 0.27) * 0.034 * motion,
        0.014 + Math.sin(time * 0.61) * 0.027 * motion
      );

      const pulse = 0.5 + 0.5 * Math.sin(time * 5.1);
      const pulse2 = 0.5 + 0.5 * Math.sin(time * 4.2 + 1.2);

      fighterFlames.forEach((flame, index) => {
        const p = index === 0 ? pulse : pulse2;
        flame.scale.y = 0.68 + p * 0.75;
        flame.material.opacity = 0.30 + p * 0.20;
      });

      fighterThrusterLights.forEach((light, index) => {
        light.intensity = 5.2 + (index === 0 ? pulse : pulse2) * 3.2;
      });

      const positionAttribute = exhaustGeometry.getAttribute("position");

      for (let i = 0; i < exhaustCount; i++) {
        const data = exhaustData[i];
        const life = (time * 0.22 + data.seed) % 1;
        const spread = life * life * 0.24;

        positionAttribute.setXYZ(
          i,
          -3.65 - life * 7.6,
          Math.sin(life * 8 + data.seed * TAU) * spread * 0.35,
          data.lane + data.drift * spread
        );
      }

      positionAttribute.needsUpdate = true;
    }

    function updateBunny(time, dt) {
      updateFighter(time);
      fighter.updateMatrixWorld(true);

      const currentPose = getBunnyPose(time);
      bunnyRoot.position.copy(currentPose.position);
      bunnyRoot.rotation.set(
        currentPose.pitch,
        currentPose.yaw,
        currentPose.bank,
        "YXZ"
      );

      if (!previousBunnyPose) {
        previousBunnyY = bunnyRoot.position.y;
      }

      const verticalVelocity = dt > 0
        ? (bunnyRoot.position.y - previousBunnyY) / dt
        : 0;

      const stretchTarget = THREE.MathUtils.clamp(
        verticalVelocity * 0.035,
        -0.085,
        0.15
      ) + (currentPose.air ? 0.035 : 0);

      currentStretch +=
        (stretchTarget - currentStretch) *
        Math.min(1, dt * 12);

      const breathing = Math.sin(time * 2.1) * 0.006;
      const sx = 1 - currentStretch * 0.43;
      const sy = 1 + currentStretch + breathing;
      const sz = 1 - currentStretch * 0.36;

      bunnyModel.scale.set(sx, sy, sz);
      bunnyUpper.rotation.z = -0.035 - currentPose.air * 0.055;
      bunnyUpper.rotation.x = currentPose.pitch * 0.22;
      head.rotation.z = 0.04 + currentPose.air * 0.035;
      head.rotation.x = Math.sin(time * 1.7) * 0.025;

      const hopSwing = Math.sin(currentPose.hopU * Math.PI);
      const transferSwing =
        currentPose.stage === "transfer" || currentPose.stage === "return"
          ? Math.sin(currentPose.hopU * Math.PI)
          : 0;

      const swing = Math.max(hopSwing, transferSwing);

      bunnyLegs.forEach(({ leg, side }, index) => {
        const offset = index === 0 ? 0.04 : -0.08;
        leg.rotation.z = offset + swing * (index === 0 ? 0.76 : 0.56);
        leg.rotation.x = side * 0.03;
      });

      bunnyArms.forEach(({ arm, side }, index) => {
        const offset = index === 0 ? -0.16 : -0.11;
        arm.rotation.z = offset - swing * (index === 0 ? 0.62 : 0.46);
        arm.rotation.x = side * 0.08 + swing * side * 0.08;
      });

      ears.forEach((ear, index) => {
        const waveA = Math.sin(time * 2.35 + ear.phase);
        const waveB = Math.sin(time * 2.35 + ear.phase - 0.48);

        ear.base.rotation.x =
          Math.sin(time * 1.3 + ear.phase) * 0.028;

        ear.lower.rotation.z =
          0.065 + waveA * 0.035 + currentPose.air * 0.09 +
          (index === 0 ? 0.025 : 0);

        ear.upper.rotation.z =
          0.095 + waveB * 0.065 +
          currentPose.air * 0.16 +
          Math.abs(verticalVelocity) * 0.004;
      });

      scarfSegments.forEach((segment, index) => {
        segment.rotation.z =
          0.06 + Math.sin(time * 2.2 - index * 0.7) * 0.045 +
          currentPose.air * (index + 1) * 0.055;

        segment.rotation.y =
          Math.sin(time * 1.6 - index * 0.8) * 0.06;
      });

      bunnyThrust +=
        ((currentPose.thrust ? 1 : 0) - bunnyThrust) *
        Math.min(1, dt * 7);

      bunnyFlames.forEach((flame, index) => {
        const flicker =
          0.84 + Math.sin(time * 28 + index * 2.1) * 0.16;
        flame.scale.y = 0.36 + bunnyThrust * flicker * 1.25;
        flame.material.opacity = 0.15 + bunnyThrust * 0.55;
      });

      bunnyThrusterMaterial.opacity = 0.20 + bunnyThrust * 0.55;
      bunnyThrusterLight.intensity = 0.8 + bunnyThrust * 7;
      eyeMaterial.color.setRGB(
        0.54 + Math.sin(time * 1.7) * 0.04,
        1,
        0.86 + Math.sin(time * 2.1) * 0.05
      );

      if (currentPose.surface) {
        bunnyShadow.position.set(
          currentPose.ground.x,
          currentPose.ground.y + 0.018,
          currentPose.ground.z
        );

        const shadowScale = 1 + currentPose.height * 0.16;
        bunnyShadow.scale.set(shadowScale, shadowScale, 1);
        bunnyShadow.material.opacity =
          0.31 / (1 + currentPose.height * 1.25);
      } else {
        bunnyShadow.material.opacity *= 0.84;
      }

      if (
        previousBunnyPose &&
        currentPose.surface &&
        !currentPose.air &&
        previousBunnyPose.air
      ) {
        spawnLanding(currentPose.ground);
      }

      previousBunnyPose = {
        air: currentPose.air,
        surface: currentPose.surface
      };

      previousBunnyY = bunnyRoot.position.y;
    }

    function updateLandingRings(dt) {
      landingRings.forEach(ring => {
        ring.userData.age += dt;

        if (ring.userData.age < 1) {
          const p = ring.userData.age;
          ring.scale.setScalar(0.65 + p * 2.4);
          ring.material.opacity = (1 - p) * 0.62;
        } else {
          ring.material.opacity = 0;
        }
      });
    }

    function updateEnvironment(time, dt) {
      const motion = reducedMotion ? 0.35 : 1;

      clouds.forEach(cloud => {
        const data = cloud.userData;
        cloud.position.x = data.base.x + Math.sin(time * data.speed + data.phase) * 1.25 * motion;
        cloud.position.y = data.base.y + Math.sin(time * 0.11 + data.phase) * 0.16 * motion;
        cloud.rotation.y = Math.sin(time * 0.04 + data.phase) * 0.06 * motion;
      });

      coastRockGroups.forEach(rock => {
        rock.position.y =
          rock.userData.base.y +
          Math.sin(time * 0.42 + rock.userData.phase) * 0.09 * motion;
        rock.rotation.y = Math.sin(time * 0.08 + rock.userData.phase) * 0.04;
      });

      titleGroup.position.y =
        9.0 + Math.sin(time * 0.34) * 0.17 * motion;
      titleGroup.rotation.z = Math.sin(time * 0.15) * 0.012 * motion;
      titlePlane.material.opacity = 0.80 + Math.sin(time * 0.7) * 0.08;
      titleHalo.material.opacity = 0.075 + Math.sin(time * 0.7) * 0.018;
      titleOrbit.rotation.z = time * 0.012 * motion;

      bokehSprites.forEach(sprite => {
        const base = sprite.userData.base;
        const phase = sprite.userData.phase;
        sprite.position.x = base.x + Math.sin(time * 0.13 + phase) * 0.45;
        sprite.position.y = base.y + Math.cos(time * 0.11 + phase) * 0.30;
      });

      dust.rotation.y = Math.sin(time * 0.025) * 0.035;
      dust.position.y = Math.sin(time * 0.16) * 0.12;

      for (let i = 0; i < routeParticleCount; i++) {
        const progress = (time * 0.065 + i / routeParticleCount) % 1;
        const point = staticRouteOut.getPoint(progress);
        point.y += Math.sin(progress * Math.PI) * Math.sin(time * 1.7 + i) * 0.09;

        routeParticlePositions[i * 3] = point.x;
        routeParticlePositions[i * 3 + 1] = point.y;
        routeParticlePositions[i * 3 + 2] = point.z;
      }

      routeParticleGeometry.getAttribute("position").needsUpdate = true;
    }

    const pointer = {
      x: 0,
      y: 0,
      targetX: 0,
      targetY: 0
    };

    addEventListener("pointermove", event => {
      pointer.targetX = (event.clientX / innerWidth - 0.5) * 2;
      pointer.targetY = (event.clientY / innerHeight - 0.5) * 2;
    }, { passive: true });

    addEventListener("pointerleave", () => {
      pointer.targetX = 0;
      pointer.targetY = 0;
    });

    function updateCamera(time, dt) {
      const motion = reducedMotion ? 0.35 : 1;
      const mobileFactor = camera.aspect < 0.8 ? 1.28 : 1;

      pointer.x += (pointer.targetX - pointer.x) * Math.min(1, dt * 1.8);
      pointer.y += (pointer.targetY - pointer.y) * Math.min(1, dt * 1.8);

      const azimuth =
        0.32 +
        Math.sin(time * 0.055) * 0.105 * motion +
        Math.sin(time * 0.021) * 0.035 * motion;

      const push =
        1.25 * (
          0.5 +
          0.5 * Math.sin(time * 0.09 - 1.0)
        );

      const radius = (18.1 - push) * mobileFactor;
      const height =
        (4.50 + Math.sin(time * 0.047) * 0.62 * motion) *
        (camera.aspect < 0.8 ? 1.12 : 1);

      desiredCamera.set(
        Math.sin(azimuth) * radius + pointer.x * 0.42 * motion,
        height - pointer.y * 0.28 * motion,
        Math.cos(azimuth) * radius
      );

      desiredLook.set(
        -0.38 + bunnyRoot.position.x * 0.075,
        2.45 + (bunnyRoot.position.y - 3.0) * 0.055,
        -0.85 + bunnyRoot.position.z * 0.035
      );

      const cameraEase = 1 - Math.exp(-dt * 0.9);
      camera.position.lerp(desiredCamera, cameraEase);
      cameraLook.lerp(desiredLook, 1 - Math.exp(-dt * 1.1));

      camera.lookAt(cameraLook);
      camera.rotateZ(
        Math.sin(time * 0.041) * 0.0045 * motion +
        pointer.x * 0.002 * motion
      );
    }

    function updateReflection() {
      water.updateMatrixWorld(true);

      mirrorCamera.position.set(
        camera.position.x,
        -camera.position.y,
        camera.position.z
      );

      mirrorCamera.up.set(0, -1, 0);
      mirrorCamera.lookAt(
        cameraLook.x,
        -cameraLook.y,
        cameraLook.z
      );

      mirrorCamera.near = camera.near;
      mirrorCamera.far = camera.far;
      mirrorCamera.fov = camera.fov;
      mirrorCamera.aspect = camera.aspect;
      mirrorCamera.updateProjectionMatrix();
      mirrorCamera.updateMatrixWorld(true);

      reflectionTextureMatrix.set(
        0.5, 0.0, 0.0, 0.5,
        0.0, 0.5, 0.0, 0.5,
        0.0, 0.0, 0.5, 0.5,
        0.0, 0.0, 0.0, 1.0
      );

      reflectionTextureMatrix.multiply(mirrorCamera.projectionMatrix);
      reflectionTextureMatrix.multiply(mirrorCamera.matrixWorldInverse);
      reflectionTextureMatrix.multiply(water.matrixWorld);

      water.visible = false;
      renderer.setRenderTarget(reflectionTarget);
      renderer.clear();
      renderer.render(scene, mirrorCamera);
      renderer.setRenderTarget(null);
      water.visible = true;
    }

    // ------------------------------------------------------------
    // 尺寸、暂停与渲染循环
    // ------------------------------------------------------------

    let paused = false;
    let simTime = 0;
    let lastTime = performance.now();
    let frameCount = 0;
    let timecodeAccumulator = 0;

    function resize() {
      const width = innerWidth;
      const height = innerHeight;

      renderer.setPixelRatio(
        Math.min(devicePixelRatio, width < 700 ? 1.4 : 1.75)
      );
      renderer.setSize(width, height);

      camera.aspect = width / height;
      camera.fov = width < 700 ? 51 : 43;
      camera.updateProjectionMatrix();

      reflectionTarget.setSize(
        width < 700 ? 512 : 768,
        width < 700 ? 384 : 576
      );
    }

    addEventListener("resize", resize);
    resize();

    function setPaused(value) {
      paused = value;
      pauseButton.textContent = paused ? "Play ▶" : "Pause Ⅱ";
      pauseButton.setAttribute(
        "aria-label",
        paused ? "继续动画" : "暂停动画"
      );
    }

    pauseButton.addEventListener("click", () => {
      setPaused(!paused);
    });

    addEventListener("keydown", event => {
      if (event.code === "Space" || event.code === "KeyP") {
        if (event.code === "Space") event.preventDefault();
        setPaused(!paused);
      }
    });

    document.addEventListener("visibilitychange", () => {
      lastTime = performance.now();
    });

    function animate(now) {
      requestAnimationFrame(animate);

      const rawDt = Math.min((now - lastTime) / 1000, 0.04);
      lastTime = now;

      const dt = paused || document.hidden ? 0 : rawDt;
      simTime += dt;

      updateBunny(simTime, dt || 0.0001);
      updateEnvironment(simTime, dt || 0.0001);
      updateLandingRings(dt);
      updateCamera(simTime, Math.max(dt, 0.001));

      sky.material.uniforms.uTime.value = simTime;
      stars.material.uniforms.uTime.value = simTime;
      moon.material.uniforms.uTime.value = simTime;
      waterMaterial.uniforms.uTime.value = simTime;

      // 移动端隔帧更新倒影,降低填充率压力。
      if (innerWidth >= 700 || frameCount % 2 === 0) {
        updateReflection();
      }

      renderer.render(scene, camera);

      timecodeAccumulator += rawDt;
      if (timecodeAccumulator > 0.15) {
        timecodeAccumulator = 0;
        const seconds = Math.floor(simTime % 120);
        const minutes = Math.floor(seconds / 60);
        const rest = seconds % 60;

        timecode.textContent =
          `T+ ${String(minutes).padStart(2, "0")}:` +
          `${String(rest).padStart(2, "0")}`;
      }

      frameCount++;

      if (frameCount === 2) {
        setTimeout(() => document.body.classList.add("loaded"), 180);
      }
    }

    requestAnimationFrame(animate);
  })();
  </script>
</body>
</html>
1.23s
TTFT
Actual wait 1.47s
70
Reasoning TPS
563s · ≈39,351 tok
151
Output TPS
145s · ≈21,843 tok
61,194
Total Tokens (official)
Input 597
Timing details
Actual wait (client)
1.47s
Client preparation
0.05s
Proxy preparation
0.00s
Upstream response headers
1.23s
Upstream first byte
1.23s
Upstream first token
1.23s
Upstream retries
0

Actual wait uses the client clock. Upstream timings start after proxy preparation and are cumulative; do not add these values. Upstream delay includes network, scheduling, processing and buffering; it does not prove provider queueing.

Diagnostic receipt
{
  "attempts": 1,
  "startedAt": "2026-09-29T03:43:55.792Z",
  "transport": "railway",
  "ttftSource": "proxy",
  "clientTtftMs": 1469.0999999046326,
  "completionId": "gen-1790653435-M4CIJLcvCG7u0G6yIaGN",
  "streamResumes": 15,
  "proxyPrepareMs": 2,
  "proxyRequestId": "N8jtqrSTSo-vM0RG2prcFg",
  "upstreamStatus": 200,
  "upstreamTtftMs": 1228,
  "clientHeadersMs": 240.69999980926514,
  "clientPrepareMs": 45.299999713897705,
  "upstreamHeadersMs": 1228,
  "upstreamFirstByteMs": 1228,
  "clientFirstContentMs": 564315.5999999046
}
Elapsed 710s · Average 86 tok/s · Peak 258 tok/s
MiniMax-M3.1-Flash-Preview
MiniMax-M3.1-Flash-Preview
Done

New test · 9/30/2026, 06:53:28

We need respond likely code full single HTML. Need create. User Chinese. Need deliver direct runnable. We need formulate with Three.js external CDN? "尽量不依赖复杂外部资源"; single HTML open runs. Could use CDN import. Direct requires internet. Maybe single HTML with script src unpkg three.min.js. Need no tooling. Need compelling scene. Need plan code robust. Three.js r160 maybe build/three.min.js still? At r160 three.min.js possibly removed? jsDelivr three@0.160.0/build/three.min.js exists warning? yes until r160 perhaps. Use 0.160.0. Need create visual: canvas, gradient sky, volumetric-ish clouds clustered translucent spheres, sea shader with reflect light, jet white green red, bunny detailed group, cinematic camera, text as 3D maybe use CanvasTexture Sprite, also HTML UI. Need single HTML. Add postprocessing? Avoid dependencies beyond Three. Implement fog, lights, renderer, tone mapping, shadow. Sea custom ShaderMaterial reflective waves and faux neon reflection. Maybe use Water? requires normals. Custom. Bunny: body capsule-ish sphere, head, muzzle, eyes with visors, flight suit collar, chest pack, suit panels, jet boots, ear groups with inner; antenna maybe star emblem. arms/legs animated physically. Distinct face with one eye visor? Cute cool. We can add transparent bubble helmet but would obscure. Maybe no helmet, pilot goggles, shoulder wings emblem, antenna. Spacesuit. Soft proportions. Use spheres. ears sway. Body bounce. Use hierarchy. Need not actual softbody. Animate groups rotations and scaling. Jet: build custom fuselage via lathe geometry, cockpit, wings using shape extrude? Main important. White body, green panels, red stripe, under engines cyan emissive. Put on right/beach or airborne near bunny. Ensure composition. Jet low flight over sea. Group oriented horizontal. Camera can see. Scene layout: y sea 0, platform x maybe -4 z 2; bunny starts x -2 y? foreground. Jet right x 5, y 1.2 z -3. Camera (9,4.2,11) looking at (0,1.8,0). low camera. Need title on floating moon? Text "just breathe" CanvasTexture Plane maybe behind bunny in sky. Natural via holographic sign. Add reflection title as transparent plane on water? Better 3D letters? Canvas texture plane with glow at x -2 y 5.8 z -7, slight tilt, subtle. Text in scene. Could include stars, moon, distant islands/planet, clouds volumetric spheres. Need avoid too heavy. Performance okay. Camera sequence 24-sec loop: starts broad, slowly dolly/orbit, maybe subtle mouse parallax. Use CatmullRomCurve3 with target curve. Respect reduced motion maybe. Overlay title minimal Chinese? User only asks text. Could use top badge "DREAM COAST / 07" and bottom "just breathe" maybe but text should in scene. Build 3D text sign plus reflection and HUD. User requested one piece, not technical demo. Add film grain CSS overlay, vignette, loading screen, controls maybe no. Single file and responsive. Need code thoroughly. Need ensure all methods and syntax. Let's define JS: import * as THREE from CDN script. If fail show message perhaps. HTML CSS. Renderer alpha false antialias; outputColorSpace = THREE.SRGBColorSpace; toneMapping ACESFilmic, exposure 1.15; shadowMap PCFSoftShadowMap. scene fog Exp2 or Fog. Background 0x090b2b. sky gradient: use giant sphere shader: vertex varying world y/local; fragment mix bottom, mid, top. side BackSide depthWrite false. But scene.background also. sky sphere. stars Points with positions on dome, colors/size? PointsMaterial, generate 700 on sphere upper. Lights: hemi, directional (moon), pink point, cyan point, jet light. ground sea plane (250x250) rotated x -pi/2, ShaderMaterial uniforms uTime, uColor, uSky, uLightPos, uJet; vertex wave displacement, calculate world pos, vPos, normal approximated. Fragment: base mix dark, purple based on y; wave sparkle; broad reflection vertical perhaps use x/z, "streaks" with sin; fake cube? Jet reflection visible impossible from plane geometry. We can create separate mirrored jet ghost: clone geometry group under sea, scale y -1 and transparent? Mirroring around y=0 with material transparent. But can make dedicated flattened reflection mesh: clone jet group scale (1,-.25,.65) position y=-.2, opacity 0.16, depthWrite false, renderedOrder. However mirrored geometry may show through sea depending sea depthWrite? Set sea transparent false. Reflection below hidden by opaque sea. Could render reflection on top at y=0.03 as flattened, horizontal? Jet itself on top. Real mirror geometry viewed from camera under sea? If clone below with reversed y, and sea depthWrite, hidden. Make sea material transparent with opacity? But then see. Or custom planar reflection using second render? Complicated but possible. Use Reflector addon dependency, but external. We can make sea ShaderMaterial transparent and reflection group above y=0.03, scale x, z and flatten? A direct flattened replica on surface with opacity looks reflection. Place clone at y=0.025, scale (1.1,.08,1.1), orientation? It will look aircraft shape atop sea, not reflection. Could create shadow/reflection silhouette in shader or plane canvas. User asks sea reflections, can provide light streaks. Add neon "JUST BREATHE" reflection using plane at y=.08 rotated x -pi/2, Canvas texture with vertical blur maybe. Jet reflection: a group of plane meshes beneath jet at y .025 but likely visible with material transparent, shape. Use clone of main jet, apply all materials cloned and transparent with low opacity; set position same x,z and y=-.12, scale(1,.08,.65) perhaps. This is above plane, visually a reflection. But meshes flat due scale. Need not interfere. Could share cloned parts and recursively clone material. Build function. Clouds: volumetric: group clusters 15 spheres with MeshStandardMaterial transparent opacity .72 depthWrite false. Add 3 layer positions. Purple moonlit. Animated slow x. Use individual sphere geometry with material. Maybe transparency sorting okay. Clouds in sky at y 5-9 and z -12. Include rim with point. Custom shader for fluffy? Sphere overlapping works. Dither? Fine. Moon: sphere with emissive pale lavender behind title. Platform: rounded geometry? Box with bevel via Extrude rounded rectangle. Mesh standard. Neon line. It creates "dream coast". x -3.2 z 2? Bunny there. Cylindrical? create roundedBox geometry? no addons. use Shape and ExtrudeGeometry with bevel. buildRoundedBox(w,h,d,r) Shape path and ExtrudeGeometry depth with bevel. But coordinate. Simpler CylinderGeometry(3.2,3.5,.45,64) platform, elliptic scale z .65. top with concentric rings. landing pad markings. Jet: Group. Build with LatheGeometry points around y? Lathe axis y. Need orient to z? Use sphere/capsule. Could create custom fuselage by SphereGeometry scaled (1.15,.65,3.2), nose at z=-? Camera from z positive looking toward negative. Jet nose should left/front? If jet at x 5 z -2 and nose left (toward bunny) could rotate y 0.25, long axis z. Use sphere body at local z, with nose cone? Better CapsuleGeometry(radius .7, length 3.6) oriented z (Capsule default along y, rotate x pi/2). But scales. Create group parts: fuselage sphere scale .8,.62,2.9 position. nose cone at z=-2.5, cone default y, rotate x -pi/2? Cylinder cone top direction y; rotate x -pi/2 maps y to? y (0,1,0) rotated -90 about x => z=-1. nose points -z. Fine. canopy sphere .7,.42,1.1 at (0,.55,-.5), material dark blue transparent. wings custom BufferGeometry triangular wedge using shape extrude? CreateShape points in x,z and extrude along thickness y difficult. Use BoxGeometry and rotate maybe. Use ShapeGeometry in xy, rotate x. Make wing as BufferGeometry vertices. For swept delta wing each side: wing Shape points [0,.2], [3.4,-1.4], [3.8,-.8],[.8,1.3] in local x,z. We can create geometry with vertices top/bottom, indexed. Utility prismShape(pointsX,Z, thickness). Need normals. simpler `THREE.ExtrudeGeometry(shape, depth:.12, bevel...)` shape in x,y where y corresponds z then rotate X(pi/2?) shape XY, extrusion z. Want horizonta

下面是一个完整的单文件作品。保存为 index.html 后直接用现代浏览器打开即可。首次加载需要联网获取 Three.js,其余场景、模型、材质、动画和文字均由文件内代码生成。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta name="theme-color" content="#080b28" />
  <title>Space Bunny — Just Breathe</title>

  <style>
    :root {
      --ink: #eff6ff;
      --muted: rgba(226, 235, 255, 0.58);
      --mint: #82ffd2;
      --pink: #ff82b8;
    }

    * {
      box-sizing: border-box;
    }

    html,
    body {
      width: 100%;
      height: 100%;
      margin: 0;
      overflow: hidden;
      background: #080b28;
      font-family: Inter, ui-sans-serif, system-ui, -apple-system,
        BlinkMacSystemFont, "Segoe UI", sans-serif;
    }

    body {
      color: var(--ink);
    }

    #stage {
      position: fixed;
      inset: 0;
      width: 100%;
      height: 100%;
      display: block;
      outline: none;
    }

    .vignette,
    .grain,
    .letterbox,
    .hud {
      position: fixed;
      pointer-events: none;
      z-index: 5;
    }

    .vignette {
      inset: 0;
      background:
        radial-gradient(
          ellipse at 48% 45%,
          transparent 28%,
          rgba(4, 5, 20, 0.08) 56%,
          rgba(3, 4, 17, 0.64) 125%
        ),
        linear-gradient(
          180deg,
          rgba(12, 11, 43, 0.18),
          transparent 20%,
          transparent 76%,
          rgba(3, 7, 22, 0.28)
        );
    }

    .grain {
      inset: -50%;
      width: 200%;
      height: 200%;
      opacity: 0.035;
      background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.92' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.85'/%3E%3C/svg%3E");
      animation: grain 0.22s steps(2) infinite;
    }

    @keyframes grain {
      0% { transform: translate3d(0, 0, 0); }
      25% { transform: translate3d(2%, -1%, 0); }
      50% { transform: translate3d(-1%, 2%, 0); }
      75% { transform: translate3d(1%, 1%, 0); }
      100% { transform: translate3d(-2%, -1%, 0); }
    }

    .letterbox::before,
    .letterbox::after {
      content: "";
      position: absolute;
      left: 0;
      right: 0;
      height: 3.4vh;
      min-height: 18px;
      background: #030513;
    }

    .letterbox::before { top: 0; }
    .letterbox::after { bottom: 0; }

    .hud {
      inset: 0;
      z-index: 8;
    }

    .brand {
      position: absolute;
      top: max(6.4vh, 38px);
      left: clamp(22px, 4vw, 64px);
      display: flex;
      align-items: center;
      gap: 13px;
      letter-spacing: 0.2em;
      text-transform: uppercase;
      font-size: 10px;
      font-weight: 650;
      text-shadow: 0 2px 20px rgba(0, 0, 0, 0.6);
    }

    .brand-mark {
      width: 26px;
      height: 26px;
      border: 1px solid rgba(130, 255, 210, 0.65);
      border-radius: 50%;
      display: grid;
      place-items: center;
      box-shadow:
        0 0 18px rgba(92, 255, 215, 0.17),
        inset 0 0 12px rgba(92, 255, 215, 0.08);
    }

    .brand-mark::before {
      content: "";
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: var(--mint);
      box-shadow: 0 0 12px var(--mint);
    }

    .telemetry {
      position: absolute;
      top: max(6.4vh, 38px);
      right: clamp(22px, 4vw, 64px);
      color: var(--muted);
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
      font-size: 9px;
      line-height: 1.8;
      letter-spacing: 0.13em;
      text-align: right;
      text-transform: uppercase;
    }

    .telemetry strong {
      display: block;
      color: rgba(232, 244, 255, 0.85);
      font-weight: 500;
    }

    .footer-note {
      position: absolute;
      left: clamp(22px, 4vw, 64px);
      bottom: max(6.5vh, 34px);
      max-width: 290px;
      color: rgba(224, 234, 255, 0.52);
      font-size: 9px;
      line-height: 1.8;
      letter-spacing: 0.18em;
      text-transform: uppercase;
    }

    .footer-note span {
      color: rgba(130, 255, 210, 0.78);
    }

    .corner {
      position: absolute;
      right: clamp(22px, 4vw, 64px);
      bottom: max(6.5vh, 34px);
      display: flex;
      align-items: center;
      gap: 10px;
      color: rgba(224, 234, 255, 0.42);
      font-size: 9px;
      letter-spacing: 0.16em;
      text-transform: uppercase;
    }

    .corner-line {
      width: 42px;
      height: 1px;
      background: linear-gradient(90deg, transparent, rgba(255, 130, 184, 0.7));
    }

    #loader {
      position: fixed;
      inset: 0;
      z-index: 100;
      display: grid;
      place-items: center;
      background:
        radial-gradient(circle at 50% 44%, #24214d 0, #101331 38%, #060817 100%);
      transition: opacity 1.4s ease, visibility 1.4s;
    }

    #loader.ready {
      opacity: 0;
      visibility: hidden;
    }

    .loader-inner {
      text-align: center;
    }

    .loader-orbit {
      width: 62px;
      height: 62px;
      margin: 0 auto 25px;
      border: 1px solid rgba(145, 240, 255, 0.18);
      border-top-color: #83ffd4;
      border-radius: 50%;
      animation: spin 1.4s linear infinite;
      box-shadow: 0 0 30px rgba(93, 255, 211, 0.08);
    }

    .loader-title {
      color: rgba(244, 248, 255, 0.9);
      font-family: Georgia, "Times New Roman", serif;
      font-size: 25px;
      font-style: italic;
      letter-spacing: 0.035em;
    }

    .loader-subtitle {
      margin-top: 11px;
      color: rgba(224, 232, 255, 0.42);
      font-size: 8px;
      letter-spacing: 0.28em;
      text-transform: uppercase;
    }

    @keyframes spin {
      to { transform: rotate(360deg); }
    }

    @media (max-width: 700px) {
      .telemetry {
        display: none;
      }

      .footer-note {
        max-width: 210px;
        font-size: 8px;
      }

      .corner {
        display: none;
      }

      .brand {
        top: max(5.5vh, 25px);
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .grain {
        animation: none;
      }
    }
  </style>
</head>

<body>
  <canvas
    id="stage"
    aria-label="梦幻太空海岸中的 Space Bunny、未来战斗机与 just breathe 发光文字"
  ></canvas>

  <div class="vignette"></div>
  <div class="grain"></div>
  <div class="letterbox"></div>

  <div class="hud">
    <div class="brand">
      <span class="brand-mark"></span>
      <span>Space Bunny / Dream Flight 07</span>
    </div>

    <div class="telemetry">
      <strong>Dream Coast</strong>
      Alt. 00.38 km<br />
      Signal / Softly alive
    </div>

    <div class="footer-note">
      <span>●</span> A small voyage between starlight &amp; sea<br />
      Move gently. The universe will follow.
    </div>

    <div class="corner">
      <span>Orbital daydream</span>
      <span class="corner-line"></span>
    </div>
  </div>

  <div id="loader">
    <div class="loader-inner">
      <div class="loader-orbit"></div>
      <div class="loader-title">just breathe</div>
      <div class="loader-subtitle">Preparing a little piece of dream</div>
    </div>
  </div>

  <noscript>
    <div style="position:fixed;inset:0;z-index:200;display:grid;place-items:center;background:#090b27;color:white">
      请启用 JavaScript 以运行 Space Bunny。
    </div>
  </noscript>

  <script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>

  <script>
  (() => {
    "use strict";

    const loader = document.getElementById("loader");

    if (!window.THREE) {
      loader.querySelector(".loader-subtitle").textContent =
        "Three.js 加载失败,请检查网络后刷新";
      loader.querySelector(".loader-orbit").style.animation = "none";
      return;
    }

    const canvas = document.getElementById("stage");
    const reducedMotion = window.matchMedia(
      "(prefers-reduced-motion: reduce)"
    ).matches;

    // ─────────────────────────────────────────────────────────────
    // Renderer / scene
    // ─────────────────────────────────────────────────────────────

    const renderer = new THREE.WebGLRenderer({
      canvas,
      antialias: true,
      alpha: false,
      powerPreference: "high-performance"
    });

    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.8));
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.outputEncoding = THREE.sRGBEncoding;
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1.15;
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.setClearColor(0x080b28, 1);

    const scene = new THREE.Scene();
    scene.fog = new THREE.FogExp2(0x171735, 0.018);

    const camera = new THREE.PerspectiveCamera(
      42,
      window.innerWidth / window.innerHeight,
      0.1,
      240
    );

    camera.position.set(7.8, 3.4, 12.4);

    const world = new THREE.Group();
    scene.add(world);

    const clock = new THREE.Clock();

    const clamp = THREE.MathUtils.clamp;
    const lerp = THREE.MathUtils.lerp;
    const TAU = Math.PI * 2;

    function random(min, max) {
      return min + Math.random() * (max - min);
    }

    function standardMaterial(color, roughness = 0.65, metalness = 0.08) {
      return new THREE.MeshStandardMaterial({
        color,
        roughness,
        metalness
      });
    }

    function addEllipsoid(
      parent,
      material,
      position,
      scale,
      segments = 24
    ) {
      const mesh = new THREE.Mesh(
        new THREE.SphereGeometry(1, segments, Math.max(12, segments / 2)),
        material
      );
      mesh.position.set(position[0], position[1], position[2]);
      mesh.scale.set(scale[0], scale[1], scale[2]);
      mesh.castShadow = true;
      mesh.receiveShadow = true;
      parent.add(mesh);
      return mesh;
    }

    function makeCanvasTexture(width, height, draw) {
      const element = document.createElement("canvas");
      element.width = width;
      element.height = height;
      const context = element.getContext("2d");
      draw(context, width, height);

      const texture = new THREE.CanvasTexture(element);
      texture.encoding = THREE.sRGBEncoding;
      texture.minFilter = THREE.LinearFilter;
      texture.magFilter = THREE.LinearFilter;
      texture.generateMipmaps = false;
      return texture;
    }

    function makeGlowTexture() {
      return makeCanvasTexture(128, 128, (ctx, w, h) => {
        const g = ctx.createRadialGradient(w / 2, h / 2, 0, w / 2, h / 2, w / 2);
        g.addColorStop(0, "rgba(255,255,255,1)");
        g.addColorStop(0.16, "rgba(255,255,255,.92)");
        g.addColorStop(0.42, "rgba(255,255,255,.26)");
        g.addColorStop(1, "rgba(255,255,255,0)");
        ctx.fillStyle = g;
        ctx.fillRect(0, 0, w, h);
      });
    }

    function makeGlow(color, size, opacity = 1) {
      const sprite = new THREE.Sprite(
        new THREE.SpriteMaterial({
          map: makeGlowTexture(),
          color,
          transparent: true,
          opacity,
          depthWrite: false,
          blending: THREE.AdditiveBlending
        })
      );
      sprite.scale.set(size, size, 1);
      return sprite;
    }

    function makeStarShape(outer = 0.18, inner = 0.075) {
      const shape = new THREE.Shape();

      for (let i = 0; i < 10; i++) {
        const angle = -Math.PI / 2 + i * Math.PI / 5;
        const radius = i % 2 === 0 ? outer : inner;
        const x = Math.cos(angle) * radius;
        const y = Math.sin(angle) * radius;

        if (i === 0) shape.moveTo(x, y);
        else shape.lineTo(x, y);
      }

      shape.closePath();
      return shape;
    }

    // ─────────────────────────────────────────────────────────────
    // Gradient sky and aurora
    // ─────────────────────────────────────────────────────────────

    const sky = new THREE.Mesh(
      new THREE.SphereGeometry(140, 48, 32),
      new THREE.ShaderMaterial({
        side: THREE.BackSide,
        depthWrite: false,
        toneMapped: false,
        uniforms: {
          uTop: { value: new THREE.Color(0x080b2b) },
          uMiddle: { value: new THREE.Color(0x28205d) },
          uHorizon: { value: new THREE.Color(0x7b4e89) }
        },
        vertexShader: `
          varying vec3 vPosition;

          void main() {
            vPosition = position;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          }
        `,
        fragmentShader: `
          varying vec3 vPosition;
          uniform vec3 uTop;
          uniform vec3 uMiddle;
          uniform vec3 uHorizon;

          void main() {
            float h = normalize(vPosition).y;
            float lower = smoothstep(-0.22, 0.22, h);
            float upper = smoothstep(0.12, 0.82, h);

            vec3 color = mix(uHorizon, uMiddle, lower);
            color = mix(color, uTop, upper);

            float haze = pow(1.0 - abs(h), 8.0);
            color += vec3(0.12, 0.055, 0.15) * haze;

            gl_FragColor = vec4(color, 1.0);
          }
        `
      })
    );

    scene.add(sky);

    const auroraUniforms = {
      uTime: { value: 0 }
    };

    const aurora = new THREE.Mesh(
      new THREE.PlaneGeometry(75, 26),
      new THREE.ShaderMaterial({
        transparent: true,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        uniforms: auroraUniforms,
        vertexShader: `
          varying vec2 vUv;

          void main() {
            vUv = uv;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          }
        `,
        fragmentShader: `
          varying vec2 vUv;
          uniform float uTime;

          void main() {
            float waveA = sin(vUv.x * 10.0 + uTime * 0.07) * 0.055;
            float waveB = sin(vUv.x * 23.0 - uTime * 0.045) * 0.025;
            float center = 0.56 + waveA + waveB;
            float ribbon = exp(-pow((vUv.y - center) * 5.0, 2.0));
            float fade = sin(vUv.x * 3.14159);
            vec3 mint = vec3(0.18, 0.9, 0.72);
            vec3 violet = vec3(0.58, 0.24, 0.9);
            vec3 color = mix(violet, mint, 0.5 + 0.5 * sin(vUv.x * 6.0));
            gl_FragColor = vec4(color, ribbon * fade * 0.085);
          }
        `
      })
    );

    aurora.position.set(-2, 11, -35);
    aurora.rotation.z = -0.035;
    scene.add(aurora);

    // ─────────────────────────────────────────────────────────────
    // Stars
    // ─────────────────────────────────────────────────────────────

    const starCount = 850;
    const starPositions = new Float32Array(starCount * 3);
    const starColors = new Float32Array(starCount * 3);
    const starPalette = [
      new THREE.Color(0xdde9ff),
      new THREE.Color(0x98ffe1),
      new THREE.Color(0xffc0dd),
      new THREE.Color(0xb9aaff)
    ];

    for (let i = 0; i < starCount; i++) {
      const angle = random(0, TAU);
      const radius = random(55, 105);
      const y = random(4, 55);

      starPositions[i * 3] = Math.cos(angle) * radius;
      starPositions[i * 3 + 1] = y;
      starPositions[i * 3 + 2] = Math.sin(angle) * radius;

      const color = starPalette[Math.floor(random(0, starPalette.length))];
      const intensity = random(0.45, 1);
      starColors[i * 3] = color.r * intensity;
      starColors[i * 3 + 1] = color.g * intensity;
      starColors[i * 3 + 2] = color.b * intensity;
    }

    const starGeometry = new THREE.BufferGeometry();
    starGeometry.setAttribute(
      "position",
      new THREE.BufferAttribute(starPositions, 3)
    );
    starGeometry.setAttribute(
      "color",
      new THREE.BufferAttribute(starColors, 3)
    );

    const stars = new THREE.Points(
      starGeometry,
      new THREE.PointsMaterial({
        size: 0.085,
        vertexColors: true,
        transparent: true,
        opacity: 0.88,
        sizeAttenuation: true,
        depthWrite: false,
        fog: false
      })
    );

    scene.add(stars);

    // ─────────────────────────────────────────────────────────────
    // Lighting
    // ─────────────────────────────────────────────────────────────

    scene.add(new THREE.HemisphereLight(0x8da8ff, 0x241635, 1.25));

    const moonLight = new THREE.DirectionalLight(0xb9c9ff, 2.5);
    moonLight.position.set(-7, 12, 8);
    moonLight.castShadow = true;
    moonLight.shadow.mapSize.set(1024, 1024);
    moonLight.shadow.camera.left = -12;
    moonLight.shadow.camera.right = 12;
    moonLight.shadow.camera.top = 12;
    moonLight.shadow.camera.bottom = -12;
    moonLight.shadow.camera.near = 1;
    moonLight.shadow.camera.far = 35;
    moonLight.shadow.bias = -0.0004;
    scene.add(moonLight);

    const mintRim = new THREE.PointLight(0x67ffd5, 18, 12, 2);
    mintRim.position.set(-3.5, 4.2, 3.2);
    scene.add(mintRim);

    const pinkRim = new THREE.PointLight(0xff74ba, 15, 15, 2);
    pinkRim.position.set(5, 3.2, 0.5);
    scene.add(pinkRim);

    // ─────────────────────────────────────────────────────────────
    // Reflective dream sea
    // ─────────────────────────────────────────────────────────────

    const seaUniforms = {
      uTime: { value: 0 },
      uDeep: { value: new THREE.Color(0x09132d) },
      uViolet: { value: new THREE.Color(0x39285c) },
      uCyan: { value: new THREE.Color(0x5fe8dc) },
      uPink: { value: new THREE.Color(0xf18ab8) },
      uFog: { value: new THREE.Color(0x171735) }
    };

    const seaGeometry = new THREE.PlaneGeometry(250, 250, 110, 110);
    seaGeometry.rotateX(-Math.PI / 2);

    const sea = new THREE.Mesh(
      seaGeometry,
      new THREE.ShaderMaterial({
        uniforms: seaUniforms,
        side: THREE.DoubleSide,
        vertexShader: `
          uniform float uTime;
          varying vec3 vWorld;
          varying float vWave;

          void main() {
            vec3 p = position;

            float waveA = sin(p.x * 0.22 + uTime * 0.65) * 0.10;
            float waveB = sin(p.z * 0.31 - uTime * 0.48) * 0.065;
            float waveC = sin((p.x + p.z) * 0.14 + uTime * 0.27) * 0.045;

            p.y += waveA + waveB + waveC;

            vWave = waveA + waveB + waveC;
            vWorld = (modelMatrix * vec4(p, 1.0)).xyz;

            gl_Position = projectionMatrix * viewMatrix * vec4(vWorld, 1.0);
          }
        `,
        fragmentShader: `
          uniform float uTime;
          uniform vec3 uDeep;
          uniform vec3 uViolet;
          uniform vec3 uCyan;
          uniform vec3 uPink;
          uniform vec3 uFog;
          varying vec3 vWorld;
          varying float vWave;

          void main() {
            vec3 viewDir = normalize(cameraPosition - vWorld);
            float fresnel = pow(1.0 - max(viewDir.y, 0.0), 2.2);

            float broad = sin(vWorld.x * 0.17 + vWorld.z * 0.09 + uTime * 0.25);
            float horizonTint = smoothstep(-5.0, 80.0, vWorld.z);

            vec3 color = mix(uDeep, uViolet, 0.30 + broad * 0.11);
            color = mix(color, uCyan, fresnel * 0.22);

            float bandX = vWorld.x - 3.6 - vWorld.z * 0.15;
            float jetReflection = exp(-abs(bandX) * 0.38);
            jetReflection *= exp(-abs(vWorld.z + 2.0) * 0.075);

            float cyanStreak = pow(
              max(0.0, sin(vWorld.x * 4.0 + sin(vWorld.z * 0.8) + uTime * 0.8)),
              17.0
            );

            float pinkStreak = pow(
              max(0.0, sin(vWorld.x * 2.2 - vWorld.z * 0.45 - uTime * 0.5)),
              22.0
            );

            color += uCyan * cyanStreak * (0.13 + jetReflection * 0.32);
            color += uPink * pinkStreak * 0.10;
            color += vWave * 0.16;

            float distanceFog = smoothstep(38.0, 115.0, distance(cameraPosition, vWorld));
            color = mix(color, uFog, distanceFog * 0.96);

            gl_FragColor = vec4(color, 1.0);
          }
        `
      })
    );

    sea.position.y = 0;
    sea.receiveShadow = true;
    scene.add(sea);

    // Soft contact shadow on the sea.
    const shadowTexture = makeCanvasTexture(128, 128, (ctx, w, h) => {
      const gradient = ctx.createRadialGradient(w / 2, h / 2, 0, w / 2, h / 2, w / 2);
      gradient.addColorStop(0, "rgba(0,0,0,.56)");
      gradient.addColorStop(0.45, "rgba(0,0,0,.24)");
      gradient.addColorStop(1, "rgba(0,0,0,0)");
      ctx.fillStyle = gradient;
      ctx.fillRect(0, 0, w, h);
    });

    const jetShadow = new THREE.Mesh(
      new THREE.PlaneGeometry(8, 3.4),
      new THREE.MeshBasicMaterial({
        map: shadowTexture,
        transparent: true,
        opacity: 0.52,
        depthWrite: false
      })
    );
    jetShadow.rotation.x = -Math.PI / 2;
    jetShadow.position.set(4.2, 0.055, -2.1);
    scene.add(jetShadow);

    // ─────────────────────────────────────────────────────────────
    // Moon
    // ─────────────────────────────────────────────────────────────

    const moon = new THREE.Mesh(
      new THREE.SphereGeometry(2.0, 48, 32),
      new THREE.MeshStandardMaterial({
        color: 0xc9c2ff,
        emissive: 0x6653a9,
        emissiveIntensity: 0.9,
        roughness: 1
      })
    );
    moon.position.set(4.4, 6.5, -18);
    scene.add(moon);

    const moonGlow = makeGlow(0xa68cff, 8.5, 0.31);
    moonGlow.position.copy(moon.position);
    scene.add(moonGlow);

    // ─────────────────────────────────────────────────────────────
    // Volumetric-looking purple clouds
    // ─────────────────────────────────────────────────────────────

    const clouds = [];
    const cloudGeometry = new THREE.SphereGeometry(1, 18, 12);

    function createCloud(position, scale, hue) {
      const group = new THREE.Group();
      group.position.copy(position);
      group.scale.set(scale[0], scale[1], scale[2]);
      group.userData.baseX = position.x;
      group.userData.phase = random(0, TAU);
      group.userData.speed = random(0.025, 0.065);

      const outerMaterial = new THREE.MeshStandardMaterial({
        color: hue,
        emissive: 0x241a49,
        emissiveIntensity: 0.28,
        roughness: 1,
        transparent: true,
        opacity: 0.74,
        depthWrite: false
      });

      const innerMaterial = new THREE.MeshStandardMaterial({
        color: new THREE.Color(hue).lerp(new THREE.Color(0xe2bdff), 0.28),
        emissive: 0x4c2d71,
        emissiveIntensity: 0.22,
        roughness: 1,
        transparent: true,
        opacity: 0.62,
        depthWrite: false
      });

      const count = Math.floor(random(8, 13));

      for (let i = 0; i < count; i++) {
        const material = i % 4 === 0 ? innerMaterial : outerMaterial;
        const puff = new THREE.Mesh(cloudGeometry, material);

        puff.position.set(
          random(-2.3, 2.3),
          random(-0.35, 0.55),
          random(-0.7, 0.7)
        );

        puff.scale.set(
          random(0.65, 1.45),
          random(0.42, 0.9),
          random(0.58, 1.1)
        );

        group.add(puff);
      }

      world.add(group);
      clouds.push(group);
      return group;
    }

    createCloud(new THREE.Vector3(-11, 5.8, -10), 1.8, 0x75539d);
    createCloud(new THREE.Vector3(1.4, 7.0, -15), 2.0, 0x8a5ca5);
    createCloud(new THREE.Vector3(12, 5.6, -11), 1.7, 0x6a4c95);
    createCloud(new THREE.Vector3(-16, 3.6, -4), 1.35, 0x5f467f);
    createCloud(new THREE.Vector3(8, 9.0, -23), 2.4, 0x684a91);

    // ─────────────────────────────────────────────────────────────
    // “just breathe” floating sky sign
    // ─────────────────────────────────────────────────────────────

    const textTexture = makeCanvasTexture(1400, 360, (ctx, w, h) => {
      ctx.clearRect(0, 0, w, h);

      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.font = 'italic 150px Georgia, "Times New Roman", serif';

      ctx.shadowColor = "rgba(106,255,225,.95)";
      ctx.shadowBlur = 24;
      ctx.fillStyle = "rgba(235,255,250,.98)";
      ctx.fillText("just breathe", w / 2, h * 0.51);

      ctx.shadowBlur = 52;
      ctx.shadowColor = "rgba(183,117,255,.75)";
      ctx.fillStyle = "rgba(255,255,255,.32)";
      ctx.fillText("just breathe", w / 2, h * 0.51);

      ctx.shadowBlur = 0;
      ctx.font = "18px ui-monospace, monospace";
      ctx.letterSpacing = "7px";
      ctx.fillStyle = "rgba(189,255,237,.56)";
      ctx.fillText("A SOFT PLACE BETWEEN STARS", w / 2, h * 0.87);
    });

    const sign = new THREE.Group();
    sign.position.set(-0.9, 5.05, -8.8);

    const signPlane = new THREE.Mesh(
      new THREE.PlaneGeometry(6.8, 1.75),
      new THREE.MeshBasicMaterial({
        map: textTexture,
        transparent: true,
        depthWrite: false,
        side: THREE.DoubleSide,
        toneMapped: false,
        fog: false
      })
    );
    sign.add(signPlane);

    const orbitMaterial = new THREE.MeshBasicMaterial({
      color: 0x87ffe0,
      transparent: true,
      opacity: 0.32,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
      toneMapped: false,
      fog: false
    });

    const signOrbit = new THREE.Mesh(
      new THREE.TorusGeometry(3.65, 0.014, 6, 110, Math.PI * 1.56),
      orbitMaterial
    );
    signOrbit.position.z = -0.05;
    signOrbit.rotation.z = -0.28;
    sign.add(signOrbit);

    const signOrbit2 = new THREE.Mesh(
      new THREE.TorusGeometry(3.78, 0.009, 5, 110, Math.PI * 0.8),
      new THREE.MeshBasicMaterial({
        color: 0xe191ff,
        transparent: true,
        opacity: 0.3,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        toneMapped: false,
        fog: false
      })
    );
    signOrbit2.position.z = -0.06;
    signOrbit2.rotation.z = 2.35;
    sign.add(signOrbit2);

    const signDot = makeGlow(0x8affe0, 0.38, 0.75);
    signDot.position.set(3.35, -0.83, 0.05);
    sign.add(signDot);

    world.add(sign);

    // Blurred title reflection on the sea.
    const reflectionTextTexture = makeCanvasTexture(1400, 360, (ctx, w, h) => {
      ctx.clearRect(0, 0, w, h);
      ctx.save();
      ctx.translate(0, h);
      ctx.scale(1, -1);
      ctx.filter = "blur(4px)";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.font = 'italic 150px Georgia, "Times New Roman", serif';
      ctx.shadowColor = "rgba(98,255,225,.9)";
      ctx.shadowBlur = 22;
      ctx.fillStyle = "rgba(121,255,222,.55)";
      ctx.fillText("just breathe", w / 2, h * 0.51);
      ctx.restore();
    });

    const titleReflection = new THREE.Mesh(
      new THREE.PlaneGeometry(7.4, 2.1),
      new THREE.MeshBasicMaterial({
        map: reflectionTextTexture,
        transparent: true,
        opacity: 0.16,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        toneMapped: false
      })
    );

    titleReflection.rotation.x = -Math.PI / 2;
    titleReflection.rotation.z = -0.05;
    titleReflection.position.set(-1.0, 0.075, -7.9);
    scene.add(titleReflection);

    // ─────────────────────────────────────────────────────────────
    // Dream coast platform
    // ─────────────────────────────────────────────────────────────

    const platform = new THREE.Group();
    platform.position.set(-2.6, 0, 1.45);
    world.add(platform);

    const platformMaterial = standardMaterial(0xdbe3ed, 0.42, 0.38);
    const platformSideMaterial = standardMaterial(0x28364b, 0.55, 0.48);
    const platformGreen = standardMaterial(0x50d7a2, 0.38, 0.2);
    const darkInset = standardMaterial(0x152039, 0.45, 0.32);

    const platformBase = new THREE.Mesh(
      new THREE.CylinderGeometry(2.65, 2.9, 0.34, 64),
      platformSideMaterial
    );
    platformBase.position.y = 0.13;
    platformBase.scale.z = 0.68;
    platformBase.castShadow = true;
    platformBase.receiveShadow = true;
    platform.add(platformBase);

    const platformTop = new THREE.Mesh(
      new THREE.CylinderGeometry(2.6, 2.62, 0.08, 64),
      platformMaterial
    );
    platformTop.position.y = 0.335;
    platformTop.scale.z = 0.68;
    platformTop.castShadow = true;
    platformTop.receiveShadow = true;
    platform.add(platformTop);

    const platformInset = new THREE.Mesh(
      new THREE.CylinderGeometry(1.45, 1.48, 0.025, 64),
      darkInset
    );
    platformInset.position.y = 0.39;
    platformInset.scale.z = 0.68;
    platform.add(platformInset);

    const ringMaterial = new THREE.MeshBasicMaterial({
      color: 0x78fbd0,
      transparent: true,
      opacity: 0.6,
      blending: THREE.AdditiveBlending,
      toneMapped: false
    });

    [1.03, 1.47, 2.16].forEach((radius, index) => {
      const ring = new THREE.Mesh(
        new THREE.TorusGeometry(radius, index === 0 ? 0.018 : 0.012, 6, 80),
        ringMaterial
      );
      ring.rotation.x = Math.PI / 2;
      ring.position.y = 0.415 + index * 0.002;
      ring.scale.y = 0.68;
      platform.add(ring);
    });

    const landingMarkMaterial = new THREE.MeshBasicMaterial({
      color: 0xf076a9,
      transparent: true,
      opacity: 0.7,
      toneMapped: false
    });

    for (let i = 0; i < 5; i++) {
      const marker = new THREE.Mesh(
        new THREE.BoxGeometry(0.12, 0.015, 0.3),
        i === 2 ? platformGreen : landingMarkMaterial
      );
      marker.position.set(-0.8 + i * 0.4, 0.43, 1.66);
      platform.add(marker);
    }

    const platformLights = [];
    for (let i = 0; i < 14; i++) {
      const angle = i / 14 * TAU;
      const light = new THREE.Mesh(
        new THREE.SphereGeometry(0.045, 10, 8),
        new THREE.MeshBasicMaterial({
          color: i % 2 ? 0xff83b9 : 0x83ffe0,
          toneMapped: false
        })
      );
      light.position.set(
        Math.cos(angle) * 2.45,
        0.29,
        Math.sin(angle) * 1.63
      );
      platform.add(light);
      platformLights.push(light);
    }

    // ─────────────────────────────────────────────────────────────
    // Future fighter
    // ─────────────────────────────────────────────────────────────

    const jet = new THREE.Group();
    jet.position.set(4.25, 1.55, -2.1);
    jet.rotation.y = -0.12;
    world.add(jet);

    const jetWhite = standardMaterial(0xf2f4f0, 0.28, 0.5);
    const jetWhiteShadow = standardMaterial(0xb9c6ce, 0.35, 0.55);
    const jetGreen = standardMaterial(0x36c98c, 0.3, 0.35);
    const jetGreenDark = standardMaterial(0x176d64, 0.32, 0.42);
    const jetRed = standardMaterial(0xef466f, 0.28, 0.32);
    const jetDark = standardMaterial(0x10192c, 0.24, 0.7);
    const canopyMaterial = new THREE.MeshPhysicalMaterial({
      color: 0x193654,
      emissive: 0x071b2a,
      emissiveIntensity: 0.45,
      metalness: 0.22,
      roughness: 0.12,
      clearcoat: 1,
      clearcoatRoughness: 0.08
    });

    const engineGlowMaterial = new THREE.MeshBasicMaterial({
      color: 0x7dffe9,
      toneMapped: false
    });

    addEllipsoid(jet, jetWhite, [0, 0, 0.05], [0.82, 0.68, 2.8], 36);
    addEllipsoid(jet, jetWhiteShadow, [0, -0.36, 0.25], [0.64, 0.28, 2.15], 28);
    addEllipsoid(jet, jetRed, [0, 0.01, -2.55], [0.56, 0.42, 0.62], 28);

    const topStripe = addEllipsoid(
      jet,
      jetGreen,
      [0, 0.58, -0.35],
      [0.2, 0.08, 1.55],
      20
    );
    topStripe.rotation.y = 0.025;

    addEllipsoid(jet, jetRed, [0.38, 0.49, -0.6], [0.06, 0.065, 1.25], 16);

    const canopy = addEllipsoid(
      jet,
      canopyMaterial,
      [0, 0.66, -0.72],
      [0.62, 0.43, 1.02],
      30
    );
    canopy.rotation.x = -0.07;

    const canopyFrame = new THREE.Mesh(
      new THREE.TorusGeometry(0.48, 0.028, 8, 40),
      jetGreen
    );
    canopyFrame.position.set(0, 0.66, -0.37);
    canopyFrame.scale.set(1, 0.78, 1);
    jet.add(canopyFrame);

    function makeHorizontalPrism(points, thickness, material) {
      const shape = new THREE.Shape();
      points.forEach((point, index) => {
        if (index === 0) shape.moveTo(point[0], point[1]);
        else shape.lineTo(point[0], point[1]);
      });
      shape.closePath();

      const geometry = new THREE.ExtrudeGeometry(shape, {
        depth: thickness,
        bevelEnabled: true,
        bevelSegments: 2,
        bevelSize: 0.025,
        bevelThickness: 0.018,
        curveSegments: 3
      });

      geometry.rotateX(Math.PI / 2);
      return new THREE.Mesh(geometry, material);
    }

    const wingPoints = [
      [0.55, -1.0],
      [3.6, 0.48],
      [3.78, 1.25],
      [0.58, 1.25]
    ];

    const greenWingPoints = [
      [1.05, -0.72],
      [2.92, 0.53],
      [3.12, 0.9],
      [1.05, 0.96]
    ];

    const tailWingPoints = [
      [0.42, 0.85],
      [2.05, 1.65],
      [2.15, 2.15],
      [0.42, 1.96]
    ];

    for (const side of [-1, 1]) {
      const wingPivot = new THREE.Group();
      wingPivot.scale.x = side;
      jet.add(wingPivot);

      const wing = makeHorizontalPrism(wingPoints, 0.13, jetWhite);
      wing.position.y = -0.08;
      wing.castShadow = true;
      wingPivot.add(wing);

      const greenWing = makeHorizontalPrism(greenWingPoints, 0.025, jetGreen);
      greenWing.position.y = 0.075;
      wingPivot.add(greenWing);

      const tailWing = makeHorizontalPrism(tailWingPoints, 0.085, jetWhiteShadow);
      tailWing.position.y = 0.06;
      wingPivot.add(tailWing);

      const wingTip = new THREE.Mesh(
        new THREE.SphereGeometry(0.13, 16, 12),
        jetRed
      );
      wingTip.position.set(3.55, 0.02, 0.86);
      wingTip.scale.y = 0.4;
      wingPivot.add(wingTip);

      const tipGlow = makeGlow(side < 0 ? 0xff6ca8 : 0x7affdf, 0.42, 0.8);
      tipGlow.position.set(3.56, 0.04, 0.86);
      wingPivot.add(tipGlow);
    }

    // Vertical fins.
    function makeFinGeometry() {
      const vertices = new Float32Array([
        -0.06, 0.22, 1.05,
         0.06, 0.22, 1.05,
         0.06, 1.45, 1.86,
        -0.06, 1.45, 1.86,

        -0.06, 0.22, 2.04,
         0.06, 0.22, 2.04,
         0.06, 0.22, 1.45,
        -0.06, 0.22, 1.45
      ]);

      const indices = [
        0, 1, 2, 0, 2, 3,
        4, 6, 5, 4, 7, 6,
        0, 4, 5, 0, 5, 1,
        3, 2, 6, 3, 6, 7,
        0, 3, 7, 0, 7, 4,
        1, 5, 6, 1, 6, 2
      ];

      const geometry = new THREE.BufferGeometry();
      geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
      geometry.setIndex(indices);
      geometry.computeVertexNormals();
      return geometry;
    }

    const finMaterial = jetGreen.clone();
    finMaterial.side = THREE.DoubleSide;

    const leftFin = new THREE.Mesh(makeFinGeometry(), finMaterial);
    leftFin.scale.x = 0.72;
    leftFin.position.x = -0.48;
    jet.add(leftFin);

    const rightFin = new THREE.Mesh(makeFinGeometry(), finMaterial);
    rightFin.scale.x = 0.72;
    rightFin.position.x = 0.48;
    jet.add(rightFin);

    const engineGlows = [];

    for (const x of [-0.49, 0.49]) {
      const engine = new THREE.Mesh(
        new THREE.CylinderGeometry(0.3, 0.37, 1.35, 24),
        jetDark
      );
      engine.rotation.x = Math.PI / 2;
      engine.position.set(x, -0.34, 1.66);
      engine.castShadow = true;
      jet.add(engine);

      const engineRing = new THREE.Mesh(
        new THREE.TorusGeometry(0.28, 0.045, 8, 28),
        jetGreen
      );
      engineRing.position.set(x, -0.34, 2.32);
      jet.add(engineRing);

      const exhaust = new THREE.Mesh(
        new THREE.CircleGeometry(0.245, 28),
        engineGlowMaterial.clone()
      );
      exhaust.position.set(x, -0.34, 2.345);
      jet.add(exhaust);

      const exhaustGlow = makeGlow(0x7dffe6, 1.25, 0.75);
      exhaustGlow.position.set(x, -0.34, 2.42);
      jet.add(exhaustGlow);
      engineGlows.push(exhaustGlow);

      const flame = new THREE.Mesh(
        new THREE.ConeGeometry(0.25, 1.4, 20, 1, true),
        new THREE.MeshBasicMaterial({
          color: 0x62ffdf,
          transparent: true,
          opacity: 0.22,
          blending: THREE.AdditiveBlending,
          depthWrite: false,
          side: THREE.DoubleSide,
          toneMapped: false
        })
      );
      flame.rotation.x = Math.PI / 2;
      flame.position.set(x, -0.34, 3.0);
      jet.add(flame);
      engineGlows.push(flame);
    }

    const cockpitGlow = makeGlow(0x9dbfff, 0.8, 0.3);
    cockpitGlow.position.set(0, 0.78, -1.12);
    jet.add(cockpitGlow);

    // Flattened pseudo-planar jet reflection.
    const jetReflection = jet.clone(true);
    jetReflection.scale.set(1, 0.085, 0.92);
    jetReflection.position.set(4.25, 0.065, -2.1);

    jetReflection.traverse((object) => {
      if (object.isMesh || object.isSprite) {
        object.castShadow = false;
        object.receiveShadow = false;
        object.renderOrder = 4;

        const materials = Array.isArray(object.material)
          ? object.material
          : [object.material];

        materials.forEach((material) => {
          material.transparent = true;
          material.opacity *= object.isSprite ? 0.11 : 0.105;
          material.depthWrite = false;
          material.blending = THREE.AdditiveBlending;
        });
      }
    });

    world.add(jetReflection);

    // ─────────────────────────────────────────────────────────────
    // Space Bunny character
    // ─────────────────────────────────────────────────────────────

    const bunny = new THREE.Group();
    bunny.position.set(-2.6, 0.41, 1.45);
    bunny.rotation.y = 0.27;
    world.add(bunny);

    const bunnySoft = new THREE.Group();
    bunny.add(bunnySoft);

    const bunnyWhite = standardMaterial(0xf3f0e9, 0.72, 0.02);
    const bunnyCream = standardMaterial(0xe9e1d8, 0.78, 0.01);
    const bunnyPink = standardMaterial(0xf0a8bd, 0.7, 0.02);
    const suitWhite = standardMaterial(0xdce8e8, 0.46, 0.22);
    const suitDark = standardMaterial(0x182c43, 0.42, 0.45);
    const suitGreen = standardMaterial(0x49d5a1, 0.36, 0.3);
    const suitRed = standardMaterial(0xec4b73, 0.38, 0.25);
    const visorMaterial = new THREE.MeshPhysicalMaterial({
      color: 0x80e5dc,
      emissive: 0x1b615d,
      emissiveIntensity: 0.3,
      transparent: true,
      opacity: 0.36,
      metalness: 0.1,
      roughness: 0.08,
      clearcoat: 1,
      side: THREE.DoubleSide,
      depthWrite: false
    });

    const eyeWhite = standardMaterial(0xffffff, 0.3, 0.02);
    const eyeDark = new THREE.MeshBasicMaterial({ color: 0x0b1630 });
    const eyeCyan = new THREE.MeshBasicMaterial({ color: 0x96fff0 });

    // Body and suit.
    addEllipsoid(bunnySoft, bunnyCream, [0, 1.12, 0], [0.56, 0.72, 0.47], 32);
    addEllipsoid(bunnySoft, suitWhite, [0, 1.05, 0.04], [0.49, 0.56, 0.43], 28);

    const bellyPanel = addEllipsoid(
      bunnySoft,
      suitDark,
      [0, 1.21, 0.39],
      [0.31, 0.35, 0.09],
      24
    );
    bellyPanel.rotation.x = -0.04;

    addEllipsoid(
      bunnySoft,
      suitGreen,
      [0, 0.91, 0.43],
      [0.23, 0.08, 0.07],
      18
    );

    const collar = new THREE.Mesh(
      new THREE.TorusGeometry(0.37, 0.095, 12, 40),
      suitRed
    );
    collar.rotation.x = Math.PI / 2;
    collar.position.set(0, 1.63, 0.02);
    collar.castShadow = true;
    bunnySoft.add(collar);

    // Chest star badge.
    const badge = new THREE.Mesh(
      new THREE.ShapeGeometry(makeStarShape(0.14, 0.06)),
      new THREE.MeshBasicMaterial({
        color: 0x83ffe0,
        side: THREE.DoubleSide,
        toneMapped: false
      })
    );
    badge.position.set(0, 1.26, 0.505);
    bunnySoft.add(badge);

    // Head.
    const head = new THREE.Group();
    head.position.set(0, 1.98, 0.015);
    bunnySoft.add(head);

    addEllipsoid(head, bunnyWhite, [0, 0, 0], [0.62, 0.56, 0.52], 36);
    addEllipsoid(head, bunnyCream, [0, -0.13, 0.32], [0.39, 0.26, 0.23], 28);

    // Eyes.
    for (const x of [-0.225, 0.225]) {
      addEllipsoid(head, eyeWhite, [x, 0.045, 0.435], [0.18, 0.205, 0.11], 24);
      addEllipsoid(head, eyeDark, [x + (x < 0 ? 0.025 : -0.012), 0.035, 0.535], [0.085, 0.135, 0.04], 20);
      addEllipsoid(head, eyeCyan, [x + 0.025, 0.092, 0.574], [0.029, 0.04, 0.012], 12);
    }

    // Futuristic pilot goggles.
    for (const x of [-0.225, 0.225]) {
      addEllipsoid(head, visorMaterial, [x, 0.06, 0.565], [0.205, 0.18, 0.045], 24);

      const lensFrame = new THREE.Mesh(
        new THREE.TorusGeometry(0.17, 0.025, 8, 30),
        suitGreen
      );
      lensFrame.position.set(x, 0.06, 0.59);
      lensFrame.scale.y = 0.92;
      head.add(lensFrame);
    }

    const goggleBridge = new THREE.Mesh(
      new THREE.BoxGeometry(0.12, 0.045, 0.055),
      suitGreen
    );
    goggleBridge.position.set(0, 0.07, 0.59);
    head.add(goggleBridge);

    // Nose, cheeks, smile.
    addEllipsoid(
      head,
      new THREE.MeshBasicMaterial({ color: 0xef7795 }),
      [0, -0.09, 0.59],
      [0.075, 0.052, 0.045],
      16
    );

    for (const x of [-0.34, 0.34]) {
      addEllipsoid(
        head,
        new THREE.MeshBasicMaterial({
          color: 0xffa9c3,
          transparent: true,
          opacity: 0.52,
          depthWrite: false
        }),
        [x, -0.13, 0.48],
        [0.095, 0.047, 0.025],
        16
      );
    }

    const smile = new THREE.Mesh(
      new THREE.TorusGeometry(0.105, 0.014, 7, 24, Math.PI),
      new THREE.MeshBasicMaterial({ color: 0x4b3042 })
    );
    smile.position.set(0, -0.2, 0.585);
    smile.rotation.z = Math.PI;
    head.add(smile);

    // Long soft ears.
    const ears = [];

    for (const side of [-1, 1]) {
      const ear = new THREE.Group();
      ear.position.set(side * 0.27, 0.4, -0.015);
      ear.rotation.z = side * -0.09;
      ear.userData.side = side;
      head.add(ear);

      addEllipsoid(
        ear,
        bunnyWhite,
        [0, 0.72, 0],
        [0.19, 0.79, 0.13],
        28
      );

      addEllipsoid(
        ear,
        bunnyPink,
        [0, 0.7, 0.105],
        [0.092, 0.55, 0.035],
        24
      );

      const earTip = addEllipsoid(
        ear,
        side < 0 ? suitGreen : suitRed,
        [0, 1.38, 0.005],
        [0.105, 0.13, 0.075],
        18
      );
      earTip.castShadow = true;

      const earBand = new THREE.Mesh(
        new THREE.TorusGeometry(0.14, 0.024, 8, 24),
        suitWhite
      );
      earBand.rotation.x = Math.PI / 2;
      earBand.position.y = 1.17;
      ear.add(earBand);

      ears.push(ear);
    }

    // Antenna and star.
    const antennaStem = new THREE.Mesh(
      new THREE.CylinderGeometry(0.018, 0.018, 0.38, 10),
      suitWhite
    );
    antennaStem.position.set(0.42, 0.6, -0.02);
    antennaStem.rotation.z = -0.14;
    head.add(antennaStem);

    const antennaStar = new THREE.Mesh(
      new THREE.SphereGeometry(0.085, 12, 8),
      new THREE.MeshBasicMaterial({
        color: 0xff75b0,
        toneMapped: false
      })
    );
    antennaStar.position.set(0.445, 0.82, -0.02);
    head.add(antennaStar);

    const antennaGlow = makeGlow(0xff79b8, 0.45, 0.8);
    antennaGlow.position.copy(antennaStar.position);
    head.add(antennaGlow);

    // Arms.
    const arms = [];

    for (const side of [-1, 1]) {
      const arm = new THREE.Group();
      arm.position.set(side * 0.45, 1.47, 0.015);
      arm.rotation.z = side * -0.12;
      bunnySoft.add(arm);

      addEllipsoid(arm, suitWhite, [0, -0.24, 0], [0.13, 0.28, 0.13], 20);
      addEllipsoid(arm, suitDark, [0, -0.49, 0.015], [0.145, 0.16, 0.14], 20);
      addEllipsoid(arm, bunnyWhite, [0, -0.57, 0.025], [0.11, 0.12, 0.115], 18);

      const armBand = new THREE.Mesh(
        new THREE.TorusGeometry(0.125, 0.025, 8, 20),
        side < 0 ? suitGreen : suitRed
      );
      armBand.rotation.x = Math.PI / 2;
      armBand.position.y = -0.42;
      arm.add(armBand);

      arms.push(arm);
    }

    // Legs and little jet boots.
    const legs = [];

    for (const side of [-1, 1]) {
      const leg = new THREE.Group();
      leg.position.set(side * 0.245, 0.7, 0.02);
      bunnySoft.add(leg);

      addEllipsoid(leg, suitWhite, [0, -0.25, 0], [0.15, 0.27, 0.15], 20);
      addEllipsoid(leg, suitDark, [0, -0.5, 0.075], [0.17, 0.15, 0.25], 22);
      addEllipsoid(leg, suitRed, [0, -0.57, 0.19], [0.16, 0.08, 0.12], 20);
      addEllipsoid(leg, suitGreen, [0, -0.62, 0.3], [0.12, 0.025, 0.055], 16);

      const bootGlow = makeGlow(0x76ffe3, 0.42, 0.55);
      bootGlow.position.set(0, -0.63, 0.33);
      leg.add(bootGlow);

      legs.push(leg);
    }

    // Backpack and small thrusters.
    const backpack = addEllipsoid(
      bunnySoft,
      suitDark,
      [0, 1.18, -0.48],
      [0.35, 0.44, 0.16],
      24
    );
    backpack.rotation.x = -0.08;

    for (const x of [-0.18, 0.18]) {
      const thruster = new THREE.Mesh(
        new THREE.CylinderGeometry(0.085, 0.105, 0.2, 16),
        suitWhite
      );
      thruster.position.set(x, 0.96, -0.55);
      thruster.rotation.x = Math.PI / 2;
      bunnySoft.add(thruster);

      const thrusterGlow = makeGlow(0x7dffe2, 0.35, 0.7);
      thrusterGlow.position.set(x, 0.96, -0.69);
      bunnySoft.add(thrusterGlow);
    }

    // Soft bunny tail.
    addEllipsoid(bunnySoft, bunnyWhite, [0, 1.1, -0.61], [0.25, 0.25, 0.22], 24);
    addEllipsoid(bunnySoft, bunnyCream, [0.16, 1.14, -0.66], [0.15, 0.15, 0.14], 20);

    // Jump halo on the platform.
    const jumpHaloMaterial = new THREE.MeshBasicMaterial({
      color: 0x77ffdc,
      transparent: true,
      opacity: 0,
      blending: THREE.AdditiveBlending,
      depthWrite: false,
      side: THREE.DoubleSide,
      toneMapped: false
    });

    const jumpHalo = new THREE.Mesh(
      new THREE.RingGeometry(0.85, 0.91, 72),
      jumpHaloMaterial
    );
    jumpHalo.rotation.x = -Math.PI / 2;
    jumpHalo.position.set(-2.6, 0.445, 1.45);
    world.add(jumpHalo);

    // ─────────────────────────────────────────────────────────────
    // Fireflies / star dust
    // ─────────────────────────────────────────────────────────────

    const fireflyCount = 85;
    const fireflyPositions = new Float32Array(fireflyCount * 3);
    const fireflyBase = new Float32Array(fireflyCount * 3);
    const fireflyPhase = new Float32Array(fireflyCount);

    for (let i = 0; i < fireflyCount; i++) {
      const x = random(-8, 8);
      const y = random(0.45, 5.5);
      const z = random(-8, 5);

      fireflyBase[i * 3] = x;
      fireflyBase[i * 3 + 1] = y;
      fireflyBase[i * 3 + 2] = z;

      fireflyPositions[i * 3] = x;
      fireflyPositions[i * 3 + 1] = y;
      fireflyPositions[i * 3 + 2] = z;

      fireflyPhase[i] = random(0, TAU);
    }

    const fireflyGeometry = new THREE.BufferGeometry();
    fireflyGeometry.setAttribute(
      "position",
      new THREE.BufferAttribute(fireflyPositions, 3)
    );

    const fireflies = new THREE.Points(
      fireflyGeometry,
      new THREE.PointsMaterial({
        color: 0xb4ffe9,
        size: 0.055,
        transparent: true,
        opacity: 0.8,
        blending: THREE.AdditiveBlending,
        depthWrite: false
      })
    );
    world.add(fireflies);

    // A few distant dream rocks.
    const rockMaterial = standardMaterial(0x302b52, 0.92, 0.05);

    [
      [-12, 0.7, -9, 0.9],
      [11, 0.55, -8, 0.65],
      [-8, 1.4, -17, 0.55]
    ].forEach(([x, y, z, scale], index) => {
      const rock = new THREE.Mesh(
        new THREE.DodecahedronGeometry(scale, 0),
        rockMaterial
      );
      rock.position.set(x, y, z);
      rock.rotation.set(random(0, 1), random(0, TAU), random(0, 1));
      rock.castShadow = true;
      rock.receiveShadow = true;
      world.add(rock);

      if (index === 0) {
        const glow = makeGlow(0x9e70d4, 1.2, 0.32);
        glow.position.set(x, y, z);
        world.add(glow);
      }
    });

    // ─────────────────────────────────────────────────────────────
    // Cinematic camera
    // ─────────────────────────────────────────────────────────────

    const cameraPath = new THREE.CatmullRomCurve3(
      [
        new THREE.Vector3(7.8, 3.45, 12.4),
        new THREE.Vector3(5.5, 2.85, 10.2),
        new THREE.Vector3(2.1, 2.35, 8.6),
        new THREE.Vector3(-1.8, 2.65, 9.2),
        new THREE.Vector3(-4.4, 3.15, 12.1),
        new THREE.Vector3(3.8, 3.85, 14.0)
      ],
      true,
      "catmullrom",
      0.42
    );

    const pointer = { x: 0, y: 0 };
    const pointerSmooth = { x: 0, y: 0 };
    const lookTarget = new THREE.Vector3(0.45, 1.55, -0.55);
    const desiredCamera = new THREE.Vector3();

    window.addEventListener("pointermove", (event) => {
      pointer.x = (event.clientX / window.innerWidth - 0.5) * 2;
      pointer.y = (event.clientY / window.innerHeight - 0.5) * 2;
    }, { passive: true });

    window.addEventListener("pointerleave", () => {
      pointer.x = 0;
      pointer.y = 0;
    });

    // ─────────────────────────────────────────────────────────────
    // Animation
    // ─────────────────────────────────────────────────────────────

    function animate() {
      requestAnimationFrame(animate);

      const delta = Math.min(clock.getDelta(), 0.05);
      const t = clock.elapsedTime;
      const motionTime = reducedMotion ? t * 0.35 : t;

      seaUniforms.uTime.value = t;
      auroraUniforms.uTime.value = t;

      // Slow breathing camera loop.
      const cameraProgress = (motionTime / 42) % 1;
      cameraPath.getPointAt(cameraProgress, desiredCamera);

      pointerSmooth.x = lerp(pointerSmooth.x, pointer.x, 0.035);
      pointerSmooth.y = lerp(pointerSmooth.y, pointer.y, 0.035);

      desiredCamera.x += pointerSmooth.x * 0.52;
      desiredCamera.y -= pointerSmooth.y * 0.28;
      camera.position.copy(desiredCamera);

      lookTarget.set(
        0.45 + Math.sin(t * 0.08) * 0.16,
        1.56 + Math.sin(t * 0.11) * 0.08,
        -0.55 + Math.cos(t * 0.07) * 0.2
      );
      camera.lookAt(lookTarget);

      // Bunny: run cycle, jump, squash/stretch, breathing, springy ears.
      const cycle = motionTime % 2.8;
      const jumpDuration = 1.16;
      const jumpPhase = cycle < jumpDuration ? cycle / jumpDuration : 0;
      const jump = cycle < jumpDuration
        ? Math.sin(jumpPhase * Math.PI)
        : 0;

      let landingSquash = 0;
      if (cycle > jumpDuration && cycle < jumpDuration + 0.34) {
        landingSquash = Math.sin(
          ((cycle - jumpDuration) / 0.34) * Math.PI
        );
      }

      const takeoffSquash = cycle < jumpDuration
        ? Math.abs(Math.cos(jumpPhase * Math.PI)) * (1 - jump) * 0.5
        : 0;

      const squash = Math.max(landingSquash, takeoffSquash);

      bunny.position.y = 0.41 + jump * 1.12;
      bunny.position.x = -2.6 + Math.sin(motionTime * 0.48) * 0.035;
      bunny.rotation.y = 0.27 + Math.sin(motionTime * 0.24) * 0.055;

      bunnySoft.scale.set(
        1 + jump * 0.055 + squash * 0.075,
        1 + jump * 0.07 - squash * 0.13,
        1 + jump * 0.035 + squash * 0.045
      );

      bunnySoft.position.y = Math.sin(t * 1.8) * 0.015;
      bunnySoft.rotation.z = Math.sin(motionTime * 0.62) * 0.026;

      head.rotation.z = -Math.sin(motionTime * 0.7) * 0.035;
      head.rotation.x = jump * -0.055;

      ears.forEach((ear, index) => {
        const side = ear.userData.side;
        const delay = index * 0.22;

        ear.rotation.z =
          side * -0.09 +
          Math.sin(motionTime * 1.72 + delay) * 0.12 +
          Math.sin(motionTime * 2.24 + delay) * 0.045;

        ear.rotation.x =
          -jump * 0.16 +
          Math.sin(motionTime * 1.35 + delay) * 0.075;
      });

      legs.forEach((leg, index) => {
        const side = index === 0 ? 1 : -1;
        leg.rotation.x = jump * side * 0.76;
        leg.rotation.z = Math.sin(motionTime * 1.4 + index) * 0.015;
      });

      arms.forEach((arm, index) => {
        const side = index === 0 ? -1 : 1;
        arm.rotation.x = -jump * side * 0.78;
        arm.rotation.z =
          side * -0.12 +
          Math.sin(motionTime * 1.55 + index * 0.6) * 0.045;
      });

      jumpHalo.visible = jump > 0.03;
      jumpHalo.scale.setScalar(0.7 + jump * 0.62);
      jumpHaloMaterial.opacity = jump * 0.28;

      // Jet hover and sea-skimming movement.
      const jetX = 4.25 + Math.sin(motionTime * 0.23) * 0.42;
      const jetZ = -2.1 + Math.sin(motionTime * 0.17 + 1) * 0.26;
      const jetY = 1.54 + Math.sin(motionTime * 0.77) * 0.13;

      jet.position.set(jetX, jetY, jetZ);
      jet.rotation.x = -0.025 + Math.sin(motionTime * 0.4) * 0.018;
      jet.rotation.y = -0.12 + Math.sin(motionTime * 0.19) * 0.055;
      jet.rotation.z = Math.sin(motionTime * 0.47) * 0.032;

      jetReflection.position.set(jetX, 0.065, jetZ);
      jetReflection.rotation.copy(jet.rotation);
      jetReflection.scale.y = 0.085 + Math.sin(t * 0.8) * 0.004;

      engineGlows.forEach((glow, index) => {
        const pulse = 0.92 + Math.sin(t * 4.5 + index * 1.4) * 0.11;
        if (glow.isSprite) glow.scale.setScalar(1.25 * pulse);
        else glow.scale.y = pulse;
      });

      jetShadow.position.set(jetX, 0.055, jetZ);
      jetShadow.material.opacity = 0.42 + (1 - (jetY - 1.35) / 0.3) * 0.08;
      jetShadow.scale.x = 1 + Math.sin(t * 0.23) * 0.05;

      // Cloud breathing.
      clouds.forEach((cloud, index) => {
        cloud.position.x =
          cloud.userData.baseX +
          Math.sin(t * cloud.userData.speed + cloud.userData.phase) * 0.75;

        cloud.position.y +=
          Math.sin(t * 0.16 + cloud.userData.phase) * 0.0007;

        cloud.rotation.y =
          Math.sin(t * 0.045 + index) * 0.05;
      });

      // Sky sign always remains readable, like a floating dream device.
      sign.lookAt(camera.position);
      sign.rotation.z = -0.025 + Math.sin(t * 0.34) * 0.018;
      sign.position.y = 5.05 + Math.sin(t * 0.32) * 0.09;
      signOrbit.rotation.z = -0.28 + t * 0.025;
      signOrbit2.rotation.z = 2.35 - t * 0.018;
      signDot.material.opacity = 0.55 + Math.sin(t * 2) * 0.2;

      // Firefly drift.
      const positionAttribute = fireflyGeometry.attributes.position;
      for (let i = 0; i < fireflyCount; i++) {
        const phase = fireflyPhase[i];
        const baseX = fireflyBase[i * 3];
        const baseY = fireflyBase[i * 3 + 1];
        const baseZ = fireflyBase[i * 3 + 2];

        positionAttribute.setXYZ(
          i,
          baseX + Math.sin(t * 0.4 + phase) * 0.16,
          baseY + Math.sin(t * 0.68 + phase) * 0.18,
          baseZ + Math.cos(t * 0.32 + phase) * 0.12
        );
      }
      positionAttribute.needsUpdate = true;

      // Platform edge lights breathe subtly.
      platformLights.forEach((light, index) => {
        const s = 0.85 + Math.sin(t * 1.4 + index * 0.52) * 0.15;
        light.scale.setScalar(s);
      });

      // Small delta so animated props remain smooth on high-refresh displays.
      camera.updateMatrixWorld();
      renderer.render(scene, camera);
    }

    // ─────────────────────────────────────────────────────────────
    // Resize and startup
    // ─────────────────────────────────────────────────────────────

    function resize() {
      const width = window.innerWidth;
      const height = window.innerHeight;

      camera.aspect = width / height;
      camera.fov = width < 700 ? 52 : 42;
      camera.updateProjectionMatrix();

      renderer.setPixelRatio(Math.min(window.devicePixelRatio, width < 700 ? 1.5 : 1.8));
      renderer.setSize(width, height);
    }

    window.addEventListener("resize", resize);
    resize();

    renderer.compile(scene, camera);
    animate();

    requestAnimationFrame(() => {
      setTimeout(() => loader.classList.add("ready"), 500);
    });

    canvas.addEventListener("webglcontextlost", (event) => {
      event.preventDefault();
      loader.classList.remove("ready");
      loader.querySelector(".loader-subtitle").textContent =
        "WebGL 暂时中断,请刷新页面重新进入梦境";
    });
  })();
  </script>
</body>
</html>
0.69s
TTFT
Actual wait 1.05s
70
Reasoning TPS
158s · 11,072 tok
180
Output TPS
96.8s · 17,454 tok
28,526
Total Tokens (official)
Input 645
Timing details
Actual wait (client)
1.05s
Client preparation
0.04s
Proxy preparation
0.00s
Upstream response headers
0.69s
Upstream first byte
0.69s
Upstream first token
0.69s
Upstream retries
0

Actual wait uses the client clock. Upstream timings start after proxy preparation and are cumulative; do not add these values. Upstream delay includes network, scheduling, processing and buffering; it does not prove provider queueing.

Diagnostic receipt
{
  "attempts": 1,
  "startedAt": "2026-09-30T06:53:28.313Z",
  "transport": "railway",
  "ttftSource": "proxy",
  "clientTtftMs": 1054.9000000953674,
  "completionId": "070be4e897fccedb7ef95e66062df534",
  "streamResumes": 5,
  "proxyPrepareMs": 1,
  "proxyRequestId": "b92fraDKSo6LMHMYlt7tkg",
  "upstreamStatus": 200,
  "upstreamTtftMs": 692,
  "clientHeadersMs": 360.80000019073486,
  "clientPrepareMs": 36.80000019073486,
  "upstreamHeadersMs": 692,
  "upstreamFirstByteMs": 692,
  "clientFirstContentMs": 158857.59999990463
}
Elapsed 256s · Average 112 tok/s · Peak 276 tok/s