ADD: cloud, particle API

This commit is contained in:
2025-12-23 23:30:24 +09:00
parent b9454e8f26
commit f02086bc32
6 changed files with 512 additions and 19 deletions

View File

@@ -401,6 +401,7 @@ IBLManager::AsyncResult IBLManager::pump_async()
PreparedIBLData data{}; PreparedIBLData data{};
bool success = false; bool success = false;
std::string error;
{ {
std::lock_guard<std::mutex> lock(state->mutex); std::lock_guard<std::mutex> lock(state->mutex);
if (!state->resultReady) if (!state->resultReady)
@@ -409,12 +410,17 @@ IBLManager::AsyncResult IBLManager::pump_async()
} }
data = std::move(state->readyData); data = std::move(state->readyData);
success = state->resultSuccess; success = state->resultSuccess;
error = std::move(state->lastError);
state->resultReady = false; state->resultReady = false;
} }
out.completed = true; out.completed = true;
if (!success) if (!success)
{ {
if (!error.empty())
{
fmt::println("[IBL] async load failed: {}", error);
}
out.success = false; out.success = false;
return out; return out;
} }

View File

@@ -1051,7 +1051,7 @@ void VulkanEngine::draw()
if (newVolume != _activeIBLVolume) if (newVolume != _activeIBLVolume)
{ {
const IBLPaths *paths = nullptr; IBLPaths *paths = nullptr;
if (newVolume >= 0) if (newVolume >= 0)
{ {
paths = &_iblVolumes[newVolume].paths; paths = &_iblVolumes[newVolume].paths;
@@ -1067,17 +1067,27 @@ void VulkanEngine::draw()
if (paths && !alreadyPendingForTarget) if (paths && !alreadyPendingForTarget)
{ {
if (_iblManager->load_async(*paths)) IBLPaths resolved = *paths;
if (_assetManager)
{
if (!resolved.specularCube.empty()) resolved.specularCube = _assetManager->assetPath(resolved.specularCube);
if (!resolved.diffuseCube.empty()) resolved.diffuseCube = _assetManager->assetPath(resolved.diffuseCube);
if (!resolved.brdfLut2D.empty()) resolved.brdfLut2D = _assetManager->assetPath(resolved.brdfLut2D);
if (!resolved.background2D.empty()) resolved.background2D = _assetManager->assetPath(resolved.background2D);
}
*paths = resolved;
if (_iblManager->load_async(resolved))
{ {
_pendingIBLRequest.active = true; _pendingIBLRequest.active = true;
_pendingIBLRequest.targetVolume = newVolume; _pendingIBLRequest.targetVolume = newVolume;
_pendingIBLRequest.paths = *paths; _pendingIBLRequest.paths = resolved;
} }
else else
{ {
fmt::println("[Engine] Warning: failed to enqueue IBL load for {} (specular='{}')", fmt::println("[Engine] Warning: failed to enqueue IBL load for {} (specular='{}')",
(newVolume >= 0) ? "volume" : "global environment", (newVolume >= 0) ? "volume" : "global environment",
paths->specularCube); resolved.specularCube);
} }
} }
} }

View File

@@ -38,6 +38,18 @@
namespace namespace
{ {
static IBLPaths resolve_ibl_paths(VulkanEngine *eng, const IBLPaths &paths)
{
IBLPaths out = paths;
if (!eng || !eng->_assetManager) return out;
if (!out.specularCube.empty()) out.specularCube = eng->_assetManager->assetPath(out.specularCube);
if (!out.diffuseCube.empty()) out.diffuseCube = eng->_assetManager->assetPath(out.diffuseCube);
if (!out.brdfLut2D.empty()) out.brdfLut2D = eng->_assetManager->assetPath(out.brdfLut2D);
if (!out.background2D.empty()) out.background2D = eng->_assetManager->assetPath(out.background2D);
return out;
}
static void ui_window(VulkanEngine *eng) static void ui_window(VulkanEngine *eng)
{ {
if (!eng || !eng->_window) return; if (!eng || !eng->_window) return;
@@ -639,6 +651,35 @@ namespace
ImGui::PushID(static_cast<int>(i)); ImGui::PushID(static_cast<int>(i));
ImGui::Separator(); ImGui::Separator();
ImGui::Text("Volume %zu", i); ImGui::Text("Volume %zu", i);
ImGui::SameLine();
if (ImGui::Button("Delete"))
{
const int idx = static_cast<int>(i);
if (eng->_activeIBLVolume == idx)
{
eng->_activeIBLVolume = -1;
}
else if (eng->_activeIBLVolume > idx)
{
eng->_activeIBLVolume -= 1;
}
if (eng->_pendingIBLRequest.active)
{
if (eng->_pendingIBLRequest.targetVolume == idx)
{
eng->_pendingIBLRequest.active = false;
}
else if (eng->_pendingIBLRequest.targetVolume > idx)
{
eng->_pendingIBLRequest.targetVolume -= 1;
}
}
eng->_iblVolumes.erase(eng->_iblVolumes.begin() + idx);
ImGui::PopID();
break;
}
ImGui::Checkbox("Enabled", &vol.enabled); ImGui::Checkbox("Enabled", &vol.enabled);
{ {
double c[3] = {vol.center_world.x, vol.center_world.y, vol.center_world.z}; double c[3] = {vol.center_world.x, vol.center_world.y, vol.center_world.z};
@@ -680,6 +721,7 @@ namespace
{ {
if (eng->_iblManager && vol.enabled) if (eng->_iblManager && vol.enabled)
{ {
vol.paths = resolve_ibl_paths(eng, vol.paths);
if (eng->_iblManager->load_async(vol.paths)) if (eng->_iblManager->load_async(vol.paths))
{ {
eng->_pendingIBLRequest.active = true; eng->_pendingIBLRequest.active = true;
@@ -691,6 +733,7 @@ namespace
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Set As Global IBL")) if (ImGui::Button("Set As Global IBL"))
{ {
vol.paths = resolve_ibl_paths(eng, vol.paths);
eng->_globalIBLPaths = vol.paths; eng->_globalIBLPaths = vol.paths;
if (eng->_iblManager) if (eng->_iblManager)
{ {

View File

@@ -6,6 +6,7 @@
#include "core/pipeline/manager.h" #include "core/pipeline/manager.h"
#include "render/passes/tonemap.h" #include "render/passes/tonemap.h"
#include "render/passes/fxaa.h" #include "render/passes/fxaa.h"
#include "render/passes/particles.h"
#include "render/renderpass.h" #include "render/renderpass.h"
#include "core/picking/picking_system.h" #include "core/picking/picking_system.h"
#include "scene/vk_scene.h" #include "scene/vk_scene.h"
@@ -1415,6 +1416,293 @@ Stats Engine::get_stats() const
return s; return s;
} }
// ----------------------------------------------------------------------------
// Volumetrics (Cloud/Smoke/Flame)
// ----------------------------------------------------------------------------
void Engine::set_volumetrics_enabled(bool enabled)
{
if (!_engine || !_engine->_context) return;
_engine->_context->enableVolumetrics = enabled;
}
bool Engine::get_volumetrics_enabled() const
{
if (!_engine || !_engine->_context) return false;
return _engine->_context->enableVolumetrics;
}
bool Engine::get_voxel_volume(size_t index, VoxelVolumeSettings& out) const
{
if (!_engine || !_engine->_context) return false;
if (index >= EngineContext::MAX_VOXEL_VOLUMES) return false;
const auto& src = _engine->_context->voxelVolumes[index];
out.enabled = src.enabled;
out.type = static_cast<VoxelVolumeType>(src.type);
out.followCameraXZ = src.followCameraXZ;
out.animateVoxels = src.animateVoxels;
out.volumeCenterLocal = src.volumeCenterLocal;
out.volumeHalfExtents = src.volumeHalfExtents;
out.volumeVelocityLocal = src.volumeVelocityLocal;
out.densityScale = src.densityScale;
out.coverage = src.coverage;
out.extinction = src.extinction;
out.stepCount = src.stepCount;
out.gridResolution = src.gridResolution;
out.windVelocityLocal = src.windVelocityLocal;
out.dissipation = src.dissipation;
out.noiseStrength = src.noiseStrength;
out.noiseScale = src.noiseScale;
out.noiseSpeed = src.noiseSpeed;
out.emitterUVW = src.emitterUVW;
out.emitterRadius = src.emitterRadius;
out.albedo = src.albedo;
out.scatterStrength = src.scatterStrength;
out.emissionColor = src.emissionColor;
out.emissionStrength = src.emissionStrength;
return true;
}
bool Engine::set_voxel_volume(size_t index, const VoxelVolumeSettings& settings)
{
if (!_engine || !_engine->_context) return false;
if (index >= EngineContext::MAX_VOXEL_VOLUMES) return false;
auto& dst = _engine->_context->voxelVolumes[index];
dst.enabled = settings.enabled;
dst.type = static_cast<::VoxelVolumeType>(settings.type);
dst.followCameraXZ = settings.followCameraXZ;
dst.animateVoxels = settings.animateVoxels;
dst.volumeCenterLocal = settings.volumeCenterLocal;
dst.volumeHalfExtents = settings.volumeHalfExtents;
dst.volumeVelocityLocal = settings.volumeVelocityLocal;
dst.densityScale = settings.densityScale;
dst.coverage = settings.coverage;
dst.extinction = settings.extinction;
dst.stepCount = settings.stepCount;
dst.gridResolution = settings.gridResolution;
dst.windVelocityLocal = settings.windVelocityLocal;
dst.dissipation = settings.dissipation;
dst.noiseStrength = settings.noiseStrength;
dst.noiseScale = settings.noiseScale;
dst.noiseSpeed = settings.noiseSpeed;
dst.emitterUVW = settings.emitterUVW;
dst.emitterRadius = settings.emitterRadius;
dst.albedo = settings.albedo;
dst.scatterStrength = settings.scatterStrength;
dst.emissionColor = settings.emissionColor;
dst.emissionStrength = settings.emissionStrength;
return true;
}
size_t Engine::get_max_voxel_volumes() const
{
return EngineContext::MAX_VOXEL_VOLUMES;
}
// ----------------------------------------------------------------------------
// Particle Systems
// ----------------------------------------------------------------------------
uint32_t Engine::create_particle_system(uint32_t particle_count)
{
if (!_engine || !_engine->_renderPassManager) return 0;
ParticlePass* particlePass = _engine->_renderPassManager->getPass<ParticlePass>();
if (!particlePass) return 0;
return particlePass->create_system(particle_count);
}
bool Engine::destroy_particle_system(uint32_t id)
{
if (!_engine || !_engine->_renderPassManager) return false;
ParticlePass* particlePass = _engine->_renderPassManager->getPass<ParticlePass>();
if (!particlePass) return false;
return particlePass->destroy_system(id);
}
bool Engine::resize_particle_system(uint32_t id, uint32_t new_count)
{
if (!_engine || !_engine->_renderPassManager) return false;
ParticlePass* particlePass = _engine->_renderPassManager->getPass<ParticlePass>();
if (!particlePass) return false;
return particlePass->resize_system(id, new_count);
}
bool Engine::get_particle_system(uint32_t id, ParticleSystem& out) const
{
if (!_engine || !_engine->_renderPassManager) return false;
ParticlePass* particlePass = _engine->_renderPassManager->getPass<ParticlePass>();
if (!particlePass) return false;
const auto& systems = particlePass->systems();
for (const auto& sys : systems)
{
if (sys.id == id)
{
out.id = sys.id;
out.particleCount = sys.count;
out.enabled = sys.enabled;
out.reset = sys.reset;
out.blendMode = static_cast<ParticleBlendMode>(sys.blend);
out.flipbookTexture = sys.flipbook_texture;
out.noiseTexture = sys.noise_texture;
// Copy parameters
const auto& p = sys.params;
out.params.emitterPosLocal = p.emitter_pos_local;
out.params.spawnRadius = p.spawn_radius;
out.params.emitterDirLocal = p.emitter_dir_local;
out.params.coneAngleDegrees = p.cone_angle_degrees;
out.params.minSpeed = p.min_speed;
out.params.maxSpeed = p.max_speed;
out.params.minLife = p.min_life;
out.params.maxLife = p.max_life;
out.params.minSize = p.min_size;
out.params.maxSize = p.max_size;
out.params.drag = p.drag;
out.params.gravity = p.gravity;
out.params.color = p.color;
out.params.softDepthDistance = p.soft_depth_distance;
out.params.flipbookCols = p.flipbook_cols;
out.params.flipbookRows = p.flipbook_rows;
out.params.flipbookFps = p.flipbook_fps;
out.params.flipbookIntensity = p.flipbook_intensity;
out.params.noiseScale = p.noise_scale;
out.params.noiseStrength = p.noise_strength;
out.params.noiseScroll = p.noise_scroll;
return true;
}
}
return false;
}
bool Engine::set_particle_system(uint32_t id, const ParticleSystem& system)
{
if (!_engine || !_engine->_renderPassManager) return false;
ParticlePass* particlePass = _engine->_renderPassManager->getPass<ParticlePass>();
if (!particlePass) return false;
auto& systems = particlePass->systems();
for (auto& sys : systems)
{
if (sys.id == id)
{
sys.enabled = system.enabled;
sys.reset = system.reset;
sys.blend = static_cast<ParticlePass::BlendMode>(system.blendMode);
sys.flipbook_texture = system.flipbookTexture;
sys.noise_texture = system.noiseTexture;
// Copy parameters
auto& p = sys.params;
p.emitter_pos_local = system.params.emitterPosLocal;
p.spawn_radius = system.params.spawnRadius;
p.emitter_dir_local = system.params.emitterDirLocal;
p.cone_angle_degrees = system.params.coneAngleDegrees;
p.min_speed = system.params.minSpeed;
p.max_speed = system.params.maxSpeed;
p.min_life = system.params.minLife;
p.max_life = system.params.maxLife;
p.min_size = system.params.minSize;
p.max_size = system.params.maxSize;
p.drag = system.params.drag;
p.gravity = system.params.gravity;
p.color = system.params.color;
p.soft_depth_distance = system.params.softDepthDistance;
p.flipbook_cols = system.params.flipbookCols;
p.flipbook_rows = system.params.flipbookRows;
p.flipbook_fps = system.params.flipbookFps;
p.flipbook_intensity = system.params.flipbookIntensity;
p.noise_scale = system.params.noiseScale;
p.noise_strength = system.params.noiseStrength;
p.noise_scroll = system.params.noiseScroll;
// Preload textures if changed
if (!sys.flipbook_texture.empty())
{
particlePass->preload_vfx_texture(sys.flipbook_texture);
}
if (!sys.noise_texture.empty())
{
particlePass->preload_vfx_texture(sys.noise_texture);
}
return true;
}
}
return false;
}
std::vector<uint32_t> Engine::get_particle_system_ids() const
{
std::vector<uint32_t> ids;
if (!_engine || !_engine->_renderPassManager) return ids;
ParticlePass* particlePass = _engine->_renderPassManager->getPass<ParticlePass>();
if (!particlePass) return ids;
const auto& systems = particlePass->systems();
ids.reserve(systems.size());
for (const auto& sys : systems)
{
ids.push_back(sys.id);
}
return ids;
}
uint32_t Engine::get_allocated_particles() const
{
if (!_engine || !_engine->_renderPassManager) return 0;
ParticlePass* particlePass = _engine->_renderPassManager->getPass<ParticlePass>();
if (!particlePass) return 0;
return particlePass->allocated_particles();
}
uint32_t Engine::get_free_particles() const
{
if (!_engine || !_engine->_renderPassManager) return 0;
ParticlePass* particlePass = _engine->_renderPassManager->getPass<ParticlePass>();
if (!particlePass) return 0;
return particlePass->free_particles();
}
uint32_t Engine::get_max_particles() const
{
return ParticlePass::k_max_particles;
}
void Engine::preload_particle_texture(const std::string& assetPath)
{
if (!_engine || !_engine->_renderPassManager) return;
ParticlePass* particlePass = _engine->_renderPassManager->getPass<ParticlePass>();
if (!particlePass) return;
particlePass->preload_vfx_texture(assetPath);
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Picking / Selection // Picking / Selection
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View File

@@ -42,6 +42,21 @@ enum class TonemapOperator : int
ACES = 1 ACES = 1
}; };
// Voxel volume type (cloud/smoke/flame)
enum class VoxelVolumeType : uint32_t
{
Clouds = 0,
Smoke = 1,
Flame = 2
};
// Particle blend mode
enum class ParticleBlendMode : uint32_t
{
Additive = 0, // Additive blending (for fire, sparks, etc.)
Alpha = 1 // Alpha blending with depth sorting
};
// Primitive geometry types // Primitive geometry types
enum class PrimitiveType enum class PrimitiveType
{ {
@@ -107,6 +122,107 @@ struct SpotLightD
float outer_angle_deg{25.0f}; float outer_angle_deg{25.0f};
}; };
// Voxel volumetric settings (cloud/smoke/flame)
struct VoxelVolumeSettings
{
bool enabled{false};
VoxelVolumeType type{VoxelVolumeType::Clouds};
// If true, volume follows camera XZ and volumeCenterLocal is treated as offset
// If false, volumeCenterLocal is absolute render-local space
bool followCameraXZ{false};
// If true, run voxel advection/update compute pass every frame
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};
};
// Particle system parameters
struct ParticleParams
{
glm::vec3 emitterPosLocal{0.0f, 0.0f, 0.0f};
float spawnRadius{0.1f};
glm::vec3 emitterDirLocal{0.0f, 1.0f, 0.0f};
float coneAngleDegrees{20.0f};
float minSpeed{2.0f};
float maxSpeed{8.0f};
float minLife{0.5f};
float maxLife{1.5f};
float minSize{0.05f};
float maxSize{0.15f};
float drag{1.0f};
float gravity{0.0f}; // positive pulls down -Y in local space
glm::vec4 color{1.0f, 0.5f, 0.1f, 1.0f};
// Fade particles near opaque geometry intersections (0 disables)
float softDepthDistance{0.15f};
// Flipbook sampling (atlas layout and animation)
uint32_t flipbookCols{16};
uint32_t flipbookRows{4};
float flipbookFps{30.0f};
float flipbookIntensity{1.0f};
// Noise UV distortion
float noiseScale{6.0f};
float noiseStrength{0.05f};
glm::vec2 noiseScroll{0.0f, 0.0f};
};
// Particle system settings
struct ParticleSystem
{
uint32_t id{0};
uint32_t particleCount{0};
bool enabled{true};
bool reset{true};
ParticleBlendMode blendMode{ParticleBlendMode::Additive};
ParticleParams params{};
// Asset-relative texture paths (e.g., "vfx/flame.ktx2")
// Empty string disables the texture
std::string flipbookTexture{"vfx/flame.ktx2"};
std::string noiseTexture{"vfx/simplex.ktx2"};
};
// IBL (Image-Based Lighting) paths // IBL (Image-Based Lighting) paths
struct IBLPaths struct IBLPaths
{ {
@@ -496,6 +612,51 @@ public:
Stats get_stats() const; Stats get_stats() const;
// ------------------------------------------------------------------------
// Volumetrics (Cloud/Smoke/Flame)
// ------------------------------------------------------------------------
// Enable/disable volumetrics system
void set_volumetrics_enabled(bool enabled);
bool get_volumetrics_enabled() const;
// Get/set voxel volume settings by index (0-3)
bool get_voxel_volume(size_t index, VoxelVolumeSettings& out) const;
bool set_voxel_volume(size_t index, const VoxelVolumeSettings& settings);
// Get maximum number of voxel volumes
size_t get_max_voxel_volumes() const;
// ------------------------------------------------------------------------
// Particle Systems
// ------------------------------------------------------------------------
// Create a new particle system (returns system ID, 0 on failure)
uint32_t create_particle_system(uint32_t particle_count);
// Destroy a particle system by ID
bool destroy_particle_system(uint32_t id);
// Resize a particle system (reallocates particle count)
bool resize_particle_system(uint32_t id, uint32_t new_count);
// Get particle system settings by ID
bool get_particle_system(uint32_t id, ParticleSystem& out) const;
// Set particle system settings by ID
bool set_particle_system(uint32_t id, const ParticleSystem& system);
// Get all particle system IDs
std::vector<uint32_t> get_particle_system_ids() const;
// Get particle pool statistics
uint32_t get_allocated_particles() const;
uint32_t get_free_particles() const;
uint32_t get_max_particles() const;
// Preload a VFX texture (e.g., "vfx/flame.ktx2")
void preload_particle_texture(const std::string& assetPath);
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Picking / Selection // Picking / Selection
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------

View File

@@ -119,21 +119,6 @@ void SceneManager::init(EngineContext *context)
sceneData.sunlightDirection = glm::vec4(-0.2f, -1.0f, -0.3f, 1.0f); sceneData.sunlightDirection = glm::vec4(-0.2f, -1.0f, -0.3f, 1.0f);
sceneData.sunlightColor = glm::vec4(1.0f, 1.0f, 1.0f, 3.0f); sceneData.sunlightColor = glm::vec4(1.0f, 1.0f, 1.0f, 3.0f);
// Seed a couple of default point lights for quick testing.
PointLight warmKey{};
warmKey.position_world = WorldVec3(0.0, 0.0, 0.0);
warmKey.radius = 25.0f;
warmKey.color = glm::vec3(1.0f, 0.95f, 0.8f);
warmKey.intensity = 15.0f;
addPointLight(warmKey);
PointLight coolFill{};
coolFill.position_world = WorldVec3(-10.0, 4.0, 10.0);
coolFill.radius = 20.0f;
coolFill.color = glm::vec3(0.6f, 0.7f, 1.0f);
coolFill.intensity = 10.0f;
addPointLight(coolFill);
_camera_position_local = world_to_local(mainCamera.position_world, _origin_world); _camera_position_local = world_to_local(mainCamera.position_world, _origin_world);
} }