mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-24 16:10:14 +00:00
Vulcan Nightmare
Experimentally bringing up vulcan support
This commit is contained in:
parent
863a786c48
commit
83b576e8d9
189 changed files with 12147 additions and 7820 deletions
|
|
@ -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 <GL/glew.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
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<Shader>();
|
||||
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<VkDynamicState> 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<float>(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
|
||||
|
|
|
|||
|
|
@ -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 <GL/glew.h>
|
||||
#include "core/application.hpp"
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <algorithm>
|
||||
#include <unordered_set>
|
||||
|
|
@ -24,11 +27,13 @@ bool CharacterPreview::initialize(pipeline::AssetManager* am) {
|
|||
assetManager_ = am;
|
||||
|
||||
charRenderer_ = std::make_unique<CharacterRenderer>();
|
||||
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<uint32_t>(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<uint32_t>(ti), hairTex);
|
||||
|
|
@ -511,8 +491,8 @@ bool CharacterPreview::applyEquipment(const std::vector<game::EquipmentItem>& 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<game::EquipmentItem>& 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<GLuint>(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) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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 <glm/gtc/matrix_transform.hpp>
|
||||
#include <random>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
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<Shader>();
|
||||
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<VkDynamicState> 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<VkVertexInputAttributeDescription> 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<VkVertexInputAttributeDescription> 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<Shader>();
|
||||
|
||||
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<int>(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<GLsizei>(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<uint32_t>(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<GLsizei>(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<uint32_t>(dustPuffs_.size()), 1, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <cmath>
|
||||
|
||||
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<Shader>();
|
||||
|
||||
// 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<VkDynamicState> 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<float>(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<float>(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<int>(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<uint32_t>(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<GLsizei>(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<float>(RINGS)) * (static_cast<float>(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<float>(SEGMENTS)) * (2.0f * static_cast<float>(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<uint32_t>(ring * (SEGMENTS + 1) + seg);
|
||||
uint32_t next = current + static_cast<uint32_t>(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<int>(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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <cmath>
|
||||
|
||||
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<Shader>();
|
||||
// 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<VkDynamicState> 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
|
||||
|
|
|
|||
|
|
@ -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 <GL/glew.h>
|
||||
#include <random>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
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<Shader>();
|
||||
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<Shader>();
|
||||
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<VkDynamicState> 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<GLsizei>(bolt.segments.size()));
|
||||
uint32_t vertexCount = static_cast<uint32_t>(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) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
#include "rendering/loading_screen.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
#include <imgui_impl_opengl3.h>
|
||||
#include <imgui_impl_vulkan.h>
|
||||
#include <imgui_impl_sdl2.h>
|
||||
#include <random>
|
||||
#include <chrono>
|
||||
|
|
@ -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<VkDeviceSize>(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<uint32_t>(imageWidth), static_cast<uint32_t>(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<uint32_t>(imageWidth), static_cast<uint32_t>(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<ImTextureID>(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<int>(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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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 <GL/glew.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <sstream>
|
||||
#include <cmath>
|
||||
|
|
@ -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<VkRenderTarget>();
|
||||
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<VkTexture>();
|
||||
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<VkDescriptorSetLayout> 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<VkVertexInputAttributeDescription> 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<Shader>();
|
||||
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<Shader>();
|
||||
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<hash>.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<VkTexture>();
|
||||
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<float>(dc + 1); // 0, 1, 2
|
||||
float row = static_cast<float>(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<float>(dc + 1),
|
||||
static_cast<float>(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<float>(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<float>(tileX) - centerWorldPos.y / TILE_SIZE;
|
||||
// fracEW: 0 = west edge of tile, 1 = east edge
|
||||
float fracEW = 32.0f - static_cast<float>(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
|
||||
|
|
|
|||
|
|
@ -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 <glm/gtc/matrix_transform.hpp>
|
||||
#include <random>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
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<Shader>();
|
||||
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<VkVertexInputAttributeDescription> 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<VkDynamicState> 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<GLsizei>(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<uint32_t>(particles.size()), 1, 0, 0);
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
|
|
|
|||
|
|
@ -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 <GL/glew.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <SDL2/SDL.h>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
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<VkDynamicState> 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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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<Skybox>();
|
||||
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<Celestial>();
|
||||
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<StarField>();
|
||||
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<Clouds>();
|
||||
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<LensFlare>();
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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 <GL/glew.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
|
@ -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<Shader>();
|
||||
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<VkDynamicState> 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<uint32_t>(indexCount), 1, 0, 0, 0);
|
||||
}
|
||||
|
||||
void Skybox::update(float deltaTime) {
|
||||
|
|
@ -193,42 +227,39 @@ void Skybox::createSkyDome() {
|
|||
|
||||
indexCount = static_cast<int>(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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <GL/glew.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/glm.hpp>
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
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<Shader>();
|
||||
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<uint32_t>(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<float> phiDist(0.0f, M_PI / 2.0f); // 0 to 90 degrees (hemisphere)
|
||||
std::uniform_real_distribution<float> thetaDist(0.0f, 2.0f * M_PI); // 0 to 360 degrees
|
||||
std::uniform_real_distribution<float> brightnessDist(0.3f, 1.0f); // Varying brightness
|
||||
std::uniform_real_distribution<float> twinkleDist(0.0f, 2.0f * M_PI); // Random twinkle phase
|
||||
std::uniform_real_distribution<float> phiDist(0.0f, M_PI / 2.0f); // 0–90° (upper hemisphere)
|
||||
std::uniform_real_distribution<float> thetaDist(0.0f, 2.0f * M_PI); // 0–360°
|
||||
std::uniform_real_distribution<float> brightnessDist(0.3f, 1.0f);
|
||||
std::uniform_real_distribution<float> 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<float> 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 {
|
||||
|
|
|
|||
|
|
@ -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 <glm/gtc/matrix_transform.hpp>
|
||||
#include <random>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
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<Shader>();
|
||||
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<VkVertexInputAttributeDescription> 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<VkDynamicState> 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<Shader>();
|
||||
|
||||
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<GLsizei>(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<uint32_t>(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<GLsizei>(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<uint32_t>(bubbleVertexData.size() / 5), 1, 0, 0);
|
||||
}
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glDepthMask(GL_TRUE);
|
||||
glDisable(GL_PROGRAM_POINT_SIZE);
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
|
|
|
|||
|
|
@ -726,7 +726,7 @@ void TerrainManager::finalizeTile(const std::shared_ptr<PendingTile>& 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<PendingTile>& 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;
|
||||
|
|
|
|||
|
|
@ -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 <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <cstring>
|
||||
|
||||
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<Shader>();
|
||||
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<VkDescriptorSetLayoutBinding> 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<VkDescriptorSetLayout> 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<VkVertexInputAttributeDescription> vertexAttribs(4);
|
||||
vertexAttribs[0] = { 0, 0, VK_FORMAT_R32G32B32_SFLOAT,
|
||||
static_cast<uint32_t>(offsetof(pipeline::TerrainVertex, position)) };
|
||||
vertexAttribs[1] = { 1, 0, VK_FORMAT_R32G32B32_SFLOAT,
|
||||
static_cast<uint32_t>(offsetof(pipeline::TerrainVertex, normal)) };
|
||||
vertexAttribs[2] = { 2, 0, VK_FORMAT_R32G32_SFLOAT,
|
||||
static_cast<uint32_t>(offsetof(pipeline::TerrainVertex, texCoord)) };
|
||||
vertexAttribs[3] = { 3, 0, VK_FORMAT_R32G32_SFLOAT,
|
||||
static_cast<uint32_t>(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<VkTexture>();
|
||||
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<VkTexture>();
|
||||
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<int>(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<int>(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<uint32_t>(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<VkTexture>();
|
||||
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<size_t>(blp.width) * static_cast<size_t>(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<std::string, pipeline::BLPImage>& textures) {
|
||||
void TerrainRenderer::uploadPreloadedTextures(
|
||||
const std::unordered_map<std::string, pipeline::BLPImage>& 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<std::stri
|
|||
};
|
||||
for (const auto& [path, blp] : textures) {
|
||||
std::string key = normalizeKey(path);
|
||||
// Skip if already cached
|
||||
if (textureCache.find(key) != textureCache.end()) continue;
|
||||
if (!blp.isValid()) {
|
||||
// Don't poison cache with white on invalid preload; allow fallback
|
||||
// path to retry loading this texture later.
|
||||
continue;
|
||||
}
|
||||
if (!blp.isValid()) continue;
|
||||
|
||||
GLuint textureID;
|
||||
glGenTextures(1, &textureID);
|
||||
glBindTexture(GL_TEXTURE_2D, textureID);
|
||||
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);
|
||||
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<VkTexture>();
|
||||
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<size_t>(blp.width) * static_cast<size_t>(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<uint8_t>& alphaData) {
|
||||
if (alphaData.empty()) {
|
||||
return opaqueAlphaTexture;
|
||||
}
|
||||
VkTexture* TerrainRenderer::createAlphaTexture(const std::vector<uint8_t>& 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<uint8_t> expanded;
|
||||
const uint8_t* src = alphaData.data();
|
||||
if (alphaData.size() < 4096) {
|
||||
|
|
@ -352,141 +411,105 @@ GLuint TerrainRenderer::createAlphaTexture(const std::vector<uint8_t>& alphaData
|
|||
src = expanded.data();
|
||||
}
|
||||
|
||||
int width = 64;
|
||||
int height = 64;
|
||||
auto tex = std::make_unique<VkTexture>();
|
||||
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<uint32_t>(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<float>::max());
|
||||
glm::vec3 max(std::numeric_limits<float>::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]);
|
||||
|
|
|
|||
91
src/rendering/vk_buffer.cpp
Normal file
91
src/rendering/vk_buffer.cpp
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#include "rendering/vk_buffer.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <cstring>
|
||||
|
||||
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<uint8_t*>(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
|
||||
664
src/rendering/vk_context.cpp
Normal file
664
src/rendering/vk_context.cpp
Normal file
|
|
@ -0,0 +1,664 @@
|
|||
#define VMA_IMPLEMENTATION
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <VkBootstrap.h>
|
||||
#include <SDL2/SDL_vulkan.h>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
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<PFN_vkDestroyDebugUtilsMessengerEXT>(
|
||||
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<const char*> 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<uint32_t>(width), static_cast<uint32_t>(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<uint32_t>(width), static_cast<uint32_t>(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<void(VkCommandBuffer cmd)>&& function) {
|
||||
VkCommandBuffer cmd = beginSingleTimeCommands();
|
||||
function(cmd);
|
||||
endSingleTimeCommands(cmd);
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
271
src/rendering/vk_pipeline.cpp
Normal file
271
src/rendering/vk_pipeline.cpp
Normal file
|
|
@ -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<VkVertexInputBindingDescription>& bindings,
|
||||
const std::vector<VkVertexInputAttributeDescription>& 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<VkDynamicState>& 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<uint32_t>(vertexBindings_.size());
|
||||
vertexInput.pVertexBindingDescriptions = vertexBindings_.data();
|
||||
vertexInput.vertexAttributeDescriptionCount = static_cast<uint32_t>(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<uint32_t>(colorBlendAttachments_.size());
|
||||
colorBlending.pAttachments = colorBlendAttachments_.data();
|
||||
|
||||
// Dynamic state
|
||||
VkPipelineDynamicStateCreateInfo dynamicState{};
|
||||
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
|
||||
dynamicState.dynamicStateCount = static_cast<uint32_t>(dynamicStates_.size());
|
||||
dynamicState.pDynamicStates = dynamicStates_.data();
|
||||
|
||||
// Create pipeline
|
||||
VkGraphicsPipelineCreateInfo pipelineInfo{};
|
||||
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
|
||||
pipelineInfo.stageCount = static_cast<uint32_t>(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<VkDescriptorSetLayout>& setLayouts,
|
||||
const std::vector<VkPushConstantRange>& pushConstants)
|
||||
{
|
||||
VkPipelineLayoutCreateInfo layoutInfo{};
|
||||
layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
||||
layoutInfo.setLayoutCount = static_cast<uint32_t>(setLayouts.size());
|
||||
layoutInfo.pSetLayouts = setLayouts.data();
|
||||
layoutInfo.pushConstantRangeCount = static_cast<uint32_t>(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<VkDescriptorSetLayoutBinding>& bindings)
|
||||
{
|
||||
VkDescriptorSetLayoutCreateInfo layoutInfo{};
|
||||
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
|
||||
layoutInfo.bindingCount = static_cast<uint32_t>(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
|
||||
170
src/rendering/vk_render_target.cpp
Normal file
170
src/rendering/vk_render_target.cpp
Normal file
|
|
@ -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<float>(colorImage_.extent.width);
|
||||
viewport.height = static_cast<float>(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
|
||||
114
src/rendering/vk_shader.cpp
Normal file
114
src/rendering/vk_shader.cpp
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
#include "rendering/vk_shader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <fstream>
|
||||
|
||||
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<size_t>(file.tellg());
|
||||
if (fileSize == 0 || fileSize % 4 != 0) {
|
||||
LOG_ERROR("Invalid SPIR-V file size (", fileSize, "): ", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> code(fileSize / sizeof(uint32_t));
|
||||
file.seekg(0);
|
||||
file.read(reinterpret_cast<char*>(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<uint32_t> code;
|
||||
if (file.is_open()) {
|
||||
size_t fileSize = static_cast<size_t>(file.tellg());
|
||||
code.resize(fileSize / sizeof(uint32_t));
|
||||
file.seekg(0);
|
||||
file.read(reinterpret_cast<char*>(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
|
||||
395
src/rendering/vk_texture.cpp
Normal file
395
src/rendering/vk_texture.cpp
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
#include "rendering/vk_texture.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
|
||||
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<uint32_t>(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<uint8_t*>(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<float>(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<float>(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<int32_t>(width);
|
||||
int32_t mipH = static_cast<int32_t>(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
|
||||
208
src/rendering/vk_utils.cpp
Normal file
208
src/rendering/vk_utils.cpp
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
#include "rendering/vk_utils.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <cstring>
|
||||
|
||||
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
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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 <glm/gtc/matrix_transform.hpp>
|
||||
#include <random>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
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<Shader>();
|
||||
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<VkDynamicState> 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<GLsizei>(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<uint32_t>(particlePositions.size()), 1, 0, 0);
|
||||
}
|
||||
|
||||
void Weather::resetParticles(const Camera& camera) {
|
||||
|
|
@ -260,15 +284,29 @@ int Weather::getParticleCount() const {
|
|||
return static_cast<int>(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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue