
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

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.

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

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.



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

Surface foam:
Use depth fade to remove foam farther from the shoreline.

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.


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.

Core Components:
1. FloatingObjectController.cs
Purpose: Bridge between GPU water simulation and CPU game objects
How It Works:
GPU → CPU Transfer: Periodically copies water height data from GPU RenderTexture to CPU Texture2D
Height Query: Provides a public method for objects to check water height at any world position
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:
Query: Each frame, asks FloatingObjectController for water height at its position
Smooth Motion: Uses SmoothDamp to gradually move toward target height (avoids jittering)
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

Key Design Decisions
Periodic Readback: GPU→CPU transfer is expensive, so we do it every 0.3s instead of every frame
SmoothDamp: Prevents jittery motion by gradually moving to target height with velocity tracking
UV Mapping: Converts 3D world positions to 2D texture coordinates for height lookup
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
Attach FloatingObjectController to a manager object
Assign water's RenderTexture and Renderer
Attach WaterPlant to each plant prefab
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.


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


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


