diff --git a/shaders/cloud_voxel_advect.comp b/shaders/cloud_voxel_advect.comp new file mode 100644 index 0000000..b498266 --- /dev/null +++ b/shaders/cloud_voxel_advect.comp @@ -0,0 +1,226 @@ +#version 450 + +// Simple voxel advection + noise injection for volumetric clouds. +// This is not a full fluid sim, but provides "fluid-like" evolving density in a voxel grid. + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 8) in; + +layout(std430, set = 0, binding = 0) readonly buffer DensityIn +{ + float density[]; +} vox_in; + +layout(std430, set = 0, binding = 1) buffer DensityOut +{ + float density[]; +} vox_out; + +layout(push_constant) uniform Push +{ + vec4 wind_dt; // xyz windVelocityLocal (units/sec), w dt_sec + vec4 volume_size_time; // xyz volume size (units), w time_sec + vec4 sim_params; // x dissipation (1/sec), y noiseStrength, z noiseScale, w noiseSpeed + vec4 emitter_params; // xyz emitterUVW, w emitterRadius + ivec4 misc; // x gridResolution, y volumeType (0=cloud,1=smoke,2=flame) +} pc; + +uint hash_u32(uint x) +{ + x ^= x >> 16; + x *= 0x7feb352du; + x ^= x >> 15; + x *= 0x846ca68bu; + x ^= x >> 16; + return x; +} + +float hash3_to_unit_float(ivec3 p) +{ + uint h = 0u; + h ^= hash_u32(uint(p.x) * 73856093u); + h ^= hash_u32(uint(p.y) * 19349663u); + h ^= hash_u32(uint(p.z) * 83492791u); + return float(h & 0x00FFFFFFu) / float(0x01000000u); +} + +float smoothstep01(float x) +{ + x = clamp(x, 0.0, 1.0); + return x * x * (3.0 - 2.0 * x); +} + +float value_noise3(vec3 p) +{ + ivec3 i0 = ivec3(floor(p)); + ivec3 i1 = i0 + ivec3(1); + + vec3 f = fract(p); + f = vec3(smoothstep01(f.x), smoothstep01(f.y), smoothstep01(f.z)); + + float c000 = hash3_to_unit_float(ivec3(i0.x, i0.y, i0.z)); + float c100 = hash3_to_unit_float(ivec3(i1.x, i0.y, i0.z)); + float c010 = hash3_to_unit_float(ivec3(i0.x, i1.y, i0.z)); + float c110 = hash3_to_unit_float(ivec3(i1.x, i1.y, i0.z)); + float c001 = hash3_to_unit_float(ivec3(i0.x, i0.y, i1.z)); + float c101 = hash3_to_unit_float(ivec3(i1.x, i0.y, i1.z)); + float c011 = hash3_to_unit_float(ivec3(i0.x, i1.y, i1.z)); + float c111 = hash3_to_unit_float(ivec3(i1.x, i1.y, i1.z)); + + float x00 = mix(c000, c100, f.x); + float x10 = mix(c010, c110, f.x); + float x01 = mix(c001, c101, f.x); + float x11 = mix(c011, c111, f.x); + + float y0 = mix(x00, x10, f.y); + float y1 = mix(x01, x11, f.y); + + return mix(y0, y1, f.z); +} + +float fbm3(vec3 p) +{ + float sum = 0.0; + float amp = 0.55; + float freq = 1.0; + for (int i = 0; i < 4; ++i) + { + sum += amp * value_noise3(p * freq); + freq *= 2.02; + amp *= 0.5; + } + return clamp(sum, 0.0, 1.0); +} + +int idx3(ivec3 c, int res) +{ + return c.x + c.y * res + c.z * res * res; +} + +float sample_density_trilinear(vec3 uvw, int res) +{ + uvw = clamp(uvw, vec3(0.0), vec3(1.0)); + + float fres = float(res); + vec3 g = uvw * (fres - 1.0); + + ivec3 base = ivec3(floor(g)); + base = clamp(base, ivec3(0), ivec3(res - 1)); + vec3 f = fract(g); + + ivec3 b1 = min(base + ivec3(1), ivec3(res - 1)); + + float d000 = vox_in.density[idx3(ivec3(base.x, base.y, base.z), res)]; + float d100 = vox_in.density[idx3(ivec3(b1.x, base.y, base.z), res)]; + float d010 = vox_in.density[idx3(ivec3(base.x, b1.y, base.z), res)]; + float d110 = vox_in.density[idx3(ivec3(b1.x, b1.y, base.z), res)]; + + float d001 = vox_in.density[idx3(ivec3(base.x, base.y, b1.z), res)]; + float d101 = vox_in.density[idx3(ivec3(b1.x, base.y, b1.z), res)]; + float d011 = vox_in.density[idx3(ivec3(base.x, b1.y, b1.z), res)]; + float d111 = vox_in.density[idx3(ivec3(b1.x, b1.y, b1.z), res)]; + + float x00 = mix(d000, d100, f.x); + float x10 = mix(d010, d110, f.x); + float x01 = mix(d001, d101, f.x); + float x11 = mix(d011, d111, f.x); + + float y0 = mix(x00, x10, f.y); + float y1 = mix(x01, x11, f.y); + + return mix(y0, y1, f.z); +} + +void main() +{ + int res = max(pc.misc.x, 1); + int vol_type = pc.misc.y; + + ivec3 c = ivec3(gl_GlobalInvocationID.xyz); + if (c.x >= res || c.y >= res || c.z >= res) + { + return; + } + + // Voxel center in [0,1]. + vec3 uvw = (vec3(c) + vec3(0.5)) / float(res); + + float dt = max(pc.wind_dt.w, 0.0); + + vec3 volSize = max(pc.volume_size_time.xyz, vec3(0.001)); + vec3 wind_uv = pc.wind_dt.xyz / volSize; // normalized per-second + + // Semi-Lagrangian advection: backtrace. + vec3 back = uvw - wind_uv * dt; + if (vol_type == 0) + { + back.xz = fract(back.xz); // wrap XZ for continuous motion (clouds) + back.y = clamp(back.y, 0.0, 1.0); // clamp Y + } + else + { + back = clamp(back, vec3(0.0), vec3(1.0)); // clamp for localized effects (smoke/flame) + } + + float advected = sample_density_trilinear(back, res); + + // Dissipation. + float dissipation = max(pc.sim_params.x, 0.0); + advected *= exp(-dissipation * dt); + + // Inject new density from procedural noise to keep volume evolving. + float time = pc.volume_size_time.w; + float noise_scale = max(pc.sim_params.z, 0.001); + float noise_speed = pc.sim_params.w; + float n = fbm3(uvw * noise_scale + vec3(0.0, time * noise_speed, 0.0)); + float injected = 0.0; + + if (vol_type == 0) + { + // Clouds: broad slab with continuous XZ wrapping. + injected = smoothstep(0.55, 0.80, n); + + // Height shaping (keep density within a slab). + float low = smoothstep(0.00, 0.18, uvw.y); + float high = 1.0 - smoothstep(0.78, 1.00, uvw.y); + injected *= clamp(low * high, 0.0, 1.0); + } + else + { + // Smoke/flame: inject near an emitter in UVW space. + vec3 e = clamp(pc.emitter_params.xyz, vec3(0.0), vec3(1.0)); + float r = max(pc.emitter_params.w, 1e-4); + + vec3 dpos = uvw - e; + dpos.y *= 1.5; // squash vertically for a more column-like source + float dist = length(dpos); + float shape = 1.0 - smoothstep(r, r * 1.25, dist); + + if (vol_type == 1) + { + // Smoke: softer noise threshold. + injected = smoothstep(0.45, 0.75, n) * shape; + } + else + { + // Flame: spikier + stronger flicker. + float f = smoothstep(0.35, 0.90, n); + injected = (f * f) * shape; + } + } + + float rate = clamp(pc.sim_params.y * dt, 0.0, 1.0); + float out_d = advected; + if (vol_type == 2) + { + // Flames flicker: blend toward injected field (avoid accumulating a "fog"). + out_d = mix(advected, injected, rate); + } + else + { + out_d = mix(advected, max(advected, injected), rate); + } + out_d = clamp(out_d, 0.0, 1.0); + + vox_out.density[idx3(c, res)] = out_d; +} + diff --git a/shaders/clouds.frag b/shaders/clouds.frag new file mode 100644 index 0000000..39a501a --- /dev/null +++ b/shaders/clouds.frag @@ -0,0 +1,208 @@ +#version 460 +#extension GL_GOOGLE_include_directive : require + +#include "input_structures.glsl" + +layout(location = 0) in vec2 inUV; +layout(location = 0) out vec4 outColor; + +// Set 1: cloud inputs +layout(set = 1, binding = 0) uniform sampler2D hdrInput; +layout(set = 1, binding = 1) uniform sampler2D posTex; + +layout(set = 1, binding = 2, std430) readonly buffer VoxelDensity +{ + float density[]; +} voxel; + +layout(push_constant) uniform VolumePush +{ + vec4 volume_center_follow; // xyz: center_local (or offset), w: followCameraXZ (0/1) + vec4 volume_half_extents; // xyz: half extents (local) + vec4 density_params; // x: densityScale, y: coverage, z: extinction, w: time_sec + vec4 scatter_params; // rgb: albedo/tint, w: scatterStrength + vec4 emission_params; // rgb: emissionColor, w: emissionStrength + ivec4 misc; // x: stepCount, y: gridResolution, z: volumeType (0=cloud,1=smoke,2=flame) +} pc; + +vec3 getCameraWorldPosition() +{ + mat3 rotT = mat3(sceneData.view); // R^T + mat3 rot = transpose(rotT); // R + vec3 T = sceneData.view[3].xyz; + return -rot * T; +} + +float hash12(vec2 p) +{ + vec3 p3 = fract(vec3(p.xyx) * 0.1031); + p3 += dot(p3, p3.yzx + 33.33); + return fract((p3.x + p3.y) * p3.z); +} + +bool intersectAABB(vec3 ro, vec3 rd, vec3 bmin, vec3 bmax, out float tmin, out float tmax) +{ + vec3 invD = 1.0 / rd; + vec3 t0s = (bmin - ro) * invD; + vec3 t1s = (bmax - ro) * invD; + vec3 tsmaller = min(t0s, t1s); + vec3 tbigger = max(t0s, t1s); + tmin = max(max(tsmaller.x, tsmaller.y), tsmaller.z); + tmax = min(min(tbigger.x, tbigger.y), tbigger.z); + return tmax >= max(tmin, 0.0); +} + +int idx3(ivec3 c, int res) +{ + return c.x + c.y * res + c.z * res * res; +} + +float sample_voxel_density(vec3 p, vec3 bmin, vec3 bmax) +{ + vec3 uvw = (p - bmin) / (bmax - bmin); + if (any(lessThan(uvw, vec3(0.0))) || any(greaterThan(uvw, vec3(1.0)))) + { + return 0.0; + } + + int res = max(pc.misc.y, 1); + float fres = float(res); + vec3 g = uvw * (fres - 1.0); + + ivec3 base = ivec3(floor(g)); + base = clamp(base, ivec3(0), ivec3(res - 1)); + vec3 f = fract(g); + + ivec3 b1 = min(base + ivec3(1), ivec3(res - 1)); + + float d000 = voxel.density[idx3(ivec3(base.x, base.y, base.z), res)]; + float d100 = voxel.density[idx3(ivec3(b1.x, base.y, base.z), res)]; + float d010 = voxel.density[idx3(ivec3(base.x, b1.y, base.z), res)]; + float d110 = voxel.density[idx3(ivec3(b1.x, b1.y, base.z), res)]; + + float d001 = voxel.density[idx3(ivec3(base.x, base.y, b1.z), res)]; + float d101 = voxel.density[idx3(ivec3(b1.x, base.y, b1.z), res)]; + float d011 = voxel.density[idx3(ivec3(base.x, b1.y, b1.z), res)]; + float d111 = voxel.density[idx3(ivec3(b1.x, b1.y, b1.z), res)]; + + float x00 = mix(d000, d100, f.x); + float x10 = mix(d010, d110, f.x); + float x01 = mix(d001, d101, f.x); + float x11 = mix(d011, d111, f.x); + + float y0 = mix(x00, x10, f.y); + float y1 = mix(x01, x11, f.y); + + return mix(y0, y1, f.z); +} + +void main() +{ + vec3 baseColor = texture(hdrInput, inUV).rgb; + + vec3 camPos = getCameraWorldPosition(); + + // Reconstruct a world-space ray for this pixel (Vulkan depth range 0..1). + mat4 invViewProj = inverse(sceneData.viewproj); + vec2 ndc = inUV * 2.0 - 1.0; + vec4 farH = invViewProj * vec4(ndc, 1.0, 1.0); + vec3 farP = farH.xyz / max(farH.w, 1e-6); + vec3 rd = normalize(farP - camPos); + + // Define a local-space cloud volume (optionally anchored to camera XZ). + vec3 center = pc.volume_center_follow.xyz; + if (pc.volume_center_follow.w > 0.5) + { + center.xz += camPos.xz; + } + vec3 halfExt = max(pc.volume_half_extents.xyz, vec3(0.01)); + vec3 bmin = center - halfExt; + vec3 bmax = center + halfExt; + + float t0, t1; + if (!intersectAABB(camPos, rd, bmin, bmax, t0, t1)) + { + outColor = vec4(baseColor, 1.0); + return; + } + + // Clamp march to geometry distance (gbufferPosition.w == 1 for valid surfaces). + vec4 posSample = texture(posTex, inUV); + if (posSample.w > 0.0) + { + float surfT = dot(posSample.xyz - camPos, rd); + if (surfT > 0.0) + { + t1 = min(t1, surfT); + } + } + + int steps = clamp(pc.misc.x, 8, 256); + if (t1 <= t0 || steps <= 0) + { + outColor = vec4(baseColor, 1.0); + return; + } + + float dt = (t1 - t0) / float(steps); + float jitter = hash12(inUV * 1024.0); + float t = max(t0, 0.0) + (jitter - 0.5) * dt; + + vec3 Lsun = normalize(-sceneData.sunlightDirection.xyz); + vec3 sunCol = sceneData.sunlightColor.rgb * sceneData.sunlightColor.a; + vec3 ambCol = sceneData.ambientColor.rgb; + + float trans = 1.0; + vec3 scatter = vec3(0.0); + int volType = pc.misc.z; + + for (int i = 0; i < steps; ++i) + { + vec3 p = camPos + rd * (t + 0.5 * dt); + + float d = sample_voxel_density(p, bmin, bmax); + d = max(0.0, d - pc.density_params.y) / max(1.0 - pc.density_params.y, 1e-3); + d *= max(pc.density_params.x, 0.0); + d *= max(pc.density_params.z, 0.0); + + if (d > 1e-4) + { + // Exponential absorption / single-scattering approximation. + float alpha = 1.0 - exp(-d * dt); + + if (volType == 2) + { + // Flames: emissive contribution. + float flicker = mix(0.65, 1.0, hash12(p.xz * 0.35 + pc.density_params.w * 0.25)); + vec3 emit = pc.emission_params.rgb * (pc.emission_params.w * flicker); + scatter += trans * alpha * emit; + } + else + { + float cosTheta = clamp(dot(rd, Lsun), 0.0, 1.0); + float phase = 0.30 + 0.70 * pow(cosTheta, 4.0); // cheap forward-scatter bias + vec3 light = ambCol * 0.25 + sunCol * phase; + + vec3 albedo = clamp(pc.scatter_params.rgb, vec3(0.0), vec3(1.0)); + float s = max(pc.scatter_params.w, 0.0); + scatter += trans * alpha * light * albedo * s; + } + trans *= (1.0 - alpha); + + if (trans < 0.01) + { + break; + } + } + + t += dt; + if (t > t1) + { + break; + } + } + + vec3 outRgb = scatter + trans * baseColor; + outColor = vec4(outRgb, 1.0); +} + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f02916e..85323f4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -83,6 +83,8 @@ add_executable (vulkan_engine render/passes/fxaa.cpp render/passes/ssr.h render/passes/ssr.cpp + render/passes/clouds.h + render/passes/clouds.cpp render/passes/particles.h render/passes/particles.cpp render/passes/transparent.h diff --git a/src/core/context.h b/src/core/context.h index 71e1287..fc50684 100644 --- a/src/core/context.h +++ b/src/core/context.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -45,9 +46,82 @@ struct ShadowSettings float hybridRayNoLThreshold = 0.25f; // trigger when N·L below this (mode==1) }; +struct CloudSettings +{ + // DEPRECATED: use EngineContext::voxelVolumes[0] instead. + // Kept temporarily to avoid breaking downstream code during refactors. + bool followCameraXZ = true; + bool animateVoxels = false; + glm::vec3 volumeCenterLocal{0.0f, 80.0f, 0.0f}; + glm::vec3 volumeHalfExtents{128.0f, 20.0f, 128.0f}; + glm::vec3 volumeVelocityLocal{0.0f, 0.0f, 0.0f}; + float densityScale = 1.25f; + float coverage = 0.45f; + int stepCount = 64; + uint32_t gridResolution = 64; + glm::vec3 windVelocityLocal{6.0f, 0.0f, 2.0f}; + float dissipation = 0.35f; + float noiseStrength = 1.0f; + float noiseScale = 6.0f; + float noiseSpeed = 0.15f; +}; + +enum class VoxelVolumeType : uint32_t +{ + Clouds = 0, + Smoke = 1, + Flame = 2, +}; + +struct VoxelVolumeSettings +{ + bool enabled = false; + VoxelVolumeType type = VoxelVolumeType::Clouds; + + // If true, the volume is anchored to camera XZ and volumeCenterLocal is treated as an offset. + // If false, volumeCenterLocal is absolute render-local space and will be compensated for floating-origin shifts. + bool followCameraXZ = false; + // If true, run a lightweight voxel advection/update compute pass every frame (procedural motion). + bool animateVoxels = true; + + // Volume AABB in render-local space. + glm::vec3 volumeCenterLocal{0.0f, 2.0f, 0.0f}; + glm::vec3 volumeHalfExtents{8.0f, 8.0f, 8.0f}; + // Optional volume drift (applied only when followCameraXZ == false). + glm::vec3 volumeVelocityLocal{0.0f, 0.0f, 0.0f}; + + // Raymarch/composite controls. + float densityScale = 1.0f; + float coverage = 0.0f; // 0..1 threshold (higher = emptier) + float extinction = 1.0f; // absorption/extinction scale + int stepCount = 48; // raymarch steps + + // Voxel grid resolution (cubic). + uint32_t gridResolution = 48; + + // Voxel animation (advection + injection) parameters. + glm::vec3 windVelocityLocal{0.0f, 2.0f, 0.0f}; // local units/sec (add buoyancy here) + float dissipation = 1.25f; // density decay rate (1/sec) + float noiseStrength = 1.0f; // injection rate + float noiseScale = 8.0f; // noise frequency in UVW space + float noiseSpeed = 1.0f; // time scale for injection noise + + // Smoke/flame source in normalized volume UVW space. + glm::vec3 emitterUVW{0.5f, 0.05f, 0.5f}; + float emitterRadius = 0.18f; // normalized (0..1-ish) + + // Shading. + glm::vec3 albedo{1.0f, 1.0f, 1.0f}; // scattering tint (cloud/smoke) + float scatterStrength = 1.0f; + glm::vec3 emissionColor{1.0f, 0.6f, 0.25f}; // flame emissive tint + float emissionStrength = 0.0f; +}; + class EngineContext { public: + static constexpr uint32_t MAX_VOXEL_VOLUMES = 4; + // Owned shared resources std::shared_ptr device; std::shared_ptr resources; @@ -85,6 +159,12 @@ public: // 0 = SSR only, 1 = SSR + RT fallback, 2 = RT only uint32_t reflectionMode = 0; + bool enableClouds = false; // DEPRECATED: use enableVolumetrics + voxelVolumes instead. + CloudSettings cloudSettings{}; // DEPRECATED: kept for compatibility during refactors. + + bool enableVolumetrics = false; // optional voxel volumetrics toggle (cloud/smoke/flame) + std::array voxelVolumes{}; + // Ray tracing manager (optional, nullptr if unsupported) RayTracingManager* ray = nullptr; diff --git a/src/core/engine.cpp b/src/core/engine.cpp index d587637..f6d9fac 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -45,6 +45,7 @@ #include "render/passes/geometry.h" #include "render/passes/imgui_pass.h" #include "render/passes/lighting.h" +#include "render/passes/clouds.h" #include "render/passes/particles.h" #include "render/passes/transparent.h" #include "render/passes/fxaa.h" @@ -254,6 +255,89 @@ void VulkanEngine::init() } _context->logicalRenderExtent = _logicalRenderExtent; + // Default voxel volumetric presets (up to 4 volumes for performance). + // Slot 0 matches the old CloudSettings defaults. + if (_context->voxelVolumes.size() >= 1) + { + VoxelVolumeSettings &v = _context->voxelVolumes[0]; + v.enabled = false; + v.type = VoxelVolumeType::Clouds; + v.followCameraXZ = true; + v.animateVoxels = false; + v.volumeCenterLocal = glm::vec3(0.0f, 80.0f, 0.0f); + v.volumeHalfExtents = glm::vec3(128.0f, 20.0f, 128.0f); + v.volumeVelocityLocal = glm::vec3(0.0f); + v.densityScale = 1.25f; + v.coverage = 0.45f; + v.extinction = 1.0f; + v.stepCount = 64; + v.gridResolution = 64; + v.windVelocityLocal = glm::vec3(6.0f, 0.0f, 2.0f); + v.dissipation = 0.35f; + v.noiseStrength = 1.0f; + v.noiseScale = 6.0f; + v.noiseSpeed = 0.15f; + v.emitterUVW = glm::vec3(0.5f, 0.05f, 0.5f); + v.emitterRadius = 0.18f; + v.albedo = glm::vec3(1.0f); + v.scatterStrength = 1.0f; + v.emissionColor = glm::vec3(1.0f, 0.6f, 0.25f); + v.emissionStrength = 0.0f; + } + if (_context->voxelVolumes.size() >= 2) + { + VoxelVolumeSettings &v = _context->voxelVolumes[1]; + v.enabled = false; + v.type = VoxelVolumeType::Smoke; + v.followCameraXZ = false; + v.animateVoxels = true; + v.volumeCenterLocal = glm::vec3(0.0f, 2.0f, 0.0f); + v.volumeHalfExtents = glm::vec3(6.0f, 6.0f, 6.0f); + v.volumeVelocityLocal = glm::vec3(0.0f); + v.densityScale = 1.0f; + v.coverage = 0.0f; + v.extinction = 2.5f; + v.stepCount = 48; + v.gridResolution = 48; + v.windVelocityLocal = glm::vec3(0.5f, 3.0f, 0.0f); + v.dissipation = 1.5f; + v.noiseStrength = 2.0f; + v.noiseScale = 10.0f; + v.noiseSpeed = 1.0f; + v.emitterUVW = glm::vec3(0.5f, 0.08f, 0.5f); + v.emitterRadius = 0.22f; + v.albedo = glm::vec3(0.25f); + v.scatterStrength = 0.25f; + v.emissionStrength = 0.0f; + } + if (_context->voxelVolumes.size() >= 3) + { + VoxelVolumeSettings &v = _context->voxelVolumes[2]; + v.enabled = false; + v.type = VoxelVolumeType::Flame; + v.followCameraXZ = false; + v.animateVoxels = true; + v.volumeCenterLocal = glm::vec3(0.0f, 1.0f, 0.0f); + v.volumeHalfExtents = glm::vec3(3.0f, 5.0f, 3.0f); + v.volumeVelocityLocal = glm::vec3(0.0f); + v.densityScale = 1.5f; + v.coverage = 0.0f; + v.extinction = 1.0f; + v.stepCount = 56; + v.gridResolution = 48; + v.windVelocityLocal = glm::vec3(0.0f, 4.5f, 0.0f); + v.dissipation = 3.0f; + v.noiseStrength = 3.0f; + v.noiseScale = 14.0f; + v.noiseSpeed = 3.0f; + v.emitterUVW = glm::vec3(0.5f, 0.10f, 0.5f); + v.emitterRadius = 0.18f; + v.albedo = glm::vec3(1.0f); + v.scatterStrength = 0.0f; + v.emissionColor = glm::vec3(1.0f, 0.5f, 0.1f); + v.emissionStrength = 12.0f; + } + _swapchainManager->init(_deviceManager.get(), _resourceManager.get()); _swapchainManager->set_render_extent(_drawExtent); _swapchainManager->init_swapchain(); @@ -1190,6 +1274,15 @@ void VulkanEngine::draw() // Downstream passes draw on top of either the SSR output or the raw HDR draw. RGImageHandle hdrTarget = (ssrEnabled && hSSR.valid()) ? hSSR : hDraw; + // Optional voxel volumetrics pass: reads hdrTarget + gbufferPosition and outputs a new HDR target. + if (_context && _context->enableVolumetrics) + { + if (auto *clouds = _renderPassManager->getPass()) + { + hdrTarget = clouds->register_graph(_renderGraph.get(), hdrTarget, hGBufferPosition); + } + } + if (auto *particles = _renderPassManager->getPass()) { particles->register_graph(_renderGraph.get(), hdrTarget, hDepth, hGBufferPosition); diff --git a/src/core/engine_ui.cpp b/src/core/engine_ui.cpp index 100b1a9..bb39a1a 100644 --- a/src/core/engine_ui.cpp +++ b/src/core/engine_ui.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include "mesh_bvh.h" @@ -1209,6 +1210,95 @@ namespace } ctx->reflectionMode = static_cast(reflMode); + ImGui::Separator(); + ImGui::TextUnformatted("Volumetrics"); + bool voxEnabled = ctx->enableVolumetrics; + if (ImGui::Checkbox("Enable Voxel Volumetrics (Cloud/Smoke/Flame)", &voxEnabled)) + { + ctx->enableVolumetrics = voxEnabled; + } + + const char *typeLabels[] = {"Clouds", "Smoke", "Flame"}; + + for (uint32_t i = 0; i < EngineContext::MAX_VOXEL_VOLUMES; ++i) + { + VoxelVolumeSettings &vs = ctx->voxelVolumes[i]; + + std::string header = "Voxel Volume " + std::to_string(i); + if (!ImGui::TreeNode(header.c_str())) + { + continue; + } + + std::string id = "##vox" + std::to_string(i); + ImGui::Checkbox(("Enabled" + id).c_str(), &vs.enabled); + + int type = static_cast(vs.type); + if (ImGui::Combo(("Type" + id).c_str(), &type, typeLabels, IM_ARRAYSIZE(typeLabels))) + { + type = std::clamp(type, 0, 2); + vs.type = static_cast(type); + } + + ImGui::Checkbox(("Follow Camera XZ" + id).c_str(), &vs.followCameraXZ); + ImGui::Checkbox(("Animate Voxels" + id).c_str(), &vs.animateVoxels); + + if (vs.followCameraXZ) + { + ImGui::InputFloat3(("Volume Offset (local)" + id).c_str(), &vs.volumeCenterLocal.x); + } + else + { + ImGui::InputFloat3(("Volume Center (local)" + id).c_str(), &vs.volumeCenterLocal.x); + } + ImGui::InputFloat3(("Volume Velocity (local)" + id).c_str(), &vs.volumeVelocityLocal.x); + ImGui::InputFloat3(("Volume Half Extents" + id).c_str(), &vs.volumeHalfExtents.x); + vs.volumeHalfExtents.x = std::max(vs.volumeHalfExtents.x, 0.01f); + vs.volumeHalfExtents.y = std::max(vs.volumeHalfExtents.y, 0.01f); + vs.volumeHalfExtents.z = std::max(vs.volumeHalfExtents.z, 0.01f); + + ImGui::SliderFloat(("Density Scale" + id).c_str(), &vs.densityScale, 0.0f, 6.0f); + ImGui::SliderFloat(("Coverage" + id).c_str(), &vs.coverage, 0.0f, 0.95f); + ImGui::SliderFloat(("Extinction" + id).c_str(), &vs.extinction, 0.0f, 8.0f); + ImGui::SliderInt(("Steps" + id).c_str(), &vs.stepCount, 8, 256); + + int gridRes = static_cast(vs.gridResolution); + if (ImGui::SliderInt(("Grid Resolution" + id).c_str(), &gridRes, 16, 128)) + { + vs.gridResolution = static_cast(std::max(4, gridRes)); + } + + if (vs.animateVoxels) + { + ImGui::InputFloat3(("Wind Velocity (local)" + id).c_str(), &vs.windVelocityLocal.x); + ImGui::SliderFloat(("Dissipation" + id).c_str(), &vs.dissipation, 0.0f, 6.0f); + ImGui::SliderFloat(("Noise Strength" + id).c_str(), &vs.noiseStrength, 0.0f, 6.0f); + ImGui::SliderFloat(("Noise Scale" + id).c_str(), &vs.noiseScale, 0.25f, 32.0f); + ImGui::SliderFloat(("Noise Speed" + id).c_str(), &vs.noiseSpeed, 0.0f, 8.0f); + + if (vs.type != VoxelVolumeType::Clouds) + { + ImGui::InputFloat3(("Emitter UVW" + id).c_str(), &vs.emitterUVW.x); + ImGui::SliderFloat(("Emitter Radius" + id).c_str(), &vs.emitterRadius, 0.01f, 0.5f); + } + } + + ImGui::ColorEdit3(("Albedo/Tint" + id).c_str(), &vs.albedo.x); + ImGui::SliderFloat(("Scatter Strength" + id).c_str(), &vs.scatterStrength, 0.0f, 2.0f); + + if (vs.type == VoxelVolumeType::Flame) + { + ImGui::ColorEdit3(("Emission Color" + id).c_str(), &vs.emissionColor.x); + ImGui::SliderFloat(("Emission Strength" + id).c_str(), &vs.emissionStrength, 0.0f, 25.0f); + } + else + { + vs.emissionStrength = 0.0f; + } + + ImGui::TreePop(); + } + ImGui::Separator(); if (auto *tm = eng->_renderPassManager ? eng->_renderPassManager->getPass() : nullptr) { diff --git a/src/main.cpp b/src/main.cpp index 9cb6405..74b0484 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,16 +20,27 @@ class ExampleGame : public GameRuntime::IGameCallbacks public: void on_init(GameRuntime::Runtime& runtime) override { - // Example: Set up initial scene auto& api = runtime.api(); + VulkanEngine* renderer = runtime.renderer(); + if (renderer && renderer->_assetManager) + { + GameAPI::IBLPaths ibl{}; + ibl.specularCube = renderer->_assetManager->assetPath("ibl/docklands.ktx2"); + ibl.diffuseCube = renderer->_assetManager->assetPath("ibl/docklands.ktx2"); // fallback: reuse specular for diffuse + ibl.brdfLut = renderer->_assetManager->assetPath("ibl/brdf_lut.ktx2"); + // Optional dedicated background texture (2D equirect); if omitted, background falls back to specularCube. + ibl.background = renderer->_assetManager->assetPath("ibl/sky.KTX2"); + api.load_global_ibl(ibl); + } + // Load a glTF model asynchronously - // api.load_gltf_async("example_model", "models/example.gltf", - // GameAPI::Transform{}.with_position({0, 0, 0})); + // api.add_gltf_instance_async("example_model", "models/example.gltf", + // GameAPI::Transform{.position = {0, 0, 0}}); // Spawn a primitive - // api.spawn_mesh_instance("test_cube", api.primitive_cube(), - // GameAPI::Transform{}.with_position({2, 0, 0})); + // api.add_primitive_instance("test_cube", GameAPI::PrimitiveType::Cube, + // GameAPI::Transform{.position = {2, 0, 0}}); // Set up camera // api.set_camera_position({0, 5, -10}); @@ -83,4 +94,4 @@ int main(int argc, char* argv[]) engine.cleanup(); return 0; -} \ No newline at end of file +} diff --git a/src/render/passes/clouds.cpp b/src/render/passes/clouds.cpp new file mode 100644 index 0000000..26fd815 --- /dev/null +++ b/src/render/passes/clouds.cpp @@ -0,0 +1,657 @@ +#include "clouds.h" + +#include "core/frame/resources.h" +#include "core/descriptor/descriptors.h" +#include "core/descriptor/manager.h" +#include "core/device/device.h" +#include "core/device/resource.h" +#include "core/device/swapchain.h" +#include "core/context.h" +#include "core/pipeline/manager.h" +#include "core/assets/manager.h" +#include "core/pipeline/sampler.h" + +#include "render/graph/graph.h" +#include "render/graph/resources.h" +#include "render/pipelines.h" + +#include +#include +#include +#include + +#include "vk_scene.h" + +namespace +{ + struct VolumePush + { + glm::vec4 volume_center_follow; // xyz: center_local (or offset), w: followCameraXZ (0/1) + glm::vec4 volume_half_extents; // xyz: half extents (local) + glm::vec4 density_params; // x: densityScale, y: coverage, z: extinction, w: time_sec + glm::vec4 scatter_params; // rgb: albedo/tint, w: scatterStrength + glm::vec4 emission_params; // rgb: emissionColor, w: emissionStrength + glm::ivec4 misc; // x: stepCount, y: gridResolution, z: volumeType + }; + + struct VolumeVoxelPush + { + glm::vec4 wind_dt; // xyz: windVelocityLocal, w: dt_sec + glm::vec4 volume_size_time; // xyz: volume size, w: time_sec + glm::vec4 sim_params; // x: dissipation, y: noiseStrength, z: noiseScale, w: noiseSpeed + glm::vec4 emitter_params; // xyz: emitterUVW, w: emitterRadius + glm::ivec4 misc; // x: gridResolution, y: volumeType + }; + + static uint32_t div_round_up(uint32_t x, uint32_t d) + { + return (x + d - 1u) / d; + } + + static uint32_t hash_u32(uint32_t x) + { + x ^= x >> 16; + x *= 0x7feb352du; + x ^= x >> 15; + x *= 0x846ca68bu; + x ^= x >> 16; + return x; + } + + static float hash3_to_unit_float(int x, int y, int z) + { + uint32_t h = 0u; + h ^= hash_u32(static_cast(x) * 73856093u); + h ^= hash_u32(static_cast(y) * 19349663u); + h ^= hash_u32(static_cast(z) * 83492791u); + // 24-bit mantissa-ish to [0,1) + return static_cast(h & 0x00FFFFFFu) / static_cast(0x01000000u); + } + + static float smoothstep01(float x) + { + x = std::clamp(x, 0.0f, 1.0f); + return x * x * (3.0f - 2.0f * x); + } + + static float lerp(float a, float b, float t) + { + return a + (b - a) * t; + } + + static float value_noise3(float x, float y, float z) + { + const int xi0 = static_cast(std::floor(x)); + const int yi0 = static_cast(std::floor(y)); + const int zi0 = static_cast(std::floor(z)); + const int xi1 = xi0 + 1; + const int yi1 = yi0 + 1; + const int zi1 = zi0 + 1; + + const float tx = smoothstep01(x - static_cast(xi0)); + const float ty = smoothstep01(y - static_cast(yi0)); + const float tz = smoothstep01(z - static_cast(zi0)); + + const float c000 = hash3_to_unit_float(xi0, yi0, zi0); + const float c100 = hash3_to_unit_float(xi1, yi0, zi0); + const float c010 = hash3_to_unit_float(xi0, yi1, zi0); + const float c110 = hash3_to_unit_float(xi1, yi1, zi0); + const float c001 = hash3_to_unit_float(xi0, yi0, zi1); + const float c101 = hash3_to_unit_float(xi1, yi0, zi1); + const float c011 = hash3_to_unit_float(xi0, yi1, zi1); + const float c111 = hash3_to_unit_float(xi1, yi1, zi1); + + const float x00 = lerp(c000, c100, tx); + const float x10 = lerp(c010, c110, tx); + const float x01 = lerp(c001, c101, tx); + const float x11 = lerp(c011, c111, tx); + + const float y0 = lerp(x00, x10, ty); + const float y1 = lerp(x01, x11, ty); + + return lerp(y0, y1, tz); + } + + static float fbm3(float x, float y, float z) + { + float sum = 0.0f; + float amp = 0.55f; + float freq = 1.0f; + for (int i = 0; i < 4; ++i) + { + sum += amp * value_noise3(x * freq, y * freq, z * freq); + freq *= 2.02f; + amp *= 0.5f; + } + return std::clamp(sum, 0.0f, 1.0f); + } +} + +void CloudPass::init(EngineContext *context) +{ + _context = context; + if (!_context || !_context->getDevice() || !_context->getDescriptorLayouts() || !_context->pipelines || + !_context->getResources() || !_context->getAssets()) + { + return; + } + + VkDevice device = _context->getDevice()->device(); + + // Set 1 layout: HDR input, gbuffer position, voxel density SSBO. + { + DescriptorLayoutBuilder builder; + builder.add_binding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); // hdrInput + builder.add_binding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); // posTex + builder.add_binding(2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER); // voxelDensity + _inputSetLayout = builder.build( + device, + VK_SHADER_STAGE_FRAGMENT_BIT, + nullptr, + VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); + } + + GraphicsPipelineCreateInfo info{}; + info.vertexShaderPath = _context->getAssets()->shaderPath("fullscreen.vert.spv"); + info.fragmentShaderPath = _context->getAssets()->shaderPath("clouds.frag.spv"); + info.setLayouts = { + _context->getDescriptorLayouts()->gpuSceneDataLayout(), // set = 0 (sceneData UBO + optional TLAS) + _inputSetLayout // set = 1 (inputs + voxel grid) + }; + + VkPushConstantRange pcr{}; + pcr.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + pcr.offset = 0; + pcr.size = sizeof(VolumePush); + info.pushConstants = {pcr}; + + info.configure = [this](PipelineBuilder &b) + { + b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); + b.set_polygon_mode(VK_POLYGON_MODE_FILL); + b.set_cull_mode(VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE); + b.set_multisampling_none(); + b.disable_depthtest(); + b.disable_blending(); + if (_context && _context->getSwapchain()) + { + b.set_color_attachment_format(_context->getSwapchain()->drawImage().imageFormat); + } + }; + + _context->pipelines->createGraphicsPipeline("clouds", info); + + // Optional voxel advection compute pipeline (used when VoxelVolumeSettings::animateVoxels is enabled). + { + ComputePipelineCreateInfo ci{}; + ci.shaderPath = _context->getAssets()->shaderPath("cloud_voxel_advect.comp.spv"); + ci.descriptorTypes = {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER}; + ci.pushConstantSize = sizeof(VolumeVoxelPush); + ci.pushConstantStages = VK_SHADER_STAGE_COMPUTE_BIT; + _context->pipelines->createComputePipeline("clouds.voxel_advect", ci); + _context->pipelines->createComputeInstance("clouds.voxel_advect", "clouds.voxel_advect"); + } + + // Voxel buffers are allocated lazily per-volume when enabled. +} + +void CloudPass::cleanup() +{ + if (_context && _context->getDevice() && _inputSetLayout) + { + vkDestroyDescriptorSetLayout(_context->getDevice()->device(), _inputSetLayout, nullptr); + _inputSetLayout = VK_NULL_HANDLE; + } + + if (_context && _context->getResources()) + { + ResourceManager *rm = _context->getResources(); + for (auto &vol : _volumes) + { + for (auto &buf : vol.voxelDensity) + { + if (buf.buffer != VK_NULL_HANDLE) + { + rm->destroy_buffer(buf); + } + buf = {}; + } + vol.voxelReadIndex = 0; + vol.voxelDensitySize = 0; + vol.gridResolution = 0; + } + } + + _deletionQueue.flush(); +} + +void CloudPass::execute(VkCommandBuffer) +{ + // Executed via render graph; nothing to do here. +} + +RGImageHandle CloudPass::register_graph(RenderGraph *graph, RGImageHandle hdrInput, RGImageHandle gbufPos) +{ + if (!graph || !hdrInput.valid() || !gbufPos.valid()) + { + return hdrInput; + } + if (!_context || !_context->enableVolumetrics) + { + return hdrInput; + } + + update_time_and_origin_delta(); + + const float origin_delta_len2 = glm::dot(_origin_delta_local, _origin_delta_local); + const bool origin_delta_valid = std::isfinite(origin_delta_len2) && origin_delta_len2 > 0.0f; + + std::array voxForRender{}; + std::array voxSize{}; + std::array gridRes{}; + std::array settings{}; + std::array active{}; + + for (uint32_t i = 0; i < MAX_VOLUMES; ++i) + { + VoxelVolumeSettings &vs = _context->voxelVolumes[i]; + if (!vs.enabled) + { + continue; + } + + // Keep the volume stable in world-space while render-local origin shifts. + if (!vs.followCameraXZ) + { + if (origin_delta_valid) + { + vs.volumeCenterLocal -= _origin_delta_local; + } + + const float vel_len2 = glm::dot(vs.volumeVelocityLocal, vs.volumeVelocityLocal); + const bool vel_valid = std::isfinite(vel_len2) && vel_len2 > 0.0f; + if (vel_valid && _dt_sec > 0.0f) + { + vs.volumeCenterLocal += vs.volumeVelocityLocal * _dt_sec; + } + } + + VolumeBuffers &bufs = _volumes[i]; + + const uint32_t wantRes = std::max(4u, vs.gridResolution); + if (wantRes != bufs.gridResolution || + bufs.voxelDensity[0].buffer == VK_NULL_HANDLE || + bufs.voxelDensity[1].buffer == VK_NULL_HANDLE) + { + rebuild_voxel_density(i, wantRes, vs); + } + + const VkDeviceSize size = bufs.voxelDensitySize; + const VkBuffer voxRead = bufs.voxelDensity[bufs.voxelReadIndex].buffer; + const VkBuffer voxWrite = bufs.voxelDensity[1u - bufs.voxelReadIndex].buffer; + + VkBuffer voxRender = voxRead; + + if (vs.animateVoxels && voxRead != VK_NULL_HANDLE && voxWrite != VK_NULL_HANDLE && size > 0 && bufs.gridResolution > 0) + { + const std::string passName = "Volumetrics.VoxelUpdate." + std::to_string(i); + + graph->add_pass( + passName.c_str(), + RGPassType::Compute, + [voxRead, voxWrite, size, i](RGPassBuilder &builder, EngineContext *) + { + const std::string inName = "volumetrics.voxel_density_in." + std::to_string(i); + const std::string outName = "volumetrics.voxel_density_out." + std::to_string(i); + builder.read_buffer(voxRead, RGBufferUsage::StorageRead, size, inName.c_str()); + builder.write_buffer(voxWrite, RGBufferUsage::StorageReadWrite, size, outName.c_str()); + }, + [this, i, voxRead, voxWrite, size, vs](VkCommandBuffer cmd, const RGPassResources &, EngineContext *ctx) + { + EngineContext *ctxLocal = ctx ? ctx : _context; + if (!ctxLocal || !ctxLocal->pipelines) return; + + VolumeBuffers &localBufs = _volumes[i]; + const uint32_t res = localBufs.gridResolution; + if (res == 0 || size == 0) return; + + // Bind the ping-pong buffers for this frame. + ctxLocal->pipelines->setComputeInstanceBuffer("clouds.voxel_advect", + 0, + voxRead, + size, + VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, + 0); + ctxLocal->pipelines->setComputeInstanceBuffer("clouds.voxel_advect", + 1, + voxWrite, + size, + VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, + 0); + + VolumeVoxelPush pc{}; + pc.wind_dt = glm::vec4(vs.windVelocityLocal, _dt_sec); + const glm::vec3 volSize = glm::max(vs.volumeHalfExtents * 2.0f, glm::vec3(0.001f)); + pc.volume_size_time = glm::vec4(volSize, _time_sec); + pc.sim_params = glm::vec4(std::max(0.0f, vs.dissipation), + std::max(0.0f, vs.noiseStrength), + std::max(0.001f, vs.noiseScale), + vs.noiseSpeed); + pc.emitter_params = glm::vec4(glm::clamp(vs.emitterUVW, glm::vec3(0.0f), glm::vec3(1.0f)), + std::max(0.0f, vs.emitterRadius)); + pc.misc = glm::ivec4(static_cast(res), static_cast(vs.type), 0, 0); + + // Match shader local_size_{x,y,z} = 8. + ComputeDispatchInfo di{}; + di.groupCountX = div_round_up(res, 8); + di.groupCountY = div_round_up(res, 8); + di.groupCountZ = div_round_up(res, 8); + di.pushConstants = &pc; + di.pushConstantSize = sizeof(pc); + + ctxLocal->pipelines->dispatchComputeInstance(cmd, "clouds.voxel_advect", di); + }); + + voxRender = voxWrite; + bufs.voxelReadIndex = 1u - bufs.voxelReadIndex; + } + + voxForRender[i] = voxRender; + voxSize[i] = size; + gridRes[i] = bufs.gridResolution; + settings[i] = vs; + active[i] = (voxRender != VK_NULL_HANDLE && size > 0 && bufs.gridResolution > 0); + } + + RGImageHandle current = hdrInput; + for (uint32_t i = 0; i < MAX_VOLUMES; ++i) + { + if (!active[i]) + { + continue; + } + + RGImageDesc desc{}; + desc.name = "hdr.volumetrics." + std::to_string(i); + desc.format = (_context && _context->getSwapchain()) ? _context->getSwapchain()->drawImage().imageFormat : VK_FORMAT_R16G16B16A16_SFLOAT; + desc.extent = _context->getDrawExtent(); + desc.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + RGImageHandle hdrOutput = graph->create_image(desc); + + const VkBuffer voxBuf = voxForRender[i]; + const VkDeviceSize size = voxSize[i]; + const uint32_t res = gridRes[i]; + const VoxelVolumeSettings vs = settings[i]; + const RGImageHandle hdrIn = current; + + const std::string passName = "Volumetrics." + std::to_string(i); + graph->add_pass( + passName.c_str(), + RGPassType::Graphics, + [hdrIn, gbufPos, hdrOutput, voxBuf, size, i](RGPassBuilder &builder, EngineContext *) + { + builder.read(hdrIn, RGImageUsage::SampledFragment); + builder.read(gbufPos, RGImageUsage::SampledFragment); + if (voxBuf != VK_NULL_HANDLE) + { + const std::string voxName = "volumetrics.voxel_density." + std::to_string(i); + builder.read_buffer(voxBuf, RGBufferUsage::StorageRead, size, voxName.c_str()); + } + builder.write_color(hdrOutput, false /*load*/); + }, + [this, hdrIn, gbufPos, voxBuf, size, res, vs](VkCommandBuffer cmd, const RGPassResources &resGraph, EngineContext *ctx) + { + draw_volume(cmd, ctx, resGraph, hdrIn, gbufPos, vs, res, voxBuf, size); + }); + + current = hdrOutput; + } + + return current; +} + +void CloudPass::update_time_and_origin_delta() +{ + _dt_sec = 0.0f; + _origin_delta_local = glm::vec3(0.0f); + + if (!_context || !_context->scene) + { + return; + } + + _dt_sec = _context->scene->getDeltaTime(); + if (!std::isfinite(_dt_sec)) _dt_sec = 0.0f; + _dt_sec = std::clamp(_dt_sec, 0.0f, 0.1f); + _time_sec += _dt_sec; + + const WorldVec3 origin_world = _context->scene->get_world_origin(); + if (_has_prev_origin) + { + const WorldVec3 delta_world = origin_world - _prev_origin_world; + _origin_delta_local = glm::vec3(delta_world); + } + _prev_origin_world = origin_world; + _has_prev_origin = true; +} + +void CloudPass::rebuild_voxel_density(uint32_t volume_index, uint32_t resolution, const VoxelVolumeSettings &settings) +{ + if (!_context || !_context->getResources() || !_context->getDevice()) + { + return; + } + if (volume_index >= MAX_VOLUMES) + { + return; + } + + resolution = std::max(4u, resolution); + + ResourceManager *resourceManager = _context->getResources(); + DeviceManager *deviceManager = _context->getDevice(); + VolumeBuffers &vol = _volumes[volume_index]; + + const VkDeviceSize voxelCount = static_cast(resolution) * + static_cast(resolution) * + static_cast(resolution); + const VkDeviceSize sizeBytes = voxelCount * sizeof(float); + + // Stage initial density data to a GPU-only SSBO (and duplicate into both ping-pong buffers). + AllocatedBuffer staging = resourceManager->create_buffer( + static_cast(sizeBytes), + VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + VMA_MEMORY_USAGE_CPU_ONLY); + + auto *dst = static_cast(staging.info.pMappedData); + if (dst && sizeBytes > 0) + { + if (settings.type != VoxelVolumeType::Clouds) + { + std::fill(dst, dst + static_cast(voxelCount), 0.0f); + } + else + { + for (uint32_t z = 0; z < resolution; ++z) + { + for (uint32_t y = 0; y < resolution; ++y) + { + const float fy = (resolution > 1) ? (static_cast(y) / static_cast(resolution - 1)) : 0.0f; + // Height falloff to keep density within a layer. + const float low = smoothstep01((fy - 0.00f) / 0.18f); + const float high = 1.0f - smoothstep01((fy - 0.78f) / 0.22f); + const float heightShape = std::clamp(low * high, 0.0f, 1.0f); + + for (uint32_t x = 0; x < resolution; ++x) + { + const float fx = (resolution > 1) ? (static_cast(x) / static_cast(resolution - 1)) : 0.0f; + const float fz = (resolution > 1) ? (static_cast(z) / static_cast(resolution - 1)) : 0.0f; + + // Low-frequency FBM noise in [0,1]. + float n = fbm3(fx * 6.0f, fy * 6.0f, fz * 6.0f); + + // Add a soft "blob" bias near center to avoid uniform fog. + const float cx = fx * 2.0f - 1.0f; + const float cy = fy * 2.0f - 1.0f; + const float cz = fz * 2.0f - 1.0f; + const float r2 = cx * cx + cy * cy + cz * cz; + const float blob = std::clamp(1.0f - r2 * 0.85f, 0.0f, 1.0f); + + float density = std::clamp(n * heightShape, 0.0f, 1.0f); + density = std::clamp(density + 0.35f * blob * heightShape, 0.0f, 1.0f); + + const uint32_t idx = x + y * resolution + z * resolution * resolution; + dst[idx] = density; + } + } + } + } + + vmaFlushAllocation(deviceManager->allocator(), staging.allocation, 0, sizeBytes); + } + + AllocatedBuffer newA = resourceManager->create_buffer( + static_cast(sizeBytes), + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VMA_MEMORY_USAGE_GPU_ONLY); + AllocatedBuffer newB = resourceManager->create_buffer( + static_cast(sizeBytes), + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VMA_MEMORY_USAGE_GPU_ONLY); + + resourceManager->immediate_submit([&](VkCommandBuffer cmd) { + VkBufferCopy region{}; + region.srcOffset = 0; + region.dstOffset = 0; + region.size = sizeBytes; + vkCmdCopyBuffer(cmd, staging.buffer, newA.buffer, 1, ®ion); + vkCmdCopyBuffer(cmd, staging.buffer, newB.buffer, 1, ®ion); + }); + + resourceManager->destroy_buffer(staging); + + if (vol.voxelDensity[0].buffer != VK_NULL_HANDLE || vol.voxelDensity[1].buffer != VK_NULL_HANDLE) + { + AllocatedBuffer old0 = vol.voxelDensity[0]; + AllocatedBuffer old1 = vol.voxelDensity[1]; + // Defer destruction if we are mid-frame; otherwise destroy immediately. + if (_context->currentFrame) + { + _context->currentFrame->_deletionQueue.push_function([resourceManager, old0, old1]() + { + if (old0.buffer != VK_NULL_HANDLE) resourceManager->destroy_buffer(old0); + if (old1.buffer != VK_NULL_HANDLE) resourceManager->destroy_buffer(old1); + }); + } + else + { + if (old0.buffer != VK_NULL_HANDLE) resourceManager->destroy_buffer(old0); + if (old1.buffer != VK_NULL_HANDLE) resourceManager->destroy_buffer(old1); + } + } + + vol.voxelDensity[0] = newA; + vol.voxelDensity[1] = newB; + vol.voxelReadIndex = 0; + vol.voxelDensitySize = sizeBytes; + vol.gridResolution = resolution; +} + +void CloudPass::draw_volume(VkCommandBuffer cmd, + EngineContext *context, + const RGPassResources &resources, + RGImageHandle hdrInput, + RGImageHandle gbufPos, + const VoxelVolumeSettings &settings, + uint32_t grid_resolution, + VkBuffer voxelBuffer, + VkDeviceSize voxelSize) +{ + EngineContext *ctxLocal = context ? context : _context; + if (!ctxLocal || !ctxLocal->currentFrame) return; + + ResourceManager *resourceManager = ctxLocal->getResources(); + DeviceManager *deviceManager = ctxLocal->getDevice(); + DescriptorManager *descriptorLayouts = ctxLocal->getDescriptorLayouts(); + PipelineManager *pipelineManager = ctxLocal->pipelines; + if (!resourceManager || !deviceManager || !descriptorLayouts || !pipelineManager) return; + + VkImageView hdrView = resources.image_view(hdrInput); + VkImageView posView = resources.image_view(gbufPos); + if (hdrView == VK_NULL_HANDLE || posView == VK_NULL_HANDLE) return; + + if (voxelBuffer == VK_NULL_HANDLE || voxelSize == 0 || grid_resolution == 0) + { + return; + } + + if (!pipelineManager->getGraphics("clouds", _pipeline, _pipelineLayout)) + { + return; + } + + // Scene UBO (set=0, binding=0) – mirror SSR/lighting behavior. + AllocatedBuffer sceneBuf = resourceManager->create_buffer( + sizeof(GPUSceneData), + VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, + VMA_MEMORY_USAGE_CPU_TO_GPU); + ctxLocal->currentFrame->_deletionQueue.push_function([resourceManager, sceneBuf]() + { + resourceManager->destroy_buffer(sceneBuf); + }); + + auto *sceneUniformData = static_cast(sceneBuf.info.pMappedData); + if (sceneUniformData) + { + *sceneUniformData = ctxLocal->getSceneData(); + vmaFlushAllocation(deviceManager->allocator(), sceneBuf.allocation, 0, sizeof(GPUSceneData)); + } + + VkDescriptorSet globalSet = ctxLocal->currentFrame->_frameDescriptors.allocate( + deviceManager->device(), descriptorLayouts->gpuSceneDataLayout()); + { + DescriptorWriter writer; + writer.write_buffer(0, sceneBuf.buffer, sizeof(GPUSceneData), 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER); + writer.update_set(deviceManager->device(), globalSet); + } + + VkDescriptorSet inputSet = ctxLocal->currentFrame->_frameDescriptors.allocate( + deviceManager->device(), _inputSetLayout); + { + DescriptorWriter writer; + writer.write_image(0, hdrView, ctxLocal->getSamplers()->defaultLinear(), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + writer.write_image(1, posView, ctxLocal->getSamplers()->defaultLinear(), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + writer.write_buffer(2, voxelBuffer, static_cast(voxelSize), 0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER); + writer.update_set(deviceManager->device(), inputSet); + } + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipelineLayout, 0, 1, &globalSet, 0, nullptr); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipelineLayout, 1, 1, &inputSet, 0, nullptr); + + VolumePush push{}; + push.volume_center_follow = glm::vec4(settings.volumeCenterLocal, settings.followCameraXZ ? 1.0f : 0.0f); + push.volume_half_extents = glm::vec4(glm::max(settings.volumeHalfExtents, glm::vec3(0.01f)), 0.0f); + push.density_params = glm::vec4(std::max(0.0f, settings.densityScale), + std::clamp(settings.coverage, 0.0f, 0.99f), + std::max(0.0f, settings.extinction), + _time_sec); + push.scatter_params = glm::vec4(glm::clamp(settings.albedo, glm::vec3(0.0f), glm::vec3(1.0f)), + std::max(0.0f, settings.scatterStrength)); + push.emission_params = glm::vec4(glm::max(settings.emissionColor, glm::vec3(0.0f)), + std::max(0.0f, settings.emissionStrength)); + push.misc = glm::ivec4(std::clamp(settings.stepCount, 8, 256), + static_cast(grid_resolution), + static_cast(settings.type), + 0); + vkCmdPushConstants(cmd, _pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(VolumePush), &push); + + VkExtent2D extent = ctxLocal->getDrawExtent(); + VkViewport vp{0.f, 0.f, (float)extent.width, (float)extent.height, 0.f, 1.f}; + VkRect2D sc{{0, 0}, extent}; + vkCmdSetViewport(cmd, 0, 1, &vp); + vkCmdSetScissor(cmd, 0, 1, &sc); + vkCmdDraw(cmd, 3, 1, 0, 0); +} diff --git a/src/render/passes/clouds.h b/src/render/passes/clouds.h new file mode 100644 index 0000000..968de25 --- /dev/null +++ b/src/render/passes/clouds.h @@ -0,0 +1,72 @@ +#pragma once + +#include "core/world.h" +#include "render/renderpass.h" +#include "render/graph/types.h" + +#include + +class RenderGraph; +class RGPassResources; +struct VoxelVolumeSettings; + +// Volumetric voxel clouds: raymarch a bounded volume and sample density from an SSBO voxel grid. +class CloudPass : public IRenderPass +{ +public: + void init(EngineContext *context) override; + void cleanup() override; + void execute(VkCommandBuffer cmd) override; + + const char *getName() const override { return "Volumetrics"; } + + // Register the cloud pass into the render graph. + // hdrInput: HDR color buffer to composite on top of. + // gbufPos : G-Buffer world/local position (w=1 for geometry, w=0 for sky). + // Returns a new HDR image handle with clouds composited. + RGImageHandle register_graph(RenderGraph *graph, RGImageHandle hdrInput, RGImageHandle gbufPos); + +private: + EngineContext *_context = nullptr; + VkDescriptorSetLayout _inputSetLayout = VK_NULL_HANDLE; // set=1: hdr input + gbuffer + voxel density buffer + + VkPipeline _pipeline = VK_NULL_HANDLE; + VkPipelineLayout _pipelineLayout = VK_NULL_HANDLE; + + static constexpr uint32_t MAX_VOLUMES = 4; + + struct VolumeBuffers + { + AllocatedBuffer voxelDensity[2]{}; + uint32_t voxelReadIndex = 0; + VkDeviceSize voxelDensitySize = 0; + uint32_t gridResolution = 0; + }; + + std::array _volumes{}; + + void rebuild_voxel_density(uint32_t volume_index, uint32_t resolution, const VoxelVolumeSettings &settings); + + void update_time_and_origin_delta(); + + void draw_volume(VkCommandBuffer cmd, + EngineContext *context, + const RGPassResources &resources, + RGImageHandle hdrInput, + RGImageHandle gbufPos, + const VoxelVolumeSettings &settings, + uint32_t grid_resolution, + VkBuffer voxelBuffer, + VkDeviceSize voxelSize); + + // Per-frame sim time (used when animateVoxels is enabled). + float _dt_sec = 0.0f; + float _time_sec = 0.0f; + + // Floating-origin tracking (used to keep the volume stable when not following camera). + bool _has_prev_origin = false; + WorldVec3 _prev_origin_world{0.0, 0.0, 0.0}; + glm::vec3 _origin_delta_local{0.0f, 0.0f, 0.0f}; + + DeletionQueue _deletionQueue; +}; diff --git a/src/render/renderpass.cpp b/src/render/renderpass.cpp index fbf1360..e7d81d0 100644 --- a/src/render/renderpass.cpp +++ b/src/render/renderpass.cpp @@ -5,6 +5,7 @@ #include "passes/imgui_pass.h" #include "passes/lighting.h" #include "passes/ssr.h" +#include "passes/clouds.h" #include "passes/particles.h" #include "passes/fxaa.h" #include "passes/transparent.h" @@ -37,6 +38,11 @@ void RenderPassManager::init(EngineContext *context) ssrPass->init(context); addPass(std::move(ssrPass)); + // Voxel volumetrics pass (cloud/smoke/flame via voxel density SSBO) + auto cloudPass = std::make_unique(); + cloudPass->init(context); + addPass(std::move(cloudPass)); + // GPU particle system (compute update + render) auto particlePass = std::make_unique(); particlePass->init(context); diff --git a/src/runtime/game_runtime.cpp b/src/runtime/game_runtime.cpp index 6ae4521..e594de6 100644 --- a/src/runtime/game_runtime.cpp +++ b/src/runtime/game_runtime.cpp @@ -186,6 +186,33 @@ namespace GameRuntime _renderer->_rayManager->pump_blas_builds(1); } + // Commit any completed async IBL load now that the GPU is idle. + if (_renderer->_iblManager && _renderer->_pendingIBLRequest.active) + { + IBLManager::AsyncResult iblRes = _renderer->_iblManager->pump_async(); + if (iblRes.completed) + { + if (iblRes.success) + { + if (_renderer->_pendingIBLRequest.targetVolume >= 0) + { + _renderer->_activeIBLVolume = _renderer->_pendingIBLRequest.targetVolume; + } + else + { + _renderer->_activeIBLVolume = -1; + _renderer->_hasGlobalIBL = true; + } + } + else + { + fmt::println("[Runtime] Warning: async IBL load failed (specular='{}')", + _renderer->_pendingIBLRequest.paths.specularCube); + } + _renderer->_pendingIBLRequest.active = false; + } + } + // --- Flush per-frame resources --- _renderer->get_current_frame()._deletionQueue.flush(); if (_renderer->_renderGraph)