3D编程指南第三部分:粒子系统和立即模式渲染(1)

作者:mydeman 文章来源:J2ME开发网

介绍
    欢迎来到M3G指南的第三部分!今天我将介绍如何控制整个渲染过程(立即渲染),以及如何创建一个精密的粒子系统。此外,这里有一些链接,万一你感到迷惑就可以参考:
    首先,或许也是最重要的,就是在索尼爱立信开发者世界上专业的移动Java 3D网络区。其次,如果你碰到困难,就去索尼爱立信移动Java 3D论坛。对于其他的任何情况,使用索尼爱立信开发者世界网络门户,在那里你可以找到你的问题的答案,并且可以了解到更多。
    这篇文章的目的是向你展示,如何根据不同的变换在不同的时间内渲染同一个对象。这被称作立即模式。你将会看到这种模式对很多事情是多么的强大。另外,这篇文章将是深入更高级的指南的基础,因为从现在开始,我们几乎只使用立即模式进行渲染。
    因为文中的代码是用来演示使用的,所以它不是最优化的,并且它也没有包括所有可能发生的错误。稍后将会有更高级的话题。
你应该了解的
    在你开始阅读这篇指南以前,你应该已经阅读过先前的两篇,并且已经对基本的M3G功能有一个稍微扎实的领会。
 第一部分:快速进入移动JAVA 3D编程世界
 第二部分:光的3D理论与定位
保留模式和立即模式渲染
    保留模式是当你使用一个世界它含有的全部信息——包括照相机和灯光渲染整个世界时使用的模式。这是一种相当受限制的模式,因为我们几乎总是希望根据不同的变换多次绘制一个模型,而不是调用整个场景图像。所以,我们在M3G中渲染一个组、节点或者是一个子网面就被称为立即模式。这些是立即模式的方法:
render ( Node  node, Transform  transform)
render ( VertexBuffer  vertices, IndexBuffer  triangles, Appearance  appearance, Transform  transform)
render ( VertexBuffer  vertices, IndexBuffer  triangles, Appearance  appearance, Transform  transform, int scope)
    像你看到的一样,三个方法都需要某种顶点数据:Node和VertexBuffer/IndexBuffer。一个Node可能主要是一个场景图像的任意部分,甚至一个世界也可以被认为是一个Node。通常你会给第一个渲染的方法传递一个Mesh或者一个Group。VertexBuffer,我们在第二篇指南中已经谈论过,它是描述3D空间中一个模型的网面数据的集合。最后的两个方法也需要Appearance类,以明白如何显示网面数据。在这篇指南中我们只使用第一个方法,向你展示立即模式是如何工作的。
    所有方法还有另外一个共同点,就是它们都需要一个Transform类,这个类描述了模型从本地到3D世界空间的转换信息。请记住,我们在最后一篇指南中讨论它。另外需要记住的一件事时,你希望的渲染的大多数模型都是可变化的。这就意味着它们拥有自己的内部变换矩阵。然而,立即模式渲染忽略所有的这种转换信息,只使用为这个方法提供的Transform矩阵。这是非常便利的,因为你可以在内存中保存一个太空船的Mesh,但是可以使用不同的Transform矩阵多次渲染它,这样就可以获得很多不同的太空船。当我们开始设计粒子引擎时,你将会看到实际使用的这种方法。
那是全部吗?
    立即模式渲染的问题是在渲染之前你必须管理更多的对象,因为你没有一个便利的World类,它存储了所有照相机、背景和照明信息。因此现在你需要手动控制的是,使用一个Background对象的视口缓冲器、场景照明信息和照相机。
背景(Background)
    为了使用立即模式渲染,我们必须手动清除视口,这样为下一次绘制周期做准备。这个操作既可以在一个渲染循环之前也可以在它之后完成,但是它必须在你绑定Graphics3D对象之后和释放Graphics3D之前完成。M3G使用一个Background类来帮助你完成这个操作。这个Background类保存了很多极好的信息,例如用来清除屏幕的背景颜色和绘制背景的图片。它是很有用的,因为你可以使用一个很大的图片作为背景,但是可以只显示它的一些小块和部分就好像你走来走去。例如,你可以在你的视野之内拥有一个大PNG,然后当玩家在游戏世界中移动时,你就可以移动背景的修剪区域来显示视野的其它部分。然而你必须明白,使用大的PNG不仅仅十分慢,而且内存效率很低。下面是Background类的最重要的方法:
setColor (int ARGB)
setCrop (int cropX, int cropY, int width, int height)
setImageMode (int modeX, int modeY)
setImage ( Image2D  image)
    让我们一个一个考虑它们。第一个方法是最简单和最常用的。它以0xAARRGGBB形式设置背景的颜色——这个颜色是在屏幕被清除时使用的。那么例如一个鲜红的颜色就是0xFFFF0000。很多人设置这个颜色为黑色或者天空的颜色。然而,默认颜色是白色。
    setCrop方法是非常有用的,如果你正在使用一个背景图片作为一个背景显示。利用这个方法,你可以决定整个背景图片的哪个部分被渲染。这里有一些特殊的情况需要考虑,像修剪区域跨出背景图片的边界之外。这是就要用到第三个方法。它决定了对在源图片之外的像素将会发生什么。有两种可用的模式:REPEAT和BORDER。REPEAT意味着这个图片无限的重复自己,就像是一个平铺的纹理;而BORDER意味着在源图片以外的像素将使用setColor方法提供的背景颜色进行着色。一个很好的事情就是你可以分开决定x轴和y轴的图像模式动作。这就意味着,你可以在x轴(水平方向)环绕一个背景图片,而在y轴(垂直方向)保持静止。
    最后一个方法,setImage是用来决定哪一个图片将会被用作背景。你可以为这个方法提供一个null参数关闭背景图像渲染,只使用背景颜色填充屏幕。这是默认的模式。不过请注意,图片是Image2D必须的,并且不是标准的Java ME平台图片。可是从一个Image创建一个Image2D并不是很难,你既可以使用十分有用的Loader类直接将它指向一个.PNG图像,也可以仅仅使用Image2D的构造方法,如下所示:
Image2D (int format, java.lang.Object image)
    它是一个十分简单的构造方法,首先它获得图片的格式(在99% 的情况下,它是Image2D.RGBA或者Image2D.RGB,这依赖于你是否使用透明度),然后就是获得图片自身。第二个参数应该是你的Image类。所以,你将转换一个正常的Java ME平台图片为一个Image2D对象:
Image img = Image.createImage("/myimage.png");
Image2D img2d = new Image2D(Image2D.RGBA, img);
    极其容易!因此Image2D没有任何威胁,它仅仅是M3G系统使用的一个Image封装。
    现在,你可能迫不及待了,想知道在你渲染任何对象之前,如何使用Background类清除背景。它是非常容易的,这是第一个向你展示方法的代码片断:
// 背景
Background back = null;

// 初始化我们的背景
public void initBackground()
{
    back = new Background();
    back.setColor(0);

}
 
public void draw(Grahics g)
{
    // 这里绑定你的Graphics3D对象
    //...

    // 现在,简单的清除屏幕
    g3d.clear(back);
}
    看到了那是多么的简单?当使用立即模式渲染时这是你需要自己控制的第一件事,现在我们离目标——粒子引擎接近了一步。
光照(Lighting)
    你需要手动控制的另外一件事是光照。你需要创建灯,并根据Transform矩阵把它们放置在3D空间中。这是你在Graphics3D类中完成的所有事情,可以使用如下的方法:
addLight ( Light  light, Transform  transform)
setLight (int index, Light  light, Transform  transform)
resetLights ()
    它们是相当明了的,所以我将快速的讲解它。你应该已经知道在M3G中如何创建一个Light,这在过去的两篇指南中已经讲解过。第一个方法仅仅是添加一个灯到内部的灯数组中,数组中的元素将在以后被使用。通过提供一个实际的Light类和它Transform增加一个灯。这个Transform矩阵决定了这个Light将被放置的位置。addLight方法返回当前光的索引,为了在以后通过setLight方法改变Light这个索引值是需要知道的,setLight方法需要一个特定的索引值。它需要改变的灯的索引值和新的Light和Transform。通过调用Light为null的setLight方法,可以将那个灯从数组中删除。最后一个方法是一个简单的清除方法,它清除Graphics3D相关的所有灯。
照相机(Camera)
    你还需要创建自己的照相机,代替使用World类提供的照相机,就像我们以前使用的那样。在这篇指南中,我们将通过调用它的默认构造函数创建一个Camera。在指南后面的部分,我们将会完成使用一个Camera可以完成的更高级的事情,例如改变射影矩阵。我不会立刻谈论这个话题,我将仅仅向你展示一个代码片断,说明如何完成这个操作:
Camera cam = new Camera();
Graphics3D g3d = Graphics3D.getInstance();
g3d.setCamera(cam, getCameraTransform());
    设置一个照相机和设置一个灯很相似,添加你的照相机,和Transform矩阵,这个矩阵转换照相机到3D空间中的一个点。再一次请小心,因为当你使用这种方式添加照相机时,Camera类的内部节点转换将被忽略。只有Transform矩阵提供的转换在setCamera方法中是使用。
设置舞台
    现在,你已经知道我们需要手动控制的三件事,并且你已经准备好使用立即模式渲染某些东西。在我向你展示任何代码之前,我们再来回顾一下需要完成步骤:
    1. 我们需要增加灯到Graphics3D对象中,它通常在场景初始化时完成。
    2. 我们需要增加照相机到Graphics3D对象中。你可以选择完成这个操作一次,或者在每一次游戏循环中完成,这依赖于你如何操作Camera的Transform矩阵。
    3. 我们需要清除背景,以至于我们可以在一个全新的画布上渲染。
    4. 我们仅仅渲染我们的网面和释放对象。
    让我们看一下代码:
// 得到Graphics3D上下文
 g3d = Graphics3D.getInstance();
 
 // 首先绑定Graphics对象. 我们使用我们预先定义的渲染提示.
 g3d.bindTarget(g, true, RENDERING_HINTS);
 
 // 清除背景
 g3d.clear(back);
 
 // 绑定照相机在一个固定的位置
 g3d.setCamera(cam, identity);
 
 // 渲染一些网面
 g3d.render(someMesh, someMeshTransform);
    它比渲染一个World稍微有些复杂,渲染一个World通过一个方法调用完成(g3d.render(world);),但是通过立即模式在渲染过程中,你将会获得更多的控制权。现在,让我们看看如何立即模式实际完成一些有用事情!一个粒子系统!
粒子系统(Particle System)
    一个3D粒子系统通常由一个代表一个粒子及其物理性质(速率、寿命和位置)的数据结构和一个处理粒子发射度的系统组成。这是一个非常简单的模式,同样,你也可以使一个粒子系统变得如你希望的一样复杂。那么,首先让我们来创建我们的Particle类。为了代表一个3D空间中的粒子,我们需要它在3D空间中的位置,由x、y和z坐标组成。我们还需要它的速率,因为我们希望Particle在3D世界中移动。我们可能还需要一个粒子的颜色,以至于我们能够不同颜色的不同粒子。最后,我们还需要一个粒子的寿命。粒子的寿命代表了它在3D世界中的存在时间,也就是在它被丢弃,或者在一个新的位置使用新的速率和颜色赋予新的寿命以前这段时间。下面是一个Particle类,它包含了我们所有的基本的需求:
/**
 * 保存一个粒子的所有信息.
 * A particle's alpha is controlled directly by its life. Its alpha is always
 * life * 255.
 */
public class Particle
{
    // 粒子的寿命. 从 1.0f 到 0.0f
    private float life = 1.0f;
   
    // 粒子的衰变
    private float degradation = 0.1f;
   
    // 粒子的速率
    private float[] vel = {0.0f, 0.0f, 0.0f};
   
    // 粒子的位置
    private float[] pos = {0.0f, 0.0f, 0.0f};
   
    // 粒子的颜色 (RGB 形式 0xRRGGBB)
    private int color = 0xffffff;
   
    /** 空初始化 */
    public Particle()
    {
       
    }
   
    /**
     * 初始化粒子
     * @param velocity 设置速率
     * @param position 设置位置
     * @param color 设置颜色 (没有透明)
     */
    public Particle(float[] velocity, float[] position, int color)
    {
        setVel(velocity);
        setPos(position);
        this.setColor(color);
    }
 /**
   * @param life 设置life.
   */
   void setLife(float life) {
   this.life = life;
   }
 /**
   * @return 返回 life.
   */
   float getLife() {
   return life;
   }
 /**
   * @param vel 设置vel.
   */
   void setVel(float[] tvel) {
   System.arraycopy(tvel, 0, vel, 0, vel.length);
   }
 /**
   * @return 返回 vel.
   */
   float[] getVel() {
   return vel;
   }
 /**
   * @param pos 设置pos.
   */
   void setPos(float[] tpos) {
   System.arraycopy(tpos, 0, pos, 0, pos.length);
   }
 /**
   * @return 返回 pos.
   */
   float[] getPos() {
   return pos;
   }
 /**
   * @param color 设置color.
   */
   void setColor(int color) {
   this.color = color;
   }
 /**
   * @return 返回 color.
   */
   int getColor() {
   return color;
   }
 /**
   * @param degradation 设置 degradation.
   */
   public void setDegradation(float degradation) {
   this.degradation = degradation;
   }
 /**
   * @return 返回 degradation.
   */
   public float getDegradation() {
   return degradation;
   }
   }
    因为我们希望我们的粒子系统可以有些改进,所以我们还将使粒子衰减到没有,就像是它们的生命慢慢消失。这是一个很棒的效果,稍后我将解释在M3G中我们如何完成它。目前,我们来看一下粒子系统的下一个部分——ParticleEffect。ParticleEffect接口为所有的ParticleEffect定义了通用的接口。Init方法在粒子被赋予生命时被调用,update方法会在每一个游戏循环中被周期性调用,以更新粒子的参数。最后render方法将被用来渲染一个Particle,它将在update方法之后完成。
import javax.microedition.m3g.Graphics3D;
/**
   * 这个接口决定了粒子引擎将要显示的效果.
   * ParticleEffect类还保存了有关显示粒子使用到的位图的信息(如果有)
   */
   public interface ParticleEffect
   {
   // 初始化一个粒子
   public void init(Particle p);
  
   // 更新一个粒子
   public void update(Particle p);
  
   // 渲染一个粒子
   public void render(Particle p, Graphics3D g3d);
   }
    现在,我们终于需要一个粒子系统(ParticleSystem),实际的创建粒子(Particle),发射它们,并且对它们应用ParticleEffect。这是我选择如何写这个类:
import javax.microedition.m3g.Graphics3D;      
/**
   * 管理粒子在我们的3D世界中发射
   */
   public class ParticleSystem
   {
   // 效果
   private ParticleEffect effect = null;
  
   // 粒子
   Particle[] parts = null;
  
   /**
   * 创建一个粒子系统,根据定义好的效果发射粒子.
   * @param effect effect 控制粒子的行为
   * @param numParticles 发射的粒子数量
   */
   public ParticleSystem(ParticleEffect effect, int numParticles)
   {
   // 复制 effect
   setEffect(effect);
  
   // 初始化 particles
   parts = new Particle[numParticles];
   for(int i = 0; i < numParticles; i++)
   {
   parts[i] = new Particle();
   effect.init(parts[i]);
   }
   }
  
   /** 这个方法完成所有操作. 需要在每一个游戏循环中调用。 */
   public void emit(Graphics3D g3d)
   {
   for(int i = 0; i < parts.length; i++)
   {
   getEffect().update(parts[i]);
   getEffect().render(parts[i], g3d);
   }
   }
 /**
   * @param effect 设置effect.
   */
   public void setEffect(ParticleEffect effect) {
   this.effect = effect;
   }
 /**
   * @return 返回 effect.
   */
   public ParticleEffect getEffect() {
   return effect;
   }
   }
    和你看到的一样,它是一个相当简单的类,仅仅是创建一个定义好的个数的粒子,通过的effect的init方法运行它们,并且当它自己的emit方法被调用时通过update方法保持它们的运行。它是一个相当简单却十分强大的粒子系统,因为允许我们做类似如下的操作:
// 创建一个ParticleEffect 类
ParticleEffect pFx = createParticleEffect();

// 现在我们创建一个拥有20个粒子的粒子系统
ParticleSystem pSys = new ParticleSystem(pFx, 20);

// 在我们游戏循环内的某处...
pSys.emit(g3d);
    看,它是多么的灵巧和简单!那三行代码初始化和使用我们的新粒子系统(Particle System)。现在,在我向你展示实际的渲染和更新粒子系统以前,让我们看另外一个话题。
使用代码创建Mesh
    为了描述3D空间中的粒子,最后的方法是使用一个Mesh,它由一个简单的有纹理的Quad(四边形)组成。Quad,可能你已经记起来了,实际是两个计划好的三角形,所以它们代表了一个方形。现在,和在3D工作室中创建一个Mesh,然后作为M3G到处,接着再装载到我们的程序中相比,使用代码创建Mesh要简单和快许多。如果你记得上一篇指南,一个模型有面组成,而这些面由3D点或者顶点组成。因此要创建一个Quad,我需要四个点,每一个角一个。在M3G中一个模型由一个Mesh类描述,Mesh类保存了所有种类的信息,例如顶点、纹理坐标、表面、多边形渲染模型等等。我们将从代码创建一个Mesh类。为了被创建,Mesh类需要三种事物来显示一个模型:一个VertexBuffer、一个IndexBuffer和一个Appearance。让我们来看看如何按照顺序创建它们。
VertexBuffer
    这个类是十分便利的。它保存了很多有关模型的信息,包括顶点、纹理坐标、法线和颜色。对于我们简单的模型,我们需要顶点和纹理坐标。这次我们不使用法线和颜色,因为我们不需要它们。VertexBuffer在VertexArray类中保存了顶点和纹理坐标信息。VertexBuffer是一个相当简单的类,它在内部使用一个数组存储值。当创建它时,你定义每一个点含有几个元素以及每一个元素占据几个字节。现在你可能想知道,我为什么要选择元素的数量?3D坐标不一直是x、y、z三个一组的坐标吗?嗯,当然你是对的,3D坐标一直是根据三个坐标轴定位的,并且的确是含有三个元素。然而,还有一些其它同样有意义的坐标,例如纹理坐标。在这个例子中,我们使用简单的纹理坐标模型,只使用一对坐标。现在,在我们实际开始创建我们的VertexArray之前,先看一下坐标。这是我们模型的顶点(一个有四个角的简单有限平面):
// 平面的顶点
 short vertrices[] = new short[] {-1, -1, 0,
 1, -1, 0,
 1, 1, 0,
 -1, 1, 0};
    就像你能看到的,我们的平面由在xy平面内的角组成。基础3D坐标没有什么困难的。在我向你展示纹理坐标之前,我不得不告诉你纹理坐标在M3G中是如何工作的。一个纹理通过给定的两个坐标映射到多边形上。这是一个有点复杂的话题,在这篇指南的后面部分我们将着重讲解,但是我已经愿意告诉你一点关于它的信息。设想,你有一个曲奇切割机,想从一个图片中产生曲奇。纹理坐标明确地告诉你曲奇切割机的大小,意味着你可以使用纹理坐标告诉M3G系统你希望映射纹理的哪一部分。如果你需要,你甚至可以变换纹理坐标,从而旋转你的曲奇切割机。今天我们不这样做。因此一个纹理的角就是(0, 0)、(0, 255)、(255, 0) 和(255, 255),这些坐标告诉3D引擎我们希望在我们的多边形上使用整个纹理。明白至一点,并且明白我们将使用整个纹理,我们可以创建十分简单的纹理坐标:
// 平面的纹理坐标
 short texCoords[] = new short[] {0, 255,
 255, 255,
 255, 0,
 0, 0};
    它并不困难,不是吗?现在我们需要做的是填充顶点和纹理坐标到VertexArray类中,并且填充它们到一个VertexBuffer中。那么我们已经完成一半了!让我们看看代码是如何实现的:
// 创建模型的顶点
 vertexArray = new VertexArray(vertrices.length/3, 3, 2);
 vertexArray.set(0, vertrices.length/3, vertrices);
    现在我来解释这里我们要完成的事情。首先,我们创建一个VertexArray。VertexArray的构造函数要获得三个参数:由数组(Array)保存的顶点的数目(或者是你希望的点),每一个顶点的元素数目(允许值是2、3或4),还有成分的大小,它告诉数组为保存每个顶点将使用多少字节。这里不同点是,如果你提供一个值2(每个元素2个字节),它将使用short整型保存元素,如果提供一个值1,它将使用byte保存元素。那么,让我们看看我们将作什么:首先,我们提供顶点的数目,当然它是我们原始的顶点数组长度被3除后的值(请记住,顶点有三个部分)。接着,我们提供元素的数目,对于顶点坐标是3。最后,我们提供字节的数目。在这里我们使用两个字节来存储一个元素。请记住,使用更多的字节也将消耗两倍的内存数量,因此如果可以尽量尝试使用单字节顶点。现在,当我们的VertexArray创建时,我们可以设置它里面的值。set方法是真正简单的,它需要三个参数。第一个是用来放置顶点的开始索引。这个当然是1,因为我们还没有向数组中放任何顶点。第二个是要复制的顶点的数量,它又一次是我们数组的长度被3除的值。第三个是需要从其中复制值的实际的数组。看,多么简单!创建一个保存纹理坐标的VertexArray是同样简单。这是关于它的代码,那么为什么你不考虑一下这里和上面例子的区别是什么,为什么会这样?记住,我说的关于纹理坐标的话。
// 创建模型的问题坐标
 texArray = new VertexArray(texCoords.length / 2, 2, 2);
 texArray.set(0, texCoords.length / 2, texCoords);
    好,我们已经为我们的模型完成了创建空间坐标和纹理坐标,现在我们不得不定义组成模型的外表面。记得第二篇指南中的表面吗?我们必须创建组成平面的三角形。
IndexBuffer
    M3G系统将表面(三角形)信息保存在一个称为IndexBuffer的类里。我们在指南的第二部分中讨论过这样一个类,因此你应该知道它是做什么的,但是无论如何我将刷新你的记忆。IndexBuffer保存在顶点数组中的索引和描述组成模型的表面(三角形)。因为我们的模型是一个由两个三角形组成的简单平面(一个四边形)。因为这个IndexBuffer类是一个抽象类,所以我们需要使用一个类继承IndexBuffer。这个类是TriangleStripArray。从它的名字你可以看出,它在一个数组中保存三角形。现在,在TriangleStripArray中有两种描述三角形的方式:显式和隐式。显式描述是最常用的,因为你可以定义组成三角形的顶点坐标。然而隐式描述也是很有用的。区别是显式形式你需要提供一个保存所有三角形坐标的数组,而隐式形式你仅仅需要提供第一个三角形的第一个顶点,然后TriangleStripArray计算其余的,假定下一个顶点比前一个大1。
    在这个例子中,为了简单我们使用显式形式。TriangleStripArray要求三角形在一个以专有方式定义的数组中,这种方式是只定义第一个三角形的三个索引,然后只需为后来的三角形定义一个。这通常被称为三角形带。你可能想知道这是如何工作的?正如我们所知,堆积的三角形在一个模型中彼此相邻(就像我们的简单平面),导致所有的三角形和另外的三角形共同拥有两个点(回忆指南2中的立方体)。那么这就意味着我们能够使用前一个三角形两个点和另外的一个点就可以实际定义一个三角形。从我们的平面坐标你可能会想起我们从点(-1,-1,0)开始顺时针方向创建平面。那么这些就是我们想要创建的三角形。
    如你所见,我们从点3到点1将网面分成两个部分,留给我们两个三角形。这些三角形是(0,1,3)和(1,3,2)。你是否已经见过这种样式?第一个三角形和第二个共同拥有点1和点3,因此我们可以这样描述它们:(0,1,3,2),那么TriangleStripArray就会明白我们有两个三角形,它们共享中间的两个索引。

    让我们看看我们在代码中完成:
// 创建索引和表面长度
int indices[] = new int[] {0, 1, 3, 2};
int[] stripLengths = new int[] {4};
       
// 创建模型的三角形
triangles = new TriangleStripArray(indices, stripLengths);
    因此索引数组正是我们讨论的,它保存我们的三角形。stripLength数组我还没有提到,但是它相当地简单。它只定义一个带的长度(用索引表示)。因为我们定义两个三角形,所以用表示的长度就是四。在一个TriangleStripArray中定义多长度带时这是一个有用的变量,但是今天我们不需要担心它。在后面的指南它将是一个话题,今天只需要知道它是四。那么,拥有这些信息,我们可以很容易的使用显式模式的构造函数创建TriangleStripArray,构造函数获得一个数组(三角形数组)和stripLength数组。十分容易!
    现在所有剩下的就是,创建Appearance类。它主要告诉M3G系统如何渲染包含了VertexBuffer和IndexBuffer类的表面。
外观(Appearance)
    Appearance类是一个大类,它包含了很多如何渲染模型的信息。透明联合、纹理、剔除,你命名它。如果我们将要创建我们的模型,那么我们将需要这些中的一个。今天我们只使用这些功能中的一小部分。首先,我们从剔除开始。剔除是一种用来加速渲染的技术,它告诉系统不应该渲染哪些特定的部分,例如它的后面或者前面。例如对于粒子引擎,我们实际不想看到粒子的后面,因此我们在粒子上可以一直使用后面剔除。可用的剔除方法如下:
PolygonMode.CULL_BACK
PolygonMode.CULL_FRONT
PolygonMode.CULL_NONE
    CULL_BACK定义了后面剔除,意味着从来不需要多边形的后面。CULL_FRONT完成的同样功能,只不过是针对多边形的前面。最后一个方法使剔除完全不可用。剔除设置在一个被称为PolygonMode的类中可用,它是Appearance类的一个组成部分。因此为了设置剔除,使用一个叫做cullFlags的变量,我们可以像下面一样做:
Appearance appearance = new Appearance();
 PolygonMode pm = new PolygonMode();
 pm.setCulling(cullFlags);
 appearance.setPolygonMode(pm);
    难道不是真的真的十分简单吗?对,的确十分简单!现在,我们已经定义了一种剔除形式(通常是CULL_BACK)。按照步骤下一件事就是设置纹理,并且把它设置到Appearance类中(请记住,Appearance也保存纹理信息)。这可以使用一种很简单的方式完成,通过创建一个保存图像的Image2D。然而,Image2D被保存在一个叫做Texture2D的类中,它正是我将要解释的类。
纹理(Textures)
    为了创建一种可以使用的纹理模型,你需要两样东西:一个Image2D保存图片数据和一个Texture2D类。Texture2D保存全部有关纹理的信息,包括图象数据和纹理转换,记得吗,它转换我们的曲奇切割机?它甚至保存其他有用的信息,例如混合(透明度混合)、包装(如果纹理包装越过多边形)和过滤(纹理投影的性质)。今天我们将使用上面所有的,但是混合将在稍微靠后一点解释。让我们一步步开始。首先我们创建一个Image2D和相应的Texture2D对象:
// 打开图片
 Image texImage = Image.createImage(texFilename);
 Texture2D theTexture = new Texture2D(new Image2D(Image2D.RGBA, texImage));
    这相当简单,不需要任何解释。你创建一个Image2D,并且仅仅把它传递到Texture2D的构造函数中。现在,我们开始讨论混合。
    你可以根据多边形的颜色(是的,模型可以拥有颜色)以很多不同的方式混合表面。我将不解释所有不同的混合方法的细节,代替的是我将告诉你去检查M3G的API文档和Texture2D类的文档。仅仅需要知道,根据透明度和颜色使用模型自己的潜在的颜色有不同的方式来混合你的纹理。因为根本不需要混合,所以我们如下所做:
// 代替网面的原始颜色 (没有混合)
 theTexture.setBlending(Texture2D.FUNC_REPLACE);
    上面的代码片断告诉系统忽略模型所有的潜在的颜色,这对大部分情形是很有用的,因为它使纹理的透明部分对模型同样透明!非常棒,不是吗?可是这种方式不能使用透明混合纹理。在这篇指南的后面我们将混合纹理,但是对于现在我希望你了解上面的方法。
    我们将要做的最后两件事是包装(wrapping)和过滤(filtering)。这些是相当简单的过程,我将不深入细节。同样,如果你想弄明白你可以使用的不同种类的包装和过滤的区别,那么请查看M3G API文档。这里,我们不使用包装,意味着只在多边形上被绘制一次;并且我们使用最简单的过滤,它产生最少的效果,却最高的速度!请牢记,实现M3G的设备和今天在控制台和PC上的图形卡的性能仍旧相差深远,因此在大多数情况,我们不得不以性能换取速度。
// 设置包装和过滤
 theTexture.setWrapping(Texture2D.WRAP_CLAMP, Texture2D.WRAP_CLAMP);
 theTexture.setFiltering(Texture2D.FILTER_BASE_LEVEL, Texture2D.FILTER_NEAREST);
    现在剩下的就是将纹理实际添加到Appearance中。是的,我说的是添加而不是设置。这是因为一个模型可以实际拥有多种纹理!对于很多情况例如动画、光影贴图等,这是一种很好的技术。它们是更高级的指南,今天我们将不覆盖它们。代替的是在这一系列的后面的部分中我将深入这些主题的细节。为了增加一个纹理,你简单的使用setTexture方法(是的,模糊不清的名字),并且提供一个索引和一个Texture2D。因此只增加一个纹理,你可以这样做:
// 添加纹理到外观中
 appearance.setTexture(0, theTexture);
    好的,现在我们已经完成了所有的工作!我们已经创建了我们的外观和所有对一个Mesh所必需的东西。现在所有剩余的就是从我们刚刚创建的各部分中实际组合成Mesh。
网面(Mesh)
    创建一个Mesh十分简单。所有我们需要做的就是提供我们刚刚创建的各个部分,并且组合它。这些使用一句代码完成,想这样:
// 最后创建Mesh
 Mesh mesh = new Mesh(vertexBuffer, triangles, appearance);
    它就是那样!现在我们有了一个应用了纹理的平面,我们可以在我们的场景中渲染这个平面。
上面创建Mesh的方法给你的第一个印象能是非常复杂,但是它实际上并不是这样。仅仅在使用几次以后,你就会意识到它实际上十分简单和快速,但是也十分直观。现在我们可以使用已经讨论过的立即模式渲染我们的Mesh,并且提供一个Transform矩阵将它放置在3D空间中的某个地方。
    在开始在一个ParticleEffect类——它将会完成一些很酷的事情的——上编码之前,让我们先讨论一些关于纹理和多边形的透明度混合的话题。
透明度混合(Alpha Blending)
    经常地,你想混合一个模型和它的纹理的周围环境,来获得半透明的模型。这可以对很多东西使用,例如窗子、水面、粒子引擎等等。这个列表实际上是没有结束的,只有你的想象设置极限。在M3G中,这个过程是十分简单的,今天我将展示在我们的粒子引擎中如何完成它。我们希望我们的粒子渐渐的消失,在它们的生命中时变得越来越透明,当它们死亡时完全消失,因此我们需要使用透明度混合完成这个效果。有两个步骤需要完成。
    为一个网面设置透明度混合的第一步是告诉系统它应该衰减的速度,通过给它一个Alpha值和这个模型的颜色。这在Mesh的VertexBuffer类中完成,还记得吗?它是一个叫做setDefaultColor的方法,被用来一致地的着色(意味着给整个模型同样的颜色)。M3G还支持每一个顶点颜色和平滑颜色渐变,它意味着你可以给模型不同的部分不同的颜色,但是今天我并不这样做。代替的是,我们对整个模型只提供一个有Alpha值的颜色,并且和它的周围的环境混合。为了完成这个,我们只需要为setDefaultColor方法以0xAARRGGBB的形式提供一个颜色。
mesh.getVertexBuffer().setDefaultColor(0x80FF0000);
    上面一行的代码将获得Mesh的实例mesh,并且使它半透明(alpha值为0x80或128表示半透明)的亮红色(0xFF或255红色)。如果这个Mesh没有纹理,那么它将是一个半透明的红色的模型。然而,我们还想将我们的纹理混合进模型中。为什么?嗯,如果我们也混合纹理,那么我们实际上可以把纹理颜色改变为任何我们想要的,不使用多重纹理。对一个例子系统来说,这是非常有用的,因为我们使用一个纹理,并且仅仅将它和Mesh的默认颜色混合以得到所有不同颜色的例子。非常好,不是吗?因此,为了混合纹理我不得不会到Appearance类和它的一个属性——CompositingMode。
CompositingMode告诉Appearance类实际中如何合成(或者混合)模型和它周围的环境。有很多方式用来混合一个模型和它周围的环境,每一种方式都会产生一点不同的结果。我们将会使用最常用和最简单的方法,ALPHA混合。这是你如何为CompositingMode设置ALPHA混合,然后添加到Mesh的Appearance中
CompositingMode cm = new CompositingMode();
 cm.setBlending(CompositingMode.ALPHA);
 m.getAppearance(0).setCompositingMode(cm);
    看,它是多么的简单!如果你想了解其他混合模式(ALPHA_ADD、MODULATE、MODULATE_X2和REPLACE),请检查M3G API文档中关于CompositingMode类的部分。
    我们几乎完成设置我们的混合。所有剩下的就是修改纹理。是否记得,当我们创建纹理时,使用FUNC_REPLACE设置它的混合,以忽略潜在的Mesh的颜色和透明度?嗯,我们不能使用那个值将我们纹理混合到我们的Mesh,因此它不得不被改变。有三种方式可以混合一个纹理到基础的模型中,他们是FUNC_REPLACE(这个我们已经看到了)、FUNC_BLEND(实际上是混合)、FUNC_DECAL和FUNC_MODULATE,它们是特殊的模式,你不得不阅读API文档或者等待这一系列指南的后续部分来了解它们。今天我们将使用FUNC_BLEND。它将简单地混合我们的纹理(和纹理的alpha值)和模型的颜色以及alpha值。为了完成这一步,我们需要重新获得一个网面的纹理(或者几个纹理,如果我们拥有许多的话),设置正确的混合。为了取得模型的第一个纹理(这将是我们今天将要做的),如果如下所作:
m.getAppearance(0).getTexture(0).setBlending(textureBlending);
    看,它是那么的简单!现在我们已经为我们的模型设置的混合,然后就可以使用不同的颜色和透明度显示它。可是在我们作别的事情以前,让我们先讨论一下我们需要混合的纹理图像的类型。
混合图像
    为了混合一个图像和潜在的模型,我不能使用任何类型的纹理。嗯,实际你可以,但是你将不能得到期望的结果。例如,如果这个纹理是全白,它将不能被和这个纹理混合,因为我们使用的算法是将模型的颜色和纹理的颜色相加。对于白色纹理,这是一个问题,因为白色是最高的颜色值(255),无论给它加上多大的值,除了白色不会产生任何效果。为了得到最好的混合和对Mesh默认颜色的完全颜色控制,使用完全黑色的纹理。这种方式,你可以对Mesh默认颜色设置任何你想要的颜色,纹理也将会是的同样的颜色。
    这是一个我们将要在游戏中使用的纹理图片。正像你看到,它完全是黑色的,并且向外模糊以产生好的和平滑的效果。如果你不想的话,并非一定要使用黑色,但是仅仅需要记住的是纹理的颜色一直和Mesh的颜色混合,因此你可能会得到一个和你想象中完全不同的颜色。

控制透明度
    现在,为了控制模型和纹理的alpha值(透明度),你只需要操纵Mesh的默认颜色(在Mesh的VertexBuffer中,还记得吗?)在默认颜色中Alpha值越低,Mesh将会越透明。为了构造一个含有特定Alpha值的颜色,你可以这样做:
int color = (alpha << 24) | (red << 16) | (green << 8) | blue;
    上面的代码片断将会联合一个alpha值和一个红色、绿色和蓝色,创建一种VertexBuffer在它的setDefaultColor方法中希望的颜色。因此,通过改变alpha变量,你可以改变透明度。稍后,你将看到一个这种的粒子的例子。
完成粒子系统(Particle System)
    现在,我们可以从代码实际创建Mesh,让我们创建一个工具函数,它将为我们完成创建Mesh的工作。首先,我们构造某些东西它们将创建一些可混合的Mesh。它可能看起来像这样:
/** 设置一个网面的Alpha混合. 如果网面已经是Alpha混合,这才有意义 */
 public static void setMeshAlpha(Mesh m, int alpha)
 {
 m.getVertexBuffer().setDefaultColor(alpha);
 }
 
 /**
 *
 * @param m 用来被转化为混合的网面
 * @param alpha 用来混合的alpha颜色
 * @param textureBlending 纹理混合参数.
 */
 public static void convertToBlended(Mesh m, int alpha, int textureBlending)
 {
 // 设置alpha
 setMeshAlpha(m, alpha);
 
 // 设置合成模式
 CompositingMode cm = new CompositingMode();
 cm.setBlending(CompositingMode.ALPHA);
 m.getAppearance(0).setCompositingMode(cm);
 m.getAppearance(0).getTexture(0).setBlending(textureBlending);
 }
 
    那两个方法可以转化任何Mesh为和我们的场景其他部分可混合的。现在让我们得到一个实际完成整个创建过程的方法:
 
/**
 * 创建一个应用纹理的平面.
 * @param texFilename 纹理图像文件的名字
 * @param cullFlags 剔出标志. 参见 PolygonMode.
 * @return 完成纹理的网面
 */
 public static Mesh createPlane(String texFilename, int cullFlags)
 {
 // 平面的顶点
 short vertrices[] = new short[] {-1, -1, 0,
 1, -1, 0,
 1, 1, 0,
 -1, 1, 0};
 // 平面的纹理坐标
   short texCoords[] = new short[] {0, 255,
   255, 255,
   255, 0,
   0, 0};
 
   // 类
   VertexArray vertexArray, texArray;
   IndexBuffer triangles;
 // 创建模型的顶点
   vertexArray = new VertexArray(vertrices.length/3, 3, 2);
   vertexArray.set(0, vertrices.length/3, vertrices);
  
   // 创建模型的纹理坐标
   texArray = new VertexArray(texCoords.length / 2, 2, 2);
   texArray.set(0, texCoords.length / 2, texCoords);
  
   // 根据前面的顶点和纹理坐标组合出一个 VertexBuffer
   VertexBuffer vertexBuffer = new VertexBuffer();
   vertexBuffer.setPositions(vertexArray, 1.0f, null);
   vertexBuffer.setTexCoords(0, texArray, 1.0f/255.0f, null);
  
   // 创建索引和面长度
   int indices[] = new int[] {0, 1, 3, 2};
   int[] stripLengths = new int[] {4};
  
   // 创建模型的三角形
   triangles = new TriangleStripArray(indices, stripLengths);
 // 创建外观
   Appearance appearance = new Appearance();
   PolygonMode pm = new PolygonMode();
   pm.setCulling(cullFlags);
   appearance.setPolygonMode(pm);
 // 创建和设置纹理
   try
   {
   // 打开图像
   Image texImage = Image.createImage(texFilename);
   Texture2D theTexture = new Texture2D(new Image2D(Image2D.RGBA, texImage));
  
   // 替换网面的原始颜色 (没有混合)
   theTexture.setBlending(Texture2D.FUNC_REPLACE);
  
   // 设置包装和过滤
   theTexture.setWrapping(Texture2D.WRAP_CLAMP, Texture2D.WRAP_CLAMP);
   theTexture.setFiltering(Texture2D.FILTER_BASE_LEVEL, Texture2D.FILTER_NEAREST);
 // 增加纹理到外观众
   appearance.setTexture(0, theTexture);
 }
   catch(Exception e)
   {
   // 发生错误时
   System.out.println("Failed to create texture");
   System.out.println(e);
   }
  
   // 最后创建网面
   Mesh mesh = new Mesh(vertexBuffer, triangles, appearance);
 // 完成一切
   return mesh;
   }
    上面有很多代码,但是真的那么难以阅读。检查它,然后返回到这篇指南中我解释如何从代码创建一个Mesh的位置,你将会意识到它相当的简单。
    现在我们能够创建网面,我需要创一个在我们的ParticleSystem中使用的ParticleEffect类以得到几种效果。我的目的是喷泉效果,你可以根据你的需要旋转屏幕。在阅读指南这篇后一个好的练习将是创建你自己的ParticleEffect类,它可以产生几种很酷的效果。尝试创建一个环形的爆炸(像烟火一样)或者可能甚至像闪烁火焰一样高级。无论如何,首先将使用面向对象编程抽象一些很多ParticleEffect都将拥有的行为,使用一个平面的Mesh作为一个粒子。那个类看起来像:
/**
 * 描绘一个使用位图的粒子效果.
 */
 public abstract class BitmapParticleEffect implements ParticleEffect
 {
 // mesh
 Mesh mesh = null;
 
 // 变换矩阵
 Transform trans = new Transform();
 
 // 刻度
 float scale = 1.0f;
 
 /** 初始化用来渲染粒子的位图 */
 public BitmapParticleEffect(String filename, float scale)
 {
 // 装载包含需要的纹理的平面
 mesh = MeshFactory.createAlphaPlane(filename, PolygonMode.CULL_BACK, 0xffffffff);
 
 // 确定设置刻度
 this.scale = scale;
 }
 
 /**
 * @see ParticleEffect#render(Particle, Graphics3D)
 */
 public void render(Particle p, Graphics3D g3d)
 {
 // 计算alpha
 int alpha = (int)(255 * p.getLife());
 
 // 创建颜色
 int color = p.getColor() | (alpha << 24);
 
 // 设置alpha
 MeshOperator.setMeshAlpha(mesh, color);
 
 // 变换
 trans.setIdentity();
 trans.postScale(scale, scale, scale);
 float[] pos = p.getPos();
 trans.postTranslate(pos[0], pos[1], pos[2]);
 
 // 渲染
 g3d.render(mesh, trans);
 }
}
    正像你看到的,它是一个抽象类,已经为我们安排了很多,像立即模式渲染。检查渲染方法,看你是否能认出它是这篇指南的前面我讨论过的。像你可以看到,我们还根据粒子的生命设置Mesh的alpha值。因为每一个粒子已经拥有了一个0xRRGGBB形式的颜色,所有我们要做的就是插入alpha值,设置它作为Mesh的默认颜色。在构造函数中使用的MeshFactory.createAlphaPlane十分简单。它首先调用我们的createPlane方法——我在前面展示过的,然后使用我们的工具函数使Mesh可混合。
    上面的类为我们作了很多工作,因此现在我们所有需要完成的就是使粒子来回移动。我选择创建一个喷泉效果,这是结果:
/**
 * 创建一个漂亮的喷泉效果, 它在特定的方向上发射粒子,由它的角度决定。
 * 这个角度可以被实时改变。
 */
 public class FountainEffect extends BitmapParticleEffect
 {
 // 粒子发射的角度
 private int angle = 90;
 
 // 当前角度的正弦和余弦
 private float[] trig = {1.0f, 0.0f};
 
 // 放射起始位置
 private float[] pos = {0.0f, 0.0f, 0.0f};
 
 // 随机发生器
 Random rand = null;
 
 /**
 * @param angle 粒子发射的角度
 */
 public FountainEffect(int angle)
 {
 // 初始化位图
 super("/res/particle.png", 0.05f);
 
 // 设置角度
 setAngle(angle);
 
 // 得到随机数发生器
 rand = new Random();
 }
 /**
   * @see ParticleEffect#init(Particle)
   */
   public void init(Particle p)
   {
   // 设置粒子寿命
   p.setLife(1.0f);
  
   // 设置粒子位置
   p.setPos(pos);
  
   // 创建粒子速度
   float[] vel = new float[3];
  
   // 我们想要粒子的速度从 0.2f 到1.0f
   float xyvel = rand.nextFloat() * 0.8f + 0.2f;
  
   // 我们想要粒子慢慢地消亡
   p.setDegradation(xyvel / 18);
  
   // 根据有一个小的偏移的三角法设置速度
   vel[0] = xyvel * trig[1] + rand.nextFloat() * 0.125f - 0.0625f;
   vel[1] = xyvel * trig[0] + rand.nextFloat() * 0.125f - 0.0625f;
  
   // 在深度上没有移动
   vel[2] = 0.0f;
  
   // 设置速度
   p.setVel(vel);
  
   // 设置随机颜色
   int r = (int)(120 * rand.nextFloat()) + 135;
   int g = (int)(120 * rand.nextFloat()) + 135;
   int b = (int)(120 * rand.nextFloat()) + 135;
   int col = (r << 16) | (g << 8) | b;
   p.setColor(col);
   }
 /**
   * @see ParticleEffect#update(Particle)
   */
   public void update(Particle p)
   {
   // 简单更新位置
   float[] ppos = p.getPos();
   float[] vel = p.getVel();
   ppos[0] += vel[0];
   ppos[1] += vel[1];
   ppos[2] += vel[2];
  
   // 更新生命
   p.setLife(p.getLife() - p.getDegradation());
  
   // 检查寿命。如果已经死亡,重新初始化
   if(p.getLife() < -0.001f)
   {
   init(p);
   }
   }
 /**
   * @param angle 设置anglet.
   */
   public void setAngle(int angle) {
   this.angle = angle;
   trig[0] = (float)Math.sin(Math.toRadians(angle));
   trig[1] = (float)Math.cos(Math.toRadians(angle));
   }
 /**
   * @return 返回 angle.
   */
   public int getAngle() {
   return angle;
   }
 /**
   * @param pos 设置 pos.
   */
   void setEmittingOrigin(float[] pos) {
   this.pos = pos;
   }
 /**
   * @return 返回 pos.
   */
   float[] getEmittingOrigin() {
   return pos;
   }
}
    这里我将不讲解细节,因为它实在是基础的三角法填充。你可以抛弃那个类,使用你自己的可以产生很酷的粒子效果的类代替它。它所有完成的工作是,根据喷泉的角度随机化一个粒子的速度,然后移动它直到它消亡。你可以旋转喷泉实时改变角度。我已经将这加入到主游戏循环中,它看起来有点像这样:
// 检查对喷泉的旋转控制
 if(key[LEFT])
 fx.setAngle(fx.getAngle() + 5);
 if(key[RIGHT])
 fx.setAngle(fx.getAngle() - 5);
    fx对象是实际上是FountainEffect的引用,我们改变它的角度。像你所了解的,按下左键增加逆时针方向的角度,右键减少逆时针角度。
为立即模式渲染设置场景
    现在我们每一件事,我将只向你展示我是如何设置立即模式渲染的。你应该已经知道了,因为在这篇指南的早期已经讲解过。因此,我将仅仅重复这部分信息。这里是怎样构造画布:
/** 构造画布
 */
 public M3GCanvas(int fps)
 {
 // 我不想正在地获取按键
 super(true);
 
 // 我们想要一个全屏的画布
 setFullScreenMode(true);
 
 // 装载照相机
 loadCamera();
 
 // 装载背景
 loadBackground();
 
 // 设置graphics 3d
 setUp();
 }
    loadCamera方法创建一个我们3D系统将要使用的照相机,这十分简单。它所有完成就是创建一个默认的照相机。你可以在下面的代码清单中看到它完成了什么。loadBackground方法创建一个背景,并且设置它的颜色为黑色(0x0)。也十分简单。最后一个方法,setUp完成初始化,通过在我们的渲染背景中添加一个环绕灯。它看起来像这样:
/** 通过增加一个灯,为立即模式渲染准备Graphics3D引擎 */
 private void setUp()
 {
 // 得到实例
 g3d = Graphics3D.getInstance();
 
 // 增加一个灯到我们的场景, 因此我们可以看到东西
 g3d.addLight(createAmbientLight(), identity);
 }
 
 
 /** 创建一个简单的环绕灯 */
 private Light createAmbientLight()
 {
 Light l = new Light();
 l.setMode(Light.AMBIENT);
 l.setIntensity(1.0f);
 return l;
 }
    那里没有任何新东西。以前我们已经创建环绕灯多次,你应该已经熟记它了。
    那么,现在我们的场景已经为渲染做好了准备,现在我们需要做的是发射一些粒子到屏幕上。像我已经提到的,这已经通过在ParticleSystem对象上调用emit方法完成了。我们创建了一个ParticleSystem,为它增加了一个ParticleEffect(FountainEffect),然后我们只须在每一个游戏循环中反复调用emit。这是我放在主游戏循环中的:
// 将所有代码放在一个 try/catch 块中,以防万一
 try
 {
 // 得到Graphics3D 上下文
 g3d = Graphics3D.getInstance();
 
 // 首先绑定graphics对象. 我们使用预先定义好的渲染线索.
 g3d.bindTarget(g, true, RENDERING_HINTS);
 
 // 清除背景
 g3d.clear(back);
 
 // 在起点邦定一个照相机在固定的位置
 g3d.setCamera(cam, identity);
 
 // 初始化粒子
 if(ps == null)
 {
 fx = new FountainEffect(90);
 ps = new ParticleSystem(fx, 20);
 }
 
 // 放射粒子
 ps.emit(g3d);
 
 // 检查喷泉旋转控制
 if(key[LEFT])
 fx.setAngle(fx.getAngle() + 5);
 if(key[RIGHT])
 fx.setAngle(fx.getAngle() - 5);
 
 // 如果用户按下 fire退出
 if(key[FIRE])
 TutorialMidlet.die();
 }
 catch(Exception e)
 {
 reportException(e);
 }
 finally
 {
 // 切记释放!
 g3d.releaseTarget();
 }
    现在让我们剖析代码并理解它。首先它是相当简单的,我获得Graphics3D对象的实例,把它绑定到我们Graphics对象(上面的方法是draw(Graphics g),因此我们已经拥有Graphics对象)。在这之后,我们在Background对象上调用g3d.clear方法清除背景和深度缓冲器。这给我们一个干净的、黑色的画布。现在我们需要设置我们的照相机到世界中,根据它自己的变换(在游戏的每一次循环中这样做是很笨拙的,但是这里为了清晰我这样做了)。我们将一直使用恒等矩阵,因为在这篇指南中我们不想移动照相机。接下来,我们检查我们的系统是否仍旧是空的,如果有需要就初始化它。我们仅仅创建一个好的喷泉效果,它从开始90度开始(指向正上方),一个粒子系统包含20个粒子。这个完成之后,我们调用emit方法,粒子将会被更新和渲染到我们的世界中。当所有的完成时,我们检查一些按键,像操纵杆键粒子系统的旋转,Fire(开火)键退出应用程序。
    好了!现在它并不那么难,不是吗?像我说的,你可以根据你自己写的东西实际替换FountainEffect类,以得到一个个性的粒子效果。你也可以尝试装载很多拥有不同的纹理的不同网面到内存中,因为现在你有为你完成这个工作的方法。
总结
    所以为了使系统完整,下面是代码实际运行中的屏幕截图。相当漂亮的颜色!
     
    这就是我们的粒子系统!当粒子慢慢减弱,逐渐变老时,它是多么的漂亮!我还选择随机化粒子的颜色从128到255,以至于我不会得到黑色和忧郁的颜色。我们只想暖色和明亮的快乐!这是代码清单。在这篇文档的最后,你可以下载包含资源文件的源文件包。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值