381 lines
16 KiB
C#
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/<layer>.
|
|
/// </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;
|
|
}
|
|
}
|
|
}
|