Обновить
37
0
Сергей @SergeySib

Пользователь

Отправить сообщение
Для 2Д точно так же хватает библиотек.

То что спрятано в 3Д-библиотеках действительно проще описанного в статье. Но простота эта не от того что предметная область проще (как утверждает статья), а от того, что никто даже не думал использовать «настоящую» математику в массовом повседневном 3Д.
Но ведь и примитивный растеризатор 2Д-сплайна пишется просто, а трёхмерного аналога SVG не существует в принципе. Подозреваю, что сравнимый по выразительности формат для трёхмерных сцен был бы куда сложнее своего двумерного собрата.
> Почему векторная графика 2D намного сложнее, чем 3D

Так почему же? В 2Д художники давно и успешно пользуются кривыми самых разных семейств, а процессоры из 1980х успешно справляются с их растеризацией на стороне пользователя. В 3Д же человечество только начало делать первые робкие шаги к рейтрейсингу в реальном времени, который есть та же растеризация неявных поверхностей. Использование полигонов (грубой линейной аппроксимации) как раз говорит о том, что мы недоросли до использования в 3Д тех же вещей, которыми с лёгкостью оперируем в 2Д.

Если бы 2Д было таким же сложным как 3Д, мы бы читали текст на шрифтах из отрезков прямой и плоских треугольников. Да, с мультисемплингом по краям.

Как пример, движущиеся 3Д-картинки от упомянутого iquilez жрут 100% GPU на последних видеокартах, показывая 30 фпс. И это с учётом их прекрасной распараллеливаемости на фрагментном шейдере.

P.S. За перевод всё равно спасибо.
Вы хотите сказать, что в C++ когда нужно иметь ссылку на один и тот же объект, то люди, чтобы не парится с контролем времени жизни, тупо размещают объект в динамической памяти и используют shared_ptr?

Я скорее хотел сказать, то в C++ без библиотечного класса приходится париться с временем жизни вручную. То есть умные указатели ничем не хуже рустовской модели памяти, просто ниша у C++ немного другая, как бы разработчики Руста не заявляли о попытках вытеснить C++ из традиционных областей его применения.
Мнение автора, а скорее даже подача материала показались мне достаточно интересными, чтобы поделиться ими с русскоязычной аудиторией. Тем более, здесь вроде встречаются переводы статей, выражающих личное (и иногда весьма спорное) мнение авторов.

Более того, в качестве «побочного эффекта», из этой статьи я вынес для себя идею make_visitor, и увидел хороший пример использования variadic using.

По вашему второму вопросу, думаю тут всё дело в умолчании. В Rust для небезопасного разделения владения объектом нужно совершить лишние телодвижения. В C++ наоборот, чтобы приблизить семантику к Rustовской, приходится совершать дополнительные действия, например использовать умные указатели.
Спасибо, поправил.
С технической точки зрения крутая статья!

Спасибо!

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

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

Primitive assembly бывает разным и в нескольких местах. После вершинного шейдера действительно есть сборка примитивов, но она собирала линии и треугольники только если за ней больше нет преобразований геометрии. Как сборка примитивов перед тесселяцией и геометрическим шейдером может объявить примитив невидимым? Ведь эти стадии могут перепахать геометрию до неузнаваемости и сделать видимым то, что было якобы невидимым после вершинного шейдера. А как интерпретировать примитив, у которого после стадий VertexShader->PrimitiveAssembly 32 вешины, а не 2 или 3? А ведь такое вполне может быть при включенной тесселяции.
При включенной тесселяции вершинный шейдер обрабатывает абстрактные наборы атрибутов, которые называются «вершинами» только условно. То есть они совсем не обязаны отражать положение конечной геометрии. Например, программа вообще может подать в конвейер набор одномерных нулей. Эти нули нужны только для того, чтобы конвейер запустился. Тогда вершинный или какой нибудь из последующих шейдеров может достать данные о геометрии из юниформов или из текстуры. Или же вообще сгенерировать их процедурно псевдослучайной функцией.
Ну или другой экстремальный вариант: рисуем ландшафт и подаём на конвейер по набор вершин с единственным атрибутом на каждый тайл. И этот атрибут обозначает цвет, не неся никакой информации о положении тайла в пространстве и его форме. Высоту и/или искривления тайлов шейдеры теселяции/геометрии возьмут из текстуры, а пространственное положение высчитают из встроенной переменной gl_PrimitiveID.

Если у меня миллион частиц, а я вижу только одну, в геометрический шейдер попадет только одна точка.

Почему? Отсечение плоскостями должно происходить после проективного преобразования. Иначе откуда конвейер узнает какова матрица проекции? Но если мы применили, например, перспективную проекцию к условному скелету до геометрического шейдера, то как нарастить на него полигональное мясо в геометрическом шейдере? Это ж будет адова некрасивая математика с вычислительным оверхедом. Куда удобнее иметь дело с геометрией в системе координат модели, и только перед самым EmitVertex() умножать её на мировую матрицу (перенос, поворот) и на проективную.
Такой способ как раз отражает последняя диаграмма на странице 8 официального референса: отсечение и перспективное деление происходят после геометрического шейдера, последующей сборки примитивов (они на это стадии могут быть только точками, line_strip или triangle_strip) и feedback transform.
В вершином шейдере ничего не отсекается, вершинный шейдер предоставляет данные для отсечения, которое происходит во время сборки примитивов.

Но ведь ничто принципиально не мешает вычислить те же данные для отсечения в геометрическом шейдере? Позвольте проиллюстрировать кодом. Правильно ли я понимаю, что вы имеете ввдиду что-то подобное?
// Вершинный:
layout(location=0) in vec4 position;
out bool visible;
void main(void) {
    visible = point_is_visible(position);
    gl_Position = billboard_position_transformations(position);
    // или просто gl_Position = position;
}

// Геометрический:
layout(points) in;
layout(triangle_strip,) out;
in bool visible[];
void main() {
    if(visible[0]) {
        // Генерируем два треугольника.
    }
}


Я же предлагаю такое:
// Вершинный:
layout(location=0) in vec4 position;
void main(void) {
    gl_Position = position;
}

// Геометрический:
layout(points) in;
layout(triangle_strip,) out;
void main() {
     vec4 position = billboard_position_transformations(gl_in[0].gl_Position);
    // опять же, функции billboard_position_transformations может не быть.
    if(point_is_visible(position)) {
        // Генерируем два треугольника.
    }
}


Эти два куска кода выполняют одно и то же: вычисляют point_is_visible для каждой точки (миллион точек), и генерируют два треугольника, если 999999 не видны. То есть в геометрическом шейдере делается всё то же, что может делаться в вершинном. Атрибуты и юниформы, используемые в point_is_visible можно сделать доступными на любой их этих стадий. Второй пример кода как раз похож на строки из демки:
float halfspaceCull =
        step(dot(eyePosition - gl_in[gl_InvocationID].gl_Position.xyz, lookDirection), 0);
gl_TessLevelOuter[1] = lod() * halfspaceCull;

Только здесь TCS вместо геометрического. Он освобождает генератор абстрактных патчей и TES от генерации геометрии, если точка позади камеры.

Отсечение и перспективное разделение происходят до фрагментного шейдера

Так я ровно об этом и говорил. То есть не будут обрабатываться фрагменты полигонов трёх больших групп примитивов:
  1. Если примитив вообще не был выпущен геометрическим шейдером, то есть когда point_is_visible(position) == true. Путь входной точки на этом заканчивается.
  2. Если из точки по какой-то причине были сгенерированы два треугольника, но они не попали в видимую область из-за проективного преобразования.
  3. Если примитив в области видимости загорожен другим примитивом. Проверка z-буфера тоже происходит перед фрагментным шейдером.

Мне всего лишь хотелось проиллюстрировать, что когда мы говорим о генерации геометрии, нет смысла говорить о явном discard() во фрагментом шейдере.
Но как отсечь точку в вершинном шейдере? Даже если вы не запишите ничего в gl_Position, точка всё равно пойдёт дальше по конвейеру со значениями по умолчанию, то есть (0, 0, 0, 0). Вызов discard() в вершинном шейдере отсутствует, он есть только во фрагментном, экономия на котором получится «автоматически» для невидимых (и даже просто для загороженных) биллбордов. В итоге, все «отсечённые» биллборды будут генерироваться в начале координат.

Можно конечно принять решение об отмене генерации в вершинном шейдере, и передать его в геометрический через in/out. Но то же самое можно сделать непосредственно в геометрическом шейдере без передачи переменной между стадиями.
Нет, пока не сравнивал. Если будет время, планирую сравнить как минимум с двумя вариантами: полностью готовая геометрия и готовые кости + геометрический шейдер.

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

Существующий порядок шейдеров мне тоже казался вывернутым наизнанку. Но наверно логика здесь примерно такая: в геометрическом шейдере можно сделать всё тоже самое, что и в вершинном, и даже ещё больше. Если нужна повершинная обработка сгенерированной геометрии, то её всегда можно написать перед EmitVertex().
Добавил видео в конец статьи.
Автор, спасибо за статью! Хотелось бы добавить небольшое уточнение:

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

Может. Стандарт С99 ввёл ключевое слово restrict, которое сообщает компилятору, что области памяти не могут пересекаться. Разумеется, ответственность за непересечение ложится на программиста, но компилятор может применять оптимизации «как в Фортране». В С++ это ключевое слово так и не попало, но разработчики компиляторов его часто реализуют в виде расширения, например __restrict__ в GCC.
Почему же при такой любви Комитета к функциональщине, в стандартную библиотеку еще не внесли «джентльменский набор» из всевозможных apply, fold, map, zip, zipWith и т.д., создающих последовательности вызовов на этапе компиляции? Ведь все предпосылки уже есть, и часто приходится самостоятельно писать вещи для std::array или std::tuple с различной степенью кривизны. Планируется ли что-нибудь из этого в ближайшем будущем кроме уже внесённого apply для кортежей?
2

Информация

В рейтинге
Не участвует
Откуда
Красноярск, Красноярский край, Россия
Дата рождения
Зарегистрирован
Активность