This blog is a chinese version of xoppa's Libgdx new 3D api tutorial. For English version, please refer to >>LINK<<
在前面的教程中,我们已经了解了如何使用Libgdx转换,加载和显示模型。现在我们要来看看如何加载一个完整的3D场景。
我们使用与之前相同的代码作为基础,我将其改名为SceneTest。在这个类中,我们有一个ModelInstance数组,我们将用它来定义场景。我们已经看到了如何装载飞船模型,让我们添加一些更多的模型。您可以 点击这里下载我用的,它包含4个模型(OBJ)文件:我们以前使用的ship模型,还有从gdx-invaders找的invader.obj,block.obj模型,和spacesphere的模型,我把他们打了一个包。球体空间就是一个大球体,与它的纹理和扭转法线(所以纹理贴图是从里面可见)。
在此之前,我们使用FBX-CONV转换模型。这里也一样,但现在让我们只使用obj文件。因此,请确保将这些文件拷贝到assets/data目录下,然后按之前说的那样,加载他们。下载是完整代码:
public class SceneTest implements ApplicationListener {
public PerspectiveCamera cam;
public CameraInputController camController;
public ModelBatch modelBatch;
public AssetManager assets;
public Array<ModelInstance> instances = new Array<ModelInstance>();
public Lights lights;
public boolean loading;
public Array<ModelInstance> blocks = new Array<ModelInstance>();
public Array<ModelInstance> invaders = new Array<ModelInstance>();
public ModelInstance ship;
public ModelInstance space;
@Override
public void create () {
modelBatch = new ModelBatch();
lights = new Lights();
lights.ambientLight.set(0.4f, 0.4f, 0.4f, 1f);
lights.add(new DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -0.8f, -0.2f));
cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
cam.position.set(0f, 7f, 10f);
cam.lookAt(0,0,0);
cam.near = 0.1f;
cam.far = 300f;
cam.update();
camController = new CameraInputController(cam);
Gdx.input.setInputProcessor(camController);
assets = new AssetManager();
assets.load("data/ship.obj", Model.class);
assets.load("data/block.obj", Model.class);
assets.load("data/invader.obj", Model.class);
assets.load("data/spacesphere.obj", Model.class);
loading = true;
}
private void doneLoading() {
ship = new ModelInstance(assets.get("data/ship.obj", Model.class));
ship.transform.setToRotation(Vector3.Y, 180).trn(0, 0, 6f);
instances.add(ship);
Model blockModel = assets.get("data/block.obj", Model.class);
for (float x = -5f; x <= 5f; x += 2f) {
ModelInstance block = new ModelInstance(blockModel);
block.transform.setToTranslation(x, 0, 3f);
instances.add(block);
blocks.add(block);
}
Model invaderModel = assets.get("data/invader.obj", Model.class);
for (float x = -5f; x <= 5f; x += 2f) {
for (float z = -8f; z <= 0f; z += 2f) {
ModelInstance invader = new ModelInstance(invaderModel);
invader.transform.setToTranslation(x, 0, z);
instances.add(invader);
invaders.add(invader);
}
}
space = new ModelInstance(assets.get("data/spacesphere.obj", Model.class));
loading = false;
}
@Override
public void render () {
if (loading && assets.update())
doneLoading();
camController.update();
Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
modelBatch.begin(cam);
for (ModelInstance instance : instances)
modelBatch.render(instance, lights);
if (space != null)
modelBatch.render(space);
modelBatch.end();
}
@Override
public void dispose () {
modelBatch.dispose();
instances.clear();
assets.dispose();
}
@Override
public void resume () {
}
@Override
public void resize (int width, int height) {
}
@Override
public void pause () {
}
}
现在来看一下都改了什么:
public Array<ModelInstance> blocks = new Array<ModelInstance>();
public Array<ModelInstance> invaders = new Array<ModelInstance>();
public ModelInstance ship;
public ModelInstance space;
我们使用数组来保存blocks和invaders. 并用两个单独的ModelInstance对象来在存储ship和space. 我们还是用instances array来渲染,但现在这样,可以让我们更方便的访问场景中的每一种元素。如,我们想要移动ship, 我们就直接使用ship instance.
public void create () {
modelBatch = new ModelBatch();
...
cam.position.set(0f, 7f, 10f);
...
assets.load("data/ship.obj", Model.class);
assets.load("data/block.obj", Model.class);
assets.load("data/invader.obj", Model.class);
assets.load("data/spacesphere.obj", Model.class);
loading = true;
}
我们重新设置了照相机的位置,这样更合适我们将要加载的场景。然后,我们要通过AssetManager来加载所有的模型。
private void doneLoading() {
ship = new ModelInstance(assets.get("data/ship.obj", Model.class));
ship.transform.setToRotation(Vector3.Y, 180).trn(0, 0, 6f);
instances.add(ship);
Model blockModel = assets.get("data/block.obj", Model.class);
for (float x = -5f; x <= 5f; x += 2f) {
ModelInstance block = new ModelInstance(blockModel);
block.transform.setToTranslation(x, 0, 3f);
instances.add(block);
blocks.add(block);
}
Model invaderModel = assets.get("data/invader.obj", Model.class);
for (float x = -5f; x <= 5f; x += 2f) {
for (float z = -8f; z <= 0f; z += 2f) {
ModelInstance invader = new ModelInstance(invaderModel);
invader.transform.setToTranslation(x, 0, z);
instances.add(invader);
invaders.add(invader);
}
}
space = new ModelInstance(assets.get("data/spacesphere.obj", Model.class));
loading = false;
}
这里开始有趣了,第一行,我们得到飞船的模型,并通过它创建了飞船的instance,旋转了180度,使它背对着相机,向着相机,移动6个单位。最后,第三行,我们把ship instance加进了array,这样ship就可以被渲染了。
接下来用同样的方法,载入block和invader模型。只是现在我们要创建多个实例,而不是单个对象。Block的实例会沿着X轴的方向横向排列,我们会把blocks添加到instances和blocks两个数组中,同样的,这样方便调用,访问对象。如:看飞船是不是撞到了block。Invader实例会在XZ构成的平面上的方格排列。
最后,初始化空间实例。这个就不用添加到instances数组中了。因为这个对象,我们不会对它添加光照效果。
public void render () {
...
modelBatch.begin(cam);
for (ModelInstance instance : instances)
modelBatch.render(instance, lights);
if (space != null)
modelBatch.render(space);
modelBatch.end();
}
在render方法里,我们渲染模型实例跟以前一样。只是现在,我们把空间模型单独渲染,并且不添加灯光效果。我们需要在渲染space之前,看看这个对象有没有成功加载,因为这个对象是通过asset manager异步加载的.
现在来看一下运行效果:
怎么样,这很漂亮吧。我们可以添加一些玩法,然后命名为“一关”(不知道这里译的准不准。),现实中,很多游戏,就是这样的形式开发出来的。但大型的游戏就行不通,所以,这里要优化一下。
比如,在这里,我们只加载了4个模型,可是更大的场景里,你一定会加载得更多,所以,让我们继续。
打开你最熟悉的建模工具,开启一个空的场景。个人用Maya,但这个例子,大家用什么都一样啦。先import测试中所用的4个模型到这个场景中,如果你像我一样,都是一个菜鸟,那你可以一次导入一个,然后看看每一个都显示正确了。比如,我手动指定了纹理,并翻转了纹理坐标(不知道译的对不对,3D的内容,我是小菜一根)。同时,给每一个模型都起个名字,其它的,不要做任何移动。然后,看起来就是下面这样:
我开启了X-Ray,这样编辑方便点,所以大家看到的效果会发现,所有的模型都是半透明的。背景是空间球,我给它起了个名字叫:“space”。前面有ship, block, 和 invader,三个模型混合在一起。因为他们现在的位置都是 0, 0, 0。当全部模型都显示正确,并且起了名字,我们就可以将这个场景导出为一个FBX文件了。我给他取名:invaders.fbx。你也许想导出到建模软件自有的格式,我们迟点也许需要。
现在,我们将FBX文件转成G3DB. 确认你下载到最新的fbx-conv,然后运行命令:
fbx-conv invaders.fbx
如果在生成FBX时,你有翻转纹理坐标,那你现在也要翻转,在命令中添加一个选项:
fbx-conv -f invaders.fbx
如果想查看全部的命令选项,可以运行无任何参数的fbx-conv命令。
现在,我们把刚刚生成的invaders.g3db文件拷贝到assets/data目录下,并在代码中加载:
public class SceneTest extends GdxTest implements ApplicationListener {
...
@Override
public void create () {
...
assets = new AssetManager();
assets.load("data/invaders.g3db", Model.class);
loading = true;
}
private void doneLoading() {
Model model = assets.get("data/invaders.g3db", Model.class);
ship = new ModelInstance(model, "ship");
ship.transform.setToRotation(Vector3.Y, 180).trn(0, 0, 6f);
instances.add(ship);
for (float x = -5f; x <= 5f; x += 2f) {
ModelInstance block = new ModelInstance(model, "block");
block.transform.setToTranslation(x, 0, 3f);
instances.add(block);
blocks.add(block);
}
for (float x = -5f; x <= 5f; x += 2f) {
for (float z = -8f; z <= 0f; z += 2f) {
ModelInstance invader = new ModelInstance(model, "invader");
invader.transform.setToTranslation(x, 0, z);
instances.add(invader);
invaders.add(invader);
}
}
space = new ModelInstance(model, "space");
loading = false;
}
...
}
在create方法中,我们移除了加载单个模型的代码,改为只加载一个invaders.g3db。在doneLoading()方法中,我们从asset manager拿到模型对象。在创建model instance的时候,我们提供了在建模时给模型起的名字。如:对于ship模型来说,我们告诉ModelInstance,只创建ship模型的实例。而这名字,就自然要跟我们建模时起的一致。我们迟点会深入讨论这个,但现在我们运行看看,出来的效果,跟原来一模一样。
嗯,这是非常有用的。一个场景,我们只需要在一个单一的文件,就可以提供所有模型。更重要的,因为我们现在只有一个模型文件,所有的ModelInstances共享资源。这样ModelBatch的性能可以得到优化(之后还会更多)。当然,你需要的话仍然可以用多个文件,(例如皮肤或动画模型),实际上有些情况更合适使用单独的文件。
但是,还有更多。让我们回到建模工具,并确保我们用的是之前相同的场景。现在选中ship模型,围绕它的Y轴旋转180度,并沿Z轴正向移动6个单位。就像我们在Java代码中做的。接下来选中block模型,沿Z轴移动3个单位,X轴上移动-5个单位,然后改名“block”为“block1”。现在,选中invader模型,X轴移动-5个单位,重命名为 "invader1"。
接下来复制(实例)block1模型五次,每个模型之间,X轴上相距2个单位长度,最后,有6个相互挨着的block。确保它们被命名为“block1”到“block6”。复制实例(可能因建模应用程序的不同而异)可以确保每个实例共享相同的顶点数据。现在对invader1做同样的处理,也沿Z轴,所以,最后得到的,是和代码中一样的网格状。它应该是这个样子:
提示一下,在建模工具中,网格的间距是5个单位。
现在导出FBX,并转成g3db:
fbx-conv -f invaders.fbx
private void doneLoading() {
Model model = assets.get("data/invaders.g3db", Model.class);
for (int i = 0; i < model.nodes.size; i++) {
String id = model.nodes.get(i).id;
ModelInstance instance = new ModelInstance(model, id);
Node node = instance.getNode(id);
instance.transform.set(node.globalTransform);
node.translation.set(0,0,0);
node.scale.set(1,1,1);
node.rotation.idt();
instance.calculateTransforms();
if (id.equals("space")) {
space = instance;
continue;
}
instances.add(instance);
if (id.equals("ship"))
ship = instance;
else if (id.startsWith("block"))
blocks.add(instance);
else if (id.startsWith("invader"))
invaders.add(instance);
}
loading = false;
}
首先,加载invaders的模型文件,跟以前一样。像我们之前看到的,这里通过我们在建模工具中指定的名字,包含了全部的模型。这些模型都是invaders模型文件的节点(Node)。所以,我们迭代全部的Node,并得到他们的名字。然后我们创建一个Model Instance,使用model和id作为参数,还是跟以前一样。下面一行,我们从实例中或取Node,这是模型文件中Node的一个复本。
接下来,我们把Node的位移信息赋给它的模型实例。这实际上是我们刚刚在建模工具中设置的每一个模型的位置信息。然后重置Node的位移信息,因为我们现在要通过Model Instance来计算位移。所以,位移设置为 0, 0, 0,缩放是1, 1, 1,重置旋转。之后调用calculateTransforms(),这样model instance就可以计算,并更新自己的位置信息。
现在,我们的ModelInstance对象就像是在建模应用程序中一样,我们需要将它添加到我们的场景。对于id = space的Model Instance,我们简单地将其赋给space Model Instance,因为接下来,我们要手动渲染space。不然,我们会把它添加到的实例数组,以便它得到正确渲染。最后,我们检查的id,无论它开始是“ship”,“block”或是“invader”,我们相应地赋给ship的ModelInstance或将它添加到相应的模型数组。
运行它,看,和我们前面做的完全一样。但是,现在完全在建模程序中设计我们的场景,这使得我们在设计的时候容易了许多。