Files
2026-06-22 16:18:34 +02:00

381 lines
16 KiB
C#

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using Ashwild.Harvesting;
namespace Ashwild.EditorTools
{
/// <summary>
/// Logique de placement pour les ScatterZone (auto + pinceau), 100 % Editor.
/// Borne le scatter à la boîte de la zone, respecte le masque d'exclusion, les contraintes
/// du profil (pente, texture, espacement) et parente les instances sous zone/&lt;layer&gt;.
/// </summary>
public static class ScatterRunner
{
// ---------- API publique ----------
public static int ScatterAll(ScatterZone zone)
{
if (zone == null) return 0;
int total = 0;
for (int i = 0; i < zone.layers.Count; i++)
total += ScatterLayer(zone, i);
return total;
}
public static int ScatterLayer(ScatterZone zone, int layerIndex)
{
if (!TryGetLayer(zone, layerIndex, out ScatterZone.Layer layer)) return 0;
if (!layer.enabled || !ValidateProfile(layer.profile)) return 0;
// Remplace : on vide d'abord les instances existantes de ce layer (pas d'accumulation).
ClearLayer(zone, layerIndex);
Transform container = GetOrCreateContainer(zone, layer);
var session = new Session(zone, layer.profile, container);
float cell = layer.profile.MinSpacing;
float targetPerCell = (layer.profile.Density / 100f) * cell * cell;
float fill = Mathf.Clamp01(targetPerCell);
Vector3 min = zone.MinCorner;
int total = 0;
foreach (Terrain terrain in Terrain.activeTerrains)
{
if (terrain == null) continue;
total += session.FillArea(terrain,
min.x, min.x + zone.size.x,
min.z, min.z + zone.size.z,
fill, null, 0f);
}
return total;
}
public static int PaintAdd(ScatterZone zone, int layerIndex, Vector3 center, float radius, float strength)
{
if (!TryGetLayer(zone, layerIndex, out ScatterZone.Layer layer)) return 0;
if (!layer.enabled || !ValidateProfile(layer.profile)) return 0;
Transform container = GetOrCreateContainer(zone, layer);
var session = new Session(zone, layer.profile, container);
int total = 0;
foreach (Terrain terrain in Terrain.activeTerrains)
{
if (terrain == null) continue;
total += session.FillArea(terrain,
center.x - radius, center.x + radius,
center.z - radius, center.z + radius,
Mathf.Clamp01(strength), center, radius);
}
return total;
}
public static int PaintDelete(ScatterZone zone, int layerIndex, Vector3 center, float radius)
{
if (!TryGetLayer(zone, layerIndex, out ScatterZone.Layer layer)) return 0;
Transform container = FindContainer(zone, layer);
if (container == null) return 0;
float r2 = radius * radius;
int removed = 0;
for (int i = container.childCount - 1; i >= 0; i--)
{
Transform child = container.GetChild(i);
Vector3 d = child.position - center;
d.y = 0f;
if (d.sqrMagnitude <= r2)
{
Undo.DestroyObjectImmediate(child.gameObject);
removed++;
}
}
return removed;
}
public static int ClearLayer(ScatterZone zone, int layerIndex)
{
if (!TryGetLayer(zone, layerIndex, out ScatterZone.Layer layer)) return 0;
Transform container = FindContainer(zone, layer);
if (container == null) return 0;
int removed = container.childCount;
for (int i = container.childCount - 1; i >= 0; i--)
Undo.DestroyObjectImmediate(container.GetChild(i).gameObject);
return removed;
}
public static int ClearAll(ScatterZone zone)
{
if (zone == null) return 0;
int total = 0;
for (int i = 0; i < zone.layers.Count; i++)
total += ClearLayer(zone, i);
return total;
}
// ---------- Session de placement ----------
private class Session
{
private readonly ScatterZone zone;
private readonly ScatterProfile profile;
private readonly Transform container;
private readonly System.Random rng;
private readonly SpatialGrid grid;
private readonly ScatterExclusionVolume[] volumes;
private int[] allowedIndices; // index des TerrainLayers autorisés sur le terrain courant
public Session(ScatterZone zone, ScatterProfile profile, Transform container)
{
this.zone = zone;
this.profile = profile;
this.container = container;
this.rng = new System.Random();
this.grid = new SpatialGrid(profile.MinSpacing);
this.volumes = zone.GetComponentsInChildren<ScatterExclusionVolume>();
for (int i = 0; i < container.childCount; i++)
grid.Add(container.GetChild(i).position);
}
public int FillArea(Terrain terrain, float xMin, float xMax, float zMin, float zMax,
float fillProbability, Vector3? circleCenter, float circleRadius)
{
if (fillProbability <= 0f) return 0;
TerrainData data = terrain.terrainData;
Vector3 tOrigin = terrain.transform.position;
// Intersection avec l'emprise du terrain.
xMin = Mathf.Max(xMin, tOrigin.x);
xMax = Mathf.Min(xMax, tOrigin.x + data.size.x);
zMin = Mathf.Max(zMin, tOrigin.z);
zMax = Mathf.Min(zMax, tOrigin.z + data.size.z);
if (xMax <= xMin || zMax <= zMin) return 0;
float cell = profile.MinSpacing;
float[,,] alphamaps = NeedsTextureFilter()
? data.GetAlphamaps(0, 0, data.alphamapWidth, data.alphamapHeight)
: null;
allowedIndices = alphamaps != null ? ResolveLayerIndices(data) : null;
float circleR2 = circleRadius * circleRadius;
int placed = 0;
for (float cz = zMin; cz < zMax; cz += cell)
{
for (float cx = xMin; cx < xMax; cx += cell)
{
if (rng.NextDouble() > fillProbability) continue;
float px = cx + (float)rng.NextDouble() * cell;
float pz = cz + (float)rng.NextDouble() * cell;
if (px > xMax || pz > zMax) continue;
if (circleCenter.HasValue)
{
float dx = px - circleCenter.Value.x;
float dz = pz - circleCenter.Value.z;
if (dx * dx + dz * dz > circleR2) continue;
}
if (TryPlace(terrain, data, tOrigin, alphamaps, px, pz))
placed++;
}
}
return placed;
}
private bool TryPlace(Terrain terrain, TerrainData data, Vector3 tOrigin,
float[,,] alphamaps, float px, float pz)
{
// Volumes d'exclusion (zones no-go).
if (IsInExcludedVolume(px, pz)) return false;
float u = Mathf.Clamp01((px - tOrigin.x) / data.size.x);
float v = Mathf.Clamp01((pz - tOrigin.z) / data.size.z);
Vector3 normal = data.GetInterpolatedNormal(u, v);
float slope = Vector3.Angle(normal, Vector3.up);
if (slope < profile.SlopeRange.x || slope > profile.SlopeRange.y) return false;
if (alphamaps != null && !PassesTexture(data, alphamaps, u, v)) return false;
float worldY = terrain.SampleHeight(new Vector3(px, 0f, pz)) + tOrigin.y;
worldY -= SinkOffset();
Vector3 pos = new Vector3(px, worldY, pz);
if (!grid.IsFarEnough(pos, profile.MinSpacing)) return false;
if (!profile.TryPickEntry(rng, out ScatterProfile.Entry entry)) return false;
GameObject instance = (GameObject)PrefabUtility.InstantiatePrefab(entry.prefab, container);
if (instance == null) return false;
Undo.RegisterCreatedObjectUndo(instance, "Scatter");
float yaw = (float)rng.NextDouble() * 360f;
Quaternion rot = Quaternion.Euler(0f, yaw, 0f);
if (entry.alignToNormal)
rot = Quaternion.FromToRotation(Vector3.up, normal) * rot;
instance.transform.SetPositionAndRotation(pos, rot);
instance.transform.localScale = Vector3.one * PickScale(entry.scaleRange);
SetLayerRecursively(instance, profile.PlacedLayer);
grid.Add(pos);
return true;
}
private bool IsInExcludedVolume(float px, float pz)
{
if (volumes == null || volumes.Length == 0) return false;
Vector3 p = new Vector3(px, zone.transform.position.y, pz);
for (int i = 0; i < volumes.Length; i++)
if (volumes[i] != null && volumes[i].ContainsXZ(p))
return true;
return false;
}
private float SinkOffset()
{
Vector2 r = profile.SinkRange;
if (r.x == 0f && r.y == 0f) return 0f;
float a = Mathf.Min(r.x, r.y);
float b = Mathf.Max(r.x, r.y);
return Mathf.Lerp(a, b, (float)rng.NextDouble());
}
private float PickScale(Vector2 range)
{
if (range.x <= 0f && range.y <= 0f) return 1f;
float a = Mathf.Min(range.x, range.y);
float b = Mathf.Max(range.x, range.y);
return Mathf.Lerp(a, b, (float)rng.NextDouble());
}
private bool NeedsTextureFilter()
=> profile.AllowedTerrainLayers != null && profile.AllowedTerrainLayers.Length > 0;
private bool PassesTexture(TerrainData data, float[,,] alphamaps, float u, float v)
{
if (allowedIndices == null || allowedIndices.Length == 0) return false;
int ax = Mathf.Clamp(Mathf.FloorToInt(u * data.alphamapWidth), 0, data.alphamapWidth - 1);
int ay = Mathf.Clamp(Mathf.FloorToInt(v * data.alphamapHeight), 0, data.alphamapHeight - 1);
int layers = alphamaps.GetLength(2);
for (int i = 0; i < allowedIndices.Length; i++)
{
int li = allowedIndices[i];
if (li < 0 || li >= layers) continue;
if (alphamaps[ay, ax, li] >= profile.MinTextureWeight) return true;
}
return false;
}
// Retrouve l'index de chaque TerrainLayer autorisé dans le terrain courant.
private int[] ResolveLayerIndices(TerrainData data)
{
TerrainLayer[] allowed = profile.AllowedTerrainLayers;
TerrainLayer[] terrainLayers = data.terrainLayers;
List<int> result = new List<int>(allowed.Length);
for (int i = 0; i < allowed.Length; i++)
{
if (allowed[i] == null) continue;
int idx = System.Array.IndexOf(terrainLayers, allowed[i]);
if (idx >= 0) result.Add(idx);
}
return result.ToArray();
}
}
/// <summary>Hash spatial léger pour les tests d'espacement.</summary>
private class SpatialGrid
{
private readonly float cell;
private readonly Dictionary<long, List<Vector3>> cells = new Dictionary<long, List<Vector3>>();
public SpatialGrid(float cellSize) { cell = Mathf.Max(0.01f, cellSize); }
private long Key(int x, int z) => ((long)x << 32) ^ (uint)z;
public void Add(Vector3 p)
{
long k = Key(Mathf.FloorToInt(p.x / cell), Mathf.FloorToInt(p.z / cell));
if (!cells.TryGetValue(k, out List<Vector3> list))
{
list = new List<Vector3>();
cells[k] = list;
}
list.Add(p);
}
public bool IsFarEnough(Vector3 p, float minDist)
{
float min2 = minDist * minDist;
int cx = Mathf.FloorToInt(p.x / cell);
int cz = Mathf.FloorToInt(p.z / cell);
for (int dz = -1; dz <= 1; dz++)
for (int dx = -1; dx <= 1; dx++)
{
if (!cells.TryGetValue(Key(cx + dx, cz + dz), out List<Vector3> list)) continue;
for (int i = 0; i < list.Count; i++)
{
float ddx = list[i].x - p.x;
float ddz = list[i].z - p.z;
if (ddx * ddx + ddz * ddz < min2) return false;
}
}
return true;
}
}
// ---------- Conteneurs & utilitaires ----------
private static string ContainerName(ScatterZone zone, ScatterZone.Layer layer)
=> layer.profile != null ? layer.profile.name : "Layer";
private static Transform GetOrCreateContainer(ScatterZone zone, ScatterZone.Layer layer)
{
string name = ContainerName(zone, layer);
Transform existing = zone.transform.Find(name);
if (existing != null) return existing;
GameObject go = new GameObject(name);
Undo.RegisterCreatedObjectUndo(go, "Create Scatter Container");
Undo.SetTransformParent(go.transform, zone.transform, "Parent Scatter Container");
go.transform.localPosition = Vector3.zero;
return go.transform;
}
private static Transform FindContainer(ScatterZone zone, ScatterZone.Layer layer)
=> zone.transform.Find(ContainerName(zone, layer));
private static void SetLayerRecursively(GameObject go, int layer)
{
go.layer = layer;
foreach (Transform c in go.transform)
SetLayerRecursively(c.gameObject, layer);
}
private static bool TryGetLayer(ScatterZone zone, int index, out ScatterZone.Layer layer)
{
layer = null;
if (zone == null || index < 0 || index >= zone.layers.Count) return false;
layer = zone.layers[index];
return layer != null;
}
private static bool ValidateProfile(ScatterProfile profile)
{
if (profile == null) return false;
if (profile.Entries == null || profile.Entries.Length == 0) return false;
if (Terrain.activeTerrains == null || Terrain.activeTerrains.Length == 0)
{
Debug.LogWarning("[Scatter] Aucun terrain actif dans la scène.");
return false;
}
return true;
}
}
}