zoukankan      html  css  js  c++  java
  • 用DirectX12实现Blinn Phong

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

    float3 SchlickFresnel(float3 r0, float3 normal, float3 lightVec)
    {
    	float nDotL = max(0, dot(normal, lightVec));
    	float3 reflectPercent = r0 + (1 - r0) * pow(1 - nDotL, 5);
    
    	return reflectPercent;
    }
    
    float4 BlinnPhong(float3 lightStrength, float3 lightVec, float3 normal, float3 toEye, Material mat)
    {
    	float4 ambient = gAmbientLight * mat.diffuseAlbedo;
    	float3 halfVec = normalize(lightVec + toEye);
    	float roughness = (mat.shininess + 8) * pow(max(0, dot(halfVec, normal)), mat.shininess) / 8.0f;
    	float3 fresnel = SchlickFresnel(mat.fresnelR0, halfVec, lightVec);
    	float3 specularAlbedo = fresnel * roughness;
    	float3 litColor = ambient.rgb + (mat.diffuseAlbedo.rgb + specularAlbedo) * lightStrength;
        
    	return float4(litColor.rgb, diffuseAlbedo.a);
    }
    

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

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

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

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

    normal = halfVec = normalize(lightVec + toEye);
    

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

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

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

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

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

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

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

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

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

    	float3 dir = light.position - pos;
    	float dist = length(dir);
    	if (dist >= light.falloffEnd)
    	{
    		float3 lightStrength = 0.0f;
    	}
    	else
    	{
    		dir /= dist;
    
    		float nDotL = max(0, dot(dir, normal));
    		float falloff = saturate((light.falloffEnd - dist) / (light.falloffEnd - light.falloffStart));
    		float3 lightStrength = light.strength * nDotL * falloff;
    	}
    

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

    	float3 dir = light.position - pos;
    	float dist = length(dir);
    	if (dist >= light.falloffEnd)
    	{
    		float3 lightStrength = 0.0f;
    	}
    	else
    	{
    		dir /= dist;
    		float nDotL = max(0, dot(dir, normal));
    		float falloff = saturate((light.falloffEnd - dist) / (light.falloffEnd - light.falloffStart));
    		float spotFactor = pow(max(0, dot(-dir, light.direction)), light.spotPower);
    		float3 lightStrength = light.strength * nDotL * falloff * spotFactor;
    	}
    

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

    cbuffer cbPerObject : register(b0)
    {
    	float4x4 gWorld;
    	float4x4 gInvWorld;
    	float4x4 gWorldViewProj; 
    };
    
    cbuffer cbPerPass : register(b1)
    {
    	float3 gEyePosW;
    	int gLightCount;
    	float4 gAmbientLight;
    	Light gLights[16];
    };
    
    cbuffer cbPerMaterial : register(b2)
    {
    	float4 diffuseAlbedo;
    	float3 fresnelR0;
    	float shininess;
    };
    

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

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

    		ThrowIfFailed(mDevice->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
    			D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(mObjectConstBufferCount * objCbSize),
    			D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&res.mObjectConstBuffer)));
    		ThrowIfFailed(mDevice->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
    			D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(mPassConstBufferCount * passCbSize),
    			D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&res.mPassConstBuffer)));
    		ThrowIfFailed(mDevice->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
    			D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(mMaterialConstBufferCount * matCbSize),
    			D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&res.mMaterialConstBuffer)));
    

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

    	UINT cbvHeapIndex = 0;
    	for (UINT i = 0; i < mCoreResourceCount; i++)
    	{
    		EngineCoreResource res = mCoreResource[i];
    		D3D12_GPU_VIRTUAL_ADDRESS objCbAddr = res.mObjectConstBuffer->GetGPUVirtualAddress();
    		for (UINT j = 0; j < mObjectConstBufferCount; j++)
    		{
    			D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
    			cbvDesc.BufferLocation = objCbAddr + j * objCbSize;
    			cbvDesc.SizeInBytes = objCbSize;
    			CD3DX12_CPU_DESCRIPTOR_HANDLE handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(
    				mCbvHeap->GetCPUDescriptorHandleForHeapStart());
    			handle.Offset(cbvHeapIndex, mCbvHeapIncSize);
    			mDevice->CreateConstantBufferView(&cbvDesc, handle);
    			cbvHeapIndex++;
    		}
    
    		D3D12_GPU_VIRTUAL_ADDRESS passCbAddr = res.mPassConstBuffer->GetGPUVirtualAddress();
    		for (UINT j = 0; j < mPassConstBufferCount; j++)
    		{
    			D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
    			cbvDesc.BufferLocation = passCbAddr + j * passCbSize;
    			cbvDesc.SizeInBytes = passCbSize;
    			CD3DX12_CPU_DESCRIPTOR_HANDLE handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(
    				mCbvHeap->GetCPUDescriptorHandleForHeapStart());
    			handle.Offset(cbvHeapIndex, mCbvHeapIncSize);
    			mDevice->CreateConstantBufferView(&cbvDesc, handle);
    			cbvHeapIndex++;
    		}
    
    		D3D12_GPU_VIRTUAL_ADDRESS matCbAddr = res.mMaterialConstBuffer->GetGPUVirtualAddress();
    		for (UINT j = 0; j < mMaterialConstBufferCount; j++)
    		{
    			D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
    			cbvDesc.BufferLocation = matCbAddr + j * matCbSize;
    			cbvDesc.SizeInBytes = matCbSize;
    			CD3DX12_CPU_DESCRIPTOR_HANDLE handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(
    				mCbvHeap->GetCPUDescriptorHandleForHeapStart());
    			handle.Offset(cbvHeapIndex, mCbvHeapIncSize);
    			mDevice->CreateConstantBufferView(&cbvDesc, handle);
    			cbvHeapIndex++;
    		}
    	}
    

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

    	CD3DX12_DESCRIPTOR_RANGE cbvTable[3];
    	cbvTable[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);
    	cbvTable[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 1);
    	cbvTable[2].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 2);
    
    	CD3DX12_ROOT_PARAMETER rootParams[3];
    	rootParams[0].InitAsDescriptorTable(1, &cbvTable[0]);
    	rootParams[1].InitAsDescriptorTable(1, &cbvTable[1]);
    	rootParams[2].InitAsDescriptorTable(1, &cbvTable[2]);
    
    	CD3DX12_ROOT_SIGNATURE_DESC sigDesc(3, rootParams, 0, nullptr, 
    		D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
    

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

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

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

  • 相关阅读:
    MySQL创建用户并修改权限
    Jenkins触发项目构建
    dotnet-cnblogs-tool使用与坑
    Jenkins集成Jmeter接口测试(Freestyle Project)
    Jenkins发送邮件没有解析变量
    JMeter + Maven in Jenkins
    Charles&Fiddler 对手机捉包失败原因分析
    Postman 脚本
    Selenium+Chrome浏览器自动加载Flash
    idea上进行远程调试项目步骤纪录
  • 原文地址:https://www.cnblogs.com/back-to-the-past/p/14106023.html
Copyright © 2011-2022 走看看