简介

这是可能一篇没有什么实际作用的文章,因为没有任何shader效果实现,整篇文章到最后,我只实现了一个旋转的立方体(o(╯□╰)o,好弱),和游戏引擎渲染的万紫千红的3D世界显得有很大落差,仿佛一切都回到了最初的起点(不知道有没有人能猜出来左侧的是哪部游戏大作的截图(*^▽^*))。


不过实时渲染繁华的背后,还是这看似简单的光栅化。今天,本人打算学习一下基本的光栅化渲染的过程,看一下怎样把一个模型从一些顶点数据,变成最终显示在屏幕上的图像,也就是所谓的渲染流水线。

所谓SoftRenderer(软渲染),就是用纯代码逻辑模拟OpenGL或者D3D渲染流水线,当然,只是实现了冰山一角,软渲染没有什么实际运用意义,但是对学习渲染原理来说应该是一个比较有效的手段了。之前开了个SoftRenderer的小坑,只是实现了基本的3D变换,三角形图元绘制,仿射纹理映射,最近本想写一篇深度相关的blog的,奈何写了一半发现好多内容需要推导计算,于是乎索性先把软渲染的坑补完,正好这几天把透视校正纹理映射加上了,顺便修了几个bug,基本可以运行起来了(然而我只画了个正方体,帧率还惨不忍睹)。一些高级的特性,比如法线贴图,复杂光照,双线性采样,RT之类的就等之后再慢慢填坑啦(恩,这很有可能是个弃坑-_-)。

简单介绍一下,项目使用的是C++,类似D3D的3D模式,左手坐标系,NDC中Z是(0,1)区间,向量为行向量,左乘矩阵,没有第三方库,只用到了Windows GDI相关,工程VS2015。基本实现了模型->世界->齐次裁剪空间->裁剪->透视除法->视口映射->ZBuffer深度测试->透视校正纹理映射等功能。实现过程中参考了知乎大佬们的各种回答。本人才疏学浅,尤其是渲染这里,很可能只是“看起来是对的“,再加上哥们600多的近视,所以如果有错误,还望各位高手批评指正。

渲染流水线

《Real Time Renderering》中把渲染流水线的功能简要描述如下:给定一个虚拟相机,一些三维物体,灯光,着色方程,贴图等资源,最终生成一张二维图片的过程。所谓流水线Pipline,也就是把原本线性执行的工作,改为并行执行,这样可以极大地提高效率。流水线的瓶颈也就是流水线中最弱的(耗时最长的)。渲染流水线在《RTR》中定义为三个基本阶段,而每一个阶段里面,又分为一些具体的操作步骤,下面分别看一下。

Application(应用程序阶段):字面意思即可,就是相对于渲染来说,其他的内容基本都可以定义在这个阶段。比如游戏逻辑,物理等等。个人理解为在调用DrawCall之前的阶段,该阶段主要在CPU上进行。

Geometry(几何阶段):主要是顶点着色器(MVP变换),裁剪,屏幕映射。处理上一阶段传递进来的图元和位置等信息,计算变换位置,最终决定物体在屏幕上的哪个位置,过程中还需进行裁剪,计算一些需要传递给下一阶段的数据。下图是《RTR3》中定义的Gemmetry阶段:


Rasterizer(光栅化阶段):光栅化阶段是将上一阶段变换投影映射后屏幕空间的顶点(包括顶点包含的各种数据),转化为屏幕上像素的一个过程。在该阶段主要进行的是三角形数据的设置,数据插值,像素着色(包括纹理采样),Alpha测试,深度测试,模板测试,混合。


为了更好地显示,一般会采用双缓冲技术,即在backbuffer渲染,完成后swap到前台。

上面的流水线,可能并不是很全面,新版本的有可能有Geometry Shader,Early-Z,Tiled Based等等,而且这个东西每个厂商可能都不太一样,但是上面的应该算是比较经典的一个流水线,并且都是必不可少的阶段(曾经以为裁剪可以不用,偷一点懒,大不了性能差点,后来发现自己真是太天真)。

下面本人就来大致实现基本的渲染流水线,本文的顺序是绘制直线,三角形,MVP矩阵变换,裁剪,视口映射,深度测试,透视校正纹理采样,是以复杂度作为顺序,并非真正的渲染顺序。

绘制直线

首先来研究一下怎么绘制直线,软渲染中每一个阶段其实都有多种方法进行实现,只不过有些是比较公认的通用方案。我这里只是找了一个适合我的方案。

我们显示在屏幕上的图像,其实就是一幅二维图片,而图片其实是离散的,换句话说,就是一个二维数组,屏幕的坐标就是数组的索引。所以,实际上我们在光栅化阶段绘制的时候,直接使用int型数据即可。俗话说得好,两点确定一条直线,那么,这一个步骤的输入就是两个二维坐标,输出就是一条直线。

实现画线有几种算法,DDA(最易理解,直接算斜率,有除法,不利于硬件实现,慢),Bresenham算法(没有除法和浮点数,快),吴小林算法(带抗锯齿,比Bresenham慢)。这里,我采用了Bresenham算法进行绘制:

//斜率k = dy / dx
//以斜率小于1为例,x轴方向每单位都应该绘制一个像素,x累加即可,而y需要判断。
//误差项errorValue = errorValue + k,一旦k > 1,errorValue = errorValue - 1,保证0 < errorValue < 1
//errorValue > 0.5时,距离y + 1点较近,因而y++,否则y不变。
int dx = x1 - x0;
int dy = y1 - y0;
float errorValue = 0;
for (int x = x0, y = y0; x <= x1; x++)
{DrawPixel(x, y);errorValue += (float)dy / dx;if (errorValue > 0.5){errorValue = errorValue - 1;y++;}
}

上面算法的主要思想是,按照直线的一个方向,以斜率小于1为例的话,在x轴方向每次步进一个像素,y判断离哪个像素近。通过y方向增加一个累计误差值,当误差值超过1了,说明y方向应该上移一个像素,否则仍然是距离当前y值近。不过上面的算法还是没有避免掉除法的问题,我们可以通过修改一下判断的条件,去掉除法和浮点数,并完善各种情况:

void ApcDevice::DrawLine(int x0, int y0, int x1, int y1)
{int dx = x1 - x0;int dy = y1 - y0;int stepx = 1;int stepy = 1;if (dx < 0){stepx = -1;dx = -dx;}if (dy < 0){stepy = -1;dy = -dy;}int dy2 = dy << 1;int dx2 = dx << 1;int x = x0;int y = y0;int errorValue;//改为整数计算,去掉除法if (dy < dx){errorValue = dy2 - dx;for (int i = 0; i <= dx; i++){DrawPixel(x, y);x += stepx;errorValue += dy2;if (errorValue >= 0){errorValue -= dx2;y += stepy;}}}else{errorValue = dx2 - dy;for (int i = 0; i <= dy; i++){DrawPixel(x, y);y += stepy;errorValue += dx2;if (errorValue >= 0){errorValue -= dy2;x += stepx;}}}
}

那么,我们随机在屏幕上画两条线的效果如下,分辨率是600*450,仔细看锯齿还是挺严重的,也让我深刻地意识到了抗锯齿的重要性o(╯□╰)o:


绘制三角形

迈出了最难的第一步,接下来就会好很多了。点,线,面,三个点确定一个三角形,所以我们再加一个参数,绘制一个三角形看一下。但是此处并非直接给三个点,连在一起,而是要把三角形内部填充上颜色。最简单的填充就是扫描线填充,换句话说我们按照屏幕的y轴方向,从上向下,每一行进行绘制,直到把三角形全部填充上为止。首先要把三角形分一下类:


我们已知的是y的方向,那么就需要带入三角形边的直线方程求得x的坐标,然后在两个点之间画线。如果三角形是平顶的或者是平底的,那么我们只需要考虑两条边即可,但是如果是一般的三角形,我们就需要做一下拆分,把三角形划分成一个平顶三角形和一个平底三角形,以上图为例,三角形GHI,以H的y值带入GI直线方程求得J的x坐标,生成GHJ和HJI两个三角形,这两个三角形就可以按照平顶和平底的方式进行光栅化了。

以平底三角形ABC为例,假设A(x0,y0),B(x1,y1),C(x2,y2),AB直线上随机一点(x,y),那么直线方程如下:

(y - y1) / (x - x1) = (y0 - y1) / (x0 - x1),我们已知y(从y0到y1循环),则x值为: x = (y - y0) * (x0 - x1) /  (y0 - y1) + x1。AC边类似得到x值,那么循环中我们就可以根据y值得到每条扫描线左右的x值。平底和平顶三角形的绘制代码如下:

void ApcDevice::DrawBottomFlatTrangle(int x0, int y0, int x1, int y1, int x2, int y2)
{for (int y = y0; y <= y1; y++){int xl = (y - y1) * (x0 - x1) / (y0 - y1) + x1;int xr = (y - y2) * (x0 - x2) / (y0 - y2) + x2;DrawLine(xl, y, xr, y);}
}void ApcDevice::DrawTopFlatTrangle(int x0, int y0, int x1, int y1, int x2, int y2)
{for (int y = y0; y <= y2; y++){int xl = (y - y0) * (x2 - x0) / (y2 - y0) + x0;int xr = (y - y1) * (x2 - x1) / (y2 - y1) + x1;DrawLine(xl, y, xr, y);}
}

知道了平顶和平底,我们只要根据拐点计算出拐点y轴对应另一边的x值,生成新的三角形即可。但是此处我们为了代码简单一些,先对传入的三个顶点进行一下排序:、

void ApcDevice::DrawTrangle(int x0, int y0, int x1, int y1, int x2, int y2)
{//按照y进行排序,使y0 < y1 < y2if (y1 < y0){std::swap(x0, x1);std::swap(y0, y1);}if (y2 < y0){std::swap(x0, x2);std::swap(y0, y2);}if (y2 < y1){std::swap(x1, x2);std::swap(y1, y2);}if (y0 == y1)	//平顶三角形{DrawTopFlatTrangle(x0, y0, x1, y1, x2, y2);}else if (y1 == y2) //平底三角形{DrawBottomFlatTrangle(x0, y0, x1, y1, x2, y2);}else			//拆分为一个平顶三角形和一个平底三角形{//中心点为直线(x0, y0),(x2, y2)上取y1的点int x3 = (y1 - y0) * (x2 - x0) / (y2 - y0) + x0;int y3 = y1;//进行x排序,此处约定x2较小if (x1 > x3){std::swap(x1, x3);std::swap(y1, y3);}DrawBottomFlatTrangle(x0, y0, x1, y1, x3, y3);DrawTopFlatTrangle(x1, y1, x3, y3, x2, y2);}
}

通过上面的步骤,我们就可以在屏幕上绘制一个填充好的三角形啦,如下图:


ObjectToWorld矩阵构建

下面就是3D相关的操作了,这也是软渲染中最有意思的部分。首先就是MVP变换,具体来说就是将模型空间坐标通过ObjectToWorld矩阵(M)变换到世界空间,然后通过WorldToView矩阵(V)变换到相机空间,最后通过透视投影矩阵(P)变换到裁剪空间。上述的几个变换都是由矩阵完成的。关于矩阵,简单点来说就是一个二维数组,只是为了更加方便地表达变换的过程,并且这些计算通过矩阵更加容易用硬件进行实现。下面主要用到的是矩阵的乘法,向量与矩阵的乘法,矩阵的转置等特性。

如果把矩阵的行解释为坐标系的基向量,那么乘以该矩阵就相当于做了一次坐标变换,vM = w,称之为M将v变换到w。用基向量[1,0,0]与任意矩阵M相乘,得到[m11,m12,m13],得出的结论是矩阵的每一行都可以解释为转化后的基向量。根据该结论,就可以通过一个期望的变换,反向构造出一个矩阵来代表这个变换。首先来看一下MVP的M,也就是模型空间转世界空间的变换,这个变换也是分为三个子变换,分别是缩放,旋转,平移。下面分别推导一下几个变换的矩阵。

缩放矩阵

缩放是最简单的变换,假设顶点为(x, y, z),缩放系数为(sx, sy, sz),那么缩放希望的就是(sx *x, sy * y, sz * z),我们用一个矩阵来表示的话就是:

                                                           

代码如下:

Matrix ApcDevice::GenScaleMatrix(const Vector3& v)
{Matrix m;m.Identity();m.value[0][0] = v.x;m.value[1][1] = v.y;m.value[2][2] = v.z;return m;
}

旋转矩阵

接下来是旋转矩阵,看起来有点麻烦,而且x,y,z三个方向需要单独实现,其实又是三个矩阵相乘,这篇文章推导的比较明了,借用一张图:


到这个时候,我才意识到,哇塞,原来三角函数展开的实际应用竟然这么重要。。。我们要表示一个旋转,比如绕Z轴旋转,那么这个时候,Z轴的坐标是不会变化的,所以只考虑X,Y轴坐标即可,比如上图,A为原始坐标(x,y),角度为β,旋转角度α变换到B点(x’,y'),半径为r。此时,左右手坐标系的差别就有了,我使用的是左手坐标系,所以用左手拇指指向Z轴正方向(指向屏幕内),另外四指环绕的方向就是旋转的正方向了。如果用右手坐标系,那么就得换成右手了。

那么x’= r * cos(α + β) = cos(α) * cos(β) * r - sin(α) * sin(β) * r = cos(α) * x - sin(α) * y

同理y’= r * sin(α + β) = sin(α) * cos(β) * r + cos(α) * sin(β) * r = sin(α) * x + cos(α) * y

根据上面的两个等式,我们就可以构建出一个矩阵,Z方向不变,置为1即可,矩阵如下:

                                                         

绕X轴旋转的矩阵:

                                                         

绕Y轴旋转的矩阵:

                                                         

最终,这样,每个轴上我们给定一个角度即可得到旋转的矩阵,最终矩阵为三者相乘,绕三个轴旋转值按一个Vector传递进来:

Matrix ApcDevice::GenRotationMatrix(const Vector3& rotAngle)
{Matrix rotX = GenRotationXMatrix(rotAngle.x);Matrix rotY = GenRotationYMatrix(rotAngle.y);Matrix rotZ = GenRotationZMatrix(rotAngle.z);return rotX * rotY * rotZ;
}Matrix ApcDevice::GenRotationXMatrix(float angle)
{Matrix m;m.Identity();float cosValue = cos(angle);float sinValue = sin(angle);m.value[1][1] = cosValue;m.value[1][2] = sinValue;m.value[2][1] = -sinValue;m.value[2][2] = cosValue;return m;
}Matrix ApcDevice::GenRotationYMatrix(float angle)
{Matrix m;m.Identity();float cosValue = cos(angle);float sinValue = sin(angle);m.value[0][0] = cosValue;m.value[0][2] = -sinValue;m.value[2][0] = sinValue;m.value[2][2] = cosValue;return m;
}Matrix ApcDevice::GenRotationZMatrix(float angle)
{Matrix m;m.Identity();float cosValue = cos(angle);float sinValue = sin(angle);m.value[0][0] = cosValue;m.value[0][1] = sinValue;m.value[1][0] = -sinValue;m.value[1][1] = cosValue;return m;
}

上面,我们只考虑物体绕自身轴旋转,如果需要绕任意一点旋转的话,我们就可以先进行平移,平移到该点,然后再旋转,旋转后再平移回去。即v = vTRT^-1

平移矩阵

原本而言,我们用3x3的矩阵即可表示上面两种变换,然而平移不行,所以就需要引入一个4x4的矩阵来表达平移变换,比如一个顶点(x,y,z)我们希望它平移(tx,ty,tz)距离,那么实际上就是(x + tx,y + ty,z + tz)。使用矩阵来表示的话就是:

                                                                     

而如果用4x4矩阵,Vector3显然也不够用了,为了能达到上述的变换,我们就需要给Vector增加一个w维度,w = 1时,Vector表示点,与上述矩阵相乘后得到变换的结果,而w = 0时,Vector表示向量,上述变换的结果仍然是Vector自身,平移变换对方向变换不生效。

平移矩阵的代码如下:

Matrix ApcDevice::GenTranslateMatrix(const Vector3& v)
{Matrix m;m.Identity();m.value[3][0] = v.x;m.value[3][1] = v.y;m.value[3][2] = v.z;return m;
}

ObjectToWorld矩阵整合

经过上面的步骤,我们得到了缩放矩阵,旋转矩阵,平移矩阵,最终将三者使用矩阵乘法相乘即可得到完整的ObjectToWorld矩阵,由于我们采用了行向量表示坐标,所以相当于左乘,即我们用vSRT的顺序进行变换。

我们可以根据矩阵连乘结合律把几个变换通过矩阵相乘的方法,先计算出一个结果矩阵再用该矩阵去变换顶点,这有一个很大的好处,就是我们可以逐对象计算一次变换整体的变换矩阵,然后这个对象的所有的顶点都应用这个变换。这样就把变换的时间从n次MVP计算+n次矩阵与顶点相乘变成了1次矩阵MVP计算+n次矩阵与顶点相乘,而且可以避免浮点计算误差(尝试了一下逐图元计算变换矩阵,最后正方形边界对不上,改为统一计算后效果正常)。我们不需要一些特殊的效果(固定管线),所以我们直接把这个SRT矩阵与后续的VP矩阵相乘,结果再来进行坐标的变换。

WorldToCamera矩阵构建 

上一阶段,我们把顶点从模型空间转换到了世界空间,但是有一个很重要的问题,相机才是我们观察世界的窗口,后续的投影,裁剪,深度等如果都在世界空间做的话就太复杂了,如果我们把这些操作的坐标原点改为相机,就会大大降低后续操作的复杂性。所以,下一步就是如何将一个世界空间的对象转化到相机空间。要定义一个相机,首先要有相机位置,还要有相机看的方向,一种方法是给出相机的旋转角度,也就是Raw,Pitch,Roll这三个值,如下图:


另一种方案就是给一个相机注视点以及一个控制相机Y轴方向的向量,也就是UVN相机模型,UVN比较容易定义相机的朝向,类似LookAt的功能,所以这里我们采用这种方式定义我们的相机位置和朝向。给定了相机位置和注视点位置,我们就能求得视方向向量N,然后我们根据给定的上向量V(只是输入临时用的,不是最终的V,输入的V在VN平面没有权重,因为N已经确定了视方向,只有UV平面上可能有权重,所以需要重新计算一个仅影响Roll的角),通过向量叉乘得到一个Cross(N,V)得到右向量U,最终我们再用Cross(N,U)得到真正的V,三者都需要Normalize。这样,我们就构建出了相机所在的空间基准坐标系。

我们要的是WorldToCamera的变换,不过,对一个点做一个变换,相当于对其所在坐标系做逆变换,所以我们只要求出整体变换的逆矩阵即可。比如上面的变换称之为WTC,上面的变换包括一个旋转R和平移T,那么我们要求的WTC^-1 (表示逆)=(RT)^-1 = (T^-1)( R^-1)。我们拆开来看:

先是旋转矩阵的逆,其实上面的计算,我们构建的UVN就是R矩阵了(把相机转换到世界空间,但是在第四行没有分量,换句话说就是3X3的矩阵,没有位移,也没有缩放,那么就只有旋转),下面一步就是求R的逆。矩阵正常求逆的运算是很费的,所以一般来说要避免直接求逆,因为3X3的旋转矩阵其实是一个正交矩阵(各行各列都是单位向量,并且两两正交,可以把上一节的旋转矩阵每个看一遍,抽出左上角3X3部分)。正交矩阵的重要性质就是MM^T (转置)= E(单位矩阵),进一步推导就是M ^ T = M ^ -1,所以上面的旋转矩阵的逆实际上就是它的转置。转置的话,我们只需要沿着对角线把数据互换一下,计算量比起求逆要小得多。 

接下来是平移矩阵的逆,这个其实不需要去推导,比如向量按照(tx,ty,tz)进行了平移变换,那么对它的逆变换其实就是取反(-tx,-ty,-tz)。

最终两个矩阵以及相乘结果如下,其中T = (tx,ty,tz),UVN同理:


我们直接结果构建最终的矩阵,避免多一次矩阵运算,代码如下:

Matrix ApcDevice::GenCameraMatrix(const Vector3& eyePos, const Vector3& lookPos, const Vector3& upAxis)
{Vector3 lookDir = lookPos - eyePos;lookDir.Normalize();Vector3 rightDir = Vector3::Cross(upAxis, lookDir);rightDir.Normalize();Vector3 upDir = Vector3::Cross(lookDir, rightDir);upDir.Normalize();//构建一个坐标系,将vector转化到该坐标系,相当于对坐标系进行逆变换//C = RT,C^-1 = (RT)^-1 = (T^-1) * (R^-1),Translate矩阵逆矩阵直接对x,y,z取反即可;R矩阵为正交矩阵,故T^-1 = Transpose(T)//最终Camera矩阵为(T^-1) * Transpose(T),此处可以直接给出矩阵乘法后的结果,减少运行时计算float transX = -Vector3::Dot(rightDir, eyePos);float transY = -Vector3::Dot(upDir, eyePos);float transZ = -Vector3::Dot(lookDir, eyePos);Matrix m;m.value[0][0] = rightDir.x;  m.value[0][1] = upDir.x;  m.value[0][2] = lookDir.x;  m.value[0][3] = 0;m.value[1][0] = rightDir.y;	 m.value[1][1] = upDir.y;  m.value[1][2] = lookDir.y;  m.value[1][3] = 0;m.value[2][0] = rightDir.z;  m.value[2][1] = upDir.z;  m.value[2][2] = lookDir.z;  m.value[2][3] = 0;m.value[3][0] = transX;		 m.value[3][1] = transY;   m.value[3][2] = transZ;	   m.value[3][3] = 1;return m;
}

透视投影矩阵构建

接下来的投影阶段是3D向2D转换的一个重要的步骤(并不是这一步就转),同时也是后续CVV裁剪,ZBuffer,透视纹理校正的基础,这里我们暂且不考虑正交投影,直接来看透视投影。上文我们提到,为了更好地变换,用4X4矩阵,进而引入齐次坐标的概念,但是实际上,齐次坐标系真正的作用在于透视投影变换。

透视投影的主要知识点在于三角形相似以及小孔呈像,透视投影实现的就是一种“近大远小”的效果,其实投影后的大小(x,y坐标)也刚好就和1/Z呈线性关系。首先看下面一张图:


上图是一个视锥体的截面图(只看x,z方向),P为空间中一点(x,y,z),那么它在近裁剪面处的投影坐标假设为P’(x',y',z’),理论上来说,呈像的面应该在眼睛后方才更符合真正的小孔呈像原理,但是那样会增加复杂度,没必要额外引入一个负号(此处有一个裁剪的注意要点,下文再说),只考虑三角形相似即可。即三角形EAP’相似于三角形EGP,我们可以得到两个等式:

x’/ x = z’/ z => x’= xz’/ z  

y’/ y = z’/ z => y’= yz’/ z

由于投影面就是近裁剪面,那么近裁剪面是我们可以定义的,我们设其为N,远裁剪面为F,那么实际上最终的投影坐标就是:

(Nx/z,Ny/z,N)。

投影后的Z坐标,实际上已经失去作用了,只用N表示就可以了,但是这个每个顶点都一样,每个顶点带一个的话简直是暴殄天物,浪费了一个珍贵的维度,所以这个Z值会被存储一个用于后续深度测试,透视校正纹理映射的变换后的Z值。

这个Z值,还是比较有说道的。在透视投影变换之前,我们的Z实际上是相机空间的Z值,直接把这个Z存下来也无可厚非,但是后续计算会比较麻烦,毕竟没有一个统一的标准。既然我们有了远近裁剪面,有了Z值的上下限,我们就可以把这个Z值映射到[0,1]区间,即当在近裁剪面时,Z值为0,远裁剪面时,Z值为1(暂时不考虑reverse-z的情况)。首先,能想到的最简单的映射方法就是depth = (Z(eye) - N)/ F - N。but,这种方案是不正确的(需要参考下文关于光栅化数据插值的内容,此处先给出结论,我认为这个1/z在光栅化阶段解释更为合适):透视投影变换之后,在屏幕空间进行插值的数据,与Z值不成正比,而是与1/Z成正比。所以,我们需要一个表达式,可以使Z = N时,depth = 0,Z = F时,depth = 1,并且需要有一个z作为分母,可以写成(az + b)/z,带入上述两个条件:

(N * a  + b) / N = 0   =>  b = -an

(F * a  +  b) / F = 0   =>   aF + b = F => aF - aN = F

进而得到: a = F / (F - N) b = NF / (N - F)

最终其实视锥体被变换为NDC,但是实际上我们一般是先不进行透视除法,那么就称之为CVV(此处不要过度纠结这两个概念,下文再去解释)过程如下图(DX文档附图)所示(下图是DX模式,左手系,NDC中Z区间是0到1,也是本文使用的模式;GL的话NDC中Z是-1到1,而且是右手系,二者最终推导出的透视投影矩阵是有差异的),主要思想就都是映射,把一个区间映射到另一个区间:

我们已经把Z值,进行了映射,秉承着映射大法好的观点,我们再把X和Y进行一下映射,上面我们已经计算出投影后的坐标值是(Nx/z,Ny/z,N)。假设近裁剪面上下分别为U,B,左右分别为L,R,我们要把原始的区间映射到[-1,1]区间。我们以X轴为例进行推导,最终X投影点映射到NDC的坐标假设为Xndc,那么映射公式如下:

Xndc - (-1) / (1 - (-1)) = (XN / Z - L) / (R - L)

计算该公式得到Xndc = 2XN / Z(R - L) - (R + L)/ (R - L)

同理Yndc  = 2YN / Z(T - B) - (T + B) / (T - B)

这样,我们就可以得到了XYZ三个方向在NDC空间的坐标。

下面在来解释一下NDC和CVV,所谓NDC,全称为Normalized Device Coordinates,也就是标准设备空间,为何要引入这样一个空间呢,主要在于我们使用不同的设备,分辨率可能都不一样,实际在写shader的时候,没办法根据分辨率进行调整,而通过这样一个空间,把x,y映射到(-1,1)区间,z映射到(0,1)区间(OpenGL是(-1,1)),在下一步屏幕坐标映射时再根据屏幕分辨率生成像素真正应该在的位置,这样可以省掉很多设备适配的问题,让我们在写shader的时候一般不需要考虑屏幕分辨率的问题(有时候有,主要是全屏后处理时屏幕宽高的比例,我之前在屏幕水波纹效果中实现就遇到了这样的问题)。

再来看一下CVV,CVV全称为Canonical View Volume,即规则观察体。其实上面的变换最终的坐标应该是NDC的,但是为了更方便地做一些其他的操作,主要是CVV裁剪,引入了一个新的空间,这个空间主要是没有NDC空间的坐标没进行除以w计算,也就是说CVV空间的顶点还是齐次空间下的,除了w之后才会变为NDC空间,两者的差距主要是是否除以了w。个人理解:CVV只是用齐次坐标系表示了的NDC(如果我的理解不正确,还希望您及时指出我的错误)。

关于CVV裁剪,下文再讲,我们还是回到透视投影矩阵:

我们既然要CVV空间,也就是齐次裁剪空间下的顶点,所以我们刚好可以把上文得到的投影点坐标的每个元素的除以Z变换成W分量的Z,然后XYZ分量下的除以Z就可以去掉了,也就是说用齐次空间表示一下最终的投影点坐标:

P’(NX,NY,aZ + b,Z)带入上面的推导结果得到透视投影矩阵:

                                                             

上面的矩阵是一个通用的矩阵,但是实际上我们绝大多数情况用的矩阵都是一个特殊情况的矩阵,也就是我们的相机刚好在视锥体中间,上下左右对称,那么R和L对称,T和B对称,两者相加都等于0,而R-L和T-B我们就可以用一个宽度和高度来表示,简化后的矩阵如下:

                                                                 

下面我们再考虑一下怎样更加优雅地表示这个矩阵(换句话说就是参考一下DX和GL真正的接口)。我们一般来说,给定一个

FOV角度,N近裁剪面,F远裁剪面,Aspect屏幕宽高比即可,如下图:


那么BF也就是的高度就是tan(0.5*fov)* N 最终的H = 2 tan(0.5fov)*N,最终的W = Aspect * H。带入上述矩阵:

2N/H = 2N/2tan(0.5fov)N = 1/tan(0.5fov) = cot(0.5fov)

2N/W = 1/(Aspect * tan(0.5fov)) = cot(0.5fov)/Aspect

最终矩阵结果如下,暂且还是以tan表示:

                                                       

代码如下:

Matrix ApcDevice::GenProjectionMatrix(float fov, float aspect, float nearPanel, float farPanel)
{float tanValue = tan(0.5f * fov * 3.1415 / 180);Matrix proj;proj.value[0][0] = 1.0f / (tanValue * aspect);proj.value[1][1] = 1.0f / (tanValue);proj.value[2][2] = farPanel / (farPanel - nearPanel);proj.value[3][2] = -nearPanel * farPanel / (farPanel - nearPanel);proj.value[2][3] = 1;return proj;
}

这里,我们只是进行了透视投影变换的第一步,将顶点转化到齐次裁剪空间,因为并没有进行透视除法,所以还没有所谓的投影。第二步是透视除法,但是中间一般还会穿插一步,CVV裁剪。

啊,终于写完了透视投影矩阵的变换,实际上这个矩阵结果只有几个数,然而背后的数学推导还是比较复杂的。有了P矩阵,我们最终用来变换的矩阵就都完成了。

CVV裁剪

经过了透视变换,坐标被变换到CVV空间,此时仍然是齐次坐标,我们正常应该是判断在裁剪的立方体内,不过齐次坐标我们也就是直接比较xyz值和w的值即可,DX模式的话,z需要比较0和w。这个是非常重要的,因为我们默认为了方便是把投影平面放到了眼睛前面,但是真的有在投影平面后面的东西,如果不剔除z<0的内容,就会导致这一部分按照不对的透视公式进行计算导致结果错误。而且更重要的一点在于,相机空间z = 0的时候(也就是齐次空间的w = 0)的这种情况,在我们透视除法的时候会有除0的问题。所以要把这个剔除掉。

比如一个齐次空间的顶点,我们可以按照上述方式判断其是否在CVV内:

inline bool ApcDevice::SimpleCVVCullCheck(const Vertex& vertex)
{float w = vertex.pos.w;if (vertex.pos.x < -w || vertex.pos.x > w)return true;if (vertex.pos.y < -w || vertex.pos.y > w)return true;if (vertex.pos.z < 0.0f || vertex.pos.z > w)return true;return false;
}

CVV裁剪个人感觉是一个比较有争议的地方,现代的GPU到底如何去做裁剪,我不敢妄加推测,看了知乎上大佬们的讨论,也是分为几个派别。有认为裁剪的,有人为只剔除不裁剪的。不过个人倒比较赞同,重新构建一个三角形对于GPU来说还不如把整个三角形都画了好,毕竟实际运用时,三角形的密度很大,面积很小,都绘制了也要比裁剪可能还省。对于CVV中比较好处理的主要在于我们可以在透视除法前就把完全不可见的三角形直接剔除掉。所以我只实现了最简单的三顶点均不在CVV内剔除的方案(好吧,为偷懒找了个理由o(╯□╰)o)。

实现了CVV裁剪,其实还是蛮爽的,尤其是本身没有场景管理,视锥体裁剪的话,提交上来的所有内容都要绘制,如果本身不可见的话,那直接就咔掉了。测试的话,直接渲染立方体,帧率25左右,立方体在CVV外不开裁剪,帧率100多些(有视口范围判断),立方体在CVV外开裁剪,帧率999+直接爆表 。

透视除法与屏幕坐标映射

经过了透视投影变换,CVV裁剪,现在我们的顶点坐标都在齐次裁剪空间,下一步就是真正地进行透视除法了,经过了这一步才真正算是完成了透视投影变换。其实这个操作比较简单,因为计算在透视投影矩阵的构建中我们都推导过了,乘过Project矩阵的顶点,w坐标就不会是默认的1了。因为我们把Z值存了进去,此时我们将三个分量都除以Z,就得到了透视变换后的NDC坐标了。

马上就可以和我们之前进行的在屏幕上绘制三角形联系起来了,中间只差了一步,那就是屏幕坐标映射。上文介绍过,NDC的作用就是为了让我们计算时不需要考虑屏幕分辨率相关的问题,因为DX或者GL替我们做了,软渲染的话,我们就需要自己做这一步。

我们创建窗口的时候,会给一个窗口的宽度和高度(RT类似),既然我们得到了NDC空间的坐标值了,并且知道了屏幕的长和宽(分辨率),那么,是时候进行一波映射了。映射大法好啊!

NDC是(-1,1)区间(现在暂时只考虑X,Y),我们要把它映射到屏幕的(0,width)和(0,height)区间即可。先看X方向:首先,我们从(-1,1)区间映射到(0,1)区间,也就是(v.x / v.w + 1)* 0.5 * deviceWidth。Y方向,屏幕实际的坐标是左上角为(0,0)点,与我们的NDC是反过来的,所以映射到(0,1)区间后,还需要反向一下,改为(1-screendCoord)* deviceHeight。代码如下,进行了透视除法&屏幕空间映射:

float reciprocalW = 1.0f / v.w;
float x = (v.x * reciprocalW + 1.0f) * 0.5f * deviceWidth;
float y = (1.0f - v.y * reciprocalW) * 0.5f * deviceHeight;

这里的x,y就是经过了上述所有变换后,最终在屏幕上的坐标点。下面我们整合一下整个3D变换的过程,最终屏幕坐标v' = vFinalMatrix => vMVP => vSRTVP => vSRxRyRzTVP,代码如下:

Matrix ApcDevice::GenMVPMatrix()
{Matrix scaleM = GenScaleMatrix(Vector3(1.0f, 1.0f, 1.0f));Matrix rotM = GenRotationMatrix(Vector3(0, 0, 0));Matrix transM = GenTranslateMatrix(Vector3(0, 0, 0));Matrix worldM = scaleM * rotM * transM;Matrix cameraM = GenCameraMatrix(Vector3(0, 0, -5), Vector3(0, 0, 0), Vector3(0, 1, 0));Matrix projM = GenProjectionMatrix(60.0f, (float)deviceWidth / deviceHeight, 0.1f, 30.0f);return worldM * cameraM * projM;
}void ApcDevice::DrawTrangle3D(const Vector3& v1, const Vector3& v2, const Vector3& v3, const Matrix& mvp)
{Vector3 vt1 = mvp.MultiplyVector3(v1);Vector3 vt2 = mvp.MultiplyVector3(v2);Vector3 vt3 = mvp.MultiplyVector3(v3);Vector3 vs1 = GetScreenCoord(vt1);Vector3 vs2 = GetScreenCoord(vt2);Vector3 vs3 = GetScreenCoord(vt3);DrawTrangle(vs1.x, vs1.y, vs2.x, vs2.y, vs3.x, vs3.y);
}

我们把MVP的计算整个抽取出来,每个对象计算一次即可,对象所有的三角形运用同一个MVP变换,即每个三角形逐顶点与MVP矩阵相乘,然后进行视口映射即可。这样,我们就得到了一个可以变换的三角形(虽然看起来和上面一样,然而这的确是一个有故事的三角形,因为他不是直接显示在屏幕上的,而是历经了无数次计算,才显示到了屏幕上):


来一张动图,应用了旋转和平移变换(有故事的三角形自然多了一些能力,比如移动,旋转,缩放,随着相机位置移动的近大远小效应):


光栅化数据插值与仿射纹理映射

直到目前为止,我们虽然可以在屏幕上看到三角形了,并且可以运用各种变换,但是我们的三角形没有任何其他数据,仅有一个位置信息,这是十分令人不爽的,尤其是我这个视觉动物,憋了半天终于画出了个三角形,然而还木有颜色,简直是不能忍了!我们要给它加点料!要有更好的表现,我们就要给顶点加一些属性,最常见的,也是最容易的,就是颜色啦。我们给每个顶点增加一个顶点色,存储在顶点中。but,我们这时候应该意识到一个问题,我们的三角形只有三个点,而最终显示在屏幕上的可是无数个像素啊,其中的数据要怎么样得到呢?

比如我们正常写shader的时候,经常会定义一个v2f之类的结构体,用来从vertex阶段传递到fragment阶段,vertex阶段我们只考虑逐顶点计算的值就可以了,传递v2f,到fragment阶段,自动就可以在输入时取到每个v2f在fragment阶段的值,这个数据实际上是渲染管线帮我们自动处理了。所运用到的知识点其实也是非常简单的,就是插值(Lerp)。

来看一维的插值代码,也就是我们经常用的Lerp函数:

float LerpFloat(float v1, float v2, float t){ return v1 + (v2 - v1) * t;}

其实非常简单,我们给一个(0,1)的插值控制函数,就可以完成从v1,v2之间的插值了,当t=0时,为v1,当t=1时为v2。

那么,在三角形设置好之后,三个顶点的数据是一定的,接下来要从上到下绘制扫描线,我们每次要绘制扫描线的时候,首先要获得扫描线两侧端点的数据值,扫描线的两侧端点的值在我们求解方程的时候可以得到,也就能求出该点在端点所在的边所处的值,此处是从上到下,那么我们就用y作为插值系数,以即每一点的t = (y - y0)/ (y2 - y0),然后我们就可以用这个系数去在顶点和底点两个点之间插值得到当前线上扫描线起始点和结束点的颜色值。扫描线本身也是同理,已知左右两点的颜色值,每次前进一个像素,都可以求出当前t = (x - x0) / (x1 - x0)作为插值系数。比如我们给上面的三角形增加一个顶点色,通过插值就可以得到如下的效果:


有了颜色,我们的三角形就好看了不少。需求总是有的,能不能再好看一点呢?既然代码写不出来,那就贴张图上去吧!(我想这应该也是前辈们发明纹理映射的初衷吧)。还是同一个问题,我们只有顶点数据,也就是美术同学展uv得到的坐标值,存在顶点中。像素上全靠插值,那么要想贴上一张图,我们应该有的其实是这一个像素点应该采样的纹理坐标也就是uv值,还是一样的思路,我们补全插值的计算和顶点上的uv数据。然后用windows自带的功能读入一张bmp贴图,然后构建一个二维数组,把这个贴图的每个像素逐步拷入数组就可以了。采样时,我们计算出uv值,然后依然是映射大法好,因为uv是(0,1)区间,我们把这个区间映射到像素数组的大小,然后就可以用这个index去纹理数组中采样该点的颜色了。其实在这一步也可以做一点小文章,比如传过来的uv是非(0,1)区间的,那么边界的颜色怎么给,如果我们直接截断,那么就是clamp,也可以取余数那就是repeat,还可以实现mirror等模式。这里我就直接Clamp了。下面是Texture类中两个主要的函数:

void Texture::LoadTexture(const char* path)
{HBITMAP bitmap = (HBITMAP)LoadImage(NULL, path, IMAGE_BITMAP, width, height, LR_LOADFROMFILE);HDC hdc = CreateCompatibleDC(NULL);SelectObject(hdc, bitmap);for (int i = 0; i < width; i++){for (int j = 0; j < height; j++){COLORREF color = GetPixel(hdc, i, j);int r = color % 256;int g = (color >> 8) % 256;int b = (color >> 16) % 256;Color c((float)r / 256, (float)g / 256, (float)b / 256, 1);textureData[i][j] = c;}}
}Color Texture::Sample(float u, float v)
{u = Clamp(0, 1.0f, u);v = Clamp(0, 1.0f, v);//暂时直接采用clamp01的方式采样int intu = width * u;int intv = height * v;return textureData[intu][intv];
}

搞一个自己的头像试一下纹理映射:


看起来好多了,是不是大功告成了呢?其实并不是,因为这一节忽略了一个重要的问题,导致我们所有的插值其实都是错误的,上面看起来没有问题,是因为仅仅看起来可能是对的。

1/Z的问题与透视校正纹理映射

我们把上面的面片,沿着Y轴旋转45度,再看一下效果:


哦,伟大的蝎同志肿么变成了小短腿。。。简直玷污了我的偶像(我得赶快改好)。如果我们换一个贴图,那么这个问题将暴露得更加明显:


这是个什么鬼。。。我贴上去的可是一个方方正正的网格贴图:


那么,问题应该比较明确了,就出在透视上。来看一张图解释一下上面的现象:


右侧的三角形是我直接复制过去的,唯一的区别就在于左侧CFE平面平行于近裁剪面,而右侧把CFE整个一条线拉歪,使C接近近裁剪面。二者在最终投影在近裁剪面的位置完全相同,但是实际上在三维空间位置是相差甚远的。

先祭出两篇大佬的推导:一篇几何证明,一篇代数证明。我数学不好,只能膜拜这些大佬了。主要证明的就是在投影空间的值与1/z成正比。

我们存在顶点中的数据,顶点颜色,顶点上uv的坐标等内容,其实都是在模型空间下制作的,换句话说,这些值实际上应该在模型空间下进行插值计算,但是投影是一个损失维度的变换,我们单纯地用最终二维屏幕上的位置距离去插值,如果Z全都一致,那没有影响,在投影平面均匀变换的值,对应相机空间(推回模型空间也一样)也是均匀变换的。但是像右图,在投影平面两段相同距离对应相机空间的距离就不同了。

我们在推导投影矩阵的时候,得到过投影点的坐标(Nx/z,Ny/z,N),换句话说,投影后的X’本身就是与1/Z呈线性关系的,然后我们还知道uv坐标与x呈线性关系,实际上就是uv坐标与1/z呈线性关系,那么通过这样一个方式,我们在三角形进行设置时,强行把uv除以一个z,那么此时uv就变成了uv/z,这个值是与投影后的nx/z呈线性关系的,也就是说我们可以在屏幕空间根据距离进行线性插值得到一点上的确切的uv/z值。不过还有一个问题,我们得到了uv/z,还需要把uv还原,也就是得到uv,我们还需要再求出当前点的1/z值,这个值被我们存在了z坐标上,我们在每个点采样的时候,再把z乘回去,就得到了这一点真正的uv坐标值(最后除以z和w其实效果差不多,两者其实也是线性关系)。

inline void ApcDevice::PrepareRasterization(Vertex& vertex)
{//透视除法&视口映射//齐次坐标转化,除以w,然后从-1,1区间转化到0,1区间,+ 1然后/2 再乘以屏幕长宽float reciprocalW = 1.0f / vertex.pos.w;vertex.pos.x = (vertex.pos.x * reciprocalW + 1.0f) * 0.5f * deviceWidth;vertex.pos.y = (1.0f - vertex.pos.y * reciprocalW) * 0.5f * deviceHeight;//将其他数据转化为1/zvertex.pos.z *= reciprocalW;vertex.u *= vertex.pos.z;vertex.v *= vertex.pos.z;
}

在采样时,使用了:

int errorValue = dy2 - dx;
for (int i = 0; i <= dx; i++)
{float t = (x - x0) / (x1 - x0);float z = Vertex::LerpFloat(v0.pos.z, v1.pos.z, t);float realz = 1.0f / z;float u = Vertex::LerpFloat(v0.u, v1.u, t);float v = Vertex::LerpFloat(v0.v, v1.v, t);	Color c = tex->Sample(u * realz, v * realz);DrawPixel(x, y, c);x += stepx;errorValue += dy2;if (errorValue >= 0){errorValue -= dx2;y += stepy;}
}

经过透视校正纹理采样后的效果:


logo的效果也正常啦:


ZBuffer算法

人的需求总是无限的,贴上了图,还是感觉不够爽,毕竟不是一个立体的东西,只是个面片,下面我决定搞个模型进来。不过我不打算引入第三方库,导入fbx那不是软渲染干的事儿了,这个东西在opengl玩更好。所以我手撸了个立方体数据,直接写个顶点缓存,每个点加个uv数据。然后坐等我的立方体出现:


额,好像。。。哪里不太对。。。立方体面之间的遮挡关系错乱了。换句话说,我们没有深度测试,没有办法保证立方体面渲染的顺序,我们需要的是像素精度的深度保证。下面来加一个ZBuffer。

其实ZBuffer的思想比较简单,就是在逐像素增加一个缓存,每次绘制的时候,把当前深度也存储进这个buffer,下次再绘制该像素的时候,先判断一下当前像素的z值,如果比该值小或相等的话,说明离得更近(仅考虑ZTest LEqual)。这种情况下就可以更新当前像素点的颜色值,并且可以选择更新深度缓存。

我们先申请一块屏幕大小的float类型内存:

zBuffer = new float*[deviceHeight];
for (int i = 0; i < deviceHeight; i++)
{zBuffer[i] = new float[deviceWidth];
}

然后每次渲染之前,除掉ClearColorBuffer外,还要把DepthBuffer也Clear掉:

void ApcDevice::Clear()
{BitBlt(screenHDC, 0, 0, deviceWidth, deviceHeight, NULL, NULL, NULL, BLACKNESS);//ClearZfor (int i = 0; i < deviceHeight; i++){for (int j = 0; j < deviceWidth; j++){zBuffer[i][j] = 0.0f;}}
}

每次绘制时进行深度判断,深度检测失败不进行绘制,深度检测成功此处默认开启ZWrite:

bool ApcDevice::ZTestAndWrite(int x, int y, float depth)
{//上面只进行了简单CVV剔除,所以还是有可能有超限制的点,此处增加判断if (x >= 0 && x < deviceWidth && y >= 0 && y < deviceHeight){if (zBuffer[y][x] <= depth){zBuffer[y][x] = depth;return true;}}return false;
}

有了深度缓存,我们的立方体就完整啦:


深度缓存,我采用的是1/Z,所以上面的深度默认为0表示无限远。为了更好的效率,我的ZTest实际上放在了计算扫描线时每次插值计算出Z之后立刻就进行深度检测,其实类似于Early-Z,而不是实际上真正渲染时的ZCheck,因为我没有考虑Alpha Test的情况,也不需要考虑分支的问题,直接Cull掉是性能最好的。

int errorValue = dy2 - dx;
for (int i = 0; i <= dx; i++)
{float t = (x - x0) / (x1 - x0);float z = Vertex::LerpFloat(v0.pos.z, v1.pos.z, t);float realz = 1.0f / z;if (ZTestAndWrite(x, y, realz)){float u = Vertex::LerpFloat(v0.u, v1.u, t);float v = Vertex::LerpFloat(v0.v, v1.v, t);//Color c = Color::Lerp(v0.color, v1.color, t);Color c = tex->Sample(u * realz, v * realz);DrawPixel(x, y, c);}x += stepx;errorValue += dy2;if (errorValue >= 0){errorValue -= dx2;y += stepy;}
}

关于深度,其实还有很多可以玩的,不过这么好玩的东西,还是另起一篇blog吧。

最后,再来一张动图:


总结

本文主要实现了基本的光栅化渲染器的一些常见的特性,MVP矩阵变换,简单CVV剔除,视口映射,光栅化,透视校正纹理采样等等。目前还有基本的光照,背面剔除,线框渲染,相机控制等几个是我打算加的。有些算法肯定很古老,而且GPU的具体实现我不敢妄加揣测,填软渲染的坑主要是为了学习的目的,加深一下对渲染知识的理解。其实实现的过程还是蛮有意思的(有点找到了几年前写了第一个命令行版本的2048的那种乐趣,当时自己玩了半宿,一边玩一边手舞足蹈,室友以为我疯了呢),很久不写的C++又温习了一下,再一个方面就是需要考虑优化了,让我清晰地意识到逐像素计算是真的很费很费,面片贴脸的时候基本就跑不动啦!!!项目从开始写的时候,只有一个三角形没经过变换,没有采样,FPS上百,逐渐增加特性之后帧率一度掉到了个位数,后来稍微优化了一下,稳定在了20-25帧,还是有很大的优化的空间的。

代码的话直接开源吧,第一次用Git,如果能您赏个星星什么的就更好啦。本人才疏学浅,如果您发现了什么问题,还望批评指正。


查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. 一个可玩的贪吃蛇源代码

    原本以为做一个贪吃蛇的雏形很简单,因为它的行为很简单,但是到具体开发的时候发现有很多细节需要注意:键盘监听 如何让蛇移动起来 如何控制蛇的速度 如何让蛇边长 蛇碰到身子要死亡 碰到墙也要死亡 如何控制蛇的速度 蛇不能后退走 如何控制蛋的随机出现 如何控制蛋的颜色变化 …...

    2024/4/17 18:15:10
  2. 服务器使用Tomcat配置server.xml文件通过域名直接跳转到项目

    最近自己注册了一个域名,然后尝试着在服务器使用tomcat发布项目,并通过域名直接跳转到自己指定的页面,如www.xxx.com直接打开自己项目主页或者其他页面,不用通过localhost:8080/xxx/index.jsp 去打开自己的项目主页 下面是配置的方法: 首先需要把项目导出为xxx.war格式,然…...

    2024/4/17 18:15:34
  3. 从芯片到系统的整个产业链软件

    ANSOFT产品: Ansoft HFSS v11.1.1 Full-ISO 1CD(HF/RM/EM,世界上第一个商业化的三维结构电磁场仿真软件。可分析仿真任意三维无源结构的高频电磁场,可直接得到特征阻抗、传播常数、S参数及电磁场、辐射场、天线方向图等结果。该软件广泛应用于无线和有线通信、计算机、卫星、雷…...

    2024/4/17 18:19:46
  4. Tomcat调节启动时间

    web工程部署时需要加载很多类和文件,需要的时间比较长。如果时间超过了45秒,tomcat就会自动退出并且报错。双击tomcat server容器,修改配置文件中的启动时间。进入tomcat配置文件界面后,点击右侧的Timeouts选项,展开里面的配置内容就可以修改启动时间。直接输入具体的启动…...

    2024/4/17 18:15:22
  5. 演员刘涛入职阿里,职级相当于“集团副总裁”,年薪秒杀阿里高管

    来源 |和讯网5月9日,刘涛发微博宣布,自己正式加入阿里巴巴大家庭,成为了聚划算官方优选官,花名刘一刀,并且晒出了工牌。近年来已有不少明星加盟阿里巴巴,例如花名矮大紧的高晓松,担任阿里娱乐战略委员会主席;花名阿拉雷的雷佳音,担任支付宝会员体验官;花名娜比的欧阳…...

    2024/4/20 6:23:54
  6. Android压缩图片和libjpeg库

    前言 Fjpeg使用Fjpeg注意如何使用 如何压缩图片只改变在硬盘的存储大小 如何改变图片分辨率让其Bitmap对象可以加载到内存中 关于重载版本开始学习之旅 补充知识的结论修改图片分辨率 防止在Android加载Bitmap的时候oom内存溢出 解决方案1 解决方案2希望压缩图片方便网络传输 第…...

    2024/5/3 7:52:04
  7. Javascript模块化开发AMD规范与CMD规范的区别

    模块化开发能够提高开发的效率,并且能优化性能。当需要什么功能模块时,去加载需要的模块,而不是全部功能都加载到页面中。但是模块化有规范,我们前端开发必须遵守。像W3C标准,这都是规定的,就像公式一样,必须按照公式,才能得出结果。 目前JavaScript模块规范有两种形式…...

    2024/4/11 19:47:43
  8. (完整源码)H5贪吃蛇小游戏——HTML+CSS+JavaScript实现

    最近因为帮助大一的学妹做网页,又接触了一下前端方面的知识。(学妹已经成了女朋友!!!,嘻嘻)感谢自己当年认真学习,大三了好多东西还能拾起来,哈哈。我翻出了我当年写的代码,还真不少呢。今天复习复习,看一看那时候写的一个贪吃蛇的代码。当时用了一整天,跟着今日头…...

    2024/4/19 16:20:10
  9. 开源EDA软件-KiCAD

    目录使用背景 使用背景 之前工作中一直用的是AD,但是由于版权问题和目前中美关系,最近选择了一款开源的EDA软件-KiCAD。这款软件界面实际类似于Protel,操作习惯类似于Candence。只要不是设计很复杂的产品,这款软件完全够用。后续我会发布一些关于这个软件的使用教程和一些心…...

    2024/4/20 6:13:49
  10. Tomcat配置成功,但输入http://localhost:8080/不显示界面的解决方法

    一、打开命令窗口(win+r),输入cmd,enter;二、输入startup;三 、将DOS窗口缩小化,然后打开浏览器输入http://localhost:8080/,就卡伊看到小猫界面了;...

    2024/4/17 18:15:35
  11. 从图像生成自动描述:对模型,数据集和评估方法的综述

    摘要从自然图像生成自动描述是一个具有挑战性的问题,近来受到计算机视觉和自然语言处理社区的大量关注。 在本次调查中,我们根据他们如何将这个问题概念化的现有方法进行分类,即将描述作为生成问题或作为视觉或多模态表征空间上的检索问题的模型。 我们提供了对现有模型的详…...

    2024/4/17 18:14:28
  12. 贪吃蛇全代码

    <script> /* * 分析: 食物 , 小蛇 , 游戏 ,三个对象 * */ //一:食物对象 ((function () {//创建一个空数组,保存每一个小方块,方便删除小方块var elements = [];//首先要通过构造函数创建对象 属性function Food(width, height, color, x, y) {this.width = width…...

    2024/4/17 18:16:22
  13. ADS系列 - 定向耦合器设计教程1

    相关文章: ADS-SystemVue 文章集合页ADS系列 - 混频器设计 - 混频器原理介绍及仿真1ADS系列 – 低噪声放大器(LNA)模型下载安装及 LNA仿真设计Keysight的 SystemVue 介绍及与 ADS 区别对比Ansys-HFSS天线仿真结果导出为S1P文件,再导入到ADS中进行匹配仿真1.1 耦合器使用90混…...

    2024/5/2 14:53:40
  14. Tomcat配置任意目录下的Web应用程序

    每一个Web应用程序都有一个起始目录,在开发和部署Web应用程序时,通过上下文路径(Context path)来区分不同的Web应用程序。Tomcat默认的Web应用程序的起始目录(也称为上下文根)是%CATALINA_HOME%/webapps/ROOT,例如一个关于Hello World的Web应用程序的起始目录是%CATALIN…...

    2024/5/3 1:55:29
  15. Android开发——常见的内存泄漏以及解决方案(二)

    0.前言 上一篇常见的内存泄漏以及解决方案(一) 中已经对部分可能会引发内存泄漏的情况进行了阐述,此篇将从图片、动画等资源角度介绍可能会造成内存泄漏的情况以及应对方法。8. 用缓存避免内存泄漏很常见的一个例子就是图片的三级缓存结构,分别为网络缓存,本地缓存以及内…...

    2024/3/31 18:29:48
  16. 前端知识体系及修炼攻略

    前端简介 Web前端开发工程师是一个很新的职业,在国内乃至国际上真正开始受到重视的时间不超过5年。Web前端开发是从网页制作演变而来的,名称上有很明显的时代特征。随着人们对用户体验的要求越来越高,前端开发的技术难度越来越大,Web前端开发工程师这一职业终于从设计和制作…...

    2024/4/17 18:20:11
  17. 在 Ubuntu 中手动安装任何版本的 Firefox

    https://linux.cn/article-2697-1.html http://blog.sina.com.cn/s/blog_49f914ab0100hfss.html把firefox完全卸载掉重装: 查看安装的firefox版本: dpkg --get-selections |grep firefox 根据命令结果卸载重装 比如: $ dpkg --get-selections |grep firefox firefox install…...

    2024/4/17 18:18:17
  18. Linux安装tomcat,配置环境变量

    一、 安装tomcat需要先配置jdk,所以没有配置jdk同学,先移步Linux安装JDK二、卸载tomcat检查linux是否安装tomcatrpm -qa|grep tomcat查看下系统信息,确认是32位还是64位:uname -a输入rpm -qa|grep tomcat如果有通过rpm -e `rpm -qa|grep tomcat`(或rpm -e 加上面rpm -qa|gr…...

    2024/4/12 14:14:41
  19. 对抗生成网络(GAN)学习笔记

    生成模型与判别模型判别模型:由数据直接学习决策函数Y=f(X)或条件概率分布P(Y|X)作为预测模型,即判别模型。判别方法关心的是对于给定的输入X,应该预测什么样的输出Y。 生成模型:由数据学习联合概率分布P(X,Y), 然后由P(Y|X)=P(X,Y)/P(X)求出概率分布P(Y|X)作为预测的模型…...

    2024/4/19 17:24:29
  20. InstaCode 2015 full\

    CIMCO.Software.Suite.v6.01.31.zip Crystal.Impact.Diamond.v3.2e.zip DK.Design.Suite.and.PDK.v5.0.SP5.zip Mentor.Graphics.ePD.2004.Spac2.zip TraCFoil 3.1.0.E机翼及翼肋设计软件.rar Bentley.Substation.V8i.SS7.08.11.12.75\ Celoxica.DK.Design.Suite.and.PDK.v5.0.…...

    2024/4/19 15:44:52

最新文章

  1. 量子城域网建设设备系列(二):量子密钥管系统(KMS)

    在上文介绍光量子交换机的文章中我们提到&#xff0c;量子保密通信网络的通道切换是由量子密钥管理系统&#xff08;Key Management System&#xff0c;KMS&#xff09;给光量子交换机下发信道切换指令&#xff0c;实现整个网络中任意两对量子密钥分发终端的量子信道互联互通&a…...

    2024/5/3 9:55:04
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. DDIM,多样性与运行效率之间的trade off

    DDPM的重大缺陷在于其在反向扩散的过程中需要逐步从 x t x_t xt​倒推到 x 0 x_0 x0​&#xff0c;因此其推理速度非常缓慢。相反&#xff0c;DDPM的训练过程是很快的&#xff0c;可以直接根据 x 0 x_0 x0​到 x t x_t xt​添加的高斯噪声 ϵ \epsilon ϵ完成一次训练。 为了解…...

    2024/5/2 19:10:17
  4. 使用阿里云试用Elasticsearch学习:1.3 基础入门——搜索-最基本的工具

    现在&#xff0c;我们已经学会了如何使用 Elasticsearch 作为一个简单的 NoSQL 风格的分布式文档存储系统。我们可以将一个 JSON 文档扔到 Elasticsearch 里&#xff0c;然后根据 ID 检索。但 Elasticsearch 真正强大之处在于可以从无规律的数据中找出有意义的信息——从“大数…...

    2024/4/30 17:12:04
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/5/1 17:30:59
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/5/2 16:16:39
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/29 2:29:43
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/5/2 9:28:15
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/27 17:58:04
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/27 14:22:49
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/28 1:28:33
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/30 9:43:09
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/27 17:59:30
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/5/2 15:04:34
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/28 1:34:08
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/26 19:03:37
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/29 20:46:55
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/30 22:21:04
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/5/1 4:32:01
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/4/27 23:24:42
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/28 5:48:52
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/30 9:42:22
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/5/2 9:07:46
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/30 9:42:49
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57