Пилю физический движок и автосимулятор на нём.

Кажется, я уже перерос libgdx и он сейчас больше мешает, чем помогает. Кто-то писал что к godot легко прикрутить свой физический движок — жду в комментариях рассказа как это сделать и ссылок с примерами.



Штуки, которых не было из коробки и я писал их сам:

1. Поддержка руля с force feedback через SDL.
2. Привязка звука к объектам в 3д. Подключил miniaudio, теперь право-лево и ближе-дальше нормально слышно.
3. Нет быстрой ECS. (Тут больше претензии к особенностям JVM, чем к libgdx).

В чём проблема с ECS — в языках типа С++ ECS может разложить все компоненты одного типа просто в один массив, доступ к ним будет линейным чтением из памяти, а в случае java объекты раскиданы как попало и на это никак не повлиять.

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

Я сделал два эксперимента и в обоих производительность проседала в разы. В итоге я решил что скорость важнее красоты кода и оставил это поле в Entity.
Эксперимент 1: как и для остальных компоненов, просто использовал HashMap[Entity, PhysicsBody] и вместо обращеня к полю лазил туда. Ужасно медленно.
Эксперимент 2: Заменил тип Entity на Int, для PhysicsBody завёл массив, где по индексу лежал нужный объект.
Производительность просела раза в два-три, и кажется JIT стал хуже справляться, т.к. производительность была очень нестабильной и где-то через десяток секунд устаканаливалась на чём-то стабильном, но раза в два-три более медленном чем с доступом к полю в Entity.

В эту неделю попробовал ради эксперимента сделать в игре модель мотоцикла. И это офигенно получилось — почти сразу после начальных настроек он поехал. Причём, как и настоящий мотоцикл, на скорости он был устойчивый, а рулить надо было контррулением. Это достигается за счёт угла наклона вилки и трейла.

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



Тут могло бы быть видео, но форум не даёт его загрузить файлом.

Проблемы, которые я не решил (и почему хочу перейти к чему-то типа Godot/Unity/UE):
В libgdx нет редактора объектов. Я не могу просто взять сцену, накидать на неё компонентов колес, пружин, амортизаторов, двигателя, коробки передач, дифференциала и в редакторе движка собрать из них транспортное средство. И ассета из этой штуки я тоже не сделаю.

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


UPD 2024-12-03


В качестве эксперимента накидал плоскую трассу из линий. Вообще мне понравился такой подход — не надо думать про текстуры и тратить время на детализацию. Можно просто накидать контуры в Blender и закинуть в движок. Этого достаточно, чтобы как-то оценить управляемость машины и сопоставить размеры моделей. Вся карта в obj весит около 50 килобайт и легко парсится самодельным парсером. Стандартный парсер obj от libgdx ожидает увидеть полигоны вместо линий и эти файлики не переваривает.



UPD 2024-12-04


Попробовал загрузить ресурсы из Ассето Корсы. Формат хранения у них бинарный, но довольно простой и на гитхабе уже есть код, который его парсит: github.com/RaduMC/kn5-converter/blob/master/kn5%20converter/Program.cs

Осталось только под свой язык программирования переписать.

Если кратко по формату и организации сцены в формате kn5 — сцена состоит из дерева нодов. У каждой ноды есть имя и список дочерних. Видов нод три — нода с матрицей преобразования, нода с мешем и нода с анимированным мешем. В файлах попадаются пустые ноды — кажется, их ещё используют в качестве зашлушек, чтобы потом по этой позиции добавить колесо или ещё что-то.



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

Так же прямо внутри хранятся текстуры в dds и какие-то параметры материалов для шейдеров. Но сами шейдеры хз где.


UPD 2024-12-13



Поправил загрузку, добавил текстуры.
Много времени потратил на борьбу с libgdx. На мой взгляд в этом движке много оверинжиниринга и какие-то простые вещи делаются сложно, а вдобавок чтобы использовать libgdx, надо знать и как работает openGL, и как работают велосипеды в движке поверх него. А для чего-то сложного движок слабо подходит, например поддержки загрузки dds текстур в движке нет
. Я нашёл какую-то внешнюю библиотеку (gdx-dds), но она на попытке загрузки каких-то текстур кидается исключениями. В итоге я вместо вызова «загрузить текстуру» трачу время.

На мой взгляд пример оверинжиниринга: RenderContext.java — чтобы его использовать, надо целиком прочитать его код, и потом понять что под капотом он что-то запоминает и как-то вызывает openGL

Больше всего я сгорел с бага с текстурными координатами. Попробуйте найти ошибку:

    val mesh = new Mesh(
      true,
      node.verticesData.length,
      node.indices.length,
      new VertexAttribute(VertexAttributes.Usage.Position, 3, ShaderProgram.POSITION_ATTRIBUTE),
      new VertexAttribute(VertexAttributes.Usage.Normal, 3, ShaderProgram.NORMAL_ATTRIBUTE),
      new VertexAttribute(VertexAttributes.Usage.TextureCoordinates, 2, ShaderProgram.TEXCOORD_ATTRIBUTE),
      new VertexAttribute(VertexAttributes.Usage.Tangent, 3, ShaderProgram.TANGENT_ATTRIBUTE),
    )
Ошибка в том, что для текстурный хоординат должно быть так
ShaderProgram.TEXCOORD_ATTRIBUTE + "0"
В движке ни намёка на то, что текстурные координаты должны быть с нулём или ещё какой-то цифрой в конце.


UPD 2024-12-15

По совету Mr F попробовал использовать MSAA и включить alpha to coverage. Это прекрасно! Офигенная, забытая технология древних. С MSAA 16x получилась почти идеальная картинка для полупрозрачных объектов буквально добавлением пары строчек. Артефакты при желании можно увидеть, но усложнять код и использовать более сложные подходы я не хочу. Сейчас у меня осовной упор именно на проработку физики, графика подойдёт почти любая.


  • 3

11 комментариев

KCEPOKC
По поводу перфа, можно инлайнить композицию, правда это не динамическая история вообще, нужно будет каждую комбинацию описывать. У меня на проекте я специально делал кодген чтобы заинлайнить массив в класс, типа делал абстрактный MyCoolArray и реализации MyCoolArray0, MyCoolArray1 и тп. Но была цель оптимизироваться по памяти в первую очередь. После 16ти элементов уже была реализация MyCoolArrayN с массивом честным.

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

Если совсем с ума сойти, можно ваще сделать «value types у нас дома» через VarHandle + byte[]. Производительность должна быть на уровне, тк JIT потом это всё вклеивает нормально.

конечно, это крайне костыльно, потому что JVM:(
Комментарий отредактирован: 29 ноября 2024, 14:46 (5 раз)
1
Igor
Интересно, я так глубоко не забирался.
Кстати насчёт кодогенерации и прочих ухищрений — в скале есть макросы и inline функции, можно с их помощью что-то навертеть.
1
KCEPOKC
а что если попробовать запустить на бете JDK с вальгаллой?

там норм валью тайпы есть
Комментарий отредактирован: 28 декабря 2024, 11:51
0
Igor
Я попробовал на очень простом примере. Как ни странно, на моём экспериментальном и очень простом примере value класс в два раза медленнее.

Как я сделал — скачал экспериментальную сборку JDK, распаковал. Скачал отсюда: jdk.java.net/valhalla/. Прям в папочку bin положил класс с файлом и вызвал его как /java --enable-preview --source 23 ValuePoint.java


public value class ValuePoint {
    public final double x;
    public final double y;

    public ValuePoint(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public ValuePoint add(ValuePoint p) {
        return new ValuePoint(x + p.x, y + p.y);
    }

    public ValuePoint sub(ValuePoint p) {
        return new ValuePoint(x - p.x, y - p.y);
    }

    public String toString() {
        return "ValuePoint2(" + x + "," + y + ")";
    }

    public static void main(String[] args) {
        ValuePoint[] arr = new ValuePoint[1000];

        for (int i = 0; i < arr.length; i++) {
            arr[i] = new ValuePoint(i, i);
        }

        for (int k = 0; k < 100; k++) {
            long start = System.nanoTime();
            ValuePoint sum = new ValuePoint(0, 0);
            for (int i = 0; i < arr.length; ++i) {
                for (int j = 0; j < arr.length; ++j) {
                    sum.add(arr[j].sub(arr[i]));
                }
            }
            long end = System.nanoTime();
            System.out.println("sum = " + sum + " time = " + (end - start) + "ns");
        }
    }
}

Без слова value шаг вычисления внутри цикла завершается за 0.7 мс, с ним за 1.3мс.
Комментарий отредактирован: 28 декабря 2024, 17:15
0
KCEPOKC
мдамсики

чёто это не вяжется с тем что они на презентациях показывали))
0
KCEPOKC
Щас попробовал у себя собрать на коленке бенчмарк по мотивам. стянул свежий бинарь JDK+valhalla и добавил туда JMH


package test;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.Random;
import java.util.concurrent.TimeUnit;


@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
@Fork(value = 1)
@Warmup(iterations = 5, timeUnit = TimeUnit.MILLISECONDS, time = 5000)
@Measurement(iterations = 5, timeUnit = TimeUnit.MILLISECONDS, time = 5000)
public class BenchValhalla {
    private Point[] mutablePoint;
    private ValuePoint[] valhallaPoint;

    @Setup
    public void setup() {
        var r = new Random(69);
        var samples = 5000;
        this.valhallaPoint = new ValuePoint[samples];
        this.mutablePoint = new Point[samples];

        for (int i = 0; i < valhallaPoint.length; i++) {
            valhallaPoint[i] = new ValuePoint(r.nextDouble(), r.nextDouble(), r.nextDouble(), r.nextDouble());
        }
        for (int i = 0; i < mutablePoint.length; i++) {
            var valh = valhallaPoint[i];
            mutablePoint[i] = new Point(valh.x, valh.y, valh.z, valh.w);
        }
    }

    @Benchmark
    public ValuePoint valhallaPoint() {
        var arr = valhallaPoint;
        var sum = new ValuePoint(0, 0, 0, 0);
        for (int i = 0; i < arr.length; ++i) {
            for (int j = 0; j < arr.length; ++j) {
                sum = sum.add(arr[j].sub(arr[i]));
            }
        }
        return sum;
    }

    @Benchmark
    public Point plainOldMutablePoint() {
        var arr = mutablePoint;
        var sum = new Point(0, 0, 0, 0);
        for (int i = 0; i < arr.length; ++i) {
            for (int j = 0; j < arr.length; ++j) {
                sum.mutableAdd(arr[j]);
                sum.mutableSub(arr[i]);
            }
        }
        return sum;
    }

    @Benchmark
    public Point plainOldImmutablePoint() {
        var arr = mutablePoint;
        var sum = new Point(0, 0, 0, 0);
        for (int i = 0; i < arr.length; ++i) {
            for (int j = 0; j < arr.length; ++j) {
                sum = sum.addImmutable(arr[j].subImmutable(arr[i]));
            }
        }
        return sum;
    }

    public static void main(String[] args) throws Exception {
        var options = new OptionsBuilder()
                .include(BenchValhalla.class.getSimpleName())
                .build();
        new Runner(options).run();
    }

    static value class ValuePoint {
        public final double x;
        public final double y;
        public final double z;
        public final double w;

        public ValuePoint(double x, double y, double z, double w) {
            this.x = x;
            this.y = y;
            this.z = z;
            this.w = w;
        }

        public ValuePoint add(ValuePoint p) {
            return new ValuePoint(x + p.x, y + p.y, z + p.z, w + p.w);
        }

        public ValuePoint sub(ValuePoint p) {
            return new ValuePoint(x - p.x, y - p.y, z - p.z, w - p.w);
        }
    }

    static class Point {
        public double x;
        public double y;
        public double z;
        public double w;

        public Point(double x, double y, double z, double w) {
            this.x = x;
            this.y = y;
            this.z = z;
            this.w = w;
        }

        public Point mutableAdd(Point p) {
            x += p.x; y += p.y; z += p.z; w += p.w;
            return this;
        }

        public Point mutableSub(Point p) {
            x -= p.x; y -= p.y; z -= p.z; w -= p.w;
            return this;
        }

        public Point addImmutable(Point p) {
            return new Point(x + p.x, y + p.y, z + p.z, w + p.w);
        }

        public Point subImmutable(Point p) {
            return new Point(x - p.x, y - p.y, z - p.z, w - p.w);
        }
    }
}


Результат по итогу вот такой:


Benchmark                             Mode  Cnt       Score      Error  Units
BenchValhalla.plainOldImmutablePoint  avgt    5  112872,954 � 7424,274  us/op
BenchValhalla.plainOldMutablePoint    avgt    5   37533,453 �  181,333  us/op
BenchValhalla.valhallaPoint           avgt    5   18884,832 �  554,865  us/op
Комментарий отредактирован: 29 декабря 2024, 14:50
1
KCEPOKC
энивэй без дженериков вальгала это недоимплемент почти неюзабельный

терпите, джавики.

вот тебе, блять, 10 лет люди работали. жаль джава не умерла.
Комментарий отредактирован: 29 декабря 2024, 14:56
0
Igor
Для истории — я сделал четыре класса.
изменяемый, с final полями, record и Value класс. Rysen 3700x, VM version: JDK 23.0.1, OpenJDK 64-Bit Server VM, 23.0.1+11-39

Benchmark                           Mode  Cnt      Score     Error  Units
BenchValhalla.finalPoint            avgt    5   3399.923 ±  88.193  us/op
BenchValhalla.plainOldMutablePoint  avgt    5  52134.455 ± 942.619  us/op
BenchValhalla.recordPoint           avgt    5   3354.371 ±  56.565  us/op
BenchValhalla.valhallaPoint         avgt    5   3385.298 ±  39.188  us/op
0
Igor
https://github.com/Kright/mySmallProjects/tree/master/2024/wellcome2valhalla
Вот мой код с уже готовым проектом если кто захочет по-быстрому попробовать.
1
Randomize
А физический движок позволил бы сделать реалистичную версию Road Rash?
Интересен именно момент, когда один мотоциклист пинает другого, влияя на стабильность аппарата на высокой скорости
0
Igor
В принципе можно, но по закону Ньютона сила будет действовать на оба мотоцилка :) В реальности мотоциклисты стараются не контактировать ни с чем, очень высокая вероятность улететь. Ещё скажу, что в реальности пилот очень сильно влияет на мотоцикл тем как сидит и держится за руль, например если вцепляться в руль — можно поймать вобблинг и начнутся автоколебания руля влево-вправо или наклониться влево-вправо и повлиять на баланс мотоцикла. Вот этих нюансов к сожалению почти никак не передать.

Вообще тема мотоциклов мне интересна, но без вестибулярного аппарата и на гоночном круглом руле управлемость как у пьяного самокатчика. Это совсем не то, что я чувствовал на реальном мотоциле.

Может быть, что-то такое можно сделать, если как в игральных автоматах поставить модель мотоцикла, сделать мощный и точный большой руль с force feedback и ещё какие-нибудь приводы для наклона мотоцикла влево-вправо (тоже с force feedback, чтобы чувствовать баланс веса игрока). Потому что мотоцикл рулится контррулением и это очень требовательно к точности и скорости реакции руля.
1