From cab59044a6e0dce2fd06e679cc0fde559287d2b1 Mon Sep 17 00:00:00 2001 From: glaszczkoldre Date: Sun, 31 May 2026 19:25:14 +0200 Subject: [PATCH 1/6] feat(lighting): implement server-like world lighting and light propagation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced a comprehensive lighting system mimicking the server's WorldLight protocol, replacing simple ambient levels with global intensity/color settings, ground-based light blocking, and translucent propagation. Added support for creature-based light sources and integrated refined controls into both the main map editor and ingame preview UI. - `ImguiMapEditor/Application/CallbackMediator.cpp` — added server light intensity/color to mediator state - `ImguiMapEditor/Application/EditorSession.h` — updated session lighting defaults - `ImguiMapEditor/Domain/CreatureType.h` — added light level/color properties to creature types - `ImguiMapEditor/Domain/ItemType.h` — added `lens_help` for translucent light propagation - `ImguiMapEditor/Domain/LightTypes.h` — defined `GlobalLight` struct and updated `LightConfig` to support server-style global lighting - `ImguiMapEditor/Domain/Tile.h` — added `TranslucentLight` flag - `ImguiMapEditor/IO/CreatureXmlReader.cpp` — parsed light properties from creature definitions - `ImguiMapEditor/Presentation/MainWindow.cpp` — updated lighting toolbar with intensity/color sliders - `ImguiMapEditor/Rendering/Frame/RenderState.h` — added config hash for cache invalidation - `ImguiMapEditor/Rendering/Light/LightGatherer.cpp` — implemented light blocking logic and translucent ground propagation - `ImguiMapEditor/Rendering/Light/LightGatherer.h` — added `GroundBrightness` tracking for occlusion - `ImguiMapEditor/Rendering/Light/LightManager.cpp` — implemented multi-layered ambient light blending and ground-based light occlusion - `ImguiMapEditor/Rendering/Light/LightManager.h` — passed `GroundBrightness` and optional light injection to renderer - `ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp` — added player light injection and updated lighting configuration - `ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h` — added config hash tracking - `ImguiMapEditor/Rendering/Passes/LightingPass.cpp` — switched to new server-based lighting configuration and validation - `ImguiMapEditor/Services/ClientDataService.cpp` — exposed `lens_help` to merged item data - `ImguiMapEditor/Services/ViewSettings.cpp` — updated config load/save for new server light parameters - `ImguiMapEditor/Services/ViewSettings.h` — added server light settings fields - `ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp` — updated preview UI with intensity/color controls --- .../Application/CallbackMediator.cpp | 4 + ImguiMapEditor/Application/EditorSession.h | 4 +- ImguiMapEditor/Domain/CreatureType.h | 4 + ImguiMapEditor/Domain/ItemType.h | 3 + ImguiMapEditor/Domain/LightTypes.h | 17 +- ImguiMapEditor/Domain/Tile.h | 3 +- ImguiMapEditor/IO/CreatureXmlReader.cpp | 8 + ImguiMapEditor/Presentation/MainWindow.cpp | 21 ++- ImguiMapEditor/Rendering/Frame/RenderState.h | 1 + .../Rendering/Light/LightGatherer.cpp | 159 +++++++++++------- .../Rendering/Light/LightGatherer.h | 40 ++++- .../Rendering/Light/LightManager.cpp | 76 +++++++-- ImguiMapEditor/Rendering/Light/LightManager.h | 8 +- .../Passes/IngamePreviewRenderer.cpp | 34 +++- .../Rendering/Passes/IngamePreviewRenderer.h | 1 + .../Rendering/Passes/LightingPass.cpp | 23 ++- ImguiMapEditor/Services/ClientDataService.cpp | 3 + ImguiMapEditor/Services/ViewSettings.cpp | 12 +- ImguiMapEditor/Services/ViewSettings.h | 8 +- ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp | 22 ++- 20 files changed, 338 insertions(+), 113 deletions(-) diff --git a/ImguiMapEditor/Application/CallbackMediator.cpp b/ImguiMapEditor/Application/CallbackMediator.cpp index 2420711..7eca4c7 100644 --- a/ImguiMapEditor/Application/CallbackMediator.cpp +++ b/ImguiMapEditor/Application/CallbackMediator.cpp @@ -213,6 +213,8 @@ void CallbackMediator::wireTabCallbacks(Context &ctx) { state.current_floor = ctx.map_panel->getCurrentFloor(); state.lighting_enabled = ctx.view_settings->map_lighting_enabled; state.ambient_light = ctx.view_settings->map_ambient_light; + state.server_light_intensity = ctx.view_settings->server_light_intensity; + state.server_light_color = ctx.view_settings->server_light_color; state.show_ingame_box = ctx.view_settings->show_ingame_box; state.show_minimap = ctx.view_settings->show_minimap_window; @@ -238,6 +240,8 @@ void CallbackMediator::wireTabCallbacks(Context &ctx) { ctx.map_panel->setCurrentFloor(static_cast(state.current_floor)); ctx.view_settings->map_lighting_enabled = state.lighting_enabled; ctx.view_settings->map_ambient_light = state.ambient_light; + ctx.view_settings->server_light_intensity = state.server_light_intensity; + ctx.view_settings->server_light_color = state.server_light_color; ctx.view_settings->show_ingame_box = state.show_ingame_box; ctx.view_settings->show_minimap_window = state.show_minimap; ctx.minimap->setMap(session->getMap(), diff --git a/ImguiMapEditor/Application/EditorSession.h b/ImguiMapEditor/Application/EditorSession.h index 58de8ec..fcd8954 100644 --- a/ImguiMapEditor/Application/EditorSession.h +++ b/ImguiMapEditor/Application/EditorSession.h @@ -118,7 +118,9 @@ class EditorSession { int current_floor = 7; // Per-map lighting settings bool lighting_enabled = false; - int ambient_light = 128; + int ambient_light = 0; + uint8_t server_light_intensity = 200; + uint8_t server_light_color = 215; // Per-map window visibility bool show_ingame_box = false; bool show_minimap = false; diff --git a/ImguiMapEditor/Domain/CreatureType.h b/ImguiMapEditor/Domain/CreatureType.h index e272e9b..b5e0f25 100644 --- a/ImguiMapEditor/Domain/CreatureType.h +++ b/ImguiMapEditor/Domain/CreatureType.h @@ -9,6 +9,10 @@ struct CreatureType { std::string name; bool is_npc = false; Outfit outfit; + + // Light properties (from DAT or creatures.xml) + uint8_t light_level = 0; // 0 = no light, >0 = light radius + uint8_t light_color = 0; // 8-bit Tibia palette index // Default constructor CreatureType() = default; diff --git a/ImguiMapEditor/Domain/ItemType.h b/ImguiMapEditor/Domain/ItemType.h index b9b4cfb..d62addf 100644 --- a/ImguiMapEditor/Domain/ItemType.h +++ b/ImguiMapEditor/Domain/ItemType.h @@ -227,6 +227,9 @@ class ItemType { // Translucency (from DAT) bool is_translucent = false; + // Lens help (from DAT) — items with value > 0 are "see-through" for light propagation + uint16_t lens_help = 0; + // Elevation (from DAT) - items on this raise subsequent items visually uint16_t elevation = 0; diff --git a/ImguiMapEditor/Domain/LightTypes.h b/ImguiMapEditor/Domain/LightTypes.h index 98c547c..7cc19b7 100644 --- a/ImguiMapEditor/Domain/LightTypes.h +++ b/ImguiMapEditor/Domain/LightTypes.h @@ -5,6 +5,15 @@ namespace MapEditor { namespace Domain { +/** + * Global world light (mimics server WorldLight protocol message). + * Defines the ambient light level for above-ground floors. + */ +struct GlobalLight { + uint8_t intensity = 0; // Server ambient level (0-255) + uint8_t color = 215; // Server ambient color (8-bit palette index) +}; + /** * A light source extracted from a tile item. * Contains position, color (8-bit palette index), and intensity. @@ -14,6 +23,7 @@ struct LightSource { int32_t y = 0; // Tile Y position uint8_t color = 215; // 8-bit palette index (default: white-ish) uint8_t intensity = 0; // 0-255 (affects light radius) + int16_t source_floor = 0; // Floor this light originates from (for blocking) }; /** @@ -22,8 +32,11 @@ struct LightSource { */ struct LightConfig { bool enabled = false; // Master enable for this viewport - uint8_t ambient_level = 255; // 0 = complete darkness, 255 = full bright - uint8_t ambient_color = 215; // 8-bit palette index for ambient + GlobalLight global_light; // Server global light (above-ground ambient) + uint8_t client_slider = 0; // Client minimum ambient slider (0-255), acts as floor + int16_t camera_floor = 7; // Current camera floor (for above/underground check) + uint8_t ambient_color = 215; // DEPRECATED: kept for backward compat, use global_light.color + uint8_t ambient_level = 255; // DEPRECATED: kept for backward compat, computed from global_light + slider }; } // namespace Domain diff --git a/ImguiMapEditor/Domain/Tile.h b/ImguiMapEditor/Domain/Tile.h index 5dd51cc..d3a0f54 100644 --- a/ImguiMapEditor/Domain/Tile.h +++ b/ImguiMapEditor/Domain/Tile.h @@ -26,7 +26,8 @@ enum class TileFlag : uint16_t { NoPvp = 1 << 2, // 0x0004 NoLogout = 1 << 3, // 0x0008 PvpZone = 1 << 4, // 0x0010 - Refresh = 1 << 5 // 0x0020 + Refresh = 1 << 5, // 0x0020 + TranslucentLight = 1 << 6 // 0x0040 — z=8 tile lit by translucent ground above }; inline TileFlag operator|(TileFlag a, TileFlag b) { diff --git a/ImguiMapEditor/IO/CreatureXmlReader.cpp b/ImguiMapEditor/IO/CreatureXmlReader.cpp index 8390e98..3c165bb 100644 --- a/ImguiMapEditor/IO/CreatureXmlReader.cpp +++ b/ImguiMapEditor/IO/CreatureXmlReader.cpp @@ -135,6 +135,14 @@ CreatureXmlReader::parseCreatureNode(const pugi::xml_node &node, bool isNpc, creature->outfit.lookMountFeet = static_cast(attr.as_uint()); } + // Light properties (optional) + if (auto attr = node.attribute("lightlevel")) { + creature->light_level = static_cast(attr.as_uint()); + } + if (auto attr = node.attribute("lightcolor")) { + creature->light_color = static_cast(attr.as_uint()); + } + return creature; } diff --git a/ImguiMapEditor/Presentation/MainWindow.cpp b/ImguiMapEditor/Presentation/MainWindow.cpp index ab63ebf..2cc8021 100644 --- a/ImguiMapEditor/Presentation/MainWindow.cpp +++ b/ImguiMapEditor/Presentation/MainWindow.cpp @@ -134,10 +134,23 @@ void MainWindow::renderEditor(Domain::ChunkedMap *current_map, // Lighting controls toolbar (per-map) ImGui::Checkbox("Enable Lighting", &view_settings_.map_lighting_enabled); - ImGui::SameLine(); - ImGui::SetNextItemWidth(120); - ImGui::SliderInt("Ambient", &view_settings_.map_ambient_light, 0, - 255); + if (view_settings_.map_lighting_enabled) { + ImGui::SameLine(); + ImGui::SetNextItemWidth(120); + int intensity = static_cast(view_settings_.server_light_intensity); + if (ImGui::SliderInt("Intensity", &intensity, 0, 255)) { + view_settings_.server_light_intensity = static_cast(intensity); + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(60); + int color = static_cast(view_settings_.server_light_color); + if (ImGui::SliderInt("Color", &color, 0, 215)) { + view_settings_.server_light_color = static_cast(color); + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(120); + ImGui::SliderInt("Min Ambient", &view_settings_.map_ambient_light, 0, 255); + } ImGui::Separator(); // Render the map panel with explicit animation ticks diff --git a/ImguiMapEditor/Rendering/Frame/RenderState.h b/ImguiMapEditor/Rendering/Frame/RenderState.h index e44d318..e68dfb1 100644 --- a/ImguiMapEditor/Rendering/Frame/RenderState.h +++ b/ImguiMapEditor/Rendering/Frame/RenderState.h @@ -45,6 +45,7 @@ class RenderState { // === State tracking for cache invalidation === float last_zoom = 0.0f; uint8_t last_ambient_light = 255; + uint32_t last_config_hash = 0; // Hash of server light + slider for change detection // === Invalidation methods === diff --git a/ImguiMapEditor/Rendering/Light/LightGatherer.cpp b/ImguiMapEditor/Rendering/Light/LightGatherer.cpp index 9368789..79650a1 100644 --- a/ImguiMapEditor/Rendering/Light/LightGatherer.cpp +++ b/ImguiMapEditor/Rendering/Light/LightGatherer.cpp @@ -3,13 +3,35 @@ #include "Domain/Tile.h" #include "Domain/Item.h" #include "Domain/ItemType.h" +#include "Domain/Creature.h" +#include "Domain/CreatureType.h" #include +#include namespace MapEditor { namespace Rendering { void LightGatherer::clear() { lights_.clear(); + ground_brightness_ = GroundBrightness{}; +} + +void LightGatherer::addLight(int32_t x, int32_t y, uint8_t color, uint8_t intensity, int16_t floor) { + // Dedup: merge consecutive lights with same pos, color, and floor + if (!lights_.empty()) { + auto& prev = lights_.back(); + if (prev.x == x && prev.y == y && prev.color == color && prev.source_floor == floor) { + prev.intensity = std::max(prev.intensity, intensity); + return; + } + } + lights_.emplace_back(Domain::LightSource{ + .x = x, + .y = y, + .color = color, + .intensity = intensity, + .source_floor = floor + }); } void LightGatherer::gatherForChunk( @@ -20,32 +42,20 @@ void LightGatherer::gatherForChunk( { if (!client_data) return; - // We need to check the target chunk AND its 8 neighbors (3x3 grid) - // because a light in a neighbor might spill into this chunk. for (int dy = -1; dy <= 1; ++dy) { for (int dx = -1; dx <= 1; ++dx) { - // Get visible chunks directly from the map using exact chunk coordinates - // Note: ChunkedMap doesn't expose "getChunk(cx, cy)" publicly in the interface I saw earlier, - // but it has `getVisibleChunks` which takes pixel/tile coords. - // Let's use tile coordinates to find the chunks. - int32_t target_cx = chunk_x + dx; int32_t target_cy = chunk_y + dy; - // Calculate world tile coordinates for this chunk int32_t tile_start_x = target_cx * Domain::Chunk::SIZE; int32_t tile_start_y = target_cy * Domain::Chunk::SIZE; int32_t tile_end_x = tile_start_x + Domain::Chunk::SIZE; int32_t tile_end_y = tile_start_y + Domain::Chunk::SIZE; - // Efficiently get the single chunk for this region (or just iterate tiles if API limits) - // The existing `getVisibleChunks` returns a vector. Let's use that. std::vector chunks; map.getVisibleChunks(tile_start_x, tile_start_y, tile_end_x - 1, tile_end_y - 1, floor, chunks); for (Domain::Chunk* chunk : chunks) { - // Ensure we are processing the correct chunk (getVisibleChunks might return overlaps if not careful, - // but with exact coordinates it should be 1 chunk) if (!chunk) continue; chunk->forEachTile([&](const Domain::Tile* tile) { @@ -59,19 +69,7 @@ void LightGatherer::gatherForChunk( if (ground) { const Domain::ItemType* item_type = client_data->getItemTypeByServerId(ground->getServerId()); if (item_type && item_type->light_level > 0) { - Domain::LightSource light; - light.x = tile_x; - light.y = tile_y; - light.color = item_type->light_color; - light.intensity = item_type->light_level; - lights_.push_back(light); - - // Check for invalid color - if (light.color == 0) { - // spdlog::warn("Light source with NO COLOR (Index 0) found at {},{}: ItemID {}, Level {}", tile_x, tile_y, ground->getServerId(), light.intensity); - } else { - // spdlog::debug("Light found at {},{}: ItemID {}, Level {}, Color {}", tile_x, tile_y, ground->getServerId(), light.intensity, light.color); - } + addLight(tile_x, tile_y, item_type->light_color, item_type->light_level, floor); } } @@ -80,18 +78,16 @@ void LightGatherer::gatherForChunk( if (!item_ptr) continue; const Domain::ItemType* item_type = client_data->getItemTypeByServerId(item_ptr->getServerId()); if (item_type && item_type->light_level > 0) { - Domain::LightSource light; - light.x = tile_x; - light.y = tile_y; - light.color = item_type->light_color; - light.intensity = item_type->light_level; - lights_.push_back(light); - - if (light.color == 0) { - // spdlog::warn("Light source with NO COLOR (Index 0) found at {},{}: ItemID {}, Level {}", tile_x, tile_y, item_ptr->getServerId(), light.intensity); - } else { - // spdlog::debug("Light found at {},{}: ItemID {}, Level {}, Color {}", tile_x, tile_y, item_ptr->getServerId(), light.intensity, light.color); - } + addLight(tile_x, tile_y, item_type->light_color, item_type->light_level, floor); + } + } + + // Check creature for light + const Domain::Creature* creature = tile->getCreature(); + if (creature) { + const Domain::CreatureType* creature_type = client_data->getCreatureType(creature->name); + if (creature_type && creature_type->light_level > 0) { + addLight(tile_x, tile_y, creature_type->light_color, creature_type->light_level, floor); } } }); @@ -111,24 +107,21 @@ void LightGatherer::gatherForChunkMultiFloor( constexpr int GROUND_LAYER = 7; - // Iterate through all floors in range (from start_floor down to end_floor) + // Reset ground brightness for the target chunk + ground_brightness_ = GroundBrightness{}; + for (int16_t floor = start_floor; floor >= end_floor; --floor) { - - // Calculate isometric offset for this floor (RME-style) - // Lights from higher floors (lower Z) appear shifted in X and Y int32_t floor_offset = 0; if (floor <= GROUND_LAYER) { - // Above ground: offset based on distance from ground layer floor_offset = GROUND_LAYER - floor; } - // Underground floors don't get offset in RME's light system - // We need to check the target chunk AND its 8 neighbors (3x3 grid) for (int dy = -1; dy <= 1; ++dy) { for (int dx = -1; dx <= 1; ++dx) { + bool is_target = (dx == 0 && dy == 0); gatherLightsFromNeighborChunk( map, chunk_x + dx, chunk_y + dy, - client_data, floor, floor_offset); + client_data, floor, floor_offset, is_target); } } } @@ -138,13 +131,19 @@ void LightGatherer::gatherLightsFromNeighborChunk( const Domain::ChunkedMap& map, int32_t target_cx, int32_t target_cy, Services::ClientDataService* client_data, - int16_t floor, int32_t floor_offset) + int16_t floor, int32_t floor_offset, + bool is_target_chunk) { int32_t tile_start_x = target_cx * Domain::Chunk::SIZE; int32_t tile_start_y = target_cy * Domain::Chunk::SIZE; int32_t tile_end_x = tile_start_x + Domain::Chunk::SIZE; int32_t tile_end_y = tile_start_y + Domain::Chunk::SIZE; + int32_t target_chunk_start_x = target_cx * Domain::Chunk::SIZE; + int32_t target_chunk_start_y = target_cy * Domain::Chunk::SIZE; + + constexpr int GROUND_LAYER = 7; + std::vector chunks; map.getVisibleChunks(tile_start_x, tile_start_y, tile_end_x - 1, tile_end_y - 1, floor, chunks); @@ -157,32 +156,78 @@ void LightGatherer::gatherLightsFromNeighborChunk( int32_t tile_x = tile->getX(); int32_t tile_y = tile->getY(); - // Apply isometric offset (RME-style: lights from higher floors - // are projected onto 2D with offset) int32_t adjusted_x = tile_x - floor_offset; int32_t adjusted_y = tile_y - floor_offset; - // Helper lambda to add light + // Track ground solidity for blocking (only for the target chunk) + if (is_target_chunk) { + const Domain::Item* ground = tile->getGround(); + if (ground) { + const Domain::ItemType* ground_type = client_data->getItemTypeByServerId(ground->getServerId()); + if (ground_type && ground_type->is_ground && !ground_type->is_translucent) { + int32_t local_x = tile_x - target_chunk_start_x; + int32_t local_y = tile_y - target_chunk_start_y; + if (local_x >= 0 && local_x < 32 && local_y >= 0 && local_y < 32) { + int idx = local_y * 32 + local_x; + if (ground_brightness_.blocking_floor[idx] < 0 || floor < ground_brightness_.blocking_floor[idx]) { + ground_brightness_.blocking_floor[idx] = floor; + } + } + } + } + } + + // Add light sources from items auto addLightFromItem = [&](const Domain::Item* item) { if (!item) return; const Domain::ItemType* item_type = client_data->getItemTypeByServerId(item->getServerId()); if (item_type && item_type->light_level > 0) { - lights_.emplace_back(Domain::LightSource{ - .x = adjusted_x, - .y = adjusted_y, - .color = item_type->light_color, - .intensity = item_type->light_level - }); + addLight(adjusted_x, adjusted_y, item_type->light_color, item_type->light_level, floor); } }; - // Check ground item addLightFromItem(tile->getGround()); - // Check all items on the tile for (const auto& item_ptr : tile->getItems()) { addLightFromItem(item_ptr.get()); } + + // Check creature for light (Phase 4) + const Domain::Creature* creature = tile->getCreature(); + if (creature) { + const Domain::CreatureType* creature_type = client_data->getCreatureType(creature->name); + if (creature_type && creature_type->light_level > 0) { + addLight(adjusted_x, adjusted_y, creature_type->light_color, creature_type->light_level, floor); + } + } + + // Translucent ground propagation (Phase 5) + if (floor == GROUND_LAYER) { + bool has_translucent = false; + + const Domain::Item* ground_check = tile->getGround(); + if (ground_check) { + const Domain::ItemType* ground_type = client_data->getItemTypeByServerId(ground_check->getServerId()); + if (ground_type && (ground_type->is_translucent || ground_type->lens_help > 0)) { + has_translucent = true; + } + } + + if (!has_translucent) { + for (const auto& item_ptr : tile->getItems()) { + const Domain::ItemType* item_type = client_data->getItemTypeByServerId(item_ptr->getServerId()); + if (item_type && (item_type->is_translucent || item_type->lens_help > 0)) { + has_translucent = true; + break; + } + } + } + + if (has_translucent) { + // Emit dim white light at z=8 (no isometric offset for underground) + addLight(tile_x, tile_y, 215, 1, static_cast(GROUND_LAYER + 1)); + } + } }); } } diff --git a/ImguiMapEditor/Rendering/Light/LightGatherer.h b/ImguiMapEditor/Rendering/Light/LightGatherer.h index edef1ef..93782cf 100644 --- a/ImguiMapEditor/Rendering/Light/LightGatherer.h +++ b/ImguiMapEditor/Rendering/Light/LightGatherer.h @@ -2,6 +2,7 @@ #include #include +#include #include "Domain/ChunkedMap.h" #include "Domain/LightTypes.h" namespace MapEditor { @@ -12,6 +13,21 @@ namespace Services { namespace Rendering { +/** + * Per-tile ground blocking data for a chunk. + * Records the highest floor (lowest Z) with solid ground above each tile. + * Lights from floors above blocking_floor are blocked. + */ +struct GroundBrightness { + // For each tile (32x32), the floor with solid ground that blocks lights from above. + // Value of -1 means no blocking (all lights pass through). + std::array blocking_floor; + + GroundBrightness() { + blocking_floor.fill(-1); + } +}; + /** * Collects light sources from visible tiles. * @@ -38,13 +54,7 @@ class LightGatherer { /** * Gather all light sources from multiple floors for a specific chunk. * Applies isometric offset to light positions based on floor difference. - * - * @param map The map to gather lights from - * @param chunk_x Chunk X coordinate - * @param chunk_y Chunk Y coordinate - * @param client_data Client data service for item type lookup - * @param start_floor First floor to gather from (highest Z) - * @param end_floor Last floor to gather from (lowest Z) + * Also populates ground_brightness_ for light blocking. */ void gatherForChunkMultiFloor( const MapEditor::Domain::ChunkedMap& map, @@ -62,6 +72,12 @@ class LightGatherer { * Get number of light sources collected. */ size_t getLightCount() const { return lights_.size(); } + + /** + * Get the ground blocking data for the target chunk. + * Only valid after gatherForChunkMultiFloor() has been called. + */ + const GroundBrightness& getGroundBrightness() const { return ground_brightness_; } private: /** @@ -71,9 +87,17 @@ class LightGatherer { const MapEditor::Domain::ChunkedMap& map, int32_t target_cx, int32_t target_cy, Services::ClientDataService* client_data, - int16_t floor, int32_t floor_offset); + int16_t floor, int32_t floor_offset, + bool is_target_chunk); + + /** + * Add a light source with deduplication. + * If the last light has the same position, color, and floor, merge (keep higher intensity). + */ + void addLight(int32_t x, int32_t y, uint8_t color, uint8_t intensity, int16_t floor); std::vector lights_; + GroundBrightness ground_brightness_; }; } // namespace Rendering diff --git a/ImguiMapEditor/Rendering/Light/LightManager.cpp b/ImguiMapEditor/Rendering/Light/LightManager.cpp index e913327..42c355a 100644 --- a/ImguiMapEditor/Rendering/Light/LightManager.cpp +++ b/ImguiMapEditor/Rendering/Light/LightManager.cpp @@ -67,7 +67,8 @@ void LightManager::render(const Domain::ChunkedMap& map, float camera_x, float camera_y, float zoom, int current_floor, int start_floor, int end_floor, - const Domain::LightConfig& config) + const Domain::LightConfig& config, + const std::vector* injected_lights) { if (!config.enabled) return; @@ -96,8 +97,10 @@ void LightManager::render(const Domain::ChunkedMap& map, end_floor != last_end_floor_; bool config_changed = - config.ambient_color != last_config_.ambient_color || - config.ambient_level != last_config_.ambient_level || + config.global_light.color != last_config_.global_light.color || + config.global_light.intensity != last_config_.global_light.intensity || + config.client_slider != last_config_.client_slider || + config.camera_floor != last_config_.camera_floor || config.enabled != last_config_.enabled; // If bounds and config match, we can reuse the texture @@ -161,7 +164,15 @@ void LightManager::render(const Domain::ChunkedMap& map, gatherer_->gatherForChunk(map, cx, cy, client_data_, static_cast(current_floor)); } - computeChunkLight(grid, gatherer_->getLights(), config, cx, cy); + // Combine gathered lights with any injected lights + if (injected_lights && !injected_lights->empty()) { + std::vector combined = gatherer_->getLights(); + combined.insert(combined.end(), injected_lights->begin(), injected_lights->end()); + computeChunkLight(grid, combined, gatherer_->getGroundBrightness(), config, cx, cy, static_cast(current_floor)); + } else { + computeChunkLight(grid, gatherer_->getLights(), gatherer_->getGroundBrightness(), config, cx, cy, static_cast(current_floor)); + } + grid.is_valid = true; } @@ -213,23 +224,49 @@ void LightManager::render(const Domain::ChunkedMap& map, void LightManager::computeChunkLight(CachedLightGrid& grid, const std::vector& lights, + const GroundBrightness& ground_brightness, const Domain::LightConfig& config, - int32_t chunk_x, int32_t chunk_y) + int32_t chunk_x, int32_t chunk_y, + int16_t current_floor) { - // OPTIMIZED: Iterate lights first, then only affected tiles - - // Get ambient light color + // Determine ambient based on camera floor (above-ground vs underground) float ambient_r, ambient_g, ambient_b; - LightColorPalette::from8bitFloat(config.ambient_color, ambient_r, ambient_g, ambient_b); - float ambient_scale = config.ambient_level / 255.0f; - ambient_r *= ambient_scale; - ambient_g *= ambient_scale; - ambient_b *= ambient_scale; + constexpr int GROUND_LAYER = 7; + if (config.camera_floor <= GROUND_LAYER) { + // Above ground: use server global light + float scale = static_cast(config.global_light.intensity) / 255.0f; + LightColorPalette::from8bitFloat(config.global_light.color, ambient_r, ambient_g, ambient_b); + ambient_r *= scale; + ambient_g *= scale; + ambient_b *= scale; + } else { + // Underground: zero ambient (pitch black without lights) + ambient_r = 0.0f; + ambient_g = 0.0f; + ambient_b = 0.0f; + } - uint8_t base_r = static_cast(ambient_r * 255.0f); - uint8_t base_g = static_cast(ambient_g * 255.0f); - uint8_t base_b = static_cast(ambient_b * 255.0f); + // Apply client slider as floor above ground; as absolute underground + float slider_scale = static_cast(config.client_slider) / 255.0f; + if (config.camera_floor <= GROUND_LAYER) { + // Above ground: max(slider, ambient) + float slider_r = slider_scale; // White light from slider + float slider_g = slider_scale; + float slider_b = slider_scale; + ambient_r = std::max(ambient_r, slider_r); + ambient_g = std::max(ambient_g, slider_g); + ambient_b = std::max(ambient_b, slider_b); + } else { + // Underground: slider is absolute + ambient_r = slider_scale; + ambient_g = slider_scale; + ambient_b = slider_scale; + } + + uint8_t base_r = static_cast(std::min(ambient_r * 255.0f, 255.0f)); + uint8_t base_g = static_cast(std::min(ambient_g * 255.0f, 255.0f)); + uint8_t base_b = static_cast(std::min(ambient_b * 255.0f, 255.0f)); // Fill with ambient first uint32_t ambient_packed = base_r | (base_g << 8) | (base_b << 16) | (255 << 24); @@ -260,6 +297,13 @@ void LightManager::computeChunkLight(CachedLightGrid& grid, int tile_x = chunk_start_x + x; int tile_y = chunk_start_y + y; + // Ground blocking check: skip if solid ground exists between light and tile + int tile_idx = y * 32 + x; + int16_t blocking_floor = ground_brightness.blocking_floor[tile_idx]; + if (blocking_floor >= 0 && light.source_floor < blocking_floor) { + continue; // Light is blocked by solid ground above + } + float dx = (static_cast(tile_x) + 0.5f) - (static_cast(light.x) + 0.5f); float dy = (static_cast(tile_y) + 0.5f) - (static_cast(light.y) + 0.5f); float dist_sq = dx*dx + dy*dy; diff --git a/ImguiMapEditor/Rendering/Light/LightManager.h b/ImguiMapEditor/Rendering/Light/LightManager.h index 204688e..0c2334b 100644 --- a/ImguiMapEditor/Rendering/Light/LightManager.h +++ b/ImguiMapEditor/Rendering/Light/LightManager.h @@ -32,13 +32,15 @@ class LightManager { * @param start_floor First floor to gather lights from (highest Z) * @param end_floor Last floor to gather lights from (lowest Z) * When start_floor == end_floor, only that floor is used + * @param injected_lights Optional extra lights to add (e.g., player light in preview) */ void render(const MapEditor::Domain::ChunkedMap& map, int viewport_width, int viewport_height, float camera_x, float camera_y, float zoom, int current_floor, int start_floor, int end_floor, - const MapEditor::Domain::LightConfig& config); + const MapEditor::Domain::LightConfig& config, + const std::vector* injected_lights = nullptr); /** * Invalidate light cache for a specific tile position. @@ -54,8 +56,10 @@ class LightManager { private: void computeChunkLight(CachedLightGrid& grid, const std::vector& lights, + const GroundBrightness& ground_brightness, const MapEditor::Domain::LightConfig& config, - int32_t chunk_x, int32_t chunk_y); + int32_t chunk_x, int32_t chunk_y, + int16_t current_floor); Services::ClientDataService* client_data_; diff --git a/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp b/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp index 18ef436..893cfa9 100644 --- a/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp +++ b/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp @@ -225,19 +225,37 @@ void IngamePreviewRenderer::render(const Domain::ChunkedMap &map, if (lighting_enabled && light_manager_ && client_data_) { Domain::LightConfig config; config.enabled = true; - config.ambient_level = - static_cast(view_settings->preview_ambient_light); - config.ambient_color = 215; - - if (config.ambient_level != last_ambient_light_) { + config.global_light.intensity = view_settings->preview_server_light_intensity; + config.global_light.color = view_settings->preview_server_light_color; + config.client_slider = static_cast(view_settings->preview_ambient_light); + config.camera_floor = floor; + config.ambient_color = config.global_light.color; + config.ambient_level = config.global_light.intensity; + + // Auto-invalidate if config changed + uint32_t config_hash = static_cast(config.global_light.intensity) | + (static_cast(config.global_light.color) << 8) | + (static_cast(config.client_slider) << 16); + if (config_hash != last_config_hash_) { light_manager_->invalidateAll(); - last_ambient_light_ = config.ambient_level; + last_config_hash_ = config_hash; } + // Player minimum light at center tile (preview only) + // Matches OTClient local player: intensity=2, color=215 (white) + std::vector player_lights; + player_lights.push_back(Domain::LightSource{ + .x = static_cast(camera_x), + .y = static_cast(camera_y), + .color = static_cast(215), + .intensity = static_cast(2), + .source_floor = static_cast(floor) + }); + light_manager_->render(map, viewport_width, viewport_height, camera_x, camera_y, zoom, floor, - render_start_z, render_end_z, // Floor range from fading calculation - config); + render_start_z, render_end_z, + config, &player_lights); } } diff --git a/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h b/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h index 30497a8..3ebcd4f 100644 --- a/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h +++ b/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h @@ -68,6 +68,7 @@ class IngamePreviewRenderer { // Light system (independent from MapRenderer) std::unique_ptr light_manager_; uint8_t last_ambient_light_ = 255; + uint32_t last_config_hash_ = 0; // Fading state int last_player_z_ = -1; diff --git a/ImguiMapEditor/Rendering/Passes/LightingPass.cpp b/ImguiMapEditor/Rendering/Passes/LightingPass.cpp index e2c1aba..f8f8213 100644 --- a/ImguiMapEditor/Rendering/Passes/LightingPass.cpp +++ b/ImguiMapEditor/Rendering/Passes/LightingPass.cpp @@ -18,17 +18,24 @@ void LightingPass::render(const RenderContext &context) { return; } + // Build LightConfig from ViewSettings (new server light + client slider) Domain::LightConfig config; config.enabled = true; - config.ambient_level = - static_cast(context.view_settings->map_ambient_light); - config.ambient_color = 215; // Default white-ish (from MapRenderer.cpp) - - // Auto-invalidate if ambient light changes - // Note: RenderState tracking logic moved here - if (config.ambient_level != context.state.last_ambient_light) { + config.global_light.intensity = context.view_settings->server_light_intensity; + config.global_light.color = context.view_settings->server_light_color; + config.client_slider = static_cast(context.view_settings->map_ambient_light); + config.camera_floor = context.current_floor; + // Legacy fields (kept for backward compat) + config.ambient_color = config.global_light.color; + config.ambient_level = config.global_light.intensity; + + // Auto-invalidate if server light or slider changes + uint32_t config_hash = static_cast(config.global_light.intensity) | + (static_cast(config.global_light.color) << 8) | + (static_cast(config.client_slider) << 16); + if (config_hash != context.state.last_config_hash) { context.state.light_manager->invalidateAll(); - context.state.last_ambient_light = config.ambient_level; + context.state.last_config_hash = config_hash; } // Calculate floor range based on show_all_floors setting diff --git a/ImguiMapEditor/Services/ClientDataService.cpp b/ImguiMapEditor/Services/ClientDataService.cpp index 92ac636..1b06322 100644 --- a/ImguiMapEditor/Services/ClientDataService.cpp +++ b/ImguiMapEditor/Services/ClientDataService.cpp @@ -349,6 +349,9 @@ void ClientDataService::mergeOtbWithDat( // Translucency — DAT flag already false for pre-10.00 items merged.is_translucent = dat->is_translucent; + // Lens help — items with value > 0 are see-through for light propagation + merged.lens_help = dat->lens_help; + // Ground speed from DAT if (dat->is_ground && dat->ground_speed > 0) { merged.speed = dat->ground_speed; diff --git a/ImguiMapEditor/Services/ViewSettings.cpp b/ImguiMapEditor/Services/ViewSettings.cpp index 0109ceb..a6f3500 100644 --- a/ImguiMapEditor/Services/ViewSettings.cpp +++ b/ImguiMapEditor/Services/ViewSettings.cpp @@ -32,10 +32,14 @@ void ViewSettings::loadFromConfig(const ConfigService &config) { // Lighting Settings map_lighting_enabled = config.get("view.map_lighting_enabled", false); - map_ambient_light = config.get("view.map_ambient_light", 255); + map_ambient_light = config.get("view.map_ambient_light", 0); preview_lighting_enabled = config.get("view.preview_lighting_enabled", false); - preview_ambient_light = config.get("view.preview_ambient_light", 255); + preview_ambient_light = config.get("view.preview_ambient_light", 0); + server_light_intensity = static_cast(config.get("view.server_light_intensity", 200)); + server_light_color = static_cast(config.get("view.server_light_color", 215)); + preview_server_light_intensity = static_cast(config.get("view.preview_server_light_intensity", 200)); + preview_server_light_color = static_cast(config.get("view.preview_server_light_color", 215)); show_minimap_window = config.get("view.show_minimap_window", false); show_browse_tile = config.get("view.show_browse_tile", false); @@ -78,6 +82,10 @@ void ViewSettings::saveToConfig(ConfigService &config) const { config.set("view.map_ambient_light", map_ambient_light); config.set("view.preview_lighting_enabled", preview_lighting_enabled); config.set("view.preview_ambient_light", preview_ambient_light); + config.set("view.server_light_intensity", static_cast(server_light_intensity)); + config.set("view.server_light_color", static_cast(server_light_color)); + config.set("view.preview_server_light_intensity", static_cast(preview_server_light_intensity)); + config.set("view.preview_server_light_color", static_cast(preview_server_light_color)); config.set("view.show_minimap_window", show_minimap_window); config.set("view.show_browse_tile", show_browse_tile); diff --git a/ImguiMapEditor/Services/ViewSettings.h b/ImguiMapEditor/Services/ViewSettings.h index be22e5e..4c6fe23 100644 --- a/ImguiMapEditor/Services/ViewSettings.h +++ b/ImguiMapEditor/Services/ViewSettings.h @@ -44,9 +44,13 @@ struct ViewSettings { // === Lighting Settings === bool map_lighting_enabled = false; // Enable lighting in main map viewport - int map_ambient_light = 128; // 0 = dark, 255 = full bright + int map_ambient_light = 0; // Client minimum ambient (0-255), acts as floor bool preview_lighting_enabled = false; // Enable lighting in ingame preview - int preview_ambient_light = 128; // 0 = dark, 255 = full bright + int preview_ambient_light = 0; // Client minimum ambient for preview (0-255) + uint8_t server_light_intensity = 200; // Server global light intensity (0-255), default bright + uint8_t server_light_color = 215; // Server global light color (8-bit palette index), default white + uint8_t preview_server_light_intensity = 200; // Server light for preview + uint8_t preview_server_light_color = 215; // Server light color for preview // === Placeholders (menu only, no rendering yet) === bool show_minimap_window = false; diff --git a/ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp b/ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp index 21d641b..fa34a7f 100644 --- a/ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp +++ b/ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp @@ -74,10 +74,28 @@ void IngameBoxWindow::render(Domain::ChunkedMap* map, // Ambient slider (only when lighting is enabled) if (settings.preview_lighting_enabled) { ImGui::SameLine(); - ImGui::SetNextItemWidth(80); + ImGui::SetNextItemWidth(70); + int intensity = static_cast(settings.preview_server_light_intensity); + if (ImGui::SliderInt("##intensity", &intensity, 0, 255)) { + settings.preview_server_light_intensity = static_cast(intensity); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Server Light Intensity"); + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(50); + int color = static_cast(settings.preview_server_light_color); + if (ImGui::SliderInt("##color", &color, 0, 215)) { + settings.preview_server_light_color = static_cast(color); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Server Light Color"); + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(70); ImGui::SliderInt("##ambient", &settings.preview_ambient_light, 0, 255); if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Ambient Light Level"); + ImGui::SetTooltip("Min Ambient"); } } From 5467baa585c3ba238805b4fc231890c364dc05ae Mon Sep 17 00:00:00 2001 From: glaszczkoldre Date: Sun, 31 May 2026 19:56:23 +0200 Subject: [PATCH 2/6] refactor(rendering): replace local ground blocking with viewport-level grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved ground-blocking logic from chunk-specific tracking to a viewport-level grid to support consistent light occlusion across multiple floors. Refactored LightGatherer and LightManager to use the new `ViewportGroundBlocking` struct instead of `GroundBrightness`. - `ImguiMapEditor/Rendering/Light/LightGatherer.cpp` — removed `GroundBrightness` tracking, updated `gatherForChunkMultiFloor` and `gatherLightsFromNeighborChunk` to use `ViewportGroundBlocking` - `ImguiMapEditor/Rendering/Light/LightGatherer.h` — replaced `GroundBrightness` with `ViewportGroundBlocking` struct - `ImguiMapEditor/Rendering/Light/LightManager.cpp` — initialized `viewport_blocking_`, updated `render` and `computeChunkLight` signature and logic to use the new blocking grid - `ImguiMapEditor/Rendering/Light/LightManager.h` — added `viewport_blocking_` member, updated `computeChunkLight` signature --- .../Rendering/Light/LightGatherer.cpp | 81 ++++++++++--------- .../Rendering/Light/LightGatherer.h | 72 ++++++++++++----- .../Rendering/Light/LightManager.cpp | 49 +++++++---- ImguiMapEditor/Rendering/Light/LightManager.h | 6 +- 4 files changed, 135 insertions(+), 73 deletions(-) diff --git a/ImguiMapEditor/Rendering/Light/LightGatherer.cpp b/ImguiMapEditor/Rendering/Light/LightGatherer.cpp index 79650a1..c55cb44 100644 --- a/ImguiMapEditor/Rendering/Light/LightGatherer.cpp +++ b/ImguiMapEditor/Rendering/Light/LightGatherer.cpp @@ -13,11 +13,9 @@ namespace Rendering { void LightGatherer::clear() { lights_.clear(); - ground_brightness_ = GroundBrightness{}; } void LightGatherer::addLight(int32_t x, int32_t y, uint8_t color, uint8_t intensity, int16_t floor) { - // Dedup: merge consecutive lights with same pos, color, and floor if (!lights_.empty()) { auto& prev = lights_.back(); if (prev.x == x && prev.y == y && prev.color == color && prev.source_floor == floor) { @@ -64,7 +62,6 @@ void LightGatherer::gatherForChunk( int32_t tile_x = tile->getX(); int32_t tile_y = tile->getY(); - // Check ground item for light const Domain::Item* ground = tile->getGround(); if (ground) { const Domain::ItemType* item_type = client_data->getItemTypeByServerId(ground->getServerId()); @@ -73,7 +70,6 @@ void LightGatherer::gatherForChunk( } } - // Check all items on the tile for (const auto& item_ptr : tile->getItems()) { if (!item_ptr) continue; const Domain::ItemType* item_type = client_data->getItemTypeByServerId(item_ptr->getServerId()); @@ -82,7 +78,6 @@ void LightGatherer::gatherForChunk( } } - // Check creature for light const Domain::Creature* creature = tile->getCreature(); if (creature) { const Domain::CreatureType* creature_type = client_data->getCreatureType(creature->name); @@ -107,9 +102,6 @@ void LightGatherer::gatherForChunkMultiFloor( constexpr int GROUND_LAYER = 7; - // Reset ground brightness for the target chunk - ground_brightness_ = GroundBrightness{}; - for (int16_t floor = start_floor; floor >= end_floor; --floor) { int32_t floor_offset = 0; if (floor <= GROUND_LAYER) { @@ -118,30 +110,66 @@ void LightGatherer::gatherForChunkMultiFloor( for (int dy = -1; dy <= 1; ++dy) { for (int dx = -1; dx <= 1; ++dx) { - bool is_target = (dx == 0 && dy == 0); gatherLightsFromNeighborChunk( map, chunk_x + dx, chunk_y + dy, - client_data, floor, floor_offset, is_target); + client_data, floor, floor_offset); } } } } +void LightGatherer::registerGroundBlockingForChunk( + const Domain::ChunkedMap& map, + int32_t chunk_x, int32_t chunk_y, + Services::ClientDataService* client_data, + int16_t floor, + int32_t floor_offset, + ViewportGroundBlocking& viewport_blocking) +{ + if (!client_data) return; + + int32_t tile_start_x = chunk_x * Domain::Chunk::SIZE; + int32_t tile_start_y = chunk_y * Domain::Chunk::SIZE; + int32_t tile_end_x = tile_start_x + Domain::Chunk::SIZE; + int32_t tile_end_y = tile_start_y + Domain::Chunk::SIZE; + + std::vector chunks; + map.getVisibleChunks(tile_start_x, tile_start_y, tile_end_x - 1, tile_end_y - 1, floor, chunks); + + for (Domain::Chunk* chunk : chunks) { + if (!chunk) continue; + + chunk->forEachTile([&](const Domain::Tile* tile) { + if (!tile || tile->getZ() != floor) return; + + const Domain::Item* ground = tile->getGround(); + if (!ground) return; + + const Domain::ItemType* ground_type = client_data->getItemTypeByServerId(ground->getServerId()); + bool is_ground_tile = ground_type && + (ground_type->is_ground || ground_type->group == Domain::ItemGroup::Ground); + if (is_ground_tile && ground_type && !ground_type->is_translucent) { + int32_t tile_x = tile->getX(); + int32_t tile_y = tile->getY(); + int32_t adjusted_x = tile_x - floor_offset; + int32_t adjusted_y = tile_y - floor_offset; + viewport_blocking.setBlockingFloor(adjusted_x, adjusted_y, floor); + } + }); + } +} + void LightGatherer::gatherLightsFromNeighborChunk( const Domain::ChunkedMap& map, int32_t target_cx, int32_t target_cy, Services::ClientDataService* client_data, - int16_t floor, int32_t floor_offset, - bool is_target_chunk) + int16_t floor, int32_t floor_offset) { int32_t tile_start_x = target_cx * Domain::Chunk::SIZE; int32_t tile_start_y = target_cy * Domain::Chunk::SIZE; int32_t tile_end_x = tile_start_x + Domain::Chunk::SIZE; int32_t tile_end_y = tile_start_y + Domain::Chunk::SIZE; - int32_t target_chunk_start_x = target_cx * Domain::Chunk::SIZE; - int32_t target_chunk_start_y = target_cy * Domain::Chunk::SIZE; - constexpr int GROUND_LAYER = 7; std::vector chunks; @@ -159,24 +187,6 @@ void LightGatherer::gatherLightsFromNeighborChunk( int32_t adjusted_x = tile_x - floor_offset; int32_t adjusted_y = tile_y - floor_offset; - // Track ground solidity for blocking (only for the target chunk) - if (is_target_chunk) { - const Domain::Item* ground = tile->getGround(); - if (ground) { - const Domain::ItemType* ground_type = client_data->getItemTypeByServerId(ground->getServerId()); - if (ground_type && ground_type->is_ground && !ground_type->is_translucent) { - int32_t local_x = tile_x - target_chunk_start_x; - int32_t local_y = tile_y - target_chunk_start_y; - if (local_x >= 0 && local_x < 32 && local_y >= 0 && local_y < 32) { - int idx = local_y * 32 + local_x; - if (ground_brightness_.blocking_floor[idx] < 0 || floor < ground_brightness_.blocking_floor[idx]) { - ground_brightness_.blocking_floor[idx] = floor; - } - } - } - } - } - // Add light sources from items auto addLightFromItem = [&](const Domain::Item* item) { if (!item) return; @@ -192,7 +202,7 @@ void LightGatherer::gatherLightsFromNeighborChunk( addLightFromItem(item_ptr.get()); } - // Check creature for light (Phase 4) + // Check creature for light const Domain::Creature* creature = tile->getCreature(); if (creature) { const Domain::CreatureType* creature_type = client_data->getCreatureType(creature->name); @@ -201,7 +211,7 @@ void LightGatherer::gatherLightsFromNeighborChunk( } } - // Translucent ground propagation (Phase 5) + // Translucent ground propagation (z=7 → z=8) if (floor == GROUND_LAYER) { bool has_translucent = false; @@ -224,7 +234,6 @@ void LightGatherer::gatherLightsFromNeighborChunk( } if (has_translucent) { - // Emit dim white light at z=8 (no isometric offset for underground) addLight(tile_x, tile_y, 215, 1, static_cast(GROUND_LAYER + 1)); } } diff --git a/ImguiMapEditor/Rendering/Light/LightGatherer.h b/ImguiMapEditor/Rendering/Light/LightGatherer.h index 93782cf..a941ef7 100644 --- a/ImguiMapEditor/Rendering/Light/LightGatherer.h +++ b/ImguiMapEditor/Rendering/Light/LightGatherer.h @@ -14,17 +14,46 @@ namespace Services { namespace Rendering { /** - * Per-tile ground blocking data for a chunk. - * Records the highest floor (lowest Z) with solid ground above each tile. - * Lights from floors above blocking_floor are blocked. + * Viewport-level ground blocking data. + * For each tile position in the viewport, records the nearest floor (lowest Z) + * with solid ground. Lights from floors BELOW (higher Z) the blocking floor are blocked. + * + * This is the equivalent of RME's TileLight::start mechanism, but using + * floor numbers instead of light indices. */ -struct GroundBrightness { - // For each tile (32x32), the floor with solid ground that blocks lights from above. - // Value of -1 means no blocking (all lights pass through). - std::array blocking_floor; +struct ViewportGroundBlocking { + std::vector blocking_floor; // -1 = no blocking, >=0 = floor with solid ground + int width = 0; + int height = 0; + int origin_x = 0; // tile-space origin (start_x from LightManager) + int origin_y = 0; + + void init(int ox, int oy, int w, int h) { + origin_x = ox; + origin_y = oy; + width = w; + height = h; + blocking_floor.assign(w * h, -1); + } - GroundBrightness() { - blocking_floor.fill(-1); + // Get blocking floor for a tile at absolute tile coords (tx, ty) + int16_t getBlockingFloor(int tx, int ty) const { + int lx = tx - origin_x; + int ly = ty - origin_y; + if (lx < 0 || lx >= width || ly < 0 || ly >= height) return -1; + return blocking_floor[ly * width + lx]; + } + + // Set blocking floor for a tile at absolute tile coords (tx, ty) + void setBlockingFloor(int tx, int ty, int16_t floor) { + int lx = tx - origin_x; + int ly = ty - origin_y; + if (lx < 0 || lx >= width || ly < 0 || ly >= height) return; + int idx = ly * width + lx; + // Take the nearest floor (lowest Z) with solid ground + if (blocking_floor[idx] < 0 || floor < blocking_floor[idx]) { + blocking_floor[idx] = floor; + } } }; @@ -53,8 +82,6 @@ class LightGatherer { /** * Gather all light sources from multiple floors for a specific chunk. - * Applies isometric offset to light positions based on floor difference. - * Also populates ground_brightness_ for light blocking. */ void gatherForChunkMultiFloor( const MapEditor::Domain::ChunkedMap& map, @@ -63,6 +90,19 @@ class LightGatherer { int16_t start_floor, int16_t end_floor); + /** + * Register ground blocking for viewport tiles from a specific chunk/floor. + * Does NOT collect lights — only populates the blocking grid. + * Used in the pre-pass to build complete blocking data before light computation. + */ + void registerGroundBlockingForChunk( + const MapEditor::Domain::ChunkedMap& map, + int32_t chunk_x, int32_t chunk_y, + Services::ClientDataService* client_data, + int16_t floor, + int32_t floor_offset, + ViewportGroundBlocking& viewport_blocking); + /** * Get the collected light sources. */ @@ -72,12 +112,6 @@ class LightGatherer { * Get number of light sources collected. */ size_t getLightCount() const { return lights_.size(); } - - /** - * Get the ground blocking data for the target chunk. - * Only valid after gatherForChunkMultiFloor() has been called. - */ - const GroundBrightness& getGroundBrightness() const { return ground_brightness_; } private: /** @@ -87,8 +121,7 @@ class LightGatherer { const MapEditor::Domain::ChunkedMap& map, int32_t target_cx, int32_t target_cy, Services::ClientDataService* client_data, - int16_t floor, int32_t floor_offset, - bool is_target_chunk); + int16_t floor, int32_t floor_offset); /** * Add a light source with deduplication. @@ -97,7 +130,6 @@ class LightGatherer { void addLight(int32_t x, int32_t y, uint8_t color, uint8_t intensity, int16_t floor); std::vector lights_; - GroundBrightness ground_brightness_; }; } // namespace Rendering diff --git a/ImguiMapEditor/Rendering/Light/LightManager.cpp b/ImguiMapEditor/Rendering/Light/LightManager.cpp index 42c355a..011d3a8 100644 --- a/ImguiMapEditor/Rendering/Light/LightManager.cpp +++ b/ImguiMapEditor/Rendering/Light/LightManager.cpp @@ -131,18 +131,39 @@ void LightManager::render(const Domain::ChunkedMap& map, last_config_ = config; force_update_ = false; - // 2. Prepare buffer + // 2. Prepare buffer and viewport blocking grid size_t required_size = width_tiles * height_tiles * 4; if (viewport_buffer_.size() < required_size) { viewport_buffer_.resize(required_size); } - // 3. Iterate over chunks in the view - // To optimized cache usage, iterate by chunks visible + // Calculate chunk bounds int chunk_start_x = start_x >> 5; int chunk_start_y = start_y >> 5; int chunk_end_x = end_x >> 5; int chunk_end_y = end_y >> 5; + + // Initialize viewport-level ground blocking (RME-style global blocking) + viewport_blocking_.init(start_x, start_y, width_tiles, height_tiles); + + // PRE-PASS: Register ground blocking for ALL visible chunks/floors BEFORE computing lights. + // This ensures the blocking grid is complete before any light computation, + // preventing chunk seams where earlier chunks have incomplete blocking data. + constexpr int GROUND_LAYER = 7; + for (int16_t floor = static_cast(start_floor); floor >= static_cast(end_floor); --floor) { + int32_t floor_offset = 0; + if (floor <= GROUND_LAYER) { + floor_offset = GROUND_LAYER - floor; + } + for (int cy = chunk_start_y; cy <= chunk_end_y; ++cy) { + for (int cx = chunk_start_x; cx <= chunk_end_x; ++cx) { + gatherer_->registerGroundBlockingForChunk( + map, cx, cy, client_data_, floor, floor_offset, viewport_blocking_); + } + } + } + + // 3. Iterate over chunks in the view for (int cy = chunk_start_y; cy <= chunk_end_y; ++cy) { for (int cx = chunk_start_x; cx <= chunk_end_x; ++cx) { @@ -160,7 +181,7 @@ void LightManager::render(const Domain::ChunkedMap& map, static_cast(start_floor), static_cast(end_floor)); } else { - // Single floor mode + // Single floor mode — no blocking needed (only one floor) gatherer_->gatherForChunk(map, cx, cy, client_data_, static_cast(current_floor)); } @@ -168,9 +189,9 @@ void LightManager::render(const Domain::ChunkedMap& map, if (injected_lights && !injected_lights->empty()) { std::vector combined = gatherer_->getLights(); combined.insert(combined.end(), injected_lights->begin(), injected_lights->end()); - computeChunkLight(grid, combined, gatherer_->getGroundBrightness(), config, cx, cy, static_cast(current_floor)); + computeChunkLight(grid, combined, viewport_blocking_, config, cx, cy); } else { - computeChunkLight(grid, gatherer_->getLights(), gatherer_->getGroundBrightness(), config, cx, cy, static_cast(current_floor)); + computeChunkLight(grid, gatherer_->getLights(), viewport_blocking_, config, cx, cy); } grid.is_valid = true; @@ -224,10 +245,9 @@ void LightManager::render(const Domain::ChunkedMap& map, void LightManager::computeChunkLight(CachedLightGrid& grid, const std::vector& lights, - const GroundBrightness& ground_brightness, + const ViewportGroundBlocking& viewport_blocking, const Domain::LightConfig& config, - int32_t chunk_x, int32_t chunk_y, - int16_t current_floor) + int32_t chunk_x, int32_t chunk_y) { // Determine ambient based on camera floor (above-ground vs underground) float ambient_r, ambient_g, ambient_b; @@ -297,11 +317,12 @@ void LightManager::computeChunkLight(CachedLightGrid& grid, int tile_x = chunk_start_x + x; int tile_y = chunk_start_y + y; - // Ground blocking check: skip if solid ground exists between light and tile - int tile_idx = y * 32 + x; - int16_t blocking_floor = ground_brightness.blocking_floor[tile_idx]; - if (blocking_floor >= 0 && light.source_floor < blocking_floor) { - continue; // Light is blocked by solid ground above + // Ground blocking check: viewport-level blocking (RME-style) + // blocking_floor = nearest floor (lowest Z) with solid ground at this projected position. + // Lights from floors BELOW (higher Z) the blocking ground are blocked. + int16_t blocking_floor = viewport_blocking.getBlockingFloor(tile_x, tile_y); + if (blocking_floor >= 0 && light.source_floor > blocking_floor) { + continue; // Light is from a floor below the solid ground — blocked } float dx = (static_cast(tile_x) + 0.5f) - (static_cast(light.x) + 0.5f); diff --git a/ImguiMapEditor/Rendering/Light/LightManager.h b/ImguiMapEditor/Rendering/Light/LightManager.h index 0c2334b..65e3cbd 100644 --- a/ImguiMapEditor/Rendering/Light/LightManager.h +++ b/ImguiMapEditor/Rendering/Light/LightManager.h @@ -56,10 +56,9 @@ class LightManager { private: void computeChunkLight(CachedLightGrid& grid, const std::vector& lights, - const GroundBrightness& ground_brightness, + const ViewportGroundBlocking& viewport_blocking, const MapEditor::Domain::LightConfig& config, - int32_t chunk_x, int32_t chunk_y, - int16_t current_floor); + int32_t chunk_x, int32_t chunk_y); Services::ClientDataService* client_data_; @@ -67,6 +66,7 @@ class LightManager { std::unique_ptr texture_; std::unique_ptr overlay_; std::unique_ptr gatherer_; + ViewportGroundBlocking viewport_blocking_; std::vector viewport_buffer_; From a230e91489787ba316fd66758625552e25eb8991 Mon Sep 17 00:00:00 2001 From: glaszczkoldre Date: Sun, 31 May 2026 20:43:40 +0200 Subject: [PATCH 3/6] refactor(rendering): implement RME-style light visibility system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored lighting system to use a central `ViewportLightBuffer` and integrated floor visibility calculations based on user map clicks, mimicking RME behavior for improved accuracy. - `ImguiMapEditor/Rendering/Core/IRenderPass.h` — added `light_visibility_origin` to `RenderContext` - `ImguiMapEditor/Rendering/Light/LightGatherer.cpp` — implemented `ViewportLightBuffer` collection and RME-style light occlusion/propagation logic - `ImguiMapEditor/Rendering/Light/LightGatherer.h` — replaced `ViewportGroundBlocking` with `ViewportLightBuffer` for unified light/occlusion data - `ImguiMapEditor/Rendering/Light/LightManager.cpp` — integrated `gatherViewportLightBuffer` and `computeViewportLight` into light render loop - `ImguiMapEditor/Rendering/Light/LightManager.h` — added `renderClientVisible` with visibility origin support, removed light cache - `ImguiMapEditor/Rendering/Map/MapRenderer.cpp` — added `light_visibility_origin_` tracking and passed to render context - `ImguiMapEditor/Rendering/Map/MapRenderer.h` — added `light_visibility_origin_` storage - `ImguiMapEditor/Rendering/Passes/LightingPass.cpp` — switched to `renderClientVisible` using context visibility origin - `ImguiMapEditor/Rendering/Passes/LightingPass.h` — removed unused floor range tracking members - `ImguiMapEditor/Rendering/Visibility/FloorVisibilityCalculator.cpp` — refined `tileLimitsFloorsView` and `isLookPossible` using explicit item flags - `ImguiMapEditor/UI/Map/MapPanel.cpp` — pushed visibility origin to renderer - `ImguiMapEditor/UI/Map/MapPanelInput.cpp` — updated visibility origin on mouse clicks and floor changes - `ImguiMapEditor/UI/Map/MapPanelInput.h` — added `light_visibility_origin` tracking --- ImguiMapEditor/Rendering/Core/IRenderPass.h | 5 + .../Rendering/Light/LightGatherer.cpp | 346 +++++++++--------- .../Rendering/Light/LightGatherer.h | 111 ++---- .../Rendering/Light/LightManager.cpp | 308 +++++++--------- ImguiMapEditor/Rendering/Light/LightManager.h | 28 +- ImguiMapEditor/Rendering/Map/MapRenderer.cpp | 8 +- ImguiMapEditor/Rendering/Map/MapRenderer.h | 4 + .../Rendering/Passes/LightingPass.cpp | 19 +- .../Rendering/Passes/LightingPass.h | 4 - .../Visibility/FloorVisibilityCalculator.cpp | 147 ++++---- ImguiMapEditor/UI/Map/MapPanel.cpp | 1 + ImguiMapEditor/UI/Map/MapPanelInput.cpp | 16 + ImguiMapEditor/UI/Map/MapPanelInput.h | 7 + 13 files changed, 463 insertions(+), 541 deletions(-) diff --git a/ImguiMapEditor/Rendering/Core/IRenderPass.h b/ImguiMapEditor/Rendering/Core/IRenderPass.h index d7247f5..190ab3f 100644 --- a/ImguiMapEditor/Rendering/Core/IRenderPass.h +++ b/ImguiMapEditor/Rendering/Core/IRenderPass.h @@ -1,11 +1,13 @@ #pragma once #include "Domain/ChunkedMap.h" +#include "Domain/Position.h" #include "Rendering/Animation/AnimationTicks.h" #include "Rendering/Camera/ViewCamera.h" #include "Rendering/Frame/RenderState.h" #include "Rendering/Visibility/VisibleBounds.h" #include +#include namespace MapEditor { namespace Services { @@ -44,6 +46,9 @@ struct RenderContext { // View settings (optional, can be nullptr) const Services::ViewSettings *view_settings; + + // RME light-mode floor visibility origin, set from the last map click. + std::optional light_visibility_origin; }; /** diff --git a/ImguiMapEditor/Rendering/Light/LightGatherer.cpp b/ImguiMapEditor/Rendering/Light/LightGatherer.cpp index c55cb44..99ecef3 100644 --- a/ImguiMapEditor/Rendering/Light/LightGatherer.cpp +++ b/ImguiMapEditor/Rendering/Light/LightGatherer.cpp @@ -1,29 +1,76 @@ #include "LightGatherer.h" + +#include + +#include "Core/Config.h" #include "Services/ClientDataService.h" #include "Domain/Tile.h" #include "Domain/Item.h" #include "Domain/ItemType.h" #include "Domain/Creature.h" #include "Domain/CreatureType.h" -#include -#include namespace MapEditor { namespace Rendering { -void LightGatherer::clear() { - lights_.clear(); +namespace { + +constexpr int LIGHT_COLLECTION_MARGIN_TILES = 16; + +int32_t projectedFloorOffsetTiles(int16_t current_floor, int16_t tile_floor) { + if (tile_floor <= Config::Map::GROUND_LAYER) { + return Config::Map::GROUND_LAYER - tile_floor; + } + return current_floor - tile_floor; +} + +bool blocksLightFromBelow(const Domain::ItemType* item_type) { + if (!item_type) return false; + const bool is_ground_tile = + item_type->is_ground || item_type->group == Domain::ItemGroup::Ground; + return is_ground_tile && !item_type->is_translucent; +} + +bool carriesTranslucentLight(const Domain::Tile* tile, + Services::ClientDataService* client_data) { + if (!tile || !client_data) return false; + + const auto check_item = [&](const Domain::Item* item) { + if (!item) return false; + const Domain::ItemType* item_type = item->getType(); + if (!item_type) { + item_type = client_data->getItemTypeByServerId(item->getServerId()); + } + return item_type && + (item_type->is_translucent || item_type->lens_help > 0); + }; + + if (check_item(tile->getGround())) return true; + + for (const auto& item : tile->getItems()) { + if (check_item(item.get())) return true; + } + + return false; } -void LightGatherer::addLight(int32_t x, int32_t y, uint8_t color, uint8_t intensity, int16_t floor) { - if (!lights_.empty()) { - auto& prev = lights_.back(); +} // namespace + +void LightGatherer::addLight(ViewportLightBuffer& light_buffer, + int32_t x, int32_t y, + uint8_t color, uint8_t intensity, + int16_t floor) { + if (intensity == 0) return; + + if (!light_buffer.lights.empty()) { + auto& prev = light_buffer.lights.back(); if (prev.x == x && prev.y == y && prev.color == color && prev.source_floor == floor) { prev.intensity = std::max(prev.intensity, intensity); return; } } - lights_.emplace_back(Domain::LightSource{ + + light_buffer.lights.emplace_back(Domain::LightSource{ .x = x, .y = y, .color = color, @@ -32,213 +79,166 @@ void LightGatherer::addLight(int32_t x, int32_t y, uint8_t color, uint8_t intens }); } -void LightGatherer::gatherForChunk( - const Domain::ChunkedMap& map, - int32_t chunk_x, int32_t chunk_y, - Services::ClientDataService* client_data, - int16_t floor) -{ - if (!client_data) return; - - for (int dy = -1; dy <= 1; ++dy) { - for (int dx = -1; dx <= 1; ++dx) { - int32_t target_cx = chunk_x + dx; - int32_t target_cy = chunk_y + dy; - - int32_t tile_start_x = target_cx * Domain::Chunk::SIZE; - int32_t tile_start_y = target_cy * Domain::Chunk::SIZE; - int32_t tile_end_x = tile_start_x + Domain::Chunk::SIZE; - int32_t tile_end_y = tile_start_y + Domain::Chunk::SIZE; - - std::vector chunks; - map.getVisibleChunks(tile_start_x, tile_start_y, tile_end_x - 1, tile_end_y - 1, floor, chunks); - - for (Domain::Chunk* chunk : chunks) { - if (!chunk) continue; - - chunk->forEachTile([&](const Domain::Tile* tile) { - if (!tile || tile->getZ() != floor) return; - - int32_t tile_x = tile->getX(); - int32_t tile_y = tile->getY(); - - const Domain::Item* ground = tile->getGround(); - if (ground) { - const Domain::ItemType* item_type = client_data->getItemTypeByServerId(ground->getServerId()); - if (item_type && item_type->light_level > 0) { - addLight(tile_x, tile_y, item_type->light_color, item_type->light_level, floor); - } - } - - for (const auto& item_ptr : tile->getItems()) { - if (!item_ptr) continue; - const Domain::ItemType* item_type = client_data->getItemTypeByServerId(item_ptr->getServerId()); - if (item_type && item_type->light_level > 0) { - addLight(tile_x, tile_y, item_type->light_color, item_type->light_level, floor); - } - } - - const Domain::Creature* creature = tile->getCreature(); - if (creature) { - const Domain::CreatureType* creature_type = client_data->getCreatureType(creature->name); - if (creature_type && creature_type->light_level > 0) { - addLight(tile_x, tile_y, creature_type->light_color, creature_type->light_level, floor); - } - } - }); - } - } - } -} - -void LightGatherer::gatherForChunkMultiFloor( +void LightGatherer::gatherViewportLightBuffer( const Domain::ChunkedMap& map, - int32_t chunk_x, int32_t chunk_y, Services::ClientDataService* client_data, + int16_t current_floor, int16_t start_floor, - int16_t end_floor) + int16_t end_floor, + ViewportLightBuffer& light_buffer) { if (!client_data) return; - - constexpr int GROUND_LAYER = 7; - + + // RME processes start_z down to superend_z. The floor_light_start snapshot + // must be taken before collecting each floor's lights, in that same order. for (int16_t floor = start_floor; floor >= end_floor; --floor) { - int32_t floor_offset = 0; - if (floor <= GROUND_LAYER) { - floor_offset = GROUND_LAYER - floor; - } - - for (int dy = -1; dy <= 1; ++dy) { - for (int dx = -1; dx <= 1; ++dx) { - gatherLightsFromNeighborChunk( - map, chunk_x + dx, chunk_y + dy, - client_data, floor, floor_offset); - } + const uint32_t floor_light_start = + static_cast(light_buffer.lights.size()); + + if (floor >= current_floor) { + registerGroundOcclusionForViewport( + map, client_data, current_floor, floor, floor_light_start, light_buffer); } + + gatherLightsForViewportFloor( + map, client_data, current_floor, floor, light_buffer); } } -void LightGatherer::registerGroundBlockingForChunk( +void LightGatherer::registerGroundOcclusionForViewport( const Domain::ChunkedMap& map, - int32_t chunk_x, int32_t chunk_y, Services::ClientDataService* client_data, + int16_t current_floor, int16_t floor, - int32_t floor_offset, - ViewportGroundBlocking& viewport_blocking) + uint32_t floor_light_start, + ViewportLightBuffer& light_buffer) { - if (!client_data) return; - - int32_t tile_start_x = chunk_x * Domain::Chunk::SIZE; - int32_t tile_start_y = chunk_y * Domain::Chunk::SIZE; - int32_t tile_end_x = tile_start_x + Domain::Chunk::SIZE; - int32_t tile_end_y = tile_start_y + Domain::Chunk::SIZE; - + const int32_t floor_offset = projectedFloorOffsetTiles(current_floor, floor); + const int32_t min_x = light_buffer.origin_x + floor_offset; + const int32_t min_y = light_buffer.origin_y + floor_offset; + const int32_t max_x = light_buffer.origin_x + light_buffer.width - 1 + floor_offset; + const int32_t max_y = light_buffer.origin_y + light_buffer.height - 1 + floor_offset; + std::vector chunks; - map.getVisibleChunks(tile_start_x, tile_start_y, tile_end_x - 1, tile_end_y - 1, floor, chunks); - + map.getVisibleChunks(min_x, min_y, max_x, max_y, floor, chunks); + for (Domain::Chunk* chunk : chunks) { if (!chunk) continue; - + chunk->forEachTile([&](const Domain::Tile* tile) { if (!tile || tile->getZ() != floor) return; - + const Domain::Item* ground = tile->getGround(); if (!ground) return; - - const Domain::ItemType* ground_type = client_data->getItemTypeByServerId(ground->getServerId()); - bool is_ground_tile = ground_type && - (ground_type->is_ground || ground_type->group == Domain::ItemGroup::Ground); - if (is_ground_tile && ground_type && !ground_type->is_translucent) { - int32_t tile_x = tile->getX(); - int32_t tile_y = tile->getY(); - int32_t adjusted_x = tile_x - floor_offset; - int32_t adjusted_y = tile_y - floor_offset; - viewport_blocking.setBlockingFloor(adjusted_x, adjusted_y, floor); + + const Domain::ItemType* ground_type = ground->getType(); + if (!ground_type) { + ground_type = client_data->getItemTypeByServerId(ground->getServerId()); } + if (!blocksLightFromBelow(ground_type)) return; + + const int32_t projected_x = tile->getX() - floor_offset; + const int32_t projected_y = tile->getY() - floor_offset; + light_buffer.setTileStart(projected_x, projected_y, floor_light_start); }); } } -void LightGatherer::gatherLightsFromNeighborChunk( +void LightGatherer::gatherLightsForViewportFloor( const Domain::ChunkedMap& map, - int32_t target_cx, int32_t target_cy, Services::ClientDataService* client_data, - int16_t floor, int32_t floor_offset) + int16_t current_floor, + int16_t floor, + ViewportLightBuffer& light_buffer) { - int32_t tile_start_x = target_cx * Domain::Chunk::SIZE; - int32_t tile_start_y = target_cy * Domain::Chunk::SIZE; - int32_t tile_end_x = tile_start_x + Domain::Chunk::SIZE; - int32_t tile_end_y = tile_start_y + Domain::Chunk::SIZE; - - constexpr int GROUND_LAYER = 7; - + const int32_t floor_offset = projectedFloorOffsetTiles(current_floor, floor); + const int32_t min_x = + light_buffer.origin_x + floor_offset - LIGHT_COLLECTION_MARGIN_TILES; + const int32_t min_y = + light_buffer.origin_y + floor_offset - LIGHT_COLLECTION_MARGIN_TILES; + const int32_t max_x = + light_buffer.origin_x + light_buffer.width - 1 + floor_offset + + LIGHT_COLLECTION_MARGIN_TILES; + const int32_t max_y = + light_buffer.origin_y + light_buffer.height - 1 + floor_offset + + LIGHT_COLLECTION_MARGIN_TILES; + std::vector chunks; - map.getVisibleChunks(tile_start_x, tile_start_y, tile_end_x - 1, tile_end_y - 1, floor, chunks); - + map.getVisibleChunks(min_x, min_y, max_x, max_y, floor, chunks); + + const auto add_item_light = [&](const Domain::Item* item, + int32_t projected_x, + int32_t projected_y) { + if (!item) return; + const Domain::ItemType* item_type = item->getType(); + if (!item_type) { + item_type = client_data->getItemTypeByServerId(item->getServerId()); + } + if (item_type && item_type->light_level > 0) { + addLight(light_buffer, projected_x, projected_y, + item_type->light_color, item_type->light_level, floor); + } + }; + for (Domain::Chunk* chunk : chunks) { if (!chunk) continue; - + chunk->forEachTile([&](const Domain::Tile* tile) { if (!tile || tile->getZ() != floor) return; - - int32_t tile_x = tile->getX(); - int32_t tile_y = tile->getY(); - - int32_t adjusted_x = tile_x - floor_offset; - int32_t adjusted_y = tile_y - floor_offset; - - // Add light sources from items - auto addLightFromItem = [&](const Domain::Item* item) { - if (!item) return; - const Domain::ItemType* item_type = client_data->getItemTypeByServerId(item->getServerId()); - if (item_type && item_type->light_level > 0) { - addLight(adjusted_x, adjusted_y, item_type->light_color, item_type->light_level, floor); - } - }; - - addLightFromItem(tile->getGround()); - - for (const auto& item_ptr : tile->getItems()) { - addLightFromItem(item_ptr.get()); + + const int32_t projected_x = tile->getX() - floor_offset; + const int32_t projected_y = tile->getY() - floor_offset; + + add_item_light(tile->getGround(), projected_x, projected_y); + + for (const auto& item : tile->getItems()) { + add_item_light(item.get(), projected_x, projected_y); } - - // Check creature for light + const Domain::Creature* creature = tile->getCreature(); if (creature) { - const Domain::CreatureType* creature_type = client_data->getCreatureType(creature->name); + const Domain::CreatureType* creature_type = + client_data->getCreatureType(creature->name); if (creature_type && creature_type->light_level > 0) { - addLight(adjusted_x, adjusted_y, creature_type->light_color, creature_type->light_level, floor); - } - } - - // Translucent ground propagation (z=7 → z=8) - if (floor == GROUND_LAYER) { - bool has_translucent = false; - - const Domain::Item* ground_check = tile->getGround(); - if (ground_check) { - const Domain::ItemType* ground_type = client_data->getItemTypeByServerId(ground_check->getServerId()); - if (ground_type && (ground_type->is_translucent || ground_type->lens_help > 0)) { - has_translucent = true; - } - } - - if (!has_translucent) { - for (const auto& item_ptr : tile->getItems()) { - const Domain::ItemType* item_type = client_data->getItemTypeByServerId(item_ptr->getServerId()); - if (item_type && (item_type->is_translucent || item_type->lens_help > 0)) { - has_translucent = true; - break; - } - } - } - - if (has_translucent) { - addLight(tile_x, tile_y, 215, 1, static_cast(GROUND_LAYER + 1)); + addLight(light_buffer, projected_x, projected_y, + creature_type->light_color, + creature_type->light_level, floor); } } }); } + + if (floor == Config::Map::GROUND_LAYER + 1) { + const int16_t above_floor = Config::Map::GROUND_LAYER; + const int32_t above_offset = projectedFloorOffsetTiles(current_floor, above_floor); + const int32_t above_min_x = + light_buffer.origin_x + above_offset - LIGHT_COLLECTION_MARGIN_TILES; + const int32_t above_min_y = + light_buffer.origin_y + above_offset - LIGHT_COLLECTION_MARGIN_TILES; + const int32_t above_max_x = + light_buffer.origin_x + light_buffer.width - 1 + above_offset + + LIGHT_COLLECTION_MARGIN_TILES; + const int32_t above_max_y = + light_buffer.origin_y + light_buffer.height - 1 + above_offset + + LIGHT_COLLECTION_MARGIN_TILES; + + std::vector above_chunks; + map.getVisibleChunks(above_min_x, above_min_y, above_max_x, above_max_y, + above_floor, above_chunks); + + for (Domain::Chunk* chunk : above_chunks) { + if (!chunk) continue; + + chunk->forEachTile([&](const Domain::Tile* tile) { + if (!tile || tile->getZ() != above_floor) return; + if (!carriesTranslucentLight(tile, client_data)) return; + + const int32_t projected_x = tile->getX() - floor_offset; + const int32_t projected_y = tile->getY() - floor_offset; + addLight(light_buffer, projected_x, projected_y, 215, 1, floor); + }); + } + } } } // namespace Rendering diff --git a/ImguiMapEditor/Rendering/Light/LightGatherer.h b/ImguiMapEditor/Rendering/Light/LightGatherer.h index a941ef7..1ead3db 100644 --- a/ImguiMapEditor/Rendering/Light/LightGatherer.h +++ b/ImguiMapEditor/Rendering/Light/LightGatherer.h @@ -2,7 +2,6 @@ #include #include -#include #include "Domain/ChunkedMap.h" #include "Domain/LightTypes.h" namespace MapEditor { @@ -14,18 +13,19 @@ namespace Services { namespace Rendering { /** - * Viewport-level ground blocking data. - * For each tile position in the viewport, records the nearest floor (lowest Z) - * with solid ground. Lights from floors BELOW (higher Z) the blocking floor are blocked. - * - * This is the equivalent of RME's TileLight::start mechanism, but using - * floor numbers instead of light indices. + * RME-style viewport light buffer. + * + * All lights across all processed floors are appended to one vector in + * top-to-bottom floor order. Each viewport tile stores the first light index + * allowed to affect it; lower indices were collected from floors above a solid + * ground tile and are blocked for that projected tile position. */ -struct ViewportGroundBlocking { - std::vector blocking_floor; // -1 = no blocking, >=0 = floor with solid ground +struct ViewportLightBuffer { + std::vector lights; + std::vector tile_start; int width = 0; int height = 0; - int origin_x = 0; // tile-space origin (start_x from LightManager) + int origin_x = 0; int origin_y = 0; void init(int ox, int oy, int w, int h) { @@ -33,27 +33,22 @@ struct ViewportGroundBlocking { origin_y = oy; width = w; height = h; - blocking_floor.assign(w * h, -1); + lights.clear(); + tile_start.assign(w * h, 0); } - // Get blocking floor for a tile at absolute tile coords (tx, ty) - int16_t getBlockingFloor(int tx, int ty) const { + uint32_t getTileStart(int tx, int ty) const { int lx = tx - origin_x; int ly = ty - origin_y; - if (lx < 0 || lx >= width || ly < 0 || ly >= height) return -1; - return blocking_floor[ly * width + lx]; + if (lx < 0 || lx >= width || ly < 0 || ly >= height) return 0; + return tile_start[ly * width + lx]; } - // Set blocking floor for a tile at absolute tile coords (tx, ty) - void setBlockingFloor(int tx, int ty, int16_t floor) { + void setTileStart(int tx, int ty, uint32_t start) { int lx = tx - origin_x; int ly = ty - origin_y; if (lx < 0 || lx >= width || ly < 0 || ly >= height) return; - int idx = ly * width + lx; - // Take the nearest floor (lowest Z) with solid ground - if (blocking_floor[idx] < 0 || floor < blocking_floor[idx]) { - blocking_floor[idx] = floor; - } + tile_start[ly * width + lx] = start; } }; @@ -66,70 +61,36 @@ struct ViewportGroundBlocking { class LightGatherer { public: /** - * Clear all collected lights for a new frame. - */ - void clear(); - - /** - * Gather all light sources relevant to a specific chunk. - * scans the target chunk AND its 8 neighbors (3x3 grid) to account for light spilling. - */ - void gatherForChunk( - const MapEditor::Domain::ChunkedMap& map, - int32_t chunk_x, int32_t chunk_y, - Services::ClientDataService* client_data, - int16_t floor); - - /** - * Gather all light sources from multiple floors for a specific chunk. + * Gather lights and ground occlusion into a single RME-style viewport buffer. */ - void gatherForChunkMultiFloor( + void gatherViewportLightBuffer( const MapEditor::Domain::ChunkedMap& map, - int32_t chunk_x, int32_t chunk_y, Services::ClientDataService* client_data, + int16_t current_floor, int16_t start_floor, - int16_t end_floor); + int16_t end_floor, + ViewportLightBuffer& light_buffer); - /** - * Register ground blocking for viewport tiles from a specific chunk/floor. - * Does NOT collect lights — only populates the blocking grid. - * Used in the pre-pass to build complete blocking data before light computation. - */ - void registerGroundBlockingForChunk( +private: + void addLight(ViewportLightBuffer& light_buffer, + int32_t x, int32_t y, + uint8_t color, uint8_t intensity, + int16_t floor); + + void registerGroundOcclusionForViewport( const MapEditor::Domain::ChunkedMap& map, - int32_t chunk_x, int32_t chunk_y, Services::ClientDataService* client_data, + int16_t current_floor, int16_t floor, - int32_t floor_offset, - ViewportGroundBlocking& viewport_blocking); - - /** - * Get the collected light sources. - */ - const std::vector& getLights() const { return lights_; } - - /** - * Get number of light sources collected. - */ - size_t getLightCount() const { return lights_.size(); } + uint32_t floor_light_start, + ViewportLightBuffer& light_buffer); -private: - /** - * Helper to gather lights from a single neighbor chunk on a specific floor. - */ - void gatherLightsFromNeighborChunk( + void gatherLightsForViewportFloor( const MapEditor::Domain::ChunkedMap& map, - int32_t target_cx, int32_t target_cy, Services::ClientDataService* client_data, - int16_t floor, int32_t floor_offset); - - /** - * Add a light source with deduplication. - * If the last light has the same position, color, and floor, merge (keep higher intensity). - */ - void addLight(int32_t x, int32_t y, uint8_t color, uint8_t intensity, int16_t floor); - - std::vector lights_; + int16_t current_floor, + int16_t floor, + ViewportLightBuffer& light_buffer); }; } // namespace Rendering diff --git a/ImguiMapEditor/Rendering/Light/LightManager.cpp b/ImguiMapEditor/Rendering/Light/LightManager.cpp index 011d3a8..ca21f81 100644 --- a/ImguiMapEditor/Rendering/Light/LightManager.cpp +++ b/ImguiMapEditor/Rendering/Light/LightManager.cpp @@ -1,14 +1,29 @@ #include "LightManager.h" -#include "LightColorPalette.h" -#include "Services/ClientDataService.h" -#include + #include +#include + #include -#include // For memcpy + +#include "LightColorPalette.h" +#include "Core/Config.h" +#include "Services/ClientDataService.h" +#include "Rendering/Visibility/FloorVisibilityCalculator.h" namespace MapEditor { namespace Rendering { +namespace { + +int32_t projectedFloorOffsetTiles(int16_t current_floor, int16_t tile_floor) { + if (tile_floor <= Config::Map::GROUND_LAYER) { + return Config::Map::GROUND_LAYER - tile_floor; + } + return current_floor - tile_floor; +} + +} // namespace + LightManager::LightManager(Services::ClientDataService* client_data) : client_data_(client_data) { @@ -17,7 +32,6 @@ LightManager::LightManager(Services::ClientDataService* client_data) LightManager::~LightManager() = default; bool LightManager::initialize() { - cache_ = std::make_unique(); texture_ = std::make_unique(); overlay_ = std::make_unique(); gatherer_ = std::make_unique(); @@ -35,31 +49,35 @@ bool LightManager::initialize() { } void LightManager::invalidateTile(int32_t x, int32_t y) { - // Flag that we need to update the light texture next frame + (void)x; + (void)y; force_update_ = true; - - if (cache_) { - // Invalidate chunk containing the tile AND its neighbors - // to propagate light changes (spilling) - - // Using explicit shift to match ChunkedMap - int32_t cx = x >> 5; - int32_t cy = y >> 5; - - // Since we don't know the floor here yet (interface limitation), - // valid safe approach is to invalidate this column on all floors. - // Most map edits are single floor, but 16 iterations is cheap. - for (int16_t z = 0; z <= 15; ++z) { - cache_->invalidateRegion(cx - 1, cy - 1, cx + 1, cy + 1, z); - } - } } void LightManager::invalidateAll() { force_update_ = true; - if (cache_) { - cache_->clear(); - } +} + +void LightManager::renderClientVisible( + const Domain::ChunkedMap& map, + int viewport_width, int viewport_height, + float camera_x, float camera_y, + float zoom, int current_floor, + const Domain::LightConfig& config, + std::optional visibility_origin, + const std::vector* injected_lights) +{ + FloorVisibilityCalculator floor_visibility(client_data_); + const int visibility_x = + visibility_origin ? visibility_origin->x : static_cast(camera_x); + const int visibility_y = + visibility_origin ? visibility_origin->y : static_cast(camera_y); + const int start_floor = floor_visibility.calcLastVisibleFloor(current_floor); + const int end_floor = floor_visibility.calcFirstVisibleFloor( + map, visibility_x, visibility_y, current_floor); + + render(map, viewport_width, viewport_height, camera_x, camera_y, zoom, + current_floor, start_floor, end_floor, config, injected_lights); } void LightManager::render(const Domain::ChunkedMap& map, @@ -131,102 +149,43 @@ void LightManager::render(const Domain::ChunkedMap& map, last_config_ = config; force_update_ = false; - // 2. Prepare buffer and viewport blocking grid + // 2. Gather one RME-style viewport light buffer and compute brightness globally. size_t required_size = width_tiles * height_tiles * 4; if (viewport_buffer_.size() < required_size) { viewport_buffer_.resize(required_size); } - - // Calculate chunk bounds - int chunk_start_x = start_x >> 5; - int chunk_start_y = start_y >> 5; - int chunk_end_x = end_x >> 5; - int chunk_end_y = end_y >> 5; - - // Initialize viewport-level ground blocking (RME-style global blocking) - viewport_blocking_.init(start_x, start_y, width_tiles, height_tiles); - - // PRE-PASS: Register ground blocking for ALL visible chunks/floors BEFORE computing lights. - // This ensures the blocking grid is complete before any light computation, - // preventing chunk seams where earlier chunks have incomplete blocking data. - constexpr int GROUND_LAYER = 7; - for (int16_t floor = static_cast(start_floor); floor >= static_cast(end_floor); --floor) { - int32_t floor_offset = 0; - if (floor <= GROUND_LAYER) { - floor_offset = GROUND_LAYER - floor; - } - for (int cy = chunk_start_y; cy <= chunk_end_y; ++cy) { - for (int cx = chunk_start_x; cx <= chunk_end_x; ++cx) { - gatherer_->registerGroundBlockingForChunk( - map, cx, cy, client_data_, floor, floor_offset, viewport_blocking_); - } - } - } - - // 3. Iterate over chunks in the view - - for (int cy = chunk_start_y; cy <= chunk_end_y; ++cy) { - for (int cx = chunk_start_x; cx <= chunk_end_x; ++cx) { - - // Get or compute the grid - // Use current_floor as cache key since all lights are projected to this floor - CachedLightGrid& grid = cache_->getOrCreateGrid(cx, cy, static_cast(current_floor)); - if (!grid.is_valid) { - gatherer_->clear(); - - // Use multi-floor gathering if we have a floor range - if (start_floor != end_floor) { - gatherer_->gatherForChunkMultiFloor( - map, cx, cy, client_data_, - static_cast(start_floor), - static_cast(end_floor)); - } else { - // Single floor mode — no blocking needed (only one floor) - gatherer_->gatherForChunk(map, cx, cy, client_data_, static_cast(current_floor)); - } - - // Combine gathered lights with any injected lights - if (injected_lights && !injected_lights->empty()) { - std::vector combined = gatherer_->getLights(); - combined.insert(combined.end(), injected_lights->begin(), injected_lights->end()); - computeChunkLight(grid, combined, viewport_blocking_, config, cx, cy); - } else { - computeChunkLight(grid, gatherer_->getLights(), viewport_blocking_, config, cx, cy); + + light_buffer_.init(start_x, start_y, width_tiles, height_tiles); + gatherer_->gatherViewportLightBuffer( + map, client_data_, static_cast(current_floor), + static_cast(start_floor), static_cast(end_floor), + light_buffer_); + + if (injected_lights) { + for (const auto& light : *injected_lights) { + Domain::LightSource projected = light; + const int32_t floor_offset = projectedFloorOffsetTiles( + static_cast(current_floor), light.source_floor); + projected.x -= floor_offset; + projected.y -= floor_offset; + + if (!light_buffer_.lights.empty()) { + auto& prev = light_buffer_.lights.back(); + if (prev.x == projected.x && prev.y == projected.y && + prev.color == projected.color && + prev.source_floor == projected.source_floor) { + prev.intensity = std::max(prev.intensity, projected.intensity); + continue; } - - grid.is_valid = true; - } - - // Copy relevant part of the grid to viewport buffer - int chunk_pixel_x = cx * 32; - int chunk_pixel_y = cy * 32; - - // Intersection between Chunk and Viewport - int ix_start = std::max(start_x, chunk_pixel_x); - int ix_end = std::min(end_x, chunk_pixel_x + 32); - int iy_start = std::max(start_y, chunk_pixel_y); - int iy_end = std::min(end_y, chunk_pixel_y + 32); - - if (ix_start >= ix_end || iy_start >= iy_end) continue; - - // Copy loops - for (int y = iy_start; y < iy_end; ++y) { - int dest_y = y - start_y; - int src_y = y - chunk_pixel_y; - - int dest_row_start = (dest_y * width_tiles + (ix_start - start_x)) * 4; - int src_row_start = (src_y * 32 + (ix_start - chunk_pixel_x)) * 4; // Assuming 32 width - int row_len = (ix_end - ix_start) * 4; // Bytes - - // Safety check - std::memcpy(&viewport_buffer_[dest_row_start], - reinterpret_cast(&grid.pixels[0]) + src_row_start, - row_len); } + + light_buffer_.lights.push_back(projected); } } - // 4. Upload and Render + computeViewportLight(light_buffer_, config); + + // 3. Upload and Render texture_->upload(viewport_buffer_, width_tiles, height_tiles); // Calc screen rect @@ -243,87 +202,71 @@ void LightManager::render(const Domain::ChunkedMap& map, glm::vec2(viewport_width, viewport_height)); } -void LightManager::computeChunkLight(CachedLightGrid& grid, - const std::vector& lights, - const ViewportGroundBlocking& viewport_blocking, - const Domain::LightConfig& config, - int32_t chunk_x, int32_t chunk_y) +void LightManager::computeViewportLight(const ViewportLightBuffer& light_buffer, + const Domain::LightConfig& config) { - // Determine ambient based on camera floor (above-ground vs underground) + const bool above_ground = config.camera_floor <= Config::Map::GROUND_LAYER; + const uint8_t ambient_color = above_ground ? config.global_light.color : 215; + const uint8_t ambient_intensity = above_ground + ? std::max(config.client_slider, config.global_light.intensity) + : config.client_slider; + float ambient_r, ambient_g, ambient_b; - - constexpr int GROUND_LAYER = 7; - if (config.camera_floor <= GROUND_LAYER) { - // Above ground: use server global light - float scale = static_cast(config.global_light.intensity) / 255.0f; - LightColorPalette::from8bitFloat(config.global_light.color, ambient_r, ambient_g, ambient_b); - ambient_r *= scale; - ambient_g *= scale; - ambient_b *= scale; - } else { - // Underground: zero ambient (pitch black without lights) - ambient_r = 0.0f; - ambient_g = 0.0f; - ambient_b = 0.0f; - } - - // Apply client slider as floor above ground; as absolute underground - float slider_scale = static_cast(config.client_slider) / 255.0f; - if (config.camera_floor <= GROUND_LAYER) { - // Above ground: max(slider, ambient) - float slider_r = slider_scale; // White light from slider - float slider_g = slider_scale; - float slider_b = slider_scale; - ambient_r = std::max(ambient_r, slider_r); - ambient_g = std::max(ambient_g, slider_g); - ambient_b = std::max(ambient_b, slider_b); - } else { - // Underground: slider is absolute - ambient_r = slider_scale; - ambient_g = slider_scale; - ambient_b = slider_scale; - } + LightColorPalette::from8bitFloat(ambient_color, ambient_r, ambient_g, ambient_b); + const float ambient_scale = static_cast(ambient_intensity) / 255.0f; + ambient_r *= ambient_scale; + ambient_g *= ambient_scale; + ambient_b *= ambient_scale; uint8_t base_r = static_cast(std::min(ambient_r * 255.0f, 255.0f)); uint8_t base_g = static_cast(std::min(ambient_g * 255.0f, 255.0f)); uint8_t base_b = static_cast(std::min(ambient_b * 255.0f, 255.0f)); // Fill with ambient first - uint32_t ambient_packed = base_r | (base_g << 8) | (base_b << 16) | (255 << 24); - std::fill(grid.pixels.begin(), grid.pixels.end(), ambient_packed); - - int chunk_start_x = chunk_x * 32; - int chunk_start_y = chunk_y * 32; + const size_t pixel_count = + static_cast(light_buffer.width) * static_cast(light_buffer.height); + for (size_t i = 0; i < pixel_count; ++i) { + const size_t base = i * 4; + viewport_buffer_[base + 0] = base_r; + viewport_buffer_[base + 1] = base_g; + viewport_buffer_[base + 2] = base_b; + viewport_buffer_[base + 3] = 255; + } // Iterate lights FIRST, then only tiles affected by each light - for (const auto& light : lights) { + for (size_t light_index = 0; light_index < light_buffer.lights.size(); ++light_index) { + const auto& light = light_buffer.lights[light_index]; // Pre-compute light color once per light float lr, lg, lb; LightColorPalette::from8bitFloat(light.color, lr, lg, lb); - // Calculate bounding box of affected tiles (in local chunk coords) + // Calculate bounding box of affected tiles (in viewport-local coords) int radius = light.intensity; - int min_x = std::max(0, light.x - radius - chunk_start_x); - int max_x = std::min(31, light.x + radius - chunk_start_x); - int min_y = std::max(0, light.y - radius - chunk_start_y); - int max_y = std::min(31, light.y + radius - chunk_start_y); + int min_x = std::max(0, light.x - radius - light_buffer.origin_x); + int max_x = std::min(light_buffer.width - 1, + light.x + radius - light_buffer.origin_x); + int min_y = std::max(0, light.y - radius - light_buffer.origin_y); + int max_y = std::min(light_buffer.height - 1, + light.y + radius - light_buffer.origin_y); - // Skip if light doesn't affect this chunk - if (min_x > 31 || max_x < 0 || min_y > 31 || max_y < 0) continue; + // Skip if light doesn't affect this viewport + if (min_x >= light_buffer.width || max_x < 0 || + min_y >= light_buffer.height || max_y < 0) { + continue; + } // Only iterate tiles within light's bounding box for (int y = min_y; y <= max_y; ++y) { for (int x = min_x; x <= max_x; ++x) { - int tile_x = chunk_start_x + x; - int tile_y = chunk_start_y + y; - - // Ground blocking check: viewport-level blocking (RME-style) - // blocking_floor = nearest floor (lowest Z) with solid ground at this projected position. - // Lights from floors BELOW (higher Z) the blocking ground are blocked. - int16_t blocking_floor = viewport_blocking.getBlockingFloor(tile_x, tile_y); - if (blocking_floor >= 0 && light.source_floor > blocking_floor) { - continue; // Light is from a floor below the solid ground — blocked + const size_t tile_index = + static_cast(y) * static_cast(light_buffer.width) + + static_cast(x); + if (light_index < light_buffer.tile_start[tile_index]) { + continue; } + + int tile_x = light_buffer.origin_x + x; + int tile_y = light_buffer.origin_y + y; float dx = (static_cast(tile_x) + 0.5f) - (static_cast(light.x) + 0.5f); float dy = (static_cast(tile_y) + 0.5f) - (static_cast(light.y) + 0.5f); @@ -340,15 +283,18 @@ void LightManager::computeChunkLight(CachedLightGrid& grid, if (intensity > 1.0f) intensity = 1.0f; // Update pixel with max blending - uint32_t& pixel = grid.pixels[y * 32 + x]; - int r = std::max(static_cast(pixel & 0xFF), static_cast(lr * intensity * 255.0f)); - int g = std::max(static_cast((pixel >> 8) & 0xFF), static_cast(lg * intensity * 255.0f)); - int b = std::max(static_cast((pixel >> 16) & 0xFF), static_cast(lb * intensity * 255.0f)); - - pixel = static_cast(std::min(r, 255)) | - (static_cast(std::min(g, 255)) << 8) | - (static_cast(std::min(b, 255)) << 16) | - (255 << 24); + const size_t base = tile_index * 4; + int r = std::max(static_cast(viewport_buffer_[base + 0]), + static_cast(lr * intensity * 255.0f)); + int g = std::max(static_cast(viewport_buffer_[base + 1]), + static_cast(lg * intensity * 255.0f)); + int b = std::max(static_cast(viewport_buffer_[base + 2]), + static_cast(lb * intensity * 255.0f)); + + viewport_buffer_[base + 0] = static_cast(std::min(r, 255)); + viewport_buffer_[base + 1] = static_cast(std::min(g, 255)); + viewport_buffer_[base + 2] = static_cast(std::min(b, 255)); + viewport_buffer_[base + 3] = 255; } } } diff --git a/ImguiMapEditor/Rendering/Light/LightManager.h b/ImguiMapEditor/Rendering/Light/LightManager.h index 65e3cbd..2d7fd73 100644 --- a/ImguiMapEditor/Rendering/Light/LightManager.h +++ b/ImguiMapEditor/Rendering/Light/LightManager.h @@ -1,11 +1,12 @@ #pragma once -#include "LightCache.h" #include "LightTexture.h" #include "LightOverlay.h" #include "LightGatherer.h" #include "Domain/ChunkedMap.h" #include "Domain/LightTypes.h" +#include "Domain/Position.h" #include +#include #include namespace MapEditor { @@ -42,6 +43,21 @@ class LightManager { const MapEditor::Domain::LightConfig& config, const std::vector* injected_lights = nullptr); + /** + * Render using RME's light-mode floor visibility range. + * + * RME ignores the editor's show-all-floors range when collecting lights and + * instead asks the client visibility algorithm which floors are actually + * visible through ground/roof blockers. + */ + void renderClientVisible(const MapEditor::Domain::ChunkedMap& map, + int viewport_width, int viewport_height, + float camera_x, float camera_y, + float zoom, int current_floor, + const MapEditor::Domain::LightConfig& config, + std::optional visibility_origin = std::nullopt, + const std::vector* injected_lights = nullptr); + /** * Invalidate light cache for a specific tile position. * Should be called when tiles change. @@ -54,19 +70,15 @@ class LightManager { void invalidateAll(); private: - void computeChunkLight(CachedLightGrid& grid, - const std::vector& lights, - const ViewportGroundBlocking& viewport_blocking, - const MapEditor::Domain::LightConfig& config, - int32_t chunk_x, int32_t chunk_y); + void computeViewportLight(const ViewportLightBuffer& light_buffer, + const MapEditor::Domain::LightConfig& config); Services::ClientDataService* client_data_; - std::unique_ptr cache_; std::unique_ptr texture_; std::unique_ptr overlay_; std::unique_ptr gatherer_; - ViewportGroundBlocking viewport_blocking_; + ViewportLightBuffer light_buffer_; std::vector viewport_buffer_; diff --git a/ImguiMapEditor/Rendering/Map/MapRenderer.cpp b/ImguiMapEditor/Rendering/Map/MapRenderer.cpp index 4bc34fd..8b4d7ba 100644 --- a/ImguiMapEditor/Rendering/Map/MapRenderer.cpp +++ b/ImguiMapEditor/Rendering/Map/MapRenderer.cpp @@ -79,6 +79,11 @@ void MapRenderer::setViewSettings(Services::ViewSettings *settings) { } } +void MapRenderer::setLightVisibilityOrigin( + std::optional origin) { + light_visibility_origin_ = origin; +} + void MapRenderer::setLODMode(bool enabled) { render_pipeline_.setLODMode(enabled); } @@ -131,7 +136,8 @@ void MapRenderer::render(const Domain::ChunkedMap &map, RenderState &state, base_bounds, // Visible bounds camera_.getFloor(), // Current floor frame_data_collector_.getMissingSpriteBuffer(), // Missing sprites buffer - view_settings_}; // View settings + view_settings_, // View settings + light_visibility_origin_}; // Light visibility origin // Execute Pipeline render_pipeline_.render(context); diff --git a/ImguiMapEditor/Rendering/Map/MapRenderer.h b/ImguiMapEditor/Rendering/Map/MapRenderer.h index 978b7e6..694844f 100644 --- a/ImguiMapEditor/Rendering/Map/MapRenderer.h +++ b/ImguiMapEditor/Rendering/Map/MapRenderer.h @@ -1,6 +1,7 @@ #pragma once #include "Core/Config.h" #include "Domain/ChunkedMap.h" +#include "Domain/Position.h" #include "Rendering/Animation/AnimationTicks.h" #include "Rendering/Backend/IRenderer.h" #include "Rendering/Backend/SpriteBatch.h" @@ -23,6 +24,7 @@ #include "Services/ViewSettings.h" #include #include +#include #include namespace MapEditor { @@ -85,6 +87,7 @@ class MapRenderer : public IRenderer { void setGridVisible(bool visible) { show_grid_ = visible; } bool isGridVisible() const { return show_grid_; } void setViewSettings(Services::ViewSettings *settings) override; + void setLightVisibilityOrigin(std::optional origin); /** * Set LOD mode to enable/disable simplified rendering and optimizations. @@ -155,6 +158,7 @@ class MapRenderer : public IRenderer { // Camera component (handles position, zoom, matrix) ViewCamera camera_; + std::optional light_visibility_origin_; bool show_grid_ = true; // Stats diff --git a/ImguiMapEditor/Rendering/Passes/LightingPass.cpp b/ImguiMapEditor/Rendering/Passes/LightingPass.cpp index f8f8213..b838167 100644 --- a/ImguiMapEditor/Rendering/Passes/LightingPass.cpp +++ b/ImguiMapEditor/Rendering/Passes/LightingPass.cpp @@ -1,7 +1,6 @@ #include "Rendering/Passes/LightingPass.h" #include "Rendering/Frame/RenderState.h" #include "Rendering/Light/LightManager.h" -#include "Rendering/Visibility/FloorIterator.h" #include "Services/ViewSettings.h" #include @@ -38,25 +37,11 @@ void LightingPass::render(const RenderContext &context) { context.state.last_config_hash = config_hash; } - // Calculate floor range based on show_all_floors setting - bool show_all_floors = context.view_settings->show_all_floors; - FloorRange floor_range = FloorIterator::calculateRangeWithToggle( - context.current_floor, show_all_floors); - - // Invalidate if floor range changed (e.g., toggling show_all_floors) - if (floor_range.start_z != last_start_floor_ || - floor_range.super_end_z != last_end_floor_) { - context.state.light_manager->invalidateAll(); - last_start_floor_ = floor_range.start_z; - last_end_floor_ = floor_range.super_end_z; - } - - context.state.light_manager->render( + context.state.light_manager->renderClientVisible( context.map, context.viewport_width, context.viewport_height, context.camera.getX(), context.camera.getY(), context.camera.getZoom(), static_cast(context.current_floor), - floor_range.start_z, floor_range.super_end_z, - config); + config, context.light_visibility_origin); } } // namespace Rendering diff --git a/ImguiMapEditor/Rendering/Passes/LightingPass.h b/ImguiMapEditor/Rendering/Passes/LightingPass.h index e1b5234..36391e8 100644 --- a/ImguiMapEditor/Rendering/Passes/LightingPass.h +++ b/ImguiMapEditor/Rendering/Passes/LightingPass.h @@ -14,10 +14,6 @@ class LightingPass : public IRenderPass { ~LightingPass() override = default; void render(const RenderContext &context) override; - -private: - int last_start_floor_ = -1; - int last_end_floor_ = -1; }; } // namespace Rendering diff --git a/ImguiMapEditor/Rendering/Visibility/FloorVisibilityCalculator.cpp b/ImguiMapEditor/Rendering/Visibility/FloorVisibilityCalculator.cpp index ba1effe..17058fc 100644 --- a/ImguiMapEditor/Rendering/Visibility/FloorVisibilityCalculator.cpp +++ b/ImguiMapEditor/Rendering/Visibility/FloorVisibilityCalculator.cpp @@ -12,72 +12,59 @@ FloorVisibilityCalculator::FloorVisibilityCalculator(Services::ClientDataService const Domain::ItemType* FloorVisibilityCalculator::getItemType(const Domain::Item* item) const { if (!item || !client_data_) return nullptr; + if (const Domain::ItemType* item_type = item->getType()) { + return item_type; + } return client_data_->getItemTypeByServerId(item->getServerId()); } bool FloorVisibilityCalculator::tileLimitsFloorsView(const Domain::Tile* tile, bool is_free_view) const { if (!tile) return false; - - // Check ground first - const Domain::Item* ground = tile->getGround(); - if (!ground) return false; - - const Domain::ItemType* ground_type = getItemType(ground); - if (!ground_type) return false; - - // Items with isDontHide never block view - if (ground_type->is_dont_hide) return false; - - // Ground tiles block view - if (ground_type->is_ground) return true; - - // isOnBottom items (walls) block view - if (ground_type->is_on_bottom) { - if (is_free_view) { - // Free view: any wall blocks - return true; - } else { - // Player view: only walls that block projectiles - return ground_type->blocks_projectile; - } + + const Domain::Item* first_thing = tile->getGround(); + if (!first_thing && !tile->getItems().empty()) { + first_thing = tile->getItems().front().get(); } - - // Check other items on tile for blocking - for (const auto& item : tile->getItems()) { - const Domain::ItemType* item_type = getItemType(item.get()); - if (!item_type) continue; - - if (item_type->is_dont_hide) continue; - - if (item_type->is_ground) return true; - - if (item_type->is_on_bottom) { - if (is_free_view) { - return true; - } else { - if (item_type->blocks_projectile) return true; - } - } + + const Domain::ItemType* item_type = getItemType(first_thing); + if (!item_type) return false; + + if (item_type->hasFlag(Domain::ItemFlag::IgnoreLook)) { + return false; } - - return false; + + const bool is_ground_tile = item_type->isGround() || item_type->is_ground; + const bool is_bottom = item_type->always_on_bottom || item_type->is_on_bottom; + const bool blocks_projectile = + item_type->blocks_projectile || + item_type->hasFlag(Domain::ItemFlag::BlockMissiles); + + if (is_free_view) { + return is_ground_tile || is_bottom; + } + + return is_ground_tile || (is_bottom && blocks_projectile); } bool FloorVisibilityCalculator::isLookPossible(const Domain::Tile* tile) const { - if (!tile) return true; // Empty tile is transparent + if (!tile) return false; // Check all items for projectile blocking const Domain::Item* ground = tile->getGround(); if (ground) { const Domain::ItemType* ground_type = getItemType(ground); - if (ground_type && ground_type->blocks_projectile) { + if (ground_type && + (ground_type->blocks_projectile || + ground_type->hasFlag(Domain::ItemFlag::BlockMissiles))) { return false; } } for (const auto& item : tile->getItems()) { const Domain::ItemType* item_type = getItemType(item.get()); - if (item_type && item_type->blocks_projectile) { + if (item_type && + (item_type->blocks_projectile || + item_type->hasFlag(Domain::ItemFlag::BlockMissiles))) { return false; } } @@ -99,48 +86,44 @@ int FloorVisibilityCalculator::calcFirstVisibleFloor( static_cast(FC::UNDERGROUND_FLOOR)); } - // Check 3x3 area around camera for blocking tiles for (int ix = -1; ix <= 1 && first_floor < camera_z; ++ix) { for (int iy = -1; iy <= 1 && first_floor < camera_z; ++iy) { - int pos_x = camera_x + ix; - int pos_y = camera_y + iy; - - // Center tile OR diagonal tiles where we can look through - bool is_center = (ix == 0 && iy == 0); - bool is_diagonal = (std::abs(ix) == std::abs(iy)) && !is_center; - - // For diagonal tiles, check if we can look through (window/door) - if (!is_center && is_diagonal) { - const Domain::Tile* current_tile = map.getTile(pos_x, pos_y, camera_z); - if (!isLookPossible(current_tile)) { - continue; // Can't look diagonally through solid tiles - } + const int pos_x = camera_x + ix; + const int pos_y = camera_y + iy; + const bool is_center = ix == 0 && iy == 0; + const bool is_straight_neighbor = std::abs(ix) != std::abs(iy); + const Domain::Tile* position_tile = map.getTile(pos_x, pos_y, camera_z); + const bool look_possible = isLookPossible(position_tile); + + if (!is_center && (!is_straight_neighbor || !look_possible)) { + continue; } - - // Walk up through floors checking for blockers - // OTClient uses coveredUp which shifts x+1, y+1, z-1 - int check_x = pos_x; - int check_y = pos_y; - - for (int check_z = camera_z - 1; check_z >= first_floor; --check_z) { - // Apply covered position shift (each floor up = x+1, y+1) - int z_diff = camera_z - check_z; - int covered_x = pos_x + z_diff; - int covered_y = pos_y + z_diff; - - // Check tile directly above (physical position) - const Domain::Tile* upper_tile = map.getTile(pos_x, pos_y, check_z); - bool can_look = isLookPossible(map.getTile(pos_x, pos_y, camera_z)); - - if (upper_tile && tileLimitsFloorsView(upper_tile, !can_look)) { - first_floor = check_z + 1; + + int upper_x = pos_x; + int upper_y = pos_y; + int upper_z = camera_z; + int covered_x = pos_x; + int covered_y = pos_y; + int covered_z = camera_z; + + while (upper_z > 0 && covered_z > 0) { + --upper_z; + --covered_z; + ++covered_x; + ++covered_y; + if (upper_z < first_floor) { break; } - - // Check tile geometrically above (covered position) - const Domain::Tile* covered_tile = map.getTile(covered_x, covered_y, check_z); - if (covered_tile && tileLimitsFloorsView(covered_tile, can_look)) { - first_floor = check_z + 1; + + if (const Domain::Tile* upper_tile = map.getTile(upper_x, upper_y, upper_z); + tileLimitsFloorsView(upper_tile, !look_possible)) { + first_floor = upper_z + 1; + break; + } + + if (const Domain::Tile* covered_tile = map.getTile(covered_x, covered_y, covered_z); + tileLimitsFloorsView(covered_tile, look_possible)) { + first_floor = covered_z + 1; break; } } diff --git a/ImguiMapEditor/UI/Map/MapPanel.cpp b/ImguiMapEditor/UI/Map/MapPanel.cpp index 647f742..6d11a2c 100644 --- a/ImguiMapEditor/UI/Map/MapPanel.cpp +++ b/ImguiMapEditor/UI/Map/MapPanel.cpp @@ -81,6 +81,7 @@ void MapPanel::renderInternal(MapType *map, Rendering::RenderState &state, // Update renderer camera renderer->setCameraPosition(camera_.getCameraPosition().x, camera_.getCameraPosition().y); + renderer->setLightVisibilityOrigin(input_.getLightVisibilityOrigin()); // Pass selection provider to renderer (session owns the adapter) if (session_) { diff --git a/ImguiMapEditor/UI/Map/MapPanelInput.cpp b/ImguiMapEditor/UI/Map/MapPanelInput.cpp index c002470..fd6ffe8 100644 --- a/ImguiMapEditor/UI/Map/MapPanelInput.cpp +++ b/ImguiMapEditor/UI/Map/MapPanelInput.cpp @@ -48,6 +48,7 @@ void MapPanelInput::handlePasteMode(MapViewCamera &camera, if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { glm::vec2 mouse_pos(io.MousePos.x, io.MousePos.y); Domain::Position target_pos = camera.screenToTile(mouse_pos); + setLightVisibilityOrigin(target_pos); bool replace_mode = session->isPasteReplaceMode() || io.KeyShift; session->confirmPaste(target_pos, replace_mode); return; @@ -108,8 +109,17 @@ void MapPanelInput::handleMouseZoom(MapViewCamera &camera) { } } +void MapPanelInput::setLightVisibilityOrigin(const Domain::Position &position) { + light_visibility_origin_ = position; +} + +void MapPanelInput::clearLightVisibilityOrigin() { + light_visibility_origin_.reset(); +} + void MapPanelInput::handleFloorChange(MapViewCamera &camera, bool is_focused) { ImGuiIO &io = ImGui::GetIO(); + const int16_t previous_floor = camera.getCurrentFloor(); // Ctrl + Scroll for floor change (inverted: scroll down = floor up, scroll up // = floor down) @@ -128,6 +138,10 @@ void MapPanelInput::handleFloorChange(MapViewCamera &camera, bool is_focused) { if (ImGui::IsKeyPressed(ImGuiKey_PageDown)) camera.floorDown(); } + + if (camera.getCurrentFloor() != previous_floor) { + clearLightVisibilityOrigin(); + } } bool MapPanelInput::shouldShowBoxOverlay() const { @@ -211,6 +225,7 @@ void MapPanelInput::handleTileSelection( // Handle Lasso Start with Alt Key if (is_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + setLightVisibilityOrigin(camera.screenToTile(mouse_pos)); if (io.KeyAlt) { lasso_mode_ = LassoMode::Drawing; lasso_points_.clear(); @@ -341,6 +356,7 @@ void MapPanelInput::handleNormalSelectionInput( // Start tracking on left click (non-lasso) if (is_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + setLightVisibilityOrigin(tile_pos); handleSelectionMouseDown(camera, session, input_controller, mouse_pos, tile_pos, mods, io); } diff --git a/ImguiMapEditor/UI/Map/MapPanelInput.h b/ImguiMapEditor/UI/Map/MapPanelInput.h index 35ef158..ce223c4 100644 --- a/ImguiMapEditor/UI/Map/MapPanelInput.h +++ b/ImguiMapEditor/UI/Map/MapPanelInput.h @@ -2,6 +2,7 @@ #include "Domain/Position.h" #include "MapViewCamera.h" #include +#include #include struct ImGuiIO; // Forward declaration @@ -60,6 +61,9 @@ class MapPanelInput { bool shouldShowLassoOverlay() const; const std::vector &getLassoPoints() const { return lasso_points_; } glm::vec2 getCurrentMousePos() const { return current_mouse_pos_; } + std::optional getLightVisibilityOrigin() const { + return light_visibility_origin_; + } // Context menu state bool shouldShowContextMenu() const { return show_context_menu_; } @@ -68,6 +72,8 @@ class MapPanelInput { private: void handlePasteMode(MapViewCamera &camera, AppLogic::EditorSession *session); + void setLightVisibilityOrigin(const Domain::Position &position); + void clearLightVisibilityOrigin(); void handleMousePan(MapViewCamera &camera, bool is_focused); void handleMouseZoom(MapViewCamera &camera); @@ -124,6 +130,7 @@ class MapPanelInput { // Panning state bool is_panning_ = false; glm::vec2 pan_start_{0, 0}; + std::optional light_visibility_origin_; // Drag selection state bool is_drag_selecting_ = false; From 9a4d5dffb5ae14aed9b29c000193ddca7e5514f9 Mon Sep 17 00:00:00 2001 From: glaszczkoldre Date: Sun, 31 May 2026 20:59:51 +0200 Subject: [PATCH 4/6] refactor(lighting): centralize constants and improve configuration management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Centralized lighting configuration constants, extracted projection logic into a shared header, and added hashing support to `LightConfig` for efficient cache invalidation. - `ImguiMapEditor/Core/Config.h` — introduced `Lighting` namespace with default configuration constants - `ImguiMapEditor/Domain/LightTypes.h` — updated to use central lighting constants and added `computeHash()` to `LightConfig` - `ImguiMapEditor/Domain/Tile.h` — updated `TranslucentLight` documentation - `ImguiMapEditor/IO/CreatureXmlReader.cpp` — added light attribute clamping for creature parsing - `ImguiMapEditor/Rendering/Frame/RenderState.h` — updated `last_config_hash` documentation - `ImguiMapEditor/Rendering/Light/LightGatherer.cpp` — utilized shared `projectedFloorOffsetTiles` and centralized lighting constants - `ImguiMapEditor/Rendering/Light/LightManager.cpp` — removed local `projectedFloorOffsetTiles`, updated ambient calculation to use central constants - `ImguiMapEditor/Rendering/Light/LightProjection.h` — created new header for shared projection logic - `ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp` — utilized `config.computeHash()` and centralized constants for player lights - `ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h` — removed unused `last_ambient_light_` member - `ImguiMapEditor/Rendering/Passes/LightingPass.cpp` — refactored invalidation logic to use `config.computeHash()` - `ImguiMapEditor/Services/ViewSettings.cpp` — updated settings loading to use central lighting constants - `ImguiMapEditor/Services/ViewSettings.h` — updated lighting settings to use central constants --- ImguiMapEditor/Core/Config.h | 11 ++++++++ ImguiMapEditor/Domain/LightTypes.h | 26 ++++++++++++++----- ImguiMapEditor/Domain/Tile.h | 4 ++- ImguiMapEditor/IO/CreatureXmlReader.cpp | 23 +++++++++++----- ImguiMapEditor/Rendering/Frame/RenderState.h | 2 +- .../Rendering/Light/LightGatherer.cpp | 13 ++++------ .../Rendering/Light/LightManager.cpp | 16 +++--------- .../Rendering/Light/LightProjection.h | 17 ++++++++++++ .../Passes/IngamePreviewRenderer.cpp | 10 +++---- .../Rendering/Passes/IngamePreviewRenderer.h | 1 - .../Rendering/Passes/LightingPass.cpp | 6 ++--- ImguiMapEditor/Services/ViewSettings.cpp | 21 ++++++++++----- ImguiMapEditor/Services/ViewSettings.h | 15 ++++++----- 13 files changed, 108 insertions(+), 57 deletions(-) create mode 100644 ImguiMapEditor/Rendering/Light/LightProjection.h diff --git a/ImguiMapEditor/Core/Config.h b/ImguiMapEditor/Core/Config.h index 9cc67af..20a86a4 100644 --- a/ImguiMapEditor/Core/Config.h +++ b/ImguiMapEditor/Core/Config.h @@ -83,6 +83,17 @@ inline constexpr int GROUND_LAYER = 7; inline constexpr uint16_t DEFAULT_MAP_SIZE = 16384; } // namespace Map +// ============================================================================ +// LIGHTING CONFIGURATION +// ============================================================================ +namespace Lighting { +inline constexpr uint8_t DEFAULT_SERVER_LIGHT_INTENSITY = 200; +inline constexpr uint8_t DEFAULT_SERVER_LIGHT_COLOR = 215; +inline constexpr uint8_t DEFAULT_MINIMUM_AMBIENT = 0; +inline constexpr uint8_t DEFAULT_PLAYER_LIGHT_INTENSITY = 2; +inline constexpr uint8_t TRANSLUCENT_PROPAGATION_INTENSITY = 1; +} // namespace Lighting + // ============================================================================ // PERFORMANCE CONFIGURATION // ============================================================================ diff --git a/ImguiMapEditor/Domain/LightTypes.h b/ImguiMapEditor/Domain/LightTypes.h index 7cc19b7..e204d05 100644 --- a/ImguiMapEditor/Domain/LightTypes.h +++ b/ImguiMapEditor/Domain/LightTypes.h @@ -1,5 +1,7 @@ #pragma once +#include "Core/Config.h" + #include namespace MapEditor { @@ -11,7 +13,7 @@ namespace Domain { */ struct GlobalLight { uint8_t intensity = 0; // Server ambient level (0-255) - uint8_t color = 215; // Server ambient color (8-bit palette index) + uint8_t color = Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR; }; /** @@ -21,7 +23,7 @@ struct GlobalLight { struct LightSource { int32_t x = 0; // Tile X position int32_t y = 0; // Tile Y position - uint8_t color = 215; // 8-bit palette index (default: white-ish) + uint8_t color = Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR; uint8_t intensity = 0; // 0-255 (affects light radius) int16_t source_floor = 0; // Floor this light originates from (for blocking) }; @@ -33,10 +35,22 @@ struct LightSource { struct LightConfig { bool enabled = false; // Master enable for this viewport GlobalLight global_light; // Server global light (above-ground ambient) - uint8_t client_slider = 0; // Client minimum ambient slider (0-255), acts as floor - int16_t camera_floor = 7; // Current camera floor (for above/underground check) - uint8_t ambient_color = 215; // DEPRECATED: kept for backward compat, use global_light.color - uint8_t ambient_level = 255; // DEPRECATED: kept for backward compat, computed from global_light + slider + uint8_t client_slider = Config::Lighting::DEFAULT_MINIMUM_AMBIENT; + int16_t camera_floor = Config::Map::GROUND_LAYER; + // Deprecated compatibility fields. Rendering computes final ambient from + // global_light, client_slider, and camera_floor elsewhere. + uint8_t ambient_color = Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR; + uint8_t ambient_level = 255; + + /** + * Hash fields that affect generated lighting. + */ + [[nodiscard]] uint32_t computeHash() const { + return static_cast(global_light.intensity) | + (static_cast(global_light.color) << 8) | + (static_cast(client_slider) << 16) | + (static_cast(camera_floor & 0xFF) << 24); + } }; } // namespace Domain diff --git a/ImguiMapEditor/Domain/Tile.h b/ImguiMapEditor/Domain/Tile.h index d3a0f54..f367b6e 100644 --- a/ImguiMapEditor/Domain/Tile.h +++ b/ImguiMapEditor/Domain/Tile.h @@ -27,7 +27,9 @@ enum class TileFlag : uint16_t { NoLogout = 1 << 3, // 0x0008 PvpZone = 1 << 4, // 0x0010 Refresh = 1 << 5, // 0x0020 - TranslucentLight = 1 << 6 // 0x0040 — z=8 tile lit by translucent ground above + // TranslucentLight marks an underground tile lit by translucent ground above; + // the current RME/OTClient propagation rule applies at z == GROUND_LAYER + 1. + TranslucentLight = 1 << 6 }; inline TileFlag operator|(TileFlag a, TileFlag b) { diff --git a/ImguiMapEditor/IO/CreatureXmlReader.cpp b/ImguiMapEditor/IO/CreatureXmlReader.cpp index 3c165bb..8d03d64 100644 --- a/ImguiMapEditor/IO/CreatureXmlReader.cpp +++ b/ImguiMapEditor/IO/CreatureXmlReader.cpp @@ -136,12 +136,23 @@ CreatureXmlReader::parseCreatureNode(const pugi::xml_node &node, bool isNpc, } // Light properties (optional) - if (auto attr = node.attribute("lightlevel")) { - creature->light_level = static_cast(attr.as_uint()); - } - if (auto attr = node.attribute("lightcolor")) { - creature->light_color = static_cast(attr.as_uint()); - } + const auto read_light_u8 = [&](const char *attribute_name) -> uint8_t { + const auto attr = node.attribute(attribute_name); + if (!attr) return 0; + + const unsigned int raw_value = attr.as_uint(); + if (raw_value > 255U) { + warnings.push_back(std::string("Creature '") + creature->name + + "' has " + attribute_name + "=" + + std::to_string(raw_value) + "; clamped to 255"); + return 255; + } + + return static_cast(raw_value); + }; + + creature->light_level = read_light_u8("lightlevel"); + creature->light_color = read_light_u8("lightcolor"); return creature; } diff --git a/ImguiMapEditor/Rendering/Frame/RenderState.h b/ImguiMapEditor/Rendering/Frame/RenderState.h index e68dfb1..2602f02 100644 --- a/ImguiMapEditor/Rendering/Frame/RenderState.h +++ b/ImguiMapEditor/Rendering/Frame/RenderState.h @@ -45,7 +45,7 @@ class RenderState { // === State tracking for cache invalidation === float last_zoom = 0.0f; uint8_t last_ambient_light = 255; - uint32_t last_config_hash = 0; // Hash of server light + slider for change detection + uint32_t last_config_hash = 0; // Hash of LightConfig fields affecting lighting // === Invalidation methods === diff --git a/ImguiMapEditor/Rendering/Light/LightGatherer.cpp b/ImguiMapEditor/Rendering/Light/LightGatherer.cpp index 99ecef3..71a9fdb 100644 --- a/ImguiMapEditor/Rendering/Light/LightGatherer.cpp +++ b/ImguiMapEditor/Rendering/Light/LightGatherer.cpp @@ -3,6 +3,7 @@ #include #include "Core/Config.h" +#include "Rendering/Light/LightProjection.h" #include "Services/ClientDataService.h" #include "Domain/Tile.h" #include "Domain/Item.h" @@ -17,13 +18,6 @@ namespace { constexpr int LIGHT_COLLECTION_MARGIN_TILES = 16; -int32_t projectedFloorOffsetTiles(int16_t current_floor, int16_t tile_floor) { - if (tile_floor <= Config::Map::GROUND_LAYER) { - return Config::Map::GROUND_LAYER - tile_floor; - } - return current_floor - tile_floor; -} - bool blocksLightFromBelow(const Domain::ItemType* item_type) { if (!item_type) return false; const bool is_ground_tile = @@ -235,7 +229,10 @@ void LightGatherer::gatherLightsForViewportFloor( const int32_t projected_x = tile->getX() - floor_offset; const int32_t projected_y = tile->getY() - floor_offset; - addLight(light_buffer, projected_x, projected_y, 215, 1, floor); + addLight(light_buffer, projected_x, projected_y, + Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR, + Config::Lighting::TRANSLUCENT_PROPAGATION_INTENSITY, + floor); }); } } diff --git a/ImguiMapEditor/Rendering/Light/LightManager.cpp b/ImguiMapEditor/Rendering/Light/LightManager.cpp index ca21f81..16f839b 100644 --- a/ImguiMapEditor/Rendering/Light/LightManager.cpp +++ b/ImguiMapEditor/Rendering/Light/LightManager.cpp @@ -7,23 +7,13 @@ #include "LightColorPalette.h" #include "Core/Config.h" +#include "Rendering/Light/LightProjection.h" #include "Services/ClientDataService.h" #include "Rendering/Visibility/FloorVisibilityCalculator.h" namespace MapEditor { namespace Rendering { -namespace { - -int32_t projectedFloorOffsetTiles(int16_t current_floor, int16_t tile_floor) { - if (tile_floor <= Config::Map::GROUND_LAYER) { - return Config::Map::GROUND_LAYER - tile_floor; - } - return current_floor - tile_floor; -} - -} // namespace - LightManager::LightManager(Services::ClientDataService* client_data) : client_data_(client_data) { @@ -206,7 +196,9 @@ void LightManager::computeViewportLight(const ViewportLightBuffer& light_buffer, const Domain::LightConfig& config) { const bool above_ground = config.camera_floor <= Config::Map::GROUND_LAYER; - const uint8_t ambient_color = above_ground ? config.global_light.color : 215; + const uint8_t ambient_color = above_ground + ? config.global_light.color + : Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR; const uint8_t ambient_intensity = above_ground ? std::max(config.client_slider, config.global_light.intensity) : config.client_slider; diff --git a/ImguiMapEditor/Rendering/Light/LightProjection.h b/ImguiMapEditor/Rendering/Light/LightProjection.h new file mode 100644 index 0000000..c4270c2 --- /dev/null +++ b/ImguiMapEditor/Rendering/Light/LightProjection.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "Core/Config.h" + +namespace MapEditor::Rendering { + +inline int32_t projectedFloorOffsetTiles(int16_t current_floor, + int16_t tile_floor) { + if (tile_floor <= Config::Map::GROUND_LAYER) { + return Config::Map::GROUND_LAYER - tile_floor; + } + return current_floor - tile_floor; +} + +} // namespace MapEditor::Rendering diff --git a/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp b/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp index 893cfa9..c14cbcc 100644 --- a/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp +++ b/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp @@ -232,10 +232,8 @@ void IngamePreviewRenderer::render(const Domain::ChunkedMap &map, config.ambient_color = config.global_light.color; config.ambient_level = config.global_light.intensity; - // Auto-invalidate if config changed - uint32_t config_hash = static_cast(config.global_light.intensity) | - (static_cast(config.global_light.color) << 8) | - (static_cast(config.client_slider) << 16); + // Auto-invalidate if lighting config changes + uint32_t config_hash = config.computeHash(); if (config_hash != last_config_hash_) { light_manager_->invalidateAll(); last_config_hash_ = config_hash; @@ -247,8 +245,8 @@ void IngamePreviewRenderer::render(const Domain::ChunkedMap &map, player_lights.push_back(Domain::LightSource{ .x = static_cast(camera_x), .y = static_cast(camera_y), - .color = static_cast(215), - .intensity = static_cast(2), + .color = Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR, + .intensity = Config::Lighting::DEFAULT_PLAYER_LIGHT_INTENSITY, .source_floor = static_cast(floor) }); diff --git a/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h b/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h index 3ebcd4f..d8eddf0 100644 --- a/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h +++ b/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h @@ -67,7 +67,6 @@ class IngamePreviewRenderer { // Light system (independent from MapRenderer) std::unique_ptr light_manager_; - uint8_t last_ambient_light_ = 255; uint32_t last_config_hash_ = 0; // Fading state diff --git a/ImguiMapEditor/Rendering/Passes/LightingPass.cpp b/ImguiMapEditor/Rendering/Passes/LightingPass.cpp index b838167..09cf0f7 100644 --- a/ImguiMapEditor/Rendering/Passes/LightingPass.cpp +++ b/ImguiMapEditor/Rendering/Passes/LightingPass.cpp @@ -28,10 +28,8 @@ void LightingPass::render(const RenderContext &context) { config.ambient_color = config.global_light.color; config.ambient_level = config.global_light.intensity; - // Auto-invalidate if server light or slider changes - uint32_t config_hash = static_cast(config.global_light.intensity) | - (static_cast(config.global_light.color) << 8) | - (static_cast(config.client_slider) << 16); + // Auto-invalidate if lighting config changes + uint32_t config_hash = config.computeHash(); if (config_hash != context.state.last_config_hash) { context.state.light_manager->invalidateAll(); context.state.last_config_hash = config_hash; diff --git a/ImguiMapEditor/Services/ViewSettings.cpp b/ImguiMapEditor/Services/ViewSettings.cpp index a6f3500..dd0d05e 100644 --- a/ImguiMapEditor/Services/ViewSettings.cpp +++ b/ImguiMapEditor/Services/ViewSettings.cpp @@ -32,14 +32,23 @@ void ViewSettings::loadFromConfig(const ConfigService &config) { // Lighting Settings map_lighting_enabled = config.get("view.map_lighting_enabled", false); - map_ambient_light = config.get("view.map_ambient_light", 0); + map_ambient_light = config.get( + "view.map_ambient_light", Config::Lighting::DEFAULT_MINIMUM_AMBIENT); preview_lighting_enabled = config.get("view.preview_lighting_enabled", false); - preview_ambient_light = config.get("view.preview_ambient_light", 0); - server_light_intensity = static_cast(config.get("view.server_light_intensity", 200)); - server_light_color = static_cast(config.get("view.server_light_color", 215)); - preview_server_light_intensity = static_cast(config.get("view.preview_server_light_intensity", 200)); - preview_server_light_color = static_cast(config.get("view.preview_server_light_color", 215)); + preview_ambient_light = config.get( + "view.preview_ambient_light", Config::Lighting::DEFAULT_MINIMUM_AMBIENT); + server_light_intensity = static_cast(config.get( + "view.server_light_intensity", + Config::Lighting::DEFAULT_SERVER_LIGHT_INTENSITY)); + server_light_color = static_cast(config.get( + "view.server_light_color", Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR)); + preview_server_light_intensity = static_cast(config.get( + "view.preview_server_light_intensity", + Config::Lighting::DEFAULT_SERVER_LIGHT_INTENSITY)); + preview_server_light_color = static_cast(config.get( + "view.preview_server_light_color", + Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR)); show_minimap_window = config.get("view.show_minimap_window", false); show_browse_tile = config.get("view.show_browse_tile", false); diff --git a/ImguiMapEditor/Services/ViewSettings.h b/ImguiMapEditor/Services/ViewSettings.h index 4c6fe23..aede411 100644 --- a/ImguiMapEditor/Services/ViewSettings.h +++ b/ImguiMapEditor/Services/ViewSettings.h @@ -44,13 +44,16 @@ struct ViewSettings { // === Lighting Settings === bool map_lighting_enabled = false; // Enable lighting in main map viewport - int map_ambient_light = 0; // Client minimum ambient (0-255), acts as floor + int map_ambient_light = Config::Lighting::DEFAULT_MINIMUM_AMBIENT; bool preview_lighting_enabled = false; // Enable lighting in ingame preview - int preview_ambient_light = 0; // Client minimum ambient for preview (0-255) - uint8_t server_light_intensity = 200; // Server global light intensity (0-255), default bright - uint8_t server_light_color = 215; // Server global light color (8-bit palette index), default white - uint8_t preview_server_light_intensity = 200; // Server light for preview - uint8_t preview_server_light_color = 215; // Server light color for preview + int preview_ambient_light = Config::Lighting::DEFAULT_MINIMUM_AMBIENT; + uint8_t server_light_intensity = + Config::Lighting::DEFAULT_SERVER_LIGHT_INTENSITY; + uint8_t server_light_color = Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR; + uint8_t preview_server_light_intensity = + Config::Lighting::DEFAULT_SERVER_LIGHT_INTENSITY; + uint8_t preview_server_light_color = + Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR; // === Placeholders (menu only, no rendering yet) === bool show_minimap_window = false; From b680d56265484f17fb7235f7190cdc0637fb7e04 Mon Sep 17 00:00:00 2001 From: glaszczkoldre Date: Sun, 31 May 2026 21:20:57 +0200 Subject: [PATCH 5/6] refactor(rendering): force light update when injected lights present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensure dynamic preview/player lights are correctly recomputed every frame by disabling texture reuse when injected lights are active. Updated preview renderer comments to clarify the source of default player light colors. - `ImguiMapEditor/Rendering/Light/LightManager.cpp` — added `has_injected_lights` check to force recomputation when dynamic lights are present - `ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp` — [minor] updated comment to reference `DEFAULT_SERVER_LIGHT_COLOR` --- ImguiMapEditor/Rendering/Light/LightManager.cpp | 7 +++++-- ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ImguiMapEditor/Rendering/Light/LightManager.cpp b/ImguiMapEditor/Rendering/Light/LightManager.cpp index 16f839b..762af1d 100644 --- a/ImguiMapEditor/Rendering/Light/LightManager.cpp +++ b/ImguiMapEditor/Rendering/Light/LightManager.cpp @@ -111,10 +111,13 @@ void LightManager::render(const Domain::ChunkedMap& map, config.camera_floor != last_config_.camera_floor || config.enabled != last_config_.enabled; - // If bounds and config match, we can reuse the texture + const bool has_injected_lights = injected_lights && !injected_lights->empty(); + + // If bounds and config match, we can reuse the texture. + // Injected lights are dynamic preview/player lights, so they always recompute. // But we MUST recalculate screen coordinates because sub-pixel camera movement // changes where the texture is drawn, even if the integer tile range is same. - if (!bounds_changed && !config_changed && !force_update_) { + if (!has_injected_lights && !bounds_changed && !config_changed && !force_update_) { float world_x = start_x * 32.0f; float world_y = start_y * 32.0f; float screen_x = (world_x - camera_x * 32.0f) * zoom + viewport_width / 2.0f; diff --git a/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp b/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp index c14cbcc..112f2cc 100644 --- a/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp +++ b/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp @@ -239,8 +239,8 @@ void IngamePreviewRenderer::render(const Domain::ChunkedMap &map, last_config_hash_ = config_hash; } - // Player minimum light at center tile (preview only) - // Matches OTClient local player: intensity=2, color=215 (white) + // Player minimum light at center tile (preview only). The color reuses + // Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR, matching OTClient's white light. std::vector player_lights; player_lights.push_back(Domain::LightSource{ .x = static_cast(camera_x), From 181dfbf2f34cbe1fb1a0393550789ec2c8658aaf Mon Sep 17 00:00:00 2001 From: glaszczkoldre Date: Sun, 31 May 2026 21:27:36 +0200 Subject: [PATCH 6/6] feat(ui): add light color palette picker widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced numerical sliders for light color selection with a visual palette picker to improve usability and offer a preview of Tibia's 8-bit color palette. - `ImguiMapEditor/CMakeLists.txt` — added `LightColorPalettePicker.cpp` to build sources - `ImguiMapEditor/Core/Config.h` — defined `LIGHT_COLOR_COUNT` and `MAX_SERVER_LIGHT_COLOR` constants - `ImguiMapEditor/Presentation/MainWindow.cpp` — integrated `LightColorPalettePicker` into main editor light settings - `ImguiMapEditor/UI/Widgets/LightColorPalettePicker.cpp` — implemented popup widget with palette grid, swatches, and hover tooltips - `ImguiMapEditor/UI/Widgets/LightColorPalettePicker.h` — defined `LightColorPalettePicker` interface - `ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp` — replaced slider with `LightColorPalettePicker` in ingame box settings --- ImguiMapEditor/CMakeLists.txt | 1 + ImguiMapEditor/Core/Config.h | 2 + ImguiMapEditor/Presentation/MainWindow.cpp | 14 +- .../UI/Widgets/LightColorPalettePicker.cpp | 158 ++++++++++++++++++ .../UI/Widgets/LightColorPalettePicker.h | 12 ++ ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp | 14 +- 6 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 ImguiMapEditor/UI/Widgets/LightColorPalettePicker.cpp create mode 100644 ImguiMapEditor/UI/Widgets/LightColorPalettePicker.h diff --git a/ImguiMapEditor/CMakeLists.txt b/ImguiMapEditor/CMakeLists.txt index b3ea327..ba4e427 100644 --- a/ImguiMapEditor/CMakeLists.txt +++ b/ImguiMapEditor/CMakeLists.txt @@ -295,6 +295,7 @@ set(UI_SOURCES UI/Ribbon/RibbonController.cpp UI/Widgets/Properties/PropertyPanelRenderer.cpp UI/Widgets/Properties/PropertyWidgets.cpp + UI/Widgets/LightColorPalettePicker.cpp UI/Widgets/QuickSearchPopup.cpp UI/Widgets/SearchResultsWidget.cpp UI/Widgets/TilesetWidget.cpp diff --git a/ImguiMapEditor/Core/Config.h b/ImguiMapEditor/Core/Config.h index 20a86a4..4ae18f0 100644 --- a/ImguiMapEditor/Core/Config.h +++ b/ImguiMapEditor/Core/Config.h @@ -87,6 +87,8 @@ inline constexpr uint16_t DEFAULT_MAP_SIZE = 16384; // LIGHTING CONFIGURATION // ============================================================================ namespace Lighting { +inline constexpr int LIGHT_COLOR_COUNT = 216; +inline constexpr uint8_t MAX_SERVER_LIGHT_COLOR = 215; inline constexpr uint8_t DEFAULT_SERVER_LIGHT_INTENSITY = 200; inline constexpr uint8_t DEFAULT_SERVER_LIGHT_COLOR = 215; inline constexpr uint8_t DEFAULT_MINIMUM_AMBIENT = 0; diff --git a/ImguiMapEditor/Presentation/MainWindow.cpp b/ImguiMapEditor/Presentation/MainWindow.cpp index 2cc8021..fa94ace 100644 --- a/ImguiMapEditor/Presentation/MainWindow.cpp +++ b/ImguiMapEditor/Presentation/MainWindow.cpp @@ -2,6 +2,7 @@ #include "Rendering/Frame/RenderingManager.h" #include "Services/ClipboardService.h" #include "UI/Core/Theme.h" +#include "UI/Widgets/LightColorPalettePicker.h" #include #include #include @@ -142,10 +143,15 @@ void MainWindow::renderEditor(Domain::ChunkedMap *current_map, view_settings_.server_light_intensity = static_cast(intensity); } ImGui::SameLine(); - ImGui::SetNextItemWidth(60); - int color = static_cast(view_settings_.server_light_color); - if (ImGui::SliderInt("Color", &color, 0, 215)) { - view_settings_.server_light_color = static_cast(color); + ImGui::TextUnformatted("Color"); + ImGui::SameLine(); + uint8_t color = view_settings_.server_light_color; + if (UI::LightColorPalettePicker( + "##serverLightColor", + color, + true, + "Server Color: world light color from the Tibia 8-bit palette")) { + view_settings_.server_light_color = color; } ImGui::SameLine(); ImGui::SetNextItemWidth(120); diff --git a/ImguiMapEditor/UI/Widgets/LightColorPalettePicker.cpp b/ImguiMapEditor/UI/Widgets/LightColorPalettePicker.cpp new file mode 100644 index 0000000..d5ce503 --- /dev/null +++ b/ImguiMapEditor/UI/Widgets/LightColorPalettePicker.cpp @@ -0,0 +1,158 @@ +#include "LightColorPalettePicker.h" + +#include + +#include + +#include "Core/Config.h" +#include "Rendering/Light/LightColorPalette.h" + +namespace MapEditor::UI { +namespace { + +constexpr int kPaletteColumns = 18; +constexpr float kCellSize = 14.0f; +constexpr float kCellPadding = 2.0f; +constexpr float kButtonSwatchWidth = 24.0f; +constexpr float kButtonSwatchHeight = 16.0f; + +ImU32 paletteColor(uint8_t color_index) { + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + Rendering::LightColorPalette::from8bit(color_index, r, g, b); + return IM_COL32(r, g, b, 255); +} + +ImU32 borderColorFor(ImU32 color) { + const int r = static_cast(color & 0xFF); + const int g = static_cast((color >> 8) & 0xFF); + const int b = static_cast((color >> 16) & 0xFF); + const int luma = (r * 299 + g * 587 + b * 114) / 1000; + return luma < 96 ? IM_COL32(200, 200, 200, 180) : IM_COL32(48, 48, 48, 180); +} + +void drawSwatch(ImDrawList& draw_list, + ImVec2 min, + ImVec2 max, + uint8_t color_index, + bool selected, + bool hovered) { + const ImU32 color = paletteColor(color_index); + draw_list.AddRectFilled(min, max, color); + draw_list.AddRect(min, max, borderColorFor(color)); + + if (selected) { + draw_list.AddRect(ImVec2(min.x - 2.0f, min.y - 2.0f), + ImVec2(max.x + 2.0f, max.y + 2.0f), + ImGui::GetColorU32(ImGuiCol_NavHighlight), 0.0f, 0, 2.0f); + } else if (hovered) { + draw_list.AddRect(ImVec2(min.x - 1.0f, min.y - 1.0f), + ImVec2(max.x + 1.0f, max.y + 1.0f), + IM_COL32(255, 255, 255, 128)); + } +} + +void drawCurrentColorButton(const char* id, uint8_t color, bool show_index) { + const float height = ImGui::GetFrameHeight(); + const float width = show_index ? 64.0f : 40.0f; + const ImVec2 start = ImGui::GetCursorScreenPos(); + + ImGui::InvisibleButton(id, ImVec2(width, height)); + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + const ImVec2 min = ImGui::GetItemRectMin(); + const ImVec2 max = ImGui::GetItemRectMax(); + const bool active = ImGui::IsItemActive(); + const bool hovered = ImGui::IsItemHovered(); + const ImU32 button_color = ImGui::GetColorU32( + active ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + + draw_list->AddRectFilled(min, max, button_color, ImGui::GetStyle().FrameRounding); + draw_list->AddRect(min, max, ImGui::GetColorU32(ImGuiCol_Border), ImGui::GetStyle().FrameRounding); + + const ImVec2 swatch_min(start.x + 6.0f, start.y + (height - kButtonSwatchHeight) * 0.5f); + const ImVec2 swatch_max(swatch_min.x + kButtonSwatchWidth, swatch_min.y + kButtonSwatchHeight); + drawSwatch(*draw_list, swatch_min, swatch_max, color, false, false); + + if (show_index) { + char label[8] = {}; + std::snprintf(label, sizeof(label), "%u", static_cast(color)); + const ImVec2 text_size = ImGui::CalcTextSize(label); + const ImVec2 text_pos(swatch_max.x + 6.0f, start.y + (height - text_size.y) * 0.5f); + draw_list->AddText(text_pos, ImGui::GetColorU32(ImGuiCol_Text), label); + } +} + +} // namespace + +bool LightColorPalettePicker(const char* id, uint8_t& color, bool show_index, const char* tooltip) { + bool changed = false; + if (color > Config::Lighting::MAX_SERVER_LIGHT_COLOR) { + color = Config::Lighting::MAX_SERVER_LIGHT_COLOR; + changed = true; + } + + ImGui::PushID(id); + drawCurrentColorButton("button", color, show_index); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + ImGui::OpenPopup("light_color_palette"); + } + if (ImGui::IsItemHovered() && tooltip) { + ImGui::SetTooltip("%s: %u", tooltip, static_cast(color)); + } + + if (ImGui::BeginPopup("light_color_palette")) { + if (tooltip) { + ImGui::TextUnformatted(tooltip); + ImGui::Separator(); + } + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(kCellPadding, kCellPadding)); + for (int index = 0; index < Config::Lighting::LIGHT_COLOR_COUNT; ++index) { + ImGui::PushID(index); + const ImVec2 swatch_min = ImGui::GetCursorScreenPos(); + ImGui::InvisibleButton("swatch", ImVec2(kCellSize, kCellSize)); + const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); + const bool hovered = ImGui::IsItemHovered(); + const auto palette_index = static_cast(index); + drawSwatch(*ImGui::GetWindowDrawList(), + swatch_min, + ImVec2(swatch_min.x + kCellSize, swatch_min.y + kCellSize), + palette_index, + color == palette_index, + hovered); + + if (hovered) { + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + Rendering::LightColorPalette::from8bit(palette_index, r, g, b); + ImGui::SetTooltip("Index %d | RGB(%u, %u, %u)", + index, + static_cast(r), + static_cast(g), + static_cast(b)); + } + + if (clicked) { + color = palette_index; + changed = true; + ImGui::CloseCurrentPopup(); + } + + ImGui::PopID(); + if ((index + 1) % kPaletteColumns != 0) { + ImGui::SameLine(); + } + } + ImGui::PopStyleVar(); + + ImGui::EndPopup(); + } + + ImGui::PopID(); + return changed; +} + +} // namespace MapEditor::UI diff --git a/ImguiMapEditor/UI/Widgets/LightColorPalettePicker.h b/ImguiMapEditor/UI/Widgets/LightColorPalettePicker.h new file mode 100644 index 0000000..f40bdf6 --- /dev/null +++ b/ImguiMapEditor/UI/Widgets/LightColorPalettePicker.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace MapEditor::UI { + +bool LightColorPalettePicker(const char* id, + uint8_t& color, + bool show_index = true, + const char* tooltip = "Server Light Color"); + +} // namespace MapEditor::UI diff --git a/ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp b/ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp index fa34a7f..7efd486 100644 --- a/ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp +++ b/ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp @@ -11,6 +11,7 @@ #include "Rendering/Map/MapRenderer.h" #include "Rendering/Passes/IngamePreviewRenderer.h" #include "Services/ViewSettings.h" +#include "UI/Widgets/LightColorPalettePicker.h" namespace MapEditor { namespace UI { @@ -83,13 +84,12 @@ void IngameBoxWindow::render(Domain::ChunkedMap* map, ImGui::SetTooltip("Server Light Intensity"); } ImGui::SameLine(); - ImGui::SetNextItemWidth(50); - int color = static_cast(settings.preview_server_light_color); - if (ImGui::SliderInt("##color", &color, 0, 215)) { - settings.preview_server_light_color = static_cast(color); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Server Light Color"); + uint8_t color = settings.preview_server_light_color; + if (LightColorPalettePicker("##previewServerLightColor", + color, + false, + "Server Color: world light color from the Tibia 8-bit palette")) { + settings.preview_server_light_color = color; } ImGui::SameLine(); ImGui::SetNextItemWidth(70);