太空兔子

5 个模型 · AI拯救打工人 · 分享快照
分享与嵌入

社交平台直接发布;GitHub/README 使用 Markdown 或 Badge。

XFacebook微博LinkedIn
[![太空兔子 在 TOKRACE 上的速度结果](/api/badge/share/upa81KJfIf?locale=zh-CN)](/zh-CN/r/upa81KJfIf)
请创建一个 可直接运行的单文件 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 场景。
复跑这次评测完整复跑会带走标题、备注、Prompt 和模型;Prompt 复跑只复用任务文本。
显示
🏁 实时竞速当前阶段速度 token/s · 快的在前思考 / 正文量 · token首响等待 · 短更好

黄色显示思考TPS,绿色显示正文输出TPS;进行中为实时估算,结束后与卡片「输出TPS」一致。速度不代表回答质量。

0500 token/s
DeepSeek-Flash
⚡378 token/s
55,631 token
🚦0.46s
MiniMax-M3.1-Flash-Preview
168 token/s
38,115 token
首响 0.61s
小米 MiMo-V2.6 Flash
154 token/s
83,369 token
首响 3.14s
OpenRouter
151 token/s
61,194 token
首响 1.23s
智谱 GLM-5.3-flash
73 token/s
49,286 token
首响 0.77s
🏁 本轮结算
🏆 最快
DeepSeek-Flash
378 tok/s
✓ 任务效果
尚未评估
DeepSeek-Flash
deepseek-flash
完成

历史结果 · 2026/9/29 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
首Token
实际等待 0.71s
228
思考TPS
194s · 44,398 tok
378
输出TPS
29.8s · 11,233 tok
55,631
总Tokens(官方)
输入 501
计时详情
实际等待(客户端)
0.71s
客户端请求准备
0.04s
本站代理准备
0.00s
上游响应头到达
0.34s
上游首字节到达
0.34s
上游首 Token 到达
0.46s
上游重试次数
0

实际等待使用客户端时钟;上游计时从本站准备完成后开始,各项为累计耗时,不能直接相加。上游等待包含网络、调度、计算及缓冲,不能仅凭这些数值断定厂商排队。

诊断记录
{
  "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
}
总用时 225s · 平均 248 tok/s · 峰值 434 tok/s
智谱 GLM-5.3-flash
glm-5.3-flash
完成

历史结果 · 2026/9/29 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 1
0.77s
首Token
实际等待 1.05s
38
思考TPS
839s · 32,142 tok
73
输出TPS
234s · 17,144 tok
49,286
总Tokens(官方)
输入 500
计时详情
实际等待(客户端)
1.05s
客户端请求准备
0.04s
本站代理准备
0.00s
上游响应头到达
0.77s
上游首字节到达
0.77s
上游首 Token 到达
0.77s
上游重试次数
0

实际等待使用客户端时钟;上游计时从本站准备完成后开始,各项为累计耗时,不能直接相加。上游等待包含网络、调度、计算及缓冲,不能仅凭这些数值断定厂商排队。

诊断记录
{
  "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
}
总用时 1074s · 平均 46 tok/s · 峰值 132 tok/s
小米 MiMo-V2.6 Flash
mimo-v2.6-flash
完成

历史结果 · 2026/9/29 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
首Token
实际等待 3.40s
89
思考TPS
825s · 73,286 tok
154
输出TPS
65.3s · 10,083 tok
83,369
总Tokens(官方)
输入 525
计时详情
实际等待(客户端)
3.40s
客户端请求准备
0.05s
本站代理准备
0.00s
上游响应头到达
3.06s
上游首字节到达
3.06s
上游首 Token 到达
3.14s
上游重试次数
0

实际等待使用客户端时钟;上游计时从本站准备完成后开始,各项为累计耗时,不能直接相加。上游等待包含网络、调度、计算及缓冲,不能仅凭这些数值断定厂商排队。

诊断记录
{
  "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
}
总用时 894s · 平均 94 tok/s · 峰值 199 tok/s
OpenRouter
stealth/space-bunny-alpha
完成

历史结果 · 2026/9/29 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
首Token
实际等待 1.47s
70
思考TPS
563s · ≈39,351 tok
151
输出TPS
145s · ≈21,843 tok
61,194
总Tokens(官方)
输入 597
计时详情
实际等待(客户端)
1.47s
客户端请求准备
0.05s
本站代理准备
0.00s
上游响应头到达
1.23s
上游首字节到达
1.23s
上游首 Token 到达
1.23s
上游重试次数
0

实际等待使用客户端时钟;上游计时从本站准备完成后开始,各项为累计耗时,不能直接相加。上游等待包含网络、调度、计算及缓冲,不能仅凭这些数值断定厂商排队。

诊断记录
{
  "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
}
总用时 710s · 平均 86 tok/s · 峰值 258 tok/s
MiniMax-M3.1-Flash-Preview
MiniMax-M3
完成

新增测试 · 2026/9/30 06:19:11

The user wants me to create a single HTML file that contains a complete Web3D scene with the theme "Space Bunny". Let me break down the requirements: 1. **Theme**: Space Bunny - a cute, slightly cool astronaut bunny with long ears, round body, bouncy movements, and futuristic equipment 2. **Setting**: Dreamy space coastline with: - Deep blue to purple gradient sky - Volumetric purple clouds - Reflective water surface - White, green, red futuristic fighter jet 3. **Animation**: - Bunny bouncing with ear wobble - Jet hovering/sweeping - Living atmosphere 4. **Materials**: Sea reflection, cloud layers, jet paint, sky gradient, lighting 5. **Camera**: Cinematic - slow push-in, slight orbit, low angle, depth of field 6. **Text**: "just breathe" integrated as 3D text or sky slogan or floating device 7. **Single HTML file**, no complex external dependencies Let me plan the technical approach: - Use Three.js (CDN) for 3D rendering - Custom geometry for the bunny (procedural) - Custom geometry for the jet - Particle/clouds with shaders or simple geometries - Reflective water plane - Animated camera with smooth movement - Text3D or sprite for "just breathe" - Procedural cloud billboards or volumetric approach Let me design the bunny: - Round body (sphere/ellipsoid) - Head (smaller sphere) - Long ears (cylinders or custom shapes) - Eyes (small dark spheres) - Cute helmet with visor - Small body suit - Backpack with lights Jet design: - Sleek fuselage - Wings with white, green, red colors - Cockpit canopy - Engine glow - Hovering animation with slight bobbing Water: - Plane with reflective shader or simple animated normal map - Could use Three.js Reflector for actual reflection, but that might be expensive. Let me use a custom approach with animated normal map Clouds: - Multiple billboard sprites with soft purple textures - Or volumetric cloud-like shapes (multiple spheres with semi-transparent material) Let me write the code now. I'll use Three.js r128 or similar stable version from CDN. For the camera: - Start from a low position, looking up - Slowly orbit around the scene - Push in towards bunny For the bunny animation: - Position with sin wave for bouncing - Rotation tilt on bounce - Ears with delayed follow movement - Slight breathing scale Let me structure the HTML file: ```html <!DOCTYPE html> <html> <head> <title>Space Bunny</title> <style> /* Reset, body, canvas styles */ /* Loading screen */ /* HUD elements */ </style> </head> <body> <div id="loading">...</div> <div id="hud">...</div> <canvas id="canvas"></canvas> <script src="three.js cdn"></script> <script> // Scene setup // Bunny construction // Jet construction // Environment (sky, clouds, water) // Text "just breathe" // Animation loop </script> </body> </html> ``` Let me start writing the detailed code. I'll use a more recent Three.js version that has good OrbitControls but keep the code self-contained. Actually, for simplicity and reliability, I'll use Three.js r128 from CDN with unpkg or jsdelivr. Let me think about the bunny design more carefully: Bunny parts: 1. Body: Round, slightly elongated sphere - cream/white color with slight glow 2. Head: Smaller sphere on top 3. Ears: Two long curved tubes/capsules that can wiggle 4. Eyes: Two small black ovals with white highlights 5. Helmet: Transparent dome with neon trim 6. Suit: Body suit with shoulder pads, gloves details 7. Boots: Small chunky boots 8. Chest light: Glowing chest piece 9. Jetpack or backpack: Small rocket pack The space suit should have: - Orange/white color scheme - Glowing seams - Helmet with translucent visor Let me make the bunny more "cool" - maybe with a sleek white suit with cyan/green accents and a transparent helmet. Jet design: - Main fuselage: Sleek white body - Wings: Swept back, white with green and red stripes - Cockpit: Green tinted canopy - Engines: Red glow at back - Landing gear could be retracted since it's hovering For the "just breathe" text: - Make it as 3D extruded geometry from canvas texture - Or as a glowing billboard/sprite - Place it floating in the sky or as a holographic projection Let me think about performance: - Keep poly count low - Use instancing where possible - Limit particles - Use simple shaders I'll proceed with the implementation now. Let me write the full HTML file. Actually, let me think about the cloud rendering. Volumetric clouds are expensive. I'll use: - Multiple layered sprite-based clouds with soft alpha textures - Generated using canvas or as procedurally placed geometric shapes For the water: - Use a simple reflective approach with a textured plane - Animate UV scrolling - Or use a normal map to fake waves Let me code this up now. I'll be thorough but efficient. ```javascript // Main components: // 1. Scene, camera, renderer // 2. Lighting (ambient, directional for sun, point lights for jet glow) // 3. Sky gradient (back sphere with shader) // 4. Sea plane (custom shader for waves and reflection) // 5. Clouds (multiple groups of soft particles) // 6. Fighter jet (procedural geometry) // 7. Space Bunny (procedural geometry) // 8. "just breathe" text (3D or sprite) // 9. Camera animation (cinematic path) // 10. Animation loop ``` Let me write detailed implementation: For the sky gradient, I'll use a large sphere with a shader that creates a gradient from deep blue at horizon to purple/black at top. For the sea, I'll use a plane with custom shader that: - Animates waves via vertex displacement - Uses normal map for surface detail - Reflects the sky color and clouds - Has specular highlights For clouds, I'll create cloud groups using multiple overlapping sphere geometries with semi-transparent material, or use particle sprites. For the jet, I'll create procedural geometry using various primitives combined. For the bunny, I'll create it from primitives too. Let me write the code. I'll aim for ~500-800 lines of well-organized JavaScript. Actually let me think about a creative approach to clouds. I'll use a combination of: - Particle sprites (billboards) with soft circular textures - Generated as instanced meshes or sprites - Positioned in 3D space at various distances For the "just breathe" text, I think I'll do a 3D floating text effect. Let me use TextGeometry but that requires loading a font. Alternative: - Use canvas texture for the text - Make it a billboard that always faces camera - Add glow/bloom effect via emissive material Let me also think about camera animation: - Start with low angle looking up at the scene - Slowly rise and orbit around - Push in towards the bunny at key moments - Have slight bob for breathing feel OK let me just write the code now. I'll keep it modular within the single file. I'll use Three.js from CDN: ```html <script src="https://unpkg.com/three@0.158.0/build/three.min.js"></script> ``` Or better, use the modular version. Actually, let me use a stable build like r152 or similar. Let me start coding: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Space Bunny — just breathe</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0420; font-family: 'Helvetica Neue', Arial, sans-serif; } canvas { display: block; } #loading { position: fixed; inset: 0; z-index: 100; background: linear-gradient(180deg, #0a0420 0%, #2a1456 100%); display: flex; align-items: center; justify-content: center; color: #fff; font-size: 1.4em; letter-spacing: 0.4em; transition: opacity 1.2s ease; } #loading.hidden { opacity: 0; pointer-events: none; } #caption { position: fixed; bottom: 28px; left: 0; right: 0; text-align: center; color: rgba(255,255,255,0.8); font-size: 14px; letter-spacing: 0.5em; z-index: 10; pointer-events: none; text-shadow: 0 0 10px rgba(180,120,255,0.6); } #vi

下面是一个完整的单文件 HTML Web3D 作品,主题为 Space Bunny —— just breathe。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Space Bunny — just breathe</title>
<style>
  *{margin:0;padding:0;box-sizing:border-box}
  html,body{width:100%;height:100%;overflow:hidden;background:#0a0420;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif}
  canvas{display:block}

  #loading{
    position:fixed;inset:0;z-index:100;
    background:radial-gradient(ellipse at center,#3a1a6a 0%,#0a0420 100%);
    display:flex;flex-direction:column;align-items:center;justify-content:center;
    color:rgba(255,255,255,.85);font-size:13px;letter-spacing:.6em;
    transition:opacity 1.6s ease;
  }
  #loading.hidden{opacity:0;pointer-events:none}
  #loading .pulse{width:6px;height:6px;background:#aaeeff;border-radius:50%;margin-top:30px;box-shadow:0 0 20px #aaeeff,0 0 40px #88ccff;animation:p 1.4s ease-in-out infinite}
  @keyframes p{0%,100%{opacity:.3;transform:scale(1)}50%{opacity:1;transform:scale(1.6)}}

  #caption{
    position:fixed;bottom:38px;left:0;right:0;text-align:center;
    color:rgba(255,240,255,.9);font-size:11px;letter-spacing:.9em;
    z-index:10;pointer-events:none;font-weight:200;
    text-shadow:0 0 18px rgba(180,120,255,.9),0 0 35px rgba(120,80,200,.5);
    opacity:0;animation:fadeIn 2.2s ease 1.8s forwards;
  }
  @keyframes fadeIn{to{opacity:1}}

  #vignette{
    position:fixed;inset:0;pointer-events:none;z-index:5;
    background:radial-gradient(ellipse at center,transparent 38%,rgba(0,0,0,.55) 100%);
  }

  .corner{position:fixed;color:rgba(255,255,255,.55);font-size:9px;letter-spacing:.4em;z-index:10;pointer-events:none;text-transform:uppercase;font-weight:300;opacity:0;animation:fadeIn 1.5s ease 1s forwards}
  .corner.tl{top:28px;left:32px}
  .corner.tr{top:28px;right:32px;text-align:right}
  .corner.bl{bottom:28px;left:32px}
  .corner.br{bottom:28px;right:32px;text-align:right}
  .corner .d{display:inline-block;width:5px;height:5px;background:#66ffcc;border-radius:50%;margin-right:8px;vertical-align:middle;box-shadow:0 0 10px #66ffcc;animation:blink 1.8s ease-in-out infinite}
  @keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
</style>
</head>
<body>
<div id="loading">S&nbsp;P&nbsp;A&nbsp;C&nbsp;E&nbsp;&nbsp;B&nbsp;U&nbsp;N&nbsp;N&nbsp;Y<div class="pulse"></div></div>
<div class="corner tl"><span class="d"></span>Sector 07 / Lunar Bay</div>
<div class="corner tr">Mission 042<br>↟ 22°C · 0.8 atm</div>
<div class="corner bl">Cam 01 · Tracking</div>
<div class="corner br">▮ REC ●</div>
<div id="caption">just&nbsp;·&nbsp;breathe</div>
<div id="vignette"></div>

<script src="https://unpkg.com/three@0.152.0/build/three.min.js"></script>
<script>
(function(){
  'use strict';

  // ===================== Setup =====================
  let W = innerWidth, H = innerHeight;
  const renderer = new THREE.WebGLRenderer({antialias:true, powerPreference:'high-performance'});
  renderer.setSize(W,H);
  renderer.setPixelRatio(Math.min(devicePixelRatio,2));
  renderer.outputEncoding = THREE.sRGBEncoding;
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 1.05;
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  document.body.appendChild(renderer.domElement);

  const scene = new THREE.Scene();
  scene.fog = new THREE.FogExp2(0x1a0a3a, 0.010);

  const camera = new THREE.PerspectiveCamera(40, W/H, 0.1, 260);
  camera.position.set(22, 4, 18);

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

  // ===================== Lighting =====================
  scene.add(new THREE.AmbientLight(0x6a5aa8, 0.45));
  scene.add(new THREE.HemisphereLight(0xb89aff, 0x2a1456, 0.55));

  const sun = new THREE.DirectionalLight(0xffd9a8, 1.5);
  sun.position.set(25, 30, -12);
  sun.castShadow = true;
  sun.shadow.mapSize.set(2048,2048);
  sun.shadow.camera.left=-22; sun.shadow.camera.right=22;
  sun.shadow.camera.top=22;   sun.shadow.camera.bottom=-22;
  sun.shadow.camera.near=0.5; sun.shadow.camera.far=90;
  sun.shadow.bias=-0.0008;
  scene.add(sun);

  const fill = new THREE.DirectionalLight(0x8870ff, 0.55);
  fill.position.set(-18, 12, 18);
  scene.add(fill);

  const rim = new THREE.DirectionalLight(0xff88cc, 0.45);
  rim.position.set(-10, 6, -22);
  scene.add(rim);

  // ===================== Sky =====================
  const skyMat = new THREE.ShaderMaterial({
    side: THREE.BackSide,
    depthWrite: false,
    uniforms:{
      uHorizon:{value:new THREE.Color(0x4a1e88)},
      uMid:    {value:new THREE.Color(0x6a30b8)},
      uZenith: {value:new THREE.Color(0x0a0420)},
      uSunCol: {value:new THREE.Color(0xffd9a8)},
      uSunDir: {value:new THREE.Vector3(0.55,0.4,-0.78).normalize()}
    },
    vertexShader:`
      varying vec3 vWP;
      void main(){
        vec4 wp = modelMatrix * vec4(position,1.0);
        vWP = wp.xyz;
        gl_Position = projectionMatrix * viewMatrix * wp;
      }
    `,
    fragmentShader:`
      uniform vec3 uHorizon, uMid, uZenith, uSunCol, uSunDir;
      varying vec3 vWP;
      float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
      void main(){
        vec3 dir = normalize(vWP);
        float h = dir.y;
        vec3 col = mix(uHorizon, uMid, smoothstep(0.0,0.35,h));
        col = mix(col, uZenith, smoothstep(0.35,0.9,h));
        // sun
        float s = max(dot(dir, normalize(uSunDir)),0.0);
        col += uSunCol * pow(s,96.0) * 0.9;
        col += uSunCol * pow(s,4.0) * 0.18;
        // stars
        if(h > 0.05){
          vec2 uv = vec2(atan(dir.z, dir.x)*22.0, asin(h)*22.0);
          vec2 id = floor(uv);
          float r = hash(id);
          if(r>0.972){
            float b = (r-0.972)*36.0;
            vec2 fc = fract(uv)-0.5;
            float d = length(fc);
            col += vec3(1.0,0.95,1.0)*b*smoothstep(0.5,0.0,d)*smoothstep(0.05,0.3,h);
          }
        }
        // distant nebula tint
        col += vec3(0.15,0.05,0.25)*smoothstep(0.0,0.6,h)*0.3;
        gl_FragColor = vec4(col,1.0);
      }
    `
  });
  scene.add(new THREE.Mesh(new THREE.SphereGeometry(130,32,32), skyMat));

  // ===================== Sea =====================
  const seaMat = new THREE.ShaderMaterial({
    uniforms:{
      uTime:{value:0},
      uDeep:{value:new THREE.Color(0x0a0420)},
      uShallow:{value:new THREE.Color(0x4a1ea0)},
      uHighlight:{value:new THREE.Color(0xffd9b0)},
      uSunDir:{value:new THREE.Vector3(0.55,0.4,-0.78).normalize()}
    },
    vertexShader:`
      uniform float uTime;
      varying vec3 vN, vWP; varying float vW;
      void main(){
        vec3 p = position;
        float w1 = sin(p.x*0.32 + uTime*0.6)*0.28;
        float w2 = cos(p.y*0.4 + uTime*0.45)*0.22;
        float w3 = sin((p.x+p.y)*0.55 + uTime*0.9)*0.14;
        float totalW = w1+w2+w3;
        p.z += totalW;
        vW = totalW;
        float nx = -cos(p.x*0.32 + uTime*0.6)*0.32*0.28 - cos((p.x+p.y)*0.55+uTime*0.9)*0.55*0.14;
        float ny = -cos(p.y*0.4 + uTime*0.45)*0.4*0.22 - cos((p.x+p.y)*0.55+uTime*0.9)*0.55*0.14;
        vN = normalize(vec3(nx,ny,1.0));
        vec4 wp = modelMatrix*vec4(p,1.0);
        vWP = wp.xyz;
        gl_Position = projectionMatrix*viewMatrix*wp;
      }
    `,
    fragmentShader:`
      uniform vec3 uDeep, uShallow, uHighlight, uSunDir;
      varying vec3 vN, vWP; varying float vW;
      void main(){
        vec3 V = normalize(cameraPosition - vWP);
        vec3 N = normalize(vN);
        float fres = pow(1.0-max(dot(V,N),0.0), 3.0);
        vec3 skyTint = mix(uShallow*1.3, vec3(0.7,0.45,1.0), 0.35);
        vec3 base = mix(uDeep, skyTint, fres);
        // specular
        vec3 L = normalize(uSunDir);
        vec3 H = normalize(V+L);
        float spec = pow(max(dot(N,H),0.0), 72.0);
        base += uHighlight * spec * 0.85;
        // sparkle
        float sp = sin(vW*30.0 + vW*vW*60.0);
        sp = smoothstep(0.88, 1.0, sp);
        base += vec3(1.0,0.95,0.85) * sp * 0.18 * fres;
        // distance fade
        float d = length(vWP.xz);
        base = mix(base, uDeep*0.5, smoothstep(40.0,120.0,d));
        gl_FragColor = vec4(base,1.0);
      }
    `
  });
  const sea = new THREE.Mesh(new THREE.PlaneGeometry(320,320,96,96), seaMat);
  sea.rotation.x = -Math.PI/2;
  sea.receiveShadow = true;
  scene.add(sea);

  // ===================== Sun lens flare =====================
  (function(){
    const c = document.createElement('canvas'); c.width=128; c.height=128;
    const x = c.getContext('2d');
    const g = x.createRadialGradient(64,64,0,64,64,60);
    g.addColorStop(0,'rgba(255,255,210,1)');
    g.addColorStop(0.25,'rgba(255,220,170,0.5)');
    g.addColorStop(1,'rgba(255,200,150,0)');
    x.fillStyle=g; x.fillRect(0,0,128,128);
    const t = new THREE.CanvasTexture(c);
    const sp = new THREE.Sprite(new THREE.SpriteMaterial({
      map:t, transparent:true, blending:THREE.AdditiveBlending, depthWrite:false, opacity:0.95
    }));
    sp.position.set(70,40,-90); sp.scale.set(22,22,1);
    scene.add(sp);
  })();

  // ===================== Distant islands =====================
  const islandMat = new THREE.MeshStandardMaterial({
    color:0x1a0838, roughness:0.95, metalness:0.1,
    emissive:0x2a1058, emissiveIntensity:0.2
  });
  function makeIsland(x,z,s){
    const g = new THREE.Group();
    const main = new THREE.Mesh(new THREE.SphereGeometry(s,16,16,0,Math.PI*2,0,Math.PI*0.5), islandMat);
    main.scale.y = 0.25;
    g.add(main);
    const mtn = new THREE.Mesh(new THREE.ConeGeometry(s*0.45, s*1.4, 8), islandMat);
    mtn.position.y = s*0.55;
    mtn.position.x = (Math.random()-0.5)*s*0.4;
    g.add(mtn);
    g.position.set(x,0,z);
    g.rotation.y = Math.random()*Math.PI;
    g.traverse(o=>{if(o.isMesh){o.castShadow=true;o.receiveShadow=true;}});
    return g;
  }
  scene.add(makeIsland(-55,-30,7));
  scene.add(makeIsland(60,-40,9));
  scene.add(makeIsland(-70,15,5));
  scene.add(makeIsland(70,-5,8));

  // ===================== Clouds =====================
  (function(){
    const c = document.createElement('canvas'); c.width=128; c.height=128;
    const x = c.getContext('2d');
    const g = x.createRadialGradient(64,64,0,64,64,62);
    g.addColorStop(0,'rgba(255,255,255,0.88)');
    g.addColorStop(0.35,'rgba(220,200,255,0.5)');
    g.addColorStop(0.8,'rgba(160,120,220,0.15)');
    g.addColorStop(1,'rgba(160,120,220,0)');
    x.fillStyle=g; x.fillRect(0,0,128,128);
    for(let i=0;i<6;i++){
      const px=30+Math.random()*70, py=30+Math.random()*70, r=10+Math.random()*22;
      const g2 = x.createRadialGradient(px,py,0,px,py,r);
      g2.addColorStop(0,'rgba(255,255,255,0.45)');
      g2.addColorStop(1,'rgba(255,255,255,0)');
      x.fillStyle=g2; x.fillRect(0,0,128,128);
    }
    const tex = new THREE.CanvasTexture(c);

    const colors = [0xc8a8ff, 0xa888d8, 0xd8b8ff, 0xb898e8, 0xe8c8ff];
    for(let i=0;i<22;i++){
      const grp = new THREE.Group();
      const mat = new THREE.SpriteMaterial({
        map:tex, color:colors[i%colors.length],
        transparent:true, opacity:0.55+Math.random()*0.3,
        depthWrite:false
      });
      const n = 7+Math.floor(Math.random()*5);
      const scale = 4+Math.random()*7;
      for(let j=0;j<n;j++){
        const s = new THREE.Sprite(mat);
        const sx = scale*(0.7+Math.random()*0.6);
        const sy = scale*0.5*(0.7+Math.random()*0.4);
        s.scale.set(sx,sy,1);
        s.position.set((Math.random()-0.5)*scale*1.9, (Math.random()-0.5)*scale*0.35, (Math.random()-0.5)*scale*0.7);
        grp.add(s);
      }
      const ang = Math.random()*Math.PI*2;
      const dist = 28+Math.random()*55;
      grp.position.set(Math.cos(ang)*dist, 11+Math.random()*18, Math.sin(ang)*dist-22);
      grp.userData.driftSpeed = 0.15+Math.random()*0.2;
      grp.userData.phase = Math.random()*Math.PI*2;
      scene.add(grp);
    }
  })();

  // ===================== Floating particles =====================
  (function(){
    const N = 260;
    const geo = new THREE.BufferGeometry();
    const pos = new Float32Array(N*3);
    const sz = new Float32Array(N);
    const col = new Float32Array(N*3);
    for(let i=0;i<N;i++){
      pos[i*3]   = (Math.random()-0.5)*90;
      pos[i*3+1] = Math.random()*28;
      pos[i*3+2] = (Math.random()-0.5)*90;
      sz[i] = 0.04+Math.random()*0.18;
      const c = new THREE.Color().setHSL(0.68 + Math.random()*0.18, 0.75, 0.55+Math.random()*0.35);
      col[i*3]=c.r; col[i*3+1]=c.g; col[i*3+2]=c.b;
    }
    geo.setAttribute('position', new THREE.BufferAttribute(pos,3));
    geo.setAttribute('size', new THREE.BufferAttribute(sz,1));
    geo.setAttribute('color', new THREE.BufferAttribute(col,3));
    const mat = new THREE.ShaderMaterial({
      uniforms:{uTime:{value:0}},
      vertexShader:`
        attribute float size; attribute vec3 color;
        varying vec3 vC; varying float a;
        uniform float uTime;
        void main(){
          vC=color;
          vec3 p=position;
          p.y += sin(uTime*0.3 + position.x*0.1)*0.6;
          p.x += cos(uTime*0.2 + position.z*0.1)*0.4;
          vec4 mv = modelViewMatrix*vec4(p,1.0);
          gl_PointSize = size*(220.0/-mv.z);
          gl_Position = projectionMatrix*mv;
          a = 0.4+0.4*sin(uTime*0.8 + position.x*0.05);
        }
      `,
      fragmentShader:`
        varying vec3 vC; varying float a;
        void main(){
          vec2 c=gl_PointCoord-vec2(0.5);
          float d=length(c);
          if(d>0.5) discard;
          float al=(1.0-d*2.0);
          gl_FragColor=vec4(vC, al*a*0.55);
        }
      `,
      transparent:true, depthWrite:false, blending:THREE.AdditiveBlending
    });
    scene.add(new THREE.Points(geo, mat));
    window._particleMat = mat;
  })();

  // ===================== Fighter Jet =====================
  function buildJet(){
    const jet = new THREE.Group();
    const white = new THREE.MeshStandardMaterial({color:0xf5f5f8, roughness:0.32, metalness:0.55, emissive:0x111122, emissiveIntensity:0.1});
    const green = new THREE.MeshStandardMaterial({color:0x55ee77, roughness:0.28, metalness:0.4, emissive:0x116633, emissiveIntensity:0.35});
    const red   = new THREE.MeshStandardMaterial({color:0xee3344, roughness:0.3, metalness:0.5, emissive:0x441111, emissiveIntensity:0.25});
    const dark  = new THREE.MeshStandardMaterial({color:0x222233, roughness:0.4, metalness:0.8});
    const canopyMat = new THREE.MeshPhysicalMaterial({
      color:0x55ee99, roughness:0.05, metalness:0.3,
      transmission:0.6, transparent:true, opacity:0.7, ior:1.4
    });

    // fuselage
    const fus = new THREE.Mesh(new THREE.CylinderGeometry(0.5,0.4,3.6,16), white);
    fus.rotation.z = Math.PI/2;
    jet.add(fus);

    // nose cone
    const nose = new THREE.Mesh(new THREE.ConeGeometry(0.5,1.2,16), dark);
    nose.rotation.z = -Math.PI/2;
    nose.position.x = 2.4;
    jet.add(nose);

    // cockpit canopy
    const canopy = new THREE.Mesh(new THREE.SphereGeometry(0.5,16,10,0,Math.PI*2,0,Math.PI*0.5), canopyMat);
    canopy.scale.set(1.5,0.9,0.9);
    canopy.position.set(1.0,0.45,0);
    canopy.rotation.z = -Math.PI/2;
    jet.add(canopy);

    // main wings - delta
    const wingShape = new THREE.Shape();
    wingShape.moveTo(0,0);
    wingShape.lineTo(2.4,-1.6);
    wingShape.lineTo(2.4,-2.3);
    wingShape.lineTo(0.3,-2.1);
    wingShape.lineTo(-0.5,-0.2);
    wingShape.lineTo(0,0);
    const wingGeo = new THREE.ExtrudeGeometry(wingShape, {depth:0.13, bevelEnabled:false});

    const wL = new THREE.Mesh(wingGeo, white);
    wL.rotation.x = -Math.PI/2;
    wL.position.set(0,0,0.4);
    jet.add(wL);
    const wR = new THREE.Mesh(wingGeo, white);
    wR.rotation.x = -Math.PI/2;
    wR.rotation.y = Math.PI;
    wR.position.set(0,0,-0.4);
    jet.add(wR);

    // green stripes
    const stGeo = new THREE.BoxGeometry(2.0,0.13,0.06);
    const stL = new THREE.Mesh(stGeo, green);
    stL.position.set(0,-0.05,1.55); stL.rotation.y=-0.25;
    jet.add(stL);
    const stR = stL.clone(); stR.position.z=-1.55; stR.rotation.y=0.25; jet.add(stR);

    // red wing tips
    const tipGeo = new THREE.BoxGeometry(0.7,0.14,0.55);
    const tipL = new THREE.Mesh(tipGeo, red); tipL.position.set(1.9,0,2.3); jet.add(tipL);
    const tipR = tipL.clone(); tipR.position.z=-2.3; jet.add(tipR);

    // fins
    const finShape = new THREE.Shape();
    finShape.moveTo(0,0); finShape.lineTo(0,1.2); finShape.lineTo(-0.8,1.0); finShape.lineTo(-0.9,0); finShape.lineTo(0,0);
    const finGeo = new THREE.ExtrudeGeometry(finShape, {depth:0.08, bevelEnabled:false});
    const finL = new THREE.Mesh(finGeo, white); finL.position.set(-1.4,0.2,0.35); finL.rotation.y=Math.PI/2; jet.add(finL);
    const finR = new THREE.Mesh(finGeo, white); finR.position.set(-1.4,0.2,-0.35); finR.rotation.y=-Math.PI/2; jet.add(finR);

    // red fin accent
    const accentGeo = new THREE.BoxGeometry(0.45,0.18,0.06);
    const acL = new THREE.Mesh(accentGeo, red); acL.position.set(-1.4,0.85,0.48); jet.add(acL);
    const acR = acL.clone(); acR.position.z=-0.48; jet.add(acR);

    // engines
    const engGeo = new THREE.CylinderGeometry(0.32,0.28,1.0,16);
    engGeo.rotateZ(Math.PI/2);
    const engMat = new THREE.MeshStandardMaterial({color:0x222233, roughness:0.35, metalness:0.85, emissive:0x331100, emissiveIntensity:0.55});
    const eL = new THREE.Mesh(engGeo, engMat); eL.position.set(-1.9,0,0.55); jet.add(eL);
    const eR = new THREE.Mesh(engGeo, engMat); eR.position.set(-1.9,0,-0.55); jet.add(eR);

    // engine glow
    const glowGeo = new THREE.CircleGeometry(0.32,16);
    const glowMat = new THREE.MeshBasicMaterial({color:0xffaa55, transparent:true, opacity:0.95});
    const gL = new THREE.Mesh(glowGeo, glowMat); gL.position.set(-2.45,0,0.55); gL.rotation.y=Math.PI/2; jet.add(gL);
    const gR = gL.clone(); gR.position.z=-0.55; jet.add(gR);

    // inner glow halo
    const haloGeo = new THREE.CircleGeometry(0.5,16);
    const haloMat = new THREE.MeshBasicMaterial({color:0xff7733, transparent:true, opacity:0.35, blending:THREE.AdditiveBlending, depthWrite:false});
    const hL = new THREE.Mesh(haloGeo, haloMat); hL.position.set(-2.5,0,0.55); hL.rotation.y=Math.PI/2; jet.add(hL);
    const hR = hL.clone(); hR.position.z=-0.55; jet.add(hR);

    // belly
    const belly = new THREE.Mesh(new THREE.BoxGeometry(2.6,0.32,0.85), white);
    belly.position.y = -0.42;
    jet.add(belly);

    // fuselage stripe
    const fuseStripe = new THREE.Mesh(new THREE.CylinderGeometry(0.51,0.41,3.2,16), green);
    fuseStripe.rotation.z = Math.PI/2;
    fuseStripe.scale.set(1.01, 0.25, 1.01);
    jet.add(fuseStripe);

    // point light at jet
    const jL = new THREE.PointLight(0xff8844, 2.2, 10);
    jL.position.set(-2.5,0,0);
    jet.add(jL);

    jet.traverse(o=>{if(o.isMesh){o.castShadow=true;o.receiveShadow=true;}});
    return {group:jet, glowL:gL, glowR:gR, haloL:hL, haloR:hR};
  }
  const jetObj = buildJet();
  scene.add(jetObj.group);

  // ===================== Space Bunny =====================
  function buildBunny(){
    const bunny = new THREE.Group();

    const fur = new THREE.MeshStandardMaterial({color:0xfaf5ed, roughness:0.55, metalness:0.05, emissive:0x1a1030, emissiveIntensity:0.04});
    const suit = new THREE.MeshStandardMaterial({color:0xf2f2f8, roughness:0.4, metalness:0.32, emissive:0x223344, emissiveIntensity:0.06});
    const accent = new THREE.MeshStandardMaterial({color:0x44eecc, roughness:0.25, metalness:0.5, emissive:0x44eecc, emissiveIntensity:0.85});
    const bootMat = new THREE.MeshStandardMaterial({color:0x3355ee, roughness:0.35, metalness:0.5, emissive:0x1144aa, emissiveIntensity:0.45});
    const gloveMat = new THREE.MeshStandardMaterial({color:0x223a66, roughness:0.4, metalness:0.5, emissive:0x113355, emissiveIntensity:0.45});
    const helmetMat = new THREE.MeshStandardMaterial({
      color:0xaaddff, roughness:0.08, metalness:0.55,
      transparent:true, opacity:0.32,
      emissive:0x224466, emissiveIntensity:0.25
    });
    const innerEarMat = new THREE.MeshStandardMaterial({color:0xffb8c8, roughness:0.7, emissive:0x441122, emissiveIntensity:0.18});

    // body
    const bodyGeo = new THREE.SphereGeometry(0.7,32,32);
    bodyGeo.scale(0.95,1.1,1.0);
    const body = new THREE.Mesh(bodyGeo, fur);
    body.position.y = 1.1;
    bunny.add(body);

    // head
    const head = new THREE.Mesh(new THREE.SphereGeometry(0.65,32,32), fur);
    head.position.y = 2.15;
    head.scale.set(1,0.95,0.95);
    bunny.add(head);

    // cheeks
    const cheekGeo = new THREE.SphereGeometry(0.18,16,16);
    const chL = new THREE.Mesh(cheekGeo, fur); chL.position.set(-0.42,2.0,0.42); bunny.add(chL);
    const chR = chL.clone(); chR.position.x=0.42; bunny.add(chR);

    // ears
    function makeEar(side){
      const e = new THREE.Group();
      const path = new THREE.CatmullRomCurve3([
        new THREE.Vector3(0,0,0),
        new THREE.Vector3(0.06*side, 0.4, 0.06),
        new THREE.Vector3(0.12*side, 0.8, -0.04),
        new THREE.Vector3(0.18*side, 1.15, -0.14)
      ]);
      const outer = new THREE.Mesh(new THREE.TubeGeometry(path,16,0.12,12,false), fur);
      e.add(outer);

      const innerPath = new THREE.CatmullRomCurve3([
        new THREE.Vector3(0.02*side, 0.1, 0.04),
        new THREE.Vector3(0.09*side, 0.5, 0.1),
        new THREE.Vector3(0.15*side, 0.9, 0.0),
        new THREE.Vector3(0.2*side, 1.1, -0.1)
      ]);
      const inner = new THREE.Mesh(new THREE.TubeGeometry(innerPath,12,0.07,10,false), innerEarMat);
      e.add(inner);

      const tip = new THREE.Mesh(new THREE.SphereGeometry(0.075,12,12), new THREE.MeshBasicMaterial({color:0xff88dd}));
      tip.position.set(0.18*side, 1.15, -0.14);
      e.add(tip);

      e.position.set(0.24*side, 2.62, -0.05);
      return e;
    }
    const earL = makeEar(-1);
    const earR = makeEar(1);
    bunny.add(earL, earR);

    // eyes
    const eyeGeo = new THREE.SphereGeometry(0.135,20,20);
    const eyeMat = new THREE.MeshStandardMaterial({color:0x0a0a18, roughness:0.2, metalness:0.3});
    const eyeL = new THREE.Mesh(eyeGeo, eyeMat); eyeL.position.set(-0.24,2.27,0.55); bunny.add(eyeL);
    const eyeR = eyeL.clone(); eyeR.position.x=0.24; bunny.add(eyeR);

    // eye highlights
    const hl1Geo = new THREE.SphereGeometry(0.05,12,12);
    const hlMat = new THREE.MeshBasicMaterial({color:0xffffff});
    const hl1L = new THREE.Mesh(hl1Geo, hlMat); hl1L.position.set(-0.19,2.32,0.66); bunny.add(hl1L);
    const hl1R = hl1L.clone(); hl1R.position.x=0.29; bunny.add(hl1R);

    const hl2Geo = new THREE.SphereGeometry(0.025,8,8);
    const hl2L = new THREE.Mesh(hl2Geo, hlMat); hl2L.position.set(-0.27,2.22,0.63); bunny.add(hl2L);
    const hl2R = hl2L.clone(); hl2R.position.x=0.27; bunny.add(hl2R);

    // nose
    const noseMat = new THREE.MeshStandardMaterial({color:0xff7799, roughness:0.55});
    const nose = new THREE.Mesh(new THREE.SphereGeometry(0.075,12,12), noseMat);
    nose.position.set(0,2.04,0.66);
    nose.scale.set(1,0.75,0.8);
    bunny.add(nose);

    // mouth (tiny smile)
    const mouthGeo = new THREE.TorusGeometry(0.05,0.012,8,12,Math.PI);
    const mouth = new THREE.Mesh(mouthGeo, new THREE.MeshStandardMaterial({color:0x441122}));
    mouth.position.set(0,1.99,0.68);
    mouth.rotation.z = Math.PI;
    bunny.add(mouth);

    // helmet
    const helmet = new THREE.Mesh(new THREE.SphereGeometry(0.85,32,24,0,Math.PI*2,0,Math.PI*0.55), helmetMat);
    helmet.position.y = 2.3;
    helmet.rotation.x = Math.PI;
    bunny.add(helmet);

    // helmet rim
    const rim = new THREE.Mesh(new THREE.TorusGeometry(0.85,0.04,12,32), accent);
    rim.position.y = 2.3;
    rim.rotation.x = Math.PI/2;
    bunny.add(rim);

    // antenna
    const ant = new THREE.Mesh(new THREE.CylinderGeometry(0.015,0.015,0.5,8), suit);
    ant.position.set(0.5,3.15,-0.05);
    ant.rotation.z = -0.5;
    bunny.add(ant);

    const antTip = new THREE.Mesh(new THREE.SphereGeometry(0.065,12,12), new THREE.MeshBasicMaterial({color:0xff5577}));
    antTip.position.set(0.72,3.32,0.0);
    bunny.add(antTip);

    // chest piece
    const chestGeo = new THREE.SphereGeometry(0.55,16,16);
    chestGeo.scale(0.7,0.9,0.5);
    const chest = new THREE.Mesh(chestGeo, suit);
    chest.position.set(0,1.4,0.5);
    bunny.add(chest);

    // chest light
    const chestLight = new THREE.Mesh(new THREE.CircleGeometry(0.18,24), new THREE.MeshBasicMaterial({color:0x88ffee, transparent:true, opacity:0.95}));
    chestLight.position.set(0,1.4,0.79);
    bunny.add(chestLight);

    // chest ring
    const chestRing = new THREE.Mesh(new THREE.TorusGeometry(0.2,0.04,12,24), accent);
    chestRing.position.set(0,1.4,0.8);
    bunny.add(chestRing);

    // suit lines
    const lineGeo = new THREE.TorusGeometry(0.65,0.02,8,32,Math.PI*1.2);
    const lineL = new THREE.Mesh(lineGeo, accent); lineL.position.set(-0.05,1.0,0); lineL.rotation.y=Math.PI/2; lineL.rotation.z=0.3; bunny.add(lineL);
    const lineR = lineL.clone(); lineR.rotation.z=-0.3; bunny.add(lineR);

    // arms
    const armGeo = new THREE.CapsuleGeometry(0.14,0.4,8,16);
    const armL = new THREE.Mesh(armGeo, suit); armL.position.set(-0.85,1.4,0); armL.rotation.z=0.6; bunny.add(armL);
    const armR = armL.clone(); armR.position.x=0.85; armR.rotation.z=-0.6; bunny.add(armR);

    // gloves
    const gloveGeo = new THREE.SphereGeometry(0.18,16,16);
    const gL = new THREE.Mesh(gloveGeo, gloveMat); gL.position.set(-1.1,1.0,0.2); gL.scale.set(1,0.9,1.2); bunny.add(gL);
    const gR = gL.clone(); gR.position.x=1.1; gR.position.z=-0.2; bunny.add(gR);

    // legs
    const legGeo = new THREE.CapsuleGeometry(0.16,0.35,8,16);
    const legL = new THREE.Mesh(legGeo, suit); legL.position.set(-0.3,0.45,0); bunny.add(legL);
    const legR = legL.clone(); legR.position.x=0.3; bunny.add(legR);

    // boots
    const bootGeo = new THREE.SphereGeometry(0.28,16,16);
    bootGeo.scale(0.85,0.6,1.4);
    const bootL = new THREE.Mesh(bootGeo, bootMat); bootL.position.set(-0.3,0.15,0.08); bunny.add(bootL);
    const bootR = bootL.clone(); bootR.position.x=0.3; bunny.add(bootR);

    // boot thrusters
    const bgGeo = new THREE.CircleGeometry(0.18,16);
    const bgMat = new THREE.MeshBasicMaterial({color:0xff88dd, transparent:true, opacity:0.9});
    const bgL = new THREE.Mesh(bgGeo, bgMat); bgL.position.set(-0.3,-0.08,0.16); bgL.rotation.x=-Math.PI/2; bunny.add(bgL);
    const bgR = bgL.clone(); bgR.position.x=0.3; bunny.add(bgR);

    // jetpack
    const pack = new THREE.Mesh(new THREE.BoxGeometry(0.7,0.65,0.3), suit);
    pack.position.set(0,1.4,-0.55);
    bunny.add(pack);

    const packThrustGeo = new THREE.CylinderGeometry(0.1,0.12,0.2,12);
    const packThrustMat = new THREE.MeshStandardMaterial({color:0x222233, roughness:0.4, metalness:0.7, emissive:0xff6622, emissiveIntensity:0.65});
    const ptL = new THREE.Mesh(packThrustGeo, packThrustMat); ptL.position.set(-0.2,1.05,-0.55); ptL.rotation.x=Math.PI/2; bunny.add(ptL);
    const ptR = ptL.clone(); ptR.position.x=0.2; bunny.add(ptR);

    // tail
    const tail = new THREE.Mesh(new THREE.SphereGeometry(0.2,16,16), fur);
    tail.position.set(0,0.95,-0.55);
    bunny.add(tail);

    // chest point light
    const cpL = new THREE.PointLight(0x44ffee, 0.9, 3.5);
    cpL.position.set(0,1.4,0.95);
    bunny.add(cpL);

    bunny.traverse(o=>{if(o.isMesh){o.castShadow=true;o.receiveShadow=true;}});

    return {group:bunny, ears:[earL,earR], chestLight, body, head, bgL, bgR};
  }
  const bunny = buildBunny();
  bunny.group.position.set(2.5, 0.15, 1.0);
  scene.add(bunny.group);

  // ===================== Platform =====================
  const platMat = new THREE.MeshStandardMaterial({color:0x4a3a8a, roughness:0.55, metalness:0.4, emissive:0x221066, emissiveIntensity:0.25});
  const plat = new THREE.Mesh(new THREE.CylinderGeometry(1.4,1.7,0.35,16), platMat);
  plat.position.set(2.5,-0.18,1.0);
  plat.castShadow = true; plat.receiveShadow = true;
  scene.add(plat);

  const platRing = new THREE.Mesh(new THREE.TorusGeometry(1.45,0.05,12,32), new THREE.MeshBasicMaterial({color:0x66ffcc}));
  platRing.position.set(2.5,0.05,1.0);
  platRing.rotation.x = Math.PI/2;
  scene.add(platRing);

  // tiny antenna on platform
  const platAnt = new THREE.Mesh(new THREE.CylinderGeometry(0.04,0.06,1.2,8), platMat);
  platAnt.position.set(2.5,0.6,1.0);
  platAnt.castShadow = true;
  scene.add(platAnt);
  const platAntTip = new THREE.Mesh(new THREE.SphereGeometry(0.08,12,12), new THREE.MeshBasicMaterial({color:0x88ffcc}));
  platAntTip.position.set(2.5,1.25,1.0);
  scene.add(platAntTip);

  // ===================== "just breathe" text =====================
  function makeTextTexture(text, fontSize, color, glowColor, letterSpacing){
    const c = document.createElement('canvas');
    const x = c.getContext('2d');
    x.font = `200 ${fontSize}px Helvetica, Arial, sans-serif`;
    const m = x.measureText(text);
    const padX = 100, padY = 60;
    c.width = m.width + padX;
    c.height = fontSize * 1.6 + padY;
    x.font = `200 ${fontSize}px Helvetica, Arial, sans-serif`;
    x.textBaseline = 'middle';
    x.textAlign = 'center';
    x.shadowBlur = 40; x.shadowColor = glowColor;
    x.fillStyle = color;
    x.fillText(text, c.width/2, c.height/2);
    x.shadowBlur = 20;
    x.fillText(text, c.width/2, c.height/2);
    x.shadowBlur = 8;
    x.fillText(text, c.width/2, c.height/2);
    x.shadowBlur = 0;
    x.fillText(text, c.width/2, c.height/2);
    const t = new THREE.CanvasTexture(c);
    t.colorSpace = THREE.sRGBEncoding;
    t.anisotropy = 8;
    return t;
  }

  // Big sky banner text
  const skyTextTex = makeTextTexture('just breathe', 180, '#ffffff', 'rgba(180,120,255,0.9)');
  const skyText = new THREE.Mesh(
    new THREE.PlaneGeometry(24, 5.2),
    new THREE.MeshBasicMaterial({map:skyTextTex, transparent:true, depthWrite:false, opacity:0.78, blending:THREE.AdditiveBlending})
  );
  skyText.position.set(-18, 16, -52);
  scene.add(skyText);

  // Holographic floating text near bunny
  const holoTex = makeTextTexture('just breathe', 64, '#aaeeff', 'rgba(120,220,255,1)');
  const holo = new THREE.Mesh(
    new THREE.PlaneGeometry(3.2, 0.85),
    new THREE.MeshBasicMaterial({map:holoTex, transparent:true, depthWrite:false, opacity:0.85, blending:THREE.AdditiveBlending})
  );
  holo.position.set(2.5, 4.2, 1.0);
  scene.add(holo);

  // ===================== Animation =====================
  const clock = new THREE.Clock();
  let t = 0;
  const jetPath = {radius:14, baseY:7, angSpeed:0.08};

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

    seaMat.uniforms.uTime.value = t;
    if(window._particleMat) window._particleMat.uniforms.uTime.value = t;

    // ----- Bunny bounce -----
    const bPhase = t * 2.6;
    const bounce = Math.abs(Math.sin(bPhase)) * 1.4;
    const grounded = Math.sin(bPhase) > 0;
    bunny.group.position.y = 0.15 + bounce;

    // squash & stretch
    const sq = grounded ? 1.05 : 0.95;
    const st = grounded ? 0.95 : 1.12;
    bunny.body.scale.set(sq, st, sq);
    bunny.head.scale.set(sq*0.95, st*0.95, sq*0.95);

    // tilt with bounce
    bunny.group.rotation.x = Math.cos(bPhase) * 0.08;

    // gentle horizontal sway
    bunny.group.position.x = 2.5 + Math.sin(t*0.55)*0.18;
    bunny.group.position.z = 1.0 + Math.cos(t*0.42)*0.18;

    // slow body rotation
    bunny.group.rotation.y = Math.sin(t*0.28)*0.2 - 0.2;

    // head look around
    bunny.head.rotation.y = Math.sin(t*0.4)*0.18;
    bunny.head.rotation.x = Math.sin(t*0.35)*0.06;

    // ear animation
    const earSway = Math.sin(t*2.8)*0.18 + Math.sin(t*1.4)*0.08;
    const earBounce = Math.cos(bPhase*2)*0.12;
    bunny.ears[0].rotation.z = -0.1 + earSway;
    bunny.ears[0].rotation.x = earBounce;
    bunny.ears[1].rotation.z = 0.1 - earSway;
    bunny.ears[1].rotation.x = earBounce;

    // chest light pulse
    const pulse = 0.7 + Math.sin(t*3.2)*0.3;
    bunny.chestLight.material.opacity = pulse;

    // boot thruster flicker
    const bootFlick = 0.7 + Math.sin(t*12)*0.15 + Math.sin(t*23)*0.1;
    bunny.bgL.material.opacity = bootFlick;
    bunny.bgR.material.opacity = bootFlick * 0.9;

    // ----- Jet -----
    const jetAng = t * jetPath.angSpeed + 1.2;
    jetObj.group.position.x = Math.cos(jetAng) * jetPath.radius;
    jetObj.group.position.z = Math.sin(jetAng) * jetPath.radius;
    jetObj.group.position.y = jetPath.baseY + Math.sin(t*0.7)*0.4;
    jetObj.group.rotation.y = -jetAng + Math.PI/2 + Math.sin(t*0.4)*0.08;
    jetObj.group.rotation.z = Math.sin(t*0.5)*0.04 - 0.08;

    // engine glow flicker
    const eg = 0.85 + Math.sin(t*15)*0.1;
    jetObj.glowL.material.opacity = eg;
    jetObj.glowR.material.opacity = eg * 0.95;
    jetObj.haloL.material.opacity = 0.3 + Math.sin(t*8)*0.08;
    jetObj.haloR.material.opacity = 0.3 + Math.sin(t*8 + 1)*0.08;

    // ----- Camera (cinematic orbit) -----
    const camT = t * 0.07;
    const camR = 19 + Math.sin(t*0.08)*4;
    const camA = camT + 0.8;
    const camHeight = 4 + Math.sin(t*0.11)*1.6;
    camera.position.x = Math.cos(camA) * camR;
    camera.position.y = camHeight;
    camera.position.z = Math.sin(camA) * camR;
    // tiny breathing shake
    camera.position.x += Math.sin(t*7)*0.012;
    camera.position.y += Math.cos(t*5)*0.012;

    // look target drifts
    const look = new THREE.Vector3(
      2.5 + Math.sin(t*0.1)*1.2,
      2.8 + Math.sin(t*0.15)*0.4,
      1.0 + Math.cos(t*0.1)*1.2
    );
    camera.lookAt(look);

    // ----- Texts -----
    skyText.lookAt(camera.position);
    skyText.material.opacity = 0.6 + Math.sin(t*0.6)*0.15;

    holo.position.x = bunny.group.position.x + 0.6;
    holo.position.y = bunny.group.position.y + 4.0;
    holo.position.z = bunny.group.position.z + 0.3;
    holo.lookAt(camera.position);
    holo.material.opacity = 0.7 + Math.sin(t*1.8)*0.15;

    // ----- Platform ring pulse -----
    platRing.material.color.setHSL(0.45, 0.8, 0.5 + Math.sin(t*2.2)*0.15);

    // ----- Clouds drift -----
    scene.children.forEach(o=>{
      if(o.userData && o.userData.driftSpeed){
        o.position.x += dt * o.userData.driftSpeed;
        o.position.y += Math.sin(t*0.3 + o.userData.phase)*0.005;
        // recycle
        if(o.position.x > 60) o.position.x = -60;
      }
    });

    renderer.render(scene, camera);
  }

  setTimeout(()=>document.getElementById('loading').classList.add('hidden'), 700);
  animate();
})();
</script>
</body>
</html>

作品亮点

🌌 角色 (Space Bunny)

  • 圆润奶油色身体 + 大长耳朵(带粉色内耳与发光耳尖)
  • 半透明青色头盔 + 金边光环 + 小天线(红尖)
  • 胸前脉动核心灯 + 太空服荧光线条 + 火箭靴 + 喷射尾焰

✈️ 战斗机

  • 白/绿/红三色涂装:机身绿色腰条、机翼红色翼尖、尾翼红色装饰
  • 绿色透明座舱罩、双引擎橙红色喷焰(带 halo 加法混合光晕)

🌊 梦境海岸

  • 自定义海面 shader:多重正弦波位移、菲涅尔反射、太阳高光、波峰闪烁
  • 深紫到靛蓝的天空 shader:地平线→中天→天顶渐变 + 太阳辉光 + 星点
  • 22 朵蓬松的体积云(多重 sprite 叠加)+ 5 个岛屿剪影
  • 260 个发光粒子营造氛围

🎬 镜头语言

  • 缓慢环绕 + 高度起伏 + 视点漂移
  • 微弱"呼吸感"相机抖动
  • 永远盯着兔子区域的有故事感的运镜

💫 "just breathe"

  • 天空巨型霓虹标语(相机始终朝向)
  • 兔子前方全息投影(小字版跟随兔子位置)
  • 底部 HUD 文字 + 四角遥测感 HUD

直接把这段代码保存为 .html 文件双击打开即可运行(需联网加载 Three.js CDN)。

0.61s
首Token
实际等待 1.43s
154
思考TPS
169s · 26,038 tok
168
输出TPS
71.7s · 12,077 tok
38,115
总Tokens(官方)
输入 617
计时详情
实际等待(客户端)
1.43s
客户端请求准备
0.42s
本站代理准备
0.00s
上游响应头到达
0.61s
上游首字节到达
0.61s
上游首 Token 到达
0.61s
上游重试次数
0

实际等待使用客户端时钟;上游计时从本站准备完成后开始,各项为累计耗时,不能直接相加。上游等待包含网络、调度、计算及缓冲,不能仅凭这些数值断定厂商排队。

诊断记录
{
  "attempts": 1,
  "startedAt": "2026-09-30T06:19:12.451Z",
  "transport": "railway",
  "ttftSource": "proxy",
  "clientTtftMs": 1433.6999998092651,
  "completionId": "070bdce07717256cddfd339fb67fad57",
  "streamResumes": 5,
  "proxyPrepareMs": 2,
  "proxyRequestId": "e9dcYC8KQcS4ANs4pHNmDw",
  "upstreamStatus": 200,
  "upstreamTtftMs": 610,
  "clientHeadersMs": 669.9000000953674,
  "clientPrepareMs": 416.7999997138977,
  "upstreamHeadersMs": 606,
  "upstreamFirstByteMs": 609,
  "clientFirstContentMs": 170530.19999980927
}
总用时 242s · 平均 158 tok/s · 峰值 357 tok/s