首页 > 代码库 > Direct2D 1.1 开发笔记 特效篇(三) 简单的像素着色器特效

Direct2D 1.1 开发笔记 特效篇(三) 简单的像素着色器特效

(转载请注明出处)


这次我们实现一个自定义的转变。


实现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;
};

可以参考D3D11的采样器状态描述:


我们在main里面使用:

<span style="font-size:14px;">	return InputTexture.Sample(InputSampler, texelSpaceInput0.xy);</span>

即可返回当前的像素信息,那个采样函数就不用说了,猜都能猜出来;



我们现在需要反相,那就简单了

// 一个简单的像素着色器范例: 反相


// 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不一样了。


下一节将简单介绍一下怎么调试图形设备,再下一节再介绍一个稍微全面点的像素着色器特效。


本节范例下载地址: 点击这里









Direct2D 1.1 开发笔记 特效篇(三) 简单的像素着色器特效