Sombras dinámicas en el juego (activado)

Las sombras son muy importantes para mejorar el realismo del juego. Un breve resumen de la implementación de sombras en el juego.

Veamos primero los componentes de la sombra. Podemos dividir la sombra en dos partes: Umbra y Penumbra. El área de penumbra es el área de transición de la sombra, es decir, la sombra suave.Cuando hay una transición de sombra de penumbra, el efecto visual será mucho mejor.

componentes de la sombra

Para escenas estáticas, podemos elegir hornear sombras en Lightmap o dibujar directamente en la textura. En este artículo, presentamos principalmente las tecnologías relacionadas con las sombras dinámicas, porque las sombras son una tecnología importante en el renderizado en tiempo real y hay muchas formas de realizarlas. En este artículo, intente cubrir varias técnicas de representación de sombras comúnmente utilizadas.

1. Sombras falsas simples pintadas a mano.

Este método se ve a menudo en juegos móviles o juegos 2D. Para personajes dinámicos, la sombra se convierte en un mapa y luego se pega en el suelo debajo de los pies. Aunque es una forma muy simple, también puede mejorar en gran medida el sentido de la realidad. . .

sombra sencilla

2. Sombras proyectadas por aviones

1. Cálculo de las sombras planas proyectadas
Las sombras planas proyectadas son para renderizar los objetos que necesitan proyectar sombras nuevamente y proyectarlos sobre el suelo para generar sombras. Según la posición del plano, podemos calcular una matriz de proyección para transformar directamente las coordenadas del objeto en el plano.

Veamos primero el caso simple, como se muestra en el lado izquierdo de la figura a continuación, donde la sombra se proyecta en el eje X. Bajo la iluminación de la fuente de luz l, necesitamos proyectar una sombra desde el punto v hasta el punto P. Según el principio de los triángulos semejantes, podemos simplemente obtener:

En consecuencia, también podemos calcular las coordenadas en el eje z como: pz = (lyvz-lzvy)/(ly-vy), y organizar los resultados en una matriz de proyección como:

De esta forma, las coordenadas de proyección se pueden calcular por matriz: p=Mv.

Ahora, veamos la situación más general en el lado derecho de la figura de arriba. En este caso, también podemos derivar la ecuación de transformación de coordenadas para proyectar sombras con base en el principio de similitud de triángulos:

Mapa del punto v al punto p:

Sea p=Mv deducida y escrita en forma matricial:

Si se trata de una fuente de luz paralela, el método de cálculo es aproximadamente el mismo y no hay ninguna dificultad especial.

在进行渲染时,我们可以选择先来渲染阴影,将投射阴影的物体,经过上述矩阵的变换到平面上,然后得到没有光照的黑色地面,此时同时把深度写入。然后再正常渲染地面和投射阴影的物体,为了使地面和阴影之间不会冲突,此时可以为深度值添加一些偏移。

添加偏移的方式可以直接通过图形API来添加,比如OpenGL中的glPolygonOffset和DirectX中的DepthBias设置。当然,你也可以选择在绘制阴影时添加偏移,绘制地面时正常绘制,最终的结果都是相同的。后面我们讲到的各种阴影技术,经常会用到添加偏移(Bias)的技术。

另外一种安全的做法是,先正常渲染地面,然后渲染地面上的阴影,渲染阴影时将深度测试关闭,就不会产生深度冲突的问题。最后再渲染投射阴影的物体,这样可以防止阴影投射到非地面的区域。

如果接受阴影的地面不是一个无穷大的平面,则可能需要通过Stencil Buffer标记出需要接受阴影的部分,这样可以只让阴影产生在需要产生的平面上。

另外一个需要注意的,是如下图所示的情况,在进行计算时,需要保证投射阴影的物体位于光源和接受阴影的地面之间,否则就会出现错误的阴影效果。

右边的情形下不应该绘制出阴影

总的来说,这种直接投射阴影的方式,简单直接,适合直接投射在平面上的阴影。目前在手机游戏中,仍然有广泛的应用。

这种直接投射的阴影无法实现软阴影效果。而且由于我们是先渲染出的地面,再将影子的颜色乘以地面的颜色,这样其实并不是完全符合阴影产生的原理。

我们知道,阴影是由于地面没有受到光照而产生的,如果直接将地面的颜色乘以阴影,可能会产生不正确的阴影效果,特别是地面上有高光效果时。这类阴影叫做调制阴影(Modulated shadow),相对普通的阴影,开销要小一些。

游戏中的平面投射阴影

2. 借助Texture的投射阴影
上面我们说到的投射阴影,是直接渲染到被投射的平面上,这样我们就无法实现软阴影的效果,因此我们这里将阴影先保存在一张贴图中,再从贴图中投射到平面上。这样还可以先得到阴影图,再渲染地面,得到正确的阴影效果。

和前面的直接投射相比,这种方式因为中间经过了一层转变,如果保存阴影的贴图分辨率很低,就可能会造成投射出来的结果有锯齿感。

这样,我们就可以将贴图中的阴影先进行边缘模糊,再进行投射,就可以非常方便地得到软阴影效果。

投射阴影实现的软阴影,先将阴影投射到贴图中,然后进行模糊,再投射至平面,实现软阴影效果

为了提升运行效率,我们还可以将多个物体的Texture打包到一个Shadow Atlas中,这样每个物体的投射阴影,占用整个大贴图的一部分。如果光源和投射阴影的物体都没有改变,我们甚至可以不用更新阴影,实现帧间阴影的复用。

三、Shadow Volume阴影

Shadow Volume以前是一种非常流行的阴影实现方案,目前在游戏中也有一定的应用,特别是后面我们将要讲到的PerObject阴影,因此了解其原理是非常重要的。Shadow Volume需要依赖Stencil Buffer来进行实现。

1. Shadow Volume
Shadow Volume就是从光源沿着模型边缘拉伸至无限远处加上前盖后盖形成的形状。可以说,位于Shadow Volume内部的物体,在渲染时具有阴影,在Shadow Volume外部的物体,在渲染时没有阴影。

shadow volume

2. ZPass算法
Shadow Volume阴影的原理就是取一条从视点到目标点的线,每次进入Shadow Volume,Stencil模板计数加一,每次离开计数减一,这样计数为0的部分就是无阴影的地方,计数不为0的地方就是有阴影的地方。

Shadow Volume的实现需要两个Pass,第一个Pass是标记具有阴影的区域,第二个Pass是进行阴影渲染。

第一个Pass,从视点渲染Shadow Volume几何体,屏幕中被Shadow Volume覆盖的区域,就是所有可能产生阴影的位置。我们这里使用Stencil Buffer来标记出实际具有阴影的位置:开启Z-Test,设置Stencil模式为正面部分+1,背面部分-1。这样渲染完成后,Stencil Buffer为0的部分就是无阴影的地方,Stencil Buffer中不为0的部分就是有阴影的地方。

ZPass的原理

第二个Pass,同样也是渲染Shadow Volume的几何体,不过此时直接关闭深度测试,使用模板测试,直接在上一步中标记出的位置渲染出阴影。

3. Z-Fail算法
ZPass算法有个缺陷,当摄影机在Shadow Volume中的时候,就会产生错误的结果。

当摄影机位于Shadow Volume中时,ZPass标记阴影区域失效

所以就有了Z-Fail的算法,Z-Fail算法和ZPass算法类似,只是改成从物体背面计数,在Z-Test fail的几何体部分,在进入Shdow Volume时计数-1,离开时计数+1,这样就可以规避这个缺陷。

使用Z-Fail算法,标记处正确的阴影位置

不过一般来说Z-Fail算法普遍要比ZPass算法慢,因为从背面渲染Shadow Volume,通常会覆盖更多的像素点。

因此在实践中,我们可以先做一个摄影机是否位于Shadow Volume中的判断,来决定使用ZPass或者是Z-Fail算法来进行标记阴影区域。

4. 生成阴影体的步骤
有一种最常见的生成Shadow Volume的方法,不过这种方法要求目标模型是封闭的多边形网格(没有空洞、裂隙、自相交)。

分为三部分:front capping 前盖-> back capping 后盖-> silhouette 轮廓拉伸成的侧面

front capping就是取模型中面向光源的三角面,方向判断可以通过判断面法线和光源方向的乘积的正负值来判断。

back capping就是取模型中背向光源的面,沿光源方向拉伸到无穷远处。

silhouette是判断两个临接面与光源方向不同的边,若认为是轮廓边,则将每条边扩展拉伸到无穷远处形成一个四边形面。

5. 在无穷远出的渲染
如何表示无穷远处的点?使用齐次坐标将w分量置为0,xyz表示方向即可。

如何避免图元在摄影机far clip plane外被裁剪掉?

一种方法是使用GL_DEPTH_CLAMP_NV扩展,将far plane外的点clamp到裁剪空间中。不过这个方法好像是只适用于OpenGL和NVIDIA显卡。

另外一种方法是稍微修改下摄影机的裁剪矩阵,将far plane设置为无穷远。

普通摄影机矩阵

变成下面这样:

远裁面在无穷远处的摄影机矩阵

当然精度或有微乎其微的减少。

6. 适用于非封闭模型的方法
把模型分成两部分,一部分是面向光源的面,一部分是背向光源的面,分别进行拉伸生成Shadow Volume,就可以支持非封闭模型。缺点是原来的轮廓边相当于生成了两次,造成性能浪费。

左边是面向光源面,右边是背向光源面,两个加在一起形成正确的结果

7. 使用Geometry Shader生成Shadow Volume
使用GS可以将生成Shadow Volume的工作移交给GPU,不过必须用TRIANGLE_STRIP的方式来输入模型。

使用GL_TRINGLES_ADJACENCY_EXT模式来向GS中输入三角形图元,就可以获取三角形的邻接面,以此在GS中进行轮廓边判断、输出Shdow Volume等操作。

Geometry Shader中输入的顶点

四、Shadowmap-当前最主流的方式

1. Shadowmap的原理
是当下应用最广泛最常见的方法,Shadowmap的使用,需要两个步骤。

假设我们现在要渲染带阴影的场景如下:

步骤1:从光源处出发,向光照的方向看去,来构造出光照空间。然后在光照空间,我们渲染需要产生阴影的物体,此时将深度写入到Z-Buffer中,得到保存最近处物体的深度值的Shdowmap。

步骤2:然后我们再次正常渲染物体,在渲染时,我们根据渲染物体的世界坐标,变换到上一阶段的光照空间坐标,再计算出该点在Shadowmap中的深度值并进行比较,如果相对光源的距离比Shadowmap中的深度要大,就说明该点处在阴影中,否则就说明不在阴影中。

下图显示了整个Lightmap工作的流程:

Shadowmap计算阴影的大致过程

对于锥形光源,我们只需要沿着光照方向生成Shadowmap。对于类似太阳光的平行光源,我们就需要使用正交投影来进行计算深度,而且投影体的空间范围,需要包含我们的视锥空间。如果是点光源,就会更加复杂一点,为了能保存各个方向的深度值,我们一般需要使用Cubemap 。如果将一个物体进行六次渲染,每次渲染深度到每个面,那么渲染深度的开销就会比较大,因此我们一般会使用RenderTargetArray配合Gemotry Shader,一次性将一个物体的深度,同时写入到六个面上。

2. Light Space Frustrum的计算
Shadowmap的效果,一般会非常依赖于Shadowmap分辨率的大小和Z-Buffer的精度。因此我们要尽量提高Shadowmap的精度。

如果直接使用整个场景的AABB转化到Light Space,肯定是不行的,这样会造成很多不需要的阴影投射计算:

过大的Light Space边界

通常我们会使用下面的方式来计算Light Space Furstrum的边界大小。将世界空间视锥的八个顶点,变换到光照空间,算出在光照空间下,最远和最近的z值,并计算出AABB边界:

不过,这样也可能会造成另外一个问题,就是当摄影机的View Frustrum很小时,造成计算出来的Light Space Frustrum非常小,无法正确地投射所有需要投射阴影的物体。

因此我们还会根据整个场景的AABB空间,对得到的Light Space Frustrum进行扩展,使其能否覆盖到可能产生阴影的物体。当然,为了防止Light Space Frustrum的Near Plane 和Far Plane的值相差过大,我们还会在光照中设置一个最大阴影距离,当阴影投射物体,超出这个最大距离后,就不再投射阴影,来提高阴影的精度。

正确的计算方式

3. Shadow Bias处理自阴影走样
如下图所示,在进行阴影计算时出现了Self-shadow Aliasing/Shadow Acne,在计算自身的阴影时,因为在Shadowmap中存储的深度值,和物体自身的深度是相同的。因为在写入 Shadowmap时,我们计算的是Shadowmap像素中心点的深度值,这样在进行深度采样时,由于Shadowmap的精度限制,就会使比较的深度值产生误差,造成错误的渲染效果。

自阴影走样,右边是加了Bias的效果

一种常见的解决自阴影误差的方式,是使用Bias Factor,对采样时的深度值,沿着光照的方向进行偏移。偏移的值可以是一个常量,这样计算起来比较方便,但是可能会在斜平面上继续产生误差,使用常量时叫做Constant Bias

下图左边展示了Shadow Acne出现的原因,黑色的竖线代表Shadowmap中像素点的位置。左边是未添加Bias的情况,当我们在彩色的位置点进行比较深度时,其实采样到的深度是旁边的竖线处x标记位置的深度,可以看出,绿色点的深度测试是正确的,蓝色和橙色的深度测试是错误。下图中间是使用了Bias的情况,将深度值沿着光照方向进行偏移固定的距离。这样绿色和橙色的点形成了正确的深度值,但是由于偏移的值比较小,蓝色的点的阴影计算,仍然是错误的。

左:出现Shadow Acne的原因;

中:使用Constant Bias;

右:使用Slope Scale Bias

我们发现,在斜面角度较大时,一个固定的偏移值就不再适用了,因此一个常见的改进,就是根据斜面角度来改变偏移值,叫做Slope Scaled Depth Bias / Slope Bias。如上图右边所示,可以看出所有的点的阴影计算结果都是正确的。

设平面法线和光照方向的夹角为θ,视锥大小为frustrumSize,Shadowmap的大小为
shadowmapSize,考虑到我们需要半像素的偏移,这样我们可以计算出需要的Slop Bias的偏移值为:

不过我们可以注意到,这个偏移值是和tan(θ)成正比的,这样的话,当θ趋近于90度时,偏移值是趋近于无穷大的,因此我们需要为偏移值设置一个最大值。

在实际游戏引擎实践中,我们常常需要结合两种Bias来使用,这样来达到较好的效果。

这两种Bias都可以通过图形API硬件来实现。例如在DX11中,我们可以在OutputMerge阶段中,通过参数指定两种Bias的值[1]DepthBiasSlopeScaledDepthBias,这样总的Bias计算方式为:

Bias = (float)DepthBias * r + SlopeScaledDepthBias * MaxDepthSlope;
复制代码

我们还可以设置DepthBiasClamp的值,防止计算出的Bias值过大:

Bias = min(DepthBiasClamp, Bias)
复制代码

另外一种常用的替代Slope Scaled Depth Bias的方案是Normal Offset Bias,将阴影的计算位置沿着物体表面的法线偏移,通过计算我们可以算出需要偏移的距离为:

相对于Slope Scaled Depth Bias,这种方式的一个优点是不用担心θ趋近于90度时,整个偏移值趋近于无穷大。

UE4中,使用的Constant Bias + Slope Scaled Depth Bias

Unity中,使用的是Constant Bias + Normal Offset Bias

当然,我们的Bias值也不能设置得过大,否则会出现漏光等问题,也叫做Peter Panning

Bias值太大导致的Peter Panning

为了保证这种Bias的方式能正确地解决深度冲突。我们应尽量保证物体几何模型是正确的,保证正反面朝向是对的,尽量保证模型封闭,且避免使用太薄的物体模型。

添加Bias可以是在生成Shadowmap阶段完成,也可以在阴影计算阶段,也就是生成Shadowmap时。在Vertex Shader中通过反向添加Bias的方式来偏移计算处的Shadowmap深度值,这样可以节省一些运行开销,且可以简化阴影的计算,这样在采样阴影时,就无需考虑计算偏移的问题。

大部分情况下二者得到的效果是基本接近的,不过在Shadowmap生成阶段添加偏移这种方式也有一些瑕疵:

  1. 不够灵活,所有点的偏移值完全相同,意味着无法根据情况灵活调整Bias值,比如在PCF采样软阴影时,只能提前给出比较大的Bias值,而无法根据PCF Radius的大小灵活调整;
  2. 和Normal Offset Bias,在光照角度比较小的时候,会导致渲染结果错误[2],Unity中的阴影就有这样的缺陷。

在光照角度较小时,Unity URP的错误阴影效果

还有一种比较少见的解决自阴影的方式,是将物体背面的深度写入到Shadowmap,进行深度测试时,就不会出现深度冲突。但是这种方式有很大限制,要求使用的模型必须是正确封闭的,且正反面没有错误。而且如果物体模型很薄,导致前面和背面深度几乎相等,这种方式仍然会失效。因此这种方式不太通用,现在已经很少能见到。

4. 移动平台的Pack
某些旧的移动平台不支持浮点数纹理,这时需要我们将Shadowmap的深度值Pack到RGBA贴图中,Pack和UnPack的公式如下:

//Pack:
vec4 comp = fract(depth * vec4(255.0 * 255.0 * 255.0, 255.0 * 255.0, 255.0, 1.0));
comp -= comp.xxyz * vec4(0.0, 1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0);
//UnPack:
float depth = dot(texture((m_tex), (m_uv)), vec4(1.0 / (255.0 * 255.0 * 255.0), 1.0 / (255.0 * 255.0), 1.0 / 255.0, 1.0))
复制代码

这里我们使用的是255作为模来使用,网上也能搜索到使用256作为模的版本。

但是测试结果表明,使用256时精度是不如255的[3],而且还会遇到不同硬件表现不一致的问题,因此强烈建议使用255 作为参数。

五、Shaowmap精度提升

由于Shdowmap的精度限制,我们在渲染中会遇到各种各样的渲染问题。

一种叫做Perspective Aliasing,由于Shadowmap是在Light Sapce中进行计算的,所以在View Frustrum近处观察时,每个像素对应Shaodowmap中Texel的比例就会降低,产生锯齿。

Perspective Aliasing在近处比较明显

另外这一种叫做Projective Aliasing,是在斜面上进行渲染时,Shadowmap精度不足产生的,本质上来说和Perspective Aliasing是相同的。

Projective Aliasing

通常,提升Shadowmap的分辨率可以改善上面两种渲染问题。但是处于性能考虑,我们不会把Shadowmap的分辨率设置的太大,而是使用一些手段,来提高渲染结果的精度。

1. 使用Perspective Warping
这类方法,通过修改光照空间的投影矩阵,来为视锥近处的物体阴影,提供更高的精度。

常见的有这样几种方式,Perspective Shadow Maps(PSM),Light Space Perspective Shadow Maps(LiSPSM)和Trapezoidal Shadow Maps (TSM)。这些修改投影矩阵的方式原理上大致都是相通的,如下图所示,显示了这类方式的原理:

改变计算Shadwomap时的投影方向

就可以为近处提供更高的精度

这类方式虽然使用起来简单,但是有很多无法处理的特殊情况,比如观察方向和光照方向完全相同时,这类方式就完全无法发挥作用。而且在摄影机移动时,这种方式非常的不稳定。

这类方式目前已经被彻底淘汰,这里也就不再深入讲解相关的原理和实现。

2. Cascaded Shadow Maps(CSM)
CSM是目前最常见的提高Shadowmap精度的手段,候也叫做Parallel-Split Shadow Maps。

通常在渲染视角附近的物体时需要更高的Shadowmap精度,而直接生成的Shadowmap往往不符合这个条件,所以将Frustum分割成数个部分,每个部分单独生成一张Shadowmap,最后组合成一张Atlas。

CSM

从理论上来说,使用指数分布的CSM划分方案是最佳的,即满足

f、n是相机的far、near值,n是指数系数。

比如我们取n=3,f=1000。 这样我们划分出来的三级CSM就是:1-10,10-100, 100-1000。

但是如果我们这样来划分,最近处1-10这个范围的一个CSM划分,物体太少,反而会导致Shadowmap空间的浪费。因此在实践中,常常会结合指数划分和其他划分手段来使用,或者直接由用户手动设置相应的比例值。

Unity中的CSM,不同的颜色代表不同的CSM区域

3. Stablize CSM [4]
在使用Shadowmap时,在移动摄影机时,我们经常会遇到阴影闪烁的问题。因为当摄影机移动后,摄影机的View Frustrum会发生改变,同时Light Space的Frustrum会相应改变,就会造成两帧直接的阴影位置不一样,产生闪烁,在没有使用PCF过滤阴影时,会尤其明显。下图显示了这种闪烁的示例,可以看出视角的微小变化,导致阴影产生了剧烈的闪烁:

视频链接

通常我们会使用Stabilize Cascades来解决这个问题,Stabilize Cascades将相机的移动分成两个部分来处理,分别是相机的旋转和平移。无论相机是如何运动的,都可以分解成沿着视锥中心的旋转和平移。

首先来看绕视锥中心的旋转,当视锥旋转时,因为视锥边界的改变,就会导致计算出来阴影的Light Space Frustrum改变,产生不稳定的结果。要解决这个问题,我们将视锥 Frustrum计算出一个球形的Bounding Volume出来,并用这个球形的Bounding Volume 来算出阴影的Light Space Frustrum,这样当我们的视锥沿着球体中心旋转时,得到的球形Bounding Volume是不变的,算出来的阴影的Light Space Frustrum自然也不会变化。

ab展示的传统的Light Space Frustrum计算过程

cd使用球形BV时的计算过程,在摄影机转动时也是稳定的

从Frustrum生成Bounding Box Sphere,可以使用简单方法求出中心点,算最大半径的方式。也可以使用能得到更加紧凑边界的标准算法[5]

接下来就是处理摄影机平移的部分了,这一步的处理,就是通过偏移投影矩阵,来保证两帧之间,世界空间中的同一点,能投影到Shaodwmap中的相同相对像素位置上。为了计算方便,我们常常取世界空间中的零点,作为参考点,将世界空间的零点,变换到Shadowmap坐标中,并通过偏移,确保得到的Shadowmap坐标是对齐于某个像素的。对齐过程实现的大致代码如下:

            // Create the rounding matrix, by projecting the world-space origin and determining
            // the fractional offset in texel space
            XMMATRIX shadowMatrix = shadowCamera.ViewProjectionMatrix().ToSIMD();
// 使用零点作为参考点
            XMVECTOR shadowOrigin = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
// 将参考点变换到 shadowmap的坐标
            shadowOrigin = XMVector4Transform(shadowOrigin, shadowMatrix);
            shadowOrigin = XMVectorScale(shadowOrigin, sMapSize / 2.0f);
// 在shadowmap坐标系中,将坐标对齐到整数坐标线上
            XMVECTOR roundedOrigin = XMVectorRound(shadowOrigin);
            XMVECTOR roundOffset = XMVectorSubtract(roundedOrigin, shadowOrigin);
            roundOffset = XMVectorScale(roundOffset, 2.0f / sMapSize);
            roundOffset = XMVectorSetZ(roundOffset, 0.0f);
            roundOffset = XMVectorSetW(roundOffset, 0.0f);
//应用偏移,得到新的 projection 矩阵
            XMMATRIX shadowProj = shadowCamera.ProjectionMatrix().ToSIMD();
            shadowProj.r[3] = XMVectorAdd(shadowProj.r[3], roundOffset);
            shadowCamera.SetProjection(shadowProj);
复制代码

在大部分游戏引擎中,Stablize CSM都是默认打开的。不过需要注意的一点是,打开Stablize CSM时,因为阴影的有效范围减少了,所以是会导致阴影精度降低的。在可以保证阴影效果足够软而不会产生闪烁的时候,也可以选择关闭这个功能,来提升阴影的精度。

4. CSM Caching
在使用CSM时,我们常常会遇到CSM开销较大的问题,比如现在使用四级CSM级联,就意味着在生成Shaodwmap时,很多物体需要重复绘制四次。因此有的时候我们会对CSM进行一些优化。

一种方式是降低远处CSM的更新频率。比如在原神的PC版中,共有八级的CSM,前四级是每帧都更新的,后四级是逐帧依次更新的,这样相当于每帧需要更新五级的CSM。

另外一种方式是将CSM中算出的阴影动态缓存,对于静态物体的Shadowmap,是可以实现前后两帧之间的复用的。上一帧中静态物体的Shadowmap,经过一些小小的处理,在当前帧仍然是可用的,对于一些没有覆盖的区域,可以动态来检测,重新绘制生成:

CSM Caching

参考:
[1] learn.microsoft.com/en-us/windo…
[2] zhuanlan.zhihu.com/p/370951892
[3] aras-p.info/blog/2009/0…
[4] ShaderX6 Stable Cascaded Shadow Maps
[5] zhuanlan.zhihu.com/p/136752363

更多内容,请关注:
游戏中的动态阴影(下)


这是侑虎科技第1380篇文章,感谢作者张亚坤供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)

作者主页:www.zhihu.com/people/tc13…

【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!

Supongo que te gusta

Origin juejin.im/post/7229155681223868477
Recomendado
Clasificación