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

Compact Framework 的光线追踪器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.62/5 (33投票s)

2004年6月3日

CPOL

9分钟阅读

viewsIcon

103455

downloadIcon

580

为 CF 实现一个简单的光线追踪器,作为图形学理论的入门。

Screenshot from the emulator of the raytracer's output

引言

光线追踪器是一种程序,它试图通过模拟我们在现实生活中所体验到的发光环境的数学等效方法,来显示三维物体。它通过模拟从光源发出的光线及其与场景中物体的相互作用来执行此操作。这是一种计算成本高昂的方法,因为它会计算光线对每个像素的绘制材料的影响,即使在现代 PC 上,渲染(显示完整图像)也可能需要很长时间。然而,人们普遍认为光线追踪器是介绍图形学理论的绝佳途径,所以我认为编写一个能在 Compact Framework (CF) 上运行的简单光线追踪器会很有趣。到目前为止,移动设备在图形处理能力方面并不出名,而且绝大多数设备都没有浮点运算单元的处理器,但考虑到我编写的第一个光线追踪器是在 8MHz 的 68000 上,支持 16 色,而现在 200+MHz 的 XScale 处理器带有 16 位图形,感觉就像一台超级计算机 :) 诚然,我在此之间没有编写过其他程序,而是被 OpenGL 和 Direct3D 的“黑暗面”所吸引。尽管如此,不要期望奇迹。即使这是一个非常简单的光线追踪器实现,在我(相当老旧)的 200MHz iPaq 上渲染上面显示的简单场景也需要大约 15-20 分钟。

背景

要理解这段代码的工作原理,就需要解释 3D 图形和光线追踪背后的原理。如果您已经了解其中任何一部分,请随意跳过。这可能是历史上最快的图形学基础理论摘要了 :)

3D 空间

作为程序员,我们习惯于在 2D 空间中设计界面,其中 x 是水平轴,y 是垂直轴。显然,在 3D 中,需要第三个轴,它与另外两个轴成直角,换句话说,就是垂直于屏幕进出。这个轴被称为 z 轴。投影到二维屏幕上

axes.jpg (2583 bytes)

在描述 3D 空间中的一个点时,使用以下形式 (x,y,z),因此 (3,4,5) 将表示沿 x 轴 3 个单位,沿 y 轴 4 个单位,沿 z 轴 5 个单位。

向量

向量可以理解为 3D 空间中的一条线,包含关于线的方向和线的长度信息。需要明确的是,3D 空间中两点 A 和 B 之间的线可以通过从一个点的分量中减去另一个点的分量来计算。还必须理解 (A-B) != (B-A)。向量 (A-B) 的方向分量指向与向量 (B-A) 的方向分量相反的方向。

令人困惑的是,向量的表示方式与 3D 空间中的点完全相同,即 (x,y,z)。由于在大多数数学运算中它们可以以类似的方式处理,因此简化了涉及的代码,并且点通常使用与这里相同的 C#/C++ 类。

向量的长度()可以通过以下方程计算

mag.gif (421 bytes)

其中 Vx、Vy 和 Vz 是向量的分量。这是一个非常重要的运算,因为最有用的向量称为单位向量,它是一个长度为 1 的向量。这在很多方面都很重要,其中最重要的是它提供了沿向量方向的测量单位。通过一个称为归一化的过程将向量转换为单位向量,在该过程中,向量的每个分量都除以其模。

作用于向量的另一个重要运算符称为点积。简单来说,它提供了关于任意两个向量之间角度的信息。事实上,点积提供了角度的余弦值。对于两个向量 V1 和 V2,其计算方法如下

dot.gif (377 bytes)

其中 |Vn| 表示向量的模。

存在一种特殊的向量,称为法向量。这是一种用于描述曲面一部分的向量,定义为垂直于曲面的该部分。在考虑光照时,法向量非常重要,因为光照计算是相对于光线与曲面之间的角度进行的。法向量通常会被归一化以简化涉及它们的计算。

光线

光线是具有明确起始位置的 3D 空间中的一条直线。它由两部分组成:它的原点 (R0) 和它的方向向量 (Rd)。通过将物体的方程代入射线的直线方程,可以使用这些部分来计算射线是否与其路径中的任何物体相交。射线的通用方程为

lineeq.gif (383 bytes)

其中 t 是沿方向向量的距离。

球体

光线与任何其他物体之间最简单的交互是与球体的交互。因此,此光线追踪器将使用的物体就是球体 :) 球体的方程为

spheq.gif (376 bytes)

其中 (l,m,n) 是球体的中心,r 是球体的半径。为了找到球体与射线的交点,必须将两个方程代入二次方程形式

quadform.gif (225 bytes)

其中两个二次根将是与球体的两个交点

sphinter.gif (1844 bytes)

因此,进行代入

quadpart.gif (1210 bytes)

可以使用二次方程求解

quadeq.gif (329 bytes)

并且可以通过从球体中心减去交点来计算交点的法向量。

光照

此光线追踪器使用两种形式的光照:漫反射镜面反射

漫反射光照是来自哑光表面的反射光量,其中入射光向所有方向反射。漫反射光照的数学公式为

diffeq.gif (555 bytes)

其中 Kd 是表面材料的漫反射颜色,N 是归一化的表面法向量,L 是从表面指向光源的归一化向量,如下图所示

diffuse.gif (1313 bytes)

N 和 L 之间的角度越小,它们之间的点积就越大,因此表面反射的光就越多。

镜面反射光照是来自闪亮表面的反射光量,以所谓的镜面反射方向反射。换句话说,光线从表面以更集中的方式反射,并且取决于眼睛的位置,可以看到多少反射光。

specular.gif (1821 bytes)

镜面反射光照的公式如下

speceq.gif (762 bytes)

其中 Ks 是表面材料的镜面反射颜色,H 是 L 和 V 之间的归一化向量,V 是指向眼睛的归一化向量。如果 N.L 大于 0,则 *facing* 为 1,否则为 0。

使用代码

代码被分成几个类,其中许多类提供了比本应用程序所需更多的功能。vector 类包含完整的浮点向量实现,并为算术函数提供了重载运算符。可能会认为定点实现更有意义,但光线追踪器需要精度。与本应用程序中的所有代码一样,向量实现未经优化,因为它更多地被用作学习工具而非其他用途。如果经过优化,不使用运算符重载会更有意义,因为在 CF 下运算符重载通常相当慢。同样,colour 类包含浮点颜色实现。移动设备上的显示器通常是 16 位的,但对于像图形应用程序那样进行颜色操作,始终最好使用完整的颜色分量(红、绿、蓝)。

光线仅由一个包含原点和方向的类以及一些辅助函数表示。

对象(例如球体)继承自 objbase 类。它提供了简单的材质和标识例程,以及射线相交函数的原型。sphere 类通过其中心点和半径表示一个球体。它还提供了上面解释的相交例程。此例程返回一个包含有关相交所有信息的结构

 public struct IntersectInfo 
 {
  public bool hit;  // does intersection take place?
  public vector  pos;  // position of intersection
  public colour  col;  // base colour at point of intersection
  public vector  normal;  // normal at intersection point
 } 

使用 objbase 类和 IntersectInfo 结构,可以添加更多对象,例如平面、圆柱体、圆锥体等。

光照由 lightmanager 类实现,该类在计算光照交互的同时,还维护光照本身。光照由以下简单结构描述

 public struct light
 {
  public vector pos; // position of light
  public colour col; // colour of light

  public light(light lgt)
  {
   pos=new vector(lgt.pos);
   col=new colour(lgt.col);
  }

  public light(vector vec,colour clr)
  {
   pos=new vector(vec);
   col=new colour(clr);
  }
 }

对于每条光线,lightmanager 会计算每种光照的作用,通过调用 getlitcolour 函数,使用上述方程来获取该点的颜色。

  public colour getlitcolour(int lnum,vector pos,colour col,
   vector normal,float shininess,vector eye)
  {
   // allocate a colour
   colour litcol=new colour();

   // does this light exist?
   if(lnum>=m_pos)
    return litcol;

   vector N=normal-pos;
   N=N.normalize();
   float L=m_lights[lnum].pos.dotproduct(N);
   if(L&gt0.0f)
    litcol=col*m_lights[lnum].col*L;


   // compute specular term
   vector Lv=(pos-m_lights[lnum].pos).normalize();
   vector V=(eye-pos).normalize();
   vector H=(Lv-V).normalize();
   float specularweight=0.0f;
   if(L&gt0.0f)
    specularweight=(float)Math.Pow(Math.Max(
     normal.dotproduct(H),0.0f),shininess);
   colour specular=m_lights[lnum].col*specularweight;

   // and combine them
   litcol+=specular;

   litcol.limit();

   return litcol;
  }

所有这些都由 raytrace 类调用,该类计算要追踪的光线以及对象的优先级,以便对被其他对象遮挡的对象产生阴影。

实际像素是通过反复调用 Graphics.FillRectangle 来绘制在屏幕上的,尽管这很慢,但它允许在设备的显示器上构建光线追踪图像,这比在计算图像的 15 分钟内观看空白屏幕更令人满意。此外,它会一边绘制一边将当前颜色与上一个颜色进行平均,以实现一些廉价的抗锯齿处理 :)

场景本身被硬编码到 Form_Load 函数中,并且清楚地说明了如何更改它。

   // add a light
   light lgt=new light();
   lgt.col=new colour(0.7f,0.7f,0.7f);
   lgt.pos=new vector(100.0f,10.0f,-150.0f);
   tracer.addlight(lgt);

   // add sphere 1
   sphere sph1=new sphere();
   colour col=new colour(0.8f,0.7f,0.5f);
   sph1.setup(new vector(0.0f,-50.0f,100.0f),40.0f);
   sph1.ColourValue=col;
   sph1.IdValue=1;
   tracer.addobject(sph1);  

   // add sphere 2
   sphere sph2=new sphere();
   col=new colour(1.0f,0.7f,1.0f);
   sph2.setup(new vector(60.0f,50.0f,100.0f),40.0f);
   sph2.ColourValue=col;
   sph2.IdValue=2;
   tracer.addobject(sph2);  

   // add sphere 3
   sphere sph3=new sphere();
   col=new colour(0.5f,0.7f,1.0f);
   sph3.setup(new vector(-60.0f,50.0f,100.0f),40.0f);
   sph3.ColourValue=col;
   sph3.IdValue=3;
   tracer.addobject(sph3);  

其中 tracer 是 raytrace 类。可以看出,让应用程序从数据文件中加载场景信息并不需要多少工作。

关注点

这是一个有趣的应用程序,因为我习惯于编写原生 C++ 代码,并且过去只为小型实用程序接触过 C#(就像代码中可能显而易见的那样 :)),所以从这个角度来看,这对我来说是一次学习经历。然而,我编写代码的方式旨在让没有图形学背景的人也能从中学习。Compact Framework 在此项目的设计中可能没有提供太多帮助,但任何了解图形编程中通常涉及的海量指针算术和内存泄漏修复的人,都能看到在任何设备上使用垃圾回收环境的好处,只要其使用不会损害应用程序的速度。

历史

  • v1.00 2004/05/28
© . All rights reserved.