Unity Shader之数学篇

news2024/11/25 19:01:53

一、坐标系

1、二维笛卡尔坐标系

屏幕坐标系是二维笛卡尔坐标系,OpenGL的屏幕坐标系原点在左下角,DirectX的屏幕坐标系原点在左上角。

2、三维笛卡尔坐标系

三维笛卡尔坐标系要区分是左手坐标系还是右手坐标系。

左手坐标系:举起你的左手,用食指和大拇指摆出一个“L”的手势,并且让你的食指指向上,大拇指指向右,现在伸出你的中指指向前方。大拇指指向就是x的正向,食指指向就是y的正向,中指指向就是z的正向。

右手坐标系:同上,改为右手操作。

左手坐标系和右手坐标系对于正向旋转的定义也不同。

左手法则和右手法则

左手法则:伸出左手,大拇指指向旋转轴的正向,四指弯曲的方向就是旋转的正向。

右手法则:伸出右手,大拇指指向旋转轴的正向,四指弯曲的方向就是旋转的正向。

Unity的模型空间世界空间使用的是左手坐标系。

Unity的观察空间(摄像机的坐标系)使用的是右手坐标系。

二、点和矢量

1、概念

点是n维空间中是一个位置,它没有大小概念。

矢量是包含大小和方向的有向线段。

标量用小写字母表示。

矢量用小写的粗体字母表示。

2、矢量运算

2.1 矢量和标量的乘除法

k\vec{v}=(kv_x, kv_y, kv_z)

\tfrac{\vec{v}}{k} = ( \tfrac{x}{k}, \tfrac{y}{k}, \tfrac{z}{k} ) ( k\neq 0)

2.2 矢量的加减法

\vec a + \vec b = (a_x+b_x, a_y+b_y, a_z+b_z)

\vec a - \vec b = (a_x-b_x, a_y-b_y, a_z-b_z)

2.3 矢量的模

\left | \vec v \right | = \sqrt{v_{x}^{2} + v_{y}^{2} + v_{z}^{2}}

2.4 单位矢量

\hat{v} = \frac{\vec v}{\left | \vec v \right |} , \vec v是非零矢量

2.5 矢量的点积

公式一:\vec a\cdot \vec b = a_xb_x+a_yb_y+a_zb_z

公式二:\vec a\cdot \vec b =\left | \vec a \right | \left | \vec b \right |\cos \theta

几何意义:是a向量在b向量上的投影的乘积,也可表示两个向量之间的夹角。

2.6 矢量的叉积

\vec a\times \vec b =(a_yb_z-a_zb_y, a_zb_x-a_xb_z,a_xb_y-a_yb_x) 结果还是一个矢量,方向是使用对应坐标系的法则来确定。

\left |\vec a\times \vec b \right |=\left | \vec a \right | \left | \vec b \right | \sin \theta 结果是向量a、b构建的一个平行四边形的面积。

3、练习题

3.1 假设,场景中有一个NPC,它位于点A处,它的前方可以用矢量\vec v来表示。

问题1:如果现在玩家运动到了点B处,那么如何判断玩家是在NPC的前方还是后方。

答:用点积来判断,结果大于0就在前方。 \overrightarrow{AB} \cdot \vec v

问题2:现在NPC只能观察到有限的视角范围\phi且视距为s,也就是说NPC最多只能看到它前方左侧或右侧 \frac{\phi }{2} 角度内且相距在s范围内的物体。那么,我们如何通过点积来判断NPC是否可以看到点B呢?

答:首先求出AB的长度,如果大于s,则必定在视野外。如果小于等于s,则求\cos \theta,根据\cos \theta = \frac{\vec a\cdot \vec b}{\left | \vec a \right | \left | \vec b \right |} 求得,然后判断\cos \theta\cos \frac{\phi }{2}的大小关系,如果小于则在视野外,否则就在视野内。

3.2 在渲染中我们常会需要判断一个三角形片是正面还是背面,这可以通过判断三角形的3个顶点在当前空间中是顺时针还是逆时针排列来得到。 

问题:已知三个点A、B、C,如何利用叉乘来判断。A、B、C都位于xy平面,人眼位于z轴的负方向上,向z轴正方向观察。

答:\overrightarrow{AB}\times \overrightarrow{BC}=(0, 0, a) 

a如果大于0,则是逆时针,看到的是三角形的反面。

a如果小于0,则是顺时针,看到的是三角形的正面。

三、矩阵

1、定义

它是由m\times n个标量组成的长方形数组。形如:

\mathbf{M} = \begin{bmatrix} m_{11} &m_{12} &m_{13} \\ m_{21} &m_{22} &m_{23} \\ m_{31} &m_{32} &m_{33} \end{bmatrix}

m_{ij} 表明了这个元素在矩阵M的第i行、第j列。

2、矩阵运算

2.1 矩阵和标量的乘法

k\mathbf{M} = \mathbf{M}k = \begin{bmatrix} km_{11} &km_{12} &km_{13} \\ km_{21} &km_{22} &km_{23} \\ km_{31} &km_{32} &km_{33} \end{bmatrix}

2.2 矩阵和矩阵的乘法

一个r\times n的矩阵A和一个n\times c的矩阵B相乘,它们的结果AB将会是一个r\times c大小的矩阵。

第一个矩阵的列数必须和第二行矩阵的行数相同,相乘得到的矩阵的行数是第一个矩阵的行数,而列数是第二个矩阵的列数。

相乘得到的矩阵C中的每个元素c_{ij} 等于A的第i行所对应的矢量和B的第j列所对应的矢量进行矢量点乘的结果。即

c_{ij}=a_{i1}b_{1j}+a_{i2}b_{2j}+\cdots +a_{in}b_{nj}=\sum_{k=1}^{n}a_{ik}b_{kj}

性质:矩阵乘法不满足交换律,满足结合律

3、特殊矩阵

3.1 方块矩阵

方块矩阵简称方阵,是指那些行和列数目相等的矩阵。

对角元素:指的是行号和列号相等的元素,如m_{11}m_{22}m_{33} 等。

对角矩阵:指的是一个方阵除了对角元素外的所有元素都为0的矩阵。

3.2 单位矩阵

一个特殊的对角矩阵,它的对角元素全为1,用\mathbf{I_{n}}来表示。如下:

\mathbf{I_{3}} = \begin{bmatrix} 1 &0 &0 \\ 0 &1 &0 \\ 0 &0 &1 \end{bmatrix}

任何矩阵和它相乘的结果还是原来的矩阵。MI=IM=M

这就跟标量中的数字1一样。

3.3 转置矩阵

转置矩阵实际是对原矩阵的一种运算。给定一个r\times c的矩阵M,它的转置可以表示成M^{T},这是一个c\times r 的矩阵。

转置矩阵的计算就是将原矩阵翻转一下即可。原矩阵的第i行变成了第i列,而第j列变成了第j行。公式如下:

M_{ij}^{T} = M_{ji}

性质一:矩阵转置的转置等于原矩阵。

(M^{T})^{T}=M

性质二:矩阵串接的转置,等于反向串接各个矩阵的转置。

(AB)^{T}=B^{T}A^{T}

3.4 逆矩阵

不是所有的矩阵都有逆矩阵,第一个前提就是,该矩阵必须是一个方阵。

给定一个方阵M,它的逆矩阵用M^{-1}来表示。逆矩阵的重要性质就是,原矩阵与逆矩阵相乘结果是一个单位矩阵。

MM^{-1}=M^{-1}M=I

性质一:逆矩阵的逆矩阵是原矩阵。

(M^{-1})^{-1}=M

性质二:单位矩阵的逆矩阵是它本身。

I^{-1}=I

性质三:转置矩阵的逆矩阵是逆矩阵的转置。

(M^{T})^{-1}=(M^{-1})^{T}

性质四:矩阵串接相乘后的逆矩阵等于反向串接各个矩阵的逆矩阵。

(AB)^{-1}=B^{-1}A^{-1}

逆矩阵的几何意义:当我们使用变换矩阵M对矢量\vec{v}进行了一次变换,然后再使用它的逆矩阵M^{-1}进行另一次变换,那么会得到原来的矢量。

M^{-1}(M\vec v)=(M^{-1}M)\vec v=I\vec v=\vec v

逆矩阵的计算:

方法一:伴随矩阵法

  1. 求伴随矩阵:对于n阶矩阵A,其伴随矩阵A^*的元素A_{ij}^{*}\left | A_{ji} \right |,其中A_{ji}是去掉A中第j行第i列后得到的n-1阶子矩阵。
  2. 求行列式:计算矩阵A的行列式∣A∣。
  3. 计算逆矩阵A^{-1}=\frac{1}{\left | A \right |}A^{*}

方法二:初等变换法

  1. 构造增广矩阵:将原矩阵A与单位矩阵I放在一起,形成增广矩阵[A∣I]。
  2. 进行初等行变换:对增广矩阵[A∣I]进行初等行变换,目标是使左边的矩阵变为单位矩阵E。
  3. 提取逆矩阵:经过初等行变换后,增广矩阵变为[E∣B],此时B即为A的逆矩阵A^{-1}

下面是一个简单的例子来说明如何使用初等变换法求逆矩阵:

假设矩阵A=\begin{bmatrix} 1 &2 \\ 3&4 \end{bmatrix},我们需要求其逆矩阵。

  1. 构造增广矩阵:\begin{bmatrix} A|I \end{bmatrix} = \begin{bmatrix} 1 & 2 &1 &0 \\ 3& 4 &0 &1 \end{bmatrix}
  2. 进行初等行变换:
    • 第一行乘以-3加到第二行:\begin{bmatrix} 1 &2 &1 &0 \\ 0&-2 &-3 &1 \end{bmatrix}
    • 第二行除以-2:\begin{bmatrix} 1 &2 &1 &0 \\ 0&1 &\frac{3}{2} &-\frac{1}{2} \end{bmatrix}
    • 第二行乘以-2加到第一行:\begin{bmatrix} 1 &0 &-2 &1 \\ 0&1 &\frac{3}{2} &-\frac{1}{2} \end{bmatrix}
  3. 提取逆矩阵:经过初等行变换后,增广矩阵变为[E∣B],其中B=\begin{bmatrix} -2 &1 \\ \frac{3}{2} &-\frac{1}{2} \end{bmatrix}即为A的逆矩阵A^{-1}

所以,矩阵A的逆矩阵为A^{-1}=\begin{bmatrix} -2 &1 \\ \frac{3}{2} &-\frac{1}{2} \end{bmatrix}

3.5 正交矩阵

如果一个方阵M和它的转置矩阵的乘积是单位矩阵的话,我们就说这个矩阵是正交的。反过来说也是成立的。

MM^T=M^TM=I

如果一个矩阵是正交的,那么它的转置矩阵和逆矩阵是一样的。

M^T=M^{-1}

正交矩阵的特点:

M^TM=\begin{bmatrix} - & c_1& -\\ - &c_2& -\\ - &c_3& - \end{bmatrix}\begin{bmatrix} | & |& |\\ c_1 &c_2& c_3\\ | &|& | \end{bmatrix}

=\begin{bmatrix} c_1\cdot c_1 &c_1\cdot c_2 &c_1\cdot c_3 \\ c_2\cdot c_1 &c_2\cdot c_2 &c_2\cdot c_3 \\ c_3\cdot c_1 &c_3\cdot c_2 &c_2\cdot c_3 \end{bmatrix}=\begin{bmatrix} 1 &0 &0 \\ 0&1 &0 \\ 0& 0& 1 \end{bmatrix}=I

c_1\cdot c_1=1,c_1\cdot c_2=0,c_1\cdot c_3=0 \newline c_2\cdot c_1=0,c_2\cdot c_2=1,c_2\cdot c_3=0 \newline c_3\cdot c_1=0,c_3\cdot c_2=0,c_3\cdot c_3=1

我们可以得出以下结论:

1、矩阵的每一行,即c_1c_2c_3是单位矢量,只有这样它们和自己的点积才能是1

2、矩阵的每一行,即c_1c_2c_3之间相互垂直,只有这样它们之间的点积才能是0

3、上述结论对矩阵的每一列同样适用

因此,如果这些基矢量是一组标准正交基的话,那么我们就可以直接使用转置矩阵来求得该变换的逆变换。

4、行矩阵还是列矩阵

一个矢量可以转换成一个行矩阵或列矩阵。它本身没什么区别,但是,当我们把它和另一个矩阵相乘时,就会出现一些差异。

假设有一个矢量\vec v = (x,y,z),我们将它的行、列矩阵分别和矩阵M相乘:

M=\begin{bmatrix} m_{11} &m_{12} &m_{13} \\ m_{21} &m_{22} &m_{23} \\ m_{31} &m_{32} &m_{33} \end{bmatrix}

和行矩阵相乘要放在矩阵M的左边:

\vec v M=[xm_{11}+ym_{21}+zm_{31},xm_{12}+ym_{22}+zm_{32},xm_{13}+ym_{23}+zm_{33}]

和列矩阵相乘要放在矩阵M的右边:

M\vec v = \begin{bmatrix} xm_{11}+ym_{12}+zm_{13}\\ xm_{21}+ym_{22}+zm_{23}\\ xm_{31}+ym_{32}+zm_{33} \end{bmatrix}

认真比较会发现,结果矩阵除了行列矩阵的区别外,里面的元素也是不一样的。这就意味着,在和矩阵相乘时选择行矩阵还是列矩阵来表示矢量是非常重要的,因为这决定了矩阵相乘法的书写次序和结果值。

在Unity中,常规做法是把矢量放在矩阵的右侧,即把矢量转换成列矩阵来进行运算。

5、矩阵的几何意义:变换

5.1 线性变换

指的是那些可以保留矢量加和标量乘的变换。用数学公式来表示这两个条件就是:

f(\vec x)+f(\vec y)=f(\vec x+\vec y) \newline \newline kf(\vec x)=f(k\vec x)

缩放就是一种线性变换。例如f(\vec x)=2\vec x,可以表示一个大小为2的统一缩放。可以发现,f(\vec x)=2\vec x是满足上述两个条件的。

线性变换包括:旋转、缩放、错切、镜像、正交投影等。

仅有线性变换时不够的,平移变换就不是一个线性变换,例如f(\vec x)=\vec x+(1,2,3),它满足标量乘法,但不满足矢量加法。

如果令\vec x = (1,1,1),那么:

f(\vec x)+f(\vec x)=(4,6,8) \newline \newline f(\vec x+\vec x)=(3,4,5)

可见,两个运算得到的结果是不一样的。因此,不能用一个3x3的矩阵来表示一个平移变换。这样就有了仿射变换。

5.2 仿射变换

是合并线性变换和平移变换的变换类型。仿射变换可以使用一个4x4的矩阵来表示,为此,我们需要把矢量扩展到四维空间下,这就是齐次坐标空间

下表给出了图形学中常见变换矩阵的名称和它们的特性。

5.3 齐次坐标

由于3x3的矩阵不能表示平移操作,那么就将其扩展到了4x4的矩阵。为此,我们还需要把原来的三维矢量转换成四维矢量,也就是我们所说的齐次坐标(齐次坐标的维度可以超过四维,本文所说的齐次坐标泛指四维齐次坐标)。

对于一个点,从三维坐标转换成齐次坐标是把其w分量设为1,而对于方向矢量来说,需要把其分量设为0。这样设置会导致,当用一个4x4矩阵对一个点进行变换时,平移、旋转、缩放都会施加于该点,但是如果是用于一个方向矢量,平移效果就会被忽略。

5.3.1 分解基础变换矩阵

把表示纯平移、纯旋转和纯缩放的变换矩阵叫做基础变换矩阵。这些矩阵具有一些共同点,我们可以把一个基础变换矩阵分解成4个组成部分:

\begin{bmatrix} M_{3\times 3} &t_{3\times 1} \\ 0_{1\times 3} &1 \end{bmatrix}

其中,左上角的矩阵M_{3\times 3}用于表示旋转和缩放,t_{3\times 1}用于表示平移,0_{1\times 3}是零矩阵,右下角的元素就是标量1。

5.3.2 平移矩阵

对点做平移变换:

\begin{bmatrix} 1 &0 &0 &t_x \\ 0&1 &0 &t_y \\ 0&0 &1 &t_z \\ 0&0 &0 &1 \end{bmatrix}\begin{bmatrix} x\\ y\\ z\\ 1 \end{bmatrix}=\begin{bmatrix} x+t_x\\ y+t_y\\ z+t_z\\ 1 \end{bmatrix}

对矢量做平移变换:

\begin{bmatrix} 1 &0 &0 &t_x \\ 0&1 &0 &t_y \\ 0&0 &1 &t_z \\ 0&0 &0 &1 \end{bmatrix}\begin{bmatrix} x\\ y\\ z\\ 0 \end{bmatrix}=\begin{bmatrix} x\\ y\\ z\\ 0 \end{bmatrix}

显而易见,平移变换不会对矢量产生任何影响。这点很容易理解,前面已经说过矢量是没有位置属性的。

平移矩阵的逆矩阵就是反向平移得到的矩阵:

\begin{bmatrix} 1 &0 &0 &-t_x \\ 0&1 &0 &-t_y \\ 0&0 &1 &-t_z \\ 0&0 &0 &1 \end{bmatrix}

平移矩阵并不是一个正交矩阵。

5.3.3 缩放变换

对一个模型沿空间的x轴、y轴、z轴进行缩放变换:

\begin{bmatrix} k_x &0 &0 &0 \\ 0&k_y &0 &0 \\ 0&0 &k_z &0 \\ 0&0 &0 &1 \end{bmatrix}\begin{bmatrix} x\\ y\\ z\\ 1 \end{bmatrix}=\begin{bmatrix} k_xx\\ k_yy\\ k_zz\\ 1 \end{bmatrix}

对矢量进行缩放变换:

\begin{bmatrix} k_x &0 &0 &0 \\ 0&k_y &0 &0 \\ 0&0 &k_z &0 \\ 0&0 &0 &1 \end{bmatrix}\begin{bmatrix} x\\ y\\ z\\ 0 \end{bmatrix}=\begin{bmatrix} k_xx\\ k_yy\\ k_zz\\ 0 \end{bmatrix}

如果缩放系数k_x=k_y=k_z,这样的缩放称为统一缩放,否则称为非统一缩放

缩放矩阵的逆矩阵是使用原缩放系数的倒数进行缩放变换:

\begin{bmatrix} \frac{1}{k_x} &0 &0 &0 \\ 0&\frac{1}{k_y} &0 &0 \\ 0&0 &\frac{1}{k_z} &0 \\ 0&0 &0 &1 \end{bmatrix}

缩放矩阵一般不是正交矩阵。上面的矩阵只适用于沿坐标轴方向进行缩放。如果沿任意方向进行缩放,就需要使用一个复合变换。其中一种方法的主要思想就是:先将缩放轴变换成标准坐标轴,然后进行沿坐标轴的缩放,再进行逆变换得到原来的缩放轴朝向。

5.3.4 旋转矩阵

旋转矩阵是三种常见的变换矩阵中最复杂的一种。旋转操作需要指定一个旋转轴,这个旋转轴不一定是空间的坐标轴,下面列举的是围绕空间的x轴、y轴、z轴进行旋转。

将点绕着x轴旋转\theta度:

R_x(\theta )=\begin{bmatrix} 1 &0 &0 &0 \\ 0&\cos \theta &-\sin \theta &0 \\ 0&\sin \theta &\cos \theta &0 \\ 0&0 &0 &1 \end{bmatrix}

将点绕着y轴旋转\theta度:

R_y(\theta )=\begin{bmatrix} \cos \theta &0 &\sin \theta &0 \\ 0&1 &0 &0 \\ -\sin \theta&0 &\cos \theta &0 \\ 0&0 &0 &1 \end{bmatrix}

将点绕着z轴旋转\theta度:

R_z(\theta )=\begin{bmatrix} \cos \theta &-\sin \theta &0 &0 \\ \sin \theta&\cos \theta &0 &0 \\ &0 &1 &0 \\ 0&0 &0 &1 \end{bmatrix}

旋转矩阵的逆矩阵是旋转相反角度得到的变换矩阵。旋转矩阵是正交矩阵,而且多个旋转矩阵之间的串联同样是正交的。

5.3.5 复合变换

复合变换就是把平移、旋转和缩放组合起来,形成一个复杂的变换过程。

复合变换可以通过矩阵的串联来实现。例如先缩放、再旋转、最后平移,可以表示如下:

P_{new} = M_{tran}M_{rotation}M_{scale}P_{old}

由于我们使用的是列矩阵,因此阅读顺序是从右到左的。

为了从数学公式上理解变换顺序的本质,我们可以对比不同变换顺序产生的变换矩阵的表达式。

如果我们只考虑对y轴的旋转的话,按先缩放、再旋转、最后平移这样的顺序组合3种变换得到的变换矩阵是:

M_{tran}M_{y }(\theta)M_s=\begin{bmatrix} 1 &0 &0 &t_x \\ 0&1 &0 &t_y \\ 0&0 &1 &t_z \\ 0&0 &0 &1 \end{bmatrix}\begin{bmatrix} \cos\theta &0 &\sin\theta &0 \\ 0&1 &0 &0 \\ -\sin\theta &0 &\cos\theta &0 \\ 0& 0& 0& 1 \end{bmatrix}\begin{bmatrix} k_x &0 &0 &0 \\ 0&k_y &0 &0 \\ 0&0 &k_z &0 \\ 0& 0 &0 &1 \end{bmatrix} \newline \newline \newline =\begin{bmatrix} k_x\cos\theta & 0 &k_z\sin\theta &t_x \\ 0&k_y &0 &t_y \\ -k_x\sin\theta &0 &k_z\cos\theta &-t_z \\ 0&0 &0 &1 \end{bmatrix}

而如果我们使用其他变换顺序,例如先平移,再缩放,最后旋转,那么得到的变换矩阵是:

M_{y }(\theta)M_sM_{tran}= \begin{bmatrix} \cos\theta &0 &\sin\theta &0 \\ 0&1 &0 &0 \\ -\sin\theta &0 &\cos\theta &0 \\ 0& 0& 0& 1 \end{bmatrix} \begin{bmatrix} k_x &0 &0 &0 \\ 0&k_y &0 &0 \\ 0&0 &k_z &0 \\ 0& 0 &0 &1 \end{bmatrix} \begin{bmatrix} 1 &0 &0 &t_x \\ 0&1 &0 &t_y \\ 0&0 &1 &t_z \\ 0&0 &0 &1 \end{bmatrix} \newline \newline \newline =\begin{bmatrix} k_x\cos\theta & 0 &k_z\sin\theta &t_xk_x\cos\theta+t_zk_z\sin\theta \\ 0&k_y &0 &t_xk_x \\ -k_x\sin\theta &0 &k_z\cos\theta &-t_xk_x\sin\theta+t_zk_z\cos\theta \\ 0&0 &0 &1 \end{bmatrix}

从两个结果可以看出,得到的变换矩阵是不一样的。

除了需要注意不同类型的变换顺序外,还要小心旋转的变换顺序。当我们给出了分别绕x轴、y轴和z轴旋转的变换矩阵。一个问题是,它们的顺序如何定义呢?

在Unity中,这个旋转顺序是zxy,这在旋转相关的API文档中都有说明。

旋转角度(\theta _x,\theta _y,\theta _z)

\bullet绕坐标系E下的z轴旋转\theta _z,绕坐标系E下的y轴旋转\theta _y,绕坐标系E下的x轴旋转\theta _x,即进行一次旋转时不一起旋转当前坐标系。

5.3.6 法线变换

法线也被称为法矢量。法线变换是一种特殊的变换。

使用原变换矩阵的逆转置矩阵来变换法线就可以得到正确的结果。值得注意的事,如果变换矩阵M_{A \to B}是正交矩阵,那么M_{A \to B}^{-1}=M_{A \to B}^{T},因此(M_{A \to B}^{T})^{-1}=M_{A \to B},也就是说我们可以使用用于变换顶点的变换矩阵来直接变换法线。

1、如果变换只包括旋转变换,那么这个变换矩阵就是正交矩阵,可以用于法线变换。

2、如果变换只包含旋转和统一缩放,而不包含非统一缩放,可以将变换矩阵乘以\frac{1}{k}用于法线变换。

3、如果变换包含了非统一变换,那么我们就必须要求解逆矩阵来得到变换法线的矩阵

四、坐标空间

坐标空间必须指明原点位置和3个坐标轴的方向。每个坐标空间都是另一个坐标空间的子空间。

现在,我们已知子空间C的3个坐标轴在父空间P下的表示x_cy_cz_c,以及原点位置O_c。当给定一个子坐标空间中的一点A_c=(a,b,c),我们可以确定其在父坐标空间下的位置A_p

1、从坐标空间的原点开始

O_c

2、向x轴方向移动a个单位

O_c+ax_c

3、向y轴方向移动b个单位

O_c+ax_c+by_c

4、向z轴方向移动c个单位

O_c+ax_c+by_c+cz_c

现在,我们已经求出了A_p

A_p=O_c+ax_c+by_c+cz_c

子坐标空间到父坐标空间的变换矩阵,记为M_{c \rightarrow p}

M_{c \rightarrow p}=\begin{bmatrix} | &| &| &| \\ x_c &y_c &z_c &O_c \\ |& | & | &| \\ 0& 0& 0& 1 \end{bmatrix}

 对矢量的坐标空间变换可以使用3x3的矩阵表示:

M_{c \rightarrow p}=\begin{bmatrix} | &| &| \\ x_c &y_c &z_c\\ |& | & | \end{bmatrix}

1、模型空间

也被称为对象空间或局部空间。每个模型都有自己独立的坐标空间,当它移动或旋转的时候,模型空间也会跟着它移动和旋转。

在Unity在中,模型空间中使用的是左手坐标系。

模型空间的原点和坐标轴通常是由美术人员在建模软件里确定好的。

2、世界空间

它是一个特殊的坐标系,因为它建立了我们所关心的最大的空间。

在Unity在中,世界空间中使用的是左手坐标系。

顶点变换的第一步,就是将顶点坐标从模型空间变换到世界空间中。这个变换通常叫做模型变换。

3、观察空间

观察空间也被称为摄像机空间

在Unity在中,观察空间中使用的是右手坐标系。

顶点变换的第二步,就是将顶点坐标从世界空间变换到观察空间中。这个变换通常叫做观察变换。

4、裁剪空间

顶点接下来要从观察空间转换到裁剪空间(也被称为齐次裁剪空间)中,这个用于变换的矩阵叫做裁剪矩阵,也被称为投影矩阵

视椎体:决定裁剪空间的范围。视椎体由六个平面包围而成,这些平面也被称为裁剪平面。

视椎体有两种类型,这涉及两种投影类型:一种是正交投影,一种是透视投影。使用的矩阵叫投影矩阵。

5、屏幕空间

经过投影矩阵的变换后,就可以进行裁剪操作。当完成了所有的裁剪工作后,就需要进行真正的投影了,也就是将视椎体投影到屏幕空间中。

屏幕空间是一个二维空间。

首先,我们需要进行标准齐次除法,也被称为透视除法。就是用齐次坐标系的w分量去除以x、y、z分量。进过这一步后会将裁剪空间变到一个立方体内。

6、总结

五、Unity Shader的内置变量

内置着色器变量 - Unity 手册

1、变换

所有这些矩阵都是float4x4类型,并且是列主序的。 

名称
UNITY_MATRIX_MVP当前模型 * 视图 * 投影矩阵。用于将顶点/方向矢量从模型空间变换到裁剪空间
UNITY_MATRIX_MV当前模型 * 视图矩阵。用于将顶点/方向矢量从模型空间变换到观察空间
UNITY_MATRIX_V当前视图矩阵。用于将顶点/方向矢量从世界空间变换到观察空间
UNITY_MATRIX_P当前投影矩阵。用于将顶点/方向矢量从观察空间变换到裁剪空间
UNITY_MATRIX_VP当前视图 * 投影矩阵。用于将顶点/方向矢量从世界空间变换到裁剪空间
UNITY_MATRIX_T_MV模型转置 * 视图矩阵。UNITY_MATRIX_MV的转置矩阵
UNITY_MATRIX_IT_MV模型逆转置 * 视图矩阵。UNITY_MATRIX_MV的逆转置矩阵,用于将法线从模型空间变换到观察空间,也可用于得到UNITY_MATRIX_MV的逆矩阵
unity_ObjectToWorld当前模型矩阵。用于将顶点/方向矢量从模型空间变换到世界空间
unity_WorldToObject当前世界矩阵的逆矩阵。用于将顶点/方向矢量从世界空间变换到模型空间

2、摄像机和屏幕

这些变量将对应于正在渲染的摄像机。例如,在阴影贴图渲染中,它们仍将引用摄像机组件值,而不是用于阴影贴图投影的“虚拟摄像机”。

名称类型
_WorldSpaceCameraPosfloat3摄像机的世界空间位置。
_ProjectionParamsfloat4x 是 1.0(如果当前使用翻转投影矩阵进行渲染,则为 –1.0),y 是摄像机的近平面,z 是摄像机的远平面,w 是远平面的倒数。
_ScreenParamsfloat4x 是摄像机目标纹理的宽度(以像素为单位),y 是摄像机目标纹理的高度(以像素为单位),z 是 1.0 + 1.0/宽度,w 为 1.0 + 1.0/高度。
_ZBufferParamsfloat4用于线性化 Z 缓冲区值。x 是 (1-远/近),y 是 (远/近),z 是 (x/远),w 是 (y/远)。
unity_OrthoParamsfloat4x 是正交摄像机的宽度,y 是正交摄像机的高度,z 未使用,w 在摄像机为正交模式时是 1.0,而在摄像机为透视模式时是 0.0。
unity_CameraProjectionfloat4x4摄像机的投影矩阵。
unity_CameraInvProjectionfloat4x4摄像机投影矩阵的逆矩阵。
unity_CameraWorldClipPlanes[6]float4摄像机视锥体平面世界空间方程,按以下顺序:左、右、底、顶、近、远。

3、时间

 时间以秒为单位,并由项目 Time 设置中的时间乘数 (Time multiplier) 进行缩放。没有内置变量可用于访问未缩放的时间。

名称类型
_Timefloat4自关卡加载以来的时间 (t/20, t, t*2, t*3),用于将着色器中的内容动画化。
_SinTimefloat4时间正弦:(t/8, t/4, t/2, t)。
_CosTimefloat4时间余弦:(t/8, t/4, t/2, t)。
unity_DeltaTimefloat4增量时间:(dt, 1/dt, smoothDt, 1/smoothDt)。

4、光照

光源参数以不同的方式传递给着色器,具体取决于使用哪个渲染路径, 以及着色器中使用哪种光源模式通道标签。

前向渲染(ForwardBase 和 ForwardAdd 通道类型):

名称类型
_LightColor0(在 UnityLightingCommon.cginc 中声明)fixed4光源颜色。
_WorldSpaceLightPos0float4方向光:(世界空间方向,0)。其他光源:(世界空间位置,1)。
unity_WorldToLight(在 AutoLight.cginc 中声明)float4x4世界/光源矩阵。用于对剪影和衰减纹理进行采样。
unity_4LightPosX0、unity_4LightPosY0、unity_4LightPosZ0float4(仅限 ForwardBase 通道)前四个非重要点光源的世界空间位置。
unity_4LightAtten0float4(仅限 ForwardBase 通道)前四个非重要点光源的衰减因子。
unity_LightColorhalf4[4](仅限 ForwardBase 通道)前四个非重要点光源的颜色。
unity_WorldToShadowfloat4x4[4]世界/阴影矩阵。聚光灯的一个矩阵,方向光级联最多有四个矩阵。

延迟着色和延迟光照,在光照通道着色器中使用(全部在 UnityDeferredLibrary.cginc 中声明):

名称类型
_LightColorfloat4光源颜色。
unity_WorldToLightfloat4x4世界/光源矩阵。用于对剪影和衰减纹理进行采样。
unity_WorldToShadowfloat4x4[4]世界/阴影矩阵。聚光灯的一个矩阵,方向光级联最多有四个矩阵。

为 ForwardBasePrePassFinal 和 Deferred 通道类型设置了球谐函数系数 (由环境光和光照探针使用)。这些系数包含由世界空间法线求值的三阶 SH 函数(请参阅 UnityCG.cginc 中的 ShadeSH9)。 这些变量都是 half4 类型、unity_SHAr 和类似名称。

顶点光照渲染(Vertex 通道类型):

最多可为 Vertex 通道类型设置 8 个光源;始终从最亮的光源开始排序。因此,如果您希望 一次渲染受两个光源影响的对象,可直接采用数组中前两个条目。如果影响对象 的光源数量少于 8,则其余光源的颜色将设置为黑色。

名称类型
unity_LightColorhalf4[8]光源颜色。
unity_LightPositionfloat4[8]视图空间光源位置。方向光为 (-direction,0);点光源/聚光灯为 (position,1)。
unity_LightAttenhalf4[8]光源衰减因子。x 是 cos(spotAngle/2) 或 –1(非聚光灯);_y_ 是1/cos(spotAngle/4) 或 1(非聚光灯);_z_ 是二次衰减;_w_ 是平方光源范围。
unity_SpotDirectionfloat4[8]视图空间聚光灯位置;非聚光灯为 (0,0,1,0)。

5、光照贴图

名称类型
unity_LightmapTexture2D包含光照贴图信息。
unity_LightmapSTfloat4[8]缩放 UV 信息并转换到正确的范围以对光照贴图纹理进行采样。

6、雾效和环境光

名称类型
unity_AmbientSkyfixed4梯度环境光照情况下的天空环境光照颜色。
unity_AmbientEquatorfixed4梯度环境光照情况下的赤道环境光照颜色。
unity_AmbientGroundfixed4梯度环境光照情况下的地面环境光照颜色。
UNITY_LIGHTMODEL_AMBIENTfixed4环境光照颜色(梯度环境情况下的天空颜色)。旧版变量。
unity_FogColorfixed4雾效颜色。
unity_FogParamsfloat4用于雾效计算的参数:(density / sqrt(ln(2))、density / ln(2)、–1/(end-start) 和 end/(end-start))。x 对于 Exp2 雾模式很有用;_y_ 对于 Exp 模式很有用,_z_ 和 w 对于 Linear 模式很有用。

7、其他

名称类型
unity_LODFadefloat4使用 LODGroup 时的细节级别淡入淡出。x 为淡入淡出(0 到 1),_y_ 为量化为 16 级的淡入淡出,_z_ 和 w 未使用。
_TextureSampleAddfloat4根据所使用的纹理是 Alpha8 格式(值设置为 (1,1,1,0))还是不是该格式(值设置为 (0,0,0,0))由 Unity 仅针对 UI 自动设置。

六、答疑解惑

1、使用3x3还是4x4的变换矩阵

        对于线性变换(例如旋转和缩放)来说,仅使用3×3的矩阵就足够表示所有的变换了。但如果存在平移变换,我们就需要使用4x4的矩阵。因此,在对顶点的变换中,我们通常使用4x4的变换矩阵。当然,在变换前我们需要把点坐标转换成齐次坐标的表示,即把顶点的W分量设为1。而在对方向失量的变换中,我们通常使用3×3的矩阵就足够了,这是因为平移变换对方向失量是没有影响的。

2、CG中的矢量和矩阵类型

        我们通常在Unity Shader中使用CG作为着色器编程语言。在CG中变量类型有很多种,但在本节我们是想解释如何使用这些类型进行数学运算。因此,我们只以float家族的变量来做说明。
        在CG中,矩阵类型是由float3x3、float4x4等关键词进行声明和定义的。而对于float3、float4
等类型的变量,我们既可以把它当成一个矢量,也可以把它当成是一个1xn的行矩阵或者一个n
x1的列矩阵。这取决于运算的种类和它们在运算中的位置。例如,当我们进行点积操作时,两个
操作数就被当成失量类型,如下:

float4 a = float4(1.0, 2.0, 3.0, 4.0);
float4 b = float4(1.0, 2.0, 3.0, 4.0);
//对两个失量进行点积操作
float result = dot(a, b);

但在进行矩阵乘法时,参数的位置将决定是按列矩阵还是行矩阵进行乘法。在CG中,矩阵乘法是通过mul函数实现的。例如:
float4 v = float4(1.0, 2.0, 3.0, 4.0);
float4x4 M = float4x4(1.0, 0.0, 0.0, 0.0,
                                   0.0, 1.0, 0.0, 0.0
                                   0.0, 0.0, 1.0, 0.0,
                                   0.0, 0.0, 0.0, 1.0);
//把v当成列矩阵和矩阵M进行右乘
float4 column_mul_result = mul(M, v);
//把v当成行矩阵和矩阵M进行左乘
float4 row_mul_result = mul(v, M);
//注意:column_mul_result不等于row_mul_result,而是
// mul (M, v) == mul (v, tranpose(M))
// mul (v, M) == mul (tranpose(M), v)

        因此,参数的位置会直接影响结果值。通常在变换顶点时,我们都是使用右乘的方式来按列矩阵进行乘法。这是因为,Unity提供的内置矩阵(如UNITY_MATRIX_MVP等)都是按列存储的。但有时,我们也会使用左乘的方式,这是因为可以省去对矩阵转置的操作。
        需要注意的一点是,CG对矩阵类型中元素的初始化和访间顺序。在CG中,对float4x4等类型的变量是按行优先的方式进行填充的。什么意思呢?我们知道,想要填充一个矩阵需要给定一串数学,例如,如果需要声明一个3×4的矩阵,我们需要提供12个数字。那么,这串数字是一行一行地填充矩阵还是一列一列地填充矩阵呢?这两种方式得到的矩阵是不同的。例如,我们使用(1, 2, 3, 4, 5, 6, 7,8, 9)去填充一个3×3的矩阵,如果是按照行优先的方式,得到的矩阵是:

\begin{bmatrix} 1 & 2 &3 \\ 4&5 &6 \\ 7& 8 &9 \end{bmatrix}

如果是按照列优先的方式,得到的矩阵是:

\begin{bmatrix} 1 &4 &7 \\ 2 & 5 &8 \\ 3& 6 &9 \end{bmatrix}

        CG使用的是行优先的方法,即是一行一行地填充矩阵的。因此,如果读者需要自已定义一个矩阵时(例如,自已构建用于空间变换的矩阵),就要注意这里的初始化方式。
        类似地,当我们在CG中访问一个矩阵中的元素时,也是按行来索引的。例如

//按行优先的方式初始化矩阵M
float3x3 M = float3x3(1.0, 2.0, 3.0,
                                   4.0, 5.0, 6.0,
                                   7.0, 8.0, 9.0);
//得到M的第一行,即(1.0, 2.0, 3.0)
float3 row = M[0];
//得到M的第2行第1列的元素,即4.0
float ele = M[1][0];

        之所以Unity Shader中的矩阵类型满足上述规则,是因为使用的是CG语言。换句话说,上
面的特性都是CG的规定。
        如果读者熟悉Unity的API,可能知道Unity在脚本中提供了一种矩阵类型Matrix4x4。脚本中的这个矩阵类型则是采用列优先的方式。这与Unity Shader中的规定不一样,希望读者在遇到时不会感到困惑。

3、Unity中的屏幕坐标:ComputeScreenPos/VPOS/WPOS

        在写 Shader 的过程中,我们有时候希望能够获得片元在屏幕上的像素位置。在顶点/片元看色器中,有两种方式来获得片元的屏幕坐标。
        一种是在片元着色器的输入中声明VPOS或WPOS语义(关于什么是语义,可参见5.4节)VPOS是HLSL中对屏幕坐标的语义,而WPOS 是CG 中对屏幕坐标的语义。两者在Unity Shader
中是等价的。我们可以在HLSL/CG 中通过语义的方式来定义顶点/片元着色器的默认输入,而不
需要目已定义输人输出的数据结构。这里的内容有一些超前,因为我们还没有具体讲解顶点/片元
看色器的写法,读者在这里可以只关注VPOS和 WPOS的语义。使用这种方法,可以在片元着色器中这样写:

fixed4 frag(float4 sp : VPOS) : SV_Target {
        //用屏幕坐标除以屏幕分辨率 ScreenParams.xy,得到视口空间中的坐标
        return fixed4 (sp.xy/_ScreenParams.xy, 0.0, 1.0);
}

另一种方式是通过Unity提供的ComputeScreenPos函数。

Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            
            struct vertOut{
                float4 pos :SV_POSITION;
                float4 scrPos : TEXCOORDO;
            };
            vertOut vert(appdata_base v) {
                vertOut o;
                o.pos = UnityObjectToClipPos (v.vertex);
                //第一步:把ComputeScreenPos 的结果保存到scrPos中
                o.scrPos = ComputeScreenPos (o.pos);
                return o;
            }
            fixed4 frag(vertOut i): SV_Target {
                //第二步:用scrPos.xy除以scrPos.w得到视口空间中的坐标
                float2 wcoord = (i.scrPos.xy/i.scrPos.w);
                return fixed4(wcoord,0.0,1.0);
            }
            ENDCG
        }

效果如下:

将上面的frag改成这样,会得到一个动态效果:

Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }
            fixed4 frag(v2f i): SV_Target {
                fixed3 col = 0.5 + 0.5*cos(_Time.y + i.uv.xyx + fixed3(0,2,4));
                return fixed4(col, 1.0);
            }
            ENDCG
        }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1597282.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

3.Burp Suite 入门篇 —— 修改请求

目录 前言 官网注册 Burp 浏览器访问漏洞页面 登陆购物网站账号 修改请求包 漏洞挖掘 前言 本篇文章会教你如何用 Burp Proxy 修改截获的请求。 修改请求包是为了通过网站程序规定之外的方式请求,然后对比查看响应内容的变化,判断是否有漏洞存在…

Wpf 使用 Prism 实战开发Day20

备忘录功能页面完善以及优化 备忘录功能基本跟前一章节的待办事项差不多一至&#xff0c;就不再做过多的笔述了 一.备忘录功能完整页面源码 MemoView.xaml <UserControl x:Class"MyToDo.Views.MemoView"xmlns"http://schemas.microsoft.com/winfx/2006/xam…

src挖掘 | 未授权访问+密码重置

0x01系统初探 通过fofa对大学进行搜索 fofa:host"edu.cn" && status_code"200" 在随意的翻阅查看时&#xff0c;发现访问xxx.edu.cn登录页面会优先访问登录后的页面&#xff0c;再跳转至登录页面。盲猜应该是前端校验&#xff0c;可以通过抓包拦…

Qt5 编译oracle数据库驱动

库文件 1、Qt源码目录&#xff1a;D:\Qt5\5.15.2\Src\qtbase\src\plugins\sqldrivers\oci 2、oracle客户端SDK: https://www.oracle.com/database/technologies/instant-client/winx64-64-downloads.html 下载各版本中的如下压缩包&#xff0c;一定要版本相同的 将两个压缩包…

【基础物理实验】【AFM虚拟实验】基于AFM的物质表面微观结构及力学性质表征仿真实验(上)【北京航空航天大学】

基于AFM的物质表面微观结构及力学性质表征仿真实验 说明&#xff1a; 本次实验为本科生《基础物理实验》课程中的虚拟实验部分&#xff0c;在虚拟实验平台中进行。 一、实验目的&#xff1a; 1. 掌握AFM的基本成像原理及系统结构&#xff1b; 2. 掌握AFM的基本操作技巧及操…

【Modelsim】保持波形格式重编译and波形的保存与查看

文章目录 保持原波形格式重编译波形的保持与查看保存波形打开工程查看波形 保持原波形格式重编译 Modelsim 仿真设置好波形格式后&#xff0c;若需要修改代码并保持原波形格式重新查看波形&#xff0c;只需将文件重新编译后仿真即可。 1.修改代码后Project页面的代码状态变成…

特氟龙(PFA)实验室器具有哪些?

PFA 是被称为塑料王&#xff0c;具有出众的化学耐受性&#xff0c;并且可在出色的温度范围内执行工作。 PFA 呈半透明&#xff0c;柔韧&#xff0c;并且由于其高密度重量有点重。PFA 具有惰性和低粘合性&#xff0c;溶出物和痕量金属含量较低。它具有较宽的含氟聚合物温度范围…

3.00 版本来了!DolphinDB V2.00.12 V3.00.0 正式发布!

一文带你了解 DolphinDB 全新版本升级&#xff01; 本次更新后&#xff0c;3.00.0版本将成为 DolphinDB 的最新版&#xff0c;2.00.12版本变更为稳定版&#xff0c;此前发布的1.30.23版本将成为1.30系列的最后一个版本。接下来&#xff0c;带大家一起看看 DolphinDB V2.00.12 …

Directory Monitor:全方位监控文件系统变动的专业利器

目录 一、软件介绍 二、软件功能 三、软件特点 四、安装说明 五、使用说明 一、软件介绍 Directory Monitor是一款强大易用的实时文件系统监视工具&#xff0c;它由Michael Humpa开发&#xff0c;专为满足用户监控特定目录下文件和子目录变化的需求。无论是为了保障系统安…

[蓝桥杯 2019 国 B] 解谜游戏

[蓝桥杯 2019 国 B] 解谜游戏 题目背景 题目描述 小明正在玩一款解谜游戏。谜题由 24 24 24 根塑料棒组成&#xff0c;其中黄色塑料棒 4 4 4 根&#xff0c;红色 8 8 8 根&#xff0c;绿色 12 12 12 根 (后面用 Y 表示黄色、R 表示红色、G 表示绿色)。初始时这些塑料棒排…

游戏实践:扫雷

一.游戏介绍 虽然很多人玩过这个游戏&#xff0c;但还是介绍一下。在下面的格子里&#xff0c;埋的有10颗雷&#xff0c;我们通过鼠标点击的方式&#xff0c;点出你认为不是雷的地方&#xff0c;等到把所有没有雷的格子点完之后&#xff0c;及视为游戏胜利。 上面的数字的意思…

租用马来西亚服务器:稳定高效的网络选择

马来西亚首都是吉隆坡。作为一个新兴的多元化经济国家&#xff0c;也属于亚洲四小龙之一。地理位置优越&#xff0c;中间隔着南中国海。一部分是北接泰国的位于马来半岛的西马来西亚&#xff0c;另一部分则是东马来西亚&#xff0c;在婆罗洲岛的北部。这种地理位置有利于促进该…

Qt | 事件第一节(QApplication、QGuiApplication、QCoreApplication)

一、QApplication、QGuiApplication、QCoreApplication 简介 1、继承关系见下图,其中左侧为顶级父类 2、一个程序中只能有一个 QCoreApplication 及其子类的对象。 3、QCoreApplication:主要提供无 GUI 程序的事件循环。 4、QGuiApplication:用于管理 GUI 程序的控制流和…

LangChain学习笔记与样程

LangChain 是一个开源的机器学习工具库&#xff0c;专门用于构建和部署基于语言的应用程序。这个库提供了一系列工具和接口&#xff0c;使开发者能够轻松地整合和使用大型语言模型&#xff0c;例如 OpenAI 提供的 GPT。LangChain 的核心特点包括模块化设计、灵活性和易用性&…

卷积神经网络结构组成与解释

卷积神经网络结构组成与解释 卷积神经网络是以卷积层为主的深度网路结构&#xff0c;网络结构包括有卷积层、激活层、BN层、池化层、FC层、损失层等。卷积操作是对图像和滤波矩阵做内积&#xff08;元素相乘再求和&#xff09;的操作。 1. 卷积层 常见的卷积操作如下&#x…

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单视频处理实战案例 之十一 简单给视频添加水印图片效果

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单视频处理实战案例 之十一 简单给视频添加水印图片效果 目录 Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单视频处理实战案例 之十一 简单给视频添加水印图片效果 一、简单介绍 二、简单给视频添加水印图片效果实现…

Linux——信号量与基于环形队列的生产者消费者模型

目录 前言 一、信号量 二、信号量的接口 1.初始化 2.销毁 3.申请信号量 4. 释放信号量 三、基于环形队列的生产者消费者模型 1.环形队列的理解 2.生产者消费者的设计 3.单消费者单生产者环形队列的实现 4.多消费者多生产者环形队列的实现 前言 之前&#xff0c;…

MGRE中的OSPF配置

一、实验图 二、实验配置 R1 R2 R3 R4 R5 R6

EXCEL中COUNT和COUNTIF的参数类型有什么不同?

来看一下它们的语法&#xff1a; 1.COUNT(值) COUNT函数是计数数字的个数&#xff0c;注意是数值型数字&#xff0c;可不包括文本型数字&#xff0c;它的参数“值”可以是来自单元格的单一区域&#xff0c;如A1:C2&#xff0c;也可以是来自单元格的复合不规则区域&#xff0c…

技术方案应该这么写

简介 新入职一个华为十年工作经验的老Java。让写一个设计方案&#xff0c;其实也不算难&#xff0c;根据业务需要存取三千万数据&#xff0c;三天没写出来&#xff0c;最后做了辞退处理。其实我相信这个老技术员是有能力的&#xff0c;只是没有合适的机会表达。但是也侧面的说明…