 |
|
Часть вторая. Облака
Облака - взвешенные в атмосфере продукты конденсации водяного пара, видимые на небе с поверхности земли.
Именно так сказано в wikipedia.ru. Но что это нам дает? Все мы знаем как они выглядят с самого детства, т.к. видим их каждый день.
Самый простой способ нарисовать неповторяющиеся формы облаков – метод шума Перлина. Суть его такая – сперва мы создаем текстуру шума, далее увеличиваем ее размеры с помощью октав, и обрезаем цвета по экспоненте, чтобы выделить отдельные облака.
Шум Перлина - математический алгоритм по генерированию процедурной текстуры псевдо-случайным методом.
Т.е. существует некая функция – которой мы передаем случайное целое число – а она возвращает нам число от -1,0f до +1,0f. Вот она :
//Функция двумерного шума, на вход два числа(int) на выход – шум (double)
double Noise2D (int x,int y)
{
int n = x + y*57;
n = (n << 13) ^ n;
return ( 1.0f - ( (n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0f);
} |
Но какие числа ей передавать? Случайные! Следующая функция получает случайные числа :
int xor128(void)
{
static int x = 123456789;
static int y = 362436069;
static int z = 521288629;
static int w = 88675123;
int t;
t = x ^ (x << 11);
x = y; y = z; z = w;
return w = w ^ (w >> 19) ^ t ^ (t >> 8);
} |
Итоговая функция создания текстуры с шумом Перлина будет такой :
HRESULT GenerateNoise(ID3D10Device* OurDevice, int size,ID3D10ShaderResourceView **Texture)
{
//ШАГ1 СОЗДАЕМ ДВУМЕРНЫЙ МАССИВ
double **NoiseR;
double **NoiseG;
double **NoiseB;
double **NoiseA;
NoiseR = new double*[size];
for (int z=0; z < size; z++){NoiseR[z] = new double[size];}
NoiseG = new double*[size];
for (int z=0; z < size; z++){NoiseG[z] = new double[size];}
NoiseB = new double*[size];
for (int z=0; z < size; z++){NoiseB[z] = new double[size];}
NoiseA = new double*[size];
for (int z=0; z < size; z++){NoiseA[z] = new double[size];}
//ШАГ2 ЗАПОЛНЯЕМ МАССИВ ШУМОМ ПЕРЛИНА
for (int q=0; q<size; q++)
for (int k=0; k<size; k++){
NoiseR[q][k]= (Noise2D(xor128(),xor128()));
NoiseG[q][k]= (Noise2D(xor128(),xor128()));
NoiseB[q][k]= (Noise2D(xor128(),xor128()));
NoiseA[q][k]= (Noise2D(xor128(),xor128()));
}
//ШАГ3 ПЕРЕВОДИМ ЗНАЧЕНИЯ ШУМА В БИТОВЫЕ(Было -1:1, стало 0:255)
for (int q=0; q<size; q++)
for (int k=0; k<size; k++)
{
NoiseR[q][k] = (int)(255*(NoiseR[q][k]/2+0.5f));
NoiseG[q][k] = (int)(255*(NoiseG[q][k]/2+0.5f));
NoiseB[q][k] = (int)(255*(NoiseB[q][k]/2+0.5f));
NoiseA[q][k] = (int)(255*(NoiseA[q][k]/2+0.5f));
}
//ШАГ4 СОЗДАЕМ И ЗАПОЛНЯЕМ ТЕКСТУРУ
ID3D10Texture2D *PerlinTex = NULL; //PERLIN NOISE
D3D10_TEXTURE2D_DESC desc;
desc.MiscFlags = D3D10_RESOURCE_MISC_SHARED;
desc.Width = size;
desc.Height = size;
desc.MipLevels = desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.SampleDesc.Quality = 0;
desc.Usage = D3D10_USAGE_DYNAMIC;
desc.BindFlags = D3D10_BIND_SHADER_RESOURCE;
desc.CPUAccessFlags = D3D10_CPU_ACCESS_WRITE;
if( FAILED( OurDevice->CreateTexture2D( &desc, NULL, &PerlinTex )))
{
return S_FALSE;
}
D3D10_MAPPED_TEXTURE2D mappedTex;
PerlinTex->Map( D3D10CalcSubresource(0, 0, 1), D3D10_MAP_WRITE_DISCARD, 0, &mappedTex );
UCHAR* pTexels = (UCHAR*)mappedTex.pData;
for( UINT row = 0; row < desc.Height; row++ )
{
UINT rowStart = row * mappedTex.RowPitch;
for( UINT col = 0; col < desc.Width; col++ )
{
UINT colStart = col * 4;
pTexels[rowStart + colStart + 0] = (UCHAR)(NoiseR[row][col]); // Red
pTexels[rowStart + colStart + 1] = (UCHAR)(NoiseG[row][col]); // Green
pTexels[rowStart + colStart + 2] = (UCHAR)(NoiseB[row][col]); // Blue
pTexels[rowStart + colStart + 3] = (UCHAR)(NoiseA[row][col]); // Alpha
}
}
PerlinTex->Unmap( D3D10CalcSubresource(0, 0, 1) );
// Create the shader-resource view
D3D10_SHADER_RESOURCE_VIEW_DESC srDesc;
srDesc.Format = desc.Format;
srDesc.ViewDimension = D3D10_SRV_DIMENSION_TEXTURE2D;
srDesc.Texture2D.MostDetailedMip = 0;
srDesc.Texture2D.MipLevels = 1;
if( FAILED( OurDevice->CreateShaderResourceView( PerlinTex, &srDesc, &(*Texture) )))
{
return S_FALSE;
}
//free recources
for (int i=0; i<size; i++)
{
delete [] NoiseR[i];
delete [] NoiseG[i];
delete [] NoiseB[i];
delete [] NoiseA[i];
}
delete [] NoiseR;
delete [] NoiseG;
delete [] NoiseB;
delete [] NoiseA;
return S_OK;
} |
Вызов функции:
ID3D10ShaderResourceView* NoiseTex = NULL;
GenerateNoise(Device,16,&NoiseTex); |
Текстура имеет размеры 16*16, теперь нужно ее преобразовать. Чем выше размеры текстуры, тем лучше, но нужно знать золотую середину. Я выбрал – 2048*2048.
Наша маленькая текстура должна быть такой:

Увеличена в миллион раз =)
Преимущество метода Перлина в том, что данная текстура уже затайлена!
Создаем рендер таргеты и необходимые ресурсы (текстуры) для рендеринга текстуры облаков
HRESULT CreateCloudRecources(ID3D10Device* OurDevice, int size, D3D10_VIEWPORT* ViewPort, ID3D10RenderTargetView** RTView, ID3D10ShaderResourceView** SRView)
{
ID3D10Texture2D* Texture;
//SET UP VIEWPORT
ViewPort->Width = size;
ViewPort->Height = size;
ViewPort->MinDepth = 0.0f;
ViewPort->MaxDepth = 1.0f;
ViewPort->TopLeftX = 0;
ViewPort->TopLeftY = 0;
D3D10_TEXTURE2D_DESC TexDesc;
TexDesc.Width = size;
TexDesc.Height = size;
TexDesc.MipLevels = 0;
TexDesc.ArraySize = 1;
TexDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
TexDesc.SampleDesc.Count = 1;
TexDesc.SampleDesc.Quality = 0;
TexDesc.Usage = D3D10_USAGE_DEFAULT;
TexDesc.BindFlags = D3D10_BIND_RENDER_TARGET | D3D10_BIND_SHADER_RESOURCE;
TexDesc.CPUAccessFlags = 0;
TexDesc.MiscFlags = D3D10_RESOURCE_MISC_GENERATE_MIPS;
if( FAILED( OurDevice->CreateTexture2D(&TexDesc, NULL, &Texture)))
{
return S_FALSE;
}
D3D10_RENDER_TARGET_VIEW_DESC RTDesc;
RTDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
RTDesc.ViewDimension = D3D10_RTV_DIMENSION_TEXTURE2D;
RTDesc.Texture2D.MipSlice = 0;
if( FAILED( OurDevice->CreateRenderTargetView(Texture, &RTDesc, &(*RTView))))
{
return S_FALSE;
}
D3D10_SHADER_RESOURCE_VIEW_DESC SRDesc;
SRDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
SRDesc.ViewDimension = D3D10_SRV_DIMENSION_TEXTURE2D;
SRDesc.Texture2D.MostDetailedMip = 0;
SRDesc.Texture2D.MipLevels = 1;
if( FAILED( OurDevice->CreateShaderResourceView(Texture, &SRDesc, &(*SRView))))
{
return S_FALSE;
}
return S_OK;
} |
Вызов такой :
D3D10_VIEWPORT vpCloud;
ID3D10RenderTargetView* CloudRTView;
ID3D10ShaderResourceView* CloudSRView;
bool FirstFrameCloudReady=false;
CreateCloudRecources(Device,2048,&vpCloud, &CloudRTView, &CloudSRView); |
FirstFrameCloudReady – переменная являющаяся флагом готовности текстуры.
Т.е. она контролирует конвейер рендеринга так, чтобы облака создавались только в первом кадре. Хотя их можно создать и на подготовке уровня, т.е. его загрузке.
Вот так мы рендерим в текстуру :
if (!FirstFrameCloudReady)
{
Device->RSSetViewports( 1, &vpCloud );
Device->OMSetRenderTargets( 1, &CloudRTView, NULL );
Device->ClearRenderTargetView(CloudRTView, ClearColor );
Scatter_Perlin->SetResource( NoiseTex );
Device->IASetVertexBuffers(0, 1, &quad, &stride, &offset);
Device->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
ScatterCloudTech->GetPassByIndex( 0 )->Apply( 0 );
Device->Draw(6,0 );
FirstFrameCloudReady=true;
} |
Рендерим облака в текстуру простейшим шейдером октав:
PS_INPUTCloud MakeCloudVS( VS_INPUT input )
{
PS_INPUTCloud output = (PS_INPUTCloud)0;
output.Pos = input.Pos;
output.Tex = input.Tex;
return output;
}
float4 MakeCloudPS( PS_INPUTCloud input) : SV_Target
{
float n1=txPerlin.Sample( PerlinSampler, input.Tex*1);
float n2=txPerlin.Sample( PerlinSampler, input.Tex*2);
float n3=txPerlin.Sample( PerlinSampler, input.Tex*4);
float n4=txPerlin.Sample( PerlinSampler, input.Tex*8);
float n5=txPerlin.Sample( PerlinSampler, input.Tex*16);
float n6=txPerlin.Sample( PerlinSampler, input.Tex*32);
float nFinal=n1+n2/2+n3/4+n4/8+n5/16+n6/32;
float c;
float CloudCover=1.0;
float CloudSharpness=0.001;
c = CloudCover - nFinal;
if (c < 0) c=0;
float CloudDensity;
CloudDensity = (1 - pow(CloudSharpness,c));
return CloudDensity;
} |
CloudCover – регулирует плотность покрытия облаков
CloudSharpness – четкость облаков
Возможен другой вариант шейдера октав – если кому интересно, вот он:
float2 st = input.Tex.xy;
float g = 1.0;
float4 r = 0.0;
for (int i = 0.0; i < 8; i++)
{
r += g * (2.0 * txPerlin.Sample( PerlinSampler, st) - 1.0);
st = st * 2.0;
g *= 0.5;
}
r=saturate(r);
return r; |
Этот пример для 8 октав.
В результате получим вот такую текстуру:

Теперь можно подготовить геометрию облаков. Открываем 3ДМакс. Создаем плоскость, размеры 2000*2000, количество сегментов 16*16. Конвертируем в Editable Mesh. В свитке выбираем Vertex Edit, в параметрах Soft Selection, выбираем Use Soft Selection. Настраиваем так:

Выделяем центральный вертекс и вытягиваем вверх на 100-200 единиц.
Все. Экспортируем. Подгружаем в движок – теперь рисуем :
D3DXMATRIX cloudWorld,cloudScale;
D3DXMatrixIdentity(&cloudWorld);
D3DXMatrixIdentity(&cloudScale);
D3DXMatrixTranslation(&cloudWorld,cam_pos.x,cam_pos.y-75,cam_pos.z);
D3DXMatrixScaling(&cloudScale,1,0.9f,1);
D3DXMatrixMultiply(&cloudWorld,&cloudScale,&cloudWorld);
Scatter_World->SetMatrix( ( float* )&cloudWorld);
Scatter_CloudTex->SetResource( CloudSRView );
Device->IASetVertexBuffers(0, 1, &scape.VB[0], &stride, &offset);
Device->IASetIndexBuffer(scape.IB[0], DXGI_FORMAT_R32_UINT, 0);
ScatterRenderCloudTech->GetPassByIndex( 0 )->Apply(0);
Device->DrawIndexed( scape.polygon_count[0][0]*3, scape.offset_index[0][0], 0 ); |
Понижаем на 75 единиц и немного масштабируем. Подберите эти значения сами чтобы облака смотрелись отлично.
Что же нужно нашему шейдеру? Ему нужно то же что и шейдеру неба, плюс текстуру облаков.
Вершинный шейдер облаков идентичен вершинному шейдеру неба. Он есть выше, поэтому пропустим его. Теперь главное – пиксельный шейдер облаков:
float4 PSCloud( PS_INPUTSB input) : SV_Target
{
//Пиксель из нашей текстуры облаков
float4 CloudTex = txCloud.Sample( PerlinSampler, input.Tex);
//ЭТА ЧАСТЬ ИДЕНТИЧНА ШЕЙДЕРУ НЕБА – ПОЛУЧАЕМ ЦВЕТ НЕБА В ТОЧКЕ ОБЛАКА
float4 colorOutput = float4(0,0,0,1);
float eyeAlt = CamPos.y;
float3 eyeVec = normalize(input.WorldEyeDirection);
float3 lightVec = normalize(LightDir.xyz);
float sunHighlight = 0.3*pow(max(0, dot(lightVec, -eyeVec)), sunRadiusAttenuation) * SunLightness;
float largeSunHighlight = pow(max(0, dot(lightVec, -eyeVec)), largeSunRadiusAttenuation) * largeSunLightness;
float3 flatLightVec = normalize(float3(lightVec.x, 0, lightVec.z));
float3 flatEyeVec = normalize(float3(eyeVec.x, 0, eyeVec.z));
float diff = dot(flatLightVec, -flatEyeVec);
float val = lerp(0.25, 1.25, min(1, hazeTopAltitude / max(0.0001, eyeAlt)));
float YAngle = pow(max(0, -eyeVec.y), val);
float4 fogColorDay = txScatt.Sample( samLinear, float2( (1 - (diff + 1) * 0.5)*0.32, 1-YAngle));
float4 fogColorSunset = txScatt.Sample( samLinear, float2( (1 - (diff + 1) * 0.5)*0.32+0.34, 1-YAngle));
float4 fogColorNight = txScatt.Sample( samLinear, float2( (1 - (diff + 1) * 0.5)*0.32+0.68, 1-YAngle));
float4 fogColor;
if (lightVec.y > 0)
{
fogColor = lerp(fogColorDay, fogColorSunset, min(1, pow(1 - lightVec.y, dayToSunsetSharpness)));
}
else
{
fogColor = lerp(fogColorSunset, fogColorNight, min(1, -lightVec.y * 4));
}
fogColor += sunHighlight + largeSunHighlight;
colorOutput = lerp(fogColor, colorOutput, input.Fog);
colorOutput = fogColor+ sunHighlight;
colorOutput.rgb += max(0,(1 - colorOutput.rgb)) * float3( 0.15, 0.15, 0.2 );
//ШЕЙДИНГ ОБЛАКА
float a = CloudTex.a;
if (LightDir.y<0.35) {a=lerp(a/3,a,LightDir.y*(1/0.35));}
if (LightDir.y<0.0) {a=CloudTex.a/3.0;}
if (YAngle<0.35) {a=lerp(0,a,YAngle*(1/0.35));}
if (YAngle<=0.0) {a=0;}
//немного глушим альфу...
a=lerp(a/2,a,CloudTex.a);
//ШЕЙДИНГ
float3 ResCol=1;
if (LightDir.y<=0.5) ResCol=lerp(float3(1.0,0.7,0.7),float3(1,1.0,1.0),LightDir.y*(1/0.5));
if (LightDir.y<=0.0) {ResCol=colorOutput.rgb+float3(0.1,0.1,0.1);}
float3 SunD=LightDir.xyz;
SunD.x+=0.85;
float3 sunDirection = normalize (SunD) * 0.015;//На сколько смещается текстура!
sunDirection.y = -sunDirection.z;
sunDirection.z = 0.0;
float2 oUv=input.Tex;
float3 absorption = colorOutput.rgb+0.1;
for (int i=0; i<6; i++)
{
absorption +=(txCloud.Sample( PerlinSampler, oUv + sunDirection.xy *0.1* i ).aaa +
txCloud.Sample( PerlinSampler, oUv + sunDirection.xy *0.1* i ).aaa)*0.5;
}
float Tmp=txCloud.Sample( PerlinSampler, oUv+sunDirection.xy).a;
float3 attenuation=(1.0 - clamp ((1-colorOutput.rgb/1.1) * Tmp, 0.0, 1.0));
ResCol.rgb *= attenuation;
return float4(ResCol,a);
} |
Ну как? Шейдер действительно выглядит пугающе. По сути мы получаем цвет неба в точке облака – такой, как если бы, там не было облака ))
Далее мы в зависимости от положения солнца глушим альфу облака.
После мы в зависимости от положения солнца подкрашиваем облако. Ближе к вечеру – оно красное. Иначе белое. Ночью серое.
Идем дальше!
Переводим источник света в 2Д координаты экрана. Немного смещаем его – чтобы было более реалистично. Начинаем затемнение – исходя из выборок текстуры вдоль вектора от солнца – мы понимаем, насколько облако затенено.
Схематически это выглядит так:
Точка 1 – будет самой светлой. Точка 4 – самой темной. Логично же!
Выражение - colorOutput.rgb/1.1 – в шейдере означает что тень облака будет такого же цвета как и освещенная сторона, но немного темнее.
Результаты показаны ниже :


<<Назад
|
Copyright CSS PSV 2009 Kherson
|
|