Как стать автором
Обновить

Вычисления на видеокарте, руководство, лёгкий уровень

Время на прочтение3 мин
Количество просмотров35K
Это руководство поясняет работу простейшей программы, производящей вычисления на GPU. Вот ссылка на проект Юнити этой программы:

ссылка на файл проекта .unitypackage

Она рисует фрактал Мандельброта.

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

Шейдер, который рисует фрактал, написан на языке HLSL. Ниже приведён его текст. Я кратко прокомментировал значимые строки, а развёрнутые объяснения будут ниже.

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

RWTexture2D<float4> textureOut;						// это текстура, в которую мы будем записывать пиксели
RWStructuredBuffer<double> rect;					// это границы области в пространстве фрактала, которую мы визуализируем
RWStructuredBuffer<float4> colors;					// а это гамма цветов, которую мы подготовили на стороне CPU и передали в видеопамять

#pragma kernel pixelCalc							// тут мы объявили кернел, по этому имени мы сможем его выполнить со стороны CPU
[numthreads(32,32,1)]								// эта директива определяет количество потоков, в которыз выполнится этот кернел
void pixelCalc (uint3 id : SV_DispatchThreadID){	// тут мы задаём код кернела. Параметр id хранит индекс потока, который используется для адресации данных
	float k = 0.0009765625;							// это просто множитель для проекции пространства 1024х1024 текстуры на маленькую область 2х2 пространства фрактала
	double dx, dy;
	double p, q;
	double x, y, xnew, ynew, d = 0;					// использованы переменные двойной точности, чтобы отдалить столкновение с пределом точности при продвижении вглубь фрактала
	uint itn = 0;
	dx = rect[2] - rect[0];
	dy = rect[3] - rect[1];
	p = rect[0] + ((int)id.x) * k * dx;
	q = rect[1] + ((int)id.y) * k * dy;
	x = p;
	y = q;
	while (itn < 255 && d < 4){						// собственно суть фрактала: в этом цикле вычисляется число шагов, за которые точка покидает пространство 2x2
		xnew = x * x - y * y + p;
		ynew = 2 * x * y + q;
		x = xnew;
		y = ynew;
		d = x * x + y * y;
		itn++;
	}
	textureOut[id.xy] = colors[itn];				// вот так мы записываем пиксель цвета: пиксель текстуры определяется индексом, а индекс цвета - числом шагов
}

Внимательный читатель скажет: автор, поясни! Размер текстуры — 1024х1024, а количество потоков — 32х32. Как же параметр id.xy адресует все пиксели текстуры?
Внимательный, но неопытный в вопросах вычислений на GPU читатель перебьёт: позвольте! А откуда следует, что количество потоков 32x32? И как понимать «id.xy»?

Второму я отвечу так: директива [numthreads(32,32,1)] говорит, что у нас 32х32х1 потоков. При этом, потоки образуют трёхмерную сетку, потому что параметр id принимает значения в виде координат пространства 32x32x1. Диапазон значений id.x [0, 31], диапазон значений id.y [0, 31], а id.z равен 0. А id.xy — это краткая запись uint2(id.x, id.y)

Именно 32x32 потоков у нас было бы (этой я уже отвечаю первому внимательному читателю), если бы мы вызвали этот кернел со стороны CPU командой

ComputeShader.Dispatch(kernelIndex, 1, 1, 1)

Видите эти три единицы? Это то же самое, что цифры в директиве [numthreads(32,32,1)], они умножаются друг с другом.

Если бы мы запустили шейдер вот с такими параметрами:

ComputeShader.Dispatch(kernelIndex, 2, 4, 1)

То по оси x у нас было бы 32 * 2 = 64, по оси у 32 * 4 = 128, то есть всего — 64х128 потоков. Параметры просто перемножаются по каждой оси.

Но нашем случае кернел запущен так:

ComputeShader.Dispatch(kernelIndex, 32, 32, 1)

Что даёт нам в итоге 1024х1024 потока. И значит, индекс id.xy будет принимать значения, покрывающие всё пространство текстуры 1024х1024

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

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

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

Объявляем переменные: шейдер, буффер и текстуру

ComputeShader _shader
RenderTexture outputTexture
ComputeBuffer colorsBuffer

Инициализируем текстуру, не забыв включить enableRandomWrite

outputTexture = new RenderTexture(1024, 1024, 32);
outputTexture.enableRandomWrite = true;
outputTexture.Create();

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

colorsBuffer = new ComputeBuffer(colorArray.Length, 4 * 4);
colorsBuffer.SetData(colorArray);

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

_shader = Resources.Load<ComputeShader>("csFractal");
kiCalc = _shader.FindKernel("pixelCalc");
_shader.SetBuffer(kiCalc, "colors", colorsBuffer);
_shader.SetTexture(kiCalc, "textureOut", outputTexture);

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

_shader.Dispatch(kiCalc, 32, 32, 1);

После выполнения этой команды текстура заполняется цветами, которые мы сразу видим, потому что текстура RenderTexture использована в качестве mainTexture для компонента Image, на который смотрит камера.
Теги:
Хабы:
+30
Комментарии12

Публикации

Истории

Работа

Ближайшие события