原理
圈一個熱力圖範圍,等分成一個個cell,對其中的熱力點weight進行計算(每個熱力點有不同的weight),處理方法是以熱力點爲中心向四周衰減式輻射,更新輻射範圍的內的cell的weight值,最後把所有cell的weight信息存到texture中,傳到shader中處理等到最後的效果。
效果圖
C#代碼
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UniRx;
using UnityEngine;
namespace ZYF
{
/// <summary>
/// 熱力圖
/// </summary>
public class ZYF_HeatMap2 : MonoBehaviour
{
/// <summary>
/// 棋盤單元格
/// </summary>
[System.Serializable]
public class CheckboardCell
{
/// <summary>
/// 單元格中心座標
/// </summary>
public Vector3 centerWPos = Vector3.zero;
/// <summary>
/// 權值
/// </summary>
public float weight01;
private int row;
private int col;
ZYF_HeatMap2 manager;
public CheckboardCell(ZYF_HeatMap2 manager, int row, int col)
{
this.row = row;
this.col = col;
weight01 = 0;
this.manager = manager;
centerWPos = this.manager.GetWPos(row: row, col: col);
}
/// <summary>
/// 清空權值
/// </summary>
private void ClearWeightEventHandler()
{
weight01 = 0;
}
public override string ToString()
{
return $"cell{row}:{col},";
}
/// <summary>
/// 更新weight
/// </summary>
/// <param name="newWeight"></param>
internal void UpdateWeight(float newWeight)
{
//weight01 = (weight01 + newWeight) / 2;
if (weight01 < newWeight)
{
weight01 = (newWeight + weight01) / 2;
}
}
}
/// <summary>
/// 熱力標記點
/// </summary>
[System.Serializable]
public class SignPoint
{
[Header("熱力點名稱")]
public string PointName = "";
/// <summary>
/// 最大權值不是cell的0-1的weight哦!!!
/// </summary>
public static float maxWeight = 0;
[Header("輻射衰減值")]
public float weightDamping = 0.5f;
//public Transform signTran;
public Vector3 position;
public float weight;
[Header("所在行")]
public int row;
[Header("所在列")]
public int col;
[Header("輻射最遠cell數")]
public int maxRadiationCellLength;
[Header("輻射cell範圍")]
public Vector4 radiationRange;
public GameObject signTran;
public SignPoint(string pointName, Vector3 wpos, float weight, float weightDamping)
{
this.position = wpos;
this.weight = weight;
this.weightDamping = weightDamping;
this.PointName = pointName;
}
/// <summary>
/// 根據與本熱力點的距離算出weight
/// </summary>
/// <param name="dis"></param>
/// <returns></returns>
public float GetWeight01(float dis)
{
if (maxWeight <= 0.000001f)
{
return 0;
}
return Mathf.Max(0, weight - weightDamping * dis) / maxWeight;
}
/// <summary>
/// 輻射的最遠距離
/// </summary>
/// <returns></returns>
public float GetMaxRadiationDis()
{
return weight / weightDamping;
}
}
[Header("cells :行/列/間隔")]
public Vector3 RCU = Vector3.one;
public CheckboardCell[,] cells;
public int Rows => (int)RCU.x;
public int Cols => (int)RCU.y;
[SerializeField, Header("熱力標記點")]
private List<SignPoint> points;
[Header("熱力圖顯示板")]
public MeshRenderer heatMapPlan;
private bool isInit = false;
Texture2D heatMaptex;
/// <summary>
/// 熱力圖生成結果
/// </summary>
public Texture2D HeatMaptex { get => heatMaptex; private set => heatMaptex = value; }
public Vector3 beginPosition;
/// <summary>
/// 初始化地圖
/// </summary>
public void InitMap()
{
beginPosition = transform.position;
CollectMsgShader();
}
/// <summary>
/// 收集數據生成weight圖更新到shader
/// </summary>
private void CollectMsgShader()
{
var cellCount = Rows * Cols;
cells = new CheckboardCell[Rows, Cols];
//UpdateMaxWeight();
//EnterHeatPoints(points: points);
//UpdateMat();
Observable.Start(() =>
{
for (int row = 0; row < Rows; row++)
{
for (int col = 0; col < Cols; col++)
{
cells[row, col] = new CheckboardCell(this, row: row, col: col);
}
}
UpdateMaxWeight();
EnterHeatPoints(points: points);
return 666;
})
.ObserveOnMainThread()
.Subscribe(
result =>
{
if (result == 666)
{
UpdateMat();
}
}
).AddTo(gameObject);
}
private void OnDestroy()
{
if (HeatMaptex != null)
{
DestroyImmediate(HeatMaptex);
}
}
/// <summary>
/// 更新材質球數據
/// </summary>
protected void UpdateMat()
{
Material mat;
mat = heatMapPlan.material;
if (HeatMaptex != null)
{
DestroyImmediate(HeatMaptex);
}
HeatMaptex = new Texture2D(width: Cols, height: Rows);
for (int row = Rows - 1; row >= 0; row--)
{
for (int col = Cols - 1; col >= 0; col--)
{
var cell = cells[row, col];
HeatMaptex.SetPixel(x: col, y: row, color: new Color(cell.weight01, 0, 0, 0));
}
}
HeatMaptex.Apply();
//weight texture
mat.SetTexture("_CellInfoTex", HeatMaptex);
//記錄左下角開始座標
mat.SetVector("_BeginPos", new Vector2(beginPosition.x, beginPosition.z));
//cellSize
mat.SetVector("_MapInfo", RCU);
}
/// <summary>
/// 一維座標
/// </summary>
/// <param name="row"></param>
/// <param name="col"></param>
/// <returns></returns>
private int GetIndex(int row, int col)
{
return row * Cols + col;
}
/// <summary>
/// 更新最大weight
/// </summary>
private void UpdateMaxWeight()
{
SignPoint.maxWeight = points.Max(data => data.weight);
}
/// <summary>
/// 進入新的熱力點
/// </summary>
/// <param name="points"></param>
private void EnterHeatPoints(List<SignPoint> points)
{
//由每個熱力點輻射四周覆蓋值小的cell
foreach (var point in points)
{
var maxRadiation = point.GetMaxRadiationDis();
//當前座標
int pcol = GetCol(wPos: point.position);
int prow = GetRow(wPos: point.position);
point.col = pcol;
point.row = prow;
// 橫向縱向輻射cell距離
int maxRadiationCellLength = GetMaxRadiationCellLength(maxRadiation: maxRadiation);
int rMinRow = Mathf.Max(1, prow - maxRadiationCellLength);
int rMinCol = Mathf.Max(1, pcol - maxRadiationCellLength);
int rMaxRow = Mathf.Min(Rows - 2, prow + maxRadiationCellLength);
int rMaxCol = Mathf.Min(Cols - 2, pcol + maxRadiationCellLength);
point.maxRadiationCellLength = maxRadiationCellLength;
point.radiationRange = new Vector4(rMinRow, rMinCol, rMaxRow, rMaxCol);
//只計算輻射範圍內的cell
for (int row = rMinRow; row <= rMaxRow; row++)
{
for (int col = rMinCol; col <= rMaxCol; col++)
{
Vector3 rcpos = GetWPos(row: row, col: col);
float dis = Vector3.Distance(rcpos, point.position);
var cell = cells[row, col];
cell.UpdateWeight(newWeight: point.GetWeight01(dis));
}
}
}
}
/// <summary>
/// 添加新的熱力點
/// </summary>
/// <param name="newPoints"></param>
/// <param name="isNeedClear">添加前是否清空其他熱力點</param>
public void AddNewHeatPoints(List<SignPoint> newPoints, bool isNeedClear = false)
{
if (isNeedClear == true)
{
points.Clear();
}
points.AddRange(newPoints);
//運行時添加需要重新成weight圖給shader
if (Application.isPlaying == true)
{
InitMap();
}
}
/// <summary>
/// 輻射cell跨越數
/// </summary>
/// <param name="maxRadiation"></param>
/// <returns></returns>
private int GetMaxRadiationCellLength(float maxRadiation)
{
return GetRow(beginPosition + Vector3.forward * maxRadiation);
}
/// <summary>
/// 獲取對應世界座標的行
/// </summary>
/// <param name="wPos"></param>
/// <returns></returns>
private int GetRow(Vector3 wPos)
{
var offsetx = Math.Max(0, wPos.z - beginPosition.z);
return (int)(offsetx / RCU.z);
}
/// <summary>
/// 根據世界座標獲得所在列
/// </summary>
/// <param name="wPos"></param>
/// <returns></returns>
private int GetCol(Vector3 wPos)
{
var offsetx = Math.Max(0, wPos.x - beginPosition.x);
return (int)(offsetx / RCU.z);
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
if (Application.isPlaying == true && cells.Length > 0)
{
if (cells.Length <= 10000)
{
//每一列
for (int col = 0; col < Cols; col++)
{
Gizmos.DrawLine(cells[0, col] == null ? Vector3.zero : cells[0, col].centerWPos, cells[Rows - 1, col] == null ? Vector3.zero : cells[Rows - 1, col].centerWPos);
}
//每一行
for (int row = 0; row < Rows; row++)
{
Gizmos.DrawLine(cells[row, 0] == null ? Vector3.zero : cells[row, 0].centerWPos, cells[row, Cols - 1] == null ? Vector3.zero : cells[row, Cols - 1].centerWPos);
}
}
else
{
Vector3 bl = cells[0, 0] == null ? Vector3.zero : cells[0, 0].centerWPos;
Vector3 br = cells[0, Cols - 1] == null ? Vector3.zero : cells[0, Cols - 1].centerWPos;
Vector3 tl = cells[Rows - 1, 0] == null ? Vector3.zero : cells[Rows - 1, 0].centerWPos;
Vector3 tr = cells[Rows - 1, Cols - 1] == null ? Vector3.zero : cells[Rows - 1, Cols - 1].centerWPos;
Gizmos.DrawLine(bl, br);
Gizmos.DrawLine(bl, tl);
Gizmos.DrawLine(tl, tr);
Gizmos.DrawLine(tr, br);
}
//foreach (var cell in cells)
//{
// Gizmos.color = cell.weight01 * Color.red;
// Gizmos.DrawSphere(cell.centerWPos, RCU.z);
//}
}
}
#endif
/// <summary>
/// 獲取世界座標
/// </summary>
/// <param name="row"></param>
/// <param name="col"></param>
/// <returns></returns>
private Vector3 GetWPos(int row, int col)
{
return beginPosition + Vector3.right * col * RCU.z + Vector3.forward * row * RCU.z;
}
}
}
Shader
測試腳本
using System;
using System.Collections;
using System.Collections.Generic;
using UniRx;
using UnityEngine;
namespace ZYF
{
/// <summary>
/// 生成測試熱力點
/// </summary>
[ExecuteInEditMode]
public class ZYF_Test_HeatMap2PointsAdd : MonoBehaviour {
public ZYF_HeatMap2 map;
public bool isExecute = false;
[Header("生成測試熱力點座標範圍標記物體0")]
public Transform spawnRangeTran_0;
[Header("生成測試熱力點座標範圍標記物體1")]
public Transform spawnRangeTran_1;
[Header("當前已經生成的熱力點")]
public List<ZYF_HeatMap2.SignPoint> spawnedPoints = new List<ZYF_HeatMap2.SignPoint>();
public int spawnNum = 10;
public KeyCode testKeycode = KeyCode.Space;
private void Start()
{
if (Application.isPlaying == true)
{
map.InitMap();
//改變Points中的point/Position後需要手動刷新
Observable.EveryUpdate()
.Where(_ => Input.GetKeyDown(testKeycode))
.Subscribe(_=>map.InitMap())
.AddTo(gameObject);
}
}
private void Update()
{
if (Application.isPlaying == false)
{
if (isExecute == true)
{
isExecute = false;
ClearSpawnedPoints();
float minx = Mathf.Min(spawnRangeTran_0.position.x,spawnRangeTran_1.position.x);
float minz = Mathf.Min(spawnRangeTran_0.position.z,spawnRangeTran_1.position.z);
float maxx = Mathf.Max(spawnRangeTran_0.position.x,spawnRangeTran_1.position.x);
float maxz = Mathf.Max(spawnRangeTran_0.position.z,spawnRangeTran_1.position.z);
for (int i = 0; i < spawnNum; i++)
{
string pname = $"point_{i}";
GameObject p = GameObject.CreatePrimitive( PrimitiveType.Cube);
//GameObject p = new GameObject();
p.name = pname;
p.transform.SetParent(transform);
Vector3 pos = new Vector3(UnityEngine.Random.Range(minx,maxx),transform.position.y,UnityEngine.Random.Range(minz,maxz));
p.transform.position = pos;
var point = new ZYF_HeatMap2.SignPoint(pname, pos, UnityEngine.Random.Range(10, 100), UnityEngine.Random.Range(10, 20))
{
signTran = p
};
spawnedPoints.Add(point);
}
//添加熱力點
map.AddNewHeatPoints(spawnedPoints,true);
}
}
}
/// <summary>
/// 把之前生成的熱力點從map中移除並且刪除掉
/// </summary>
private void ClearSpawnedPoints()
{
while (spawnedPoints.Count >0)
{
var point = spawnedPoints[0];
var go = point.signTran;
DestroyImmediate(go);
spawnedPoints.Remove(point);
}
spawnedPoints.Clear();
}
}
}
···