Почему переключение с Mersenne Twister на другие PRNG в Gradient Noise Generator дает плохие результаты?

Я пытался создать обобщенный генератор градиентного шума (который не использует хэш-метод для получения градиентов). Код ниже:

class GradientNoise {
std::uint64_t m_seed;
std::uniform_int_distribution<std::uint8_t> distribution;
const std::array<glm::vec2, 4> vector_choice = {glm::vec2(1.0, 1.0), glm::vec2(-1.0, 1.0), glm::vec2(1.0, -1.0),
glm::vec2(-1.0, -1.0)};

public:
GradientNoise(uint64_t seed) {
m_seed = seed;
distribution = std::uniform_int_distribution<std::uint8_t>(0, 3);
}

// 0 -> 1
// just passes the value through, origionally was perlin noise activation
double nonLinearActivationFunction(double value) {
//return value * value * value * (value * (value * 6.0 - 15.0) + 10.0);
return value;
}

// 0 -> 1
//cosine interpolation
double interpolate(double a, double b, double t) {
double mu2 = (1 - cos(t * M_PI)) / 2;
return (a * (1 - mu2) + b * mu2);
}

double noise(double x, double y) {
std::mt19937_64 rng;
//first get the bottom left corner associated
// with these coordinates
int corner_x = std::floor(x);
int corner_y = std::floor(y);

// then get the respective distance from that corner
double dist_x = x - corner_x;
double dist_y = y - corner_y;

double corner_0_contrib; // bottom left
double corner_1_contrib; // top left
double corner_2_contrib; // top right
double corner_3_contrib; // bottom right

std::uint64_t s1 = ((std::uint64_t(corner_x) << 32) + std::uint64_t(corner_y) + m_seed);
std::uint64_t s2 = ((std::uint64_t(corner_x) << 32) + std::uint64_t(corner_y + 1) + m_seed);
std::uint64_t s3 = ((std::uint64_t(corner_x + 1) << 32) + std::uint64_t(corner_y + 1) + m_seed);
std::uint64_t s4 = ((std::uint64_t(corner_x + 1) << 32) + std::uint64_t(corner_y) + m_seed);// each xy pair turns into distance vector from respective corner, corner zero is our starting corner (bottom
// left)
rng.seed(s1);
corner_0_contrib = glm::dot(vector_choice[distribution(rng)], {dist_x, dist_y});

rng.seed(s2);
corner_1_contrib = glm::dot(vector_choice[distribution(rng)], {dist_x, dist_y - 1});rng.seed(s3);
corner_2_contrib = glm::dot(vector_choice[distribution(rng)], {dist_x - 1, dist_y - 1});rng.seed(s4);
corner_3_contrib = glm::dot(vector_choice[distribution(rng)], {dist_x - 1, dist_y});double u = nonLinearActivationFunction(dist_x);
double v = nonLinearActivationFunction(dist_y);double x_bottom = interpolate(corner_0_contrib, corner_3_contrib, u);
double x_top = interpolate(corner_1_contrib, corner_2_contrib, u);
double total_xy = interpolate(x_bottom, x_top, v);
return total_xy;
}
};

Затем я генерирую текстуру OpenGL для отображения следующим образом:

int width = 1024;
int height = 1024;
unsigned char *temp_texture = new unsigned char[width*height * 4];
double octaves[5] = {2,4,8,16,32};

for( int i = 0; i < height; i++){
for(int j = 0; j < width; j++){
double d_noise = 0;
d_noise += temp_1.noise(j/octaves[0], i/octaves[0]);
d_noise += temp_1.noise(j/octaves[1], i/octaves[1]);
d_noise += temp_1.noise(j/octaves[2], i/octaves[2]);
d_noise += temp_1.noise(j/octaves[3], i/octaves[3]);
d_noise += temp_1.noise(j/octaves[4], i/octaves[4]);
d_noise/=5;
uint8_t noise = static_cast<uint8_t>(((d_noise * 128.0) + 128.0));
temp_texture[j*4 + (i * width * 4) + 0] = (noise);
temp_texture[j*4 + (i * width * 4) + 1] = (noise);
temp_texture[j*4 + (i * width * 4) + 2] = (noise);
temp_texture[j*4 + (i * width * 4) + 3] = (255);
}
}

Которые дают хорошие результаты:

введите описание изображения здесь

Но gprof говорит мне, что твистер Мерсенна занимает 62,4% моего времени и растет с большими текстурами. Ничто иное не занимает столько времени у человека. Несмотря на то, что твистер Mersenne работает быстро после инициализации, тот факт, что я инициализирую его каждый раз, когда использую его, кажется, делает его довольно медленным.

Эта инициализация на 100% необходима для того, чтобы убедиться, что одинаковые x и y генерируют один и тот же градиент в каждой целочисленной точке (поэтому вам нужна либо хеш-функция, либо каждый раз заполнять RNG).

Я попытался изменить PRNG и на линейный конгруэнтный генератор, и на Xorshiftplus, и хотя оба работали на несколько порядков быстрее, они дали странные результаты:

LCG (один раз, затем работает 5 раз перед использованием)
введите описание изображения здесь

введите описание изображения здесь

Xorshiftplus

После одной итерации
введите описание изображения здесь

После 10000 итераций.
введите описание изображения здесь

Я пробовал:

Запуск генератора несколько раз перед использованием выходных данных, это приводит к медленному выполнению или просто другим артефактам.

Используя выходные данные двух последовательных прогонов после начального заполнения, снова запустите PRNG и используйте значение после подопечных. Нет разницы в результате.

Что происходит? Что я могу сделать, чтобы получить более быстрые результаты того же качества, что и твистер Мерсенна?

ОК, БОЛЬШОЕ ОБНОВЛЕНИЕ:

Я не знаю, почему это работает, я знаю, что это как-то связано с использованным простым числом, но после небольшой возни кажется, что работает следующее:

Шаг 1, включите значения x и y в качестве начальных значений отдельно (и включите в них некоторое другое значение смещения или дополнительное начальное значение, это число должно быть простым / нетривиальным фактором)

Шаг 2. Используйте эти два начальных результата для заполнения генератора снова вернуться в функцию (так, как сказал Геза, семена были плохие)

Шаг 3, при получении результата вместо того, чтобы по модулю пытаться получить количество предметов (4), или & 3, по модулю результата на простое число первый затем применить & 3. Я не уверен, имеет ли значение праймер, являющийся мерсенновым, или нет.

Вот результат с использованием Prime = 257 и xorshiftplus! (обратите внимание, что я использовал 2048 на 2048 для этого, остальные были 256 на 256)

введите описание изображения здесь

5

Решение

Известно, что LCG не подходит для ваших целей.

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

Тем не менее, я рекомендую использовать целочисленный хэш. Например, один из Страница Боба.

Вот результат первого хэша этой страницы, он выглядит хорошо для меня, и это быстро (я думаю, что это намного быстрее, чем Mersenne Twister):
введите описание изображения здесь

Вот код, который я написал, чтобы сгенерировать это:

#include <cmath>
#include <stdio.h>

unsigned int hash(unsigned int a) {
a = (a ^ 61) ^ (a >> 16);
a = a + (a << 3);
a = a ^ (a >> 4);
a = a * 0x27d4eb2d;
a = a ^ (a >> 15);
return a;
}

unsigned int ivalue(int x, int y) {
return hash(y<<16|x)&0xff;
}

float smooth(float x) {
return 6*x*x*x*x*x - 15*x*x*x*x + 10*x*x*x;
}

float value(float x, float y) {
int ix = floor(x);
int iy = floor(y);
float fx = smooth(x-ix);
float fy = smooth(y-iy);

int v00 = ivalue(iy+0, ix+0);
int v01 = ivalue(iy+0, ix+1);
int v10 = ivalue(iy+1, ix+0);
int v11 = ivalue(iy+1, ix+1);
float v0 = v00*(1-fx) + v01*fx;
float v1 = v10*(1-fx) + v11*fx;
return v0*(1-fy) + v1*fy;
}

unsigned char pic[1024*1024];

int main() {
for (int y=0; y<1024; y++) {
for (int x=0; x<1024; x++) {
float v = 0;

for (int o=0; o<=9; o++) {
v += value(x/64.0f*(1<<o), y/64.0f*(1<<o))/(1<<o);
}

int r = rint(v*0.5f);

pic[y*1024+x] = r;
}
}

FILE *f = fopen("x.pnm", "wb");
fprintf(f, "P5\n1024 1024\n255\n");
fwrite(pic, 1, 1024*1024, f);
fclose(f);
}

Если вы хотите понять, как работает хеш-функция (или еще лучше, какие свойства есть у хорошего хеша), посмотрите, например, страницу Боба. этот.

3

Другие решения

Вы (по незнанию?) Реализовали визуализацию неслучайных паттернов PRNG. Это выглядит очень круто!

За исключением Mersenne Twister, все ваши протестированные PRNG не подходят для ваших целей. Поскольку я сам не проводил дальнейших испытаний, я могу только предложить попробовать и измерить дальнейшие PRNG.

2

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

Точно так же, чтобы получить длительный период от Xorshift PRNGs.

Вы заметили, что некоторые PRNG дают хорошие процедурные результаты генерации, а другие нет. Чтобы изолировать причину, я бы выделил весь материал & изучите выход PRNG напрямую. Простой способ визуализации данных состоит в создании изображения в оттенках серого, где каждое значение пикселя является (возможно, масштабированным) случайным значением. Для изображений, основанных на изображениях, я считаю, что это простой способ найти вещи, которые могут привести к визуальным артефактам. Любые артефакты, которые вы видите с этим, могут вызвать проблемы с вашей продукцией.

Другой вариант — попробовать что-то вроде Diehard тесты. Если вышеупомянутый тест изображения не выявил каких-либо проблем, я мог бы использовать это, просто чтобы убедиться, что мои методы PRNG заслуживают доверия.

1

Обратите внимание, что ваш код запускает PRNG, а затем генерирует одно случайное число из PRNG. Причина неслучайности в xorshift128+ что вы обнаружили, что xorshift128+ просто добавляет две половинки семени (и использует результат мод 264 как сгенерированное число) перед изменением его состояния (просмотрите его исходный код). Это сильно отличает PRNG от хеш-функции.

1

То, что вы видите, — это практическая демонстрация качества PRNG. Mersenne Twister — один из лучших PRNG с хорошей производительностью, он проходит ЖИВУЧИ тесты. Нужно знать, что генерация случайных чисел не является легкой вычислительной задачей, поэтому поиск лучшей производительности неизбежно приведет к низкому качеству. Известно, что LCG является самым простым и худшим из когда-либо разработанных PRNG, и он четко показывает двумерную корреляцию, как на вашей картинке. Качество генераторов Xorshift во многом зависит от разрядности и параметров. Они определенно хуже, чем Mersenne Twister, но некоторые (xorshift128 +) могут работать достаточно хорошо, чтобы пройти BigCrush батарея testu01 тесты.

Другими словами, если вы проводите важный численный эксперимент по физическому моделированию, вам лучше продолжать использовать Mersenne Twister, который, как известно, является хорошим компромиссом между скоростью и качеством, и он поставляется во многих стандартных библиотеках. В менее важном случае вы можете попробовать использовать генератор xorshift128 +. Для получения окончательных результатов вам необходимо использовать PRNG криптографического качества (ни один из упомянутых здесь не может быть использован в криптографических целях).

0
По вопросам рекламы [email protected]