Unity:自定义封装Vector3

276 阅读4分钟

image.png 向量:既有方向又有大小
点向量:不记录方向,只记录位置
方向向量:基于原点记录了一个方向
零向量:zero


创建一个结构体

// 自定义封装一个向量的结构体
public struct Vector3
{
    // 分别标识三维向量的x,y,z
    public float x;
    public float y;
    public float z;

    // 构造方法赋初值
    public Vector3(float x, float y, float z)
    {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    // 后序添加一些属性和方法...
}

在 Unity 中 Vector3 是一个标识三维向量的类,同时结构体对于值类型的运算效率较高,所以我通过结构体来封装一个自定义的 Vector3。

添加方向向量属性

// 零向量
public static Vector3 zero
{
    get
    {
        return new Vector3(0, 0, 0);
    }
}

// 前进向量
public static Vector3 forward
{
    get
    {
        return new Vector3(0, 0, 1);
    }
}

// 后退向量
public static Vector3 back
{
    get
    {
        return new Vector3(0, 0, -1);
    }
}

// 左移动向量
public static Vector3 left
{
    get
    {
        return new Vector3(-1, 0, 0);
    }
}

// 右移动向量
public static Vector3 right
{
    get
    {
        return new Vector3(1, 0, 0);
    }
}

// 跳起向量
public static Vector3 up
{
    get
    {
        return new Vector3(0, 1, 0);
    }
}

// 向下向量
public static Vector3 down
{
    get
    {
        return new Vector3(0, -1, 0);
    }
}

计算向量的长度(模长)/对应的单位向量

  1. 计算向量的模长(平方和开算术平方根)\color{red}{(平方和开算术平方根)}:
    Math.Sqrt(xx+yy+zz)\color{blue}{Math.Sqrt(x * x + y * y + z * z)}
  2. 计算对应的单位向量(x/y/z分别除以模长)\color{red}{(x/y/z分别除以模长)}:
    Vector3(x/向量模长,y/向量模长,z/向量模长)\color{blue}{Vector3(x/向量模长,y/向量模长,z/向量模长)}
// 向量的长度 模长
public float magnitude
{
    get
    {
        return (float)Math.Sqrt(x * x + y * y + z * z);
    }
}

// 单位向量 单位为1的
public Vector3 normalized
{
    get
    {
        if(x == 0 && y == 0 && z == 0)
        {
            throw new Exception("零向量没有单位向量!");
        }
        return new Vector3(x / magnitude, y / magnitude, z / magnitude);
    }
}

向量的加法/减法

  1. 加法(两个向量的x/y/z分别相加)\color{red}{(两个向量的x/y/z分别相加)}:
    Vector3(v1.x+v2.x,v1.y+v2.y,v1.z+v2.z)\color{blue}{Vector3(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z)}
  2. 减法(先取反再可以换算成加法)\color{red}{(先取反再可以换算成加法)}:
    取反Vector3(v.x,v.y,v.z)\color{blue}{Vector3(-v.x, -v.y, -v.z)}
// 向量的加法,通过运算符重写
public static Vector3 operator +(Vector3 v1, Vector3 v2)
{
    return new Vector3(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z);
}

// 向量的减法
// 取反
public static Vector3 operator -(Vector3 v)
{
    return new Vector3(-v.x, -v.y, -v.z);
}

// 减法换成 加法
public static Vector3 operator -(Vector3 v1, Vector3 v2)
{
    return v1+(-v2);
}

向量的数乘

向量的数乘就是为了将向量放大或者缩小多少倍。
x/y/z分别乘n\color{red}{x/y/z分别乘n}:
Vector3(v.xn,v.yn,v.zn)\color{blue}{Vector3(v.x * n,v.y*n,v.z*n)}

// 向量的 数乘
public static Vector3 operator *(Vector3 v, float n)
{
    return new Vector3(v.x * n,v.y*n,v.z*n);
}
// 重载:防止 数字在前,向量在后的情况
public static Vector3 operator *(float n, Vector3 v)
{
    return new Vector3(v.x * n, v.y * n, v.z * n);
}

后续内容需要了解的前置知识:

  1. 勾股定理(直角三角形)
    x2+y2=z2\color{blue}{x^2 + y^2 = z^2}
  2. 三角函数
    sinα=对边/斜边\color{blue}{sinα = 对边/斜边}
    cosα=邻边/斜边\color{blue}{cosα = 邻边/斜边}

推导三角和差公式:

image.png

已知半径为1(OP2 = OP1 = 1),求 OM 的公式?

OM = OB + BM

  1. OB = cosβ * OA;     OA = cosα * OP2;     OB = cosβ * cosα;
  2. BM = CP2;     CP2 = sinθ * AP2;     sinθ = sinβ;     CP2 = sinβ * AP2;     AP2 = OP2 * sinα;     CP2 = sinβ * OP2 * sinα = sinβ * sinα;     BM = sinβ * sinα;
  3. OM = OB + BM = cosβ * cosα + sinβ * sinα;
  4. OM = OP2 * cos(β-α) = cos(β-α);
  5. cos(β-α) = cosβ * cosα + sinβ * sinα(三角和差公式)\color{red}{(三角和差公式)};

向量的点乘

点乘的结果是一个标量,点乘的目的就是用来求夹角用的。

  1. 点乘公式:
    v1v2=x1x2+y1y2\color{blue}{v1·v2 = x1*x2+y1*y2}
  2. 点乘的几何意义:
    v1v2=v1v2cosθ(v1v2的夹角)\color{blue}{v1·v2 = |v1| * |v2| * cosθ(v1和v2的夹角)}
    接下来推导一下点乘的几何意义的公式: image.png x1 = |v1| * cosα
    x2 = |v2| * cosβ
    y1 = |v1| * sinα
    y2 = |v2| * sinβ
    v1 · v2 = x1 * x2 + y1 * y2
    v1 · v2 = |v1| * cosα * |v2| * cosβ + |v1| * sinα * |v2| * sinβ = |v1||v2|(cosα * cosβ + sinα * sinβ)
    通过上面推导的三角和差公式\color{blue}{三角和差公式},我们可以知道: (cosαcosβ+sinαsinβ)就是cos(αβ),也就是点乘的几何意义公式里的cosθ\color{red}{(cosα * cosβ + sinα * sinβ)就是cos(α-β),也就是点乘的几何意义公式里的cosθ。}
    cosθ:就是两个向量之间的夹角。

最终就得到了: v1·v2 = |v1| * |v2| * cosθ(两个向量之间的夹角)

// 向量的点乘
// 点乘的结果就是一个标量
// v1 · v2 = x1 * x2 + y1 * y2;
public static float Dot(Vector3 v1, Vector3 v2)
{
    return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
}

求向量之间的夹角

通过上面的向量点乘几何意义的公式,我们可以很轻易的求出来:
v1v2=v1v2cosθ(v1v2的夹角)\color{blue}{v1·v2 = |v1| * |v2| * cosθ(v1和v2的夹角)}
cosθ=v1v2/v1v2\color{blue}{cosθ = v1 · v2 / |v1| * |v2|}

// 求夹角
public static float Angle(Vector3 v1, Vector3 v2)
{
    // 一种方式
    //return Dot(v1, v2) / v1.magnitude * v2.magnitude;

    // 另一种方式:两个向量之间的夹角 和 它们所对应的单位向量的夹角 一样。
    float dot = Dot(v1.normalized, v2.normalized);
    // 返回弧度表示的角度
    float tmp = (float)Math.Acos(dot);
    // 转化为角度
    return (float)(tmp * 180 / Math.PI);
}
static void Main(string[] args)
{
    // 测试方法的正确性
    Vector3 v1 = new Vector3(1, 0, 0);
    Vector3 v2 = new Vector3(0, 1, 0);

    float result = Vector3.Angle(v1, v2);

    // 夹角为 90°
    Console.WriteLine(result);
}

投影向量

如图: 投影向量就是求 v1 在 v3 投影的模长,也就是 v2 的模长。

image.png

|v2| = |v1| * cosα(v1和v3之间的夹角,通过三角和差公式得来)
cosα = v1 · v3 / |v1| * |v3|
|v2| = |v1| * (v1 · v3) / |v1| * |v3| = (v1 · v3) / |v3|
v2 = |v2| * v3的单位向量(就是向量的数乘,大小加上方向就成了一个完整的向量)\color{green}{(就是向量的数乘,大小加上方向就成了一个完整的向量)}

// 投影向量
// |v2| = |v1| * (v1 · v3) / |v1| * |v3| = (v1 · v3) / |v3|
// v2 = |v2| * v3.normalized
public static Vector3 Project(Vector3 v1, Vector3 v3)
{
    // 返回的结果就是 v2 , |v2| = (Dot(v1, v3) / v3.magnitude)
    return v3.normalized * (Dot(v1, v3) / v3.magnitude);
}

求两个向量之间的距离

// 求两个位置之间的长度
public static float Distance(Vector3 v1, Vector3 v2)
{
    return (v1 - v2).magnitude;
}

两个向量的叉乘

两个向量的叉乘就是求得法线向量。
法线向量:\color{green}{法线向量:}
两条向量之间,垂直于两条向量的向量。\color{green}{两条向量之间,垂直于两条向量的向量。}

Vector3(v1.yv2.zv1.zv2.y,v1.zv2.xv1.xv2.z,v1.xv2.yv1.yv2.x)\color{blue}{Vector3(v1.y * v2.z - v1.z * v2.y, v1.z * v2.x - v1.x * v2.z, v1.x * v2.y - v1.y * v2.x)}

public static Vector3 Cross(Vector3 v1, Vector3 v2)
{
    return new Vector3(v1.y * v2.z - v1.z * v2.y, v1.z * v2.x - v1.x * v2.z, v1.x * v2.y - v1.y * v2.x);
}

插值

// 插值
public static Vector3 Lerp(Vector3 v1, Vector3 v2, float time)
{
    if(time < 0)
    {
        time = 0;
    }
    return v1+((v2-v1)*time);
}

插值的使用场景:
使用移动变得平滑(开始会很快,离目标越近就越慢,最后无限接近于目标)

void Update()
{
    // 参数分别是: 起始点,终点,时间
    transform.position = Vector3.Lerp(transform.position, target.position, Time.deltaTime);
}