mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
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.
This commit is contained in:
parent
69cf39ba02
commit
4fc3689dcc
9 changed files with 39 additions and 155 deletions
|
|
@ -38,10 +38,10 @@ float fbm(vec2 p) {
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec3 dir = normalize(vWorldDir);
|
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;
|
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;
|
uv += push.windOffset;
|
||||||
|
|
||||||
float cloud1 = fbm(uv * 0.8);
|
float cloud1 = fbm(uv * 0.8);
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -19,13 +19,30 @@ layout(push_constant) uniform Push {
|
||||||
float timeOfDay;
|
float timeOfDay;
|
||||||
} push;
|
} push;
|
||||||
|
|
||||||
layout(location = 0) in vec3 WorldPos;
|
layout(location = 0) in vec2 TexCoord;
|
||||||
layout(location = 1) in float Altitude;
|
|
||||||
|
|
||||||
layout(location = 0) out vec4 outColor;
|
layout(location = 0) out vec4 outColor;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
float t = clamp(Altitude, 0.0, 1.0);
|
// Reconstruct world-space ray direction from screen position.
|
||||||
|
// TexCoord is [0,1]^2; convert to NDC [-1,1]^2.
|
||||||
|
float ndcX = TexCoord.x * 2.0 - 1.0;
|
||||||
|
float ndcY = -(TexCoord.y * 2.0 - 1.0); // flip Y: Vulkan NDC Y-down, but projection already flipped
|
||||||
|
|
||||||
|
// Unproject to view space using focal lengths from projection matrix.
|
||||||
|
// projection[0][0] = 2*near/(right-left) = 1/tan(fovX/2)
|
||||||
|
// projection[1][1] = 2*near/(top-bottom) (already negated for Vulkan Y-flip)
|
||||||
|
// We want the original magnitude, so take abs to get the focal length.
|
||||||
|
vec3 viewDir = vec3(ndcX / projection[0][0],
|
||||||
|
ndcY / abs(projection[1][1]),
|
||||||
|
-1.0);
|
||||||
|
|
||||||
|
// Rotate to world space: view = R*T, so R^-1 = R^T = transpose(mat3(view))
|
||||||
|
mat3 invViewRot = transpose(mat3(view));
|
||||||
|
vec3 worldDir = normalize(invViewRot * viewDir);
|
||||||
|
|
||||||
|
// worldDir.z = sin(elevation); +1 = zenith, 0 = horizon, -1 = nadir
|
||||||
|
float t = clamp(worldDir.z, 0.0, 1.0);
|
||||||
t = pow(t, 1.5);
|
t = pow(t, 1.5);
|
||||||
vec3 sky = mix(push.horizonColor.rgb, push.zenithColor.rgb, t);
|
vec3 sky = mix(push.horizonColor.rgb, push.zenithColor.rgb, t);
|
||||||
float scatter = max(0.0, 1.0 - t * 2.0) * 0.15;
|
float scatter = max(0.0, 1.0 - t * 2.0) * 0.15;
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,27 +1,12 @@
|
||||||
#version 450
|
#version 450
|
||||||
|
|
||||||
layout(set = 0, binding = 0) uniform PerFrame {
|
// Fullscreen triangle sky — no vertex buffer, no mesh.
|
||||||
mat4 view;
|
// Draws 3 vertices covering the entire screen, depth forced to 1.0 (far plane).
|
||||||
mat4 projection;
|
|
||||||
mat4 lightSpaceMatrix;
|
|
||||||
vec4 lightDir;
|
|
||||||
vec4 lightColor;
|
|
||||||
vec4 ambientColor;
|
|
||||||
vec4 viewPos;
|
|
||||||
vec4 fogColor;
|
|
||||||
vec4 fogParams; // x=fogStart, y=fogEnd, z=time
|
|
||||||
vec4 shadowParams; // x=enabled, y=strength
|
|
||||||
};
|
|
||||||
|
|
||||||
layout(location = 0) in vec3 aPos;
|
layout(location = 0) out vec2 TexCoord;
|
||||||
|
|
||||||
layout(location = 0) out vec3 WorldPos;
|
|
||||||
layout(location = 1) out float Altitude;
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
WorldPos = aPos;
|
// Produces triangle covering NDC [-1,1]² with depth = 1.0 (far)
|
||||||
Altitude = aPos.y;
|
TexCoord = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
|
||||||
mat4 rotView = mat4(mat3(view)); // strip translation
|
gl_Position = vec4(TexCoord * 2.0 - 1.0, 1.0, 1.0);
|
||||||
vec4 pos = projection * rotView * vec4(aPos, 1.0);
|
|
||||||
gl_Position = pos.xyww; // force far plane
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -13,8 +13,9 @@ class VkContext;
|
||||||
/**
|
/**
|
||||||
* Skybox renderer
|
* Skybox renderer
|
||||||
*
|
*
|
||||||
* Renders an atmospheric sky dome with gradient colors.
|
* Renders an atmospheric sky gradient using a fullscreen triangle.
|
||||||
* The sky uses a dome/sphere approach for realistic appearance.
|
* No vertex buffer: 3 vertices cover the entire screen via gl_VertexIndex.
|
||||||
|
* World-space ray direction is reconstructed from the inverse view+projection.
|
||||||
*/
|
*/
|
||||||
class Skybox {
|
class Skybox {
|
||||||
public:
|
public:
|
||||||
|
|
@ -62,9 +63,6 @@ public:
|
||||||
glm::vec3 getHorizonColor(float time) const;
|
glm::vec3 getHorizonColor(float time) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void createSkyDome();
|
|
||||||
void destroySkyDome();
|
|
||||||
|
|
||||||
glm::vec3 getSkyColor(float altitude, float time) const;
|
glm::vec3 getSkyColor(float altitude, float time) const;
|
||||||
glm::vec3 getZenithColor(float time) const;
|
glm::vec3 getZenithColor(float time) const;
|
||||||
|
|
||||||
|
|
@ -73,13 +71,6 @@ private:
|
||||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||||
|
|
||||||
VkBuffer vertexBuffer = VK_NULL_HANDLE;
|
|
||||||
VmaAllocation vertexAlloc = VK_NULL_HANDLE;
|
|
||||||
VkBuffer indexBuffer = VK_NULL_HANDLE;
|
|
||||||
VmaAllocation indexAlloc = VK_NULL_HANDLE;
|
|
||||||
|
|
||||||
int indexCount = 0;
|
|
||||||
|
|
||||||
float timeOfDay = 12.0f; // Default: noon
|
float timeOfDay = 12.0f; // Default: noon
|
||||||
float timeSpeed = 1.0f; // 1.0 = 1 hour per real second
|
float timeSpeed = 1.0f; // 1.0 = 1 hour per real second
|
||||||
bool timeProgressionEnabled = false;
|
bool timeProgressionEnabled = false;
|
||||||
|
|
|
||||||
|
|
@ -202,17 +202,17 @@ void Clouds::generateMesh() {
|
||||||
vertices_.clear();
|
vertices_.clear();
|
||||||
indices_.clear();
|
indices_.clear();
|
||||||
|
|
||||||
// Upper hemisphere
|
// Upper hemisphere — Z-up world: altitude goes into Z, horizontal spread in X/Y
|
||||||
for (int ring = 0; ring <= RINGS; ++ring) {
|
for (int ring = 0; ring <= RINGS; ++ring) {
|
||||||
float phi = (ring / static_cast<float>(RINGS)) * (static_cast<float>(M_PI) * 0.5f);
|
float phi = (ring / static_cast<float>(RINGS)) * (static_cast<float>(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);
|
float ringRadius = RADIUS * std::sin(phi);
|
||||||
|
|
||||||
for (int seg = 0; seg <= SEGMENTS; ++seg) {
|
for (int seg = 0; seg <= SEGMENTS; ++seg) {
|
||||||
float theta = (seg / static_cast<float>(SEGMENTS)) * (2.0f * static_cast<float>(M_PI));
|
float theta = (seg / static_cast<float>(SEGMENTS)) * (2.0f * static_cast<float>(M_PI));
|
||||||
float x = ringRadius * std::cos(theta);
|
float x = ringRadius * std::cos(theta);
|
||||||
float z = ringRadius * std::sin(theta);
|
float y = ringRadius * std::sin(theta);
|
||||||
vertices_.push_back(glm::vec3(x, y, z));
|
vertices_.push_back(glm::vec3(x, y, altZ));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,9 @@
|
||||||
#include "rendering/vk_shader.hpp"
|
#include "rendering/vk_shader.hpp"
|
||||||
#include "rendering/vk_pipeline.hpp"
|
#include "rendering/vk_pipeline.hpp"
|
||||||
#include "rendering/vk_frame_data.hpp"
|
#include "rendering/vk_frame_data.hpp"
|
||||||
#include "rendering/vk_utils.hpp"
|
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
#include <glm/gtc/matrix_transform.hpp>
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
@ -54,18 +52,7 @@ bool Skybox::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertex input: position only (vec3), stride = 3 * sizeof(float)
|
// Fullscreen triangle — no vertex buffer, no vertex input.
|
||||||
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
|
// Dynamic viewport and scissor
|
||||||
std::vector<VkDynamicState> dynamicStates = {
|
std::vector<VkDynamicState> dynamicStates = {
|
||||||
VK_DYNAMIC_STATE_VIEWPORT,
|
VK_DYNAMIC_STATE_VIEWPORT,
|
||||||
|
|
@ -74,7 +61,7 @@ bool Skybox::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) {
|
||||||
|
|
||||||
pipeline = PipelineBuilder()
|
pipeline = PipelineBuilder()
|
||||||
.setShaders(vertStage, fragStage)
|
.setShaders(vertStage, fragStage)
|
||||||
.setVertexInput({binding}, {posAttr})
|
.setVertexInput({}, {})
|
||||||
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
||||||
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
.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
|
.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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create sky dome mesh and upload to GPU
|
|
||||||
createSkyDome();
|
|
||||||
|
|
||||||
LOG_INFO("Skybox initialized");
|
LOG_INFO("Skybox initialized");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Skybox::shutdown() {
|
void Skybox::shutdown() {
|
||||||
destroySkyDome();
|
|
||||||
|
|
||||||
if (vkCtx) {
|
if (vkCtx) {
|
||||||
VkDevice device = vkCtx->getDevice();
|
VkDevice device = vkCtx->getDevice();
|
||||||
if (pipeline != VK_NULL_HANDLE) {
|
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,
|
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||||
0, sizeof(push), &push);
|
0, sizeof(push), &push);
|
||||||
|
|
||||||
// Bind vertex buffer
|
// Draw fullscreen triangle — no vertex buffer needed
|
||||||
VkDeviceSize offset = 0;
|
vkCmdDraw(cmd, 3, 1, 0, 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) {
|
void Skybox::update(float deltaTime) {
|
||||||
|
|
@ -179,90 +154,6 @@ void Skybox::setTimeOfDay(float time) {
|
||||||
timeOfDay = 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<float> vertices;
|
|
||||||
std::vector<uint32_t> 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<float>(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<float>(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<int>(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 {
|
glm::vec3 Skybox::getHorizonColor(float time) const {
|
||||||
// Time-based horizon colors
|
// Time-based horizon colors
|
||||||
// 0-6: Night (dark blue)
|
// 0-6: Night (dark blue)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue