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