GPU Particles с использованием Compute и Geometry шейдеров

  • Tutorial
Привет, дорогой читатель!

Сегодня мы продолжим изучение графического конвейера, и я расскажу о таких замечательных вещах, как Compute Shader и Geometry Shader на примере создания системы на 1000000+ частиц, которые в свою очередь являются не точками, а квадратами (billboard quads) и имеют свою текстуру. Другими словами, мы выведем 2000000+ текстурированных треугольников при FPS > 100 (на бюджетной видеокарте GeForce 550 Ti).



Введение


Я очень много писал о шейдерах среди своих статей, но оперировали мы всегда только двумя типами: Vertex Shader, Pixel Shader. Однако, с появлением DX10+ появились новые типы шейдеров: Geometry Shader, Domain Shader, Hull Shader, Compute Shader. На всякий случай напомню, как выглядит графический конвейер сейчас:



Сразу оговорюсь, что в этой статье мы не затронем Domain Shader и Hull Shader, про тесселяцию я напишу в следующих статьях.

Неизученным остается только Geometry Shader. Что же такое Geometry Shader?

Глава 1: Geometry Shader


Vertex Shader занимается обработкой вертексов, Pixel Shader занимается обработкой пикселей, и как можно догадаться – Geometry Shader занимается обработкой примитивов.

Этот шейдер является опциональной частью конвейера, т.е. его вовсе может и не быть: вертексы напрямую поступают в Primitive Assembly Stage и дальше идет растеризация примитива.
Geometry Shader находится между Primitive Assembly Stage и Rasterizer Stage.

На вход он может получить информацию как о собранном примитиве, так и о соседних примитивах:

image

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

struct PixelInput
{
	float4 Position : SV_POSITION; // стандартный System-Value для вертекса
};

[maxvertexcount(1)] // максимальное кол-во вертексов, которое мы можем добавить
void SimpleGS( point PixelInput input[1], inout PointStream<PixelInput> stream )
{
	PixelInput pointOut = input[0]; // получение вертекса
	
	stream.Append(pointOut); // добавление вертекса
	stream.RestartStrip(); // создаем примитив (для Point – требуется один вертекс)
}

Глава 2: StructuredBuffer


В DirectX10+ появился такой тип буферов как Structured Buffer, такой буфер может быть описан программистом как ему угодно, т.е. в самом классическом понимании – это однородный массив структур определенного типа, который хранится в памяти GPU.

Давайте попробуем создать подобный буфер для нашей системы частиц. Опишем какими свойствами обладает частица (на стороне C#):

public struct GPUParticleData
{
     public Vector3 Position;
     public Vector3 Velocity;
};

И создадим сам буфер (с использованием хелпера SharpDX.Toolkit):

_particlesBuffer = Buffer.Structured.New<GPUParticleData>(graphics, initialParticles, true);

Где initialParticles – массив GPUParticleData с размером в нужное кол-во частиц.

Стоит отметить, что флаги при создании буфера устанавливаются следующие:

BufferFlags.ShaderResource – для возможности обращение к буферу из шейдера
BufferFlags.StructuredBuffer – указывает на принадлежность буфера
BufferFlags.UnorderedAccess – для возможности изменения буфера из шейдера

Создадим буфер размером в 1 000 000 элементов и заполним его случайными элементами:

GPUParticleData[] initialParticles = new GPUParticleData[PARTICLES_COUNT];
for (int i = 0; i < PARTICLES_COUNT; i++)
{
	initialParticles[i].Position = random.NextVector3(new Vector3(-30f, -30f, -30f), new Vector3(30f, 30f, 30f));
}

После чего у нас в памяти GPU будет храниться буфер из 1 000 000 элементов со случайными значениями.

Глава 3. Рендер Point-частиц


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

Создадим два шейдера – Vertex Shader и Pixel Shader.
Для начала, опишем входные данные для шейдеров:

struct Particle // описание структуры на GPU
{
    float3 Position;
    float3 Velocity;
};

StructuredBuffer<Particle> Particles : register(t0); // буфер частиц
cbuffer Params : register(b0) // матрицы вида и проекции
{
	float4x4 View;
	float4x4 Projection;
};

// т.к. вертексов у нас нет, мы можем получить текущий ID вертекса при рисовании без использования Vertex Buffer
struct VertexInput
{
 	uint VertexID : SV_VertexID;
};

struct PixelInput // описывает вертекс на выходе из Vertex Shader
{
	float4 Position : SV_POSITION; 
};

struct PixelOutput // цвет результирующего пикселя
{
    float4 Color : SV_TARGET0;
};

Ну и рассмотрим более подробно шейдеры, для начала, вертексный:

PixelInput DefaultVS(VertexInput input)
{
	PixelInput output = (PixelInput)0;

	Particle particle = Particles[input.VertexID];

	float4 worldPosition = float4(particle.Position, 1);
	float4 viewPosition = mul(worldPosition, View);
	output.Position = mul(viewPosition, Projection);
	return output;
}

В этой стране магии – мы просто читаем из буфера частиц конкретную частицу по текущему VertexID (а он у нас лежит в пределах от 0 до 999999) и используя позицию частицы – проектируем в экранное пространство.

Ну а с Pixel Shader проще простого:

PixelOutput DefaultPS(PixelInput input)
{
	PixelOutput output = (PixelOutput)0;

	output.Color = float4((float3)0.1, 1);
	
	return output;
}

Задаем цвет частицы как float4(0.1, 0.1, 0.1, 1). Почему 0.1? Потому, что частиц у нас миллион, и мы будем использовать Additive Blending.

Зададим буферы и отрисуем геометрию:

graphics.ResetVertexBuffers(); // на всякий случай сбросим буферы

graphics.SetBlendState(_additiveBlendState); // включим Additive Blend State

// и установим наш буфер частиц как SRV (только чтение).
_particlesRender.Parameters["Particles"].SetResource<SharpDX.Direct3D11.ShaderResourceView>(0, _particlesBuffer); 

// матрицы
_particlesRender.Parameters["View"].SetValue(camera.View);
_particlesRender.Parameters["Projection"].SetValue(camera.Projection);

// установим шейдер
_particlesRender.CurrentTechnique.Passes[0].Apply();

// выполним отрисвоку 1000000 частиц в виде точек
graphics.Draw(PrimitiveType.PointList, PARTICLES_COUNT);

Ну и полюбуемся первой победой:



Глава 4: Рендер QuadBillboard-частиц


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

Чуть-чуть расскажу о том, что такое QuadBillboard: это квадрат выполненный из двух треугольников и этот квадрат всегда повернут к камере.

Как создать этот квадрат? Нам нужно придумать алгоритм быстрой генерации подобных квадратов. Давайте посмотрим на кое-что в Vertex Shader. Там у нас есть три пространства при построении SV_Position:

  1. World Space – позиция вертекса в мировых координатах
  2. View Space – позиция вертекса в видовых координатах
  3. Projection Space – позиция вертекса в экранных координатах


View Space как раз то, что нам нужно, ведь эти координаты находятся как раз относительно камеры и плоскость (-1 + p.x, -1 + p.y, p.z) -> (1 + p.x, 1 + p.y, p.z) созданная в этом пространстве всегда будет иметь нормаль, которая направлена на камеру.

Поэтому, кое-что изменим в шейдере:

PixelInput TriangleVS(VertexInput input)
{
	PixelInput output = (PixelInput)0;

	Particle particle = Particles[input.VertexID];

	float4 worldPosition = float4(particle.Position, 1);
	float4 viewPosition = mul(worldPosition, View);
	output.Position = viewPosition;
	output.UV = 0;

	return output;
}

На выход SV_Position мы будем передавать не ProjectionSpace-position, а ViewSpace-position, для того, чтобы создать новые примитивы в Geometry Shader в ViewSpace.

Добавим новую стадию:

// функция изменения вертекса и последующая проекция его в Projection Space
PixelInput _offsetNprojected(PixelInput data, float2 offset, float2 uv)
{
	data.Position.xy += offset;
	data.Position = mul(data.Position, Projection);
	data.UV = uv;

	return data;
}

[maxvertexcount(4)] // результат работы GS – 4 вертекса, которые образуют TriangleStrip
void TriangleGS( point PixelInput input[1], inout TriangleStream<PixelInput> stream )
{
	PixelInput pointOut = input[0];
	
	const float size = 0.1f; // размер конченого квадрата 
	// описание квадрата
	stream.Append( _offsetNprojected(pointOut, float2(-1,-1) * size, float2(0, 0)) );
	stream.Append( _offsetNprojected(pointOut, float2(-1, 1) * size, float2(0, 1)) );
	stream.Append( _offsetNprojected(pointOut, float2( 1,-1) * size, float2(1, 0)) );
	stream.Append( _offsetNprojected(pointOut, float2( 1, 1) * size, float2(1, 1)) );

	// создать TriangleStrip
	stream.RestartStrip();
}

Ну и так, как у нас есть теперь UV – мы можем прочитать текстуру в пиксельном шейдере:
PixelOutput TrianglePS(PixelInput input)
{
	PixelOutput output = (PixelOutput)0;
	float particle = ParticleTexture.Sample(ParticleSampler, input.UV).x * 0.3;	
        output.Color = float4((float3)particle, 1);
	
	return output;
}

Дополнительно установим сэмплер и текстуру частицы для рендера:

_particlesRender.Parameters["ParticleSampler"].SetResource<SamplerState>(_particleSampler);
_particlesRender.Parameters["ParticleTexture"].SetResource<Texture2D>(_particleTexture);

Проверяем, тестируем:



Глава 5: Движение частиц


Теперь, все готово, у нас есть особый буфер в памяти GPU и есть рендер частиц, построенный с помощью Geometry Shader, но подобная система — статична. Можно, конечно, менять позицию на CPU, просто каждый раз читать с GPU данные буфера, менять их, а потом загружать обратно, но о какой GPU Power может идти речь? Такая система не выдержит и 100 000 частиц.

И для работы на GPU с такими буферами можно использовать особый шейдер — Compute Shader. Он находится вне традиционного render-pipeline и может использоваться отдельно.

Что же такое Compute Shader?

Своими словами, вычислительный шейдер (Compute Shader) — это особая стадия конвейера, которая заменяет все традиционные (однако все еще могут использоваться вместе с ним), позволяет выполнять произвольный код с помощь GPU, читать/записывать данные в буферы (в том числе и текстурные). Причем исполнение этого кода происходит так параллельно, как настроит разработчик.

Давайте рассмотрим выполнение самого простого кода:

[numthreads(1, 1, 1)]
void DefaultCS( uint3 DTiD: SV_DispatchThreadID )
{
	// DTiD.xyz - текущий поток
	// ... произвольный код
}

technique ComputeShader
{
	pass DefaultPass
	{
		Profile = 10.0;
		ComputeShader = DefaultCS;
	}
}

В самом начале кода есть поле numthreads, которое указывает кол-во потоков в группе. Пока мы не будем использовать групповые потоки и сделаем так, чтобы на одну группу приходился один поток.
uint3 DTiD.xyz указывает на текущий поток.

Следующий этап — это запуск такого шейдера, производится он следующим образом:

_effect.CurrentTechnique.Passes[0].Apply();
graphics.Dispatch(1, 1, 1);

В методе Dispatch мы указываем — сколько групп потоков у нас должно быть, причем максимальное кол-во каждой размерности ограниченно 65536. И если мы выполним такой код, то код шейдера на GPU выполнится один раз, т.к. у нас 1 группа потоков, в каждой группе 1 поток. Если поставить, например, Dispatch(5, 1, 1) — код шейдера на GPU выполнится пять раз, 5 групп потоков, в каждой группе 1 поток. Если при этом изменить еще и numthreads -> (5, 1, 1), то код выполнится 25 раз, причем в 5 групп потоков, в каждой группе 5 потоков. Более подробно можно рассмотреть, если взглянуть на картинку:



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

В DX10 (именно эту версию CS мы используем, для поддержки DX10 карт) максимальное кол-во потоков на группу потоков — 768, причем во всех трех измерениях. Я создаю 32 * 24 * 1 = 768 потоков в сумме на каждую группу потоков, т.е. наша одна группа способна обработать 768 частиц (1 поток — 1 частица). Далее, необходимо посчитать, сколько нужно групп потоков (с учетом того, что одна группа обработает 768 частиц) для того, чтобы обработать N-ое кол-во частиц.
Посчитать это можно по формуле:

int numGroups = (PARTICLES_COUNT % 768 != 0) ? ((PARTICLES_COUNT / 768) + 1) : (PARTICLES_COUNT / 768);
double secondRoot= System.Math.Pow((double)numGroups, (double)(1.0 / 2.0));
secondRoot= System.Math.Ceiling(secondRoot);
_groupSizeX = _groupSizeY = (int)secondRoot;

После чего — мы можем вызвать Dispatch(_groupSizeX, _groupSizeY, 1), и шейдер будет способен параллельно обработать N-ое кол-во элементов.

Для доступа к конкретному элементу используют формулу:
uint index = groupID.x * THREAD_IN_GROUP_TOTAL + groupID.y * GROUP_COUNT_Y * THREAD_IN_GROUP_TOTAL + groupIndex; 

Далее приведу обновленный код шейдера:

struct Particle
{
    float3 Position;
    float3 Velocity;
};

cbuffer Handler : register(c0)
{
	int GroupDim;
	uint MaxParticles;
	float DeltaTime;
};

RWStructuredBuffer<Particle> Particles : register(u0);

#define THREAD_GROUP_X 32
#define THREAD_GROUP_Y 24
#define THREAD_GROUP_TOTAL 768

[numthreads(THREAD_GROUP_X, THREAD_GROUP_Y, 1)]
void DefaultCS( uint3 groupID : SV_GroupID, uint groupIndex : SV_GroupIndex )
{
	uint index = groupID.x * THREAD_GROUP_TOTAL + groupID.y * GroupDim * THREAD_GROUP_TOTAL + groupIndex; 
	
	[flatten]
	if(index >= MaxParticles)
		return;

	Particle particle = Particles[index];

	float3 position = particle.Position;
	float3 velocity = particle.Velocity;

        // payload

	particle.Position = position + velocity * DeltaTime;
	particle.Velocity = velocity;

	Particles[index] = particle;
}

technique ParticleSolver
{
	pass DefaultPass
	{
		Profile = 10.0;
		ComputeShader = DefaultCS;
	}
}

Тут происходит еще одна магия, мы используем наш буфер частиц как особый ресурс: RWStructuredBuffer, это означает, что мы можем читать и писать в этот буфер.
(!) Необходимое условие для записи — этот буфер должен быть при создании помечен флагом UnorderedAccess.

Ну и финальный этап, мы устанавливаем ресурс для шейдера как UnorderedAccessView наш буфер и вызываем Dispatch:

/* SOLVE PARTICLES */
_particlesSolver.Parameters["GroupDim"].SetValue(_threadGroupSize);
_particlesSolver.Parameters["MaxParticles"].SetValue(PARTICLES_COUNT);
_particlesSolver.Parameters["DeltaTime"].SetValue(deltaTime);

_particlesSolver.Parameters["Particles"].SetResource<SharpDX.Direct3D11.UnorderedAccessView>(0, _particlesBuffer);

_particlesSolver.CurrentTechnique.Passes[0].Apply();

graphics.Dispatch(
	_threadSize,
	_threadSize,
	1);

_particlesSolver.CurrentTechnique.Passes[0].UnApply(false);

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

Давайте что-нибудь сделаем с частицами, напишем простейший солвер:

float3 _calculate(float3 anchor, float3 position)
{
	float3 direction = anchor - position;
	float distance = length(direction);
	direction /= distance;

	return direction * max(0.01, (1 / (distance*distance)));
}

// main
{
...
velocity += _calculate(Attractor, position);
velocity += _calculate(-Attractor, position);
...
}

Attractor зададим в константном буфере.

Компилируем, запускает и любуемся:


Заключение 1


Если говорить о частицах, то ничего не мешает создать полноценную и мощную систему частиц: точки достаточно легко отсортировать (для обеспечения прозрачности), применить технику soft particles при рисовании, а так же учитывать освещение «не светящихся» частиц. Вычислительные шейдеры в основном применяются для создания эффекта Bokeh Blur (тут нужны еще геометрические), для создания Tiled Deferred Renderer, etc. Геометрические шейдеры, например, можно использовать тогда, когда необходимо сгенерировать много геометрии. Самый яркий пример — трава и частицы. К слову, применение GS и CS безграничны и ограничиваются лишь фантазией разработчика.

Заключение 2


Традиционно прикрепляю к посту полный исходный код и демо.
P.S. для запуска демо — нужна видеокарта с поддержкой DX10 и Compute Shader.

Заключение 3


Мне очень приятно, когда люди проявляют интерес к тому, что я пишу. И для меня очень важна реакция на статью, будь она в виде плюса или минуса с конструктивным комментарием. Так я смогу определить — какие темы больше интересны хабрасообществу, а какие нет.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 44

    +1
    А какие аналоги этих типов шейдеров существуют в OpenGL?
      0
      В целом, тоже самое.
      ARB_geometry_shader4
      ARB_compute_shader
        +3
        Было бы неплохо увидеть туториал именно на OpenGL, а для исходных текстов есть GitHub.

        нужна видеокарта с поддержкой DX10

        Что-то мне подсказывает, что одной видеокартой с поддержкой DX тут не обойтись.
          +1
          Работает 61фпс на интегрированном видео в процессоре i7.
        0
        ARB_compute_shader, к сожалению, доступен только для OpenGL 4.3.
        Но в качестве альтернативы можно использовать «Transform feedback». Он доступен с версии 3.0.
          0
          Почему «к сожалению»? На десктопах OpenGL 4.3 сейчас не поддерживает только Intel под линуксом, потому что работает через Mesa.
          В OpenGL ES 3 compute shaders тоже есть, хотя вряд ли кто захочет симулировать 1М частиц на мобильном устройстве.

          А вот OpenCL как раз Nvidia не поддерживает дальше версии 1.1, что значительно ограничевает возможности.
            0
            Сейчас 3 основные десктоп платформы. Винда, Линукс, МакОС.
            Под макам версия GL традиционно отстает. ref support.apple.com/en-us/HT202823
            Текущий максимум под OSX — это GL 4.1

            UPD: если верить табличке www.g-truc.net/post-0702.html#menu — то и 4.1 Apple поддерживает на 83%
              +1
              Оу, прошу прощения. Не думал что на маках с этим проблемы.
              0
              «К сожалению» — потому что ни моя домашняя, ни офисная видеокарты OpenGL 4.3 не поддерживают.
              И таких людей довольно много, чтобы сбрасывать их со счетов.
            0
            Да, ещё для вычислений вполне можно использовать OpenCL. Там есть возможность использовать контекст и буферы OpenGL.
            0
            Спасибо за статью.

            Правда проект просто так нельзя взять и запустить, тем кто не работает с SharpDX:
            Cannot process file '\GPUParticlesSources\GPUParticles\Content\ParticleSolver.fx': Could not load file or assembly 'SharpDX.D3DCompiler,

            Было бы здорово иметь самодостаточный солюшен.

              0
              Поставьте с помощью NuGet (возможно сделать прямо в студии, обновите только сам NuGet — если им не пользуетесь) пакет SharpDX, пакеты тут.
                0
                Мой косяк, не доложил файлы, обновил исходники в статье, теперь должен быть самодостаточным.
                  0
                  Огромное спасибо. Теперь все работает из коробки.
                0
                Извините, но с какого перепугу GeForce 550 Ti вдруг стала бюджетной видеокартой. Начнем с того, что это карта из линейки GTX а не GT, и по умолчанию является игровой. Более того, на момент появления была топовой картой и мечтой каждого игромана. Да, сегодня она по номеру устарела но по прежнему выдает достаточную производительность в играх, так как по сути в новых моделях ничего толкового не появилось.
                  +3
                  А в чём противоречие? 550 Ti — самая дешёвая игровая видеокарта из 500-й линейки. Ниже — офисные, выше — средне-бюджетная 560, более дорогие 570, 580-я и топовая 590-я.
                    +1
                    По видимому у нас с вами разная шкала оценки, мне всегда казалось что бюджетная и оффисная это одно и тоже. Но это мое личное мнение, спасибо за ответ.
                      0
                      Офисные нынче интеграшки интелловские в процессорах. А бюджетными называют обычно игровые дешевые карты, способные тянуть игры ну хоть как-то приемлимо.
                  0
                  Спасибо за статью)
                  На GTX770 полет отличный (скачал исходник, сбилдил). Показывает 61 fps, хотя мне кажется циферка врет немного).
                    0
                    Она не врет, FPS ограничен — 60 кадров в секунду, отключите (поставьте false) в конструкторе класса Logic -> SynchronizeWithVerticalRetrace и IsFixedTimeStep, увидите максимальное кол-во FPS. На GTX 770, думаю, будет около 220.
                      +1
                      Спасибо за ответ.
                      Выдает 440-453
                        0
                        Ну поэкспериментируйте еще и с кол-вом частиц: класс GPUParticlesHandler, константа — PARTICLES_COUNT, достаточно изменить значение — а уж исходя из этого построится нужный буфер на нужное кол-во частиц в этом буфере.
                          0
                          Можно узнать частоты вашей карточки? Просто моя 770 (ядро/память: 1058/7010 МГц) выдает 380-420 :)
                            0
                            Gigabyte GeForce GTX 770
                            AIDA64 показывает:
                            Частота- 1189 МГц
                            Частота памяти — 3505
                            c2n.me/3b9VuYb

                            Не разгонял.
                              0
                              Это все очень неоднозначно, потому что в частицах тормозить начинает уже на филлрейте. Открыли окно на весь экран или подлетели камерой поближе, и минус 50% FPS.
                              Нужно делать динамический LOD-инг чтобы выжать еще больше попугаев.
                        0
                        Не могу скачать демо — пишет превышен лимит скачивания файлов.
                          0
                          Обновил, залил на хостинг.
                          0
                          В полноэкранный режим не переключается по альт-энтер: экран мигает на секунду и все возвращается. Или просто нет такой опции?
                            0
                            Скорее всего такое происходит из-за того, что разрешение окна — 1200x600, а такого полноэкранного режима нет. Я не реализовывал опцию Alt+Enter.
                              +1
                              Эхх, жаль :( А вообще — получилась бы отличнейшая заставка :)
                            +2
                            В статье вовсю Compute шейдера, но в требованиях упоминается только DirectX 10.
                            В статью неплохо было бы добавить, что для тех кто хочет миллионы частиц без DirectX 10 и выше — все прекрасно (практически с идентичной производительностью) делается на DX9. Вместо геометрических шейдеров и structured буфера — инстансинг. Вместо Compute шейдеров — рендер в текстуру.

                            Ну и по поводу шейдеров. Поскольку у нас частицы ориентированы на нас, то data.Position = mul(data.Position, Projection); в геометрическом шейдере не нужен. Так же размер частицы можно посчитать в вертексном, а в геометрическом делать только сдвиг. Оптимизация.

                            p.s. Знаю, что о опечатках принято в личку, но не удержался:
                            const float size = 0.1f; // размер конченого квадрата
                              –1
                              Вместо геометрических шейдеров и structured буфера — инстансинг.

                              … что для тех кто хочет миллионы частиц без DirectX 10 и выше — все прекрасно (практически с идентичной производительностью) делается на DX9

                              Проверьте (касаемо «практические идентичной производительности»)? Сделайте систему на один миллион частиц на DX9 через рендер в текстуру и VFetch (еще и с инстансингом), и потом сравните с CS, GS. Когда уже дадут умереть DX9? :)

                              Ну и по поводу шейдеров. Поскольку у нас частицы ориентированы на нас, то data.Position = mul(data.Position, Projection); в геометрическом шейдере не нужен.

                              Попробуйте сделать сдвиг вертекса во ViewSpace и ProjectionSpace, ну как?

                              Так же размер частицы можно посчитать в вертексном, а в геометрическом делать только сдвиг

                              Вы думаете в копилированном варианте шейдера есть такое понятие — как функции? В геометрическом не считается размер частиц и делается только сдвиг.
                              0
                              Проверьте (касаемо «практические идентичной производительности»)? Сделайте систему на один миллион частиц на DX9 через рендер в текстуру и VFetch (еще и с инстансингом), и потом сравните с CS, GS. Когда уже дадут умереть DX9? :)
                              Спасибо, проверял уже в те времена, когда производительность GF550 Ti была чем-то очень крутым: habrahabr.ru/post/151821/
                              Все свободно крутилось на очень дохлых встроенных видеокартах. А в моем случае частицы еще и создавались динамически, и кол-во частиц в сцене могло скакать очень сильно. CS тогда еще в зачатках не было, а GS только только анонсировали. Вон выше в комментариях уже обсуждали, что DX11 возможности сегодня далеко не везде есть. Если можно обойтись без них — почему бы это не сделать (особенно это касается OpenGL)?

                              Попробуйте сделать сдвиг вертекса во ViewSpace и ProjectionSpace, ну как?
                              Легко. Вам шейдер запилить в качестве доказательства? Мы можем такое делать, потому что частица всегда ориентирована на нас и перспективных искажений нет => интерполятор даст идентичные результаты.

                              Вы думаете в копилированном варианте шейдера есть такое понятие — как функции? В геометрическом не считается размер частиц и делается только сдвиг.
                              Вы size не считате, потому что вы 4 раза умножаете на матрицу проекции. Я же говорю, что лучше умножать на матрицу проекции в вершинном, там же считать Size и передавать его в геометрический.

                              upd. Ответ на комментарий выше habrahabr.ru/post/248755/#comment_8243109
                                0
                                С радостью увижу шейдер (а еще лучше скомпилированный экзешник) с инстансингом (quadbillboard) и vfetch миллиона партиклов с FPS на GeForce 550 Ti в 110 (при реализации с FL9.3 у меня вышло 59 FPS).
                                А так же, сделайте геометрический шейдер с вашей оптимизацией, я посмотрю как это будет работать (скачайте исходник и сделайте).

                                Потом:

                                Вон выше в комментариях уже обсуждали, что DX11 возможности сегодня далеко не везде есть. Если можно обойтись без них — почему бы это не сделать (особенно это касается OpenGL)?


                                Так и пишите на ассемблере, у всех будет работать, в чем проблема? У DX10+ есть режим совместимости с feature level 9.1-9.3. DX10 карта сейчас есть практически у всех, DX9 (и всякие мертвые WinXP) — поддерживать смысла нет.

                                И да, если вы пишите игры для «офисных» компьютеров — использовать DX смысла вообще нет, используйте OpenGL.
                                  0
                                  А так же, сделайте геометрический шейдер с вашей оптимизацией, я посмотрю как это будет работать (скачайте исходник и сделайте).
                                  К сожалению вы не залили проект на git, сделал бы пуллреквест… так что шейдер под спойлером:
                                  ParticleRender.fx
                                  struct Particle
                                  {
                                      float3 Position;
                                      float3 Velocity;
                                  };
                                  
                                  StructuredBuffer<Particle> Particles : register(t0);
                                  
                                  Texture2D<float> ParticleTexture : register(t1);
                                  SamplerState ParticleSampler : register(s1);
                                  
                                  cbuffer Params : register(b0)
                                  {
                                  	float4x4 ViewProjection;
                                  	float4x4 Projection;
                                  };
                                  
                                  struct VertexInput
                                  {
                                      uint VertexID : SV_VertexID;
                                  };
                                  
                                  struct GeometryInput
                                  {
                                  	float4 Position : SV_POSITION;
                                  	float3 PositionTWS : TEXCOORD0;
                                  	float2 Size : SIZE;
                                  };
                                  
                                  struct PixelInput
                                  {
                                  	float4 Position : SV_POSITION;
                                  	float2 UV : TEXCOORD0;
                                  	float3 PositionTWS : TEXCOORD1;
                                  };
                                  
                                  struct PixelOutput
                                  {
                                      float4 Color : SV_TARGET0;
                                  };
                                  
                                  GeometryInput TriangleVS(VertexInput input)
                                  {
                                  	GeometryInput output = (GeometryInput)0;
                                  
                                  	Particle particle = Particles[input.VertexID];
                                  
                                  	output.Position = mul(float4(particle.Position, 1), ViewProjection);
                                  	float size = 0.1;
                                  	output.Size = size * float2(Projection[0][0], Projection[1][1]);
                                  	float speedLength = length(particle.Velocity);
                                  	//float magnitude = saturate(length(worldPosition.xyz) * 0.1);
                                  
                                  	output.PositionTWS = float3(lerp(float3(0.1, 0.5, 1.0), float3(1.0, 0.5, 0.1), speedLength * 0.1));
                                   
                                  	return output;
                                  }
                                  
                                  void FillQuadVertex(inout PixelInput data, in float2 NewXY, in float2 UV)
                                  {
                                  	data.Position.xy = NewXY;
                                  	data.UV = UV;
                                  }
                                  
                                  [maxvertexcount(4)]
                                  void TriangleGS( point GeometryInput input[1], inout TriangleStream<PixelInput> stream )
                                  {
                                  	PixelInput Out;
                                  	Out.Position.zw = input[0].Position.zw;
                                      Out.PositionTWS = input[0].PositionTWS;
                                  
                                  	FillQuadVertex(Out, input[0].Position.xy + float2(-1,-1) * input[0].Size, float2(0, 0)); stream.Append(Out);
                                  	FillQuadVertex(Out, input[0].Position.xy + float2(-1, 1) * input[0].Size, float2(0, 1)); stream.Append(Out);
                                  	FillQuadVertex(Out, input[0].Position.xy + float2( 1,-1) * input[0].Size, float2(1, 0)); stream.Append(Out);
                                  	FillQuadVertex(Out, input[0].Position.xy + float2( 1, 1) * input[0].Size, float2(1, 1)); stream.Append(Out);
                                  
                                  	stream.RestartStrip();
                                  }
                                  
                                  PixelOutput TrianglePS(PixelInput input)
                                  {
                                  	PixelOutput output = (PixelOutput)0;
                                  	float particle = ParticleTexture.Sample(ParticleSampler, input.UV).x * 0.3;
                                  	float3 speedColor = input.PositionTWS;
                                  	output.Color = float4(speedColor * particle, 1);
                                  	return output;
                                  }
                                  
                                  technique ParticleRender
                                  {
                                  	pass DefaultPass
                                  	{
                                  		Profile = 10.0;
                                  		VertexShader = TriangleVS;
                                  		GeometryShader = TriangleGS;
                                  		PixelShader = TrianglePS;
                                  	}
                                  }

                                  Убрал умножение на матрицу проекции в геометрическом шейдере. Поскольку все умножения теперь в вертексном, то делаю одно умножение на ViewProjection. Поэтому не забудьте изменить в GPUParticlesHandler.cs установку матрицы вида на _particlesRender.Parameters[«ViewProjection»].SetValue(camera.ViewProjection);
                                  Но как я и говорил, прирост фпс после данной «оптимизации» сложно заметить, потому что узко на филлрейте.

                                  С радостью увижу шейдер (а еще лучше скомпилированный экзешник) с инстансингом (quadbillboard) и vfetch миллиона партиклов с FPS на GeForce 550 Ti в 110.
                                  Может быть как-нибудь позже, но не обещаю. Времени требует много.

                                  Так и пишите на ассемблере, у всех будет работать, в чем проблема? У DX10+ есть режим совместимости с feature level 9.1-9.3. DX10 карта сейчас есть практически у всех, DX9 (и всякие мертвые WinXP) — поддерживать смысла нет.
                                  Я сейчас говорю не столько о самом DX9, сколько об аппаратных возможностях. Компьют и геометрические шейдера с feature level 9 не взлетят.

                                  И да, если вы пишите игры для «офисных» компьютеров — использовать DX смысла вообще нет, используйте OpenGL.
                                  Вот уж не могу согласится. «Офисные» компьютеры часто с дефолтными драйверами после установки Win, и с OGL там вообще печаль. Вон даже хром «эмулирует» OGL через DirectX.
                                    0
                                    С радостью увижу шейдер (а еще лучше скомпилированный экзешник) с инстансингом (quadbillboard) и vfetch миллиона партиклов с FPS на GeForce 550 Ti в 110 (при реализации с FL9.3 у меня вышло 59 FPS).

                                    mrdoob.com/lab/javascript/webgl/particles/particles_zz85_2m.html

                                    И т.п. — гугл в помощь. 59 фпс — очень похоже на vsync (в webgl скорее всего тоже около 60 фпс покажет).
                                      0
                                      Я знаю что такое vsynch (я же ведь не идиот, замерять производительность с включенным vsynch), только… что я должен был увидеть по вышей ссылке? Это обычный point-render с vfetch.
                                  0
                                  Убрал умножение на матрицу проекции в геометрическом шейдере. Поскольку все умножения теперь в вертексном, то делаю одно умножение на ViewProjection. Поэтому не забудьте изменить в GPUParticlesHandler.cs установку матрицы вида на _particlesRender.Parameters[«ViewProjection»].SetValue(camera.ViewProjection);
                                  Но как я и говорил, прирост фпс после данной «оптимизации» сложно заметить, потому что узко на филлрейте.


                                  10 000 000 частиц, фуллскрин, разрешение 1920x1080, камера на всю сцену:
                                  с «оптимизацией» — FPS: 9, Draw time: прыгает в районе 0.02s с максимальным отклонением 0.0052s.
                                  без «оптимизации» — FPS: 9, Draw time: прыгает в районе 0.02s с максимальным отклонением 0.0049s.

                                  Оптимизация говорите?

                                  Может быть как-нибудь позже, но не обещаю. Времени требует много.

                                  Я же говорю, я делал сначала на Feature Level 9.3, через две текстуры R32, G32, B32 формата и временного буфера с таким же форматом, через RTсчитал частицы, с обычным (!) point-рендером, было 59 fps. В то же время, CS + GS (конечный продукт — quadbillboard) — 110 fps, а вы говорите такой же FPS: проиграл по FPS, качеству, еще и памяти больше нужно.

                                  Я сейчас говорю не столько о самом DX9, сколько об аппаратных возможностях. Компьют и геометрические шейдера с feature level 9 не взлетят.


                                  Так не используйте FL9.x, это же по сути эмулятор фиксированного мамонта. DX10 карта сейчас (кто интересует играми хотя-бы чуть-чуть) есть у всех, а Windows XP как таргет-платформу рассматривать вообще не стоит, через год вы о ней уже забудете.
                                    0
                                    Как всегда — промазал.
                                      0
                                      10 000 000 частиц, фуллскрин, разрешение 1920x1080, камера на всю сцену:
                                      с «оптимизацией» — FPS: 9, Draw time: прыгает в районе 0.02s с максимальным отклонением 0.0052s.
                                      без «оптимизации» — FPS: 9, Draw time: прыгает в районе 0.02s с максимальным отклонением 0.0049s.

                                      Оптимизация говорите?
                                      Я же говорю, узко на филлрейте. Упирается в запись в память. Если оптимзировать запись в память, то 10кк частиц можно выжать с гораздо большим фпс, и эта оптимизация уже будет иметь смысл. А сейчас чтобы заметить эту оптимизацию — камеру отворачивайте, чтобы частицы в кадр не попадали, и отключите резолв.
                                        0
                                        30 000 000 частиц, отключенный солвер, камера отвернута, FPS 5.4 в обоих случаях, Draw time одинаковые (шумные отклонения). Не могу заметить оптимизацию.
                                      0
                                      Решил делать аналогичную партикловую систему и сразу же уперся в проблему — как убивать и добавлять частицы? Вроде ясно, что нужно неактивные частицы просто загонять в конец буфера, меняя их с живыми, но как это мапить на многопоточность не понятно… Может у вас уже есть готовый способ?
                                        0
                                        Сначала гуглить, потом спрашивать :)
                                        http://twvideo01.ubm-us.net/o1/vault/GDC2014/Presentations/Gareth_Thomas_Compute-based_GPU_Particle.pdf

                                      Only users with full accounts can post comments. Log in, please.