diff --git a/.gitignore b/.gitignore
index 8e047dfe..1ece92da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -89,3 +89,4 @@ ingest/
# Local texture dumps / extracted art should never be committed
assets/textures/
+node_modules/
diff --git a/BUILD_INSTRUCTIONS.md b/BUILD_INSTRUCTIONS.md
index e6f74b19..bd1a09a7 100644
--- a/BUILD_INSTRUCTIONS.md
+++ b/BUILD_INSTRUCTIONS.md
@@ -1,146 +1,88 @@
-# Build Instructions
+# WoWee Build Instructions
-This project builds as a native C++ client for WoW 3.3.5a in online mode.
+This document provides platform-specific build instructions for WoWee.
-## 1. Install Dependencies
+---
-### Ubuntu / Debian
+## π§ Linux (Ubuntu / Debian)
+
+### Install Dependencies
```bash
sudo apt update
-sudo apt install -y \
- cmake build-essential pkg-config git \
- libsdl2-dev libglew-dev libglm-dev \
- libssl-dev zlib1g-dev \
- libavformat-dev libavcodec-dev libswscale-dev libavutil-dev \
- libstorm-dev
+sudo apt install -y build-essential cmake pkg-config git libsdl2-dev libglew-dev libglm-dev libssl-dev zlib1g-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libunicorn-dev libstorm-dev
```
-If `libstorm-dev` is unavailable in your distro repos, build StormLib from source:
+---
+
+## π§ Linux (Arch)
+
+### Install Dependencies
```bash
-cd /tmp
-git clone https://github.com/ladislav-zezula/StormLib.git
-cd StormLib
-mkdir build && cd build
-cmake ..
-make -j"$(nproc)"
-sudo make install
-sudo ldconfig
+sudo pacman -S --needed base-devel cmake pkgconf git sdl2 glew glm openssl zlib ffmpeg unicorn stormlib
```
-### Fedora
+---
+
+## π§ Linux (All Distros)
+
+### Clone Repository
+
+Always clone with submodules:
```bash
-sudo dnf install -y \
- cmake gcc-c++ make pkg-config git \
- SDL2-devel glew-devel glm-devel \
- openssl-devel zlib-devel \
- ffmpeg-devel \
- StormLib-devel
+git clone --recurse-submodules https://github.com/Kelsidavis/WoWee.git
+cd WoWee
```
-### Arch
+If you already cloned without submodules:
```bash
-sudo pacman -S --needed \
- cmake base-devel pkgconf git \
- sdl2 glew glm openssl zlib ffmpeg stormlib
+git submodule update --init --recursive
```
-## 2. Clone + Prepare
-
-```bash
-git clone https://github.com/Kelsidavis/WoWee.git
-cd wowee
-git clone https://github.com/ocornut/imgui.git extern/imgui
-```
-
-## 3. Configure + Build
+### Build
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
```
-Binary output:
+---
-```text
-build/bin/wowee
+## πͺ Windows (Visual Studio 2022)
+
+### Install
+
+- Visual Studio 2022
+- Desktop development with C++
+- CMake tools for Windows
+
+### Clone
+
+```powershell
+git clone --recurse-submodules https://github.com/Kelsidavis/WoWee.git
+cd WoWee
```
-## 4. Provide WoW Data (Extract + Manifest)
+### Build
-Wowee loads assets from an extracted loose-file tree indexed by `manifest.json` (it does not read MPQs at runtime).
+Open the folder in Visual Studio (it will detect CMake automatically)
+or build from Developer PowerShell:
-### Option A: Extract into `./Data/` (recommended)
-
-Run:
-
-```bash
-# WotLK 3.3.5a example
-./extract_assets.sh /path/to/WoW/Data wotlk
-```
-
-The output includes:
-
-```text
-Data/
- manifest.json
- interface/
- sound/
- world/
- expansions/
-```
-
-### Option B: Use an existing extracted data tree
-
-Point wowee at your extracted `Data/` directory:
-
-```bash
-export WOW_DATA_PATH=/path/to/extracted/Data
-```
-
-## 5. Run
-
-```bash
-./build/bin/wowee
-```
-
-## 6. Local AzerothCore (Optional)
-
-If you are using a local AzerothCore Docker stack, start it first and then connect from the client realm screen.
-
-See:
-
-- `docs/server-setup.md`
-
-## Troubleshooting
-
-### `StormLib` not found
-
-Install distro package or build from source (section 1).
-
-### `ImGui` missing
-
-Ensure `extern/imgui` exists:
-
-```bash
-git clone https://github.com/ocornut/imgui.git extern/imgui
-```
-
-### Data not found at runtime
-
-Verify `Data/manifest.json` exists (or re-run `./extract_assets.sh ...`), or set:
-
-```bash
-export WOW_DATA_PATH=/path/to/extracted/Data
-```
-
-### Clean rebuild
-
-```bash
-rm -rf build
+```powershell
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
-cmake --build build -j"$(nproc)"
+cmake --build build --config Release
```
+
+---
+
+## β οΈ Notes
+
+- Case matters on Linux (`WoWee` not `wowee`).
+- Always use `--recurse-submodules` when cloning.
+- If you encounter missing headers for ImGui, run:
+ ```bash
+ git submodule update --init --recursive
+ ```
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b6157ea2..ff956973 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -14,6 +14,7 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
# Options
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
option(WOWEE_BUILD_TESTS "Build tests" OFF)
+option(WOWEE_ENABLE_ASAN "Enable AddressSanitizer (Debug builds)" OFF)
# Opcode registry generation/validation
find_package(Python3 COMPONENTS Interpreter QUIET)
@@ -34,8 +35,13 @@ endif()
# Find required packages
find_package(SDL2 REQUIRED)
-find_package(OpenGL REQUIRED)
-find_package(GLEW REQUIRED)
+find_package(Vulkan REQUIRED)
+# GL/GLEW kept temporarily for unconverted sub-renderers during Vulkan migration.
+# These files compile against GL types but their code is never called β the Vulkan
+# path is the only active rendering backend. Remove in Phase 7 when all renderers
+# are converted and grep confirms zero GL references.
+find_package(OpenGL QUIET)
+find_package(GLEW QUIET)
find_package(OpenSSL REQUIRED)
find_package(Threads REQUIRED)
find_package(ZLIB REQUIRED)
@@ -45,7 +51,7 @@ else()
find_package(PkgConfig REQUIRED)
endif()
if(PkgConfig_FOUND)
- pkg_check_modules(FFMPEG REQUIRED libavformat libavcodec libswscale libavutil)
+ pkg_check_modules(FFMPEG libavformat libavcodec libswscale libavutil)
else()
# Fallback for MSVC/vcpkg β find FFmpeg libraries manually
find_path(FFMPEG_INCLUDE_DIRS libavformat/avformat.h)
@@ -54,9 +60,15 @@ else()
find_library(AVUTIL_LIB NAMES avutil)
find_library(SWSCALE_LIB NAMES swscale)
set(FFMPEG_LIBRARIES ${AVFORMAT_LIB} ${AVCODEC_LIB} ${AVUTIL_LIB} ${SWSCALE_LIB})
- if(NOT AVFORMAT_LIB)
- message(FATAL_ERROR "FFmpeg not found. On Windows install via MSYS2: mingw-w64-x86_64-ffmpeg")
- endif()
+endif()
+if(FFMPEG_INCLUDE_DIRS AND AVFORMAT_LIB)
+ set(HAVE_FFMPEG TRUE)
+ message(STATUS "Found FFmpeg: ${AVFORMAT_LIB}")
+elseif(FFMPEG_FOUND)
+ set(HAVE_FFMPEG TRUE)
+else()
+ set(HAVE_FFMPEG FALSE)
+ message(WARNING "FFmpeg not found β video_player will be disabled. Install via vcpkg: ffmpeg:x64-windows")
endif()
# Unicorn Engine (x86 emulator for cross-platform Warden module execution)
@@ -77,13 +89,68 @@ if(NOT glm_FOUND)
message(STATUS "GLM not found, will use system includes or download")
endif()
# GLM GTX extensions (quaternion, norm, etc.) require this flag on newer GLM versions
-add_compile_definitions(GLM_ENABLE_EXPERIMENTAL)
+add_compile_definitions(GLM_ENABLE_EXPERIMENTAL GLM_FORCE_DEPTH_ZERO_TO_ONE)
+if(WIN32)
+ add_compile_definitions(NOMINMAX _CRT_SECURE_NO_WARNINGS)
+endif()
+
+# SPIR-V shader compilation via glslc
+find_program(GLSLC glslc HINTS ${Vulkan_GLSLC_EXECUTABLE} "$ENV{VULKAN_SDK}/bin")
+if(GLSLC)
+ message(STATUS "Found glslc: ${GLSLC}")
+else()
+ message(WARNING "glslc not found. Install the Vulkan SDK or vulkan-tools package.")
+ message(WARNING "Shaders will not be compiled to SPIR-V.")
+endif()
+
+# Function to compile GLSL shaders to SPIR-V
+function(compile_shaders TARGET_NAME)
+ set(SHADER_DIR ${CMAKE_CURRENT_SOURCE_DIR}/assets/shaders)
+ set(SPV_DIR ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/assets/shaders)
+ file(MAKE_DIRECTORY ${SPV_DIR})
+
+ file(GLOB GLSL_SOURCES "${SHADER_DIR}/*.glsl")
+ set(SPV_OUTPUTS)
+
+ foreach(GLSL_FILE ${GLSL_SOURCES})
+ get_filename_component(FILE_NAME ${GLSL_FILE} NAME)
+ # e.g. skybox.vert.glsl -> skybox.vert.spv
+ string(REGEX REPLACE "\\.glsl$" ".spv" SPV_NAME ${FILE_NAME})
+ set(SPV_FILE ${SPV_DIR}/${SPV_NAME})
+
+ # Determine shader stage from filename
+ if(FILE_NAME MATCHES "\\.vert\\.glsl$")
+ set(SHADER_STAGE vertex)
+ elseif(FILE_NAME MATCHES "\\.frag\\.glsl$")
+ set(SHADER_STAGE fragment)
+ elseif(FILE_NAME MATCHES "\\.comp\\.glsl$")
+ set(SHADER_STAGE compute)
+ elseif(FILE_NAME MATCHES "\\.geom\\.glsl$")
+ set(SHADER_STAGE geometry)
+ else()
+ message(WARNING "Cannot determine shader stage for: ${FILE_NAME}")
+ continue()
+ endif()
+
+ add_custom_command(
+ OUTPUT ${SPV_FILE}
+ COMMAND ${GLSLC} -fshader-stage=${SHADER_STAGE} -O ${GLSL_FILE} -o ${SPV_FILE}
+ DEPENDS ${GLSL_FILE}
+ COMMENT "Compiling SPIR-V: ${FILE_NAME} -> ${SPV_NAME}"
+ VERBATIM
+ )
+ list(APPEND SPV_OUTPUTS ${SPV_FILE})
+ endforeach()
+
+ add_custom_target(${TARGET_NAME}_shaders ALL DEPENDS ${SPV_OUTPUTS})
+ add_dependencies(${TARGET_NAME} ${TARGET_NAME}_shaders)
+endfunction()
# StormLib for MPQ extraction tool (not needed for main executable)
find_library(STORMLIB_LIBRARY NAMES StormLib stormlib storm)
find_path(STORMLIB_INCLUDE_DIR StormLib.h PATH_SUFFIXES StormLib)
-# Include ImGui as a static library (we'll add the sources)
+# Include ImGui as a static library (Vulkan backend)
set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/extern/imgui)
if(EXISTS ${IMGUI_DIR})
add_library(imgui STATIC
@@ -93,19 +160,30 @@ if(EXISTS ${IMGUI_DIR})
${IMGUI_DIR}/imgui_widgets.cpp
${IMGUI_DIR}/imgui_demo.cpp
${IMGUI_DIR}/backends/imgui_impl_sdl2.cpp
- ${IMGUI_DIR}/backends/imgui_impl_opengl3.cpp
+ ${IMGUI_DIR}/backends/imgui_impl_vulkan.cpp
)
target_include_directories(imgui PUBLIC
${IMGUI_DIR}
${IMGUI_DIR}/backends
)
- target_link_libraries(imgui PUBLIC SDL2::SDL2 OpenGL::GL ${CMAKE_DL_LIBS})
- target_compile_definitions(imgui PUBLIC IMGUI_IMPL_OPENGL_LOADER_GLEW)
+ target_link_libraries(imgui PUBLIC SDL2::SDL2 Vulkan::Vulkan ${CMAKE_DL_LIBS})
else()
message(WARNING "ImGui not found in extern/imgui. Clone it with:")
message(WARNING " git clone https://github.com/ocornut/imgui.git extern/imgui")
endif()
+# vk-bootstrap (Vulkan device/instance setup)
+set(VK_BOOTSTRAP_DIR ${CMAKE_CURRENT_SOURCE_DIR}/extern/vk-bootstrap)
+if(EXISTS ${VK_BOOTSTRAP_DIR})
+ add_library(vk-bootstrap STATIC
+ ${VK_BOOTSTRAP_DIR}/src/VkBootstrap.cpp
+ )
+ target_include_directories(vk-bootstrap PUBLIC ${VK_BOOTSTRAP_DIR}/src)
+ target_link_libraries(vk-bootstrap PUBLIC Vulkan::Vulkan)
+else()
+ message(FATAL_ERROR "vk-bootstrap not found in extern/vk-bootstrap")
+endif()
+
# Source files
set(WOWEE_SOURCES
# Core
@@ -180,6 +258,15 @@ set(WOWEE_SOURCES
src/pipeline/terrain_mesh.cpp
+ # Rendering (Vulkan infrastructure)
+ src/rendering/vk_context.cpp
+ src/rendering/vk_utils.cpp
+ src/rendering/vk_shader.cpp
+ src/rendering/vk_texture.cpp
+ src/rendering/vk_buffer.cpp
+ src/rendering/vk_pipeline.cpp
+ src/rendering/vk_render_target.cpp
+
# Rendering
src/rendering/renderer.cpp
src/rendering/shader.cpp
@@ -215,7 +302,7 @@ set(WOWEE_SOURCES
src/rendering/levelup_effect.cpp
src/rendering/charge_effect.cpp
src/rendering/loading_screen.cpp
- src/rendering/video_player.cpp
+ $<$:${CMAKE_CURRENT_SOURCE_DIR}/src/rendering/video_player.cpp>
# UI
src/ui/ui_manager.cpp
@@ -287,6 +374,13 @@ set(WOWEE_HEADERS
include/pipeline/dbc_loader.hpp
include/pipeline/terrain_mesh.hpp
+ include/rendering/vk_context.hpp
+ include/rendering/vk_utils.hpp
+ include/rendering/vk_shader.hpp
+ include/rendering/vk_texture.hpp
+ include/rendering/vk_buffer.hpp
+ include/rendering/vk_pipeline.hpp
+ include/rendering/vk_render_target.hpp
include/rendering/renderer.hpp
include/rendering/shader.hpp
include/rendering/texture.hpp
@@ -343,19 +437,26 @@ if(TARGET opcodes-generate)
add_dependencies(wowee opcodes-generate)
endif()
+# Compile GLSL shaders to SPIR-V
+if(GLSLC)
+ compile_shaders(wowee)
+endif()
+
# Include directories
target_include_directories(wowee PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/extern
- ${FFMPEG_INCLUDE_DIRS}
+ ${CMAKE_CURRENT_SOURCE_DIR}/extern/vk-bootstrap/src
)
+if(HAVE_FFMPEG)
+ target_include_directories(wowee PRIVATE ${FFMPEG_INCLUDE_DIRS})
+endif()
# Link libraries
target_link_libraries(wowee PRIVATE
SDL2::SDL2
- OpenGL::GL
- GLEW::GLEW
+ Vulkan::Vulkan
OpenSSL::SSL
OpenSSL::Crypto
Threads::Threads
@@ -363,9 +464,20 @@ target_link_libraries(wowee PRIVATE
${CMAKE_DL_LIBS}
)
-target_link_libraries(wowee PRIVATE ${FFMPEG_LIBRARIES})
-if (FFMPEG_LIBRARY_DIRS)
- target_link_directories(wowee PRIVATE ${FFMPEG_LIBRARY_DIRS})
+# GL/GLEW linked temporarily for unconverted sub-renderers (removed in Phase 7)
+if(TARGET OpenGL::GL)
+ target_link_libraries(wowee PRIVATE OpenGL::GL)
+endif()
+if(TARGET GLEW::GLEW)
+ target_link_libraries(wowee PRIVATE GLEW::GLEW)
+endif()
+
+if(HAVE_FFMPEG)
+ target_compile_definitions(wowee PRIVATE HAVE_FFMPEG)
+ target_link_libraries(wowee PRIVATE ${FFMPEG_LIBRARIES})
+ if(FFMPEG_LIBRARY_DIRS)
+ target_link_directories(wowee PRIVATE ${FFMPEG_LIBRARY_DIRS})
+ endif()
endif()
# Platform-specific libraries
@@ -385,6 +497,11 @@ if(TARGET imgui)
target_link_libraries(wowee PRIVATE imgui)
endif()
+# Link vk-bootstrap
+if(TARGET vk-bootstrap)
+ target_link_libraries(wowee PRIVATE vk-bootstrap)
+endif()
+
# Link Unicorn if available
if(HAVE_UNICORN)
target_link_libraries(wowee PRIVATE ${UNICORN_LIBRARY})
@@ -406,6 +523,47 @@ else()
target_compile_options(wowee PRIVATE -Wall -Wextra -Wpedantic)
endif()
+# Debug build flags
+if(MSVC)
+ # /ZI β Edit-and-Continue debug info (works with hot-reload in VS 2022)
+ # /RTC1 β stack-frame and uninitialised-variable runtime checks (Debug only)
+ # /sdl β additional SDL security checks
+ # /Od β disable optimisation so stepping matches source lines exactly
+ target_compile_options(wowee PRIVATE
+ $<$:/ZI /RTC1 /sdl /Od>
+ )
+ # Ensure the linker emits a .pdb alongside the .exe for every config
+ target_link_options(wowee PRIVATE
+ $<$:/DEBUG:FULL>
+ $<$:/DEBUG:FASTLINK>
+ )
+else()
+ # -g3 β maximum DWARF debug info (includes macro definitions)
+ # -Og β optimise for debugging (better than -O0, keeps most frames)
+ # -fno-omit-frame-pointer β preserve frame pointers so stack traces are clean
+ target_compile_options(wowee PRIVATE
+ $<$:-g3 -Og -fno-omit-frame-pointer>
+ $<$:-g -fno-omit-frame-pointer>
+ )
+endif()
+
+# AddressSanitizer β catch buffer overflows, use-after-free, etc.
+# Enable with: cmake ... -DWOWEE_ENABLE_ASAN=ON -DCMAKE_BUILD_TYPE=Debug
+if(WOWEE_ENABLE_ASAN)
+ if(MSVC)
+ target_compile_options(wowee PRIVATE /fsanitize=address)
+ # ASAN on MSVC requires the dynamic CRT (/MD or /MDd)
+ target_compile_options(wowee PRIVATE
+ $<$:/MDd>
+ $<$:/MD>
+ )
+ else()
+ target_compile_options(wowee PRIVATE -fsanitize=address -fno-omit-frame-pointer)
+ target_link_options(wowee PRIVATE -fsanitize=address)
+ endif()
+ message(STATUS "AddressSanitizer: ENABLED")
+endif()
+
# Release build optimizations
include(CheckIPOSupported)
check_ipo_supported(RESULT _ipo_supported OUTPUT _ipo_error)
@@ -420,9 +578,27 @@ if(NOT MSVC)
target_compile_options(wowee PRIVATE $<$:-fvisibility=hidden -fvisibility-inlines-hidden>)
endif()
-# Copy assets to build directory
-file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/assets
- DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+# Copy assets next to the executable (runs every build, not just configure).
+# Uses $ so MSVC multi-config generators place assets
+# in bin/Debug/ or bin/Release/ alongside the exe, not the common bin/ parent.
+add_custom_command(TARGET wowee POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_directory
+ ${CMAKE_CURRENT_SOURCE_DIR}/assets
+ $/assets
+ COMMENT "Syncing assets to $/assets"
+)
+
+# On Windows, SDL 2.28+ uses LoadLibraryExW with LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
+# which does NOT include System32. Copy vulkan-1.dll into the output directory so
+# SDL_Vulkan_LoadLibrary can locate it without needing a full system PATH search.
+if(WIN32)
+ add_custom_command(TARGET wowee POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
+ "$ENV{SystemRoot}/System32/vulkan-1.dll"
+ "$/vulkan-1.dll"
+ COMMENT "Copying vulkan-1.dll to output directory"
+ )
+endif()
# Install targets
install(TARGETS wowee
@@ -482,6 +658,9 @@ if(STORMLIB_LIBRARY AND STORMLIB_INCLUDE_DIR)
ZLIB::ZLIB
Threads::Threads
)
+ if(WIN32)
+ target_link_libraries(asset_extract PRIVATE wininet bz2)
+ endif()
set_target_properties(asset_extract PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
@@ -575,6 +754,7 @@ message(STATUS " Build Type: ${CMAKE_BUILD_TYPE}")
message(STATUS " SDL2: ${SDL2_VERSION}")
message(STATUS " OpenSSL: ${OPENSSL_VERSION}")
message(STATUS " ImGui: ${IMGUI_DIR}")
+message(STATUS " ASAN: ${WOWEE_ENABLE_ASAN}")
message(STATUS "")
# ---- CPack packaging ----
@@ -628,7 +808,7 @@ chmod +x /usr/local/bin/wowee
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Wowee")
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
set(CPACK_DEBIAN_PACKAGE_DEPENDS
- "libsdl2-2.0-0, libglew2.2 | libglew2.1, libssl3, zlib1g")
+ "libsdl2-2.0-0, libvulkan1, libssl3, zlib1g")
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA
"${CMAKE_CURRENT_BINARY_DIR}/packaging/postinst;${CMAKE_CURRENT_BINARY_DIR}/packaging/prerm")
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
diff --git a/README.md b/README.md
index 43aa4de5..debe3d16 100644
--- a/README.md
+++ b/README.md
@@ -4,22 +4,23 @@
-A native C++ World of Warcraft client with a custom OpenGL renderer.
+A native C++ World of Warcraft client with a custom Vulkan renderer.
[](https://github.com/sponsors/Kelsidavis)
+[](https://discord.gg/SDqjA79B)
[](https://youtu.be/Pd9JuYYxu0o)
[](https://youtu.be/J4NXegzqWSQ)
-Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**. All three expansions are broadly functional with roughly even support.
+Protocol Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**.
> **Legal Disclaimer**: This is an educational/research project. It does not include any Blizzard Entertainment assets, data files, or proprietary code. World of Warcraft and all related assets are the property of Blizzard Entertainment, Inc. This project is not affiliated with or endorsed by Blizzard Entertainment. Users are responsible for supplying their own legally obtained game data files and for ensuring compliance with all applicable laws in their jurisdiction.
## Status & Direction (2026-02-18)
-- **Compatibility**: **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a** are all broadly supported via expansion profiles and per-expansion packet parsers (`src/game/packet_parsers_classic.cpp`, `src/game/packet_parsers_tbc.cpp`). All three expansions are roughly on par β no single one is significantly more complete than the others.
-- **Tested against**: AzerothCore, TrinityCore, and ChromieCraft.
+- **Compatibility**: **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a** are all supported via expansion profiles and per-expansion packet parsers (`src/game/packet_parsers_classic.cpp`, `src/game/packet_parsers_tbc.cpp`). All three expansions are roughly on par β no single one is significantly more complete than the others.
+- **Tested against**: AzerothCore, TrinityCore, and Mangos.
- **Current focus**: protocol correctness across server variants, visual accuracy (M2/WMO edge cases, equipment textures), and multi-expansion coverage.
- **Warden**: Full module execution via Unicorn Engine CPU emulation. Decrypts (RC4βRSAβzlib), parses and relocates the PE module, executes via x86 emulation with Windows API interception. Module cache at `~/.local/share/wowee/warden_cache/`.
@@ -27,18 +28,10 @@ Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**. All three
### Rendering Engine
- **Terrain** -- Multi-tile streaming with async loading, texture splatting (4 layers), frustum culling
-- **Water** -- Animated surfaces, reflections, refractions, Fresnel effect
-- **Sky System** -- WoW-accurate DBC-driven lighting with skybox authority
- - **Skybox** -- Camera-locked celestial sphere (M2 model support, gradient fallback)
- - **Celestial Bodies** -- Sun (lighting-driven), White Lady + Blue Child (Azeroth's two moons)
- - **Moon Phases** -- Game time-driven deterministic phases when server time is available (fallback: local cycling for development)
- - **Stars** -- Baked into skybox assets (procedural fallback for development/debug only)
- **Atmosphere** -- Procedural clouds (FBM noise), lens flare with chromatic aberration, cloud/fog star occlusion
-- **Weather** -- Rain and snow particle systems (2000 particles, camera-relative)
- **Characters** -- Skeletal animation with GPU vertex skinning (256 bones), race-aware textures
- **Buildings** -- WMO renderer with multi-material batches, frustum culling, 160-unit distance culling
- **Particles** -- M2 particle emitters with WotLK struct parsing, billboarded glow effects
-- **Post-Processing** -- HDR, tonemapping, shadow mapping (2048x2048)
### Asset Pipeline
- Extracted loose-file **`Data/`** tree indexed by **`manifest.json`** (fast lookup + caching)
@@ -71,21 +64,26 @@ Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**. All three
```bash
# Ubuntu/Debian
-sudo apt install libsdl2-dev libglew-dev libglm-dev \
- libssl-dev cmake build-essential \
- libunicorn-dev \ # for Warden module execution
- libstorm-dev # for asset_extract
+sudo apt install libsdl2-dev libglm-dev libssl-dev \
+ libvulkan-dev vulkan-tools glslc \
+ libavformat-dev libavcodec-dev libswscale-dev libavutil-dev \
+ zlib1g-dev cmake build-essential libx11-dev \
+ libunicorn-dev \ # optional: Warden module execution
+ libstorm-dev # optional: asset_extract tool
# Fedora
-sudo dnf install SDL2-devel glew-devel glm-devel \
- openssl-devel cmake gcc-c++ \
- unicorn-devel \ # for Warden module execution
- StormLib-devel # for asset_extract
+sudo dnf install SDL2-devel glm-devel openssl-devel \
+ vulkan-devel vulkan-tools glslc \
+ ffmpeg-devel zlib-devel cmake gcc-c++ libX11-devel \
+ unicorn-devel \ # optional: Warden module execution
+ StormLib-devel # optional: asset_extract tool
# Arch
-sudo pacman -S sdl2 glew glm openssl cmake base-devel \
- unicorn \ # for Warden module execution
- stormlib # for asset_extract
+sudo pacman -S sdl2 glm openssl \
+ vulkan-devel vulkan-tools shaderc \
+ ffmpeg zlib cmake base-devel libx11 \
+ unicorn # optional: Warden module execution
+ # StormLib: install from AUR for asset_extract tool
```
### Container build
@@ -208,15 +206,12 @@ make -j$(nproc)
## Technical Details
-- **Graphics**: OpenGL 3.3 Core, GLSL 330, forward rendering with post-processing
-- **Performance**: 60 FPS (vsync), ~50k triangles/frame, ~30 draw calls, <10% GPU
- **Platform**: Linux (primary), C++20, CMake 3.15+
-- **Dependencies**: SDL2, OpenGL/GLEW, GLM, OpenSSL, ImGui, FFmpeg, Unicorn Engine (StormLib for asset extraction tooling)
+- **Dependencies**: SDL2, Vulkan, GLM, OpenSSL, ImGui, FFmpeg, Unicorn Engine (StormLib for asset extraction tooling)
- **Architecture**: Modular design with clear separation (core, rendering, networking, game logic, asset pipeline, UI, audio)
- **Networking**: Non-blocking TCP, SRP6a authentication, RC4 encryption, WoW 3.3.5a protocol
- **Asset Loading**: Extracted loose-file tree + `manifest.json` indexing, async terrain streaming, overlay layers
- **Sky System**: WoW-accurate DBC-driven architecture
- - **Skybox Authority**: Stars baked into M2 sky dome models (not procedurally generated)
- **Lore-Accurate Moons**: White Lady (30-day cycle) + Blue Child (27-day cycle)
- **Deterministic Phases**: Computed from server game time when available (fallback: local time/dev cycling)
- **Camera-Locked**: Sky dome uses rotation-only transform (translation ignored)
@@ -238,8 +233,4 @@ This project does not include any Blizzard Entertainment proprietary data, asset
## Known Issues
-### Water Rendering
-- **Stormwind Canal Overflow**: Canal water surfaces extend spatially beyond their intended boundaries, causing water to appear in tunnels, buildings, and the park. This is due to oversized water mesh extents in the WoW data files.
- - **Current Workaround**: Water heights are lowered by 1 unit in Stormwind (tiles 28-50, 28-52) for surfaces above 94 units, with a 20-unit exclusion zone around the moonwell (-8755.9, 1108.9, 96.1). This hides most problem water while keeping canals and the moonwell functional.
- - **Limitation**: Some park water may still be visible. The workaround uses hardcoded coordinates and height thresholds rather than fixing the root cause.
- - **Proper Fix**: Would require trimming water surface meshes to actual boundaries in ADT/WMO data, or implementing spatial clipping at render time.
+MANY issues this is actively under development
diff --git a/assets/krayonload.png b/assets/krayonload.png
new file mode 100644
index 00000000..4d580744
Binary files /dev/null and b/assets/krayonload.png differ
diff --git a/assets/krayonsignin.png b/assets/krayonsignin.png
new file mode 100644
index 00000000..f0d3bfa4
Binary files /dev/null and b/assets/krayonsignin.png differ
diff --git a/assets/loading1.jpeg b/assets/loading1.jpeg
deleted file mode 100755
index 79cae0dc..00000000
Binary files a/assets/loading1.jpeg and /dev/null differ
diff --git a/assets/loading2.jpeg b/assets/loading2.jpeg
deleted file mode 100755
index 2403b28a..00000000
Binary files a/assets/loading2.jpeg and /dev/null differ
diff --git a/assets/shaders/basic.frag.glsl b/assets/shaders/basic.frag.glsl
new file mode 100644
index 00000000..f6a07030
--- /dev/null
+++ b/assets/shaders/basic.frag.glsl
@@ -0,0 +1,49 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(set = 1, binding = 0) uniform sampler2D uTexture;
+
+layout(set = 1, binding = 1) uniform BasicMaterial {
+ vec4 color;
+ vec3 lightPos;
+ int useTexture;
+};
+
+layout(location = 0) in vec3 FragPos;
+layout(location = 1) in vec3 Normal;
+layout(location = 2) in vec2 TexCoord;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ vec3 ambient = 0.3 * vec3(1.0);
+ vec3 norm = normalize(Normal);
+ vec3 lightDir2 = normalize(lightPos - FragPos);
+ float diff = max(dot(norm, lightDir2), 0.0);
+ vec3 diffuse = diff * vec3(1.0);
+
+ vec3 viewDir2 = normalize(viewPos.xyz - FragPos);
+ vec3 reflectDir = reflect(-lightDir2, norm);
+ float spec = pow(max(dot(viewDir2, reflectDir), 0.0), 32.0);
+ vec3 specular = 0.5 * spec * vec3(1.0);
+
+ vec3 result = ambient + diffuse + specular;
+
+ if (useTexture != 0) {
+ outColor = texture(uTexture, TexCoord) * vec4(result, 1.0);
+ } else {
+ outColor = color * vec4(result, 1.0);
+ }
+}
diff --git a/assets/shaders/basic.frag.spv b/assets/shaders/basic.frag.spv
new file mode 100644
index 00000000..e58cf2d6
Binary files /dev/null and b/assets/shaders/basic.frag.spv differ
diff --git a/assets/shaders/basic.vert.glsl b/assets/shaders/basic.vert.glsl
new file mode 100644
index 00000000..2817fb8e
--- /dev/null
+++ b/assets/shaders/basic.vert.glsl
@@ -0,0 +1,34 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(push_constant) uniform Push {
+ mat4 model;
+} push;
+
+layout(location = 0) in vec3 aPosition;
+layout(location = 1) in vec3 aNormal;
+layout(location = 2) in vec2 aTexCoord;
+
+layout(location = 0) out vec3 FragPos;
+layout(location = 1) out vec3 Normal;
+layout(location = 2) out vec2 TexCoord;
+
+void main() {
+ vec4 worldPos = push.model * vec4(aPosition, 1.0);
+ FragPos = worldPos.xyz;
+ Normal = mat3(push.model) * aNormal;
+ TexCoord = aTexCoord;
+ gl_Position = projection * view * worldPos;
+}
diff --git a/assets/shaders/basic.vert.spv b/assets/shaders/basic.vert.spv
new file mode 100644
index 00000000..f3b4b7b2
Binary files /dev/null and b/assets/shaders/basic.vert.spv differ
diff --git a/assets/shaders/celestial.frag.glsl b/assets/shaders/celestial.frag.glsl
new file mode 100644
index 00000000..8c450c46
--- /dev/null
+++ b/assets/shaders/celestial.frag.glsl
@@ -0,0 +1,64 @@
+#version 450
+
+layout(push_constant) uniform Push {
+ mat4 model;
+ vec4 celestialColor; // xyz = color, w = unused
+ float intensity;
+ float moonPhase;
+ float animTime;
+} push;
+
+layout(location = 0) in vec2 TexCoord;
+
+layout(location = 0) out vec4 outColor;
+
+float valueNoise(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, 0.0), vec2(127.1, 311.7))) * 43758.5453);
+ float c = fract(sin(dot(i + vec2(0.0, 1.0), vec2(127.1, 311.7))) * 43758.5453);
+ float d = fract(sin(dot(i + vec2(1.0, 1.0), vec2(127.1, 311.7))) * 43758.5453);
+ return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
+}
+
+void main() {
+ vec2 uv = TexCoord - 0.5;
+ float dist = length(uv);
+
+ // Hard circular cutoff β nothing beyond radius 0.35
+ if (dist > 0.35) discard;
+
+ // Hard disc with smooth edge
+ float disc = smoothstep(0.35, 0.28, dist);
+
+ // Soft glow confined within cutoff radius
+ float glow = exp(-dist * dist * 40.0) * 0.5;
+
+ // Combine disc and glow
+ float alpha = max(disc, glow) * push.intensity;
+
+ // Smooth fade to zero at cutoff boundary
+ float edgeFade = 1.0 - smoothstep(0.25, 0.35, dist);
+ alpha *= edgeFade;
+
+ vec3 color = push.celestialColor.rgb;
+
+ // Animated haze/turbulence overlay for the sun disc
+ if (push.intensity > 0.5) {
+ float noise = valueNoise(uv * 8.0 + vec2(push.animTime * 0.3, push.animTime * 0.2));
+ float noise2 = valueNoise(uv * 16.0 - vec2(push.animTime * 0.5, push.animTime * 0.15));
+ float turbulence = (noise * 0.6 + noise2 * 0.4) * disc;
+ color += vec3(turbulence * 0.3, turbulence * 0.15, 0.0);
+ }
+
+ // Moon phase shadow (only applied when intensity < 0.5, i.e. for moons)
+ float phaseX = uv.x * 2.0 + push.moonPhase;
+ float phaseShadow = smoothstep(-0.1, 0.1, phaseX);
+ alpha *= mix(phaseShadow, 1.0, step(0.5, push.intensity));
+
+ if (alpha < 0.01) discard;
+ // Pre-multiply for additive blending: RGB is the light contribution
+ outColor = vec4(color * alpha, alpha);
+}
diff --git a/assets/shaders/celestial.frag.spv b/assets/shaders/celestial.frag.spv
new file mode 100644
index 00000000..9fce6c5e
Binary files /dev/null and b/assets/shaders/celestial.frag.spv differ
diff --git a/assets/shaders/celestial.vert.glsl b/assets/shaders/celestial.vert.glsl
new file mode 100644
index 00000000..f71b9ba0
--- /dev/null
+++ b/assets/shaders/celestial.vert.glsl
@@ -0,0 +1,34 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(push_constant) uniform Push {
+ mat4 model;
+ vec4 celestialColor; // xyz = color, w = unused
+ float intensity;
+ float moonPhase;
+ float animTime;
+} push;
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in vec2 aTexCoord;
+
+layout(location = 0) out vec2 TexCoord;
+
+void main() {
+ TexCoord = aTexCoord;
+ // Sky object: remove camera translation so celestial bodies are at infinite distance
+ mat4 rotView = mat4(mat3(view));
+ gl_Position = projection * rotView * push.model * vec4(aPos, 1.0);
+}
diff --git a/assets/shaders/celestial.vert.spv b/assets/shaders/celestial.vert.spv
new file mode 100644
index 00000000..3ab1ecdd
Binary files /dev/null and b/assets/shaders/celestial.vert.spv differ
diff --git a/assets/shaders/character.frag.glsl b/assets/shaders/character.frag.glsl
new file mode 100644
index 00000000..b28fa314
--- /dev/null
+++ b/assets/shaders/character.frag.glsl
@@ -0,0 +1,187 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(set = 1, binding = 0) uniform sampler2D uTexture;
+
+layout(set = 1, binding = 1) uniform CharMaterial {
+ float opacity;
+ int alphaTest;
+ int colorKeyBlack;
+ int unlit;
+ float emissiveBoost;
+ vec3 emissiveTint;
+ float specularIntensity;
+ int enableNormalMap;
+ int enablePOM;
+ float pomScale;
+ int pomMaxSamples;
+ float heightMapVariance;
+ float normalMapStrength;
+};
+
+layout(set = 1, binding = 2) uniform sampler2D uNormalHeightMap;
+
+layout(set = 0, binding = 1) uniform sampler2DShadow uShadowMap;
+
+layout(location = 0) in vec3 FragPos;
+layout(location = 1) in vec3 Normal;
+layout(location = 2) in vec2 TexCoord;
+layout(location = 3) in vec3 Tangent;
+layout(location = 4) in vec3 Bitangent;
+
+layout(location = 0) out vec4 outColor;
+
+const float SHADOW_TEXEL = 1.0 / 4096.0;
+
+float sampleShadowPCF(sampler2DShadow smap, vec3 coords) {
+ float shadow = 0.0;
+ for (int x = -1; x <= 1; ++x) {
+ for (int y = -1; y <= 1; ++y) {
+ shadow += texture(smap, vec3(coords.xy + vec2(x, y) * SHADOW_TEXEL, coords.z));
+ }
+ }
+ return shadow / 9.0;
+}
+
+// LOD factor from screen-space UV derivatives
+float computeLodFactor() {
+ vec2 dx = dFdx(TexCoord);
+ vec2 dy = dFdy(TexCoord);
+ float texelDensity = max(dot(dx, dx), dot(dy, dy));
+ return smoothstep(0.0001, 0.005, texelDensity);
+}
+
+// Parallax Occlusion Mapping with angle-adaptive sampling
+vec2 parallaxOcclusionMap(vec2 uv, vec3 viewDirTS, float lodFactor) {
+ float VdotN = abs(viewDirTS.z);
+
+ if (VdotN < 0.15) return uv;
+
+ float angleFactor = clamp(VdotN, 0.15, 1.0);
+ int maxS = pomMaxSamples;
+ int minS = max(maxS / 4, 4);
+ int numSamples = int(mix(float(minS), float(maxS), angleFactor));
+ numSamples = int(mix(float(minS), float(numSamples), 1.0 - lodFactor));
+
+ float layerDepth = 1.0 / float(numSamples);
+ float currentLayerDepth = 0.0;
+
+ vec2 P = viewDirTS.xy / max(VdotN, 0.15) * pomScale;
+ float maxOffset = pomScale * 3.0;
+ P = clamp(P, vec2(-maxOffset), vec2(maxOffset));
+ vec2 deltaUV = P / float(numSamples);
+
+ vec2 currentUV = uv;
+ float currentDepthMapValue = 1.0 - texture(uNormalHeightMap, currentUV).a;
+
+ for (int i = 0; i < 64; i++) {
+ if (i >= numSamples || currentLayerDepth >= currentDepthMapValue) break;
+ currentUV -= deltaUV;
+ currentDepthMapValue = 1.0 - texture(uNormalHeightMap, currentUV).a;
+ currentLayerDepth += layerDepth;
+ }
+
+ vec2 prevUV = currentUV + deltaUV;
+ float afterDepth = currentDepthMapValue - currentLayerDepth;
+ float beforeDepth = (1.0 - texture(uNormalHeightMap, prevUV).a) - currentLayerDepth + layerDepth;
+ float weight = afterDepth / (afterDepth - beforeDepth + 0.0001);
+ vec2 result = mix(currentUV, prevUV, weight);
+
+ float fadeFactor = smoothstep(0.15, 0.35, VdotN);
+ return mix(uv, result, fadeFactor);
+}
+
+void main() {
+ float lodFactor = computeLodFactor();
+
+ vec3 vertexNormal = normalize(Normal);
+ if (!gl_FrontFacing) vertexNormal = -vertexNormal;
+
+ vec2 finalUV = TexCoord;
+
+ // Build TBN matrix
+ vec3 T = normalize(Tangent);
+ vec3 B = normalize(Bitangent);
+ vec3 N = vertexNormal;
+ mat3 TBN = mat3(T, B, N);
+
+ if (enablePOM != 0 && heightMapVariance > 0.001 && lodFactor < 0.99) {
+ mat3 TBN_inv = transpose(TBN);
+ vec3 viewDirWorld = normalize(viewPos.xyz - FragPos);
+ vec3 viewDirTS = TBN_inv * viewDirWorld;
+ finalUV = parallaxOcclusionMap(TexCoord, viewDirTS, lodFactor);
+ }
+
+ vec4 texColor = texture(uTexture, finalUV);
+
+ if (alphaTest != 0 && texColor.a < 0.5) discard;
+ if (colorKeyBlack != 0) {
+ float lum = dot(texColor.rgb, vec3(0.299, 0.587, 0.114));
+ float ck = smoothstep(0.12, 0.30, lum);
+ texColor.a *= ck;
+ if (texColor.a < 0.01) discard;
+ }
+
+ // Compute normal (with normal mapping if enabled)
+ vec3 norm = vertexNormal;
+ if (enableNormalMap != 0 && lodFactor < 0.99 && normalMapStrength > 0.001) {
+ vec3 mapNormal = texture(uNormalHeightMap, finalUV).rgb * 2.0 - 1.0;
+ mapNormal.xy *= normalMapStrength;
+ mapNormal = normalize(mapNormal);
+ vec3 worldNormal = normalize(TBN * mapNormal);
+ if (!gl_FrontFacing) worldNormal = -worldNormal;
+ float blendFactor = max(lodFactor, 1.0 - normalMapStrength);
+ norm = normalize(mix(worldNormal, vertexNormal, blendFactor));
+ }
+
+ vec3 result;
+
+ if (unlit != 0) {
+ vec3 warm = emissiveTint * emissiveBoost;
+ result = texColor.rgb * (1.0 + warm);
+ } else {
+ vec3 ldir = normalize(-lightDir.xyz);
+ float diff = max(dot(norm, ldir), 0.0);
+
+ vec3 viewDir = normalize(viewPos.xyz - FragPos);
+ vec3 halfDir = normalize(ldir + viewDir);
+ float spec = pow(max(dot(norm, halfDir), 0.0), 32.0) * specularIntensity;
+
+ float shadow = 1.0;
+ if (shadowParams.x > 0.5) {
+ float normalOffset = SHADOW_TEXEL * 2.0 * (1.0 - abs(dot(norm, ldir)));
+ vec3 biasedPos = FragPos + norm * normalOffset;
+ vec4 lsPos = lightSpaceMatrix * vec4(biasedPos, 1.0);
+ vec3 proj = lsPos.xyz / lsPos.w;
+ proj.xy = proj.xy * 0.5 + 0.5;
+ if (proj.x >= 0.0 && proj.x <= 1.0 &&
+ proj.y >= 0.0 && proj.y <= 1.0 &&
+ proj.z >= 0.0 && proj.z <= 1.0) {
+ float bias = max(0.0005 * (1.0 - dot(norm, ldir)), 0.00005);
+ shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias));
+ }
+ shadow = mix(1.0, shadow, shadowParams.y);
+ }
+
+ result = ambientColor.rgb * texColor.rgb
+ + shadow * (diff * lightColor.rgb * texColor.rgb + spec * lightColor.rgb);
+ }
+
+ float dist = length(viewPos.xyz - FragPos);
+ float fogFactor = clamp((fogParams.y - dist) / (fogParams.y - fogParams.x), 0.0, 1.0);
+ result = mix(fogColor.rgb, result, fogFactor);
+
+ outColor = vec4(result, texColor.a * opacity);
+}
diff --git a/assets/shaders/character.frag.spv b/assets/shaders/character.frag.spv
new file mode 100644
index 00000000..99c68edf
Binary files /dev/null and b/assets/shaders/character.frag.spv differ
diff --git a/assets/shaders/character.vert.glsl b/assets/shaders/character.vert.glsl
new file mode 100644
index 00000000..52e4ee65
--- /dev/null
+++ b/assets/shaders/character.vert.glsl
@@ -0,0 +1,63 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(push_constant) uniform Push {
+ mat4 model;
+} push;
+
+layout(set = 2, binding = 0) readonly buffer BoneSSBO {
+ mat4 bones[];
+};
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in vec4 aBoneWeights;
+layout(location = 2) in ivec4 aBoneIndices;
+layout(location = 3) in vec3 aNormal;
+layout(location = 4) in vec2 aTexCoord;
+layout(location = 5) in vec4 aTangent;
+
+layout(location = 0) out vec3 FragPos;
+layout(location = 1) out vec3 Normal;
+layout(location = 2) out vec2 TexCoord;
+layout(location = 3) out vec3 Tangent;
+layout(location = 4) out vec3 Bitangent;
+
+void main() {
+ mat4 skinMat = bones[aBoneIndices.x] * aBoneWeights.x
+ + bones[aBoneIndices.y] * aBoneWeights.y
+ + bones[aBoneIndices.z] * aBoneWeights.z
+ + bones[aBoneIndices.w] * aBoneWeights.w;
+
+ vec4 skinnedPos = skinMat * vec4(aPos, 1.0);
+ vec3 skinnedNorm = mat3(skinMat) * aNormal;
+ vec3 skinnedTan = mat3(skinMat) * aTangent.xyz;
+
+ vec4 worldPos = push.model * skinnedPos;
+ mat3 modelMat3 = mat3(push.model);
+ FragPos = worldPos.xyz;
+ Normal = modelMat3 * skinnedNorm;
+ TexCoord = aTexCoord;
+
+ // Gram-Schmidt re-orthogonalize tangent w.r.t. normal
+ vec3 N = normalize(Normal);
+ vec3 T = normalize(modelMat3 * skinnedTan);
+ T = normalize(T - dot(T, N) * N);
+ vec3 B = cross(N, T) * aTangent.w;
+
+ Tangent = T;
+ Bitangent = B;
+
+ gl_Position = projection * view * worldPos;
+}
diff --git a/assets/shaders/character.vert.spv b/assets/shaders/character.vert.spv
new file mode 100644
index 00000000..cbe4916d
Binary files /dev/null and b/assets/shaders/character.vert.spv differ
diff --git a/assets/shaders/character_shadow.frag.glsl b/assets/shaders/character_shadow.frag.glsl
new file mode 100644
index 00000000..c37cbd20
--- /dev/null
+++ b/assets/shaders/character_shadow.frag.glsl
@@ -0,0 +1,19 @@
+#version 450
+
+layout(set = 1, binding = 0) uniform sampler2D uTexture;
+
+layout(set = 1, binding = 1) uniform ShadowParams {
+ int alphaTest;
+ int colorKeyBlack;
+};
+
+layout(location = 0) in vec2 TexCoord;
+
+void main() {
+ vec4 texColor = texture(uTexture, TexCoord);
+ if (alphaTest != 0 && texColor.a < 0.5) discard;
+ if (colorKeyBlack != 0) {
+ float lum = dot(texColor.rgb, vec3(0.299, 0.587, 0.114));
+ if (lum < 0.12) discard;
+ }
+}
diff --git a/assets/shaders/character_shadow.frag.spv b/assets/shaders/character_shadow.frag.spv
new file mode 100644
index 00000000..d792a329
Binary files /dev/null and b/assets/shaders/character_shadow.frag.spv differ
diff --git a/assets/shaders/character_shadow.vert.glsl b/assets/shaders/character_shadow.vert.glsl
new file mode 100644
index 00000000..b48a48d9
--- /dev/null
+++ b/assets/shaders/character_shadow.vert.glsl
@@ -0,0 +1,27 @@
+#version 450
+
+layout(push_constant) uniform Push {
+ mat4 lightSpaceMatrix;
+ mat4 model;
+} push;
+
+layout(set = 2, binding = 0) readonly buffer BoneSSBO {
+ mat4 bones[];
+};
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in vec4 aBoneWeights;
+layout(location = 2) in ivec4 aBoneIndices;
+layout(location = 3) in vec2 aTexCoord;
+
+layout(location = 0) out vec2 TexCoord;
+
+void main() {
+ mat4 skinMat = bones[aBoneIndices.x] * aBoneWeights.x
+ + bones[aBoneIndices.y] * aBoneWeights.y
+ + bones[aBoneIndices.z] * aBoneWeights.z
+ + bones[aBoneIndices.w] * aBoneWeights.w;
+ vec4 skinnedPos = skinMat * vec4(aPos, 1.0);
+ TexCoord = aTexCoord;
+ gl_Position = push.lightSpaceMatrix * push.model * skinnedPos;
+}
diff --git a/assets/shaders/character_shadow.vert.spv b/assets/shaders/character_shadow.vert.spv
new file mode 100644
index 00000000..4a5beee2
Binary files /dev/null and b/assets/shaders/character_shadow.vert.spv differ
diff --git a/assets/shaders/charge_dust.frag.glsl b/assets/shaders/charge_dust.frag.glsl
new file mode 100644
index 00000000..0c078d51
--- /dev/null
+++ b/assets/shaders/charge_dust.frag.glsl
@@ -0,0 +1,13 @@
+#version 450
+
+layout(location = 0) in float vAlpha;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ vec2 p = gl_PointCoord - vec2(0.5);
+ float dist = length(p);
+ if (dist > 0.5) discard;
+ float alpha = smoothstep(0.5, 0.1, dist) * vAlpha * 0.45;
+ outColor = vec4(0.65, 0.55, 0.40, alpha);
+}
diff --git a/assets/shaders/charge_dust.frag.spv b/assets/shaders/charge_dust.frag.spv
new file mode 100644
index 00000000..a8f78d8f
Binary files /dev/null and b/assets/shaders/charge_dust.frag.spv differ
diff --git a/assets/shaders/charge_dust.vert.glsl b/assets/shaders/charge_dust.vert.glsl
new file mode 100644
index 00000000..25e0dea8
--- /dev/null
+++ b/assets/shaders/charge_dust.vert.glsl
@@ -0,0 +1,26 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in float aSize;
+layout(location = 2) in float aAlpha;
+
+layout(location = 0) out float vAlpha;
+
+void main() {
+ gl_PointSize = aSize;
+ vAlpha = aAlpha;
+ gl_Position = projection * view * vec4(aPos, 1.0);
+}
diff --git a/assets/shaders/charge_dust.vert.spv b/assets/shaders/charge_dust.vert.spv
new file mode 100644
index 00000000..2b84488c
Binary files /dev/null and b/assets/shaders/charge_dust.vert.spv differ
diff --git a/assets/shaders/charge_ribbon.frag.glsl b/assets/shaders/charge_ribbon.frag.glsl
new file mode 100644
index 00000000..417feed1
--- /dev/null
+++ b/assets/shaders/charge_ribbon.frag.glsl
@@ -0,0 +1,16 @@
+#version 450
+
+layout(location = 0) in float vAlpha;
+layout(location = 1) in float vHeat;
+layout(location = 2) in float vHeight;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ vec3 top = vec3(1.0, 0.2, 0.0);
+ vec3 mid = vec3(1.0, 0.5, 0.0);
+ vec3 color = mix(mid, top, vHeight);
+ color = mix(color, vec3(1.0, 0.8, 0.3), vHeat * 0.5);
+ float alpha = vAlpha * smoothstep(0.0, 0.3, vHeight);
+ outColor = vec4(color, alpha);
+}
diff --git a/assets/shaders/charge_ribbon.frag.spv b/assets/shaders/charge_ribbon.frag.spv
new file mode 100644
index 00000000..0063155e
Binary files /dev/null and b/assets/shaders/charge_ribbon.frag.spv differ
diff --git a/assets/shaders/charge_ribbon.vert.glsl b/assets/shaders/charge_ribbon.vert.glsl
new file mode 100644
index 00000000..d50f947c
--- /dev/null
+++ b/assets/shaders/charge_ribbon.vert.glsl
@@ -0,0 +1,30 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in float aAlpha;
+layout(location = 2) in float aHeat;
+layout(location = 3) in float aHeight;
+
+layout(location = 0) out float vAlpha;
+layout(location = 1) out float vHeat;
+layout(location = 2) out float vHeight;
+
+void main() {
+ vAlpha = aAlpha;
+ vHeat = aHeat;
+ vHeight = aHeight;
+ gl_Position = projection * view * vec4(aPos, 1.0);
+}
diff --git a/assets/shaders/charge_ribbon.vert.spv b/assets/shaders/charge_ribbon.vert.spv
new file mode 100644
index 00000000..d037bf55
Binary files /dev/null and b/assets/shaders/charge_ribbon.vert.spv differ
diff --git a/assets/shaders/clouds.frag.glsl b/assets/shaders/clouds.frag.glsl
new file mode 100644
index 00000000..1e443a2f
--- /dev/null
+++ b/assets/shaders/clouds.frag.glsl
@@ -0,0 +1,104 @@
+#version 450
+
+layout(push_constant) uniform Push {
+ vec4 cloudColor; // xyz = DBC-derived base cloud color, w = unused
+ vec4 sunDirDensity; // xyz = sun direction, w = density
+ vec4 windAndLight; // x = windOffset, y = sunIntensity, z = ambient, w = unused
+} push;
+
+layout(location = 0) in vec3 vWorldDir;
+
+layout(location = 0) out vec4 outColor;
+
+// --- Gradient noise (smoother than hash-based) ---
+vec2 hash2(vec2 p) {
+ p = vec2(dot(p, vec2(127.1, 311.7)),
+ dot(p, vec2(269.5, 183.3)));
+ return fract(sin(p) * 43758.5453);
+}
+
+float gradientNoise(vec2 p) {
+ vec2 i = floor(p);
+ vec2 f = fract(p);
+
+ // Quintic interpolation for smoother results
+ vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
+
+ float a = dot(hash2(i + vec2(0.0, 0.0)) * 2.0 - 1.0, f - vec2(0.0, 0.0));
+ float b = dot(hash2(i + vec2(1.0, 0.0)) * 2.0 - 1.0, f - vec2(1.0, 0.0));
+ float c = dot(hash2(i + vec2(0.0, 1.0)) * 2.0 - 1.0, f - vec2(0.0, 1.0));
+ float d = dot(hash2(i + vec2(1.0, 1.0)) * 2.0 - 1.0, f - vec2(1.0, 1.0));
+
+ return mix(mix(a, b, u.x), mix(c, d, u.x), u.y) * 0.5 + 0.5;
+}
+
+float fbm(vec2 p) {
+ float val = 0.0;
+ float amp = 0.5;
+ for (int i = 0; i < 6; i++) {
+ val += amp * gradientNoise(p);
+ p *= 2.0;
+ amp *= 0.5;
+ }
+ return val;
+}
+
+void main() {
+ vec3 dir = normalize(vWorldDir);
+ float altitude = dir.z;
+ if (altitude < 0.0) discard;
+
+ vec3 sunDir = push.sunDirDensity.xyz;
+ float density = push.sunDirDensity.w;
+ float windOffset = push.windAndLight.x;
+ float sunIntensity = push.windAndLight.y;
+ float ambient = push.windAndLight.z;
+
+ vec2 uv = dir.xy / (altitude + 0.001);
+ uv += windOffset;
+
+ // --- 6-octave FBM for cloud shape ---
+ float cloud1 = fbm(uv * 0.8);
+ float cloud2 = fbm(uv * 1.6 + 5.0);
+ float cloud = cloud1 * 0.7 + cloud2 * 0.3;
+
+ // Coverage control: base coverage with detail erosion
+ float baseCoverage = smoothstep(0.30, 0.55, cloud);
+ float detailErosion = gradientNoise(uv * 4.0);
+ cloud = baseCoverage * smoothstep(0.2, 0.5, detailErosion);
+ cloud *= density;
+
+ // Horizon fade
+ float horizonFade = smoothstep(0.0, 0.15, altitude);
+ cloud *= horizonFade;
+
+ if (cloud < 0.01) discard;
+
+ // --- Sun lighting on clouds ---
+ // Sun dot product for view-relative brightness
+ float sunDot = max(dot(vec3(0.0, 0.0, 1.0), sunDir), 0.0);
+
+ // Self-shadowing: sample noise offset toward sun direction, darken if occluded
+ float lightSample = fbm((uv + sunDir.xy * 0.05) * 0.8);
+ float shadow = smoothstep(0.3, 0.7, lightSample);
+
+ // Base lit color: mix dark (shadow) and bright (sunlit) based on shadow and sun
+ vec3 baseColor = push.cloudColor.rgb;
+ vec3 shadowColor = baseColor * (ambient * 0.8);
+ vec3 litColor = baseColor * (ambient + sunIntensity * 0.6);
+ vec3 cloudRgb = mix(shadowColor, litColor, shadow * sunDot);
+
+ // Add ambient fill so clouds aren't too dark
+ cloudRgb = mix(baseColor * ambient, cloudRgb, 0.7 + 0.3 * sunIntensity);
+
+ // --- Silver lining effect at cloud edges ---
+ float edgeLight = smoothstep(0.0, 0.3, cloud) * (1.0 - smoothstep(0.3, 0.8, cloud));
+ cloudRgb += vec3(1.0, 0.95, 0.9) * edgeLight * sunDot * sunIntensity * 0.4;
+
+ // --- Edge softness for alpha ---
+ float edgeSoftness = smoothstep(0.0, 0.3, cloud);
+ float alpha = cloud * edgeSoftness;
+
+ if (alpha < 0.01) discard;
+ outColor = vec4(cloudRgb, alpha);
+}
diff --git a/assets/shaders/clouds.frag.spv b/assets/shaders/clouds.frag.spv
new file mode 100644
index 00000000..70b318db
Binary files /dev/null and b/assets/shaders/clouds.frag.spv differ
diff --git a/assets/shaders/clouds.vert.glsl b/assets/shaders/clouds.vert.glsl
new file mode 100644
index 00000000..5ddd612e
--- /dev/null
+++ b/assets/shaders/clouds.vert.glsl
@@ -0,0 +1,25 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(location = 0) in vec3 aPos;
+
+layout(location = 0) out vec3 vWorldDir;
+
+void main() {
+ vWorldDir = aPos;
+ mat4 rotView = mat4(mat3(view));
+ vec4 pos = projection * rotView * vec4(aPos, 1.0);
+ gl_Position = pos.xyww;
+}
diff --git a/assets/shaders/clouds.vert.spv b/assets/shaders/clouds.vert.spv
new file mode 100644
index 00000000..cf1b62d9
Binary files /dev/null and b/assets/shaders/clouds.vert.spv differ
diff --git a/assets/shaders/lens_flare.frag.glsl b/assets/shaders/lens_flare.frag.glsl
new file mode 100644
index 00000000..98b99b5c
--- /dev/null
+++ b/assets/shaders/lens_flare.frag.glsl
@@ -0,0 +1,22 @@
+#version 450
+
+layout(push_constant) uniform Push {
+ vec2 position;
+ float size;
+ float aspectRatio;
+ vec4 color; // rgb + brightness in w
+} push;
+
+layout(location = 0) in vec2 UV;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ vec2 center = UV - 0.5;
+ float dist = length(center);
+ float alpha = smoothstep(0.5, 0.0, dist);
+ float glow = exp(-dist * dist * 8.0) * 0.5;
+ alpha = max(alpha, glow) * push.color.w;
+ if (alpha < 0.01) discard;
+ outColor = vec4(push.color.rgb, alpha);
+}
diff --git a/assets/shaders/lens_flare.frag.spv b/assets/shaders/lens_flare.frag.spv
new file mode 100644
index 00000000..1b19ee3a
Binary files /dev/null and b/assets/shaders/lens_flare.frag.spv differ
diff --git a/assets/shaders/lens_flare.vert.glsl b/assets/shaders/lens_flare.vert.glsl
new file mode 100644
index 00000000..75dc772a
--- /dev/null
+++ b/assets/shaders/lens_flare.vert.glsl
@@ -0,0 +1,19 @@
+#version 450
+
+layout(push_constant) uniform Push {
+ vec2 position;
+ float size;
+ float aspectRatio;
+} push;
+
+layout(location = 0) in vec2 aPos;
+layout(location = 1) in vec2 aUV;
+
+layout(location = 0) out vec2 UV;
+
+void main() {
+ UV = aUV;
+ vec2 scaled = aPos * push.size;
+ scaled.x /= push.aspectRatio;
+ gl_Position = vec4(scaled + push.position, 0.0, 1.0);
+}
diff --git a/assets/shaders/lens_flare.vert.spv b/assets/shaders/lens_flare.vert.spv
new file mode 100644
index 00000000..f1b0dd91
Binary files /dev/null and b/assets/shaders/lens_flare.vert.spv differ
diff --git a/assets/shaders/lightning_bolt.frag.glsl b/assets/shaders/lightning_bolt.frag.glsl
new file mode 100644
index 00000000..4f63bfc1
--- /dev/null
+++ b/assets/shaders/lightning_bolt.frag.glsl
@@ -0,0 +1,10 @@
+#version 450
+
+layout(location = 0) in float vBrightness;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ vec3 color = mix(vec3(0.6, 0.8, 1.0), vec3(1.0), vBrightness * 0.5);
+ outColor = vec4(color, vBrightness);
+}
diff --git a/assets/shaders/lightning_bolt.frag.spv b/assets/shaders/lightning_bolt.frag.spv
new file mode 100644
index 00000000..0093886c
Binary files /dev/null and b/assets/shaders/lightning_bolt.frag.spv differ
diff --git a/assets/shaders/lightning_bolt.vert.glsl b/assets/shaders/lightning_bolt.vert.glsl
new file mode 100644
index 00000000..7cc2d045
--- /dev/null
+++ b/assets/shaders/lightning_bolt.vert.glsl
@@ -0,0 +1,27 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(push_constant) uniform Push {
+ float brightness;
+} push;
+
+layout(location = 0) in vec3 aPos;
+
+layout(location = 0) out float vBrightness;
+
+void main() {
+ vBrightness = push.brightness;
+ gl_Position = projection * view * vec4(aPos, 1.0);
+}
diff --git a/assets/shaders/lightning_bolt.vert.spv b/assets/shaders/lightning_bolt.vert.spv
new file mode 100644
index 00000000..6dc9f545
Binary files /dev/null and b/assets/shaders/lightning_bolt.vert.spv differ
diff --git a/assets/shaders/lightning_flash.frag.glsl b/assets/shaders/lightning_flash.frag.glsl
new file mode 100644
index 00000000..01ebcdea
--- /dev/null
+++ b/assets/shaders/lightning_flash.frag.glsl
@@ -0,0 +1,11 @@
+#version 450
+
+layout(push_constant) uniform Push {
+ float intensity;
+} push;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ outColor = vec4(1.0, 1.0, 1.0, push.intensity * 0.6);
+}
diff --git a/assets/shaders/lightning_flash.frag.spv b/assets/shaders/lightning_flash.frag.spv
new file mode 100644
index 00000000..048485a7
Binary files /dev/null and b/assets/shaders/lightning_flash.frag.spv differ
diff --git a/assets/shaders/lightning_flash.vert.glsl b/assets/shaders/lightning_flash.vert.glsl
new file mode 100644
index 00000000..b748774b
--- /dev/null
+++ b/assets/shaders/lightning_flash.vert.glsl
@@ -0,0 +1,7 @@
+#version 450
+
+layout(location = 0) in vec2 aPos;
+
+void main() {
+ gl_Position = vec4(aPos, 0.0, 1.0);
+}
diff --git a/assets/shaders/lightning_flash.vert.spv b/assets/shaders/lightning_flash.vert.spv
new file mode 100644
index 00000000..00341bee
Binary files /dev/null and b/assets/shaders/lightning_flash.vert.spv differ
diff --git a/assets/shaders/m2.frag.glsl b/assets/shaders/m2.frag.glsl
new file mode 100644
index 00000000..36b0fc2c
--- /dev/null
+++ b/assets/shaders/m2.frag.glsl
@@ -0,0 +1,190 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(set = 1, binding = 0) uniform sampler2D uTexture;
+
+layout(set = 1, binding = 2) uniform M2Material {
+ int hasTexture;
+ int alphaTest;
+ int colorKeyBlack;
+ float colorKeyThreshold;
+ int unlit;
+ int blendMode;
+ float fadeAlpha;
+ float interiorDarken;
+ float specularIntensity;
+};
+
+layout(set = 0, binding = 1) uniform sampler2DShadow uShadowMap;
+
+layout(location = 0) in vec3 FragPos;
+layout(location = 1) in vec3 Normal;
+layout(location = 2) in vec2 TexCoord;
+layout(location = 3) flat in vec3 InstanceOrigin;
+layout(location = 4) in float ModelHeight;
+
+layout(location = 0) out vec4 outColor;
+
+const float SHADOW_TEXEL = 1.0 / 4096.0;
+
+float sampleShadowPCF(sampler2DShadow smap, vec3 coords) {
+ float shadow = 0.0;
+ for (int x = -1; x <= 1; ++x) {
+ for (int y = -1; y <= 1; ++y) {
+ shadow += texture(smap, vec3(coords.xy + vec2(x, y) * SHADOW_TEXEL, coords.z));
+ }
+ }
+ return shadow / 9.0;
+}
+
+// 4x4 Bayer dither matrix (normalized to 0..1)
+float bayerDither4x4(ivec2 p) {
+ int idx = (p.x & 3) + (p.y & 3) * 4;
+ float m[16] = float[16](
+ 0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0,
+ 12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0,
+ 3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0,
+ 15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0
+ );
+ return m[idx];
+}
+
+void main() {
+ vec4 texColor = hasTexture != 0 ? texture(uTexture, TexCoord) : vec4(1.0);
+
+ bool isFoliage = (alphaTest == 2);
+
+ // Fix DXT fringe: transparent edge texels have garbage (black) RGB.
+ // At low alpha the original RGB is untrustworthy β replace with the
+ // averaged color from nearby opaque texels (high mip). The lower
+ // the alpha the more we distrust the original color.
+ if (alphaTest != 0 && texColor.a > 0.01 && texColor.a < 1.0) {
+ vec3 mipColor = textureLod(uTexture, TexCoord, 4.0).rgb;
+ // trust = 0 at alpha 0, trust = 1 at alpha ~0.9
+ float trust = smoothstep(0.0, 0.9, texColor.a);
+ texColor.rgb = mix(mipColor, texColor.rgb, trust);
+ }
+
+ float alphaCutoff = 0.5;
+ if (alphaTest == 2) {
+ alphaCutoff = 0.4;
+ } else if (alphaTest == 3) {
+ alphaCutoff = 0.25;
+ } else if (alphaTest != 0) {
+ alphaCutoff = 0.4;
+ }
+ if (alphaTest != 0 && texColor.a < alphaCutoff) {
+ discard;
+ }
+ if (colorKeyBlack != 0) {
+ float lum = dot(texColor.rgb, vec3(0.299, 0.587, 0.114));
+ if (lum < colorKeyThreshold) discard;
+ }
+ if (blendMode == 1 && texColor.a < 0.004) discard;
+
+ // Per-instance color variation (foliage only)
+ if (isFoliage) {
+ float hash = fract(sin(dot(InstanceOrigin.xy, vec2(127.1, 311.7))) * 43758.5453);
+ float hueShiftR = 1.0 + (hash - 0.5) * 0.16; // Β±8% red
+ float hueShiftB = 1.0 + (fract(hash * 7.13) - 0.5) * 0.16; // Β±8% blue
+ float brightness = 0.85 + hash * 0.30; // 85β115%
+ texColor.rgb *= vec3(hueShiftR, 1.0, hueShiftB) * brightness;
+ }
+
+ vec3 norm = normalize(Normal);
+ bool foliageTwoSided = (alphaTest == 2);
+ if (!foliageTwoSided && !gl_FrontFacing) norm = -norm;
+
+ // Detail normal perturbation (foliage only) β UV-based only so wind doesn't cause flicker
+ if (isFoliage) {
+ float nx = sin(TexCoord.x * 12.0 + TexCoord.y * 5.3) * 0.10;
+ float ny = sin(TexCoord.y * 14.0 + TexCoord.x * 4.7) * 0.10;
+ norm = normalize(norm + vec3(nx, ny, 0.0));
+ }
+
+ vec3 ldir = normalize(-lightDir.xyz);
+ float nDotL = dot(norm, ldir);
+ float diff = foliageTwoSided ? abs(nDotL) : max(nDotL, 0.0);
+
+ vec3 result;
+ if (unlit != 0) {
+ result = texColor.rgb;
+ } else {
+ vec3 viewDir = normalize(viewPos.xyz - FragPos);
+
+ float spec = 0.0;
+ float shadow = 1.0;
+ if (!isFoliage) {
+ vec3 halfDir = normalize(ldir + viewDir);
+ spec = pow(max(dot(norm, halfDir), 0.0), 32.0) * specularIntensity;
+ }
+
+ if (shadowParams.x > 0.5) {
+ float normalOffset = SHADOW_TEXEL * 2.0 * (1.0 - abs(dot(norm, ldir)));
+ vec3 biasedPos = FragPos + norm * normalOffset;
+ vec4 lsPos = lightSpaceMatrix * vec4(biasedPos, 1.0);
+ vec3 proj = lsPos.xyz / lsPos.w;
+ proj.xy = proj.xy * 0.5 + 0.5;
+ if (proj.x >= 0.0 && proj.x <= 1.0 &&
+ proj.y >= 0.0 && proj.y <= 1.0 &&
+ proj.z >= 0.0 && proj.z <= 1.0) {
+ float bias = max(0.0005 * (1.0 - abs(dot(norm, ldir))), 0.00005);
+ shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias));
+ }
+ shadow = mix(1.0, shadow, shadowParams.y);
+ }
+
+ // Leaf subsurface scattering (foliage only) β uses stable normal, no FragPos dependency
+ vec3 sss = vec3(0.0);
+ if (isFoliage) {
+ float backLit = max(-nDotL, 0.0);
+ float viewDotLight = max(dot(viewDir, -ldir), 0.0);
+ float sssAmount = backLit * pow(viewDotLight, 4.0) * 0.35 * texColor.a;
+ sss = sssAmount * vec3(1.0, 0.9, 0.5) * lightColor.rgb;
+ }
+
+ result = ambientColor.rgb * texColor.rgb
+ + shadow * (diff * lightColor.rgb * texColor.rgb + spec * lightColor.rgb)
+ + sss;
+
+ if (interiorDarken > 0.0) {
+ result *= mix(1.0, 0.5, interiorDarken);
+ }
+ }
+
+ // Canopy ambient occlusion (foliage only)
+ if (isFoliage) {
+ float normalizedHeight = clamp(ModelHeight / 18.0, 0.0, 1.0);
+ float aoFactor = mix(0.55, 1.0, smoothstep(0.0, 0.6, normalizedHeight));
+ result *= aoFactor;
+ }
+
+ float dist = length(viewPos.xyz - FragPos);
+ float fogFactor = clamp((fogParams.y - dist) / (fogParams.y - fogParams.x), 0.0, 1.0);
+ result = mix(fogColor.rgb, result, fogFactor);
+
+ float outAlpha = texColor.a * fadeAlpha;
+ // Cutout materials should not remain partially transparent after discard,
+ // otherwise foliage cards look view-dependent.
+ if (alphaTest != 0 || colorKeyBlack != 0) {
+ outAlpha = fadeAlpha;
+ }
+ // Foliage cutout should stay opaque after alpha discard to avoid
+ // view-angle translucency artifacts.
+ if (alphaTest == 2 || alphaTest == 3) {
+ outAlpha = 1.0 * fadeAlpha;
+ }
+ outColor = vec4(result, outAlpha);
+}
diff --git a/assets/shaders/m2.frag.spv b/assets/shaders/m2.frag.spv
new file mode 100644
index 00000000..7deda700
Binary files /dev/null and b/assets/shaders/m2.frag.spv differ
diff --git a/assets/shaders/m2.vert.glsl b/assets/shaders/m2.vert.glsl
new file mode 100644
index 00000000..7b0f9451
--- /dev/null
+++ b/assets/shaders/m2.vert.glsl
@@ -0,0 +1,91 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(push_constant) uniform Push {
+ mat4 model;
+ vec2 uvOffset;
+ int texCoordSet;
+ int useBones;
+ int isFoliage;
+} push;
+
+layout(set = 2, binding = 0) readonly buffer BoneSSBO {
+ mat4 bones[];
+};
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in vec3 aNormal;
+layout(location = 2) in vec2 aTexCoord;
+layout(location = 3) in vec4 aBoneWeights;
+layout(location = 4) in vec4 aBoneIndicesF;
+layout(location = 5) in vec2 aTexCoord2;
+
+layout(location = 0) out vec3 FragPos;
+layout(location = 1) out vec3 Normal;
+layout(location = 2) out vec2 TexCoord;
+layout(location = 3) flat out vec3 InstanceOrigin;
+layout(location = 4) out float ModelHeight;
+
+void main() {
+ vec4 pos = vec4(aPos, 1.0);
+ vec4 norm = vec4(aNormal, 0.0);
+
+ if (push.useBones != 0) {
+ ivec4 bi = ivec4(aBoneIndicesF);
+ mat4 skinMat = bones[bi.x] * aBoneWeights.x
+ + bones[bi.y] * aBoneWeights.y
+ + bones[bi.z] * aBoneWeights.z
+ + bones[bi.w] * aBoneWeights.w;
+ pos = skinMat * pos;
+ norm = skinMat * norm;
+ }
+
+ // Wind animation for foliage
+ if (push.isFoliage != 0) {
+ float windTime = fogParams.z;
+ vec3 worldRef = push.model[3].xyz;
+ float heightFactor = clamp(pos.z / 20.0, 0.0, 1.0);
+ heightFactor *= heightFactor; // quadratic β base stays grounded
+
+ // Layer 1: Trunk sway β slow, large amplitude
+ float trunkPhase = windTime * 0.8 + dot(worldRef.xy, vec2(0.1, 0.13));
+ float trunkSwayX = sin(trunkPhase) * 0.35 * heightFactor;
+ float trunkSwayY = cos(trunkPhase * 0.7) * 0.25 * heightFactor;
+
+ // Layer 2: Branch sway β medium frequency, per-branch phase
+ float branchPhase = windTime * 1.7 + dot(worldRef.xy, vec2(0.37, 0.71));
+ float branchSwayX = sin(branchPhase + pos.y * 0.4) * 0.15 * heightFactor;
+ float branchSwayY = cos(branchPhase * 1.1 + pos.x * 0.3) * 0.12 * heightFactor;
+
+ // Layer 3: Leaf flutter β fast, small amplitude, per-vertex
+ float leafPhase = windTime * 4.5 + dot(aPos, vec3(1.7, 2.3, 0.9));
+ float leafFlutterX = sin(leafPhase) * 0.06 * heightFactor;
+ float leafFlutterY = cos(leafPhase * 1.3) * 0.05 * heightFactor;
+
+ pos.x += trunkSwayX + branchSwayX + leafFlutterX;
+ pos.y += trunkSwayY + branchSwayY + leafFlutterY;
+ }
+
+ vec4 worldPos = push.model * pos;
+ FragPos = worldPos.xyz;
+ Normal = mat3(push.model) * norm.xyz;
+
+ TexCoord = (push.texCoordSet == 1 ? aTexCoord2 : aTexCoord) + push.uvOffset;
+
+ InstanceOrigin = push.model[3].xyz;
+ ModelHeight = pos.z;
+
+ gl_Position = projection * view * worldPos;
+}
diff --git a/assets/shaders/m2.vert.spv b/assets/shaders/m2.vert.spv
new file mode 100644
index 00000000..9d5411d3
Binary files /dev/null and b/assets/shaders/m2.vert.spv differ
diff --git a/assets/shaders/m2_particle.frag.glsl b/assets/shaders/m2_particle.frag.glsl
new file mode 100644
index 00000000..f91a3fb7
--- /dev/null
+++ b/assets/shaders/m2_particle.frag.glsl
@@ -0,0 +1,30 @@
+#version 450
+
+layout(set = 1, binding = 0) uniform sampler2D uTexture;
+
+layout(push_constant) uniform Push {
+ vec2 tileCount;
+ int alphaKey;
+} push;
+
+layout(location = 0) in vec4 vColor;
+layout(location = 1) in float vTile;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ vec2 p = gl_PointCoord;
+ float tile = floor(vTile);
+ float tx = mod(tile, push.tileCount.x);
+ float ty = floor(tile / push.tileCount.x);
+ vec2 uv = (vec2(tx, ty) + p) / push.tileCount;
+ vec4 texColor = texture(uTexture, uv);
+
+ if (push.alphaKey != 0) {
+ float lum = dot(texColor.rgb, vec3(0.299, 0.587, 0.114));
+ if (lum < 0.05) discard;
+ }
+
+ float edge = smoothstep(0.5, 0.4, length(p - 0.5));
+ outColor = texColor * vColor * vec4(vec3(1.0), edge);
+}
diff --git a/assets/shaders/m2_particle.frag.spv b/assets/shaders/m2_particle.frag.spv
new file mode 100644
index 00000000..ee8899bc
Binary files /dev/null and b/assets/shaders/m2_particle.frag.spv differ
diff --git a/assets/shaders/m2_particle.vert.glsl b/assets/shaders/m2_particle.vert.glsl
new file mode 100644
index 00000000..61f4d141
--- /dev/null
+++ b/assets/shaders/m2_particle.vert.glsl
@@ -0,0 +1,31 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in vec4 aColor;
+layout(location = 2) in float aSize;
+layout(location = 3) in float aTile;
+
+layout(location = 0) out vec4 vColor;
+layout(location = 1) out float vTile;
+
+void main() {
+ vec4 viewPos4 = view * vec4(aPos, 1.0);
+ float dist = -viewPos4.z;
+ gl_PointSize = clamp(aSize * 500.0 / max(dist, 1.0), 1.0, 128.0);
+ vColor = aColor;
+ vTile = aTile;
+ gl_Position = projection * viewPos4;
+}
diff --git a/assets/shaders/m2_particle.vert.spv b/assets/shaders/m2_particle.vert.spv
new file mode 100644
index 00000000..eee59cf8
Binary files /dev/null and b/assets/shaders/m2_particle.vert.spv differ
diff --git a/assets/shaders/m2_smoke.frag.glsl b/assets/shaders/m2_smoke.frag.glsl
new file mode 100644
index 00000000..a9b150ec
--- /dev/null
+++ b/assets/shaders/m2_smoke.frag.glsl
@@ -0,0 +1,25 @@
+#version 450
+
+layout(location = 0) in float vLifeRatio;
+layout(location = 1) in float vIsSpark;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ vec2 p = gl_PointCoord - vec2(0.5);
+ float dist = length(p);
+ if (dist > 0.5) discard;
+
+ if (vIsSpark > 0.5) {
+ float glow = smoothstep(0.5, 0.0, dist);
+ float life = 1.0 - vLifeRatio;
+ vec3 color = mix(vec3(1.0, 0.6, 0.1), vec3(1.0, 0.2, 0.0), vLifeRatio);
+ outColor = vec4(color * glow, glow * life);
+ } else {
+ float edge = smoothstep(0.5, 0.3, dist);
+ float fadeIn = smoothstep(0.0, 0.2, vLifeRatio);
+ float fadeOut = 1.0 - smoothstep(0.6, 1.0, vLifeRatio);
+ float alpha = edge * fadeIn * fadeOut * 0.4;
+ outColor = vec4(vec3(0.5), alpha);
+ }
+}
diff --git a/assets/shaders/m2_smoke.frag.spv b/assets/shaders/m2_smoke.frag.spv
new file mode 100644
index 00000000..b6ff6c1b
Binary files /dev/null and b/assets/shaders/m2_smoke.frag.spv differ
diff --git a/assets/shaders/m2_smoke.vert.glsl b/assets/shaders/m2_smoke.vert.glsl
new file mode 100644
index 00000000..6eed1ea0
--- /dev/null
+++ b/assets/shaders/m2_smoke.vert.glsl
@@ -0,0 +1,36 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(push_constant) uniform Push {
+ float screenHeight;
+} push;
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in float aLifeRatio;
+layout(location = 2) in float aSize;
+layout(location = 3) in float aIsSpark;
+
+layout(location = 0) out float vLifeRatio;
+layout(location = 1) out float vIsSpark;
+
+void main() {
+ vec4 viewPos4 = view * vec4(aPos, 1.0);
+ float dist = -viewPos4.z;
+ float scale = aIsSpark > 0.5 ? 0.12 : 0.3;
+ gl_PointSize = clamp(aSize * scale * push.screenHeight / max(dist, 1.0), 1.0, 64.0);
+ vLifeRatio = aLifeRatio;
+ vIsSpark = aIsSpark;
+ gl_Position = projection * viewPos4;
+}
diff --git a/assets/shaders/m2_smoke.vert.spv b/assets/shaders/m2_smoke.vert.spv
new file mode 100644
index 00000000..a50b5168
Binary files /dev/null and b/assets/shaders/m2_smoke.vert.spv differ
diff --git a/assets/shaders/minimap_display.frag.glsl b/assets/shaders/minimap_display.frag.glsl
new file mode 100644
index 00000000..ff0477ae
--- /dev/null
+++ b/assets/shaders/minimap_display.frag.glsl
@@ -0,0 +1,68 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform sampler2D uComposite;
+
+layout(push_constant) uniform Push {
+ vec4 rect;
+ vec2 playerUV;
+ float rotation;
+ float arrowRotation;
+ float zoomRadius;
+ int squareShape;
+ float opacity;
+} push;
+
+layout(location = 0) in vec2 TexCoord;
+
+layout(location = 0) out vec4 outColor;
+
+float cross2d(vec2 a, vec2 b) {
+ return a.x * b.y - a.y * b.x;
+}
+
+bool pointInTriangle(vec2 p, vec2 a, vec2 b, vec2 c) {
+ float d1 = cross2d(b - a, p - a);
+ float d2 = cross2d(c - b, p - b);
+ float d3 = cross2d(a - c, p - c);
+ bool hasNeg = (d1 < 0.0) || (d2 < 0.0) || (d3 < 0.0);
+ bool hasPos = (d1 > 0.0) || (d2 > 0.0) || (d3 > 0.0);
+ return !(hasNeg && hasPos);
+}
+
+void main() {
+ vec2 center = TexCoord - 0.5;
+ float dist = length(center);
+
+ if (push.squareShape == 0) {
+ if (dist > 0.5) discard;
+ }
+
+ float cs = cos(push.rotation);
+ float sn = sin(push.rotation);
+ vec2 rotated = vec2(center.x * cs - center.y * sn, center.x * sn + center.y * cs);
+ vec2 mapUV = push.playerUV + rotated * push.zoomRadius * 2.0;
+
+ vec4 mapColor = texture(uComposite, mapUV);
+
+ // Player arrow
+ float acs = cos(push.arrowRotation);
+ float asn = sin(push.arrowRotation);
+ vec2 ac = center;
+ vec2 arrowPos = vec2(ac.x * acs - ac.y * asn, ac.x * asn + ac.y * acs);
+
+ vec2 tip = vec2(0.0, -0.04);
+ vec2 left = vec2(-0.02, 0.02);
+ vec2 right = vec2(0.02, 0.02);
+
+ if (pointInTriangle(arrowPos, tip, left, right)) {
+ mapColor = vec4(1.0, 0.8, 0.0, 1.0);
+ }
+
+ // Dark border ring
+ float border = smoothstep(0.48, 0.5, dist);
+ if (push.squareShape == 0) {
+ mapColor.rgb *= 1.0 - border * 0.7;
+ }
+
+ outColor = vec4(mapColor.rgb, mapColor.a * push.opacity);
+}
diff --git a/assets/shaders/minimap_display.frag.spv b/assets/shaders/minimap_display.frag.spv
new file mode 100644
index 00000000..5fd812cd
Binary files /dev/null and b/assets/shaders/minimap_display.frag.spv differ
diff --git a/assets/shaders/minimap_display.vert.glsl b/assets/shaders/minimap_display.vert.glsl
new file mode 100644
index 00000000..4c03e80c
--- /dev/null
+++ b/assets/shaders/minimap_display.vert.glsl
@@ -0,0 +1,16 @@
+#version 450
+
+layout(push_constant) uniform Push {
+ vec4 rect; // x, y, w, h in 0..1 screen space
+} push;
+
+layout(location = 0) in vec2 aPos;
+layout(location = 1) in vec2 aUV;
+
+layout(location = 0) out vec2 TexCoord;
+
+void main() {
+ TexCoord = aUV;
+ vec2 screenPos = push.rect.xy + aPos * push.rect.zw;
+ gl_Position = vec4(screenPos * 2.0 - 1.0, 0.0, 1.0);
+}
diff --git a/assets/shaders/minimap_display.vert.spv b/assets/shaders/minimap_display.vert.spv
new file mode 100644
index 00000000..6bab4d92
Binary files /dev/null and b/assets/shaders/minimap_display.vert.spv differ
diff --git a/assets/shaders/minimap_tile.frag.glsl b/assets/shaders/minimap_tile.frag.glsl
new file mode 100644
index 00000000..04d04c65
--- /dev/null
+++ b/assets/shaders/minimap_tile.frag.glsl
@@ -0,0 +1,11 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform sampler2D uTileTexture;
+
+layout(location = 0) in vec2 TexCoord;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ outColor = texture(uTileTexture, vec2(TexCoord.y, TexCoord.x));
+}
diff --git a/assets/shaders/minimap_tile.frag.spv b/assets/shaders/minimap_tile.frag.spv
new file mode 100644
index 00000000..37106faf
Binary files /dev/null and b/assets/shaders/minimap_tile.frag.spv differ
diff --git a/assets/shaders/minimap_tile.vert.glsl b/assets/shaders/minimap_tile.vert.glsl
new file mode 100644
index 00000000..44c41d34
--- /dev/null
+++ b/assets/shaders/minimap_tile.vert.glsl
@@ -0,0 +1,17 @@
+#version 450
+
+layout(push_constant) uniform Push {
+ vec2 gridOffset;
+} push;
+
+layout(location = 0) in vec2 aPos;
+layout(location = 1) in vec2 aUV;
+
+layout(location = 0) out vec2 TexCoord;
+
+void main() {
+ TexCoord = aUV;
+ vec2 pos = (aPos + push.gridOffset) / 3.0;
+ pos = pos * 2.0 - 1.0;
+ gl_Position = vec4(pos, 0.0, 1.0);
+}
diff --git a/assets/shaders/minimap_tile.vert.spv b/assets/shaders/minimap_tile.vert.spv
new file mode 100644
index 00000000..bb53f1f0
Binary files /dev/null and b/assets/shaders/minimap_tile.vert.spv differ
diff --git a/assets/shaders/mount_dust.frag.glsl b/assets/shaders/mount_dust.frag.glsl
new file mode 100644
index 00000000..699e8d20
--- /dev/null
+++ b/assets/shaders/mount_dust.frag.glsl
@@ -0,0 +1,13 @@
+#version 450
+
+layout(location = 0) in float vAlpha;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ vec2 p = gl_PointCoord - vec2(0.5);
+ float dist = length(p);
+ if (dist > 0.5) discard;
+ float alpha = smoothstep(0.5, 0.1, dist) * vAlpha * 0.4;
+ outColor = vec4(0.7, 0.65, 0.55, alpha);
+}
diff --git a/assets/shaders/mount_dust.frag.spv b/assets/shaders/mount_dust.frag.spv
new file mode 100644
index 00000000..c68b479f
Binary files /dev/null and b/assets/shaders/mount_dust.frag.spv differ
diff --git a/assets/shaders/mount_dust.vert.glsl b/assets/shaders/mount_dust.vert.glsl
new file mode 100644
index 00000000..25e0dea8
--- /dev/null
+++ b/assets/shaders/mount_dust.vert.glsl
@@ -0,0 +1,26 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in float aSize;
+layout(location = 2) in float aAlpha;
+
+layout(location = 0) out float vAlpha;
+
+void main() {
+ gl_PointSize = aSize;
+ vAlpha = aAlpha;
+ gl_Position = projection * view * vec4(aPos, 1.0);
+}
diff --git a/assets/shaders/mount_dust.vert.spv b/assets/shaders/mount_dust.vert.spv
new file mode 100644
index 00000000..2b84488c
Binary files /dev/null and b/assets/shaders/mount_dust.vert.spv differ
diff --git a/assets/shaders/overlay.frag.glsl b/assets/shaders/overlay.frag.glsl
new file mode 100644
index 00000000..2dd4116d
--- /dev/null
+++ b/assets/shaders/overlay.frag.glsl
@@ -0,0 +1,14 @@
+#version 450
+
+// Full-screen color overlay (e.g. underwater tint).
+// Uses postprocess.vert.glsl as vertex shader (fullscreen triangle, no vertex input).
+
+layout(push_constant) uniform Push {
+ vec4 color; // rgb = tint color, a = opacity
+} push;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ outColor = push.color;
+}
diff --git a/assets/shaders/overlay.frag.spv b/assets/shaders/overlay.frag.spv
new file mode 100644
index 00000000..da839bab
Binary files /dev/null and b/assets/shaders/overlay.frag.spv differ
diff --git a/assets/shaders/postprocess.frag.glsl b/assets/shaders/postprocess.frag.glsl
new file mode 100644
index 00000000..ad47ac8a
--- /dev/null
+++ b/assets/shaders/postprocess.frag.glsl
@@ -0,0 +1,20 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform sampler2D uScene;
+
+layout(location = 0) in vec2 TexCoord;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ vec3 hdr = texture(uScene, TexCoord).rgb;
+ // Shoulder tone map
+ vec3 mapped = hdr;
+ for (int i = 0; i < 3; i++) {
+ if (mapped[i] > 0.9) {
+ float excess = mapped[i] - 0.9;
+ mapped[i] = 0.9 + 0.1 * excess / (excess + 0.1);
+ }
+ }
+ outColor = vec4(mapped, 1.0);
+}
diff --git a/assets/shaders/postprocess.frag.spv b/assets/shaders/postprocess.frag.spv
new file mode 100644
index 00000000..20dfedd1
Binary files /dev/null and b/assets/shaders/postprocess.frag.spv differ
diff --git a/assets/shaders/postprocess.vert.glsl b/assets/shaders/postprocess.vert.glsl
new file mode 100644
index 00000000..aa78b1b5
--- /dev/null
+++ b/assets/shaders/postprocess.vert.glsl
@@ -0,0 +1,10 @@
+#version 450
+
+layout(location = 0) out vec2 TexCoord;
+
+void main() {
+ // Fullscreen triangle trick: 3 vertices, no vertex buffer
+ TexCoord = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
+ gl_Position = vec4(TexCoord * 2.0 - 1.0, 0.0, 1.0);
+ TexCoord.y = 1.0 - TexCoord.y; // flip Y for Vulkan
+}
diff --git a/assets/shaders/postprocess.vert.spv b/assets/shaders/postprocess.vert.spv
new file mode 100644
index 00000000..afc10472
Binary files /dev/null and b/assets/shaders/postprocess.vert.spv differ
diff --git a/assets/shaders/quest_marker.frag.glsl b/assets/shaders/quest_marker.frag.glsl
new file mode 100644
index 00000000..020b625d
--- /dev/null
+++ b/assets/shaders/quest_marker.frag.glsl
@@ -0,0 +1,18 @@
+#version 450
+
+layout(set = 1, binding = 0) uniform sampler2D markerTexture;
+
+layout(push_constant) uniform Push {
+ mat4 model;
+ float alpha;
+} push;
+
+layout(location = 0) in vec2 TexCoord;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ vec4 texColor = texture(markerTexture, TexCoord);
+ if (texColor.a < 0.1) discard;
+ outColor = vec4(texColor.rgb, texColor.a * push.alpha);
+}
diff --git a/assets/shaders/quest_marker.frag.spv b/assets/shaders/quest_marker.frag.spv
new file mode 100644
index 00000000..e947d04c
Binary files /dev/null and b/assets/shaders/quest_marker.frag.spv differ
diff --git a/assets/shaders/quest_marker.vert.glsl b/assets/shaders/quest_marker.vert.glsl
new file mode 100644
index 00000000..8525181a
--- /dev/null
+++ b/assets/shaders/quest_marker.vert.glsl
@@ -0,0 +1,28 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(push_constant) uniform Push {
+ mat4 model;
+} push;
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in vec2 aTexCoord;
+
+layout(location = 0) out vec2 TexCoord;
+
+void main() {
+ TexCoord = aTexCoord;
+ gl_Position = projection * view * push.model * vec4(aPos, 1.0);
+}
diff --git a/assets/shaders/quest_marker.vert.spv b/assets/shaders/quest_marker.vert.spv
new file mode 100644
index 00000000..f414a916
Binary files /dev/null and b/assets/shaders/quest_marker.vert.spv differ
diff --git a/assets/shaders/selection_circle.frag.glsl b/assets/shaders/selection_circle.frag.glsl
new file mode 100644
index 00000000..dbddfaff
--- /dev/null
+++ b/assets/shaders/selection_circle.frag.glsl
@@ -0,0 +1,19 @@
+#version 450
+
+layout(push_constant) uniform Push {
+ mat4 mvp;
+ vec4 color;
+} push;
+
+layout(location = 0) in vec2 vLocalPos;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ float r = length(vLocalPos);
+ float ring = smoothstep(0.93, 0.97, r) * smoothstep(1.0, 0.97, r);
+ float inward = (1.0 - smoothstep(0.0, 0.93, r)) * 0.15;
+ float alpha = max(ring, inward);
+ if (alpha < 0.01) discard;
+ outColor = vec4(push.color.rgb, alpha);
+}
diff --git a/assets/shaders/selection_circle.frag.spv b/assets/shaders/selection_circle.frag.spv
new file mode 100644
index 00000000..0cafe9ad
Binary files /dev/null and b/assets/shaders/selection_circle.frag.spv differ
diff --git a/assets/shaders/selection_circle.vert.glsl b/assets/shaders/selection_circle.vert.glsl
new file mode 100644
index 00000000..1edd1b2e
--- /dev/null
+++ b/assets/shaders/selection_circle.vert.glsl
@@ -0,0 +1,14 @@
+#version 450
+
+layout(push_constant) uniform Push {
+ mat4 mvp;
+} push;
+
+layout(location = 0) in vec3 aPos;
+
+layout(location = 0) out vec2 vLocalPos;
+
+void main() {
+ vLocalPos = aPos.xz;
+ gl_Position = push.mvp * vec4(aPos, 1.0);
+}
diff --git a/assets/shaders/selection_circle.vert.spv b/assets/shaders/selection_circle.vert.spv
new file mode 100644
index 00000000..185d8cc9
Binary files /dev/null and b/assets/shaders/selection_circle.vert.spv differ
diff --git a/assets/shaders/shadow.frag.glsl b/assets/shaders/shadow.frag.glsl
new file mode 100644
index 00000000..986c5166
--- /dev/null
+++ b/assets/shaders/shadow.frag.glsl
@@ -0,0 +1,22 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform sampler2D uTexture;
+
+layout(set = 0, binding = 1) uniform ShadowParams {
+ int useBones;
+ int useTexture;
+ int alphaTest;
+ int foliageSway;
+ float windTime;
+ float foliageMotionDamp;
+};
+
+layout(location = 0) in vec2 TexCoord;
+layout(location = 1) in vec3 WorldPos;
+
+void main() {
+ if (useTexture != 0) {
+ vec4 texColor = textureLod(uTexture, TexCoord, 0.0);
+ if (alphaTest != 0 && texColor.a < 0.5) discard;
+ }
+}
diff --git a/assets/shaders/shadow.frag.spv b/assets/shaders/shadow.frag.spv
new file mode 100644
index 00000000..b14825fc
Binary files /dev/null and b/assets/shaders/shadow.frag.spv differ
diff --git a/assets/shaders/shadow.vert.glsl b/assets/shaders/shadow.vert.glsl
new file mode 100644
index 00000000..f8c64618
--- /dev/null
+++ b/assets/shaders/shadow.vert.glsl
@@ -0,0 +1,57 @@
+#version 450
+
+layout(push_constant) uniform Push {
+ mat4 lightSpaceMatrix;
+ mat4 model;
+} push;
+
+layout(set = 0, binding = 1) uniform ShadowParams {
+ int useBones;
+ int useTexture;
+ int alphaTest;
+ int foliageSway;
+ float windTime;
+ float foliageMotionDamp;
+};
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in vec2 aTexCoord;
+layout(location = 2) in vec4 aBoneWeights;
+layout(location = 3) in vec4 aBoneIndicesF;
+
+layout(location = 0) out vec2 TexCoord;
+layout(location = 1) out vec3 WorldPos;
+
+void main() {
+ vec4 pos = vec4(aPos, 1.0);
+
+ // Wind vertex displacement for foliage (matches m2.vert.glsl)
+ if (foliageSway != 0) {
+ vec3 worldRef = push.model[3].xyz;
+ float heightFactor = clamp(pos.z / 20.0, 0.0, 1.0);
+ heightFactor *= heightFactor;
+
+ // Layer 1: Trunk sway
+ float trunkPhase = windTime * 0.8 + dot(worldRef.xy, vec2(0.1, 0.13));
+ float trunkSwayX = sin(trunkPhase) * 0.35 * heightFactor;
+ float trunkSwayY = cos(trunkPhase * 0.7) * 0.25 * heightFactor;
+
+ // Layer 2: Branch sway
+ float branchPhase = windTime * 1.7 + dot(worldRef.xy, vec2(0.37, 0.71));
+ float branchSwayX = sin(branchPhase + pos.y * 0.4) * 0.15 * heightFactor;
+ float branchSwayY = cos(branchPhase * 1.1 + pos.x * 0.3) * 0.12 * heightFactor;
+
+ // Layer 3: Leaf flutter
+ float leafPhase = windTime * 4.5 + dot(aPos, vec3(1.7, 2.3, 0.9));
+ float leafFlutterX = sin(leafPhase) * 0.06 * heightFactor;
+ float leafFlutterY = cos(leafPhase * 1.3) * 0.05 * heightFactor;
+
+ pos.x += trunkSwayX + branchSwayX + leafFlutterX;
+ pos.y += trunkSwayY + branchSwayY + leafFlutterY;
+ }
+
+ vec4 worldPos = push.model * pos;
+ WorldPos = worldPos.xyz;
+ TexCoord = aTexCoord;
+ gl_Position = push.lightSpaceMatrix * worldPos;
+}
diff --git a/assets/shaders/shadow.vert.spv b/assets/shaders/shadow.vert.spv
new file mode 100644
index 00000000..3399c4cc
Binary files /dev/null and b/assets/shaders/shadow.vert.spv differ
diff --git a/assets/shaders/skybox.frag.glsl b/assets/shaders/skybox.frag.glsl
new file mode 100644
index 00000000..3c4709ee
--- /dev/null
+++ b/assets/shaders/skybox.frag.glsl
@@ -0,0 +1,97 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(push_constant) uniform Push {
+ vec4 zenithColor; // DBC skyTopColor
+ vec4 midColor; // DBC skyMiddleColor
+ vec4 horizonColor; // DBC skyBand1Color
+ vec4 fogColorPush; // DBC skyBand2Color
+ vec4 sunDirAndTime; // xyz = sun direction, w = timeOfDay
+} push;
+
+layout(location = 0) in vec2 TexCoord;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ // Reconstruct world-space ray direction from screen position.
+ float ndcX = TexCoord.x * 2.0 - 1.0;
+ float ndcY = -(TexCoord.y * 2.0 - 1.0);
+
+ vec3 viewDir = vec3(ndcX / projection[0][0],
+ ndcY / abs(projection[1][1]),
+ -1.0);
+
+ mat3 invViewRot = transpose(mat3(view));
+ vec3 worldDir = normalize(invViewRot * viewDir);
+
+ vec3 sunDir = push.sunDirAndTime.xyz;
+ float timeOfDay = push.sunDirAndTime.w;
+
+ // Elevation: +1 = zenith, 0 = horizon, -1 = nadir
+ float elev = worldDir.z;
+ float elevClamped = clamp(elev, 0.0, 1.0);
+
+ // --- 3-band sky gradient using DBC colors ---
+ // Zenith dominates upper sky, mid color fills the middle,
+ // horizon band at the bottom with a thin fog fringe.
+ vec3 sky;
+ if (elevClamped > 0.4) {
+ // Upper sky: mid -> zenith
+ float t = (elevClamped - 0.4) / 0.6;
+ sky = mix(push.midColor.rgb, push.zenithColor.rgb, t);
+ } else if (elevClamped > 0.05) {
+ // Lower sky: horizon -> mid (wide band)
+ float t = (elevClamped - 0.05) / 0.35;
+ sky = mix(push.horizonColor.rgb, push.midColor.rgb, t);
+ } else {
+ // Thin fog fringe right at horizon
+ float t = elevClamped / 0.05;
+ sky = mix(push.fogColorPush.rgb, push.horizonColor.rgb, t);
+ }
+
+ // --- Below-horizon darkening (nadir) ---
+ if (elev < 0.0) {
+ float nadirFade = clamp(-elev * 3.0, 0.0, 1.0);
+ vec3 nadirColor = push.fogColorPush.rgb * 0.3;
+ sky = mix(push.fogColorPush.rgb, nadirColor, nadirFade);
+ }
+
+ // --- Rayleigh-like scattering (subtle warm glow near sun) ---
+ float sunDot = max(dot(worldDir, sunDir), 0.0);
+ float sunAboveHorizon = clamp(sunDir.z, 0.0, 1.0);
+
+ float rayleighStrength = pow(1.0 - elevClamped, 3.0) * 0.15;
+ vec3 scatterColor = mix(vec3(0.8, 0.45, 0.15), vec3(0.3, 0.5, 1.0), elevClamped);
+ sky += scatterColor * rayleighStrength * sunDot * sunAboveHorizon;
+
+ // --- Mie-like forward scatter (sun disk glow) ---
+ float mieSharp = pow(sunDot, 64.0) * 0.4;
+ float mieSoft = pow(sunDot, 8.0) * 0.1;
+ vec3 sunGlowColor = mix(vec3(1.0, 0.85, 0.55), vec3(1.0, 1.0, 0.95), elevClamped);
+ sky += sunGlowColor * (mieSharp + mieSoft) * sunAboveHorizon;
+
+ // --- Subtle horizon haze ---
+ float hazeDensity = exp(-elevClamped * 12.0) * 0.06;
+ sky += push.horizonColor.rgb * hazeDensity * sunAboveHorizon;
+
+ // --- Night: slight moonlight tint ---
+ if (sunDir.z < 0.0) {
+ float moonlight = clamp(-sunDir.z * 0.5, 0.0, 0.15);
+ sky += vec3(0.02, 0.03, 0.08) * moonlight;
+ }
+
+ outColor = vec4(sky, 1.0);
+}
diff --git a/assets/shaders/skybox.frag.spv b/assets/shaders/skybox.frag.spv
new file mode 100644
index 00000000..29865049
Binary files /dev/null and b/assets/shaders/skybox.frag.spv differ
diff --git a/assets/shaders/skybox.vert.glsl b/assets/shaders/skybox.vert.glsl
new file mode 100644
index 00000000..d1df7d5a
--- /dev/null
+++ b/assets/shaders/skybox.vert.glsl
@@ -0,0 +1,12 @@
+#version 450
+
+// Fullscreen triangle sky β no vertex buffer, no mesh.
+// Draws 3 vertices covering the entire screen, depth forced to 1.0 (far plane).
+
+layout(location = 0) out vec2 TexCoord;
+
+void main() {
+ // Produces triangle covering NDC [-1,1]Β² with depth = 1.0 (far)
+ TexCoord = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
+ gl_Position = vec4(TexCoord * 2.0 - 1.0, 1.0, 1.0);
+}
diff --git a/assets/shaders/skybox.vert.spv b/assets/shaders/skybox.vert.spv
new file mode 100644
index 00000000..bc7f0006
Binary files /dev/null and b/assets/shaders/skybox.vert.spv differ
diff --git a/assets/shaders/starfield.frag.glsl b/assets/shaders/starfield.frag.glsl
new file mode 100644
index 00000000..0927c923
--- /dev/null
+++ b/assets/shaders/starfield.frag.glsl
@@ -0,0 +1,13 @@
+#version 450
+
+layout(location = 0) in float vBrightness;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ vec2 p = gl_PointCoord - vec2(0.5);
+ float dist = length(p);
+ if (dist > 0.5) discard;
+ float alpha = vBrightness * smoothstep(0.5, 0.2, dist);
+ outColor = vec4(vec3(0.9, 0.95, 1.0) * vBrightness, alpha);
+}
diff --git a/assets/shaders/starfield.frag.spv b/assets/shaders/starfield.frag.spv
new file mode 100644
index 00000000..d572951f
Binary files /dev/null and b/assets/shaders/starfield.frag.spv differ
diff --git a/assets/shaders/starfield.vert.glsl b/assets/shaders/starfield.vert.glsl
new file mode 100644
index 00000000..cb6f67c9
--- /dev/null
+++ b/assets/shaders/starfield.vert.glsl
@@ -0,0 +1,33 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(push_constant) uniform Push {
+ float time;
+ float intensity;
+} push;
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in float aBrightness;
+layout(location = 2) in float aTwinklePhase;
+
+layout(location = 0) out float vBrightness;
+
+void main() {
+ mat4 rotView = mat4(mat3(view));
+ float twinkle = 0.7 + 0.3 * sin(push.time * 1.5 + aTwinklePhase);
+ vBrightness = aBrightness * twinkle * push.intensity;
+ gl_PointSize = mix(2.0, 4.0, aBrightness);
+ gl_Position = projection * rotView * vec4(aPos, 1.0);
+}
diff --git a/assets/shaders/starfield.vert.spv b/assets/shaders/starfield.vert.spv
new file mode 100644
index 00000000..1f5bc30c
Binary files /dev/null and b/assets/shaders/starfield.vert.spv differ
diff --git a/assets/shaders/swim_bubble.frag.glsl b/assets/shaders/swim_bubble.frag.glsl
new file mode 100644
index 00000000..6270eb51
--- /dev/null
+++ b/assets/shaders/swim_bubble.frag.glsl
@@ -0,0 +1,15 @@
+#version 450
+
+layout(location = 0) in float vAlpha;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ vec2 p = gl_PointCoord - vec2(0.5);
+ float dist = length(p);
+ if (dist > 0.5) discard;
+ float ring = smoothstep(0.5, 0.4, dist) - smoothstep(0.38, 0.28, dist);
+ float highlight = smoothstep(0.3, 0.1, length(p - vec2(-0.15, 0.15))) * 0.5;
+ float alpha = (ring + highlight) * vAlpha;
+ outColor = vec4(0.8, 0.9, 1.0, alpha);
+}
diff --git a/assets/shaders/swim_bubble.frag.spv b/assets/shaders/swim_bubble.frag.spv
new file mode 100644
index 00000000..a0374c36
Binary files /dev/null and b/assets/shaders/swim_bubble.frag.spv differ
diff --git a/assets/shaders/swim_bubble.vert.glsl b/assets/shaders/swim_bubble.vert.glsl
new file mode 100644
index 00000000..25e0dea8
--- /dev/null
+++ b/assets/shaders/swim_bubble.vert.glsl
@@ -0,0 +1,26 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in float aSize;
+layout(location = 2) in float aAlpha;
+
+layout(location = 0) out float vAlpha;
+
+void main() {
+ gl_PointSize = aSize;
+ vAlpha = aAlpha;
+ gl_Position = projection * view * vec4(aPos, 1.0);
+}
diff --git a/assets/shaders/swim_bubble.vert.spv b/assets/shaders/swim_bubble.vert.spv
new file mode 100644
index 00000000..2b84488c
Binary files /dev/null and b/assets/shaders/swim_bubble.vert.spv differ
diff --git a/assets/shaders/swim_insect.frag.glsl b/assets/shaders/swim_insect.frag.glsl
new file mode 100644
index 00000000..06ab430a
--- /dev/null
+++ b/assets/shaders/swim_insect.frag.glsl
@@ -0,0 +1,14 @@
+#version 450
+
+layout(location = 0) in float vAlpha;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ vec2 p = gl_PointCoord - vec2(0.5);
+ float dist = length(p);
+ if (dist > 0.5) discard;
+ float alpha = smoothstep(0.5, 0.05, dist) * vAlpha;
+ // Dark brown/black insect color
+ outColor = vec4(0.12, 0.08, 0.05, alpha);
+}
diff --git a/assets/shaders/swim_insect.frag.spv b/assets/shaders/swim_insect.frag.spv
new file mode 100644
index 00000000..6e849c37
Binary files /dev/null and b/assets/shaders/swim_insect.frag.spv differ
diff --git a/assets/shaders/swim_ripple.frag.glsl b/assets/shaders/swim_ripple.frag.glsl
new file mode 100644
index 00000000..43219998
--- /dev/null
+++ b/assets/shaders/swim_ripple.frag.glsl
@@ -0,0 +1,13 @@
+#version 450
+
+layout(location = 0) in float vAlpha;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ vec2 p = gl_PointCoord - vec2(0.5);
+ float dist = length(p);
+ if (dist > 0.5) discard;
+ float alpha = smoothstep(0.5, 0.1, dist) * vAlpha;
+ outColor = vec4(0.85, 0.92, 1.0, alpha);
+}
diff --git a/assets/shaders/swim_ripple.frag.spv b/assets/shaders/swim_ripple.frag.spv
new file mode 100644
index 00000000..84a69295
Binary files /dev/null and b/assets/shaders/swim_ripple.frag.spv differ
diff --git a/assets/shaders/swim_ripple.vert.glsl b/assets/shaders/swim_ripple.vert.glsl
new file mode 100644
index 00000000..25e0dea8
--- /dev/null
+++ b/assets/shaders/swim_ripple.vert.glsl
@@ -0,0 +1,26 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in float aSize;
+layout(location = 2) in float aAlpha;
+
+layout(location = 0) out float vAlpha;
+
+void main() {
+ gl_PointSize = aSize;
+ vAlpha = aAlpha;
+ gl_Position = projection * view * vec4(aPos, 1.0);
+}
diff --git a/assets/shaders/swim_ripple.vert.spv b/assets/shaders/swim_ripple.vert.spv
new file mode 100644
index 00000000..2b84488c
Binary files /dev/null and b/assets/shaders/swim_ripple.vert.spv differ
diff --git a/assets/shaders/terrain.frag.glsl b/assets/shaders/terrain.frag.glsl
new file mode 100644
index 00000000..0a424090
--- /dev/null
+++ b/assets/shaders/terrain.frag.glsl
@@ -0,0 +1,132 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(set = 1, binding = 0) uniform sampler2D uBaseTexture;
+layout(set = 1, binding = 1) uniform sampler2D uLayer1Texture;
+layout(set = 1, binding = 2) uniform sampler2D uLayer2Texture;
+layout(set = 1, binding = 3) uniform sampler2D uLayer3Texture;
+layout(set = 1, binding = 4) uniform sampler2D uLayer1Alpha;
+layout(set = 1, binding = 5) uniform sampler2D uLayer2Alpha;
+layout(set = 1, binding = 6) uniform sampler2D uLayer3Alpha;
+
+layout(set = 1, binding = 7) uniform TerrainParams {
+ int layerCount;
+ int hasLayer1;
+ int hasLayer2;
+ int hasLayer3;
+};
+
+layout(set = 0, binding = 1) uniform sampler2DShadow uShadowMap;
+
+layout(location = 0) in vec3 FragPos;
+layout(location = 1) in vec3 Normal;
+layout(location = 2) in vec2 TexCoord;
+layout(location = 3) in vec2 LayerUV;
+
+layout(location = 0) out vec4 outColor;
+
+const float SHADOW_TEXEL = 1.0 / 4096.0;
+
+float sampleShadowPCF(sampler2DShadow smap, vec3 coords) {
+ float shadow = 0.0;
+ for (int x = -1; x <= 1; ++x) {
+ for (int y = -1; y <= 1; ++y) {
+ shadow += texture(smap, vec3(coords.xy + vec2(x, y) * SHADOW_TEXEL, coords.z));
+ }
+ }
+ return shadow / 9.0;
+}
+
+float sampleAlpha(sampler2D tex, vec2 uv) {
+ vec2 edge = min(uv, 1.0 - uv);
+ float border = min(edge.x, edge.y);
+ float doBlur = step(border, 2.0 / 64.0);
+ if (doBlur < 0.5) {
+ return texture(tex, uv).r;
+ }
+ vec2 texel = vec2(1.0 / 64.0);
+ float a = 0.0;
+ a += texture(tex, uv + vec2(-texel.x, 0.0)).r;
+ a += texture(tex, uv + vec2(texel.x, 0.0)).r;
+ a += texture(tex, uv + vec2(0.0, -texel.y)).r;
+ a += texture(tex, uv + vec2(0.0, texel.y)).r;
+ return a * 0.25;
+}
+
+void main() {
+ vec4 baseColor = texture(uBaseTexture, TexCoord);
+
+ // WoW terrain: layers are blended sequentially, each on top of the previous result.
+ // Alpha=1 means the layer fully covers everything below; alpha=0 means invisible.
+ vec4 finalColor = baseColor;
+ if (hasLayer1 != 0) {
+ float a1 = sampleAlpha(uLayer1Alpha, LayerUV);
+ finalColor = mix(finalColor, texture(uLayer1Texture, TexCoord), a1);
+ }
+ if (hasLayer2 != 0) {
+ float a2 = sampleAlpha(uLayer2Alpha, LayerUV);
+ finalColor = mix(finalColor, texture(uLayer2Texture, TexCoord), a2);
+ }
+ if (hasLayer3 != 0) {
+ float a3 = sampleAlpha(uLayer3Alpha, LayerUV);
+ finalColor = mix(finalColor, texture(uLayer3Texture, TexCoord), a3);
+ }
+
+ vec3 norm = normalize(Normal);
+
+ // Derivative-based normal mapping: perturb vertex normal using texture detail.
+ // Fade out with distance β looks noisy/harsh beyond ~100 units.
+ float fragDist = length(viewPos.xyz - FragPos);
+ float bumpFade = 1.0 - smoothstep(50.0, 125.0, fragDist);
+ if (bumpFade > 0.001) {
+ float lum = dot(finalColor.rgb, vec3(0.299, 0.587, 0.114));
+ float dLdx = dFdx(lum);
+ float dLdy = dFdy(lum);
+ vec3 dpdx = dFdx(FragPos);
+ vec3 dpdy = dFdy(FragPos);
+ float bumpStrength = 9.0 * bumpFade;
+ vec3 perturbation = (dLdx * cross(norm, dpdy) + dLdy * cross(dpdx, norm)) * bumpStrength;
+ vec3 candidate = norm - perturbation;
+ float len2 = dot(candidate, candidate);
+ norm = (len2 > 1e-8) ? candidate * inversesqrt(len2) : norm;
+ }
+
+ vec3 lightDir2 = normalize(-lightDir.xyz);
+ vec3 ambient = ambientColor.rgb * finalColor.rgb;
+ float diff = max(abs(dot(norm, lightDir2)), 0.2);
+ vec3 diffuse = diff * lightColor.rgb * finalColor.rgb;
+
+ float shadow = 1.0;
+ if (shadowParams.x > 0.5) {
+ vec3 ldir = normalize(-lightDir.xyz);
+ float normalOffset = SHADOW_TEXEL * 2.0 * (1.0 - abs(dot(norm, ldir)));
+ vec3 biasedPos = FragPos + norm * normalOffset;
+ vec4 lsPos = lightSpaceMatrix * vec4(biasedPos, 1.0);
+ vec3 proj = lsPos.xyz / lsPos.w;
+ proj.xy = proj.xy * 0.5 + 0.5;
+ if (proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0 && proj.z <= 1.0) {
+ float bias = 0.0002;
+ shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias));
+ shadow = mix(1.0, shadow, shadowParams.y);
+ }
+ }
+
+ vec3 result = ambient + shadow * diffuse;
+
+ float fogFactor = clamp((fogParams.y - fragDist) / (fogParams.y - fogParams.x), 0.0, 1.0);
+ result = mix(fogColor.rgb, result, fogFactor);
+
+ outColor = vec4(result, 1.0);
+}
diff --git a/assets/shaders/terrain.frag.spv b/assets/shaders/terrain.frag.spv
new file mode 100644
index 00000000..d8f3d684
Binary files /dev/null and b/assets/shaders/terrain.frag.spv differ
diff --git a/assets/shaders/terrain.vert.glsl b/assets/shaders/terrain.vert.glsl
new file mode 100644
index 00000000..d05ba494
--- /dev/null
+++ b/assets/shaders/terrain.vert.glsl
@@ -0,0 +1,37 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(push_constant) uniform Push {
+ mat4 model;
+} push;
+
+layout(location = 0) in vec3 aPosition;
+layout(location = 1) in vec3 aNormal;
+layout(location = 2) in vec2 aTexCoord;
+layout(location = 3) in vec2 aLayerUV;
+
+layout(location = 0) out vec3 FragPos;
+layout(location = 1) out vec3 Normal;
+layout(location = 2) out vec2 TexCoord;
+layout(location = 3) out vec2 LayerUV;
+
+void main() {
+ vec4 worldPos = push.model * vec4(aPosition, 1.0);
+ FragPos = worldPos.xyz;
+ Normal = aNormal;
+ TexCoord = aTexCoord;
+ LayerUV = aLayerUV;
+ gl_Position = projection * view * worldPos;
+}
diff --git a/assets/shaders/terrain.vert.spv b/assets/shaders/terrain.vert.spv
new file mode 100644
index 00000000..a21d68a5
Binary files /dev/null and b/assets/shaders/terrain.vert.spv differ
diff --git a/assets/shaders/water.frag.glsl b/assets/shaders/water.frag.glsl
new file mode 100644
index 00000000..ecd7ee1d
--- /dev/null
+++ b/assets/shaders/water.frag.glsl
@@ -0,0 +1,364 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(push_constant) uniform Push {
+ mat4 model;
+ float waveAmp;
+ float waveFreq;
+ float waveSpeed;
+ float liquidBasicType;
+} push;
+
+layout(set = 1, binding = 0) uniform WaterMaterial {
+ vec4 waterColor;
+ float waterAlpha;
+ float shimmerStrength;
+ float alphaScale;
+};
+
+layout(set = 2, binding = 0) uniform sampler2D SceneColor;
+layout(set = 2, binding = 1) uniform sampler2D SceneDepth;
+layout(set = 2, binding = 2) uniform sampler2D ReflectionColor;
+layout(set = 2, binding = 3) uniform ReflectionData {
+ mat4 reflViewProj;
+};
+
+layout(location = 0) in vec3 FragPos;
+layout(location = 1) in vec3 Normal;
+layout(location = 2) in vec2 TexCoord;
+layout(location = 3) in float WaveOffset;
+layout(location = 4) in vec2 ScreenUV;
+
+layout(location = 0) out vec4 outColor;
+
+// ============================================================
+// Dual-scroll detail normals (multi-octave ripple overlay)
+// ============================================================
+vec3 dualScrollWaveNormal(vec2 p, float time) {
+ vec2 d1 = normalize(vec2(0.86, 0.51));
+ vec2 d2 = normalize(vec2(-0.47, 0.88));
+ vec2 d3 = normalize(vec2(0.32, -0.95));
+ float f1 = 0.19, f2 = 0.43, f3 = 0.72;
+ float s1 = 0.95, s2 = 1.73, s3 = 2.40;
+ float a1 = 0.22, a2 = 0.10, a3 = 0.05;
+
+ vec2 p1 = p + d1 * (time * s1 * 4.0);
+ vec2 p2 = p + d2 * (time * s2 * 4.0);
+ vec2 p3 = p + d3 * (time * s3 * 4.0);
+
+ float c1 = cos(dot(p1, d1) * f1);
+ float c2 = cos(dot(p2, d2) * f2);
+ float c3 = cos(dot(p3, d3) * f3);
+
+ float dHx = c1 * d1.x * f1 * a1 + c2 * d2.x * f2 * a2 + c3 * d3.x * f3 * a3;
+ float dHy = c1 * d1.y * f1 * a1 + c2 * d2.y * f2 * a2 + c3 * d3.y * f3 * a3;
+
+ return normalize(vec3(-dHx, -dHy, 1.0));
+}
+
+// ============================================================
+// GGX/Cook-Torrance BRDF
+// ============================================================
+float DistributionGGX(vec3 N, vec3 H, float roughness) {
+ float a = roughness * roughness;
+ float a2 = a * a;
+ float NdotH = max(dot(N, H), 0.0);
+ float NdotH2 = NdotH * NdotH;
+ float denom = NdotH2 * (a2 - 1.0) + 1.0;
+ return a2 / (3.14159265 * denom * denom + 1e-7);
+}
+
+float GeometrySmith(float NdotV, float NdotL, float roughness) {
+ float r = roughness + 1.0;
+ float k = (r * r) / 8.0;
+ float ggx1 = NdotV / (NdotV * (1.0 - k) + k);
+ float ggx2 = NdotL / (NdotL * (1.0 - k) + k);
+ return ggx1 * ggx2;
+}
+
+vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {
+ return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
+}
+
+// ============================================================
+// Linearize depth
+// ============================================================
+float linearizeDepth(float d, float near, float far) {
+ return near * far / (far - d * (far - near));
+}
+
+// ============================================================
+// Noise functions for foam
+// ============================================================
+float hash21(vec2 p) {
+ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
+}
+
+float hash22x(vec2 p) {
+ return fract(sin(dot(p, vec2(269.5, 183.3))) * 43758.5453);
+}
+
+float noiseValue(vec2 p) {
+ vec2 i = floor(p);
+ vec2 f = fract(p);
+ f = f * f * (3.0 - 2.0 * f);
+ float a = hash21(i);
+ float b = hash21(i + vec2(1.0, 0.0));
+ float c = hash21(i + vec2(0.0, 1.0));
+ float d = hash21(i + vec2(1.0, 1.0));
+ return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
+}
+
+float fbmNoise(vec2 p, float time) {
+ float v = 0.0;
+ v += noiseValue(p * 3.0 + time * 0.3) * 0.5;
+ v += noiseValue(p * 6.0 - time * 0.5) * 0.25;
+ v += noiseValue(p * 12.0 + time * 0.7) * 0.125;
+ return v;
+}
+
+// Voronoi-like cellular noise for foam particles
+// jitter parameter controls how much cell points deviate from grid centers
+// (0.0 = regular grid, 1.0 = fully random within cell)
+float cellularFoam(vec2 p, float jitter) {
+ vec2 i = floor(p);
+ vec2 f = fract(p);
+ float minDist = 1.0;
+ for (int y = -1; y <= 1; y++) {
+ for (int x = -1; x <= 1; x++) {
+ vec2 neighbor = vec2(float(x), float(y));
+ vec2 cellId = i + neighbor;
+ // Jittered cell point β higher jitter = more irregular placement
+ vec2 point = vec2(hash21(cellId), hash22x(cellId)) * jitter
+ + vec2(0.5) * (1.0 - jitter);
+ float d = length(neighbor + point - f);
+ minDist = min(minDist, d);
+ }
+ }
+ return minDist;
+}
+float cellularFoam(vec2 p) { return cellularFoam(p, 1.0); }
+
+void main() {
+ float time = fogParams.z;
+ float basicType = push.liquidBasicType;
+
+ vec2 screenUV = gl_FragCoord.xy / vec2(textureSize(SceneColor, 0));
+
+ // --- Normal computation ---
+ vec3 meshNorm = normalize(Normal);
+ vec3 detailNorm = dualScrollWaveNormal(FragPos.xy, time);
+ vec3 norm = normalize(mix(meshNorm, detailNorm, 0.55));
+
+ // Player interaction ripple normal perturbation
+ vec2 playerPos = vec2(shadowParams.z, shadowParams.w);
+ float rippleStrength = fogParams.w;
+ float d = length(FragPos.xy - playerPos);
+ float rippleEnv = rippleStrength * exp(-d * 0.12);
+ if (rippleEnv > 0.001) {
+ vec2 radialDir = (FragPos.xy - playerPos) / max(d, 0.01);
+ float dHdr = rippleEnv * 0.12 * (-0.12 * sin(d * 2.5 - time * 6.0) + 2.5 * cos(d * 2.5 - time * 6.0));
+ norm = normalize(norm + vec3(-radialDir * dHdr, 0.0));
+ }
+
+ vec3 viewDir = normalize(viewPos.xyz - FragPos);
+ vec3 ldir = normalize(-lightDir.xyz);
+ float NdotV = max(dot(norm, viewDir), 0.001);
+ float NdotL = max(dot(norm, ldir), 0.0);
+
+ float dist = length(viewPos.xyz - FragPos);
+
+ // --- Schlick Fresnel ---
+ const vec3 F0 = vec3(0.02);
+ float fresnel = F0.x + (1.0 - F0.x) * pow(1.0 - NdotV, 5.0);
+
+ // ============================================================
+ // Refraction (screen-space from scene history)
+ // ============================================================
+ vec2 refractOffset = norm.xy * (0.02 + 0.03 * fresnel);
+ vec2 refractUV = clamp(screenUV + refractOffset, vec2(0.001), vec2(0.999));
+ vec3 sceneRefract = texture(SceneColor, refractUV).rgb;
+
+ float sceneDepth = texture(SceneDepth, refractUV).r;
+
+ float near = 0.05;
+ float far = 30000.0;
+ float sceneLinDepth = linearizeDepth(sceneDepth, near, far);
+ float waterLinDepth = linearizeDepth(gl_FragCoord.z, near, far);
+ float depthDiff = max(sceneLinDepth - waterLinDepth, 0.0);
+
+ // Convert screen-space depth difference to approximate vertical water depth.
+ // depthDiff is along the view ray; multiply by the vertical component of
+ // the view direction so grazing angles don't falsely trigger shoreline foam
+ // on occluding geometry (bridges, posts) that isn't at the waterline.
+ float verticalFactor = abs(viewDir.z); // 1.0 looking straight down, ~0 at grazing
+ float verticalDepth = depthDiff * max(verticalFactor, 0.05);
+
+ // ============================================================
+ // Beer-Lambert absorption
+ // ============================================================
+ vec3 absorptionCoeff = vec3(0.46, 0.09, 0.06);
+ if (basicType > 0.5 && basicType < 1.5) {
+ absorptionCoeff = vec3(0.35, 0.06, 0.04);
+ }
+ vec3 absorbed = exp(-absorptionCoeff * verticalDepth);
+
+ // Underwater blue fog β geometry below the waterline fades to a blue haze
+ // with depth, masking occlusion edge artifacts and giving a natural look.
+ vec3 underwaterFogColor = waterColor.rgb * 0.5 + vec3(0.04, 0.10, 0.20);
+ float underwaterFogFade = 1.0 - exp(-verticalDepth * 0.35);
+ vec3 foggedScene = mix(sceneRefract, underwaterFogColor, underwaterFogFade);
+
+ vec3 shallowColor = waterColor.rgb * 1.2;
+ vec3 deepColor = waterColor.rgb * vec3(0.3, 0.5, 0.7);
+ float depthFade = 1.0 - exp(-verticalDepth * 0.15);
+ vec3 waterBody = mix(shallowColor, deepColor, depthFade);
+
+ vec3 refractedColor = mix(foggedScene * absorbed, waterBody, depthFade * 0.7);
+
+ if (verticalDepth < 0.01) {
+ float opticalDepth = 1.0 - exp(-dist * 0.004);
+ refractedColor = mix(foggedScene, waterBody, opticalDepth * 0.6);
+ }
+
+ vec3 litBase = waterBody * (ambientColor.rgb * 0.7 + NdotL * lightColor.rgb * 0.5);
+ refractedColor = mix(refractedColor, litBase, clamp(depthFade * 0.3, 0.0, 0.5));
+
+ // ============================================================
+ // Planar reflection β subtle, not mirror-like
+ // ============================================================
+ // reflWeight starts at 0; only contributes where we have valid reflection data
+ float reflAmount = 0.0;
+ vec3 envReflect = vec3(0.0);
+
+ vec4 reflClip = reflViewProj * vec4(FragPos, 1.0);
+ if (reflClip.w > 0.1) {
+ vec2 reflUV = reflClip.xy / reflClip.w * 0.5 + 0.5;
+ reflUV.y = 1.0 - reflUV.y;
+ reflUV += norm.xy * 0.015;
+
+ // Wide fade so there's no visible boundary β fully gone well inside the edge
+ float edgeFade = smoothstep(0.0, 0.15, reflUV.x) * smoothstep(1.0, 0.85, reflUV.x)
+ * smoothstep(0.0, 0.15, reflUV.y) * smoothstep(1.0, 0.85, reflUV.y);
+
+ reflUV = clamp(reflUV, vec2(0.002), vec2(0.998));
+ vec3 texReflect = texture(ReflectionColor, reflUV).rgb;
+
+ float reflBrightness = dot(texReflect, vec3(0.299, 0.587, 0.114));
+ float reflValidity = smoothstep(0.002, 0.05, reflBrightness) * edgeFade;
+
+ envReflect = texReflect * 0.5;
+ reflAmount = reflValidity * 0.4;
+ }
+
+ // ============================================================
+ // GGX Specular
+ // ============================================================
+ float roughness = 0.18;
+ vec3 halfDir = normalize(ldir + viewDir);
+ float D = DistributionGGX(norm, halfDir, roughness);
+ float G = GeometrySmith(NdotV, NdotL, roughness);
+ vec3 F = fresnelSchlickRoughness(max(dot(halfDir, viewDir), 0.0), F0, roughness);
+ vec3 specular = (D * G * F) / (4.0 * NdotV * NdotL + 0.001) * lightColor.rgb * NdotL;
+ specular = min(specular, vec3(2.0));
+
+ // Noise-based sparkle
+ float sparkleNoise = fbmNoise(FragPos.xy * 4.0 + time * 0.5, time * 1.5);
+ float sparkle = pow(max(sparkleNoise - 0.55, 0.0) / 0.45, 3.0) * shimmerStrength * 0.10;
+ specular += sparkle * lightColor.rgb;
+
+ // ============================================================
+ // Subsurface scattering
+ // ============================================================
+ float sssBase = pow(max(dot(viewDir, -ldir), 0.0), 4.0);
+ float sss = sssBase * max(0.0, WaveOffset * 3.0) * 0.25;
+ vec3 sssColor = vec3(0.05, 0.55, 0.35) * sss * lightColor.rgb;
+
+ // ============================================================
+ // Combine β reflection only where valid, no dark fallback
+ // ============================================================
+ // reflAmount is 0 where no valid reflection data exists β no dark arc
+ float reflectWeight = clamp(fresnel * reflAmount, 0.0, 0.30);
+ vec3 color = mix(refractedColor, envReflect, reflectWeight);
+ color += specular + sssColor;
+
+ float crest = smoothstep(0.5, 1.0, WaveOffset) * 0.04;
+ color += vec3(crest);
+
+ // ============================================================
+ // Shoreline foam β scattered particles, not smooth bands
+ // Only on terrain water (waveAmp > 0); WMO water (canals, indoor)
+ // has waveAmp == 0 and should not show shoreline interaction.
+ // ============================================================
+ if (basicType < 1.5 && verticalDepth > 0.01 && push.waveAmp > 0.0) {
+ float foamDepthMask = 1.0 - smoothstep(0.0, 1.8, verticalDepth);
+
+ // Warp UV coords with noise to break up cellular regularity
+ vec2 warpOffset = vec2(
+ noiseValue(FragPos.xy * 2.5 + time * 0.08) - 0.5,
+ noiseValue(FragPos.xy * 2.5 + vec2(37.0) + time * 0.06) - 0.5
+ ) * 1.6;
+ vec2 foamUV = FragPos.xy + warpOffset;
+
+ // Fine scattered particles
+ float cells1 = cellularFoam(foamUV * 14.0 + time * vec2(0.15, 0.08));
+ float foam1 = (1.0 - smoothstep(0.0, 0.12, cells1)) * 0.45;
+
+ // Tiny spray dots
+ float cells2 = cellularFoam(foamUV * 28.0 + time * vec2(-0.12, 0.22));
+ float foam2 = (1.0 - smoothstep(0.0, 0.07, cells2)) * 0.3;
+
+ // Micro specks
+ float cells3 = cellularFoam(foamUV * 50.0 + time * vec2(0.25, -0.1));
+ float foam3 = (1.0 - smoothstep(0.0, 0.05, cells3)) * 0.18;
+
+ // Noise breakup for clumping
+ float noiseMask = noiseValue(FragPos.xy * 3.0 + time * 0.15);
+ float foam = (foam1 + foam2 + foam3) * foamDepthMask * smoothstep(0.3, 0.6, noiseMask);
+
+ foam *= smoothstep(0.0, 0.1, verticalDepth);
+ // Bluer foam tint instead of near-white
+ color = mix(color, vec3(0.68, 0.78, 0.88), clamp(foam, 0.0, 0.40));
+ }
+
+ // ============================================================
+ // Wave crest foam (ocean only) β particle-based
+ // ============================================================
+ if (basicType > 0.5 && basicType < 1.5 && push.waveAmp > 0.0) {
+ float crestMask = smoothstep(0.5, 1.0, WaveOffset);
+ vec2 crestWarp = vec2(
+ noiseValue(FragPos.xy * 1.8 + time * 0.1) - 0.5,
+ noiseValue(FragPos.xy * 1.8 + vec2(53.0) + time * 0.07) - 0.5
+ ) * 2.0;
+ float crestCells = cellularFoam((FragPos.xy + crestWarp) * 6.0 + time * vec2(0.12, 0.08));
+ float crestFoam = (1.0 - smoothstep(0.0, 0.18, crestCells)) * crestMask;
+ float crestNoise = noiseValue(FragPos.xy * 3.0 - time * 0.3);
+ crestFoam *= smoothstep(0.3, 0.6, crestNoise);
+ color = mix(color, vec3(0.68, 0.78, 0.88), crestFoam * 0.30);
+ }
+
+ // ============================================================
+ // Alpha and fog
+ // ============================================================
+ float baseAlpha = mix(waterAlpha, min(1.0, waterAlpha * 1.5), depthFade);
+ float alpha = mix(baseAlpha, min(1.0, baseAlpha * 1.3), fresnel) * alphaScale;
+ alpha *= smoothstep(1600.0, 350.0, dist);
+ alpha = clamp(alpha, 0.15, 0.92);
+
+ float fogFactor = clamp((fogParams.y - dist) / (fogParams.y - fogParams.x), 0.0, 1.0);
+ color = mix(fogColor.rgb, color, fogFactor);
+
+ outColor = vec4(color, alpha);
+}
diff --git a/assets/shaders/water.frag.spv b/assets/shaders/water.frag.spv
new file mode 100644
index 00000000..5f1d56b1
Binary files /dev/null and b/assets/shaders/water.frag.spv differ
diff --git a/assets/shaders/water.vert.glsl b/assets/shaders/water.vert.glsl
new file mode 100644
index 00000000..6eb0e796
--- /dev/null
+++ b/assets/shaders/water.vert.glsl
@@ -0,0 +1,166 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(push_constant) uniform Push {
+ mat4 model;
+ float waveAmp;
+ float waveFreq;
+ float waveSpeed;
+ float liquidBasicType; // 0=water, 1=ocean, 2=magma, 3=slime
+} push;
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in vec2 aTexCoord;
+
+layout(location = 0) out vec3 FragPos;
+layout(location = 1) out vec3 Normal;
+layout(location = 2) out vec2 TexCoord;
+layout(location = 3) out float WaveOffset;
+layout(location = 4) out vec2 ScreenUV;
+
+// --- Gerstner wave ---
+// Coordinate system: X,Y = horizontal plane, Z = up (height)
+// displacement.xy = horizontal, displacement.z = vertical
+struct GerstnerResult {
+ vec3 displacement;
+ vec3 tangent; // along X
+ vec3 binormal; // along Y
+ float waveHeight; // raw wave height for foam
+};
+
+GerstnerResult evaluateGerstnerWaves(vec2 pos, float time, float amp, float freq, float spd, float basicType) {
+ GerstnerResult r;
+ r.displacement = vec3(0.0);
+ r.tangent = vec3(1.0, 0.0, 0.0);
+ r.binormal = vec3(0.0, 1.0, 0.0);
+ r.waveHeight = 0.0;
+
+ // Magma/slime: simple slow undulation
+ if (basicType >= 1.5) {
+ float wave = sin(pos.x * freq * 0.5 + time * spd * 0.3) * 0.4
+ + sin(pos.y * freq * 0.3 + time * spd * 0.5) * 0.3;
+ r.displacement.z = wave * amp * 0.5;
+ float dx = cos(pos.x * freq * 0.5 + time * spd * 0.3) * freq * 0.5 * amp * 0.5 * 0.4;
+ float dy = cos(pos.y * freq * 0.3 + time * spd * 0.5) * freq * 0.3 * amp * 0.5 * 0.3;
+ r.tangent = vec3(1.0, 0.0, dx);
+ r.binormal = vec3(0.0, 1.0, dy);
+ r.waveHeight = wave;
+ return r;
+ }
+
+ // 6 wave directions for more chaotic, natural-looking water
+ // Spread across many angles to avoid visible patterns
+ vec2 dirs[6] = vec2[6](
+ normalize(vec2(0.86, 0.51)),
+ normalize(vec2(-0.47, 0.88)),
+ normalize(vec2(0.32, -0.95)),
+ normalize(vec2(-0.93, -0.37)),
+ normalize(vec2(0.67, -0.29)),
+ normalize(vec2(-0.15, 0.74))
+ );
+ float amps[6];
+ float freqs[6];
+ float spds_arr[6];
+ float steepness[6];
+
+ if (basicType > 0.5) {
+ // Ocean: broader range of wave scales for realistic chop
+ amps[0] = amp * 1.0; amps[1] = amp * 0.55; amps[2] = amp * 0.30;
+ amps[3] = amp * 0.18; amps[4] = amp * 0.10; amps[5] = amp * 0.06;
+ freqs[0] = freq * 0.7; freqs[1] = freq * 1.3; freqs[2] = freq * 2.1;
+ freqs[3] = freq * 3.4; freqs[4] = freq * 5.0; freqs[5] = freq * 7.5;
+ spds_arr[0] = spd * 0.8; spds_arr[1] = spd * 1.0; spds_arr[2] = spd * 1.3;
+ spds_arr[3] = spd * 1.6; spds_arr[4] = spd * 2.0; spds_arr[5] = spd * 2.5;
+ steepness[0] = 0.35; steepness[1] = 0.30; steepness[2] = 0.25;
+ steepness[3] = 0.20; steepness[4] = 0.15; steepness[5] = 0.10;
+ } else {
+ // Inland water: gentle but multi-scale ripples
+ amps[0] = amp * 0.5; amps[1] = amp * 0.25; amps[2] = amp * 0.15;
+ amps[3] = amp * 0.08; amps[4] = amp * 0.05; amps[5] = amp * 0.03;
+ freqs[0] = freq * 1.0; freqs[1] = freq * 1.8; freqs[2] = freq * 3.0;
+ freqs[3] = freq * 4.5; freqs[4] = freq * 7.0; freqs[5] = freq * 10.0;
+ spds_arr[0] = spd * 0.6; spds_arr[1] = spd * 0.9; spds_arr[2] = spd * 1.2;
+ spds_arr[3] = spd * 1.5; spds_arr[4] = spd * 1.9; spds_arr[5] = spd * 2.3;
+ steepness[0] = 0.20; steepness[1] = 0.18; steepness[2] = 0.15;
+ steepness[3] = 0.12; steepness[4] = 0.10; steepness[5] = 0.08;
+ }
+
+ float totalWave = 0.0;
+ for (int i = 0; i < 6; i++) {
+ float w = freqs[i];
+ float A = amps[i];
+ float phi = spds_arr[i] * w; // phase speed
+ float Q = steepness[i] / (w * A * 6.0);
+ Q = clamp(Q, 0.0, 1.0);
+
+ float phase = w * dot(dirs[i], pos) + phi * time;
+ float s = sin(phase);
+ float c = cos(phase);
+
+ // Gerstner displacement: xy = horizontal, z = vertical (up)
+ r.displacement.x += Q * A * dirs[i].x * c;
+ r.displacement.y += Q * A * dirs[i].y * c;
+ r.displacement.z += A * s;
+
+ // Tangent/binormal accumulation for analytical normal
+ float WA = w * A;
+ r.tangent.x -= Q * dirs[i].x * dirs[i].x * WA * s;
+ r.tangent.y -= Q * dirs[i].x * dirs[i].y * WA * s;
+ r.tangent.z += dirs[i].x * WA * c;
+
+ r.binormal.x -= Q * dirs[i].x * dirs[i].y * WA * s;
+ r.binormal.y -= Q * dirs[i].y * dirs[i].y * WA * s;
+ r.binormal.z += dirs[i].y * WA * c;
+
+ totalWave += A * s;
+ }
+
+ r.waveHeight = totalWave;
+ return r;
+}
+
+void main() {
+ float time = fogParams.z;
+ vec4 worldPos = push.model * vec4(aPos, 1.0);
+
+ // Evaluate Gerstner waves using X,Y horizontal plane
+ GerstnerResult waves = evaluateGerstnerWaves(
+ vec2(worldPos.x, worldPos.y), time,
+ push.waveAmp, push.waveFreq, push.waveSpeed, push.liquidBasicType
+ );
+
+ // Apply displacement: xy = horizontal, z = vertical (up)
+ worldPos.x += waves.displacement.x;
+ worldPos.y += waves.displacement.y;
+ worldPos.z += waves.displacement.z;
+ WaveOffset = waves.waveHeight; // raw wave height for fragment shader foam
+
+ // Player interaction ripples β concentric waves emanating from player position
+ vec2 playerPos = vec2(shadowParams.z, shadowParams.w);
+ float rippleStrength = fogParams.w;
+ float d = length(worldPos.xy - playerPos);
+ float ripple = rippleStrength * 0.12 * exp(-d * 0.12) * sin(d * 2.5 - time * 6.0);
+ worldPos.z += ripple;
+
+ // Analytical normal from Gerstner tangent/binormal (cross product gives Z-up normal)
+ Normal = normalize(cross(waves.binormal, waves.tangent));
+
+ FragPos = worldPos.xyz;
+ TexCoord = aTexCoord;
+ vec4 clipPos = projection * view * worldPos;
+ gl_Position = clipPos;
+ vec2 ndc = clipPos.xy / max(clipPos.w, 1e-5);
+ ScreenUV = ndc * 0.5 + 0.5;
+}
diff --git a/assets/shaders/water.vert.spv b/assets/shaders/water.vert.spv
new file mode 100644
index 00000000..3f585319
Binary files /dev/null and b/assets/shaders/water.vert.spv differ
diff --git a/assets/shaders/weather.frag.glsl b/assets/shaders/weather.frag.glsl
new file mode 100644
index 00000000..f1d4be21
--- /dev/null
+++ b/assets/shaders/weather.frag.glsl
@@ -0,0 +1,19 @@
+#version 450
+
+layout(push_constant) uniform Push {
+ float particleSize;
+ float pad0;
+ float pad1;
+ float pad2;
+ vec4 particleColor;
+} push;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ vec2 p = gl_PointCoord - vec2(0.5);
+ float dist = length(p);
+ if (dist > 0.5) discard;
+ float alpha = push.particleColor.a * smoothstep(0.5, 0.2, dist);
+ outColor = vec4(push.particleColor.rgb, alpha);
+}
diff --git a/assets/shaders/weather.frag.spv b/assets/shaders/weather.frag.spv
new file mode 100644
index 00000000..5d71905f
Binary files /dev/null and b/assets/shaders/weather.frag.spv differ
diff --git a/assets/shaders/weather.vert.glsl b/assets/shaders/weather.vert.glsl
new file mode 100644
index 00000000..68dd7e73
--- /dev/null
+++ b/assets/shaders/weather.vert.glsl
@@ -0,0 +1,25 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(push_constant) uniform Push {
+ float particleSize;
+} push;
+
+layout(location = 0) in vec3 aPos;
+
+void main() {
+ gl_PointSize = push.particleSize;
+ gl_Position = projection * view * vec4(aPos, 1.0);
+}
diff --git a/assets/shaders/weather.vert.spv b/assets/shaders/weather.vert.spv
new file mode 100644
index 00000000..4977f00a
Binary files /dev/null and b/assets/shaders/weather.vert.spv differ
diff --git a/assets/shaders/wmo.frag.glsl b/assets/shaders/wmo.frag.glsl
new file mode 100644
index 00000000..dbd55436
--- /dev/null
+++ b/assets/shaders/wmo.frag.glsl
@@ -0,0 +1,226 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(set = 1, binding = 0) uniform sampler2D uTexture;
+
+layout(set = 1, binding = 1) uniform WMOMaterial {
+ int hasTexture;
+ int alphaTest;
+ int unlit;
+ int isInterior;
+ float specularIntensity;
+ int isWindow;
+ int enableNormalMap;
+ int enablePOM;
+ float pomScale;
+ int pomMaxSamples;
+ float heightMapVariance;
+ float normalMapStrength;
+};
+
+layout(set = 1, binding = 2) uniform sampler2D uNormalHeightMap;
+
+layout(set = 0, binding = 1) uniform sampler2DShadow uShadowMap;
+
+layout(location = 0) in vec3 FragPos;
+layout(location = 1) in vec3 Normal;
+layout(location = 2) in vec2 TexCoord;
+layout(location = 3) in vec4 VertColor;
+layout(location = 4) in vec3 Tangent;
+layout(location = 5) in vec3 Bitangent;
+
+layout(location = 0) out vec4 outColor;
+
+const float SHADOW_TEXEL = 1.0 / 4096.0;
+
+float sampleShadowPCF(sampler2DShadow smap, vec3 coords) {
+ float shadow = 0.0;
+ for (int x = -1; x <= 1; ++x) {
+ for (int y = -1; y <= 1; ++y) {
+ shadow += texture(smap, vec3(coords.xy + vec2(x, y) * SHADOW_TEXEL, coords.z));
+ }
+ }
+ return shadow / 9.0;
+}
+
+// LOD factor from screen-space UV derivatives
+float computeLodFactor() {
+ vec2 dx = dFdx(TexCoord);
+ vec2 dy = dFdy(TexCoord);
+ float texelDensity = max(dot(dx, dx), dot(dy, dy));
+ // Low density = close/head-on = full detail (0)
+ // High density = far/steep = vertex normals only (1)
+ return smoothstep(0.0001, 0.005, texelDensity);
+}
+
+// Parallax Occlusion Mapping with angle-adaptive sampling
+vec2 parallaxOcclusionMap(vec2 uv, vec3 viewDirTS, float lodFactor) {
+ float VdotN = abs(viewDirTS.z); // 1=head-on, 0=grazing
+
+ // Fade out POM at grazing angles to avoid distortion
+ if (VdotN < 0.15) return uv;
+
+ float angleFactor = clamp(VdotN, 0.15, 1.0);
+ int maxS = pomMaxSamples;
+ int minS = max(maxS / 4, 4);
+ int numSamples = int(mix(float(minS), float(maxS), angleFactor));
+ numSamples = int(mix(float(minS), float(numSamples), 1.0 - lodFactor));
+
+ float layerDepth = 1.0 / float(numSamples);
+ float currentLayerDepth = 0.0;
+
+ // Direction to shift UV per layer β clamp denominator to prevent explosion at grazing angles
+ vec2 P = viewDirTS.xy / max(VdotN, 0.15) * pomScale;
+ // Hard-clamp total UV offset to prevent texture swimming
+ float maxOffset = pomScale * 3.0;
+ P = clamp(P, vec2(-maxOffset), vec2(maxOffset));
+ vec2 deltaUV = P / float(numSamples);
+
+ vec2 currentUV = uv;
+ float currentDepthMapValue = 1.0 - texture(uNormalHeightMap, currentUV).a;
+
+ // Ray march through layers
+ for (int i = 0; i < 64; i++) {
+ if (i >= numSamples || currentLayerDepth >= currentDepthMapValue) break;
+ currentUV -= deltaUV;
+ currentDepthMapValue = 1.0 - texture(uNormalHeightMap, currentUV).a;
+ currentLayerDepth += layerDepth;
+ }
+
+ // Interpolate between last two layers for smooth result
+ vec2 prevUV = currentUV + deltaUV;
+ float afterDepth = currentDepthMapValue - currentLayerDepth;
+ float beforeDepth = (1.0 - texture(uNormalHeightMap, prevUV).a) - currentLayerDepth + layerDepth;
+ float weight = afterDepth / (afterDepth - beforeDepth + 0.0001);
+ vec2 result = mix(currentUV, prevUV, weight);
+
+ // Fade toward original UV at grazing angles for smooth transition
+ float fadeFactor = smoothstep(0.15, 0.35, VdotN);
+ return mix(uv, result, fadeFactor);
+}
+
+void main() {
+ float lodFactor = computeLodFactor();
+
+ vec3 vertexNormal = normalize(Normal);
+ if (!gl_FrontFacing) vertexNormal = -vertexNormal;
+
+ // Compute final UV (with POM if enabled)
+ vec2 finalUV = TexCoord;
+
+ // Build TBN matrix
+ vec3 T = normalize(Tangent);
+ vec3 B = normalize(Bitangent);
+ vec3 N = vertexNormal;
+ mat3 TBN = mat3(T, B, N);
+
+ if (enablePOM != 0 && heightMapVariance > 0.001 && lodFactor < 0.99) {
+ mat3 TBN_inv = transpose(TBN);
+ vec3 viewDirWorld = normalize(viewPos.xyz - FragPos);
+ vec3 viewDirTS = TBN_inv * viewDirWorld;
+ finalUV = parallaxOcclusionMap(TexCoord, viewDirTS, lodFactor);
+ }
+
+ vec4 texColor = hasTexture != 0 ? texture(uTexture, finalUV) : vec4(1.0);
+ if (alphaTest != 0 && texColor.a < 0.5) discard;
+
+ // Compute normal (with normal mapping if enabled)
+ vec3 norm = vertexNormal;
+ if (enableNormalMap != 0 && lodFactor < 0.99 && normalMapStrength > 0.001) {
+ vec3 mapNormal = texture(uNormalHeightMap, finalUV).rgb * 2.0 - 1.0;
+ // Scale XY by strength to control effect intensity
+ mapNormal.xy *= normalMapStrength;
+ mapNormal = normalize(mapNormal);
+ vec3 worldNormal = normalize(TBN * mapNormal);
+ if (!gl_FrontFacing) worldNormal = -worldNormal;
+ // Blend: strength + LOD both contribute to fade toward vertex normal
+ float blendFactor = max(lodFactor, 1.0 - normalMapStrength);
+ norm = normalize(mix(worldNormal, vertexNormal, blendFactor));
+ }
+
+ vec3 result;
+
+ // Sample shadow map for all non-window WMO surfaces
+ float shadow = 1.0;
+ if (shadowParams.x > 0.5) {
+ vec3 ldir = normalize(-lightDir.xyz);
+ float normalOffset = SHADOW_TEXEL * 2.0 * (1.0 - abs(dot(norm, ldir)));
+ vec3 biasedPos = FragPos + norm * normalOffset;
+ vec4 lsPos = lightSpaceMatrix * vec4(biasedPos, 1.0);
+ vec3 proj = lsPos.xyz / lsPos.w;
+ proj.xy = proj.xy * 0.5 + 0.5;
+ if (proj.x >= 0.0 && proj.x <= 1.0 &&
+ proj.y >= 0.0 && proj.y <= 1.0 &&
+ proj.z >= 0.0 && proj.z <= 1.0) {
+ float bias = max(0.0005 * (1.0 - dot(norm, ldir)), 0.00005);
+ shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias));
+ }
+ shadow = mix(1.0, shadow, shadowParams.y);
+ }
+
+ if (unlit != 0) {
+ result = texColor.rgb * shadow;
+ } else if (isInterior != 0) {
+ vec3 mocv = max(VertColor.rgb, vec3(0.5));
+ result = texColor.rgb * mocv * shadow;
+ } else {
+ vec3 ldir = normalize(-lightDir.xyz);
+ float diff = max(dot(norm, ldir), 0.0);
+
+ vec3 viewDir = normalize(viewPos.xyz - FragPos);
+ vec3 halfDir = normalize(ldir + viewDir);
+ float spec = pow(max(dot(norm, halfDir), 0.0), 32.0) * specularIntensity;
+
+ result = ambientColor.rgb * texColor.rgb
+ + shadow * (diff * lightColor.rgb * texColor.rgb + spec * lightColor.rgb);
+
+ result *= max(VertColor.rgb, vec3(0.5));
+ }
+
+ float dist = length(viewPos.xyz - FragPos);
+ float fogFactor = clamp((fogParams.y - dist) / (fogParams.y - fogParams.x), 0.0, 1.0);
+ result = mix(fogColor.rgb, result, fogFactor);
+
+ float alpha = texColor.a;
+
+ // Window glass: opaque but simulates dark tinted glass with reflections.
+ if (isWindow != 0) {
+ vec3 viewDir = normalize(viewPos.xyz - FragPos);
+ float NdotV = abs(dot(norm, viewDir));
+ float fresnel = 0.08 + 0.92 * pow(1.0 - NdotV, 4.0);
+
+ vec3 ldir = normalize(-lightDir.xyz);
+ vec3 reflectDir = reflect(-viewDir, norm);
+ float sunGlint = pow(max(dot(reflectDir, ldir), 0.0), 32.0);
+
+ float baseBrightness = mix(0.3, 0.9, sunGlint);
+ vec3 glass = result * baseBrightness;
+
+ vec3 reflectTint = mix(ambientColor.rgb * 1.2, vec3(0.6, 0.75, 1.0), 0.6);
+ glass = mix(glass, reflectTint, fresnel * 0.8);
+
+ vec3 halfDir = normalize(ldir + viewDir);
+ float spec = pow(max(dot(norm, halfDir), 0.0), 256.0);
+ glass += spec * lightColor.rgb * 0.8;
+
+ float specBroad = pow(max(dot(norm, halfDir), 0.0), 12.0);
+ glass += specBroad * lightColor.rgb * 0.12;
+
+ result = glass;
+ alpha = mix(0.4, 0.95, NdotV);
+ }
+
+ outColor = vec4(result, alpha);
+}
diff --git a/assets/shaders/wmo.frag.spv b/assets/shaders/wmo.frag.spv
new file mode 100644
index 00000000..a7b9ef94
Binary files /dev/null and b/assets/shaders/wmo.frag.spv differ
diff --git a/assets/shaders/wmo.vert.glsl b/assets/shaders/wmo.vert.glsl
new file mode 100644
index 00000000..737b30aa
--- /dev/null
+++ b/assets/shaders/wmo.vert.glsl
@@ -0,0 +1,53 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform PerFrame {
+ mat4 view;
+ mat4 projection;
+ mat4 lightSpaceMatrix;
+ vec4 lightDir;
+ vec4 lightColor;
+ vec4 ambientColor;
+ vec4 viewPos;
+ vec4 fogColor;
+ vec4 fogParams;
+ vec4 shadowParams;
+};
+
+layout(push_constant) uniform Push {
+ mat4 model;
+} push;
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in vec3 aNormal;
+layout(location = 2) in vec2 aTexCoord;
+layout(location = 3) in vec4 aColor;
+layout(location = 4) in vec4 aTangent;
+
+layout(location = 0) out vec3 FragPos;
+layout(location = 1) out vec3 Normal;
+layout(location = 2) out vec2 TexCoord;
+layout(location = 3) out vec4 VertColor;
+layout(location = 4) out vec3 Tangent;
+layout(location = 5) out vec3 Bitangent;
+
+void main() {
+ vec4 worldPos = push.model * vec4(aPos, 1.0);
+ FragPos = worldPos.xyz;
+
+ mat3 normalMatrix = mat3(push.model);
+ Normal = normalMatrix * aNormal;
+ TexCoord = aTexCoord;
+ VertColor = aColor;
+
+ // Compute TBN basis vectors for normal mapping
+ vec3 T = normalize(normalMatrix * aTangent.xyz);
+ vec3 N = normalize(Normal);
+ // Gram-Schmidt re-orthogonalize
+ T = normalize(T - dot(T, N) * N);
+ vec3 B = cross(N, T) * aTangent.w;
+
+ Tangent = T;
+ Bitangent = B;
+
+ gl_Position = projection * view * worldPos;
+}
diff --git a/assets/shaders/wmo.vert.spv b/assets/shaders/wmo.vert.spv
new file mode 100644
index 00000000..95428d7e
Binary files /dev/null and b/assets/shaders/wmo.vert.spv differ
diff --git a/assets/shaders/wmo_occlusion.frag.glsl b/assets/shaders/wmo_occlusion.frag.glsl
new file mode 100644
index 00000000..2f58b7d6
--- /dev/null
+++ b/assets/shaders/wmo_occlusion.frag.glsl
@@ -0,0 +1,5 @@
+#version 450
+
+void main() {
+ // depth-only pass, no color output
+}
diff --git a/assets/shaders/wmo_occlusion.frag.spv b/assets/shaders/wmo_occlusion.frag.spv
new file mode 100644
index 00000000..f73da9ea
Binary files /dev/null and b/assets/shaders/wmo_occlusion.frag.spv differ
diff --git a/assets/shaders/wmo_occlusion.vert.glsl b/assets/shaders/wmo_occlusion.vert.glsl
new file mode 100644
index 00000000..1b4bdd58
--- /dev/null
+++ b/assets/shaders/wmo_occlusion.vert.glsl
@@ -0,0 +1,11 @@
+#version 450
+
+layout(push_constant) uniform Push {
+ mat4 mvp;
+} push;
+
+layout(location = 0) in vec3 aPos;
+
+void main() {
+ gl_Position = push.mvp * vec4(aPos, 1.0);
+}
diff --git a/assets/shaders/wmo_occlusion.vert.spv b/assets/shaders/wmo_occlusion.vert.spv
new file mode 100644
index 00000000..c6ec2444
Binary files /dev/null and b/assets/shaders/wmo_occlusion.vert.spv differ
diff --git a/assets/shaders/world_map.frag.glsl b/assets/shaders/world_map.frag.glsl
new file mode 100644
index 00000000..d4884b1e
--- /dev/null
+++ b/assets/shaders/world_map.frag.glsl
@@ -0,0 +1,11 @@
+#version 450
+
+layout(set = 0, binding = 0) uniform sampler2D uTileTexture;
+
+layout(location = 0) in vec2 TexCoord;
+
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ outColor = texture(uTileTexture, TexCoord);
+}
diff --git a/assets/shaders/world_map.frag.spv b/assets/shaders/world_map.frag.spv
new file mode 100644
index 00000000..5209ba44
Binary files /dev/null and b/assets/shaders/world_map.frag.spv differ
diff --git a/assets/shaders/world_map.vert.glsl b/assets/shaders/world_map.vert.glsl
new file mode 100644
index 00000000..bfb4ac17
--- /dev/null
+++ b/assets/shaders/world_map.vert.glsl
@@ -0,0 +1,19 @@
+#version 450
+
+layout(push_constant) uniform Push {
+ vec2 gridOffset;
+ float gridCols;
+ float gridRows;
+} push;
+
+layout(location = 0) in vec2 aPos;
+layout(location = 1) in vec2 aUV;
+
+layout(location = 0) out vec2 TexCoord;
+
+void main() {
+ TexCoord = aUV;
+ vec2 pos = (aPos + push.gridOffset) / vec2(push.gridCols, push.gridRows);
+ pos = pos * 2.0 - 1.0;
+ gl_Position = vec4(pos, 0.0, 1.0);
+}
diff --git a/assets/shaders/world_map.vert.spv b/assets/shaders/world_map.vert.spv
new file mode 100644
index 00000000..8ff81cc5
Binary files /dev/null and b/assets/shaders/world_map.vert.spv differ
diff --git a/assets/startscreen.mp4 b/assets/startscreen.mp4
deleted file mode 100755
index d58bb228..00000000
Binary files a/assets/startscreen.mp4 and /dev/null differ
diff --git a/include/audio/activity_sound_manager.hpp b/include/audio/activity_sound_manager.hpp
index 471f785c..764565ea 100644
--- a/include/audio/activity_sound_manager.hpp
+++ b/include/audio/activity_sound_manager.hpp
@@ -27,6 +27,7 @@ public:
void playLanding(FootstepSurface surface, bool hardLanding);
void setSwimmingState(bool swimming, bool moving);
void setCharacterVoiceProfile(const std::string& modelName);
+ void setCharacterVoiceProfile(const std::string& raceFolder, const std::string& raceBase, bool male);
void playWaterEnter();
void playWaterExit();
void playMeleeSwing();
diff --git a/include/core/logger.hpp b/include/core/logger.hpp
index 65f38040..4ba4e3ce 100644
--- a/include/core/logger.hpp
+++ b/include/core/logger.hpp
@@ -5,6 +5,8 @@
#include
#include
#include
+#include
+#include
namespace wowee {
namespace core {
@@ -30,29 +32,35 @@ public:
void log(LogLevel level, const std::string& message);
void setLogLevel(LogLevel level);
+ bool shouldLog(LogLevel level) const;
template
void debug(Args&&... args) {
+ if (!shouldLog(LogLevel::DEBUG)) return;
log(LogLevel::DEBUG, format(std::forward(args)...));
}
template
void info(Args&&... args) {
+ if (!shouldLog(LogLevel::INFO)) return;
log(LogLevel::INFO, format(std::forward(args)...));
}
template
void warning(Args&&... args) {
+ if (!shouldLog(LogLevel::WARNING)) return;
log(LogLevel::WARNING, format(std::forward(args)...));
}
template
void error(Args&&... args) {
+ if (!shouldLog(LogLevel::ERROR)) return;
log(LogLevel::ERROR, format(std::forward(args)...));
}
template
void fatal(Args&&... args) {
+ if (!shouldLog(LogLevel::FATAL)) return;
log(LogLevel::FATAL, format(std::forward(args)...));
}
@@ -69,10 +77,13 @@ private:
return oss.str();
}
- LogLevel minLevel = LogLevel::INFO; // Changed from DEBUG to reduce log spam
+ std::atomic minLevel_{static_cast(LogLevel::INFO)};
std::mutex mutex;
std::ofstream fileStream;
bool fileReady = false;
+ bool echoToStdout_ = true;
+ std::chrono::steady_clock::time_point lastFlushTime_{};
+ uint32_t flushIntervalMs_ = 250;
void ensureFile();
};
diff --git a/include/core/window.hpp b/include/core/window.hpp
index 3da55977..dc95e2c5 100644
--- a/include/core/window.hpp
+++ b/include/core/window.hpp
@@ -3,8 +3,11 @@
#include
#include
#include
+#include
namespace wowee {
+namespace rendering { class VkContext; }
+
namespace core {
struct WindowConfig {
@@ -27,7 +30,7 @@ public:
bool initialize();
void shutdown();
- void swapBuffers();
+ void swapBuffers() {} // No-op: Vulkan presents in Renderer::endFrame()
void pollEvents();
bool shouldClose() const { return shouldCloseFlag; }
@@ -44,12 +47,14 @@ public:
void applyResolution(int w, int h);
SDL_Window* getSDLWindow() const { return window; }
- SDL_GLContext getGLContext() const { return glContext; }
+
+ // Vulkan context access
+ rendering::VkContext* getVkContext() const { return vkContext.get(); }
private:
WindowConfig config;
SDL_Window* window = nullptr;
- SDL_GLContext glContext = nullptr;
+ std::unique_ptr vkContext;
int width;
int height;
diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp
index ad993511..d0364020 100644
--- a/include/game/game_handler.hpp
+++ b/include/game/game_handler.hpp
@@ -593,6 +593,9 @@ public:
using GameObjectDespawnCallback = std::function;
void setGameObjectDespawnCallback(GameObjectDespawnCallback cb) { gameObjectDespawnCallback_ = std::move(cb); }
+ using GameObjectCustomAnimCallback = std::function;
+ void setGameObjectCustomAnimCallback(GameObjectCustomAnimCallback cb) { gameObjectCustomAnimCallback_ = std::move(cb); }
+
// Faction hostility map (populated from FactionTemplate.dbc by Application)
void setFactionHostileMap(std::unordered_map map) { factionHostileMap_ = std::move(map); }
@@ -1401,6 +1404,7 @@ private:
GameObjectSpawnCallback gameObjectSpawnCallback_;
GameObjectMoveCallback gameObjectMoveCallback_;
GameObjectDespawnCallback gameObjectDespawnCallback_;
+ GameObjectCustomAnimCallback gameObjectCustomAnimCallback_;
// Transport tracking
struct TransportAttachment {
diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp
index 42ad5b5f..edd97e8c 100644
--- a/include/game/packet_parsers.hpp
+++ b/include/game/packet_parsers.hpp
@@ -353,6 +353,7 @@ class TurtlePacketParsers : public ClassicPacketParsers {
public:
uint8_t movementFlags2Size() const override { return 0; }
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
+ bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) override;
};
/**
diff --git a/include/game/spell_defines.hpp b/include/game/spell_defines.hpp
index a57650e9..cf0e7ea9 100644
--- a/include/game/spell_defines.hpp
+++ b/include/game/spell_defines.hpp
@@ -180,7 +180,7 @@ inline const char* getSpellCastResultString(uint8_t result, int powerType = -1)
case 91: return "Only mounted";
case 92: return "Only nighttime";
case 93: return "Only outdoors";
- case 94: return "Only shapeshift";
+ case 94: return "Requires correct stance/form";
case 95: return "Only stealthed";
case 96: return "Only underwater";
case 97: return "Out of range";
diff --git a/include/network/packet.hpp b/include/network/packet.hpp
index 4171fbad..fbfb85bf 100644
--- a/include/network/packet.hpp
+++ b/include/network/packet.hpp
@@ -12,6 +12,7 @@ public:
Packet() = default;
explicit Packet(uint16_t opcode);
Packet(uint16_t opcode, const std::vector& data);
+ Packet(uint16_t opcode, std::vector&& data);
void writeUInt8(uint8_t value);
void writeUInt16(uint16_t value);
diff --git a/include/network/world_socket.hpp b/include/network/world_socket.hpp
index eee99bda..1d8f1d00 100644
--- a/include/network/world_socket.hpp
+++ b/include/network/world_socket.hpp
@@ -91,6 +91,13 @@ private:
// Receive buffer
std::vector receiveBuffer;
+ size_t receiveReadOffset_ = 0;
+ // Optional reused packet queue (feature-gated) to reduce per-update allocations.
+ std::vector parsedPacketsScratch_;
+
+ // Runtime-gated network optimization toggles (default off).
+ bool useFastRecvAppend_ = false;
+ bool useParseScratchQueue_ = false;
// Track how many header bytes have been decrypted (0-4)
// This prevents re-decrypting the same header when waiting for more data
diff --git a/include/pipeline/wmo_loader.hpp b/include/pipeline/wmo_loader.hpp
index 718042c8..fecb287d 100644
--- a/include/pipeline/wmo_loader.hpp
+++ b/include/pipeline/wmo_loader.hpp
@@ -216,6 +216,7 @@ struct WMOModel {
// Group names
std::vector groupNames;
+ std::vector groupNameRaw; // Raw MOGN chunk for offset-based name lookup
bool isValid() const {
return nGroups > 0 && !groups.empty();
diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp
index 400383b1..835627bc 100644
--- a/include/rendering/camera_controller.hpp
+++ b/include/rendering/camera_controller.hpp
@@ -27,6 +27,8 @@ public:
float getMouseSensitivity() const { return mouseSensitivity; }
void setInvertMouse(bool invert) { invertMouse = invert; }
bool isInvertMouse() const { return invertMouse; }
+ void setExtendedZoom(bool extended) { extendedZoom_ = extended; }
+ bool isExtendedZoom() const { return extendedZoom_; }
void setEnabled(bool enabled) { this->enabled = enabled; }
void setTerrainManager(TerrainManager* tm) { terrainManager = tm; }
void setWMORenderer(WMORenderer* wmo) { wmoRenderer = wmo; }
@@ -137,7 +139,9 @@ private:
float collisionDistance = 10.0f; // Max allowed by collision
bool externalFollow_ = false;
static constexpr float MIN_DISTANCE = 0.5f; // Minimum zoom (first-person threshold)
- static constexpr float MAX_DISTANCE = 50.0f; // Maximum zoom out
+ static constexpr float MAX_DISTANCE_NORMAL = 22.0f; // Default max zoom out
+ static constexpr float MAX_DISTANCE_EXTENDED = 50.0f; // Extended max zoom out
+ bool extendedZoom_ = false;
static constexpr float ZOOM_SMOOTH_SPEED = 15.0f; // How fast zoom eases
static constexpr float CAM_SMOOTH_SPEED = 20.0f; // How fast camera position smooths
static constexpr float PIVOT_HEIGHT = 1.8f; // Pivot at head height
diff --git a/include/rendering/celestial.hpp b/include/rendering/celestial.hpp
index 27e78ffb..26abd6c6 100644
--- a/include/rendering/celestial.hpp
+++ b/include/rendering/celestial.hpp
@@ -1,142 +1,133 @@
#pragma once
-#include
#include
+#include
+#include
namespace wowee {
namespace rendering {
-class Shader;
-class Camera;
+class VkContext;
/**
- * Celestial body renderer
+ * Celestial body renderer (Vulkan)
*
* Renders sun and moon that move across the sky based on time of day.
* Sun rises at dawn, sets at dusk. Moon is visible at night.
+ *
+ * Pipeline layout:
+ * set 0 = perFrameLayout (camera UBO β view, projection, etc.)
+ * push = CelestialPush (mat4 model + vec4 celestialColor + float intensity
+ * + float moonPhase + float animTime = 96 bytes)
*/
class Celestial {
public:
Celestial();
~Celestial();
- bool initialize();
+ /**
+ * Initialize the renderer.
+ * @param ctx Vulkan context
+ * @param perFrameLayout Descriptor set layout for set 0 (camera UBO)
+ */
+ bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
void shutdown();
+ void recreatePipelines();
/**
- * Render celestial bodies (sun and moon)
- * @param camera Camera for view matrix
- * @param timeOfDay Time of day in hours (0-24)
- * @param sunDir Optional sun direction from lighting system (normalized)
- * @param sunColor Optional sun color from lighting system
- * @param gameTime Optional server game time in seconds (for deterministic moon phases)
+ * Render celestial bodies (sun and moons).
+ * @param cmd Command buffer to record into
+ * @param perFrameSet Per-frame descriptor set (set 0, camera UBO)
+ * @param timeOfDay Time of day in hours (0-24)
+ * @param sunDir Optional sun direction from lighting system (normalized)
+ * @param sunColor Optional sun colour from lighting system
+ * @param gameTime Optional server game time in seconds (deterministic moon phases)
*/
- void render(const Camera& camera, float timeOfDay,
- const glm::vec3* sunDir = nullptr,
+ void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
+ float timeOfDay,
+ const glm::vec3* sunDir = nullptr,
const glm::vec3* sunColor = nullptr,
float gameTime = -1.0f);
/**
- * Enable/disable celestial rendering
- */
- void setEnabled(bool enabled) { renderingEnabled = enabled; }
- bool isEnabled() const { return renderingEnabled; }
-
- /**
- * Update celestial bodies (for moon phase cycling)
+ * Update celestial bodies (moon phase cycling, haze timer).
*/
void update(float deltaTime);
- /**
- * Set White Lady phase (primary moon, 0.0 = new, 0.5 = full, 1.0 = new)
- */
+ // --- Enable / disable ---
+ void setEnabled(bool enabled) { renderingEnabled_ = enabled; }
+ bool isEnabled() const { return renderingEnabled_; }
+
+ // --- Moon phases ---
+ /** Set White Lady phase (primary moon, 0 = new, 0.5 = full, 1 = new). */
void setMoonPhase(float phase);
float getMoonPhase() const { return whiteLadyPhase_; }
- /**
- * Set Blue Child phase (secondary moon, 0.0 = new, 0.5 = full, 1.0 = new)
- */
+ /** Set Blue Child phase (secondary moon, 0 = new, 0.5 = full, 1 = new). */
void setBlueChildPhase(float phase);
float getBlueChildPhase() const { return blueChildPhase_; }
- /**
- * Enable/disable automatic moon phase cycling
- */
- void setMoonPhaseCycling(bool enabled) { moonPhaseCycling = enabled; }
- bool isMoonPhaseCycling() const { return moonPhaseCycling; }
+ void setMoonPhaseCycling(bool enabled) { moonPhaseCycling_ = enabled; }
+ bool isMoonPhaseCycling() const { return moonPhaseCycling_; }
- /**
- * Enable/disable two-moon rendering (White Lady + Blue Child)
- */
+ /** Enable / disable two-moon rendering (White Lady + Blue Child). */
void setDualMoonMode(bool enabled) { dualMoonMode_ = enabled; }
bool isDualMoonMode() const { return dualMoonMode_; }
- /**
- * Get sun position in world space
- */
+ // --- Positional / colour queries (unchanged from GL version) ---
glm::vec3 getSunPosition(float timeOfDay) const;
-
- /**
- * Get moon position in world space
- */
glm::vec3 getMoonPosition(float timeOfDay) const;
-
- /**
- * Get sun color (changes with time of day)
- */
glm::vec3 getSunColor(float timeOfDay) const;
-
- /**
- * Get sun intensity (0-1, fades at dawn/dusk)
- */
- float getSunIntensity(float timeOfDay) const;
+ float getSunIntensity(float timeOfDay) const;
private:
- void createCelestialQuad();
- void destroyCelestialQuad();
+ // Push constant block β MUST match celestial.vert.glsl / celestial.frag.glsl
+ struct CelestialPush {
+ glm::mat4 model; // 64 bytes
+ glm::vec4 celestialColor; // 16 bytes (xyz = colour, w unused)
+ float intensity; // 4 bytes
+ float moonPhase; // 4 bytes
+ float animTime; // 4 bytes
+ float _pad; // 4 bytes (round to 16-byte boundary = 96 bytes total)
+ };
+ static_assert(sizeof(CelestialPush) == 96, "CelestialPush size mismatch");
- void renderSun(const Camera& camera, float timeOfDay,
- const glm::vec3* sunDir = nullptr,
- const glm::vec3* sunColor = nullptr);
- void renderMoon(const Camera& camera, float timeOfDay); // White Lady (primary)
- void renderBlueChild(const Camera& camera, float timeOfDay); // Blue Child (secondary)
+ void createQuad();
+ void destroyQuad();
+
+ void renderSun(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
+ float timeOfDay,
+ const glm::vec3* sunDir, const glm::vec3* sunColor);
+ void renderMoon(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, float timeOfDay);
+ void renderBlueChild(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, float timeOfDay);
float calculateCelestialAngle(float timeOfDay, float riseTime, float setTime) const;
-
- /**
- * Compute moon phase from game time (deterministic)
- * @param gameTime Server game time in seconds
- * @param cycleDays Lunar cycle length in game days
- * @return Phase 0.0-1.0 (0=new, 0.5=full, 1.0=new)
- */
float computePhaseFromGameTime(float gameTime, float cycleDays) const;
+ void updatePhasesFromGameTime(float gameTime);
- /**
- * Update moon phases from game time (server-driven)
- */
- void updatePhasesFromGameTime(float gameTime);
+ // Vulkan objects
+ VkContext* vkCtx_ = nullptr;
+ VkPipeline pipeline_ = VK_NULL_HANDLE;
+ VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE;
+ VkBuffer vertexBuffer_ = VK_NULL_HANDLE;
+ VmaAllocation vertexAlloc_ = VK_NULL_HANDLE;
+ VkBuffer indexBuffer_ = VK_NULL_HANDLE;
+ VmaAllocation indexAlloc_ = VK_NULL_HANDLE;
- std::unique_ptr celestialShader;
-
- uint32_t vao = 0;
- uint32_t vbo = 0;
- uint32_t ebo = 0;
-
- bool renderingEnabled = true;
+ bool renderingEnabled_ = true;
// Moon phase system (two moons in Azeroth lore)
- float whiteLadyPhase_ = 0.5f; // 0.0-1.0 (0=new, 0.5=full) - primary moon
- float blueChildPhase_ = 0.25f; // 0.0-1.0 (0=new, 0.5=full) - secondary moon
- bool moonPhaseCycling = true;
- float moonPhaseTimer = 0.0f; // Fallback for deltaTime mode (development)
- float sunHazeTimer_ = 0.0f; // Always-running timer for sun haze animation
- bool dualMoonMode_ = true; // Default: render both moons (Azeroth-specific)
+ float whiteLadyPhase_ = 0.5f; // 0-1, 0=new, 0.5=full
+ float blueChildPhase_ = 0.25f; // 0-1
+ bool moonPhaseCycling_ = true;
+ float moonPhaseTimer_ = 0.0f; // Fallback deltaTime mode
+ float sunHazeTimer_ = 0.0f; // Always-running haze animation timer
+ bool dualMoonMode_ = true;
- // WoW lunar cycle constants (in game days)
- // WoW day = 24 real minutes, so these are ~realistic game-world cycles
- static constexpr float WHITE_LADY_CYCLE_DAYS = 30.0f; // ~12 real hours for full cycle
- static constexpr float BLUE_CHILD_CYCLE_DAYS = 27.0f; // ~10.8 real hours (slightly faster)
- static constexpr float MOON_CYCLE_DURATION = 240.0f; // Fallback: 4 minutes (deltaTime mode)
+ // WoW lunar cycle constants (game days; 1 game day = 24 real minutes)
+ static constexpr float WHITE_LADY_CYCLE_DAYS = 30.0f;
+ static constexpr float BLUE_CHILD_CYCLE_DAYS = 27.0f;
+ static constexpr float MOON_CYCLE_DURATION = 240.0f; // Fallback: 4 minutes
};
} // namespace rendering
diff --git a/include/rendering/character_preview.hpp b/include/rendering/character_preview.hpp
index 14ca587f..7ff5352c 100644
--- a/include/rendering/character_preview.hpp
+++ b/include/rendering/character_preview.hpp
@@ -1,7 +1,8 @@
#pragma once
#include "game/character.hpp"
-#include
+#include
+#include
#include
#include
#include
@@ -13,6 +14,9 @@ namespace rendering {
class CharacterRenderer;
class Camera;
+class VkContext;
+class VkTexture;
+class VkRenderTarget;
class CharacterPreview {
public:
@@ -34,7 +38,15 @@ public:
void render();
void rotate(float yawDelta);
- GLuint getTextureId() const { return colorTexture_; }
+ // Off-screen composite pass β call from Renderer::beginFrame() before main render pass
+ void compositePass(VkCommandBuffer cmd, uint32_t frameIndex);
+
+ // Mark that the preview needs compositing this frame (call from UI each frame)
+ void requestComposite() { compositeRequested_ = true; }
+
+ // Returns the ImGui texture handle. Returns VK_NULL_HANDLE until the first
+ // compositePass has run (image is in UNDEFINED layout before that).
+ VkDescriptorSet getTextureId() const { return compositeRendered_ ? imguiTextureId_ : VK_NULL_HANDLE; }
int getWidth() const { return fboWidth_; }
int getHeight() const { return fboHeight_; }
@@ -48,19 +60,36 @@ private:
void destroyFBO();
pipeline::AssetManager* assetManager_ = nullptr;
+ VkContext* vkCtx_ = nullptr;
std::unique_ptr charRenderer_;
std::unique_ptr camera_;
- GLuint fbo_ = 0;
- GLuint colorTexture_ = 0;
- GLuint depthRenderbuffer_ = 0;
+ // Off-screen render target (color + depth)
+ std::unique_ptr renderTarget_;
+
+ // Per-frame UBO for preview camera/lighting (double-buffered)
+ static constexpr uint32_t MAX_FRAMES = 2;
+ VkDescriptorPool previewDescPool_ = VK_NULL_HANDLE;
+ VkBuffer previewUBO_[MAX_FRAMES] = {};
+ VmaAllocation previewUBOAlloc_[MAX_FRAMES] = {};
+ void* previewUBOMapped_[MAX_FRAMES] = {};
+ VkDescriptorSet previewPerFrameSet_[MAX_FRAMES] = {};
+
+ // Dummy 1x1 white texture for shadow map placeholder
+ std::unique_ptr dummyWhiteTex_;
+
+ // ImGui texture handle for displaying the preview (VkDescriptorSet in Vulkan backend)
+ VkDescriptorSet imguiTextureId_ = VK_NULL_HANDLE;
+
static constexpr int fboWidth_ = 400;
static constexpr int fboHeight_ = 500;
static constexpr uint32_t PREVIEW_MODEL_ID = 9999;
uint32_t instanceId_ = 0;
bool modelLoaded_ = false;
- float modelYaw_ = 180.0f;
+ bool compositeRequested_ = false;
+ bool compositeRendered_ = false; // True after first successful compositePass
+ float modelYaw_ = 90.0f;
// Cached info from loadCharacter() for later recompositing.
game::Race race_ = game::Race::HUMAN;
diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp
index 30001846..9ee98a7d 100644
--- a/include/rendering/character_renderer.hpp
+++ b/include/rendering/character_renderer.hpp
@@ -1,22 +1,25 @@
#pragma once
#include "pipeline/m2_loader.hpp"
-#include
+#include
+#include
#include
#include
#include
#include
#include
#include
+#include
+#include
namespace wowee {
namespace pipeline { class AssetManager; }
namespace rendering {
// Forward declarations
-class Shader;
-class Texture;
class Camera;
+class VkContext;
+class VkTexture;
// Weapon attached to a character instance at a bone attachment point
struct WeaponAttachment {
@@ -33,7 +36,7 @@ struct WeaponAttachment {
* Features:
* - Skeletal animation with bone transformations
* - Keyframe interpolation (linear position/scale, slerp rotation)
- * - Vertex skinning (GPU-accelerated)
+ * - Vertex skinning (GPU-accelerated via bone SSBO)
* - Texture loading from BLP via AssetManager
*/
class CharacterRenderer {
@@ -41,7 +44,9 @@ public:
CharacterRenderer();
~CharacterRenderer();
- bool initialize();
+ bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* am,
+ VkRenderPass renderPassOverride = VK_NULL_HANDLE,
+ VkSampleCountFlagBits msaaSamples = VK_SAMPLE_COUNT_1_BIT);
void shutdown();
void setAssetManager(pipeline::AssetManager* am) { assetManager = am; }
@@ -56,8 +61,11 @@ public:
void update(float deltaTime, const glm::vec3& cameraPos = glm::vec3(0.0f));
- void render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection);
- void renderShadow(const glm::mat4& lightSpaceMatrix);
+ void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera);
+ void recreatePipelines();
+ bool initializeShadow(VkRenderPass shadowRenderPass);
+ void renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix,
+ const glm::vec3& shadowCenter = glm::vec3(0), float shadowRadius = 1e9f);
void setInstancePosition(uint32_t instanceId, const glm::vec3& position);
void setInstanceRotation(uint32_t instanceId, const glm::vec3& rotation);
@@ -65,8 +73,8 @@ public:
void startFadeIn(uint32_t instanceId, float durationSeconds);
const pipeline::M2Model* getModelData(uint32_t modelId) const;
void setActiveGeosets(uint32_t instanceId, const std::unordered_set& geosets);
- void setGroupTextureOverride(uint32_t instanceId, uint16_t geosetGroup, GLuint textureId);
- void setTextureSlotOverride(uint32_t instanceId, uint16_t textureSlot, GLuint textureId);
+ void setGroupTextureOverride(uint32_t instanceId, uint16_t geosetGroup, VkTexture* texture);
+ void setTextureSlotOverride(uint32_t instanceId, uint16_t textureSlot, VkTexture* texture);
void clearTextureSlotOverride(uint32_t instanceId, uint16_t textureSlot);
void setInstanceVisible(uint32_t instanceId, bool visible);
void removeInstance(uint32_t instanceId);
@@ -88,45 +96,38 @@ public:
/** Detach a weapon from the given attachment point. */
void detachWeapon(uint32_t charInstanceId, uint32_t attachmentId);
- /** Get the world-space transform of an attachment point on an instance.
- * Used for mount seats, weapon positions, etc.
- * @param instanceId The character/mount instance
- * @param attachmentId The attachment point ID (0=Mount, 1=RightHand, 2=LeftHand, etc.)
- * @param outTransform The resulting world-space transform matrix
- * @return true if attachment found and matrix computed
- */
+ /** Get the world-space transform of an attachment point on an instance. */
bool getAttachmentTransform(uint32_t instanceId, uint32_t attachmentId, glm::mat4& outTransform);
size_t getInstanceCount() const { return instances.size(); }
- void setFog(const glm::vec3& color, float start, float end) {
- fogColor = color; fogStart = start; fogEnd = end;
- }
+ // Normal mapping / POM settings
+ void setNormalMappingEnabled(bool enabled) { normalMappingEnabled_ = enabled; }
+ void setNormalMapStrength(float strength) { normalMapStrength_ = strength; }
+ void setPOMEnabled(bool enabled) { pomEnabled_ = enabled; }
+ void setPOMQuality(int quality) { pomQuality_ = quality; }
- void setLighting(const float lightDirIn[3], const float lightColorIn[3],
- const float ambientColorIn[3]) {
- lightDir = glm::vec3(lightDirIn[0], lightDirIn[1], lightDirIn[2]);
- lightColor = glm::vec3(lightColorIn[0], lightColorIn[1], lightColorIn[2]);
- ambientColor = glm::vec3(ambientColorIn[0], ambientColorIn[1], ambientColorIn[2]);
- }
-
- void setShadowMap(GLuint depthTex, const glm::mat4& lightSpace) {
- shadowDepthTex = depthTex; lightSpaceMatrix = lightSpace; shadowEnabled = true;
- }
- void clearShadowMap() { shadowEnabled = false; }
+ // Fog/lighting/shadow are now in per-frame UBO β keep stubs for callers that haven't been updated
+ void setFog(const glm::vec3&, float, float) {}
+ void setLighting(const float[3], const float[3], const float[3]) {}
+ void setShadowMap(VkTexture*, const glm::mat4&) {}
+ void clearShadowMap() {}
private:
// GPU representation of M2 model
struct M2ModelGPU {
- uint32_t vao = 0;
- uint32_t vbo = 0;
- uint32_t ebo = 0;
+ VkBuffer vertexBuffer = VK_NULL_HANDLE;
+ VmaAllocation vertexAlloc = VK_NULL_HANDLE;
+ VkBuffer indexBuffer = VK_NULL_HANDLE;
+ VmaAllocation indexAlloc = VK_NULL_HANDLE;
+ uint32_t indexCount = 0;
+ uint32_t vertexCount = 0;
pipeline::M2Model data; // Original model data
std::vector bindPose; // Inverse bind pose matrices
// Textures loaded from BLP (indexed by texture array position)
- std::vector textureIds;
+ std::vector textureIds;
};
// Character instance
@@ -151,11 +152,11 @@ private:
// Empty = render all (for non-character models)
std::unordered_set activeGeosets;
- // Per-geoset-group texture overrides (group β GL texture ID)
- std::unordered_map groupTextureOverrides;
+ // Per-geoset-group texture overrides (group β VkTexture*)
+ std::unordered_map groupTextureOverrides;
- // Per-texture-slot overrides (slot β GL texture ID)
- std::unordered_map textureSlotOverrides;
+ // Per-texture-slot overrides (slot β VkTexture*)
+ std::unordered_map textureSlotOverrides;
// Weapon attachments (weapons parented to this instance's bones)
std::vector weaponAttachments;
@@ -175,6 +176,12 @@ private:
// Override model matrix (used for weapon instances positioned by parent bone)
bool hasOverrideModelMatrix = false;
glm::mat4 overrideModelMatrix{1.0f};
+
+ // Per-instance bone SSBO (double-buffered per frame)
+ VkBuffer boneBuffer[2] = {};
+ VmaAllocation boneAlloc[2] = {};
+ void* boneMapped[2] = {};
+ VkDescriptorSet boneSet[2] = {};
};
void setupModelBuffers(M2ModelGPU& gpuModel);
@@ -183,6 +190,8 @@ private:
void calculateBoneMatrices(CharacterInstance& instance);
glm::mat4 getBoneTransform(const pipeline::M2Bone& bone, float time, int sequenceIndex);
glm::mat4 getModelMatrix(const CharacterInstance& instance) const;
+ void destroyModelGPU(M2ModelGPU& gpuModel);
+ void destroyInstanceBones(CharacterInstance& inst);
// Keyframe interpolation helpers
static int findKeyframeIndex(const std::vector& timestamps, float time);
@@ -194,84 +203,107 @@ private:
public:
/**
* Build a composited character skin texture by alpha-blending overlay
- * layers (e.g. underwear) onto a base skin BLP. Each overlay is placed
- * at the correct CharComponentTextureSections region based on its
- * filename (pelvis, torso, etc.). Returns the resulting GL texture ID.
+ * layers onto a base skin BLP. Returns the resulting VkTexture*.
*/
- GLuint compositeTextures(const std::vector& layerPaths);
+ VkTexture* compositeTextures(const std::vector& layerPaths);
/**
* Build a composited character skin with explicit region-based equipment overlays.
- * @param basePath Body skin texture path
- * @param baseLayers Underwear overlay paths (placed by filename keyword)
- * @param regionLayers Pairs of (region_index, blp_path) for equipment textures
- * @return GL texture ID of the composited result
*/
- GLuint compositeWithRegions(const std::string& basePath,
+ VkTexture* compositeWithRegions(const std::string& basePath,
const std::vector& baseLayers,
const std::vector>& regionLayers);
/** Clear the composite texture cache (forces re-compositing on next call). */
void clearCompositeCache();
- /** Load a BLP texture from MPQ and return the GL texture ID (cached). */
- GLuint loadTexture(const std::string& path);
- GLuint getTransparentTexture() const { return transparentTexture; }
+ /** Load a BLP texture from MPQ and return VkTexture* (cached). */
+ VkTexture* loadTexture(const std::string& path);
+ VkTexture* getTransparentTexture() const { return transparentTexture_.get(); }
- /** Replace a loaded model's texture at the given slot with a new GL texture. */
- void setModelTexture(uint32_t modelId, uint32_t textureSlot, GLuint textureId);
+ /** Replace a loaded model's texture at the given slot. */
+ void setModelTexture(uint32_t modelId, uint32_t textureSlot, VkTexture* texture);
/** Reset a model's texture slot back to white fallback. */
void resetModelTexture(uint32_t modelId, uint32_t textureSlot);
private:
- std::unique_ptr characterShader;
- GLuint shadowCasterProgram = 0;
+ VkContext* vkCtx_ = nullptr;
+ VkRenderPass renderPassOverride_ = VK_NULL_HANDLE;
+ VkSampleCountFlagBits msaaSamplesOverride_ = VK_SAMPLE_COUNT_1_BIT;
pipeline::AssetManager* assetManager = nullptr;
- // Fog parameters
- glm::vec3 fogColor = glm::vec3(0.5f, 0.6f, 0.7f);
- float fogStart = 400.0f;
- float fogEnd = 1200.0f;
+ // Vulkan pipelines (one per blend mode)
+ VkPipeline opaquePipeline_ = VK_NULL_HANDLE;
+ VkPipeline alphaTestPipeline_ = VK_NULL_HANDLE;
+ VkPipeline alphaPipeline_ = VK_NULL_HANDLE;
+ VkPipeline additivePipeline_ = VK_NULL_HANDLE;
+ VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE;
- // Lighting parameters
- glm::vec3 lightDir = glm::vec3(0.0f, -1.0f, 0.3f);
- glm::vec3 lightColor = glm::vec3(1.5f, 1.4f, 1.3f);
- glm::vec3 ambientColor = glm::vec3(0.4f, 0.4f, 0.45f);
+ // Descriptor set layouts
+ VkDescriptorSetLayout perFrameLayout_ = VK_NULL_HANDLE; // set 0 (owned by Renderer)
+ VkDescriptorSetLayout materialSetLayout_ = VK_NULL_HANDLE; // set 1
+ VkDescriptorSetLayout boneSetLayout_ = VK_NULL_HANDLE; // set 2
- // Shadow mapping
- GLuint shadowDepthTex = 0;
- glm::mat4 lightSpaceMatrix = glm::mat4(1.0f);
- bool shadowEnabled = false;
+ // Descriptor pool
+ VkDescriptorPool materialDescPools_[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
+ VkDescriptorPool boneDescPool_ = VK_NULL_HANDLE;
+ uint32_t lastMaterialPoolResetFrame_ = 0xFFFFFFFFu;
+ std::vector> transientMaterialUbos_[2];
// Texture cache
struct TextureCacheEntry {
- GLuint id = 0;
+ std::unique_ptr texture;
+ std::unique_ptr normalHeightMap;
+ float heightMapVariance = 0.0f;
size_t approxBytes = 0;
uint64_t lastUse = 0;
bool hasAlpha = false;
bool colorKeyBlack = false;
};
std::unordered_map textureCache;
- std::unordered_map textureHasAlphaById_;
- std::unordered_map textureColorKeyBlackById_;
- std::unordered_map compositeCache_; // key β GPU texture for reuse
- std::unordered_set failedTextureCache_; // negative cache for missing textures
+ std::unordered_map textureHasAlphaByPtr_;
+ std::unordered_map textureColorKeyBlackByPtr_;
+ std::unordered_map compositeCache_; // key β texture for reuse
+ std::unordered_set failedTextureCache_; // negative cache for budget exhaustion
+ std::unordered_set loggedTextureLoadFails_; // dedup warning logs
size_t textureCacheBytes_ = 0;
uint64_t textureCacheCounter_ = 0;
- size_t textureCacheBudgetBytes_ = 1024ull * 1024 * 1024; // Default, overridden at init
- GLuint whiteTexture = 0;
- GLuint transparentTexture = 0;
+ size_t textureCacheBudgetBytes_ = 1024ull * 1024 * 1024;
+ uint32_t textureBudgetRejectWarnings_ = 0;
+ std::unique_ptr whiteTexture_;
+ std::unique_ptr transparentTexture_;
+ std::unique_ptr flatNormalTexture_;
std::unordered_map models;
std::unordered_map instances;
uint32_t nextInstanceId = 1;
- // Maximum bones supported (GPU uniform limit)
- // WoW character models can have 210+ bones; GPU reports 4096 components (~256 mat4)
+ // Normal map generation (same algorithm as WMO renderer)
+ std::unique_ptr generateNormalHeightMap(
+ const uint8_t* pixels, uint32_t width, uint32_t height, float& outVariance);
+
+ // Normal mapping / POM settings
+ bool normalMappingEnabled_ = true;
+ float normalMapStrength_ = 0.8f;
+ bool pomEnabled_ = true;
+ int pomQuality_ = 1; // 0=Low(16), 1=Medium(32), 2=High(64)
+
+ // Maximum bones supported
static constexpr int MAX_BONES = 240;
+ uint32_t numAnimThreads_ = 1;
+ std::vector> animFutures_;
+
+ // Shadow pipeline resources
+ VkPipeline shadowPipeline_ = VK_NULL_HANDLE;
+ VkPipelineLayout shadowPipelineLayout_ = VK_NULL_HANDLE;
+ VkDescriptorSetLayout shadowParamsLayout_ = VK_NULL_HANDLE;
+ VkDescriptorPool shadowParamsPool_ = VK_NULL_HANDLE;
+ VkDescriptorSet shadowParamsSet_ = VK_NULL_HANDLE;
+ VkBuffer shadowParamsUBO_ = VK_NULL_HANDLE;
+ VmaAllocation shadowParamsAlloc_ = VK_NULL_HANDLE;
};
} // namespace rendering
diff --git a/include/rendering/charge_effect.hpp b/include/rendering/charge_effect.hpp
index 9319a601..4154df4d 100644
--- a/include/rendering/charge_effect.hpp
+++ b/include/rendering/charge_effect.hpp
@@ -1,8 +1,8 @@
#pragma once
-#include
+#include
+#include
#include
-#include
#include
#include
#include
@@ -12,7 +12,7 @@ namespace pipeline { class AssetManager; }
namespace rendering {
class Camera;
-class Shader;
+class VkContext;
class M2Renderer;
/// Renders a red-orange ribbon streak trailing behind the warrior during Charge,
@@ -22,8 +22,9 @@ public:
ChargeEffect();
~ChargeEffect();
- bool initialize();
+ bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
void shutdown();
+ void recreatePipelines();
/// Try to load M2 spell models (Charge_Caster.m2, etc.)
void tryLoadM2Models(M2Renderer* m2Renderer, pipeline::AssetManager* assets);
@@ -41,7 +42,7 @@ public:
void triggerImpact(const glm::vec3& position);
void update(float deltaTime);
- void render(const Camera& camera);
+ void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet);
bool isActive() const { return emitting_ || !trail_.empty() || !dustPuffs_.empty(); }
@@ -59,10 +60,17 @@ private:
static constexpr float TRAIL_SPAWN_DIST = 0.4f; // Min distance between trail points
std::deque trail_;
- GLuint ribbonVao_ = 0;
- GLuint ribbonVbo_ = 0;
- std::unique_ptr ribbonShader_;
- std::vector ribbonVerts_; // pos(3) + alpha(1) + heat(1) = 5 floats per vert
+ // Vulkan objects
+ VkContext* vkCtx_ = nullptr;
+
+ // Ribbon pipeline + dynamic buffer
+ VkPipeline ribbonPipeline_ = VK_NULL_HANDLE;
+ VkPipelineLayout ribbonPipelineLayout_ = VK_NULL_HANDLE;
+ ::VkBuffer ribbonDynamicVB_ = VK_NULL_HANDLE;
+ VmaAllocation ribbonDynamicVBAlloc_ = VK_NULL_HANDLE;
+ VmaAllocationInfo ribbonDynamicVBAllocInfo_{};
+ VkDeviceSize ribbonDynamicVBSize_ = 0;
+ std::vector ribbonVerts_; // pos(3) + alpha(1) + heat(1) + height(1) = 6 floats per vert
// --- Dust puffs (small point sprites at feet) ---
struct DustPuff {
@@ -77,9 +85,13 @@ private:
static constexpr int MAX_DUST = 80;
std::vector dustPuffs_;
- GLuint dustVao_ = 0;
- GLuint dustVbo_ = 0;
- std::unique_ptr dustShader_;
+ // Dust pipeline + dynamic buffer
+ VkPipeline dustPipeline_ = VK_NULL_HANDLE;
+ VkPipelineLayout dustPipelineLayout_ = VK_NULL_HANDLE;
+ ::VkBuffer dustDynamicVB_ = VK_NULL_HANDLE;
+ VmaAllocation dustDynamicVBAlloc_ = VK_NULL_HANDLE;
+ VmaAllocationInfo dustDynamicVBAllocInfo_{};
+ VkDeviceSize dustDynamicVBSize_ = 0;
std::vector dustVerts_;
bool emitting_ = false;
diff --git a/include/rendering/clouds.hpp b/include/rendering/clouds.hpp
index 39f8dcd6..800b5c0f 100644
--- a/include/rendering/clouds.hpp
+++ b/include/rendering/clouds.hpp
@@ -1,94 +1,96 @@
#pragma once
-#include
#include
-#include
+#include
+#include
#include
namespace wowee {
namespace rendering {
-class Camera;
-class Shader;
+class VkContext;
+struct SkyParams;
/**
- * @brief Renders procedural animated clouds on a sky dome
+ * Procedural cloud renderer (Vulkan)
*
- * Features:
- * - Procedural cloud generation using multiple noise layers
- * - Two cloud layers at different altitudes
- * - Animated wind movement
- * - Time-of-day color tinting (orange at sunrise/sunset)
- * - Transparency and soft edges
+ * Renders animated procedural clouds on a sky hemisphere using FBM noise.
+ * Sun-lit edges, self-shadowing, and DBC-driven cloud colors for realistic appearance.
+ *
+ * Pipeline layout:
+ * set 0 = perFrameLayout (camera UBO β view, projection, etc.)
+ * push = CloudPush (3 x vec4 = 48 bytes)
*/
class Clouds {
public:
Clouds();
~Clouds();
- /**
- * @brief Initialize cloud system (generate mesh and shaders)
- * @return true if initialization succeeded
- */
- bool initialize();
+ bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
+ void shutdown();
+ void recreatePipelines();
/**
- * @brief Render clouds
- * @param camera The camera to render from
- * @param timeOfDay Current time (0-24 hours)
+ * Render clouds using DBC-driven colors and sun lighting.
+ * @param cmd Command buffer to record into
+ * @param perFrameSet Per-frame descriptor set (set 0, camera UBO)
+ * @param params Sky parameters with DBC colors and sun direction
*/
- void render(const Camera& camera, float timeOfDay);
+ void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const SkyParams& params);
/**
- * @brief Update cloud animation
- * @param deltaTime Time since last frame
+ * Update cloud animation (wind drift).
*/
void update(float deltaTime);
- /**
- * @brief Enable or disable cloud rendering
- */
- void setEnabled(bool enabled) { this->enabled = enabled; }
- bool isEnabled() const { return enabled; }
+ // --- Enable / disable ---
+ void setEnabled(bool enabled) { enabled_ = enabled; }
+ bool isEnabled() const { return enabled_; }
- /**
- * @brief Set cloud density (0.0 = clear, 1.0 = overcast)
- */
+ // --- Cloud parameters ---
void setDensity(float density);
- float getDensity() const { return density; }
+ float getDensity() const { return density_; }
- /**
- * @brief Set wind speed multiplier
- */
- void setWindSpeed(float speed) { windSpeed = speed; }
- float getWindSpeed() const { return windSpeed; }
+ void setWindSpeed(float speed) { windSpeed_ = speed; }
+ float getWindSpeed() const { return windSpeed_; }
private:
+ // Push constant block β must match clouds.frag.glsl
+ struct CloudPush {
+ glm::vec4 cloudColor; // xyz = DBC-derived base cloud color, w = unused
+ glm::vec4 sunDirDensity; // xyz = sun direction, w = density
+ glm::vec4 windAndLight; // x = windOffset, y = sunIntensity, z = ambient, w = unused
+ };
+ static_assert(sizeof(CloudPush) == 48, "CloudPush size mismatch");
+
void generateMesh();
- void cleanup();
- glm::vec3 getCloudColor(float timeOfDay) const;
+ void createBuffers();
+ void destroyBuffers();
- // OpenGL objects
- GLuint vao = 0;
- GLuint vbo = 0;
- GLuint ebo = 0;
- std::unique_ptr shader;
+ // Vulkan objects
+ VkContext* vkCtx_ = nullptr;
+ VkPipeline pipeline_ = VK_NULL_HANDLE;
+ VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE;
+ VkBuffer vertexBuffer_ = VK_NULL_HANDLE;
+ VmaAllocation vertexAlloc_ = VK_NULL_HANDLE;
+ VkBuffer indexBuffer_ = VK_NULL_HANDLE;
+ VmaAllocation indexAlloc_ = VK_NULL_HANDLE;
- // Mesh data
- std::vector vertices;
- std::vector indices;
- int triangleCount = 0;
+ // Mesh data (CPU side, used during initialization only)
+ std::vector vertices_;
+ std::vector indices_;
+ int indexCount_ = 0;
// Cloud parameters
- bool enabled = true;
- float density = 0.5f; // Cloud coverage
- float windSpeed = 1.0f;
- float windOffset = 0.0f; // Accumulated wind movement
+ bool enabled_ = true;
+ float density_ = 0.35f;
+ float windSpeed_ = 1.0f;
+ float windOffset_ = 0.0f;
// Mesh generation parameters
- static constexpr int SEGMENTS = 32; // Horizontal segments
- static constexpr int RINGS = 8; // Vertical rings (only upper hemisphere)
- static constexpr float RADIUS = 900.0f; // Slightly smaller than skybox
+ static constexpr int SEGMENTS = 32;
+ static constexpr int RINGS = 8;
+ static constexpr float RADIUS = 900.0f;
};
} // namespace rendering
diff --git a/include/rendering/lens_flare.hpp b/include/rendering/lens_flare.hpp
index 4b390bcb..f4dac6fd 100644
--- a/include/rendering/lens_flare.hpp
+++ b/include/rendering/lens_flare.hpp
@@ -1,15 +1,15 @@
#pragma once
-#include
+#include
+#include
#include
-#include
#include
namespace wowee {
namespace rendering {
class Camera;
-class Shader;
+class VkContext;
/**
* @brief Renders lens flare effect when looking at the sun
@@ -28,17 +28,32 @@ public:
/**
* @brief Initialize lens flare system
+ * @param ctx Vulkan context
+ * @param perFrameLayout Per-frame descriptor set layout (unused, kept for API consistency)
* @return true if initialization succeeded
*/
- bool initialize();
+ bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
+
+ /**
+ * @brief Destroy Vulkan resources
+ */
+ void shutdown();
+
+ void recreatePipelines();
/**
* @brief Render lens flare effect
+ * @param cmd Command buffer to record into
* @param camera The camera to render from
* @param sunPosition World-space sun position
* @param timeOfDay Current time (0-24 hours)
+ * @param fogDensity Fog density 0-1 (attenuates flare)
+ * @param cloudDensity Cloud density 0-1 (attenuates flare)
+ * @param weatherIntensity Weather intensity 0-1 (rain/snow attenuates flare)
*/
- void render(const Camera& camera, const glm::vec3& sunPosition, float timeOfDay);
+ void render(VkCommandBuffer cmd, const Camera& camera, const glm::vec3& sunPosition,
+ float timeOfDay, float fogDensity = 0.0f, float cloudDensity = 0.0f,
+ float weatherIntensity = 0.0f);
/**
* @brief Enable or disable lens flare rendering
@@ -60,15 +75,24 @@ private:
float brightness; // Brightness multiplier
};
+ struct FlarePushConstants {
+ glm::vec2 position; // Screen-space position (-1 to 1)
+ float size; // Size in screen space
+ float aspectRatio; // Viewport aspect ratio
+ glm::vec4 colorBrightness; // RGB color + brightness in w
+ };
+
void generateFlareElements();
- void cleanup();
float calculateSunVisibility(const Camera& camera, const glm::vec3& sunPosition) const;
glm::vec2 worldToScreen(const Camera& camera, const glm::vec3& worldPos) const;
- // OpenGL objects
- GLuint vao = 0;
- GLuint vbo = 0;
- std::unique_ptr shader;
+ VkContext* vkCtx = nullptr;
+
+ VkPipeline pipeline = VK_NULL_HANDLE;
+ VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
+
+ VkBuffer vertexBuffer = VK_NULL_HANDLE;
+ VmaAllocation vertexAlloc = VK_NULL_HANDLE;
// Flare elements
std::vector flareElements;
diff --git a/include/rendering/lighting_manager.hpp b/include/rendering/lighting_manager.hpp
index f6b23627..11c079a5 100644
--- a/include/rendering/lighting_manager.hpp
+++ b/include/rendering/lighting_manager.hpp
@@ -28,7 +28,7 @@ struct LightingParams {
glm::vec3 skyBand1Color{0.9f, 0.95f, 1.0f}; // Sky band 1
glm::vec3 skyBand2Color{1.0f, 0.98f, 0.9f}; // Sky band 2
- float cloudDensity = 1.0f; // Cloud density/opacity
+ float cloudDensity = 0.3f; // Cloud density/opacity
float horizonGlow = 0.3f; // Horizon glow intensity
};
diff --git a/include/rendering/lightning.hpp b/include/rendering/lightning.hpp
index f4e94cfb..33152192 100644
--- a/include/rendering/lightning.hpp
+++ b/include/rendering/lightning.hpp
@@ -1,15 +1,15 @@
#pragma once
+#include
+#include
#include
-#include
#include
namespace wowee {
namespace rendering {
-// Forward declarations
-class Shader;
class Camera;
+class VkContext;
/**
* Lightning system for thunder storm effects
@@ -26,11 +26,12 @@ public:
Lightning();
~Lightning();
- bool initialize();
+ bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
void shutdown();
+ void recreatePipelines();
void update(float deltaTime, const Camera& camera);
- void render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection);
+ void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet);
// Control
void setEnabled(bool enabled);
@@ -68,8 +69,8 @@ private:
void updateFlash(float deltaTime);
void spawnRandomStrike(const glm::vec3& cameraPos);
- void renderBolts(const glm::mat4& viewProj);
- void renderFlash();
+ void renderBolts(VkCommandBuffer cmd, VkDescriptorSet perFrameSet);
+ void renderFlash(VkCommandBuffer cmd);
bool enabled = true;
float intensity = 0.5f; // Strike frequency multiplier
@@ -82,13 +83,22 @@ private:
std::vector bolts;
Flash flash;
- // Rendering
- std::unique_ptr boltShader;
- std::unique_ptr flashShader;
- unsigned int boltVAO = 0;
- unsigned int boltVBO = 0;
- unsigned int flashVAO = 0;
- unsigned int flashVBO = 0;
+ // Vulkan objects
+ VkContext* vkCtx = nullptr;
+
+ // Bolt pipeline + dynamic buffer
+ VkPipeline boltPipeline = VK_NULL_HANDLE;
+ VkPipelineLayout boltPipelineLayout = VK_NULL_HANDLE;
+ ::VkBuffer boltDynamicVB = VK_NULL_HANDLE;
+ VmaAllocation boltDynamicVBAlloc = VK_NULL_HANDLE;
+ VmaAllocationInfo boltDynamicVBAllocInfo{};
+ VkDeviceSize boltDynamicVBSize = 0;
+
+ // Flash pipeline + static quad buffer
+ VkPipeline flashPipeline = VK_NULL_HANDLE;
+ VkPipelineLayout flashPipelineLayout = VK_NULL_HANDLE;
+ ::VkBuffer flashQuadVB = VK_NULL_HANDLE;
+ VmaAllocation flashQuadVBAlloc = VK_NULL_HANDLE;
// Configuration
static constexpr int MAX_BOLTS = 3;
diff --git a/include/rendering/loading_screen.hpp b/include/rendering/loading_screen.hpp
index 3d5272bb..52f0ea08 100644
--- a/include/rendering/loading_screen.hpp
+++ b/include/rendering/loading_screen.hpp
@@ -1,12 +1,14 @@
#pragma once
-#include
+#include
#include
#include
namespace wowee {
namespace rendering {
+class VkContext;
+
class LoadingScreen {
public:
LoadingScreen();
@@ -15,32 +17,28 @@ public:
bool initialize();
void shutdown();
- // Select a random loading screen image
void selectRandomImage();
- // Render the loading screen with progress bar and status text
+ // Render the loading screen with progress bar and status text (pure ImGui)
void render();
- // Update loading progress (0.0 to 1.0)
void setProgress(float progress) { loadProgress = progress; }
-
- // Set loading status text
void setStatus(const std::string& status) { statusText = status; }
+ // Must be set before initialize() for Vulkan texture upload
+ void setVkContext(VkContext* ctx) { vkCtx = ctx; }
+
private:
bool loadImage(const std::string& path);
- void createQuad();
- void createBarQuad();
- GLuint textureId = 0;
- GLuint vao = 0;
- GLuint vbo = 0;
- GLuint shaderId = 0;
+ VkContext* vkCtx = nullptr;
- // Progress bar GL objects
- GLuint barVao = 0;
- GLuint barVbo = 0;
- GLuint barShaderId = 0;
+ // Vulkan texture for background image
+ VkImage bgImage = VK_NULL_HANDLE;
+ VkDeviceMemory bgMemory = VK_NULL_HANDLE;
+ VkImageView bgImageView = VK_NULL_HANDLE;
+ VkSampler bgSampler = VK_NULL_HANDLE;
+ VkDescriptorSet bgDescriptorSet = VK_NULL_HANDLE; // ImGui texture handle
std::vector imagePaths;
int currentImageIndex = 0;
diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp
index c6c3a32c..c2a663b5 100644
--- a/include/rendering/m2_renderer.hpp
+++ b/include/rendering/m2_renderer.hpp
@@ -1,7 +1,8 @@
#pragma once
#include "pipeline/m2_loader.hpp"
-#include
+#include
+#include
#include
#include
#include
@@ -20,15 +21,20 @@ namespace pipeline {
namespace rendering {
-class Shader;
class Camera;
+class VkContext;
+class VkTexture;
/**
* GPU representation of an M2 model
*/
struct M2ModelGPU {
struct BatchGPU {
- GLuint texture = 0;
+ VkTexture* texture = nullptr; // from cache, NOT owned
+ VkDescriptorSet materialSet = VK_NULL_HANDLE; // set 1
+ ::VkBuffer materialUBO = VK_NULL_HANDLE;
+ VmaAllocation materialUBOAlloc = VK_NULL_HANDLE;
+ void* materialUBOMapped = nullptr; // cached mapped pointer (avoids per-frame vmaGetAllocationInfo)
uint32_t indexStart = 0; // offset in indices (not bytes)
uint32_t indexCount = 0;
bool hasAlpha = false;
@@ -47,9 +53,10 @@ struct M2ModelGPU {
float glowSize = 1.0f; // Approx radius of batch geometry
};
- GLuint vao = 0;
- GLuint vbo = 0;
- GLuint ebo = 0;
+ ::VkBuffer vertexBuffer = VK_NULL_HANDLE;
+ VmaAllocation vertexAlloc = VK_NULL_HANDLE;
+ ::VkBuffer indexBuffer = VK_NULL_HANDLE;
+ VmaAllocation indexAlloc = VK_NULL_HANDLE;
uint32_t indexCount = 0;
uint32_t vertexCount = 0;
std::vector batches;
@@ -69,6 +76,8 @@ struct M2ModelGPU {
bool isSmallFoliage = false; // Small foliage (bushes, grass, plants) - skip during taxi
bool isInvisibleTrap = false; // Invisible trap objects (don't render, no collision)
bool isGroundDetail = false; // Ground clutter/detail doodads (special fallback render path)
+ bool isWaterVegetation = false; // Cattails, reeds, kelp etc. near water (insect spawning)
+ bool isFireflyEffect = false; // Firefly/fireflies M2 (exempt from particle dampeners)
// Collision mesh with spatial grid (from M2 bounding geometry)
struct CollisionMesh {
@@ -105,18 +114,22 @@ struct M2ModelGPU {
bool isSpellEffect = false; // True for spell effect models (skip particle dampeners)
bool disableAnimation = false; // Keep foliage/tree doodads visually stable
bool shadowWindFoliage = false; // Apply wind sway in shadow pass for foliage/tree cards
+ bool isFoliageLike = false; // Model name matches foliage/tree/bush/grass etc (precomputed)
+ bool isElvenLike = false; // Model name matches elf/elven/quel (precomputed)
+ bool isLanternLike = false; // Model name matches lantern/lamp/light (precomputed)
+ bool isKoboldFlame = false; // Model name matches kobold+(candle/torch/mine) (precomputed)
bool hasTextureAnimation = false; // True if any batch has UV animation
// Particle emitter data (kept from M2Model)
std::vector particleEmitters;
- std::vector particleTextures; // Resolved GL textures per emitter
+ std::vector particleTextures; // Resolved Vulkan textures per emitter
// Texture transform data for UV animation
std::vector textureTransforms;
std::vector textureTransformLookup;
std::vector idleVariationIndices; // Sequence indices for idle variations (animId 0)
- bool isValid() const { return vao != 0 && indexCount > 0; }
+ bool isValid() const { return vertexBuffer != VK_NULL_HANDLE && indexCount > 0; }
};
/**
@@ -164,6 +177,12 @@ struct M2Instance {
// Frame-skip optimization (update distant animations less frequently)
uint8_t frameSkipCounter = 0;
+ // Per-instance bone SSBO (double-buffered)
+ ::VkBuffer boneBuffer[2] = {};
+ VmaAllocation boneAlloc[2] = {};
+ void* boneMapped[2] = {};
+ VkDescriptorSet boneSet[2] = {};
+
void updateModelMatrix();
};
@@ -180,8 +199,29 @@ struct SmokeParticle {
uint32_t instanceId = 0;
};
+// M2 material UBO β matches M2Material in m2.frag.glsl (set 1, binding 2)
+struct M2MaterialUBO {
+ int32_t hasTexture;
+ int32_t alphaTest;
+ int32_t colorKeyBlack;
+ float colorKeyThreshold;
+ int32_t unlit;
+ int32_t blendMode;
+ float fadeAlpha;
+ float interiorDarken;
+ float specularIntensity;
+};
+
+// M2 params UBO β matches M2Params in m2.vert.glsl (set 1, binding 1)
+struct M2ParamsUBO {
+ float uvOffsetX;
+ float uvOffsetY;
+ int32_t texCoordSet;
+ int32_t useBones;
+};
+
/**
- * M2 Model Renderer
+ * M2 Model Renderer (Vulkan)
*
* Handles rendering of M2 models (doodads like trees, rocks, bushes)
*/
@@ -190,137 +230,59 @@ public:
M2Renderer();
~M2Renderer();
- bool initialize(pipeline::AssetManager* assets);
+ bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout,
+ pipeline::AssetManager* assets);
void shutdown();
- /**
- * Check if a model is already loaded
- * @param modelId ID to check
- * @return True if model is loaded
- */
bool hasModel(uint32_t modelId) const;
-
- /**
- * Load an M2 model to GPU
- * @param model Parsed M2 model data
- * @param modelId Unique ID for this model
- * @return True if successful
- */
bool loadModel(const pipeline::M2Model& model, uint32_t modelId);
- /**
- * Create an instance of a loaded model
- * @param modelId ID of the loaded model
- * @param position World position
- * @param rotation Rotation in degrees (x, y, z)
- * @param scale Scale factor (1.0 = normal)
- * @return Instance ID
- */
uint32_t createInstance(uint32_t modelId, const glm::vec3& position,
const glm::vec3& rotation = glm::vec3(0.0f),
float scale = 1.0f);
-
- /**
- * Create an instance with a pre-computed model matrix
- * Used for WMO doodads where the full transform is computed externally
- */
uint32_t createInstanceWithMatrix(uint32_t modelId, const glm::mat4& modelMatrix,
const glm::vec3& position);
- /**
- * Update animation state for all instances
- * @param deltaTime Time since last frame
- * @param cameraPos Camera world position (for frustum-culling bones)
- * @param viewProjection Combined view*projection matrix
- */
void update(float deltaTime, const glm::vec3& cameraPos, const glm::mat4& viewProjection);
/**
- * Render all visible instances
+ * Render all visible instances (Vulkan)
*/
- void render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection);
+ void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera);
+
+ /**
+ * Initialize shadow pipeline (Phase 7)
+ */
+ bool initializeShadow(VkRenderPass shadowRenderPass);
/**
* Render depth-only pass for shadow casting
*/
- void renderShadow(GLuint shadowShaderProgram, const glm::vec3& shadowCenter, float halfExtent);
+ void renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix, float globalTime = 0.0f,
+ const glm::vec3& shadowCenter = glm::vec3(0), float shadowRadius = 1e9f);
/**
- * Render smoke particles (call after render())
+ * Render M2 particle emitters (point sprites)
*/
- void renderSmokeParticles(const Camera& camera, const glm::mat4& view, const glm::mat4& projection);
+ void renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrameSet);
/**
- * Render M2 particle emitter particles (call after renderSmokeParticles())
+ * Render smoke particles from chimneys etc.
*/
- void renderM2Particles(const glm::mat4& view, const glm::mat4& proj);
+ void renderSmokeParticles(VkCommandBuffer cmd, VkDescriptorSet perFrameSet);
- /**
- * Update the world position of an existing instance (e.g., for transports)
- * @param instanceId Instance ID returned by createInstance()
- * @param position New world position
- */
void setInstancePosition(uint32_t instanceId, const glm::vec3& position);
-
- /**
- * Update the full transform of an existing instance (e.g., for WMO doodads following parent WMO)
- * @param instanceId Instance ID returned by createInstance()
- * @param transform New world transform matrix
- */
void setInstanceTransform(uint32_t instanceId, const glm::mat4& transform);
-
- /**
- * Remove a specific instance by ID
- * @param instanceId Instance ID returned by createInstance()
- */
+ void setInstanceAnimationFrozen(uint32_t instanceId, bool frozen);
void removeInstance(uint32_t instanceId);
- /**
- * Remove multiple instances with one spatial-index rebuild.
- */
void removeInstances(const std::vector& instanceIds);
-
- /**
- * Clear all models and instances
- */
void clear();
-
- /**
- * Remove models that have no instances referencing them
- * Call periodically to free GPU memory
- */
void cleanupUnusedModels();
- /**
- * Check collision with M2 objects and adjust position
- * @param from Starting position
- * @param to Desired position
- * @param adjustedPos Output adjusted position
- * @param playerRadius Collision radius of player
- * @return true if collision occurred
- */
bool checkCollision(const glm::vec3& from, const glm::vec3& to,
glm::vec3& adjustedPos, float playerRadius = 0.5f) const;
-
- /**
- * Approximate top surface height for standing/jumping on doodads.
- * @param glX World X
- * @param glY World Y
- * @param glZ Query/reference Z (used to ignore unreachable tops)
- */
std::optional getFloorHeight(float glX, float glY, float glZ, float* outNormalZ = nullptr) const;
-
- /**
- * Raycast against M2 bounding boxes for camera collision
- * @param origin Ray origin (e.g., character head position)
- * @param direction Ray direction (normalized)
- * @param maxDistance Maximum ray distance to check
- * @return Distance to first intersection, or maxDistance if no hit
- */
float raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3& direction, float maxDistance) const;
-
- /**
- * Limit expensive collision/raycast queries to objects near a focus point.
- */
void setCollisionFocus(const glm::vec3& worldPos, float radius);
void clearCollisionFocus();
@@ -328,6 +290,8 @@ public:
double getQueryTimeMs() const { return queryTimeMs; }
uint32_t getQueryCallCount() const { return queryCallCount; }
+ void recreatePipelines();
+
// Stats
bool isInitialized() const { return initialized_; }
uint32_t getModelCount() const { return static_cast(models.size()); }
@@ -335,69 +299,99 @@ public:
uint32_t getTotalTriangleCount() const;
uint32_t getDrawCallCount() const { return lastDrawCallCount; }
- void setFog(const glm::vec3& color, float start, float end) {
- fogColor = color; fogStart = start; fogEnd = end;
- }
-
- void setLighting(const float lightDirIn[3], const float lightColorIn[3],
- const float ambientColorIn[3]) {
- lightDir = glm::vec3(lightDirIn[0], lightDirIn[1], lightDirIn[2]);
- lightColor = glm::vec3(lightColorIn[0], lightColorIn[1], lightColorIn[2]);
- ambientColor = glm::vec3(ambientColorIn[0], ambientColorIn[1], ambientColorIn[2]);
- }
-
- void setShadowMap(GLuint depthTex, const glm::mat4& lightSpace) {
- shadowDepthTex = depthTex; lightSpaceMatrix = lightSpace; shadowEnabled = true;
- }
- void clearShadowMap() { shadowEnabled = false; }
+ // Lighting/fog/shadow are now in per-frame UBO; these are no-ops for API compat
+ void setFog(const glm::vec3& /*color*/, float /*start*/, float /*end*/) {}
+ void setLighting(const float /*lightDirIn*/[3], const float /*lightColorIn*/[3],
+ const float /*ambientColorIn*/[3]) {}
+ void setShadowMap(uint32_t /*depthTex*/, const glm::mat4& /*lightSpace*/) {}
+ void clearShadowMap() {}
void setInsideInterior(bool inside) { insideInterior = inside; }
void setOnTaxi(bool onTaxi) { onTaxi_ = onTaxi; }
+ std::vector getWaterVegetationPositions(const glm::vec3& camPos, float maxDist) const;
+
private:
bool initialized_ = false;
bool insideInterior = false;
bool onTaxi_ = false;
pipeline::AssetManager* assetManager = nullptr;
- std::unique_ptr shader;
+
+ // Vulkan context
+ VkContext* vkCtx_ = nullptr;
+
+ // Vulkan pipelines (one per blend mode)
+ VkPipeline opaquePipeline_ = VK_NULL_HANDLE; // blend mode 0
+ VkPipeline alphaTestPipeline_ = VK_NULL_HANDLE; // blend mode 1
+ VkPipeline alphaPipeline_ = VK_NULL_HANDLE; // blend mode 2
+ VkPipeline additivePipeline_ = VK_NULL_HANDLE; // blend mode 3+
+ VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE;
+
+ // Shadow rendering (Phase 7)
+ VkPipeline shadowPipeline_ = VK_NULL_HANDLE;
+ VkPipelineLayout shadowPipelineLayout_ = VK_NULL_HANDLE;
+ VkDescriptorSetLayout shadowParamsLayout_ = VK_NULL_HANDLE;
+ VkDescriptorPool shadowParamsPool_ = VK_NULL_HANDLE;
+ VkDescriptorSet shadowParamsSet_ = VK_NULL_HANDLE;
+ ::VkBuffer shadowParamsUBO_ = VK_NULL_HANDLE;
+ VmaAllocation shadowParamsAlloc_ = VK_NULL_HANDLE;
+ // Per-frame pool for foliage shadow texture descriptor sets
+ VkDescriptorPool shadowTexPool_ = VK_NULL_HANDLE;
+
+ // Particle pipelines
+ VkPipeline particlePipeline_ = VK_NULL_HANDLE; // M2 emitter particles
+ VkPipeline particleAdditivePipeline_ = VK_NULL_HANDLE; // Additive particle blend
+ VkPipelineLayout particlePipelineLayout_ = VK_NULL_HANDLE;
+ VkPipeline smokePipeline_ = VK_NULL_HANDLE; // Smoke particles
+ VkPipelineLayout smokePipelineLayout_ = VK_NULL_HANDLE;
+
+ // Descriptor set layouts
+ VkDescriptorSetLayout materialSetLayout_ = VK_NULL_HANDLE; // set 1
+ VkDescriptorSetLayout boneSetLayout_ = VK_NULL_HANDLE; // set 2
+ VkDescriptorSetLayout particleTexLayout_ = VK_NULL_HANDLE; // particle set 1 (texture only)
+
+ // Descriptor pools
+ VkDescriptorPool materialDescPool_ = VK_NULL_HANDLE;
+ VkDescriptorPool boneDescPool_ = VK_NULL_HANDLE;
+ static constexpr uint32_t MAX_MATERIAL_SETS = 8192;
+ static constexpr uint32_t MAX_BONE_SETS = 2048;
+
+ // Dynamic particle buffers
+ ::VkBuffer smokeVB_ = VK_NULL_HANDLE;
+ VmaAllocation smokeVBAlloc_ = VK_NULL_HANDLE;
+ void* smokeVBMapped_ = nullptr;
+ ::VkBuffer m2ParticleVB_ = VK_NULL_HANDLE;
+ VmaAllocation m2ParticleVBAlloc_ = VK_NULL_HANDLE;
+ void* m2ParticleVBMapped_ = nullptr;
std::unordered_map models;
std::vector instances;
uint32_t nextInstanceId = 1;
uint32_t lastDrawCallCount = 0;
+ size_t modelCacheLimit_ = 6000;
+ uint32_t modelLimitRejectWarnings_ = 0;
- GLuint loadTexture(const std::string& path, uint32_t texFlags = 0);
+ VkTexture* loadTexture(const std::string& path, uint32_t texFlags = 0);
struct TextureCacheEntry {
- GLuint id = 0;
+ std::unique_ptr texture;
size_t approxBytes = 0;
uint64_t lastUse = 0;
bool hasAlpha = true;
bool colorKeyBlack = false;
};
std::unordered_map textureCache;
- std::unordered_map textureHasAlphaById_;
- std::unordered_map textureColorKeyBlackById_;
+ std::unordered_map textureHasAlphaByPtr_;
+ std::unordered_map textureColorKeyBlackByPtr_;
size_t textureCacheBytes_ = 0;
uint64_t textureCacheCounter_ = 0;
- size_t textureCacheBudgetBytes_ = 2048ull * 1024 * 1024; // Default, overridden at init
- GLuint whiteTexture = 0;
- GLuint glowTexture = 0; // Soft radial gradient for glow sprites
-
- // Lighting uniforms
- glm::vec3 lightDir = glm::vec3(0.5f, 0.5f, 1.0f);
- glm::vec3 lightColor = glm::vec3(1.5f, 1.4f, 1.3f);
- glm::vec3 ambientColor = glm::vec3(0.4f, 0.4f, 0.45f);
-
- // Fog parameters
- glm::vec3 fogColor = glm::vec3(0.5f, 0.6f, 0.7f);
- float fogStart = 400.0f;
- float fogEnd = 1200.0f;
-
- // Shadow mapping
- GLuint shadowDepthTex = 0;
- glm::mat4 lightSpaceMatrix = glm::mat4(1.0f);
- bool shadowEnabled = false;
+ size_t textureCacheBudgetBytes_ = 2048ull * 1024 * 1024;
+ std::unordered_set failedTextureCache_;
+ std::unordered_set loggedTextureLoadFails_;
+ uint32_t textureBudgetRejectWarnings_ = 0;
+ std::unique_ptr whiteTexture_;
+ std::unique_ptr glowTexture_;
+ VkDescriptorSet glowTexDescSet_ = VK_NULL_HANDLE; // cached glow texture descriptor (allocated once)
// Optional query-space culling for collision/raycast hot paths.
bool collisionFocusEnabled = false;
@@ -458,17 +452,11 @@ private:
// Smoke particle system
std::vector smokeParticles;
- GLuint smokeVAO = 0;
- GLuint smokeVBO = 0;
- std::unique_ptr smokeShader;
static constexpr int MAX_SMOKE_PARTICLES = 1000;
float smokeEmitAccum = 0.0f;
std::mt19937 smokeRng{42};
// M2 particle emitter system
- GLuint m2ParticleShader_ = 0;
- GLuint m2ParticleVAO_ = 0;
- GLuint m2ParticleVBO_ = 0;
static constexpr size_t MAX_M2_PARTICLES = 4000;
std::mt19937 particleRng_{123};
@@ -486,6 +474,15 @@ private:
glm::vec3 interpFBlockVec3(const pipeline::M2FBlock& fb, float lifeRatio);
void emitParticles(M2Instance& inst, const M2ModelGPU& gpu, float dt);
void updateParticles(M2Instance& inst, float dt);
+
+ // Helper to allocate descriptor sets
+ VkDescriptorSet allocateMaterialSet();
+ VkDescriptorSet allocateBoneSet();
+
+ // Helper to destroy model GPU resources
+ void destroyModelGPU(M2ModelGPU& model);
+ // Helper to destroy instance bone buffers
+ void destroyInstanceBones(M2Instance& inst);
};
} // namespace rendering
diff --git a/include/rendering/minimap.hpp b/include/rendering/minimap.hpp
index bd34d338..ca7c5345 100644
--- a/include/rendering/minimap.hpp
+++ b/include/rendering/minimap.hpp
@@ -1,6 +1,7 @@
#pragma once
-#include
+#include
+#include
#include
#include
#include
@@ -12,22 +13,30 @@ namespace wowee {
namespace pipeline { class AssetManager; }
namespace rendering {
-class Shader;
class Camera;
+class VkContext;
+class VkTexture;
+class VkRenderTarget;
class Minimap {
public:
Minimap();
~Minimap();
- bool initialize(int size = 200);
+ bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, int size = 200);
void shutdown();
+ void recreatePipelines();
void setAssetManager(pipeline::AssetManager* am) { assetManager = am; }
void setMapName(const std::string& name);
- void render(const Camera& playerCamera, const glm::vec3& centerWorldPos,
- int screenWidth, int screenHeight);
+ /// Off-screen composite pass β call BEFORE the main render pass begins.
+ void compositePass(VkCommandBuffer cmd, const glm::vec3& centerWorldPos);
+
+ /// Display quad β call INSIDE the main render pass.
+ void render(VkCommandBuffer cmd, const Camera& playerCamera,
+ const glm::vec3& centerWorldPos, int screenWidth, int screenHeight,
+ float playerOrientation = 0.0f, bool hasPlayerOrientation = false);
void setEnabled(bool enabled) { this->enabled = enabled; }
bool isEnabled() const { return enabled; }
@@ -44,18 +53,18 @@ public:
void zoomIn() { viewRadius = std::max(100.0f, viewRadius - 50.0f); }
void zoomOut() { viewRadius = std::min(800.0f, viewRadius + 50.0f); }
+ void setOpacity(float opacity) { opacity_ = opacity; }
+
// Public accessors for WorldMap
- GLuint getOrLoadTileTexture(int tileX, int tileY);
+ VkTexture* getOrLoadTileTexture(int tileX, int tileY);
void ensureTRSParsed() { if (!trsParsed) parseTRS(); }
- GLuint getTileQuadVAO() const { return tileQuadVAO; }
const std::string& getMapName() const { return mapName; }
private:
void parseTRS();
- void compositeTilesToFBO(const glm::vec3& centerWorldPos);
- void renderQuad(const Camera& playerCamera, const glm::vec3& centerWorldPos,
- int screenWidth, int screenHeight);
+ void updateTileDescriptors(uint32_t frameIdx, int centerTileX, int centerTileY);
+ VkContext* vkCtx = nullptr;
pipeline::AssetManager* assetManager = nullptr;
std::string mapName = "Azeroth";
@@ -63,31 +72,40 @@ private:
std::unordered_map trsLookup;
bool trsParsed = false;
- // Tile texture cache: hash β GL texture ID
- std::unordered_map tileTextureCache;
- GLuint noDataTexture = 0; // dark fallback for missing tiles
+ // Tile texture cache: hash β VkTexture
+ std::unordered_map> tileTextureCache;
+ std::unique_ptr noDataTexture;
- // Composite FBO (3x3 tiles = 768x768)
- GLuint compositeFBO = 0;
- GLuint compositeTexture = 0;
+ // Composite render target (3x3 tiles = 768x768)
+ std::unique_ptr compositeTarget;
static constexpr int TILE_PX = 256;
static constexpr int COMPOSITE_PX = TILE_PX * 3; // 768
- // Tile compositing quad
- GLuint tileQuadVAO = 0;
- GLuint tileQuadVBO = 0;
- std::unique_ptr tileShader;
+ // Shared quad vertex buffer (6 verts, pos2 + uv2 = 16 bytes/vert)
+ ::VkBuffer quadVB = VK_NULL_HANDLE;
+ VmaAllocation quadVBAlloc = VK_NULL_HANDLE;
- // Screen quad
- GLuint quadVAO = 0;
- GLuint quadVBO = 0;
- std::unique_ptr quadShader;
+ // Descriptor resources (shared layout: 1 combined image sampler at binding 0)
+ VkDescriptorSetLayout samplerSetLayout = VK_NULL_HANDLE;
+ VkDescriptorPool descPool = VK_NULL_HANDLE;
+ static constexpr uint32_t MAX_DESC_SETS = 24;
+
+ // Tile composite pipeline (renders into VkRenderTarget)
+ VkPipeline tilePipeline = VK_NULL_HANDLE;
+ VkPipelineLayout tilePipelineLayout = VK_NULL_HANDLE;
+ VkDescriptorSet tileDescSets[2][9] = {}; // [frameInFlight][tileSlot]
+
+ // Display pipeline (renders into main render pass)
+ VkPipeline displayPipeline = VK_NULL_HANDLE;
+ VkPipelineLayout displayPipelineLayout = VK_NULL_HANDLE;
+ VkDescriptorSet displayDescSet = VK_NULL_HANDLE;
int mapSize = 200;
- float viewRadius = 400.0f; // world units visible in minimap radius
+ float viewRadius = 400.0f;
bool enabled = true;
bool rotateWithCamera = false;
bool squareShape = false;
+ float opacity_ = 1.0f;
// Throttling
float updateIntervalSec = 0.25f;
diff --git a/include/rendering/mount_dust.hpp b/include/rendering/mount_dust.hpp
index fa729fa9..de0cce60 100644
--- a/include/rendering/mount_dust.hpp
+++ b/include/rendering/mount_dust.hpp
@@ -1,29 +1,30 @@
#pragma once
-#include
+#include
+#include
#include
-#include
#include
namespace wowee {
namespace rendering {
class Camera;
-class Shader;
+class VkContext;
class MountDust {
public:
MountDust();
~MountDust();
- bool initialize();
+ bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
void shutdown();
+ void recreatePipelines();
// Spawn dust particles at mount feet when moving on ground
void spawnDust(const glm::vec3& position, const glm::vec3& velocity, bool isMoving);
void update(float deltaTime);
- void render(const Camera& camera);
+ void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet);
private:
struct Particle {
@@ -38,11 +39,18 @@ private:
static constexpr int MAX_DUST_PARTICLES = 300;
std::vector particles;
- GLuint vao = 0;
- GLuint vbo = 0;
- std::unique_ptr shader;
- std::vector vertexData;
+ // Vulkan objects
+ VkContext* vkCtx = nullptr;
+ VkPipeline pipeline = VK_NULL_HANDLE;
+ VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
+ // Dynamic mapped buffer for particle vertex data (updated every frame)
+ ::VkBuffer dynamicVB = VK_NULL_HANDLE;
+ VmaAllocation dynamicVBAlloc = VK_NULL_HANDLE;
+ VmaAllocationInfo dynamicVBAllocInfo{};
+ VkDeviceSize dynamicVBSize = 0;
+
+ std::vector vertexData;
float spawnAccum = 0.0f;
};
diff --git a/include/rendering/quest_marker_renderer.hpp b/include/rendering/quest_marker_renderer.hpp
index 47e4e044..2d6a73d3 100644
--- a/include/rendering/quest_marker_renderer.hpp
+++ b/include/rendering/quest_marker_renderer.hpp
@@ -1,15 +1,20 @@
#pragma once
#include
+#include
+#include
#include
#include
#include
+#include "rendering/vk_texture.hpp"
+
namespace wowee {
namespace pipeline { class AssetManager; }
namespace rendering {
class Camera;
+class VkContext;
/**
* Renders quest markers as billboarded sprites above NPCs
@@ -20,8 +25,9 @@ public:
QuestMarkerRenderer();
~QuestMarkerRenderer();
- bool initialize(pipeline::AssetManager* assetManager);
+ bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* assetManager);
void shutdown();
+ void recreatePipelines();
/**
* Add or update a quest marker at a position
@@ -44,8 +50,11 @@ public:
/**
* Render all quest markers (call after world rendering, before UI)
+ * @param cmd Command buffer to record into
+ * @param perFrameSet Per-frame descriptor set (set 0, contains camera UBO)
+ * @param camera Camera for billboard calculation (CPU-side view matrix)
*/
- void render(const Camera& camera);
+ void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera);
private:
struct Marker {
@@ -55,16 +64,29 @@ private:
};
std::unordered_map markers_;
-
- // OpenGL resources
- uint32_t vao_ = 0;
- uint32_t vbo_ = 0;
- uint32_t shaderProgram_ = 0;
- uint32_t textures_[3] = {0, 0, 0}; // available, turnin, incomplete
+
+ // Vulkan context
+ VkContext* vkCtx_ = nullptr;
+
+ // Pipeline
+ VkPipeline pipeline_ = VK_NULL_HANDLE;
+ VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE;
+
+ // Descriptor resources for per-material texture (set 1)
+ VkDescriptorSetLayout materialSetLayout_ = VK_NULL_HANDLE;
+ VkDescriptorPool descriptorPool_ = VK_NULL_HANDLE;
+ VkDescriptorSet texDescSets_[3] = {VK_NULL_HANDLE, VK_NULL_HANDLE, VK_NULL_HANDLE};
+
+ // Textures: available, turnin, incomplete
+ VkTexture textures_[3];
+
+ // Quad vertex buffer
+ VkBuffer quadVB_ = VK_NULL_HANDLE;
+ VmaAllocation quadVBAlloc_ = VK_NULL_HANDLE;
void createQuad();
void loadTextures(pipeline::AssetManager* assetManager);
- void createShader();
+ void createDescriptorResources();
};
} // namespace rendering
diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp
index fdd38747..4f163e89 100644
--- a/include/rendering/renderer.hpp
+++ b/include/rendering/renderer.hpp
@@ -5,9 +5,14 @@
#include
#include
#include
+#include
+#include
+#include "rendering/vk_frame_data.hpp"
+#include "rendering/sky_system.hpp"
namespace wowee {
namespace core { class Window; }
+namespace rendering { class VkContext; }
namespace game { class World; class ZoneManager; class GameHandler; }
namespace audio { class MusicManager; class FootstepManager; class ActivitySoundManager; class MountSoundManager; class NpcVoiceManager; class AmbientSoundManager; class UiSoundManager; class CombatSoundManager; class SpellSoundManager; class MovementSoundManager; enum class FootstepSurface : uint8_t; enum class VoiceType; }
namespace pipeline { class AssetManager; }
@@ -28,7 +33,6 @@ class Clouds;
class LensFlare;
class Weather;
class LightingManager;
-class SkySystem;
class SwimEffects;
class MountDust;
class LevelUpEffect;
@@ -37,7 +41,9 @@ class CharacterRenderer;
class WMORenderer;
class M2Renderer;
class Minimap;
+class WorldMap;
class QuestMarkerRenderer;
+class CharacterPreview;
class Shader;
class Renderer {
@@ -101,19 +107,23 @@ public:
TerrainManager* getTerrainManager() const { return terrainManager.get(); }
PerformanceHUD* getPerformanceHUD() { return performanceHUD.get(); }
WaterRenderer* getWaterRenderer() const { return waterRenderer.get(); }
- Skybox* getSkybox() const { return skybox.get(); }
- Celestial* getCelestial() const { return celestial.get(); }
- StarField* getStarField() const { return starField.get(); }
- Clouds* getClouds() const { return clouds.get(); }
- LensFlare* getLensFlare() const { return lensFlare.get(); }
+ Skybox* getSkybox() const { return skySystem ? skySystem->getSkybox() : nullptr; }
+ Celestial* getCelestial() const { return skySystem ? skySystem->getCelestial() : nullptr; }
+ StarField* getStarField() const { return skySystem ? skySystem->getStarField() : nullptr; }
+ Clouds* getClouds() const { return skySystem ? skySystem->getClouds() : nullptr; }
+ LensFlare* getLensFlare() const { return skySystem ? skySystem->getLensFlare() : nullptr; }
Weather* getWeather() const { return weather.get(); }
CharacterRenderer* getCharacterRenderer() const { return characterRenderer.get(); }
WMORenderer* getWMORenderer() const { return wmoRenderer.get(); }
M2Renderer* getM2Renderer() const { return m2Renderer.get(); }
Minimap* getMinimap() const { return minimap.get(); }
+ WorldMap* getWorldMap() const { return worldMap.get(); }
QuestMarkerRenderer* getQuestMarkerRenderer() const { return questMarkerRenderer.get(); }
SkySystem* getSkySystem() const { return skySystem.get(); }
const std::string& getCurrentZoneName() const { return currentZoneName; }
+ VkContext* getVkContext() const { return vkCtx; }
+ VkDescriptorSetLayout getPerFrameSetLayout() const { return perFrameSetLayout; }
+ VkRenderPass getShadowRenderPass() const { return shadowRenderPass; }
// Third-person character follow
void setCharacterFollow(uint32_t instanceId);
@@ -202,6 +212,7 @@ private:
std::unique_ptr wmoRenderer;
std::unique_ptr m2Renderer;
std::unique_ptr minimap;
+ std::unique_ptr worldMap;
std::unique_ptr questMarkerRenderer;
std::unique_ptr musicManager;
std::unique_ptr footstepManager;
@@ -214,45 +225,35 @@ private:
std::unique_ptr spellSoundManager;
std::unique_ptr movementSoundManager;
std::unique_ptr zoneManager;
- std::unique_ptr underwaterOverlayShader;
- uint32_t underwaterOverlayVAO = 0;
- uint32_t underwaterOverlayVBO = 0;
-
- // Post-process FBO pipeline (HDR MSAA β resolve β tonemap)
- uint32_t sceneFBO = 0; // MSAA render target
- uint32_t sceneColorRBO = 0; // GL_RGBA16F multisampled renderbuffer
- uint32_t sceneDepthRBO = 0; // GL_DEPTH_COMPONENT24 multisampled renderbuffer
- uint32_t resolveFBO = 0; // Non-MSAA resolve target
- uint32_t resolveColorTex = 0; // GL_RGBA16F resolved texture (sampled by post-process)
- uint32_t resolveDepthTex = 0; // GL_DEPTH_COMPONENT24 resolved texture (for future SSAO)
- uint32_t screenQuadVAO = 0;
- uint32_t screenQuadVBO = 0;
- std::unique_ptr postProcessShader;
- int fbWidth = 0, fbHeight = 0;
-
- void initPostProcess(int w, int h);
- void resizePostProcess(int w, int h);
- void shutdownPostProcess();
-
- // Shadow mapping
- static constexpr int SHADOW_MAP_SIZE = 2048;
- uint32_t shadowFBO = 0;
- uint32_t shadowDepthTex = 0;
- uint32_t shadowShaderProgram = 0;
+ // Shadow mapping (Vulkan)
+ static constexpr uint32_t SHADOW_MAP_SIZE = 4096;
+ VkImage shadowDepthImage = VK_NULL_HANDLE;
+ VmaAllocation shadowDepthAlloc = VK_NULL_HANDLE;
+ VkImageView shadowDepthView = VK_NULL_HANDLE;
+ VkSampler shadowSampler = VK_NULL_HANDLE;
+ VkRenderPass shadowRenderPass = VK_NULL_HANDLE;
+ VkFramebuffer shadowFramebuffer = VK_NULL_HANDLE;
+ VkImageLayout shadowDepthLayout_ = VK_IMAGE_LAYOUT_UNDEFINED;
glm::mat4 lightSpaceMatrix = glm::mat4(1.0f);
glm::vec3 shadowCenter = glm::vec3(0.0f);
bool shadowCenterInitialized = false;
bool shadowsEnabled = true;
- int shadowPostMoveFrames_ = 0; // transition marker for movement->idle shadow recenter
+
public:
+ // Character preview registration (for off-screen composite pass)
+ void registerPreview(CharacterPreview* preview);
+ void unregisterPreview(CharacterPreview* preview);
+
void setShadowsEnabled(bool enabled) { shadowsEnabled = enabled; }
bool areShadowsEnabled() const { return shadowsEnabled; }
+ void setMsaaSamples(VkSampleCountFlagBits samples);
private:
- void initShadowMap();
+ void applyMsaaChange();
+ VkSampleCountFlagBits pendingMsaaSamples_ = VK_SAMPLE_COUNT_1_BIT;
+ bool msaaChangePending_ = false;
void renderShadowPass();
- uint32_t compileShadowShader();
glm::mat4 computeLightSpaceMatrix();
pipeline::AssetManager* cachedAssetManager = nullptr;
@@ -289,10 +290,13 @@ private:
const glm::vec3* targetPosition = nullptr;
bool inCombat_ = false;
- // Selection circle rendering
- uint32_t selCircleVAO = 0;
- uint32_t selCircleVBO = 0;
- uint32_t selCircleShader = 0;
+ // Selection circle rendering (Vulkan)
+ VkPipeline selCirclePipeline = VK_NULL_HANDLE;
+ VkPipelineLayout selCirclePipelineLayout = VK_NULL_HANDLE;
+ ::VkBuffer selCircleVertBuf = VK_NULL_HANDLE;
+ VmaAllocation selCircleVertAlloc = VK_NULL_HANDLE;
+ ::VkBuffer selCircleIdxBuf = VK_NULL_HANDLE;
+ VmaAllocation selCircleIdxAlloc = VK_NULL_HANDLE;
int selCircleVertCount = 0;
void initSelectionCircle();
void renderSelectionCircle(const glm::mat4& view, const glm::mat4& projection);
@@ -301,6 +305,12 @@ private:
float selCircleRadius = 1.5f;
bool selCircleVisible = false;
+ // Fullscreen color overlay (underwater tint)
+ VkPipeline overlayPipeline = VK_NULL_HANDLE;
+ VkPipelineLayout overlayPipelineLayout = VK_NULL_HANDLE;
+ void initOverlayPipeline();
+ void renderOverlay(const glm::vec4& color);
+
// Footstep event tracking (animation-driven)
uint32_t footstepLastAnimationId = 0;
float footstepLastNormTime = 0.0f;
@@ -360,6 +370,37 @@ private:
bool taxiFlight_ = false;
bool taxiAnimsLogged_ = false;
+ // Vulkan frame state
+ VkContext* vkCtx = nullptr;
+ VkCommandBuffer currentCmd = VK_NULL_HANDLE;
+ uint32_t currentImageIndex = 0;
+
+ // Per-frame UBO + descriptors (set 0)
+ static constexpr uint32_t MAX_FRAMES = 2;
+ VkDescriptorSetLayout perFrameSetLayout = VK_NULL_HANDLE;
+ VkDescriptorPool sceneDescriptorPool = VK_NULL_HANDLE;
+ VkDescriptorSet perFrameDescSets[MAX_FRAMES] = {};
+ VkBuffer perFrameUBOs[MAX_FRAMES] = {};
+ VmaAllocation perFrameUBOAllocs[MAX_FRAMES] = {};
+ void* perFrameUBOMapped[MAX_FRAMES] = {};
+ GPUPerFrameData currentFrameData{};
+ float globalTime = 0.0f;
+
+ // Per-frame reflection UBO (mirrors camera for planar reflections)
+ VkBuffer reflPerFrameUBO = VK_NULL_HANDLE;
+ VmaAllocation reflPerFrameUBOAlloc = VK_NULL_HANDLE;
+ void* reflPerFrameUBOMapped = nullptr;
+ VkDescriptorSet reflPerFrameDescSet = VK_NULL_HANDLE;
+
+ bool createPerFrameResources();
+ void destroyPerFrameResources();
+ void updatePerFrameUBO();
+ void setupWater1xPass();
+ void renderReflectionPass();
+
+ // Active character previews for off-screen rendering
+ std::vector activePreviews_;
+
bool terrainEnabled = true;
bool terrainLoaded = false;
diff --git a/include/rendering/sky_system.hpp b/include/rendering/sky_system.hpp
index 4ead334e..6b3d9c47 100644
--- a/include/rendering/sky_system.hpp
+++ b/include/rendering/sky_system.hpp
@@ -2,11 +2,13 @@
#include
#include
+#include
namespace wowee {
namespace rendering {
class Camera;
+class VkContext;
class Skybox;
class Celestial;
class StarField;
@@ -29,9 +31,10 @@ struct SkyParams {
glm::vec3 skyBand2Color{1.0f, 0.98f, 0.9f};
// Atmospheric effects
- float cloudDensity = 0.0f; // 0-1
- float fogDensity = 0.0f; // 0-1
- float horizonGlow = 0.3f; // 0-1
+ float cloudDensity = 0.0f; // 0-1
+ float fogDensity = 0.0f; // 0-1
+ float horizonGlow = 0.3f; // 0-1
+ float weatherIntensity = 0.0f; // 0-1 (rain/snow intensity, attenuates lens flare)
// Time
float timeOfDay = 12.0f; // 0-24 hours
@@ -59,9 +62,11 @@ public:
~SkySystem();
/**
- * Initialize sky system components
+ * Initialize sky system components.
+ * @param ctx Vulkan context (required for Vulkan renderers)
+ * @param perFrameLayout Descriptor set layout for set 0 (camera UBO)
*/
- bool initialize();
+ bool initialize(VkContext* ctx = nullptr, VkDescriptorSetLayout perFrameLayout = VK_NULL_HANDLE);
void shutdown();
/**
@@ -70,11 +75,14 @@ public:
void update(float deltaTime);
/**
- * Render complete sky
- * @param camera Camera for view/projection
- * @param params Sky parameters from lighting system
+ * Render complete sky.
+ * @param cmd Active Vulkan command buffer
+ * @param perFrameSet Per-frame descriptor set (set 0, camera UBO)
+ * @param camera Camera for legacy sub-renderers (lens flare, etc.)
+ * @param params Sky parameters from lighting system
*/
- void render(const Camera& camera, const SkyParams& params);
+ void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
+ const Camera& camera, const SkyParams& params);
/**
* Enable/disable procedural stars (DEBUG/FALLBACK)
@@ -109,21 +117,21 @@ public:
float getBlueChildPhase() const;
// Component accessors (for direct control if needed)
- Skybox* getSkybox() const { return skybox_.get(); }
+ Skybox* getSkybox() const { return skybox_.get(); }
Celestial* getCelestial() const { return celestial_.get(); }
StarField* getStarField() const { return starField_.get(); }
- Clouds* getClouds() const { return clouds_.get(); }
+ Clouds* getClouds() const { return clouds_.get(); }
LensFlare* getLensFlare() const { return lensFlare_.get(); }
private:
- std::unique_ptr skybox_; // Authoritative sky (gradient now, M2 models later)
- std::unique_ptr celestial_; // Sun + 2 moons
- std::unique_ptr starField_; // Fallback procedural stars
- std::unique_ptr clouds_; // Cloud layer
- std::unique_ptr lensFlare_; // Sun lens flare
+ std::unique_ptr skybox_; // Authoritative sky
+ std::unique_ptr celestial_; // Sun + 2 moons
+ std::unique_ptr starField_; // Fallback procedural stars
+ std::unique_ptr clouds_; // Cloud layer
+ std::unique_ptr lensFlare_; // Sun lens flare
- bool proceduralStarsEnabled_ = false; // Default: OFF (skybox is authoritative)
- bool debugSkyMode_ = false; // Force procedural stars for debugging
+ bool proceduralStarsEnabled_ = false;
+ bool debugSkyMode_ = false;
bool initialized_ = false;
};
diff --git a/include/rendering/skybox.hpp b/include/rendering/skybox.hpp
index 19f19905..51e098cd 100644
--- a/include/rendering/skybox.hpp
+++ b/include/rendering/skybox.hpp
@@ -2,33 +2,41 @@
#include
#include
+#include
+#include
namespace wowee {
namespace rendering {
-class Shader;
-class Camera;
+class VkContext;
+struct SkyParams;
/**
* Skybox renderer
*
- * Renders an atmospheric sky dome with gradient colors.
- * The sky uses a dome/sphere approach for realistic appearance.
+ * Renders an atmospheric sky gradient using a fullscreen triangle.
+ * No vertex buffer: 3 vertices cover the entire screen via gl_VertexIndex.
+ * World-space ray direction is reconstructed from the inverse view+projection.
+ *
+ * Sky colors are driven by DBC data (Light.dbc / LightIntBand.dbc) via SkyParams,
+ * with Rayleigh/Mie atmospheric scattering for realistic appearance.
*/
class Skybox {
public:
Skybox();
~Skybox();
- bool initialize();
+ bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
void shutdown();
+ void recreatePipelines();
/**
- * Render the skybox
- * @param camera Camera for view matrix (position is ignored for skybox)
- * @param timeOfDay Time of day in hours (0-24), affects sky color
+ * Render the skybox using DBC-driven sky colors.
+ * @param cmd Command buffer to record into
+ * @param perFrameSet Per-frame descriptor set (set 0, contains camera UBO)
+ * @param params Sky parameters with DBC colors and sun direction
*/
- void render(const Camera& camera, float timeOfDay = 12.0f);
+ void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const SkyParams& params);
/**
* Enable/disable skybox rendering
@@ -54,24 +62,11 @@ public:
*/
void update(float deltaTime);
- /**
- * Get horizon color for fog (public for fog system)
- */
- glm::vec3 getHorizonColor(float time) const;
-
private:
- void createSkyDome();
- void destroySkyDome();
+ VkContext* vkCtx = nullptr;
- glm::vec3 getSkyColor(float altitude, float time) const;
- glm::vec3 getZenithColor(float time) const;
-
- std::unique_ptr skyShader;
-
- uint32_t vao = 0;
- uint32_t vbo = 0;
- uint32_t ebo = 0;
- int indexCount = 0;
+ VkPipeline pipeline = VK_NULL_HANDLE;
+ VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
float timeOfDay = 12.0f; // Default: noon
float timeSpeed = 1.0f; // 1.0 = 1 hour per real second
diff --git a/include/rendering/starfield.hpp b/include/rendering/starfield.hpp
index 36aa94bf..8ac1e655 100644
--- a/include/rendering/starfield.hpp
+++ b/include/rendering/starfield.hpp
@@ -1,14 +1,14 @@
#pragma once
-#include
#include
#include
+#include
+#include
namespace wowee {
namespace rendering {
-class Shader;
-class Camera;
+class VkContext;
/**
* Star field renderer
@@ -21,17 +21,19 @@ public:
StarField();
~StarField();
- bool initialize();
+ bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
void shutdown();
+ void recreatePipelines();
/**
* Render the star field
- * @param camera Camera for view matrix
- * @param timeOfDay Time of day in hours (0-24)
+ * @param cmd Command buffer to record into
+ * @param perFrameSet Per-frame descriptor set (set 0, contains camera UBO)
+ * @param timeOfDay Time of day in hours (0-24)
* @param cloudDensity Optional cloud density from lighting (0-1, reduces star visibility)
- * @param fogDensity Optional fog density from lighting (reduces star visibility)
+ * @param fogDensity Optional fog density from lighting (reduces star visibility)
*/
- void render(const Camera& camera, float timeOfDay,
+ void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, float timeOfDay,
float cloudDensity = 0.0f, float fogDensity = 0.0f);
/**
@@ -57,8 +59,6 @@ private:
float getStarIntensity(float timeOfDay) const;
- std::unique_ptr starShader;
-
struct Star {
glm::vec3 position;
float brightness; // 0.3 to 1.0
@@ -68,8 +68,13 @@ private:
std::vector stars;
int starCount = 1000;
- uint32_t vao = 0;
- uint32_t vbo = 0;
+ VkContext* vkCtx = nullptr;
+
+ VkPipeline pipeline = VK_NULL_HANDLE;
+ VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
+
+ VkBuffer vertexBuffer = VK_NULL_HANDLE;
+ VmaAllocation vertexAlloc = VK_NULL_HANDLE;
float twinkleTime = 0.0f;
bool renderingEnabled = true;
diff --git a/include/rendering/swim_effects.hpp b/include/rendering/swim_effects.hpp
index 1d5d0569..20d63176 100644
--- a/include/rendering/swim_effects.hpp
+++ b/include/rendering/swim_effects.hpp
@@ -1,8 +1,8 @@
#pragma once
-#include
+#include
+#include
#include
-#include
#include
namespace wowee {
@@ -11,18 +11,22 @@ namespace rendering {
class Camera;
class CameraController;
class WaterRenderer;
-class Shader;
+class M2Renderer;
+class VkContext;
class SwimEffects {
public:
SwimEffects();
~SwimEffects();
- bool initialize();
+ bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
void shutdown();
+ void recreatePipelines();
void update(const Camera& camera, const CameraController& cc,
const WaterRenderer& water, float deltaTime);
- void render(const Camera& camera);
+ void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet);
+ void spawnFootSplash(const glm::vec3& footPos, float waterH);
+ void setM2Renderer(M2Renderer* renderer) { m2Renderer = renderer; }
private:
struct Particle {
@@ -34,25 +38,66 @@ private:
float alpha;
};
+ struct InsectParticle {
+ glm::vec3 position;
+ glm::vec3 orbitCenter; // vegetation position to orbit around
+ float lifetime;
+ float maxLifetime;
+ float size;
+ float alpha;
+ float phase; // random phase offset for erratic motion
+ float orbitRadius;
+ float orbitSpeed;
+ float heightOffset; // height above plant
+ };
+
static constexpr int MAX_RIPPLE_PARTICLES = 200;
static constexpr int MAX_BUBBLE_PARTICLES = 150;
+ static constexpr int MAX_INSECT_PARTICLES = 50;
std::vector ripples;
std::vector bubbles;
+ std::vector