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