Шум Перлина на графическом процессоре Часть 2

Процедурное текстурирование

Так как теперь Шум Перлина работает у нас и в вершинном шейдере, и в пиксельном, то можно их совместить. Белая сфера не так впечатляет, так что воспользуемся Шумом Перлина в пиксельном шейдере для создания текстур, чтобы сделать пример поинтереснее.


Для начала нужно создать несколько простых двухцветных градиентных текстур. Поскольку их программная генерация не вызывает затруднений, Я решил создать 4 градиентных текстуры 128х1 с помощью редактора изображений. Они находятся в папке Content проекта PerlinCombined. Их загружают в С# также, как и любую другую XNA текстуру.

Теперь нужно изменить шейдер, добавив переменные под эти градиенты:

texture sandscale; 
sampler sandscaleSampler = sampler_state 
{ 
 texture = ; 
 AddressU = Wrap; 
 AddressV = Wrap; 
 MAGFILTER = POINT; 
 MINFILTER = POINT; 
 MIPFILTER = NONE; 
}; 

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

struct VertexShaderOutput 
{ 
 float4 Position : POSITION0; 
 float4 TexWeights : TEXCOORD0; 
}; 

В вершинный шейдер нужно добавить установку весов этих текстур. Веса для каждой из четырхе текстур сохраняются в полях X, Y, Z и W. Эти веса вычислены в соответствии с посчитанным fBm значением высоты. Это позволяет пиксельному шейдеру выполнять гладкий переход между различными градиентами.

// calc texture weights that are based upon the height value 
output.TexWeights = 0; 
output.TexWeights.x = saturate( 1.0f - abs(heightValue - 0.0)); 
output.TexWeights.y = saturate( 1.0f - abs(heightValue - 0.3)); 
output.TexWeights.z = saturate( 1.0f - abs(heightValue - 0.6)); 
output.TexWeights.w = saturate( 1.0f - abs(heightValue - 0.9)); 
float totalWeight = output.TexWeights.x + 
 output.TexWeights.y + 
 output.TexWeights.z + 
 output.TexWeights.w; 
output.TexWeights /= totalWeight; 

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

// calc 4 colors using Perlin noise lookups into gradient textures 
// then blend those colors together based on the terrain height 
float noiseResult1 = turbulence(input.Normal*10, 2); 
float4 noiseColor1 = tex1D(sandscaleSampler, saturate(noiseResult1)); 
float noiseResult2 = turbulence(input.Normal*10, 2); 
float4 noiseColor2 = tex1D(grassscaleSampler, saturate(noiseResult2)); 
float noiseResult3 = turbulence(input.Normal*10, 2); 
float4 noiseColor3 = tex1D(rockscaleSampler, saturate(noiseResult3)); 
float noiseResult4 = turbulence(input.Normal*10, 2); 
float4 noiseColor4 = tex1D(snowscaleSampler, saturate(noiseResult4)); 
float4 resultColor = noiseColor1 * input.TexWeights.x + 
 noiseColor2 * input.TexWeights.y + 
 noiseColor3 * input.TexWeights.z + 
 noiseColor4 * input.TexWeights.w; 

Сначала четыре значения шума вычислены суммированием Турбуленцией. Турбуленция – это сумма значений модулей, что гарантирует положительность результата. В этом плюс выполнения поиска текстур в градиентах с использованием результата шума в качестве координат текстур. Конечный цвет состоит из суммы цветов, умноженных на соответствующий вес текстуры.

Получившаяся сфера текстурирована интересней:

Градиентное освещение.

Осталось добавить свет. Для этого необходима нормаль к каждой поверхности, но вы должно быть удивляетесь, как мы будем считать нормаль к смещенным ячейкам. Вот где в игру вступает градиент шума. В главе 5 первой книги GPU Gems, Кен Перлин коротко описывает его. По существу, градиент шума есть производная Шума Перлина в конкретной точке. Зная нормаль к сфере, мы можем вычислить новую нормаль, используя градиент.

Для начала нужно добавить нормаль к сфере в выходные данные вершинного шейдера.

struct VertexShaderOutput 
{ 
 float4 Position : POSITION0; 
 float3 Normal : NORMAL0; 
 float4 TexWeights : TEXCOORD0; 
}; 

Одну строку необходимо дописать к самому вершинному шейдеру, которая будет выполнять вычисление нормали к сфере нормализацией позиции. Позиция – это точка на сфере, т.е. нормаль к позиции равна нормали к поверхности в этой точке.

// calc the normal of the vertex 
output.Normal = normalize(input.Position.xyz); 

Теперь нужно добавить функцию к нашему шейдеру, считающую градиент суммарного шума fBm.

// calculate gradient of fBm noise (very expensive!) 
float3 fBmGradient(float3 pos, float delta, float octaves) 
{ 
 float f0 = fBm(pos, octaves); 
 float fx = fBm(pos + float3(delta, 0, 0), octaves); 
 float fy = fBm(pos + float3(0, delta, 0), octaves); 
 float fz = fBm(pos + float3(0, 0, delta), octaves); 
 return float3(fx - f0, fy - f0, fz - f0) / delta; 
} 

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

В пиксельном шейдере код вычисления освещения совмещен с оценкой производной с помощью градиента и истинной нормали для каждого пикселя:

// lighting calculation 
if (enableLighting) 
{ 
 // use Perlin Noise gradient to calc each pixel's normal vector 
 input.Normal = normalize(input.Normal - fBmGradient(input.Normal, 0.000001, 12)); 

 float lightingFactor = saturate(dot(input.Normal, -lightDirection)); 
 resultColor = resultColor * lightingFactor; 
} 

Этот код помещен под условие ради возможности настраивать освещение.

В итоге получается такая картинка:

В проекте PerlinCombined использованы все техники: смещенные вершины, процедурное текстурирование, и попиксельное освещение. По нажатию F1 происходит переключение к каркасному режиму, по F2 — к сплошному, а F3 включает или выключает освещение. Стрелками можно вращать сферу.

Заключение.

Реализация Шума Перлина как для пиксельного, так и для вершинного шейдера имеет под собой широкий потенциал. Многие вещи теперь могут быть вычислены программно на GPU.

Возможно, что некоторые моменты придется поменять для использования в «настоящей» игре. Например, переключение из градиентного освещения к отложенному вычислению освещения при удвоенной частоте кадра, что сделает свет точнее( никаких больше оценок ). Я бы представил решение здесь, но оно довольно сложное и требует отдельного описания. К тому же, для XNA существуют описания отложенной отрисовки.

В качестве финального примера потенциала Шума Перлина на GPU, я предоставляю эту картинку.

Она выполнена с использованием ровно той же реализации Шума Перлина на HLSL, что представлена выше, с включенным уровнем детального поиска и отложенным освещением.

Что дальше?

Shader Model 4.0 в DirectX 10 предоставляет для пиксельных шейдеров возможность индексации в постоянной памяти. Это значит, что можно переписать реализацию HLSL, сделав ее чище и точно повторяющей реализацию на CPU. Значения перемещения и градиентов могут храниться в обычных массивах, а текстуры не нужно будет создавать и пересылать перед вызовом отрисовки. Это также означает, что больше не потребуется поиск по текстуре, ведь будет просто постоянная индексация в памяти. Я указываю эти факты на случай, если какая-нибудь будущая версия XNA будет поддерживать Shader Model 4.0 или более позднюю его реализацию.

Реклама
Запись опубликована в рубрике Uncategorized. Добавьте в закладки постоянную ссылку.

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

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s