Now terrains serialization works

This commit is contained in:
2026-06-27 04:53:33 +03:00
parent 2c095de9f2
commit 4cd2c72fb0
7 changed files with 346 additions and 10 deletions
@@ -1308,11 +1308,6 @@ radius.
instead of the base heightmap.
- "Clear All Fixups" button — deletes all fixup chunks and marks affected
pages dirty for rebuild.
**Camera pivot above terrain** (on-demand, not automatic):
- "Snap Camera Above Terrain" button or hotkey in TerrainEditor.
- Raycast downward from camera at current XZ, get terrain height, place camera
at `(cam.x, terrainHeight + 5.0, cam.z)`.
- Only on user request — never auto-corrects.
**Definition of done**: user can sculpt terrain with visual brush feedback;
save scene, reload, terrain restored with edited heightmap and layer paint;
@@ -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) {
@@ -33,6 +33,7 @@
#include "../components/BuoyancyInfo.hpp"
#include "../components/WaterPhysics.hpp"
#include "../components/WaterPlane.hpp"
#include "../components/Terrain.hpp"
#include "../components/Sun.hpp"
#include "../components/Skybox.hpp"
#include "../components/EventHandler.hpp"
@@ -319,6 +320,9 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
if (entity.has<WaterPlane>()) {
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>()) {
@@ -552,6 +556,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 +863,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 +4135,65 @@ 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;
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);
}
}
entity.set<TerrainComponent>(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);
@@ -18,6 +18,8 @@
#include <Jolt/Physics/PhysicsSystem.h>
#include <cmath>
#include <fstream>
#include <sys/stat.h>
TerrainSystem *TerrainSystem::s_instance = nullptr;
@@ -98,7 +100,7 @@ void TerrainSystem::CustomTerrainDefiner::define(Ogre::TerrainGroup *group,
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->sampleHeightAt(wx, wz);
}
}
@@ -358,6 +360,25 @@ void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform)
m_physics->setBodyDrawFilter(&mBodyDrawFilter);
processColliderCreates();
/* If no heightmap loaded, generate m_heightData from procedural
* so that Save Heightmap has something to write. */
if (!m_heightmapLoaded) {
m_heightmapRes = tc.heightmapSize;
m_heightData.resize(m_heightmapRes * m_heightmapRes);
float ws = mTerrainGroup->getTerrainWorldSize();
for (int z = 0; z < m_heightmapRes; ++z) {
for (int x = 0; x < m_heightmapRes; ++x) {
float fx = (float)x *
ws / (float)(m_heightmapRes - 1);
float fz = (float)z *
ws / (float)(m_heightmapRes - 1);
m_heightData[z * m_heightmapRes + x] =
proceduralHeight((long)fx, (long)fz);
}
}
m_heightmapLoaded = true;
}
m_active = true;
Ogre::LogManager::getSingleton().logMessage(
"TerrainSystem: activation complete");
@@ -483,4 +504,156 @@ bool TerrainSystem::raycastTerrain(const Ogre::Ray &ray, float &outT,
void TerrainSystem::setShowTerrainColliders(bool show)
{
m_showTerrainColliders = show;
}
/* ------------------------------------------------------------------ */
/* Heightmap data (M3) */
/* ------------------------------------------------------------------ */
float TerrainSystem::sampleHeightAt(long worldX, long worldZ) const
{
if (!m_heightmapLoaded || m_heightData.empty())
return proceduralHeight(worldX, worldZ);
/* Bilinear sample of base heightmap (resolution m_heightmapRes x m_heightmapRes). */
float ws = mTerrainGroup ? mTerrainGroup->getTerrainWorldSize() : 2000.0f;
float fx = (float)worldX / ws * (float)m_heightmapRes;
float fz = (float)worldZ / ws * (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)
{
if (!m_heightmapLoaded) return;
float ws = mTerrainGroup ? mTerrainGroup->getTerrainWorldSize() : 2000.0f;
int x = (int)((float)worldX / ws * (float)m_heightmapRes);
int z = (int)((float)worldZ / ws * (float)m_heightmapRes);
if (x < 0 || x >= m_heightmapRes || z < 0 || z >= m_heightmapRes) return;
m_heightData[z * m_heightmapRes + x] = value;
}
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;
/* Ensure directory exists. */
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;
}
void TerrainSystem::markPageDirty(long pageX, long pageY)
{
if (mTerrainGroup)
m_dirtyPages.insert(mTerrainGroup->packIndex(pageX, pageY));
}
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);
mTerrainDefiner->define(mTerrainGroup, x, y);
queueColliderCreate(x, y);
}
m_dirtyPages.clear();
}
/* ------------------------------------------------------------------ */
/* snapCameraAboveTerrain (M3) */
/* ------------------------------------------------------------------ */
void TerrainSystem::snapCameraAboveTerrain()
{
if (!m_active || !m_sceneMgr)
return;
/* Move the camera target ("pivot"), not the camera node.
* EditorCamera creates m_targetNode as "EditorCameraTarget"
* the CameraMan orbits the camera around this node. */
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);
}
@@ -49,6 +49,34 @@ public:
return m_showTerrainColliders;
}
/* --- Heightmap data (M3) --- */
float sampleHeightAt(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);
void markPageDirty(long pageX, long pageY);
void rebuildDirtyPages();
/* --- Sculpting (M3) --- */
enum class SculptTool { Raise, Lower, Smooth, Flatten, SplatPaint };
bool m_sculptMode = false;
SculptTool m_sculptTool = SculptTool::Raise;
float m_sculptRadius = 5.0f;
float m_sculptStrength = 0.5f;
int m_sculptLayerIndex = 0;
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();
private:
void activate(struct TerrainComponent &tc,
struct TransformComponent &xform);
@@ -136,6 +164,13 @@ private:
std::deque<std::pair<long, long>> mColliderCreateQueue;
TerrainBodyDrawFilter mBodyDrawFilter;
/* Heightmap data (M3) */
std::vector<float> m_heightData;
bool m_heightmapLoaded = false;
int m_heightmapRes = 0;
std::set<uint64_t> m_dirtyPages;
bool hasHeightmap() const { return m_heightmapLoaded; }
flecs::query<struct TerrainComponent, struct TransformComponent>
m_terrainQuery;
};
+58 -2
View File
@@ -39,8 +39,64 @@ 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();
if (ImGui::Button("Save Heightmap"))
ts->saveHeightmap("heightmaps/ht_" +
std::to_string(tc.terrainId) + ".bin");
ImGui::SameLine();
if (ImGui::Button("Load Heightmap"))
ts->loadHeightmap("heightmaps/ht_" +
std::to_string(tc.terrainId) + ".bin");
}
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);