diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 1ed6247..b85c52e 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -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 diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index dd3586b..1d4ac73 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -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(m_world, m_sceneMgr); m_skyboxSystem = std::make_unique(m_world, m_sceneMgr); + m_waterPlaneSystem = + std::make_unique(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(); } diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index b7f4398..45edebf 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -31,6 +31,7 @@ class PlayerControllerSystem; class BuoyancySystem; class EditorSunSystem; class EditorSkyboxSystem; +class EditorWaterPlaneSystem; class EditorApp; /** @@ -200,6 +201,7 @@ private: std::unique_ptr m_buoyancySystem; std::unique_ptr m_sunSystem; std::unique_ptr m_skyboxSystem; + std::unique_ptr m_waterPlaneSystem; std::unique_ptr m_lightSystem; std::unique_ptr m_cameraSystem; std::unique_ptr m_lodSystem; diff --git a/src/features/editScene/components/WaterPlane.hpp b/src/features/editScene/components/WaterPlane.hpp index 51ef08c..7223fab 100644 --- a/src/features/editScene/components/WaterPlane.hpp +++ b/src/features/editScene/components/WaterPlane.hpp @@ -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 \ No newline at end of file +#endif // EDITSCENE_WATERPLANE_HPP diff --git a/src/features/editScene/components/WaterPlaneModule.cpp b/src/features/editScene/components/WaterPlaneModule.cpp index d5a536f..c72ff64 100644 --- a/src/features/editScene/components/WaterPlaneModule.cpp +++ b/src/features/editScene/components/WaterPlaneModule.cpp @@ -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 -#include -#include -#include -#include -#include -#include -// Register WaterPlane component -REGISTER_COMPONENT("Water Plane", WaterPlane, WaterPlaneEditor) +REGISTER_COMPONENT_GROUP("Water Plane", "Water", WaterPlane, WaterPlaneEditor) { registry.registerComponent( - "Water Plane", "Water", std::make_unique(), + "Water Plane", "Water", + std::make_unique(), // Adder [](flecs::entity e) { if (!e.has()) { @@ -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(); - - // Create system for updating water planes - world.system("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("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( - [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().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(); - - // 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().each( - [&](flecs::entity, WaterPlane &) { - hasWaterPlane = true; - }); - - if (!hasWaterPlane) { - flecs::entity waterPlaneEntity = - world.entity("WaterPlane"); - waterPlaneEntity.set({}); - waterPlaneEntity.add(); - waterPlaneEntity.set( - EntityNameComponent("Water Plane")); - - // Add Transform component for potential future use - waterPlaneEntity.set( - TransformComponent()); - - Ogre::LogManager::getSingleton().logMessage( - "Created default WaterPlane entity"); - } - } -} \ No newline at end of file diff --git a/src/features/editScene/resources/materials/scripts/water_plane.frag b/src/features/editScene/resources/materials/scripts/water_plane.frag new file mode 100644 index 0000000..b7a2a66 --- /dev/null +++ b/src/features/editScene/resources/materials/scripts/water_plane.frag @@ -0,0 +1,47 @@ +OGRE_NATIVE_GLSL_VERSION_DIRECTIVE +#include + +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; +} diff --git a/src/features/editScene/resources/materials/scripts/water_plane.material b/src/features/editScene/resources/materials/scripts/water_plane.material new file mode 100644 index 0000000..fae37c2 --- /dev/null +++ b/src/features/editScene/resources/materials/scripts/water_plane.material @@ -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 + } + } + } +} diff --git a/src/features/editScene/resources/materials/scripts/water_plane.program b/src/features/editScene/resources/materials/scripts/water_plane.program new file mode 100644 index 0000000..fc88a8f --- /dev/null +++ b/src/features/editScene/resources/materials/scripts/water_plane.program @@ -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 + } +} diff --git a/src/features/editScene/resources/materials/scripts/water_plane.vert b/src/features/editScene/resources/materials/scripts/water_plane.vert new file mode 100644 index 0000000..cd005b4 --- /dev/null +++ b/src/features/editScene/resources/materials/scripts/water_plane.vert @@ -0,0 +1,21 @@ +OGRE_NATIVE_GLSL_VERSION_DIRECTIVE +#include + +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; +} diff --git a/src/features/editScene/resources/textures/waves2.png b/src/features/editScene/resources/textures/waves2.png new file mode 100644 index 0000000..a463d2f Binary files /dev/null and b/src/features/editScene/resources/textures/waves2.png differ diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 17dd20a..f7d2e12 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -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()) { + auto &wp = entity.get_mut(); + if (m_componentRegistry.render(entity, wp)) { + wp.markDirty(); + } + componentCount++; + } + // Render LOD Settings if present if (entity.has()) { auto &lodSettings = entity.get_mut(); diff --git a/src/features/editScene/systems/EditorWaterPlaneSystem.cpp b/src/features/editScene/systems/EditorWaterPlaneSystem.cpp new file mode 100644 index 0000000..90a84b7 --- /dev/null +++ b/src/features/editScene/systems/EditorWaterPlaneSystem.cpp @@ -0,0 +1,407 @@ +#include "EditorWaterPlaneSystem.hpp" +#include "../components/WaterPlane.hpp" +#include "../components/Transform.hpp" +#include "../components/WaterPhysics.hpp" +#include +#include +#include +#include +#include +#include + +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()) +{ +} + +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().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); +} diff --git a/src/features/editScene/systems/EditorWaterPlaneSystem.hpp b/src/features/editScene/systems/EditorWaterPlaneSystem.hpp new file mode 100644 index 0000000..bf1f35e --- /dev/null +++ b/src/features/editScene/systems/EditorWaterPlaneSystem.hpp @@ -0,0 +1,49 @@ +#ifndef EDITSCENE_EDITOR_WATERPLANE_SYSTEM_HPP +#define EDITSCENE_EDITOR_WATERPLANE_SYSTEM_HPP +#pragma once + +#include +#include + +/** + * 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 m_query; + WaterRenderTargetListener m_listener; +}; + +#endif // EDITSCENE_EDITOR_WATERPLANE_SYSTEM_HPP diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index a6a07d4..01d9975 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -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()) { + json["waterPlane"] = serializeWaterPlane(entity); + } + if (entity.has()) { 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(sky); } + + +nlohmann::json SceneSerializer::serializeWaterPlane(flecs::entity entity) +{ + const WaterPlane &wp = entity.get(); + 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(wp); +} diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index 1f2c48a..2ceb266 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -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); diff --git a/src/features/editScene/ui/WaterPlaneEditor.hpp b/src/features/editScene/ui/WaterPlaneEditor.hpp index 5b429e9..7a1208f 100644 --- a/src/features/editScene/ui/WaterPlaneEditor.hpp +++ b/src/features/editScene/ui/WaterPlaneEditor.hpp @@ -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 \ No newline at end of file +#endif // WATER_PLANE_EDITOR_HPP