补充:过滤器(filter)相关知识
1)使用情景:图片的放大和缩小
i.图片在经过放大操作后,除了图片原有的像素被拉伸,还需要对其余空缺的像素位置选用合适的方式来进行填充。比如一个2x2位图被拉伸成8x8的,除了角上4个像素,还需要对其余60个像素进行填充。
ii.图片在经过缩小操作后,需要抛弃掉一些像素。但显然每次绘制都按实际宽高来进行缩小会对性能有很大影响。 在d3d中可以使用mipmapping技术,以额外牺牲一些内存代价的方式来获得高效的拟合效果。
2)图片放大后的处理方法
i.常量插值法
对于2x2位图,它的宽高表示范围都为[0,1]
,而8x8位图的都为[0,7]
,且只允许取整数。那么对于放大后的像素点(1, 4)
就会映射到(1/7, 4/7)
上。
常量插值法的做法十分简单粗暴,就是对X和Y值都进行四舍五入操作,然后取邻近像素点的颜色。比如对于映射后的值如果落在[20.5, 21.5)
的范围,最终都会取21。根据上面的例子,最终会落入到像素点(0, 1)
上,然后取该像素点的颜色。
这其实就是过滤器中的箱式过滤器(box filter)的应用
ii.1线性插值法
现在只讨论一维情况,已知第20个像素点的颜色p0
和第21个像素点的颜色p1
,并且经过拉伸放大后,有一个像素点落在范围(20, 21)
之间,我们就可以使用线性插值法求出最终的颜色(t取(0,1)
):
p
=
t
p
1
+
(
1
−
t
)
p
0
\mathbf{p}=t\mathbf{p_1}+(1-t)\mathbf{p_0}
p=tp1+(1−t)p0
对于二维情况,会有三种使用线性插值法的情况:
- X方向使用常量插值法,Y方向使用线性插值法
- X方向使用线性插值法,Y方向使用常量插值法
- X和Y方向均使用线性插值法
下图展示了双线性插值法的过程,已知4个相邻像素点,当前采样的纹理坐标在这四个点内,则首先根据x方向的纹理坐标进行线性插值,然后根据y方向的纹理坐标再进行一遍线性插值:
这其实就是帐篷过滤器(Tent Filter)的应用
ii.2两种插值方法的对比(左为常量插值,右为双线性插值)
可见常量插值锯齿感明显,而双线性插值模糊感明显
3)图片缩小后的处理
这里估计使用的是金字塔下采样的原理。一张256x256的纹理,通过不断的向下采样,可以获得256x256、128x128、64x64…一直到1x1的一系列位图,这些位图构建了一条mipmap链,并且不同的纹理标注有不同的mipmap等级
其中mipmap等级为0的纹理即为原来的纹理,等级为1的纹理所占内存为等级为0的1/4,等级为2的纹理所占内存为等级为1的1/4…以此类推我们可以知道包含完整mipmap的纹理占用的内存空间不超过原来纹理的
lim
n
→
+
∞
=
1
(
1
−
(
1
4
)
n
)
1
−
1
4
=
4
3
\lim_{{n}\rightarrow{+\infty}}=\frac{1(1-(\frac{1}{4})^n)}{1-\frac{1}{4}}=\frac{4}{3}
n→+∞lim=1−411(1−(41)n)=34
各级mipmap如图所示:
接下来会有两种情况:
- 选取mipmap等级对应图片和缩小后的图片大小最接近的一张,然后进行线性插值法或者常量插值法,这种方式叫做点过滤(point filtering)
- 选取两张mipmap等级相邻的图片,使得缩小后的图片大小在那两张位图之间,然后对这两张位图进行常量插值法或者线性插值法分别取得颜色结果,最后对两个颜色结果进行线性插值,这种方式叫做线性过滤(linear filtering)。
4)各向异性过滤
Anisotropic Filtering可以帮助我们处理那些不与屏幕平行的平面,需要额外使用平面的法向量和摄像机的观察方向向量。虽然使用该种过滤器会有比较大的性能损耗,但是能诞生出比较理想的效果。
下面左图使用了线性过滤法,右边使用的是各向异性过滤,可以看到顶面纹理比左边的更加清晰
一.进行纹理采样的步骤概括
在DirectX 11中,纹理采样可以通过以下步骤来实现:
- 创建一个纹理对象:使用D3D11_TEXTURE2D_DESC结构体描述纹理的属性,包括宽度、高度、格式、MIP级别等。
- 创建一个纹理采样器对象:使用D3D11_SAMPLER_DESC结构体描述采样器的属性,包括过滤模式、边界模式、MIP级别等。
- 将纹理对象和纹理采样器对象绑定到渲染管线的像素着色器阶段:使用ID3D11DeviceContext::PSSetShaderResources和ID3D11DeviceContext::PSSetSamplers函数将纹理和采样器对象绑定到像素着色器阶段的着色器资源和采样器寄存器上。
- 在像素着色器中使用采样器对象对纹理进行采样:使用tex2D或tex2Dlod等纹理采样函数对纹理进行采样,并将采样结果作为像素颜色输出。
下面是一个简单的纹理采样的代码示例:
Texture2D tex;
SamplerState samp;
float4 PSMain(float4 pos : SV_POSITION, float2 texcoord : TEXCOORD) : SV_TARGET
{
return tex.Sample(samp, texcoord);
}
在这个像素着色器中,使用了tex.Sample函数对纹理进行采样,并将采样结果作为像素颜色输出。samp是一个采样器对象,tex是一个纹理对象。
二.教程项目09中的做法
这里以设置木箱纹理为例进行说明
1.用CreateDDsTextureFromFile函数从文件读取DDS纹理
函数参数如下所示:
HRESULT CreateDDSTextureFromFile(
ID3D11Device* d3dDevice, // [In]D3D设备
const wchar_t* szFileName, // [In]dds图片文件名
ID3D11Resource** texture, // [Out]输出一个指向资源接口类的指针,也可以填nullptr
ID3D11ShaderResourceView** textureView, // [Out]输出一个指向着色器资源视图的指针,也可以填nullptr
size_t maxsize = 0, // [In]忽略
DDS_ALPHA_MODE* alphaMode = nullptr); // [In]忽略
比如初始化木箱纹理可以这样做:
// 初始化木箱纹理
HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"Texture\\WoodCrate.dds", nullptr, m_pWoodCrate.GetAddressOf()));
2.修改HLSL代码,新建像素着色器
首先修改Basic.hlsli的代码
#include "LightHelper.hlsli"
//新建的Texture2D变量和SamplerState变量
Texture2D gTex : register(t0);
SamplerState gSamLinear : register(s0);
cbuffer VSConstantBuffer : register(b0)
{
matrix g_World;
matrix g_View;
matrix g_Proj;
matrix g_WorldInvTranspose;
}
cbuffer PSConstantBuffer : register(b1)
{
DirectionalLight g_DirLight[10];
PointLight g_PointLight[10];
SpotLight g_SpotLight[10];
Material g_Material;
int g_NumDirLight;
int g_NumPointLight;
int g_NumSpotLight;
float g_Pad1;
float3 g_EyePosW;
float g_Pad2;
}
struct VertexPosNormalTex
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
};
struct VertexPosTex
{
float3 PosL : POSITION;
float2 Tex : TEXCOORD;
};
struct VertexPosHWNormalTex
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION; // 在世界中的位置
float3 NormalW : NORMAL; // 法向量在世界中的方向
float2 Tex : TEXCOORD;
};
struct VertexPosHTex
{
float4 PosH : SV_POSITION;
float2 Tex : TEXCOORD;
};
其中Texture2D类型用于保存2D纹理的信息,这里通过register绑定到寄存器t索引0位置
SamplerState类型用于确定采样器应如何进行采样,这里通过register绑定到寄存器s索引0位置
新建的2d像素着色器如下:
// Basic_PS_2D.hlsl
#include "Basic.hlsli"
// 像素着色器(2D)
float4 PS_2D(VertexPosHTex pIn) : SV_Target
{
return g_Tex.Sample(g_SamLinear, pIn.Tex);
}
其中返回值 g_Tex.Sample(g_SamLinear, pIn.Tex)是根据纹理坐标取出纹理对应位置最为接近的像素
3d像素着色器如下:
// Basic_PS_3D.hlsl
#include "Basic.hlsli"
// 像素着色器(3D)
float4 PS_3D(VertexPosHWNormalTex pIn) : SV_Target
{
// 标准化法向量
pIn.NormalW = normalize(pIn.NormalW);
// 顶点指向眼睛的向量
float3 toEyeW = normalize(g_EyePosW - pIn.PosW);
// 初始化为0
float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 A = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 D = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 S = float4(0.0f, 0.0f, 0.0f, 0.0f);
int i;
for (i = 0; i < g_NumDirLight; ++i)
{
ComputeDirectionalLight(g_Material, g_DirLight[i], pIn.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
}
for (i = 0; i < g_NumPointLight; ++i)
{
ComputePointLight(g_Material, g_PointLight[i], pIn.PosW, pIn.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
}
for (i = 0; i < g_NumSpotLight; ++i)
{
ComputeSpotLight(g_Material, g_SpotLight[i], pIn.PosW, pIn.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
}
float4 texColor = g_Tex.Sample(g_SamLinear, pIn.Tex);//纹理采样
float4 litColor = texColor * (ambient + diffuse) + spec;
litColor.a = texColor.a * g_Material.Diffuse.a;
return litColor;
}
3.在GameApp::InitEffect函数中创建顶点着色器和像素着色器,设置顶点布局
bool GameApp::InitEffect()
{
ComPtr<ID3DBlob> blob;
// 创建顶点着色器(2D)
HR(CreateShaderFromFile(L"HLSL\\Basic_VS_2D.cso", L"HLSL\\Basic_VS_2D.hlsl", "VS_2D", "vs_5_0", blob.ReleaseAndGetAddressOf()));
HR(m_pd3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pVertexShader2D.GetAddressOf()));
// 创建顶点布局(2D)
HR(m_pd3dDevice->CreateInputLayout(VertexPosTex::inputLayout, ARRAYSIZE(VertexPosTex::inputLayout),
blob->GetBufferPointer(), blob->GetBufferSize(), m_pVertexLayout2D.GetAddressOf()));
// 创建像素着色器(2D)
HR(CreateShaderFromFile(L"HLSL\\Basic_PS_2D.cso", L"HLSL\\Basic_PS_2D.hlsl", "PS_2D", "ps_5_0", blob.ReleaseAndGetAddressOf()));
HR(m_pd3dDevice->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pPixelShader2D.GetAddressOf()));
// 创建顶点着色器(3D)
HR(CreateShaderFromFile(L"HLSL\\Basic_VS_3D.cso", L"HLSL\\Basic_VS_3D.hlsl", "VS_3D", "vs_5_0", blob.ReleaseAndGetAddressOf()));
HR(m_pd3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pVertexShader3D.GetAddressOf()));
// 创建顶点布局(3D)
HR(m_pd3dDevice->CreateInputLayout(VertexPosNormalTex::inputLayout, ARRAYSIZE(VertexPosNormalTex::inputLayout),
blob->GetBufferPointer(), blob->GetBufferSize(), m_pVertexLayout3D.GetAddressOf()));
// 创建像素着色器(3D)
HR(CreateShaderFromFile(L"HLSL\\Basic_PS_3D.cso", L"HLSL\\Basic_PS_3D.hlsl", "PS_3D", "ps_5_0", blob.ReleaseAndGetAddressOf()));
HR(m_pd3dDevice->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pPixelShader3D.GetAddressOf()));
return true;
}
4.在GameApp::InitResourse函数中初始化纹理,新建采样器状态并设置采样器状态
// 初始化木箱纹理
HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"Texture\\WoodCrate.dds", nullptr, m_pWoodCrate.GetAddressOf()));
// 初始化采样器状态
D3D11_SAMPLER_DESC sampDesc;
ZeroMemory(&sampDesc, sizeof(sampDesc));
sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
sampDesc.MinLOD = 0;
sampDesc.MaxLOD = D3D11_FLOAT32_MAX;
HR(m_pd3dDevice->CreateSamplerState(&sampDesc, m_pSamplerState.GetAddressOf()));
5.在GameApp::UpdateScene函数中更新画面
void GameApp::UpdateScene(float dt)
{
Keyboard::State state = m_pKeyboard->GetState();
m_KeyboardTracker.Update(state);
// 键盘切换模式
if (m_KeyboardTracker.IsKeyPressed(Keyboard::D1))
{
// 播放木箱动画
m_CurrMode = ShowMode::WoodCrate;
m_pd3dImmediateContext->IASetInputLayout(m_pVertexLayout3D.Get());
auto meshData = Geometry::CreateBox();
ResetMesh(meshData);
m_pd3dImmediateContext->VSSetShader(m_pVertexShader3D.Get(), nullptr, 0);
m_pd3dImmediateContext->PSSetShader(m_pPixelShader3D.Get(), nullptr, 0);
m_pd3dImmediateContext->PSSetShaderResources(0, 1, m_pWoodCrate.GetAddressOf());
}
//其余部分...
if (m_CurrMode == ShowMode::WoodCrate)
{
static float phi = 0.0f, theta = 0.0f;
phi += 0.00003f, theta += 0.00005f;
XMMATRIX W = XMMatrixRotationX(phi) * XMMatrixRotationY(theta);
m_VSConstantBuffer.world = XMMatrixTranspose(W);
m_VSConstantBuffer.worldInvTranspose = XMMatrixTranspose(InverseTranspose(W));
// 更新常量缓冲区,让立方体转起来
D3D11_MAPPED_SUBRESOURCE mappedData;
HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[0].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
memcpy_s(mappedData.pData, sizeof(VSConstantBuffer), &m_VSConstantBuffer, sizeof(VSConstantBuffer));
m_pd3dImmediateContext->Unmap(m_pConstantBuffers[0].Get(), 0);
}
//其余部分...
}
}
三.基于项目09加入新的纹理并实现纹理采样的过程
1.准备纹理
首先我在Quixel Bridge上下载了一个纹理,在文件夹中找到其纹理的jpg文件
接着打开英伟达官方提供的软件NVIDIA Texture Tools Exporter将图片格式转换为dds格式的文件,直接将选中的图片拖到右边后就会变成下面的界面:
需要检查左上角两项参数的设置,Format和Image type,要设置成图中的格式,接着点右下角的Save As…设置保存的格式和路径,如下图:
保存类型选择DDS,这里我将纹理文件命名为wall,这样纹理就准备好了。
2.在GameApp.h中新定义用于调用wall纹理的变量和枚举值
wall的指针声明如下:
ComPtr<ID3D11ShaderResourceView> m_pTestCrate; //纹理采样测试用纹理
枚举设置如下:
enum class ShowMode { WoodCrate, FireAnim,TestCrate };//TestCrate用于wall纹理
3.在GameApp::InitResource中用CreateDDSTextureFromFile函数初始化wall纹理
HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"..\\Texture\\wall.dds", nullptr, m_pTestCrate.GetAddressOf()));
4.在GameApp::UpdateScene中增加wall纹理的更新画面
由于最新版的教程项目09将按键操作改成用imgui操作,所以基于imgui作修改
void GameApp::UpdateScene(float dt)
{
if (ImGui::Begin("Texture Mapping"))
{
static int curr_mode_item = static_cast<int>(m_CurrMode);
const char* mode_strs[] = {
"Box",
"Fire Anim",
"Test"//转换为wall纹理
};
if (ImGui::Combo("Mode", &curr_mode_item, mode_strs, ARRAYSIZE(mode_strs)))
{
if (curr_mode_item == 0)
{
// 播放木箱动画
m_CurrMode = ShowMode::WoodCrate;
m_pd3dImmediateContext->IASetInputLayout(m_pVertexLayout3D.Get());
auto meshData = Geometry::CreateBox();
ResetMesh(meshData);
m_pd3dImmediateContext->VSSetShader(m_pVertexShader3D.Get(), nullptr, 0);
m_pd3dImmediateContext->PSSetShader(m_pPixelShader3D.Get(), nullptr, 0);
m_pd3dImmediateContext->PSSetShaderResources(0, 1, m_pWoodCrate.GetAddressOf());
}
else if(curr_mode_item == 1)
{
m_CurrMode = ShowMode::FireAnim;
m_CurrFrame = 0;
m_pd3dImmediateContext->IASetInputLayout(m_pVertexLayout2D.Get());
auto meshData = Geometry::Create2DShow();
ResetMesh(meshData);
m_pd3dImmediateContext->VSSetShader(m_pVertexShader2D.Get(), nullptr, 0);
m_pd3dImmediateContext->PSSetShader(m_pPixelShader2D.Get(), nullptr, 0);
m_pd3dImmediateContext->PSSetShaderResources(0, 1, m_pFireAnims[0].GetAddressOf());
}
else//将状态改为显示wall纹理
{
m_CurrMode = ShowMode::TestCrate;
m_pd3dImmediateContext->IASetInputLayout(m_pVertexLayout3D.Get());
auto meshData = Geometry::CreateCone();//创建圆锥体
ResetMesh(meshData);
m_pd3dImmediateContext->VSSetShader(m_pVertexShader3D.Get(), nullptr, 0);
m_pd3dImmediateContext->PSSetShader(m_pPixelShader3D.Get(), nullptr, 0);
m_pd3dImmediateContext->PSSetShaderResources(0, 1, m_pTestCrate.GetAddressOf());
}
}
}
ImGui::End();
ImGui::Render();
if (m_CurrMode == ShowMode::WoodCrate)
{
static float phi = 0.0f, theta = 0.0f;
phi += 0.0001f, theta += 0.00015f;
XMMATRIX W = XMMatrixRotationX(phi) * XMMatrixRotationY(theta);
m_VSConstantBuffer.world = XMMatrixTranspose(W);
//m_VSConstantBuffer.worldInvTranspose = XMMatrixTranspose(InverseTranspose(W));
static float alpha = 0.0f;
alpha += 0.001f;//使纹理逆时针旋转
XMMATRIX RotateMatrix = XMMatrixTranslation(-0.5f, -0.5f, 0.0f) * XMMatrixRotationZ(alpha) * XMMatrixTranslation(0.5f, 0.5f, 0.0f);
m_VSConstantBuffer.worldInvTranspose = XMMatrixTranspose(RotateMatrix);
// 更新常量缓冲区,让立方体转起来
D3D11_MAPPED_SUBRESOURCE mappedData;
HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[0].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
memcpy_s(mappedData.pData, sizeof(VSConstantBuffer), &m_VSConstantBuffer, sizeof(VSConstantBuffer));
m_pd3dImmediateContext->Unmap(m_pConstantBuffers[0].Get(), 0);
}
else if (m_CurrMode == ShowMode::FireAnim)
{
// 用于限制在1秒60帧
static float totDeltaTime = 0;
totDeltaTime += dt;
if (totDeltaTime > 1.0f / 60)
{
totDeltaTime -= 1.0f / 60;
m_CurrFrame = (m_CurrFrame + 1) % 120;
m_pd3dImmediateContext->PSSetShaderResources(0, 1, m_pFireAnims[m_CurrFrame].GetAddressOf());
}
}
else
{ //显示wall纹理
static float phiT = 0.0f, thetaT = 0.0f;
phiT += 0.0001f, thetaT += 0.0001f;
XMMATRIX W = XMMatrixRotationX(phiT) * XMMatrixRotationY(thetaT);
m_VSConstantBuffer.world = XMMatrixTranspose(W);
//m_VSConstantBuffer.worldInvTranspose = XMMatrixTranspose(InverseTranspose(W));
static float alphaT = 0.0f;
alphaT -= 0.001f;//想使纹理逆时针旋转,但好像没转起来
XMMATRIX RotateMatrix = XMMatrixTranslation(-0.5f, -0.5f, 0.0f) * XMMatrixRotationZ(alphaT) * XMMatrixTranslation(0.5f, 0.5f, 0.0f);
m_VSConstantBuffer.worldInvTranspose = XMMatrixTranspose(RotateMatrix);
// 更新常量缓冲区,让圆锥体转起来
D3D11_MAPPED_SUBRESOURCE mappedData;
HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[0].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
memcpy_s(mappedData.pData, sizeof(VSConstantBuffer), &m_VSConstantBuffer, sizeof(VSConstantBuffer));
m_pd3dImmediateContext->Unmap(m_pConstantBuffers[0].Get(), 0);
}
}