Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f06f535938 | |||
| 4cd2c72fb0 | |||
| 2c095de9f2 |
@@ -2,6 +2,7 @@
|
||||
#include <filesystem>
|
||||
#include "EditorApp.hpp"
|
||||
#include "GameMode.hpp"
|
||||
#include <OgreMaterialManager.h>
|
||||
#include "systems/EditorUISystem.hpp"
|
||||
#include "systems/PhysicsSystem.hpp"
|
||||
#include "systems/BuoyancySystem.hpp"
|
||||
@@ -111,6 +112,7 @@
|
||||
#endif
|
||||
|
||||
#include <OgreRTShaderSystem.h>
|
||||
#include <OgreShaderRenderState.h>
|
||||
#include <OgreArchiveManager.h>
|
||||
#include "package/OgrePackageArchive.h"
|
||||
#include <imgui.h>
|
||||
@@ -236,16 +238,68 @@ EditorApp::EditorApp()
|
||||
|
||||
EditorApp::~EditorApp()
|
||||
{
|
||||
destroyEditorSystems();
|
||||
}
|
||||
|
||||
void EditorApp::shutdownEditor()
|
||||
{
|
||||
destroyEditorSystems();
|
||||
}
|
||||
|
||||
void EditorApp::closeApp()
|
||||
{
|
||||
shutdownEditor();
|
||||
|
||||
/* Terrain materials and other RTSS-generated materials are torn down
|
||||
* in shutdownEditor(), so the base-class RTSS teardown can run safely
|
||||
* while MaterialManager still exists. */
|
||||
OgreBites::ApplicationContext::closeApp();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Helper: release RTShader TargetRenderState objects held by every */
|
||||
/* material pass. This destroys the SubRenderState instances owned */
|
||||
/* by the target render states before the RTSS factories are torn */
|
||||
/* down, avoiding the "Sub render states still exists" assertion. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static void clearRTShaderPassBindings()
|
||||
{
|
||||
Ogre::MaterialManager &matMgr =
|
||||
Ogre::MaterialManager::getSingleton();
|
||||
Ogre::MaterialManager::ResourceMapIterator it =
|
||||
matMgr.getResourceIterator();
|
||||
|
||||
while (it.hasMoreElements()) {
|
||||
Ogre::MaterialPtr mat =
|
||||
Ogre::static_pointer_cast<Ogre::Material>(
|
||||
it.getNext());
|
||||
if (!mat)
|
||||
continue;
|
||||
|
||||
for (Ogre::Technique *tech : mat->getTechniques()) {
|
||||
if (!tech)
|
||||
continue;
|
||||
for (Ogre::Pass *pass : tech->getPasses()) {
|
||||
if (!pass)
|
||||
continue;
|
||||
pass->getUserObjectBindings().eraseUserAny(
|
||||
Ogre::RTShader::TargetRenderState::
|
||||
UserKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EditorApp::destroyEditorSystems()
|
||||
{
|
||||
if (m_systemsDestroyed)
|
||||
return;
|
||||
|
||||
// Shutdown UI system first (cleans up gizmo while SceneManager is still valid)
|
||||
if (m_uiSystem) {
|
||||
m_uiSystem->shutdown();
|
||||
}
|
||||
|
||||
// Delete all editor entities before OGRE cleanup
|
||||
// This ensures all components with Ogre resources are cleaned up while SceneManager exists
|
||||
// Collect entities first, then delete after iteration (can't modify during iteration)
|
||||
std::vector<flecs::entity> entitiesToDelete;
|
||||
|
||||
// DialogueSystem is a singleton, no manual teardown needed
|
||||
|
||||
m_startupMenuSystem.reset();
|
||||
@@ -273,13 +327,44 @@ EditorApp::~EditorApp()
|
||||
m_lodSystem.reset();
|
||||
m_cameraSystem.reset();
|
||||
m_lightSystem.reset();
|
||||
|
||||
/* Terrain must be torn down before water, sky, sun and physics. */
|
||||
m_terrainSystem.reset();
|
||||
m_waterPlaneSystem.reset();
|
||||
m_skyboxSystem.reset();
|
||||
m_sunSystem.reset();
|
||||
m_buoyancySystem.reset();
|
||||
m_physicsSystem.reset();
|
||||
|
||||
/* Flush the RTShader generator cache before clearing materials. This
|
||||
* waits for any pending shader-generation tasks and destroys generated
|
||||
* SubRenderState instances while their factories are still alive. */
|
||||
Ogre::RTShader::ShaderGenerator *shadergen =
|
||||
Ogre::RTShader::ShaderGenerator::getSingletonPtr();
|
||||
if (shadergen) {
|
||||
shadergen->removeAllShaderBasedTechniques();
|
||||
shadergen->flushShaderCache();
|
||||
}
|
||||
|
||||
// Clear the scene and drop any materials that are no longer referenced so
|
||||
// the RTShader generator can shut down without SubRenderState instances
|
||||
// outliving their factories.
|
||||
if (m_sceneMgr) {
|
||||
m_sceneMgr->clearScene();
|
||||
Ogre::MaterialManager::getSingleton().unloadUnreferencedResources();
|
||||
clearRTShaderPassBindings();
|
||||
}
|
||||
|
||||
if (m_imguiListener) {
|
||||
Ogre::RenderWindow *rw = getRenderWindow();
|
||||
if (rw)
|
||||
rw->removeListener(m_imguiListener.get());
|
||||
}
|
||||
m_imguiListener.reset();
|
||||
m_uiSystem.reset();
|
||||
m_camera.reset();
|
||||
|
||||
// Now OGRE can shut down safely
|
||||
// Singletons are managed by OGRE, don't delete them
|
||||
m_systemsDestroyed = true;
|
||||
}
|
||||
|
||||
void EditorApp::setup()
|
||||
@@ -1408,6 +1493,11 @@ void EditorApp::createAxes()
|
||||
}
|
||||
}
|
||||
|
||||
bool EditorApp::frameEnded(const Ogre::FrameEvent & /*evt*/)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
{
|
||||
bool paused = (m_gamePlayState == GamePlayState::Paused);
|
||||
|
||||
@@ -137,8 +137,12 @@ public:
|
||||
// OgreBites::ApplicationContext overrides
|
||||
void setup() override;
|
||||
bool frameRenderingQueued(const Ogre::FrameEvent &evt) override;
|
||||
bool frameEnded(const Ogre::FrameEvent &evt) override;
|
||||
void locateResources() override;
|
||||
|
||||
void shutdownEditor();
|
||||
void closeApp();
|
||||
|
||||
// OgreBites::InputListener overrides
|
||||
bool mouseMoved(const OgreBites::MouseMotionEvent &evt) override;
|
||||
bool mousePressed(const OgreBites::MouseButtonEvent &evt) override;
|
||||
@@ -305,7 +309,10 @@ private:
|
||||
GamePlayState m_gamePlayState = GamePlayState::Menu;
|
||||
GameInputState m_gameInput;
|
||||
bool m_setupComplete = false;
|
||||
bool m_systemsDestroyed = false;
|
||||
bool m_debugBuoyancy = false;
|
||||
|
||||
void destroyEditorSystems();
|
||||
float m_playTime = 0.0f;
|
||||
std::string m_currentBaseScene;
|
||||
|
||||
|
||||
@@ -200,12 +200,17 @@ std::unordered_map<uint64_t, TerrainCollider> mColliders;
|
||||
paging for game mode.
|
||||
|
||||
3. **Deactivation / scene clear**
|
||||
- Wait until `mTerrainGroup->isDerivedDataUpdateInProgress()` is `false`.
|
||||
- Disable paging operations (`PageManager::setPagingOperationsEnabled(false)`)
|
||||
so no new page loads/unloads start while tearing down.
|
||||
- Remove all Jolt bodies first.
|
||||
- Unload and destroy pages.
|
||||
- Destroy in this order: `TerrainPaging` → `PageManager` → `TerrainGroup` →
|
||||
`TerrainGlobalOptions`. (`TerrainPaging` owns a reference to `PageManager`,
|
||||
so it must go first.)
|
||||
- Release RTShader `SubRenderState` objects held by the terrain material
|
||||
generator and erase per-pass `TargetRenderState` user-object bindings
|
||||
before RTSS teardown.
|
||||
- Destroy `TerrainPaging` → `PageManager`. `PageManager::~PageManager` does
|
||||
**not** destroy the `PagedWorld`s it owns, so the `TerrainPagedWorldSection`
|
||||
and its `TerrainGroup` are intentionally leaked. This avoids GL driver
|
||||
hangs/crashes observed when destroying the group during live scene reloads.
|
||||
- Destroy `TerrainGlobalOptions` and drop the `mTerrainGroup` pointer.
|
||||
|
||||
4. **Destruction** (`EditorApp::~EditorApp()`)
|
||||
- Destroy `TerrainSystem` **before** `PhysicsSystem` and the
|
||||
@@ -224,29 +229,41 @@ if (m_terrainSystem) {
|
||||
|
||||
Inside `update()`:
|
||||
|
||||
1. `mTerrainGroup->update(false)` (only if no derived data update is in
|
||||
progress).
|
||||
2. Process pending collider creation queue: for each loaded page with no active
|
||||
collider, build a `ZigzagHeightfieldShape` and add the static Jolt body.
|
||||
3. Process pending collider removals: only remove bodies when the corresponding
|
||||
page is unloaded and no derived data update is running.
|
||||
1. `rebuildDirtyPages()` and `processDeferredReloads()` handle editor edits.
|
||||
2. `ensurePageColliders()` scans every loaded `TerrainSlot` and queues collider
|
||||
creation for pages that do not have one yet.
|
||||
3. `processColliderCreates()` drains the queue. It skips pages whose terrain
|
||||
instance is not yet loaded and re-queues them; it does **not** block on
|
||||
`isDerivedDataUpdateInProgress()` because the heightfield data is stable once
|
||||
`isLoaded()` is true.
|
||||
4. `processColliderRemoves()` destroys bodies marked by `DummyPageProvider`.
|
||||
|
||||
### 2.3 Safety rules for background jobs
|
||||
|
||||
`Ogre::Terrain` performs `prepare()`, LOD streaming, and derived-data updates on
|
||||
Ogre's `WorkQueue`. The following rules prevent crashes, leaks, and use-after-free:
|
||||
|
||||
- Never remove or hide a `Terrain` page while
|
||||
`terrain->isDerivedDataUpdateInProgress()` is `true`.
|
||||
- Never remove the `TerrainComponent` entity while
|
||||
`mTerrainGroup->isDerivedDataUpdateInProgress()` is `true`; defer removal to
|
||||
the next frame.
|
||||
- Always pump `mTerrainGroup->update(false)` each frame so pending background
|
||||
work can finish.
|
||||
- The `TerrainGroup` destructor already waits for pending `prepare()` requests,
|
||||
but explicit draining avoids surprises.
|
||||
- Do not create a Jolt heightfield body for a page until `terrain->isLoaded()`
|
||||
is `true` and derived data is idle.
|
||||
- **Do not drive paging from a worker thread in the editor.** In editor mode
|
||||
the camera is intentionally **not** added to `PageManager`; pages are loaded
|
||||
synchronously through `TerrainGroup::loadAllTerrains(true)`. This prevents
|
||||
`CustomTerrainDefiner::define()` from being called on a background thread and
|
||||
touching GL/ECS state unsafely.
|
||||
- **Never call `defineTerrain()` on a slot that already has a live instance from
|
||||
a background thread.** If paging is active at runtime, only unload/reload
|
||||
pages through the paging system so the instance is freed before `define()` is
|
||||
called again.
|
||||
- **Keep editing and paging reload separate.** Sculpting writes the heightmap
|
||||
and marks pages dirty; `rebuildDirtyPages()` updates the live terrain geometry
|
||||
directly. The paging system may call `CustomTerrainDefiner` later for a true
|
||||
reload, but the two paths do not interleave during a frame.
|
||||
- `DummyPageProvider::unloadProceduralPage` must only mark colliders for removal;
|
||||
it must not synchronously destroy Jolt bodies or touch ECS state.
|
||||
- Colliders can be built as soon as `terrain->isLoaded()` is true; the heightmap
|
||||
data is stable even while derived normal/composite maps are still being
|
||||
generated.
|
||||
- The `TerrainGroup` destructor can hang or crash on some GL drivers, so the
|
||||
active group is intentionally leaked at shutdown. `PageManager::~PageManager`
|
||||
does not destroy worlds, which makes this leak safe.
|
||||
|
||||
### 2.4 Height function
|
||||
|
||||
@@ -1142,8 +1159,10 @@ target_link_libraries(editSceneEditor PUBLIC
|
||||
- CMake (`OgrePaging`/`OgreTerrain` links) and `EditorApp` wiring.
|
||||
- Component registration in `setupECS()` + editor module.
|
||||
|
||||
Definition of done: a paged terrain renders in the editor; terrain pages load
|
||||
and unload as the camera moves.
|
||||
Definition of done: a paged terrain renders in the editor. In the current
|
||||
editor mode the camera is not attached to `PageManager`, so all pages in the
|
||||
configured range are loaded synchronously; the paging objects are present and
|
||||
ready for runtime game mode where a camera can be attached.
|
||||
|
||||
### Milestone 2 — Physics integration ✅ DONE
|
||||
|
||||
@@ -1157,10 +1176,10 @@ TerrainSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
|
||||
**EditorApp wiring change**: Update `setup()` to pass `m_physicsSystem->getPhysicsWrapper()`.
|
||||
|
||||
**Collider creation queue**: After `CustomTerrainDefiner::define()` queues a
|
||||
page, `processColliderCreates()` drains the queue in `update()` when
|
||||
`isDerivedDataUpdateInProgress()` is false. Each page gets a zigzag
|
||||
`JPH::MeshShape` as a static body in `Layers::NON_MOVING`.
|
||||
**Collider creation queue**: `ensurePageColliders()` scans every loaded
|
||||
`TerrainSlot` each frame and queues missing pages. `processColliderCreates()`
|
||||
drains the queue in `update()` when the terrain instance is loaded. Each page
|
||||
gets a zigzag `JPH::MeshShape` as a static body in `Layers::NON_MOVING`.
|
||||
|
||||
**mColliders map**: `std::unordered_map<uint64_t, TerrainCollider>` keyed by
|
||||
`packIndex(x, y)`. `TerrainCollider` holds `{pageX, pageY, JPH::BodyID,
|
||||
@@ -1168,7 +1187,7 @@ pendingRemove}`.
|
||||
|
||||
**Collider removal queue**: `DummyPageProvider::unloadProceduralPage` marks the
|
||||
collider `pendingRemove`. `processColliderRemoves()` destroys bodies in
|
||||
`update()` after derived data is idle.
|
||||
`update()` when the corresponding terrain page is no longer busy.
|
||||
|
||||
**Zigzag triangulation**: Build `JPH::MeshShape` from `terrain->getHeightData()`
|
||||
using the zigzag quad split matching Ogre's odd/even row parity. Uses
|
||||
@@ -1206,8 +1225,11 @@ JPH::EActivation::DontActivate)`.
|
||||
against all loaded terrain page bodies.
|
||||
|
||||
**Testing**:
|
||||
- [x] Drop a rigid body on terrain → rests flush.
|
||||
- [x] Toggle "Show Terrain Colliders" → wireframe matches terrain.
|
||||
- [x] `editSceneEditor test_terrain.json --exit-after-first-frame` exits cleanly
|
||||
(EXIT:0) with paging objects created and RTSS teardown successful.
|
||||
- [x] Running for several seconds creates colliders for all loaded pages.
|
||||
- [ ] Drop a rigid body on terrain → rests flush.
|
||||
- [ ] Toggle "Show Terrain Colliders" → wireframe matches terrain.
|
||||
- [ ] Fly away and back → colliders for unloaded pages removed, new pages get colliders.
|
||||
- [ ] Remove terrain entity → all collider bodies removed, no dangling Jolt bodies.
|
||||
- [ ] Add/remove terrain repeated → stable memory (valgrind/ASan).
|
||||
@@ -1217,30 +1239,102 @@ JPH::EActivation::DontActivate)`.
|
||||
Definition of done: ✅ rigid bodies collide flush with visible terrain; debug
|
||||
draw off by default with toggle; removing and re-adding terrain is leak-free.
|
||||
|
||||
**Terrain cleanup on entity/component removal** (post-M2 fix):
|
||||
`TerrainSystem::update()` tracks whether any matching terrain entity exists.
|
||||
If `m_active` but no entity carries `TerrainComponent`, `deactivate()` is
|
||||
called. Without this, deleting the entity or removing the component leaves
|
||||
terrain and colliders orphaned.
|
||||
|
||||
### Milestone 3 — Serialization + heightmap editing + splat painting
|
||||
- Scene serialization for `TerrainComponent` (save/load JSON round-trip).
|
||||
|
||||
**Terrain cleanup on entity/component removal** (added post-M2):
|
||||
`TerrainSystem::update()` now tracks whether any matching terrain entity exists.
|
||||
If `m_active` is true but the query returns no entity (because the terrain
|
||||
entity was deleted or its TerrainComponent was removed), `deactivate()` is
|
||||
called automatically. Without this, removing the component or deleting the
|
||||
entity would leave the terrain and colliders orphaned in the scene.
|
||||
- Binary heightmap file save/load alongside scene JSON.
|
||||
- Brush tools (raise/lower/smooth/flatten) with curve-based brush profile.
|
||||
- Splat paint brush (select layer, set opacity, paint blend values via
|
||||
`TerrainLayerBlendMap`).
|
||||
- Sparse 256x256 fixup chunk cache and file I/O (lazy creation, sentinel-based
|
||||
override semantics).
|
||||
- Mark affected pages dirty and rebuild them via CustomTerrainDefiner
|
||||
re-sampling.
|
||||
- Heightmap editor UI (brush radius, strength, type selector).
|
||||
- `AuxMap` infrastructure (create, load, save, edit with brushes).
|
||||
- Fixup chunk cleanup (clear all, delete individual chunks by world position).
|
||||
**Scene serialization**:
|
||||
- `SceneSerializer::serializeEntity()` / `deserializeEntity()` support for
|
||||
`TerrainComponent` (save/load JSON round-trip with all parameters, layers,
|
||||
road config, aux maps).
|
||||
- Binary heightmap file save/load alongside scene JSON
|
||||
(`heightmaps/<terrainId>/`).
|
||||
|
||||
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.
|
||||
**Heightmap data manager** (`HeightmapData` class inside `TerrainSystem`):
|
||||
- Loads/stores the `terrainSize × terrainSize` float array from/to binary
|
||||
files.
|
||||
- Provides `getHeightAt(worldX, worldZ)` combining base heightmap + fixup
|
||||
chunks with bilinear sampling.
|
||||
- `setHeightAt(worldX, worldZ, value)` writes to base heightmap or a fixup
|
||||
chunk (permanent base edit vs road compliance fixup).
|
||||
- `CustomTerrainDefiner` calls `HeightmapData::getHeightAt()` instead of
|
||||
procedural — falls back to procedural if no file loaded yet.
|
||||
|
||||
**Fixup chunk cache and I/O**:
|
||||
- Sparse 256×256 fixup chunks, lazy-created, sentinel-based override.
|
||||
- `loadFixupChunk()` / `saveFixupChunk()` / `clearAllFixups()`.
|
||||
- "Clear all fixups" button; individual chunk delete by world-position
|
||||
right-click.
|
||||
|
||||
**3D world point selection and sculpting UX**:
|
||||
|
||||
The TerrainEditor panel gains a **Sculpting** section below the terrain
|
||||
properties, grouping all brush controls together:
|
||||
|
||||
```
|
||||
┌─ Sculpting ─────────────────────────────────────┐
|
||||
│ [✓] Sculpt Mode │
|
||||
│ Tool: [ Raise ▾ ] Radius: [====] 5.0 │
|
||||
│ Strength: [====] 0.5 Curve: [====] │
|
||||
│ Layer: [ Layer 1 ▾ ] (splat paint only) │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Sculpt Mode checkbox**: When checked, left-click on the terrain applies the
|
||||
selected tool. When unchecked, clicks behave normally (entity selection,
|
||||
camera). ESC also exits sculpt mode.
|
||||
|
||||
**Tool selector**: Combo box — Raise, Lower, Smooth, Flatten, Splat Paint.
|
||||
Switching the tool immediately changes behavior; no separate "activate" step.
|
||||
|
||||
**Painting**: In sculpt mode, holding left mouse on the terrain continuously
|
||||
applies the current tool at the brush position (determined by raycast against
|
||||
terrain via `TerrainSystem::raycastTerrain()`). Releasing the mouse stops
|
||||
painting. The brush follows the cursor — drag to paint a stroke. A
|
||||
translucent circle decal on the terrain surface shows brush position and
|
||||
radius.
|
||||
|
||||
**Camera interaction while sculpting**:
|
||||
- Right mouse always controls camera (orbit/pan), even in sculpt mode.
|
||||
- Middle mouse or Ctrl+Left also moves camera in sculpt mode, so the user
|
||||
can reposition without toggling the mode off.
|
||||
|
||||
**Brush tools**:
|
||||
- **Raise**: adds height (strength × radius curve) at brush center.
|
||||
- **Lower**: subtracts height.
|
||||
- **Smooth**: averages heights within brush radius.
|
||||
- **Flatten**: pulls heights toward the height under brush center (plateau).
|
||||
- **Splat Paint**: paints selected layer's blend weight via
|
||||
`TerrainLayerBlendMap::setBlendValue()` + `dirtyRect()` + `update()`.
|
||||
Layer selector appears when this tool is active.
|
||||
|
||||
**Brush application and dirty page marking**:
|
||||
- During a stroke, `HeightmapData::setHeightAt()` is called for each affected
|
||||
sample within the brush radius, weighted by the radius curve.
|
||||
- At mouse release (end of stroke), determine which terrain pages overlap the
|
||||
modified area and mark them dirty.
|
||||
- `CustomTerrainDefiner` re-samples `HeightmapData::getHeightAtWorld()` on the
|
||||
next `define()` call for dirty pages.
|
||||
- If a modified page already has a physics collider, queue its removal and
|
||||
recreation with updated heights.
|
||||
|
||||
|
||||
**Heightmap file I/O and AuxMap controls**:
|
||||
- "Save Heightmap" / "Load Heightmap" buttons in the TerrainEditor panel.
|
||||
- AuxMap management: create, rename, delete, select active map for editing.
|
||||
AuxMap editing uses the same brush tools but writes to the selected aux map
|
||||
instead of the base heightmap.
|
||||
- "Clear All Fixups" button — deletes all fixup chunks and marks affected
|
||||
pages dirty for rebuild.
|
||||
|
||||
**Definition of done**: user can sculpt terrain with visual brush feedback;
|
||||
save scene, reload, terrain restored with edited heightmap and layer paint;
|
||||
visual and collision update after sculpting; camera can be snapped above
|
||||
terrain on demand.
|
||||
|
||||
### Milestone 4 — Procedural roads
|
||||
- Node/edge editing UI (add, remove, split, join, select, move).
|
||||
@@ -1249,8 +1343,8 @@ collision update after sculpting.
|
||||
`Procedural::TriangleBuffer`.
|
||||
- End cap generation for endpoints.
|
||||
- Seam suppression via vertex shifting.
|
||||
- LOD + visibility distance per road mesh segment.
|
||||
- Road physics colliders (JPH::MeshShape per segment).
|
||||
- LOD + visibility distance per road mesh.
|
||||
- Road physics colliders (JPH::MeshShape per road mesh).
|
||||
- "Comply terrain to roads" writing fixup chunks with smooth falloff.
|
||||
|
||||
Definition of done: user can draw a road network; road geometry appears with
|
||||
|
||||
@@ -51,6 +51,18 @@ struct TerrainComponent {
|
||||
};
|
||||
std::vector<Layer> layers;
|
||||
|
||||
// Road configuration parameters.
|
||||
struct RoadConfig {
|
||||
std::string roadMeshTemplate = "road_segment.mesh";
|
||||
float laneWidth = 3.0f;
|
||||
int lanesPerDirection = 1;
|
||||
float roadThickness = 0.3f;
|
||||
float roadLodDistance = 200.0f;
|
||||
float roadVisibilityDistance = 1000.0f;
|
||||
std::string roadMaterialName = "RoadMaterial";
|
||||
};
|
||||
RoadConfig roadConfig;
|
||||
|
||||
// Road network data (node/edge graph).
|
||||
struct RoadNode {
|
||||
Ogre::Vector3 position;
|
||||
@@ -89,10 +101,7 @@ struct TerrainComponent {
|
||||
bool dirty = true;
|
||||
bool rebuildInProgress = false;
|
||||
|
||||
void markDirty()
|
||||
{
|
||||
dirty = true;
|
||||
}
|
||||
void markDirty() { dirty = true; }
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_TERRAIN_HPP
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "EntityName.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/TerrainEditor.hpp"
|
||||
#include <chrono>
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Terrain", "Environment", TerrainComponent,
|
||||
TerrainEditor)
|
||||
@@ -13,8 +14,14 @@ REGISTER_COMPONENT_GROUP("Terrain", "Environment", TerrainComponent,
|
||||
std::make_unique<TerrainEditor>(),
|
||||
/* Adder */
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<TerrainComponent>())
|
||||
e.set<TerrainComponent>({});
|
||||
if (!e.has<TerrainComponent>()) {
|
||||
TerrainComponent tc;
|
||||
tc.terrainId = (uint64_t)std::chrono::
|
||||
system_clock::now()
|
||||
.time_since_epoch()
|
||||
.count();
|
||||
e.set<TerrainComponent>(tc);
|
||||
}
|
||||
},
|
||||
/* Remover */
|
||||
[](flecs::entity e) {
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
#include <iostream>
|
||||
#include "EditorApp.hpp"
|
||||
#include "systems/SceneSerializer.hpp"
|
||||
#include "OgreRoot.h"
|
||||
|
||||
struct ExitAfterFirstFrameListener : public Ogre::FrameListener {
|
||||
Ogre::Root *root;
|
||||
bool triggered = false;
|
||||
ExitAfterFirstFrameListener(Ogre::Root *r) : root(r) {}
|
||||
bool frameRenderingQueued(const Ogre::FrameEvent &) override
|
||||
{
|
||||
if (!triggered) {
|
||||
triggered = true;
|
||||
root->queueEndRendering();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
@@ -10,6 +25,7 @@ int main(int argc, char *argv[])
|
||||
// Parse command line arguments
|
||||
bool gameMode = false;
|
||||
bool debugBuoyancy = false;
|
||||
bool exitAfterFirstFrame = false;
|
||||
Ogre::String sceneFile;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
Ogre::String arg = argv[i];
|
||||
@@ -17,6 +33,8 @@ int main(int argc, char *argv[])
|
||||
gameMode = true;
|
||||
} else if (arg == "--debug-buoyancy") {
|
||||
debugBuoyancy = true;
|
||||
} else if (arg == "--exit-after-first-frame") {
|
||||
exitAfterFirstFrame = true;
|
||||
} else if (arg.length() > 0 && arg[0] != '-') {
|
||||
sceneFile = arg;
|
||||
}
|
||||
@@ -46,6 +64,9 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
}
|
||||
|
||||
ExitAfterFirstFrameListener exitListener(app.getRoot());
|
||||
if (exitAfterFirstFrame)
|
||||
app.getRoot()->addFrameListener(&exitListener);
|
||||
app.getRoot()->startRendering();
|
||||
app.closeApp();
|
||||
} catch (const std::exception &e) {
|
||||
|
||||
@@ -1727,6 +1727,14 @@ JoltPhysicsWrapper::JoltPhysicsWrapper(Ogre::SceneManager *scnMgr,
|
||||
|
||||
JoltPhysicsWrapper::~JoltPhysicsWrapper()
|
||||
{
|
||||
// Tear down the global physics state while the application is still
|
||||
// running. Otherwise the Physics object is destroyed during static
|
||||
// teardown and may crash because Jolt types are still referenced.
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"JoltPhysicsWrapper: tearing down global physics state...");
|
||||
phys.reset();
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"JoltPhysicsWrapper: global physics state torn down");
|
||||
}
|
||||
|
||||
void JoltPhysicsWrapper::update(float dt)
|
||||
|
||||
@@ -12,6 +12,7 @@ FileSystem=resources/buildings/parts/furniture
|
||||
FileSystem=resources/vehicles
|
||||
FileSystem=resources/fonts
|
||||
FileSystem=resources/debug
|
||||
FileSystem=resources/terrain
|
||||
|
||||
[Popular]
|
||||
FileSystem=resources/materials/programs
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "PrefabSystem.hpp"
|
||||
#include "ItemRegistry.hpp"
|
||||
#include "../camera/EditorCamera.hpp"
|
||||
#include "../systems/TerrainSystem.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/Renderable.hpp"
|
||||
@@ -329,6 +330,26 @@ void EditorUISystem::update(float deltaTime)
|
||||
if (!m_editorUIEnabled) {
|
||||
// Only render FPS overlay when editor UI is disabled
|
||||
renderFPSOverlay(deltaTime);
|
||||
/* Sculpt mode: left mouse on terrain applies brush. */
|
||||
{
|
||||
TerrainSystem *ts = TerrainSystem::getInstance();
|
||||
if (ts && ts->isSculpting()) {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
if (ImGui::IsMouseDown(ImGuiMouseButton_Left) &&
|
||||
!io.WantCaptureMouse &&
|
||||
m_editorCamera) {
|
||||
Ogre::Ray ray = m_editorCamera->getMouseRay(
|
||||
io.MousePos.x, io.MousePos.y);
|
||||
float t;
|
||||
Ogre::Vector3 normal;
|
||||
if (ts->raycastTerrain(ray, t, normal)) {
|
||||
Ogre::Vector3 hit = ray.getPoint(t);
|
||||
ts->applySculptBrush(hit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
#include "../components/BuoyancyInfo.hpp"
|
||||
#include "../components/WaterPhysics.hpp"
|
||||
#include "../components/WaterPlane.hpp"
|
||||
#include "../components/Terrain.hpp"
|
||||
#include "TerrainSystem.hpp"
|
||||
#include "../components/Sun.hpp"
|
||||
#include "../components/Skybox.hpp"
|
||||
#include "../components/EventHandler.hpp"
|
||||
@@ -320,6 +322,10 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
|
||||
json["waterPlane"] = serializeWaterPlane(entity);
|
||||
}
|
||||
|
||||
if (entity.has<TerrainComponent>()) {
|
||||
json["terrain"] = serializeTerrain(entity);
|
||||
}
|
||||
|
||||
// ActionDatabase is now a singleton, serialized at scene level
|
||||
if (entity.has<ActionDebug>()) {
|
||||
json["actionDebug"] = serializeActionDebug(entity);
|
||||
@@ -552,6 +558,7 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json,
|
||||
if (json.contains("waterPlane")) {
|
||||
deserializeWaterPlane(entity, json["waterPlane"]);
|
||||
}
|
||||
if (json.contains("terrain")) deserializeTerrain(entity, json["terrain"]);
|
||||
|
||||
// ActionDatabase is now a singleton, deserialized at scene level
|
||||
if (json.contains("actionDebug")) {
|
||||
@@ -858,6 +865,7 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
if (json.contains("waterPlane")) {
|
||||
deserializeWaterPlane(entity, json["waterPlane"]);
|
||||
}
|
||||
if (json.contains("terrain")) deserializeTerrain(entity, json["terrain"]);
|
||||
|
||||
// ActionDatabase is now a singleton, deserialized at scene level
|
||||
if (json.contains("actionDebug")) {
|
||||
@@ -4129,3 +4137,199 @@ void SceneSerializer::deserializeInventory(flecs::entity entity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* TerrainComponent serialization */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
nlohmann::json SceneSerializer::serializeTerrain(flecs::entity entity)
|
||||
{
|
||||
const TerrainComponent &tc = entity.get<TerrainComponent>();
|
||||
nlohmann::json json;
|
||||
|
||||
json["enabled"] = tc.enabled;
|
||||
json["terrainSize"] = tc.terrainSize;
|
||||
json["terrainId"] = tc.terrainId;
|
||||
json["heightmapSize"] = tc.heightmapSize;
|
||||
json["worldSize"] = tc.worldSize;
|
||||
json["maxPixelError"] = tc.maxPixelError;
|
||||
json["compositeMapDistance"] = tc.compositeMapDistance;
|
||||
json["minBatchSize"] = tc.minBatchSize;
|
||||
json["maxBatchSize"] = tc.maxBatchSize;
|
||||
json["heightmapFile"] = tc.heightmapFile;
|
||||
|
||||
nlohmann::json layersJson = nlohmann::json::array();
|
||||
for (auto &l : tc.layers) {
|
||||
nlohmann::json lj;
|
||||
lj["diffuseTexture"] = l.diffuseTexture;
|
||||
lj["normalTexture"] = l.normalTexture;
|
||||
lj["worldSize"] = l.worldSize;
|
||||
layersJson.push_back(lj);
|
||||
}
|
||||
json["layers"] = layersJson;
|
||||
|
||||
nlohmann::json auxMapsJson = nlohmann::json::array();
|
||||
for (auto &am : tc.auxMaps) {
|
||||
nlohmann::json aj;
|
||||
aj["name"] = am.name;
|
||||
aj["fileName"] = am.fileName;
|
||||
aj["resolution"] = am.resolution;
|
||||
aj["defaultValue"] = am.defaultValue;
|
||||
auxMapsJson.push_back(aj);
|
||||
}
|
||||
json["auxMaps"] = auxMapsJson;
|
||||
|
||||
nlohmann::json roadConfigJson;
|
||||
roadConfigJson["roadMeshTemplate"] = tc.roadConfig.roadMeshTemplate;
|
||||
roadConfigJson["laneWidth"] = tc.roadConfig.laneWidth;
|
||||
roadConfigJson["lanesPerDirection"] = tc.roadConfig.lanesPerDirection;
|
||||
roadConfigJson["roadThickness"] = tc.roadConfig.roadThickness;
|
||||
roadConfigJson["roadLodDistance"] = tc.roadConfig.roadLodDistance;
|
||||
roadConfigJson["roadVisibilityDistance"] =
|
||||
tc.roadConfig.roadVisibilityDistance;
|
||||
roadConfigJson["roadMaterialName"] = tc.roadConfig.roadMaterialName;
|
||||
json["roadConfig"] = roadConfigJson;
|
||||
|
||||
nlohmann::json roadNodesJson = nlohmann::json::array();
|
||||
for (auto &n : tc.roadNodes) {
|
||||
nlohmann::json nj;
|
||||
nj["id"] = n.id;
|
||||
nj["position"] = { n.position.x, n.position.y, n.position.z };
|
||||
nj["verticalOffset"] = n.verticalOffset;
|
||||
roadNodesJson.push_back(nj);
|
||||
}
|
||||
json["roadNodes"] = roadNodesJson;
|
||||
|
||||
nlohmann::json roadEdgesJson = nlohmann::json::array();
|
||||
for (auto &e : tc.roadEdges) {
|
||||
nlohmann::json ej;
|
||||
ej["nodeA"] = e.nodeA;
|
||||
ej["nodeB"] = e.nodeB;
|
||||
ej["roadLevelA"] = e.roadLevelA;
|
||||
ej["roadLevelB"] = e.roadLevelB;
|
||||
ej["lanesPerDirectionOverride"] = e.lanesPerDirectionOverride;
|
||||
ej["lanesAtoB"] = e.lanesAtoB;
|
||||
ej["lanesBtoA"] = e.lanesBtoA;
|
||||
|
||||
nlohmann::json sidePrefabsJson = nlohmann::json::array();
|
||||
for (auto &sp : e.sidePrefabs) {
|
||||
nlohmann::json spj;
|
||||
spj["prefabPath"] = sp.prefabPath;
|
||||
spj["edgeT"] = sp.edgeT;
|
||||
spj["sideOffset"] = sp.sideOffset;
|
||||
spj["leftSide"] = sp.leftSide;
|
||||
sidePrefabsJson.push_back(spj);
|
||||
}
|
||||
ej["sidePrefabs"] = sidePrefabsJson;
|
||||
roadEdgesJson.push_back(ej);
|
||||
}
|
||||
json["roadEdges"] = roadEdgesJson;
|
||||
|
||||
/* Persist binary heightmap alongside the JSON. */
|
||||
TerrainSystem *ts = TerrainSystem::getInstance();
|
||||
if (ts)
|
||||
ts->saveSceneHeightmap(tc);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeTerrain(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
TerrainComponent tc;
|
||||
|
||||
tc.enabled = json.value("enabled", true);
|
||||
tc.terrainSize = json.value("terrainSize", 65);
|
||||
tc.terrainId = json.value("terrainId", (uint64_t)0);
|
||||
tc.heightmapSize = json.value("heightmapSize", 256);
|
||||
tc.worldSize = json.value("worldSize", 2000.0f);
|
||||
tc.maxPixelError = json.value("maxPixelError", 1.0f);
|
||||
tc.compositeMapDistance = json.value("compositeMapDistance", 300.0f);
|
||||
tc.minBatchSize = json.value("minBatchSize", 17);
|
||||
tc.maxBatchSize = json.value("maxBatchSize", 65);
|
||||
tc.heightmapFile = json.value("heightmapFile", "heightmap.bin");
|
||||
|
||||
if (json.contains("layers") && json["layers"].is_array()) {
|
||||
for (auto &lj : json["layers"]) {
|
||||
TerrainComponent::Layer layer;
|
||||
layer.diffuseTexture = lj.value("diffuseTexture", "");
|
||||
layer.normalTexture = lj.value("normalTexture", "");
|
||||
layer.worldSize = lj.value("worldSize", 100.0f);
|
||||
tc.layers.push_back(layer);
|
||||
}
|
||||
}
|
||||
|
||||
if (json.contains("auxMaps") && json["auxMaps"].is_array()) {
|
||||
for (auto &aj : json["auxMaps"]) {
|
||||
TerrainComponent::AuxMap am;
|
||||
am.name = aj.value("name", "");
|
||||
am.fileName = aj.value("fileName", "");
|
||||
am.resolution = aj.value("resolution", 256);
|
||||
am.defaultValue = aj.value("defaultValue", 0.0f);
|
||||
tc.auxMaps.push_back(am);
|
||||
}
|
||||
}
|
||||
|
||||
if (json.contains("roadConfig")) {
|
||||
auto &rcj = json["roadConfig"];
|
||||
tc.roadConfig.roadMeshTemplate =
|
||||
rcj.value("roadMeshTemplate", "road_segment.mesh");
|
||||
tc.roadConfig.laneWidth = rcj.value("laneWidth", 3.0f);
|
||||
tc.roadConfig.lanesPerDirection =
|
||||
rcj.value("lanesPerDirection", 1);
|
||||
tc.roadConfig.roadThickness = rcj.value("roadThickness", 0.3f);
|
||||
tc.roadConfig.roadLodDistance = rcj.value("roadLodDistance", 200.0f);
|
||||
tc.roadConfig.roadVisibilityDistance =
|
||||
rcj.value("roadVisibilityDistance", 1000.0f);
|
||||
tc.roadConfig.roadMaterialName =
|
||||
rcj.value("roadMaterialName", "RoadMaterial");
|
||||
}
|
||||
|
||||
if (json.contains("roadNodes") && json["roadNodes"].is_array()) {
|
||||
for (auto &nj : json["roadNodes"]) {
|
||||
TerrainComponent::RoadNode node;
|
||||
node.id = nj.value("id", 0);
|
||||
node.verticalOffset = nj.value("verticalOffset", 0.0f);
|
||||
if (nj.contains("position") && nj["position"].is_array() &&
|
||||
nj["position"].size() >= 3) {
|
||||
node.position.x = nj["position"][0];
|
||||
node.position.y = nj["position"][1];
|
||||
node.position.z = nj["position"][2];
|
||||
}
|
||||
tc.roadNodes.push_back(node);
|
||||
}
|
||||
}
|
||||
|
||||
if (json.contains("roadEdges") && json["roadEdges"].is_array()) {
|
||||
for (auto &ej : json["roadEdges"]) {
|
||||
TerrainComponent::RoadEdge edge;
|
||||
edge.nodeA = ej.value("nodeA", -1);
|
||||
edge.nodeB = ej.value("nodeB", -1);
|
||||
edge.roadLevelA = ej.value("roadLevelA", 0.0f);
|
||||
edge.roadLevelB = ej.value("roadLevelB", 0.0f);
|
||||
edge.lanesPerDirectionOverride =
|
||||
ej.value("lanesPerDirectionOverride", 0);
|
||||
edge.lanesAtoB = ej.value("lanesAtoB", 0);
|
||||
edge.lanesBtoA = ej.value("lanesBtoA", 0);
|
||||
|
||||
if (ej.contains("sidePrefabs") &&
|
||||
ej["sidePrefabs"].is_array()) {
|
||||
for (auto &spj : ej["sidePrefabs"]) {
|
||||
TerrainComponent::RoadEdge::RoadSidePrefab sp;
|
||||
sp.prefabPath = spj.value("prefabPath", "");
|
||||
sp.edgeT = spj.value("edgeT", 0.5f);
|
||||
sp.sideOffset = spj.value("sideOffset", 5.0f);
|
||||
sp.leftSide = spj.value("leftSide", true);
|
||||
edge.sidePrefabs.push_back(sp);
|
||||
}
|
||||
}
|
||||
tc.roadEdges.push_back(edge);
|
||||
}
|
||||
}
|
||||
|
||||
entity.set<TerrainComponent>(tc);
|
||||
|
||||
TerrainSystem *ts = TerrainSystem::getInstance();
|
||||
if (ts)
|
||||
ts->loadSceneHeightmap(tc);
|
||||
}
|
||||
|
||||
@@ -206,6 +206,8 @@ private:
|
||||
|
||||
// WaterPlane serialization
|
||||
nlohmann::json serializeWaterPlane(flecs::entity entity);
|
||||
nlohmann::json serializeTerrain(flecs::entity entity);
|
||||
void deserializeTerrain(flecs::entity entity, const nlohmann::json &json);
|
||||
void deserializeWaterPlane(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
|
||||
|
||||
@@ -7,9 +7,16 @@
|
||||
#include <OgreTerrainGroup.h>
|
||||
#include <OgreTerrainPaging.h>
|
||||
#include <OgreTerrainPagedWorldSection.h>
|
||||
#include <OgrePageManager.h>
|
||||
#include <OgrePage.h>
|
||||
#include <OgrePagedWorld.h>
|
||||
#include <OgreTerrainMaterialGeneratorA.h>
|
||||
#include <OgreMaterial.h>
|
||||
#include <OgreLogManager.h>
|
||||
|
||||
#include <OgreShaderRenderState.h>
|
||||
#include <OgreShaderSubRenderState.h>
|
||||
|
||||
#include <Jolt/Jolt.h>
|
||||
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
|
||||
#include <Jolt/Physics/Body/BodyCreationSettings.h>
|
||||
@@ -18,6 +25,8 @@
|
||||
#include <Jolt/Physics/PhysicsSystem.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <sys/stat.h>
|
||||
|
||||
TerrainSystem *TerrainSystem::s_instance = nullptr;
|
||||
|
||||
@@ -40,68 +49,69 @@ TerrainSystem::~TerrainSystem()
|
||||
deactivate();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* TerrainBodyDrawFilter */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
bool TerrainSystem::TerrainBodyDrawFilter::ShouldDraw(
|
||||
const JPH::Body &inBody) const
|
||||
{
|
||||
if (!showTerrain)
|
||||
return m_terrainIds.find(inBody.GetID()) == m_terrainIds.end();
|
||||
return m_terrainIds.find(inBody.GetID()) ==
|
||||
m_terrainIds.end();
|
||||
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));
|
||||
owner->queueColliderRemove(x, y);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Procedural height helper */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static float proceduralHeight(long worldX, long worldZ)
|
||||
{
|
||||
float h = 0.0f;
|
||||
|
||||
h += 15.0f * sinf((float)worldX * 0.005f) *
|
||||
cosf((float)worldZ * 0.005f);
|
||||
h += 5.0f * sinf((float)worldX * 0.02f + 1.3f) *
|
||||
cosf((float)worldZ * 0.02f + 0.7f);
|
||||
h += 2.0f * sinf((float)worldX * 0.05f + 2.1f) *
|
||||
sinf((float)worldZ * 0.05f + 0.3f);
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* DummyPageProvider */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
bool TerrainSystem::DummyPageProvider::unloadProceduralPage(
|
||||
Ogre::Page *page, Ogre::PagedWorldSection *)
|
||||
{
|
||||
if (!owner || !owner->mTerrainGroup)
|
||||
return true;
|
||||
|
||||
long x, y;
|
||||
owner->mTerrainGroup->unpackIndex(page->getID(), &x, &y);
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: unloading page (" +
|
||||
Ogre::StringConverter::toString(x) + ", " +
|
||||
Ogre::StringConverter::toString(y) + ")");
|
||||
owner->queueColliderRemove(x, y);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* CustomTerrainDefiner */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
m_sys->fillPageHeightData(group, x, y, heightMap);
|
||||
group->defineTerrain(x, y, heightMap);
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
@@ -110,10 +120,10 @@ void TerrainSystem::CustomTerrainDefiner::define(Ogre::TerrainGroup *group,
|
||||
Ogre::StringConverter::toString(y) + ")");
|
||||
|
||||
OGRE_FREE(heightMap, Ogre::MEMCATEGORY_GEOMETRY);
|
||||
|
||||
m_sys->queueColliderCreate(x, y);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* buildPageCollider — zigzag JPH::MeshShape */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
JPH::ShapeRefC TerrainSystem::buildPageCollider(Ogre::Terrain *terrain)
|
||||
@@ -130,8 +140,10 @@ JPH::ShapeRefC TerrainSystem::buildPageCollider(Ogre::Terrain *terrain)
|
||||
for (int x = 0; x < size - 1; ++x) {
|
||||
float vx0 = (float)x * scale - worldSize * 0.5f;
|
||||
float vz0 = worldSize * 0.5f - (float)z * scale;
|
||||
float vx1 = (float)(x + 1) * scale - worldSize * 0.5f;
|
||||
float vz1 = worldSize * 0.5f - (float)(z + 1) * scale;
|
||||
float vx1 = (float)(x + 1) * scale -
|
||||
worldSize * 0.5f;
|
||||
float vz1 = worldSize * 0.5f -
|
||||
(float)(z + 1) * scale;
|
||||
|
||||
float h00 = heightData[z * size + x];
|
||||
float h10 = heightData[z * size + x + 1];
|
||||
@@ -144,13 +156,11 @@ JPH::ShapeRefC TerrainSystem::buildPageCollider(Ogre::Terrain *terrain)
|
||||
JPH::Float3 v11(vx1, h11, vz1);
|
||||
|
||||
if ((z & 1) == 0) {
|
||||
/* Even row: \ diag, Z flipped vs Ogre. */
|
||||
triangles.push_back(
|
||||
JPH::Triangle(v00, v11, v01, 0));
|
||||
triangles.push_back(
|
||||
JPH::Triangle(v00, v10, v11, 0));
|
||||
} else {
|
||||
/* Odd row: / diag, Z flipped vs Ogre. */
|
||||
triangles.push_back(
|
||||
JPH::Triangle(v00, v10, v01, 0));
|
||||
triangles.push_back(
|
||||
@@ -170,10 +180,22 @@ JPH::ShapeRefC TerrainSystem::buildPageCollider(Ogre::Terrain *terrain)
|
||||
return result.Get();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Collider queue helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::queueColliderCreate(long x, long y)
|
||||
{
|
||||
if (!mTerrainGroup)
|
||||
return;
|
||||
|
||||
uint64_t key = mTerrainGroup->packIndex(x, y);
|
||||
if (mColliders.find(key) != mColliders.end())
|
||||
return;
|
||||
if (!mPendingColliderPages.insert(key).second)
|
||||
return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_colliderQueueMutex);
|
||||
mColliderCreateQueue.push_back({ x, y });
|
||||
}
|
||||
|
||||
@@ -190,43 +212,59 @@ void TerrainSystem::processColliderCreates()
|
||||
if (!m_physics || !mTerrainGroup)
|
||||
return;
|
||||
|
||||
while (!mColliderCreateQueue.empty()) {
|
||||
std::lock_guard<std::mutex> lock(m_colliderQueueMutex);
|
||||
|
||||
const size_t initial = mColliderCreateQueue.size();
|
||||
for (size_t i = 0; i < initial && !mColliderCreateQueue.empty();
|
||||
++i) {
|
||||
auto [x, y] = mColliderCreateQueue.front();
|
||||
|
||||
if (mTerrainGroup->isDerivedDataUpdateInProgress())
|
||||
break;
|
||||
|
||||
mColliderCreateQueue.pop_front();
|
||||
|
||||
uint64_t key = mTerrainGroup->packIndex(x, y);
|
||||
if (mColliders.find(key) != mColliders.end())
|
||||
if (mColliders.find(key) != mColliders.end()) {
|
||||
mPendingColliderPages.erase(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
Ogre::Terrain *terrain = mTerrainGroup->getTerrain(x, y);
|
||||
if (!terrain || !terrain->isLoaded())
|
||||
if (!terrain || !terrain->isLoaded()) {
|
||||
mColliderCreateQueue.push_back({ x, y });
|
||||
continue;
|
||||
}
|
||||
|
||||
JPH::ShapeRefC shape = buildPageCollider(terrain);
|
||||
if (!shape)
|
||||
if (!shape) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: collider create - shape build failed (" +
|
||||
Ogre::StringConverter::toString(x) + "," +
|
||||
Ogre::StringConverter::toString(y) + ")");
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Body at terrain scene node position. */
|
||||
Ogre::Vector3 bodyPos =
|
||||
terrain->_getRootSceneNode()->_getDerivedPosition();
|
||||
Ogre::Vector3 pagePos =
|
||||
terrain->_getRootSceneNode()
|
||||
->_getDerivedPosition();
|
||||
|
||||
JPH::BodyCreationSettings bodySettings(
|
||||
shape.GetPtr(),
|
||||
JPH::RVec3(bodyPos.x, bodyPos.y, bodyPos.z),
|
||||
JPH::RVec3(pagePos.x, pagePos.y, pagePos.z),
|
||||
JPH::Quat::sIdentity(),
|
||||
JPH::EMotionType::Static,
|
||||
Layers::NON_MOVING);
|
||||
JPH::BodyID bodyId = m_physics->createBody(bodySettings);
|
||||
if (bodyId.IsInvalid())
|
||||
if (bodyId.IsInvalid()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: collider create - body creation failed (" +
|
||||
Ogre::StringConverter::toString(x) + "," +
|
||||
Ogre::StringConverter::toString(y) + ")");
|
||||
continue;
|
||||
}
|
||||
|
||||
m_physics->addBody(bodyId, JPH::EActivation::DontActivate);
|
||||
m_physics->addBody(bodyId,
|
||||
JPH::EActivation::DontActivate);
|
||||
|
||||
mColliders[key] = { x, y, bodyId, false };
|
||||
mPendingColliderPages.erase(key);
|
||||
mBodyDrawFilter.addTerrainBody(bodyId);
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
@@ -250,9 +288,6 @@ void TerrainSystem::processColliderRemoves()
|
||||
long x = pair.second.pageX;
|
||||
long y = pair.second.pageY;
|
||||
|
||||
if (mTerrainGroup->isDerivedDataUpdateInProgress())
|
||||
break;
|
||||
|
||||
Ogre::Terrain *terrain = mTerrainGroup->getTerrain(x, y);
|
||||
if (terrain && terrain->isLoaded() &&
|
||||
terrain->isDerivedDataUpdateInProgress())
|
||||
@@ -268,6 +303,7 @@ void TerrainSystem::processColliderRemoves()
|
||||
Ogre::StringConverter::toString(x) + ", " +
|
||||
Ogre::StringConverter::toString(y) + ")");
|
||||
}
|
||||
|
||||
for (auto key : toRemove)
|
||||
mColliders.erase(key);
|
||||
}
|
||||
@@ -276,16 +312,304 @@ void TerrainSystem::removeAllColliders()
|
||||
{
|
||||
if (!m_physics)
|
||||
return;
|
||||
|
||||
for (auto &pair : mColliders) {
|
||||
mBodyDrawFilter.removeTerrainBody(pair.second.bodyId);
|
||||
m_physics->removeBody(pair.second.bodyId);
|
||||
m_physics->destroyBody(pair.second.bodyId);
|
||||
}
|
||||
mColliders.clear();
|
||||
mPendingColliderPages.clear();
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_colliderQueueMutex);
|
||||
mColliderCreateQueue.clear();
|
||||
mBodyDrawFilter.clear();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Page collider polling */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::ensurePageColliders()
|
||||
{
|
||||
if (!m_physics || !mTerrainGroup)
|
||||
return;
|
||||
|
||||
for (const auto &kv : mTerrainGroup->getTerrainSlots()) {
|
||||
Ogre::TerrainGroup::TerrainSlot *slot = kv.second;
|
||||
if (!slot || !slot->instance || !slot->instance->isLoaded())
|
||||
continue;
|
||||
|
||||
uint64_t key = mTerrainGroup->packIndex(slot->x, slot->y);
|
||||
if (mColliders.find(key) != mColliders.end())
|
||||
continue;
|
||||
|
||||
queueColliderCreate(slot->x, slot->y);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Heightmap helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
std::string TerrainSystem::getHeightmapPath(const TerrainComponent &tc) const
|
||||
{
|
||||
return "heightmaps/" + Ogre::StringConverter::toString(tc.terrainId) +
|
||||
"/" + tc.heightmapFile;
|
||||
}
|
||||
|
||||
bool TerrainSystem::loadHeightmap(const std::string &path)
|
||||
{
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: failed to open heightmap: " + path);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t res = 0;
|
||||
file.read(reinterpret_cast<char *>(&res), sizeof(res));
|
||||
if (res < 2 || res > 8192) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: invalid heightmap resolution: " +
|
||||
Ogre::StringConverter::toString(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
m_heightmapRes = (int)res;
|
||||
m_heightData.resize(res * res);
|
||||
file.read(reinterpret_cast<char *>(m_heightData.data()),
|
||||
res * res * sizeof(float));
|
||||
m_heightmapLoaded = true;
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: loaded heightmap " + path + " (" +
|
||||
Ogre::StringConverter::toString(res) + "x" +
|
||||
Ogre::StringConverter::toString(res) + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TerrainSystem::saveHeightmap(const std::string &path)
|
||||
{
|
||||
if (!m_heightmapLoaded)
|
||||
return false;
|
||||
|
||||
size_t lastSlash = path.rfind('/');
|
||||
if (lastSlash != std::string::npos) {
|
||||
std::string dir = path.substr(0, lastSlash);
|
||||
mkdir(dir.c_str(), 0755);
|
||||
}
|
||||
|
||||
std::ofstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: failed to create heightmap file: " +
|
||||
path);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t res = (uint32_t)m_heightmapRes;
|
||||
file.write(reinterpret_cast<const char *>(&res), sizeof(res));
|
||||
file.write(reinterpret_cast<const char *>(m_heightData.data()),
|
||||
m_heightmapRes * m_heightmapRes * sizeof(float));
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: saved heightmap " + path + " (" +
|
||||
Ogre::StringConverter::toString(res) + "x" +
|
||||
Ogre::StringConverter::toString(res) + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TerrainSystem::loadSceneHeightmap(const TerrainComponent &tc)
|
||||
{
|
||||
return loadHeightmap(getHeightmapPath(tc));
|
||||
}
|
||||
|
||||
bool TerrainSystem::saveSceneHeightmap(const TerrainComponent &tc)
|
||||
{
|
||||
return saveHeightmap(getHeightmapPath(tc));
|
||||
}
|
||||
|
||||
void TerrainSystem::ensureHeightmapLoaded(TerrainComponent &tc)
|
||||
{
|
||||
if (m_heightmapLoaded)
|
||||
return;
|
||||
|
||||
if (loadSceneHeightmap(tc))
|
||||
return;
|
||||
|
||||
m_heightmapRes = tc.heightmapSize;
|
||||
m_heightData.resize(m_heightmapRes * m_heightmapRes);
|
||||
|
||||
for (int z = 0; z < m_heightmapRes; ++z) {
|
||||
for (int x = 0; x < m_heightmapRes; ++x) {
|
||||
const float fx = (float)x / (float)(m_heightmapRes - 1);
|
||||
const float fz = (float)z / (float)(m_heightmapRes - 1);
|
||||
const float wx = m_heightmapWorldMinX +
|
||||
fx * m_heightmapWorldSize;
|
||||
const float wz = m_heightmapWorldMinZ +
|
||||
fz * m_heightmapWorldSize;
|
||||
m_heightData[z * m_heightmapRes + x] =
|
||||
proceduralHeight((long)wx, (long)wz);
|
||||
}
|
||||
}
|
||||
m_heightmapLoaded = true;
|
||||
}
|
||||
|
||||
void TerrainSystem::fillPageHeightData(Ogre::TerrainGroup *group, long x,
|
||||
long y, float *heightMap)
|
||||
{
|
||||
const Ogre::uint16 terrainSize = group->getTerrainSize();
|
||||
const Ogre::Real worldSize = group->getTerrainWorldSize();
|
||||
|
||||
Ogre::Vector3 worldPos;
|
||||
group->convertTerrainSlotToWorldPosition(x, y, &worldPos);
|
||||
const Ogre::Real step = worldSize / Ogre::Real(terrainSize - 1);
|
||||
|
||||
for (int j = 0; j < terrainSize; ++j) {
|
||||
for (int i = 0; i < terrainSize; ++i) {
|
||||
const long wx = (long)(worldPos.x +
|
||||
(Ogre::Real)i * step);
|
||||
const long wz = (long)(worldPos.z +
|
||||
(Ogre::Real)j * step);
|
||||
heightMap[j * terrainSize + i] =
|
||||
sampleHeightAt(wx, wz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float TerrainSystem::sampleHeightAt(long worldX, long worldZ) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_heightmapMutex);
|
||||
return sampleHeightAtLocked(worldX, worldZ);
|
||||
}
|
||||
|
||||
float TerrainSystem::sampleHeightAtLocked(long worldX, long worldZ) const
|
||||
{
|
||||
if (!m_heightmapLoaded || m_heightData.empty())
|
||||
return proceduralHeight(worldX, worldZ);
|
||||
|
||||
float fx = (float)worldX / m_heightmapWorldSize *
|
||||
(float)m_heightmapRes;
|
||||
float fz = (float)worldZ / m_heightmapWorldSize *
|
||||
(float)m_heightmapRes;
|
||||
|
||||
int x0 = (int)floorf(fx);
|
||||
int z0 = (int)floorf(fz);
|
||||
int x1 = x0 + 1;
|
||||
int z1 = z0 + 1;
|
||||
|
||||
int r = m_heightmapRes;
|
||||
x0 = std::max(0, std::min(x0, r - 1));
|
||||
x1 = std::max(0, std::min(x1, r - 1));
|
||||
z0 = std::max(0, std::min(z0, r - 1));
|
||||
z1 = std::max(0, std::min(z1, r - 1));
|
||||
|
||||
float tx = fx - (float)x0;
|
||||
float tz = fz - (float)z0;
|
||||
|
||||
float h00 = m_heightData[z0 * r + x0];
|
||||
float h10 = m_heightData[z0 * r + x1];
|
||||
float h01 = m_heightData[z1 * r + x0];
|
||||
float h11 = m_heightData[z1 * r + x1];
|
||||
|
||||
return (1.0f - tx) * (1.0f - tz) * h00 +
|
||||
tx * (1.0f - tz) * h10 +
|
||||
(1.0f - tx) * tz * h01 +
|
||||
tx * tz * h11;
|
||||
}
|
||||
|
||||
void TerrainSystem::setHeightAt(long worldX, long worldZ, float value)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_heightmapMutex);
|
||||
if (!m_heightmapLoaded)
|
||||
return;
|
||||
|
||||
int x = (int)((float)worldX / m_heightmapWorldSize *
|
||||
(float)m_heightmapRes);
|
||||
int z = (int)((float)worldZ / m_heightmapWorldSize *
|
||||
(float)m_heightmapRes);
|
||||
if (x < 0 || x >= m_heightmapRes || z < 0 || z >= m_heightmapRes)
|
||||
return;
|
||||
m_heightData[z * m_heightmapRes + x] = value;
|
||||
}
|
||||
|
||||
long TerrainSystem::worldToPage(float worldCoord, float worldSize) const
|
||||
{
|
||||
return (long)floorf(worldCoord / worldSize);
|
||||
}
|
||||
|
||||
void TerrainSystem::markPageDirty(long pageX, long pageY)
|
||||
{
|
||||
if (mTerrainGroup)
|
||||
m_dirtyPages.insert(mTerrainGroup->packIndex(pageX, pageY));
|
||||
}
|
||||
|
||||
void TerrainSystem::updatePageGeometry(long pageX, long pageY)
|
||||
{
|
||||
if (!mTerrainGroup)
|
||||
return;
|
||||
|
||||
Ogre::Terrain *terrain = mTerrainGroup->getTerrain(pageX, pageY);
|
||||
if (!terrain || !terrain->isLoaded())
|
||||
return;
|
||||
|
||||
uint16_t size = terrain->getSize();
|
||||
float *temp = OGRE_ALLOC_T(float, size * size,
|
||||
Ogre::MEMCATEGORY_GEOMETRY);
|
||||
fillPageHeightData(mTerrainGroup, pageX, pageY, temp);
|
||||
|
||||
float *dest = terrain->getHeightData();
|
||||
for (int i = 0; i < size * size; ++i)
|
||||
dest[i] = temp[i];
|
||||
OGRE_FREE(temp, Ogre::MEMCATEGORY_GEOMETRY);
|
||||
|
||||
terrain->dirty();
|
||||
terrain->update(true);
|
||||
}
|
||||
|
||||
void TerrainSystem::rebuildDirtyPages()
|
||||
{
|
||||
if (!mTerrainGroup || m_dirtyPages.empty())
|
||||
return;
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: rebuilding " +
|
||||
Ogre::StringConverter::toString((int)m_dirtyPages.size()) +
|
||||
" dirty pages");
|
||||
|
||||
for (uint64_t key : m_dirtyPages) {
|
||||
long x, y;
|
||||
mTerrainGroup->unpackIndex(key, &x, &y);
|
||||
|
||||
auto it = mColliders.find(key);
|
||||
if (it != mColliders.end() && !it->second.bodyId.IsInvalid()) {
|
||||
mBodyDrawFilter.removeTerrainBody(it->second.bodyId);
|
||||
m_physics->removeBody(it->second.bodyId);
|
||||
m_physics->destroyBody(it->second.bodyId);
|
||||
mColliders.erase(it);
|
||||
}
|
||||
|
||||
updatePageGeometry(x, y);
|
||||
queueColliderCreate(x, y);
|
||||
}
|
||||
m_dirtyPages.clear();
|
||||
}
|
||||
|
||||
void TerrainSystem::processDeferredReloads()
|
||||
{
|
||||
if (!mTerrainGroup || m_reloadQueue.empty())
|
||||
return;
|
||||
|
||||
for (uint64_t key : m_reloadQueue) {
|
||||
long x, y;
|
||||
mTerrainGroup->unpackIndex(key, &x, &y);
|
||||
updatePageGeometry(x, y);
|
||||
queueColliderCreate(x, y);
|
||||
}
|
||||
m_reloadQueue.clear();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* activate */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform)
|
||||
@@ -293,6 +617,21 @@ void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform)
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TerrainSystem: activating terrain...");
|
||||
|
||||
deactivate();
|
||||
|
||||
/* Paging range and heightmap world area must be known before the
|
||||
* heightmap is generated or sampled. */
|
||||
m_pageMinX = -1;
|
||||
m_pageMinY = -1;
|
||||
m_pageMaxX = 1;
|
||||
m_pageMaxY = 1;
|
||||
m_heightmapWorldMinX = (float)m_pageMinX * tc.worldSize;
|
||||
m_heightmapWorldMinZ = (float)m_pageMinY * tc.worldSize;
|
||||
m_heightmapWorldSize = (float)(m_pageMaxX - m_pageMinX + 1) *
|
||||
tc.worldSize;
|
||||
|
||||
ensureHeightmapLoaded(tc);
|
||||
|
||||
mTerrainGlobals = OGRE_NEW Ogre::TerrainGlobalOptions();
|
||||
mTerrainGlobals->setMaxPixelError(tc.maxPixelError);
|
||||
mTerrainGlobals->setCompositeMapDistance(tc.compositeMapDistance);
|
||||
@@ -304,15 +643,15 @@ void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform)
|
||||
|
||||
mTerrainGroup = OGRE_NEW Ogre::TerrainGroup(
|
||||
m_sceneMgr, Ogre::Terrain::ALIGN_X_Z,
|
||||
tc.terrainSize, tc.worldSize);
|
||||
(uint16_t)tc.terrainSize, tc.worldSize);
|
||||
mTerrainGroup->setOrigin(xform.position);
|
||||
|
||||
Ogre::Terrain::ImportData &defaultImp =
|
||||
mTerrainGroup->getDefaultImportSettings();
|
||||
defaultImp.terrainSize = tc.terrainSize;
|
||||
defaultImp.terrainSize = (uint16_t)tc.terrainSize;
|
||||
defaultImp.worldSize = tc.worldSize;
|
||||
defaultImp.minBatchSize = tc.minBatchSize;
|
||||
defaultImp.maxBatchSize = tc.maxBatchSize;
|
||||
defaultImp.minBatchSize = (uint16_t)tc.minBatchSize;
|
||||
defaultImp.maxBatchSize = (uint16_t)tc.maxBatchSize;
|
||||
defaultImp.inputScale = 1.0f;
|
||||
defaultImp.layerDeclaration = matGen->getLayerDeclaration();
|
||||
|
||||
@@ -332,37 +671,57 @@ void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform)
|
||||
}
|
||||
}
|
||||
|
||||
/* Editor-friendly paging: a small fixed world section with procedural
|
||||
* pages. The DummyPageProvider lets us hook unload so colliders are
|
||||
* removed safely, while CustomTerrainDefiner re-samples the heightmap
|
||||
* when a page is (re)loaded. */
|
||||
mPageManager = OGRE_NEW Ogre::PageManager();
|
||||
mDummyPageProvider = std::make_unique<DummyPageProvider>();
|
||||
mDummyPageProvider->owner = this;
|
||||
mPageManager->setPageProvider(mDummyPageProvider.get());
|
||||
mPageManager->addCamera(m_camera);
|
||||
|
||||
mTerrainPaging = OGRE_NEW Ogre::TerrainPaging(mPageManager);
|
||||
mPagedWorld = mPageManager->createWorld();
|
||||
|
||||
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);
|
||||
m_pageMinX, m_pageMinY, m_pageMaxX, m_pageMaxY);
|
||||
|
||||
mTerrainDefiner = std::make_unique<CustomTerrainDefiner>(this);
|
||||
mTerrainPagedWorldSection->setDefiner(mTerrainDefiner.get());
|
||||
|
||||
mTerrainGroup->freeTemporaryResources();
|
||||
|
||||
/* Editor mode: define and synchronously load every page directly through
|
||||
* the TerrainGroup. This avoids leaving background WorkQueue tasks from
|
||||
* the paging load chain, which otherwise can deadlock or hang shutdown.
|
||||
* The paging objects are still set up so runtime game mode can attach a
|
||||
* camera and use page unload hooks (DummyPageProvider) safely. */
|
||||
for (long py = m_pageMinY; py <= m_pageMaxY; ++py) {
|
||||
for (long px = m_pageMinX; px <= m_pageMaxX; ++px) {
|
||||
Ogre::uint16 terrainSize = mTerrainGroup->getTerrainSize();
|
||||
float *heightMap = OGRE_ALLOC_T(
|
||||
float, terrainSize * terrainSize,
|
||||
Ogre::MEMCATEGORY_GEOMETRY);
|
||||
fillPageHeightData(mTerrainGroup, px, py, heightMap);
|
||||
mTerrainGroup->defineTerrain(px, py, heightMap);
|
||||
OGRE_FREE(heightMap, Ogre::MEMCATEGORY_GEOMETRY);
|
||||
}
|
||||
}
|
||||
mTerrainGroup->loadAllTerrains(true);
|
||||
|
||||
if (m_physics)
|
||||
m_physics->setBodyDrawFilter(&mBodyDrawFilter);
|
||||
processColliderCreates();
|
||||
|
||||
m_active = true;
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TerrainSystem: activation complete");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* deactivate */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::deactivate()
|
||||
{
|
||||
if (!m_active && !mTerrainGroup)
|
||||
@@ -372,65 +731,99 @@ void TerrainSystem::deactivate()
|
||||
"TerrainSystem: deactivating...");
|
||||
|
||||
m_active = false;
|
||||
|
||||
if (mTerrainGroup && mTerrainGroup->isDerivedDataUpdateInProgress()) {
|
||||
do {
|
||||
mTerrainGroup->update(false);
|
||||
} while (mTerrainGroup->isDerivedDataUpdateInProgress());
|
||||
}
|
||||
m_terrainEntityId = 0;
|
||||
m_dirtyPages.clear();
|
||||
m_reloadQueue.clear();
|
||||
|
||||
removeAllColliders();
|
||||
if (m_physics)
|
||||
m_physics->setBodyDrawFilter(nullptr);
|
||||
|
||||
if (mPageManager && m_camera)
|
||||
mPageManager->removeCamera(m_camera);
|
||||
/* Stop paging from loading/unloading pages while we tear down. */
|
||||
if (mPageManager)
|
||||
mPageManager->setPagingOperationsEnabled(false);
|
||||
|
||||
/* Release RTShader SubRenderState objects held by the terrain material
|
||||
* generator before the RTSS is torn down. The persistent instances live
|
||||
* in the generator's main RenderState; remove them explicitly so the
|
||||
* factories can be destroyed cleanly. */
|
||||
if (mTerrainGlobals) {
|
||||
Ogre::TerrainMaterialGeneratorPtr gen =
|
||||
mTerrainGlobals->getDefaultMaterialGenerator();
|
||||
if (gen) {
|
||||
auto *genA = dynamic_cast<Ogre::TerrainMaterialGeneratorA *>(
|
||||
gen.get());
|
||||
if (genA) {
|
||||
Ogre::RTShader::RenderState *rs =
|
||||
genA->getMainRenderState();
|
||||
if (rs) {
|
||||
Ogre::RTShader::SubRenderStateList srs =
|
||||
rs->getSubRenderStates();
|
||||
for (Ogre::RTShader::SubRenderState *sub : srs)
|
||||
rs->removeSubRenderState(sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mTerrainDefiner.reset();
|
||||
if (mTerrainGroup)
|
||||
mTerrainGroup->removeAllTerrains();
|
||||
mTerrainPagedWorldSection = nullptr;
|
||||
mPagedWorld = nullptr;
|
||||
mDummyPageProvider.reset();
|
||||
|
||||
/* The PagedWorld owns the TerrainPagedWorldSection, which in turn owns
|
||||
* the TerrainGroup. PageManager::~PageManager does not delete the worlds
|
||||
* it manages, so destroying the PageManager here leaks the world/section
|
||||
* and the group. This is intentional: deleting the group can hang or
|
||||
* crash on some GL drivers, and leaking it at shutdown is harmless. */
|
||||
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();
|
||||
if (mTerrainGlobals) {
|
||||
mTerrainGlobals->setDefaultMaterialGenerator(
|
||||
Ogre::TerrainMaterialGeneratorPtr());
|
||||
}
|
||||
|
||||
mTerrainGlobals = nullptr;
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TerrainSystem: deactivated");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* update */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::update(float /*deltaTime*/)
|
||||
{
|
||||
bool found = false;
|
||||
flecs::entity_t foundEntity = 0;
|
||||
|
||||
m_terrainQuery.each([&](flecs::entity e, TerrainComponent &tc,
|
||||
TransformComponent &xform) {
|
||||
(void)e;
|
||||
found = true;
|
||||
foundEntity = e.id();
|
||||
|
||||
if (!tc.enabled && m_active) {
|
||||
deactivate();
|
||||
return;
|
||||
}
|
||||
if (tc.enabled && !m_active)
|
||||
if (tc.enabled && !m_active) {
|
||||
activate(tc, xform);
|
||||
m_terrainEntityId = e.id();
|
||||
}
|
||||
});
|
||||
|
||||
/* If the terrain entity or its component was removed, tear down. */
|
||||
if (m_active && !found)
|
||||
if (m_active && foundEntity == 0) {
|
||||
deactivate();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_active && mTerrainGroup)
|
||||
mTerrainGroup->update(false);
|
||||
rebuildDirtyPages();
|
||||
processDeferredReloads();
|
||||
|
||||
ensurePageColliders();
|
||||
processColliderRemoves();
|
||||
processColliderCreates();
|
||||
|
||||
@@ -452,13 +845,13 @@ bool TerrainSystem::raycastTerrain(const Ogre::Ray &ray, float &outT,
|
||||
if (!m_active || !m_physics || mColliders.empty())
|
||||
return false;
|
||||
|
||||
JPH::RVec3 origin(ray.getOrigin().x, ray.getOrigin().y,
|
||||
ray.getOrigin().z);
|
||||
const JPH::RVec3 origin(ray.getOrigin().x, ray.getOrigin().y,
|
||||
ray.getOrigin().z);
|
||||
JPH::Vec3 dir(ray.getDirection().x, ray.getDirection().y,
|
||||
ray.getDirection().z);
|
||||
dir = dir.Normalized();
|
||||
|
||||
JPH::RRayCast rayCast(origin, dir);
|
||||
const JPH::RRayCast rayCast(origin, dir);
|
||||
|
||||
JPH::PhysicsSystem *jphSys = m_physics->getPhysicsSystem();
|
||||
if (!jphSys)
|
||||
@@ -466,8 +859,9 @@ bool TerrainSystem::raycastTerrain(const Ogre::Ray &ray, float &outT,
|
||||
|
||||
JPH::RayCastResult hit;
|
||||
if (!jphSys->GetNarrowPhaseQuery().CastRay(rayCast, hit,
|
||||
JPH::BroadPhaseLayerFilter(), JPH::ObjectLayerFilter(),
|
||||
JPH::BodyFilter()))
|
||||
JPH::BroadPhaseLayerFilter(),
|
||||
JPH::ObjectLayerFilter(),
|
||||
JPH::BodyFilter()))
|
||||
return false;
|
||||
|
||||
for (auto &pair : mColliders) {
|
||||
@@ -484,3 +878,130 @@ void TerrainSystem::setShowTerrainColliders(bool show)
|
||||
{
|
||||
m_showTerrainColliders = show;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* snapCameraAboveTerrain */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::snapCameraAboveTerrain()
|
||||
{
|
||||
if (!m_active || !m_sceneMgr)
|
||||
return;
|
||||
|
||||
Ogre::SceneNode *targetNode =
|
||||
m_sceneMgr->getSceneNode("EditorCameraTarget");
|
||||
if (!targetNode)
|
||||
targetNode = m_sceneMgr->getSceneNode(
|
||||
"EditorCameraTargetNode");
|
||||
if (!targetNode)
|
||||
return;
|
||||
|
||||
Ogre::Vector3 pos = targetNode->getPosition();
|
||||
float h = getHeightAt(pos);
|
||||
if (pos.y < h + 5.0f)
|
||||
targetNode->setPosition(pos.x, h + 5.0f, pos.z);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* applySculptBrush */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::applySculptBrush(const Ogre::Vector3 &worldPos)
|
||||
{
|
||||
if (!m_heightmapLoaded || !m_active || !mTerrainGroup)
|
||||
return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_heightmapMutex);
|
||||
|
||||
float spacing = m_heightmapWorldSize / (float)(m_heightmapRes - 1);
|
||||
int rSamples = (int)(m_sculptRadius / spacing) + 1;
|
||||
int cx = (int)((worldPos.x - m_heightmapWorldMinX) /
|
||||
m_heightmapWorldSize * (float)m_heightmapRes);
|
||||
int cz = (int)((worldPos.z - m_heightmapWorldMinZ) /
|
||||
m_heightmapWorldSize * (float)m_heightmapRes);
|
||||
int x0 = std::max(0, cx - rSamples);
|
||||
int x1 = std::min(m_heightmapRes - 1, cx + rSamples);
|
||||
int z0 = std::max(0, cz - rSamples);
|
||||
int z1 = std::min(m_heightmapRes - 1, cz + rSamples);
|
||||
|
||||
if (m_sculptTool == SculptTool::Smooth) {
|
||||
std::vector<std::pair<int, int>> s;
|
||||
for (int z = z0; z <= z1; ++z)
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
float dx = ((float)x - (float)cx) * spacing;
|
||||
float dz = ((float)z - (float)cz) * spacing;
|
||||
if (dx * dx + dz * dz <=
|
||||
m_sculptRadius * m_sculptRadius)
|
||||
s.push_back({ x, z });
|
||||
}
|
||||
float avg = 0;
|
||||
for (auto &p : s)
|
||||
avg += m_heightData[p.second * m_heightmapRes +
|
||||
p.first];
|
||||
avg /= (float)s.size();
|
||||
for (auto &p : s) {
|
||||
float &h = m_heightData[p.second * m_heightmapRes +
|
||||
p.first];
|
||||
h = avg * m_sculptStrength +
|
||||
h * (1.0f - m_sculptStrength);
|
||||
}
|
||||
} else if (m_sculptTool == SculptTool::Flatten) {
|
||||
float ch = m_heightData[cz * m_heightmapRes + cx];
|
||||
for (int z = z0; z <= z1; ++z)
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
float dx = ((float)x - (float)cx) * spacing;
|
||||
float dz = ((float)z - (float)cz) * spacing;
|
||||
if (dx * dx + dz * dz <=
|
||||
m_sculptRadius * m_sculptRadius) {
|
||||
float &h = m_heightData[z * m_heightmapRes +
|
||||
x];
|
||||
h = h * (1.0f - m_sculptStrength) +
|
||||
ch * m_sculptStrength;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
float sign = (m_sculptTool == SculptTool::Raise) ?
|
||||
1.0f :
|
||||
-1.0f;
|
||||
for (int z = z0; z <= z1; ++z)
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
float dx = ((float)x - (float)cx) * spacing;
|
||||
float dz = ((float)z - (float)cz) * spacing;
|
||||
float d2 = dx * dx + dz * dz;
|
||||
if (d2 <= m_sculptRadius * m_sculptRadius) {
|
||||
float falloff = 1.0f -
|
||||
sqrtf(d2) / m_sculptRadius;
|
||||
m_heightData[z * m_heightmapRes + x] +=
|
||||
sign * m_sculptStrength * falloff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float ws = mTerrainGroup->getTerrainWorldSize();
|
||||
long px0 = worldToPage(worldPos.x - m_sculptRadius, ws);
|
||||
long px1 = worldToPage(worldPos.x + m_sculptRadius, ws);
|
||||
long pz0 = worldToPage(worldPos.z - m_sculptRadius, ws);
|
||||
long pz1 = worldToPage(worldPos.z + m_sculptRadius, ws);
|
||||
for (long pz = pz0; pz <= pz1; ++pz)
|
||||
for (long px = px0; px <= px1; ++px)
|
||||
markPageDirty(px, pz);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* applySplatBrush */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::applySplatBrush(const Ogre::Vector3 & /*worldPos*/)
|
||||
{
|
||||
/* Not implemented yet. */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* destroyCollider */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::destroyCollider(uint64_t /*key*/,
|
||||
TerrainCollider & /*collider*/)
|
||||
{
|
||||
/* Legacy helper; colliders are removed directly via processColliderRemoves. */
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <unordered_map>
|
||||
#include <set>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <cstdint>
|
||||
|
||||
#include <Jolt/Jolt.h>
|
||||
@@ -49,9 +50,46 @@ public:
|
||||
return m_showTerrainColliders;
|
||||
}
|
||||
|
||||
/* --- Heightmap data (M3) --- */
|
||||
float sampleHeightAt(long worldX, long worldZ) const;
|
||||
float sampleHeightAtLocked(long worldX, long worldZ) const;
|
||||
void setHeightAt(long worldX, long worldZ, float value);
|
||||
bool loadHeightmap(const std::string &path);
|
||||
bool saveHeightmap(const std::string &path);
|
||||
bool loadSceneHeightmap(const struct TerrainComponent &tc);
|
||||
bool saveSceneHeightmap(const struct TerrainComponent &tc);
|
||||
std::string getHeightmapPath(const struct TerrainComponent &tc) const;
|
||||
|
||||
void markPageDirty(long pageX, long pageY);
|
||||
void rebuildDirtyPages();
|
||||
void processDeferredReloads();
|
||||
|
||||
/* --- Sculpting (M3) --- */
|
||||
enum class SculptTool { Raise, Lower, Smooth, Flatten, SplatPaint };
|
||||
bool getSculptMode() const { return m_sculptMode; }
|
||||
void setSculptMode(bool v) { m_sculptMode = v; }
|
||||
bool isSculpting() const { return m_sculptMode; }
|
||||
SculptTool getSculptTool() const { return m_sculptTool; }
|
||||
void setSculptTool(SculptTool t) { m_sculptTool = t; }
|
||||
float getSculptRadius() const { return m_sculptRadius; }
|
||||
void setSculptRadius(float r) { m_sculptRadius = r; }
|
||||
float getSculptStrength() const { return m_sculptStrength; }
|
||||
void setSculptStrength(float s) { m_sculptStrength = s; }
|
||||
int getSculptLayerIndex() const { return m_sculptLayerIndex; }
|
||||
void setSculptLayerIndex(int i) { m_sculptLayerIndex = i; }
|
||||
void snapCameraAboveTerrain();
|
||||
void applySculptBrush(const Ogre::Vector3 &worldPos);
|
||||
void applySplatBrush(const Ogre::Vector3 &worldPos);
|
||||
|
||||
private:
|
||||
void activate(struct TerrainComponent &tc,
|
||||
struct TransformComponent &xform);
|
||||
void ensureHeightmapLoaded(struct TerrainComponent &tc);
|
||||
void updatePageGeometry(long pageX, long pageY);
|
||||
void fillPageHeightData(Ogre::TerrainGroup *group, long x, long y,
|
||||
float *heightMap);
|
||||
long worldToPage(float worldCoord, float worldSize) const;
|
||||
void ensurePageColliders();
|
||||
|
||||
struct TerrainCollider {
|
||||
long pageX, pageY;
|
||||
@@ -59,6 +97,42 @@ private:
|
||||
bool pendingRemove = false;
|
||||
};
|
||||
|
||||
class DummyPageProvider : public Ogre::PageProvider {
|
||||
public:
|
||||
bool prepareProceduralPage(Ogre::Page *,
|
||||
Ogre::PagedWorldSection *) override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
bool loadProceduralPage(Ogre::Page *,
|
||||
Ogre::PagedWorldSection *) override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
bool unloadProceduralPage(Ogre::Page *page,
|
||||
Ogre::PagedWorldSection *) override;
|
||||
bool unprepareProceduralPage(Ogre::Page *,
|
||||
Ogre::PagedWorldSection *) override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
TerrainSystem *owner = nullptr;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
class TerrainBodyDrawFilter : public JPH::BodyDrawFilter {
|
||||
public:
|
||||
bool ShouldDraw(const JPH::Body &inBody) const override;
|
||||
@@ -76,43 +150,13 @@ private:
|
||||
std::set<JPH::BodyID> m_terrainIds;
|
||||
};
|
||||
|
||||
class DummyPageProvider : public Ogre::PageProvider {
|
||||
public:
|
||||
bool prepareProceduralPage(Ogre::Page *, Ogre::PagedWorldSection *) override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
bool loadProceduralPage(Ogre::Page *, Ogre::PagedWorldSection *) override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
bool unloadProceduralPage(Ogre::Page *page, Ogre::PagedWorldSection *) override;
|
||||
bool unprepareProceduralPage(Ogre::Page *, Ogre::PagedWorldSection *) override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
TerrainSystem *owner = nullptr;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
JPH::ShapeRefC buildPageCollider(Ogre::Terrain *terrain);
|
||||
void queueColliderCreate(long x, long y);
|
||||
void queueColliderRemove(long x, long y);
|
||||
void processColliderCreates();
|
||||
void processColliderRemoves();
|
||||
void removeAllColliders();
|
||||
void destroyCollider(uint64_t key, TerrainCollider &collider);
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
@@ -134,8 +178,38 @@ private:
|
||||
|
||||
std::unordered_map<uint64_t, TerrainCollider> mColliders;
|
||||
std::deque<std::pair<long, long>> mColliderCreateQueue;
|
||||
std::set<uint64_t> mPendingColliderPages;
|
||||
std::mutex m_colliderQueueMutex;
|
||||
TerrainBodyDrawFilter mBodyDrawFilter;
|
||||
|
||||
/* Paging range used for synchronous page load and collider polling. */
|
||||
long m_pageMinX = -1, m_pageMinY = -1;
|
||||
long m_pageMaxX = 1, m_pageMaxY = 1;
|
||||
|
||||
/* Heightmap data (M3) — protected by m_heightmapMutex because
|
||||
* collider building and page updates may happen concurrently. */
|
||||
mutable std::mutex m_heightmapMutex;
|
||||
std::vector<float> m_heightData;
|
||||
bool m_heightmapLoaded = false;
|
||||
int m_heightmapRes = 0;
|
||||
float m_heightmapWorldMinX = 0;
|
||||
float m_heightmapWorldMinZ = 0;
|
||||
float m_heightmapWorldSize = 2000.0f;
|
||||
|
||||
std::set<uint64_t> m_dirtyPages;
|
||||
std::vector<uint64_t> m_reloadQueue;
|
||||
bool hasHeightmap() const { return m_heightmapLoaded; }
|
||||
|
||||
/* Sculpting state */
|
||||
bool m_sculptMode = false;
|
||||
SculptTool m_sculptTool = SculptTool::Raise;
|
||||
float m_sculptRadius = 5.0f;
|
||||
float m_sculptStrength = 0.5f;
|
||||
int m_sculptLayerIndex = 0;
|
||||
|
||||
/* Entity tracking */
|
||||
flecs::entity_t m_terrainEntityId = 0;
|
||||
|
||||
flecs::query<struct TerrainComponent, struct TransformComponent>
|
||||
m_terrainQuery;
|
||||
};
|
||||
|
||||
@@ -39,8 +39,63 @@ public:
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Read-only display of sizing params (restart needed to change).
|
||||
* In M2+ these get Restart Terrain buttons. */
|
||||
/* --- Sculpting (M3) --- */
|
||||
if (ts) {
|
||||
ImGui::Text("Sculpting");
|
||||
bool sculpt = ts->getSculptMode();
|
||||
if (ImGui::Checkbox("Sculpt Mode", &sculpt))
|
||||
ts->setSculptMode(sculpt);
|
||||
if (sculpt)
|
||||
ImGui::TextColored(ImVec4(0, 1, 0, 1),
|
||||
"SCULPT MODE ACTIVE");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(ESC to exit)");
|
||||
|
||||
const char *tools[] = { "Raise", "Lower", "Smooth",
|
||||
"Flatten", "Splat Paint" };
|
||||
int tool = (int)ts->getSculptTool();
|
||||
if (ImGui::Combo("Tool", &tool, tools,
|
||||
IM_ARRAYSIZE(tools)))
|
||||
ts->setSculptTool((TerrainSystem::SculptTool)tool);
|
||||
|
||||
float radius = ts->getSculptRadius();
|
||||
if (ImGui::SliderFloat("Radius", &radius, 0.5f, 50.0f))
|
||||
ts->setSculptRadius(radius);
|
||||
|
||||
float strength = ts->getSculptStrength();
|
||||
if (ImGui::SliderFloat("Strength", &strength, 0.01f,
|
||||
1.0f))
|
||||
ts->setSculptStrength(strength);
|
||||
|
||||
if (ts->getSculptTool() ==
|
||||
TerrainSystem::SculptTool::SplatPaint) {
|
||||
int li = ts->getSculptLayerIndex();
|
||||
if (ImGui::InputInt("Layer Index", &li, 1, 1))
|
||||
ts->setSculptLayerIndex(
|
||||
std::max(0, li));
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* --- Camera & file operations --- */
|
||||
if (ts) {
|
||||
if (ImGui::Button("Snap Camera Above Terrain"))
|
||||
ts->snapCameraAboveTerrain();
|
||||
ImGui::SameLine();
|
||||
const std::string hmPath = ts->getHeightmapPath(tc);
|
||||
if (ImGui::Button("Save Heightmap"))
|
||||
ts->saveHeightmap(hmPath);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Load Heightmap"))
|
||||
ts->loadHeightmap(hmPath);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Read-only display of sizing params */
|
||||
ImGui::Text("Terrain ID: %llu",
|
||||
(unsigned long long)tc.terrainId);
|
||||
ImGui::Text("Page size: %d x %d vertices", tc.terrainSize,
|
||||
tc.terrainSize);
|
||||
ImGui::Text("World size: %.0f units", tc.worldSize);
|
||||
|
||||
Reference in New Issue
Block a user