新闻详情

新闻详情

首页 / 资讯中心 / 详情

DX12手搓PBR渲染管线:从Cook-Torrance到IBL的完整实现

发布时间:2026/10/1 3:26:47来源:尧图网络
DX12手搓PBR渲染管线:从Cook-Torrance到IBL的完整实现
1. 从零到一为什么要在DX12里手搓PBR上一篇文章里我们把DX12的初始化流程跑通了命令队列、交换链、描述符堆这些基础设施算是搭起来了屏幕上也能画出个三角形或者简单立方体。但那个画面说实话跟十年前DX11的固定管线效果差不多光照假得不行金属看着像塑料粗糙度全靠贴图硬怼。这次要干的事情就一个把基于物理的渲染Physically Based RenderingPBR塞进DX12的渲染管线里。PBR这套东西在DX11时代就已经很成熟了Cook-Torrance BRDF、GGX法线分布、Smith几何遮蔽、Schlick菲涅尔近似这些公式网上随便一搜就是一大把。但DX12跟DX11最大的区别在于你得自己管资源状态、自己管描述符、自己管根签名。PBR需要的那几张贴图——反照率、法线、金属度、粗糙度、环境光遮蔽——在DX11里就是几个ShaderResourceView往槽位上一绑就完事在DX12里你得先规划好描述符堆的布局再通过根参数把描述符表传给着色器。这个过程中任何一个环节出错画面要么全黑要么全白要么直接给你弹个设备移除的报错。这篇文章适合谁看如果你已经跟着上一篇文章把DX12的框架搭起来了能画出带纹理的立方体但对着PBR的一堆公式不知道从哪下手那这篇就是写给你的。如果你还在用DX11或者更早的API也可以看看DX12这套资源绑定机制到底麻烦在哪为什么很多人说它“底层但啰嗦”。我会把整个实现过程拆成几个阶段先讲清楚PBR的核心原理和数学基础再讲DX12里怎么组织资源然后是着色器代码的具体实现最后是调试过程中踩过的坑和排查方法。每个阶段都会给出可以直接抄的代码片段和参数配置尽量让你少走弯路。注意本文假设你已经有一个能跑通基础渲染的DX12项目包括命令列表录制、描述符堆创建、根签名定义这些前置知识。如果这些还没搞定建议先回去把上一篇文章的代码跑通再来看这篇。2. PBR核心原理与数学拆解2.1 从经验模型到物理模型为什么要换以前做渲染光照计算基本就是Lambert漫反射加Blinn-Phong高光参数全靠美术手调。一个金属材质高光颜色调亮点粗糙度靠高光指数控制看着差不多就行。但这套东西有个致命问题参数没有物理意义换个光照环境就得重新调而且金属和非金属的区分全靠美术自己把握做出来的东西经不起推敲。PBR的核心思想是把光照计算建立在物理定律上。光与物体表面的交互被拆成两个部分漫反射和镜面反射。漫反射是光线进入物体内部经过多次散射后从表面不同位置射出这部分用Lambert模型就够了。镜面反射是光线在物体表面直接反射这部分用微表面理论来描述——假设表面是由无数微小的镜面组成的每个微面的朝向服从某种统计分布宏观上的粗糙度就是这些微面朝向的离散程度。这个理论框架下材质参数变成了三个有物理意义的量反照率Albedo决定漫反射颜色金属度Metallic决定有多少光被反射而不是被吸收粗糙度Roughness决定微面朝向的分布。这三个参数加上法线贴图就能描述绝大多数现实世界中的材质。金属度高的时候漫反射几乎为零所有反射都变成镜面反射而且镜面反射的颜色会被反照率染色——这就是为什么金子是黄色的银子是白色的。2.2 Cook-Torrance BRDF的四个组成部分PBR的核心公式是Cook-Torrance BRDF它把镜面反射拆成三个函数的乘积再除以一个归一化项f_specular (D * G * F) / (4 * NdotL * NdotV)其中D是法线分布函数描述微面朝向的统计分布G是几何遮蔽函数描述微面之间互相遮挡的概率F是菲涅尔函数描述不同角度下反射率的差异。分母的4 * NdotL * NdotV是归一化因子保证能量守恒。法线分布函数我选的是GGX/Trowbridge-Reitz这是目前业界最常用的因为它有很长的拖尾能产生那种柔和的高光边缘看起来更接近真实材质。公式是D_GGX(NdotH, roughness) alpha^2 / (PI * (NdotH^2 * (alpha^2 - 1) 1)^2)其中alpha roughness^2这个平方关系是Disney提出来的为了让粗糙度的感知更线性。NdotH是法线和半角向量的点积。几何遮蔽函数我用的是Smith-GGX的变体Schlick-GGXG_SchlickGGX(NdotV, roughness) NdotV / (NdotV * (1 - k) k)其中k对于直接光照是(roughness 1)^2 / 8对于IBL是roughness^2 / 2。然后Smith方法把两个方向的遮蔽乘起来G_Smith(NdotV, NdotL, roughness) G_SchlickGGX(NdotV, roughness) * G_SchlickGGX(NdotL, roughness)菲涅尔函数用Schlick近似F_Schlick(HdotV, F0) F0 (1 - F0) * (1 - HdotV)^5F0是垂直入射时的反射率对于非金属大约是0.04对于金属就是反照率本身。这个公式的好处是计算便宜而且在大多数情况下足够准确。2.3 直接光照与IBL的分工直接光照就是场景里的平行光、点光源、聚光灯这些每个光源单独计算BRDF然后累加。这部分在像素着色器里循环处理就行DX12里可以用常量缓冲区把光源数据传进去。但只有直接光照的话金属材质会看起来特别假因为金属的漫反射几乎为零没有环境光反射的话背光面就是纯黑的。所以还需要基于图像的光照Image-Based LightingIBL用一张环境贴图来模拟周围环境对物体的光照贡献。IBL的核心是把环境贴图预计算成两张图一张是辐照度图Irradiance Map用来计算漫反射部分的贡献另一张是预过滤环境图Pre-filtered Environment Map用来计算镜面反射部分的贡献。辐照度图可以用卷积计算把半球积分简化成几个采样点的加权和。预过滤环境图需要按照不同的粗糙度级别做模糊每个mipmap对应一个粗糙度值。另外还需要一张BRDF积分查找表LUT把NdotV和roughness作为输入输出菲涅尔项的缩放和偏移。这张图是跟环境无关的可以预计算一次然后重复使用。在DX12里这三张图都是纹理资源需要通过描述符堆绑定到着色器。辐照度图和预过滤图用TextureCubeBRDF LUT用Texture2D。采样的时候要注意mipmap的选取预过滤图的mip级别跟粗糙度是线性对应的。3. DX12资源组织与描述符管理3.1 描述符堆的规划策略DX12里描述符堆Descriptor Heap是着色器访问资源的唯一入口跟DX11的槽位绑定完全不是一个概念。你可以把它理解成一个数组每个元素是一个描述符描述了某个资源在GPU内存中的位置和格式。着色器通过根签名里的描述符表来访问这些描述符根签名告诉驱动这个表在堆里的偏移量。对于PBR渲染我需要的描述符类型有CBV常量缓冲区视图用来传光照参数和变换矩阵SRV着色器资源视图用来传五张材质贴图和IBL的三张图还有采样器。采样器在DX12里也是通过描述符堆管理的这点跟DX11不一样DX11有独立的采样器状态对象。我的规划是创建两个描述符堆一个CBV_SRV_UAV堆容量设大一点比如1024因为后面可能还要加阴影贴图、后处理之类的另一个采样器堆容量设16就够了。CBV_SRV_UAV堆里我把前几个槽位固定分配给每帧变化的常量缓冲区然后是材质贴图最后是IBL贴图。采样器堆里放一个各向异性过滤的采样器给材质贴图用一个线性过滤的给IBL用一个点采样给BRDF LUT用。实操心得描述符堆的容量一旦创建就不能改所以宁可开大一点。我一开始设了256结果加到阴影贴图的时候就不够了只能重建整个堆所有描述符都要重新创建特别麻烦。3.2 根签名的设计与优化根签名Root Signature定义了着色器能访问哪些资源以及这些资源怎么绑定。它是DX12里最核心也最容易出错的部分。根签名有两种类型的参数根常量/根描述符直接存在根签名里访问速度快但数量有限描述符表存在描述符堆里通过根参数指向堆的某个位置。我的根签名设计是这样的根参数0是一个CBV直接存根描述符用来传每帧的变换矩阵和相机参数因为这部分数据每帧都变用根描述符最快。根参数1是一个描述符表包含一个CBV用来传光照参数和材质参数这部分数据每个draw call可能不同。根参数2是一个描述符表包含五个SRV对应反照率、法线、金属度、粗糙度、环境光遮蔽这五张材质贴图。根参数3是一个描述符表包含三个SRV对应辐照度图、预过滤图和BRDF LUT。根参数4是一个描述符表包含两个采样器一个给材质贴图一个给IBL。这里有个优化点根参数2和根参数3可以合并成一个描述符表因为它们在堆里是连续存放的。但分开写更清晰而且根签名的开销主要在根参数的数量上描述符表里的描述符数量不影响根签名的大小。实测下来这个根签名在大多数硬件上都能跑到很好的性能。// 根签名定义示例 CD3DX12_ROOT_PARAMETER rootParams[5]; rootParams[0].InitAsConstantBufferView(0, 0, D3D12_SHADER_VISIBILITY_ALL); CD3DX12_DESCRIPTOR_RANGE range1; range1.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 1); rootParams[1].InitAsDescriptorTable(1, range1); CD3DX12_DESCRIPTOR_RANGE range2; range2.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 5, 0); rootParams[2].InitAsDescriptorTable(1, range2); CD3DX12_DESCRIPTOR_RANGE range3; range3.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 3, 5); rootParams[3].InitAsDescriptorTable(1, range3); CD3DX12_DESCRIPTOR_RANGE range4; range4.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 2, 0); rootParams[4].InitAsDescriptorTable(1, range4);3.3 资源状态转换与屏障DX12里资源状态是要显式管理的从COPY_DEST到PIXEL_SHADER_RESOURCE从RENDER_TARGET到PRESENT每次转换都要插入资源屏障Resource Barrier。PBR渲染流程里材质贴图在初始化时从上传堆拷贝到默认堆然后转换到PIXEL_SHADER_RESOURCE状态之后就不再变了。IBL的几张图也是类似预计算完之后就固定在着色器可读状态。每帧的流程是先转换后备缓冲区到RENDER_TARGET渲染场景再转换到PRESENT。如果用了深度缓冲区还要在渲染前转换到DEPTH_WRITE渲染后转换到DEPTH_READ或者PIXEL_SHADER_RESOURCE如果后面要做SSAO或者别的后处理。注意资源屏障是DX12里最容易出bug的地方之一。状态转换写错了轻则画面不对重则设备移除。建议在开发阶段开启调试层它会帮你检查状态转换是否合法。我踩过的一个坑是在循环里重复转换同一个资源的状态其实只需要在循环外转换一次就行白白浪费了性能。4. 着色器实现与光照计算4.1 顶点着色器从模型空间到世界空间顶点着色器的任务很简单把顶点位置从模型空间变换到裁剪空间把法线、切线、纹理坐标传到像素着色器。但PBR对法线的精度要求比较高因为法线贴图是在切线空间里采样的需要构建TBN矩阵。我的做法是在顶点着色器里输出世界空间的位置、法线、切线和纹理坐标。TBN矩阵在像素着色器里构建因为法线贴图采样需要用到。切线空间到世界空间的变换矩阵是float3 N normalize(input.normal); float3 T normalize(input.tangent - N * dot(N, input.tangent)); float3 B cross(N, T) * input.tangent.w; float3x3 TBN float3x3(T, B, N);这里要注意切线的正交化因为从模型导入的切线可能跟法线不完全垂直。tangent.w用来处理镜像的UV保证副切线的方向正确。4.2 像素着色器PBR核心计算像素着色器是PBR的主战场。首先采样五张材质贴图float3 albedo albedoMap.Sample(samplerAniso, uv).rgb; albedo pow(albedo, 2.2); // sRGB转线性 float3 normal normalMap.Sample(samplerAniso, uv).rgb; normal normal * 2.0 - 1.0; normal normalize(mul(normal, TBN)); float metallic metallicMap.Sample(samplerAniso, uv).r; float roughness roughnessMap.Sample(samplerAniso, uv).r; float ao aoMap.Sample(samplerAniso, uv).r;然后计算F0和漫反射颜色float3 F0 lerp(float3(0.04, 0.04, 0.04), albedo, metallic); float3 diffuseColor albedo * (1.0 - metallic);接下来是直接光照循环。对于每个光源计算光线方向L、半角向量H、以及各种点积float3 L normalize(lightPos - worldPos); float3 V normalize(cameraPos - worldPos); float3 H normalize(L V); float NdotL max(dot(N, L), 0.0); float NdotV max(dot(N, V), 0.0); float NdotH max(dot(N, H), 0.0); float HdotV max(dot(H, V), 0.0);然后代入Cook-Torrance公式float D D_GGX(NdotH, roughness); float G G_Smith(NdotV, NdotL, roughness); float3 F F_Schlick(HdotV, F0); float3 specular (D * G * F) / (4.0 * NdotV * NdotL 0.001); float3 kS F; float3 kD (1.0 - kS) * (1.0 - metallic); float3 diffuse kD * diffuseColor / PI; float3 radiance lightColor * lightIntensity; float3 directLighting (diffuse specular) * radiance * NdotL;这里有几个细节要注意。分母加0.001是为了防止除零当NdotV或NdotL接近零的时候会出现数值问题。kD的计算里乘了(1-metallic)因为金属没有漫反射。diffuse除以PI是因为Lambert BRDF的定义。4.3 IBL部分环境光照的贡献IBL部分分漫反射和镜面反射两块。漫反射用辐照度图float3 irradiance irradianceMap.Sample(samplerLinear, N).rgb; float3 diffuseIBL irradiance * diffuseColor;镜面反射用预过滤图和BRDF LUTfloat3 R reflect(-V, N); float mipLevel roughness * (prefilteredMap.GetNumMips() - 1); float3 prefilteredColor prefilteredMap.SampleLevel(samplerLinear, R, mipLevel).rgb; float2 brdf brdfLUT.Sample(samplerLinear, float2(NdotV, roughness)).rg; float3 specularIBL prefilteredColor * (F0 * brdf.x brdf.y);最后把直接光照和IBL加起来再乘上环境光遮蔽float3 color (directLighting diffuseIBL specularIBL) * ao; color color / (color 1.0); // Reinhard色调映射 color pow(color, 1.0 / 2.2); // 线性转sRGB实操心得BRDF LUT的采样坐标是(NdotV, roughness)不是(NdotL, roughness)。我一开始写反了结果金属材质的高光形状完全不对调了半天才发现是这里的问题。另外预过滤图的mip级别跟粗糙度的对应关系要确认好有些工具生成的预过滤图mip级别不是线性的需要手动调整。5. 常见问题与排查实录5.1 画面全黑或全白这是最常见的问题原因可能有多种。首先检查描述符堆里的描述符有没有正确创建特别是SRV的格式和维度。TextureCube的SRV要指定ViewDimension为TEXTURECUBETexture2D就是TEXTURE2D。如果格式写错了采样出来的值可能是零或者随机值。其次检查根签名的绑定。根参数里的描述符表偏移量要跟描述符堆里的实际位置对应。我习惯在创建描述符堆的时候就规划好每个槽位的用途然后在设置根参数的时候用常量来索引避免硬编码数字。还有一个容易忽略的点是资源状态。如果贴图还在COPY_DEST状态就被采样了调试层会报错但如果不看输出窗口可能发现不了。建议在每次状态转换后都加一个断言或者日志。5.2 金属材质看起来像塑料这个问题通常是F0计算错了。非金属的F0是0.04金属的F0是反照率。如果金属度贴图没有正确采样或者lerp的参数写反了金属就会看起来像非金属。检查一下metallicMap的通道有些工具把金属度存在B通道有些存在R通道要跟美术确认好。另一个原因是IBL的预过滤图没有正确生成。如果预过滤图的所有mip级别都是同一张图镜面反射就没有粗糙度的变化看起来就像光滑的塑料。可以用RenderDoc抓一帧看看预过滤图的各个mip级别是不是有不同的模糊程度。5.3 性能突然下降DX12里性能问题往往跟描述符堆的切换有关。如果在渲染过程中频繁切换描述符堆驱动会插入额外的同步操作导致GPU空闲。我的做法是把所有材质贴图放在同一个描述符堆里渲染不同物体的时候只需要改变根参数的偏移量不需要切换堆。另一个性能陷阱是根签名的大小。根签名有64个DWORD的限制如果根参数太多驱动可能会把一些参数放到显存里访问速度会变慢。我的根签名用了5个根参数其中4个是描述符表1个是根CBV总共占用的DWORD数在限制之内。问题现象可能原因排查方法画面全黑描述符未创建或状态错误检查调试层输出用RenderDoc查看描述符堆金属像塑料F0计算错误或IBL缺失检查金属度贴图通道确认预过滤图mip高光形状异常BRDF LUT采样坐标错误确认采样坐标是(NdotV, roughness)性能下降频繁切换描述符堆合并描述符堆用偏移量索引设备移除资源状态转换非法开启调试层检查每个屏障5.4 调试工具的使用技巧RenderDoc是DX12调试的必备工具。它可以抓取一帧的完整渲染过程查看每个draw call的输入输出、描述符堆的内容、根签名的绑定情况。我习惯在渲染循环里加一个快捷键按下的时候触发抓帧这样可以在特定视角下抓取有问题的帧。PIX是微软官方的调试工具功能比RenderDoc更强大特别是对DX12的支持更好。它可以查看GPU时间线分析每个pass的耗时。不过PIX的界面比较复杂上手需要一点时间。注意使用调试工具的时候要确保开启了调试层Debug Layer否则很多错误不会被报告。在创建设备的时候加上D3D12_CREATE_DEVICE_DEBUG标志然后在输出窗口里查看警告和错误信息。6. 参数调优与效果验证6.1 粗糙度与金属度的美术规范PBR的效果很大程度上取决于美术制作的贴图是否规范。金属度贴图应该是二值的要么是0要么是1中间值只用在金属和非金属的过渡区域。粗糙度贴图的范围是0到10表示镜面1表示完全漫反射。反照率贴图应该是去光照的不能有高光或者阴影信息。我一般会给美术一个检查清单反照率贴图的亮度范围是否合理金属度贴图是否只有0和1粗糙度贴图的对比度是否足够法线贴图的强度是否合适。这些检查可以在Photoshop里用动作批量处理也可以在引擎里写个工具自动检测。6.2 光照强度的单位与校准PBR的光照强度是有物理单位的。平行光的强度单位是勒克斯lux点光源是坎德拉candela环境光也是勒克斯。但在实际项目里我们通常用一个相对值然后通过色调映射来调整最终亮度。我的做法是先把平行光强度设为1.0然后调整曝光值让画面亮度合适。点光源的强度跟距离的平方成反比所以需要根据场景尺度来设置。如果场景是以米为单位一个普通的室内点光源强度大概在10到100之间。6.3 色调映射与伽马校正PBR渲染出来的结果是线性空间的HDR值需要经过色调映射和伽马校正才能显示。色调映射我用的Reinhard简单而且效果不错。伽马校正是把线性值转成sRGB指数是1/2.2。需要注意的是反照率贴图在采样后要转成线性空间pow 2.2而最终输出要转回sRGB。如果中间有渲染到纹理的操作也要注意纹理的格式是sRGB还是线性的。DX12里创建纹理的时候可以指定格式为DXGI_FORMAT_R8G8B8A8_UNORM_SRGB这样采样的时候硬件会自动做sRGB到线性的转换。6.4 验证PBR效果的标准场景我一般用几个标准场景来验证PBR效果一个金属球阵列粗糙度从0到1渐变一个非金属球阵列同样粗糙度渐变一个混合材质的场景有金属有非金属。然后放一个平行光和一个环境贴图观察高光形状、菲涅尔效应、金属反射的颜色是否正确。如果金属球的高光在粗糙度低的时候是一个锐利的点粗糙度高的时候变成一片柔和的光晕说明GGX分布函数工作正常。如果金属球的边缘有明亮的菲涅尔反射说明Schlick近似正确。如果金属球的反射颜色跟反照率一致说明F0计算正确。实操心得验证的时候把环境贴图换成一张有明显特征的图比如带窗户的房间或者户外天空这样能直观地看到反射是否正确。如果反射是上下颠倒的说明立方体贴图的采样方向错了需要调整反射向量的Y分量。7. 从直接光照到IBL的完整管线7.1 预计算流程的实现IBL的三张图需要在渲染开始前预计算好。辐照度图可以用一个简单的卷积着色器对立方体贴图的每个面做半球积分。预过滤图需要对每个mip级别做不同粗糙度的模糊可以用重要性采样来加速。BRDF LUT可以用一个全屏quad渲染把NdotV和roughness作为输入输出菲涅尔项的缩放和偏移。在DX12里预计算可以用计算着色器来做也可以用像素着色器渲染到纹理。我用的方法是创建一个离屏的渲染目标把立方体贴图的每个面依次渲染上去。预过滤图的每个mip级别需要单独渲染采样的时候用GGX的重要性采样。// 预过滤着色器的重要性采样 float3 ImportanceSampleGGX(float2 Xi, float3 N, float roughness) { float a roughness * roughness; float phi 2.0 * PI * Xi.x; float cosTheta sqrt((1.0 - Xi.y) / (1.0 (a * a - 1.0) * Xi.y)); float sinTheta sqrt(1.0 - cosTheta * cosTheta); float3 H; H.x sinTheta * cos(phi); H.y sinTheta * sin(phi); H.z cosTheta; float3 up abs(N.z) 0.999 ? float3(0, 0, 1) : float3(1, 0, 0); float3 tangentX normalize(cross(up, N)); float3 tangentY cross(N, tangentX); return normalize(tangentX * H.x tangentY * H.y N * H.z); }7.2 运行时IBL的绑定与采样运行时IBL的绑定跟材质贴图类似也是通过描述符表。辐照度图和预过滤图是TextureCubeBRDF LUT是Texture2D。采样的时候要注意mip级别的选择预过滤图的mip级别跟粗糙度是线性对应的但有些工具生成的预过滤图mip级别不是线性的需要手动调整。float mipLevel roughness * (prefilteredMap.GetNumMips() - 1); float3 prefilteredColor prefilteredMap.SampleLevel(samplerLinear, R, mipLevel).rgb;这里用SampleLevel而不是Sample因为我们需要显式指定mip级别。如果用了Sample硬件会根据屏幕空间的导数自动选择mip但IBL的反射向量跟屏幕空间没有直接关系自动选择的mip级别是不对的。7.3 性能优化与质量平衡IBL的预计算可以在加载时做一次运行时只需要采样。但如果场景的环境贴图会动态变化比如昼夜交替就需要实时预计算这个开销就比较大了。我的做法是预计算几张不同时间的环境贴图运行时根据时间插值。采样的性能开销主要在纹理采样上。辐照度图和预过滤图都是立方体贴图采样开销比2D纹理大。如果性能吃紧可以降低预过滤图的分辨率或者减少mip级别。BRDF LUT的分辨率可以设小一点256x256就足够了因为它是低频数据。注意预过滤图的mip级别数量要跟粗糙度的范围对应。如果粗糙度范围是0到1mip级别数量是8那么每个mip对应的粗糙度是0, 0.125, 0.25, ..., 1.0。如果mip级别数量是5对应的粗糙度就是0, 0.25, 0.5, 0.75, 1.0。这个对应关系要在着色器里保持一致。8. 工程化实践与后续扩展8.1 材质系统的设计在实际项目里材质不会只有一种。不同的物体可能有不同的贴图组合有些有法线贴图有些没有有些用不同的采样器。我的做法是设计一个材质系统每个材质包含一组贴图的索引和采样器的索引渲染的时候根据材质的索引去描述符堆里取对应的描述符。材质数据可以存在一个结构化缓冲区里每个材质一个结构体包含反照率、金属度、粗糙度的乘数因子以及各个贴图在描述符堆里的偏移量。渲染的时候把材质的索引传到着色器里着色器根据索引去缓冲区里取数据。8.2 多光源与阴影直接光照部分可以扩展成多光源。把光源数据存在一个结构化缓冲区里每个光源包含位置、颜色、强度、类型等参数。像素着色器里循环遍历所有光源累加每个光源的贡献。光源数量多的时候可以考虑用计算着色器做聚类把光源分配到屏幕空间的tile里减少每个像素需要计算的光源数量。阴影部分可以用阴影贴图。平行光用正交投影渲染深度图点光源用立方体贴图渲染深度。采样的时候用PCF做软阴影。阴影贴图在DX12里也是纹理资源需要额外的描述符和根参数。8.3 后处理与色调映射PBR渲染出来的HDR结果需要经过后处理才能显示。常见的后处理包括泛光Bloom、色调映射、颜色分级、抗锯齿等。泛光可以用降采样和升采样来做色调映射可以用ACES或者Filmic。这些后处理在DX12里可以用计算着色器实现也可以用像素着色器渲染到全屏quad。后处理的输入输出都是纹理需要在描述符堆里分配额外的槽位。如果后处理链比较长可以考虑用多个渲染目标来回切换或者用UAV做读写。8.4 跨平台与不同硬件的适配DX12在不同硬件上的表现可能有差异。NVIDIA和AMD的驱动对根签名的优化策略不同有些根签名在NVIDIA上跑得快在AMD上可能就慢。我的经验是尽量保持根签名简单根参数数量控制在4到6个描述符表里的描述符数量不要太多。另外要注意的是不同硬件对纹理格式的支持可能不同。BC压缩格式在大多数硬件上都支持但有些移动端硬件可能不支持。如果要做跨平台最好在运行时检查硬件能力选择合适的格式。实操心得在开发阶段可以准备几套不同的根签名在运行时根据硬件厂商选择最优的那套。但这个优化只有在性能瓶颈确实在根签名上的时候才值得做大多数情况下瓶颈在像素着色器的计算或者纹理采样上。9. 我个人在实际操作中的体会PBR这套东西从原理到实现我前前后后折腾了大概两个月。最开始看理论的时候觉得挺清楚的不就是几个公式吗但真正写到代码里才发现到处都是坑。最大的体会是DX12的资源管理比DX11复杂太多了但一旦把描述符堆和根签名规划好后面的扩展其实很顺畅。我踩过的最大的坑是描述符堆的容量规划。一开始只开了256个槽位觉得肯定够用了结果加到阴影贴图和后处理的时候就不够了。重建描述符堆意味着所有描述符都要重新创建所有根参数都要重新绑定调试了大半天才搞定。所以我的建议是描述符堆的容量宁可开大一点1024或者2048都不嫌多反正描述符本身占不了多少显存。另一个体会是调试工具的重要性。RenderDoc和PIX在DX12开发里是必不可少的特别是PIX的GPU时间线能帮你快速定位性能瓶颈。我习惯在每次加新功能之后都抓一帧看看确认没有资源状态错误和性能回退。最后分享一个小技巧如果画面效果跟预期不符先把所有贴图换成纯色把光照简化成只有一个平行光然后逐步加回复杂度和贴图。这样能快速定位是哪个环节出了问题。我调BRDF LUT的时候就是这么干的把LUT换成纯白确认其他部分没问题之后再单独调LUT的采样坐标很快就找到了问题所在。这个项目后续还可以继续扩展比如加入阴影、多光源、后处理、材质系统这些。每一步都会遇到新的DX12特性比如阴影需要额外的渲染目标和深度纹理多光源需要结构化缓冲区后处理需要UAV和计算着色器。但有了PBR这个基础后面的扩展都是在这个框架上添砖加瓦不会像第一次搭框架那么痛苦了。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

基于RetinaFace+ArcFace+FAISS的人脸识别会议签到系统实战 2026/10/1 4:25:06

基于RetinaFace+ArcFace+FAISS的人脸识别会议签到系统实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
iShot Pro 深度指南:macOS 高效截图与系统级信任配置 2026/10/1 4:25:06

iShot Pro 深度指南:macOS 高效截图与系统级信任配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Android Profiler实战:CPU与内存监控定位卡顿和泄漏 2026/10/1 4:25:06

Android Profiler实战:CPU与内存监控定位卡顿和泄漏

做Android开发的人,大概都经历过这种场景:线上反馈说App卡顿、内存嗖嗖涨、甚至直接OOM崩溃,但你本地怎么跑都一切正常。翻Logcat日志,全是无关紧要的Warning,真正的性能问题像泥鳅一样滑不留手。这时候,An…

阅读更多 →
马德拉酒入门指南:从马德拉化工艺到选酒品鉴全解析 2026/10/1 4:24:59

马德拉酒入门指南:从马德拉化工艺到选酒品鉴全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
彻底搞懂二分查找:check函数与边界条件详解 2026/10/1 4:24:59

彻底搞懂二分查找:check函数与边界条件详解

二分查找这名字,凡是写代码的没有不知道的。但就这么个基础算法,年年面试、年年刷题、年年有同事在边界条件上写崩。我自己带过好几个实习生,让他们写一个“在有序数组里找第一个大于等于 target 的下标”,交上来的版本五花八门&a…

阅读更多 →
YOLOv8象棋棋子识别实战:小目标检测与真实场景畸变处理 2026/10/1 4:24:53

YOLOv8象棋棋子识别实战:小目标检测与真实场景畸变处理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉