using UnityEngine;
namespace Synaptic.Water
{
///
/// Creates an infinite ocean plane that follows the camera
///
[ExecuteAlways]
public class OceanSystem : MonoBehaviour
{
[Header("Ocean Settings")]
public Material oceanMaterial;
public int gridSize = 128;
public float tileSize = 100f;
public int tilesAroundCamera = 3;
[Header("LOD Settings")]
public bool useLOD = true;
public float lodDistance = 200f;
public int lodLevels = 3;
[Header("Camera")]
public Transform followCamera;
private MeshFilter meshFilter;
private MeshRenderer meshRenderer;
private Mesh oceanMesh;
private Vector3 lastCameraPosition;
void Start()
{
if (followCamera == null)
followCamera = Camera.main?.transform;
CreateOceanMesh();
}
void Update()
{
if (followCamera == null) return;
// Snap to grid position following camera
Vector3 camPos = followCamera.position;
float snapX = Mathf.Floor(camPos.x / tileSize) * tileSize;
float snapZ = Mathf.Floor(camPos.z / tileSize) * tileSize;
transform.position = new Vector3(snapX, transform.position.y, snapZ);
}
void CreateOceanMesh()
{
meshFilter = GetComponent();
if (meshFilter == null)
meshFilter = gameObject.AddComponent();
meshRenderer = GetComponent();
if (meshRenderer == null)
meshRenderer = gameObject.AddComponent();
oceanMesh = GenerateOceanMesh(gridSize, tileSize * tilesAroundCamera * 2);
meshFilter.sharedMesh = oceanMesh;
if (oceanMaterial != null)
meshRenderer.sharedMaterial = oceanMaterial;
}
Mesh GenerateOceanMesh(int resolution, float size)
{
Mesh mesh = new Mesh();
mesh.name = "OceanMesh";
mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
int vertCount = (resolution + 1) * (resolution + 1);
Vector3[] vertices = new Vector3[vertCount];
Vector2[] uvs = new Vector2[vertCount];
Vector3[] normals = new Vector3[vertCount];
float halfSize = size * 0.5f;
float step = size / resolution;
for (int z = 0; z <= resolution; z++)
{
for (int x = 0; x <= resolution; x++)
{
int i = z * (resolution + 1) + x;
float xPos = x * step - halfSize;
float zPos = z * step - halfSize;
vertices[i] = new Vector3(xPos, 0, zPos);
uvs[i] = new Vector2((float)x / resolution, (float)z / resolution);
normals[i] = Vector3.up;
}
}
int[] triangles = new int[resolution * resolution * 6];
int t = 0;
for (int z = 0; z < resolution; z++)
{
for (int x = 0; x < resolution; x++)
{
int i = z * (resolution + 1) + x;
triangles[t++] = i;
triangles[t++] = i + resolution + 1;
triangles[t++] = i + 1;
triangles[t++] = i + 1;
triangles[t++] = i + resolution + 1;
triangles[t++] = i + resolution + 2;
}
}
mesh.vertices = vertices;
mesh.uv = uvs;
mesh.normals = normals;
mesh.triangles = triangles;
mesh.RecalculateBounds();
return mesh;
}
///
/// Get wave height at world position (for buoyancy)
///
public float GetWaveHeight(Vector3 worldPos)
{
if (oceanMaterial == null) return transform.position.y;
float time = Time.time * oceanMaterial.GetFloat("_WaveSpeed");
float oceanScale = oceanMaterial.HasProperty("_OceanScale") ? oceanMaterial.GetFloat("_OceanScale") : 1f;
float waveHeight = oceanMaterial.HasProperty("_WaveHeight") ? oceanMaterial.GetFloat("_WaveHeight") : 1f;
Vector3 scaledPos = worldPos * oceanScale;
float height = transform.position.y;
// Sample waves A through H
height += SampleGerstnerWave(oceanMaterial.GetVector("_WaveA"), scaledPos, time * 0.8f) * waveHeight;
height += SampleGerstnerWave(oceanMaterial.GetVector("_WaveB"), scaledPos, time * 0.9f) * waveHeight;
height += SampleGerstnerWave(oceanMaterial.GetVector("_WaveC"), scaledPos, time) * waveHeight;
height += SampleGerstnerWave(oceanMaterial.GetVector("_WaveD"), scaledPos, time * 1.1f) * waveHeight;
if (oceanMaterial.HasProperty("_WaveE"))
{
height += SampleGerstnerWave(oceanMaterial.GetVector("_WaveE"), scaledPos, time * 1.2f) * waveHeight;
height += SampleGerstnerWave(oceanMaterial.GetVector("_WaveF"), scaledPos, time * 1.4f) * waveHeight;
height += SampleGerstnerWave(oceanMaterial.GetVector("_WaveG"), scaledPos, time * 1.6f) * waveHeight;
height += SampleGerstnerWave(oceanMaterial.GetVector("_WaveH"), scaledPos, time * 1.8f) * waveHeight;
}
return height;
}
float SampleGerstnerWave(Vector4 wave, Vector3 pos, float time)
{
float steepness = wave.z;
float wavelength = wave.w;
if (wavelength <= 0) return 0;
float k = 2f * Mathf.PI / wavelength;
float c = Mathf.Sqrt(9.8f / k);
Vector2 d = new Vector2(wave.x, wave.y).normalized;
float f = k * (Vector2.Dot(d, new Vector2(pos.x, pos.z)) - c * time);
float a = steepness / k;
return a * Mathf.Sin(f);
}
///
/// Get wave normal at world position
///
public Vector3 GetWaveNormal(Vector3 worldPos)
{
float delta = 0.1f;
float h = GetWaveHeight(worldPos);
float hX = GetWaveHeight(worldPos + Vector3.right * delta);
float hZ = GetWaveHeight(worldPos + Vector3.forward * delta);
Vector3 tangentX = new Vector3(delta, hX - h, 0).normalized;
Vector3 tangentZ = new Vector3(0, hZ - h, delta).normalized;
return Vector3.Cross(tangentZ, tangentX).normalized;
}
void OnValidate()
{
if (Application.isPlaying && oceanMesh != null)
{
CreateOceanMesh();
}
}
}
}