commit e17b09ff6d59bb08f37a0bdb584000473294c4c3 Author: como Date: Wed Mar 25 17:28:56 2026 +0800 init diff --git a/.claude/skills/threejs b/.claude/skills/threejs new file mode 160000 index 0000000..b1c6230 --- /dev/null +++ b/.claude/skills/threejs @@ -0,0 +1 @@ +Subproject commit b1c623076c661fc9b03dac19292e825a5d106823 diff --git a/.claude/skills/webgpu b/.claude/skills/webgpu new file mode 160000 index 0000000..4adcfe8 --- /dev/null +++ b/.claude/skills/webgpu @@ -0,0 +1 @@ +Subproject commit 4adcfe8ef2317eae0801dbf4a396844954f8e996 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46342b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Environment +.env +.env.local +.env.*.local diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ea8d75 --- /dev/null +++ b/README.md @@ -0,0 +1,212 @@ +# 写实无尽海洋场景 + +一个基于 Three.js 的写实海洋场景,包含程序化生成的地形、植被、写实的水面和天空效果。 + +## 功能特性 + +### 🌊 写实海洋 +- 使用 Three.js 官方 Water shader +- 动态水面波纹和反射 +- 基于法线贴图的真实水面效果 +- 支持天空颜色反射 + +### 🏔️ 程序化地形 +- 使用 Simplex Noise 和 FBM(分形布朗运动)生成 +- 多层噪声叠加创建自然起伏 +- 基于高度的颜色渐变(深海、浅滩、沙滩、草地、岩石、雪山) +- 平滑的海岸线过渡 + +### 🌲 植被系统 +- 40,000+ 片草地(实例化渲染) +- 400+ 棵程序化生成的树木 +- 根据地形高度智能分布 +- 逼真的树木几何形状(树干 + 多层树冠) + +### ☀️ 天空与光影 +- Three.js Sky shader(大气散射) +- 真实的太阳位置计算 +- 动态雾效随太阳高度变化 +- ACES Filmic 色调映射 +- 高质量阴影 + +### 🎮 交互控制 +- 轨道相机控制(拖拽旋转、右键平移、滚轮缩放) +- 太阳高度角控制(-10° 到 90°) +- 太阳方位角控制(-180° 到 180°) +- 曝光度调节 +- 大气浑浊度控制 +- 瑞利散射强度控制 + +## 技术栈 + +- **Three.js r171** - 3D 渲染引擎 +- **Vite** - 现代化构建工具 +- **ES Modules** - 模块化代码组织 + +## 项目结构 + +``` +. +├── index.html # HTML 入口 +├── package.json # 项目配置 +├── vite.config.js # Vite 配置 +└── src/ + ├── main.js # 应用入口 + ├── OceanScene.js # 主场景管理器 + ├── TerrainGenerator.js # 地形生成器 + ├── VegetationSystem.js # 植被系统 + └── utils/ + └── SimplexNoise.js # Simplex Noise 实现 +``` + +## 安装与运行 + +### 安装依赖 + +```bash +npm install +``` + +### 启动开发服务器 + +```bash +npm run dev +``` + +访问 http://localhost:3000 + +### 构建生产版本 + +```bash +npm run build +``` + +## 使用说明 + +### 相机控制 +- **左键拖动**: 旋转视角 +- **右键拖动**: 平移相机 +- **滚轮**: 缩放 + +### 太阳控制面板 + +右上角提供完整的太阳和大气控制: + +1. **太阳高度角 (Elevation)** + - 范围: -10° 到 90° + - 控制太阳在天空中的高度 + - 日出/日落效果在 0-15° + - 正午效果在 70-90° + +2. **太阳方位角 (Azimuth)** + - 范围: -180° 到 180° + - 控制太阳的水平位置 + - 180° = 正南方向 + +3. **曝光度 (Exposure)** + - 范围: 0.1 到 2.0 + - 调节整体亮度 + - 默认 0.5 适合大多数场景 + +4. **大气浑浊度 (Turbidity)** + - 范围: 1 到 20 + - 较低值: 清澈蓝天 + - 较高值: 雾霾/阴天效果 + +5. **瑞利散射 (Rayleigh)** + - 范围: 0 到 4 + - 影响天空蓝色强度 + - 较高值: 更蓝的天空 + +## 性能优化 + +- 实例化渲染草地(减少 draw calls) +- 程序化生成(无需加载外部模型) +- 自适应像素比(限制最大 2x) +- PCF 软阴影 +- 雾效减少远处渲染负担 + +## 浏览器要求 + +- Chrome 90+ +- Firefox 88+ +- Safari 14+ +- Edge 90+ + +需要支持 WebGL 2.0 + +## 参数调优建议 + +### 日出场景 +``` +Elevation: 5-15 +Azimuth: 90-180 +Exposure: 0.4-0.6 +Turbidity: 8-12 +Rayleigh: 2-3 +``` + +### 正午场景 +``` +Elevation: 70-85 +Azimuth: 180 +Exposure: 0.5-0.7 +Turbidity: 6-10 +Rayleigh: 1.5-2.5 +``` + +### 黄昏场景 +``` +Elevation: 0-10 +Azimuth: 270 +Exposure: 0.3-0.5 +Turbidity: 12-16 +Rayleigh: 2-4 +``` + +## 自定义修改 + +### 调整地形大小 +编辑 `src/OceanScene.js` 中的 `initTerrain()`: +```javascript +const terrainGen = new TerrainGenerator({ + size: 1000, // 地形尺寸 + segments: 256, // 细分程度(影响细节) + maxHeight: 50, // 最大高度 + waterLevel: 0, // 水面高度 + seed: 42 // 随机种子 +}); +``` + +### 调整植被密度 +编辑 `src/OceanScene.js` 中的 `initVegetation()`: +```javascript +const vegSystem = new VegetationSystem(this.terrainGenerator, { + grassCount: 40000, // 草地数量 + treeCount: 400, // 树木数量 + terrainSize: 1000, + waterLevel: 0 +}); +``` + +### 调整水面效果 +编辑 `src/OceanScene.js` 中的 `initWater()`: +```javascript +this.water = new Water(waterGeometry, { + textureWidth: 512, + textureHeight: 512, + waterColor: 0x001e0f, // 水面颜色 + distortionScale: 3.7, // 波纹扭曲强度 + // ... +}); +``` + +## 许可证 + +MIT License + +## 参考资源 + +- [Three.js Ocean Example](https://threejs.org/examples/webgl_shaders_ocean.html) +- [Three.js Sky Shader](https://threejs.org/docs/#api/en/objects/Sky) +- [Simplex Noise Algorithm](https://en.wikipedia.org/wiki/Simplex_noise) diff --git a/index.html b/index.html new file mode 100644 index 0000000..656a407 --- /dev/null +++ b/index.html @@ -0,0 +1,202 @@ + + + + + + 写实无尽海洋场景 + + + +
+

写实海洋场景

+
+

正在加载场景...

+
+ +
+ +
+

🌊 写实无尽海洋

+

🖱️ 左键拖动旋转视角

+

🖱️ 右键拖动平移

+

🖱️ 滚轮缩放

+

☀️ 使用右上角控制太阳

+
+ +
+

☀️ 太阳控制

+
+ + +
15.0°
+
+
+ + +
180.0°
+
+
+ + +
0.50
+
+
+ + +
10.0
+
+
+ + +
2.00
+
+
+ +
FPS: 60
+ + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0c2f1a9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1152 @@ +{ + "name": "realistic-ocean-scene", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "realistic-ocean-scene", + "version": "1.0.0", + "dependencies": { + "three": "^0.171.0" + }, + "devDependencies": { + "vite": "^6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/three": { + "version": "0.171.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.171.0.tgz", + "integrity": "sha512-Y/lAXPaKZPcEdkKjh0JOAHVv8OOnv/NDJqm0wjfCzyQmfKxV7zvkwsnBgPBKTzJHToSOhRGQAGbPJObT59B/PQ==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..75ea386 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "realistic-ocean-scene", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "three": "^0.171.0" + }, + "devDependencies": { + "vite": "^6.0.0" + } +} diff --git a/src/OceanScene.js b/src/OceanScene.js new file mode 100644 index 0000000..54b3fcb --- /dev/null +++ b/src/OceanScene.js @@ -0,0 +1,305 @@ +import * as THREE from 'three'; +import { Water } from 'three/addons/objects/Water.js'; +import { Sky } from 'three/addons/objects/Sky.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { TerrainGenerator } from './TerrainGenerator.js'; +import { VegetationSystem } from './VegetationSystem.js'; + +export class OceanScene { + constructor(container) { + this.container = container; + this.scene = null; + this.camera = null; + this.renderer = null; + this.controls = null; + this.water = null; + this.sky = null; + this.sun = new THREE.Vector3(); + this.terrain = null; + this.vegetation = null; + this.pmremGenerator = null; + this.renderTarget = null; + this.sunLight = null; + + this.params = { + elevation: 2, + azimuth: 180, + exposure: 0.5, + turbidity: 10, + rayleigh: 2, + mieCoefficient: 0.005, + mieDirectionalG: 0.8 + }; + + this.clock = new THREE.Clock(); + this.frameCount = 0; + this.lastTime = performance.now(); + } + + async init() { + this.initRenderer(); + this.initScene(); + this.initCamera(); + this.initControls(); + this.initLighting(); + await this.initSky(); + await this.initWater(); + await this.initTerrain(); + await this.initVegetation(); + this.initSunPosition(); + this.initEventListeners(); + } + + initRenderer() { + this.renderer = new THREE.WebGLRenderer({ + antialias: true, + powerPreference: 'high-performance' + }); + this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + this.renderer.setSize(window.innerWidth, window.innerHeight); + this.renderer.toneMapping = THREE.ACESFilmicToneMapping; + this.renderer.toneMappingExposure = this.params.exposure; + this.renderer.shadowMap.enabled = true; + this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; + this.container.appendChild(this.renderer.domElement); + } + + initScene() { + this.scene = new THREE.Scene(); + this.scene.fog = new THREE.FogExp2(0x8cb8d4, 0.0008); + } + + initCamera() { + this.camera = new THREE.PerspectiveCamera( + 60, + window.innerWidth / window.innerHeight, + 1, + 20000 + ); + this.camera.position.set(100, 50, 200); + } + + initControls() { + this.controls = new OrbitControls(this.camera, this.renderer.domElement); + this.controls.enableDamping = true; + this.controls.dampingFactor = 0.05; + this.controls.maxPolarAngle = Math.PI * 0.48; + this.controls.minDistance = 30; + this.controls.maxDistance = 1000; + this.controls.target.set(0, 10, 0); + this.controls.update(); + } + + initLighting() { + const ambientLight = new THREE.AmbientLight(0x555555); + this.scene.add(ambientLight); + + this.sunLight = new THREE.DirectionalLight(0xffffff, 1.5); + this.sunLight.castShadow = true; + this.sunLight.shadow.mapSize.width = 2048; + this.sunLight.shadow.mapSize.height = 2048; + this.sunLight.shadow.camera.near = 0.5; + this.sunLight.shadow.camera.far = 500; + this.sunLight.shadow.camera.left = -100; + this.sunLight.shadow.camera.right = 100; + this.sunLight.shadow.camera.top = 100; + this.sunLight.shadow.camera.bottom = -100; + this.scene.add(this.sunLight); + } + + async initSky() { + this.sky = new Sky(); + this.sky.scale.setScalar(10000); + this.scene.add(this.sky); + + const skyUniforms = this.sky.material.uniforms; + skyUniforms['turbidity'].value = this.params.turbidity; + skyUniforms['rayleigh'].value = this.params.rayleigh; + skyUniforms['mieCoefficient'].value = this.params.mieCoefficient; + skyUniforms['mieDirectionalG'].value = this.params.mieDirectionalG; + + this.pmremGenerator = new THREE.PMREMGenerator(this.renderer); + } + + async initWater() { + const waterGeometry = new THREE.PlaneGeometry(10000, 10000, 128, 128); + + const waterNormals = await new Promise((resolve) => { + new THREE.TextureLoader().load( + 'https://threejs.org/examples/textures/waternormals.jpg', + (texture) => { + texture.wrapS = texture.wrapT = THREE.RepeatWrapping; + resolve(texture); + } + ); + }); + + this.water = new Water(waterGeometry, { + textureWidth: 512, + textureHeight: 512, + waterNormals: waterNormals, + sunDirection: new THREE.Vector3(), + sunColor: 0xffffff, + waterColor: 0x001e0f, + distortionScale: 3.7, + fog: true + }); + + this.water.rotation.x = -Math.PI / 2; + this.water.position.y = 0; + this.scene.add(this.water); + } + + async initTerrain() { + const terrainGen = new TerrainGenerator({ + size: 1200, + segments: 200, + maxHeight: 30, + waterLevel: 0, + seed: 42 + }); + + this.terrain = terrainGen.generate(); + this.scene.add(this.terrain); + + this.terrainGenerator = terrainGen; + } + + async initVegetation() { + const vegSystem = new VegetationSystem(this.terrainGenerator, { + grassCount: 30000, + treeCount: 300, + terrainSize: 1200, + waterLevel: 1 + }); + + this.vegetation = vegSystem.generate(); + vegSystem.addToScene(this.scene); + } + + initSunPosition() { + this.updateSun(); + } + + updateSun() { + const phi = THREE.MathUtils.degToRad(90 - this.params.elevation); + const theta = THREE.MathUtils.degToRad(this.params.azimuth); + + this.sun.setFromSphericalCoords(1, phi, theta); + + this.sky.material.uniforms['sunPosition'].value.copy(this.sun); + this.water.material.uniforms['sunDirection'].value.copy(this.sun).normalize(); + + if (this.sunLight) { + const sunDistance = 100; + this.sunLight.position.set( + this.sun.x * sunDistance, + this.sun.y * sunDistance, + this.sun.z * sunDistance + ); + } + + if (this.renderTarget) { + this.renderTarget.dispose(); + } + + const sceneEnv = new THREE.Scene(); + sceneEnv.add(this.sky); + this.renderTarget = this.pmremGenerator.fromScene(sceneEnv); + this.scene.environment = this.renderTarget.texture; + this.scene.add(this.sky); + + this.scene.fog.color.setHex(this.getFogColor()); + } + + getFogColor() { + const elevation = this.params.elevation; + + if (elevation < 0) { + return 0x1a2a3a; + } else if (elevation < 10) { + return 0x4a5a6a; + } else if (elevation < 20) { + return 0x8cb8d4; + } else if (elevation < 45) { + return 0x9ec5db; + } else if (elevation < 70) { + return 0xb8d4e8; + } else { + return 0xd4e8f4; + } + } + + initEventListeners() { + window.addEventListener('resize', () => this.onWindowResize()); + } + + onWindowResize() { + this.camera.aspect = window.innerWidth / window.innerHeight; + this.camera.updateProjectionMatrix(); + this.renderer.setSize(window.innerWidth, window.innerHeight); + } + + setSunElevation(value) { + this.params.elevation = value; + this.updateSun(); + } + + setSunAzimuth(value) { + this.params.azimuth = value; + this.updateSun(); + } + + setExposure(value) { + this.params.exposure = value; + this.renderer.toneMappingExposure = value; + } + + setTurbidity(value) { + this.params.turbidity = value; + this.sky.material.uniforms['turbidity'].value = value; + this.updateSun(); + } + + setRayleigh(value) { + this.params.rayleigh = value; + this.sky.material.uniforms['rayleigh'].value = value; + this.updateSun(); + } + + animate() { + requestAnimationFrame(() => this.animate()); + + const time = this.clock.getElapsedTime(); + + if (this.water) { + this.water.material.uniforms['time'].value += 1.0 / 60.0; + } + + this.controls.update(); + this.renderer.render(this.scene, this.camera); + + this.frameCount++; + const currentTime = performance.now(); + if (currentTime - this.lastTime >= 1000) { + const fps = Math.round(this.frameCount * 1000 / (currentTime - this.lastTime)); + const fpsElement = document.getElementById('fps'); + if (fpsElement) { + fpsElement.textContent = fps; + } + this.frameCount = 0; + this.lastTime = currentTime; + } + } + + hideLoading() { + const loading = document.getElementById('loading'); + if (loading) { + loading.style.opacity = '0'; + loading.style.transition = 'opacity 0.5s ease'; + setTimeout(() => { + loading.style.display = 'none'; + }, 500); + } + } +} diff --git a/src/TerrainGenerator.js b/src/TerrainGenerator.js new file mode 100644 index 0000000..e59bc2f --- /dev/null +++ b/src/TerrainGenerator.js @@ -0,0 +1,142 @@ +import * as THREE from 'three'; +import { SimplexNoise } from './utils/SimplexNoise.js'; + +export class TerrainGenerator { + constructor(options = {}) { + this.size = options.size || 1000; + this.segments = options.segments || 256; + this.maxHeight = options.maxHeight || 50; + this.waterLevel = options.waterLevel || 0; + this.beachWidth = options.beachWidth || 20; + + this.noise = new SimplexNoise(options.seed || 42); + this.terrain = null; + } + + generate() { + const geometry = new THREE.PlaneGeometry( + this.size, + this.size, + this.segments, + this.segments + ); + + const positions = geometry.attributes.position.array; + const colors = new Float32Array(positions.length); + + for (let i = 0; i < positions.length; i += 3) { + const x = positions[i]; + const y = positions[i + 1]; + + let height = this.getHeight(x, y); + positions[i + 2] = height; + + const color = this.getTerrainColor(x, y, height); + colors[i] = color.r; + colors[i + 1] = color.g; + colors[i + 2] = color.b; + } + + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); + geometry.computeVertexNormals(); + geometry.rotateX(-Math.PI / 2); + + const material = new THREE.MeshStandardMaterial({ + vertexColors: true, + roughness: 0.85, + metalness: 0.0, + flatShading: false + }); + + this.terrain = new THREE.Mesh(geometry, material); + this.terrain.receiveShadow = true; + this.terrain.castShadow = true; + + return this.terrain; + } + + getHeight(x, y) { + const scale = 0.0008; + + let height = this.noise.fbm(x * scale, y * scale, 3, 2.0, 0.5); + const detail = this.noise.fbm(x * scale * 3, y * scale * 3, 2, 2.0, 0.5) * 0.2; + height += detail; + + const distFromCenter = Math.sqrt(x * x + y * y); + const maxLandDist = this.size * 0.45; + const falloffStart = this.size * 0.25; + + let continentMask = 1.0; + if (distFromCenter > falloffStart) { + const t = (distFromCenter - falloffStart) / (maxLandDist - falloffStart); + continentMask = Math.max(0, 1 - Math.pow(t, 1.5)); + } + + height = height * continentMask; + height = height * this.maxHeight * 0.4; + + const beachZone = 2; + if (height > this.waterLevel && height < this.waterLevel + beachZone) { + const blend = Math.max(0, Math.min(1, (height - this.waterLevel) / beachZone)); + height = this.waterLevel + Math.pow(blend, 0.5) * beachZone; + } + + return height; + } + + getTerrainColor(x, y, height) { + const normalizedHeight = (height - this.waterLevel) / this.maxHeight; + + const deepWater = new THREE.Color(0x1a3d5c); + const shallowWater = new THREE.Color(0x2d6b8a); + const beach = new THREE.Color(0xc2b280); + const grass = new THREE.Color(0x3d6b3d); + const darkGrass = new THREE.Color(0x2d4a2d); + const rock = new THREE.Color(0x5a5a5a); + const snow = new THREE.Color(0xe8e8e8); + + let color = new THREE.Color(); + + if (normalizedHeight < -0.1) { + color.copy(deepWater); + } else if (normalizedHeight < 0) { + color.lerpColors(deepWater, shallowWater, (normalizedHeight + 0.1) / 0.1); + } else if (normalizedHeight < 0.05) { + const beachBlend = Math.min(1, normalizedHeight / 0.05); + color.lerpColors(shallowWater, beach, beachBlend); + } else if (normalizedHeight < 0.15) { + const sandToGrass = (normalizedHeight - 0.05) / 0.1; + color.lerpColors(beach, grass, sandToGrass); + } else if (normalizedHeight < 0.4) { + const grassBlend = (normalizedHeight - 0.15) / 0.25; + color.lerpColors(grass, darkGrass, grassBlend); + } else if (normalizedHeight < 0.6) { + const rockBlend = (normalizedHeight - 0.4) / 0.2; + color.lerpColors(darkGrass, rock, rockBlend); + } else if (normalizedHeight < 0.8) { + color.copy(rock); + } else { + const snowBlend = (normalizedHeight - 0.8) / 0.2; + color.lerpColors(rock, snow, snowBlend); + } + + const noiseVariation = this.noise.noise2D(x * 0.05, y * 0.05) * 0.1; + color.r = Math.max(0, Math.min(1, color.r + noiseVariation)); + color.g = Math.max(0, Math.min(1, color.g + noiseVariation)); + color.b = Math.max(0, Math.min(1, color.b + noiseVariation)); + + return color; + } + + getTerrain() { + return this.terrain; + } + + getHeightAt(x, z) { + return this.getHeight(x, -z); + } + + isLand(x, z) { + return this.getHeight(x, z) > this.waterLevel; + } +} diff --git a/src/VegetationSystem.js b/src/VegetationSystem.js new file mode 100644 index 0000000..5264845 --- /dev/null +++ b/src/VegetationSystem.js @@ -0,0 +1,195 @@ +import * as THREE from 'three'; +import { SimplexNoise } from './utils/SimplexNoise.js'; + +export class VegetationSystem { + constructor(terrain, options = {}) { + this.terrain = terrain; + this.options = { + grassCount: options.grassCount || 50000, + treeCount: options.treeCount || 500, + terrainSize: options.terrainSize || 1000, + waterLevel: options.waterLevel || 0 + }; + + this.noise = new SimplexNoise(12345); + this.grass = null; + this.trees = []; + } + + generate() { + this.generateGrass(); + this.generateTrees(); + + return { + grass: this.grass, + trees: this.trees + }; + } + + generateGrass() { + const geometry = new THREE.BufferGeometry(); + const positions = []; + const colors = []; + const uvs = []; + const indices = []; + + const grassBladeHeight = 0.8; + const grassBladeWidth = 0.1; + + let vertexIndex = 0; + + for (let i = 0; i < this.options.grassCount; i++) { + const x = (Math.random() - 0.5) * this.options.terrainSize * 0.7; + const z = (Math.random() - 0.5) * this.options.terrainSize * 0.7; + const y = this.terrain.getHeightAt(x, z); + + if (y < this.options.waterLevel + 0.5 || y > this.options.waterLevel + 10) continue; + + const bendX = this.noise.noise2D(x * 0.1, z * 0.1) * 0.3; + const bendZ = this.noise.noise2D(x * 0.1 + 100, z * 0.1) * 0.3; + + positions.push( + x - grassBladeWidth / 2, y, z, + x + grassBladeWidth / 2, y, z, + x + bendX + grassBladeWidth / 4, y + grassBladeHeight, z + bendZ + ); + + const greenVariation = 0.7 + Math.random() * 0.3; + const baseColor = new THREE.Color().setHSL(0.3, 0.6 * greenVariation, 0.25 * greenVariation); + + colors.push( + baseColor.r, baseColor.g, baseColor.b, + baseColor.r * 0.9, baseColor.g * 0.9, baseColor.b * 0.9, + baseColor.r * 0.8, baseColor.g * 0.8, baseColor.b * 0.8 + ); + + uvs.push(0, 0, 1, 0, 0.5, 1); + + indices.push( + vertexIndex, vertexIndex + 1, vertexIndex + 2, + vertexIndex + 2, vertexIndex + 1, vertexIndex + ); + + vertexIndex += 3; + } + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); + geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2)); + geometry.setIndex(indices); + geometry.computeVertexNormals(); + + const material = new THREE.MeshStandardMaterial({ + vertexColors: true, + side: THREE.DoubleSide, + roughness: 0.9, + metalness: 0.0 + }); + + this.grass = new THREE.Mesh(geometry, material); + this.grass.castShadow = true; + this.grass.receiveShadow = true; + } + + generateTrees() { + const treePositions = []; + + for (let i = 0; i < this.options.treeCount * 10; i++) { + if (treePositions.length >= this.options.treeCount) break; + + const x = (Math.random() - 0.5) * this.options.terrainSize * 0.6; + const z = (Math.random() - 0.5) * this.options.terrainSize * 0.6; + const y = this.terrain.getHeightAt(x, z); + + if (y < this.options.waterLevel + 1 || y > this.options.waterLevel + 10) continue; + + const densityNoise = this.noise.noise2D(x * 0.005, z * 0.005); + if (densityNoise < 0.3) continue; + + let tooClose = false; + for (const pos of treePositions) { + const dist = Math.sqrt((pos.x - x) ** 2 + (pos.z - z) ** 2); + if (dist < 10) { + tooClose = true; + break; + } + } + + if (!tooClose) { + treePositions.push({ x, y, z }); + } + } + + for (const pos of treePositions) { + const tree = this.createTree(pos); + this.trees.push(tree); + } + } + + createTree(pos) { + const tree = new THREE.Group(); + + const trunkHeight = 5 + Math.random() * 5; + const trunkRadius = 0.3 + Math.random() * 0.2; + + const trunkGeometry = new THREE.CylinderGeometry( + trunkRadius * 0.7, + trunkRadius, + trunkHeight, + 8 + ); + const trunkMaterial = new THREE.MeshStandardMaterial({ + color: 0x4a3728, + roughness: 0.9, + metalness: 0.0 + }); + const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial); + trunk.position.y = trunkHeight / 2; + trunk.castShadow = true; + trunk.receiveShadow = true; + tree.add(trunk); + + const foliageLayers = 3 + Math.floor(Math.random() * 2); + let foliageY = trunkHeight; + + for (let i = 0; i < foliageLayers; i++) { + const foliageRadius = (2.5 - i * 0.4) + Math.random() * 0.5; + const foliageHeight = 2 + Math.random() * 1; + + const foliageGeometry = new THREE.ConeGeometry( + foliageRadius, + foliageHeight, + 8 + ); + + const greenVariation = 0.8 + Math.random() * 0.2; + const foliageMaterial = new THREE.MeshStandardMaterial({ + color: new THREE.Color().setHSL(0.28 + Math.random() * 0.05, 0.5 * greenVariation, 0.2 * greenVariation), + roughness: 0.8, + metalness: 0.0 + }); + + const foliage = new THREE.Mesh(foliageGeometry, foliageMaterial); + foliage.position.y = foliageY; + foliage.castShadow = true; + foliage.receiveShadow = true; + tree.add(foliage); + + foliageY += foliageHeight * 0.6; + } + + tree.position.set(pos.x, pos.y, pos.z); + + const scale = 0.8 + Math.random() * 0.4; + tree.scale.setScalar(scale); + + tree.rotation.y = Math.random() * Math.PI * 2; + + return tree; + } + + addToScene(scene) { + if (this.grass) scene.add(this.grass); + this.trees.forEach(tree => scene.add(tree)); + } +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..3cb9c88 --- /dev/null +++ b/src/main.js @@ -0,0 +1,76 @@ +import { OceanScene } from './OceanScene.js'; + +async function main() { + const container = document.getElementById('container'); + + const oceanScene = new OceanScene(container); + + try { + await oceanScene.init(); + + setupControls(oceanScene); + + oceanScene.hideLoading(); + + oceanScene.animate(); + + console.log('写实海洋场景加载完成!'); + } catch (error) { + console.error('场景加载失败:', error); + + const loading = document.getElementById('loading'); + if (loading) { + loading.innerHTML = ` +

加载失败

+

${error.message}

+

请刷新页面重试

+ `; + } + } +} + +function setupControls(oceanScene) { + const elevationSlider = document.getElementById('sun-elevation'); + const azimuthSlider = document.getElementById('sun-azimuth'); + const exposureSlider = document.getElementById('exposure'); + const turbiditySlider = document.getElementById('turbidity'); + const rayleighSlider = document.getElementById('rayleigh'); + + const elevationValue = document.getElementById('elevation-value'); + const azimuthValue = document.getElementById('azimuth-value'); + const exposureValue = document.getElementById('exposure-value'); + const turbidityValue = document.getElementById('turbidity-value'); + const rayleighValue = document.getElementById('rayleigh-value'); + + elevationSlider.addEventListener('input', (e) => { + const value = parseFloat(e.target.value); + oceanScene.setSunElevation(value); + elevationValue.textContent = value.toFixed(1) + '°'; + }); + + azimuthSlider.addEventListener('input', (e) => { + const value = parseFloat(e.target.value); + oceanScene.setSunAzimuth(value); + azimuthValue.textContent = value.toFixed(1) + '°'; + }); + + exposureSlider.addEventListener('input', (e) => { + const value = parseFloat(e.target.value); + oceanScene.setExposure(value); + exposureValue.textContent = value.toFixed(2); + }); + + turbiditySlider.addEventListener('input', (e) => { + const value = parseFloat(e.target.value); + oceanScene.setTurbidity(value); + turbidityValue.textContent = value.toFixed(1); + }); + + rayleighSlider.addEventListener('input', (e) => { + const value = parseFloat(e.target.value); + oceanScene.setRayleigh(value); + rayleighValue.textContent = value.toFixed(2); + }); +} + +main().catch(console.error); diff --git a/src/utils/SimplexNoise.js b/src/utils/SimplexNoise.js new file mode 100644 index 0000000..86441eb --- /dev/null +++ b/src/utils/SimplexNoise.js @@ -0,0 +1,131 @@ +export class SimplexNoise { + constructor(seed = Math.random()) { + this.p = new Uint8Array(256); + this.perm = new Uint8Array(512); + this.permMod12 = new Uint8Array(512); + + const random = this.seededRandom(seed); + + for (let i = 0; i < 256; i++) { + this.p[i] = i; + } + + for (let i = 255; i > 0; i--) { + const j = Math.floor(random() * (i + 1)); + [this.p[i], this.p[j]] = [this.p[j], this.p[i]]; + } + + for (let i = 0; i < 512; i++) { + this.perm[i] = this.p[i & 255]; + this.permMod12[i] = this.perm[i] % 12; + } + + this.grad3 = new Float32Array([ + 1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1, 0, + 1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, -1, + 0, 1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1 + ]); + } + + seededRandom(seed) { + return function() { + seed = (seed * 9301 + 49297) % 233280; + return seed / 233280; + }; + } + + dot2(g, x, y) { + return g[0] * x + g[1] * y; + } + + noise2D(xin, yin) { + const F2 = 0.5 * (Math.sqrt(3) - 1); + const G2 = (3 - Math.sqrt(3)) / 6; + + const s = (xin + yin) * F2; + const i = Math.floor(xin + s); + const j = Math.floor(yin + s); + + const t = (i + j) * G2; + const X0 = i - t; + const Y0 = j - t; + const x0 = xin - X0; + const y0 = yin - Y0; + + let i1, j1; + if (x0 > y0) { + i1 = 1; + j1 = 0; + } else { + i1 = 0; + j1 = 1; + } + + const x1 = x0 - i1 + G2; + const y1 = y0 - j1 + G2; + const x2 = x0 - 1 + 2 * G2; + const y2 = y0 - 1 + 2 * G2; + + const ii = i & 255; + const jj = j & 255; + + let n0 = 0, n1 = 0, n2 = 0; + + let t0 = 0.5 - x0 * x0 - y0 * y0; + if (t0 >= 0) { + const gi0 = this.permMod12[ii + this.perm[jj]] * 3; + t0 *= t0; + n0 = t0 * t0 * this.dot2([this.grad3[gi0], this.grad3[gi0 + 1]], x0, y0); + } + + let t1 = 0.5 - x1 * x1 - y1 * y1; + if (t1 >= 0) { + const gi1 = this.permMod12[ii + i1 + this.perm[jj + j1]] * 3; + t1 *= t1; + n1 = t1 * t1 * this.dot2([this.grad3[gi1], this.grad3[gi1 + 1]], x1, y1); + } + + let t2 = 0.5 - x2 * x2 - y2 * y2; + if (t2 >= 0) { + const gi2 = this.permMod12[ii + 1 + this.perm[jj + 1]] * 3; + t2 *= t2; + n2 = t2 * t2 * this.dot2([this.grad3[gi2], this.grad3[gi2 + 1]], x2, y2); + } + + return 70 * (n0 + n1 + n2); + } + + fbm(x, y, octaves = 6, lacunarity = 2, persistence = 0.5) { + let value = 0; + let amplitude = 1; + let frequency = 1; + let maxValue = 0; + + for (let i = 0; i < octaves; i++) { + value += amplitude * this.noise2D(x * frequency, y * frequency); + maxValue += amplitude; + amplitude *= persistence; + frequency *= lacunarity; + } + + return value / maxValue; + } + + ridgedNoise(x, y, octaves = 6, lacunarity = 2, persistence = 0.5) { + let value = 0; + let amplitude = 1; + let frequency = 1; + let maxValue = 0; + + for (let i = 0; i < octaves; i++) { + let noise = 1 - Math.abs(this.noise2D(x * frequency, y * frequency)); + noise = noise * noise; + value += amplitude * noise; + maxValue += amplitude; + amplitude *= persistence; + frequency *= lacunarity; + } + + return value / maxValue; + } +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..19edbc3 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { + host: '0.0.0.0', + port: 3000 + } +});