From 4fc3689dcc93863bcdec06a64ec8e240b50c6f2f Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 21 Feb 2026 21:57:16 -0800 Subject: [PATCH] Fix sky and clouds orientation for Z-up world coordinates Skybox: replace sphere-mesh approach with a fullscreen triangle that reconstructs the world-space ray direction analytically via inverse projection/view matrices. Eliminates clip.w=0 degeneracy at the horizon and correctly maps altitude to dir.z in the Z-up coordinate system. Clouds: hemisphere mesh was storing altitude in aPos.y (Y-up convention); the Z-up view matrix projected this sideways, making clouds appear vertical. Store altitude in aPos.z and update the fragment shader to read dir.z as altitude and dir.xy as the horizontal UV plane. --- assets/shaders/clouds.frag.glsl | 4 +- assets/shaders/clouds.frag.spv | Bin 5356 -> 5372 bytes assets/shaders/skybox.frag.glsl | 23 ++++++- assets/shaders/skybox.frag.spv | Bin 2532 -> 3568 bytes assets/shaders/skybox.vert.glsl | 27 ++------ assets/shaders/skybox.vert.spv | Bin 2756 -> 1220 bytes include/rendering/skybox.hpp | 15 +--- src/rendering/clouds.cpp | 8 +-- src/rendering/skybox.cpp | 117 ++------------------------------ 9 files changed, 39 insertions(+), 155 deletions(-) diff --git a/assets/shaders/clouds.frag.glsl b/assets/shaders/clouds.frag.glsl index 419e0b7b..fec28507 100644 --- a/assets/shaders/clouds.frag.glsl +++ b/assets/shaders/clouds.frag.glsl @@ -38,10 +38,10 @@ float fbm(vec2 p) { void main() { vec3 dir = normalize(vWorldDir); - float altitude = dir.y; + float altitude = dir.z; // Z is up in the Z-up world coordinate system if (altitude < 0.0) discard; - vec2 uv = dir.xz / (altitude + 0.001); + vec2 uv = dir.xy / (altitude + 0.001); // XY is the horizontal plane uv += push.windOffset; float cloud1 = fbm(uv * 0.8); diff --git a/assets/shaders/clouds.frag.spv b/assets/shaders/clouds.frag.spv index b1118d83e373e82aec13dfc6476ea52aee338654..7d09eb467a3823429dcd040c36878a0ecf280f69 100644 GIT binary patch literal 5372 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 Low#4}zA63(RINGS)) * (static_cast(M_PI) * 0.5f); - float y = RADIUS * std::cos(phi); + float altZ = RADIUS * std::cos(phi); // altitude → world Z (up) float ringRadius = RADIUS * std::sin(phi); for (int seg = 0; seg <= SEGMENTS; ++seg) { float theta = (seg / static_cast(SEGMENTS)) * (2.0f * static_cast(M_PI)); float x = ringRadius * std::cos(theta); - float z = ringRadius * std::sin(theta); - vertices_.push_back(glm::vec3(x, y, z)); + float y = ringRadius * std::sin(theta); + vertices_.push_back(glm::vec3(x, y, altZ)); } } diff --git a/src/rendering/skybox.cpp b/src/rendering/skybox.cpp index 368c7cbe..a51e2706 100644 --- a/src/rendering/skybox.cpp +++ b/src/rendering/skybox.cpp @@ -3,11 +3,9 @@ #include "rendering/vk_shader.hpp" #include "rendering/vk_pipeline.hpp" #include "rendering/vk_frame_data.hpp" -#include "rendering/vk_utils.hpp" #include "core/logger.hpp" #include #include -#include namespace wowee { namespace rendering { @@ -54,18 +52,7 @@ bool Skybox::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { 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; - + // Fullscreen triangle — no vertex buffer, no vertex input. // Dynamic viewport and scissor std::vector dynamicStates = { VK_DYNAMIC_STATE_VIEWPORT, @@ -74,7 +61,7 @@ bool Skybox::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { pipeline = PipelineBuilder() .setShaders(vertStage, fragStage) - .setVertexInput({binding}, {posAttr}) + .setVertexInput({}, {}) .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 @@ -93,16 +80,11 @@ bool Skybox::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { return false; } - // Create sky dome mesh and upload to GPU - createSkyDome(); - LOG_INFO("Skybox initialized"); return true; } void Skybox::shutdown() { - destroySkyDome(); - if (vkCtx) { VkDevice device = vkCtx->getDevice(); if (pipeline != VK_NULL_HANDLE) { @@ -149,15 +131,8 @@ void Skybox::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, float time VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), &push); - // Bind vertex buffer - VkDeviceSize offset = 0; - vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset); - - // Bind index buffer - vkCmdBindIndexBuffer(cmd, indexBuffer, 0, VK_INDEX_TYPE_UINT32); - - // Draw - vkCmdDrawIndexed(cmd, static_cast(indexCount), 1, 0, 0, 0); + // Draw fullscreen triangle — no vertex buffer needed + vkCmdDraw(cmd, 3, 1, 0, 0); } void Skybox::update(float deltaTime) { @@ -179,90 +154,6 @@ void Skybox::setTimeOfDay(float time) { timeOfDay = time; } -void Skybox::createSkyDome() { - // Create an extended dome that goes below horizon for better coverage - const int rings = 16; // Vertical resolution - const int sectors = 32; // Horizontal resolution - const float radius = 2000.0f; // Large enough to cover view without looking curved - - std::vector vertices; - std::vector indices; - - // Generate vertices - extend slightly below horizon - const float minPhi = -M_PI / 12.0f; // Start 15° below horizon - const float maxPhi = M_PI / 2.0f; // End at zenith - for (int ring = 0; ring <= rings; ring++) { - float phi = minPhi + (maxPhi - minPhi) * (static_cast(ring) / rings); - float y = radius * std::sin(phi); - float ringRadius = radius * std::cos(phi); - - for (int sector = 0; sector <= sectors; sector++) { - float theta = (2.0f * M_PI) * (static_cast(sector) / sectors); - float x = ringRadius * std::cos(theta); - float z = ringRadius * std::sin(theta); - - // Position - vertices.push_back(x); - vertices.push_back(z); // Z up in WoW coordinates - vertices.push_back(y); - } - } - - // Generate indices - for (int ring = 0; ring < rings; ring++) { - for (int sector = 0; sector < sectors; sector++) { - int current = ring * (sectors + 1) + sector; - int next = current + sectors + 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); - } - } - - indexCount = static_cast(indices.size()); - - // 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; - - // 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 (!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; - } -} - glm::vec3 Skybox::getHorizonColor(float time) const { // Time-based horizon colors // 0-6: Night (dark blue)