DX11 天空盒&第一人称下拾取、破坏、放置
目录
题目&要求
•绘制天空盒,天空盒纹理自己到网上找(或自己想办法制作),不能使用博客项目中已经有的天空盒
•实现第一个第一人称摄像机,并且实现在第一人称下拾取、破坏、放置方块(Minecraft)
整体思路
1、首先了解什么是立方体映射(Cube Mapping)&环境映射(Environment Maps)
-
立方体映射(Cube Mapping):
-
-
一个立方体(通常是正方体)包含六个面,对于立方体映射来说,它的六个面对应的是六张纹理贴图,然后以该立方体建系,中心为原点,且三个坐标轴是轴对齐的。我们可以使用方向向量(±X,±Y,±Z),从原点开始,发射一条射线(取方向向量的方向)来与某个面产生交点,取得该纹理交点对应的颜色。
注意:
- 方向向量的大小并不重要,只要方向一致,那么不管长度是多少,最终选择的纹理和取样的像素都是一致的。
- 使用方向向量时要确保所处的坐标系和立方体映射所处的坐标系一致,如方向向量和立方体映射同时处在世界坐标系中。
- Direct3D提供了枚举类型
D3D11_TEXTURECUBE_FACE
来标识立方体某一表面:
typedef enum D3D11_TEXTURECUBE_FACE { D3D11_TEXTURECUBE_FACE_POSITIVE_X = 0,//索引0指向+X表面; D3D11_TEXTURECUBE_FACE_NEGATIVE_X = 1,//索引1指向-X表面; D3D11_TEXTURECUBE_FACE_POSITIVE_Y = 2,//索引2指向+Y表面; D3D11_TEXTURECUBE_FACE_NEGATIVE_Y = 3,//索引3指向-Y表面; D3D11_TEXTURECUBE_FACE_POSITIVE_Z = 4,//索引4指向+Z表面; D3D11_TEXTURECUBE_FACE_NEGATIVE_Z = 5 //索引5指向-Z表面; } D3D11_TEXTURECUBE_FACE;
- 使用立方体映射意味着我们需要使用3D纹理坐标进行寻址。在HLSL中就是立方体纹理TextureCube来表示的。
Sky.hlsli TextureCube g_TexCube : register(t0); SamplerState g_Sam : register(s0); cbuffer CBChangesEveryFrame : register(b0) { matrix g_WorldViewProj; } struct VertexPos { float3 PosL : POSITION; }; struct VertexPosHL { float4 PosH : SV_POSITION; float3 PosL : POSITION; }; Sky_PS.hlsl #include "Sky.hlsli" float4 PS(VertexPosHL pIn) : SV_Target { return g_TexCube.Sample(g_Sam, pIn.PosL); }
-
-
环境映射(Environment Maps):
-
- 简单来说,环境映射就是在立方体表面的纹理中存储了周围环境的图像。
2、使用DXTex构建天空盒
-
当然有三种方法
-
1、已经创建好的.dds文件,可以直接通过
DDSTextureLoader
读取使用 //沙漠的天空盒 -
2、6张天空盒的正方形贴图,格式不限。(暂不考虑只有5张的) //日落的天空盒
-
3、1张天空盒贴图,包含了6个面,格式不限,图片宽高比为4:3 //白天的天空盒
-
X_jun师兄那里有详细的介绍,这里不多说
-
bool GameApp::InitResource() //白天的天空盒,这里是我的作业的天空盒,利用第三种方法,就是自己找一个新的图片 m_pDaylight = std::make_unique<SkyRender>(); HR(m_pDaylight->InitResource(m_pd3dDevice.Get(), m_pd3dImmediateContext.Get(), L"..\\Texture\\kkk.jpeg", 5000.0f)); //日落的天空盒 m_pSunset = std::make_unique<SkyRender>(); HR(m_pSunset->InitResource(m_pd3dDevice.Get(), m_pd3dImmediateContext.Get(), std::vector<std::wstring>{ L"..\\Texture\\sunset_posX.bmp", L"..\\Texture\\sunset_negX.bmp", L"..\\Texture\\sunset_posY.bmp", L"..\\Texture\\sunset_negY.bmp", L"..\\Texture\\sunset_posZ.bmp", L"..\\Texture\\sunset_negZ.bmp", }, 5000.0f)); //这里沙漠就是这样处理的,里面是包含有六张图片的。 m_pDesert = std::make_unique<SkyRender>(); HR(m_pDesert->InitResource(m_pd3dDevice.Get(), m_pd3dImmediateContext.Get(), L"..\\Texture\\desertcube1024.dds", 5000.0f));
-
//对于创建好的DDS立方体纹理,我们只需要使用DDSTextureLoader就可以很方便地读取进来: CreateDDSTextureFromFile( device.Get(), cubemapFilename.c_str(), nullptr, textureCubeSRV.GetAddressOf());//沙漠图片的读取方法 // 根据给定的一张包含立方体六个面的位图,创建纹理立方体 // 要求纹理宽高比为4:3,且按下面形式布局: // . +Y . . // -X +Z +X -Z // . -Y . . // [In]d3dDevice D3D设备 // [In]d3dDeviceContext D3D设备上下文 // [In]cubeMapFileName 位图文件名 // [OutOpt]textureArray 输出的纹理数组资源 // [OutOpt]textureCubeView 输出的纹理立方体资源视图 // [In]generateMips 是否生成mipmaps HRESULT CreateWICTexture2DCubeFromFile( ID3D11Device * d3dDevice, ID3D11DeviceContext * d3dDeviceContext, const std::wstring& cubeMapFileName, ID3D11Texture2D** textureArray, ID3D11ShaderResourceView** textureCubeView, bool generateMips = false); //白天图片的读取方法 // 根据按D3D11_TEXTURECUBE_FACE索引顺序给定的六张纹理,创建纹理立方体 // 要求位图是同样宽高、数据格式的正方形 // 你也可以给定超过6张的纹理,然后在获取到纹理数组的基础上自行创建更多的资源视图 // [In]d3dDevice D3D设备 // [In]d3dDeviceContext D3D设备上下文 // [In]cubeMapFileNames 位图文件名数组 // [OutOpt]textureArray 输出的纹理数组资源 // [OutOpt]textureCubeView 输出的纹理立方体资源视图 // [In]generateMips 是否生成mipmaps HRESULT CreateWICTexture2DCubeFromFile( ID3D11Device * d3dDevice, ID3D11DeviceContext * d3dDeviceContext, const std::vector<std::wstring>& cubeMapFileNames, ID3D11Texture2D** textureArray, ID3D11ShaderResourceView** textureCubeView, bool generateMips = false); //日落的读取方法
-
也可以自己编写代码来构造立方体纹理
3、纹理都准备好了,那就绘制天空盒
- 绘制天空盒需要以下准备工作:
-
将天空盒载入HLSL的TextureCube中
-
在光栅化阶段关闭背面消隐(正面是球面向外,但摄像机在球内)
目的:从内部观察时不会导致背面消隐
3、在输出合并阶段的深度/模板状态,设置深度比较函数为小于等于,以允许深度值为1的像素绘制
因为:该状态用于绘制天空盒,因为深度值为1.0时默认无法通过深度测试
void SkyEffect::SetRenderDefault(ID3D11DeviceContext * deviceContext) { deviceContext->IASetInputLayout(pImpl->m_pVertexPosLayout.Get()); deviceContext->VSSetShader(pImpl->m_pSkyVS.Get(), nullptr, 0); deviceContext->PSSetShader(pImpl->m_pSkyPS.Get(), nullptr, 0); deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); deviceContext->GSSetShader(nullptr, nullptr, 0); //光栅化阶段关闭背面消隐 deviceContext->RSSetState(RenderStates::RSNoCull.Get()); deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf()); //设置深度比较函数为小于等于,以允许深度值为1的像素绘制 deviceContext->OMSetDepthStencilState(RenderStates::DSSLessEqual.Get(), 0); deviceContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF); }
功能模块分析&代码
1、第一人称摄像机
这个区别与自由摄像机,就是两种情况,自由模式会按照鼠标方向走(可飞),第一人称摄像机就是走了,没有y的坐标移动,在水平面是移动。
void GameApp::UpdateScene(float dt)
if (m_KeyboardTracker.IsKeyPressed(Keyboard::E) || m_CameraMode == CameraMode::Free)
{
//自由摄像机的操作
m_CameraMode = CameraMode::Free;
if (keyState.IsKeyDown(Keyboard::W))
cam1st->MoveForward(dt * 6.0f);
if (keyState.IsKeyDown(Keyboard::S))
cam1st->MoveForward(dt * -6.0f);
if (keyState.IsKeyDown(Keyboard::A))
cam1st->Strafe(dt * -6.0f);
if (keyState.IsKeyDown(Keyboard::D))
cam1st->Strafe(dt * 6.0f);
}
if (m_KeyboardTracker.IsKeyPressed(Keyboard::Q)|| m_CameraMode ==CameraMode::FirstPerson)
{
// 第一人称摄像机
m_CameraMode = CameraMode::FirstPerson;
if (keyState.IsKeyDown(Keyboard::W))
cam1st->Walk(dt * 6.0f);
if (keyState.IsKeyDown(Keyboard::S))
cam1st->Walk(dt * -6.0f);
if (keyState.IsKeyDown(Keyboard::A))
cam1st->Strafe(dt * -6.0f);
if (keyState.IsKeyDown(Keyboard::D))
cam1st->Strafe(dt * 6.0f);
}
2、拾取功能
这里主要说一下核心思想:
因为我们所能观察到的3D对象都处于视锥体的区域,而且又已经知道摄像机所在的位置。因此在屏幕上选取一点可以理解为从摄像机发出一条射线,然后判断该射线是否与场景中视锥体内的物体相交。若相交,则说明选中了该对象。
当然,有时候射线会经过多个对象,这个时候我们就应该选取距离最近的物体。
一个3D对象的顶点原本是位于局部坐标系的,然后经历了世界变换、观察变换、投影变换后,会来到NDC空间中,可视物体的深度值(z值)通常会处于0.0到1.0之间。而在NDC空间的坐标点还需要经过视口变换,才会来到最终的屏幕坐标系。在该坐标系中,坐标原点位于屏幕左上角,x轴向右,y轴向下,其中x和y的值指定了绘制在屏幕的位置,z的值则用作深度测试。而且从NDC空间到屏幕坐标系的变换只影响x和y的值,对z值不会影响。
而现在我们要做的,就是将选中的2D屏幕点按顺序进行视口逆变换、投影逆变换和观察逆变换,让其变换到世界坐标系并以摄像机位置为射线原点,构造出一条3D射线,最终才来进行射线与物体的相交。在构造屏幕一点的时候,将z值设为0.0即可。z值的变动,不会影响构造出来的射线,相当于在射线中前后移动而已。
代码方面请看鼠标拾取
作业代码实现:
Ray ray = Ray::ScreenToRay(*m_pCamera, (float)m_ClientWidth/2, (float)m_ClientHeight/2);
其中静态方法Ray::ScreenToRay
执行的正是鼠标拾取中射线构建的部分,其实现灵感来自于DirectX::XMVector3Unproject
函数,它通过给定在屏幕坐标系上的一点、视口属性、投影矩阵、观察矩阵和世界矩阵,来进行逆变换,得到在物体坐标系的位置:
那好,我以摄像机得到的视口的中心点构造出来的射线,((float)m_ClientWidth/2, (float)m_ClientHeight/2)
否则它原来的鼠标相对偏移量,这里好像是获取不到的(屏幕根本就看不见鼠标在哪里)那是因为
enum Mode
{
MODE_ABSOLUTE = 0, // 绝对坐标模式,每次状态更新xy值为屏幕像素坐标,且鼠标可见
MODE_RELATIVE, // 相对运动模式,每次状态更新xy值为每一帧之间的像素位移量,且鼠标不可见
};
// 初始化鼠标,键盘不需要
m_pMouse->SetWindow(m_hMainWnd);
m_pMouse->SetMode(DirectX::Mouse::MODE_RELATIVE);
所以不是屏幕像素坐标,那就不行,导致在只能屏幕左上角(0,0)指向物体才能拾取
//GameApp.h中实现
std::wstring m_PickedObjStr; // 已经拾取的对象名
DirectX::BoundingSphere m_BoundingSphere; // 球的包围盒
void GameApp::UpdateScene(float dt)
m_PickedObjStr = L"无";
Ray ray = Ray::ScreenToRay(*m_pCamera, 640.0f, 360.0f);
bool hitObject = false;
if (ray.Hit(m_Sphere.GetBoundingOrientedBox()))
{
m_PickedObjStr = L"球体";
hitObject = true;
}
else if (ray.Hit(m_Box[0].GetBoundingOrientedBox()))
{
j = 0; //用于下面的指向销毁
m_PickedObjStr = L"立方体";
hitObject = true;
}
else if (ray.Hit(m_Box[1].GetBoundingOrientedBox()))
{
j = 1;
m_PickedObjStr = L"立方体";
hitObject = true;
}
else if (ray.Hit(m_Box[2].GetBoundingOrientedBox()))
{
j = 2;
m_PickedObjStr = L"立方体";
hitObject = true;
}
else if (ray.Hit(m_Cylinder.GetBoundingOrientedBox()))
{
m_PickedObjStr = L"圆柱体";
hitObject = true;
}
else
{
//没有指向任何东西
j = 3;
}
bool GameApp::InitResource()
//预先设好包围球
m_BoundingSphere.Center = XMFLOAT3(0.0f, 0.0f, 0.0f);
m_BoundingSphere.Radius = 1.0f;
3、破坏、放置方块(Minecraft)
问题1、这里就是有点问题的了,其实破坏好像也不算是破坏吧,我只是让方块变成(0,0,0),然后位置放到-10.0f,让拾取不了。
当然可以像我的世界一样,放多个方块,这里我指定3了。
破坏呢,我是让其拾取到,然后拿到这个方块的下标,就可以指定破坏了
问题2、还有一个问题,在放置方块的时候,大幅度移动视角,鼠标可能会出来,然后放置、破坏方块是实现不了的,请好好看看鼠标的位置是否出来了。
好了,请看代码
//GameApp.h中实现
GameObject m_Box[4]; // 正方体,4个呢,是因为最后面一个 //是用来,在没有指向正方块时的破坏不了那3个的。
//也可以不让破坏代码运行实现
#include "GameApp.h"
#include "d3dUtil.h"
#include "DXTrace.h"
using namespace DirectX;
int i = 0,j = 4; //全局变量
void GameApp::UpdateScene(float dt)
//放置物块
if (m_MouseTracker.leftButton == Mouse::ButtonStateTracker::PRESSED)
{
Model mode2;
mode2.SetMesh(m_pd3dDevice.Get(), Geometry::CreateBox());
mode2.modelParts[0].material.ambient = XMFLOAT4(0.2f, 0.2f, 0.2f, 1.0f);
mode2.modelParts[0].material.diffuse = XMFLOAT4(0.8f, 0.8f, 0.8f, 1.0f);
mode2.modelParts[0].material.specular = XMFLOAT4(0.2f, 0.2f, 0.2f, 16.0f);
mode2.modelParts[0].material.reflect = XMFLOAT4();
HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(),
L"..\\Texture\\floor.dds",
nullptr,
mode2.modelParts[0].texDiffuse.GetAddressOf()));
m_Box[i].SetModel(std::move(mode2));
m_Box[i].GetTransform().SetPosition(m_pCamera->GetLookAxis().x * 8.0f + m_pCamera->GetPosition().x,
m_pCamera->GetLookAxis().y * 8.0f + m_pCamera->GetPosition().y,
m_pCamera->GetLookAxis().z * 8.0f + m_pCamera->GetPosition().z ); //摄像机方向向量+摄像机位置 = 任意位置放置方块
i++;
if (i >= 3)
i = 0; //不回来会报错的
}
//销毁物块
if (m_MouseTracker.rightButton == Mouse::ButtonStateTracker::PRESSED)
{
Model mode2;
mode2.SetMesh(m_pd3dDevice.Get(), Geometry::CreateBox(0.0f,0.0f,0.0f));
m_Box[j].SetModel(std::move(mode2));
m_Box[j].GetTransform().SetPosition(m_pCamera->GetPosition().x, -10.0f, m_pCamera->GetPosition().z + 5.0f);
i = j;//破坏了这个,那么回到这个下标,可以继续创建
}
void GameApp::DrawScene()
m_Box[0].Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Box[1].Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Box[2].Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
其他:
// 地面
//model.SetMesh(m_pd3dDevice.Get(), Geometry::CreatePlane(XMFLOAT2(10.0f, 10.0f), XMFLOAT2(5.0f, 5.0f)));
model.SetMesh(m_pd3dDevice.Get(), Geometry::CreatePlane(XMFLOAT2(20.0f, 20.0f), XMFLOAT2(5.0f, 5.0f)));//可以扩大一下
model.modelParts[0].material.ambient = XMFLOAT4(0.2f, 0.2f, 0.2f, 1.0f);
model.modelParts[0].material.diffuse = XMFLOAT4(0.8f, 0.8f, 0.8f, 1.0f);
model.modelParts[0].material.specular = XMFLOAT4(0.2f, 0.2f, 0.2f, 16.0f);
model.modelParts[0].material.reflect = XMFLOAT4();
// ******************
// 初始化摄像机
//
auto camera = std::shared_ptr<FirstPersonCamera>(new FirstPersonCamera);
m_pCamera = camera;
camera->SetViewPort(0.0f, 0.0f, (float)m_ClientWidth, (float)m_ClientHeight);
camera->SetFrustum(XM_PI / 3, AspectRatio(), 1.0f, 1000.0f);
//camera->LookTo(XMFLOAT3(0.0f, 0.0f, -10.0f), XMFLOAT3(0.0f, 0.0f, 1.0f), XMFLOAT3(0.0f, 1.0f, 0.0f));
camera->LookTo(XMFLOAT3(0.0f, 0.0f, -5.0f), XMFLOAT3(0.0f, 0.0f, 1.0f), XMFLOAT3(0.0f, 1.0f, 0.0f));//第一人称,近一点好
实现:
思考&小结
- 看见师兄表扬别人写博客,我也想尝试一下了,之前没有知道可以这样写的,现在就拿这一次来试试啦。嗯…来说说这次作业遇见的一些问题吧。主要来说,这次作业难点是在拾取那个地方,因为鼠标拾取这篇的代码是有点复杂的,我尝试过让鼠标显示出来,可是好像在这篇是不行的。然后便去反复看鼠标拾取这一篇的代码实现,终于也是功夫不负有心人,让我找到了方法。然后呢,我想实现我的世界里面的操作的,指向那里就放方块在哪里,就是上面的问题3了,但是很难在摄像头视口前面的位置,可能我对这还不是很熟练,然后方块会堆积在一起,这个…不像我的世界那样。然后就是纹理图片啦!!!我想个星空的天空盒的,就是找不到,然后去买,发现被坑了…
- 来说这一次的收获吧,总的来说呢是学会了基本天空盒的实现和一些基本的原理,可以说,又是收获满满的的一周吧。已经做了四次作业了,还是这样说吧,从刚刚开始的一脸懵逼,到现在的基本应该可以去慢慢看明白,没有刚刚开始那么的折磨了。
- 差不多也迎来了最后的最终考核了,好好加油努力才行,发现别人写的博客,真的好厉害呀。说实话,大家的是人,为什么你这么牛逼呀呀呀,好羡慕,都是他们努力的结果。压力大,只能再再努力一把了,希望不要被别人拉的太远了。