Показаны сообщения с ярлыком effect. Показать все сообщения
Показаны сообщения с ярлыком effect. Показать все сообщения

четверг, 21 января 2016 г.

Unity3D – Chunky post process effect

Мы периодически встречаемся с друзьями из старой (древней) команды "Accept Corp".
SpeccyWiki: http://speccy.info/Accept_Corp
Вспоминаем былые времена, что творили на ZX Spectrum, немножко ностальгируем. Это всегда приятные встречи старых друзей.

Приветы:
Phantom Lord (Дмитрий Селин)
Cardinal (Виталий Казаков)
M.O.T (Виктор Васильев)
A.Soft (Владимир Коновалов)
DJ.Wolf (Сергей Малышев)
Cannibal (Григорий Садовой)
FF-Soft (Александр Макушин)
Shov (Олег Шашкин)
Layabout (Егор Волкодамов)
Y.Soft (Равиль Габитбаев)
Visual Graphics (Павел Мирчук)

К чему я это говорю? К тому что создание данного пост эффекта навеяно именно теми временами из 90х годов, когда появилась мода в Demo рендерить 3D сцены из блоков Chunky.

Как это выглядит:


Мы не будем останавливаться на моментах, описанных в предыдущей вводной статье:
Unity3D: Simple post process effect
Пропустим очевидные манипуляции. К тому же в данном подходе много общего.

Сама суть эффекта заключается в следующем:
- результат эффекта будет монохромный, черные пиксели на белом фоне;
- размер исходных блоков chunky сделаем 4x4 пикселя;
- спрайты chunky будем выбирать из заранее подготовленной текстуры. Выглядит она как полоска 64x4 пикселя, логически это наши 16 спрайтов 4x4 пикселя:


Маленькая правда? :) Для наглядности увеличим картинку в 4 раза:


- число спрайтов 16 соответствует количеству оттенков белого, которые мы будем использовать. Так же это обусловлено рисунком, комбинациями пикселей в каждой градации;
- мы выбираем из текстуры экрана цвет, приводим к чёрно-белому, условно соответствующему очередному блоку;
- приводим значение градации к индексу спрайта в листе chunky;
- и выводим сам спрайт.

А теперь по-настоящему.
Код шейдера:

Shader "Hidden/Chunky"
{
 Properties
 {
  _MainTex("Texture", 2D) = "white" {}
  _SprTex("Texture", 2D) = "white" {}
 }
 SubShader
 {
  Pass
  {
   CGPROGRAM
   #pragma vertex vert_img
   #pragma fragment frag

   #include "UnityCG.cginc"

   sampler2D _MainTex;
   sampler2D _SprTex;
   float4 _Color = float4(1, 1, 1, 1);
   float2 BlockCount;
   float2 BlockSize;

   fixed4 frag(v2f_img i) : SV_Target
   {
    // (1)
    float2 blockPos = floor(i.uv * BlockCount);
    float2 blockCenter = blockPos * BlockSize + BlockSize * 0.5;

    // (2)
    float4 del = float4(1, 1, 1, 1) - _Color;

    // (3)
    float4 tex = tex2D(_MainTex, blockCenter) - del;
    float grayscale = dot(tex.rgb, float3(0.3, 0.59, 0.11));
    grayscale = clamp(grayscale, 0.0, 1.0);

    // (4)
    float dx = floor(grayscale * 16.0);

    // (5)
    float2 sprPos = i.uv;
    sprPos -= blockPos*BlockSize;
    sprPos.x /= 16;
    sprPos *= BlockCount;
    sprPos.x += 1.0 / 16.0 * dx;

    // (6)
    float4 tex2 = tex2D(_SprTex, sprPos);
    return tex2;
   }
   ENDCG
  }
 }
}

Где:
1) - Уже знакомое нам, по прошлой статье, приведение экранных координат к текстурным
2) - Управление яркостью картинки, нужное больше для баловства и немножко для тестов;
3) - Перевод в grayscale;
4) - Индекс спрайта в листе;
5) - Расчет координат спрайта в листе chunky;
6) - Вывод.

using UnityEngine;
using UnityStandardAssets.ImageEffects;
 
namespace Assets.Scripts.PostEffects
{
    [ExecuteInEditMode]
    [AddComponentMenu("Image Effects/Color Adjustments/Chunky")]
    public class Chunky : ImageEffectBase
    {
        public Texture2D SprTex;
 
        public Color Color = Color.white;
 
        private void OnRenderImage(RenderTexture source, RenderTexture destination)
        {
            float k = Camera.main.aspect;
            float w = Camera.main.pixelWidth;
            float h = Camera.main.pixelHeight;
            Vector2 count = new Vector2(w/SprTex.height, h/SprTex.height);
            Vector2 size = new Vector2(1.0f/count.x, 1.0f/count.y);
            //
            material.SetVector("BlockCount", count);
            material.SetVector("BlockSize", size);
            material.SetColor("_Color", Color);
            material.SetTexture("_SprTex", SprTex);
            Graphics.Blit(source, destination, material);
        }
    }
}

Ни чего сложного не добавилось.

Знакомый нам исходный экран:



Результат:


Конечно на живую в fullscreen и в динамике выглядит по интереснее.
Но, в любом случае, на картинки лучше кликать для просмотра.

среда, 20 января 2016 г.

Unity3D: Simple post process effect

В этой статье мы рассмотрим:
- порядок создания простого пост эффекта;
- некоторые нюансы приведения размеров.

              Для наших экспериментов нам потребуется сцена, с объектами или картинками, не важно, главное по пестрее. Я набросал такую:



                На этой сцене отлаживался не один пост эффект, поэтому все что на ней присутствует – закономерно и не лишнее.
                Подготовим файлы. Мы будем работать с файлом шейдера и скриптом.
               
                Начнем с шейдера.
В папке Assets кликаем правой кнопкой мыши и выбираем:
Create->Shader-> NewImageEffectShader.
               
                Нам будет предложено на выбор несколько типов шейдеров, но это не так важно, внутренности мы перепишем. Я переименовал созданный файл в «Pixelation». Открываем и зачищаем от всего лишнего до такого вида:

Shader "Hidden/Pixelation Effect"
{
 Properties
 {
  _MainTex ("Texture", 2D) = "white" {}
 }
 SubShader
 {
  Pass
  {
   CGPROGRAM
   #pragma vertex vert_img
   #pragma fragment frag
   
   #include "UnityCG.cginc"

   fixed4 frag (v2f_img i) : SV_Target
   {
   }
   ENDCG
  }
 }
}


Обратите внимание на то, что входные данные пиксельного (фрагментного, привык я к терминологии HLSL) шейдера описывает структура v2f_img. Ее вполне достаточно для нас.
Далее добавляем входные параметры шейдера и необходимый код как в примере:

Shader "Hidden/Pixelation Effect"
{
 Properties
 {
  _MainTex ("Texture", 2D) = "white" {}
 }
 SubShader
 {
  Pass
  {
   CGPROGRAM
   #pragma vertex vert_img
   #pragma fragment frag
   
   #include "UnityCG.cginc"

   sampler2D _MainTex;
   float2 BlockCount;
   float2 BlockSize;

   fixed4 frag (v2f_img i) : SV_Target
   {
    float2 blockPos = floor(i.uv * BlockCount);
    float2 blockCenter = blockPos * BlockSize + BlockSize * 0.5;

    float4 tex = tex2D(_MainTex, blockCenter);
    return tex;
   }
   ENDCG
  }
 }
}

sampler2D _MainTex – буфер экрана как текстура;
float2 BlockCount – количество кирпичиков на экране (псевдопикселей) по X и Y;
float2 BlockSize – размеры кирпичиков тоже по X и Y;

Шейдер простой, вся суть в функции floor. Из справки:
floor(x) - возвращает самое большое целое число, которое является меньше чем или равным x.
Назначение и подготовку BlockCount и BlockSize рассмотрим в скрипте эффекта.
Как создать новый скрипт я писать не буду, но стоит уточнить вот что.
Нам потребуется импортировать некоторые стандартные ассеты, а именно:
Assets->Import package->Effects
С точки зрения изучения пост эффектов нам интересен весь пакет, в дальнейшем можно самостоятельно почитать типовые эффекты, входящие в него. Но пока на нужен только ImageEffectBase.cs что бы наследовать от него класс нашего скрипта.
В результате у нас должен получиться вот такой код:

using UnityEngine;
using UnityStandardAssets.ImageEffects;
 
namespace Assets.Scripts.PostEffects
{
    [ExecuteInEditMode]
    [AddComponentMenu("Image Effects/Color Adjustments/Pixelation")]
    public class Pixelation : ImageEffectBase
    {
        [Range(64.0f, 512.0f)]
        public float BlockCount = 256;
 
        private void OnRenderImage(RenderTexture source, RenderTexture destination)
        {
            float k = Camera.main.aspect;
            Vector2 count = new Vector2(BlockCount, BlockCount/k);
            Vector2 size = new Vector2(1.0f/count.x, 1.0f/count.y);
            //
            material.SetVector("BlockCount", count);
            material.SetVector("BlockSize", size);
            Graphics.Blit(source, destination, material);
        }
    }
}

Где:
BlockCount – исходное количество блоков по горизонтали;
k – понятно из определения, коэффициент соотношения сторон экрана;
count – вектор в который складываем расчетные значения количества кирпичиков по горизонтали и вертикали;
size – расчетное значение размеров кирпичиков, основанное на их количестве и коэффициенте соотношения сторон экрана.
               
                Может быть лишнее, но я хотел бы пояснить то, что в шейдере мы имеем дело с текстурами размером (1f, 1f) не зависимо от их реальных размеров в пикселях. И т.к. текстурные координаты носят относительный характер, то эту самую относительность мы и выражаем в size. Для пущей очевидности замечу, что size делает условные пикселы квадратными на всех разрешениях мониторов с разным соотношением сторон.
               
                Далее файл скрипта перетаскиваем на объект нашей сцены - камеру, в поле скрипта Shader перетаскиваем наш шейдер. Итог:



Готово.
Запускаем.




суббота, 20 июня 2009 г.

Effect “Bump mapping”

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



И так поехали.

Для чистоты эксперимента рассмотрим частный случай.

Описание условий эксперимента:
Объект – (Поверхность текстурирования) плоскость, квадрат.
Расположение объекта – (изначальное) параллельно плоскости экрана, лицом к камере.
Карта нормалей – (текстура) для ее построения используем расчет, приведенный в моей статье «Calculation deep normal map».

Нам потребуется новый тип описания формата вершин, назовем его VertexPositionTangentSpaceTexture.

(код его реализации)
public struct VertexPositionTangentSpaceTexture
{
public Vector3 Position;
public Vector3 Normal;
public Vector3 Binormal;
public Vector3 Tangent;
public Vector2 TextureCoordinate;
public static readonly VertexElement[] VertexElements;
static VertexPositionTangentSpaceTexture()
{
VertexElements = new VertexElement[5];
short offset = 0;
// Position
VertexElements[0] = new VertexElement(
0, offset,
VertexElementFormat.Vector3,
VertexElementMethod.Default,
VertexElementUsage.Position,
0);
offset += (short)Marshal.SizeOf(new Vector3());
// Normal
VertexElements[1] = new VertexElement(
0, offset,
VertexElementFormat.Vector3,
VertexElementMethod.Default,
VertexElementUsage.Normal,
0);
offset += (short)Marshal.SizeOf(new Vector3());
// Binormal
VertexElements[2] = new VertexElement(
0, offset,
VertexElementFormat.Vector3,
VertexElementMethod.Default,
VertexElementUsage.Binormal,
0);
offset += (short)Marshal.SizeOf(new Vector3());
// Tangent
VertexElements[3] = new VertexElement(
0, offset,
VertexElementFormat.Vector3,
VertexElementMethod.Default,
VertexElementUsage.Tangent,
0);
offset += (short)Marshal.SizeOf(new Vector3());
// TextureCoordinate
VertexElements[4] = new VertexElement(
0, offset,
VertexElementFormat.Vector2,
VertexElementMethod.Default,
VertexElementUsage.TextureCoordinate,
0);
}
public VertexPositionTangentSpaceTexture(
Vector3 position,
Vector3 normal,
Vector3 binormal,
Vector3 tangent,
Vector2 textureCoordinate)
{
this.Position = position;
this.Normal = normal;
this.Binormal = binormal;
this.Tangent = tangent;
this.TextureCoordinate = textureCoordinate;
}
public static int SizeInBytes
{
get
{
return (int)( Marshal.SizeOf(new Vector3()) +
Marshal.SizeOf(new Vector3()) +
Marshal.SizeOf(new Vector3()) +
Marshal.SizeOf(new Vector3()) +
Marshal.SizeOf(new Vector2()));
}
}
}
В его описании поля: Normal, Binormal и Tangent. Послужат нам описанием локальной системы координат для каждой вершины, так называемый tangent space – пространство косательной. В данном примере мы не будем ударятся в оптимизацию и экономию. Потому для наглядности примера используем все три вектора и тем самым заострим внимание на самой сути Bump Map’инга и возможно это помежет кому либо избежать подводных камней при изучении этого примера.

Генерируем поверхность с вершинами типа VertexPositionTangentSpaceTexture.

(пример)
public void Quad()
{
float y0 = 0.5f;
float y1 = -y0;
float x0 = -0.5f;
float x1 = -x0;
float z0 = 0.5f;
float z1 = -z0;
z0 = 0; z1 = 0;
verts = new VertexPositionTangentSpaceTexture[4];
indices = new int[6];
int iv, ii;
iv = 0;
ii = 0;
// front
verts[iv + 0].Position = new Vector3(x0, y0, z0);
verts[iv + 1].Position = new Vector3(x1, y0, z0);
verts[iv + 2].Position = new Vector3(x0, y1, z0);
verts[iv + 3].Position = new Vector3(x1, y1, z0);
verts[iv + 0].Normal = Vector3.Backward;
verts[iv + 1].Normal = Vector3.Backward;
verts[iv + 2].Normal = Vector3.Backward;
verts[iv + 3].Normal = Vector3.Backward;
verts[iv + 0].Binormal = Vector3.Up;
verts[iv + 1].Binormal = Vector3.Up;
verts[iv + 2].Binormal = Vector3.Up;
verts[iv + 3].Binormal = Vector3.Up;
verts[iv + 0].Tangent = Vector3.Right;
verts[iv + 1].Tangent = Vector3.Right;
verts[iv + 2].Tangent = Vector3.Right;
verts[iv + 3].Tangent = Vector3.Right;
verts[iv + 0].TextureCoordinate = Vector2.Zero;
verts[iv + 1].TextureCoordinate = Vector2.UnitX;
verts[iv + 2].TextureCoordinate = Vector2.UnitY;
verts[iv + 3].TextureCoordinate = Vector2.One;
indices[ii + 0] = iv + 0;
indices[ii + 1] = iv + 1;
indices[ii + 2] = iv + 2;
indices[ii + 3] = iv + 2;
indices[ii + 4] = iv + 1;
indices[ii + 5] = iv + 3;
iv += 4;
ii += 6;
// verts
vertexBuffer = new VertexBuffer(
graphics.GraphicsDevice,
VertexPositionTangentSpaceTexture.SizeInBytes * verts.Length,
BufferUsage.None);
vertexBuffer.SetData(verts);
// index
indexBuffer = new IndexBuffer(
graphics.GraphicsDevice,
sizeof(int) * indices.Length,
BufferUsage.None, IndexElementSize.ThirtyTwoBits);
indexBuffer.SetData(indices);
vertexDeclaration = new VertexDeclaration(graphics.GraphicsDevice, VertexPositionTangentSpaceTexture.VertexElements);
}
}

Обращаю Ваше внимание на инициализацию опять же Normal, Binormal и Tangent. Фишка в том, что по условиям эксперимента я заранее знаю как будет расположен в пространстве текстурируемый объект. Потому так же знаю как будет расположенна нормаль к его лицевой стороне и соответствующие ей Binormal и Tangent.

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

Для чего нам это нужен tangent space?
Все просто. Как вы наверно знаете, в вершинном шейдере при обычном текстурировании, для правильного проецирования в плоскость экрана, мы выполняем следующие преобразования:

(пример)
Out.Position = mul( In.Position, matWVP );
Out.Light = normalize( vLight );
Out.Normal = normalize( mul( In.Normal, matWorld ) );
Out.Texcoord = In.Texcoord;

Т.е.
- преобразуем и проецируем координаты вершины в плоскость экрана матрицей matWVP (семантика :WORLDVIEWPROJECTION);
- преобразуем нормали матрицей matWorld (семантика :WORLD) – поворот нормалей для правильного расчета освещения в пиксельном шейдере;

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

(пример)
float3 vLight = mul(vLightPos, matWorldBack) - In.Position;
//float3 tangent = float3(1,0,0); \
//float3 binormal = float3(0,1,0); - в нашем частном случае
//float3 normal = float3(0,0,1); /
float3 normal = normalize(In.Normal);
float3 binormal = normalize(In.Binormal);
float3 tangent = normalize(In.Tangent);
float3x3 matTS = float3x3(tangent, binormal, normal);
Out.vLightTS = normalize(mul(vLight, matTS));

Мы поворачиваем вектор источника света vLightPos в соответствии с преобразованиями накопленными в матрице matWorldBack. Эта матрица является обратной матрицей для мировой матрицы объекта. Прернос и масштабирование в данном случае нас не интересуют, потому как vLightPos вектор, а не координата, и мы нормализуем итоговый результат.
Что значит обратная матрица? Матрица в которой собраны преобразования обратные оригинальной матрице. В данном случае с помощью обратной матрицы мы вращаем вектор освещения в сторону обратную вращению нашего объекта.
Получить обратную матрицу средствами XNA очень просто:
matWorldBack = Matrix.Invert(matWorld);
Далее переводим координаты источника света (в статичную!) локальную систему координат очередной вершины действиями:
float3 normal = normalize(In.Normal);
float3 binormal = normalize(In.Binormal);
float3 tangent = normalize(In.Tangent);
float3x3 matTS = float3x3(tangent, binormal, normal);
Out.vLightTS = normalize(mul(vLight, matTS));
Где, matTS – матрица описывающая tangent space вершины.

После чего мы готовы к текстурированию с учетом освещения и псевдо рельефа.
В пиксельном шейде (пример):
float4 ps(PS_INPUT In) : COLOR0
{
float3 vNormalTS = tex2D( sTexture0, In.Texcoord ) * 2 - 1;
float4 color = float4(0.8f, 0.8f, 0, 1);
float3 vlightTS = In.vLightTS;

float light = clamp(dot(vNormalTS, vlightTS), 0, 1);
return mul(color, light);
}
Стоит обратить внимание на две строчки:
1) float3 vNormalTS = tex2D( sTexture0, In.Texcoord ) * 2 - 1;
где, мы берем нормаль к поверхности в данном пикселе текстуры и распаковываем ее в первоначальный вид, т.к. в normal map у нас хранятся данные о (естественно) нормалях, но упакованными по условной формуле:
color = normal * 0.5f + vector(0.5f, 0.5f, 0.5f);
Подробнее смотреть статью о расчете - «Calculation deep normal map».
2) float light = clamp(dot(vNormalTS, vlightTS, 0, 1);
сам расчет интенсивности освещения для данного пикселя текстуры.

(полный пример шейдера)
float4x4 matWorld : WORLD0;
float4x4 matWorldBack : WORLD1;
float4x4 matWVP : WORLDVIEWPROJECTION0;
float3 vLightPos;
float3 vViewPos;
struct VS_INPUT
{
float3 Position : POSITION0;
float3 Normal : NORMAL0;
float3 Binormal : BINORMAL0;
float3 Tangent : TANGENT0;
float2 Texcoord : TEXCOORD0;
};
struct VS_OUTPUT
{
float4 Position : POSITION0;
float2 Texcoord : TEXCOORD0;
float3 vLightTS : TEXCOORD3;
};
VS_OUTPUT vs( VS_INPUT In )
{
float3 vLight = mul(vLightPos, matWorldBack) - In.Position;
float3 normal = normalize(In.Normal);
float3 binormal = normalize(In.Binormal);
float3 tangent = normalize(In.Tangent);
float3x3 matTS = float3x3( tangent, binormal, normal );
VS_OUTPUT Out = ( VS_OUTPUT ) 0;
Out.Position = mul(float4( In.Position, 1 ), matWVP);
Out.Texcoord = In.Texcoord;
Out.vLightTS = normalize(mul(vLight, matTS));
return Out;
}
texture tex0;
sampler2D sTexture0 = sampler_state
{
Texture = (tex0);
MinFilter = LINEAR;
MagFilter = LINEAR;
MipFilter = LINEAR;
AddressU = CLAMP;
AddressV = CLAMP;
};
struct PS_INPUT
{
float2 Texcoord : TEXCOORD0;
float3 vLightTS : TEXCOORD3;
};
float4 ps(PS_INPUT In) : COLOR0
{
float3 vNormalTS = tex2D( sTexture0, In.Texcoord ) * 2 - 1;
float4 color = float4(0.8f, 0.8f, 0, 1);
float3 vlightTS = In.vLightTS;
float light = clamp(dot(vNormalTS, vlightTS), 0, 1);
return mul(color, light);
}
{
pass Pass_0
{
VertexShader = compile vs_2_0 vs();
PixelShader = compile ps_2_0 ps();
}
}

Результат:



Абсолютно прозрачные и простые примеры это хорошо. Но закончить статью хочется ярче. Потому поковыряв в приложении к RenderMonkey пример «Parallax Occlusion Mapping». Я добавил бликовую модель освещения и тут же картинка (ее можно увидеть в начале статьи) стала живее.
(пример шейдера)

float4x4 matWorld : WORLD0;
float4x4 matWorldBack : WORLD1;
float4x4 matWVP : WORLDVIEWPROJECTION0;
float3 vLightPos;
float3 vViewPos;
struct VS_INPUT
{
float3 Position : POSITION0;
float3 Normal : NORMAL0;
float3 Binormal : BINORMAL0;
float3 Tangent : TANGENT0;
float2 Texcoord : TEXCOORD0;
};
struct VS_OUTPUT
{
float4 Position : POSITION0;
float2 Texcoord : TEXCOORD0;
float3 vNormalWS : TEXCOORD1;
float3 vViewWS : TEXCOORD2;
float3 vLightTS : TEXCOORD3;
float3 vViewTS : TEXCOORD4;
};
VS_OUTPUT vs( VS_INPUT In )
{
float3 vLight = mul(vLightPos, matWorldBack) - In.Position;
float3 vViewDir = mul(vViewPos, matWorldBack) - In.Position;
float3 normal = normalize(In.Normal);
float3 binormal = normalize(In.Binormal);
float3 tangent = normalize(In.Tangent);
float3x3 matTS = float3x3( tangent, binormal, normal );
VS_OUTPUT Out = ( VS_OUTPUT ) 0;
Out.Position = mul(float4( In.Position, 1 ), matWVP);
Out.Texcoord = In.Texcoord;
Out.vNormalWS = In.Normal;
Out.vViewWS = vViewDir;
Out.vLightTS = normalize(mul(vLight, matTS));
Out.vViewTS = normalize(mul(vViewDir, matTS));
return Out;
}
texture tex0;
sampler2D sTexture0 = sampler_state
{
Texture = (tex0);
MinFilter = LINEAR;
MagFilter = LINEAR;
MipFilter = LINEAR;
AddressU = CLAMP;
AddressV = CLAMP;
};
struct PS_INPUT
{
float2 Texcoord : TEXCOORD0;
float3 vNormalWS : TEXCOORD1;
float3 vViewWS : TEXCOORD2;
float3 vLightTS : TEXCOORD3;
float3 vViewTS : TEXCOORD4;
};
float4 cSpecularColor = float4(1, 1, 1, 1);
float fSpecularExponent = 300;
float4 ps(PS_INPUT In) : COLOR0
{
float3 vNormalTS = tex2D( sTexture0, In.Texcoord ) * 2 - 1;
float4 color = float4(0.8f, 0.8f, 0, 1);
float3 vlightTS = In.vLightTS;
float3 vReflectionTS = normalize( 2 * dot( In.vViewTS, vNormalTS ) * vNormalTS - In.vViewTS );
float fRdotL = dot( vReflectionTS, In.vLightTS );
float4 cSpecular = saturate( pow( fRdotL, fSpecularExponent )) * cSpecularColor;
float light = clamp(dot(vNormalTS, vlightTS) * (color + cSpecular), 0, 1);
return mul(color, light);
}
technique BumpMapping
{
pass Pass_0
{
VertexShader = compile vs_2_0 vs();
PixelShader = compile ps_2_0 ps();
}
}

Скачать пример можно тут (требуется регистрация xnadev.ru) (чуть позже, в оригинальном блоге, ссылка станет доступна)

Оригинал блога

понедельник, 15 июня 2009 г.

Effect “Edge Detect”

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

Суть в следующем. Рендеринг выполняется в два прохода, потому для этого нам понадобятся два файла эффектов PositionNormal.fx и EdgeDetect.fx с различными техниками.

I. Как вы уже догадались первой техникой мы будем рисовать normal map.

Т.е. в эффекте PositionNormal.fx вместо цвета будем сохранять данные о нормали к данной точке. Но в следствии того, что каждый компонент цвета может хранить число из интервала {0, 1}, а нормали могут иметь отрицательные компоненты, то перед сохранением нормаль упакуем старым добрым способом: 
Out.Normal = mul(normal, 0.5f) + 0.5f;

1) Очищаем экран в цвет Color(0.5f, 0.5f, 0f, 1f), на самом деле это не цвет а так же упокованная нормаль (0f, 0f, -1f, 1f).

2) Рисуем сцену первый раз при помощи PositionNormal.fx
(Текст эффекта)
float4x4 matWorld : WORLD;
float4x4 matWVP : WORLDVIEWPROJECTION; 
struct VS_INPUT 
{
  float4 Position : POSITION0;
  float3 Normal : NORMAL0;
};
struct VS_OUTPUT 
{
  float4 Position : POSITION0;
  float3 Normal : COLOR0;
};
VS_OUTPUT vs_main( VS_INPUT In )
{
  VS_OUTPUT Out = ( VS_OUTPUT ) 0;
  Out.Position = mul( In.Position, matWVP );
  float3 normal = normalize(mul(In.Normal, matWorld)); 
  Out.Normal = mul(normal, 0.5f) + 0.5f;
  return Out;
}
struct PS_INPUT 
{
  float3 Normal : COLOR0;
};

float4 ps_main(PS_INPUT In) : COLOR0
{  
 return float4(In.Normal, 1.0f);
}
technique PositionNormal
{
  pass Pass_0
  {
  VertexShader = compile vs_3_0 vs_main();
  PixelShader = compile ps_3_0 ps_main();
  }
}
все просто, стоит только заострить внимание на двух строчках:
...
Out.Normal = mul(normal, 0.5f) + 0.5f;
...
return float4(In.Normal, 1.0f);
...

3) Нарисованную сцену сохраняем в текстуру

II. Анализируя полученную normal map, выводим контуры объектов сцены.

У меня есть несколько вариаций техник для этого эффекта.
Различаются они сложностью геометрии сцены и подходами сравнения нормалей соседних пикселей.
Своем пример я не стал усложнять и рассмотрел случай, когда фигура на сцене имеет гладкие грани (по всей плоскости грани, нормали имеют одинаковое значение).
Рисуем при помощи EdgeDetect.fx
(Текст эффекта)

float dx;
float dy;
texture ScreenTexture; 
sampler ScreenS = sampler_state
{
 Texture =
};
float4 PS(float2 texCoord: TEXCOORD0) : COLOR
{
 float4 normal = tex2D(ScreenS, texCoord); 
 float4 normalLeft = tex2D(ScreenS, float2(texCoord.x - dx, texCoord.y));
 float4 normalRight = tex2D(ScreenS, float2(texCoord.x + dx, texCoord.y));
 float4 normalUp = tex2D(ScreenS, float2(texCoord.x, texCoord.y - dy));
 float4 normalDown = tex2D(ScreenS, float2(texCoord.x, texCoord.y + dy));
 
 float4 normalSum = mul(normalLeft + normalRight + normalUp + normalDown, 0.25f);
 
 if(normal.x == normalSum.x && normal.y == normalSum.y && normal.z == normalSum.z)
  return normal;
 else
  return float4(1.0f, 1.0f, 1.0f ,1.0f); // цвет контура
}
technique EdgeDetect
{
 pass P0
 {
  PixelShader = compile ps_3_0 PS();
 }
}

где:
dx, dy - задают толщину линии контура.

В этом примере я не стал распаковывать сравниваемые нормали normal и normalSum.
Тут этого не требовалось, и мы не будем перегружать пиксельный шейдер.
Распаковка типа:
normal = (normal - 0.5f) * 2.0f;
Может потребоваться тогда, когда Вам нужно будет анализировать normal map с отображенными объектами, поверхности которых изогнуты.

В примере кроме контура объекта выводятся цветные грани, не пугайтесь, это для наглядности я отобразил нормали.

Скачать пример можно тут (для скачивания требуется регистрация на xnadev.ru). 

На реализацию эффекта вдохновило демо, группы ASD.