【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);
        }
    }
}

使用说明

  1. 附加组件:在需要映射的 GameObject 上附加 InverseDistanceMapper。
  2. 配置影响点:在 Inspector 中添加 InfluencerPoint,设置位置、值和半径。
  3. 功率调整:power >1 为快速衰减,<1 为缓慢。
  4. 输出使用:通过 mappedValue 或 onValueChanged 应用到材质/音频等。
  5. 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 完整示例,请提供更多细节!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注