ADD: cloud
This commit is contained in:
226
shaders/cloud_voxel_advect.comp
Normal file
226
shaders/cloud_voxel_advect.comp
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
208
shaders/clouds.frag
Normal file
208
shaders/clouds.frag
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <core/types.h>
|
||||
#include <core/descriptor/descriptors.h>
|
||||
@@ -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<DeviceManager> device;
|
||||
std::shared_ptr<ResourceManager> 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<VoxelVolumeSettings, MAX_VOXEL_VOLUMES> voxelVolumes{};
|
||||
|
||||
// Ray tracing manager (optional, nullptr if unsupported)
|
||||
RayTracingManager* ray = nullptr;
|
||||
|
||||
|
||||
@@ -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<CloudPass>())
|
||||
{
|
||||
hdrTarget = clouds->register_graph(_renderGraph.get(), hdrTarget, hGBufferPosition);
|
||||
}
|
||||
}
|
||||
|
||||
if (auto *particles = _renderPassManager->getPass<ParticlePass>())
|
||||
{
|
||||
particles->register_graph(_renderGraph.get(), hdrTarget, hDepth, hGBufferPosition);
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include "mesh_bvh.h"
|
||||
|
||||
@@ -1209,6 +1210,95 @@ namespace
|
||||
}
|
||||
ctx->reflectionMode = static_cast<uint32_t>(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<int>(vs.type);
|
||||
if (ImGui::Combo(("Type" + id).c_str(), &type, typeLabels, IM_ARRAYSIZE(typeLabels)))
|
||||
{
|
||||
type = std::clamp(type, 0, 2);
|
||||
vs.type = static_cast<VoxelVolumeType>(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<int>(vs.gridResolution);
|
||||
if (ImGui::SliderInt(("Grid Resolution" + id).c_str(), &gridRes, 16, 128))
|
||||
{
|
||||
vs.gridResolution = static_cast<uint32_t>(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<TonemapPass>() : nullptr)
|
||||
{
|
||||
|
||||
21
src/main.cpp
21
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});
|
||||
|
||||
657
src/render/passes/clouds.cpp
Normal file
657
src/render/passes/clouds.cpp
Normal file
@@ -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 <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#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<uint32_t>(x) * 73856093u);
|
||||
h ^= hash_u32(static_cast<uint32_t>(y) * 19349663u);
|
||||
h ^= hash_u32(static_cast<uint32_t>(z) * 83492791u);
|
||||
// 24-bit mantissa-ish to [0,1)
|
||||
return static_cast<float>(h & 0x00FFFFFFu) / static_cast<float>(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<int>(std::floor(x));
|
||||
const int yi0 = static_cast<int>(std::floor(y));
|
||||
const int zi0 = static_cast<int>(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<float>(xi0));
|
||||
const float ty = smoothstep01(y - static_cast<float>(yi0));
|
||||
const float tz = smoothstep01(z - static_cast<float>(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<VkBuffer, MAX_VOLUMES> voxForRender{};
|
||||
std::array<VkDeviceSize, MAX_VOLUMES> voxSize{};
|
||||
std::array<uint32_t, MAX_VOLUMES> gridRes{};
|
||||
std::array<VoxelVolumeSettings, MAX_VOLUMES> settings{};
|
||||
std::array<bool, MAX_VOLUMES> 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<int>(res), static_cast<int>(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<VkDeviceSize>(resolution) *
|
||||
static_cast<VkDeviceSize>(resolution) *
|
||||
static_cast<VkDeviceSize>(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<size_t>(sizeBytes),
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
|
||||
VMA_MEMORY_USAGE_CPU_ONLY);
|
||||
|
||||
auto *dst = static_cast<float *>(staging.info.pMappedData);
|
||||
if (dst && sizeBytes > 0)
|
||||
{
|
||||
if (settings.type != VoxelVolumeType::Clouds)
|
||||
{
|
||||
std::fill(dst, dst + static_cast<size_t>(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<float>(y) / static_cast<float>(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<float>(x) / static_cast<float>(resolution - 1)) : 0.0f;
|
||||
const float fz = (resolution > 1) ? (static_cast<float>(z) / static_cast<float>(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<size_t>(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<size_t>(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<GPUSceneData *>(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<size_t>(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<int>(grid_resolution),
|
||||
static_cast<int>(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);
|
||||
}
|
||||
72
src/render/passes/clouds.h
Normal file
72
src/render/passes/clouds.h
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/world.h"
|
||||
#include "render/renderpass.h"
|
||||
#include "render/graph/types.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
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<VolumeBuffers, MAX_VOLUMES> _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;
|
||||
};
|
||||
@@ -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>();
|
||||
cloudPass->init(context);
|
||||
addPass(std::move(cloudPass));
|
||||
|
||||
// GPU particle system (compute update + render)
|
||||
auto particlePass = std::make_unique<ParticlePass>();
|
||||
particlePass->init(context);
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user