在数学课上学习立体几何时,有一些同学对于书本上的平面图无法直观地了解到它的立体形象。不仅如此,生物课上的高分子三维结构、劳技课上的三视图,都让空间想象力不足的同学很是头痛。于是我想到了将平面上的立体图形以真正立体的形式展现在同学们的面前。
通过以前学习过光的反射,并想到如果通过一个类似四棱锥的透明物体将物体四个面进行漫反射,就可以得到一个看似完整的物体。
在听过我的想法之后,我的辅导老师非常支持,并提供给我木材、有机玻璃和投影仪等材料,我用这些材料搭建了一个类3D投影仪。
图0-3 用投影仪制成的类3D全息投影
    图0
然而,在投影仪上显示的影响是个大问题。通过老师介绍给我的基于Visual Studio的XNA 4.0开发平台,我很轻松地完成了我的想法(最终效果见第三章)。
目 录
项目由来 1
一、 类3D投影仪 2
1、 原理 2
2、 结构 3
(1) 投影仪 3
(2) 木制框架 4
(3) 毛玻璃 6
(4) 四棱锥 7
二、 XNA程序 8
1、 简介 8
2、 特点 8
3、 开发平台 9
4、 源代码 10
(1) 主程序——Program.CS 10
(2) 游戏内容——Game1.CS 11
5、 运行效果 14
三、 整体效果 15
总结 17
参考文献 17
通过投影仪将投影源(如图0-1)投影在毛玻璃上,再经有机玻璃制成的成像四棱锥分别反射呈现出图像。
    图0
投影源分别显示出模型的四个面——前、后、左、右,并进行相应的旋转;而四棱锥的四个面分别反射出模型的四个面,并进行组合,使人产生四个图像拼接在一起形成一个立体图像的错觉。然而,这个装置只能显示出物体的四个面,所以并不能算是真正的全息3D,只能算得上是“类”全息3D。
图1-1 类3D投影仪的结构
    图1
投影仪
这里使用的是PLUS U5-162投影仪,作为投影源。投影仪的一部分露在木制框架的外部,方便接线与操作,另一部分被掩盖在木制框架的内部。为使得成像大小为40cm*40cm,经试验计算得投影仪距毛玻璃高约为60cm。
图1-2 在木制框架外的投影仪
    图1
图1-3 在木制框架内的投影仪
    图1
木制框架
木制框架的骨架是4根长约1m的松木以及底部的一块大小为40cm*40cm的木板。为了整个装置的牢固以及架起投影仪,在顶端加了四根横向的松木用于固定。
此外,在顶端还固定了一块白色的薄木板,用于放置连接投影仪的计算机(见图1-4);在距低端约30cm处在四根松木上还固定四个金属直角,用来放置毛玻璃(见图1-1,金属直角位于毛玻璃下方)。
    图1
图1-4 在木制框架上方的白色薄木板
    图1
不仅如此,由于投影仪射出的图像有一部分是不需要、投影在装置外的,影像美观,而且为了使投影仪的光不受外界光线影响,在装置的四周围起了几块木板。
图1-5 在装置周围的木板
    图1
毛玻璃
用于承接投影仪的图像,使四棱锥通过漫反射成像。毛玻璃大小为40cm*40cm,即成像大小,由最初设计时决定。毛玻璃距装置底部约为30cm,这样便可以在其下方放下一个四棱锥。
四棱锥
最终成像的部分。四棱锥由4块三角形有机玻璃组成(无底面),每块有机玻璃是底长为30cm,高约为21.22cm的等腰三角形,顶角约为70.5度,这样可以使四块三角形拼接成一个四棱锥时,每个面与地面成45度角,使得毛玻璃上与视线平行的图像可以竖直地反射入人眼。
图1-6 有机玻璃制成的四棱锥
    图1
本项目的程序是基于Visual Studio 2012与XNA4.0开发平台、使用C#语言编程。XNA中的X表示能够在Windows、Xbox和合作伙伴之间达到跨平台的强大的软件工具。N表示“下一代(Next-generation)”,A表示“架构(Architecture)”。XNA是基于DirectX的游戏开发环境,是微软对于 Managed DirectX 的修正及扩充版本。
XNA游戏开发有以下特点:
①加快游戏开发的速度。以前使用DirectX来开发Windows平台游戏,游戏开发公司大概花费80%的时间在程序开发上,而在游戏的创意上仅占20%。而使用XNA .NET Framework进行游戏开发,大大减少了开发者的工作量,不仅降低了开发的成本,而且在游戏开发上可以更加关注游戏的创意。
②开发的游戏更加易用,有更高的扩展性。XNA Framework把所有用作游戏编程的底层技术封装起来,由此,游戏开发员就可以把精力大部分专注于游戏内容和构思开发,而不用关心游戏移植至不同平台上的问题,只要游戏开发于XNA的平台上,支持XNA的所有硬件都能运行。
③XNA Framework同时支持2D和3D的游戏开发。
抛开XNA本身作为游戏开发平台不看,我发现用它来制作投影仪的投影源十分方便——事实也的确如此。
我使用的是Visual Studio 2012与XNA 4.0开发平台,使用 C# 语言进行开发。
图2-1 开发平台——Visual Studio Ultimate 2012
    图2
尽管XNA 4.0原生支持Visual Studio 2010,但我还是使用了VS2012,因为VS2012集成了比VS2010更强的调试功能,启动更加快速以及美观的UI。
源代码主要由两部分构成——主程序[Program.CS]与游戏内容[Game1.CS](毕竟XNA是用来开发游戏的)。
图2-2 工程结构
    图2
主程序——Program.CS
using System;
namespace WindowsGame3
{
#if WINDOWS || XBOX
static class Program
{
static void (string[] args)
{
using (Game1 game = new Game1())
{
game.Run();
}
}
}
#endif
}
主程序十分简短,只是通过game.Run()来运行游戏内容,并不需要任何注释。
游戏内容——Game1.CS
这个部分就是整个程序的核心了,所有的工作都由这一部分组成。
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
namespace WindowsGame3
{
// 常量、变量定义
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
int Width = 0;
int Height = 0;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
graphics.PreferredBackBufferWidth = 1024;
graphics.PreferredBackBufferHeight = 768;
// 前两句定义游戏窗口大小为1024x768
graphics.IsFullScreen=true; // 全屏运行游戏
Content.RootDirectory = "Content";
}
protected override void Initialize()
{
// 游戏初始化
Width = graphics.GraphicsDevice.Viewport.Width;
Height = graphics.GraphicsDevice.Viewport.Height;
// 取得模型大小
base.Initialize();
}
Texture2D texture;
Model model;
protected override void LoadContent()// 游戏开始
{
// 创建一个精灵——此处就是指我们要显示的模型
spriteBatch = new SpriteBatch(GraphicsDevice);
            texture = Content.Load
            model = Content.Load
// 前两句为定义我们要显示的模型("teapot")和它的贴图("ji")
}
protected override void UnloadContent()
{
// 游戏结束,本程序中不需要
}
protected override void Update(GameTime gameTime) // 游戏运行
{
// 判断退出条件
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
base.Update(gameTime);
}
private void DrawTeapot(Matrix world, Matrix view, Matrix projection, Matrix[] transforms) // 绘图函数
{
foreach (ModelMesh mesh in model.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
effect.TextureEnabled = true; //使用模型贴图
effect.Texture = texture;
effect.EnableDefaultLighting();//定义光照:默认
effect.View = view;
effect.Projection = projection;
effect.World = transforms[mesh.ParentBone.Index] * world;
}
mesh.Draw();
}
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
// 以下为绘制语句
Matrix world = Matrix.CreateScale(1.0f);
Matrix view = Matrix.CreateLookAt(new Vector3(0, 0, 50), Vector3.Zero, Vector3.Up);
Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 100f);
Matrix[] transforms = new Matrix[model.Bones.Count];
model.CopyAbsoluteBoneTransformsTo(transforms);
DrawTeapot(world*Matrix.CreateTranslation(0,-15.0f,0), view, projection, transforms); // 绘制第一个茶壶
DrawTeapot(world * Matrix.CreateRotationY(MathHelper.ToRadians(270)) * Matrix.CreateRotationZ(MathHelper.ToRadians(90)) * Matrix.CreateTranslation(15.0f, 0, 0), view, projection, transforms); // 绘制第二个茶壶,进行旋转,使茶壶的侧面对着我们
DrawTeapot(world * Matrix.CreateRotationY(MathHelper.ToRadians(180)) * Matrix.CreateRotationZ(MathHelper.ToRadians(180)) * Matrix.CreateTranslation(0, 15.0f, 0), view, projection, transforms); // 绘制第三个茶壶
DrawTeapot(world * Matrix.CreateRotationY(MathHelper.ToRadians(90)) * Matrix.CreateRotationZ(MathHelper.ToRadians(270)) * Matrix.CreateTranslation(-15.0f,0,0), view, projection, transforms); // 绘制第四个茶壶
base.Draw(gameTime);
}
}
}
由于XNA是游戏开发平台,其程序特点就像游戏一样,每隔一段时间清屏,重新绘制新的图像,再清屏,再绘制。
在这一长段程序中,我们只需把目光集中在最后几句绘制茶壶的语句上。以第二个茶壶为例,DrawTeapot(world * Matrix.CreateRotationY(MathHelper.ToRadians(270)) * Matrix.CreateRotationZ(MathHelper.ToRadians(90)) * Matrix.CreateTranslation(15.0f, 0, 0), view, projection, transforms);
其中Matrix.CreateRotationY(MathHelper.ToRadians(270))表示在Y轴方向上旋转270角度,由于Matrix.CreateRotationY()函数采用弧度制,所以在里面嵌套了一个MathHelper.ToRadians()函数。
同理,Matrix.CreateRotationZ(MathHelper.ToRadians(90))表示在Z轴方向上旋转90角度。由于这里不需要在X轴方向上旋转,所以Matrix.CreateRotationX()函数可以被省略,而第一个茶壶不需要旋转,所以把Y轴与Z轴的旋转都省略了。
Matrix.CreateTranslation(15.0f, 0, 0) 函数中的参数代表模型出现的位置。我们可以通过修改15.0f为其他数字以使四个模型间的距离不同。
可以看出,使用XNA来制作——或者说编写投影源真的是十分方便,上述的代码也十分清楚地表示出了四个茶壶的状态。
图2-3 程序运行效果(全屏)
    图2
图3-1 投影效果(毛玻璃)
    图3
图3-2 投影效果(背面)
    图3
图3-3 投影效果(右侧)
    图3
图3-4 投影效果(左侧)
这个项目已经不仅仅只是网友的娱乐或者装饰品,通过XNA,任何模型如化学分子、机械部件等都可以实现类3D投影,甚至可以动起来。它可以与教学结合,让学生们通过3D更有效地学习。
我还计划以下改进:
选择一种显示设备取代投影仪+毛玻璃这个耗费空间巨大的组合,可以节约空间。
将更多的模型融合进XNA程序,并通过键盘或触控旋转视角,实现互动。
《基于 PC ,Xbox 360 和 Windows Phone 的游戏开发——XNA 4.0 学习指南》
Araon Reed 著 裴小星 译