(转载请注明出处)
这次我们实现一个自定义的转变。
实现Direct2D 自定义转变Shader Models需要HLSL(High Level Shading Language)的实现。
HLSL是Shader的一种实现,但是HLSL只能在D3D中使用,所以有点蛋疼。
Shader被描述为显卡执行的小段程序,能够高效(并行)地执行。
没学过?没关系,笔者也没有,但是详细的不会在这里说明(你TM逗我(╯‵□′)╯︵┴─┴),请到官网中看看。
D2D 特效能用 HLSL 的 4.0 及其以上版本(Shader Models 4.0),这是在D3D 10中实现的。
但是我们为了编程的方便,强行要求显卡支持D3D11,毕竟笔者的破集成显卡都支持D3D11。
D2D 特效能够使用的着色器有: 像素着色器,顶点着色器与计算着色器。
这次的主题就是写一个简单的像素着色器转变——反相,就是将颜色反转过来。
想想若是CPU执行,先是需要将几兆的数据翻转过来,再送到显卡显示,效率堪忧。
先看看D3D11 的渲染管线吧。
当然D2D没有这么复杂,光栅化也简单,毕竟是2D。
这个管线看看就行,除非你开发D3D11程序,D2D特效只需了解即可——像素着色器是几个操作最后一步(除了OM输出)
实现D2D像素着色器特效,需要实现ID2D1DrawTransform,查看头文件会发现:
ID2D1DrawTransform-----继承于---->ID2D1Transform-----继承于---->ID2D1TransformNode-----继承于---->IUnknown
而ID2D1TransformNode就是我们上一节中提到的"转变节点",什么AddNode、SetOutputNode的
实现接口:
既然我们要实现这个接口,就倒着看吧(其实是正着看...):
0. IUnknown的三个接口:
这个不解释,实现看着办。
1.ID2D1TransformNode的一个接口:
ID2D1TransformNode::GetInputCount 获取输入对象的个数,我们这次是"反相",需要一个输入,直接返回1即可
2.ID2D1Transform的三个接口:
ID2D1Transform::MapInputRectsToOutputRect 每当D2D渲染这个转变时就会调用这个接口,这个方法负责计算输出区域。
参数分别为: 输入矩形数组,输入不透明矩形数组,输入数组长度。后面2个是输出: 输出矩形,输出不透明矩形。
理论上讲可以将输入输出不透明矩形那两个参数去掉,但是因为透明的的地方需要与下面的图像进行混合,加之渲染
D2D特效提供的混合方法非常多,计算较复杂。而D2D 像素着色器仅仅只关心自己的,下面的交给D2D自动完成。
所以为了优化,提供了这两个参数。如果您不知道预先不知道哪些地方是透明的,请将输出不透明矩形设为(0,0,0,0)
下面是微软提供的一张图片:
执行高斯模糊,假设程度为5,那么(l-5, t-5, r+5, b+5)是输出矩形, (l+5, t+5, r-5, b-5)是不透明矩形。
因为我们这里仅仅一个输入,再加之不需要透明信息。我们简单地这样实现:
if (inputRectCount != 1) return E_INVALIDARG;
*pOutputRect = pInputRects[0];
m_inputRect = pInputRects[0];
*pOutputOpaqueSubRect = *pOutputRect;
return S_OK;
当然,我们需要保存输入矩形(其实算是输出矩形),不然我们在哪画画:
ID2D1Transform::MapOutputRectToInputRects D2D在调用上一个方法后,接着就会调用本方法.这个方法指定
D2D应该读取图片的那些地方,如果那个地方没有像素点(不在输入图像范围内),D2D会自动采样为透明黑色(0,0,0,0)
同上,微软也提供了一个图方便理解:
可以理解: 比如输出左上角的那个像素点,是它周围N个像素点的平均值(或加权平均值),
那么就需要扩展输入范围。
当然,我们这里直接返回刚才保留的输入矩形即可。毕竟是点对点的特效。
ID2D1Transform::MapInvalidRect 不同于上面两个方法,这个方法不一定会调用。官方的说明是
为本次转变渲染通道设置输入矩形。参数一是矩形索引。同上,有需要扩大,没需要不变。
3.ID2D1DrawTransform的一个方法
ID2D1DrawTransform::SetDrawInfo 参数只有一个ID2D1DrawInfo,现在只需要用到其中1个:
ID2D1DrawInfo::SetPixelShader 提供一个像素着色器的GUID即可,很明显需要注册Shader的GUID。
实现ID2D1EffectImpl
上节讲了,就不说了,不过这次仅仅一个转变,直接SetSingleTransformNode即可
注册Shader:
0. 编译对象文件
早在ID2D1EffectImpl::Initialize 时,就可以创建注册Shader的GUID了。假设我们已经写好了一个hlsl,
在VS Express 2013 for Windows Desktop中选中文件点击右键----属性:
“项目类”设置为“HLSL 编译器”,请注意不同的配置也要注意设置一下(Debug/Release, x86/x64啥的)
“常规”子支中,入口点名称随意,着色器类型选择像素着色器,着色器模型选择5.0
输出中选择对象文件名,这个笔者设置的就是 ShaderObject\%(Filename).cso
1. 注册Shader
需要自行生成一个GUID,上节讲了,这里不说了。
FILE* file = _wfopen(L"ShaderObject\\InvertShader.cso", L"rb");
if (file){
fseek(file, 0L, SEEK_END);
size_t length = ftell(file);
BYTE* pBuffer = new BYTE[length];
fseek(file, 0L, SEEK_SET);
if (pBuffer){
fread(pBuffer, 1, length, file);
m_hr = context->LoadPixelShader(GUID_MyInvertShader, pBuffer, length);
delete[] pBuffer;
}
else{
m_hr = E_OUTOFMEMORY;
}
fclose(file);
}
else{
m_hr = E_FAIL;
}
这样就能注册了。
注意:
使用LoadPixelShader注册一次之后即可,但是本次范例中因为仅仅调用一次创建这个对象,所以没有处理。
负面结果是第二次创建同一特效也需要读取文件,造成不必要的效率问题。
编写Shader
所有的装备工作都完成了。万事俱备,就欠Shader了。
我们这次编写的是像素着色器,它是按照以像素为单位进行(并行)计算。
先给个简单的:
// Shader入口
float4 main() : SV_Target
{
return float4(1, 1, 0, 1);
}
main就是刚刚指定的入口,float4表示返回的是一个4维向量,这里返回的是颜色(1,1,0,1)即黄色。
那么为了让编译器明白我们返回的是颜色,需要指定语义, SV_Target就是一个自带的语义,表示颜色。
如果您熟悉D3D9的Shader的话,它使用的颜色语义是COLOR,更加直观,但是有点不同,这个稍后讲诉。
SV表示System Value, Target应该就是Render Target了。
参数:
大多数函数都是带参数的。虽然上面的没用参数(但是能够编译使用),
D2D 特效自动传给像素着色器默认的参数有3个,写完全的应该这样:
// Shader入口
float4 main(
float4 sceneSpaceOutput : SCENE_POSITION,
float4 clipSpaceOutput : SV_POSITION,
float4 texelSpaceInput0 : TEXCOORD0
) : SV_Target
{
return float4(1, 1, 0, 1);
}
获取图像:
D2D 自动将第一张图像写入 纹理缓存寄存器0(t0), 第二张写入t1......依此类推
我们仅需绑定即可:
// 2D纹理 第一个输入储存在t0
Texture2D InputTexture : register(t0);
D2D 也自动将采样器状态写入 采样器寄存器(s0,s1.....依此类推)
// 采样器状态 第一个储存在s0
SamplerState InputSampler : register(s0);
我们也能直接在Shader内定义自己的采样器状态,比如:
SamplerState MySampler
{
Filter = MIN_MAG_MIP_POINT;
AddressU = Wrap;
AddressV = Wrap;
};
我们在main里面使用:
return InputTexture.Sample(InputSampler, texelSpaceInput0.xy);
即可返回当前的像素信息,那个采样函数就不用说了,猜都能猜出来;
我们现在需要反相,那就简单了
// 一个简单的像素着色器范例: 反相
// 2D纹理 第一个输入储存在t0
Texture2D InputTexture : register(t0);
// 采样器状态 第一个储存在s0
SamplerState InputSampler : register(s0);
// Shader入口
float4 main(
float4 sceneSpaceOutput : SCENE_POSITION,
float4 clipSpaceOutput : SV_POSITION,
float4 texelSpaceInput0 : TEXCOORD0
) : SV_Target
{
// 反相代码 不透明可以用这个
//return float4(1,1,1,2) - InputTexture.Sample(InputSampler, texelSpaceInput0.xy);
float4 color = InputTexture.Sample(InputSampler, texelSpaceInput0.xy);
color.xyz = float3(1, 1, 1) - color.xyz;
return color;
}
color.xyz直接计算xyz3维向量,shader有些地方非常苛刻,但是这个简单易懂:
效果:
注:
自然需要说明一下了。
texelSpaceInput0.xy并不是对齐的真实坐标,而是经过转换的,
texelSpaceInput0.xy / texelSpaceInput0.zw就能获取当前的真实位置。
注意!是“真实位置”,而不是像素坐标。是比当前像素坐标偏移了半像素,横纵都是。为什么?
简单的说一个像素的中心位置就是它的真实位置,比如(0, 0)的像素点,真实位置在(0.5, 0.5),
详细的可以Google"Half-Pixel Offset in DirectX 11"
也就是在DX10后,SV_Target 与DX9的 COLOR不一样了。
下一节将简单介绍一下怎么调试图形设备,再下一节再介绍一个稍微全面点的像素着色器特效。
本节范例下载地址: 点击这里