Last active
September 29, 2020 19:58
-
-
Save FleshMobProductions/d43425fd47ba7005c45ebac3953b66d8 to your computer and use it in GitHub Desktop.
Example of Weighted Random point distribution within a value range of Perlin Noise Maps in Unity.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
namespace FMPUtils.Randomness | |
{ | |
[System.Serializable] | |
public class PerlinNoiseMap | |
{ | |
[SerializeField] private Vector2Int originOffset; | |
[SerializeField] private Vector2Int mapSize; | |
[SerializeField] private float cellSampleSizeMultiplier; | |
[SerializeField] private bool use0to1BaseRange; | |
public Vector2Int OriginOffset | |
{ | |
get => originOffset; | |
set => originOffset = value; | |
} | |
public Vector2Int MapSize | |
{ | |
get => mapSize; | |
set => mapSize = value; | |
} | |
public float CellSampleSizeMultiplier | |
{ | |
get => cellSampleSizeMultiplier; | |
set => cellSampleSizeMultiplier = value; | |
} | |
public float[,] GenerateMap() | |
{ | |
return GenerateMap(originOffset, mapSize, cellSampleSizeMultiplier, use0to1BaseRange); | |
} | |
public static float[,] GenerateMap(Vector2Int originOffset, Vector2Int mapSize, float cellSampleSizeMultiplier, bool use0to1BaseRange = false) | |
{ | |
float[,] valueMap = new float[mapSize.x, mapSize.y]; | |
float xFraction = 1f / mapSize.x; | |
float yFraction = 1f / mapSize.y; | |
for (int x = 0; x < mapSize.x; x++) | |
{ | |
for (int y = 0; y < mapSize.y; y++) | |
{ | |
if (!use0to1BaseRange) | |
{ | |
valueMap[x, y] = Mathf.PerlinNoise(originOffset.x + x * cellSampleSizeMultiplier, originOffset.y + y * cellSampleSizeMultiplier); | |
} | |
else | |
{ | |
valueMap[x, y] = Mathf.PerlinNoise(originOffset.x + x * xFraction * cellSampleSizeMultiplier, originOffset.y + y * yFraction * cellSampleSizeMultiplier); | |
} | |
} | |
} | |
return valueMap; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEditor; | |
namespace FMPUtils.Randomness | |
{ | |
// Belongs into an "Editor" folder. Used for visualizing the noise map of a WeightedRandomMapActionBehaviour. | |
public class PerlinNoiseMapVisualizerEditor : EditorWindow | |
{ | |
private WeightedRandomMapActionBehaviour mapObject; | |
private Texture2D previewTexture; | |
private bool fitHeight; | |
[MenuItem("FMPUtils/Perlin Noise Visualiizer")] | |
public static void OpenWindow() | |
{ | |
GetWindow<PerlinNoiseMapVisualizerEditor>(); | |
} | |
private void OnGUI() | |
{ | |
EditorGUILayout.LabelField("Perlin Noise Map Visualizer", EditorStyles.boldLabel); | |
EditorGUILayout.Space(); | |
var mapObjectNew = (WeightedRandomMapActionBehaviour) EditorGUILayout.ObjectField(mapObject, typeof(WeightedRandomMapActionBehaviour), true); | |
if (mapObject != mapObjectNew) | |
{ | |
mapObject = mapObjectNew; | |
UpdateTexture(); | |
} | |
if (previewTexture != null) | |
{ | |
EditorGUILayout.Space(); | |
if (GUILayout.Button("Update Texture Data")) | |
{ | |
UpdateTexture(); | |
} | |
EditorGUILayout.Space(); | |
if (GUILayout.Button("Save Texture")) | |
{ | |
byte[] pngBytes = previewTexture.EncodeToPNG(); | |
string savePath = EditorUtility.SaveFilePanel("PerlinNoiseTexture", Application.streamingAssetsPath, "PerlinNoiseTexture", "png"); | |
System.IO.File.WriteAllBytes(savePath, pngBytes); | |
} | |
EditorGUILayout.Space(); | |
EditorGUILayout.LabelField(new GUIContent("Preview:"), EditorStyles.boldLabel); | |
fitHeight = EditorGUILayout.Toggle("Fit Texture Height", fitHeight); | |
EditorGUILayout.Space(); | |
int xPadding = 15; | |
int previewTexDefaultSize = 250; | |
int previewPixels = Mathf.Min(previewTexDefaultSize, (int) position.width - xPadding); | |
float aspect = (float) previewTexture.width / previewTexture.height; | |
int previewTexWidth = fitHeight ? Mathf.RoundToInt(previewPixels * aspect) : previewPixels; | |
int previewTexHeight = fitHeight ? previewPixels : Mathf.RoundToInt(previewPixels / aspect); | |
Rect lastRect = GUILayoutUtility.GetLastRect(); | |
EditorGUI.DrawPreviewTexture(new Rect(xPadding, lastRect.yMax + 10, previewTexWidth, previewTexHeight), previewTexture); | |
} | |
} | |
private void UpdateTexture() | |
{ | |
if (mapObject != null) | |
{ | |
var noiseMap = mapObject.DistributionDetails?.RandomnessMap; | |
if (noiseMap != null) | |
{ | |
if (previewTexture != null) | |
{ | |
if (Application.isPlaying) | |
Destroy(previewTexture); | |
else | |
DestroyImmediate(previewTexture); | |
} | |
float[,] noiseValues = noiseMap.GenerateMap(); | |
int xLength = noiseValues.GetLength(0); | |
int yLength = noiseValues.GetLength(1); | |
previewTexture = new Texture2D(xLength, yLength); | |
Color[] colors = new Color[xLength * yLength]; | |
for (int y = 0; y < yLength; y++) | |
{ | |
for (int x = 0; x < xLength; x++) | |
{ | |
float noiseVal = noiseValues[x, y]; | |
colors[y * xLength + x] = new Color(noiseVal, noiseVal, noiseVal); | |
} | |
} | |
previewTexture.SetPixels(colors); | |
previewTexture.Apply(); | |
} | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using UnityEngine; | |
namespace FMPUtils.Randomness | |
{ | |
[System.Serializable] | |
public struct WeightedElementStruct<T> | |
{ | |
public T value; | |
[Range(0, 100)] | |
public int weight; | |
} | |
public static class RandomnessExtensions | |
{ | |
public static T GetRandomValueWeighted<T>(this List<WeightedElementStruct<T>> weightedValues) | |
{ | |
int weightSum = 0; | |
int count = weightedValues.Count; | |
for (int i = 0; i < count; i++) | |
{ | |
weightSum += weightedValues[i].weight; | |
} | |
int randomVal = UnityEngine.Random.Range(0, weightSum); | |
int weightSumTemp = 0; | |
for (int i = 0; i < count; i++) | |
{ | |
weightSumTemp += weightedValues[i].weight; | |
if (randomVal < weightSumTemp) | |
{ | |
return weightedValues[i].value; | |
} | |
} | |
return weightedValues[count - 1].value; | |
} | |
public static T GetRandomValueWeighted<T>(this List<WeightedElementStruct<T>> weightedValues, System.Random random) | |
{ | |
int weightSum = 0; | |
int count = weightedValues.Count; | |
for (int i = 0; i < count; i++) | |
{ | |
weightSum += weightedValues[i].weight; | |
} | |
int randomVal = random.Next(weightSum); | |
int weightSumTemp = 0; | |
for (int i = 0; i < count; i++) | |
{ | |
weightSumTemp += weightedValues[i].weight; | |
if (randomVal < weightSumTemp) | |
{ | |
return weightedValues[i].value; | |
} | |
} | |
return weightedValues[count - 1].value; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
namespace FMPUtils.Randomness | |
{ | |
public class SpawnPrefabOnSurface : MonoBehaviour | |
{ | |
[SerializeField] private GameObject prefab; | |
[SerializeField] private float raycastYOrigin; | |
[SerializeField] private float ySpawnPosDefault; | |
[SerializeField] private LayerMask groundLayers; | |
[SerializeField] private float raycastDistance = 1000; | |
[SerializeField] private bool useRandomLocalYRot; | |
[SerializeField] private bool alignToSurfaceNormal; | |
public void SpawnPrefabAtPosition(float x, float z) | |
{ | |
Vector3 origin = new Vector3(x, raycastYOrigin, z); | |
GameObject instance; | |
if (Physics.Raycast(origin, -Vector3.up, out RaycastHit hit, raycastDistance, groundLayers.value, QueryTriggerInteraction.Ignore)) | |
{ | |
Quaternion rotation = !alignToSurfaceNormal ? Quaternion.identity : Quaternion.FromToRotation(Vector3.up, hit.normal); | |
instance = Instantiate(prefab, hit.point, rotation); | |
} | |
else | |
{ | |
instance = Instantiate(prefab, new Vector3(x, ySpawnPosDefault, z), Quaternion.identity); | |
} | |
if (useRandomLocalYRot) | |
{ | |
float randomYAngle = UnityEngine.Random.Range(0f, 360f); | |
instance.transform.rotation *= Quaternion.AngleAxis(randomYAngle, instance.transform.up); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.Events; | |
namespace FMPUtils.Randomness | |
{ | |
[System.Serializable] | |
public class Position2DUnityAction : UnityEvent<float, float> | |
{ | |
} | |
[System.Serializable] | |
public class WeightedRandomMapAction | |
{ | |
private const int maxIterationsPerSample = 1000; | |
private static List<WeightedElementStruct<Vector2Int>> tempWeightValues = new List<WeightedElementStruct<Vector2Int>>(1000); | |
[Tooltip("If true, will use 'randomnessSeed' value as Random instance seed, otherwise a random value is used")] | |
[SerializeField] private bool useCustomSeed; | |
[SerializeField] private int randomnessSeed; | |
[Tooltip("If true, map size extends in approx. equal length to all sides. If false, the origin point is the bottom left map point and map extends to the right and up")] | |
[SerializeField] private bool centerGrid; | |
[Range(0f, 1f)] | |
[SerializeField] private float noiseValMin; | |
[Range(0f, 1f)] | |
[SerializeField] private float noiseValMax = 1f; | |
[Range(1, 1000)] | |
[SerializeField] private int desiredExecutionCount = 1; | |
[Range(1, 100)] | |
[SerializeField] private int randomPointCountPerSample = 20; | |
[Range(0.01f, 10f)] | |
[SerializeField] private float mapUnitSize = 1f; | |
[SerializeField] private AnimationCurve minToMaxWeightDistribution; | |
[Tooltip("Used to add a slight variation to the found target position by offsetting it within a random interval around resultOffsetInterval")] | |
[SerializeField] private bool addResultPosRandomOffset; | |
[SerializeField] private Vector2 resultOffsetInterval; | |
[SerializeField] private PerlinNoiseMap randomnessMap; | |
public PerlinNoiseMap RandomnessMap | |
{ | |
get => randomnessMap; | |
set => randomnessMap = value; | |
} | |
public void RunAction(Position2DUnityAction action, float originX, float originY) | |
{ | |
if (action == null) return; | |
if (noiseValMin == noiseValMax) return; | |
if (noiseValMin > noiseValMax) | |
{ | |
float temp = noiseValMin; | |
noiseValMin = noiseValMax; | |
noiseValMax = temp; | |
} | |
float[,] noiseValues = randomnessMap.GenerateMap(); | |
int xLength = noiseValues.GetLength(0); | |
int yLength = noiseValues.GetLength(1); | |
int gridOffsetX = centerGrid ? -xLength / 2 : 0; | |
int gridOffsetY = centerGrid ? -yLength / 2 : 0; | |
System.Random rand = !useCustomSeed ? (new System.Random()) : (new System.Random(randomnessSeed)); | |
for (int i = 0; i < desiredExecutionCount; i++) | |
{ | |
tempWeightValues.Clear(); | |
int validRandomEntries = 0; | |
int iteration = 0; | |
while (validRandomEntries < randomPointCountPerSample && iteration < maxIterationsPerSample) | |
{ | |
iteration++; | |
int x = rand.Next(xLength); | |
int y = rand.Next(yLength); | |
float currentValue = noiseValues[x, y]; | |
int probability = GetProbablilty(currentValue); | |
if (probability > 0) | |
{ | |
validRandomEntries++; | |
tempWeightValues.Add(new WeightedElementStruct<Vector2Int> { value = new Vector2Int(gridOffsetX + x, gridOffsetY + y), weight = probability }); | |
} | |
} | |
if (tempWeightValues.Count == 0) | |
{ | |
Debug.LogError($"Could not find any valid noise value or probability for iteration {i}"); | |
continue; | |
} | |
Vector2Int randomMapGridPos = tempWeightValues.GetRandomValueWeighted(rand); | |
float xResult = randomMapGridPos.x * mapUnitSize; | |
float yResult = randomMapGridPos.y * mapUnitSize; | |
if (addResultPosRandomOffset) | |
{ | |
// Next double retrieves a value betweewn 0 and 1 | |
float xOffset = -resultOffsetInterval.x * 0.5f + ((float)rand.NextDouble()) * resultOffsetInterval.x; | |
xResult += xOffset; | |
float yOffset = -resultOffsetInterval.y * 0.5f + ((float)rand.NextDouble()) * resultOffsetInterval.y; | |
yResult += yOffset; | |
} | |
action.Invoke(xResult, yResult); | |
} | |
} | |
private int GetProbablilty(float noiseVal) | |
{ | |
if (noiseVal < noiseValMin || noiseVal > noiseValMax) return 0; | |
float sampleTime = Mathf.InverseLerp(noiseValMin, noiseValMax, noiseVal); | |
float probability = minToMaxWeightDistribution.Evaluate(sampleTime); | |
return Mathf.RoundToInt(probability * 100); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
namespace FMPUtils.Randomness | |
{ | |
// Usage example: Create a Scene with a large plane at position (0,0,0). | |
// Scale the x and z in a way to cover the selected Perlin Noise Map size. | |
// Create a GameObject and add a "WeightedRandomMapActionBehaviour" and a "SpawnPrefabOnSurface" component. | |
// Then assign the "SpawnPrefabOnSurface.SpawnPrefabAtPosition" method to the targetAction of | |
// "WeightedRandomMapActionBehaviour". | |
// The "PerlinNoiseMapVisualizerEditor" script goes into an "Editor" folder and can be used to preview perlin noise textures. | |
public class WeightedRandomMapActionBehaviour : MonoBehaviour | |
{ | |
[SerializeField] private bool runBehaviourInStart; | |
[SerializeField] private bool isMapCenter; | |
[SerializeField] private Position2DUnityAction targetAction; | |
[SerializeField] private WeightedRandomMapAction distributionDetails; | |
public WeightedRandomMapAction DistributionDetails | |
{ | |
get => distributionDetails; | |
set => distributionDetails = value; | |
} | |
private void Start() | |
{ | |
if (runBehaviourInStart) | |
{ | |
RunBehaviour(); | |
} | |
} | |
[ContextMenu("Run Behaviour")] | |
private void RunBehaviour() | |
{ | |
if (targetAction == null) | |
{ | |
Debug.LogError("Please assign a target action!"); | |
return; | |
} | |
int eventCount = targetAction.GetPersistentEventCount(); | |
if (eventCount <= 0) | |
{ | |
Debug.LogError("Target action has no referenced method targets!"); | |
return; | |
} | |
Vector3 mapCenter = isMapCenter ? transform.position : Vector3.zero; | |
distributionDetails.RunAction(targetAction, mapCenter.x, mapCenter.z); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment