Water plane

This commit is contained in:
2026-04-23 01:29:53 +03:00
parent 4335a8cb05
commit 9d4fad1d10
16 changed files with 749 additions and 309 deletions

View File

@@ -18,6 +18,7 @@ set(EDITSCENE_SOURCES
systems/BuoyancySystem.cpp
systems/EditorSunSystem.cpp
systems/EditorSkyboxSystem.cpp
systems/EditorWaterPlaneSystem.cpp
systems/LightSystem.cpp
systems/CameraSystem.cpp
systems/LodSystem.cpp
@@ -140,6 +141,7 @@ set(EDITSCENE_HEADERS
systems/BuoyancySystem.hpp
systems/EditorSunSystem.hpp
systems/EditorSkyboxSystem.hpp
systems/EditorWaterPlaneSystem.hpp
systems/LightSystem.hpp
systems/CameraSystem.hpp
systems/LodSystem.hpp

View File

@@ -5,6 +5,7 @@
#include "systems/BuoyancySystem.hpp"
#include "systems/EditorSunSystem.hpp"
#include "systems/EditorSkyboxSystem.hpp"
#include "systems/EditorWaterPlaneSystem.hpp"
#include "systems/LightSystem.hpp"
#include "systems/CameraSystem.hpp"
#include "systems/LodSystem.hpp"
@@ -238,6 +239,9 @@ void EditorApp::setup()
m_sunSystem = std::make_unique<EditorSunSystem>(m_world, m_sceneMgr);
m_skyboxSystem =
std::make_unique<EditorSkyboxSystem>(m_world, m_sceneMgr);
m_waterPlaneSystem =
std::make_unique<EditorWaterPlaneSystem>(m_world,
m_sceneMgr);
// Apply debug setting if it was set before system creation
if (m_debugBuoyancy) {
@@ -705,6 +709,14 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
cam = m_camera->getCamera();
m_skyboxSystem->update(cam);
}
if (m_waterPlaneSystem) {
Ogre::Camera *cam = nullptr;
if (m_sceneMgr->hasCamera("PlayerCamera"))
cam = m_sceneMgr->getCamera("PlayerCamera");
if (!cam && m_camera)
cam = m_camera->getCamera();
m_waterPlaneSystem->update(evt.timeSinceLastFrame, cam);
}
if (m_lightSystem) {
m_lightSystem->update();
}

View File

@@ -31,6 +31,7 @@ class PlayerControllerSystem;
class BuoyancySystem;
class EditorSunSystem;
class EditorSkyboxSystem;
class EditorWaterPlaneSystem;
class EditorApp;
/**
@@ -200,6 +201,7 @@ private:
std::unique_ptr<BuoyancySystem> m_buoyancySystem;
std::unique_ptr<EditorSunSystem> m_sunSystem;
std::unique_ptr<EditorSkyboxSystem> m_skyboxSystem;
std::unique_ptr<EditorWaterPlaneSystem> m_waterPlaneSystem;
std::unique_ptr<EditorLightSystem> m_lightSystem;
std::unique_ptr<EditorCameraSystem> m_cameraSystem;
std::unique_ptr<EditorLodSystem> m_lodSystem;

View File

@@ -6,58 +6,68 @@
/**
* WaterPlane component
* Provides visual representation of water surface for buoyancy system
* Creates a plane mesh at the water surface Y level for visualization
* Visual water surface with reflection/refraction
* for the editScene editor. Lightweight and OpenGL ES 2.0 compatible.
*/
struct WaterPlane {
// Enable/disable water plane visualization
// Enable/disable water
bool enabled = true;
// Water surface Y level (world space) - should match WaterPhysics::waterSurfaceY
// Water surface Y level (world space)
float waterSurfaceY = -0.1f;
// Plane size (width and depth)
float planeSize = 100.0f;
float planeSize = 500.0f;
// Material name for water plane
Ogre::String materialName = "BaseWhiteNoLighting";
// Opacity (0.0 = transparent, 1.0 = opaque)
float opacity = 0.3f;
// Color tint
Ogre::ColourValue color = Ogre::ColourValue(0.2f, 0.4f, 0.8f, 0.3f);
// Whether to update automatically from WaterPhysics component
// Whether to update waterSurfaceY from WaterPhysics component
bool autoUpdateFromWaterPhysics = true;
// Scene node for the water plane (managed by system)
Ogre::SceneNode *sceneNode = nullptr;
// Visual settings
Ogre::ColourValue waterColor = Ogre::ColourValue(0.0f, 0.3f, 0.5f,
0.8f);
float reflectivity = 0.5f;
float waveSpeed = 1.0f;
float waveScale = 0.02f;
float tiling = 0.012f;
// Manual object for the water plane (managed by system)
// Render texture size (power of two recommended)
int renderTextureSize = 512;
// Runtime objects (managed by EditorWaterPlaneSystem)
Ogre::SceneNode *sceneNode = nullptr;
Ogre::ManualObject *manualObject = nullptr;
// Mark component as dirty (needs update)
// Render-to-texture resources
Ogre::TexturePtr renderTexture;
Ogre::Camera *reflectionCamera = nullptr;
Ogre::Camera *refractionCamera = nullptr;
Ogre::Viewport *reflectionViewport = nullptr;
Ogre::Viewport *refractionViewport = nullptr;
Ogre::RenderTarget *renderTarget = nullptr;
// Clip planes
Ogre::Plane reflectionPlane;
Ogre::Plane reflectionClipPlane;
Ogre::Plane refractionClipPlane;
// Camera following state
Ogre::Vector3 lastCameraPos = Ogre::Vector3::ZERO;
float positionUpdateTimer = 0.0f;
static constexpr float POSITION_UPDATE_INTERVAL = 0.3f;
// Which viewport to update this frame (0=reflection, 1=refraction)
int updateViewportIndex = 0;
// Time accumulator for shader
float shaderTime = 0.0f;
// Dirty flag
bool dirty = true;
void markDirty()
{
dirty = true;
}
WaterPlane() = default;
WaterPlane(float surfaceY, float size, const Ogre::String &material,
float opac, const Ogre::ColourValue &col, bool autoUpdate)
: waterSurfaceY(surfaceY)
, planeSize(size)
, materialName(material)
, opacity(opac)
, color(col)
, autoUpdateFromWaterPhysics(autoUpdate)
, dirty(true)
{
}
};
#endif // EDITSCENE_WATERPLANE_HPP
#endif // EDITSCENE_WATERPLANE_HPP

View File

@@ -1,23 +1,15 @@
#include "WaterPlane.hpp"
#include "../components/WaterPhysics.hpp"
#include "../components/EditorMarker.hpp"
#include "../components/EntityName.hpp"
#include "../components/Transform.hpp"
#include "Transform.hpp"
#include "EditorMarker.hpp"
#include "EntityName.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/WaterPlaneEditor.hpp"
#include <OgreLogManager.h>
#include <OgreManualObject.h>
#include <OgreSceneManager.h>
#include <OgreSceneNode.h>
#include <OgreMaterialManager.h>
#include <OgreTechnique.h>
#include <OgrePass.h>
// Register WaterPlane component
REGISTER_COMPONENT("Water Plane", WaterPlane, WaterPlaneEditor)
REGISTER_COMPONENT_GROUP("Water Plane", "Water", WaterPlane, WaterPlaneEditor)
{
registry.registerComponent<WaterPlane>(
"Water Plane", "Water", std::make_unique<WaterPlaneEditor>(),
"Water Plane", "Water",
std::make_unique<WaterPlaneEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<WaterPlane>()) {
@@ -31,236 +23,3 @@ REGISTER_COMPONENT("Water Plane", WaterPlane, WaterPlaneEditor)
}
});
}
/**
* WaterPlaneModule
* Manages WaterPlane components and creates visual water surface representations
*/
class WaterPlaneModule {
public:
WaterPlaneModule(flecs::world &world, Ogre::SceneManager *sceneMgr)
: m_world(world)
, m_sceneMgr(sceneMgr)
{
// Register WaterPlane component
world.component<WaterPlane>();
// Create system for updating water planes
world.system<WaterPlane>("UpdateWaterPlanes")
.kind(flecs::OnUpdate)
.each([this](flecs::entity entity,
WaterPlane &waterPlane) {
updateWaterPlane(entity, waterPlane);
});
// Create system for cleaning up water planes when entities are destroyed
world.observer<WaterPlane>("CleanupWaterPlanes")
.event(flecs::OnRemove)
.each([this](flecs::entity entity,
WaterPlane &waterPlane) {
cleanupWaterPlane(waterPlane);
});
Ogre::LogManager::getSingleton().logMessage(
"WaterPlaneModule initialized");
}
~WaterPlaneModule()
{
// Clean up any remaining water planes
m_world.each<WaterPlane>(
[this](flecs::entity entity, WaterPlane &waterPlane) {
cleanupWaterPlane(waterPlane);
});
}
private:
void updateWaterPlane(flecs::entity entity, WaterPlane &waterPlane)
{
// Skip if not enabled
if (!waterPlane.enabled) {
if (waterPlane.sceneNode) {
waterPlane.sceneNode->setVisible(false);
}
return;
}
// Auto-update water surface Y from WaterPhysics if enabled
if (waterPlane.autoUpdateFromWaterPhysics) {
// Find WaterPhysics component in the world
m_world.query<WaterPhysics>().each(
[&](flecs::entity wpEntity,
WaterPhysics &waterPhysics) {
if (waterPhysics.waterSurfaceY !=
waterPlane.waterSurfaceY) {
waterPlane.waterSurfaceY =
waterPhysics
.waterSurfaceY;
waterPlane.markDirty();
}
});
}
// Create or update water plane visualization
if (waterPlane.dirty || !waterPlane.sceneNode) {
createOrUpdateWaterPlane(entity, waterPlane);
waterPlane.dirty = false;
}
// Ensure visibility
if (waterPlane.sceneNode) {
waterPlane.sceneNode->setVisible(true);
}
}
void createOrUpdateWaterPlane(flecs::entity entity,
WaterPlane &waterPlane)
{
// Clean up existing water plane
cleanupWaterPlane(waterPlane);
// Create scene node
Ogre::String nodeName =
"WaterPlaneNode_" +
Ogre::StringConverter::toString(entity.id());
waterPlane.sceneNode =
m_sceneMgr->getRootSceneNode()->createChildSceneNode(
nodeName);
// Create manual object
Ogre::String objName =
"WaterPlaneObject_" +
Ogre::StringConverter::toString(entity.id());
waterPlane.manualObject =
m_sceneMgr->createManualObject(objName);
// Create or get material for water plane
Ogre::String materialName =
"WaterPlaneMaterial_" +
Ogre::StringConverter::toString(entity.id());
Ogre::MaterialPtr material =
Ogre::MaterialManager::getSingleton()
.createOrRetrieve(
materialName,
Ogre::ResourceGroupManager::
DEFAULT_RESOURCE_GROUP_NAME)
.first.staticCast<Ogre::Material>();
// Configure material for transparent water
material->setReceiveShadows(false);
material->getTechnique(0)->getPass(0)->setDiffuse(
waterPlane.color);
material->getTechnique(0)->getPass(0)->setAmbient(
waterPlane.color * 0.5f);
material->getTechnique(0)->getPass(0)->setSelfIllumination(
waterPlane.color * 0.2f);
material->getTechnique(0)->getPass(0)->setSceneBlending(
Ogre::SBT_TRANSPARENT_ALPHA);
material->getTechnique(0)->getPass(0)->setDepthWriteEnabled(
false);
material->getTechnique(0)->getPass(0)->setLightingEnabled(true);
// Create water plane geometry
float halfSize = waterPlane.planeSize * 0.5f;
waterPlane.manualObject->begin(
materialName, Ogre::RenderOperation::OT_TRIANGLE_LIST);
// Create a simple plane (2 triangles)
// Vertex 0: (-halfSize, waterSurfaceY, -halfSize)
waterPlane.manualObject->position(
-halfSize, waterPlane.waterSurfaceY, -halfSize);
waterPlane.manualObject->normal(0.0f, 1.0f, 0.0f);
waterPlane.manualObject->textureCoord(0.0f, 0.0f);
// Vertex 1: (halfSize, waterSurfaceY, -halfSize)
waterPlane.manualObject->position(
halfSize, waterPlane.waterSurfaceY, -halfSize);
waterPlane.manualObject->normal(0.0f, 1.0f, 0.0f);
waterPlane.manualObject->textureCoord(1.0f, 0.0f);
// Vertex 2: (halfSize, waterSurfaceY, halfSize)
waterPlane.manualObject->position(
halfSize, waterPlane.waterSurfaceY, halfSize);
waterPlane.manualObject->normal(0.0f, 1.0f, 0.0f);
waterPlane.manualObject->textureCoord(1.0f, 1.0f);
// Vertex 3: (-halfSize, waterSurfaceY, halfSize)
waterPlane.manualObject->position(
-halfSize, waterPlane.waterSurfaceY, halfSize);
waterPlane.manualObject->normal(0.0f, 1.0f, 0.0f);
waterPlane.manualObject->textureCoord(0.0f, 1.0f);
// First triangle
waterPlane.manualObject->triangle(0, 1, 2);
// Second triangle
waterPlane.manualObject->triangle(0, 2, 3);
waterPlane.manualObject->end();
// Attach to scene node
waterPlane.sceneNode->attachObject(waterPlane.manualObject);
// Log creation
Ogre::LogManager::getSingleton().logMessage(
"Created water plane at Y=" +
Ogre::StringConverter::toString(
waterPlane.waterSurfaceY) +
", size=" +
Ogre::StringConverter::toString(waterPlane.planeSize));
}
void cleanupWaterPlane(WaterPlane &waterPlane)
{
if (waterPlane.manualObject) {
if (waterPlane.sceneNode) {
waterPlane.sceneNode->detachObject(
waterPlane.manualObject);
}
m_sceneMgr->destroyManualObject(
waterPlane.manualObject);
waterPlane.manualObject = nullptr;
}
if (waterPlane.sceneNode) {
m_sceneMgr->destroySceneNode(waterPlane.sceneNode);
waterPlane.sceneNode = nullptr;
}
}
flecs::world &m_world;
Ogre::SceneManager *m_sceneMgr;
};
// Function to initialize WaterPlaneModule
void initializeWaterPlaneModule(flecs::world &world,
Ogre::SceneManager *sceneMgr)
{
static WaterPlaneModule *module = nullptr;
if (!module) {
module = new WaterPlaneModule(world, sceneMgr);
// Create default WaterPlane entity if none exists
bool hasWaterPlane = false;
world.query<WaterPlane>().each(
[&](flecs::entity, WaterPlane &) {
hasWaterPlane = true;
});
if (!hasWaterPlane) {
flecs::entity waterPlaneEntity =
world.entity("WaterPlane");
waterPlaneEntity.set<WaterPlane>({});
waterPlaneEntity.add<EditorMarkerComponent>();
waterPlaneEntity.set<EntityNameComponent>(
EntityNameComponent("Water Plane"));
// Add Transform component for potential future use
waterPlaneEntity.set<TransformComponent>(
TransformComponent());
Ogre::LogManager::getSingleton().logMessage(
"Created default WaterPlane entity");
}
}
}

View File

@@ -0,0 +1,47 @@
OGRE_NATIVE_GLSL_VERSION_DIRECTIVE
#include <OgreUnifiedShader.h>
SAMPLER2D(reflectRefractMap, 0);
SAMPLER2D(noiseMap, 1);
OGRE_UNIFORMS(
uniform float time;
uniform float waveScale;
uniform vec4 waterColor;
uniform float reflectivity;
)
IN(vec4 vClipSpace, TEXCOORD0)
IN(vec2 vUV, TEXCOORD1)
MAIN_DECLARATION
{
// Normalized device coordinates
vec2 ndc = vClipSpace.xy / vClipSpace.w;
ndc = ndc * 0.5 + 0.5;
// Noise-based distortion at 3 scales
vec2 distortion1 = (texture2D(noiseMap, vUV + vec2(time * 0.01, time * 0.008)).rg * 2.0 - 1.0) * waveScale;
vec2 distortion2 = (texture2D(noiseMap, vUV * 2.0 + vec2(-time * 0.006, time * 0.012)).rg * 2.0 - 1.0) * waveScale * 0.5;
vec2 distortion3 = (texture2D(noiseMap, vUV * 4.0 + vec2(time * 0.004, -time * 0.005)).rg * 2.0 - 1.0) * waveScale * 0.25;
vec2 totalDistortion = distortion1 + distortion2 + distortion3;
// Reflection samples from left half of texture
vec2 reflectionUV = vec2(ndc.x, 1.0 - ndc.y) * 0.5 + totalDistortion;
// Refraction samples from right half of texture
vec2 refractionUV = vec2(ndc.x, ndc.y) * 0.5 + vec2(0.5, 0.0) + totalDistortion;
// Clamp to avoid sampling outside viewport
reflectionUV = clamp(reflectionUV, vec2(0.001, 0.001), vec2(0.499, 0.999));
refractionUV = clamp(refractionUV, vec2(0.501, 0.001), vec2(0.999, 0.999));
vec4 reflectionColour = texture2D(reflectRefractMap, reflectionUV);
vec4 refractionColour = texture2D(reflectRefractMap, refractionUV);
// Mix reflection and refraction
vec4 water = mix(refractionColour, reflectionColour, reflectivity);
// Apply water color tint
gl_FragColor = water * waterColor;
gl_FragColor.a = waterColor.a;
}

View File

@@ -0,0 +1,36 @@
material WaterPlane/Dynamic
{
technique
{
pass
{
lighting off
depth_write off
scene_blend alpha_blend
cull_hardware none
vertex_program_ref WaterPlaneVP
{
param_named tiling float 0.012
}
fragment_program_ref WaterPlaneFP
{
}
texture_unit
{
texture ReflectionRefractionTexture
tex_address_mode mirror
filtering linear linear linear
}
texture_unit
{
texture waves2.png
tex_address_mode wrap
filtering linear linear linear
}
}
}
}

View File

@@ -0,0 +1,20 @@
vertex_program WaterPlaneVP glsl glsles glslang hlsl
{
source water_plane.vert
default_params
{
param_named_auto worldViewProj worldviewproj_matrix
}
}
fragment_program WaterPlaneFP glsl glsles glslang hlsl
{
source water_plane.frag
default_params
{
param_named time float 0.0
param_named waveScale float 0.02
param_named waterColor float4 0.0 0.3 0.5 0.8
param_named reflectivity float 0.5
}
}

View File

@@ -0,0 +1,21 @@
OGRE_NATIVE_GLSL_VERSION_DIRECTIVE
#include <OgreUnifiedShader.h>
OGRE_UNIFORMS(
uniform mat4 worldViewProj;
uniform float tiling;
)
OUT(vec4 vClipSpace, TEXCOORD0)
OUT(vec2 vUV, TEXCOORD1)
MAIN_PARAMETERS
IN(vec4 vertex, POSITION)
IN(vec2 uv0, TEXCOORD0)
MAIN_DECLARATION
{
vec4 worldPos = mul(worldViewProj, vec4(vertex.xyz, 1.0));
gl_Position = worldPos;
vClipSpace = worldPos;
vUV = uv0 * tiling;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

View File

@@ -8,6 +8,7 @@
#include "../components/RigidBody.hpp"
#include "../components/BuoyancyInfo.hpp"
#include "../components/WaterPhysics.hpp"
#include "../components/WaterPlane.hpp"
#include "../components/Sun.hpp"
#include "../components/Skybox.hpp"
#include "../components/Light.hpp"
@@ -639,6 +640,15 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
componentCount++;
}
// Render WaterPlane if present
if (entity.has<WaterPlane>()) {
auto &wp = entity.get_mut<WaterPlane>();
if (m_componentRegistry.render<WaterPlane>(entity, wp)) {
wp.markDirty();
}
componentCount++;
}
// Render LOD Settings if present
if (entity.has<LodSettingsComponent>()) {
auto &lodSettings = entity.get_mut<LodSettingsComponent>();

View File

@@ -0,0 +1,407 @@
#include "EditorWaterPlaneSystem.hpp"
#include "../components/WaterPlane.hpp"
#include "../components/Transform.hpp"
#include "../components/WaterPhysics.hpp"
#include <OgreLogManager.h>
#include <OgreManualObject.h>
#include <OgreMaterialManager.h>
#include <OgreTechnique.h>
#include <OgrePass.h>
#include <OgreGpuProgramParams.h>
static const uint32_t WATER_MASK = 0xF00;
void EditorWaterPlaneSystem::WaterRenderTargetListener::preRenderTargetUpdate(
const Ogre::RenderTargetEvent &evt)
{
if (waterMesh)
waterMesh->setVisible(false);
}
void EditorWaterPlaneSystem::WaterRenderTargetListener::postRenderTargetUpdate(
const Ogre::RenderTargetEvent &evt)
{
if (waterMesh)
waterMesh->setVisible(true);
}
EditorWaterPlaneSystem::EditorWaterPlaneSystem(flecs::world &world,
Ogre::SceneManager *sceneMgr)
: m_world(world)
, m_sceneMgr(sceneMgr)
, m_query(world.query<WaterPlane, TransformComponent>())
{
}
EditorWaterPlaneSystem::~EditorWaterPlaneSystem() = default;
void EditorWaterPlaneSystem::update(float deltaTime, Ogre::Camera *camera)
{
m_query.each([&](flecs::entity entity, WaterPlane &wp,
TransformComponent &transform) {
if (!wp.enabled) {
if (wp.sceneNode)
wp.sceneNode->setVisible(false);
return;
}
if (wp.dirty || !wp.manualObject) {
rebuildWaterPlane(entity, wp, transform);
wp.dirty = false;
}
if (!wp.sceneNode || !wp.manualObject)
return;
// Auto-sync Y from WaterPhysics
if (wp.autoUpdateFromWaterPhysics) {
m_world.query<WaterPhysics>().each(
[&](flecs::entity, WaterPhysics &physics) {
if (physics.waterSurfaceY != wp.waterSurfaceY) {
wp.waterSurfaceY = physics.waterSurfaceY;
wp.sceneNode->setPosition(
wp.sceneNode->getPosition().x,
wp.waterSurfaceY,
wp.sceneNode->getPosition().z);
}
});
}
// Update shader time and params
updateShaderParams(wp, deltaTime);
// Update cameras
if (camera)
updateCameras(wp, camera);
// Follow camera on XZ plane
if (camera)
updatePosition(wp, deltaTime, camera);
// Alternate viewport updates
if (wp.renderTarget) {
if (wp.updateViewportIndex == 0 && wp.reflectionViewport)
wp.reflectionViewport->update();
else if (wp.updateViewportIndex == 1 && wp.refractionViewport)
wp.refractionViewport->update();
wp.updateViewportIndex = 1 - wp.updateViewportIndex;
}
wp.sceneNode->setVisible(true);
});
}
void EditorWaterPlaneSystem::rebuildWaterPlane(
flecs::entity entity, WaterPlane &wp,
TransformComponent &transform)
{
cleanupWaterPlane(wp);
Ogre::String baseName = "WaterPlane_" +
Ogre::StringConverter::toString(entity.id());
// Create scene node
if (transform.node) {
wp.sceneNode =
transform.node->createChildSceneNode(baseName + "Node");
} else {
wp.sceneNode = m_sceneMgr->getRootSceneNode()->createChildSceneNode(
baseName + "Node");
}
wp.sceneNode->setPosition(0.0f, wp.waterSurfaceY, 0.0f);
// Create plane mesh
createPlaneMesh(wp);
// Create render texture
Ogre::String texName = baseName + "_RTT";
wp.renderTexture = Ogre::TextureManager::getSingleton().createManual(
texName, Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
Ogre::TEX_TYPE_2D, wp.renderTextureSize, wp.renderTextureSize, 0,
Ogre::PF_R8G8B8A8, Ogre::TU_RENDERTARGET);
wp.renderTarget = wp.renderTexture->getBuffer()->getRenderTarget();
wp.renderTarget->setAutoUpdated(false);
// Set up clip planes
wp.reflectionPlane =
Ogre::Plane(Ogre::Vector3(0.0f, 1.0f, 0.0f), wp.waterSurfaceY);
wp.reflectionClipPlane = Ogre::Plane(Ogre::Vector3(0.0f, 1.0f, 0.0f),
wp.waterSurfaceY - 0.1f);
wp.refractionClipPlane = Ogre::Plane(
Ogre::Vector3(0.0f, -1.0f, 0.0f), -(wp.waterSurfaceY + 0.1f));
// Set listener to hide water during RTT
m_listener.waterMesh = wp.manualObject;
wp.renderTarget->addListener(&m_listener);
// Create cameras with their own nodes
wp.reflectionCamera = m_sceneMgr->createCamera(baseName + "ReflectionCam");
wp.refractionCamera = m_sceneMgr->createCamera(baseName + "RefractionCam");
Ogre::SceneNode *reflNode =
m_sceneMgr->getRootSceneNode()->createChildSceneNode(
baseName + "ReflectionNode");
reflNode->attachObject(wp.reflectionCamera);
Ogre::SceneNode *refrNode =
m_sceneMgr->getRootSceneNode()->createChildSceneNode(
baseName + "RefractionNode");
refrNode->attachObject(wp.refractionCamera);
// Reflection viewport (left half)
wp.reflectionViewport = wp.renderTarget->addViewport(
wp.reflectionCamera, 0, 0.0f, 0.0f, 0.5f, 1.0f);
wp.reflectionViewport->setClearEveryFrame(true);
wp.reflectionViewport->setBackgroundColour(
Ogre::ColourValue(0.0f, 0.0f, 0.1f, 1.0f));
wp.reflectionViewport->setOverlaysEnabled(false);
wp.reflectionViewport->setSkiesEnabled(true);
wp.reflectionViewport->setAutoUpdated(false);
wp.reflectionViewport->setVisibilityMask(~WATER_MASK);
// Refraction viewport (right half)
wp.refractionViewport = wp.renderTarget->addViewport(
wp.refractionCamera, 1, 0.5f, 0.0f, 0.5f, 1.0f);
wp.refractionViewport->setClearEveryFrame(true);
wp.refractionViewport->setBackgroundColour(
Ogre::ColourValue(0.0f, 0.1f, 0.2f, 1.0f));
wp.refractionViewport->setOverlaysEnabled(false);
wp.refractionViewport->setSkiesEnabled(false);
wp.refractionViewport->setAutoUpdated(false);
wp.refractionViewport->setVisibilityMask(~WATER_MASK);
// Apply material
Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().getByName(
"WaterPlane/Dynamic",
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
if (!mat) {
Ogre::LogManager::getSingleton().logMessage(
"WaterPlaneSystem: Warning - WaterPlane/Dynamic material not found, using placeholder");
}
wp.manualObject->setMaterialName(0, "WaterPlane/Dynamic");
// Set visibility mask so cameras can hide water
wp.manualObject->setVisibilityFlags(WATER_MASK);
Ogre::LogManager::getSingleton().logMessage(
"WaterPlaneSystem: Created water plane for entity " +
Ogre::StringConverter::toString(entity.id()) +
", size=" + Ogre::StringConverter::toString(wp.planeSize) +
", Y=" + Ogre::StringConverter::toString(wp.waterSurfaceY));
}
void EditorWaterPlaneSystem::cleanupWaterPlane(WaterPlane &wp)
{
if (wp.renderTarget) {
wp.renderTarget->removeListener(&m_listener);
wp.renderTarget->removeAllViewports();
wp.renderTarget = nullptr;
}
wp.reflectionViewport = nullptr;
wp.refractionViewport = nullptr;
if (wp.renderTexture) {
Ogre::TextureManager::getSingleton().remove(wp.renderTexture);
wp.renderTexture.reset();
}
if (wp.reflectionCamera) {
Ogre::SceneNode *node = wp.reflectionCamera->getParentSceneNode();
if (node) {
node->detachObject(wp.reflectionCamera);
m_sceneMgr->destroySceneNode(node);
}
m_sceneMgr->destroyCamera(wp.reflectionCamera);
wp.reflectionCamera = nullptr;
}
if (wp.refractionCamera) {
Ogre::SceneNode *node = wp.refractionCamera->getParentSceneNode();
if (node) {
node->detachObject(wp.refractionCamera);
m_sceneMgr->destroySceneNode(node);
}
m_sceneMgr->destroyCamera(wp.refractionCamera);
wp.refractionCamera = nullptr;
}
if (wp.manualObject) {
if (wp.sceneNode)
wp.sceneNode->detachObject(wp.manualObject);
m_sceneMgr->destroyManualObject(wp.manualObject);
wp.manualObject = nullptr;
}
if (wp.sceneNode) {
if (wp.sceneNode->getParentSceneNode()) {
wp.sceneNode->getParentSceneNode()->removeChild(
wp.sceneNode);
}
m_sceneMgr->destroySceneNode(wp.sceneNode);
wp.sceneNode = nullptr;
}
}
void EditorWaterPlaneSystem::createPlaneMesh(WaterPlane &wp)
{
Ogre::String objName = "WaterPlaneMesh_" +
Ogre::StringConverter::toString(
(size_t)wp.sceneNode);
wp.manualObject = m_sceneMgr->createManualObject(objName);
int segments = 20;
float halfSize = wp.planeSize * 0.5f;
float step = wp.planeSize / (float)segments;
wp.manualObject->begin("WaterPlane/Dynamic",
Ogre::RenderOperation::OT_TRIANGLE_LIST);
for (int z = 0; z <= segments; z++) {
for (int x = 0; x <= segments; x++) {
float px = -halfSize + x * step;
float pz = -halfSize + z * step;
float u = (float)x / (float)segments;
float v = (float)z / (float)segments;
wp.manualObject->position(px, 0.0f, pz);
wp.manualObject->normal(0.0f, 1.0f, 0.0f);
wp.manualObject->textureCoord(u, v);
}
}
for (int z = 0; z < segments; z++) {
for (int x = 0; x < segments; x++) {
int i0 = z * (segments + 1) + x;
int i1 = i0 + 1;
int i2 = i0 + (segments + 1);
int i3 = i2 + 1;
wp.manualObject->triangle(i0, i2, i1);
wp.manualObject->triangle(i1, i2, i3);
}
}
wp.manualObject->end();
wp.sceneNode->attachObject(wp.manualObject);
}
Ogre::SceneNode *EditorWaterPlaneSystem::getReflectionNode(WaterPlane &wp)
{
if (!wp.reflectionCamera)
return nullptr;
Ogre::SceneNode *node = wp.reflectionCamera->getParentSceneNode();
return node;
}
Ogre::SceneNode *EditorWaterPlaneSystem::getRefractionNode(WaterPlane &wp)
{
if (!wp.refractionCamera)
return nullptr;
Ogre::SceneNode *node = wp.refractionCamera->getParentSceneNode();
return node;
}
void EditorWaterPlaneSystem::updateCameras(WaterPlane &wp,
Ogre::Camera *camera)
{
if (!camera)
return;
Ogre::SceneNode *camNode = camera->getParentSceneNode();
Ogre::Vector3 camPos = camNode ? camNode->_getDerivedPosition()
: Ogre::Vector3::ZERO;
Ogre::Quaternion camOrient = camNode
? camNode->_getDerivedOrientation()
: Ogre::Quaternion::IDENTITY;
// Reflection camera: mirrored below water plane
if (wp.reflectionCamera) {
wp.reflectionCamera->setAspectRatio(
camera->getAspectRatio());
wp.reflectionCamera->setNearClipDistance(
camera->getNearClipDistance());
wp.reflectionCamera->setFarClipDistance(
camera->getFarClipDistance());
Ogre::SceneNode *node = getReflectionNode(wp);
if (node) {
node->setPosition(camPos);
node->setOrientation(camOrient);
}
wp.reflectionCamera->enableReflection(wp.reflectionPlane);
wp.reflectionCamera->enableCustomNearClipPlane(
wp.reflectionClipPlane);
}
// Refraction camera: same position/orientation, clipped above water
if (wp.refractionCamera) {
wp.refractionCamera->setAspectRatio(
camera->getAspectRatio());
wp.refractionCamera->setNearClipDistance(
camera->getNearClipDistance());
wp.refractionCamera->setFarClipDistance(
camera->getFarClipDistance());
Ogre::SceneNode *node = getRefractionNode(wp);
if (node) {
node->setPosition(camPos);
node->setOrientation(camOrient);
}
wp.refractionCamera->enableCustomNearClipPlane(
wp.refractionClipPlane);
}
}
void EditorWaterPlaneSystem::updatePosition(WaterPlane &wp, float deltaTime,
Ogre::Camera *camera)
{
if (!camera)
return;
wp.positionUpdateTimer += deltaTime;
if (wp.positionUpdateTimer < WaterPlane::POSITION_UPDATE_INTERVAL)
return;
wp.positionUpdateTimer = 0.0f;
Ogre::SceneNode *camNode = camera->getParentSceneNode();
Ogre::Vector3 camPos = camNode ? camNode->_getDerivedPosition()
: Ogre::Vector3::ZERO;
Ogre::Vector3 waterPos = wp.sceneNode->getPosition();
Ogre::Vector3 targetPos = camPos;
targetPos.y = wp.waterSurfaceY;
Ogre::Vector3 d = targetPos - waterPos;
d.y = 0.0f; // Only move on XZ
if (d.squaredLength() < 100.0f * 100.0f)
wp.sceneNode->translate(d * 3.0f * deltaTime);
else
wp.sceneNode->translate(d);
}
void EditorWaterPlaneSystem::updateShaderParams(WaterPlane &wp,
float deltaTime)
{
wp.shaderTime += deltaTime * wp.waveSpeed;
Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().getByName(
"WaterPlane/Dynamic",
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
if (!mat)
return;
Ogre::Pass *pass = mat->getTechnique(0)->getPass(0);
Ogre::GpuProgramParametersSharedPtr fpParams =
pass->getFragmentProgramParameters();
if (!fpParams)
return;
fpParams->setNamedConstant("time", wp.shaderTime);
fpParams->setNamedConstant("waveScale", wp.waveScale);
fpParams->setNamedConstant(
"waterColor",
Ogre::Vector4(wp.waterColor.r, wp.waterColor.g,
wp.waterColor.b, wp.waterColor.a));
fpParams->setNamedConstant("reflectivity", wp.reflectivity);
Ogre::GpuProgramParametersSharedPtr vpParams =
pass->getVertexProgramParameters();
if (vpParams)
vpParams->setNamedConstant("tiling", wp.tiling);
}

View File

@@ -0,0 +1,49 @@
#ifndef EDITSCENE_EDITOR_WATERPLANE_SYSTEM_HPP
#define EDITSCENE_EDITOR_WATERPLANE_SYSTEM_HPP
#pragma once
#include <flecs.h>
#include <Ogre.h>
/**
* WaterPlane system - manages visual water surface with
* reflection/refraction render-to-texture.
*/
class EditorWaterPlaneSystem {
public:
EditorWaterPlaneSystem(flecs::world &world,
Ogre::SceneManager *sceneMgr);
~EditorWaterPlaneSystem();
void update(float deltaTime, Ogre::Camera *camera);
private:
void rebuildWaterPlane(flecs::entity entity, struct WaterPlane &wp,
struct TransformComponent &transform);
void cleanupWaterPlane(WaterPlane &wp);
void createPlaneMesh(WaterPlane &wp);
void updateCameras(WaterPlane &wp, Ogre::Camera *camera);
void updatePosition(WaterPlane &wp, float deltaTime,
Ogre::Camera *camera);
void updateShaderParams(WaterPlane &wp, float deltaTime);
Ogre::SceneNode *getReflectionNode(WaterPlane &wp);
Ogre::SceneNode *getRefractionNode(WaterPlane &wp);
// Render target listener - hides water during RTT update
class WaterRenderTargetListener :
public Ogre::RenderTargetListener {
public:
Ogre::ManualObject *waterMesh = nullptr;
void preRenderTargetUpdate(
const Ogre::RenderTargetEvent &evt) override;
void postRenderTargetUpdate(
const Ogre::RenderTargetEvent &evt) override;
};
flecs::world &m_world;
Ogre::SceneManager *m_sceneMgr;
flecs::query<struct WaterPlane, struct TransformComponent> m_query;
WaterRenderTargetListener m_listener;
};
#endif // EDITSCENE_EDITOR_WATERPLANE_SYSTEM_HPP

View File

@@ -25,6 +25,7 @@
#include "../components/GeneratedPhysicsTag.hpp"
#include "../components/BuoyancyInfo.hpp"
#include "../components/WaterPhysics.hpp"
#include "../components/WaterPlane.hpp"
#include "../components/Sun.hpp"
#include "../components/Skybox.hpp"
#include "EditorUISystem.hpp"
@@ -261,6 +262,10 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
json["waterPhysics"] = serializeWaterPhysics(entity);
}
if (entity.has<WaterPlane>()) {
json["waterPlane"] = serializeWaterPlane(entity);
}
if (entity.has<SunComponent>()) {
json["sun"] = serializeSun(entity);
}
@@ -430,6 +435,10 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json,
deserializeWaterPhysics(entity, json["waterPhysics"]);
}
if (json.contains("waterPlane")) {
deserializeWaterPlane(entity, json["waterPlane"]);
}
if (json.contains("sun")) {
deserializeSun(entity, json["sun"]);
}
@@ -2518,3 +2527,49 @@ void SceneSerializer::deserializeSkybox(flecs::entity entity,
entity.set<SkyboxComponent>(sky);
}
nlohmann::json SceneSerializer::serializeWaterPlane(flecs::entity entity)
{
const WaterPlane &wp = entity.get<WaterPlane>();
nlohmann::json json;
json["enabled"] = wp.enabled;
json["waterSurfaceY"] = wp.waterSurfaceY;
json["planeSize"] = wp.planeSize;
json["autoUpdateFromWaterPhysics"] = wp.autoUpdateFromWaterPhysics;
json["waterColor"] = { wp.waterColor.r, wp.waterColor.g,
wp.waterColor.b, wp.waterColor.a };
json["reflectivity"] = wp.reflectivity;
json["waveSpeed"] = wp.waveSpeed;
json["waveScale"] = wp.waveScale;
json["tiling"] = wp.tiling;
json["renderTextureSize"] = wp.renderTextureSize;
return json;
}
void SceneSerializer::deserializeWaterPlane(flecs::entity entity,
const nlohmann::json &json)
{
WaterPlane wp;
wp.enabled = json.value("enabled", true);
wp.waterSurfaceY = json.value("waterSurfaceY", -0.1f);
wp.planeSize = json.value("planeSize", 500.0f);
wp.autoUpdateFromWaterPhysics =
json.value("autoUpdateFromWaterPhysics", true);
if (json.contains("waterColor") && json["waterColor"].is_array() &&
json["waterColor"].size() >= 4) {
wp.waterColor = Ogre::ColourValue(
json["waterColor"][0], json["waterColor"][1],
json["waterColor"][2], json["waterColor"][3]);
}
wp.reflectivity = json.value("reflectivity", 0.5f);
wp.waveSpeed = json.value("waveSpeed", 1.0f);
wp.waveScale = json.value("waveScale", 0.02f);
wp.tiling = json.value("tiling", 0.012f);
wp.renderTextureSize = json.value("renderTextureSize", 512);
entity.set<WaterPlane>(wp);
}

View File

@@ -141,6 +141,11 @@ private:
void deserializeWaterPhysics(flecs::entity entity,
const nlohmann::json &json);
// WaterPlane serialization
nlohmann::json serializeWaterPlane(flecs::entity entity);
void deserializeWaterPlane(flecs::entity entity,
const nlohmann::json &json);
// Sun / Skybox serialization
nlohmann::json serializeSun(flecs::entity entity);
nlohmann::json serializeSkybox(flecs::entity entity);

View File

@@ -15,22 +15,20 @@ public:
{
bool changed = false;
ImGui::Text("Water Plane Visualization");
ImGui::PushID("WaterPlane");
ImGui::Text("Water Surface");
ImGui::Separator();
// Enabled toggle
if (ImGui::Checkbox("Enabled", &component.enabled)) {
changed = true;
component.markDirty();
}
// Auto-update from WaterPhysics
if (ImGui::Checkbox("Auto-update from Water Physics",
&component.autoUpdateFromWaterPhysics)) {
changed = true;
}
// Water surface Y level (editable if not auto-updating)
if (!component.autoUpdateFromWaterPhysics) {
if (ImGui::SliderFloat("Water Surface Y",
&component.waterSurfaceY, -10.0f,
@@ -44,50 +42,57 @@ public:
component.waterSurfaceY);
}
// Plane size
if (ImGui::SliderFloat("Plane Size", &component.planeSize,
10.0f, 500.0f, "%.0f")) {
100.0f, 2000.0f, "%.0f")) {
changed = true;
component.markDirty();
}
if (ImGui::SliderInt("RTT Size", &component.renderTextureSize,
128, 1024)) {
changed = true;
component.markDirty();
}
ImGui::Separator();
ImGui::Text("Visual Appearance");
ImGui::Text("Appearance");
// Color picker
float color[4] = { component.color.r, component.color.g,
component.color.b, component.color.a };
if (ImGui::ColorEdit4("Color", color,
float color[4] = { component.waterColor.r, component.waterColor.g,
component.waterColor.b, component.waterColor.a };
if (ImGui::ColorEdit4("Water Color", color,
ImGuiColorEditFlags_AlphaBar)) {
component.color = Ogre::ColourValue(color[0], color[1],
color[2], color[3]);
component.waterColor = Ogre::ColourValue(
color[0], color[1], color[2], color[3]);
changed = true;
component.markDirty();
}
// Opacity
if (ImGui::SliderFloat("Opacity", &component.opacity, 0.0f,
1.0f, "%.2f")) {
component.color.a = component.opacity;
if (ImGui::SliderFloat("Reflectivity", &component.reflectivity,
0.0f, 1.0f, "%.2f")) {
changed = true;
component.markDirty();
}
// Material name (read-only for now)
ImGui::Text("Material: %s", component.materialName.c_str());
if (ImGui::SliderFloat("Wave Speed", &component.waveSpeed, 0.0f,
5.0f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Wave Scale", &component.waveScale, 0.0f,
0.2f, "%.3f")) {
changed = true;
}
if (ImGui::SliderFloat("Tiling", &component.tiling, 0.001f,
0.1f, "%.3f")) {
changed = true;
}
// Force update button
ImGui::Separator();
if (ImGui::Button("Force Update")) {
component.markDirty();
changed = true;
}
ImGui::SameLine();
if (ImGui::Button("Reset to Defaults")) {
component = WaterPlane();
changed = true;
component.markDirty();
}
ImGui::PopID();
return changed;
}
@@ -98,4 +103,4 @@ public:
}
};
#endif // WATER_PLANE_EDITOR_HPP
#endif // WATER_PLANE_EDITOR_HPP