libGDX. Отбраковка объектов не попадающих в обзор камеры.
При визуализации 3D сцены часто число видимых объектов намного меньше общего числа объектов в сцене. Некоторые объекты не попадают в угол обзора камеры. Рендеринг не попавших в камеру объектов - неблагоразумная трата ресурсов графического процессора и как следствие снижение производительности. Поэтому нам нужно позаботится, чтобы отрисовывались только те объекты, которые попадают в обзор камеры. Рассмотрим, что нужно для этого сделать.
В качестве заготовки будем использовать сцену с деревьями из прошлой статьи. Для того, что бы иметь обратную связь (видеть числовое представление FPS и количество отрисованных объектов), нам понадобятся несколько новых объектов. Stage - сцена для отображения двумерных объектов, Label - надпись, BitmapFont - шрифт. А также конструктор строк - для построения текстовой строки и целочисленная переменная - в качестве счетчика отрисованных моделей.
protected Stage stage;
protected Label label;
protected BitmapFont font;
protected StringBuilder stringBuilder;
private int visibleModels;
@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();
......
}
Обратите внимание, как надпись добавляется в двумерную сцену. Теперь изменим немного метод render():
public void render () {
camController.update();
Gdx.gl.glViewport ( 0 , 0 , Gdx.graphics.getWidth (), Gdx.graphics.getHeight ());
Gdx.gl.glClearColor(0.3f, 0.5f, 1f, 1f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT|GL20.GL_DEPTH_BUFFER_BIT);
modelBatch.begin(cam);
visibleModels = 0;
for(final ModelInstance tree: forest){
if(IsVisible(cam, tree)){
modelBatch.render(tree, environment);
visibleModels++;
}
}
modelBatch.render(groundInstance, environment);
visibleModels++;
modelBatch.end();
stringBuilder.setLength(0);
stringBuilder.append(" FPS: ").append(Gdx.graphics.getFramesPerSecond());
stringBuilder.append(" Visible: ").append(visibleModels);
label.setText(stringBuilder);
stage.draw();
}
protected boolean IsVisible(final Camera cam, final ModelInstance instance) {
return true;
}
Обнуляем счетчик, Далее отрисовываем модели из массива forest, но теперь передаем в ModelBatch весь массив, а перебираем каждый элемент массива по отдельности в цикле. Внутри цикла мы проверяем видимость объекта методом IsVisible, который пока напишем как заглушку - он всегда будет возвращать true. В случае выполнения условия рисуем модель и увеличиваем счетчик на 1. Затем отрисовываем площадку на которой все будет стоять и увеличиваем счетчик еще на 1 (площадка ведь тоже модель).
Далее собираем строку. FPS получаем методом Gdx.graphics.getFramesPerSecond(), а количество отрисованных моделей из нашего счетчика. Передаем строку в надпись и рисуем двумерную сцену. Попробуем запустить. Поскольку метод IsVisible пока ничего не делает и render() отображает все модели, в строке Visible будет показано общее число моделей.

Теперь изменим метод IsVisible. Добавим в него проверку видимости объекта. В libGDX для этого есть специальные методы.
protected boolean IsVisible(final Camera cam, final ModelInstance instance) {
Vector3 position = new Vector3();
instance.transform.getTranslation(position);
return cam.frustum.pointInFrustum(position);
}
Объявим объект Vector3 - в нем могут храниться координаты точки в трехмерном пространстве. И запишем в него координаты центра проверяемой модели. Передаем полученные координаты в метод cam.frustum.pointInFrustum, который возвратит нам true в случае, если эта точка попадает в угол обзора камеры. Запускаем. Пробуем перемещать камеру и наблюдаем как меняется число отрисованных моделей в строке Visible.

Количество отображаемых моделей меняется, FPS нет. Это потому, что общее количество моделей не очень большое и графический процессор нормально справляется со всей сценой. Что бы продемонстрировать изменение FPS, я увеличил количество моделей в сцене. Вот что у меня получилось:

Число FPS в данном случае упало, когда количество отрисовываемых моделей перевалило за 700 штук. Но следует помнить, что FPS зависит не столько от количества моделей, сколько от числа используемых в них полигонов. При использовании высокополигональных объектов FPS упадет при меньшем количестве моделей. А также FPS зависит от мощности графического процессора и общей производительности компьютера. Если вы разрабатываете приложение для андроид, не забывайте, что производительность мобильных устройств обычно ниже, чем у настольных компьютеров.
Это было небольшое отступление. Вернемся к предыдущей сцене. Если внимательно посмотреть на края окна, то можно заметить, что модели исчезают внезапно, даже когда должна быть видна часть объекта. И также внезапно появляются Это происходит потому, что для проверки видимости мы используем центр модели, т.е. если центр объекта не попадает в область видимости камеры - модель не отрисовывается. Это не очень хорошо (например, мы не увидим нападающего монстра, пока большая его часть не попадет в область видимости камеры).
Для того чтобы решить эту проблему, мы должны убедиться, что весь объект на самом деле не попадает в обзор камеры. Проверка каждой вершины модели будет делом очень утомительным и скорее всего будет иметь обратный эффект - производительность упадет. Мы можем убедится, что объект находится вне видимости камеры, используя размеры модели. Такой способ позволит нам отрисовать модели, которые видны только частично, но в некоторых случаях могут быть ложные срабатывания.
Чтобы реализовать такой способ, удобно будет хранить размер модели вместе с самим экземпляром. Для этого нам нужно расширить класс ModelInstance.
public static class ModelInstanceAdvanced extends ModelInstance{
public final Vector3 center = new Vector3();
public final Vector3 dimens = new Vector3();
private final static BoundingBox bound = new BoundingBox();
public ModelInstanceAdvanced(Model model) {
super(model);
calculateBoundingBox(bound);
bound.getCenter(center);
bound.getDimensions(dimens);
}
}
Объявляем два объекта Vector3 для хранения координат центра и размеров модели. А также объект BoundingBox для хранения границ невидимой "коробки", в которой будет находиться модель. Именно попадание этой коробки в обзор камеры мы и будем проверять. В конструкторе вычисляем границы "коробки" для модели и записываем полученные центр и размеры в объекты Vector3. Теперь наш новый класс будет вычислять размеры и координаты центра при создании экземпляра и хранить их вместе с экземпляром. Такое расширение класса нам необходимо для того, чтобы не вычислять эти данные при каждом цикле рендеринга.
Изменяем тип массива экземпляров моделей и вносим соответствующие изменения в коде.
public Array<ModelInstanceAdvanced> forest =
new Array<ModelInstanceAdvanced>();
.......
@Override
public void create () {
.........
Random rnd = new Random();
for(float x = -25f; x < 25f; x +=5f){
for(float z = -25f; z < 25f; z +=5f){
if(rnd.nextInt(10) > 2){
.....
ModelInstanceAdvanced tree;
if(rnd.nextInt(2) == 0){
tree = new ModelInstanceAdvanced(LeafTree);
}else{
tree = new ModelInstanceAdvanced(pineTree);
}
......
}
}
}
......
}
@Override
public void render () {
.......
for(final ModelInstanceAdvanced tree: forest){
if(IsVisible(cam, tree)){
modelBatch.render(tree, environment);
visibleModels++;
}
}
.....
}
Теперь изменим проверку попадания объекта в область обзора камеры. Получаем положение модели относительно начала координат, добавляем к нему локальные координаты центра модели. Затем передаем полученные координаты центра и размеры экземпляра модели передаем в метод cam.frustum.boundsInFrustum, который возвращает true, в случае попадания границ "коробки" в обзор камеры.
protected boolean IsVisible(final Camera cam, final ModelInstanceAdvanced instance) {
Vector3 position = new Vector3();
instance.transform.getTranslation(position);
position.add (instance.center);
return cam.frustum.boundsInFrustum(position, instance.dimens);
}
Мне кажется, что здесь нужно некоторое объяснение - для чего мы складываем два вектора. Дело в том, что метод instance.transform.getTranslation дает вектор, который указывает на точку экземпляра модели находившуюся в точке начала координат в момент создания экземпляра, т.е., можно сказать, на центр основания модели. А instance.center дает центр модели относительно координатного начала самой модели, который находится как раз в точке куда указывает первый вектор. Наверное, не совсем понятно, поэтому я решил объяснить это графически:

Красный вектор - это instance.transform.getTranslation, желтый - instance.center. Сумма этих векторов даст координаты центра объекта относительно глобального начала координат. Не знаю насколько это объяснение было необходимо - возможно кому-то это понятно и так. Но для меня причина сложения векторов была неочевидна с первого взгляда, поэтому я заострил на ней внимание.
Запускаем приложение. Подвигав камерой видим, объекты не появляются и не исчезают внезапно. Но иногда число видимых моделей в информационной строке больше, чем число реально видимых объектов в камере. Это ложные срабатывания - часть границы "коробки" попадает в обзор камеры, а графической составляющей модели в этой области нет. Вот например такой момент - число реально видимых моделей 10, а рендерится 12.

У такого метода отбраковки есть еще один минус - если экземпляр модели повернуть объект, границы его "коробки" не повернутся и, соответственно, перестанут отражать его реальное положение. Самый простой и, вероятно, самый лучший, способ - использовать вместо "коробки" сферу с центром совпадающим с центром объекта, которая будет учитывать все возможные повороты экземпляра модели. Внесем поправки в код.
public static class ModelInstanceAdvanced extends ModelInstance{
public final Vector3 center = new Vector3();
public final Vector3 dimens = new Vector3();
public final float radius;
private final static BoundingBox bound = new BoundingBox();
public ModelInstanceAdvanced(Model model) {
super(model);
calculateBoundingBox(bound);
bound.getCenter(center);
bound.getDimensions(dimens);
radius = dimens.len()/2f;
}
}
.........
protected boolean IsVisible(final Camera cam, final ModelInstanceAdvanced instance) {
Vector3 position = new Vector3();
instance.transform.getTranslation(position);
position.add (instance.center);
return cam.frustum.sphereInFrustum(position, instance.radius);
}
В классе ModelInstanceAdvanced устанавливаем радиус равным половине размера модели. А в методе IsVisible проверяем попадание объекта в камеру еще одним методом - cam.frustum.sphereInFrustum. Такой способ учитывает все положения модели, но приводит к большему числу ложных срабатываний.
В статье использованы материалы туториала xoppa.
Полный листинг главного класса (проверка по сфере).
import java.util.Random;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.VertexAttributes.Usage;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g3d.Environment;
import com.badlogic.gdx.graphics.g3d.Material;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.ModelBatch;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
import com.badlogic.gdx.graphics.g3d.model.Node;
import com.badlogic.gdx.graphics.g3d.utils.CameraInputController;
import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder;
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.math.collision.BoundingBox;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.utils.Array;
public class Proba3D extends ApplicationAdapter {
public static class ModelInstanceAdvanced extends ModelInstance{
public final Vector3 center = new Vector3();
public final Vector3 dimens = new Vector3();
public final float radius;
private final static BoundingBox bound = new BoundingBox();
public ModelInstanceAdvanced(Model model) {
super(model);
calculateBoundingBox(bound);
bound.getCenter(center);
bound.getDimensions(dimens);
radius = dimens.len()/2f;
}
}
public PerspectiveCamera cam;
public Model pineTree, LeafTree, ground;
public ModelInstance groundInstance;
public Array<ModelInstanceAdvanced> forest = new Array<ModelInstanceAdvanced>();
public ModelBatch modelBatch;
public Environment environment;
public CameraInputController camController;
public Material pine, trunk, land, crown, skin;
protected Stage stage;
protected Label label;
protected BitmapFont font;
protected StringBuilder stringBuilder;
private int visibleModels;
@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(10f, 10f, 10f);
cam.lookAt(0f, 0f, 0f);
cam.near = 1f;
cam.far = 300f;
cam.update();
camController = new CameraInputController(cam);
Gdx.input.setInputProcessor(camController);
pine = new Material(ColorAttribute.createDiffuse(Color.GREEN));
crown = new Material(ColorAttribute.createDiffuse(Color.OLIVE));
trunk = new Material(ColorAttribute.createDiffuse(Color.GRAY));
TextureAttribute land_attr = TextureAttribute.createDiffuse(new Texture("land.jpg"));
land = new Material(land_attr);
skin = new Material(ColorAttribute.createDiffuse(Color.PINK));
ModelBuilder modelBuilder = new ModelBuilder();
modelBuilder.begin();
Node node1 = modelBuilder.node();
node1.id = "node1";
node1.translation.set(0f, 2.8f, 0f);
MeshPartBuilder meshBuilder;
meshBuilder = modelBuilder.part("part1", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, pine);
meshBuilder.cone(1f, 1f, 1f, 20);
Node node2 = modelBuilder.node();
node2.id = "node2";
node2.translation.set(0f, 2f, 0f);
meshBuilder = modelBuilder.part("part2", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, pine);
meshBuilder.cone(2f, 1.5f, 2f, 20);
Node node3 = modelBuilder.node();
node3.id = "node3";
node3.translation.set(0f, 1f, 0f);
meshBuilder = modelBuilder.part("part3", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, pine);
meshBuilder.cone(3f, 2f, 3f, 20);
Node node4 = modelBuilder.node();
node4.id = "node4";
meshBuilder = modelBuilder.part("part4", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, trunk);
meshBuilder.cylinder(1f, 1f, 1f, 20);
pineTree = modelBuilder.end();
modelBuilder.begin();
Node lnode1 = modelBuilder.node();
lnode1.id = "lnode1";
lnode1.translation.set(0f, 1f, 0f);
meshBuilder = modelBuilder.part("part1", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, crown);
meshBuilder.sphere(2f, 2f, 2f, 20, 20);
Node lnode2 = modelBuilder.node();
lnode2.id = "lnode2";
lnode2.translation.set(0.3f, 1.7f, 0f);
meshBuilder = modelBuilder.part("part1", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, crown);
meshBuilder.sphere(2f, 2f, 2f, 20, 20);
Node lnode3 = modelBuilder.node();
lnode3.id = "lnode3";
lnode3.translation.set(-0.3f, 1.7f, 0.3f);
meshBuilder = modelBuilder.part("part1", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, crown);
meshBuilder.sphere(2f, 2f, 2f, 20, 20);
Node lnode4 = modelBuilder.node();
lnode4.id = "lnode4";
lnode4.translation.set(-0.3f, 1.7f, -0.3f);
meshBuilder = modelBuilder.part("part1", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, crown);
meshBuilder.sphere(2f, 2f, 2f, 20, 20);
Node lnode5 = modelBuilder.node();
lnode5.id = "lnode5";
lnode5.translation.set(0f, 2.2f, 0f);
meshBuilder = modelBuilder.part("part1", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, crown);
meshBuilder.sphere(2f, 2f, 2f, 20, 20);
Node lnode6 = modelBuilder.node();
lnode6.id = "lnode6";
meshBuilder = modelBuilder.part("part4", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, trunk);
meshBuilder.cylinder(0.7f, 1f, 0.7f, 20);
LeafTree = modelBuilder.end();
Random rnd = new Random();
for(float x = -25f; x < 25f; x +=5f){
for(float z = -25f; z < 25f; z +=5f){
if(rnd.nextInt(10) > 2){
float x_offset = 4*(0.5f - rnd.nextFloat());
float z_offset = 4*(0.5f - rnd.nextFloat());
ModelInstanceAdvanced tree;
if(rnd.nextInt(2) == 0){
tree = new ModelInstanceAdvanced(LeafTree);
}else{
tree = new ModelInstanceAdvanced(pineTree);
}
tree.transform.setToTranslation(x + x_offset, 0, z + z_offset );
forest.add(tree);
}
}
}
ground = modelBuilder.createBox(54f, 0.1f, 54f, land, Usage.Position|Usage.Normal|Usage.TextureCoordinates);
groundInstance = new ModelInstance(ground);
groundInstance.transform.setTranslation(0f, -0.5f, 0f);
}
@Override
public void render () {
camController.update();
Gdx.gl.glViewport ( 0 , 0 , Gdx.graphics.getWidth (), Gdx.graphics.getHeight ());
Gdx.gl.glClearColor(0.3f, 0.5f, 1f, 1f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT|GL20.GL_DEPTH_BUFFER_BIT);
modelBatch.begin(cam);
visibleModels = 0;
for(final ModelInstanceAdvanced tree: forest){
if(IsVisible(cam, tree)){
modelBatch.render(tree, environment);
visibleModels++;
}
}
modelBatch.render(groundInstance, environment);
visibleModels++;
modelBatch.end();
stringBuilder.setLength(0);
stringBuilder.append(" FPS: ").append(Gdx.graphics.getFramesPerSecond());
stringBuilder.append(" Visible: ").append(visibleModels);
label.setText(stringBuilder);
stage.draw();
}
protected boolean IsVisible(final Camera cam, final ModelInstanceAdvanced instance) {
Vector3 position = new Vector3();
instance.transform.getTranslation(position);
position.add (instance.center);
return cam.frustum.sphereInFrustum(position, instance.radius);
}
@Override
public void resize(int width, int height) {
stage.getViewport().update(width, height, true);
}
@Override
public void dispose(){
modelBatch.dispose();
pineTree.dispose();
LeafTree.dispose();
}
}
3D модель для libGDX. Пишем код.
3D модель для движка libGDX
Строим модель с помощью ModelBuilder
libGDX. Основы 3D программирования.
Игра Flower. Ловим капли.
TexturePacker.Создаем атлас текстур.
Создаем проект на движке libGDX
Кастомизация EditText
Кастомизация SeekBar'а
9-patch изображения для Андроид
Кастомный ползунок в виде дуги (аналог SeekBar)
Анимация в Андроид
Кастомизация элементов управления в Android
Смартфон DEXP Ixion ML 5, обзор.
Создание кастомного View-элемента интерфейса.
Создание виджета - электронные часы с кастомным шрифтом
Будильник для Андроид "Разбуди меня"
Программируем калькулятор на андроид. Урок 1. |