渲染到纹理(Render To Texture, RTT)详解
RTT是现在很多特效里面都会用到的一项很基本的技术,实现起来很简单,也很重要。但是让人不解的是网上搜索了半天只找到很少的文章说这个事儿,不知道是因为太简单还是因为这项技术已经出现很长时间了。总之我是在摸索这个东西的时候绕了不少弯子。现在把具体的实现方法写下来。
什么是纹理
熟悉DX的兄弟们都知道什么叫纹理了,这里简单介绍一下,先看看现实生活中的例子吧,其实纹理的例子比比皆是,比如地板,墙面都是纹理。在图形学中,纹理主要是为了增强场景的真实感,如果你想绘制一个地面,简单一点可以直接使用一个矩形,稍微复杂一点可以用三角形网格,再复杂一点可以使用地面纹理,有了纹理以后真实感明显增强了。DX中的纹理映射其实就是对现实生活中纹理的模拟,D3D中有专门的数据结构来管理纹理。
渲染到纹理
常规的渲染操作都是直接将场景呈现到backbuffer中的,backbuffer说白了其实就是一个表面,再说白了就是一块内存,场景通过绘制函数载入显存后,再通过Present函数送至显示器。那么为什么还要渲染到纹理呢?这是为了实现一些特殊的效果,比如常见的环境映射,简单的说,想象你有一个光滑的球体,它应该是可以反射周围环境的,这就是环境映射。
实现步骤
上面说了常规的渲染操作是将场景送至backbuffer,而backbuffer实际上是一个Surface,而纹理恰恰又包含了Surface,所以我们只需要取得纹理的Surface,其次将场景送至这个Surface,最后再把这个纹理渲染到backbuffer中即可。举个例子,假设你要在一面墙壁上画一幅画,你有两种方法
1 直接在墙上画,这个很好理解,就对应常规的backbuffer渲染。
2 先将画绘制在纸上,然后将纸贴到墙上,这就对应渲染到纹理的过程。
这里墙壁相当于backbuffer,而纸张相当于纹理的Surface,在纸上作画相当于渲染到纹理,把纸贴到墙上相当于把纹理渲染到backbuffer,希望大家没有迷糊就好。具体的步骤如下
1 创建纹理并获得纹理的表面(Surface)
2 向纹理的表面渲染场景
3 渲染纹理本身
代码
1. 声明变量
- LPDIRECT3DTEXTURE9 pRenderTexture = NULL; // 目标纹理
- PDIRECT3DSURFACE9 pRenderSurface = NULL,pBackBuffer = NULL, pTempSurface;
- // pRenderSurface是pRenderTexture 对应的Surface
- // pBackBuffer用于保存原来的Render Target
LPDIRECT3DTEXTURE9 pRenderTexture = NULL; // 目标纹理
LPDIRECT3DSURFACE9 pRenderSurface = NULL,pBackBuffer = NULL, pTempSurface;
// pRenderSurface是pRenderTexture 对应的Surface
// pBackBuffer用于保存原来的Render Target
2.创建一个纹理作为渲染目标(Render Target)
- //注意这里的第三个参数必须为D3DUSAGE_RENDERTARGET
- //第四个参数决定纹理的格式,不同的场景会要求不同的格式
- pd3dDevice->CreateTexture( TEX_WIDTH,TEX_HEIGHT,1,D3DUSAGE_RENDERTARGET,
- D3DFMT_R5G6B5,D3DPOOL_DEFAULT,&pRenderTexture,NULL);
- //获得pRenderTexture对应的Surface
- pRenderTexture->GetSurfaceLevel(0,&pRenderSurface);
//注意这里的第三个参数必须为D3DUSAGE_RENDERTARGET
//第四个参数决定纹理的格式,不同的场景会要求不同的格式
pd3dDevice->CreateTexture( TEX_WIDTH,TEX_HEIGHT,1,D3DUSAGE_RENDERTARGET,
D3DFMT_R5G6B5,D3DPOOL_DEFAULT,&pRenderTexture,NULL);
//获得pRenderTexture对应的Surface
pRenderTexture->GetSurfaceLevel(0,&pRenderSurface);
3.渲染场景
- //这里保存下原来的Render target,在做完RTT后再恢复
- pd3dDevice->GetRenderTarget(0,&pBackBuffer);
- if( SUCCEEDED( pd3dDevice->BeginScene() ) )
- {
- //设置我们的纹理为render target
- pd3dDevice->SetRenderTarget(0, pRenderSurface);
- pd3dDevice->Clear( 0, NULL,
- D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
- D3DXCOLOR(0.0f,0.00f,0.00f,1.00f), 1.0f, 0);
- //重新将render target设置为帧缓存
- pd3dDevice->SetRenderTarget(0, pBackBuffer);
- pd3dDevice->EndScene();
- pBackBuffer->Release();
- }
//这里保存下原来的Render target,在做完RTT后再恢复
pd3dDevice->GetRenderTarget(0,&pBackBuffer);
if( SUCCEEDED( pd3dDevice->BeginScene() ) )
{
//设置我们的纹理为render target
pd3dDevice->SetRenderTarget(0, pRenderSurface);
pd3dDevice->Clear( 0, NULL,
D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
D3DXCOLOR(0.0f,0.00f,0.00f,1.00f), 1.0f, 0);
//重新将render target设置为帧缓存
pd3dDevice->SetRenderTarget(0, pBackBuffer);
pd3dDevice->EndScene();
pBackBuffer->Release();
}
4. 善后
- SAFE_RELEASE(pRenderSurface);
- SAFE_RELEASE(pRenderTexture);
SAFE_RELEASE(pRenderSurface);
SAFE_RELEASE(pRenderTexture);
这里需要注意的几点:
渲染的时候要选择正确的纹理格式。如果你要在纹理里面保存高精度浮点数的话。通常所用的A8R8G8B8格式每一个颜色分量只有8位,只能表示0-255。详情可以参考DirectX SDK Help中关于D3DFORMAT的说明。
如果你的纹理长宽比和帧缓存的不同的话,那么你需要在切换RenderTarget以后重新设置投影矩阵。否则渲染出来的图像会被拉伸。
纹理的大小不能太大。经过试验发现是在窗口模式下面窗口和纹理的大小不能超过屏幕减去任务栏的大小。如果超过的话似乎对纹理的任何操作都不会有效果。(可能是深度缓存不够大,参考注意事项4)
如果想要验证自己渲染出来的纹理是否正确,可以用D3DXSaveTextureToFile把纹理保存为图像。
如果想要直接访问纹理中的值则要麻烦一些。按照SDK文档里面的说法,作为RenderTarget的纹理是保存在显存上面的,无法lock与unlock。要向访问其中的值需要做如下操作:
- LPDIRECT3DTEXTURE9 text;
- LPDIRECT3DSURFACE9 surf;
- D3DLOCKED_RECT lockbits;
- pd3dDevice->CreateTexture(TEX_WIDTH,TEX_HEIGHT,1,0,
- D3DFMT_R5G6B5, D3DPOOL_SYSTEMMEM,
- &text, NULL);
- text->GetSurfaceLevel(0,&surf);
- if (pd3dDevice->GetRenderTargetData(pRenderSurface, surf) == D3D_OK)
- if (surf->LockRect(&lockbits, NULL, D3DLOCK_READONLY) == D3D_OK)
- {
- pRenderSurface->UnlockRect();
- float* bits=(float*)(lockbits.pBits);
- // SAVE BITS TO TEXT FILE
- FILE* ofile = fopen("output.txt", "w");
- for (int i=0; i<64; i++)
- {
- for (int j=0; j<64; j++)
- fprintf(ofile, "(%2.2f,%2.2f,%2.2f) ", bits[i*64*4+j*4], bits[i*64*4+j*4+1], bits[i*64*4+j*4+2]);
- fprintf(ofile, "\n");
- }
- fclose(ofile);
- }
- text->Release();
- surf->Release();
LPDIRECT3DTEXTURE9 text;
LPDIRECT3DSURFACE9 surf;
D3DLOCKED_RECT lockbits;
pd3dDevice->CreateTexture(TEX_WIDTH,TEX_HEIGHT,1,0,
D3DFMT_R5G6B5, D3DPOOL_SYSTEMMEM,
&text, NULL);
text->GetSurfaceLevel(0,&surf);
if (pd3dDevice->GetRenderTargetData(pRenderSurface, surf) == D3D_OK)
if (surf->LockRect(&lockbits, NULL, D3DLOCK_READONLY) == D3D_OK)
{
pRenderSurface->UnlockRect();
float* bits=(float*)(lockbits.pBits);
// SAVE BITS TO TEXT FILE
FILE* ofile = fopen("output.txt", "w");
for (int i=0; i<64; i++)
{
for (int j=0; j<64; j++)
fprintf(ofile, "(%2.2f,%2.2f,%2.2f) ", bits[i*64*4+j*4], bits[i*64*4+j*4+1], bits[i*64*4+j*4+2]);
fprintf(ofile, "\n");
}
fclose(ofile);
}
text->Release();
surf->Release();
这个技术可以用来在多通道渲染中传递渲染结果。比如可以把RTT出来的结果用来作为第二编渲染中的纹理来使用,这样可以实现水面反射等效果。另外在通用计算中可以用来保存数据。例如可以把GPU数值计算以后的结果保存在纹理中,再用上面所说的方法把数字读出来(如果真要这么做的话别忘了把纹理格式设置为足够大精度的格式,比如说A32B32G32R32F)。
转载至:http://shiba.hpe.sh.cn/jiaoyanzu/WULI/showArticle.aspx?articleId=451&classId=4
渲染目标是一个缓冲,显卡通过这个缓冲使用一个Effect类绘制场景的像素。
默认的渲染目标叫做后备缓冲- 物理上就是包含下一帧要绘制的信息的一块显存。你可以使用RenderTarget2D类创建另一个渲染目标,在显存中保留一块新区域用于绘制。大多数游戏在后备缓冲之外将大量的内容绘制到其他渲染目标内("offscreen"),然后编译这些不同的图像元素,将它们组合起来构成最终的后备缓冲。
一个渲染目标具有高和宽。后备缓冲的高和宽就是游戏的最终分辨率(而在Xbox 360中最终结果要进行缩放匹配用户的屏幕)。而一个offscreen渲染目标无需和后背缓冲有相同大小的高和宽,最终图像的小部分可以绘制到一个小渲染目标中,然后将它复制到另一个渲染目标。渲染目标还有一个surface格式,表明每个像素分配到多少bits和它们如何分割成红,绿,蓝,alpha通道。例如,SurfaceFormat.Bgr32给每个像素分配32 bits:每个颜色通道8 bits ,alpha通道8 bits。渲染目标还可以对所有绘制在其中的图像施加反锯齿(antialiasing)。
要使用渲染目标,需要创建一个指定高、宽或其他选项的RenderTarget2D对象。然后调用GraphicsDevice.SetRenderTarget将这个渲染目标作为当前渲染目标。从这一步开始,任何对Draw的调用会绘制到这个渲染目标。当结束渲染目标后,调用 GraphicsDevice.SetRenderTarget切换到一个新的渲染目标(或设置为null切换到后备缓冲)。 然后你就可以在任何时候调用RenderTarget2D.GetTexture获取渲染目标的内容进行后继处理。
;渲染目标可以和depth-stencil缓冲结合起来使用。如果你设置一个新的渲染目标,这个渲染目标会使用一个已存在的depth-stencil缓冲。如果新渲染目标有一个不同于depth-stencil缓冲的multisampling设置,或更大的宽和高,你就需要一个新的depth-stencil缓冲匹配这种情况。你还需要在depth-stencil缓冲中使用一个匹配渲染目标表面格式的深度格式。有时你可以同时渲染超过一个以上的渲染目标。你的图形设备支持的渲染目标的数量可以从MaxSimultaneousRenderTargets属性获得。使用多个渲染目标有很多变量,更多的信息可见Render
Targets。
转
关于RenderTarget的注意事项
1. 设置一个RenderTarget会导致viewport变成跟RenderTarget一样大
2. 反锯齿类型必须跟DepthStencilBuffer一样
3. RenderTarget的类型必须跟DepthStencilBuffer的类型兼容, 可以用IDirect3D9::CheckDepthStencilMatch进行检测
4. DepthStencilBuffer的大小必须>=RenderTarget的大小
5. IDirect3DDevice9::SetRenderTarget的第0个不能为NULL
6. Usage为D3DUSAGE_RENDERTARGET的Texture不能进行反锯齿, 而且Pool必须为D3DPOOL_DEFAULT. 如果想利用RenderTarget做为纹理又想反锯齿, 可以先把场景渲染到一个CreateRenderTarget创建的Surface(或BackBuffer)上, 再用IDirect3DDevice9::StretchRect拷贝到纹理上
7. D3DX提供了一个ID3DXRenderToSurface, 简化了RenderTarget的使用. 注意它的BeginScene跟EndScene与再用IDirect3DDevice9的同名函数对不能嵌套, 因为实际上内部还是调用的IDirect3DDevice9的, 用PIX可以看到它进行了哪些调用. 还有就是这个接口仍然不能反锯齿, 而且每次都要保存/恢复一堆状态, 总觉得不爽
8. RTT不能既做为输入就做为输出目标, 某些显卡上可能只会给一个warning, 有些显卡上则会发生报错/黑屏/死机之类不可预计的事情...另外, Depth stencil texture(参见Hareware shadow map)也有同样的问题, 用完之后要SetTexture(n, NULL)清空, 不然A卡会黑屏/花屏/深度错误, 既使你没有使用, 只要它被寄存器引用了, 显卡还是会当做是正在使用的, 这时就不能做为depth stencil buffer
9. RTT如果想保存到文件中, 是不能直接SaveToTexture的. 需要创建一个OffscreenSurface, 拷贝过去, 再保存. 不过N卡好像不支持DXT1格式的OffscreenSurface, 可以创建Texture, 取其level0的surface代替.
10. N卡在开启了锯齿后冒似所有的RTT都要反锯齿, 不然深度测试会失败-_-
11. Intel的显卡在RTT没有设置DepthBuffer时可能所有绘制全部深度测试失败, 需要关闭深度测试再画.
12. SRGBWRITE不支持Float格式的RT
13. MRT时使用第一个RT的alpha来做alpha test
14. MRT不支持反锯齿, 必须相同bitdepth, 可以不同格式, 必须相同大小