Navmesh generation works with cell grids

This commit is contained in:
2026-04-26 14:28:54 +03:00
parent e0e8e316d4
commit ce2f6c1306
6 changed files with 241 additions and 143 deletions

View File

@@ -2,6 +2,7 @@
#include "../ui/ComponentRegistration.hpp"
#include "../ui/ProceduralMaterialEditor.hpp"
#include <OgreMaterialManager.h>
#include <OgreRTShaderSystem.h>
// Register ProceduralMaterial component
REGISTER_COMPONENT_GROUP("Procedural Material", "Material", ProceduralMaterialComponent, ProceduralMaterialEditor)
@@ -22,6 +23,12 @@ REGISTER_COMPONENT_GROUP("Procedural Material", "Material", ProceduralMaterialCo
// Clean up Ogre material - wrap in try/catch since MaterialManager may be shutting down
if (material.ogreMaterial) {
try {
Ogre::RTShader::ShaderGenerator *shaderGen =
Ogre::RTShader::ShaderGenerator::getSingletonPtr();
if (shaderGen) {
shaderGen->removeAllShaderBasedTechniques(
*material.ogreMaterial);
}
if (Ogre::MaterialManager::getSingletonPtr()) {
Ogre::MaterialManager::getSingleton().remove(material.ogreMaterial);
}

View File

@@ -18,9 +18,14 @@ static bool isNavMeshGeometry(const Ogre::String &meshName)
Ogre::String lower = meshName;
std::transform(lower.begin(), lower.end(), lower.begin(),
[](unsigned char c) { return std::tolower(c); });
static const char *exclude[] = { "wall", "ceiling", "roof",
"door", "window", "frame",
"furniture" };
// Exclude geometry that doesn't affect floor-level navigation.
// Ceilings are above walkable floors and don't block movement.
// Furniture is handled separately (or excluded to keep navmesh
// focused on architectural walkable surfaces).
// Frames placed via StaticGeometry aren't traversed by the
// SceneNode walker anyway.
static const char *exclude[] = { "ceiling", "frame", "furniture" };
for (const char *pat : exclude) {
if (lower.find(pat) != Ogre::String::npos)
return false;

View File

@@ -20,6 +20,7 @@
#include <OgreTechnique.h>
#include <OgrePass.h>
#include <OgreTextureManager.h>
#include <OgreRTShaderSystem.h>
#include <OgreMeshLodGenerator.h>
#include <ProceduralTriangleBuffer.h>
#include <ProceduralBoxGenerator.h>
@@ -2639,6 +2640,19 @@ void CellGridSystem::updateTownMaterial(flecs::entity townEntity)
pass->setAmbient(Ogre::ColourValue(0.4f, 0.4f, 0.4f));
pass->setSpecular(Ogre::ColourValue(0.1f, 0.1f, 0.1f));
// Register with RTSS so the material works on shader-only render systems
Ogre::RTShader::ShaderGenerator *shaderGen =
Ogre::RTShader::ShaderGenerator::getSingletonPtr();
if (shaderGen) {
shaderGen->createShaderBasedTechnique(
*mat,
Ogre::MaterialManager::DEFAULT_SCHEME_NAME,
Ogre::RTShader::ShaderGenerator::DEFAULT_SCHEME_NAME);
shaderGen->validateMaterial(
Ogre::RTShader::ShaderGenerator::DEFAULT_SCHEME_NAME,
town.materialName);
}
Ogre::LogManager::getSingleton().logMessage(
"CellGrid: Updated town material '" + town.materialName + "'");
}

View File

@@ -19,14 +19,13 @@ static bool isNavMeshGeometry(const Ogre::String &meshName)
std::transform(lower.begin(), lower.end(), lower.begin(),
[](unsigned char c) { return std::tolower(c); });
// Exclude vertical / enclosing geometry that causes Recast
// span-merging artefacts. When a floor span merges with a wall
// span the walkable surface is pushed to the top of the wall;
// if a roof is close above, rcFilterWalkableLowHeightSpans
// removes the whole column.
static const char *exclude[] = { "wall", "ceiling", "roof",
"door", "window", "frame",
"furniture" };
// Exclude geometry that doesn't affect floor-level navigation.
// Ceilings are above walkable floors and don't block movement.
// Furniture is handled separately (or excluded to keep navmesh
// focused on architectural walkable surfaces).
// Frames placed via StaticGeometry aren't traversed by the
// SceneNode walker anyway.
static const char *exclude[] = { "ceiling", "frame", "furniture" };
for (const char *pat : exclude) {
if (lower.find(pat) != Ogre::String::npos)
return false;

View File

@@ -67,22 +67,32 @@ void ProceduralMaterialSystem::createMaterial(
"_" + std::to_string(m_createdCount++);
}
// Destroy old material if exists
if (component.ogreMaterial) {
destroyMaterial(component);
Ogre::MaterialManager &matMgr =
Ogre::MaterialManager::getSingleton();
// Re-use the existing material object when possible.
// Destroying and recreating breaks cached MaterialPtr
// references held by Ogre::SubEntity instances that use
// this material.
if (!component.ogreMaterial) {
if (matMgr.resourceExists(component.materialName)) {
component.ogreMaterial =
matMgr.getByName(component.materialName);
} else {
component.ogreMaterial = matMgr.create(
component.materialName,
Ogre::ResourceGroupManager::
DEFAULT_RESOURCE_GROUP_NAME);
}
}
// Create new material
component.ogreMaterial =
Ogre::MaterialManager::getSingleton().create(
component.materialName,
Ogre::ResourceGroupManager::
DEFAULT_RESOURCE_GROUP_NAME);
// Get the default technique and pass
// Clear existing technique and rebuild passes so that every
// SubEntity::mMaterialPtr (which points to this same object)
// sees the updated state.
Ogre::Technique *technique =
component.ogreMaterial->getTechnique(0);
Ogre::Pass *pass = technique->getPass(0);
technique->removeAllPasses();
Ogre::Pass *pass = technique->createPass();
// Set material colors
pass->setAmbient(component.ambient[0], component.ambient[1],
@@ -116,10 +126,12 @@ void ProceduralMaterialSystem::createMaterial(
}
}
// Register with RTSS to generate shaders (fixes "RenderSystem does not support FixedFunction")
// Re-generate RTSS shaders for the updated material
Ogre::RTShader::ShaderGenerator *shaderGen =
Ogre::RTShader::ShaderGenerator::getSingletonPtr();
if (shaderGen && component.ogreMaterial) {
shaderGen->removeAllShaderBasedTechniques(
*component.ogreMaterial);
shaderGen->createShaderBasedTechnique(
*component.ogreMaterial,
Ogre::MaterialManager::DEFAULT_SCHEME_NAME,
@@ -136,7 +148,7 @@ void ProceduralMaterialSystem::createMaterial(
component.textureVersion = currentTexVersion;
Ogre::LogManager::getSingleton().logMessage(
"ProceduralMaterial: Created '" +
"ProceduralMaterial: Updated '" +
component.materialName + "'");
} catch (const Ogre::Exception &e) {
@@ -151,6 +163,15 @@ void ProceduralMaterialSystem::destroyMaterial(
{
if (component.ogreMaterial) {
try {
// Remove RTSS shader techniques before destroying the
// material, otherwise the shader generator keeps stale
// cache entries keyed by material name.
Ogre::RTShader::ShaderGenerator *shaderGen =
Ogre::RTShader::ShaderGenerator::getSingletonPtr();
if (shaderGen) {
shaderGen->removeAllShaderBasedTechniques(
*component.ogreMaterial);
}
Ogre::MaterialManager::getSingleton().remove(
component.ogreMaterial);
} catch (...) {

View File

@@ -6,128 +6,147 @@
#include <OgreImage.h>
#include <OgreLogManager.h>
void ProceduralTextureEditor::renderColorButton(int index, ProceduralTextureComponent &texture)
void ProceduralTextureEditor::renderColorButton(
int index, ProceduralTextureComponent &texture)
{
Ogre::ColourValue color = texture.getColor(index);
// Create a unique ID for this button
ImGui::PushID(index);
// Show color as a small colored button
ImVec4 buttonColor(color.r, color.g, color.b, color.a);
ImGui::ColorButton("##color", buttonColor, ImGuiColorEditFlags_NoTooltip, ImVec2(24, 24));
ImGui::ColorButton("##color", buttonColor,
ImGuiColorEditFlags_NoTooltip, ImVec2(24, 24));
// Handle click
if (ImGui::IsItemClicked()) {
m_selectedRect = index;
}
// Show tooltip with rectangle index
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Rect %d: (%.2f, %.2f, %.2f)", index, color.r, color.g, color.b);
ImGui::SetTooltip("Rect %d: (%.2f, %.2f, %.2f)", index, color.r,
color.g, color.b);
}
ImGui::PopID();
}
void ProceduralTextureEditor::renderColorGrid(flecs::entity entity, ProceduralTextureComponent &texture)
void ProceduralTextureEditor::renderColorGrid(
flecs::entity entity, ProceduralTextureComponent &texture)
{
const int gridSize = ProceduralTextureComponent::GRID_SIZE;
ImGui::Text("10x10 Grid - Click a rectangle to edit its color or add to atlas:");
ImGui::Text(
"10x10 Grid - Click a rectangle to edit its color or add to atlas:");
// Calculate grid width for centering
float buttonSize = 24.0f;
float spacing = 2.0f;
float gridWidth = gridSize * buttonSize + (gridSize - 1) * spacing;
// Get window width for centering
float windowWidth = ImGui::GetContentRegionAvail().x;
float startX = ImGui::GetCursorPosX();
float offset = (windowWidth - gridWidth) * 0.5f;
if (offset < 0) offset = 0;
if (offset < 0)
offset = 0;
// Render the grid - each row needs its own offset
for (int row = 0; row < gridSize; ++row) {
// Set cursor position for this row
ImGui::SetCursorPosX(startX + offset);
for (int col = 0; col < gridSize; ++col) {
int index = row * gridSize + col;
renderColorButton(index, texture);
if (col < gridSize - 1) {
ImGui::SameLine(0, spacing);
}
}
}
// Color picker and naming for selected rectangle
if (m_selectedRect >= 0 && m_selectedRect < ProceduralTextureComponent::RECT_COUNT) {
if (m_selectedRect >= 0 &&
m_selectedRect < ProceduralTextureComponent::RECT_COUNT) {
ImGui::Separator();
ImGui::Text("Editing Rectangle %d (Row %d, Col %d)",
m_selectedRect,
m_selectedRect / gridSize,
m_selectedRect % gridSize);
ImGui::Text("Editing Rectangle %d (Row %d, Col %d)",
m_selectedRect, m_selectedRect / gridSize,
m_selectedRect % gridSize);
// UV coordinates display
float u1, v1, u2, v2;
texture.getRectUVs(m_selectedRect, u1, v1, u2, v2);
ImGui::TextDisabled("UVs: (%.3f, %.3f) - (%.3f, %.3f)", u1, v1, u2, v2);
Ogre::ColourValue currentColor = texture.getColor(m_selectedRect);
float colorArray[4] = { currentColor.r, currentColor.g, currentColor.b, currentColor.a };
ImGui::TextDisabled("UVs: (%.3f, %.3f) - (%.3f, %.3f)", u1, v1,
u2, v2);
Ogre::ColourValue currentColor =
texture.getColor(m_selectedRect);
float colorArray[4] = { currentColor.r, currentColor.g,
currentColor.b, currentColor.a };
if (ImGui::ColorEdit4("Color", colorArray)) {
Ogre::ColourValue newColor(colorArray[0], colorArray[1], colorArray[2], colorArray[3]);
Ogre::ColourValue newColor(colorArray[0], colorArray[1],
colorArray[2],
colorArray[3]);
texture.setColor(m_selectedRect, newColor);
}
// Add to atlas section
ImGui::Separator();
ImGui::Text("Add to Texture Atlas:");
ImGui::InputText("Name", m_nameBuffer, sizeof(m_nameBuffer));
ImGui::SameLine();
if (ImGui::Button("Add")) {
if (strlen(m_nameBuffer) > 0) {
if (texture.hasNamedRect(m_nameBuffer)) {
ImGui::TextColored(ImVec4(1, 0, 0, 1), "Name already exists!");
ImGui::TextColored(
ImVec4(1, 0, 0, 1),
"Name already exists!");
} else {
texture.addNamedRect(m_nameBuffer, m_selectedRect);
m_nameBuffer[0] = '\0'; // Clear buffer
texture.addNamedRect(m_nameBuffer,
m_selectedRect);
m_nameBuffer[0] = '\0'; // Clear buffer
}
}
}
if (ImGui::Button("Deselect")) {
m_selectedRect = -1;
m_nameBuffer[0] = '\0';
}
ImGui::SameLine();
// Preset colors
ImGui::Text("Presets:");
ImGui::SameLine();
const ImVec4 presets[] = {
ImVec4(1, 1, 1, 1), // White
ImVec4(0, 0, 0, 1), // Black
ImVec4(1, 0, 0, 1), // Red
ImVec4(0, 1, 0, 1), // Green
ImVec4(0, 0, 1, 1), // Blue
ImVec4(1, 1, 0, 1), // Yellow
ImVec4(1, 0, 1, 1), // Magenta
ImVec4(0, 1, 1, 1), // Cyan
ImVec4(0.5f, 0.5f, 0.5f, 1), // Gray
ImVec4(1, 1, 1, 1), // White
ImVec4(0, 0, 0, 1), // Black
ImVec4(1, 0, 0, 1), // Red
ImVec4(0, 1, 0, 1), // Green
ImVec4(0, 0, 1, 1), // Blue
ImVec4(1, 1, 0, 1), // Yellow
ImVec4(1, 0, 1, 1), // Magenta
ImVec4(0, 1, 1, 1), // Cyan
ImVec4(0.5f, 0.5f, 0.5f, 1), // Gray
};
for (size_t i = 0; i < IM_ARRAYSIZE(presets); ++i) {
ImGui::PushID((int)i);
if (ImGui::ColorButton("##preset", presets[i], ImGuiColorEditFlags_NoTooltip, ImVec2(20, 20))) {
Ogre::ColourValue presetColor(presets[i].x, presets[i].y, presets[i].z, presets[i].w);
if (ImGui::ColorButton("##preset", presets[i],
ImGuiColorEditFlags_NoTooltip,
ImVec2(20, 20))) {
Ogre::ColourValue presetColor(presets[i].x,
presets[i].y,
presets[i].z,
presets[i].w);
texture.setColor(m_selectedRect, presetColor);
}
ImGui::PopID();
@@ -138,35 +157,40 @@ void ProceduralTextureEditor::renderColorGrid(flecs::entity entity, ProceduralTe
}
}
void ProceduralTextureEditor::renderNamedRects(flecs::entity entity, ProceduralTextureComponent &texture)
void ProceduralTextureEditor::renderNamedRects(
flecs::entity entity, ProceduralTextureComponent &texture)
{
ImGui::Separator();
ImGui::Text("Texture Atlas (Named Rectangles):");
const auto& namedRects = texture.getAllNamedRects();
const auto &namedRects = texture.getAllNamedRects();
if (namedRects.empty()) {
ImGui::TextDisabled("No named rectangles. Select a rectangle above and add it to the atlas.");
ImGui::TextDisabled(
"No named rectangles. Select a rectangle above and add it to the atlas.");
} else {
// Table header
if (ImGui::BeginTable("NamedRects", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
if (ImGui::BeginTable("NamedRects", 3,
ImGuiTableFlags_Borders |
ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("UV Coordinates");
ImGui::TableSetupColumn("Action");
ImGui::TableHeadersRow();
for (const auto& pair : namedRects) {
const auto& rect = pair.second;
for (const auto &pair : namedRects) {
const auto &rect = pair.second;
ImGui::TableNextRow();
// Name column
ImGui::TableNextColumn();
ImGui::Text("%s", rect.name.c_str());
// UVs column
ImGui::TableNextColumn();
ImGui::Text("(%.3f, %.3f) - (%.3f, %.3f)", rect.u1, rect.v1, rect.u2, rect.v2);
ImGui::Text("(%.3f, %.3f) - (%.3f, %.3f)",
rect.u1, rect.v1, rect.u2, rect.v2);
// Action column
ImGui::TableNextColumn();
ImGui::PushID(rect.name.c_str());
@@ -175,49 +199,62 @@ void ProceduralTextureEditor::renderNamedRects(flecs::entity entity, ProceduralT
}
ImGui::PopID();
}
ImGui::EndTable();
}
ImGui::TextDisabled("Total: %zu named rectangles", namedRects.size());
ImGui::TextDisabled("Total: %zu named rectangles",
namedRects.size());
}
}
bool ProceduralTextureEditor::renderComponent(flecs::entity entity, ProceduralTextureComponent &texture)
bool ProceduralTextureEditor::renderComponent(
flecs::entity entity, ProceduralTextureComponent &texture)
{
bool modified = false;
if (ImGui::CollapsingHeader("Procedural Texture", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::CollapsingHeader("Procedural Texture",
ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent();
// Texture info
if (texture.generated) {
ImGui::TextColored(ImVec4(0, 1, 0, 1), "Status: Generated");
ImGui::TextColored(ImVec4(0, 1, 0, 1),
"Status: Generated");
ImGui::Text("Texture: %s", texture.textureName.c_str());
ImGui::Text("Size: %dx%d", texture.textureSize, texture.textureSize);
ImGui::Text("Size: %dx%d", texture.textureSize,
texture.textureSize);
// Save PNG button
if (ImGui::Button("Save as PNG...")) {
ImGui::OpenPopup("SaveTexturePopup");
}
// Save popup
if (ImGui::BeginPopup("SaveTexturePopup")) {
static char filename[256] = "procedural_texture.png";
ImGui::InputText("Filename", filename, sizeof(filename));
static char filename[256] =
"procedural_texture.png";
ImGui::InputText("Filename", filename,
sizeof(filename));
if (ImGui::Button("Save", ImVec2(120, 0))) {
try {
if (texture.ogreTexture) {
Ogre::Image image;
texture.ogreTexture->convertToImage(image);
texture.ogreTexture
->convertToImage(
image);
image.save(filename);
Ogre::LogManager::getSingleton().logMessage(
"ProceduralTexture: Saved to " + std::string(filename));
Ogre::LogManager::getSingleton()
.logMessage(
"ProceduralTexture: Saved to " +
std::string(
filename));
}
} catch (const Ogre::Exception& e) {
} catch (const Ogre::Exception &e) {
Ogre::LogManager::getSingleton().logMessage(
"ProceduralTexture ERROR: Failed to save: " + e.getDescription());
"ProceduralTexture ERROR: Failed to save: " +
e.getDescription());
}
ImGui::CloseCurrentPopup();
}
@@ -228,91 +265,106 @@ bool ProceduralTextureEditor::renderComponent(flecs::entity entity, ProceduralTe
ImGui::EndPopup();
}
} else if (texture.dirty) {
ImGui::TextColored(ImVec4(1, 1, 0, 1), "Status: Needs Generation");
ImGui::TextColored(ImVec4(1, 1, 0, 1),
"Status: Needs Generation");
} else {
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1), "Status: Not Generated");
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1),
"Status: Not Generated");
}
ImGui::Separator();
// Texture size selector
const int sizes[] = { 128, 256, 512, 1024, 2048 };
int currentSizeIndex = 2; // Default 512
int currentSizeIndex = 2; // Default 512
for (int i = 0; i < IM_ARRAYSIZE(sizes); ++i) {
if (sizes[i] == texture.textureSize) {
currentSizeIndex = i;
break;
}
}
const char* sizeNames[] = { "128x128", "256x256", "512x512", "1024x1024", "2048x2048" };
if (ImGui::Combo("Texture Size", &currentSizeIndex, sizeNames, IM_ARRAYSIZE(sizeNames))) {
const char *sizeNames[] = { "128x128", "256x256", "512x512",
"1024x1024", "2048x2048" };
if (ImGui::Combo("Texture Size", &currentSizeIndex, sizeNames,
IM_ARRAYSIZE(sizeNames))) {
texture.textureSize = sizes[currentSizeIndex];
texture.markDirty();
modified = true;
}
// UV Margin control to prevent color bleeding
ImGui::Separator();
ImGui::Text("UV Margin (prevents color bleeding):");
if (ImGui::DragFloat("Margin", &texture.uvMargin, 0.001f, 0.01f, 0.025f, "%.3f")) {
if (ImGui::DragFloat("Margin", &texture.uvMargin, 0.001f, 0.01f,
0.035f, "%.3f")) {
// Clamp to valid range
if (texture.uvMargin < 0.01f) texture.uvMargin = 0.01f;
if (texture.uvMargin > 0.025f) texture.uvMargin = 0.025f;
if (texture.uvMargin < 0.01f)
texture.uvMargin = 0.01f;
if (texture.uvMargin > 0.035f)
texture.uvMargin = 0.035f;
texture.markDirty();
modified = true;
}
ImGui::TextDisabled("Default: 0.010, Range: 0.010-0.025, Step: 0.001");
ImGui::TextDisabled("Higher values = more padding, less color bleeding");
ImGui::TextDisabled(
"Default: 0.010, Range: 0.010-0.035, Step: 0.001");
ImGui::TextDisabled(
"Higher values = more padding, less color bleeding");
ImGui::Separator();
// Color grid
renderColorGrid(entity, texture);
// Named rectangles list
renderNamedRects(entity, texture);
ImGui::Separator();
// Global actions
if (ImGui::Button("Randomize Colors")) {
for (int i = 0; i < ProceduralTextureComponent::RECT_COUNT; ++i) {
for (int i = 0;
i < ProceduralTextureComponent::RECT_COUNT; ++i) {
Ogre::ColourValue randomColor(
(float)rand() / RAND_MAX,
(float)rand() / RAND_MAX,
(float)rand() / RAND_MAX,
1.0f
);
(float)rand() / RAND_MAX, 1.0f);
texture.setColor(i, randomColor);
}
modified = true;
}
ImGui::SameLine();
if (ImGui::Button("Reset to Checkerboard")) {
for (int i = 0; i < ProceduralTextureComponent::RECT_COUNT; ++i) {
int row = i / ProceduralTextureComponent::GRID_SIZE;
int col = i % ProceduralTextureComponent::GRID_SIZE;
for (int i = 0;
i < ProceduralTextureComponent::RECT_COUNT; ++i) {
int row = i /
ProceduralTextureComponent::GRID_SIZE;
int col = i %
ProceduralTextureComponent::GRID_SIZE;
bool isWhite = (row + col) % 2 == 0;
Ogre::ColourValue color(isWhite ? 1.0f : 0.0f, isWhite ? 1.0f : 0.0f, isWhite ? 1.0f : 0.0f, 1.0f);
Ogre::ColourValue color(isWhite ? 1.0f : 0.0f,
isWhite ? 1.0f : 0.0f,
isWhite ? 1.0f : 0.0f,
1.0f);
texture.setColor(i, color);
}
modified = true;
}
ImGui::SameLine();
if (ImGui::Button("All White")) {
for (int i = 0; i < ProceduralTextureComponent::RECT_COUNT; ++i) {
for (int i = 0;
i < ProceduralTextureComponent::RECT_COUNT; ++i) {
texture.setColor(i, Ogre::ColourValue::White);
}
modified = true;
}
ImGui::Unindent();
}
return modified;
}