Skip to content

Instantly share code, notes, and snippets.

@FleshMobProductions
Last active September 29, 2020 19:58
Show Gist options
  • Save FleshMobProductions/d43425fd47ba7005c45ebac3953b66d8 to your computer and use it in GitHub Desktop.
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.
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;
}
}
}
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();
}
}
}
}
}
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;
}
}
}
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);
}
}
}
}
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);
}
}
}
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