decal works
This commit is contained in:
@@ -1247,95 +1247,70 @@ terrain and colliders orphaned.
|
||||
|
||||
### Milestone 3 — Serialization + heightmap editing + splat painting 🚧 IN PROGRESS
|
||||
|
||||
**Status (2026-07-02)**: Core data pipeline verified working. Two
|
||||
blocking issues prevent visual terrain update. See "Known Issues" below.
|
||||
**Status (2026-07-03)**: ManualObject sculpt preview renders correctly and
|
||||
matches terrain output perfectly. Brush decal works (translucent green
|
||||
circle with white centre cross). Sculpt mode works in both editor and
|
||||
game modes. The remaining blocking issue is a Z-mapping mismatch between
|
||||
the raycast physical-hit position and the visual display position of
|
||||
heightmap data within terrain pages.
|
||||
|
||||
**Completed**:
|
||||
- [x] Scene serialization (save/load JSON round-trip for TerrainComponent)
|
||||
- [x] Binary heightmap file save/load (`heightmaps/<terrainId>/`)
|
||||
- [x] `HeightmapData` class (256×256 float array, `getHeightAt` / `setHeightAt`)
|
||||
- [x] `CustomTerrainDefiner` samples `HeightmapData` (falls back to procedural)
|
||||
- [x] `CustomTerrainDefiner` samples heightmap data (falls back to procedural)
|
||||
- [x] `TerrainCommand` / `TerrainCommandQueue` (Raise, Lower, Smooth, Flatten, SplatPaint)
|
||||
- [x] Sculpt mode checkbox + brush controls in TerrainEditor panel
|
||||
- [x] Brush tools: Raise, Lower, Smooth, Flatten, Splat Paint
|
||||
- [x] `applySculptBrush()` modifies heightmap data correctly
|
||||
- [x] `markPageDirty()` / `rebuildDirtyPages()` infrastructure
|
||||
- [x] Coordinate formula in `sampleHeightAtLocked()` — FIXED (2026-07-02)
|
||||
- [x] Sculpt works in both editor **and** game modes (Issue 2 — FIXED)
|
||||
- [x] Iterative ray-heightfield intersection for sculpt mode (Issue 3 — FIXED)
|
||||
- [x] ManualObject sculpt preview — GPU update via ManualObject rebuild (Issue 4 — FIXED)
|
||||
- [x] Brush decal: translucent green circle + white centre cross (Issue 5 — FIXED)
|
||||
- [x] Default brush radius 100, slider range 5–500 (Issue 6 — FIXED)
|
||||
- [x] `getHeightAt()` uses in-memory heightmap in sculpt mode
|
||||
- [x] ManualObject geometry matches terrain output perfectly
|
||||
|
||||
**Known issues (must fix before M3 ship)**:
|
||||
**Verified alignment**: The ManualObject sculpt preview uses the identical
|
||||
local-coordinate convention as the terrain (scene node at page minimum
|
||||
corner from `convertTerrainSlotToWorldPosition`, local Z = `halfSize – j*step`,
|
||||
zigzag triangulation matching collider pattern). Heightmap sampling
|
||||
(`corner.z + j*step`) matches `fillPageHeightData`. Unedited manual
|
||||
objects and terrain chunks are visually identical. After exiting sculpt mode
|
||||
the output geometry (reloaded terrain pages) matches the manual objects.
|
||||
|
||||
1. **Coordinate formula in `sampleHeightAtLocked()`** — FIXED.
|
||||
Old: `fx = worldX / worldSize * res` — maps world (0,0) to index 0.
|
||||
Fixed: `fx = (worldX - m_heightmapWorldMinX) / worldSize * res` — maps to index 85.
|
||||
This bug caused terrain pages to read from wrong heightmap indices.
|
||||
**Fix committed 2026-07-02.**
|
||||
**Z-mapping mismatch — FIXED (2026-07-03)**:
|
||||
|
||||
2. **Sculpt only works in game mode** — the sculpt code in
|
||||
`EditorUISystem::update()` is inside `if (!m_editorUIEnabled)`.
|
||||
In editor mode sculpting never activates. Move the sculpt block
|
||||
before this check so it runs in both editor and game modes.
|
||||
**Not yet fixed.**
|
||||
Ogre terrain (`ALIGN_X_Z`) renders vertex `(i,j)` at local Z =
|
||||
`halfSize – j*step` (from `getPointAlign` with `mBase = -halfSize`).
|
||||
The heightmap stores data at physical world Z = `pageMinZ + j*step`
|
||||
(via `fillPageHeightData`). The relationship is:
|
||||
|
||||
3. **Jolt raycast misses terrain when camera is above/below** —
|
||||
`raycastTerrain()` uses Jolt physics raycast which misses mesh
|
||||
surface when the camera is under or far above the terrain.
|
||||
A fallback (Y=0 plane intersection + `getHeightAt()`) is needed
|
||||
and was implemented during testing but lost in git resets.
|
||||
**Must re-add.**
|
||||
visualZ = 2*pageMinZ + halfSize – physicalZ (Z-inverted around page centre)
|
||||
|
||||
4. **`dirty()+update(true)` does not push to GPU** — THE BLOCKING
|
||||
ISSUE. The heightmap data pipeline works correctly (verified:
|
||||
CPU terrain heights track modified heightmap). But Ogre terrain's
|
||||
`dirty()` + `update(true)` updates CPU memory only; the GPU
|
||||
vertex buffers are NOT refreshed. This is specific to terrains
|
||||
created through the paging system (`loadAllTerrains(true)`).
|
||||
|
||||
**Approaches tried and failed**:
|
||||
- `dirty()` + `update(true)` — CPU updates, GPU does not
|
||||
- `dirty()` + `updateGeometry()` — same
|
||||
- `dirty()` + `updateDerivedData(true)` — same
|
||||
- `_getRootSceneNode()->needUpdate(true)` — same
|
||||
- `terrain->unload()` + `terrain->load()` — crashes (null quad tree)
|
||||
- `removeTerrain + defineTerrain + loadTerrain` with paging disabled —
|
||||
terrain IS recreated with correct heights (PostLoad verified),
|
||||
but visual still doesn't update. Paging system's definer
|
||||
(`CustomTerrainDefiner`) overwrites import data unless paging is
|
||||
disabled; even with paging off, GPU doesn't refresh.
|
||||
The raycast returns the *physical* Z. The **decal** must be at
|
||||
*visual* Z (so it appears where the user clicked), and the **brush**
|
||||
must use *physical* Z directly (because the heightmap is stored in
|
||||
physical coordinates). `physicalToVisualZ()` converts physical Z to
|
||||
visual Z for the decal position.
|
||||
|
||||
**Promising direction**: The legacy `updateHeightmap()` in
|
||||
`src/gamedata/EditorGUIModule.cpp` uses `mTerrainPagedWorldSection->
|
||||
unloadPage()` + `mTerrainGroup->update(false)` — this works through
|
||||
the paging system properly. For editor mode, the camera needs to
|
||||
be attached to `PageManager` so auto-reload works. It is very slow.
|
||||
Previously the conversion was applied to the brush (not the decal),
|
||||
which meant: the decal appeared at the wrong world Z position
|
||||
(confusing the user), and the brush modified heightmap cells that
|
||||
rendered at the page-opposite visual location (symmetric around page
|
||||
centre). Now fixed in `EditorUISystem::update()`.
|
||||
|
||||
**Alternative**: Render terrain heights via a custom
|
||||
`Ogre::ManualObject` mesh that mirrors the heightmap, bypassing
|
||||
Ogre terrain rendering entirely for real-time editing preview.
|
||||
It is more work but much better. Make sure the mesh is generated in the same way
|
||||
as Ogre terrain's mesh in zigzag pattern. Then only vertices Y coordinates can be updated.
|
||||
The mesh should show all loaded chunks pages and show height directly allowing to see the change quickly.
|
||||
The flow should be like:
|
||||
- Sclupt Mode checkbox enabled
|
||||
- Page loading should be disabled by removing camera from PageManager or other way.
|
||||
- Already loaded pages should be remembered and unloaded so no terrain chunks rendered.
|
||||
- For all remembered pages ManualObject should be generated which directly represents heightmap
|
||||
- It should be showing height replica of original terrain chunk, but with single color but shaded look
|
||||
to understand its geometry easier.
|
||||
- As geometry modified via brush, the manual object for appropriate chunk should be rebuilt with changed vertices.
|
||||
It can also easily be just plain vertex buffer update with only changing vertex part as index part is the same.
|
||||
This can be optimized for partial updates and read/write buffers, so that updates are fast.
|
||||
- As Sculpt mode checkbox gets updated, the height updates get flushed, then manual objects get deleted, pages loaded back and paging enabled as before sculpt mode.
|
||||
**Remaining M3 items**:
|
||||
- [ ] Splat paint (`applySplatBrush` is a stub — needs blend map access)
|
||||
- [ ] Test: sculpt → visual update shows change at decal location
|
||||
- [ ] Test: sculpt, save heightmap, reload → changes persist
|
||||
- [ ] Test: sculpt data survives save/load scene round-trip
|
||||
|
||||
|
||||
5. **Brush decal not working** — The translucent circle indicator
|
||||
(`Ogre::ManualObject` with alpha blending) renders as opaque white.
|
||||
RTSS material override prevents `setSceneBlending()` from taking
|
||||
effect. **Fix**: Use per-vertex colours with alpha + material
|
||||
with `scene_blend alpha_blend` explicitly set on the pass
|
||||
(bypassing RTSS by setting the scheme or using raw materials).
|
||||
|
||||
6. **Default brush radius too small** — Default 5.0 on a 2000-unit
|
||||
page covers ~1 vertex. Should default to 50-150 for visible
|
||||
effect. Slider range currently 0.5–50; should be 5–500.
|
||||
**Files modified for M3**:
|
||||
- `systems/TerrainSystem.hpp/cpp` — ManualObject preview, brush decal, raycast, Z-helper
|
||||
- `systems/EditorUISystem.cpp` — sculpt block placement, decal update, Z-inversion call (FIXED)
|
||||
- `ui/TerrainEditor.hpp` — slider range
|
||||
|
||||
### Milestone 4 — Procedural roads
|
||||
- Node/edge editing UI (add, remove, split, join, select, move).
|
||||
|
||||
@@ -327,29 +327,39 @@ void EditorUISystem::registerModularComponents()
|
||||
|
||||
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. */
|
||||
/* Sculpt mode: left mouse on terrain applies brush.
|
||||
* Also updates brush decal at mouse position.
|
||||
* Runs in both editor and game mode. */
|
||||
{
|
||||
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);
|
||||
if (!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);
|
||||
if (ts->raycastTerrain(ray, t,
|
||||
normal)) {
|
||||
Ogre::Vector3 hit =
|
||||
ray.getPoint(t);
|
||||
ts->updateBrushDecal(hit, normal,
|
||||
ts->getSculptRadius());
|
||||
if (ImGui::IsMouseDown(
|
||||
ImGuiMouseButton_Left)) {
|
||||
ts->applySculptBrush(hit);
|
||||
}
|
||||
} else {
|
||||
ts->hideBrushDecal();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_editorUIEnabled) {
|
||||
renderFPSOverlay(deltaTime);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <OgreMaterial.h>
|
||||
#include <OgreLogManager.h>
|
||||
|
||||
#include <OgreRTShaderSystem.h>
|
||||
#include <OgreShaderRenderState.h>
|
||||
#include <OgreShaderSubRenderState.h>
|
||||
|
||||
@@ -571,6 +572,19 @@ void TerrainSystem::rebuildDirtyPages()
|
||||
if (!mTerrainGroup || m_dirtyPages.empty())
|
||||
return;
|
||||
|
||||
/* When sculpt previews are active, only update ManualObjects.
|
||||
* Terrain pages are unloaded — don't touch colliders or try to
|
||||
* update terrain geometry. */
|
||||
if (m_sculptPreviewsActive) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: updating " +
|
||||
Ogre::StringConverter::toString(
|
||||
(int)m_dirtyPages.size()) +
|
||||
" dirty sculpt preview pages");
|
||||
updateSculptDirtyPages();
|
||||
return;
|
||||
}
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: rebuilding " +
|
||||
Ogre::StringConverter::toString((int)m_dirtyPages.size()) +
|
||||
@@ -734,6 +748,36 @@ void TerrainSystem::deactivate()
|
||||
m_terrainEntityId = 0;
|
||||
m_dirtyPages.clear();
|
||||
m_reloadQueue.clear();
|
||||
m_sculptMode = false;
|
||||
|
||||
/* Clean up sculpt previews without trying to reload terrain
|
||||
* pages (we're shutting down the whole terrain). */
|
||||
if (m_sculptPreviewsActive) {
|
||||
for (auto &kv : m_sculptPreviews) {
|
||||
auto &sp = kv.second;
|
||||
if (sp.manualObj) {
|
||||
if (sp.sceneNode)
|
||||
sp.sceneNode->detachObject(
|
||||
sp.manualObj);
|
||||
m_sceneMgr->destroyManualObject(sp.manualObj);
|
||||
}
|
||||
if (sp.sceneNode)
|
||||
m_sceneMgr->destroySceneNode(sp.sceneNode);
|
||||
}
|
||||
m_sculptPreviews.clear();
|
||||
m_sculptPreviewsActive = false;
|
||||
}
|
||||
|
||||
/* Destroy brush decal. */
|
||||
if (m_brushDecal) {
|
||||
m_brushDecalNode->detachObject(m_brushDecal);
|
||||
m_sceneMgr->destroyManualObject(m_brushDecal);
|
||||
m_brushDecal = nullptr;
|
||||
}
|
||||
if (m_brushDecalNode) {
|
||||
m_sceneMgr->destroySceneNode(m_brushDecalNode);
|
||||
m_brushDecalNode = nullptr;
|
||||
}
|
||||
|
||||
removeAllColliders();
|
||||
if (m_physics)
|
||||
@@ -834,43 +878,138 @@ void TerrainSystem::update(float /*deltaTime*/)
|
||||
|
||||
float TerrainSystem::getHeightAt(const Ogre::Vector3 &worldPos) const
|
||||
{
|
||||
if (!m_active || !mTerrainGroup)
|
||||
if (!m_active)
|
||||
return 0.0f;
|
||||
return mTerrainGroup->getHeightAtWorldPosition(worldPos);
|
||||
|
||||
/* In sculpt mode the Ogre terrain instances are unloaded;
|
||||
* sample directly from the in-memory heightmap. */
|
||||
if (m_sculptPreviewsActive && m_heightmapLoaded)
|
||||
return sampleHeightAt((long)worldPos.x, (long)worldPos.z);
|
||||
|
||||
if (mTerrainGroup)
|
||||
return mTerrainGroup->getHeightAtWorldPosition(worldPos);
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
bool TerrainSystem::raycastTerrain(const Ogre::Ray &ray, float &outT,
|
||||
Ogre::Vector3 &outNormal) const
|
||||
{
|
||||
if (!m_active || !m_physics || mColliders.empty())
|
||||
if (!m_active || !mTerrainGroup || !m_sceneMgr)
|
||||
return false;
|
||||
|
||||
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();
|
||||
/* --- Sculpt mode: terrain pages are unloaded (no Jolt
|
||||
* colliders). Iteratively intersect the camera ray with
|
||||
* the heightmap: start at Y=0, look up the terrain height
|
||||
* at that XZ, then solve for the ray's intersection with a
|
||||
* plane at that height, and repeat. Converges in 2–4
|
||||
* iterations even on steep slopes. --- */
|
||||
if (m_sculptPreviewsActive && m_heightmapLoaded) {
|
||||
const Ogre::Vector3 &origin = ray.getOrigin();
|
||||
const Ogre::Vector3 &dir = ray.getDirection();
|
||||
|
||||
const JPH::RRayCast rayCast(origin, dir);
|
||||
if (fabsf(dir.y) < 1e-6f)
|
||||
return false;
|
||||
|
||||
JPH::PhysicsSystem *jphSys = m_physics->getPhysicsSystem();
|
||||
if (!jphSys)
|
||||
return false;
|
||||
/* Seed: start just in front of the camera, not
|
||||
* from Y=0 (which fails when camera is below Y=0
|
||||
* looking down). The iterative refinement converges
|
||||
* in 2-4 steps from any reasonable seed. */
|
||||
float t = 0.01f;
|
||||
Ogre::Vector3 pt = origin + dir * t;
|
||||
|
||||
JPH::RayCastResult hit;
|
||||
if (!jphSys->GetNarrowPhaseQuery().CastRay(rayCast, hit,
|
||||
JPH::BroadPhaseLayerFilter(),
|
||||
JPH::ObjectLayerFilter(),
|
||||
JPH::BodyFilter()))
|
||||
return false;
|
||||
/* Safety: don't march past 100 km. */
|
||||
const float maxDist = 100000.0f;
|
||||
for (int iter = 0; iter < 16; ++iter) {
|
||||
float h = sampleHeightAt((long)pt.x,
|
||||
(long)pt.z);
|
||||
float tNew = (h - origin.y) / dir.y;
|
||||
if (tNew < 0.0f || tNew > maxDist)
|
||||
return false;
|
||||
Ogre::Vector3 ptNew = origin + dir * tNew;
|
||||
|
||||
for (auto &pair : mColliders) {
|
||||
if (pair.second.bodyId == hit.mBodyID) {
|
||||
outT = hit.mFraction;
|
||||
outNormal = Ogre::Vector3::UNIT_Y;
|
||||
return true;
|
||||
/* Converged? */
|
||||
if (fabsf(ptNew.x - pt.x) < 0.2f &&
|
||||
fabsf(ptNew.z - pt.z) < 0.2f) {
|
||||
outT = tNew;
|
||||
/* Normal from heightmap gradient. */
|
||||
float eps = 1.0f;
|
||||
float dx = sampleHeightAt(
|
||||
(long)(ptNew.x + eps),
|
||||
(long)ptNew.z) -
|
||||
h;
|
||||
float dz = sampleHeightAt(
|
||||
(long)ptNew.x,
|
||||
(long)(ptNew.z + eps)) -
|
||||
h;
|
||||
outNormal = Ogre::Vector3(-dx,
|
||||
eps * 2.0f,
|
||||
-dz);
|
||||
outNormal.normalise();
|
||||
return true;
|
||||
}
|
||||
pt = ptNew;
|
||||
t = tNew;
|
||||
}
|
||||
/* No convergence — use last result. */
|
||||
outT = t;
|
||||
outNormal = Ogre::Vector3::UNIT_Y;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* --- Primary: Jolt physics raycast against colliders --- */
|
||||
if (m_physics && !mColliders.empty()) {
|
||||
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();
|
||||
|
||||
const JPH::RRayCast rayCast(origin, dir);
|
||||
|
||||
JPH::PhysicsSystem *jphSys = m_physics->getPhysicsSystem();
|
||||
if (jphSys) {
|
||||
JPH::RayCastResult hit;
|
||||
if (jphSys->GetNarrowPhaseQuery().CastRay(
|
||||
rayCast, hit,
|
||||
JPH::BroadPhaseLayerFilter(),
|
||||
JPH::ObjectLayerFilter(),
|
||||
JPH::BodyFilter())) {
|
||||
for (auto &pair : mColliders) {
|
||||
if (pair.second.bodyId ==
|
||||
hit.mBodyID) {
|
||||
outT = hit.mFraction;
|
||||
outNormal = Ogre::Vector3::UNIT_Y;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Fallback: Y=0 plane intersection for when Jolt
|
||||
* colliders miss (camera far above/below or shallow
|
||||
* angle). Not used in sculpt mode which has its own
|
||||
* heightmap-based path above. --- */
|
||||
if (m_sculptPreviewsActive)
|
||||
return false;
|
||||
|
||||
{
|
||||
const Ogre::Vector3 &origin = ray.getOrigin();
|
||||
const Ogre::Vector3 &dir = ray.getDirection();
|
||||
if (fabsf(dir.y) < 1e-6f)
|
||||
return false;
|
||||
|
||||
const float planeY = 0.0f;
|
||||
float t = (planeY - origin.y) / dir.y;
|
||||
if (t < 0.0f)
|
||||
return false;
|
||||
|
||||
outT = t;
|
||||
outNormal = Ogre::Vector3::UNIT_Y;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -911,6 +1050,14 @@ void TerrainSystem::applySculptBrush(const Ogre::Vector3 &worldPos)
|
||||
if (!m_heightmapLoaded || !m_active || !mTerrainGroup)
|
||||
return;
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: applySculptBrush at (" +
|
||||
Ogre::StringConverter::toString(worldPos.x) + ", " +
|
||||
Ogre::StringConverter::toString(worldPos.y) + ", " +
|
||||
Ogre::StringConverter::toString(worldPos.z) +
|
||||
") radius=" +
|
||||
Ogre::StringConverter::toString(m_sculptRadius));
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_heightmapMutex);
|
||||
|
||||
float spacing = m_heightmapWorldSize / (float)(m_heightmapRes - 1);
|
||||
@@ -991,15 +1138,501 @@ void TerrainSystem::applySculptBrush(const Ogre::Vector3 &worldPos)
|
||||
/* applySplatBrush */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::applySplatBrush(const Ogre::Vector3 & /*worldPos*/)
|
||||
void TerrainSystem::applySplatBrush(const Ogre::Vector3 &worldPos)
|
||||
{
|
||||
/* Not implemented yet. */
|
||||
if (!mTerrainGroup || !m_active)
|
||||
return;
|
||||
|
||||
/* Blend maps live on the terrain instance — must be loaded.
|
||||
* In sculpt mode terrain is unloaded; skip. */
|
||||
if (m_sculptPreviewsActive)
|
||||
return;
|
||||
|
||||
int layerIdx = m_sculptLayerIndex;
|
||||
if (layerIdx < 1)
|
||||
return; /* layer 0 is base, no blend map */
|
||||
|
||||
/* Find the page at this world position. */
|
||||
long px, py;
|
||||
mTerrainGroup->convertWorldPositionToTerrainSlot(
|
||||
worldPos, &px, &py);
|
||||
|
||||
Ogre::Terrain *terrain = mTerrainGroup->getTerrain(px, py);
|
||||
if (!terrain || !terrain->isLoaded())
|
||||
return;
|
||||
|
||||
if (layerIdx >= (int)terrain->getLayerCount())
|
||||
return;
|
||||
|
||||
/* Blend map index is layerIdx-1 (layer 0 has no blend map). */
|
||||
Ogre::TerrainLayerBlendMap *blendMap =
|
||||
terrain->getLayerBlendMap((Ogre::uint8)(layerIdx - 1));
|
||||
if (!blendMap)
|
||||
return;
|
||||
|
||||
int bmSize = (int)terrain->getLayerBlendMapSize();
|
||||
Ogre::Real ws = terrain->getWorldSize();
|
||||
Ogre::Real halfSize = ws * 0.5f;
|
||||
|
||||
/* Page corner in world space. */
|
||||
Ogre::Vector3 pageCorner;
|
||||
mTerrainGroup->convertTerrainSlotToWorldPosition(px, py, &pageCorner);
|
||||
|
||||
/* World position → blend-map texel.
|
||||
* For ALIGN_X_Z: blend (0,0) = world (corner.x - halfSize, corner.z + halfSize). */
|
||||
int cx = (int)(((worldPos.x - pageCorner.x + halfSize) / ws) *
|
||||
(float)bmSize);
|
||||
int cy = (int)(((pageCorner.z + halfSize - worldPos.z) / ws) *
|
||||
(float)bmSize);
|
||||
|
||||
float spacing = ws / (float)bmSize;
|
||||
int rSamples = (int)(m_sculptRadius / spacing) + 1;
|
||||
|
||||
int x0 = std::max(0, cx - rSamples);
|
||||
int x1 = std::min(bmSize - 1, cx + rSamples);
|
||||
int y0 = std::max(0, cy - rSamples);
|
||||
int y1 = std::min(bmSize - 1, cy + rSamples);
|
||||
|
||||
for (int y = y0; y <= y1; ++y) {
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
float dx = ((float)x - (float)cx) * spacing;
|
||||
float dy = ((float)y - (float)cy) * spacing;
|
||||
float d2 = dx * dx + dy * dy;
|
||||
if (d2 <= m_sculptRadius * m_sculptRadius) {
|
||||
float falloff =
|
||||
1.0f - sqrtf(d2) / m_sculptRadius;
|
||||
float cur = blendMap->getBlendValue(
|
||||
(Ogre::uint32)x, (Ogre::uint32)y);
|
||||
float val = std::min(
|
||||
1.0f, cur + m_sculptStrength * falloff);
|
||||
blendMap->setBlendValue((Ogre::uint32)x, (Ogre::uint32)y,
|
||||
val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blendMap->dirtyRect(Ogre::Rect((long)x0, (long)y0,
|
||||
(long)x1, (long)y1));
|
||||
blendMap->update();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Sculpt preview material (M3) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::ensureSculptPreviewMaterial()
|
||||
{
|
||||
if (m_sculptPreviewMaterial)
|
||||
return;
|
||||
|
||||
Ogre::MaterialManager &matMgr = Ogre::MaterialManager::getSingleton();
|
||||
|
||||
m_sculptPreviewMaterial = matMgr.create(
|
||||
"TerrainSculptPreview",
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
|
||||
|
||||
Ogre::Pass *pass = m_sculptPreviewMaterial->getTechnique(0)->createPass();
|
||||
pass->setDiffuse(0.55f, 0.50f, 0.40f, 1.0f); /* earthy brown */
|
||||
pass->setAmbient(0.35f, 0.30f, 0.25f);
|
||||
pass->setSpecular(0.1f, 0.1f, 0.1f, 1.0f);
|
||||
pass->setShininess(4.0f);
|
||||
pass->setCullingMode(Ogre::CULL_CLOCKWISE);
|
||||
|
||||
/* Let RTSS inject per-pixel lighting shaders. */
|
||||
Ogre::RTShader::ShaderGenerator *shaderGen =
|
||||
Ogre::RTShader::ShaderGenerator::getSingletonPtr();
|
||||
if (shaderGen) {
|
||||
shaderGen->createShaderBasedTechnique(
|
||||
*m_sculptPreviewMaterial,
|
||||
Ogre::MaterialManager::DEFAULT_SCHEME_NAME,
|
||||
Ogre::RTShader::ShaderGenerator::DEFAULT_SCHEME_NAME);
|
||||
shaderGen->validateMaterial(
|
||||
Ogre::RTShader::ShaderGenerator::DEFAULT_SCHEME_NAME,
|
||||
"TerrainSculptPreview");
|
||||
}
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: created sculpt preview material");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* beginSculptPreviews */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::beginSculptPreviews()
|
||||
{
|
||||
if (!m_active || !mTerrainGroup || !m_sceneMgr)
|
||||
return;
|
||||
|
||||
if (m_sculptPreviewsActive)
|
||||
endSculptPreviews();
|
||||
|
||||
ensureSculptPreviewMaterial();
|
||||
|
||||
/* 1. Disable paging so no new pages load/unload while sculpting. */
|
||||
if (mPageManager)
|
||||
mPageManager->setPagingOperationsEnabled(false);
|
||||
|
||||
/* 2. Remember all loaded pages and unload them. */
|
||||
std::vector<std::pair<long, long>> loadedPages;
|
||||
for (const auto &kv : mTerrainGroup->getTerrainSlots()) {
|
||||
Ogre::TerrainGroup::TerrainSlot *slot = kv.second;
|
||||
if (slot && slot->instance && slot->instance->isLoaded())
|
||||
loadedPages.push_back({ slot->x, slot->y });
|
||||
}
|
||||
|
||||
/* Unload terrain instances so they stop rendering. */
|
||||
for (auto [x, y] : loadedPages)
|
||||
mTerrainGroup->removeTerrain(x, y);
|
||||
|
||||
/* 3. Create ManualObject + SceneNode per page. */
|
||||
for (auto [x, y] : loadedPages) {
|
||||
uint64_t key = mTerrainGroup->packIndex(x, y);
|
||||
|
||||
/* SceneNode at page minimum corner (match terrain
|
||||
* root node position). */
|
||||
Ogre::Vector3 worldPos;
|
||||
mTerrainGroup->convertTerrainSlotToWorldPosition(
|
||||
x, y, &worldPos);
|
||||
|
||||
Ogre::SceneNode *node = m_sceneMgr->getRootSceneNode()
|
||||
->createChildSceneNode(worldPos);
|
||||
|
||||
Ogre::ManualObject *mo = m_sceneMgr->createManualObject();
|
||||
mo->setDynamic(true);
|
||||
node->attachObject(mo);
|
||||
|
||||
m_sculptPreviews[key] = { mo, node, x, y };
|
||||
buildSculptPreview(x, y);
|
||||
mo->setVisible(true);
|
||||
}
|
||||
|
||||
m_sculptPreviewsActive = true;
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: sculpt previews active (" +
|
||||
Ogre::StringConverter::toString((int)loadedPages.size()) +
|
||||
" pages)");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* buildSculptPreview */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::buildSculptPreview(long pageX, long pageY)
|
||||
{
|
||||
uint64_t key = mTerrainGroup->packIndex(pageX, pageY);
|
||||
auto it = m_sculptPreviews.find(key);
|
||||
if (it == m_sculptPreviews.end())
|
||||
return;
|
||||
|
||||
Ogre::ManualObject *mo = it->second.manualObj;
|
||||
if (!mo)
|
||||
return;
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: building sculpt preview page (" +
|
||||
Ogre::StringConverter::toString(pageX) + ", " +
|
||||
Ogre::StringConverter::toString(pageY) + ")");
|
||||
|
||||
const Ogre::uint16 size = mTerrainGroup->getTerrainSize();
|
||||
const Ogre::Real worldSize = mTerrainGroup->getTerrainWorldSize();
|
||||
const Ogre::Real step = worldSize / (Ogre::Real)(size - 1);
|
||||
|
||||
/* Sample heights. */
|
||||
std::vector<float> heights(size * size);
|
||||
{
|
||||
Ogre::Vector3 worldPos;
|
||||
mTerrainGroup->convertTerrainSlotToWorldPosition(
|
||||
pageX, pageY, &worldPos);
|
||||
for (int j = 0; j < size; ++j) {
|
||||
for (int i = 0; i < size; ++i) {
|
||||
long wx = (long)(worldPos.x +
|
||||
(Ogre::Real)i * step);
|
||||
long wz = (long)(worldPos.z +
|
||||
(Ogre::Real)j * step);
|
||||
heights[j * size + i] =
|
||||
sampleHeightAt(wx, wz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Compute per-vertex normals via central differences. */
|
||||
std::vector<Ogre::Vector3> normals(size * size);
|
||||
for (int j = 0; j < size; ++j) {
|
||||
for (int i = 0; i < size; ++i) {
|
||||
float hl = (i > 0) ?
|
||||
heights[j * size + (i - 1)] :
|
||||
heights[j * size + i];
|
||||
float hr = (i < size - 1) ?
|
||||
heights[j * size + (i + 1)] :
|
||||
heights[j * size + i];
|
||||
float hu = (j > 0) ?
|
||||
heights[(j - 1) * size + i] :
|
||||
heights[j * size + i];
|
||||
float hd = (j < size - 1) ?
|
||||
heights[(j + 1) * size + i] :
|
||||
heights[j * size + i];
|
||||
Ogre::Vector3 n(-(hr - hl), 2.0f * step, -(hd - hu));
|
||||
n.normalise();
|
||||
normals[j * size + i] = n;
|
||||
}
|
||||
}
|
||||
|
||||
/* Build ManualObject with zigzag triangulation matching Ogre
|
||||
* terrain and Jolt collider layout. */
|
||||
mo->clear();
|
||||
mo->begin("TerrainSculptPreview",
|
||||
Ogre::RenderOperation::OT_TRIANGLE_LIST);
|
||||
|
||||
float halfSize = worldSize * 0.5f;
|
||||
for (int j = 0; j < size; ++j) {
|
||||
for (int i = 0; i < size; ++i) {
|
||||
float vx = (float)i * step - halfSize;
|
||||
float vz = halfSize - (float)j * step;
|
||||
float vy = heights[j * size + i];
|
||||
mo->position(vx, vy, vz);
|
||||
mo->normal(normals[j * size + i].x,
|
||||
normals[j * size + i].y,
|
||||
normals[j * size + i].z);
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 0; j < size - 1; ++j) {
|
||||
for (int i = 0; i < size - 1; ++i) {
|
||||
int idx00 = j * size + i;
|
||||
int idx10 = j * size + i + 1;
|
||||
int idx01 = (j + 1) * size + i;
|
||||
int idx11 = (j + 1) * size + i + 1;
|
||||
if ((j & 1) == 0) {
|
||||
mo->triangle(idx00, idx11, idx01);
|
||||
mo->triangle(idx00, idx10, idx11);
|
||||
} else {
|
||||
mo->triangle(idx00, idx10, idx01);
|
||||
mo->triangle(idx10, idx11, idx01);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mo->end();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* updateSculptDirtyPages */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::updateSculptDirtyPages()
|
||||
{
|
||||
if (!m_sculptPreviewsActive || m_dirtyPages.empty())
|
||||
return;
|
||||
|
||||
for (uint64_t key : m_dirtyPages) {
|
||||
long x, y;
|
||||
mTerrainGroup->unpackIndex(key, &x, &y);
|
||||
buildSculptPreview(x, y);
|
||||
}
|
||||
m_dirtyPages.clear();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* endSculptPreviews */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::endSculptPreviews()
|
||||
{
|
||||
if (!m_sculptPreviewsActive || !mTerrainGroup)
|
||||
return;
|
||||
|
||||
hideBrushDecal();
|
||||
|
||||
/* 1. Destroy all ManualObjects and SceneNodes. */
|
||||
for (auto &kv : m_sculptPreviews) {
|
||||
auto &sp = kv.second;
|
||||
if (sp.manualObj) {
|
||||
if (sp.sceneNode)
|
||||
sp.sceneNode->detachObject(sp.manualObj);
|
||||
m_sceneMgr->destroyManualObject(sp.manualObj);
|
||||
}
|
||||
if (sp.sceneNode)
|
||||
m_sceneMgr->destroySceneNode(sp.sceneNode);
|
||||
}
|
||||
m_sculptPreviews.clear();
|
||||
|
||||
/* 2. Reload terrain pages with updated heightmap data. */
|
||||
const Ogre::uint16 terrainSize = mTerrainGroup->getTerrainSize();
|
||||
for (long py = m_pageMinY; py <= m_pageMaxY; ++py) {
|
||||
for (long px = m_pageMinX; px <= m_pageMaxX; ++px) {
|
||||
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);
|
||||
|
||||
/* 3. Re-enable paging. */
|
||||
if (mPageManager)
|
||||
mPageManager->setPagingOperationsEnabled(true);
|
||||
|
||||
m_sculptPreviewsActive = false;
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Terrain: sculpt previews destroyed, paging restored");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Brush decal (M3) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void TerrainSystem::updateBrushDecal(const Ogre::Vector3 &worldPos,
|
||||
const Ogre::Vector3 &normal,
|
||||
float radius)
|
||||
{
|
||||
if (!m_sceneMgr)
|
||||
return;
|
||||
|
||||
/* Lazy-create the decal. */
|
||||
if (!m_brushDecal) {
|
||||
m_brushDecalNode = m_sceneMgr->getRootSceneNode()
|
||||
->createChildSceneNode();
|
||||
|
||||
m_brushDecal = m_sceneMgr->createManualObject();
|
||||
m_brushDecal->setDynamic(true);
|
||||
m_brushDecalNode->attachObject(m_brushDecal);
|
||||
|
||||
/* Material: unlit, alpha-blended, vertex-coloured.
|
||||
* RTSS reads the pass settings (lighting off,
|
||||
* scene_blend, TVC_DIFFUSE) and generates a shader
|
||||
* that passes vertex colours straight through with
|
||||
* proper alpha blending. */
|
||||
Ogre::MaterialManager &matMgr =
|
||||
Ogre::MaterialManager::getSingleton();
|
||||
if (!matMgr.resourceExists("TerrainBrushDecal")) {
|
||||
auto mat = matMgr.create(
|
||||
"TerrainBrushDecal",
|
||||
Ogre::ResourceGroupManager::
|
||||
DEFAULT_RESOURCE_GROUP_NAME);
|
||||
Ogre::Pass *pass =
|
||||
mat->getTechnique(0)->createPass();
|
||||
pass->setLightingEnabled(false);
|
||||
pass->setSceneBlending(
|
||||
Ogre::SBT_TRANSPARENT_ALPHA);
|
||||
pass->setDepthWriteEnabled(false);
|
||||
pass->setDepthCheckEnabled(true);
|
||||
pass->setCullingMode(Ogre::CULL_NONE);
|
||||
/* Vertex colours provide the actual colour. */
|
||||
pass->setVertexColourTracking(
|
||||
Ogre::TVC_DIFFUSE | Ogre::TVC_AMBIENT);
|
||||
pass->setDiffuse(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
pass->setAmbient(1.0f, 1.0f, 1.0f);
|
||||
|
||||
Ogre::RTShader::ShaderGenerator *shaderGen =
|
||||
Ogre::RTShader::ShaderGenerator::
|
||||
getSingletonPtr();
|
||||
if (shaderGen) {
|
||||
shaderGen->createShaderBasedTechnique(
|
||||
*mat,
|
||||
Ogre::MaterialManager::
|
||||
DEFAULT_SCHEME_NAME,
|
||||
Ogre::RTShader::ShaderGenerator::
|
||||
DEFAULT_SCHEME_NAME);
|
||||
shaderGen->validateMaterial(
|
||||
Ogre::RTShader::ShaderGenerator::
|
||||
DEFAULT_SCHEME_NAME,
|
||||
"TerrainBrushDecal");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Section 1: semi-transparent circle (triangle fan). */
|
||||
const int segments = 32;
|
||||
m_brushDecal->begin("TerrainBrushDecal",
|
||||
Ogre::RenderOperation::OT_TRIANGLE_FAN);
|
||||
/* Center — low alpha, marks the exact brush origin. */
|
||||
m_brushDecal->position(0, 0, 0);
|
||||
m_brushDecal->colour(0.0f, 0.8f, 0.0f, 0.10f);
|
||||
for (int i = 0; i <= segments; ++i) {
|
||||
float angle = 2.0f * M_PI * (float)i /
|
||||
(float)segments;
|
||||
m_brushDecal->position(cosf(angle), 0,
|
||||
sinf(angle));
|
||||
/* Edge — higher alpha, shows brush boundary. */
|
||||
m_brushDecal->colour(0.0f, 0.8f, 0.0f, 0.28f);
|
||||
}
|
||||
m_brushDecal->end();
|
||||
|
||||
/* Section 2: centre cross (line list) so the
|
||||
* operator can always see the exact hit point. */
|
||||
m_brushDecal->begin("TerrainBrushDecal",
|
||||
Ogre::RenderOperation::OT_LINE_LIST);
|
||||
float cs = 0.12f; /* cross arm half-length */
|
||||
m_brushDecal->position(-cs, 0, 0);
|
||||
m_brushDecal->colour(1.0f, 1.0f, 1.0f, 0.85f);
|
||||
m_brushDecal->position(cs, 0, 0);
|
||||
m_brushDecal->colour(1.0f, 1.0f, 1.0f, 0.85f);
|
||||
m_brushDecal->position(0, 0, -cs);
|
||||
m_brushDecal->colour(1.0f, 1.0f, 1.0f, 0.85f);
|
||||
m_brushDecal->position(0, 0, cs);
|
||||
m_brushDecal->colour(1.0f, 1.0f, 1.0f, 0.85f);
|
||||
m_brushDecal->end();
|
||||
}
|
||||
|
||||
/* Position on terrain surface with slight Y offset to
|
||||
* avoid z-fighting. */
|
||||
m_brushDecalNode->setVisible(true);
|
||||
m_brushDecalNode->setPosition(worldPos.x,
|
||||
worldPos.y + 0.5f,
|
||||
worldPos.z);
|
||||
|
||||
/* Orient the decal to face the terrain normal. */
|
||||
m_brushDecalNode->setOrientation(
|
||||
Ogre::Vector3::UNIT_Y.getRotationTo(normal));
|
||||
|
||||
/* Scale to match brush radius (the template is unit radius). */
|
||||
m_brushDecalNode->setScale(radius, 1.0f, radius);
|
||||
}
|
||||
|
||||
void TerrainSystem::hideBrushDecal()
|
||||
{
|
||||
if (m_brushDecalNode)
|
||||
m_brushDecalNode->setVisible(false);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* destroyCollider */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* physicalToVisualX */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
float TerrainSystem::physicalToVisualX(float physicalX) const
|
||||
{
|
||||
if (!mTerrainGroup)
|
||||
return physicalX;
|
||||
/* For ALIGN_X_Z: Ogre terrain local X = i*step - halfSize.
|
||||
* The heightmap physical X = pageMinX + i*step.
|
||||
* Visual world X = pageMinX + i*step - halfSize = physicalX - halfSize.
|
||||
* Same shift for all pages. */
|
||||
return physicalX - mTerrainGroup->getTerrainWorldSize() * 0.5f;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* physicalToVisualZ */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
float TerrainSystem::physicalToVisualZ(float physicalZ) const
|
||||
{
|
||||
if (!mTerrainGroup)
|
||||
return physicalZ;
|
||||
Ogre::Real ws = mTerrainGroup->getTerrainWorldSize();
|
||||
float halfSize = ws * 0.5f;
|
||||
float pageZ = floorf(physicalZ / ws) * ws;
|
||||
return 2.0f * pageZ + halfSize - physicalZ;
|
||||
}
|
||||
|
||||
void TerrainSystem::destroyCollider(uint64_t /*key*/,
|
||||
TerrainCollider & /*collider*/)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include <OgrePageManager.h>
|
||||
#include <OgrePage.h>
|
||||
#include <OgrePagedWorld.h>
|
||||
#include <OgreManualObject.h>
|
||||
#include <OgreMaterial.h>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <set>
|
||||
@@ -67,7 +69,16 @@ public:
|
||||
/* --- Sculpting (M3) --- */
|
||||
enum class SculptTool { Raise, Lower, Smooth, Flatten, SplatPaint };
|
||||
bool getSculptMode() const { return m_sculptMode; }
|
||||
void setSculptMode(bool v) { m_sculptMode = v; }
|
||||
void setSculptMode(bool v)
|
||||
{
|
||||
if (v == m_sculptMode)
|
||||
return;
|
||||
m_sculptMode = v;
|
||||
if (v)
|
||||
beginSculptPreviews();
|
||||
else
|
||||
endSculptPreviews();
|
||||
}
|
||||
bool isSculpting() const { return m_sculptMode; }
|
||||
SculptTool getSculptTool() const { return m_sculptTool; }
|
||||
void setSculptTool(SculptTool t) { m_sculptTool = t; }
|
||||
@@ -81,6 +92,22 @@ public:
|
||||
void applySculptBrush(const Ogre::Vector3 &worldPos);
|
||||
void applySplatBrush(const Ogre::Vector3 &worldPos);
|
||||
|
||||
/* --- Brush decal (M3) --- */
|
||||
void updateBrushDecal(const Ogre::Vector3 &worldPos,
|
||||
const Ogre::Vector3 &normal, float radius);
|
||||
void hideBrushDecal();
|
||||
|
||||
/* Convert physical heightmap X to visual display X.
|
||||
* Terrain pages render column i at X = i*step - halfSize
|
||||
* but the height data lives at X = pageMinX + i*step.
|
||||
* Same for Z but Z-inverted (see physicalToVisualZ). */
|
||||
float physicalToVisualX(float physicalX) const;
|
||||
|
||||
/* Convert physical heightmap Z to visual display Z.
|
||||
* Terrain pages render row j at Z = pageMinZ + halfSize - j*step
|
||||
* but the height data lives at Z = pageMinZ + j*step. */
|
||||
float physicalToVisualZ(float physicalZ) const;
|
||||
|
||||
private:
|
||||
void activate(struct TerrainComponent &tc,
|
||||
struct TransformComponent &xform);
|
||||
@@ -203,10 +230,30 @@ private:
|
||||
/* Sculpting state */
|
||||
bool m_sculptMode = false;
|
||||
SculptTool m_sculptTool = SculptTool::Raise;
|
||||
float m_sculptRadius = 5.0f;
|
||||
float m_sculptRadius = 100.0f;
|
||||
float m_sculptStrength = 0.5f;
|
||||
int m_sculptLayerIndex = 0;
|
||||
|
||||
/* --- Sculpt preview (M3) --- */
|
||||
struct SculptPreview {
|
||||
Ogre::ManualObject *manualObj = nullptr;
|
||||
Ogre::SceneNode *sceneNode = nullptr;
|
||||
long pageX, pageY;
|
||||
};
|
||||
std::unordered_map<uint64_t, SculptPreview> m_sculptPreviews;
|
||||
Ogre::MaterialPtr m_sculptPreviewMaterial;
|
||||
bool m_sculptPreviewsActive = false;
|
||||
|
||||
void beginSculptPreviews();
|
||||
void endSculptPreviews();
|
||||
void buildSculptPreview(long pageX, long pageY);
|
||||
void updateSculptDirtyPages();
|
||||
void ensureSculptPreviewMaterial();
|
||||
|
||||
/* Brush decal (M3) */
|
||||
Ogre::ManualObject *m_brushDecal = nullptr;
|
||||
Ogre::SceneNode *m_brushDecalNode = nullptr;
|
||||
|
||||
/* Entity tracking */
|
||||
flecs::entity_t m_terrainEntityId = 0;
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ public:
|
||||
ts->setSculptTool((TerrainSystem::SculptTool)tool);
|
||||
|
||||
float radius = ts->getSculptRadius();
|
||||
if (ImGui::SliderFloat("Radius", &radius, 0.5f, 50.0f))
|
||||
if (ImGui::SliderFloat("Radius", &radius, 5.0f, 500.0f))
|
||||
ts->setSculptRadius(radius);
|
||||
|
||||
float strength = ts->getSculptStrength();
|
||||
|
||||
Reference in New Issue
Block a user