图形学基础|球谐光照(Spherical Harmonics Lighting)
球谐光照(Spherical Harmonics Lighting)
文章目录
- 球谐光照(Spherical Harmonics Lighting)
- 一、前言
- 二、球谐函数
- 2.1 基函数
- 2.2 投影与重建
- 2.3 应用
- 三、漫反射环境光
- 3.1 IrradianceMap
- 3.2 数学推导
- 3.3 实践
- 参考博文
一、前言
在学习图形渲染的过程中,一直對球谐函数(球谐光照)有一点了解,但没有亲手实现过。
关于球谐函数的数学概念是比较复杂的,笔者也实在无法完全理解,这篇文章只能从做东西的角度来说,公式是如何和代码进行对应的。
笔者的代码是基于DirectX
实现的。期间也遇到了一些坑,也顺便记录一下。
二、球谐函数
2.1 基函数
在数学中,一个基函数是一个函数空间(Function Space)中的一个基底,就像欧拉空间中的一个坐标轴一样。
在函数空间中,每个连续的函数都可以表示为基函数的线性组合。
例如,对于所有的二次多项式,可以用基函数组{1,t,t21,t,t^21,t,t2}进行线性组合得到,即a+bt+ct2a+bt+ct^2a+bt+ct2。这里{a,b,ca,b,ca,b,c}即表示各个基函数的系数。
球谐函数是将谐函数限制于球坐标系下的单位球面的一组基函数yi(t)y_i(t)yi(t),其各项常数系数为cic_ici,可以用来近似目标函数f(t)f(t)f(t)。
球谐函数的表达式定义如下:
f(t)≈f~=∑l=0∑m=−ll=+lclmylm=∑i=1Nciyif(t)\approx \tilde{f} = \sum_{l=0}\sum_{m=-l}^{l=+l}c_l^my_l^m = \sum_{i=1}^{N}c_iy_if(t)≈f~=l=0∑m=−l∑l=+lclmylm=i=1∑Nciyi
其中,N表示了系数或者说基底函数的数量,一个连续函数,需要无限个基函数的线性组合,才能完全恢复。
而ylmy_l^mylm是一个阶数(l,m)(l,m)(l,m)和角度(法线nnn)相关的量,称作球谐基。clmc_l^mclm为对应球谐基方向上的系数。
球面空间上的球谐函数ylmy_l^mylm可视化如下:
绿色表示球谐函数的值为正值,而红色表示球谐函数的值为负值;矢径越大球谐函数值的绝对值越大,反之矢径越小球谐函数值的绝对值越小。
而在实际的使用中,一般只取前几阶的球谐函数来近似球面上的函数。
例如,前三阶的球谐基函数如下:
l=0:
Y00=s=121πY_0^0 = s = \frac{1}{2} \sqrt{\frac{1}{\pi}}Y00=s=21π1
l=1:
Y1−1=py=34πyrY_1^-1 = p_y = \sqrt{\frac{3}{4\pi}} \frac{y}{r}Y1−1=py=4π3ry
Y10=pz=34πzrY_1^0 = p_z = \sqrt{\frac{3}{4\pi}} \frac{z}{r}Y10=pz=4π3rz
Y11=px=34πxrY_1^1 = p_x = \sqrt{\frac{3}{4\pi}} \frac{x}{r}Y11=px=4π3rx
l=2:
Y2−2=dxy=1215πxyr2Y_2^-2 = d_{xy} = \frac{1}{2}\sqrt{\frac{15}{\pi}} \frac{xy}{r^2}Y2−2=dxy=21π15r2xy
Y2−1=dyz=1215πyzr2Y_2^-1 = d_{yz} = \frac{1}{2}\sqrt{\frac{15}{\pi}} \frac{yz}{r^2}Y2−1=dyz=21π15r2yz
Y20=dz2=145π−x2−y2+2z2r2Y_2^0 = d_{z^2} = \frac{1}{4}\sqrt{\frac{5}{\pi}} \frac{-x^2-y^2+2z^2}{r^2}Y20=dz2=41π5r2−x2−y2+2z2
Y21=dxz=1215πzxr2Y_2^1 = d_{xz} = \frac{1}{2}\sqrt{\frac{15}{\pi}} \frac{zx}{r^2}Y21=dxz=21π15r2zx
Y22=dx2−y2=1415πx2−y2r2Y_2^2 = d_{x^2-y^2} = \frac{1}{4}\sqrt{\frac{15}{\pi}} \frac{x^2-y^2}{r^2}Y22=dx2−y2=41π15r2x2−y2
其中,r=1r=1r=1,更多公式请参考:wiki-Table_of_spherical_harmonics。
2.2 投影与重建
2.1中提到了球谐函数表达式,但还没有介绍其中的系数clmc_l^mclm是如何求解的。
其求解方法如下公式所示:
Clm=∫Sf(s)Ylm(s)dsC_l^m = \int_S{f(s)Y_l^m(s)}dsClm=∫Sf(s)Ylm(s)ds
其中,SSS表示积分区间单位球面。这个公式其实就是用原函数f(s)f(s)f(s)和对应的球谐函数Ylm(s)Y_l^m(s)Ylm(s)在整个球面空间积分即可。这个求解系数(生成球谐系数)的过程,称之为投影。
在实际过程中,投影所得的系数ClmC_l^mClm通过采用蒙特卡洛积分来近似求解。
Clm=∫Sf(s)Ylm(s)ds=1N∑j=1Nf(xj)w(xj)C_l^m = \int_S{f(s)Y_l^m(s)}ds = \frac{1}{N} \sum_{j=1}^{N}f(x_j)w(x_j)Clm=∫Sf(s)Ylm(s)ds=N1j=1∑Nf(xj)w(xj)
其中,权重w(xj)=1p(wj)w(x_j)=\frac{1}{p(w_j)}w(xj)=p(wj)1,对于球面均匀采样的而言,有:
w(xj)=1p(wj)=1S球面=14πw(x_j)=\frac{1}{p(w_j)}=\frac{1}{S_{球面}}=\frac{1}{4\pi}w(xj)=p(wj)1=S球面1=4π1
所以,所以球谐系数 (SH Coefficient)的数值估计表达式是:
Ci=∫Slight(v)Yi(v)ds≈1N∑j=1Nlight(xj)Y(xj)⋅4π=4πN∑j=1Nlight(xj)Y(xj)\begin{aligned} C_i = & \int_{S}{light(v)Y_i(v)}ds \\ \approx & \frac{1}{N} \sum_{j=1}^{N}{light(x_j)Y(x_j)\cdot 4\pi}\\ = & \frac{4\pi}{N} \sum_{j=1}^{N}{light(x_j)Y(x_j)} \end{aligned}Ci=≈=∫Slight(v)Yi(v)dsN1j=1∑Nlight(xj)Y(xj)⋅4πN4πj=1∑Nlight(xj)Y(xj)
而重建则是通过一系列的系数ClmC_l^mClm近似恢复函数fff的过程。
正如前面所描述的,因为我们不会存储无穷阶系数,对fff采用lll阶的球谐函数进行恢复。
f(t)≈f~=∑l=0∑m=−ll=+lclmylm=∑i=1Nciyif(t)\approx \tilde{f} = \sum_{l=0}\sum_{m=-l}^{l=+l}c_l^my_l^m = \sum_{i=1}^{N}c_iy_if(t)≈f~=l=0∑m=−l∑l=+lclmylm=i=1∑Nciyi
2.3 应用
在渲染中,球谐函数可以用来重建辐射亮度(Radiance),一般为环境贴图Cubemap。
以下是1阶到6阶SH重建辐射亮度的效果图:
可以看出:
- 当球谐函数的阶数越高,还原的效果越好。
- 球谐函数的结果是比较粗糙的,只能模拟低频信号。
笔者输入了一张HDR的Cubemap,重建辐射亮度如下:
- 效果不好,根本原理是球谐函数只能模拟低频信号,而HDR图像的数值范围较大,无法模拟好。
如果采用一张较为低频的图像,则是以下的结果:
当然,重建辐射亮度(Radiance)并非球谐函数最主要的作用。
其最重要的作用是第三节中要介绍的恢復IrradianceMap!
三、漫反射环境光
3.1 IrradianceMap
这里所说的环境光指的是天空盒/天空球(无限远)所发出的光,如此一来我们可以忽略掉位置信息而只考虑法线信息。
漫反射光照计算公式如下:
L(p,wo)=∫ΩL(p,wi)n⋅widwiL(p,w_o)=\int_{\Omega} L(p,w_i)n\cdot w_idw_iL(p,wo)=∫ΩL(p,wi)n⋅widwi
通常采用预积分的方式来生成一张环境贴图的IrradianceMap(下图右侧)。
在实时渲染时,仅需使用一个法线向量对立方体贴图进行采样,就可以获取该方向上的场景辐照度。
由于辐照度图(IrradianceMap)变化较为缓慢,看起来有点像环境的平均颜色或光照图,可以以较低的分辨率进行存储,例如64x64x6。
3.2 数学推导
这里要对球谐函数近似IrradianceMap的数学理论进行简单的介绍。
这里的数学推导摘錄自以下兩篇博客,寫的實在太棒了!建议观看原文!
筆者在這僅僅摘录了其中的结果!
【论文复现】Spherical Harmonic Lighting:The Gritty Details
【论文复现】An Efficient Representation for Irradiance Environment Maps
首先,对漫反射光照计算公式按照球谐函数进行展开:
KaTeX parse error: No such environment: align* at position 7: \begin{̲a̲l̲i̲g̲n̲*̲}̲ f(x) = & \sum…
其中,系数为clm=∫Ωf(w)Ylm(w)dwc_l^m = \int_{\Omega } f(w)Y_l^m(w)dw clm=∫Ωf(w)Ylm(w)dw
接下来,对光照方程进行简化。
将积分里的函数分为两个部分即:
{light(w)=L(p,w)t(w)=n⋅w\begin{cases} &\text{ } light(w) = L(p,w) \\ &\text{ } t(w) = n \cdot w \end{cases}{ light(w)=L(p,w) t(w)=n⋅w
然后我们分别对着两个函数进行球谐函数展开即:
{light(w)=∑i=0LiYi(w)t(w)=∑j=0tjYj(w)\begin{cases} &\text{ } light(w) = \sum _{i=0} L_i Y_i(w) \\ &\text{ } t(w) = \sum _{j=0} t_j Y_j(w) \end{cases}{ light(w)=∑i=0LiYi(w) t(w)=∑j=0tjYj(w)
其中,LiL_iLi和tjt_jtj都是常数。
将上述带回到光照积分公式中:
L(p,wo)=∫Ωlight(w)t(w)dw=∫Ω(∑i=0LiYi(w))⋅(∑j=0tjYj(w))dw=∫Ω∑i=0∑j=0(LiYi(w)tjYj(w))dw=∑i=0∑j=0Litj∫ΩYi(w)Yj(w)dw\begin{aligned} L(p,w_o) = & \int_{\Omega} light(w) t(w)dw \\ = & \int_{\Omega} (\sum _{i=0} L_i Y_i(w)) \cdot (\sum _{j=0} t_j Y_j(w)) dw \\ = & \int_{\Omega} \sum _{i=0}\sum _{j=0}(L_i Y_i(w) t_j Y_j(w)) dw \\ = & \sum _{i=0}\sum _{j=0} L_i t_j \int_{\Omega} Y_i(w)Y_j(w) dw \end{aligned} L(p,wo)====∫Ωlight(w)t(w)dw∫Ω(i=0∑LiYi(w))⋅(j=0∑tjYj(w))dw∫Ωi=0∑j=0∑(LiYi(w)tjYj(w))dwi=0∑j=0∑Litj∫ΩYi(w)Yj(w)dw
由于球谐系数的正交完备性,有且仅有i==ji == ji==j的时候,∫ΩYi(w)Yj(w)dw=1\int_{\Omega} Y_i(w)Y_j(w) dw=1∫ΩYi(w)Yj(w)dw=1。
则光照积分公式可以简化为:
L(p,wo)=∑i=0LitiL(p,w_o) = \sum_{i=0} L_i t_i L(p,wo)=i=0∑Liti
此时整个光照函数就简化为了常数积分之和。
我们接着分析球谐系数LiL_iLi和tit_iti。
分析LiL_iLi
Li=∫ΩL(p,w)Yi(w)dwL_i= \int_{\Omega } L(p,w)Y_i(w)dw Li=∫ΩL(p,w)Yi(w)dw
函数里面虽然有着色亲P,但实际与具体的着色点无关。因此这一项我们可以直接对整个天空盒进行积分。
伪代码如下:
for(pixel p : cubeMap)Li += p.color * Yi(normalise(p.position))*dw;
这里注意,我们直接将天空盒的位置信息进行归一化后就作为自变量。
因为我们这个积分是球面积分,需要球面上的点,天空盒的位置进行归一化就投影到球面上去了(就好像在球面上去点一样)。
分析tit_iti
ti=∫Ω(n⋅w)Yi(w)dwt_i= \int_{\Omega } (n \cdot w)Y_i(w)dw ti=∫Ω(n⋅w)Yi(w)dw
可惜,求tit_iti时域具体的着色器是有关的。因为我们需要知道法线的信息nnn。
这意味着如果要预计算tit_iti,也就需要对每一个方向的法线nnn都要算一组tit_iti,若采用三阶球谐(需存储9个系数),那就需要三张CubeMap(每一张存3个系数)。
- 还要和IBL的CubeMap一样大小的纹理。
伪代码如下:
// t与法线相关,只能把所有的法线都计算了
for(normal &n: sphere)
{for(pixel &p : Cubemap)ti[n] += dot(n,normalise(p.position)) * Yi(normalise(p.position)) * dw;
}
直接采用球谐系数替代原有光照的问题,即:针对每一个方向的法线都需要预计算出一组球谐系数。
球谐函数旋转
所谓的旋转,不是说直接去改变原函数,而是将传入自变量在三维空间中被旋转之后传入原函数。
// … 这里有一系列公式的推导,笔者也看不完全懂就不摘录了。
经过一系列推导,光照函数表示为:
L(n)=∑l=0∞∑m=−ll4π2l+1LlmtlYlm(n)L(n)=\sum_{l=0}^{\infty } \sum_{m=-l}^{l} \sqrt{\frac{4\pi }{2l+1}} L_l^m t_l Y_l^m(n) L(n)=l=0∑∞m=−l∑l2l+14πLlmtlYlm(n)
可以分为3项:
1)4π2l+1tl\sqrt{\frac{4\pi }{2l+1}} t_l2l+14πtl 为一系列常数。下面列出了前三阶的系数。
// Convolve with SH-projected cosinus lobe
float ConvolveCosineLobeBandFactor[] =
{PI,2.0f * PI/3.0f, 2.0f * PI/3.0f, 2.0f * PI/3.0f,PI/4.0f, PI/4.0f, PI/4.0f, PI/4.0f, PI/4.0f
}
2)LlmL_l^mLlm 为预计算的,Li=∫ΩL(p,w)Yi(w)dwL_i= \int_{\Omega } L(p,w)Y_i(w)dw Li=∫ΩL(p,w)Yi(w)dw。
3)Ylm(n)Y_l^m(n)Ylm(n) 为球谐基函数,参数是具体着色点的法线nnn。可以根据着色器中的法线进行实时计算。
在预计算LlmL_l^mLlm 时,其实可以将第一项也包含进来,一起进行存储cic_ici。如下公式所示:
ci=4π2l+1tl⋅∫ΩL(p,w)Yi(w)dwc_i = \sqrt{\frac{4\pi }{2l+1}} t_l \cdot \int_{\Omega } L(p,w)Y_i(w)dw ci=2l+14πtl⋅∫ΩL(p,w)Yi(w)dw
对这个式子Li=∫ΩL(p,w)Yi(w)dwL_i= \int_{\Omega } L(p,w)Y_i(w)dw Li=∫ΩL(p,w)Yi(w)dw使用蒙特卡洛积分I=∫f(x)dx=∫f(x)p(x)p(x)dx≈1N∑i=1Nf(X)p(x)I=\int f(x)dx = \int \frac{f(x)}{p(x)} p(x) dx \approx \frac{1}{N} \sum_{i=1}^{N} \frac{f(X)}{p(x)}I=∫f(x)dx=∫p(x)f(x)p(x)dx≈N1∑i=1Np(x)f(X)近似,有:
对于均匀的球面上采样,概率p=14πp=\frac{1}{4\pi}p=4π1,则有:
ci=4π2l+1tl⋅4πN∑LjYjc_i = \sqrt{\frac{4\pi }{2l+1}} t_l \cdot \frac{4\pi}{N}\sum L_jY_j ci=2l+14πtl⋅N4π∑LjYj
则渲染还原时为:
L(n)=∑i=0ciYi(n)L(n)=\sum_{i=0} c_i Y_i(n) L(n)=i=0∑ciYi(n)
此时求出的为SH近似出来的IrradianceMap。
3.3 实践
相较于2.2对Radiance的重建,3.2重建IrradianceMap仅多出来一项,其他完全相同。即4π2l+1tl\sqrt{\frac{4\pi }{2l+1}} t_l2l+14πtl 常数项。
而已仅需在预计算时乘以对应的常数项系数,或者在渲染时乘以对应的常数项系数,都可以获得IrradianceMap。
但是为什么要使用SH替代CubeMap呢?
原因很简单:
- 对于一个大的场景而言,每个位置的环境光都可能不同,如果每个点的环境光都采用一张Cubemap来存储,并且每次在Shader进行采样,那么这种方法无疑是非常昂贵的。
- 而使用球谐函数的话,可以将Cubemap减少到只有9个SH系数(如果是三阶的话)。
下面给出笔者实现的SH重建的Irradiancemap。
原图:
预积分的IrradianceMap:
SH恢复的IrradianceMap:
笔者在实践中使用的DirectX,期间遇到一个读取数据的问题,也记录一下。
笔者通过DirectXTex
库的CaptureTexture
函数获取了CubeMap在CPU中的数据存储于DirectX::ScratchImage
类型中。
DirectX::ScratchImage image;
FCommandQueue& Queue = g_CommandListManager.GetQueue(D3D12_COMMAND_LIST_TYPE_DIRECT);
hr = DirectX::CaptureTexture(Queue.GetD3D12CommandQueue(), m_Resource.Get(), true/*isCubeMap*/, image, m_CurrentState, m_CurrentState);
但由于格式为DXGI_FORMAT_R16G16B16A16_FLOAT
,以下是笔者尝试的代码,并不能正确地读取到图像数据。
const Image img = dstSImg.GetImages()[i];
{size_t rowPitch = 0;size_t slicePitch = 0;ComputePitch(img.format, img.width, img.height, rowPitch, slicePitch);for (int rowIndex = 0; rowIndex < img.height; rowIndex++){uint16* dst = (uint16*)(img.pixels + rowPitch * rowIndex);for (int colIndex = 0; colIndex < img.width; colIndex++){// ... 这样并不能读取到正确的数据uint16 R = dst[4*colIndex+0];}}
}
最终,笔者通过将DXGI_FORMAT_R16G16B16A16_FLOAT
转为DXGI_FORMAT_R32G32B32A32_FLOAT
格式,才可以顺利读取到正确的像素值。代码如下:
/*
* InputImage 输入的R16G16B16A16_FLOAT格式Cubemap
* Width、Height 图像分辨率
* SampleNum 随机采样的总数量
* OutSamples 输出的所有样本点(法线方向、颜色)
*/
class Vertex
{
public:Vector3f pos, color;
};
void RandomSample(const DirectX::ScratchImage& InputImage, int Width, int Height,int SampleNum, std::vector<Vertex>& OutSamples)
{// 通过 DirectX::_ConvertFromR16G16B16A16 将其转成DXGI_FORMAT_R32G32B32A32_FLOAT格式DirectX::ScratchImage dstSImg;dstSImg.Initialize2D(DXGI_FORMAT_R32G32B32A32_FLOAT, InputImage.GetMetadata().width, InputImage.GetMetadata().height, InputImage.GetMetadata().arraySize, InputImage.GetMetadata().mipLevels);for (int i = 0; i < InputImage.GetImageCount(); i++){DirectX::_ConvertFromR16G16B16A16(InputImage.GetImages()[i], dstSImg.GetImages()[i]);}OutSamples.clear();OutSamples.resize(SampleNum);for (int i = 0; i < SampleNum; ++i){float x, y, z;do {x = NormalRandom();y = NormalRandom();z = NormalRandom();} while (x == 0 && y == 0 && z == 0);Vertex vex;Vector3f pos(x, y, z);vex.pos = pos.Normalize();CubeUV cubeuv = XYZ2CubeUV(pos);int colIndex = (int)(cubeuv.u * (Width - 1));int rowIndex = (int)((1.f - cubeuv.v) * (Height - 1));const DirectX::Image* images = dstSImg.GetImages();int Lod0FaceIndex = cubeuv.index * dstSImg.GetMetadata().mipLevels;const DirectX::Image image = images[Lod0FaceIndex];size_t rowPitch = 0;size_t slicePitch = 0;ComputePitch(image.format, image.width, image.height, rowPitch, slicePitch);// 注意强转为float类型float* dst = (float*)(image.pixels + rowPitch * rowIndex);float R = dst[colIndex * 4 + 0];float G = dst[colIndex * 4 + 1];float B = dst[colIndex * 4 + 2];//printf("[%f,%f,%f]\n", R, G, B);vex.color = { R,G,B };OutSamples[i] = vex;}
}
同时需要注意的是对于Cubemap,其Mipmap的存储方式为:
//Face0: Mip0, Mip1, Mip2, ...
//Face1: Mip0, Mip1, Mip2, ...
//...
//Face5: Mip0, Mip1, Mip2, ...
参考博文
SphericalHarmonicsLighting
【论文复现】Spherical Harmonic Lighting:The Gritty Details
【论文复现】An Efficient Representation for Irradiance Environment Maps
球谐光照技术 —— 原理+程序(全网最通俗易懂的版本)
Unity渲染编程(灯光篇)【第一卷:Spherical Harmonics Lighting】
PI or not to PI in game lighting equation
球谐光照-实验篇
球谐光照-应用篇
球谐(SH)和预计算辐射度(PRT)
球谐光照
图形学基础|球谐光照(Spherical Harmonics Lighting)相关推荐
- PRT(Precomputed Radiance Transfer)球谐光照(Spherical Harmonic Lighting)
最近因为开始做PRT(Precomputed Radiance Transfer),看了一些资料.wikipedia上的解释: Precomputed Radiance Transfer ...
- Global Illumination_Spherical Harmonic Lighting(球谐光照)
首先我们需要知道的是,如何计算环境光shading,一般我们会想到IBL,其实我们也可以使用球谐函数来进行表示,本部分我们就先来了解下如何使用SH来计算环境光照(后续我们也会继续来看一下环境光阴影的计 ...
- 球谐光照与PRT学习笔记(一):引入
球谐光照与PRT学习笔记(一):引入 https://zhuanlan.zhihu.com/p/49436452 球谐光照与PRT学习笔记(一):引入 鸡哥 计算机图形学话题下的优秀回答者 球谐光 ...
- Spherical Harmonic Lighting(球谐光照)
1.简介 球谐光照是实时渲染技术中的一种,属于Precompute Radiance Transfer(PRT)的范畴.经过预处理并存储相应的信息之后,它可以产生高质量的渲染及阴影效果.球谐光照需要使 ...
- [高级光照]球谐光照
Spherical Harmonic Lighting(球谐光照) Robin Green 这篇文章只是Spherical Harmonic Lighting这个论文的解释,看的时候请参照原文,原文需 ...
- Unity间接光 ibl(基于图像的渲染)和SH(球谐光照)
转自:https://zhuanlan.zhihu.com/p/68025039 间接光 间接光的实现与ibl(基于图像的渲染)和SH(球谐光照)这两个名词分不开.基于图像的渲染已经是很大的一个体系了 ...
- (八)unity自带的着色器源码剖析之——————Unity3D的全局光照和阴影:下篇(unity3D中的球谐光照和SH球谐函数、unity实时阴影抗锯齿解决方案)
一.探针基于球谐函数的全局光照 球谐光照是基于预计算辐射度传输理论实现的一种实时渲染技术.预计算辐射度传输技术能够重现在区域面光源照射下的全局照明效果.这种技术通过在运行前对场景中光线的相互作用进行预 ...
- Spherical Harmonics Lighting
1.背景知识 1.1 光照表示 之前我们都只考虑光源点和物体表面点的光照作用,而现在,我们考虑物体表面点延伸的微型平面,这个微型平面作为半球形的底部,因此光照射进来的范围就是整个半球形,这也是B ...
- Spherical Harmonics Lighting的代码实现(基于OpenGL)
1.二维空间的勒让德多项式 勒让德多项式定义在[-1,1]范围内,其递归式是 下面这个函数的参数是给定的x,给定的l和m,其中l必须是正整数,而且m在[-l,l]范围内. //勒让德多项式计算方法 d ...
最新文章
- 强烈推荐一款Python可视化神器!
- MySQL数据库之索引的应用
- F​P​G​A​工​作​原​理
- openwrt系统安装到云服务器异常,OpenWrt路由器系统下服务OpenClash 安装教程及其折腾踩坑记录...
- 查找窗口隐藏了怎么办_如何快速查找网站管理页面
- JVM-并发-Java 内存模型
- 在JavaScript中以日期/月/年格式获取当前日期
- 拳王虚拟项目公社:解除网站禁止复制的插件,Simple Allow Copy V 0.8.2
- .net知识和学习方法系列(二十五) .net中的windows service与服务操作
- 数据库与表的操作之创建表(CREATE TABLE)
- js为lable和div赋值
- 项目管理(二)责任划分
- 硬件厂商 Linux社区 代码,Linux企业版需加强的10个方面
- 视差图Disparity与深度图Depth Map的一点知识
- 【JavaScript】封装对象与强制类型转换
- linux远程拷贝东西
- php goeasy,PHP使用GOEASY实现WEB实时推送
- Rust高并发编程总结
- android常用快捷键大全,AndroidStudio 快捷键使用总结大全
- c语言瑞年条件,C语言如何判断是闰年,闰年判断条件