【Unity笔记】基于距离驱动的参数映射器 InverseDistanceMapper 设计与实现
引言
InverseDistanceMapper 是一个基于逆距离加权(Inverse Distance Weighting, IDW)算法的参数映射器,用于 Unity 项目中根据物体间距离动态调整参数值(如着色器强度、音频音量、动画混合或粒子效果)。它通过计算目标点与多个影响点(Influencer Points)的距离权重,实现平滑的参数插值,适用于热区效果、环境响应、AI 行为调整等场景。相比线性插值,IDW 支持多点影响和非线性衰减,增强真实感。
设计灵感来源于空间插值算法,常用于 GIS 或游戏中的热图生成(如 和 中的 IDW 实现)。核心优势:实时计算、编辑器可视化、可序列化配置、多参数支持。时间复杂度 O(n),n 为影响点数(推荐 < 50 以保持性能)。适用于 Unity 2022.3+,兼容 URP/HDRP,支持移动端(需启用 Burst 编译优化)。
设计原理
- IDW 算法核心:目标参数值 = Σ (value_i / dist_i^p) / Σ (1 / dist_i^p),其中:
- value_i:第 i 个影响点的参数值。
- dist_i:目标点到影响点的距离(使用 Vector3.Distance)。
- p:功率参数(默认 2,控制衰减速度;p=1 为线性,p>2 为陡峭衰减)。
- 零距离处理:如果 dist=0,使用 value_i 避免除零。
- 影响范围:可选半径过滤,超出范围权重为 0。
- 多参数支持:每个影响点可映射多个参数(如 float、Vector3、Color)。
- 实时更新:在 Update 中计算,或事件驱动(如位置变化时)。
- 编辑器可视化:使用 Gizmos 绘制影响点和范围,支持拖拽调整。
- 优化:缓存距离计算,使用 Job System(Burst)并行化大 n 场景。
完整实现源码
主组件(InverseDistanceMapper.cs)
附加到需要映射参数的 GameObject 上。
using UnityEngine;
using System.Collections.Generic;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
public class InverseDistanceMapper : MonoBehaviour
{
[System.Serializable]
public class InfluencerPoint
{
public Vector3 position; // 世界坐标
public float value = 1f; // 参数值(可扩展为 Vector/Color 等)
public float radius = Mathf.Infinity; // 影响半径
}
[Header("映射设置")]
public List<InfluencerPoint> influencers = new List<InfluencerPoint>();
[Range(0.1f, 10f)] public float power = 2f; // IDW 功率
public float defaultValue = 0f; // 无影响时的默认值
[Header("输出参数")]
public float mappedValue { get; private set; } // 计算结果
public UnityEngine.Events.UnityEvent<float> onValueChanged;
private bool useBurst = true; // 启用 Burst 优化
void Update()
{
mappedValue = CalculateIDW(transform.position);
onValueChanged?.Invoke(mappedValue);
// 示例:应用到着色器参数
GetComponent<Renderer>().material.SetFloat("_MappedParam", mappedValue);
}
public float CalculateIDW(Vector3 targetPos)
{
if (influencers.Count == 0) return defaultValue;
if (useBurst && SystemInfo.supportsJobSystem)
{
return CalculateIDW_Burst(targetPos);
}
else
{
return CalculateIDW_Serial(targetPos);
}
}
private float CalculateIDW_Serial(Vector3 targetPos)
{
float sumWeightedValue = 0f;
float sumWeights = 0f;
foreach (var point in influencers)
{
float dist = Vector3.Distance(targetPos, point.position);
if (dist > point.radius) continue;
if (Mathf.Approximately(dist, 0f))
{
return point.value; // 零距离直接返回
}
float weight = 1f / Mathf.Pow(dist, power);
sumWeightedValue += point.value * weight;
sumWeights += weight;
}
return sumWeights > 0f ? sumWeightedValue / sumWeights : defaultValue;
}
[BurstCompile]
private struct IDWJob : IJobParallelFor
{
[ReadOnly] public NativeArray<InfluencerPoint> influencers;
public Vector3 targetPos;
public float power;
public float defaultValue;
public NativeArray<float> results; // 0: sumWeightedValue, 1: sumWeights
public void Execute(int index)
{
var point = influencers[index];
float dist = Vector3.Distance(targetPos, point.position);
if (dist > point.radius) return;
if (Mathf.Approximately(dist, 0f))
{
results[0] = point.value;
results[1] = 1f; // 标记零距离
return;
}
float weight = 1f / Mathf.Pow(dist, power);
results[0] += point.value * weight;
results[1] += weight;
}
}
private float CalculateIDW_Burst(Vector3 targetPos)
{
NativeArray<InfluencerPoint> nativeInfluencers = new NativeArray<InfluencerPoint>(influencers.ToArray(), Allocator.TempJob);
NativeArray<float> results = new NativeArray<float>(2, Allocator.TempJob);
IDWJob job = new IDWJob
{
influencers = nativeInfluencers,
targetPos = targetPos,
power = power,
defaultValue = defaultValue,
results = results
};
JobHandle handle = job.Schedule(influencers.Count, 16);
handle.Complete();
float sumWeightedValue = results[0];
float sumWeights = results[1];
nativeInfluencers.Dispose();
results.Dispose();
return sumWeights > 0f ? sumWeightedValue / sumWeights : defaultValue;
}
// 编辑器 Gizmos 可视化
void OnDrawGizmosSelected()
{
Gizmos.color = Color.green;
foreach (var point in influencers)
{
Gizmos.DrawWireSphere(point.position, point.radius);
Gizmos.DrawLine(transform.position, point.position);
Gizmos.DrawSphere(point.position, 0.1f);
}
}
}
使用说明
- 附加组件:在需要映射的 GameObject 上附加 InverseDistanceMapper。
- 配置影响点:在 Inspector 中添加 InfluencerPoint,设置位置、值和半径。
- 功率调整:power >1 为快速衰减,<1 为缓慢。
- 输出使用:通过 mappedValue 或 onValueChanged 应用到材质/音频等。
- Burst 优化:启用 useBurst 后,适用于大量影响点场景。
编辑器扩展
自定义 Inspector(InverseDistanceMapperEditor.cs)
放置在 Editor 文件夹中,提供点拖拽和预览。
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(InverseDistanceMapper))]
public class InverseDistanceMapperEditor : Editor
{
private InverseDistanceMapper mapper;
private void OnEnable()
{
mapper = (InverseDistanceMapper)target;
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
EditorGUILayout.Space();
GUILayout.Label("实时预览", EditorStyles.boldLabel);
EditorGUILayout.LabelField("映射值", mapper.mappedValue.ToString("F2"));
if (GUILayout.Button("添加影响点"))
{
Undo.RecordObject(mapper, "Add Influencer");
mapper.influencers.Add(new InverseDistanceMapper.InfluencerPoint
{
position = mapper.transform.position + Vector3.forward
});
EditorUtility.SetDirty(mapper);
}
}
[MenuItem("GameObject/Create InverseDistanceMapper", false, 0)]
private static void CreateMapper()
{
GameObject go = new GameObject("InverseDistanceMapper");
go.AddComponent<InverseDistanceMapper>();
Selection.activeGameObject = go;
}
private void OnSceneGUI()
{
// 拖拽编辑影响点
for (int i = 0; i < mapper.influencers.Count; i++)
{
var point = mapper.influencers[i];
EditorGUI.BeginChangeCheck();
Vector3 newPos = Handles.PositionHandle(point.position, Quaternion.identity);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(mapper, "Move Influencer");
point.position = newPos;
EditorUtility.SetDirty(mapper);
}
// 值滑块
Handles.Label(point.position + Vector3.up * 0.5f, $"Value: {point.value}");
}
}
}
#endif
高级扩展
多参数映射
扩展 InfluencerPoint 为 Vector4 或 struct,支持同时映射多个参数(如 RGB + Alpha)。
[System.Serializable]
public class MultiParamInfluencer
{
public Vector3 position;
public Vector4 values; // x=param1, y=param2 等
public float radius = Mathf.Infinity;
}
public Vector4 CalculateMultiIDW(Vector3 targetPos)
{
// 类似 CalculateIDW,扩展为 Vector4 计算
}
着色器集成示例
在材质中使用 mappedValue:
// Shader "Custom/IDWShader"
{
Properties
{
_MappedParam ("Mapped Parameter", Range(0,1)) = 0
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float _MappedParam;
float4 frag(v2f i) : SV_Target
{
return float4(_MappedParam, 0, 0, 1); // 示例:基于参数变色
}
ENDCG
}
}
}
性能优化
- Job System:Burst 编译加速多点计算。
- 空间分区:大场景使用 KD-Tree 或 Octree 过滤远点。
- 缓存:如果目标位置不变,缓存上次结果。
此 InverseDistanceMapper 提供高效的距离驱动参数映射,支持游戏动态效果。如果需要多参数扩展或 Job System 完整示例,请提供更多细节!