using System.Collections.Generic; using UnityEditor; using UnityEngine; using Ashwild.Harvesting; namespace Ashwild.EditorTools { /// /// 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>. /// 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(); 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 result = new List(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(); } } /// Hash spatial léger pour les tests d'espacement. private class SpatialGrid { private readonly float cell; private readonly Dictionary> cells = new Dictionary>(); 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 list)) { list = new List(); 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 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; } } }