Реализация точечного источника света в XNA 4.0. Часть 2

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

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

Напомню несколько основных моментов о законе Ламберта.

Закон Ламберта — физический закон, согласно которому яркость L рассеивающей свет (диффузной) поверхности одинакова во всех направлениях.

Имеется также простая зависимость между силой света, излучаемого плоской рассеивающей площадкой dS в каком-либо направлении, от угла α между этим направлением и перпендикуляром к dS:

Iα = I0cosα.

Последнее выражение означает, что сила света плоской поверхности максимальна (I0) по перпендикуляру к ней и, убывая с увеличением α, становится равной нулю в касательных к поверхности направлениях.

Теперь перейдем к реализации.

Во-первых, теперь нам нужно модифицировать входную и выходную структуры для вершинного шейдера. Нужно передавать нормаль в вершинный шейдер, для того чтобы вычислять нормаль в мировых координатах и передавать их дальше – в пиксельный шейдер.

struct VertexShaderInput

{

float4 Position : POSITION0;

    float3 Normal : NORMAL0;

// TODO: add input channels such as texture

// coordinates and vertex colors here.

};

struct VertexShaderOutput

{

float4 Position : POSITION0;

          float3 WorldNormal : NORMAL0;

float3 WorldPosition : 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;

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

float4 viewPosition = mul(worldPosition, View);

output.Position = mul(viewPosition, Projection);

// TODO: add your vertex shader code here.

output.WorldPosition = worldPosition;

          output.WorldNormal = normalize(mul(input.Normal, World));

return output;

}

Дальше нужно изменить пиксельный шейдер в соответствии с моделью Ламберта.

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    float4 color = Color;
          float3 light = float3(0.1f,0.1f,0.1f);

          float3 normal = normalize(input.WorldNormal);
           float3 position = input.WorldPosition;  
    if (xLightsEnabled)
    for (int i = 0; i < xLightCount; i++)
          {
                    float dist = distance(xLights[i], input.WorldPosition);
                     float3 lightDirection = normalize(xLights[i] - position);
                     light += max(0, dot(normal, lightDirection) / (dist*dist) / 5);
           }

          //        light += (1 - saturate(distance(xLights[i], input.WorldPosition) * 1.5));

          color.rgb *= light;

    return color;
}

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

Также я изменю количество источников света и их позиции для того, чтобы эффект был виден более отчетливо.

Еще я добавлю управляемую свободную камеру, чтобы потом можно было взглянуть на сцену под разными углами.

FreeCamera camera;

protected override void Initialize()

{

// TODO: Add your initialization logic here

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

Components.Add(camera);

base.Initialize();

}

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

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

    primitives.Add(new GameObject(new CubePrimitive(GraphicsDevice, 6), new Vector3(0, -4, 0), Color.Green));

primitives.Add(new GameObject(new TeapotPrimitive(GraphicsDevice, 0.3f, 40), new Vector3(-0.5f, -0.9f, 1), Color.Yellow));

primitives.Add(new GameObject(new TeapotPrimitive(GraphicsDevice, 1f, 40), new Vector3(1, -0.65f, -1), Color.Red));

primitives.Add(new GameObject(new TeapotPrimitive(GraphicsDevice, 0.3f, 40), new Vector3(-0.5f, -0.9f, 2), Color.Tomato));

primitives.Add(new GameObject(new TeapotPrimitive(GraphicsDevice, 0.3f, 40), new Vector3(0.5f, -0.9f, 2f), Color.Gray));

primitives.Add(new GameObject(new TeapotPrimitive(GraphicsDevice, 0.3f, 40), new Vector3(0.5f, -0.9f, 1), Color.Brown));

    for (int i = -2; i <= 2; i++)

{

for (int j = -2; j <= 2; j++)

{

var pos = new Vector3(i, -0.9f, j);

pointLights.Add(pos);

primitives.Add(new GameObject(new SpherePrimitive(GraphicsDevice, 0.1f, 10), pos, Color.White));

}

}

}

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 world = Matrix.Identity;

    Matrix view = camera.View;

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

lightEffect.Parameters[«xLights»].SetValue(pointLights.ToArray());

lightEffect.Parameters[«xLightCount»].SetValue(pointLights.Count);

lightEffect.Parameters[«xLightsEnabled»].SetValue(true);

foreach (var item in primitives)

{

item.Draw(world, view, projection, lightEffect);

}

base.Draw(gameTime);

}

Пока что мы использовали очень простую модель освещения, которая не учитывала зеркальную компоненту освещенности.

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

Модифицируем наш шейдер:

float3 Eye;

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0

{

float4 color = Color;

float3 light = float3(0.1f,0.1f,0.1f);

          float3 lightSpecular = float3(0,0,0);

float3 normal = normalize(input.WorldNormal);

float3 position = input.WorldPosition;

          float3 view = normalize(Eye — position);

if (xLightsEnabled)

for (int i = 0; i < xLightCount; i++)

{

float dist = distance(xLights[i], input.WorldPosition);

float3 lightDirection = normalize(xLights[i] — position);

light += max(0, dot(normal, lightDirection) / (dist*dist) / 5);

                    float3 halfVector = normalize(lightDirection + view);

lightSpecular += pow(max(0.000001f,dot(halfVector, normal) / (dist*dist) / 6), 24);

}

          color.rgb *= (light  + lightSpecular);

return color;

}

В Game1 нужно только передать параметр Eye – позицию наблюдателя.

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 world = Matrix.Identity;

Matrix view = camera.View;

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

lightEffect.Parameters[«xLights»].SetValue(pointLights.ToArray());

lightEffect.Parameters[«xLightCount»].SetValue(pointLights.Count);

lightEffect.Parameters[«xLightsEnabled»].SetValue(true);

    lightEffect.Parameters[«Eye»].SetValue(camera.position);

foreach (var item in primitives)

{

item.Draw(world, view, projection, lightEffect);

}

base.Draw(gameTime);

}

Тут я использовал стандартные формулы из модели Блинна-Фонга, описания которых уже достаточно в моей блоге. Разница в том, что сейчас я использовал коэффициент затухания и еще добавил в формулу затухание пропорциональное квадрату расстояния до источника света.

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

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

using Utils;

namespace PointLightGame

{

/// <summary>

/// This is the main type for your game

/// </summary>

public class Game1 : Microsoft.Xna.Framework.Game

{

GraphicsDeviceManager graphics;

SpriteBatch spriteBatch;

List<GameObject> primitives = new List<GameObject>();

Effect lightEffect;

List<Vector3> pointLights = new List<Vector3>();

FreeCamera camera;

public Game1()

{

graphics = new GraphicsDeviceManager(this);

Content.RootDirectory = «Content»;

graphics.PreferredBackBufferWidth = 640;

graphics.PreferredBackBufferHeight = 480;

}

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

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

Components.Add(camera);

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

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

primitives.Add(new GameObject(new CubePrimitive(GraphicsDevice, 6), new Vector3(0, -4, 0), Color.Green));

primitives.Add(new GameObject(new TeapotPrimitive(GraphicsDevice, 0.3f, 40), new Vector3(-0.5f, -0.9f, 1), Color.Yellow));

primitives.Add(new GameObject(new TeapotPrimitive(GraphicsDevice, 1f, 40), new Vector3(1, -0.65f, -1), Color.Red));

primitives.Add(new GameObject(new TeapotPrimitive(GraphicsDevice, 0.3f, 40), new Vector3(-0.5f, -0.9f, 2), Color.Tomato));

primitives.Add(new GameObject(new TeapotPrimitive(GraphicsDevice, 0.3f, 40), new Vector3(0.5f, -0.9f, 2f), Color.Gray));

primitives.Add(new GameObject(new TeapotPrimitive(GraphicsDevice, 0.3f, 40), new Vector3(0.5f, -0.9f, 1), Color.Brown));

for (int i = -2; i <= 2; i++)

{

for (int j = -2; j <= 2; j++)

{

var pos = new Vector3(i, -0.9f, j);

pointLights.Add(pos);

primitives.Add(new GameObject(new SpherePrimitive(GraphicsDevice, 0.1f, 10), pos, Color.White));

}

}

}

/// <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 world = Matrix.Identity;

Matrix view = camera.View;

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

lightEffect.Parameters[«xLights»].SetValue(pointLights.ToArray());

lightEffect.Parameters[«xLightCount»].SetValue(pointLights.Count);

lightEffect.Parameters[«xLightsEnabled»].SetValue(true);

lightEffect.Parameters[«Eye»].SetValue(camera.position);

foreach (var item in primitives)

{

item.Draw(world, view, projection, lightEffect);

}

base.Draw(gameTime);

}

}

}

GameObject.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Primitives3D;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace PointLightGame
{
    class GameObject
    {
        GeometricPrimitive primitive;
        Vector3 position;
        Color color;

        public GameObject(GeometricPrimitive primitive, Vector3 position, Color color)
        {
            this.primitive = primitive;
            this.position = position;
            this.color = color;
        }

        public void Draw(Matrix world, Matrix view, Matrix projection)
        {
            world *= Matrix.CreateTranslation(position);
            primitive.Draw(world, view, projection, color);
        }

        public void Draw(Matrix world, Matrix view, Matrix projection, Effect effect)
        {
            world *= Matrix.CreateTranslation(position);

            effect.Parameters["World"].SetValue(world);
            effect.Parameters["View"].SetValue(view);
            effect.Parameters["Projection"].SetValue(projection);
            effect.Parameters["Color"].SetValue(color.ToVector4());
            primitive.Draw(effect);
        }

    }

}

Light.fx

#define MAXLIGHTS 30

bool xLightsEnabled;

int xLightCount;

float3 xLights[MAXLIGHTS];

float4x4 World;

float4x4 View;

float4x4 Projection;

float4 Color;

float3 Eye;

// TODO: add effect parameters here.

struct VertexShaderInput

{

float4 Position : POSITION0;

float3 Normal : NORMAL0;

// TODO: add input channels such as texture

// coordinates and vertex colors here.

};

struct VertexShaderOutput

{

float4 Position : POSITION0;

float3 WorldNormal : NORMAL0;

float3 WorldPosition : 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;

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

float4 viewPosition = mul(worldPosition, View);

output.Position = mul(viewPosition, Projection);

// TODO: add your vertex shader code here.

output.WorldPosition = worldPosition;

output.WorldNormal = normalize(mul(input.Normal, World));

return output;

}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0

{

float4 color = Color;

float3 light = float3(0.1f,0.1f,0.1f);

float3 lightSpecular = float3(0,0,0);

float3 normal = normalize(input.WorldNormal);

float3 position = input.WorldPosition;

float3 view = normalize(Eye — position);

if (xLightsEnabled)

for (int i = 0; i < xLightCount; i++)

{

float dist = distance(xLights[i], input.WorldPosition);

float3 lightDirection = normalize(xLights[i] — position);

light += max(0, dot(normal, lightDirection) / (dist*dist) / 5);

float3 halfVector = normalize(lightDirection + view);

lightSpecular += pow(max(0.000001f,dot(halfVector, normal) / (dist*dist) / 6), 24);

}

color.rgb *= (light  + lightSpecular);

return color;

}

technique Technique1

{

pass Pass1

{

// TODO: set renderstates here.

VertexShader = compile vs_3_0 VertexShaderFunction();

PixelShader = compile ps_3_0 PixelShaderFunction();

}

}

FreeCamera.cs

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using Microsoft.Xna.Framework;

using Microsoft.Xna.Framework.Input;

namespace Utils

{

class FreeCamera : GameComponent

{

public Matrix View;

public Vector3 position;

Vector3 angle;

float speed = 1;

float turnSpeed = 20;

public FreeCamera(Game game, Vector3 position, Vector3 lookAt) : base(game)

{

this.position = position;

View = Matrix.CreateLookAt(position, lookAt, Vector3.Up);

}

public override void Update(GameTime gameTime)

{

if (Keyboard.GetState().IsKeyDown(Keys.A))

{

// temporary disable camera

return;

}

int centerX = Game.GraphicsDevice.Viewport.Width / 2;

int centerY = Game.GraphicsDevice.Viewport.Height / 2;

MouseState mouse = Mouse.GetState();

Mouse.SetPosition(centerX, centerY);

float seconds = (float)(gameTime.ElapsedGameTime.TotalSeconds);

float yaw = MathHelper.ToRadians((mouse.X — centerX) * turnSpeed * seconds);

float pitch = MathHelper.ToRadians((mouse.Y — centerY) * turnSpeed * seconds);

angle = new Vector3(angle.X + pitch, angle.Y + yaw, angle.Z);

Vector3 forward = -Vector3.Normalize(new Vector3(

(float)Math.Sin(-angle.Y) * (float)Math.Cos(angle.X),

(float)Math.Sin(angle.X),

(float)Math.Cos(-angle.Y) * (float)Math.Cos(angle.X))

);

Vector3 left = -Vector3.Normalize(new Vector3(

(float)Math.Cos(angle.Y),

0f,

(float)Math.Sin(angle.Y))

);

KeyboardState state = Keyboard.GetState();

if (state.IsKeyDown(Keys.Up))

position += forward * speed * seconds;

if (state.IsKeyDown(Keys.Down))

position -= forward * speed * seconds;

if (state.IsKeyDown(Keys.Left))

position += left * speed * seconds;

if (state.IsKeyDown(Keys.Right))

position -= left * speed * seconds;

View = Matrix.CreateTranslation(-position) *

Matrix.CreateRotationZ(angle.Z) *

Matrix.CreateRotationY(angle.Y) *

Matrix.CreateRotationX(angle.X);

base.Update(gameTime);

}

}

}

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

2 комментария на «Реализация точечного источника света в XNA 4.0. Часть 2»

  1. float3 normal = normalize(input.WorldNormal);
    Нормализация уже была была проведена на выходе вершинного шейдера

    • Да, но в пиксельном шейдера берутся интерполированные значения для пикселей объектов. И эти данные уже могут быть не нормализованы

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

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

Логотип WordPress.com

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

Фотография Twitter

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

Фотография Facebook

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

Google+ photo

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

Connecting to %s