要使用DirectX来获得三维效果,一般首先要生成一个三维模型,然后计算它在可视空间中的投影。这样得到的二维图像十分真实,但是计算量也很大。在大规模场景渲染中,随着模型精度的提高,这样的处理方式十分消耗资源。人眼的分辨率是有限的,对于远处的模型,模糊一些不会影响到整体效果。Billboard技术就是用二维图片来模拟三维模型的投影,从而提高渲染效率。只要距离足够远,通过将二维图片旋转至合适角度,实际渲染效果与三维模型相差无几,但计算量减少很多。本文使用几何着色器,利用Billboard技术在之前的模型中添加树木贴图。

整个过程与上一篇的内容类似。不过这一次树木模型的顶点结构与其他模型不同,所以要重新写一套着色器(TreeVertexShader.hlsl、TreeGeometryShader.hlsl、TreePixelShader.hlsl)。使用Billboard绘制树木时,CPU只要生成树木的位置和大小即可,计算过程均由几何着色器完成,而顶点着色器只起到传递参数的作用,代码如下:

struct VertexShaderInput
{float3 center : POSITION;float2 size : SIZE;
};struct VertexShaderOutput
{ float3 center : POSITION;float2 size : SIZE;
};VertexShaderOutput main( VertexShaderInput input )
{VertexShaderOutputoutput;output.center =input.center;output.size =input.size;return output;
}

另外,为了方便观察绘制效果,新像素着色器只进行纹理采样,不实现光照等效果。

SamplerState samplerLinear : register(s0);
Texture2D texDiffuse : register(t0);struct PixelInputType
{float4 posH    : SV_POSITION;float3 posW    : POSITION;float3 normalW : NORMAL;float2 texC    : TEXCOORD;
};float4 main(PixelInputType pIn) : SV_Target
{float4 diffuse =texDiffuse.Sample(samplerLinear, pIn.texC);// alpha值小于0.25,放弃该像素clip(diffuse.a -0.25f);// 输出纹理颜色return diffuse;
}

三个新着色器中,几何着色器是重点。由于几何着色器在顶点着色器和像素着色器之间,根据前面的代码可以很容易地得到几何着色器的结构定义:

struct GSInput
{float3 center : POSITION;float2 size : SIZE;
};struct GSOutput
{float4 posH : SV_POSITION;float3 posW : POSITION;float3 normal : NORMAL;float2 tex : TEXCOOD;
};

而计算树木贴图的变换矩阵时需要观察点的位置等信息,所以在几何着色器中定义一个常量缓冲区来存储相关信息:

cbuffer cbTreeConstanBuffer : register(b0)
{matrix model;matrix view;matrix projection;float4 eye;
};

接下来就根据输入的点信息来生成树木模型。具体的数学原理在DirectX游戏编程中有详细的介绍,这里主要关注其实现。

[maxvertexcount(4)]
void main(point GSInput input[1],inout TriangleStream<GSOutput > output
)
{//// 根据size计算树木贴图的四个顶点坐标//float halfWidth =0.5f*input[0].size.x;float halfHeight =0.5f*input[0].size.y;float4 v[4];v[0] = float4(-halfWidth,-halfHeight, 0.0f, 1.0f);v[1] = float4(+halfWidth,-halfHeight, 0.0f, 1.0f);v[2] = float4(-halfWidth,+halfHeight, 0.0f, 1.0f);v[3] = float4(+halfWidth,+halfHeight, 0.0f, 1.0f);//// 四个顶点的纹理坐标//float2 texC[4];texC[0] = float2(0.0f, 1.0f);texC[1] = float2(1.0f, 1.0f);texC[2] = float2(0.0f, 0.0f);texC[3] = float2(1.0f, 0.0f);//// 计算使贴图面向观察点的变换矩阵//float3 up = float3(0.0f, 1.0f, 0.0f);float3 look =input[0].center - eye.xyz;look.y =0.0f; look =normalize(look);float3 right = cross(up,look);float4x4 W;W[0] = float4(right,           0.0f);W[1] = float4(up,              0.0f);W[2] = float4(look,            0.0f);W[3] = float4(input[0].center,1.0f);float4x4 gViewProj =mul(view, projection);float4x4 WVP =mul(W,gViewProj);//// 转换顶点坐标到世界空间// 输出三角形带//GSOutput gOut;[unroll]for(int i = 0; i < 4;++i){gOut.posH    = mul(v[i], WVP);gOut.posW    = mul(v[i], W).xyz;gOut.normal  = look;gOut.tex     = texC[i];output.Append(gOut);}
}

有上一篇文章的基础,着色器的代码很容易理解。读入一个顶点(即树木贴图的中心点坐标和贴图的尺寸),生成四个顶点,之后将四个顶点转换到投影空间,并设置好其对应的纹理坐标,接着就可以由像素着色器进行处理。从这个过程中可以看出,顶点能够包含的内容是很广泛的,并不仅仅是坐标信息而已,感觉顶点应该是可由GPU处理的信息集合。

着色器编写完成后,就能在程序中使用了。首先还是定义顶点和常量缓冲区的结构体,与着色器代码对应。在Direct3Dbase.h中添加:

struct TreeVertex
{DirectX::XMFLOAT3 center;DirectX::XMFLOAT2 size;
};struct TreeConstantBuffer
{DirectX::XMFLOAT4X4 model;DirectX::XMFLOAT4X4 view;DirectX::XMFLOAT4X4 projection;DirectX::XMFLOAT4 eye;
};

然后仿照之前的模型类编写TreeModel类,负责生成树木的顶点信息和渲染树木贴图。主要方法的代码如下

void TreeModel::Initialize(ID3D11Device* d3dDevice)
{TreeVertex treeVertices[] ={{XMFLOAT3( 60.0,  GetHeight(60.0f, 50.0f), 50.0f), XMFLOAT2( 15.0f, 15.0f )},{XMFLOAT3( 20.0f,  GetHeight(20.0f, 30.0f), 30.0f), XMFLOAT2( 15.0f, 15.0f )},{XMFLOAT3( 20.0f,  GetHeight(20.0f, 40.0f), 40.0f), XMFLOAT2( 15.0f, 15.0f )},{XMFLOAT3( 50.0f,  GetHeight(50.0f, 10.0f), 10.0f), XMFLOAT2( 15.0f, 15.0f )},};m_indexCount = ARRAYSIZE(treeVertices);D3D11_SUBRESOURCE_DATA vertexBufferData ={0};vertexBufferData.pSysMem= treeVertices;vertexBufferData.SysMemPitch= 0;vertexBufferData.SysMemSlicePitch= 0;CD3D11_BUFFER_DESC vertexBufferDesc(sizeof(treeVertices), D3D11_BIND_VERTEX_BUFFER);DX::ThrowIfFailed(d3dDevice->CreateBuffer(&vertexBufferDesc,&vertexBufferData,&m_vertexBuffer));
}void TreeModel::Render(ID3D11DeviceContext* d3dContext)
{UINT stride = sizeof(TreeVertex);UINT offset = 0;d3dContext->IASetVertexBuffers(0,1,m_vertexBuffer.GetAddressOf(),&stride,&offset);d3dContext->Draw(m_indexCount,0);
}float TreeModel::GetHeight(float xPos, float zPos)
{return 8.0f + 0.3f * (zPos*sinf(0.1f*xPos) + xPos*cosf(0.1f*zPos));
}

注意,虽然实际绘制的是一个个树木贴图,但是从程序中看,绘制的只是一个个点,而不是两个三角形拼成的矩形。所以,TreeModel中可以不用索引数组,同时要用Draw方法来渲染这个模型,而不是其他模型的DrawIndexed方法。另外,GetHeight方法在地标坐标的基础上增加了8.0f,保证树木在地表上方。定义好树木模型后,接下来要修改Renderer类,添加与树木贴图相关的成员:

//----------------------------------------------------------// 树木贴图相关//----------------------------------------------------------void DrawTrees();TreeModel m_tree;Microsoft::WRL::ComPtr<ID3D11VertexShader>m_treeVertexShader;Microsoft::WRL::ComPtr<ID3D11GeometryShader>m_treeGeometryShader;Microsoft::WRL::ComPtr<ID3D11PixelShader>m_treePixelShader;Microsoft::WRL::ComPtr<ID3D11InputLayout> m_treeInputLayout;Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> m_treeSRV;Microsoft::WRL::ComPtr<ID3D11Buffer>m_treeConstantBuffer;TreeConstantBufferm_treeConstantBufferData;

然后在CreateDeviceResources方法中添加载入新着色器的代码,并初始化树木顶点的输入布局和常量缓冲区:

auto loadTreeVSTask =DX::ReadDataAsync("TreeVertexShader.cso");auto loadTreeGSTask =DX::ReadDataAsync("TreeGeometryShader.cso");auto loadTreePSTask =DX::ReadDataAsync("TreePixelShader.cso");auto createTreeVSTask =loadTreeVSTask.then([this](Platform::Array<byte>^ fileData) {DX::ThrowIfFailed(m_d3dDevice->CreateVertexShader(fileData->Data,fileData->Length,nullptr,&m_treeVertexShader));const D3D11_INPUT_ELEMENT_DESC treeVertexDesc[] ={{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0,  0,  D3D11_INPUT_PER_VERTEX_DATA, 0 },{ "SIZE",    0, DXGI_FORMAT_R32G32_FLOAT,    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },};DX::ThrowIfFailed(m_d3dDevice->CreateInputLayout(treeVertexDesc,ARRAYSIZE(treeVertexDesc),fileData->Data,fileData->Length,&m_treeInputLayout));});auto createTreeGSTask =loadTreeGSTask.then([this](Platform::Array<byte>^ fileData) {DX::ThrowIfFailed(m_d3dDevice->CreateGeometryShader(fileData->Data,fileData->Length,nullptr,&m_treeGeometryShader));CD3D11_BUFFER_DESCtreeConstantBufferDesc(sizeof(TreeConstantBuffer), D3D11_BIND_CONSTANT_BUFFER);DX::ThrowIfFailed(m_d3dDevice->CreateBuffer(&treeConstantBufferDesc,nullptr,&m_treeConstantBuffer));});auto createTreePSTask =loadTreePSTask.then([this](Platform::Array<byte>^ fileData) {DX::ThrowIfFailed(m_d3dDevice->CreatePixelShader(fileData->Data,fileData->Length,nullptr,&m_treePixelShader));});

之后还要初始化各个模型:

auto createModelTask =(createPSTask && createVSTask).then([this] () {m_hill.Initialize(m_d3dDevice.Get(),128, 128);m_water.Initialize(m_d3dDevice.Get(),128, 128, 1.0f, 0.03f, 3.25f, 0.4f);m_cube.Initialize(m_d3dDevice.Get(),XMFLOAT2(60.0f, 30.0f));m_tree.Initialize(m_d3dDevice.Get());});

不要忘了还要添加载入树木纹理的代码(这里的纹理使用的是DirectX游戏编程入门的资源)。


DX::ThrowIfFailed(CreateDDSTextureFromFile(m_d3dDevice.Get(),L"Texture/tree0.dds",NULL,m_treeSRV.GetAddressOf()));

还有最后一项需要填充的内容,常量缓冲区。树木模型与其他模型都在一个空间内,所以常量也相同,只是组织结构不同。为了方便几何着色器使用这些常量,所以新定义一个常量缓冲区。填充这个缓冲区在Update方法中进行:


// 更新树木模型缓冲区
m_treeConstantBufferData.model= m_constantBufferData.model;
m_treeConstantBufferData.view= m_constantBufferData.view;
m_treeConstantBufferData.projection= m_constantBufferData.projection;
XMStoreFloat4(&m_treeConstantBufferData.eye,eye);

完成后就能进入到渲染流程。下面的DrawTrees方法是仿照已有的绘制模型过程编写的。由于绘制树木模型用到的资源与绘制其它模型完全不同,所以为树木模型渲染单独创建这个方法。

void Renderer::DrawTrees()
{// 设置图元类型为点并修改输入布局m_d3dContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST);m_d3dContext->IASetInputLayout(m_treeInputLayout.Get());// 设置顶点着色器m_d3dContext->VSSetShader(m_treeVertexShader.Get(),nullptr,0);// 设置几何着色器及其常量缓冲区m_d3dContext->GSSetShader(m_treeGeometryShader.Get(),nullptr,0);m_d3dContext->GSSetConstantBuffers(0,               1,               m_treeConstantBuffer.GetAddressOf());m_d3dContext->UpdateSubresource(m_treeConstantBuffer.Get(),0,NULL,&m_treeConstantBufferData,0,0);// 设置像素着色器m_d3dContext->PSSetShader(m_treePixelShader.Get(),nullptr,0);// 设置树木纹理m_d3dContext->PSSetShaderResources(0,1,m_treeSRV.GetAddressOf());// 设置纹理采样器m_d3dContext->PSSetSamplers(0,1,m_Sampler.GetAddressOf());// 设置渲染模式SetFillMode(D3D11_FILL_SOLID);m_tree.Render(m_d3dContext.Get());
}

因为现在绘制的是点图元,所以第一句IASetPrimitiveTopology方法的参数是D3D11_PRIMITIVE_TOPOLOGY_POINTLIST,之前都是绘制三角形,用的参数是D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST。

完成DrawTrees方法后就可以在Render方法中调用它来实现树木的绘制。这里还有一点要注意,因为绘制普通模型无需几何着色器,所以在绘制过程中要添加关闭几何着色器的代码,不然在渲染普通模型时也会使用几何着色器,而几何着色器的输出与普通的像素着色器输入并不对应,会使渲染结果出错。


// 关闭几何着色器
m_d3dContext->GSSetShader(NULL,nullptr,0);

实际运行效果如下图:

本篇文章源代码:Direct3DApp_HillWaveTree

原文地址:http://blog.csdn.net/raymondcode/article/details/8528159

Windows 8 DirectX 开发学习笔记(十五)使用Billboard实现树木贴图相关推荐

  1. Windows 8 Directx 开发学习笔记(五)山峰河谷模型的简单实现

    通过之前对DirectX示例程序代码的研究,基本了解DirectX最简单的工作过程,为了更好的理解整个过程,基于示例程序做一个山峰河谷的简单模型.首先还是根据模板创建正方体的示例程序,Visual C ...

  2. windows内核开发学习笔记十五:IRP结构

    windows内核开发学习笔记十五:IRP结构   IRP(I/O Request Package)在windows内核中,有一种系统组件--IRP,即输入输出请求包.当上层应用程序需要访问底层输入输 ...

  3. Polyworks脚本开发学习笔记(十五)-用Python连接Polyworks的COM组件

    Polyworks脚本开发学习笔记(十五)-用Python连接Polyworks的COM组件 用Polyworks脚本开发,没有高级语言的支持,功能难免单一,一些比较复杂的交互实现不了,界面和报告也很 ...

  4. Windows 8 Directx开发学习笔记(一)应用基本框架

    Windows 8系统10月25日就要正式发布,其应用可与Windows Phone 8应用兼容,所以打算转到Windows 8系列的开发.之前虽然开发过应用,但对游戏开发更感兴趣,随意开始学习Met ...

  5. Windows 8 Directx 开发学习笔记(十一)地形纹理贴图

    前一篇实现木箱贴图时,木箱的六个面都正好用一整张纹理图,即六个面的纹理坐标均在[0,1]内.然而在为比较大的模型贴图时,像山峰河谷模型,如果只用一张纹理图,那么每个三角形只得到几个纹理元素,无法为提供 ...

  6. Windows 8 DirectX 开发学习笔记(十六)使用Terragen生成自然环境贴图

    DirectX 游戏编程入门中提到一个Terragen软件可以生成环境贴图,所以登陆它的官方网站看了下.没想到Terragen生成的图片和照片一样,效果非常好,很多电影里有用到,所以下载免费版尝试一下 ...

  7. Windows 8 Directx 开发学习笔记(十四)使用几何着色器实现三角形细分

    几何着色器是从DirectX 10才引入的着色器,是一个可选阶段,位于顶点着色器和像素着色器阶段之间.顶点着色器以顶点作为输入数据,而几何着色器以完整的图元作为输入数据,像点.直线.三角形等.之所以引 ...

  8. Windows 8 Directx 开发学习笔记(十二)利用混合实现浮在水面的木箱

    在场景中绘制多个不透明物体时很简单,哪个物体离得近,看到的就是哪个物体.但如果加入一个透明的物体,像玻璃,如何渲染就有些麻烦.拿一块红色的玻璃挡住眼睛,看到的物体都偏红,换成蓝色的玻璃,物体都偏蓝.D ...

  9. Windows 8 Directx 开发学习笔记(十)纹理贴图实现旋转的木箱

    纹理贴图映射(texturemapping)是可以显著提高场景细节和真实感的一种技术,基本原理是将图像数据映射到3D三角形表面(之前的文章提到过,三维模型其实是由很多个三角形拼接而成).当使用纹理资源 ...

最新文章

  1. 选择Scrum看板工具的七点特征
  2. zxing android最新下载,Zxing简单集成
  3. 如何学习微信公众平台的开发?
  4. 第二届(2009年)中国信息技术应用学术研讨会征文延期通知
  5. java监听机制_详解java的事件监听机制和观察者设计模式
  6. preview窗口 unity_Unity3D在Preview中打印日志的方法
  7. iOS中UIKit继承结构
  8. SSM项目使用junit单元测试时Mybatis通配符加载Mapper不能正常加载
  9. MySql表名的大小写问题
  10. 在 Linux 上如何清除内存的 Cache、Buffer 和交换空间
  11. 最短路径问题---SPEA算法详解
  12. koa2入门之使用koa-generator生成koa2项目
  13. 欧几里得、扩展的欧几里得算法
  14. 专访黄文斌丨中专文凭的他,辞掉了9年的国企“铁饭碗”
  15. 关于Python爬虫之获取海量表情包+存入数据库+搭建网站通过关键字查询表情包
  16. 知识丨软件定义汽车的价值
  17. Learning Pyramid-Context Encoder Network for High-Quality Image Inpainting论文理解
  18. latex调整毕业论文目录中章节之间的间距
  19. python推荐算法课程_Python数据科学:全栈技术详解4-推荐算法
  20. matlab双对数纵坐标,双对数坐标的两个坐标轴上的数值是什么样的对应关系?

热门文章

  1. 华为机试HJ87:密码强度等级
  2. OpenCV-怀旧色滤镜
  3. 模板之家php mysql_PHP+APACHE+MYSQL安装方法
  4. 循环下标_【转】【Python效率】五种Pandas循环方法效率对比
  5. dijkstra邻接表_[力扣743] 带权邻接表的单源最短路
  6. echarts 柱状图 不要平均占据_扎心!工资跑不赢房价!平均薪资4127元/月,配得上8字头的九江?...
  7. java filesearcher_Java Object Searcher | java内存对象搜索辅助工具
  8. mooc上c语言怎么改作业,C语言的编写、运行和修复代码
  9. docker mysql 漂移_Centos7系统Docker环境下Mysql部署
  10. Ecilpse常用快捷键总结