Libgdx New 3D API 教程之 -- 使用Libgdx加载3D场景

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


使用-f选项来翻转纹理坐标,这个是有些建模工具才有需要的。然后在代码中加载:

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或将它添加到相应的模型数组。


运行它,看,和我们前面做的完全一样。但是,现在完全在建模程序中设计我们的场景,这使得我们在设计的时候容易了许多。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值