65.9K
CodeProject 正在变化。 阅读更多。
Home

新的视角

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (42投票s)

2009年10月5日

CPOL

16分钟阅读

viewsIcon

134352

downloadIcon

2612

用于OpenGL和Direct3D的简单而全面的查看代码。

NewView

引言

3D计算机图形查看本质上是将3D“视图”体积中的图形项映射到2D图像的简单过程。然而,没有标准的方法来指定视图,并且当前存在各种不同的视图实现。与视图参数紧密相关的是3D交互,同样存在大量不同的3D交互实现,它们通常提供相同的基本功能。用户被迫学习并为他们使用的每个3D程序使用不同的平移、缩放和旋转技术。由于每个3D程序都必须重新发明此功能,为什么几十年来没有出现标准?这种缺乏标准化导致了更高的开发和支持成本,并阻碍了立体声等用户界面功能的普及。

一个标准的查看软件工具包,以及一个标准的运动工具包,将通过在应用程序之间提供一致且全面的3D交互来造福最终用户,并将通过减少开发时间、软件支持和客户支持来造福开发人员。所有各种交互技术都可以提供,以满足不同类型的3D程序和不同级别的最终用户专业知识的各种要求。

在查看代码的基础上,会调用底层的3D API。流行的3D API,即OpenGL®和Direct3D®,具有不同的查看调用。需要一组独立的低级查看参数来处理所有类型的3D视图。然后可以在这些参数之上构建查看和运动工具包。

这里提出了一种新的、更简单的3D查看视角(双关语),它包含了一组七个查看参数,可以处理3D计算机图形中所有类型的视图。代码提供了用于使用这些低级查看参数配置OpenGL和Direct3D。示例代码还展示了一组更高级的查看参数,适用于许多常见的3D程序。

附带的“NewView”演示具有并排的OpenGL和Direct3D窗口,显示了查看参数如何用于配置相应的视图。还提供了一些简单的鼠标交互。

查看基础知识

查看将3D视图体积映射到2D图像。查看涉及变换、裁剪、遮挡和渲染操作。变换将坐标或方向等几何信息映射到指定坐标系或“空间”,在该空间中执行其他操作。裁剪排除任何在视图体积外部的图形项,并修剪任何穿过视图体积表面或表面的项。遮挡隐藏任何在视图体积前方更近的项后面的项或项的部分。渲染基于图形项的几何形状和颜色或纹理信息,以及照明等其他场景信息,生成像素或线绘制值。

虚拟世界通常定义为一个树结构,该结构将图形项关联到集合中,并指定项之间如何相互定位。树的顶层或根通常被认为是“世界”空间。子项相对于其父项的相对空间位置由旋转和翻译变换指定。缩放和倾斜(又名剪切)变换可用于更改图形项的大小和形状。视图体积本身可以位于树结构中的任何位置。由于计算机图形学教科书中对查看的处理方式,大多数3D程序恰好使用一组独立于树结构的查看参数。

可以通过定义3D视图体积的形状,然后相对于世界空间定向和定位该形状来完全指定它。定义一个“视图”空间来指定形状,然后使用旋转/平移变换将此视图空间定位在世界空间中。

当2D输出图像为矩形时,3D视图体积要么是矩形棱柱(平行视图),要么是截头金字塔(透视视图)。截头金字塔称为视锥体。考虑通过沿直线段扫过矩形形成棱柱,或绕3D点缩放矩形形成视锥体来形成视图体积。请注意,棱柱或视锥体的轴可能与矩形横截面法线对齐,也可能不对齐。为简单起见,视图体积在视图空间中与 z=0 平面平行的矩形横截面对齐;这使得前方和后方裁剪平面平行于 z=0 平面。矩形横截面的边与 X 和 Y 轴对齐,因此视图空间 +X 向右,+Y 向上。

当3D视图体积变换到裁剪空间后,由 x=0、y=0、z=0、x=1、y=1、z=1、x=-1、y=-1 和 z=-1 的一组六个裁剪空间平面界定时,裁剪计算会更简单。这意味着视图到裁剪空间的变换通常需要将视图空间中的矩形棱柱或视锥体变形为裁剪空间中的方形棱柱或立方体。有两种常见的裁剪体积:一种是具有 (-1,-1,-1) 和 (1,1,1) 对角线顶点的立方体,另一种是具有 (1,-1,0) 和 (1,1,1) 对角线顶点的方形棱柱。最流行的两个3D图形编程接口是OpenGL®和Direct3D®。OpenGL裁剪到立方体,Direct3D裁剪到方形棱柱。“投影”变换将视图体积从视图空间映射到裁剪空间。注意:一些较旧的线框图形系统不沿深度方向裁剪,视图体积理论上无限延伸。

NewView2.GIF

图1. 裁剪空间视图体积

角度计算,例如表面的角度与光源的角度,对于许多类型的照明和着色操作是必需的。视图到裁剪空间的变换不保留角度或距离,因此不能在裁剪空间中进行角度和距离计算。视图空间具有正确的角度,因此通常将所有项变换到视图空间,在此空间中执行角度和距离照明计算。然后将视图空间坐标变换到裁剪空间进行裁剪。最后将裁剪空间坐标变换到像素和深度缓冲区空间,在此空间中执行遮挡和渲染计算。

通用视图体积

通用矩形棱柱或视锥体视图体积可以在视图空间中使用以下七个值指定:HalfWidth、HalfHeight、ZNear、ZFar、InverseEyeZ、TanSkewX、TanSkewY。这些值缩写为:hwhhznzfieztsxtsyhwhh 定义 z=0 平面中的一个矩形。前方和后方裁剪平面是 z=znz=zf 平面。tsxtsy 值指定穿过视图空间原点的轴的水平和垂直三角函数 tan 值,iez 指定眼睛点 Z 视图空间坐标的倒数。右手坐标系具有 zn > zf,左手坐标系具有 zn < zf。对于透视视图,请确保 1/iez > zn > zf1/iez < zn < zf

图2显示了通用视图体积以及包括近左下角(LLN)和远右上角(URF)的各种坐标。

Figure 2a. General view volume.

Figure 2b. LLN and URF formulae.

图2. 通用视图体积。

几乎所有3D程序都使用法线或直棱柱和视锥体,其中轴垂直于矩形横截面,因此 tsxtsy 都为零。正确的立体查看需要非零 tsxtsy 值,如下文所示。使用 ZNear 和 ZFar 而不是 HalfDepth 值,可以将宽度和高度值与近裁剪平面和远裁剪平面的放置分离开来。

齐次坐标和变换几乎普遍用于变换和裁剪几何图形。变换到 OpenGL 或 Direct3D 裁剪空间的视图到裁剪空间的“投影”矩阵为

Pgl formula

PD3d formula

这些矩阵同时提供平行视图和透视视图。

将视图体积的近左下角和远右上角变换到两个裁剪空间有助于确认这些矩阵。这些点,作为齐次坐标,是

LLN, URF coordinates

所以:

Transformed LLN, URF

如要求。

iez 为零时,眼睛点在无穷远处,导致平行投影。透视投影由 1/iez > zn > zf 或 1/iez < zn < zf 指定。一种新型投影,在此称为反向透视投影,由 zn > zf > 1/iezzn < zf < 1/iez 定义。请注意,在反向透视投影中正面查看立方体面如何暴露立方体的六个面中的五个。

Perspective, orthographic and reverse perspective views

图3. 立方体在透视、正交和反向透视视图中正面显示

所有历史类型的视图,其中直线3D线映射到直线2D线,都通过这种通用投影变换实现。

  • 平行(iez = 0)
    • 正交(tsx = 0 且 tsy = 0)
      • 顶视图、前视图、侧视图、平面图、立面图等。
      • 二测
      • 三测
      • 等轴
    • 斜交(tsx != 0 且/或 tsy != 0)
      • 骑兵
      • 内阁
  • 透视(1/iez > zn > zf 或 1/iez < zn < zf
    • 一点
    • 两点
    • 三点
  • 反透视(zn > zf > 1/iezzn < zf < 1/iez

注意:物理相机镜头不能精确地将3D直线映射到2D直线。

iez 对于查看和运动计算效率很高,但对于用户界面来说是一个不方便的值。一个更好的用户界面值是“视角”,这里定义为未倾斜的水平和垂直视图体积的锥角中较小的一个。

ViewAngle formula

使用此视角参数将扩大宽视图的宽度,并将高视图扩展到垂直方向,以确保始终可见对应于此角度的方形棱柱或视锥体。拖动 NewView 演示窗口的角以查看此效果。更改视角类似于更改物理相机变焦镜头的变焦。对于大多数3D程序,透视标志足以将视角从零(用于平行视图)切换到 45°(用于透视视图)。

3D计算机图形编程接口,如OpenGL和Direct3D,遵循计算机图形学查看文献,将透视投影和并行投影定义为不同的变换。这种差异是由于眼睛点被设置为原点。眼睛点是投影奇点。因此,右下角的透视投影矩阵值必须为零,而并行投影要求此值非零。并行和透视投影实际上是相似的,并且可以通过相同的查看代码来处理,这从上面的投影矩阵可以看出。对平行和透视投影使用一组通用的查看参数有助于简化用于控制视图或移动其他项目的运动算法。

OpenGL和Direct3D通常需要并行矩阵,最后一列为[0, 0, 0, 1],透视矩阵最后一列为[0, 0, -1, 0]**。当 iez 为零时,上述标准形式具有所需的并行最后一列。对于透视视图,需要将原点平移到眼睛点,以便最后一列为[0, 0, -1, 0]。**注意**:将齐次投影矩阵乘以任何非零值对变换没有影响。

**** 截至2009年10月,微软的在线OpenGL文档已被否定,并且与实际二进制文件不符。

ez = 1/iez

Pgl matrix formula

Pd3d matrix formula

附录A中的示例代码使用这些公式,根据通用查看参数来设置OpenGL和Direct3D变换矩阵。附录B是使用一组有用的查看和交互变量来计算通用查看参数的示例。

立体视图

立体查看硬件和软件试图通过将虚拟左视图和右视图体积映射到观看者的眼睛,使左视图和右视图2D图像在观看者前面的同一物理空间中重叠,从而模仿真实世界的查看。立体视图可以与通过同一物理空间中的矩形孔观看真实世界场景进行比较。然而,与真实世界不同的是,焦点距离是2D图像的光学距离,而不是真实物体之间的距离。此外,在矩形前面存在图形只对一只眼睛可见的区域。在真实世界中,物体会遮挡矩形的框架,但在虚拟立体视图中,物体会从一只眼睛的视野中消失。

精确的立体视图要求虚拟观看几何形状与物理观看几何形状匹配。只有当物理眼睛直接位于观看图像中心的正前方时,虚拟视图视锥体才应该是法线或直视锥体。在所有其他情况下,视图视锥体应倾斜。此外,只有一个真正的物理位置可以观看一对立体图像。人脑会补偿虚拟和物理观看几何形状之间的显著差异,这就是为什么数百万人可以观看3D电影。

虚拟眼睛点之间的虚拟距离相对于观看者物理眼睛之间的距离来缩放场景。例如,安装在机场航站楼相距 63 米(207 英尺)的立体摄像机对将显示一个缩放到其大小 1/1000 的机场,供眼睛间隔为 63 毫米(2.48 英寸)的观看者观看。

许多立体查看文本、文章和实现错误地使用直或法线视图体积并稍微旋转它们。结果非常接近正确,因此观看者可以看到不错的3D图像。此外,绝大多数立体查看实现对两只眼睛具有相同的屏幕到眼睛距离。如果物理观看者的眼睛距离可以变化,那么虚拟几何形状就不应该有这个限制。

图形学文献中对“眼睛方向”一词存在一些混淆,因为眼睛方向在数学上是矩形横截面的法线。为了说明这种误称,请考虑从屏幕的一侧很远的地方观看计算机屏幕。相应的“视线方向”是屏幕的法线,实际上指向屏幕旁边的空中,甚至不穿过视图体积。

立体视图的一个合适定义使用了与上面相同的视图空间定义。重叠的矩形以视图空间原点为中心。每个眼睛在视图空间中的3D位置完成了定义。HalfWidth、HalfHeight、ZNear、ZFar、LeftEye(x,y,z)、RightEye(x,y,z)。

沉浸式虚拟现实

系统,如CAVE[1]和iCinema[2],使用环绕观看者(们)的多个立体视图来提供沉浸式效果。CAVE有墙壁、地板和天花板。iCinema有一个高圆柱形屏幕,十二个摄像头投影多达24对立体图像。相邻的立体对被对齐,使得一对的边缘与其相邻对的边缘紧密匹配。效果非常震撼。

增强现实

增强现实是将真实世界图像与相应的虚拟世界图像相结合。虚拟视图体积几何形状必须与物理观看几何形状匹配。增强现实可以提供X射线视觉的效果,例如,“看到”飞机墙壁中的电线。可以在施工现场的实时视图上叠加已完工的建筑物。

使用带摄像机和头部跟踪器的头戴式显示器可以产生令人惊叹的动态X射线视觉效果,为,例如,维修技术人员带来显著的好处。机器人手术已经发展到外科医生能够使用相机和机器人进行手术,同时在物理上远离患者。3D计算机图形图像增加了相机看不到的细节。具有实时扫描技术的增强现实可以提供等同于X射线视觉的效果,在这种效果中,外科医生可以在患者的真实世界视图上叠加图形化地看到患者的内部。

令人惊奇的是,第一个带头部跟踪器的头戴式3D显示器是由计算机图形学先驱Ivan Sutherland在20世纪60年代中期制造的。这项技术从发明到商业化历时数十年。

Using the Code

配置OpenGL和Direct3D的代码位于GeneralView.cpp文件中的Configure_OpenGL()Configure_Direct3D()函数中。这些函数在GeneralView.h中声明为

// GeneralView.h 

void ConfigureView_OpenGL( float ViewVolume[7], float ViewToWorld[4][3] );
#ifdef _D3D9_H_
void ConfigureView_Direct3D( FLOAT ViewVolume[7], FLOAT ViewToWorld[4][3],
                             D3DMATRIX& Projection, D3DMATRIX& World );
#endif

将代码或GeneralView.*文件复制到您的项目中。这些文件被编写为独立于其他代码,如SpatialMath模块。要显示一个半径为Radius的球体,以世界空间原点为中心的对象,请使用

ViewVolume[0] = 0.6f * Radius;  // view rectangle HalfWidth
ViewVolume[1] = 0.6f * Radius;  // view rectangle HalfHeight
if (PixelsAcross >= PixelsUpDown) // window width and height
    ViewVolume[0] *= (float) PixelsAcross / PixelsUpDown;
else
    ViewVolume[1] *= (float) PixelsUpDown / PixelsAcross;
ViewVolume[2] =  4.0f * Radius;  // near clipping plane z-coord
ViewVolume[3] = -4.0f * Radius;  // far clipping plane z-coord
ViewVolume[4] = 0;               // parallel view
ViewVolume[5] = ViewVolume[6] = 0;  // no skew
float ViewToWorld[4][3] = {{1,0,0},{0,1,0},{0,0,1},{0,0,0}};

然后,调用Configure_OpenGL()Configure_Direct3D()

Configure_Direct3D()之后是示例代码,其中包含一组适用于常见3D程序的高级查看参数。这可以用作用户界面的3D交互部分的基础。

演示源代码展示了如何使用鼠标交互,但需要更详细的解释。敬请期待未来的续集...

结论

将3D计算机图形查看视为将3D视图体积映射到2D图像,与大多数计算机图形学教科书中发现的传统方法相比,可以得出非常简单且全面的查看参数集。

此处提供的查看代码虽然小,但在实现3D程序时可以节省大量时间。它还提供了可移植性,并可以作为3D交互工具包的基础,从而节省大量的开发时间和精力。只有一小部分3D程序支持倾斜视图(例如,Cabinet projection)和立体视图。一旦在工具包中提供,这些类型的特性将自动可用。用户将受益于他们使用的程序中一致且丰富的3D查看和交互功能。七个查看参数集,以及此处描述的旋转和平移,足够全面、简单和灵活,可以满足3D查看、运动和交互的所有需求。

关注点

为了展示查看代码的运行,需要一个小的OpenGL和Direct3D程序。同时运行OpenGL和Direct3D并排运行很有趣。我以前从未见过这样做,尽管一些程序可以从一个切换到另一个。初始化OpenGL和Direct3D的代码可能会引起许多读者的兴趣。

创建一个简单干净的通用3D API,同时支持OpenGL和Direct3D,这有点挑战,因为它们是相当不同的架构。结果很简单,效果很好。

基本的SpatialMath模块也可能感兴趣,因为它提供了一种清理代码向量和矩阵算法的方法。当然,我更喜欢它而不是我多年来遇到的其他数学模块。

参考文献

附录 A

以下代码使用通用查看参数和 ViewToWorld 旋转/平移变换来配置 OpenGL 和 Direct3D

// View.cpp
//
// Code to use the general viewing parameters to configure OpenGL and Direct3D.
//
// Copyright (C) 2009 John Hilton
//
// For an explanation see the "A New Perspective on Viewing" at www.codeproject.com.
//

#include "stdafx.h"

// Direct3D includes
#include <d3d9.h>
#include <d3dx9.h>
#include <dxerr9.h>

// OpenGL includes
#include "gl/gl.h"
#include "gl/glu.h"

#include "GeneralView.h"

// Provide an inverse rotate/translate transformation
void Vec3MultInverseRotTrn( float vout[3], float v3[3], float RotTrn[4][3] )
{
    float tmp[3] = {
        v3[0]-RotTrn[3][0],
        v3[0]-RotTrn[3][1],
        v3[0]-RotTrn[3][2]
    };
    for (int i=0; i<3; i++)
        vout[i] = tmp[0] * RotTrn[i][0]
                + tmp[1] * RotTrn[i][1]
                + tmp[2] * RotTrn[i][2];
}

void ConfigureView_OpenGL( float ViewVolume[7], float ViewToWorld[4][3] )
{
    // Set up the GL_PROJECTION and GL_MODELVIEW matrices which transform
    // from world space to clip space. Parallel and perspective views are
    // handled along with left or right handed spaces.
    // ViewToWorld must only be a rotation and translation.
    //      WorldToClip = ModelView * Projection
    //                  = Inverse(ViewToWorld) * ViewToClipGL(ViewVolume)
    // ViewToClipGL = [    1/hw       0                           0    0 ]
    //                [       0    1/hh                           0    0 ]
    //                [ -tsx/hw -tsy/hh    -(2-(zn+zf)*iez)/(zn-zf) -iez ]
    //                [       0       0 (zn+zf-2*zn*zf*iez)/(zn-zf)    1 ]
    // Abbreviations stand for
    //          HalfWidth, HalfHeight, ZNear, ZFar
    //          InverseEyeZ, TanSkewX, TanSkewY
    // Note: OpenGL's perspective view space has a different origin
    // to ViewToWorld's and ViewToClip's view space origin.

    // Use references for code readability
    struct TViewVolume { FLOAT hw, hh, zn, zf, iez, tsx, tsy; };
    TViewVolume& vv = *(TViewVolume*) ViewVolume;
    float &hw  = vv.hw , &hh  = vv.hh , &zn  = vv.zn , &zf  = vv.zf;
    float &iez = vv.iez, &tsx = vv.tsx, &tsy = vv.tsy;

    glMatrixMode( GL_PROJECTION );
    static const float kVerySmall = 1e-6f;
    if (abs(iez) < kVerySmall)
    {
        // Orthographic view
        // Create a matrix with the 4th column [0, 0, 0, 1]
        GLfloat Ortho[4][4] = {
            {    1/hw,       0,               0, 0 },
            {       0,    1/hh,               0, 0 },
            { -tsx/hw, -tsy/hh,      -2/(zn-zf), 0 },
            {       0,       0, (zn+zf)/(zn-zf), 1 }
        };
        glLoadMatrixf( Ortho[0] );
        glMatrixMode( GL_MODELVIEW );
        glLoadIdentity();
    }
    else
    {
        // Perspective view
        // Create a perspective matrix with the 4th column [0, 0, -1, 0]
        // which has the origin at the eyepoint.
        float ez = 1/iez;   // The eyepoint's view space Z coordinate
        GLfloat Persp[4][4] = {
            {      ez/hw,          0,                                  0,  0 },
            {          0,      ez/hh,                                  0,  0 },
            { -ez*tsx/hw, -ez*tsy/hh,            -(2*ez-(zn+zf))/(zn-zf), -1 },
            {          0,          0, -2*(ez*(ez-(zn+zf))+zn*zf)/(zn-zf),  0 }
        };
        glLoadMatrixf( Persp[0] );
        glMatrixMode( GL_MODELVIEW );
        glLoadIdentity();
        glTranslatef( -ez*tsx, -ez*tsy, -ez );
    }

    // Do the inverse of the ViewToWorld rotation and translation
    // Transpose the rotation to invert it
    GLfloat WorldToViewRotate[4][4] = {
        {   ViewToWorld[0][0], ViewToWorld[1][0], ViewToWorld[2][0], 0 },
        {   ViewToWorld[0][1], ViewToWorld[1][1], ViewToWorld[2][1], 0 },
        {   ViewToWorld[0][2], ViewToWorld[1][2], ViewToWorld[2][2], 0 },
        {                   0,                 0,                 0, 1 }
    };
    glMultMatrixf( WorldToViewRotate[0] );
    glTranslatef( -ViewToWorld[3][0], -ViewToWorld[3][1], -ViewToWorld[3][2] );
}

void ConfigureView_Direct3D( FLOAT ViewVolume[7], FLOAT ViewToWorld[4][3],
                             D3DMATRIX& Projection, D3DMATRIX& World )
{
    // Set up the Projection and World matrices which transform from
    // world space to clip space. Parallel and perspective views are
    // handled along with left or right handed spaces.
    // ViewToWorld must only be a rotation and translation.
    // Direct3D's View transform is the identity transform.
    //   WorldToClip = World * View * Projection
    //               = Inverse(ViewToWorld) * WorldToClipD3D(WorldVolume)
    //   WorldToClipD3D = [    1/hw       0                     0    0 ]
    //                    [       0    1/hh                     0    0 ]
    //                    [ -tsx/hw -tsy/hh   -(1-zf*iez)/(zn-zf) -iez ]
    //                    [       0       0 zn*(1-zf*iez)/(zn-zf)    1 ]
    // Abbreviations stand for
    //          HalfWidth, HalfHeight, ZNear, ZFar
    //          InverseEyeZ, TanSkewX, TanSkewY
    // Note: Direct3D's perspective view space has a different origin
    // to ViewToWorld's and WorldToClip's view space origin.

    // Use references for code readability
    struct TViewVolume { FLOAT hw, hh, zn, zf, iez, tsx, tsy; };
    TViewVolume& vv = *(TViewVolume*) ViewVolume;
    float &hw  = vv.hw , &hh  = vv.hh , &zn  = vv.zn , &zf  = vv.zf;
    float &iez = vv.iez, &tsx = vv.tsx, &tsy = vv.tsy;

    // Set World = Inverse(ViewToWorld)
    //          = XFRM(-ViewToWorld.trn) * XFRM(Transpose(ViewToWorld.rot))
    //          = [      1      0      0 0 ] * [ rot00 rot10 rot20 0 ]
    //            [      0      1      0 0 ]   [ rot01 rot11 rot21 0 ]
    //            [      0      0      1 0 ]   [ rot02 rot12 rot22 0 ]
    //            [ -trn.x -trn.y -trn.z 1 ]   [     0     0     0 1 ]
    // where trn is the ViewToWorld.m[3][0] to m[3][2] vector and rot is the
    // ViewToWorld.m[0][0] to m[2][2] matrix.
    for (int i=0; i<3; i++)
    {
        // Transform -ViewToWorld.trn from world space to view space
        World.m[3][i] = -ViewToWorld[3][0] * ViewToWorld[i][0]
                       - ViewToWorld[3][1] * ViewToWorld[i][1]
                       - ViewToWorld[3][2] * ViewToWorld[i][2];
        // Invert ViewToWorld.rot
        World.m[0][i] = ViewToWorld[i][0];
        World.m[1][i] = ViewToWorld[i][1];
        World.m[2][i] = ViewToWorld[i][2];
        // Zero m[0][3] to m[2][3]
        World.m[i][3] = 0;
    }
    World.m[3][3] = 1;

    static const float kVerySmall = 1e-6f;
    if (abs(iez) < kVerySmall)
    {
        // Orthographic view
        // Create a matrix with the 4th column [0, 0, 0, 1]
        D3DXMATRIX Ortho(    1/hw,       0,          0, 0,
                                0,    1/hh,          0, 0,
                          -tsx/hw, -tsy/hh, -1/(zn-zf), 0,
                                0,       0, zn/(zn-zf), 1 );
        Projection = Ortho;
    }
    else
    {
        // Perspective view
        // Create a perspective matrix with the 4th column [0, 0, -1, 0]
        // which has the origin at the eyepoint.
        FLOAT ez = 1/iez;   // The eyepoint's view space Z coordinate
        D3DXMATRIX Persp(      ez/hw,          0,                        0,  0,
                                   0,      ez/hh,                        0,  0,
                          -ez*tsx/hw, -ez*tsy/hh,         -(ez-zf)/(zn-zf), -1,
                                   0,          0, -(ez-zn)*(ez-zf)/(zn-zf),  0 );
        Projection = Persp;
        // When tsx=tsy=0 this is equivalent to...
        //D3DXMATRIX Persp2;
        //D3DXMatrixPerspectiveFovRH( &Persp2, 2*atan2(hh,ez), hw/hh, ez-zn, ez-zf );

        // Add the translation from the eyepoint to the view space origin
        World.m[3][0] += -ez * tsx;
        World.m[3][1] += -ez * tsy;
        World.m[3][2] += -ez;
    }
}

附录B

以下代码基于一组适用于许多3D程序的高级查看参数

// Declare a 3D vector type
template< typename T >
union TVec3 { T v3[3]; struct { T x, y, z; }; };

// The following variables are the interface between the interactive user
// interface and the viewing code.
float g_ViewOrientation[3][3];    // orients the view in world space about
                                  // ViewCenter
float g_ViewCenter[3];            // a world space point mapped to the center
                                  // of the 2D image
float g_ViewSize;                 // minimum width or height of the view's
                                  // cross section at ViewCenter
float g_ViewAngle;                // either
                                  //    0 - a parallel view
                                  //    45*M_PI/180 - a perspective view
float g_BoundingSphere[4];        // world space x, y, z and radius, bounds the
                                  // world space region of renderable geometry
int   g_ViewportWidth, g_ViewportHeight;    // in pixels

template< typename T >
void ConfigureView( T ViewToWorld[4][3], T ViewVolume[7] )
{
    // Create the ViewToWorld transformation by combining g_ViewOrientation
    // and g_ViewCenter.
    for (int i=0; i<3; i++)
        *(TVec3%lt;T%gt;*) ViewToWorld[i] = *(TVec3%lt;T%gt;*) g_ViewOrientation[i];
    *(TVec3%lt;T%gt;*) ViewToWorld[3] = *(TVec3%lt;T%gt;*) g_ViewCenter;
    ViewToWorld[3][0] = ViewToWorld[3][1] =  ViewToWorld[3][2] = 0;

    // Declare TViewVolume
    //    HalfWidth, HalfHeight, ZNear, ZFar, InverseEyeZ, TanSkewX, TanSkewY
    union TViewVolume {
        T v7[7];
        struct { T hw, hh, zn, zf, iez, tsx, tsy; };
    };
    // Use a reference to ViewVolume[]
    TViewVolume& vv = *(TViewVolume*) ViewVolume;

    vv.tsx = vv.tsy = 0;    // not using skewed views
    vv.hw = vv.hh = 0.5f * g_ViewSize;
    // Increase the wider dimension
    if (g_ViewportWidth >= g_ViewportHeight)
        vv.hw *= (float) g_ViewportWidth / g_ViewportHeight;
    else
        vv.hh *= (float) g_ViewportHeight / g_ViewportWidth;

    // Map the bounding sphere from world space to view space
    TVec3<T> SphereCenterView;
    Vec3MultInverseRotTrn( SphereCenterView.v3, g_BoundingSphere, ViewToWorld );

    float& radius = g_BoundingSphere[3];
    vv.zn = SphereCenterView.x + radius;
    vv.zf = SphereCenterView.x - radius;
    static const float kVerySmall = 1e-6f;
    if (g_ViewAngle < kVerySmall)
        vv.iez = 0;     // parallel view
    else
    {
        // Perspective view
        // Ensure the near clipping plane is not too close to the eyepoint
        // and the ratio between the near and far clipping plane distances
        // provides adequate resolution for the z-buffer.
        float EyeZ = min(vv.hw,vv.hh) / tan(0.5f*g_ViewAngle);
        static const float kMinNearDistance = 1e-4f;
        if (vv.zn > EyeZ-kMinNearDistance)
            vv.zn = EyeZ-kMinNearDistance;
        static const float kMinNearFarFactor = 5e-4f;
        if (vv.zn > (EyeZ-vv.zf)*kMinNearFarFactor)
            vv.zn = (EyeZ-vv.zf)*kMinNearFarFactor;
        vv.iez = 1 / EyeZ;
    }
}
template void ConfigureView<float>( float ViewToWorld[4][3], float ViewVolume[7] );

历史

  • 2009年10月5日:初次发布。
  • 2009年10月6日:修复了“!=”拼写错误。
  • 2009年10月10日:格式调整,修正了上面代码中的“<”和“>”。
  • 2009年10月23日:将“(VS2005)”添加到下载链接。
© . All rights reserved.