Создаём свои собственные шейдеры
Valve обещала дать возможность создавать свои собственные шейдеры под HL2. (Возможно Valve скоро выпустит этот инструментарий и будет доступен с очередным SourceSDK update)
Создавать шейдеры очень не просто, поэтому я решил опубликовать небольшой пример.
Я использовал программу RenderMonkey (Это версия работает только с установленным DirectX 9.0c) от aTI. Скачать можно по следующему адресу: http://www2.ati.com/developer/rendermonkey/files/RenderMonkey.2004−08−03-v1.5.424.exe (46 Мб).
Все наверняка помнят следующую тех демку от aTI:

Вооружившись презентациями по этой демке, я создал упрощённую модель. Упрощённую потому, что у меня всего дишь Radeon 8500, который может выполнить не более 22 aLU (пиксельный шейдер 1.4).
Пример 100% будет работать на следующих картах:

RaDEONTM X850 series RaDEONTM 9600 series
RaDEONTM X800 series RaDEONTM 9500 series
RaDEONTM X600 series RaDEONTM 9200 series
RaDEONTM X300 series RaDEONTM 9100 series
RaDEONTM 9800 series RaDEONTM 9000 series
RaDEONTM 9700 series RaDEONTM 8500 series

Я создал следующую структуру проекта:

NormalMap текстура для машины (я использовал эту текстуру в разрешении 2048×2048):


Теперь переёдем непосредственно к самим шейдерам:
Пиксельный шейдер:
sampler normalMap;
sampler showroomMap;
float4 paintColor0;
float4 paintColor2;
float4 paintColorMid;
float glossLevel;
float brightnessFactor;

float4 ps_main (//float4 Diff: COLOR0,
float2 Tex :TEXCOORD0,
float3 Tangent :TEXCOORD1,
float3 Binormal: TEXCOORD2,
float3 Normal :TEXCOORD3,
float3 View :TEXCOORD4 ): COLOR
{
float3 vNormal = tex2D (normalMap, Tex);
vNormal = 2 * vNormal — 1.0;

float3x3 mTangentToWorld = transpose (float3x3( Tangent, Binormal, Normal));
float3 vNormalWorld = mul (mTangentToWorld, vNormal);
float4 fNdotV = mul (vNormalWorld, View );
float fNdotVSq = fNdotV * fNdotV;
float4 paintColor = fNdotV * paintColor0 +
fNdotVSq * paintColorMid +
fNdotVSq * fNdotVSq * paintColor2;

float fFresnel = saturate (dot (vNormalWorld, View));
float3 vReflection = 2 * vNormalWorld * fFresnel — View;

float fEnvBias = glossLevel;

// Sample environment map using this reflection vector and bias:
float4 envMap = texCUBE ( showroomMap, float4(vReflection, fEnvBias));

// Premultiply by alpha:
envMap.rgb = envMap.rgb * envMap.a;

// Brighten the environment map sampling result:
envMap.rgb *= brightnessFactor;

// Combine result of environment map reflection with the paint
//float fEnvContribution = 1.0 — 0.5 * fFresnel;

return float4 (envMap.rgb + paintColor, 1.0);//* fEnvContribution
}

Вершинный шейдер:
float4x4 view_proj_matrix;
float4 view_position;
float4x4 inv_view_matrix;

struct VS_OUTPUT
{
float4 Pos: POSITION;
float2 Tex :TEXCOORD0;
float3 Tangent :TEXCOORD1;
float3 Binormal: TEXCOORD2;
float3 Normal :TEXCOORD3;
float3 View :TEXCOORD4;
};

VS_OUTPUT main (float4 Pos: POSITION,
float3 Normal: NORMaL,
float2 Tex :TEXCOORD,
float3 Tangent: TaNGENT,
float3 Binormal: BINORMaL
)
{
VS_OUTPUT Out = (VS_OUTPUT) 0;

Out.Pos = mul (view_proj_matrix, Pos);

Out. View = normalize (mul (inv_view_matrix, float4(0, 0, 0, 1)) — Pos );

Out.Tex = Tex;
Out.Normal = Normal;
Out.Tangent = Tangent;
Out.Binormal = Binormal;
return Out;
}

В результате этого должно получиться следующее:

Различные цвета можно изменять. В результате этого мы можем получить различные цветовые схемы машины.

Часть 2.
Из презентаций от aTI мы увидим, что у этой машины есть также слой микрочастиц:


В Render Monkey мной был создан проект со следующей структурой:

Для тех, у кого видеоадаптеры не поддерживают выполнение шейдеров 2-ого поколения, то им необходимо воспользоваться Reference Rasterizer. В моём понимании — это модуль к DirectX, который позволяет обрабатывать шейдеры программно, причём всех моделей (Shader Model 1. x, Shader Model 2, Shader Model 3).
Этот Reference Rasterizer есть в комплекте DirectX SDK. (самый последний dxsdk_oct2004.exe)
Для слоя микрочастиц я использовал следующую текстуру:

Теперь перейдём к самим шейдерам:
Пиксельный шейдер:
sampler normalMap;
sampler microflakeNMap;
float microflakePerturbationa;
float normalPerturbation;
float microflakePerturbation;
float4 paintColor0;
float4 paintColor2;
float4 paintColorMid;
float4 flakeLayerColor;

float4 ps_main (float4 Diff: COLOR0,
float2 Tex: TEXCOORD0,
float3 Tangent: TEXCOORD1,
float3 Binormal: TEXCOORD2,
float3 Normal: TEXCOORD3,
float3 View: TEXCOORD4,
float3 SparkleTex: TEXCOORD5): COLOR
{// Пертурбированная нормаль из карты шумов
float3 vNormal = tex2D (normalMap, Tex);
vNormal = 2 * vNormal — 1.0;
// Подсчитываем нормали для обоих слоев микрочастиц
float3 vFlakesNormal = 2 * tex2D (microflakeNMap, SparkleTex) — 1;
float3 vNp1 = microflakePerturbationa * vFlakesNormal +
normalPerturbation * vNormal;
float3 vNp2 = microflakePerturbation * (vFlakesNormal + vNormal);
float3 vView = View;
float3x3 mTangentToWorld = transpose (float3x3(Tangent, Binormal, Normal));
// Подсчитываем скалярные продукты нормализированного вектора обозревателя с нормалями для обоих слоев микрочастиц
float3 vNp1World = mul (mTangentToWorld, vNp1);
float fFresnel1 = saturate (dot (vNp1World, vView));
float3 vNp2World = mul (mTangentToWorld, vNp2);
float fFresnel2 = saturate (dot (vNp2World, vView));
// Компонуем многотональный цвет прослойки микрочастиц
float fFresnel1Sq = fFresnel1 * fFresnel1;
float4 paintColor = fFresnel1 * flakeLayerColor + fFresnel1Sq * flakeLayerColor +
fFresnel1Sq * fFresnel1Sq * flakeLayerColor+
pow (fFresnel2, 16) * flakeLayerColor;
return float4 (paintColor);
}
Вершинный шейдер :
float4x4 view_proj_matrix;
float4x4 inv_view_matrix;
float fFlakeTilingFactor;

struct VS_OUTPUT
{
float4 Pos: POSITION;
float2 Tex: TEXCOORD0;
float3 Tangent: TEXCOORD1;
float3 Binormal: TEXCOORD2;
float3 Normal: TEXCOORD3;
float3 View: TEXCOORD4;
float3 SparkleTex: TEXCOORD5;
};

VS_OUTPUT vs_main (float4 Pos: POSITION,
float3 Normal: NORMaL,
float2 Tex: TEXCOORD0,
float3 Tangent: TaNGENT,
float3 Binormal: BINORMaL)
{
VS_OUTPUT Out = (VS_OUTPUT) 0;
Out.Pos = mul (view_proj_matrix, Pos);
Out.View = normalize (mul (inv_view_matrix, float4(0, 0, 0, 1)) — Pos);
Out.Tex = Tex;
Out.Normal = Normal;
Out.Tangent = Tangent;
Out.Binormal = Binormal;
Out.SparkleTex = float4(Tex * fFlakeTilingFactor, 0,1);
return Out;
}
В результате мы должны получить следующее:

При более близком рассмотрении увидим слой микрочастиц, состоящий из маленьких квадратиков:

Конечно, изображение можно сделать гораздо лучше. Здесь уже многое зависит от самой текстуры микрочастиц, которую вы используете.
Часть 3.
Вот пояснения от Valve относительно шейдеров в HL 2:

Valve принимала участие в GDC 2004, а в результате на сайте aTI появился файл D3DTutorial10_Half-Life2_Shading.pdf (который также можно скачать с сайта aTI). А пример будет строиться по подобному дереву эффектов в этой документации. В помощь приходит проектный файл Illumination.rfx, входящий в состав RM 1.6 (в нём содержится весь нужный нам код для описания подобной структуры). Разумеется, проект был немного переделан, чтоб как можно больше соответствовал структуре, описанной в документации. Также убраны все лишние объекты.

Исходный код, приводящийся в документации, я не использовал ввиду того, что это займёт не мало времени на переделку в структуру RM.
Вот структура, которую мы попытаемся сделать (Взято из D3DTutorial10_Half-Life2_Shading.pdf):



А вот как это получилось описать у меня для примера, который мы рассматриваем:




В Render Monkey проект выглядит следующим образом:




Для тех, у кого видеоадаптеры не поддерживают выполнение шейдеров 2-ого поколения, то им необходимо воспользоваться Reference Rasterizer. В моём понимании — это модуль к DirectX, который позволяет обрабатывать шейдеры программно, причём всех моделей (Shader Model 1. x, Shader Model 2, Shader Model 3).
Этот Reference Rasterizer есть в комплекте DirectX SDK.
Т.к. у меня всего лишь Radeon 8500 (PS 1.4), то мне пришлось им воспользоваться. Reference Rasterizer занимает где-то 8 Мб и его возможно установить отдельно от DX 9 SDK — достаточно скопировать нужные библиотеки в system 32.
Под PS 1.4 сделать подобный пример невозможно (даже если будет несколько проходов (pass)). Вот, например,
bump = normalize (bump * 2.0 f); занимает 4 операции, а это ¼ PS 1.4.


Теперь перейдём к самим шейдерам:
Вершинный шейдер:
float4x4 view_proj_matrix: register (c0);
float4 light_position: register (c8);
float4 eye_position: register (c9);
float4x4 view_matrix: register (c10);
float4x4 inv_view_matrix;
struct VS_INPUT_StrUCT
{
float4 position: POSITION;
float3 normal: NORMaL;
float2 texcoord0: TEXCOORD0;
float3 binormal: BINORMaL0;
float3 tangent: TaNGENT0;
};

struct VS_OUTPUT_StrUCT
{
float4 position: POSITION;
float2 bump_map: TEXCOORD0;
float3 light_vector: TEXCOORD1;
float3 half_angle: TEXCOORD2;
float3 basis1: TEXCOORD3;
float3 basis2: TEXCOORD4;
float3 basis3: TEXCOORD5;
};

//** main — Главная точка входа для вершинного шейдера
//** Returns: VS_OUTPUT_StrUCT
//**---------------------------------------------------------
VS_OUTPUT_StrUCT main (VS_INPUT_StrUCT vsInStruct)
{
VS_OUTPUT_StrUCT vsOutStruct; //** Declare the output struct

vsOutStruct.position = mul (view_proj_matrix, vsInStruct. position);
float3 position = mul (view_matrix, vsInStruct. position);

//**----------------------------------------------
//** Проход для вычисления координат текстур bump и base
//**----------------------------------------------
vsOutStruct.bump_map = vsInStruct. texcoord0;

//**--------------------------------------------
//** Подсчёт вектора света в пространстве,
//** и далее трансформация его в область текстуры.
//**--------------------------------------------
float3 temp_light_position = mul (inv_view_matrix, light_position);
float3 temp_light_vector = temp_light_position — vsInStruct. position;
vsOutStruct.light_vector.x = dot (temp_light_vector, vsInStruct. tangent);
vsOutStruct.light_vector.y = dot (temp_light_vector, vsInStruct. binormal);
vsOutStruct.light_vector.z = dot (temp_light_vector, vsInStruct. normal);

//**-------------------------------------------
//** Calculate the view vector in object space,
//** and then transform it into texture space.
//**-------------------------------------------
float3 temp_eye_position = mul (inv_view_matrix, eye_position);
float3 temp_view_vector = temp_eye_position — vsInStruct. position;
float3 temp_view_vector2;
temp_view_vector2.x = dot (temp_view_vector, vsInStruct. tangent);
temp_view_vector2.y = dot (temp_view_vector, vsInStruct. binormal);
temp_view_vector2.z = dot (temp_view_vector, vsInStruct. normal);

//**-------------------------
//** Calculate the half angle
//**-------------------------
vsOutStruct.half_angle = vsOutStruct. light_vector + temp_view_vector2;

//**-----------------------------------------
//** Construct the transpose of the tangent
//** space basis vectors, so we can transform
//** the reflection vector from texture space
//** into view space
//**-----------------------------------------
vsOutStruct.basis1.x = vsInStruct.tangent.x;
vsOutStruct.basis1.y = vsInStruct.binormal.x;
vsOutStruct.basis1.z = vsInStruct.normal.x;
vsOutStruct.basis2.x = vsInStruct.tangent.y;
vsOutStruct.basis2.y = vsInStruct.binormal.y;
vsOutStruct.basis2.z = vsInStruct.normal.y;
vsOutStruct.basis3.x = vsInStruct.tangent.z;
vsOutStruct.basis3.y = vsInStruct.binormal.z;
vsOutStruct.basis3.z = vsInStruct.normal.z;

return vsOutStruct; //** Return the resulting output struct
}
В результате у вершинного шейдера 34 операции.

Пиксельный шейдер:
float4 specular: register (c6);
float Ka: register (c7);
float Kd: register (c8);
float Ks: register (c9);
float specular_power: register (c10);
float bumpiness: register (c11);
float reflection_clarity: register (c12);
float reflectance: register (c13);
float4 ambient: register (c4);
float4 diffuse: register (c5);
float4x4 view_matrix: register (c0);
sampler base_map: register (s0);
sampler bump_map: register (s1);
sampler environment_map: register (s2);
struct PS_INPUT_StrUCT
{
float2 bump_map: TEXCOORD0;
float3 light_vector: TEXCOORD1;
float3 half_angle: TEXCOORD2;
float3 basis1: TEXCOORD3;
float3 basis2: TEXCOORD4;
float3 basis3: TEXCOORD5;
};

struct PS_OUTPUT_StrUCT
{
float4 color0: COLOR0;
};

//**---------------------------------------------------------
//** Function: main
//** Description: Declare the main entry point for the shader
//** Input: PS_INPUT_StrUCT, derived from the output of
//** the associated vertex shader
//** Returns: PS_OUTPUT_StrUCT
//**---------------------------------------------------------
PS_OUTPUT_StrUCT main (PS_INPUT_StrUCT psInStruct)
{
PS_OUTPUT_StrUCT psOutStruct;

//**----------------------------------------
//** Get the base and bump map texture coord
//**----------------------------------------
float4 bump_coord = { psInStruct.bump_map, 0.0f, reflection_clarity };

//**------------------------------------------------------
//** Retreive the base color and bump components from the
//** respective textures, based on the passed bump coords.
//**------------------------------------------------------
float3 base = tex2D (base_map, bump_coord);
float3 bump = tex2D (bump_map, bump_coord);

//**--------------------------------------------------
//** Includes MIP bias to help clairify the reflection
//**--------------------------------------------------
float3 reflection_bump = tex2Dbias (bump_map, bump_coord);

//**----------------------------------------------------
//** Normalize the passed vectors from the vertex shader
//**----------------------------------------------------
float3 normalized_light_vector = normalize (psInStruct.light_vector);
float3 normalized_half_angle = normalize (psInStruct.half_angle);
//**--------------------------------------------------------
//** «Smooth out» the bumps based on the bumpiness parameter.
//** This is simply a linear interpolation between a «flat»
//** normal and a «bumped» normal. Note that this «flat»
//** normal is based on the texture space coordinate basis.
//**--------------------------------------------------------
float3 smooth;
smooth.r = 0.5f;
smooth.g = 0.5f;
smooth.b = 1.0f;
bump = lerp (smooth, bump, bumpiness);
bump = normalize ((bump * 2.0f) — 1.0f);
reflection_bump = lerp (smooth, reflection_bump, bumpiness);
reflection_bump = normalize ((reflection_bump * 2.0f) — 1.0f);

//**---------------------------------------------
//** translate the reflection normal from texture
//** space into view space, so we can case the
//** reflection vector into an environment map.
//**---------------------------------------------
float3 reflection = reflection_bump;
reflection.x = dot (reflection_bump, psInStruct. basis1);
reflection.y = dot (reflection_bump, psInStruct. basis2);
reflection.z = dot (reflection_bump, psInStruct. basis3);
float3 reflection_vector = mul (view_matrix, reflection);
reflection_vector = normalize (reflection_vector);

//**------------------------------------------
//** Calculate the resulting reflection color.
//**------------------------------------------
// float3 reflection_color = texCUBE (environment_map, reflection_vector);
float3 reflection_color = texCUBE (environment_map, reflection_vector);
//**----------------------------------------------------------
//** The following modifiers are used to enhance the effect of
//** the basic reflection idea. Normally, specular / gloss
//** maps will take the place of these modifiers.
//**----------------------------------------------------------
float3 result_color = lerp (base, reflection_color, reflectance);
// float3 modified_specular_color = specular;// * base;
// float3 modified_specular_coefficient = Ks;// * base;

//**---------------------------------------------------------
//** These dot products are used for the lighting model
//** equations. The surface normal dotted with the light
//** vector is denoted by n_dot_l. The normal vector
//** dotted with the half angle vector is denoted by n_dot_h.
//**---------------------------------------------------------
float3 n_dot_l = dot (bump, normalized_light_vector);
float3 n_dot_h = dot (bump, normalized_half_angle);
//**--------------------------------------
//** Далее идёт формирование финального результата
//** --------------------------------------
float3 Specular_final = (specular * Ks * pow (max (0, n_dot_h), specular_power));
float3 Diffuse_final = (diffuse * Kd * max (0, n_dot_l));
float3 ambient_final = (base * ambient*Ka);
float3 bump_Cube_specular = result_color * Specular_final;
float3 Diffuse_bump_ambient = Diffuse_final + ambient_final;
psOutStruct.color0.rgb =bump_Cube_specular+Diffuse_bump_ambient;

//**------------------------------------
//** Interpolate the resulting color
//** based on the reflectance parameter.
//**------------------------------------
psOutStruct.color0.a = 1.0f; //** Set the alpha component manually

return psOutStruct; //** Return the resulting output struct
}
У пиксельного шейдера 5 5 операций .
В результате мы должны получить следующее:

Можно менять значения объектов (например, specular), при этом мы будем получать разные результаты:

В результате мы получили технологию, похожую в Unreal 3 Engine.

Рекомендую всем ознакомиться со статьёй по созданию шейдеров на gamedev. ru:
http://www.gamedev.ru/articles/read.shtml?id=10109&page=1
Автор описывает основы программирования на HLSL.

Спасибо всем за внимание.
Автор: DomenER.
24 февраля 2005, 20:08