当前位置: 首页 > news >正文

tinyrenderer笔记(中)

  • tinyrenderer
  • 个人代码仓库:tinyrenderer个人练习代码

前言

原教程的第 4 课与第 5 课主要介绍了坐标变换的一些知识点,但这一篇文章的内容主要是手动构建 MVP 矩阵,LookAt 矩阵以及原教程涉及到的一些知识点,不是从一个图形学小白的角度来描述。如果读者没有系统上过图形学的课程,还是建议查阅原教程,以及这篇文章:从零构建光栅器,tinyrenderer笔记(下) - 知乎。

games101:

  • Lecture 03 Transformation
  • Lecture 04 Transformation Cont

本篇使用的一切向量/矩阵规范类比 OpenGL。

齐次坐标

表示一个三维空间的点 p p p 只需要三个分量: ( x , y , z ) (x,y,z) (x,y,z),齐次坐标只是将 3 维扩展成了 4 维 ( x , y , z , w ) (x,y,z,w) (x,y,z,w)。这样做的主要目的是让平移操作也能用矩阵乘法实现,与其它几何变换(旋转、缩放)统一。

将齐次坐标转化为普通笛卡尔三维坐标很简单,将 x x x y y y z z z 分量都除以 w w w 即可: ( x / w , y / w , z / w ) (x/w, y/w, z/w) (x/w,y/w,z/w)。当 w = 0 w=0 w=0 时,你会发现坐标分量无穷大,这可以代表无穷远处的点,也可以代表一个向量。

我们来模仿 OpenGl 的 glm 库,实现各种矩阵变换函数的内部细节。万丈高楼平地起,为了表示后续的矩阵,我在代码中创建了一个模板矩阵类 mat, 它位于 glm.h 下,实现了一些基本的矩阵运算:乘法、求逆、转置。

平移

有了齐次坐标之后,平移的矩阵表示非常简单:设平移向量为 ( t x , t y , t z ) (t_x, t_y, t_z) (tx,ty,tz),平移变换矩阵:

[ 1 0 0 t x 0 1 0 t y 0 0 1 t z 0 0 0 1 ] [ x y z 1 ] = [ x + t x y + t y z + t z 1 ] \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} 100001000010txtytz1 xyz1 = x+txy+tyz+tz1

glm 的 translate 函数:

translatedMatrix = glm::translate(matrix, vector);

第一个参数为要变换的矩阵,第二个参数为平移向量,这个函数的实现非常简单,不多赘述:

mat4 translate(const mat4& matrix, const Vec3f& v)
{glm::mat4 translateMatrix;for (int i = 0;i < 3;i++){translateMatrix[i][3] = v[i];}return matrix * translateMatrix;
}

缩放

设缩放因子 ( s x , s y , s z ) (s_x, s_y, s_z) (sx,sy,sz),缩放矩阵:

[ s x 0 0 0 0 s y 0 0 0 0 s z 0 0 0 0 1 ] [ x y z 1 ] = [ x ∗ s x y ∗ s y z ∗ s z 1 ] \begin{bmatrix}s_x&0&0&0\\0&s_y&0&0\\0&0&s_z&0\\0&0&0&1\end{bmatrix}\begin{bmatrix}x\\y\\z\\1\end{bmatrix}=\begin{bmatrix}x*s_x\\y*s_y\\z*s_z\\1\end{bmatrix} sx0000sy0000sz00001 xyz1 = xsxysyzsz1

glm 的 scale 函数:

scaledMatrix = glm::scale(matrix, vector);

模仿实现:

mat4 scale(const mat4& matrix, const Vec3f& v)
{mat4 scaleMatrix;for (int i = 0;i < 3;i++){scaleMatrix[i][i] = v[i];}return matrix * scaleMatrix;
}

旋转

三维空间下,绕任意轴 ( R x , R y , R z ) (R_x,R_y,R_z) (Rx,Ry,Rz) 旋转 θ \theta θ 的旋转矩阵要复杂许多:
[ cos ⁡ θ + R x 2 ( 1 − cos ⁡ θ ) R x R y ( 1 − cos ⁡ θ ) − R z sin ⁡ θ R x R z ( 1 − cos ⁡ θ ) + R y sin ⁡ θ 0 R y R x ( 1 − cos ⁡ θ ) + R z sin ⁡ θ cos ⁡ θ + R y 2 ( 1 − cos ⁡ θ ) R y R z ( 1 − cos ⁡ θ ) − R x sin ⁡ θ 0 R z R x ( 1 − cos ⁡ θ ) − R y sin ⁡ θ R z R y ( 1 − cos ⁡ θ ) + R x sin ⁡ θ cos ⁡ θ + R z 2 ( 1 − cos ⁡ θ ) 0 0 0 0 1 ] \begin{bmatrix} \cos\theta + R_x^{2}(1 - \cos\theta) & R_xR_y(1 - \cos\theta) - R_z\sin\theta & R_xR_z(1 - \cos\theta) + R_y\sin\theta & 0 \\ R_yR_x(1 - \cos\theta) + R_z\sin\theta & \cos\theta + R_y^{2}(1 - \cos\theta) & R_yR_z(1 - \cos\theta) - R_x\sin\theta & 0 \\ R_zR_x(1 - \cos\theta) - R_y\sin\theta & R_zR_y(1 - \cos\theta) + R_x\sin\theta & \cos\theta + R_z^{2}(1 - \cos\theta) & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} cosθ+Rx2(1cosθ)RyRx(1cosθ)+RzsinθRzRx(1cosθ)Rysinθ0RxRy(1cosθ)Rzsinθcosθ+Ry2(1cosθ)RzRy(1cosθ)+Rxsinθ0RxRz(1cosθ)+RysinθRyRz(1cosθ)Rxsinθcosθ+Rz2(1cosθ)00001

glm 的 rotate 函数:

rotatedMatrix = glm::rotate(matrix, angle, axis);

模仿实现:

mat4 rotate(const mat4& matrix, float angle, const Vec3f& axis) 
{// 将角度转换为弧度float rad = radians(angle);float cosTheta = cos(rad);float sinTheta = sin(rad);float oneMinusCos = 1.0f - cosTheta;// 归一化旋转轴Vec3f normalizedAxis = axis.normalized();float rx = normalizedAxis.x;float ry = normalizedAxis.y;float rz = normalizedAxis.z;mat4 rotateMatrix;// 第一列rotateMatrix[0][0] = cosTheta + rx * rx * oneMinusCos;rotateMatrix[0][1] = rx * ry * oneMinusCos + rz * sinTheta;rotateMatrix[0][2] = rx * rz * oneMinusCos - ry * sinTheta;// 第二列rotateMatrix[1][0] = rx * ry * oneMinusCos - rz * sinTheta;rotateMatrix[1][1] = cosTheta + ry * ry * oneMinusCos;rotateMatrix[1][2] = ry * rz * oneMinusCos + rx * sinTheta;// 第三列rotateMatrix[2][0] = rx * rz * oneMinusCos + ry * sinTheta;rotateMatrix[2][1] = ry * rz * oneMinusCos - rx * sinTheta;rotateMatrix[2][2] = cosTheta + rz * rz * oneMinusCos;return matrix * rotateMatrix;
}

模型矩阵

模型矩阵用于将物体的坐标空间从局部空间转换到世界空间下,若你还对各种坐标空间傻傻分不清楚,推荐观看 LearnOpenGL 的这篇教程:坐标系统 - LearnOpenGL CN

我们从 obj 文件直接读取的顶点坐标就是位于局部空间下的,之前我们只渲染了一个模型,并希望它位于屏幕的中央,所以区分局部空间与世界空间并没有多大意义。现在让我们渲染两个模型,一个在左边、一个在右边,右边的模型我们通过之前实现的变换矩阵将其缩小一倍,并且向右看,左边的同样缩小一倍,并朝左看。

首先创建一个 drawModel 函数,用于渲染特定的模型:

void drawModel(TGAImage& image, TGAImage& tex, Vec3f light_dir, float* zbuffer, Model* model, glm::mat4 modelMatrix)
{for (int i = 0; i < model->nfaces(); i++){std::vector<int> face_v = model->face_v(i);std::vector<int> face_vt = model->face_vt(i);VertexInfo vertex[3];for (int j = 0; j < 3; j++){vertex[j].textureCoor = model->textureCoor(face_vt[j]);// 得到局部空间坐标Vec3f model_coords = model->vert(face_v[j]);// 局部空间坐标转化为世界空间坐标Vec3f world_coords = modelMatrix * Vec4f(model_coords, 1.f);vertex[j].location = Vec3f((world_coords.x + 1.0) * width / 2.0, (world_coords.y + 1.0) * height / 2.0, world_coords.z);}Vec3f n = (model->vert(face_v[1]) - model->vert(face_v[0])) ^ (model->vert(face_v[2]) - model->vert(face_v[0]));n = modelMatrix.inverse().transpose().to_mat3() * n;n.normalize();float intensity = -(n * light_dir);if (intensity > 0){triangle(vertex[0], vertex[1], vertex[2], zbuffer, image, tex, intensity);}}
}

代码与之前并没有什么不同,只是多了一步,将局部空间坐标转换到世界空间,法线同样需要变换,但这里涉及到另外一个知识:法线矩阵,留到后面讲解。

在 main 函数里面:

glm::mat4 modelMatrix;
modelMatrix = glm::translate(modelMatrix, Vec3f(-0.5, 0, 0));
modelMatrix = glm::rotate(modelMatrix, 45.f, Vec3f(0, 1, 0));
modelMatrix = glm::scale(modelMatrix, Vec3f(0.5, 0.5, 0.5));
drawModel(result, texture, light_dir, zbuffer, model, modelMatrix);
modelMatrix = glm::mat4::identity();
modelMatrix = glm::translate(modelMatrix, Vec3f(0.5, 0, 0));
modelMatrix = glm::rotate(modelMatrix, -45.f, Vec3f(0, 1, 0));
modelMatrix = glm::scale(modelMatrix, Vec3f(0.5, 0.5, 0.5));
drawModel(result, texture, light_dir, zbuffer, model, modelMatrix);

结果:

image.png

视图矩阵

视图矩阵又称为观察矩阵,用于将世界空间坐标转化为视图空间(又称为摄像机空间、观察空间)坐标。在视图空间下,摄像机位于 ( 0 , 0 , 0 ) (0,0,0) (0,0,0),并向 ( 0 , 0 , − 1 ) (0,0,-1) (0,0,1) 方向观察,因此视图空间就是从摄像机的视角所观察到的空间。为什么会存在这样一个坐标转换步骤呢?这其实是为了后续的投影与裁剪步骤。

三个相互垂直(或线性无关)的轴定义了一个坐标空间,我们可以利用这三个轴以及一个原点位置来构建一个变换矩阵。使用该矩阵乘以任何向量,就可以将该向量从一个坐标空间变换到新定义的坐标空间。LookAt 矩阵正是利用了这个特性。

LookAt

假设我们已经拥有了构成摄像机局部坐标系(视图空间)的三个相互垂直的轴:右向量( R R R)、上向量( U U U)和方向向量( D D D),以及定义摄像机在世界空间中位置的 P P P,我们可以构建自己的 LookAt 矩阵:

L o o k A t = [ R x R y R z 0 U x U y U z 0 − D x − D y − D z 0 0 0 0 1 ] ∗ [ 1 0 0 − P x 0 1 0 − P y 0 0 1 − P z 0 0 0 1 ] \begin{gathered}LookAt=\begin{bmatrix}R_x&R_y&R_z&0\\U_x&U_y&U_z&0\\-D_x&-D_y&-D_z&0\\0&0&0&1\end{bmatrix}*\begin{bmatrix}1&0&0&-P_x\\0&1&0&-P_y\\0&0&1&-P_z\\0&0&0&1\end{bmatrix}\end{gathered} LookAt= RxUxDx0RyUyDy0RzUzDz00001 100001000010PxPyPz1

其中:

  • R R R: 右向量(Right Vector),表示摄像机局部坐标系的 x x x 轴正方向。
  • U U U: 上向量(Up Vector),表示摄像机局部坐标系的 y y y 轴正方向。
  • D D D: 方向向量(Direction Vector),表示摄像机局部坐标系的负 z z z 轴方向(摄像机实际观察的方向)。
  • P P P: 摄像机在世界空间中的位置(Camera Position)。

glm 的 lookAt 函数:

view = glm::lookAt(cameraPosition, targetPosition, Up);

模仿实现:

mat4 lookAt(const Vec3f& eye, const Vec3f& center, const Vec3f& up)
{Vec3f D = (center - eye).normalized();Vec3f R = D.cross(up).normalized();Vec3f U = R.cross(D);mat4 viewMatrix;viewMatrix[0][0] = R.x;viewMatrix[0][1] = R.y;viewMatrix[0][2] = R.z;viewMatrix[1][0] = U.x;viewMatrix[1][1] = U.y;viewMatrix[1][2] = U.z;viewMatrix[2][0] = -D.x;viewMatrix[2][1] = -D.y;viewMatrix[2][2] = -D.z;viewMatrix[0][3] = -R.dot(eye);viewMatrix[1][3] = -U.dot(eye);viewMatrix[2][3] = D.dot(eye);return viewMatrix;
}

假设摄像机位于 ( 0 , 0 , − 2 ) (0,0,-2) (0,0,2),并向 ( 0 , 0 , 0 ) (0,0,0) (0,0,0) 看过去,则我们可以利用 lookAt 创建一个视图矩阵:

glm::mat4 viewMatrix = glm::lookAt(Vec3f(0, 0, -2), Vec3f(0, 0, 0), Vec3f(0, 1, 0));

将其传参到 drawModel 函数里,并应用起来:

// 世界空间坐标转化为视图空间坐标
Vec4f view_coords = viewMatrix * world_coords;
vertex[j].location = Vec3f((view_coords.x + 1.0) * width / 2.0, (view_coords.y + 1.0) * height / 2.0, view_coords.z);

别忘了将光照方向改为 ( 0 , 0 , 1 ) (0,0,1) (0,0,1) ,让模型的背面被照亮:

Vec3f light_dir(0, 0, 1);

image.png

投影矩阵

投影矩阵会将视图空间坐标转化为裁剪空间(Clip Space)坐标,这是将 3 维空间投影到 2 维空间的关键步骤。参考这篇文章:图形学基础 - 变换 - 投影 - 知乎
投影方式分为两种:正交投影与透视投影。

正交投影

通过平行投影将物体投射到屏幕上,所有光线平行于摄像机方向,不考虑距离对物体大小的影响 (即远近物体的尺寸保持一致)。

image.png

正交投影过程可概括为:

用六个参数 ( l , r , b , t , n , f ) (l,r,b,t,n,f) (l,r,b,t,n,f) 定义一个 AABB 包围盒,然后将这个包围盒变换到规范视域体(OpenGL 里是 ( − 1 , − 1 , − 1 ) (-1,-1,-1) (1,1,1) ( 1 , 1 , 1 ) (1,1,1) (1,1,1) )。即从一个长方体变换到另一个定向与之相同的长方体,所以只涉及到平移和缩放操作。

第一步先将定义的包围盒移动到原点:

( 1 0 0 − l + r 2 0 1 0 − b + t 2 0 0 1 − n + f 2 0 0 0 1 ) \begin{pmatrix} 1 & 0 & 0 & -\frac{l+r}{2} \\ 0 & 1 & 0 & -\frac{b+t}{2} \\ 0 & 0 & 1 & -\frac{n+f}{2} \\ 0 & 0 & 0 & 1 \end{pmatrix} 1000010000102l+r2b+t2n+f1

然后将包围盒各边缩放到 1:

( 2 r − l 0 0 0 0 2 t − b 0 0 0 0 2 n − f 0 0 0 0 1 ) ∗ ( 1 0 0 − l + r 2 0 1 0 − b + t 2 0 0 1 − n + f 2 0 0 0 1 ) = ( 2 r − l 0 0 l + r l − r 0 2 t − b 0 b + t b − t 0 0 2 f − n n + f n − f 0 0 0 1 ) \begin{pmatrix} \frac{2}{r-l} & 0 & 0 & 0 \\ 0 & \frac{2}{t-b} & 0 & 0 \\ 0 & 0 & \frac{2}{n-f} & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} *\begin{pmatrix} 1 & 0 & 0 & -\frac{l+r}{2} \\ 0 & 1 & 0 & -\frac{b+t}{2} \\ 0 & 0 & 1 & -\frac{n+f}{2} \\ 0 & 0 & 0 & 1 \end{pmatrix} = \begin{pmatrix} \frac{2}{r - l} & 0 & 0 & \frac{l + r}{l - r} \\ 0 & \frac{2}{t - b} & 0 & \frac{b + t}{b - t} \\ 0 & 0 & \frac{2}{f - n} & \frac{n + f}{n - f} \\ 0 & 0 & 0 & 1 \end{pmatrix} rl20000tb20000nf200001 1000010000102l+r2b+t2n+f1 = rl20000tb20000fn20lrl+rbtb+tnfn+f1

注意由于摄像机朝向 − z -z z 轴,在视图空间下,所有深度值为负值,所以需要乘上一个关于 z 值的翻转矩阵:

( 2 r − l 0 0 l + r l − r 0 2 t − b 0 b + t b − t 0 0 2 f − n n + f n − f 0 0 0 1 ) ∗ ( 1 0 0 0 0 1 0 0 0 0 − 1 0 0 0 0 1 ) = ( 2 r − l 0 0 l + r l − r 0 2 t − b 0 b + t b − t 0 0 2 n − f n + f n − f 0 0 0 1 ) \begin{pmatrix} \frac{2}{r - l} & 0 & 0 & \frac{l + r}{l - r} \\ 0 & \frac{2}{t - b} & 0 & \frac{b + t}{b - t} \\ 0 & 0 & \frac{2}{f - n} & \frac{n + f}{n - f} \\ 0 & 0 & 0 & 1 \end{pmatrix} * \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & -1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} = \begin{pmatrix} \frac{2}{r - l} & 0 & 0 & \frac{l + r}{l - r} \\ 0 & \frac{2}{t - b} & 0 & \frac{b + t}{b - t} \\ 0 & 0 & \frac{2}{n - f} & \frac{n + f}{n - f} \\ 0 & 0 & 0 & 1 \end{pmatrix} rl20000tb20000fn20lrl+rbtb+tnfn+f1 1000010000100001 = rl20000tb20000nf20lrl+rbtb+tnfn+f1

glm 的 ortho 函数:

glm::ortho(left, right, bottom, top, near, far);

模仿实现:

mat4 ortho(float left, float right, float bottom, float top, float n, float f)
{mat4 orthoMatrix;// 计算缩放分量orthoMatrix[0][0] = 2.0f / (right - left);orthoMatrix[1][1] = 2.0f / (top - bottom);orthoMatrix[2][2] = 2.0f / (n - f); // z轴方向反转// 计算平移分量orthoMatrix[0][3] = (left + right) / (left - right);orthoMatrix[1][3] = (bottom + top) / (bottom - top);orthoMatrix[2][3] = (n + f) / (n - f);return orthoMatrix;
}

正交投影矩阵会将顶点坐标的各分量映射到 [ − 1 , 1 ] [-1,1] [1,1] 的范围之内,而因为我们在投影矩阵中施加了 z z z 值反转,让负的深度值变成了正的,所以这时候 z z z 值越大代表深度值越深,更符合直觉判断了。所以我们还需要修改一下 zbuffer 的算法:

// 改成初始值为极大值
std::fill(zbuffer, zbuffer + width * height, std::numeric_limits<float>::max());
if (P.z < zbuffer[x + y * width])
{zbuffer[x + y * width] = P.z;·····
}

应用正交投影矩阵也很简单,不多赘述了:

// 将视图空间坐标转化为齐次裁剪空间坐标
Vec4f proj_coords = proMatrix * view_coords;

如果我们将正交投影矩阵定义为:

glm::mat4 projMatrix = glm::ortho(0, 1.f, -0.5, 0.5, 0.1f, 100.0f);

此时只会投影视图空间正 x x x 轴部分,所以我们就只渲染出来了右边的人头:

image.png

透视投影

透视投影模拟人眼或真实摄像机的成像效果,所有光线汇聚于一点(摄像机位置),距离越远的物体在投影后显得越小(近大远小)。

image.png

透视投影矩阵的推导较为复杂,此处借鉴闫令琪老师的思想:先将视锥体“原地”变换为 AABB 包围盒,再经过平移和缩放变换到规范视域体。

所以透视投影矩阵的计算分为了两步:

  • 第一步先将视锥体压缩为长方体
  • 第二步将长方体经正交投影变换到规范视域体。

在透视投影里,视角中心穿过原点 O O O,设近平面宽为 w i d t h width width,高为 h e i g h t height height。设近平面左边 x x x 坐标为 l l l,右边为 r r r,上边 y y y 坐标为 t t t,下边为 b b b,它们满足:

r = − l , r − l = w i d t h , t = − b , t − b = h e i g h t r = -l, r-l=width, t = -b, t-b=height r=l,rl=width,t=b,tb=height

宽高比 a s p e c t = w i d t h h e i g h t = r t aspect=\frac{width}{height}=\frac{r}{t} aspect=heightwidth=tr y y y 方向上视野角 f o v Y fovY fovY 满足: h e i g h t 2 n = t n = tan ⁡ f o v Y 2 \frac{height}{2n}=\frac{t}{n}=\tan{\frac{fovY}{2}} 2nheight=nt=tan2fovY

image.png

过相机位置(原点 O O O)作一条直线,连接三维点 P ( x , y , z , 1 ) P(x, y, z, 1) P(x,y,z,1)。这条直线与近平面相交于一点,该点即为 P P P 在近平面上的投影 P ′ ( x ′ , y ′ , z ′ , 1 ) P^\prime (x^\prime,y^\prime,z^\prime,1) P(x,y,z,1)。根据相似三角形原理,我们可以得到以下比例关系:

image.png

x ′ n = x − z y ′ n = y − z \begin{gathered} \frac{x'}{n} = \frac{x}{-z}\\ \frac{y'}{n} = \frac{y}{-z} \end{gathered} nx=zxny=zy

通过上述比例关系,我们可以解出投影点的坐标: x ′ = − n x / z x' = -nx / z x=nx/z y ′ = − n y / z y' = -ny / z y=ny/z,变换后的 z ′ z' z 暂时未知。

因此,三维空间中的点 ( x , y , z , 1 ) (x, y, z, 1) (x,y,z,1) 透视变换后,投影到近平面上的坐标为 ( − n x / z , − n y / z , u n k n o w n , 1 ) (-nx/z, -ny/z, unknown, 1) (nx/z,ny/z,unknown,1)。为了方便后续计算,我们将投影坐标乘以 − z −z z,得到 ( n x , n y , u n k n o w n , − z ) (nx,ny,unknown,−z) (nx,ny,unknown,z)

根据上述投影结果,我们可以初步确定变换矩阵的形式:

( n 0 0 0 0 n 0 0 ? ? ? ? 0 0 − 1 0 ) ∗ ( x y z 1 ) = ( n x n y u n k o n w n − z ) \begin{pmatrix} n & 0 & 0 & 0 \\ 0 & n & 0 & 0 \\ ? & ? & ? & ? \\ 0 & 0 & -1 & 0 \end{pmatrix} * \begin{pmatrix}x\\y\\z\\1\end{pmatrix}=\begin{pmatrix}nx\\ny\\unkonwn\\-z\end{pmatrix} n0?00n?000?100?0 xyz1 = nxnyunkonwnz

现在需要确定变换矩阵的第三行元素。点经过透视变换后,有三点性质可以利用:

  1. 近平面上的点: ( x , y , − n , 1 ) (x,y,−n,1) (x,y,n,1) 变换后仍位于近平面上,即 ( x , y , − n , 1 ) (x,y,−n,1) (x,y,n,1),乘以 n n n 保持不变,得到 ( n x , n y , − n 2 , n ) (nx,ny,−n^2,n) (nx,ny,n2,n)
  2. 远平面中心的点: ( 0 , 0 , − f , 1 ) (0,0,−f,1) (0,0,f,1) 变换后仍位于远平面中心上,即 ( 0 , 0 , − f , 1 ) (0,0,−f,1) (0,0,f,1),乘以 f f f 保持不变,得到 ( 0 , 0 , − f 2 , f ) (0,0,−f^2,f) (0,0,f2,f)
  3. 远平面上的点: 远平面的所有点坐标 z z z 值不变都是 f f f

我们利用第 2 点性质,可设第三行元素为: ( 0 , 0 , A , B ) (0,0,A,B) (0,0,A,B)。又根据 1、3 条性质可得:

  • 对于近平面点: − A n + B = − n 2 −An+B=−n^2 An+B=n2
  • 对于远平面点: − A f + B = − f 2 −Af+B=−f^2 Af+B=f2

联立两式,解得 A = n + f , B = n f A=n+f,B=nf A=n+f,B=nf。结合以上推导,得到第一步的结果:

( n 0 0 0 0 n 0 0 0 0 n + f n f 0 0 − 1 0 ) \begin{pmatrix} n & 0 & 0 & 0 \\ 0 & n & 0 & 0 \\ 0 & 0 & n+f & nf \\ 0 & 0 & -1 & 0 \end{pmatrix} n0000n0000n+f100nf0

随后就是进行一次正交投影:

( 2 r − l 0 0 l + r l − r 0 2 t − b 0 b + t b − t 0 0 2 n − f n + f n − f 0 0 0 1 ) ∗ ( n 0 0 0 0 n 0 0 0 0 n + f n f 0 0 − 1 0 ) = ( 2 n r − l 0 0 0 0 2 n t − b 0 0 0 0 n + f n − f 2 n f n − f 0 0 − 1 0 ) \begin{pmatrix} \frac{2}{r - l} & 0 & 0 & \frac{l + r}{l - r} \\ 0 & \frac{2}{t - b} & 0 & \frac{b + t}{b - t} \\ 0 & 0 & \frac{2}{n - f} & \frac{n + f}{n - f} \\ 0 & 0 & 0 & 1 \end{pmatrix}*\begin{pmatrix} n & 0 & 0 & 0 \\ 0 & n & 0 & 0 \\ 0 & 0 & n+f & nf \\ 0 & 0 & -1 & 0 \end{pmatrix}=\begin{pmatrix} \frac{2n}{r-l} & 0 & 0 & 0 \\ 0 & \frac{2n}{t-b} & 0 & 0 \\ 0 & 0 & \frac{n + f}{n - f} & \frac{2nf}{n - f} \\ 0 & 0 & -1 & 0 \end{pmatrix} rl20000tb20000nf20lrl+rbtb+tnfn+f1 n0000n0000n+f100nf0 = rl2n0000tb2n0000nfn+f100nf2nf0

请记住 w i d t h , h e i g h t , r , l , t , b width,height,r,l,t,b width,height,r,l,t,b 是我们为了推导矩阵而定义出来的,我们只关心它们的相对关系:宽高比 a s p e c t aspect aspect y y y 方向上的视野角 f o v Y fovY fovY,所以我们需要将矩阵转化一下:

2 n r − l = n r = n a s p e c t ∗ t = 1 a s p e c t ∗ tan ⁡ f o v Y 2 \frac{2n}{r-l}=\frac{n}{r}=\frac{n}{aspect*t}=\frac{1}{aspect*\tan{\frac{fovY}{2}}} rl2n=rn=aspecttn=aspecttan2fovY1

2 n t − b = n t = 1 tan ⁡ f o v Y 2 \frac{2n}{t-b}=\frac{n}{t}=\frac{1}{\tan{\frac{fovY}{2}}} tb2n=tn=tan2fovY1

所以最后的透视投影矩阵变为了:

( 1 a s p e c t ∗ tan ⁡ f o v Y 2 0 0 0 0 1 tan ⁡ f o v Y 2 0 0 0 0 n + f n − f 2 n f n − f 0 0 − 1 0 ) \begin{pmatrix} \frac{1}{aspect*\tan{\frac{fovY}{2}}} & 0 & 0 & 0 \\ 0 & \frac{1}{\tan{\frac{fovY}{2}}} & 0 & 0 \\ 0 & 0 & \frac{n + f}{n - f} & \frac{2nf}{n - f} \\ 0 & 0 & -1 & 0 \end{pmatrix} aspecttan2fovY10000tan2fovY10000nfn+f100nf2nf0

glm 的 perspective 函数:

glm::perspective(fovY, aspect, near, far);

模仿实现:

mat4 perspective(float fov, float aspect, float n, float f)
{mat4 proj;float tanHalfFov = tan(glm::radians(fov) / 2);proj[0][0] = 1 / (aspect * tanHalfFov);proj[1][1] = 1 / tanHalfFov;proj[2][2] = (f + n) / (n - f);proj[2][3] = 2 * n * f / (n - f);proj[3][2] = -1;return proj;
}

现在我们将视角换回到正面,然后将右边的模型置后,来看看近大远小的效果:

glm::mat4 projMatrix = glm::perspective(45, float(width) / height, 0.1f, 100.f);glm::mat4 modelMatrix;
modelMatrix = glm::translate(modelMatrix, Vec3f(-0.5, 0, 0));
modelMatrix = glm::rotate(modelMatrix, 45, Vec3f(0, 1, 0));
modelMatrix = glm::scale(modelMatrix, Vec3f(0.5, 0.5, 0.5));
drawModel(result, texture, light_dir, zbuffer, model, modelMatrix, viewMatrix, projMatrix);
modelMatrix = glm::mat4::identity();
modelMatrix = glm::translate(modelMatrix, Vec3f(0.5, 0, -10));
modelMatrix = glm::rotate(modelMatrix, -45, Vec3f(0, 1, 0));
modelMatrix = glm::scale(modelMatrix, Vec3f(0.5, 0.5, 0.5));
drawModel(result, texture, light_dir, zbuffer, model, modelMatrix, viewMatrix, projMatrix);

image.png

现在改为正交投影,看一下效果:

glm::mat4 projMatrix = glm::ortho(-1, 1, -1, 1, 0.1, 100);

image.png

你可以清楚地看到正交投影并没有对远处的模型进行缩放。

透视除法

经过透视投影矩阵变换后,坐标的各分量会处于 [ − w , w ] [-w,w] [w,w] 之间(正交投影 w w w 保持不变始终为 1,所以可以不进行这一步),我们需要将其映射到 [ − 1 , 1 ] [-1,1] [1,1] 之间,这一步操作被称为透视除法,只需将各分量除以 w w w 即可。经过透视除法后的坐标被称为 NDC(归一化/规范化设备) 坐标

proj_coords = proj_coords / proj_coords.w;

这一步操作在 opengl 中是自动发生的(顶点着色器之后)。经过透视除法之后,会将坐标分量位于 [ − 1 , 1 ] [-1,1] [1,1] 之外的顶点全部剔除,不对它们进行渲染,这就是裁剪操作。

如果一个三角形三个顶点都不在 [ − 1 , 1 ] [-1,1] [1,1] 之间,那么这个三角形可以被直接裁剪。但如果这个三角形只是部分顶点不在 [ − 1 , 1 ] [-1,1] [1,1] 之内,OpenGL会重新构建这个三角形为一个或多个三角形让其能够适合这个裁剪范围。

这个重新构建三角形的算法较为复杂,现在让我们简化处理,在 triangle 函数内,不渲染那些超过范围的片段即可:

if (x < 0 || x >= width || y < 0 || y >= height){continue;}
if (P.z < -1 || P.z > 1){continue;}

视口矩阵

经过投影变换与透视除法之后,坐标都汇集到 [ − 1 , 1 ] [-1,1] [11] 的规范立方体内,然后需要经过视口变换映射到屏幕上。映射后的 x 、 y x、y xy 坐标范围取决于你设定的窗口大小 x ∈ [ 0 , w i d t h ] 、 y ∈ [ 0 , h e i g h t ] x \in [0,width]、y \in [0,height] x[0,width]y[0,height]。映射后的深度值 z ∈ [ 0 , 1 ] z \in [0,1] z[0,1],当然也可以继续保持为 [ − 1 , 1 ] [-1,1] [1,1],我们只关心深度值的相对大小而已。

在以前的代码中,我们是这样做的:

vertex[j].location = Vec3f((proj_coords.x + 1.0) * width / 2.0, (proj_coords.y + 1.0) * height / 2.0, proj_coords.z);

这里就是视口变换,我们可以把它转化为矩阵形式:

( w i d t h 2 0 0 w i d t h 2 0 h e i g h t 2 0 h e i g h t 2 0 0 1 2 1 2 0 0 0 1 ) ∗ ( x y z 1 ) = ( ( x + 1 ) w i d t h 2 ( y + 1 ) h e i g h t 2 z + 1 2 1 ) \begin{pmatrix} \frac{width}{2} & 0 & 0 & \frac{width}{2} \\ 0 & \frac{height}{2} & 0 & \frac{height}{2} \\ 0 & 0 & \frac{1}{2} & \frac{1}{2} \\ 0 & 0 & 0 & 1 \end{pmatrix} * \begin{pmatrix}x\\y\\z\\1\end{pmatrix}=\begin{pmatrix}\frac{(x+1)width}{2}\\\frac{(y+1)height}{2}\\\frac{z+1}{2}\\1\end{pmatrix} 2width00002height00002102width2height211 xyz1 = 2(x+1)width2(y+1)height2z+11

mat4 viewport(float w, float h)
{mat4 viewportMatrix;viewportMatrix[0][0] = w / 2;viewportMatrix[1][1] = h / 2;viewportMatrix[2][2] = 0.5;viewportMatrix[0][3] = w / 2;viewportMatrix[1][3] = h / 2;viewportMatrix[2][3] = 0.5;return viewportMatrix;
}
// 视口变换
Vec4f viewport_coords = viewportMatrix * proj_coords;
vertex[j].location = viewport_coords;

法线矩阵

物体表面的光照计算需要法线,而光照计算通常处于世界空间。所以我们需要将物体表面的法线从模型空间转换到世界空间下,一种简单的做法是将法线直接乘上模型矩阵,但这样做通常是错误的。请思考下面的情况(内容来自:LearnOpenGL——光照-CSDN博客,我对基础光照 - LearnOpenGL CN 的润色):

  1. 法向量的性质: 法向量本质上是一个方向向量,它描述的是一个表面的朝向,而不是空间中的一个特定位置。与顶点位置不同,法向量没有齐次坐标中的 w 分量。这意味着平移变换不应影响法向量的方向。如果我们直接将法向量乘以一个完整的 4x4 模型矩阵,其中的平移部分会错误地影响法向量。为了消除平移的影响,我们应该只使用模型矩阵的左上角 3x3 矩阵进行变换。另一种等效的方法是将法向量的 w 分量设置为 0,然后乘以 4x4 矩阵,这样也能有效地忽略平移部分。总之,对于法向量,我们只希望应用缩放和旋转变换。
  2. 不等比缩放的影响: 更重要的是,如果模型矩阵包含不等比缩放(即在不同的轴向上缩放比例不同),那么变换后的法向量将不再垂直于变换后的表面。
    • 下图展示了这种情况:
      image.png
    • 如图所示,原始的法向量 N N N 垂直于表面。经过不等比缩放后,表面发生了变形,而如果直接使用模型矩阵变换法向量,得到的 N ‾ \overline{N} N 不再垂直于新的表面。

每当我们应用一个不等比缩放时(需要注意的是,等比缩放不会破坏法线的垂直性,因为它只是改变了法线的长度,而这可以通过标准化来简单地修复),法向量就不再垂直于对应的表面,这会导致光照计算出现错误。

解决这个问题的方法是使用一个专门为法向量定制的变换矩阵,称为 法线矩阵(Normal Matrix)。法线矩阵通过特定的线性代数运算来消除不等比缩放对法向量的影响,从而保证变换后的法向量仍然垂直于变换后的表面。

如果你想知道这个矩阵是如何计算出来的,建议去阅读这个文章。译文

法线矩阵的定义是“模型矩阵左上角 3x3 部分的逆矩阵的转置矩阵”。

  • 转置矩阵(Transpose Matrix): 将矩阵的行和列互换得到的新矩阵。
  • 逆矩阵(Inverse Matrix): 一个矩阵与其逆矩阵相乘得到单位矩阵。

所以我们在 drawModel 函数内,将法线 n n n 转换到世界空间写了如下的代码:

// 法线矩阵变换
n = modelMatrix.inverse().transpose().to_mat3() * n;
n.normalize();

Phong shading

还记得我们是如何获得三角面的法线的?我们通过三角形两边向量的叉乘来获得,这可以获得三角面宏观的法线。而三角形内部的所有着色点都是用这个宏观法线来计算光照,这就是典型的 Flat Shading 着色方法。

obj 文件内存储了三角面三个顶点的法线,我们可以将它们利用起来,内部着色点的法线可以根据重心坐标对三个顶点法线插值来得到,这样每个着色点就可以使用自己的法线来计算光照。这就是 Gouraud shading 着色方法,能带来更平滑的光照效果。

前面我们已经在 model 中存储了顶点的法线信息,现在做的工作就很简单了。先在 VertexInfo 中添加一个向量存储法线:

struct VertexInfo
{Vec3f location;Vec2f textureCoor;Vec3f normal;
};

drawModel 函数内,循环计算顶点坐标时,顺便计算一下法线信息:

for(int j = 0; j < 3; j++)
{// 法向量变换vertex[j].normal = normalMatrix * model->vertNormal(face_vn[j]);vertex[j].normal.normalize();
}

绘制三角形内部的着色点时,插值计算法线并计算光照:

Vec3f n = (t0.normal * bc_screen.x + t1.normal * bc_screen.y + t2.normal * bc_screen.z).normalize();
float light = std::max(0.f, - n * light_dir);

image.png

你可以明显地看到模型表面变光滑了!

本次代码提交记录:

image.png

这个版本的 LookAt 函数存在错误,文章是正确!2025-4-29 16.23 提交修复

Gouraud shading

原谅我在 git 的提交将 Phong shading 当作了 Gouraud shading,写完才反应过来。

理解了 Phong shading ,Gouraud shading 就很简单了。对于三角形内部的每个着色点,Gouraud shading 不是插值法线然后去计算颜色,而是直接插值三个顶点的颜色来得到内部着色点的颜色,顶点的颜色通过顶点的法线计算出来。光滑效果来说, P h o n g > G o u r a u d > F l a t Phong > Gouraud > Flat Phong>Gouraud>Flat。实现很简单,就不多赘述了(其实是因为先实现了 Phong shading 而懒得实现 Gouraud shading)。

参考

  • tinyrenderer
  • 图形学基础 - 变换 - 投影 - 知乎
  • 坐标系统 - LearnOpenGL CN
  • 摄像机 - LearnOpenGL CN
  • Lecture 03 Transformation
  • Lecture 04 Transformation Cont
  • 从零构建光栅器,tinyrenderer笔记(下) - 知乎

相关文章:

tinyrenderer笔记(中)

tinyrenderer个人代码仓库&#xff1a;tinyrenderer个人练习代码 前言 原教程的第 4 课与第 5 课主要介绍了坐标变换的一些知识点&#xff0c;但这一篇文章的内容主要是手动构建 MVP 矩阵&#xff0c;LookAt 矩阵以及原教程涉及到的一些知识点&#xff0c;不是从一个图形学小白…...

人工智能对人类的影响

人工智能对人类的影响 近年来&#xff0c;人工智能&#xff08;AI&#xff09;技术以惊人的速度发展&#xff0c;深刻改变了人类社会的方方面面。从医疗、教育到交通、制造业&#xff0c;AI的应用正在重塑我们的生活方式。然而&#xff0c;这一技术革命也带来了机遇与挑战并存…...

LeetCode 220 存在重复元素 III 题解

LeetCode 220 存在重复元素 III 题解 题目描述 给定一个整数数组 nums 和两个整数 k 和 t&#xff0c;请判断数组中是否存在两个不同的索引 i 和 j&#xff0c;使得&#xff1a; abs(nums[i] - nums[j]) < tabs(i - j) < k 方法思路&#xff1a;桶排序 滑动窗口 核…...

0506--01-DA

36. 单选题 在娱乐方式多元化的今天&#xff0c;“ ”是不少人&#xff08;特别是中青年群体&#xff09;对待戏曲的态度。这里面固然存在 的偏见、难以静下心来欣赏戏曲之美等因素&#xff0c;却也有另一个无法回避的原因&#xff1a;一些戏曲虽然与观众…...

单应性估计

单应性估计是计算机视觉中的核心技术&#xff0c;主要用于描述同一平面在不同视角下的投影变换关系。以下从定义、数学原理、估计方法及应用场景等方面进行综合解析&#xff1a; 一、单应性的定义与核心特性 单应性&#xff08;Homography&#xff09;是射影几何中的概念&…...

Missashe考研日记-day33

Missashe考研日记-day33 1 专业课408 学习时间&#xff1a;2h30min学习内容&#xff1a; 今天开始学习OS最后一章I/O管理的内容&#xff0c;听了第一小节的内容&#xff0c;然后把课后习题也做了。知识点回顾&#xff1a; 1.I/O设备分类&#xff1a;按信息交换单位、按设备传…...

YOLO8之学习指南

一、引言 在计算机视觉领域,目标检测是一项核心任务,其应用范围广泛,涵盖安防监控、自动驾驶、智能医疗等众多领域。YOLO(You Only Look Once)系列算法凭借其高效、快速的特点,在目标检测领域占据重要地位。YOLO8 作为 YOLO 系列的最新版本,进一步提升了检测精度和速度…...

中达瑞和便携式高光谱相机:珠宝鉴定领域的“光谱之眼”

在珠宝行业中&#xff0c;真伪鉴定始终是核心需求。随着合成技术与优化处理手段的日益精进&#xff0c;传统鉴定方法逐渐面临挑战。中达瑞和推出的便携式高光谱相机&#xff0c;凭借其独特的“图谱合一”技术&#xff0c;为珠宝真假鉴定提供了科学、高效且无损的解决方案&#…...

C++自动重连机制设计与实现指南

一、为什么需要自动重连 在网络通信场景中&#xff0c;连接中断是不可避免的常见问题&#xff1a; 网络波动&#xff08;移动网络切换、WiFi信号不稳&#xff09; 服务端维护/重启 中间设备故障&#xff08;路由器、负载均衡器&#xff09; 操作系统资源限制 长时间空闲断…...

昇腾Atlas 200I DK A2 开发者套件无法上网问题的解决

目录 引言 USB WiFi网卡 USB以太网卡 结语 引言 今年通过华为的智能基座项目得到了三个Atlas 200I DK A2 开发者套件&#xff0c;很不幸其中有一块是坏的&#xff0c;其上网部分不能使用&#xff1a;2个RJ45的口在Linux系统内都无法识别&#xff0c;而USB口虽然能够识别&a…...

私有仓库 Harbor、GitLab

gitlab 部署资料 Harbor...

极狐GitLab 如何将项目共享给群组?

极狐GitLab 是 GitLab 在中国的发行版&#xff0c;关于中文参考文档和资料有&#xff1a; 极狐GitLab 中文文档极狐GitLab 中文论坛极狐GitLab 官网 共享项目和群组 (BASIC ALL) 在极狐GitLab 16.10 中&#xff0c;更改为在成员页面的成员选项卡上显示被邀请群组成员&#xf…...

QGIS分割平行四边形

需求&#xff1a;四个点确定的平行四边形的范围&#xff0c;我想把他们均分成20份&#xff0c;然后取质心。 解决方案&#xff1a;找了好几个插件&#xff0c;Polygon Divider、Split Polygon发现不好用&#xff0c;不能满足需求。最终找到了Equalyzer&#xff0c;就是比较麻烦…...

NestJS 的核心构建块有哪些?请简要描述它们的作用(例如,Modules, Controllers, Providers)

NestJS 核心构建块解析&#xff08;Modules、Controllers、Providers&#xff09; NestJS 是一个基于 TypeScript 的渐进式 Node.js 框架&#xff0c;核心设计借鉴了 Angular 的模块化思想。下面从实际开发角度解析它的三大核心构建块&#xff0c;并附代码示例和避坑指南。 一…...

Nginx 安全防护与Https 部署实战

目录 一、核心安全配置 1. 编译安装 Nginx 2. 隐藏版本号 3. 限制危险请求方法 4. 请求限制&#xff08;CC 攻击防御&#xff09; &#xff08;1&#xff09;使用 Nginx 的 limit_req 模块限制请求速率 &#xff08;2&#xff09;压力测试验证 5. 防盗链 二、高级防护 …...

电商双十一美妆数据分析

1. 数据读取与基础查看 库导入&#xff1a;使用 import numpy as np 和 import pandas as pd 导入常用数据分析库。数据读取&#xff1a; df pd.read_csv(双十一_淘宝美妆数据.csv) 读取数据文件。数据查看&#xff1a;通过 df.head() 查看数据前几行&#xff1b; df.info() 了…...

高等数学第六章---定积分(§6.1元素法6.2定积分在几何上的应用1)

本文是关于定积分应用的系列讲解的第一讲&#xff0c;主要介绍元素法的基本思想&#xff0c;并重点讲解如何运用定积分计算平面图形的面积&#xff0c;包括直角坐标系和极坐标系下的情况。 6.1 元素法 曲边梯形的面积回顾 我们首先回顾曲边梯形的面积。设函数 f ( x ) ≥ 0 …...

十分钟了解 @MapperScan

MapperScan 是 MyBatis 和 MyBatis-Plus 提供的一个 Spring Boot 注解&#xff0c;用于自动扫描并注册 Mapper 接口&#xff0c;使其能够被 Spring 容器管理&#xff0c;并与对应的 XML 或注解 SQL 绑定。它的核心作用是简化 MyBatis Mapper 接口的配置&#xff0c;避免手动逐个…...

爬虫程序中如何添加异常处理?

在爬虫程序中添加异常处理是确保程序稳定性和可靠性的关键步骤。异常处理可以帮助你在遇到错误时捕获问题、记录日志&#xff0c;并采取适当的措施&#xff0c;而不是让程序直接崩溃。以下是一些常见的异常处理方法和示例&#xff0c;帮助你在爬虫程序中实现健壮的错误处理机制…...

[250506] Auto-cpufreq 2.6 版本发布:带来增强的 TUI 监控及多项改进

目录 Auto-cpufreq 2.6 版本发布&#xff1a;带来增强的 TUI 监控及多项改进 Auto-cpufreq 2.6 版本发布&#xff1a;带来增强的 TUI 监控及多项改进 Auto-cpufreq&#xff0c;一款适用于 Linux 的免费开源自动 CPU 速度与功耗优化器&#xff0c;已发布其最新版本 2.6。该工具…...

探索Hello Robot开源移动操作机器人Stretch 3的技术亮点与市场定位

Hello Robot 推出的 Stretch 3 机器人凭借其前沿技术和多功能性在众多产品中占据优势。Stretch 3 机器人采用开源设计&#xff0c;为开发者提供了灵活的定制空间&#xff0c;能够满足各种不同的需求。其配备的灵活手腕组件和 Intel Realsense D405 摄像头&#xff0c;显著增强了…...

【Harbor v2.13.0 详细安装步骤 安装证书启用 HTTPS】

Harbor v2.13.0 详细安装步骤&#xff08;启用 HTTPS&#xff09; 1. 环境准备 系统要求&#xff1a;至少 4GB 内存&#xff0c;100GB 磁盘空间。 已安装组件&#xff1a; Docker&#xff08;版本 ≥ 20.10&#xff09;Docker Compose&#xff08;版本 ≥ v2.0&#xff09; 域…...

码蹄集——直角坐标到极坐标的转换、射线、线段

目录 MT1052 直角坐标到极坐标的转换 MT1066 射线 MT1067 线段 MT1052 直角坐标到极坐标的转换 思路&#xff1a; arctan()在c中是atan()&#xff0c;结果是弧度要转换为度&#xff0c;即乘与180/PI 拓展&#xff1a;cos()、sin()在c代码中表示方式不变 #include<bits/…...

accept() reject() hide()

1. accept() 用途 确认操作&#xff1a;表示用户完成了对话框的交互并确认了操作&#xff08;如点击“确定”按钮&#xff09;。 关闭模态对话框&#xff1a;结束 exec() 的事件循环&#xff0c;返回 QDialog::Accepted 结果码。适用场景 模态对话框&#xff08;通过 exec()…...

天文探秘学习小结

宇宙 宇宙大爆炸 时间 130亿年前 10-30次方秒内发生大爆炸 发现 20世纪80年代 哈勃发现 通过基于其他星系相对地球的移动速度得出的结论 哈勃发现离地球越远的星系 离开地球的速度越快 得出宇宙加速膨胀的结论 测量造父变星到地球的距离 哈勃测量的是一种恒星 叫造父变星 造…...

游戏引擎学习第261天:切换到静态帧数组

game_debug.cpp: 将ProfileGraph的尺寸初始化为相对较大的值 今天的讨论主要围绕性能分析器&#xff08;Profiler&#xff09;以及如何改进它的可用性展开。当前性能分析器已经能够正常工作&#xff0c;但我们希望通过一些改进&#xff0c;使其更易于使用&#xff0c;特别是在…...

利用 Kali Linux 进行信息收集和枚举

重要提示&#xff1a; 在对任何系统进行信息收集和枚举之前&#xff0c;务必获得明确的授权。未经授权的扫描和探测行为是非法的&#xff0c;并可能导致严重的法律后果。本教程仅用于教育和授权测试目的。 Kali Linux 官方链接&#xff1a; 官方网站&#xff1a; https://www…...

深入解析代理服务器:原理、应用与实战配置指南

一、代理服务器的核心原理与工作机制 1.1 网络通信的中介架构 代理服务器&#xff08;Proxy Server&#xff09;本质上是位于客户端与目标服务器之间的中间层节点&#xff0c;其核心工作机制遵循OSI模型的​​会话层​​与​​应用层​​协议。当客户端发起网络请求时&#x…...

[蓝桥杯 2025 省 B] 水质检测(暴力 )

暴力暴力 菜鸟第一次写题解&#xff0c;多多包涵&#xff01;&#xff01;! 这个题目的数据量很小&#xff0c;所以没必要去使用bfs&#xff0c;直接分情况讨论即可 一共两排数据&#xff0c;我们使用贪心的思想&#xff0c;只需要实现从左往右的过程中每个检测器相互连接即…...

区块链+数据库:技术融合下的应用革新与挑战突围

引言 近年来&#xff0c;区块链技术凭借其去中心化、不可篡改、透明可追溯等特性&#xff0c;逐渐从数字货币领域扩展到更广泛的应用场景&#xff0c;包括供应链管理、医疗健康、政务服务和数字身份等。与此同时&#xff0c;传统数据库系统在应对海量数据、多方协作与安全需求…...

油气地震资料信号处理中的NMO(正常时差校正)

油气地震资料信号处理中的NMO&#xff08;正常时差校正&#xff09;介绍与应用 NMO基本概念 **正常时差校正&#xff08;Normal Moveout Correction&#xff0c;NMO&#xff09;**是地震资料处理中的一项关键技术&#xff0c;主要用于消除由于炮检距&#xff08;source-recei…...

TDengine 车联网案例

简介 随着科技的迅猛发展和智能设备的广泛普及&#xff0c;车联网技术已逐渐成为现代交通领域的核心要素。在这样的背景下&#xff0c;选择一个合适的车联网时序数据库显得尤为关键。车联网时序数据库不仅仅是数据存储的解决方案&#xff0c;更是一个集车辆信息交互、深度分析…...

探索编程世界:从“爱编程的小黄鸭”B站账号启航

探索编程世界&#xff1a;从“爱编程的小黄鸭”B站账号启航 在编程学习的漫漫长路上&#xff0c;你是否常常为寻找优质、易懂的学习资源而烦恼&#xff1f;今天&#xff0c;我想给大家分享一个宝藏B站账号——“爱编程的小黄鸭”&#xff0c;希望能为大家的编程学习之旅提供一…...

使用 git subtree 方法将六个项目合并到一个仓库并保留提交记录

使用 git subtree 方法将六个项目合并到一个仓库并保留提交记录 步骤 1&#xff1a;初始化主仓库步骤 2&#xff1a;逐个添加子项目2.1 添加子项目远程仓库2.2 将子项目合并到主仓库的指定目录2.3 重复操作其他子项目 步骤 3&#xff1a;验证提交历史步骤 4&#xff08;可选&am…...

Django缓存框架API

这里写自定义目录标题 访问缓存django.core.cache.cachesdjango.core.cache.cache 基本用法cache.set(key, value, timeoutDEFAULT_TIMEOUT, versionNone)cache.get(key, defaultNone, versionNone)cache.add(key, value, timeoutDEFAULT_TIMEOUT, versionNone)cache.get_or_se…...

Linux云计算训练营笔记day02(Linux、计算机网络、进制)

Linux 是一个操作系统 Linux版本 RedHat Rocky Linux CentOS7 Linux Ubuntu Linux Debian Linux Deepin Linux 登录用户 管理员 root a 普通用户 nsd a 打开终端 放大: ctrl shift 缩小: ctrl - 命令行提示符 [rootlocalhost ~]# ~ 家目录 /root 当前登录的用户…...

LIO-Livox

用单台Livox Horizon (含内置IMU) 实现高鲁棒性的激光-惯性里程计&#xff0c;可在各类极端场景下鲁棒运行&#xff0c;并达到高精度的定位和建图效果。(城区拥堵、高速公路、幽暗隧道) 注&#xff1a;该系统主要面向大型室外环境中的汽车平台设计。用户可以使用 Livox Horizo…...

VNP46A3灯光遥感数据全球拼接并重采样

感谢Deepseek帮我写代码&#xff0c;本人在此过程中仅对其进行调试和部分修改&#xff1a; 灯光遥感2024年1月全球拼接结果 代码如下&#xff1a; import os import glob import h5py import numpy as np from osgeo import gdal, osr import rasterio from rasterio.merge im…...

CEF格式说明

又是一年护网季&#xff0c;现在甲方hw已经主流采用SIEM平台了&#xff0c;IPS、IDS、WAF、FW、EDR等安全数据经过安全态势感知这个二道贩子展现在蓝队面前&#xff0c;勉强能用&#xff0c;今天来说一下SIEM中常见的CEF格式&#xff0c;Common Event Format&#xff0c;公共事…...

【Trea】Trea国际版|海外版下载

Trea目前有两个版本&#xff0c;海外版和国内版。‌ Trae 版本差异 ‌大模型选择‌&#xff1a; ‌国内版‌&#xff1a;提供了字节自己的Doubao-1.5-pro以及DeepSeek的V3版本和R1版本。海外版&#xff1a;提供了ChartGPT以及Claude-3.5-Sonnet和3.7-Sonnt. ‌功能和界面‌&a…...

如何管理两个Git账户

背景 在开发过程中&#xff0c;我们有时需要同时使用 多个 Git 账户&#xff08;如个人 GitHub 账户和公司 GitLab 账户&#xff09;。但由于 Git 默认使用全局配置&#xff0c;可能会导致提交信息混乱、权限冲突等问题。本文将介绍如何在同一台机器上 安全、高效地管理多个 G…...

概统期末复习--速成

随机事件及其概率 加法公式 推三个的时候ABC&#xff0c;夹逼准则 减法准则 除法公式 相互独立定义 两种分析 两个解法 古典概型求概率&#xff08;排列组合&#xff09; 分步相乘、分类相加 全概率公式和贝叶斯公式 两阶段问题 第一个小概率*A在小概率的概率。。。累计 …...

Linux系统之shell脚本基础:条件测试、正整数字符串比较与if、case语句

目录 一.条件测试 1.三种测试方法 2.正整数值比较 3.字符串比较 4.逻辑测试 二.脚本中常用命令 1.echo命令 2.date命令 3.cal命令 4.tr命令 5.cut命令 6.sort命令 7.uniq命令 8.cat多行重定向 三.if语句 1.使用格式 2.if语句实例 四.case格式 1.使用格式 2…...

15.Spring Security对Actuator进行访问控制

15.Spring Security对Actuator进行访问控制 pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocati…...

Eigen矩阵的平移,旋转,缩放

#include <Eigen/Core> #include <Eigen/Dense>平移 x轴 // 原始点或对象的坐标Eigen::Vector3d original_point(1.0, 2.0, 3.0);std::cout << "original_point: " << std::endl << original_point << std::endl;// x 轴上的平…...

基站综合测试仪核心功能详解:从射频参数到5G协议测试实战指南

基站综合测试仪是通信网络建设和维护中的关键工具&#xff0c;主要用于对基站设备进行全面的性能验证和故障诊断&#xff0c;确保其符合行业标准并稳定运行。其主要作用包括&#xff1a; 1. 基站发射机性能测试 射频参数测量&#xff1a;检测发射功率、频率精度、调制质量&…...

Android setContentView()源码分析

文章目录 Android setContentView()源码分析前提setContentView() 源码分析总结 Android setContentView()源码分析 前提 Activity 的生命周期与 ActivityThread 相关&#xff0c;调用 startActivity() 时&#xff0c;会调用 ActivityThread#performLaunchActivity()&#xf…...

BERT 微调

BERT微调 微调 BERT BERT 对每一个词元&#xff08; token &#xff09;返回抽取了上下文信息的特征向量 不同的任务使用不同的特征 句子分类 将 < cls > 对应的向量输入到全连接层分类 命名实体识别 识别一个词元是不是命名实体&#xff0c;例如人名、机构、位置…...

K8S使用--dry-run输出资源模版和兼容性测试

1、生成资源模版 使用 --dry-run 创建资源&#xff1a; kubectl create deploy web-ng --imagenginx:1.28 --replicas2 --dry-runclient -o yaml # 查询是否存在 web-ng的资源 kubectl get deployment -A |grep web-ng 通过以上命令可以看到&#xff0c;web-ng的deployment并没…...

01硬件原理图

一、硬件设计关键信息 原理图概要: 1. 核心板&#xff1a;上电时序控制&#xff0c;DDR3&#xff0c;Flash。 2. 底板&#xff1a;以太网&#xff0c;USB&#xff0c;IO&#xff0c;AD9361&#xff0c;射频链路等。 设计Xlinx的原理图和PCB设计需要的文档&#xff1a; 1、…...