Vulcan Nightmare

Experimentally bringing up vulcan support
This commit is contained in:
Kelsi 2026-02-21 19:41:21 -08:00
parent 863a786c48
commit 83b576e8d9
189 changed files with 12147 additions and 7820 deletions

View file

@ -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:0019: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:005: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:005: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