nsf gamedev


ЖурналСтатьиФорумИнфо

Статьи

2 ноя 2007

Неудавшийся эксперимент по текстурированию ландшафта.

В августе/сентябре этого года я занимался небольшим исследованием в области ландшафтов. Это статья о том, что получилось и что не получилось :)

Читать | Комментарии [1]

Журнал

27 сен 2014

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

Лежит тут: https://github.com/nsf/nextgame

Работа над движком продолжается. Недавно тыкал систему материалов в рендере ( http://imgur.com/a/Ui6OD ), тени ( http://imgur.com/a/vcI9E ), ну и всякое разное.

http://i.imgur.com/CJqMosa.jpg

http://i.imgur.com/SgjGtgb.jpg

Ссылка

29 апр 2014

Да, я знаю. Обещал статью написать про воксели. Честно признаюсь - даже начал, но пока еще не закончил. Оказалось, что мало знаю о вокселях. Хотел написать подробно. Меня в первую очередь интересует практическое применение вокселей в геймдеве. А для этого не достаточно просто взять их и сконвертировать в геометрию и подключить физику.

Изначально идея была взять воксели и их использовать в качестве материала для LOD механизма. Т.е. упрощать не отображаемую геометрию, а исходный материал. Думал это будет полезно и для сетевой игры - меньше передавать данных по кабелю. Но оказалось, что как только я упрощаю "воксельные поля", они становятся на 100% непригодными для работы с игровой логикой. Кроме этого, "продвинутые" алгоритмы визуализации вокселей усложняют работу с ними. Так например в популярных ныне двойственных алгоритмах, для того чтобы получить информацию об одном генерируемом кваде, нужно рассмотреть 3х3х2 вокселей (18), когда как в старом marching cubes достаточно лишь 8 вокселей. У всех алгоритмов есть плюсы и миунсы. Двойственные алгоритмы генерируют более качественный mesh, но он по умолчанию non-manifold, т.е. топологию качественной не назовешь, это решается дополнительными усилиями. MC хорош тем, что генерирует исключительно manifold геометрию, но у него имеются проблемы с вырожденными треугольниками. Все решаемо.

На данный момент решил вернутся к базовому marching cubes и добавить пост-процессинг для упрощения геометрии. Результаты пока впечатляют, скоро перейду непосредственно к работе с LOD механизмами и логикой редактирования. Привожу пару скриншотов работы алгоритма упрощения геометрии: http://imgur.com/a/LFEBr. Результат убедителен, уменьшая количество треугольников в 12.75 раз, визуальное качество практически остается не тронутым. Конечно, нужно понимать, что это карта с одним материалом, но с другой стороны приведенная геометрия достаточно капризная, реальный ландшафт будет чуток проще.

Ссылка

16 мар 2014

Многие наверянка уже не раз слышали это сравнение. Дескать, С++ это своего рода швейцарский нож среди языков программирования. Кто-то представляет такое сравнение в выгодном свете, подчеркивая этим факт, что в С++ есть все необходимое. Для других же, это признак излишества и композиционной бесвкусицы. Но задумывались ли вы о социальных эффектах сего сравнения? Приходит к вам программист, а вы его спрашиваете: "Умеешь гвозди забивать?" - и даете ему молоток. Крайне высока вероятность, что кандидат все-таки сообразит какой конец к чему нужно приложить. А если дать швейцарский ножик? Стало быть, отличный инструмент для вскрытия неадекватности получается.

Мысль дня. Навеяло чужим говнокодом. Свой не пахнет.

Ссылка

1 янв 2014

Сразу предупреждаю, качество на ютубе так себе. Смотрите лучше по альтернативной ссылке в vimeo.

Запустить видео по клику - Как делать игрыЗапустить видео по клику - Как делать игры

Vimeo: https://vimeo.com/83150040

Что на видео? Если вкратце, то ничего продвинутого. Ландшафт представлен в виде воксельных данных, генерируется простой комбинацией 3ех самплов разного перлин нойса (один из самплов из 3д перлин нойса). Геометрия генерируется алгоритмом naive surface nets. Как видно в видео имеется поддержка разных уровней детализации. И базовая система чанков с возможностью распараллеливания работы на несколько потоков. Что касается рендера там совсем ничего интересного. Перлин нойс небо на шейдерах, освещение из фиксированного источника, считается только диффузная часть, нормал мап никаких нет пока что, текстуринг обычный трипланарный.

Видео записано на i5-3470, с использованием 3ех рабочих потоков. Видяшка вполне обычная gtx560.

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

Ссылка | Комментарии [3]

7 ноя 2013

Изображение

Продолжается мое путешествие в страну слоников и фей вокселей. Потихоньку пишу систему чанков и LOD'ов. Пока только картинки: http://imgur.com/a/rymiz

Думаю сделать видео с комментами в ближайшем будущем.

Изображение

19 окт 2013

Изображение

К моему удивлению нашел применение геометрическим шейдерам - per-vertex материал! В вершинный буфер грузим индекс материала. В геометрическом шейдере для каждой вершины создаем два новых вектора из 3 элементов, в первом индексы, во втором вес материала:
vec3(mat[0], mat[1], mat[2]); vec3(1, 0, 0)
vec3(mat[0], mat[1], mat[2]); vec3(0, 1, 0)
vec3(mat[0], mat[1], mat[2]); vec3(0, 0, 1)

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

Может кто знает что на эту тему? Хочется per-vertex материал, при этом чтобы геометрия осталась из индексированных вершин.

P.S. Если интересно про то, как делал ландшафт, в комментах отпишитесь, может напишу и про это.

Ссылка | Комментарии [5]

5 мая 2013

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

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

Место в грамматике


Интуитивно кажется, что constexpr это нечто похожее на const, а поэтому является частью типа в грамматике С++11, но это не так. На самом деле constexpr это совершенно отдельный declaration specifier. Вполне себе такой же как и typedef или friend, а поэтому может появляться именно там, где могут появляться эти два. Почему это важно? А потому что при работе с неимоверно сложным синтаксисом declarations в С++, стоит понимать, что правила которые действуют для const, не действуют для constexpr. Поясню на примере с указателями:
constexpr int number = 43;
int main(int argc, char **argv) {
        constexpr int *p = &number; // error: cannot initialize a variable of type 'int *const' with an rvalue of type 'const int *'
        return 0;
}

Любезный clang ясно дает понять, тип указателя p - int *const, вместо ожидаемого const int*. Так вот, собственно первый вывод: constexpr применяется к declaration в целом и не является частью типа. Если constexpr с указателями в общем-то редко используется, знать этот факт вдвойне важно при определении функций. Дело в том, что constexpr не является частью типа возвращаемого значения функции как это было бы в случае с const. Приведу опять пример с указателем и функциями, где этот факт четко иллюстрирован:

int number = 10;
constexpr int *number_addr() {
        return &number;
}
int main(int argc, char **argv) {
        int *p = number_addr();
        return 0;
}

Вполне компилируется и работает. Согласен, что пример не особо полезен на практике, но позже я покажу почему важно это все знать. Бывают реально случаи, когда указатели используются в constexpr функциях. А пока давайте суммируем наконец то, как constexpr связан с const:

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

Надеюсь, все выше сказанное помогло в какой-то мере понять каким образом constexpr применяется к сущностям языка и устронило недопомнимание на тему const vs constexpr.

Когда применять constexpr


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

Если коротко, то constexpr следует применять везде, где можно. И если достаточно понятно, когда применять constexpr к переменным и обычным функциям, то применение в классах требует небольшого пояснения. Рассмотрим пример:

class vec3 {
        union {
                struct {
                        float _x, _y, _z;
                };
                float _v[3];
        };

public:
        constexpr vec3(): _x(0), _y(0), _z(0) {} // (1)
        constexpr vec3(float x, float y, float z): _x(x), _y(y), _z(z) {}
        constexpr vec3(const vec3&) = default; // (2)
        vec3 &operator=(const vec3&) = default; // (3)
        constexpr float x() { return _x; } // (4)
        constexpr float y() { return _y; }
        constexpr float z() { return _z; }
        constexpr const float *v() { return _v; } // (5)
        float *v() { return _v; } // (6)
};

1. С конструкторами я думаю понятно. Если у конструктора пустое тело и есть возможность сделать его constexpr - делайте.
2. Default и delete члены тоже можно объявлять как constexpr. Вполне возможно, что они становятся constexpr при возможности по умолчанию, но для целей самодокументирования никогда не повредит явно указать это.
3. Оператор присвоения обычно не определяется как constexpr, потому что традицонно он возвращает не константную ссылку на *this. А как мы знаем из первой секции этого поста, constexpr делает функцию-член константной (this становится const). Просто не уместно, да и область применения сомнительна, хотя вполне возможно и этот оператор сделать constexpr.
4. Если кто-то еще не понял, объявление constexpr float x() { return _x; } внутри класса эквивалентно float x() const { return _x; }, а поэтому везде где нам хотелось бы определить константную функцию-член и где есть возможность сделать ее constexpr функцией, надо делать.
5. Не забываем важную информацию из первой секции поста - constexpr не влияет на тип возвращаемого значения функции, а поэтому constexpr const сочетание очень даже имеет смысл. Кроме того, т.к. constexpr неявно делает функцию-член константной, мы не можем вернуть float* используя const float _v[3].
6. Еще раз, учитывая, что (5) является константной функией-членом, можно сделать перегрузку для не константного случая, как это традиционно делается в С++ для акцессоров возвращающих указатель или ссылку. Так что все тут вполне корректно.

Заключение


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

Ссылка | Комментарии [2]

3 мар 2013

Совсем маленький, возможно абсолютно бесполезный пример. Просто пришло в голову, решил записать куда-то.

#include <string>
#include <cstdio>
#include <cstddef>

class formatter {
  const char *format;
public:
  explicit formatter(const char *format): format(format) {}

  template <typename ...Args>
  std::string operator()(Args ...args) {
    char tmp[4096];
    size_t n = std::snprintf(tmp, sizeof(tmp), format, args...);
    return {tmp, n};
  }
};

formatter operator "" _fmt(const char *format, size_t) {
  return formatter(format);
}

int main(int argc, char **argv) {
  std::string s = "hello, %s"_fmt("nsf");
  std::string s2 = "%d + %d = %d"_fmt(1, 2, 3);
  std::printf("%s\n", s.c_str());
  std::printf("%s\n", s2.c_str());
  return 0;
}

Стоит отметить, что возможно лучше использовать variadic функцию вместо variadic шаблона - меньше кода компилятор будет генерировать. Но вспоминать va_start/va_end и va_list было лень, поэтому так.

Ссылка