Now terrains serialization works
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user