Простые эксперименты по процедурной генерации

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

Но, к сожалению, ничего занимательного в голову не приходило (как ни странно, фантазия быстро закончилась), и тут я вспомнил о том, что платы с программируемой логикой больше всего подходят под цифровую обработку сигналов, таких как аудио-данные или цифровые образы изображений… Эта мысль стала прародительницей идеи о процедурной генерации, и первоначально, я планировал показать две различных по своему типу реализации и сравнить их по расходам ресурсов и времени на разработку, но как всегда бывает, реальное воплощение сильно отличается от планировавшегося изначально…

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

Теперь, когда вы имеете некоторое представление о том, что нас заинтересовало, мы можем перейти к самому интересному — экспериментам.

Для начала, я бы рекомендовал ознакомится с англоязычным материалом по этому адресу, который объясняет как с помощью простой формулы, содержащей умножения и побитовые операции, получить весьма нехарактерный и теплый ламповый «8 битный» тру-металл. Авторы данного материала используют для экспериментов язык программирования C и операционную систему на базе Linux (пользователи Windows, извините, у вас этот номер не пройдет — слишком уж все заточено под другую платформу), и если вы хотите краткое объяснение, того как это работает, то я могу сказать примерно следующее: в небольшой программе на C происходит заполнение линейного сдвигового регистра с обратной связью и вывод данных с него в звуковое устройство, которое и осуществляет перекодировку цифрового потока во вполне ощущаемый «металлический звук».

Итак, реализация процедурной генерации музыки линейным сдвиговым регистром с обратной связью на D выглядит так:

import std.stdio;

void main()
{
	 for (int t = 0;; t++)
	 {
	 	write(cast(char) (t*(((t>>12)|(t>>8))&(63&(t>>4)))));
	 }	
}

Чтобы использовать эту небольшую программку, необходимо ее откомпилировать с помощью dmd, а после этого применить такую команду:

./music > /dev/dsp

(где music — это файл с откомпилированной программой)
Если указанная выше команда не дала результата, то вам стоит применить несколько иное указание для терминала:

./music | aplay

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

Данная программа на D, почти точно следует описанному в уже упоминавшемся выше материалу, и даже использует ту же самую формулу (которую впрочем вы можете менять по своему усмотрению, получая все более интересные и разнообразные звуки), но содержит иное приведение типа (иное оно только по сути — так как несколько отлично от варианта в C работает).

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

Для проведения экспериментов я создал простую процедуру, которая генерирует файл с изображением формата PPM P6 и принимает несколько параметров: функцию, которая задает необходимую формулу для регистра сдвига, имя файла, длину и ширину картинки:

auto proceduralPPM(alias func)(string imageName, size_t width, size_t height) 
{
	import std.algorithm;
	import std.range;
	import std.string;

	File file;
	file.open(imageName, "w");
	file.write(
		format(
			"P6 %d %d 255\n %s",
			width,
			height,
			iota(0, 3 * width * height)
								.map!(a => func(a))
								.map!(a => [cast(char) a])
								.join
		)
	);					
}

Работает процедура так: определяем сначала некоторую функцию, которая выдает и принимает тип ulong, и которая содержит умножение и побитовые операции. Эта функция должна быть определена вне процедуры proceduralPPM и должна быть определена согласно описанной перед этим сигнатурой, а также она обязательно должна быть известной на этапе компиляции. Просле этого, функция-формула регистра подается в качестве шаблонного аргумента в proceduralPPM, которая открывает обычный (не бинарный!) текстовой файл с уже заданным именем, и используя упрощенное описание формата P6, производит запись отформатированной информации об изображении в файл с помощью format и метода write объекта file. Эта информация генерируется с помощью алгоритма iota, который генерирует набор чисел от 0 до утроенного произведения длины изображения на его ширину (т.к на каждый пиксель изображения требуется три байта данных, соответственно нотации цвета в RGB) и который «по цепочке вызовов» передает дальше полученный массив с числам на обработку. Обработка в данном случае представляет собой применение формулы регистра к каждому значению массива (первый map), последующий перевод полученных значений в набор диапазонов (map!(a => [cast(char) a])), состоящих из одного символа и склеивание набора диапазонов в единую строку (join), которая будет передана в format и записана в файл данных PPM.

Провести испытания можно создав простую программку, которая в командной строке примет два аргумента — длину и ширину изображения и сгенерирует два изображения, которые будут соответствовать двум разным формулам:

import std.conv;
import std.stdio;

auto proceduralPPM(alias func)(string imageName, size_t width, size_t height) 
{
	import std.algorithm;
	import std.range;
	import std.string;

	File file;
	file.open(imageName, "w");
	file.write(
		format(
			"P6 %d %d 255\n %s",
			width,
			height,
			iota(0, 3 * width * height)
								.map!(a => func(a))
								.map!(a => [cast(char) a])
								.join
		)
	);					
}

void main(string[] args)
{	
	auto width = to!int(args[1]);
	auto height = to!int(args[2]);
	
	ulong procedure1(ulong a)
	{
		return a * (((a >> 12) | (a >> 5)) & (51 & (a >> 4)));
	}

	ulong procedure2(ulong a)
	{
		return a * (((a >> 8) | (a >> 9)) & (170 & (a >> 10)));
	}

	proceduralPPM!procedure1("image.ppm", width, height);
	proceduralPPM!procedure2("image2.ppm", width, height);
}

Результатом будут две картинки, для удобства размещения в блоге преобразованные в PNG:

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

Помните, в начале статьи я упоминал про реализацию данной идеи на отладочной плате FPGA ?

Так вот, у меня с этим ничего не получилось, и отмучавшись в течении пары свободных дней с платой iceStick и «пищалкой», я не добился результатов, в отличие от этих ребят, которым удалось приручить FPGA…

Добавить комментарий