什么是D3D?
D3D全称Direct X 3D,即一组API可以用来针对GPU编程,不过他最主要的作用是用来渲染(不过现在也有很多其他应用比如d3d11va[Direct X 3D 11 Video API]用来进行硬件加速解码)
Tips:Direct X 3D主要用来渲染,既然我们说到可以针对GPU编程了,当然不只是渲染的工作可以交给GPU做,一些比较繁杂的计算比如排个两亿个数的序我们仍然可以交给GPU完成,所以推出一项技术就叫做GPGPU(General-purpose computing on graphics processing units)通用GPU计算,由此而衍生的技术便是CUDA,OpenCL等技术
此处只是给小伙伴扩充一下知识面,本文并不会涉及到这些GPGPU技术
有些小伙伴可能对渲染没什么概念,我举一个例子,比如你的显示屏是1920*1080的分辨率,那么你只需要指定好1920*1080个像素点中的每一个像素点的颜色,即可以让显示器帮你显示一张图片
那么问题来了 我刚刚提到的1920*1080个像素点的颜色应该怎么计算出来呢?
答案就是由GPU或者CPU计算得出
比如你现在正在使用一个显示屏看我的文章,你的屏幕上的每一个像素点就是由你的CPU或者GPU算出的(非游戏等需要高性能渲染的地方,通常是用CPU算出每一个像素点的颜色)
那我们平常编写的代码比如你写的C++代码是不能够直接控制GPU的,因为你编译好的可执行文件里存放的全是对应CPU指令集架构的指令,而不是GPU的指令,所以GPU并不能执行你的C++代码
所以我们需要一个桥梁啊,可以让我们的代码能够直接控制GPU, 让我们能够把数据和控制这些数据的操作上传到GPU给我们渲染,所以微软和各家显卡厂商合作一起推出了Windows上的渲染API Direct X 3D,并且规定了很多渲染过程中会使用到的东西 比如纹理,交换链,缓冲区,命令队列等等渲染图像会使用到的东西
当然也不止微软在做这个渲染API,常用的渲染API还有OpenGL,Vulkan等等
那么问题就来到了 Direct X 3D中已经规定好的东西有哪些东西呢?
纹理(texture)
纹理又可以分为2D 纹理和3D 纹理,如果读者接触过c++ 那么可以直接把2D 纹理当做一个二维数组,如下代码所示
#define DATATYPE int
vector<vector<DATATYPE>> 2D_texture;
而3D 纹理则是如下形式
#define DATATYPE int
vector<vector<vector<DATATYPE>>> 3D_texture;
Tips:任何颜色都可以通过三基色(RGB,R[红色],G[绿色],B[蓝色])混合得到,所以对于任意像素点,只要我们知道了他的红色究竟有多红,绿色究竟有多绿,蓝色究竟有多蓝,即可以组合成世界上所有的颜色.
基于2D 纹理的这种二维数组形式,所以我们经常用2D 纹理来存储一张图片,比如一张图片是1920*1080的分辨率,每个像素点是由RGBA来构成那么2D 纹理便可以表示为如下这种格式
struct pixel_color
{
unsigned char RED;//0-255 数字越大表示越红
unsigned char GREEN;
unsigned char BLUE;
unsigned char ALPHA;//数字越大表示越不透明
}
#define DATATYPE pixel_color
vector<vector<DATATYPE>> 2D_texture(1920,vector<DATATYPE>(1080));
那么对于任意2D_texture[i][j]中的元素便是表示位于(i,j)这个点的像素颜色是什么
对于3D纹理也是一样的原理,读者可以自由扩展
那么问题来了 D3D中纹理支持的DATATYPE(也就是我们上文中二维数组可以装载的数据类型)有哪些呢?
typedef enum DXGI_FORMAT
{
DXGI_FORMAT_UNKNOWN = 0,
DXGI_FORMAT_R32G32B32A32_TYPELESS = 1,
DXGI_FORMAT_R32G32B32A32_FLOAT = 2,
DXGI_FORMAT_R32G32B32A32_UINT = 3,
DXGI_FORMAT_R32G32B32A32_SINT = 4,
DXGI_FORMAT_R32G32B32_TYPELESS = 5,
DXGI_FORMAT_R32G32B32_FLOAT = 6,
DXGI_FORMAT_R32G32B32_UINT = 7,
DXGI_FORMAT_R32G32B32_SINT = 8,
DXGI_FORMAT_R16G16B16A16_TYPELESS = 9,
DXGI_FORMAT_R16G16B16A16_FLOAT = 10,
DXGI_FORMAT_R16G16B16A16_UNORM = 11,
DXGI_FORMAT_R16G16B16A16_UINT = 12,
DXGI_FORMAT_R16G16B16A16_SNORM = 13,
DXGI_FORMAT_R16G16B16A16_SINT = 14,
DXGI_FORMAT_R32G32_TYPELESS = 15,
DXGI_FORMAT_R32G32_FLOAT = 16,
DXGI_FORMAT_R32G32_UINT = 17,
DXGI_FORMAT_R32G32_SINT = 18,
DXGI_FORMAT_R32G8X24_TYPELESS = 19,
DXGI_FORMAT_D32_FLOAT_S8X24_UINT = 20,
DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS = 21,
DXGI_FORMAT_X32_TYPELESS_G8X24_UINT = 22,
DXGI_FORMAT_R10G10B10A2_TYPELESS = 23,
DXGI_FORMAT_R10G10B10A2_UNORM = 24,
DXGI_FORMAT_R10G10B10A2_UINT = 25,
DXGI_FORMAT_R11G11B10_FLOAT = 26,
DXGI_FORMAT_R8G8B8A8_TYPELESS = 27,
DXGI_FORMAT_R8G8B8A8_UNORM = 28,
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29,
DXGI_FORMAT_R8G8B8A8_UINT = 30,
DXGI_FORMAT_R8G8B8A8_SNORM = 31,
DXGI_FORMAT_R8G8B8A8_SINT = 32,
DXGI_FORMAT_R16G16_TYPELESS = 33,
DXGI_FORMAT_R16G16_FLOAT = 34,
DXGI_FORMAT_R16G16_UNORM = 35,
DXGI_FORMAT_R16G16_UINT = 36,
DXGI_FORMAT_R16G16_SNORM = 37,
DXGI_FORMAT_R16G16_SINT = 38,
DXGI_FORMAT_R32_TYPELESS = 39,
DXGI_FORMAT_D32_FLOAT = 40,
DXGI_FORMAT_R32_FLOAT = 41,
DXGI_FORMAT_R32_UINT = 42,
DXGI_FORMAT_R32_SINT = 43,
DXGI_FORMAT_R24G8_TYPELESS = 44,
DXGI_FORMAT_D24_UNORM_S8_UINT = 45,
DXGI_FORMAT_R24_UNORM_X8_TYPELESS = 46,
DXGI_FORMAT_X24_TYPELESS_G8_UINT = 47,
DXGI_FORMAT_R8G8_TYPELESS = 48,
DXGI_FORMAT_R8G8_UNORM = 49,
DXGI_FORMAT_R8G8_UINT = 50,
DXGI_FORMAT_R8G8_SNORM = 51,
DXGI_FORMAT_R8G8_SINT = 52,
DXGI_FORMAT_R16_TYPELESS = 53,
DXGI_FORMAT_R16_FLOAT = 54,
DXGI_FORMAT_D16_UNORM = 55,
DXGI_FORMAT_R16_UNORM = 56,
DXGI_FORMAT_R16_UINT = 57,
DXGI_FORMAT_R16_SNORM = 58,
DXGI_FORMAT_R16_SINT = 59,
DXGI_FORMAT_R8_TYPELESS = 60,
DXGI_FORMAT_R8_UNORM = 61,
DXGI_FORMAT_R8_UINT = 62,
DXGI_FORMAT_R8_SNORM = 63,
DXGI_FORMAT_R8_SINT = 64,
DXGI_FORMAT_A8_UNORM = 65,
DXGI_FORMAT_R1_UNORM = 66,
DXGI_FORMAT_R9G9B9E5_SHAREDEXP = 67,
DXGI_FORMAT_R8G8_B8G8_UNORM = 68,
DXGI_FORMAT_G8R8_G8B8_UNORM = 69,
DXGI_FORMAT_BC1_TYPELESS = 70,
DXGI_FORMAT_BC1_UNORM = 71,
DXGI_FORMAT_BC1_UNORM_SRGB = 72,
DXGI_FORMAT_BC2_TYPELESS = 73,
DXGI_FORMAT_BC2_UNORM = 74,
DXGI_FORMAT_BC2_UNORM_SRGB = 75,
DXGI_FORMAT_BC3_TYPELESS = 76,
DXGI_FORMAT_BC3_UNORM = 77,
DXGI_FORMAT_BC3_UNORM_SRGB = 78,
DXGI_FORMAT_BC4_TYPELESS = 79,
DXGI_FORMAT_BC4_UNORM = 80,
DXGI_FORMAT_BC4_SNORM = 81,
DXGI_FORMAT_BC5_TYPELESS = 82,
DXGI_FORMAT_BC5_UNORM = 83,
DXGI_FORMAT_BC5_SNORM = 84,
DXGI_FORMAT_B5G6R5_UNORM = 85,
DXGI_FORMAT_B5G5R5A1_UNORM = 86,
DXGI_FORMAT_B8G8R8A8_UNORM = 87,
DXGI_FORMAT_B8G8R8X8_UNORM = 88,
DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM = 89,
DXGI_FORMAT_B8G8R8A8_TYPELESS = 90,
DXGI_FORMAT_B8G8R8A8_UNORM_SRGB = 91,
DXGI_FORMAT_B8G8R8X8_TYPELESS = 92,
DXGI_FORMAT_B8G8R8X8_UNORM_SRGB = 93,
DXGI_FORMAT_BC6H_TYPELESS = 94,
DXGI_FORMAT_BC6H_UF16 = 95,
DXGI_FORMAT_BC6H_SF16 = 96,
DXGI_FORMAT_BC7_TYPELESS = 97,
DXGI_FORMAT_BC7_UNORM = 98,
DXGI_FORMAT_BC7_UNORM_SRGB = 99,
DXGI_FORMAT_AYUV = 100,
DXGI_FORMAT_Y410 = 101,
DXGI_FORMAT_Y416 = 102,
DXGI_FORMAT_NV12 = 103,
DXGI_FORMAT_P010 = 104,
DXGI_FORMAT_P016 = 105,
DXGI_FORMAT_420_OPAQUE = 106,
DXGI_FORMAT_YUY2 = 107,
DXGI_FORMAT_Y210 = 108,
DXGI_FORMAT_Y216 = 109,
DXGI_FORMAT_NV11 = 110,
DXGI_FORMAT_AI44 = 111,
DXGI_FORMAT_IA44 = 112,
DXGI_FORMAT_P8 = 113,
DXGI_FORMAT_A8P8 = 114,
DXGI_FORMAT_B4G4R4A4_UNORM = 115,
DXGI_FORMAT_P208 = 130,
DXGI_FORMAT_V208 = 131,
DXGI_FORMAT_V408 = 132,
DXGI_FORMAT_SAMPLER_FEEDBACK_MIN_MIP_OPAQUE = 189,
DXGI_FORMAT_SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE = 190,
DXGI_FORMAT_FORCE_UINT = 0xffffffff
} DXGI_FORMAT;
常用的DATATYPE有这些:
交换链(swap chain)和缓冲区置换(也叫页面翻转或presenting)
上文我们说到,渲染所做的事情就是确定我们1080P屏幕上的每一个像素点的颜色,那么如果把我们的屏幕需要的数据比作一个2D 纹理(上文我们说到2D纹理其实就是2维数组)
1080P也就是分辨率为1920*1080
struct pixel_color
{
unsigned char RED;//0-255 数字越大表示越红
unsigned char GREEN;
unsigned char BLUE;
unsigned char ALPHA;//数字越大表示越不透明
}
#define DATATYPE pixel_color
vector<vector<DATATYPE>> screen_2D_texture(1920,vector<DATATYPE>(1080));
那么我们便是要填充screen_2D_texture这个数组,把他填充满,然后交给显示器就行了,显示器就可以显示一张图片给用户看
那么问题就来到了 当我们把screen_2D_texture这个数组交给显示器的时候,我们是不是也应该继续填充下一个screen_2D_texture,这样才能继续显示下一帧,问题就在于screen_2D_texture这个数组目前是显示屏要获取资源的数组,我们总不能直接把下一帧的数组又写进同一个数组吧?如果写进同一个数组,那么显示屏在读,GPU在写,那么显示屏显示的图片不就乱码了嘛?
所以我们需要多个screen_2D_texture,当数组1交给显示器读的时候,我们只需要把下一帧写进数组2即可,当数组1被读完后,我们把数组2交给显示器读,我们继续把下一帧写进数组1,那么这样交叉的读写便形成了交换链的概念
上文中的技术可以用这张图片表示:
其中的前台缓冲区便是数组1,后台缓冲区便是数组2,而这种交换的操作,我们又叫他页面翻转或者是presenting(呈现)
使用两个数组 我们便叫做双重缓冲 如果使用三个数组 我们便叫做三重缓冲
深度缓冲(depth buffer)
在d3d渲染流程中,我们最主要的工作就是把三维场景里的所有数据变成我们刚刚提到的后台缓冲区里的数据 也就是把三维物体渲染到二维平面上来 并且确定他渲染到屏幕上之后 对应像素点的颜色
比如上图中的观察窗口 观察窗口中的像素数据便是我们需要的二维平面的数据 我们从观察点出发
关于观察点这些内容是计算机图形学中的内容,如果想详细了解 请移步至:
GAMES101-现代计算机图形学入门-闫令琪_哔哩哔哩_bilibili
如果读者玩过第一人称游戏就会知道 你的观察点就是从人物的头部进行观察
而第三人称游戏则是从人物背后的天空进行观察
如现代游戏中两种视角的不同(第一人称,第三人称) 仅仅只是因为观察点发生了变化,但是三维空间中的所有三维物体本身并不会发生变化
现在我们继续回到这幅图中,因为我们从目前的观察点进行观察 会发生什么情况呢 球体上的P1点 圆锥体的P2点 圆柱体的P3点均想把自己的颜色渲染到真正的屏幕数据 P点上,那么我们如何知道究竟应该取P1的颜色还是P2的颜色还是P3的颜色呢?
可能有些小伙伴会说 你直接看谁离的近不就完事了嘛?
计算机确实是取最近的点 但是记住 计算机可没有人的眼睛 他可不能看出三维空间中哪一个三维物体离他近 只能你计算出每个物体的深度后再告诉他才行
但是目前问题就来了 我们看侧面图 并且给出三个物体的深度d1 d2 d3
这个时候,当你告诉计算机 d1 d2 d3后 计算机当然可以立马得出是P1点的颜色应该被采用 但是问题就在于 在渲染这三个三维物体的时候 并不是一次性就全部渲染到三维空间中 而是一个物体一个物体的渲染 比如先渲染球,再渲染圆锥体,再渲染圆柱体
那这个时候 你是不是就需要一个记录当前像素点的最近深度是多少? 我举一个例子
如果你是先渲染圆锥体 然后圆柱体 然后球体
那么渲染圆锥的时候 P点的目前距离最近的深度就应该是d2 再渲染圆柱的时候 因为d3大于d2所以P3点不会被渲染到P点上 轮到球体的时候 d1<d2 那么P1点可以绘制到P点上 并且更新最小深度值为d1
这个过程我们叫做深度测试
从管擦点出发 我们能够观察到的深度值范围为0-1
对于1平面 他的深度便是0 对于2平面 他的深度便是1
所以现在我们推导一下先渲染圆柱体 再渲染球体 最后渲染圆锥体的流程来加深我们的记忆
操作步骤 | P的值 | 目前最小的深度值min_d=1 | 步骤描述 |
绘制圆柱体 | P3 | d3 | 因为d3<1.0=min_d,深度测试通过,更新P,更新min_d=d3 |
绘制球体 | P1 | d1 | 因为d1<min_d=d3,深度测试通过,更新P,更新min_d=d1 |
绘制圆柱体 | P1 | d1 | 因为d2>min_d=d1,深度测试不通过,不更新 |
最终确定P值 | P1 | d1 | 交给显示器渲染 |
所以我们的深度缓冲也是一块2D 纹理,并且其中的元素应该和我们的后台缓冲区一一对应
比如screen_2D_texture[i][j]就应该对应depth_2D_texture[i][j]
当我们需要去取得(i,j)像素点目前的最小深度的时候,我们直接去depth_2Dtexture[i][j]中获取即可