DirectX вики
Advertisement

Введение[]

640px-PointLight

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

Характеристики точечного света[]

При создании источника точечного света нам надо задать его позицию, дистанцию освещения и "затухание" (attenuation). Attenuation влияет на интенсивность освещения.

У точечного света нет направления в отличие от направленного света или света от прожектора, так как данный источник распространяет свет во всех направлениях.

Позиция определяет расположение центра источника из которого во всех направлениях будет исходить свет. Мы опишем ее трехмерным вектором.

Дистанция освещения будет представлена значением float. Свет от точечного источника не распрострняется бесконечно, в отличие от направленного. Расстояние на которое свет распространится и описывается данной характеристикой. Любые объекты за пределами этой дистанции не будут освещены.

Затухание представлено трехмерным вектором из трех компонент - att0, att1 и att2. Данная характеристика будет эмулировать тот процесс, когда чем дальше мы от источника света, тем свет тусклее. Вот формула его вычисления:

Attenuation = att0 + (att1 * d) + (att2 * d²)

Где d - дистанция между источником света и пикселем.

att0 - Константный модификатор. Это значение задает минимальное освещение. Если вы установите значение в "1.0f", то все внутри диапазона источника света будет иметь полный цвет, или полный свет.

att1 - Линейный модификатор, что означает, что это будет постепенно уменьшать количество света, который пиксель получает при увеличении расстояния между источником света и пикселом. Это - вероятно, самый реалистический фактор. Это значение умножается на расстояние между источником света и пикселем, т.е. на d в уравнении.

att2 - Exponential (растущий в геометрической прогрессии, быстрорастущий) модификатор. Он быстро увеличивается при приближении к источнику света, но медленно затухает при удалении от него.

Пишем код[]

Создайте новый проект, не забудьте подключить пятый фреймворк, мы из него воспользуемся двумя новыми классами - Shader и Buffer.

Создайте main.cpp

#include "MyRender.h"

int main()
{
	Framework framework;

	MyRender *render = new MyRender();

	FrameworkDesc desc;
	desc.render = render;

	framework.Init(desc);

	framework.Run();

	framework.Close();

	return 0;
}


Создайте MyRender.h

#pragma once

#include "D3D11_Framework.h"

using namespace D3D11Framework;

class MyRender : public Render
{
public:
	MyRender();
	bool Init();
	bool Draw();
	void Close();

private:
	ID3D11Buffer* IndexBuffer;
	ID3D11Buffer* VertBuffer;
	ID3D11Buffer* constMatrixBuffer;
	ID3D11Buffer* constLightBuffer;	
	XMMATRIX camView;
	Shader *shader;
};

Для сцены нам нужны 4 буфера (индексный, вершинный, константный для матриц, константный для света), видовая матрица ( camView ) и наш класс-обертка для шейдеров и текстуры.

Создайте MyRender.cpp

#include "MyRender.h"

struct cbMatrixData
{
	XMMATRIX WVP;
	XMMATRIX World;
};

struct Light
{
	Light()
	{
		ZeroMemory(this, sizeof(Light));
	}
	XMFLOAT3 dir;
	float pad1;

	XMFLOAT3 pos;
	float range;
	XMFLOAT3 att;
	float pad2;

	XMFLOAT4 ambient;
	XMFLOAT4 diffuse;
} light;

struct cbLightData
{
	Light light;
};

struct Vertex
{
	Vertex(	float x, float y, float z,
			float u, float v,
			float nx, float ny, float nz) : 
		pos(x,y,z), tex(u, v), normal(nx, ny, nz){}

	XMFLOAT3 pos;
	XMFLOAT2 tex;
	XMFLOAT3 normal;
};

Здесь интересным является константный буфер для цвета. В этом уроке мы будем передавать в шейдер целый класс, что очень удобно. Также обратите внимание что после описания Light, мы создаем его экземпляр light, которым будем пользоваться по коду.

MyRender::MyRender()
{
	IndexBuffer = nullptr;
	VertBuffer = nullptr;
	constMatrixBuffer = nullptr;
	constLightBuffer = nullptr;
	shader = nullptr;
}

bool MyRender::Init()
{
	shader = new Shader(this);
	if (!shader)
		return false;

	
	if ( !shader->LoadTexture(L"image.png") )
		return false;
	
	shader->AddInputElementDesc("POSITION", DXGI_FORMAT_R32G32B32_FLOAT);
	shader->AddInputElementDesc("TEXCOORD", DXGI_FORMAT_R32G32_FLOAT);
	shader->AddInputElementDesc("NORMAL", DXGI_FORMAT_R32G32B32_FLOAT);

	if ( !shader->CreateShader(L"pointlight.vs",L"pointlight.ps") )
		return false;

В методе Init() мы создаем наш класс шейдера, грузим текстуру, заполняем входной формат, и грузим два шейдера. Как видите, намного удобней чем мы это делали ранее:) .

	Vertex v[] =
	{
		Vertex(-1.0f, -1.0f, -1.0f, 0.0f, 1.0f,-1.0f, -1.0f, -1.0f),
		Vertex(-1.0f,  1.0f, -1.0f, 0.0f, 0.0f,-1.0f,  1.0f, -1.0f),
		Vertex( 1.0f,  1.0f, -1.0f, 1.0f, 0.0f, 1.0f,  1.0f, -1.0f),
		Vertex( 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f),

		Vertex(-1.0f, -1.0f, 1.0f, 1.0f, 1.0f,-1.0f, -1.0f, 1.0f),
		Vertex( 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 1.0f),
		Vertex( 1.0f,  1.0f, 1.0f, 0.0f, 0.0f, 1.0f,  1.0f, 1.0f),
		Vertex(-1.0f,  1.0f, 1.0f, 1.0f, 0.0f,-1.0f,  1.0f, 1.0f),

		Vertex(-1.0f, 1.0f, -1.0f, 0.0f, 1.0f,-1.0f, 1.0f, -1.0f),
		Vertex(-1.0f, 1.0f,  1.0f, 0.0f, 0.0f,-1.0f, 1.0f,  1.0f),
		Vertex( 1.0f, 1.0f,  1.0f, 1.0f, 0.0f, 1.0f, 1.0f,  1.0f),
		Vertex( 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f),

		Vertex(-1.0f, -1.0f, -1.0f, 1.0f, 1.0f,-1.0f, -1.0f, -1.0f),
		Vertex( 1.0f, -1.0f, -1.0f, 0.0f, 1.0f, 1.0f, -1.0f, -1.0f),
		Vertex( 1.0f, -1.0f,  1.0f, 0.0f, 0.0f, 1.0f, -1.0f,  1.0f),
		Vertex(-1.0f, -1.0f,  1.0f, 1.0f, 0.0f,-1.0f, -1.0f,  1.0f),

		Vertex(-1.0f, -1.0f,  1.0f, 0.0f, 1.0f,-1.0f, -1.0f,  1.0f),
		Vertex(-1.0f,  1.0f,  1.0f, 0.0f, 0.0f,-1.0f,  1.0f,  1.0f),
		Vertex(-1.0f,  1.0f, -1.0f, 1.0f, 0.0f,-1.0f,  1.0f, -1.0f),
		Vertex(-1.0f, -1.0f, -1.0f, 1.0f, 1.0f,-1.0f, -1.0f, -1.0f),

		Vertex( 1.0f, -1.0f, -1.0f, 0.0f, 1.0f, 1.0f, -1.0f, -1.0f),
		Vertex( 1.0f,  1.0f, -1.0f, 0.0f, 0.0f, 1.0f,  1.0f, -1.0f),
		Vertex( 1.0f,  1.0f,  1.0f, 1.0f, 0.0f, 1.0f,  1.0f,  1.0f),
		Vertex( 1.0f, -1.0f,  1.0f, 1.0f, 1.0f, 1.0f, -1.0f,  1.0f),
	};

	DWORD indices[] =
	{
		0,  1,  2,
		0,  2,  3,

		4,  5,  6,
		4,  6,  7,

		8,  9, 10,
		8, 10, 11,

		12, 13, 14,
		12, 14, 15,

		16, 17, 18,
		16, 18, 19,

		20, 21, 22,
		20, 22, 23
	};

	IndexBuffer = Buffer::CreateIndexBuffer(m_pd3dDevice, sizeof(DWORD)*36, false, indices);

	VertBuffer = Buffer::CreateVertexBuffer(m_pd3dDevice, sizeof( Vertex )*24, false, v);

	constMatrixBuffer = Buffer::CreateConstantBuffer(m_pd3dDevice, sizeof(cbMatrixData), false);
	constLightBuffer = Buffer::CreateConstantBuffer(m_pd3dDevice, sizeof(cbLightData), false);

Здесь мы создаем 4 буфера, тут тоже все теперь просто.

	XMVECTOR camPosition = XMVectorSet( 0.0f, 3.0f, -8.0f, 0.0f );
	XMVECTOR camTarget = XMVectorSet( 0.0f, 0.0f, 0.0f, 0.0f );
	XMVECTOR camUp = XMVectorSet( 0.0f, 1.0f, 0.0f, 0.0f );
	camView = XMMatrixLookAtLH( camPosition, camTarget, camUp );
				
	light.pos = XMFLOAT3(0.0f, 0.0f, 0.0f);
	light.range = 100.0f;
	light.att = XMFLOAT3(0.0f, 0.2f, 0.0f);
	light.ambient = XMFLOAT4(0.3f, 0.3f, 0.3f, 1.0f);
	light.diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
	
	return true;
}

Устанавливаем видовую матрицу, и описываем наш свет. На этом инициализация завершена.

bool MyRender::Draw()
{
	static float rot = 0.0f;
	static DWORD dwTimeStart = 0;
	DWORD dwTimeCur = GetTickCount();
	if( dwTimeStart == 0 )
		dwTimeStart = dwTimeCur;
	rot = ( dwTimeCur - dwTimeStart ) / 1000.0f;


	XMMATRIX cube1World = XMMatrixIdentity();
	XMVECTOR rotaxis = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
	XMMATRIX Rotation = XMMatrixRotationAxis( rotaxis, rot);
	XMMATRIX Translation = XMMatrixTranslation( 0.0f, 0.0f, 4.0f );
	cube1World = Translation * Rotation;	

	XMVECTOR lightVector = XMVectorSet( 0.0f, 0.0f, 0.0f, 0.0f );
	lightVector = XMVector3TransformCoord(lightVector,cube1World);

	light.pos.x = XMVectorGetX(lightVector);
	light.pos.y = XMVectorGetY(lightVector);
	light.pos.z = XMVectorGetZ(lightVector);

	XMMATRIX cube2World = XMMatrixIdentity();
	Rotation = XMMatrixRotationAxis( rotaxis, -rot);
	XMMATRIX Scale = XMMatrixScaling( 1.3f, 1.3f, 1.3f );
	cube2World = Rotation * Scale;


	UINT stride = sizeof( Vertex );
	UINT offset = 0;
	m_pImmediateContext->IASetVertexBuffers( 0, 1, &VertBuffer, &stride, &offset );
	m_pImmediateContext->IASetIndexBuffer( IndexBuffer, DXGI_FORMAT_R32_UINT, 0);
	m_pImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	
	cbLightData cblgh;
	cblgh.light = light;
	m_pImmediateContext->UpdateSubresource( constLightBuffer, 0, NULL, &cblgh, 0, 0 );
	m_pImmediateContext->PSSetConstantBuffers(0, 1, &constLightBuffer);	

	XMMATRIX WVP = cube1World * camView * m_Projection;   
	cbMatrixData cbMat;
	cbMat.World = XMMatrixTranspose(cube1World);	
	cbMat.WVP = XMMatrixTranspose(WVP);	
	m_pImmediateContext->UpdateSubresource( constMatrixBuffer, 0, NULL, &cbMat, 0, 0 );
	m_pImmediateContext->VSSetConstantBuffers( 0, 1, &constMatrixBuffer );

	shader->Draw();
	m_pImmediateContext->DrawIndexed( 36, 0, 0 );

	WVP = cube2World * camView * m_Projection;    
	cbMat.World = XMMatrixTranspose(cube2World);	
	cbMat.WVP = XMMatrixTranspose(WVP);	
	m_pImmediateContext->UpdateSubresource( constMatrixBuffer, 0, NULL, &cbMat, 0, 0 );
	m_pImmediateContext->VSSetConstantBuffers( 0, 1, &constMatrixBuffer );
	shader->Draw();
	m_pImmediateContext->DrawIndexed( 36, 0, 0 );

	return true;
}

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

void MyRender::Close()
{
	_CLOSE(shader);
	_RELEASE(IndexBuffer);
	_RELEASE(VertBuffer);
	_RELEASE(constMatrixBuffer);
	_RELEASE(constLightBuffer);
}

Шейдеры[]

pointlight.vs

cbuffer cbPerObject
{
	float4x4 WVP;
	float4x4 World;
};

struct VertexInputType
{
	float4 pos : POSITION;
	float2 tex : TEXCOORD;
	float3 normal : NORMAL;	
};

struct PixelInputType
{
	float4 pos : SV_POSITION;
	float4 worldPos : POSITION;
	float2 tex : TEXCOORD;
	float3 normal : NORMAL;	
};

PixelInputType VS(VertexInputType input)
{
	PixelInputType output;

	output.pos = mul(input.pos, WVP);	
	output.worldPos = mul(input.pos, World);
	output.tex= input.tex;
	output.normal = mul(input.normal, World);

	return output;
}

pointlight.ps

struct Light
{
	float3 dir;
	float3 pos;
	float  range;
	float3 att;
	float4 ambient;
	float4 diffuse;
};

cbuffer cbPerFrame
{
	Light light;
};

Texture2D ObjTexture;
SamplerState ObjSamplerState;

struct PixelInputType
{
    float4 pos : SV_POSITION;
	float4 worldPos : POSITION;
    float2 tex : TEXCOORD;
	float3 normal : NORMAL;	
};

float4 PS(PixelInputType input) : SV_TARGET
{
	input.normal = normalize(input.normal);

    float4 diffuse = ObjTexture.Sample( ObjSamplerState, input.tex );

	float3 finalColor = float3(0.0f, 0.0f, 0.0f);
	
	// Создаем вектор между позицией света и позицией пикселя
	float3 lightToPixelVec = light.pos - input.worldPos;
		
	// Находим расстояние между светом и пикселем (это длина вектора)
	float d = length(lightToPixelVec);
	
	// Создаем фоновый (Ambient) свет
	float3 finalAmbient = diffuse * light.ambient;

	// Если пиксель слишком далеко расположен, возвращаем цвет пикселя фонового света
	if( d > light.range )
		return float4(finalAmbient, diffuse.a);
		
	// Делаем lightToPixelVec единичным вектором, описывая направление пикселей по отношению позиции света
	lightToPixelVec /= d; 
	
	// Узнаем интенсивность света в зависимости от угла к поверхности
	float howMuchLight = dot(lightToPixelVec, input.normal);

	// Если свет на передней поверхности
	if( howMuchLight > 0.0f )
	{	
		// Добавляем освещение к finalColor
		finalColor += howMuchLight * diffuse * light.diffuse;
		
		// Вычисляем фактор затухания
		finalColor /= light.att[0] + (light.att[1] * d) + (light.att[2] * (d*d));
	}	
        
	// Убеждаемся, что результат от 1 до 0, и добавляем фоновое освещение 
	finalColor = saturate(finalColor + finalAmbient);
	
	// возвращаем получившийся свет
	return float4(finalColor, diffuse.a);
}

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

Уравнение точечного света[]

Это фактически не уравнение, а серия шагов и уравнений вычисляющих свет.

Вектор позиции пикселя к позиции света[]

Сначала мы создадим вектор описывающий направление света попадающего на пискель:

lightToPixelVec = light.pos - input.worldPos;

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

Дистанция между позицией пикселя и позицией света[]

Теперь найдем расстояние между позицией пикселя и позицией света, используя вектор который мы до этого создали. Тогда мы можем определить, находится ли пиксель в зоне света или нет. Длину вектора мы можем найти используя функцию HLSL - length().

d = length(lightToPixelVec);

Если d больше дистанции света, то пиксель не получает этот свет, и мы можем вернуть фоновый свет.

Нормализуем lightToPixelVec вектор[]

Теперь мы должны нормализовать этот вектор, чтобы использовать дальше. Для этого мы делим этот вектор на дистанцию полученную ранее:

lightToPixelVec /= d;

Интенсивность света[]

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

howMuchLight = dot(lightToPixelVec, input.normal);

Добавляем интенсивность света к пикселю[]

Сначала мы проверяем что больше нуля, это означает, что свет попадает спереди на поверхность пикселя. Затем мы добавляем фактор интенсивности света к пикселю. Для этого мы умножаем интенсивность света на diffuse пикселя и diffuse нашего света.

finalColor += howMuchLight * diffuse * light.diffuse;

Добавляем фактор затухания[]

Последняя вещь которую мы сделаем - добавим фактор затухания. Мы сделаем это деля цвет пикселя на фактор затухания:

finalColor /= light.att[0] + (light.att[1] * d) + (light.att[2] * (d*d));

Результат[]

Исходный код примера и фреймворка - скачать

У вас должно получится следующее (возможно не так ярко, деффект скрина :) )

Test 2012-10-14 11-30-24-68

Advertisement