Direct3D 10教程4:3D空间

概览

在前面的教程中,我们在窗口中央绘制了一个三角形,我们着重讲解如何从顶点缓存中的获取顶点位置。而在本教程中我们将讨论3D位置和变换的细节。

本教程会在屏幕上绘制一个3D物体,上一个教程我们关注如何在屏幕上绘制一个2D对象,现在我们要显示一个3D物体。

程序截图

源代码

(SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial04

3D空间

在前面的教程中,三角形的顶点坐标数据就是它的屏幕坐标,但是,这种情况并不常见。我们需要一个系统用来表示在3D空间中的物体并显示这些物体。

在真实世界中,物体是处在3D空间中的。这意味着要将一个物体放置在世界的指定位置,我们需要使用一个坐标系统并定义位置对应的坐标。在计算机图形学中,3D空间通常使用笛卡尔坐标系。在这个坐标系中,三根轴X,Y和Z互相垂直,定义了空间中的每个点。这个坐标系统分为左手系和右手系。在左手系中,X轴指向右方,Y轴指向上方,Z轴指向前方。在右手系中,X轴和Y轴的指向相同,但Z轴指向后方。

图1. 左手坐标系和右手坐标系

图1. 左手坐标系和右手坐标系

下面讨论一下坐标系统。在不同的空间一个点具有不同的坐标。以一维为例,假设我们有一把尺,P点在5英寸处。如果将尺向右移动1英寸,则P点的坐标变为4。通过移动尺,参照系发生了变化,结果是该点有了一个新的坐标。

图2. 1D空间

图2. 1D空间

在3D中,一个空间是由原点和三个从原点出发的轴定义的。

在计算机图形学中通常使用下列空间:对象空间(object space),世界空间(world space),视空间(view space),投影空间(projection space)和屏幕空间(screen space)。

图3. 定义在对象空间中的立方体

图3. 定义在对象空间中的立方体

对象空间(Object Space)

注意立方体的中心在原点。对象空间又称为模型空间(model space),表示创建3D模型的艺术家使用的空间。通常,艺术家在创建模型时将它的中点放置在原点上,这样做可以很容易地进行诸如旋转之类的变换操作,在下一个教程中我们会讨论到变换操作。立方体的8个顶点坐标如下:


(-1, 1, -1)
( 1, 1, -1)
 (-1, -1, -1)
 ( 1, -1, -1)
 (-1, 1, 1)
 ( 1, 1, 1)
 (-1, -1, 1)
 ( 1, -1, 1)

因为对象空间是艺术家在设计和创建模型时使用的,所以存储在磁盘上的模型也处于对象空间。应用程序可以创建一个顶点缓存代表这个模型并使用模型数据进行缓存的初始化。因此,顶点缓存中的顶点通常也是处于对象空间中的,这样意味着顶点着色器接收的是对象空间中的顶点数据。

世界空间(World Space)

世界空间被场景中的所有对象共享,它用来定义对象的空间联系。要理解世界空间,你可以想象自己站在一个长方体房间的西南角面朝北方。我们定义自己的脚站在原点(0, 0, 0)。X轴在我们右方,Y轴向上,Z轴向前,即我们面朝的方向。这样,房间中的每个位置都可以定义为一组XYZ坐标。例如,有一个椅子在我们前方5英尺,右方2英尺,椅子上方8英尺高处的天花板上有一盏灯,我们可以将椅子的位置定为(2, 0, 5),灯的位置定为(2, 8, 5)。如你所见,世界空间如其名所示,它说明了物体在世界中的位置联系。

视空间(View Space)

视空间,有时也称为相机空间,但是,视空间中的原点是观察者或者相机。观察方向(观察者视线方向)定义了Z轴,由程序定义的“up”方向成为Y轴,如下图所示。

图4. 同一个物体在世界空间(左)和时空间(右)

图4. 同一个物体在世界空间(左)和时空间(右)

左图表示一个由人形对象和观察者(相机)组成的场景。用于世界空间的原点和轴为红色。右图表示对应世界空间的视空间。视空间轴用蓝色表示。为了能说明得更清楚,视空间与世界空间的朝向不同。注意在视空间中,相机朝向Z轴方向。

投影空间(Projection Space)

投影空间表示在视空间上施加了投影变换的空间。在这个空间中,可视内容的X、Y轴坐标范围从-1到1,而Z轴坐标的范围从0到1。

屏幕空间(Screen Space)

屏幕空间常常用来表示在帧缓存中的位置。因为帧缓存通常是一张2D纹理,所以屏幕空间是一个2D空间。左上角为原点坐标为(0, 0)。X轴正方向指向右方,Y轴正方向指向下方。一个宽w像素高h像素的缓存右下角的坐标为(w - 1, h - 1)。

空间之间的变换

变换通常用来将顶点坐标从一个空间转换为另一个。在3D计算机图形学中,图形管道有三种变换:世界变换,视变换和投影变换。诸如平移、旋转和缩放之类的独立变换会在下一个教程中讨论。

世界变换

世界变换将一个顶点从对象空间转换到世界空间,它通常由一个或多个缩放,旋转和平移变换组合而成。场景中的每个对象都有自己的世界变换,这是因为每个对象都有自己的大小、朝向和位置。

视变换

当顶点转换到世界空间之后,视变换就将这些顶点从世界空间转换为视空间。前面我们提到过,视空间就是从观察者(相机)角度观察到的世界。在视空间中,观察者位于原点沿Z轴方向观看。

以观察者为参照系的视空间意义不大,视转换矩阵是施加在顶点上的,而不是施加在观察者上。所以,视矩阵执行的变换与施加在观察者或相机上的变换恰相反。例如,如果你想将相机朝-Z方向移动5个单位,你需要计算出一个视矩阵将顶点沿+Z方法移动5个单位。虽然相机朝后移动,但是从相机的视点看来是顶点朝前移动。Direct3D有一个叫做D3DXMatrixLookAtLH()的方法被常常用来计算视矩阵。我们只需告知相机的位置,观察目标和观察者的向上方向(又称为up矢量),就可以获得一个对应的视矩阵。

投影变换

投影空间将顶点从诸如世界空间和视空间之类的3D空间转换到投影空间。在投影空间中,顶点的X和Y坐标是按X/Z和Y/Z的比例获取的。

图5. 投影

图5. 投影

在3D空间中,物体看起来有透视效果。即近处的物体看起来大一些。如上图所示,离开观察者d距离、高为h的树的顶端和离开观察者2d距离、高为2h的树的顶端的位置是相同的。因此,2D屏幕上的顶点的位置与X/Z和Y/Z直接相关。

有一个定义3D空间的参数称为视场(field-of-view,FOV)。视场表示对一个指定位置、指定观察方向来说,哪些物体是可见的。人类具有一个先前看的视场(我们不能看到自己背后的东西),我们无法看到离我们太近或太远的东西。在计算机图形学中,视场包含在一个视锥体中。视锥体由3D空间中的六个面定义,其中两个面平行于XY平面,它们叫做近裁平面(near-Z plane)远裁平面(far-Z plane)。另外4个平面是根据观察者的水平和竖直视场定义的。视场越大,视锥体越大,观察者看到的物体就越多。

GPU会过滤掉位于视锥体之外的物体,这样就无需绘制不可见的物体了。这个过程称为剪裁(clipping)。视锥体是一个顶部被削去的金字塔。因为是针对视锥体平面进行剪裁,所以这个过程是复杂的,GPU必须根据每个顶点与平面的判断方程进行计算。因此,取而代之的是GPU通常首先进行投影变换,然后对视锥体进行剪裁操作。在视锥体上进行投影变换的结果是金字塔状的视锥体在投影空间中变为一个长方体。这是因为,如上所述,投影空间中的X和Y坐标是基于3D空间中的X/Z和Y/Z的。因此,点a和点b在投影空间中具有相同的X和Y坐标,这就是视锥体变为长方体的原因。

图6. 视锥体

图6. 视锥体

设想有两棵树,它们的顶端都位于视锥体的边缘,d = 2h。顶部边缘的Y坐标在投影空间中就会变为0.5 (因为h/d = 0.5)。因此,任何经过投影变换后的Y值超过0.5的都会被GPU剪裁。这里的问题是0.5取决于由程序定义的垂直视场,不同的FOV值会导致GPU剪裁的值也不同。为了让处理过程更加简单,3D程序通常会缩放经过投影的X和Y值,使之的范围在-1和1之间。换句话说,任何在[-1 1] 区间范围之外的X和Y值会被剪裁。要让这种剪裁模式正确工作,投影矩阵必须对经过投影变换的顶点的X和Y坐标进行缩放,这是通过对h/d或d/h取倒数实现的。d/h也就是FOV半角的余切值。经过缩放,视锥体的顶部变为h/d * d/h = 1。任何超过1的值会被GPU剪裁。这就是我们想要的结果。

同样的方法也用于处理投影空间中的Z值。我们想让近裁平面的Z值为0,远裁平面的Z值为1。当3D空间中的Z值 = 近裁平面的Z值,则投影空间中Z应为0;当3D空间中的Z值=远裁平面的Z值,则投影空间中的Z值应为1。经过以上处理,在[0 1]区间之外的Z值就会被GPU剪裁。

在Direct3D 10中,获取投影矩阵最简单的方法就是调用D3DXMatrixPerspectiveFovLH()方法。我们只需提供4个参数——FOVy,Aspect,Zn和Zf——就会返回一个拥有所需信息的一个矩阵。FOVy是Y方向的视场,Aspect是长宽比,它是视空间的长宽比。基于FOVy和Aspect可以计算FOVx。这个长宽比可以从渲染目标的长宽比获得。Zn和Zf是视空间中的近裁平面和远裁平面的Z值。

使用变换

在前面的教程中,我们写了一个程序在屏幕上绘制了一个三角形。当定义顶点缓存时,顶点位置是直接定义在投影空间中的,因此我们无需进行变换操作。现在我们理解了3D空间和变换,需要修改程序让顶点缓存定义在对象空间中。然后,我们会修改顶点着色器将顶点从对象空间变换到投影空间。

修改顶点缓存

因为我们是在三维空间中表示物体的,所以将上一个教程中的平面三角形变为一个立方体,这样能更好地理解其中的概念。


1
2
3
4
5
6
7
8
9
10
11
12
// Create vertex buffer
SimpleVertex vertices[] =
{
     { D3DXVECTOR3( -1.0f, 1.0f, -1.0f ), D3DXVECTOR4( 0.0f, 0.0f, 1.0f, 1.0f ) },
     { D3DXVECTOR3( 1.0f, 1.0f, -1.0f ), D3DXVECTOR4( 0.0f, 1.0f, 0.0f, 1.0f ) },
     { D3DXVECTOR3( 1.0f, 1.0f, 1.0f ), D3DXVECTOR4( 0.0f, 1.0f, 1.0f, 1.0f ) },
     { D3DXVECTOR3( -1.0f, 1.0f, 1.0f ), D3DXVECTOR4( 1.0f, 0.0f, 0.0f, 1.0f ) },
     { D3DXVECTOR3( -1.0f, -1.0f, -1.0f ), D3DXVECTOR4( 1.0f, 0.0f, 1.0f, 1.0f ) },
     { D3DXVECTOR3( 1.0f, -1.0f, -1.0f ), D3DXVECTOR4( 1.0f, 1.0f, 0.0f, 1.0f ) },
     { D3DXVECTOR3( 1.0f, -1.0f, 1.0f ), D3DXVECTOR4( 1.0f, 1.0f, 1.0f, 1.0f ) },
     { D3DXVECTOR3( -1.0f, -1.0f, 1.0f ), D3DXVECTOR4( 0.0f, 0.0f, 0.0f, 1.0f ) },
};

如你所见,我们定义了立方体的8个顶点,但我们并没有表示出三角形。如果直接将这些数据输入,那么输出的结果不会如我们所愿。我们还需要根据这8个点指定三角形构成立方体。

在立方体中,很多三角形会共享相同的顶点,重复定义相同的点会浪费空间。有一个方法可以只定义8个顶点,然后让Direct3D知道选取哪些点构成三角形。这个方法就是使用索引缓存。索引缓存包含一个列表,表示在缓存中的顶点的索引,用来指定每个三角形到底使用哪些点。如下面的代码所示:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Create index buffer
DWORD indices[] =
{
     3,1,0,
     2,1,3,
 
     0,5,4,
     1,5,0,
 
     3,4,7,
     0,4,3,
 
     1,6,5,
     2,6,1,
 
     2,7,6,
     3,7,2,
 
     6,4,5,
     7,4,6,
};

如你所见,第一个三角形由点3,1,0定义,即这个三角形顶点坐标为( -1.0f, 1.0f, 1.0f ),( 1.0f, 1.0f, -1.0f )和( -1.0f, 1.0f, -1.0f )。立方体有六个面,每个面有两个三角形,所以需要定义12个三角形。

因为每个顶点都是明确排列的,没有两个三角形共享边(至少以上定义的方式没有),所以是按照三角形列表的方式定义的。要定义12个三角形列表方式的三角形,需要36个顶点。

索引缓存的定义方式与顶点缓存类似,我们需要指定结构体大小、类型等参数,然后调用CreateBuffer方法。类型为D3D10_BIND_INDEX_BUFFER,因为我们使用DWORD定义数组,所以这里使用sizeof(DWORD)。


1
2
3
4
5
6
7
bd.Usage = D3D10_USAGE_DEFAULT;
bd.ByteWidth = sizeof ( DWORD ) * 36; // 36 vertices needed for 12 triangles in a triangle list
bd.BindFlags = D3D10_BIND_INDEX_BUFFER;
bd.CPUAccessFlags = 0; bd.MiscFlags = 0;
InitData.pSysMem = indices;
if ( FAILED( g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pIndexBuffer ) ) )
     return FALSE;

创建了索引缓存之后,我们需要让Direct3D引用它们。我们需要指定一个指向缓存、格式和偏移量的指针。


1
2
// Set index buffer
g_pd3dDevice->IASetIndexBuffer( g_pIndexBuffer, DXGI_FORMAT_R32_UINT, 0 );
修改顶点着色器

在上一个教程的顶点着色器中,我们对输入的顶点位置没有进行任何操作直接输出,这样做的原因是顶点位置已经定义在投影空间中了。但现在的顶点位置是定义在对象空间中的,所以必须在顶点着色器输出前对其进行变换。变换有3步:从对象空间变换到世界空间,从世界空间变换到视空间,从视空间变换到投影空间。要做的第一件事就是声明3个常量缓存变量。常量缓存用来存储应用程序传递到shader中的数据。在进行绘制前,应用程序通常需要将重要的数据写入到常量缓存中,在绘制过程中这些数据可以从shader中读取。在一个FX文件中,常量缓存变量的声明方式类似于C++中的全局变量。我们需要的三个变量为世界、视、投影变换矩阵,它们在HLSL中的数据类型为matrix。

声明了矩阵之后,我们就可以修改顶点着色器代码对输入位置进行变换。一个矢量乘以另一个矢量就可以进行变换,这是通过矩阵进行操作的。在HLSL中,使用的是mul()指令。变量声明和顶点着色器的代码如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
matrix World;
matrix View;
matrix Projection;
 
  //
  // Vertex Shader
  //
  VS_OUTPUT VS( float4 Pos : POSITION, float4 Color : COLOR )
  {
      VS_OUTPUT output = (VS_OUTPUT)0;
      output.Pos = mul( Pos, World );
      output.Pos = mul( output.Pos, View);
      output.Pos = mul( output.Pos, Projection );
      output.Color = Color;
      return output;
  }

在顶点着色器中,每个mul()对输入位置进行一次变换操作。世界变换、视变换、投影变换是按顺序进行的,这是必须的,因为矢量和矩阵的乘法不符合交换律。

设置矩阵

更新了顶点着色器之后,我们还需要在程序中定义三个矩阵。这三个矩阵存储了绘制所需的变换。在绘制前,我们将这些矩阵数据复制到着色器常量缓存中。然后调用Draw()方法进行初始化,顶点着色器就会读取储存在常量缓存中的矩阵了。对于矩阵,我们需要定义3个ID3D10EffectMatrixVariable指针。ID3D10EffectMatrixVariable代表常量缓存中的矩阵。我们可以使用这3个对象将矩阵复制到常量缓存中。代码如下:


1
2
3
4
5
6
ID3D10EffectMatrixVariable* g_pWorldVariable = NULL;
ID3D10EffectMatrixVariable* g_pViewVariable = NULL;
ID3D10EffectMatrixVariable* g_pProjectionVariable = NULL;
D3DXMATRIX g_World;
D3DXMATRIX g_View;
D3DXMATRIX g_Projection;

要初始化3个ID3D10EffectMatrixVariable对象,我们需要在创建了effect调用ID3D10Effect::GetVariableByName()方法,以变量的名称为参数。GetVariableByName()返回一个ID3D10EffectVariable接口,其中包含一个基础对象对应FX文件中定义的变量类型。本例中,基础对象是ID3D10EffectMatrixVariable。要从ID3D10EffectVariable中获取ID3D10EffectMatrixVariable接口,我们可以调用它的AsMatrix()方法:


1
2
3
g_pWorldVariable = g_pEffect->GetVariableByName( "World" )->AsMatrix();
g_pViewVariable = g_pEffect->GetVariableByName( "View" )->AsMatrix();
g_pProjectionVariable = g_pEffect->GetVariableByName( "Projection" )->AsMatrix();

下一步需要组合三个矩阵进行变换操作。我们想让三角形位于原点,平行于XY平面,这恰是这个三角形存储在顶点缓存中的形式,因此,无需进行世界变换,我们将世界矩阵设置为单位矩阵。我们将相机放置在[0 1 -5],观察目标为[0 1 0]。我们可以调用D3DXMatrixLookAtLH()方法方便地计算一个视矩阵,因为我们需要让+Y轴指向上,所以使用 [0 1 0]作为up向量。最后是投影矩阵,调用D3DXMatrixPerspectiveFovLH(),视场垂直角度为90度(pi/2),长宽比640/480(即后备缓存的大小),近裁平面为0.1,远裁平面为100。这意味着任何比0.1还近、比100还远的物体在平面上是不显示的。这三个矩阵存储在全局变量g_World, g_View和g_Projection中。

更新常量缓存

有了矩阵,我们必须将它们写入常量缓存中让GPU读取。常量缓存绘制以后的教程中详细讨论。现在,只需把它们想象成一个将常量传递到shader的容器。因为需要将矩阵写入常量缓存,所以需要调用ID3D10EffectMatrixVariable的SetMatrix()方法,将它设置为matrix并写入到缓冲中。这一步必须在调用Draw()方法前完成。


1
2
3
4
5
6
//
// Update variables
//
g_pWorldVariable->SetMatrix( ( float *)&g_World );
g_pViewVariable->SetMatrix( ( float *)&g_View );
g_pProjectionVariable->SetMatrix( ( float *)&g_Projection );
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值