Implemented terrain system core
This commit is contained in:
@@ -151,8 +151,9 @@ to avoid stuck keys caused by missed `keyReleased` events. One-shot action flags
|
||||
`animation_tree.json`. Each `AnimationTreeComponent` references a registry
|
||||
tree by name only.
|
||||
- Trees are authored in **Tools -> Animation Tree Registry**. Each registry
|
||||
entry can select a skeleton source mesh from the `Characters` resource group;
|
||||
this is used only by the editor to populate animation-name dropdowns.
|
||||
entry can select a skeleton source from the `Characters` resource group (one
|
||||
representative mesh per skeleton); this is used only by the editor to
|
||||
populate animation-name dropdowns.
|
||||
- `AnimationTreeSystem` resolves the tree from the registry each frame and
|
||||
evaluates it.
|
||||
- Selects a root bone (`Root`, `mixamorig:Hips`, then `Spineroot`), freezes it,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
project(editScene)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
find_package(OGRE REQUIRED COMPONENTS Bites Overlay MeshLodGenerator CONFIG)
|
||||
find_package(OGRE REQUIRED COMPONENTS Bites Overlay MeshLodGenerator Paging Terrain CONFIG)
|
||||
find_package(flecs REQUIRED CONFIG)
|
||||
find_package(nlohmann_json REQUIRED)
|
||||
find_package(SDL2 REQUIRED)
|
||||
@@ -23,6 +23,7 @@ set(EDITSCENE_SOURCES
|
||||
systems/EditorSunSystem.cpp
|
||||
systems/EditorSkyboxSystem.cpp
|
||||
systems/EditorWaterPlaneSystem.cpp
|
||||
systems/TerrainSystem.cpp
|
||||
systems/LightSystem.cpp
|
||||
systems/CameraSystem.cpp
|
||||
systems/LodSystem.cpp
|
||||
@@ -163,6 +164,7 @@ set(EDITSCENE_SOURCES
|
||||
components/BuoyancyInfoModule.cpp
|
||||
components/WaterPhysicsModule.cpp
|
||||
components/WaterPlaneModule.cpp
|
||||
components/TerrainModule.cpp
|
||||
components/SunModule.cpp
|
||||
components/SkyboxModule.cpp
|
||||
camera/EditorCamera.cpp
|
||||
@@ -193,6 +195,7 @@ set(EDITSCENE_HEADERS
|
||||
components/BuoyancyInfo.hpp
|
||||
components/WaterPhysics.hpp
|
||||
components/WaterPlane.hpp
|
||||
components/Terrain.hpp
|
||||
components/Sun.hpp
|
||||
components/Skybox.hpp
|
||||
components/Light.hpp
|
||||
@@ -284,6 +287,7 @@ set(EDITSCENE_HEADERS
|
||||
systems/EditorSunSystem.hpp
|
||||
systems/EditorSkyboxSystem.hpp
|
||||
systems/EditorWaterPlaneSystem.hpp
|
||||
systems/TerrainSystem.hpp
|
||||
systems/LightSystem.hpp
|
||||
systems/CameraSystem.hpp
|
||||
systems/LodSystem.hpp
|
||||
@@ -378,6 +382,8 @@ target_link_libraries(editSceneEditor
|
||||
OgreBites
|
||||
OgreOverlay
|
||||
OgreMeshLodGenerator
|
||||
OgrePaging
|
||||
OgreTerrain
|
||||
flecs::flecs_static
|
||||
nlohmann_json::nlohmann_json
|
||||
Jolt::Jolt
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "systems/EditorSunSystem.hpp"
|
||||
#include "systems/EditorSkyboxSystem.hpp"
|
||||
#include "systems/EditorWaterPlaneSystem.hpp"
|
||||
#include "systems/TerrainSystem.hpp"
|
||||
#include "systems/LightSystem.hpp"
|
||||
#include "systems/CameraSystem.hpp"
|
||||
#include "systems/LodSystem.hpp"
|
||||
@@ -60,6 +61,7 @@
|
||||
#include "components/InWater.hpp"
|
||||
#include "components/WaterPhysics.hpp"
|
||||
#include "components/WaterPlane.hpp"
|
||||
#include "components/Terrain.hpp"
|
||||
#include "components/Sun.hpp"
|
||||
#include "components/Skybox.hpp"
|
||||
#include "components/Light.hpp"
|
||||
@@ -356,6 +358,13 @@ void EditorApp::setup()
|
||||
m_waterPlaneSystem = std::make_unique<EditorWaterPlaneSystem>(
|
||||
m_world, m_sceneMgr);
|
||||
|
||||
// TerrainSystem — owns Ogre terrain singletons, needs camera for paging.
|
||||
{
|
||||
Ogre::Camera *cam = m_camera ? m_camera->getCamera() : nullptr;
|
||||
m_terrainSystem = std::make_unique<TerrainSystem>(
|
||||
m_world, m_sceneMgr, cam);
|
||||
}
|
||||
|
||||
// Apply debug setting if it was set before system creation
|
||||
if (m_debugBuoyancy) {
|
||||
m_buoyancySystem->setDebugEnabled(true);
|
||||
@@ -1195,6 +1204,7 @@ void EditorApp::setupECS()
|
||||
m_world.component<BuoyancyInfo>();
|
||||
m_world.component<WaterPhysics>();
|
||||
m_world.component<WaterPlane>();
|
||||
m_world.component<TerrainComponent>();
|
||||
|
||||
// Register light and camera components
|
||||
m_world.component<LightComponent>();
|
||||
@@ -1440,6 +1450,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
m_proceduralMeshSystem->update();
|
||||
}
|
||||
|
||||
/* --- Terrain update (before static world so navmesh can use terrain) --- */
|
||||
if (m_terrainSystem) {
|
||||
m_terrainSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Static world generation (meshes + physics) --- */
|
||||
if (m_roomLayoutSystem) {
|
||||
m_roomLayoutSystem->update();
|
||||
|
||||
@@ -40,6 +40,7 @@ class EditorSunSystem;
|
||||
class EditorSkyboxSystem;
|
||||
class EditorWaterPlaneSystem;
|
||||
class NormalDebugSystem;
|
||||
class TerrainSystem;
|
||||
class SmartObjectSystem;
|
||||
class GoapRunnerSystem;
|
||||
class PathFollowingSystem;
|
||||
@@ -266,6 +267,7 @@ private:
|
||||
std::unique_ptr<EditorSunSystem> m_sunSystem;
|
||||
std::unique_ptr<EditorSkyboxSystem> m_skyboxSystem;
|
||||
std::unique_ptr<EditorWaterPlaneSystem> m_waterPlaneSystem;
|
||||
std::unique_ptr<TerrainSystem> m_terrainSystem;
|
||||
std::unique_ptr<EditorLightSystem> m_lightSystem;
|
||||
std::unique_ptr<EditorCameraSystem> m_cameraSystem;
|
||||
std::unique_ptr<EditorLodSystem> m_lodSystem;
|
||||
|
||||
@@ -274,19 +274,26 @@ Sources (in order of priority):
|
||||
Page-local sampling in `define()`:
|
||||
|
||||
```cpp
|
||||
// convertTerrainSlotToWorldPosition returns the MINIMUM CORNER of page (x,y),
|
||||
// NOT the center. Vertex (i,j) maps to:
|
||||
// world.x = corner.x + i * worldSize / (terrainSize-1)
|
||||
// world.z = corner.z + j * worldSize / (terrainSize-1)
|
||||
Ogre::Real worldSize = terrainGroup->getTerrainWorldSize();
|
||||
Ogre::Real step = worldSize / (Ogre::Real)(terrainSize - 1);
|
||||
Ogre::Vector3 worldPos;
|
||||
terrainGroup->convertTerrainSlotToWorldPosition(x, y, &worldPos);
|
||||
for (int z = 0; z < terrainSize; ++z) {
|
||||
for (int x = 0; x < terrainSize; ++x) {
|
||||
long wx = (long)(worldPos.x + x - (terrainSize - 1) / 2);
|
||||
long wz = (long)(worldPos.z + z - (terrainSize - 1) / 2);
|
||||
long wx = (long)(worldPos.x + (Ogre::Real)x * step);
|
||||
long wz = (long)(worldPos.z + (Ogre::Real)z * step);
|
||||
heightMap[z * terrainSize + x] = getHeightAtWorld(wx, wz);
|
||||
}
|
||||
}
|
||||
terrainGroup->defineTerrain(x, y, heightMap);
|
||||
```
|
||||
|
||||
Note: Ogre's internal `getHeightData(x, y)` uses `index = y * size + x`, so the
|
||||
Note: Ogre's internal `getHeightData(x, y)` uses `index = y * size + x`, so row
|
||||
index `z` in the buffer corresponds to world Z.
|
||||
row index in the buffer corresponds to world Z.
|
||||
|
||||
### 2.5 RTShader and material configuration
|
||||
@@ -453,6 +460,41 @@ m_buoyancySystem.reset();
|
||||
m_physicsSystem.reset();
|
||||
```
|
||||
|
||||
|
||||
### 2.9 EditorUISystem wiring
|
||||
|
||||
|
||||
|
||||
`EditorUISystem::renderComponentList()` hardcodes each component type with
|
||||
|
||||
explicit `entity.has<ComponentType>()` checks. Add the following after the
|
||||
|
||||
WaterPlane block in `src/features/editScene/systems/EditorUISystem.cpp`:
|
||||
|
||||
|
||||
|
||||
```cpp
|
||||
|
||||
#include "../components/Terrain.hpp" // at top with other includes
|
||||
|
||||
|
||||
|
||||
// In renderComponentList(), after WaterPlane:
|
||||
|
||||
// Render Terrain if present
|
||||
|
||||
if (entity.has<TerrainComponent>()) {
|
||||
|
||||
auto &tc = entity.get_mut<TerrainComponent>();
|
||||
|
||||
m_componentRegistry.render<TerrainComponent>(entity, tc);
|
||||
|
||||
componentCount++;
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 3. ZigzagHeightfieldShape (custom Jolt collision shape)
|
||||
|
||||
`JPH::HeightFieldShape` is plain row-major and always splits quads along the
|
||||
@@ -1102,21 +1144,20 @@ target_link_libraries(editSceneEditor PUBLIC
|
||||
Definition of done: a paged terrain renders in the editor; terrain pages load
|
||||
and unload as the camera moves.
|
||||
|
||||
### Milestone 2 — Serialization + physics integration
|
||||
- Scene serialization for `TerrainComponent` (save/load round-trip).
|
||||
### Milestone 2 — Physics integration
|
||||
- Implement `ZigzagHeightfieldShape` as a `JPH::MeshShape` per page with zigzag
|
||||
triangulation matching Ogre.
|
||||
- Queue collider creation after page load and derived-data completion.
|
||||
- Maintain `mColliders` map; safe remove/re-add on scene clear or component
|
||||
removal.
|
||||
- Add raycast helper to `TerrainSystem` for gameplay/editor tools.
|
||||
|
||||
Definition of done: rigid bodies and characters collide flush with the visible
|
||||
terrain; removing/re-adding the terrain entity is leak-free.
|
||||
|
||||
### Milestone 3 — Serialization + heightmap editing + splat painting
|
||||
- Scene serialization for `TerrainComponent` (save/load JSON round-trip).
|
||||
- Binary heightmap file save/load alongside scene JSON.
|
||||
|
||||
Definition of done: terrain saves/loads from JSON; rigid bodies and characters
|
||||
collide flush with the visible terrain; removing/re-adding the terrain entity
|
||||
is leak-free.
|
||||
|
||||
### Milestone 3 — Heightmap editing + splat painting
|
||||
- Brush tools (raise/lower/smooth/flatten) with curve-based brush profile.
|
||||
- Splat paint brush (select layer, set opacity, paint blend values via
|
||||
`TerrainLayerBlendMap`).
|
||||
@@ -1128,8 +1169,9 @@ is leak-free.
|
||||
- `AuxMap` infrastructure (create, load, save, edit with brushes).
|
||||
- Fixup chunk cleanup (clear all, delete individual chunks by world position).
|
||||
|
||||
Definition of done: user can sculpt the terrain in the editor and save/load the
|
||||
result; visual and collision update after sculpting.
|
||||
Definition of done: user can sculpt the terrain in the editor; save scene,
|
||||
reload, terrain restored with edited heightmap and layer paint; visual and
|
||||
collision update after sculpting.
|
||||
|
||||
### Milestone 4 — Procedural roads
|
||||
- Node/edge editing UI (add, remove, split, join, select, move).
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
#ifndef EDITSCENE_TERRAIN_HPP
|
||||
#define EDITSCENE_TERRAIN_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* Terrain component — singleton ECS component holding serializable
|
||||
* parameters for the Ogre::Terrain-based terrain feature.
|
||||
*
|
||||
* Only one entity per scene should carry this component. All runtime
|
||||
* Ogre/Jolt objects live in TerrainSystem, not here.
|
||||
*/
|
||||
struct TerrainComponent {
|
||||
bool enabled = true;
|
||||
|
||||
// Page/tile vertex resolution. Must be 2^n+1 (e.g. 33, 65, 129, 257).
|
||||
int terrainSize = 65;
|
||||
|
||||
// Stable unique ID — generated once at component creation, serialized,
|
||||
// never changed. Used as the directory key for binary terrain data.
|
||||
uint64_t terrainId = 0;
|
||||
|
||||
// Base binary heightmap resolution. Default 256x256.
|
||||
int heightmapSize = 256;
|
||||
|
||||
// World-space size of one Ogre terrain page in units.
|
||||
float worldSize = 2000.0f;
|
||||
|
||||
// Maximum geometric error in pixels before a LOD tile splits.
|
||||
float maxPixelError = 1.0f;
|
||||
|
||||
// Distance at which the cheap composite map is used.
|
||||
float compositeMapDistance = 300.0f;
|
||||
|
||||
// Minimum and maximum batch LOD sizes. Must be 2^n+1 and <= terrainSize.
|
||||
int minBatchSize = 17;
|
||||
int maxBatchSize = 65;
|
||||
|
||||
// Base heightmap binary file path.
|
||||
std::string heightmapFile = "heightmap.bin";
|
||||
|
||||
// Layer texture settings (diffuse+normal pairs).
|
||||
struct Layer {
|
||||
std::string diffuseTexture;
|
||||
std::string normalTexture;
|
||||
float worldSize = 100.0f;
|
||||
};
|
||||
std::vector<Layer> layers;
|
||||
|
||||
// Road network data (node/edge graph).
|
||||
struct RoadNode {
|
||||
Ogre::Vector3 position;
|
||||
float verticalOffset = 0.0f;
|
||||
int id = 0;
|
||||
};
|
||||
struct RoadEdge {
|
||||
int nodeA = -1;
|
||||
int nodeB = -1;
|
||||
float roadLevelA = 0.0f;
|
||||
float roadLevelB = 0.0f;
|
||||
int lanesPerDirectionOverride = 0;
|
||||
int lanesAtoB = 0;
|
||||
int lanesBtoA = 0;
|
||||
struct RoadSidePrefab {
|
||||
std::string prefabPath;
|
||||
float edgeT = 0.5f;
|
||||
float sideOffset = 5.0f;
|
||||
bool leftSide = true;
|
||||
};
|
||||
std::vector<RoadSidePrefab> sidePrefabs;
|
||||
};
|
||||
std::vector<RoadNode> roadNodes;
|
||||
std::vector<RoadEdge> roadEdges;
|
||||
|
||||
// Auxiliary maps (foliage density, material masks, etc.).
|
||||
struct AuxMap {
|
||||
std::string name;
|
||||
std::string fileName;
|
||||
int resolution = 256;
|
||||
float defaultValue = 0.0f;
|
||||
};
|
||||
std::vector<AuxMap> auxMaps;
|
||||
|
||||
// Runtime-only: managed by TerrainSystem. Not serialized.
|
||||
bool dirty = true;
|
||||
bool rebuildInProgress = false;
|
||||
|
||||
void markDirty()
|
||||
{
|
||||
dirty = true;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_TERRAIN_HPP
|
||||
@@ -0,0 +1,24 @@
|
||||
#include "Terrain.hpp"
|
||||
#include "Transform.hpp"
|
||||
#include "EditorMarker.hpp"
|
||||
#include "EntityName.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/TerrainEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Terrain", "Environment", TerrainComponent,
|
||||
TerrainEditor)
|
||||
{
|
||||
registry.registerComponent<TerrainComponent>(
|
||||
"Terrain", "Environment",
|
||||
std::make_unique<TerrainEditor>(),
|
||||
/* Adder */
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<TerrainComponent>())
|
||||
e.set<TerrainComponent>({});
|
||||
},
|
||||
/* Remover */
|
||||
[](flecs::entity e) {
|
||||
if (e.has<TerrainComponent>())
|
||||
e.remove<TerrainComponent>();
|
||||
});
|
||||
}
|
||||
@@ -107,23 +107,55 @@ void AnimationTreeRegistry::markModified(const Ogre::String &name)
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Ogre::String>
|
||||
std::vector<AnimationTreeRegistry::SkeletonSource>
|
||||
AnimationTreeRegistry::getAvailableSkeletonSources() const
|
||||
{
|
||||
std::vector<Ogre::String> result;
|
||||
std::vector<SkeletonSource> result;
|
||||
Ogre::ResourceGroupManager &rgm =
|
||||
Ogre::ResourceGroupManager::getSingleton();
|
||||
try {
|
||||
Ogre::StringVectorPtr names =
|
||||
rgm.findResourceNames("Characters", "*.mesh");
|
||||
if (names)
|
||||
result.assign(names->begin(), names->end());
|
||||
if (!names)
|
||||
return result;
|
||||
|
||||
std::unordered_map<Ogre::String, Ogre::String> seen;
|
||||
for (const auto &meshName : *names) {
|
||||
Ogre::String skelName =
|
||||
getSkeletonNameForMesh(meshName);
|
||||
if (skelName.empty())
|
||||
continue;
|
||||
if (seen.find(skelName) != seen.end())
|
||||
continue;
|
||||
seen[skelName] = meshName;
|
||||
result.push_back({ skelName, meshName });
|
||||
}
|
||||
} catch (...) {
|
||||
}
|
||||
std::sort(result.begin(), result.end());
|
||||
std::sort(result.begin(), result.end(),
|
||||
[](const SkeletonSource &a, const SkeletonSource &b) {
|
||||
return a.skeletonName < b.skeletonName;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
Ogre::String AnimationTreeRegistry::getSkeletonNameForMesh(
|
||||
const Ogre::String &meshName) const
|
||||
{
|
||||
try {
|
||||
Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().load(
|
||||
meshName, "Characters");
|
||||
if (!mesh || !mesh->hasSkeleton())
|
||||
return Ogre::String();
|
||||
return mesh->getSkeletonName();
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"AnimationTreeRegistry: failed to read skeleton for '" +
|
||||
meshName + "': " + e.what());
|
||||
}
|
||||
return Ogre::String();
|
||||
}
|
||||
|
||||
std::vector<Ogre::String>
|
||||
AnimationTreeRegistry::getAnimationsForSource(const Ogre::String &source) const
|
||||
{
|
||||
|
||||
@@ -74,10 +74,18 @@ public:
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Skeleton source helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
std::vector<Ogre::String> getAvailableSkeletonSources() const;
|
||||
struct SkeletonSource {
|
||||
Ogre::String skeletonName;
|
||||
Ogre::String meshName;
|
||||
};
|
||||
|
||||
std::vector<SkeletonSource> getAvailableSkeletonSources() const;
|
||||
std::vector<Ogre::String> getAnimationsForSource(
|
||||
const Ogre::String &source) const;
|
||||
|
||||
/* Return the skeleton resource name for a given mesh, or empty. */
|
||||
Ogre::String getSkeletonNameForMesh(const Ogre::String &meshName) const;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Persistence */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "../components/BuoyancyInfo.hpp"
|
||||
#include "../components/WaterPhysics.hpp"
|
||||
#include "../components/WaterPlane.hpp"
|
||||
#include "../components/Terrain.hpp"
|
||||
#include "../components/Sun.hpp"
|
||||
#include "../components/Skybox.hpp"
|
||||
#include "../components/Light.hpp"
|
||||
@@ -1005,6 +1006,13 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render Terrain if present
|
||||
if (entity.has<TerrainComponent>()) {
|
||||
auto &tc = entity.get_mut<TerrainComponent>();
|
||||
m_componentRegistry.render<TerrainComponent>(entity, tc);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render LOD Settings if present
|
||||
if (entity.has<LodSettingsComponent>()) {
|
||||
auto &lodSettings = entity.get_mut<LodSettingsComponent>();
|
||||
|
||||
@@ -473,15 +473,13 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json,
|
||||
deserializeCharacter(entity, json["character"]);
|
||||
}
|
||||
|
||||
/* Deserialize identity before slots so legacy slot data migrates into
|
||||
* the correct registry record. */
|
||||
if (json.contains("characterIdentity")) {
|
||||
deserializeCharacterIdentity(entity, json["characterIdentity"]);
|
||||
}
|
||||
|
||||
if (json.contains("characterSlots")) {
|
||||
deserializeCharacterSlots(entity, json["characterSlots"]);
|
||||
}
|
||||
/* CharacterSlots -> CharacterRegistry migration is disabled; all scenes
|
||||
* have been migrated. */
|
||||
(void)0;
|
||||
|
||||
if (json.contains("characterSpawner")) {
|
||||
deserializeCharacterSpawner(entity, json["characterSpawner"]);
|
||||
@@ -491,10 +489,9 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json,
|
||||
deserializeAnimationTree(entity, json["animationTree"]);
|
||||
}
|
||||
|
||||
if (json.contains("animationTreeTemplate")) {
|
||||
deserializeAnimationTreeTemplate(entity,
|
||||
json["animationTreeTemplate"]);
|
||||
}
|
||||
/* AnimationTreeTemplate migration is disabled; all scenes have been
|
||||
* migrated to AnimationTreeRegistry. */
|
||||
(void)0;
|
||||
|
||||
if (json.contains("startupMenu")) {
|
||||
deserializeStartupMenu(entity, json["startupMenu"]);
|
||||
@@ -712,15 +709,13 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
deserializeCharacter(entity, json["character"]);
|
||||
}
|
||||
|
||||
/* Deserialize identity before slots so legacy slot data migrates into
|
||||
* the correct registry record. */
|
||||
if (json.contains("characterIdentity")) {
|
||||
deserializeCharacterIdentity(entity, json["characterIdentity"]);
|
||||
}
|
||||
|
||||
if (json.contains("characterSlots")) {
|
||||
deserializeCharacterSlots(entity, json["characterSlots"]);
|
||||
}
|
||||
/* CharacterSlots -> CharacterRegistry migration is disabled; all scenes
|
||||
* have been migrated. */
|
||||
(void)0;
|
||||
|
||||
if (json.contains("characterSpawner")) {
|
||||
deserializeCharacterSpawner(entity, json["characterSpawner"]);
|
||||
@@ -730,10 +725,9 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
deserializeAnimationTree(entity, json["animationTree"]);
|
||||
}
|
||||
|
||||
if (json.contains("animationTreeTemplate")) {
|
||||
deserializeAnimationTreeTemplate(entity,
|
||||
json["animationTreeTemplate"]);
|
||||
}
|
||||
/* AnimationTreeTemplate migration is disabled; all scenes have been
|
||||
* migrated to AnimationTreeRegistry. */
|
||||
(void)0;
|
||||
|
||||
if (json.contains("startupMenu")) {
|
||||
deserializeStartupMenu(entity, json["startupMenu"]);
|
||||
@@ -2300,78 +2294,14 @@ nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity)
|
||||
void SceneSerializer::deserializeCharacterSlots(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
/* Migrate legacy CharacterSlots data into the character registry.
|
||||
* The empty component is added as a placeholder reminder. */
|
||||
uint64_t registryId = 0;
|
||||
if (entity.has<CharacterIdentityComponent>())
|
||||
registryId = entity.get<CharacterIdentityComponent>().registryId;
|
||||
|
||||
if (registryId == 0) {
|
||||
registryId = CharacterRegistry::getSingleton().createCharacter(
|
||||
"Migrated", "Character", "", true);
|
||||
entity.set<CharacterIdentityComponent>(
|
||||
CharacterIdentityComponent{ registryId });
|
||||
}
|
||||
|
||||
CharacterRegistry::CharacterRecord *rec =
|
||||
CharacterRegistry::getSingleton().findCharacter(registryId);
|
||||
if (rec) {
|
||||
rec->inlineSex = json.value("sex", rec->inlineSex);
|
||||
if (json.contains("slots") && json["slots"].is_object()) {
|
||||
for (auto &[slot, mesh] : json["slots"].items()) {
|
||||
SlotSelection sel;
|
||||
sel.explicitMesh = mesh.get<std::string>();
|
||||
rec->inlineSlotSelections[slot] = sel;
|
||||
}
|
||||
}
|
||||
if (json.contains("slotSelections") &&
|
||||
json["slotSelections"].is_object()) {
|
||||
for (auto &[slot, selJson] :
|
||||
json["slotSelections"].items()) {
|
||||
SlotSelection sel;
|
||||
if (selJson.contains("layer0Mesh"))
|
||||
sel.layer0Mesh =
|
||||
selJson.value("layer0Mesh", "");
|
||||
if (selJson.contains("layer1Mesh"))
|
||||
sel.layer1Mesh =
|
||||
selJson.value("layer1Mesh", "");
|
||||
if (selJson.contains("layer2Mesh"))
|
||||
sel.layer2Mesh =
|
||||
selJson.value("layer2Mesh", "");
|
||||
if (selJson.contains("layer") &&
|
||||
sel.layer1Mesh.empty() &&
|
||||
sel.layer2Mesh.empty()) {
|
||||
int oldLayer = selJson.value("layer", 2);
|
||||
if (oldLayer == 1)
|
||||
sel.layer1Mesh = "auto";
|
||||
else if (oldLayer >= 2)
|
||||
sel.layer2Mesh = "auto";
|
||||
}
|
||||
sel.explicitMesh =
|
||||
selJson.value("explicitMesh", "");
|
||||
rec->inlineSlotSelections[slot] = sel;
|
||||
}
|
||||
}
|
||||
if (json.contains("frontAxis") &&
|
||||
json["frontAxis"].is_array() &&
|
||||
json["frontAxis"].size() >= 3) {
|
||||
Ogre::Vector3 fa(
|
||||
json["frontAxis"][0].get<float>(),
|
||||
json["frontAxis"][1].get<float>(),
|
||||
json["frontAxis"][2].get<float>());
|
||||
if (fa.squaredLength() > 0.0001f)
|
||||
fa.normalise();
|
||||
rec->frontAxis = fa;
|
||||
}
|
||||
CharacterRegistry::getSingleton().autoSave();
|
||||
}
|
||||
|
||||
entity.add<CharacterSlotsComponent>();
|
||||
CharacterRegistry::getSingleton().markCharacterDirty(registryId);
|
||||
/* CharacterSlots -> CharacterRegistry migration is disabled. */
|
||||
(void)entity;
|
||||
(void)json;
|
||||
}
|
||||
|
||||
/* Migrate a legacy inline animation tree into the registry and return
|
||||
* the registry name to use. */
|
||||
/* Migration disabled: legacy inline animation trees are no longer
|
||||
* automatically imported into the registry. */
|
||||
#if 0
|
||||
static Ogre::String migrateInlineAnimationTree(
|
||||
const nlohmann::json &json, const Ogre::String &preferredName)
|
||||
{
|
||||
@@ -2392,6 +2322,7 @@ static Ogre::String migrateInlineAnimationTree(
|
||||
reg.registerTree(def);
|
||||
return name;
|
||||
}
|
||||
#endif
|
||||
|
||||
nlohmann::json SceneSerializer::serializeAnimationTree(flecs::entity entity)
|
||||
{
|
||||
@@ -2414,7 +2345,10 @@ void SceneSerializer::deserializeAnimationTree(flecs::entity entity,
|
||||
if (json.contains("treeName")) {
|
||||
/* Current format. */
|
||||
at.treeName = json.value("treeName", "");
|
||||
} else if (json.contains("root")) {
|
||||
}
|
||||
/* Legacy inline tree / templateName migration is disabled. */
|
||||
#if 0
|
||||
else if (json.contains("root")) {
|
||||
/* Legacy inline tree: register in registry. */
|
||||
Ogre::String preferred = json.value("templateName", "");
|
||||
at.treeName = migrateInlineAnimationTree(json, preferred);
|
||||
@@ -2429,6 +2363,7 @@ void SceneSerializer::deserializeAnimationTree(flecs::entity entity,
|
||||
reg.registerTree(def);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
entity.set<AnimationTreeComponent>(at);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
#include "TerrainSystem.hpp"
|
||||
#include "../components/Terrain.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
|
||||
#include <OgreTerrain.h>
|
||||
#include <OgreTerrainGroup.h>
|
||||
#include <OgreTerrainPaging.h>
|
||||
#include <OgreTerrainPagedWorldSection.h>
|
||||
#include <OgreTerrainMaterialGeneratorA.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <cmath>
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
TerrainSystem::TerrainSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
Ogre::Camera *camera)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
, m_camera(camera)
|
||||
, m_terrainQuery(world.query<TerrainComponent, TransformComponent>())
|
||||
{
|
||||
}
|
||||
|
||||
TerrainSystem::~TerrainSystem()
|
||||
{
|
||||
deactivate();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
bool TerrainSystem::DummyPageProvider::prepareProceduralPage(
|
||||
Ogre::Page *, Ogre::PagedWorldSection *)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TerrainSystem::DummyPageProvider::loadProceduralPage(
|
||||
Ogre::Page *, Ogre::PagedWorldSection *)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TerrainSystem::DummyPageProvider::unloadProceduralPage(
|
||||
Ogre::Page *page, Ogre::PagedWorldSection *)
|
||||
{
|
||||
if (owner && owner->mTerrainGroup) {
|
||||
long x, y;
|
||||
owner->mTerrainGroup->unpackIndex(page->CHUNK_ID, &x, &y);
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: unloaded page " +
|
||||
Ogre::StringConverter::toString(x) + ", " +
|
||||
Ogre::StringConverter::toString(y));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TerrainSystem::DummyPageProvider::unprepareProceduralPage(
|
||||
Ogre::Page *, Ogre::PagedWorldSection *)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* CustomTerrainDefiner — generates height procedurally */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static float proceduralHeight(long worldX, long worldZ)
|
||||
{
|
||||
/* Simple procedural terrain: rolling hills using layered sine waves.
|
||||
* The heights center around 0 so the terrain sits on the Y=0 plane. */
|
||||
float h = 0.0f;
|
||||
|
||||
/* Large-scale hills (amplitude ±15) */
|
||||
h += 15.0f * sinf((float)worldX * 0.005f) *
|
||||
cosf((float)worldZ * 0.005f);
|
||||
|
||||
/* Medium bumps (amplitude ±5) */
|
||||
h += 5.0f * sinf((float)worldX * 0.02f + 1.3f) *
|
||||
cosf((float)worldZ * 0.02f + 0.7f);
|
||||
|
||||
/* Fine detail (amplitude ±2) */
|
||||
h += 2.0f * sinf((float)worldX * 0.05f + 2.1f) *
|
||||
sinf((float)worldZ * 0.05f + 0.3f);
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
void TerrainSystem::CustomTerrainDefiner::define(Ogre::TerrainGroup *group,
|
||||
long x, long y)
|
||||
{
|
||||
Ogre::uint16 terrainSize = group->getTerrainSize();
|
||||
Ogre::Real worldSize = group->getTerrainWorldSize();
|
||||
float *heightMap = OGRE_ALLOC_T(float, terrainSize * terrainSize,
|
||||
Ogre::MEMCATEGORY_GEOMETRY);
|
||||
|
||||
/* convertTerrainSlotToWorldPosition returns the MINIMUM CORNER
|
||||
* of page (x,y). Vertex (i,j) is at:
|
||||
* wx = worldPos.x + i * worldSize / (terrainSize - 1)
|
||||
* wz = worldPos.z + j * worldSize / (terrainSize - 1) */
|
||||
Ogre::Vector3 worldPos;
|
||||
group->convertTerrainSlotToWorldPosition(x, y, &worldPos);
|
||||
Ogre::Real step = worldSize / Ogre::Real(terrainSize - 1);
|
||||
|
||||
for (int j = 0; j < terrainSize; ++j) {
|
||||
for (int i = 0; i < terrainSize; ++i) {
|
||||
long wx = (long)(worldPos.x +
|
||||
(Ogre::Real)i * step);
|
||||
long wz = (long)(worldPos.z +
|
||||
(Ogre::Real)j * step);
|
||||
heightMap[j * terrainSize + i] =
|
||||
proceduralHeight(wx, wz);
|
||||
}
|
||||
}
|
||||
|
||||
group->defineTerrain(x, y, heightMap);
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: defined page (" +
|
||||
Ogre::StringConverter::toString(x) + ", " +
|
||||
Ogre::StringConverter::toString(y) + ")");
|
||||
|
||||
OGRE_FREE(heightMap, Ogre::MEMCATEGORY_GEOMETRY);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* activate — build all Ogre terrain singletons */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform)
|
||||
{
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TerrainSystem: activating terrain...");
|
||||
|
||||
/* 1 — TerrainGlobalOptions */
|
||||
mTerrainGlobals = OGRE_NEW Ogre::TerrainGlobalOptions();
|
||||
mTerrainGlobals->setMaxPixelError(tc.maxPixelError);
|
||||
mTerrainGlobals->setCompositeMapDistance(tc.compositeMapDistance);
|
||||
mTerrainGlobals->setCompositeMapAmbient(
|
||||
m_sceneMgr->getAmbientLight());
|
||||
|
||||
Ogre::TerrainMaterialGeneratorPtr matGen(
|
||||
new Ogre::TerrainMaterialGeneratorA());
|
||||
mTerrainGlobals->setDefaultMaterialGenerator(matGen);
|
||||
|
||||
/* 2 — TerrainGroup */
|
||||
mTerrainGroup = OGRE_NEW Ogre::TerrainGroup(
|
||||
m_sceneMgr, Ogre::Terrain::ALIGN_X_Z,
|
||||
tc.terrainSize, tc.worldSize);
|
||||
mTerrainGroup->setOrigin(xform.position);
|
||||
|
||||
/* 3 — ImportData defaults */
|
||||
Ogre::Terrain::ImportData &defaultImp =
|
||||
mTerrainGroup->getDefaultImportSettings();
|
||||
defaultImp.terrainSize = tc.terrainSize;
|
||||
defaultImp.worldSize = tc.worldSize;
|
||||
defaultImp.minBatchSize = tc.minBatchSize;
|
||||
defaultImp.maxBatchSize = tc.maxBatchSize;
|
||||
defaultImp.inputScale = 1.0f;
|
||||
|
||||
/* Layer declaration from the material generator. */
|
||||
defaultImp.layerDeclaration = matGen->getLayerDeclaration();
|
||||
|
||||
/* Build layer instances from the component. For M1, if no layers
|
||||
* are configured, create a single default dirt/rock layer so the
|
||||
* terrain is visible. */
|
||||
if (tc.layers.empty()) {
|
||||
Ogre::Terrain::LayerInstance li;
|
||||
li.worldSize = 100.0f;
|
||||
li.textureNames.push_back("Ground23_col.jpg");
|
||||
li.textureNames.push_back("Ground23_normheight.dds");
|
||||
defaultImp.layerList.push_back(li);
|
||||
} else {
|
||||
for (auto &cl : tc.layers) {
|
||||
Ogre::Terrain::LayerInstance li;
|
||||
li.worldSize = cl.worldSize;
|
||||
li.textureNames.push_back(cl.diffuseTexture);
|
||||
li.textureNames.push_back(cl.normalTexture);
|
||||
defaultImp.layerList.push_back(li);
|
||||
}
|
||||
}
|
||||
|
||||
/* 4 — PageManager + camera */
|
||||
mPageManager = OGRE_NEW Ogre::PageManager();
|
||||
mDummyPageProvider = std::make_unique<DummyPageProvider>();
|
||||
mDummyPageProvider->owner = this;
|
||||
mPageManager->setPageProvider(mDummyPageProvider.get());
|
||||
mPageManager->addCamera(m_camera);
|
||||
|
||||
/* 5 — TerrainPaging → PagedWorld → section */
|
||||
mTerrainPaging = OGRE_NEW Ogre::TerrainPaging(mPageManager);
|
||||
mPagedWorld = mPageManager->createWorld();
|
||||
|
||||
/* Set page range — start with a modest area for M1. */
|
||||
const long pageMinX = -2, pageMinY = -2;
|
||||
const long pageMaxX = 2, pageMaxY = 2;
|
||||
|
||||
mTerrainPagedWorldSection = mTerrainPaging->createWorldSection(
|
||||
mPagedWorld, mTerrainGroup, 300.0f, 500.0f,
|
||||
pageMinX, pageMinY, pageMaxX, pageMaxY);
|
||||
|
||||
/* 6 — TerrainDefiner */
|
||||
mTerrainDefiner = std::make_unique<CustomTerrainDefiner>(this);
|
||||
mTerrainPagedWorldSection->setDefiner(mTerrainDefiner.get());
|
||||
|
||||
/* 7 — Load initial pages synchronously (editor mode). */
|
||||
mTerrainGroup->freeTemporaryResources();
|
||||
mTerrainGroup->loadAllTerrains(true);
|
||||
|
||||
m_active = true;
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TerrainSystem: activation complete");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* deactivate */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::deactivate()
|
||||
{
|
||||
if (!m_active && !mTerrainGroup)
|
||||
return;
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TerrainSystem: deactivating...");
|
||||
|
||||
m_active = false;
|
||||
|
||||
/* Wait for background work to finish. */
|
||||
if (mTerrainGroup && mTerrainGroup->isDerivedDataUpdateInProgress()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TerrainSystem: waiting for derived data update...");
|
||||
do {
|
||||
mTerrainGroup->update(false);
|
||||
} while (mTerrainGroup->isDerivedDataUpdateInProgress());
|
||||
}
|
||||
|
||||
/* Remove camera from page manager. */
|
||||
if (mPageManager && m_camera)
|
||||
mPageManager->removeCamera(m_camera);
|
||||
|
||||
/* Destroy in correct order. */
|
||||
mTerrainDefiner.reset();
|
||||
|
||||
if (mTerrainGroup) {
|
||||
mTerrainGroup->removeAllTerrains();
|
||||
}
|
||||
|
||||
/* TerrainPaging holds ref to PageManager — destroy first. */
|
||||
OGRE_DELETE mTerrainPaging;
|
||||
mTerrainPaging = nullptr;
|
||||
|
||||
OGRE_DELETE mPageManager;
|
||||
mPageManager = nullptr;
|
||||
|
||||
OGRE_DELETE mTerrainGroup;
|
||||
mTerrainGroup = nullptr;
|
||||
|
||||
OGRE_DELETE mTerrainGlobals;
|
||||
mTerrainGlobals = nullptr;
|
||||
|
||||
mPagedWorld = nullptr;
|
||||
mTerrainPagedWorldSection = nullptr;
|
||||
mDummyPageProvider.reset();
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TerrainSystem: deactivated");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* update */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::update(float /*deltaTime*/)
|
||||
{
|
||||
/* Check if a terrain entity appeared. */
|
||||
m_terrainQuery.each([&](flecs::entity e, TerrainComponent &tc,
|
||||
TransformComponent &xform) {
|
||||
(void)e;
|
||||
if (!tc.enabled && m_active) {
|
||||
deactivate();
|
||||
return;
|
||||
}
|
||||
if (tc.enabled && !m_active)
|
||||
activate(tc, xform);
|
||||
});
|
||||
|
||||
/* Pump the terrain group — drives LOD, page load/unload. */
|
||||
if (m_active && mTerrainGroup) {
|
||||
mTerrainGroup->update(false);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* getHeightAt */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
float TerrainSystem::getHeightAt(const Ogre::Vector3 &worldPos) const
|
||||
{
|
||||
if (!m_active || !mTerrainGroup)
|
||||
return 0.0f;
|
||||
|
||||
return mTerrainGroup->getHeightAtWorldPosition(worldPos);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
#ifndef EDITSCENE_TERRAINSYSTEM_HPP
|
||||
#define EDITSCENE_TERRAINSYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <OgreTerrain.h>
|
||||
#include <OgreTerrainGroup.h>
|
||||
#include <OgreTerrainPaging.h>
|
||||
#include <OgreTerrainPagedWorldSection.h>
|
||||
#include <OgrePageManager.h>
|
||||
#include <OgrePage.h>
|
||||
#include <OgrePagedWorld.h>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <cstdint>
|
||||
|
||||
// Forward declarations
|
||||
namespace JPH {
|
||||
class BodyID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton system that owns all Ogre terrain objects (TerrainGroup,
|
||||
* TerrainPaging, PageManager, etc.) and the Jolt collision state.
|
||||
*
|
||||
* Created by EditorApp in setup(), one instance per scene. Activation
|
||||
* happens when an entity with TerrainComponent + TransformComponent
|
||||
* appears in the ECS world.
|
||||
*/
|
||||
class TerrainSystem {
|
||||
public:
|
||||
TerrainSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
Ogre::Camera *camera);
|
||||
~TerrainSystem();
|
||||
|
||||
TerrainSystem(const TerrainSystem &) = delete;
|
||||
TerrainSystem &operator=(const TerrainSystem &) = delete;
|
||||
|
||||
/** Per-frame update — pump terrain group, process collider queues. */
|
||||
void update(float deltaTime);
|
||||
|
||||
/** Deactivate terrain: remove colliders, unload pages, destroy
|
||||
* Ogre singletons. Called on scene clear. */
|
||||
void deactivate();
|
||||
|
||||
/** Return true when terrain is active and rendering. */
|
||||
bool isActive() const { return m_active; }
|
||||
|
||||
/** Get terrain height at world position (for raycasts/query). */
|
||||
float getHeightAt(const Ogre::Vector3 &worldPos) const;
|
||||
|
||||
private:
|
||||
/** Internal: build all Ogre terrain singletons from the component. */
|
||||
void activate(struct TerrainComponent &tc,
|
||||
struct TransformComponent &xform);
|
||||
|
||||
/** Dummy page provider — satisfies PageManager without disk I/O. */
|
||||
class DummyPageProvider : public Ogre::PageProvider {
|
||||
public:
|
||||
bool prepareProceduralPage(Ogre::Page *page,
|
||||
Ogre::PagedWorldSection *section) override;
|
||||
bool loadProceduralPage(Ogre::Page *page,
|
||||
Ogre::PagedWorldSection *section) override;
|
||||
bool unloadProceduralPage(Ogre::Page *page,
|
||||
Ogre::PagedWorldSection *section) override;
|
||||
bool unprepareProceduralPage(Ogre::Page *page,
|
||||
Ogre::PagedWorldSection *section) override;
|
||||
|
||||
TerrainSystem *owner = nullptr;
|
||||
};
|
||||
|
||||
/** Custom terrain definer — generates height from procedural
|
||||
* function or base heightmap. */
|
||||
class CustomTerrainDefiner :
|
||||
public Ogre::TerrainPagedWorldSection::TerrainDefiner {
|
||||
public:
|
||||
CustomTerrainDefiner(TerrainSystem *sys)
|
||||
: Ogre::TerrainPagedWorldSection::TerrainDefiner()
|
||||
, m_sys(sys)
|
||||
{
|
||||
}
|
||||
|
||||
void define(Ogre::TerrainGroup *terrainGroup, long x,
|
||||
long y) override;
|
||||
|
||||
private:
|
||||
TerrainSystem *m_sys;
|
||||
};
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
Ogre::Camera *m_camera;
|
||||
bool m_active = false;
|
||||
|
||||
// Ogre terrain singletons
|
||||
Ogre::TerrainGlobalOptions *mTerrainGlobals = nullptr;
|
||||
Ogre::TerrainGroup *mTerrainGroup = nullptr;
|
||||
Ogre::PageManager *mPageManager = nullptr;
|
||||
Ogre::TerrainPaging *mTerrainPaging = nullptr;
|
||||
Ogre::PagedWorld *mPagedWorld = nullptr;
|
||||
Ogre::TerrainPagedWorldSection *mTerrainPagedWorldSection = nullptr;
|
||||
std::unique_ptr<CustomTerrainDefiner> mTerrainDefiner;
|
||||
std::unique_ptr<DummyPageProvider> mDummyPageProvider;
|
||||
|
||||
// Query for the active terrain entity
|
||||
flecs::query<struct TerrainComponent, struct TransformComponent>
|
||||
m_terrainQuery;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_TERRAINSYSTEM_HPP
|
||||
@@ -51,13 +51,16 @@ void AnimationTreeRegistryEditor::renderListPane()
|
||||
std::vector<Ogre::String> names = reg.getTreeNames();
|
||||
std::sort(names.begin(), names.end());
|
||||
|
||||
for (const auto &name : names) {
|
||||
for (size_t i = 0; i < names.size(); ++i) {
|
||||
const auto &name = names[i];
|
||||
bool selected = (m_selectedTreeName == name);
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
if (ImGui::Selectable(name.c_str(), selected)) {
|
||||
if (m_selectedTreeName != name)
|
||||
m_nodeEditor.reset();
|
||||
m_selectedTreeName = name;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
@@ -96,28 +99,42 @@ void AnimationTreeRegistryEditor::renderEditPane()
|
||||
}
|
||||
|
||||
/* Skeleton source */
|
||||
std::vector<Ogre::String> sources = reg.getAvailableSkeletonSources();
|
||||
std::vector<AnimationTreeRegistry::SkeletonSource> sources =
|
||||
reg.getAvailableSkeletonSources();
|
||||
Ogre::String currentSource = def->skeletonSource;
|
||||
Ogre::String preview = currentSource.empty() ? "(none)" : currentSource;
|
||||
Ogre::String currentSkelName =
|
||||
currentSource.empty() ?
|
||||
Ogre::String() :
|
||||
reg.getSkeletonNameForMesh(currentSource);
|
||||
Ogre::String preview =
|
||||
currentSkelName.empty() ? "(none)" : currentSkelName;
|
||||
if (ImGui::BeginCombo("Skeleton Source", preview.c_str())) {
|
||||
bool noneSel = currentSource.empty();
|
||||
if (ImGui::Selectable("(none)", noneSel)) {
|
||||
def->skeletonSource = "";
|
||||
reg.markModified(def->name);
|
||||
}
|
||||
for (const auto &src : sources) {
|
||||
bool sel = (currentSource == src);
|
||||
if (ImGui::Selectable(src.c_str(), sel)) {
|
||||
def->skeletonSource = src;
|
||||
for (size_t i = 0; i < sources.size(); ++i) {
|
||||
const auto &src = sources[i];
|
||||
bool sel = (currentSource == src.meshName);
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
if (ImGui::Selectable(src.skeletonName.c_str(), sel)) {
|
||||
def->skeletonSource = src.meshName;
|
||||
reg.markModified(def->name);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Representative mesh: %s",
|
||||
src.meshName.c_str());
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Mesh from the Characters resource group used to "
|
||||
"populate animation-name dropdowns.");
|
||||
"Skeleton used to populate animation-name dropdowns. "
|
||||
"One representative mesh per skeleton is shown.");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#ifndef EDITSCENE_TERRAINEDITOR_HPP
|
||||
#define EDITSCENE_TERRAINEDITOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/Terrain.hpp"
|
||||
|
||||
/**
|
||||
* Editor panel for TerrainComponent.
|
||||
*
|
||||
* Milestone 1: minimal — enable/disable toggle and display of
|
||||
* key parameters. Heightmap editing, road tools, splat painting
|
||||
* come in later milestones.
|
||||
*/
|
||||
class TerrainEditor : public ComponentEditor<TerrainComponent> {
|
||||
public:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
TerrainComponent &tc) override
|
||||
{
|
||||
bool changed = false;
|
||||
|
||||
ImGui::PushID("Terrain");
|
||||
|
||||
/* Enable */
|
||||
if (ImGui::Checkbox("Enabled", &tc.enabled))
|
||||
changed = true;
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Read-only display of sizing params (restart needed to change).
|
||||
* In M2+ these get Restart Terrain buttons. */
|
||||
ImGui::Text("Page size: %d x %d vertices", tc.terrainSize,
|
||||
tc.terrainSize);
|
||||
ImGui::Text("World size: %.0f units", tc.worldSize);
|
||||
ImGui::Text("Heightmap: %d x %d", tc.heightmapSize,
|
||||
tc.heightmapSize);
|
||||
ImGui::Text("Batch: %d - %d", tc.minBatchSize,
|
||||
tc.maxBatchSize);
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Quality params (hot — change takes effect next define). */
|
||||
if (ImGui::SliderFloat("Max Pixel Error", &tc.maxPixelError,
|
||||
0.5f, 10.0f, "%.1f"))
|
||||
changed = true;
|
||||
if (ImGui::SliderFloat("Composite Map Dist",
|
||||
&tc.compositeMapDistance, 50.0f,
|
||||
2000.0f, "%.0f"))
|
||||
changed = true;
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Layers summary */
|
||||
ImGui::Text("Layers: %zu", tc.layers.size());
|
||||
for (size_t i = 0; i < tc.layers.size(); ++i) {
|
||||
auto &l = tc.layers[i];
|
||||
ImGui::BulletText("[%zu] %s / %s (ws=%.0f)", i,
|
||||
l.diffuseTexture.c_str(),
|
||||
l.normalTexture.c_str(),
|
||||
l.worldSize);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::Button("Reset to Defaults")) {
|
||||
tc = TerrainComponent();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
return changed;
|
||||
}
|
||||
|
||||
const char *getName() const override
|
||||
{
|
||||
return "Terrain";
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_TERRAINEDITOR_HPP
|
||||
Reference in New Issue
Block a user