前言

上一章我们介绍了级联阴影贴图。刚开始的时候我尝试了给CSM直接加上PCSS,但不管怎么调难以达到说得过去的效果。然后文章越翻越觉得阴影就是一个巨大的坑,考虑到时间关系,本章只实现了方差阴影贴图(VSM)和指数阴影贴图(ESM)作为引子,然后将相关扩展放在文末。

现在假定读者已经读过下面的内容:

章节
38 级联阴影贴图

DirectX11 With Windows SDK完整目录

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

Variance Shadow Map

关于Shadow Mapping,我们可以将比较深度的过程用这样一个函数表示:\(H(d_o-d_r)\)。其中\(d_r\)是receiver的深度,\(d_o\)是occluder的深度。很明显,当\(d_o-d_r<0\)时,\(H(d_o-d_r)=0\);\(d_o-d_r\geq0\)时,\(H(d_o-d_r)=1\)。

将该函数拆分成occluder项和receiver项,有利于我们对occluder项使用图片空间的模糊或者硬件mipmap进行pre-filter处理以用于软阴影。并且由于我们将要改变阴影测试的方法,就不再需要为了缓解shadow acne(阴影粉刺)而使用Depth bias。

受到Deep Shadow Maps的启发,可以使用概率表示的方式。给定当前receiver的深度值,occluder的深度值现在表示为一个随机变量:

\[f(d_r)=P(d_o\geq d_r)
\]

上式变成了一个概率分布函数,判断当前像素位于阴影中的概率。

假设occluder近似满足单峰分布,那么它可以由均值和方差表示。这两者可通过一阶(moment)和二阶矩所派生:

\[\mu=E(d_o)\\
\sigma^2=E(d_o^2)-E(d_o)^2
\]

其中一阶矩和二阶矩由下面的公式计算:

\[E(x)=\int xp(x)dx\\
E(x^2)=\int x^2p(x)dx
\]

本质上就是对shadow map做一个滤波(如盒型滤波或高斯滤波等):

\[E(d_o)\approx\sum w_i d_i\\
E(d_o^2)\approx\sum w_i d_i^2
\]

在算出均值和方差后,紧接着我们就可以根据切比雪夫不等式来找出\(P(d_o\geq d_r)\)的上界:

\[P(d_o\geq d_r)\leq p_{max}(d_r)\equiv \frac{\sigma^2}{\sigma^2+(\mu-d_r)^2}
\]

当\(\sigma^2=0, \mu=d_r\)时,上式未定义,为此可以在分子分母同时加上一个极小量\(\epsilon\),或者是\(\sigma^2<\epsilon\)时直接让\(\sigma^2:=\epsilon\)。此时没有遮蔽的话值为1; 产生遮蔽的话值接近0。

看上图,黑点所属的区域完全被Occluder遮蔽,因此\(\sigma^2=0, \mu < d_r, p_{max}(d_r)\approx 0\)

红点所属的区域部分被Occluder遮蔽,有\(\sigma^2>0, \mu < d_r\),且红点越往右靠,\(p_{max}(d_r)\)越接近1

蓝点所属的区域没有遮蔽,因此\(\sigma^2=0, \mu = d_r, p_{max}(d_r)=1\)

根据上式我们可以写出如下HLSL代码:

  1. float ChebyshevUpperBound(float2 moments,
  2. float receiverDepth,
  3. float minVariance,
  4. float lightBleedingReduction)
  5. {
  6. float variance = moments.y - (moments.x * moments.x);
  7. variance = max(variance, minVariance); // 防止0除
  8. float d = receiverDepth - moments.x;
  9. float p_max = variance / (variance + d * d);
  10. // 单边切比雪夫
  11. return (receiverDepth <= moments.x ? 1.0f : p_max);
  12. }

对VSM滤波

而为了能够获得\(d_o\)和\(d_o^2\),显然我们不能靠深度图来缓存,而需要额外的R32G32_FLOAT纹理来记录。如果只是单纯为了记录\(d_o\)和\(d_o^2\),可以在绘制深度图的同时将\(d_o\)和\(d_o^2\)写入到RTV上。

而由于我们最终要使用的是\(E(d_o)\)和\(E(d_o^2)\),我们可以对其进行一个pre-filter的处理,具体包括:

  • 使用MSAA记录更多深度
  • 使用盒型滤波或高斯滤波处理方差阴影贴图
  • 使用mipmap

而采样的时候我们可以对方差阴影贴图使用各种方式,比如点采样、线性采样、各向异性采样。

下面的代码展示的是深度图开启或关闭MSAA时,可以在全屏绘制阶段进行一个Resolve来进行一个pre-filter的处理:

  1. // Shadow.hlsl
  2. Texture2DMS<float, MSAA_SAMPLES> g_ShadowMap : register(t0); // 用于VSM生成
  3. float2 VarianceShadowPS(float4 posH : SV_Position,
  4. float2 texCoord : TEXCOORD) : SV_Target
  5. {
  6. float sampleWeight = 1.0f / float(MSAA_SAMPLES);
  7. uint2 coords = uint2(posH.xy);
  8. float2 avg = float2(0.0f, 0.0f);
  9. [unroll]
  10. for (int i = 0; i < MSAA_SAMPLES; ++i)
  11. {
  12. float depth = g_ShadowMap.Load(coords, i);
  13. avg.x += depth * sampleWeight;
  14. avg.y += depth * depth * sampleWeight;
  15. }
  16. return avg;
  17. }

为了更近一步考虑周围像素的深度,可以使用屏幕空间滤波获得\(E(d_o)\)和\(E(d_o^2)\),使用盒型滤波或高斯滤波都可以:

  1. // Shadow.hlsl
  2. #ifndef BLUR_KERNEL_SIZE
  3. #define BLUR_KERNEL_SIZE 3
  4. #endif
  5. static const int BLUR_KERNEL_BEGIN = BLUR_KERNEL_SIZE / -2;
  6. static const int BLUR_KERNEL_END = BLUR_KERNEL_SIZE / 2 + 1;
  7. static const float FLOAT_BLUR_KERNEL_SIZE = (float)BLUR_KERNEL_SIZE;
  8. Texture2D g_TextureShadow : register(t1); // 用于模糊
  9. SamplerState g_SamplerPointClamp : register(s0);
  10. float2 VSMHorizontialBlurPS(float4 posH : SV_Position,
  11. float2 texcoord : TEXCOORD) : SV_Target
  12. {
  13. float2 depths = 0.0f;
  14. [unroll]
  15. for (int x = BLUR_KERNEL_BEGIN; x < BLUR_KERNEL_END; ++x)
  16. {
  17. depths += g_TextureShadow.Sample(g_SamplerPointClamp, texcoord, int2(x, 0));
  18. }
  19. depths /= FLOAT_BLUR_KERNEL_SIZE;
  20. return depths;
  21. }
  22. float2 VSMVerticalBlurPS(float4 posH : SV_Position,
  23. float2 texcoord : TEXCOORD) : SV_Target
  24. {
  25. float2 depths = 0.0f;
  26. [unroll]
  27. for (int y = BLUR_KERNEL_BEGIN; y < BLUR_KERNEL_END; ++y)
  28. {
  29. depths += g_TextureShadow.Sample(g_SamplerPointClamp, texcoord, int2(0, y));
  30. }
  31. depths /= FLOAT_BLUR_KERNEL_SIZE;
  32. return depths;
  33. }

其中Sample的可选第三个参数offset用来控制采样行为,往x方向和y方向偏移多少个像素单位,其范围只能在[-8, 7],超过这个范围编译就会报错。你也可以不使用offset,改为额外提供宽高信息来求texel的uv offset。

最后在绘制完所有级联的方差阴影贴图后,我们可以选择是否使用GenerateMips

漏光(Light Bleeding)

VSM最大的问题在于漏光现象,见下图(不得不说这漏光是真的严重)。

我们固定\((\mu-d_r)^2\)的值(非0)来观察随着\(\sigma^2\)变化,\(p_{max}(d_r)\)的函数图像:

随着方差的增大,\(p_{max}(d_r)\)逐渐变大,这是造成漏光现象的主要原因。方差较大的情况可以参考下图:

从shadow map的视角来看,中间的区域遮挡物深度值发生了很大的跳变,求得的平均值在两个遮挡物之间,而平均值与遮挡物都距离较远,导致方差很大,从而出现漏光现象。同理,如果遮挡物或者接受物的平面与光路接近平行,也会产生大的方差值,导致漏光现象的出现。因此,在简单的场景下应让光路与尽可能多的平面垂直。但对于复杂的场景来说,仅调整光线方向并不能解决问题,不得不吐槽发电厂这个模型简直就是各路算法的埋葬场。

但如果我们尝试增加更多采样来解决这个问题,那又会牺牲效率,那还不如使用PCF。因为使用VSM等基于概率的阴影算法是相比于传统PCF的效率较高,当然代价是在极端情况下带来的物理不准确性。

减少漏光的近似算法

如果receiver的深度值为\(z\),且它被某个滤波区域完全阻挡,那么有\(d_o-d_r<0, (z-d)^2>0, p_{max}<1\),即该表面永远接受不到满光照的强度

我们可以修改\(p_{max}\)的值,让其在低于某个\(amount\in[0, 1]\)值的时候直接归零,然后将\([amount,1]\)重新映射到\([0,1]\):

  1. float Linstep(float a, float b, float v)
  2. {
  3. return saturate((v - a) / (b - a));
  4. }
  5. // 令[0, amount]的部分归零并将(amount, 1]重新映射到(0, 1]
  6. float ReduceLightBleeding(float pMax, float amount)
  7. {
  8. return Linstep(amount, 1.0f, pMax);
  9. }

当然,我们也可以向VarianceShadows11的例子中,对\(p_{max}\)套上一个幂指数,然后通过这个幂指数来控制漏光。

现在求\(p_{max}\)的方法变成了:

  1. float ChebyshevUpperBound(float2 moments,
  2. float receiverDepth,
  3. float minVariance,
  4. float lightBleedingReduction)
  5. {
  6. float variance = moments.y - (moments.x * moments.x);
  7. variance = max(variance, minVariance); // 防止0除
  8. float d = receiverDepth - moments.x;
  9. float p_max = variance / (variance + d * d);
  10. p_max = ReduceLightBleeding(p_max, lightBleedingReduction);
  11. // 单边切比雪夫
  12. return (receiverDepth <= moments.x ? 1.0f : p_max);
  13. }

使用梯度对级联阴影采样

在使用梯度采样级联阴影时,可能会在两个级联的边界区域出现下图所示的问题。

使用各项异性滤波由于动态流控制导致在级联之间出现的接缝

采样指令使用像素之间的导数来计算mipmap等级,也被各项异性过滤所需。这可能会在各项异性过滤或mipmap选择的时候引发问题。当2x2像素块在像素着色器中使用不同的分支时,GPU硬件计算出的导数是不合理的。这会导致在级联边缘出现跳变。

该问题可以通过计算光照空间下位置的偏导来解决;光照空间的坐标并没有指定所选的级联。计算出的导数可以变换到对应级联所属的投影纹理空间,从而可以求出正确的mipmap等级或被各项异性过滤使用:

  1. float CalculateVarianceShadow(float4 shadowTexCoord,
  2. float4 shadowTexCoordViewSpace,
  3. int currentCascadeIndex)
  4. {
  5. float percentLit = 0.0f;
  6. float2 moments = 0.0f;
  7. // 为了将求导从动态流控制中拉出来,我们计算观察空间坐标的偏导
  8. // 从而得到投影纹理空间坐标的偏导
  9. float3 shadowTexCoordDDX = ddx(shadowTexCoordViewSpace).xyz;
  10. float3 shadowTexCoordDDY = ddy(shadowTexCoordViewSpace).xyz;
  11. shadowTexCoordDDX *= g_CascadeScale[currentCascadeIndex].xyz;
  12. shadowTexCoordDDY *= g_CascadeScale[currentCascadeIndex].xyz;
  13. moments += g_TextureShadow.SampleGrad(g_SamplerShadow,
  14. float3(shadowTexCoord.xy, (float) currentCascadeIndex),
  15. shadowTexCoordDDX.xy, shadowTexCoordDDY.xy).xy;
  16. percentLit = ChebyshevUpperBound(moments, shadowTexCoord.z, 0.00001f, g_LightBleedingReduction);
  17. return percentLit;
  18. }

优缺点总结

VSM具有如下优点:

  • 可以使用图片空间blur或硬件filtering来产生软阴影
  • 不需要处理shadow acne问题,因此也不需要引入depth bias

但它也有如下缺点:

  • 需要原来深度图占用显存空间的两倍来存放\(d_o\)和\(d_o^2\)
  • 在具有高方差分布的区域容易产生漏光(Light Bleeding)
  • 大卷积核滤波会使漏光现象更加严重(因为方差值变大了)

Exponential Shadow Map

指数阴影贴图的核心公式如下:

\[f(z)=saturate(e^{c(d-z)}), d<z, c>0
\]

在固定\(c\)的情况下,随着occluder逐渐远离receiver,\(d-z\)从0向负数变动,对应的函数图像如下:

为此我们可以将上式拆分成\(e^{cd}\)和\(e^{-cz}\)项。深度图负责前面一项,receiver可以得到后一项。

这种表示的好处在于简单,并且和VSM一样,可以对\(e^{cd}\)项进行blur,并且没有shadow acne的问题。而相比于VSM,它只需要存一项就可以用。

上图中的\(c=20\),可以看出,如果\(d\)和\(z\)比较接近的话仍然会出现比较严重的漏光,为此需要让c的值变得更大。下图是\(c=100\)的效果:

但深度图直接保存\(e^{cd}\)的话会面临一个严重的问题:浮点数的表示范围是有限的,到\(e^{88}\)的时候就已经接近浮点表示的上界了,\(c\)值过大则无法表示左边部分的范围。而为了能够产生跟一开始那张函数图接近跳变的效果,需要让c能够表示得更大,否则在\(d-z\)逼近0的时候误差会很大。

提升精度

前面提到如果\(c\)太大,\(e^{cd}\)可能会超过float的表示上界,但\(c(d-z)\)本身远小于\(cd\),不容易越界。在不需要blur的情况下只需要在shadow map生成的时候保存d或者cd即可。

但可以blur也是ESM的优点之一,为此我们需要在blur的部分进行改进。在Lighting Research at Bungie中,提到了一种指数空间滤波的方式。首先对N个样本的加权求和有:

\[\begin{aligned}\sum_{i=0}^N w_i e^{cd_{o_i}}&=
e^{cd_{o_0}}(w_0+\sum_{i=1}^Nw_i e^{c(d_{o_i}-d_{o_0})})\\
&=e^{cd_{o_0}}\cdot e^{ln(w_0+\sum_{i=1}^Nw_i e^{c(d_{o_i}-d_{o_0})})}\\
&=e^{cd_{o_0} + ln(w_0+\sum_{i=1}^Nw_i e^{c(d_{o_i}-d_{o_0})})}
\end{aligned}
\]

即我们只需要在blur的时候求出即可:

\[cd_{o_0} + ln(w_0+\sum_{i=1}^Nw_i e^{c(d_{o_i}-d_{o_0})})
\]

HLSL代码

指数阴影贴图相关的HLSL代码如下:

  1. float ESMLogGaussianBlurPS(float4 posH : SV_Position,
  2. float2 texcoord : TEXCOORD) : SV_Target
  3. {
  4. float cd0 = g_TextureShadow.Sample(g_SamplerPointClamp, texcoord);
  5. float sum = g_BlurWeights[FLOAT_BLUR_KERNEL_SIZE / 2] * g_BlurWeights[FLOAT_BLUR_KERNEL_SIZE / 2];
  6. [unroll]
  7. for (int i = BLUR_KERNEL_BEGIN; i < BLUR_KERNEL_END; ++i)
  8. {
  9. for (int j = BLUR_KERNEL_BEGIN; j < BLUR_KERNEL_END; ++j)
  10. {
  11. float cdk = g_TextureShadow.Sample(g_SamplerPointClamp, texcoord, int2(i, j)) * (float) (i != 0 || j != 0);
  12. sum += g_BlurWeights[i - BLUR_KERNEL_BEGIN] * g_BlurWeights[j - BLUR_KERNEL_BEGIN] * exp(cdk - cd0);
  13. }
  14. }
  15. sum = log(sum) + cd0;
  16. sum = isinf(sum) ? 84.0f : sum; // 防止溢出
  17. return sum;
  18. }
  1. //--------------------------------------------------------------------------------------
  2. // ESM:采样深度图并返回着色百分比
  3. //--------------------------------------------------------------------------------------
  4. float CalculateExponentialShadow(float4 shadowTexCoord,
  5. float4 shadowTexCoordViewSpace,
  6. int currentCascadeIndex)
  7. {
  8. float percentLit = 0.0f;
  9. float occluder = 0.0f;
  10. float3 shadowTexCoordDDX = ddx(shadowTexCoordViewSpace).xyz;
  11. float3 shadowTexCoordDDY = ddy(shadowTexCoordViewSpace).xyz;
  12. shadowTexCoordDDX *= g_CascadeScale[currentCascadeIndex].xyz;
  13. shadowTexCoordDDY *= g_CascadeScale[currentCascadeIndex].xyz;
  14. occluder += g_TextureShadow.SampleGrad(g_SamplerShadow,
  15. float3(shadowTexCoord.xy, (float) currentCascadeIndex),
  16. shadowTexCoordDDX.xy, shadowTexCoordDDY.xy).x;
  17. percentLit = saturate(exp(occluder - g_MagicPower * shadowTexCoord.z));
  18. return percentLit;
  19. }

这样就把receiver和occluder之间深度的矛盾,转移到了occluder与相邻occluder之间深度的矛盾了。但如果相邻occluder之间的深度差很大,依然开不了很大的c。由于级联的Near/Far与发电厂尽可能贴近,在发电厂中可能存在相邻occluder之间的深度差接近0.5,那么此时c开到180就会溢出了。而上面的代码虽然能够防止溢出,却会导致出现下图的锯齿现象(类似于回到没开模糊的情况):



由于深度值已经位于线性空间,那么c值一定会有一个随深度差最大值变化的上界。这时候更多需要依赖于手工调整。

优缺点总结

ESM具有如下优点:

  • 可以使用图片空间blur或硬件filtering来产生软阴影,也不需要开很大的Blur
  • 不需要处理shadow acne问题,因此也不需要引入depth bias
  • 相比VSM只需要用1个float

但它也有如下缺点:

  • 为了提升精度需要用特定的Blur,并且没法缩减成水平和竖直方向的Blur
  • 邻近像素深度变化较大的话c的大小会受限

后记

阴影本身就是一个巨大的坑。实际上搞这两章阴影就已经搞掉我很长时间了,加上中间还要忙各种事情,再往后的阴影效果现在暂时也没有耐心去实现,也许以后还会回来添砖加瓦。总体来说,VSM和ESM这些尝试拟合最开头图像函数的方法都难以避免出现漏光的问题,对于具有复杂深度的场景表现不尽如人意。这些方法可以放在级联等级较大,即距离较远的地方,当然也有人在远距离尝试使用距离场,这些都是遥远的后话了。

建议读者直接打开项目进行尝试,这里只解释部分可调参数的含义:

VSM

  • Shadow MSAA:记录阴影图时开启MSAA,然后生成VSM的时候进行Resolve
  • Light Bleeding:将[0, amount]映射到0,将[amount, 1]映射到[0, 1]
  • Enable Mipmap:级联阴影开启mipmap
  • Sampler:采样VSM使用的滤波

ESM

  • Blur Sigma:Log高斯滤波用于控制权重分散情况
  • Magic Power:控制\(e^{cd}\)和\(e^{cz}\)的c项

GPU Profile那边开Release来查看各个Pass下。至于EVSM和MSM等,可以尝试跑TheRealMJP/Shadows的项目,但需要一些动手修改的能力,它那边可以调的参数更多。

参考与扩展阅读材料

如果有兴趣的话可以了解下面这些内容,当然肯定是有我没注意到的。

Fixed-Size Penumbra

  • PCF(Percentage Closer Filtering)
  • VSM(Variance Shadow Maps, 2006)
  • LVSM(Layered Variance Shadow Maps)
  • ESM(Exponential Shadow Maps, 2008)
  • EVSM(Exponential Variance Shadow Maps)
  • MSM(Movement Shadow Maps, 2015)
  • Virtual Shadow Map(这个估计只能在DX12做)

Variable-Size Penumbra

  • PCSS(Percentage Closer Soft Shadows)
  • VSSM = PCSS + VSM(Variance Soft Shadow Maps)
  • SAVSM = VSM + SAT(Summed Area Table)

Others

  • 距离场阴影
  • Reflective Shadow Maps
  • 光线追踪白给的阴影,但需要显卡支持

Cascade Optimization & Technique

  • Sample Distribution Shadow Map
  • GPU-Driven Cascade Setup and Scene Submission
  • Deferred Shadow

[1]Cascade Shadow Maps--MSDN

[2]Playing with Real-Time Shadows(Siggraph 2013)

[3]Lighting Research at Bungie(Siggraph 2009)

[4]Advanced Soft Shadow Mapping Techniques(GDC 2008)

[5]Variance Shadow Maps(GDC 2006)

[6]A Sampling of Shadow Techniques

[7]论文:Layered Variance Shadow Maps

[8]KlayGE:切换到ESM

[9]Exponential Variance Shadow Maps

[10]知乎:方差阴影(Variance Shadow Map)实现

[11]知乎:Unreal Engine UE4 静态阴影实现 Static ShadowMap ESM,改进ESM(log space 下做模糊)

[12]Percentage-Closer Soft Shadows

[13]Integrating Realistic Soft Shadows Into Your Game Engine

[14]VSSM

[15]Moment Shadow Mapping (momentsingraphics.de)

[16]Sample Distribution Shadow Map(自动级联分层)

参考项目:

VarianceShadows11

TheRealMJP/Shadows

DirectX11 With Windows SDK完整目录

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

DirectX11 With Windows SDK--39 阴影技术(VSM、ESM)的更多相关文章

  1. DirectX11 With Windows SDK--38 级联阴影映射(CSM)

    前言 在31章我们曾经实现过阴影映射,但是受到阴影贴图精度的限制,只能在场景中相当有限的范围内投射阴影.本章我们将以微软提供的例子和博客作为切入点,学习如何解决阴影中出现的Atrifacts: 边缘闪 ...

  2. DirectX11 With Windows SDK--24 Render-To-Texture(RTT)技术的应用

    前言 尽管在上一章的动态天空盒中用到了Render-To-Texture技术,但那是针对纹理立方体的特化实现.考虑到该技术的应用层面非常广,在这里抽出独立的一章专门来讲有关它的通用实现以及各种应用. ...

  3. 粒子系统与雨的效果 (DirectX11 with Windows SDK)

    前言 最近在学粒子系统,看这之前的<<3D图形编程基础 基于DirectX 11 >>是基于Direct SDK的,而DXSDK微软已经很久没有更新过了并且我学的DX11是用W ...

  4. DirectX11 With Windows SDK--31 阴影映射

    前言 阴影既暗示着光源相对于观察者的位置关系,也从侧面传达了场景中各物体之间的相对位置.本章将起底最基础的阴影映射算法,而像复杂如级联阴影映射这样的技术,也是在阴影映射的基础上发展而来的. 学习目标: ...

  5. DirectX11 With Windows SDK--40 抗锯齿:FXAA

    前言 在默认的情况下渲染,会看到物体的边缘会有强烈的锯齿感,究其原因在于采样不足.但是,尝试提升采样的SSAA会增大渲染的负担:而硬件MSAA与延迟渲染又不能协同工作.为此我们可以考虑使用后处理的方式 ...

  6. DirectX11 With Windows SDK--23 立方体映射:动态天空盒的实现

    前言 上一章的静态天空盒已经可以满足绝大部分日常使用了.但对于自带反射/折射属性的物体来说,它需要依赖天空盒进行绘制,但静态天空盒并不会记录周边的物体,更不用说正在其周围运动的物体了.因此我们需要在运 ...

  7. DirectX11 With Windows SDK--29 计算着色器:内存模型、线程同步;实现顺序无关透明度(OIT)

    前言 由于透明混合在不同的绘制顺序下结果会不同,这就要求绘制前要对物体进行排序,然后再从后往前渲染.但即便是仅渲染一个物体(如上一章的水波),也会出现透明绘制顺序不对的情况,普通的绘制是无法避免的.如 ...

  8. DirectX11 With Windows SDK--32 SSAO(屏幕空间环境光遮蔽)

    前言 由于性能的限制,实时光照模型往往会忽略间接光因素(即场景中其他物体所反弹的光线).但在现实生活中,大部分光照其实是间接光.在第7章里面的光照方程里面引入了环境光项: \[C_a = \mathb ...

  9. DirectX11 With Windows SDK--36 延迟渲染基础

    前言 随着图形硬件变得越来越通用和可编程化,采用实时3D图形渲染的应用程序已经开始探索传统渲染管线的替代方案,以避免其缺点.其中一项最流行的技术就是所谓的延迟渲染.这项技术主要是为了支持大量的动态灯光 ...

随机推荐

  1. mysql8.0时区问题

    今天在mysql新增一条数据的时候,发现时间类型的字段比起现在少了8个小时,查了资料才发现,这个是MySQL8.0出现的问题,讲下解决方法. 1.在java项目中application.yml文件中的 ...

  2. Flink调优

      第1章 资源配置调优 Flink性能调优的第一步,就是为任务分配合适的资源,在一定范围内,增加资源的分配与性能的提升是成正比的,实现了最优的资源配置后,在此基础上再考虑进行后面论述的性能调优策略. ...

  3. ros工作空间中文件夹结构

    ROS 编译系统 catkin 详解 ros系统学习之Catkin编译系统 ROS--catkin编译系统.package.xml和CMakeList.txt文件 1.build:编译空间 存放CMa ...

  4. xacro 语法总结

    xacro是为了解决在编写urdf文件过程中,出现的标签内容重复,但又不得不去写.一些值的计算有依赖的问题.为了和urdf文件区分,我们定义的模型文件名后缀为.xacro. 从功能的角度来说,xacr ...

  5. Pandas查询数据的几种方法

    Pandas查询数据 Pandas查询数据的几种方法 df.loc方法,根据行.列的标签值查询 df.iloc方法,根据行.列的数字位置查询 df.where方法 df.query方法 .loc既能查 ...

  6. C++ | 简单工厂模式 | 复数计算器

    简单工厂模式最直观的一个应用便是实现一个计算器的程序. 比如,公司让你给计算器添加一个幂运算的功能,你只需要设计一个幂运算的类,并实现幂运算的逻辑,然后让该类继承自运算类即可. 简单工厂模式: 简单工 ...

  7. 解决 css 浮动后 父元素高度失效问题

    应用场景 子元素标签使用 浮动后,会出现浮在父元素上层,脱离了.导致父元素没办法根据子元素的高度而变化,提供以下解决方案. 解决代码 把 '.clearfix ' Class 样式添加到 父元素即可. ...

  8. Java/C++实现观察者模式--股票价格

    当股票的价格上涨或下降5%时,会通知持有该股票的股民,当股民听到价格上涨的消息时会买股票,当价格下降时会大哭一场. 类图: Java代码: public class Investor implemen ...

  9. Typora 设置图片自动上传

    使用 PicGo-Core(command line) 下载 PicGo-Core 依次点击 文件 -> 偏好设置 -> 图像 来到下图所示界面: 点击①位置选择 PicGo-Gore(c ...

  10. 接口和抽象类的区别(不讲废话,干货满满,JDK1.8最新整理)

    接口和抽象类的区别(不讲废话,干货满满,JDK1.8最新整理) 1.抽象类 以下说辞可能不太准确,但是会让你醍醐灌顶 抽象类是把一些具有共同属性(包括行为)的东西抽象出来,比如: 小狗有身高,体重,颜 ...