Модель освещения Гуч (Gooch)

О реализации некоторых нефотореалистичных эффектов. Модель освещения Гуч (Gooch)

В этой статье мы рассмотрим еще одну нефотореалистичную модель освещения – модель Гуч (Gooch), названную в честь Эми Гуч (известной, кроме прочего, как соавтор одной из немногих книг по нефотореалистичной компьютерной графике Non-Photorealistic Rendering Bruce Gooch and Amy Gooch).

Данная модель освещения подробно рассмотрена в «A Non-Photorealistic Lighting Model For Automatic Technical Illustration«, by Amy Gooch, Bruce Gooch, Peter Shirley.Elaine Cohen, SIGGRAPH 98. Правда в статье большое внимание уделено моделированию данного алгоритма на основе систем, использующий модель Фонга. Сейчас же в таком подходе нет необходимости посколько легко можно разработать шейдер, подменяющий стандартную модель освещения. Этим мы сейчас и займемся.

Модель Гуч позволяет более четко отображать контуры объекта.


Рассмотрим чайник освещенный по модели Фонга с практически отсутствующей рассеянной (Ambient) составляющей. Неосвещенная левая часть чайника сливается с фоном и становится невозможный определение границ объекта.

Модель Гуч базируется на смешивании холодного и теплого цветов в зависимости от диффузной составляющей.


Где I · n – скалярное произведение вектора нормали и вектора направления освещения (вообще, в оригинальной статье под вектором I понималось направление на источник света, но в таком случае получается неправильный результат, сейчас при реализации данного эффекта используется несколько другая формула, в которой kcool и kwarm поменяны местами, а I действительно является направлением на источник света).

kcool – холодный цвет (синий, фиолетовый и т.д.).

kwarm – теплый цвет (красный, оранжевый, желтый).

Получается, что для точек, в которых скалярное произведение положительно (то есть они освещены), больший вклад в результирующий цвет вносит теплый цвет, а для точек, лежащих с «обратной» стороны объекта (нормаль в которых сонаправлена с направлением освещения), больший вклад вносит холодный цвет. Таким образом создается интересный эффект, как будто свет, проходящий через объект меняет свой цвет. Также объединение двух цветов помогает выделить границы объекта: нет больше областей не имеющих цвета (окрашенных в черный).

Напишем простой шейдер, реализующий формулу:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0

{

// TODO: add your pixel shader code here.

float3 worldPosition = input.WorldPosition;

float3 worldNormal = normalize(input.Normal);

float3 lightDirection = normalize(LightPosition — worldPosition);

float NL = dot(worldNormal, lightDirection);

float3 warmColor = float3(0.6, 0.6, 0.0);

float3 coldColor = float3(0, 0.0, 0.8);

float3 result = lerp(coldColor, warmColor, (NL + 1)/2.0);

return float4(saturate(result), 1);;

}


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

Обратите внимание на то, что мы используем вектор направленный на источник света для вычислений, а фунцкия lerp эквивалентна coldColor * (1-(NL + 1)/2.0) + warmColor * (NL + 1)/2.0. То есть мы поменяли местами холодный и теплый цвета.

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


Где kd – собственный цвет объекта, и



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

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0

{

// TODO: add your pixel shader code here.

float3 worldPosition = input.WorldPosition;

float3 worldNormal = normalize(input.Normal);

float3 lightDirection = normalize(LightPosition — worldPosition);

float NL = dot(worldNormal, lightDirection);

float3 eyeDirection = normalize(Eye — worldPosition);

float3 reflectedLight = normalize(reflect(-lightDirection, worldNormal));

float Specular = ks * pow(max(0, dot(eyeDirection, reflectedLight)), SpecularPower);

float3 warmColor = float3(0.6, 0.6, 0.0);

float3 coldColor = float3(0, 0.0, 0.8);

float alpha = 0.6;

float betta = 0.6;

float3 cold = saturate(coldColor + alpha * Color);

float3 warm = saturate(warmColor + betta * Color);

float3 result = lerp(cold, warm, (NL + 1)/2.0);

return float4(saturate(result), 1) + Specular;

}

Собственный цвет пускай будет немного красноватым:

float4 Color = float4(0.3, 0.0, 0.0, 1);


Теперь я изменю немного метод Draw, чтобы можно было посмотреть на чайник с другой стороны (то есть так, чтобы чайник находился между камерой и источником света) и увидеть эффект прохождения света через объект.

protected
override
void Draw(GameTime gameTime)

{

GraphicsDevice.Clear(Color.Black);

Matrix world = Matrix.CreateRotationY(-(float)gameTime.TotalGameTime.TotalSeconds) * Matrix.CreateTranslation(-1.0f, 0, 0) * Matrix.CreateRotationY(-(float)gameTime.TotalGameTime.TotalSeconds);

Matrix view = Matrix.CreateLookAt(new
Vector3(0, 1, 4), Vector3.Zero, Vector3.Up);

Matrix proj = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 10);

effect.CurrentTechnique = effect.Techniques[«Phong»];

effect.Parameters[«World»].SetValue(world);

effect.Parameters[«View»].SetValue(view);

effect.Parameters[«Projection»].SetValue(proj);

effect.Parameters[«Eye»].SetValue(new
Vector3(0, 1, 4));

teapot.Draw(effect);

world = Matrix.CreateRotationY(-(float)gameTime.TotalGameTime.TotalSeconds) * Matrix.CreateTranslation(1.0f, 0, 0) * Matrix.CreateRotationY(-(float)gameTime.TotalGameTime.TotalSeconds);

effect.CurrentTechnique = effect.Techniques[«Gooch»];

effect.Parameters[«World»].SetValue(world);

effect.Parameters[«View»].SetValue(view);

effect.Parameters[«Projection»].SetValue(proj);

effect.Parameters[«Eye»].SetValue(new
Vector3(0, 1, 4));

teapot.Draw(effect);

base.Draw(gameTime);

}


Или так, при других значениях параметров:


Исходный код:

using System;

using System.Collections.Generic;

using System.Linq;

using Microsoft.Xna.Framework;

using Microsoft.Xna.Framework.Audio;

using Microsoft.Xna.Framework.Content;

using Microsoft.Xna.Framework.GamerServices;

using Microsoft.Xna.Framework.Graphics;

using Microsoft.Xna.Framework.Input;

using Microsoft.Xna.Framework.Media;

using Microsoft.Xna.Framework.Net;

using Microsoft.Xna.Framework.Storage;

using Primitives3D;

namespace NPR_1

{


///
<summary>


/// This is the main type for your game


///
</summary>


public
class
Game1 : Microsoft.Xna.Framework.Game

{


GraphicsDeviceManager graphics;


SpriteBatch spriteBatch;


TeapotPrimitive teapot;


Effect effect;


public Game1()

{

graphics = new
GraphicsDeviceManager(this);

Content.RootDirectory = «Content»;

}


///
<summary>


/// Allows the game to perform any initialization it needs to before starting to run.


/// This is where it can query for any required services and load any non-graphic


/// related content. Calling base.Initialize will enumerate through any components


/// and initialize them as well.


///
</summary>


protected
override
void Initialize()

{


// TODO: Add your initialization logic here

teapot = new
TeapotPrimitive(GraphicsDevice);


base.Initialize();

}


///
<summary>


/// LoadContent will be called once per game and is the place to load


/// all of your content.


///
</summary>


protected
override
void LoadContent()

{


// Create a new SpriteBatch, which can be used to draw textures.

spriteBatch = new
SpriteBatch(GraphicsDevice);


// TODO: use this.Content to load your game content here

effect = Content.Load<Effect>(«light»);

}


///
<summary>


/// UnloadContent will be called once per game and is the place to unload


/// all content.


///
</summary>


protected
override
void UnloadContent()

{


// TODO: Unload any non ContentManager content here

}


///
<summary>


/// Allows the game to run logic such as updating the world,


/// checking for collisions, gathering input, and playing audio.


///
</summary>


///
<param name=»gameTime»>Provides a snapshot of timing values.</param>


protected
override
void Update(GameTime gameTime)

{


// Allows the game to exit


if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)


this.Exit();


// TODO: Add your update logic here


base.Update(gameTime);

}


///
<summary>


/// This is called when the game should draw itself.


///
</summary>


///
<param name=»gameTime»>Provides a snapshot of timing values.</param>


protected
override
void Draw(GameTime gameTime)

{

GraphicsDevice.Clear(Color.Black);


// TODO: Add your drawing code here


Matrix world = Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds) * Matrix.CreateTranslation(-1.0f, 0, 0);


Matrix view = Matrix.CreateLookAt(new
Vector3(0, 1, 4), Vector3.Zero, Vector3.Up);


Matrix proj = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 10);

effect.CurrentTechnique = effect.Techniques[«Phong»];

effect.Parameters[«World»].SetValue(world);

effect.Parameters[«View»].SetValue(view);

effect.Parameters[«Projection»].SetValue(proj);

effect.Parameters[«Eye»].SetValue(new
Vector3(0, 1, 4));

teapot.Draw(effect);

world = Matrix.CreateRotationY(-(float)gameTime.TotalGameTime.TotalSeconds) * Matrix.CreateTranslation(1.0f, 0, 0);

effect.CurrentTechnique = effect.Techniques[«Gooch»];

effect.Parameters[«World»].SetValue(world);

effect.Parameters[«View»].SetValue(view);

effect.Parameters[«Projection»].SetValue(proj);

effect.Parameters[«Eye»].SetValue(new
Vector3(0, 1, 4));

teapot.Draw(effect);


base.Draw(gameTime);

}

}

}

Шейдер:

float4x4 World;

float4x4 View;

float4x4 Projection;

float4 AmbientColor = float4(0.1, 0.1, 0.1, 1);

float ka = 0;

float4 DiffuseColor = float4(0, 0, 1, 1);

float kd = 0.7;

float4 SpecularColor = float4(1, 0, 0, 1);

float ks = 1;

float SpecularPower = 8;

float3 LightPosition = float3(0, 0.5, 1);

float3 Eye;

texture LightMask;

sampler LigthMaskSampler=sampler_state

{

texture = <LightMask>;

};

float4 Color = float4(0.3, 0, 0, 1);

// TODO: add effect parameters here.

struct VertexShaderInput

{

float4 Position : POSITION0;

float3 Normal : NORMAL;

// TODO: add input channels such as texture

// coordinates and vertex colors here.

};

struct VertexShaderOutput

{

float4 Position : POSITION0;

float3 WorldPosition : TEXCOORD0;

float3 Normal : TEXCOORD1;

// TODO: add vertex shader outputs such as colors and texture

// coordinates here. These values will automatically be interpolated

// over the triangle, and provided as input to your pixel shader.

};

VertexShaderOutput VertexShaderFunctionPhong(VertexShaderInput input)

{

VertexShaderOutput output;

float4 worldPosition = mul(input.Position, World);

float4 viewPosition = mul(worldPosition, View);

float3 worldNormal = normalize(mul(input.Normal, World));

output.Position = mul(viewPosition, Projection);

output.WorldPosition = worldPosition;

output.Normal = worldNormal;

// TODO: add your vertex shader code here.

return output;

}

float4 PixelShaderFunctionPhong(VertexShaderOutput input) : COLOR0

{

// TODO: add your pixel shader code here.

float3 worldPosition = input.WorldPosition;

float3 worldNormal = normalize(input.Normal);

float4 Ambient = ka * AmbientColor;

float3 lightDirection = normalize(LightPosition — worldPosition);

float4 Diffuse = kd * max(0, dot(worldNormal, lightDirection)) * DiffuseColor;

float3 eyeDirection = normalize(Eye — worldPosition);

float3 reflectedLight = normalize(reflect(-lightDirection, worldNormal));

float4 Specular = ks * pow(max(0, dot(eyeDirection, reflectedLight)), SpecularPower) * SpecularColor;

return Color + Ambient + Diffuse + Specular;

}

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)

{

VertexShaderOutput output;

float4 worldPosition = mul(input.Position, World);

float4 viewPosition = mul(worldPosition, View);

float3 worldNormal = normalize(mul(input.Normal, World));

output.Position = mul(viewPosition, Projection);

output.WorldPosition = worldPosition;

output.Normal = worldNormal;

// TODO: add your vertex shader code here.

return output;

}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0

{

// TODO: add your pixel shader code here.

float3 worldPosition = input.WorldPosition;

float3 worldNormal = normalize(input.Normal);

float3 lightDirection = normalize(LightPosition — worldPosition);

float NL = dot(worldNormal, lightDirection);

float3 eyeDirection = normalize(Eye — worldPosition);

float3 reflectedLight = normalize(reflect(-lightDirection, worldNormal));

float Specular = ks * pow(max(0, dot(eyeDirection, reflectedLight)), SpecularPower);

float3 warmColor = float3(0.6, 0.6, 0.0);

float3 coldColor = float3(0, 0.0, 0.8);

float alpha = 0.6;

float betta = 0.6;

float3 cold = saturate(coldColor + alpha * Color);

float3 warm = saturate(warmColor + betta * Color);

float3 result = lerp(cold, warm, (NL+1)/2.0);

return float4(saturate(result), 1) + Specular;

}

technique Phong

{

pass Pass1

{

// TODO: set renderstates here.

VertexShader = compile vs_1_1 VertexShaderFunctionPhong();

PixelShader = compile ps_2_0 PixelShaderFunctionPhong();

}

}

technique Gooch

{

pass Pass1

{

// TODO: set renderstates here.

VertexShader = compile vs_1_1 VertexShaderFunction();

PixelShader = compile ps_2_0 PixelShaderFunction();

}

}

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

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

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

Логотип WordPress.com

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

Фотография Twitter

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

Фотография Facebook

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

Google+ photo

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

Connecting to %s