Докуметация Cтарт Статьи Форум Лента Вход
Не официальное русскоязычное сообщество
Главная
    Визуализация
        Визуализация

Andry

Опубликованно: AndryAndry10.01.2019, 15:34
Последняя редакция, Andry: 10.01.2019 21:20

В первой части мы имели дело с настройкой и некоторым грубым скелетом и просто кораблем на экране, но все еще без системы сущностей. Ну, мы сделали настройку, но ещё не использовали систему сущностей. В этой части мы действительно будем её использовать. Обещаю!

Мы хотим захватчиков

Действительно, игра про захватчиков без захватчиков не является игрой о захватчиках, поэтому снова запустите ваш blender и смоделируйте захватчика. Конечно, у меня есть один для вас на моем Google диске, чтобы поделиться с вами. Я назвал его BasicInvader.

А теперь, мой друг, мы будем использовать систему сущностей для хранения данных группы захватчиков и звездного корабля на базе. Что нам нужно, чтобы указать корабль и захватчиков, которые должны появиться на экране? Нам нужна позиция и модель, чтобы иметь возможность напечатать ее на экране. Хорошо, обычно вы начинаете с какого-то игрового объекта и наследуете от него свой корабль, своего захватчика и так далее, и так далее. Возможно, вы бы злоупотребили JME Spatial, чтобы даже придерживаться некоторой игровой логики, и довольно скоро начали бы создавать беспорядочные спагетти, подобные архитектуре. По крайней мере, это происходит со мной. Поэтому очень хорошая идея отделить логику от визуализации. С системой сущностей вы делаете это в экстремальных условиях. С помощью системы управления сущностями мы можем в любое время расширить наш корабль с помощью любого необходимого нам компонента, не затрагивая большую часть существующего кода, в основном ни одного. Даже данные и методы разделены, что полностью противоположно объектно-ориентированному программированию, и если вы глубоко в объектно-ориентированном программировании, этот подход может вас или не может сбить с толку. Но будь терпелив со мной и с самим собой.

Какова моя позиция

Наш корабль, а также наши захватчики нуждаются в позиции, и это делается с помощью компонента позиции.

package mygame;

import com.jme3.math.Vector3f;
import com.simsilica.es.EntityComponent;

public class Position implements EntityComponent {

    private final Vector3f location;

    public Position(Vector3f location) {
        this.location = location;
    }

    public Vector3f getLocation() {
        return location;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "[" + location + "]";
    }
}

Вы видите, что компонент — это исходные и чистые данные, которые помогают запускать системы в разных потоках, не мешая синхронизации. Мы будем иметь дело с многопоточностью позже.

Кто, черт возьми, я

Конечно, чтобы знать, какую модель нам следует загрузить, нам также необходим компонент модели. Похоже на компонент позиции выше и представляет собой чистые данные.

package mygame;

import com.simsilica.es.EntityComponent;

public class Model implements EntityComponent {
    private final String name;
    public final static String SpaceShip = "SpaceShip";
    public final static String BasicInvader = "BasicInvader";

    public Model(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
    
    @Override
    public String toString() {
        return "Model[" + name + "]";
    }
}

Чтобы избежать опечаток, я ввел некоторые статические строки для космических кораблей и базовых кораблей захватчиков. Теперь, как это входит в игру?

Генерировать энтиты

Наконец, мы будем использовать систему сущностей для хранения данных захватчиков и корабля, в данный момент графического представления нет. Сущности просто больше ничего. Вы должны добавить компоненты к этой сущности, чтобы придать ей смысл. А затем вы запрашиваете сущности с конкретными компонентами, в нашем случае Position и Model. Это как база данных. Итак, что нам сейчас нужно, так это GameAppState, в котором мы выстраиваем наши уровни, осуществляем переходы от начала к игре, от игры к мертвому и от мертвого к возрожденному и тому подобное. Но давайте сначала сделаем это глупо простым, а потом уточним.

package mygame;

import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.math.Vector3f;
import com.simsilica.es.EntityData;
import com.simsilica.es.EntityId;

public class GameAppState extends AbstractAppState {

    private EntityData ed;
    private SimpleApplication app;

    @Override
    public void initialize(AppStateManager stateManager, Application app) {

        this.app = (SimpleApplication) app;
        this.app.setPauseOnLostFocus(true);
        this.app.setDisplayStatView(true);

        this.ed = this.app.getStateManager().getState(EntityDataState.class).getEntityData();

        EntityId ship = ed.createEntity();
        this.ed.setComponents(ship,
                new Position(new Vector3f(0, -20, 0)),
                new Model(Model.SpaceShip));
        for (int x = -20; x < 20; x += 4) {
            for (int y = 0; y < 20; y += 4) {
                EntityId invader = ed.createEntity();
                this.ed.setComponents(invader,
                        new Position(new Vector3f(x, y, 0)),
                        new Model(Model.BasicInvader));
            }
        }
    }

    @Override
    public void cleanup() {
    }

    @Override
    public void update(float tpf) {
    }

}

Таким образом, мы генерируем только корабли, которые достаточно, чтобы показать вам систему сущностей в действии.

Покажи что у тебя есть

И вот оно … первая система сущностей, которая отображает сгенерированные захватчики. И наш корабль конечно. Просто замените ваш текущий VisualAppState на следующий.

package mygame;

import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.light.DirectionalLight;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
import com.simsilica.es.Entity;
import com.simsilica.es.EntityData;
import com.simsilica.es.EntityId;
import com.simsilica.es.EntitySet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class VisualAppState extends AbstractAppState {

    private SimpleApplication app;
    private EntityData ed;
    private EntitySet entities;
    private final Map<EntityId, Spatial> models;
    private ModelFactory modelFactory;

    public VisualAppState() {
        this.models = new HashMap<>();
    }

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        this.app = (SimpleApplication) app;

        ed = this.app.getStateManager().getState(EntityDataState.class).getEntityData();
        entities = ed.getEntities(Position.class, Model.class);

        app.getCamera().lookAt(Vector3f.UNIT_Z, Vector3f.UNIT_Y);
        app.getCamera().setLocation(new Vector3f(0, 0, 60));

        DirectionalLight light = new DirectionalLight();
        light.setDirection(new Vector3f(1, 1, -1));
        this.app.getRootNode().addLight(light);

        modelFactory = new ModelFactory(this.app.getAssetManager());
    }

    @Override
    public void cleanup() {
        entities.release();
        entities = null;
    }

    @Override
    public void update(float tpf) {
        if (entities.applyChanges()) {
            removeModels(entities.getRemovedEntities());
            addModels(entities.getAddedEntities());
            updateModels(entities.getChangedEntities());
        }
    }

    private void removeModels(Set<Entity> entities) {
        for (Entity e : entities) {
            Spatial s = models.remove(e.getId());
            s.removeFromParent();
        }
    }

    private void addModels(Set<Entity> entities) {
        for (Entity e : entities) {
            Spatial s = createVisual(e);
            models.put(e.getId(), s);
            updateModelSpatial(e, s);
            this.app.getRootNode().attachChild(s);
        }
    }

    private void updateModels(Set<Entity> entities) {
        for (Entity e : entities) {
            Spatial s = models.get(e.getId());
            updateModelSpatial(e, s);
        }
    }

    private void updateModelSpatial(Entity e, Spatial s) {
        Position p = e.get(Position.class);
        s.setLocalTranslation(p.getLocation());
    }

    private Spatial createVisual(Entity e) {
        Model model = e.get(Model.class);
        return modelFactory.create(model.getName());
    }
}

это, кстати, своего рода первый шаблон проектирования в программировании систем сущностей, поскольку вы наверняка будете использовать его во всех своих играх, по крайней мере, на стороне клиента.

Подробности пожалуйста

Так что у нас здесь. Сначала мы определяем набор сущностей, с которыми мы хотим работать

        ed = this.app.getStateManager().getState(EntityDataState.class).getEntityData();
        entities = ed.getEntities(Position.class, Model.class);

Нас интересуют все лица с позицией и моделью. Это работает для захватчиков и для нашего космического корабля, так как оба имеют эти компоненты.

Следующая часть — это цикл обновления, сердце каждой игры. JME предлагает это для каждого зарегистрированного состояния приложения.

    public void update(float tpf) {
        if (entities.applyChanges()) {
            removeModels(entities.getRemovedEntities());
            addModels(entities.getAddedEntities());
            updateModels(entities.getChangedEntities());
        }
    }

Мы проверяем, есть ли изменения в наборе сущностей, а затем обрабатываем удаленные сущности, добавленные сущности и измененные сущности. Все пространственные объекты и соответствующий идентификатор сущности управляются с помощью «локальной» хэш-карты

    private final Map<EntityId, Spatial> models;

Таким образом, мы можем удалить добавленную сущность из визуального узла позже. Давайте прямо перейдем к методу removeModels.

    private void removeModels(Set<Entity> entities) {
        for (Entity e : entities) {
            Spatial s = models.remove(e.getId());
            s.removeFromParent();
        }
    }

Пространство удерживается в хэш-карте моделей и получается с идентификатором удаленного объекта, поэтому я могу удалить его из визуального узла. Это все. Хорошо, прежде чем мы можем удалить что-то, это должно быть добавлено некоторое время, прежде чем, конечно. Добавленные объекты обрабатываются методом addModels

    private void addModels(Set<Entity> entities) {
        for (Entity e : entities) {
            Spatial s = createVisual(e);
            models.put(e.getId(), s);
            updateModelSpatial(e, s);
            this.app.getRootNode().attachChild(s);
        }
    }

Вы можете видеть, что мы храним все созданные пространственные объекты в хэш-карте моделей и добавляем их также в корневой узел, чтобы сделать их визуальными.

UpdateModel довольно прост и просто обновляет позицию пространства, в которой изменяются соответствующие сигналы идентификатора объекта.

В конце вы должны зарегистрировать все эти новые состояния приложения в Main, как это

    public Main() {
        super(new VisualAppState(),
                new GameAppState(),
                new EntityDataState());
    }

Вот и все. Ваш первый программный продукт, управляемый ES.


Переведено для jmonkeyengine.ru, оригинал
Автор перевода: Andry

Начнем с главной части …

package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.renderer.RenderManager;

public class Main extends SimpleApplication {

    public static void main(String[] args) {
        Main app = new Main();
        app.start();
    }

    public Main() {
    }

    @Override
    public void simpleInitApp() {
    }

    @Override
    public void simpleUpdate(float tpf) {
    }

    @Override
    public void simpleRender(RenderManager rm) {
    }
}

Нажмите F6 и начните игру. После выбора разрешения вы видите только черный экран. Не очень страшно на самом деле.

Говорят все дело в системе сущностей, поэтому давайте настроим и инициализируем Zay ES, прежде чем мы перейдем к самой игре.

package mygame;

import com.jme3.app.state.AbstractAppState;
import com.simsilica.es.EntityData;
import com.simsilica.es.base.DefaultEntityData;

public class EntityDataState extends AbstractAppState {
    private EntityData entityData;

    public EntityDataState() {
        this(new DefaultEntityData());
    }

    public EntityDataState( EntityData ed ) {
        this.entityData = ed;
    }

    public EntityData getEntityData() {
        return entityData;
    }

    @Override
    public void cleanup() {
        entityData.close();
        entityData = null; // не может быть повторно использован
    }
}

Существует не так много, способов чтобы описать это состоянии приложения. Это как если мозг просто мёртв.

Черный экран? Я имею в виду … привет ?! На самом деле играть без визуального представления не очень весело. Кроме того, так мы всегда можем видеть что-то на экране и иметь обратную связь, что поддерживает нас счастливыми и мотивированными.

Звездный корабль. Нам нужны модели, которые мы могли бы сделать с помощью Blender и импортировать их в JME3.

Сначала нам нужен космический корабль, так что запустите свой blender и сделайте его. Но, я не хочу тратить ваше время, поэтому у меня есть один на моем Google диске, чтобы поделиться с вами. Но это не значит, что вы не можете придумать вашу собственную модель.

Если у вас есть модель, то поместите blender файл в папку игровых ресурсов assets/Models, затем просто щелкните [ПК мыши] blender модель в JME3 и выберите Convert to JME Binary(Преобразовать в двоичный j3o файл).

Фабрика моделей. Решение проблемы создания моделей с помощью Blender, чтобы получать spatial-ы в JME3, была бы отличной, это упрощает ситуацию.

package mygame;

import com.jme3.asset.AssetManager;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
 
public class ModelFactory {
    private final AssetManager assetManager;
 
    public ModelFactory(AssetManager assetManager) {
        this.assetManager = assetManager;
    }

    public Spatial create(String name) {
        Node visual = new Node("Visual");
        Node model = (Node) assetManager.loadModel("Models/" + name + ".j3o");
        visual.attachChild(model);
        return visual;
    }
}

Это подразумевает, что у нас есть наши модели в Project Assets (на самом деле это папка с именем assets) и там в папке Models. Я думаю, что не нужно много объяснений, я позволю говорить коду самому за себя. Конечно, вы можете сделать свою фабрику лучше, больше ОО или нет, мне все равно.

Быстро и грязно. Итак, теперь мы хотим увидеть этот корабль на экране. Для этого мы начнем с визуальной системы, я назвал ее VisualAppState, и она выглядит следующим образом

package mygame;

import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.light.DirectionalLight;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;

public class VisualAppState extends AbstractAppState {
    private SimpleApplication app;
    private ModelFactory modelFactory;
 
    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        this.app = (SimpleApplication) app;
        app.getCamera().lookAt(Vector3f.UNIT_Z, Vector3f.UNIT_Y);
        app.getCamera().setLocation(new Vector3f(0, 0, 60));
        DirectionalLight light = new DirectionalLight();
        light.setDirection(new Vector3f(1, 1, -1));
        this.app.getRootNode().addLight(light);
        modelFactory = new ModelFactory(this.app.getAssetManager());
        Spatial myVisual = modelFactory.create("SpaceShip");
        this.app.getRootNode().attachChild(myVisual);
    }

    @Override
    public void cleanup() {
    }

    @Override
    public void update(float tpf) {
    }
}

Я буду считать, что ваш космический корабль хранится в файле assets/Models/SpaceShip.j3o.

Если у вашей модели есть материал, то вам понадобится свет, я взял направленный свет(directional light) здесь. И зарегистрируйте его в классе Main следующим образом.

    public Main() {
        super(new VisualAppState(),
                new EntityDataState(),);
    }

Вот и все, запустите свою игру нажав F6 и посмотрите. Я согласен, что здесь ES пока не так много, но мы теперь готовы сделать первую реальную систему сущностей, на самом деле нам нужно добавить немного плоти в VisualAppState и добавить что-то вроде GameAppState.


Переведено для jmonkeyengine.ru, оригинал
Автор перевода: Andry

Добро пожаловать в вики Zay-ES!

Чтобы впервые начать работу с Zay-ES, сначала посмотрите страницу С Чего Начать.

Для более глубокого понимания здесь есть раздел документации: Документация.

Или взгляните на некоторые примеры в разделе Примеры из практики Zay Es.

Для дальнейшего чтения на Entity-Component-Systems проверьте раздел Интересные ссылки.

оригинал


О Christian Liesch

Меня зовут Christian Liesch, и я далёк от обычных программистов игр и занимаюсь этим как хобби, в дождливую погоду и в бурные ночи.

Мне слишком часто приходилось перебарывал себя что бы завершить даже простые игры. Но потом я увидел эту систему сущностей где-то в вики jME3. Сначала я понятия не имел, как это должно быть встроено в игру, и как вы отображаете свои игровые элементы на экране. Поэтому я изучил asteroid panic от Paul Speed и начал понимать. Система сущностей вернула меня в прошло в день взлома ZX81 когда я был 13-летним мальчиком более 30 лет назад. Я нашел способ четко разделить логику, данные и представление. С помощью системы сущностей я смог написать простую игру менее чем за 24 часа, что потребовало бы от меня недели и месяцы классическим способом. Потому что система сущностей дала мне структуру, которую я так отчаянно искал. Простая архитектура, с «модулями», где все ясно чётко, и не надо задумываться о наследовании, а объединяют все это, только сущности, компоненты и системы.

Моё тематическое исследование — это моя первая игра на основе системы сущностей, которую я написал для вас. Я многому научился и хочу поделиться с вами своими выводами и извлеченными уроками. В игре Invaders(Захватчики). Тематическое исследование основанное на идее, что в итоге у вас будет простая запущенная игра, и даже больше, вы можете попробовать все шаги и получить немедленную обратную связь.

оригинал


Документация

Когда нибудь возможно здесь что то будет.

Эмпирические правила

Обдумывание концепции Сущность-Компонент-Система(Entity-Component-System) может быть затруднено. Многолетний опыт объектно-ориентированного программирования может затруднить мышление в новом режиме. Ниже приведены некоторые общие правила, которые следует учитывать при разработке собственного приложения ECS дизайна:

  • Компоненты только данные.
  • Две системы не должны производить один и тот же тип компонента для одних и тех же сущностей.
  • это еще не всё

API Javadoc

Всегда стоит ознакомиться с javadocs проекта и держать эти ссылки под рукой:

Документация по API генерируется как часть последнего выпуска Zay-ES и может содержать более актуальную и точную информацию, чем эти страницы документации.

оригинал


С Чего Начать


Получение библиотеки

Загрузите последнюю версию ядра (и/или сети) отсюда: Релизы

Или сконфигурируйте ваш инструмент сборки так, чтобы они автоматически вытягивались (gradle/maven/итп.). Jar-ы Zay-ES доступны в открытом репозитории jcenter.

Вот пример раздела из файла build.gradle, который включает в себя как ядро Zay-ES, так и сетевую библиотеку:

dependencies {
    compile "com.simsilica:zay-es:1.2.1"
    compile "com.simsilica:zay-es-net:1.2.1"
}

Требуемые зависимости

Если вы не используете современный инструмент для сборки, который автоматически разбирается с зависимостями, то тогда вам понадобится скачать несколько дополнительных jar-файлов:

  1. Guava версия 12 или новее
  2. slf4j версии 1.7.5 или новее, а также адаптер для предпочитаемой вами среды ведения журнала отладки. (Как минимум, вам понадобится API jar, что-то вроде: slf4j-api-1.7.5.jar)

Создайте главный объект EntityData

EntityData — это главный класс, который будет содержать все компоненты. Создать базовую версию «в памяти» так же просто, как:

    EntityData ed = new DefaultEntityData();

Создать Сущность

Создание сущности это просто генерация нового EntityId, к которому могут быть присоединены компоненты.

    EntityId myEntity = ed.createEntity();

Задание Компонент

Обычно приложение создает свои собственные классы реализации EntityComponent, но Zay-ES поставляется с парой стандартных. Следующий код иллюстрирует задание названия ранее созданному объекту.

    ed.setComponent(new Name("My Entity"));

Наблюдение за изменениями (игровой цикл)

Одно из основных отличий между Zay-ES и другими системами ECS заключается в том, что вас не принуждают к конкретной «системной» модели. То, как вы пишете свои системы и как вы используете сущности, более или менее зависит от вас. Таким образом, вместо подхода Инверсии управления, в котором вы реализуете какой-то системный класс и фреймворк вызывается вами, вы можете просто запрашивать нужные сущности, когда захотите. Zay-ES предоставляет внутренние механизмы, чтобы сделать это эффективным.

Основной цикл опроса:

    EntitySet namedEntities = ed.getEntities(Name.class);
    while( true ) {
        namedEntities.applyChanges();
        for( Entity e : namedEntities ) {
            // делать что то каждый кадр
        }
    }

Более эффективный цикл опроса, где код запускается только при изменении сущностей:

    EntitySet namedEntities = ed.getEntities(Name.class);
    while( true ) {
        if( namedEntities.applyChanges() ) {
            // сделать что-то с новыми соответствующими объектами
            addSomeStuff(namedEntities.getAddedEntities());

            // сделать что-то с сущностями, которые больше не соответствуют
            removeSomeStuff(namedEntities.getRemovedEntities());

            // сделать что-то с сущностями, которые просто изменились
            changeSomeStuff(namedEntities.getChangedEntities());
        }
    }

Для дальнейшего чтения смотрите:

оригинал


Zay-ES Case Studies

Сначала вы должны загрузить JME 3.1+ и заставить его работать, это довольно просто. Далее скачиваем также Zay ES и его зависимости guava и slf4j-api. После этого запустите новый игровой проект в JME, выполнив следующие действия: Создать проект ▸ JME3 ▸ BasicGame

Jar-ы могут быть добавлены с помощью: Щелкните [ПК мыши] ваш проект Свойства ▸ Библиотеки ▸ Добавить файл JAR/папку. Вы можете разместить их там, где захотите, лично у меня они находятся в папке lib в моем проекте JME3.

Автор этих тематических исследований Christian Liesch.

Invaders(Захватчики)

Простой игровой сервер

  • Мой первый игровой сервер
  • Мой первый игровой клиент

оригинал


Интересные ссылки

Этот раздел для интересных ссылок, связанных с Системами Сущностей(Entity Systems).

Мое первоначальное знакомство с ESes было в серии статей в блоге на сайте t-machines … сейчас они, по-видимому, уже не существуют, но некоторые основные оригинальные материалы можно найти в этих трех статьях:

оригинал


Переведено для jmonkeyengine.ru
Автор перевода: Andry

Библиотека основанного на физике, рендеринга неба.

cloudimg4

Файл пример

Выберите файл TestSky для начала.

Лицензия

Библиотека находится в папке src, под лицензией BSD 3-Clause.

Дополнительные библиотеки, которые могут также использоваться, расположены в:

  • src-hyg Каталог HYG Star (https://github.com/astronexus/HYG-Database)
  • src-nuta Функция Нутации (лицензия программного обеспечения SOFA), используйте если вас интересует точный расчет небесных тел относительно времени и местоположения наблюдателя. Возможность использования функции нутации.
    Space.NutationFunction = new NutationFunction();
  • src-vsop Местоположение Планет в Солнечной системе (GNU Affero General Public Лицензия, версия 3)
    Space.EarthPosition = new Earth();
    //например:
    Space.getEarthPos(jullianDate, observerLon, observerLan);
    //или
    Space.EarthPosition.getLon(jullianDate);

Поддержка анимации на основе динамической обработки CSG не является одной из целей моего дизайна. Я всегда ожидал серьезного падения производительности из-за сопоставления всех поверхностей в одну фигуру в сравнении со всеми поверхностями в некоторых других заданных фигурах. Я предполагал использование CSG во время операции загрузки, что бы потери производительности шли в рамках начальной настройки. Если бы анимация была желательной, я бы допускал, что некоторое N количество кадров должно будет построено-заранее.

Но по-прежнему забавно проверять, что CSG может делать динамически. Достаточно простой тестовый вариант состоит в том, чтобы иметь две фигуры с общим центром, вращающиеся в противоположных направлениях. Затем отобразите их пересечение. С помощью простых кубов (12 треугольников в кубе) вы получаете довольно хороший результат. Замените один из этих кубов на, до некоторой степени гладкий цилиндр, и частота кадров снижается. Вы можете сделать опрос смешанной фигуры после регенерации, чтобы получать количество наносекунд, необходимых для создания фигуры.

Настройка выполняется напрямую. Создайте две фигуры и соедините их с третьей. В цикле обновления примените вращение к двум примитивам, и затем регенерируйте смешанную фигуру. Код CSG перестроит смешение с нуля при каждом вызове регенерации.
скачать видео (которое идёт рывками из-за активной записи, но это происходит довольно плавно в ​​реальной жизни)

Прогрессивная Перестройка

Более практичная форма полуавтоматически применяет прогрессивные изменения к одной целевой фигуре. Эти изменения могут быть вызваны действием пользователя или могут быть непрерывными, двигаясь с любой скоростью, которую может обеспечить ваше железо. CSG структурирована так, чтобы вы могли легко выполнять прогрессивные изменения. Ключевым в этом является то, что возвратом вызова .regenerate() является CSGShape. Эта фигура является единственным результатом предыдущего смешивания и готова к использованию.

Типичным для структуры дизайна является сохранение результата первоначальной .regenerate() в переменной «priorResult». Действие обновления состоит в том, чтобы сначала удалить все фигуры из вашей целевой фигуры, смешать в priorResult, смешать любые новые изменения и затем сохранить результат этого нового .regenerate() в priorResult. Этот цикл обновления можно запустить повторно, включая любые новые изменения в каждом цикле.

Например, CSGTestM начинается с кубического блока «сыра». Цикл обновления — случайное перемещение сферическую «мыши» в пределах одного квадранта блока, а затем вычитает сферу из куба. Каждый результат становится следующей начальной точкой, так как цикл продолжается,и все больше и больше сыра грызёт мышь. Несмотря на то, что анимация не является достаточно гладкой, она по-прежнему является эффективной на экране дисплея.
скачать видео

Многопоточность

Для улучшения поддержки анимации и прогрессивной перестройки процесс сборки CSG был сделан thread aware. Регенерация была разбита на два отдельных шага. Первый включает в себя весё тяжелую работу анализа фигур смешивания, разрешения конфликтов и принятия решения о структуре конечного результата. Второй шаг — применить окончательную фигуру к активной визуальной сцене.

По умолчанию .regenerate() выполняет оба шага. Но вы можете использовать:

 myCSGSpatial.deferSceneChanges( true );

из-за которого регенерацию будет выполнять только первый шаг. Изменения проводятся внутри и в пределах spatial до явного применения:

myCSGSpatial.applySceneChanges();

который обновляет базовую сцену. .regenerate() и .applySceneChanges() являются thread-aware и должным образом синхронизируются. Это означает, что вы можете управлять регенерацией (шаг нагрузки на CPU) из фонового потока и вызывать .applySceneChanges() из стандартного потока jme3 .update(). .applySceneChanges() возвращает true, если работа была выполнена, false если обновления не были применены.

Как часть являющаяся thread-aware, главный внутренние циклы обработки монитор Thread.interrupted(). Если такое прерывание обнаружено, процесс регенерации прерывается и генерируется исключение CSGConstructionException.

Обратите внимание, что CSGConstructionException является RuntimeException, поэтому вызывающему не требуется явно писать код обработки исключений. java.lang.InterruptedException не используется в обработке CSG.

Переведено для jmonkeyengine.ru, оригинал
Автор перевода: Andry

Я начал трудится над CSG, чтобы удовлетворить свои потребность в программных инструментах построения, которые я могу использовать в других экспериментах. Для достижения этой цели пришлось поддержке CSG несколько выйти за минимальные рамки потребности в объединение/вычитании/пересечении фигур. Ниже перечислены только некоторые возможности, реализованные для удовлетворения моих других личных требований.

Освещение

Я не ожидал, что использование Освещения будет локально привязано к примитивным фигурам, которые смешиваются вместе. В исходном коде не было предусмотрено никаких положений для моего случая, и нужное мне освещение было блаженно проигнорировано. Но я начал работать больше с внутренними пространствами, которые были результатом вычитания меньшего объекта из середины более крупного. Я знаю, что глобально действующее освещение влияет на все поверхности одинаково, без особого знания о внутреннем пространстве. Чтобы получить правильное освещение закрытого/внутреннего пространства, необходимо включить обработку теней.

Ядро движка jMonkey поддерживает обработку теней через Рендеры/Фильтры, привязанные к определенным типам освещения. Но мои эксперименты дали не очень вдохновляющие результаты. Некоторые из эффектов были вполне реалистичными, но было и много примеров просто странных. И частота кадров резко упала. Я готов приписать много причин моему отсутствию понимания, но пока я не смогу получить доступ к более лучшим примерам, мне нужен другой подход.

Так как локальное освещение применяются только к элементам в пределах данного Spatial, я понял, что могу создать некоторый уровень освещения только для интерьера, если я смогу создать узел как часть процесса смешивания. Поддержка нескольких Материалов уже знает, как связать поверхность конечного продукта с исходной примитивной фигурой. Эта процедура была легко расширена, чтобы отслеживать локальное освещение, а также материалы. Конечный результат включает в себя Узел, который содержит локальное освещение, и все элементы поверхности Сетки(Mesh), которые освещаются таким освещением.

Например, если я вычитаю цилиндр из куба, я могу реализовать освещение на сам цилиндр. Это освещение распространяется только на конечные элементы внутренней поверхности, и более нигде. Хотя, конечно, это не комплексное решение того как правильно будут тени, это позволяет мне создавать некоторые хорошие эффекты. Это также хорошо работает с группами под-фигур. Чтобы создать коридор в форме арки, я группирую вместе половину-высоты Куба для дна и Цилиндр для верха. Затем я могу вычесть эту группу из внешней Куба. Добавление света в группу приводит к приятному внутреннему освещению. Добавление локального освещения к внешнему кубу означает, что я могу осветить его снаружи, без просачивания света внутрь.

CorridorLights

По умолчанию jme3-поведение состоит в том, что все освещения расположены в мировом пространстве, даже если они реализованы локально. Целью CSG является создание сложной фигуры из простых примитивов. Когда добавляются освещения, можно было бы ожидать, что преобразование в окончательную фигуру сохранит правильное освещение. Но если освещения расположены глобально, преобразование в окончательную фигуру означает, что свет больше не освещает там, где можно было бы ожидать. Jme3 решает эту проблему через LightControl, который может изменить положение Освещения, поскольку связанный с ним Spatial преобразуется. По умолчанию CSG создаст и назначит соответствующий Control всем локальным освещениям, встретившимся во время смешивания. Он использует настраиваемый CSGLightControl, который лучше поддерживает базовую структуру CSG. Но вы можете отключить эту функцию или предоставить любой другой Control для управления освещением.

Физика

Я не ожидал, что обработка Физики станет неотъемлемой частью CSG в стиле, очень похожем на Освещения. Но по мере того, как я проработал больше тестов, я сделал ставку на требование реализовывать Физику на уровне примитивов, а не пытаться добавить ее извне. В общем, гораздо проще добавить реализации Физики через XML на правильном уровне, а затем система позаботится обо всем этом для вас.

Для этого Физики добавляются так же, как освещения. Физики применяются только к сегменту конечной сетки, соответствующему смешанному примитиву. Поэтому, если вы создаете комнату, вычитая маленький куб из куба большего размера, и вам нужны упругие стены, но инертный(неактивный) пол, вы можете применять перед-гранью физики на разных поверхностях. Окончательная смесь держит все точно для вас.

2D-Поверхности

Основным направлением CSG является смешивание твердых тел. Но я столкнулся с ситуацией, когда захотел поместить твёрдое тело на своем 2D полу без смешивания пола. Разумеется, нет необходимости собирать пол как твердое тело, и я надеялся использовать jme3 Поверхность(Surface) и Ландшафт(Terrain). Обработчик IOB отлично справляется с обнаружением наложения и выбором правильных треугольников для окончательной смешанной фигуры. Вычитание твердого тела и пола дает мне хорошее отверстие, в которые должно пройти твердое тело. Пересечение твердого тела и пола показывает мне, где именно происходит наложение. И объединение дает мне броские фигуры, я не уверен, что это очень полезно (но выглядит корректно).

Я понятия не имею, будет ли обработчик BSP работать должным образом, и я не планирую экспериментировать или работать над ней.


Переведено для jmonkeyengine.ru, оригинал
Автор перевода: Andry

Общий

Нижеприведенные параметры обычно полезны, независимо от используемых привязок LWJGL.

Простые проверки

LWJGL выполняет несколько проверок во всех привязках и внутреннем коде. Эти проверки сосредоточены главным образом на обнаружении общих ошибок, которые могут привести к сбою процесса работы JVM. Примеры:

  • Если вызывается необязательная функция (например, функция расширения OpenGL), LWJGL проверяет и выдает исключение NullPointerException, если оно недоступно.
  • Если параметр указателя функции или член структуры никогда не должен быть равен NULL, LWJGL проверяет и выдает исключение NullPointerException, если оно есть.
  • Если параметр буфер функции или член структуры должен иметь определенный минимальный размер, LWJGL проверяет и выдает исключение IllegalArgumentException, если оно есть.

Эти проверки, как правило, оказывают низкую нагрузку на систему и потому не должны оказывать заметного влияния на производительность, поэтому они включены по умолчанию, и их отключение не рекомендуется, особенно во время разработки. Для обеспечения оптимальной производительности в конечных сборках их можно отключить с помощью -Dorg.lwjgl.util.NoChecks=true или Configuration.DISABLE_CHECKS.set(true).

Отключенные LWJGL проверки не оказывают нагрузку во время выполнения. Но, судя по Java утверждениям (assertion), они увеличивают размер байт-кода окружающих методов. В редких случаях это может повлиять на принятие решений JVM и в конечном итоге повлиять на производительность. Если такая проблема была обнаружена, то в этом случае LWJGL можно собрать с удалением всех проверок, задав binding.DISABLE_CHECKS в config/build-bindings.xml в true.

Режим отладки

LWJGL выполняет дополнительные, более дорогие проверки, когда режим отладки включен с -Dorg.lwjgl.util.Debug=true или Configuration.DEBUG.set(true). Это должен быть первый вариант, который можно использовать при столкновении с LWJGL. Режим отладки также создает дополнительный выход (по умолчанию stderr, может быть переопределен с Configuration.DEBUG_STREAM) и требуется некоторыми другими инструментами устранения неполадок.

Когда вы сообщаете о проблеме LWJGL, настоятельно рекомендуется включить режим отладки и включить её вывод с описанием проблемы.

Режим отладки загрузчика общей библиотеки

Одной из наиболее распространенных проблем, с которой сталкиваются новые пользователи LWJGL, является их проект, так что общие библиотеки загружаются правильно. Включение этого режима с помощью -Dorg.lwjgl.util.DebugLoader = true или Configuration.DEBUG_LOADER.set (true) создает дополнительный вывод в DEBUG_STREAM, что очень полезно при определении проблем с загрузкой библиотеки.

Также должен быть включен режим отладки.

Режим отладки распределителя памяти

Родные библиотеки означают неактивную память, что означает повышенный риск утечки памяти по сравнению со стандартным кодом Java. Включение этого режима с помощью -Dorg.lwjgl.util.DebugAllocator = true или Configuration.DEBUG_MEMORY_ALLOCATOR.set (true) делает LWJGL отслеживать все распределения вне кучи, выполняемые через org.lwjgl.system.MemoryUtil. Когда процесс JVM завершается, любые распределения, которые не были явно освобождены, будут сообщены в DEBUG_STREAM.

Этот режим может оказать очень негативное влияние на производительность.

Режим отладки в стеке памяти
Использование org.lwjgl.system.MemoryStack прост, но может быть источником сложной идентификации проблем. Включение этого режима с помощью -Dorg.lwjgl.util.DebugStack = true или Configuration.DEBUG_MEMORY_DEBUG_STACK.set (true) делает LWJGL сообщать о любом стеке pop без соответствующего нажатия в том же методе.

Этот режим может оказать очень негативное влияние на производительность.

Привязки

Некоторые привязки предлагают специальные инструменты, которые могут быть очень полезны для устранения неполадок.

EGL

EGL-реализации могут поддерживать расширение KHR_debug. См. OpenGL ниже для получения дополнительной информации.

GLFW

GLFW поддерживает регистрацию функции обратного вызова для уведомлений об ошибках, даже до инициализации самого GLFW, с помощью функции glfwSetErrorCallback. Пример кода:

// custom
glfwSetErrorCallback((error, description) -> {
	System.err.println("GLFW error [" + Integer.toHexString(error) + "]: " + GLFWErrorCallback.getDescription(description));
});
// or shortcut that prints to DEBUG_STREAM
GLFWErrorCallback.createPrint().set();
// or shortcut that throws an exception on error
GLFWErrorCallback.createThrow().set();

// easy clean-up
glfwSetErrorCallback(null).free();

OpenAL

OpenAL-Soft, реализация OpenAL в комплекте с LWJGL, может быть настроена с переменными окружения.

Переменная ALSOFT_LOGLEVEL определяет количество протоколирования. OpenAL Soft выписывает:

Эффективно отключает все протоколирование
Распечатывает только ошибки
Распечатывает предупреждения и ошибки
Распечатывает дополнительную информацию, а также предупреждения и ошибки
То же, что и 3, но и счетчик ссылок на устройство и контекст. Это позволит распечатать много информации и, как правило, не полезно, если вы не пытаетесь отслеживать утечку ссылок в библиотеке.

OpenCL

Когда создается контекст OpenCL, может быть зарегистрирована функция обратного вызова, которая будет использоваться реализацией OpenCL для отправки информации об ошибках при создании контекста, а также ошибок, возникающих во время выполнения в этом контексте. Пример кода:

CLContextCallback contextCB = CLContextCallback.create((errinfo, private_info, cb, user_data) -> {
    System.err.println("cl_context_callback info: " + memUTF8(errinfo));
});

long context = clCreateContext(ctxProps, device, contextCB), NULL, errcode_ret);

Функция обратного вызова может быть вызвана асинхронно с помощью реализации OpenCL. Обязанностью приложения является обеспечение того, чтобы функция обратного вызова была потокобезопасной.

OpenGL

Современные версии OpenGL поддерживают очень мощный режим отладки для сообщения об ошибках и предупреждениях. Этот режим стал стандартным в OpenGL 4.3, но есть расширения, которые обеспечивают такую же функциональность в более ранних версиях (KHR_debug, ARB_debug_output, AMD_debug_output). Пример кода:

// before context creation
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);

// after context creation
glfwMakeContextCurrent(window);
GL.createCapabilities();
Callback debugProc = GLUtil.setupDebugMessageCallback(); // may return null if the debug mode is not available

// cleanup
if ( debugProc != null )
    debugProc.free();

Некоторые драйверы OpenGL предоставляют подробные информационные сообщения и предупреждения по умолчанию. Количество и тип выпускаемой продукции можно контролировать с помощью таких функций, как glDebugMessageControl.

OpenGL ES

Подобно OpenGL, OpenGL ES поддерживает режим отладки, начиная с OpenGL ES 3.2. Расширение KHR_debug также может быть доступно в более ранних версиях.

Vulkan

Несмотря на то, что реализации Vulkan оптимизированы для максимальной производительности и по умолчанию выполняются очень мало проверок, система расширения и уровня может использоваться для обеспечения большого количества проверок времени выполнения, от базовых проверок до комплексного отслеживания использования вызовов функций.

Как и в случае с Vulkan, код для такой проверки не является тривиальным. Не забудьте проверить LunarG Vulkan SDK и образец HelloVulkan LWJGL для примера.


Переведено для jmonkeyengine.ru, оригинал
Автор перевода: Andry

Каковы различные компоненты нативных привязок?

Чтобы вызвать нативную функцию, LWJGL требует как Java, так и нативный код. Давайте используем функцию glShaderSource, которая была введена в версии 2.0 спецификации OpenGL:

void glShaderSource(GLuint shader, GLsizei count, const GLchar **strings, const GLint *length)

Это void функция, которая принимает целое число без знака, целое число со знаком, указатель на массив строк и указатель на массив целых чисел со знаком. В LWJGL это преобразование происходит следующим образом:

public static native void nglShaderSource(int shader, int count, long strings, long length)

Обратите внимание на префикс ‘n’ и сопоставления типов: Как со знаком, так и беззнаковые целочисленные параметры были сопоставлены с Java int и оба указателя, сопоставлены с Java long. Мы обсудим эти сопоставления позже.

Префикс ‘n’ не является абсолютно необходимым, но добавляется для того что бы автозавершение кода в среде IDE позволяло разделить нативные и не нативные методы.

В дополнение к нативному методу, так же включены несколько обычных Java-методов:

public static void glShaderSource(int shader, PointerBuffer strings, IntBuffer length)
public static void glShaderSource(int shader, CharSequence... strings)
public static void glShaderSource(int shader, CharSequence string)

Это дружественные Java методы, которые разработчики обычно используют. Первый специализирует типы указателей и сбрасывает аргумент count, второй заменяет все с помощью параметра CharSequence vararg, а третий с одним CharSequence.

С нативной стороны все очень просто. Это реализация C указателя функции:

typedef void (APIENTRY *glShaderSourcePROC) (jint, jint, const intptr_t, const intptr_t);

и реализация функции JNI:

JNIEXPORT void JNICALL Java_org_lwjgl_opengl_GL20_nglShaderSource(
    JNIEnv *__env, jclass clazz, // unsed
    jint shader, jint count, jlong stringsAddress, jlong lengthAddress
) {
	glShaderSourcePROC glShaderSource = /* получить указатель функции */;
	const intptr_t strings = (const intptr_t)stringsAddress;
	const intptr_t length = (const intptr_t)lengthAddress;
	UNUSED_PARAM(clazz)
	glShaderSource(shader, count, strings, length);
}
Используемые C типы не соответствуют исходной функции, но они бинарно совместимы.

LWJGL осуществляет здесь очень важный выбор о реализации; нативный код ничего не делает, кроме параметров заливки для соответствующих типов и вызывает нативную функцию. Все сложности и сопоставление с удобными для пользователя параметрами для корректных нативных данных, обрабатываются в Java-коде. Все указатели передаются как примитивные long-и, примитивные значения передаются без изменений и это все. Параметры JNIEnv и jclass никогда не затрагиваются, и никакого взаимодействия с JVM нет; никакие jobject параметры не передаются, и нет вызовов C-к-Java. Нативные методы также public, что позволяет пользователям создавать свои удобные для них методы поверх исходных, и они могут быть спокойны, что никаких проблем не будет происходит при вызове функции JNI.

Двигаясь дальше, нынешний дизайн будет делать принятие Project Panama легче для LWJGL, который будет доступен на Java 10.

Вышеупомянутое относится ко всем привязкам LWJGL. Большинство пользователей никогда не будут иметь дело с нативными методами напрямую, и они не должны, если нет веской причины; их использование по сути является небезопасным. Более опытные разработчики могут делать интересные вещи с помощью исходных указателей арифметики и утилиты org.lwjgl.system.MemoryUtil.

Что такое параметр __functionAddress, который требуется многим нативным методам?

Существует две проблемы с динамически подключаемыми библиотеками: a) указатели на функции должны быть динамически обнаружены во время выполнения и b) некоторые библиотеки могут использовать разные указатели на функции в разных контекстах. С другой стороны, код JNI является статическим, что означает, что единственный способ вызвать такие функции, это передать адреса функций до нативного кода.

Префикс ‘__’ позволяет сделать более очевидным, что это синтетический параметр.

Если нативные методы не используются напрямую, пользователям редко приходится явно передавать адрес функции. LWJGL использует специальные механизмы что бы делать это автоматически. Например, методы OpenGL используют локальную память потока для текущего контекста, а методы Vulkan используют параметр dispatchable handle для получения корректного указателя функции.

Как сопоставляются примитивные значения?

Сопоставления обычно является простым, за исключением без знаковых типов и непрозрачных указателей:

  • Без знаковые целые типы сопоставляются в Java соответствующему типу со знаком. Может показаться что это звучит как с ошибкой, но в большинстве случаев это не так. Они могут быть легко преобразованы в, и из целых чисел с высокой точностью, используя при этом простые бинарные операции. Основные арифметические операции, кроме деления, работают одинаково как для целых чисел со знаком, так и без знака. Java 8 также предоставляет полезные утилиты для их обработки.
  • Непрозрачные указатели сопоставляются с Java через long.
Непрозрачные указатели являются указателями на данные, которые имеют неопределенную структуру и обычно соответствуют внутреннему состоянию, управляемому сторонним API. Значения таких указателей просто передаются в, и выходят из API и никогда не используются для прямого доступа к памяти.

Давайте посмотрим на некоторые примеры. Следующие объявления функций:

void glDepthMask(GLboolean mask)
void glAlphaFunc(GLenum func, GLfloat ref)
void glClear(GLbitfield mask)
void glCopyPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum type)
void glVertexAttrib4s(GLuint index, GLshort v0, GLshort v1, GLshort v2, GLshort v3)
void glVertexAttrib4Nub(GLuint dinex, GLubyte x, GLubyte y, GLubyte z, GLubyte w)
void glfwSwapBuffers(GLFWwindow* window)

сопоставляется со следующими Java-методами:

public static void glDepthMask(boolean flag)
public static void glAlphaFunc(int func, float ref)
public static void glClear(int mask)
public static void glCopyPixels(int x, int y, int width, int height, int type)
public static void glVertexAttrib4s(int index, short v0, short v1, short v2, short v3)
public static void glVertexAttrib4Nub(int index, byte x, byte y, byte z, byte w)
public static void glfwSwapBuffers(long window)

Как сопоставляются указатели на примитивные значения?

Данная нативная функции объявляется так:

void glUniform4fv(GLint location, GLsizei count, const GLfloat *value);

мы имеем следующие методы Java:

public static native void nglUniform4fv(int location, int count, long value) // A
public static void glUniform4fv(int location, FloatBuffer value) // B

В этом случае мы имеем float указатель и явно заданный параметр count, который определяет, сколько векторов vec4 следует читать из этого указателя.

В GLSL vec4 представляет собой вектор из 4 float значений. Поэтому указатель должен указывать на массив count * 4 * 4 байт.

В методе A, который является исходным методом JNI, параметр count и адрес указателя являются явными.

В методе B произошли две трансформации; a) параметр value теперь является FloatBuffer, соответствующим типу нативного указателя, и b) параметр count теперь неявный, замененный value.remaining() / 4. На 4 потому, что каждый count представляет собой вектор из 4-х значений.

Текущий Buffer.position() влияет на адрес указателя, который будет передан в нативную функцию. Если FloatBuffer обертывает указатель с адресом x, а текущая позиция равна 2, будет использоваться адрес x + 8.
Текущий Buffer.limit() определяет, сколько значений будет считано или записано в буфер. В сочетании с примечанием о Buffer.position() выше это соответствует тому, как буферы java.nio обычно используются в JDK. Единственное различие заключается в том, что LWJGL никогда не изменяет текущую позицию или ограничение(limit) буфера. Это уменьшает необходимость flip() или rewind() буферов.

Как обрабатываются параметры вывода(output)?

Параметры вывода являются стандартными параметрами указателя данных. Память, на которую они указывают, будет использоваться функцией для возврата данных.

Классическим примером является glGetIntegerv, очень полезная функция OpenGL:

void glGetIntegerv(GLenum pname, GLint *data)

LWJGL включает следующие методы для этой функции:

public static native void nglGetIntegerv(int pname, long params) // A
public static void glGetIntegerv(int pname, IntBuffer params) // B
public static int glGetInteger(int pname) // C

Методы A и B аналогичны тем что были описаны выше, за исключением того, что нет явного параметра count. Если бы был параметр count, то такое же преобразование было бы применено в случае B (params.remaining() будет использоваться неявно). Для этой функции пользователь должен убедиться, что для возвращаемых данных запрошенного параметра, достаточно места.

Это, очевидно плохой дизайн, но эта функция очень старая. Современные функции OpenGL делают работу с проверкой намного лучше, и большинство из них требуют точный размеров.

Метод C является интересным. Его можно использовать, когда запрос возвращает одно значение. Без него пользователь должен будет выделить одно значение буферу, вызвать метод, а затем прочитать значение из буфера, сложная процедура. Вместо этого LWJGL сбрасывает параметр params и изменяет тип возвращаемого метода из void в int. Это делает его более естественным и удобным в использовании.

LWJGL использует org.lwjgl.system.MemoryStack для реализации таких методов, как C. Такие методы можно безопасно использовать из нескольких контекстов одновременно.

Это круто. Можно ли это сделать для единичных-значений параметров ввода(input)?

Конечно, если это полезно для какой то конкретной функции. Например:

void glDeleteTextures(GLsizei n, const GLuint *textures)

может использоваться с любым из них:

public static native void nglDeleteTextures(int n, long textures)
public static void glDeleteTextures(IntBuffer textures)
public static void glDeleteTextures(int texture) // одно значение!

Есть ли удобный способ использования String или CharSequence?

Да, LWJGL упрощает обработку текстовых данных. Соглашением для текста является то, что параметры ввода сопоставляются с CharSequence, а вывода (возвращенные) значения сопоставляются с String. LWJGL корректно обрабатывает кодировки символов, поддерживает кодирование/декодирование ASCII, UTF-8 и UTF-16 и будет использовать правильный вариант в зависимости от используемой функции. Наконец, он может так же легко обрабатывать нуль-терминированные строки и строки с точной длиной. Некоторые примеры:

GLint glGetAttribLocation(GLuint program, const GLchar *name) // A) нуль-терминированный ввод
GLubyte *glGetString(GLenum name) // B) нуль-терминированный вывод
void glGetProgramInfoLog(GLuint program, GLsizei bufSize, GLsizei *length, GLchar *infoLog) // C) вывод w/ точной длинны

и Java методы:

// A)
public static native int nglGetAttribLocation(int program, long name)
public static int glGetAttribLocation(int program, ByteBuffer name)
public static int glGetAttribLocation(int program, CharSequence name)
// B)
public static native long nglGetString(int name)
public static String glGetString(int name)
// C)
public static native void nglGetProgramInfoLog(int program, int maxLength, long length, long infoLog)
public static void glGetProgramInfoLog(int program, IntBuffer length, ByteBuffer infoLog)
public static String glGetProgramInfoLog(int program, int maxLength)
public static String glGetProgramInfoLog(int program)

Бонусная функция: последний glGetProgramInfoLog будет использовать glGetProgrami(program, GL_INFO_LOG_LENGTH) внутри для автоматического выделения достаточного количества память для возврата лога текста!

Как можно настроить обратные вызовы из нативного кода?

Каждый тип нативного обратного вызова сопоставляется с парой interface/abstract класса. Подобно CharSequence и String, когда тип обратного вызова является параметром ввода, он сопоставляется с interface и когда вывода (возвращаемое значение) относится к классу abstract. Это важно по двум причинам:

  • Лямбда SAM преобразования применимы только к интерфейсам. Все интерфейсы обратного вызова являются функциональными интерфейсами по определению.
  • Нативные функции обратного вызова генерируются во время выполнения. Это означает, что есть нативные ресурсы, которые должны быть сохранены как состояние и должны быть явно освобождены, если их больше не используют. Только абстрактный класс может иметь состояние и метод free().

Примером функции обратного вызова, которая может быть зарегистрирована в контексте OpenGL, является:

// Сигнатура функции:
typedef void (APIENTRY *GLDEBUGPROC)(GLenum source, GLenum type, GLuint id,
 GLenum severity, GLsizei length, const GLchar* message, GLvoid* userParam)
// Регистрация обратного вызова:
void glDebugMessageCallback(GLDEBUGPROC callback, const void *userParam)

Они сопоставляются с:

public static native void nglDebugMessageCallback(long callback, long userParam)
public static void glDebugMessageCallback(GLDebugMessageCallbackI callback, long userParam)

и интерфейс org.lwjgl.opengl.GLDebugMessageCallbackI, который реализует один метод:

void invoke(int source, int type, int id, int severity, int length, long message, long userParam);

Обратите внимание, что glDebugMessageCallback принимает GLDebugMessageCallbackI. Нативная функция обратного вызова генерируется при вызове glDebugMessageCallback и указатель на эту функцию передается в LWJGL. Это означает, что простое перенос лямбды в glDebugMessageCallback вызовет утечку памяти, потому что ничего не возвращается, что разработчик может использовать для освобождения генерируемой нативной функции обратного вызова. Существует два подхода к решению этой проблемы:

// Способ 1. Сохраните эту ссылку в переменной Java.
GLDebugMessageCallback cb = GLDebugMessageCallback.create((source, type, id, severity, length, message, userParam) -> {
    // печатать сообщение
});
glDebugMessageCallback(cb, NULL);
// потом...
cb.free();

// Способ 2: используйте glGetPointer для получения обратного вызова
glDebugMessageCallback((source, type, id, severity, length, message, userParam) -> {
    // печатать сообщение
}, NULL);
// потом...
GLDebugMessageCallback.create(glGetPointer(GL_DEBUG_CALLBACK_FUNCTION)).free();

Второй способ более удобен, но не все API-интерфейсы предоставляют способ получения указателя функции обратного вызова, который был ранее задан. В таких случаях пользователь LWJGL ответственен за сохранение ссылки обратного вызова в Java-коде.

Существует ли специальное сопоставление для нативных структур?

Да, каждый тип структуры сопоставляется с классом. Класс содержит методы создания/распределения и геттеры/сеттеры для элементов структуры. Компоновка структуры (смещения поля, sizeof, alignof) доступеа как static final int поле.

LWJGL поддерживает структуры, объединение и любую комбинацию вложений друг в друга. Смещение элементов и характеристики выравнивания/размера вычисляются во время выполнения, для обработки разности указателя размера и динамического заполнения.

Каждый класс структуры также имеет внутренний класс Buffer, который представляет массив этих структур. Класс Buffer имеет API, который совместим с буферами java.nio, а также геттеры/сеттеры, которые могут использоваться для доступа к массиву структур с приспособленец (flyweight pattern).

Пример:

typedef struct GLFWimage
{
    int width;
    int height;
    unsigned char* pixels;
} GLFWimage;
GLFWcursor* glfwCreateCursor(const GLFWimage* image, int xhot, int yhot);
GLFWAPI void glfwSetWindowIcon(GLFWwindow* window, int count, const GLFWimage* images);

Вышеуказанные значения сопоставляются с:

public class GLFWImage extends Struct implements NativeResource

public static long nglfwCreateCursor(long image, int xhot, int yhot)
public static long glfwCreateCursor(GLFWImage image, int xhot, int yhot) // A

public static void nglfwSetWindowIcon(long window, int count, long images)
public static void glfwSetWindowIcon(long window, GLFWImage.Buffer images) // B

GLFWImage имеет следующие статические поля:

GLFWImage.SIZEOF // sizeof(GLFWimage)
GLFWImage.ALIGNOF // alignof(GLFWimage)
GLFWImage.WIDTH // offsetof(GLFWimage, width)
GLFWImage.HEIGHT // offsetof(GLFWimage, height)
GLFWImage.PIXELS // offsetof(GLFWimage, pixels)

и следующие геттеры и сеттеры:

img.width()
img.height()
img.pixels()
Существуют также «небезопасные» статические геттеры и сеттеры, которые могут использоваться для доступа к ячейкам памяти, которые не обёрнуты в класс структуры или структурный буфер.

В приведенных выше методах A просто. Результат image.address() передается в нативный код. В B images представляет собой массив структуры GLFWimage. Обратите внимание, что он был сопоставлен с GLFWImage.Buffer, и параметр count был удален, как и в других методах, которые принимают автоматический размер java.nio буферов.

Поддержка памяти структур и структурные буферы могут быть любой памятью без кучи. MemoryStack, MemoryUtil и BufferUtil могут использоваться для распределения экземпляров объектов структуры, отвечающие характеристикам удобства использования/производительности.

Переведено для jmonkeyengine.ru, оригинал
Автор перевода: Andry

Эта страница обязательна для всех пользователей LWJGL.

Почему LWJGL использует буферы так много?

LWJGL требует использования памяти вне-кучи при передаче данных в нативные библиотеки. Аналогично, любые буферы, возвращаемые из нативных библиотек, всегда возвращаются в память вне-кучи. Это не ограничение LWJGL. Есть две проблемы с Java объектами и массивами, которые живут в куче JVM:

  • Невозможно управлять компоновкой Java объектов. Различные JVM и различные настройки JVM создают очень разные компоновки полей. С другой стороны, нативные библиотеки ожидают данные с очень точно определенными компоновками.
  • Любой Java объект или массив может быть перемещен GC в любое время, одновременно с выполнением вызова нативного метода. Все методы JNI выполняются из безопасной точки(safepoint), поэтому по определению не должны обращаться к данным кучи.

Стандартный подход:

  1. Использование JNI функций для доступа к Java объектам, что очень медленно.
  2. Использование JNI функций для «привязки» Java массивов (Get/ReleasePrimitiveArrayCritical или Hotspot Critical Natives), что также неэффективен по нескольким причинам.

С другой стороны, LWJGL предназначен для использования на прямую (вне-кучи) с java.nio буфер классами для передачи данных в и из нативного кода. ByteBuffer и другие классы не являются наилучшей из возможных абстракцией для данных вне-кучи, и их API не идеален, но это единственный официально поддерживаемый способ доступа к данным вне-кучи на Java.

Самый простой способ того как стоит рассматривать ByteBuffer, это рассматривать его как обертку поверх нативного C указателя, плюс длина массива (buffer.capacity()). LWJGL сопоставляет примитивные типы C с соответствующим классом в java.nio. Массивы указателей сопоставляются в классе org.lwjgl.PointerBuffer. Указатели на структуры сопоставляются с соответствующим структуре классом. Указатели на структуры массивов сопоставляются с соответствующим <StructClass>.Buffer классом. PointerBuffer и структура Buffer классов имеют API, очень похожий на java.nio буферы.

Какой java.nio.ByteOrder следует использовать?

Порядок байтов буфера должен быть задан в ByteOrder.nativeOrder(). Это в основном требуется для правильного кросс-платформенного поведения. Это также приводит к лучшей производительности.

Все экземпляры буфера, созданные LWJGL, всегда задаются в нативном порядке байтов.

Как распределить и освободить буферы?

После ознакомления с приведенными выше сопоставлениями, следующим шагом будет научиться обрабатывать распределение таких буферов. Это критическая проблема, и LWJGL предлагает несколько вариантов. Варианты перечисленные ниже, из более-менее эффективных. Каждый раз, когда вы принимаете решение о том, как обрабатывать распределение, вы должны рассмотреть первый вариант. Если это неприменимо, тогда рассмотрите второй вариант и так далее.

1. Распределение стека

Java не поддерживает явное распределение стека из Java объектов, и очевидно не поддерживает распределение стека вне-кучи. В C это очень просто: вы объявляете переменную внутри функции и распределяете это стек. Когда функция возвращает, память переменной автоматически утилизируется (и без накладных расходов). На Java этому нет эквивалента.

Точно так же в Java нельзя использовать нативные функции, которые ожидают или возвращает структуру по значению. Такие функции в привязках LWJGL обернуты и отображаются с помощью параметра указатель-к-структуре или возвращаемого значения.

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

GLuint vbo;
glGenBuffers(1, &vbo); // очень просто

и с LWJGL:

IntBuffer ip = ...; // здесь нужно 4-byte буфера
glGenBuffers(ip);
int vbo = ip.get(0);

Реальное распределение IntBuffer в приведенном выше примере, независимо от реализации, будет намного более неэффективным, чем указатель стека в эквивалентном C коде.

Обычный ответ на эту проблему в LWJGL 2 и других Java-библиотеках заключается в том, чтобы один раз распределить буфер, кэшировать его и повторно использовать во многих вызовах методов. Это невероятно неудовлетворительное решение:

  • Это приводит к уродливому коду и пустой трате памяти.
  • Чтобы избежать пустой траты памяти, обычно используются статические буферы.
  • Использование статических буферов приводит либо к ошибкам параллелизма, либо к снижению производительности (из-за синхронизации).

Ответ LWJGL 3 это API org.lwjgl.system.MemoryStack. Он был разработан для использования со статическим импортом и блоками try-with-resources. Вышеприведенный пример:

int vbo;
try (MemoryStack stack = stackPush()) {
    IntBuffer ip = stack.callocInt(1);
    glGenBuffers(ip);
    vbo = ip.get(0);
} // стек автоматически выталкивается, ip-память автоматически утилизируется

Это, очевидно, более многословное, но имеет следующие преимущества:

  • Обычно требуется более одного распределения, но шаблон try-with-resources остается неизменным.
  • Семантика вышеуказанного кода полностью соответствует требованиям. Память стека является поточно-локальным, подобно реальному потоку потока С.
  • Производительность идеальна. Push и pop стека это простые bumps указателя, а распределение экземпляра IntBuffer либо устраняется с помощью анализом выхода кода, либо обрабатывается следующим minor/eden GC циклом (супер эффективно).
Примечание 1: Размер стека по умолчанию — 64kb. Его можно изменить с помощью -Dorg.lwjgl.system.stackSize или Configuration.STACK_SIZE.
Примечание 2: Структуры и структура буферов также могут быть распределены на MemoryStack.
Примечание 3. Статический, поточно-локальный API MemoryStack, это просто удобно. Существует дополнительный API, который позволяет создавать и/или использовать экземпляры MemoryStack, как вы сочтете нужным.

2. MemoryUtil (malloc/free)

Иногда распределение стека не может быть использовано. Память, которая должна быть распределена, слишком велика или распределяется долгое время. В таких случаях следующим наилучшим вариантом является явно заданное управление памятью. Либо через API org.lwjgl.system.MemoryUtil, либо конкретным распределителем памяти (в настоящее время в LWJGL доступны: stdlib, jemalloc). Пример:

ByteBuffer buffer = memAlloc(2 * 1024 * 1024); // 2MB
// использовать буфер...
memFree(buffer); // освободить, когда больше не требуется
Примечание 1: Как и в C, пользователь отвечает за освобождение памяти, распределённой с помощью malloc, используя free.
Примечание 2: API для стандартных функций calloc, realloc и aligned_alloc также доступен.
Примечание 3: Объекты Java, распределённые с явно заданными функциями управления памятью, также подлежат избеганию анализу.

3. BufferUtils (ByteBuffer.allocateDirect)

Иногда API явно заданного управления памятью также не может быть использован. Возможно, данное конкретное распределение трудно отслеживать не усложняя код, или возможно не удастся точно узнать, когда оно больше не требуется. Такие случаи являются законными кандидатами на использование org.lwjgl.BufferUtils. Этот класс существовал в более старых версиях LWJGL с тем же API. Он использует ByteBuffer.allocateDirect что бы делать распределения, которые имеют одно важное преимущество: пользователю не нужно явно освобождать память кучи, это делает GC автоматически.

С другой стороны, он имеет следующие недостатки:

  • Он медленный, намного медленнее, чем вызов raw malloc. Много накладных расходов над функцией, которая уже и без того медленная.
  • Он плохо масштабируется при одновременном распределении.
  • Он произвольно ограничивает объем распределямой памяти (-XX:MaxDirectMemorySize).
  • Подобно массивам Java, выделенная память всегда обнуляется. Это не обязательно плохо, но возможность выбора будет лучше.
  • Невозможно освободить выделенную память по требованию (без JQK-специальных рефлексивных хаков). Вместо этого используется reference queue, которая обычно требует двух циклов GC для освобождения нативной памяти. Это может привести к ошибкам OOM при нагрузках.
Примером LWJGL, в котором используется BufferUtils, является распределение памяти, которое поддерживает поточно-локальные экземпляры MemoryStack. Это долгое живущее распределение, которое должно быть освобождено, когда поток умирает, поэтому мы позволяем GC позаботиться о нём.
  1. Используйте org.lwjgl.system.MemoryStack, и если это невозможно…
  2. Используйте org.lwjgl.system.MemoryUtil, и если это невозможно…
  3. Использовать org.lwjgl.BufferUtil

Я хотел бы узнать больше, есть ещё что-то для меня?

Да, прочитайте Управление Памятью в блоге LWJGL 3.


Переведено для jmonkeyengine.ru, оригинал
Автор перевода: Andry

LWJGL организован в виде модулей. Каждый модуль упакован как набор артефактов, где каждый артефакт имеет одно и то же название, но другой классификатор.

  • Название артефакта для модуля ядра LWJGL, это только lwjgl.
  • Название артефакта для модуля привязки lwjgl-<привязка>, например для модуля OpenGL будет lwjgl-opengl.

Артефакты, основанные на классификаторе:

  • Нет классификатора: скомпилированные классы Java
  • Исходники: Java исходники
  • javadoc: документация по API
  • natives-<платформа>: нативные общедоступные библиотеки для соответствующей платформы
Не все артефакты имеют нативные, и не все артефакты с нативным доступны на всех платформах.

Этот дизайн означает, что вам нужно скачать только те модули, которые необходимы для вашего проекта.

Дополнительные модули могут быть скачаны и добавлены позже, если ваши потребности изменятся.

Загрузка LWJGL — Использование конфигуратора сборки

Веб-сайт LWJGL предоставляет удобный конфигуратор для выбора тех частей библиотеки, которые вам нужно скачать. В этом разделе будет приведено краткое описание того, что делают отдельные части этого конфигуратора.

Существует три отдельных варианта выпуска: Release(Релиз), Stable(Стабильный) и Nightly(Ночной). Если вы не знаете, что вам с этим делать, то тогда вы должны использовать Release, так как он будет более стабильным.

Селектор Mode позволяет выбрать, нужный вам вариант скачивания библиотеки. Если вы планируете использовать IDE или вам нужны сами .jar файлы, выберите ZIP Bundle. Если вы собираетесь использовать maven или gradle, выберите соответствующий параметр для создания сценария сборки.

Во время разработки рекомендуется использовать Maven/Gradle. ZIP Bundle рекомендуется для вашего приложения при создании сборки или установке.

Нативные(Natives) не нужны для компиляции вашего проекта, но необходимы для его запуска. Таким образом, вам нужно получить нативное для всех платформ, на которых вы хотите запустить ваше приложение. Помните, что вам не нужно загружать нативное для всех платформ сразу. Вы можете скачать дополнительных нативные, когда будете готовы распространять(продавать) свой проект.

Если вы впервые используете LWJGL, предварительные настройки(presets) Getting Started или Minimal OpenGL должны быть достаточными для начала работы с большинством обучающих программ. Кроме того, вы можете создать настраиваемую конфигурацию и выбрать именно те модули, которые вам нужны. Имейте в виду, что вы можете просмотреть описание различных модулей, отметив Show descriptions(Показывать описания) в разделе Options.

Извлечение нативных (необязательно)

Общедоступные нативные библиотеки LWJGL НЕ нужно извлекать из их .jar файлов. SharedLibraryLoader, который включен в LWJGL, делает это автоматически при выполнении.

SharedLibraryLoader извлекает нативные один раз и повторно использует их от запуска к запуску. Нативное замещается только при обнаружении другой версии LWJGL.
Папку для извлечения можно настроить с помощью Configuration.SHARED_LIBRARY_EXTRACT_DIRECTORY и Configuration.SHARED_LIBRARY_EXTRACT_PATH.

Использование SharedLibraryLoader является необязательным. Например, пользовательский установщик для приложения может извлекать туземцев в определенное место. В таких случаях привычные -Djava.library.path и -Dorg.lwjgl.librarypath (или Configuration.LIBRARY_PATH) по-прежнему полностью поддерживаются.

Настройка инструмента сборки или IDE

Gradle или maven

При использовании gradle или maven вы можете просто скопировать скрипт сборки, сгенерированный генератором, и использовать его для сборки вашего проекта. Обратите внимание, что при использовании gradle вы, вероятно, захотите добавить apply plugin: ‘java’ и apply plugin: ‘application’ в build.gradle.

IntelliJ IDEA

IDEA поддерживает Gradle/Maven проекты и делает их использование тривиальным. Для пользовательской конфигурации выполните следующие действия:

  1. Перейдите на страницу загрузки, выберите канал, выберите почтовый пакет, просмотрите параметры и выберите нужные вам привязки, затем нажмите «Создать пакет».
  2. Когда загрузка будет завершена, извлеките ее содержимое в свой проект.
  3. В IntelliJ перейдите в раздел Структура проекта> Библиотеки> Новая библиотека проектов. Добавьте все библиотеки LWJGL .jar (классы + туземцы) в библиотеку и добавьте библиотеку в качестве зависимости от вашего модуля (ов) проекта. Вам не нужно извлекать собственные библиотеки.
  4. Если вы также загрузили источники, добавьте соответствующие файлы .jar в качестве источников в библиотеку LWJGL. Вам не нужно загружать и прикреплять файлы javadoc .jar, IntelliJ генерирует javadoc непосредственно из источников.

Если вы хотите точное соответствие поведения Maven/Gradle, вы можете создать две отдельные библиотеки:

  1. Один с .jar файлами классами + исходники, сделать его компиляцией зависимостей вашего модуля(ей).
  2. Один с нативными .jar файлами, сделать его с зависимостями времени выполнения вашего модуля(ей).

Вы можете сделать это в Project Structure ▸ Modules ▸ выберите модуль вкладка Dependencies , изменив Scope каждой зависимости.

NetBeans

  1. Загрузите LWJGL в качестве ZIP-пакета с помощью конфигуратора сборки и извлеките архив туда, где вы сможете его найти позже.
  2. Внутри Netbeans в разделе «Инструменты»> «Библиотеки» нажмите «Новая библиотека» … в нижнем левом углу.
  3. Назовите библиотеку что-то разумное, например «LWJGL 3.1.0», и нажмите ОК.
  4. В центре окна библиотеки теперь должны быть параметры для настройки библиотеки.
  5. На вкладке Classpath добавьте все файлы .jar из ранее загруженного ZIP-файла, за исключением тех, которые содержат источники или javadoc в их имени.
  6. (Необязательно) На вкладке «Источники» добавьте все файлы .jar с источниками в их имени. Это даст вам доступ к документации и исходному коду непосредственно из NetBeans. Источник можно просмотреть в редакторе нажатием Ctrl+Left при наведении курсора на функцию или класс, принадлежащий LWJGL.
  7. Создайте новый проект, выбрав Файл> Новый проект …. Выберите Java | Приложение Java в качестве типа проекта.
  8. Найдите проект в окне проекта (можно открыть с помощью Ctrl-1). Щелкните правой кнопкой мыши на «Библиотеки», выберите «Импорт», найдите созданную ранее библиотеку и выберите «Импортировать библиотеку», а затем «Добавить библиотеку».
  9. Убедитесь, что все работает по назначению, добавив следующую строку в ваш основной класс и запустив проект: System.out.println (org.lwjgl.Version.getVersion ()) ;. Это должно напечатать версию LWJGL, которую вы используете.

Eclipse

(вам понадобится хотя бы Eclipse Luna(4.4) или Eclipse Kepler(4.3) с патчем «Eclipse Java 8 Support (для Kepler SR2)», установленным для работы с LWJGL 3 в Eclipse)

Eclipse поддерживает проекты Gradle/Maven, и настоятельно рекомендуется использовать их вместо ручной настройки проекта Eclipse. Однако, если вы предпочитаете настраивать собственный проект Eclipse, следуйте следующим инструкциям (работает с Eclipse Neon):

  1. Загрузите пакет ZIP с https://www.lwjgl.org/customize
  2. Когда загрузка будет завершена, извлеките ее содержимое в какой-либо каталог файловой системы, отныне называемый .
  3. В Eclipse перейдите в меню «Окно»> «Настройки» и в древовидном представлении слева найдите «Java»> «Путь сборки»> «Библиотеки пользователей»,
  4. Нажмите «Создать …» в диалоговом окне «Библиотеки пользователей». В открывшемся модальном диалоговом окне «Новая пользовательская библиотека» напишите «LWJGL3» в текстовом поле «Имя пользователя:» и нажмите «ОК». Недавно созданная библиотека «LWJGL3» должна отображаться в списке «Определенные пользовательские библиотеки:».
  5. Теперь выберите этот элемент в списке и нажмите «Добавить внешние JAR …». Это открывает стандартный диалог выбора файла ОС, позволяющий выбрать файлы * .jar, которые будут добавлены в путь к классам / путь сборки всех проектов, используя пользовательскую библиотеку LWJGL3. Перейдите в и выберите все * .jar-файлы, в которых нет -javadoc или -sources. Убедитесь, что вы не забыли файл lwjgl-natives- .jar и нажмите «Открыть».
  6. Это заполнит пользовательскую библиотеку LWJGL3 в списке соответствующими записями для всех выбранных файлов jar. Теперь вы можете оставить его, чтобы использовать LWJGL 3.
  7. Однако, если вы хотите иметь источники и JavaDocs, вам нужно будет выбрать каждую из записей, щелкнуть «Вложение источника: (Нет)» и «Изменить …». Откроется диалоговое окно «Конфигурация источника подключения». Здесь вы можете выбрать «Внешнее местоположение» и «Внешний файл …», чтобы выбрать соответствующий файл * -sources.jar.
  8. Чтобы фактически использовать пользовательскую библиотеку LWJGL3 в одном из ваших проектов, перейдите на вкладку «Путь сборки» вашего проекта и выберите вкладку «Библиотеки». Здесь нажмите «Добавить библиотеку …», выберите «Пользовательская библиотека» и отметьте «Пользовательскую библиотеку LWJGL3».
  9. Теперь вы настроены на использование LWJGL 3 в своем проекте.

Другие инструменты

Если вы знакомы с другими инструментами или IDE, не стесняйтесь добавлять информацию о настройке LWJGL в них в этом разделе. Если вы хотите внести свой вклад, пожалуйста, создайте сообщение на форуме.


Переведено для jmonkeyengine.ru, оригинал
Автор перевода: Andry

jMonkeyEngine.ru © 2017. Все права сохранены.