很久没更新博客了, 有点忙, 最近有点疲惫了, 写写新的东西也不错, 这次接着说Direct2D 的特效篇. 有一年多没更新这部分了. 发现代码水平比一年前又提高了不少:)
自从用了vs2015, 腰不酸了, 腿不疼了, 一口气码几百行也不费劲儿. 所以工程代码可能需要Win10 SDK + VS2015才能编译成功. 理论Win8可以, 稍微改下Win7也行, 但是Win10能保证没问题:)
微软提供的内建特效中拥有高斯模糊和定向模糊. 对于基本应用来说还差个径向模糊.
那么我们现在就动手做一个吧, 先做一个”游戏版”的径向模糊, 稍微简单点:
模糊算法我们大概都懂: 处理后的像素点是原来”附近”几个采样点的加权平均值. 高斯模糊就是高斯函数的加权, 普通就算术平均值即可, 简单.
所以理论上我们只要顺着一个中心点, 进行采样即可.
比如8个采样点, 强度大致上60%, (当然这是我提供的样图而已, 实际是64个采样点否则不能这么细腻),
这个输出像素点颜色就是:
point = ?, ?
center = ?, ?
step = (center - point) / 8 * 0.6
color = 0
for(i: 0...8) color += get_color(point + step*i)
color /= 8
怎么样, 简单吧, 如果强度达到100%, 基本每个点都为有中心点的”基因”(忘记用啥词了).
所以这次依然是用像素着色器, 不过带有常量缓存区了(cbuffer):
// 2D纹理 第一个输入储存在t0
Texture2D InputTexture : register(t0);
// 采样器状态 第一个储存在s0
SamplerState InputSampler : register(s0);
// 常量缓存 (b0)
cbuffer RadialBlurProperties : register(b0) {
float2 center : packoffset(c0);
float magnitude : packoffset(c0.z);
float samples : packoffset(c0.w);
};
// Shader入口
float4 main(
float4 clipSpaceOutput : SV_POSITION,
float4 sceneSpaceOutput : SCENE_POSITION,
float4 texelSpaceInput0 : TEXCOORD0
) : SV_Target {
// 初始化
float4 color = 0;
// 计算步进值
float2 step = (center - texelSpaceInput0.xy) / samples * magnitude;
// 遍历每个采样点
for(float i = 0; i < samples; ++i) {
// 计算采样地点
float2 pos = texelSpaceInput0.xy + step * i;
// 添加采样数据
color += InputTexture.Sample(InputSampler, pos);
}
// 平均
return color / samples;
}
main函数没什么说的吧, 与之前的特殊情况基本一个道理.
// 常量缓存 (b0)
cbuffer RadialBlurProperties : register(b0) {
float2 center : packoffset(c0);
float magnitude : packoffset(c0.z);
float samples : packoffset(c0.w);
};
center, 模糊控制中心点。
magnitude, 模糊强度.
samples, 采样数量. 可以用整型, 但是自以为显卡的浮点运算能力比较强, 所以就用float了.
cbuffer是个关键字, 表示常量缓存, 从D3D12里面知道是以256字节对齐的, 所以尽可能用上256字节的倍数吧.
b0表示第一个常量缓存寄存器.
packoffset是因为默认128位对齐, 所以用这个.
c++端 就简单了, 这次二合一:
// 径向模糊特效
class RadialBlurEffect final : public ID2D1EffectImpl, public ID2D1DrawTransform {
public:
// 属性列表
enum : UINT32 {
// D2D1_POINT_2F
Properties_CenterPoint = 0,
// float
PROPERTIES_Magnitude,
// float
PROPERTIES_Samples
};
.........
public:
// 设置中心点
auto SetCenter(const D2D1_POINT_2F& pt) noexcept { m_cbuffer.center = pt; }
// 获取中心点
auto GetCenter() const noexcept { return m_cbuffer.center; }
// 设置程度
auto SetMagnitude(float m) noexcept { m_cbuffer.magnitude = m; }
// 获取程度
auto GetMagnitude() const noexcept { return m_cbuffer.magnitude; }
// 设置采样数量
auto SetSamples(float s) noexcept { m_cbuffer.samples = s; }
// 获取采样数量
auto GetSamples() const noexcept { return m_cbuffer.samples; }
private:
.................
// 常量缓存
struct {
// 中心点
D2D1_POINT_2F center = D2D1_POINT_2F();
// 程度
float magnitude = 0.1f;
// 采样点数量
float samples = 16.f;
// 常量缓存
} m_cbuffer;
};
这次同时继承于ID2D1EffectImpl, ID2D1DrawTransform, 方便编写.
修改了控制变量, 就同步修改cbuffer, 这是正解. 但是修改一个就同步修改, 性能肯定低下, 毕竟受限于显卡带宽. 所以我们只需要在渲染前检查: 如果修改了, 就同步显卡数据. 微软为我们提供了ID2D1EffectImpl::PrepareForRender接口, 方便检查:
// 准备渲染
IFACEMETHODIMP RadialBlurEffect::PrepareForRender(D2D1_CHANGE_TYPE changeType) noexcept {
if (changeType == D2D1_CHANGE_TYPE_NONE) return S_OK;
// 设置像素着色器
m_pDrawInfo->SetPixelShader(GUID_DustPG_RadialBlurShader);
// 修改了属性
if (changeType == D2D1_CHANGE_TYPE_PROPERTIES) {
// 更新常量缓存
return m_pDrawInfo->SetPixelShaderConstantBuffer(
reinterpret_cast(&m_cbuffer), sizeof(m_cbuffer)
);
}
return S_OK;
}
需要两部分:
D2D1_PROPERTY_BINDING数据
Xml比较简单添加Property节点就行:
xml(
<Effect>
<Property name = "DisplayName" type = "string" value = "RadiaBlurEffect" />
<Property name = "Author" type = "string" value = "dustpg" />
<Property name = "Category" type = "string" value = "Transform" />
<Property name = "Description" type = "string" value = "径向模糊" />
<Inputs>
<Input name = "Source" />
Inputs>
<Property name='Center' type='vector2'>
<Property name='DisplayName' type='string' value='radial blur center'/>
Property>
<Property name='Magnitude' type='float'>
<Property name='DisplayName' type='string' value='radial blur cagnitude'/>
Property>
<Property name='Samples' type='float'>
<Property name='DisplayName' type='string' value='radial blur samples count'/>
Property>
Effect>
参考前面几篇有张列表, type属性里面需要填写.
D2D1_PROPERTY_BINDING用微软提供的宏D2D1_VALUE_TYPE_BINDING就行
, 不过我的失败了, 不知道是不是多继承的锅, 所以就自己写了个, 匿名表达式就是不错:
#define MY_D2D1_VALUE_TYPE_BINDING(CLASS, TYPE, NAME)\
{\
L#NAME, [](IUnknown* obj, const BYTE* data, UINT32 len) noexcept {\
assert(obj && data && len == sizeof(TYPE));\
auto impl = static_cast(obj);\
auto ths = static_cast(impl);\
ths->Set##NAME(*reinterpret_cast(data));\
return S_OK;\
}, [](const IUnknown* obj, BYTE* OPTIONAL data, UINT32 len, UINT32* OPTIONAL outeln) noexcept {\
assert(obj);\
if (data) {\
auto impl = static_cast<const ID2D1EffectImpl*>(obj);\
auto ths = static_cast<const RadialBlurEffect*>(impl);\
*reinterpret_cast(data) = ths->Get##NAME();\
}\
if (outeln) {\
*outeln = sizeof(TYPE);\
}\
return S_OK;\
}\
}
有点麻烦, 不过也就那样了. 不过我发现, 这样的话, 岂不是要限定CPU与GPU的大小段一致? ╮( ̄▽ ̄)╭ 不过这不是我们关心的事情.
注册, 初始化, 就不说了.
// 设置径向模糊参数
void ImageRenderer::config_blur_properties() noexcept {
// 设置特性
const int NAMELESS = 400;
const int NAMELESS_2 = NAMELESS / 2;
{
// 计算中心点
POINT pt; ::GetCursorPos(&pt);
::ScreenToClient(m_hwnd, &pt);
auto size = m_pTestBitmap->GetSize();
size.width = float(pt.x) / size.width;
size.height = float(pt.y) / size.height;
// 计算强度
auto magnitude = static_cast<float>(int(m_cFrameCount) % NAMELESS - NAMELESS_2)
/ static_cast<float>(NAMELESS_2);
magnitude = std::abs(magnitude) * 2.f - 1.f;
// 计算采样量
float sam = 64.f;
// 设置数据
m_pRadialBlurEffect->SetValue(RadialBlurEffect::Properties_CenterPoint, size);
m_pRadialBlurEffect->SetValue(RadialBlurEffect::PROPERTIES_Magnitude, magnitude);
m_pRadialBlurEffect->SetValue(RadialBlurEffect::PROPERTIES_Samples, sam);
}
}
工程代码为了方便大家, 我放在了github上面,
因为可以在线浏览, 这一节的地址在这里. 对了, 不忘记shader的编译方式, 为了节约运行时时间, 这里依然采用预先编译的方式, 忘了的看看前面.