今天,右眼有点痛,估计是耗太多了。还是坚持翻译了jME Flag Rush的第四篇。废话不多说,下面译文。完工,睡觉!
注:本系列教程全部翻译完之后可能会以 PDF 的形式发布。
如果有什么错误可以留言或 EMAIL : kakashi9bi@gmail.com 给我。
jME 版本 : jME_2.0.1_Stable
开发工具: MyEclipse8.5
操作系统: Window7/Vista
既然我们已经有了 terrain (或者说是我们即将交互的真实平面),我们需要其他“没用”的对象去让它看起来像个足够大的世界包围着我们。因为地形是有限的,我们需要一些方法去保持玩家包含在我们狭小的空间内,而且是以一种合理的方式。所以,这个向导,我们将创建一个力场( Force-Field )舞台( Fence ),那定义了游戏的区域。可能我们的玩家是被监禁的,然后被迫为的典狱长提供消遣?这将创建一个感觉,那就是我们永远也不能超出那个 terrain 的原因。再者,我们想让它看起来像是在 Fence 外有个世界。我们将使用 Skybox 来这么做。 Skybox 将创建大世界的幻觉,让我们开始吧。
4.1 、准备和代码修改
有些和环境无关的代码需要修改,我将会提到。由于,我正在潦草地写一个游戏,并为此写一个向导,那将会有一些东西随着时间推进需要调整。放心,我将提及所有的改变。
首先,我为这个测试改变 camera 的设置(以后将使用 input ,所以这将是最后一次)。不管怎样,由于我正在添加一个非常大的 Geometry ,我想让它看得更清楚。为了做到这一点,我将 camera 往后移动,往下移,并往下调整角度。你将注意到这些改变:
Vector3f loc = new Vector3f(250f,100f,250f);
Vector3f left = new Vector3f(-0.5f,0.0f,0.5f);
Vector3f up = new Vector3f(0.0f,1.0f,0.0f);
Vector3f dir = new Vector3f(-0.5f,0.0f,-0.5f);
接着,我们忘记给窗口加标题。我们当然想在屏幕上显示我们的标题!为了这么做,我们告诉 DisplaySystem 我们的标题将会是什么。这个被加在 initGame 的顶部。
display .setTitle( "Flag Rush" );
另一个小清除是我从 initGame 中移除 terrain 的 attach 代码,并把它放到 buildTerrain 中。
最后,但并不意味着不重要,我调整了 terrain 的 scale 。由于我以非传统格式做这个游戏(向导),我也应该做一些非传统的事(这样以致我能展示底层细节)。就这一点,这要求我对对象做一些调整,以致它们能更适合彼此。在这个例子中,我的 Fence 的大小只是不能超出,所以我需要调整 terrain 对准他。新的 terrain 行是:
Vector3f terrainScale = new Vector3f(4, .0575f, 4);
是的,这些值的产生只是简单的尝试和误差。我减少高度的变化是因为 terrain 太崎岖了。现在那些改变应该让你的东西看起来像这样:
·现在,我们准备开始!
4.2 、深度缓冲( Depth Buffer )
我们现在准备增加多个对象给 scene 。如果我们一不小心,对象将画得覆盖其它它本不该被覆盖的对象。我们想要的是前面的对象覆盖后面的。我们将做 2 件事去处理这种 render 顺序, RenderQueue 和 ZBufferState 。我会先讲下 ZBufferState (或者说深度缓冲),而保留 RenderQueue ,直到我们处理透明度( transparency )的时候。 Depth Buffer 允许 OpenGL 为每个像素( pixel )保留深度( depth )信息。那就是,一个颜色值被赋给了 pixel , OpenGL 能知道那个 pixel 距离 camera 多远,从而决定是否用其它对象的 pixel 覆盖它。在我们的例子中,我们想当即将到来的 pixel 和 camera 的距离小于或等于当前 pixel 与 camera 的距离时,当前 pixel 被覆盖。我们将通过创建一个 ZBufferState 并把它赋给 scene graph 的根( root )来这么做。
我们将把它增加到 initGame 方法。
ZBufferState buf = display .getRenderer().createZBufferState();
buf.setEnabled( true );
buf.setFunction(ZBufferState.TestFunction. LessThanOrEqualTo );
scene .setRenderState(buf);
就是这样。现在任何加到场景中的东西在它允许被 render 到缓冲区之前,都不得不有一个小于或等于当前 pixel 的距离。这个将能防止我们的 fence 在 terrain 后面。
4.3 、创建环境(手动)
这是一个大的部分,我们将通过手写代码创建 fence 。通常,你可以简单加载一个描绘 fence 的模型并把它放在 terrain 上。然而,出于向导的目的,我想要展示怎样加载不同的图形( Shape ),并把它们安排到一个单一的 Node 里面。
fence 将有下面几个参数:
1、 方形的
2、 每个角落有守卫塔
3、 从一个塔到下一个之间有伸展的框架相连。
4、 一个 force field 从框架“坠下”
所有这些代码将放在一个叫 buildEnvironment 的方法里面并由 initGame 调用。
4.4 、守卫塔和 SharedMesh
我已经决定在 fence 的每个角落创建圆柱体的塔。这意味着有 4 个塔。我应该怎样创建圆柱体对象?很好, Cylinder 听起来是个不错的选择。然而,替代创建 4 个 Cylinder 并创建 4 份相同的数据,我只创建了一个 Cylinder 。我然后将旋转 Cylinder 让它的面垂直(默认上 Cylinder 是沿着边躺下的)。在这之后,我将使用 SharedMesh 创建它的四分副本。 SharedMesh 允许我们使用相同的 Geometry 数据但把它们放在 scene 中不同的区域。这同时节省了内存(只保留一份数据)和 render 时间(只渲染一次)
由于我正在使用 SharedMesh ,我应该增加原始的 Cylinder Geometry 到 scene 。这是因为 SharedMesh 在 render 阶段操纵原始的本地数据。
所以,我判定 fence 每一个边有 32 单元长。为什么我选择 32 ?没有特别的原因,只是在那个时候觉得合理。我然后让 Cylinder10 单元高。同样的,没有原因,只是尝试和误差。所以,将每个塔之间距离 32 单元,我设置它们的 localTranslation 到适当的点。
// 这个圆柱体将扮演每个角落那 4 个主要的柱子
Cylinder postGeometry = new Cylinder( "Cylinder" , 10, 10, 10, 100);
Quaternion q = new Quaternion();
// 将圆柱体转为垂直
q.fromAngleAxis(FastMath. PI /2, new Vector3f(1,0,0));
postGeometry.setLocalRotation(q);
postGeometry.setModelBound( new BoundingBox());
postGeometry.updateModelBound();
// 我们将共享柱子 4 次(每个柱子一次)
// 增加原始的圆柱体不是一种好的方法
// 因为 sharedmesh 将修改它的本地值
// 我们然后将调整柱子到它的位置
// 使用神奇的数字是不好的,但帮助我们声明它的位置 ^_^
SharedMesh post1 = new SharedMesh( "post1" ,postGeometry);
post1.setLocalTranslation( new Vector3f(0,0.5f,0));
SharedMesh post2 = new SharedMesh( "post2" ,postGeometry);
post2.setLocalTranslation( new Vector3f(32,0.5f,0));
SharedMesh post3 = new SharedMesh( "post3" ,postGeometry);
post3.setLocalTranslation( new Vector3f(0,0.5f,32));
SharedMesh post4 = new SharedMesh( "post4" ,postGeometry);
post4.setLocalTranslation( new Vector3f(32,0.5f,32));
我们现在已经有了 4 个塔,而且他们都在正确的位置。现在我想要组织这些塔。它们将使用相同的纹理,因此,我把它们移动到一个单一的组并为这一组的父亲应用 TextureState 。
// 将所有的柱子放入一个 tower Node
Node towerNode = new Node( "tower" );
towerNode.attachChild(post1);
towerNode .attachChild(post2);
towerNode.attachChild(post3);
towerNode.attachChild(post4);
// 将 towerNode 放入不透明队列( Opaque queue ),我们不必看穿它
// 而我们想穿过 forcefield 看到它
towerNode.setRenderQueueMode(Renderer. QUEUE_OPAQUE );
// 为 towerNode 加载纹理
TextureState ts2 = display .getRenderer().createTextureState();
Texture t2 = TextureManager. loadTexture (
getClass().getClassLoader()
.getResource( "res/post.jpg" ),
Texture.MinificationFilter. Trilinear ,
Texture.MagnificationFilter. Bilinear
);
ts2.setTexture(t2);
towerNode.setRenderState(ts2);
我现在能通过控制 towerNode 一次性操纵 4 个 tower (移动、缩放它们等等)。
4.5 、剩余的框架
现在,我们准备构建 fence 中剩余的部分。我们将使用 Cylinder 构建一个框架,关于这些框架最有趣的一点是其中 2 个将需要旋转 90 度。为了做到这个,我们将使用 Quaternion 。我不会深入讲解 Quaternion 是什么(你可以看看用户向导)。但我们使用它的 fromAngleAxis 去旋转 2 个框架。同样的,这 4 个框架实际上是 SharedMesh 对象。而就像前面的塔,它们被 attach 到一个 Node 并拥有它们自己的 texture 。
我调整框架的位置,把它们放在塔那么高的地方。同样的,这里没有什么特别的,只是尝试并找出看起来好的。这些任意的数字,正是你通过手动创建对象而不是加载模型所能获得的。
// 这个圆柱体将是水平的框架
// 它将 field 限制在一个地方
Cylinder strutsGeometry = new Cylinder(
"struts" ,10,10,0.125f,32
);
strutsGeometry.setModelBound( new BoundingBox());
strutsGeometry.updateModelBound();
// 同样的,我们将共享 mesh
SharedMesh strut1 = new SharedMesh( "strut1" ,strutsGeometry);
Quaternion rotate90 = new Quaternion();
rotate90.fromAngleAxis(FastMath. PI /2, new Vector3f(0,1,0));
strut1.setLocalRotation(rotate90);
strut1.setLocalTranslation( new Vector3f(16,3f,0));
SharedMesh strut2 = new SharedMesh( "strut2" ,strutsGeometry);
strut2.setLocalTranslation( new Vector3f(0,3f,16));
SharedMesh strut3 = new SharedMesh( "strut3" ,strutsGeometry);
strut3.setLocalTranslation( new Vector3f(32,3f,16));
SharedMesh strut4 = new SharedMesh( "strut4" ,strutsGeometry);
strut4.setLocalRotation(rotate90);
strut4.setLocalTranslation( new Vector3f(16,3f,32));
// 将所有框架放入一个结点
Node strutNode = new Node( "strutNode" );
strutNode.attachChild(strut1);
strutNode.attachChild(strut2);
strutNode.attachChild(strut3);
strutNode.attachChild(strut4);
// 为框架加载纹理
TextureState ts3 = display .getRenderer().createTextureState();
Texture t3 = TextureManager. loadTexture (
getClass().getClassLoader()
.getResource( "res/rust.jpg" ),
Texture.MinificationFilter. Trilinear ,
Texture.MagnificationFilter. Bilinear
);
ts3.setTexture(t3);
strutNode.setRenderState(ts3);
现在,我们已经花了一些心思在塔和框架上。现在这个 fence 是一个高科技的 force field ,而且它不允许任何东西穿越它。这个 force field 看起来像是个挂在框架上的平面对象。我们将使用拥有未来主义 texture 的 Box 作为 force field 。我不再深入讨论这部分的细节,因为它只是再次使用 SharedMesh 并 attach 到它自己的 Node 。有趣的一点是,我没有像旋转框架一样去旋转 2 个 box 。取而代之的是,我使用 Box 的构造方法去创建 2 个符合我需求的形状。这没有真实的原因,只是想向你展示 Box 形状能不同。
// 创建真实的 forcefield
// 第一个 box 控制着 X 轴,而第二个控制着 z 轴
// 作为示例,我们没有旋转 box ,而是展示 box 能被不同地创建
Box forceFieldX = new Box(
"forceFieldX" ,
new Vector3f(-16, -3f, -0.1f),
new Vector3f(16, 3f, 0.1f)
);
forceFieldX.setModelBound( new BoundingBox());
forceFieldX.updateModelBound();
// 我们也将共享这些 box
SharedMesh forceFieldX1 = new SharedMesh(
"forceFieldX1" , forceFieldX
);
forceFieldX1.setLocalTranslation( new Vector3f(16,0,0));
SharedMesh forceFieldX2 = new SharedMesh(
"forceFieldX2" , forceFieldX
);
forceFieldX2.setLocalTranslation( new Vector3f(16,0,32));
// 另一个 box ,控制 z 轴的那个
Box forceFieldZ = new Box(
"forceFieldY" ,
new Vector3f(-0.1f, -3f, -16f),
new Vector3f(0.1f, 3f, 16f)
);
forceFieldZ.setModelBound( new BoundingBox());
forceFieldZ.updateModelBound();
// 我们也将共享这些 box
SharedMesh forceFieldZ1 = new SharedMesh(
"forceFieldZ1" , forceFieldZ
);
forceFieldZ1.setLocalTranslation( new Vector3f(0,0,16));
SharedMesh forceFieldZ2 = new SharedMesh(
"forceFieldZ2" , forceFieldZ
);
forceFieldZ2.setLocalTranslation( new Vector3f(32,0,16));
// 增加所有的 forceField 到一个单一的 Node
Node forceFieldNode = new Node( "forceFieldNode" );
forceFieldNode.attachChild(forceFieldX1);
forceFieldNode.attachChild(forceFieldX2);
forceFieldNode.attachChild(forceFieldZ1);
forceFieldNode.attachChild(forceFieldZ2);
// 为 force field 元素增加 texture
TextureState ts = display .getRenderer().createTextureState();
Texture t = TextureManager. loadTexture (
getClass().getClassLoader()
.getResource( "res/reflector.jpg" ),
Texture.MinificationFilter. Trilinear ,
Texture.MagnificationFilter. Bilinear
);
t.setWrap(Texture.WrapMode. Repeat );
t.setTranslation( new Vector3f());
ts.setTexture(t);
forceFieldNode.setRenderState(ts);
我们现在有三个包含所有 fence 中 Geometry 的 node 。为了后面更容易工作,我们将这三个 node 增加到一个单一的 Node 。我们能随着将这个 node attach 到 scene 并移动它的位置。
Node forceFieldFence = new Node( "forceFieldFence" );
forceFieldFence.attachChild(towerNode);
forceFieldFence.attachChild(strutNode);
forceFieldFence.attachChild(forceFieldNode);
一切都还好,但是 force field 确实看起来不像一个 force field 。我想做 2 件事情来改进它:透明和动画。
4.6 、透明和 RenderQueue
我想让 ForceField 透明。那就是,我想穿过它看到另一边,但仍然能辨别出粒子。为了做到这个,我们为包含 box 的构成 force field 的结点设置 AlphaState 。我们将附加 AlphaState :
// 为 transparent 结点增加 Alpha 值
BlendState as1 = display .getRenderer().createBlendState();
as1.setSourceFunction(
BlendState.SourceFunction. SourceAlpha
);
as1.setDestinationFunction(
BlendState.DestinationFunction. One
);
as1.setTestFunction(BlendState.TestFunction. GreaterThan );
as1.setBlendEnabled( true );
as1.setTestEnabled( true );
as1.setEnabled( true );
forceFieldNode.setRenderState(as1);
这允许 texture 的颜色和在它后面的其它颜色混合。因为我们的 force field 是由很多黑色组成的,黑色将会 100% 透明。那么,我们现在拥有悬停的点。
可是我们的程序仍然有问题。如果我们通过当前的设置能移动地图,我们可能有时候会看到在 force field 后面的对象,但不总是这样。那是因为透明对象取决于其他你能看到的在它后面的对象去决定是否绘制。但我们目前的 render 顺序不担保能这样做。 RenderQueue 的出现可以解决这个问题。我们能把 scene 中所有的元素放到这个 queue 中,而它将为我们正确排序。有 3 个主要的 queue (虽然还有其它的,但对我们而言不是很重要): transparent queue 正是我们放入 force field 的地方。 Opaque 是我们放入不透明对象的地方,而 Ortho 是 GUI 元素(在后面章节将讲到)将前往的地方。 RenderQueue 在下面几个方式上优化顺序:
l Opaque queue 最先被 render (所以我们所有不需要透明的对象都在这个 buffer 中)
l Transparency queue 接着被 render 。与 camera 相距最远的透明对象最先被 render 。这确保其它透明对象能通过另一个透明对象看到。
所以,我们将把我们的 Geometry 放入这个 queue 中:
forceFieldNode.setRenderQueueMode(Renderer. QUEUE_TRANSPARENT );
towerNode.setRenderQueueMode(Renderer. QUEUE_OPAQUE );
strutNode.setRenderQueueMode(Renderer. QUEUE_OPAQUE );
我们能设置实际 Geometry 对象的父亲 node ,因为默认上它们将继承 mode 。现在,当我们 render 时,透明的 forcefield 将能从其它面正确地看到。
force-field 看起来还是不够完美。我想要点看上去像是从框架下坠落一样,就像它们不断被产生。我们让 texture 运动!
4.7 、让 Force Field 的 texture 运动
让 texture 运动实际是很简单的。首先,我们将需要访问包含在 force-field 中的 Texture 对象。将它的引用移到类级别(使 force-field 的 texture 成为类变量)。
我们现在能在 update 方法中访问 Texture 了。
每个 texure 有它自己的矩阵。这个 texture 矩阵定义了 texture 的坐标是怎样被应用的。你能旋转 texture ,缩放它们,或者移动。在我们的例子中,我们想要沿着 Y 轴移动 texture 。实际上做起来和听起来是一样容易的:
// 我们将使用插值 (interpolation) 去保持 forcefield
//texture 运动速度和计算机保持一致
// 我们更新 texture 矩阵的 Y 值去让
//force-field 看起来像是在移动
t .getTranslation(). y += 0.3f*interpolation;
// 如果 translation 超过 1 ,它被换行,因此回到开始
// 并检查这个(防止 Vector 的 Y 值变得太大)
t .getTranslation(). y =
t .getTranslation(). y > 1 ? 0 : t .getTranslation(). y ;
一旦上面完成之后,我们将更真实看到一个强大的 fence( 如果你看不到下面这个图,不要紧张,因为可能是你的 fence field 放的位置太低的, 使用以下代码调整位置 forceFieldFence.setLocalTranslation(200,100,200); 这些数字只是简单的尝试得出的,看到效果一样之后再把这句去掉,因为后面会讲到怎样 调整 force-field 到 terrain 上 ) 。
4.8 、将 force-field 放置在 terrain 上
现在,我们想要放置这个 fence 在 terrain 上。我想要做到一些事。首先,我想要它在 terrain 里面一点,这样当玩家到达 fence 的时候它们不会看到它脱离了 terrain 。其次,它需要足够高和足够大去盖在 terrain 上。
我需要去玩这个东西直到找到我想要的结果。我发现将 fence 放大 5 倍正好是我想要的。我接着把它在 X 轴和 Z 轴移动 25 个单元。把它升到一个合适的高度,我获取 TerrainBlock 在 25 , 25 处的高度,并增加一些误差范围。
// 我们将手工做一些调整去让它更好适应 terrain
// 首先我们将实体 “ 模型 ” 放大
forceFieldFence.setLocalScale(5);
// 现在,让我们移动 fence 到 terrain 的高度并有一点陷入它里面
forceFieldFence.setLocalTranslation(
new Vector3f(25, tb .getHeight(25,25)+15,25)
);
4.9 、环绕的 Skybox
OK ,我们现在已经有一个很好的 fence 阻止我们的玩家跳出世界并掉落导致死亡。我们仍然看到非 terrain 部分是黑色,然而,这破坏了我们对这个大世界的感觉。我们所能做的是使用 Skybox 给人们一个巨大的世界的错觉。这个将通过一个有描述世界的图像的 box 包围 camera 。由于你不应该靠近地平线,这个 box 应该随着 camera 移动。
创建一个新的叫 createSkybox 并创建个类变量 skybox 。我想要这成为一个类变量,因为我们在 camera 移动的时候准备更新它。
Skybox 处理它自己所有的内部状态。这让它和其它大多数 scene 元素有点不同。所以,在这个例子中我们所要做的是创建一个新的 Skybox 并设置它的 textures 。我们将使用 texture 包含在 jmetest 部分并用于其它测试。我能寻找自己的 texture 并创建一个原创的 FlagRush 向导,但我有点懒,之后再说吧。
我们接着通过设置它的 localTranslation 告诉 Skybox 去初始化 texture 。最后把它作为 scene 的孩子添加到 scene 。
基本上我们做完了,下一步我们需要设置它的 localTranslation 为我们 cam 对象的位置。到 update 方法中增加:
// 我们想让 skybox 一直在我们的视野内,所以让它和 camera 一起移动
skybox .setLocalTranslation( cam .getLocation());
// 由于我们改变了场景(移动 skybox ),我们需要更新 scene graph
scene .updateGeometricState(interpolation, true );
这将让 Skybox 随着 camera 移动。就是那样。很简单就增加了世界!我们现在有一个看起来像下面一样的完整的水平面:
4.10 、总结
我们现在拥有一个完整的水平面,有 terrain , fence 和 sky 。这给我们一个可行的场地去放置我们的游戏对象并开始飞奔。
下一节课我将讨论怎样使用 ThirdPersonHandler 去控制一辆交通工具。我们离飞奔更近了。
4.11 、源码
import javax.swing.ImageIcon;
import com.jme.app.BaseGame;
import com.jme.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.input.KeyBindingManager;
import com.jme.input.KeyInput;
import com.jme.light.DirectionalLight;
import com.jme.math.FastMath;
import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.scene.Node;
import com.jme.scene.SharedMesh;
import com.jme.scene.Skybox;
import com.jme.scene.shape.Box;
import com.jme.scene.shape.Cylinder;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.LightState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.ZBufferState;
import com.jme.system.DisplaySystem;
import com.jme.system.JmeException;
import com.jme.util.TextureManager;
import com.jme.util.Timer;
import com.jmex.terrain.TerrainBlock;
import com.jmex.terrain.util.MidPointHeightMap;
import com.jmex.terrain.util.ProceduralTextureGenerator;
public class Lesson4 extends BaseGame{
private int width , height ;
private int freq , depth ;
private boolean fullscreen ;
// 我们的 camera 对象,用于观看 scene
private Camera cam ;
protected Timer timer ;
private Node scene ;
private TextureState ts ;
private TerrainBlock tb ;
private Texture t ;
private Skybox skybox ;
public static void main(String[] args) {
Lesson4 app = new Lesson4();
java.net.URL url =
app.getClass().getClassLoader()
.getResource( "res/logo.png" );
app.setConfigShowMode(ConfigShowMode. AlwaysShow ,url);
app.start();
}
/*
* 清除 texture
*/
protected void cleanup() {
ts .deleteAll();
}
protected void initGame() {
display .setTitle( "Flag Rush" );
scene = new Node( "Scene Graph Node" );
ZBufferState buf = display .getRenderer().createZBufferState();
buf.setEnabled( true );
buf.setFunction(ZBufferState.TestFunction. LessThanOrEqualTo );
scene .setRenderState(buf);
buildTerrain();
buildLighting();
buildEnvironment();
createSkybox();
// 更新 scene 用于渲染
scene .updateGeometricState(0.0f, true );
scene .updateRenderState();
}
private void createSkybox() {
skybox = new Skybox( "skybox" ,10,10,10);
Texture north = TextureManager. loadTexture (
Lesson4. class .getClassLoader()
.getResource( "res/texture/north.jpg" ),
Texture.MinificationFilter. BilinearNearestMipMap ,
Texture.MagnificationFilter. Bilinear
);
Texture south = TextureManager. loadTexture (
Lesson4. class .getClassLoader()
.getResource( "res/texture/south.jpg" ),
Texture.MinificationFilter. BilinearNearestMipMap ,
Texture.MagnificationFilter. Bilinear
);
Texture east = TextureManager. loadTexture (
Lesson4. class .getClassLoader()
.getResource( "res/texture/east.jpg" ),
Texture.MinificationFilter. BilinearNearestMipMap ,
Texture.MagnificationFilter. Bilinear
);
Texture west = TextureManager. loadTexture (
Lesson4. class .getClassLoader()
.getResource( "res/texture/west.jpg" ),
Texture.MinificationFilter. BilinearNearestMipMap ,
Texture.MagnificationFilter. Bilinear
);
Texture up = TextureManager. loadTexture (
Lesson4. class .getClassLoader()
.getResource( "res/texture/top.jpg" ),
Texture.MinificationFilter. BilinearNearestMipMap ,
Texture.MagnificationFilter. Bilinear
);
Texture down = TextureManager. loadTexture (
Lesson4. class .getClassLoader()
.getResource( "res/texture/bottom.jpg" ),
Texture.MinificationFilter. BilinearNearestMipMap ,
Texture.MagnificationFilter. Bilinear
);
skybox .setTexture(Skybox.Face. North , north);
skybox .setTexture(Skybox.Face. West , west);
skybox .setTexture(Skybox.Face. South , south);
skybox .setTexture(Skybox.Face. East , east);
skybox .setTexture(Skybox.Face. Up , up);
skybox .setTexture(Skybox.Face. Down , down);
skybox .preloadTextures();
scene .attachChild( skybox );
}
private void buildEnvironment() {
// 这个圆柱体将扮演每个角落那 4 个主要的柱子
Cylinder postGeometry = new Cylinder( "Cylinder" , 10, 10, 1, 10);
Quaternion q = new Quaternion();
// 将圆柱体转为垂直
q.fromAngleAxis(FastMath. PI /2, new Vector3f(1,0,0));
postGeometry.setLocalRotation(q);
postGeometry.setModelBound( new BoundingBox());
postGeometry.updateModelBound();
// 我们将共享柱子 4 次(每个柱子一次)
// 增加原始的圆柱体不是一种好的方法
// 因为 sharedmesh 将修改它的本地值
// 我们然后将调整柱子到它的位置
// 使用神奇的数字是不好的,但帮助我们声明它的位置 ^_^
SharedMesh post1 = new SharedMesh( "post1" ,postGeometry);
post1.setLocalTranslation( new Vector3f(0,0.5f,0));
SharedMesh post2 = new SharedMesh( "post2" ,postGeometry);
post2.setLocalTranslation( new Vector3f(32,0.5f,0));
SharedMesh post3 = new SharedMesh( "post3" ,postGeometry);
post3.setLocalTranslation( new Vector3f(0,0.5f,32));
SharedMesh post4 = new SharedMesh( "post4" ,postGeometry);
post4.setLocalTranslation( new Vector3f(32,0.5f,32));
// 将所有的柱子放入一个 tower Node
Node towerNode = new Node( "tower" );
towerNode.attachChild(post1);
towerNode.attachChild(post2);
towerNode.attachChild(post3);
towerNode.attachChild(post4);
// 将 towerNode 放入不透明队列( Opaque queue ),我们不必看穿它
// 而我们想穿过 forcefield 看到它
towerNode.setRenderQueueMode(Renderer. QUEUE_OPAQUE );
// 为 towerNode 加载纹理
TextureState ts2 = display .getRenderer().createTextureState();
Texture t2 = TextureManager. loadTexture (
getClass().getClassLoader()
.getResource( "res/post.jpg" ),
Texture.MinificationFilter. Trilinear ,
Texture.MagnificationFilter. Bilinear
);
ts2.setTexture(t2);
towerNode.setRenderState(ts2);
// 这个圆柱体将是水平的建筑
// 它将 field 限制在一个地方
Cylinder strutsGeometry =
new Cylinder( "struts" ,10,10,0.125f,32);
strutsGeometry.setModelBound( new BoundingBox());
strutsGeometry.updateModelBound();
// 同样的,我们将共享 mesh
SharedMesh strut1 = new SharedMesh( "strut1" ,strutsGeometry);
Quaternion rotate90 = new Quaternion();
rotate90.fromAngleAxis(FastMath. PI /2, new Vector3f(0,1,0));
strut1.setLocalRotation(rotate90);
strut1.setLocalTranslation( new Vector3f(16,3f,0));
SharedMesh strut2 = new SharedMesh( "strut2" ,strutsGeometry);
strut2.setLocalTranslation( new Vector3f(0,3f,16));
SharedMesh strut3 = new SharedMesh( "strut3" ,strutsGeometry);
strut3.setLocalTranslation( new Vector3f(32,3f,16));
SharedMesh strut4 = new SharedMesh( "strut4" ,strutsGeometry);
strut4.setLocalRotation(rotate90);
strut4.setLocalTranslation( new Vector3f(16,3f,32));
// 将所有建筑放入一个结点
Node strutNode = new Node( "strutNode" );
strutNode.attachChild(strut1);
strutNode.attachChild(strut2);
strutNode.attachChild(strut3);
strutNode.attachChild(strut4);
// 为建筑加载纹理
TextureState ts3 = display .getRenderer().createTextureState();
Texture t3 = TextureManager. loadTexture (
getClass().getClassLoader()
.getResource( "res/rust.jpg" ),
Texture.MinificationFilter. Trilinear ,
Texture.MagnificationFilter. Bilinear
);
ts3.setTexture(t3);
strutNode.setRenderState(ts3);
// 创建真实的 forcefield
// 第一个 box 控制着 X 轴,而第二个控制着 z 轴
// 作为示例,我们没有旋转 box ,而是展示 box 能被不同地创建
Box forceFieldX = new Box(
"forceFieldX" ,
new Vector3f(-16, -3f, -0.1f),
new Vector3f(16, 3f, 0.1f)
);
forceFieldX.setModelBound( new BoundingBox());
forceFieldX.updateModelBound();
// 我们也将共享这些 box
SharedMesh forceFieldX1 = new SharedMesh(
"forceFieldX1" , forceFieldX
);
forceFieldX1.setLocalTranslation( new Vector3f(16,0,0));
SharedMesh forceFieldX2 = new SharedMesh(
"forceFieldX2" , forceFieldX
);
forceFieldX2.setLocalTranslation( new Vector3f(16,0,32));
// 另一个 box ,控制 z 轴的那个
Box forceFieldZ = new Box(
"forceFieldY" ,
new Vector3f(-0.1f, -3f, -16f),
new Vector3f(0.1f, 3f, 16f)
);
forceFieldZ.setModelBound( new BoundingBox());
forceFieldZ.updateModelBound();
// 我们也将共享这些 box
SharedMesh forceFieldZ1 = new SharedMesh(
"forceFieldZ1" , forceFieldZ
);
forceFieldZ1.setLocalTranslation( new Vector3f(0,0,16));
SharedMesh forceFieldZ2 = new SharedMesh(
"forceFieldZ2" , forceFieldZ
);
forceFieldZ2.setLocalTranslation( new Vector3f(32,0,16));
// 增加所有的 forceField 到一个单一的 Node
Node forceFieldNode = new Node( "forceFieldNode" );
forceFieldNode.attachChild(forceFieldX1);
forceFieldNode.attachChild(forceFieldX2);
forceFieldNode.attachChild(forceFieldZ1);
forceFieldNode.attachChild(forceFieldZ2);
// 为 force field 元素增加 texture
TextureState ts = display .getRenderer().createTextureState();
t = TextureManager. loadTexture (
getClass().getClassLoader()
.getResource( "res/reflector.jpg" ),
Texture.MinificationFilter. Trilinear ,
Texture.MagnificationFilter. Bilinear
);
t .setWrap(Texture.WrapMode. Repeat );
t .setTranslation( new Vector3f());
ts.setTexture( t );
// 为 transparent 结点增加 Alpha 值
BlendState as1 = display .getRenderer().createBlendState();
as1.setSourceFunction(
BlendState.SourceFunction. SourceAlpha
);
as1.setDestinationFunction(
BlendState.DestinationFunction. One
);
as1.setTestFunction(BlendState.TestFunction. GreaterThan );
as1.setBlendEnabled( true );
as1.setTestEnabled( true );
as1.setEnabled( true );
forceFieldNode.setRenderState(as1);
forceFieldNode.setRenderState(ts);
forceFieldNode.setRenderQueueMode(Renderer. QUEUE_TRANSPARENT );
towerNode.setRenderQueueMode(Renderer. QUEUE_OPAQUE );
strutNode.setRenderQueueMode(Renderer. QUEUE_OPAQUE );
Node forceFieldFence = new Node( "forceFieldFence" );
forceFieldFence.attachChild(towerNode);
forceFieldFence.attachChild(strutNode);
forceFieldFence.attachChild(forceFieldNode);
// 我们将手工做一些调整去让它更好适应 terrain
// 首先我们将实体 “ 模型 ” 放大
forceFieldFence.setLocalScale(5);
// 现在,让我们移动 fence 到 terrain 的高度并有一点陷入它里面
forceFieldFence.setLocalTranslation(
new Vector3f(25, tb .getHeight(25,25)+15,25)
);
scene .attachChild(forceFieldFence);
}
private void buildLighting() {
/* 设置一个基础、默认灯光 */
DirectionalLight light = new DirectionalLight();
light.setDiffuse( new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
light.setAmbient( new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f));
light.setDirection( new Vector3f(1, -1, 0));
light.setEnabled( true );
LightState lightState =
display .getRenderer().createLightState();
lightState.setEnabled( true );
lightState.attach(light);
scene .setRenderState(lightState);
}
/**
* 创建 heightmap 和 terrainBlock
*/
private void buildTerrain() {
// 生成随机地形数据
MidPointHeightMap heightMap = new MidPointHeightMap(64,1f);
// 缩放数据
Vector3f terrainScale = new Vector3f(4, .0575f, 4);
// 创建一个 terrain block
tb = new TerrainBlock(
"terrain" ,
heightMap.getSize(),
terrainScale,
heightMap.getHeightMap(),
new Vector3f(0, 0, 0)
);
tb .setModelBound( new BoundingBox());
tb .updateModelBound();
// 通过三个纹理生成地形纹理
ProceduralTextureGenerator pt =
new ProceduralTextureGenerator(heightMap);
pt.addTexture(
new ImageIcon(
getClass().getClassLoader()
.getResource( "res/grassb.png" )
),
-128, 0, 128
);
pt.addTexture(
new ImageIcon(
getClass().getClassLoader()
.getResource( "res/dirt.jpg" )
),
0, 128, 256
);
pt.addTexture(
new ImageIcon(
getClass().getClassLoader()
.getResource( "res/highest.jpg" )
),
128, 256, 374
);
pt.createTexture(32);
// 将纹理赋予地形
ts = display .getRenderer().createTextureState();
Texture t1 = TextureManager. loadTexture (
pt.getImageIcon().getImage(),
Texture.MinificationFilter. Trilinear ,
Texture.MagnificationFilter. Bilinear ,
true
);
ts .setTexture(t1, 0);
tb .setRenderState( ts );
scene .attachChild( tb );
}
protected void initSystem() {
// 保存属性信息
width = settings .getWidth();
height = settings .getHeight();
depth = settings .getDepth();
freq = settings .getFrequency();
fullscreen = settings .isFullscreen();
try {
display = DisplaySystem. getDisplaySystem (
settings .getRenderer()
);
display .createWindow(
width , height , depth , freq , fullscreen
);
cam = display .getRenderer().createCamera( width , height );
} catch (JmeException e){
e.printStackTrace();
System. exit (-1);
}
// 设置背景为黑色
display .getRenderer().setBackgroundColor(ColorRGBA. black );
// 初始化摄像机
cam .setFrustumPerspective(
45.0f,
( float ) width /( float ) height ,
1f,
5000f
);
Vector3f loc = new Vector3f(250f,100f,250f);
Vector3f left = new Vector3f(-0.5f,0.0f,0.5f);
Vector3f up = new Vector3f(0.0f,1.0f,0.0f);
Vector3f dir = new Vector3f(-0.5f,0.0f,-0.5f);
// 将摄像机移到正确位置和方向
cam .setFrame(loc, left, up, dir);
// 我们改变自己的摄像机位置和视锥的标志
cam .update();
// 获取一个高分辨率用于 FPS 更新
timer = Timer. getTimer ();
display .getRenderer().setCamera( cam );
KeyBindingManager. getKeyBindingManager ().set(
"exit" ,
KeyInput. KEY_ESCAPE
);
}
/*
* 如果分辨率改变将被调用
*/
protected void reinit() {
display .recreateWindow( width , height , depth , freq , fullscreen );
}
/*
* 绘制场景图
*/
protected void render( float interpolation) {
// 清除屏幕
display .getRenderer().clearBuffers();
display .getRenderer().draw( scene );
}
/*
* 在 update 期间,我们只需寻找 Escape 按钮
* 并更新 timer 去获取帧率
*/
protected void update( float interpolation) {
// 更新 timer 去获取帧率
timer .update();
interpolation = timer .getTimePerFrame();
// 我们将使用插值 (interpolation) 去保持 forcefield
//texture 运动速度和计算机保持一致
// 我们更新 texture 矩阵的 Y 值去让
//force-field 看起来像是在移动
t .getTranslation(). y += 0.3f*interpolation;
// 如果 translation 超过 1 ,它被换行,因此回到开始
// 并检查这个(防止 Vector 的 Y 值变得太大)
t .getTranslation(). y =
t .getTranslation(). y > 1 ? 0 : t .getTranslation(). y ;
// 我们想让 skybox 一直在我们的视野内,所以让它和 camera 一起移动
skybox .setLocalTranslation( cam .getLocation());
// 当 Escape 被按下时,我们退出游戏
if (KeyBindingManager. getKeyBindingManager ()
.isValidCommand( "exit" )
){
finished = true ;
}
// 由于我们改变了场景(移动 skybox ),我们需要更新 scene graph
scene .updateGeometricState(interpolation, true );
}
}