2026-02-02 12:24:50 -08:00
|
|
|
|
#include "rendering/starfield.hpp"
|
2026-02-21 19:41:21 -08:00
|
|
|
|
#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"
|
2026-02-02 12:24:50 -08:00
|
|
|
|
#include "core/logger.hpp"
|
2026-02-21 19:41:21 -08:00
|
|
|
|
#include <glm/glm.hpp>
|
2026-02-02 12:24:50 -08:00
|
|
|
|
#include <cmath>
|
|
|
|
|
|
#include <random>
|
2026-02-21 19:41:21 -08:00
|
|
|
|
#include <vector>
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
namespace wowee {
|
|
|
|
|
|
namespace rendering {
|
|
|
|
|
|
|
|
|
|
|
|
StarField::StarField() = default;
|
|
|
|
|
|
|
|
|
|
|
|
StarField::~StarField() {
|
|
|
|
|
|
shutdown();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
bool StarField::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) {
|
2026-02-02 12:24:50 -08:00
|
|
|
|
LOG_INFO("Initializing star field");
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
vkCtx = ctx;
|
|
|
|
|
|
VkDevice device = vkCtx->getDevice();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkShaderModule fragModule;
|
|
|
|
|
|
if (!fragModule.loadFromFile(device, "assets/shaders/starfield.frag.spv")) {
|
|
|
|
|
|
LOG_ERROR("Failed to load starfield fragment shader");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
|
|
|
|
|
|
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// 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
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// 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");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Generate star positions and upload to GPU
|
2026-02-02 12:24:50 -08:00
|
|
|
|
generateStars();
|
|
|
|
|
|
createStarBuffers();
|
|
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Star field initialized: ", starCount, " stars");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void StarField::shutdown() {
|
|
|
|
|
|
destroyStarBuffers();
|
2026-02-21 19:41:21 -08:00
|
|
|
|
|
|
|
|
|
|
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;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
stars.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
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()) {
|
2026-02-02 12:24:50 -08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Compute intensity from time of day then attenuate for clouds/fog
|
2026-02-02 12:24:50 -08:00
|
|
|
|
float intensity = getStarIntensity(timeOfDay);
|
Implement WoW-accurate DBC-driven sky system with lore-faithful celestial bodies
Add SkySystem coordinator that follows WoW's actual architecture where skyboxes
are authoritative and procedural elements serve as fallbacks. Integrate lighting
system across all renderers (terrain, WMO, M2, character) with unified parameters.
Sky System:
- SkySystem coordinator manages skybox, celestial bodies, stars, clouds, lens flare
- Skybox is authoritative (baked stars from M2 models, procedural fallback only)
- skyboxHasStars flag gates procedural star rendering (prevents double-star bug)
Celestial Bodies (Lore-Accurate):
- Two moons: White Lady (30-day cycle, pale white) + Blue Child (27-day cycle, pale blue)
- Deterministic moon phases from server gameTime (not deltaTime toys)
- Sun positioning driven by LightingManager directionalDir (DBC-sourced)
- Camera-locked sky dome (translation ignored, rotation applied)
Lighting Integration:
- Apply LightingManager params to WMO, M2, character renderers
- Unified lighting: directional light, diffuse color, ambient color, fog
- Star occlusion by cloud density (70% weight) and fog density (30% weight)
Documentation:
- Add comprehensive SKY_SYSTEM.md technical guide
- Update MEMORY.md with sky system architecture and anti-patterns
- Update README.md with WoW-accurate descriptions
Critical design decisions:
- NO latitude-based star rotation (Azeroth not modeled as spherical planet)
- NO always-on procedural stars (skybox authority prevents zone identity loss)
- NO universal dual-moon setup (map-specific celestial configurations)
2026-02-10 14:36:17 -08:00
|
|
|
|
intensity *= (1.0f - glm::clamp(cloudDensity * 0.7f, 0.0f, 1.0f));
|
|
|
|
|
|
intensity *= (1.0f - glm::clamp(fogDensity * 0.3f, 0.0f, 1.0f));
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
if (intensity <= 0.01f) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Push constants: time and intensity
|
|
|
|
|
|
struct StarPushConstants {
|
|
|
|
|
|
float time;
|
|
|
|
|
|
float intensity;
|
|
|
|
|
|
};
|
|
|
|
|
|
StarPushConstants push{twinkleTime, intensity};
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// 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);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
vkCmdPushConstants(cmd, pipelineLayout,
|
|
|
|
|
|
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
|
|
|
|
|
0, sizeof(push), &push);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Bind vertex buffer
|
|
|
|
|
|
VkDeviceSize offset = 0;
|
|
|
|
|
|
vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Draw all stars as individual points
|
|
|
|
|
|
vkCmdDraw(cmd, static_cast<uint32_t>(starCount), 1, 0, 0);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void StarField::update(float deltaTime) {
|
|
|
|
|
|
twinkleTime += deltaTime;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void StarField::generateStars() {
|
|
|
|
|
|
stars.clear();
|
|
|
|
|
|
stars.reserve(starCount);
|
|
|
|
|
|
|
|
|
|
|
|
std::random_device rd;
|
|
|
|
|
|
std::mt19937 gen(rd());
|
2026-02-21 19:41:21 -08:00
|
|
|
|
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);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
const float radius = 900.0f; // Slightly larger than skybox
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < starCount; i++) {
|
|
|
|
|
|
Star star;
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
float phi = phiDist(gen); // Elevation angle
|
2026-02-02 12:24:50 -08:00
|
|
|
|
float theta = thetaDist(gen); // Azimuth angle
|
|
|
|
|
|
|
|
|
|
|
|
float x = radius * std::sin(phi) * std::cos(theta);
|
|
|
|
|
|
float y = radius * std::sin(phi) * std::sin(theta);
|
|
|
|
|
|
float z = radius * std::cos(phi);
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
star.position = glm::vec3(x, y, z);
|
|
|
|
|
|
star.brightness = brightnessDist(gen);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
star.twinklePhase = twinkleDist(gen);
|
|
|
|
|
|
|
|
|
|
|
|
stars.push_back(star);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG("Generated ", stars.size(), " stars");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void StarField::createStarBuffers() {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Interleaved vertex data: pos.x, pos.y, pos.z, brightness, twinklePhase
|
2026-02-02 12:24:50 -08:00
|
|
|
|
std::vector<float> vertexData;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
vertexData.reserve(stars.size() * 5);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
for (const auto& star : stars) {
|
|
|
|
|
|
vertexData.push_back(star.position.x);
|
|
|
|
|
|
vertexData.push_back(star.position.y);
|
|
|
|
|
|
vertexData.push_back(star.position.z);
|
|
|
|
|
|
vertexData.push_back(star.brightness);
|
|
|
|
|
|
vertexData.push_back(star.twinklePhase);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkDeviceSize bufferSize = vertexData.size() * sizeof(float);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Upload via staging buffer to GPU-local memory
|
|
|
|
|
|
AllocatedBuffer gpuBuf = uploadBuffer(*vkCtx, vertexData.data(), bufferSize,
|
|
|
|
|
|
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
vertexBuffer = gpuBuf.buffer;
|
|
|
|
|
|
vertexAlloc = gpuBuf.allocation;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void StarField::destroyStarBuffers() {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (vkCtx && vertexBuffer != VK_NULL_HANDLE) {
|
|
|
|
|
|
vmaDestroyBuffer(vkCtx->getAllocator(), vertexBuffer, vertexAlloc);
|
|
|
|
|
|
vertexBuffer = VK_NULL_HANDLE;
|
|
|
|
|
|
vertexAlloc = VK_NULL_HANDLE;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
float StarField::getStarIntensity(float timeOfDay) const {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Full night: 20:00–4:00
|
2026-02-02 12:24:50 -08:00
|
|
|
|
if (timeOfDay >= 20.0f || timeOfDay < 4.0f) {
|
|
|
|
|
|
return 1.0f;
|
|
|
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Fade in at dusk: 18:00–20:00
|
2026-02-02 12:24:50 -08:00
|
|
|
|
else if (timeOfDay >= 18.0f && timeOfDay < 20.0f) {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
return (timeOfDay - 18.0f) / 2.0f; // 0 → 1 over 2 hours
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Fade out at dawn: 4:00–6:00
|
2026-02-02 12:24:50 -08:00
|
|
|
|
else if (timeOfDay >= 4.0f && timeOfDay < 6.0f) {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
return 1.0f - (timeOfDay - 4.0f) / 2.0f; // 1 → 0 over 2 hours
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
// Daytime: no stars
|
|
|
|
|
|
else {
|
|
|
|
|
|
return 0.0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} // namespace rendering
|
|
|
|
|
|
} // namespace wowee
|