Libgdx New 3D API 教程之 -- Libgdx中的3D frustum culling

This blog is a chinese version of xoppa's Libgdx new 3D api tutorial. For English version, please refer to >>LINK<<

我要偷懒了,好久没看LibGDX,也很久没看3D,新教程的题目我就不懂了。不过看了课程的内容,相信你们都会理解。

正文:

当渲染一个3d场景时,其中真正可见的对象通常都比总对象数少很多。因此渲染全部的物体,包括那些根本看不到的,即浪费了富贵的GPU时间,还会影响游戏的画面速度。理想情况下,你可以可希望渲染对当前相机可见的对象,而忽略掉那些不可见的,比较在相机后面的。这就是题目中所说的frustum culling,并且,几种方法都可以实现。这篇教程,将会展示在LibGDX 3D API中,最基本的实现方法。

在我们真正开始之前,我们需要准备一个场景。所以我使用了Libgdx New 3D API 教程之 -- 使用Libgdx加载3D场景中的场景和代码。我当你都看过以前的教程啦,所以代码的细节就不详说了。

我希望在在frustum culling时,能有一些反馈,来说明frustum culling的表现。所以让我们添加一个label,显示,有渲染对象的个数,再加上FPS。看看下面参考的代码先:

protected PerspectiveCamera cam;
    protected CameraInputController camController;
    protected ModelBatch modelBatch;
    protected AssetManager assets;
    protected Array instances = new Array();
    protected Environment environment;
    protected boolean loading;
      
    protected Array blocks = new Array();
    protected Array invaders = new Array();
    protected ModelInstance ship;
    protected ModelInstance space;
     
    protected Stage stage;
    protected Label label;
    protected BitmapFont font;
    protected StringBuilder stringBuilder;
      
    @Override
    public void create () {
        stage = new Stage();
        font = new BitmapFont();
        label = new Label(" ", new Label.LabelStyle(font, Color.WHITE));
        stage.addActor(label);
        stringBuilder = new StringBuilder();
         
        modelBatch = new ModelBatch();
        environment = new Environment();
        environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f));
        environment.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 = 1f;
        cam.far = 300f;
        cam.update();
  
        camController = new CameraInputController(cam);
        Gdx.input.setInputProcessor(camController);
          
        assets = new AssetManager();
        assets.load(data+"/invaderscene.g3db", Model.class);
        loading = true;
    }
  
    private void doneLoading() {
        Model model = assets.get(data+"/invaderscene.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, true);
 
            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;
    }
      
    private int visibleCount;
    @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);
        visibleCount = 0;
        for (final ModelInstance instance : instances) {
            if (isVisible(cam, instance)) {
                modelBatch.render(instance, environment);
                visibleCount++;
            }
        }
        if (space != null)
            modelBatch.render(space);
        modelBatch.end();
         
        stringBuilder.setLength(0);
        stringBuilder.append(" FPS: ").append(Gdx.graphics.getFramesPerSecond());
        stringBuilder.append(" Visible: ").append(visibleCount);
        label.setText(stringBuilder);
        stage.draw();
    }
     
    protected boolean isVisible(final Camera cam, final ModelInstance instance) {
        return true; // FIXME: Implement frustum culling
    }
      
    @Override
    public void dispose () {
        modelBatch.dispose();
        instances.clear();
        assets.dispose();
    }
 
    @Override
    public void resize(int width, int height) {
        stage.getViewport().update(width, height, true);
    }
 
    @Override
    public void pause() {
    }
 
    @Override
    public void resume() {
    }
}

只有几处改动,看一下,首先有一个Stage, 然后有Label, BitmapFont 和 StringBuilder

protected Stage stage;
protected Label label;
protected BitmapFont font;
protected StringBuilder stringBuilder;

然后在create方法中,对这些进行初始化:

@Override
public void create () {
    stage = new Stage();
    font = new BitmapFont();
    label = new Label(" ", new Label.LabelStyle(font, Color.WHITE));
    stage.addActor(label);
    stringBuilder = new StringBuilder();
    ...
}

注意在doneLoading方法中,我用了一个简便方法去创建ModelInstance。第三个参数(mergeTransform)等价于我们以前手动所做的内容,即:设置ModelInstance的位移,并重置Node的位移。(翻译的好啰嗦,还能再啰嗦点吗?)

如果你对Stage的概念不熟,建议你去看一看,因为它是实现UI界面很好的方式。现在我们来看看在render方法里,是如何去画界面的。

private int visibleCount;
@Override
public void render () {
    ...
    modelBatch.begin(cam);
    visibleCount = 0;
    for (final ModelInstance instance : instances) {
        if (isVisible(cam, instance)) {
            modelBatch.render(instance, environment);
            visibleCount++;
        }
    }
    if (space != null)
        modelBatch.render(space);
    modelBatch.end();
     
    stringBuilder.setLength(0);
    stringBuilder.append(" FPS: ").append(Gdx.graphics.getFramesPerSecond());
    stringBuilder.append(" Visible: ").append(visibleCount);
    label.setText(stringBuilder);
    stage.draw();
}

我们不再一次性画出所有的对象,而是先去看看它是否可见,才去渲染。我们还会修改visibleCount去检测实际渲染对象的数量。这里space model instance不去判断,因为一直可见。

通过StringBuilder构造一个字符串,去显示FPS和可见对象数量(不包括space)。设置label的值,然后,终于,开draw画了。在render方法中,极度推荐使用StringBuilder而不是默认的string还是其他什么东东。StringBuilder制造更少的垃圾,几乎不会有垃圾回收引起的卡顿。

@Override
public void resize(int width, int height) {
    stage.getViewport().update(width, height, true);
}

在resize方法中,更新stage的viewport大小。最后的boolean参数表示原点位于左下角。而label对象就画在那个位置。

所以,真正神奇的地方就在isVisible()方法里,通过它来判断,对象是否将会渲染。现在,还只是一个方法体,始终返回true。

protected boolean isVisible(final Camera cam, final ModelInstance instance) {
    return true; // FIXME: Implement frustum culling
}

先来跑跑,看看效果:


可以看到,现在共有37个对象,1艘飞船,6个方块,30个入侵者,(还有一个星空,这里不计算在内)。如果转一下相机,会发现,不管对象是否都真的可见,数值一直为37。是时候实现frustum culling了。

如果你对frustum这个词不熟:frustum可以被看成是3D空间中一个金字塔型的锥体,以相机为一起始,向前延伸,整个这个相机可以看到的空间范围,(中文翻译为:视锥体原。原作者还推荐了一篇讲视锥体的文章,我这就不译了)。

好在LibGDX提供了一些简便方法来判断对象是否在frustum中。我们添上这段验证:

private Vector3 position = new Vector3();
protected boolean isVisible(final Camera cam, final ModelInstance instance) {
    instance.transform.getTranslation(position);
    return cam.frustum.pointInFrustum(position);
}

我们在这里添加了一个Vector3对象,来保存位置。在isVisible方法中,我们得到ModelInstance的位置,然后判断是否在frustum中。看看实际效果:


看起来不错,可是你要看仔细点,就会发现,在转相机的时候,对象会闪入,闪出!他们被剔除的太早了。因为我们通过instance.transform.getTranslation(position)方法得到对象的位置时,对应的是他们的中心点。而当这一点在frustum之外时,并不一定表示整个对象都在frustum之外。比如飞船的中心在frustum之外时,对于相机而言,还可以看到它的右翼。

要解决这个问题,就要确保对象整个都在frustum之外时,才剔除。然而,如果对这个对象的每一个点都进行这样的判断,那代价就太大了。我们可以估计对象在相机中的尺寸,以此判断。这样可以保证,当对象在frustum中时,一定会被渲染,但会造成一点多余的消耗。(有时,虽然对象本体不在frustum中,但是它的尺寸还在,比如一个正方体的盒子,刚刚好装下一个球,虽然球离开了frustum,但是盒子,还是有一块留在于frustum中)。

做这样做,我们必须在ModelInstance中,保存它自身的尺寸。可以简单的扩展ModelInstance类来实现:

public class FrustumCullingTest implements ApplicationListener {
    public static class GameObject extends ModelInstance {
        public final Vector3 center = new Vector3();
        public final Vector3 dimensions = new Vector3();
         
        private final static BoundingBox bounds = new BoundingBox();
         
        public GameObject(Model model, String rootNode, boolean mergeTransform) {
            super(model, rootNode, mergeTransform);
            calculateBoundingBox(bounds);
            bounds.getCenter(center);
            bounds.getDimensions(dimensions);
        }
    }
    ...
}

这里,我们计算了ModelInstance的BoundingBox,BoundingBox的中心也许与物体的中心不重合。因此,我们把它保存到Vector3类型的center中,将ModelInstance的demensions,保存在dimensions成员中。这里的boundingbox是一个静态成员,所以它是被全部的GameObject重用的。

我们继承了ModelInstance对象,所以把之前的都替换掉。这里不去把全部代码都打出来了,只讲讲重点的内容。

接下来,我们更新isVisible方法,使用这些尺寸数据:

protected boolean isVisible(final Camera cam, final GameObject instance) {
    instance.transform.getTranslation(position);
    position.add(instance.center);
    return cam.frustum.boundsInFrustum(position, instance.dimensions);
}

如果你运行这个,会发现,边缘的对象不再闪入闪出了,而且frustum culling的效果也还不错。然而,这种方法不是万能的。比如,当一个GameObject旋转时,他的尺寸向量不会跟着转。最简单,可能也最快速的方法就是用一个球体来包含旋转时的最大范围。我们来改一下GameObject:

public static class GameObject extends ModelInstance {
    public final Vector3 center = new Vector3();
    public final Vector3 dimensions = new Vector3();
    public final float radius;
     
    private final static BoundingBox bounds = new BoundingBox();
     
    public GameObject(Model model, String rootNode, boolean mergeTransform) {
        super(model, rootNode, mergeTransform);
        calculateBoundingBox(bounds);
        bounds.getCenter(center);
        bounds.getDimensions(dimensions);
        radius = dimensions.len() / 2f;
    }
}

我简单的用dimension的一半当作半径,然后接下来再改isVisible方法:

protected boolean isVisible(final Camera cam, final GameObject instance) {
    instance.transform.getTranslation(position);
    position.add(instance.center);
    return cam.frustum.sphereInFrustum(position, instance.radius);
}

这里使用了sphereInFrustum这个方法来做判断。虽然使用球体半径来判断会快很多,但也可能有更多的边缘对象,被多余的渲染了。


                
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值