top of page

Real-Time Interactive Water System

Real-Time Interactive Water System, a technical-art sandbox (Personal Project)

Real-Time Interactive Water System, a technical-art sandbox featuring an interactive water surface, a wind system for vegetation, and simple tools for procedural noise generation. It’s designed for experimenting with shaders, real-time effects, and environment behavior in a lightweight and flexible way.


Link to Github

https://github.com/Swaiky666/Art



⚙️ Real-Time Interactive Water System


1. Overall Approach


The interactive water system is based on capturing player interactions as dynamic ripple data and feeding that data into the water rendering pipeline. A camera positioned perpendicular to the water surface records interaction-induced ripples into an off-screen texture that is not visible to the player. This texture serves as the driving input for the water shader, enabling real-time ripple deformation and depth-based visual variation across the surface.


In parallel, a dynamic grayscale height map is generated using the same data source. By mapping world-space object positions to texture-space coordinates, the system evaluates wave height at specific points on the water surface. This allows floating and interacting objects to respond physically to the simulated water motion.


Water(shader) Overall View

ree

2. Earlier version VS Recent Version



In earlier versions, the water waves generated by the interaction became too high due to the accumulation of ripples.

ree
The latest version has fixed this issue, and the splashes from the small boat are now somewhat controlled.


ree



3. Technical Showcase


Dynamic interactive water ripples and waves:
From a black-and-white water ripple texture, generate interlaced dynamic base water waves.
Use a special camera to capture water ripples created by object–water interactions (visible only to that camera) and pass them as a texture into the water shader.
Combine the two to produce real-time ripples and waves.


ree


ree

ree

The color of water:

Use depth fade to control the color of shallow and deep water.

ree

Surface foam:

Use depth fade to remove foam farther from the shoreline.

ree

Obtain a real-time black-and-white image of the water surface height using the same method, and then use FloatingObjectController to dynamically change the height of objects affected by water ripples.
ree


ree


Water Surface Floating System:

A system that makes objects (like water plants) float naturally on a dynamic water surface by reading GPU-rendered wave heights and applying smooth motion.


ree

Core Components:
1. FloatingObjectController.cs

Purpose: Bridge between GPU water simulation and CPU game objects


How It Works:

  1. GPU → CPU Transfer: Periodically copies water height data from GPU RenderTexture to CPU Texture2D

  2. Height Query: Provides a public method for objects to check water height at any world position

  3. Performance Control: Uses configurable read interval to balance accuracy vs performance


Key Parameters:

  • waterHeightRT: The GPU texture containing wave heights

  • heightScaleFactor: Multiplier for wave intensity (1.0 = default)

  • readInterval: Time between GPU readbacks (default 0.3s)



Core Function:

csharp

public float GetScaledWaterHeight(Vector3 worldPos)
{
    // 1. Convert world position to UV coordinates (0-1)
    float u = (worldPos.x - waterBounds.min.x) / waterBounds.size.x;
    float v = (worldPos.z - waterBounds.min.z) / waterBounds.size.z;
    
    // 2. Convert UV to texture pixel coordinates
    int x = Mathf.FloorToInt(u * textureWidth);
    int y = Mathf.FloorToInt(v * textureHeight);
    
    // 3. Sample height from CPU texture
    float waveOffset = cpuHeightMap.GetPixel(x, y).r;
    
    // 4. Apply scaling and return final height
    return baseWaterLevel + (waveOffset * heightScaleFactor);
}
2. WaterPlant.cs

Purpose: Makes individual plant objects follow water surface smoothly


How It Works:

  1. Query: Each frame, asks FloatingObjectController for water height at its position

  2. Smooth Motion: Uses SmoothDamp to gradually move toward target height (avoids jittering)

  3. Update Position: Only changes Y coordinate, keeps X and Z unchanged


Key Parameters:

  • smoothDampTime: Controls how quickly plant follows waves (0.1s = responsive but smooth)






Core Update Loop:

csharp

void Update()
{
    // Get target water height at plant's position
    float targetY = waterController.GetScaledWaterHeight(transform.position);
    
    // Smoothly interpolate to target height
    float newY = Mathf.SmoothDamp(currentY, targetY, ref yVelocity, smoothDampTime);
    
    // Update position
    transform.position = new Vector3(x, newY, z);
}
System Flow

ree

Key Design Decisions

  1. Periodic Readback: GPU→CPU transfer is expensive, so we do it every 0.3s instead of every frame

  2. SmoothDamp: Prevents jittery motion by gradually moving to target height with velocity tracking

  3. UV Mapping: Converts 3D world positions to 2D texture coordinates for height lookup

  4. Scaling Factor: Allows global control of wave intensity without changing the shader


Performance Considerations

  • Trade-off: Lower readInterval = more accurate but more CPU load

  • Blocking Operation: ReadPixels() blocks the CPU temporarily

  • Memory: CPU texture uses RHalf format (half-precision float) to save memory

  • Recommended: 0.3s read interval is a good balance for most cases


Usage

  1. Attach FloatingObjectController to a manager object

  2. Assign water's RenderTexture and Renderer

  3. Attach WaterPlant to each plant prefab

  4. Adjust smoothDampTime for desired motion feel (smaller = faster response)


⚙️ Procedural Lotus Field Generator


1. Inspector View



A Unity Editor tool that uses Perlin noise-based distribution to procedurally generate natural-looking lotus fields. This tool combines noise generation with probability-based spawning to create organic flower arrangements.



ree
ree


2. Core Implementation

Multi-Octave Perlin Noise Generation(csharp)
public void GenerateNoiseTexture()
{
    System.Random prng = new System.Random(seed);
    Vector2[] octaveOffsets = new Vector2[octaves];
    
    for (int i = 0; i < octaves; i++)
    {
        float offsetX = prng.Next(-100000, 100000);
        float offsetY = prng.Next(-100000, 100000);
        octaveOffsets[i] = new Vector2(offsetX, offsetY);
    }

    for (int x = 0; x < textureResolution; x++)
    {
        for (int y = 0; y < textureResolution; y++)
        {
            float amplitude = 1;
            float frequency = 1;
            float noiseHeight = 0;
            float totalAmplitude = 0;

            for (int i = 0; i < octaves; i++)
            {
                float sampleX = (x / (float)textureResolution) * scale * frequency + octaveOffsets[i].x;
                float sampleY = (y / (float)textureResolution) * scale * frequency + octaveOffsets[i].y;

                float perlinValue = Mathf.PerlinNoise(sampleX, sampleY) * 2 - 1;

                noiseHeight += perlinValue * amplitude;
                totalAmplitude += amplitude;

                amplitude *= persistence;
                frequency *= lacunarity;
            }

            float finalValue = (noiseHeight / totalAmplitude) * 0.5f + 0.5f;
            noiseTexture.SetPixel(x, y, new Color(finalValue, finalValue, finalValue));
        }
    }
    
    noiseTexture.Apply();
}
Noise-Driven Probability Spawning(csharp)
// Sample noise texture for each grid pointfloat u = (float)i / sampleResolution;
float v = (float)j / sampleResolution;

int pixelX = Mathf.FloorToInt(u * noiseRes);
int pixelY = Mathf.FloorToInt(v * noiseRes);

float noiseValue = noiseTexture.GetPixel(pixelX, pixelY).r;

// Probability-based spawning with thresholdif (noiseValue >= minSpawnThreshold)
{
    float spawnProbability = (noiseValue - minSpawnThreshold) / (1.0f - minSpawnThreshold);

    if (Random.value < spawnProbability)
    {
        // Apply jittering for natural distribution
        float maxJitter = stepSize * maxJitterPercentage;
        float offsetX = Random.Range(-maxJitter, maxJitter);
        float offsetZ = Random.Range(-maxJitter, maxJitter);

        Vector3 spawnPositionXZ = new Vector3(
            gridX + offsetX,
            0,
            gridZ + offsetZ
        );

        SpawnFlower(spawnPositionXZ);
    }
}
Custom Editor Integration(csharp)
[CustomEditor(typeof(NoiseGenerator))]
public class NoiseGeneratorEditor : Editor
{
    public override void OnInspectorGUI()
    {
        // Real-time noise preview
        if (generator.noiseTexture != null)
        {
            float previewSize = EditorGUIUtility.currentViewWidth - 40;
            Rect rect = GUILayoutUtility.GetRect(previewSize, previewSize);
            EditorGUI.DrawPreviewTexture(rect, generator.noiseTexture);
        }

        // Auto-refresh on parameter change
        if (GUI.changed)
        {
            generator.GenerateNoiseTexture();
            EditorUtility.SetDirty(generator);
            Repaint();
        }
    }
}

3. Key Features




Noise-Based Distribution
  • Multi-octave Perlin noise for organic patterns

  • Adjustable scale, persistence, and lacunarity parameters

  • Real-time noise texture preview in editor



Smart Spawning System
  • Threshold-based density control

  • Probability mapping from noise values

  • Position jittering to break grid regularity

  • Random Y-axis rotation for variation



Artist-Friendly Workflow
  • One-click generation and clearing

  • Visual noise preview

  • Seed randomization for quick iteration

  • Support for multiple prefab variants

ree

ree

4. Technical Highlights


  • Procedural Generation: Eliminates manual placement of hundreds of objects

  • Performance: Grid-based sampling with configurable resolution

  • Flexibility: Works with any prefab type, not limited to flowers

  • Reusability: Can be adapted for grass, rocks, trees, or any scattered elements


Project Gallery

bottom of page