IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 图形学基础|实时阴影渲染 -> 正文阅读

[数据结构与算法]图形学基础|实时阴影渲染

图形学基础|实时阴影渲染

一、前言

很早之前,在图形学基础 | 阴影技术 Shadow Map中,简单地介绍一下Shadow Map实现阴影的思路以及阴影存在的问题。

本文将再重新整理一下,并介绍PCF、PCSS、VSM等阴影算法,温故而知新。

二、Standard Shadow map

2.1 基本原理

在光栅化算法中,基于ShadowMap的实现的阴影较为常见。

由于光栅化的渲染管线相比基于光线追踪的实现方式缺少全局性信息,每个 fragment(片元) 在着色时并不清楚全局的光照情况,无法直接判断自己是否处于阴影中,因此需要额外的预渲染阶段

第一次渲染中以光源位置作为视点

  • 基于 Z-buffering 算法,将每个像素点的深度值(z-depth)也就是距离光源最近的对象距离记录在 Z-buffer 中,生成 Shadow Map

第二次场景渲染时,以正常摄像机作为视点

  • 将每个 fragment 到光源的距离和 Shadow Map 中保存的深度值进行比较,用于判断fragment是否被其他物体遮挡,处于阴影之中。

在这里插入图片描述

整体思路就是这样,针对不同的光源类型有一些额外的注意点。比如:

  • 平行光(Directional Light)并不存在确定的光源位置,在投影矩阵的选择上应该采用正交投影而非透视投影;
  • 平行光和聚光灯(Spot Light)都有固定的方向,而点光源(omnidirectional shadow maps)向四面八方发光。思路是一致,具体可以使用 Cubemap 保存 Shadow Map。

Pass1,以光源的视角进行渲染时,要注意投影光源的投影矩阵的计算!

  • 因为投影矩阵间接决定可视区域的范围,以及哪些东西不会被裁切;
  • 需要保证投影视锥(frustum)的大小,以包含打算在深度贴图中包含的物体。当物体和片段不在深度贴图中时,它们就不会产生阴影。

可以通过物体的包围盒来计算!代码如下:

在这里插入图片描述

Pass2,采样ShadowMap的UV必须经过ClipSpace、透视除法和映射到纹理空间(而非真实的Width和Height)。

  • 需要在Shader进行模拟这个过程。

在这里插入图片描述

由于笔者使用的是DirectX12,其屏幕空间的UV需要反转一下,如下左图:DX的ClipSpace,右图:DX的屏幕空间。

在这里插入图片描述

2.2 ShadowMap问题

2.3.1 精度问题

当物体到光源的距离过远时,使用RGBA 中任何一个分量存储深度值都会存在精度丢失问题,毕竟只有1byte。

合适的做法是:在 Shadow Fragment Shader 中充分利用四个分量也就是 4 bytes 存储。

2.3.2 深度偏移问题

现象如Shadow Acne(完全受光的屏幕上回出现条纹状的自阴影)。

在这里插入图片描述

出现的原因如下:

在这里插入图片描述

Shadow Map 的分辨率是离散、有限的,多个 fragment 会对应到同一个纹素;如上图,图片每个斜坡代表深度贴图一个单独的纹理像素。

特别说明:上面的图是错误的。黑色的部分其实应该是被照亮的地方,而下图才是笔者认为正确的,红色圈起来的地方是被错误变成阴影的地方

在这里插入图片描述

常见的解决办法是:

  1. 增加 Shadow Map 的分辨率只能减少可能性,并不能完全避免这个问题;(并且增加分辨率意味着带来宽带的压力)
  2. 给 Shadow map 中保存的深度值增加一个偏移值(Depth bias),即在一定的深度范围偏移下,不认为是阴影。

方法2的示意图如下:

在这里插入图片描述

  • 下图通过一个偏移使得几何体完全在阴影之中,避免了条纹状的自阴影问题。

在这里插入图片描述

所以这个偏移值的选择十分重要,其中若采用一个固定值例如 0.005,当表面法线与光源方向夹角很大时还是会出现。

更好的做法是:根据法线方向和光线方向计算:当法线和光线夹角较小的时,深度偏移值取较小的值;

float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);

偏移值(Depth bias) 太大会造成Peter Pan现象(物体似乎飘在了空中),也叫漏光(light leaking),如下图所示。

解决的方法,当然就是不要使用太大的shadow bias

在这里插入图片描述

2.3.3 走样问题

锯齿状的痕迹也被称作走样(Aliasing),如下图所示。

原因:Shadow Map是离散的,分辨率有限。多个fragment对应同一个纹素了。

解决方法:反走样、抗锯齿。

在这里插入图片描述

2.3 实现基础阴影

Shader代码:

  • 直接比较当前深度和ShadowMap中的深度值。

在这里插入图片描述

得到了一个很难看的结果:出现了自阴影等现象。

在这里插入图片描述

采用2.3.2中提到的添加一个偏移值方法,并加入与光源方向相关的计算。

在这里插入图片描述

这样修改之后,就可以看到一个阴影的基础效果。

  • 亮处和阴影的过渡很锐利,阴影边缘有锯齿现象

在这里插入图片描述

硬件上的一些与ShadowMap有关的参数:参考:D3D12_RASTERIZER_DESCDepth Bias

  • CullMode:设置剔除面(正面or背面)
  • DepthBias:Depth value added to a given pixel(用于加到像素上的深度值)
  • DepthBiasClamp:Maximum depth bias of a pixel(最大的深度偏移)
  • SlopeScaledDepthBias:Scalar on a given pixel’s slope(Scale参数)

其中,后三者用于给深度做一个Offset(硬件帮忙计算),可以进行设置。

在这里插入图片描述

三、PCF(Percentage-Closer Filtering)

3.1 软阴影和硬阴影

如下图所示,a点光源之间没有任何物体遮挡,因此是完全照亮(Lit)的。

而地面也就是接受者(receiver)上的 c 点被遮挡者(occluder)立方体遮挡,处于本影区(umbra)。

b 点处于被部分遮挡形成的半影区(penumbra)。

在这里插入图片描述

2.3 实现的基础阴影,其边缘与亮处的过渡非常锐利,这是硬阴影(hard shadows)

但是现实中的光源毕竟本身有体积,会形成拥有半影区的软阴影(soft shadows)

两者的关系不是简单地将硬阴影的边缘模糊化处理就能得到软阴影,根据我们日常生活中的经验,光源和接受者的距离越近,软阴影的边缘就越清晰(软度降低)。

3.2 PCF原理

PCF(Percentage-Closer Filtering),是Shadow Map的扩展技术,用来提供一种人工伪造的软阴影

相比于基本的ShadowMap算法,其主要的区别在于:

  • 计算着色点与Shadowmap中该点的深度值比较时,不仅采样当前像素点的深度,同时采样周围多个像素Shadowmap的深度值,逐一比较并求平均值,从而得到一个从0到1的连续分布,能够表现不同明暗程度的阴影。(而基本的Shadowmap算法结果非0则1)。

注:这里是将深度比较值的结果进行平均,而非先对采样得到的深度值进行平均再比较!

如下图:

  • 左边是正确的软阴影实现过程,区域光源在着色点不均匀部分。

  • 右边表示的PCF方法则通过获取着色点周围的一系列点的由点光源造成的深度值,与P点深度比较再计算一个平均值(不是平均深度,也不是平均结果),近似得到着色点软阴影程度。

在这里插入图片描述

PCF的方法不是基于物理的,即并非对光源进行采样,结果依赖于接收表面,且与遮挡物距离也不会影响最终结果。

因而,这只是一种对软阴影的近似方法,但也能在许多情况下提供一个可信结果。

PCF中在着色点周围如何采样会影响软阴影结果。如下图所示:

  • P1是采样了一个4*4的方格区域,显示出较大的走样纹;
  • P2使用了P4的泊松采样图案执行采样,只显示少量走样纹;
  • P3对于P4的采样图案在每一个着色点上进行了一个随机旋转再采样,将阴影走样转为了更加可接受的噪声。

在这里插入图片描述

实时阴影总结提供了PCF多种采样方式(Poisson DiskStratified Poisson DiskRotated Poisson Disk)得到的效果对比。

PCF的优点:

  • 实现简单,硬件直接提供支持。
  • 容易与PCSS等动态半影算法相结合(接下来会介绍)。

PCF的缺点:

  • 边缘有噪点
  • 采样数增多,开销较大;

3.3 实现

这里实现的一个5X5的PCF:

float Visibility(float d, float2 uv)
{
	float z = ShadowMap.Sample(ShadowSampler, uv).x;
	return z >= d;
}

float SampleFixedSizePCF(float3 ShadowPos, float3 LightDirection, float3 Normal)
{
	float shadow = 0.0f;

	float NumSlices;
	float2 ShadowMapSize;
	ShadowMap.GetDimensions(0, ShadowMapSize.x, ShadowMapSize.y, NumSlices);
	float2 texelSize = 1.0 / ShadowMapSize;

	float CurentDepth = saturate(ShadowPos.z);

	for (float i = -2.5f; i < 3.f; ++i) 
	{
		for (float j = -2.5f; j < 3.f; ++j) 
		{
			shadow += Visibility(CurentDepth, ShadowPos.xy + float2(i, j) * texelSize);
		}
	}

	shadow *= 0.04f;
	shadow = saturate(shadow);
	return shadow;
}

float ComputeShadow(float4 ShadowCoord, float3 Normal)
{
	float3 ShadowPos = ShadowCoord.xyz;
	return SampleFixedSizePCF(ShadowPos, -LightDirection, Normal);
}

PixelOutput ps_main(VertexOutput Input)
{
	PixelOutput output;
	float4 texColor = DiffuseTexture.Sample(LinearSampler, Input.tex);
	float Shadow = ComputeShadow(Input.ShadowCoord, Input.normal);	
	output.outFragColor = texColor * saturate(Shadow);
	return output;
}

得到的效果如下:

  • 可以看到相比于2.3的结果,可以看到阴影边缘的锯齿基本不存在

在这里插入图片描述

近距离看会有一定的走样条纹,可采用上述提到的泊松采样图+随机旋转进行优化。

在这里插入图片描述

四、PCSS(Percentage-Closer Soft Shadows)

PCF由于采样区域是固定的大小,因此会在所有地方展示同样形状的软阴影,但这样并不符合现实的现象。

如下图,左边使用了一个小区域采样的PCF,中间使用了一个较大区域的采样,其结果显然都不正确。

合理的软阴影会像最后一张这样,在遮挡物与地面靠近处阴影显得更硬,在较远处更加模糊。而这第三张图片正是通过PCF的改进算法PCSS计算得到的。

在这里插入图片描述

如3.1(软阴影与硬阴影)介绍的:现实生活中,光源都是有体积的,从阴影区到无阴影区的这一部分叫做半影。在半影区只有一部分光源被遮挡,并且半影的大小跟遮挡物的距离有关

如下图, W L i g h t W_{Light} WLight?表示模拟的光源大小,Blocker为遮挡物,Receiver为我们接收光源的面。 W p e n u m b r a W_{penumbra} Wpenumbra?即为半影,也就是Filter的范围。

在这里插入图片描述

根据相似三角形的关系,可以求得半影区域 W p e n u m b r a W_{penumbra} Wpenumbra?的计算公式:

W p e n u m b r a = ( d R e c e i v e r ? d B l o c k e r ) ? W L i g h t d B l o c k e r W_{penumbra} = \frac{(d_{Receiver}-d_{Blocker}) \cdot W_{Light}}{d_{Blocker}} Wpenumbra?=dBlocker?(dReceiver??dBlocker?)?WLight??

其中

  • d R e c e i v e r d_{Receiver} dReceiver?:接收物(着色点)距离光源的竖直距离;
  • d B l o c k e r d_{Blocker} dBlocker?:遮挡物距离光源的竖直距离;

d R e c e i v e r d_{Receiver} dReceiver?是已知的,那么 d B l o c k e r d_{Blocker} dBlocker?(遮挡物的深度)如何确定呢?

阴影图中不就存储了深度吗?

是否能够采样shadow map的单个点作为遮挡物的深度呢

  • 答案是否定的!

  • 因为如果该点的深度和周围点的深度差距较大(遮挡物的表面陡峭或者对应点正好有一个孔洞)将产生一个错误的效果。

PCSS算法选择平均的遮挡距离来代替!

具体方法是:在shadow map上采样该点周围取许多点来计算各自的遮挡距离后求平均

这样又涉及到一个问题,采样的范围要如何确定呢?

一种方法是:采用固定的范围,例如 4 × 4 4 \times 4 4×4 16 × 16 16 \times 16 16×16

另一种更好的方法是:动态计算遮挡范围,如图所示。

  • 把shadow map放在相机的近截面,然后将光源和要渲染的点相连,在shadow map上截出来的面就是要查询计算平均遮挡距离的部分,这部分的深度求一个均值,就是Blocker到光源的平均遮挡距离。

在这里插入图片描述

在这里插入图片描述

同样的,根据相似三角形可以求出所需查找的 S e a r c h R a d i u s SearchRadius SearchRadius

对PCSS算法进行一下小结,其步骤如下:

  1. Blocker Search:先搜索一个范围内的那些texels是遮挡物,将这个范围内所有遮挡物的深度记录下来并取平均值;
  2. 用取得的遮挡物深度距离来算filtering的范围,即利用公式计算出 W p e n u m b r a W_{penumbra} Wpenumbra?
  3. 获得了Filter的范围,就可以用PCF计算Shadow;

可以看出,PCSS本质上就是:求出阴影中需要做PCF的半影部分再使用PCF计算。

这样动态调节了半影范围,即动态设置了PCF的搜索范围,这样使得我们的硬阴影部分清晰,软阴影部分模糊,动态的实现了不错的软阴影效果。

但是第一步和第三步都涉及到了在Shadowmap上采样一个范围的深度,这样是非常慢的。

五、VSM(Variance soft shadow mapping)

5.1 算法

上面介绍的PCSS算法,虽然可以较为准确地计算出需要进行PCF过滤的区域大小,但是其效率是非常的低的。

第一步和第三步都要在采样某个区域的深度进行比,这会导致其效率比较低,比较耗时。

本小节介绍的 VSM(Variance soft shadow mapping) 算法,就是解决PCSS第一步和第三步慢的问题。

5.1.1 PCSS第三步问题

第三步(PCF),即要和周围选择的采样点进行深度比较,得到有多少比当前深度浅或深。

但其实我们并不是想知道周围选择的采样点每个深度,而是想知道当前着色点的深度在周围深度中的比例。

基于此,可以把深度当作一个正态分布。而正态分布仅需要知道方差和均值即可。

问题:均值如何获得?

在ShadowMap中,一个区域的平均值,即均值。

问题:方差如何获得?

想要快速得到方差,概率论中有一个经典公式,即:一个随机变量的方差等于它平方的期望减去它的期望的平方

V a r ( X ) = E ( X 2 ) ? E 2 ( X ) Var(X) = E(X^2) - E^2(X) Var(X)=E(X2)?E2(X)

其中:

  • E 2 ( X ) E^2(X) E2(X),即期望的平方。取ShadowMap的一个区域的平均值(即均值),平方一下就得到了。
  • E ( X 2 ) E(X^2) E(X2),即平方的期望,即在生成shadow Map时,额外存储一个通道,这个通道的每一个像素的深度都是原来的深度的平方,再获得一个区域的均值,就获得了平方的期望。

有了均值方差,就能得到正态分布

接下来,就可以通过计算面积CDF(X),得到有多少texels的深度比当前深度要小!

注:CDF(累积分布函数)即F(x)表示的是P(X<x),即表示随机变量小于x的概率。

对于通用的高斯的PDF,可以把积分的值打成表,积分的值就是误差函数(error function)。这个积分没有解析解,只有数值解。

VSM采用了切比雪夫不等式来解决这个计算问题。

任意分布,只要有均值和方差,取一个值 t t t,就能得到其右边的面积,不大于( ≤ \le )求出的值。

右边的面积表示的是:随机变量大于x的概率。

公式如下图所示。

在这里插入图片描述

在这里插入图片描述

注意:使用这个公式的前提条件为: t t t必须大于均值。

Shader代码实现如下:

float Chebychev(float2 moments, float d)
{
	float mean		= moments.x;
	float variance	= max(moments.y - moments.x * moments.x, 1e-5f);
	float md		= mean - d;
	float pmax		= variance / (variance + md * md);

	pmax = smoothstep(0.1f, 1.0f, pmax);

	// NEVER forget that d > mean in Chebychev's inequality!!!
	return max(d <= mean, pmax);
}

小结:

  • 通过把切比雪夫不等式当约等式,就能得到一个深度比较结果的近似值,从而解决PCSS第三步计算慢的问题。

5.1.2 PCSS第一步问题

PCSS第一步做的是:得到一个范围内遮挡物的平均深度(是小于当前深度的有效深度的平均),其选择了一个范围采样了所有的texels。

这样的运行速度是比较慢的。

VSM定义以下量,遮挡物的平均深度为 Z o c c Z_{occ} Zocc?,非遮挡物的平均深度为 Z u n o c c Z_{unocc} Zunocc?

有公式如下:

N 1 N Z u o c c + N 2 N Z o c c = Z a v g \frac{N_1}{N} Z_{uocc} + \frac{N_2}{N} Z_{occ} = Z_{avg} NN1??Zuocc?+NN2??Zocc?=Zavg?

即,遮挡物所占比例 * 遮挡物的平均深度 + 非遮挡为所占比例 * 非遮挡平均深度 = 平均深度。

其中:

  • 遮挡物的平均深度 Z o c c Z_{occ} Zocc?,是我们要求解的;

  • 遮挡物所占比例和非遮挡为所占比例,二者和为1,可以用切比雪夫不等式近似得成。

  • 非遮挡物的平均深度 Z u n o c c Z_{unocc} Zunocc?,确实不知道!VSM做了一个假设,令 Z u n o c c = t Z_{unocc}=t Zunocc?=t,即用阴影接收者的深度作为非遮挡物的平均深度。

  • 平均深度 Z A v g Z_{Avg} ZAvg?,可以用Mipmap或SAT求得。

在这里插入图片描述

通过上述的近似和假设,就可以求解得到PCSS第一步所需求解的遮挡物的平均深度。

5.1.3 漏光问题

相比于PCSS,VSM有很多优点,但其有一个很大的问题,就是所谓的漏光问题。

VSM的漏光,实际来自于离光源最近的物体的软阴影边缘,如下图所示。

当计算C物体接受的阴影时,使用的是A和B相对光源的可见部分(红线和绿线)的深度期望和方差,但其实应该只基于物体B(蓝线和绿线)来计算。

于是 Δ x \Delta x Δx Δ y \Delta y Δy 比值越大,漏光会越明显,因为阴影贴图中并没有存储蓝色信息,因此无法完全消除漏光。

在这里插入图片描述

GPU PRO2 阴影篇介绍了几个简单的trick来减弱漏光。

例如:切除尾部,简单的切掉pmax函数的尾部,即将pmax的结果减去一个固定值来使得阴影整体变暗从而使漏光区域不明显。

但当是 Δ x \Delta x Δx Δ y \Delta y Δy 比值非常大时该方法难以奏效。

代码如下:

float Linstep(float a, float b, float v)
{
	return saturate((v - a) / (b - a));
}

// Reduces VSM light bleedning
float ReduceLightBleeding(float pMax, float amount)
{
	// Remove the [0, amount] tail and linearly rescale (amount, 1].
	return Linstep(amount, 1.0f, pMax);
}

5.2 实现

5.2.1 算法流程

在5.1中,我们介绍了VSM对PCSS算法中步骤一和步骤三运行速度慢的解决方法,那么在这里我们先总结一下VSM算法执行过程。

  1. 以光源视角渲染ShadowMap,与普通的ShadowMap不同,需要存储深度 d d d以及深度的平方 d 2 d^2 d2(用于后续计算方差);
  2. 对存储深度的纹理进行滤波模糊,因为我们需要平均深度,即公式中的 E ( X ) E(X) E(X)
  3. 对于任意一个着色点 p p p,该点的在光源视角下的深度为 t t t,投影到光源视角进行纹理采样,得到得到 p p p点的深度的期望 E ( d ) E(d) E(d)以及深度平方的期望 E ( d 2 ) E(d^2) E(d2),根据期望与方差的关系可以得出方差 σ 2 \sigma^2 σ2
  4. 用切比雪夫不等式近似求得Shadow值。

5.2.2 示例代码

float Linstep(float a, float b, float v)
{
	return saturate((v - a) / (b - a));
}

// Reduces VSM light bleedning
float ReduceLightBleeding(float pMax, float amount)
{
	// Remove the [0, amount] tail and linearly rescale (amount, 1].
	return Linstep(amount, 1.0f, pMax);
}

float ChebyshevUpperBound(float2 Moments, float t) 
{
	float Variance = Moments.y - Moments.x * Moments.x;
	float MinVariance = 0.0000001;
	Variance = max(Variance, MinVariance);
	// Compute probabilistic upper bound.
	float d = t - Moments.x;
	float pMax = Variance / (Variance + d * d);
   	
    // 较少漏光 
    float lightBleedingReduction = 0.0;
	pMax = ReduceLightBleeding(pMax, lightBleedingReduction);
    
    // t必须大于均值,切比雪夫不等式才可以使用
	return (t <= Moments.x ? 1.0 : pMax);
}

float ComputeShadow(float4 ShadowCoord, float3 Normal)
{
    // Moments.x 深度的均值;
    // Moments.y 深度平方的均值;
	float2 Moments = ShadowMap.Sample(ShadowSampler, ShadowCoord.xy).xy;    
	return ChebyshevUpperBound(Moments, saturate(ShadowCoord.z));
}

PixelOutput ps_main(VertexOutput Input)
{
	PixelOutput output;
	float4 texColor = DiffuseTexture.Sample(LinearSampler, Input.tex);
	float Shadow = ComputeShadow(Input.ShadowCoord, Input.normal);
	output.outFragColor = texColor * (saturate(Shadow));
	return output;
}

直接渲染阴影的效果如下:

  • 可以看出阴影的锯齿也是没有的。
  • 在Cube背面上侧由于深度比较接近,导致了漏光现象。

在这里插入图片描述

看了下虚幻这边的代码:

  1. 搭建了一个类似的场景,如下图所示,阴影如此漂亮。

在这里插入图片描述

  1. 通过RenderDoc截帧,发现其阴影图也存在漏光现象

在这里插入图片描述

  1. 通过Debug像素着色器,发现通过着色中 N o L NoL NoL就可以将背面的阴影完全变黑。

修改Shader如下:

  • NoL可以帮忙解决背面的漏光问题。
  • 加0.2让场景变得亮点,纯粹为了好看。
PixelOutput ps_main(VertexOutput Input)
{
	PixelOutput output;
	float4 texColor = DiffuseTexture.Sample(LinearSampler, Input.tex);
	float Shadow = ComputeShadow(Input.ShadowCoord, Input.normal);
    // NoL-Weight 
	float NoL = dot(-LightDirection, Input.normal);
	NoL = saturate(NoL);
	output.outFragColor = texColor * (NoL * saturate(Shadow) + 0.2);
	return output;
}

得到如下效果:

在这里插入图片描述

六、更多

更多阴影算法,将在后续进行练习和补充。

TODO:

  • AVSM(adaptive volumetric shadow maps)自适应体积阴影贴图;
  • CSM(Cascaded Shadow Maps)级联阴影;

参考博文

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2021-09-03 12:10:17  更:2021-09-03 12:10:35 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 1:38:50-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码