这次我们来用DirectX12实现一下基本的Blinn Phong光照模型。让我们再把这个光照模型的概念过一遍:一个物体的颜色由三个因素决定:ambient, diffuse, specular。ambient表示场景中的其他物体反射出来的光线到该物体所呈现的颜色,与摄像机所在的位置无关;diffuse表示物体内部所吸收的光线反射出来所呈现的颜色,它是完全杂乱无章而且随机的,可以假设散射到任意方向的分量都是相等的,因此也与摄像机所在的位置无关;specular表示物体根据Fresnel效应,入射光线镜面反射出去所呈现的颜色:

  1. float3 SchlickFresnel(float3 r0, float3 normal, float3 lightVec)
  2. {
  3. float nDotL = max(0, dot(normal, lightVec));
  4. float3 reflectPercent = r0 + (1 - r0) * pow(1 - nDotL, 5);
  5. return reflectPercent;
  6. }
  7. float4 BlinnPhong(float3 lightStrength, float3 lightVec, float3 normal, float3 toEye, Material mat)
  8. {
  9. float4 ambient = gAmbientLight * mat.diffuseAlbedo;
  10. float3 halfVec = normalize(lightVec + toEye);
  11. float roughness = (mat.shininess + 8) * pow(max(0, dot(halfVec, normal)), mat.shininess) / 8.0f;
  12. float3 fresnel = SchlickFresnel(mat.fresnelR0, halfVec, lightVec);
  13. float3 specularAlbedo = fresnel * roughness;
  14. float3 litColor = ambient.rgb + (mat.diffuseAlbedo.rgb + specularAlbedo) * lightStrength;
  15. return float4(litColor.rgb, diffuseAlbedo.a);
  16. }

让我们从shader代码一行行地看过去。

首先ambient分量的计算比较好理解,就是我们假定场景中其他环境散射过来的光照强度为gAmbientLight,而mat是物体的材质属性,diffuseAlbedo表示该材质对不同颜色光的rgb分量的反射率。

再看diffuse分量的计算,也很简单,lightStrength表示入射光的光照强度,具体这个光照强度怎么计算的,我们先放到后面说,也很容易理解材质的``diffuseAlbedo属性乘以入射光的光照得到的就是diffuse`分量。

最后来看下specular分量的计算,这个相对比较复杂,我们首先假设物体表面其实是凹凸不平的,是由若干个微表面所组成。而只有镜面反射向量恰好为视线所在方向的微表面,才会对specular分量做出贡献。也即这些微表面的法向量都满足:

  1. normal = halfVec = normalize(lightVec + toEye);

那么,这样的微表面有多少呢?有一点我们是知道的,这些微表面中,与物体表面法线偏移程度越小的,可能性越大;偏离越远的,可能性越小。自然而然,我们想到可以用cos三角函数来衡量两个向量的临近程度。

另外,我们在材质上引入了shininess这个概念,它表示物体表面的光滑程度,物体越光滑,微表面法线集中分布在接近物体表面法线的地方上,这样看上去高光会比较集中锐利;物体越粗糙,微表面法线会相对均匀分布在不同临近程度上,这样看上去高光会形成一块光斑。通过以上两点,我们得到:

  1. float roughness = (mat.shininess + 8) * pow(max(0, dot(halfVec, normal)), mat.shininess) / 8.0f;

还有一点别忘了,不是所有的入射光都参与这个镜面反射高光计算,我们根据Fresnel效应计算得到参与镜面反射的光照:

  1. float3 fresnel = SchlickFresnel(mat.fresnelR0, halfVec, lightVec);
  2. float3 specularAlbedo = fresnel * roughness;

最后,我们回过头来说下lightStrength这个光照强度的计算。在Blinn Phong光照模型中,入射的光有三种类型,directional,point,spot。下面分别讨论不同类型的光照强度计算:

directional即平行光,从无穷远处的光源发射出来,光照强度不会随着距离衰减。容易知道,当平行光的方向与物体表面垂直时,物体接受到的光照强度是最大的;而当平行光方向与物体表面的法线的夹角为\(\theta\)时,相同密度下的光照要覆盖\(1/cos\theta\)的物体表面。所以,平行光的光照强度为:

  1. float3 dir = -light.direction;
  2. float nDotL = max(0, dot(dir, normal));
  3. float3 lightStrength = light.strength * nDotL;

point即为点光源,光源有一个具体的位置,朝任意方向发射光线,而且光照强度会随着距离不断衰减,这里衰减我们简化采用线性衰减的方式:

  1. float3 dir = light.position - pos;
  2. float dist = length(dir);
  3. if (dist >= light.falloffEnd)
  4. {
  5. float3 lightStrength = 0.0f;
  6. }
  7. else
  8. {
  9. dir /= dist;
  10. float nDotL = max(0, dot(dir, normal));
  11. float falloff = saturate((light.falloffEnd - dist) / (light.falloffEnd - light.falloffStart));
  12. float3 lightStrength = light.strength * nDotL * falloff;
  13. }

spot即为聚光灯,光源有一个具体的位置,并且只向某个具体的方向发射光线。在这个方向的一定范围内的物体才能接受到光照,光照强度也会随着距离不断衰减。类似specular分量的计算,我们也可以用相同的方式对不同光照方向的光照强度进行建模:

  1. float3 dir = light.position - pos;
  2. float dist = length(dir);
  3. if (dist >= light.falloffEnd)
  4. {
  5. float3 lightStrength = 0.0f;
  6. }
  7. else
  8. {
  9. dir /= dist;
  10. float nDotL = max(0, dot(dir, normal));
  11. float falloff = saturate((light.falloffEnd - dist) / (light.falloffEnd - light.falloffStart));
  12. float spotFactor = pow(max(0, dot(-dir, light.direction)), light.spotPower);
  13. float3 lightStrength = light.strength * nDotL * falloff * spotFactor;
  14. }

至此,我们shader部分算是构建完成了。我们还需要在DirectX12中把hlsl中所用的全局变量通过const buffer传递过去,我们先在hlsl部分中定义所需的全局变量:

  1. cbuffer cbPerObject : register(b0)
  2. {
  3. float4x4 gWorld;
  4. float4x4 gInvWorld;
  5. float4x4 gWorldViewProj;
  6. };
  7. cbuffer cbPerPass : register(b1)
  8. {
  9. float3 gEyePosW;
  10. int gLightCount;
  11. float4 gAmbientLight;
  12. Light gLights[16];
  13. };
  14. cbuffer cbPerMaterial : register(b2)
  15. {
  16. float4 diffuseAlbedo;
  17. float3 fresnelR0;
  18. float shininess;
  19. };

之所以定义3个const buffer,是因为不同buffer的更新频率不一样,有的是每个pass就需要更新,有的是只有某个object发生变化才需要更新,有的是只有某个material发生变化才需要更新。需要注意的是,cbuffer中变量定义的顺序至关重要。为了保证4字节对齐,我们需要调整变量定义的顺序,避免让类似一个float3,float4横跨两个4字节的情况出现,而发生一些不可预料的错误。

回到DirectX12,我们首先需要创建3个const buffer:

  1. ThrowIfFailed(mDevice->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
  2. D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(mObjectConstBufferCount * objCbSize),
  3. D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&res.mObjectConstBuffer)));
  4. ThrowIfFailed(mDevice->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
  5. D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(mPassConstBufferCount * passCbSize),
  6. D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&res.mPassConstBuffer)));
  7. ThrowIfFailed(mDevice->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
  8. D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(mMaterialConstBufferCount * matCbSize),
  9. D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&res.mMaterialConstBuffer)));

接下来,我们要创建const buffer view,将buffer资源绑定到heap上:

  1. UINT cbvHeapIndex = 0;
  2. for (UINT i = 0; i < mCoreResourceCount; i++)
  3. {
  4. EngineCoreResource res = mCoreResource[i];
  5. D3D12_GPU_VIRTUAL_ADDRESS objCbAddr = res.mObjectConstBuffer->GetGPUVirtualAddress();
  6. for (UINT j = 0; j < mObjectConstBufferCount; j++)
  7. {
  8. D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
  9. cbvDesc.BufferLocation = objCbAddr + j * objCbSize;
  10. cbvDesc.SizeInBytes = objCbSize;
  11. CD3DX12_CPU_DESCRIPTOR_HANDLE handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(
  12. mCbvHeap->GetCPUDescriptorHandleForHeapStart());
  13. handle.Offset(cbvHeapIndex, mCbvHeapIncSize);
  14. mDevice->CreateConstantBufferView(&cbvDesc, handle);
  15. cbvHeapIndex++;
  16. }
  17. D3D12_GPU_VIRTUAL_ADDRESS passCbAddr = res.mPassConstBuffer->GetGPUVirtualAddress();
  18. for (UINT j = 0; j < mPassConstBufferCount; j++)
  19. {
  20. D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
  21. cbvDesc.BufferLocation = passCbAddr + j * passCbSize;
  22. cbvDesc.SizeInBytes = passCbSize;
  23. CD3DX12_CPU_DESCRIPTOR_HANDLE handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(
  24. mCbvHeap->GetCPUDescriptorHandleForHeapStart());
  25. handle.Offset(cbvHeapIndex, mCbvHeapIncSize);
  26. mDevice->CreateConstantBufferView(&cbvDesc, handle);
  27. cbvHeapIndex++;
  28. }
  29. D3D12_GPU_VIRTUAL_ADDRESS matCbAddr = res.mMaterialConstBuffer->GetGPUVirtualAddress();
  30. for (UINT j = 0; j < mMaterialConstBufferCount; j++)
  31. {
  32. D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
  33. cbvDesc.BufferLocation = matCbAddr + j * matCbSize;
  34. cbvDesc.SizeInBytes = matCbSize;
  35. CD3DX12_CPU_DESCRIPTOR_HANDLE handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(
  36. mCbvHeap->GetCPUDescriptorHandleForHeapStart());
  37. handle.Offset(cbvHeapIndex, mCbvHeapIncSize);
  38. mDevice->CreateConstantBufferView(&cbvDesc, handle);
  39. cbvHeapIndex++;
  40. }
  41. }

然后,我们需要创建根签名,用来指明hlsl需要3个const buffer,分别使用寄存器b0,b1,b2存放数据:

  1. CD3DX12_DESCRIPTOR_RANGE cbvTable[3];
  2. cbvTable[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);
  3. cbvTable[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 1);
  4. cbvTable[2].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 2);
  5. CD3DX12_ROOT_PARAMETER rootParams[3];
  6. rootParams[0].InitAsDescriptorTable(1, &cbvTable[0]);
  7. rootParams[1].InitAsDescriptorTable(1, &cbvTable[1]);
  8. rootParams[2].InitAsDescriptorTable(1, &cbvTable[2]);
  9. CD3DX12_ROOT_SIGNATURE_DESC sigDesc(3, rootParams, 0, nullptr,
  10. D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

这里我们用了3个根参数,是因为我们需要根据不同时机和不同的调用频率,调用SetGraphicsRootDescriptorTable设置不同的buffer绑定到GPU上,用3个根参数就比较方便灵活。拷贝const buffer数据到GPU层的代码就不贴了,这个和之前的操作方式基本一致,只要记住只在必要的时间拷贝必要的数据即可。

让我们看一下最终的效果,这里加载了一个简单的汽车模型,用了3个平行光源:

如果你觉得我的文章有帮助,欢迎关注我的微信公众号(大龄社畜的游戏开发之路-

用DirectX12实现Blinn Phong的更多相关文章

  1. KlayGE 4.4中渲染的改进(四):SSSSS

    转载请注明出处为KlayGE游戏引擎,本文的永久链接为http://www.klayge.org/?p=2774 本系列的上一篇提到了KlayGE 4.4将会出现的高质量地形渲染.本篇仍讲一个新功能, ...

  2. Opengles 管线编程介绍

      OpenGL ES 2.0可编程管道 上图橙色部分(Vertex Shader和Fragment Shader)为此管道的可编程部分.整个管道包含以下两个规范: 1)         OpenGL ...

  3. 合金装备V 幻痛 制作技术特辑

    合金装备V:幻痛 制作特辑 资料原文出自日版CGWORLD2015年10月号   在[合金装备4(Metal Gear Solid IV)]7年后,序章作品[合金装备5 :原爆点 (Metal Gea ...

  4. 【Aladdin Unity3D Shader编程】之三 光照模型(二)

    高光反射模型 Specular=直射光*pow(cosθ,高光的参数) θ:是反射光和视野方向的夹角 编写高光反射Shader Shader "AladdinShader/07 Specul ...

  5. 【Unity Shader】(六) ------ 复杂的光照(上)

    笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题.              [Unity Sha ...

  6. 三种光照模型的shader实现

    1.Lambert模型,公式为I=Kd*Il(N*L): Shader "Custom/Lambert_A" { Properties { _Diffuse(,,,) } SubS ...

  7. 【温故知新】——BABYLON.js学习之路·前辈经验(二)

    前言:在上一篇随笔BABYLON.js学习之路·前辈经验(一)中回顾了组内同事们长时间在Babylon开发实践中的总结出的学习之路和经验,这一篇主要对开发中常见的一些功能点做一个梳理,这里只作为温故知 ...

  8. Unity 图形学 基础知识总结

    1. 渲染流水线     三大块:应用阶段,几何阶段,光栅化阶段                       渲染图元   顶点信息    GPU流水线     顶点数据=>     顶点着色器 ...

  9. 腾讯暑期 前后七面 + hr(已拿offer面经)

    以下是时间线 魔方 魔术师工作室 3.19 一面(120mins) c++ struct和union区别? 指针和引用的区别? 左值和右值? 字节对齐的作用? 什么情况下需要自定义new? mallo ...

随机推荐

  1. 【JAVA基础】数组练习案例一

    /* * * 输入5个学生成绩 * 计算出每个成绩与最高分的差距 * 根据差距分配等级 * * */ import java.util.Scanner; public class ArrayTask ...

  2. 如何用pdfFactory新建打印机并设置属性

    今天我们来讲一讲,在pdfFactory中如何去修改PDF文件打印页面的页边距.页面大小.页面清晰度等属性参数. pdfFactory是一款Windows平台上的虚拟打印机,在没有打印机可以安装的情况 ...

  3. 聊聊 elasticsearch 之分词器配置 (IK+pinyin)

    系统:windows 10 elasticsearch版本:5.6.9 es分词的选择 使用es是考虑服务的性能调优,通过读写分离的方式降低频繁访问数据库的压力,至于分词的选择考虑主要是根据目前比较流 ...

  4. 年轻人不讲武德,竟然重构出这么优雅后台 API 接口

    Hello,早上好,我是楼下小黑哥~ 最近偶然间在看到 Spring 官方文档的时候,新学到一个注解 @ControllerAdvice,并且成功使用这个注解重构我们项目的对外 API 接口,去除繁琐 ...

  5. JavaSE 学习笔记01丨开发前言与环境搭建、基础语法

    本蒟蒻学习过C/C++的语法,故在学习Java的过程中,会关注于C++与Java的区别.开发前言部分,看了苏星河教程中的操作步骤.而后,主要阅读了<Java核心技术 卷1 基础知识>(第8 ...

  6. for循环与while循环

    1.两中循环的语法结构 for循环结构: for(表达式1;表达式2;表达式3) { 执行语句; } while循环结构: while(表达式1) { 执行语句; } 2.两者区别: 应用场景:由于f ...

  7. 汇编中的inc和dec

    原文链接:https://www.cnblogs.com/whzym111/p/6370198.htmlinc 加1指令 dec 减1指令 一.加一指令inc inc a 相当于 add a,1 // ...

  8. 老猿学5G专栏完结说明

    老猿学5G是因为工作原因促成的,主要目的是为了研究5G的计费架构相关内容,到今天为止,基本上达成目标,因此这个专栏基本上告一段落了. 回想这2个多月的日子,从一个对5G相关知识完全不熟悉的小白,到现在 ...

  9. Python中使用f字符串进行字符串格式化的方法

    在<第3.10节 Python强大的字符串格式化新功能:使用format字符串格式化>介绍了使用format进行字符串格式化的方法,在Python 3.6中,如果格式化字符串中的关键字参数 ...

  10. 第11.11节 Python正则表达式的指定重复次数匹配模式及元字符”{}”功能介绍

    在<第11.8节 Pytho正则表达式的重复匹配模式及元字符"?". "". "+"功能介绍>和<第11.10节 Pyth ...