Подсветка края. Rim lighting

Подсветка края. Rim lighting

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



  • N – нормаль к поверхности
  • L – направление к источнику света
  • R – направление отраженного луча
  • V – направление к наблюдателю

Каждый из векторов является единичным.

Модель освещения Ламберта описывается следующей формулой:


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


В реальных приложениях обычно вводят дополнительный параметр, назовем его — яркость краев, как обычно имеет смысл поэкспериментировать с этим параметром.


Изменим немного наш шейдер, так чтобы он реализовывал описанные формулы (нам также понадобится позиция наблюдателя):

float3 Eye;

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

float RimPower;

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0

{

// TODO: add your pixel shader code here.

float3 worldPosition = input.WorldPosition;

float3 worldNormal = normalize(input.Normal);

float4 Ambient = ka * AmbientColor;

float3 eyeDirection = normalize(Eye — worldPosition);

float4 Rim = pow(1 — max(0,dot(worldNormal, eyeDirection)), RimPower) * RimColor;

return Ambient + Rim;

}

А в Game1.cs, зададим новые параметры шейдера и напишем код для управления новым параметром – яркостью краев:


KeyboardState oldState;


float rimPower = 1;


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


KeyboardState state = Keyboard.GetState();


if (state.IsKeyDown(Keys.Z) && oldState.IsKeyUp(Keys.Z))

{

rimPower = rimPower — 0.1f;

}


if (state.IsKeyDown(Keys.X) && oldState.IsKeyUp(Keys.X))

{

rimPower = rimPower + 0.1f;

}


DebugInfo.SetVariable(«RimPower», rimPower.ToString());

oldState = state;


base.Update(gameTime);

}


protected
override
void Draw(GameTime gameTime)

{

GraphicsDevice.Clear(Color.Black);


Matrix view = camera.View;


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


// TODO: Add your drawing code here

effect.Parameters[«World»].SetValue(Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds));

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

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

effect.Parameters[«LightPosition»].SetValue(light.Position);

effect.Parameters[«Eye»].SetValue(camera.Position);

effect.Parameters[«RimPower»].SetValue(rimPower);

teapot.Draw(effect);

light.Draw(view, proj);


base.Draw(gameTime);

}


Теперь имеет смысл попробовать объединить полученный эффект с моделью освещения Ламберта. Для этого вернем диффузную составляющую в шейдер.

float4 PixelShaderFunction(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);

float4 Rim = pow(1 — max(0,dot(worldNormal, eyeDirection)), RimPower) * RimColor;

return Ambient + Rim + Diffuse;

}

Получится примерно такая картинка:


Можно также добавить в полученный эффект блики по модели Блинна-Фонга (раз у нас все равно есть позиция наблюдателя в шейдере).

Привожу код измененного пиксельного шейдера, изменения в Game1 практически не требуются.

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

float ks = 1;

float SpecularPower = 8;

float4 PixelShaderFunction(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);

float4 Rim = pow(1 — max(0,dot(worldNormal, eyeDirection)), RimPower) * RimColor;

float3 halfv = normalize((lightDirection + eyeDirection))    ;

float4 Specular = ks * pow(max(0, dot(worldNormal, halfv)), SpecularPower) * SpecularColor;

return Ambient + Rim + Diffuse + Specular;

}


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

Game1.cs

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 MyGame

{


///
<summary>


/// This is the main type for your game


///
</summary>


public
class
Game1 : Microsoft.Xna.Framework.Game

{


GraphicsDeviceManager graphics;


SpriteBatch spriteBatch;


Light light;


Effect effect;


TeapotPrimitive teapot;


FreeCamera camera;


KeyboardState oldState;


float rimPower = 1;


float specularPower = 8;


public Game1()

{

graphics = new
GraphicsDeviceManager(this);

Content.RootDirectory = «Content»;

graphics.SynchronizeWithVerticalRetrace = false;

IsFixedTimeStep = false;

}


///
<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

Components.Add(new
FPSCounter(this));

light = new
Light(this);

light.DefaultPosition = new
Vector3(1, 1, 1);

Components.Add(light);

camera = new
FreeCamera(this, new
Vector3(0, 0, 2), Vector3.Zero);

Components.Add(camera);

Components.Add(new
DebugInfo(this));

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


KeyboardState state = Keyboard.GetState();


if (state.IsKeyDown(Keys.Z) && oldState.IsKeyUp(Keys.Z))

{

rimPower = rimPower — 0.1f;

}


if (state.IsKeyDown(Keys.X) && oldState.IsKeyUp(Keys.X))

{

rimPower = rimPower + 0.1f;

}


if (state.IsKeyDown(Keys.C) && oldState.IsKeyUp(Keys.C))

{

specularPower = specularPower — 1f;

}


if (state.IsKeyDown(Keys.V) && oldState.IsKeyUp(Keys.V))

{

specularPower = specularPower + 1f;

}


DebugInfo.SetVariable(«RimPower», rimPower.ToString());


DebugInfo.SetVariable(«SpecularPower», specularPower.ToString());

oldState = state;


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);


Matrix view = camera.View;


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


// TODO: Add your drawing code here

effect.Parameters[«World»].SetValue(Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds));

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

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

effect.Parameters[«LightPosition»].SetValue(light.Position);

effect.Parameters[«Eye»].SetValue(camera.Position);

effect.Parameters[«RimPower»].SetValue(rimPower);

effect.Parameters[«SpecularPower»].SetValue(specularPower);

teapot.Draw(effect);

light.Draw(view, proj);


base.Draw(gameTime);

}

}

}

Light.cs

float4x4 World;

float4x4 View;

float4x4 Projection;

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

float ka = 1;

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

float kd = 1;

float3 LightPosition;

float3 Eye;

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

float RimPower;

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

float ks = 1;

float SpecularPower = 8;

// 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 VertexShaderFunction(VertexShaderInput input)

{

VertexShaderOutput output;

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

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

float4 viewPosition = mul(worldPosition, View);

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);

float4 Ambient = ka * AmbientColor;

float3 lightDirection = normalize(LightPosition — worldPosition);

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

float3 eyeDirection = normalize(Eye — worldPosition);

float4 Rim = pow(1 — max(0,dot(worldNormal, eyeDirection)), RimPower) * RimColor;

float3 halfv = normalize((lightDirection + eyeDirection))    ;

float4 Specular = ks * pow(max(0, dot(worldNormal, halfv)), SpecularPower) * SpecularColor;

return Ambient + Rim + Diffuse + Specular;

}

technique Light

{

pass Pass1

{

// TODO: set renderstates here.

VertexShader = compile vs_1_1 VertexShaderFunction();

PixelShader = compile ps_2_0 PixelShaderFunction();

}

}

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

2 комментария на «Подсветка края. Rim lighting»

  1. Как можно выделить контур сплошной линией? Может быть так?
    float4 Rim = max(1, 1 — max (0, (1 — max(0,dot(worldNormal, eyeDirection)) — 0.95) * RimColor;
    Таким образом, в сплошной цвет будут закрашены пиксели, близость к контуру которых от 0 до 0.05

    • О выделении контуров сплошными линиями у меня в блоге есть много других статей.
      А эту формулу рекомендую просто проверить, это самый простой вариант

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

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

Логотип WordPress.com

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

Фотография Twitter

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

Фотография Facebook

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

Google+ photo

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

Connecting to %s