6.将顶点坐标转换到0~1区间内

6.如何将顶点坐标转换到0~1区间内


6.1 知识点

为何要将顶点坐标转换到0~1区间内

  • 目标是把顶点位置数据存进纹理。
  • 纹理里每个像素存的是颜色;在 Unity 里往 Color 里写时,RGBA 各分量用 0~1 表示。
  • 顶点坐标一般不在 0~1 里。
  • 所以要先做归一化:把某个区间 [min, max] 上的数映射到 [0, 1]

如何进行归一化

主要目的

  • 把某个区间 [min, max] 上的数值映射到 [0, 1]

主要思路

  • 目标值 = (x - min) / (max - min)
  • 例如原区间为 [20, 80]
    • x = 20 时:(20 - 20) / (80 - 20) = 0
    • x = 80 时:(80 - 20) / (80 - 20) = 1
    • x = 50 时:(50 - 20) / (80 - 20) = 0.5
  • Unity 里用 Mathf.InverseLerp(a, b, value),内部就是 (value - a) / (b - a)

主要步骤

  1. 在所有动画、所有帧里扫一遍顶点,得到全局最小值和最大值。
  2. 再对每个顶点的 x、y、z 分别做归一化,然后写入纹理或你自己的缓冲。

下面代码接在第 4 篇的 childAnimatorStateArray、第 5 篇的 SampleAnimation / BakeMesh 之后;开头几行只是把前两篇的取法缩写在同一屏里,避免对着三篇文章跳行。示例里 x、y、z 共用一对 min / max;若 max == minInverseLerp 会除零,工具里要单独处理。

第一段:统计 minmaxDebug.Log 打边界。

using UnityEngine;
using UnityEditor.Animations;

//1.得到目标GameObject
//(由外部 targetGameObject 传入)

//2.得到GameObject上的Animator组件
Animator animator = targetGameObject.GetComponent<Animator>();
if (animator == null)
{
    return;
}

//3.通过Animator组件得到AnimatorController
AnimatorController animatorController = animator.runtimeAnimatorController as AnimatorController;
if (animatorController == null)
{
    return;
}

//4.得到AnimatorController中对应层的状态机AnimatorStateMachine
AnimatorStateMachine animatorStateMachine = animatorController.layers[0].stateMachine;

//5.得到状态机中的所有状态ChildAnimatorState[]数组
ChildAnimatorState[] childAnimatorStateArray = animatorStateMachine.states;

//之所以绕一圈去获取动画切片 是为了之后做铺垫
//我们可以结合Animator,蒙皮网格渲染器,利用他们一起去得到某一个动画某一帧对应的顶点数据

// 拿到SkinnedMeshRenderer和创建Mesh
SkinnedMeshRenderer skinnedMeshRenderer = targetGameObject.GetComponentInChildren<SkinnedMeshRenderer>();
Mesh mesh = new Mesh();

//用于存储顶点最大最小值的变量
int min = int.MaxValue;
int max = int.MinValue;

for (int i = 0; i < childAnimatorStateArray.Length; i++)
{
    AnimatorState animatorState = childAnimatorStateArray[i].state;
    AnimationClip animationClip = animatorState.motion as AnimationClip;

    // animationClip.frameRate 帧率
    // animationClip.length 时长
    int frameCount = Mathf.CeilToInt(animationClip.frameRate * animationClip.length); //得到这个clip多少帧

    //需要把每一帧对应的时间点传递进去 => 帧数转时间 帧数/帧率
    for (int j = 0; j < frameCount; j++)
    {
        //一帧一帧的得到对应的顶点数据 存储到VAT中
        //让对象定格到了某一帧
        animationClip.SampleAnimation(targetGameObject, j / animationClip.frameRate);

        //得到当前帧的顶点信息
        //每次烘焙时把上一次的数据清理了
        mesh.Clear(false);

        //得到了此时此刻的网格相关的数据
        skinnedMeshRenderer.BakeMesh(mesh);

        //得到了此时此刻网格对应的所有顶点位置
        // 获取网格的顶点数组:每个顶点存储了三维空间中的坐标(x,y,z)
        Vector3[] vertices = mesh.vertices;
        // 获取网格的法线数组:每个顶点的法线方向,用于计算光照效果
        Vector3[] normals = mesh.normals;
        // 获取网格的切线数组:用于法线贴图(凹凸贴图)的计算,是四维向量(x,y,z,w),w 用于控制切线方向
        Vector4[] tangents = mesh.tangents;

        //我们需要遍历所有动画切片的所有帧的网格顶点位置
        //从而找到最大最小值 才能进行归一化计算
        //帧数 * 顶点数
        //(57+50+20)* 9500
        // 遍历网格的所有顶点,计算顶点坐标的最小/最大整数值
        for (int k = 0; k < vertices.Length; k++)
        {
            // 获取当前遍历到的顶点的三维坐标(x,y,z)
            Vector3 vertice = vertices[k];

            // 1. 更新最小值:
            // - 对比当前min值、顶点x/y/z坐标,取其中最小的数值
            // - Mathf.FloorToInt:向下取整(比如 2.9 → 2,-1.2 → -2),确保得到整数边界
            min = Mathf.FloorToInt(Mathf.Min(min, vertice.x, vertice.y, vertice.z));

            // 2. 更新最大值:
            // - 对比当前max值、顶点x/y/z坐标,取其中最大的数值
            // - Mathf.CeilToInt:向上取整(比如 2.1 → 3,-1.9 → -1),确保得到整数边界
            max = Mathf.CeilToInt(Mathf.Max(max, vertice.x, vertice.y, vertice.z));
        }
    }
}

Debug.Log("顶点坐标最小值" + min);
Debug.Log("顶点坐标最大值" + max);

第一段跑完后,Console 里会有两行边界。课程工程里示例如下(具体数字随资源而变):

第二段:用上面算好的 minmax,再套同样的双重循环,把每个顶点压到 0~1

for (int i = 0; i < childAnimatorStateArray.Length; i++)
{
    AnimatorState animatorState = childAnimatorStateArray[i].state;
    AnimationClip animationClip = animatorState.motion as AnimationClip;
    int frameCount = Mathf.CeilToInt(animationClip.length * animationClip.frameRate);

    //需要把每一帧对应的时间点传递进去 => 帧数转时间 帧数/帧率
    for (int j = 0; j < frameCount; j++)
    {
        //一帧一帧的得到对应的顶点数据 存储到VAT中
        //让对象定格到了某一帧
        animationClip.SampleAnimation(targetGameObject, j / animationClip.frameRate);
        //得到当前帧的顶点信息
        //每次烘焙时把上一次的数据清理了
        mesh.Clear(false);
        //得到了此时此刻的网格相关的数据
        skinnedMeshRenderer.BakeMesh(mesh);
        //得到了此时此刻网格对应的所有顶点位置
        Vector3[] vertices = mesh.vertices;
        Vector3[] normals = mesh.normals;
        Vector4[] tangents = mesh.tangents;

        //我们需要遍历所有动画切片的所有帧的网格顶点位置
        //从而找到最大最小值 才能进行归一化计算
        //帧数 * 顶点数
        //(57+50+20)* 9500
        for (int k = 0; k < vertices.Length; k++)
        {
            // 获取当前索引k对应的顶点三维坐标(x/y/z)
            Vector3 vertice = vertices[k];

            // 将顶点的三维坐标从 [min, max] 范围归一化到 [0, 1] 区间(核心:逆线性插值)
            // 归一化后的坐标存储到 normalVertice,用于后续颜色映射/纹理绘制等操作
            Vector3 normalVertice = new Vector3(
                Mathf.InverseLerp(min, max, vertice.x), // 对顶点x坐标做归一化
                Mathf.InverseLerp(min, max, vertice.y), // 对顶点y坐标做归一化
                Mathf.InverseLerp(min, max, vertice.z) // 对顶点z坐标做归一化
            );
        }
    }
}

6.2 知识点代码

NormalizeVerticesForVatExample.cs

using UnityEngine;
using UnityEditor.Animations;

// 将本脚本放在项目的 Editor 目录下,并确保程序集引用了 UnityEditor

public static class NormalizeVerticesForVatExample
{
    public static void ExampleNormalizeVertices(GameObject targetGameObject)
    {
        if (targetGameObject == null)
        {
            return;
        }

        //我们的目标是要把顶点位置数据存储到纹理图片中
        //纹理图片中每一个像素是一个颜色信息,在Unity中颜色信息中的RGBA分量需要用0~1表示
        //而我们的顶点位置显然不在0~1的范围内
        //因此我们需要对顶点坐标进行归一化操作
        //即把某个区间[min,max]的数值,映射到[0,1]区间内

        //主要目的:
        //把某个区间[min,max]的数值,映射到[0,1]区间内

        //主要思路:
        //目标值 = (x - min) / (max - min)
        //比如:
        //原区间为[20,80]
        // x = 20时
        // (20 - 20) / (80 - 20) = 0
        // x = 80时
        // (80 - 20) / (80 - 20) = 1
        // x = 50时
        // (50 - 20) / (80 - 20) = 0.5
        //Unity自带API
        //Mathf.InverseLerp(a, b, value);
        //该函数内部就是利用该公式进行计算的 返回值为 (value - a) / (b - a)

        //主要步骤:
        //1.得到所有动画中顶点坐标的最小值和最大值
        //2.对每个顶点的位置进行归一化后再存储

        // —— 第 4 篇:Animator → AnimatorController → 状态数组 ——
        Animator animator = targetGameObject.GetComponent<Animator>();
        if (animator == null)
        {
            return;
        }

        AnimatorController animatorController = animator.runtimeAnimatorController as AnimatorController;
        if (animatorController == null)
        {
            return;
        }

        AnimatorStateMachine animatorStateMachine = animatorController.layers[0].stateMachine;
        ChildAnimatorState[] childAnimatorStateArray = animatorStateMachine.states;

        // —— 第 5 篇:蒙皮网格 + Bake 用的 Mesh ——
        SkinnedMeshRenderer skinnedMeshRenderer = targetGameObject.GetComponentInChildren<SkinnedMeshRenderer>();
        if (skinnedMeshRenderer == null)
        {
            return;
        }

        Mesh mesh = new Mesh();

        //用于存储顶点最大最小值的变量
        int min = int.MaxValue;
        int max = int.MinValue;

        for (int i = 0; i < childAnimatorStateArray.Length; i++)
        {
            AnimatorState animatorState = childAnimatorStateArray[i].state;
            AnimationClip animationClip = animatorState.motion as AnimationClip;
            if (animationClip == null)
            {
                continue;
            }

            // animationClip.frameRate 帧率
            // animationClip.length 时长
            int frameCount = Mathf.CeilToInt(animationClip.frameRate * animationClip.length); //得到这个clip多少帧

            //需要把每一帧对应的时间点传递进去 => 帧数转时间 帧数/帧率
            for (int j = 0; j < frameCount; j++)
            {
                //一帧一帧的得到对应的顶点数据 存储到VAT中
                //让对象定格到了某一帧
                animationClip.SampleAnimation(targetGameObject, j / animationClip.frameRate);

                //得到当前帧的顶点信息
                //每次烘焙时把上一次的数据清理了
                mesh.Clear(false);

                //得到了此时此刻的网格相关的数据
                skinnedMeshRenderer.BakeMesh(mesh);

                //得到了此时此刻网格对应的所有顶点位置
                // 获取网格的顶点数组:每个顶点存储了三维空间中的坐标(x,y,z)
                Vector3[] vertices = mesh.vertices;
                // 获取网格的法线数组:每个顶点的法线方向,用于计算光照效果
                Vector3[] normals = mesh.normals;
                // 获取网格的切线数组:用于法线贴图(凹凸贴图)的计算,是四维向量(x,y,z,w),w 用于控制切线方向
                Vector4[] tangents = mesh.tangents;

                //我们需要遍历所有动画切片的所有帧的网格顶点位置
                //从而找到最大最小值 才能进行归一化计算
                //帧数 * 顶点数
                //(57+50+20)* 9500
                // 遍历网格的所有顶点,计算顶点坐标的最小/最大整数值
                for (int k = 0; k < vertices.Length; k++)
                {
                    // 获取当前遍历到的顶点的三维坐标(x,y,z)
                    Vector3 vertice = vertices[k];

                    // 1. 更新最小值:
                    // - 对比当前min值、顶点x/y/z坐标,取其中最小的数值
                    // - Mathf.FloorToInt:向下取整(比如 2.9 → 2,-1.2 → -2),确保得到整数边界
                    min = Mathf.FloorToInt(Mathf.Min(min, vertice.x, vertice.y, vertice.z));

                    // 2. 更新最大值:
                    // - 对比当前max值、顶点x/y/z坐标,取其中最大的数值
                    // - Mathf.CeilToInt:向上取整(比如 2.1 → 3,-1.9 → -1),确保得到整数边界
                    max = Mathf.CeilToInt(Mathf.Max(max, vertice.x, vertice.y, vertice.z));
                }
            }
        }

        Debug.Log("顶点坐标最小值" + min);
        Debug.Log("顶点坐标最大值" + max);


        for (int i = 0; i < childAnimatorStateArray.Length; i++)
        {
            AnimatorState animatorState = childAnimatorStateArray[i].state;
            AnimationClip animationClip = animatorState.motion as AnimationClip;
            if (animationClip == null)
            {
                continue;
            }

            int frameCount = Mathf.CeilToInt(animationClip.length * animationClip.frameRate);

            //需要把每一帧对应的时间点传递进去 => 帧数转时间 帧数/帧率
            for (int j = 0; j < frameCount; j++)
            {
                //一帧一帧的得到对应的顶点数据 存储到VAT中
                //让对象定格到了某一帧
                animationClip.SampleAnimation(targetGameObject, j / animationClip.frameRate);
                //得到当前帧的顶点信息
                //每次烘焙时把上一次的数据清理了
                mesh.Clear(false);
                //得到了此时此刻的网格相关的数据
                skinnedMeshRenderer.BakeMesh(mesh);
                //得到了此时此刻网格对应的所有顶点位置
                Vector3[] vertices = mesh.vertices;
                Vector3[] normals = mesh.normals;
                Vector4[] tangents = mesh.tangents;

                //我们需要遍历所有动画切片的所有帧的网格顶点位置
                //从而找到最大最小值 才能进行归一化计算
                //帧数 * 顶点数
                //(57+50+20)* 9500
                for (int k = 0; k < vertices.Length; k++)
                {
                    // 获取当前索引k对应的顶点三维坐标(x/y/z)
                    Vector3 vertice = vertices[k];

                    // 将顶点的三维坐标从 [min, max] 范围归一化到 [0, 1] 区间(核心:逆线性插值)
                    // 归一化后的坐标存储到 normalVertice,用于后续颜色映射/纹理绘制等操作
                    Vector3 normalVertice = new Vector3(
                        Mathf.InverseLerp(min, max, vertice.x), // 对顶点x坐标做归一化
                        Mathf.InverseLerp(min, max, vertice.y), // 对顶点y坐标做归一化
                        Mathf.InverseLerp(min, max, vertice.z) // 对顶点z坐标做归一化
                    );
                }
            }
        }
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏