Unity 熱力圖Heatmap生成

使用Unity 熱力圖Heatmap生成教程

原理

圈一個熱力圖範圍,等分成一個個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();
        }
    } 

}
···

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章