diff --git a/src/core/assets/ibl_manager.cpp b/src/core/assets/ibl_manager.cpp index ceafab2..9286085 100644 --- a/src/core/assets/ibl_manager.cpp +++ b/src/core/assets/ibl_manager.cpp @@ -401,6 +401,7 @@ IBLManager::AsyncResult IBLManager::pump_async() PreparedIBLData data{}; bool success = false; + std::string error; { std::lock_guard lock(state->mutex); if (!state->resultReady) @@ -409,12 +410,17 @@ IBLManager::AsyncResult IBLManager::pump_async() } data = std::move(state->readyData); success = state->resultSuccess; + error = std::move(state->lastError); state->resultReady = false; } out.completed = true; if (!success) { + if (!error.empty()) + { + fmt::println("[IBL] async load failed: {}", error); + } out.success = false; return out; } diff --git a/src/core/engine.cpp b/src/core/engine.cpp index f6d9fac..6074a38 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -1051,7 +1051,7 @@ void VulkanEngine::draw() if (newVolume != _activeIBLVolume) { - const IBLPaths *paths = nullptr; + IBLPaths *paths = nullptr; if (newVolume >= 0) { paths = &_iblVolumes[newVolume].paths; @@ -1067,17 +1067,27 @@ void VulkanEngine::draw() 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.targetVolume = newVolume; - _pendingIBLRequest.paths = *paths; + _pendingIBLRequest.paths = resolved; } else { fmt::println("[Engine] Warning: failed to enqueue IBL load for {} (specular='{}')", (newVolume >= 0) ? "volume" : "global environment", - paths->specularCube); + resolved.specularCube); } } } diff --git a/src/core/engine_ui.cpp b/src/core/engine_ui.cpp index bb39a1a..15f892e 100644 --- a/src/core/engine_ui.cpp +++ b/src/core/engine_ui.cpp @@ -38,6 +38,18 @@ 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) { if (!eng || !eng->_window) return; @@ -639,6 +651,35 @@ namespace ImGui::PushID(static_cast(i)); ImGui::Separator(); ImGui::Text("Volume %zu", i); + ImGui::SameLine(); + if (ImGui::Button("Delete")) + { + const int idx = static_cast(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); { 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) { + vol.paths = resolve_ibl_paths(eng, vol.paths); if (eng->_iblManager->load_async(vol.paths)) { eng->_pendingIBLRequest.active = true; @@ -691,6 +733,7 @@ namespace ImGui::SameLine(); if (ImGui::Button("Set As Global IBL")) { + vol.paths = resolve_ibl_paths(eng, vol.paths); eng->_globalIBLPaths = vol.paths; if (eng->_iblManager) { diff --git a/src/core/game_api.cpp b/src/core/game_api.cpp index 2625300..7ea3781 100644 --- a/src/core/game_api.cpp +++ b/src/core/game_api.cpp @@ -6,6 +6,7 @@ #include "core/pipeline/manager.h" #include "render/passes/tonemap.h" #include "render/passes/fxaa.h" +#include "render/passes/particles.h" #include "render/renderpass.h" #include "core/picking/picking_system.h" #include "scene/vk_scene.h" @@ -1415,6 +1416,293 @@ Stats Engine::get_stats() const 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(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(); + 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(); + 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(); + 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(); + 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(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(); + 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(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 Engine::get_particle_system_ids() const +{ + std::vector ids; + + if (!_engine || !_engine->_renderPassManager) return ids; + + ParticlePass* particlePass = _engine->_renderPassManager->getPass(); + 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(); + 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(); + 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(); + if (!particlePass) return; + + particlePass->preload_vfx_texture(assetPath); +} + // ---------------------------------------------------------------------------- // Picking / Selection // ---------------------------------------------------------------------------- diff --git a/src/core/game_api.h b/src/core/game_api.h index 8d857c0..a23fc08 100644 --- a/src/core/game_api.h +++ b/src/core/game_api.h @@ -42,6 +42,21 @@ enum class TonemapOperator : int 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 enum class PrimitiveType { @@ -107,6 +122,107 @@ struct SpotLightD 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 struct IBLPaths { @@ -496,6 +612,51 @@ public: 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 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 // ------------------------------------------------------------------------ diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index dee487a..e5d2097 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -119,21 +119,6 @@ void SceneManager::init(EngineContext *context) sceneData.sunlightDirection = glm::vec4(-0.2f, -1.0f, -0.3f, 1.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); }