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这个方法来做判断。虽然使用球体半径来判断会快很多,但也可能有更多的边缘对象,被多余的渲染了。

				
时间: 2024-11-27 20:00:03

Libgdx New 3D API 教程之 -- Libgdx中的3D frustum culling的相关文章

Libgdx New 3D API 教程之 -- 加载3D场景的背后-第二部分

http://bbs.9ria.com/thread-221699-1-1.html 在本教程的第一部分,我们已经看过LibGDX 3D API中Model类的总体结构.在第2部分中,我们将会分析渲染管道,从加载模型开始,到真正的渲染模型.我们将不会在渲染管道的某个问题上进行深入探讨.我们只会介绍一些非常基本的内容,这是我觉得你使用3D API时,应该了解的. 在这一部分,我们要分析渲染究竟做了什么.明白我们在渲染时所做的事很重要.在前一部分本教程,我们已经看到,一个Model是由很多个Node

Libgdx New 3D API 教程之 -- 使用Libgdx加载模型

http://bbs.9ria.com/thread-221701-1-1.html 在前面的教程中,我们已经看到如何设置libgdx渲染3D场景.我们已经设置了Camera,增加了一些灯光并渲染一个绿色的盒子.现在让我们添加一个比盒子更有趣的东西,模型Model. 您可以从您喜爱的建模应用程序或使用已有的模型.我找了gdx-invaders里面的飞船模型文件,你可以点这里下载.您可以解压缩后,将文件放到的android项目的assets目录下.请注意,它包含三个文件,这些文件需要放同一个文件夹

libgdx游戏引擎3D开发教程-第一章-基础教程

开卷语:我最近才开始学习游戏编程,因为想做个网游玩,所以前几天找了不少引擎来看,于是不出意料的选中了libgdx,值得感谢的是libgdx的文档很多很全,所以没有走多少弯路就成功的配置好了环境.基础教程很完善,好多大神都已经写的很详细了,但是3D方面的很少见,所以我正好要学,索性直接翻译过来,大家共同进步.注意:教程基本是从Wiki上翻译过来,外加自己的小部分理解,所以一般来说应该没什么问题,如有错误请多多指教. =========================================

Html5 本地文件读取 API 研究使用过程中的意外发现 - MDN

太阳火神的美丽人生 (http://blog.csdn.net/opengl_es) 本文遵循"署名-非商业用途-保持一致"创作公用协议 转载请保留此句:太阳火神的美丽人生 -  本博客专注于 敏捷开发及移动和物联设备研究:iOS.Android.Html5.Arduino.pcDuino,否则,出自本博客的文章拒绝转载或再转载,谢谢合作. 补充: 终于可以读出肉的眼能看懂的人类文字了!!! 经在 safari 7.0.4(9537.76.4) 测试,即使关闭 WebGL 支持,Fil

Yii2框架RESTful API教程(二) - 格式化响应,授权认证和速率限制

之前写过一篇Yii2框架RESTful API教程(一) - 快速入门,今天接着来探究一下Yii2 RESTful的格式化响应,授权认证和速率限制三个部分 一.目录结构 先列出需要改动的文件.目录如下: web ├─ common │ └─ models │ └ User.php └─ frontend ├─ config │ └ main.php └─ controllers └ BookController.php 二.格式化响应 Yii2 RESTful支持JSON和XML格式,如果想指定

Yii2框架RESTful API教程(一) - 快速入门

前不久做一个项目,是用Yii2框架写一套RESTful风格的API,就去查了下<Yii 2.0 权威指南 >,发现上面写得比较简略.所以就在这里写一篇教程贴,希望帮助刚接触Yii2框架RESTful的小伙伴快速入门. 一.目录结构 实现一个简单地RESTful API只需用到三个文件.目录如下: frontend ├─ config │ └ main.php ├─ controllers │ └ BookController.php └─ models └ Book.php 二.配置URL规则

Windows API 教程(七) hook 钩子监听

Windows API 教程(七) hook 钩子监听 Posted on 2013-08-15 茵蒂克丝 如何创建一个窗口 手动创建窗口的流程 实际代码 安装钩子 (Install hook) 钩子简介 SetWindowsHookEx 函数 设置监听[键盘]消息 设置监听[鼠标]消息 如何创建一个窗口 另外一个再录的 Windows SDK教程 里面有讲到快捷创建窗口的方式,不过这样的话要分好几个文件,感觉有点混所以这里就用原始的方式创建一个窗口. 那么,为什么讲到 hook(钩子)的时候要

NeHe OpenGL教程 第五课:3D空间

转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢. 第五课:3D空间 3D空间: 我们使用多边形和四边形创建3D物体,在这一课里,我们把三角形变为立体的金子塔形状,把四边形变为立方体. 在上节课的内容上作些扩展,我们现在开始生成真正的3D对象,而不是象前两节课中那样3D世界中的2D对象.我们给三角形增加一

Windows API教程文件系统

本篇文章主要介绍了"Windows API教程文件系统",主要涉及到Windows API教程文件系统方面的内容,对于Windows API教程文件系统感兴趣的同学可以参考一下. 索引 概念简介 文件对象 文件流 文件句柄 文件指针 文件系统操作 常见 API 高级文件操作 本讲程序功能列表 CreateFile 具体参数 返回值 DeleteFile 参数 返回值 CopyFile.MoveFile.FindFirstFile ReadFile GetCurrentDirectory