第18章 水乳交融的艺术——Alpha混合技术

18.1 初识Alpha 通道与混合技术

大家应该都知道, Alpha 通道是计算机中存储一张图片的透明和半透明度信息的通道。它是一 个8 位的灰度通道,用256 级灰度来记录图像中的透明度信息,定义透明、不透明和半透明区域, 其中黑表示全透明,白表示不透明,灰表示半透明。
混合的英文术语为Blending , 是计算机图形学中常用的一种技术,即混合像素。我们通常用已 经光栅化的像素光栅化同一位置的像素,或者说是在某图元上混合图元。为了方便理解,我们举个 例子。说白了就是在一张图元的地盘上又来了另一张图元,然后它们按照我们指定的某种方式来像 “揉面团” 一样揉在一起,处理完后再在原地显示出来。
Alpha 混合技术对熟悉游戏的朋友们来说应该不会陌生,这种技术在如今的游戏特效里已经被 用烂了。且不说3D 游戏中它的频繁登场,就算是2D 的游戏中,这种技术也是满眼皆是。

Alpha 混合听上去很神秘,实际非常简单,其作用就是要实现一种半透明效果。假设一种不透明东西的颜色是A,另一种透明的东西的颜色是B,那么透过B 去看A,看上去的颜色C 就是B和A 的混合颜色,可以用这个式子来近似,设B 物体的透明度为alpha (取值为0~1 ,

0为完全透明, 1 为完全不透明〉。


其中R(x)、G(x) 、B(x)分别指颜色x 的RGB 分量(这里自变量x 取的是颜色C )。看起来这个东西这么简单,可是用它实现的效果确实非常地华丽,应用Alpha 混合技术,可以实现出最眩目的火光、烟雾、阴影、动态光源等一切我们可以想象的出来的半透明效果。

18.2 Direct3D 中的融合套路——融合因子

上面我们讲到的是一般意义上的混合技术,而在Direct3D 中,我们应该按如下的思路来进一 步理解。
首先, Direct3D 中依然是用Alpha 通道来实现多个像素颜色值的融合。每个像素都包含四个分 量: Alpha 分量、红色分量、绿色分量和蓝色分量(即ARGB 四分量) 。其中, Alpha 分量用于指 定像素的透明度,在0~255 之间取值, 0 表示完全透明, 255 表示完全不透明。另外,根据使用的 不同的颜色宏的区别, 还可能是在0.0~1.0 之间取值。
在Direct3D 中,融合这一领域有一个权威,那便是Alpha 融合公式。Alpha 融合公式如下:

OutPutColor = (RGDsrc·Ksrc)OP(RGBdst·Kdst)
其中, RGBsrc和RGBdst分别表示源像素和目标像素的颜色值,为包含四个颜色分量的颜色值。Ksrc 和Kdst分别表示源融合因子和目标融合因子。它们指定了源像素和目标像素的颜色值在融合过程中所占的比例, 在[0 , 1]之间取值。通过源融合因子和目标融合因子,我们能够以多种方式来修改源像素和目标像素的颜色值,从而获得我们满意的最终的融合后的颜色值。稍后会讲解融合因子的具体取法,这里我们先把这个融合公式解析完。
在融合公式中, OP 表示源和目标的融合运算方式,由D3DBLENDOP 枚举体来指定,需要注意的是它的默认值是源计算结果和目标计算结果相加。而运算符“·”表示颜色值的每个分量都与Ksrc 和Kdst 相乘。

18.3 融合运算方式的取法

上面我们提到过,融合运算方式由D3DBLENDOP 枚举体来指定。
我们指的是SetRenderState 中的第二个参数在D3DBLENDOP 枚举体中取值,而第一个参数取 D3DRS_BLENDOP 。
D3DBLENDOP 枚举体的定义如下:

typedef enum D3DBLENDOP {
  D3DBLENDOP_ADD           = 1,
  D3DBLENDOP_SUBTRACT      = 2,
  D3DBLENDOP_REVSUBTRACT   = 3,
  D3DBLENDOP_MIN           = 4,
  D3DBLENDOP_MAX           = 5,
  D3DBLENDOP_FORCE_DWORD   = 0x7fffffff 
} D3DBLENDOP, *LPD3DBLENDOP;
我们用一个列表来进行讲解吧:


我们需要取什么类型的融合运算方式,在表中查阅即可。再提醒大家一下, Direct3D 中为我们默认取了融合运算方式为D3DBLENDOP_ADD,即源像素计算结果与目标像素的计算结果相加。

18.4 融合因子的取法

本节我们来看一下融合因子Ksrc 和Kdst的取法。源融合因子和目标融合因子可以在SetRenderState 方法中第一个参数取

D3DRS_SRCBLEND 和D3DRS_DESTBLEND 分别进行设置,而第二个参数都是在一个D3DBLEND 枚举体中进行的取值,我们在MSDN 中查到它的原型如下:

typedef enum D3DBLEND {
  D3DBLEND_ZERO              = 1,
  D3DBLEND_ONE               = 2,
  D3DBLEND_SRCCOLOR          = 3,
  D3DBLEND_INVSRCCOLOR       = 4,
  D3DBLEND_SRCALPHA          = 5,
  D3DBLEND_INVSRCALPHA       = 6,
  D3DBLEND_DESTALPHA         = 7,
  D3DBLEND_INVDESTALPHA      = 8,
  D3DBLEND_DESTCOLOR         = 9,
  D3DBLEND_INVDESTCOLOR      = 10,
  D3DBLEND_SRCALPHASAT       = 11,
  D3DBLEND_BOTHSRCALPHA      = 12,
  D3DBLEND_BOTHINVSRCALPHA   = 13,
  D3DBLEND_BLENDFACTOR       = 14,
  D3DBLEND_INVBLENDFACTOR    = 15,
  D3DBLEND_SRCCOLOR2         = 16,
  D3DBLEND_INVSRCCOLOR2      = 17,
  D3DBLEND_FORCE_DWORD       = 0x7fffffff 
} D3DBLEND, *LPD3DBLEND;

依旧是通过一个表格来对其中常用的参数进行讲解:



在上表中, Rsrc、Gsrc、Bsrc、Asrc 分别表示源(即source )像素的红、绿、蓝、透明四个分量值,而Rdst 、Gdst 、Bdst 、Adst,表示目标( 即destination )像素的红、绿、蓝、透明四个分量值。
大家需要什么类型的融合因子,在上表中进行查阅就行了。

18.5 Alpha 的三处来源

在使用Alpha 融合之前,还需要明确源像素和目标像素颜色值的Alpha 分量来自何方。像素的 Alpha 值一般有三处来源,分别是顶点颜色的Alpha 值、材质的Alpha 值、纹理的Alpha 值。我们 通常在这三处来源中取一处就可以了。

它们的优先级是这样的,纹理>材质>顶点颜色。

即这样理解:
首先我们看有没有使用纹理贴图。如果是使用了纹理贴图,那么像素的Alpha 值就优先来源于纹理贴图的Alpha 通道。

再看有没有使用光照和材质。如果使用了材质,那么像素的Alpha 值就优先来源于物体表面的材质。

最后,若既没有使用纹理贴图也没有使用光照和材质,那么像素的Alpha 值就只能来源于顶点的颜色值的Alpha 分量了。

下面我们分别看看这三处来源如何通过代码来指定。
1. 顶点Alpha 分量
首先我们要知道,顶点Alpha 分量只是备胎的备胎而己,它在没有使用光照和材质的情况下才有上场的机会。
如果在程序中直接指定每个顶点的颜色,那么可以直接给出每个顶点颜色的Alpha 值,并且这些顶点的Alpha 值是可以在程序运行过程中动态修改的。

我们可以通过IDirect3DDevice9::SetTextureStageState 方法指定Alpha 值的来源,把第三个参数指定为D3DTA_DIFFUSE ,来指定Alpha 值来自顶点颜色。
在MSDN 中查到这个函数原型如下:

HRESULT SetTextureStageState(
  [in]  DWORD Stage,
  [in]  D3DTEXTURESTAGESTATETYPE Type,
  [in]  DWORD Value
);

  •  第一个参数, DWORD 类型的Stage , 指定当前设置的纹理层为第几层(有效值0~7 )。
  •  第二个参数, D3DTEXTURESTAGESTATETYPE 类型的Type , 填将要设置的纹理渲染状态,在枚举类型D3DTEXTURESTAGESTATETYPE 中任意取值。
  •  第三个参数, DWORD 类型的Value,表示所设置的状态值,它是根据第二个参数来决定具体取什么值的。
对于顶点Alpha 分量,我们就这样写两句:

  //计算漫反射颜色的alpha值
 g pd3dDevice->SetTextureStageState(O, D3DTSS ALPHAARGl, D3DTA DIFFUSE);
 g_pd3dDevice->SetTextureStageState(O, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
SetTextureStageState 方法在21.2.3 节有更详细的讲解。

2. 材质的Alpha 分量
这一小节我们讲的材质的Alpha 分量,充其量也只是个备胎而己, 在场景内的物体没有指定纹理的时候,才有用武之地。在这种情况下,顶点的Alpha 值取决于材质属性中漫反射颜色的Alpha系数以及灯光颜色中的Alpha 系数,通过材质和光照中的Alpha 系数相互作用,计算得到。我们知道,顶点的光照计算过程是分别针对红、绿、蓝、Alpha 这四个颜色分量分开独立计算的。而我们关注的顶点的Alpha 值就决定于光照计算结果的Alpha 分量,和其他的红、绿、蓝三分量毫无瓜葛;比如可以这样来设定某材质的Alpha 分量值,这句代码中我们把这种材质的漫反射颜色值的Alpha 分量设为了0.2 (范围为0 .0~ 1.0 )。

g_pMaterial.Diffuse.a= 0.2f;
3 . 纹理的Alpha 分量
作为不可一世的“ 高富帅”一一纹理,既然它在物体表面上使用了,就必须首先满足它的要求,这样,像素的Alpha 值就是纹理Alpha 混合之后的值了。所以这时候混合后的像素就取决于纹理的Alpha 混合方式。而纹理Alpha 混合方式决定了纹理Alpha 混合之后的Alpha 值是取自材质,还是取自纹理,抑或是取自这两者的某种运算。
若是取自纹理,我们就这样写:

 m_pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE ); // Alpha值是取自材质
 m_pd3dDevice->SetTextureStageState(O, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1) ; // 将纹理颜色混合的第一个参数的ALPHA值用于输出
可以使用DirectX SDK 中提供的DirectX Texture Tool 来为我们的素材纹理图片创建Alpha 通道。

18.6 Alpha 融合使用三步曲

前面讲了那么多,现在依然是落实到一个字“用”上,依旧是为大家总结一个使用的三步曲, 方便大家立竿见影,快速掌握Alpha 融合技术的使用。
1. 三步曲之一:启用Alpha 融合
在Direct3D 中,混合默认是被关闭着的,要启用Alpha 融合的话,我们就通过设置 D3DRS_ALPHABLENDENABLE 渲染状态为true,即写上这句代码:

g_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, true);
2. 三步曲之二:设置融合因子
启用了Alpha 融合,第二步便是设置源融合因子和目标融合因子。前面我们已经详细讲解过源融合因子Ksrc 和目标融合因子Kdst的取值。源融合因子和目标融合因子分别可以在SetRenderState方法中第一个参数取D3DRS_SRCBLEND 和D3DRS_DESTBLEND 分别进行设置,第二个参数都是在一个D3DBLEND 枚举体中进行的取值。
比如这样写,就是源融合因子= (Asrc, Asrc, Asrc), 目标融合因子= ( 1| Asrc, 1| Asrc, 1| Asrc ),这是我们最常用的Alpha 混合因子取法:

	//三步曲之二,设置融合因子
	g_pd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
	g_pd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
3. 三步曲之三:设置Alpha 融合运算方式
因为Direct3D 中有默认的融合运算方式D3DBLENDOP_ADD ,即源像素计算结果与目标像素的计算结果相加。所以这一步其实可以省略,但是如果不想用这种融合运算方式,我们可以加上这一步,用SetRenderState 方法来改成我们需要的运算方式,比如如下的代码,便将融合运算方式改成了D3DBLENDOP_SUBTRACT,即源像素计算结果与目标像素的计算结果相减:
g_pd3dDevice->SetRenderState(D3DRS_BLENDOP, D3DBLENDOP_SUBTRACT);
4 . 以代码为载体理解
依旧是把这三步曲整合一下,在Direct3D 中运用Alpha 混合技术,其实最少只用写3 句代码,
第三步通常可以省略,即如下的代码:

	// 三步曲之一,开启Alpha融合
	g_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, true);
	//三步曲之二,设置融合因子
	g_pd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
	g_pd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
	//三步曲之三,设置融合运算方式
	g_pd3dDevice->SetRenderState(D3DRS_BLENDOP, D3DBLENDOP_ADD);  //这句设置运算方式为D3DBLENDOP_ADD的代码Direct3D默认为我们写了,所以注释掉这句也没大碍

18.7 示例程序D3Ddemo13

这个示例程序在D3Ddemo12 的基础上,我们没有载入模型的纹理, 于是可以看到材质Alpha的效果。在Object_lnit()函数中我们载入材质时代码经过了一些调整,主要是28-39 行。然后是63 -69行一气呵成的Alpha 融合使用三步曲:
//-----------------------------------【Object_Init( )函数】--------------------------------------
//	描述:渲染资源初始化函数,在此函数中进行要被渲染的物体的资源的初始化
//--------------------------------------------------------------------------------------------------
HRESULT Objects_Init()
{
	//创建字体
	D3DXCreateFont(g_pd3dDevice, 36, 0, 0, 1000, false, DEFAULT_CHARSET, 
		OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, _T("Calibri"), &g_pTextFPS);
	D3DXCreateFont(g_pd3dDevice, 20, 0, 1000, 0, false, DEFAULT_CHARSET, 
		OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"华文中宋", &g_pTextAdaperName); 
	D3DXCreateFont(g_pd3dDevice, 23, 0, 1000, 0, false, DEFAULT_CHARSET, 
		OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"微软雅黑", &g_pTextHelper); 
	D3DXCreateFont(g_pd3dDevice, 26, 0, 1000, 0, false, DEFAULT_CHARSET, 
		OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"黑体", &g_pTextInfor); 

	// 从X文件中加载网格数据
	LPD3DXBUFFER pAdjBuffer  = NULL;
	LPD3DXBUFFER pMtrlBuffer = NULL;

	D3DXLoadMeshFromX(L"65.X", D3DXMESH_MANAGED, g_pd3dDevice, 
		&pAdjBuffer, &pMtrlBuffer, NULL, &g_dwNumMtrls, &g_pMesh);

	// 读取材质和纹理数据
	D3DXMATERIAL *pMtrls = (D3DXMATERIAL*)pMtrlBuffer->GetBufferPointer(); //创建一个D3DXMATERIAL结构体用于读取材质和纹理信息
	g_pMaterials = new D3DMATERIAL9[g_dwNumMtrls];
	g_pTextures  = new LPDIRECT3DTEXTURE9[g_dwNumMtrls];

	for (DWORD i=0; i<g_dwNumMtrls; i++) 
	{
		//获取材质,并设置一下环境光的颜色值
		g_pMaterials[i] = pMtrls[i].MatD3D;
		//g_pMaterials[i].Ambient = g_pMaterials[i].Diffuse;

		g_pMaterials[i].Diffuse.a = 0.3f;//设置材质的Alpha分量

		//创建一下纹理对象
		g_pTextures[i]  = NULL;
		//注释掉纹理的载入,让“备胎”材质的Alpha通道有上场的机会
		//D3DXCreateTextureFromFileA(g_pd3dDevice, pMtrls[i].pTextureFilename, &g_pTextures[i]);
	}

	SAFE_RELEASE(pAdjBuffer)
	SAFE_RELEASE(pMtrlBuffer)


	// 设置光照
	D3DLIGHT9 light;
	::ZeroMemory(&light, sizeof(light));
	light.Type          = D3DLIGHT_DIRECTIONAL;
	light.Ambient       = D3DXCOLOR(0.5f, 0.5f, 0.5f, 1.0f);
	light.Diffuse       = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
	light.Specular      = D3DXCOLOR(0.0f, 0.0f, 0.0f, 1.0f);
	light.Direction     = D3DXVECTOR3(1.0f, 0.0f, 1.0f);
	g_pd3dDevice->SetLight(0, &light);
	g_pd3dDevice->LightEnable(0, true);
	g_pd3dDevice->SetRenderState(D3DRS_NORMALIZENORMALS, true);
	g_pd3dDevice->SetRenderState(D3DRS_SPECULARENABLE, true);

	// 设置渲染状态
	g_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);   //开启背面消隐
	g_pd3dDevice->SetRenderState(D3DRS_ZFUNC, D3DCMP_LESS);   //将深度测试函数设为D3DCMP_LESS
	g_pd3dDevice->SetRenderState(D3DRS_ZWRITEENABLE, true);     //深度测试成功后,更新深度缓存


	// 三步曲之一,开启Alpha融合
	g_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, true);
	//三步曲之二,设置融合因子
	g_pd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
	g_pd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
	//三步曲之三,设置融合运算方式
	g_pd3dDevice->SetRenderState(D3DRS_BLENDOP, D3DBLENDOP_ADD);  //这句设置运算方式为D3DBLENDOP_ADD的代码Direct3D默认为我们写了,所以注释掉这句也没大碍
	
	return S_OK;
}
另外一个地方是在Direct3D_Update()函数中加入了通过按键的按下来控制漫反射Alpha 分量值的变化的相关处理代码,如下:

	//通过按键的按下来控制漫反射Alpha分量值的变化
	if (g_pDInput->IsKeyDown(DIK_1))  //按下1键
	{
		for (DWORD i=0; i<g_dwNumMtrls; i++) 
		{
			g_pMaterials[i].Diffuse.a+= 0.001f;
		}
	}

	if (g_pDInput->IsKeyDown(DIK_2)) //按下1键
	{
		for (DWORD i=0; i<g_dwNumMtrls; i++) 
		{
			g_pMaterials[i].Diffuse.a-= 0.001f;
		}
	}
其余代码基本上和上章中的demo 一致,这里就不再贴出浪费篇幅。我们看一下运行截图:
    

我们可以通过键盘按键1与键2 ,来增大或者缩小材质的Alpha 值,以得到透明度不同的人物模型。

18.8 章节小憩

是不是觉得自己现在能写出来的程序越来越炫了呢?哈哈,好戏还在后头,加油往后学, 继续我们追梦的旅程吧。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值