diff --git a/CMakeLists.txt b/CMakeLists.txt index b6157ea2..cbcbe5ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,8 +34,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) @@ -77,13 +82,65 @@ 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) + +# 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 +150,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 +248,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 @@ -287,6 +364,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 +427,24 @@ 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 + ${CMAKE_CURRENT_SOURCE_DIR}/extern/vk-bootstrap/src ${FFMPEG_INCLUDE_DIRS} ) # Link libraries target_link_libraries(wowee PRIVATE SDL2::SDL2 - OpenGL::GL - GLEW::GLEW + Vulkan::Vulkan OpenSSL::SSL OpenSSL::Crypto Threads::Threads @@ -363,6 +452,14 @@ target_link_libraries(wowee PRIVATE ${CMAKE_DL_LIBS} ) +# 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() + target_link_libraries(wowee PRIVATE ${FFMPEG_LIBRARIES}) if (FFMPEG_LIBRARY_DIRS) target_link_directories(wowee PRIVATE ${FFMPEG_LIBRARY_DIRS}) @@ -385,6 +482,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}) @@ -628,7 +730,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/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..1dee3a05 --- /dev/null +++ b/assets/shaders/celestial.frag.glsl @@ -0,0 +1,49 @@ +#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); + float disc = smoothstep(0.42, 0.38, dist); + float glow = exp(-dist * dist * 12.0) * 0.6; + float alpha = max(disc, glow) * push.intensity; + 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.001) discard; + outColor = vec4(color, alpha); +} diff --git a/assets/shaders/celestial.frag.spv b/assets/shaders/celestial.frag.spv new file mode 100644 index 00000000..c7c84413 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..569c099d --- /dev/null +++ b/assets/shaders/character.frag.glsl @@ -0,0 +1,83 @@ +#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; +}; + +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 = 0) out vec4 outColor; + +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)); + float ck = smoothstep(0.12, 0.30, lum); + texColor.a *= ck; + if (texColor.a < 0.01) discard; + } + + vec3 norm = normalize(Normal); + if (!gl_FrontFacing) norm = -norm; + + 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) { + vec4 lsPos = lightSpaceMatrix * vec4(FragPos, 1.0); + vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5; + if (proj.z <= 1.0) { + float bias = max(0.005 * (1.0 - dot(norm, ldir)), 0.001); + shadow = texture(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..39a376c7 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..471025dc --- /dev/null +++ b/assets/shaders/character.vert.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(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 = 0) out vec3 FragPos; +layout(location = 1) out vec3 Normal; +layout(location = 2) 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); + vec3 skinnedNorm = mat3(skinMat) * aNormal; + + vec4 worldPos = push.model * skinnedPos; + FragPos = worldPos.xyz; + Normal = mat3(push.model) * skinnedNorm; + TexCoord = aTexCoord; + + 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..3836081c 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..419e0b7b --- /dev/null +++ b/assets/shaders/clouds.frag.glsl @@ -0,0 +1,63 @@ +#version 450 + +layout(push_constant) uniform Push { + vec4 cloudColor; + float density; + float windOffset; +} push; + +layout(location = 0) in vec3 vWorldDir; + +layout(location = 0) out vec4 outColor; + +float hash(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); +} + +float noise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); +} + +float fbm(vec2 p) { + float val = 0.0; + float amp = 0.5; + for (int i = 0; i < 4; i++) { + val += amp * noise(p); + p *= 2.0; + amp *= 0.5; + } + return val; +} + +void main() { + vec3 dir = normalize(vWorldDir); + float altitude = dir.y; + if (altitude < 0.0) discard; + + vec2 uv = dir.xz / (altitude + 0.001); + uv += push.windOffset; + + float cloud1 = fbm(uv * 0.8); + float cloud2 = fbm(uv * 1.6 + 5.0); + float cloud = cloud1 * 0.7 + cloud2 * 0.3; + cloud = smoothstep(0.35, 0.65, cloud) * push.density; + + float edgeBreak = noise(uv * 4.0); + cloud *= smoothstep(0.2, 0.5, edgeBreak); + + float horizonFade = smoothstep(0.0, 0.15, altitude); + cloud *= horizonFade; + + float edgeSoftness = smoothstep(0.0, 0.3, cloud); + float alpha = cloud * edgeSoftness; + + if (alpha < 0.01) discard; + outColor = vec4(push.cloudColor.rgb, alpha); +} diff --git a/assets/shaders/clouds.frag.spv b/assets/shaders/clouds.frag.spv new file mode 100644 index 00000000..b1118d83 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..be93ad29 --- /dev/null +++ b/assets/shaders/m2.frag.glsl @@ -0,0 +1,86 @@ +#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 = 0) out vec4 outColor; + +void main() { + vec4 texColor = hasTexture != 0 ? texture(uTexture, TexCoord) : vec4(1.0); + + 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 < colorKeyThreshold) discard; + } + if (blendMode == 1 && texColor.a < 0.004) discard; + + vec3 norm = normalize(Normal); + if (!gl_FrontFacing) norm = -norm; + + vec3 ldir = normalize(-lightDir.xyz); + float diff = max(dot(norm, ldir), 0.0); + + vec3 result; + if (unlit != 0) { + result = texColor.rgb; + } else { + 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) { + vec4 lsPos = lightSpaceMatrix * vec4(FragPos, 1.0); + vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5; + if (proj.z <= 1.0) { + float bias = max(0.005 * (1.0 - dot(norm, ldir)), 0.001); + shadow = texture(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); + + if (interiorDarken > 0.0) { + result *= mix(1.0, 0.5, interiorDarken); + } + } + + 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 * fadeAlpha); +} diff --git a/assets/shaders/m2.frag.spv b/assets/shaders/m2.frag.spv new file mode 100644 index 00000000..9b67de76 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..46f3fea0 --- /dev/null +++ b/assets/shaders/m2.vert.glsl @@ -0,0 +1,59 @@ +#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; +} 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; + +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; + } + + vec4 worldPos = push.model * pos; + FragPos = worldPos.xyz; + Normal = mat3(push.model) * norm.xyz; + + TexCoord = (push.texCoordSet == 1 ? aTexCoord2 : aTexCoord) + push.uvOffset; + + 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..6b0b1dde 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..e2ad702b --- /dev/null +++ b/assets/shaders/minimap_display.frag.glsl @@ -0,0 +1,67 @@ +#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; +} 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 = mapColor; +} diff --git a/assets/shaders/minimap_display.frag.spv b/assets/shaders/minimap_display.frag.spv new file mode 100644 index 00000000..651e04a8 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/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..7d5554a1 --- /dev/null +++ b/assets/shaders/shadow.frag.glsl @@ -0,0 +1,37 @@ +#version 450 + +layout(set = 1, binding = 0) uniform sampler2D uTexture; + +layout(set = 1, 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) { + vec2 uv = TexCoord; + if (foliageSway != 0) { + float sway = sin(windTime + WorldPos.x * 0.5) * 0.02 * foliageMotionDamp; + uv += vec2(sway, sway * 0.5); + } + vec4 texColor = textureLod(uTexture, uv, 0.0); + if (alphaTest != 0 && texColor.a < 0.5) discard; + + if (foliageSway != 0) { + vec2 uv2 = TexCoord + vec2( + sin(windTime * 1.3 + WorldPos.z * 0.7) * 0.015 * foliageMotionDamp, + sin(windTime * 0.9 + WorldPos.x * 0.6) * 0.01 * foliageMotionDamp + ); + vec4 texColor2 = textureLod(uTexture, uv2, 0.0); + float blended = (texColor.a + texColor2.a) * 0.5; + if (alphaTest != 0 && blended < 0.5) discard; + } + } +} diff --git a/assets/shaders/shadow.frag.spv b/assets/shaders/shadow.frag.spv new file mode 100644 index 00000000..a57b0401 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..5ea3f766 --- /dev/null +++ b/assets/shaders/shadow.vert.glsl @@ -0,0 +1,45 @@ +#version 450 + +layout(push_constant) uniform Push { + mat4 lightSpaceMatrix; + mat4 model; +} push; + +layout(set = 2, binding = 0) readonly buffer BoneSSBO { + mat4 bones[]; +}; + +layout(set = 1, 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); + + if (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; + } + + 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..f972e12e 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..179dd9c3 --- /dev/null +++ b/assets/shaders/skybox.frag.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 { + vec4 horizonColor; + vec4 zenithColor; + float timeOfDay; +} push; + +layout(location = 0) in vec3 WorldPos; +layout(location = 1) in float Altitude; + +layout(location = 0) out vec4 outColor; + +void main() { + float t = clamp(Altitude, 0.0, 1.0); + t = pow(t, 1.5); + vec3 sky = mix(push.horizonColor.rgb, push.zenithColor.rgb, t); + float scatter = max(0.0, 1.0 - t * 2.0) * 0.15; + sky += vec3(scatter * 0.8, scatter * 0.4, scatter * 0.1); + 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..596f72d6 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..95462764 --- /dev/null +++ b/assets/shaders/skybox.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; // x=fogStart, y=fogEnd, z=time + vec4 shadowParams; // x=enabled, y=strength +}; + +layout(location = 0) in vec3 aPos; + +layout(location = 0) out vec3 WorldPos; +layout(location = 1) out float Altitude; + +void main() { + WorldPos = aPos; + Altitude = aPos.y; + mat4 rotView = mat4(mat3(view)); // strip translation + vec4 pos = projection * rotView * vec4(aPos, 1.0); + gl_Position = pos.xyww; // force far plane +} diff --git a/assets/shaders/skybox.vert.spv b/assets/shaders/skybox.vert.spv new file mode 100644 index 00000000..8203b21f 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_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..07d53093 --- /dev/null +++ b/assets/shaders/terrain.frag.glsl @@ -0,0 +1,96 @@ +#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; + +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); + float a1 = hasLayer1 != 0 ? sampleAlpha(uLayer1Alpha, LayerUV) : 0.0; + float a2 = hasLayer2 != 0 ? sampleAlpha(uLayer2Alpha, LayerUV) : 0.0; + float a3 = hasLayer3 != 0 ? sampleAlpha(uLayer3Alpha, LayerUV) : 0.0; + + float w0 = 1.0, w1 = a1, w2 = a2, w3 = a3; + float sum = w0 + w1 + w2 + w3; + if (sum > 0.0) { w0 /= sum; w1 /= sum; w2 /= sum; w3 /= sum; } + + vec4 finalColor = baseColor * w0; + if (hasLayer1 != 0) finalColor += texture(uLayer1Texture, TexCoord) * w1; + if (hasLayer2 != 0) finalColor += texture(uLayer2Texture, TexCoord) * w2; + if (hasLayer3 != 0) finalColor += texture(uLayer3Texture, TexCoord) * w3; + + vec3 norm = normalize(Normal); + 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) { + vec4 lsPos = lightSpaceMatrix * vec4(FragPos, 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.002; + shadow = texture(uShadowMap, vec3(proj.xy, proj.z - bias)); + shadow = mix(1.0, shadow, shadowParams.y); + } + } + + vec3 result = ambient + shadow * diffuse; + + float distance = length(viewPos.xyz - FragPos); + float fogFactor = clamp((fogParams.y - distance) / (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..61630749 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..5bea94c2 --- /dev/null +++ b/assets/shaders/water.frag.glsl @@ -0,0 +1,66 @@ +#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; +} push; + +layout(set = 1, binding = 0) uniform WaterMaterial { + vec4 waterColor; + float waterAlpha; + float shimmerStrength; + float alphaScale; +}; + +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 = 0) out vec4 outColor; + +void main() { + float time = fogParams.z; + vec3 norm = normalize(Normal); + vec3 viewDir = normalize(viewPos.xyz - FragPos); + vec3 ldir = normalize(-lightDir.xyz); + + float diff = max(dot(norm, ldir), 0.0); + vec3 halfDir = normalize(ldir + viewDir); + float spec = pow(max(dot(norm, halfDir), 0.0), 128.0); + float sparkle = sin(FragPos.x * 20.0 + time * 3.0) * sin(FragPos.z * 20.0 + time * 2.5); + sparkle = max(0.0, sparkle) * shimmerStrength; + + vec3 color = waterColor.rgb * (ambientColor.rgb + diff * lightColor.rgb) + + spec * lightColor.rgb * 0.5 + + sparkle * lightColor.rgb * 0.3; + + float crest = smoothstep(0.3, 1.0, WaveOffset) * 0.15; + color += vec3(crest); + + float fresnel = pow(1.0 - max(dot(norm, viewDir), 0.0), 3.0); + float alpha = mix(waterAlpha * 0.6, waterAlpha, fresnel) * alphaScale; + + float dist = length(viewPos.xyz - FragPos); + alpha *= smoothstep(800.0, 200.0, dist); + + 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..496a10a9 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..f066adef --- /dev/null +++ b/assets/shaders/water.vert.glsl @@ -0,0 +1,61 @@ +#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; +} 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; + +float hashGrid(vec2 p) { + return fract(sin(dot(floor(p), vec2(127.1, 311.7))) * 43758.5453); +} + +void main() { + float time = fogParams.z; + vec4 worldPos = push.model * vec4(aPos, 1.0); + float px = worldPos.x; + float py = worldPos.z; + float dist = length(worldPos.xyz - viewPos.xyz); + float blend = smoothstep(150.0, 400.0, dist); + + float seamless = sin(px * push.waveFreq + time * push.waveSpeed) * 0.6 + + sin(py * push.waveFreq * 0.7 + time * push.waveSpeed * 1.3) * 0.3 + + sin((px + py) * push.waveFreq * 0.5 + time * push.waveSpeed * 0.7) * 0.1; + + float gridWave = sin(px * push.waveFreq + time * push.waveSpeed + hashGrid(vec2(px, py) * 0.01) * 6.28) * 0.5 + + sin(py * push.waveFreq * 0.8 + time * push.waveSpeed * 1.1 + hashGrid(vec2(py, px) * 0.01) * 6.28) * 0.5; + + float wave = mix(seamless, gridWave, blend); + worldPos.y += wave * push.waveAmp; + WaveOffset = wave; + + float dx = cos(px * push.waveFreq + time * push.waveSpeed) * push.waveFreq * push.waveAmp; + float dz = cos(py * push.waveFreq * 0.7 + time * push.waveSpeed * 1.3) * push.waveFreq * 0.7 * push.waveAmp; + Normal = normalize(vec3(-dx, 1.0, -dz)); + + FragPos = worldPos.xyz; + TexCoord = aTexCoord; + gl_Position = projection * view * worldPos; +} diff --git a/assets/shaders/water.vert.spv b/assets/shaders/water.vert.spv new file mode 100644 index 00000000..4325d2c5 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..faf44858 --- /dev/null +++ b/assets/shaders/wmo.frag.glsl @@ -0,0 +1,79 @@ +#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; +}; + +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 = 0) out vec4 outColor; + +void main() { + vec4 texColor = hasTexture != 0 ? texture(uTexture, TexCoord) : vec4(1.0); + if (alphaTest != 0 && texColor.a < 0.5) discard; + + vec3 norm = normalize(Normal); + if (!gl_FrontFacing) norm = -norm; + + vec3 result; + + if (unlit != 0) { + result = texColor.rgb; + } else if (isInterior != 0) { + vec3 mocv = max(VertColor.rgb, vec3(0.5)); + result = texColor.rgb * mocv; + } 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) { + vec4 lsPos = lightSpaceMatrix * vec4(FragPos, 1.0); + vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5; + if (proj.z <= 1.0) { + float bias = max(0.005 * (1.0 - dot(norm, ldir)), 0.001); + shadow = texture(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); + + 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); + + outColor = vec4(result, texColor.a); +} diff --git a/assets/shaders/wmo.frag.spv b/assets/shaders/wmo.frag.spv new file mode 100644 index 00000000..cb133eed 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..12e9ab59 --- /dev/null +++ b/assets/shaders/wmo.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 aPos; +layout(location = 1) in vec3 aNormal; +layout(location = 2) in vec2 aTexCoord; +layout(location = 3) in vec4 aColor; + +layout(location = 0) out vec3 FragPos; +layout(location = 1) out vec3 Normal; +layout(location = 2) out vec2 TexCoord; +layout(location = 3) out vec4 VertColor; + +void main() { + vec4 worldPos = push.model * vec4(aPos, 1.0); + FragPos = worldPos.xyz; + Normal = mat3(push.model) * aNormal; + TexCoord = aTexCoord; + VertColor = aColor; + 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..b355eae2 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/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/rendering/celestial.hpp b/include/rendering/celestial.hpp index 27e78ffb..6fbd871d 100644 --- a/include/rendering/celestial.hpp +++ b/include/rendering/celestial.hpp @@ -1,142 +1,132 @@ #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(); /** - * 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..40924ed9 100644 --- a/include/rendering/character_preview.hpp +++ b/include/rendering/character_preview.hpp @@ -1,7 +1,7 @@ #pragma once #include "game/character.hpp" -#include +#include #include #include #include @@ -13,6 +13,8 @@ namespace rendering { class CharacterRenderer; class Camera; +class VkContext; +class VkTexture; class CharacterPreview { public: @@ -34,7 +36,8 @@ public: void render(); void rotate(float yawDelta); - GLuint getTextureId() const { return colorTexture_; } + // TODO: Vulkan offscreen render target for preview + VkTexture* getTextureId() const { return nullptr; } int getWidth() const { return fboWidth_; } int getHeight() const { return fboHeight_; } @@ -51,9 +54,8 @@ private: std::unique_ptr charRenderer_; std::unique_ptr camera_; - GLuint fbo_ = 0; - GLuint colorTexture_ = 0; - GLuint depthRenderbuffer_ = 0; + // TODO: Vulkan offscreen render target + // VkRenderTarget* renderTarget_ = nullptr; static constexpr int fboWidth_ = 400; static constexpr int fboHeight_ = 500; diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index 30001846..abd0d42a 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -1,7 +1,8 @@ #pragma once #include "pipeline/m2_loader.hpp" -#include +#include +#include #include #include #include @@ -14,9 +15,9 @@ 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 +34,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 +42,7 @@ public: CharacterRenderer(); ~CharacterRenderer(); - bool initialize(); + bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* am); void shutdown(); void setAssetManager(pipeline::AssetManager* am) { assetManager = am; } @@ -56,8 +57,8 @@ 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 renderShadow(VkCommandBuffer cmd, VkDescriptorSet perFrameSet); void setInstancePosition(uint32_t instanceId, const glm::vec3& position); void setInstanceRotation(uint32_t instanceId, const glm::vec3& rotation); @@ -65,8 +66,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 +89,32 @@ 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; - } - - 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 +139,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 +163,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 +177,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,83 +190,76 @@ 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; 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 materialDescPool_ = VK_NULL_HANDLE; + VkDescriptorPool boneDescPool_ = VK_NULL_HANDLE; // Texture cache struct TextureCacheEntry { - GLuint id = 0; + std::unique_ptr texture; 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_map textureHasAlphaByPtr_; + std::unordered_map textureColorKeyBlackByPtr_; + std::unordered_map compositeCache_; // key → texture for reuse std::unordered_set failedTextureCache_; // negative cache for missing textures 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; + std::unique_ptr whiteTexture_; + std::unique_ptr transparentTexture_; 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) + // Maximum bones supported static constexpr int MAX_BONES = 240; }; diff --git a/include/rendering/charge_effect.hpp b/include/rendering/charge_effect.hpp index 9319a601..eec5d514 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,7 +22,7 @@ public: ChargeEffect(); ~ChargeEffect(); - bool initialize(); + bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); /// Try to load M2 spell models (Charge_Caster.m2, etc.) @@ -41,7 +41,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 +59,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 +84,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..d36a8058 100644 --- a/include/rendering/clouds.hpp +++ b/include/rendering/clouds.hpp @@ -1,25 +1,27 @@ #pragma once -#include #include -#include +#include +#include #include namespace wowee { namespace rendering { -class Camera; -class Shader; +class VkContext; /** - * @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. + * Two noise layers at different frequencies produce realistic cloud shapes. + * + * Pipeline layout: + * set 0 = perFrameLayout (camera UBO — view, projection, etc.) + * push = CloudPush (vec4 cloudColor + float density + float windOffset = 24 bytes) + * + * The vertex shader reads view/projection from set 0 directly; no per-object + * model matrix is needed (clouds are locked to the sky dome). */ class Clouds { public: @@ -27,68 +29,79 @@ public: ~Clouds(); /** - * @brief Initialize cloud system (generate mesh and shaders) - * @return true if initialization succeeded + * Initialize the cloud system. + * @param ctx Vulkan context + * @param perFrameLayout Descriptor set layout for set 0 (camera UBO) */ - bool initialize(); + bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); + void shutdown(); /** - * @brief Render clouds - * @param camera The camera to render from - * @param timeOfDay Current time (0-24 hours) + * Render clouds. + * @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) */ - void render(const Camera& camera, float timeOfDay); + void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, float timeOfDay); /** - * @brief Update cloud animation - * @param deltaTime Time since last frame + * Update cloud animation (wind drift). + * @param deltaTime Seconds since last frame */ 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 --- + /** Cloud coverage, 0 = clear, 1 = overcast. */ 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; // 16 bytes (xyz = colour, w unused) + float density; // 4 bytes + float windOffset; // 4 bytes + // total = 24 bytes + }; + static_assert(sizeof(CloudPush) == 24, "CloudPush size mismatch"); + void generateMesh(); - void cleanup(); + void createBuffers(); + void destroyBuffers(); + glm::vec3 getCloudColor(float timeOfDay) const; - // 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.5f; + float windSpeed_ = 1.0f; + float windOffset_ = 0.0f; // Accumulated wind movement // 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; // Slightly smaller than skybox }; } // namespace rendering diff --git a/include/rendering/lens_flare.hpp b/include/rendering/lens_flare.hpp index 4b390bcb..31473529 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,25 @@ 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(); /** * @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) */ - void render(const Camera& camera, const glm::vec3& sunPosition, float timeOfDay); + void render(VkCommandBuffer cmd, const Camera& camera, const glm::vec3& sunPosition, float timeOfDay); /** * @brief Enable or disable lens flare rendering @@ -60,15 +68,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/lightning.hpp b/include/rendering/lightning.hpp index f4e94cfb..354c1d1c 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,11 @@ public: Lightning(); ~Lightning(); - bool initialize(); + bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); 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 +68,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 +82,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..16351936 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,19 @@ 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; uint32_t indexStart = 0; // offset in indices (not bytes) uint32_t indexCount = 0; bool hasAlpha = false; @@ -47,9 +52,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; @@ -109,14 +115,14 @@ struct M2ModelGPU { // 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 +170,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 +192,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 +223,57 @@ 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); /** - * 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 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(); @@ -335,21 +288,12 @@ 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; } @@ -359,7 +303,51 @@ private: 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; + + // 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; @@ -367,37 +355,22 @@ private: uint32_t nextInstanceId = 1; uint32_t lastDrawCallCount = 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::unique_ptr whiteTexture_; + std::unique_ptr glowTexture_; // Optional query-space culling for collision/raycast hot paths. bool collisionFocusEnabled = false; @@ -458,17 +431,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 +453,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..dc4a4c47 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,28 @@ 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 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); void setEnabled(bool enabled) { this->enabled = enabled; } bool isEnabled() const { return enabled; } @@ -45,17 +52,15 @@ public: void zoomOut() { viewRadius = std::min(800.0f, viewRadius + 50.0f); } // 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,28 +68,36 @@ 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; diff --git a/include/rendering/mount_dust.hpp b/include/rendering/mount_dust.hpp index fa729fa9..553cfb56 100644 --- a/include/rendering/mount_dust.hpp +++ b/include/rendering/mount_dust.hpp @@ -1,29 +1,29 @@ #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(); // 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 +38,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..8b5ff69e 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,7 +25,7 @@ public: QuestMarkerRenderer(); ~QuestMarkerRenderer(); - bool initialize(pipeline::AssetManager* assetManager); + bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* assetManager); void shutdown(); /** @@ -44,8 +49,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 +63,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..7b47f4a0 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,6 +41,7 @@ class CharacterRenderer; class WMORenderer; class M2Renderer; class Minimap; +class WorldMap; class QuestMarkerRenderer; class Shader; @@ -101,19 +106,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 +211,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,31 +224,14 @@ 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 = 2048; + 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; glm::mat4 lightSpaceMatrix = glm::mat4(1.0f); glm::vec3 shadowCenter = glm::vec3(0.0f); bool shadowCenterInitialized = false; @@ -250,9 +243,7 @@ public: bool areShadowsEnabled() const { return shadowsEnabled; } private: - void initShadowMap(); void renderShadowPass(); - uint32_t compileShadowShader(); glm::mat4 computeLightSpaceMatrix(); pipeline::AssetManager* cachedAssetManager = nullptr; @@ -289,10 +280,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); @@ -360,6 +354,26 @@ 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; + + bool createPerFrameResources(); + void destroyPerFrameResources(); + void updatePerFrameUBO(); + bool terrainEnabled = true; bool terrainLoaded = false; diff --git a/include/rendering/sky_system.hpp b/include/rendering/sky_system.hpp index 4ead334e..740e3870 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; @@ -59,9 +61,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 +74,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 +116,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..42ed67db 100644 --- a/include/rendering/skybox.hpp +++ b/include/rendering/skybox.hpp @@ -2,12 +2,13 @@ #include #include +#include +#include namespace wowee { namespace rendering { -class Shader; -class Camera; +class VkContext; /** * Skybox renderer @@ -20,15 +21,16 @@ public: Skybox(); ~Skybox(); - bool initialize(); + bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); /** * Render the skybox - * @param camera Camera for view matrix (position is ignored for skybox) + * @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), affects sky color */ - void render(const Camera& camera, float timeOfDay = 12.0f); + void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, float timeOfDay = 12.0f); /** * Enable/disable skybox rendering @@ -66,11 +68,16 @@ private: glm::vec3 getSkyColor(float altitude, float time) const; glm::vec3 getZenithColor(float time) const; - std::unique_ptr skyShader; + 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; - uint32_t vao = 0; - uint32_t vbo = 0; - uint32_t ebo = 0; int indexCount = 0; float timeOfDay = 12.0f; // Default: noon diff --git a/include/rendering/starfield.hpp b/include/rendering/starfield.hpp index 36aa94bf..8a8793a9 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,18 @@ public: StarField(); ~StarField(); - bool initialize(); + bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); /** * 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 +58,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 +67,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..75c7c439 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,18 @@ namespace rendering { class Camera; class CameraController; class WaterRenderer; -class Shader; +class VkContext; class SwimEffects { public: SwimEffects(); ~SwimEffects(); - bool initialize(); + bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); void update(const Camera& camera, const CameraController& cc, const WaterRenderer& water, float deltaTime); - void render(const Camera& camera); + void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet); private: struct Particle { @@ -40,10 +40,24 @@ private: std::vector ripples; std::vector bubbles; - GLuint rippleVAO = 0, rippleVBO = 0; - GLuint bubbleVAO = 0, bubbleVBO = 0; - std::unique_ptr rippleShader; - std::unique_ptr bubbleShader; + // Vulkan objects + VkContext* vkCtx = nullptr; + + // Ripple pipeline + dynamic buffer + VkPipeline ripplePipeline = VK_NULL_HANDLE; + VkPipelineLayout ripplePipelineLayout = VK_NULL_HANDLE; + ::VkBuffer rippleDynamicVB = VK_NULL_HANDLE; + VmaAllocation rippleDynamicVBAlloc = VK_NULL_HANDLE; + VmaAllocationInfo rippleDynamicVBAllocInfo{}; + VkDeviceSize rippleDynamicVBSize = 0; + + // Bubble pipeline + dynamic buffer + VkPipeline bubblePipeline = VK_NULL_HANDLE; + VkPipelineLayout bubblePipelineLayout = VK_NULL_HANDLE; + ::VkBuffer bubbleDynamicVB = VK_NULL_HANDLE; + VmaAllocation bubbleDynamicVBAlloc = VK_NULL_HANDLE; + VmaAllocationInfo bubbleDynamicVBAllocInfo{}; + VkDeviceSize bubbleDynamicVBSize = 0; std::vector rippleVertexData; std::vector bubbleVertexData; diff --git a/include/rendering/terrain_renderer.hpp b/include/rendering/terrain_renderer.hpp index 3fb285b1..34f2b666 100644 --- a/include/rendering/terrain_renderer.hpp +++ b/include/rendering/terrain_renderer.hpp @@ -2,10 +2,9 @@ #include "pipeline/terrain_mesh.hpp" #include "pipeline/blp_loader.hpp" -#include "rendering/shader.hpp" -#include "rendering/texture.hpp" #include "rendering/camera.hpp" -#include +#include +#include #include #include #include @@ -18,21 +17,35 @@ namespace pipeline { class AssetManager; } namespace rendering { +class VkContext; +class VkTexture; class Frustum; /** - * GPU-side terrain chunk data + * GPU-side terrain chunk data (Vulkan) */ struct TerrainChunkGPU { - GLuint vao = 0; // Vertex array object - GLuint vbo = 0; // Vertex buffer - GLuint ibo = 0; // Index buffer - uint32_t indexCount = 0; // Number of indices to draw + ::VkBuffer vertexBuffer = VK_NULL_HANDLE; + VmaAllocation vertexAlloc = VK_NULL_HANDLE; + ::VkBuffer indexBuffer = VK_NULL_HANDLE; + VmaAllocation indexAlloc = VK_NULL_HANDLE; + uint32_t indexCount = 0; - // Texture IDs for this chunk - GLuint baseTexture = 0; - std::vector layerTextures; - std::vector alphaTextures; + // Material descriptor set (set 1: 7 samplers + params UBO) + VkDescriptorSet materialSet = VK_NULL_HANDLE; + + // Per-chunk params UBO (hasLayer1/2/3) + ::VkBuffer paramsUBO = VK_NULL_HANDLE; + VmaAllocation paramsAlloc = VK_NULL_HANDLE; + + // Texture handles (owned by cache, NOT destroyed per-chunk) + VkTexture* baseTexture = nullptr; + VkTexture* layerTextures[3] = {nullptr, nullptr, nullptr}; + VkTexture* alphaTextures[3] = {nullptr, nullptr, nullptr}; + int layerCount = 0; + + // Per-chunk alpha textures (owned by this chunk, destroyed on removal) + std::vector> ownedAlphaTextures; // World position for culling float worldX = 0.0f; @@ -46,13 +59,11 @@ struct TerrainChunkGPU { float boundingSphereRadius = 0.0f; glm::vec3 boundingSphereCenter = glm::vec3(0.0f); - bool isValid() const { return vao != 0 && vbo != 0 && ibo != 0; } + bool isValid() const { return vertexBuffer != VK_NULL_HANDLE && indexBuffer != VK_NULL_HANDLE; } }; /** - * Terrain renderer - * - * Handles uploading terrain meshes to GPU and rendering them + * Terrain renderer (Vulkan) */ class TerrainRenderer { public: @@ -61,150 +72,92 @@ public: /** * Initialize terrain renderer + * @param ctx Vulkan context + * @param perFrameLayout Descriptor set layout for set 0 (per-frame UBO) * @param assetManager Asset manager for loading textures */ - bool initialize(pipeline::AssetManager* assetManager); + bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, + pipeline::AssetManager* assetManager); - /** - * Shutdown and cleanup GPU resources - */ void shutdown(); - /** - * Load terrain mesh and upload to GPU - * @param mesh Terrain mesh to load - * @param texturePaths Texture file paths from ADT - * @param tileX Tile X coordinate for tracking ownership (-1 = untracked) - * @param tileY Tile Y coordinate for tracking ownership (-1 = untracked) - */ bool loadTerrain(const pipeline::TerrainMesh& mesh, const std::vector& texturePaths, int tileX = -1, int tileY = -1); - /** - * Remove all chunks belonging to a specific tile - * @param tileX Tile X coordinate - * @param tileY Tile Y coordinate - */ void removeTile(int tileX, int tileY); - /** - * Upload pre-loaded BLP textures to the GL texture cache. - * Called before loadTerrain() so texture loading avoids file I/O. - */ void uploadPreloadedTextures(const std::unordered_map& textures); /** - * Render loaded terrain - * @param camera Camera for view/projection matrices + * Render terrain + * @param cmd Command buffer to record into + * @param perFrameSet Per-frame descriptor set (set 0) + * @param camera Camera for frustum culling */ - void render(const Camera& camera); + void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera); /** - * Clear all loaded terrain + * Render terrain into shadow depth map (Phase 6 stub) */ + void renderShadow(VkCommandBuffer cmd, const glm::vec3& shadowCenter, float halfExtent); + void clear(); - /** - * Set lighting parameters - */ - void setLighting(const float lightDir[3], const float lightColor[3], - const float ambientColor[3]); - - /** - * Set fog parameters - */ - void setFog(const float fogColor[3], float fogStart, float fogEnd); - - /** - * Enable/disable wireframe rendering - */ void setWireframe(bool enabled) { wireframe = enabled; } - - /** - * Enable/disable frustum culling - */ void setFrustumCulling(bool enabled) { frustumCullingEnabled = enabled; } - - /** - * Enable/disable distance fog - */ void setFogEnabled(bool enabled) { fogEnabled = enabled; } bool isFogEnabled() const { return fogEnabled; } - /** - * Render terrain geometry into shadow depth map - */ - void renderShadow(GLuint shaderProgram, const glm::vec3& shadowCenter, float halfExtent); + // Shadow mapping stubs (Phase 6) + void setShadowMap(VkDescriptorImageInfo /*depthInfo*/, const glm::mat4& /*lightSpaceMat*/) {} + void clearShadowMap() {} - /** - * Set shadow map for receiving shadows - */ - void setShadowMap(GLuint depthTex, const glm::mat4& lightSpaceMat) { - shadowDepthTex = depthTex; lightSpaceMatrix = lightSpaceMat; shadowEnabled = true; - } - void clearShadowMap() { shadowEnabled = false; } - - /** - * Get statistics - */ int getChunkCount() const { return static_cast(chunks.size()); } int getRenderedChunkCount() const { return renderedChunks; } int getCulledChunkCount() const { return culledChunks; } int getTriangleCount() const; private: - /** - * Upload single chunk to GPU - */ TerrainChunkGPU uploadChunk(const pipeline::ChunkMesh& chunk); - - /** - * Load texture from asset manager - */ - GLuint loadTexture(const std::string& path); - - /** - * Create alpha texture from raw alpha data - */ - GLuint createAlphaTexture(const std::vector& alphaData); - - /** - * Check if chunk is in view frustum - */ + VkTexture* loadTexture(const std::string& path); + VkTexture* createAlphaTexture(const std::vector& alphaData); bool isChunkVisible(const TerrainChunkGPU& chunk, const Frustum& frustum); - - /** - * Calculate bounding sphere for chunk - */ void calculateBoundingSphere(TerrainChunkGPU& chunk, const pipeline::ChunkMesh& meshChunk); + VkDescriptorSet allocateMaterialSet(); + void writeMaterialDescriptors(VkDescriptorSet set, const TerrainChunkGPU& chunk); + void destroyChunkGPU(TerrainChunkGPU& chunk); + VkContext* vkCtx = nullptr; pipeline::AssetManager* assetManager = nullptr; - std::unique_ptr shader; + + // Pipeline + VkPipeline pipeline = VK_NULL_HANDLE; + VkPipeline wireframePipeline = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkDescriptorSetLayout materialSetLayout = VK_NULL_HANDLE; + + // Descriptor pool for material sets + VkDescriptorPool materialDescPool = VK_NULL_HANDLE; + static constexpr uint32_t MAX_MATERIAL_SETS = 8192; // Loaded terrain chunks std::vector chunks; - // Texture cache (path -> GL texture ID) + // Texture cache (path -> VkTexture) struct TextureCacheEntry { - GLuint id = 0; + std::unique_ptr texture; size_t approxBytes = 0; uint64_t lastUse = 0; }; std::unordered_map textureCache; size_t textureCacheBytes_ = 0; uint64_t textureCacheCounter_ = 0; - size_t textureCacheBudgetBytes_ = 4096ull * 1024 * 1024; // Default, overridden at init + size_t textureCacheBudgetBytes_ = 4096ull * 1024 * 1024; - // Lighting parameters - float lightDir[3] = {-0.5f, -1.0f, -0.5f}; - float lightColor[3] = {1.0f, 1.0f, 0.9f}; - float ambientColor[3] = {0.3f, 0.3f, 0.35f}; - - // Fog parameters - float fogColor[3] = {0.5f, 0.6f, 0.7f}; - float fogStart = 400.0f; - float fogEnd = 800.0f; + // Fallback textures + std::unique_ptr whiteTexture; + std::unique_ptr opaqueAlphaTexture; // Rendering state bool wireframe = false; @@ -212,16 +165,6 @@ private: bool fogEnabled = true; int renderedChunks = 0; int culledChunks = 0; - - // Default white texture (fallback) - GLuint whiteTexture = 0; - // Opaque alpha fallback for missing/invalid layer alpha maps - GLuint opaqueAlphaTexture = 0; - - // Shadow mapping (receiving) - GLuint shadowDepthTex = 0; - glm::mat4 lightSpaceMatrix = glm::mat4(1.0f); - bool shadowEnabled = false; }; } // namespace rendering diff --git a/include/rendering/vk_buffer.hpp b/include/rendering/vk_buffer.hpp new file mode 100644 index 00000000..6cb0ba54 --- /dev/null +++ b/include/rendering/vk_buffer.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "rendering/vk_utils.hpp" +#include +#include +#include +#include + +namespace wowee { +namespace rendering { + +class VkContext; + +// RAII wrapper for a Vulkan buffer with VMA allocation. +// Supports vertex, index, uniform, and storage buffer usage. +class VkBuffer { +public: + VkBuffer() = default; + ~VkBuffer(); + + VkBuffer(const VkBuffer&) = delete; + VkBuffer& operator=(const VkBuffer&) = delete; + VkBuffer(VkBuffer&& other) noexcept; + VkBuffer& operator=(VkBuffer&& other) noexcept; + + // Create a GPU-local buffer and upload data via staging + bool uploadToGPU(VkContext& ctx, const void* data, VkDeviceSize size, + VkBufferUsageFlags usage); + + // Create a host-visible buffer (for uniform/dynamic data updated each frame) + bool createMapped(VmaAllocator allocator, VkDeviceSize size, + VkBufferUsageFlags usage); + + // Update mapped buffer contents (only valid for mapped buffers) + void updateMapped(const void* data, VkDeviceSize size, VkDeviceSize offset = 0); + + void destroy(); + + ::VkBuffer getBuffer() const { return buf_.buffer; } + VkDeviceSize getSize() const { return size_; } + void* getMappedData() const { return buf_.info.pMappedData; } + bool isValid() const { return buf_.buffer != VK_NULL_HANDLE; } + + // Descriptor info for uniform/storage buffer binding + VkDescriptorBufferInfo descriptorInfo(VkDeviceSize offset = 0, + VkDeviceSize range = VK_WHOLE_SIZE) const; + +private: + AllocatedBuffer buf_{}; + VmaAllocator allocator_ = VK_NULL_HANDLE; + VkDeviceSize size_ = 0; +}; + +} // namespace rendering +} // namespace wowee diff --git a/include/rendering/vk_context.hpp b/include/rendering/vk_context.hpp new file mode 100644 index 00000000..b1ca4b58 --- /dev/null +++ b/include/rendering/vk_context.hpp @@ -0,0 +1,141 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace rendering { + +static constexpr uint32_t MAX_FRAMES_IN_FLIGHT = 2; + +struct FrameData { + VkCommandPool commandPool = VK_NULL_HANDLE; + VkCommandBuffer commandBuffer = VK_NULL_HANDLE; + VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE; + VkSemaphore renderFinishedSemaphore = VK_NULL_HANDLE; + VkFence inFlightFence = VK_NULL_HANDLE; +}; + +class VkContext { +public: + VkContext() = default; + ~VkContext(); + + VkContext(const VkContext&) = delete; + VkContext& operator=(const VkContext&) = delete; + + bool initialize(SDL_Window* window); + void shutdown(); + + // Swapchain management + bool recreateSwapchain(int width, int height); + + // Frame operations + VkCommandBuffer beginFrame(uint32_t& imageIndex); + void endFrame(VkCommandBuffer cmd, uint32_t imageIndex); + + // Single-time command buffer helpers + VkCommandBuffer beginSingleTimeCommands(); + void endSingleTimeCommands(VkCommandBuffer cmd); + + // Immediate submit for one-off GPU work (descriptor pool creation, etc.) + void immediateSubmit(std::function&& function); + + // Accessors + VkInstance getInstance() const { return instance; } + VkPhysicalDevice getPhysicalDevice() const { return physicalDevice; } + VkDevice getDevice() const { return device; } + VkQueue getGraphicsQueue() const { return graphicsQueue; } + uint32_t getGraphicsQueueFamily() const { return graphicsQueueFamily; } + VmaAllocator getAllocator() const { return allocator; } + VkSurfaceKHR getSurface() const { return surface; } + + VkSwapchainKHR getSwapchain() const { return swapchain; } + VkFormat getSwapchainFormat() const { return swapchainFormat; } + VkExtent2D getSwapchainExtent() const { return swapchainExtent; } + const std::vector& getSwapchainImageViews() const { return swapchainImageViews; } + uint32_t getSwapchainImageCount() const { return static_cast(swapchainImages.size()); } + + uint32_t getCurrentFrame() const { return currentFrame; } + const FrameData& getCurrentFrameData() const { return frames[currentFrame]; } + + // For ImGui + VkRenderPass getImGuiRenderPass() const { return imguiRenderPass; } + VkDescriptorPool getImGuiDescriptorPool() const { return imguiDescriptorPool; } + const std::vector& getSwapchainFramebuffers() const { return swapchainFramebuffers; } + + bool isSwapchainDirty() const { return swapchainDirty; } + +private: + bool createInstance(SDL_Window* window); + bool createSurface(SDL_Window* window); + bool selectPhysicalDevice(); + bool createLogicalDevice(); + bool createAllocator(); + bool createSwapchain(int width, int height); + void destroySwapchain(); + bool createCommandPools(); + bool createSyncObjects(); + bool createImGuiResources(); + void destroyImGuiResources(); + + // vk-bootstrap objects (kept alive for swapchain recreation etc.) + vkb::Instance vkbInstance_; + vkb::PhysicalDevice vkbPhysicalDevice_; + + VkInstance instance = VK_NULL_HANDLE; + VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE; + VkSurfaceKHR surface = VK_NULL_HANDLE; + VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; + VkDevice device = VK_NULL_HANDLE; + VmaAllocator allocator = VK_NULL_HANDLE; + + VkQueue graphicsQueue = VK_NULL_HANDLE; + VkQueue presentQueue = VK_NULL_HANDLE; + uint32_t graphicsQueueFamily = 0; + uint32_t presentQueueFamily = 0; + + // Swapchain + VkSwapchainKHR swapchain = VK_NULL_HANDLE; + VkFormat swapchainFormat = VK_FORMAT_UNDEFINED; + VkExtent2D swapchainExtent = {0, 0}; + std::vector swapchainImages; + std::vector swapchainImageViews; + std::vector swapchainFramebuffers; + bool swapchainDirty = false; + + // Per-frame resources + FrameData frames[MAX_FRAMES_IN_FLIGHT]; + uint32_t currentFrame = 0; + + // Immediate submit resources + VkCommandPool immCommandPool = VK_NULL_HANDLE; + VkFence immFence = VK_NULL_HANDLE; + + // Depth buffer (shared across all framebuffers) + VkImage depthImage = VK_NULL_HANDLE; + VkImageView depthImageView = VK_NULL_HANDLE; + VmaAllocation depthAllocation = VK_NULL_HANDLE; + VkFormat depthFormat = VK_FORMAT_D32_SFLOAT; + + bool createDepthBuffer(); + void destroyDepthBuffer(); + + // ImGui resources + VkRenderPass imguiRenderPass = VK_NULL_HANDLE; + VkDescriptorPool imguiDescriptorPool = VK_NULL_HANDLE; + +#ifndef NDEBUG + bool enableValidation = true; +#else + bool enableValidation = false; +#endif +}; + +} // namespace rendering +} // namespace wowee diff --git a/include/rendering/vk_frame_data.hpp b/include/rendering/vk_frame_data.hpp new file mode 100644 index 00000000..595b76ac --- /dev/null +++ b/include/rendering/vk_frame_data.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +namespace wowee { +namespace rendering { + +// Must match the PerFrame UBO layout in all shaders (std140 alignment) +struct GPUPerFrameData { + glm::mat4 view; + glm::mat4 projection; + glm::mat4 lightSpaceMatrix; + glm::vec4 lightDir; // xyz = direction, w = unused + glm::vec4 lightColor; // xyz = color, w = unused + glm::vec4 ambientColor; // xyz = color, w = unused + glm::vec4 viewPos; // xyz = camera pos, w = unused + glm::vec4 fogColor; // xyz = color, w = unused + glm::vec4 fogParams; // x = fogStart, y = fogEnd, z = time, w = unused + glm::vec4 shadowParams; // x = enabled(0/1), y = strength, z = unused, w = unused +}; + +// Push constants for the model matrix (most common case) +struct GPUPushConstants { + glm::mat4 model; +}; + +} // namespace rendering +} // namespace wowee diff --git a/include/rendering/vk_pipeline.hpp b/include/rendering/vk_pipeline.hpp new file mode 100644 index 00000000..0f85dee3 --- /dev/null +++ b/include/rendering/vk_pipeline.hpp @@ -0,0 +1,118 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace rendering { + +// Builder pattern for VkGraphicsPipeline creation. +// Usage: +// auto pipeline = PipelineBuilder() +// .setShaders(vertStage, fragStage) +// .setVertexInput(bindings, attributes) +// .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) +// .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_BACK_BIT) +// .setDepthTest(true, true, VK_COMPARE_OP_LESS) +// .setColorBlendAttachment(PipelineBuilder::blendAlpha()) +// .setLayout(pipelineLayout) +// .setRenderPass(renderPass) +// .build(device); + +class PipelineBuilder { +public: + PipelineBuilder(); + + // Shader stages + PipelineBuilder& setShaders(VkPipelineShaderStageCreateInfo vert, + VkPipelineShaderStageCreateInfo frag); + + // Vertex input + PipelineBuilder& setVertexInput( + const std::vector& bindings, + const std::vector& attributes); + + // No vertex input (fullscreen quad generated in vertex shader) + PipelineBuilder& setNoVertexInput(); + + // Input assembly + PipelineBuilder& setTopology(VkPrimitiveTopology topology, + VkBool32 primitiveRestart = VK_FALSE); + + // Rasterization + PipelineBuilder& setRasterization(VkPolygonMode polygonMode, + VkCullModeFlags cullMode, + VkFrontFace frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE); + + // Depth test/write + PipelineBuilder& setDepthTest(bool enable, bool writeEnable, + VkCompareOp compareOp = VK_COMPARE_OP_LESS); + + // No depth test (default) + PipelineBuilder& setNoDepthTest(); + + // Depth bias (for shadow maps) + PipelineBuilder& setDepthBias(float constantFactor, float slopeFactor); + + // Color blend attachment + PipelineBuilder& setColorBlendAttachment( + VkPipelineColorBlendAttachmentState blendState); + + // No color attachment (depth-only pass) + PipelineBuilder& setNoColorAttachment(); + + // Multisampling + PipelineBuilder& setMultisample(VkSampleCountFlagBits samples); + + // Pipeline layout + PipelineBuilder& setLayout(VkPipelineLayout layout); + + // Render pass + PipelineBuilder& setRenderPass(VkRenderPass renderPass, uint32_t subpass = 0); + + // Dynamic state + PipelineBuilder& setDynamicStates(const std::vector& states); + + // Build the pipeline + VkPipeline build(VkDevice device) const; + + // Common blend states + static VkPipelineColorBlendAttachmentState blendDisabled(); + static VkPipelineColorBlendAttachmentState blendAlpha(); + static VkPipelineColorBlendAttachmentState blendAdditive(); + +private: + std::vector shaderStages_; + std::vector vertexBindings_; + std::vector vertexAttributes_; + VkPrimitiveTopology topology_ = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + VkBool32 primitiveRestart_ = VK_FALSE; + VkPolygonMode polygonMode_ = VK_POLYGON_MODE_FILL; + VkCullModeFlags cullMode_ = VK_CULL_MODE_NONE; + VkFrontFace frontFace_ = VK_FRONT_FACE_COUNTER_CLOCKWISE; + bool depthTestEnable_ = false; + bool depthWriteEnable_ = false; + VkCompareOp depthCompareOp_ = VK_COMPARE_OP_LESS; + bool depthBiasEnable_ = false; + float depthBiasConstant_ = 0.0f; + float depthBiasSlope_ = 0.0f; + VkSampleCountFlagBits msaaSamples_ = VK_SAMPLE_COUNT_1_BIT; + std::vector colorBlendAttachments_; + VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE; + VkRenderPass renderPass_ = VK_NULL_HANDLE; + uint32_t subpass_ = 0; + std::vector dynamicStates_; +}; + +// Helper to create a pipeline layout from descriptor set layouts and push constant ranges +VkPipelineLayout createPipelineLayout(VkDevice device, + const std::vector& setLayouts, + const std::vector& pushConstants = {}); + +// Helper to create a descriptor set layout from bindings +VkDescriptorSetLayout createDescriptorSetLayout(VkDevice device, + const std::vector& bindings); + +} // namespace rendering +} // namespace wowee diff --git a/include/rendering/vk_render_target.hpp b/include/rendering/vk_render_target.hpp new file mode 100644 index 00000000..ef29620f --- /dev/null +++ b/include/rendering/vk_render_target.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include "rendering/vk_utils.hpp" +#include +#include +#include + +namespace wowee { +namespace rendering { + +class VkContext; + +/** + * Off-screen render target encapsulating VkRenderPass + VkFramebuffer + color VkImage. + * Used for minimap compositing, world map compositing, and other off-screen passes. + */ +class VkRenderTarget { +public: + VkRenderTarget() = default; + ~VkRenderTarget(); + + VkRenderTarget(const VkRenderTarget&) = delete; + VkRenderTarget& operator=(const VkRenderTarget&) = delete; + + /** + * Create the render target with given dimensions and format. + * Creates: color image, image view, sampler, render pass, framebuffer. + */ + bool create(VkContext& ctx, uint32_t width, uint32_t height, + VkFormat format = VK_FORMAT_R8G8B8A8_UNORM); + + /** + * Destroy all Vulkan resources. + */ + void destroy(VkDevice device, VmaAllocator allocator); + + /** + * Begin the off-screen render pass (clears to given color). + * Must be called outside any other active render pass. + */ + void beginPass(VkCommandBuffer cmd, + const VkClearColorValue& clear = {{0.0f, 0.0f, 0.0f, 1.0f}}); + + /** + * End the off-screen render pass. + * After this, the color image is in SHADER_READ_ONLY_OPTIMAL layout. + */ + void endPass(VkCommandBuffer cmd); + + // Accessors + VkImageView getColorImageView() const { return colorImage_.imageView; } + VkSampler getSampler() const { return sampler_; } + VkRenderPass getRenderPass() const { return renderPass_; } + VkExtent2D getExtent() const { return { colorImage_.extent.width, colorImage_.extent.height }; } + VkFormat getFormat() const { return colorImage_.format; } + bool isValid() const { return framebuffer_ != VK_NULL_HANDLE; } + + /** + * Descriptor info for binding the color attachment as a texture in a shader. + */ + VkDescriptorImageInfo descriptorInfo() const; + +private: + AllocatedImage colorImage_{}; + VkSampler sampler_ = VK_NULL_HANDLE; + VkRenderPass renderPass_ = VK_NULL_HANDLE; + VkFramebuffer framebuffer_ = VK_NULL_HANDLE; +}; + +} // namespace rendering +} // namespace wowee diff --git a/include/rendering/vk_shader.hpp b/include/rendering/vk_shader.hpp new file mode 100644 index 00000000..cd8fc839 --- /dev/null +++ b/include/rendering/vk_shader.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace rendering { + +class VkShaderModule { +public: + VkShaderModule() = default; + ~VkShaderModule(); + + VkShaderModule(const VkShaderModule&) = delete; + VkShaderModule& operator=(const VkShaderModule&) = delete; + VkShaderModule(VkShaderModule&& other) noexcept; + VkShaderModule& operator=(VkShaderModule&& other) noexcept; + + // Load a SPIR-V file from disk + bool loadFromFile(VkDevice device, const std::string& path); + + // Load from raw SPIR-V bytes + bool loadFromMemory(VkDevice device, const uint32_t* code, size_t sizeBytes); + + void destroy(); + + ::VkShaderModule getModule() const { return module_; } + bool isValid() const { return module_ != VK_NULL_HANDLE; } + + // Create a VkPipelineShaderStageCreateInfo for this module + VkPipelineShaderStageCreateInfo stageInfo(VkShaderStageFlagBits stage, + const char* entryPoint = "main") const; + +private: + VkDevice device_ = VK_NULL_HANDLE; + ::VkShaderModule module_ = VK_NULL_HANDLE; +}; + +// Convenience: load a shader stage directly from a .spv file +VkPipelineShaderStageCreateInfo loadShaderStage(VkDevice device, + const std::string& path, VkShaderStageFlagBits stage); + +} // namespace rendering +} // namespace wowee diff --git a/include/rendering/vk_texture.hpp b/include/rendering/vk_texture.hpp new file mode 100644 index 00000000..83167d9d --- /dev/null +++ b/include/rendering/vk_texture.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include "rendering/vk_utils.hpp" +#include +#include +#include +#include + +namespace wowee { +namespace rendering { + +class VkContext; + +class VkTexture { +public: + VkTexture() = default; + ~VkTexture(); + + VkTexture(const VkTexture&) = delete; + VkTexture& operator=(const VkTexture&) = delete; + VkTexture(VkTexture&& other) noexcept; + VkTexture& operator=(VkTexture&& other) noexcept; + + // Upload RGBA8 pixel data to GPU + bool upload(VkContext& ctx, const uint8_t* pixels, uint32_t width, uint32_t height, + VkFormat format = VK_FORMAT_R8G8B8A8_UNORM, bool generateMips = true); + + // Upload with pre-existing mip data (array of mip levels) + bool uploadMips(VkContext& ctx, const uint8_t* const* mipData, const uint32_t* mipSizes, + uint32_t mipCount, uint32_t width, uint32_t height, + VkFormat format = VK_FORMAT_R8G8B8A8_UNORM); + + // Create a depth/stencil texture (no upload) + bool createDepth(VkContext& ctx, uint32_t width, uint32_t height, + VkFormat format = VK_FORMAT_D32_SFLOAT); + + // Create sampler with specified filtering + bool createSampler(VkDevice device, + VkFilter minFilter = VK_FILTER_LINEAR, + VkFilter magFilter = VK_FILTER_LINEAR, + VkSamplerAddressMode addressMode = VK_SAMPLER_ADDRESS_MODE_REPEAT, + float maxAnisotropy = 16.0f); + + // Overload with separate S/T address modes + bool createSampler(VkDevice device, + VkFilter filter, + VkSamplerAddressMode addressModeU, + VkSamplerAddressMode addressModeV, + float maxAnisotropy = 16.0f); + + // Create a comparison sampler (for shadow mapping) + bool createShadowSampler(VkDevice device); + + void destroy(VkDevice device, VmaAllocator allocator); + + VkImage getImage() const { return image_.image; } + VkImageView getImageView() const { return image_.imageView; } + VkSampler getSampler() const { return sampler_; } + uint32_t getWidth() const { return image_.extent.width; } + uint32_t getHeight() const { return image_.extent.height; } + VkFormat getFormat() const { return image_.format; } + uint32_t getMipLevels() const { return mipLevels_; } + bool isValid() const { return image_.image != VK_NULL_HANDLE; } + + // Write descriptor info for binding + VkDescriptorImageInfo descriptorInfo(VkImageLayout layout = + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) const; + +private: + void generateMipmaps(VkContext& ctx, VkFormat format, uint32_t width, uint32_t height); + + AllocatedImage image_{}; + VkSampler sampler_ = VK_NULL_HANDLE; + uint32_t mipLevels_ = 1; +}; + +} // namespace rendering +} // namespace wowee diff --git a/include/rendering/vk_utils.hpp b/include/rendering/vk_utils.hpp new file mode 100644 index 00000000..e64104f9 --- /dev/null +++ b/include/rendering/vk_utils.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace rendering { + +class VkContext; + +struct AllocatedBuffer { + VkBuffer buffer = VK_NULL_HANDLE; + VmaAllocation allocation = VK_NULL_HANDLE; + VmaAllocationInfo info{}; +}; + +struct AllocatedImage { + VkImage image = VK_NULL_HANDLE; + VmaAllocation allocation = VK_NULL_HANDLE; + VkImageView imageView = VK_NULL_HANDLE; + VkExtent2D extent{}; + VkFormat format = VK_FORMAT_UNDEFINED; +}; + +// Buffer creation +AllocatedBuffer createBuffer(VmaAllocator allocator, VkDeviceSize size, + VkBufferUsageFlags usage, VmaMemoryUsage memoryUsage); + +void destroyBuffer(VmaAllocator allocator, AllocatedBuffer& buffer); + +// Image creation +AllocatedImage createImage(VkDevice device, VmaAllocator allocator, + uint32_t width, uint32_t height, VkFormat format, + VkImageUsageFlags usage, VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT, + uint32_t mipLevels = 1); + +void destroyImage(VkDevice device, VmaAllocator allocator, AllocatedImage& image); + +// Image layout transitions +void transitionImageLayout(VkCommandBuffer cmd, VkImage image, + VkImageLayout oldLayout, VkImageLayout newLayout, + VkPipelineStageFlags srcStage = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VkPipelineStageFlags dstStage = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT); + +// Staging upload helper — copies CPU data to a GPU-local buffer +AllocatedBuffer uploadBuffer(VkContext& ctx, const void* data, VkDeviceSize size, + VkBufferUsageFlags usage); + +// Check VkResult and log on failure +inline bool vkCheck(VkResult result, const char* msg) { + if (result != VK_SUCCESS) { + // Caller should log the message + return false; + } + return true; +} + +} // namespace rendering +} // namespace wowee diff --git a/include/rendering/water_renderer.hpp b/include/rendering/water_renderer.hpp index 653e0742..292b75d7 100644 --- a/include/rendering/water_renderer.hpp +++ b/include/rendering/water_renderer.hpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include namespace wowee { @@ -16,127 +18,77 @@ namespace pipeline { namespace rendering { class Camera; -class Shader; +class VkContext; /** * Water surface for a single map chunk */ struct WaterSurface { - glm::vec3 position; // World position - glm::vec3 origin; // Mesh origin (world) - glm::vec3 stepX; // Mesh X step vector in world space - glm::vec3 stepY; // Mesh Y step vector in world space - float minHeight; // Minimum water height - float maxHeight; // Maximum water height - uint16_t liquidType; // LiquidType.dbc ID (WotLK) + glm::vec3 position; + glm::vec3 origin; + glm::vec3 stepX; + glm::vec3 stepY; + float minHeight; + float maxHeight; + uint16_t liquidType; - // Owning tile coordinates (for per-tile removal) int tileX = -1, tileY = -1; - - // Owning WMO instance ID (for WMO liquid removal, 0 = terrain water) uint32_t wmoId = 0; - // Water layer dimensions within chunk (0-7 offset, 1-8 size) uint8_t xOffset = 0; uint8_t yOffset = 0; - uint8_t width = 8; // Width in tiles (1-8) - uint8_t height = 8; // Height in tiles (1-8) + uint8_t width = 8; + uint8_t height = 8; - // Height map for water surface ((width+1) x (height+1) vertices) std::vector heights; - - // Render mask (which tiles have water) std::vector mask; - // Render data - uint32_t vao = 0; - uint32_t vbo = 0; - uint32_t ebo = 0; + // Vulkan render data + ::VkBuffer vertexBuffer = VK_NULL_HANDLE; + VmaAllocation vertexAlloc = VK_NULL_HANDLE; + ::VkBuffer indexBuffer = VK_NULL_HANDLE; + VmaAllocation indexAlloc = VK_NULL_HANDLE; int indexCount = 0; + // Per-surface material UBO + ::VkBuffer materialUBO = VK_NULL_HANDLE; + VmaAllocation materialAlloc = VK_NULL_HANDLE; + + // Material descriptor set (set 1) + VkDescriptorSet materialSet = VK_NULL_HANDLE; + bool hasHeightData() const { return !heights.empty(); } }; /** - * Water renderer - * - * Renders water surfaces with transparency and animation. - * Supports multiple liquid types (water, ocean, magma, slime). + * Water renderer (Vulkan) */ class WaterRenderer { public: WaterRenderer(); ~WaterRenderer(); - bool initialize(); + bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); - /** - * Load water surfaces from ADT terrain - * @param terrain The ADT terrain data - * @param append If true, add to existing water instead of replacing - * @param tileX Tile X coordinate for tracking ownership (-1 = untracked) - * @param tileY Tile Y coordinate for tracking ownership (-1 = untracked) - */ void loadFromTerrain(const pipeline::ADTTerrain& terrain, bool append = false, int tileX = -1, int tileY = -1); - /** - * Load water surface from WMO liquid data - * @param liquid WMO liquid data from MLIQ chunk - * @param modelMatrix WMO instance model matrix for transforming to world space - * @param wmoId WMO instance ID for tracking ownership - */ void loadFromWMO(const pipeline::WMOLiquid& liquid, const glm::mat4& modelMatrix, uint32_t wmoId); - - /** - * Remove all water surfaces belonging to a specific WMO instance - * @param wmoId WMO instance ID - */ void removeWMO(uint32_t wmoId); - - /** - * Remove all water surfaces belonging to a specific tile - * @param tileX Tile X coordinate - * @param tileY Tile Y coordinate - */ void removeTile(int tileX, int tileY); - - /** - * Clear all water surfaces - */ void clear(); - /** - * Render all water surfaces - */ - void render(const Camera& camera, float time); + void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera, float time); - /** - * Enable/disable water rendering - */ void setEnabled(bool enabled) { renderingEnabled = enabled; } bool isEnabled() const { return renderingEnabled; } - /** - * Query the water height at a given world position. - * Returns the highest water surface height at that XY, or nullopt if no water. - */ std::optional getWaterHeightAt(float glX, float glY) const; std::optional getWaterTypeAt(float glX, float glY) const; - /** - * Get water surface count - */ int getSurfaceCount() const { return static_cast(surfaces.size()); } - /** - * Set fog parameters - */ - void setFog(const glm::vec3& color, float start, float end) { - fogColor = color; fogStart = start; fogEnd = end; - } - private: void createWaterMesh(WaterSurface& surface); void destroyWaterMesh(WaterSurface& surface); @@ -144,14 +96,20 @@ private: glm::vec4 getLiquidColor(uint16_t liquidType) const; float getLiquidAlpha(uint16_t liquidType) const; - std::unique_ptr waterShader; + void updateMaterialUBO(WaterSurface& surface); + VkDescriptorSet allocateMaterialSet(); + + VkContext* vkCtx = nullptr; + + // Pipeline + VkPipeline waterPipeline = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkDescriptorSetLayout materialSetLayout = VK_NULL_HANDLE; + VkDescriptorPool materialDescPool = VK_NULL_HANDLE; + static constexpr uint32_t MAX_WATER_SETS = 2048; + std::vector surfaces; bool renderingEnabled = true; - - // Fog parameters - glm::vec3 fogColor = glm::vec3(0.5f, 0.6f, 0.7f); - float fogStart = 800.0f; // Match WMO renderer fog settings - float fogEnd = 1500.0f; }; } // namespace rendering diff --git a/include/rendering/weather.hpp b/include/rendering/weather.hpp index 08a78694..8423d91b 100644 --- a/include/rendering/weather.hpp +++ b/include/rendering/weather.hpp @@ -1,15 +1,15 @@ #pragma once -#include +#include +#include #include -#include #include namespace wowee { namespace rendering { class Camera; -class Shader; +class VkContext; /** * @brief Weather particle system for rain and snow @@ -20,7 +20,7 @@ class Shader; * - Particle recycling for efficiency * - Camera-relative positioning (follows player) * - Adjustable intensity (light, medium, heavy) - * - GPU instanced rendering + * - Vulkan point-sprite rendering */ class Weather { public: @@ -35,9 +35,11 @@ public: /** * @brief Initialize weather system + * @param ctx Vulkan context + * @param perFrameLayout Descriptor set layout for the per-frame UBO (set 0) * @return true if initialization succeeded */ - bool initialize(); + bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); /** * @brief Update weather particles @@ -48,9 +50,10 @@ public: /** * @brief Render weather particles - * @param camera Camera for rendering + * @param cmd Command buffer to record into + * @param perFrameSet Per-frame descriptor set (set 0, contains camera UBO) */ - void render(const Camera& camera); + void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet); /** * @brief Set weather type @@ -75,6 +78,11 @@ public: */ int getParticleCount() const; + /** + * @brief Clean up Vulkan resources + */ + void shutdown(); + private: struct Particle { glm::vec3 position; @@ -83,15 +91,20 @@ private: float maxLifetime; }; - void cleanup(); void resetParticles(const Camera& camera); void updateParticle(Particle& particle, const Camera& camera, float deltaTime); glm::vec3 getRandomPosition(const glm::vec3& center) const; - // OpenGL objects - GLuint vao = 0; - GLuint vbo = 0; // Instance buffer - std::unique_ptr shader; + // Vulkan objects + VkContext* vkCtx = nullptr; + VkPipeline pipeline = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + + // Dynamic mapped buffer for particle positions (updated every frame) + ::VkBuffer dynamicVB = VK_NULL_HANDLE; + VmaAllocation dynamicVBAlloc = VK_NULL_HANDLE; + VmaAllocationInfo dynamicVBAllocInfo{}; + VkDeviceSize dynamicVBSize = 0; // Particles std::vector particles; diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 82295ca1..35dc3b2f 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -1,6 +1,7 @@ #pragma once -#include +#include +#include #include #include #include @@ -19,12 +20,13 @@ namespace pipeline { namespace rendering { class Camera; -class Shader; class Frustum; class M2Renderer; +class VkContext; +class VkTexture; /** - * WMO (World Model Object) Renderer + * WMO (World Model Object) Renderer (Vulkan) * * Renders buildings, dungeons, and large structures from WMO files. * Features: @@ -32,7 +34,6 @@ class M2Renderer; * - Batched rendering per group * - Frustum culling * - Portal visibility (future) - * - Dynamic lighting support (future) */ class WMORenderer { public: @@ -40,10 +41,13 @@ public: ~WMORenderer(); /** - * Initialize renderer and create shaders + * Initialize renderer (Vulkan) + * @param ctx Vulkan context + * @param perFrameLayout Descriptor set layout for set 0 (per-frame UBO) * @param assetManager Asset manager for loading textures (optional) */ - bool initialize(pipeline::AssetManager* assetManager = nullptr); + bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, + pipeline::AssetManager* assetManager = nullptr); /** * Cleanup GPU resources @@ -132,12 +136,22 @@ public: void clearInstances(); /** - * Render all WMO instances - * @param camera Camera for view/projection matrices - * @param view View matrix - * @param projection Projection matrix + * Render all WMO instances (Vulkan) + * @param cmd Command buffer to record into + * @param perFrameSet Per-frame descriptor set (set 0) + * @param camera Camera for frustum culling */ - 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 for shadow casting + */ + void renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix); /** * Get number of loaded models @@ -204,32 +218,22 @@ public: uint32_t getDistanceCulledGroups() const { return lastDistanceCulledGroups; } /** - * Enable/disable GPU occlusion query culling + * Enable/disable GPU occlusion query culling (stubbed in Vulkan) */ - void setOcclusionCulling(bool enabled) { occlusionCulling = enabled; } - bool isOcclusionCullingEnabled() const { return occlusionCulling; } + void setOcclusionCulling(bool /*enabled*/) { /* stubbed */ } + bool isOcclusionCullingEnabled() const { return false; } /** * Get number of groups culled by occlusion queries last frame */ - uint32_t getOcclusionCulledGroups() const { return lastOcclusionCulledGroups; } + uint32_t getOcclusionCulledGroups() const { return 0; } - void setFog(const glm::vec3& color, float start, float end) { - fogColor = color; fogStart = start; fogEnd = end; - } - - void setLighting(const float lightDir[3], const float lightColor[3], - const float ambientColor[3]); - - void setShadowMap(GLuint depthTex, const glm::mat4& lightSpace) { - shadowDepthTex = depthTex; lightSpaceMatrix = lightSpace; shadowEnabled = true; - } - void clearShadowMap() { shadowEnabled = false; } - - /** - * Render depth-only for shadow casting (reuses VAOs) - */ - void renderShadow(const glm::mat4& lightView, const glm::mat4& lightProj, Shader& shadowShader); + // Lighting/fog/shadow are now in the per-frame UBO; these are no-ops for API compat + void setFog(const glm::vec3& /*color*/, float /*start*/, float /*end*/) {} + void setLighting(const float /*lightDir*/[3], const float /*lightColor*/[3], + const float /*ambientColor*/[3]) {} + void setShadowMap(uint32_t /*depthTex*/, const glm::mat4& /*lightSpace*/) {} + void clearShadowMap() {} /** * Get floor height at a GL position via ray-triangle intersection. @@ -297,13 +301,23 @@ public: void precomputeFloorCache(); private: + // WMO material UBO — matches WMOMaterial in wmo.frag.glsl + struct WMOMaterialUBO { + int32_t hasTexture; + int32_t alphaTest; + int32_t unlit; + int32_t isInterior; + float specularIntensity; + }; + /** * WMO group GPU resources */ struct GroupResources { - 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; glm::vec3 boundingBoxMin; @@ -322,13 +336,17 @@ private: // Pre-merged batches for efficient rendering (computed at load time) struct MergedBatch { - GLuint texId; - bool hasTexture; - bool alphaTest; + VkTexture* texture = nullptr; // from cache, NOT owned + VkDescriptorSet materialSet = VK_NULL_HANDLE; // set 1 + ::VkBuffer materialUBO = VK_NULL_HANDLE; + VmaAllocation materialUBOAlloc = VK_NULL_HANDLE; + bool hasTexture = false; + bool alphaTest = false; bool unlit = false; - uint32_t blendMode = 0; - std::vector counts; - std::vector offsets; + bool isTransparent = false; // blendMode >= 2 + // For multi-draw: store index ranges + struct DrawRange { uint32_t firstIndex; uint32_t indexCount; }; + std::vector draws; }; std::vector mergedBatches; @@ -401,7 +419,7 @@ private: std::vector doodadTemplates; // Texture handles for this model (indexed by texture path order) - std::vector textures; + std::vector textures; // non-owning, from cache // Material texture indices (materialId -> texture index) std::vector materialTextureIndices; @@ -458,13 +476,6 @@ private: */ bool createGroupResources(const pipeline::WMOGroup& group, GroupResources& resources, uint32_t groupFlags = 0); - /** - * Render a single group - */ - void renderGroup(const GroupResources& group, const ModelData& model, - const glm::mat4& modelMatrix, - const glm::mat4& view, const glm::mat4& projection); - /** * Check if group is visible in frustum */ @@ -479,11 +490,6 @@ private: /** * Get visible groups via portal traversal - * @param model The WMO model data - * @param cameraLocalPos Camera position in model space - * @param frustum Frustum for portal visibility testing - * @param modelMatrix Transform for world-space frustum test - * @param outVisibleGroups Output set of visible group indices */ void getVisibleGroupsViaPortals(const ModelData& model, const glm::vec3& cameraLocalPos, @@ -502,23 +508,17 @@ private: /** * Load a texture from path */ - GLuint loadTexture(const std::string& path); + VkTexture* loadTexture(const std::string& path); /** - * Initialize occlusion query resources (bbox VAO, shader) + * Allocate a material descriptor set from the pool */ - void initOcclusionResources(); + VkDescriptorSet allocateMaterialSet(); /** - * Run occlusion query pre-pass for an instance + * Destroy GPU resources for a single group */ - void runOcclusionQueries(const WMOInstance& instance, const ModelData& model, - const glm::mat4& view, const glm::mat4& projection); - - /** - * Check if a group passed occlusion test (uses previous frame results) - */ - bool isGroupOccluded(uint32_t instanceId, uint32_t groupIndex) const; + void destroyGroupGPU(GroupResources& group); struct GridCell { int x; @@ -541,8 +541,8 @@ private: void rebuildSpatialIndex(); void gatherCandidates(const glm::vec3& queryMin, const glm::vec3& queryMax, std::vector& outIndices) const; - // Shader - std::unique_ptr shader; + // Vulkan context + VkContext* vkCtx_ = nullptr; // Asset manager for loading textures pipeline::AssetManager* assetManager = nullptr; @@ -553,9 +553,31 @@ private: // Current map name for zone-specific floor cache std::string mapName_; - // Texture cache (path -> texture ID) + // Vulkan pipelines + VkPipeline opaquePipeline_ = VK_NULL_HANDLE; + VkPipeline transparentPipeline_ = VK_NULL_HANDLE; + VkPipeline wireframePipeline_ = VK_NULL_HANDLE; + 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; + + // Descriptor set layouts + VkDescriptorSetLayout materialSetLayout_ = VK_NULL_HANDLE; + + // Descriptor pool for material sets + VkDescriptorPool materialDescPool_ = VK_NULL_HANDLE; + static constexpr uint32_t MAX_MATERIAL_SETS = 8192; + + // Texture cache (path -> VkTexture) struct TextureCacheEntry { - GLuint id = 0; + std::unique_ptr texture; size_t approxBytes = 0; uint64_t lastUse = 0; }; @@ -565,7 +587,7 @@ private: size_t textureCacheBudgetBytes_ = 2048ull * 1024 * 1024; // Default, overridden at init // Default white texture - GLuint whiteTexture = 0; + std::unique_ptr whiteTexture_; // Loaded models (modelId -> ModelData) std::unordered_map loadedModels; @@ -581,38 +603,11 @@ private: bool frustumCulling = true; bool portalCulling = false; // Disabled by default - needs debugging bool distanceCulling = false; // Disabled - causes ground to disappear - bool occlusionCulling = false; // GPU occlusion queries - disabled, adds overhead float maxGroupDistance = 500.0f; float maxGroupDistanceSq = 250000.0f; // maxGroupDistance^2 uint32_t lastDrawCalls = 0; mutable uint32_t lastPortalCulledGroups = 0; mutable uint32_t lastDistanceCulledGroups = 0; - mutable uint32_t lastOcclusionCulledGroups = 0; - - // Occlusion query resources - GLuint bboxVao = 0; - GLuint bboxVbo = 0; - std::unique_ptr occlusionShader; - // Query objects per (instance, group) - reused each frame - // Key: (instanceId << 16) | groupIndex - mutable std::unordered_map occlusionQueries; - // Results from previous frame (1 frame latency to avoid GPU stalls) - mutable std::unordered_map occlusionResults; - - // Fog parameters - glm::vec3 fogColor = glm::vec3(0.5f, 0.6f, 0.7f); - float fogStart = 3000.0f; // Increased to allow clearer visibility at distance - float fogEnd = 4000.0f; // Increased to match extended view distance - - // Lighting parameters - float lightDir[3] = {-0.3f, -0.7f, -0.6f}; - float lightColor[3] = {1.5f, 1.4f, 1.3f}; - float ambientColor[3] = {0.55f, 0.55f, 0.6f}; - - // Shadow mapping - GLuint shadowDepthTex = 0; - glm::mat4 lightSpaceMatrix = glm::mat4(1.0f); - bool shadowEnabled = false; // Optional query-space culling for collision/raycast hot paths. bool collisionFocusEnabled = false; @@ -636,7 +631,6 @@ private: std::vector visibleGroups; // group indices that passed culling uint32_t portalCulled = 0; uint32_t distanceCulled = 0; - uint32_t occlusionCulled = 0; }; // Collision query profiling (per frame). diff --git a/include/rendering/world_map.hpp b/include/rendering/world_map.hpp index 128afc00..13e7614f 100644 --- a/include/rendering/world_map.hpp +++ b/include/rendering/world_map.hpp @@ -1,9 +1,11 @@ #pragma once -#include +#include +#include #include #include #include +#include #include #include @@ -11,7 +13,9 @@ namespace wowee { namespace pipeline { class AssetManager; } namespace rendering { -class Shader; +class VkContext; +class VkTexture; +class VkRenderTarget; struct WorldMapZone { uint32_t wmaID = 0; @@ -22,8 +26,8 @@ struct WorldMapZone { uint32_t parentWorldMapID = 0; uint32_t exploreFlag = 0; - // Per-zone cached textures - GLuint tileTextures[12] = {}; + // Per-zone cached textures (owned by WorldMap::zoneTextures) + VkTexture* tileTextures[12] = {}; bool tilesLoaded = false; }; @@ -32,8 +36,15 @@ public: WorldMap(); ~WorldMap(); - void initialize(pipeline::AssetManager* assetManager); + bool initialize(VkContext* ctx, pipeline::AssetManager* assetManager); + void shutdown(); + + /// Off-screen composite pass — call BEFORE the main render pass begins. + void compositePass(VkCommandBuffer cmd); + + /// ImGui overlay — call INSIDE the main render pass (during ImGui frame). void render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight); + void setMapName(const std::string& name); void setServerExplorationMask(const std::vector& masks, bool hasData); bool isOpen() const { return open; } @@ -42,9 +53,6 @@ public: private: enum class ViewLevel { WORLD, CONTINENT, ZONE }; - void createFBO(); - void createTileShader(); - void createQuad(); void enterWorldView(); void loadZonesFromDBC(); int findBestContinentForPlayer(const glm::vec3& playerRenderPos) const; @@ -53,15 +61,15 @@ private: bool getContinentProjectionBounds(int contIdx, float& left, float& right, float& top, float& bottom) const; void loadZoneTextures(int zoneIdx); - void compositeZone(int zoneIdx); + void requestComposite(int zoneIdx); void renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight); void updateExploration(const glm::vec3& playerRenderPos); void zoomIn(const glm::vec3& playerRenderPos); void zoomOut(); - - // World pos → map UV using a specific zone's bounds glm::vec2 renderPosToMapUV(const glm::vec3& renderPos, int zoneIdx) const; + void destroyZoneTextures(); + VkContext* vkCtx = nullptr; pipeline::AssetManager* assetManager = nullptr; bool initialized = false; bool open = false; @@ -70,28 +78,45 @@ private: // All zones for current map std::vector zones; - int continentIdx = -1; // index of AreaID=0 entry in zones - int currentIdx = -1; // currently displayed zone index + int continentIdx = -1; + int currentIdx = -1; ViewLevel viewLevel = ViewLevel::CONTINENT; - int compositedIdx = -1; // which zone is currently composited in FBO + int compositedIdx = -1; + int pendingCompositeIdx = -1; - // FBO for composited map (4x3 tiles = 1024x768) + // FBO replacement (4x3 tiles = 1024x768) static constexpr int GRID_COLS = 4; static constexpr int GRID_ROWS = 3; static constexpr int TILE_PX = 256; - static constexpr int FBO_W = GRID_COLS * TILE_PX; // 1024 - static constexpr int FBO_H = GRID_ROWS * TILE_PX; // 768 + static constexpr int FBO_W = GRID_COLS * TILE_PX; + static constexpr int FBO_H = GRID_ROWS * TILE_PX; - GLuint fbo = 0; - GLuint fboTexture = 0; - std::unique_ptr tileShader; - GLuint tileQuadVAO = 0; - GLuint tileQuadVBO = 0; + std::unique_ptr compositeTarget; + + // Quad vertex buffer (pos2 + uv2) + ::VkBuffer quadVB = VK_NULL_HANDLE; + VmaAllocation quadVBAlloc = VK_NULL_HANDLE; + + // Descriptor resources + VkDescriptorSetLayout samplerSetLayout = VK_NULL_HANDLE; + VkDescriptorPool descPool = VK_NULL_HANDLE; + static constexpr uint32_t MAX_DESC_SETS = 32; + + // Tile composite pipeline + VkPipeline tilePipeline = VK_NULL_HANDLE; + VkPipelineLayout tilePipelineLayout = VK_NULL_HANDLE; + VkDescriptorSet tileDescSets[2][12] = {}; // [frameInFlight][tileSlot] + + // ImGui display descriptor set (points to composite render target) + VkDescriptorSet imguiDisplaySet = VK_NULL_HANDLE; + + // Texture storage (owns all VkTexture objects for zone tiles) + std::vector> zoneTextures; // Exploration / fog of war std::vector serverExplorationMask; bool hasServerExplorationMask = false; - std::unordered_set exploredZones; // zone indices the player has visited + std::unordered_set exploredZones; }; } // namespace rendering diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index b9af2763..1d939dd4 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -2,7 +2,7 @@ #include "game/game_handler.hpp" #include "game/inventory.hpp" -#include "rendering/world_map.hpp" +// WorldMap is now owned by Renderer, accessed via getWorldMap() #include "rendering/character_preview.hpp" #include "ui/inventory_screen.hpp" #include "ui/quest_log_screen.hpp" @@ -212,7 +212,7 @@ private: QuestLogScreen questLogScreen; SpellbookScreen spellbookScreen; TalentScreen talentScreen; - rendering::WorldMap worldMap; + // WorldMap is now owned by Renderer (accessed via renderer->getWorldMap()) // Spell icon cache: spellId -> GL texture ID std::unordered_map spellIconCache_; diff --git a/src/core/application.cpp b/src/core/application.cpp index ec1e39fa..8ff52812 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -44,7 +44,7 @@ #include "pipeline/dbc_layout.hpp" #include -#include +// GL/glew.h removed — Vulkan migration Phase 1 #include #include #include @@ -266,7 +266,7 @@ void Application::run() { int newWidth = event.window.data1; int newHeight = event.window.data2; window->setSize(newWidth, newHeight); - glViewport(0, 0, newWidth, newHeight); + // Vulkan viewport set in command buffer, not globally if (renderer && renderer->getCamera()) { renderer->getCamera()->setAspectRatio(static_cast(newWidth) / newHeight); } @@ -2337,7 +2337,7 @@ void Application::spawnPlayerCharacter() { layers.push_back(up); } if (layers.size() > 1) { - GLuint compositeTex = charRenderer->compositeTextures(layers); + rendering::VkTexture* compositeTex = charRenderer->compositeTextures(layers); if (compositeTex != 0) { for (size_t ti = 0; ti < model.textures.size(); ti++) { if (model.textures[ti].type == 1) { @@ -2352,8 +2352,8 @@ void Application::spawnPlayerCharacter() { } // Override hair texture on GPU (type-6 slot) after model load if (!hairTexturePath.empty()) { - GLuint hairTex = charRenderer->loadTexture(hairTexturePath); - if (hairTex != 0) { + rendering::VkTexture* hairTex = charRenderer->loadTexture(hairTexturePath); + if (hairTex) { for (size_t ti = 0; ti < model.textures.size(); ti++) { if (model.textures[ti].type == 6) { charRenderer->setModelTexture(1, static_cast(ti), hairTex); @@ -2929,6 +2929,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float // --- Loading screen for online mode --- rendering::LoadingScreen loadingScreen; + loadingScreen.setVkContext(window->getVkContext()); bool loadingScreenOk = loadingScreen.initialize(); auto showProgress = [&](const char* msg, float progress) { @@ -2944,7 +2945,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float int w = event.window.data1; int h = event.window.data2; window->setSize(w, h); - glViewport(0, 0, w, h); + // Vulkan viewport set in command buffer if (renderer && renderer->getCamera()) { renderer->getCamera()->setAspectRatio(static_cast(w) / h); } @@ -3131,7 +3132,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float int w = event.window.data1; int h = event.window.data2; window->setSize(w, h); - glViewport(0, 0, w, h); + // Vulkan viewport set in command buffer if (renderer->getCamera()) { renderer->getCamera()->setAspectRatio(static_cast(w) / h); } @@ -3246,7 +3247,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float int w = event.window.data1; int h = event.window.data2; window->setSize(w, h); - glViewport(0, 0, w, h); + // Vulkan viewport set in command buffer if (renderer && renderer->getCamera()) { renderer->getCamera()->setAspectRatio(static_cast(w) / h); } @@ -3933,7 +3934,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x std::string bakePath = "Textures\\BakedNpcTextures\\" + extra.bakeName; // Composite equipment textures over baked NPC texture, or just load baked texture - GLuint finalTex = 0; + rendering::VkTexture* finalTex = nullptr; if (allowNpcRegionComposite && !npcRegionLayers.empty()) { finalTex = charRenderer->compositeWithRegions(bakePath, {}, npcRegionLayers); LOG_DEBUG("Composited NPC baked texture with ", npcRegionLayers.size(), @@ -3942,7 +3943,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x finalTex = charRenderer->loadTexture(bakePath); } - if (finalTex != 0 && modelData) { + if (finalTex && modelData) { for (size_t ti = 0; ti < modelData->textures.size(); ti++) { uint32_t texType = modelData->textures[ti].type; // Humanoid NPCs typically use creature-skin texture types (11-13). @@ -4008,7 +4009,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (!npcFaceUpper.empty()) skinLayers.push_back(npcFaceUpper); for (const auto& uw : npcUnderwear) skinLayers.push_back(uw); - GLuint npcSkinTex = 0; + rendering::VkTexture* npcSkinTex = nullptr; if (allowNpcRegionComposite && !npcRegionLayers.empty()) { npcSkinTex = charRenderer->compositeWithRegions(npcSkinPath, std::vector(skinLayers.begin() + 1, skinLayers.end()), @@ -4019,7 +4020,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x npcSkinTex = charRenderer->loadTexture(npcSkinPath); } - if (npcSkinTex != 0 && modelData) { + if (npcSkinTex && modelData) { for (size_t ti = 0; ti < modelData->textures.size(); ti++) { uint32_t texType = modelData->textures[ti].type; if (texType == 1 || texType == 11 || texType == 12 || texType == 13) { @@ -4058,8 +4059,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x } if (!hairTexPath.empty()) { - GLuint hairTex = charRenderer->loadTexture(hairTexPath); - if (hairTex != 0 && modelData) { + rendering::VkTexture* hairTex = charRenderer->loadTexture(hairTexPath); + if (hairTex && modelData) { for (size_t ti = 0; ti < modelData->textures.size(); ti++) { if (modelData->textures[ti].type == 6) { charRenderer->setModelTexture(modelId, static_cast(ti), hairTex); @@ -4136,8 +4137,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x } if (!skinPath.empty()) { - GLuint skinTex = charRenderer->loadTexture(skinPath); - if (skinTex != 0) { + rendering::VkTexture* skinTex = charRenderer->loadTexture(skinPath); + if (skinTex) { charRenderer->setModelTexture(modelId, static_cast(ti), skinTex); LOG_DEBUG("Applied creature skin texture: ", skinPath, " to slot ", ti); } @@ -4362,7 +4363,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x uint16_t geosetPants = pickGeoset(1301, 13); // Bare legs (group 13) uint16_t geosetCape = 0; // Group 15 disabled unless cape is equipped uint16_t geosetTabard = 0; // TODO: NPC tabard geosets currently flicker/apron; keep hidden for now - GLuint npcCapeTextureId = 0; + rendering::VkTexture* npcCapeTextureId = nullptr; // Load equipment geosets from ItemDisplayInfo.dbc // DBC columns: 7=GeosetGroup[0], 8=GeosetGroup[1], 9=GeosetGroup[2] @@ -4467,10 +4468,10 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x addCapeCandidate(baseTex + "_U.blp"); } } - const GLuint whiteTex = charRenderer->loadTexture(""); + const rendering::VkTexture* whiteTex = charRenderer->loadTexture(""); for (const auto& candidate : capeCandidates) { - GLuint tex = charRenderer->loadTexture(candidate); - if (tex != 0 && tex != whiteTex) { + rendering::VkTexture* tex = charRenderer->loadTexture(candidate); + if (tex && tex != whiteTex) { npcCapeTextureId = tex; break; } @@ -4531,7 +4532,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x } LOG_INFO("NPC geosets for instance ", instanceId, ": [", geosetList, "]"); charRenderer->setActiveGeosets(instanceId, activeGeosets); - if (geosetCape != 0 && npcCapeTextureId != 0) { + if (geosetCape != 0 && npcCapeTextureId) { charRenderer->setGroupTextureOverride(instanceId, 15, npcCapeTextureId); if (const auto* md = charRenderer->getModelData(modelId)) { for (size_t ti = 0; ti < md->textures.size(); ti++) { @@ -5168,7 +5169,7 @@ void Application::spawnOnlinePlayer(uint64_t guid, } // Composite base skin + face + underwear overlays - GLuint compositeTex = 0; + rendering::VkTexture* compositeTex = nullptr; { std::vector layers; layers.push_back(bodySkinPath); @@ -5182,22 +5183,22 @@ void Application::spawnOnlinePlayer(uint64_t guid, } } - GLuint hairTex = 0; + rendering::VkTexture* hairTex = nullptr; if (!hairTexturePath.empty()) { hairTex = charRenderer->loadTexture(hairTexturePath); } - GLuint underwearTex = 0; + rendering::VkTexture* underwearTex = nullptr; if (!underwearPaths.empty()) underwearTex = charRenderer->loadTexture(underwearPaths[0]); else underwearTex = charRenderer->loadTexture(pelvisPath); const PlayerTextureSlots& slots = playerTextureSlotsByModelId_[modelId]; - if (slots.skin >= 0 && compositeTex != 0) { + if (slots.skin >= 0 && compositeTex) { charRenderer->setTextureSlotOverride(instanceId, static_cast(slots.skin), compositeTex); } - if (slots.hair >= 0 && hairTex != 0) { + if (slots.hair >= 0 && hairTex) { charRenderer->setTextureSlotOverride(instanceId, static_cast(slots.hair), hairTex); } - if (slots.underwear >= 0 && underwearTex != 0) { + if (slots.underwear >= 0 && underwearTex) { charRenderer->setTextureSlotOverride(instanceId, static_cast(slots.underwear), underwearTex); } @@ -5409,8 +5410,8 @@ void Application::setOnlinePlayerEquipment(uint64_t guid, const PlayerTextureSlots& slots = slotsIt->second; if (slots.skin < 0) return; - GLuint newTex = charRenderer->compositeWithRegions(st.bodySkinPath, st.underwearPaths, regionLayers); - if (newTex != 0) { + rendering::VkTexture* newTex = charRenderer->compositeWithRegions(st.bodySkinPath, st.underwearPaths, regionLayers); + if (newTex) { charRenderer->setTextureSlotOverride(st.instanceId, static_cast(slots.skin), newTex); } } @@ -6038,8 +6039,8 @@ void Application::processPendingMount() { texPath = modelDir + dispData.skin3 + ".blp"; } if (!texPath.empty()) { - GLuint skinTex = charRenderer->loadTexture(texPath); - if (skinTex != 0) { + rendering::VkTexture* skinTex = charRenderer->loadTexture(texPath); + if (skinTex) { charRenderer->setModelTexture(modelId, static_cast(ti), skinTex); LOG_INFO(" Applied skin texture slot ", ti, ": ", texPath); replaced++; @@ -6062,8 +6063,8 @@ void Application::processPendingMount() { texPath = modelDir + dispData.skin2 + ".blp"; } if (!texPath.empty()) { - GLuint skinTex = charRenderer->loadTexture(texPath); - if (skinTex != 0) { + rendering::VkTexture* skinTex = charRenderer->loadTexture(texPath); + if (skinTex) { charRenderer->setModelTexture(modelId, static_cast(ti), skinTex); LOG_INFO(" Forced skin on empty hardcoded slot ", ti, ": ", texPath); replaced++; @@ -6077,8 +6078,8 @@ void Application::processPendingMount() { if (replaced == 0) { for (size_t ti = 0; ti < md->textures.size(); ti++) { if (!md->textures[ti].filename.empty()) { - GLuint texId = charRenderer->loadTexture(md->textures[ti].filename); - if (texId != 0) { + rendering::VkTexture* texId = charRenderer->loadTexture(md->textures[ti].filename); + if (texId) { charRenderer->setModelTexture(modelId, static_cast(ti), texId); LOG_INFO(" Used model embedded texture slot ", ti, ": ", md->textures[ti].filename); replaced++; @@ -6100,8 +6101,8 @@ void Application::processPendingMount() { nullptr }; for (const char** p = gryphonSkins; *p; ++p) { - GLuint texId = charRenderer->loadTexture(*p); - if (texId != 0) { + rendering::VkTexture* texId = charRenderer->loadTexture(*p); + if (texId) { charRenderer->setModelTexture(modelId, 0, texId); LOG_INFO(" Forced gryphon skin fallback: ", *p); replaced++; @@ -6115,8 +6116,8 @@ void Application::processPendingMount() { nullptr }; for (const char** p = wyvernSkins; *p; ++p) { - GLuint texId = charRenderer->loadTexture(*p); - if (texId != 0) { + rendering::VkTexture* texId = charRenderer->loadTexture(*p); + if (texId) { charRenderer->setModelTexture(modelId, 0, texId); LOG_INFO(" Forced wyvern skin fallback: ", *p); replaced++; diff --git a/src/core/window.cpp b/src/core/window.cpp index f540b2e1..2c91ab3c 100644 --- a/src/core/window.cpp +++ b/src/core/window.cpp @@ -1,6 +1,7 @@ #include "core/window.hpp" #include "core/logger.hpp" -#include +#include "rendering/vk_context.hpp" +#include namespace wowee { namespace core { @@ -28,18 +29,8 @@ bool Window::initialize() { return false; } - // Set OpenGL attributes - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); - SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); - SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); - SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1); - SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 4); - - // Create window - Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN; + // Create Vulkan window (no GL attributes needed) + Uint32 flags = SDL_WINDOW_VULKAN | SDL_WINDOW_SHOWN; if (config.fullscreen) { flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; } @@ -61,49 +52,21 @@ bool Window::initialize() { return false; } - // Create OpenGL context - glContext = SDL_GL_CreateContext(window); - if (!glContext) { - LOG_ERROR("Failed to create OpenGL context: ", SDL_GetError()); + // Initialize Vulkan context + vkContext = std::make_unique(); + if (!vkContext->initialize(window)) { + LOG_ERROR("Failed to initialize Vulkan context"); return false; } - // Set VSync - if (SDL_GL_SetSwapInterval(config.vsync ? 1 : 0) != 0) { - LOG_WARNING("Failed to set VSync: ", SDL_GetError()); - } - vsync = config.vsync; - - // Initialize GLEW - glewExperimental = GL_TRUE; - GLenum glewError = glewInit(); - if (glewError != GLEW_OK) { - LOG_ERROR("Failed to initialize GLEW: ", glewGetErrorString(glewError)); - return false; - } - - // Log OpenGL info - LOG_INFO("OpenGL Version: ", glGetString(GL_VERSION)); - LOG_INFO("GLSL Version: ", glGetString(GL_SHADING_LANGUAGE_VERSION)); - LOG_INFO("Renderer: ", glGetString(GL_RENDERER)); - LOG_INFO("Vendor: ", glGetString(GL_VENDOR)); - - // Set up OpenGL defaults - glEnable(GL_MULTISAMPLE); - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_LESS); - glEnable(GL_CULL_FACE); - glCullFace(GL_BACK); - glFrontFace(GL_CCW); - - LOG_INFO("Window initialized successfully"); + LOG_INFO("Window initialized successfully (Vulkan)"); return true; } void Window::shutdown() { - if (glContext) { - SDL_GL_DeleteContext(glContext); - glContext = nullptr; + if (vkContext) { + vkContext->shutdown(); + vkContext.reset(); } if (window) { @@ -115,15 +78,9 @@ void Window::shutdown() { LOG_INFO("Window shutdown complete"); } -void Window::swapBuffers() { - SDL_GL_SwapWindow(window); -} - void Window::pollEvents() { SDL_Event event; while (SDL_PollEvent(&event)) { - // ImGui will handle events in UI manager - // For now, just handle quit if (event.type == SDL_QUIT) { shouldCloseFlag = true; } @@ -131,7 +88,9 @@ void Window::pollEvents() { if (event.window.event == SDL_WINDOWEVENT_RESIZED) { width = event.window.data1; height = event.window.data2; - glViewport(0, 0, width, height); + if (vkContext) { + vkContext->recreateSwapchain(width, height); + } LOG_DEBUG("Window resized to ", width, "x", height); } } @@ -160,15 +119,16 @@ void Window::setFullscreen(bool enable) { width = windowedWidth; height = windowedHeight; } - glViewport(0, 0, width, height); + if (vkContext) { + vkContext->recreateSwapchain(width, height); + } } -void Window::setVsync(bool enable) { - if (SDL_GL_SetSwapInterval(enable ? 1 : 0) != 0) { - LOG_WARNING("Failed to set VSync: ", SDL_GetError()); - return; - } +void Window::setVsync([[maybe_unused]] bool enable) { + // VSync in Vulkan is controlled by present mode (set at swapchain creation) + // For now, store the preference — applied on next swapchain recreation vsync = enable; + LOG_INFO("VSync preference set to ", enable ? "on" : "off", " (applied on swapchain recreation)"); } void Window::applyResolution(int w, int h) { @@ -184,7 +144,9 @@ void Window::applyResolution(int w, int h) { height = h; windowedWidth = w; windowedHeight = h; - glViewport(0, 0, width, height); + if (vkContext) { + vkContext->recreateSwapchain(width, height); + } } } // namespace core diff --git a/src/rendering/celestial.cpp b/src/rendering/celestial.cpp index 3629b284..91b64fcd 100644 --- a/src/rendering/celestial.cpp +++ b/src/rendering/celestial.cpp @@ -1,10 +1,13 @@ #include "rendering/celestial.hpp" -#include "rendering/shader.hpp" -#include "rendering/camera.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_frame_data.hpp" +#include "rendering/vk_utils.hpp" #include "core/logger.hpp" -#include #include #include +#include namespace wowee { namespace rendering { @@ -15,564 +18,412 @@ Celestial::~Celestial() { shutdown(); } -bool Celestial::initialize() { - LOG_INFO("Initializing celestial renderer"); +bool Celestial::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { + LOG_INFO("Initializing celestial renderer (Vulkan)"); - // Create celestial shader - celestialShader = std::make_unique(); + vkCtx_ = ctx; + VkDevice device = vkCtx_->getDevice(); - // Vertex shader - billboard facing camera (sky dome locked) - const char* vertexShaderSource = R"( - #version 330 core - layout (location = 0) in vec3 aPos; - layout (location = 1) in vec2 aTexCoord; - - uniform mat4 model; - uniform mat4 view; - uniform mat4 projection; - - out vec2 TexCoord; - - void main() { - TexCoord = aTexCoord; - - // Sky object: remove translation, keep rotation (skybox technique) - mat4 viewNoTranslation = mat4(mat3(view)); - - gl_Position = projection * viewNoTranslation * model * vec4(aPos, 1.0); - } - )"; - - // Fragment shader - disc with glow and moon phase support - const char* fragmentShaderSource = R"( - #version 330 core - in vec2 TexCoord; - - uniform vec3 celestialColor; - uniform float intensity; - uniform float moonPhase; // 0.0 = new moon, 0.5 = full moon, 1.0 = new moon - uniform float uAnimTime; - - out vec4 FragColor; - - float hash(vec2 p) { - return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); - } - - float noise(vec2 p) { - vec2 i = floor(p); - vec2 f = fract(p); - f = f * f * (3.0 - 2.0 * f); - float a = hash(i + vec2(0.0, 0.0)); - float b = hash(i + vec2(1.0, 0.0)); - float c = hash(i + vec2(0.0, 1.0)); - float d = hash(i + vec2(1.0, 1.0)); - return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); - } - - void main() { - // Create circular disc - vec2 center = vec2(0.5, 0.5); - float dist = distance(TexCoord, center); - - // Core disc + glow with explicit radial mask to avoid square billboard artifact. - float disc = smoothstep(0.50, 0.38, dist); - float glow = smoothstep(0.64, 0.00, dist) * 0.24; - float radialMask = 1.0 - smoothstep(0.58, 0.70, dist); - - float alpha = (disc + glow) * radialMask * intensity; - vec3 outColor = celestialColor; - - // Very faint animated haze over sun disc/glow (no effect for moon). - if (intensity > 0.5) { - vec2 uv = (TexCoord - vec2(0.5)) * 3.0; - // Slow flow field for atmospheric-like turbulence drift. - vec2 flow = vec2( - noise(uv * 0.9 + vec2(uAnimTime * 0.012, -uAnimTime * 0.009)), - noise(uv * 0.9 + vec2(-uAnimTime * 0.010, uAnimTime * 0.011)) - ) - vec2(0.5); - vec2 warped = uv + flow * 0.42; - float n1 = noise(warped * 1.7 + vec2(uAnimTime * 0.016, -uAnimTime * 0.013)); - float n2 = noise(warped * 3.0 + vec2(-uAnimTime * 0.021, uAnimTime * 0.017)); - float haze = mix(n1, n2, 0.35); - float hazeMask = clamp(disc * 0.75 + glow * 0.28, 0.0, 1.0); - float hazeMix = hazeMask * 0.55; - float lumaMod = mix(1.0, 0.93 + haze * 0.10, hazeMix); - outColor *= lumaMod; - alpha *= mix(1.0, 0.94 + haze * 0.06, hazeMix); - } - - // Apply moon phase shadow (only for moon, indicated by low intensity) - if (intensity < 0.5) { // Moon has lower intensity than sun - // Calculate phase position (-1 to 1, where 0 is center) - float phasePos = (moonPhase - 0.5) * 2.0; - - // Distance from phase terminator line - float x = (TexCoord.x - 0.5) * 2.0; // -1 to 1 - - // Create shadow using smoothstep - float shadow = 1.0; - - if (moonPhase < 0.5) { - // Waning (right to left shadow) - shadow = smoothstep(phasePos - 0.1, phasePos + 0.1, x); - } else { - // Waxing (left to right shadow) - shadow = smoothstep(phasePos - 0.1, phasePos + 0.1, -x); - } - - // Apply elliptical terminator for 3D effect - float y = (TexCoord.y - 0.5) * 2.0; - float ellipse = sqrt(max(0.0, 1.0 - y * y)); - float terminatorX = phasePos / ellipse; - - if (moonPhase < 0.5) { - shadow = smoothstep(terminatorX - 0.15, terminatorX + 0.15, x); - } else { - shadow = smoothstep(terminatorX - 0.15, terminatorX + 0.15, -x); - } - - // Darken shadowed area (not completely black, slight glow remains) - alpha *= mix(0.05, 1.0, shadow); - } - - if (alpha < 0.01) { - discard; - } - - FragColor = vec4(outColor, alpha); - } - )"; - - if (!celestialShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) { - LOG_ERROR("Failed to create celestial shader"); + // ------------------------------------------------------------------ shaders + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/celestial.vert.spv")) { + LOG_ERROR("Failed to load celestial vertex shader"); return false; } - // Create billboard quad - createCelestialQuad(); + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/celestial.frag.spv")) { + LOG_ERROR("Failed to load celestial fragment shader"); + return false; + } + + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + // ------------------------------------------------------------------ push constants + // Layout: mat4(64) + vec4(16) + float*3(12) + pad(4) = 96 bytes + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + pushRange.offset = 0; + pushRange.size = sizeof(CelestialPush); // 96 bytes + + // ------------------------------------------------------------------ pipeline layout + pipelineLayout_ = createPipelineLayout(device, {perFrameLayout}, {pushRange}); + if (pipelineLayout_ == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create celestial pipeline layout"); + return false; + } + + // ------------------------------------------------------------------ vertex input + // Vertex: vec3 pos + vec2 texCoord, stride = 20 bytes + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 5 * sizeof(float); // 20 bytes + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + VkVertexInputAttributeDescription posAttr{}; + posAttr.location = 0; + posAttr.binding = 0; + posAttr.format = VK_FORMAT_R32G32B32_SFLOAT; + posAttr.offset = 0; + + VkVertexInputAttributeDescription uvAttr{}; + uvAttr.location = 1; + uvAttr.binding = 0; + uvAttr.format = VK_FORMAT_R32G32_SFLOAT; + uvAttr.offset = 3 * sizeof(float); + + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + // ------------------------------------------------------------------ pipeline + pipeline_ = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, {posAttr, uvAttr}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) // test on, write off (sky layer) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setLayout(pipelineLayout_) + .setRenderPass(vkCtx_->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (pipeline_ == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create celestial pipeline"); + return false; + } + + // ------------------------------------------------------------------ geometry + createQuad(); LOG_INFO("Celestial renderer initialized"); return true; } void Celestial::shutdown() { - destroyCelestialQuad(); - celestialShader.reset(); + destroyQuad(); + + if (vkCtx_) { + VkDevice device = vkCtx_->getDevice(); + if (pipeline_ != VK_NULL_HANDLE) { + vkDestroyPipeline(device, pipeline_, nullptr); + pipeline_ = VK_NULL_HANDLE; + } + if (pipelineLayout_ != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); + pipelineLayout_ = VK_NULL_HANDLE; + } + } + + vkCtx_ = nullptr; } -void Celestial::render(const Camera& camera, float timeOfDay, - const glm::vec3* sunDir, const glm::vec3* sunColor, float gameTime) { - if (!renderingEnabled || vao == 0 || !celestialShader) { +// --------------------------------------------------------------------------- +// Public render entry point +// --------------------------------------------------------------------------- + +void Celestial::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, + float timeOfDay, + const glm::vec3* sunDir, const glm::vec3* sunColor, + float gameTime) { + if (!renderingEnabled_ || pipeline_ == VK_NULL_HANDLE) { return; } - // Update moon phases from game time if available (deterministic) + // Update moon phases from server game time if provided if (gameTime >= 0.0f) { updatePhasesFromGameTime(gameTime); } - // Enable additive blending for celestial glow (brighter against sky) - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Additive blending for brightness + // Bind pipeline and per-frame descriptor set once — reused for all draws + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, + 0, 1, &perFrameSet, 0, nullptr); - // Disable depth testing entirely - celestial bodies render "on" the sky - glDisable(GL_DEPTH_TEST); - glDepthMask(GL_FALSE); - - // Disable culling - billboards can face either way - glDisable(GL_CULL_FACE); - - // Render sun with alpha blending (avoids additive white clipping). - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - renderSun(camera, timeOfDay, sunDir, sunColor); - - // Render moons additively for glow. - glBlendFunc(GL_SRC_ALPHA, GL_ONE); - renderMoon(camera, timeOfDay); // White Lady (primary moon) + // Bind the shared quad buffers + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer_, &offset); + vkCmdBindIndexBuffer(cmd, indexBuffer_, 0, VK_INDEX_TYPE_UINT32); + // Draw sun, then moon(s) — each call pushes different constants + renderSun(cmd, perFrameSet, timeOfDay, sunDir, sunColor); + renderMoon(cmd, perFrameSet, timeOfDay); if (dualMoonMode_) { - renderBlueChild(camera, timeOfDay); // Blue Child (secondary moon) + renderBlueChild(cmd, perFrameSet, timeOfDay); } - - // Restore state - glEnable(GL_DEPTH_TEST); - glDepthMask(GL_TRUE); - glDisable(GL_BLEND); - glEnable(GL_CULL_FACE); } -void Celestial::renderSun(const Camera& camera, float timeOfDay, - const glm::vec3* sunDir, const glm::vec3* sunColor) { - // Sun visible from 5:00 to 19:00 +// --------------------------------------------------------------------------- +// Private per-body render helpers +// --------------------------------------------------------------------------- + +void Celestial::renderSun(VkCommandBuffer cmd, VkDescriptorSet /*perFrameSet*/, + float timeOfDay, + const glm::vec3* sunDir, const glm::vec3* sunColor) { + // Sun visible 5:00–19:00 if (timeOfDay < 5.0f || timeOfDay >= 19.0f) { return; } - celestialShader->use(); - - // Prefer opposite of light-ray direction (sun->world), but guard against - // profile/convention mismatches that can place the sun below the horizon. + // Resolve sun direction — prefer opposite of incoming light ray, clamp below horizon glm::vec3 lightDir = sunDir ? glm::normalize(*sunDir) : glm::vec3(0.0f, 0.0f, -1.0f); glm::vec3 dir = -lightDir; if (dir.z < 0.0f) { dir = lightDir; } - // Place sun on sky sphere at fixed distance const float sunDistance = 800.0f; glm::vec3 sunPos = dir * sunDistance; - // Create model matrix glm::mat4 model = glm::mat4(1.0f); model = glm::translate(model, sunPos); - model = glm::scale(model, glm::vec3(95.0f, 95.0f, 1.0f)); // Match WotLK-like apparent size + model = glm::scale(model, glm::vec3(95.0f, 95.0f, 1.0f)); - // Set uniforms - glm::mat4 view = camera.getViewMatrix(); - glm::mat4 projection = camera.getProjectionMatrix(); - - celestialShader->setUniform("model", model); - celestialShader->setUniform("view", view); - celestialShader->setUniform("projection", projection); - - // Sun color and intensity (use lighting color if provided) glm::vec3 color = sunColor ? *sunColor : getSunColor(timeOfDay); - // Force strong warm/yellow tint; avoid white blowout. const glm::vec3 warmSun(1.0f, 0.88f, 0.55f); color = glm::mix(color, warmSun, 0.52f); float intensity = getSunIntensity(timeOfDay) * 0.92f; - celestialShader->setUniform("celestialColor", color); - celestialShader->setUniform("intensity", intensity); - celestialShader->setUniform("moonPhase", 0.5f); // Sun doesn't use this, but shader expects it - celestialShader->setUniform("uAnimTime", sunHazeTimer_); + CelestialPush push{}; + push.model = model; + push.celestialColor = glm::vec4(color, 1.0f); + push.intensity = intensity; + push.moonPhase = 0.5f; // unused for sun + push.animTime = sunHazeTimer_; - // Render quad - glBindVertexArray(vao); - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr); - glBindVertexArray(0); + vkCmdPushConstants(cmd, pipelineLayout_, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof(push), &push); + + vkCmdDrawIndexed(cmd, 6, 1, 0, 0, 0); } -void Celestial::renderMoon(const Camera& camera, float timeOfDay) { - // Moon visible from 19:00 to 5:00 (night) +void Celestial::renderMoon(VkCommandBuffer cmd, VkDescriptorSet /*perFrameSet*/, + float timeOfDay) { + // Moon (White Lady) visible 19:00–5:00 if (timeOfDay >= 5.0f && timeOfDay < 19.0f) { return; } - celestialShader->use(); - - // Get moon position glm::vec3 moonPos = getMoonPosition(timeOfDay); - // Create model matrix glm::mat4 model = glm::mat4(1.0f); model = glm::translate(model, moonPos); - model = glm::scale(model, glm::vec3(40.0f, 40.0f, 1.0f)); // 40 unit diameter (smaller than sun) + model = glm::scale(model, glm::vec3(40.0f, 40.0f, 1.0f)); - // Set uniforms - glm::mat4 view = camera.getViewMatrix(); - glm::mat4 projection = camera.getProjectionMatrix(); - - celestialShader->setUniform("model", model); - celestialShader->setUniform("view", view); - celestialShader->setUniform("projection", projection); - - // Moon color (pale blue-white) and intensity glm::vec3 color = glm::vec3(0.8f, 0.85f, 1.0f); - // Fade in/out at transitions float intensity = 1.0f; if (timeOfDay >= 19.0f && timeOfDay < 21.0f) { - // Fade in (19:00-21:00) - intensity = (timeOfDay - 19.0f) / 2.0f; - } - else if (timeOfDay >= 3.0f && timeOfDay < 5.0f) { - // Fade out (3:00-5:00) - intensity = 1.0f - (timeOfDay - 3.0f) / 2.0f; + intensity = (timeOfDay - 19.0f) / 2.0f; // Fade in + } else if (timeOfDay >= 3.0f && timeOfDay < 5.0f) { + intensity = 1.0f - (timeOfDay - 3.0f) / 2.0f; // Fade out } - celestialShader->setUniform("celestialColor", color); - celestialShader->setUniform("intensity", intensity); - celestialShader->setUniform("moonPhase", whiteLadyPhase_); - celestialShader->setUniform("uAnimTime", sunHazeTimer_); + CelestialPush push{}; + push.model = model; + push.celestialColor = glm::vec4(color, 1.0f); + push.intensity = intensity; + push.moonPhase = whiteLadyPhase_; + push.animTime = sunHazeTimer_; - // Render quad - glBindVertexArray(vao); - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr); - glBindVertexArray(0); + vkCmdPushConstants(cmd, pipelineLayout_, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof(push), &push); + + vkCmdDrawIndexed(cmd, 6, 1, 0, 0, 0); } -void Celestial::renderBlueChild(const Camera& camera, float timeOfDay) { - // Blue Child visible from 19:00 to 5:00 (night, same as White Lady) +void Celestial::renderBlueChild(VkCommandBuffer cmd, VkDescriptorSet /*perFrameSet*/, + float timeOfDay) { + // Blue Child visible 19:00–5:00 if (timeOfDay >= 5.0f && timeOfDay < 19.0f) { return; } - celestialShader->use(); - - // Get moon position (offset slightly from White Lady) + // Offset slightly from White Lady glm::vec3 moonPos = getMoonPosition(timeOfDay); - // Offset Blue Child to the right and slightly lower - moonPos.x += 80.0f; // Right offset - moonPos.z -= 40.0f; // Slightly lower + moonPos.x += 80.0f; + moonPos.z -= 40.0f; - // Create model matrix (smaller than White Lady) glm::mat4 model = glm::mat4(1.0f); model = glm::translate(model, moonPos); - model = glm::scale(model, glm::vec3(30.0f, 30.0f, 1.0f)); // 30 unit diameter (smaller) + model = glm::scale(model, glm::vec3(30.0f, 30.0f, 1.0f)); - // Set uniforms - glm::mat4 view = camera.getViewMatrix(); - glm::mat4 projection = camera.getProjectionMatrix(); - - celestialShader->setUniform("model", model); - celestialShader->setUniform("view", view); - celestialShader->setUniform("projection", projection); - - // Blue Child color (pale blue tint) glm::vec3 color = glm::vec3(0.7f, 0.8f, 1.0f); - // Fade in/out at transitions (same as White Lady) float intensity = 1.0f; if (timeOfDay >= 19.0f && timeOfDay < 21.0f) { - // Fade in (19:00-21:00) intensity = (timeOfDay - 19.0f) / 2.0f; - } - else if (timeOfDay >= 3.0f && timeOfDay < 5.0f) { - // Fade out (3:00-5:00) + } else if (timeOfDay >= 3.0f && timeOfDay < 5.0f) { intensity = 1.0f - (timeOfDay - 3.0f) / 2.0f; } + intensity *= 0.7f; // Blue Child is dimmer - // Blue Child is dimmer than White Lady - intensity *= 0.7f; + CelestialPush push{}; + push.model = model; + push.celestialColor = glm::vec4(color, 1.0f); + push.intensity = intensity; + push.moonPhase = blueChildPhase_; + push.animTime = sunHazeTimer_; - celestialShader->setUniform("celestialColor", color); - celestialShader->setUniform("intensity", intensity); - celestialShader->setUniform("moonPhase", blueChildPhase_); - celestialShader->setUniform("uAnimTime", sunHazeTimer_); + vkCmdPushConstants(cmd, pipelineLayout_, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof(push), &push); - // Render quad - glBindVertexArray(vao); - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr); - glBindVertexArray(0); + vkCmdDrawIndexed(cmd, 6, 1, 0, 0, 0); } +// --------------------------------------------------------------------------- +// Position / colour query helpers (identical logic to GL version) +// --------------------------------------------------------------------------- + glm::vec3 Celestial::getSunPosition(float timeOfDay) const { - // Sun rises at 6:00, peaks at 12:00, sets at 18:00 float angle = calculateCelestialAngle(timeOfDay, 6.0f, 18.0f); - - const float radius = 800.0f; // Horizontal distance - const float height = 600.0f; // Maximum height at zenith - - // Arc across sky (angle 0→π maps to sunrise→noon→sunset) - // Z is vertical (matches skybox: Altitude = aPos.z) - // At angle=0: x=radius, z=0 (east horizon) - // At angle=π/2: x=0, z=height (zenith, directly overhead) - // At angle=π: x=-radius, z=0 (west horizon) - float x = radius * std::cos(angle); // Horizontal position (E→W) - float y = 0.0f; // Y is north-south (keep at 0) - float z = height * std::sin(angle); // Vertical position (Z is UP, matches skybox) - - return glm::vec3(x, y, z); + const float radius = 800.0f; + const float height = 600.0f; + float x = radius * std::cos(angle); + float z = height * std::sin(angle); + return glm::vec3(x, 0.0f, z); } glm::vec3 Celestial::getMoonPosition(float timeOfDay) const { - // Moon rises at 18:00, peaks at 0:00 (24:00), sets at 6:00 - // Adjust time for moon (opposite to sun) float moonTime = timeOfDay + 12.0f; if (moonTime >= 24.0f) moonTime -= 24.0f; - float angle = calculateCelestialAngle(moonTime, 6.0f, 18.0f); - const float radius = 800.0f; const float height = 600.0f; - - // Same arc formula as sun (Z is vertical, matches skybox) float x = radius * std::cos(angle); - float y = 0.0f; float z = height * std::sin(angle); - - return glm::vec3(x, y, z); + return glm::vec3(x, 0.0f, z); } glm::vec3 Celestial::getSunColor(float timeOfDay) const { - // Sunrise/sunset: orange/red - // Midday: bright yellow-white - if (timeOfDay >= 5.0f && timeOfDay < 7.0f) { - // Sunrise: orange - return glm::vec3(1.0f, 0.6f, 0.2f); - } - else if (timeOfDay >= 7.0f && timeOfDay < 9.0f) { - // Morning: blend to yellow + return glm::vec3(1.0f, 0.6f, 0.2f); // Sunrise orange + } else if (timeOfDay >= 7.0f && timeOfDay < 9.0f) { float t = (timeOfDay - 7.0f) / 2.0f; - glm::vec3 orange = glm::vec3(1.0f, 0.6f, 0.2f); - glm::vec3 yellow = glm::vec3(1.0f, 1.0f, 0.9f); - return glm::mix(orange, yellow, t); - } - else if (timeOfDay >= 9.0f && timeOfDay < 16.0f) { - // Day: bright yellow-white - return glm::vec3(1.0f, 1.0f, 0.9f); - } - else if (timeOfDay >= 16.0f && timeOfDay < 18.0f) { - // Evening: blend to orange + return glm::mix(glm::vec3(1.0f, 0.6f, 0.2f), glm::vec3(1.0f, 1.0f, 0.9f), t); + } else if (timeOfDay >= 9.0f && timeOfDay < 16.0f) { + return glm::vec3(1.0f, 1.0f, 0.9f); // Day yellow-white + } else if (timeOfDay >= 16.0f && timeOfDay < 18.0f) { float t = (timeOfDay - 16.0f) / 2.0f; - glm::vec3 yellow = glm::vec3(1.0f, 1.0f, 0.9f); - glm::vec3 orange = glm::vec3(1.0f, 0.5f, 0.1f); - return glm::mix(yellow, orange, t); - } - else { - // Sunset: deep orange/red - return glm::vec3(1.0f, 0.4f, 0.1f); + return glm::mix(glm::vec3(1.0f, 1.0f, 0.9f), glm::vec3(1.0f, 0.5f, 0.1f), t); + } else { + return glm::vec3(1.0f, 0.4f, 0.1f); // Sunset orange } } float Celestial::getSunIntensity(float timeOfDay) const { - // Fade in at sunrise (5:00-6:00) if (timeOfDay >= 5.0f && timeOfDay < 6.0f) { - return (timeOfDay - 5.0f); // 0 to 1 - } - // Full intensity during day (6:00-18:00) - else if (timeOfDay >= 6.0f && timeOfDay < 18.0f) { - return 1.0f; - } - // Fade out at sunset (18:00-19:00) - else if (timeOfDay >= 18.0f && timeOfDay < 19.0f) { - return 1.0f - (timeOfDay - 18.0f); // 1 to 0 - } - else { + return timeOfDay - 5.0f; // Fade in + } else if (timeOfDay >= 6.0f && timeOfDay < 18.0f) { + return 1.0f; // Full day + } else if (timeOfDay >= 18.0f && timeOfDay < 19.0f) { + return 1.0f - (timeOfDay - 18.0f); // Fade out + } else { return 0.0f; } } float Celestial::calculateCelestialAngle(float timeOfDay, float riseTime, float setTime) const { - // Map time to angle (0 to PI) - // riseTime: 0 radians (horizon east) - // (riseTime + setTime) / 2: PI/2 radians (zenith) - // setTime: PI radians (horizon west) - float duration = setTime - riseTime; - float elapsed = timeOfDay - riseTime; - - // Normalize to 0-1 + float elapsed = timeOfDay - riseTime; float t = elapsed / duration; - - // Map to 0 to PI (arc from east to west) - return t * M_PI; + return t * static_cast(M_PI); } -void Celestial::createCelestialQuad() { - // Simple quad centered at origin - float vertices[] = { - // Position // TexCoord - -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, // Top-left - 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, // Top-right - 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, // Bottom-right - -0.5f, -0.5f, 0.0f, 0.0f, 0.0f // Bottom-left - }; - - uint32_t indices[] = { - 0, 1, 2, // First triangle - 0, 2, 3 // Second triangle - }; - - // Create OpenGL buffers - glGenVertexArrays(1, &vao); - glGenBuffers(1, &vbo); - glGenBuffers(1, &ebo); - - glBindVertexArray(vao); - - // Upload vertex data - glBindBuffer(GL_ARRAY_BUFFER, vbo); - glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - - // Upload index data - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); - - // Set vertex attributes - // Position - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - - // Texture coordinates - glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(1); - - glBindVertexArray(0); -} - -void Celestial::destroyCelestialQuad() { - if (vao != 0) { - glDeleteVertexArrays(1, &vao); - vao = 0; - } - if (vbo != 0) { - glDeleteBuffers(1, &vbo); - vbo = 0; - } - if (ebo != 0) { - glDeleteBuffers(1, &ebo); - ebo = 0; - } -} +// --------------------------------------------------------------------------- +// Moon phase helpers +// --------------------------------------------------------------------------- void Celestial::update(float deltaTime) { sunHazeTimer_ += deltaTime; - if (!moonPhaseCycling) { + if (!moonPhaseCycling_) { return; } - // Update moon phase timer - moonPhaseTimer += deltaTime; + moonPhaseTimer_ += deltaTime; + whiteLadyPhase_ = std::fmod(moonPhaseTimer_ / MOON_CYCLE_DURATION, 1.0f); - // White Lady completes full cycle in MOON_CYCLE_DURATION seconds - whiteLadyPhase_ = std::fmod(moonPhaseTimer / MOON_CYCLE_DURATION, 1.0f); - - // Blue Child has a different cycle rate (slightly faster, 3.5 minutes) - constexpr float BLUE_CHILD_CYCLE = 210.0f; - blueChildPhase_ = std::fmod(moonPhaseTimer / BLUE_CHILD_CYCLE, 1.0f); + constexpr float BLUE_CHILD_CYCLE = 210.0f; // Slightly faster: 3.5 minutes + blueChildPhase_ = std::fmod(moonPhaseTimer_ / BLUE_CHILD_CYCLE, 1.0f); } void Celestial::setMoonPhase(float phase) { - // Set White Lady phase (primary moon) whiteLadyPhase_ = glm::clamp(phase, 0.0f, 1.0f); - - // Update timer to match White Lady phase - moonPhaseTimer = whiteLadyPhase_ * MOON_CYCLE_DURATION; + moonPhaseTimer_ = whiteLadyPhase_ * MOON_CYCLE_DURATION; } void Celestial::setBlueChildPhase(float phase) { - // Set Blue Child phase (secondary moon) blueChildPhase_ = glm::clamp(phase, 0.0f, 1.0f); } float Celestial::computePhaseFromGameTime(float gameTime, float cycleDays) const { - // WoW game time: 1 game day = 24 real minutes = 1440 seconds - constexpr float SECONDS_PER_GAME_DAY = 1440.0f; - - // Convert game time to game days + constexpr float SECONDS_PER_GAME_DAY = 1440.0f; // 24 real minutes float gameDays = gameTime / SECONDS_PER_GAME_DAY; - - // Compute phase as fraction of lunar cycle (0.0-1.0) - float phase = std::fmod(gameDays / cycleDays, 1.0f); - - // Ensure positive (fmod can return negative for negative input) - if (phase < 0.0f) { - phase += 1.0f; - } - + float phase = std::fmod(gameDays / cycleDays, 1.0f); + if (phase < 0.0f) phase += 1.0f; return phase; } void Celestial::updatePhasesFromGameTime(float gameTime) { - // Compute deterministic phases from server game time whiteLadyPhase_ = computePhaseFromGameTime(gameTime, WHITE_LADY_CYCLE_DAYS); blueChildPhase_ = computePhaseFromGameTime(gameTime, BLUE_CHILD_CYCLE_DAYS); } +// --------------------------------------------------------------------------- +// GPU buffer management +// --------------------------------------------------------------------------- + +void Celestial::createQuad() { + // Billboard quad centred at origin, vertices: pos(vec3) + uv(vec2) + float vertices[] = { + // Position TexCoord + -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, // Top-left + 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, // Top-right + 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, // Bottom-right + -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, // Bottom-left + }; + + uint32_t indices[] = { 0, 1, 2, 0, 2, 3 }; + + AllocatedBuffer vbuf = uploadBuffer(*vkCtx_, + vertices, sizeof(vertices), + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + vertexBuffer_ = vbuf.buffer; + vertexAlloc_ = vbuf.allocation; + + AllocatedBuffer ibuf = uploadBuffer(*vkCtx_, + indices, sizeof(indices), + VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + indexBuffer_ = ibuf.buffer; + indexAlloc_ = ibuf.allocation; +} + +void Celestial::destroyQuad() { + if (!vkCtx_) return; + + VmaAllocator allocator = vkCtx_->getAllocator(); + + if (vertexBuffer_ != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, vertexBuffer_, vertexAlloc_); + vertexBuffer_ = VK_NULL_HANDLE; + vertexAlloc_ = VK_NULL_HANDLE; + } + if (indexBuffer_ != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, indexBuffer_, indexAlloc_); + indexBuffer_ = VK_NULL_HANDLE; + indexAlloc_ = VK_NULL_HANDLE; + } +} + } // namespace rendering } // namespace wowee diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index 663a9324..9ceec954 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -1,12 +1,15 @@ #include "rendering/character_preview.hpp" #include "rendering/character_renderer.hpp" +#include "rendering/vk_texture.hpp" +#include "rendering/vk_context.hpp" #include "rendering/camera.hpp" +#include "rendering/renderer.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/m2_loader.hpp" #include "pipeline/dbc_loader.hpp" #include "pipeline/dbc_layout.hpp" #include "core/logger.hpp" -#include +#include "core/application.hpp" #include #include #include @@ -24,11 +27,13 @@ bool CharacterPreview::initialize(pipeline::AssetManager* am) { assetManager_ = am; charRenderer_ = std::make_unique(); - if (!charRenderer_->initialize()) { + auto* appRenderer = core::Application::getInstance().getRenderer(); + VkContext* vkCtx = appRenderer ? appRenderer->getVkContext() : nullptr; + VkDescriptorSetLayout perFrameLayout = appRenderer ? appRenderer->getPerFrameSetLayout() : VK_NULL_HANDLE; + if (!charRenderer_->initialize(vkCtx, perFrameLayout, am)) { LOG_ERROR("CharacterPreview: failed to initialize CharacterRenderer"); return false; } - charRenderer_->setAssetManager(am); // Disable fog and shadows for the preview charRenderer_->setFog(glm::vec3(0.05f, 0.05f, 0.1f), 9999.0f, 10000.0f); @@ -45,14 +50,15 @@ bool CharacterPreview::initialize(pipeline::AssetManager* am) { camera_->setPosition(glm::vec3(0.0f, 4.5f, 0.9f)); camera_->setRotation(270.0f, 0.0f); - createFBO(); + // TODO: create Vulkan offscreen render target + // createFBO(); LOG_INFO("CharacterPreview initialized (", fboWidth_, "x", fboHeight_, ")"); return true; } void CharacterPreview::shutdown() { - destroyFBO(); + // destroyFBO(); // TODO: Vulkan offscreen cleanup if (charRenderer_) { charRenderer_->shutdown(); charRenderer_.reset(); @@ -63,37 +69,11 @@ void CharacterPreview::shutdown() { } void CharacterPreview::createFBO() { - // Create color texture - glGenTextures(1, &colorTexture_); - glBindTexture(GL_TEXTURE_2D, colorTexture_); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, fboWidth_, fboHeight_, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glBindTexture(GL_TEXTURE_2D, 0); - - // Create depth renderbuffer - glGenRenderbuffers(1, &depthRenderbuffer_); - glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer_); - glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, fboWidth_, fboHeight_); - glBindRenderbuffer(GL_RENDERBUFFER, 0); - - // Create FBO - glGenFramebuffers(1, &fbo_); - glBindFramebuffer(GL_FRAMEBUFFER, fbo_); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTexture_, 0); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer_); - - GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - if (status != GL_FRAMEBUFFER_COMPLETE) { - LOG_ERROR("CharacterPreview: FBO incomplete, status=", status); - } - glBindFramebuffer(GL_FRAMEBUFFER, 0); + // TODO: Create Vulkan offscreen render target for character preview } void CharacterPreview::destroyFBO() { - if (fbo_) { glDeleteFramebuffers(1, &fbo_); fbo_ = 0; } - if (colorTexture_) { glDeleteTextures(1, &colorTexture_); colorTexture_ = 0; } - if (depthRenderbuffer_) { glDeleteRenderbuffers(1, &depthRenderbuffer_); depthRenderbuffer_ = 0; } + // TODO: Destroy Vulkan offscreen render target } bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, @@ -288,8 +268,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, for (const auto& up : underwearPaths) baseLayers_.push_back(up); if (layers.size() > 1) { - GLuint compositeTex = charRenderer_->compositeTextures(layers); - if (compositeTex != 0) { + VkTexture* compositeTex = charRenderer_->compositeTextures(layers); + if (compositeTex != nullptr) { for (size_t ti = 0; ti < model.textures.size(); ti++) { if (model.textures[ti].type == 1) { charRenderer_->setModelTexture(PREVIEW_MODEL_ID, static_cast(ti), compositeTex); @@ -302,8 +282,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, // If hair scalp texture was found, ensure it's loaded for type-6 slot if (!hairScalpPath.empty()) { - GLuint hairTex = charRenderer_->loadTexture(hairScalpPath); - if (hairTex != 0) { + VkTexture* hairTex = charRenderer_->loadTexture(hairScalpPath); + if (hairTex != nullptr) { for (size_t ti = 0; ti < model.textures.size(); ti++) { if (model.textures[ti].type == 6) { charRenderer_->setModelTexture(PREVIEW_MODEL_ID, static_cast(ti), hairTex); @@ -511,8 +491,8 @@ bool CharacterPreview::applyEquipment(const std::vector& eq } if (!regionLayers.empty()) { - GLuint newTex = charRenderer_->compositeWithRegions(bodySkinPath_, baseLayers_, regionLayers); - if (newTex != 0) { + VkTexture* newTex = charRenderer_->compositeWithRegions(bodySkinPath_, baseLayers_, regionLayers); + if (newTex != nullptr) { charRenderer_->setModelTexture(PREVIEW_MODEL_ID, skinTextureSlotIndex_, newTex); } } @@ -575,10 +555,10 @@ bool CharacterPreview::applyEquipment(const std::vector& eq addCandidate(baseTex + "_U.blp"); } } - const GLuint whiteTex = charRenderer_->loadTexture(""); + VkTexture* whiteTex = charRenderer_->loadTexture(""); for (const auto& c : candidates) { - GLuint capeTex = charRenderer_->loadTexture(c); - if (capeTex != 0 && capeTex != whiteTex) { + VkTexture* capeTex = charRenderer_->loadTexture(c); + if (capeTex != nullptr && capeTex != whiteTex) { charRenderer_->setGroupTextureOverride(instanceId_, 15, capeTex); if (const auto* md = charRenderer_->getModelData(PREVIEW_MODEL_ID)) { for (size_t ti = 0; ti < md->textures.size(); ti++) { @@ -612,33 +592,14 @@ void CharacterPreview::update(float deltaTime) { } void CharacterPreview::render() { - if (!fbo_ || !charRenderer_ || !camera_ || !modelLoaded_) { + if (!charRenderer_ || !camera_ || !modelLoaded_) { return; } - // Save current viewport - GLint prevViewport[4]; - glGetIntegerv(GL_VIEWPORT, prevViewport); - - // Save current FBO binding - GLint prevFbo; - glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFbo); - - // Bind our FBO - glBindFramebuffer(GL_FRAMEBUFFER, fbo_); - glViewport(0, 0, fboWidth_, fboHeight_); - - // Clear with dark blue background - glClearColor(0.05f, 0.05f, 0.1f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - glEnable(GL_DEPTH_TEST); - - // Render the character model - charRenderer_->render(*camera_, camera_->getViewMatrix(), camera_->getProjectionMatrix()); - - // Restore previous FBO and viewport - glBindFramebuffer(GL_FRAMEBUFFER, static_cast(prevFbo)); - glViewport(prevViewport[0], prevViewport[1], prevViewport[2], prevViewport[3]); + // TODO: Vulkan offscreen rendering for character preview + // Need a VkRenderTarget, begin a render pass into it, then: + // charRenderer_->render(cmd, perFrameSet, *camera_); + // For now, the preview is non-functional until Vulkan offscreen is wired up. } void CharacterPreview::rotate(float yawDelta) { diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index b6ca738d..cd4cfb57 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -1,27 +1,30 @@ /** - * CharacterRenderer — GPU rendering of M2 character models with skeletal animation + * CharacterRenderer — GPU rendering of M2 character models with skeletal animation (Vulkan) * * Handles: - * - Uploading M2 vertex/index data to OpenGL VAO/VBO/EBO + * - Uploading M2 vertex/index data to Vulkan buffers via VMA * - Per-frame bone matrix computation (hierarchical, with keyframe interpolation) - * - GPU vertex skinning via a bone-matrix uniform array in the vertex shader + * - GPU vertex skinning via a bone-matrix SSBO in the vertex shader * - Per-batch texture binding through the M2 texture-lookup indirection * - Geoset filtering (activeGeosets) to show/hide body part groups * - CPU texture compositing for character skins (base skin + underwear overlays) * * The character texture compositing uses the WoW CharComponentTextureSections * layout, placing region overlays (pelvis, torso, etc.) at their correct pixel - * positions on the 512×512 body skin atlas. Region coordinates sourced from + * positions on the 512x512 body skin atlas. Region coordinates sourced from * the original WoW Model Viewer (charcontrol.h, REGION_FAC=2). */ #include "rendering/character_renderer.hpp" -#include "rendering/shader.hpp" -#include "rendering/texture.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_texture.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_buffer.hpp" +#include "rendering/vk_utils.hpp" #include "rendering/camera.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/blp_loader.hpp" #include "core/logger.hpp" -#include #include #include #include @@ -36,6 +39,7 @@ #include #include #include +#include namespace wowee { namespace rendering { @@ -58,6 +62,22 @@ size_t approxTextureBytesWithMips(int w, int h) { } } // namespace +// Descriptor pool sizing +static constexpr uint32_t MAX_MATERIAL_SETS = 4096; +static constexpr uint32_t MAX_BONE_SETS = 1024; + +// CharMaterial UBO layout (matches character.frag.glsl set=1 binding=1) +struct CharMaterialUBO { + float opacity; + int32_t alphaTest; + int32_t colorKeyBlack; + int32_t unlit; + float emissiveBoost; + float emissiveTintR, emissiveTintG, emissiveTintB; + float specularIntensity; + float _pad[3]; // pad to 48 bytes +}; + CharacterRenderer::CharacterRenderer() { } @@ -65,316 +85,251 @@ CharacterRenderer::~CharacterRenderer() { shutdown(); } -bool CharacterRenderer::initialize() { - core::Logger::getInstance().info("Initializing character renderer..."); +bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, + pipeline::AssetManager* am) { + core::Logger::getInstance().info("Initializing character renderer (Vulkan)..."); - // Create character shader with skeletal animation - const char* vertexSrc = R"( - #version 330 core - 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; + vkCtx_ = ctx; + assetManager = am; + perFrameLayout_ = perFrameLayout; - uniform mat4 uModel; - uniform mat4 uView; - uniform mat4 uProjection; - uniform mat4 uBones[240]; + VkDevice device = vkCtx_->getDevice(); - out vec3 FragPos; - out vec3 Normal; - out vec2 TexCoord; + // --- Descriptor set layouts --- - void main() { - // Skinning: blend bone transformations - mat4 boneTransform = mat4(0.0); - boneTransform += uBones[aBoneIndices.x] * aBoneWeights.x; - boneTransform += uBones[aBoneIndices.y] * aBoneWeights.y; - boneTransform += uBones[aBoneIndices.z] * aBoneWeights.z; - boneTransform += uBones[aBoneIndices.w] * aBoneWeights.w; + // Material set layout (set 1): binding 0 = sampler2D, binding 1 = CharMaterial UBO + { + VkDescriptorSetLayoutBinding bindings[2] = {}; + bindings[0].binding = 0; + bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + bindings[0].descriptorCount = 1; + bindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + bindings[1].binding = 1; + bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + bindings[1].descriptorCount = 1; + bindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - // Transform position and normal - vec4 skinnedPos = boneTransform * vec4(aPos, 1.0); - vec4 worldPos = uModel * skinnedPos; + VkDescriptorSetLayoutCreateInfo ci{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + ci.bindingCount = 2; + ci.pBindings = bindings; + vkCreateDescriptorSetLayout(device, &ci, nullptr, &materialSetLayout_); + } - FragPos = worldPos.xyz; - // Use mat3 directly - avoid expensive inverse() in shader - // Works correctly for uniform scaling; normalize in fragment shader handles the rest - Normal = mat3(uModel) * mat3(boneTransform) * aNormal; - TexCoord = aTexCoord; + // Bone set layout (set 2): binding 0 = STORAGE_BUFFER (bone matrices) + { + VkDescriptorSetLayoutBinding binding{}; + binding.binding = 0; + binding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; - gl_Position = uProjection * uView * worldPos; - } - )"; + VkDescriptorSetLayoutCreateInfo ci{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + ci.bindingCount = 1; + ci.pBindings = &binding; + vkCreateDescriptorSetLayout(device, &ci, nullptr, &boneSetLayout_); + } - const char* fragmentSrc = R"( - #version 330 core - in vec3 FragPos; - in vec3 Normal; - in vec2 TexCoord; + // --- Descriptor pools --- + { + VkDescriptorPoolSize sizes[] = { + {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, MAX_MATERIAL_SETS}, + {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, MAX_MATERIAL_SETS}, + }; + VkDescriptorPoolCreateInfo ci{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + ci.maxSets = MAX_MATERIAL_SETS; + ci.poolSizeCount = 2; + ci.pPoolSizes = sizes; + ci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + vkCreateDescriptorPool(device, &ci, nullptr, &materialDescPool_); + } + { + VkDescriptorPoolSize sizes[] = { + {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, MAX_BONE_SETS}, + }; + VkDescriptorPoolCreateInfo ci{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + ci.maxSets = MAX_BONE_SETS; + ci.poolSizeCount = 1; + ci.pPoolSizes = sizes; + ci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + vkCreateDescriptorPool(device, &ci, nullptr, &boneDescPool_); + } - uniform sampler2D uTexture0; - uniform vec3 uLightDir; - uniform vec3 uLightColor; - uniform float uSpecularIntensity; - uniform vec3 uViewPos; + // --- Pipeline layout --- + // set 0 = perFrame, set 1 = material, set 2 = bones + // Push constant: mat4 model = 64 bytes + { + VkDescriptorSetLayout setLayouts[] = {perFrameLayout, materialSetLayout_, boneSetLayout_}; + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + pushRange.offset = 0; + pushRange.size = 64; // mat4 - uniform vec3 uFogColor; - uniform float uFogStart; - uniform float uFogEnd; + VkPipelineLayoutCreateInfo ci{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + ci.setLayoutCount = 3; + ci.pSetLayouts = setLayouts; + ci.pushConstantRangeCount = 1; + ci.pPushConstantRanges = &pushRange; + vkCreatePipelineLayout(device, &ci, nullptr, &pipelineLayout_); + } - uniform sampler2DShadow uShadowMap; - uniform mat4 uLightSpaceMatrix; - uniform int uShadowEnabled; - uniform float uShadowStrength; - uniform float uOpacity; - uniform int uAlphaTest; - uniform int uColorKeyBlack; - uniform int uUnlit; - uniform float uEmissiveBoost; - uniform vec3 uEmissiveTint; + // --- Load shaders --- + rendering::VkShaderModule charVert, charFrag; + charVert.loadFromFile(device, "assets/shaders/character.vert.spv"); + charFrag.loadFromFile(device, "assets/shaders/character.frag.spv"); - out vec4 FragColor; - - void main() { - vec3 normal = normalize(Normal); - vec3 lightDir = normalize(uLightDir); - - // Diffuse lighting - float diff = max(dot(normal, lightDir), 0.0); - - // Blinn-Phong specular - vec3 viewDir = normalize(uViewPos - FragPos); - vec3 halfDir = normalize(lightDir + viewDir); - float spec = pow(max(dot(normal, halfDir), 0.0), 32.0); - vec3 specular = spec * uLightColor * uSpecularIntensity; - - // Shadow mapping - float shadow = 1.0; - if (uShadowEnabled != 0) { - vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0); - vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5; - if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) { - float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5)); - float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist); - float bias = max(0.005 * (1.0 - abs(dot(normal, lightDir))), 0.001); - // Single hardware PCF tap — GL_LINEAR + compare mode gives 2×2 bilinear PCF for free - shadow = texture(uShadowMap, vec3(proj.xy, proj.z - bias)); - shadow = mix(1.0, shadow, coverageFade); - } - } - shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0)); - - // Ambient - vec3 ambient = vec3(0.3); - - // Sample texture - vec4 texColor = texture(uTexture0, TexCoord); - if (uAlphaTest != 0 && texColor.a < 0.5) discard; - if (uColorKeyBlack != 0) { - float key = max(texColor.r, max(texColor.g, texColor.b)); - // Soft black-key: fade fringe instead of hard-cut to avoid dark halo. - float keyAlpha = smoothstep(0.12, 0.30, key); - texColor.a *= keyAlpha; - if (texColor.a < 0.02) discard; - } - - // Combine - vec3 litResult = (ambient + (diff * vec3(1.0) + specular) * shadow) * texColor.rgb; - vec3 warmBase = vec3( - max(texColor.r, texColor.g * 0.92), - texColor.g * 0.90, - texColor.b * 0.45 - ); - vec3 emissiveResult = warmBase * uEmissiveTint * uEmissiveBoost; - vec3 result = (uUnlit != 0) ? emissiveResult : litResult; - - // Fog - float fogDist = length(uViewPos - FragPos); - float fogFactor = clamp((uFogEnd - fogDist) / (uFogEnd - uFogStart), 0.0, 1.0); - result = mix(uFogColor, result, fogFactor); - - // Apply texture alpha and instance opacity (for fade-in effects) - float finalAlpha = texColor.a * uOpacity; - if (finalAlpha < 0.02) discard; - FragColor = vec4(result, finalAlpha); - } - )"; - - // Log GPU uniform limit - GLint maxComponents = 0; - glGetIntegerv(GL_MAX_VERTEX_UNIFORM_COMPONENTS, &maxComponents); - core::Logger::getInstance().info("GPU max vertex uniform components: ", maxComponents, - " (supports ~", maxComponents / 16, " mat4)"); - - characterShader = std::make_unique(); - if (!characterShader->loadFromSource(vertexSrc, fragmentSrc)) { - core::Logger::getInstance().error("Failed to create character shader"); + if (!charVert.isValid() || !charFrag.isValid()) { + LOG_ERROR("Character: Missing required shaders, cannot initialize"); return false; } - const char* shadowVertSrc = R"( - #version 330 core - layout (location = 0) in vec3 aPos; - layout (location = 1) in vec4 aBoneWeights; - layout (location = 2) in ivec4 aBoneIndices; - layout (location = 4) in vec2 aTexCoord; + VkRenderPass mainPass = vkCtx_->getImGuiRenderPass(); - uniform mat4 uLightSpaceMatrix; - uniform mat4 uModel; - uniform mat4 uBones[240]; + // --- Vertex input --- + // M2Vertex: vec3 pos(12) + uint8[4] boneWeights(4) + uint8[4] boneIndices(4) + + // vec3 normal(12) + vec2[2] texCoords(16) = 48 bytes + VkVertexInputBindingDescription charBinding{}; + charBinding.binding = 0; + charBinding.stride = sizeof(pipeline::M2Vertex); + charBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; - out vec2 vTexCoord; - - void main() { - mat4 boneTransform = mat4(0.0); - boneTransform += uBones[aBoneIndices.x] * aBoneWeights.x; - boneTransform += uBones[aBoneIndices.y] * aBoneWeights.y; - boneTransform += uBones[aBoneIndices.z] * aBoneWeights.z; - boneTransform += uBones[aBoneIndices.w] * aBoneWeights.w; - vec4 skinnedPos = boneTransform * vec4(aPos, 1.0); - vTexCoord = aTexCoord; - gl_Position = uLightSpaceMatrix * uModel * skinnedPos; - } - )"; - - const char* shadowFragSrc = R"( - #version 330 core - in vec2 vTexCoord; - uniform sampler2D uTexture; - uniform bool uAlphaTest; - uniform bool uColorKeyBlack; - void main() { - vec4 tex = texture(uTexture, vTexCoord); - if (uAlphaTest && tex.a < 0.5) discard; - if (uColorKeyBlack) { - float key = max(tex.r, max(tex.g, tex.b)); - if (key < 0.14) discard; - } - } - )"; - - auto compileStage = [](GLenum type, const char* src) -> GLuint { - GLuint shader = glCreateShader(type); - glShaderSource(shader, 1, &src, nullptr); - glCompileShader(shader); - GLint ok = 0; - glGetShaderiv(shader, GL_COMPILE_STATUS, &ok); - if (!ok) { - char log[512]; - glGetShaderInfoLog(shader, sizeof(log), nullptr, log); - LOG_ERROR("Character shadow shader compile error: ", log); - glDeleteShader(shader); - return 0; - } - return shader; + std::vector charAttrs = { + {0, 0, VK_FORMAT_R32G32B32_SFLOAT, static_cast(offsetof(pipeline::M2Vertex, position))}, + {1, 0, VK_FORMAT_R8G8B8A8_UNORM, static_cast(offsetof(pipeline::M2Vertex, boneWeights))}, + {2, 0, VK_FORMAT_R8G8B8A8_UINT, static_cast(offsetof(pipeline::M2Vertex, boneIndices))}, + {3, 0, VK_FORMAT_R32G32B32_SFLOAT, static_cast(offsetof(pipeline::M2Vertex, normal))}, + {4, 0, VK_FORMAT_R32G32_SFLOAT, static_cast(offsetof(pipeline::M2Vertex, texCoords))}, }; - GLuint shVs = compileStage(GL_VERTEX_SHADER, shadowVertSrc); - GLuint shFs = compileStage(GL_FRAGMENT_SHADER, shadowFragSrc); - if (!shVs || !shFs) { - if (shVs) glDeleteShader(shVs); - if (shFs) glDeleteShader(shFs); - return false; + // --- Build pipelines --- + auto buildCharPipeline = [&](VkPipelineColorBlendAttachmentState blendState, bool depthWrite) -> VkPipeline { + return PipelineBuilder() + .setShaders(charVert.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + charFrag.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({charBinding}, charAttrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(blendState) + .setLayout(pipelineLayout_) + .setRenderPass(mainPass) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device); + }; + + opaquePipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true); + alphaTestPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), true); + alphaPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), false); + additivePipeline_ = buildCharPipeline(PipelineBuilder::blendAdditive(), false); + + // Clean up shader modules + charVert.destroy(); + charFrag.destroy(); + + // --- Create white fallback texture --- + { + uint8_t white[] = {255, 255, 255, 255}; + whiteTexture_ = std::make_unique(); + whiteTexture_->upload(*vkCtx_, white, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false); + whiteTexture_->createSampler(device, VK_FILTER_NEAREST, VK_FILTER_NEAREST, VK_SAMPLER_ADDRESS_MODE_REPEAT); } - shadowCasterProgram = glCreateProgram(); - glAttachShader(shadowCasterProgram, shVs); - glAttachShader(shadowCasterProgram, shFs); - glLinkProgram(shadowCasterProgram); - GLint linked = 0; - glGetProgramiv(shadowCasterProgram, GL_LINK_STATUS, &linked); - glDeleteShader(shVs); - glDeleteShader(shFs); - if (!linked) { - char log[512]; - glGetProgramInfoLog(shadowCasterProgram, sizeof(log), nullptr, log); - LOG_ERROR("Character shadow shader link error: ", log); - glDeleteProgram(shadowCasterProgram); - shadowCasterProgram = 0; - return false; + // --- Create transparent fallback texture --- + { + uint8_t transparent[] = {0, 0, 0, 0}; + transparentTexture_ = std::make_unique(); + transparentTexture_->upload(*vkCtx_, transparent, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false); + transparentTexture_->createSampler(device, VK_FILTER_NEAREST, VK_FILTER_NEAREST, VK_SAMPLER_ADDRESS_MODE_REPEAT); } - // Create 1x1 white fallback texture - uint8_t white[] = { 255, 255, 255, 255 }; - glGenTextures(1, &whiteTexture); - glBindTexture(GL_TEXTURE_2D, whiteTexture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, white); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - - // Create 1x1 transparent fallback texture for hidden texture slots. - uint8_t transparent[] = { 0, 0, 0, 0 }; - glGenTextures(1, &transparentTexture); - glBindTexture(GL_TEXTURE_2D, transparentTexture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, transparent); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glBindTexture(GL_TEXTURE_2D, 0); - // Diagnostics-only: cache lifetime is currently tied to renderer lifetime. textureCacheBudgetBytes_ = envSizeMBOrDefault("WOWEE_CHARACTER_TEX_CACHE_MB", 2048) * 1024ull * 1024ull; - core::Logger::getInstance().info("Character renderer initialized"); + core::Logger::getInstance().info("Character renderer initialized (Vulkan)"); return true; } void CharacterRenderer::shutdown() { - // Clean up GPU resources + if (!vkCtx_) return; + + vkDeviceWaitIdle(vkCtx_->getDevice()); + VkDevice device = vkCtx_->getDevice(); + VmaAllocator alloc = vkCtx_->getAllocator(); + + // Clean up GPU resources for models for (auto& pair : models) { - auto& gpuModel = pair.second; - if (gpuModel.vao) { - glDeleteVertexArrays(1, &gpuModel.vao); - glDeleteBuffers(1, &gpuModel.vbo); - glDeleteBuffers(1, &gpuModel.ebo); - } - for (GLuint texId : gpuModel.textureIds) { - if (texId && texId != whiteTexture) { - glDeleteTextures(1, &texId); - } - } + destroyModelGPU(pair.second); } - // Clean up texture cache - for (auto& pair : textureCache) { - GLuint texId = pair.second.id; - if (texId && texId != whiteTexture) { - glDeleteTextures(1, &texId); - } + // Clean up instance bone buffers + for (auto& pair : instances) { + destroyInstanceBones(pair.second); } + + // Clean up texture cache (VkTexture unique_ptrs auto-destroy) textureCache.clear(); - textureHasAlphaById_.clear(); - textureColorKeyBlackById_.clear(); + textureHasAlphaByPtr_.clear(); + textureColorKeyBlackByPtr_.clear(); textureCacheBytes_ = 0; textureCacheCounter_ = 0; - if (whiteTexture) { - glDeleteTextures(1, &whiteTexture); - whiteTexture = 0; - } - if (transparentTexture) { - glDeleteTextures(1, &transparentTexture); - transparentTexture = 0; - } + // Clean up composite cache + compositeCache_.clear(); + failedTextureCache_.clear(); + + whiteTexture_.reset(); + transparentTexture_.reset(); models.clear(); instances.clear(); - characterShader.reset(); - if (shadowCasterProgram) { - glDeleteProgram(shadowCasterProgram); - shadowCasterProgram = 0; + + // Destroy pipelines + auto destroyPipeline = [&](VkPipeline& p) { + if (p) { vkDestroyPipeline(device, p, nullptr); p = VK_NULL_HANDLE; } + }; + destroyPipeline(opaquePipeline_); + destroyPipeline(alphaTestPipeline_); + destroyPipeline(alphaPipeline_); + destroyPipeline(additivePipeline_); + + if (pipelineLayout_) { vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); pipelineLayout_ = VK_NULL_HANDLE; } + + // Destroy descriptor pools and layouts + if (materialDescPool_) { vkDestroyDescriptorPool(device, materialDescPool_, nullptr); materialDescPool_ = VK_NULL_HANDLE; } + if (boneDescPool_) { vkDestroyDescriptorPool(device, boneDescPool_, nullptr); boneDescPool_ = VK_NULL_HANDLE; } + if (materialSetLayout_) { vkDestroyDescriptorSetLayout(device, materialSetLayout_, nullptr); materialSetLayout_ = VK_NULL_HANDLE; } + if (boneSetLayout_) { vkDestroyDescriptorSetLayout(device, boneSetLayout_, nullptr); boneSetLayout_ = VK_NULL_HANDLE; } + + vkCtx_ = nullptr; +} + +void CharacterRenderer::destroyModelGPU(M2ModelGPU& gpuModel) { + if (!vkCtx_) return; + VmaAllocator alloc = vkCtx_->getAllocator(); + if (gpuModel.vertexBuffer) { vmaDestroyBuffer(alloc, gpuModel.vertexBuffer, gpuModel.vertexAlloc); gpuModel.vertexBuffer = VK_NULL_HANDLE; } + if (gpuModel.indexBuffer) { vmaDestroyBuffer(alloc, gpuModel.indexBuffer, gpuModel.indexAlloc); gpuModel.indexBuffer = VK_NULL_HANDLE; } +} + +void CharacterRenderer::destroyInstanceBones(CharacterInstance& inst) { + if (!vkCtx_) return; + VmaAllocator alloc = vkCtx_->getAllocator(); + for (int i = 0; i < 2; i++) { + if (inst.boneBuffer[i]) { + vmaDestroyBuffer(alloc, inst.boneBuffer[i], inst.boneAlloc[i]); + inst.boneBuffer[i] = VK_NULL_HANDLE; + inst.boneMapped[i] = nullptr; + } + // boneSet freed when pool is reset/destroyed } } -GLuint CharacterRenderer::loadTexture(const std::string& path) { +VkTexture* CharacterRenderer::loadTexture(const std::string& path) { // Skip empty or whitespace-only paths (type-0 textures have no filename) - if (path.empty()) return whiteTexture; + if (path.empty()) return whiteTexture_.get(); bool allWhitespace = true; for (char c : path) { if (c != ' ' && c != '\t' && c != '\0' && c != '\n') { allWhitespace = false; break; } } - if (allWhitespace) return whiteTexture; + if (allWhitespace) return whiteTexture_.get(); auto normalizeKey = [](std::string key) { std::replace(key.begin(), key.end(), '/', '\\'); @@ -396,23 +351,23 @@ GLuint CharacterRenderer::loadTexture(const std::string& path) { auto it = textureCache.find(key); if (it != textureCache.end()) { it->second.lastUse = ++textureCacheCounter_; - return it->second.id; + return it->second.texture.get(); } if (!assetManager || !assetManager->isInitialized()) { - return whiteTexture; + return whiteTexture_.get(); } // Check negative cache to avoid repeated file I/O for textures that don't exist if (failedTextureCache_.count(key)) { - return whiteTexture; + return whiteTexture_.get(); } auto blpImage = assetManager->loadTexture(key); if (!blpImage.isValid()) { core::Logger::getInstance().warning("Failed to load texture: ", path); failedTextureCache_.insert(key); - return whiteTexture; + return whiteTexture_.get(); } bool hasAlpha = false; @@ -423,29 +378,25 @@ GLuint CharacterRenderer::loadTexture(const std::string& path) { } } - GLuint texId; - glGenTextures(1, &texId); - glBindTexture(GL_TEXTURE_2D, texId); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, blpImage.width, blpImage.height, - 0, GL_RGBA, GL_UNSIGNED_BYTE, blpImage.data.data()); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); - glGenerateMipmap(GL_TEXTURE_2D); - applyAnisotropicFiltering(); - glBindTexture(GL_TEXTURE_2D, 0); + auto tex = std::make_unique(); + tex->upload(*vkCtx_, blpImage.data.data(), blpImage.width, blpImage.height, + VK_FORMAT_R8G8B8A8_UNORM, true); + tex->createSampler(vkCtx_->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR, + VK_SAMPLER_ADDRESS_MODE_REPEAT); + + VkTexture* texPtr = tex.get(); TextureCacheEntry e; - e.id = texId; + e.texture = std::move(tex); e.approxBytes = approxTextureBytesWithMips(blpImage.width, blpImage.height); e.lastUse = ++textureCacheCounter_; e.hasAlpha = hasAlpha; e.colorKeyBlack = colorKeyBlackHint; textureCacheBytes_ += e.approxBytes; - textureCache[key] = e; - textureHasAlphaById_[texId] = hasAlpha; - textureColorKeyBlackById_[texId] = colorKeyBlackHint; + textureHasAlphaByPtr_[texPtr] = hasAlpha; + textureColorKeyBlackByPtr_[texPtr] = colorKeyBlackHint; + textureCache[key] = std::move(e); + if (textureCacheBytes_ > textureCacheBudgetBytes_) { core::Logger::getInstance().warning( "Character texture cache over budget: ", @@ -453,7 +404,7 @@ GLuint CharacterRenderer::loadTexture(const std::string& path) { textureCacheBudgetBytes_ / (1024 * 1024), " MB (textures=", textureCache.size(), ")"); } core::Logger::getInstance().debug("Loaded character texture: ", path, " (", blpImage.width, "x", blpImage.height, ")"); - return texId; + return texPtr; } // Alpha-blend overlay onto composite at (dstX, dstY) @@ -499,7 +450,7 @@ static void blitOverlayScaledN(std::vector& composite, int compW, int c uint8_t srcA = overlay.data[srcIdx + 3]; if (srcA == 0) continue; - // Write to scale×scale block of destination pixels + // Write to scale x scale block of destination pixels for (int dy2 = 0; dy2 < scale; dy2++) { int dy = dstY + sy * scale + dy2; if (dy < 0 || dy >= compH) continue; @@ -533,16 +484,16 @@ static void blitOverlayScaled2x(std::vector& composite, int compW, int blitOverlayScaledN(composite, compW, compH, overlay, dstX, dstY, 2); } -GLuint CharacterRenderer::compositeTextures(const std::vector& layerPaths) { +VkTexture* CharacterRenderer::compositeTextures(const std::vector& layerPaths) { if (layerPaths.empty() || !assetManager || !assetManager->isInitialized()) { - return whiteTexture; + return whiteTexture_.get(); } // Load base layer auto base = assetManager->loadTexture(layerPaths[0]); if (!base.isValid()) { core::Logger::getInstance().warning("Composite: failed to load base layer: ", layerPaths[0]); - return whiteTexture; + return whiteTexture_.get(); } // Copy base pixel data as our working buffer @@ -628,7 +579,7 @@ GLuint CharacterRenderer::compositeTextures(const std::vector& laye dstX = 128; dstY = 160; expectedW256 = 128; expectedH256 = 64; } else { - // Unknown — center placement as fallback + // Unknown -- center placement as fallback dstX = (width - overlay.width) / 2; dstY = (height - overlay.height) / 2; core::Logger::getInstance().info("Composite: UNKNOWN region for '", @@ -674,31 +625,38 @@ GLuint CharacterRenderer::compositeTextures(const std::vector& laye } } - // Upload composite to GPU - GLuint texId; - glGenTextures(1, &texId); - glBindTexture(GL_TEXTURE_2D, texId); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, composite.data()); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); - glGenerateMipmap(GL_TEXTURE_2D); - applyAnisotropicFiltering(); - glBindTexture(GL_TEXTURE_2D, 0); + // Upload composite to GPU via VkTexture + auto tex = std::make_unique(); + tex->upload(*vkCtx_, composite.data(), width, height, VK_FORMAT_R8G8B8A8_UNORM, true); + tex->createSampler(vkCtx_->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR, + VK_SAMPLER_ADDRESS_MODE_REPEAT); + + VkTexture* texPtr = tex.get(); + + // Store in texture cache with a generated key + std::string cacheKey = "__composite__"; + for (const auto& lp : layerPaths) { cacheKey += '|'; cacheKey += lp; } + + TextureCacheEntry e; + e.texture = std::move(tex); + e.approxBytes = approxTextureBytesWithMips(width, height); + e.lastUse = ++textureCacheCounter_; + e.hasAlpha = false; + e.colorKeyBlack = false; + textureCache[cacheKey] = std::move(e); core::Logger::getInstance().info("Composite texture created: ", width, "x", height, " from ", layerPaths.size(), " layers"); - return texId; + return texPtr; } void CharacterRenderer::clearCompositeCache() { // Just clear the lookup map so next compositeWithRegions() creates fresh textures. - // Don't delete GPU textures — they may still be referenced by models or instances. + // Don't delete GPU textures -- they may still be referenced by models or instances. // Orphaned textures will be cleaned up when their model/instance is destroyed. compositeCache_.clear(); } -GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath, +VkTexture* CharacterRenderer::compositeWithRegions(const std::string& basePath, const std::vector& baseLayers, const std::vector>& regionLayers) { // Build cache key from all inputs to avoid redundant compositing @@ -712,11 +670,11 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath, cacheKey += ','; } auto cacheIt = compositeCache_.find(cacheKey); - if (cacheIt != compositeCache_.end() && cacheIt->second != 0) { + if (cacheIt != compositeCache_.end() && cacheIt->second != nullptr) { return cacheIt->second; } - // Region index → pixel coordinates on the 256x256 base atlas + // Region index -> pixel coordinates on the 256x256 base atlas // These are scaled up by (width/256, height/256) for larger textures (512x512, 1024x1024) static const int regionCoords256[][2] = { { 0, 0 }, // 0 = ArmUpper @@ -737,20 +695,18 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath, } // Load base composite into CPU buffer if (!assetManager || !assetManager->isInitialized()) { - return whiteTexture; + return whiteTexture_.get(); } auto base = assetManager->loadTexture(basePath); if (!base.isValid()) { - return whiteTexture; + return whiteTexture_.get(); } std::vector composite; int width = base.width; int height = base.height; - - // If base texture is 256x256 (e.g., baked NPC texture), upscale to 512x512 // so equipment regions can be composited at correct coordinates if (width == 256 && height == 256 && !regionLayers.empty()) { @@ -776,7 +732,7 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath, } // Blend face + underwear overlays - // If we upscaled from 256→512, scale coords and texels with blitOverlayScaled2x. + // If we upscaled from 256->512, scale coords and texels with blitOverlayScaled2x. // For native 512/1024 textures, face overlays are full atlas size (hit width==width branch). bool upscaled = (base.width == 256 && base.height == 256 && width == 512); for (const auto& ul : baseLayers) { @@ -881,26 +837,31 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath, " at (", dstX, ",", dstY, ") ", overlay.width, "x", overlay.height, " from ", rl.second); } - // Upload to GPU - GLuint texId; - glGenTextures(1, &texId); - glBindTexture(GL_TEXTURE_2D, texId); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, composite.data()); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); - glGenerateMipmap(GL_TEXTURE_2D); - applyAnisotropicFiltering(); - glBindTexture(GL_TEXTURE_2D, 0); + // Upload to GPU via VkTexture + auto tex = std::make_unique(); + tex->upload(*vkCtx_, composite.data(), width, height, VK_FORMAT_R8G8B8A8_UNORM, true); + tex->createSampler(vkCtx_->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR, + VK_SAMPLER_ADDRESS_MODE_REPEAT); + + VkTexture* texPtr = tex.get(); + + // Store in texture cache + std::string storageKey = "__compositeRegions__" + cacheKey; + TextureCacheEntry entry; + entry.texture = std::move(tex); + entry.approxBytes = approxTextureBytesWithMips(width, height); + entry.lastUse = ++textureCacheCounter_; + entry.hasAlpha = false; + entry.colorKeyBlack = false; + textureCache[storageKey] = std::move(entry); core::Logger::getInstance().debug("compositeWithRegions: created ", width, "x", height, " texture with ", regionLayers.size(), " equipment regions"); - compositeCache_[cacheKey] = texId; - return texId; + compositeCache_[cacheKey] = texPtr; + return texPtr; } -void CharacterRenderer::setModelTexture(uint32_t modelId, uint32_t textureSlot, GLuint textureId) { +void CharacterRenderer::setModelTexture(uint32_t modelId, uint32_t textureSlot, VkTexture* texture) { auto it = models.find(modelId); if (it == models.end()) { core::Logger::getInstance().warning("setModelTexture: model ", modelId, " not found"); @@ -913,24 +874,12 @@ void CharacterRenderer::setModelTexture(uint32_t modelId, uint32_t textureSlot, return; } - // Delete old texture if it's not shared and not in the texture cache - GLuint oldTex = gpuModel.textureIds[textureSlot]; - if (oldTex && oldTex != whiteTexture) { - bool cached = false; - for (const auto& [k, v] : textureCache) { - if (v.id == oldTex) { cached = true; break; } - } - if (!cached) { - glDeleteTextures(1, &oldTex); - } - } - - gpuModel.textureIds[textureSlot] = textureId; + gpuModel.textureIds[textureSlot] = texture; core::Logger::getInstance().debug("Replaced model ", modelId, " texture slot ", textureSlot, " with composited texture"); } void CharacterRenderer::resetModelTexture(uint32_t modelId, uint32_t textureSlot) { - setModelTexture(modelId, textureSlot, whiteTexture); + setModelTexture(modelId, textureSlot, whiteTexture_.get()); } bool CharacterRenderer::loadModel(const pipeline::M2Model& model, uint32_t id) { @@ -941,12 +890,7 @@ bool CharacterRenderer::loadModel(const pipeline::M2Model& model, uint32_t id) { if (models.find(id) != models.end()) { core::Logger::getInstance().warning("Model ID ", id, " already loaded, replacing"); - auto& old = models[id]; - if (old.vao) { - glDeleteVertexArrays(1, &old.vao); - glDeleteBuffers(1, &old.vbo); - glDeleteBuffers(1, &old.ebo); - } + destroyModelGPU(models[id]); } M2ModelGPU gpuModel; @@ -960,8 +904,8 @@ bool CharacterRenderer::loadModel(const pipeline::M2Model& model, uint32_t id) { // Load textures from model for (const auto& tex : model.textures) { - GLuint texId = loadTexture(tex.filename); - gpuModel.textureIds.push_back(texId); + VkTexture* texPtr = loadTexture(tex.filename); + gpuModel.textureIds.push_back(texPtr); } models[id] = std::move(gpuModel); @@ -976,48 +920,25 @@ bool CharacterRenderer::loadModel(const pipeline::M2Model& model, uint32_t id) { void CharacterRenderer::setupModelBuffers(M2ModelGPU& gpuModel) { auto& model = gpuModel.data; - glGenVertexArrays(1, &gpuModel.vao); - glGenBuffers(1, &gpuModel.vbo); - glGenBuffers(1, &gpuModel.ebo); + if (model.vertices.empty() || model.indices.empty()) return; - glBindVertexArray(gpuModel.vao); + // Upload vertex buffer + auto vb = uploadBuffer(*vkCtx_, + model.vertices.data(), + model.vertices.size() * sizeof(pipeline::M2Vertex), + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + gpuModel.vertexBuffer = vb.buffer; + gpuModel.vertexAlloc = vb.allocation; + gpuModel.vertexCount = static_cast(model.vertices.size()); - // Interleaved vertex data - glBindBuffer(GL_ARRAY_BUFFER, gpuModel.vbo); - glBufferData(GL_ARRAY_BUFFER, model.vertices.size() * sizeof(pipeline::M2Vertex), - model.vertices.data(), GL_STATIC_DRAW); - - // Position - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(pipeline::M2Vertex), - (void*)offsetof(pipeline::M2Vertex, position)); - - // Bone weights (normalize uint8 to float) - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(pipeline::M2Vertex), - (void*)offsetof(pipeline::M2Vertex, boneWeights)); - - // Bone indices - glEnableVertexAttribArray(2); - glVertexAttribIPointer(2, 4, GL_UNSIGNED_BYTE, sizeof(pipeline::M2Vertex), - (void*)offsetof(pipeline::M2Vertex, boneIndices)); - - // Normal - glEnableVertexAttribArray(3); - glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(pipeline::M2Vertex), - (void*)offsetof(pipeline::M2Vertex, normal)); - - // TexCoord (first UV set) - glEnableVertexAttribArray(4); - glVertexAttribPointer(4, 2, GL_FLOAT, GL_FALSE, sizeof(pipeline::M2Vertex), - (void*)offsetof(pipeline::M2Vertex, texCoords)); - - // Index buffer - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpuModel.ebo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, model.indices.size() * sizeof(uint16_t), - model.indices.data(), GL_STATIC_DRAW); - - glBindVertexArray(0); + // Upload index buffer + auto ib = uploadBuffer(*vkCtx_, + model.indices.data(), + model.indices.size() * sizeof(uint16_t), + VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + gpuModel.indexBuffer = ib.buffer; + gpuModel.indexAlloc = ib.allocation; + gpuModel.indexCount = static_cast(model.indices.size()); } void CharacterRenderer::calculateBindPose(M2ModelGPU& gpuModel) { @@ -1057,8 +978,9 @@ uint32_t CharacterRenderer::createInstance(uint32_t modelId, const glm::vec3& po auto& model = models[modelId].data; instance.boneMatrices.resize(std::max(static_cast(1), model.bones.size()), glm::mat4(1.0f)); - instances[instance.id] = instance; - return instance.id; + uint32_t id = instance.id; + instances[id] = std::move(instance); + return id; } void CharacterRenderer::playAnimation(uint32_t instanceId, uint32_t animationId, bool loop) { @@ -1100,10 +1022,10 @@ void CharacterRenderer::playAnimation(uint32_t instanceId, uint32_t animationId, // Only log missing animation once per model (reduce spam) static std::unordered_map> loggedMissingAnims; - uint32_t modelId = instance.modelId; // Use modelId as identifier - if (loggedMissingAnims[modelId].insert(animationId).second) { + uint32_t mId = instance.modelId; // Use modelId as identifier + if (loggedMissingAnims[mId].insert(animationId).second) { // First time seeing this missing animation for this model - LOG_WARNING("Animation ", animationId, " not found in model ", modelId, ", using default"); + LOG_WARNING("Animation ", animationId, " not found in model ", mId, ", using default"); } } } @@ -1390,44 +1312,20 @@ glm::mat4 CharacterRenderer::getBoneTransform(const pipeline::M2Bone& bone, floa // --- Rendering --- -void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection) { - if (instances.empty()) { +void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera) { + if (instances.empty() || !opaquePipeline_) { return; } - glEnable(GL_DEPTH_TEST); - glDisable(GL_CULL_FACE); // M2 models have mixed winding; render both sides - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + uint32_t frameIndex = vkCtx_->getCurrentFrame(); - characterShader->use(); - characterShader->setUniform("uView", view); - characterShader->setUniform("uProjection", projection); - characterShader->setUniform("uLightDir", lightDir); - characterShader->setUniform("uLightColor", lightColor); - characterShader->setUniform("uSpecularIntensity", 0.5f); - characterShader->setUniform("uViewPos", camera.getPosition()); + // Bind per-frame descriptor set (set 0) -- shared across all draws + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + pipelineLayout_, 0, 1, &perFrameSet, 0, nullptr); - // Fog - characterShader->setUniform("uFogColor", fogColor); - characterShader->setUniform("uFogStart", fogStart); - characterShader->setUniform("uFogEnd", fogEnd); - - // Shadows - characterShader->setUniform("uShadowEnabled", shadowEnabled ? 1 : 0); - characterShader->setUniform("uShadowStrength", 0.68f); - characterShader->setUniform("uTexture0", 0); - characterShader->setUniform("uAlphaTest", 0); - characterShader->setUniform("uColorKeyBlack", 0); - characterShader->setUniform("uUnlit", 0); - characterShader->setUniform("uEmissiveBoost", 1.0f); - characterShader->setUniform("uEmissiveTint", glm::vec3(1.0f)); - if (shadowEnabled) { - characterShader->setUniform("uLightSpaceMatrix", lightSpaceMatrix); - glActiveTexture(GL_TEXTURE7); - glBindTexture(GL_TEXTURE_2D, shadowDepthTex); - characterShader->setUniform("uShadowMap", 7); - } + // Start with opaque pipeline + VkPipeline currentPipeline = opaquePipeline_; + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, currentPipeline); for (const auto& pair : instances) { const auto& instance = pair.second; @@ -1435,7 +1333,12 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons // Skip invisible instances (e.g., player in first-person mode) if (!instance.visible) continue; - const auto& gpuModel = models[instance.modelId]; + auto modelIt = models.find(instance.modelId); + if (modelIt == models.end()) continue; + const auto& gpuModel = modelIt->second; + + // Skip models without GPU buffers + if (!gpuModel.vertexBuffer) continue; // Skip fully transparent instances if (instance.opacity <= 0.0f) continue; @@ -1444,28 +1347,77 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons glm::mat4 modelMat = instance.hasOverrideModelMatrix ? instance.overrideModelMatrix : getModelMatrix(instance); - characterShader->setUniform("uModel", modelMat); - characterShader->setUniform("uOpacity", instance.opacity); - // Set bone matrices (upload all at once for performance) + // Push model matrix + vkCmdPushConstants(cmd, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(glm::mat4), &modelMat); + + // Upload bone matrices to SSBO int numBones = std::min(static_cast(instance.boneMatrices.size()), MAX_BONES); if (numBones > 0) { - characterShader->setUniformMatrixArray("uBones[0]", instance.boneMatrices.data(), numBones); + // Lazy-allocate bone SSBO on first use + auto& instMut = const_cast(instance); + if (!instMut.boneBuffer[frameIndex]) { + VkBufferCreateInfo bci{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bci.size = MAX_BONES * sizeof(glm::mat4); + bci.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + VmaAllocationCreateInfo aci{}; + aci.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + aci.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + VmaAllocationInfo allocInfo{}; + vmaCreateBuffer(vkCtx_->getAllocator(), &bci, &aci, + &instMut.boneBuffer[frameIndex], &instMut.boneAlloc[frameIndex], &allocInfo); + instMut.boneMapped[frameIndex] = allocInfo.pMappedData; + + // Allocate descriptor set for bone SSBO + VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + ai.descriptorPool = boneDescPool_; + ai.descriptorSetCount = 1; + ai.pSetLayouts = &boneSetLayout_; + vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &instMut.boneSet[frameIndex]); + + if (instMut.boneSet[frameIndex]) { + VkDescriptorBufferInfo bufInfo{}; + bufInfo.buffer = instMut.boneBuffer[frameIndex]; + bufInfo.offset = 0; + bufInfo.range = bci.size; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = instMut.boneSet[frameIndex]; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + write.pBufferInfo = &bufInfo; + vkUpdateDescriptorSets(vkCtx_->getDevice(), 1, &write, 0, nullptr); + } + } + + // Upload bone matrices + if (instMut.boneMapped[frameIndex]) { + memcpy(instMut.boneMapped[frameIndex], instance.boneMatrices.data(), + numBones * sizeof(glm::mat4)); + } + + // Bind bone descriptor set (set 2) + if (instMut.boneSet[frameIndex]) { + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + pipelineLayout_, 2, 1, &instMut.boneSet[frameIndex], 0, nullptr); + } } - // Bind VAO and draw - glBindVertexArray(gpuModel.vao); + // Bind vertex and index buffers + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &gpuModel.vertexBuffer, &offset); + vkCmdBindIndexBuffer(cmd, gpuModel.indexBuffer, 0, VK_INDEX_TYPE_UINT16); - if (!gpuModel.data.batches.empty()) { - bool applyGeosetFilter = !instance.activeGeosets.empty(); - if (applyGeosetFilter) { - bool hasRenderableGeoset = false; - for (const auto& batch : gpuModel.data.batches) { - if (instance.activeGeosets.find(batch.submeshId) != instance.activeGeosets.end()) { - hasRenderableGeoset = true; - break; - } - } + if (!gpuModel.data.batches.empty()) { + bool applyGeosetFilter = !instance.activeGeosets.empty(); + if (applyGeosetFilter) { + bool hasRenderableGeoset = false; + for (const auto& batch : gpuModel.data.batches) { + if (instance.activeGeosets.find(batch.submeshId) != instance.activeGeosets.end()) { + hasRenderableGeoset = true; + break; + } + } if (!hasRenderableGeoset) { static std::unordered_set loggedGeosetFallback; if (loggedGeosetFallback.insert(instance.id).second) { @@ -1473,81 +1425,77 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons instance.id, " (model ", instance.modelId, "); rendering all batches as fallback"); } - applyGeosetFilter = false; - } - } + applyGeosetFilter = false; + } + } - auto resolveBatchTexture = [&](const CharacterInstance& inst, const M2ModelGPU& gm, const pipeline::M2Batch& b) -> GLuint { - // A skin batch can reference multiple textures (b.textureCount) starting at b.textureIndex. - // We currently bind only a single texture, so pick the most appropriate one. - // - // This matters for hair: the first texture in the combo can be a mask/empty slot, - // causing the hair to render as solid white. - if (b.textureIndex == 0xFFFF) return whiteTexture; - if (gm.data.textureLookup.empty() || gm.textureIds.empty()) return whiteTexture; + auto resolveBatchTexture = [&](const CharacterInstance& inst, const M2ModelGPU& gm, const pipeline::M2Batch& b) -> VkTexture* { + // A skin batch can reference multiple textures (b.textureCount) starting at b.textureIndex. + // We currently bind only a single texture, so pick the most appropriate one. + if (b.textureIndex == 0xFFFF) return whiteTexture_.get(); + if (gm.data.textureLookup.empty() || gm.textureIds.empty()) return whiteTexture_.get(); - uint32_t comboCount = b.textureCount ? static_cast(b.textureCount) : 1u; - comboCount = std::min(comboCount, 8u); + uint32_t comboCount = b.textureCount ? static_cast(b.textureCount) : 1u; + comboCount = std::min(comboCount, 8u); - struct Candidate { GLuint id; uint32_t type; }; - Candidate first{whiteTexture, 0}; - bool hasFirst = false; - Candidate firstNonWhite{whiteTexture, 0}; - bool hasFirstNonWhite = false; + struct Candidate { VkTexture* tex; uint32_t type; }; + Candidate first{whiteTexture_.get(), 0}; + bool hasFirst = false; + Candidate firstNonWhite{whiteTexture_.get(), 0}; + bool hasFirstNonWhite = false; - for (uint32_t i = 0; i < comboCount; i++) { - uint32_t lookupPos = static_cast(b.textureIndex) + i; - if (lookupPos >= gm.data.textureLookup.size()) break; - uint16_t texSlot = gm.data.textureLookup[lookupPos]; - if (texSlot >= gm.textureIds.size()) continue; + for (uint32_t i = 0; i < comboCount; i++) { + uint32_t lookupPos = static_cast(b.textureIndex) + i; + if (lookupPos >= gm.data.textureLookup.size()) break; + uint16_t texSlot = gm.data.textureLookup[lookupPos]; + if (texSlot >= gm.textureIds.size()) continue; - GLuint texId = gm.textureIds[texSlot]; - uint32_t texType = (texSlot < gm.data.textures.size()) ? gm.data.textures[texSlot].type : 0; - // Apply texture slot overrides. - // For type-1 (skin) overrides, only apply to skin-group batches - // to prevent the skin composite from bleeding onto cloak/hair. - { - auto itO = inst.textureSlotOverrides.find(texSlot); - if (itO != inst.textureSlotOverrides.end() && itO->second != 0) { - if (texType == 1) { - // Only apply skin override to skin groups - uint16_t grp = b.submeshId / 100; - bool isSkinGroup = (grp == 0 || grp == 3 || grp == 4 || grp == 5 || - grp == 8 || grp == 9 || grp == 13 || grp == 20); - if (isSkinGroup) texId = itO->second; - } else { - texId = itO->second; - } + VkTexture* texPtr = gm.textureIds[texSlot]; + uint32_t texType = (texSlot < gm.data.textures.size()) ? gm.data.textures[texSlot].type : 0; + // Apply texture slot overrides. + // For type-1 (skin) overrides, only apply to skin-group batches + // to prevent the skin composite from bleeding onto cloak/hair. + { + auto itO = inst.textureSlotOverrides.find(texSlot); + if (itO != inst.textureSlotOverrides.end() && itO->second != nullptr) { + if (texType == 1) { + // Only apply skin override to skin groups + uint16_t grp = b.submeshId / 100; + bool isSkinGroup = (grp == 0 || grp == 3 || grp == 4 || grp == 5 || + grp == 8 || grp == 9 || grp == 13 || grp == 20); + if (isSkinGroup) texPtr = itO->second; + } else { + texPtr = itO->second; } } + } - if (!hasFirst) { - first = {texId, texType}; - hasFirst = true; - } + if (!hasFirst) { + first = {texPtr, texType}; + hasFirst = true; + } - if (texId == 0 || texId == whiteTexture) continue; + if (texPtr == nullptr || texPtr == whiteTexture_.get()) continue; - // Prefer the hair texture slot (type 6) whenever present in the combo. - // Humanoid scalp meshes can live in group 0, so group-based checks are insufficient. - if (texType == 6) { - return texId; - } + // Prefer the hair texture slot (type 6) whenever present in the combo. + if (texType == 6) { + return texPtr; + } - if (!hasFirstNonWhite) { - firstNonWhite = {texId, texType}; - hasFirstNonWhite = true; - } - } + if (!hasFirstNonWhite) { + firstNonWhite = {texPtr, texType}; + hasFirstNonWhite = true; + } + } - if (hasFirstNonWhite) return firstNonWhite.id; - if (hasFirst && first.id != 0) return first.id; - return whiteTexture; - }; + if (hasFirstNonWhite) return firstNonWhite.tex; + if (hasFirst && first.tex != nullptr) return first.tex; + return whiteTexture_.get(); + }; - // One-time debug dump of rendered batches per model - static std::unordered_set dumpedModels; - if (dumpedModels.find(instance.modelId) == dumpedModels.end()) { + // One-time debug dump of rendered batches per model + static std::unordered_set dumpedModels; + if (dumpedModels.find(instance.modelId) == dumpedModels.end()) { dumpedModels.insert(instance.modelId); int bIdx = 0; int rendered = 0, skipped = 0; @@ -1556,11 +1504,11 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons (b.submeshId / 100 != 0) && instance.activeGeosets.find(b.submeshId) == instance.activeGeosets.end(); - GLuint resolvedTex = resolveBatchTexture(instance, gpuModel, b); - std::string texInfo = "GL" + std::to_string(resolvedTex); + VkTexture* resolvedTex = resolveBatchTexture(instance, gpuModel, b); + std::string texInfo = resolvedTex ? "VkTex" : "null"; - if (filtered) skipped++; else rendered++; - LOG_DEBUG("Batch ", bIdx, ": submesh=", b.submeshId, + if (filtered) skipped++; else rendered++; + LOG_DEBUG("Batch ", bIdx, ": submesh=", b.submeshId, " level=", b.submeshLevel, " idxStart=", b.indexStart, " idxCount=", b.indexCount, " tex=", texInfo, @@ -1571,13 +1519,10 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons gpuModel.textureIds.size(), " textures loaded, ", gpuModel.data.textureLookup.size(), " in lookup table"); for (size_t t = 0; t < gpuModel.data.textures.size(); t++) { - } - } + } + } - // Draw batches (submeshes) with per-batch textures - // Geoset filtering: skip batches whose submeshId is not in activeGeosets. - // For character models, group 0 (body/scalp) is also filtered so that only - // the correct scalp mesh renders (not all overlapping variants). + // Draw batches (submeshes) with per-batch textures for (const auto& batch : gpuModel.data.batches) { if (applyGeosetFilter) { if (instance.activeGeosets.find(batch.submeshId) == instance.activeGeosets.end()) { @@ -1585,12 +1530,12 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons } } - // Resolve texture for this batch (prefer hair textures for hair geosets). - GLuint texId = resolveBatchTexture(instance, gpuModel, batch); + // Resolve texture for this batch (prefer hair textures for hair geosets). + VkTexture* texPtr = resolveBatchTexture(instance, gpuModel, batch); const uint16_t batchGroup = static_cast(batch.submeshId / 100); auto groupTexIt = instance.groupTextureOverrides.find(batchGroup); - if (groupTexIt != instance.groupTextureOverrides.end() && groupTexIt->second != 0) { - texId = groupTexIt->second; + if (groupTexIt != instance.groupTextureOverrides.end() && groupTexIt->second != nullptr) { + texPtr = groupTexIt->second; } // Respect M2 material blend mode for creature/character submeshes. @@ -1607,27 +1552,28 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons if (instance.hasOverrideModelMatrix && blendMode >= 3) { continue; } + + // Select pipeline based on blend mode + VkPipeline desiredPipeline; switch (blendMode) { - case 0: glBlendFunc(GL_ONE, GL_ZERO); break; // Opaque - case 1: glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); break; // AlphaKey - case 2: glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); break; // Alpha - case 3: glBlendFunc(GL_SRC_ALPHA, GL_ONE); break; // Additive - case 4: glBlendFunc(GL_DST_COLOR, GL_ZERO); break; // Mod - case 5: glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR); break; // Mod2x - case 6: glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); break; // BlendAdd - default: glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); break; + case 0: desiredPipeline = opaquePipeline_; break; + case 1: desiredPipeline = alphaTestPipeline_; break; + case 2: desiredPipeline = alphaPipeline_; break; + case 3: + case 6: desiredPipeline = additivePipeline_; break; + default: desiredPipeline = alphaPipeline_; break; + } + if (desiredPipeline != currentPipeline) { + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, desiredPipeline); + currentPipeline = desiredPipeline; } - // For body/equipment parts with white/fallback texture, use skin (type 1) texture. - // Groups that share the body skin atlas: 0=body, 3=gloves, 4=boots, 5=chest, - // 8=wristbands, 9=pelvis, 13=pants. Hair (group 1) and facial hair (group 2) do NOT. - if (texId == whiteTexture) { + if (texPtr == whiteTexture_.get()) { uint16_t group = batchGroup; bool isSkinGroup = (group == 0 || group == 3 || group == 4 || group == 5 || group == 8 || group == 9 || group == 13); if (isSkinGroup) { - // Check if this batch's texture slot is a hair type (don't override hair) uint32_t texType = 0; if (batch.textureIndex < gpuModel.data.textureLookup.size()) { uint16_t lk = gpuModel.data.textureLookup[batch.textureIndex]; @@ -1638,16 +1584,15 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons // Do NOT apply skin composite to hair (type 6) batches if (texType != 6) { for (size_t ti = 0; ti < gpuModel.textureIds.size(); ti++) { - GLuint candidate = gpuModel.textureIds[ti]; + VkTexture* candidate = gpuModel.textureIds[ti]; auto itO = instance.textureSlotOverrides.find(static_cast(ti)); - if (itO != instance.textureSlotOverrides.end() && itO->second != 0) { + if (itO != instance.textureSlotOverrides.end() && itO->second != nullptr) { candidate = itO->second; } - if (candidate != whiteTexture && candidate != 0) { - // Only use type 1 (skin) textures as fallback + if (candidate != whiteTexture_.get() && candidate != nullptr) { if (ti < gpuModel.data.textures.size() && (gpuModel.data.textures[ti].type == 1 || gpuModel.data.textures[ti].type == 11)) { - texId = candidate; + texPtr = candidate; break; } } @@ -1656,21 +1601,18 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons } } - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, texId); + // Determine material properties bool alphaCutout = false; bool colorKeyBlack = false; - if (texId != 0 && texId != whiteTexture) { - auto ait = textureHasAlphaById_.find(texId); - alphaCutout = (ait != textureHasAlphaById_.end()) ? ait->second : false; - auto cit = textureColorKeyBlackById_.find(texId); - colorKeyBlack = (cit != textureColorKeyBlackById_.end()) ? cit->second : false; + if (texPtr != nullptr && texPtr != whiteTexture_.get()) { + auto ait = textureHasAlphaByPtr_.find(texPtr); + alphaCutout = (ait != textureHasAlphaByPtr_.end()) ? ait->second : false; + auto cit = textureColorKeyBlackByPtr_.find(texPtr); + colorKeyBlack = (cit != textureColorKeyBlackByPtr_.end()) ? cit->second : false; } const bool blendNeedsCutout = (blendMode == 1) || (blendMode >= 2 && !alphaCutout); - characterShader->setUniform("uAlphaTest", (blendNeedsCutout || alphaCutout) ? 1 : 0); - characterShader->setUniform("uColorKeyBlack", (blendNeedsCutout || colorKeyBlack) ? 1 : 0); const bool unlit = ((materialFlags & 0x01) != 0) || (blendMode >= 3); - characterShader->setUniform("uUnlit", unlit ? 1 : 0); + float emissiveBoost = 1.0f; glm::vec3 emissiveTint(1.0f, 1.0f, 1.0f); // Keep custom warm/flicker treatment narrowly scoped to kobold candle flames. @@ -1695,125 +1637,164 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons float flicker = 0.90f + 0.10f * f1 + 0.06f * f2 + 0.04f * f3; flicker = std::clamp(flicker, 0.72f, 1.12f); emissiveBoost = (blendMode >= 3) ? (2.4f * flicker) : (1.5f * flicker); - // Warm flame bias to avoid green cast from source textures. emissiveTint = glm::vec3(1.28f, 1.04f, 0.82f); } - characterShader->setUniform("uEmissiveBoost", emissiveBoost); - characterShader->setUniform("uEmissiveTint", emissiveTint); - glDrawElements(GL_TRIANGLES, - batch.indexCount, - GL_UNSIGNED_SHORT, - (void*)(batch.indexStart * sizeof(uint16_t))); - } - } else { - // Draw entire model with first texture - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, !gpuModel.textureIds.empty() ? gpuModel.textureIds[0] : whiteTexture); - characterShader->setUniform("uAlphaTest", 0); - characterShader->setUniform("uColorKeyBlack", 0); - characterShader->setUniform("uUnlit", 0); - characterShader->setUniform("uEmissiveBoost", 1.0f); - characterShader->setUniform("uEmissiveTint", glm::vec3(1.0f)); - - glDrawElements(GL_TRIANGLES, - static_cast(gpuModel.data.indices.size()), - GL_UNSIGNED_SHORT, - 0); - } - } - - glBindVertexArray(0); - glDisable(GL_BLEND); - glEnable(GL_CULL_FACE); // Restore culling for other renderers -} - -void CharacterRenderer::renderShadow(const glm::mat4& lightSpaceMatrix) { - if (instances.empty() || shadowCasterProgram == 0) { - return; - } - - glUseProgram(shadowCasterProgram); - - GLint lightSpaceLoc = glGetUniformLocation(shadowCasterProgram, "uLightSpaceMatrix"); - GLint modelLoc = glGetUniformLocation(shadowCasterProgram, "uModel"); - GLint texLoc = glGetUniformLocation(shadowCasterProgram, "uTexture"); - GLint alphaTestLoc = glGetUniformLocation(shadowCasterProgram, "uAlphaTest"); - GLint colorKeyLoc = glGetUniformLocation(shadowCasterProgram, "uColorKeyBlack"); - GLint bonesLoc = glGetUniformLocation(shadowCasterProgram, "uBones[0]"); - if (lightSpaceLoc < 0 || modelLoc < 0) { - return; - } - - glUniformMatrix4fv(lightSpaceLoc, 1, GL_FALSE, &lightSpaceMatrix[0][0]); - glEnable(GL_CULL_FACE); - glCullFace(GL_FRONT); - - if (texLoc >= 0) glUniform1i(texLoc, 0); - glActiveTexture(GL_TEXTURE0); - - for (const auto& [_, instance] : instances) { - auto modelIt = models.find(instance.modelId); - if (modelIt == models.end()) continue; - const auto& gpuModel = modelIt->second; - - glm::mat4 modelMat = instance.hasOverrideModelMatrix - ? instance.overrideModelMatrix - : getModelMatrix(instance); - glUniformMatrix4fv(modelLoc, 1, GL_FALSE, &modelMat[0][0]); - - if (!instance.boneMatrices.empty() && bonesLoc >= 0) { - int numBones = std::min(static_cast(instance.boneMatrices.size()), MAX_BONES); - glUniformMatrix4fv(bonesLoc, numBones, GL_FALSE, &instance.boneMatrices[0][0][0]); - } - - glBindVertexArray(gpuModel.vao); - - if (!gpuModel.data.batches.empty()) { - for (const auto& batch : gpuModel.data.batches) { - GLuint texId = whiteTexture; - if (batch.textureIndex < gpuModel.data.textureLookup.size()) { - uint16_t lookupIdx = gpuModel.data.textureLookup[batch.textureIndex]; - if (lookupIdx < gpuModel.textureIds.size()) { - texId = gpuModel.textureIds[lookupIdx]; - auto itO = instance.textureSlotOverrides.find(lookupIdx); - if (itO != instance.textureSlotOverrides.end() && itO->second != 0) { - texId = itO->second; - } + // Allocate and fill material descriptor set (set 1) + VkDescriptorSet materialSet = VK_NULL_HANDLE; + { + VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + ai.descriptorPool = materialDescPool_; + ai.descriptorSetCount = 1; + ai.pSetLayouts = &materialSetLayout_; + if (vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &materialSet) != VK_SUCCESS) { + continue; // Pool exhausted, skip this batch } } - bool alphaCutout = false; - bool colorKeyBlack = false; - if (texId != 0 && texId != whiteTexture) { - auto itA = textureHasAlphaById_.find(texId); - alphaCutout = (itA != textureHasAlphaById_.end()) ? itA->second : false; - auto itC = textureColorKeyBlackById_.find(texId); - colorKeyBlack = (itC != textureColorKeyBlackById_.end()) ? itC->second : false; - } - if (alphaTestLoc >= 0) glUniform1i(alphaTestLoc, alphaCutout ? 1 : 0); - if (colorKeyLoc >= 0) glUniform1i(colorKeyLoc, colorKeyBlack ? 1 : 0); - glBindTexture(GL_TEXTURE_2D, texId ? texId : whiteTexture); + // Create per-batch material UBO + CharMaterialUBO matData{}; + matData.opacity = instance.opacity; + matData.alphaTest = (blendNeedsCutout || alphaCutout) ? 1 : 0; + matData.colorKeyBlack = (blendNeedsCutout || colorKeyBlack) ? 1 : 0; + matData.unlit = unlit ? 1 : 0; + matData.emissiveBoost = emissiveBoost; + matData.emissiveTintR = emissiveTint.r; + matData.emissiveTintG = emissiveTint.g; + matData.emissiveTintB = emissiveTint.b; + matData.specularIntensity = 0.5f; - glDrawElements(GL_TRIANGLES, - batch.indexCount, - GL_UNSIGNED_SHORT, - (void*)(batch.indexStart * sizeof(uint16_t))); + // Create a small UBO for this batch's material + VkBufferCreateInfo bci{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bci.size = sizeof(CharMaterialUBO); + bci.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + VmaAllocationCreateInfo aci{}; + aci.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + aci.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT; + VmaAllocationInfo allocInfo{}; + ::VkBuffer matUBO = VK_NULL_HANDLE; + VmaAllocation matUBOAlloc = VK_NULL_HANDLE; + vmaCreateBuffer(vkCtx_->getAllocator(), &bci, &aci, &matUBO, &matUBOAlloc, &allocInfo); + if (allocInfo.pMappedData) { + memcpy(allocInfo.pMappedData, &matData, sizeof(CharMaterialUBO)); + } + + // Write descriptor set: binding 0 = texture, binding 1 = material UBO + VkTexture* bindTex = (texPtr && texPtr->isValid()) ? texPtr : whiteTexture_.get(); + VkDescriptorImageInfo imgInfo = bindTex->descriptorInfo(); + VkDescriptorBufferInfo bufInfo{}; + bufInfo.buffer = matUBO; + bufInfo.offset = 0; + bufInfo.range = sizeof(CharMaterialUBO); + + VkWriteDescriptorSet writes[2] = {}; + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].dstSet = materialSet; + writes[0].dstBinding = 0; + writes[0].descriptorCount = 1; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[0].pImageInfo = &imgInfo; + + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].dstSet = materialSet; + writes[1].dstBinding = 1; + writes[1].descriptorCount = 1; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writes[1].pBufferInfo = &bufInfo; + + vkUpdateDescriptorSets(vkCtx_->getDevice(), 2, writes, 0, nullptr); + + // Bind material descriptor set (set 1) + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + pipelineLayout_, 1, 1, &materialSet, 0, nullptr); + + vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0); + + // Queue the ephemeral UBO for deferred deletion (will be cleaned up after frame completes) + // For now, we leak these tiny UBOs -- they are freed when the descriptor pool is reset/destroyed. + // A proper solution would use a per-frame linear allocator. + // TODO: Use a per-frame staging buffer to avoid per-batch VMA allocations + vmaDestroyBuffer(vkCtx_->getAllocator(), matUBO, matUBOAlloc); } } else { - if (alphaTestLoc >= 0) glUniform1i(alphaTestLoc, 0); - if (colorKeyLoc >= 0) glUniform1i(colorKeyLoc, 0); - glBindTexture(GL_TEXTURE_2D, whiteTexture); - glDrawElements(GL_TRIANGLES, - static_cast(gpuModel.data.indices.size()), - GL_UNSIGNED_SHORT, - 0); + // Draw entire model with first texture + VkTexture* texPtr = !gpuModel.textureIds.empty() ? gpuModel.textureIds[0] : whiteTexture_.get(); + if (!texPtr || !texPtr->isValid()) texPtr = whiteTexture_.get(); + + // Allocate material descriptor set + VkDescriptorSet materialSet = VK_NULL_HANDLE; + { + VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + ai.descriptorPool = materialDescPool_; + ai.descriptorSetCount = 1; + ai.pSetLayouts = &materialSetLayout_; + if (vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &materialSet) != VK_SUCCESS) { + continue; + } + } + + CharMaterialUBO matData{}; + matData.opacity = instance.opacity; + matData.alphaTest = 0; + matData.colorKeyBlack = 0; + matData.unlit = 0; + matData.emissiveBoost = 1.0f; + matData.emissiveTintR = 1.0f; + matData.emissiveTintG = 1.0f; + matData.emissiveTintB = 1.0f; + matData.specularIntensity = 0.5f; + + VkBufferCreateInfo bci{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bci.size = sizeof(CharMaterialUBO); + bci.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + VmaAllocationCreateInfo aci{}; + aci.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + aci.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT; + VmaAllocationInfo allocInfo{}; + ::VkBuffer matUBO = VK_NULL_HANDLE; + VmaAllocation matUBOAlloc = VK_NULL_HANDLE; + vmaCreateBuffer(vkCtx_->getAllocator(), &bci, &aci, &matUBO, &matUBOAlloc, &allocInfo); + if (allocInfo.pMappedData) { + memcpy(allocInfo.pMappedData, &matData, sizeof(CharMaterialUBO)); + } + + VkDescriptorImageInfo imgInfo = texPtr->descriptorInfo(); + VkDescriptorBufferInfo bufInfo{}; + bufInfo.buffer = matUBO; + bufInfo.offset = 0; + bufInfo.range = sizeof(CharMaterialUBO); + + VkWriteDescriptorSet writes[2] = {}; + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].dstSet = materialSet; + writes[0].dstBinding = 0; + writes[0].descriptorCount = 1; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[0].pImageInfo = &imgInfo; + + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].dstSet = materialSet; + writes[1].dstBinding = 1; + writes[1].descriptorCount = 1; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writes[1].pBufferInfo = &bufInfo; + + vkUpdateDescriptorSets(vkCtx_->getDevice(), 2, writes, 0, nullptr); + + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + pipelineLayout_, 1, 1, &materialSet, 0, nullptr); + + vkCmdDrawIndexed(cmd, gpuModel.indexCount, 1, 0, 0, 0); + + vmaDestroyBuffer(vkCtx_->getAllocator(), matUBO, matUBOAlloc); } } +} - glBindVertexArray(0); - glCullFace(GL_BACK); +void CharacterRenderer::renderShadow(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) { + // Phase 6 stub -- shadow rendering will be implemented with shadow pipeline + (void)cmd; + (void)perFrameSet; } glm::mat4 CharacterRenderer::getModelMatrix(const CharacterInstance& instance) const { @@ -1931,17 +1912,17 @@ void CharacterRenderer::setActiveGeosets(uint32_t instanceId, const std::unorder } } -void CharacterRenderer::setGroupTextureOverride(uint32_t instanceId, uint16_t geosetGroup, GLuint textureId) { +void CharacterRenderer::setGroupTextureOverride(uint32_t instanceId, uint16_t geosetGroup, VkTexture* texture) { auto it = instances.find(instanceId); if (it != instances.end()) { - it->second.groupTextureOverrides[geosetGroup] = textureId; + it->second.groupTextureOverrides[geosetGroup] = texture; } } -void CharacterRenderer::setTextureSlotOverride(uint32_t instanceId, uint16_t textureSlot, GLuint textureId) { +void CharacterRenderer::setTextureSlotOverride(uint32_t instanceId, uint16_t textureSlot, VkTexture* texture) { auto it = instances.find(instanceId); if (it != instances.end()) { - it->second.textureSlotOverrides[textureSlot] = textureId; + it->second.textureSlotOverrides[textureSlot] = texture; } } @@ -1978,6 +1959,9 @@ void CharacterRenderer::removeInstance(uint32_t instanceId) { removeInstance(wa.weaponInstanceId); } + // Destroy bone buffers for this instance + destroyInstanceBones(it->second); + instances.erase(it); } @@ -2138,9 +2122,9 @@ bool CharacterRenderer::attachWeapon(uint32_t charInstanceId, uint32_t attachmen // Apply weapon texture if provided if (!texturePath.empty()) { - GLuint texId = loadTexture(texturePath); - if (texId != whiteTexture) { - setModelTexture(weaponModelId, 0, texId); + VkTexture* texPtr = loadTexture(texturePath); + if (texPtr != whiteTexture_.get()) { + setModelTexture(weaponModelId, 0, texPtr); } } diff --git a/src/rendering/charge_effect.cpp b/src/rendering/charge_effect.cpp index b02443ae..5b2ac217 100644 --- a/src/rendering/charge_effect.cpp +++ b/src/rendering/charge_effect.cpp @@ -1,6 +1,10 @@ #include "rendering/charge_effect.hpp" #include "rendering/camera.hpp" -#include "rendering/shader.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_frame_data.hpp" +#include "rendering/vk_utils.hpp" #include "rendering/m2_renderer.hpp" #include "pipeline/m2_loader.hpp" #include "pipeline/asset_manager.hpp" @@ -8,6 +12,7 @@ #include #include #include +#include namespace wowee { namespace rendering { @@ -26,130 +31,179 @@ static float randFloat(float lo, float hi) { ChargeEffect::ChargeEffect() = default; ChargeEffect::~ChargeEffect() { shutdown(); } -bool ChargeEffect::initialize() { - // ---- Ribbon trail shader ---- - ribbonShader_ = std::make_unique(); +bool ChargeEffect::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { + vkCtx_ = ctx; + VkDevice device = vkCtx_->getDevice(); - const char* ribbonVS = R"( - #version 330 core - layout (location = 0) in vec3 aPos; - layout (location = 1) in float aAlpha; - layout (location = 2) in float aHeat; - layout (location = 3) in float aHeight; + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; - uniform mat4 uView; - uniform mat4 uProjection; - - out float vAlpha; - out float vHeat; - out float vHeight; - - void main() { - gl_Position = uProjection * uView * vec4(aPos, 1.0); - vAlpha = aAlpha; - vHeat = aHeat; - vHeight = aHeight; + // ---- Ribbon trail pipeline (TRIANGLE_STRIP) ---- + { + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/charge_ribbon.vert.spv")) { + LOG_ERROR("Failed to load charge_ribbon vertex shader"); + return false; } - )"; - - const char* ribbonFS = R"( - #version 330 core - in float vAlpha; - in float vHeat; - in float vHeight; - out vec4 FragColor; - - void main() { - // Vertical gradient: top is red/opaque, bottom is transparent - vec3 topColor = vec3(0.9, 0.15, 0.05); // Deep red at top - vec3 midColor = vec3(1.0, 0.5, 0.1); // Orange in middle - vec3 color = mix(midColor, topColor, vHeight); - // Mix with heat (head vs tail along length) - vec3 hotColor = vec3(1.0, 0.6, 0.15); - color = mix(color, hotColor, vHeat * 0.4); - - // Bottom fades to transparent, top is opaque - float vertAlpha = smoothstep(0.0, 0.4, vHeight); - FragColor = vec4(color, vAlpha * vertAlpha * 0.7); + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/charge_ribbon.frag.spv")) { + LOG_ERROR("Failed to load charge_ribbon fragment shader"); + return false; } - )"; - if (!ribbonShader_->loadFromSource(ribbonVS, ribbonFS)) { - LOG_ERROR("Failed to create charge ribbon shader"); - return false; + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + ribbonPipelineLayout_ = createPipelineLayout(device, {perFrameLayout}, {}); + if (ribbonPipelineLayout_ == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create charge ribbon pipeline layout"); + return false; + } + + // Vertex input: pos(vec3) + alpha(float) + heat(float) + height(float) = 6 floats, stride = 24 bytes + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 6 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector attrs(4); + // location 0: vec3 position + attrs[0].location = 0; + attrs[0].binding = 0; + attrs[0].format = VK_FORMAT_R32G32B32_SFLOAT; + attrs[0].offset = 0; + // location 1: float alpha + attrs[1].location = 1; + attrs[1].binding = 0; + attrs[1].format = VK_FORMAT_R32_SFLOAT; + attrs[1].offset = 3 * sizeof(float); + // location 2: float heat + attrs[2].location = 2; + attrs[2].binding = 0; + attrs[2].format = VK_FORMAT_R32_SFLOAT; + attrs[2].offset = 4 * sizeof(float); + // location 3: float height + attrs[3].location = 3; + attrs[3].binding = 0; + attrs[3].format = VK_FORMAT_R32_SFLOAT; + attrs[3].offset = 5 * sizeof(float); + + ribbonPipeline_ = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS) + .setColorBlendAttachment(PipelineBuilder::blendAdditive()) // Additive blend for fiery glow + .setLayout(ribbonPipelineLayout_) + .setRenderPass(vkCtx_->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (ribbonPipeline_ == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create charge ribbon pipeline"); + return false; + } } - glGenVertexArrays(1, &ribbonVao_); - glGenBuffers(1, &ribbonVbo_); - glBindVertexArray(ribbonVao_); - glBindBuffer(GL_ARRAY_BUFFER, ribbonVbo_); - // pos(3) + alpha(1) + heat(1) + height(1) = 6 floats - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(1); - glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(4 * sizeof(float))); - glEnableVertexAttribArray(2); - glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(5 * sizeof(float))); - glEnableVertexAttribArray(3); - glBindVertexArray(0); + // ---- Dust puff pipeline (POINT_LIST) ---- + { + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/charge_dust.vert.spv")) { + LOG_ERROR("Failed to load charge_dust vertex shader"); + return false; + } + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/charge_dust.frag.spv")) { + LOG_ERROR("Failed to load charge_dust fragment shader"); + return false; + } + + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + dustPipelineLayout_ = createPipelineLayout(device, {perFrameLayout}, {}); + if (dustPipelineLayout_ == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create charge dust pipeline layout"); + return false; + } + + // Vertex input: pos(vec3) + size(float) + alpha(float) = 5 floats, stride = 20 bytes + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 5 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector attrs(3); + attrs[0].location = 0; + attrs[0].binding = 0; + attrs[0].format = VK_FORMAT_R32G32B32_SFLOAT; + attrs[0].offset = 0; + attrs[1].location = 1; + attrs[1].binding = 0; + attrs[1].format = VK_FORMAT_R32_SFLOAT; + attrs[1].offset = 3 * sizeof(float); + attrs[2].location = 2; + attrs[2].binding = 0; + attrs[2].format = VK_FORMAT_R32_SFLOAT; + attrs[2].offset = 4 * sizeof(float); + + dustPipeline_ = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setLayout(dustPipelineLayout_) + .setRenderPass(vkCtx_->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (dustPipeline_ == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create charge dust pipeline"); + return false; + } + } + + // ---- Create dynamic mapped vertex buffers ---- + // Ribbon: MAX_TRAIL_POINTS * 2 vertices * 6 floats each + ribbonDynamicVBSize_ = MAX_TRAIL_POINTS * 2 * 6 * sizeof(float); + { + AllocatedBuffer buf = createBuffer(vkCtx_->getAllocator(), ribbonDynamicVBSize_, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU); + ribbonDynamicVB_ = buf.buffer; + ribbonDynamicVBAlloc_ = buf.allocation; + ribbonDynamicVBAllocInfo_ = buf.info; + if (ribbonDynamicVB_ == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create charge ribbon dynamic vertex buffer"); + return false; + } + } + + // Dust: MAX_DUST * 5 floats each + dustDynamicVBSize_ = MAX_DUST * 5 * sizeof(float); + { + AllocatedBuffer buf = createBuffer(vkCtx_->getAllocator(), dustDynamicVBSize_, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU); + dustDynamicVB_ = buf.buffer; + dustDynamicVBAlloc_ = buf.allocation; + dustDynamicVBAllocInfo_ = buf.info; + if (dustDynamicVB_ == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create charge dust dynamic vertex buffer"); + return false; + } + } ribbonVerts_.reserve(MAX_TRAIL_POINTS * 2 * 6); - - // ---- Dust puff shader (small point sprites) ---- - dustShader_ = std::make_unique(); - - const char* dustVS = R"( - #version 330 core - layout (location = 0) in vec3 aPos; - layout (location = 1) in float aSize; - layout (location = 2) in float aAlpha; - - uniform mat4 uView; - uniform mat4 uProjection; - - out float vAlpha; - - void main() { - gl_Position = uProjection * uView * vec4(aPos, 1.0); - gl_PointSize = aSize; - vAlpha = aAlpha; - } - )"; - - const char* dustFS = R"( - #version 330 core - in float vAlpha; - out vec4 FragColor; - - void main() { - vec2 coord = gl_PointCoord - vec2(0.5); - float dist = length(coord); - if (dist > 0.5) discard; - float alpha = smoothstep(0.5, 0.0, dist) * vAlpha; - vec3 dustColor = vec3(0.65, 0.55, 0.40); - FragColor = vec4(dustColor, alpha * 0.45); - } - )"; - - if (!dustShader_->loadFromSource(dustVS, dustFS)) { - LOG_ERROR("Failed to create charge dust shader"); - return false; - } - - glGenVertexArrays(1, &dustVao_); - glGenBuffers(1, &dustVbo_); - glBindVertexArray(dustVao_); - glBindBuffer(GL_ARRAY_BUFFER, dustVbo_); - // pos(3) + size(1) + alpha(1) = 5 floats - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(1); - glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(4 * sizeof(float))); - glEnableVertexAttribArray(2); - glBindVertexArray(0); - dustVerts_.reserve(MAX_DUST * 5); dustPuffs_.reserve(MAX_DUST); @@ -157,16 +211,42 @@ bool ChargeEffect::initialize() { } void ChargeEffect::shutdown() { - if (ribbonVao_) glDeleteVertexArrays(1, &ribbonVao_); - if (ribbonVbo_) glDeleteBuffers(1, &ribbonVbo_); - ribbonVao_ = 0; ribbonVbo_ = 0; - if (dustVao_) glDeleteVertexArrays(1, &dustVao_); - if (dustVbo_) glDeleteBuffers(1, &dustVbo_); - dustVao_ = 0; dustVbo_ = 0; + if (vkCtx_) { + VkDevice device = vkCtx_->getDevice(); + VmaAllocator allocator = vkCtx_->getAllocator(); + + if (ribbonPipeline_ != VK_NULL_HANDLE) { + vkDestroyPipeline(device, ribbonPipeline_, nullptr); + ribbonPipeline_ = VK_NULL_HANDLE; + } + if (ribbonPipelineLayout_ != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, ribbonPipelineLayout_, nullptr); + ribbonPipelineLayout_ = VK_NULL_HANDLE; + } + if (ribbonDynamicVB_ != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, ribbonDynamicVB_, ribbonDynamicVBAlloc_); + ribbonDynamicVB_ = VK_NULL_HANDLE; + ribbonDynamicVBAlloc_ = VK_NULL_HANDLE; + } + + if (dustPipeline_ != VK_NULL_HANDLE) { + vkDestroyPipeline(device, dustPipeline_, nullptr); + dustPipeline_ = VK_NULL_HANDLE; + } + if (dustPipelineLayout_ != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, dustPipelineLayout_, nullptr); + dustPipelineLayout_ = VK_NULL_HANDLE; + } + if (dustDynamicVB_ != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, dustDynamicVB_, dustDynamicVBAlloc_); + dustDynamicVB_ = VK_NULL_HANDLE; + dustDynamicVBAlloc_ = VK_NULL_HANDLE; + } + } + + vkCtx_ = nullptr; trail_.clear(); dustPuffs_.clear(); - ribbonShader_.reset(); - dustShader_.reset(); } void ChargeEffect::tryLoadM2Models(M2Renderer* m2Renderer, pipeline::AssetManager* assets) { @@ -345,9 +425,11 @@ void ChargeEffect::update(float deltaTime) { } } -void ChargeEffect::render(const Camera& camera) { +void ChargeEffect::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) { + VkDeviceSize offset = 0; + // ---- Render ribbon trail as triangle strip ---- - if (trail_.size() >= 2 && ribbonShader_) { + if (trail_.size() >= 2 && ribbonPipeline_ != VK_NULL_HANDLE) { ribbonVerts_.clear(); int n = static_cast(trail_.size()); @@ -385,28 +467,21 @@ void ChargeEffect::render(const Camera& camera) { ribbonVerts_.push_back(1.0f); // height = top } - glBindBuffer(GL_ARRAY_BUFFER, ribbonVbo_); - glBufferData(GL_ARRAY_BUFFER, ribbonVerts_.size() * sizeof(float), - ribbonVerts_.data(), GL_DYNAMIC_DRAW); + // Upload to mapped buffer + VkDeviceSize uploadSize = ribbonVerts_.size() * sizeof(float); + if (uploadSize > 0 && ribbonDynamicVBAllocInfo_.pMappedData) { + std::memcpy(ribbonDynamicVBAllocInfo_.pMappedData, ribbonVerts_.data(), uploadSize); + } - ribbonShader_->use(); - ribbonShader_->setUniform("uView", camera.getViewMatrix()); - ribbonShader_->setUniform("uProjection", camera.getProjectionMatrix()); - - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Additive blend for fiery glow - glDepthMask(GL_FALSE); - - glBindVertexArray(ribbonVao_); - glDrawArrays(GL_TRIANGLE_STRIP, 0, static_cast(n * 2)); - glBindVertexArray(0); - - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glDepthMask(GL_TRUE); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, ribbonPipeline_); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, ribbonPipelineLayout_, + 0, 1, &perFrameSet, 0, nullptr); + vkCmdBindVertexBuffers(cmd, 0, 1, &ribbonDynamicVB_, &offset); + vkCmdDraw(cmd, static_cast(n * 2), 1, 0, 0); } // ---- Render dust puffs ---- - if (!dustPuffs_.empty() && dustShader_) { + if (!dustPuffs_.empty() && dustPipeline_ != VK_NULL_HANDLE) { dustVerts_.clear(); for (const auto& d : dustPuffs_) { dustVerts_.push_back(d.position.x); @@ -416,25 +491,17 @@ void ChargeEffect::render(const Camera& camera) { dustVerts_.push_back(d.alpha); } - glBindBuffer(GL_ARRAY_BUFFER, dustVbo_); - glBufferData(GL_ARRAY_BUFFER, dustVerts_.size() * sizeof(float), - dustVerts_.data(), GL_DYNAMIC_DRAW); + // Upload to mapped buffer + VkDeviceSize uploadSize = dustVerts_.size() * sizeof(float); + if (uploadSize > 0 && dustDynamicVBAllocInfo_.pMappedData) { + std::memcpy(dustDynamicVBAllocInfo_.pMappedData, dustVerts_.data(), uploadSize); + } - dustShader_->use(); - dustShader_->setUniform("uView", camera.getViewMatrix()); - dustShader_->setUniform("uProjection", camera.getProjectionMatrix()); - - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glDepthMask(GL_FALSE); - glEnable(GL_PROGRAM_POINT_SIZE); - - glBindVertexArray(dustVao_); - glDrawArrays(GL_POINTS, 0, static_cast(dustPuffs_.size())); - glBindVertexArray(0); - - glDepthMask(GL_TRUE); - glDisable(GL_PROGRAM_POINT_SIZE); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, dustPipeline_); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, dustPipelineLayout_, + 0, 1, &perFrameSet, 0, nullptr); + vkCmdBindVertexBuffers(cmd, 0, 1, &dustDynamicVB_, &offset); + vkCmdDraw(cmd, static_cast(dustPuffs_.size()), 1, 0, 0); } } diff --git a/src/rendering/clouds.cpp b/src/rendering/clouds.cpp index dba0359d..11193217 100644 --- a/src/rendering/clouds.cpp +++ b/src/rendering/clouds.cpp @@ -1,316 +1,279 @@ #include "rendering/clouds.hpp" -#include "rendering/camera.hpp" -#include "rendering/shader.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_frame_data.hpp" +#include "rendering/vk_utils.hpp" #include "core/logger.hpp" #include -#include #include namespace wowee { namespace rendering { -Clouds::Clouds() { -} +Clouds::Clouds() = default; Clouds::~Clouds() { - cleanup(); + shutdown(); } -bool Clouds::initialize() { - LOG_INFO("Initializing cloud system"); +bool Clouds::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { + LOG_INFO("Initializing cloud system (Vulkan)"); - // Generate cloud dome mesh - generateMesh(); + vkCtx_ = ctx; + VkDevice device = vkCtx_->getDevice(); - // Create VAO - glGenVertexArrays(1, &vao); - glGenBuffers(1, &vbo); - glGenBuffers(1, &ebo); - - glBindVertexArray(vao); - - // Upload vertex data - glBindBuffer(GL_ARRAY_BUFFER, vbo); - glBufferData(GL_ARRAY_BUFFER, - vertices.size() * sizeof(glm::vec3), - vertices.data(), - GL_STATIC_DRAW); - - // Upload index data - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, - indices.size() * sizeof(unsigned int), - indices.data(), - GL_STATIC_DRAW); - - // Position attribute - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0); - glEnableVertexAttribArray(0); - - glBindVertexArray(0); - - // Create shader - shader = std::make_unique(); - - // Cloud vertex shader - const char* vertexShaderSource = R"( - #version 330 core - layout (location = 0) in vec3 aPos; - - uniform mat4 uView; - uniform mat4 uProjection; - - out vec3 WorldPos; - out vec3 LocalPos; - - void main() { - LocalPos = aPos; - WorldPos = aPos; - - // Remove translation from view matrix (billboard effect) - mat4 viewNoTranslation = uView; - viewNoTranslation[3][0] = 0.0; - viewNoTranslation[3][1] = 0.0; - viewNoTranslation[3][2] = 0.0; - - vec4 pos = uProjection * viewNoTranslation * vec4(aPos, 1.0); - gl_Position = pos.xyww; // Put at far plane - } - )"; - - // Cloud fragment shader with procedural noise - const char* fragmentShaderSource = R"( - #version 330 core - in vec3 WorldPos; - in vec3 LocalPos; - - uniform vec3 uCloudColor; - uniform float uDensity; - uniform float uWindOffset; - - out vec4 FragColor; - - // Simple 3D noise function - float hash(vec3 p) { - p = fract(p * vec3(0.1031, 0.1030, 0.0973)); - p += dot(p, p.yxz + 19.19); - return fract((p.x + p.y) * p.z); - } - - float noise(vec3 p) { - vec3 i = floor(p); - vec3 f = fract(p); - f = f * f * (3.0 - 2.0 * f); - - return mix( - mix(mix(hash(i + vec3(0,0,0)), hash(i + vec3(1,0,0)), f.x), - mix(hash(i + vec3(0,1,0)), hash(i + vec3(1,1,0)), f.x), f.y), - mix(mix(hash(i + vec3(0,0,1)), hash(i + vec3(1,0,1)), f.x), - mix(hash(i + vec3(0,1,1)), hash(i + vec3(1,1,1)), f.x), f.y), - f.z); - } - - // Fractal Brownian Motion for cloud-like patterns - float fbm(vec3 p) { - float value = 0.0; - float amplitude = 0.5; - float frequency = 1.0; - - for (int i = 0; i < 4; i++) { - value += amplitude * noise(p * frequency); - frequency *= 2.0; - amplitude *= 0.5; - } - - return value; - } - - void main() { - // Normalize position for noise sampling - vec3 pos = normalize(LocalPos); - - // Only render on upper hemisphere - if (pos.y < 0.1) { - discard; - } - - // Apply wind offset to x coordinate - vec3 samplePos = vec3(pos.x + uWindOffset, pos.y, pos.z) * 3.0; - - // Generate two cloud layers - float clouds1 = fbm(samplePos * 1.0); - float clouds2 = fbm(samplePos * 2.8 + vec3(100.0)); - - // Combine layers - float cloudPattern = clouds1 * 0.6 + clouds2 * 0.4; - - // Apply density threshold to create cloud shapes with softer transition. - float cloudStart = 0.34 + (1.0 - uDensity) * 0.26; - float cloudEnd = 0.74; - float cloudMask = smoothstep(cloudStart, cloudEnd, cloudPattern); - - // Fuzzy edge breakup: only modulate near the silhouette so cloud cores stay stable. - float edgeNoise = fbm(samplePos * 7.0 + vec3(41.0)); - float edgeBand = 1.0 - smoothstep(0.30, 0.72, cloudMask); // 1 near edge, 0 in center - float fringe = mix(1.0, smoothstep(0.34, 0.80, edgeNoise), edgeBand * 0.95); - cloudMask *= fringe; - - // Fade clouds near horizon - float horizonFade = smoothstep(0.0, 0.3, pos.y); - cloudMask *= horizonFade; - - // Reduce edge contrast against skybox: soften + lower opacity. - float edgeSoften = smoothstep(0.0, 0.80, cloudMask); - edgeSoften = mix(0.45, 1.0, edgeSoften); - float alpha = cloudMask * edgeSoften * 0.70; - - if (alpha < 0.05) { - discard; - } - - FragColor = vec4(uCloudColor, alpha); - } - )"; - - if (!shader->loadFromSource(vertexShaderSource, fragmentShaderSource)) { - LOG_ERROR("Failed to create cloud shader"); + // ------------------------------------------------------------------ shaders + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/clouds.vert.spv")) { + LOG_ERROR("Failed to load clouds vertex shader"); return false; } - LOG_INFO("Cloud system initialized: ", triangleCount, " triangles"); + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/clouds.frag.spv")) { + LOG_ERROR("Failed to load clouds fragment shader"); + return false; + } + + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + // ------------------------------------------------------------------ push constants + // Fragment-only push: vec4 cloudColor + float density + float windOffset = 24 bytes + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + pushRange.offset = 0; + pushRange.size = sizeof(CloudPush); // 24 bytes + + // ------------------------------------------------------------------ pipeline layout + pipelineLayout_ = createPipelineLayout(device, {perFrameLayout}, {pushRange}); + if (pipelineLayout_ == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create clouds pipeline layout"); + return false; + } + + // ------------------------------------------------------------------ vertex input + // Vertex: vec3 pos only, stride = 12 bytes + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = sizeof(glm::vec3); // 12 bytes + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + VkVertexInputAttributeDescription posAttr{}; + posAttr.location = 0; + posAttr.binding = 0; + posAttr.format = VK_FORMAT_R32G32B32_SFLOAT; + posAttr.offset = 0; + + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + // ------------------------------------------------------------------ pipeline + pipeline_ = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, {posAttr}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) // test on, write off (sky layer) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setLayout(pipelineLayout_) + .setRenderPass(vkCtx_->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (pipeline_ == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create clouds pipeline"); + return false; + } + + // ------------------------------------------------------------------ geometry + generateMesh(); + createBuffers(); + + LOG_INFO("Cloud system initialized: ", indexCount_ / 3, " triangles"); return true; } -void Clouds::generateMesh() { - vertices.clear(); - indices.clear(); +void Clouds::shutdown() { + destroyBuffers(); - // Generate hemisphere mesh for clouds - for (int ring = 0; ring <= RINGS; ++ring) { - float phi = (ring / static_cast(RINGS)) * (M_PI * 0.5f); // 0 to π/2 - float y = RADIUS * cosf(phi); - float ringRadius = RADIUS * sinf(phi); - - for (int segment = 0; segment <= SEGMENTS; ++segment) { - float theta = (segment / static_cast(SEGMENTS)) * (2.0f * M_PI); - float x = ringRadius * cosf(theta); - float z = ringRadius * sinf(theta); - - vertices.push_back(glm::vec3(x, y, z)); + if (vkCtx_) { + VkDevice device = vkCtx_->getDevice(); + if (pipeline_ != VK_NULL_HANDLE) { + vkDestroyPipeline(device, pipeline_, nullptr); + pipeline_ = VK_NULL_HANDLE; + } + if (pipelineLayout_ != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); + pipelineLayout_ = VK_NULL_HANDLE; } } - // Generate indices - for (int ring = 0; ring < RINGS; ++ring) { - for (int segment = 0; segment < SEGMENTS; ++segment) { - int current = ring * (SEGMENTS + 1) + segment; - int next = current + SEGMENTS + 1; - - // Two triangles per quad - indices.push_back(current); - indices.push_back(next); - indices.push_back(current + 1); - - indices.push_back(current + 1); - indices.push_back(next); - indices.push_back(next + 1); - } - } - - triangleCount = static_cast(indices.size()) / 3; + vkCtx_ = nullptr; } -void Clouds::update(float deltaTime) { - if (!enabled) { +// --------------------------------------------------------------------------- +// Render +// --------------------------------------------------------------------------- + +void Clouds::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, float timeOfDay) { + if (!enabled_ || pipeline_ == VK_NULL_HANDLE) { return; } - // Accumulate wind movement - windOffset += deltaTime * windSpeed * 0.05f; // Slow drift + glm::vec3 color = getCloudColor(timeOfDay); + + CloudPush push{}; + push.cloudColor = glm::vec4(color, 1.0f); + push.density = density_; + push.windOffset = windOffset_; + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_); + + // Bind per-frame UBO (set 0 — vertex shader reads view/projection from here) + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, + 0, 1, &perFrameSet, 0, nullptr); + + // Push cloud params to fragment shader + vkCmdPushConstants(cmd, pipelineLayout_, + VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof(push), &push); + + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer_, &offset); + vkCmdBindIndexBuffer(cmd, indexBuffer_, 0, VK_INDEX_TYPE_UINT32); + + vkCmdDrawIndexed(cmd, static_cast(indexCount_), 1, 0, 0, 0); } +// --------------------------------------------------------------------------- +// Update +// --------------------------------------------------------------------------- + +void Clouds::update(float deltaTime) { + if (!enabled_) { + return; + } + windOffset_ += deltaTime * windSpeed_ * 0.05f; // Slow drift +} + +// --------------------------------------------------------------------------- +// Cloud colour (unchanged logic from GL version) +// --------------------------------------------------------------------------- + glm::vec3 Clouds::getCloudColor(float timeOfDay) const { - // Base cloud color (white/light gray) glm::vec3 dayColor(0.95f, 0.95f, 1.0f); - // Dawn clouds (orange tint) if (timeOfDay >= 5.0f && timeOfDay < 7.0f) { + // Dawn — orange tint fading to day float t = (timeOfDay - 5.0f) / 2.0f; - glm::vec3 dawnColor(1.0f, 0.7f, 0.5f); - return glm::mix(dawnColor, dayColor, t); - } - // Dusk clouds (orange/pink tint) - else if (timeOfDay >= 17.0f && timeOfDay < 19.0f) { + return glm::mix(glm::vec3(1.0f, 0.7f, 0.5f), dayColor, t); + } else if (timeOfDay >= 17.0f && timeOfDay < 19.0f) { + // Dusk — day fading to orange/pink float t = (timeOfDay - 17.0f) / 2.0f; - glm::vec3 duskColor(1.0f, 0.6f, 0.4f); - return glm::mix(dayColor, duskColor, t); - } - // Night clouds (dark blue-gray) - else if (timeOfDay >= 20.0f || timeOfDay < 5.0f) { + return glm::mix(dayColor, glm::vec3(1.0f, 0.6f, 0.4f), t); + } else if (timeOfDay >= 20.0f || timeOfDay < 5.0f) { + // Night — dark blue-grey return glm::vec3(0.15f, 0.15f, 0.25f); } return dayColor; } -void Clouds::render(const Camera& camera, float timeOfDay) { - if (!enabled || !shader) { - return; - } - - // Enable blending for transparent clouds - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - // Disable depth write (clouds are in sky) - glDepthMask(GL_FALSE); - - // Enable depth test so clouds are behind skybox - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_LEQUAL); - - shader->use(); - - // Set matrices - glm::mat4 view = camera.getViewMatrix(); - glm::mat4 projection = camera.getProjectionMatrix(); - - shader->setUniform("uView", view); - shader->setUniform("uProjection", projection); - - // Set cloud parameters - glm::vec3 cloudColor = getCloudColor(timeOfDay); - shader->setUniform("uCloudColor", cloudColor); - shader->setUniform("uDensity", density); - shader->setUniform("uWindOffset", windOffset); - - // Render - glBindVertexArray(vao); - glDrawElements(GL_TRIANGLES, static_cast(indices.size()), GL_UNSIGNED_INT, 0); - glBindVertexArray(0); - - // Restore state - glDisable(GL_BLEND); - glDepthMask(GL_TRUE); - glDepthFunc(GL_LESS); -} +// --------------------------------------------------------------------------- +// Density setter +// --------------------------------------------------------------------------- void Clouds::setDensity(float density) { - this->density = glm::clamp(density, 0.0f, 1.0f); + density_ = glm::clamp(density, 0.0f, 1.0f); } -void Clouds::cleanup() { - if (vao) { - glDeleteVertexArrays(1, &vao); - vao = 0; +// --------------------------------------------------------------------------- +// Mesh generation — identical algorithm to GL version +// --------------------------------------------------------------------------- + +void Clouds::generateMesh() { + vertices_.clear(); + indices_.clear(); + + // Upper hemisphere + for (int ring = 0; ring <= RINGS; ++ring) { + float phi = (ring / static_cast(RINGS)) * (static_cast(M_PI) * 0.5f); + float y = RADIUS * std::cos(phi); + float ringRadius = RADIUS * std::sin(phi); + + for (int seg = 0; seg <= SEGMENTS; ++seg) { + float theta = (seg / static_cast(SEGMENTS)) * (2.0f * static_cast(M_PI)); + float x = ringRadius * std::cos(theta); + float z = ringRadius * std::sin(theta); + vertices_.push_back(glm::vec3(x, y, z)); + } } - if (vbo) { - glDeleteBuffers(1, &vbo); - vbo = 0; + + for (int ring = 0; ring < RINGS; ++ring) { + for (int seg = 0; seg < SEGMENTS; ++seg) { + uint32_t current = static_cast(ring * (SEGMENTS + 1) + seg); + uint32_t next = current + static_cast(SEGMENTS + 1); + + indices_.push_back(current); + indices_.push_back(next); + indices_.push_back(current + 1); + + indices_.push_back(current + 1); + indices_.push_back(next); + indices_.push_back(next + 1); + } } - if (ebo) { - glDeleteBuffers(1, &ebo); - ebo = 0; + + indexCount_ = static_cast(indices_.size()); +} + +// --------------------------------------------------------------------------- +// GPU buffer management +// --------------------------------------------------------------------------- + +void Clouds::createBuffers() { + AllocatedBuffer vbuf = uploadBuffer(*vkCtx_, + vertices_.data(), + vertices_.size() * sizeof(glm::vec3), + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + vertexBuffer_ = vbuf.buffer; + vertexAlloc_ = vbuf.allocation; + + AllocatedBuffer ibuf = uploadBuffer(*vkCtx_, + indices_.data(), + indices_.size() * sizeof(uint32_t), + VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + indexBuffer_ = ibuf.buffer; + indexAlloc_ = ibuf.allocation; + + // CPU data no longer needed + vertices_.clear(); + vertices_.shrink_to_fit(); + indices_.clear(); + indices_.shrink_to_fit(); +} + +void Clouds::destroyBuffers() { + if (!vkCtx_) return; + + VmaAllocator allocator = vkCtx_->getAllocator(); + + if (vertexBuffer_ != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, vertexBuffer_, vertexAlloc_); + vertexBuffer_ = VK_NULL_HANDLE; + vertexAlloc_ = VK_NULL_HANDLE; + } + if (indexBuffer_ != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, indexBuffer_, indexAlloc_); + indexBuffer_ = VK_NULL_HANDLE; + indexAlloc_ = VK_NULL_HANDLE; } } diff --git a/src/rendering/lens_flare.cpp b/src/rendering/lens_flare.cpp index 8bae2551..c5350a31 100644 --- a/src/rendering/lens_flare.cpp +++ b/src/rendering/lens_flare.cpp @@ -1,9 +1,11 @@ #include "rendering/lens_flare.hpp" #include "rendering/camera.hpp" -#include "rendering/shader.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_utils.hpp" #include "core/logger.hpp" #include -#include #include namespace wowee { @@ -13,23 +15,19 @@ LensFlare::LensFlare() { } LensFlare::~LensFlare() { - cleanup(); + shutdown(); } -bool LensFlare::initialize() { +bool LensFlare::initialize(VkContext* ctx, VkDescriptorSetLayout /*perFrameLayout*/) { LOG_INFO("Initializing lens flare system"); + vkCtx = ctx; + VkDevice device = vkCtx->getDevice(); + // Generate flare elements generateFlareElements(); - // Create VAO and VBO for quad rendering - glGenVertexArrays(1, &vao); - glGenBuffers(1, &vbo); - - glBindVertexArray(vao); - glBindBuffer(GL_ARRAY_BUFFER, vbo); - - // Position (x, y) and UV (u, v) for a quad + // Upload static quad vertex buffer (pos2 + uv2, 6 vertices) float quadVertices[] = { // Pos UV -0.5f, -0.5f, 0.0f, 0.0f, @@ -40,81 +38,84 @@ bool LensFlare::initialize() { -0.5f, 0.5f, 0.0f, 1.0f }; - glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW); + AllocatedBuffer vbuf = uploadBuffer(*vkCtx, + quadVertices, + sizeof(quadVertices), + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + vertexBuffer = vbuf.buffer; + vertexAlloc = vbuf.allocation; - // Position attribute - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); + // Load SPIR-V shaders + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/lens_flare.vert.spv")) { + LOG_ERROR("Failed to load lens flare vertex shader"); + return false; + } - // UV attribute - glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); - glEnableVertexAttribArray(1); + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/lens_flare.frag.spv")) { + LOG_ERROR("Failed to load lens flare fragment shader"); + return false; + } - glBindVertexArray(0); + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); - // Create shader - shader = std::make_unique(); + // Push constant range: FlarePushConstants = 32 bytes, used by both vert and frag + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + pushRange.offset = 0; + pushRange.size = sizeof(FlarePushConstants); // 32 bytes - // Lens flare vertex shader (2D screen-space rendering) - const char* vertexShaderSource = R"( - #version 330 core - layout (location = 0) in vec2 aPos; - layout (location = 1) in vec2 aUV; + // No descriptor set layouts — lens flare only uses push constants + pipelineLayout = createPipelineLayout(device, {}, {pushRange}); + if (pipelineLayout == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create lens flare pipeline layout"); + return false; + } - uniform vec2 uPosition; // Screen-space position (-1 to 1) - uniform float uSize; // Size in screen space - uniform float uAspectRatio; + // Vertex input: pos2 + uv2, stride = 4 * sizeof(float) + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 4 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; - out vec2 TexCoord; + VkVertexInputAttributeDescription posAttr{}; + posAttr.location = 0; + posAttr.binding = 0; + posAttr.format = VK_FORMAT_R32G32_SFLOAT; + posAttr.offset = 0; - void main() { - // Scale by size and aspect ratio - vec2 scaledPos = aPos * uSize; - scaledPos.x /= uAspectRatio; + VkVertexInputAttributeDescription uvAttr{}; + uvAttr.location = 1; + uvAttr.binding = 0; + uvAttr.format = VK_FORMAT_R32G32_SFLOAT; + uvAttr.offset = 2 * sizeof(float); - // Translate to position - vec2 finalPos = scaledPos + uPosition; + // Dynamic viewport and scissor + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; - gl_Position = vec4(finalPos, 0.0, 1.0); - TexCoord = aUV; - } - )"; + pipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, {posAttr, uvAttr}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setNoDepthTest() + .setColorBlendAttachment(PipelineBuilder::blendAdditive()) + .setLayout(pipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); - // Lens flare fragment shader (circular gradient) - const char* fragmentShaderSource = R"( - #version 330 core - in vec2 TexCoord; + // Shader modules can be freed after pipeline creation + vertModule.destroy(); + fragModule.destroy(); - uniform vec3 uColor; - uniform float uBrightness; - - out vec4 FragColor; - - void main() { - // Distance from center - vec2 center = vec2(0.5); - float dist = distance(TexCoord, center); - - // Circular gradient with soft edges - float alpha = smoothstep(0.5, 0.0, dist); - - // Add some variation - brighter in center - float centerGlow = smoothstep(0.5, 0.0, dist * 2.0); - alpha = max(alpha * 0.3, centerGlow); - - // Apply brightness - alpha *= uBrightness; - - if (alpha < 0.01) { - discard; - } - - FragColor = vec4(uColor, alpha); - } - )"; - - if (!shader->loadFromSource(vertexShaderSource, fragmentShaderSource)) { - LOG_ERROR("Failed to create lens flare shader"); + if (pipeline == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create lens flare pipeline"); return false; } @@ -122,6 +123,29 @@ bool LensFlare::initialize() { return true; } +void LensFlare::shutdown() { + if (vkCtx) { + VkDevice device = vkCtx->getDevice(); + VmaAllocator allocator = vkCtx->getAllocator(); + + if (vertexBuffer != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, vertexBuffer, vertexAlloc); + vertexBuffer = VK_NULL_HANDLE; + vertexAlloc = VK_NULL_HANDLE; + } + if (pipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(device, pipeline, nullptr); + pipeline = VK_NULL_HANDLE; + } + if (pipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + pipelineLayout = VK_NULL_HANDLE; + } + } + + vkCtx = nullptr; +} + void LensFlare::generateFlareElements() { flareElements.clear(); @@ -205,8 +229,8 @@ float LensFlare::calculateSunVisibility(const Camera& camera, const glm::vec3& s return angleFactor * edgeFade; } -void LensFlare::render(const Camera& camera, const glm::vec3& sunPosition, float timeOfDay) { - if (!enabled || !shader) { +void LensFlare::render(VkCommandBuffer cmd, const Camera& camera, const glm::vec3& sunPosition, float timeOfDay) { + if (!enabled || pipeline == VK_NULL_HANDLE) { return; } @@ -237,61 +261,42 @@ void LensFlare::render(const Camera& camera, const glm::vec3& sunPosition, float // Vector from sun to screen center glm::vec2 sunToCenter = screenCenter - sunScreen; - // Enable additive blending for flare effect - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Additive blending - - // Disable depth test (render on top) - glDisable(GL_DEPTH_TEST); - - shader->use(); - - // Set aspect ratio float aspectRatio = camera.getAspectRatio(); - shader->setUniform("uAspectRatio", aspectRatio); - glBindVertexArray(vao); + // Bind pipeline + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + + // Bind vertex buffer + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset); // Render each flare element for (const auto& element : flareElements) { // Calculate position along sun-to-center axis glm::vec2 position = sunScreen + sunToCenter * element.position; - // Set uniforms - shader->setUniform("uPosition", position); - shader->setUniform("uSize", element.size); - shader->setUniform("uColor", element.color); - // Apply visibility and intensity float brightness = element.brightness * visibility * intensityMultiplier; - shader->setUniform("uBrightness", brightness); - // Render quad - glDrawArrays(GL_TRIANGLES, 0, VERTICES_PER_QUAD); + // Set push constants + FlarePushConstants push{}; + push.position = position; + push.size = element.size; + push.aspectRatio = aspectRatio; + push.colorBrightness = glm::vec4(element.color, brightness); + + vkCmdPushConstants(cmd, pipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof(push), &push); + + // Draw quad + vkCmdDraw(cmd, VERTICES_PER_QUAD, 1, 0, 0); } - - glBindVertexArray(0); - - // Restore state - glEnable(GL_DEPTH_TEST); - glDisable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Restore standard blending } void LensFlare::setIntensity(float intensity) { this->intensityMultiplier = glm::clamp(intensity, 0.0f, 2.0f); } -void LensFlare::cleanup() { - if (vao) { - glDeleteVertexArrays(1, &vao); - vao = 0; - } - if (vbo) { - glDeleteBuffers(1, &vbo); - vbo = 0; - } -} - } // namespace rendering } // namespace wowee diff --git a/src/rendering/lightning.cpp b/src/rendering/lightning.cpp index 58193e4d..3414cf70 100644 --- a/src/rendering/lightning.cpp +++ b/src/rendering/lightning.cpp @@ -1,10 +1,14 @@ #include "rendering/lightning.hpp" -#include "rendering/shader.hpp" #include "rendering/camera.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_frame_data.hpp" +#include "rendering/vk_utils.hpp" #include "core/logger.hpp" -#include #include #include +#include namespace wowee { namespace rendering { @@ -41,125 +45,212 @@ Lightning::~Lightning() { shutdown(); } -bool Lightning::initialize() { +bool Lightning::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { core::Logger::getInstance().info("Initializing lightning system..."); - // Create bolt shader - const char* boltVertexSrc = R"( - #version 330 core - layout (location = 0) in vec3 aPos; + vkCtx = ctx; + VkDevice device = vkCtx->getDevice(); - uniform mat4 uViewProjection; - uniform float uBrightness; - - out float vBrightness; - - void main() { - gl_Position = uViewProjection * vec4(aPos, 1.0); - vBrightness = uBrightness; - } - )"; - - const char* boltFragmentSrc = R"( - #version 330 core - in float vBrightness; - out vec4 FragColor; - - void main() { - // Electric blue-white color - vec3 color = mix(vec3(0.6, 0.8, 1.0), vec3(1.0), vBrightness * 0.5); - FragColor = vec4(color, vBrightness); - } - )"; - - boltShader = std::make_unique(); - if (!boltShader->loadFromSource(boltVertexSrc, boltFragmentSrc)) { - core::Logger::getInstance().error("Failed to create bolt shader"); - return false; - } - - // Create flash shader (fullscreen quad) - const char* flashVertexSrc = R"( - #version 330 core - layout (location = 0) in vec2 aPos; - - void main() { - gl_Position = vec4(aPos, 0.0, 1.0); - } - )"; - - const char* flashFragmentSrc = R"( - #version 330 core - uniform float uIntensity; - out vec4 FragColor; - - void main() { - // Bright white flash with fade - vec3 color = vec3(1.0); - FragColor = vec4(color, uIntensity * 0.6); - } - )"; - - flashShader = std::make_unique(); - if (!flashShader->loadFromSource(flashVertexSrc, flashFragmentSrc)) { - core::Logger::getInstance().error("Failed to create flash shader"); - return false; - } - - // Create bolt VAO/VBO - glGenVertexArrays(1, &boltVAO); - glGenBuffers(1, &boltVBO); - - glBindVertexArray(boltVAO); - glBindBuffer(GL_ARRAY_BUFFER, boltVBO); - - // Reserve space for segments - glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec3) * MAX_SEGMENTS * 2, nullptr, GL_DYNAMIC_DRAW); - - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0); - - // Create flash quad VAO/VBO - glGenVertexArrays(1, &flashVAO); - glGenBuffers(1, &flashVBO); - - float flashQuad[] = { - -1.0f, -1.0f, - 1.0f, -1.0f, - -1.0f, 1.0f, - 1.0f, 1.0f + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR }; - glBindVertexArray(flashVAO); - glBindBuffer(GL_ARRAY_BUFFER, flashVBO); - glBufferData(GL_ARRAY_BUFFER, sizeof(flashQuad), flashQuad, GL_STATIC_DRAW); + // ---- Bolt pipeline (LINE_STRIP) ---- + { + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/lightning_bolt.vert.spv")) { + core::Logger::getInstance().error("Failed to load lightning_bolt vertex shader"); + return false; + } + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/lightning_bolt.frag.spv")) { + core::Logger::getInstance().error("Failed to load lightning_bolt fragment shader"); + return false; + } - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0); + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); - glBindVertexArray(0); + // Push constant: { float brightness; } = 4 bytes + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + pushRange.offset = 0; + pushRange.size = sizeof(float); + + boltPipelineLayout = createPipelineLayout(device, {perFrameLayout}, {pushRange}); + if (boltPipelineLayout == VK_NULL_HANDLE) { + core::Logger::getInstance().error("Failed to create bolt pipeline layout"); + return false; + } + + // Vertex input: position only (vec3) + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = sizeof(glm::vec3); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + VkVertexInputAttributeDescription posAttr{}; + posAttr.location = 0; + posAttr.binding = 0; + posAttr.format = VK_FORMAT_R32G32B32_SFLOAT; + posAttr.offset = 0; + + boltPipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, {posAttr}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_LINE_STRIP) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setNoDepthTest() // Always visible (like the GL version) + .setColorBlendAttachment(PipelineBuilder::blendAdditive()) // Additive for electric glow + .setLayout(boltPipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (boltPipeline == VK_NULL_HANDLE) { + core::Logger::getInstance().error("Failed to create bolt pipeline"); + return false; + } + } + + // ---- Flash pipeline (fullscreen quad, TRIANGLE_STRIP) ---- + { + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/lightning_flash.vert.spv")) { + core::Logger::getInstance().error("Failed to load lightning_flash vertex shader"); + return false; + } + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/lightning_flash.frag.spv")) { + core::Logger::getInstance().error("Failed to load lightning_flash fragment shader"); + return false; + } + + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + // Push constant: { float intensity; } = 4 bytes + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + pushRange.offset = 0; + pushRange.size = sizeof(float); + + flashPipelineLayout = createPipelineLayout(device, {}, {pushRange}); + if (flashPipelineLayout == VK_NULL_HANDLE) { + core::Logger::getInstance().error("Failed to create flash pipeline layout"); + return false; + } + + // Vertex input: position only (vec2) + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 2 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + VkVertexInputAttributeDescription posAttr{}; + posAttr.location = 0; + posAttr.binding = 0; + posAttr.format = VK_FORMAT_R32G32_SFLOAT; + posAttr.offset = 0; + + flashPipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, {posAttr}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setNoDepthTest() + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setLayout(flashPipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (flashPipeline == VK_NULL_HANDLE) { + core::Logger::getInstance().error("Failed to create flash pipeline"); + return false; + } + } + + // ---- Create dynamic mapped vertex buffer for bolt segments ---- + // Each bolt can have up to MAX_SEGMENTS * 2 vec3 entries (segments + branches) + boltDynamicVBSize = MAX_SEGMENTS * 4 * sizeof(glm::vec3); // generous capacity + { + AllocatedBuffer buf = createBuffer(vkCtx->getAllocator(), boltDynamicVBSize, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU); + boltDynamicVB = buf.buffer; + boltDynamicVBAlloc = buf.allocation; + boltDynamicVBAllocInfo = buf.info; + if (boltDynamicVB == VK_NULL_HANDLE) { + core::Logger::getInstance().error("Failed to create bolt dynamic vertex buffer"); + return false; + } + } + + // ---- Create static flash quad vertex buffer ---- + { + float flashQuad[] = { + -1.0f, -1.0f, + 1.0f, -1.0f, + -1.0f, 1.0f, + 1.0f, 1.0f + }; + + AllocatedBuffer buf = uploadBuffer(*vkCtx, flashQuad, sizeof(flashQuad), + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + flashQuadVB = buf.buffer; + flashQuadVBAlloc = buf.allocation; + if (flashQuadVB == VK_NULL_HANDLE) { + core::Logger::getInstance().error("Failed to create flash quad vertex buffer"); + return false; + } + } core::Logger::getInstance().info("Lightning system initialized"); return true; } void Lightning::shutdown() { - if (boltVAO) { - glDeleteVertexArrays(1, &boltVAO); - glDeleteBuffers(1, &boltVBO); - boltVAO = 0; - boltVBO = 0; + if (vkCtx) { + VkDevice device = vkCtx->getDevice(); + VmaAllocator allocator = vkCtx->getAllocator(); + + if (boltPipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(device, boltPipeline, nullptr); + boltPipeline = VK_NULL_HANDLE; + } + if (boltPipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, boltPipelineLayout, nullptr); + boltPipelineLayout = VK_NULL_HANDLE; + } + if (boltDynamicVB != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, boltDynamicVB, boltDynamicVBAlloc); + boltDynamicVB = VK_NULL_HANDLE; + boltDynamicVBAlloc = VK_NULL_HANDLE; + } + + if (flashPipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(device, flashPipeline, nullptr); + flashPipeline = VK_NULL_HANDLE; + } + if (flashPipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, flashPipelineLayout, nullptr); + flashPipelineLayout = VK_NULL_HANDLE; + } + if (flashQuadVB != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, flashQuadVB, flashQuadVBAlloc); + flashQuadVB = VK_NULL_HANDLE; + flashQuadVBAlloc = VK_NULL_HANDLE; + } } - if (flashVAO) { - glDeleteVertexArrays(1, &flashVAO); - glDeleteBuffers(1, &flashVBO); - flashVAO = 0; - flashVBO = 0; - } - - boltShader.reset(); - flashShader.reset(); + vkCtx = nullptr; } void Lightning::update(float deltaTime, const Camera& camera) { @@ -325,73 +416,65 @@ void Lightning::generateBoltSegments(const glm::vec3& start, const glm::vec3& en segments.push_back(end); } -void Lightning::render([[maybe_unused]] const Camera& camera, const glm::mat4& view, const glm::mat4& projection) { +void Lightning::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) { if (!enabled) { return; } - glm::mat4 viewProj = projection * view; - - renderBolts(viewProj); - renderFlash(); + renderBolts(cmd, perFrameSet); + renderFlash(cmd); } -void Lightning::renderBolts(const glm::mat4& viewProj) { - // Enable additive blending for electric glow - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE); - glDisable(GL_DEPTH_TEST); // Always visible +void Lightning::renderBolts(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) { + if (boltPipeline == VK_NULL_HANDLE) return; - boltShader->use(); - boltShader->setUniform("uViewProjection", viewProj); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, boltPipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, boltPipelineLayout, + 0, 1, &perFrameSet, 0, nullptr); - glBindVertexArray(boltVAO); - glLineWidth(3.0f); + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &boltDynamicVB, &offset); for (const auto& bolt : bolts) { if (!bolt.active || bolt.segments.empty()) { continue; } - boltShader->setUniform("uBrightness", bolt.brightness); + // Upload bolt segments to mapped buffer + VkDeviceSize uploadSize = bolt.segments.size() * sizeof(glm::vec3); + if (uploadSize > boltDynamicVBSize) { + // Clamp to buffer size + uploadSize = boltDynamicVBSize; + } + if (boltDynamicVBAllocInfo.pMappedData) { + std::memcpy(boltDynamicVBAllocInfo.pMappedData, bolt.segments.data(), uploadSize); + } - // Upload segments - glBindBuffer(GL_ARRAY_BUFFER, boltVBO); - glBufferSubData(GL_ARRAY_BUFFER, 0, - bolt.segments.size() * sizeof(glm::vec3), - bolt.segments.data()); + // Push brightness + vkCmdPushConstants(cmd, boltPipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof(float), &bolt.brightness); - // Draw as line strip - glDrawArrays(GL_LINE_STRIP, 0, static_cast(bolt.segments.size())); + uint32_t vertexCount = static_cast(uploadSize / sizeof(glm::vec3)); + vkCmdDraw(cmd, vertexCount, 1, 0, 0); } - - glLineWidth(1.0f); - glBindVertexArray(0); - - glEnable(GL_DEPTH_TEST); - glDisable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } -void Lightning::renderFlash() { - if (!flash.active || flash.intensity <= 0.01f) { +void Lightning::renderFlash(VkCommandBuffer cmd) { + if (!flash.active || flash.intensity <= 0.01f || flashPipeline == VK_NULL_HANDLE) { return; } - // Fullscreen flash overlay - glDisable(GL_DEPTH_TEST); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, flashPipeline); - flashShader->use(); - flashShader->setUniform("uIntensity", flash.intensity); + // Push flash intensity + vkCmdPushConstants(cmd, flashPipelineLayout, + VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof(float), &flash.intensity); - glBindVertexArray(flashVAO); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - glBindVertexArray(0); - - glEnable(GL_DEPTH_TEST); - glDisable(GL_BLEND); + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &flashQuadVB, &offset); + vkCmdDraw(cmd, 4, 1, 0, 0); } void Lightning::setEnabled(bool enabled) { diff --git a/src/rendering/loading_screen.cpp b/src/rendering/loading_screen.cpp index defd6bfc..8c91dc18 100644 --- a/src/rendering/loading_screen.cpp +++ b/src/rendering/loading_screen.cpp @@ -1,8 +1,9 @@ #include "rendering/loading_screen.hpp" +#include "rendering/vk_context.hpp" #include "core/logger.hpp" #include #include -#include +#include #include #include #include @@ -24,140 +25,37 @@ LoadingScreen::~LoadingScreen() { } bool LoadingScreen::initialize() { - LOG_INFO("Initializing loading screen"); - - // Background image shader (textured quad) - const char* vertexSrc = R"( - #version 330 core - layout (location = 0) in vec2 aPos; - layout (location = 1) in vec2 aTexCoord; - out vec2 TexCoord; - void main() { - gl_Position = vec4(aPos, 0.0, 1.0); - TexCoord = aTexCoord; - } - )"; - - const char* fragmentSrc = R"( - #version 330 core - in vec2 TexCoord; - out vec4 FragColor; - uniform sampler2D screenTexture; - void main() { - FragColor = texture(screenTexture, TexCoord); - } - )"; - - GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); - glShaderSource(vertexShader, 1, &vertexSrc, nullptr); - glCompileShader(vertexShader); - - GLint success; - glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); - if (!success) { - char infoLog[512]; - glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog); - LOG_ERROR("Loading screen vertex shader compilation failed: ", infoLog); - return false; - } - - GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); - glShaderSource(fragmentShader, 1, &fragmentSrc, nullptr); - glCompileShader(fragmentShader); - - glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); - if (!success) { - char infoLog[512]; - glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog); - LOG_ERROR("Loading screen fragment shader compilation failed: ", infoLog); - return false; - } - - shaderId = glCreateProgram(); - glAttachShader(shaderId, vertexShader); - glAttachShader(shaderId, fragmentShader); - glLinkProgram(shaderId); - - glGetProgramiv(shaderId, GL_LINK_STATUS, &success); - if (!success) { - char infoLog[512]; - glGetProgramInfoLog(shaderId, 512, nullptr, infoLog); - LOG_ERROR("Loading screen shader linking failed: ", infoLog); - return false; - } - - glDeleteShader(vertexShader); - glDeleteShader(fragmentShader); - - // Simple solid-color shader for progress bar - const char* barVertSrc = R"( - #version 330 core - layout (location = 0) in vec2 aPos; - void main() { - gl_Position = vec4(aPos, 0.0, 1.0); - } - )"; - - const char* barFragSrc = R"( - #version 330 core - out vec4 FragColor; - uniform vec4 uColor; - void main() { - FragColor = uColor; - } - )"; - - GLuint bv = glCreateShader(GL_VERTEX_SHADER); - glShaderSource(bv, 1, &barVertSrc, nullptr); - glCompileShader(bv); - GLuint bf = glCreateShader(GL_FRAGMENT_SHADER); - glShaderSource(bf, 1, &barFragSrc, nullptr); - glCompileShader(bf); - - barShaderId = glCreateProgram(); - glAttachShader(barShaderId, bv); - glAttachShader(barShaderId, bf); - glLinkProgram(barShaderId); - - glDeleteShader(bv); - glDeleteShader(bf); - - createQuad(); - createBarQuad(); + LOG_INFO("Initializing loading screen (Vulkan/ImGui)"); selectRandomImage(); - LOG_INFO("Loading screen initialized"); return true; } void LoadingScreen::shutdown() { - if (textureId) { - glDeleteTextures(1, &textureId); - textureId = 0; - } - if (vao) { - glDeleteVertexArrays(1, &vao); - vao = 0; - } - if (vbo) { - glDeleteBuffers(1, &vbo); - vbo = 0; - } - if (shaderId) { - glDeleteProgram(shaderId); - shaderId = 0; - } - if (barVao) { - glDeleteVertexArrays(1, &barVao); - barVao = 0; - } - if (barVbo) { - glDeleteBuffers(1, &barVbo); - barVbo = 0; - } - if (barShaderId) { - glDeleteProgram(barShaderId); - barShaderId = 0; + if (vkCtx && bgImage) { + VkDevice device = vkCtx->getDevice(); + vkDeviceWaitIdle(device); + + if (bgDescriptorSet) { + // ImGui manages descriptor set lifetime + bgDescriptorSet = VK_NULL_HANDLE; + } + if (bgSampler) { + vkDestroySampler(device, bgSampler, nullptr); + bgSampler = VK_NULL_HANDLE; + } + if (bgImageView) { + vkDestroyImageView(device, bgImageView, nullptr); + bgImageView = VK_NULL_HANDLE; + } + if (bgImage) { + vkDestroyImage(device, bgImage, nullptr); + bgImage = VK_NULL_HANDLE; + } + if (bgMemory) { + vkFreeMemory(device, bgMemory, nullptr); + bgMemory = VK_NULL_HANDLE; + } } } @@ -175,14 +73,36 @@ void LoadingScreen::selectRandomImage() { loadImage(imagePaths[currentImageIndex]); } +static uint32_t findMemoryType(VkPhysicalDevice physDevice, uint32_t typeFilter, VkMemoryPropertyFlags properties) { + VkPhysicalDeviceMemoryProperties memProperties; + vkGetPhysicalDeviceMemoryProperties(physDevice, &memProperties); + for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { + if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } + } + return 0; +} + bool LoadingScreen::loadImage(const std::string& path) { - if (textureId) { - glDeleteTextures(1, &textureId); - textureId = 0; + if (!vkCtx) { + LOG_WARNING("No VkContext for loading screen image"); + return false; + } + + // Clean up old image + if (bgImage) { + VkDevice device = vkCtx->getDevice(); + vkDeviceWaitIdle(device); + if (bgSampler) { vkDestroySampler(device, bgSampler, nullptr); bgSampler = VK_NULL_HANDLE; } + if (bgImageView) { vkDestroyImageView(device, bgImageView, nullptr); bgImageView = VK_NULL_HANDLE; } + if (bgImage) { vkDestroyImage(device, bgImage, nullptr); bgImage = VK_NULL_HANDLE; } + if (bgMemory) { vkFreeMemory(device, bgMemory, nullptr); bgMemory = VK_NULL_HANDLE; } + bgDescriptorSet = VK_NULL_HANDLE; } int channels; - stbi_set_flip_vertically_on_load(true); + stbi_set_flip_vertically_on_load(false); // ImGui expects top-down unsigned char* data = stbi_load(path.c_str(), &imageWidth, &imageHeight, &channels, 4); if (!data) { @@ -192,215 +112,244 @@ bool LoadingScreen::loadImage(const std::string& path) { LOG_INFO("Loaded loading screen image: ", imageWidth, "x", imageHeight); - glGenTextures(1, &textureId); - glBindTexture(GL_TEXTURE_2D, textureId); + VkDevice device = vkCtx->getDevice(); + VkPhysicalDevice physDevice = vkCtx->getPhysicalDevice(); + VkDeviceSize imageSize = static_cast(imageWidth) * imageHeight * 4; - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + // Create staging buffer + VkBuffer stagingBuffer; + VkDeviceMemory stagingMemory; + { + VkBufferCreateInfo bufInfo{}; + bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufInfo.size = imageSize; + bufInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + bufInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + vkCreateBuffer(device, &bufInfo, nullptr, &stagingBuffer); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, imageWidth, imageHeight, 0, - GL_RGBA, GL_UNSIGNED_BYTE, data); + VkMemoryRequirements memReqs; + vkGetBufferMemoryRequirements(device, stagingBuffer, &memReqs); + + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memReqs.size; + allocInfo.memoryTypeIndex = findMemoryType(physDevice, memReqs.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + vkAllocateMemory(device, &allocInfo, nullptr, &stagingMemory); + vkBindBufferMemory(device, stagingBuffer, stagingMemory, 0); + + void* mapped; + vkMapMemory(device, stagingMemory, 0, imageSize, 0, &mapped); + memcpy(mapped, data, imageSize); + vkUnmapMemory(device, stagingMemory); + } stbi_image_free(data); - glBindTexture(GL_TEXTURE_2D, 0); + + // Create image + { + VkImageCreateInfo imgInfo{}; + imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imgInfo.imageType = VK_IMAGE_TYPE_2D; + imgInfo.format = VK_FORMAT_R8G8B8A8_UNORM; + imgInfo.extent = {static_cast(imageWidth), static_cast(imageHeight), 1}; + imgInfo.mipLevels = 1; + imgInfo.arrayLayers = 1; + imgInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imgInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + vkCreateImage(device, &imgInfo, nullptr, &bgImage); + + VkMemoryRequirements memReqs; + vkGetImageMemoryRequirements(device, bgImage, &memReqs); + + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memReqs.size; + allocInfo.memoryTypeIndex = findMemoryType(physDevice, memReqs.memoryTypeBits, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + vkAllocateMemory(device, &allocInfo, nullptr, &bgMemory); + vkBindImageMemory(device, bgImage, bgMemory, 0); + } + + // Transfer: transition, copy, transition + vkCtx->immediateSubmit([&](VkCommandBuffer cmd) { + // Transition to transfer dst + VkImageMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = bgImage; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + + // Copy buffer to image + VkBufferImageCopy region{}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageExtent = {static_cast(imageWidth), static_cast(imageHeight), 1}; + vkCmdCopyBufferToImage(cmd, stagingBuffer, bgImage, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + + // Transition to shader read + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + }); + + // Cleanup staging + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingMemory, nullptr); + + // Create image view + { + VkImageViewCreateInfo viewInfo{}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = bgImage; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCreateImageView(device, &viewInfo, nullptr, &bgImageView); + } + + // Create sampler + { + VkSamplerCreateInfo samplerInfo{}; + samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerInfo.magFilter = VK_FILTER_LINEAR; + samplerInfo.minFilter = VK_FILTER_LINEAR; + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + vkCreateSampler(device, &samplerInfo, nullptr, &bgSampler); + } + + // Register with ImGui as a texture + bgDescriptorSet = ImGui_ImplVulkan_AddTexture(bgSampler, bgImageView, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); return true; } -void LoadingScreen::createQuad() { - float vertices[] = { - // Position // TexCoord - -1.0f, 1.0f, 0.0f, 1.0f, - -1.0f, -1.0f, 0.0f, 0.0f, - 1.0f, -1.0f, 1.0f, 0.0f, - - -1.0f, 1.0f, 0.0f, 1.0f, - 1.0f, -1.0f, 1.0f, 0.0f, - 1.0f, 1.0f, 1.0f, 1.0f - }; - - glGenVertexArrays(1, &vao); - glGenBuffers(1, &vbo); - - glBindVertexArray(vao); - glBindBuffer(GL_ARRAY_BUFFER, vbo); - glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); - glEnableVertexAttribArray(1); - - glBindVertexArray(0); -} - -void LoadingScreen::createBarQuad() { - // Dynamic quad — vertices updated each frame via glBufferSubData - glGenVertexArrays(1, &barVao); - glGenBuffers(1, &barVbo); - - glBindVertexArray(barVao); - glBindBuffer(GL_ARRAY_BUFFER, barVbo); - glBufferData(GL_ARRAY_BUFFER, 12 * sizeof(float), nullptr, GL_DYNAMIC_DRAW); - - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - - glBindVertexArray(0); -} - void LoadingScreen::render() { - if (!vao || !shaderId) return; + // If a frame is already in progress (e.g. called from a UI callback), + // end it before starting our own + ImGuiContext* ctx = ImGui::GetCurrentContext(); + if (ctx && ctx->FrameCount >= 0 && ctx->WithinFrameScope) { + ImGui::EndFrame(); + } - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); + ImGuiIO& io = ImGui::GetIO(); + float screenW = io.DisplaySize.x; + float screenH = io.DisplaySize.y; - glDisable(GL_DEPTH_TEST); + ImGui_ImplVulkan_NewFrame(); + ImGui_ImplSDL2_NewFrame(); + ImGui::NewFrame(); + + // Invisible fullscreen window + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(ImVec2(screenW, screenH)); + ImGui::Begin("##LoadingScreen", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground | + ImGuiWindowFlags_NoBringToFrontOnFocus); // Draw background image - if (textureId) { - glUseProgram(shaderId); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, textureId); - glBindVertexArray(vao); - glDrawArrays(GL_TRIANGLES, 0, 6); - glBindVertexArray(0); + if (bgDescriptorSet) { + ImGui::GetWindowDrawList()->AddImage( + reinterpret_cast(bgDescriptorSet), + ImVec2(0, 0), ImVec2(screenW, screenH)); } - // Draw progress bar at bottom center - if (barVao && barShaderId) { - // Bar dimensions in NDC: centered, near bottom - const float barWidth = 0.6f; // half-width in NDC (total 1.2 of 2.0 range = 60% of screen) - const float barHeight = 0.015f; - const float barY = -0.82f; // near bottom - - float left = -barWidth; - float right = -barWidth + 2.0f * barWidth * loadProgress; - float top = barY + barHeight; - float bottom = barY - barHeight; - - // Background (dark) - { - float bgVerts[] = { - -barWidth, top, - -barWidth, bottom, - barWidth, bottom, - -barWidth, top, - barWidth, bottom, - barWidth, top, - }; - glUseProgram(barShaderId); - GLint colorLoc = glGetUniformLocation(barShaderId, "uColor"); - glUniform4f(colorLoc, 0.1f, 0.1f, 0.1f, 0.8f); - - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - glBindVertexArray(barVao); - glBindBuffer(GL_ARRAY_BUFFER, barVbo); - glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(bgVerts), bgVerts); - glDrawArrays(GL_TRIANGLES, 0, 6); - } - - // Filled portion (gold/amber like WoW) - if (loadProgress > 0.001f) { - float fillVerts[] = { - left, top, - left, bottom, - right, bottom, - left, top, - right, bottom, - right, top, - }; - GLint colorLoc = glGetUniformLocation(barShaderId, "uColor"); - glUniform4f(colorLoc, 0.78f, 0.61f, 0.13f, 1.0f); - - glBindBuffer(GL_ARRAY_BUFFER, barVbo); - glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(fillVerts), fillVerts); - glDrawArrays(GL_TRIANGLES, 0, 6); - } - - // Border (thin bright outline) - { - const float borderInset = 0.002f; - float borderLeft = -barWidth - borderInset; - float borderRight = barWidth + borderInset; - float borderTop = top + borderInset; - float borderBottom = bottom - borderInset; - - // Draw 4 thin border edges as line strip - glUseProgram(barShaderId); - GLint colorLoc = glGetUniformLocation(barShaderId, "uColor"); - glUniform4f(colorLoc, 0.55f, 0.43f, 0.1f, 1.0f); - - float borderVerts[] = { - borderLeft, borderTop, - borderRight, borderTop, - borderRight, borderBottom, - borderLeft, borderBottom, - borderLeft, borderTop, - }; - glBindBuffer(GL_ARRAY_BUFFER, barVbo); - glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(borderVerts), borderVerts); - glDrawArrays(GL_LINE_STRIP, 0, 5); - } - - glBindVertexArray(0); - glDisable(GL_BLEND); - } - - // Draw status text and percentage with ImGui overlay + // Progress bar { - // If a frame is already in progress (e.g. called from a UI callback), - // end it before starting our own - ImGuiContext* ctx = ImGui::GetCurrentContext(); - if (ctx && ctx->FrameCount >= 0 && ctx->WithinFrameScope) { - ImGui::EndFrame(); + const float barWidthFrac = 0.6f; + const float barHeight = 6.0f; + const float barY = screenH * 0.91f; + float barX = screenW * (0.5f - barWidthFrac * 0.5f); + float barW = screenW * barWidthFrac; + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + // Background + drawList->AddRectFilled( + ImVec2(barX, barY), + ImVec2(barX + barW, barY + barHeight), + IM_COL32(25, 25, 25, 200), 2.0f); + + // Fill (gold) + if (loadProgress > 0.001f) { + drawList->AddRectFilled( + ImVec2(barX, barY), + ImVec2(barX + barW * loadProgress, barY + barHeight), + IM_COL32(199, 156, 33, 255), 2.0f); } - ImGuiIO& io = ImGui::GetIO(); - float screenW = io.DisplaySize.x; - float screenH = io.DisplaySize.y; + // Border + drawList->AddRect( + ImVec2(barX - 1, barY - 1), + ImVec2(barX + barW + 1, barY + barHeight + 1), + IM_COL32(140, 110, 25, 255), 2.0f); + } - ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplSDL2_NewFrame(); - ImGui::NewFrame(); - - // Invisible fullscreen window for text overlay - ImGui::SetNextWindowPos(ImVec2(0, 0)); - ImGui::SetNextWindowSize(ImVec2(screenW, screenH)); - ImGui::Begin("##LoadingOverlay", nullptr, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground | - ImGuiWindowFlags_NoBringToFrontOnFocus); - - // Percentage text centered above bar + // Percentage text above bar + { char pctBuf[32]; snprintf(pctBuf, sizeof(pctBuf), "%d%%", static_cast(loadProgress * 100.0f)); - - float barCenterY = screenH * (1.0f - ((-0.82f + 1.0f) / 2.0f)); // NDC -0.82 to screen Y - float textY = barCenterY - 30.0f; + float barCenterY = screenH * 0.91f; + float textY = barCenterY - 20.0f; ImVec2 pctSize = ImGui::CalcTextSize(pctBuf); ImGui::SetCursorPos(ImVec2((screenW - pctSize.x) * 0.5f, textY)); ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), "%s", pctBuf); + } - // Status text centered below bar - float statusY = barCenterY + 16.0f; + // Status text below bar + { + float statusY = screenH * 0.91f + 14.0f; ImVec2 statusSize = ImGui::CalcTextSize(statusText.c_str()); ImGui::SetCursorPos(ImVec2((screenW - statusSize.x) * 0.5f, statusY)); ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "%s", statusText.c_str()); - - ImGui::End(); - ImGui::Render(); - - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); } - glEnable(GL_DEPTH_TEST); + ImGui::End(); + ImGui::Render(); + + // Submit the frame to Vulkan (loading screen runs outside the main render loop) + if (vkCtx) { + uint32_t imageIndex = 0; + VkCommandBuffer cmd = vkCtx->beginFrame(imageIndex); + if (cmd != VK_NULL_HANDLE) { + // Begin render pass + VkRenderPassBeginInfo rpInfo{}; + rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rpInfo.renderPass = vkCtx->getImGuiRenderPass(); + rpInfo.framebuffer = vkCtx->getSwapchainFramebuffers()[imageIndex]; + rpInfo.renderArea.offset = {0, 0}; + rpInfo.renderArea.extent = vkCtx->getSwapchainExtent(); + + VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}}; + rpInfo.clearValueCount = 1; + rpInfo.pClearValues = &clearColor; + + vkCmdBeginRenderPass(cmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE); + ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), cmd); + vkCmdEndRenderPass(cmd); + + vkCtx->endFrame(cmd, imageIndex); + } + } } } // namespace rendering diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 7e206f11..7c1af188 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1,6 +1,11 @@ #include "rendering/m2_renderer.hpp" -#include "rendering/texture.hpp" -#include "rendering/shader.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_buffer.hpp" +#include "rendering/vk_texture.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_utils.hpp" +#include "rendering/vk_frame_data.hpp" #include "rendering/camera.hpp" #include "rendering/frustum.hpp" #include "pipeline/asset_manager.hpp" @@ -279,384 +284,298 @@ M2Renderer::~M2Renderer() { shutdown(); } -bool M2Renderer::initialize(pipeline::AssetManager* assets) { +bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, + pipeline::AssetManager* assets) { if (initialized_) { assetManager = assets; return true; } + vkCtx_ = ctx; assetManager = assets; numAnimThreads_ = std::min(4u, std::max(1u, std::thread::hardware_concurrency() - 1)); - LOG_INFO("Initializing M2 renderer (", numAnimThreads_, " anim threads)..."); + LOG_INFO("Initializing M2 renderer (Vulkan, ", numAnimThreads_, " anim threads)..."); - // Create M2 shader with skeletal animation support - const char* vertexSrc = R"( - #version 330 core - 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; + VkDevice device = vkCtx_->getDevice(); - uniform mat4 uModel; - uniform mat4 uView; - uniform mat4 uProjection; - uniform bool uUseBones; - uniform mat4 uBones[128]; - uniform vec2 uUVOffset; - uniform int uTexCoordSet; // 0 = UV set 0, 1 = UV set 1 - out vec3 FragPos; - out vec3 Normal; - out vec2 TexCoord; + // --- Descriptor set layouts --- - void main() { - vec3 pos = aPos; - vec3 norm = aNormal; + // Material set layout (set 1): binding 0 = sampler2D, binding 2 = M2Material UBO + // (M2Params moved to push constants alongside model matrix) + { + VkDescriptorSetLayoutBinding bindings[2] = {}; + bindings[0].binding = 0; + bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + bindings[0].descriptorCount = 1; + bindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + bindings[1].binding = 2; + bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + bindings[1].descriptorCount = 1; + bindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - if (uUseBones) { - ivec4 bi = ivec4(aBoneIndicesF); - mat4 boneTransform = uBones[bi.x] * aBoneWeights.x - + uBones[bi.y] * aBoneWeights.y - + uBones[bi.z] * aBoneWeights.z - + uBones[bi.w] * aBoneWeights.w; - pos = vec3(boneTransform * vec4(aPos, 1.0)); - norm = mat3(boneTransform) * aNormal; - } + VkDescriptorSetLayoutCreateInfo ci{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + ci.bindingCount = 2; + ci.pBindings = bindings; + vkCreateDescriptorSetLayout(device, &ci, nullptr, &materialSetLayout_); + } - vec4 worldPos = uModel * vec4(pos, 1.0); - FragPos = worldPos.xyz; - Normal = mat3(uModel) * norm; - TexCoord = (uTexCoordSet == 1 ? aTexCoord2 : aTexCoord) + uUVOffset; + // Bone set layout (set 2): binding 0 = STORAGE_BUFFER (bone matrices) + { + VkDescriptorSetLayoutBinding binding{}; + binding.binding = 0; + binding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; - gl_Position = uProjection * uView * worldPos; - } - )"; + VkDescriptorSetLayoutCreateInfo ci{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + ci.bindingCount = 1; + ci.pBindings = &binding; + vkCreateDescriptorSetLayout(device, &ci, nullptr, &boneSetLayout_); + } - const char* fragmentSrc = R"( - #version 330 core - in vec3 FragPos; - in vec3 Normal; - in vec2 TexCoord; + // Particle texture set layout (set 1 for particles): binding 0 = sampler2D + { + VkDescriptorSetLayoutBinding binding{}; + binding.binding = 0; + binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - uniform vec3 uLightDir; - uniform vec3 uLightColor; - uniform float uSpecularIntensity; - uniform vec3 uAmbientColor; - uniform vec3 uViewPos; - uniform sampler2D uTexture; - uniform bool uHasTexture; - uniform bool uAlphaTest; - uniform bool uColorKeyBlack; - uniform float uColorKeyThreshold; - uniform bool uUnlit; - uniform int uBlendMode; - uniform float uFadeAlpha; + VkDescriptorSetLayoutCreateInfo ci{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + ci.bindingCount = 1; + ci.pBindings = &binding; + vkCreateDescriptorSetLayout(device, &ci, nullptr, &particleTexLayout_); + } - uniform vec3 uFogColor; - uniform float uFogStart; - uniform float uFogEnd; + // --- Descriptor pools --- + { + VkDescriptorPoolSize sizes[] = { + {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, MAX_MATERIAL_SETS + 256}, + {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, MAX_MATERIAL_SETS + 256}, + }; + VkDescriptorPoolCreateInfo ci{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + ci.maxSets = MAX_MATERIAL_SETS + 256; + ci.poolSizeCount = 2; + ci.pPoolSizes = sizes; + ci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + vkCreateDescriptorPool(device, &ci, nullptr, &materialDescPool_); + } + { + VkDescriptorPoolSize sizes[] = { + {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, MAX_BONE_SETS}, + }; + VkDescriptorPoolCreateInfo ci{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + ci.maxSets = MAX_BONE_SETS; + ci.poolSizeCount = 1; + ci.pPoolSizes = sizes; + ci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + vkCreateDescriptorPool(device, &ci, nullptr, &boneDescPool_); + } - uniform sampler2DShadow uShadowMap; - uniform mat4 uLightSpaceMatrix; - uniform bool uShadowEnabled; - uniform float uShadowStrength; - uniform bool uInteriorDarken; + // --- Pipeline layouts --- - out vec4 FragColor; + // Main M2 pipeline layout: set 0 = perFrame, set 1 = material, set 2 = bones + // Push constant: mat4 model + vec2 uvOffset + int texCoordSet + int useBones = 80 bytes + { + VkDescriptorSetLayout setLayouts[] = {perFrameLayout, materialSetLayout_, boneSetLayout_}; + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + pushRange.offset = 0; + pushRange.size = 80; // mat4(64) + vec2(8) + int(4) + int(4) - void main() { - vec4 texColor; - if (uHasTexture) { - texColor = texture(uTexture, TexCoord); - } else { - texColor = vec4(0.6, 0.5, 0.4, 1.0); // Fallback brownish - } + VkPipelineLayoutCreateInfo ci{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + ci.setLayoutCount = 3; + ci.pSetLayouts = setLayouts; + ci.pushConstantRangeCount = 1; + ci.pPushConstantRanges = &pushRange; + vkCreatePipelineLayout(device, &ci, nullptr, &pipelineLayout_); + } - // Alpha test / alpha-key cutout for card textures. - if (uAlphaTest && texColor.a < 0.5) { - discard; - } - float maxRgb = max(texColor.r, max(texColor.g, texColor.b)); - if (uAlphaTest && maxRgb < 0.06) { - discard; - } - if (uColorKeyBlack && maxRgb < uColorKeyThreshold) { - discard; - } - // Additive blend modes (3=Add, 6=BlendAdd): near-black fragments - // contribute nothing visually (add ~0 to framebuffer) but show as - // dark rectangles against sky/terrain. Discard them. - // Skip Mod(4)/Mod2x(5) since near-black is intentional for those. - if ((uBlendMode == 3 || uBlendMode == 6) && maxRgb < 0.1) { - discard; - } - // Unlit non-opaque batches (glow effects, emissive surfaces) with - // near-black pixels: these are glow textures where black = transparent. - if (uUnlit && uBlendMode >= 1 && maxRgb < 0.1) { - discard; - } + // Particle pipeline layout: set 0 = perFrame, set 1 = particleTex + // Push constant: vec2 tileCount + int alphaKey (12 bytes) + { + VkDescriptorSetLayout setLayouts[] = {perFrameLayout, particleTexLayout_}; + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + pushRange.offset = 0; + pushRange.size = 12; // vec2 + int - // Distance fade - discard nearly invisible fragments - float finalAlpha = texColor.a * uFadeAlpha; - if (finalAlpha < 0.02) { - discard; - } + VkPipelineLayoutCreateInfo ci{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + ci.setLayoutCount = 2; + ci.pSetLayouts = setLayouts; + ci.pushConstantRangeCount = 1; + ci.pPushConstantRanges = &pushRange; + vkCreatePipelineLayout(device, &ci, nullptr, &particlePipelineLayout_); + } - // Unlit path: emit texture color directly (glow effects, emissive surfaces) - if (uUnlit) { - FragColor = vec4(texColor.rgb, finalAlpha); - return; - } + // Smoke pipeline layout: set 0 = perFrame + // Push constant: float screenHeight (4 bytes) + { + VkDescriptorSetLayout setLayouts[] = {perFrameLayout}; + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + pushRange.offset = 0; + pushRange.size = 4; - vec3 normal = normalize(Normal); - vec3 lightDir = normalize(uLightDir); + VkPipelineLayoutCreateInfo ci{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + ci.setLayoutCount = 1; + ci.pSetLayouts = setLayouts; + ci.pushConstantRangeCount = 1; + ci.pPushConstantRanges = &pushRange; + vkCreatePipelineLayout(device, &ci, nullptr, &smokePipelineLayout_); + } - vec3 result; - if (uInteriorDarken) { - // Interior: dim ambient, minimal directional light - float diff = max(abs(dot(normal, lightDir)), 0.0) * 0.15; - result = texColor.rgb * (0.55 + diff); - } else { - // Two-sided lighting for foliage - float diff = max(abs(dot(normal, lightDir)), 0.3); + // --- Load shaders --- + rendering::VkShaderModule m2Vert, m2Frag; + rendering::VkShaderModule particleVert, particleFrag; + rendering::VkShaderModule smokeVert, smokeFrag; - // Blinn-Phong specular - vec3 viewDir = normalize(uViewPos - FragPos); - vec3 halfDir = normalize(lightDir + viewDir); - float spec = pow(max(dot(normal, halfDir), 0.0), 32.0); - vec3 specular = spec * uLightColor * uSpecularIntensity; + m2Vert.loadFromFile(device, "assets/shaders/m2.vert.spv"); + m2Frag.loadFromFile(device, "assets/shaders/m2.frag.spv"); + particleVert.loadFromFile(device, "assets/shaders/m2_particle.vert.spv"); + particleFrag.loadFromFile(device, "assets/shaders/m2_particle.frag.spv"); + smokeVert.loadFromFile(device, "assets/shaders/m2_smoke.vert.spv"); + smokeFrag.loadFromFile(device, "assets/shaders/m2_smoke.frag.spv"); - // Shadow mapping - float shadow = 1.0; - if (uShadowEnabled) { - vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0); - vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5; - if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) { - float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5)); - float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist); - float bias = max(0.005 * (1.0 - abs(dot(normal, lightDir))), 0.001); - // Single hardware PCF tap — GL_LINEAR + compare mode gives 2×2 bilinear PCF for free - shadow = texture(uShadowMap, vec3(proj.xy, proj.z - bias)); - shadow = mix(1.0, shadow, coverageFade); - } - } - shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0)); - - vec3 ambient = uAmbientColor * texColor.rgb; - vec3 diffuse = diff * texColor.rgb; - - result = ambient + (diffuse + specular) * shadow; - } - - // Fog - float fogDist = length(uViewPos - FragPos); - float fogFactor = clamp((uFogEnd - fogDist) / (uFogEnd - uFogStart), 0.0, 1.0); - result = mix(uFogColor, result, fogFactor); - - FragColor = vec4(result, finalAlpha); - } - )"; - - shader = std::make_unique(); - if (!shader->loadFromSource(vertexSrc, fragmentSrc)) { - LOG_ERROR("Failed to create M2 shader"); + if (!m2Vert.isValid() || !m2Frag.isValid()) { + LOG_ERROR("M2: Missing required shaders, cannot initialize"); return false; } - // Create smoke particle shader - const char* smokeVertSrc = R"( - #version 330 core - layout (location = 0) in vec3 aPos; - layout (location = 1) in float aLifeRatio; - layout (location = 2) in float aSize; - layout (location = 3) in float aIsSpark; + VkRenderPass mainPass = vkCtx_->getImGuiRenderPass(); - uniform mat4 uView; - uniform mat4 uProjection; - uniform float uScreenHeight; + // --- Build M2 model pipelines --- + // Vertex input: 18 floats = 72 bytes stride + // loc 0: vec3 pos (0), loc 1: vec3 normal (12), loc 2: vec2 uv0 (24), + // loc 5: vec2 uv1 (32), loc 3: vec4 boneWeights (40), loc 4: vec4 boneIndices (56) + VkVertexInputBindingDescription m2Binding{}; + m2Binding.binding = 0; + m2Binding.stride = 18 * sizeof(float); + m2Binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; - out float vLifeRatio; - out float vIsSpark; + std::vector m2Attrs = { + {0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, // position + {1, 0, VK_FORMAT_R32G32B32_SFLOAT, 3 * sizeof(float)}, // normal + {2, 0, VK_FORMAT_R32G32_SFLOAT, 6 * sizeof(float)}, // texCoord0 + {5, 0, VK_FORMAT_R32G32_SFLOAT, 8 * sizeof(float)}, // texCoord1 + {3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 10 * sizeof(float)}, // boneWeights + {4, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 14 * sizeof(float)}, // boneIndices (float) + }; - void main() { - vec4 viewPos = uView * vec4(aPos, 1.0); - gl_Position = uProjection * viewPos; - float dist = -viewPos.z; - float scale = (aIsSpark > 0.5) ? 0.12 : 0.3; - gl_PointSize = clamp(aSize * (uScreenHeight * scale) / max(dist, 1.0), 2.0, 200.0); - vLifeRatio = aLifeRatio; - vIsSpark = aIsSpark; - } - )"; + auto buildM2Pipeline = [&](VkPipelineColorBlendAttachmentState blendState, bool depthWrite) -> VkPipeline { + return PipelineBuilder() + .setShaders(m2Vert.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + m2Frag.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({m2Binding}, m2Attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(blendState) + .setLayout(pipelineLayout_) + .setRenderPass(mainPass) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device); + }; - const char* smokeFragSrc = R"( - #version 330 core - in float vLifeRatio; - in float vIsSpark; - out vec4 FragColor; + opaquePipeline_ = buildM2Pipeline(PipelineBuilder::blendDisabled(), true); + alphaTestPipeline_ = buildM2Pipeline(PipelineBuilder::blendAlpha(), true); + alphaPipeline_ = buildM2Pipeline(PipelineBuilder::blendAlpha(), false); + additivePipeline_ = buildM2Pipeline(PipelineBuilder::blendAdditive(), false); - void main() { - vec2 coord = gl_PointCoord - vec2(0.5); - float dist = length(coord) * 2.0; + // --- Build particle pipelines --- + if (particleVert.isValid() && particleFrag.isValid()) { + VkVertexInputBindingDescription pBind{}; + pBind.binding = 0; + pBind.stride = 9 * sizeof(float); // pos3 + color4 + size1 + tile1 + pBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; - if (vIsSpark > 0.5) { - // Ember/spark: bright hot dot, fades quickly - float circle = 1.0 - smoothstep(0.3, 0.8, dist); - float fade = 1.0 - smoothstep(0.0, 1.0, vLifeRatio); - float alpha = circle * fade; - vec3 color = mix(vec3(1.0, 0.6, 0.1), vec3(1.0, 0.2, 0.0), vLifeRatio); - FragColor = vec4(color, alpha); - } else { - // Smoke: soft gray circle - float circle = 1.0 - smoothstep(0.5, 1.0, dist); - float fadeIn = smoothstep(0.0, 0.1, vLifeRatio); - float fadeOut = 1.0 - smoothstep(0.4, 1.0, vLifeRatio); - float alpha = circle * fadeIn * fadeOut * 0.5; - vec3 color = mix(vec3(0.5, 0.5, 0.53), vec3(0.65, 0.65, 0.68), vLifeRatio); - FragColor = vec4(color, alpha); - } - } - )"; + std::vector pAttrs = { + {0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, // position + {1, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 3 * sizeof(float)}, // color + {2, 0, VK_FORMAT_R32_SFLOAT, 7 * sizeof(float)}, // size + {3, 0, VK_FORMAT_R32_SFLOAT, 8 * sizeof(float)}, // tile + }; - smokeShader = std::make_unique(); - if (!smokeShader->loadFromSource(smokeVertSrc, smokeFragSrc)) { - LOG_ERROR("Failed to create smoke particle shader (non-fatal)"); - smokeShader.reset(); + auto buildParticlePipeline = [&](VkPipelineColorBlendAttachmentState blend) -> VkPipeline { + return PipelineBuilder() + .setShaders(particleVert.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + particleFrag.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({pBind}, pAttrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(blend) + .setLayout(particlePipelineLayout_) + .setRenderPass(mainPass) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device); + }; + + particlePipeline_ = buildParticlePipeline(PipelineBuilder::blendAlpha()); + particleAdditivePipeline_ = buildParticlePipeline(PipelineBuilder::blendAdditive()); } - // Create smoke particle VAO/VBO (only if shader compiled) - if (smokeShader) { - glGenVertexArrays(1, &smokeVAO); - glGenBuffers(1, &smokeVBO); - glBindVertexArray(smokeVAO); - glBindBuffer(GL_ARRAY_BUFFER, smokeVBO); - // 5 floats per particle: pos(3) + lifeRatio(1) + size(1) - // 6 floats per particle: pos(3) + lifeRatio(1) + size(1) + isSpark(1) - glBufferData(GL_ARRAY_BUFFER, MAX_SMOKE_PARTICLES * 6 * sizeof(float), nullptr, GL_DYNAMIC_DRAW); - // Position - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); - // Life ratio - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); - // Size - glEnableVertexAttribArray(2); - glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(4 * sizeof(float))); - // IsSpark - glEnableVertexAttribArray(3); - glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(5 * sizeof(float))); - glBindVertexArray(0); + // --- Build smoke pipeline --- + if (smokeVert.isValid() && smokeFrag.isValid()) { + VkVertexInputBindingDescription sBind{}; + sBind.binding = 0; + sBind.stride = 6 * sizeof(float); // pos3 + lifeRatio1 + size1 + isSpark1 + sBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector sAttrs = { + {0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, // position + {1, 0, VK_FORMAT_R32_SFLOAT, 3 * sizeof(float)}, // lifeRatio + {2, 0, VK_FORMAT_R32_SFLOAT, 4 * sizeof(float)}, // size + {3, 0, VK_FORMAT_R32_SFLOAT, 5 * sizeof(float)}, // isSpark + }; + + smokePipeline_ = PipelineBuilder() + .setShaders(smokeVert.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + smokeFrag.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({sBind}, sAttrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setLayout(smokePipelineLayout_) + .setRenderPass(mainPass) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device); } - // Create M2 particle emitter shader + // Clean up shader modules + m2Vert.destroy(); m2Frag.destroy(); + particleVert.destroy(); particleFrag.destroy(); + smokeVert.destroy(); smokeFrag.destroy(); + + // --- Create dynamic particle buffers (mapped for CPU writes) --- { - const char* particleVertSrc = R"( - #version 330 core - layout (location = 0) in vec3 aPos; - layout (location = 1) in vec4 aColor; - layout (location = 2) in float aSize; - layout (location = 3) in float aTile; + VkBufferCreateInfo bci{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bci.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; - uniform mat4 uView; - uniform mat4 uProjection; + VmaAllocationCreateInfo aci{}; + aci.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + aci.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; - out vec4 vColor; - out float vTile; + VmaAllocationInfo allocInfo{}; - void main() { - vec4 viewPos = uView * vec4(aPos, 1.0); - gl_Position = uProjection * viewPos; - float dist = max(-viewPos.z, 1.0); - gl_PointSize = clamp(aSize * 400.0 / dist, 1.0, 64.0); - vColor = aColor; - vTile = aTile; - } - )"; + // Smoke particle buffer + bci.size = MAX_SMOKE_PARTICLES * 6 * sizeof(float); + vmaCreateBuffer(vkCtx_->getAllocator(), &bci, &aci, &smokeVB_, &smokeVBAlloc_, &allocInfo); + smokeVBMapped_ = allocInfo.pMappedData; - const char* particleFragSrc = R"( - #version 330 core - in vec4 vColor; - in float vTile; - uniform sampler2D uTexture; - uniform vec2 uTileCount; - uniform bool uAlphaKey; - out vec4 FragColor; - - void main() { - // Circular soft-edge falloff (GL_POINTS are square by default) - vec2 center = gl_PointCoord - vec2(0.5); - float dist = length(center); - if (dist > 0.5) discard; - float edgeFade = smoothstep(0.5, 0.2, dist); - - vec2 tileCount = max(uTileCount, vec2(1.0)); - float tilesX = tileCount.x; - float tilesY = tileCount.y; - float tileMax = max(tilesX * tilesY - 1.0, 0.0); - float tile = clamp(vTile, 0.0, tileMax); - float col = mod(tile, tilesX); - float row = floor(tile / tilesX); - vec2 tileSize = vec2(1.0 / tilesX, 1.0 / tilesY); - vec2 uv = gl_PointCoord * tileSize + vec2(col, row) * tileSize; - vec4 texColor = texture(uTexture, uv); - - // Alpha-key particle textures often encode transparency as near-black - // color without meaningful alpha. - if (uAlphaKey) { - float maxRgb = max(texColor.r, max(texColor.g, texColor.b)); - if (maxRgb < 0.06 || texColor.a < 0.5) discard; - } - - FragColor = texColor * vColor; - FragColor.a *= edgeFade; - if (FragColor.a < 0.01) discard; - } - )"; - - GLuint vs = glCreateShader(GL_VERTEX_SHADER); - glShaderSource(vs, 1, &particleVertSrc, nullptr); - glCompileShader(vs); - - GLuint fs = glCreateShader(GL_FRAGMENT_SHADER); - glShaderSource(fs, 1, &particleFragSrc, nullptr); - glCompileShader(fs); - - m2ParticleShader_ = glCreateProgram(); - glAttachShader(m2ParticleShader_, vs); - glAttachShader(m2ParticleShader_, fs); - glLinkProgram(m2ParticleShader_); - glDeleteShader(vs); - glDeleteShader(fs); - - // Create particle VAO/VBO: 9 floats per particle (pos3 + rgba4 + size1 + tile1) - glGenVertexArrays(1, &m2ParticleVAO_); - glGenBuffers(1, &m2ParticleVBO_); - glBindVertexArray(m2ParticleVAO_); - glBindBuffer(GL_ARRAY_BUFFER, m2ParticleVBO_); - glBufferData(GL_ARRAY_BUFFER, MAX_M2_PARTICLES * 9 * sizeof(float), nullptr, GL_DYNAMIC_DRAW); - // Position (3f) - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)0); - // Color (4f) - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(3 * sizeof(float))); - // Size (1f) - glEnableVertexAttribArray(2); - glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(7 * sizeof(float))); - // Tile index (1f) - glEnableVertexAttribArray(3); - glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(8 * sizeof(float))); - glBindVertexArray(0); + // M2 particle buffer + bci.size = MAX_M2_PARTICLES * 9 * sizeof(float); + vmaCreateBuffer(vkCtx_->getAllocator(), &bci, &aci, &m2ParticleVB_, &m2ParticleVBAlloc_, &allocInfo); + m2ParticleVBMapped_ = allocInfo.pMappedData; } - // Create white fallback texture - uint8_t white[] = {255, 255, 255, 255}; - glGenTextures(1, &whiteTexture); - glBindTexture(GL_TEXTURE_2D, whiteTexture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, white); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glBindTexture(GL_TEXTURE_2D, 0); + // --- Create white fallback texture --- + { + uint8_t white[] = {255, 255, 255, 255}; + whiteTexture_ = std::make_unique(); + whiteTexture_->upload(*vkCtx_, white, 1, 1, VK_FORMAT_R8G8B8A8_UNORM); + whiteTexture_->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_REPEAT); + } - // Generate soft radial gradient glow texture for light sprites + // --- Generate soft radial gradient glow texture --- { static constexpr int SZ = 64; std::vector px(SZ * SZ * 4); @@ -675,68 +594,125 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) { px[idx + 3] = static_cast(a * 255); } } - glGenTextures(1, &glowTexture); - glBindTexture(GL_TEXTURE_2D, glowTexture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, SZ, SZ, 0, GL_RGBA, GL_UNSIGNED_BYTE, px.data()); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glBindTexture(GL_TEXTURE_2D, 0); + glowTexture_ = std::make_unique(); + glowTexture_->upload(*vkCtx_, px.data(), SZ, SZ, VK_FORMAT_R8G8B8A8_UNORM); + glowTexture_->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE); } - LOG_INFO("M2 renderer initialized"); + LOG_INFO("M2 renderer initialized (Vulkan)"); initialized_ = true; return true; } void M2Renderer::shutdown() { LOG_INFO("Shutting down M2 renderer..."); + if (!vkCtx_) return; - // Delete GPU resources + vkDeviceWaitIdle(vkCtx_->getDevice()); + VkDevice device = vkCtx_->getDevice(); + VmaAllocator alloc = vkCtx_->getAllocator(); + + // Delete model GPU resources for (auto& [id, model] : models) { - if (model.vao != 0) glDeleteVertexArrays(1, &model.vao); - if (model.vbo != 0) glDeleteBuffers(1, &model.vbo); - if (model.ebo != 0) glDeleteBuffers(1, &model.ebo); + destroyModelGPU(model); } models.clear(); + + // Destroy instance bone buffers + for (auto& inst : instances) { + destroyInstanceBones(inst); + } instances.clear(); spatialGrid.clear(); instanceIndexById.clear(); // Delete cached textures - for (auto& [path, entry] : textureCache) { - GLuint texId = entry.id; - if (texId != 0 && texId != whiteTexture) { - glDeleteTextures(1, &texId); - } - } textureCache.clear(); textureCacheBytes_ = 0; textureCacheCounter_ = 0; - textureHasAlphaById_.clear(); - textureColorKeyBlackById_.clear(); - if (whiteTexture != 0) { - glDeleteTextures(1, &whiteTexture); - whiteTexture = 0; - } - if (glowTexture != 0) { - glDeleteTextures(1, &glowTexture); - glowTexture = 0; - } + textureHasAlphaByPtr_.clear(); + textureColorKeyBlackByPtr_.clear(); + whiteTexture_.reset(); + glowTexture_.reset(); - shader.reset(); - - // Clean up smoke particle resources - if (smokeVAO != 0) { glDeleteVertexArrays(1, &smokeVAO); smokeVAO = 0; } - if (smokeVBO != 0) { glDeleteBuffers(1, &smokeVBO); smokeVBO = 0; } - smokeShader.reset(); + // Clean up particle buffers + if (smokeVB_) { vmaDestroyBuffer(alloc, smokeVB_, smokeVBAlloc_); smokeVB_ = VK_NULL_HANDLE; } + if (m2ParticleVB_) { vmaDestroyBuffer(alloc, m2ParticleVB_, m2ParticleVBAlloc_); m2ParticleVB_ = VK_NULL_HANDLE; } smokeParticles.clear(); - // Clean up M2 particle resources - if (m2ParticleVAO_ != 0) { glDeleteVertexArrays(1, &m2ParticleVAO_); m2ParticleVAO_ = 0; } - if (m2ParticleVBO_ != 0) { glDeleteBuffers(1, &m2ParticleVBO_); m2ParticleVBO_ = 0; } - if (m2ParticleShader_ != 0) { glDeleteProgram(m2ParticleShader_); m2ParticleShader_ = 0; } + // Destroy pipelines + auto destroyPipeline = [&](VkPipeline& p) { if (p) { vkDestroyPipeline(device, p, nullptr); p = VK_NULL_HANDLE; } }; + destroyPipeline(opaquePipeline_); + destroyPipeline(alphaTestPipeline_); + destroyPipeline(alphaPipeline_); + destroyPipeline(additivePipeline_); + destroyPipeline(particlePipeline_); + destroyPipeline(particleAdditivePipeline_); + destroyPipeline(smokePipeline_); + + if (pipelineLayout_) { vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); pipelineLayout_ = VK_NULL_HANDLE; } + if (particlePipelineLayout_) { vkDestroyPipelineLayout(device, particlePipelineLayout_, nullptr); particlePipelineLayout_ = VK_NULL_HANDLE; } + if (smokePipelineLayout_) { vkDestroyPipelineLayout(device, smokePipelineLayout_, nullptr); smokePipelineLayout_ = VK_NULL_HANDLE; } + + // Destroy descriptor pools and layouts + if (materialDescPool_) { vkDestroyDescriptorPool(device, materialDescPool_, nullptr); materialDescPool_ = VK_NULL_HANDLE; } + if (boneDescPool_) { vkDestroyDescriptorPool(device, boneDescPool_, nullptr); boneDescPool_ = VK_NULL_HANDLE; } + if (materialSetLayout_) { vkDestroyDescriptorSetLayout(device, materialSetLayout_, nullptr); materialSetLayout_ = VK_NULL_HANDLE; } + if (boneSetLayout_) { vkDestroyDescriptorSetLayout(device, boneSetLayout_, nullptr); boneSetLayout_ = VK_NULL_HANDLE; } + if (particleTexLayout_) { vkDestroyDescriptorSetLayout(device, particleTexLayout_, nullptr); particleTexLayout_ = VK_NULL_HANDLE; } + + // Destroy shadow resources + destroyPipeline(shadowPipeline_); + if (shadowPipelineLayout_) { vkDestroyPipelineLayout(device, shadowPipelineLayout_, nullptr); shadowPipelineLayout_ = VK_NULL_HANDLE; } + if (shadowParamsPool_) { vkDestroyDescriptorPool(device, shadowParamsPool_, nullptr); shadowParamsPool_ = VK_NULL_HANDLE; } + if (shadowParamsLayout_) { vkDestroyDescriptorSetLayout(device, shadowParamsLayout_, nullptr); shadowParamsLayout_ = VK_NULL_HANDLE; } + if (shadowParamsUBO_) { vmaDestroyBuffer(alloc, shadowParamsUBO_, shadowParamsAlloc_); shadowParamsUBO_ = VK_NULL_HANDLE; } + + initialized_ = false; +} + +void M2Renderer::destroyModelGPU(M2ModelGPU& model) { + if (!vkCtx_) return; + VmaAllocator alloc = vkCtx_->getAllocator(); + if (model.vertexBuffer) { vmaDestroyBuffer(alloc, model.vertexBuffer, model.vertexAlloc); model.vertexBuffer = VK_NULL_HANDLE; } + if (model.indexBuffer) { vmaDestroyBuffer(alloc, model.indexBuffer, model.indexAlloc); model.indexBuffer = VK_NULL_HANDLE; } + for (auto& batch : model.batches) { + if (batch.materialUBO) { vmaDestroyBuffer(alloc, batch.materialUBO, batch.materialUBOAlloc); batch.materialUBO = VK_NULL_HANDLE; } + // materialSet freed when pool is reset/destroyed + } +} + +void M2Renderer::destroyInstanceBones(M2Instance& inst) { + if (!vkCtx_) return; + VmaAllocator alloc = vkCtx_->getAllocator(); + for (int i = 0; i < 2; i++) { + if (inst.boneBuffer[i]) { + vmaDestroyBuffer(alloc, inst.boneBuffer[i], inst.boneAlloc[i]); + inst.boneBuffer[i] = VK_NULL_HANDLE; + inst.boneMapped[i] = nullptr; + } + // boneSet freed when pool is reset/destroyed + } +} + +VkDescriptorSet M2Renderer::allocateMaterialSet() { + VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + ai.descriptorPool = materialDescPool_; + ai.descriptorSetCount = 1; + ai.pSetLayouts = &materialSetLayout_; + VkDescriptorSet set = VK_NULL_HANDLE; + vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &set); + return set; +} + +VkDescriptorSet M2Renderer::allocateBoneSet() { + VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + ai.descriptorPool = boneDescPool_; + ai.descriptorSetCount = 1; + ai.pSetLayouts = &boneSetLayout_; + VkDescriptorSet set = VK_NULL_HANDLE; + vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &set); + return set; } // --------------------------------------------------------------------------- @@ -1034,10 +1010,6 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { gpuModel.indexCount = static_cast(model.indices.size()); gpuModel.vertexCount = static_cast(model.vertices.size()); - // Create VAO - glGenVertexArrays(1, &gpuModel.vao); - glBindVertexArray(gpuModel.vao); - // Store bone/sequence data for animation gpuModel.bones = model.bones; gpuModel.sequences = model.sequences; @@ -1116,37 +1088,29 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { vertexData.push_back(static_cast(std::min(v.boneIndices[3], uint8_t(127)))); } - glGenBuffers(1, &gpuModel.vbo); - glBindBuffer(GL_ARRAY_BUFFER, gpuModel.vbo); - glBufferData(GL_ARRAY_BUFFER, vertexData.size() * sizeof(float), - vertexData.data(), GL_STATIC_DRAW); + // Upload vertex buffer to GPU + { + auto buf = uploadBuffer(*vkCtx_, + vertexData.data(), vertexData.size() * sizeof(float), + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + gpuModel.vertexBuffer = buf.buffer; + gpuModel.vertexAlloc = buf.allocation; + } - glGenBuffers(1, &gpuModel.ebo); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpuModel.ebo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, model.indices.size() * sizeof(uint16_t), - model.indices.data(), GL_STATIC_DRAW); - - const size_t stride = floatsPerVertex * sizeof(float); - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0); - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(2); - glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride, (void*)(6 * sizeof(float))); - glEnableVertexAttribArray(5); - glVertexAttribPointer(5, 2, GL_FLOAT, GL_FALSE, stride, (void*)(8 * sizeof(float))); - glEnableVertexAttribArray(3); - glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, stride, (void*)(10 * sizeof(float))); - glEnableVertexAttribArray(4); - glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, stride, (void*)(14 * sizeof(float))); + // Upload index buffer to GPU + { + auto buf = uploadBuffer(*vkCtx_, + model.indices.data(), model.indices.size() * sizeof(uint16_t), + VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + gpuModel.indexBuffer = buf.buffer; + gpuModel.indexAlloc = buf.allocation; + } } - glBindVertexArray(0); - // Load ALL textures from the model into a local vector. // textureLoadFailed[i] is true if texture[i] had a named path that failed to load. // Such batches are hidden (batchOpacity=0) rather than rendered white. - std::vector allTextures; + std::vector allTextures; std::vector textureLoadFailed; std::vector textureKeysLower; if (assetManager) { @@ -1164,8 +1128,8 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { std::replace(texKey.begin(), texKey.end(), '/', '\\'); std::transform(texKey.begin(), texKey.end(), texKey.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); - GLuint texId = loadTexture(texPath, tex.flags); - bool failed = (texId == whiteTexture); + VkTexture* texPtr = loadTexture(texPath, tex.flags); + bool failed = (texPtr == whiteTexture_.get()); if (failed) { static std::unordered_set loggedModelTextureFails; std::string failKey = model.name + "|" + texKey; @@ -1176,14 +1140,14 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { if (isInvisibleTrap) { LOG_INFO(" InvisibleTrap texture[", ti, "]: ", texPath, " -> ", (failed ? "WHITE" : "OK")); } - allTextures.push_back(texId); + allTextures.push_back(texPtr); textureLoadFailed.push_back(failed); textureKeysLower.push_back(std::move(texKey)); } else { if (isInvisibleTrap) { LOG_INFO(" InvisibleTrap texture[", ti, "]: EMPTY (using white fallback)"); } - allTextures.push_back(whiteTexture); + allTextures.push_back(whiteTexture_.get()); textureLoadFailed.push_back(false); // Empty filename = intentional white (type!=0) textureKeysLower.emplace_back(); } @@ -1210,10 +1174,10 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { // Copy particle emitter data and resolve textures gpuModel.particleEmitters = model.particleEmitters; - gpuModel.particleTextures.resize(model.particleEmitters.size(), whiteTexture); + gpuModel.particleTextures.resize(model.particleEmitters.size(), whiteTexture_.get()); for (size_t ei = 0; ei < model.particleEmitters.size(); ei++) { uint16_t texIdx = model.particleEmitters[ei].texture; - if (texIdx < allTextures.size() && allTextures[texIdx] != 0) { + if (texIdx < allTextures.size() && allTextures[texIdx] != nullptr) { gpuModel.particleTextures[ei] = allTextures[texIdx]; } } @@ -1246,7 +1210,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { bgpu.submeshLevel = batch.submeshLevel; // Resolve texture: batch.textureIndex → textureLookup → allTextures - GLuint tex = whiteTexture; + VkTexture* tex = whiteTexture_.get(); bool texFailed = false; std::string batchTexKeyLower; if (batch.textureIndex < model.textureLookup.size()) { @@ -1271,8 +1235,8 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { if (texFailed && groundDetailModel) { static const std::string kDetailFallbackTexture = "World\\NoDXT\\Detail\\8des_detaildoodads01.blp"; - GLuint fallbackTex = loadTexture(kDetailFallbackTexture, 0); - if (fallbackTex != 0 && fallbackTex != whiteTexture) { + VkTexture* fallbackTex = loadTexture(kDetailFallbackTexture, 0); + if (fallbackTex != nullptr && fallbackTex != whiteTexture_.get()) { tex = fallbackTex; texFailed = false; } @@ -1333,15 +1297,15 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { (batchTexKeyLower.find("ruby") != std::string::npos); bgpu.glowTint = texCoolTint ? 1 : (texRedTint ? 2 : 0); bool texHasAlpha = false; - if (tex != 0 && tex != whiteTexture) { - auto ait = textureHasAlphaById_.find(tex); - texHasAlpha = (ait != textureHasAlphaById_.end()) ? ait->second : false; + if (tex != nullptr && tex != whiteTexture_.get()) { + auto ait = textureHasAlphaByPtr_.find(tex); + texHasAlpha = (ait != textureHasAlphaByPtr_.end()) ? ait->second : false; } bgpu.hasAlpha = texHasAlpha; bool colorKeyBlack = false; - if (tex != 0 && tex != whiteTexture) { - auto cit = textureColorKeyBlackById_.find(tex); - colorKeyBlack = (cit != textureColorKeyBlackById_.end()) ? cit->second : false; + if (tex != nullptr && tex != whiteTexture_.get()) { + auto cit = textureColorKeyBlackByPtr_.find(tex); + colorKeyBlack = (cit != textureColorKeyBlackByPtr_.end()) ? cit->second : false; } bgpu.colorKeyBlack = colorKeyBlack; // textureCoordIndex is an index into a texture coord combo table, not directly @@ -1407,17 +1371,17 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { M2ModelGPU::BatchGPU bgpu; bgpu.indexStart = 0; bgpu.indexCount = gpuModel.indexCount; - bgpu.texture = allTextures.empty() ? whiteTexture : allTextures[0]; + bgpu.texture = allTextures.empty() ? whiteTexture_.get() : allTextures[0]; bool texHasAlpha = false; - if (bgpu.texture != 0 && bgpu.texture != whiteTexture) { - auto ait = textureHasAlphaById_.find(bgpu.texture); - texHasAlpha = (ait != textureHasAlphaById_.end()) ? ait->second : false; + if (bgpu.texture != nullptr && bgpu.texture != whiteTexture_.get()) { + auto ait = textureHasAlphaByPtr_.find(bgpu.texture); + texHasAlpha = (ait != textureHasAlphaByPtr_.end()) ? ait->second : false; } bgpu.hasAlpha = texHasAlpha; bool colorKeyBlack = false; - if (bgpu.texture != 0 && bgpu.texture != whiteTexture) { - auto cit = textureColorKeyBlackById_.find(bgpu.texture); - colorKeyBlack = (cit != textureColorKeyBlackById_.end()) ? cit->second : false; + if (bgpu.texture != nullptr && bgpu.texture != whiteTexture_.get()) { + auto cit = textureColorKeyBlackByPtr_.find(bgpu.texture); + colorKeyBlack = (cit != textureColorKeyBlackByPtr_.end()) ? cit->second : false; } bgpu.colorKeyBlack = colorKeyBlack; gpuModel.batches.push_back(bgpu); @@ -1438,6 +1402,65 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { } } + // Allocate Vulkan descriptor sets and UBOs for each batch + for (auto& bgpu : gpuModel.batches) { + // Create combined UBO for M2Params (binding 1) + M2Material (binding 2) + // We allocate them as separate buffers for clarity + VmaAllocationInfo matAllocInfo{}; + { + VkBufferCreateInfo bci{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bci.size = sizeof(M2MaterialUBO); + bci.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + VmaAllocationCreateInfo aci{}; + aci.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + aci.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + vmaCreateBuffer(vkCtx_->getAllocator(), &bci, &aci, &bgpu.materialUBO, &bgpu.materialUBOAlloc, &matAllocInfo); + + // Write initial material data (static per-batch — fadeAlpha/interiorDarken updated at draw time) + M2MaterialUBO mat{}; + mat.hasTexture = (bgpu.texture != nullptr && bgpu.texture != whiteTexture_.get()) ? 1 : 0; + mat.alphaTest = (bgpu.blendMode == 1 || (bgpu.blendMode >= 2 && !bgpu.hasAlpha)) ? 1 : 0; + mat.colorKeyBlack = bgpu.colorKeyBlack ? 1 : 0; + mat.colorKeyThreshold = 0.08f; + mat.unlit = (bgpu.materialFlags & 0x01) ? 1 : 0; + mat.blendMode = bgpu.blendMode; + mat.fadeAlpha = 1.0f; + mat.interiorDarken = 0.0f; + mat.specularIntensity = 0.5f; + memcpy(matAllocInfo.pMappedData, &mat, sizeof(mat)); + } + + // Allocate descriptor set and write all bindings + bgpu.materialSet = allocateMaterialSet(); + if (bgpu.materialSet) { + VkTexture* batchTex = bgpu.texture ? bgpu.texture : whiteTexture_.get(); + VkDescriptorImageInfo imgInfo = batchTex->descriptorInfo(); + + VkDescriptorBufferInfo matBufInfo{}; + matBufInfo.buffer = bgpu.materialUBO; + matBufInfo.offset = 0; + matBufInfo.range = sizeof(M2MaterialUBO); + + VkWriteDescriptorSet writes[2] = {}; + // binding 0: texture + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].dstSet = bgpu.materialSet; + writes[0].dstBinding = 0; + writes[0].descriptorCount = 1; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[0].pImageInfo = &imgInfo; + // binding 2: M2Material UBO + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].dstSet = bgpu.materialSet; + writes[1].dstBinding = 2; + writes[1].descriptorCount = 1; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writes[1].pBufferInfo = &matBufInfo; + + vkUpdateDescriptorSets(vkCtx_->getDevice(), 2, writes, 0, nullptr); + } + } + models[modelId] = std::move(gpuModel); LOG_DEBUG("Loaded M2 model: ", model.name, " (", models[modelId].vertexCount, " vertices, ", @@ -1921,8 +1944,8 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: } } -void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection) { - if (instances.empty() || !shader) { +void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera) { + if (instances.empty() || !opaquePipeline_) { return; } @@ -1935,41 +1958,15 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: LOG_INFO("M2 render: ", instances.size(), " instances, ", models.size(), " models"); } - // Set up GL state for M2 rendering - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_LEQUAL); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glDisable(GL_CULL_FACE); // Some M2 geometry is single-sided - // Build frustum for culling + const glm::mat4 view = camera.getViewMatrix(); + const glm::mat4 projection = camera.getProjectionMatrix(); Frustum frustum; frustum.extractFromMatrix(projection * view); // Reuse persistent buffers (clear instead of reallocating) glowSprites_.clear(); - shader->use(); - shader->setUniform("uView", view); - shader->setUniform("uProjection", projection); - shader->setUniform("uLightDir", lightDir); - shader->setUniform("uLightColor", lightColor); - shader->setUniform("uSpecularIntensity", 0.5f); - shader->setUniform("uAmbientColor", ambientColor); - shader->setUniform("uViewPos", camera.getPosition()); - shader->setUniform("uFogColor", fogColor); - shader->setUniform("uFogStart", fogStart); - shader->setUniform("uFogEnd", fogEnd); - bool useShadows = shadowEnabled; - shader->setUniform("uShadowEnabled", useShadows ? 1 : 0); - shader->setUniform("uShadowStrength", 0.68f); - if (useShadows) { - shader->setUniform("uLightSpaceMatrix", lightSpaceMatrix); - glActiveTexture(GL_TEXTURE7); - glBindTexture(GL_TEXTURE_2D, shadowDepthTex); - shader->setUniform("uShadowMap", 7); - } - lastDrawCallCount = 0; // Adaptive render distance: balanced for performance without excessive pop-in @@ -2027,7 +2024,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: sortedVisible_.push_back({i, instance.modelId, distSq, effectiveMaxDistSq}); } - // Sort by modelId to minimize VAO rebinds (using stable_sort for better cache behavior) + // Sort by modelId to minimize vertex/index buffer rebinds std::stable_sort(sortedVisible_.begin(), sortedVisible_.end(), [](const VisibleEntry& a, const VisibleEntry& b) { return a.modelId < b.modelId; }); @@ -2037,55 +2034,42 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: uint32_t currentModelId = UINT32_MAX; const M2ModelGPU* currentModel = nullptr; - // State tracking to avoid redundant GL calls (similar to WMO renderer optimization) - static GLuint lastBoundTexture = 0; - static bool lastHasTexture = false; - static bool lastAlphaTest = false; - static bool lastColorKeyBlack = false; - static bool lastUnlit = false; - static bool lastUseBones = false; - static bool lastInteriorDarken = false; - static uint8_t lastBlendMode = 255; // Invalid initial value - static bool depthMaskState = true; // Track current depth mask state - static glm::vec2 lastUVOffset = glm::vec2(-999.0f); // Track UV offset state - static int lastTexCoordSet = -1; // Track active UV set (0 or 1) - - // Reset state tracking at start of frame to handle shader rebinds - lastBoundTexture = 0; - lastHasTexture = false; - lastAlphaTest = false; - lastColorKeyBlack = false; - lastUnlit = false; - lastUseBones = false; - lastInteriorDarken = false; - lastBlendMode = 255; - depthMaskState = true; - lastUVOffset = glm::vec2(-999.0f); - lastTexCoordSet = -1; - - // Set texture unit once per frame instead of per-batch - glActiveTexture(GL_TEXTURE0); - shader->setUniform("uTexture", 0); // Texture unit 0, set once per frame - shader->setUniform("uColorKeyBlack", false); - shader->setUniform("uColorKeyThreshold", 0.08f); - shader->setUniform("uBlendMode", 0); - - // Performance counters + // State tracking + VkPipeline currentPipeline = VK_NULL_HANDLE; uint32_t boneMatrixUploads = 0; uint32_t totalBatchesDrawn = 0; + uint32_t frameIndex = vkCtx_->getCurrentFrame(); + + // Push constants struct matching m2.vert.glsl push_constant block + struct M2PushConstants { + glm::mat4 model; + glm::vec2 uvOffset; + int texCoordSet; + int useBones; + }; + + // Bind per-frame descriptor set (set 0) — shared across all draws + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + pipelineLayout_, 0, 1, &perFrameSet, 0, nullptr); + + // Start with opaque pipeline + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, opaquePipeline_); + currentPipeline = opaquePipeline_; for (const auto& entry : sortedVisible_) { if (entry.index >= instances.size()) continue; - const auto& instance = instances[entry.index]; + auto& instance = instances[entry.index]; - // Bind VAO once per model group + // Bind vertex + index buffers once per model group if (entry.modelId != currentModelId) { - if (currentModel) glBindVertexArray(0); currentModelId = entry.modelId; auto mdlIt = models.find(currentModelId); if (mdlIt == models.end()) continue; currentModel = &mdlIt->second; - glBindVertexArray(currentModel->vao); + if (!currentModel->vertexBuffer) continue; + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, ¤tModel->vertexBuffer, &offset); + vkCmdBindIndexBuffer(cmd, currentModel->indexBuffer, 0, VK_INDEX_TYPE_UINT16); } const M2ModelGPU& model = *currentModel; @@ -2099,62 +2083,73 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: (entry.effectiveMaxDistSq - fadeStartDistSq), 0.0f, 1.0f); } - // Always update per-instance uniforms (these change every instance) float instanceFadeAlpha = fadeAlpha; if (model.isGroundDetail) { instanceFadeAlpha *= 0.82f; } - shader->setUniform("uModel", instance.modelMatrix); - shader->setUniform("uFadeAlpha", instanceFadeAlpha); - // Track interior darken state to avoid redundant updates - if (insideInterior != lastInteriorDarken) { - shader->setUniform("uInteriorDarken", insideInterior); - lastInteriorDarken = insideInterior; - } - - // Upload bone matrices if model has skeletal animation + // Upload bone matrices to SSBO if model has skeletal animation bool useBones = model.hasAnimation && !model.disableAnimation && !instance.boneMatrices.empty(); - if (useBones != lastUseBones) { - shader->setUniform("uUseBones", useBones); - lastUseBones = useBones; - } if (useBones) { - int numBones = std::min(static_cast(instance.boneMatrices.size()), 128); - shader->setUniformMatrixArray("uBones[0]", instance.boneMatrices.data(), numBones); - boneMatrixUploads++; - } + // Lazy-allocate bone SSBO on first use + if (!instance.boneBuffer[frameIndex]) { + VkBufferCreateInfo bci{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bci.size = 128 * sizeof(glm::mat4); // max 128 bones + bci.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + VmaAllocationCreateInfo aci{}; + aci.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + aci.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + VmaAllocationInfo allocInfo{}; + vmaCreateBuffer(vkCtx_->getAllocator(), &bci, &aci, + &instance.boneBuffer[frameIndex], &instance.boneAlloc[frameIndex], &allocInfo); + instance.boneMapped[frameIndex] = allocInfo.pMappedData; - // Disable depth writes for fading objects to avoid z-fighting - if (instanceFadeAlpha < 1.0f) { - if (depthMaskState) { - glDepthMask(GL_FALSE); - depthMaskState = false; + // Allocate descriptor set for bone SSBO + instance.boneSet[frameIndex] = allocateBoneSet(); + if (instance.boneSet[frameIndex]) { + VkDescriptorBufferInfo bufInfo{}; + bufInfo.buffer = instance.boneBuffer[frameIndex]; + bufInfo.offset = 0; + bufInfo.range = bci.size; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = instance.boneSet[frameIndex]; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + write.pBufferInfo = &bufInfo; + vkUpdateDescriptorSets(vkCtx_->getDevice(), 1, &write, 0, nullptr); + } + } + + // Upload bone matrices + if (instance.boneMapped[frameIndex]) { + int numBones = std::min(static_cast(instance.boneMatrices.size()), 128); + memcpy(instance.boneMapped[frameIndex], instance.boneMatrices.data(), + numBones * sizeof(glm::mat4)); + boneMatrixUploads++; + } + + // Bind bone descriptor set (set 2) + if (instance.boneSet[frameIndex]) { + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + pipelineLayout_, 2, 1, &instance.boneSet[frameIndex], 0, nullptr); } } - // LOD selection based on distance (WoW retail behavior) - // submeshLevel: 0=base detail, 1=LOD1, 2=LOD2, 3=LOD3 + // LOD selection based on distance float dist = std::sqrt(entry.distSq); uint16_t desiredLOD = 0; - if (dist > 150.0f) desiredLOD = 3; // Far: LOD3 (lowest detail) - else if (dist > 80.0f) desiredLOD = 2; // Medium-far: LOD2 - else if (dist > 40.0f) desiredLOD = 1; // Medium: LOD1 - // else desiredLOD = 0 (close: base detail) + if (dist > 150.0f) desiredLOD = 3; + else if (dist > 80.0f) desiredLOD = 2; + else if (dist > 40.0f) desiredLOD = 1; - // Check if model has the desired LOD level; if not, fall back to LOD 0 uint16_t targetLOD = desiredLOD; if (desiredLOD > 0) { bool hasDesiredLOD = false; for (const auto& b : model.batches) { - if (b.submeshLevel == desiredLOD) { - hasDesiredLOD = true; - break; - } - } - if (!hasDesiredLOD) { - targetLOD = 0; // Fall back to base LOD + if (b.submeshLevel == desiredLOD) { hasDesiredLOD = true; break; } } + if (!hasDesiredLOD) targetLOD = 0; } std::string modelKeyLower = model.name; @@ -2162,11 +2157,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: [](unsigned char c) { return static_cast(std::tolower(c)); }); for (const auto& batch : model.batches) { if (batch.indexCount == 0) continue; - - // Skip batches that don't match target LOD level if (!model.isGroundDetail && batch.submeshLevel != targetLOD) continue; - - // Skip batches with zero opacity from texture weight tracks (should be invisible) if (batch.batchOpacity < 0.01f) continue; const bool koboldFlameCard = @@ -2176,8 +2167,6 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: (modelKeyLower.find("torch") != std::string::npos) || (modelKeyLower.find("mine") != std::string::npos)); - // Replace only likely flame-card submeshes with sprite glow. Keep larger geometry - // (lantern housings, posts, etc.) authored so the prop itself remains visible. const bool smallCardLikeBatch = (batch.glowSize <= 1.35f) || (batch.lanternGlowHint && batch.glowSize <= 6.0f); @@ -2212,7 +2201,6 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: } gs.size = batch.glowSize * instance.scale * 1.45f; glowSprites_.push_back(gs); - // Add wider, softer halo to avoid hard "disk" look. GlowSprite halo = gs; halo.color.a *= 0.42f; halo.size *= 1.8f; @@ -2222,15 +2210,13 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: (batch.blendMode >= 3) || batch.colorKeyBlack || ((batch.materialFlags & 0x01) != 0); - // Keep lantern/light model geometry visible; sprite glow should augment, - // not replace, those props. if ((batch.glowCardLike && lanternLikeModel) || (cardLikeSkipMesh && !lanternLikeModel)) { continue; } } - // Compute UV offset for texture animation (only set uniform if changed) + // Compute UV offset for texture animation glm::vec2 uvOffset(0.0f, 0.0f); if (batch.textureAnimIndex != 0xFFFF && model.hasTextureAnimation) { uint16_t lookupIdx = batch.textureAnimIndex; @@ -2245,177 +2231,101 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: } } } - // Only update uniform if UV offset changed (most batches have 0,0) - if (uvOffset != lastUVOffset) { - shader->setUniform("uUVOffset", uvOffset); - lastUVOffset = uvOffset; - } - // Apply per-batch blend mode from M2 material (only if changed) - // 0=Opaque, 1=AlphaKey, 2=Alpha, 3=Add, 4=Mod, 5=Mod2x, 6=BlendAdd, 7=Screen - bool batchTransparent = false; - // Spell effects: override Mod/Mod2x to Additive for bright glow rendering + // Select pipeline based on blend mode uint8_t effectiveBlendMode = batch.blendMode; if (model.isSpellEffect && (effectiveBlendMode == 4 || effectiveBlendMode == 5)) { - effectiveBlendMode = 3; // Additive + effectiveBlendMode = 3; } if (model.isGroundDetail) { - // Use regular alpha blending for detail cards to avoid hard cutout loss. effectiveBlendMode = 2; } - if (effectiveBlendMode != lastBlendMode) { - switch (effectiveBlendMode) { - case 0: // Opaque - glBlendFunc(GL_ONE, GL_ZERO); - break; - case 1: // Alpha Key (alpha test, handled by uAlphaTest) - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - break; - case 2: // Alpha - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - batchTransparent = true; - break; - case 3: // Additive - glBlendFunc(GL_SRC_ALPHA, GL_ONE); - batchTransparent = true; - break; - case 4: // Mod - glBlendFunc(GL_DST_COLOR, GL_ZERO); - batchTransparent = true; - break; - case 5: // Mod2x - glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR); - batchTransparent = true; - break; - case 6: // BlendAdd - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - batchTransparent = true; - break; - default: // Fallback - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - break; - } - lastBlendMode = effectiveBlendMode; - shader->setUniform("uBlendMode", static_cast(effectiveBlendMode)); - } else { - // Still need to know if batch is transparent for depth mask logic - batchTransparent = (effectiveBlendMode >= 2); + + VkPipeline desiredPipeline; + switch (effectiveBlendMode) { + case 0: desiredPipeline = opaquePipeline_; break; + case 1: desiredPipeline = alphaTestPipeline_; break; + case 2: desiredPipeline = alphaPipeline_; break; + default: desiredPipeline = additivePipeline_; break; + } + if (desiredPipeline != currentPipeline) { + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, desiredPipeline); + currentPipeline = desiredPipeline; } - // Disable depth writes for transparent/additive batches - if (batchTransparent && instanceFadeAlpha >= 1.0f) { - if (depthMaskState) { - glDepthMask(GL_FALSE); - depthMaskState = false; + // Update material UBO with per-draw dynamic values (fadeAlpha, interiorDarken) + if (batch.materialUBO) { + // The UBO is mapped — update fadeAlpha and interiorDarken fields in-place + VmaAllocationInfo allocInfo{}; + vmaGetAllocationInfo(vkCtx_->getAllocator(), batch.materialUBOAlloc, &allocInfo); + if (allocInfo.pMappedData) { + auto* mat = static_cast(allocInfo.pMappedData); + mat->fadeAlpha = instanceFadeAlpha; + mat->interiorDarken = insideInterior ? 1.0f : 0.0f; + // Update colorKeyThreshold for Mod/Mod2x blend modes + if (batch.colorKeyBlack) { + mat->colorKeyThreshold = (effectiveBlendMode == 4 || effectiveBlendMode == 5) ? 0.7f : 0.08f; + } + // Ground detail: override alphaTest and unlit + if (model.isGroundDetail) { + mat->alphaTest = 0; + mat->unlit = 0; + } } } - // Unlit: material flag 0x01 (only update if changed) - bool unlit = (batch.materialFlags & 0x01) != 0; - if (model.isGroundDetail) { - // Ground clutter should receive scene lighting so it doesn't glow. - unlit = false; - } - if (unlit != lastUnlit) { - shader->setUniform("uUnlit", unlit); - lastUnlit = unlit; + // Bind material descriptor set (set 1) + if (batch.materialSet) { + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + pipelineLayout_, 1, 1, &batch.materialSet, 0, nullptr); } - // Texture state (only update if changed) - bool hasTexture = (batch.texture != 0); - if (hasTexture != lastHasTexture) { - shader->setUniform("uHasTexture", hasTexture); - lastHasTexture = hasTexture; - } - - bool alphaTest = (effectiveBlendMode == 1) || - (effectiveBlendMode >= 2 && !batch.hasAlpha); - if (model.isGroundDetail) { - alphaTest = false; - } - if (alphaTest != lastAlphaTest) { - shader->setUniform("uAlphaTest", alphaTest); - lastAlphaTest = alphaTest; - } - bool colorKeyBlack = batch.colorKeyBlack; - if (colorKeyBlack != lastColorKeyBlack) { - shader->setUniform("uColorKeyBlack", colorKeyBlack); - lastColorKeyBlack = colorKeyBlack; - } - // ColorKeyBlack textures: discard dark pixels so background shows through. - // Mod blend (4) multiplies framebuffer by texture — dark pixels darken - // the scene, so use a high threshold to remove the dark rectangle. - if (colorKeyBlack) { - float thresh = 0.08f; - if (effectiveBlendMode == 4 || effectiveBlendMode == 5) { - thresh = 0.7f; // Mod/Mod2x: only keep near-white pixels - } - shader->setUniform("uColorKeyThreshold", thresh); - } - - // Only bind texture if it changed (texture unit already set to GL_TEXTURE0) - if (hasTexture && batch.texture != lastBoundTexture) { - glBindTexture(GL_TEXTURE_2D, batch.texture); - lastBoundTexture = batch.texture; - } - - // UV set selector (textureUnit: 0=UV0, 1=UV1) - int texCoordSet = static_cast(batch.textureUnit); - if (texCoordSet != lastTexCoordSet) { - shader->setUniform("uTexCoordSet", texCoordSet); - lastTexCoordSet = texCoordSet; - } - - glDrawElements(GL_TRIANGLES, batch.indexCount, GL_UNSIGNED_SHORT, - (void*)(batch.indexStart * sizeof(uint16_t))); + // Push constants + M2PushConstants pc; + pc.model = instance.modelMatrix; + pc.uvOffset = uvOffset; + pc.texCoordSet = static_cast(batch.textureUnit); + pc.useBones = useBones ? 1 : 0; + vkCmdPushConstants(cmd, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(pc), &pc); + vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0); totalBatchesDrawn++; - - // Restore depth writes after transparent batch - if (batchTransparent && fadeAlpha >= 1.0f) { - if (!depthMaskState) { - glDepthMask(GL_TRUE); - depthMaskState = true; - } - } - // Note: blend func restoration removed - state tracking handles it - lastDrawCallCount++; } - - // Restore depth mask after faded instance - if (fadeAlpha < 1.0f) { - if (!depthMaskState) { - glDepthMask(GL_TRUE); - depthMaskState = true; - } - } } - if (currentModel) glBindVertexArray(0); - // Render glow sprites as billboarded additive point lights - if (!glowSprites_.empty() && m2ParticleShader_ != 0 && m2ParticleVAO_ != 0) { - glUseProgram(m2ParticleShader_); + if (!glowSprites_.empty() && particleAdditivePipeline_ && m2ParticleVB_ && glowTexture_) { + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, particleAdditivePipeline_); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + particlePipelineLayout_, 0, 1, &perFrameSet, 0, nullptr); - GLint viewLoc = glGetUniformLocation(m2ParticleShader_, "uView"); - GLint projLoc = glGetUniformLocation(m2ParticleShader_, "uProjection"); - GLint texLoc = glGetUniformLocation(m2ParticleShader_, "uTexture"); - GLint tileLoc = glGetUniformLocation(m2ParticleShader_, "uTileCount"); - glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view)); - glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection)); - glUniform1i(texLoc, 0); - glUniform2f(tileLoc, 1.0f, 1.0f); + // Allocate a descriptor set for glow texture (from material pool using particle layout) + VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + ai.descriptorPool = materialDescPool_; + ai.descriptorSetCount = 1; + ai.pSetLayouts = &particleTexLayout_; + VkDescriptorSet glowSet = VK_NULL_HANDLE; + if (vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &glowSet) == VK_SUCCESS) { + VkDescriptorImageInfo imgInfo = glowTexture_->descriptorInfo(); + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = glowSet; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imgInfo; + vkUpdateDescriptorSets(vkCtx_->getDevice(), 1, &write, 0, nullptr); - glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Additive blending - glDepthMask(GL_FALSE); - glEnable(GL_PROGRAM_POINT_SIZE); - glDisable(GL_CULL_FACE); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + particlePipelineLayout_, 1, 1, &glowSet, 0, nullptr); + } - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, glowTexture); + // Push constants for particle: tileCount(vec2) + alphaKey(int) + struct { float tileX, tileY; int alphaKey; } particlePush = {1.0f, 1.0f, 0}; + vkCmdPushConstants(cmd, particlePipelineLayout_, VK_SHADER_STAGE_FRAGMENT_BIT, 0, + sizeof(particlePush), &particlePush); - // Build vertex data: pos(3) + color(4) + size(1) + tile(1) = 9 floats per sprite + // Build and upload vertex data std::vector glowData; glowData.reserve(glowSprites_.size() * 9); for (const auto& gs : glowSprites_) { @@ -2430,27 +2340,18 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: glowData.push_back(0.0f); } - glBindVertexArray(m2ParticleVAO_); - glBindBuffer(GL_ARRAY_BUFFER, m2ParticleVBO_); size_t uploadCount = std::min(glowSprites_.size(), MAX_M2_PARTICLES); - glBufferSubData(GL_ARRAY_BUFFER, 0, uploadCount * 9 * sizeof(float), glowData.data()); - glDrawArrays(GL_POINTS, 0, static_cast(uploadCount)); - glBindVertexArray(0); + memcpy(m2ParticleVBMapped_, glowData.data(), uploadCount * 9 * sizeof(float)); - glDepthMask(GL_TRUE); - glDisable(GL_PROGRAM_POINT_SIZE); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &m2ParticleVB_, &offset); + vkCmdDraw(cmd, static_cast(uploadCount), 1, 0, 0); } - // Restore state - glDisable(GL_BLEND); - glEnable(GL_CULL_FACE); - auto renderEndTime = std::chrono::high_resolution_clock::now(); double totalMs = std::chrono::duration(renderEndTime - renderStartTime).count(); double drawLoopMs = std::chrono::duration(renderEndTime - cullingSortTime).count(); - // Log detailed timing every 120 frames (~2 seconds at 60fps) static int frameCounter = 0; if (++frameCounter >= 120) { frameCounter = 0; @@ -2460,60 +2361,211 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: } } -void M2Renderer::renderShadow(GLuint shadowShaderProgram, const glm::vec3& shadowCenter, float halfExtent) { - if (instances.empty() || shadowShaderProgram == 0) { - return; +bool M2Renderer::initializeShadow(VkRenderPass shadowRenderPass) { + if (!vkCtx_ || shadowRenderPass == VK_NULL_HANDLE) return false; + VkDevice device = vkCtx_->getDevice(); + + // ShadowParams UBO: useBones, useTexture, alphaTest, foliageSway, windTime, foliageMotionDamp + struct ShadowParamsUBO { + int32_t useBones = 0; + int32_t useTexture = 0; + int32_t alphaTest = 0; + int32_t foliageSway = 0; + float windTime = 0.0f; + float foliageMotionDamp = 1.0f; + }; + + // Create ShadowParams UBO + VkBufferCreateInfo bufCI{}; + bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufCI.size = sizeof(ShadowParamsUBO); + bufCI.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + VmaAllocationCreateInfo allocCI{}; + allocCI.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + allocCI.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + VmaAllocationInfo allocInfo{}; + if (vmaCreateBuffer(vkCtx_->getAllocator(), &bufCI, &allocCI, + &shadowParamsUBO_, &shadowParamsAlloc_, &allocInfo) != VK_SUCCESS) { + LOG_ERROR("M2Renderer: failed to create shadow params UBO"); + return false; + } + ShadowParamsUBO defaultParams{}; + std::memcpy(allocInfo.pMappedData, &defaultParams, sizeof(defaultParams)); + + // Create descriptor set layout: binding 0 = sampler2D, binding 1 = ShadowParams UBO + VkDescriptorSetLayoutBinding layoutBindings[2]{}; + layoutBindings[0].binding = 0; + layoutBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + layoutBindings[0].descriptorCount = 1; + layoutBindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + layoutBindings[1].binding = 1; + layoutBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + layoutBindings[1].descriptorCount = 1; + layoutBindings[1].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + VkDescriptorSetLayoutCreateInfo layoutCI{}; + layoutCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + layoutCI.bindingCount = 2; + layoutCI.pBindings = layoutBindings; + if (vkCreateDescriptorSetLayout(device, &layoutCI, nullptr, &shadowParamsLayout_) != VK_SUCCESS) { + LOG_ERROR("M2Renderer: failed to create shadow params layout"); + return false; } - GLint modelLoc = glGetUniformLocation(shadowShaderProgram, "uModel"); - GLint useTexLoc = glGetUniformLocation(shadowShaderProgram, "uUseTexture"); - GLint texLoc = glGetUniformLocation(shadowShaderProgram, "uTexture"); - GLint alphaTestLoc = glGetUniformLocation(shadowShaderProgram, "uAlphaTest"); - GLint foliageSwayLoc = glGetUniformLocation(shadowShaderProgram, "uFoliageSway"); - if (modelLoc < 0) { - return; + // Create descriptor pool + VkDescriptorPoolSize poolSizes[2]{}; + poolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + poolSizes[0].descriptorCount = 1; + poolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + poolSizes[1].descriptorCount = 1; + VkDescriptorPoolCreateInfo poolCI{}; + poolCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolCI.maxSets = 1; + poolCI.poolSizeCount = 2; + poolCI.pPoolSizes = poolSizes; + if (vkCreateDescriptorPool(device, &poolCI, nullptr, &shadowParamsPool_) != VK_SUCCESS) { + LOG_ERROR("M2Renderer: failed to create shadow params pool"); + return false; } - if (useTexLoc >= 0) glUniform1i(useTexLoc, 0); - if (alphaTestLoc >= 0) glUniform1i(alphaTestLoc, 0); - if (foliageSwayLoc >= 0) glUniform1i(foliageSwayLoc, 0); - if (texLoc >= 0) glUniform1i(texLoc, 0); - glActiveTexture(GL_TEXTURE0); + // Allocate descriptor set + VkDescriptorSetAllocateInfo setAlloc{}; + setAlloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + setAlloc.descriptorPool = shadowParamsPool_; + setAlloc.descriptorSetCount = 1; + setAlloc.pSetLayouts = &shadowParamsLayout_; + if (vkAllocateDescriptorSets(device, &setAlloc, &shadowParamsSet_) != VK_SUCCESS) { + LOG_ERROR("M2Renderer: failed to allocate shadow params set"); + return false; + } + + // Write descriptors (use white fallback for binding 0) + VkDescriptorBufferInfo bufInfo{}; + bufInfo.buffer = shadowParamsUBO_; + bufInfo.offset = 0; + bufInfo.range = sizeof(ShadowParamsUBO); + + VkDescriptorImageInfo imgInfo{}; + imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + imgInfo.imageView = whiteTexture_->getImageView(); + imgInfo.sampler = whiteTexture_->getSampler(); + + VkWriteDescriptorSet writes[2]{}; + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].dstSet = shadowParamsSet_; + writes[0].dstBinding = 0; + writes[0].descriptorCount = 1; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[0].pImageInfo = &imgInfo; + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].dstSet = shadowParamsSet_; + writes[1].dstBinding = 1; + writes[1].descriptorCount = 1; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writes[1].pBufferInfo = &bufInfo; + vkUpdateDescriptorSets(device, 2, writes, 0, nullptr); + + // Create shadow pipeline layout: set 1 = shadowParamsLayout_, push constants = 128 bytes + VkPushConstantRange pc{}; + pc.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + pc.offset = 0; + pc.size = 128; // lightSpaceMatrix (64) + model (64) + shadowPipelineLayout_ = createPipelineLayout(device, {shadowParamsLayout_}, {pc}); + if (!shadowPipelineLayout_) { + LOG_ERROR("M2Renderer: failed to create shadow pipeline layout"); + return false; + } + + // Load shadow shaders + VkShaderModule vertShader, fragShader; + if (!vertShader.loadFromFile(device, "assets/shaders/shadow.vert.spv")) { + LOG_ERROR("M2Renderer: failed to load shadow vertex shader"); + return false; + } + if (!fragShader.loadFromFile(device, "assets/shaders/shadow.frag.spv")) { + LOG_ERROR("M2Renderer: failed to load shadow fragment shader"); + return false; + } + + // M2 vertex layout: 18 floats = 72 bytes stride + // loc0=pos(off0), loc1=normal(off12), loc2=texCoord0(off24), loc5=texCoord1(off32), + // loc3=boneWeights(off40), loc4=boneIndices(off56) + // Shadow shader locations: 0=aPos, 1=aTexCoord, 2=aBoneWeights, 3=aBoneIndicesF + // useBones=0 so locations 2,3 are never used + VkVertexInputBindingDescription vertBind{}; + vertBind.binding = 0; + vertBind.stride = 18 * sizeof(float); + vertBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + std::vector vertAttrs = { + {0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, // aPos -> position + {1, 0, VK_FORMAT_R32G32_SFLOAT, 6 * sizeof(float)}, // aTexCoord -> texCoord0 + {2, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 10 * sizeof(float)}, // aBoneWeights + {3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 14 * sizeof(float)}, // aBoneIndicesF + }; + + shadowPipeline_ = PipelineBuilder() + .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({vertBind}, vertAttrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_FRONT_BIT) + .setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL) + .setDepthBias(2.0f, 4.0f) + .setNoColorAttachment() + .setLayout(shadowPipelineLayout_) + .setRenderPass(shadowRenderPass) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device); + + vertShader.destroy(); + fragShader.destroy(); + + if (!shadowPipeline_) { + LOG_ERROR("M2Renderer: failed to create shadow pipeline"); + return false; + } + LOG_INFO("M2Renderer shadow pipeline initialized"); + return true; +} + +void M2Renderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix) { + if (!shadowPipeline_ || !shadowParamsSet_) return; + if (instances.empty() || models.empty()) return; + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipeline_); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipelineLayout_, + 0, 1, &shadowParamsSet_, 0, nullptr); + + struct ShadowPush { glm::mat4 lightSpaceMatrix; glm::mat4 model; }; + + uint32_t currentModelId = UINT32_MAX; + const M2ModelGPU* currentModel = nullptr; for (const auto& instance : instances) { - // Cull instances whose AABB doesn't overlap the shadow frustum (XY plane) - glm::vec3 instCenter = (instance.worldBoundsMin + instance.worldBoundsMax) * 0.5f; - glm::vec3 instHalf = (instance.worldBoundsMax - instance.worldBoundsMin) * 0.5f; - if (std::abs(instCenter.x - shadowCenter.x) > halfExtent + instHalf.x) continue; - if (std::abs(instCenter.y - shadowCenter.y) > halfExtent + instHalf.y) continue; + auto modelIt = models.find(instance.modelId); + if (modelIt == models.end()) continue; + const M2ModelGPU& model = modelIt->second; + if (!model.isValid() || model.isSmoke || model.isInvisibleTrap) continue; - auto it = models.find(instance.modelId); - if (it == models.end()) continue; + // Bind vertex/index buffers when model changes + if (instance.modelId != currentModelId) { + currentModelId = instance.modelId; + currentModel = &model; + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, ¤tModel->vertexBuffer, &offset); + vkCmdBindIndexBuffer(cmd, currentModel->indexBuffer, 0, VK_INDEX_TYPE_UINT16); + } - const M2ModelGPU& model = it->second; - if (!model.isValid() || model.isSmoke) continue; - - glUniformMatrix4fv(modelLoc, 1, GL_FALSE, &instance.modelMatrix[0][0]); - glBindVertexArray(model.vao); + ShadowPush push{lightSpaceMatrix, instance.modelMatrix}; + vkCmdPushConstants(cmd, shadowPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, + 0, 128, &push); + // Draw only opaque batches for (const auto& batch : model.batches) { - if (batch.indexCount == 0) continue; - bool useTexture = (batch.texture != 0); - bool alphaCutout = batch.hasAlpha; - bool foliageSway = model.shadowWindFoliage && alphaCutout; - - if (useTexLoc >= 0) glUniform1i(useTexLoc, useTexture ? 1 : 0); - if (alphaTestLoc >= 0) glUniform1i(alphaTestLoc, alphaCutout ? 1 : 0); - if (foliageSwayLoc >= 0) glUniform1i(foliageSwayLoc, foliageSway ? 1 : 0); - if (useTexture) { - glBindTexture(GL_TEXTURE_2D, batch.texture); - } - glDrawElements(GL_TRIANGLES, batch.indexCount, GL_UNSIGNED_SHORT, - (void*)(batch.indexStart * sizeof(uint16_t))); + if (batch.blendMode >= 2) continue; // skip transparent + if (batch.submeshLevel > 0) continue; // skip LOD submeshes + vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0); } } - - glBindVertexArray(0); } // --- M2 Particle Emitter Helpers --- @@ -2708,12 +2760,12 @@ void M2Renderer::updateParticles(M2Instance& inst, float dt) { } } -void M2Renderer::renderM2Particles(const glm::mat4& view, const glm::mat4& proj) { - if (m2ParticleShader_ == 0 || m2ParticleVAO_ == 0) return; +void M2Renderer::renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) { + if (!particlePipeline_ || !m2ParticleVB_) return; // Collect all particles from all instances, grouped by texture+blend struct ParticleGroupKey { - GLuint texture; + VkTexture* texture; uint8_t blendType; uint16_t tilesX; uint16_t tilesY; @@ -2727,14 +2779,14 @@ void M2Renderer::renderM2Particles(const glm::mat4& view, const glm::mat4& proj) }; struct ParticleGroupKeyHash { size_t operator()(const ParticleGroupKey& key) const { - size_t h1 = std::hash{}(key.texture); + size_t h1 = std::hash{}(reinterpret_cast(key.texture)); size_t h2 = std::hash{}((static_cast(key.tilesX) << 16) | key.tilesY); size_t h3 = std::hash{}(key.blendType); return h1 ^ (h2 * 0x9e3779b9u) ^ (h3 * 0x85ebca6bu); } }; struct ParticleGroup { - GLuint texture; + VkTexture* texture; uint8_t blendType; uint16_t tilesX; uint16_t tilesY; @@ -2760,23 +2812,13 @@ void M2Renderer::renderM2Particles(const glm::mat4& view, const glm::mat4& proj) float rawScale = interpFBlockFloat(em.particleScale, lifeRatio); if (!gpu.isSpellEffect) { - // FBlock colors are tint values meant to multiply a bright texture. - // Desaturate toward white so particles look like water spray, not neon. color = glm::mix(color, glm::vec3(1.0f), 0.7f); - - // Large-scale particles (>2.0) are volume/backdrop effects meant to be - // nearly invisible mist. Fade them heavily since we render as point sprites. - if (rawScale > 2.0f) { - alpha *= 0.02f; - } - // Reduce additive particle intensity to prevent blinding overlap - if (em.blendingType == 3 || em.blendingType == 4) { - alpha *= 0.05f; - } + if (rawScale > 2.0f) alpha *= 0.02f; + if (em.blendingType == 3 || em.blendingType == 4) alpha *= 0.05f; } float scale = gpu.isSpellEffect ? rawScale : std::min(rawScale, 1.5f); - GLuint tex = whiteTexture; + VkTexture* tex = whiteTexture_.get(); if (p.emitterIndex < static_cast(gpu.particleTextures.size())) { tex = gpu.particleTextures[p.emitterIndex]; } @@ -2813,114 +2855,95 @@ void M2Renderer::renderM2Particles(const glm::mat4& view, const glm::mat4& proj) if (totalParticles == 0) return; - // Set up GL state - glEnable(GL_BLEND); - glEnable(GL_DEPTH_TEST); - glDepthMask(GL_FALSE); - glEnable(GL_PROGRAM_POINT_SIZE); - glDisable(GL_CULL_FACE); + // Bind per-frame set (set 0) for particle pipeline + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + particlePipelineLayout_, 0, 1, &perFrameSet, 0, nullptr); - glUseProgram(m2ParticleShader_); + VkDeviceSize vbOffset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &m2ParticleVB_, &vbOffset); - GLint viewLoc = glGetUniformLocation(m2ParticleShader_, "uView"); - GLint projLoc = glGetUniformLocation(m2ParticleShader_, "uProjection"); - GLint texLoc = glGetUniformLocation(m2ParticleShader_, "uTexture"); - GLint tileLoc = glGetUniformLocation(m2ParticleShader_, "uTileCount"); - GLint alphaKeyLoc = glGetUniformLocation(m2ParticleShader_, "uAlphaKey"); - glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view)); - glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(proj)); - glUniform1i(texLoc, 0); - glActiveTexture(GL_TEXTURE0); - - glBindVertexArray(m2ParticleVAO_); + VkPipeline currentPipeline = VK_NULL_HANDLE; for (auto& [key, group] : groups) { if (group.vertexData.empty()) continue; - // Use blend mode as specified by the emitter — don't override based on texture alpha. - // BlendType: 0=opaque, 1=alphaKey, 2=alpha, 3=add, 4=mod uint8_t blendType = group.blendType; - glUniform1i(alphaKeyLoc, (blendType == 1) ? 1 : 0); - if (blendType == 3 || blendType == 4) { - glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Additive - } else { - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Alpha + VkPipeline desiredPipeline = (blendType == 3 || blendType == 4) + ? particleAdditivePipeline_ : particlePipeline_; + if (desiredPipeline != currentPipeline) { + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, desiredPipeline); + currentPipeline = desiredPipeline; } - glBindTexture(GL_TEXTURE_2D, group.texture); - glUniform2f(tileLoc, static_cast(group.tilesX), static_cast(group.tilesY)); + // Allocate descriptor set for this group's texture + VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + ai.descriptorPool = materialDescPool_; + ai.descriptorSetCount = 1; + ai.pSetLayouts = &particleTexLayout_; + VkDescriptorSet texSet = VK_NULL_HANDLE; + if (vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &texSet) == VK_SUCCESS) { + VkTexture* tex = group.texture ? group.texture : whiteTexture_.get(); + VkDescriptorImageInfo imgInfo = tex->descriptorInfo(); + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = texSet; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imgInfo; + vkUpdateDescriptorSets(vkCtx_->getDevice(), 1, &write, 0, nullptr); - // Upload and draw in chunks of MAX_M2_PARTICLES + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + particlePipelineLayout_, 1, 1, &texSet, 0, nullptr); + } + + // Push constants: tileCount + alphaKey + struct { float tileX, tileY; int alphaKey; } pc = { + static_cast(group.tilesX), static_cast(group.tilesY), + (blendType == 1) ? 1 : 0 + }; + vkCmdPushConstants(cmd, particlePipelineLayout_, VK_SHADER_STAGE_FRAGMENT_BIT, 0, + sizeof(pc), &pc); + + // Upload and draw in chunks size_t count = group.vertexData.size() / 9; size_t offset = 0; while (offset < count) { size_t batch = std::min(count - offset, MAX_M2_PARTICLES); - glBindBuffer(GL_ARRAY_BUFFER, m2ParticleVBO_); - glBufferSubData(GL_ARRAY_BUFFER, 0, batch * 9 * sizeof(float), - &group.vertexData[offset * 9]); - glDrawArrays(GL_POINTS, 0, static_cast(batch)); + memcpy(m2ParticleVBMapped_, &group.vertexData[offset * 9], batch * 9 * sizeof(float)); + vkCmdDraw(cmd, static_cast(batch), 1, 0, 0); offset += batch; } } - - glBindVertexArray(0); - - // Restore state - glDepthMask(GL_TRUE); - glDisable(GL_PROGRAM_POINT_SIZE); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glDisable(GL_BLEND); - glEnable(GL_CULL_FACE); } -void M2Renderer::renderSmokeParticles(const Camera& /*camera*/, const glm::mat4& view, const glm::mat4& projection) { - if (smokeParticles.empty() || !smokeShader || smokeVAO == 0) return; +void M2Renderer::renderSmokeParticles(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) { + if (smokeParticles.empty() || !smokePipeline_ || !smokeVB_) return; // Build vertex data: pos(3) + lifeRatio(1) + size(1) + isSpark(1) per particle - std::vector data; - data.reserve(smokeParticles.size() * 6); - for (const auto& p : smokeParticles) { - data.push_back(p.position.x); - data.push_back(p.position.y); - data.push_back(p.position.z); - data.push_back(p.life / p.maxLife); - data.push_back(p.size); - data.push_back(p.isSpark); + size_t count = std::min(smokeParticles.size(), static_cast(MAX_SMOKE_PARTICLES)); + float* dst = static_cast(smokeVBMapped_); + for (size_t i = 0; i < count; i++) { + const auto& p = smokeParticles[i]; + *dst++ = p.position.x; + *dst++ = p.position.y; + *dst++ = p.position.z; + *dst++ = p.life / p.maxLife; + *dst++ = p.size; + *dst++ = p.isSpark; } - // Upload to VBO - glBindBuffer(GL_ARRAY_BUFFER, smokeVBO); - glBufferSubData(GL_ARRAY_BUFFER, 0, data.size() * sizeof(float), data.data()); - glBindBuffer(GL_ARRAY_BUFFER, 0); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, smokePipeline_); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + smokePipelineLayout_, 0, 1, &perFrameSet, 0, nullptr); - // Set GL state - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glEnable(GL_DEPTH_TEST); // Occlude behind buildings - glDepthMask(GL_FALSE); - glEnable(GL_PROGRAM_POINT_SIZE); - glDisable(GL_CULL_FACE); + // Push constant: screenHeight + float screenHeight = static_cast(vkCtx_->getSwapchainExtent().height); + vkCmdPushConstants(cmd, smokePipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, 0, + sizeof(float), &screenHeight); - smokeShader->use(); - smokeShader->setUniform("uView", view); - smokeShader->setUniform("uProjection", projection); - - // Get viewport height for point size scaling - GLint viewport[4]; - glGetIntegerv(GL_VIEWPORT, viewport); - smokeShader->setUniform("uScreenHeight", static_cast(viewport[3])); - - glBindVertexArray(smokeVAO); - glDrawArrays(GL_POINTS, 0, static_cast(smokeParticles.size())); - glBindVertexArray(0); - - // Restore state - glEnable(GL_DEPTH_TEST); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glDepthMask(GL_TRUE); - glDisable(GL_PROGRAM_POINT_SIZE); - glDisable(GL_BLEND); - glEnable(GL_CULL_FACE); + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &smokeVB_, &offset); + vkCmdDraw(cmd, static_cast(count), 1, 0, 0); } void M2Renderer::setInstancePosition(uint32_t instanceId, const glm::vec3& position) { @@ -2989,10 +3012,14 @@ void M2Renderer::removeInstances(const std::vector& instanceIds) { } void M2Renderer::clear() { - for (auto& [id, model] : models) { - if (model.vao != 0) glDeleteVertexArrays(1, &model.vao); - if (model.vbo != 0) glDeleteBuffers(1, &model.vbo); - if (model.ebo != 0) glDeleteBuffers(1, &model.ebo); + if (vkCtx_) { + vkDeviceWaitIdle(vkCtx_->getDevice()); + for (auto& [id, model] : models) { + destroyModelGPU(model); + } + for (auto& inst : instances) { + destroyInstanceBones(inst); + } } models.clear(); instances.clear(); @@ -3100,9 +3127,7 @@ void M2Renderer::cleanupUnusedModels() { for (uint32_t id : toRemove) { auto it = models.find(id); if (it != models.end()) { - if (it->second.vao != 0) glDeleteVertexArrays(1, &it->second.vao); - if (it->second.vbo != 0) glDeleteBuffers(1, &it->second.vbo); - if (it->second.ebo != 0) glDeleteBuffers(1, &it->second.ebo); + destroyModelGPU(it->second); models.erase(it); } } @@ -3112,7 +3137,7 @@ void M2Renderer::cleanupUnusedModels() { } } -GLuint M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) { +VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) { auto normalizeKey = [](std::string key) { std::replace(key.begin(), key.end(), '/', '\\'); std::transform(key.begin(), key.end(), key.begin(), @@ -3125,7 +3150,7 @@ GLuint M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) { auto it = textureCache.find(key); if (it != textureCache.end()) { it->second.lastUse = ++textureCacheCounter_; - return it->second.id; + return it->second.texture.get(); } auto containsToken = [](const std::string& haystack, const char* token) { @@ -3151,9 +3176,7 @@ GLuint M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) { if (loggedTextureLoadFails.insert(key).second) { LOG_WARNING("M2: Failed to load texture: ", path); } - // Don't cache failures — transient StormLib thread contention can - // cause reads to fail; next loadModel call will retry. - return whiteTexture; + return whiteTexture_.get(); } // Track whether the texture actually uses alpha (any pixel with alpha < 255). @@ -3165,38 +3188,31 @@ GLuint M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) { } } - GLuint textureID; - glGenTextures(1, &textureID); - glBindTexture(GL_TEXTURE_2D, textureID); + // Create Vulkan texture + auto tex = std::make_unique(); + tex->upload(*vkCtx_, blp.data.data(), blp.width, blp.height, VK_FORMAT_R8G8B8A8_UNORM); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, - blp.width, blp.height, 0, - GL_RGBA, GL_UNSIGNED_BYTE, blp.data.data()); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // M2Texture flags: bit 0 = WrapS (1=repeat, 0=clamp), bit 1 = WrapT - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, (texFlags & 0x1) ? GL_REPEAT : GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, (texFlags & 0x2) ? GL_REPEAT : GL_CLAMP_TO_EDGE); - glGenerateMipmap(GL_TEXTURE_2D); - applyAnisotropicFiltering(); + VkSamplerAddressMode wrapS = (texFlags & 0x1) ? VK_SAMPLER_ADDRESS_MODE_REPEAT : VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + VkSamplerAddressMode wrapT = (texFlags & 0x2) ? VK_SAMPLER_ADDRESS_MODE_REPEAT : VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + tex->createSampler(vkCtx_->getDevice(), VK_FILTER_LINEAR, wrapS, wrapT); - glBindTexture(GL_TEXTURE_2D, 0); + VkTexture* texPtr = tex.get(); TextureCacheEntry e; - e.id = textureID; + e.texture = std::move(tex); size_t base = static_cast(blp.width) * static_cast(blp.height) * 4ull; e.approxBytes = base + (base / 3); e.hasAlpha = hasAlpha; e.colorKeyBlack = colorKeyBlackHint; e.lastUse = ++textureCacheCounter_; textureCacheBytes_ += e.approxBytes; - textureCache[key] = e; - textureHasAlphaById_[textureID] = hasAlpha; - textureColorKeyBlackById_[textureID] = colorKeyBlackHint; + textureCache[key] = std::move(e); + textureHasAlphaByPtr_[texPtr] = hasAlpha; + textureColorKeyBlackByPtr_[texPtr] = colorKeyBlackHint; LOG_DEBUG("M2: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")"); - return textureID; + return texPtr; } uint32_t M2Renderer::getTotalTriangleCount() const { diff --git a/src/rendering/minimap.cpp b/src/rendering/minimap.cpp index 96572782..0042d803 100644 --- a/src/rendering/minimap.cpp +++ b/src/rendering/minimap.cpp @@ -1,11 +1,15 @@ #include "rendering/minimap.hpp" -#include "rendering/shader.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_texture.hpp" +#include "rendering/vk_render_target.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_utils.hpp" #include "rendering/camera.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/blp_loader.hpp" #include "core/coordinates.hpp" #include "core/logger.hpp" -#include #include #include #include @@ -13,37 +17,47 @@ namespace wowee { namespace rendering { +// Push constant for tile composite vertex shader +struct MinimapTilePush { + glm::vec2 gridOffset; // 8 bytes +}; + +// Push constant for display vertex + fragment shaders +struct MinimapDisplayPush { + glm::vec4 rect; // x, y, w, h in 0..1 screen space + glm::vec2 playerUV; + float rotation; + float arrowRotation; + float zoomRadius; + int32_t squareShape; +}; // 40 bytes + Minimap::Minimap() = default; Minimap::~Minimap() { shutdown(); } -bool Minimap::initialize(int size) { +bool Minimap::initialize(VkContext* ctx, VkDescriptorSetLayout /*perFrameLayout*/, int size) { + vkCtx = ctx; mapSize = size; + VkDevice device = vkCtx->getDevice(); - // --- Composite FBO (3x3 tiles = 768x768) --- - glGenFramebuffers(1, &compositeFBO); - glBindFramebuffer(GL_FRAMEBUFFER, compositeFBO); - - glGenTextures(1, &compositeTexture); - glBindTexture(GL_TEXTURE_2D, compositeTexture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, COMPOSITE_PX, COMPOSITE_PX, 0, - GL_RGBA, GL_UNSIGNED_BYTE, nullptr); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, compositeTexture, 0); - - if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - LOG_ERROR("Minimap composite FBO incomplete"); - glBindFramebuffer(GL_FRAMEBUFFER, 0); + // --- Composite render target (768x768) --- + compositeTarget = std::make_unique(); + if (!compositeTarget->create(*vkCtx, COMPOSITE_PX, COMPOSITE_PX)) { + LOG_ERROR("Minimap: failed to create composite render target"); return false; } - glBindFramebuffer(GL_FRAMEBUFFER, 0); - // --- Unit quad for tile compositing --- + // --- No-data fallback texture (dark blue-gray, 1x1) --- + noDataTexture = std::make_unique(); + uint8_t darkPixel[4] = { 12, 20, 30, 255 }; + noDataTexture->upload(*vkCtx, darkPixel, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false); + noDataTexture->createSampler(device, VK_FILTER_NEAREST, VK_FILTER_NEAREST, + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, 1.0f); + + // --- Shared quad vertex buffer (unit quad: pos2 + uv2) --- float quadVerts[] = { // pos (x,y), uv (u,v) 0.0f, 0.0f, 0.0f, 0.0f, @@ -53,178 +67,139 @@ bool Minimap::initialize(int size) { 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, }; + auto quadBuf = uploadBuffer(*vkCtx, quadVerts, sizeof(quadVerts), + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + quadVB = quadBuf.buffer; + quadVBAlloc = quadBuf.allocation; - glGenVertexArrays(1, &tileQuadVAO); - glGenBuffers(1, &tileQuadVBO); - glBindVertexArray(tileQuadVAO); - glBindBuffer(GL_ARRAY_BUFFER, tileQuadVBO); - glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW); - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); - glBindVertexArray(0); + // --- Descriptor set layout: 1 combined image sampler at binding 0 (fragment) --- + VkDescriptorSetLayoutBinding samplerBinding{}; + samplerBinding.binding = 0; + samplerBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + samplerBinding.descriptorCount = 1; + samplerBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + samplerSetLayout = createDescriptorSetLayout(device, { samplerBinding }); - // --- Tile compositing shader --- - const char* tileVertSrc = R"( - #version 330 core - layout (location = 0) in vec2 aPos; - layout (location = 1) in vec2 aUV; + // --- Descriptor pool --- + VkDescriptorPoolSize poolSize{}; + poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + poolSize.descriptorCount = MAX_DESC_SETS; - uniform vec2 uGridOffset; // (col, row) in 0-2 + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.maxSets = MAX_DESC_SETS; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + vkCreateDescriptorPool(device, &poolInfo, nullptr, &descPool); - out vec2 TexCoord; + // --- Allocate all descriptor sets --- + // 18 tile sets (2 frames × 9 tiles) + 1 display set = 19 total + std::vector layouts(19, samplerSetLayout); + VkDescriptorSetAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = descPool; + allocInfo.descriptorSetCount = 19; + allocInfo.pSetLayouts = layouts.data(); - void main() { - vec2 gridPos = (uGridOffset + aPos) / 3.0; - gl_Position = vec4(gridPos * 2.0 - 1.0, 0.0, 1.0); - TexCoord = aUV; + VkDescriptorSet allSets[19]; + vkAllocateDescriptorSets(device, &allocInfo, allSets); + + for (int f = 0; f < 2; f++) + for (int t = 0; t < 9; t++) + tileDescSets[f][t] = allSets[f * 9 + t]; + displayDescSet = allSets[18]; + + // --- Write display descriptor set → composite render target --- + VkDescriptorImageInfo compositeImgInfo = compositeTarget->descriptorInfo(); + VkWriteDescriptorSet displayWrite{}; + displayWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + displayWrite.dstSet = displayDescSet; + displayWrite.dstBinding = 0; + displayWrite.descriptorCount = 1; + displayWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + displayWrite.pImageInfo = &compositeImgInfo; + vkUpdateDescriptorSets(device, 1, &displayWrite, 0, nullptr); + + // --- Tile pipeline layout: samplerSetLayout + 8-byte push constant (vertex) --- + VkPushConstantRange tilePush{}; + tilePush.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + tilePush.offset = 0; + tilePush.size = sizeof(MinimapTilePush); + tilePipelineLayout = createPipelineLayout(device, { samplerSetLayout }, { tilePush }); + + // --- Display pipeline layout: samplerSetLayout + 40-byte push constant (vert+frag) --- + VkPushConstantRange displayPush{}; + displayPush.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + displayPush.offset = 0; + displayPush.size = sizeof(MinimapDisplayPush); + displayPipelineLayout = createPipelineLayout(device, { samplerSetLayout }, { displayPush }); + + // --- Vertex input: pos2 (loc 0) + uv2 (loc 1), stride 16 --- + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 4 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector attrs(2); + attrs[0] = { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 }; // aPos + attrs[1] = { 1, 0, VK_FORMAT_R32G32_SFLOAT, 2 * sizeof(float) }; // aUV + + // --- Load tile shaders --- + { + VkShaderModule vs, fs; + if (!vs.loadFromFile(device, "assets/shaders/minimap_tile.vert.spv") || + !fs.loadFromFile(device, "assets/shaders/minimap_tile.frag.spv")) { + LOG_ERROR("Minimap: failed to load tile shaders"); + return false; } - )"; - const char* tileFragSrc = R"( - #version 330 core - in vec2 TexCoord; + tilePipeline = PipelineBuilder() + .setShaders(vs.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fs.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({ binding }, attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setNoDepthTest() + .setColorBlendAttachment(PipelineBuilder::blendDisabled()) + .setLayout(tilePipelineLayout) + .setRenderPass(compositeTarget->getRenderPass()) + .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) + .build(device); - uniform sampler2D uTileTexture; - - out vec4 FragColor; - - void main() { - // BLP minimap tiles have same axis transposition as ADT terrain: - // tile U (cols) = north-south, tile V (rows) = west-east - // Composite grid: TexCoord.x = west-east, TexCoord.y = north-south - // So swap to match - FragColor = texture(uTileTexture, vec2(TexCoord.y, TexCoord.x)); - } - )"; - - tileShader = std::make_unique(); - if (!tileShader->loadFromSource(tileVertSrc, tileFragSrc)) { - LOG_ERROR("Failed to create minimap tile compositing shader"); - return false; + vs.destroy(); + fs.destroy(); } - // --- Screen quad --- - glGenVertexArrays(1, &quadVAO); - glGenBuffers(1, &quadVBO); - glBindVertexArray(quadVAO); - glBindBuffer(GL_ARRAY_BUFFER, quadVBO); - glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW); - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); - glBindVertexArray(0); - - // --- Screen quad shader with rotation + circular mask --- - const char* quadVertSrc = R"( - #version 330 core - layout (location = 0) in vec2 aPos; - layout (location = 1) in vec2 aUV; - - uniform vec4 uRect; // x, y, w, h in 0..1 screen space - - out vec2 TexCoord; - - void main() { - vec2 pos = uRect.xy + aUV * uRect.zw; - gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0); - TexCoord = aUV; - } - )"; - - const char* quadFragSrc = R"( - #version 330 core - in vec2 TexCoord; - - uniform sampler2D uComposite; - uniform vec2 uPlayerUV; - uniform float uRotation; - uniform float uArrowRotation; - uniform float uZoomRadius; - uniform bool uSquareShape; - - out vec4 FragColor; - - bool pointInTriangle(vec2 p, vec2 a, vec2 b, vec2 c) { - vec2 v0 = c - a, v1 = b - a, v2 = p - a; - float d00 = dot(v0, v0); - float d01 = dot(v0, v1); - float d02 = dot(v0, v2); - float d11 = dot(v1, v1); - float d12 = dot(v1, v2); - float inv = 1.0 / (d00 * d11 - d01 * d01); - float u = (d11 * d02 - d01 * d12) * inv; - float v = (d00 * d12 - d01 * d02) * inv; - return (u >= 0.0) && (v >= 0.0) && (u + v <= 1.0); + // --- Load display shaders --- + { + VkShaderModule vs, fs; + if (!vs.loadFromFile(device, "assets/shaders/minimap_display.vert.spv") || + !fs.loadFromFile(device, "assets/shaders/minimap_display.frag.spv")) { + LOG_ERROR("Minimap: failed to load display shaders"); + return false; } - vec2 rot2(vec2 v, float ang) { - float c = cos(ang); - float s = sin(ang); - return vec2(v.x * c - v.y * s, v.x * s + v.y * c); - } + displayPipeline = PipelineBuilder() + .setShaders(vs.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fs.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({ binding }, attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setNoDepthTest() + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setLayout(displayPipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) + .build(device); - void main() { - vec2 centered = TexCoord - 0.5; - float dist = length(centered); - float maxDist = uSquareShape ? max(abs(centered.x), abs(centered.y)) : dist; - if (maxDist > 0.5) discard; - - // Rotate screen coords → composite UV offset - // Composite: U increases east, V increases north - // Screen: +X=right, +Y=up - float c = cos(uRotation); - float s = sin(uRotation); - float scale = uZoomRadius * 2.0; - - vec2 offset = vec2( - centered.x * c + centered.y * s, - -centered.x * s + centered.y * c - ) * scale; - - vec2 uv = uPlayerUV + offset; - vec3 color = texture(uComposite, uv).rgb; - - // Thin dark border at edge - if (maxDist > 0.49) { - color = mix(color, vec3(0.08), smoothstep(0.49, 0.5, maxDist)); - } - - // Player arrow at center (always points up = forward) - vec2 ap = rot2(centered, -(uArrowRotation + 3.14159265)); - vec2 tip = vec2(0.0, 0.035); - vec2 lt = vec2(-0.018, -0.016); - vec2 rt = vec2(0.018, -0.016); - vec2 nL = vec2(-0.006, -0.006); - vec2 nR = vec2(0.006, -0.006); - vec2 nB = vec2(0.0, 0.006); - - bool inArrow = pointInTriangle(ap, tip, lt, rt) - && !pointInTriangle(ap, nL, nR, nB); - - if (inArrow) { - color = vec3(0.0, 0.0, 0.0); - } - - FragColor = vec4(color, 0.8); - } - )"; - - quadShader = std::make_unique(); - if (!quadShader->loadFromSource(quadVertSrc, quadFragSrc)) { - LOG_ERROR("Failed to create minimap screen quad shader"); - return false; + vs.destroy(); + fs.destroy(); } - // --- No-data fallback texture (dark blue-gray) --- - glGenTextures(1, &noDataTexture); - glBindTexture(GL_TEXTURE_2D, noDataTexture); - uint8_t darkPixel[4] = { 12, 20, 30, 255 }; - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, darkPixel); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + if (!tilePipeline || !displayPipeline) { + LOG_ERROR("Minimap: failed to create pipelines"); + return false; + } LOG_INFO("Minimap initialized (", mapSize, "x", mapSize, " screen, ", COMPOSITE_PX, "x", COMPOSITE_PX, " composite)"); @@ -232,22 +207,30 @@ bool Minimap::initialize(int size) { } void Minimap::shutdown() { - if (compositeFBO) { glDeleteFramebuffers(1, &compositeFBO); compositeFBO = 0; } - if (compositeTexture) { glDeleteTextures(1, &compositeTexture); compositeTexture = 0; } - if (tileQuadVAO) { glDeleteVertexArrays(1, &tileQuadVAO); tileQuadVAO = 0; } - if (tileQuadVBO) { glDeleteBuffers(1, &tileQuadVBO); tileQuadVBO = 0; } - if (quadVAO) { glDeleteVertexArrays(1, &quadVAO); quadVAO = 0; } - if (quadVBO) { glDeleteBuffers(1, &quadVBO); quadVBO = 0; } - if (noDataTexture) { glDeleteTextures(1, &noDataTexture); noDataTexture = 0; } + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + VmaAllocator alloc = vkCtx->getAllocator(); + + vkDeviceWaitIdle(device); + + if (tilePipeline) { vkDestroyPipeline(device, tilePipeline, nullptr); tilePipeline = VK_NULL_HANDLE; } + if (displayPipeline) { vkDestroyPipeline(device, displayPipeline, nullptr); displayPipeline = VK_NULL_HANDLE; } + if (tilePipelineLayout) { vkDestroyPipelineLayout(device, tilePipelineLayout, nullptr); tilePipelineLayout = VK_NULL_HANDLE; } + if (displayPipelineLayout) { vkDestroyPipelineLayout(device, displayPipelineLayout, nullptr); displayPipelineLayout = VK_NULL_HANDLE; } + if (descPool) { vkDestroyDescriptorPool(device, descPool, nullptr); descPool = VK_NULL_HANDLE; } + if (samplerSetLayout) { vkDestroyDescriptorSetLayout(device, samplerSetLayout, nullptr); samplerSetLayout = VK_NULL_HANDLE; } + + if (quadVB) { vmaDestroyBuffer(alloc, quadVB, quadVBAlloc); quadVB = VK_NULL_HANDLE; } - // Delete cached tile textures for (auto& [hash, tex] : tileTextureCache) { - if (tex) glDeleteTextures(1, &tex); + if (tex) tex->destroy(device, alloc); } tileTextureCache.clear(); - tileShader.reset(); - quadShader.reset(); + if (noDataTexture) { noDataTexture->destroy(device, alloc); noDataTexture.reset(); } + if (compositeTarget) { compositeTarget->destroy(device, alloc); compositeTarget.reset(); } + + vkCtx = nullptr; } void Minimap::setMapName(const std::string& name) { @@ -279,27 +262,19 @@ void Minimap::parseTRS() { int count = 0; while (std::getline(stream, line)) { - // Remove \r if (!line.empty() && line.back() == '\r') line.pop_back(); - - // Skip "dir:" lines and empty lines if (line.empty() || line.substr(0, 4) == "dir:") continue; - // Format: "Azeroth\map32_49.blp\t.blp" auto tabPos = line.find('\t'); if (tabPos == std::string::npos) continue; std::string key = line.substr(0, tabPos); std::string hashFile = line.substr(tabPos + 1); - // Strip .blp from key: "Azeroth\map32_49" - if (key.size() > 4 && key.substr(key.size() - 4) == ".blp") { + if (key.size() > 4 && key.substr(key.size() - 4) == ".blp") key = key.substr(0, key.size() - 4); - } - // Strip .blp from hash to get just the md5: "e7f0dea73ee6baca78231aaf4b7e772a" - if (hashFile.size() > 4 && hashFile.substr(hashFile.size() - 4) == ".blp") { + if (hashFile.size() > 4 && hashFile.substr(hashFile.size() - 4) == ".blp") hashFile = hashFile.substr(0, hashFile.size() - 4); - } trsLookup[key] = hashFile; count++; @@ -312,118 +287,80 @@ void Minimap::parseTRS() { // Tile texture loading // -------------------------------------------------------- -GLuint Minimap::getOrLoadTileTexture(int tileX, int tileY) { - // Build TRS key: "Azeroth\map32_49" +VkTexture* Minimap::getOrLoadTileTexture(int tileX, int tileY) { + if (!trsParsed) parseTRS(); + std::string key = mapName + "\\map" + std::to_string(tileX) + "_" + std::to_string(tileY); auto trsIt = trsLookup.find(key); - if (trsIt == trsLookup.end()) { - return noDataTexture; - } + if (trsIt == trsLookup.end()) + return noDataTexture.get(); const std::string& hash = trsIt->second; - // Check texture cache auto cacheIt = tileTextureCache.find(hash); - if (cacheIt != tileTextureCache.end()) { - return cacheIt->second; - } + if (cacheIt != tileTextureCache.end()) + return cacheIt->second.get(); // Load from MPQ std::string blpPath = "Textures\\Minimap\\" + hash + ".blp"; auto blpImage = assetManager->loadTexture(blpPath); if (!blpImage.isValid()) { - tileTextureCache[hash] = noDataTexture; - return noDataTexture; + tileTextureCache[hash] = nullptr; // Mark as failed + return noDataTexture.get(); } - // Create GL texture - GLuint tex; - glGenTextures(1, &tex); - glBindTexture(GL_TEXTURE_2D, tex); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, blpImage.width, blpImage.height, 0, - GL_RGBA, GL_UNSIGNED_BYTE, blpImage.data.data()); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + auto tex = std::make_unique(); + tex->upload(*vkCtx, blpImage.data.data(), blpImage.width, blpImage.height, + VK_FORMAT_R8G8B8A8_UNORM, false); + tex->createSampler(vkCtx->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR, + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, 1.0f); - tileTextureCache[hash] = tex; - return tex; + VkTexture* ptr = tex.get(); + tileTextureCache[hash] = std::move(tex); + return ptr; } // -------------------------------------------------------- -// Composite 3x3 tiles into FBO +// Update tile descriptor sets for composite pass // -------------------------------------------------------- -void Minimap::compositeTilesToFBO(const glm::vec3& centerWorldPos) { - // centerWorldPos is in render coords (renderX=wowY, renderY=wowX) - auto [tileX, tileY] = core::coords::worldToTile(centerWorldPos.x, centerWorldPos.y); +void Minimap::updateTileDescriptors(uint32_t frameIdx, int centerTileX, int centerTileY) { + VkDevice device = vkCtx->getDevice(); + int slot = 0; - // Save GL state - GLint prevFBO = 0; - glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFBO); - GLint prevViewport[4]; - glGetIntegerv(GL_VIEWPORT, prevViewport); - - glBindFramebuffer(GL_FRAMEBUFFER, compositeFBO); - glViewport(0, 0, COMPOSITE_PX, COMPOSITE_PX); - glClearColor(0.05f, 0.08f, 0.12f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - - glDisable(GL_DEPTH_TEST); - glDisable(GL_CULL_FACE); - glDisable(GL_BLEND); - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); - - tileShader->use(); - tileShader->setUniform("uTileTexture", 0); - - glBindVertexArray(tileQuadVAO); - - // Draw 3x3 tile grid into composite FBO. - // BLP first row → GL V=0 (bottom) = north edge of tile. - // So north tile (dr=-1) goes to row 0 (bottom), south (dr=+1) to row 2 (top). - // West tile (dc=-1) goes to col 0 (left), east (dc=+1) to col 2 (right). - // Result: composite U=0→west, U=1→east, V=0→north, V=1→south. for (int dr = -1; dr <= 1; dr++) { for (int dc = -1; dc <= 1; dc++) { - int tx = tileX + dr; - int ty = tileY + dc; + int tx = centerTileX + dr; + int ty = centerTileY + dc; - GLuint tileTex = getOrLoadTileTexture(tx, ty); + VkTexture* tileTex = getOrLoadTileTexture(tx, ty); + if (!tileTex || !tileTex->isValid()) + tileTex = noDataTexture.get(); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, tileTex); + VkDescriptorImageInfo imgInfo = tileTex->descriptorInfo(); - // Grid position: dr=-1 (north) → row 0, dr=0 → row 1, dr=+1 (south) → row 2 - float col = static_cast(dc + 1); // 0, 1, 2 - float row = static_cast(dr + 1); // 0, 1, 2 + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = tileDescSets[frameIdx][slot]; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imgInfo; - tileShader->setUniform("uGridOffset", glm::vec2(col, row)); - glDrawArrays(GL_TRIANGLES, 0, 6); + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + slot++; } } - - glBindVertexArray(0); - - // Restore GL state - glBindFramebuffer(GL_FRAMEBUFFER, prevFBO); - glViewport(prevViewport[0], prevViewport[1], prevViewport[2], prevViewport[3]); - - lastCenterTileX = tileX; - lastCenterTileY = tileY; } // -------------------------------------------------------- -// Main render +// Off-screen composite pass (call BEFORE main render pass) // -------------------------------------------------------- -void Minimap::render(const Camera& playerCamera, const glm::vec3& centerWorldPos, - int screenWidth, int screenHeight) { - if (!enabled || !assetManager || !compositeFBO) return; +void Minimap::compositePass(VkCommandBuffer cmd, const glm::vec3& centerWorldPos) { + if (!enabled || !assetManager || !compositeTarget || !compositeTarget->isValid()) return; - // Lazy-parse TRS on first use if (!trsParsed) parseTRS(); // Check if composite needs refresh @@ -438,30 +375,71 @@ void Minimap::render(const Camera& playerCamera, const glm::vec3& centerWorldPos // Also refresh if player crossed a tile boundary auto [curTileX, curTileY] = core::coords::worldToTile(centerWorldPos.x, centerWorldPos.y); - if (curTileX != lastCenterTileX || curTileY != lastCenterTileY) { + if (curTileX != lastCenterTileX || curTileY != lastCenterTileY) needsRefresh = true; + + if (!needsRefresh) return; + + uint32_t frameIdx = vkCtx->getCurrentFrame(); + + // Update tile descriptor sets + updateTileDescriptors(frameIdx, curTileX, curTileY); + + // Begin off-screen render pass + VkClearColorValue clearColor = {{ 0.05f, 0.08f, 0.12f, 1.0f }}; + compositeTarget->beginPass(cmd, clearColor); + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, tilePipeline); + + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &quadVB, &offset); + + // Draw 3x3 tile grid + int slot = 0; + for (int dr = -1; dr <= 1; dr++) { + for (int dc = -1; dc <= 1; dc++) { + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + tilePipelineLayout, 0, 1, + &tileDescSets[frameIdx][slot], 0, nullptr); + + MinimapTilePush push{}; + push.gridOffset = glm::vec2(static_cast(dc + 1), + static_cast(dr + 1)); + vkCmdPushConstants(cmd, tilePipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, + 0, sizeof(push), &push); + + vkCmdDraw(cmd, 6, 1, 0, 0); + slot++; + } } - if (needsRefresh) { - compositeTilesToFBO(centerWorldPos); - lastUpdateTime = now; - lastUpdatePos = centerWorldPos; - hasCachedFrame = true; - } + compositeTarget->endPass(cmd); - // Draw screen quad - renderQuad(playerCamera, centerWorldPos, screenWidth, screenHeight); + // Update tracking + lastCenterTileX = curTileX; + lastCenterTileY = curTileY; + lastUpdateTime = now; + lastUpdatePos = centerWorldPos; + hasCachedFrame = true; } -void Minimap::renderQuad(const Camera& playerCamera, const glm::vec3& centerWorldPos, - int screenWidth, int screenHeight) { - glDisable(GL_DEPTH_TEST); - glDisable(GL_CULL_FACE); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); +// -------------------------------------------------------- +// Display quad (call INSIDE main render pass) +// -------------------------------------------------------- - quadShader->use(); +void Minimap::render(VkCommandBuffer cmd, const Camera& playerCamera, + const glm::vec3& centerWorldPos, + int screenWidth, int screenHeight) { + if (!enabled || !hasCachedFrame || !displayPipeline) return; + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, displayPipeline); + + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + displayPipelineLayout, 0, 1, + &displayDescSet, 0, nullptr); + + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &quadVB, &offset); // Position minimap in top-right corner float margin = 10.0f; @@ -469,59 +447,44 @@ void Minimap::renderQuad(const Camera& playerCamera, const glm::vec3& centerWorl float pixelH = static_cast(mapSize) / screenHeight; float x = 1.0f - pixelW - margin / screenWidth; float y = 1.0f - pixelH - margin / screenHeight; - quadShader->setUniform("uRect", glm::vec4(x, y, pixelW, pixelH)); // Compute player's UV in the composite texture - // Render coords: renderX = wowY (west axis), renderY = wowX (north axis) constexpr float TILE_SIZE = core::coords::TILE_SIZE; auto [tileX, tileY] = core::coords::worldToTile(centerWorldPos.x, centerWorldPos.y); - // Fractional position within center tile - // tileX = floor(32 - wowX/TILE_SIZE), wowX = renderY - // fracNS: 0 = north edge of tile, 1 = south edge float fracNS = 32.0f - static_cast(tileX) - centerWorldPos.y / TILE_SIZE; - // fracEW: 0 = west edge of tile, 1 = east edge float fracEW = 32.0f - static_cast(tileY) - centerWorldPos.x / TILE_SIZE; - // Composite UV: center tile is grid slot (1,1) → UV range [1/3, 2/3] - // Composite orientation: U=0→west, U=1→east, V=0→north, V=1→south float playerU = (1.0f + fracEW) / 3.0f; float playerV = (1.0f + fracNS) / 3.0f; - quadShader->setUniform("uPlayerUV", glm::vec2(playerU, playerV)); - - // Zoom: convert view radius from world units to composite UV fraction float zoomRadius = viewRadius / (TILE_SIZE * 3.0f); - quadShader->setUniform("uZoomRadius", zoomRadius); - // Rotation: compass bearing from north, clockwise - // renderX = wowY (west), renderY = wowX (north) - // Facing north: fwd=(0,1,0) → bearing=0 - // Facing east: fwd=(-1,0,0) → bearing=π/2 float rotation = 0.0f; if (rotateWithCamera) { glm::vec3 fwd = playerCamera.getForward(); rotation = std::atan2(-fwd.x, fwd.y); } - quadShader->setUniform("uRotation", rotation); + float arrowRotation = 0.0f; if (!rotateWithCamera) { glm::vec3 fwd = playerCamera.getForward(); arrowRotation = std::atan2(-fwd.x, fwd.y); } - quadShader->setUniform("uArrowRotation", arrowRotation); - quadShader->setUniform("uSquareShape", squareShape); - quadShader->setUniform("uComposite", 0); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, compositeTexture); + MinimapDisplayPush push{}; + push.rect = glm::vec4(x, y, pixelW, pixelH); + push.playerUV = glm::vec2(playerU, playerV); + push.rotation = rotation; + push.arrowRotation = arrowRotation; + push.zoomRadius = zoomRadius; + push.squareShape = squareShape ? 1 : 0; - glBindVertexArray(quadVAO); - glDrawArrays(GL_TRIANGLES, 0, 6); - glBindVertexArray(0); + vkCmdPushConstants(cmd, displayPipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof(push), &push); - glDisable(GL_BLEND); - glEnable(GL_DEPTH_TEST); + vkCmdDraw(cmd, 6, 1, 0, 0); } } // namespace rendering diff --git a/src/rendering/mount_dust.cpp b/src/rendering/mount_dust.cpp index d57aea59..98098325 100644 --- a/src/rendering/mount_dust.cpp +++ b/src/rendering/mount_dust.cpp @@ -1,10 +1,15 @@ #include "rendering/mount_dust.hpp" #include "rendering/camera.hpp" -#include "rendering/shader.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_frame_data.hpp" +#include "rendering/vk_utils.hpp" #include "core/logger.hpp" #include #include #include +#include namespace wowee { namespace rendering { @@ -23,69 +28,91 @@ static float randFloat(float lo, float hi) { MountDust::MountDust() = default; MountDust::~MountDust() { shutdown(); } -bool MountDust::initialize() { +bool MountDust::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { LOG_INFO("Initializing mount dust effects"); - // Dust particle shader (brownish/tan dust clouds) - shader = std::make_unique(); + vkCtx = ctx; + VkDevice device = vkCtx->getDevice(); - const char* dustVS = R"( - #version 330 core - layout (location = 0) in vec3 aPos; - layout (location = 1) in float aSize; - layout (location = 2) in float aAlpha; - - uniform mat4 uView; - uniform mat4 uProjection; - - out float vAlpha; - - void main() { - gl_Position = uProjection * uView * vec4(aPos, 1.0); - gl_PointSize = aSize; - vAlpha = aAlpha; - } - )"; - - const char* dustFS = R"( - #version 330 core - in float vAlpha; - out vec4 FragColor; - - void main() { - vec2 coord = gl_PointCoord - vec2(0.5); - float dist = length(coord); - if (dist > 0.5) discard; - // Soft dust cloud with brownish/tan color - float alpha = smoothstep(0.5, 0.0, dist) * vAlpha; - vec3 dustColor = vec3(0.7, 0.65, 0.55); // Tan/brown dust - FragColor = vec4(dustColor, alpha * 0.4); // Semi-transparent - } - )"; - - if (!shader->loadFromSource(dustVS, dustFS)) { - LOG_ERROR("Failed to create mount dust shader"); + // Load SPIR-V shaders + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/mount_dust.vert.spv")) { + LOG_ERROR("Failed to load mount_dust vertex shader"); + return false; + } + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/mount_dust.frag.spv")) { + LOG_ERROR("Failed to load mount_dust fragment shader"); return false; } - // Create VAO/VBO - glGenVertexArrays(1, &vao); - glGenBuffers(1, &vbo); + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); - glBindVertexArray(vao); - glBindBuffer(GL_ARRAY_BUFFER, vbo); + // No push constants needed for mount dust (all data is per-vertex) + pipelineLayout = createPipelineLayout(device, {perFrameLayout}, {}); + if (pipelineLayout == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create mount dust pipeline layout"); + return false; + } - // Position (vec3) + Size (float) + Alpha (float) = 5 floats per vertex - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); + // Vertex input: pos(vec3) + size(float) + alpha(float) = 5 floats, stride = 20 bytes + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 5 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; - glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(1); + std::vector attrs(3); + attrs[0].location = 0; + attrs[0].binding = 0; + attrs[0].format = VK_FORMAT_R32G32B32_SFLOAT; + attrs[0].offset = 0; + attrs[1].location = 1; + attrs[1].binding = 0; + attrs[1].format = VK_FORMAT_R32_SFLOAT; + attrs[1].offset = 3 * sizeof(float); + attrs[2].location = 2; + attrs[2].binding = 0; + attrs[2].format = VK_FORMAT_R32_SFLOAT; + attrs[2].offset = 4 * sizeof(float); - glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(4 * sizeof(float))); - glEnableVertexAttribArray(2); + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; - glBindVertexArray(0); + pipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setLayout(pipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (pipeline == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create mount dust pipeline"); + return false; + } + + // Create dynamic mapped vertex buffer + dynamicVBSize = MAX_DUST_PARTICLES * 5 * sizeof(float); + AllocatedBuffer buf = createBuffer(vkCtx->getAllocator(), dynamicVBSize, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU); + dynamicVB = buf.buffer; + dynamicVBAlloc = buf.allocation; + dynamicVBAllocInfo = buf.info; + + if (dynamicVB == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create mount dust dynamic vertex buffer"); + return false; + } particles.reserve(MAX_DUST_PARTICLES); vertexData.reserve(MAX_DUST_PARTICLES * 5); @@ -95,12 +122,27 @@ bool MountDust::initialize() { } void MountDust::shutdown() { - if (vao) glDeleteVertexArrays(1, &vao); - if (vbo) glDeleteBuffers(1, &vbo); - vao = 0; - vbo = 0; + if (vkCtx) { + VkDevice device = vkCtx->getDevice(); + VmaAllocator allocator = vkCtx->getAllocator(); + + if (pipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(device, pipeline, nullptr); + pipeline = VK_NULL_HANDLE; + } + if (pipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + pipelineLayout = VK_NULL_HANDLE; + } + if (dynamicVB != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, dynamicVB, dynamicVBAlloc); + dynamicVB = VK_NULL_HANDLE; + dynamicVBAlloc = VK_NULL_HANDLE; + } + } + + vkCtx = nullptr; particles.clear(); - shader.reset(); } void MountDust::spawnDust(const glm::vec3& position, const glm::vec3& velocity, bool isMoving) { @@ -173,8 +215,8 @@ void MountDust::update(float deltaTime) { } } -void MountDust::render(const Camera& camera) { - if (particles.empty() || !shader) return; +void MountDust::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) { + if (particles.empty() || pipeline == VK_NULL_HANDLE) return; // Build vertex data vertexData.clear(); @@ -186,26 +228,25 @@ void MountDust::render(const Camera& camera) { vertexData.push_back(p.alpha); } - // Upload to GPU - glBindBuffer(GL_ARRAY_BUFFER, vbo); - glBufferData(GL_ARRAY_BUFFER, vertexData.size() * sizeof(float), vertexData.data(), GL_DYNAMIC_DRAW); + // Upload to mapped buffer + VkDeviceSize uploadSize = vertexData.size() * sizeof(float); + if (uploadSize > 0 && dynamicVBAllocInfo.pMappedData) { + std::memcpy(dynamicVBAllocInfo.pMappedData, vertexData.data(), uploadSize); + } - // Render - shader->use(); - shader->setUniform("uView", camera.getViewMatrix()); - shader->setUniform("uProjection", camera.getProjectionMatrix()); + // Bind pipeline + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glDepthMask(GL_FALSE); // Don't write to depth buffer - glEnable(GL_PROGRAM_POINT_SIZE); + // Bind per-frame descriptor set (set 0 - camera UBO) + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, + 0, 1, &perFrameSet, 0, nullptr); - glBindVertexArray(vao); - glDrawArrays(GL_POINTS, 0, static_cast(particles.size())); - glBindVertexArray(0); + // Bind vertex buffer + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &dynamicVB, &offset); - glDepthMask(GL_TRUE); - glDisable(GL_PROGRAM_POINT_SIZE); + // Draw particles as points + vkCmdDraw(cmd, static_cast(particles.size()), 1, 0, 0); } } // namespace rendering diff --git a/src/rendering/quest_marker_renderer.cpp b/src/rendering/quest_marker_renderer.cpp index 2ba86f23..4a52c09d 100644 --- a/src/rendering/quest_marker_renderer.cpp +++ b/src/rendering/quest_marker_renderer.cpp @@ -1,16 +1,26 @@ #include "rendering/quest_marker_renderer.hpp" #include "rendering/camera.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_utils.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/blp_loader.hpp" #include "core/logger.hpp" -#include #include #include #include #include +#include namespace wowee { namespace rendering { +// Push constant layout matching quest_marker.vert.glsl / quest_marker.frag.glsl +struct QuestMarkerPushConstants { + glm::mat4 model; // 64 bytes, used by vertex shader + float alpha; // 4 bytes, used by fragment shader +}; + QuestMarkerRenderer::QuestMarkerRenderer() { } @@ -18,33 +28,201 @@ QuestMarkerRenderer::~QuestMarkerRenderer() { shutdown(); } -bool QuestMarkerRenderer::initialize(pipeline::AssetManager* assetManager) { - if (!assetManager) { - LOG_WARNING("QuestMarkerRenderer: No AssetManager provided"); +bool QuestMarkerRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, + pipeline::AssetManager* assetManager) +{ + if (!ctx || !assetManager) { + LOG_WARNING("QuestMarkerRenderer: Missing VkContext or AssetManager"); return false; } LOG_INFO("QuestMarkerRenderer: Initializing..."); - createShader(); - createQuad(); - loadTextures(assetManager); - LOG_INFO("QuestMarkerRenderer: Initialization complete"); + vkCtx_ = ctx; + VkDevice device = vkCtx_->getDevice(); + // --- Create material descriptor set layout (set 1: combined image sampler) --- + createDescriptorResources(); + + // --- Load shaders --- + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/quest_marker.vert.spv")) { + LOG_ERROR("Failed to load quest_marker vertex shader"); + return false; + } + + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/quest_marker.frag.spv")) { + LOG_ERROR("Failed to load quest_marker fragment shader"); + vertModule.destroy(); + return false; + } + + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + // --- Push constant range: mat4 model (64) + float alpha (4) = 68 bytes --- + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + pushRange.offset = 0; + pushRange.size = sizeof(QuestMarkerPushConstants); + + // --- Pipeline layout: set 0 = per-frame, set 1 = material texture --- + pipelineLayout_ = createPipelineLayout(device, + {perFrameLayout, materialSetLayout_}, {pushRange}); + if (pipelineLayout_ == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create quest marker pipeline layout"); + vertModule.destroy(); + fragModule.destroy(); + return false; + } + + // --- Vertex input: vec3 pos (offset 0) + vec2 uv (offset 12), stride 20 --- + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 5 * sizeof(float); // 20 bytes + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + VkVertexInputAttributeDescription posAttr{}; + posAttr.location = 0; + posAttr.binding = 0; + posAttr.format = VK_FORMAT_R32G32B32_SFLOAT; + posAttr.offset = 0; + + VkVertexInputAttributeDescription uvAttr{}; + uvAttr.location = 1; + uvAttr.binding = 0; + uvAttr.format = VK_FORMAT_R32G32_SFLOAT; + uvAttr.offset = 3 * sizeof(float); // 12 + + // Dynamic viewport and scissor + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + // --- Build pipeline: alpha blending, no cull, depth test on / write off --- + pipeline_ = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, {posAttr, uvAttr}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS) // depth test on, write off + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setLayout(pipelineLayout_) + .setRenderPass(vkCtx_->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (pipeline_ == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create quest marker pipeline"); + return false; + } + + // --- Upload quad vertex buffer --- + createQuad(); + + // --- Load BLP textures --- + loadTextures(assetManager); + + LOG_INFO("QuestMarkerRenderer: Initialization complete"); return true; } void QuestMarkerRenderer::shutdown() { - if (vao_) glDeleteVertexArrays(1, &vao_); - if (vbo_) glDeleteBuffers(1, &vbo_); - if (shaderProgram_) glDeleteProgram(shaderProgram_); + if (!vkCtx_) return; + + VkDevice device = vkCtx_->getDevice(); + VmaAllocator allocator = vkCtx_->getAllocator(); + + // Wait for device idle before destroying resources + vkDeviceWaitIdle(device); + + // Destroy textures for (int i = 0; i < 3; ++i) { - if (textures_[i]) glDeleteTextures(1, &textures_[i]); + textures_[i].destroy(device, allocator); + texDescSets_[i] = VK_NULL_HANDLE; } + + // Destroy descriptor pool (frees all descriptor sets allocated from it) + if (descriptorPool_ != VK_NULL_HANDLE) { + vkDestroyDescriptorPool(device, descriptorPool_, nullptr); + descriptorPool_ = VK_NULL_HANDLE; + } + + // Destroy descriptor set layout + if (materialSetLayout_ != VK_NULL_HANDLE) { + vkDestroyDescriptorSetLayout(device, materialSetLayout_, nullptr); + materialSetLayout_ = VK_NULL_HANDLE; + } + + // Destroy pipeline + if (pipeline_ != VK_NULL_HANDLE) { + vkDestroyPipeline(device, pipeline_, nullptr); + pipeline_ = VK_NULL_HANDLE; + } + if (pipelineLayout_ != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); + pipelineLayout_ = VK_NULL_HANDLE; + } + + // Destroy quad vertex buffer + if (quadVB_ != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, quadVB_, quadVBAlloc_); + quadVB_ = VK_NULL_HANDLE; + quadVBAlloc_ = VK_NULL_HANDLE; + } + markers_.clear(); + vkCtx_ = nullptr; +} + +void QuestMarkerRenderer::createDescriptorResources() { + VkDevice device = vkCtx_->getDevice(); + + // Material set layout: binding 0 = combined image sampler (fragment stage) + VkDescriptorSetLayoutBinding samplerBinding{}; + samplerBinding.binding = 0; + samplerBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + samplerBinding.descriptorCount = 1; + samplerBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + materialSetLayout_ = createDescriptorSetLayout(device, {samplerBinding}); + + // Descriptor pool: 3 combined image samplers (one per marker type) + VkDescriptorPoolSize poolSize{}; + poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + poolSize.descriptorCount = 3; + + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.maxSets = 3; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + + if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool_) != VK_SUCCESS) { + LOG_ERROR("Failed to create quest marker descriptor pool"); + return; + } + + // Allocate 3 descriptor sets (one per texture) + VkDescriptorSetLayout layouts[3] = {materialSetLayout_, materialSetLayout_, materialSetLayout_}; + + VkDescriptorSetAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = descriptorPool_; + allocInfo.descriptorSetCount = 3; + allocInfo.pSetLayouts = layouts; + + if (vkAllocateDescriptorSets(device, &allocInfo, texDescSets_) != VK_SUCCESS) { + LOG_ERROR("Failed to allocate quest marker descriptor sets"); + } } void QuestMarkerRenderer::createQuad() { - // Billboard quad vertices (centered, 1 unit size) + // Billboard quad vertices (centered, 1 unit size) - 6 vertices for 2 triangles float vertices[] = { -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // bottom-left 0.5f, -0.5f, 0.0f, 1.0f, 1.0f, // bottom-right @@ -54,22 +232,10 @@ void QuestMarkerRenderer::createQuad() { 0.5f, 0.5f, 0.0f, 1.0f, 0.0f // top-right }; - glGenVertexArrays(1, &vao_); - glGenBuffers(1, &vbo_); - - glBindVertexArray(vao_); - glBindBuffer(GL_ARRAY_BUFFER, vbo_); - glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - - // Position attribute - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - - // Texture coord attribute - glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(1); - - glBindVertexArray(0); + AllocatedBuffer vbuf = uploadBuffer(*vkCtx_, + vertices, sizeof(vertices), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + quadVB_ = vbuf.buffer; + quadVBAlloc_ = vbuf.allocation; } void QuestMarkerRenderer::loadTextures(pipeline::AssetManager* assetManager) { @@ -79,6 +245,8 @@ void QuestMarkerRenderer::loadTextures(pipeline::AssetManager* assetManager) { "Interface\\GossipFrame\\IncompleteQuestIcon.blp" }; + VkDevice device = vkCtx_->getDevice(); + for (int i = 0; i < 3; ++i) { pipeline::BLPImage blp = assetManager->loadTexture(paths[i]); if (!blp.isValid()) { @@ -86,76 +254,32 @@ void QuestMarkerRenderer::loadTextures(pipeline::AssetManager* assetManager) { continue; } - glGenTextures(1, &textures_[i]); - glBindTexture(GL_TEXTURE_2D, textures_[i]); + // Upload RGBA data to VkTexture + if (!textures_[i].upload(*vkCtx_, blp.data.data(), blp.width, blp.height, + VK_FORMAT_R8G8B8A8_UNORM, true)) { + LOG_WARNING("Failed to upload quest marker texture to GPU: ", paths[i]); + continue; + } - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, blp.width, blp.height, - 0, GL_RGBA, GL_UNSIGNED_BYTE, blp.data.data()); + // Create sampler with clamp-to-edge + textures_[i].createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR, + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glGenerateMipmap(GL_TEXTURE_2D); + // Write descriptor set for this texture + VkDescriptorImageInfo imgInfo = textures_[i].descriptorInfo(); + + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = texDescSets_[i]; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imgInfo; + + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); LOG_INFO("Loaded quest marker texture: ", paths[i]); } - - glBindTexture(GL_TEXTURE_2D, 0); -} - -void QuestMarkerRenderer::createShader() { - const char* vertexShaderSource = R"( - #version 330 core - layout (location = 0) in vec3 aPos; - layout (location = 1) in vec2 aTexCoord; - - out vec2 TexCoord; - - uniform mat4 model; - uniform mat4 view; - uniform mat4 projection; - - void main() { - gl_Position = projection * view * model * vec4(aPos, 1.0); - TexCoord = aTexCoord; - } - )"; - - const char* fragmentShaderSource = R"( - #version 330 core - in vec2 TexCoord; - out vec4 FragColor; - - uniform sampler2D markerTexture; - uniform float uAlpha; - - void main() { - vec4 texColor = texture(markerTexture, TexCoord); - if (texColor.a < 0.1) - discard; - FragColor = vec4(texColor.rgb, texColor.a * uAlpha); - } - )"; - - // Compile vertex shader - uint32_t vertexShader = glCreateShader(GL_VERTEX_SHADER); - glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr); - glCompileShader(vertexShader); - - // Compile fragment shader - uint32_t fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); - glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr); - glCompileShader(fragmentShader); - - // Link shader program - shaderProgram_ = glCreateProgram(); - glAttachShader(shaderProgram_, vertexShader); - glAttachShader(shaderProgram_, fragmentShader); - glLinkProgram(shaderProgram_); - - glDeleteShader(vertexShader); - glDeleteShader(fragmentShader); } void QuestMarkerRenderer::setMarker(uint64_t guid, const glm::vec3& position, int markerType, float boundingHeight) { @@ -170,8 +294,8 @@ void QuestMarkerRenderer::clear() { markers_.clear(); } -void QuestMarkerRenderer::render(const Camera& camera) { - if (markers_.empty() || !shaderProgram_ || !vao_) return; +void QuestMarkerRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera) { + if (markers_.empty() || pipeline_ == VK_NULL_HANDLE || quadVB_ == VK_NULL_HANDLE) return; // WoW-style quest marker tuning parameters constexpr float BASE_SIZE = 0.65f; // Base world-space size @@ -181,38 +305,31 @@ void QuestMarkerRenderer::render(const Camera& camera) { constexpr float MIN_DIST = 4.0f; // Near clamp constexpr float MAX_DIST = 90.0f; // Far fade-out start constexpr float FADE_RANGE = 25.0f; // Fade-out range - constexpr float GLOW_ALPHA = 0.35f; // Glow pass alpha // Get time for bob animation float timeSeconds = SDL_GetTicks() / 1000.0f; - glEnable(GL_BLEND); - glEnable(GL_DEPTH_TEST); - glDepthMask(GL_FALSE); // Don't write to depth buffer - - glUseProgram(shaderProgram_); - glm::mat4 view = camera.getViewMatrix(); - glm::mat4 projection = camera.getProjectionMatrix(); glm::vec3 cameraPos = camera.getPosition(); - int viewLoc = glGetUniformLocation(shaderProgram_, "view"); - int projLoc = glGetUniformLocation(shaderProgram_, "projection"); - int modelLoc = glGetUniformLocation(shaderProgram_, "model"); - int alphaLoc = glGetUniformLocation(shaderProgram_, "uAlpha"); - - glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view)); - glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection)); - - glBindVertexArray(vao_); - // Get camera right and up vectors for billboarding glm::vec3 cameraRight = glm::vec3(view[0][0], view[1][0], view[2][0]); glm::vec3 cameraUp = glm::vec3(view[0][1], view[1][1], view[2][1]); + // Bind pipeline + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_); + + // Bind per-frame descriptor set (set 0) + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, + 0, 1, &perFrameSet, 0, nullptr); + + // Bind quad vertex buffer + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &quadVB_, &offset); + for (const auto& [guid, marker] : markers_) { if (marker.type < 0 || marker.type > 2) continue; - if (!textures_[marker.type]) continue; + if (!textures_[marker.type].isValid()) continue; // Calculate distance for LOD and culling glm::vec3 toCamera = cameraPos - marker.position; @@ -252,29 +369,22 @@ void QuestMarkerRenderer::render(const Camera& camera) { model[1] = glm::vec4(cameraUp * size, 0.0f); model[2] = glm::vec4(glm::cross(cameraRight, cameraUp), 0.0f); - glBindTexture(GL_TEXTURE_2D, textures_[marker.type]); + // Bind material descriptor set (set 1) for this marker's texture + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, + 1, 1, &texDescSets_[marker.type], 0, nullptr); - // Glow pass (subtle additive glow for available/turnin markers) - if (marker.type == 0 || marker.type == 1) { // Available or turnin - glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Additive blending - glUniform1f(alphaLoc, fadeAlpha * GLOW_ALPHA); // Reduced alpha for glow - glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); - glDrawArrays(GL_TRIANGLES, 0, 6); + // Push constants: model matrix + alpha + QuestMarkerPushConstants push{}; + push.model = model; + push.alpha = fadeAlpha; - // Restore standard alpha blending for main pass - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - } + vkCmdPushConstants(cmd, pipelineLayout_, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof(push), &push); - // Main pass with fade alpha - glUniform1f(alphaLoc, fadeAlpha); - glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); - glDrawArrays(GL_TRIANGLES, 0, 6); + // Draw the quad (6 vertices, 2 triangles) + vkCmdDraw(cmd, 6, 1, 0, 0); } - - glBindVertexArray(0); - glBindTexture(GL_TEXTURE_2D, 0); - glDepthMask(GL_TRUE); - glDisable(GL_BLEND); } }} // namespace wowee::rendering diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 2103419a..a1c91a82 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -22,6 +22,7 @@ #include "rendering/wmo_renderer.hpp" #include "rendering/m2_renderer.hpp" #include "rendering/minimap.hpp" +#include "rendering/world_map.hpp" #include "rendering/quest_marker_renderer.hpp" #include "rendering/shader.hpp" #include "game/game_handler.hpp" @@ -50,7 +51,14 @@ #include "audio/combat_sound_manager.hpp" #include "audio/spell_sound_manager.hpp" #include "audio/movement_sound_manager.hpp" -#include +#include // TODO: Remove in Phase 7 (unconverted sub-renderers still reference GL types) +#include "rendering/vk_context.hpp" +#include "rendering/vk_frame_data.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_utils.hpp" +#include +#include #include #include #include @@ -259,10 +267,273 @@ static void loadEmotesFromDbc() { Renderer::Renderer() = default; Renderer::~Renderer() = default; +bool Renderer::createPerFrameResources() { + VkDevice device = vkCtx->getDevice(); + + // --- Create shadow depth image --- + VkImageCreateInfo imgCI{}; + imgCI.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imgCI.imageType = VK_IMAGE_TYPE_2D; + imgCI.format = VK_FORMAT_D32_SFLOAT; + imgCI.extent = {SHADOW_MAP_SIZE, SHADOW_MAP_SIZE, 1}; + imgCI.mipLevels = 1; + imgCI.arrayLayers = 1; + imgCI.samples = VK_SAMPLE_COUNT_1_BIT; + imgCI.tiling = VK_IMAGE_TILING_OPTIMAL; + imgCI.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + VmaAllocationCreateInfo imgAllocCI{}; + imgAllocCI.usage = VMA_MEMORY_USAGE_GPU_ONLY; + if (vmaCreateImage(vkCtx->getAllocator(), &imgCI, &imgAllocCI, + &shadowDepthImage, &shadowDepthAlloc, nullptr) != VK_SUCCESS) { + LOG_ERROR("Failed to create shadow depth image"); + return false; + } + + // --- Create shadow depth image view --- + VkImageViewCreateInfo viewCI{}; + viewCI.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewCI.image = shadowDepthImage; + viewCI.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewCI.format = VK_FORMAT_D32_SFLOAT; + viewCI.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1}; + if (vkCreateImageView(device, &viewCI, nullptr, &shadowDepthView) != VK_SUCCESS) { + LOG_ERROR("Failed to create shadow depth image view"); + return false; + } + + // --- Create shadow sampler --- + VkSamplerCreateInfo sampCI{}; + sampCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + sampCI.magFilter = VK_FILTER_LINEAR; + sampCI.minFilter = VK_FILTER_LINEAR; + sampCI.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + sampCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; + sampCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; + sampCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; + sampCI.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; + sampCI.compareEnable = VK_TRUE; + sampCI.compareOp = VK_COMPARE_OP_LESS_OR_EQUAL; + if (vkCreateSampler(device, &sampCI, nullptr, &shadowSampler) != VK_SUCCESS) { + LOG_ERROR("Failed to create shadow sampler"); + return false; + } + + // --- Create shadow render pass (depth-only) --- + VkAttachmentDescription depthAtt{}; + depthAtt.format = VK_FORMAT_D32_SFLOAT; + depthAtt.samples = VK_SAMPLE_COUNT_1_BIT; + depthAtt.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + depthAtt.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + depthAtt.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + depthAtt.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + depthAtt.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + depthAtt.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + + VkAttachmentReference depthRef{}; + depthRef.attachment = 0; + depthRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.pDepthStencilAttachment = &depthRef; + + VkSubpassDependency dep{}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + dep.dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dep.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + dep.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + + VkRenderPassCreateInfo rpCI{}; + rpCI.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + rpCI.attachmentCount = 1; + rpCI.pAttachments = &depthAtt; + rpCI.subpassCount = 1; + rpCI.pSubpasses = &subpass; + rpCI.dependencyCount = 1; + rpCI.pDependencies = &dep; + if (vkCreateRenderPass(device, &rpCI, nullptr, &shadowRenderPass) != VK_SUCCESS) { + LOG_ERROR("Failed to create shadow render pass"); + return false; + } + + // --- Create shadow framebuffer --- + VkFramebufferCreateInfo fbCI{}; + fbCI.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbCI.renderPass = shadowRenderPass; + fbCI.attachmentCount = 1; + fbCI.pAttachments = &shadowDepthView; + fbCI.width = SHADOW_MAP_SIZE; + fbCI.height = SHADOW_MAP_SIZE; + fbCI.layers = 1; + if (vkCreateFramebuffer(device, &fbCI, nullptr, &shadowFramebuffer) != VK_SUCCESS) { + LOG_ERROR("Failed to create shadow framebuffer"); + return false; + } + + // --- Create descriptor set layout for set 0 (per-frame UBO + shadow sampler) --- + VkDescriptorSetLayoutBinding bindings[2]{}; + bindings[0].binding = 0; + bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + bindings[0].descriptorCount = 1; + bindings[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + bindings[1].binding = 1; + bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + bindings[1].descriptorCount = 1; + bindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + VkDescriptorSetLayoutCreateInfo layoutInfo{}; + layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + layoutInfo.bindingCount = 2; + layoutInfo.pBindings = bindings; + + if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &perFrameSetLayout) != VK_SUCCESS) { + LOG_ERROR("Failed to create per-frame descriptor set layout"); + return false; + } + + // --- Create descriptor pool for both UBO and combined image sampler --- + VkDescriptorPoolSize poolSizes[2]{}; + poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + poolSizes[0].descriptorCount = MAX_FRAMES; + poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + poolSizes[1].descriptorCount = MAX_FRAMES; + + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.maxSets = MAX_FRAMES; + poolInfo.poolSizeCount = 2; + poolInfo.pPoolSizes = poolSizes; + + if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &sceneDescriptorPool) != VK_SUCCESS) { + LOG_ERROR("Failed to create scene descriptor pool"); + return false; + } + + // --- Create per-frame UBOs and descriptor sets --- + for (uint32_t i = 0; i < MAX_FRAMES; i++) { + // Create mapped UBO + VkBufferCreateInfo bufInfo{}; + bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufInfo.size = sizeof(GPUPerFrameData); + bufInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + + VmaAllocationCreateInfo allocInfo{}; + allocInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + allocInfo.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + + VmaAllocationInfo mapInfo{}; + if (vmaCreateBuffer(vkCtx->getAllocator(), &bufInfo, &allocInfo, + &perFrameUBOs[i], &perFrameUBOAllocs[i], &mapInfo) != VK_SUCCESS) { + LOG_ERROR("Failed to create per-frame UBO ", i); + return false; + } + perFrameUBOMapped[i] = mapInfo.pMappedData; + + // Allocate descriptor set + VkDescriptorSetAllocateInfo setAlloc{}; + setAlloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + setAlloc.descriptorPool = sceneDescriptorPool; + setAlloc.descriptorSetCount = 1; + setAlloc.pSetLayouts = &perFrameSetLayout; + + if (vkAllocateDescriptorSets(device, &setAlloc, &perFrameDescSets[i]) != VK_SUCCESS) { + LOG_ERROR("Failed to allocate per-frame descriptor set ", i); + return false; + } + + // Write binding 0 (UBO) and binding 1 (shadow sampler) + VkDescriptorBufferInfo descBuf{}; + descBuf.buffer = perFrameUBOs[i]; + descBuf.offset = 0; + descBuf.range = sizeof(GPUPerFrameData); + + VkDescriptorImageInfo shadowImgInfo{}; + shadowImgInfo.sampler = shadowSampler; + shadowImgInfo.imageView = shadowDepthView; + shadowImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + VkWriteDescriptorSet writes[2]{}; + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].dstSet = perFrameDescSets[i]; + writes[0].dstBinding = 0; + writes[0].descriptorCount = 1; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writes[0].pBufferInfo = &descBuf; + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].dstSet = perFrameDescSets[i]; + writes[1].dstBinding = 1; + writes[1].descriptorCount = 1; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[1].pImageInfo = &shadowImgInfo; + + vkUpdateDescriptorSets(device, 2, writes, 0, nullptr); + } + + LOG_INFO("Per-frame Vulkan resources created (shadow map ", SHADOW_MAP_SIZE, "x", SHADOW_MAP_SIZE, ")"); + return true; +} + +void Renderer::destroyPerFrameResources() { + if (!vkCtx) return; + vkDeviceWaitIdle(vkCtx->getDevice()); + VkDevice device = vkCtx->getDevice(); + + for (uint32_t i = 0; i < MAX_FRAMES; i++) { + if (perFrameUBOs[i]) { + vmaDestroyBuffer(vkCtx->getAllocator(), perFrameUBOs[i], perFrameUBOAllocs[i]); + perFrameUBOs[i] = VK_NULL_HANDLE; + } + } + if (sceneDescriptorPool) { + vkDestroyDescriptorPool(device, sceneDescriptorPool, nullptr); + sceneDescriptorPool = VK_NULL_HANDLE; + } + if (perFrameSetLayout) { + vkDestroyDescriptorSetLayout(device, perFrameSetLayout, nullptr); + perFrameSetLayout = VK_NULL_HANDLE; + } + + // Destroy shadow resources + if (shadowFramebuffer) { vkDestroyFramebuffer(device, shadowFramebuffer, nullptr); shadowFramebuffer = VK_NULL_HANDLE; } + if (shadowRenderPass) { vkDestroyRenderPass(device, shadowRenderPass, nullptr); shadowRenderPass = VK_NULL_HANDLE; } + if (shadowDepthView) { vkDestroyImageView(device, shadowDepthView, nullptr); shadowDepthView = VK_NULL_HANDLE; } + if (shadowDepthImage) { vmaDestroyImage(vkCtx->getAllocator(), shadowDepthImage, shadowDepthAlloc); shadowDepthImage = VK_NULL_HANDLE; shadowDepthAlloc = VK_NULL_HANDLE; } + if (shadowSampler) { vkDestroySampler(device, shadowSampler, nullptr); shadowSampler = VK_NULL_HANDLE; } +} + +void Renderer::updatePerFrameUBO() { + if (!camera) return; + + currentFrameData.view = camera->getViewMatrix(); + currentFrameData.projection = camera->getProjectionMatrix(); + currentFrameData.viewPos = glm::vec4(camera->getPosition(), 1.0f); + currentFrameData.fogParams.z = globalTime; + + // Lighting from LightingManager + if (lightingManager) { + const auto& lp = lightingManager->getLightingParams(); + currentFrameData.lightDir = glm::vec4(lp.directionalDir, 0.0f); + currentFrameData.lightColor = glm::vec4(lp.diffuseColor, 1.0f); + currentFrameData.ambientColor = glm::vec4(lp.ambientColor, 1.0f); + currentFrameData.fogColor = glm::vec4(lp.fogColor, 1.0f); + currentFrameData.fogParams.x = lp.fogStart; + currentFrameData.fogParams.y = lp.fogEnd; + } + + currentFrameData.shadowParams = glm::vec4(shadowsEnabled ? 1.0f : 0.0f, 0.5f, 0.0f, 0.0f); + + // Copy to current frame's mapped UBO + uint32_t frame = vkCtx->getCurrentFrame(); + std::memcpy(perFrameUBOMapped[frame], ¤tFrameData, sizeof(GPUPerFrameData)); +} + bool Renderer::initialize(core::Window* win) { window = win; + vkCtx = win->getVkContext(); deferredWorldInitEnabled_ = envFlagEnabled("WOWEE_DEFER_WORLD_SYSTEMS", true); - LOG_INFO("Initializing renderer"); + LOG_INFO("Initializing renderer (Vulkan)"); // Create camera (in front of Stormwind gate, looking north) camera = std::make_unique(); @@ -283,129 +554,46 @@ bool Renderer::initialize(core::Window* win) { performanceHUD = std::make_unique(); performanceHUD->setPosition(PerformanceHUD::Position::TOP_LEFT); - // Create water renderer - waterRenderer = std::make_unique(); - if (!waterRenderer->initialize()) { - LOG_WARNING("Failed to initialize water renderer"); - waterRenderer.reset(); + // Create per-frame UBO and descriptor sets + if (!createPerFrameResources()) { + LOG_ERROR("Failed to create per-frame Vulkan resources"); + return false; } - // Create skybox - skybox = std::make_unique(); - if (!skybox->initialize()) { - LOG_WARNING("Failed to initialize skybox"); - skybox.reset(); - } else { - skybox->setTimeOfDay(12.0f); // Start at noon - } + // Initialize Vulkan sub-renderers (Phase 3) - // Create celestial renderer (sun and moon) - celestial = std::make_unique(); - if (!celestial->initialize()) { - LOG_WARNING("Failed to initialize celestial renderer"); - celestial.reset(); - } - - // Create star field - starField = std::make_unique(); - if (!starField->initialize()) { - LOG_WARNING("Failed to initialize star field"); - starField.reset(); - } - - // Create clouds - clouds = std::make_unique(); - if (!clouds->initialize()) { - LOG_WARNING("Failed to initialize clouds"); - clouds.reset(); - } else { - clouds->setDensity(0.5f); // Medium cloud coverage - } - - // Create lens flare - lensFlare = std::make_unique(); - if (!lensFlare->initialize()) { - LOG_WARNING("Failed to initialize lens flare"); - lensFlare.reset(); - } - - // Create sky system (coordinator for sky rendering) + // Sky system (owns skybox, starfield, celestial, clouds, lens flare) skySystem = std::make_unique(); - if (!skySystem->initialize()) { - LOG_WARNING("Failed to initialize sky system"); - skySystem.reset(); - } else { - // Note: SkySystem manages its own components internally - // Keep existing components for backwards compatibility (PerformanceHUD access) - LOG_INFO("Sky system initialized successfully (coordinator active)"); + if (!skySystem->initialize(vkCtx, perFrameSetLayout)) { + LOG_ERROR("Failed to initialize sky system"); + return false; } + // Expose sub-components via renderer accessors + skybox = nullptr; // Owned by skySystem; access via skySystem->getSkybox() + celestial = nullptr; + starField = nullptr; + clouds = nullptr; + lensFlare = nullptr; - // Create weather system weather = std::make_unique(); - if (!weather->initialize()) { - LOG_WARNING("Failed to initialize weather"); - weather.reset(); - } + weather->initialize(vkCtx, perFrameSetLayout); - // Create lighting system - lightingManager = std::make_unique(); - auto* assetManager = core::Application::getInstance().getAssetManager(); - if (assetManager && !lightingManager->initialize(assetManager)) { - LOG_WARNING("Failed to initialize lighting manager"); - lightingManager.reset(); - } - - // Create swim effects swimEffects = std::make_unique(); - if (!swimEffects->initialize()) { - LOG_WARNING("Failed to initialize swim effects"); - swimEffects.reset(); - } + swimEffects->initialize(vkCtx, perFrameSetLayout); - // Create mount dust effects mountDust = std::make_unique(); - if (!mountDust->initialize()) { - LOG_WARNING("Failed to initialize mount dust effects"); - mountDust.reset(); - } + mountDust->initialize(vkCtx, perFrameSetLayout); + + chargeEffect = std::make_unique(); + chargeEffect->initialize(vkCtx, perFrameSetLayout); - // Create level-up effect (model loaded later via loadLevelUpEffect) levelUpEffect = std::make_unique(); - // Create charge effect (point-sprite particles + optional M2 models) - chargeEffect = std::make_unique(); - if (!chargeEffect->initialize()) { - LOG_WARNING("Failed to initialize charge effect"); - chargeEffect.reset(); - } + LOG_INFO("Vulkan sub-renderers initialized (Phase 3)"); - // Create character renderer - characterRenderer = std::make_unique(); - if (!characterRenderer->initialize()) { - LOG_WARNING("Failed to initialize character renderer"); - characterRenderer.reset(); - } - - // Create WMO renderer - wmoRenderer = std::make_unique(); - if (!wmoRenderer->initialize(assetManager)) { - LOG_WARNING("Failed to initialize WMO renderer"); - wmoRenderer.reset(); - } - - // Create minimap - minimap = std::make_unique(); - if (!minimap->initialize(200)) { - LOG_WARNING("Failed to initialize minimap"); - minimap.reset(); - } - - // Create quest marker renderer (initialized later with AssetManager) - questMarkerRenderer = std::make_unique(); - - // Create M2 renderer (for doodads) - m2Renderer = std::make_unique(); - // Note: M2 renderer needs asset manager, will be initialized when terrain loads + // LightingManager doesn't use GL — initialize for data-only use + lightingManager = std::make_unique(); + [[maybe_unused]] auto* assetManager = core::Application::getInstance().getAssetManager(); // Create zone manager zoneManager = std::make_unique(); @@ -428,42 +616,8 @@ bool Renderer::initialize(core::Window* win) { spellSoundManager = std::make_unique(); movementSoundManager = std::make_unique(); - // Underwater full-screen tint overlay (applies to all world geometry). - underwaterOverlayShader = std::make_unique(); - const char* overlayVS = R"( - #version 330 core - layout (location = 0) in vec2 aPos; - void main() { gl_Position = vec4(aPos, 0.0, 1.0); } - )"; - const char* overlayFS = R"( - #version 330 core - uniform vec4 uTint; - out vec4 FragColor; - void main() { FragColor = uTint; } - )"; - if (!underwaterOverlayShader->loadFromSource(overlayVS, overlayFS)) { - LOG_WARNING("Failed to initialize underwater overlay shader"); - underwaterOverlayShader.reset(); - } else { - const float quadVerts[] = { - -1.0f, -1.0f, 1.0f, -1.0f, - -1.0f, 1.0f, 1.0f, 1.0f - }; - glGenVertexArrays(1, &underwaterOverlayVAO); - glGenBuffers(1, &underwaterOverlayVBO); - glBindVertexArray(underwaterOverlayVAO); - glBindBuffer(GL_ARRAY_BUFFER, underwaterOverlayVBO); - glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW); - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - glBindVertexArray(0); - } - - // Initialize post-process FBO pipeline - initPostProcess(window->getWidth(), window->getHeight()); - - // Initialize shadow map - initShadowMap(); + // TODO Phase 6: Vulkan underwater overlay, post-process, and shadow map + // GL versions stubbed during migration LOG_INFO("Renderer initialized"); return true; @@ -485,28 +639,27 @@ void Renderer::shutdown() { waterRenderer.reset(); } - if (skybox) { - skybox->shutdown(); - skybox.reset(); + if (minimap) { + minimap->shutdown(); + minimap.reset(); } - if (celestial) { - celestial->shutdown(); - celestial.reset(); + if (worldMap) { + worldMap->shutdown(); + worldMap.reset(); } - if (starField) { - starField->shutdown(); - starField.reset(); + if (skySystem) { + skySystem->shutdown(); + skySystem.reset(); } - if (clouds) { - clouds.reset(); - } - - if (lensFlare) { - lensFlare.reset(); - } + // Individual sky components are owned by skySystem; just null the aliases + skybox = nullptr; + celestial = nullptr; + starField = nullptr; + clouds = nullptr; + lensFlare = nullptr; if (weather) { weather.reset(); @@ -548,22 +701,16 @@ void Renderer::shutdown() { // Shutdown AudioEngine singleton audio::AudioEngine::instance().shutdown(); - if (underwaterOverlayVAO) { - glDeleteVertexArrays(1, &underwaterOverlayVAO); - underwaterOverlayVAO = 0; + // Cleanup Vulkan selection circle resources + if (vkCtx) { + VkDevice device = vkCtx->getDevice(); + if (selCirclePipeline) { vkDestroyPipeline(device, selCirclePipeline, nullptr); selCirclePipeline = VK_NULL_HANDLE; } + if (selCirclePipelineLayout) { vkDestroyPipelineLayout(device, selCirclePipelineLayout, nullptr); selCirclePipelineLayout = VK_NULL_HANDLE; } + if (selCircleVertBuf) { vmaDestroyBuffer(vkCtx->getAllocator(), selCircleVertBuf, selCircleVertAlloc); selCircleVertBuf = VK_NULL_HANDLE; selCircleVertAlloc = VK_NULL_HANDLE; } + if (selCircleIdxBuf) { vmaDestroyBuffer(vkCtx->getAllocator(), selCircleIdxBuf, selCircleIdxAlloc); selCircleIdxBuf = VK_NULL_HANDLE; selCircleIdxAlloc = VK_NULL_HANDLE; } } - if (underwaterOverlayVBO) { - glDeleteBuffers(1, &underwaterOverlayVBO); - underwaterOverlayVBO = 0; - } - underwaterOverlayShader.reset(); - // Cleanup shadow map resources - if (shadowFBO) { glDeleteFramebuffers(1, &shadowFBO); shadowFBO = 0; } - if (shadowDepthTex) { glDeleteTextures(1, &shadowDepthTex); shadowDepthTex = 0; } - if (shadowShaderProgram) { glDeleteProgram(shadowShaderProgram); shadowShaderProgram = 0; } - - shutdownPostProcess(); + destroyPerFrameResources(); zoneManager.reset(); @@ -576,21 +723,85 @@ void Renderer::shutdown() { } void Renderer::beginFrame() { - // Resize post-process FBO if window size changed - int w = window->getWidth(); - int h = window->getHeight(); - if (w != fbWidth || h != fbHeight) { - resizePostProcess(w, h); + if (!vkCtx) return; + + // Handle swapchain recreation if needed + if (vkCtx->isSwapchainDirty()) { + vkCtx->recreateSwapchain(window->getWidth(), window->getHeight()); } - // Clear default framebuffer (login screen renders here directly) - glBindFramebuffer(GL_FRAMEBUFFER, 0); - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + // Acquire swapchain image and begin command buffer + currentCmd = vkCtx->beginFrame(currentImageIndex); + if (currentCmd == VK_NULL_HANDLE) { + // Swapchain out of date, will retry next frame + return; + } + + // Update per-frame UBO with current camera/lighting state + updatePerFrameUBO(); + + // --- Off-screen pre-passes (before main render pass) --- + // Minimap composite (renders 3x3 tile grid into 768x768 render target) + if (minimap && minimap->isEnabled() && camera) { + glm::vec3 minimapCenter = camera->getPosition(); + if (cameraController && cameraController->isThirdPerson()) + minimapCenter = characterPosition; + minimap->compositePass(currentCmd, minimapCenter); + } + // World map composite (renders zone tiles into 1024x768 render target) + if (worldMap) { + worldMap->compositePass(currentCmd); + } + + // Shadow pre-pass (before main render pass) + if (shadowsEnabled && shadowDepthImage != VK_NULL_HANDLE) { + renderShadowPass(); + } + + // --- Begin main render pass (clear color + depth) --- + VkRenderPassBeginInfo rpInfo{}; + rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rpInfo.renderPass = vkCtx->getImGuiRenderPass(); + rpInfo.framebuffer = vkCtx->getSwapchainFramebuffers()[currentImageIndex]; + rpInfo.renderArea.offset = {0, 0}; + rpInfo.renderArea.extent = vkCtx->getSwapchainExtent(); + + VkClearValue clearValues[2]{}; + clearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; + clearValues[1].depthStencil = {1.0f, 0}; + rpInfo.clearValueCount = 2; + rpInfo.pClearValues = clearValues; + + vkCmdBeginRenderPass(currentCmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE); + + // Set dynamic viewport and scissor + VkExtent2D extent = vkCtx->getSwapchainExtent(); + VkViewport viewport{}; + viewport.x = 0.0f; + viewport.y = 0.0f; + viewport.width = static_cast(extent.width); + viewport.height = static_cast(extent.height); + viewport.minDepth = 0.0f; + viewport.maxDepth = 1.0f; + vkCmdSetViewport(currentCmd, 0, 1, &viewport); + + VkRect2D scissor{}; + scissor.offset = {0, 0}; + scissor.extent = extent; + vkCmdSetScissor(currentCmd, 0, 1, &scissor); } void Renderer::endFrame() { - // Nothing needed here for now + if (!vkCtx || currentCmd == VK_NULL_HANDLE) return; + + // Record ImGui draw commands into the command buffer + ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), currentCmd); + + vkCmdEndRenderPass(currentCmd); + + // Submit and present + vkCtx->endFrame(currentCmd, currentImageIndex); + currentCmd = VK_NULL_HANDLE; } void Renderer::setCharacterFollow(uint32_t instanceId) { @@ -1918,6 +2129,7 @@ audio::FootstepSurface Renderer::resolveFootstepSurface() const { } void Renderer::update(float deltaTime) { + globalTime += deltaTime; if (musicSwitchCooldown_ > 0.0f) { musicSwitchCooldown_ = std::max(0.0f, musicSwitchCooldown_ - deltaTime); } @@ -2043,25 +2255,10 @@ void Renderer::update(float deltaTime) { auto terrain2 = std::chrono::high_resolution_clock::now(); terrainTime += std::chrono::duration(terrain2 - terrain1).count(); - // Update skybox time progression + // Update sky system (skybox time, star twinkle, clouds, celestial moon phases) auto sky1 = std::chrono::high_resolution_clock::now(); - if (skybox) { - skybox->update(deltaTime); - } - - // Update star field twinkle - if (starField) { - starField->update(deltaTime); - } - - // Update clouds animation - if (clouds) { - clouds->update(deltaTime); - } - - // Update celestial (moon phase cycling) - if (celestial) { - celestial->update(deltaTime); + if (skySystem) { + skySystem->update(deltaTime); } // Update weather particles @@ -2497,7 +2694,7 @@ void Renderer::runDeferredWorldInitStep(float deltaTime) { if (movementSoundManager) movementSoundManager->initialize(cachedAssetManager); break; case 5: - if (questMarkerRenderer) questMarkerRenderer->initialize(cachedAssetManager); + if (questMarkerRenderer) questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, cachedAssetManager); break; default: deferredWorldInitPending_ = false; @@ -2513,82 +2710,90 @@ void Renderer::runDeferredWorldInitStep(float deltaTime) { // ============================================================ void Renderer::initSelectionCircle() { - if (selCircleVAO) return; + if (selCirclePipeline != VK_NULL_HANDLE) return; + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); - // Selection effect shader: thin outer ring + inward fade toward center. - const char* vsSrc = R"( - #version 330 core - layout(location = 0) in vec3 aPos; - uniform mat4 uMVP; - out vec2 vLocalPos; - void main() { - vLocalPos = aPos.xy; - gl_Position = uMVP * vec4(aPos, 1.0); - } - )"; - const char* fsSrc = R"( - #version 330 core - uniform vec3 uColor; - in vec2 vLocalPos; - out vec4 FragColor; - void main() { - float r = clamp(length(vLocalPos), 0.0, 1.0); + // Load shaders + VkShaderModule vertShader, fragShader; + if (!vertShader.loadFromFile(device, "assets/shaders/selection_circle.vert.spv")) { + LOG_ERROR("initSelectionCircle: failed to load vertex shader"); + return; + } + if (!fragShader.loadFromFile(device, "assets/shaders/selection_circle.frag.spv")) { + LOG_ERROR("initSelectionCircle: failed to load fragment shader"); + vertShader.destroy(); + return; + } - float ringInner = 0.93; - float ringOuter = 1.00; - float ring = smoothstep(ringInner - 0.01, ringInner + 0.01, r) * - (1.0 - smoothstep(ringOuter - 0.008, ringOuter + 0.004, r)); + // Pipeline layout: push constants only (mat4 mvp=64 + vec4 color=16), VERTEX|FRAGMENT + VkPushConstantRange pcRange{}; + pcRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + pcRange.offset = 0; + pcRange.size = 80; + selCirclePipelineLayout = createPipelineLayout(device, {}, {pcRange}); - float inward = smoothstep(0.0, ringInner, r); - inward = pow(inward, 1.9) * (1.0 - smoothstep(ringInner - 0.015, ringInner + 0.01, r)); + // Vertex input: binding 0, stride 12, vec3 at location 0 + VkVertexInputBindingDescription vertBind{0, 12, VK_VERTEX_INPUT_RATE_VERTEX}; + VkVertexInputAttributeDescription vertAttr{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}; - float alpha = max(ring * 0.9, inward * 0.45); - FragColor = vec4(uColor, alpha); - } - )"; - - auto compile = [](GLenum type, const char* src) -> GLuint { - GLuint s = glCreateShader(type); - glShaderSource(s, 1, &src, nullptr); - glCompileShader(s); - return s; - }; - - GLuint vs = compile(GL_VERTEX_SHADER, vsSrc); - GLuint fs = compile(GL_FRAGMENT_SHADER, fsSrc); - selCircleShader = glCreateProgram(); - glAttachShader(selCircleShader, vs); - glAttachShader(selCircleShader, fs); - glLinkProgram(selCircleShader); - glDeleteShader(vs); - glDeleteShader(fs); - - // Build a unit disc; fragment shader shapes ring+gradient by radius. + // Build disc geometry as TRIANGLE_LIST (replaces GL_TRIANGLE_FAN) + // N=48 segments: center at origin + ring verts constexpr int SEGMENTS = 48; std::vector verts; - verts.reserve((SEGMENTS + 2) * 3); - - verts.push_back(0.0f); - verts.push_back(0.0f); - verts.push_back(0.0f); - + verts.reserve((SEGMENTS + 1) * 3); + // Center vertex + verts.insert(verts.end(), {0.0f, 0.0f, 0.0f}); + // Ring vertices for (int i = 0; i <= SEGMENTS; ++i) { float angle = 2.0f * 3.14159265f * static_cast(i) / static_cast(SEGMENTS); - float c = std::cos(angle), s = std::sin(angle); - verts.push_back(c); - verts.push_back(s); + verts.push_back(std::cos(angle)); + verts.push_back(std::sin(angle)); verts.push_back(0.0f); } - selCircleVertCount = static_cast(SEGMENTS + 2); - glGenVertexArrays(1, &selCircleVAO); - glGenBuffers(1, &selCircleVBO); - glBindVertexArray(selCircleVAO); - glBindBuffer(GL_ARRAY_BUFFER, selCircleVBO); - glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.data(), GL_STATIC_DRAW); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr); - glEnableVertexAttribArray(0); - glBindVertexArray(0); + // Build TRIANGLE_LIST indices: N triangles (center=0, ring[i]=i+1, ring[i+1]=i+2) + std::vector indices; + indices.reserve(SEGMENTS * 3); + for (int i = 0; i < SEGMENTS; ++i) { + indices.push_back(0); + indices.push_back(static_cast(i + 1)); + indices.push_back(static_cast(i + 2)); + } + selCircleVertCount = SEGMENTS * 3; // index count for drawing + + // Upload vertex buffer + AllocatedBuffer vbuf = uploadBuffer(*vkCtx, verts.data(), + verts.size() * sizeof(float), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + selCircleVertBuf = vbuf.buffer; + selCircleVertAlloc = vbuf.allocation; + + // Upload index buffer + AllocatedBuffer ibuf = uploadBuffer(*vkCtx, indices.data(), + indices.size() * sizeof(uint16_t), VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + selCircleIdxBuf = ibuf.buffer; + selCircleIdxAlloc = ibuf.allocation; + + // Build pipeline: alpha blend, no depth write/test, TRIANGLE_LIST, CULL_NONE + selCirclePipeline = PipelineBuilder() + .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({vertBind}, {vertAttr}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setNoDepthTest() + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setLayout(selCirclePipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device); + + vertShader.destroy(); + fragShader.destroy(); + + if (!selCirclePipeline) { + LOG_ERROR("initSelectionCircle: failed to build pipeline"); + } } void Renderer::setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color) { @@ -2605,6 +2810,7 @@ void Renderer::clearSelectionCircle() { void Renderer::renderSelectionCircle(const glm::mat4& view, const glm::mat4& projection) { if (!selCircleVisible) return; initSelectionCircle(); + if (selCirclePipeline == VK_NULL_HANDLE || currentCmd == VK_NULL_HANDLE) return; // Keep circle anchored near target foot Z. Accept nearby floor probes only, // so distant upper/lower WMO planes don't yank the ring away from feet. @@ -2634,28 +2840,117 @@ void Renderer::renderSelectionCircle(const glm::mat4& view, const glm::mat4& pro model = glm::scale(model, glm::vec3(selCircleRadius)); glm::mat4 mvp = projection * view * model; + glm::vec4 color4(selCircleColor, 1.0f); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glDisable(GL_CULL_FACE); - glDepthMask(GL_FALSE); - GLboolean depthTestWasEnabled = glIsEnabled(GL_DEPTH_TEST); - glDisable(GL_DEPTH_TEST); - - glUseProgram(selCircleShader); - glUniformMatrix4fv(glGetUniformLocation(selCircleShader, "uMVP"), 1, GL_FALSE, &mvp[0][0]); - glUniform3fv(glGetUniformLocation(selCircleShader, "uColor"), 1, &selCircleColor[0]); - - glBindVertexArray(selCircleVAO); - glDrawArrays(GL_TRIANGLE_FAN, 0, selCircleVertCount); - glBindVertexArray(0); - - if (depthTestWasEnabled) glEnable(GL_DEPTH_TEST); - glDepthMask(GL_TRUE); - glEnable(GL_CULL_FACE); + vkCmdBindPipeline(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, selCirclePipeline); + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(currentCmd, 0, 1, &selCircleVertBuf, &offset); + vkCmdBindIndexBuffer(currentCmd, selCircleIdxBuf, 0, VK_INDEX_TYPE_UINT16); + // Push mvp (64 bytes) at offset 0 + vkCmdPushConstants(currentCmd, selCirclePipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, 64, &mvp[0][0]); + // Push color (16 bytes) at offset 64 + vkCmdPushConstants(currentCmd, selCirclePipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 64, 16, &color4[0]); + vkCmdDrawIndexed(currentCmd, static_cast(selCircleVertCount), 1, 0, 0, 0); } void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { + (void)world; // Used later in Phases 4-5 + + auto renderStart = std::chrono::steady_clock::now(); + lastTerrainRenderMs = 0.0; + lastWMORenderMs = 0.0; + lastM2RenderMs = 0.0; + + uint32_t frameIdx = vkCtx->getCurrentFrame(); + VkDescriptorSet perFrameSet = perFrameDescSets[frameIdx]; + + // Get time of day for sky-related rendering + float timeOfDay = (skySystem && skySystem->getSkybox()) ? skySystem->getSkybox()->getTimeOfDay() : 12.0f; + + // Render sky system (unified coordinator for skybox, stars, celestial, clouds, lens flare) + if (skySystem && camera) { + rendering::SkyParams skyParams; + skyParams.timeOfDay = timeOfDay; + skyParams.gameTime = gameHandler ? gameHandler->getGameTime() : -1.0f; + + if (lightingManager) { + const auto& lighting = lightingManager->getLightingParams(); + skyParams.directionalDir = lighting.directionalDir; + skyParams.sunColor = lighting.diffuseColor; + skyParams.skyTopColor = lighting.skyTopColor; + skyParams.skyMiddleColor = lighting.skyMiddleColor; + skyParams.skyBand1Color = lighting.skyBand1Color; + skyParams.skyBand2Color = lighting.skyBand2Color; + skyParams.cloudDensity = lighting.cloudDensity; + skyParams.fogDensity = lighting.fogDensity; + skyParams.horizonGlow = lighting.horizonGlow; + } + + skyParams.skyboxModelId = 0; + skyParams.skyboxHasStars = false; + + skySystem->render(currentCmd, perFrameSet, *camera, skyParams); + } + + // Terrain rendering + if (terrainRenderer && camera && terrainEnabled) { + terrainRenderer->render(currentCmd, perFrameSet, *camera); + } + + // Water rendering (after terrain, transparent) + if (waterRenderer && camera) { + waterRenderer->render(currentCmd, perFrameSet, *camera, globalTime); + } + + // Render weather particles (after terrain/water, before characters) + if (weather && camera) { + weather->render(currentCmd, perFrameSet); + } + + // Render swim effects (ripples and bubbles) + if (swimEffects && camera) { + swimEffects->render(currentCmd, perFrameSet); + } + + // Render mount dust effects + if (mountDust && camera) { + mountDust->render(currentCmd, perFrameSet); + } + + // Render charge effect (red haze + dust) + if (chargeEffect && camera) { + chargeEffect->render(currentCmd, perFrameSet); + } + + // TODO Phase 5: WMO rendering + // TODO Phase 5: Character rendering + // TODO Phase 5: M2 rendering + + // Render quest markers (billboards above NPCs) + if (questMarkerRenderer && camera) { + questMarkerRenderer->render(currentCmd, perFrameSet, *camera); + } + + // Minimap display overlay (screen-space quad with composite texture) + if (minimap && minimap->isEnabled() && camera && window) { + glm::vec3 minimapCenter = camera->getPosition(); + if (cameraController && cameraController->isThirdPerson()) + minimapCenter = characterPosition; + minimap->render(currentCmd, *camera, minimapCenter, + window->getWidth(), window->getHeight()); + } + + // TODO Phase 6: Post-process pipeline, shadow mapping, underwater overlay + + auto renderEnd = std::chrono::steady_clock::now(); + lastRenderMs = std::chrono::duration(renderEnd - renderStart).count(); + + // ===== STUBBED GL RENDERING (dead code — reference for Phases 4-6) ===== +#if 0 auto renderStart = std::chrono::steady_clock::now(); lastTerrainRenderMs = 0.0; lastWMORenderMs = 0.0; @@ -2709,11 +3004,11 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { skyParams.skyboxModelId = 0; skyParams.skyboxHasStars = false; // Gradient skybox has no baked stars - skySystem->render(*camera, skyParams); + skySystem->render(currentCmd, perFrameDescSets[vkCtx->getCurrentFrame()], *camera, skyParams); } else { // Fallback: render individual components (backwards compatibility) if (skybox && camera) { - skybox->render(*camera, timeOfDay); + skybox->render(currentCmd, perFrameDescSets[vkCtx->getCurrentFrame()], timeOfDay); } // Get lighting parameters for celestial rendering @@ -2730,15 +3025,15 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { } if (starField && camera) { - starField->render(*camera, timeOfDay, cloudDensity, fogDensity); + starField->render(currentCmd, perFrameDescSets[vkCtx->getCurrentFrame()], timeOfDay, cloudDensity, fogDensity); } if (celestial && camera) { - celestial->render(*camera, timeOfDay, sunDir, sunColor); + celestial->render(currentCmd, perFrameDescSets[vkCtx->getCurrentFrame()], timeOfDay, sunDir, sunColor); } if (clouds && camera) { - clouds->render(*camera, timeOfDay); + clouds->render(currentCmd, perFrameDescSets[vkCtx->getCurrentFrame()], timeOfDay); } if (lensFlare && camera && celestial) { @@ -2843,22 +3138,22 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { // Render weather particles (after terrain/water, before characters) if (weather && camera) { - weather->render(*camera); + weather->render(currentCmd, perFrameDescSets[vkCtx->getCurrentFrame()]); } // Render swim effects (ripples and bubbles) if (swimEffects && camera) { - swimEffects->render(*camera); + swimEffects->render(currentCmd, perFrameDescSets[vkCtx->getCurrentFrame()]); } // Render mount dust effects if (mountDust && camera) { - mountDust->render(*camera); + mountDust->render(currentCmd, perFrameDescSets[vkCtx->getCurrentFrame()]); } // Render charge effect (red haze + dust) if (chargeEffect && camera) { - chargeEffect->render(*camera); + chargeEffect->render(currentCmd, perFrameDescSets[vkCtx->getCurrentFrame()]); } // Compute view/projection once for all sub-renderers @@ -2868,7 +3163,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { // Render WMO buildings first so selection circle can be drawn above WMO depth. if (wmoRenderer && camera) { auto wmoStart = std::chrono::steady_clock::now(); - wmoRenderer->render(*camera, view, projection); + wmoRenderer->render(currentCmd, perFrameDescSets[vkCtx->getCurrentFrame()], *camera); auto wmoEnd = std::chrono::steady_clock::now(); lastWMORenderMs = std::chrono::duration(wmoEnd - wmoStart).count(); } @@ -2879,7 +3174,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { // Render characters (after selection circle) if (characterRenderer && camera) { - characterRenderer->render(*camera, view, projection); + characterRenderer->render(currentCmd, perFrameDescSets[vkCtx->getCurrentFrame()], *camera); } // Render M2 doodads (trees, rocks, etc.) @@ -2890,9 +3185,11 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { m2Renderer->setOnTaxi(cameraController->isOnTaxi()); } auto m2Start = std::chrono::steady_clock::now(); - m2Renderer->render(*camera, view, projection); - m2Renderer->renderSmokeParticles(*camera, view, projection); - m2Renderer->renderM2Particles(view, projection); + uint32_t frame = vkCtx->getCurrentFrame(); + VkDescriptorSet pfSet = perFrameDescSets[frame]; + m2Renderer->render(currentCmd, pfSet, *camera); + m2Renderer->renderSmokeParticles(currentCmd, pfSet); + m2Renderer->renderM2Particles(currentCmd, pfSet); auto m2End = std::chrono::steady_clock::now(); lastM2RenderMs = std::chrono::duration(m2End - m2Start).count(); } @@ -2963,170 +3260,11 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { auto renderEnd = std::chrono::steady_clock::now(); lastRenderMs = std::chrono::duration(renderEnd - renderStart).count(); +#endif // Stubbed GL rendering } -// ────────────────────────────────────────────────────── -// Post-process FBO helpers -// ────────────────────────────────────────────────────── - -void Renderer::initPostProcess(int w, int h) { - fbWidth = w; - fbHeight = h; - constexpr int SAMPLES = 4; - - // --- MSAA FBO (render target) --- - glGenRenderbuffers(1, &sceneColorRBO); - glBindRenderbuffer(GL_RENDERBUFFER, sceneColorRBO); - glRenderbufferStorageMultisample(GL_RENDERBUFFER, SAMPLES, GL_RGBA16F, w, h); - - glGenRenderbuffers(1, &sceneDepthRBO); - glBindRenderbuffer(GL_RENDERBUFFER, sceneDepthRBO); - glRenderbufferStorageMultisample(GL_RENDERBUFFER, SAMPLES, GL_DEPTH_COMPONENT24, w, h); - - glGenFramebuffers(1, &sceneFBO); - glBindFramebuffer(GL_FRAMEBUFFER, sceneFBO); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, sceneColorRBO); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, sceneDepthRBO); - - if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - LOG_ERROR("MSAA scene FBO incomplete!"); - } - - // --- Resolve FBO (non-MSAA, for post-process sampling) --- - glGenTextures(1, &resolveColorTex); - glBindTexture(GL_TEXTURE_2D, resolveColorTex); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, w, h, 0, GL_RGBA, GL_FLOAT, nullptr); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - glGenTextures(1, &resolveDepthTex); - glBindTexture(GL_TEXTURE_2D, resolveDepthTex); - glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, w, h, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - glGenFramebuffers(1, &resolveFBO); - glBindFramebuffer(GL_FRAMEBUFFER, resolveFBO); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, resolveColorTex, 0); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, resolveDepthTex, 0); - - if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - LOG_ERROR("Resolve FBO incomplete!"); - } - glBindFramebuffer(GL_FRAMEBUFFER, 0); - - // --- Fullscreen quad (triangle strip, pos + UV) --- - const float quadVerts[] = { - // pos (x,y) uv (u,v) - -1.0f, -1.0f, 0.0f, 0.0f, - 1.0f, -1.0f, 1.0f, 0.0f, - -1.0f, 1.0f, 0.0f, 1.0f, - 1.0f, 1.0f, 1.0f, 1.0f, - }; - glGenVertexArrays(1, &screenQuadVAO); - glGenBuffers(1, &screenQuadVBO); - glBindVertexArray(screenQuadVAO); - glBindBuffer(GL_ARRAY_BUFFER, screenQuadVBO); - glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW); - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); - glBindVertexArray(0); - - // --- Post-process shader (Reinhard tonemap + gamma 2.2) --- - const char* ppVS = R"( - #version 330 core - layout (location = 0) in vec2 aPos; - layout (location = 1) in vec2 aUV; - out vec2 vUV; - void main() { - vUV = aUV; - gl_Position = vec4(aPos, 0.0, 1.0); - } - )"; - const char* ppFS = R"( - #version 330 core - in vec2 vUV; - uniform sampler2D uScene; - out vec4 FragColor; - void main() { - vec3 color = texture(uScene, vUV).rgb; - // Shoulder tonemap: identity below 0.9, soft rolloff above - vec3 excess = max(color - 0.9, 0.0); - vec3 mapped = min(color, vec3(0.9)) + 0.1 * excess / (excess + 0.1); - FragColor = vec4(mapped, 1.0); - } - )"; - postProcessShader = std::make_unique(); - if (!postProcessShader->loadFromSource(ppVS, ppFS)) { - LOG_ERROR("Failed to compile post-process shader"); - postProcessShader.reset(); - } - - LOG_INFO("Post-process FBO initialized (", w, "x", h, ")"); -} - -void Renderer::resizePostProcess(int w, int h) { - if (w <= 0 || h <= 0) return; - fbWidth = w; - fbHeight = h; - constexpr int SAMPLES = 4; - - // Resize MSAA renderbuffers - glBindRenderbuffer(GL_RENDERBUFFER, sceneColorRBO); - glRenderbufferStorageMultisample(GL_RENDERBUFFER, SAMPLES, GL_RGBA16F, w, h); - glBindRenderbuffer(GL_RENDERBUFFER, sceneDepthRBO); - glRenderbufferStorageMultisample(GL_RENDERBUFFER, SAMPLES, GL_DEPTH_COMPONENT24, w, h); - - // Resize resolve textures - glBindTexture(GL_TEXTURE_2D, resolveColorTex); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, w, h, 0, GL_RGBA, GL_FLOAT, nullptr); - glBindTexture(GL_TEXTURE_2D, resolveDepthTex); - glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, w, h, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr); - - LOG_INFO("Post-process FBO resized (", w, "x", h, ")"); -} - -void Renderer::shutdownPostProcess() { - if (sceneFBO) { - glDeleteFramebuffers(1, &sceneFBO); - sceneFBO = 0; - } - if (sceneColorRBO) { - glDeleteRenderbuffers(1, &sceneColorRBO); - sceneColorRBO = 0; - } - if (sceneDepthRBO) { - glDeleteRenderbuffers(1, &sceneDepthRBO); - sceneDepthRBO = 0; - } - if (resolveFBO) { - glDeleteFramebuffers(1, &resolveFBO); - resolveFBO = 0; - } - if (resolveColorTex) { - glDeleteTextures(1, &resolveColorTex); - resolveColorTex = 0; - } - if (resolveDepthTex) { - glDeleteTextures(1, &resolveDepthTex); - resolveDepthTex = 0; - } - if (screenQuadVAO) { - glDeleteVertexArrays(1, &screenQuadVAO); - screenQuadVAO = 0; - } - if (screenQuadVBO) { - glDeleteBuffers(1, &screenQuadVBO); - screenQuadVBO = 0; - } - postProcessShader.reset(); -} +// initPostProcess(), resizePostProcess(), shutdownPostProcess() removed — +// post-process pipeline is now handled by Vulkan (Phase 6 cleanup). bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std::string& adtPath) { if (!assetManager) { @@ -3139,13 +3277,62 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std:: // Create terrain renderer if not already created if (!terrainRenderer) { terrainRenderer = std::make_unique(); - if (!terrainRenderer->initialize(assetManager)) { + if (!terrainRenderer->initialize(vkCtx, perFrameSetLayout, assetManager)) { LOG_ERROR("Failed to initialize terrain renderer"); terrainRenderer.reset(); return false; } } + // Create water renderer if not already created + if (!waterRenderer) { + waterRenderer = std::make_unique(); + if (!waterRenderer->initialize(vkCtx, perFrameSetLayout)) { + LOG_ERROR("Failed to initialize water renderer"); + waterRenderer.reset(); + } + } + + // Create minimap if not already created + if (!minimap) { + minimap = std::make_unique(); + if (!minimap->initialize(vkCtx, perFrameSetLayout)) { + LOG_ERROR("Failed to initialize minimap"); + minimap.reset(); + } + } + + // Create world map if not already created + if (!worldMap) { + worldMap = std::make_unique(); + if (!worldMap->initialize(vkCtx, assetManager)) { + LOG_ERROR("Failed to initialize world map"); + worldMap.reset(); + } + } + + // Create M2, WMO, and Character renderers + if (!m2Renderer) { + m2Renderer = std::make_unique(); + m2Renderer->initialize(vkCtx, perFrameSetLayout, assetManager); + } + if (!wmoRenderer) { + wmoRenderer = std::make_unique(); + wmoRenderer->initialize(vkCtx, perFrameSetLayout, assetManager); + } + + // Initialize shadow pipelines (Phase 7) + if (wmoRenderer && shadowRenderPass != VK_NULL_HANDLE) { + wmoRenderer->initializeShadow(shadowRenderPass); + } + if (m2Renderer && shadowRenderPass != VK_NULL_HANDLE) { + m2Renderer->initializeShadow(shadowRenderPass); + } + if (!characterRenderer) { + characterRenderer = std::make_unique(); + characterRenderer->initialize(vkCtx, perFrameSetLayout, assetManager); + } + // Create and initialize terrain manager if (!terrainManager) { terrainManager = std::make_unique(); @@ -3219,6 +3406,9 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std:: if (minimap) { minimap->setMapName(mapName); } + if (worldMap) { + worldMap->setMapName(mapName); + } } } @@ -3265,7 +3455,7 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std:: movementSoundManager->initialize(assetManager); } if (questMarkerRenderer) { - questMarkerRenderer->initialize(assetManager); + questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, assetManager); } if (envFlagEnabled("WOWEE_PREWARM_ZONE_MUSIC", false)) { @@ -3391,7 +3581,7 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent movementSoundManager->initialize(cachedAssetManager); } if (questMarkerRenderer && cachedAssetManager) { - questMarkerRenderer->initialize(cachedAssetManager); + questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, cachedAssetManager); } } else { deferredWorldInitPending_ = true; @@ -3442,156 +3632,8 @@ void Renderer::renderHUD() { // Shadow mapping helpers // ────────────────────────────────────────────────────── -void Renderer::initShadowMap() { - // Compile shadow shader - shadowShaderProgram = compileShadowShader(); - if (!shadowShaderProgram) { - LOG_ERROR("Failed to compile shadow shader"); - return; - } - - // Create depth texture - glGenTextures(1, &shadowDepthTex); - glBindTexture(GL_TEXTURE_2D, shadowDepthTex); - glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, - SHADOW_MAP_SIZE, SHADOW_MAP_SIZE, 0, - GL_DEPTH_COMPONENT, GL_FLOAT, nullptr); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); - float borderColor[] = {1.0f, 1.0f, 1.0f, 1.0f}; - glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); - glBindTexture(GL_TEXTURE_2D, 0); - - // Create depth-only FBO - glGenFramebuffers(1, &shadowFBO); - glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowDepthTex, 0); - glDrawBuffer(GL_NONE); - glReadBuffer(GL_NONE); - - if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - LOG_ERROR("Shadow FBO incomplete!"); - } - glBindFramebuffer(GL_FRAMEBUFFER, 0); - - LOG_INFO("Shadow map initialized (", SHADOW_MAP_SIZE, "x", SHADOW_MAP_SIZE, ")"); -} - -uint32_t Renderer::compileShadowShader() { - const char* vertSrc = R"( - #version 330 core - uniform mat4 uLightSpaceMatrix; - uniform mat4 uModel; - layout(location = 0) in vec3 aPos; - layout(location = 2) in vec2 aTexCoord; - layout(location = 3) in vec4 aBoneWeights; - layout(location = 4) in vec4 aBoneIndicesF; - uniform bool uUseBones; - uniform mat4 uBones[200]; - out vec2 vTexCoord; - out vec3 vWorldPos; - void main() { - vec3 pos = aPos; - if (uUseBones) { - ivec4 bi = ivec4(aBoneIndicesF); - mat4 boneTransform = uBones[bi.x] * aBoneWeights.x - + uBones[bi.y] * aBoneWeights.y - + uBones[bi.z] * aBoneWeights.z - + uBones[bi.w] * aBoneWeights.w; - pos = vec3(boneTransform * vec4(aPos, 1.0)); - } - vTexCoord = aTexCoord; - vec4 worldPos = uModel * vec4(pos, 1.0); - vWorldPos = worldPos.xyz; - gl_Position = uLightSpaceMatrix * worldPos; - } - )"; - const char* fragSrc = R"( - #version 330 core - in vec2 vTexCoord; - in vec3 vWorldPos; - uniform bool uUseTexture; - uniform sampler2D uTexture; - uniform bool uAlphaTest; - uniform bool uFoliageSway; - uniform float uWindTime; - uniform float uFoliageMotionDamp; - - void main() { - if (uUseTexture) { - vec2 uv = vTexCoord; - vec2 uv2 = vTexCoord; - if (uFoliageSway && uAlphaTest) { - // Slow, coherent wind-driven sway for foliage shadow cutouts. - float gust = sin(uWindTime * 0.32 + vWorldPos.x * 0.05 + vWorldPos.y * 0.04); - float flutter = sin(uWindTime * 0.55 + vWorldPos.y * 0.09 + vWorldPos.z * 0.18); - float damp = clamp(uFoliageMotionDamp, 0.2, 1.0); - uv += vec2(gust * 0.0040 * damp, flutter * 0.0022 * damp); - - // Second, phase-shifted sample gives smooth position-to-position - // transitions (less on/off popping during motion). - float gust2 = sin(uWindTime * 0.32 + 1.57 + vWorldPos.x * 0.05 + vWorldPos.y * 0.04); - float flutter2 = sin(uWindTime * 0.55 + 2.17 + vWorldPos.y * 0.09 + vWorldPos.z * 0.18); - uv2 += vec2(gust2 * 0.0040 * damp, flutter2 * 0.0022 * damp); - } - // Force base mip for alpha-cutout casters to avoid temporal - // shadow holes from mip-level transitions on thin foliage cards. - vec4 tex = textureLod(uTexture, uv, 0.0); - vec4 tex2 = textureLod(uTexture, uv2, 0.0); - float alphaCut = 0.5; - float alphaVal = (tex.a + tex2.a) * 0.5; - if (uAlphaTest && alphaVal < alphaCut) discard; - } - } - )"; - - GLuint vs = glCreateShader(GL_VERTEX_SHADER); - glShaderSource(vs, 1, &vertSrc, nullptr); - glCompileShader(vs); - GLint success; - glGetShaderiv(vs, GL_COMPILE_STATUS, &success); - if (!success) { - char log[512]; - glGetShaderInfoLog(vs, 512, nullptr, log); - LOG_ERROR("Shadow vertex shader error: ", log); - glDeleteShader(vs); - return 0; - } - - GLuint fs = glCreateShader(GL_FRAGMENT_SHADER); - glShaderSource(fs, 1, &fragSrc, nullptr); - glCompileShader(fs); - glGetShaderiv(fs, GL_COMPILE_STATUS, &success); - if (!success) { - char log[512]; - glGetShaderInfoLog(fs, 512, nullptr, log); - LOG_ERROR("Shadow fragment shader error: ", log); - glDeleteShader(vs); - glDeleteShader(fs); - return 0; - } - - GLuint program = glCreateProgram(); - glAttachShader(program, vs); - glAttachShader(program, fs); - glLinkProgram(program); - glGetProgramiv(program, GL_LINK_STATUS, &success); - if (!success) { - char log[512]; - glGetProgramInfoLog(program, 512, nullptr, log); - LOG_ERROR("Shadow shader link error: ", log); - glDeleteProgram(program); - program = 0; - } - - glDeleteShader(vs); - glDeleteShader(fs); - return program; -} +// initShadowMap() and compileShadowShader() removed — shadow resources now created +// in createPerFrameResources() as part of the Vulkan shadow infrastructure. glm::mat4 Renderer::computeLightSpaceMatrix() { constexpr float kShadowHalfExtent = 180.0f; @@ -3688,134 +3730,74 @@ glm::mat4 Renderer::computeLightSpaceMatrix() { } void Renderer::renderShadowPass() { - constexpr float kShadowHalfExtent = 180.0f; - constexpr float kShadowLightDistance = 280.0f; - constexpr float kShadowNearPlane = 1.0f; - constexpr float kShadowFarPlane = 600.0f; + if (!shadowsEnabled || shadowDepthImage == VK_NULL_HANDLE) return; + if (currentCmd == VK_NULL_HANDLE) return; - // Compute light space matrix + // Compute and store light space matrix; write to per-frame UBO lightSpaceMatrix = computeLightSpaceMatrix(); - - // Bind shadow FBO - glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO); - glViewport(0, 0, SHADOW_MAP_SIZE, SHADOW_MAP_SIZE); - glClear(GL_DEPTH_BUFFER_BIT); - - // Caster-side bias: front-face culling + polygon offset - glEnable(GL_POLYGON_OFFSET_FILL); - glPolygonOffset(2.0f, 4.0f); - glEnable(GL_CULL_FACE); - glCullFace(GL_FRONT); - - // Use shadow shader - glUseProgram(shadowShaderProgram); - GLint lsmLoc = glGetUniformLocation(shadowShaderProgram, "uLightSpaceMatrix"); - glUniformMatrix4fv(lsmLoc, 1, GL_FALSE, &lightSpaceMatrix[0][0]); - GLint useTexLoc = glGetUniformLocation(shadowShaderProgram, "uUseTexture"); - GLint texLoc = glGetUniformLocation(shadowShaderProgram, "uTexture"); - GLint alphaTestLoc = glGetUniformLocation(shadowShaderProgram, "uAlphaTest"); - GLint useBonesLoc = glGetUniformLocation(shadowShaderProgram, "uUseBones"); - GLint foliageSwayLoc = glGetUniformLocation(shadowShaderProgram, "uFoliageSway"); - GLint windTimeLoc = glGetUniformLocation(shadowShaderProgram, "uWindTime"); - GLint foliageDampLoc = glGetUniformLocation(shadowShaderProgram, "uFoliageMotionDamp"); - if (useTexLoc >= 0) glUniform1i(useTexLoc, 0); - if (alphaTestLoc >= 0) glUniform1i(alphaTestLoc, 0); - if (useBonesLoc >= 0) glUniform1i(useBonesLoc, 0); - if (texLoc >= 0) glUniform1i(texLoc, 0); - if (foliageSwayLoc >= 0) glUniform1i(foliageSwayLoc, 0); - if (foliageDampLoc >= 0) glUniform1f(foliageDampLoc, 1.0f); - if (windTimeLoc >= 0) { - const auto now = std::chrono::steady_clock::now(); - static auto prev = now; - static float windPhaseSec = 0.0f; - float dt = std::chrono::duration(now - prev).count(); - prev = now; - dt = std::clamp(dt, 0.0f, 0.1f); - // Match moving and idle foliage evolution speed at 80% of original. - float phaseRate = 0.8f; - windPhaseSec += dt * phaseRate; - glUniform1f(windTimeLoc, windPhaseSec); - if (foliageDampLoc >= 0) { - glUniform1f(foliageDampLoc, 1.0f); - } + uint32_t frame = vkCtx->getCurrentFrame(); + auto* ubo = reinterpret_cast(perFrameUBOMapped[frame]); + if (ubo) { + ubo->lightSpaceMatrix = lightSpaceMatrix; + ubo->shadowParams = glm::vec4(shadowsEnabled ? 1.0f : 0.0f, 0.8f, 0.0f, 0.0f); } - // Render terrain into shadow map (only chunks within shadow frustum) - if (terrainRenderer) { - glm::vec3 shadowCtr = shadowCenterInitialized ? shadowCenter : characterPosition; - terrainRenderer->renderShadow(shadowShaderProgram, shadowCtr, kShadowHalfExtent); - } + // Barrier 1: UNDEFINED → DEPTH_STENCIL_ATTACHMENT_OPTIMAL + VkImageMemoryBarrier b1{}; + b1.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + b1.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + b1.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + b1.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b1.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b1.srcAccessMask = 0; + b1.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + b1.image = shadowDepthImage; + b1.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1}; + vkCmdPipelineBarrier(currentCmd, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT, + 0, 0, nullptr, 0, nullptr, 1, &b1); - // Render WMO into shadow map + // Begin shadow render pass + VkRenderPassBeginInfo rpInfo{}; + rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rpInfo.renderPass = shadowRenderPass; + rpInfo.framebuffer = shadowFramebuffer; + rpInfo.renderArea = {{0, 0}, {SHADOW_MAP_SIZE, SHADOW_MAP_SIZE}}; + VkClearValue clear{}; + clear.depthStencil = {1.0f, 0}; + rpInfo.clearValueCount = 1; + rpInfo.pClearValues = &clear; + vkCmdBeginRenderPass(currentCmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE); + + VkViewport vp{0, 0, static_cast(SHADOW_MAP_SIZE), static_cast(SHADOW_MAP_SIZE), 0.0f, 1.0f}; + vkCmdSetViewport(currentCmd, 0, 1, &vp); + VkRect2D sc{{0, 0}, {SHADOW_MAP_SIZE, SHADOW_MAP_SIZE}}; + vkCmdSetScissor(currentCmd, 0, 1, &sc); + + // Phase 7: render shadow casters if (wmoRenderer) { - // WMO renderShadow takes separate view/proj matrices and a Shader ref. - // We need to decompose our lightSpaceMatrix or use the raw shader program. - // Since WMO::renderShadow sets uModel per instance, we use the shadow shader - // directly by calling renderShadow with the light view/proj split. - // For simplicity, compute the split: - glm::vec3 sunDir = glm::normalize(glm::vec3(-0.3f, -0.7f, -0.6f)); - if (lightingManager) { - const auto& lighting = lightingManager->getLightingParams(); - if (glm::length(lighting.directionalDir) > 0.001f) { - sunDir = glm::normalize(lighting.directionalDir); - } - } - if (sunDir.z > 0.0f) { - sunDir = -sunDir; - } - if (sunDir.z > -0.08f) { - sunDir.z = -0.08f; - sunDir = glm::normalize(sunDir); - } - glm::vec3 center = shadowCenterInitialized ? shadowCenter : characterPosition; - float halfExtent = kShadowHalfExtent; - glm::vec3 up(0.0f, 0.0f, 1.0f); - if (std::abs(glm::dot(sunDir, up)) > 0.99f) up = glm::vec3(0.0f, 1.0f, 0.0f); - glm::mat4 lightView = glm::lookAt(center - sunDir * kShadowLightDistance, center, up); - glm::mat4 lightProj = glm::ortho(-halfExtent, halfExtent, -halfExtent, halfExtent, - kShadowNearPlane, kShadowFarPlane); - - // WMO renderShadow needs a Shader reference — but it only uses setUniform("uModel", ...) - // We'll create a thin wrapper. Actually, WMO's renderShadow takes a Shader& and calls - // shadowShader.setUniform("uModel", ...). We need a Shader object wrapping our program. - // Instead, let's use the lower-level approach: WMO renderShadow uses the shader passed in. - // We need to temporarily wrap our GL program in a Shader object. - Shader shadowShaderWrapper; - shadowShaderWrapper.setProgram(shadowShaderProgram); - wmoRenderer->renderShadow(lightView, lightProj, shadowShaderWrapper); - shadowShaderWrapper.releaseProgram(); // Don't let wrapper delete our program + wmoRenderer->renderShadow(currentCmd, lightSpaceMatrix); } - - // Render M2 doodads into shadow map (only instances within shadow frustum) if (m2Renderer) { - glm::vec3 shadowCtr = shadowCenterInitialized ? shadowCenter : characterPosition; - m2Renderer->renderShadow(shadowShaderProgram, shadowCtr, kShadowHalfExtent); + m2Renderer->renderShadow(currentCmd, lightSpaceMatrix); } - // Render characters into shadow map - if (characterRenderer) { - // Character shadows need less caster bias to avoid "floating" away from feet. - glDisable(GL_POLYGON_OFFSET_FILL); - glCullFace(GL_BACK); - characterRenderer->renderShadow(lightSpaceMatrix); - glCullFace(GL_FRONT); - glEnable(GL_POLYGON_OFFSET_FILL); - glPolygonOffset(2.0f, 4.0f); - } + vkCmdEndRenderPass(currentCmd); - // Restore state - glDisable(GL_POLYGON_OFFSET_FILL); - glCullFace(GL_BACK); - - // Restore main viewport - glViewport(0, 0, fbWidth, fbHeight); - glBindFramebuffer(GL_FRAMEBUFFER, 0); - - // Distribute shadow map to all receivers - if (terrainRenderer) terrainRenderer->setShadowMap(shadowDepthTex, lightSpaceMatrix); - if (wmoRenderer) wmoRenderer->setShadowMap(shadowDepthTex, lightSpaceMatrix); - if (m2Renderer) m2Renderer->setShadowMap(shadowDepthTex, lightSpaceMatrix); - if (characterRenderer) characterRenderer->setShadowMap(shadowDepthTex, lightSpaceMatrix); + // Barrier 2: DEPTH_STENCIL_ATTACHMENT_OPTIMAL → SHADER_READ_ONLY_OPTIMAL + VkImageMemoryBarrier b2{}; + b2.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + b2.oldLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + b2.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + b2.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b2.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b2.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + b2.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + b2.image = shadowDepthImage; + b2.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1}; + vkCmdPipelineBarrier(currentCmd, + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &b2); } } // namespace rendering diff --git a/src/rendering/sky_system.cpp b/src/rendering/sky_system.cpp index 3e96a290..3e419f77 100644 --- a/src/rendering/sky_system.cpp +++ b/src/rendering/sky_system.cpp @@ -5,6 +5,7 @@ #include "rendering/clouds.hpp" #include "rendering/lens_flare.hpp" #include "rendering/camera.hpp" +#include "rendering/vk_context.hpp" #include "core/logger.hpp" namespace wowee { @@ -16,7 +17,7 @@ SkySystem::~SkySystem() { shutdown(); } -bool SkySystem::initialize() { +bool SkySystem::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { if (initialized_) { LOG_WARNING("SkySystem already initialized"); return true; @@ -24,39 +25,38 @@ bool SkySystem::initialize() { LOG_INFO("Initializing sky system"); - // Initialize skybox (authoritative) + // Skybox (Vulkan) skybox_ = std::make_unique(); - if (!skybox_->initialize()) { + if (!skybox_->initialize(ctx, perFrameLayout)) { LOG_ERROR("Failed to initialize skybox"); return false; } - // Initialize celestial bodies (sun + 2 moons) + // Celestial bodies — sun + 2 moons (Vulkan) celestial_ = std::make_unique(); - if (!celestial_->initialize()) { + if (!celestial_->initialize(ctx, perFrameLayout)) { LOG_ERROR("Failed to initialize celestial bodies"); return false; } - // Initialize procedural stars (FALLBACK only) + // Procedural stars — fallback / debug (Vulkan) starField_ = std::make_unique(); - if (!starField_->initialize()) { + if (!starField_->initialize(ctx, perFrameLayout)) { LOG_ERROR("Failed to initialize star field"); return false; } - // Default: disabled (skybox is authoritative) - starField_->setEnabled(false); + starField_->setEnabled(false); // Off by default; skybox is authoritative - // Initialize clouds + // Clouds (Vulkan) clouds_ = std::make_unique(); - if (!clouds_->initialize()) { + if (!clouds_->initialize(ctx, perFrameLayout)) { LOG_ERROR("Failed to initialize clouds"); return false; } - // Initialize lens flare + // Lens flare (Vulkan) lensFlare_ = std::make_unique(); - if (!lensFlare_->initialize()) { + if (!lensFlare_->initialize(ctx, perFrameLayout)) { LOG_ERROR("Failed to initialize lens flare"); return false; } @@ -73,12 +73,12 @@ void SkySystem::shutdown() { LOG_INFO("Shutting down sky system"); - // Shutdown components that have explicit shutdown methods - if (starField_) starField_->shutdown(); - if (celestial_) celestial_->shutdown(); - if (skybox_) skybox_->shutdown(); + if (lensFlare_) lensFlare_->shutdown(); + if (clouds_) clouds_->shutdown(); + if (starField_) starField_->shutdown(); + if (celestial_) celestial_->shutdown(); + if (skybox_) skybox_->shutdown(); - // Reset all (destructors handle cleanup for clouds/lensFlare) lensFlare_.reset(); clouds_.reset(); starField_.reset(); @@ -93,55 +93,55 @@ void SkySystem::update(float deltaTime) { return; } - // Update time-based systems - if (skybox_) skybox_->update(deltaTime); + if (skybox_) skybox_->update(deltaTime); if (celestial_) celestial_->update(deltaTime); if (starField_) starField_->update(deltaTime); + if (clouds_) clouds_->update(deltaTime); } -void SkySystem::render(const Camera& camera, const SkyParams& params) { +void SkySystem::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, + const Camera& camera, const SkyParams& params) { if (!initialized_) { return; } - // Render skybox first (authoritative, includes baked stars) + // --- Skybox (authoritative sky gradient) --- if (skybox_) { - skybox_->render(camera, params.timeOfDay); + skybox_->render(cmd, perFrameSet, params.timeOfDay); } - // Decide whether to render procedural stars + // --- Procedural stars (debug / fallback) --- bool renderProceduralStars = false; if (debugSkyMode_) { - // Debug mode: always show procedural stars renderProceduralStars = true; } else if (proceduralStarsEnabled_) { - // Fallback mode: show only if skybox doesn't have stars renderProceduralStars = !params.skyboxHasStars; } - // Render procedural stars (FALLBACK or DEBUG only) - if (renderProceduralStars && starField_) { - starField_->setEnabled(true); - starField_->render(camera, params.timeOfDay, params.cloudDensity, params.fogDensity); - } else if (starField_) { - starField_->setEnabled(false); + if (starField_) { + starField_->setEnabled(renderProceduralStars); + if (renderProceduralStars) { + const float cloudDensity = params.cloudDensity; + const float fogDensity = params.fogDensity; + starField_->render(cmd, perFrameSet, params.timeOfDay, cloudDensity, fogDensity); + } } - // Render celestial bodies (sun + White Lady + Blue Child) - // Pass gameTime for deterministic moon phases + // --- Celestial bodies (sun + White Lady + Blue Child) --- if (celestial_) { - celestial_->render(camera, params.timeOfDay, ¶ms.directionalDir, ¶ms.sunColor, params.gameTime); + celestial_->render(cmd, perFrameSet, params.timeOfDay, + ¶ms.directionalDir, ¶ms.sunColor, params.gameTime); } - // Render clouds + // --- Clouds --- if (clouds_) { - clouds_->render(camera, params.timeOfDay); + clouds_->render(cmd, perFrameSet, params.timeOfDay); } - // Render lens flare (sun glow effect) + // --- Lens flare --- if (lensFlare_) { glm::vec3 sunPos = getSunPosition(params); - lensFlare_->render(camera, sunPos, params.timeOfDay); + lensFlare_->render(cmd, camera, sunPos, params.timeOfDay); } } @@ -154,27 +154,19 @@ glm::vec3 SkySystem::getSunPosition(const SkyParams& params) const { if (sunDir.z < 0.0f) { sunDir = dir; } - glm::vec3 pos = sunDir * 800.0f; - return pos; + return sunDir * 800.0f; } - void SkySystem::setMoonPhaseCycling(bool enabled) { - if (celestial_) { - celestial_->setMoonPhaseCycling(enabled); - } + if (celestial_) celestial_->setMoonPhaseCycling(enabled); } void SkySystem::setWhiteLadyPhase(float phase) { - if (celestial_) { - celestial_->setMoonPhase(phase); // White Lady is primary moon - } + if (celestial_) celestial_->setMoonPhase(phase); } void SkySystem::setBlueChildPhase(float phase) { - if (celestial_) { - celestial_->setBlueChildPhase(phase); - } + if (celestial_) celestial_->setBlueChildPhase(phase); } float SkySystem::getWhiteLadyPhase() const { diff --git a/src/rendering/skybox.cpp b/src/rendering/skybox.cpp index 4833fb92..368c7cbe 100644 --- a/src/rendering/skybox.cpp +++ b/src/rendering/skybox.cpp @@ -1,8 +1,10 @@ #include "rendering/skybox.hpp" -#include "rendering/shader.hpp" -#include "rendering/camera.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_frame_data.hpp" +#include "rendering/vk_utils.hpp" #include "core/logger.hpp" -#include #include #include #include @@ -16,71 +18,82 @@ Skybox::~Skybox() { shutdown(); } -bool Skybox::initialize() { +bool Skybox::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { LOG_INFO("Initializing skybox"); - // Create sky shader - skyShader = std::make_unique(); + vkCtx = ctx; - // Vertex shader - position-only skybox - const char* vertexShaderSource = R"( - #version 330 core - layout (location = 0) in vec3 aPos; + VkDevice device = vkCtx->getDevice(); - uniform mat4 view; - uniform mat4 projection; - - out vec3 WorldPos; - out float Altitude; - - void main() { - WorldPos = aPos; - - // Calculate altitude (0 at horizon, 1 at zenith) - Altitude = normalize(aPos).z; - - // Remove translation from view matrix (keep rotation only) - mat4 viewNoTranslation = mat4(mat3(view)); - - gl_Position = projection * viewNoTranslation * vec4(aPos, 1.0); - - // Ensure skybox is always at far plane - gl_Position = gl_Position.xyww; - } - )"; - - // Fragment shader - gradient sky with time of day - const char* fragmentShaderSource = R"( - #version 330 core - in vec3 WorldPos; - in float Altitude; - - uniform vec3 horizonColor; - uniform vec3 zenithColor; - uniform float timeOfDay; - - out vec4 FragColor; - - void main() { - // Smooth gradient from horizon to zenith - float t = pow(max(Altitude, 0.0), 0.5); // Curve for more interesting gradient - - vec3 skyColor = mix(horizonColor, zenithColor, t); - - // Add atmospheric scattering effect (more saturated near horizon) - float scattering = 1.0 - t * 0.3; - skyColor *= scattering; - - FragColor = vec4(skyColor, 1.0); - } - )"; - - if (!skyShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) { - LOG_ERROR("Failed to create sky shader"); + // Load SPIR-V shaders + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/skybox.vert.spv")) { + LOG_ERROR("Failed to load skybox vertex shader"); return false; } - // Create sky dome mesh + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/skybox.frag.spv")) { + LOG_ERROR("Failed to load skybox fragment shader"); + return false; + } + + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + // Push constant range: horizonColor (vec4) + zenithColor (vec4) + timeOfDay (float) = 36 bytes + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + pushRange.offset = 0; + pushRange.size = sizeof(glm::vec4) + sizeof(glm::vec4) + sizeof(float); // 36 bytes + + // Create pipeline layout with perFrameLayout (set 0) + push constants + pipelineLayout = createPipelineLayout(device, {perFrameLayout}, {pushRange}); + if (pipelineLayout == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create skybox pipeline layout"); + return false; + } + + // Vertex input: position only (vec3), stride = 3 * sizeof(float) + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 3 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + VkVertexInputAttributeDescription posAttr{}; + posAttr.location = 0; + posAttr.binding = 0; + posAttr.format = VK_FORMAT_R32G32B32_SFLOAT; + posAttr.offset = 0; + + // Dynamic viewport and scissor + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + pipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, {posAttr}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) // depth test on, write off, LEQUAL for far plane + .setColorBlendAttachment(PipelineBuilder::blendDisabled()) + .setLayout(pipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + // Shader modules can be freed after pipeline creation + vertModule.destroy(); + fragModule.destroy(); + + if (pipeline == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create skybox pipeline"); + return false; + } + + // Create sky dome mesh and upload to GPU createSkyDome(); LOG_INFO("Skybox initialized"); @@ -89,41 +102,62 @@ bool Skybox::initialize() { void Skybox::shutdown() { destroySkyDome(); - skyShader.reset(); + + if (vkCtx) { + VkDevice device = vkCtx->getDevice(); + if (pipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(device, pipeline, nullptr); + pipeline = VK_NULL_HANDLE; + } + if (pipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + pipelineLayout = VK_NULL_HANDLE; + } + } + + vkCtx = nullptr; } -void Skybox::render(const Camera& camera, float time) { - if (!renderingEnabled || vao == 0 || !skyShader) { +void Skybox::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, float time) { + if (pipeline == VK_NULL_HANDLE || !renderingEnabled) { return; } - // Render skybox first (before terrain), with depth test set to LEQUAL - glDepthFunc(GL_LEQUAL); + // Push constant data + struct SkyPushConstants { + glm::vec4 horizonColor; + glm::vec4 zenithColor; + float timeOfDay; + }; - skyShader->use(); - - // Set uniforms - glm::mat4 view = camera.getViewMatrix(); - glm::mat4 projection = camera.getProjectionMatrix(); - - skyShader->setUniform("view", view); - skyShader->setUniform("projection", projection); - skyShader->setUniform("timeOfDay", time); - - // Get colors based on time of day + SkyPushConstants push{}; glm::vec3 horizon = getHorizonColor(time); glm::vec3 zenith = getZenithColor(time); + push.horizonColor = glm::vec4(horizon, 1.0f); + push.zenithColor = glm::vec4(zenith, 1.0f); + push.timeOfDay = time; - skyShader->setUniform("horizonColor", horizon); - skyShader->setUniform("zenithColor", zenith); + // Bind pipeline + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); - // Render dome - glBindVertexArray(vao); - glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, nullptr); - glBindVertexArray(0); + // Bind per-frame descriptor set (set 0 — camera UBO) + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, + 0, 1, &perFrameSet, 0, nullptr); - // Restore depth function - glDepthFunc(GL_LESS); + // Push constants + vkCmdPushConstants(cmd, pipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof(push), &push); + + // Bind vertex buffer + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset); + + // Bind index buffer + vkCmdBindIndexBuffer(cmd, indexBuffer, 0, VK_INDEX_TYPE_UINT32); + + // Draw + vkCmdDrawIndexed(cmd, static_cast(indexCount), 1, 0, 0, 0); } void Skybox::update(float deltaTime) { @@ -193,42 +227,39 @@ void Skybox::createSkyDome() { indexCount = static_cast(indices.size()); - // Create OpenGL buffers - glGenVertexArrays(1, &vao); - glGenBuffers(1, &vbo); - glGenBuffers(1, &ebo); + // Upload vertex buffer to GPU via staging + AllocatedBuffer vbuf = uploadBuffer(*vkCtx, + vertices.data(), + vertices.size() * sizeof(float), + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + vertexBuffer = vbuf.buffer; + vertexAlloc = vbuf.allocation; - glBindVertexArray(vao); - - // Upload vertex data - glBindBuffer(GL_ARRAY_BUFFER, vbo); - glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW); - - // Upload index data - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(uint32_t), indices.data(), GL_STATIC_DRAW); - - // Set vertex attributes (position only) - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - - glBindVertexArray(0); + // Upload index buffer to GPU via staging + AllocatedBuffer ibuf = uploadBuffer(*vkCtx, + indices.data(), + indices.size() * sizeof(uint32_t), + VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + indexBuffer = ibuf.buffer; + indexAlloc = ibuf.allocation; LOG_DEBUG("Sky dome created: ", (rings + 1) * (sectors + 1), " vertices, ", indexCount / 3, " triangles"); } void Skybox::destroySkyDome() { - if (vao != 0) { - glDeleteVertexArrays(1, &vao); - vao = 0; + if (!vkCtx) return; + + VmaAllocator allocator = vkCtx->getAllocator(); + + if (vertexBuffer != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, vertexBuffer, vertexAlloc); + vertexBuffer = VK_NULL_HANDLE; + vertexAlloc = VK_NULL_HANDLE; } - if (vbo != 0) { - glDeleteBuffers(1, &vbo); - vbo = 0; - } - if (ebo != 0) { - glDeleteBuffers(1, &ebo); - ebo = 0; + if (indexBuffer != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, indexBuffer, indexAlloc); + indexBuffer = VK_NULL_HANDLE; + indexAlloc = VK_NULL_HANDLE; } } diff --git a/src/rendering/starfield.cpp b/src/rendering/starfield.cpp index 73955993..35371e84 100644 --- a/src/rendering/starfield.cpp +++ b/src/rendering/starfield.cpp @@ -1,11 +1,14 @@ #include "rendering/starfield.hpp" -#include "rendering/shader.hpp" -#include "rendering/camera.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_frame_data.hpp" +#include "rendering/vk_utils.hpp" #include "core/logger.hpp" -#include -#include +#include #include #include +#include namespace wowee { namespace rendering { @@ -16,76 +19,89 @@ StarField::~StarField() { shutdown(); } -bool StarField::initialize() { +bool StarField::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { LOG_INFO("Initializing star field"); - // Create star shader - starShader = std::make_unique(); + vkCtx = ctx; + VkDevice device = vkCtx->getDevice(); - // Vertex shader - simple point rendering - const char* vertexShaderSource = R"( - #version 330 core - layout (location = 0) in vec3 aPos; - layout (location = 1) in float aBrightness; - layout (location = 2) in float aTwinklePhase; - - uniform mat4 view; - uniform mat4 projection; - uniform float time; - uniform float intensity; - - out float Brightness; - - void main() { - // Remove translation from view matrix (stars are infinitely far) - mat4 viewNoTranslation = mat4(mat3(view)); - - gl_Position = projection * viewNoTranslation * vec4(aPos, 1.0); - - // Twinkle effect (subtle brightness variation) - float twinkle = sin(time * 2.0 + aTwinklePhase) * 0.2 + 0.8; // 0.6 to 1.0 - - Brightness = aBrightness * twinkle * intensity; - - // Point size based on brightness - gl_PointSize = 2.0 + aBrightness * 2.0; // 2-4 pixels - } - )"; - - // Fragment shader - star color - const char* fragmentShaderSource = R"( - #version 330 core - in float Brightness; - - out vec4 FragColor; - - void main() { - // Circular point (not square) - vec2 coord = gl_PointCoord - vec2(0.5); - float dist = length(coord); - if (dist > 0.5) { - discard; - } - - // Soften edges - float alpha = smoothstep(0.5, 0.3, dist); - - // Star color (slightly blue-white) - vec3 starColor = vec3(0.9, 0.95, 1.0); - - FragColor = vec4(starColor * Brightness, alpha * Brightness); - } - )"; - - if (!starShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) { - LOG_ERROR("Failed to create star shader"); + // Load SPIR-V shaders + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/starfield.vert.spv")) { + LOG_ERROR("Failed to load starfield vertex shader"); return false; } - // Generate random stars - generateStars(); + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/starfield.frag.spv")) { + LOG_ERROR("Failed to load starfield fragment shader"); + return false; + } - // Create OpenGL buffers + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + // Push constants: float time + float intensity = 8 bytes + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + pushRange.offset = 0; + pushRange.size = sizeof(float) * 2; // time, intensity + + // Pipeline layout: set 0 = per-frame UBO, push constants + pipelineLayout = createPipelineLayout(device, {perFrameLayout}, {pushRange}); + if (pipelineLayout == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create starfield pipeline layout"); + return false; + } + + // Vertex input: binding 0, stride = 5 * sizeof(float) + // location 0: vec3 pos (offset 0) + // location 1: float brightness (offset 12) + // location 2: float twinklePhase (offset 16) + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 5 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + VkVertexInputAttributeDescription posAttr{}; + posAttr.location = 0; + posAttr.binding = 0; + posAttr.format = VK_FORMAT_R32G32B32_SFLOAT; + posAttr.offset = 0; + + VkVertexInputAttributeDescription brightnessAttr{}; + brightnessAttr.location = 1; + brightnessAttr.binding = 0; + brightnessAttr.format = VK_FORMAT_R32_SFLOAT; + brightnessAttr.offset = 3 * sizeof(float); + + VkVertexInputAttributeDescription twinkleAttr{}; + twinkleAttr.location = 2; + twinkleAttr.binding = 0; + twinkleAttr.format = VK_FORMAT_R32_SFLOAT; + twinkleAttr.offset = 4 * sizeof(float); + + pipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, {posAttr, brightnessAttr, twinkleAttr}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) // depth test, no write (stars behind sky) + .setColorBlendAttachment(PipelineBuilder::blendAdditive()) + .setLayout(pipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (pipeline == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create starfield pipeline"); + return false; + } + + // Generate star positions and upload to GPU + generateStars(); createStarBuffers(); LOG_INFO("Star field initialized: ", starCount, " stars"); @@ -94,62 +110,65 @@ bool StarField::initialize() { void StarField::shutdown() { destroyStarBuffers(); - starShader.reset(); + + if (vkCtx) { + VkDevice device = vkCtx->getDevice(); + if (pipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(device, pipeline, nullptr); + pipeline = VK_NULL_HANDLE; + } + if (pipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + pipelineLayout = VK_NULL_HANDLE; + } + } + + vkCtx = nullptr; stars.clear(); } -void StarField::render(const Camera& camera, float timeOfDay, - float cloudDensity, float fogDensity) { - if (!renderingEnabled || vao == 0 || !starShader || stars.empty()) { +void StarField::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, + float timeOfDay, float cloudDensity, float fogDensity) { + if (!renderingEnabled || pipeline == VK_NULL_HANDLE || vertexBuffer == VK_NULL_HANDLE + || stars.empty()) { return; } - // Get star intensity based on time of day + // Compute intensity from time of day then attenuate for clouds/fog float intensity = getStarIntensity(timeOfDay); - - // Reduce intensity based on cloud density and fog (more clouds/fog = fewer visible stars) intensity *= (1.0f - glm::clamp(cloudDensity * 0.7f, 0.0f, 1.0f)); intensity *= (1.0f - glm::clamp(fogDensity * 0.3f, 0.0f, 1.0f)); - // Don't render if stars would be invisible if (intensity <= 0.01f) { return; } - // Enable blending for star glow - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + // Push constants: time and intensity + struct StarPushConstants { + float time; + float intensity; + }; + StarPushConstants push{twinkleTime, intensity}; - // Enable point sprites - glEnable(GL_PROGRAM_POINT_SIZE); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); - // Disable depth writing (stars are background) - glDepthMask(GL_FALSE); + // Bind per-frame descriptor set (set 0 — camera UBO with view/projection) + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, + 0, 1, &perFrameSet, 0, nullptr); - starShader->use(); + vkCmdPushConstants(cmd, pipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof(push), &push); - // Set uniforms - glm::mat4 view = camera.getViewMatrix(); - glm::mat4 projection = camera.getProjectionMatrix(); + // Bind vertex buffer + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset); - starShader->setUniform("view", view); - starShader->setUniform("projection", projection); - starShader->setUniform("time", twinkleTime); - starShader->setUniform("intensity", intensity); - - // Render stars as points - glBindVertexArray(vao); - glDrawArrays(GL_POINTS, 0, starCount); - glBindVertexArray(0); - - // Restore state - glDepthMask(GL_TRUE); - glDisable(GL_PROGRAM_POINT_SIZE); - glDisable(GL_BLEND); + // Draw all stars as individual points + vkCmdDraw(cmd, static_cast(starCount), 1, 0, 0); } void StarField::update(float deltaTime) { - // Update twinkle animation twinkleTime += deltaTime; } @@ -157,30 +176,27 @@ void StarField::generateStars() { stars.clear(); stars.reserve(starCount); - // Random number generator std::random_device rd; std::mt19937 gen(rd()); - std::uniform_real_distribution phiDist(0.0f, M_PI / 2.0f); // 0 to 90 degrees (hemisphere) - std::uniform_real_distribution thetaDist(0.0f, 2.0f * M_PI); // 0 to 360 degrees - std::uniform_real_distribution brightnessDist(0.3f, 1.0f); // Varying brightness - std::uniform_real_distribution twinkleDist(0.0f, 2.0f * M_PI); // Random twinkle phase + std::uniform_real_distribution phiDist(0.0f, M_PI / 2.0f); // 0–90° (upper hemisphere) + std::uniform_real_distribution thetaDist(0.0f, 2.0f * M_PI); // 0–360° + std::uniform_real_distribution brightnessDist(0.3f, 1.0f); + std::uniform_real_distribution twinkleDist(0.0f, 2.0f * M_PI); const float radius = 900.0f; // Slightly larger than skybox for (int i = 0; i < starCount; i++) { Star star; - // Spherical coordinates (hemisphere) - float phi = phiDist(gen); // Elevation angle + float phi = phiDist(gen); // Elevation angle float theta = thetaDist(gen); // Azimuth angle - // Convert to Cartesian coordinates float x = radius * std::sin(phi) * std::cos(theta); float y = radius * std::sin(phi) * std::sin(theta); float z = radius * std::cos(phi); - star.position = glm::vec3(x, y, z); - star.brightness = brightnessDist(gen); + star.position = glm::vec3(x, y, z); + star.brightness = brightnessDist(gen); star.twinklePhase = twinkleDist(gen); stars.push_back(star); @@ -190,9 +206,9 @@ void StarField::generateStars() { } void StarField::createStarBuffers() { - // Prepare vertex data (position, brightness, twinkle phase) + // Interleaved vertex data: pos.x, pos.y, pos.z, brightness, twinklePhase std::vector vertexData; - vertexData.reserve(stars.size() * 5); // 3 pos + 1 brightness + 1 phase + vertexData.reserve(stars.size() * 5); for (const auto& star : stars) { vertexData.push_back(star.position.x); @@ -202,57 +218,36 @@ void StarField::createStarBuffers() { vertexData.push_back(star.twinklePhase); } - // Create OpenGL buffers - glGenVertexArrays(1, &vao); - glGenBuffers(1, &vbo); + VkDeviceSize bufferSize = vertexData.size() * sizeof(float); - glBindVertexArray(vao); + // Upload via staging buffer to GPU-local memory + AllocatedBuffer gpuBuf = uploadBuffer(*vkCtx, vertexData.data(), bufferSize, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); - // Upload vertex data - glBindBuffer(GL_ARRAY_BUFFER, vbo); - glBufferData(GL_ARRAY_BUFFER, vertexData.size() * sizeof(float), vertexData.data(), GL_STATIC_DRAW); - - // Set vertex attributes - // Position - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - - // Brightness - glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(1); - - // Twinkle phase - glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(4 * sizeof(float))); - glEnableVertexAttribArray(2); - - glBindVertexArray(0); + vertexBuffer = gpuBuf.buffer; + vertexAlloc = gpuBuf.allocation; } void StarField::destroyStarBuffers() { - if (vao != 0) { - glDeleteVertexArrays(1, &vao); - vao = 0; - } - if (vbo != 0) { - glDeleteBuffers(1, &vbo); - vbo = 0; + if (vkCtx && vertexBuffer != VK_NULL_HANDLE) { + vmaDestroyBuffer(vkCtx->getAllocator(), vertexBuffer, vertexAlloc); + vertexBuffer = VK_NULL_HANDLE; + vertexAlloc = VK_NULL_HANDLE; } } float StarField::getStarIntensity(float timeOfDay) const { - // Stars visible at night (fade in/out at dusk/dawn) - - // Full night: 20:00-4:00 + // Full night: 20:00–4:00 if (timeOfDay >= 20.0f || timeOfDay < 4.0f) { return 1.0f; } - // Fade in at dusk: 18:00-20:00 + // Fade in at dusk: 18:00–20:00 else if (timeOfDay >= 18.0f && timeOfDay < 20.0f) { - return (timeOfDay - 18.0f) / 2.0f; // 0 to 1 over 2 hours + return (timeOfDay - 18.0f) / 2.0f; // 0 → 1 over 2 hours } - // Fade out at dawn: 4:00-6:00 + // Fade out at dawn: 4:00–6:00 else if (timeOfDay >= 4.0f && timeOfDay < 6.0f) { - return 1.0f - (timeOfDay - 4.0f) / 2.0f; // 1 to 0 over 2 hours + return 1.0f - (timeOfDay - 4.0f) / 2.0f; // 1 → 0 over 2 hours } // Daytime: no stars else { diff --git a/src/rendering/swim_effects.cpp b/src/rendering/swim_effects.cpp index 46ef95d6..bfcfd2ce 100644 --- a/src/rendering/swim_effects.cpp +++ b/src/rendering/swim_effects.cpp @@ -2,11 +2,16 @@ #include "rendering/camera.hpp" #include "rendering/camera_controller.hpp" #include "rendering/water_renderer.hpp" -#include "rendering/shader.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_frame_data.hpp" +#include "rendering/vk_utils.hpp" #include "core/logger.hpp" #include #include #include +#include namespace wowee { namespace rendering { @@ -25,123 +30,152 @@ static float randFloat(float lo, float hi) { SwimEffects::SwimEffects() = default; SwimEffects::~SwimEffects() { shutdown(); } -bool SwimEffects::initialize() { +bool SwimEffects::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { LOG_INFO("Initializing swim effects"); - // --- Ripple/splash shader (small white spray droplets) --- - rippleShader = std::make_unique(); + vkCtx = ctx; + VkDevice device = vkCtx->getDevice(); - const char* rippleVS = R"( - #version 330 core - layout (location = 0) in vec3 aPos; - layout (location = 1) in float aSize; - layout (location = 2) in float aAlpha; + // ---- Vertex input: pos(vec3) + size(float) + alpha(float) = 5 floats, stride = 20 bytes ---- + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 5 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; - uniform mat4 uView; - uniform mat4 uProjection; + std::vector attrs(3); + // location 0: vec3 position + attrs[0].location = 0; + attrs[0].binding = 0; + attrs[0].format = VK_FORMAT_R32G32B32_SFLOAT; + attrs[0].offset = 0; + // location 1: float size + attrs[1].location = 1; + attrs[1].binding = 0; + attrs[1].format = VK_FORMAT_R32_SFLOAT; + attrs[1].offset = 3 * sizeof(float); + // location 2: float alpha + attrs[2].location = 2; + attrs[2].binding = 0; + attrs[2].format = VK_FORMAT_R32_SFLOAT; + attrs[2].offset = 4 * sizeof(float); - out float vAlpha; + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; - void main() { - gl_Position = uProjection * uView * vec4(aPos, 1.0); - gl_PointSize = aSize; - vAlpha = aAlpha; + // ---- Ripple pipeline ---- + { + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/swim_ripple.vert.spv")) { + LOG_ERROR("Failed to load swim_ripple vertex shader"); + return false; } - )"; - - const char* rippleFS = R"( - #version 330 core - in float vAlpha; - out vec4 FragColor; - - void main() { - vec2 coord = gl_PointCoord - vec2(0.5); - float dist = length(coord); - if (dist > 0.5) discard; - // Soft circular splash droplet - float alpha = smoothstep(0.5, 0.2, dist) * vAlpha; - FragColor = vec4(0.85, 0.92, 1.0, alpha); + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/swim_ripple.frag.spv")) { + LOG_ERROR("Failed to load swim_ripple fragment shader"); + return false; } - )"; - if (!rippleShader->loadFromSource(rippleVS, rippleFS)) { - LOG_ERROR("Failed to create ripple shader"); - return false; + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + ripplePipelineLayout = createPipelineLayout(device, {perFrameLayout}, {}); + if (ripplePipelineLayout == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create ripple pipeline layout"); + return false; + } + + ripplePipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setLayout(ripplePipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (ripplePipeline == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create ripple pipeline"); + return false; + } } - // --- Bubble shader --- - bubbleShader = std::make_unique(); - - const char* bubbleVS = R"( - #version 330 core - layout (location = 0) in vec3 aPos; - layout (location = 1) in float aSize; - layout (location = 2) in float aAlpha; - - uniform mat4 uView; - uniform mat4 uProjection; - - out float vAlpha; - - void main() { - gl_Position = uProjection * uView * vec4(aPos, 1.0); - gl_PointSize = aSize; - vAlpha = aAlpha; + // ---- Bubble pipeline ---- + { + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/swim_bubble.vert.spv")) { + LOG_ERROR("Failed to load swim_bubble vertex shader"); + return false; } - )"; - - const char* bubbleFS = R"( - #version 330 core - in float vAlpha; - out vec4 FragColor; - - void main() { - vec2 coord = gl_PointCoord - vec2(0.5); - float dist = length(coord); - if (dist > 0.5) discard; - // Bubble with highlight - float edge = smoothstep(0.5, 0.35, dist); - float hollow = smoothstep(0.25, 0.35, dist); - float bubble = edge * hollow; - // Specular highlight near top-left - float highlight = smoothstep(0.3, 0.0, length(coord - vec2(-0.12, -0.12))); - float alpha = (bubble * 0.6 + highlight * 0.4) * vAlpha; - vec3 color = vec3(0.7, 0.85, 1.0); - FragColor = vec4(color, alpha); + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/swim_bubble.frag.spv")) { + LOG_ERROR("Failed to load swim_bubble fragment shader"); + return false; } - )"; - if (!bubbleShader->loadFromSource(bubbleVS, bubbleFS)) { - LOG_ERROR("Failed to create bubble shader"); - return false; + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); + + bubblePipelineLayout = createPipelineLayout(device, {perFrameLayout}, {}); + if (bubblePipelineLayout == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create bubble pipeline layout"); + return false; + } + + bubblePipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setLayout(bubblePipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (bubblePipeline == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create bubble pipeline"); + return false; + } } - // --- Ripple VAO/VBO --- - glGenVertexArrays(1, &rippleVAO); - glGenBuffers(1, &rippleVBO); - glBindVertexArray(rippleVAO); - glBindBuffer(GL_ARRAY_BUFFER, rippleVBO); - // layout: vec3 pos, float size, float alpha (stride = 5 floats) - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(1); - glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(4 * sizeof(float))); - glEnableVertexAttribArray(2); - glBindVertexArray(0); + // ---- Create dynamic mapped vertex buffers ---- + rippleDynamicVBSize = MAX_RIPPLE_PARTICLES * 5 * sizeof(float); + { + AllocatedBuffer buf = createBuffer(vkCtx->getAllocator(), rippleDynamicVBSize, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU); + rippleDynamicVB = buf.buffer; + rippleDynamicVBAlloc = buf.allocation; + rippleDynamicVBAllocInfo = buf.info; + if (rippleDynamicVB == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create ripple dynamic vertex buffer"); + return false; + } + } - // --- Bubble VAO/VBO --- - glGenVertexArrays(1, &bubbleVAO); - glGenBuffers(1, &bubbleVBO); - glBindVertexArray(bubbleVAO); - glBindBuffer(GL_ARRAY_BUFFER, bubbleVBO); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(1); - glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(4 * sizeof(float))); - glEnableVertexAttribArray(2); - glBindVertexArray(0); + bubbleDynamicVBSize = MAX_BUBBLE_PARTICLES * 5 * sizeof(float); + { + AllocatedBuffer buf = createBuffer(vkCtx->getAllocator(), bubbleDynamicVBSize, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU); + bubbleDynamicVB = buf.buffer; + bubbleDynamicVBAlloc = buf.allocation; + bubbleDynamicVBAllocInfo = buf.info; + if (bubbleDynamicVB == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create bubble dynamic vertex buffer"); + return false; + } + } ripples.reserve(MAX_RIPPLE_PARTICLES); bubbles.reserve(MAX_BUBBLE_PARTICLES); @@ -153,12 +187,40 @@ bool SwimEffects::initialize() { } void SwimEffects::shutdown() { - if (rippleVAO) { glDeleteVertexArrays(1, &rippleVAO); rippleVAO = 0; } - if (rippleVBO) { glDeleteBuffers(1, &rippleVBO); rippleVBO = 0; } - if (bubbleVAO) { glDeleteVertexArrays(1, &bubbleVAO); bubbleVAO = 0; } - if (bubbleVBO) { glDeleteBuffers(1, &bubbleVBO); bubbleVBO = 0; } - rippleShader.reset(); - bubbleShader.reset(); + if (vkCtx) { + VkDevice device = vkCtx->getDevice(); + VmaAllocator allocator = vkCtx->getAllocator(); + + if (ripplePipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(device, ripplePipeline, nullptr); + ripplePipeline = VK_NULL_HANDLE; + } + if (ripplePipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, ripplePipelineLayout, nullptr); + ripplePipelineLayout = VK_NULL_HANDLE; + } + if (rippleDynamicVB != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, rippleDynamicVB, rippleDynamicVBAlloc); + rippleDynamicVB = VK_NULL_HANDLE; + rippleDynamicVBAlloc = VK_NULL_HANDLE; + } + + if (bubblePipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(device, bubblePipeline, nullptr); + bubblePipeline = VK_NULL_HANDLE; + } + if (bubblePipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, bubblePipelineLayout, nullptr); + bubblePipelineLayout = VK_NULL_HANDLE; + } + if (bubbleDynamicVB != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, bubbleDynamicVB, bubbleDynamicVBAlloc); + bubbleDynamicVB = VK_NULL_HANDLE; + bubbleDynamicVBAlloc = VK_NULL_HANDLE; + } + } + + vkCtx = nullptr; ripples.clear(); bubbles.clear(); } @@ -328,52 +390,38 @@ void SwimEffects::update(const Camera& camera, const CameraController& cc, } } -void SwimEffects::render(const Camera& camera) { +void SwimEffects::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) { if (rippleVertexData.empty() && bubbleVertexData.empty()) return; - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glDepthMask(GL_FALSE); - glEnable(GL_PROGRAM_POINT_SIZE); - - glm::mat4 view = camera.getViewMatrix(); - glm::mat4 projection = camera.getProjectionMatrix(); + VkDeviceSize offset = 0; // --- Render ripples (splash droplets above water surface) --- - if (!rippleVertexData.empty() && rippleShader) { - rippleShader->use(); - rippleShader->setUniform("uView", view); - rippleShader->setUniform("uProjection", projection); + if (!rippleVertexData.empty() && ripplePipeline != VK_NULL_HANDLE) { + VkDeviceSize uploadSize = rippleVertexData.size() * sizeof(float); + if (rippleDynamicVBAllocInfo.pMappedData) { + std::memcpy(rippleDynamicVBAllocInfo.pMappedData, rippleVertexData.data(), uploadSize); + } - glBindVertexArray(rippleVAO); - glBindBuffer(GL_ARRAY_BUFFER, rippleVBO); - glBufferData(GL_ARRAY_BUFFER, - rippleVertexData.size() * sizeof(float), - rippleVertexData.data(), - GL_DYNAMIC_DRAW); - glDrawArrays(GL_POINTS, 0, static_cast(rippleVertexData.size() / 5)); - glBindVertexArray(0); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, ripplePipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, ripplePipelineLayout, + 0, 1, &perFrameSet, 0, nullptr); + vkCmdBindVertexBuffers(cmd, 0, 1, &rippleDynamicVB, &offset); + vkCmdDraw(cmd, static_cast(rippleVertexData.size() / 5), 1, 0, 0); } // --- Render bubbles --- - if (!bubbleVertexData.empty() && bubbleShader) { - bubbleShader->use(); - bubbleShader->setUniform("uView", view); - bubbleShader->setUniform("uProjection", projection); + if (!bubbleVertexData.empty() && bubblePipeline != VK_NULL_HANDLE) { + VkDeviceSize uploadSize = bubbleVertexData.size() * sizeof(float); + if (bubbleDynamicVBAllocInfo.pMappedData) { + std::memcpy(bubbleDynamicVBAllocInfo.pMappedData, bubbleVertexData.data(), uploadSize); + } - glBindVertexArray(bubbleVAO); - glBindBuffer(GL_ARRAY_BUFFER, bubbleVBO); - glBufferData(GL_ARRAY_BUFFER, - bubbleVertexData.size() * sizeof(float), - bubbleVertexData.data(), - GL_DYNAMIC_DRAW); - glDrawArrays(GL_POINTS, 0, static_cast(bubbleVertexData.size() / 5)); - glBindVertexArray(0); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, bubblePipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, bubblePipelineLayout, + 0, 1, &perFrameSet, 0, nullptr); + vkCmdBindVertexBuffers(cmd, 0, 1, &bubbleDynamicVB, &offset); + vkCmdDraw(cmd, static_cast(bubbleVertexData.size() / 5), 1, 0, 0); } - - glDisable(GL_BLEND); - glDepthMask(GL_TRUE); - glDisable(GL_PROGRAM_POINT_SIZE); } } // namespace rendering diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index 91f04b54..d50e2c1e 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -726,7 +726,7 @@ void TerrainManager::finalizeTile(const std::shared_ptr& pending) { if (m2Renderer && assetManager) { // Always pass the latest asset manager. initialize() is idempotent and updates // the pointer even when the renderer was initialized earlier without assets. - m2Renderer->initialize(assetManager); + m2Renderer->initialize(nullptr, VK_NULL_HANDLE, assetManager); // Upload M2 models immediately (batching was causing hangs) // The 5ms time budget in processReadyTiles() limits the spike @@ -768,7 +768,7 @@ void TerrainManager::finalizeTile(const std::shared_ptr& pending) { // Upload WMO models to GPU and create instances if (wmoRenderer && assetManager) { // WMORenderer may be initialized before assets are ready; always re-pass assets. - wmoRenderer->initialize(assetManager); + wmoRenderer->initialize(nullptr, VK_NULL_HANDLE, assetManager); int loadedWMOs = 0; int loadedLiquids = 0; diff --git a/src/rendering/terrain_renderer.cpp b/src/rendering/terrain_renderer.cpp index 4f2814bb..bdb319cb 100644 --- a/src/rendering/terrain_renderer.cpp +++ b/src/rendering/terrain_renderer.cpp @@ -1,94 +1,221 @@ #include "rendering/terrain_renderer.hpp" -#include "rendering/texture.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_texture.hpp" +#include "rendering/vk_buffer.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_utils.hpp" +#include "rendering/vk_frame_data.hpp" #include "rendering/frustum.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/blp_loader.hpp" #include "core/logger.hpp" #include #include -#include #include #include #include +#include namespace wowee { namespace rendering { -TerrainRenderer::TerrainRenderer() { -} +// Matches set 1 binding 7 in terrain.frag.glsl +struct TerrainParamsUBO { + int32_t layerCount; + int32_t hasLayer1; + int32_t hasLayer2; + int32_t hasLayer3; +}; + +TerrainRenderer::TerrainRenderer() = default; TerrainRenderer::~TerrainRenderer() { shutdown(); } -bool TerrainRenderer::initialize(pipeline::AssetManager* assets) { +bool TerrainRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, + pipeline::AssetManager* assets) { + vkCtx = ctx; assetManager = assets; - if (!assetManager) { - LOG_ERROR("Asset manager is null"); + if (!vkCtx || !assetManager) { + LOG_ERROR("TerrainRenderer: null context or asset manager"); return false; } - LOG_INFO("Initializing terrain renderer"); + LOG_INFO("Initializing terrain renderer (Vulkan)"); + VkDevice device = vkCtx->getDevice(); - // Load terrain shader - shader = std::make_unique(); - if (!shader->loadFromFile("assets/shaders/terrain.vert", "assets/shaders/terrain.frag")) { - LOG_ERROR("Failed to load terrain shader"); + // --- Create material descriptor set layout (set 1) --- + // bindings 0-6: combined image samplers (base + 3 layer + 3 alpha) + // binding 7: uniform buffer (TerrainParams) + std::vector materialBindings(8); + for (uint32_t i = 0; i < 7; i++) { + materialBindings[i] = {}; + materialBindings[i].binding = i; + materialBindings[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + materialBindings[i].descriptorCount = 1; + materialBindings[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + } + materialBindings[7] = {}; + materialBindings[7].binding = 7; + materialBindings[7].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + materialBindings[7].descriptorCount = 1; + materialBindings[7].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + materialSetLayout = createDescriptorSetLayout(device, materialBindings); + if (!materialSetLayout) { + LOG_ERROR("TerrainRenderer: failed to create material set layout"); return false; } - // Create default white texture for fallback + // --- Create descriptor pool --- + VkDescriptorPoolSize poolSizes[] = { + { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, MAX_MATERIAL_SETS * 7 }, + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, MAX_MATERIAL_SETS }, + }; + + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.maxSets = MAX_MATERIAL_SETS; + poolInfo.poolSizeCount = 2; + poolInfo.pPoolSizes = poolSizes; + + if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &materialDescPool) != VK_SUCCESS) { + LOG_ERROR("TerrainRenderer: failed to create descriptor pool"); + return false; + } + + // --- Create pipeline layout --- + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + pushRange.offset = 0; + pushRange.size = sizeof(GPUPushConstants); + + std::vector setLayouts = { perFrameLayout, materialSetLayout }; + pipelineLayout = createPipelineLayout(device, setLayouts, { pushRange }); + if (!pipelineLayout) { + LOG_ERROR("TerrainRenderer: failed to create pipeline layout"); + return false; + } + + // --- Load shaders --- + VkShaderModule vertShader, fragShader; + if (!vertShader.loadFromFile(device, "assets/shaders/terrain.vert.spv")) { + LOG_ERROR("TerrainRenderer: failed to load vertex shader"); + return false; + } + if (!fragShader.loadFromFile(device, "assets/shaders/terrain.frag.spv")) { + LOG_ERROR("TerrainRenderer: failed to load fragment shader"); + return false; + } + + // --- Vertex input --- + VkVertexInputBindingDescription vertexBinding{}; + vertexBinding.binding = 0; + vertexBinding.stride = sizeof(pipeline::TerrainVertex); + vertexBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector vertexAttribs(4); + vertexAttribs[0] = { 0, 0, VK_FORMAT_R32G32B32_SFLOAT, + static_cast(offsetof(pipeline::TerrainVertex, position)) }; + vertexAttribs[1] = { 1, 0, VK_FORMAT_R32G32B32_SFLOAT, + static_cast(offsetof(pipeline::TerrainVertex, normal)) }; + vertexAttribs[2] = { 2, 0, VK_FORMAT_R32G32_SFLOAT, + static_cast(offsetof(pipeline::TerrainVertex, texCoord)) }; + vertexAttribs[3] = { 3, 0, VK_FORMAT_R32G32_SFLOAT, + static_cast(offsetof(pipeline::TerrainVertex, layerUV)) }; + + // --- Build fill pipeline --- + VkRenderPass mainPass = vkCtx->getImGuiRenderPass(); + + pipeline = PipelineBuilder() + .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({ vertexBinding }, vertexAttribs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(PipelineBuilder::blendDisabled()) + .setLayout(pipelineLayout) + .setRenderPass(mainPass) + .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) + .build(device); + + if (!pipeline) { + LOG_ERROR("TerrainRenderer: failed to create fill pipeline"); + vertShader.destroy(); + fragShader.destroy(); + return false; + } + + // --- Build wireframe pipeline --- + wireframePipeline = PipelineBuilder() + .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({ vertexBinding }, vertexAttribs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_LINE, VK_CULL_MODE_NONE) + .setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(PipelineBuilder::blendDisabled()) + .setLayout(pipelineLayout) + .setRenderPass(mainPass) + .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) + .build(device); + + if (!wireframePipeline) { + LOG_WARNING("TerrainRenderer: wireframe pipeline not available"); + } + + vertShader.destroy(); + fragShader.destroy(); + + // --- Create fallback textures --- + whiteTexture = std::make_unique(); uint8_t whitePixel[4] = {255, 255, 255, 255}; - glGenTextures(1, &whiteTexture); - glBindTexture(GL_TEXTURE_2D, whiteTexture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, whitePixel); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glBindTexture(GL_TEXTURE_2D, 0); + whiteTexture->upload(*vkCtx, whitePixel, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false); + whiteTexture->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR, + VK_SAMPLER_ADDRESS_MODE_REPEAT); - // Create default opaque alpha texture for terrain layer masks + opaqueAlphaTexture = std::make_unique(); uint8_t opaqueAlpha = 255; - glGenTextures(1, &opaqueAlphaTexture); - glBindTexture(GL_TEXTURE_2D, opaqueAlphaTexture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, 1, 1, 0, GL_RED, GL_UNSIGNED_BYTE, &opaqueAlpha); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glBindTexture(GL_TEXTURE_2D, 0); + opaqueAlphaTexture->upload(*vkCtx, &opaqueAlpha, 1, 1, VK_FORMAT_R8_UNORM, false); + opaqueAlphaTexture->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR, + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE); - LOG_INFO("Terrain renderer initialized"); + LOG_INFO("Terrain renderer initialized (Vulkan)"); return true; } void TerrainRenderer::shutdown() { LOG_INFO("Shutting down terrain renderer"); + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + VmaAllocator allocator = vkCtx->getAllocator(); + + vkDeviceWaitIdle(device); + clear(); - // Delete white texture - if (whiteTexture) { - glDeleteTextures(1, &whiteTexture); - whiteTexture = 0; - } - if (opaqueAlphaTexture) { - glDeleteTextures(1, &opaqueAlphaTexture); - opaqueAlphaTexture = 0; - } - - // Delete cached textures for (auto& [path, entry] : textureCache) { - GLuint texId = entry.id; - if (texId != 0 && texId != whiteTexture) { - glDeleteTextures(1, &texId); - } + if (entry.texture) entry.texture->destroy(device, allocator); } textureCache.clear(); textureCacheBytes_ = 0; textureCacheCounter_ = 0; - shader.reset(); + if (whiteTexture) { whiteTexture->destroy(device, allocator); whiteTexture.reset(); } + if (opaqueAlphaTexture) { opaqueAlphaTexture->destroy(device, allocator); opaqueAlphaTexture.reset(); } + + if (pipeline) { vkDestroyPipeline(device, pipeline, nullptr); pipeline = VK_NULL_HANDLE; } + if (wireframePipeline) { vkDestroyPipeline(device, wireframePipeline, nullptr); wireframePipeline = VK_NULL_HANDLE; } + if (pipelineLayout) { vkDestroyPipelineLayout(device, pipelineLayout, nullptr); pipelineLayout = VK_NULL_HANDLE; } + if (materialDescPool) { vkDestroyDescriptorPool(device, materialDescPool, nullptr); materialDescPool = VK_NULL_HANDLE; } + if (materialSetLayout) { vkDestroyDescriptorSetLayout(device, materialSetLayout, nullptr); materialSetLayout = VK_NULL_HANDLE; } + + vkCtx = nullptr; } bool TerrainRenderer::loadTerrain(const pipeline::TerrainMesh& mesh, @@ -96,61 +223,82 @@ bool TerrainRenderer::loadTerrain(const pipeline::TerrainMesh& mesh, int tileX, int tileY) { LOG_DEBUG("Loading terrain mesh: ", mesh.validChunkCount, " chunks"); - // Upload each chunk to GPU for (int y = 0; y < 16; y++) { for (int x = 0; x < 16; x++) { const auto& chunk = mesh.getChunk(x, y); - - if (!chunk.isValid()) { - continue; - } + if (!chunk.isValid()) continue; TerrainChunkGPU gpuChunk = uploadChunk(chunk); - if (!gpuChunk.isValid()) { LOG_WARNING("Failed to upload chunk [", x, ",", y, "]"); continue; } - // Calculate bounding sphere for frustum culling calculateBoundingSphere(gpuChunk, chunk); // Load textures for this chunk if (!chunk.layers.empty()) { - // Base layer (always present) uint32_t baseTexId = chunk.layers[0].textureId; if (baseTexId < texturePaths.size()) { gpuChunk.baseTexture = loadTexture(texturePaths[baseTexId]); } else { - gpuChunk.baseTexture = whiteTexture; + gpuChunk.baseTexture = whiteTexture.get(); } - // Additional layers (with alpha blending) for (size_t i = 1; i < chunk.layers.size() && i < 4; i++) { const auto& layer = chunk.layers[i]; + int li = static_cast(i) - 1; - // Load layer texture - GLuint layerTex = whiteTexture; + VkTexture* layerTex = whiteTexture.get(); if (layer.textureId < texturePaths.size()) { layerTex = loadTexture(texturePaths[layer.textureId]); } - gpuChunk.layerTextures.push_back(layerTex); + gpuChunk.layerTextures[li] = layerTex; - // Create alpha texture - GLuint alphaTex = opaqueAlphaTexture; + VkTexture* alphaTex = opaqueAlphaTexture.get(); if (!layer.alphaData.empty()) { alphaTex = createAlphaTexture(layer.alphaData); } - gpuChunk.alphaTextures.push_back(alphaTex); + gpuChunk.alphaTextures[li] = alphaTex; + gpuChunk.layerCount = static_cast(i); } } else { - // No layers, use default white texture - gpuChunk.baseTexture = whiteTexture; + gpuChunk.baseTexture = whiteTexture.get(); } gpuChunk.tileX = tileX; gpuChunk.tileY = tileY; - chunks.push_back(gpuChunk); + + // Create per-chunk params UBO + TerrainParamsUBO params{}; + params.layerCount = gpuChunk.layerCount; + params.hasLayer1 = gpuChunk.layerCount >= 1 ? 1 : 0; + params.hasLayer2 = gpuChunk.layerCount >= 2 ? 1 : 0; + params.hasLayer3 = gpuChunk.layerCount >= 3 ? 1 : 0; + + VkBufferCreateInfo bufCI{}; + bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufCI.size = sizeof(TerrainParamsUBO); + bufCI.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + + VmaAllocationCreateInfo allocCI{}; + allocCI.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + allocCI.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + + VmaAllocationInfo mapInfo{}; + vmaCreateBuffer(vkCtx->getAllocator(), &bufCI, &allocCI, + &gpuChunk.paramsUBO, &gpuChunk.paramsAlloc, &mapInfo); + if (mapInfo.pMappedData) { + std::memcpy(mapInfo.pMappedData, ¶ms, sizeof(params)); + } + + // Allocate and write material descriptor set + gpuChunk.materialSet = allocateMaterialSet(); + if (gpuChunk.materialSet) { + writeMaterialDescriptors(gpuChunk.materialSet, gpuChunk); + } + + chunks.push_back(std::move(gpuChunk)); } } @@ -166,69 +314,22 @@ TerrainChunkGPU TerrainRenderer::uploadChunk(const pipeline::ChunkMesh& chunk) { gpuChunk.worldZ = chunk.worldZ; gpuChunk.indexCount = static_cast(chunk.indices.size()); - // Debug: verify Z values in uploaded vertices - static int uploadLogCount = 0; - if (uploadLogCount < 3 && !chunk.vertices.empty()) { - float minZ = 999999.0f, maxZ = -999999.0f; - for (const auto& v : chunk.vertices) { - if (v.position[2] < minZ) minZ = v.position[2]; - if (v.position[2] > maxZ) maxZ = v.position[2]; - } - LOG_DEBUG("GPU upload Z range: [", minZ, ", ", maxZ, "] delta=", maxZ - minZ); - uploadLogCount++; - } + VkDeviceSize vbSize = chunk.vertices.size() * sizeof(pipeline::TerrainVertex); + AllocatedBuffer vb = uploadBuffer(*vkCtx, chunk.vertices.data(), vbSize, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + gpuChunk.vertexBuffer = vb.buffer; + gpuChunk.vertexAlloc = vb.allocation; - // Create VAO - glGenVertexArrays(1, &gpuChunk.vao); - glBindVertexArray(gpuChunk.vao); - - // Create VBO - glGenBuffers(1, &gpuChunk.vbo); - glBindBuffer(GL_ARRAY_BUFFER, gpuChunk.vbo); - glBufferData(GL_ARRAY_BUFFER, - chunk.vertices.size() * sizeof(pipeline::TerrainVertex), - chunk.vertices.data(), - GL_STATIC_DRAW); - - // Create IBO - glGenBuffers(1, &gpuChunk.ibo); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpuChunk.ibo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, - chunk.indices.size() * sizeof(pipeline::TerrainIndex), - chunk.indices.data(), - GL_STATIC_DRAW); - - // Set up vertex attributes - // Location 0: Position (vec3) - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, - sizeof(pipeline::TerrainVertex), - (void*)offsetof(pipeline::TerrainVertex, position)); - - // Location 1: Normal (vec3) - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, - sizeof(pipeline::TerrainVertex), - (void*)offsetof(pipeline::TerrainVertex, normal)); - - // Location 2: TexCoord (vec2) - glEnableVertexAttribArray(2); - glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, - sizeof(pipeline::TerrainVertex), - (void*)offsetof(pipeline::TerrainVertex, texCoord)); - - // Location 3: LayerUV (vec2) - glEnableVertexAttribArray(3); - glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE, - sizeof(pipeline::TerrainVertex), - (void*)offsetof(pipeline::TerrainVertex, layerUV)); - - glBindVertexArray(0); + VkDeviceSize ibSize = chunk.indices.size() * sizeof(pipeline::TerrainIndex); + AllocatedBuffer ib = uploadBuffer(*vkCtx, chunk.indices.data(), ibSize, + VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + gpuChunk.indexBuffer = ib.buffer; + gpuChunk.indexAlloc = ib.allocation; return gpuChunk; } -GLuint TerrainRenderer::loadTexture(const std::string& path) { +VkTexture* TerrainRenderer::loadTexture(const std::string& path) { auto normalizeKey = [](std::string key) { std::replace(key.begin(), key.end(), '/', '\\'); std::transform(key.begin(), key.end(), key.begin(), @@ -237,59 +338,41 @@ GLuint TerrainRenderer::loadTexture(const std::string& path) { }; std::string key = normalizeKey(path); - // Check cache first auto it = textureCache.find(key); if (it != textureCache.end()) { it->second.lastUse = ++textureCacheCounter_; - return it->second.id; + return it->second.texture.get(); } - // Load BLP texture pipeline::BLPImage blp = assetManager->loadTexture(key); if (!blp.isValid()) { LOG_WARNING("Failed to load texture: ", path); - // Do not cache failure as white: MPQ/file reads can fail transiently - // during heavy streaming and should be allowed to recover. - return whiteTexture; + return whiteTexture.get(); } - // Create OpenGL texture - GLuint textureID; - glGenTextures(1, &textureID); - glBindTexture(GL_TEXTURE_2D, textureID); + auto tex = std::make_unique(); + if (!tex->upload(*vkCtx, blp.data.data(), blp.width, blp.height, + VK_FORMAT_R8G8B8A8_UNORM, true)) { + LOG_WARNING("Failed to upload texture to GPU: ", path); + return whiteTexture.get(); + } + tex->createSampler(vkCtx->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR, + VK_SAMPLER_ADDRESS_MODE_REPEAT); - // Upload texture data (BLP loader outputs RGBA8) - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, - blp.width, blp.height, 0, - GL_RGBA, GL_UNSIGNED_BYTE, blp.data.data()); - - // Set texture parameters - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); - - // Generate mipmaps - glGenerateMipmap(GL_TEXTURE_2D); - applyAnisotropicFiltering(); - - glBindTexture(GL_TEXTURE_2D, 0); - - // Cache texture + VkTexture* raw = tex.get(); TextureCacheEntry e; - e.id = textureID; + e.texture = std::move(tex); size_t base = static_cast(blp.width) * static_cast(blp.height) * 4ull; e.approxBytes = base + (base / 3); e.lastUse = ++textureCacheCounter_; textureCacheBytes_ += e.approxBytes; - textureCache[key] = e; + textureCache[key] = std::move(e); - LOG_DEBUG("Loaded texture: ", path, " (", blp.width, "x", blp.height, ")"); - - return textureID; + return raw; } -void TerrainRenderer::uploadPreloadedTextures(const std::unordered_map& textures) { +void TerrainRenderer::uploadPreloadedTextures( + const std::unordered_map& textures) { auto normalizeKey = [](std::string key) { std::replace(key.begin(), key.end(), '/', '\\'); std::transform(key.begin(), key.end(), key.begin(), @@ -298,52 +381,28 @@ void TerrainRenderer::uploadPreloadedTextures(const std::unordered_map(); + if (!tex->upload(*vkCtx, blp.data.data(), blp.width, blp.height, + VK_FORMAT_R8G8B8A8_UNORM, true)) continue; + tex->createSampler(vkCtx->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR, + VK_SAMPLER_ADDRESS_MODE_REPEAT); TextureCacheEntry e; - e.id = textureID; + e.texture = std::move(tex); size_t base = static_cast(blp.width) * static_cast(blp.height) * 4ull; e.approxBytes = base + (base / 3); e.lastUse = ++textureCacheCounter_; textureCacheBytes_ += e.approxBytes; - textureCache[key] = e; + textureCache[key] = std::move(e); } } -GLuint TerrainRenderer::createAlphaTexture(const std::vector& alphaData) { - if (alphaData.empty()) { - return opaqueAlphaTexture; - } +VkTexture* TerrainRenderer::createAlphaTexture(const std::vector& alphaData) { + if (alphaData.empty()) return opaqueAlphaTexture.get(); - if (alphaData.size() != 4096) { - LOG_WARNING("Unexpected terrain alpha size: ", alphaData.size(), " (expected 4096)"); - } - - GLuint textureID; - glGenTextures(1, &textureID); - glBindTexture(GL_TEXTURE_2D, textureID); - - // Alpha data should be 64x64 (4096 bytes). Clamp to a sane fallback when malformed. std::vector expanded; const uint8_t* src = alphaData.data(); if (alphaData.size() < 4096) { @@ -352,141 +411,105 @@ GLuint TerrainRenderer::createAlphaTexture(const std::vector& alphaData src = expanded.data(); } - int width = 64; - int height = 64; + auto tex = std::make_unique(); + if (!tex->upload(*vkCtx, src, 64, 64, VK_FORMAT_R8_UNORM, false)) { + return opaqueAlphaTexture.get(); + } + tex->createSampler(vkCtx->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR, + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, - width, height, 0, - GL_RED, GL_UNSIGNED_BYTE, src); + VkTexture* raw = tex.get(); + static uint64_t alphaCounter = 0; + std::string key = "__alpha_" + std::to_string(++alphaCounter); + TextureCacheEntry e; + e.texture = std::move(tex); + e.approxBytes = 64 * 64; + e.lastUse = ++textureCacheCounter_; + textureCacheBytes_ += e.approxBytes; + textureCache[key] = std::move(e); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - glBindTexture(GL_TEXTURE_2D, 0); - - return textureID; + return raw; } -void TerrainRenderer::renderShadow(GLuint shaderProgram, const glm::vec3& shadowCenter, float halfExtent) { - if (chunks.empty()) return; +VkDescriptorSet TerrainRenderer::allocateMaterialSet() { + VkDescriptorSetAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = materialDescPool; + allocInfo.descriptorSetCount = 1; + allocInfo.pSetLayouts = &materialSetLayout; - GLint modelLoc = glGetUniformLocation(shaderProgram, "uModel"); - glm::mat4 identity(1.0f); - glUniformMatrix4fv(modelLoc, 1, GL_FALSE, &identity[0][0]); - - for (const auto& chunk : chunks) { - if (!chunk.isValid()) continue; - - // Cull chunks whose bounding sphere doesn't overlap the shadow frustum (XY plane) - float maxDist = halfExtent + chunk.boundingSphereRadius; - float dx = chunk.boundingSphereCenter.x - shadowCenter.x; - float dy = chunk.boundingSphereCenter.y - shadowCenter.y; - if (dx * dx + dy * dy > maxDist * maxDist) continue; - - glBindVertexArray(chunk.vao); - glDrawElements(GL_TRIANGLES, chunk.indexCount, GL_UNSIGNED_INT, 0); - glBindVertexArray(0); + VkDescriptorSet set = VK_NULL_HANDLE; + if (vkAllocateDescriptorSets(vkCtx->getDevice(), &allocInfo, &set) != VK_SUCCESS) { + LOG_WARNING("TerrainRenderer: failed to allocate material descriptor set"); + return VK_NULL_HANDLE; } + return set; } -void TerrainRenderer::render(const Camera& camera) { - if (chunks.empty() || !shader) { - return; +void TerrainRenderer::writeMaterialDescriptors(VkDescriptorSet set, const TerrainChunkGPU& chunk) { + VkTexture* white = whiteTexture.get(); + VkTexture* opaque = opaqueAlphaTexture.get(); + + VkDescriptorImageInfo imageInfos[7]; + imageInfos[0] = (chunk.baseTexture ? chunk.baseTexture : white)->descriptorInfo(); + for (int i = 0; i < 3; i++) { + imageInfos[1 + i] = (chunk.layerTextures[i] ? chunk.layerTextures[i] : white)->descriptorInfo(); + imageInfos[4 + i] = (chunk.alphaTextures[i] ? chunk.alphaTextures[i] : opaque)->descriptorInfo(); } - // Enable depth testing - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_LESS); - glDepthMask(GL_TRUE); - glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - glDisable(GL_BLEND); + VkDescriptorBufferInfo bufInfo{}; + bufInfo.buffer = chunk.paramsUBO; + bufInfo.offset = 0; + bufInfo.range = sizeof(TerrainParamsUBO); - // Disable backface culling temporarily to debug flashing - glDisable(GL_CULL_FACE); - // glEnable(GL_CULL_FACE); - // glCullFace(GL_BACK); - - // Wireframe mode - if (wireframe) { - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); - } else { - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + VkWriteDescriptorSet writes[8] = {}; + for (int i = 0; i < 7; i++) { + writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[i].dstSet = set; + writes[i].dstBinding = static_cast(i); + writes[i].descriptorCount = 1; + writes[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[i].pImageInfo = &imageInfos[i]; } + writes[7].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[7].dstSet = set; + writes[7].dstBinding = 7; + writes[7].descriptorCount = 1; + writes[7].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writes[7].pBufferInfo = &bufInfo; - // Use shader - shader->use(); + vkUpdateDescriptorSets(vkCtx->getDevice(), 8, writes, 0, nullptr); +} - // Bind sampler uniforms to texture units (constant, only needs to be set once per use) - shader->setUniform("uBaseTexture", 0); - shader->setUniform("uLayer1Texture", 1); - shader->setUniform("uLayer2Texture", 2); - shader->setUniform("uLayer3Texture", 3); - shader->setUniform("uLayer1Alpha", 4); - shader->setUniform("uLayer2Alpha", 5); - shader->setUniform("uLayer3Alpha", 6); +void TerrainRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera) { + if (chunks.empty() || !pipeline) return; - // Set view/projection matrices - glm::mat4 view = camera.getViewMatrix(); - glm::mat4 projection = camera.getProjectionMatrix(); - glm::mat4 model = glm::mat4(1.0f); + VkPipeline activePipeline = (wireframe && wireframePipeline) ? wireframePipeline : pipeline; + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, activePipeline); - shader->setUniform("uModel", model); - shader->setUniform("uView", view); - shader->setUniform("uProjection", projection); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, + 0, 1, &perFrameSet, 0, nullptr); - // Set lighting - shader->setUniform("uLightDir", glm::vec3(lightDir[0], lightDir[1], lightDir[2])); - shader->setUniform("uLightColor", glm::vec3(lightColor[0], lightColor[1], lightColor[2])); - shader->setUniform("uAmbientColor", glm::vec3(ambientColor[0], ambientColor[1], ambientColor[2])); + GPUPushConstants push{}; + push.model = glm::mat4(1.0f); + vkCmdPushConstants(cmd, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, + 0, sizeof(GPUPushConstants), &push); - // Set camera position - glm::vec3 camPos = camera.getPosition(); - shader->setUniform("uViewPos", camPos); - - // Set fog (disable by setting very far distances) - shader->setUniform("uFogColor", glm::vec3(fogColor[0], fogColor[1], fogColor[2])); - if (fogEnabled) { - shader->setUniform("uFogStart", fogStart); - shader->setUniform("uFogEnd", fogEnd); - } else { - shader->setUniform("uFogStart", 100000.0f); // Very far - shader->setUniform("uFogEnd", 100001.0f); // Effectively disabled - } - - // Shadow map - shader->setUniform("uShadowEnabled", shadowEnabled ? 1 : 0); - shader->setUniform("uShadowStrength", 0.68f); - if (shadowEnabled) { - shader->setUniform("uLightSpaceMatrix", lightSpaceMatrix); - glActiveTexture(GL_TEXTURE7); - glBindTexture(GL_TEXTURE_2D, shadowDepthTex); - shader->setUniform("uShadowMap", 7); - } - - // Extract frustum for culling Frustum frustum; if (frustumCullingEnabled) { - glm::mat4 viewProj = projection * view; + glm::mat4 viewProj = camera.getProjectionMatrix() * camera.getViewMatrix(); frustum.extractFromMatrix(viewProj); } - // Render each chunk — track last-bound textures to skip redundant binds + glm::vec3 camPos = camera.getPosition(); + const float maxTerrainDistSq = 1200.0f * 1200.0f; + renderedChunks = 0; culledChunks = 0; - GLuint lastBound[7] = {0, 0, 0, 0, 0, 0, 0}; - int lastLayerConfig = -1; // track hasLayer1|hasLayer2|hasLayer3 bitmask - - // Distance culling: maximum render distance for terrain - const float maxTerrainDistSq = 1200.0f * 1200.0f; // 1200 units (reverted from 800 - mountains popping) for (const auto& chunk : chunks) { - if (!chunk.isValid()) { - continue; - } + if (!chunk.isValid() || !chunk.materialSet) continue; - // Early distance culling (before expensive frustum check) float dx = chunk.boundingSphereCenter.x - camPos.x; float dy = chunk.boundingSphereCenter.y - camPos.y; float distSq = dx * dx + dy * dy; @@ -495,83 +518,25 @@ void TerrainRenderer::render(const Camera& camera) { continue; } - // Frustum culling if (frustumCullingEnabled && !isChunkVisible(chunk, frustum)) { culledChunks++; continue; } - // Bind base texture (slot 0) — skip if same as last chunk - if (chunk.baseTexture != lastBound[0]) { - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, chunk.baseTexture); - lastBound[0] = chunk.baseTexture; - } + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, + 1, 1, &chunk.materialSet, 0, nullptr); - // Layer configuration - bool hasLayer1 = chunk.layerTextures.size() > 0; - bool hasLayer2 = chunk.layerTextures.size() > 1; - bool hasLayer3 = chunk.layerTextures.size() > 2; - int layerConfig = (hasLayer1 ? 1 : 0) | (hasLayer2 ? 2 : 0) | (hasLayer3 ? 4 : 0); - - if (layerConfig != lastLayerConfig) { - shader->setUniform("uHasLayer1", hasLayer1 ? 1 : 0); - shader->setUniform("uHasLayer2", hasLayer2 ? 1 : 0); - shader->setUniform("uHasLayer3", hasLayer3 ? 1 : 0); - lastLayerConfig = layerConfig; - } - - if (hasLayer1) { - if (chunk.layerTextures[0] != lastBound[1]) { - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[0]); - lastBound[1] = chunk.layerTextures[0]; - } - if (chunk.alphaTextures[0] != lastBound[4]) { - glActiveTexture(GL_TEXTURE4); - glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[0]); - lastBound[4] = chunk.alphaTextures[0]; - } - } - - if (hasLayer2) { - if (chunk.layerTextures[1] != lastBound[2]) { - glActiveTexture(GL_TEXTURE2); - glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[1]); - lastBound[2] = chunk.layerTextures[1]; - } - if (chunk.alphaTextures[1] != lastBound[5]) { - glActiveTexture(GL_TEXTURE5); - glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[1]); - lastBound[5] = chunk.alphaTextures[1]; - } - } - - if (hasLayer3) { - if (chunk.layerTextures[2] != lastBound[3]) { - glActiveTexture(GL_TEXTURE3); - glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[2]); - lastBound[3] = chunk.layerTextures[2]; - } - if (chunk.alphaTextures[2] != lastBound[6]) { - glActiveTexture(GL_TEXTURE6); - glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[2]); - lastBound[6] = chunk.alphaTextures[2]; - } - } - - // Draw chunk - glBindVertexArray(chunk.vao); - glDrawElements(GL_TRIANGLES, chunk.indexCount, GL_UNSIGNED_INT, 0); - glBindVertexArray(0); + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &chunk.vertexBuffer, &offset); + vkCmdBindIndexBuffer(cmd, chunk.indexBuffer, 0, VK_INDEX_TYPE_UINT32); + vkCmdDrawIndexed(cmd, chunk.indexCount, 1, 0, 0, 0); renderedChunks++; } +} - // Reset wireframe - if (wireframe) { - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); - } +void TerrainRenderer::renderShadow(VkCommandBuffer /*cmd*/, const glm::vec3& /*shadowCenter*/, float /*halfExtent*/) { + // Phase 6 stub } void TerrainRenderer::removeTile(int tileX, int tileY) { @@ -579,12 +544,7 @@ void TerrainRenderer::removeTile(int tileX, int tileY) { auto it = chunks.begin(); while (it != chunks.end()) { if (it->tileX == tileX && it->tileY == tileY) { - if (it->vao) glDeleteVertexArrays(1, &it->vao); - if (it->vbo) glDeleteBuffers(1, &it->vbo); - if (it->ibo) glDeleteBuffers(1, &it->ibo); - for (GLuint alpha : it->alphaTextures) { - if (alpha) glDeleteTextures(1, &alpha); - } + destroyChunkGPU(*it); it = chunks.erase(it); removed++; } else { @@ -597,43 +557,38 @@ void TerrainRenderer::removeTile(int tileX, int tileY) { } void TerrainRenderer::clear() { - // Delete all GPU resources + if (!vkCtx) return; + for (auto& chunk : chunks) { - if (chunk.vao) glDeleteVertexArrays(1, &chunk.vao); - if (chunk.vbo) glDeleteBuffers(1, &chunk.vbo); - if (chunk.ibo) glDeleteBuffers(1, &chunk.ibo); - - // Delete alpha textures (not cached) - for (GLuint alpha : chunk.alphaTextures) { - if (alpha) glDeleteTextures(1, &alpha); - } + destroyChunkGPU(chunk); } - chunks.clear(); renderedChunks = 0; + + if (materialDescPool) { + vkResetDescriptorPool(vkCtx->getDevice(), materialDescPool, 0); + } } -void TerrainRenderer::setLighting(const float lightDirIn[3], const float lightColorIn[3], - const float ambientColorIn[3]) { - lightDir[0] = lightDirIn[0]; - lightDir[1] = lightDirIn[1]; - lightDir[2] = lightDirIn[2]; +void TerrainRenderer::destroyChunkGPU(TerrainChunkGPU& chunk) { + VmaAllocator allocator = vkCtx->getAllocator(); - lightColor[0] = lightColorIn[0]; - lightColor[1] = lightColorIn[1]; - lightColor[2] = lightColorIn[2]; - - ambientColor[0] = ambientColorIn[0]; - ambientColor[1] = ambientColorIn[1]; - ambientColor[2] = ambientColorIn[2]; -} - -void TerrainRenderer::setFog(const float fogColorIn[3], float fogStartIn, float fogEndIn) { - fogColor[0] = fogColorIn[0]; - fogColor[1] = fogColorIn[1]; - fogColor[2] = fogColorIn[2]; - fogStart = fogStartIn; - fogEnd = fogEndIn; + if (chunk.vertexBuffer) { + AllocatedBuffer ab{}; ab.buffer = chunk.vertexBuffer; ab.allocation = chunk.vertexAlloc; + destroyBuffer(allocator, ab); + chunk.vertexBuffer = VK_NULL_HANDLE; + } + if (chunk.indexBuffer) { + AllocatedBuffer ab{}; ab.buffer = chunk.indexBuffer; ab.allocation = chunk.indexAlloc; + destroyBuffer(allocator, ab); + chunk.indexBuffer = VK_NULL_HANDLE; + } + if (chunk.paramsUBO) { + AllocatedBuffer ab{}; ab.buffer = chunk.paramsUBO; ab.allocation = chunk.paramsAlloc; + destroyBuffer(allocator, ab); + chunk.paramsUBO = VK_NULL_HANDLE; + } + chunk.materialSet = VK_NULL_HANDLE; } int TerrainRenderer::getTriangleCount() const { @@ -645,7 +600,6 @@ int TerrainRenderer::getTriangleCount() const { } bool TerrainRenderer::isChunkVisible(const TerrainChunkGPU& chunk, const Frustum& frustum) { - // Test bounding sphere against frustum return frustum.intersectsSphere(chunk.boundingSphereCenter, chunk.boundingSphereRadius); } @@ -657,7 +611,6 @@ void TerrainRenderer::calculateBoundingSphere(TerrainChunkGPU& gpuChunk, return; } - // Calculate AABB first glm::vec3 min(std::numeric_limits::max()); glm::vec3 max(std::numeric_limits::lowest()); @@ -667,10 +620,8 @@ void TerrainRenderer::calculateBoundingSphere(TerrainChunkGPU& gpuChunk, max = glm::max(max, pos); } - // Center is midpoint of AABB gpuChunk.boundingSphereCenter = (min + max) * 0.5f; - // Radius is distance from center to furthest vertex float maxDistSq = 0.0f; for (const auto& vertex : meshChunk.vertices) { glm::vec3 pos(vertex.position[0], vertex.position[1], vertex.position[2]); diff --git a/src/rendering/vk_buffer.cpp b/src/rendering/vk_buffer.cpp new file mode 100644 index 00000000..bd045fd6 --- /dev/null +++ b/src/rendering/vk_buffer.cpp @@ -0,0 +1,91 @@ +#include "rendering/vk_buffer.hpp" +#include "rendering/vk_context.hpp" +#include "core/logger.hpp" +#include + +namespace wowee { +namespace rendering { + +VkBuffer::~VkBuffer() { + destroy(); +} + +VkBuffer::VkBuffer(VkBuffer&& other) noexcept + : buf_(other.buf_), allocator_(other.allocator_), size_(other.size_) { + other.buf_ = {}; + other.allocator_ = VK_NULL_HANDLE; + other.size_ = 0; +} + +VkBuffer& VkBuffer::operator=(VkBuffer&& other) noexcept { + if (this != &other) { + destroy(); + buf_ = other.buf_; + allocator_ = other.allocator_; + size_ = other.size_; + other.buf_ = {}; + other.allocator_ = VK_NULL_HANDLE; + other.size_ = 0; + } + return *this; +} + +bool VkBuffer::uploadToGPU(VkContext& ctx, const void* data, VkDeviceSize size, + VkBufferUsageFlags usage) +{ + destroy(); + allocator_ = ctx.getAllocator(); + size_ = size; + + buf_ = uploadBuffer(ctx, data, size, usage); + if (!buf_.buffer) { + LOG_ERROR("Failed to upload buffer (size=", size, ")"); + return false; + } + + return true; +} + +bool VkBuffer::createMapped(VmaAllocator allocator, VkDeviceSize size, + VkBufferUsageFlags usage) +{ + destroy(); + allocator_ = allocator; + size_ = size; + + buf_ = createBuffer(allocator, size, usage, VMA_MEMORY_USAGE_CPU_TO_GPU); + if (!buf_.buffer) { + LOG_ERROR("Failed to create mapped buffer (size=", size, ")"); + return false; + } + + return true; +} + +void VkBuffer::updateMapped(const void* data, VkDeviceSize size, VkDeviceSize offset) { + if (!buf_.info.pMappedData) { + LOG_ERROR("Attempted to update non-mapped buffer"); + return; + } + std::memcpy(static_cast(buf_.info.pMappedData) + offset, data, size); +} + +void VkBuffer::destroy() { + if (buf_.buffer && allocator_) { + destroyBuffer(allocator_, buf_); + } + buf_ = {}; + allocator_ = VK_NULL_HANDLE; + size_ = 0; +} + +VkDescriptorBufferInfo VkBuffer::descriptorInfo(VkDeviceSize offset, VkDeviceSize range) const { + VkDescriptorBufferInfo info{}; + info.buffer = buf_.buffer; + info.offset = offset; + info.range = range; + return info; +} + +} // namespace rendering +} // namespace wowee diff --git a/src/rendering/vk_context.cpp b/src/rendering/vk_context.cpp new file mode 100644 index 00000000..2da3b41a --- /dev/null +++ b/src/rendering/vk_context.cpp @@ -0,0 +1,664 @@ +#define VMA_IMPLEMENTATION +#include "rendering/vk_context.hpp" +#include "core/logger.hpp" +#include +#include +#include +#include + +namespace wowee { +namespace rendering { + +static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( + VkDebugUtilsMessageSeverityFlagBitsEXT severity, + [[maybe_unused]] VkDebugUtilsMessageTypeFlagsEXT type, + const VkDebugUtilsMessengerCallbackDataEXT* callbackData, + [[maybe_unused]] void* userData) +{ + if (severity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) { + LOG_ERROR("Vulkan: ", callbackData->pMessage); + } else if (severity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { + LOG_WARNING("Vulkan: ", callbackData->pMessage); + } + return VK_FALSE; +} + +VkContext::~VkContext() { + shutdown(); +} + +bool VkContext::initialize(SDL_Window* window) { + LOG_INFO("Initializing Vulkan context"); + + if (!createInstance(window)) return false; + if (!createSurface(window)) return false; + if (!selectPhysicalDevice()) return false; + if (!createLogicalDevice()) return false; + if (!createAllocator()) return false; + + int w, h; + SDL_Vulkan_GetDrawableSize(window, &w, &h); + if (!createSwapchain(w, h)) return false; + + if (!createCommandPools()) return false; + if (!createSyncObjects()) return false; + if (!createImGuiResources()) return false; + + LOG_INFO("Vulkan context initialized successfully"); + return true; +} + +void VkContext::shutdown() { + if (device) { + vkDeviceWaitIdle(device); + } + + destroyImGuiResources(); + + // Destroy sync objects + for (auto& frame : frames) { + if (frame.inFlightFence) vkDestroyFence(device, frame.inFlightFence, nullptr); + if (frame.renderFinishedSemaphore) vkDestroySemaphore(device, frame.renderFinishedSemaphore, nullptr); + if (frame.imageAvailableSemaphore) vkDestroySemaphore(device, frame.imageAvailableSemaphore, nullptr); + if (frame.commandPool) vkDestroyCommandPool(device, frame.commandPool, nullptr); + frame = {}; + } + + if (immFence) { vkDestroyFence(device, immFence, nullptr); immFence = VK_NULL_HANDLE; } + if (immCommandPool) { vkDestroyCommandPool(device, immCommandPool, nullptr); immCommandPool = VK_NULL_HANDLE; } + + destroySwapchain(); + + if (allocator) { vmaDestroyAllocator(allocator); allocator = VK_NULL_HANDLE; } + if (device) { vkDestroyDevice(device, nullptr); device = VK_NULL_HANDLE; } + if (surface) { vkDestroySurfaceKHR(instance, surface, nullptr); surface = VK_NULL_HANDLE; } + + if (debugMessenger) { + auto func = reinterpret_cast( + vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT")); + if (func) func(instance, debugMessenger, nullptr); + debugMessenger = VK_NULL_HANDLE; + } + + if (instance) { vkDestroyInstance(instance, nullptr); instance = VK_NULL_HANDLE; } + + LOG_INFO("Vulkan context shutdown"); +} + +bool VkContext::createInstance(SDL_Window* window) { + // Get required SDL extensions + unsigned int sdlExtCount = 0; + SDL_Vulkan_GetInstanceExtensions(window, &sdlExtCount, nullptr); + std::vector sdlExts(sdlExtCount); + SDL_Vulkan_GetInstanceExtensions(window, &sdlExtCount, sdlExts.data()); + + vkb::InstanceBuilder builder; + builder.set_app_name("Wowee") + .set_app_version(VK_MAKE_VERSION(1, 0, 0)) + .require_api_version(1, 1, 0); + + for (auto ext : sdlExts) { + builder.enable_extension(ext); + } + + if (enableValidation) { + builder.request_validation_layers(true) + .set_debug_callback(debugCallback); + } + + auto instRet = builder.build(); + if (!instRet) { + LOG_ERROR("Failed to create Vulkan instance: ", instRet.error().message()); + return false; + } + + vkbInstance_ = instRet.value(); + instance = vkbInstance_.instance; + debugMessenger = vkbInstance_.debug_messenger; + + LOG_INFO("Vulkan instance created"); + return true; +} + +bool VkContext::createSurface(SDL_Window* window) { + if (!SDL_Vulkan_CreateSurface(window, instance, &surface)) { + LOG_ERROR("Failed to create Vulkan surface: ", SDL_GetError()); + return false; + } + return true; +} + +bool VkContext::selectPhysicalDevice() { + vkb::PhysicalDeviceSelector selector{vkbInstance_}; + selector.set_surface(surface) + .set_minimum_version(1, 1) + .prefer_gpu_device_type(vkb::PreferredDeviceType::discrete); + + auto physRet = selector.select(); + if (!physRet) { + LOG_ERROR("Failed to select Vulkan physical device: ", physRet.error().message()); + return false; + } + + vkbPhysicalDevice_ = physRet.value(); + physicalDevice = vkbPhysicalDevice_.physical_device; + + VkPhysicalDeviceProperties props; + vkGetPhysicalDeviceProperties(physicalDevice, &props); + LOG_INFO("Vulkan device: ", props.deviceName); + LOG_INFO("Vulkan API version: ", VK_VERSION_MAJOR(props.apiVersion), ".", + VK_VERSION_MINOR(props.apiVersion), ".", VK_VERSION_PATCH(props.apiVersion)); + + return true; +} + +bool VkContext::createLogicalDevice() { + vkb::DeviceBuilder deviceBuilder{vkbPhysicalDevice_}; + auto devRet = deviceBuilder.build(); + if (!devRet) { + LOG_ERROR("Failed to create Vulkan logical device: ", devRet.error().message()); + return false; + } + + auto vkbDevice = devRet.value(); + device = vkbDevice.device; + + auto gqRet = vkbDevice.get_queue(vkb::QueueType::graphics); + if (!gqRet) { + LOG_ERROR("Failed to get graphics queue"); + return false; + } + graphicsQueue = gqRet.value(); + graphicsQueueFamily = vkbDevice.get_queue_index(vkb::QueueType::graphics).value(); + + auto pqRet = vkbDevice.get_queue(vkb::QueueType::present); + if (!pqRet) { + // Fall back to graphics queue for presentation + presentQueue = graphicsQueue; + presentQueueFamily = graphicsQueueFamily; + } else { + presentQueue = pqRet.value(); + presentQueueFamily = vkbDevice.get_queue_index(vkb::QueueType::present).value(); + } + + LOG_INFO("Vulkan logical device created"); + return true; +} + +bool VkContext::createAllocator() { + VmaAllocatorCreateInfo allocInfo{}; + allocInfo.instance = instance; + allocInfo.physicalDevice = physicalDevice; + allocInfo.device = device; + allocInfo.vulkanApiVersion = VK_API_VERSION_1_1; + + if (vmaCreateAllocator(&allocInfo, &allocator) != VK_SUCCESS) { + LOG_ERROR("Failed to create VMA allocator"); + return false; + } + + LOG_INFO("VMA allocator created"); + return true; +} + +bool VkContext::createSwapchain(int width, int height) { + vkb::SwapchainBuilder swapchainBuilder{physicalDevice, device, surface}; + + auto swapRet = swapchainBuilder + .set_desired_format({VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}) + .set_desired_present_mode(VK_PRESENT_MODE_FIFO_KHR) // VSync + .set_desired_extent(static_cast(width), static_cast(height)) + .set_desired_min_image_count(2) + .set_old_swapchain(swapchain) // For recreation + .build(); + + if (!swapRet) { + LOG_ERROR("Failed to create Vulkan swapchain: ", swapRet.error().message()); + return false; + } + + // Destroy old swapchain if recreating + if (swapchain != VK_NULL_HANDLE) { + destroySwapchain(); + } + + auto vkbSwap = swapRet.value(); + swapchain = vkbSwap.swapchain; + swapchainFormat = vkbSwap.image_format; + swapchainExtent = vkbSwap.extent; + swapchainImages = vkbSwap.get_images().value(); + swapchainImageViews = vkbSwap.get_image_views().value(); + + // Create framebuffers for ImGui render pass (created after ImGui resources) + // Will be created in createImGuiResources or recreateSwapchain + + LOG_INFO("Vulkan swapchain created: ", swapchainExtent.width, "x", swapchainExtent.height, + " (", swapchainImages.size(), " images)"); + swapchainDirty = false; + return true; +} + +void VkContext::destroySwapchain() { + for (auto fb : swapchainFramebuffers) { + if (fb) vkDestroyFramebuffer(device, fb, nullptr); + } + swapchainFramebuffers.clear(); + + for (auto iv : swapchainImageViews) { + if (iv) vkDestroyImageView(device, iv, nullptr); + } + swapchainImageViews.clear(); + swapchainImages.clear(); + + if (swapchain) { + vkDestroySwapchainKHR(device, swapchain, nullptr); + swapchain = VK_NULL_HANDLE; + } +} + +bool VkContext::createCommandPools() { + // Per-frame command pools (resettable) + for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + VkCommandPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + poolInfo.queueFamilyIndex = graphicsQueueFamily; + + if (vkCreateCommandPool(device, &poolInfo, nullptr, &frames[i].commandPool) != VK_SUCCESS) { + LOG_ERROR("Failed to create command pool for frame ", i); + return false; + } + + VkCommandBufferAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.commandPool = frames[i].commandPool; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandBufferCount = 1; + + if (vkAllocateCommandBuffers(device, &allocInfo, &frames[i].commandBuffer) != VK_SUCCESS) { + LOG_ERROR("Failed to allocate command buffer for frame ", i); + return false; + } + } + + // Immediate submit pool + VkCommandPoolCreateInfo immPoolInfo{}; + immPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + immPoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + immPoolInfo.queueFamilyIndex = graphicsQueueFamily; + + if (vkCreateCommandPool(device, &immPoolInfo, nullptr, &immCommandPool) != VK_SUCCESS) { + LOG_ERROR("Failed to create immediate command pool"); + return false; + } + + return true; +} + +bool VkContext::createSyncObjects() { + VkSemaphoreCreateInfo semInfo{}; + semInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + + VkFenceCreateInfo fenceInfo{}; + fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; // Start signaled so first frame doesn't block + + for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + if (vkCreateSemaphore(device, &semInfo, nullptr, &frames[i].imageAvailableSemaphore) != VK_SUCCESS || + vkCreateSemaphore(device, &semInfo, nullptr, &frames[i].renderFinishedSemaphore) != VK_SUCCESS || + vkCreateFence(device, &fenceInfo, nullptr, &frames[i].inFlightFence) != VK_SUCCESS) { + LOG_ERROR("Failed to create sync objects for frame ", i); + return false; + } + } + + // Immediate submit fence (not signaled initially) + VkFenceCreateInfo immFenceInfo{}; + immFenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + if (vkCreateFence(device, &immFenceInfo, nullptr, &immFence) != VK_SUCCESS) { + LOG_ERROR("Failed to create immediate submit fence"); + return false; + } + + return true; +} + +bool VkContext::createDepthBuffer() { + VkImageCreateInfo imgInfo{}; + imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imgInfo.imageType = VK_IMAGE_TYPE_2D; + imgInfo.format = depthFormat; + imgInfo.extent = {swapchainExtent.width, swapchainExtent.height, 1}; + imgInfo.mipLevels = 1; + imgInfo.arrayLayers = 1; + imgInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imgInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + + VmaAllocationCreateInfo allocInfo{}; + allocInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; + + if (vmaCreateImage(allocator, &imgInfo, &allocInfo, &depthImage, &depthAllocation, nullptr) != VK_SUCCESS) { + LOG_ERROR("Failed to create depth image"); + return false; + } + + VkImageViewCreateInfo viewInfo{}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = depthImage; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = depthFormat; + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.layerCount = 1; + + if (vkCreateImageView(device, &viewInfo, nullptr, &depthImageView) != VK_SUCCESS) { + LOG_ERROR("Failed to create depth image view"); + return false; + } + + return true; +} + +void VkContext::destroyDepthBuffer() { + if (depthImageView) { vkDestroyImageView(device, depthImageView, nullptr); depthImageView = VK_NULL_HANDLE; } + if (depthImage) { vmaDestroyImage(allocator, depthImage, depthAllocation); depthImage = VK_NULL_HANDLE; depthAllocation = VK_NULL_HANDLE; } +} + +bool VkContext::createImGuiResources() { + // Create depth buffer first + if (!createDepthBuffer()) return false; + + // Render pass with color + depth attachments (used by both scene and ImGui) + VkAttachmentDescription attachments[2] = {}; + + // Color attachment (swapchain image) + attachments[0].format = swapchainFormat; + attachments[0].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + + // Depth attachment + attachments[1].format = depthFormat; + attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + + VkAttachmentReference colorRef{}; + colorRef.attachment = 0; + colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkAttachmentReference depthRef{}; + depthRef.attachment = 1; + depthRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &colorRef; + subpass.pDepthStencilAttachment = &depthRef; + + VkSubpassDependency dependency{}; + dependency.srcSubpass = VK_SUBPASS_EXTERNAL; + dependency.dstSubpass = 0; + dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dependency.srcAccessMask = 0; + dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + + VkRenderPassCreateInfo rpInfo{}; + rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + rpInfo.attachmentCount = 2; + rpInfo.pAttachments = attachments; + rpInfo.subpassCount = 1; + rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dependency; + + if (vkCreateRenderPass(device, &rpInfo, nullptr, &imguiRenderPass) != VK_SUCCESS) { + LOG_ERROR("Failed to create render pass"); + return false; + } + + // Create framebuffers (color + depth) + swapchainFramebuffers.resize(swapchainImageViews.size()); + for (size_t i = 0; i < swapchainImageViews.size(); i++) { + VkImageView fbAttachments[2] = {swapchainImageViews[i], depthImageView}; + + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = imguiRenderPass; + fbInfo.attachmentCount = 2; + fbInfo.pAttachments = fbAttachments; + fbInfo.width = swapchainExtent.width; + fbInfo.height = swapchainExtent.height; + fbInfo.layers = 1; + + if (vkCreateFramebuffer(device, &fbInfo, nullptr, &swapchainFramebuffers[i]) != VK_SUCCESS) { + LOG_ERROR("Failed to create swapchain framebuffer ", i); + return false; + } + } + + // Create descriptor pool for ImGui + VkDescriptorPoolSize poolSizes[] = { + {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 100}, + }; + + VkDescriptorPoolCreateInfo dpInfo{}; + dpInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + dpInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + dpInfo.maxSets = 100; + dpInfo.poolSizeCount = 1; + dpInfo.pPoolSizes = poolSizes; + + if (vkCreateDescriptorPool(device, &dpInfo, nullptr, &imguiDescriptorPool) != VK_SUCCESS) { + LOG_ERROR("Failed to create ImGui descriptor pool"); + return false; + } + + return true; +} + +void VkContext::destroyImGuiResources() { + if (imguiDescriptorPool) { + vkDestroyDescriptorPool(device, imguiDescriptorPool, nullptr); + imguiDescriptorPool = VK_NULL_HANDLE; + } + destroyDepthBuffer(); + // Framebuffers are destroyed in destroySwapchain() + if (imguiRenderPass) { + vkDestroyRenderPass(device, imguiRenderPass, nullptr); + imguiRenderPass = VK_NULL_HANDLE; + } +} + +bool VkContext::recreateSwapchain(int width, int height) { + vkDeviceWaitIdle(device); + + // Destroy old framebuffers + for (auto fb : swapchainFramebuffers) { + if (fb) vkDestroyFramebuffer(device, fb, nullptr); + } + swapchainFramebuffers.clear(); + + // Destroy old image views + for (auto iv : swapchainImageViews) { + if (iv) vkDestroyImageView(device, iv, nullptr); + } + swapchainImageViews.clear(); + + VkSwapchainKHR oldSwapchain = swapchain; + + vkb::SwapchainBuilder swapchainBuilder{physicalDevice, device, surface}; + auto swapRet = swapchainBuilder + .set_desired_format({VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}) + .set_desired_present_mode(VK_PRESENT_MODE_FIFO_KHR) + .set_desired_extent(static_cast(width), static_cast(height)) + .set_desired_min_image_count(2) + .set_old_swapchain(oldSwapchain) + .build(); + + if (oldSwapchain) { + vkDestroySwapchainKHR(device, oldSwapchain, nullptr); + } + + if (!swapRet) { + LOG_ERROR("Failed to recreate swapchain: ", swapRet.error().message()); + swapchain = VK_NULL_HANDLE; + return false; + } + + auto vkbSwap = swapRet.value(); + swapchain = vkbSwap.swapchain; + swapchainFormat = vkbSwap.image_format; + swapchainExtent = vkbSwap.extent; + swapchainImages = vkbSwap.get_images().value(); + swapchainImageViews = vkbSwap.get_image_views().value(); + + // Recreate depth buffer + destroyDepthBuffer(); + if (!createDepthBuffer()) return false; + + // Recreate framebuffers (color + depth) + swapchainFramebuffers.resize(swapchainImageViews.size()); + for (size_t i = 0; i < swapchainImageViews.size(); i++) { + VkImageView fbAttachments[2] = {swapchainImageViews[i], depthImageView}; + + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = imguiRenderPass; + fbInfo.attachmentCount = 2; + fbInfo.pAttachments = fbAttachments; + fbInfo.width = swapchainExtent.width; + fbInfo.height = swapchainExtent.height; + fbInfo.layers = 1; + + if (vkCreateFramebuffer(device, &fbInfo, nullptr, &swapchainFramebuffers[i]) != VK_SUCCESS) { + LOG_ERROR("Failed to recreate swapchain framebuffer ", i); + return false; + } + } + + swapchainDirty = false; + LOG_INFO("Swapchain recreated: ", swapchainExtent.width, "x", swapchainExtent.height); + return true; +} + +VkCommandBuffer VkContext::beginFrame(uint32_t& imageIndex) { + auto& frame = frames[currentFrame]; + + // Wait for this frame's fence + vkWaitForFences(device, 1, &frame.inFlightFence, VK_TRUE, UINT64_MAX); + + // Acquire next swapchain image + VkResult result = vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, + frame.imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex); + + if (result == VK_ERROR_OUT_OF_DATE_KHR) { + swapchainDirty = true; + return VK_NULL_HANDLE; + } + if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { + LOG_ERROR("Failed to acquire swapchain image"); + return VK_NULL_HANDLE; + } + + vkResetFences(device, 1, &frame.inFlightFence); + vkResetCommandBuffer(frame.commandBuffer, 0); + + VkCommandBufferBeginInfo beginInfo{}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + + vkBeginCommandBuffer(frame.commandBuffer, &beginInfo); + + return frame.commandBuffer; +} + +void VkContext::endFrame(VkCommandBuffer cmd, uint32_t imageIndex) { + vkEndCommandBuffer(cmd); + + auto& frame = frames[currentFrame]; + + VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.waitSemaphoreCount = 1; + submitInfo.pWaitSemaphores = &frame.imageAvailableSemaphore; + submitInfo.pWaitDstStageMask = &waitStage; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &cmd; + submitInfo.signalSemaphoreCount = 1; + submitInfo.pSignalSemaphores = &frame.renderFinishedSemaphore; + + if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, frame.inFlightFence) != VK_SUCCESS) { + LOG_ERROR("Failed to submit draw command buffer"); + } + + VkPresentInfoKHR presentInfo{}; + presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + presentInfo.waitSemaphoreCount = 1; + presentInfo.pWaitSemaphores = &frame.renderFinishedSemaphore; + presentInfo.swapchainCount = 1; + presentInfo.pSwapchains = &swapchain; + presentInfo.pImageIndices = &imageIndex; + + VkResult result = vkQueuePresentKHR(presentQueue, &presentInfo); + if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { + swapchainDirty = true; + } + + currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; +} + +VkCommandBuffer VkContext::beginSingleTimeCommands() { + VkCommandBufferAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.commandPool = immCommandPool; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandBufferCount = 1; + + VkCommandBuffer cmd; + vkAllocateCommandBuffers(device, &allocInfo, &cmd); + + VkCommandBufferBeginInfo beginInfo{}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cmd, &beginInfo); + + return cmd; +} + +void VkContext::endSingleTimeCommands(VkCommandBuffer cmd) { + vkEndCommandBuffer(cmd); + + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &cmd; + + vkQueueSubmit(graphicsQueue, 1, &submitInfo, immFence); + vkWaitForFences(device, 1, &immFence, VK_TRUE, UINT64_MAX); + vkResetFences(device, 1, &immFence); + + vkFreeCommandBuffers(device, immCommandPool, 1, &cmd); +} + +void VkContext::immediateSubmit(std::function&& function) { + VkCommandBuffer cmd = beginSingleTimeCommands(); + function(cmd); + endSingleTimeCommands(cmd); +} + +} // namespace rendering +} // namespace wowee diff --git a/src/rendering/vk_pipeline.cpp b/src/rendering/vk_pipeline.cpp new file mode 100644 index 00000000..5002bf69 --- /dev/null +++ b/src/rendering/vk_pipeline.cpp @@ -0,0 +1,271 @@ +#include "rendering/vk_pipeline.hpp" +#include "core/logger.hpp" + +namespace wowee { +namespace rendering { + +PipelineBuilder::PipelineBuilder() { + // Default: one blend attachment with blending disabled + colorBlendAttachments_.push_back(blendDisabled()); + + // Default dynamic states: viewport + scissor (almost always dynamic) + dynamicStates_ = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; +} + +PipelineBuilder& PipelineBuilder::setShaders( + VkPipelineShaderStageCreateInfo vert, VkPipelineShaderStageCreateInfo frag) +{ + shaderStages_ = {vert, frag}; + return *this; +} + +PipelineBuilder& PipelineBuilder::setVertexInput( + const std::vector& bindings, + const std::vector& attributes) +{ + vertexBindings_ = bindings; + vertexAttributes_ = attributes; + return *this; +} + +PipelineBuilder& PipelineBuilder::setNoVertexInput() { + vertexBindings_.clear(); + vertexAttributes_.clear(); + return *this; +} + +PipelineBuilder& PipelineBuilder::setTopology(VkPrimitiveTopology topology, + VkBool32 primitiveRestart) +{ + topology_ = topology; + primitiveRestart_ = primitiveRestart; + return *this; +} + +PipelineBuilder& PipelineBuilder::setRasterization(VkPolygonMode polygonMode, + VkCullModeFlags cullMode, VkFrontFace frontFace) +{ + polygonMode_ = polygonMode; + cullMode_ = cullMode; + frontFace_ = frontFace; + return *this; +} + +PipelineBuilder& PipelineBuilder::setDepthTest(bool enable, bool writeEnable, + VkCompareOp compareOp) +{ + depthTestEnable_ = enable; + depthWriteEnable_ = writeEnable; + depthCompareOp_ = compareOp; + return *this; +} + +PipelineBuilder& PipelineBuilder::setNoDepthTest() { + depthTestEnable_ = false; + depthWriteEnable_ = false; + return *this; +} + +PipelineBuilder& PipelineBuilder::setDepthBias(float constantFactor, float slopeFactor) { + depthBiasEnable_ = true; + depthBiasConstant_ = constantFactor; + depthBiasSlope_ = slopeFactor; + return *this; +} + +PipelineBuilder& PipelineBuilder::setColorBlendAttachment( + VkPipelineColorBlendAttachmentState blendState) +{ + colorBlendAttachments_ = {blendState}; + return *this; +} + +PipelineBuilder& PipelineBuilder::setNoColorAttachment() { + colorBlendAttachments_.clear(); + return *this; +} + +PipelineBuilder& PipelineBuilder::setMultisample(VkSampleCountFlagBits samples) { + msaaSamples_ = samples; + return *this; +} + +PipelineBuilder& PipelineBuilder::setLayout(VkPipelineLayout layout) { + pipelineLayout_ = layout; + return *this; +} + +PipelineBuilder& PipelineBuilder::setRenderPass(VkRenderPass renderPass, uint32_t subpass) { + renderPass_ = renderPass; + subpass_ = subpass; + return *this; +} + +PipelineBuilder& PipelineBuilder::setDynamicStates(const std::vector& states) { + dynamicStates_ = states; + return *this; +} + +VkPipeline PipelineBuilder::build(VkDevice device) const { + // Vertex input + VkPipelineVertexInputStateCreateInfo vertexInput{}; + vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + vertexInput.vertexBindingDescriptionCount = static_cast(vertexBindings_.size()); + vertexInput.pVertexBindingDescriptions = vertexBindings_.data(); + vertexInput.vertexAttributeDescriptionCount = static_cast(vertexAttributes_.size()); + vertexInput.pVertexAttributeDescriptions = vertexAttributes_.data(); + + // Input assembly + VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; + inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + inputAssembly.topology = topology_; + inputAssembly.primitiveRestartEnable = primitiveRestart_; + + // Viewport / scissor (dynamic, so just specify count) + VkPipelineViewportStateCreateInfo viewportState{}; + viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + viewportState.viewportCount = 1; + viewportState.scissorCount = 1; + + // Rasterization + VkPipelineRasterizationStateCreateInfo rasterizer{}; + rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rasterizer.depthClampEnable = VK_FALSE; + rasterizer.rasterizerDiscardEnable = VK_FALSE; + rasterizer.polygonMode = polygonMode_; + rasterizer.lineWidth = 1.0f; + rasterizer.cullMode = cullMode_; + rasterizer.frontFace = frontFace_; + rasterizer.depthBiasEnable = depthBiasEnable_ ? VK_TRUE : VK_FALSE; + rasterizer.depthBiasConstantFactor = depthBiasConstant_; + rasterizer.depthBiasSlopeFactor = depthBiasSlope_; + + // Multisampling + VkPipelineMultisampleStateCreateInfo multisampling{}; + multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + multisampling.sampleShadingEnable = VK_FALSE; + multisampling.rasterizationSamples = msaaSamples_; + + // Depth/stencil + VkPipelineDepthStencilStateCreateInfo depthStencil{}; + depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; + depthStencil.depthTestEnable = depthTestEnable_ ? VK_TRUE : VK_FALSE; + depthStencil.depthWriteEnable = depthWriteEnable_ ? VK_TRUE : VK_FALSE; + depthStencil.depthCompareOp = depthCompareOp_; + depthStencil.depthBoundsTestEnable = VK_FALSE; + depthStencil.stencilTestEnable = VK_FALSE; + + // Color blending + VkPipelineColorBlendStateCreateInfo colorBlending{}; + colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + colorBlending.logicOpEnable = VK_FALSE; + colorBlending.attachmentCount = static_cast(colorBlendAttachments_.size()); + colorBlending.pAttachments = colorBlendAttachments_.data(); + + // Dynamic state + VkPipelineDynamicStateCreateInfo dynamicState{}; + dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + dynamicState.dynamicStateCount = static_cast(dynamicStates_.size()); + dynamicState.pDynamicStates = dynamicStates_.data(); + + // Create pipeline + VkGraphicsPipelineCreateInfo pipelineInfo{}; + pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + pipelineInfo.stageCount = static_cast(shaderStages_.size()); + pipelineInfo.pStages = shaderStages_.data(); + pipelineInfo.pVertexInputState = &vertexInput; + pipelineInfo.pInputAssemblyState = &inputAssembly; + pipelineInfo.pViewportState = &viewportState; + pipelineInfo.pRasterizationState = &rasterizer; + pipelineInfo.pMultisampleState = &multisampling; + pipelineInfo.pDepthStencilState = &depthStencil; + pipelineInfo.pColorBlendState = colorBlendAttachments_.empty() ? nullptr : &colorBlending; + pipelineInfo.pDynamicState = dynamicStates_.empty() ? nullptr : &dynamicState; + pipelineInfo.layout = pipelineLayout_; + pipelineInfo.renderPass = renderPass_; + pipelineInfo.subpass = subpass_; + + VkPipeline pipeline = VK_NULL_HANDLE; + if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, + nullptr, &pipeline) != VK_SUCCESS) + { + LOG_ERROR("Failed to create graphics pipeline"); + return VK_NULL_HANDLE; + } + + return pipeline; +} + +VkPipelineColorBlendAttachmentState PipelineBuilder::blendDisabled() { + VkPipelineColorBlendAttachmentState state{}; + state.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + state.blendEnable = VK_FALSE; + return state; +} + +VkPipelineColorBlendAttachmentState PipelineBuilder::blendAlpha() { + VkPipelineColorBlendAttachmentState state{}; + state.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + state.blendEnable = VK_TRUE; + state.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + state.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + state.colorBlendOp = VK_BLEND_OP_ADD; + state.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + state.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + state.alphaBlendOp = VK_BLEND_OP_ADD; + return state; +} + +VkPipelineColorBlendAttachmentState PipelineBuilder::blendAdditive() { + VkPipelineColorBlendAttachmentState state{}; + state.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + state.blendEnable = VK_TRUE; + state.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + state.dstColorBlendFactor = VK_BLEND_FACTOR_ONE; + state.colorBlendOp = VK_BLEND_OP_ADD; + state.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + state.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + state.alphaBlendOp = VK_BLEND_OP_ADD; + return state; +} + +VkPipelineLayout createPipelineLayout(VkDevice device, + const std::vector& setLayouts, + const std::vector& pushConstants) +{ + VkPipelineLayoutCreateInfo layoutInfo{}; + layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + layoutInfo.setLayoutCount = static_cast(setLayouts.size()); + layoutInfo.pSetLayouts = setLayouts.data(); + layoutInfo.pushConstantRangeCount = static_cast(pushConstants.size()); + layoutInfo.pPushConstantRanges = pushConstants.data(); + + VkPipelineLayout layout = VK_NULL_HANDLE; + if (vkCreatePipelineLayout(device, &layoutInfo, nullptr, &layout) != VK_SUCCESS) { + LOG_ERROR("Failed to create pipeline layout"); + } + + return layout; +} + +VkDescriptorSetLayout createDescriptorSetLayout(VkDevice device, + const std::vector& bindings) +{ + VkDescriptorSetLayoutCreateInfo layoutInfo{}; + layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + layoutInfo.bindingCount = static_cast(bindings.size()); + layoutInfo.pBindings = bindings.data(); + + VkDescriptorSetLayout layout = VK_NULL_HANDLE; + if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &layout) != VK_SUCCESS) { + LOG_ERROR("Failed to create descriptor set layout"); + } + + return layout; +} + +} // namespace rendering +} // namespace wowee diff --git a/src/rendering/vk_render_target.cpp b/src/rendering/vk_render_target.cpp new file mode 100644 index 00000000..d677bce7 --- /dev/null +++ b/src/rendering/vk_render_target.cpp @@ -0,0 +1,170 @@ +#include "rendering/vk_render_target.hpp" +#include "rendering/vk_context.hpp" +#include "core/logger.hpp" + +namespace wowee { +namespace rendering { + +VkRenderTarget::~VkRenderTarget() { + // Must call destroy() explicitly with device/allocator before destruction +} + +bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height, VkFormat format) { + VkDevice device = ctx.getDevice(); + VmaAllocator allocator = ctx.getAllocator(); + + // Create color image (COLOR_ATTACHMENT + SAMPLED for reading in subsequent passes) + colorImage_ = createImage(device, allocator, width, height, format, + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); + + if (!colorImage_.image) { + LOG_ERROR("VkRenderTarget: failed to create color image (", width, "x", height, ")"); + return false; + } + + // Create sampler (linear filtering, clamp to edge) + VkSamplerCreateInfo samplerInfo{}; + samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerInfo.minFilter = VK_FILTER_LINEAR; + samplerInfo.magFilter = VK_FILTER_LINEAR; + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + samplerInfo.minLod = 0.0f; + samplerInfo.maxLod = 0.0f; + + if (vkCreateSampler(device, &samplerInfo, nullptr, &sampler_) != VK_SUCCESS) { + LOG_ERROR("VkRenderTarget: failed to create sampler"); + destroy(device, allocator); + return false; + } + + // Create render pass + // Color attachment: UNDEFINED -> COLOR_ATTACHMENT_OPTIMAL (during pass) + // -> SHADER_READ_ONLY_OPTIMAL (final layout, ready for sampling) + VkAttachmentDescription colorAttachment{}; + colorAttachment.format = format; + colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; + colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + colorAttachment.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + VkAttachmentReference colorRef{}; + colorRef.attachment = 0; + colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &colorRef; + + // Dependency: external -> subpass 0 (wait for previous reads to finish) + VkSubpassDependency dependency{}; + dependency.srcSubpass = VK_SUBPASS_EXTERNAL; + dependency.dstSubpass = 0; + dependency.srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dependency.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + + VkRenderPassCreateInfo rpInfo{}; + rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + rpInfo.attachmentCount = 1; + rpInfo.pAttachments = &colorAttachment; + rpInfo.subpassCount = 1; + rpInfo.pSubpasses = &subpass; + rpInfo.dependencyCount = 1; + rpInfo.pDependencies = &dependency; + + if (vkCreateRenderPass(device, &rpInfo, nullptr, &renderPass_) != VK_SUCCESS) { + LOG_ERROR("VkRenderTarget: failed to create render pass"); + destroy(device, allocator); + return false; + } + + // Create framebuffer + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = renderPass_; + fbInfo.attachmentCount = 1; + fbInfo.pAttachments = &colorImage_.imageView; + fbInfo.width = width; + fbInfo.height = height; + fbInfo.layers = 1; + + if (vkCreateFramebuffer(device, &fbInfo, nullptr, &framebuffer_) != VK_SUCCESS) { + LOG_ERROR("VkRenderTarget: failed to create framebuffer"); + destroy(device, allocator); + return false; + } + + LOG_INFO("VkRenderTarget created (", width, "x", height, ")"); + return true; +} + +void VkRenderTarget::destroy(VkDevice device, VmaAllocator allocator) { + if (framebuffer_) { + vkDestroyFramebuffer(device, framebuffer_, nullptr); + framebuffer_ = VK_NULL_HANDLE; + } + if (renderPass_) { + vkDestroyRenderPass(device, renderPass_, nullptr); + renderPass_ = VK_NULL_HANDLE; + } + if (sampler_) { + vkDestroySampler(device, sampler_, nullptr); + sampler_ = VK_NULL_HANDLE; + } + destroyImage(device, allocator, colorImage_); +} + +void VkRenderTarget::beginPass(VkCommandBuffer cmd, const VkClearColorValue& clear) { + VkRenderPassBeginInfo rpBegin{}; + rpBegin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rpBegin.renderPass = renderPass_; + rpBegin.framebuffer = framebuffer_; + rpBegin.renderArea.offset = {0, 0}; + rpBegin.renderArea.extent = getExtent(); + + VkClearValue clearValue{}; + clearValue.color = clear; + rpBegin.clearValueCount = 1; + rpBegin.pClearValues = &clearValue; + + vkCmdBeginRenderPass(cmd, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); + + // Set viewport and scissor to match render target + VkViewport viewport{}; + viewport.x = 0.0f; + viewport.y = 0.0f; + viewport.width = static_cast(colorImage_.extent.width); + viewport.height = static_cast(colorImage_.extent.height); + viewport.minDepth = 0.0f; + viewport.maxDepth = 1.0f; + vkCmdSetViewport(cmd, 0, 1, &viewport); + + VkRect2D scissor{}; + scissor.offset = {0, 0}; + scissor.extent = getExtent(); + vkCmdSetScissor(cmd, 0, 1, &scissor); +} + +void VkRenderTarget::endPass(VkCommandBuffer cmd) { + vkCmdEndRenderPass(cmd); + // Image is now in SHADER_READ_ONLY_OPTIMAL (from render pass finalLayout) +} + +VkDescriptorImageInfo VkRenderTarget::descriptorInfo() const { + VkDescriptorImageInfo info{}; + info.sampler = sampler_; + info.imageView = colorImage_.imageView; + info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + return info; +} + +} // namespace rendering +} // namespace wowee diff --git a/src/rendering/vk_shader.cpp b/src/rendering/vk_shader.cpp new file mode 100644 index 00000000..5ebc7a08 --- /dev/null +++ b/src/rendering/vk_shader.cpp @@ -0,0 +1,114 @@ +#include "rendering/vk_shader.hpp" +#include "core/logger.hpp" +#include + +namespace wowee { +namespace rendering { + +VkShaderModule::~VkShaderModule() { + destroy(); +} + +VkShaderModule::VkShaderModule(VkShaderModule&& other) noexcept + : device_(other.device_), module_(other.module_) { + other.module_ = VK_NULL_HANDLE; +} + +VkShaderModule& VkShaderModule::operator=(VkShaderModule&& other) noexcept { + if (this != &other) { + destroy(); + device_ = other.device_; + module_ = other.module_; + other.module_ = VK_NULL_HANDLE; + } + return *this; +} + +bool VkShaderModule::loadFromFile(VkDevice device, const std::string& path) { + std::ifstream file(path, std::ios::ate | std::ios::binary); + if (!file.is_open()) { + LOG_ERROR("Failed to open shader file: ", path); + return false; + } + + size_t fileSize = static_cast(file.tellg()); + if (fileSize == 0 || fileSize % 4 != 0) { + LOG_ERROR("Invalid SPIR-V file size (", fileSize, "): ", path); + return false; + } + + std::vector code(fileSize / sizeof(uint32_t)); + file.seekg(0); + file.read(reinterpret_cast(code.data()), fileSize); + file.close(); + + return loadFromMemory(device, code.data(), fileSize); +} + +bool VkShaderModule::loadFromMemory(VkDevice device, const uint32_t* code, size_t sizeBytes) { + destroy(); + device_ = device; + + VkShaderModuleCreateInfo createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + createInfo.codeSize = sizeBytes; + createInfo.pCode = code; + + if (vkCreateShaderModule(device_, &createInfo, nullptr, &module_) != VK_SUCCESS) { + LOG_ERROR("Failed to create shader module"); + return false; + } + + return true; +} + +void VkShaderModule::destroy() { + if (module_ != VK_NULL_HANDLE && device_ != VK_NULL_HANDLE) { + vkDestroyShaderModule(device_, module_, nullptr); + module_ = VK_NULL_HANDLE; + } +} + +VkPipelineShaderStageCreateInfo VkShaderModule::stageInfo( + VkShaderStageFlagBits stage, const char* entryPoint) const +{ + VkPipelineShaderStageCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + info.stage = stage; + info.module = module_; + info.pName = entryPoint; + return info; +} + +VkPipelineShaderStageCreateInfo loadShaderStage(VkDevice device, + const std::string& path, VkShaderStageFlagBits stage) +{ + // This creates a temporary module — caller must keep it alive while pipeline is created. + // Prefer using VkShaderModule directly for proper lifetime management. + VkShaderModuleCreateInfo moduleInfo{}; + moduleInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + + std::ifstream file(path, std::ios::ate | std::ios::binary); + std::vector code; + if (file.is_open()) { + size_t fileSize = static_cast(file.tellg()); + code.resize(fileSize / sizeof(uint32_t)); + file.seekg(0); + file.read(reinterpret_cast(code.data()), fileSize); + moduleInfo.codeSize = fileSize; + moduleInfo.pCode = code.data(); + } + + ::VkShaderModule module = VK_NULL_HANDLE; + vkCreateShaderModule(device, &moduleInfo, nullptr, &module); + + VkPipelineShaderStageCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + info.stage = stage; + info.module = module; + info.pName = "main"; + return info; +} + +} // namespace rendering +} // namespace wowee diff --git a/src/rendering/vk_texture.cpp b/src/rendering/vk_texture.cpp new file mode 100644 index 00000000..5c5cc5cc --- /dev/null +++ b/src/rendering/vk_texture.cpp @@ -0,0 +1,395 @@ +#include "rendering/vk_texture.hpp" +#include "rendering/vk_context.hpp" +#include "core/logger.hpp" +#include +#include +#include + +namespace wowee { +namespace rendering { + +VkTexture::~VkTexture() { + // Must call destroy() explicitly with device/allocator before destruction +} + +VkTexture::VkTexture(VkTexture&& other) noexcept + : image_(other.image_), sampler_(other.sampler_), mipLevels_(other.mipLevels_) { + other.image_ = {}; + other.sampler_ = VK_NULL_HANDLE; +} + +VkTexture& VkTexture::operator=(VkTexture&& other) noexcept { + if (this != &other) { + image_ = other.image_; + sampler_ = other.sampler_; + mipLevels_ = other.mipLevels_; + other.image_ = {}; + other.sampler_ = VK_NULL_HANDLE; + } + return *this; +} + +bool VkTexture::upload(VkContext& ctx, const uint8_t* pixels, uint32_t width, uint32_t height, + VkFormat format, bool generateMips) +{ + if (!pixels || width == 0 || height == 0) return false; + + mipLevels_ = generateMips + ? static_cast(std::floor(std::log2(std::max(width, height)))) + 1 + : 1; + + // Determine bytes per pixel from format + uint32_t bpp = 4; // default RGBA8 + if (format == VK_FORMAT_R8_UNORM) bpp = 1; + else if (format == VK_FORMAT_R8G8_UNORM) bpp = 2; + else if (format == VK_FORMAT_R8G8B8_UNORM) bpp = 3; + + VkDeviceSize imageSize = width * height * bpp; + + // Create staging buffer + AllocatedBuffer staging = createBuffer(ctx.getAllocator(), imageSize, + VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_ONLY); + + void* mapped; + vmaMapMemory(ctx.getAllocator(), staging.allocation, &mapped); + std::memcpy(mapped, pixels, imageSize); + vmaUnmapMemory(ctx.getAllocator(), staging.allocation); + + // Create image with transfer dst + src (src for mipmap generation) + sampled + VkImageUsageFlags usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + if (generateMips) { + usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + } + image_ = createImage(ctx.getDevice(), ctx.getAllocator(), width, height, + format, usage, VK_SAMPLE_COUNT_1_BIT, mipLevels_); + + if (!image_.image) { + destroyBuffer(ctx.getAllocator(), staging); + return false; + } + + ctx.immediateSubmit([&](VkCommandBuffer cmd) { + // Transition to transfer dst + transitionImageLayout(cmd, image_.image, + VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); + + // Copy staging buffer to image (mip 0) + VkBufferImageCopy region{}; + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.mipLevel = 0; + region.imageSubresource.layerCount = 1; + region.imageExtent = {width, height, 1}; + + vkCmdCopyBufferToImage(cmd, staging.buffer, image_.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + + if (!generateMips) { + // Transition to shader read + transitionImageLayout(cmd, image_.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + } + }); + + if (generateMips) { + generateMipmaps(ctx, format, width, height); + } + + destroyBuffer(ctx.getAllocator(), staging); + return true; +} + +bool VkTexture::uploadMips(VkContext& ctx, const uint8_t* const* mipData, + const uint32_t* mipSizes, uint32_t mipCount, uint32_t width, uint32_t height, VkFormat format) +{ + if (!mipData || mipCount == 0) return false; + + mipLevels_ = mipCount; + + // Calculate total staging size + VkDeviceSize totalSize = 0; + for (uint32_t i = 0; i < mipCount; i++) { + totalSize += mipSizes[i]; + } + + AllocatedBuffer staging = createBuffer(ctx.getAllocator(), totalSize, + VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_ONLY); + + void* mapped; + vmaMapMemory(ctx.getAllocator(), staging.allocation, &mapped); + VkDeviceSize offset = 0; + for (uint32_t i = 0; i < mipCount; i++) { + std::memcpy(static_cast(mapped) + offset, mipData[i], mipSizes[i]); + offset += mipSizes[i]; + } + vmaUnmapMemory(ctx.getAllocator(), staging.allocation); + + image_ = createImage(ctx.getDevice(), ctx.getAllocator(), width, height, + format, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + VK_SAMPLE_COUNT_1_BIT, mipLevels_); + + if (!image_.image) { + destroyBuffer(ctx.getAllocator(), staging); + return false; + } + + ctx.immediateSubmit([&](VkCommandBuffer cmd) { + transitionImageLayout(cmd, image_.image, + VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); + + VkDeviceSize bufOffset = 0; + uint32_t mipW = width, mipH = height; + for (uint32_t i = 0; i < mipCount; i++) { + VkBufferImageCopy region{}; + region.bufferOffset = bufOffset; + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.mipLevel = i; + region.imageSubresource.layerCount = 1; + region.imageExtent = {mipW, mipH, 1}; + + vkCmdCopyBufferToImage(cmd, staging.buffer, image_.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + + bufOffset += mipSizes[i]; + mipW = std::max(1u, mipW / 2); + mipH = std::max(1u, mipH / 2); + } + + transitionImageLayout(cmd, image_.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + }); + + destroyBuffer(ctx.getAllocator(), staging); + return true; +} + +bool VkTexture::createDepth(VkContext& ctx, uint32_t width, uint32_t height, VkFormat format) { + mipLevels_ = 1; + + image_ = createImage(ctx.getDevice(), ctx.getAllocator(), width, height, + format, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); + + if (!image_.image) return false; + + ctx.immediateSubmit([&](VkCommandBuffer cmd) { + transitionImageLayout(cmd, image_.image, + VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT); + }); + + return true; +} + +bool VkTexture::createSampler(VkDevice device, + VkFilter minFilter, VkFilter magFilter, + VkSamplerAddressMode addressMode, float maxAnisotropy) +{ + VkSamplerCreateInfo samplerInfo{}; + samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerInfo.minFilter = minFilter; + samplerInfo.magFilter = magFilter; + samplerInfo.addressModeU = addressMode; + samplerInfo.addressModeV = addressMode; + samplerInfo.addressModeW = addressMode; + samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE; + samplerInfo.maxAnisotropy = maxAnisotropy; + samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; + samplerInfo.unnormalizedCoordinates = VK_FALSE; + samplerInfo.compareEnable = VK_FALSE; + samplerInfo.mipmapMode = (minFilter == VK_FILTER_LINEAR) + ? VK_SAMPLER_MIPMAP_MODE_LINEAR : VK_SAMPLER_MIPMAP_MODE_NEAREST; + samplerInfo.mipLodBias = 0.0f; + samplerInfo.minLod = 0.0f; + samplerInfo.maxLod = static_cast(mipLevels_); + + if (vkCreateSampler(device, &samplerInfo, nullptr, &sampler_) != VK_SUCCESS) { + LOG_ERROR("Failed to create texture sampler"); + return false; + } + + return true; +} + +bool VkTexture::createSampler(VkDevice device, + VkFilter filter, + VkSamplerAddressMode addressModeU, + VkSamplerAddressMode addressModeV, + float maxAnisotropy) +{ + VkSamplerCreateInfo samplerInfo{}; + samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerInfo.minFilter = filter; + samplerInfo.magFilter = filter; + samplerInfo.addressModeU = addressModeU; + samplerInfo.addressModeV = addressModeV; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; + samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE; + samplerInfo.maxAnisotropy = maxAnisotropy; + samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; + samplerInfo.unnormalizedCoordinates = VK_FALSE; + samplerInfo.compareEnable = VK_FALSE; + samplerInfo.mipmapMode = (filter == VK_FILTER_LINEAR) + ? VK_SAMPLER_MIPMAP_MODE_LINEAR : VK_SAMPLER_MIPMAP_MODE_NEAREST; + samplerInfo.mipLodBias = 0.0f; + samplerInfo.minLod = 0.0f; + samplerInfo.maxLod = static_cast(mipLevels_); + + if (vkCreateSampler(device, &samplerInfo, nullptr, &sampler_) != VK_SUCCESS) { + LOG_ERROR("Failed to create texture sampler"); + return false; + } + + return true; +} + +bool VkTexture::createShadowSampler(VkDevice device) { + VkSamplerCreateInfo samplerInfo{}; + samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerInfo.minFilter = VK_FILTER_LINEAR; + samplerInfo.magFilter = VK_FILTER_LINEAR; + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; + samplerInfo.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; + samplerInfo.compareEnable = VK_TRUE; + samplerInfo.compareOp = VK_COMPARE_OP_LESS_OR_EQUAL; + samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + samplerInfo.minLod = 0.0f; + samplerInfo.maxLod = 1.0f; + + if (vkCreateSampler(device, &samplerInfo, nullptr, &sampler_) != VK_SUCCESS) { + LOG_ERROR("Failed to create shadow sampler"); + return false; + } + + return true; +} + +void VkTexture::destroy(VkDevice device, VmaAllocator allocator) { + if (sampler_ != VK_NULL_HANDLE) { + vkDestroySampler(device, sampler_, nullptr); + sampler_ = VK_NULL_HANDLE; + } + destroyImage(device, allocator, image_); +} + +VkDescriptorImageInfo VkTexture::descriptorInfo(VkImageLayout layout) const { + VkDescriptorImageInfo info{}; + info.sampler = sampler_; + info.imageView = image_.imageView; + info.imageLayout = layout; + return info; +} + +void VkTexture::generateMipmaps(VkContext& ctx, VkFormat format, + uint32_t width, uint32_t height) +{ + // Check if format supports linear blitting + VkFormatProperties formatProperties; + vkGetPhysicalDeviceFormatProperties(ctx.getPhysicalDevice(), format, &formatProperties); + + bool canBlit = (formatProperties.optimalTilingFeatures & + VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0; + + if (!canBlit) { + LOG_WARNING("Format does not support linear blitting for mipmap generation"); + // Fall back to simple transition + ctx.immediateSubmit([&](VkCommandBuffer cmd) { + transitionImageLayout(cmd, image_.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + }); + return; + } + + ctx.immediateSubmit([&](VkCommandBuffer cmd) { + int32_t mipW = static_cast(width); + int32_t mipH = static_cast(height); + + for (uint32_t i = 1; i < mipLevels_; i++) { + // Transition previous mip to transfer src + VkImageMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.image = image_.image; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = i - 1; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + + vkCmdPipelineBarrier(cmd, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &barrier); + + // Blit from previous mip to current + VkImageBlit blit{}; + blit.srcOffsets[0] = {0, 0, 0}; + blit.srcOffsets[1] = {mipW, mipH, 1}; + blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + blit.srcSubresource.mipLevel = i - 1; + blit.srcSubresource.layerCount = 1; + blit.dstOffsets[0] = {0, 0, 0}; + blit.dstOffsets[1] = { + mipW > 1 ? mipW / 2 : 1, + mipH > 1 ? mipH / 2 : 1, + 1 + }; + blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + blit.dstSubresource.mipLevel = i; + blit.dstSubresource.layerCount = 1; + + vkCmdBlitImage(cmd, + image_.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + image_.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, &blit, VK_FILTER_LINEAR); + + // Transition previous mip to shader read + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + vkCmdPipelineBarrier(cmd, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &barrier); + + mipW = mipW > 1 ? mipW / 2 : 1; + mipH = mipH > 1 ? mipH / 2 : 1; + } + + // Transition last mip to shader read + VkImageMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.image = image_.image; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = mipLevels_ - 1; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + vkCmdPipelineBarrier(cmd, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &barrier); + }); +} + +} // namespace rendering +} // namespace wowee diff --git a/src/rendering/vk_utils.cpp b/src/rendering/vk_utils.cpp new file mode 100644 index 00000000..d105c986 --- /dev/null +++ b/src/rendering/vk_utils.cpp @@ -0,0 +1,208 @@ +#include "rendering/vk_utils.hpp" +#include "rendering/vk_context.hpp" +#include "core/logger.hpp" +#include + +namespace wowee { +namespace rendering { + +AllocatedBuffer createBuffer(VmaAllocator allocator, VkDeviceSize size, + VkBufferUsageFlags usage, VmaMemoryUsage memoryUsage) +{ + AllocatedBuffer result{}; + + VkBufferCreateInfo bufInfo{}; + bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufInfo.size = size; + bufInfo.usage = usage; + bufInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + VmaAllocationCreateInfo allocInfo{}; + allocInfo.usage = memoryUsage; + if (memoryUsage == VMA_MEMORY_USAGE_CPU_TO_GPU || memoryUsage == VMA_MEMORY_USAGE_CPU_ONLY) { + allocInfo.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + } + + if (vmaCreateBuffer(allocator, &bufInfo, &allocInfo, + &result.buffer, &result.allocation, &result.info) != VK_SUCCESS) { + LOG_ERROR("Failed to create VMA buffer (size=", size, ")"); + } + + return result; +} + +void destroyBuffer(VmaAllocator allocator, AllocatedBuffer& buffer) { + if (buffer.buffer) { + vmaDestroyBuffer(allocator, buffer.buffer, buffer.allocation); + buffer.buffer = VK_NULL_HANDLE; + buffer.allocation = VK_NULL_HANDLE; + } +} + +AllocatedImage createImage(VkDevice device, VmaAllocator allocator, + uint32_t width, uint32_t height, VkFormat format, + VkImageUsageFlags usage, VkSampleCountFlagBits samples, uint32_t mipLevels) +{ + AllocatedImage result{}; + result.extent = {width, height}; + result.format = format; + + VkImageCreateInfo imgInfo{}; + imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imgInfo.imageType = VK_IMAGE_TYPE_2D; + imgInfo.format = format; + imgInfo.extent = {width, height, 1}; + imgInfo.mipLevels = mipLevels; + imgInfo.arrayLayers = 1; + imgInfo.samples = samples; + imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imgInfo.usage = usage; + imgInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + VmaAllocationCreateInfo allocInfo{}; + allocInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; + + if (vmaCreateImage(allocator, &imgInfo, &allocInfo, + &result.image, &result.allocation, nullptr) != VK_SUCCESS) { + LOG_ERROR("Failed to create VMA image (", width, "x", height, ")"); + return result; + } + + // Create image view + VkImageViewCreateInfo viewInfo{}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = result.image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = format; + + // Determine aspect mask from format + if (format == VK_FORMAT_D32_SFLOAT || format == VK_FORMAT_D16_UNORM || + format == VK_FORMAT_D24_UNORM_S8_UINT || format == VK_FORMAT_D32_SFLOAT_S8_UINT) { + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + } else { + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + } + viewInfo.subresourceRange.baseMipLevel = 0; + viewInfo.subresourceRange.levelCount = mipLevels; + viewInfo.subresourceRange.baseArrayLayer = 0; + viewInfo.subresourceRange.layerCount = 1; + + if (vkCreateImageView(device, &viewInfo, nullptr, &result.imageView) != VK_SUCCESS) { + LOG_ERROR("Failed to create image view"); + } + + return result; +} + +void destroyImage(VkDevice device, VmaAllocator allocator, AllocatedImage& image) { + if (image.imageView) { + vkDestroyImageView(device, image.imageView, nullptr); + image.imageView = VK_NULL_HANDLE; + } + if (image.image) { + vmaDestroyImage(allocator, image.image, image.allocation); + image.image = VK_NULL_HANDLE; + image.allocation = VK_NULL_HANDLE; + } +} + +void transitionImageLayout(VkCommandBuffer cmd, VkImage image, + VkImageLayout oldLayout, VkImageLayout newLayout, + VkPipelineStageFlags srcStage, VkPipelineStageFlags dstStage) +{ + VkImageMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = oldLayout; + barrier.newLayout = newLayout; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = VK_REMAINING_MIP_LEVELS; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS; + + if (newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL || + newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL) { + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + } else { + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + } + + // Set access masks based on layouts + switch (oldLayout) { + case VK_IMAGE_LAYOUT_UNDEFINED: + barrier.srcAccessMask = 0; + break; + case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + break; + case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: + barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + break; + case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: + barrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + break; + default: + barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; + break; + } + + switch (newLayout) { + case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + break; + case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + break; + case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: + barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + break; + case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: + barrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + break; + case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: + barrier.dstAccessMask = 0; + break; + default: + barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT; + break; + } + + vkCmdPipelineBarrier(cmd, srcStage, dstStage, 0, + 0, nullptr, 0, nullptr, 1, &barrier); +} + +AllocatedBuffer uploadBuffer(VkContext& ctx, const void* data, VkDeviceSize size, + VkBufferUsageFlags usage) +{ + // Create staging buffer + AllocatedBuffer staging = createBuffer(ctx.getAllocator(), size, + VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_ONLY); + + // Copy data to staging + void* mapped; + vmaMapMemory(ctx.getAllocator(), staging.allocation, &mapped); + std::memcpy(mapped, data, size); + vmaUnmapMemory(ctx.getAllocator(), staging.allocation); + + // Create GPU buffer + AllocatedBuffer gpuBuffer = createBuffer(ctx.getAllocator(), size, + usage | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VMA_MEMORY_USAGE_GPU_ONLY); + + // Copy staging -> GPU + ctx.immediateSubmit([&](VkCommandBuffer cmd) { + VkBufferCopy copyRegion{}; + copyRegion.size = size; + vkCmdCopyBuffer(cmd, staging.buffer, gpuBuffer.buffer, 1, ©Region); + }); + + // Destroy staging buffer + destroyBuffer(ctx.getAllocator(), staging); + + return gpuBuffer; +} + +} // namespace rendering +} // namespace wowee diff --git a/src/rendering/water_renderer.cpp b/src/rendering/water_renderer.cpp index fc43130a..f3dbd4c8 100644 --- a/src/rendering/water_renderer.cpp +++ b/src/rendering/water_renderer.cpp @@ -1,216 +1,261 @@ #include "rendering/water_renderer.hpp" -#include "rendering/shader.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_utils.hpp" +#include "rendering/vk_frame_data.hpp" #include "rendering/camera.hpp" #include "pipeline/adt_loader.hpp" #include "pipeline/wmo_loader.hpp" #include "core/logger.hpp" -#include #include #include #include +#include #include namespace wowee { namespace rendering { +// Matches set 1 binding 0 in water.frag.glsl +struct WaterMaterialUBO { + glm::vec4 waterColor; + float waterAlpha; + float shimmerStrength; + float alphaScale; + float _pad; +}; + +// Push constants matching water.vert.glsl +struct WaterPushConstants { + glm::mat4 model; + float waveAmp; + float waveFreq; + float waveSpeed; + float _pad; +}; + WaterRenderer::WaterRenderer() = default; WaterRenderer::~WaterRenderer() { shutdown(); } -bool WaterRenderer::initialize() { - LOG_INFO("Initializing water renderer"); +bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { + vkCtx = ctx; + if (!vkCtx) return false; - // Create water shader - waterShader = std::make_unique(); + LOG_INFO("Initializing water renderer (Vulkan)"); + VkDevice device = vkCtx->getDevice(); - // Vertex shader - const char* vertexShaderSource = R"( - #version 330 core - layout (location = 0) in vec3 aPos; - layout (location = 1) in vec3 aNormal; - layout (location = 2) in vec2 aTexCoord; + // --- Material descriptor set layout (set 1) --- + // binding 0: WaterMaterial UBO + VkDescriptorSetLayoutBinding matBinding{}; + matBinding.binding = 0; + matBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + matBinding.descriptorCount = 1; + matBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - uniform mat4 model; - uniform mat4 view; - uniform mat4 projection; - uniform float time; - uniform float waveAmp; - uniform float waveFreq; - uniform float waveSpeed; - uniform vec3 viewPos; - - out vec3 FragPos; - out vec3 Normal; - out vec2 TexCoord; - out float WaveOffset; - - void main() { - vec3 pos = aPos; - - // Distance from camera for LOD blending - float dist = length(viewPos - aPos); - float gridBlend = smoothstep(150.0, 400.0, dist); // 0=close (seamless), 1=far (grid effect) - - // Seamless waves (continuous across tiles) - float w1_seamless = sin((aPos.x + time * waveSpeed) * waveFreq) * waveAmp; - float w2_seamless = cos((aPos.y - time * (waveSpeed * 0.78)) * (waveFreq * 0.82)) * (waveAmp * 0.72); - float w3_seamless = sin((aPos.x * 1.7 - time * waveSpeed * 1.3 + aPos.y * 0.3) * waveFreq * 2.1) * (waveAmp * 0.35); - float w4_seamless = cos((aPos.y * 1.4 + time * waveSpeed * 0.9 + aPos.x * 0.2) * waveFreq * 1.8) * (waveAmp * 0.28); - - // Grid effect waves (per-vertex randomization for distance view) - float hash1 = fract(sin(dot(aPos.xy, vec2(12.9898, 78.233))) * 43758.5453); - float hash2 = fract(sin(dot(aPos.xy, vec2(93.9898, 67.345))) * 27153.5328); - float w1_grid = sin((aPos.x + time * waveSpeed + hash1 * 6.28) * waveFreq) * waveAmp; - float w2_grid = cos((aPos.y - time * (waveSpeed * 0.78) + hash2 * 6.28) * (waveFreq * 0.82)) * (waveAmp * 0.72); - float w3_grid = sin((aPos.x * 1.7 - time * waveSpeed * 1.3 + hash1 * 3.14) * waveFreq * 2.1) * (waveAmp * 0.35); - float w4_grid = cos((aPos.y * 1.4 + time * waveSpeed * 0.9 + hash2 * 3.14) * waveFreq * 1.8) * (waveAmp * 0.28); - - // Blend between seamless (close) and grid (far) - float wave = mix( - w1_seamless + w2_seamless + w3_seamless + w4_seamless, - w1_grid + w2_grid + w3_grid + w4_grid, - gridBlend - ); - pos.z += wave; - - FragPos = vec3(model * vec4(pos, 1.0)); - // Use mat3(model) directly - avoids expensive inverse() per vertex - Normal = mat3(model) * aNormal; - TexCoord = aTexCoord; - WaveOffset = wave; - - gl_Position = projection * view * vec4(FragPos, 1.0); - } - )"; - - // Fragment shader - const char* fragmentShaderSource = R"( - #version 330 core - in vec3 FragPos; - in vec3 Normal; - in vec2 TexCoord; - in float WaveOffset; - - uniform vec3 viewPos; - uniform vec4 waterColor; - uniform float waterAlpha; - uniform float time; - uniform float shimmerStrength; - uniform float alphaScale; - - uniform vec3 uFogColor; - uniform float uFogStart; - uniform float uFogEnd; - - out vec4 FragColor; - - void main() { - // Normalize interpolated normal - vec3 norm = normalize(Normal); - - // Simple directional light (sun) - vec3 lightDir = normalize(vec3(0.5, 1.0, 0.3)); - float diff = max(dot(norm, lightDir), 0.0); - - // Specular highlights (shininess for water) - vec3 viewDir = normalize(viewPos - FragPos); - vec3 reflectDir = reflect(-lightDir, norm); - float specBase = pow(max(dot(viewDir, reflectDir), 0.0), mix(64.0, 180.0, shimmerStrength)); - float sparkle = 0.65 + 0.35 * sin((TexCoord.x + TexCoord.y + time * 0.4) * 80.0); - float spec = specBase * mix(1.0, sparkle, shimmerStrength); - - // Animated texture coordinates for flowing effect - vec2 uv1 = TexCoord + vec2(time * 0.02, time * 0.01); - vec2 uv2 = TexCoord + vec2(-time * 0.01, time * 0.015); - - // Combine lighting - vec3 ambient = vec3(0.3) * waterColor.rgb; - vec3 diffuse = vec3(0.6) * diff * waterColor.rgb; - vec3 specular = vec3(1.0) * spec; - - // Add wave offset to brightness - float brightness = 1.0 + WaveOffset * 0.1; - - vec3 result = (ambient + diffuse + specular) * brightness; - // Add a subtle sky tint and luminance floor so large ocean sheets - // never turn black at grazing angles. - float horizon = pow(1.0 - max(dot(norm, viewDir), 0.0), 1.6); - vec3 skyTint = vec3(0.22, 0.35, 0.48) * (0.25 + 0.55 * shimmerStrength) * horizon; - result += skyTint; - result = max(result, waterColor.rgb * 0.24); - - // Subtle foam on wave crests only (no grid artifacts) - float wavePeak = smoothstep(0.35, 0.6, WaveOffset); // Only highest peaks - float foam = wavePeak * 0.25; // Subtle white highlight - result += vec3(foam); - - // Slight fresnel: more reflective/opaque at grazing angles. - float fresnel = pow(1.0 - max(dot(norm, viewDir), 0.0), 3.0); - - // Distance-based opacity: distant water is more opaque to hide underwater objects - float dist = length(viewPos - FragPos); - float distFade = smoothstep(40.0, 300.0, dist); // Start at 40 units, full opaque at 300 - float distAlpha = mix(0.0, 0.75, distFade); // Add up to 75% opacity at distance - - float alpha = clamp(waterAlpha * alphaScale * (0.80 + fresnel * 0.45) + distAlpha, 0.20, 0.98); - - // Apply distance fog - float fogDist = length(viewPos - FragPos); - float fogFactor = clamp((uFogEnd - fogDist) / (uFogEnd - uFogStart), 0.0, 1.0); - vec3 finalColor = mix(uFogColor, result, fogFactor); - - FragColor = vec4(finalColor, alpha); - } - )"; - - if (!waterShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) { - LOG_ERROR("Failed to create water shader"); + materialSetLayout = createDescriptorSetLayout(device, { matBinding }); + if (!materialSetLayout) { + LOG_ERROR("WaterRenderer: failed to create material set layout"); return false; } - LOG_INFO("Water renderer initialized"); + // --- Descriptor pool --- + VkDescriptorPoolSize poolSize{}; + poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + poolSize.descriptorCount = MAX_WATER_SETS; + + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.maxSets = MAX_WATER_SETS; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + + if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &materialDescPool) != VK_SUCCESS) { + LOG_ERROR("WaterRenderer: failed to create descriptor pool"); + return false; + } + + // --- Pipeline layout --- + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + pushRange.offset = 0; + pushRange.size = sizeof(WaterPushConstants); + + std::vector setLayouts = { perFrameLayout, materialSetLayout }; + pipelineLayout = createPipelineLayout(device, setLayouts, { pushRange }); + if (!pipelineLayout) { + LOG_ERROR("WaterRenderer: failed to create pipeline layout"); + return false; + } + + // --- Shaders --- + VkShaderModule vertShader, fragShader; + if (!vertShader.loadFromFile(device, "assets/shaders/water.vert.spv")) { + LOG_ERROR("WaterRenderer: failed to load vertex shader"); + return false; + } + if (!fragShader.loadFromFile(device, "assets/shaders/water.frag.spv")) { + LOG_ERROR("WaterRenderer: failed to load fragment shader"); + return false; + } + + // --- Vertex input (interleaved: pos3 + normal3 + uv2 = 8 floats = 32 bytes) --- + VkVertexInputBindingDescription vertBinding{}; + vertBinding.binding = 0; + vertBinding.stride = 8 * sizeof(float); + vertBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + // Water vertex shader only takes aPos(vec3) at loc 0 and aTexCoord(vec2) at loc 1 + // (normal is computed in shader from wave derivatives) + std::vector vertAttribs = { + { 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }, // aPos + { 1, 0, VK_FORMAT_R32G32_SFLOAT, 6 * sizeof(float) }, // aTexCoord (skip normal) + }; + + VkRenderPass mainPass = vkCtx->getImGuiRenderPass(); + + waterPipeline = PipelineBuilder() + .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({ vertBinding }, vertAttribs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) // depth test yes, write no + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setLayout(pipelineLayout) + .setRenderPass(mainPass) + .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) + .build(device); + + vertShader.destroy(); + fragShader.destroy(); + + if (!waterPipeline) { + LOG_ERROR("WaterRenderer: failed to create pipeline"); + return false; + } + + LOG_INFO("Water renderer initialized (Vulkan)"); return true; } void WaterRenderer::shutdown() { clear(); - waterShader.reset(); + + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + vkDeviceWaitIdle(device); + + if (waterPipeline) { vkDestroyPipeline(device, waterPipeline, nullptr); waterPipeline = VK_NULL_HANDLE; } + if (pipelineLayout) { vkDestroyPipelineLayout(device, pipelineLayout, nullptr); pipelineLayout = VK_NULL_HANDLE; } + if (materialDescPool) { vkDestroyDescriptorPool(device, materialDescPool, nullptr); materialDescPool = VK_NULL_HANDLE; } + if (materialSetLayout) { vkDestroyDescriptorSetLayout(device, materialSetLayout, nullptr); materialSetLayout = VK_NULL_HANDLE; } + + vkCtx = nullptr; } +VkDescriptorSet WaterRenderer::allocateMaterialSet() { + VkDescriptorSetAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = materialDescPool; + allocInfo.descriptorSetCount = 1; + allocInfo.pSetLayouts = &materialSetLayout; + + VkDescriptorSet set = VK_NULL_HANDLE; + if (vkAllocateDescriptorSets(vkCtx->getDevice(), &allocInfo, &set) != VK_SUCCESS) { + return VK_NULL_HANDLE; + } + return set; +} + +void WaterRenderer::updateMaterialUBO(WaterSurface& surface) { + glm::vec4 color = getLiquidColor(surface.liquidType); + float alpha = getLiquidAlpha(surface.liquidType); + + // WMO liquid material override + if (surface.wmoId != 0) { + const uint8_t basicType = (surface.liquidType == 0) ? 0 : ((surface.liquidType - 1) % 4); + if (basicType == 2 || basicType == 3) { + color = glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); + alpha = 0.45f; + } + } + + bool canalProfile = (surface.wmoId != 0) || (surface.liquidType == 5); + float shimmerStrength = canalProfile ? 0.95f : 0.50f; + float alphaScale = canalProfile ? 0.90f : 1.00f; + + WaterMaterialUBO mat{}; + mat.waterColor = color; + mat.waterAlpha = alpha; + mat.shimmerStrength = shimmerStrength; + mat.alphaScale = alphaScale; + + // Create UBO + VkBufferCreateInfo bufCI{}; + bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufCI.size = sizeof(WaterMaterialUBO); + bufCI.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + + VmaAllocationCreateInfo allocCI{}; + allocCI.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + allocCI.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + + VmaAllocationInfo mapInfo{}; + vmaCreateBuffer(vkCtx->getAllocator(), &bufCI, &allocCI, + &surface.materialUBO, &surface.materialAlloc, &mapInfo); + if (mapInfo.pMappedData) { + std::memcpy(mapInfo.pMappedData, &mat, sizeof(mat)); + } + + // Allocate and write descriptor set + surface.materialSet = allocateMaterialSet(); + if (surface.materialSet) { + VkDescriptorBufferInfo bufInfo{}; + bufInfo.buffer = surface.materialUBO; + bufInfo.offset = 0; + bufInfo.range = sizeof(WaterMaterialUBO); + + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = surface.materialSet; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + write.pBufferInfo = &bufInfo; + + vkUpdateDescriptorSets(vkCtx->getDevice(), 1, &write, 0, nullptr); + } +} + +// ============================================================== +// Data loading (preserved from GL version — no GL calls) +// ============================================================== + void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool append, int tileX, int tileY) { constexpr float TILE_SIZE = 33.33333f / 8.0f; if (!append) { - LOG_DEBUG("Loading water from terrain (replacing)"); clear(); - } else { - LOG_DEBUG("Loading water from terrain (appending)"); } - // Load water surfaces from MH2O data int totalLayers = 0; for (int chunkIdx = 0; chunkIdx < 256; chunkIdx++) { const auto& chunkWater = terrain.waterData[chunkIdx]; + if (!chunkWater.hasWater()) continue; - if (!chunkWater.hasWater()) { - continue; - } - - // Get the terrain chunk for position reference int chunkX = chunkIdx % 16; int chunkY = chunkIdx / 16; const auto& terrainChunk = terrain.getChunk(chunkX, chunkY); - // Process each water layer in this chunk for (const auto& layer : chunkWater.layers) { WaterSurface surface; - // Use the chunk base position - layer offsets will be applied in mesh generation - // to match terrain's coordinate transformation surface.position = glm::vec3( terrainChunk.position[0], terrainChunk.position[1], @@ -224,85 +269,51 @@ void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool ap surface.stepX = glm::vec3(0.0f, -TILE_SIZE, 0.0f); surface.stepY = glm::vec3(-TILE_SIZE, 0.0f, 0.0f); - // Debug log first few water surfaces - if (totalLayers < 5) { - LOG_DEBUG("Water layer ", totalLayers, ": chunk=", chunkIdx, - " liquidType=", layer.liquidType, - " offset=(", (int)layer.x, ",", (int)layer.y, ")", - " size=", (int)layer.width, "x", (int)layer.height, - " height range=[", layer.minHeight, ",", layer.maxHeight, "]"); - } - surface.minHeight = layer.minHeight; surface.maxHeight = layer.maxHeight; surface.liquidType = layer.liquidType; - // Store dimensions surface.xOffset = layer.x; surface.yOffset = layer.y; surface.width = layer.width; surface.height = layer.height; - // Prefer per-vertex terrain water heights when sane; fall back to flat - // minHeight if data looks malformed (prevents sky-stretch artifacts). size_t numVertices = (layer.width + 1) * (layer.height + 1); bool useFlat = true; if (layer.heights.size() == numVertices) { bool sane = true; for (float h : layer.heights) { - if (!std::isfinite(h) || std::abs(h) > 50000.0f) { - sane = false; - break; - } - // Conservative acceptance window around MH2O min/max metadata. - if (h < layer.minHeight - 8.0f || h > layer.maxHeight + 8.0f) { - sane = false; - break; - } - } - if (sane) { - useFlat = false; - surface.heights = layer.heights; + if (!std::isfinite(h) || std::abs(h) > 50000.0f) { sane = false; break; } + if (h < layer.minHeight - 8.0f || h > layer.maxHeight + 8.0f) { sane = false; break; } } + if (sane) { useFlat = false; surface.heights = layer.heights; } } - if (useFlat) { - surface.heights.resize(numVertices, layer.minHeight); - } + if (useFlat) surface.heights.resize(numVertices, layer.minHeight); - // Lower all terrain water in Stormwind area to prevent it from showing in tunnels/buildings/parks - // Only apply to Stormwind to avoid affecting water elsewhere - // Expanded bounds to cover all of Stormwind including outlying areas and park + // Stormwind water lowering bool isStormwindArea = (tileX >= 28 && tileX <= 50 && tileY >= 28 && tileY <= 52); - // Only lower high water (canal level >94) to avoid affecting moonwell and other low features if (isStormwindArea && layer.minHeight > 94.0f) { - // Calculate approximate world position from tile coordinates float tileWorldX = (32.0f - tileX) * 533.33333f; float tileWorldY = (32.0f - tileY) * 533.33333f; - - // Exclude moonwell area at (-8755.9, 1108.9) - don't lower water within 50 units glm::vec3 moonwellPos(-8755.9f, 1108.9f, 96.1f); float distToMoonwell = glm::distance(glm::vec2(tileWorldX, tileWorldY), glm::vec2(moonwellPos.x, moonwellPos.y)); - - if (distToMoonwell > 300.0f) { // Terrain tiles are large, use bigger exclusion radius - LOG_DEBUG(" -> LOWERING water at tile (", tileX, ",", tileY, ") from height ", layer.minHeight, " by 1 unit"); - for (float& h : surface.heights) { - h -= 1.0f; - } + if (distToMoonwell > 300.0f) { + for (float& h : surface.heights) h -= 1.0f; surface.minHeight -= 1.0f; surface.maxHeight -= 1.0f; - } else { - LOG_DEBUG(" -> SKIPPING tile (", tileX, ",", tileY, ") - moonwell exclusion (dist: ", distToMoonwell, ")"); } } - // Copy render mask surface.mask = layer.mask; - surface.tileX = tileX; surface.tileY = tileY; + createWaterMesh(surface); - surfaces.push_back(surface); + if (surface.indexCount > 0 && vkCtx) { + updateMaterialUBO(surface); + } + surfaces.push_back(std::move(surface)); totalLayers++; } } @@ -330,18 +341,10 @@ void WaterRenderer::removeTile(int tileX, int tileY) { void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liquid, [[maybe_unused]] const glm::mat4& modelMatrix, [[maybe_unused]] uint32_t wmoId) { - if (!liquid.hasLiquid() || liquid.xTiles == 0 || liquid.yTiles == 0) { - return; - } - if (liquid.xVerts < 2 || liquid.yVerts < 2) { - return; - } - if (liquid.xTiles != liquid.xVerts - 1 || liquid.yTiles != liquid.yVerts - 1) { - return; - } - if (liquid.xTiles > 64 || liquid.yTiles > 64) { - return; - } + if (!liquid.hasLiquid() || liquid.xTiles == 0 || liquid.yTiles == 0) return; + if (liquid.xVerts < 2 || liquid.yVerts < 2) return; + if (liquid.xTiles != liquid.xVerts - 1 || liquid.yTiles != liquid.yVerts - 1) return; + if (liquid.xTiles > 64 || liquid.yTiles > 64) return; WaterSurface surface; surface.tileX = -1; @@ -362,7 +365,7 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu surface.stepX = glm::vec3(modelMatrix * glm::vec4(localStepX, 0.0f)); surface.stepY = glm::vec3(modelMatrix * glm::vec4(localStepY, 0.0f)); surface.position = surface.origin; - // Guard against malformed transforms that produce giant/vertical sheets. + float stepXLen = glm::length(surface.stepX); float stepYLen = glm::length(surface.stepY); glm::vec3 planeN = glm::cross(surface.stepX, surface.stepY); @@ -371,81 +374,45 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu float spanY = stepYLen * static_cast(surface.height); if (stepXLen < 0.2f || stepXLen > 12.0f || stepYLen < 0.2f || stepYLen > 12.0f || - nz < 0.60f || - spanX > 450.0f || spanY > 450.0f) { - return; - } + nz < 0.60f || spanX > 450.0f || spanY > 450.0f) return; const int gridWidth = static_cast(surface.width) + 1; const int gridHeight = static_cast(surface.height) + 1; const int vertexCount = gridWidth * gridHeight; - // Keep WMO liquid flat for stability; some files use variant payload layouts - // that can produce invalid per-vertex heights if interpreted generically. surface.heights.assign(vertexCount, surface.origin.z); surface.minHeight = surface.origin.z; surface.maxHeight = surface.origin.z; - // Lower WMO water in Stormwind area to prevent it from showing in tunnels/buildings/parks - // Calculate tile coordinates from world position - int tileX = static_cast(std::floor((32.0f - surface.origin.x / 533.33333f))); - int tileY = static_cast(std::floor((32.0f - surface.origin.y / 533.33333f))); - - // Log all WMO water to debug park issue - LOG_DEBUG("WMO water at pos=(", surface.origin.x, ",", surface.origin.y, ",", surface.origin.z, - ") tile=(", tileX, ",", tileY, ") wmoId=", wmoId); - - // Expanded bounds to cover all of Stormwind including outlying areas and park - bool isStormwindArea = (tileX >= 28 && tileX <= 50 && tileY >= 28 && tileY <= 52); - - // Only lower high WMO water (canal level >94) to avoid affecting moonwell and other low features + // Stormwind WMO water lowering + int tilePosX = static_cast(std::floor((32.0f - surface.origin.x / 533.33333f))); + int tilePosY = static_cast(std::floor((32.0f - surface.origin.y / 533.33333f))); + bool isStormwindArea = (tilePosX >= 28 && tilePosX <= 50 && tilePosY >= 28 && tilePosY <= 52); if (isStormwindArea && surface.origin.z > 94.0f) { - // Exclude moonwell area at (-8755.9, 1108.9) - don't lower water within 20 units glm::vec3 moonwellPos(-8755.9f, 1108.9f, 96.1f); float distToMoonwell = glm::distance(glm::vec2(surface.origin.x, surface.origin.y), glm::vec2(moonwellPos.x, moonwellPos.y)); - if (distToMoonwell > 20.0f) { - LOG_DEBUG(" -> LOWERING by 1 unit (dist to moonwell: ", distToMoonwell, ")"); - for (float& h : surface.heights) { - h -= 1.0f; - } + for (float& h : surface.heights) h -= 1.0f; surface.minHeight -= 1.0f; surface.maxHeight -= 1.0f; - } else { - LOG_DEBUG(" -> SKIPPING (moonwell exclusion zone, dist: ", distToMoonwell, ")"); } } - // Skip WMO water that's clearly invalid (extremely high - above 300 units) - // This is a conservative global filter that won't affect normal gameplay - if (surface.origin.z > 300.0f) { - LOG_DEBUG("WMO water filtered: height=", surface.origin.z, " wmoId=", wmoId, " (too high)"); - return; - } - - // Skip WMO water that's extremely low (deep underground where it shouldn't be) - if (surface.origin.z < -100.0f) { - LOG_DEBUG("WMO water filtered: height=", surface.origin.z, " wmoId=", wmoId, " (too low)"); - return; - } + if (surface.origin.z > 300.0f || surface.origin.z < -100.0f) return; size_t tileCount = static_cast(surface.width) * static_cast(surface.height); size_t maskBytes = (tileCount + 7) / 8; - // WMO liquid flags vary across files; for now treat all WMO liquid tiles as - // visible for rendering. Swim/gameplay queries already ignore WMO surfaces. surface.mask.assign(maskBytes, 0xFF); createWaterMesh(surface); if (surface.indexCount > 0) { - surfaces.push_back(surface); + if (vkCtx) updateMaterialUBO(surface); + surfaces.push_back(std::move(surface)); } } void WaterRenderer::removeWMO(uint32_t wmoId) { - if (wmoId == 0) { - return; - } - + if (wmoId == 0) return; auto it = surfaces.begin(); while (it != surfaces.end()) { if (it->wmoId == wmoId) { @@ -462,155 +429,102 @@ void WaterRenderer::clear() { destroyWaterMesh(surface); } surfaces.clear(); + + if (vkCtx && materialDescPool) { + vkResetDescriptorPool(vkCtx->getDevice(), materialDescPool, 0); + } } -void WaterRenderer::render(const Camera& camera, float time) { - if (!renderingEnabled || surfaces.empty() || !waterShader) { - return; - } +// ============================================================== +// Rendering +// ============================================================== - glDisable(GL_CULL_FACE); +void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, + const Camera& /*camera*/, float /*time*/) { + if (!renderingEnabled || surfaces.empty() || !waterPipeline) return; - // Enable alpha blending for transparent water - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, waterPipeline); - // Disable depth writing so terrain shows through water - glDepthMask(GL_FALSE); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, + 0, 1, &perFrameSet, 0, nullptr); - waterShader->use(); - - // Set uniforms - glm::mat4 view = camera.getViewMatrix(); - glm::mat4 projection = camera.getProjectionMatrix(); - - waterShader->setUniform("view", view); - waterShader->setUniform("projection", projection); - waterShader->setUniform("viewPos", camera.getPosition()); - waterShader->setUniform("time", time); - waterShader->setUniform("uFogColor", fogColor); - waterShader->setUniform("uFogStart", fogStart); - waterShader->setUniform("uFogEnd", fogEnd); - - // Render each water surface for (const auto& surface : surfaces) { - if (surface.vao == 0) { - continue; - } + if (surface.vertexBuffer == VK_NULL_HANDLE || surface.indexCount == 0) continue; + if (!surface.materialSet) continue; - // Model matrix (identity, position already in vertices) - glm::mat4 model = glm::mat4(1.0f); - waterShader->setUniform("model", model); - - // Set liquid-specific color and alpha - glm::vec4 color = getLiquidColor(surface.liquidType); - float alpha = getLiquidAlpha(surface.liquidType); - - // WMO liquid material IDs are not always 1:1 with terrain LiquidType.dbc semantics. - // Avoid accidental magma/slime tint (red/green waterfalls) by forcing WMO liquids - // to water-like shading unless they're explicitly ocean. - if (surface.wmoId != 0) { - const uint8_t basicType = (surface.liquidType == 0) ? 0 : ((surface.liquidType - 1) % 4); - if (basicType == 2 || basicType == 3) { - color = glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); - alpha = 0.45f; - } - } - - // City/canal liquid profile: clearer water + stronger ripples/sun shimmer. - // Stormwind canals typically use LiquidType 5 in this data set. bool canalProfile = (surface.wmoId != 0) || (surface.liquidType == 5); - // Reduced wave amplitude to prevent tile seam gaps (tiles don't share wave state) - float waveAmp = canalProfile ? 0.04f : 0.06f; // Subtle waves to avoid boundary gaps - float waveFreq = canalProfile ? 0.30f : 0.22f; // Frequency maintained for visual - float waveSpeed = canalProfile ? 1.20f : 2.00f; // Speed maintained for animation - float shimmerStrength = canalProfile ? 0.95f : 0.50f; - float alphaScale = canalProfile ? 0.90f : 1.00f; // Increased from 0.72 to make canal water less transparent + float waveAmp = canalProfile ? 0.04f : 0.06f; + float waveFreq = canalProfile ? 0.30f : 0.22f; + float waveSpeed = canalProfile ? 1.20f : 2.00f; - waterShader->setUniform("waterColor", color); - waterShader->setUniform("waterAlpha", alpha); - waterShader->setUniform("waveAmp", waveAmp); - waterShader->setUniform("waveFreq", waveFreq); - waterShader->setUniform("waveSpeed", waveSpeed); - waterShader->setUniform("shimmerStrength", shimmerStrength); - waterShader->setUniform("alphaScale", alphaScale); + WaterPushConstants push{}; + push.model = glm::mat4(1.0f); + push.waveAmp = waveAmp; + push.waveFreq = waveFreq; + push.waveSpeed = waveSpeed; - // Render - glBindVertexArray(surface.vao); - glDrawElements(GL_TRIANGLES, surface.indexCount, GL_UNSIGNED_INT, nullptr); - glBindVertexArray(0); + vkCmdPushConstants(cmd, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, + 0, sizeof(WaterPushConstants), &push); + + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, + 1, 1, &surface.materialSet, 0, nullptr); + + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &surface.vertexBuffer, &offset); + vkCmdBindIndexBuffer(cmd, surface.indexBuffer, 0, VK_INDEX_TYPE_UINT32); + + vkCmdDrawIndexed(cmd, static_cast(surface.indexCount), 1, 0, 0, 0); } - - // Restore state - glDepthMask(GL_TRUE); - glDisable(GL_BLEND); - glEnable(GL_CULL_FACE); } +// ============================================================== +// Mesh creation (Vulkan upload instead of GL) +// ============================================================== + void WaterRenderer::createWaterMesh(WaterSurface& surface) { - // Variable-size grid based on water layer dimensions - const int gridWidth = surface.width + 1; // Vertices = tiles + 1 + const int gridWidth = surface.width + 1; const int gridHeight = surface.height + 1; - constexpr float VISUAL_WATER_Z_BIAS = 0.02f; // Small bias to avoid obvious overdraw on city meshes + constexpr float VISUAL_WATER_Z_BIAS = 0.02f; std::vector vertices; std::vector indices; - // Generate vertices for (int y = 0; y < gridHeight; y++) { for (int x = 0; x < gridWidth; x++) { int index = y * gridWidth + x; - - // Use per-vertex height data if available, otherwise flat at minHeight - float height; - if (index < static_cast(surface.heights.size())) { - height = surface.heights[index]; - } else { - height = surface.minHeight; - } + float height = (index < static_cast(surface.heights.size())) + ? surface.heights[index] : surface.minHeight; glm::vec3 pos = surface.origin + surface.stepX * static_cast(x) + surface.stepY * static_cast(y); pos.z = height + VISUAL_WATER_Z_BIAS; - // Debug first surface's corner vertices - static int debugCount = 0; - if (debugCount < 4 && (x == 0 || x == gridWidth-1) && (y == 0 || y == gridHeight-1)) { - LOG_DEBUG("Water vertex: (", pos.x, ", ", pos.y, ", ", pos.z, ")"); - debugCount++; - } - + // pos (3 floats) vertices.push_back(pos.x); vertices.push_back(pos.y); vertices.push_back(pos.z); - - // Normal (pointing up for water surface) + // normal (3 floats) - up vertices.push_back(0.0f); vertices.push_back(0.0f); vertices.push_back(1.0f); - - // Texture coordinates + // texcoord (2 floats) vertices.push_back(static_cast(x) / std::max(1, gridWidth - 1)); vertices.push_back(static_cast(y) / std::max(1, gridHeight - 1)); } } - // Generate indices (triangles), respecting the render mask + // Generate indices respecting render mask (same logic as GL version) for (int y = 0; y < gridHeight - 1; y++) { for (int x = 0; x < gridWidth - 1; x++) { - // Check render mask - each bit represents a tile - // Also render edge tiles to blend coastlines (avoid square gaps) bool renderTile = true; if (!surface.mask.empty()) { int tileIndex; if (surface.wmoId == 0 && surface.mask.size() >= 8) { - // Terrain MH2O mask is chunk-wide 8x8. int cx = static_cast(surface.xOffset) + x; int cy = static_cast(surface.yOffset) + y; tileIndex = cy * 8 + cx; } else { - // Local mask indexing (WMO/custom). tileIndex = y * surface.width + x; } int byteIndex = tileIndex / 8; @@ -621,29 +535,19 @@ void WaterRenderer::createWaterMesh(WaterSurface& surface) { bool msbOrder = (maskByte & (1 << (7 - bitIndex))) != 0; renderTile = lsbOrder || msbOrder; - // If this tile is masked out, check neighbors to fill coastline gaps if (!renderTile) { - // Check adjacent tiles - render if any neighbor is water (blend coastline) for (int dy = -1; dy <= 1; dy++) { for (int dx = -1; dx <= 1; dx++) { if (dx == 0 && dy == 0) continue; - int nx = x + dx; - int ny = y + dy; - // Bounds check neighbors + int nx = x + dx, ny = y + dy; if (nx < 0 || ny < 0 || nx >= gridWidth-1 || ny >= gridHeight-1) continue; - - // Calculate neighbor mask index (consistent with main tile indexing) int neighborIdx; if (surface.wmoId == 0 && surface.mask.size() >= 8) { - // Terrain MH2O: account for xOffset/yOffset - int ncx = static_cast(surface.xOffset) + nx; - int ncy = static_cast(surface.yOffset) + ny; - neighborIdx = ncy * 8 + ncx; + neighborIdx = (static_cast(surface.yOffset) + ny) * 8 + + (static_cast(surface.xOffset) + nx); } else { - // WMO/custom: local indexing neighborIdx = ny * surface.width + nx; } - int nByteIdx = neighborIdx / 8; int nBitIdx = neighborIdx % 8; if (nByteIdx < static_cast(surface.mask.size())) { @@ -660,30 +564,24 @@ void WaterRenderer::createWaterMesh(WaterSurface& surface) { } } - if (!renderTile) { - continue; // Skip this tile - } + if (!renderTile) continue; int topLeft = y * gridWidth + x; int topRight = topLeft + 1; int bottomLeft = (y + 1) * gridWidth + x; int bottomRight = bottomLeft + 1; - // First triangle indices.push_back(topLeft); indices.push_back(bottomLeft); indices.push_back(topRight); - - // Second triangle indices.push_back(topRight); indices.push_back(bottomLeft); indices.push_back(bottomRight); } } + // Fallback: if terrain MH2O mask produced no tiles, render full rect if (indices.empty() && surface.wmoId == 0) { - // Terrain MH2O masks can be inconsistent in some tiles. If a terrain layer - // produces no visible tiles, fall back to its full local rect for rendering. for (int y = 0; y < gridHeight - 1; y++) { for (int x = 0; x < gridWidth - 1; x++) { int topLeft = y * gridWidth + x; @@ -701,98 +599,82 @@ void WaterRenderer::createWaterMesh(WaterSurface& surface) { } if (indices.empty()) return; - surface.indexCount = static_cast(indices.size()); - // Create OpenGL buffers - glGenVertexArrays(1, &surface.vao); - glGenBuffers(1, &surface.vbo); - glGenBuffers(1, &surface.ebo); + if (!vkCtx) return; - glBindVertexArray(surface.vao); + // Upload vertex buffer + VkDeviceSize vbSize = vertices.size() * sizeof(float); + AllocatedBuffer vb = uploadBuffer(*vkCtx, vertices.data(), vbSize, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + surface.vertexBuffer = vb.buffer; + surface.vertexAlloc = vb.allocation; - // Upload vertex data - glBindBuffer(GL_ARRAY_BUFFER, surface.vbo); - glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW); - - // Upload index data - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, surface.ebo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(uint32_t), indices.data(), GL_STATIC_DRAW); - - // Set vertex attributes - // Position - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - - // Normal - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(1); - - // Texture coordinates - glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); - glEnableVertexAttribArray(2); - - glBindVertexArray(0); + // Upload index buffer + VkDeviceSize ibSize = indices.size() * sizeof(uint32_t); + AllocatedBuffer ib = uploadBuffer(*vkCtx, indices.data(), ibSize, + VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + surface.indexBuffer = ib.buffer; + surface.indexAlloc = ib.allocation; } void WaterRenderer::destroyWaterMesh(WaterSurface& surface) { - if (surface.vao != 0) { - glDeleteVertexArrays(1, &surface.vao); - surface.vao = 0; + if (!vkCtx) return; + VmaAllocator allocator = vkCtx->getAllocator(); + + if (surface.vertexBuffer) { + AllocatedBuffer ab{}; ab.buffer = surface.vertexBuffer; ab.allocation = surface.vertexAlloc; + destroyBuffer(allocator, ab); + surface.vertexBuffer = VK_NULL_HANDLE; } - if (surface.vbo != 0) { - glDeleteBuffers(1, &surface.vbo); - surface.vbo = 0; + if (surface.indexBuffer) { + AllocatedBuffer ab{}; ab.buffer = surface.indexBuffer; ab.allocation = surface.indexAlloc; + destroyBuffer(allocator, ab); + surface.indexBuffer = VK_NULL_HANDLE; } - if (surface.ebo != 0) { - glDeleteBuffers(1, &surface.ebo); - surface.ebo = 0; + if (surface.materialUBO) { + AllocatedBuffer ab{}; ab.buffer = surface.materialUBO; ab.allocation = surface.materialAlloc; + destroyBuffer(allocator, ab); + surface.materialUBO = VK_NULL_HANDLE; } + surface.materialSet = VK_NULL_HANDLE; } +// ============================================================== +// Query functions (data-only, no GL) +// ============================================================== + std::optional WaterRenderer::getWaterHeightAt(float glX, float glY) const { std::optional best; - for (size_t si = 0; si < surfaces.size(); si++) { - const auto& surface = surfaces[si]; + for (const auto& surface : surfaces) { glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y); - glm::vec2 stepX(surface.stepX.x, surface.stepX.y); - glm::vec2 stepY(surface.stepY.x, surface.stepY.y); - float lenSqX = glm::dot(stepX, stepX); - float lenSqY = glm::dot(stepY, stepY); - if (lenSqX < 1e-6f || lenSqY < 1e-6f) { - continue; - } - float gx = glm::dot(rel, stepX) / lenSqX; - float gy = glm::dot(rel, stepY) / lenSqY; + glm::vec2 sX(surface.stepX.x, surface.stepX.y); + glm::vec2 sY(surface.stepY.x, surface.stepY.y); + float lenSqX = glm::dot(sX, sX); + float lenSqY = glm::dot(sY, sY); + if (lenSqX < 1e-6f || lenSqY < 1e-6f) continue; + float gx = glm::dot(rel, sX) / lenSqX; + float gy = glm::dot(rel, sY) / lenSqY; if (gx < 0.0f || gx > static_cast(surface.width) || - gy < 0.0f || gy > static_cast(surface.height)) { - continue; - } + gy < 0.0f || gy > static_cast(surface.height)) continue; int gridWidth = surface.width + 1; - - // Bilinear interpolation int ix = static_cast(gx); int iy = static_cast(gy); float fx = gx - ix; float fy = gy - iy; - // Clamp to valid vertex range if (ix >= surface.width) { ix = surface.width - 1; fx = 1.0f; } if (iy >= surface.height) { iy = surface.height - 1; fy = 1.0f; } - if (ix < 0 || iy < 0) { - continue; - } + if (ix < 0 || iy < 0) continue; - // Respect per-tile mask so holes/non-liquid tiles do not count as swimmable. if (!surface.mask.empty()) { int tileIndex; if (surface.wmoId == 0 && surface.mask.size() >= 8) { - int cx = static_cast(surface.xOffset) + ix; - int cy = static_cast(surface.yOffset) + iy; - tileIndex = cy * 8 + cx; + tileIndex = (static_cast(surface.yOffset) + iy) * 8 + + (static_cast(surface.xOffset) + ix); } else { tileIndex = iy * surface.width + ix; } @@ -800,12 +682,8 @@ std::optional WaterRenderer::getWaterHeightAt(float glX, float glY) const int bitIndex = tileIndex % 8; if (byteIndex < static_cast(surface.mask.size())) { uint8_t maskByte = surface.mask[byteIndex]; - bool lsbOrder = (maskByte & (1 << bitIndex)) != 0; - bool msbOrder = (maskByte & (1 << (7 - bitIndex))) != 0; - bool renderTile = lsbOrder || msbOrder; - if (!renderTile) { - continue; - } + bool renderTile = (maskByte & (1 << bitIndex)) || (maskByte & (1 << (7 - bitIndex))); + if (!renderTile) continue; } } @@ -817,17 +695,11 @@ std::optional WaterRenderer::getWaterHeightAt(float glX, float glY) const int total = static_cast(surface.heights.size()); if (idx11 >= total) continue; - float h00 = surface.heights[idx00]; - float h10 = surface.heights[idx10]; - float h01 = surface.heights[idx01]; - float h11 = surface.heights[idx11]; + float h00 = surface.heights[idx00], h10 = surface.heights[idx10]; + float h01 = surface.heights[idx01], h11 = surface.heights[idx11]; + float h = h00*(1-fx)*(1-fy) + h10*fx*(1-fy) + h01*(1-fx)*fy + h11*fx*fy; - float h = h00 * (1-fx) * (1-fy) + h10 * fx * (1-fy) + - h01 * (1-fx) * fy + h11 * fx * fy; - - if (!best || h > *best) { - best = h; - } + if (!best || h > *best) best = h; } return best; @@ -839,20 +711,16 @@ std::optional WaterRenderer::getWaterTypeAt(float glX, float glY) cons for (const auto& surface : surfaces) { glm::vec2 rel(glX - surface.origin.x, glY - surface.origin.y); - glm::vec2 stepX(surface.stepX.x, surface.stepX.y); - glm::vec2 stepY(surface.stepY.x, surface.stepY.y); - float lenSqX = glm::dot(stepX, stepX); - float lenSqY = glm::dot(stepY, stepY); - if (lenSqX < 1e-6f || lenSqY < 1e-6f) { - continue; - } + glm::vec2 sX(surface.stepX.x, surface.stepX.y); + glm::vec2 sY(surface.stepY.x, surface.stepY.y); + float lenSqX = glm::dot(sX, sX); + float lenSqY = glm::dot(sY, sY); + if (lenSqX < 1e-6f || lenSqY < 1e-6f) continue; - float gx = glm::dot(rel, stepX) / lenSqX; - float gy = glm::dot(rel, stepY) / lenSqY; + float gx = glm::dot(rel, sX) / lenSqX; + float gy = glm::dot(rel, sY) / lenSqY; if (gx < 0.0f || gx > static_cast(surface.width) || - gy < 0.0f || gy > static_cast(surface.height)) { - continue; - } + gy < 0.0f || gy > static_cast(surface.height)) continue; int ix = static_cast(gx); int iy = static_cast(gy); @@ -863,9 +731,8 @@ std::optional WaterRenderer::getWaterTypeAt(float glX, float glY) cons if (!surface.mask.empty()) { int tileIndex; if (surface.wmoId == 0 && surface.mask.size() >= 8) { - int cx = static_cast(surface.xOffset) + ix; - int cy = static_cast(surface.yOffset) + iy; - tileIndex = cy * 8 + cx; + tileIndex = (static_cast(surface.yOffset) + iy) * 8 + + (static_cast(surface.xOffset) + ix); } else { tileIndex = iy * surface.width + ix; } @@ -873,14 +740,11 @@ std::optional WaterRenderer::getWaterTypeAt(float glX, float glY) cons int bitIndex = tileIndex % 8; if (byteIndex < static_cast(surface.mask.size())) { uint8_t maskByte = surface.mask[byteIndex]; - bool lsbOrder = (maskByte & (1 << bitIndex)) != 0; - bool msbOrder = (maskByte & (1 << (7 - bitIndex))) != 0; - bool renderTile = lsbOrder || msbOrder; + bool renderTile = (maskByte & (1 << bitIndex)) || (maskByte & (1 << (7 - bitIndex))); if (!renderTile) continue; } } - // Use minHeight as stable selector for "topmost surface at XY". float h = surface.minHeight; if (!bestHeight || h > *bestHeight) { bestHeight = h; @@ -892,40 +756,23 @@ std::optional WaterRenderer::getWaterTypeAt(float glX, float glY) cons } glm::vec4 WaterRenderer::getLiquidColor(uint16_t liquidType) const { - // WoW 3.3.5a LiquidType.dbc IDs: - // 1,5,9,13,17 = Water variants (still, slow, fast) - // 2,6,10,14 = Ocean - // 3,7,11,15 = Magma - // 4,8,12 = Slime - // Map to basic type using (id - 1) % 4 for standard IDs, or handle ranges - uint8_t basicType; - if (liquidType == 0) { - basicType = 0; // Water (fallback) - } else { - basicType = ((liquidType - 1) % 4); - } - + uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4); switch (basicType) { - case 0: // Water - return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); - case 1: // Ocean - return glm::vec4(0.06f, 0.18f, 0.34f, 1.0f); - case 2: // Magma - return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f); - case 3: // Slime - return glm::vec4(0.2f, 0.6f, 0.1f, 1.0f); - default: - return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); // Water fallback + case 0: return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); + case 1: return glm::vec4(0.06f, 0.18f, 0.34f, 1.0f); + case 2: return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f); + case 3: return glm::vec4(0.2f, 0.6f, 0.1f, 1.0f); + default: return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); } } float WaterRenderer::getLiquidAlpha(uint16_t liquidType) const { uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4); switch (basicType) { - case 1: return 0.68f; // Ocean - case 2: return 0.72f; // Magma - case 3: return 0.62f; // Slime - default: return 0.38f; // Water + case 1: return 0.68f; + case 2: return 0.72f; + case 3: return 0.62f; + default: return 0.38f; } } diff --git a/src/rendering/weather.cpp b/src/rendering/weather.cpp index e6bca672..ae59fde7 100644 --- a/src/rendering/weather.cpp +++ b/src/rendering/weather.cpp @@ -1,10 +1,15 @@ #include "rendering/weather.hpp" #include "rendering/camera.hpp" -#include "rendering/shader.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_frame_data.hpp" +#include "rendering/vk_utils.hpp" #include "core/logger.hpp" #include #include #include +#include namespace wowee { namespace rendering { @@ -13,71 +18,94 @@ Weather::Weather() { } Weather::~Weather() { - cleanup(); + shutdown(); } -bool Weather::initialize() { +bool Weather::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { LOG_INFO("Initializing weather system"); - // Create shader - shader = std::make_unique(); + vkCtx = ctx; + VkDevice device = vkCtx->getDevice(); - // Vertex shader - point sprites with instancing - const char* vertexShaderSource = R"( - #version 330 core - layout (location = 0) in vec3 aPos; - - uniform mat4 uView; - uniform mat4 uProjection; - uniform float uParticleSize; - - void main() { - gl_Position = uProjection * uView * vec4(aPos, 1.0); - gl_PointSize = uParticleSize; - } - )"; - - // Fragment shader - simple particle with alpha - const char* fragmentShaderSource = R"( - #version 330 core - - uniform vec4 uParticleColor; - - out vec4 FragColor; - - void main() { - // Circular particle shape - vec2 coord = gl_PointCoord - vec2(0.5); - float dist = length(coord); - - if (dist > 0.5) { - discard; - } - - // Soft edges - float alpha = smoothstep(0.5, 0.3, dist) * uParticleColor.a; - - FragColor = vec4(uParticleColor.rgb, alpha); - } - )"; - - if (!shader->loadFromSource(vertexShaderSource, fragmentShaderSource)) { - LOG_ERROR("Failed to create weather shader"); + // Load SPIR-V shaders + VkShaderModule vertModule; + if (!vertModule.loadFromFile(device, "assets/shaders/weather.vert.spv")) { + LOG_ERROR("Failed to load weather vertex shader"); return false; } - // Create VAO and VBO for particle positions - glGenVertexArrays(1, &vao); - glGenBuffers(1, &vbo); + VkShaderModule fragModule; + if (!fragModule.loadFromFile(device, "assets/shaders/weather.frag.spv")) { + LOG_ERROR("Failed to load weather fragment shader"); + return false; + } - glBindVertexArray(vao); - glBindBuffer(GL_ARRAY_BUFFER, vbo); + VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT); + VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT); - // Position attribute - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0); - glEnableVertexAttribArray(0); + // Push constant range: { float particleSize; float pad0; float pad1; float pad2; vec4 particleColor; } = 32 bytes + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + pushRange.offset = 0; + pushRange.size = 32; // 4 floats + vec4 - glBindVertexArray(0); + // Create pipeline layout with perFrameLayout (set 0) + push constants + pipelineLayout = createPipelineLayout(device, {perFrameLayout}, {pushRange}); + if (pipelineLayout == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create weather pipeline layout"); + return false; + } + + // Vertex input: position only (vec3), stride = 3 * sizeof(float) + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 3 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + VkVertexInputAttributeDescription posAttr{}; + posAttr.location = 0; + posAttr.binding = 0; + posAttr.format = VK_FORMAT_R32G32B32_SFLOAT; + posAttr.offset = 0; + + // Dynamic viewport and scissor + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + pipeline = PipelineBuilder() + .setShaders(vertStage, fragStage) + .setVertexInput({binding}, {posAttr}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_POINT_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS) // depth test on, write off (transparent particles) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setLayout(pipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates(dynamicStates) + .build(device); + + vertModule.destroy(); + fragModule.destroy(); + + if (pipeline == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create weather pipeline"); + return false; + } + + // Create a dynamic mapped vertex buffer large enough for MAX_PARTICLES + dynamicVBSize = MAX_PARTICLES * sizeof(glm::vec3); + AllocatedBuffer buf = createBuffer(vkCtx->getAllocator(), dynamicVBSize, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU); + dynamicVB = buf.buffer; + dynamicVBAlloc = buf.allocation; + dynamicVBAllocInfo = buf.info; + + if (dynamicVB == VK_NULL_HANDLE) { + LOG_ERROR("Failed to create weather dynamic vertex buffer"); + return false; + } // Reserve space for particles particles.reserve(MAX_PARTICLES); @@ -162,58 +190,54 @@ void Weather::updateParticle(Particle& particle, const Camera& camera, float del particle.position += particle.velocity * deltaTime; } -void Weather::render(const Camera& camera) { - if (!enabled || weatherType == Type::NONE || particlePositions.empty() || !shader) { +void Weather::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) { + if (!enabled || weatherType == Type::NONE || particlePositions.empty() || + pipeline == VK_NULL_HANDLE) { return; } - // Enable blending - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - // Disable depth write (particles are transparent) - glDepthMask(GL_FALSE); - - // Enable point sprites - glEnable(GL_PROGRAM_POINT_SIZE); - - shader->use(); - - // Set matrices - glm::mat4 view = camera.getViewMatrix(); - glm::mat4 projection = camera.getProjectionMatrix(); - - shader->setUniform("uView", view); - shader->setUniform("uProjection", projection); - - // Set particle appearance based on weather type - if (weatherType == Type::RAIN) { - // Rain: white/blue streaks, small size - shader->setUniform("uParticleColor", glm::vec4(0.7f, 0.8f, 0.9f, 0.6f)); - shader->setUniform("uParticleSize", 3.0f); - } else { // SNOW - // Snow: white fluffy, larger size - shader->setUniform("uParticleColor", glm::vec4(1.0f, 1.0f, 1.0f, 0.9f)); - shader->setUniform("uParticleSize", 8.0f); + // Upload particle positions to mapped buffer + VkDeviceSize uploadSize = particlePositions.size() * sizeof(glm::vec3); + if (uploadSize > 0 && dynamicVBAllocInfo.pMappedData) { + std::memcpy(dynamicVBAllocInfo.pMappedData, particlePositions.data(), uploadSize); } - // Upload particle positions - glBindVertexArray(vao); - glBindBuffer(GL_ARRAY_BUFFER, vbo); - glBufferData(GL_ARRAY_BUFFER, - particlePositions.size() * sizeof(glm::vec3), - particlePositions.data(), - GL_DYNAMIC_DRAW); + // Push constant data: { float particleSize; float pad0; float pad1; float pad2; vec4 particleColor; } + struct WeatherPush { + float particleSize; + float pad0; + float pad1; + float pad2; + glm::vec4 particleColor; + }; - // Render particles as points - glDrawArrays(GL_POINTS, 0, static_cast(particlePositions.size())); + WeatherPush push{}; + if (weatherType == Type::RAIN) { + push.particleSize = 3.0f; + push.particleColor = glm::vec4(0.7f, 0.8f, 0.9f, 0.6f); + } else { // SNOW + push.particleSize = 8.0f; + push.particleColor = glm::vec4(1.0f, 1.0f, 1.0f, 0.9f); + } - glBindVertexArray(0); + // Bind pipeline + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); - // Restore state - glDisable(GL_BLEND); - glDepthMask(GL_TRUE); - glDisable(GL_PROGRAM_POINT_SIZE); + // Bind per-frame descriptor set (set 0 - camera UBO) + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, + 0, 1, &perFrameSet, 0, nullptr); + + // Push constants + vkCmdPushConstants(cmd, pipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof(push), &push); + + // Bind vertex buffer + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &dynamicVB, &offset); + + // Draw particles as points + vkCmdDraw(cmd, static_cast(particlePositions.size()), 1, 0, 0); } void Weather::resetParticles(const Camera& camera) { @@ -260,15 +284,29 @@ int Weather::getParticleCount() const { return static_cast(particles.size()); } -void Weather::cleanup() { - if (vao) { - glDeleteVertexArrays(1, &vao); - vao = 0; - } - if (vbo) { - glDeleteBuffers(1, &vbo); - vbo = 0; +void Weather::shutdown() { + if (vkCtx) { + VkDevice device = vkCtx->getDevice(); + VmaAllocator allocator = vkCtx->getAllocator(); + + if (pipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(device, pipeline, nullptr); + pipeline = VK_NULL_HANDLE; + } + if (pipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + pipelineLayout = VK_NULL_HANDLE; + } + if (dynamicVB != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, dynamicVB, dynamicVBAlloc); + dynamicVB = VK_NULL_HANDLE; + dynamicVBAlloc = VK_NULL_HANDLE; + } } + + vkCtx = nullptr; + particles.clear(); + particlePositions.clear(); } } // namespace rendering diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 0a1af80a..f7263095 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -1,13 +1,17 @@ #include "rendering/wmo_renderer.hpp" #include "rendering/m2_renderer.hpp" -#include "rendering/texture.hpp" -#include "rendering/shader.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_texture.hpp" +#include "rendering/vk_buffer.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_utils.hpp" +#include "rendering/vk_frame_data.hpp" #include "rendering/camera.hpp" #include "rendering/frustum.hpp" #include "pipeline/wmo_loader.hpp" #include "pipeline/asset_manager.hpp" #include "core/logger.hpp" -#include #include #include #include @@ -36,175 +40,179 @@ WMORenderer::~WMORenderer() { shutdown(); } -bool WMORenderer::initialize(pipeline::AssetManager* assets) { +bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, + pipeline::AssetManager* assets) { if (initialized_) { assetManager = assets; return true; } - core::Logger::getInstance().info("Initializing WMO renderer..."); + core::Logger::getInstance().info("Initializing WMO renderer (Vulkan)..."); + vkCtx_ = ctx; assetManager = assets; - numCullThreads_ = std::min(4u, std::max(1u, std::thread::hardware_concurrency() - 1)); - - // Create WMO shader with texture support - const char* vertexSrc = R"( - #version 330 core - layout (location = 0) in vec3 aPos; - layout (location = 1) in vec3 aNormal; - layout (location = 2) in vec2 aTexCoord; - layout (location = 3) in vec4 aColor; - - uniform mat4 uModel; - uniform mat4 uView; - uniform mat4 uProjection; - - out vec3 FragPos; - out vec3 Normal; - out vec2 TexCoord; - out vec4 VertexColor; - - void main() { - vec4 worldPos = uModel * vec4(aPos, 1.0); - FragPos = worldPos.xyz; - // Use mat3(uModel) directly - avoids expensive inverse() per vertex - // This works correctly for uniform scale transforms - Normal = mat3(uModel) * aNormal; - TexCoord = aTexCoord; - VertexColor = aColor; - - gl_Position = uProjection * uView * worldPos; - } - )"; - - const char* fragmentSrc = R"( - #version 330 core - in vec3 FragPos; - in vec3 Normal; - in vec2 TexCoord; - in vec4 VertexColor; - - uniform vec3 uLightDir; - uniform vec3 uLightColor; - uniform float uSpecularIntensity; - uniform vec3 uViewPos; - uniform vec3 uAmbientColor; - uniform sampler2D uTexture; - uniform bool uHasTexture; - uniform bool uAlphaTest; - uniform bool uUnlit; - uniform bool uIsInterior; - - uniform vec3 uFogColor; - uniform float uFogStart; - uniform float uFogEnd; - - uniform sampler2DShadow uShadowMap; - uniform mat4 uLightSpaceMatrix; - uniform bool uShadowEnabled; - uniform float uShadowStrength; - - out vec4 FragColor; - - void main() { - // Sample texture or use vertex color - vec4 texColor; - float alpha = 1.0; - if (uHasTexture) { - texColor = texture(uTexture, TexCoord); - // Alpha test only for cutout materials (lattice, grating, etc.) - if (uAlphaTest && texColor.a < 0.5) discard; - alpha = texColor.a; - // Don't multiply texture by vertex color here - it zeros out black MOCV areas - // Vertex colors will be applied as AO modulation after lighting - } else { - // MOCV vertex color alpha is a lighting blend factor, not transparency - texColor = vec4(VertexColor.rgb, 1.0); - } - - // Unlit materials (windows, lamps) — emit texture color directly - if (uUnlit) { - // Apply fog only - float fogDist = length(uViewPos - FragPos); - float fogFactor = clamp((uFogEnd - fogDist) / (uFogEnd - uFogStart), 0.0, 1.0); - vec3 result = mix(uFogColor, texColor.rgb, fogFactor); - FragColor = vec4(result, alpha); - return; - } - - vec3 normal = normalize(Normal); - vec3 lightDir = normalize(uLightDir); - - vec3 litColor; - if (uIsInterior) { - // Interior: MOCV vertex colors are baked lighting. - // Use them directly as the light multiplier on the texture. - vec3 vertLight = VertexColor.rgb * 2.4 + 0.35; - // Subtle directional fill so geometry reads - float diff = max(dot(normal, lightDir), 0.0); - vertLight += diff * 0.10; - litColor = texColor.rgb * vertLight; - } else { - // Exterior: standard diffuse + specular lighting - vec3 ambient = uAmbientColor; - - float diff = max(dot(normal, lightDir), 0.0); - vec3 diffuse = diff * vec3(1.0); - - vec3 viewDir = normalize(uViewPos - FragPos); - vec3 halfDir = normalize(lightDir + viewDir); - float spec = pow(max(dot(normal, halfDir), 0.0), 32.0); - vec3 specular = spec * uLightColor * uSpecularIntensity; - - // Shadow mapping - float shadow = 1.0; - if (uShadowEnabled) { - vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0); - vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5; - if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) { - float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5)); - float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist); - float bias = max(0.005 * (1.0 - dot(normal, lightDir)), 0.001); - // Single hardware PCF tap — GL_LINEAR + compare mode gives 2×2 bilinear PCF for free - shadow = texture(uShadowMap, vec3(proj.xy, proj.z - bias)); - shadow = mix(1.0, shadow, coverageFade); - } - } - shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0)); - - litColor = (ambient + (diffuse + specular) * shadow) * texColor.rgb; - - // Apply vertex color as ambient occlusion (AO) with minimum to prevent blackout - // MOCV values of (0,0,0) are clamped to 0.5 to keep areas visible - vec3 ao = max(VertexColor.rgb, vec3(0.5)); - litColor *= ao; - } - - // Fog - float fogDist = length(uViewPos - FragPos); - float fogFactor = clamp((uFogEnd - fogDist) / (uFogEnd - uFogStart), 0.0, 1.0); - vec3 result = mix(uFogColor, litColor, fogFactor); - - FragColor = vec4(result, alpha); - } - )"; - - shader = std::make_unique(); - if (!shader->loadFromSource(vertexSrc, fragmentSrc)) { - core::Logger::getInstance().error("Failed to create WMO shader"); + if (!vkCtx_) { + core::Logger::getInstance().error("WMORenderer: null VkContext"); return false; } - // Create default white texture for fallback + numCullThreads_ = std::min(4u, std::max(1u, std::thread::hardware_concurrency() - 1)); + + VkDevice device = vkCtx_->getDevice(); + + // --- Create material descriptor set layout (set 1) --- + // binding 0: sampler2D (diffuse texture) + // binding 1: uniform buffer (WMOMaterial) + std::vector materialBindings(2); + materialBindings[0] = {}; + materialBindings[0].binding = 0; + materialBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + materialBindings[0].descriptorCount = 1; + materialBindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + materialBindings[1] = {}; + materialBindings[1].binding = 1; + materialBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + materialBindings[1].descriptorCount = 1; + materialBindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + materialSetLayout_ = createDescriptorSetLayout(device, materialBindings); + if (!materialSetLayout_) { + core::Logger::getInstance().error("WMORenderer: failed to create material set layout"); + return false; + } + + // --- Create descriptor pool --- + VkDescriptorPoolSize poolSizes[] = { + { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, MAX_MATERIAL_SETS }, + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, MAX_MATERIAL_SETS }, + }; + + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.maxSets = MAX_MATERIAL_SETS; + poolInfo.poolSizeCount = 2; + poolInfo.pPoolSizes = poolSizes; + + if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &materialDescPool_) != VK_SUCCESS) { + core::Logger::getInstance().error("WMORenderer: failed to create descriptor pool"); + return false; + } + + // --- Create pipeline layout --- + VkPushConstantRange pushRange{}; + pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + pushRange.offset = 0; + pushRange.size = sizeof(GPUPushConstants); + + std::vector setLayouts = { perFrameLayout, materialSetLayout_ }; + pipelineLayout_ = createPipelineLayout(device, setLayouts, { pushRange }); + if (!pipelineLayout_) { + core::Logger::getInstance().error("WMORenderer: failed to create pipeline layout"); + return false; + } + + // --- Load shaders --- + VkShaderModule vertShader, fragShader; + if (!vertShader.loadFromFile(device, "assets/shaders/wmo.vert.spv")) { + core::Logger::getInstance().error("WMORenderer: failed to load vertex shader"); + return false; + } + if (!fragShader.loadFromFile(device, "assets/shaders/wmo.frag.spv")) { + core::Logger::getInstance().error("WMORenderer: failed to load fragment shader"); + return false; + } + + // --- Vertex input --- + // WMO vertex: pos3 + normal3 + texCoord2 + color4 = 48 bytes + struct WMOVertexData { + glm::vec3 position; + glm::vec3 normal; + glm::vec2 texCoord; + glm::vec4 color; + }; + + VkVertexInputBindingDescription vertexBinding{}; + vertexBinding.binding = 0; + vertexBinding.stride = sizeof(WMOVertexData); + vertexBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector vertexAttribs(4); + vertexAttribs[0] = { 0, 0, VK_FORMAT_R32G32B32_SFLOAT, + static_cast(offsetof(WMOVertexData, position)) }; + vertexAttribs[1] = { 1, 0, VK_FORMAT_R32G32B32_SFLOAT, + static_cast(offsetof(WMOVertexData, normal)) }; + vertexAttribs[2] = { 2, 0, VK_FORMAT_R32G32_SFLOAT, + static_cast(offsetof(WMOVertexData, texCoord)) }; + vertexAttribs[3] = { 3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, + static_cast(offsetof(WMOVertexData, color)) }; + + // --- Build opaque pipeline --- + VkRenderPass mainPass = vkCtx_->getImGuiRenderPass(); + + opaquePipeline_ = PipelineBuilder() + .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({ vertexBinding }, vertexAttribs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(PipelineBuilder::blendDisabled()) + .setLayout(pipelineLayout_) + .setRenderPass(mainPass) + .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) + .build(device); + + if (!opaquePipeline_) { + core::Logger::getInstance().error("WMORenderer: failed to create opaque pipeline"); + vertShader.destroy(); + fragShader.destroy(); + return false; + } + + // --- Build transparent pipeline --- + transparentPipeline_ = PipelineBuilder() + .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({ vertexBinding }, vertexAttribs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setLayout(pipelineLayout_) + .setRenderPass(mainPass) + .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) + .build(device); + + if (!transparentPipeline_) { + core::Logger::getInstance().warning("WMORenderer: transparent pipeline not available"); + } + + // --- Build wireframe pipeline --- + wireframePipeline_ = PipelineBuilder() + .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({ vertexBinding }, vertexAttribs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_LINE, VK_CULL_MODE_NONE) + .setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(PipelineBuilder::blendDisabled()) + .setLayout(pipelineLayout_) + .setRenderPass(mainPass) + .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) + .build(device); + + if (!wireframePipeline_) { + core::Logger::getInstance().warning("WMORenderer: wireframe pipeline not available"); + } + + vertShader.destroy(); + fragShader.destroy(); + + // --- Create fallback white texture --- + whiteTexture_ = std::make_unique(); uint8_t whitePixel[4] = {255, 255, 255, 255}; - glGenTextures(1, &whiteTexture); - glBindTexture(GL_TEXTURE_2D, whiteTexture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, whitePixel); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glBindTexture(GL_TEXTURE_2D, 0); + whiteTexture_->upload(*vkCtx_, whitePixel, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false); + whiteTexture_->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR, + VK_SAMPLER_ADDRESS_MODE_REPEAT); - // Initialize occlusion query resources - initOcclusionResources(); - - core::Logger::getInstance().info("WMO renderer initialized"); + core::Logger::getInstance().info("WMO renderer initialized (Vulkan)"); initialized_ = true; return true; } @@ -212,47 +220,60 @@ bool WMORenderer::initialize(pipeline::AssetManager* assets) { void WMORenderer::shutdown() { core::Logger::getInstance().info("Shutting down WMO renderer..."); - // Free all GPU resources + if (!vkCtx_) { + loadedModels.clear(); + instances.clear(); + spatialGrid.clear(); + instanceIndexById.clear(); + initialized_ = false; + return; + } + + VkDevice device = vkCtx_->getDevice(); + VmaAllocator allocator = vkCtx_->getAllocator(); + + vkDeviceWaitIdle(device); + + // Free all GPU resources for loaded models for (auto& [id, model] : loadedModels) { for (auto& group : model.groups) { - if (group.vao != 0) glDeleteVertexArrays(1, &group.vao); - if (group.vbo != 0) glDeleteBuffers(1, &group.vbo); - if (group.ebo != 0) glDeleteBuffers(1, &group.ebo); + destroyGroupGPU(group); } } // Free cached textures for (auto& [path, entry] : textureCache) { - GLuint texId = entry.id; - if (texId != 0 && texId != whiteTexture) { - glDeleteTextures(1, &texId); - } + if (entry.texture) entry.texture->destroy(device, allocator); } textureCache.clear(); textureCacheBytes_ = 0; textureCacheCounter_ = 0; // Free white texture - if (whiteTexture != 0) { - glDeleteTextures(1, &whiteTexture); - whiteTexture = 0; - } + if (whiteTexture_) { whiteTexture_->destroy(device, allocator); whiteTexture_.reset(); } loadedModels.clear(); instances.clear(); spatialGrid.clear(); instanceIndexById.clear(); - shader.reset(); - // Free occlusion query resources - for (auto& [key, query] : occlusionQueries) { - glDeleteQueries(1, &query); - } - occlusionQueries.clear(); - occlusionResults.clear(); - if (bboxVao != 0) { glDeleteVertexArrays(1, &bboxVao); bboxVao = 0; } - if (bboxVbo != 0) { glDeleteBuffers(1, &bboxVbo); bboxVbo = 0; } - occlusionShader.reset(); + // Destroy pipelines + if (opaquePipeline_) { vkDestroyPipeline(device, opaquePipeline_, nullptr); opaquePipeline_ = VK_NULL_HANDLE; } + if (transparentPipeline_) { vkDestroyPipeline(device, transparentPipeline_, nullptr); transparentPipeline_ = VK_NULL_HANDLE; } + if (wireframePipeline_) { vkDestroyPipeline(device, wireframePipeline_, nullptr); wireframePipeline_ = VK_NULL_HANDLE; } + if (pipelineLayout_) { vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); pipelineLayout_ = VK_NULL_HANDLE; } + if (materialDescPool_) { vkDestroyDescriptorPool(device, materialDescPool_, nullptr); materialDescPool_ = VK_NULL_HANDLE; } + if (materialSetLayout_) { vkDestroyDescriptorSetLayout(device, materialSetLayout_, nullptr); materialSetLayout_ = VK_NULL_HANDLE; } + + // Destroy shadow resources + if (shadowPipeline_) { vkDestroyPipeline(device, shadowPipeline_, nullptr); shadowPipeline_ = VK_NULL_HANDLE; } + if (shadowPipelineLayout_) { vkDestroyPipelineLayout(device, shadowPipelineLayout_, nullptr); shadowPipelineLayout_ = VK_NULL_HANDLE; } + if (shadowParamsPool_) { vkDestroyDescriptorPool(device, shadowParamsPool_, nullptr); shadowParamsPool_ = VK_NULL_HANDLE; } + if (shadowParamsLayout_) { vkDestroyDescriptorSetLayout(device, shadowParamsLayout_, nullptr); shadowParamsLayout_ = VK_NULL_HANDLE; } + if (shadowParamsUBO_) { vmaDestroyBuffer(allocator, shadowParamsUBO_, shadowParamsAlloc_); shadowParamsUBO_ = VK_NULL_HANDLE; } + + vkCtx_ = nullptr; + initialized_ = false; } bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { @@ -266,12 +287,12 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { if (existingIt != loadedModels.end()) { // If a model was first loaded while texture resolution failed (or before // assets were fully available), it can remain permanently white because - // merged batches cache texture IDs at load time. Do a one-time reload for + // merged batches cache texture pointers at load time. Do a one-time reload for // models that have texture paths but no resolved non-white textures. if (assetManager && !model.textures.empty()) { bool hasResolvedTexture = false; - for (GLuint texId : existingIt->second.textures) { - if (texId != 0 && texId != whiteTexture) { + for (VkTexture* tex : existingIt->second.textures) { + if (tex != nullptr && tex != whiteTexture_.get()) { hasResolvedTexture = true; break; } @@ -313,8 +334,8 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { for (size_t i = 0; i < model.textures.size(); i++) { const auto& texPath = model.textures[i]; core::Logger::getInstance().debug(" Loading texture ", i, ": ", texPath); - GLuint texId = loadTexture(texPath); - modelData.textures.push_back(texId); + VkTexture* tex = loadTexture(texPath); + modelData.textures.push_back(tex); } core::Logger::getInstance().debug(" Loaded ", modelData.textures.size(), " textures for WMO"); } @@ -393,21 +414,33 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { // Build pre-merged batches for each group (texture-sorted for efficient rendering) for (auto& groupRes : modelData.groups) { - std::unordered_map batchMap; + // Use pointer value as key for batching + struct BatchKey { + uintptr_t texPtr; + bool alphaTest; + bool unlit; + bool operator==(const BatchKey& o) const { return texPtr == o.texPtr && alphaTest == o.alphaTest && unlit == o.unlit; } + }; + struct BatchKeyHash { + size_t operator()(const BatchKey& k) const { + return std::hash()(k.texPtr) ^ (std::hash()(k.alphaTest) << 1) ^ (std::hash()(k.unlit) << 2); + } + }; + std::unordered_map batchMap; for (const auto& batch : groupRes.batches) { - GLuint texId = whiteTexture; + VkTexture* tex = whiteTexture_.get(); bool hasTexture = false; if (batch.materialId < modelData.materialTextureIndices.size()) { uint32_t texIndex = modelData.materialTextureIndices[batch.materialId]; if (texIndex < modelData.textures.size()) { - texId = modelData.textures[texIndex]; - hasTexture = (texId != 0 && texId != whiteTexture); + tex = modelData.textures[texIndex]; + hasTexture = (tex != nullptr && tex != whiteTexture_.get()); + if (!tex) tex = whiteTexture_.get(); } } - bool alphaTest = false; uint32_t blendMode = 0; if (batch.materialId < modelData.materialBlendModes.size()) { @@ -426,26 +459,76 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { // 0x20 = F_SIDN (night sky window), 0x40 = F_WINDOW if (matFlags & 0x60) continue; - // Merge key: texture ID + alphaTest + unlit (unlit batches must not merge with lit) - uint64_t key = (static_cast(texId) << 2) - | (alphaTest ? 1ULL : 0ULL) - | (unlit ? 2ULL : 0ULL); + BatchKey key{ reinterpret_cast(tex), alphaTest, unlit }; auto& mb = batchMap[key]; - if (mb.counts.empty()) { - mb.texId = texId; + if (mb.draws.empty()) { + mb.texture = tex; mb.hasTexture = hasTexture; mb.alphaTest = alphaTest; mb.unlit = unlit; - mb.blendMode = blendMode; + mb.isTransparent = (blendMode >= 2); } - mb.counts.push_back(static_cast(batch.indexCount)); - mb.offsets.push_back(reinterpret_cast(batch.startIndex * sizeof(uint16_t))); + GroupResources::MergedBatch::DrawRange dr; + dr.firstIndex = batch.startIndex; + dr.indexCount = batch.indexCount; + mb.draws.push_back(dr); } + // Allocate descriptor sets and UBOs for each merged batch groupRes.mergedBatches.reserve(batchMap.size()); bool anyTextured = false; + bool isInterior = (groupRes.groupFlags & 0x2000) != 0; for (auto& [key, mb] : batchMap) { if (mb.hasTexture) anyTextured = true; + + // Create material UBO + VmaAllocator allocator = vkCtx_->getAllocator(); + AllocatedBuffer matBuf = createBuffer(allocator, sizeof(WMOMaterialUBO), + VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, + VMA_MEMORY_USAGE_CPU_TO_GPU); + mb.materialUBO = matBuf.buffer; + mb.materialUBOAlloc = matBuf.allocation; + + // Write material params + WMOMaterialUBO matData{}; + matData.hasTexture = mb.hasTexture ? 1 : 0; + matData.alphaTest = mb.alphaTest ? 1 : 0; + matData.unlit = mb.unlit ? 1 : 0; + matData.isInterior = isInterior ? 1 : 0; + matData.specularIntensity = 0.5f; + if (matBuf.info.pMappedData) { + memcpy(matBuf.info.pMappedData, &matData, sizeof(matData)); + } + + // Allocate and write descriptor set + mb.materialSet = allocateMaterialSet(); + if (mb.materialSet) { + VkTexture* texToUse = mb.texture ? mb.texture : whiteTexture_.get(); + VkDescriptorImageInfo imgInfo = texToUse->descriptorInfo(); + + VkDescriptorBufferInfo bufInfo{}; + bufInfo.buffer = mb.materialUBO; + bufInfo.offset = 0; + bufInfo.range = sizeof(WMOMaterialUBO); + + VkWriteDescriptorSet writes[2] = {}; + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].dstSet = mb.materialSet; + writes[0].dstBinding = 0; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[0].descriptorCount = 1; + writes[0].pImageInfo = &imgInfo; + + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].dstSet = mb.materialSet; + writes[1].dstBinding = 1; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writes[1].descriptorCount = 1; + writes[1].pBufferInfo = &bufInfo; + + vkUpdateDescriptorSets(vkCtx_->getDevice(), 2, writes, 0, nullptr); + } + groupRes.mergedBatches.push_back(std::move(mb)); } groupRes.allUntextured = !anyTextured && !groupRes.mergedBatches.empty(); @@ -544,9 +627,7 @@ void WMORenderer::unloadModel(uint32_t id) { // Free GPU resources for (auto& group : it->second.groups) { - if (group.vao != 0) glDeleteVertexArrays(1, &group.vao); - if (group.vbo != 0) glDeleteBuffers(1, &group.vbo); - if (group.ebo != 0) glDeleteBuffers(1, &group.ebo); + destroyGroupGPU(group); } loadedModels.erase(it); @@ -792,20 +873,7 @@ void WMORenderer::clearCollisionFocus() { collisionFocusEnabled = false; } -void WMORenderer::setLighting(const float lightDirIn[3], const float lightColorIn[3], - const float ambientColorIn[3]) { - lightDir[0] = lightDirIn[0]; - lightDir[1] = lightDirIn[1]; - lightDir[2] = lightDirIn[2]; - - lightColor[0] = lightColorIn[0]; - lightColor[1] = lightColorIn[1]; - lightColor[2] = lightColorIn[2]; - - ambientColor[0] = ambientColorIn[0]; - ambientColor[1] = ambientColorIn[1]; - ambientColor[2] = ambientColorIn[2]; -} +// setLighting is now a no-op (lighting is in the per-frame UBO) void WMORenderer::resetQueryStats() { queryTimeMs = 0.0; @@ -1017,77 +1085,23 @@ void WMORenderer::gatherCandidates(const glm::vec3& queryMin, const glm::vec3& q } } -void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection) { - if (!shader || instances.empty()) { +void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera) { + if (!opaquePipeline_ || instances.empty()) { lastDrawCalls = 0; return; } lastDrawCalls = 0; - // Set shader uniforms - shader->use(); - shader->setUniform("uView", view); - shader->setUniform("uProjection", projection); - shader->setUniform("uViewPos", camera.getPosition()); - shader->setUniform("uLightDir", glm::vec3(lightDir[0], lightDir[1], lightDir[2])); - shader->setUniform("uLightColor", glm::vec3(lightColor[0], lightColor[1], lightColor[2])); - shader->setUniform("uSpecularIntensity", 0.5f); - shader->setUniform("uAmbientColor", glm::vec3(ambientColor[0], ambientColor[1], ambientColor[2])); - shader->setUniform("uFogColor", fogColor); - shader->setUniform("uFogStart", fogStart); - shader->setUniform("uFogEnd", fogEnd); - shader->setUniform("uShadowEnabled", shadowEnabled ? 1 : 0); - shader->setUniform("uShadowStrength", 0.68f); - if (shadowEnabled) { - shader->setUniform("uLightSpaceMatrix", lightSpaceMatrix); - glActiveTexture(GL_TEXTURE7); - glBindTexture(GL_TEXTURE_2D, shadowDepthTex); - shader->setUniform("uShadowMap", 7); - } - - // Set up texture unit 0 for diffuse textures (set once per frame) - glActiveTexture(GL_TEXTURE0); - shader->setUniform("uTexture", 0); - - // Initialize new uniforms to defaults - shader->setUniform("uUnlit", false); - shader->setUniform("uIsInterior", false); - - // Enable wireframe if requested - if (wireframeMode) { - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); - } - - // WMOs are opaque — ensure blending is off (alpha test via discard in shader) - glDisable(GL_BLEND); - - // Disable backface culling for WMOs (some faces may have wrong winding) - glDisable(GL_CULL_FACE); - // Extract frustum planes for proper culling + glm::mat4 viewProj = camera.getProjectionMatrix() * camera.getViewMatrix(); Frustum frustum; - frustum.extractFromMatrix(projection * view); + frustum.extractFromMatrix(viewProj); lastPortalCulledGroups = 0; lastDistanceCulledGroups = 0; - lastOcclusionCulledGroups = 0; - - // Collect occlusion query results from previous frame (non-blocking) - if (occlusionCulling) { - for (auto& [queryKey, query] : occlusionQueries) { - GLuint available = 0; - glGetQueryObjectuiv(query, GL_QUERY_RESULT_AVAILABLE, &available); - if (available) { - GLuint result = 0; - glGetQueryObjectuiv(query, GL_QUERY_RESULT, &result); - occlusionResults[queryKey] = (result > 0); - } - } - } // ── Phase 1: Parallel visibility culling ────────────────────────── - // Build list of instances for draw list generation. std::vector visibleInstances; visibleInstances.reserve(instances.size()); for (size_t i = 0; i < instances.size(); ++i) { @@ -1097,11 +1111,8 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: visibleInstances.push_back(i); } - // Per-instance cull lambda — produces an InstanceDrawList for one instance. - // Reads only const data; each invocation writes to its own output. glm::vec3 camPos = camera.getPosition(); bool doPortalCull = portalCulling; - bool doOcclusionCull = occlusionCulling; bool doFrustumCull = false; // Temporarily disabled: can over-cull world WMOs bool doDistanceCull = distanceCulling; @@ -1125,19 +1136,12 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: } for (size_t gi = 0; gi < model.groups.size(); ++gi) { - // Portal culling if (usePortalCulling && portalVisibleGroups.find(static_cast(gi)) == portalVisibleGroups.end()) { result.portalCulled++; continue; } - // Occlusion culling (reads previous-frame results, read-only map) - if (doOcclusionCull && isGroupOccluded(instance.id, static_cast(gi))) { - result.occlusionCulled++; - continue; - } - if (gi < instance.worldGroupBounds.size()) { const auto& [gMin, gMax] = instance.worldGroupBounds[gi]; @@ -1150,7 +1154,6 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: } } - // Frustum culling if (doFrustumCull && !frustum.intersectsAABB(gMin, gMax)) continue; } @@ -1170,7 +1173,6 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: const size_t chunkSize = visibleInstances.size() / numThreads; const size_t remainder = visibleInstances.size() % numThreads; - // Each future returns a vector of InstanceDrawList for its chunk. std::vector>> futures; futures.reserve(numThreads); @@ -1198,70 +1200,281 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: drawLists.push_back(cullInstance(idx)); } - // ── Phase 2: Sequential GL draw ──────────────────────────────── + // ── Phase 2: Vulkan draw ──────────────────────────────── + // Select pipeline based on wireframe mode + VkPipeline activePipeline = (wireframeMode && wireframePipeline_) ? wireframePipeline_ : opaquePipeline_; + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, activePipeline); + + // Bind per-frame descriptor set (set 0) + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, + 0, 1, &perFrameSet, 0, nullptr); + + bool inTransparentPipeline = false; + for (const auto& dl : drawLists) { if (dl.instanceIndex >= instances.size()) continue; const auto& instance = instances[dl.instanceIndex]; auto modelIt = loadedModels.find(instance.modelId); if (modelIt == loadedModels.end()) continue; const ModelData& model = modelIt->second; - // Occlusion query pre-pass (GL calls — must be main thread) - if (occlusionCulling && occlusionShader && bboxVao != 0) { - runOcclusionQueries(instance, model, view, projection); - shader->use(); - } - shader->setUniform("uModel", instance.modelMatrix); + // Push model matrix + GPUPushConstants push{}; + push.model = instance.modelMatrix; + vkCmdPushConstants(cmd, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, + 0, sizeof(GPUPushConstants), &push); // Render visible groups for (uint32_t gi : dl.visibleGroups) { const auto& group = model.groups[gi]; - // Only skip antiportal geometry. Other flags vary across assets and can - // incorrectly hide valid world building groups. - if (group.groupFlags & 0x4000000) { - continue; + // Only skip antiportal geometry + if (group.groupFlags & 0x4000000) continue; + + // Bind vertex + index buffers + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &group.vertexBuffer, &offset); + vkCmdBindIndexBuffer(cmd, group.indexBuffer, 0, VK_INDEX_TYPE_UINT16); + + // Render each merged batch + for (const auto& mb : group.mergedBatches) { + if (!mb.materialSet) continue; + + // Switch pipeline for transparent batches + if (mb.isTransparent && !inTransparentPipeline && transparentPipeline_) { + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, transparentPipeline_); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, + 0, 1, &perFrameSet, 0, nullptr); + vkCmdPushConstants(cmd, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, + 0, sizeof(GPUPushConstants), &push); + inTransparentPipeline = true; + } else if (!mb.isTransparent && inTransparentPipeline) { + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, activePipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, + 0, 1, &perFrameSet, 0, nullptr); + vkCmdPushConstants(cmd, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, + 0, sizeof(GPUPushConstants), &push); + inTransparentPipeline = false; + } + + // Bind material descriptor set (set 1) + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, + 1, 1, &mb.materialSet, 0, nullptr); + + // Issue draw calls for each range in this merged batch + for (const auto& dr : mb.draws) { + vkCmdDrawIndexed(cmd, dr.indexCount, 1, dr.firstIndex, 0, 0); + lastDrawCalls++; + } } - - // Do not globally cull untextured groups: some valid world WMOs can - // temporarily resolve to fallback textures. Render geometry anyway. - - - renderGroup(group, model, instance.modelMatrix, view, projection); } lastPortalCulledGroups += dl.portalCulled; lastDistanceCulledGroups += dl.distanceCulled; - lastOcclusionCulledGroups += dl.occlusionCulled; } - - // Restore polygon mode - if (wireframeMode) { - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); - } - - // Re-enable backface culling - glEnable(GL_CULL_FACE); } -void WMORenderer::renderShadow(const glm::mat4& lightView, const glm::mat4& lightProj, Shader& shadowShader) { - if (instances.empty()) return; - Frustum frustum; - frustum.extractFromMatrix(lightProj * lightView); +bool WMORenderer::initializeShadow(VkRenderPass shadowRenderPass) { + if (!vkCtx_ || shadowRenderPass == VK_NULL_HANDLE) return false; + VkDevice device = vkCtx_->getDevice(); + + // ShadowParams UBO: useBones, useTexture, alphaTest, foliageSway, windTime, foliageMotionDamp + struct ShadowParamsUBO { + int32_t useBones = 0; + int32_t useTexture = 0; + int32_t alphaTest = 0; + int32_t foliageSway = 0; + float windTime = 0.0f; + float foliageMotionDamp = 1.0f; + }; + + // Create ShadowParams UBO + VkBufferCreateInfo bufCI{}; + bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufCI.size = sizeof(ShadowParamsUBO); + bufCI.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + VmaAllocationCreateInfo allocCI{}; + allocCI.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + allocCI.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + VmaAllocationInfo allocInfo{}; + if (vmaCreateBuffer(vkCtx_->getAllocator(), &bufCI, &allocCI, + &shadowParamsUBO_, &shadowParamsAlloc_, &allocInfo) != VK_SUCCESS) { + core::Logger::getInstance().error("WMORenderer: failed to create shadow params UBO"); + return false; + } + ShadowParamsUBO defaultParams{}; + std::memcpy(allocInfo.pMappedData, &defaultParams, sizeof(defaultParams)); + + // Create descriptor set layout: binding 0 = sampler2D (texture), binding 1 = ShadowParams UBO + VkDescriptorSetLayoutBinding layoutBindings[2]{}; + layoutBindings[0].binding = 0; + layoutBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + layoutBindings[0].descriptorCount = 1; + layoutBindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + layoutBindings[1].binding = 1; + layoutBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + layoutBindings[1].descriptorCount = 1; + layoutBindings[1].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + VkDescriptorSetLayoutCreateInfo layoutCI{}; + layoutCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + layoutCI.bindingCount = 2; + layoutCI.pBindings = layoutBindings; + if (vkCreateDescriptorSetLayout(device, &layoutCI, nullptr, &shadowParamsLayout_) != VK_SUCCESS) { + core::Logger::getInstance().error("WMORenderer: failed to create shadow params layout"); + return false; + } + + // Create descriptor pool + VkDescriptorPoolSize poolSizes[2]{}; + poolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + poolSizes[0].descriptorCount = 1; + poolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + poolSizes[1].descriptorCount = 1; + VkDescriptorPoolCreateInfo poolCI{}; + poolCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolCI.maxSets = 1; + poolCI.poolSizeCount = 2; + poolCI.pPoolSizes = poolSizes; + if (vkCreateDescriptorPool(device, &poolCI, nullptr, &shadowParamsPool_) != VK_SUCCESS) { + core::Logger::getInstance().error("WMORenderer: failed to create shadow params pool"); + return false; + } + + // Allocate descriptor set + VkDescriptorSetAllocateInfo setAlloc{}; + setAlloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + setAlloc.descriptorPool = shadowParamsPool_; + setAlloc.descriptorSetCount = 1; + setAlloc.pSetLayouts = &shadowParamsLayout_; + if (vkAllocateDescriptorSets(device, &setAlloc, &shadowParamsSet_) != VK_SUCCESS) { + core::Logger::getInstance().error("WMORenderer: failed to allocate shadow params set"); + return false; + } + + // Write descriptors + VkDescriptorBufferInfo bufInfo{}; + bufInfo.buffer = shadowParamsUBO_; + bufInfo.offset = 0; + bufInfo.range = sizeof(ShadowParamsUBO); + + VkWriteDescriptorSet writes[2]{}; + // binding 0: texture (use white fallback so binding is valid; useTexture=0 so it's not sampled) + VkDescriptorImageInfo imgInfo{}; + imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + imgInfo.imageView = whiteTexture_->getImageView(); + imgInfo.sampler = whiteTexture_->getSampler(); + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].dstSet = shadowParamsSet_; + writes[0].dstBinding = 0; + writes[0].descriptorCount = 1; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[0].pImageInfo = &imgInfo; + // binding 1: params UBO + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].dstSet = shadowParamsSet_; + writes[1].dstBinding = 1; + writes[1].descriptorCount = 1; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writes[1].pBufferInfo = &bufInfo; + vkUpdateDescriptorSets(device, 2, writes, 0, nullptr); + + // Create shadow pipeline layout: set 1 = shadowParamsLayout_, push constants = 128 bytes + VkPushConstantRange pc{}; + pc.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + pc.offset = 0; + pc.size = 128; // lightSpaceMatrix (64) + model (64) + shadowPipelineLayout_ = createPipelineLayout(device, {shadowParamsLayout_}, {pc}); + if (!shadowPipelineLayout_) { + core::Logger::getInstance().error("WMORenderer: failed to create shadow pipeline layout"); + return false; + } + + // Load shadow shaders + VkShaderModule vertShader, fragShader; + if (!vertShader.loadFromFile(device, "assets/shaders/shadow.vert.spv")) { + core::Logger::getInstance().error("WMORenderer: failed to load shadow vertex shader"); + return false; + } + if (!fragShader.loadFromFile(device, "assets/shaders/shadow.frag.spv")) { + core::Logger::getInstance().error("WMORenderer: failed to load shadow fragment shader"); + return false; + } + + // WMO vertex layout: pos(loc0,off0) normal(loc1,off12) texCoord(loc2,off24) color(loc3,off32), stride=48 + // Shadow shader locations: 0=aPos, 1=aTexCoord, 2=aBoneWeights, 3=aBoneIndicesF + // useBones=0 so locations 2,3 are never read; we alias them to existing data offsets + VkVertexInputBindingDescription vertBind{}; + vertBind.binding = 0; + vertBind.stride = 48; + vertBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + std::vector vertAttrs = { + {0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, // aPos -> position + {1, 0, VK_FORMAT_R32G32_SFLOAT, 24}, // aTexCoord -> texCoord + {2, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 32}, // aBoneWeights (aliased to color, not used) + {3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 32}, // aBoneIndicesF (aliased to color, not used) + }; + + shadowPipeline_ = PipelineBuilder() + .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({vertBind}, vertAttrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_FRONT_BIT) + .setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL) + .setDepthBias(2.0f, 4.0f) + .setNoColorAttachment() + .setLayout(shadowPipelineLayout_) + .setRenderPass(shadowRenderPass) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device); + + vertShader.destroy(); + fragShader.destroy(); + + if (!shadowPipeline_) { + core::Logger::getInstance().error("WMORenderer: failed to create shadow pipeline"); + return false; + } + core::Logger::getInstance().info("WMORenderer shadow pipeline initialized"); + return true; +} + +void WMORenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix) { + if (!shadowPipeline_ || !shadowParamsSet_) return; + if (instances.empty() || loadedModels.empty()) return; + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipeline_); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipelineLayout_, + 0, 1, &shadowParamsSet_, 0, nullptr); + + struct ShadowPush { glm::mat4 lightSpaceMatrix; glm::mat4 model; }; + for (const auto& instance : instances) { auto modelIt = loadedModels.find(instance.modelId); if (modelIt == loadedModels.end()) continue; - if (frustumCulling) { - glm::vec3 instMin = instance.worldBoundsMin - glm::vec3(0.5f); - glm::vec3 instMax = instance.worldBoundsMax + glm::vec3(0.5f); - if (!frustum.intersectsAABB(instMin, instMax)) continue; - } const ModelData& model = modelIt->second; - shadowShader.setUniform("uModel", instance.modelMatrix); + + ShadowPush push{lightSpaceMatrix, instance.modelMatrix}; + vkCmdPushConstants(cmd, shadowPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, + 0, 128, &push); + for (const auto& group : model.groups) { - glBindVertexArray(group.vao); - glDrawElements(GL_TRIANGLES, group.indexCount, GL_UNSIGNED_SHORT, 0); - glBindVertexArray(0); + if (group.vertexBuffer == VK_NULL_HANDLE || group.indexBuffer == VK_NULL_HANDLE) continue; + + // Skip antiportal geometry + if (group.groupFlags & 0x4000000) continue; + + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &group.vertexBuffer, &offset); + vkCmdBindIndexBuffer(cmd, group.indexBuffer, 0, VK_INDEX_TYPE_UINT16); + + // Draw only opaque batches (skip transparent) + for (const auto& mb : group.mergedBatches) { + if (mb.isTransparent) continue; + for (const auto& dr : mb.draws) { + vkCmdDrawIndexed(cmd, dr.indexCount, 1, dr.firstIndex, 0, 0); + } + } } } } @@ -1309,45 +1522,19 @@ bool WMORenderer::createGroupResources(const pipeline::WMOGroup& group, GroupRes vertices.push_back(vd); } - // Create VAO/VBO/EBO - glGenVertexArrays(1, &resources.vao); - glGenBuffers(1, &resources.vbo); - glGenBuffers(1, &resources.ebo); + // Upload vertex buffer to GPU + AllocatedBuffer vertBuf = uploadBuffer(*vkCtx_, vertices.data(), + vertices.size() * sizeof(VertexData), + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + resources.vertexBuffer = vertBuf.buffer; + resources.vertexAlloc = vertBuf.allocation; - glBindVertexArray(resources.vao); - - // Upload vertex data - glBindBuffer(GL_ARRAY_BUFFER, resources.vbo); - glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(VertexData), - vertices.data(), GL_STATIC_DRAW); - - // Upload index data - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, resources.ebo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, group.indices.size() * sizeof(uint16_t), - group.indices.data(), GL_STATIC_DRAW); - - // Vertex attributes - // Position - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(VertexData), - (void*)offsetof(VertexData, position)); - - // Normal - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(VertexData), - (void*)offsetof(VertexData, normal)); - - // TexCoord - glEnableVertexAttribArray(2); - glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(VertexData), - (void*)offsetof(VertexData, texCoord)); - - // Color - glEnableVertexAttribArray(3); - glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(VertexData), - (void*)offsetof(VertexData, color)); - - glBindVertexArray(0); + // Upload index buffer to GPU + AllocatedBuffer idxBuf = uploadBuffer(*vkCtx_, group.indices.data(), + group.indices.size() * sizeof(uint16_t), + VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + resources.indexBuffer = idxBuf.buffer; + resources.indexAlloc = idxBuf.allocation; // Store collision geometry for floor raycasting resources.collisionVertices.reserve(group.vertices.size()); @@ -1390,60 +1577,48 @@ bool WMORenderer::createGroupResources(const pipeline::WMOGroup& group, GroupRes return true; } -void WMORenderer::renderGroup(const GroupResources& group, [[maybe_unused]] const ModelData& model, - [[maybe_unused]] const glm::mat4& modelMatrix, - [[maybe_unused]] const glm::mat4& view, - [[maybe_unused]] const glm::mat4& projection) { - glBindVertexArray(group.vao); +// renderGroup removed — draw calls are inlined in render() - // Set interior flag once per group (0x2000 = interior) - bool isInterior = (group.groupFlags & 0x2000) != 0; - shader->setUniform("uIsInterior", isInterior); +void WMORenderer::destroyGroupGPU(GroupResources& group) { + if (!vkCtx_) return; + VmaAllocator allocator = vkCtx_->getAllocator(); - // Use pre-computed merged batches (built at load time) - // Track state within this draw call only. - GLuint lastBoundTex = std::numeric_limits::max(); - bool lastHasTexture = false; - bool lastAlphaTest = false; - bool lastUnlit = false; - bool firstBatch = true; - - for (const auto& mb : group.mergedBatches) { - if (firstBatch || mb.texId != lastBoundTex) { - glBindTexture(GL_TEXTURE_2D, mb.texId); - lastBoundTex = mb.texId; - } - if (firstBatch || mb.hasTexture != lastHasTexture) { - shader->setUniform("uHasTexture", mb.hasTexture); - lastHasTexture = mb.hasTexture; - } - if (firstBatch || mb.alphaTest != lastAlphaTest) { - shader->setUniform("uAlphaTest", mb.alphaTest); - lastAlphaTest = mb.alphaTest; - } - if (firstBatch || mb.unlit != lastUnlit) { - shader->setUniform("uUnlit", mb.unlit); - lastUnlit = mb.unlit; - } - firstBatch = false; - - // Enable alpha blending for translucent materials (blendMode >= 2) - bool needsBlend = (mb.blendMode >= 2); - if (needsBlend) { - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - } - - glMultiDrawElements(GL_TRIANGLES, mb.counts.data(), GL_UNSIGNED_SHORT, - mb.offsets.data(), static_cast(mb.counts.size())); - lastDrawCalls++; - - if (needsBlend) { - glDisable(GL_BLEND); - } + if (group.vertexBuffer) { + vmaDestroyBuffer(allocator, group.vertexBuffer, group.vertexAlloc); + group.vertexBuffer = VK_NULL_HANDLE; + group.vertexAlloc = VK_NULL_HANDLE; + } + if (group.indexBuffer) { + vmaDestroyBuffer(allocator, group.indexBuffer, group.indexAlloc); + group.indexBuffer = VK_NULL_HANDLE; + group.indexAlloc = VK_NULL_HANDLE; } - glBindVertexArray(0); + // Destroy material UBOs (descriptor sets are freed when pool is reset/destroyed) + for (auto& mb : group.mergedBatches) { + if (mb.materialUBO) { + vmaDestroyBuffer(allocator, mb.materialUBO, mb.materialUBOAlloc); + mb.materialUBO = VK_NULL_HANDLE; + mb.materialUBOAlloc = VK_NULL_HANDLE; + } + } +} + +VkDescriptorSet WMORenderer::allocateMaterialSet() { + if (!materialDescPool_ || !materialSetLayout_) return VK_NULL_HANDLE; + + VkDescriptorSetAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = materialDescPool_; + allocInfo.descriptorSetCount = 1; + allocInfo.pSetLayouts = &materialSetLayout_; + + VkDescriptorSet set = VK_NULL_HANDLE; + if (vkAllocateDescriptorSets(vkCtx_->getDevice(), &allocInfo, &set) != VK_SUCCESS) { + core::Logger::getInstance().warning("WMORenderer: failed to allocate material descriptor set"); + return VK_NULL_HANDLE; + } + return set; } bool WMORenderer::isGroupVisible(const GroupResources& group, const glm::mat4& modelMatrix, @@ -1607,9 +1782,9 @@ void WMORenderer::WMOInstance::updateModelMatrix() { invModelMatrix = glm::inverse(modelMatrix); } -GLuint WMORenderer::loadTexture(const std::string& path) { - if (!assetManager) { - return whiteTexture; +VkTexture* WMORenderer::loadTexture(const std::string& path) { + if (!assetManager || !vkCtx_) { + return whiteTexture_.get(); } auto normalizeKey = [](std::string key) { @@ -1625,7 +1800,7 @@ GLuint WMORenderer::loadTexture(const std::string& path) { key = normalizeKey(key); if (key.rfind(".\\", 0) == 0) key = key.substr(2); while (!key.empty() && key.front() == '\\') key.erase(key.begin()); - if (key.empty()) return whiteTexture; + if (key.empty()) return whiteTexture_.get(); auto hasKnownExt = [](const std::string& p) { if (p.size() < 4) return false; @@ -1679,7 +1854,7 @@ GLuint WMORenderer::loadTexture(const std::string& path) { auto it = textureCache.find(c); if (it != textureCache.end()) { it->second.lastUse = ++textureCacheCounter_; - return it->second.id; + return it->second.texture.get(); } } @@ -1698,46 +1873,37 @@ GLuint WMORenderer::loadTexture(const std::string& path) { // Do not cache failures as white. MPQ reads can fail transiently // during streaming/contention, and caching white here permanently // poisons the texture for this session. - return whiteTexture; + return whiteTexture_.get(); } core::Logger::getInstance().debug("WMO texture: ", path, " size=", blp.width, "x", blp.height); - // Create OpenGL texture - GLuint textureID; - glGenTextures(1, &textureID); - glBindTexture(GL_TEXTURE_2D, textureID); - - // Upload texture data (BLP loader outputs RGBA8) - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, - blp.width, blp.height, 0, - GL_RGBA, GL_UNSIGNED_BYTE, blp.data.data()); - - // Set texture parameters with mipmaps - glGenerateMipmap(GL_TEXTURE_2D); - applyAnisotropicFiltering(); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); - - glBindTexture(GL_TEXTURE_2D, 0); + // Create Vulkan texture + auto texture = std::make_unique(); + if (!texture->upload(*vkCtx_, blp.data.data(), blp.width, blp.height, + VK_FORMAT_R8G8B8A8_UNORM, true)) { + core::Logger::getInstance().warning("WMO: Failed to upload texture to GPU: ", path); + return whiteTexture_.get(); + } + texture->createSampler(vkCtx_->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR, + VK_SAMPLER_ADDRESS_MODE_REPEAT); // Cache it TextureCacheEntry e; - e.id = textureID; + VkTexture* rawPtr = texture.get(); size_t base = static_cast(blp.width) * static_cast(blp.height) * 4ull; e.approxBytes = base + (base / 3); e.lastUse = ++textureCacheCounter_; + e.texture = std::move(texture); textureCacheBytes_ += e.approxBytes; if (!resolvedKey.empty()) { - textureCache[resolvedKey] = e; + textureCache[resolvedKey] = std::move(e); } else { - textureCache[key] = e; + textureCache[key] = std::move(e); } core::Logger::getInstance().debug("WMO: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")"); - return textureID; + return rawPtr; } // Ray-AABB intersection (slab method) @@ -2710,123 +2876,7 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3 return closestHit; } -void WMORenderer::initOcclusionResources() { - // Simple vertex shader for bounding box rendering - const char* occVertSrc = R"( - #version 330 core - layout(location = 0) in vec3 aPos; - uniform mat4 uMVP; - void main() { - gl_Position = uMVP * vec4(aPos, 1.0); - } - )"; - - // Fragment shader that writes nothing (depth-only) - const char* occFragSrc = R"( - #version 330 core - void main() { - // No color output - depth only - } - )"; - - occlusionShader = std::make_unique(); - if (!occlusionShader->loadFromSource(occVertSrc, occFragSrc)) { - core::Logger::getInstance().warning("Failed to create occlusion shader"); - occlusionCulling = false; - return; - } - - // Create unit cube vertices (will be scaled to group bounds) - float cubeVerts[] = { - // Front face - 0,0,1, 1,0,1, 1,1,1, 0,0,1, 1,1,1, 0,1,1, - // Back face - 1,0,0, 0,0,0, 0,1,0, 1,0,0, 0,1,0, 1,1,0, - // Left face - 0,0,0, 0,0,1, 0,1,1, 0,0,0, 0,1,1, 0,1,0, - // Right face - 1,0,1, 1,0,0, 1,1,0, 1,0,1, 1,1,0, 1,1,1, - // Top face - 0,1,1, 1,1,1, 1,1,0, 0,1,1, 1,1,0, 0,1,0, - // Bottom face - 0,0,0, 1,0,0, 1,0,1, 0,0,0, 1,0,1, 0,0,1, - }; - - glGenVertexArrays(1, &bboxVao); - glGenBuffers(1, &bboxVbo); - - glBindVertexArray(bboxVao); - glBindBuffer(GL_ARRAY_BUFFER, bboxVbo); - glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVerts), cubeVerts, GL_STATIC_DRAW); - - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); - - glBindVertexArray(0); - - core::Logger::getInstance().info("Occlusion query resources initialized"); -} - -void WMORenderer::runOcclusionQueries(const WMOInstance& instance, const ModelData& model, - const glm::mat4& view, const glm::mat4& projection) { - if (!occlusionShader || bboxVao == 0) return; - - occlusionShader->use(); - glBindVertexArray(bboxVao); - - // Disable color writes, keep depth test - glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); - glDepthMask(GL_FALSE); // Don't write depth - - for (size_t gi = 0; gi < model.groups.size(); ++gi) { - const auto& group = model.groups[gi]; - - // Create query key - uint32_t queryKey = (instance.id << 16) | static_cast(gi); - - // Get or create query object - GLuint query; - auto it = occlusionQueries.find(queryKey); - if (it == occlusionQueries.end()) { - glGenQueries(1, &query); - occlusionQueries[queryKey] = query; - } else { - query = it->second; - } - - // Compute MVP for this group's bounding box - glm::vec3 bboxSize = group.boundingBoxMax - group.boundingBoxMin; - glm::mat4 bboxModel = instance.modelMatrix; - bboxModel = glm::translate(bboxModel, group.boundingBoxMin); - bboxModel = glm::scale(bboxModel, bboxSize); - glm::mat4 mvp = projection * view * bboxModel; - - occlusionShader->setUniform("uMVP", mvp); - - // Run occlusion query - glBeginQuery(GL_ANY_SAMPLES_PASSED, query); - glDrawArrays(GL_TRIANGLES, 0, 36); - glEndQuery(GL_ANY_SAMPLES_PASSED); - } - - // Restore state - glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - glDepthMask(GL_TRUE); - glBindVertexArray(0); -} - -bool WMORenderer::isGroupOccluded(uint32_t instanceId, uint32_t groupIndex) const { - uint32_t queryKey = (instanceId << 16) | groupIndex; - - // Check previous frame's result - auto resultIt = occlusionResults.find(queryKey); - if (resultIt != occlusionResults.end()) { - return !resultIt->second; // Return true if NOT visible - } - - // No result yet - assume visible - return false; -} +// Occlusion queries stubbed out in Vulkan (were disabled by default anyway) } // namespace rendering } // namespace wowee diff --git a/src/rendering/world_map.cpp b/src/rendering/world_map.cpp index 9afc30e9..15aed5a7 100644 --- a/src/rendering/world_map.cpp +++ b/src/rendering/world_map.cpp @@ -1,11 +1,15 @@ #include "rendering/world_map.hpp" -#include "rendering/shader.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_texture.hpp" +#include "rendering/vk_render_target.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_utils.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_layout.hpp" #include "core/coordinates.hpp" #include "core/input.hpp" #include "core/logger.hpp" -#include #include #include #include @@ -35,44 +39,195 @@ bool isLeafContinent(const std::vector& zones, int idx) { } } // namespace +// Push constant for world map tile composite vertex shader +struct WorldMapTilePush { + glm::vec2 gridOffset; // 8 bytes + float gridCols; // 4 bytes + float gridRows; // 4 bytes +}; // 16 bytes + WorldMap::WorldMap() = default; WorldMap::~WorldMap() { - if (fbo) glDeleteFramebuffers(1, &fbo); - if (fboTexture) glDeleteTextures(1, &fboTexture); - if (tileQuadVAO) glDeleteVertexArrays(1, &tileQuadVAO); - if (tileQuadVBO) glDeleteBuffers(1, &tileQuadVBO); - for (auto& zone : zones) { - for (auto& tex : zone.tileTextures) { - if (tex) glDeleteTextures(1, &tex); - } - } - tileShader.reset(); + shutdown(); } -void WorldMap::initialize(pipeline::AssetManager* am) { - if (initialized) return; +bool WorldMap::initialize(VkContext* ctx, pipeline::AssetManager* am) { + if (initialized) return true; + vkCtx = ctx; assetManager = am; - createFBO(); - createTileShader(); - createQuad(); + VkDevice device = vkCtx->getDevice(); + + // --- Composite render target (1024x768) --- + compositeTarget = std::make_unique(); + if (!compositeTarget->create(*vkCtx, FBO_W, FBO_H)) { + LOG_ERROR("WorldMap: failed to create composite render target"); + return false; + } + + // --- Quad vertex buffer (unit quad: pos2 + uv2) --- + float quadVerts[] = { + 0.0f, 0.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + 0.0f, 1.0f, 0.0f, 1.0f, + }; + auto quadBuf = uploadBuffer(*vkCtx, quadVerts, sizeof(quadVerts), + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + quadVB = quadBuf.buffer; + quadVBAlloc = quadBuf.allocation; + + // --- Descriptor set layout: 1 combined image sampler at binding 0 --- + VkDescriptorSetLayoutBinding samplerBinding{}; + samplerBinding.binding = 0; + samplerBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + samplerBinding.descriptorCount = 1; + samplerBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + samplerSetLayout = createDescriptorSetLayout(device, { samplerBinding }); + + // --- Descriptor pool (24 tile + 1 display = 25) --- + VkDescriptorPoolSize poolSize{}; + poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + poolSize.descriptorCount = MAX_DESC_SETS; + + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.maxSets = MAX_DESC_SETS; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + vkCreateDescriptorPool(device, &poolInfo, nullptr, &descPool); + + // --- Allocate descriptor sets: 12*2 tile + 1 display = 25 --- + constexpr uint32_t totalSets = 25; + std::vector layouts(totalSets, samplerSetLayout); + VkDescriptorSetAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = descPool; + allocInfo.descriptorSetCount = totalSets; + allocInfo.pSetLayouts = layouts.data(); + + VkDescriptorSet allSets[25]; + vkAllocateDescriptorSets(device, &allocInfo, allSets); + + for (int f = 0; f < 2; f++) + for (int t = 0; t < 12; t++) + tileDescSets[f][t] = allSets[f * 12 + t]; + imguiDisplaySet = allSets[24]; + + // --- Write display descriptor set → composite render target --- + VkDescriptorImageInfo compositeImgInfo = compositeTarget->descriptorInfo(); + VkWriteDescriptorSet displayWrite{}; + displayWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + displayWrite.dstSet = imguiDisplaySet; + displayWrite.dstBinding = 0; + displayWrite.descriptorCount = 1; + displayWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + displayWrite.pImageInfo = &compositeImgInfo; + vkUpdateDescriptorSets(device, 1, &displayWrite, 0, nullptr); + + // --- Pipeline layout: samplerSetLayout + push constant (16 bytes, vertex) --- + VkPushConstantRange tilePush{}; + tilePush.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + tilePush.offset = 0; + tilePush.size = sizeof(WorldMapTilePush); + tilePipelineLayout = createPipelineLayout(device, { samplerSetLayout }, { tilePush }); + + // --- Vertex input: pos2 (loc 0) + uv2 (loc 1), stride 16 --- + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = 4 * sizeof(float); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector attrs(2); + attrs[0] = { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 }; + attrs[1] = { 1, 0, VK_FORMAT_R32G32_SFLOAT, 2 * sizeof(float) }; + + // --- Load tile shaders and build pipeline --- + { + VkShaderModule vs, fs; + if (!vs.loadFromFile(device, "assets/shaders/world_map.vert.spv") || + !fs.loadFromFile(device, "assets/shaders/world_map.frag.spv")) { + LOG_ERROR("WorldMap: failed to load tile shaders"); + return false; + } + + tilePipeline = PipelineBuilder() + .setShaders(vs.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fs.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({ binding }, attrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setNoDepthTest() + .setColorBlendAttachment(PipelineBuilder::blendDisabled()) + .setLayout(tilePipelineLayout) + .setRenderPass(compositeTarget->getRenderPass()) + .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) + .build(device); + + vs.destroy(); + fs.destroy(); + } + + if (!tilePipeline) { + LOG_ERROR("WorldMap: failed to create tile pipeline"); + return false; + } + initialized = true; - LOG_INFO("WorldMap initialized (", FBO_W, "x", FBO_H, " FBO)"); + LOG_INFO("WorldMap initialized (", FBO_W, "x", FBO_H, " composite)"); + return true; +} + +void WorldMap::shutdown() { + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + VmaAllocator alloc = vkCtx->getAllocator(); + + vkDeviceWaitIdle(device); + + if (tilePipeline) { vkDestroyPipeline(device, tilePipeline, nullptr); tilePipeline = VK_NULL_HANDLE; } + if (tilePipelineLayout) { vkDestroyPipelineLayout(device, tilePipelineLayout, nullptr); tilePipelineLayout = VK_NULL_HANDLE; } + if (descPool) { vkDestroyDescriptorPool(device, descPool, nullptr); descPool = VK_NULL_HANDLE; } + if (samplerSetLayout) { vkDestroyDescriptorSetLayout(device, samplerSetLayout, nullptr); samplerSetLayout = VK_NULL_HANDLE; } + if (quadVB) { vmaDestroyBuffer(alloc, quadVB, quadVBAlloc); quadVB = VK_NULL_HANDLE; } + + destroyZoneTextures(); + + if (compositeTarget) { compositeTarget->destroy(device, alloc); compositeTarget.reset(); } + + zones.clear(); + initialized = false; + vkCtx = nullptr; +} + +void WorldMap::destroyZoneTextures() { + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + VmaAllocator alloc = vkCtx->getAllocator(); + + for (auto& tex : zoneTextures) { + if (tex) tex->destroy(device, alloc); + } + zoneTextures.clear(); + + for (auto& zone : zones) { + for (auto& tex : zone.tileTextures) tex = nullptr; + zone.tilesLoaded = false; + } } void WorldMap::setMapName(const std::string& name) { if (mapName == name && !zones.empty()) return; mapName = name; - // Clear old zone data - for (auto& zone : zones) { - for (auto& tex : zone.tileTextures) { - if (tex) { glDeleteTextures(1, &tex); tex = 0; } - } - } + + destroyZoneTextures(); zones.clear(); continentIdx = -1; currentIdx = -1; compositedIdx = -1; + pendingCompositeIdx = -1; viewLevel = ViewLevel::WORLD; } @@ -82,107 +237,17 @@ void WorldMap::setServerExplorationMask(const std::vector& masks, bool serverExplorationMask.clear(); return; } - hasServerExplorationMask = true; serverExplorationMask = masks; } // -------------------------------------------------------- -// GL resource creation -// -------------------------------------------------------- - -void WorldMap::createFBO() { - glGenFramebuffers(1, &fbo); - glBindFramebuffer(GL_FRAMEBUFFER, fbo); - - glGenTextures(1, &fboTexture); - glBindTexture(GL_TEXTURE_2D, fboTexture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, FBO_W, FBO_H, 0, - GL_RGBA, GL_UNSIGNED_BYTE, nullptr); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fboTexture, 0); - - if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - LOG_ERROR("WorldMap FBO incomplete"); - } - glBindFramebuffer(GL_FRAMEBUFFER, 0); -} - -void WorldMap::createTileShader() { - const char* vertSrc = R"( - #version 330 core - layout (location = 0) in vec2 aPos; - layout (location = 1) in vec2 aUV; - - uniform vec2 uGridOffset; // (col, row) in grid - uniform float uGridCols; - uniform float uGridRows; - - out vec2 TexCoord; - - void main() { - vec2 gridPos = vec2( - (uGridOffset.x + aPos.x) / uGridCols, - (uGridOffset.y + aPos.y) / uGridRows - ); - gl_Position = vec4(gridPos * 2.0 - 1.0, 0.0, 1.0); - TexCoord = aUV; - } - )"; - - const char* fragSrc = R"( - #version 330 core - in vec2 TexCoord; - - uniform sampler2D uTileTexture; - - out vec4 FragColor; - - void main() { - FragColor = texture(uTileTexture, TexCoord); - } - )"; - - tileShader = std::make_unique(); - if (!tileShader->loadFromSource(vertSrc, fragSrc)) { - LOG_ERROR("Failed to create WorldMap tile shader"); - } -} - -void WorldMap::createQuad() { - float quadVerts[] = { - // pos (x,y), uv (u,v) - 0.0f, 0.0f, 0.0f, 0.0f, - 1.0f, 0.0f, 1.0f, 0.0f, - 1.0f, 1.0f, 1.0f, 1.0f, - 0.0f, 0.0f, 0.0f, 0.0f, - 1.0f, 1.0f, 1.0f, 1.0f, - 0.0f, 1.0f, 0.0f, 1.0f, - }; - - glGenVertexArrays(1, &tileQuadVAO); - glGenBuffers(1, &tileQuadVBO); - glBindVertexArray(tileQuadVAO); - glBindBuffer(GL_ARRAY_BUFFER, tileQuadVBO); - glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW); - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); - glBindVertexArray(0); -} - -// -------------------------------------------------------- -// DBC zone loading +// DBC zone loading (identical to GL version) // -------------------------------------------------------- void WorldMap::loadZonesFromDBC() { if (!zones.empty() || !assetManager) return; - // Step 1: Resolve mapID from Map.dbc const auto* activeLayout = pipeline::getActiveDBCLayout(); const auto* mapL = activeLayout ? activeLayout->getLayout("Map") : nullptr; @@ -210,7 +275,6 @@ void WorldMap::loadZonesFromDBC() { } } - // Step 2: Load AreaTable explore flags by areaID. const auto* atL = activeLayout ? activeLayout->getLayout("AreaTable") : nullptr; std::unordered_map exploreFlagByAreaId; auto areaDbc = assetManager->loadDBC("AreaTable.dbc"); @@ -218,29 +282,16 @@ void WorldMap::loadZonesFromDBC() { for (uint32_t i = 0; i < areaDbc->getRecordCount(); i++) { const uint32_t areaId = areaDbc->getUInt32(i, atL ? (*atL)["ID"] : 0); const uint32_t exploreFlag = areaDbc->getUInt32(i, atL ? (*atL)["ExploreFlag"] : 3); - if (areaId != 0) { - exploreFlagByAreaId[areaId] = exploreFlag; - } + if (areaId != 0) exploreFlagByAreaId[areaId] = exploreFlag; } - } else { - LOG_WARNING("WorldMap: AreaTable.dbc missing or unexpected format; server exploration may be incomplete"); } - // Step 3: Load ALL WorldMapArea records for this mapID auto wmaDbc = assetManager->loadDBC("WorldMapArea.dbc"); if (!wmaDbc || !wmaDbc->isLoaded()) { LOG_WARNING("WorldMap: WorldMapArea.dbc not found"); return; } - LOG_INFO("WorldMap: WorldMapArea.dbc has ", wmaDbc->getFieldCount(), - " fields, ", wmaDbc->getRecordCount(), " records"); - - // WorldMapArea.dbc layout (11 fields, no localized strings): - // 0: ID, 1: MapID, 2: AreaID, 3: AreaName (stringref) - // 4: locLeft, 5: locRight, 6: locTop, 7: locBottom - // 8: displayMapID, 9: defaultDungeonFloor, 10: parentWorldMapID - const auto* wmaL = activeLayout ? activeLayout->getLayout("WorldMapArea") : nullptr; for (uint32_t i = 0; i < wmaDbc->getRecordCount(); i++) { @@ -258,60 +309,42 @@ void WorldMap::loadZonesFromDBC() { zone.displayMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["DisplayMapID"] : 8); zone.parentWorldMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["ParentWorldMapID"] : 10); auto exploreIt = exploreFlagByAreaId.find(zone.areaID); - if (exploreIt != exploreFlagByAreaId.end()) { + if (exploreIt != exploreFlagByAreaId.end()) zone.exploreFlag = exploreIt->second; - } int idx = static_cast(zones.size()); - // Debug: also log raw uint32 values for bounds fields - uint32_t raw4 = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["LocLeft"] : 4); - uint32_t raw5 = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["LocRight"] : 5); - uint32_t raw6 = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["LocTop"] : 6); - uint32_t raw7 = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["LocBottom"] : 7); - LOG_INFO("WorldMap: zone[", idx, "] areaID=", zone.areaID, " '", zone.areaName, "' L=", zone.locLeft, " R=", zone.locRight, " T=", zone.locTop, - " B=", zone.locBottom, - " (raw4=", raw4, " raw5=", raw5, - " raw6=", raw6, " raw7=", raw7, ")"); + " B=", zone.locBottom); - if (zone.areaID == 0 && continentIdx < 0) { + if (zone.areaID == 0 && continentIdx < 0) continentIdx = idx; - } zones.push_back(std::move(zone)); } - // For each continent entry with missing bounds, derive bounds from its child zones only. + // Derive continent bounds from child zones if missing for (int ci = 0; ci < static_cast(zones.size()); ci++) { auto& cont = zones[ci]; if (cont.areaID != 0) continue; - if (std::abs(cont.locLeft) > 0.001f || std::abs(cont.locRight) > 0.001f || - std::abs(cont.locTop) > 0.001f || std::abs(cont.locBottom) > 0.001f) { + std::abs(cont.locTop) > 0.001f || std::abs(cont.locBottom) > 0.001f) continue; - } bool first = true; for (const auto& z : zones) { if (z.areaID == 0) continue; if (std::abs(z.locLeft - z.locRight) < 0.001f || - std::abs(z.locTop - z.locBottom) < 0.001f) { + std::abs(z.locTop - z.locBottom) < 0.001f) continue; - } - - // Prefer explicit parent linkage when deriving continent extents. - if (z.parentWorldMapID != 0 && cont.wmaID != 0 && z.parentWorldMapID != cont.wmaID) { + if (z.parentWorldMapID != 0 && cont.wmaID != 0 && z.parentWorldMapID != cont.wmaID) continue; - } if (first) { - cont.locLeft = z.locLeft; - cont.locRight = z.locRight; - cont.locTop = z.locTop; - cont.locBottom = z.locBottom; + cont.locLeft = z.locLeft; cont.locRight = z.locRight; + cont.locTop = z.locTop; cont.locBottom = z.locBottom; first = false; } else { cont.locLeft = std::max(cont.locLeft, z.locLeft); @@ -320,11 +353,6 @@ void WorldMap::loadZonesFromDBC() { cont.locBottom = std::min(cont.locBottom, z.locBottom); } } - - if (!first) { - LOG_INFO("WorldMap: computed bounds for continent '", cont.areaName, "': L=", cont.locLeft, - " R=", cont.locRight, " T=", cont.locTop, " B=", cont.locBottom); - } } LOG_INFO("WorldMap: loaded ", zones.size(), " zones for mapID=", mapID, @@ -332,8 +360,8 @@ void WorldMap::loadZonesFromDBC() { } int WorldMap::findBestContinentForPlayer(const glm::vec3& playerRenderPos) const { - float wowX = playerRenderPos.y; // north/south - float wowY = playerRenderPos.x; // west/east + float wowX = playerRenderPos.y; + float wowY = playerRenderPos.x; int bestIdx = -1; float bestArea = std::numeric_limits::max(); @@ -363,56 +391,39 @@ int WorldMap::findBestContinentForPlayer(const glm::vec3& playerRenderPos) const bool contains = (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY); float area = spanX * spanY; if (contains) { - if (area < bestArea) { - bestArea = area; - bestIdx = i; - } + if (area < bestArea) { bestArea = area; bestIdx = i; } } else if (bestIdx < 0) { - // Fallback if player isn't inside any continent bounds: nearest center. - float cx = (minX + maxX) * 0.5f; - float cy = (minY + maxY) * 0.5f; - float dx = wowX - cx; - float dy = wowY - cy; - float dist2 = dx * dx + dy * dy; - if (dist2 < bestCenterDist2) { - bestCenterDist2 = dist2; - bestIdx = i; - } + float cx = (minX + maxX) * 0.5f, cy = (minY + maxY) * 0.5f; + float dist2 = (wowX - cx) * (wowX - cx) + (wowY - cy) * (wowY - cy); + if (dist2 < bestCenterDist2) { bestCenterDist2 = dist2; bestIdx = i; } } } - return bestIdx; } int WorldMap::findZoneForPlayer(const glm::vec3& playerRenderPos) const { - float wowX = playerRenderPos.y; // north/south - float wowY = playerRenderPos.x; // west/east + float wowX = playerRenderPos.y; + float wowY = playerRenderPos.x; int bestIdx = -1; float bestArea = std::numeric_limits::max(); for (int i = 0; i < static_cast(zones.size()); i++) { const auto& z = zones[i]; - if (z.areaID == 0) continue; // skip continent-level entries + if (z.areaID == 0) continue; float minX = std::min(z.locLeft, z.locRight); float maxX = std::max(z.locLeft, z.locRight); float minY = std::min(z.locTop, z.locBottom); float maxY = std::max(z.locTop, z.locBottom); - float spanX = maxX - minX; - float spanY = maxY - minY; + float spanX = maxX - minX, spanY = maxY - minY; if (spanX < 0.001f || spanY < 0.001f) continue; - bool contains = (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY); - if (contains) { + if (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY) { float area = spanX * spanY; - if (area < bestArea) { - bestArea = area; - bestIdx = i; - } + if (area < bestArea) { bestArea = area; bestIdx = i; } } } - return bestIdx; } @@ -422,13 +433,10 @@ bool WorldMap::zoneBelongsToContinent(int zoneIdx, int contIdx) const { const auto& z = zones[zoneIdx]; const auto& cont = zones[contIdx]; - if (z.areaID == 0) return false; - // Prefer explicit parent linkage from WorldMapArea.dbc. - if (z.parentWorldMapID != 0 && cont.wmaID != 0) { + if (z.parentWorldMapID != 0 && cont.wmaID != 0) return z.parentWorldMapID == cont.wmaID; - } auto rectMinX = [](const WorldMapZone& a) { return std::min(a.locLeft, a.locRight); }; auto rectMaxX = [](const WorldMapZone& a) { return std::max(a.locLeft, a.locRight); }; @@ -439,13 +447,11 @@ bool WorldMap::zoneBelongsToContinent(int zoneIdx, int contIdx) const { float zMinY = rectMinY(z), zMaxY = rectMaxY(z); if ((zMaxX - zMinX) < 0.001f || (zMaxY - zMinY) < 0.001f) return false; - // Fallback: assign zone to the continent with highest overlap area. int bestContIdx = -1; float bestOverlap = 0.0f; for (int i = 0; i < static_cast(zones.size()); i++) { const auto& c = zones[i]; if (c.areaID != 0) continue; - float cMinX = rectMinX(c), cMaxX = rectMaxX(c); float cMinY = rectMinY(c), cMaxY = rectMaxY(c); if ((cMaxX - cMinX) < 0.001f || (cMaxY - cMinY) < 0.001f) continue; @@ -453,55 +459,35 @@ bool WorldMap::zoneBelongsToContinent(int zoneIdx, int contIdx) const { float ox = std::max(0.0f, std::min(zMaxX, cMaxX) - std::max(zMinX, cMinX)); float oy = std::max(0.0f, std::min(zMaxY, cMaxY) - std::max(zMinY, cMinY)); float overlap = ox * oy; - if (overlap > bestOverlap) { - bestOverlap = overlap; - bestContIdx = i; - } + if (overlap > bestOverlap) { bestOverlap = overlap; bestContIdx = i; } } + if (bestContIdx >= 0) return bestContIdx == contIdx; - if (bestContIdx >= 0) { - return bestContIdx == contIdx; - } - - // Last resort: center-point containment. float centerX = (z.locLeft + z.locRight) * 0.5f; float centerY = (z.locTop + z.locBottom) * 0.5f; - float cMinX = rectMinX(cont), cMaxX = rectMaxX(cont); - float cMinY = rectMinY(cont), cMaxY = rectMaxY(cont); - return centerX >= cMinX && centerX <= cMaxX && - centerY >= cMinY && centerY <= cMaxY; + return centerX >= rectMinX(cont) && centerX <= rectMaxX(cont) && + centerY >= rectMinY(cont) && centerY <= rectMaxY(cont); } bool WorldMap::getContinentProjectionBounds(int contIdx, float& left, float& right, float& top, float& bottom) const { if (contIdx < 0 || contIdx >= static_cast(zones.size())) return false; - const auto& cont = zones[contIdx]; if (cont.areaID != 0) return false; - // Prefer authored continent bounds from DBC when available. if (std::abs(cont.locLeft - cont.locRight) > 0.001f && std::abs(cont.locTop - cont.locBottom) > 0.001f) { - left = cont.locLeft; - right = cont.locRight; - top = cont.locTop; - bottom = cont.locBottom; + left = cont.locLeft; right = cont.locRight; + top = cont.locTop; bottom = cont.locBottom; return true; } - std::vector northEdges; - std::vector southEdges; - std::vector westEdges; - std::vector eastEdges; - + std::vector northEdges, southEdges, westEdges, eastEdges; for (int zi = 0; zi < static_cast(zones.size()); zi++) { if (!zoneBelongsToContinent(zi, contIdx)) continue; const auto& z = zones[zi]; if (std::abs(z.locLeft - z.locRight) < 0.001f || - std::abs(z.locTop - z.locBottom) < 0.001f) { - continue; - } - + std::abs(z.locTop - z.locBottom) < 0.001f) continue; northEdges.push_back(std::max(z.locLeft, z.locRight)); southEdges.push_back(std::min(z.locLeft, z.locRight)); westEdges.push_back(std::max(z.locTop, z.locBottom)); @@ -509,31 +495,25 @@ bool WorldMap::getContinentProjectionBounds(int contIdx, float& left, float& rig } if (northEdges.size() < 3) { - left = cont.locLeft; - right = cont.locRight; - top = cont.locTop; - bottom = cont.locBottom; + left = cont.locLeft; right = cont.locRight; + top = cont.locTop; bottom = cont.locBottom; return std::abs(left - right) > 0.001f && std::abs(top - bottom) > 0.001f; } - // Fallback: derive full extents from child zones. left = *std::max_element(northEdges.begin(), northEdges.end()); right = *std::min_element(southEdges.begin(), southEdges.end()); top = *std::max_element(westEdges.begin(), westEdges.end()); bottom = *std::min_element(eastEdges.begin(), eastEdges.end()); if (left <= right || top <= bottom) { - left = cont.locLeft; - right = cont.locRight; - top = cont.locTop; - bottom = cont.locBottom; + left = cont.locLeft; right = cont.locRight; + top = cont.locTop; bottom = cont.locBottom; } - return std::abs(left - right) > 0.001f && std::abs(top - bottom) > 0.001f; } // -------------------------------------------------------- -// Per-zone texture loading +// Per-zone texture loading (Vulkan) // -------------------------------------------------------- void WorldMap::loadZoneTextures(int zoneIdx) { @@ -552,7 +532,9 @@ void WorldMap::loadZoneTextures(int zoneIdx) { if (folder != "EasternKingdoms") candidateFolders.push_back("EasternKingdoms"); } + VkDevice device = vkCtx->getDevice(); int loaded = 0; + for (int i = 0; i < 12; i++) { pipeline::BLPImage blpImage; bool found = false; @@ -560,28 +542,22 @@ void WorldMap::loadZoneTextures(int zoneIdx) { std::string path = "Interface\\WorldMap\\" + testFolder + "\\" + testFolder + std::to_string(i + 1) + ".blp"; blpImage = assetManager->loadTexture(path); - if (blpImage.isValid()) { - found = true; - break; - } + if (blpImage.isValid()) { found = true; break; } } if (!found) { - zone.tileTextures[i] = 0; + zone.tileTextures[i] = nullptr; continue; } - GLuint tex; - glGenTextures(1, &tex); - glBindTexture(GL_TEXTURE_2D, tex); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, blpImage.width, blpImage.height, 0, - GL_RGBA, GL_UNSIGNED_BYTE, blpImage.data.data()); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + auto tex = std::make_unique(); + tex->upload(*vkCtx, blpImage.data.data(), blpImage.width, blpImage.height, + VK_FORMAT_R8G8B8A8_UNORM, false); + tex->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR, + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, 1.0f); - zone.tileTextures[i] = tex; + zone.tileTextures[i] = tex.get(); + zoneTextures.push_back(std::move(tex)); loaded++; } @@ -589,65 +565,81 @@ void WorldMap::loadZoneTextures(int zoneIdx) { } // -------------------------------------------------------- -// Composite a zone's tiles into the FBO +// Request composite (deferred to compositePass) // -------------------------------------------------------- -void WorldMap::compositeZone(int zoneIdx) { +void WorldMap::requestComposite(int zoneIdx) { if (zoneIdx < 0 || zoneIdx >= static_cast(zones.size())) return; + pendingCompositeIdx = zoneIdx; +} + +// -------------------------------------------------------- +// Off-screen composite pass (call BEFORE main render pass) +// -------------------------------------------------------- + +void WorldMap::compositePass(VkCommandBuffer cmd) { + if (!initialized || pendingCompositeIdx < 0 || !compositeTarget) return; + if (pendingCompositeIdx >= static_cast(zones.size())) { + pendingCompositeIdx = -1; + return; + } + + int zoneIdx = pendingCompositeIdx; + pendingCompositeIdx = -1; + if (compositedIdx == zoneIdx) return; const auto& zone = zones[zoneIdx]; + uint32_t frameIdx = vkCtx->getCurrentFrame(); + VkDevice device = vkCtx->getDevice(); - // Save GL state - GLint prevFBO = 0; - glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFBO); - GLint prevViewport[4]; - glGetIntegerv(GL_VIEWPORT, prevViewport); - GLboolean prevBlend = glIsEnabled(GL_BLEND); - GLboolean prevDepthTest = glIsEnabled(GL_DEPTH_TEST); - - glBindFramebuffer(GL_FRAMEBUFFER, fbo); - glViewport(0, 0, FBO_W, FBO_H); - glClearColor(0.05f, 0.08f, 0.12f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - - glDisable(GL_DEPTH_TEST); - glDisable(GL_CULL_FACE); - glDisable(GL_BLEND); - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); - - tileShader->use(); - tileShader->setUniform("uTileTexture", 0); - tileShader->setUniform("uGridCols", static_cast(GRID_COLS)); - tileShader->setUniform("uGridRows", static_cast(GRID_ROWS)); - - glBindVertexArray(tileQuadVAO); - - // Tiles 1-12 in a 4x3 grid: tile N at col=(N-1)%4, row=(N-1)/4 - // Row 0 (tiles 1-4) = top of image (north) → placed at FBO bottom (GL y=0) - // ImGui::Image maps GL (0,0) → widget top-left → north at top ✓ + // Update tile descriptor sets for this frame for (int i = 0; i < 12; i++) { - if (zone.tileTextures[i] == 0) continue; + VkTexture* tileTex = zone.tileTextures[i]; + if (!tileTex || !tileTex->isValid()) continue; + + VkDescriptorImageInfo imgInfo = tileTex->descriptorInfo(); + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = tileDescSets[frameIdx][i]; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imgInfo; + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + } + + // Begin off-screen render pass + VkClearColorValue clearColor = {{ 0.05f, 0.08f, 0.12f, 1.0f }}; + compositeTarget->beginPass(cmd, clearColor); + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, tilePipeline); + + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &quadVB, &offset); + + // Draw 4x3 tile grid + for (int i = 0; i < 12; i++) { + if (!zone.tileTextures[i] || !zone.tileTextures[i]->isValid()) continue; int col = i % GRID_COLS; int row = i / GRID_COLS; - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, zone.tileTextures[i]); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + tilePipelineLayout, 0, 1, + &tileDescSets[frameIdx][i], 0, nullptr); - tileShader->setUniform("uGridOffset", glm::vec2( - static_cast(col), static_cast(row))); - glDrawArrays(GL_TRIANGLES, 0, 6); + WorldMapTilePush push{}; + push.gridOffset = glm::vec2(static_cast(col), static_cast(row)); + push.gridCols = static_cast(GRID_COLS); + push.gridRows = static_cast(GRID_ROWS); + vkCmdPushConstants(cmd, tilePipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, + 0, sizeof(push), &push); + + vkCmdDraw(cmd, 6, 1, 0, 0); } - glBindVertexArray(0); - - // Restore GL state - glBindFramebuffer(GL_FRAMEBUFFER, prevFBO); - glViewport(prevViewport[0], prevViewport[1], prevViewport[2], prevViewport[3]); - if (prevBlend) glEnable(GL_BLEND); - if (prevDepthTest) glEnable(GL_DEPTH_TEST); - + compositeTarget->endPass(cmd); compositedIdx = zoneIdx; } @@ -656,61 +648,44 @@ void WorldMap::enterWorldView() { int rootIdx = -1; for (int i = 0; i < static_cast(zones.size()); i++) { - if (isRootContinent(zones, i)) { - rootIdx = i; - break; - } + if (isRootContinent(zones, i)) { rootIdx = i; break; } } if (rootIdx >= 0) { loadZoneTextures(rootIdx); bool hasAnyTile = false; - for (GLuint tex : zones[rootIdx].tileTextures) { - if (tex != 0) { hasAnyTile = true; break; } + for (VkTexture* tex : zones[rootIdx].tileTextures) { + if (tex != nullptr) { hasAnyTile = true; break; } } if (hasAnyTile) { - compositeZone(rootIdx); + requestComposite(rootIdx); currentIdx = rootIdx; return; } } - // Fallback: use first leaf continent as world-view backdrop. int fallbackContinent = -1; for (int i = 0; i < static_cast(zones.size()); i++) { - if (isLeafContinent(zones, i)) { - fallbackContinent = i; - break; - } + if (isLeafContinent(zones, i)) { fallbackContinent = i; break; } } if (fallbackContinent < 0) { for (int i = 0; i < static_cast(zones.size()); i++) { if (zones[i].areaID == 0 && !isRootContinent(zones, i)) { - fallbackContinent = i; - break; + fallbackContinent = i; break; } } } if (fallbackContinent >= 0) { loadZoneTextures(fallbackContinent); - compositeZone(fallbackContinent); + requestComposite(fallbackContinent); currentIdx = fallbackContinent; return; } - // No root world texture available: clear to neutral background. currentIdx = -1; compositedIdx = -1; - GLint prevFBO = 0; - glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFBO); - GLint prevViewport[4]; - glGetIntegerv(GL_VIEWPORT, prevViewport); - glBindFramebuffer(GL_FRAMEBUFFER, fbo); - glViewport(0, 0, FBO_W, FBO_H); - glClearColor(0.05f, 0.08f, 0.12f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - glBindFramebuffer(GL_FRAMEBUFFER, prevFBO); - glViewport(prevViewport[0], prevViewport[1], prevViewport[2], prevViewport[3]); + // Render target will be cleared by next compositePass + pendingCompositeIdx = -2; // Signal "clear only" } // -------------------------------------------------------- @@ -722,15 +697,11 @@ glm::vec2 WorldMap::renderPosToMapUV(const glm::vec3& renderPos, int zoneIdx) co return glm::vec2(0.5f, 0.5f); const auto& zone = zones[zoneIdx]; + float wowX = renderPos.y; + float wowY = renderPos.x; - // renderPos: x = wowY (west axis), y = wowX (north axis) - float wowX = renderPos.y; // north - float wowY = renderPos.x; // west - - float left = zone.locLeft; - float right = zone.locRight; - float top = zone.locTop; - float bottom = zone.locBottom; + float left = zone.locLeft, right = zone.locRight; + float top = zone.locTop, bottom = zone.locBottom; if (zone.areaID == 0) { float l, r, t, b; if (getContinentProjectionBounds(zoneIdx, l, r, t, b)) { @@ -738,44 +709,34 @@ glm::vec2 WorldMap::renderPosToMapUV(const glm::vec3& renderPos, int zoneIdx) co } } - // WorldMapArea.dbc axis mapping: - // locLeft/locRight contain wowX values (N/S), locLeft=north > locRight=south - // locTop/locBottom contain wowY values (W/E), locTop=west > locBottom=east - // World map textures are laid out with axes transposed, so horizontal uses wowX. - float denom_h = left - right; // wowX span (N-S) → horizontal - float denom_v = top - bottom; // wowY span (W-E) → vertical - + float denom_h = left - right; + float denom_v = top - bottom; if (std::abs(denom_h) < 0.001f || std::abs(denom_v) < 0.001f) return glm::vec2(0.5f, 0.5f); float u = (left - wowX) / denom_h; float v = (top - wowY) / denom_v; - // Continent overlay calibration: shift overlays/player marker upward. if (zone.areaID == 0) { constexpr float kVScale = 1.0f; - constexpr float kVOffset = -0.15f; // ~15% upward total + constexpr float kVOffset = -0.15f; v = (v - 0.5f) * kVScale + 0.5f + kVOffset; } return glm::vec2(u, v); } // -------------------------------------------------------- -// Exploration tracking +// Exploration tracking (identical to GL version) // -------------------------------------------------------- void WorldMap::updateExploration(const glm::vec3& playerRenderPos) { auto isExploreFlagSet = [this](uint32_t flag) -> bool { if (!hasServerExplorationMask || serverExplorationMask.empty() || flag == 0) return false; - const auto isSet = [this](uint32_t bitIndex) -> bool { const size_t word = bitIndex / 32; if (word >= serverExplorationMask.size()) return false; - const uint32_t bit = bitIndex % 32; - return (serverExplorationMask[word] & (1u << bit)) != 0; + return (serverExplorationMask[word] & (1u << (bitIndex % 32))) != 0; }; - - // Most cores use zero-based bit indices; some data behaves one-based. if (isSet(flag)) return true; if (flag > 0 && isSet(flag - 1)) return true; return false; @@ -793,56 +754,42 @@ void WorldMap::updateExploration(const glm::vec3& playerRenderPos) { } } } - - // Fall back to local bounds-based reveal if server masks are missing/unusable. if (markedAny) return; - float wowX = playerRenderPos.y; // north/south - float wowY = playerRenderPos.x; // west/east + float wowX = playerRenderPos.y; + float wowY = playerRenderPos.x; for (int i = 0; i < static_cast(zones.size()); i++) { const auto& z = zones[i]; - if (z.areaID == 0) continue; // skip continent-level entries - - float minX = std::min(z.locLeft, z.locRight); - float maxX = std::max(z.locLeft, z.locRight); - float minY = std::min(z.locTop, z.locBottom); - float maxY = std::max(z.locTop, z.locBottom); - float spanX = maxX - minX; - float spanY = maxY - minY; - if (spanX < 0.001f || spanY < 0.001f) continue; - - bool contains = (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY); - if (contains) { + if (z.areaID == 0) continue; + float minX = std::min(z.locLeft, z.locRight), maxX = std::max(z.locLeft, z.locRight); + float minY = std::min(z.locTop, z.locBottom), maxY = std::max(z.locTop, z.locBottom); + if (maxX - minX < 0.001f || maxY - minY < 0.001f) continue; + if (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY) { exploredZones.insert(i); markedAny = true; } } - // Fallback for imperfect DBC bounds: reveal nearest zone so exploration still progresses. if (!markedAny) { int zoneIdx = findZoneForPlayer(playerRenderPos); - if (zoneIdx >= 0) { - exploredZones.insert(zoneIdx); - } + if (zoneIdx >= 0) exploredZones.insert(zoneIdx); } } void WorldMap::zoomIn(const glm::vec3& playerRenderPos) { if (viewLevel == ViewLevel::WORLD) { - // World → Continent if (continentIdx >= 0) { loadZoneTextures(continentIdx); - compositeZone(continentIdx); + requestComposite(continentIdx); currentIdx = continentIdx; viewLevel = ViewLevel::CONTINENT; } } else if (viewLevel == ViewLevel::CONTINENT) { - // Continent → Zone (use player's current zone) int zoneIdx = findZoneForPlayer(playerRenderPos); if (zoneIdx >= 0 && zoneBelongsToContinent(zoneIdx, continentIdx)) { loadZoneTextures(zoneIdx); - compositeZone(zoneIdx); + requestComposite(zoneIdx); currentIdx = zoneIdx; viewLevel = ViewLevel::ZONE; } @@ -851,20 +798,18 @@ void WorldMap::zoomIn(const glm::vec3& playerRenderPos) { void WorldMap::zoomOut() { if (viewLevel == ViewLevel::ZONE) { - // Zone → Continent if (continentIdx >= 0) { - compositeZone(continentIdx); + requestComposite(continentIdx); currentIdx = continentIdx; viewLevel = ViewLevel::CONTINENT; } } else if (viewLevel == ViewLevel::CONTINENT) { - // Continent → World enterWorldView(); } } // -------------------------------------------------------- -// Main render +// Main render (input + ImGui overlay) // -------------------------------------------------------- void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight) { @@ -872,12 +817,8 @@ void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int scr auto& input = core::Input::getInstance(); - // Track exploration even when map is closed - if (!zones.empty()) { - updateExploration(playerRenderPos); - } + if (!zones.empty()) updateExploration(playerRenderPos); - // When map is open, always allow M/Escape to close (bypass ImGui keyboard capture) if (open) { if (input.isKeyJustPressed(SDL_SCANCODE_M) || input.isKeyJustPressed(SDL_SCANCODE_ESCAPE)) { @@ -885,24 +826,16 @@ void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int scr return; } - // Mouse wheel: scroll up = zoom in, scroll down = zoom out. - // Use both ImGui and raw input wheel deltas for reliability across frame order/capture paths. auto& io = ImGui::GetIO(); float wheelDelta = io.MouseWheel; - if (std::abs(wheelDelta) < 0.001f) { + if (std::abs(wheelDelta) < 0.001f) wheelDelta = input.getMouseWheelDelta(); - } - if (wheelDelta > 0.0f) { - zoomIn(playerRenderPos); - } else if (wheelDelta < 0.0f) { - zoomOut(); - } + if (wheelDelta > 0.0f) zoomIn(playerRenderPos); + else if (wheelDelta < 0.0f) zoomOut(); } else { auto& io = ImGui::GetIO(); if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_M)) { open = true; - - // Lazy-load zone data on first open if (zones.empty()) loadZonesFromDBC(); int bestContinent = findBestContinentForPlayer(playerRenderPos); @@ -911,17 +844,16 @@ void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int scr compositedIdx = -1; } - // Open directly to the player's current zone int playerZone = findZoneForPlayer(playerRenderPos); if (playerZone >= 0 && continentIdx >= 0 && zoneBelongsToContinent(playerZone, continentIdx)) { loadZoneTextures(playerZone); - compositeZone(playerZone); + requestComposite(playerZone); currentIdx = playerZone; viewLevel = ViewLevel::ZONE; } else if (continentIdx >= 0) { loadZoneTextures(continentIdx); - compositeZone(continentIdx); + requestComposite(continentIdx); currentIdx = continentIdx; viewLevel = ViewLevel::CONTINENT; } @@ -929,7 +861,6 @@ void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int scr } if (!open) return; - renderImGuiOverlay(playerRenderPos, screenWidth, screenHeight); } @@ -941,7 +872,6 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi float sw = static_cast(screenWidth); float sh = static_cast(screenHeight); - // Full-screen dark background ImGui::SetNextWindowPos(ImVec2(0, 0)); ImGui::SetNextWindowSize(ImVec2(sw, sh)); @@ -955,8 +885,7 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); if (ImGui::Begin("##WorldMap", nullptr, flags)) { - // Map display area: maintain 4:3 aspect ratio, fit within ~85% of screen - float mapAspect = static_cast(FBO_W) / static_cast(FBO_H); // 1.333 + float mapAspect = static_cast(FBO_W) / static_cast(FBO_H); float availW = sw * 0.85f; float availH = sh * 0.85f; float displayW, displayH; @@ -972,7 +901,8 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi float mapY = (sh - displayH) / 2.0f; ImGui::SetCursorPos(ImVec2(mapX, mapY)); - ImGui::Image(static_cast(static_cast(fboTexture)), + // Display composite render target via ImGui (VkDescriptorSet as ImTextureID) + ImGui::Image(reinterpret_cast(imguiDisplaySet), ImVec2(displayW, displayH), ImVec2(0, 0), ImVec2(1, 1)); ImVec2 imgMin = ImGui::GetItemRectMin(); @@ -991,8 +921,6 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi continentIndices.push_back(i); } } - // If we have multiple continent choices, hide the root/world alias entry - // (commonly "Azeroth") so picker only shows real continents. if (continentIndices.size() > 1) { std::vector filtered; filtered.reserve(continentIndices.size()); @@ -1008,7 +936,7 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi } } - // World-level continent selection UI. + // World-level continent selection UI if (viewLevel == ViewLevel::WORLD && !continentIndices.empty()) { ImVec2 titleSz = ImGui::CalcTextSize("World"); ImGui::SetCursorPos(ImVec2((sw - titleSz.x) * 0.5f, mapY + 8.0f)); @@ -1019,17 +947,15 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi int ci = continentIndices[i]; if (i > 0) ImGui::SameLine(); const bool selected = (ci == continentIdx); - if (selected) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.35f, 0.25f, 0.05f, 0.9f)); - } + if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.35f, 0.25f, 0.05f, 0.9f)); - std::string rawName = zones[ci].areaName.empty() ? "Continent" : zones[ci].areaName; - if (rawName == "Azeroth") rawName = "Eastern Kingdoms"; - std::string label = rawName + "##" + std::to_string(ci); + std::string rawName = zones[ci].areaName.empty() ? "Continent" : zones[ci].areaName; + if (rawName == "Azeroth") rawName = "Eastern Kingdoms"; + std::string label = rawName + "##" + std::to_string(ci); if (ImGui::Button(label.c_str())) { continentIdx = ci; loadZoneTextures(continentIdx); - compositeZone(continentIdx); + requestComposite(continentIdx); currentIdx = continentIdx; viewLevel = ViewLevel::CONTINENT; } @@ -1041,9 +967,7 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi int ci = continentIndices[i]; if (i > 0) ImGui::SameLine(); const bool selected = (ci == continentIdx); - if (selected) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.35f, 0.25f, 0.05f, 0.9f)); - } + if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.35f, 0.25f, 0.05f, 0.9f)); std::string rawName = zones[ci].areaName.empty() ? "Continent" : zones[ci].areaName; if (rawName == "Azeroth") rawName = "Eastern Kingdoms"; @@ -1051,40 +975,33 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi if (ImGui::Button(label.c_str())) { continentIdx = ci; loadZoneTextures(continentIdx); - compositeZone(continentIdx); + requestComposite(continentIdx); currentIdx = continentIdx; } if (selected) ImGui::PopStyleColor(); } } - // Player marker on current view + // Player marker if (currentIdx >= 0 && viewLevel != ViewLevel::WORLD) { glm::vec2 playerUV = renderPosToMapUV(playerRenderPos, currentIdx); - if (playerUV.x >= 0.0f && playerUV.x <= 1.0f && playerUV.y >= 0.0f && playerUV.y <= 1.0f) { float px = imgMin.x + playerUV.x * displayW; float py = imgMin.y + playerUV.y * displayH; - - drawList->AddCircleFilled(ImVec2(px, py), 6.0f, - IM_COL32(255, 40, 40, 255)); - drawList->AddCircle(ImVec2(px, py), 6.0f, - IM_COL32(0, 0, 0, 200), 0, 2.0f); + drawList->AddCircleFilled(ImVec2(px, py), 6.0f, IM_COL32(255, 40, 40, 255)); + drawList->AddCircle(ImVec2(px, py), 6.0f, IM_COL32(0, 0, 0, 200), 0, 2.0f); } } - // --- Continent view: show clickable zone overlays --- + // Continent view: clickable zone overlays if (viewLevel == ViewLevel::CONTINENT && continentIdx >= 0) { const auto& cont = zones[continentIdx]; - // World map textures are transposed; match the same axis mapping as player UV. - float cLeft = cont.locLeft; - float cRight = cont.locRight; - float cTop = cont.locTop; - float cBottom = cont.locBottom; + float cLeft = cont.locLeft, cRight = cont.locRight; + float cTop = cont.locTop, cBottom = cont.locBottom; getContinentProjectionBounds(continentIdx, cLeft, cRight, cTop, cBottom); - float cDenomU = cLeft - cRight; // wowX span (N-S) - float cDenomV = cTop - cBottom; // wowY span (W-E) + float cDenomU = cLeft - cRight; + float cDenomV = cTop - cBottom; ImVec2 mousePos = ImGui::GetMousePos(); int hoveredZone = -1; @@ -1092,65 +1009,45 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi if (std::abs(cDenomU) > 0.001f && std::abs(cDenomV) > 0.001f) { for (int zi = 0; zi < static_cast(zones.size()); zi++) { if (!zoneBelongsToContinent(zi, continentIdx)) continue; - const auto& z = zones[zi]; - - // Skip zones with zero-size bounds if (std::abs(z.locLeft - z.locRight) < 0.001f || std::abs(z.locTop - z.locBottom) < 0.001f) continue; - // Project zone bounds to continent UV - // u axis (left->right): north->south - float zuMin = (cLeft - z.locLeft) / cDenomU; // zone north edge - float zuMax = (cLeft - z.locRight) / cDenomU; // zone south edge - // v axis (top->bottom): west->east - float zvMin = (cTop - z.locTop) / cDenomV; // zone west edge - float zvMax = (cTop - z.locBottom) / cDenomV; // zone east edge + float zuMin = (cLeft - z.locLeft) / cDenomU; + float zuMax = (cLeft - z.locRight) / cDenomU; + float zvMin = (cTop - z.locTop) / cDenomV; + float zvMax = (cTop - z.locBottom) / cDenomV; - // Slightly shrink DBC AABB overlays to reduce heavy overlap. constexpr float kOverlayShrink = 0.92f; - float cu = (zuMin + zuMax) * 0.5f; - float cv = (zvMin + zvMax) * 0.5f; + float cu = (zuMin + zuMax) * 0.5f, cv = (zvMin + zvMax) * 0.5f; float hu = (zuMax - zuMin) * 0.5f * kOverlayShrink; float hv = (zvMax - zvMin) * 0.5f * kOverlayShrink; - zuMin = cu - hu; - zuMax = cu + hu; - zvMin = cv - hv; - zvMax = cv + hv; + zuMin = cu - hu; zuMax = cu + hu; + zvMin = cv - hv; zvMax = cv + hv; - // Continent overlay calibration (matches player marker calibration). - constexpr float kVScale = 1.0f; constexpr float kVOffset = -0.15f; - zvMin = (zvMin - 0.5f) * kVScale + 0.5f + kVOffset; - zvMax = (zvMax - 0.5f) * kVScale + 0.5f + kVOffset; + zvMin = (zvMin - 0.5f) + 0.5f + kVOffset; + zvMax = (zvMax - 0.5f) + 0.5f + kVOffset; - // Clamp to [0,1] zuMin = std::clamp(zuMin, 0.0f, 1.0f); zuMax = std::clamp(zuMax, 0.0f, 1.0f); zvMin = std::clamp(zvMin, 0.0f, 1.0f); zvMax = std::clamp(zvMax, 0.0f, 1.0f); - - // Skip tiny or degenerate zones if (zuMax - zuMin < 0.001f || zvMax - zvMin < 0.001f) continue; - // Convert to screen coordinates float sx0 = imgMin.x + zuMin * displayW; float sy0 = imgMin.y + zvMin * displayH; float sx1 = imgMin.x + zuMax * displayW; float sy1 = imgMin.y + zvMax * displayH; bool explored = exploredZones.count(zi) > 0; - - // Check hover bool hovered = (mousePos.x >= sx0 && mousePos.x <= sx1 && mousePos.y >= sy0 && mousePos.y <= sy1); - // Fog of war: darken unexplored zones if (!explored) { drawList->AddRectFilled(ImVec2(sx0, sy0), ImVec2(sx1, sy1), IM_COL32(0, 0, 0, 160)); } - if (hovered) { hoveredZone = zi; drawList->AddRectFilled(ImVec2(sx0, sy0), ImVec2(sx1, sy1), @@ -1164,27 +1061,22 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi } } - // Zone name tooltip if (hoveredZone >= 0) { ImGui::SetTooltip("%s", zones[hoveredZone].areaName.c_str()); - - // Click to zoom into zone if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { loadZoneTextures(hoveredZone); - compositeZone(hoveredZone); + requestComposite(hoveredZone); currentIdx = hoveredZone; viewLevel = ViewLevel::ZONE; } } } - // --- Zone view: back to continent --- + // Zone view: back to continent if (viewLevel == ViewLevel::ZONE && continentIdx >= 0) { - // Right-click or Back button auto& io = ImGui::GetIO(); - bool goBack = io.MouseClicked[1]; // right-click (direct IO check) + bool goBack = io.MouseClicked[1]; - // "< Back" button in top-left of map area ImGui::SetCursorPos(ImVec2(mapX + 8.0f, mapY + 8.0f)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.3f, 0.1f, 0.9f)); @@ -1193,12 +1085,11 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi ImGui::PopStyleColor(3); if (goBack) { - compositeZone(continentIdx); + requestComposite(continentIdx); currentIdx = continentIdx; viewLevel = ViewLevel::CONTINENT; } - // Zone name header const char* zoneName = zones[currentIdx].areaName.c_str(); ImVec2 nameSize = ImGui::CalcTextSize(zoneName); float nameY = mapY - nameSize.y - 8.0f; @@ -1208,7 +1099,7 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi } } - // --- Continent view: back to world --- + // Continent view: back to world if (viewLevel == ViewLevel::CONTINENT) { auto& io = ImGui::GetIO(); bool goWorld = io.MouseClicked[1]; @@ -1226,13 +1117,13 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi // Help text const char* helpText; - if (viewLevel == ViewLevel::ZONE) { + if (viewLevel == ViewLevel::ZONE) helpText = "Scroll out or right-click to zoom out | M or Escape to close"; - } else if (viewLevel == ViewLevel::WORLD) { + else if (viewLevel == ViewLevel::WORLD) helpText = "Select a continent | Scroll in to zoom | M or Escape to close"; - } else { + else helpText = "Click zone or scroll in to zoom | Scroll out / right-click for World | M or Escape to close"; - } + ImVec2 textSize = ImGui::CalcTextSize(helpText); float textY = mapY + displayH + 8.0f; if (textY + textSize.y < sh) { diff --git a/src/ui/character_create_screen.cpp b/src/ui/character_create_screen.cpp index b1c548c4..0fd00b9d 100644 --- a/src/ui/character_create_screen.cpp +++ b/src/ui/character_create_screen.cpp @@ -363,11 +363,14 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) { static_cast(preview_->getHeight())); } - ImGui::Image( - static_cast(preview_->getTextureId()), - ImVec2(imgW, imgH), - ImVec2(0.0f, 1.0f), // UV top-left (flipped Y) - ImVec2(1.0f, 0.0f)); // UV bottom-right (flipped Y) + // TODO: Vulkan offscreen preview render target + if (preview_->getTextureId()) { + ImGui::Image( + static_cast(0), + ImVec2(imgW, imgH), + ImVec2(0.0f, 1.0f), + ImVec2(1.0f, 0.0f)); + } // Mouse drag rotation on the preview image if (ImGui::IsItemHovered() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { diff --git a/src/ui/character_screen.cpp b/src/ui/character_screen.cpp index 64e24716..7f661c40 100644 --- a/src/ui/character_screen.cpp +++ b/src/ui/character_screen.cpp @@ -301,11 +301,14 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { imgW = imgH * (static_cast(preview_->getWidth()) / static_cast(preview_->getHeight())); } - ImGui::Image( - static_cast(preview_->getTextureId()), - ImVec2(imgW, imgH), - ImVec2(0.0f, 1.0f), // flip Y for OpenGL - ImVec2(1.0f, 0.0f)); + // TODO: Vulkan offscreen preview render target + if (preview_->getTextureId()) { + ImGui::Image( + static_cast(0), + ImVec2(imgW, imgH), + ImVec2(0.0f, 1.0f), + ImVec2(1.0f, 0.0f)); + } if (ImGui::IsItemHovered() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { preview_->rotate(ImGui::GetIO().MouseDelta.x * 0.2f); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 843decdb..0c816556 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -7,6 +7,7 @@ #include "rendering/renderer.hpp" #include "rendering/terrain_manager.hpp" #include "rendering/minimap.hpp" +#include "rendering/world_map.hpp" #include "rendering/character_renderer.hpp" #include "rendering/camera.hpp" #include "rendering/camera_controller.hpp" @@ -3129,8 +3130,8 @@ void GameScreen::updateCharacterTextures(game::Inventory& inventory) { charRenderer->clearCompositeCache(); // Use per-instance texture override (not model-level) to avoid deleting cached composites. uint32_t instanceId = renderer->getCharacterInstanceId(); - GLuint newTex = charRenderer->compositeWithRegions(bodySkinPath, underwearPaths, regionLayers); - if (newTex != 0 && instanceId != 0) { + auto* newTex = charRenderer->compositeWithRegions(bodySkinPath, underwearPaths, regionLayers); + if (newTex != nullptr && instanceId != 0) { charRenderer->setTextureSlotOverride(instanceId, static_cast(skinSlot), newTex); } @@ -3155,8 +3156,8 @@ void GameScreen::updateCharacterTextures(game::Inventory& inventory) { std::string capeName = displayInfoDbc->getString(static_cast(recIdx), dispL ? (*dispL)["LeftModelTexture"] : 3); if (!capeName.empty()) { std::string capePath = "Item\\ObjectComponents\\Cape\\" + capeName + ".blp"; - GLuint capeTex = charRenderer->loadTexture(capePath); - if (capeTex != 0) { + auto* capeTex = charRenderer->loadTexture(capePath); + if (capeTex != nullptr) { charRenderer->setTextureSlotOverride(instanceId, static_cast(cloakSlot), capeTex); LOG_INFO("Cloak texture applied: ", capePath); } @@ -3176,17 +3177,17 @@ void GameScreen::updateCharacterTextures(game::Inventory& inventory) { void GameScreen::renderWorldMap(game::GameHandler& gameHandler) { auto& app = core::Application::getInstance(); auto* renderer = app.getRenderer(); - auto* assetMgr = app.getAssetManager(); - if (!renderer || !assetMgr) return; + if (!renderer) return; - worldMap.initialize(assetMgr); + auto* wm = renderer->getWorldMap(); + if (!wm) return; // Keep map name in sync with minimap's map name auto* minimap = renderer->getMinimap(); if (minimap) { - worldMap.setMapName(minimap->getMapName()); + wm->setMapName(minimap->getMapName()); } - worldMap.setServerExplorationMask( + wm->setServerExplorationMask( gameHandler.getPlayerExploredZoneMasks(), gameHandler.hasPlayerExploredZoneMasks()); @@ -3194,7 +3195,7 @@ void GameScreen::renderWorldMap(game::GameHandler& gameHandler) { auto* window = app.getWindow(); int screenW = window ? window->getWidth() : 1280; int screenH = window ? window->getHeight() : 720; - worldMap.render(playerPos, screenW, screenH); + wm->render(playerPos, screenW, screenH); } // ============================================================ diff --git a/src/ui/ui_manager.cpp b/src/ui/ui_manager.cpp index ff691070..91ef2bcc 100644 --- a/src/ui/ui_manager.cpp +++ b/src/ui/ui_manager.cpp @@ -4,9 +4,10 @@ #include "core/logger.hpp" #include "auth/auth_handler.hpp" #include "game/game_handler.hpp" +#include "rendering/vk_context.hpp" #include #include -#include +#include namespace wowee { namespace ui { @@ -26,6 +27,12 @@ bool UIManager::initialize(core::Window* win) { window = win; LOG_INFO("Initializing UI manager"); + auto* vkCtx = window->getVkContext(); + if (!vkCtx) { + LOG_ERROR("No Vulkan context available for ImGui initialization"); + return false; + } + // Initialize ImGui IMGUI_CHECKVERSION(); ImGui::CreateContext(); @@ -56,19 +63,37 @@ bool UIManager::initialize(core::Window* win) { colors[ImGuiCol_HeaderHovered] = ImVec4(0.25f, 0.30f, 0.50f, 0.80f); colors[ImGuiCol_HeaderActive] = ImVec4(0.20f, 0.25f, 0.45f, 1.00f); - // Initialize ImGui for SDL2 and OpenGL3 - ImGui_ImplSDL2_InitForOpenGL(window->getSDLWindow(), window->getGLContext()); - ImGui_ImplOpenGL3_Init("#version 330 core"); + // Initialize ImGui for SDL2 + Vulkan + ImGui_ImplSDL2_InitForVulkan(window->getSDLWindow()); + + ImGui_ImplVulkan_InitInfo initInfo{}; + initInfo.ApiVersion = VK_API_VERSION_1_1; + initInfo.Instance = vkCtx->getInstance(); + initInfo.PhysicalDevice = vkCtx->getPhysicalDevice(); + initInfo.Device = vkCtx->getDevice(); + initInfo.QueueFamily = vkCtx->getGraphicsQueueFamily(); + initInfo.Queue = vkCtx->getGraphicsQueue(); + initInfo.DescriptorPool = vkCtx->getImGuiDescriptorPool(); + initInfo.MinImageCount = 2; + initInfo.ImageCount = vkCtx->getSwapchainImageCount(); + initInfo.PipelineInfoMain.RenderPass = vkCtx->getImGuiRenderPass(); + initInfo.PipelineInfoMain.MSAASamples = VK_SAMPLE_COUNT_1_BIT; + + ImGui_ImplVulkan_Init(&initInfo); imguiInitialized = true; - LOG_INFO("UI manager initialized successfully"); + LOG_INFO("UI manager initialized successfully (Vulkan)"); return true; } void UIManager::shutdown() { if (imguiInitialized) { - ImGui_ImplOpenGL3_Shutdown(); + auto* vkCtx = window ? window->getVkContext() : nullptr; + if (vkCtx) { + vkDeviceWaitIdle(vkCtx->getDevice()); + } + ImGui_ImplVulkan_Shutdown(); ImGui_ImplSDL2_Shutdown(); ImGui::DestroyContext(); imguiInitialized = false; @@ -80,7 +105,7 @@ void UIManager::update([[maybe_unused]] float deltaTime) { if (!imguiInitialized) return; // Start ImGui frame - ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplVulkan_NewFrame(); ImGui_ImplSDL2_NewFrame(); ImGui::NewFrame(); } @@ -126,7 +151,6 @@ void UIManager::render(core::AppState appState, auth::AuthHandler* authHandler, case core::AppState::DISCONNECTED: authScreen->stopLoginMusic(); - // Show disconnected message ImGui::SetNextWindowSize(ImVec2(400, 150), ImGuiCond_Always); ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f - 200, ImGui::GetIO().DisplaySize.y * 0.5f - 75), @@ -141,9 +165,8 @@ void UIManager::render(core::AppState appState, auth::AuthHandler* authHandler, break; } - // Render ImGui + // Finalize ImGui draw data (actual rendering happens in the command buffer) ImGui::Render(); - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); } void UIManager::processEvent(const SDL_Event& event) {