diff --git a/examples/ocean-demo/1r.png b/examples/ocean-demo/1r.png new file mode 100644 index 00000000..d31997f0 Binary files /dev/null and b/examples/ocean-demo/1r.png differ diff --git a/examples/ocean-demo/README.md b/examples/ocean-demo/README.md new file mode 100644 index 00000000..bcd7e546 --- /dev/null +++ b/examples/ocean-demo/README.md @@ -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` 弹出退出确认框 diff --git a/examples/ocean-demo/cpp-src/ocean_backend.cc b/examples/ocean-demo/cpp-src/ocean_backend.cc new file mode 100644 index 00000000..52c84fdf --- /dev/null +++ b/examples/ocean-demo/cpp-src/ocean_backend.cc @@ -0,0 +1,1206 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace php; + +static HWND g_hwnd = nullptr; +static HDC g_hdc = nullptr; +static HGLRC g_glrc = nullptr; +static bool g_should_close = false; +static bool g_keys[256] = {}; +static bool g_mouse_captured = false; +static bool g_mouse_ready = false; +static POINT g_last_mouse = {}; +static double g_mouse_dx = 0.0; +static double g_mouse_dy = 0.0; +static int g_width = 1280; +static int g_height = 720; + +static float g_boat_x = 0.0f; +static float g_boat_z = 0.0f; +static float g_boat_yaw = 0.0f; +static float g_boat_speed = 0.0f; +static float g_camera_yaw = 0.0f; +static float g_camera_pitch = 0.32f; +static float g_camera_distance = 18.0f; +static float g_day_time = 0.28f; +static int g_weather = 0; +static float g_weather_mix = 0.0f; +static float g_rain = 0.0f; +static double g_start_time = 0.0; + +struct ChunkKey { + int x; + int z; + + bool operator==(const ChunkKey &other) const { + return x == other.x && z == other.z; + } +}; + +struct ChunkKeyHash { + size_t operator()(const ChunkKey &key) const { + const uint64_t x = static_cast(key.x); + const uint64_t z = static_cast(key.z); + return static_cast((x * 73856093u) ^ (z * 83492791u)); + } +}; + +struct OceanMarker { + float x; + float z; + int type; + float size; +}; + +static std::unordered_map, ChunkKeyHash> g_marker_chunks; +static std::vector g_pending_markers; +static ChunkKey g_pending_chunk{0, 0}; +static bool g_has_pending_chunk = false; + +struct SpectralWave { + float dir_x; + float dir_z; + float amplitude; + float wavelength; + float phase; + float steepness; +}; + +static const SpectralWave SPECTRAL_WAVES[] = { + {0.78f, 0.62f, 0.34f, 28.0f, 0.0f, 0.42f}, + {0.52f, 0.85f, 0.22f, 17.0f, 1.7f, 0.36f}, + {-0.28f, 0.96f, 0.14f, 9.5f, 3.2f, 0.25f}, + {0.98f, -0.20f, 0.08f, 5.8f, 4.6f, 0.16f}, + {-0.76f, 0.65f, 0.06f, 3.4f, 2.4f, 0.11f}, +}; + +static double now_seconds() { + LARGE_INTEGER freq; + LARGE_INTEGER counter; + QueryPerformanceFrequency(&freq); + QueryPerformanceCounter(&counter); + return static_cast(counter.QuadPart) / static_cast(freq.QuadPart); +} + +static void update_cursor_clip() { + if (!g_hwnd) { + return; + } + RECT rect{}; + GetClientRect(g_hwnd, &rect); + POINT top_left{rect.left, rect.top}; + POINT bottom_right{rect.right, rect.bottom}; + ClientToScreen(g_hwnd, &top_left); + ClientToScreen(g_hwnd, &bottom_right); + rect.left = top_left.x; + rect.top = top_left.y; + rect.right = bottom_right.x; + rect.bottom = bottom_right.y; + ClipCursor(&rect); +} + +static void enable_mouse_capture() { + if (!g_hwnd || g_mouse_captured) { + if (g_mouse_captured) { + update_cursor_clip(); + } + return; + } + SetCapture(g_hwnd); + ShowCursor(FALSE); + update_cursor_clip(); + g_mouse_captured = true; +} + +static void disable_mouse_capture() { + if (!g_mouse_captured) { + return; + } + ClipCursor(nullptr); + ReleaseCapture(); + ShowCursor(TRUE); + g_mouse_captured = false; + g_mouse_ready = false; +} + +static LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { + switch (msg) { + case WM_CLOSE: + case WM_DESTROY: + g_should_close = true; + PostQuitMessage(0); + return 0; + case WM_SIZE: + g_width = std::max(1, static_cast(LOWORD(lp))); + g_height = std::max(1, static_cast(HIWORD(lp))); + update_cursor_clip(); + return 0; + case WM_ACTIVATE: + if (LOWORD(wp) == WA_INACTIVE) { + disable_mouse_capture(); + } else { + enable_mouse_capture(); + } + return 0; + case WM_KEYDOWN: + if (wp < 256) { + g_keys[wp] = true; + } + return 0; + case WM_MOUSEMOVE: { + POINT p{GET_X_LPARAM(lp), GET_Y_LPARAM(lp)}; + if (g_mouse_ready) { + g_mouse_dx += static_cast(p.x - g_last_mouse.x); + g_mouse_dy += static_cast(p.y - g_last_mouse.y); + } + g_last_mouse = p; + g_mouse_ready = true; + return 0; + } + case WM_KEYUP: + if (wp < 256) { + g_keys[wp] = false; + } + return 0; + } + return DefWindowProc(hwnd, msg, wp, lp); +} + +static void set_perspective(double fov_y, double aspect, double near_z, double far_z) { + const double top = near_z * std::tan(fov_y * 3.14159265358979323846 / 360.0); + const double right = top * aspect; + glFrustum(-right, right, -top, top, near_z, far_z); +} + +static float clamp01(float v) { + return std::max(0.0f, std::min(1.0f, v)); +} + +static float smoothstep(float edge0, float edge1, float x) { + const float t = clamp01((x - edge0) / (edge1 - edge0)); + return t * t * (3.0f - 2.0f * t); +} + +static float active_weather_strength() { + const float base = g_weather == 1 ? 0.55f : (g_weather == 2 ? 1.0f : 0.0f); + const float transition = g_weather_mix * 0.30f; + return clamp01(std::max(base, transition)); +} + +static float wave_height(float x, float z, float t) { + float height = 0.0f; + const float storm = active_weather_strength(); + for (const SpectralWave &wave : SPECTRAL_WAVES) { + const float k = 6.2831853f / wave.wavelength; + const float omega = std::sqrt(9.81f * k); + const float phase = k * (x * wave.dir_x + z * wave.dir_z) + omega * t + wave.phase; + const float choppy = std::sin(phase) + 0.34f * std::sin(phase * 2.07f + wave.phase); + height += choppy * wave.amplitude * (1.0f + storm * wave.steepness); + } + return height; +} + +static void normalize3(float v[3]) { + const float len = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); + if (len > 0.0001f) { + v[0] /= len; + v[1] /= len; + v[2] /= len; + } +} + +static void wave_normal(float x, float z, float t, float out[3]) { + const float e = 0.45f; + const float hx1 = wave_height(x + e, z, t); + const float hx0 = wave_height(x - e, z, t); + const float hz1 = wave_height(x, z + e, t); + const float hz0 = wave_height(x, z - e, t); + out[0] = -(hx1 - hx0) / (e * 2.0f); + out[1] = 1.0f; + out[2] = -(hz1 - hz0) / (e * 2.0f); + normalize3(out); +} + +static float sun_height() { + return std::sin((g_day_time - 0.25f) * 6.2831853f); +} + +static void sun_direction(float out[3]) { + const float a = (g_day_time - 0.25f) * 6.2831853f; + out[0] = std::cos(a) * 0.45f; + out[1] = std::max(0.14f, std::sin(a)); + out[2] = -0.72f; + normalize3(out); +} + +static float fresnel_schlick(float cos_theta) { + const float f0 = 0.02f; + return f0 + (1.0f - f0) * std::pow(clamp01(1.0f - cos_theta), 5.0f); +} + +static float ggx_distribution(float n_dot_h, float roughness) { + const float a = roughness * roughness; + const float a2 = a * a; + const float d = n_dot_h * n_dot_h * (a2 - 1.0f) + 1.0f; + return a2 / std::max(0.0001f, 3.14159265f * d * d); +} + +static float slope_roughness(const float n[3]) { + const float slope = std::sqrt(n[0] * n[0] + n[2] * n[2]); + return std::max(0.05f, std::min(0.92f, 0.18f + slope * 1.9f + g_rain * 0.20f)); +} + +static float slope_ambient_occlusion(const float n[3]) { + const float slope = std::sqrt(n[0] * n[0] + n[2] * n[2]); + return clamp01(1.0f - 3.8f * slope); +} + +static float exp_fog_factor(float distance, float density) { + const float f = std::exp(-std::pow(density * distance, 2.0f)); + return clamp01(f); +} + +static float night_factor() { + return clamp01(1.0f - smoothstep(0.18f, 0.30f, g_day_time) + + smoothstep(0.72f, 0.90f, g_day_time)); +} + +static float dawn_dusk_factor() { + const float dawn = smoothstep(0.18f, 0.30f, g_day_time) * (1.0f - smoothstep(0.35f, 0.46f, g_day_time)); + const float dusk = smoothstep(0.60f, 0.72f, g_day_time) * (1.0f - smoothstep(0.78f, 0.90f, g_day_time)); + return std::max(dawn, dusk); +} + +static float tone_map(float color) { + color = std::max(0.0f, color); + color = color / (1.0f + color); + return std::pow(color, 1.0f / 2.2f); +} + +static void tone_map_rgb(float &r, float &g, float &b, float exposure) { + r = tone_map(r * exposure); + g = tone_map(g * exposure); + b = tone_map(b * exposure); +} + +static float remap_value(float value, float old_min, float old_max, float new_min, float new_max) { + return new_min + ((value - old_min) / (old_max - old_min)) * (new_max - new_min); +} + +static float hash31(float x, float y, float z) { + const float n = std::sin(x * 127.1f + y * 311.7f + z * 74.7f) * 43758.5453f; + return n - std::floor(n); +} + +static float value_noise3(float x, float y, float z) { + const float ix = std::floor(x); + const float iy = std::floor(y); + const float iz = std::floor(z); + const float fx = x - ix; + const float fy = y - iy; + const float fz = z - iz; + const float ux = fx * fx * fx * (fx * (fx * 6.0f - 15.0f) + 10.0f); + const float uy = fy * fy * fy * (fy * (fy * 6.0f - 15.0f) + 10.0f); + const float uz = fz * fz * fz * (fz * (fz * 6.0f - 15.0f) + 10.0f); + + float c[2][2][2]; + for (int dx = 0; dx <= 1; dx++) { + for (int dy = 0; dy <= 1; dy++) { + for (int dz = 0; dz <= 1; dz++) { + c[dx][dy][dz] = hash31(ix + dx, iy + dy, iz + dz); + } + } + } + + const float x00 = c[0][0][0] + (c[1][0][0] - c[0][0][0]) * ux; + const float x10 = c[0][1][0] + (c[1][1][0] - c[0][1][0]) * ux; + const float x01 = c[0][0][1] + (c[1][0][1] - c[0][0][1]) * ux; + const float x11 = c[0][1][1] + (c[1][1][1] - c[0][1][1]) * ux; + const float y0 = x00 + (x10 - x00) * uy; + const float y1 = x01 + (x11 - x01) * uy; + return y0 + (y1 - y0) * uz; +} + +static float worley_noise2(float x, float y) { + const int ix = static_cast(std::floor(x)); + const int iy = static_cast(std::floor(y)); + float min_dist = 10000.0f; + for (int ox = -1; ox <= 1; ox++) { + for (int oy = -1; oy <= 1; oy++) { + const float cx = static_cast(ix + ox) + hash31(static_cast(ix + ox), static_cast(iy + oy), 11.0f); + const float cy = static_cast(iy + oy) + hash31(static_cast(ix + ox), static_cast(iy + oy), 29.0f); + const float dx = x - cx; + const float dy = y - cy; + min_dist = std::min(min_dist, dx * dx + dy * dy); + } + } + return clamp01(1.0f - min_dist); +} + +static float cloud_density_sample(float x, float y, float time, float coverage) { + const float height_fraction = clamp01((y + 0.20f) / 0.82f); + const float wind_x = std::cos(g_day_time * 6.2831853f + 0.7f); + const float wind_y = std::sin(g_day_time * 6.2831853f + 0.7f); + const float shear = height_fraction * 0.34f; + x += wind_x * time * 0.012f + shear; + y += wind_y * time * 0.008f; + + const float base = value_noise3(x * 2.2f, y * 3.0f, time * 0.015f); + const float worley = worley_noise2(x * 3.4f, y * 2.1f) * 0.625f + + worley_noise2(x * 6.8f + 17.0f, y * 4.2f - 9.0f) * 0.25f + + worley_noise2(x * 13.6f - 5.0f, y * 8.4f + 23.0f) * 0.125f; + float shape = remap_value(base, 1.0f - worley * 0.78f, 1.0f, 0.0f, 1.0f); + const float vertical = smoothstep(0.0f, 0.16f, height_fraction) * (1.0f - smoothstep(0.78f, 1.0f, height_fraction)); + shape *= vertical; + + const float detail = value_noise3(x * 13.0f + 4.0f, y * 16.0f - 2.0f, time * 0.05f); + shape = remap_value(shape, (1.0f - detail) * 0.34f, 1.0f, 0.0f, 1.0f); + shape = remap_value(shape, coverage, 1.0f, 0.0f, 1.0f) * coverage; + return clamp01(shape); +} + +static float henyey_greenstein_phase(float cos_angle, float g) { + const float g2 = g * g; + return ((1.0f - g2) / std::pow(1.0f + g2 - 2.0f * g * cos_angle, 1.5f)) * 0.07957747f; +} + +static float cloud_powder(float density, float cos_angle) { + const float powder = 1.0f - std::exp(-density * 2.0f); + const float edge = clamp01((-cos_angle * 0.5f) + 0.5f); + return 1.0f + (powder - 1.0f) * edge; +} + +static void sky_colors(float top[3], float horizon[3], float *light) { + const float night = 1.0f - smoothstep(0.18f, 0.27f, g_day_time) + + smoothstep(0.78f, 0.92f, g_day_time); + const float dawn = smoothstep(0.18f, 0.30f, g_day_time) * (1.0f - smoothstep(0.34f, 0.43f, g_day_time)); + const float dusk = smoothstep(0.62f, 0.72f, g_day_time) * (1.0f - smoothstep(0.78f, 0.88f, g_day_time)); + const float golden = std::max(dawn, dusk); + const float cloudy = g_weather == 1 ? 0.72f : (g_weather == 2 ? 0.90f : 0.0f); + + top[0] = 0.12f + 0.28f * (1.0f - night) + 0.20f * golden; + top[1] = 0.18f + 0.42f * (1.0f - night) + 0.10f * golden; + top[2] = 0.34f + 0.54f * (1.0f - night) - 0.10f * golden; + horizon[0] = 0.18f + 0.55f * (1.0f - night) + 0.34f * golden; + horizon[1] = 0.23f + 0.47f * (1.0f - night) + 0.17f * golden; + horizon[2] = 0.34f + 0.44f * (1.0f - night) - 0.22f * golden; + + for (int i = 0; i < 3; i++) { + top[i] = top[i] * (1.0f - cloudy) + 0.33f * cloudy; + horizon[i] = horizon[i] * (1.0f - cloudy) + 0.46f * cloudy; + } + const float exposure = 1.05f + dawn_dusk_factor() * 0.25f - cloudy * 0.10f; + tone_map_rgb(top[0], top[1], top[2], exposure); + tone_map_rgb(horizon[0], horizon[1], horizon[2], exposure); + *light = std::max(0.12f, 1.0f - night * 0.74f - cloudy * 0.28f); +} + +static void draw_sky() { + float top[3]; + float horizon[3]; + float light = 1.0f; + sky_colors(top, horizon, &light); + + glMatrixMode(GL_PROJECTION); + glPushMatrix(); + glLoadIdentity(); + glOrtho(-1, 1, -1, 1, -1, 1); + glMatrixMode(GL_MODELVIEW); + glPushMatrix(); + glLoadIdentity(); + glDisable(GL_DEPTH_TEST); + glDisable(GL_TEXTURE_2D); + glDisable(GL_FOG); + + glBegin(GL_QUADS); + glColor3f(horizon[0], horizon[1], horizon[2]); + glVertex2f(-1.0f, -1.0f); + glVertex2f(1.0f, -1.0f); + glColor3f(top[0], top[1], top[2]); + glVertex2f(1.0f, 1.0f); + glVertex2f(-1.0f, 1.0f); + glEnd(); + + const float sun_angle = (g_day_time - 0.25f) * 6.2831853f; + const float sx = std::cos(sun_angle) * 0.68f; + const float sy = std::sin(sun_angle) * 0.74f; + const bool moon = sy < -0.05f; + const float body_y = moon ? -sy : sy; + if (body_y > -0.10f) { + glColor4f(moon ? 0.74f : 1.0f, moon ? 0.80f : 0.86f, moon ? 0.92f : 0.42f, moon ? 0.85f : 0.95f); + const float r = moon ? 0.045f : 0.065f; + glBegin(GL_TRIANGLE_FAN); + glVertex2f(sx, body_y); + for (int i = 0; i <= 48; i++) { + const float a = static_cast(i) / 48.0f * 6.2831853f; + glVertex2f(sx + std::cos(a) * r, body_y + std::sin(a) * r); + } + glEnd(); + } + + const float time = static_cast(now_seconds() - g_start_time); + const float weather_coverage = g_weather == 0 ? 0.48f : (g_weather == 1 ? 0.34f : 0.24f); + const float cloud_strength = g_weather == 0 ? 0.46f : (g_weather == 1 ? 0.78f : 0.92f); + float sun_dir[3]; + sun_direction(sun_dir); + const float cos_angle = clamp01(sun_dir[1] * 0.45f + 0.35f); + const float hg = std::max(henyey_greenstein_phase(cos_angle, 0.62f), henyey_greenstein_phase(cos_angle, -0.25f)) * 0.45f + 0.64f; + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + for (int layer = 0; layer < 5; layer++) { + const float layer_y = 0.26f + layer * 0.075f; + const float layer_scale = 1.0f + layer * 0.20f; + const float alpha_scale = (0.10f + layer * 0.055f) * cloud_strength; + const float y_step = 0.055f; + const float x_step = 0.075f; + for (float cy = -0.12f; cy <= 0.76f; cy += y_step) { + glBegin(GL_QUAD_STRIP); + for (float cx = -1.24f; cx <= 1.26f; cx += x_step) { + for (int row = 0; row < 2; row++) { + const float px = cx; + const float py = cy + row * y_step; + const float density = cloud_density_sample(px * layer_scale + layer * 2.7f, py * layer_scale, time, weather_coverage); + if (density <= 0.01f || py < -0.10f) { + glColor4f(1.0f, 1.0f, 1.0f, 0.0f); + } else { + const float height_fraction = clamp01((py + 0.12f) / 0.88f); + const float transmittance = std::exp(-density * (1.45f + g_rain * 0.55f)); + const float powder = cloud_powder(density, cos_angle); + const float lit = (0.38f + hg * powder * (0.85f - g_rain * 0.28f)) * (0.72f + height_fraction * 0.35f); + float cr = (0.55f + top[0] * 0.48f) * lit; + float cg = (0.58f + top[1] * 0.44f) * lit; + float cb = (0.62f + top[2] * 0.40f) * lit; + cr = cr * (1.0f - g_rain * 0.24f) + 0.34f * g_rain; + cg = cg * (1.0f - g_rain * 0.22f) + 0.36f * g_rain; + cb = cb * (1.0f - g_rain * 0.18f) + 0.40f * g_rain; + tone_map_rgb(cr, cg, cb, 1.18f + dawn_dusk_factor() * 0.22f); + const float alpha = clamp01((1.0f - transmittance) * alpha_scale); + glColor4f(cr, cg, cb, alpha); + } + glVertex2f(px, layer_y + py * 0.34f + row * 0.004f); + } + } + glEnd(); + } + } + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glEnable(GL_FOG); + glEnable(GL_DEPTH_TEST); + glPopMatrix(); + glMatrixMode(GL_PROJECTION); + glPopMatrix(); + glMatrixMode(GL_MODELVIEW); +} + +static void draw_skybox() { + float top[3]; + float horizon[3]; + float light = 1.0f; + sky_colors(top, horizon, &light); + + glMatrixMode(GL_MODELVIEW); + glPushMatrix(); + glLoadIdentity(); + glRotatef(g_camera_pitch * 57.2957795f, 1, 0, 0); + glRotatef(g_camera_yaw * 57.2957795f, 0, 1, 0); + + glDisable(GL_DEPTH_TEST); + glDisable(GL_TEXTURE_2D); + glDisable(GL_CULL_FACE); + glDisable(GL_FOG); + glDepthMask(GL_FALSE); + + const float radius = 135.0f; + const int rings = 8; + const int segments = 64; + for (int ring = 0; ring < rings; ring++) { + const float v0 = static_cast(ring) / static_cast(rings); + const float v1 = static_cast(ring + 1) / static_cast(rings); + const float e0 = 0.04f + v0 * 1.18f; + const float e1 = 0.04f + v1 * 1.18f; + const float y0 = std::sin(e0) * radius - 12.0f; + const float y1 = std::sin(e1) * radius - 12.0f; + const float r0 = std::cos(e0) * radius; + const float r1 = std::cos(e1) * radius; + const float c0 = smoothstep(0.0f, 1.0f, v0); + const float c1 = smoothstep(0.0f, 1.0f, v1); + + glBegin(GL_QUAD_STRIP); + for (int seg = 0; seg <= segments; seg++) { + const float a = static_cast(seg) / static_cast(segments) * 6.2831853f; + const float ca = std::cos(a); + const float sa = std::sin(a); + glColor3f( + horizon[0] * (1.0f - c0) + top[0] * c0, + horizon[1] * (1.0f - c0) + top[1] * c0, + horizon[2] * (1.0f - c0) + top[2] * c0); + glVertex3f(ca * r0, y0, sa * r0); + glColor3f( + horizon[0] * (1.0f - c1) + top[0] * c1, + horizon[1] * (1.0f - c1) + top[1] * c1, + horizon[2] * (1.0f - c1) + top[2] * c1); + glVertex3f(ca * r1, y1, sa * r1); + } + glEnd(); + } + + glDepthMask(GL_TRUE); + glEnable(GL_FOG); + glEnable(GL_CULL_FACE); + glEnable(GL_DEPTH_TEST); + glPopMatrix(); +} + +static void draw_water() { + const double t = now_seconds() - g_start_time; + float top[3]; + float horizon[3]; + float light = 1.0f; + sky_colors(top, horizon, &light); + float sun[3]; + sun_direction(sun); + const float day_spec = smoothstep(-0.05f, 0.35f, sun_height()) * (1.0f - g_rain * 0.70f); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDisable(GL_CULL_FACE); + + const int half = 68; + const float step = 1.55f; + for (int z = -half; z < half; z++) { + glBegin(GL_TRIANGLE_STRIP); + for (int x = -half; x <= half; x++) { + for (int row = 0; row < 2; row++) { + const float wx = g_boat_x + x * step; + const float wz = g_boat_z + (z + row) * step; + const float y = wave_height(wx, wz, static_cast(t)); + float n[3]; + wave_normal(wx, wz, static_cast(t), n); + float view[3] = {g_boat_x - wx, 5.0f - y, g_boat_z + g_camera_distance - wz}; + normalize3(view); + const float diffuse = std::max(0.0f, n[0] * sun[0] + n[1] * sun[1] + n[2] * sun[2]); + const float n_dot_v = clamp01(n[0] * view[0] + n[1] * view[1] + n[2] * view[2]); + const float fresnel = fresnel_schlick(n_dot_v); + const float roughness = slope_roughness(n); + const float ao = slope_ambient_occlusion(n); + float halfway[3] = { + view[0] + sun[0], + view[1] + sun[1], + view[2] + sun[2], + }; + normalize3(halfway); + const float n_dot_h = std::max(0.0f, n[0] * halfway[0] + n[1] * halfway[1] + n[2] * halfway[2]); + const float spec = ggx_distribution(n_dot_h, roughness) * day_spec * (0.08f + fresnel * 0.55f); + const float dist = std::sqrt((wx - g_boat_x) * (wx - g_boat_x) + (wz - g_boat_z) * (wz - g_boat_z)); + const float fog = exp_fog_factor(dist * 0.045f, 0.50f + g_rain * 0.22f); + const float radial = std::sqrt(static_cast(x * x + (z + row) * (z + row))) / static_cast(half); + const float fade = 1.0f - smoothstep(0.74f, 1.0f, radial); + const float depth_tint = clamp01(0.58f + y * 0.45f); + const float shallow = std::max(0.0f, n[1] - 0.82f) * (1.0f - g_rain * 0.35f); + const float sky_reflect = fresnel * (0.42f + n[1] * 0.18f); + const float body_r = 0.004f + 0.012f * shallow; + const float body_g = 0.075f + 0.055f * shallow + 0.025f * diffuse; + const float body_b = 0.145f + 0.120f * shallow + 0.050f * diffuse; + const float deep_r = body_r * (0.68f + depth_tint * 0.20f); + const float deep_g = body_g * (0.74f + depth_tint * 0.22f); + const float deep_b = body_b * (0.88f + depth_tint * 0.18f); + float r = deep_r + horizon[0] * sky_reflect * 0.45f + spec * 0.68f; + float g = deep_g + horizon[1] * sky_reflect * 0.42f + spec * 0.76f; + float b = deep_b + horizon[2] * sky_reflect * 0.38f + spec * 0.92f; + r *= 0.70f + ao * 0.30f; + g *= 0.72f + ao * 0.28f; + b *= 0.76f + ao * 0.24f; + r = r * (1.0f - g_rain * 0.24f) + 0.030f * g_rain; + g = g * (1.0f - g_rain * 0.20f) + 0.070f * g_rain; + b = b * (1.0f - g_rain * 0.16f) + 0.105f * g_rain; + const float lantern = night_factor() * (1.0f - g_rain * 0.35f); + if (lantern > 0.02f) { + const float lx = wx - g_boat_x; + const float lz = wz - g_boat_z; + const float ldist2 = lx * lx + lz * lz + 2.0f; + const float point = lantern * std::min(1.0f, 16.0f / ldist2) * std::max(0.0f, n[1]); + r += point * 1.45f; + g += point * 0.84f; + b += point * 0.36f; + } + r = horizon[0] * (1.0f - fog) + r * fog; + g = horizon[1] * (1.0f - fog) + g * fog; + b = horizon[2] * (1.0f - fog) + b * fog; + tone_map_rgb(r, g, b, 1.25f + dawn_dusk_factor() * 0.35f); + glColor4f(r * light, g * light, b * light, 0.92f * fade); + glVertex3f(wx, y, wz); + } + } + glEnd(); + } + + glColor4f(0.56f, 0.78f, 0.88f, (0.18f + g_boat_speed * 0.018f) * (1.0f - g_rain * 0.62f)); + glLineWidth(1.0f); + glBegin(GL_LINES); + for (int i = -30; i <= 30; i += 2) { + const float wx = g_boat_x + i * 2.2f; + const float wz = g_boat_z - 12.0f + std::sin(i * 0.7f + static_cast(t)) * 7.0f; + const float y = wave_height(wx, wz, static_cast(t)) + 0.025f; + glVertex3f(wx - 0.7f, y, wz); + glVertex3f(wx + 1.1f, y + 0.01f, wz + 0.25f); + } + glEnd(); + glEnable(GL_CULL_FACE); +} + +static void draw_marker_geometry(const OceanMarker &marker) { + const float t = static_cast(now_seconds() - g_start_time); + const float y = wave_height(marker.x, marker.z, t); + glPushMatrix(); + glTranslatef(marker.x, y, marker.z); + glScalef(marker.size, marker.size, marker.size); + glDisable(GL_TEXTURE_2D); + + if (marker.type == 3) { + glTranslatef(0.0f, -0.18f, 0.0f); + for (int tier = 0; tier < 4; tier++) { + const float h0 = tier * 0.38f; + const float h1 = h0 + 0.42f + tier * 0.12f; + const float r0 = 2.35f - tier * 0.38f; + const float r1 = 1.62f - tier * 0.28f; + glColor3f(0.28f + tier * 0.035f, 0.23f + tier * 0.025f, 0.20f + tier * 0.018f); + glBegin(GL_QUAD_STRIP); + for (int i = 0; i <= 18; i++) { + const float a = static_cast(i) / 18.0f * 6.2831853f; + const float wobble = 1.0f + std::sin(a * 3.0f + marker.x * 0.01f) * 0.13f; + glVertex3f(std::cos(a) * r0 * wobble, h0, std::sin(a) * r0 * (1.0f - tier * 0.04f)); + glVertex3f(std::cos(a) * r1 * wobble, h1, std::sin(a) * r1 * (1.0f - tier * 0.04f)); + } + glEnd(); + } + glColor3f(0.16f, 0.38f, 0.17f); + glBegin(GL_TRIANGLE_FAN); + glVertex3f(0.0f, 1.85f, 0.0f); + for (int i = 0; i <= 24; i++) { + const float a = static_cast(i) / 24.0f * 6.2831853f; + glVertex3f(std::cos(a) * 1.35f, 1.52f + std::sin(i * 0.9f) * 0.05f, std::sin(a) * 1.15f); + } + glEnd(); + for (int tree = 0; tree < 5; tree++) { + const float a = tree * 1.37f + marker.z * 0.01f; + const float tx = std::cos(a) * (0.45f + 0.18f * tree); + const float tz = std::sin(a) * (0.38f + 0.16f * tree); + glColor3f(0.18f, 0.10f, 0.055f); + glBegin(GL_QUADS); + glVertex3f(tx - 0.035f, 1.45f, tz); + glVertex3f(tx + 0.035f, 1.45f, tz); + glVertex3f(tx + 0.035f, 1.92f, tz); + glVertex3f(tx - 0.035f, 1.92f, tz); + glEnd(); + glColor3f(0.20f, 0.46f + tree * 0.018f, 0.20f); + glBegin(GL_TRIANGLE_FAN); + glVertex3f(tx, 2.10f, tz); + for (int i = 0; i <= 14; i++) { + const float ca = static_cast(i) / 14.0f * 6.2831853f; + glVertex3f(tx + std::cos(ca) * 0.18f, 1.88f + std::sin(ca) * 0.12f, tz + std::sin(ca) * 0.18f); + } + glEnd(); + } + glPopMatrix(); + return; + } + + if (marker.type == 4) { + glRotatef(-18.0f + std::fmod(marker.x + marker.z, 36.0f), 0, 1, 0); + glColor3f(0.10f, 0.065f, 0.045f); + glBegin(GL_QUADS); + glVertex3f(-1.70f, 0.05f, -0.55f); + glVertex3f(1.70f, 0.05f, -0.55f); + glVertex3f(1.15f, -0.35f, 0.72f); + glVertex3f(-1.15f, -0.35f, 0.72f); + glVertex3f(-1.70f, 0.05f, -0.55f); + glVertex3f(-1.15f, -0.35f, 0.72f); + glVertex3f(-0.58f, -0.52f, 0.45f); + glVertex3f(-1.05f, -0.12f, -0.46f); + glVertex3f(1.70f, 0.05f, -0.55f); + glVertex3f(1.05f, -0.12f, -0.46f); + glVertex3f(0.58f, -0.52f, 0.45f); + glVertex3f(1.15f, -0.35f, 0.72f); + glEnd(); + glColor3f(0.58f, 0.45f, 0.29f); + for (int mast = -1; mast <= 1; mast++) { + const float mx = mast * 0.72f; + glBegin(GL_QUADS); + glVertex3f(mx - 0.025f, -0.05f, -0.05f); + glVertex3f(mx + 0.025f, -0.05f, -0.05f); + glVertex3f(mx + 0.025f, 1.95f + (mast == 0 ? 0.35f : 0.0f), -0.05f); + glVertex3f(mx - 0.025f, 1.95f + (mast == 0 ? 0.35f : 0.0f), -0.05f); + glEnd(); + glColor3f(0.77f, 0.72f, 0.62f); + glBegin(GL_TRIANGLES); + glVertex3f(mx + 0.05f, 1.65f, -0.04f); + glVertex3f(mx + 0.05f, 0.45f, -0.04f); + glVertex3f(mx + 0.58f, 0.68f, -0.04f); + glEnd(); + glColor3f(0.58f, 0.45f, 0.29f); + } + glColor3f(0.03f, 0.025f, 0.02f); + glBegin(GL_LINES); + for (int mast = -1; mast <= 1; mast++) { + const float mx = mast * 0.72f; + glVertex3f(mx, 1.9f, -0.05f); + glVertex3f(-1.55f, 0.10f, -0.55f); + glVertex3f(mx, 1.9f, -0.05f); + glVertex3f(1.55f, 0.10f, -0.55f); + } + glEnd(); + glPopMatrix(); + return; + } + + if (marker.type == 2) { + glColor3f(0.18f, 0.17f, 0.15f); + glBegin(GL_TRIANGLES); + glVertex3f(-1.1f, 0.0f, -0.8f); + glVertex3f(1.0f, 0.0f, -0.7f); + glVertex3f(0.1f, 0.9f, -0.2f); + glVertex3f(1.0f, 0.0f, -0.7f); + glVertex3f(0.8f, 0.0f, 1.0f); + glVertex3f(0.1f, 0.9f, -0.2f); + glVertex3f(0.8f, 0.0f, 1.0f); + glVertex3f(-0.9f, 0.0f, 0.8f); + glVertex3f(0.1f, 0.9f, -0.2f); + glVertex3f(-0.9f, 0.0f, 0.8f); + glVertex3f(-1.1f, 0.0f, -0.8f); + glVertex3f(0.1f, 0.9f, -0.2f); + glEnd(); + glPopMatrix(); + return; + } + + glColor3f(marker.type == 1 ? 0.95f : 0.82f, marker.type == 1 ? 0.78f : 0.08f, marker.type == 1 ? 0.18f : 0.06f); + glBegin(GL_QUADS); + glVertex3f(-0.35f, 0.05f, -0.35f); + glVertex3f(0.35f, 0.05f, -0.35f); + glVertex3f(0.35f, 0.65f, -0.35f); + glVertex3f(-0.35f, 0.65f, -0.35f); + glVertex3f(0.35f, 0.05f, 0.35f); + glVertex3f(-0.35f, 0.05f, 0.35f); + glVertex3f(-0.35f, 0.65f, 0.35f); + glVertex3f(0.35f, 0.65f, 0.35f); + glVertex3f(-0.35f, 0.05f, 0.35f); + glVertex3f(-0.35f, 0.05f, -0.35f); + glVertex3f(-0.35f, 0.65f, -0.35f); + glVertex3f(-0.35f, 0.65f, 0.35f); + glVertex3f(0.35f, 0.05f, -0.35f); + glVertex3f(0.35f, 0.05f, 0.35f); + glVertex3f(0.35f, 0.65f, 0.35f); + glVertex3f(0.35f, 0.65f, -0.35f); + glEnd(); + + glColor3f(0.88f, 0.88f, 0.78f); + glBegin(GL_QUADS); + glVertex3f(-0.07f, 0.65f, -0.07f); + glVertex3f(0.07f, 0.65f, -0.07f); + glVertex3f(0.07f, 1.45f, -0.07f); + glVertex3f(-0.07f, 1.45f, -0.07f); + glVertex3f(0.07f, 0.65f, 0.07f); + glVertex3f(-0.07f, 0.65f, 0.07f); + glVertex3f(-0.07f, 1.45f, 0.07f); + glVertex3f(0.07f, 1.45f, 0.07f); + glEnd(); + + if (marker.type == 1) { + const float glow = 0.35f + night_factor() * 0.65f; + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE); + glColor4f(1.0f, 0.70f, 0.20f, 0.45f * glow); + glBegin(GL_TRIANGLE_FAN); + glVertex3f(0.0f, 1.55f, 0.0f); + for (int i = 0; i <= 24; i++) { + const float a = static_cast(i) / 24.0f * 6.2831853f; + glVertex3f(std::cos(a) * 0.55f, 1.55f + std::sin(a) * 0.55f, 0.0f); + } + glEnd(); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } + + glPopMatrix(); +} + +static void draw_world_markers() { + glDisable(GL_CULL_FACE); + for (const auto &chunk : g_marker_chunks) { + for (const OceanMarker &marker : chunk.second) { + const float dx = marker.x - g_boat_x; + const float dz = marker.z - g_boat_z; + if (dx * dx + dz * dz <= 430.0f * 430.0f) { + draw_marker_geometry(marker); + } + } + } + glEnable(GL_CULL_FACE); +} + +static void draw_boat() { + const double t = now_seconds() - g_start_time; + const float bob = wave_height(g_boat_x, g_boat_z, static_cast(t)) + 0.20f; + glPushMatrix(); + glTranslatef(g_boat_x, bob, g_boat_z); + glRotatef(g_boat_yaw * 57.2957795f, 0, 1, 0); + glRotatef(std::sin(static_cast(t) * 2.2f + g_boat_x) * 2.2f, 0, 0, 1); + + glDisable(GL_TEXTURE_2D); + glColor3f(0.30f, 0.13f, 0.055f); + glBegin(GL_QUADS); + glVertex3f(-1.25f, 0.10f, -1.80f); + glVertex3f(1.25f, 0.10f, -1.80f); + glVertex3f(0.82f, 0.02f, 1.85f); + glVertex3f(-0.82f, 0.02f, 1.85f); + glVertex3f(-0.82f, 0.02f, 1.85f); + glVertex3f(0.82f, 0.02f, 1.85f); + glVertex3f(0.44f, -0.45f, 1.35f); + glVertex3f(-0.44f, -0.45f, 1.35f); + glVertex3f(-1.25f, 0.10f, -1.80f); + glVertex3f(-0.82f, 0.02f, 1.85f); + glVertex3f(-0.44f, -0.45f, 1.35f); + glVertex3f(-0.72f, -0.36f, -1.40f); + glVertex3f(1.25f, 0.10f, -1.80f); + glVertex3f(0.72f, -0.36f, -1.40f); + glVertex3f(0.44f, -0.45f, 1.35f); + glVertex3f(0.82f, 0.02f, 1.85f); + glEnd(); + + glColor3f(0.72f, 0.58f, 0.38f); + glBegin(GL_QUADS); + glVertex3f(-0.08f, 0.00f, -0.45f); + glVertex3f(0.08f, 0.00f, -0.45f); + glVertex3f(0.08f, 1.85f, -0.45f); + glVertex3f(-0.08f, 1.85f, -0.45f); + glEnd(); + + glColor3f(0.88f, 0.86f, 0.76f); + glBegin(GL_TRIANGLES); + glVertex3f(0.10f, 1.70f, -0.45f); + glVertex3f(0.10f, 0.28f, -0.45f); + glVertex3f(1.25f, 0.34f, -0.45f); + glEnd(); + + const float lantern = night_factor() * (1.0f - g_rain * 0.25f); + if (lantern > 0.02f) { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE); + glDisable(GL_CULL_FACE); + glColor4f(1.0f, 0.62f, 0.24f, 0.95f * lantern); + glBegin(GL_QUADS); + glVertex3f(-0.18f, 0.75f, 0.24f); + glVertex3f(0.18f, 0.75f, 0.24f); + glVertex3f(0.18f, 1.12f, 0.24f); + glVertex3f(-0.18f, 1.12f, 0.24f); + glEnd(); + glColor4f(1.0f, 0.54f, 0.18f, 0.20f * lantern); + glBegin(GL_TRIANGLE_FAN); + glVertex3f(0.0f, 0.94f, 0.26f); + for (int i = 0; i <= 32; i++) { + const float a = static_cast(i) / 32.0f * 6.2831853f; + glVertex3f(std::cos(a) * 1.1f, 0.94f + std::sin(a) * 0.65f, 0.28f); + } + glEnd(); + glEnable(GL_CULL_FACE); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } + + glPopMatrix(); +} + +static void draw_rain() { + if (g_rain <= 0.02f) { + return; + } + const float t = static_cast(now_seconds() - g_start_time); + glDisable(GL_TEXTURE_2D); + glDisable(GL_CULL_FACE); + glColor4f(0.66f, 0.78f, 0.86f, 0.42f * g_rain); + glLineWidth(1.0f); + glBegin(GL_LINES); + for (int i = 0; i < 150; i++) { + const float seed = static_cast(i) * 12.9898f; + const float rx = g_boat_x + std::fmod(std::sin(seed) * 43758.5f, 64.0f) - 32.0f; + const float rz = g_boat_z + std::fmod(std::cos(seed * 1.31f) * 21758.5f, 64.0f) - 32.0f; + const float fall = std::fmod(t * 19.0f + i * 0.37f, 9.0f); + glVertex3f(rx, 8.0f - fall, rz); + glVertex3f(rx + 0.26f, 6.8f - fall, rz - 0.36f); + } + glEnd(); + glEnable(GL_CULL_FACE); +} + +static void draw_screen_disc(float x, float y, float radius_x, float radius_y, float r, float g, float b, float alpha) { + if (alpha <= 0.001f) { + return; + } + glBegin(GL_TRIANGLE_FAN); + glColor4f(r, g, b, alpha); + glVertex2f(x, y); + glColor4f(r, g, b, 0.0f); + for (int i = 0; i <= 48; i++) { + const float a = static_cast(i) / 48.0f * 6.2831853f; + glVertex2f(x + std::cos(a) * radius_x, y + std::sin(a) * radius_y); + } + glEnd(); +} + +static void draw_bloom_overlay() { + glMatrixMode(GL_PROJECTION); + glPushMatrix(); + glLoadIdentity(); + glOrtho(-1, 1, -1, 1, -1, 1); + glMatrixMode(GL_MODELVIEW); + glPushMatrix(); + glLoadIdentity(); + + glDisable(GL_DEPTH_TEST); + glDisable(GL_TEXTURE_2D); + glDisable(GL_FOG); + glDisable(GL_CULL_FACE); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE); + + const float sun_angle = (g_day_time - 0.25f) * 6.2831853f; + const float sx = std::cos(sun_angle) * 0.68f; + const float sy = std::sin(sun_angle) * 0.74f; + const float body_y = sy < -0.05f ? -sy : sy; + const float daylight = smoothstep(-0.05f, 0.28f, sun_height()) * (1.0f - g_rain * 0.55f); + draw_screen_disc(sx, body_y, 0.24f, 0.24f, 1.0f, 0.68f, 0.28f, 0.17f * daylight); + draw_screen_disc(sx, body_y, 0.48f, 0.34f, 1.0f, 0.58f, 0.22f, 0.07f * daylight); + + const float golden = dawn_dusk_factor() * (1.0f - g_rain * 0.45f); + draw_screen_disc(0.0f, -0.42f, 1.25f, 0.28f, 1.0f, 0.48f, 0.18f, 0.11f * golden); + + const float lantern = night_factor() * (1.0f - g_rain * 0.20f); + draw_screen_disc(0.0f, -0.18f, 0.28f, 0.18f, 1.0f, 0.48f, 0.12f, 0.12f * lantern); + + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_CULL_FACE); + glEnable(GL_FOG); + glEnable(GL_DEPTH_TEST); + + glPopMatrix(); + glMatrixMode(GL_PROJECTION); + glPopMatrix(); + glMatrixMode(GL_MODELVIEW); +} + +Bool php_ocean_init(String title, Int width, Int height) { + SetProcessDPIAware(); + SetConsoleOutputCP(65001); + g_width = static_cast(width); + g_height = static_cast(height); + g_start_time = now_seconds(); + + WNDCLASS wc{}; + wc.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = wnd_proc; + wc.hInstance = GetModuleHandle(nullptr); + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + wc.lpszClassName = "TypePhpOceanWindow"; + RegisterClass(&wc); + + g_hwnd = CreateWindowEx( + 0, wc.lpszClassName, title.data(), + WS_OVERLAPPEDWINDOW | WS_VISIBLE, + CW_USEDEFAULT, CW_USEDEFAULT, g_width, g_height, + nullptr, nullptr, wc.hInstance, nullptr); + if (!g_hwnd) { + return false; + } + + g_hdc = GetDC(g_hwnd); + PIXELFORMATDESCRIPTOR pfd{}; + pfd.nSize = sizeof(pfd); + pfd.nVersion = 1; + pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; + pfd.iPixelType = PFD_TYPE_RGBA; + pfd.cColorBits = 32; + pfd.cDepthBits = 24; + pfd.cAlphaBits = 8; + pfd.iLayerType = PFD_MAIN_PLANE; + const int pf = ChoosePixelFormat(g_hdc, &pfd); + SetPixelFormat(g_hdc, pf, &pfd); + + g_glrc = wglCreateContext(g_hdc); + wglMakeCurrent(g_hdc, g_glrc); + glEnable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_CULL_FACE); + glClearColor(0.42f, 0.67f, 0.88f, 1.0f); + enable_mouse_capture(); + return true; +} + +void php_ocean_shutdown() { + disable_mouse_capture(); + g_marker_chunks.clear(); + g_pending_markers.clear(); + if (g_glrc) { + wglMakeCurrent(nullptr, nullptr); + wglDeleteContext(g_glrc); + g_glrc = nullptr; + } + if (g_hwnd && g_hdc) { + ReleaseDC(g_hwnd, g_hdc); + g_hdc = nullptr; + } + if (g_hwnd) { + DestroyWindow(g_hwnd); + g_hwnd = nullptr; + } +} + +Bool php_ocean_should_close() { + return g_should_close; +} + +void php_ocean_poll_events() { + MSG msg; + while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } +} + +Bool php_ocean_key_pressed(Int key) { + const int k = static_cast(key); + return k >= 0 && k < 256 && g_keys[k]; +} + +Float php_ocean_mouse_delta_x() { + const double value = g_mouse_dx; + g_mouse_dx = 0.0; + return value; +} + +Float php_ocean_mouse_delta_y() { + const double value = g_mouse_dy; + g_mouse_dy = 0.0; + return value; +} + +Float php_ocean_get_time() { + return now_seconds(); +} + +void php_ocean_sleep(Int milliseconds) { + Sleep(static_cast(std::max(0, static_cast(milliseconds)))); +} + +Bool php_ocean_confirm_exit() { + disable_mouse_capture(); + const int result = MessageBoxW( + g_hwnd, + L"\x662f\x5426\x9000\x51fa\x6d77\x6d0b\x573a\x666f\xff1f", + L"\x786e\x8ba4\x9000\x51fa", + MB_ICONQUESTION | MB_YESNO | MB_DEFBUTTON2); + enable_mouse_capture(); + g_mouse_dx = 0.0; + g_mouse_dy = 0.0; + return result == IDYES; +} + +void php_ocean_set_boat(Float x, Float z, Float yaw, Float speed) { + g_boat_x = static_cast(x); + g_boat_z = static_cast(z); + g_boat_yaw = static_cast(yaw); + g_boat_speed = static_cast(speed); +} + +void php_ocean_set_camera(Float yaw, Float pitch, Float distance) { + g_camera_yaw = static_cast(yaw); + g_camera_pitch = static_cast(pitch); + g_camera_distance = std::max(8.0f, std::min(42.0f, static_cast(distance))); +} + +void php_ocean_set_environment(Float day_time, Int weather, Float weather_mix, Float rain_amount) { + g_day_time = clamp01(static_cast(day_time)); + g_weather = std::max(0, std::min(2, static_cast(weather))); + g_weather_mix = clamp01(static_cast(weather_mix)); + g_rain = clamp01(static_cast(rain_amount)); +} + +void php_ocean_begin_chunk(Int chunk_x, Int chunk_z) { + g_pending_chunk = ChunkKey{static_cast(chunk_x), static_cast(chunk_z)}; + g_pending_markers.clear(); + g_has_pending_chunk = true; +} + +void php_ocean_add_marker(Float x, Float z, Int type, Float size) { + if (!g_has_pending_chunk) { + return; + } + OceanMarker marker{}; + marker.x = static_cast(x); + marker.z = static_cast(z); + marker.type = std::max(0, std::min(2, static_cast(type))); + marker.size = std::max(0.35f, std::min(4.0f, static_cast(size))); + g_pending_markers.push_back(marker); +} + +void php_ocean_commit_chunk(Int chunk_x, Int chunk_z) { + const ChunkKey key{static_cast(chunk_x), static_cast(chunk_z)}; + if (!g_has_pending_chunk) { + return; + } + g_marker_chunks[key] = g_pending_markers; + g_pending_markers.clear(); + g_pending_chunk = key; + g_has_pending_chunk = false; +} + +void php_ocean_remove_chunk(Int chunk_x, Int chunk_z) { + const ChunkKey key{static_cast(chunk_x), static_cast(chunk_z)}; + g_marker_chunks.erase(key); +} + +void php_ocean_render_frame() { + SetWindowTextA(g_hwnd, "TypePHP Ocean Demo - OpenGL"); + glViewport(0, 0, g_width, std::max(1, g_height)); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + draw_sky(); + glClear(GL_DEPTH_BUFFER_BIT); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + set_perspective(62.0, static_cast(g_width) / std::max(1, g_height), 0.1, 260.0); + + draw_skybox(); + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + glRotatef(g_camera_pitch * 57.2957795f, 1, 0, 0); + glRotatef(g_camera_yaw * 57.2957795f, 0, 1, 0); + glTranslatef(-g_boat_x, -4.2f, -g_boat_z - g_camera_distance); + + const GLfloat fog_color[] = {0.38f + g_rain * 0.04f, 0.52f + g_rain * 0.02f, 0.61f + g_rain * 0.03f, 1.0f}; + glEnable(GL_FOG); + glFogfv(GL_FOG_COLOR, fog_color); + glFogi(GL_FOG_MODE, GL_EXP2); + glFogf(GL_FOG_DENSITY, 0.010f + g_rain * 0.006f + active_weather_strength() * 0.003f); + + draw_water(); + draw_world_markers(); + draw_boat(); + draw_rain(); + + glDisable(GL_FOG); + draw_bloom_overlay(); + SwapBuffers(g_hdc); +} diff --git a/examples/ocean-demo/main.php b/examples/ocean-demo/main.php new file mode 100644 index 00000000..65b712e1 --- /dev/null +++ b/examples/ocean-demo/main.php @@ -0,0 +1,287 @@ += 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(); +} diff --git a/examples/ocean-demo/php-src/ocean.stub.php b/examples/ocean-demo/php-src/ocean.stub.php new file mode 100644 index 00000000..d146644c --- /dev/null +++ b/examples/ocean-demo/php-src/ocean.stub.php @@ -0,0 +1,20 @@ + + +#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(php_ocean_island_count()) : 0; +} + +TYPEPHP_OCEAN_API double typephp_ocean_island_x(int index) +{ + return typephp_ocean_ensure_runtime() ? static_cast(php_ocean_island_x(index)) : 0.0; +} + +TYPEPHP_OCEAN_API double typephp_ocean_island_z(int index) +{ + return typephp_ocean_ensure_runtime() ? static_cast(php_ocean_island_z(index)) : 0.0; +} + +TYPEPHP_OCEAN_API double typephp_ocean_island_radius(int index) +{ + return typephp_ocean_ensure_runtime() ? static_cast(php_ocean_island_radius(index)) : 0.0; +} + +TYPEPHP_OCEAN_API double typephp_ocean_island_height(int index) +{ + return typephp_ocean_ensure_runtime() ? static_cast(php_ocean_island_height(index)) : 0.0; +} + +TYPEPHP_OCEAN_API double typephp_ocean_island_seed(int index) +{ + return typephp_ocean_ensure_runtime() ? static_cast(php_ocean_island_seed(index)) : 0.0; +} + +TYPEPHP_OCEAN_API int typephp_ocean_marker_count() +{ + return typephp_ocean_ensure_runtime() ? static_cast(php_ocean_marker_count()) : 0; +} + +TYPEPHP_OCEAN_API double typephp_ocean_marker_x(int index) +{ + return typephp_ocean_ensure_runtime() ? static_cast(php_ocean_marker_x(index)) : 0.0; +} + +TYPEPHP_OCEAN_API double typephp_ocean_marker_z(int index) +{ + return typephp_ocean_ensure_runtime() ? static_cast(php_ocean_marker_z(index)) : 0.0; +} + +TYPEPHP_OCEAN_API int typephp_ocean_marker_type(int index) +{ + return typephp_ocean_ensure_runtime() ? static_cast(php_ocean_marker_type(index)) : 0; +} + +TYPEPHP_OCEAN_API double typephp_ocean_marker_size(int index) +{ + return typephp_ocean_ensure_runtime() ? static_cast(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(php_ocean_choose_next_weather(current, roll)) : current; +} diff --git a/examples/ocean-godot/gdextension-src/CMakeLists.txt b/examples/ocean-godot/gdextension-src/CMakeLists.txt new file mode 100644 index 00000000..73337a73 --- /dev/null +++ b/examples/ocean-godot/gdextension-src/CMakeLists.txt @@ -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" +) diff --git a/examples/ocean-godot/gdextension-src/register_types.cpp b/examples/ocean-godot/gdextension-src/register_types.cpp new file mode 100644 index 00000000..dbdb1bd0 --- /dev/null +++ b/examples/ocean-godot/gdextension-src/register_types.cpp @@ -0,0 +1,33 @@ +#include "typephp_ocean_bridge.hpp" + +#include +#include + +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(); +} diff --git a/examples/ocean-godot/gdextension-src/typephp_ocean_bridge.cpp b/examples/ocean-godot/gdextension-src/typephp_ocean_bridge.cpp new file mode 100644 index 00000000..f5777f74 --- /dev/null +++ b/examples/ocean-godot/gdextension-src/typephp_ocean_bridge.cpp @@ -0,0 +1,141 @@ +#include "typephp_ocean_bridge.hpp" + +#include +#include + +#include + +#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(&__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(GetProcAddress(library, "typephp_ocean_init")); + island_count = reinterpret_cast(GetProcAddress(library, "typephp_ocean_island_count")); + island_x = reinterpret_cast(GetProcAddress(library, "typephp_ocean_island_x")); + island_z = reinterpret_cast(GetProcAddress(library, "typephp_ocean_island_z")); + island_radius = reinterpret_cast(GetProcAddress(library, "typephp_ocean_island_radius")); + island_height = reinterpret_cast(GetProcAddress(library, "typephp_ocean_island_height")); + island_seed = reinterpret_cast(GetProcAddress(library, "typephp_ocean_island_seed")); + marker_count = reinterpret_cast(GetProcAddress(library, "typephp_ocean_marker_count")); + marker_x = reinterpret_cast(GetProcAddress(library, "typephp_ocean_marker_x")); + marker_z = reinterpret_cast(GetProcAddress(library, "typephp_ocean_marker_z")); + marker_type = reinterpret_cast(GetProcAddress(library, "typephp_ocean_marker_type")); + marker_size = reinterpret_cast(GetProcAddress(library, "typephp_ocean_marker_size")); + choose_weather = reinterpret_cast(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 diff --git a/examples/ocean-godot/gdextension-src/typephp_ocean_bridge.hpp b/examples/ocean-godot/gdextension-src/typephp_ocean_bridge.hpp new file mode 100644 index 00000000..1ce657ba --- /dev/null +++ b/examples/ocean-godot/gdextension-src/typephp_ocean_bridge.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +#ifdef _WIN32 +#include +#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 diff --git a/examples/ocean-godot/php-src/ocean.php b/examples/ocean-godot/php-src/ocean.php new file mode 100644 index 00000000..00ab5e1e --- /dev/null +++ b/examples/ocean-godot/php-src/ocean.php @@ -0,0 +1,94 @@ + 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; +} diff --git a/examples/ocean-godot/project.godot b/examples/ocean-godot/project.godot new file mode 100644 index 00000000..664a1651 --- /dev/null +++ b/examples/ocean-godot/project.godot @@ -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 diff --git a/examples/ocean-godot/scenes/main.tscn b/examples/ocean-godot/scenes/main.tscn new file mode 100644 index 00000000..7c82782c --- /dev/null +++ b/examples/ocean-godot/scenes/main.tscn @@ -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") diff --git a/examples/ocean-godot/scripts/ocean_world.gd b/examples/ocean-godot/scripts/ocean_world.gd new file mode 100644 index 00000000..ec68d199 --- /dev/null +++ b/examples/ocean-godot/scripts/ocean_world.gd @@ -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) diff --git a/examples/ocean-godot/typephp.yml b/examples/ocean-godot/typephp.yml new file mode 100644 index 00000000..57f6a844 --- /dev/null +++ b/examples/ocean-godot/typephp.yml @@ -0,0 +1,7 @@ +name: typephp_ocean +mode: lib +version: 0.1.0 +cxx-std: c++17 +sources: + - php-src + - cpp-src diff --git a/examples/ocean-godot/typephp_ocean_bridge.gdextension b/examples/ocean-godot/typephp_ocean_bridge.gdextension new file mode 100644 index 00000000..7b5e44fc --- /dev/null +++ b/examples/ocean-godot/typephp_ocean_bridge.gdextension @@ -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"