目录
1 前言
上一篇我们讲了如何进行立方体的纹理贴图,为了方便立方体的贴图后效果的展示,我们对立方体进行了旋转,但代码中并没有进行体现,这一节我们来讲讲旋转、平移、缩放。实际上,在WebGL中进行旋转、平移、缩放只需要乘以相应的旋转、平移、缩放矩阵即可。
2 二维
在二维空间中,平移、旋转和缩放是常见的几何变换。为了系统地表示这些变换,我们通常使用齐次坐标,即将二维坐标 ( x , y ) (x, y) (x,y) 扩展为三维齐次坐标 ( x , y , 1 ) (x, y, 1) (x,y,1)。这样可以统一表示线性变换和平移变换。下面分别推导这三种变换的矩阵表示。
2.1 平移
定义:将任意一个点 ( x , y ) (x, y) (x,y) 平移 ( t x , t y ) (t_x, t_y) (tx,ty),得到新点 ( x ′ , y ′ ) (x', y') (x′,y′),即
{ x ′ = x + t x y ′ = y + t y \begin{cases} x' = x + t_x \\ y' = y + t_y \end{cases} {x′=x+txy′=y+ty
齐次坐标表示:
[
x
′
y
′
1
]
=
[
1
0
t
x
0
1
t
y
0
0
1
]
[
x
y
1
]
\begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix}=\begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix}\begin{bmatrix} x \\ y \\ 1 \end{bmatrix}
x′y′1
=
100010txty1
xy1
因此,二维平移矩阵为:
T = [ 1 0 t x 0 1 t y 0 0 1 ] T = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} T= 100010txty1
2.2 旋转
定义:将任意一个点 ( x , y ) (x, y) (x,y) 绕原点逆时针旋转角度 θ \theta θ,得到新点 ( x ′ , y ′ ) (x', y') (x′,y′),即
{ x ′ = x cos θ − y sin θ y ′ = x sin θ + y cos θ \begin{cases} x' = x \cos\theta - y \sin\theta \\ y' = x \sin\theta + y \cos\theta \end{cases} {x′=xcosθ−ysinθy′=xsinθ+ycosθ
齐次坐标表示:
[
x
′
y
′
1
]
=
[
cos
θ
−
sin
θ
0
sin
θ
cos
θ
0
0
0
1
]
[
x
y
1
]
\begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} =\begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}
x′y′1
=
cosθsinθ0−sinθcosθ0001
xy1
因此,二维旋转矩阵为:
R ( θ ) = [ cos θ − sin θ 0 sin θ cos θ 0 0 0 1 ] R(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix} R(θ)= cosθsinθ0−sinθcosθ0001
2.3 缩放
定义:将任意一个点 ( x , y ) (x, y) (x,y) 沿 x x x 轴缩放因子 s x s_x sx,沿 y y y 轴缩放因子 s y s_y sy,得到新点 ( x ′ , y ′ ) (x', y') (x′,y′),即
{ x ′ = s x ⋅ x y ′ = s y ⋅ y \begin{cases} x' = s_x \cdot x \\ y' = s_y \cdot y \end{cases} {x′=sx⋅xy′=sy⋅y
齐次坐标表示:
[ x ′ y ′ 1 ] = [ s x 0 0 0 s y 0 0 0 1 ] [ x y 1 ] \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} =\begin{bmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} x′y′1 = sx000sy0001 xy1
因此,二维缩放矩阵为:
S = [ s x 0 0 0 s y 0 0 0 1 ] S = \begin{bmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{bmatrix} S= sx000sy0001
3 三维
在三维空间中,平移、旋转和缩放是类似的,我们通常也使用齐次坐标,即将三维坐标 ( x , y , z ) (x, y, z) (x,y,z) 扩展为四维齐次坐标 ( x , y , z , 1 ) (x, y, z, 1) (x,y,z,1)。这样可以统一表示线性变换和平移变换。下面分别推导这三种变换的矩阵表示。
3.1 平移
定义:将任意一个点 ( x , y , z ) (x, y, z) (x,y,z) 平移 ( t x , t y , t z ) (t_x, t_y, t_z) (tx,ty,tz),得到新点 ( x ′ , y ′ , z ′ ) (x', y', z') (x′,y′,z′),即
{ x ′ = x + t x y ′ = y + t y z ′ = z + t z \begin{cases} x' = x + t_x \\ y' = y + t_y \\ z' = z + t_z \end{cases} ⎩ ⎨ ⎧x′=x+txy′=y+tyz′=z+tz
齐次坐标表示:
[ x ′ y ′ z ′ 1 ] = [ 1 0 0 t x 0 1 0 t y 0 0 1 t z 0 0 0 1 ] [ x y z 1 ] \begin{bmatrix} x' \\ y' \\ 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 \\ 1 \end{bmatrix} x′y′z′1 = 100001000010txtytz1 xyz1
因此,三维平移矩阵为:
T
=
[
1
0
0
t
x
0
1
0
t
y
0
0
1
t
z
0
0
0
1
]
T = \begin{bmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{bmatrix}
T=
100001000010txtytz1
3.2 旋转
在三维空间中,旋转可以绕任意一个坐标轴进行。下面分别给出绕
X
X
X 轴、
Y
Y
Y 轴和
Z
Z
Z 轴旋转角度
θ
\theta
θ 的旋转矩阵。在三维空间中的旋转看着复杂其实很简单,因为绕X轴旋转其实X的值并不会产生变化,只有Y和Z的值会产生变化,那不就相当于在二维空间中的旋转吗?是的,我们依次来讨论。
3.2.1 绕 X X X 轴旋转
定义:将任意一个点 ( x , y , z ) (x, y, z) (x,y,z) 绕 X X X 轴逆时针旋转角度 θ \theta θ,得到新点 ( x ′ , y ′ , z ′ ) (x', y', z') (x′,y′,z′),x的大小其实是不动的,y和z的值和二维平面类似,即
{ x ′ = x y ′ = y cos θ − z sin θ z ′ = y sin θ + z cos θ \begin{cases} x' = x \\ y' = y \cos\theta - z \sin\theta \\ z' = y \sin\theta + z \cos\theta \end{cases} ⎩ ⎨ ⎧x′=xy′=ycosθ−zsinθz′=ysinθ+zcosθ
齐次坐标表示:
R x ( θ ) = [ 1 0 0 0 0 cos θ − sin θ 0 0 sin θ cos θ 0 0 0 0 1 ] 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} Rx(θ)= 10000cosθsinθ00−sinθcosθ00001
3.2.2 绕 Y Y Y 轴旋转
定义:将任意一个点 ( x , y , z ) (x, y, z) (x,y,z) 绕 Y Y Y 轴逆时针旋转角度 θ \theta θ,得到新点 ( x ′ , y ′ , z ′ ) (x', y', z') (x′,y′,z′),y值也是不动的,x和z的值与二维平面类似,即
{ x ′ = x cos θ + z sin θ y ′ = y z ′ = − x sin θ + z cos θ \begin{cases} x' = x \cos\theta + z \sin\theta \\ y' = y \\ z' = -x \sin\theta + z \cos\theta \end{cases} ⎩ ⎨ ⎧x′=xcosθ+zsinθy′=yz′=−xsinθ+zcosθ
齐次坐标表示:
R y ( θ ) = [ cos θ 0 sin θ 0 0 1 0 0 − sin θ 0 cos θ 0 0 0 0 1 ] 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} Ry(θ)= cosθ0−sinθ00100sinθ0cosθ00001
3.2.3 绕 Z Z Z 轴旋转
定义:将一个点 ( x , y , z ) (x, y, z) (x,y,z) 绕 Z Z Z 轴逆时针旋转角度 θ \theta θ,得到新点 ( x ′ , y ′ , z ′ ) (x', y', z') (x′,y′,z′),z值是不变的,x和y的值与二维平面类似,即
{ x ′ = x cos θ − y sin θ y ′ = x sin θ + y cos θ z ′ = z \begin{cases} x' = x \cos\theta - y \sin\theta \\ y' = x \sin\theta + y \cos\theta \\ z' = z \end{cases} ⎩ ⎨ ⎧x′=xcosθ−ysinθy′=xsinθ+ycosθz′=z
齐次坐标表示:
R z ( θ ) = [ cos θ − sin θ 0 0 sin θ cos θ 0 0 0 0 1 0 0 0 0 1 ] R_z(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta & 0 & 0 \\ \sin\theta & \cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} Rz(θ)= cosθsinθ00−sinθcosθ0000100001
因此,三维旋转矩阵可以根据旋转轴选择对应的 R x ( θ ) R_x(\theta) Rx(θ)、 R y ( θ ) R_y(\theta) Ry(θ) 或 R z ( θ ) R_z(\theta) Rz(θ)。
3.2.4 绕任意轴旋转
怎么表示任意轴呢?用一个向量便可以表示任意轴。那怎么绕任意轴旋转呢?答案是先将物体平移到原点去,分解为分别绕XYZ的旋转,而绕这三个轴旋转的角度就被叫做欧拉角,旋转完毕后再平移到原来的地方就可以了。这个思想也就是著名的罗德里格斯旋转公式所采用的思想。
设旋转轴为单位向量 u = ( u x , u y , u z ) \mathbf{u} = (u_x, u_y, u_z) u=(ux,uy,uz),旋转角度为 θ \theta θ。为了确保计算的准确性,旋转轴必须为单位向量,即:
u
x
2
+
u
y
2
+
u
z
2
=
1
u_x^2 + u_y^2 + u_z^2 = 1
ux2+uy2+uz2=1
罗德里格斯旋转公式提供了一种通过旋转轴和旋转角度来计算旋转矩阵的方法。旋转矩阵
R
\mathbf{R}
R 可以表示为:
R = I cos θ + ( 1 − cos θ ) u u T + K sin θ \mathbf{R} = \mathbf{I} \cos\theta + (1 - \cos\theta) \mathbf{u} \mathbf{u}^T + \mathbf{K} \sin\theta R=Icosθ+(1−cosθ)uuT+Ksinθ
其中:
- I \mathbf{I} I 是 3 × 3 3 \times 3 3×3 的单位矩阵。
- u u T \mathbf{u} \mathbf{u}^T uuT 是旋转轴的外积矩阵。
- K \mathbf{K} K 是旋转轴的反对称矩阵,定义为:
K
=
[
0
−
u
z
u
y
u
z
0
−
u
x
−
u
y
u
x
0
]
\mathbf{K} = \begin{bmatrix} 0 & -u_z & u_y \\ u_z & 0 & -u_x \\ -u_y & u_x & 0 \end{bmatrix}
K=
0uz−uy−uz0uxuy−ux0
结合上述推导,绕任意单位向量
u
=
(
u
x
,
u
y
,
u
z
)
\mathbf{u} = (u_x, u_y, u_z)
u=(ux,uy,uz) 旋转角度
θ
\theta
θ 的旋转矩阵
R
\mathbf{R}
R 为:
R = [ cos θ + u x 2 ( 1 − cos θ ) u x u y ( 1 − cos θ ) − u z sin θ u x u z ( 1 − cos θ ) + u y sin θ u y u x ( 1 − cos θ ) + u z sin θ cos θ + u y 2 ( 1 − cos θ ) u y u z ( 1 − cos θ ) − u x sin θ u z u x ( 1 − cos θ ) − u y sin θ u z u y ( 1 − cos θ ) + u x sin θ cos θ + u z 2 ( 1 − cos θ ) ] \mathbf{R} = \begin{bmatrix} \cos\theta + u_x^2 (1 - \cos\theta) & u_x u_y (1 - \cos\theta) - u_z \sin\theta & u_x u_z (1 - \cos\theta) + u_y \sin\theta \\ u_y u_x (1 - \cos\theta) + u_z \sin\theta & \cos\theta + u_y^2 (1 - \cos\theta) & u_y u_z (1 - \cos\theta) - u_x \sin\theta \\ u_z u_x (1 - \cos\theta) - u_y \sin\theta & u_z u_y (1 - \cos\theta) + u_x \sin\theta & \cos\theta + u_z^2 (1 - \cos\theta) \end{bmatrix} R= cosθ+ux2(1−cosθ)uyux(1−cosθ)+uzsinθuzux(1−cosθ)−uysinθuxuy(1−cosθ)−uzsinθcosθ+uy2(1−cosθ)uzuy(1−cosθ)+uxsinθuxuz(1−cosθ)+uysinθuyuz(1−cosθ)−uxsinθcosθ+uz2(1−cosθ)
3.3 缩放
定义:将一个点 ( x , y , z ) (x, y, z) (x,y,z) 沿 x x x 轴缩放因子 s x s_x sx,沿 y y y 轴缩放因子 s y s_y sy,沿 z z z 轴缩放因子 s z s_z sz,得到新点 ( x ′ , y ′ , z ′ ) (x', y', z') (x′,y′,z′),即
{ x ′ = s x ⋅ x y ′ = s y ⋅ y z ′ = s z ⋅ z \begin{cases} x' = s_x \cdot x \\ y' = s_y \cdot y \\ z' = s_z \cdot z \end{cases} ⎩ ⎨ ⎧x′=sx⋅xy′=sy⋅yz′=sz⋅z
齐次坐标表示:
S = [ s x 0 0 0 0 s y 0 0 0 0 s z 0 0 0 0 1 ] S = \begin{bmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} S= sx0000sy0000sz00001
4 WebGL中怎么实现旋转、平移、缩放
OK.终于讲完理论了,现在我们来讲下实操。通过上面的讲解,大家应该明白了,要旋转一个立方体,只要把立方体中的每一个点都乘以一个旋转矩阵,就可以了。类似的,要实现平移,只要把立方体中的每一个点都乘以一个平移矩阵就可以了。我们以旋转立方体为例。
4.1 声明顶点着色器和片元着色器
<script id="vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_Position;
attribute vec4 a_Color;
uniform mat4 u_RorateMatrix;
varying vec4 v_Color;
void main(){
//给所有点都乘以一个旋转矩阵
gl_Position = uRotateMatrix * a_Position;
v_Color = a_Color;
v_TexCoord = a_TexCoord;
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
precision highp float;
varying vec4 v_Color;
void main(){
gl_FragColor = v_Color;
}
</script>
4.2 计算旋转矩阵
计算旋转矩阵的类库太多了,比如gl-Matrix、Cesiumjs、Threejs、Babylonjs
都可以实现。本文中使用的是WebGL
编程指南中自带的方法,只要调用时,指明要旋转的角度和要绕哪个轴旋转即可。
var modelMatrix = new Matrix4();
//绕Y轴【0,1,0】旋转60度
modelMatrix.setRotate(60,0,1,0);
//赋值给u_RorateMatrix
var u_RorateMatrix= gl.getUniformLocation(program, 'u_RorateMatrix');
//false表示不使用归一化
gl.uniformMatrix4fv(u_RorateMatrix,false,modelMatrix.elements);
旋转方法源码如下:
/**
* Set the matrix for rotation.
* The vector of rotation axis may not be normalized.
* @param angle The angle of rotation (degrees)
* @param x The X coordinate of vector of rotation axis.
* @param y The Y coordinate of vector of rotation axis.
* @param z The Z coordinate of vector of rotation axis.
* @return this
*/
Matrix4.prototype.setRotate = function(angle, x, y, z) {
var e, s, c, len, rlen, nc, xy, yz, zx, xs, ys, zs;
angle = Math.PI * angle / 180;
e = this.elements;
s = Math.sin(angle);
c = Math.cos(angle);
if (0 !== x && 0 === y && 0 === z) {
// Rotation around X axis
if (x < 0) {
s = -s;
}
e[0] = 1; e[4] = 0; e[ 8] = 0; e[12] = 0;
e[1] = 0; e[5] = c; e[ 9] =-s; e[13] = 0;
e[2] = 0; e[6] = s; e[10] = c; e[14] = 0;
e[3] = 0; e[7] = 0; e[11] = 0; e[15] = 1;
} else if (0 === x && 0 !== y && 0 === z) {
// Rotation around Y axis
if (y < 0) {
s = -s;
}
e[0] = c; e[4] = 0; e[ 8] = s; e[12] = 0;
e[1] = 0; e[5] = 1; e[ 9] = 0; e[13] = 0;
e[2] =-s; e[6] = 0; e[10] = c; e[14] = 0;
e[3] = 0; e[7] = 0; e[11] = 0; e[15] = 1;
} else if (0 === x && 0 === y && 0 !== z) {
// Rotation around Z axis
if (z < 0) {
s = -s;
}
e[0] = c; e[4] =-s; e[ 8] = 0; e[12] = 0;
e[1] = s; e[5] = c; e[ 9] = 0; e[13] = 0;
e[2] = 0; e[6] = 0; e[10] = 1; e[14] = 0;
e[3] = 0; e[7] = 0; e[11] = 0; e[15] = 1;
} else {
// Rotation around another axis
len = Math.sqrt(x*x + y*y + z*z);
if (len !== 1) {
rlen = 1 / len;
x *= rlen;
y *= rlen;
z *= rlen;
}
nc = 1 - c;
xy = x * y;
yz = y * z;
zx = z * x;
xs = x * s;
ys = y * s;
zs = z * s;
e[ 0] = x*x*nc + c;
e[ 1] = xy *nc + zs;
e[ 2] = zx *nc - ys;
e[ 3] = 0;
e[ 4] = xy *nc - zs;
e[ 5] = y*y*nc + c;
e[ 6] = yz *nc + xs;
e[ 7] = 0;
e[ 8] = zx *nc + ys;
e[ 9] = yz *nc - xs;
e[10] = z*z*nc + c;
e[11] = 0;
e[12] = 0;
e[13] = 0;
e[14] = 0;
e[15] = 1;
}
return this;
};
4.3 绘制立方体并进行旋转完整代码
const verticesColors = new Float32Array([
// 前面
-1.0, -1.0, 1.0, 1.0, 0.0,1.0,//v2 红色
1.0, -1.0, 1.0, 1.0, 0.0,1.0,//v3 红色
1.0, 1.0, 1.0, 1.0, 0.0,1.0,//v0 红色
-1.0, 1.0, 1.0, 1.0, 0.0,1.0,//v1 红色
// 后面
-1.0, -1.0, -1.0, 0.0, 1.0, 0.0,//v5 绿色
1.0, -1.0, -1.0, 0.0, 1.0, 0.0,//v4 绿色
1.0, 1.0, -1.0, 0.0, 1.0, 0.0,//v7 绿色
-1.0, 1.0, -1.0, 0.0, 1.0, 0.0,//v6 绿色
// 上面
-1.0, 1.0, 1.0, 0.0, 0.0,1.0,//v1 蓝色
1.0, 1.0, 1.0, 0.0, 0.0,1.0,//v0 蓝色
1.0, 1.0, -1.0, 0.0, 0.0,1.0,//v7 蓝色
-1.0, 1.0, -1.0, 0.0, 0.0,1.0,//v6 蓝色
// 下面
-1.0, -1.0, 1.0, 0.0, 0.0,0.0,//v2 黑色
1.0, -1.0, 1.0, 0.0, 0.0,0.0,//v3 黑色
1.0, -1.0, -1.0, 0.0, 0.0,0.0,//v4 黑色
-1.0, -1.0, -1.0, 0.0, 0.0,0.0,//v5 黑色
// 左面
-1.0, -1.0, -1.0, 0.0, 1.0,1.0,//v5 青色
-1.0, -1.0, 1.0, 0.0, 1.0,1.0,//v2 青色
-1.0, 1.0, 1.0, 0.0, 1.0,1.0,//v1 青色
-1.0, 1.0, -1.0, 0.0, 1.0,1.0,//v6 青色
// 右面
1.0, -1.0, 1.0, 1.0, 1.0,1.0,//v3 白色
1.0, -1.0, -1.0, 1.0, 1.0,1.0,//v4 白色
1.0, 1.0, -1.0, 1.0, 1.0,1.0,//v7 白色
1.0, 1.0, 1.0, 1.0, 1.0,1.0,//v0 白色
]);
const indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // 前面
4, 5, 6, 4, 6, 7, // 后面
8, 9, 10, 8, 10, 11, // 上面
12, 13, 14, 12, 14, 15, // 下面
16, 17, 18, 16, 18, 19, // 左面
20, 21, 22, 20, 22, 23 // 右面
]);
//使webgl视口和canvas画板一样大
gl.viewport(0, 0, canvas.width, canvas.height);
//开启深度检测
gl.enable(gl.DEPTH_TEST);
//顶点
let vertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,vertexColorBuffer);
gl.bufferData(gl.ARRAY_BUFFER,verticesColors,gl.STATIC_DRAW);
let FSIZE = verticesColors.BYTES_PER_ELEMENT;
let a_Position = gl.getAttribLocation(program,'a_Position');
gl.vertexAttribPointer(a_Position,3,gl.FLOAT,false,FSIZE*6,0);
gl.enableVertexAttribArray(a_Position);
//颜色
var a_Color = gl.getAttribLocation(program, 'a_Color');
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
gl.enableVertexAttribArray(a_Color);
//旋转
var modelMatrix = new Matrix4();
modelMatrix.setRotate(60,0,1,0);
//赋值给uRotateMatrix
var u_RorateMatrix= gl.getUniformLocation(program, 'u_RorateMatrix');
gl.uniformMatrix4fv(u_RorateMatrix,false,modelMatrix .elements);
//绑定索引缓冲
let indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,indices,gl.STATIC_DRAW);
gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_BYTE, 0);
4.4 效果
5 总结
本文我们通过梳理二维及三维的旋转、平移、缩放矩阵,搞明白了旋转、平移、缩放对应的矩阵,并在WebGL
中进行了实验操作,实现了一个立方体的旋转。本文在理解上有一定的难度,尤其是矩阵的变换,需要读者具有一定的线性代数知识,希望读者仔细体会,回见~