DirectX вики
Advertisement

Введение[]

В этом уроке мы сделаем самое простое освещение.

Типы источников света[]

Есть три типа источников света: Directional (направленный), Spotlights (прожектор) и Point (точечный)

Направленный (Directional)[]

Направленный свет (иногда еще называемый параллельным) является самым простым в реализации. У него нет никакого позиции, только направление и цвет. То есть данный тип света излучает свет в определенном направлении без затухания. В этом уроке мы расмотрим только его.

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

640px-DirectionalLight

Точечный (Point)[]

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

640px-PointLight

Прожектор (Spotlights)[]

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

640px-SpotLight

Компоненты света[]

Свет состоит из следующих компонентов:

Фоновый (Ambient)[]

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

Рассеиваемый свет (diffuse light)[]

Этот компонент распространяет свет в заданном направлении. Сталкиваясь с поверхностью, он равномерно отражается во всех направлениях. Это основной компонент света.

Отражаемый свет (specular light)[]

Этот свет распространяется в заданном направлении. Сталкиваясь с поверхностью он отражается строго в одном направлении, формируя блики, которые видимы только при взгляде на объект под определенным углом. Отражаемый свет используется для моделирования света, формируемого освещенными объектами, такого как блики, появляющиеся при освещении полированных поверхностей. В этом уроке мы не будет рассматривать данный компонент и вернемся к нему в одном из следующих.

Нормали[]

Нормали используются для определения направления поверхности. Здесь мы будем использовать их для определения того, находится ли поверхность в пределах света и сколько света она должна получить. При переводе вершин из мирового пространства в вектор нормали, мы можем получить значения больше 1 или меньше 0. Это называется денормализацией. Данный факт мы должны устранять путем нормализации вектора нормали.

Нормаль вершин (Vertex Normal)[]

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

Vertexnorms

Нормаль грани (Face Normal)[]

Она подобна нормали вершины, но вместо направления вершины определяет направление поверхности. При создании объекта мы создаем только нормали вершин и из них вычисляем нормаль грани.

Facenorms

Усредненная нормаль (Normal Averaging)[]

Усредненная нормаль - это техника используемая для того чтобы заставить освещение выглядеть менее "граненным". Обычно используется для выпуклых поверхностей, например шаров. Чтобы получить усредненную нормаль, мы берем нормали каждой из поверхностей и вычисляем среднее значение.

Normavg

Пишем код[]

Создайте новый проект, подключите фреймворк третьей версии, добавьте 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"
#include <xnamath.h>

using namespace D3D11Framework;

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

	void* operator new(size_t i)
	{
		return _aligned_malloc(i,16);
	}

	void operator delete(void* p)
	{
		_aligned_free(p);
	}

private:		
	ID3D11Buffer *m_pVertexBuffer;
	ID3D11InputLayout *m_pVertexLayout;	
	ID3D11VertexShader *m_pVertexShader;
	ID3D11PixelShader *m_pPixelShader;
	ID3D11PixelShader *m_pPixelShaderSolid;

	ID3D11Buffer *m_pIndexBuffer;
	ID3D11Buffer *m_pConstantBuffer;

	XMMATRIX m_World;
	XMMATRIX m_View;
	XMMATRIX m_Projection;
};

Здесь мы добавили еще один пиксельный шейдер m_pPixelShaderSolid, данный шейдер будет применяться для куба на который не будет действовать освещение (этот куб будет показывать направление освещения - типа лампочка).

Создайте MyRender.cpp:

#include "MyRender.h"


struct SimpleVertex
{
	XMFLOAT3 Pos;
	XMFLOAT3 Normal;
};

struct ConstantBuffer
{
	XMMATRIX mWorld;
	XMMATRIX mView;
	XMMATRIX mProjection;
	XMFLOAT4 vLightDir[2];
	XMFLOAT4 vLightColor[2];
	XMFLOAT4 vOutputColor;
};

MyRender::MyRender()
{
	m_pVertexShader = nullptr;
	m_pPixelShader = nullptr;
	m_pVertexLayout = nullptr;
	m_pVertexBuffer = nullptr;
	m_pIndexBuffer = nullptr;
	m_pConstantBuffer = nullptr;
	m_pPixelShaderSolid = nullptr;
}

bool MyRender::Init(HWND hwnd)
{
	HRESULT hr = S_OK;
	ID3DBlob* pVSBlob = NULL;
	hr = m_compileshaderfromfile(L"shader.fx", "VS", "vs_4_0", &pVSBlob);
	if( FAILED( hr ) )
		return false;

	hr = m_pd3dDevice->CreateVertexShader(pVSBlob->GetBufferPointer(), pVSBlob->GetBufferSize(), NULL, &m_pVertexShader );
	if( FAILED( hr ) )
	{	
		_RELEASE(pVSBlob);
		return false;
	}

	D3D11_INPUT_ELEMENT_DESC layout[] =
	{
		{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
		{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
	};
	UINT numElements = ARRAYSIZE( layout );

	hr = m_pd3dDevice->CreateInputLayout( layout, numElements, pVSBlob->GetBufferPointer(), pVSBlob->GetBufferSize(), &m_pVertexLayout );
	_RELEASE(pVSBlob);
	if( FAILED( hr ) )
		return false;

	m_pImmediateContext->IASetInputLayout( m_pVertexLayout );

	ID3DBlob* pPSBlob = NULL;
	hr = m_compileshaderfromfile( L"shader.fx", "PS", "ps_4_0", &pPSBlob );
	if( FAILED( hr ) )
		return false;

	hr = m_pd3dDevice->CreatePixelShader( pPSBlob->GetBufferPointer(), pPSBlob->GetBufferSize(), NULL, &m_pPixelShader );
	_RELEASE(pPSBlob);
	if( FAILED( hr ) )
		return false;

	pPSBlob = NULL;
	hr = m_compileshaderfromfile( L"shader.fx", "PSSolid", "ps_4_0", &pPSBlob );
	if( FAILED( hr ) )
		return false;

	hr = m_pd3dDevice->CreatePixelShader( pPSBlob->GetBufferPointer(), pPSBlob->GetBufferSize(), NULL, &m_pPixelShaderSolid );
	_RELEASE(pPSBlob);
	if( FAILED( hr ) )
		return false;

	SimpleVertex vertices[] =
	{
		{ XMFLOAT3( -1.0f, 1.0f, -1.0f ), XMFLOAT3( 0.0f, 1.0f, 0.0f ) },
		{ XMFLOAT3( 1.0f, 1.0f, -1.0f ), XMFLOAT3( 0.0f, 1.0f, 0.0f ) },
		{ XMFLOAT3( 1.0f, 1.0f, 1.0f ), XMFLOAT3( 0.0f, 1.0f, 0.0f ) },
		{ XMFLOAT3( -1.0f, 1.0f, 1.0f ), XMFLOAT3( 0.0f, 1.0f, 0.0f ) },

		{ XMFLOAT3( -1.0f, -1.0f, -1.0f ), XMFLOAT3( 0.0f, -1.0f, 0.0f ) },
		{ XMFLOAT3( 1.0f, -1.0f, -1.0f ), XMFLOAT3( 0.0f, -1.0f, 0.0f ) },
		{ XMFLOAT3( 1.0f, -1.0f, 1.0f ), XMFLOAT3( 0.0f, -1.0f, 0.0f ) },
		{ XMFLOAT3( -1.0f, -1.0f, 1.0f ), XMFLOAT3( 0.0f, -1.0f, 0.0f ) },

		{ XMFLOAT3( -1.0f, -1.0f, 1.0f ), XMFLOAT3( -1.0f, 0.0f, 0.0f ) },
		{ XMFLOAT3( -1.0f, -1.0f, -1.0f ), XMFLOAT3( -1.0f, 0.0f, 0.0f ) },
		{ XMFLOAT3( -1.0f, 1.0f, -1.0f ), XMFLOAT3( -1.0f, 0.0f, 0.0f ) },
		{ XMFLOAT3( -1.0f, 1.0f, 1.0f ), XMFLOAT3( -1.0f, 0.0f, 0.0f ) },

		{ XMFLOAT3( 1.0f, -1.0f, 1.0f ), XMFLOAT3( 1.0f, 0.0f, 0.0f ) },
		{ XMFLOAT3( 1.0f, -1.0f, -1.0f ), XMFLOAT3( 1.0f, 0.0f, 0.0f ) },
		{ XMFLOAT3( 1.0f, 1.0f, -1.0f ), XMFLOAT3( 1.0f, 0.0f, 0.0f ) },
		{ XMFLOAT3( 1.0f, 1.0f, 1.0f ), XMFLOAT3( 1.0f, 0.0f, 0.0f ) },

		{ XMFLOAT3( -1.0f, -1.0f, -1.0f ), XMFLOAT3( 0.0f, 0.0f, -1.0f ) },
		{ XMFLOAT3( 1.0f, -1.0f, -1.0f ), XMFLOAT3( 0.0f, 0.0f, -1.0f ) },
		{ XMFLOAT3( 1.0f, 1.0f, -1.0f ), XMFLOAT3( 0.0f, 0.0f, -1.0f ) },
		{ XMFLOAT3( -1.0f, 1.0f, -1.0f ), XMFLOAT3( 0.0f, 0.0f, -1.0f ) },

		{ XMFLOAT3( -1.0f, -1.0f, 1.0f ), XMFLOAT3( 0.0f, 0.0f, 1.0f ) },
		{ XMFLOAT3( 1.0f, -1.0f, 1.0f ), XMFLOAT3( 0.0f, 0.0f, 1.0f ) },
		{ XMFLOAT3( 1.0f, 1.0f, 1.0f ), XMFLOAT3( 0.0f, 0.0f, 1.0f ) },
		{ XMFLOAT3( -1.0f, 1.0f, 1.0f ), XMFLOAT3( 0.0f, 0.0f, 1.0f ) },
	};

	D3D11_BUFFER_DESC bd;
	ZeroMemory( &bd, sizeof(bd) );
	bd.Usage = D3D11_USAGE_DEFAULT;
	bd.ByteWidth = sizeof( SimpleVertex ) * 24;
	bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
	bd.CPUAccessFlags = 0;
	D3D11_SUBRESOURCE_DATA InitData;
	ZeroMemory( &InitData, sizeof(InitData) );
	InitData.pSysMem = vertices;
	hr = m_pd3dDevice->CreateBuffer(&bd, &InitData, &m_pVertexBuffer);
	if( FAILED( hr ) )
		return false;

	UINT stride = sizeof( SimpleVertex );
	UINT offset = 0;
	m_pImmediateContext->IASetVertexBuffers( 0, 1, &m_pVertexBuffer, &stride, &offset );

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

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

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

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

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

		22,20,21,
		23,20,22
	};
	bd.Usage = D3D11_USAGE_DEFAULT;
	bd.ByteWidth = sizeof( WORD ) * 36; 
	bd.BindFlags = D3D11_BIND_INDEX_BUFFER;
	bd.CPUAccessFlags = 0;
	InitData.pSysMem = indices;
	hr = m_pd3dDevice->CreateBuffer( &bd, &InitData, &m_pIndexBuffer );
	if( FAILED( hr ) )
		return false;

	m_pImmediateContext->IASetIndexBuffer( m_pIndexBuffer, DXGI_FORMAT_R16_UINT, 0 );

	m_pImmediateContext->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

	bd.Usage = D3D11_USAGE_DEFAULT;
	bd.ByteWidth = sizeof(ConstantBuffer);
	bd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
	bd.CPUAccessFlags = 0;
	hr = m_pd3dDevice->CreateBuffer( &bd, NULL, &m_pConstantBuffer );
	if( FAILED( hr ) )
		return false;

	m_World = XMMatrixIdentity();


	XMVECTOR Eye = XMVectorSet( 0.0f, 3.0f, -7.0f, 0.0f );
	XMVECTOR At = XMVectorSet( 0.0f, 1.0f, 0.0f, 0.0f );
	XMVECTOR Up = XMVectorSet( 0.0f, 1.0f, 0.0f, 0.0f );
	m_View = XMMatrixLookAtLH( Eye, At, Up );

	float width = 640.0f;
	float height = 480.0f;
	m_Projection = XMMatrixPerspectiveFovLH(XM_PIDIV2, width/height, 0.01f, 100.0f );

	return true;
}

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

Теперь пишите:

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

	m_World = XMMatrixRotationY( t );

	XMFLOAT4 vLightDirs[2] =
	{
		XMFLOAT4( -0.577f, 0.577f, -0.577f, 1.0f ),
		XMFLOAT4( 0.0f, 0.0f, -1.0f, 1.0f ),
	};
	XMFLOAT4 vLightColors[2] =
	{
		XMFLOAT4( 0.0f, 0.0f, 1.0f, 1.0f ),
		XMFLOAT4( 1.0f, 0.0f, 0.0f, 1.0f )
	};

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

	XMMATRIX mRotate = XMMatrixRotationY( -2.0f * t );
	XMVECTOR vLightDir = XMLoadFloat4( &vLightDirs[1] );
	vLightDir = XMVector3Transform( vLightDir, mRotate );
	XMStoreFloat4( &vLightDirs[1], vLightDir );

	ConstantBuffer cb1;
	cb1.mWorld = XMMatrixTranspose( m_World );
	cb1.mView = XMMatrixTranspose( m_View );
	cb1.mProjection = XMMatrixTranspose(m_Projection );
	cb1.vLightDir[0] = vLightDirs[0];
	cb1.vLightDir[1] = vLightDirs[1];
	cb1.vLightColor[0] = vLightColors[0];
	cb1.vLightColor[1] = vLightColors[1];
	cb1.vOutputColor = XMFLOAT4(0, 0, 0, 0);
	m_pImmediateContext->UpdateSubresource( m_pConstantBuffer, 0, NULL, &cb1, 0, 0 );

Здесь в начале мы вращаем, красный свет, а затем передаем нужные параметры в шейдер, заполняя константный буфер. Функция XMVector3Transform() производит умножение вектора на матрицу.

Далее рисуем куб:

	m_pImmediateContext->VSSetShader( m_pVertexShader, NULL, 0 );
	m_pImmediateContext->VSSetConstantBuffers( 0, 1, &m_pConstantBuffer );
	m_pImmediateContext->PSSetShader( m_pPixelShader, NULL, 0 );
	m_pImmediateContext->PSSetConstantBuffers( 0, 1, &m_pConstantBuffer );
	m_pImmediateContext->DrawIndexed( 36, 0, 0 );

И в конце рисуем еще два куба которые будут показывать направление и цвет источника света:

	for( int m = 0; m < 2; m++ )
	{
		XMMATRIX mLight = XMMatrixTranslationFromVector( 5.0f * XMLoadFloat4( &vLightDirs[m] ) );
		XMMATRIX mLightScale = XMMatrixScaling( 0.2f, 0.2f, 0.2f );
		mLight = mLightScale * mLight;

		cb1.mWorld = XMMatrixTranspose( mLight );
		cb1.vOutputColor = vLightColors[m];
		m_pImmediateContext->UpdateSubresource( m_pConstantBuffer, 0, NULL, &cb1, 0, 0 );

		m_pImmediateContext->PSSetShader( m_pPixelShaderSolid, NULL, 0 );
		m_pImmediateContext->DrawIndexed( 36, 0, 0 );
	}

	return true;
}

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

И завершаем код:

void MyRender::Close()
{
	_RELEASE(m_pConstantBuffer);
	_RELEASE(m_pVertexBuffer);
	_RELEASE(m_pIndexBuffer);
	_RELEASE(m_pVertexLayout);
	_RELEASE(m_pVertexShader);
	_RELEASE(m_pPixelShader);
	_RELEASE(m_pPixelShaderSolid);
}

Теперь остался сам шейдер shader.fx:

cbuffer ConstantBuffer
{
	matrix World;
	matrix View;
	matrix Projection;
	float4 vLightDir[2];
	float4 vLightColor[2];
	float4 vOutputColor;
}

struct VS_INPUT
{
    float4 Pos : POSITION;
    float3 Norm : NORMAL;
};

struct PS_INPUT
{
    float4 Pos : SV_POSITION;
    float3 Norm : TEXCOORD0;
};

PS_INPUT VS( VS_INPUT input )
{
    PS_INPUT output = (PS_INPUT)0;
    output.Pos = mul( input.Pos, World );
    output.Pos = mul( output.Pos, View );
    output.Pos = mul( output.Pos, Projection );
    output.Norm = mul( input.Norm, World );
    
    return output;
}

float4 PS( PS_INPUT input) : SV_Target
{
    float4 finalColor = 0;	

    input.Norm = normalize(input.Norm);
    
    for(int i=0; i<2; i++)
    {
        finalColor += saturate( dot( (float3)vLightDir[i],input.Norm) * vLightColor[i] );
    }

    return finalColor;
}

float4 PSSolid( PS_INPUT input) : SV_Target
{
    return vOutputColor;
}

Нормаль мы вычисляем в вершинном шейдере, вот в этой строчке:

output.Norm = mul( input.Norm, World );

Здесь мы умножаем текущую нормаль на мировую матрицу.

В пиксельном шейдере мы вычисляем освещение:

float4 PS( PS_INPUT input) : SV_Target
{
    float4 finalColor = 0;	

    input.Norm = normalize(input.Norm);
    
    for(int i=0; i<2; i++)
    {
        finalColor += saturate( dot( (float3)vLightDir[i],input.Norm) * vLightColor[i] );
    }

    return finalColor;
}

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

Сначала мы определяем скалярное произведение двух векторов - направления света и нормали поверхности через функцию dot. Затем умножаем это число на рассеиваемый цвет нашего света. Функция saturate следит за тем чтобы результат не был больше 1.0 или меньше 0.0.

Итог[]

Скачать

Lesson 2012-11-09 18-17-49-50
Advertisement