当前位置:   article > 正文

Frank Luna DirectX12阅读笔记:绘制进阶(第八章-第十四章)_d3d12_filter_min_point_mag_linear_mip_point

d3d12_filter_min_point_mag_linear_mip_point

目录

第八章 光照

8.1 光和材质的交互

8.2 法向

  • 使用顶点法向取代面法向
  • 当世界坐标矩阵不是单位阵时,注意法向的变换

8.3 光照中其他重要的向量

  • E为眼镜,星号为光源

图片

8.4 Lambert余弦定律

  • radiant flux P(辐射通量):单位时间的光能量
  • irradiance E(辐照度):单位面积单位时间的光能量(density of radiant flux per area)
    • 决定了物体(接受到光)的明暗

图片

  • Lambert余弦定律:

E 2 = P A 2 = P A 1 cos ⁡ θ = E 1 cos ⁡ θ = E 1 ( n ⋅ L ) E_2 = \frac{P}{A_2} = \frac{P}{A_1} \cos \theta = E_1 \cos \theta = E_1 (\mathbf{n} \cdot \mathbf{L}) E2=A2P=A1Pcosθ=E1cosθ=E1(nL)

8.5 散射光(diffuse lighting)

  • 出射散射光的强度和入射光强度B_L、入射光角度L、散射系数m_d相关

c d = max ⁡ ( L ⋅ n , 0 ) ⋅ B L ⊗ m d \mathbf{c}_d = \max(\mathbf{L} \cdot \mathbf{n}, 0) \cdot \mathbf{B}_L \otimes \mathbf{m}_d cd=max(Ln,0)BLmd

8.6 环境光(ambient lighting)

  • 出射环境光的强度和环境光强度A_L、散射系数m_d相关

c a = A L ⊗ m d \mathbf{c}_a = \mathbf{A}_L \otimes \mathbf{m}_d ca=ALmd

8.7 镜面光(specular lighting)

8.7.1 Fresnel效应

  • Fresnel效应:当光线到达两种介质的分界面时,一部分被反射,一部分被折射。记R_F为反射光的比例,则1-R_F为折射光的比例。R_F随入射角的变化而变化,当入射角为90°时,光线平行分界面,R_F为1;当入射角为0°时,光线垂直于分界面,R_F为R_F(0°)。中间,根据Schlick估计,有

R F ( θ i ) = R F ( 0 ) + ( 1 − R F ( 0 ) ) ( 1 − cos ⁡ ( θ i ) ) 5 \mathbf{R}_F(\theta_i) = \mathbf{R}_F(0) + (1 - \mathbf{R}_F(0)) (1 - \cos(\theta_i))^5 RF(θi)=RF(0)+(1RF(0))(1cos(θi))5

  • 常见的R_F(0):
    • 水(0.02,0.02,0.02)
    • 玻璃(0.08,0.08,0.08)
    • 塑料(0.05,0.05,0.05)
    • 金(1.0,0.71,0.29)
    • 银(0.95,0.93,0.88)
    • 水(0.95,0.64,0.54)

图片

  • 对于透明/半透明的物体,则折射光就是折射光;但对于不透明的物体,折射光在物体内部多次反射、吸收,最终成为散射光

8.7.2 粗糙度

  • 微平面的法向和宏观物体法向不同,使得镜面反射光呈现光锥

图片

  • 反射光分布近似余弦函数幂乘的形状,再乘以一个近似的保持能量的归一化项,有

S ( θ h ) = m + 8 8 cos ⁡ m ( θ h ) = m + 8 8 ( n ⋅ h ) m S(\theta_h) = \frac{m+8}{8} \cos^m (\theta_h) = \frac{m+8}{8} (\mathbf{n} \cdot \mathbf{h})^m S(θh)=8m+8cosm(θh)=8m+8(nh)m

  • 出射镜面光强度与入射光方向L、入射光强度B_L、半途向量h、材质Fresnel效应下反射比例R_F、粗糙度m相关

c s = max ⁡ ( L ⋅ n , 0 ) ⋅ B L ⊗ R F ( α h ) m + 8 8 ( n ⋅ h ) m \mathbf{c}_s = \max(\mathbf{L} \cdot \mathbf{n}, 0) \cdot \mathbf{B}_L \otimes R_F(\alpha_h) \frac{m+8}{8} (\mathbf{n} \cdot \mathbf{h})^m cs=max(Ln,0)BLRF(αh)8m+8(nh)m

8.8 光照模型

c = c a + c d + c s \mathbf{c} = \mathbf{c}_a + \mathbf{c}_d + \mathbf{c}_s c=ca+cd+cs

8.9 材质的实现

  • 材质的粒度:即使材质作用在顶点上,如果模型本身比较粗糙,效果也是比较差的;比较好的解决方案是,将材质作用在纹理上
  • RenderItem类中会包含渲染物体的材质,材质类中需要保存各种纹理在SRV heap中的相对位置,从而可以在DrawRenderItems()函数中赋予正确的材质

8.10 平行光源

  • 平行光定义成向量

8.11 点光源

  • 点光源定义成点
  • 点光源强度随距离二次衰减,但如果简化,可以调成一次衰减

8.12 聚光源

  • 聚光源和点光源除了光照范围外,最大的区别是聚光源光强随着远离聚光中心而下降,因此可以如下调节轴向偏移的光强衰减:

max ⁡ ( cos ⁡ ( ϕ ) , 0 ) s \max(\cos(\phi), 0)^s max(cos(ϕ),0)s

  • 使用max而非分支,是因为GPU不擅长处理分支。\phi为顶点光源连线和聚光轴的夹角,s可以调节聚光的程度
  • 聚光源比点光源运算代价高,点光源比平行光源运算代价高

8.13 光照的实现

  • Blinn-Phong之前光强的计算:
    • 平行光:需考虑Lambert余弦定律
    • 点光源:需考虑Lambert余弦定律+距离衰减
    • 聚光源:需考虑Lambert余弦定律+距离衰减+聚光衰减
  • 光的数据结构:这里的顺序不是随机的,而是按照尽量对齐成4个float来排列
struct Light {
  XMFLOAT3 Strenth; // 光强(颜色)
  float FalloffStart; // 线性衰减替代二次衰减,开始衰减位置(仅点光源和聚光源)
  XMFLOAT3 Direction; // 光照方向(仅平行光和聚光源)
  float FalloffEnd; // 终止衰减位置(仅点光源和聚光源)
  XMFLOAT3 Position; // 位置(仅点光源和聚光源)
  float SpotPower; // 聚光衰减系数(仅聚光源)
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • Blinn-Phong模型的实现,平行光、点光源、聚光源的实现,详见代码

8.14 Demo

  • 详见代码

第九章 纹理

9.1 复习纹理和资源

  • 纹理用ID3D12Resource进行表示,之前用过的depth buffer和back buffer都是将D3D12_RESOURCE_DESC::Dimension设置为D3D12_RESOURCE_DIMENSION_TEXTURE2D的纹理
  • 纹理格式详见4.1.3
  • 纹理常用于render target或shader resource,或既是render target又是shader resource,但在不同时间读(shader resource)和写(render target),这被称为render-to-texture。但它需要两个descriptor,一个RTV,放到D3D12_DESCRIPTOR_HEAP_TYPE_RTV的堆里;一个SRV,放到D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV的堆里

9.2 纹理坐标

  • 以左上角为原点,取值0-1之间

9.3 纹理数据来源

  • 最常见的方法是先得到BMP、PNG之类的图片,然后在加载的时候载入ID3D12Resource类。但是,DDS格式是GPU原生支持的,对实时图形应用更加有利。同时,它支持GPU原生支持解压的压缩图片格式
  • DDS格式包含了以下数据,从而对GPU有了专门的支持:
    • mipmaps
    • GPU可解压的压缩格式
    • texture arrays
    • cube maps
    • volume textures
  • 生成DDS图片,可以:
    • Photoshop导出
    • texconv命令行工具

9.4 创建和启用纹理

9.4.1 加载DDS文件

  • 使用辅助函数DDSTextureLoader.h/.cpp中的CreateDDSTextureFromFile12()
  • 由于数据要从CPU传到GPU,因此和之前的constant buffer、动态vertex buffer类似,也需要先放到upload buffer中

9.4.2 SRV Heap

  • ID3D12Device::CreateDescriptorHeap()创建一个SRV堆

9.4.3 创建SRV Descriptor

  • 填写D3D12_SHADER_RESOURCE_VIEW_DESC数据结构,然后调用md3dDevice->CreateShaderResourceView()创建descriptor

9.4.4 绑定到渲染管线

  • 之前材质是绑定到constant buffer上的,因此每个顶点都是一样的材质,现在我们要将材质绑定到纹理上
  • 本章我们只考虑将材质中的反射率(albedo)一项用纹理表示,FresnelR0和粗糙度仍然用constant buffer

9.5 Filters

  • 放大:纹理上的一个像素对应了屏幕上的许多像素。这种情况下,屏幕上的像素对应了纹理像素间的值,可以选择常量插值(constant interpolation / point interpolation)或线性差值(linear interpolation)
  • 缩小:屏幕上的一个像素对应了纹理上的许多像素。如果此时仍然使用线性插值,可能出现走样的现象,因此使用 mipmap,在初始化阶段就预先计算好平均降采样(或人工指定)的mipmap chain。运行时,可以有两种做法:
    • point filtering:选择最接近的mipmap层,进行插值
    • linear filtering:选择最接近的两个mipmap层,对两层分别进行插值,再对得到的两个数字插值
  • 对于一个方向被压缩(和视平面垂直)的情况,应使用各向异性的filter
  • 不同filter由D3D12_FILTER枚举类区别,常见的有:
    • D3D12_FILTER_MIN_MAG_MIP_POINT:纹理内常量插值,mipmap常量插值
    • D3D12_FILTER_MIN_MAG_LINEAR_MIP_POINT:纹理内线性插值,mipmap常量插值
    • D3D12_FILTER_MIN_MAG_MIP_LINEAR:纹理内线性插值,mipmap线性插值
    • D3D12_FILTER_ANISOTROPIC:各项异性插值

9.6 Address Modes

  • 纹理坐标如果超出了[0,1]范围,则有四种取值方式:
    • wrap:平铺模式(默认)
    • border color:取用户指定的边缘颜色
    • clamp:取和定义域最近点的颜色
    • mirror:镜像地平铺模式
  • wrap模式是默认的,通过把纹理做成无缝的(即上下左右可以无缝贴合),则可以很容易地将纹理扩展开
  • address mode由D3D12_TEXTURE_ADDRESS_MODE枚举类来指定

9.7 采样器对象(Sampler Object)

  • filter和address mode由采样器对象管理,并传到shader中
  • 我们需要先创建一个sampler heap,这需要填写一个D3D12_DESCRIPTOR_HEAP_DESC结构,然后将类型设置为D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER;使用sampler heap,我们可以填写一个D3D12_SAMPLER_DESC结构,调用md3dDevice->CreateSampler()来生成一个sampler descriptor;在root signature中,如果使用descriptor table模式传参,则需要在CD3DX12_DESCRIPTOR_RANGE中,Init为D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER类型。最后,使用mCommandList->SetGraphicsRootDescriptorTable()来在绘制时传参
  • 为了简化上述步骤,Direct3D提供了一些静态sampler可以直接使用(最多可定义2032个静态sampler),我们需要填写CD3DX12_STATIC_SAMPLER_DESC结构,组合成数组,然后在创建root signature时,作为参数传入,如下代码所示。在使用时,这个例子中有6个静态shader,因此我们可以直接在Shader中使用register(s0)到register(s5)
CD3DX12_ROOT_PARAMETER slotRootParameter[4];
// 初始化root parameter
// ......
array<const CD3DX12_STATIC_SAMPLER_DESC, 6> staticSamplers;
// 填写静态sampler结构
// ......
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(4, slotRootParameter,
  (UINT)staticSamplers.size(), staticSamplers.data(),
  D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
// 创建root signature
// ......
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

9.8 在Shader中采样纹理

  • 纹理和采样器在shader中为如下的结构:
Texture2D gDiffuseMap : register(t0);
SamplerState gsamPointWrap : register(s0);
  • 1
  • 2
  • 采样时:
float4 diffuseAlbedo = gDiffuseMap.Sample(gsamPointWrap, pin.TexC) * gDiffuseAlbedo;
  • 1

9.9 Crate Demo

  • 可以调整不同的filter,可以看到,使用D3D12_FILTER_MIN_MAG_MIP_POINT,在方块平面和视平面接近垂直时,不仅变糊,还出现了马赛克;使用D3D12_FILTER_MIN_MAG_MIP_LINEAR,不会出现马赛克,但也会变糊;使用D3D12_FILTER_ANISOTROPIC,则不会变糊
  • 其他详见代码

9.10 纹理变换

  • 纹理变换可以对纹理进行平移、旋转、缩放,它可能的应用有:
    • 假设目前一个砖墙的纹理坐标范围是[0,1],通过缩放,可以放大和缩小墙上的砖(而不需要改变纹理坐标或纹理贴图)
    • 蓝天上贴上白云的贴图,通过按时间平移纹理,可以做出云朵飘动的效果
    • 纹理旋转可以在粒子特效中发挥作用,如旋转的火球
  • 纹理变换包括两个矩阵,一个是对纹理坐标进行变换,另一个则是对纹理贴图进行变换

9.11 增加纹理的山水Demo

  • 详见代码

第十章 融合(Blending)

10.1 融合方程

  • 如果前物体颜色为C_{src},融合系数为F_{src},后物体颜色为C_{dst},融合系数为F_{dst},则混合后的颜色为(其中 ⊕ \oplus 为10.2定义的运算)

C = ( C d s t ⊗ F d s t ) ⊕ ( C s r c ⊗ F s r c ) C = (C_{dst} \otimes F_{dst}) \oplus (C_{src} \otimes F_{src}) C=(CdstFdst)(CsrcFsrc)

  • 透明度也类似计算,系数取f_{src}和f_{dst}

10.2 融合运算

  • 常规的融合运算定义在D3D12_BLEND_OP中:
    • D3D12_BLEND_OP_ADD
    • D3D12_BLEND_OP_SUBTRACT
    • D3D12_BLEND_OP_REV_SUBTRACT
    • D3D12_BLEND_OP_MIN
    • D3D12_BLEND_OP_MAX
  • 另一类融合运算是逻辑融合运算,定义在D3D12_LOGIC_OP中:
    • D3D2_LOGIC_OP_CLEAR
    • xxx_SET
    • xxx_COPY
    • xxx_COPY_INVERTED
    • xxx_NOOP
    • xxx_INVERT
    • xxx_AND
    • xxx_NAND
    • xxx_OR
    • xxx_NOR
    • xxx_XOR
    • xxx_EQUIV
  • 常规融合运算和逻辑融合运算只能二选一

10.3 融合系数

  • 常见的融合系数类型定义在D3D12_BLEND中
    • D3D12_BLEND_ZERO: F = ( 0 , 0 , 0 ) , f = 0 F=(0,0,0), f=0 F=(0,0,0),f=0
    • D3D12_BLEND_ONE: F = ( 1 , 1 , 1 ) , f = 1 F=(1,1,1), f=1 F=(1,1,1),f=1
    • D3D12_BLEND_SRC_COLOR: F = ( r s , g s , b s ) F=(r_s,g_s,b_s) F=(rs,gs,bs)
    • D3D12_BLEND_INV_SRC_COLOR: F = ( 1 − r s , 1 − r g , 1 − r b ) F=(1-r_s,1-r_g,1-r_b) F=(1rs,1rg,1rb)
    • D3D12_BLEND_SRC_ALPHA: F = ( a s , a s , a s ) , f = a s F=(a_s,a_s,a_s), f=a_s F=(as,as,as),f=as
    • D3D12_BLEND_INV_SRC_ALPHA: F = ( 1 − a s , 1 − a s , 1 − a s ) , f = 1 − a s F=(1-a_s,1-a_s,1-a_s), f=1-a_s F=(1as,1as,1as),f=1as
    • D3D12_BLEND_DST_COLOR
    • D3D12_BLEND_INV_DST_COLOR
    • D3D12_BLEND_DST_ALPHA
    • D3D12_BLEND_INV_DST_ALPHA
    • D3D12_BLEND_SRC_ALPHA_SAT:KaTeX parse error: Undefined control sequence: \mbox at position 39: …_s' ~~~ a_s' = \̲m̲b̲o̲x̲{clamp}(a_s, 0,…
    • D3D12_BLEND_BELND_FACTOR:自定义 F = ( r , g , b ) , f = a F=(r,g,b), f=a F=(r,g,b),f=a
    • D3D12_BLEND_INV_BELND_FACTOR:自定义 F = ( 1 − r , 1 − g , 1 − b ) , f = 1 − a F=(1-r,1-g,1-b), f=1-a F=(1r,1g,1b),f=1a

10.4 融合状态

  • 之前,我们一直使用了默认的融合状态,即
mPsoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
  • 1
  • 对于非默认融合状态,我们需要先填一个D3D12_BLEND_DESC的结构:
typedef struct D3D12_BLEND_DESC {
  // 在多重采样中,将采样点的alpha纳入考虑中,从而达到柔和边缘的作用,
  // 在绘制叶子、草时较为有用
  // 参考:https://blog.csdn.net/leonwei/article/details/53099634
  BOOL AlphaToCoverageEnable; // 默认为False
  // Direct3D支持同时渲染八个对象,它们的融合系数和运算都不同;
  // 若关闭,则多个对象都使用第一个元素的参数
  BOOL IndependentBlendEnable; // 默认为False
  D3D11_RENDER_TARGET_BLEND_DESC RenderTarget[8];
} D3D11_BLEND_DESC;
typedef struct D3D12_RENDER_TARGET_BLEND_DESC {
  // 前两个只有一个可以为true
  BOOL BlendEnable; // 默认为False
  BOOL LogicOpEnable; // 默认为False 
  D3D12_BLEND SrcBlend; // 默认为D3D12_BLEND_ONE
  D3D12_BLEND DstBlend; // 默认为D3D12_BLEND_ZERO
  D3D12_BLEND_OP BlendOp; // 默认为D3D12_BLEND_OP_ADD
  D3D12_BLEND SrcBlendAlpha; // 默认为D3D12_BLEND_ONE
  D3D12_BLEND DstBlendAlpha; // 默认为D3D12_BLEND_ZERO
  D3D12_BLEND_OP BlendOpAlpha; // 默认为D3D12_BLEND_OP_AD
  D3D12_LOGIC_OP LogicOp;  // 默认为D3D12_LOGIC_OP_NOOP
  // 可以选择只融合某一个或某几个RGBA通道
  UINT8 RenderTargetWriteMask; // 默认为D3D12_COLOR_WRITE_ENABLE_ALL
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

10.5 例子

  • 相加:会变亮

图片

  • 相减:会变暗

图片

  • 相乘:

图片

  • 透明: C = a s C s r c + ( 1 − a s ) C d s t C = a_s C_{src} + (1-a_s) C_{dst} C=asCsrc+(1as)Cdst
    • 和绘制顺序相关:首先绘制不透明物体,然后从后向前绘制透明物体
  • 和depth buffer的关系:
    • 对于相加、相减、相乘,我们可以不从后向前绘制,因为这些操作是可交换的。然而,我们不应使用深度检测,否则如果先绘制了前物体,后物体就会被遮挡,不再由pixel shader计算。一种方法是,对于透明物体,我们不将它们的深度写入depth buffer,但仍绘制到back buffer上。注意我们仅仅关闭了depth buffer的写,而没有关闭深度检测,通过这样的方法,如果一堵墙后面有一个半透明的物体,我们仍然可以通过深度检测跳过它的计算
    • 下图是许多半透明粒子叠加的效果

图片

10.6 Alpha通道

  • 纹理的alpha通道可以用来做透明度的设置

10.7 Clipping Pixels

  • HLSL中有一个clip(x)函数,如果x小于0,shader就直接退出,不再进行计算
  • 对于网格状或其他有大面积透明区域的纹理,可以通过clip()函数来去除透明区域的颜色计算,从而简化运算

10.8 雾

  • 雾的效果除了可以带来雾以外,还有许多其他好处:
    • 防止popping,popping指当远处物体进入视锥的远平面时,会突然被绘制。雾可以消除这种突兀感。因此即使是晴天,我们也可以在较远的地方设置一些雾气
  • 雾的颜色:

C f o g = C d s t + s ( C f o g − C d s t ) C_{fog} = C_{dst} + s(C_{fog} - C_{dst}) Cfog=Cdst+s(CfogCdst)

KaTeX parse error: Undefined control sequence: \mbox at position 5: s = \̲m̲b̲o̲x̲{saturate} \lef…

第十一章 模板(Stenciling)

  • 在实现镜面时,可以将物体镜像后绘制,但此时无法保证只绘制镜面内的物体,这可以通过模板来解决,如:

图片

  • 填写D3D12_DEPTH_STENCIL_DESC结构,然后在填写PSO时赋值给相应的成员变量

11.1 Depth/Stencil格式和清除

  • 使用ID3D12GraphicsCommandList::ClearDepthStencilView()清除缓存

11.2 模板测试

if (comp(StencilRef & StencilReadMask, Value & StencilReadMask))
  // accept pixel
else // reject pixel
  • 1
  • 2
  • 3
  • StencilRef是程序预先设置好的阈值,而Value则是根据实际情况运算得到的值,comp是枚举类D3D12_COMPARISON_FUNC中的一个:
    • D3D12_COMPARISON_NEVER/_ALWAYS
    • xxx_LESS/_EQUAL/_LESS_EQUAL/_GREATER/_NOT_EQUAL/_GREATER_EQUAL

11.3 描述Depth/Stencil状态

  • 需填写D3D12_DEPTH_STENCIL_DESC结构:
typedef struct D3D12_DEPTH_STENCIL_DESC {
  // 是否启用depth buffer,如果为false,则DepthWriteMask无效
  BOOL DepthEnable; // 默认:true
  
  // D3D11_DEPTH_WRITE_MASK_ZERO:禁止写入depth buffer,但仍depth test
  // D3D11_DEPTH_WRITE_MASK_ALL:允许写入,通过depth test和stencil test才能绘制
  D3D12_DEPTH_WRITE_MASK DepthWriteMask; // 默认:D3D11_DEPTH_WRITE_MASK_ALL
  
  D3D12_COMPARISON_FUNC DepthFunc; // 默认:D3D11_COMPARISON_LESS
  
  // 是否启用stencil buffer
  BOOL StencilEnable; // 默认:false
  // 读取时的掩码,在stencil test中使用
  UINT8 StencilReadMask; // 默认:0xff
  // 写入时的掩码
  UINT8 StencilWriteMask; // 默认:0xff
  
  // 前面和后面使用stencil buffer的方法
  D3D12_DEPTH_STENCILOP_DESC FrontFace;
  D3D12_DEPTH_STENCILOP_DESC BackFace;  
} D3D12_DEPTH_STENCIL_DESC;
typedef struct D3D12_DEPTH_STENCILOP_DESC {
  D3D12_STENCIL_OP StencilFailOp; // 默认:D3D12_STENCIL_OP_KEEP
  D3D12_STENCIL_OP StencilDepthFailOp; // 默认:D3D12_STENCIL_OP_KEEP
  D3D12_STENCIL_OP StencilPassOp; // 默认:D3D12_STENCIL_OP_KEEP
  D3D12_COMPARISON_FUNC StencilFunc; // 默认:D3D12_COMPARISON_ALWAYS
} D3D12_DEPTH_STENCILOP_DESC;
typedef enum D3D12_STENCIL_OP {
  D3D12_STENCIL_OP_KEEP, // 不修改stencil buffer的值
  xxx_ZERO, // stencil buffer设置为0
  xxx_REPLACE, // 使用StencilRef的值进行替换
  xxx_INCR_SAT, // stencil buffer值加一,直到最大值
  xxx_DECR_SAT, // stencil buffer值减一,直到最小值
  xxx_INVERT, // stencil buffer的值取逆
  xxx_INCR, // stencil buffer值加一,到最大值继续增加溢出到最小值
  xxx_DECR, // stencil buffer值减一,到最小值继续减小溢出到最大值
} D3D12_STENCIL_OP;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 填写完成后赋值给PSO的DepthStencilState成员
  • 使用mCommandList->OMSetStencilRef()来设置stencil buffer阈值

11.4 实现平面镜

  • 平面镜的实现分两步:将物体镜面对称(对于每个顶点都知道的物体而言很容易),仅在镜子的范围内绘制。对于第二步,又分为:
    • 首先绘制镜子之外的物体
    • 将stencil buffer清零
    • 仅将镜子绘制在stencil buffer上。为了完成这一步,我们需要:
      • 禁止将颜色写到back buffer上。D3D12_RENDER_TARGET_BLENDER_DESC::RenderTargetWriteMask = 0
      • 禁止写depth buffer。D3D12_DEPTH_STENCIL_DESC::DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO
      • 开启stencil test,设置StencilFunc为D3D12_COMPARISON_ALWAYS,设置StencilRef为1,设置StencilPassOp为D3D12_STENCIL_OP_REPLACE,设置StencilDepthFailOp为D3D12_STENCIL_OP_KEEP
    • 开始绘制需要镜像的物体到镜子区域。设置StencilRef为1,设置StencilFunc为D3D12_COMPARISON_EQUAL。这样,只有镜子区域被绘制了,其他区域都没有通过stencil test
    • 绘制镜子。为了让后面的物体能够被看到,镜子需要绘制成半透明的。若将镜子透明度设置为a,则颜色为 C = a C s r c + ( 1 − a ) C d s t C = a C_{src} + (1-a) C_{dst} C=aCsrc+(1a)Cdst
  • 另一个需要注意的是,物体镜像后,面片顶点方向发生变化,导致顶点法向变反。因此要将mPsoDesc.RasterizationState.FrontCounterClockwise设置为true

11.5 实现平面镜中的阴影

  • 此节仅讲述平面阴影的情况,因此是一种比较粗糙的方法
  • 将物体投影到平面上,然后按照一定透明度、黑色材质绘制物体投影体。需要注意的是,物体投影体可能会有很多重叠,造成黑色透明材质被绘制很多遍,从而颜色不均匀。这一问题可以通过stencil进行解决
  • 如何计算投影,过程详见书本,结论如下图所示(适用于平行光和点光源):

图片

  • 如何通过stencil test避免绘制重叠部分,如下:
    • stencil buffer初始化为0
    • 设置stencil buffer只接受值为0的pixel进行绘制,接受后,通过D3D12_STENCIL_INCR_SAT将值修改为1

第十二章 几何着色器(Geometry Shader)

  • 若我们没有使用tessellation stage,则介于vertex shader和pixel shader之间的几何着色器是可选的
  • vertex shader以顶点为输入,而geometry shader以面元为输入,从观念上,geometry shader相当于一个如下的函数,它以一列的顶点作为输入,输出一列面元
for (UINT i=0; i<numTriangles; ++i) {
  OutputPrimitiveList = GeometryShader(T[i].vertexList);
}
  • 1
  • 2
  • 3
  • 因此,vertex shader不可以创造或毁灭顶点,但geometry shader就可以。因此,geometry shader可以将输入的一个元素变成多个元素(如粒子特效,或将一个顶点变成一个正方形),或依据一些条件不输出相应的面元

12.1 Geometry Shader编程

  • Geometry Shader的结构如下:
[maxvertexcount(N)]
void ShaderName(
  PrimitiveType InputVertexType InputName[NumElements],
  inout StreamOutputObject<OutputVertexType> OutputName) {
  // 代码主体
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • N是geometry shader一次调用最大的顶点输出数量。实际输出数量可以小于这一数值,但不可以大于它。根据2008年Nvidia的一篇文章,geometry shader的效率巅峰在输出1-20个标量数值,如果输出在27-40个标量数值,效率就会下降到50%。标量数值就是顶点个数和顶点数据结构大小的乘积
  • NumElements如果为
    • 1:一个顶点
    • 2:一条线
    • 3:一个三角形
    • 4:连接的线(lists方式或strips方式)
    • 6:连接的三角形(lists方式或strips方式)
  • 例子:输入一个单位圆上的三角形,输出三角形(在圆上)的四等分
struct VertexOut {
  float3 PosL : POSITION;
  float3 NormalL : NORMAL;
  float2 Tex : TEXCOORD;
};
struct GeoOut {
  float4 PosH : SV_POSITION;
  float3 PosW : POSITION;
  float3 NormalW : NORMAL;
  float3 Tex : TEXCOORD;
  float2 FogLerp : FOG;
};
// 顶点为0-1-2的三角形,0-1中点为m0,1-2中点为m1,0-2中点为m2
void Subdivide(VertexOut inVerts[3], out VertexOut outVerts[6]) {
  VertexOut m[3];
  m[0].PosL = 0.5f * (inVerts[0].PosL + inVerts[1].PosL);
  m[1].PosL = 0.5f * (inVerts[1].PosL + inVerts[2].PosL);
  m[2].PosL = 0.5f * (inVerts[2].PosL + inVerts[0].PosL);
  
  // 投影到圆上
  m[0].PosL = normalize(m[0].PosL);
  m[1].PosL = normalize(m[1].PosL);
  m[2].PosL = normalize(m[2].PosL);
  m[0].NormalL = m[0].PosL;
  m[1].NormalL = m[1].PosL;
  m[2].NormalL = m[2].PosL;
  
  m[0].Tex = 0.5f * (inVerts[0].Tex + inVerts[1].Tex);
  m[1].Tex = 0.5f * (inVerts[1].Tex + inVerts[2].Tex);
  m[2].Tex = 0.5f * (inVerts[2].Tex + inVerts[0].Tex);
  outVerts[0] = inVerts[0];
  outVerts[1] = m[0];
  outVerts[2] = m[2];
  outVerts[3] = m[1];
  outVerts[4] = inVerts[2];
  outVerts[5] = inVerts[1];
}
void OutputSubdivision(VertexOut v[6], 
  inout TriangleStream<GeoOut> triStream) {
  GeoOut gout[6];
  
  [unroll]
  for (int i=0; i<6; ++i) {
    // 转换到世界坐标
    gout[i].PosW = mul(float4(v[i].PosL, 1.0f), gWorld).xyz;
    gout[i].NormalW = mul(v[i].NormalL, (float3x3)gWorldInvTranspose);
    
    // 转换到裁剪坐标
    gout[i].PosH = mul(float4(v[i].PosL, 1.0f), gWorldViewProj);
    
    gout[i].Tex = v[i].Tex;
  }
  
  //    1
  //  m0  m1
  // 0  m2  2
  // 将三角形按照triangle strips的方式排列到triStream中
  // 注意,上述4个三角形不可能由一个strip完成,因此需要restart,用两个strip组合
  // 第一个是0-m0-m2-m1-2,第二个是m0-1-m1
  [unroll]
  for (int j=0; j<5; ++j) {
    triStream.Append(gout[j]);
  }
  triStream.RestartStrip();
  
  triStream.Append(gout[1]);
  triStream.Append(gout[5]);
  triStream.Append(gout[3]);
}
[maxvertexcount(8)]
void GS(triangle VertexOut gin[3], inout TriangleStream<GeoOut>) {
  VertexOut v[6];
  Subdivide(gin, v);
  OutputSubdivision(v, triStream);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75

12.2 树的Demo

  • 当树在很远的地方时,可以使用billboard技术来加速。即,我们不绘制一个完整的树的模型,而是绘制一个矩形,上面放上树的图片。这一技术的关键在于这个矩形必须时时垂直于摄像机。
  • 每棵树对应了一个顶点。以该顶点为原点,朝向摄像机为w轴,竖直向上为v轴,叉乘结果为u轴,则可以垂直w轴,平行v轴和u轴,通过geometry shader生成一个矩形,矩形大小由树贴图的包围盒大小决定。在矩形上渲染树的贴图,即可得到树的绘制
  • geometry shader还可以增加一个可选的输入:
[maxvertexcount(4)]
void GS(point VertexOut gin[1],
  uint primID : SV_PrimitiveID,
  inout TriangleStream<GeoOut> triStream)
  • 1
  • 2
  • 3
  • 4
  • primitive ID是input assembly阶段对每个面元自动生成的编号,对于每次draw call,面元都会从0开始编号,因此在一次draw call中primitive ID是唯一的。如果geometry shader被省略了,SV_PrimitiveID也可以加到pixel shader中;但如果geometry shader未省略,则若要使用primitive ID,则必须通过geometry shader走
  • 此外,input assembly阶段也可以产生一个vertex ID,只需要在vertex shader的参数中加一个SV_VertexID的类型就可以了
  • 获取面元的primitive ID,在pixel shader中就可以根据不同的ID在不同的纹理上采样,详见12.3

12.3 纹理序列

12.4 Alpha-to-Coverage

  • 如果生硬地使用alpha通道,则树容易出现硬边
    • 一种方法是在边缘使用半透明融合而不是alpha test。但这一方法需要对渲染物体排序,且从后向前渲染。若对一片森林每帧都要进行排序,则是非常低效的
    • 另一方面方法是使用多重采样,但简单的多重采样仅仅根据是否物体是否覆盖了子像素,来平均像素的颜色
    • 因此,alpha-to-coverage运用了多重采样的方法,对alpha通道也进行了平均
  • 开启此功能,需D3D12_BLEND_DESC::AlphaToCoverageEnable = true以及mEnable4xMsaa = true

第十三章 计算着色器(Compute Shader)

  • GPU的并行计算能力,可以运用在一些非渲染的工作上(General Purpose GPU)但它仅适合对大量数据进行相似计算的场景,如:
    • 在每个像素上计算颜色
    • 水波模拟时在每个顶点上求解波函数
    • 粒子特效
  • 对这一类GPGPU编程,通常计算输入需要从CPU拿到GPU,计算结果需要从GPU拿回到CPU,尽管CPU和RAM交换数据非常快,GPU和VRAM交换数据更加快,但CPU和GPU交换数据是比较慢的。在图形学上,我们通常完成计算后,再把结果交给GPU渲染,从而避免GPU拿回数据到CPU
  • 计算着色器不直接作为渲染管线的一部分,但它可以完成CPU和GPU的交互

13.1 线程和线程群

  • thread们被分配到一堆的thread group中,一个thread group只能被一个处理器处理。比如假设有16个处理器,则我们希望将问题分配到至少16个thread group,从而每个处理器都能被利用起来。此外,最好每个预处理器上至少有2个thread group,这样可以在一个thread group陷入等待时处理另一个thread group
  • 每个thread group中,thread们可以共享内存,但不同的thread group之间不能共享内存;同一个thread group中可以对thread进行同步,但不同的thread group之间不能进行同步,且它们的执行顺序是不确定的
  • 一个thread group中包含n个thread,硬件通常将32个thread组成一个warp,从而一个warp可以使用SIMD32指令来加速。CUDA中,每个CUDA核心处理一个线程,而一个Fermi架构的处理器中含有32个CUDA核心。在Direct3D中,出于性能考虑,最好一个thread group的维度应为warp大小的整数倍
  • 在Direct3D中,使用ID3D12GraphicsCommandList::Dispatch(ThreadGroupCountX, ThreadGroupCountY, ThreadGroupCountZ)来分配3D格点的thread group,例如,下图分配了一个3*2的thread group,每个thread group中含有64个thread

图片

13.2 一个简单的Compate Shader例子

cbuffer cbSettings {
  // compute shader可以从constant buffer中获取值
};
Texture2D gInputA;
Texture2D gInputB;
RWTexture2D<float4> gOutput;
// 一个thread group中thread的数量
[numthreads(16,16,1)]
void CS(int3 dispatchThreadID : SV_DispatchThreadID) {
  gOutput[dispatchThreadID.xy] = gInputA[dispatchThreadID.xy] + gInputB[dispatchThreadID.xy];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 为了运行一个compute shader,我们需要一个平行于渲染管线的计算管线,因此首先要填写一个D3D12_COMPUTE_PIPELINE_STATE_DESC的数据结构,然后使用md3dDevice->CreateComptePipelineState()生成compute PSO

13.3 数据输入输出资源

13.3.1 输入纹理

  • 和之前类似,在root signature中设置好,然后使用mCommandList->SetComputeRootDescriptorTable()传入

13.3.2 输出纹理和UAV(Unordered Access Views)

  • 在computer shader中,输出纹理需用RWTexture2D来表示,RW表示read-write
  • 输出也需要绑定到descriptor heap中的一个descriptor,这一类的descriptor应使用unordered access view (UAV)
  • 首先填写D3D12_RESOURCE_DESC结构,用md3dDevice->CreateCommitedResource()生成一个资源
  • 由于纹理既需要作为computer shader的输出,又需要作为后续渲染的输入,因此既需要作为一个UAV,又需要作为一个SRV,因此填写D3D12_UNORDERED_ACCESS_VIEW_DESC结构和D3D12_SHADER_RESOURCE_VIEW_DESC结构,然后调用md3dDevice->CreateShaderResourceView()和md3dDevice->CreateUnorderedAccessView()
  • 注意对于UAV的资源,D3D12_RESOURCE_DESC::Flags需要设置为D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS
  • 可以将UAV放到D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV类型的heap中

13.3.3 纹理索引和采样

  • 纹理的每个像素可以通过thread ID进行索引,thread ID见13.4
  • 读写越界在compute shader中都是有定义的行为,读越界返回0,写越界什么都不发生
  • 在这里,采样不可以使用Sample方法,而必须要使用SampleLevel方法,因为:
    • SampleLevel取三个参数,前两个参数表示纹理坐标,最后一个参数表示mipmap级别;而Sample只取最合适的一层(或两层)mipmap
    • Sample将纹理坐标归一化到0-1之间,而SampleLevel则是原始的大小

13.3.4 结构化缓存资源

  • 之前的例子都是纹理,对于数组,则在compute shader中使用StructuredBuffer(作为输入)和RWStructuredBuffer(作为输出)来表示,它们照样还是用SRV或者UAV在计算管线中表示

13.3.5 拷贝Computer Shader结果回内存

  • 首先需创建一个类型为D3D12_HEAP_TYPE_READBACK的资源,然后使用mCommandList->CopyResource()方法来取回数据
ThrowIfFailed(md3dDevice->CreateCommitedResource(
  &CD3DX12_HEAP_PEOPERTIES(D3D12_HEAP_TYPE_READBACK),
  D3D12_HEAP_FLAG_NONE,
  &CD3DX12_RESOURCE_DESC::Buffer(byteSize),
  D3D12_RESOURCE_STATE_COPY_DEST,
  nullptr, IID_PPV_ARGS(&mReadBackBuffer)));
  
// 完成computer shader计算,保存在mOutputBuffer中
mCommandList->ResourceBarrier(1,
  &CD3DX12_RESOURCE_BARRIER::Transition(mOutputBuffer.Get(), 
  D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_SOURCE));
mCommandList->CopyResource(mReadBackBuffer.Get(), 
  mOutputBuffer.Get());
mCommandList->ResourceBarrier(1,
  &CD3DX12_RESOURCE_BARRIER::Transition(mOutputBuffer.Get(), 
  D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_COMMON));
  
// 关闭mCommandList,等待command执行完成
FlushCommandQueue();
Data* mappedData = nullptr;
ThrowifFailed(
  mReadBackBuffer->Map(0, nullptr, reinterpret<void**>(&mappedData)));
  
// 使用数据
mReadBackBuffer->Unmap(0, nullptr);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 从GPU拷贝数据到CPU的ReadBack类型的资源,以及拷贝方法,和从CPU拷贝数据到GPU的Upload类型的资源非常相似

13.4 线程ID

图片

  • 如图,thread T的:
    • SV_GroupID:(1,1,0)
    • SV_GroupThreadID:(2,5,0)
    • SV_DispatchThreadID为

( 1 , 1 , 0 ) ⊗ ( 8 , 8 , 0 ) + ( 2 , 5 , 0 ) = ( 10 , 13 , 0 ) (1,1,0) \otimes (8,8,0) + (2,5,0) = (10,13,0) (1,1,0)(8,8,0)+(2,5,0)=(10,13,0)

* SV_GroupIndex为1*3+1=4
  • 1

13.5 消费者-生产者缓冲

  • 有时我们不关心计算的顺序,如粒子系统,给定每个例子的位置、速度和加速度,求解下一时刻的位置、速度和加速度,则粒子的计算顺序是不重要的,这时生产者-消费者模型就非常好,不同的thread从生产者那儿拿到数据进行“消费”,计算得到结果。这时需使用ConsumeStructuredBuffer和AppendStructuredBuffer,如:
struct Particle {
  float3 Position, Velocity, Acceleration;
};
float TimeStep = 1.0f / 60.0f;
ConsumeStructuredBuffer<Particle> gInput;
AppendStructuredBuffer<Particle> gOutput;
[numthreads(16, 16, 1)]
void CS() {
  Particle p = gInput.Consume();
  // 计算p
  // ......
  gOutput.Append(p);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • AppendStructuredBuffer并不是动态增长的,而是预先一个足够大的空间

13.6 共享内存和同步

  • 共享内存可如下定义。它最大空间为32k。
groupshared float4 gCache[256];
  • 1
  • 使用太大的共享内存会有性能问题。假设处理器支持32k的共享内存,而每个thread group使用了20k的共享内存,则处理器一次只能运行一个thread group,降低了并发性
  • 共享内存的常见例子是纹理,一些算法(如模糊运算)需要多次访问纹理的每个texel,而在纹理上采样是比较慢的运算,因此可以让每个thread先将对应的texel存下来,放在共享内存中,然后再进行算法运算,如:
Texture2D gInput;
RWTexture2D<float4> gOutput;
groupshared float4 gCache[256];
[numthreads(256, 1, 1)]
void CS(int3 groupThreadID : SV_GroupThreadID,
  int3 dispatchThreadID : SV_DispatchThreadID) {
  // 每个线程先采样,保存到共享内存中
  gCache[groupThreadID.x] = gInput[dispatchThreadID.xy];
  
  // 如果直接进行运算,则无法保证其他thread已经完成采样的工作,因此这里需要先进行同步
  GroupMemoryBarrierWithGroupSync();
  
  // 其他运算
  // ......
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

13.7 Blur Demo

  • Blur就是使用高斯卷积核进行模糊。由于高斯分布在各个方向上的独立性,一个二维高斯卷积核可以被分成两个一维高斯卷积核,这不但简化了compute shader的实现(在thread group边缘的像素很难提前存好texel值),还减少了采样数(假设9*9的卷积核,若在二维上操作,则需要采样81个texel;在一维上操作,只需要采样9+9个texel)
  • 之前,我们之所以可以渲染到back buffer中,只是因为我们在swap chain中建立了texture resource,并在绘制时使用了mCommandList->OMSetRenderTargets()指定绘制在这个texture上,最后使用mSwapChain->Present()方法将它展示出来。因此,我们完全可以创建一个其他texture(选择另一个视角),绑定到Output Merger上进行绘制,这一技术就是离屏渲染(render-to-off-screen)或纹理绘制(render-to-texture)。它可以
    • 生成3D小地图
    • 阴影映射(shadow mapping)
    • 屏幕空间环境光遮挡(screen space ambient occlusion)
    • 立方体纹理的动态反射(dynamic reflection with cube maps)
  • 实现模糊算法的整体流程:
    • 将正常的场景渲染到off-screen texture中
    • 将渲染得到的texture输入compute shader计算它的模糊结果
    • 重新设置back buffer为render target,并绘制一个覆盖整个屏幕的矩形,矩形使用模糊结果作为纹理
  • 如果模糊前后的纹理在参数上(如大小、格式)一致,则可以简化上述流程,将正常场景渲染到back buffer上但不显示,然后通过mCommandList->CopyResource()将back buffer拷贝到另一个texture上输入compute shader
  • 上述过程先渲染,再计算,又渲染,多次切换,会降低效率。出于性能考虑,应尽量先一次性做完全部计算工作,再一次性做完全部渲染工作。这里确实无法避免这一情况的出现。
  • Blur实现流程:
    • 创建两个资源A和B
    • 将A绑定到SRV上,B绑定到UAV上
    • 分配thread group,进行水平方向blur,结果存储在B上
    • 将B绑定到SRV上,A绑定到UAV上
    • 分配thread group,进行竖直方向blur,结果存储在A上

13.8 更多关于Compute Shader的资料

第十四章 细分(Tessellation)

  • 动机:为何要细分而不是一个已经细分好的模型?
    • 动态LOD。可以根据摄像机距离以及其他因素,动态调节模型细节级别
    • 简化物理动画
    • 节省空间
  • tessellation在vertex shader到geometry shader之间,包含了hull shader、tessellator stage、domain shader stage三个部分

14.1 Tessellation元素类型

  • 如果使用了tessellation,我们不再上传三角形到Input Assembly阶段,而是上传一组由控制点组成的patch,一个patch可以由1-32个控制点组成,它们的类型被定义为:D3D_PRIMITIVE_TOPOLOGY_{n}_CONTROAL_POINT_PATCHLIST,n为1-32
  • 一个三角形可以被认为是一个由三个控制点组成的三角形patch,因此我们仍然可以上传常规的三角形网格到tessellation。一个四边形由四个控制点组成,它会在tessellation阶段被细分为三角形
  • 更多的控制点则和贝塞尔曲线、曲面有关
  • 由于我们处理的是控制点,因此输入和输出vertex shader的也是控制点

14.2 Hull Shader

  • hull shader分成了constant hull shader和control point hull shader

14.2.1 Constant Hull Shader

  • constant hull shader逐patch进行,输出网格的tessellation factors,它们将在tessellation stage决定如何细分一个patch
  • 一个例子:均匀地细分一个四边形3次
struct PatchTess {
  // 决定了四边形的四条边如何细分
  float EdgeTess[4] : SV_TessFactor;
  // 决定四边形的内部(二维流形,因此只有u轴和v轴,水平/竖直)如何细分
  float InsideTess[2] : SV_InsideTessFactor;
  // 以及其他vertex shader传出的数据
};
PatchTess ConstantHS(InputPatch<VertexOut, 4> patch, // 四个控制点
  uint patchID : SV_PrimitiveID // 第几个patch
) {
  PatchTess pt;
  // 均匀细分3次
  pt.EdgeTess[0] = 3; // 左边
  pt.EdgeTess[1] = 3; // 上边
  pt.EdgeTess[2] = 3; // 右边
  pt.EdgeTess[3] = 3; // 下边
  pt.InsideTess[0] = 3; // u轴
  pt.InsideTess[1] = 3; // v轴  
  
  return pt;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 四边形有4个EdgeTess参数,2个InsideTess参数,而三角形则只有3个EdgeTess参数,1个InsideTess参数。例子详见14.3
  • 最大tessellation factor为64,如果所有tessellation factors都是0,则这个patch不显示
  • 何时增加或减少细节:
    • 和相机的距离
    • 占据屏幕面积
    • 朝向,显示出物体轮廓的需更加细分
    • 粗糙度,粗糙的物体需要更多细分才能显示出细节
  • 性能建议:
    • 如果tessellation factors都是1,即不进行细分,则跳过tessellation环节
    • 不要对占据像素少于8个的三角形进行细分
    • 将使用tessellation的draw call放到一起绘制,不要频繁开关tessellation

14.2.2 Control Point Hull Shader

  • control point hull shader输入一系列控制点并输出一系列控制点。它在每个输出控制点上都会运行一次。一个例子是输入一个常规的三角形(3个控制点),输出一个三次贝塞尔三角形片(10个控制点),这个策略被称为N-patches scheme或PN triangles scheme
  • 简单的“穿过”例子:不做任何处理,直接输出
struct HullOut {
  float3 PosL : POSITION;
};
[domain("quad")] // patch类型,可以是tri,quad和isoline
[partitioning("integer")] // 分割模式,integer表示新的顶点只会增加在整数tessellation factor的位置,fractional_even/fractional_odd表示新的顶点会增加在整数位置,但稍稍偏移,这适用于细分数慢慢增加或减少时,可以平滑过渡,而不会发生跳变
[outputtopology("triangle_cw")] // 输出拓扑:triangle_cw表示顺时针三角形,triangle_ccw表示逆时针三角形,line表示线细分
[outputcontrolpoints(4)] // 一共输出4个控制点,由于每个输出控制点运行一次,因此整个shader运行了4次
[patchconstantfunc("ConstantHS")] // 指定constant hull shader名称
[maxtessfactor(64.0f)] // 最大tessellation factor
PHullOut HS(InputPatch<VertexOut, 4> p,
  uint i : SV_OutputControlPointID, // 输出控制点ID
  uint patchId : SV_PrimitiveID
) {
  HullOut hout;
  hout.PosL = p[i].PosL;
  return hout;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

14.3 Tessellation阶段

  • 四边形细分例子:

图片

  • 三角形细分例子:

图片

14.4 Domain Shader

  • tessellation阶段输出了新创建的顶点和三角形,domain shader在每个顶点上运行一次
  • 开启tessellation后,原有的vertex shader成为了每个输入控制点上的shader,而hull shader成为了一个细分patch上每个顶点的shader,那么我们还需要一个裁剪空间的vertex shader,至少将坐标从局部空间转换到裁剪空间,这就是domain shader做的事情
  • domain shader的输入是tessellation factors,细分顶点的参数坐标(uv轴上的坐标)和control point hull shader产生的其他控制点。注意,细分顶点的坐标不是直接给出的,而仅仅是一个uv坐标,因此需要自己进行双线性插值
struct DomainOut {
  float4 PosH : SV_POSITION;
};
[domain("quad")]
DomainOut DS(PatchTess patchTess,
  float2 uv : SV_DomainLocation,
  const OutputPatch<HullOut, 4> quad
) {
  DomainOut dout;
  // 双线性插值
  float3 v1 = lerp(quad[0].PosL, quad[1].PosL, uv.x);
  float3 v2 = lerp(quad[2].PosL, quad[3].PosL, uv.x);
  float3 p = lerp(v1, v2, uv.y);
  
  float4 posW = mul(float4(p, 1.0f), gWorld);
  dout.PosH = mul(posW, gViewProj);
  
  return dout;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

14.5 细分一个四边形

  • 通过细分,将一个四边形细分成山峦形,摄像机越近,则细分越厉害

图片

struct VertexIn {
  float3 PosL : POSITION;
};
struct VertexOut {
  float3 PosL : POSITION;
};
VertexOut VS(VertexIn vin) {
  VertexOut vout;
  vout.PosL = vin.PosL;
  return vout;
}
struct PatchTess {
   float EdgeTess[4] : SV_TessFactor;
   float InsideTess[2] : SV_InsideTessFactor;
};
PatchTess ConstantHS(InputPatch<VertexOut, 4> patch,
  uint patchID : SV_PrimitiveID) {
  PatchTess pt;
  // 测量摄像机与物体的距离
  float3 centerL = 0.25 * (patch[0].PosL + ... + patch[3].PosL);
  float3 centerW = mul(float4(centerL, 1.0f), gWorld).xyz;
  float d = distance(centerW, gEyePosW);
  // 在最近距离和最远距离之间,按0-64插值
  const float d0 = 20.0f, d1 = 100.0f;
  float tess = 64.0f * saturate( (d1-d)/(d1-d0) );
  // 均匀细分
  pt.EdgeTess[0] = ... = pt.EdgeTess[3] = tess;
  pt.InsideTess[0] = pt.InsideTess[1] = tess;
  return pt;
}
struct HullOut {
  float3 PosL : POSITION;
};
[domain("quad")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(4)]
[patchconstantfunc("ConstantHS")]
[maxtessfactor(64.0f)]
PHullOut HS(InputPatch<VertexOut, 4> p,
  uint i : SV_OutputControlPointID, // 输出控制点ID
  uint patchId : SV_PrimitiveID
) {
  HullOut hout;
  hout.PosL = p[i].PosL;
  return hout;
}
struct DomainOut {
  float4 PosH : SV_POSITION;
};
[domain("quad")]
DomainOut DS(PatchTess patchTess,
  float2 uv : SV_DomainLocation,
  const OutputPatch<HullOut, 4> quad
) {
  DomainOut dout;
  // 双线性插值
  float3 v1 = lerp(quad[0].PosL, quad[1].PosL, uv.x);
  float3 v2 = lerp(quad[2].PosL, quad[3].PosL, uv.x);
  float3 p = lerp(v1, v2, uv.y);
  // 山峦位移
  p.y = 0.3f * (p.z * sin(p.x)+ p.x * sin(p.z));
  
  float4 posW = mul(float4(p, 1.0f), gWorld);
  dout.PosH = mul(posW, gViewProj);
  
  return dout;
}
float4 PS(DomainOut pin) : SV_Target {
  return float4(1.0f, 1.0f, 1.0f, 1.0f);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

14.6 三次贝塞尔四边形patch

14.6.1 三次贝塞尔曲线

14.6.2 三次贝塞尔曲面

14.6.3 三次贝塞尔曲面代码

  • 4条沿u轴方向的贝塞尔曲线(其中B为Bernstein基函数):

q 0 ( u ) = B 0 3 ( u ) p 0 , 0 + B 1 3 ( u ) p 0 , 1 + B 2 3 ( u ) p 0 , 2 + B 3 3 ( u ) p 0 , 3 q 1 ( u ) = B 0 3 ( u ) p 1 , 0 + B 1 3 ( u ) p 1 , 1 + B 2 3 ( u ) p 1 , 2 + B 3 3 ( u ) p 1 , 3 q 2 ( u ) = B 0 3 ( u ) p 2 , 0 + B 1 3 ( u ) p 2 , 1 + B 2 3 ( u ) p 2 , 2 + B 3 3 ( u ) p 2 , 3 q 3 ( u ) = B 0 3 ( u ) p 3 , 0 + B 1 3 ( u ) p 3 , 1 + B 2 3 ( u ) p 3 , 2 + B 3 3 ( u ) p 3 , 3

q0(u)=B03(u)p0,0+B13(u)p0,1+B23(u)p0,2+B33(u)p0,3q1(u)=B03(u)p1,0+B13(u)p1,1+B23(u)p1,2+B33(u)p1,3q2(u)=B03(u)p2,0+B13(u)p2,1+B23(u)p2,2+B33(u)p2,3q3(u)=B03(u)p3,0+B13(u)p3,1+B23(u)p3,2+B33(u)p3,3
q0(u)=B03(u)p0,0+B13(u)p0,1+B23(u)p0,2+B33(u)p0,3q1(u)=B03(u)p1,0+B13(u)p1,1+B23(u)p1,2+B33(u)p1,3q2(u)=B03(u)p2,0+B13(u)p2,1+B23(u)p2,2+B33(u)p2,3q3(u)=B03(u)p3,0+B13(u)p3,1+B23(u)p3,2+B33(u)p3,3

  • 曲面上的点:

p ( u , v ) = B 0 3 ( u ) q 0 ( u ) + B 1 3 ( u ) q 1 ( u ) + B 2 3 ( u ) q 2 ( u ) + B 3 3 ( u ) q 3 ( u ) p(u,v) = B_0^3(u) q_0(u) + B_1^3(u) q_1(u) + B_2^3(u) q_2(u) + B_3^3(u) q_3(u) p(u,v)=B03(u)q0(u)+B13(u)q1(u)+B23(u)q2(u)+B33(u)q3(u)

14.6.4 Demo

  • 详见代码
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/407132
推荐阅读
相关标签
  

闽ICP备14008679号