feat(ocean): add ocean demo with Godot integration

- Add CMakeLists.txt for building TypePHP Ocean Godot bridge extension
- Create main.php game logic with chunk-based world generation and weather system
- Add Godot scene configuration with environment settings and particle effects
- Implement PHP stubs for ocean backend functions
- Add complete C++ backend implementation with OpenGL rendering and physics
- Integrate spectral wave simulation and dynamic weather transitions
- Implement chunk loading/unloading system for infinite world navigation
- Add mouse capture and input handling for 3D camera controls
- Create marker placement system for world objects and islands
pull/16/head
韩天峰 2 months ago
parent 348c3d01fc
commit e5dbf10ceb
  1. BIN
      examples/ocean-demo/1r.png
  2. 98
      examples/ocean-demo/README.md
  3. 1206
      examples/ocean-demo/cpp-src/ocean_backend.cc
  4. 287
      examples/ocean-demo/main.php
  5. 20
      examples/ocean-demo/php-src/ocean.stub.php
  6. 12
      examples/ocean-demo/project.yml
  7. 44
      examples/ocean-demo/ref1.txt
  8. 36
      examples/ocean-demo/ref2.txt
  9. BIN
      examples/ocean-godot/2.jpg
  10. 38
      examples/ocean-godot/README.md
  11. 102
      examples/ocean-godot/cpp-src/typephp_ocean_api.cc
  12. 24
      examples/ocean-godot/gdextension-src/CMakeLists.txt
  13. 33
      examples/ocean-godot/gdextension-src/register_types.cpp
  14. 141
      examples/ocean-godot/gdextension-src/typephp_ocean_bridge.cpp
  15. 53
      examples/ocean-godot/gdextension-src/typephp_ocean_bridge.hpp
  16. 94
      examples/ocean-godot/php-src/ocean.php
  17. 50
      examples/ocean-godot/project.godot
  18. 87
      examples/ocean-godot/scenes/main.tscn
  19. 869
      examples/ocean-godot/scripts/ocean_world.gd
  20. 7
      examples/ocean-godot/typephp.yml
  21. 10
      examples/ocean-godot/typephp_ocean_bridge.gdextension

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 KiB

@ -0,0 +1,98 @@
# TypePHP Ocean Demo
这是一个新的 OpenGL 示例项目,场景为海洋、天空和一条可移动小船。它复用 `minecraft-demo` 的 Win32/WGL 思路,但不使用方块世界,重点展示程序化水面和天气变化。
## 特性
- PHP/TypePHP 负责游戏逻辑:小船移动、惯性、昼夜循环、随机天气切换。
- C++ 只负责底层窗口、键盘输入、OpenGL 渲染和退出确认框。
- 程序化天空:白天、夜晚、傍晚、清晨的颜色变化。
- 程序化 skybox:远景只保留镜头旋转,不参与世界平移。
- 程序化海面:动态波浪、法线扰动、漫反射、镜面高光和菲涅尔近似。
- 参考 `D:\workspace\Ocean-Simulation\Ocean_Simulation` 的 Phillips Spectrum、PBR/Fresnel、roughness/AO 和指数雾思路,移植为固定管线可运行的轻量版本。
- 天气:晴天、阴天、雨天随机变化。
- 小船支持 `W/A/S/D` 四方向移动。
- 鼠标控制自由摄像机视角。
- WASD 采用类似 Minecraft 的镜头相对移动:`W` 向镜头前方,`A/D` 横向移动。
- 开放世界分块:PHP 按小船所在 chunk 渐进生成和卸载海上对象。
- 海上对象包括浮标、灯浮标和小礁石,用于提供移动参照和远景层次。
- 新增程序化海岛和大型帆船对象,尝试接近 `1r.png` 的前景船、中景岛、远景层次构图。
- 远处线性雾效,使海平线自然融合。
- HDR 风格 tone mapping,使强高光和暗部层次更自然。
- Bloom 近似:太阳、晨昏地平线和夜间船灯有柔和光晕。
- 夜间小船灯作为点光源影响近处水面。
- 参考 `D:\workspace\volumetric-clouds` 的体积云 shader,将 coverage、wind shear、多层噪声、Beer-Lambert 透射、powder effect 和 Henyey-Greenstein 相位函数移植为固定管线可绘制的分层云近似。
## 关于 ref1.txt 建议
`ref1.txt` 建议使用 OpenGL 3.3、GLFW、GLEW/GLAD、GLM 和 stb_image。这个示例为了保持 TypePHP 示例项目轻量、可直接用当前编译器构建,继续使用 Win32/WGL 和 OpenGL 固定管线,没有引入额外第三方依赖。
已按同类思路实现:
- 天空盒式远景层
- 动态水面网格
- 多正弦波浪
- 基础光照、高光和菲涅尔近似
- 鼠标摄像机控制
- 雾效
## 关于 Ocean-Simulation
`D:\workspace\Ocean-Simulation\Ocean_Simulation` 使用现代 OpenGL shader、tessellation、OpenCL FFT、高度贴图、HDR skybox 和 PBR/IBL。当前 TypePHP demo 没有引入这些运行时依赖,而是提取了其中适合示例项目的核心算法思想:
- 多频谱波浪叠加,使用深水色散关系 `sqrt(g * k)`
- 斜率推导法线、roughness 和 ambient occlusion。
- Fresnel-Schlick 近似。
- GGX/Reitz 风格高光分布的轻量化版本。
- 指数平方雾效,使海面远处自然融入天空。
## 关于 ref2.txt 建议
`ref2.txt` 进一步建议 PBR/IBL、FFT、HDR、Bloom、多光源和大气雾效。当前示例仍保持低依赖固定管线实现,但继续补上了可落地的近似:
- HDR 风格曝光和 tone mapping。
- 屏幕空间 Bloom 近似,不依赖 framebuffer 后处理。
- 夜间船灯点光源。
- 水面材质继续使用 Fresnel、roughness、AO 和高光分布。
- 继续保留指数平方雾效来模拟大气融合。
## 关于 volumetric-clouds
`D:\workspace\volumetric-clouds` 使用 OpenGL 4、3D 噪声纹理、compute shader 和全屏 ray marching。当前示例没有引入这些依赖,而是提取核心思路做轻量实现:
- 使用 value noise + Worley FBM 近似云密度。
- 使用 coverage 控制云层覆盖率,天气越差云层越厚。
- 使用 wind shear 和时间偏移让云层缓慢移动。
- 使用 Beer-Lambert 透射模拟云体厚度。
- 使用 powder effect 和 Henyey-Greenstein 相位函数近似云层散射。
- 使用多层半透明 quad strip 叠加,替代原先简单椭圆云。
- 海洋颜色使用深海吸收色、浅层散射、天空反射和雨天灰蓝衰减,比早期的亮青色更接近真实海面。
## 已知视觉问题修正
- 早期 skybox 使用立方体四个侧面,镜头旋转时会看到类似四面“屏障”的硬边界。现在已改为半球天空穹顶。
- 早期水面是方形网格并按单方向淡出,远处边缘可能像边界。现在改为更大范围的水面网格,并按径向距离平滑淡出。
## 编译
```powershell
php bin\compiler.php examples\ocean-demo\project.yml
```
编译产物输出到仓库根目录:
```text
ocean_demo.exe
```
## 运行
```powershell
.\ocean_demo.exe
```
## 操作
- `W/A/S/D` 移动小船
- 鼠标移动视角
- `Esc` 弹出退出确认框

File diff suppressed because it is too large Load Diff

@ -0,0 +1,287 @@
<?php
declare(strict_types=1);
const KEY_W = 0x57;
const KEY_A = 0x41;
const KEY_S = 0x53;
const KEY_D = 0x44;
const KEY_ESCAPE = 0x1B;
const WEATHER_SUNNY = 0;
const WEATHER_CLOUDY = 1;
const WEATHER_RAIN = 2;
const WORLD_CHUNK_SIZE = 96;
const RENDER_CHUNK_RADIUS = 4;
const KEEP_CHUNK_RADIUS = 5;
const CHUNKS_PER_FRAME = 1;
function weather_name(int $weather): string
{
if ($weather === WEATHER_CLOUDY) {
return 'cloudy';
}
if ($weather === WEATHER_RAIN) {
return 'rainy';
}
return 'sunny';
}
function choose_next_weather(int $current): int
{
$roll = mt_rand(0, 99);
if ($current === WEATHER_SUNNY) {
return $roll < 58 ? WEATHER_CLOUDY : ($roll < 78 ? WEATHER_RAIN : WEATHER_SUNNY);
}
if ($current === WEATHER_CLOUDY) {
return $roll < 42 ? WEATHER_SUNNY : ($roll < 76 ? WEATHER_RAIN : WEATHER_CLOUDY);
}
return $roll < 52 ? WEATHER_CLOUDY : ($roll < 76 ? WEATHER_SUNNY : WEATHER_RAIN);
}
function floor_chunk(float $value): int
{
$v = (int) floor($value);
if ($v >= 0) {
return intdiv($v, WORLD_CHUNK_SIZE);
}
return -intdiv(-$v + WORLD_CHUNK_SIZE - 1, WORLD_CHUNK_SIZE);
}
function chunk_key(int $chunkX, int $chunkZ): string
{
return (string) $chunkX . ':' . (string) $chunkZ;
}
function hash01(int $x, int $z, int $salt): float
{
$n = sin($x * 127.1 + $z * 311.7 + $salt * 74.7) * 43758.5453123;
return $n - floor($n);
}
function sort_pending_chunks(array $pendingChunks, int $centerChunkX, int $centerChunkZ): array
{
uasort($pendingChunks, static function (array $a, array $b) use ($centerChunkX, $centerChunkZ): int {
$adx = (int) $a[0] - $centerChunkX;
$adz = (int) $a[1] - $centerChunkZ;
$bdx = (int) $b[0] - $centerChunkX;
$bdz = (int) $b[1] - $centerChunkZ;
return ($adx * $adx + $adz * $adz) <=> ($bdx * $bdx + $bdz * $bdz);
});
return $pendingChunks;
}
function enqueue_visible_chunks(int $centerChunkX, int $centerChunkZ, array $loadedChunks, array $pendingChunks): array
{
$queued = 0;
for ($cx = $centerChunkX - RENDER_CHUNK_RADIUS; $cx <= $centerChunkX + RENDER_CHUNK_RADIUS; $cx++) {
for ($cz = $centerChunkZ - RENDER_CHUNK_RADIUS; $cz <= $centerChunkZ + RENDER_CHUNK_RADIUS; $cz++) {
$key = chunk_key($cx, $cz);
if (!isset($loadedChunks[$key]) && !isset($pendingChunks[$key])) {
$pendingChunks[$key] = [$cx, $cz];
$queued++;
}
}
}
return $queued > 0 ? sort_pending_chunks($pendingChunks, $centerChunkX, $centerChunkZ) : $pendingChunks;
}
function generate_world_chunk(int $chunkX, int $chunkZ): void
{
ocean_begin_chunk($chunkX, $chunkZ);
$baseX = $chunkX * WORLD_CHUNK_SIZE;
$baseZ = $chunkZ * WORLD_CHUNK_SIZE;
$chunkRoll = hash01($chunkX, $chunkZ, 101);
if (($chunkX === 0 && $chunkZ === -2) || $chunkRoll > 0.86) {
$x = $baseX + WORLD_CHUNK_SIZE * (0.35 + hash01($chunkX, $chunkZ, 102) * 0.30);
$z = $baseZ + WORLD_CHUNK_SIZE * (0.30 + hash01($chunkX, $chunkZ, 103) * 0.38);
ocean_add_marker($x, $z, 3, 8.0 + hash01($chunkX, $chunkZ, 104) * 5.0);
}
if (($chunkX === -1 && $chunkZ === -1) || $chunkRoll < 0.08) {
$x = $baseX + WORLD_CHUNK_SIZE * (0.42 + hash01($chunkX, $chunkZ, 105) * 0.24);
$z = $baseZ + WORLD_CHUNK_SIZE * (0.36 + hash01($chunkX, $chunkZ, 106) * 0.30);
ocean_add_marker($x, $z, 4, 5.5 + hash01($chunkX, $chunkZ, 107) * 2.4);
}
for ($i = 0; $i < 5; $i++) {
$chance = hash01($chunkX * 17 + $i, $chunkZ * 19 - $i, 3);
if ($chance < 0.48) {
continue;
}
$x = $baseX + hash01($chunkX, $chunkZ, 20 + $i) * WORLD_CHUNK_SIZE;
$z = $baseZ + hash01($chunkX, $chunkZ, 40 + $i) * WORLD_CHUNK_SIZE;
$typeRoll = hash01($chunkX, $chunkZ, 60 + $i);
$type = $typeRoll > 0.78 ? 2 : ($typeRoll > 0.52 ? 1 : 0);
$size = 0.8 + hash01($chunkX, $chunkZ, 80 + $i) * 1.8;
ocean_add_marker($x, $z, $type, $size);
}
ocean_commit_chunk($chunkX, $chunkZ);
}
function process_chunk_queue(array $loadedChunks, array $pendingChunks, int $maxChunks): array
{
$loaded = 0;
foreach ($pendingChunks as $key => $chunk) {
generate_world_chunk((int) $chunk[0], (int) $chunk[1]);
$loadedChunks[$key] = [(int) $chunk[0], (int) $chunk[1]];
unset($pendingChunks[$key]);
$loaded++;
if ($loaded >= $maxChunks) {
break;
}
}
return [$loadedChunks, $pendingChunks];
}
function unload_far_chunks(int $centerChunkX, int $centerChunkZ, array $loadedChunks): array
{
foreach ($loadedChunks as $key => $chunk) {
$dx = abs((int) $chunk[0] - $centerChunkX);
$dz = abs((int) $chunk[1] - $centerChunkZ);
if ($dx > KEEP_CHUNK_RADIUS || $dz > KEEP_CHUNK_RADIUS) {
ocean_remove_chunk((int) $chunk[0], (int) $chunk[1]);
unset($loadedChunks[$key]);
}
}
return $loadedChunks;
}
function main(): void
{
if (!ocean_init('TypePHP Ocean Demo - OpenGL', 1280, 720)) {
echo "OpenGL init failed\n";
return;
}
mt_srand((int) (ocean_get_time() * 1000000.0));
$x = 0.0;
$z = 0.0;
$yaw = 0.0;
$velocityX = 0.0;
$velocityZ = 0.0;
$cameraYaw = 0.0;
$cameraPitch = 0.24;
$cameraDistance = 24.0;
$last = ocean_get_time();
$escapeWasDown = false;
$dayTime = 0.28;
$weather = WEATHER_SUNNY;
$targetWeather = WEATHER_CLOUDY;
$weatherMix = 0.0;
$nextWeatherAt = $last + 18.0;
$centerChunkX = floor_chunk($x);
$centerChunkZ = floor_chunk($z);
$loadedChunks = [];
$pendingChunks = enqueue_visible_chunks($centerChunkX, $centerChunkZ, [], []);
while (count($pendingChunks) > 0) {
$queueState = process_chunk_queue($loadedChunks, $pendingChunks, 4);
$loadedChunks = $queueState[0];
$pendingChunks = $queueState[1];
ocean_poll_events();
}
while (!ocean_should_close()) {
$now = ocean_get_time();
$dt = max(0.001, min(0.05, $now - $last));
$last = $now;
ocean_poll_events();
$escapeDown = ocean_key_pressed(KEY_ESCAPE);
if ($escapeDown && !$escapeWasDown) {
if (ocean_confirm_exit()) {
break;
}
$last = ocean_get_time();
}
$escapeWasDown = $escapeDown;
$cameraYaw -= ocean_mouse_delta_x() * 0.0025;
$cameraPitch += ocean_mouse_delta_y() * 0.0020;
$cameraPitch = max(-0.20, min(0.85, $cameraPitch));
$inputX = 0.0;
$inputZ = 0.0;
if (ocean_key_pressed(KEY_W)) {
$inputZ -= 1.0;
}
if (ocean_key_pressed(KEY_S)) {
$inputZ += 1.0;
}
if (ocean_key_pressed(KEY_A)) {
$inputX -= 1.0;
}
if (ocean_key_pressed(KEY_D)) {
$inputX += 1.0;
}
$len = sqrt($inputX * $inputX + $inputZ * $inputZ);
if ($len > 0.0) {
$inputX /= $len;
$inputZ /= $len;
}
$accel = 18.0;
$drag = pow(0.08, $dt);
$forwardX = -sin($cameraYaw);
$forwardZ = -cos($cameraYaw);
$rightX = cos($cameraYaw);
$rightZ = -sin($cameraYaw);
$worldInputX = $rightX * $inputX + $forwardX * -$inputZ;
$worldInputZ = $rightZ * $inputX + $forwardZ * -$inputZ;
if ($len > 0.0) {
$yaw = atan2($worldInputX, -$worldInputZ);
}
$velocityX = ($velocityX + $worldInputX * $accel * $dt) * $drag;
$velocityZ = ($velocityZ + $worldInputZ * $accel * $dt) * $drag;
$x += $velocityX * $dt;
$z += $velocityZ * $dt;
$speed = sqrt($velocityX * $velocityX + $velocityZ * $velocityZ);
$currentChunkX = floor_chunk($x);
$currentChunkZ = floor_chunk($z);
if ($currentChunkX !== $centerChunkX || $currentChunkZ !== $centerChunkZ) {
$centerChunkX = $currentChunkX;
$centerChunkZ = $currentChunkZ;
$pendingChunks = enqueue_visible_chunks($centerChunkX, $centerChunkZ, $loadedChunks, $pendingChunks);
$loadedChunks = unload_far_chunks($centerChunkX, $centerChunkZ, $loadedChunks);
}
$queueState = process_chunk_queue($loadedChunks, $pendingChunks, CHUNKS_PER_FRAME);
$loadedChunks = $queueState[0];
$pendingChunks = $queueState[1];
$dayTime += $dt / 96.0;
if ($dayTime >= 1.0) {
$dayTime -= 1.0;
}
if ($now >= $nextWeatherAt && $targetWeather === $weather) {
$targetWeather = choose_next_weather($weather);
$weatherMix = 0.0;
$nextWeatherAt = $now + 16.0 + mt_rand(0, 16);
echo 'Weather changed: ' . weather_name($targetWeather) . PHP_EOL;
}
if ($targetWeather !== $weather) {
$weatherMix = min(1.0, $weatherMix + $dt / 7.0);
if ($weatherMix >= 1.0) {
$weather = $targetWeather;
$weatherMix = 0.0;
}
}
$rainAmount = $weather === WEATHER_RAIN ? 1.0 : 0.0;
if ($targetWeather === WEATHER_RAIN) {
$rainAmount = max($rainAmount, $weatherMix);
} elseif ($weather === WEATHER_RAIN) {
$rainAmount = 1.0 - $weatherMix;
}
ocean_set_boat($x, $z, $yaw, $speed);
ocean_set_camera($cameraYaw, $cameraPitch, $cameraDistance);
ocean_set_environment($dayTime, $targetWeather, $weatherMix, max(0.0, min(1.0, $rainAmount)));
ocean_render_frame();
ocean_sleep(1);
}
ocean_shutdown();
}

@ -0,0 +1,20 @@
<?php
function ocean_init(string $title, int $width, int $height): bool {}
function ocean_shutdown(): void {}
function ocean_should_close(): bool {}
function ocean_poll_events(): void {}
function ocean_key_pressed(int $key): bool {}
function ocean_mouse_delta_x(): float {}
function ocean_mouse_delta_y(): float {}
function ocean_get_time(): float {}
function ocean_sleep(int $milliseconds): void {}
function ocean_confirm_exit(): bool {}
function ocean_set_boat(float $x, float $z, float $yaw, float $speed): void {}
function ocean_set_camera(float $yaw, float $pitch, float $distance): void {}
function ocean_set_environment(float $dayTime, int $weather, float $weatherMix, float $rainAmount): void {}
function ocean_begin_chunk(int $chunkX, int $chunkZ): void {}
function ocean_add_marker(float $x, float $z, int $type, float $size): void {}
function ocean_commit_chunk(int $chunkX, int $chunkZ): void {}
function ocean_remove_chunk(int $chunkX, int $chunkZ): void {}
function ocean_render_frame(): void {}

@ -0,0 +1,12 @@
name: ocean_demo
version: 0.1.0
mode: bin
cxx-std: c++17
sources:
- main.php
- php-src
- cpp-src
cxx-flags:
- "/D_CRT_SECURE_NO_WARNINGS"
ld-flags:
- "opengl32.lib"

@ -0,0 +1,44 @@
要实现一个包含海洋、蓝天、白云的3D场景,最直接、经典的技术路线是:**使用 OpenGL + C++,配合 `天空盒` 来模拟蓝天白云,并单独构建一个动态的`水面网格`来表现海洋**。
这个方案的技术栈和实现思路都比较清晰,我为你拆解一下:
### 🧱 技术栈
这是一个组合方案,主要通过几个核心库来构建:
* **核心图形库**: **OpenGL** (建议使用3.3以上核心版本)
* **窗口与输入管理**: **GLFW**
* **OpenGL扩展加载**: **GLEW** 或 **GLAD**
* **数学计算库**: **GLM** (用于矩阵、向量运算)
* **图像加载库**: **stb_image.h** (用于加载纹理图片)
### 🌅 天空与远景:天空盒 (Skybox)
对于远处的蓝天和白云,最简单有效的办法是使用**天空盒**技术。
1. **原理**:创建一个巨大的立方体,将相机放在它的正中心。在立方体的6个面上,分别贴上精心准备的、能无缝拼接的天空纹理(上、下、左、右、前、后)。
2. **效果**:因为立方体始终跟随相机,会给人一种无限远背景的错觉,蓝天白云的效果就出来了。天空盒性能开销很低,实现也简单。
3. **实现关键**:渲染天空盒时,需要**关闭深度测试**,并**移除平移变换**(只保留视图矩阵的旋转部分),以确保它永远在场景最远处。这也是许多入门和进阶项目会采用的标准做法。
### 🌊 海洋:程序化生成的水面网格
海洋水面可以用一个**大面积、程序化生成**的网格平面来实现。
1. **基础网格**:在你的3D空间中,生成一个覆盖视野范围的大网格(例如,包含 `N x N` 个顶点),将其高度初始设为0,作为水平面。
2. **动态效果**:在顶点着色器或片段着色器中,通过**修改顶点的高度(Y轴位置)**,并**扰动法线**来模拟波浪。
* **简单方法**:叠加多个正弦波(Sin)或使用噪声纹理采样,来让水面起伏。
* **进阶方法**:使用基于**快速傅里叶变换(FFT)** 的频谱模拟,生成更逼真的海浪效果,例如参考知名的“Tessendorf海洋模拟”方法。
### 🛠 提升真实感的其他要素
为了让场景更生动,你还可以加入一些细节:
* **光照**:实现基础的**Phong光照模型**(环境光、漫反射、镜面高光),让水面和云朵有立体感。
* **摄像机控制**:集成一个**自由移动的摄像机**(例如,用WASD移动,鼠标控制视角),实现场景漫游。
* **场景效果**:添加**雾效**可以使地平线更自然地融合,**菲涅尔效应**则能让水面在视角倾斜时反射更强烈,看起来更真实。
### 🏁 从哪里开始?
一个很好的起点是寻找一个 **“OpenGL + C++ 3D场景”** 的入门教程或开源项目模板。这些项目通常会帮你搭建好基础框架(创建窗口、初始化OpenGL、建立渲染循环),你可以在此基础上,一步步添加天空盒、水面网格、光照和摄像机控制。
GitHub上有很多相关的学习项目,例如 `OneFile_3DWorld` 展示了如何在一个文件里快速搭建OpenGL基础环境,`OpenGL-3D-Scene` 则是一个包含了光照和物体的简单场景示例。

@ -0,0 +1,36 @@
要让画面从“看着像游戏”进一步迈向“贴近真实物理世界”,关键在于引入一套**基于物理**的渲染和光照体系,并结合先进的模拟技术。这能让光线、材质和动态效果都遵循物理规律,呈现出更自然、更令人信服的视觉真实感。
技术路线可以从这几个方面展开:
### 📈 核心升级:从“模拟”到“物理真实”
| 方向 | 技术方案 | 效果与提升 |
| :--- | :--- | :--- |
| **水面与自然模拟** | **基于物理的渲染(PBR)** 与**基于图像的光照(IBL)** | 水面材质会更真实地反射和折射光线,产生细腻的高光和通透感。 |
| | **快速傅里叶变换(FFT)海洋模拟** | 模拟出高度逼真的海浪运动,包括波峰、波谷和复杂的波浪叠加,不再是简单的正弦波。 |
| | **HDR天空盒** | 让天空拥有从明亮云层到阴暗区域的高动态范围亮度信息,为场景提供更真实的全局环境光照。 |
| | **大气雾效** | 增加场景的景深感,让远处的水面和天空自然地融合在一起,更符合真实视觉体验。 |
| **整体光照质感** | **多光源系统**与**HDR渲染** | 结合方向光(太阳)、多个点光源等多种光源,模拟复杂光照环境,并结合HDR技术让亮部和暗部都保留丰富的细节,避免过曝或死黑。 |
| | **泛光效果 (Bloom)** | 模拟强光(如太阳或水面高光)周围的光晕和散射效果,这是实现“电影级”画质的关键一笔。 |
| | **菲涅尔效应** | 让水面在低角度观察时反射更强(像镜子),垂直观察时更透,这是实现水体真实感的重要细节。 |
### 🚀 进阶探索:前沿技术
除了上述经典技术,还有一个非常前沿的方向值得关注:**3D高斯泼溅 (3D Gaussian Splatting)**。
这是一种较新的渲染技术,它用一系列“点”来建模和渲染3D场景,而不是传统的三角形网格。根据一项研究,它能够在保持高帧率的同时,渲染出比传统三角网格方法**更高质量的、接近照片级别**的逼真画面。这是一个你可以关注、未来尝试的方向。
### 🚧 性能与实现的平衡
需要注意的是,以上大多数技术(尤其是FFT海洋模拟、PBR等)对GPU的计算能力和图形API的调用都有更高的要求。一个实际的工程常常需要在**渲染真实感**与**保持流畅运行**之间找到平衡点。
### 💡 如何开始?
这是一个很有挑战性但也非常有意思的目标。建议可以分步实现:
1. **巩固基础光照**:先从LearnOpenGL等教程入手,确保你的场景实现了**多光源**(平行光+点光源)的**Blinn-Phong**或更简单的光照模型。
2. **升级水面模拟**:研究和集成基于**FFT**的波浪模拟算法,这是实现真实海洋效果的核心。
3. **提升整体质感**:为场景添加**HDR渲染**和**泛光(Bloom)** 后处理特效,这会让画面质感有质的飞跃。
4. **探索PBR和IBL**:深入研究**基于物理的渲染(PBR)** 和**基于图像的光照(IBL)**,这是实现“次时代”画质的必经之路。
GitHub上的一些开源项目,如 `Ocean-Simulation`,就集成了FFT、PBR和IBL等技术,是一个很好的学习和参考对象。祝你打造出那个逼真的海洋世界!

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

@ -0,0 +1,38 @@
# TypePHP Ocean Odyssey
Godot + TypePHP open ocean demo with procedural ocean, sky, clouds, rain, islands, navigation markers, and a controllable sailboat.
Godot owns rendering, input, camera, weather presentation, and procedural meshes. TypePHP owns deterministic world data and weather transitions once the bridge DLL is built. The Godot scene still runs with fallback data if the native bridge is not present.
## Run in Godot
```powershell
D:\workspace\godot\Godot_v4.7-stable_win64_console.exe --path examples\ocean-godot
```
Controls:
- `W/A/S/D` sail relative to the camera
- `Shift` boost
- Mouse to orbit the camera
- `Esc` release or capture the mouse
## Build TypePHP library
```powershell
php bin\compiler.php examples\ocean-godot\typephp.yml -f
Copy-Item -LiteralPath typephp_ocean.dll -Destination examples\ocean-godot\bin\typephp_ocean.dll -Force
```
## Build Godot GDExtension
```powershell
cmake -S examples\ocean-godot\gdextension-src -B examples\ocean-godot\gdextension-src\build-nmake -G "NMake Makefiles" -DCMAKE_TOOLCHAIN_FILE=D:\workspace\vcpkg\scripts\buildsystems\vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release
cmake --build examples\ocean-godot\gdextension-src\build-nmake --config Release
```
The bridge outputs:
```text
examples\ocean-godot\bin\typephp_ocean_godot_bridge.dll
```

@ -0,0 +1,102 @@
#include <php_typephp_ocean_func_decl.h>
#ifdef _WIN32
#define TYPEPHP_OCEAN_API extern "C" __declspec(dllexport)
#else
#define TYPEPHP_OCEAN_API extern "C" __attribute__((visibility("default")))
#endif
extern "C" int php_aot_runtime_init(int argc, char **argv);
extern "C" void php_aot_runtime_shutdown();
static bool g_typephp_ocean_initialized = false;
static int typephp_ocean_ensure_runtime()
{
if (g_typephp_ocean_initialized) {
return 1;
}
char app_name[] = "typephp_ocean";
char *argv[] = {app_name, nullptr};
if (php_aot_runtime_init(1, argv) != 0) {
return 0;
}
g_typephp_ocean_initialized = true;
return 1;
}
TYPEPHP_OCEAN_API int typephp_ocean_init()
{
return typephp_ocean_ensure_runtime();
}
TYPEPHP_OCEAN_API void typephp_ocean_shutdown()
{
if (!g_typephp_ocean_initialized) {
return;
}
php_aot_runtime_shutdown();
g_typephp_ocean_initialized = false;
}
TYPEPHP_OCEAN_API int typephp_ocean_island_count()
{
return typephp_ocean_ensure_runtime() ? static_cast<int>(php_ocean_island_count()) : 0;
}
TYPEPHP_OCEAN_API double typephp_ocean_island_x(int index)
{
return typephp_ocean_ensure_runtime() ? static_cast<double>(php_ocean_island_x(index)) : 0.0;
}
TYPEPHP_OCEAN_API double typephp_ocean_island_z(int index)
{
return typephp_ocean_ensure_runtime() ? static_cast<double>(php_ocean_island_z(index)) : 0.0;
}
TYPEPHP_OCEAN_API double typephp_ocean_island_radius(int index)
{
return typephp_ocean_ensure_runtime() ? static_cast<double>(php_ocean_island_radius(index)) : 0.0;
}
TYPEPHP_OCEAN_API double typephp_ocean_island_height(int index)
{
return typephp_ocean_ensure_runtime() ? static_cast<double>(php_ocean_island_height(index)) : 0.0;
}
TYPEPHP_OCEAN_API double typephp_ocean_island_seed(int index)
{
return typephp_ocean_ensure_runtime() ? static_cast<double>(php_ocean_island_seed(index)) : 0.0;
}
TYPEPHP_OCEAN_API int typephp_ocean_marker_count()
{
return typephp_ocean_ensure_runtime() ? static_cast<int>(php_ocean_marker_count()) : 0;
}
TYPEPHP_OCEAN_API double typephp_ocean_marker_x(int index)
{
return typephp_ocean_ensure_runtime() ? static_cast<double>(php_ocean_marker_x(index)) : 0.0;
}
TYPEPHP_OCEAN_API double typephp_ocean_marker_z(int index)
{
return typephp_ocean_ensure_runtime() ? static_cast<double>(php_ocean_marker_z(index)) : 0.0;
}
TYPEPHP_OCEAN_API int typephp_ocean_marker_type(int index)
{
return typephp_ocean_ensure_runtime() ? static_cast<int>(php_ocean_marker_type(index)) : 0;
}
TYPEPHP_OCEAN_API double typephp_ocean_marker_size(int index)
{
return typephp_ocean_ensure_runtime() ? static_cast<double>(php_ocean_marker_size(index)) : 0.0;
}
TYPEPHP_OCEAN_API int typephp_ocean_choose_next_weather(int current, double roll)
{
return typephp_ocean_ensure_runtime() ? static_cast<int>(php_ocean_choose_next_weather(current, roll)) : current;
}

@ -0,0 +1,24 @@
cmake_minimum_required(VERSION 3.21)
project(typephp_ocean_godot_bridge LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(unofficial-godot-cpp CONFIG REQUIRED)
add_library(typephp_ocean_godot_bridge SHARED
typephp_ocean_bridge.cpp
register_types.cpp
)
target_link_libraries(typephp_ocean_godot_bridge PRIVATE unofficial::godot::cpp)
if (MSVC)
target_compile_options(typephp_ocean_godot_bridge PRIVATE /EHsc /bigobj)
endif()
set_target_properties(typephp_ocean_godot_bridge PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../bin"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../bin"
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../bin"
)

@ -0,0 +1,33 @@
#include "typephp_ocean_bridge.hpp"
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/godot.hpp>
using namespace godot;
void initialize_typephp_ocean_bridge(ModuleInitializationLevel level)
{
if (level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
GDREGISTER_CLASS(TypePhpOceanBridge);
}
void uninitialize_typephp_ocean_bridge(ModuleInitializationLevel level)
{
if (level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
}
extern "C" GDExtensionBool GDE_EXPORT typephp_ocean_bridge_init(
GDExtensionInterfaceGetProcAddress get_proc_address,
GDExtensionClassLibraryPtr library,
GDExtensionInitialization *initialization)
{
GDExtensionBinding::InitObject init_obj(get_proc_address, library, initialization);
init_obj.register_initializer(initialize_typephp_ocean_bridge);
init_obj.register_terminator(uninitialize_typephp_ocean_bridge);
init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE);
return init_obj.init();
}

@ -0,0 +1,141 @@
#include "typephp_ocean_bridge.hpp"
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/dictionary.hpp>
#include <cmath>
#ifdef _WIN32
extern "C" IMAGE_DOS_HEADER __ImageBase;
#endif
namespace godot {
void TypePhpOceanBridge::_bind_methods()
{
ClassDB::bind_method(D_METHOD("get_islands", "radius"), &TypePhpOceanBridge::get_islands);
ClassDB::bind_method(D_METHOD("get_markers", "radius"), &TypePhpOceanBridge::get_markers);
ClassDB::bind_method(D_METHOD("choose_next_weather", "current", "roll"), &TypePhpOceanBridge::choose_next_weather);
}
TypePhpOceanBridge::TypePhpOceanBridge() = default;
TypePhpOceanBridge::~TypePhpOceanBridge()
{
#ifdef _WIN32
if (library != nullptr) {
FreeLibrary(library);
library = nullptr;
}
#endif
}
bool TypePhpOceanBridge::ensure_loaded()
{
#ifndef _WIN32
return false;
#else
if (library != nullptr) {
return ocean_init != nullptr;
}
wchar_t module_path[MAX_PATH];
const DWORD len = GetModuleFileNameW(reinterpret_cast<HMODULE>(&__ImageBase), module_path, MAX_PATH);
if (len == 0 || len >= MAX_PATH) {
return false;
}
std::wstring path(module_path, len);
const size_t slash = path.find_last_of(L"\\/");
if (slash != std::wstring::npos) {
path.resize(slash + 1);
} else {
path.clear();
}
path += L"typephp_ocean.dll";
library = LoadLibraryW(path.c_str());
if (library == nullptr) {
return false;
}
ocean_init = reinterpret_cast<FnInit>(GetProcAddress(library, "typephp_ocean_init"));
island_count = reinterpret_cast<FnCount>(GetProcAddress(library, "typephp_ocean_island_count"));
island_x = reinterpret_cast<FnDoubleAt>(GetProcAddress(library, "typephp_ocean_island_x"));
island_z = reinterpret_cast<FnDoubleAt>(GetProcAddress(library, "typephp_ocean_island_z"));
island_radius = reinterpret_cast<FnDoubleAt>(GetProcAddress(library, "typephp_ocean_island_radius"));
island_height = reinterpret_cast<FnDoubleAt>(GetProcAddress(library, "typephp_ocean_island_height"));
island_seed = reinterpret_cast<FnDoubleAt>(GetProcAddress(library, "typephp_ocean_island_seed"));
marker_count = reinterpret_cast<FnCount>(GetProcAddress(library, "typephp_ocean_marker_count"));
marker_x = reinterpret_cast<FnDoubleAt>(GetProcAddress(library, "typephp_ocean_marker_x"));
marker_z = reinterpret_cast<FnDoubleAt>(GetProcAddress(library, "typephp_ocean_marker_z"));
marker_type = reinterpret_cast<FnIntAt>(GetProcAddress(library, "typephp_ocean_marker_type"));
marker_size = reinterpret_cast<FnDoubleAt>(GetProcAddress(library, "typephp_ocean_marker_size"));
choose_weather = reinterpret_cast<FnWeather>(GetProcAddress(library, "typephp_ocean_choose_next_weather"));
return ocean_init != nullptr && island_count != nullptr && island_x != nullptr && island_z != nullptr &&
island_radius != nullptr && island_height != nullptr && island_seed != nullptr &&
marker_count != nullptr && marker_x != nullptr && marker_z != nullptr &&
marker_type != nullptr && marker_size != nullptr && choose_weather != nullptr &&
ocean_init() != 0;
#endif
}
Array TypePhpOceanBridge::get_islands(double radius)
{
Array result;
if (!ensure_loaded()) {
return result;
}
const int count = island_count();
for (int i = 0; i < count; i++) {
const double x = island_x(i);
const double z = island_z(i);
if (std::sqrt(x * x + z * z) > radius) {
continue;
}
Dictionary island;
island["x"] = x;
island["z"] = z;
island["radius"] = island_radius(i);
island["height"] = island_height(i);
island["seed"] = island_seed(i);
result.append(island);
}
return result;
}
Array TypePhpOceanBridge::get_markers(double radius)
{
Array result;
if (!ensure_loaded()) {
return result;
}
const int count = marker_count();
for (int i = 0; i < count; i++) {
const double x = marker_x(i);
const double z = marker_z(i);
if (std::sqrt(x * x + z * z) > radius) {
continue;
}
Dictionary marker;
marker["x"] = x;
marker["z"] = z;
marker["type"] = marker_type(i);
marker["size"] = marker_size(i);
result.append(marker);
}
return result;
}
int TypePhpOceanBridge::choose_next_weather(int current, double roll)
{
if (!ensure_loaded()) {
return current;
}
return choose_weather(current, roll);
}
} // namespace godot

@ -0,0 +1,53 @@
#pragma once
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/variant/array.hpp>
#ifdef _WIN32
#include <windows.h>
#endif
namespace godot {
class TypePhpOceanBridge : public Node {
GDCLASS(TypePhpOceanBridge, Node)
using FnInit = int(__cdecl *)();
using FnCount = int(__cdecl *)();
using FnDoubleAt = double(__cdecl *)(int);
using FnIntAt = int(__cdecl *)(int);
using FnWeather = int(__cdecl *)(int, double);
#ifdef _WIN32
HMODULE library = nullptr;
#endif
FnInit ocean_init = nullptr;
FnCount island_count = nullptr;
FnDoubleAt island_x = nullptr;
FnDoubleAt island_z = nullptr;
FnDoubleAt island_radius = nullptr;
FnDoubleAt island_height = nullptr;
FnDoubleAt island_seed = nullptr;
FnCount marker_count = nullptr;
FnDoubleAt marker_x = nullptr;
FnDoubleAt marker_z = nullptr;
FnIntAt marker_type = nullptr;
FnDoubleAt marker_size = nullptr;
FnWeather choose_weather = nullptr;
protected:
static void _bind_methods();
public:
TypePhpOceanBridge();
~TypePhpOceanBridge();
Array get_islands(double radius);
Array get_markers(double radius);
int choose_next_weather(int current, double roll);
private:
bool ensure_loaded();
};
} // namespace godot

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
const OCEAN_WEATHER_SUNNY = 0;
const OCEAN_WEATHER_CLOUDY = 1;
const OCEAN_WEATHER_RAIN = 2;
function ocean_hash01(int $x, int $z, int $salt): float
{
$n = sin($x * 127.1 + $z * 311.7 + $salt * 74.7) * 43758.5453123;
return $n - floor($n);
}
function ocean_island_count(): int
{
return 18;
}
function ocean_island_x(int $index): float
{
$angle = $index * 2.39996323 + ocean_hash01($index, 11, 31) * 0.62;
$ring = 190.0 + ($index % 5) * 145.0 + ocean_hash01($index, 13, 37) * 120.0;
return cos($angle) * $ring;
}
function ocean_island_z(int $index): float
{
$angle = $index * 2.39996323 + ocean_hash01($index, 11, 31) * 0.62;
$ring = 190.0 + ($index % 5) * 145.0 + ocean_hash01($index, 13, 37) * 120.0;
return sin($angle) * $ring;
}
function ocean_island_radius(int $index): float
{
return 36.0 + ocean_hash01($index, 3, 11) * 58.0;
}
function ocean_island_height(int $index): float
{
return 10.0 + ocean_hash01($index, 7, 13) * 26.0;
}
function ocean_island_seed(int $index): float
{
return 17.0 + $index * 31.0;
}
function ocean_marker_count(): int
{
return 12;
}
function ocean_marker_x(int $index): float
{
$ring = 160.0 + ($index % 4) * 150.0;
$angle = $index * 2.39996323 + ocean_hash01($index, 1, 5) * 0.35;
return cos($angle) * $ring;
}
function ocean_marker_z(int $index): float
{
$ring = 160.0 + ($index % 4) * 150.0;
$angle = $index * 2.39996323 + ocean_hash01($index, 1, 5) * 0.35;
return sin($angle) * $ring;
}
function ocean_marker_type(int $index): int
{
$roll = ocean_hash01($index, 9, 17);
if ($roll > 0.78) {
return 3;
}
if ($roll > 0.48) {
return 2;
}
return 1;
}
function ocean_marker_size(int $index): float
{
return 1.6 + ocean_hash01($index, 4, 23) * 5.8;
}
function ocean_choose_next_weather(int $current, float $roll): int
{
if ($current === OCEAN_WEATHER_SUNNY) {
return $roll < 0.58 ? OCEAN_WEATHER_CLOUDY : OCEAN_WEATHER_RAIN;
}
if ($current === OCEAN_WEATHER_CLOUDY) {
return $roll < 0.42 ? OCEAN_WEATHER_SUNNY : OCEAN_WEATHER_RAIN;
}
return $roll < 0.58 ? OCEAN_WEATHER_CLOUDY : OCEAN_WEATHER_SUNNY;
}

@ -0,0 +1,50 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly.
config_version=5
[application]
config/name="TypePHP Ocean Odyssey"
run/main_scene="res://scenes/main.tscn"
config/features=PackedStringArray("4.7")
[display]
window/size/viewport_width=1280
window/size/viewport_height=720
[input]
move_forward={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":87,"physical_keycode":87,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)]
}
move_back={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":83,"physical_keycode":83,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)]
}
move_left={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":65,"physical_keycode":65,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)]
}
move_right={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":68,"physical_keycode":68,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)]
}
toggle_mouse={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194305,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)]
}
boost={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194325,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)]
}
[rendering]
renderer/rendering_method="gl_compatibility"
lights_and_shadows/directional_shadow/size=4096
anti_aliasing/quality/msaa_3d=2
anti_aliasing/quality/screen_space_aa=0
environment/volumetric_fog/use_filter=true

@ -0,0 +1,87 @@
[gd_scene load_steps=5 format=3 uid="uid://typephp_ocean_odyssey"]
[ext_resource type="Script" path="res://scripts/ocean_world.gd" id="1_world"]
[sub_resource type="Environment" id="Environment_1"]
background_mode = 1
background_color = Color(0.52, 0.72, 0.92, 1)
ambient_light_source = 2
ambient_light_color = Color(0.55, 0.64, 0.72, 1)
ambient_light_energy = 0.95
reflected_light_source = 2
tonemap_mode = 2
tonemap_exposure = 1.08
tonemap_white = 3.2
ssr_enabled = false
ssr_max_steps = 32
ssr_fade_in = 0.08
ssr_fade_out = 2.4
ssao_enabled = true
ssao_radius = 1.25
ssao_intensity = 0.82
glow_enabled = true
glow_intensity = 0.42
glow_bloom = 0.18
fog_enabled = true
fog_light_color = Color(0.50, 0.70, 0.92, 1)
fog_density = 0.003
volumetric_fog_enabled = false
volumetric_fog_density = 0.008
volumetric_fog_albedo = Color(0.72, 0.82, 0.92, 1)
[sub_resource type="Curve" id="RainAmountCurve"]
_data = [Vector2(0, 0.85), 0.0, 0.0, 0, 0, Vector2(1, 0.25), 0.0, 0.0, 0, 0]
point_count = 2
[sub_resource type="ParticleProcessMaterial" id="RainProcess"]
emission_shape = 2
emission_box_extents = Vector3(130, 1, 130)
direction = Vector3(0.18, -1, 0.08)
spread = 6.0
gravity = Vector3(4, -42, 1.5)
initial_velocity_min = 24.0
initial_velocity_max = 34.0
scale_min = 0.55
scale_max = 1.4
scale_curve = SubResource("RainAmountCurve")
color = Color(0.7, 0.86, 1, 0.42)
[sub_resource type="BoxMesh" id="RainDropMesh"]
size = Vector3(0.012, 0.62, 0.012)
[node name="Main" type="Node3D"]
script = ExtResource("1_world")
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_1")
[node name="Sun" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.731354, -0.328178, 0.598388, 0, 0.876156, 0.482028, -0.681998, -0.352584, 0.640786, 0, 28, 0)
light_color = Color(1, 0.92, 0.78, 1)
light_energy = 3.2
shadow_enabled = true
directional_shadow_mode = 2
directional_shadow_max_distance = 240.0
shadow_blur = 1.1
[node name="Moon" type="DirectionalLight3D" parent="."]
transform = Transform3D(-0.866025, -0.25, -0.433013, 0, 0.866025, -0.5, 0.5, -0.433013, -0.75, 0, 28, 0)
light_color = Color(0.46, 0.58, 0.92, 1)
light_energy = 0.0
[node name="CameraRig" type="Node3D" parent="."]
[node name="Camera3D" type="Camera3D" parent="CameraRig"]
transform = Transform3D(1, 0, 0, 0, 0.956305, 0.292372, 0, -0.292372, 0.956305, 0, 8.5, 24)
current = true
fov = 68.0
near = 0.05
far = 1000.0
[node name="Rain" type="GPUParticles3D" parent="."]
amount = 3000
lifetime = 1.85
preprocess = 1.0
visibility_aabb = AABB(-110, -70, -110, 220, 140, 220)
process_material = SubResource("RainProcess")
draw_pass_1 = SubResource("RainDropMesh")

@ -0,0 +1,869 @@
extends Node3D
const OCEAN_SIZE := 960.0
const ISLAND_GRID := 44
const CLOUD_LAYER_COUNT := 3
const WEATHER_SUNNY := 0
const WEATHER_CLOUDY := 1
const WEATHER_RAIN := 2
const CAMERA_MIN_PITCH := deg_to_rad(-10.0)
const CAMERA_MAX_PITCH := deg_to_rad(28.0)
var boat_position := Vector3.ZERO
var boat_yaw := 0.0
var boat_velocity := Vector3.ZERO
var camera_yaw := 0.0
var camera_pitch := deg_to_rad(4.0)
var day_time := 0.27
var weather := WEATHER_SUNNY
var target_weather := WEATHER_CLOUDY
var weather_mix := 0.0
var next_weather_change := 14.0
var rain_amount := 0.0
var mouse_sensitivity := 0.0022
var bridge: Object
var ocean_material: ShaderMaterial
var island_material: ShaderMaterial
var sky_material: ShaderMaterial
var cloud_materials: Array[ShaderMaterial] = []
var island_root: Node3D
var marker_root: Node3D
var boat: Node3D
var sail_material: StandardMaterial3D
var ocean_tiles: Array[MeshInstance3D] = []
var sky_dome: MeshInstance3D
@onready var camera_rig: Node3D = $CameraRig
@onready var camera: Camera3D = $CameraRig/Camera3D
@onready var sun: DirectionalLight3D = $Sun
@onready var moon: DirectionalLight3D = $Moon
@onready var rain: GPUParticles3D = $Rain
@onready var world_environment: WorldEnvironment = $WorldEnvironment
func _ready() -> void:
_ensure_input_map()
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
bridge = _try_create_bridge()
_create_materials()
_create_sky()
_create_ocean()
_create_islands()
_create_markers()
_create_boat()
_update_environment(0.0)
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("toggle_mouse"):
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED else Input.MOUSE_MODE_CAPTURED
get_viewport().set_input_as_handled()
elif event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
camera_yaw -= event.relative.x * mouse_sensitivity
camera_pitch = clampf(camera_pitch - event.relative.y * mouse_sensitivity, CAMERA_MIN_PITCH, CAMERA_MAX_PITCH)
get_viewport().set_input_as_handled()
func _process(delta: float) -> void:
var dt := clampf(delta, 0.001, 0.05)
_update_boat(dt)
_update_weather(dt)
_update_environment(dt)
_update_camera(dt)
_update_ocean_tiles()
rain.position = boat_position + Vector3(0.0, 58.0, 0.0)
func _try_create_bridge() -> Object:
if not ClassDB.class_exists("TypePhpOceanBridge"):
return null
var obj: Object = ClassDB.instantiate("TypePhpOceanBridge")
if obj is Node:
add_child(obj)
return obj
func _create_materials() -> void:
ocean_material = _make_ocean_material()
island_material = _make_island_material()
sky_material = _make_sky_material()
sail_material = StandardMaterial3D.new()
sail_material.albedo_color = Color(0.94, 0.9, 0.78)
sail_material.roughness = 0.82
sail_material.metallic = 0.0
sail_material.cull_mode = BaseMaterial3D.CULL_DISABLED
func _make_ocean_material() -> ShaderMaterial:
var shader := Shader.new()
shader.code = """
shader_type spatial;
render_mode specular_schlick_ggx, cull_disabled;
uniform vec4 deep_color : source_color = vec4(0.002, 0.026, 0.105, 1.0);
uniform vec4 mid_color : source_color = vec4(0.006, 0.100, 0.290, 1.0);
uniform vec4 shallow_color : source_color = vec4(0.018, 0.260, 0.560, 1.0);
uniform vec4 storm_color : source_color = vec4(0.006, 0.026, 0.065, 1.0);
uniform float storm = 0.0;
uniform float rain = 0.0;
uniform float time_scale = 1.0;
uniform vec3 sun_dir = vec3(-0.45, 0.72, -0.54);
uniform vec3 boat_pos = vec3(0.0, 0.0, 0.0);
uniform vec2 boat_forward = vec2(0.0, -1.0);
uniform float boat_speed = 0.0;
float wave(vec2 p, vec2 d, float amp, float freq, float speed) {
float phase = dot(p, normalize(d)) * freq + TIME * speed * time_scale;
return sin(phase) * amp * (1.0 + storm * 0.55);
}
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float height_at(vec2 p) {
float h = 0.0;
h += wave(p, vec2(0.92, 0.28), 0.72, 0.035, 0.54);
h += wave(p, vec2(0.38, 1.0), 0.38, 0.070, 0.86);
h += wave(p, vec2(-0.68, 0.55), 0.16, 0.155, 1.58);
h += wave(p, vec2(0.24, -0.92), 0.045, 0.410, 2.45);
return h;
}
void vertex() {
VERTEX.y += height_at(VERTEX.xz + NODE_POSITION_WORLD.xz) * 0.035;
}
void fragment() {
vec2 p = (VERTEX.xz + NODE_POSITION_WORLD.xz);
vec2 d0 = normalize(vec2(0.92, 0.28));
vec2 d1 = normalize(vec2(0.38, 1.0));
vec2 d2 = normalize(vec2(-0.68, 0.55));
vec2 d3 = normalize(vec2(0.24, -0.92));
float p0 = dot(p, d0) * 0.035 + TIME * 0.54;
float p1 = dot(p, d1) * 0.070 + TIME * 0.86;
float p2 = dot(p, d2) * 0.155 + TIME * 1.58;
float p3 = dot(p, d3) * 0.410 + TIME * 2.45;
float p4 = dot(p, normalize(vec2(0.78, -0.18))) * 0.82 + TIME * 3.35;
float p5 = dot(p, normalize(vec2(-0.22, 0.96))) * 1.37 + TIME * 4.25;
float h = sin(p0) * 0.72 + sin(p1) * 0.38 + sin(p2) * 0.16 + sin(p3) * 0.045 + sin(p4) * 0.018 + sin(p5) * 0.010;
vec2 micro = vec2(noise(p * 0.085 + vec2(TIME * 0.08, 3.7)), noise(p * 0.095 + vec2(11.4, TIME * 0.07))) - vec2(0.5);
vec2 slope = d0 * cos(p0) * 0.025 + d1 * cos(p1) * 0.027 + d2 * cos(p2) * 0.025 + d3 * cos(p3) * 0.018 + normalize(vec2(0.78, -0.18)) * cos(p4) * 0.015 + normalize(vec2(-0.22, 0.96)) * cos(p5) * 0.010 + micro * 0.018;
float view_distance = length(VERTEX.xz);
float near_detail = 1.0 - smoothstep(280.0, 820.0, view_distance);
NORMAL = normalize(vec3(-slope.x * mix(10.0, 19.0, near_detail), 1.0, -slope.y * mix(10.0, 19.0, near_detail)));
vec3 view_dir = normalize(VIEW);
vec3 light_dir = normalize(sun_dir);
vec3 half_dir = normalize(light_dir - view_dir);
float facing = pow(1.0 - clamp(dot(view_dir, NORMAL), 0.0, 1.0), 4.0);
float chop = clamp(length(slope) * 8.0 + abs(h) * 0.18 + storm * 0.36, 0.0, 1.0);
float sky_reflect = pow(facing, 0.85) * (0.10 - rain * 0.05);
float long_bands = sin(dot(p, vec2(0.010, 0.018)) + TIME * 0.16) * 0.5 + 0.5;
vec3 sea = mix(deep_color.rgb, mid_color.rgb, 0.30 + chop * 0.16 + long_bands * 0.08);
sea = mix(sea, shallow_color.rgb, smoothstep(0.80, 1.22, h) * 0.20 * near_detail);
sea = mix(sea, storm_color.rgb, storm * 0.72);
sea += vec3(0.025, 0.075, 0.14) * sky_reflect;
float foam_lines = smoothstep(0.84, 1.0, sin(dot(p, vec2(0.055, 0.072)) + TIME * 1.35) * 0.5 + 0.5);
float sun_glint = pow(max(dot(NORMAL, half_dir), 0.0), 110.0) * smoothstep(0.05, 0.65, light_dir.y) * (1.0 - rain * 0.75);
float sparkle_a = smoothstep(0.88, 1.0, sin(dot(p, vec2(0.72, -0.18)) + TIME * 3.4) * 0.5 + 0.5);
float sparkle_b = smoothstep(0.90, 1.0, sin(dot(p, vec2(-0.24, 1.18)) + TIME * 4.7) * 0.5 + 0.5);
float grid_breakup = sin(p.x * 0.173 + p.y * 0.097 + TIME * 0.41) * 0.5 + 0.5;
float glitter = (sparkle_a * 0.50 + sparkle_b * 0.34 + grid_breakup * 0.16) * smoothstep(0.38, 0.92, chop) * (1.0 - storm * 0.55) * near_detail;
float crest_foam = smoothstep(0.80, 1.0, chop) * foam_lines * (0.040 + storm * 0.22) * near_detail;
vec2 to_boat = p - boat_pos.xz;
vec2 forward = normalize(boat_forward);
vec2 right = vec2(forward.y, -forward.x);
float behind = smoothstep(3.0, -18.0, dot(to_boat, forward));
float side = abs(dot(normalize(to_boat + vec2(0.001)), right));
float wake_wing = smoothstep(0.72, 0.46, abs(side - 0.55));
float wake_fade = smoothstep(46.0, 3.0, length(to_boat));
float wake_core = smoothstep(0.18, 0.02, abs(dot(to_boat, right))) * smoothstep(22.0, 3.0, length(to_boat));
float wake = (wake_wing * 0.88 + wake_core * 0.32) * wake_fade * behind * clamp(boat_speed / 8.0, 0.0, 1.0);
float foam = clamp(crest_foam + wake * 0.72, 0.0, 1.0);
vec3 glint_color = vec3(0.80, 0.93, 1.0) * (sun_glint * 0.95 + glitter * 0.10);
float horizon_blend = smoothstep(520.0, 1180.0, view_distance);
vec3 horizon_water = vec3(0.050, 0.150, 0.310);
ALBEDO = mix(mix(sea, vec3(0.72, 0.88, 0.94), foam) + glint_color, horizon_water, horizon_blend * (0.22 + storm * 0.20));
ROUGHNESS = mix(0.070, 0.22, storm + rain * 0.45);
SPECULAR = mix(0.92, 0.45, rain);
RIM = facing * 0.28;
RIM_TINT = 0.28;
}
"""
var material := ShaderMaterial.new()
material.shader = shader
return material
func _make_island_material() -> ShaderMaterial:
var shader := Shader.new()
shader.code = """
shader_type spatial;
render_mode cull_back, diffuse_burley, specular_schlick_ggx;
uniform vec4 sand : source_color = vec4(0.76, 0.62, 0.36, 1.0);
uniform vec4 soil : source_color = vec4(0.62, 0.45, 0.22, 1.0);
uniform vec4 rock : source_color = vec4(0.46, 0.35, 0.21, 1.0);
uniform vec4 grass : source_color = vec4(0.12, 0.34, 0.12, 1.0);
float patch_noise(vec2 p) {
return fract(sin(dot(floor(p), vec2(127.1, 311.7))) * 43758.5453);
}
void fragment() {
float slope = 1.0 - clamp(NORMAL.y, 0.0, 1.0);
float height = VERTEX.y;
float beach = smoothstep(0.0, 1.8, height);
vec3 color = mix(sand.rgb, soil.rgb, beach);
color = mix(color, rock.rgb, smoothstep(0.38, 0.78, slope));
float vegetation_noise = patch_noise(VERTEX.xz * 0.11);
float vegetation = smoothstep(2.7, 7.0, height) * smoothstep(0.66, 0.18, slope) * smoothstep(0.36, 0.74, vegetation_noise);
color = mix(color, grass.rgb, vegetation * 0.78);
ALBEDO = color;
ROUGHNESS = 0.88;
SPECULAR = 0.25;
}
"""
var material := ShaderMaterial.new()
material.shader = shader
return material
func _make_sky_material() -> ShaderMaterial:
var shader := Shader.new()
shader.code = """
shader_type spatial;
render_mode unshaded, cull_front, depth_draw_never, fog_disabled;
uniform float day_time = 0.28;
uniform float storm = 0.0;
uniform vec3 sun_dir = vec3(-0.45, 0.72, -0.54);
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
void fragment() {
vec3 v = normalize(VIEW);
float up = clamp(v.y * 0.5 + 0.5, 0.0, 1.0);
float daylight = smoothstep(-0.16, 0.18, sun_dir.y);
vec3 day_top = vec3(0.10, 0.46, 0.92);
vec3 day_horizon = vec3(0.70, 0.88, 1.0);
vec3 dusk_top = vec3(0.08, 0.12, 0.30);
vec3 dusk_horizon = vec3(1.0, 0.44, 0.22);
vec3 night_top = vec3(0.01, 0.018, 0.055);
vec3 night_horizon = vec3(0.05, 0.08, 0.16);
float dusk = 1.0 - smoothstep(0.08, 0.42, abs(sun_dir.y));
vec3 top = mix(night_top, day_top, daylight);
vec3 horizon = mix(night_horizon, day_horizon, daylight);
top = mix(top, dusk_top, dusk * 0.5);
horizon = mix(horizon, dusk_horizon, dusk);
vec3 sky = mix(horizon, top, smoothstep(0.02, 0.92, up));
sky += vec3(0.04, 0.10, 0.16) * smoothstep(0.36, 0.88, up) * daylight;
sky = mix(sky, vec3(0.09, 0.11, 0.13), storm * 0.78);
float sun = pow(max(dot(normalize(-v), normalize(sun_dir)), 0.0), 640.0) * daylight;
float glow = pow(max(dot(normalize(-v), normalize(sun_dir)), 0.0), 14.0) * daylight;
float stars = step(0.996, hash(floor(v.xz * 460.0))) * (1.0 - daylight) * smoothstep(0.18, 0.8, up);
ALBEDO = sky + vec3(1.0, 0.76, 0.35) * sun * 4.0 + vec3(1.0, 0.45, 0.20) * glow * dusk * 0.55 + vec3(stars);
}
"""
var material := ShaderMaterial.new()
material.shader = shader
return material
func _create_sky() -> void:
sky_dome = MeshInstance3D.new()
sky_dome.name = "SkyDome"
var sphere := SphereMesh.new()
sphere.radius = 900.0
sphere.height = 900.0
sphere.radial_segments = 64
sphere.rings = 32
sky_dome.mesh = sphere
sky_dome.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
sky_dome.material_override = sky_material
add_child(sky_dome)
for i in range(CLOUD_LAYER_COUNT):
var cloud := MeshInstance3D.new()
cloud.name = "CloudLayer%d" % i
var mesh := PlaneMesh.new()
mesh.size = Vector2(1100.0 + i * 120.0, 1100.0 + i * 120.0)
mesh.subdivide_width = 1
mesh.subdivide_depth = 1
cloud.mesh = mesh
cloud.position = Vector3((i - 2) * 80.0, 84.0 + i * 13.0, -70.0 + i * 45.0)
cloud.rotation.x = deg_to_rad(4.0 + i * 1.5)
var material := _make_cloud_material(i)
cloud_materials.append(material)
cloud.material_override = material
add_child(cloud)
_create_horizon_clouds()
_create_cirrus_clouds()
func _create_horizon_clouds() -> void:
var material := _make_horizon_cloud_material()
cloud_materials.append(material)
for i in range(8):
var cloud := MeshInstance3D.new()
cloud.name = "HorizonCloudBand%d" % i
var mesh := PlaneMesh.new()
mesh.size = Vector2(320.0, 76.0)
cloud.mesh = mesh
var angle := TAU * float(i) / 8.0
var pos := Vector3(sin(angle) * 620.0, 64.0 + sin(angle * 2.0) * 10.0, cos(angle) * 620.0)
cloud.position = pos
cloud.look_at_from_position(pos, Vector3(0.0, 58.0, 0.0), Vector3.UP)
cloud.rotation.x += deg_to_rad(-4.0)
cloud.material_override = material
add_child(cloud)
func _create_cirrus_clouds() -> void:
var material := _make_cirrus_cloud_material()
cloud_materials.append(material)
for i in range(3):
var cloud := MeshInstance3D.new()
cloud.name = "CirrusCloud%d" % i
var mesh := PlaneMesh.new()
mesh.size = Vector2(920.0, 210.0)
cloud.mesh = mesh
cloud.position = Vector3((i - 1) * 170.0, 210.0 + i * 16.0, -260.0 + i * 130.0)
cloud.rotation = Vector3(deg_to_rad(12.0), deg_to_rad(8.0 + i * 9.0), deg_to_rad(-4.0 + i * 3.0))
cloud.material_override = material
add_child(cloud)
func _make_cloud_material(layer: int) -> ShaderMaterial:
var shader := Shader.new()
shader.code = """
shader_type spatial;
render_mode blend_mix, depth_draw_never, cull_disabled, unshaded, fog_disabled;
uniform float coverage = 0.42;
uniform float density = 0.64;
uniform float storm = 0.0;
uniform float speed = 0.018;
uniform vec4 bright : source_color = vec4(1.0, 1.0, 0.96, 0.68);
uniform vec4 dark : source_color = vec4(0.33, 0.39, 0.46, 0.82);
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = fract(sin(dot(i, vec2(127.1, 311.7))) * 43758.5453);
float b = fract(sin(dot(i + vec2(1, 0), vec2(127.1, 311.7))) * 43758.5453);
float c = fract(sin(dot(i + vec2(0, 1), vec2(127.1, 311.7))) * 43758.5453);
float d = fract(sin(dot(i + vec2(1, 1), vec2(127.1, 311.7))) * 43758.5453);
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
for (int i = 0; i < 5; i++) {
v += noise(p) * a;
p *= 2.17;
a *= 0.52;
}
return v;
}
void fragment() {
vec2 uv = UV * 5.0 + vec2(TIME * speed, TIME * speed * 0.23);
float base = fbm(uv);
float detail = fbm(uv * 3.0 + vec2(17.0, 9.0));
float edge = smoothstep(coverage, 1.0, base * 0.82 + detail * 0.26);
float alpha = edge * density * (0.55 + storm * 0.42);
vec3 color = mix(bright.rgb, dark.rgb, storm * 0.72 + (1.0 - detail) * 0.18);
ALBEDO = color;
ALPHA = alpha;
}
"""
var material := ShaderMaterial.new()
material.shader = shader
material.set_shader_parameter("coverage", 0.45 + layer * 0.035)
material.set_shader_parameter("density", 0.36 + layer * 0.065)
material.set_shader_parameter("speed", 0.012 + layer * 0.003)
return material
func _make_horizon_cloud_material() -> ShaderMaterial:
var shader := Shader.new()
shader.code = """
shader_type spatial;
render_mode blend_mix, depth_draw_never, cull_disabled, unshaded, fog_disabled;
uniform float storm = 0.0;
uniform vec4 bright : source_color = vec4(1.0, 0.98, 0.91, 0.82);
uniform vec4 shade : source_color = vec4(0.52, 0.66, 0.78, 0.72);
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float fbm(vec2 p) {
float v = 0.0;
float a = 0.55;
for (int i = 0; i < 4; i++) {
v += noise(p) * a;
p *= 2.1;
a *= 0.5;
}
return v;
}
void fragment() {
vec2 uv = UV;
float mound = smoothstep(0.12, 0.36, uv.y) * smoothstep(1.0, 0.44, uv.y);
float n = fbm(vec2(uv.x * 8.0 + TIME * 0.006, uv.y * 3.2));
float shape = smoothstep(0.34, 0.72, n + mound * 0.42);
float horizon_fade = smoothstep(0.02, 0.18, uv.y) * smoothstep(0.96, 0.48, uv.y);
vec3 color = mix(shade.rgb, bright.rgb, smoothstep(0.35, 0.86, uv.y + n * 0.22));
color = mix(color, vec3(0.42, 0.48, 0.54), storm * 0.72);
ALBEDO = color;
ALPHA = shape * horizon_fade * mix(0.70, 0.88, storm);
}
"""
var material := ShaderMaterial.new()
material.shader = shader
return material
func _make_cirrus_cloud_material() -> ShaderMaterial:
var shader := Shader.new()
shader.code = """
shader_type spatial;
render_mode blend_mix, depth_draw_never, cull_disabled, unshaded, fog_disabled;
uniform float storm = 0.0;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(269.5, 183.3))) * 43758.5453);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
void fragment() {
vec2 uv = UV;
vec2 stretched = vec2(uv.x * 13.0 + uv.y * 4.5 + TIME * 0.01, uv.y * 2.3);
float fibers = noise(stretched) * 0.62 + noise(stretched * 2.4 + vec2(3.1, 7.4)) * 0.30;
float long_shape = smoothstep(0.06, 0.22, uv.y) * smoothstep(0.98, 0.34, uv.y);
float torn_edges = smoothstep(0.48, 0.78, fibers);
float alpha = torn_edges * long_shape * (1.0 - storm * 0.72) * 0.54;
ALBEDO = mix(vec3(0.82, 0.91, 1.0), vec3(1.0, 0.98, 0.92), smoothstep(0.36, 0.86, uv.y));
ALPHA = alpha;
}
"""
var material := ShaderMaterial.new()
material.shader = shader
return material
func _create_ocean() -> void:
var mesh := PlaneMesh.new()
mesh.size = Vector2(OCEAN_SIZE, OCEAN_SIZE)
mesh.subdivide_width = 16
mesh.subdivide_depth = 16
for x in range(-1, 2):
for z in range(-1, 2):
var tile := MeshInstance3D.new()
tile.name = "OceanTile_%d_%d" % [x, z]
tile.mesh = mesh
tile.material_override = ocean_material
tile.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
add_child(tile)
ocean_tiles.append(tile)
_update_ocean_tiles()
func _update_ocean_tiles() -> void:
var center_x: float = floor(boat_position.x / OCEAN_SIZE) * OCEAN_SIZE
var center_z: float = floor(boat_position.z / OCEAN_SIZE) * OCEAN_SIZE
var index := 0
for x in range(-1, 2):
for z in range(-1, 2):
var tile := ocean_tiles[index]
tile.position = Vector3(center_x + float(x) * OCEAN_SIZE, 0.0, center_z + float(z) * OCEAN_SIZE)
index += 1
func _create_islands() -> void:
island_root = Node3D.new()
island_root.name = "Islands"
add_child(island_root)
var islands: Array = _bridge_array("get_islands", [1180.0])
if islands.is_empty():
islands = _fallback_islands()
for island in islands:
if island is Dictionary:
_add_island(island)
func _fallback_islands() -> Array:
return [
{"x": 170.0, "z": -210.0, "radius": 58.0, "height": 20.0, "seed": 11.0},
{"x": -260.0, "z": 120.0, "radius": 42.0, "height": 14.0, "seed": 27.0},
{"x": 420.0, "z": 310.0, "radius": 78.0, "height": 28.0, "seed": 43.0},
{"x": -520.0, "z": -390.0, "radius": 64.0, "height": 22.0, "seed": 61.0},
{"x": 720.0, "z": -110.0, "radius": 52.0, "height": 18.0, "seed": 83.0},
{"x": -810.0, "z": 470.0, "radius": 70.0, "height": 31.0, "seed": 97.0},
{"x": 120.0, "z": 820.0, "radius": 46.0, "height": 16.0, "seed": 109.0},
{"x": -650.0, "z": -40.0, "radius": 88.0, "height": 34.0, "seed": 131.0},
{"x": 910.0, "z": 560.0, "radius": 61.0, "height": 24.0, "seed": 149.0},
]
func _add_island(info: Dictionary) -> void:
var center: Vector3 = Vector3(float(info.get("x", 0.0)), 0.0, float(info.get("z", 0.0)))
var radius: float = float(info.get("radius", 45.0))
var height: float = float(info.get("height", 16.0))
var seed: float = float(info.get("seed", 1.0))
var mesh := ArrayMesh.new()
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var uvs := PackedVector2Array()
var indices := PackedInt32Array()
for z in range(ISLAND_GRID + 1):
for x in range(ISLAND_GRID + 1):
var fx := (float(x) / ISLAND_GRID - 0.5) * radius * 2.35
var fz := (float(z) / ISLAND_GRID - 0.5) * radius * 2.35
var d := Vector2(fx, fz).length() / radius
var core := pow(clampf(1.0 - d, 0.0, 1.0), 2.0)
var beach_ring := smoothstep(1.04, 0.72, d)
var ridge_a := sin((fx + seed) * 0.055) * cos((fz - seed) * 0.047)
var ridge_b := sin((fx * 0.12 - fz * 0.09) + seed * 0.31)
var ridges := (ridge_a * 0.30 + ridge_b * 0.14) * core
var detail := sin((fx - fz) * 0.21 + seed) * sin((fx + fz) * 0.13) * 0.07 * core
var y := (core + ridges + detail) * height
y = y * beach_ring - smoothstep(0.76, 1.05, d) * 1.6
if d > 0.70:
y = minf(y, 1.3 + (1.0 - d) * 2.2)
vertices.append(center + Vector3(fx, y, fz))
normals.append(Vector3.UP)
uvs.append(Vector2(float(x) / ISLAND_GRID, float(z) / ISLAND_GRID))
for z in range(ISLAND_GRID):
for x in range(ISLAND_GRID):
var i := z * (ISLAND_GRID + 1) + x
indices.append_array([i, i + ISLAND_GRID + 1, i + 1, i + 1, i + ISLAND_GRID + 1, i + ISLAND_GRID + 2])
var arrays := []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_NORMAL] = _derive_normals(vertices, indices)
arrays[Mesh.ARRAY_TEX_UV] = uvs
arrays[Mesh.ARRAY_INDEX] = indices
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
var island := MeshInstance3D.new()
island.mesh = mesh
island.material_override = island_material
island_root.add_child(island)
_add_palms(center, radius, height, int(seed))
func _derive_normals(vertices: PackedVector3Array, indices: PackedInt32Array) -> PackedVector3Array:
var normals := PackedVector3Array()
normals.resize(vertices.size())
for i in range(0, indices.size(), 3):
var a := indices[i]
var b := indices[i + 1]
var c := indices[i + 2]
var n := (vertices[b] - vertices[a]).cross(vertices[c] - vertices[a]).normalized()
normals[a] += n
normals[b] += n
normals[c] += n
for i in range(normals.size()):
normals[i] = normals[i].normalized()
return normals
func _add_palms(center: Vector3, radius: float, height: float, seed: int) -> void:
var trunk_mat := StandardMaterial3D.new()
trunk_mat.albedo_color = Color(0.36, 0.22, 0.11)
trunk_mat.roughness = 0.9
var leaf_mat := StandardMaterial3D.new()
leaf_mat.albedo_color = Color(0.06, 0.38, 0.14)
leaf_mat.roughness = 0.8
for i in range(8):
var a := float(seed * 37 + i * 53)
var r := radius * (0.24 + absf(sin(a * 1.71)) * 0.42)
var angle := a * 0.37
var pos := center + Vector3(cos(angle) * r, height * 0.34, sin(angle) * r)
var trunk := MeshInstance3D.new()
trunk.mesh = CylinderMesh.new()
(trunk.mesh as CylinderMesh).top_radius = 0.22
(trunk.mesh as CylinderMesh).bottom_radius = 0.34
(trunk.mesh as CylinderMesh).height = 6.5
trunk.position = pos + Vector3(0, 3.0, 0)
trunk.rotation.z = sin(a) * 0.18
trunk.material_override = trunk_mat
island_root.add_child(trunk)
for k in range(7):
var leaf := MeshInstance3D.new()
var plane := PlaneMesh.new()
plane.size = Vector2(1.7, 6.4)
leaf.mesh = plane
leaf.position = pos + Vector3(0, 6.8, 0)
leaf.rotation = Vector3(deg_to_rad(68.0), angle + TAU * float(k) / 7.0, 0.0)
leaf.material_override = leaf_mat
island_root.add_child(leaf)
func _create_markers() -> void:
marker_root = Node3D.new()
marker_root.name = "Markers"
add_child(marker_root)
var markers: Array = _bridge_array("get_markers", [760.0])
if markers.is_empty():
markers = [
{"x": -90.0, "z": -155.0, "type": 1.0, "size": 2.6},
{"x": 285.0, "z": 70.0, "type": 2.0, "size": 4.5},
{"x": -390.0, "z": 280.0, "type": 3.0, "size": 7.0},
]
for marker in markers:
if marker is Dictionary:
_add_marker(marker)
func _add_marker(info: Dictionary) -> void:
var type: int = int(info.get("type", 0))
var size: float = float(info.get("size", 2.0))
var x: float = float(info.get("x", 0.0))
var z: float = float(info.get("z", 0.0))
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(0.85, 0.12, 0.08) if type != 2 else Color(0.1, 0.25, 0.95)
mat.emission_enabled = type == 3
mat.emission = Color(1.0, 0.75, 0.32)
mat.emission_energy_multiplier = 1.4
var node := MeshInstance3D.new()
node.mesh = CylinderMesh.new()
(node.mesh as CylinderMesh).top_radius = size * 0.22
(node.mesh as CylinderMesh).bottom_radius = size * 0.35
(node.mesh as CylinderMesh).height = size * 2.4
node.position = Vector3(x, size * 1.1, z)
node.material_override = mat
marker_root.add_child(node)
if type == 3:
var light := OmniLight3D.new()
light.light_color = Color(1.0, 0.62, 0.26)
light.light_energy = 1.8
light.omni_range = 34.0
node.add_child(light)
func _create_boat() -> void:
boat = Node3D.new()
boat.name = "Sailboat"
add_child(boat)
var hull_mat := StandardMaterial3D.new()
hull_mat.albedo_color = Color(0.34, 0.14, 0.055)
hull_mat.roughness = 0.72
var trim_mat := StandardMaterial3D.new()
trim_mat.albedo_color = Color(0.72, 0.54, 0.30)
trim_mat.roughness = 0.62
var hull := MeshInstance3D.new()
var hull_mesh := BoxMesh.new()
hull_mesh.size = Vector3(3.2, 0.9, 8.0)
hull.mesh = hull_mesh
hull.position.y = 0.48
hull.scale = Vector3(1.0, 0.7, 1.0)
hull.material_override = hull_mat
boat.add_child(hull)
var mast := MeshInstance3D.new()
mast.mesh = CylinderMesh.new()
(mast.mesh as CylinderMesh).top_radius = 0.08
(mast.mesh as CylinderMesh).bottom_radius = 0.12
(mast.mesh as CylinderMesh).height = 8.0
mast.position = Vector3(0.0, 4.1, -0.7)
mast.material_override = trim_mat
boat.add_child(mast)
_add_sail(Vector3(0.08, 4.8, -0.8), 3.2, 6.0, false)
_add_sail(Vector3(-0.08, 3.5, -0.35), 2.2, 4.0, true)
var lamp := OmniLight3D.new()
lamp.name = "WarmLantern"
lamp.position = Vector3(0.0, 1.35, -3.4)
lamp.light_color = Color(1.0, 0.58, 0.24)
lamp.light_energy = 1.2
lamp.omni_range = 18.0
boat.add_child(lamp)
func _add_sail(offset: Vector3, width: float, height: float, flip: bool) -> void:
var mesh := ArrayMesh.new()
var vertices := PackedVector3Array([
Vector3(0, -height * 0.5, 0),
Vector3(width * ( -1.0 if flip else 1.0), -height * 0.35, 0),
Vector3(0, height * 0.5, 0),
])
var arrays := []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_NORMAL] = PackedVector3Array([Vector3.BACK, Vector3.BACK, Vector3.BACK])
arrays[Mesh.ARRAY_INDEX] = PackedInt32Array([0, 1, 2])
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
var sail := MeshInstance3D.new()
sail.mesh = mesh
sail.position = offset
sail.material_override = sail_material
boat.add_child(sail)
func _update_boat(dt: float) -> void:
var input: Vector2 = Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var forward := Vector3(-sin(camera_yaw), 0.0, -cos(camera_yaw))
var right := Vector3(cos(camera_yaw), 0.0, -sin(camera_yaw))
var desired := (right * input.x + forward * -input.y)
if desired.length_squared() > 0.001:
desired = desired.normalized()
boat_yaw = lerp_angle(boat_yaw, atan2(desired.x, -desired.z), 1.0 - pow(0.05, dt))
var wind := Vector3(-0.35, 0.0, -0.94).normalized()
var sail_power := clampf(0.55 + max(0.0, desired.dot(wind)) * 0.85, 0.35, 1.6)
var boost: float = 1.55 if Input.is_action_pressed("boost") else 1.0
boat_velocity += desired * 13.0 * sail_power * boost * dt
boat_velocity *= pow(0.10, dt)
boat_position += boat_velocity * dt
var boat_forward := Vector2(-sin(boat_yaw), -cos(boat_yaw))
ocean_material.set_shader_parameter("boat_pos", boat_position)
ocean_material.set_shader_parameter("boat_forward", boat_forward)
ocean_material.set_shader_parameter("boat_speed", boat_velocity.length())
var bob := _water_height(boat_position.x, boat_position.z)
boat.position = Vector3(boat_position.x, bob + 0.52, boat_position.z)
boat.rotation = Vector3(sin(Time.get_ticks_msec() * 0.0017) * 0.035, boat_yaw, sin(Time.get_ticks_msec() * 0.0011) * 0.055)
func _water_height(x: float, z: float) -> float:
var t: float = Time.get_ticks_msec() * 0.001
var p := Vector2(x, z)
return sin(p.dot(Vector2(0.92, 0.28).normalized()) * 0.035 + t * 0.54) * 0.72 + sin(p.dot(Vector2(0.38, 1.0).normalized()) * 0.070 + t * 0.86) * 0.38 + sin(p.dot(Vector2(-0.68, 0.55).normalized()) * 0.155 + t * 1.58) * 0.16
func _update_weather(dt: float) -> void:
day_time = fmod(day_time + dt / 180.0, 1.0)
next_weather_change -= dt
if next_weather_change <= 0.0 and target_weather == weather:
target_weather = _choose_next_weather(weather)
weather_mix = 0.0
next_weather_change = 20.0 + randf() * 22.0
if target_weather != weather:
weather_mix = minf(1.0, weather_mix + dt / 9.0)
if weather_mix >= 1.0:
weather = target_weather
weather_mix = 0.0
var base_rain := 1.0 if weather == WEATHER_RAIN else 0.0
if target_weather == WEATHER_RAIN:
rain_amount = maxf(base_rain, weather_mix)
elif weather == WEATHER_RAIN:
rain_amount = 1.0 - weather_mix
else:
rain_amount = 0.0
func _choose_next_weather(current: int) -> int:
var roll := randf()
if bridge != null and bridge.has_method("choose_next_weather"):
return int(bridge.call("choose_next_weather", current, roll))
if current == WEATHER_SUNNY:
return WEATHER_CLOUDY if roll < 0.58 else WEATHER_RAIN
if current == WEATHER_CLOUDY:
return WEATHER_SUNNY if roll < 0.42 else WEATHER_RAIN
return WEATHER_CLOUDY if roll < 0.58 else WEATHER_SUNNY
func _update_environment(_dt: float) -> void:
var sun_angle := day_time * TAU - PI * 0.5
var sun_dir := Vector3(cos(sun_angle) * -0.52, sin(sun_angle), -0.62).normalized()
sun.rotation = Basis.looking_at(-sun_dir, Vector3.UP).get_euler()
var daylight := smoothstep(-0.12, 0.28, sun_dir.y)
var storm := clampf((0.55 if target_weather == WEATHER_CLOUDY else 0.0) + rain_amount, 0.0, 1.0)
sun.light_energy = lerpf(0.04, 3.4, daylight) * (1.0 - storm * 0.58)
moon.light_energy = (1.0 - daylight) * 0.48
ocean_material.set_shader_parameter("storm", storm)
ocean_material.set_shader_parameter("rain", rain_amount)
ocean_material.set_shader_parameter("sun_dir", sun_dir)
sky_material.set_shader_parameter("day_time", day_time)
sky_material.set_shader_parameter("storm", storm)
sky_material.set_shader_parameter("sun_dir", sun_dir)
for material in cloud_materials:
material.set_shader_parameter("storm", storm)
material.set_shader_parameter("coverage", lerpf(0.46, 0.28, rain_amount))
rain.emitting = rain_amount > 0.05
rain.amount_ratio = rain_amount
var env: Environment = world_environment.environment
env.fog_density = lerpf(0.004, 0.018, storm)
env.volumetric_fog_density = lerpf(0.012, 0.045, storm)
func _update_camera(dt: float) -> void:
var distance := lerpf(26.0, 34.0, clampf(boat_velocity.length() / 16.0, 0.0, 1.0))
var camera_offset := Basis(Vector3.UP, camera_yaw) * Vector3(0.0, 8.8, distance)
var target_height := maxf(2.2, 5.0 + sin(camera_pitch) * 10.0)
var target := boat_position + Vector3(0.0, target_height, 0.0)
camera_rig.position = camera_rig.position.lerp(target, 1.0 - pow(0.01, dt))
camera.position = camera.position.lerp(camera_offset, 1.0 - pow(0.02, dt))
camera.look_at(target, Vector3.UP)
func _bridge_array(method: StringName, args: Array) -> Array:
if bridge == null or not bridge.has_method(method):
return []
var value = bridge.callv(method, args)
return value if value is Array else []
func _ensure_input_map() -> void:
_add_key_action("move_forward", [KEY_W, KEY_UP])
_add_key_action("move_back", [KEY_S, KEY_DOWN])
_add_key_action("move_left", [KEY_A, KEY_LEFT])
_add_key_action("move_right", [KEY_D, KEY_RIGHT])
_add_key_action("toggle_mouse", [KEY_ESCAPE])
_add_key_action("boost", [KEY_SHIFT])
func _add_key_action(action: StringName, keys: Array) -> void:
if not InputMap.has_action(action):
InputMap.add_action(action)
for key in keys:
var exists := false
for event in InputMap.action_get_events(action):
if event is InputEventKey and event.keycode == key:
exists = true
break
if exists:
continue
var input := InputEventKey.new()
input.keycode = key
input.physical_keycode = key
InputMap.action_add_event(action, input)

@ -0,0 +1,7 @@
name: typephp_ocean
mode: lib
version: 0.1.0
cxx-std: c++17
sources:
- php-src
- cpp-src

@ -0,0 +1,10 @@
[configuration]
entry_symbol = "typephp_ocean_bridge_init"
compatibility_minimum = "4.4"
reloadable = true
[libraries]
windows.debug.x86_64 = "res://bin/typephp_ocean_godot_bridge.dll"
windows.release.x86_64 = "res://bin/typephp_ocean_godot_bridge.dll"
windows.template_debug.x86_64 = "res://bin/typephp_ocean_godot_bridge.dll"
windows.template_release.x86_64 = "res://bin/typephp_ocean_godot_bridge.dll"
Loading…
Cancel
Save