向量点乘和叉乘 游戏中的应用

361 阅读3分钟

点乘(也叫内积)

点乘是一个适用于任意维数向量的重要运算。给定两个向量 a\boldsymbol{a}b\boldsymbol{b},它们的夹角为 θ\theta,那么它们的点乘定义如下: ab=abcosθ\boldsymbol{a} \cdot \boldsymbol{b} = |\boldsymbol{a}| |\boldsymbol{b}| \cos \theta

其中:

  • a|\boldsymbol{a}|:向量 a\boldsymbol{a} 的模(长度)。
  • b|\boldsymbol{b}|:向量 b\boldsymbol{b} 的模(长度)。
  • cosθ\cos \theta:向量 a\boldsymbol{a}b\boldsymbol{b} 之间夹角 θ\theta 的余弦值。

几何意义

点乘 ab\boldsymbol{a} \cdot \boldsymbol{b} 可以解释为向量 a\boldsymbol{a} 的模与向量 b\boldsymbol{b}a\boldsymbol{a} 方向上的投影的乘积。 其中:

  • bcosθ|\boldsymbol{b}| \cos \theta:向量 b\boldsymbol{b}a\boldsymbol{a} 方向上的投影长度。

因此,点乘几何上表示为向量 a\boldsymbol{a} 的模与 b\boldsymbol{b}a\boldsymbol{a} 方向上的投影的乘积。

代数表示

对于 nn 维空间中的两个向量 a=(a1,a2,,an)\boldsymbol{a} = (a_1, a_2, \ldots, a_n)b=(b1,b2,,bn)\boldsymbol{b} = (b_1, b_2, \ldots, b_n), 它们的点乘可以用分量的形式表示为: ab=a1b1+a2b2++anbn=i=1naibi\boldsymbol{a} \cdot \boldsymbol{b} = a_1 b_1 + a_2 b_2 + \cdots + a_n b_n = \sum_{i=1}^n a_i b_i

游戏中的应用

0. 判断目标点是否在物体前方

在游戏开发中,可以使用点乘来判断一个目标点是否在物体的前方。

假设有物体的位置向量为 P\boldsymbol{P},物体朝向的单位向量为 F\boldsymbol{F},目标点的位置向量为 T\boldsymbol{T}

如果点乘 F\boldsymbol{F}(TP)(\boldsymbol{T} - \boldsymbol{P}) 的结果大于零,则目标点 T\boldsymbol{T} 在物体的前方;如果结果小于等于零,则目标点在物体的后方或者正好在物体上。

公式表示如下: F(TP)>0\boldsymbol{F} \cdot (\boldsymbol{T} - \boldsymbol{P}) > 0

其中,

  • F\boldsymbol{F} 是物体的朝向单位向量。
  • T\boldsymbol{T} 是目标点的位置向量。
  • P\boldsymbol{P} 是物体的位置向量。

1. 判断技能是否命中

灵魂画图:

image.png

例如,判断扇形技能的命中: 假设我们有一个扇形的范围技能, 玩家站在点 PP,扇形的方向向量(可以是玩家的释放技能的方向)为 d\boldsymbol{d},扇形的半角为 θ\theta,其中 θ\theta 的取值范围是 00^\circ180180^\circ。敌人的位置为点 EE,我们想判断敌人是否在这个扇形范围内。

  1. 计算向量:计算玩家到敌人的向量 v=EP\boldsymbol{v} = E - P
  2. 归一化:将方向向量 d\boldsymbol{d} 和向量 v\boldsymbol{v} 归一化。
  3. 计算点乘:计算点乘 cosϕ=dv\cos \phi = \boldsymbol{d} \cdot \boldsymbol{v}
  4. 判断角度:如果 cosϕcosθ\cos \phi \geq \cos \theta,表示 ϕθ\phi \leq \theta,则敌人在扇形范围内,否则不在。

2. 光照计算

在光照计算中,点乘被用来计算光线与物体表面法线之间的夹角。这可以帮助确定表面上某一点的亮度。

例如,Phong光照模型中计算漫反射分量时,会用到点乘: Idiffuse=kd(LN)I_{\text{diffuse}} = k_d (\boldsymbol{L} \cdot \boldsymbol{N})

其中,L\boldsymbol{L} 是指向光源的向量,N\boldsymbol{N} 是表面法线,kdk_d 是漫反射系数。

可以通过计算相机方向向量与对象位置向量之间的点乘来确定对象是否在相机的视野内。

3. 角色移动操控

在计算物体的运动和操控时,点乘可以帮助确定两个向量之间的关系。例如,在角色控制中,可以使用点乘来计算角色移动方向和摄像机朝向之间的夹角,从而调整角色的移动方向。

4. 投影计算

点乘在投影计算中有重要作用。例如,计算一个向量在另一个向量上的投影,可以帮助确定物体在某个方向上的分量,这在物理模拟和动画中都很有用。 投影长度=abb\text{投影长度} = \frac{\boldsymbol{a} \cdot \boldsymbol{b}}{|\boldsymbol{b}|}

叉乘 (外积)

叉乘是三维向量特有的运算。

定义向量 a,b\boldsymbol{a}, \boldsymbol{b} 的外积为一个向量,记为 a×b\boldsymbol{a} \times \boldsymbol{b},其模与方向定义如下:

a×b=absina,b|\boldsymbol{a} \times \boldsymbol{b}| = |\boldsymbol{a}| |\boldsymbol{b}| \sin \langle \boldsymbol{a}, \boldsymbol{b} \rangle

a×b\boldsymbol{a} \times \boldsymbol{b}a,b\boldsymbol{a}, \boldsymbol{b} 都垂直,且 a,b,a×b\boldsymbol{a}, \boldsymbol{b}, \boldsymbol{a} \times \boldsymbol{b} 符合右手法则。注意到外积的模,联想到三角形面积计算公式:

S=12absinCS = \frac{1}{2} ab \sin C

可以发现外积的几何意义是:a×b|\boldsymbol{a} \times \boldsymbol{b}| 是以 a,b\boldsymbol{a}, \boldsymbol{b} 为邻边的平行四边形的面积。

两个向量 a=(x1,y1,z1)\boldsymbol{a} = (x_1, y_1, z_1)b=(x2,y2,z2)\boldsymbol{b} = (x_2, y_2, z_2) 外积的结果是一个向量 c\boldsymbol{c},记作 c=a×b\boldsymbol{c} = \boldsymbol{a} \times \boldsymbol{b}

向量的外积可以使用三阶行列式表示:

ijkx1y1z1x2y2z2\begin{vmatrix} i & j & k \\ x_1 & y_1 & z_1 \\ x_2 & y_2 & z_2 \end{vmatrix}

其中 i,j,ki, j, k 表示和坐标轴 x,y,zx, y, z 平行的单位向量,并写在对应坐标处。

展开得到 c=(y1z2y2z1,x2z1x1z2,x1y2x2y1)\boldsymbol{c} = (y_1 z_2 - y_2 z_1, x_2 z_1 - x_1 z_2, x_1 y_2 - x_2 y_1)

游戏开发的应用

0. 来判断一个物体 B 是在另一个物体 A 的左侧还是右侧。

假设物体 A 的位置向量为 A\boldsymbol{A},物体 A 的朝向向量为 F\boldsymbol{F},物体 B 的位置向量为 B\boldsymbol{B}

首先计算向量 AB=BA\boldsymbol{AB} = \boldsymbol{B} - \boldsymbol{A},然后进行叉乘运算 F×AB\boldsymbol{F} \times \boldsymbol{AB}

根据右手法则,如果结果向量的 zz 分量(或者根据需要选择合适的分量)大于零,则物体 B 在物体 A 的左侧; 如果小于零,则物体 B 在物体 A 的右侧

公式表示如下: (F×AB).z>0(\boldsymbol{F} \times \boldsymbol{AB}).z > 0

其中,

  • F\boldsymbol{F} 是物体 A 的朝向单位向量。
  • A\boldsymbol{A} 是物体 A 的位置向量。
  • B\boldsymbol{B} 是物体 B 的位置向量。
  • AB=BA\boldsymbol{AB} = \boldsymbol{B} - \boldsymbol{A} 是从物体 A 指向物体 B 的向量。

使用法线贴图下,根据模型切线,法线计算副切线

Shader "Unlit/Lesson51"
{
    Properties
    {
        _MainColor("MainColor", Color) = (1,1,1,1)
        _MainTex("MainTex", 2D) = ""{}
        _BumpMap("BumpMap", 2D) = ""{}
        _BumpScale("BumpScale", Range(0,1)) = 1
        _SpecularColor("SpecularColor", Color) = (1,1,1,1)
        _SpecularNum("SpecularNum", Range(0,20)) = 18
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct v2f
            {
                float4 pos:SV_POSITION;
                //float2 uvTex:TEXCOORD0;
                //float2 uvBump:TEXCOORD1;
                //我们可以单独的声明两个float2的成员用于记录 颜色和法线纹理的uv坐标
                //也可以直接声明一个float4的成员 xy用于记录颜色纹理的uv,zw用于记录法线纹理的uv
                float4 uv:TEXCOORD0;
                //光的方向 相对于切线空间下的
                float3 lightDir:TEXCOORD1;
                //视角的方向 相对于切线空间下的
                float3 viewDir:TEXCOORD2;
            };

            float4 _MainColor;//漫反射颜色
            sampler2D _MainTex;//颜色纹理
            float4 _MainTex_ST;//颜色纹理的缩放和平移
            sampler2D _BumpMap;//法线纹理
            float4 _BumpMap_ST;//法线纹理的缩放和平移
            float _BumpScale;//凹凸程度
            float4 _SpecularColor;//高光颜色
            fixed _SpecularNum;//光泽度

            v2f vert (appdata_full v)
            {
                v2f data;
                //把模型空间下的顶点转到裁剪空间下
                data.pos = UnityObjectToClipPos(v.vertex);
                //计算纹理的缩放偏移
                data.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                data.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

                //在顶点着色器当中 得到 模型空间到切线空间的 转换矩阵
                //切线、副切线、法线
                //计算副切线 计算叉乘结果后 垂直与切线和法线的向量有两条 通过乘以 切线当中的w,就可以确定是哪一条
                float3 binormal = cross(normalize(v.tangent), normalize(v.normal)) * v.tangent.w;
                //转换矩阵
                float3x3 rotation = float3x3( v.tangent.xyz,
                                              binormal,
                                              v.normal);
                //模型空间下的光的方向
                //data.lightDir = ObjSpaceLightDir(v.vertex);
                //乘以模型空间到切线空间的转换矩阵 就可以得到切线空间下的 光的方向了
                data.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex));

                //模型空间下的视角的方向
                //data.viewDir = ObjSpaceViewDir(v.vertex);
                data.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex));

                return data;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                //通过纹理采样函数 取出法线纹理贴图当中的数据
                float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
                //将我们取出来的法线数据 进行逆运算并且可能会进行解压缩的运算,最终得到切线空间下的法线数据
                float3 tangentNormal = UnpackNormal(packedNormal);
                //乘以凹凸程度的系数
                tangentNormal *= _BumpScale;

                //接下来就来处理 带颜色纹理的 布林方光照模型计算

                //颜色纹理和漫反射颜色的 叠加
                fixed3 albedo = tex2D(_MainTex, i.uv.xy) * _MainColor.rgb;
                //兰伯特
                fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(tangentNormal, normalize(i.lightDir)));
                //半角向量
                float3 halfA = normalize(normalize(i.viewDir) + normalize(i.lightDir));
                //高光反射
                fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(tangentNormal, halfA)), _SpecularNum);
                //布林方
                fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;

                return fixed4(color.rgb, 1);
            }
            ENDCG
        }
    }
}