Pravimo game engine od nule, deo 4: kompjuterska grafika 101

Posle nekoliko veoma brzih nedelja, vraćamo se u akciju i nastavljamo tamo gde smo stali! Počnimo današnji deo sa nekim osnovnim definicijama: šta je to tačka, šta je transformacija, šta je stek transformacija, šta je shader, šta je matrica, što je matrica, itd. Ova pitanja nekima možda zvuče banalno, a nekima prekomplikovano, no nađimo se na nekoj sredini.

Miroslav Gavrilov - 27. Avgust, 2015.

Posle nekoliko veoma brzih nedelja, vraćamo se u akciju i nastavljamo tamo gde smo stali! Počnimo današnji deo sa nekim osnovnim definicijama: šta je to tačka, šta je transformacija, šta je stek transformacija, šta je shader, šta je matrica, što je matrica, itd. Ova pitanja nekima možda zvuče banalno, a nekima prekomplikovano, no nađimo se na nekoj sredini. Svakako, sve su to samo koraci koji nas vode ka krajnjem proizvodu.

Tačka

Tačka je, u najširem smislu, uređena n-torka brojeva, ili vektor nekog n-dimenzionalnog vektorskog polja. Znam, zvuči strašno, no samo pogledajmo šta ti izrazi znače ustvari i biće odjednom mnogo jednostavnije: n-torka znači da imamo nekih n (n je neki broj) brojeva, a uređen znači da ne možemo proizvoljno menjati mesta tim brojevima. Vektori vektorskog polja znači da postoji jedno polje koje se sastoji od različitih koordinata, a svaku od tih zovemo jednim vektorom. Recimo, jedna tačka je u oba ova smisla, na primer, ovakva: (2, 3). Ovo je uređen par (dvojka), a takođe i vektor dvodimenzionalnog vektorskog prostora. U zavisnosti od toga o čemu govorimo, u grafici ćemo obično koristiti dvodimenzionalne i trodimenzionalne vektore kao tačke, samo ponekad (u veoma komplikovanim slučajevima) odlazeći dalje od toga u mističan svet četvorodimenzionalnih vektora kojima izražavamo čudne matematičke formalizme obavezne za besprekornu rotaciju, beskonačnost horizonta, itd. Ništa što nas sada zanima, svakako.

Projektovanje

Sada kad znamo šta je tačka, moramo pričati o malo komplikovanijem pojmu poznatom kao projektovanje. Naime, dvodimenzionalne tačke (tačke u ravni) se mogu projektovati u prostor više dimenzija (recimo, tri), a iz tog prostora možemo projektovati nazad na ravan. Projektovanje je, dakle, proces dodavanja ili oduzimanja jedne dimenzije vektorima sa kojima radimo. U zavisnosti od toga kako projektujemo stvari, možemo završiti sa manje ili više realnim pregledom stvari, u perspektivi, izometriji, itd. Projektovanje nam je bitno kao korak kojim 3D modele, na kraju krajeva, prikazujemo na ekranu (čast izuzecima sa hologramskim projektorima umesto ekrana).

Matrice

Gde su vektori, tu su i matrice, kojima predstavljamo transformacije u datim vektorskim prostorima. Transformacije su baš ono što i zamišljate: mrdanja, rotacije, skaliranja, itd. Te sve stvari se daju predstaviti matricama (tablicama nekih dimenzija) brojeva, o kojima više možete čitati ovde. Matrice su zgodne jer prostim množenjem matrica možemo dobiti kombinaciju transformacija, što nam mnogo dobro dođe kad hoćemo da brzo računamo gde se šta nalazi (većma zato što su množenja matrica danas ugrađena u hardver naših računara, te su stoga mnogo brža nego što pretpostavljate). Jedan elegantan primer ovog koncepta transformacija-kroz-matrice je kamera: kamera je množenje nekoliko matrica – one pomeraju početni pogled-na-svet i diktiraju kako da se svet projektuje na ekran. U najopštijem slučaju, koristimo world, view, projection i/ili ortho matrice kao matrice kamere (stek matrica). Takođe, za svaki transformisani model u svetu imamo matricu tog modela, u okviru transforma. To radi ovako nekako.

Počnimo lagano:

// Geometry.h
#pragma once

#include <d3dx11.h>
#include <d3dx10math.h>

struct MatrixStack
{
	D3DXMATRIX projection;
	D3DXMATRIX world;
	D3DXMATRIX view;
	D3DXMATRIX ortho;
};

Transform je osnova svake instance stvari u svetu i najmanji gradivni element istih. To je jedna kolekcija transformacija i referentnih vrednosti kojim zaključujemo gde je šta i kako je šta okrenuto.

struct Transform
{
	D3DXVECTOR3 position;
	D3DXVECTOR3 rotscale;
	D3DXVECTOR2 pivot;

	Transform()
		: 
			position(0, 0, 0),
			rotscale(0, 1, 1),
			pivot(0.5, 0.5)
	{}
};

Dodatno, svaki transform mora da može da se pretvori u matricu koja će izraziti gde se i kako nalazi:

	D3DMATRIX GetMatrix()
	{
		D3DXMATRIX mat;

		D3DXMatrixIdentity(&mat);
		D3DXMatrixTranslation(&mat, -this->pivot.x, -this->pivot.y, 0);
		D3DXMatrixRotationZ(&mat, this->rotscale.x);
		D3DXMatrixScaling(&mat, this->rotscale.y, this->rotscale.z, 1);
		D3DXMatrixTranslation(&mat, this->pivot.x + this->position.x, this->pivot.y + this->position.y, 0);

		return mat;
	}

Da biste danas mogli da radite sa grafikom, doduše, potrebno vam je uvek još malo više: sve što se crta na ekran mora proći kroz unapred zadate i kompajlirane algoritme koji se paralelno izvršavaju, prvo za svaku 3D tačku modela (da, čak i kad radite 2D, to su još uvek 3D tačke sa beskorisnom trećom koordinatom), a onda za svaku tačku ekrana: pogodili ste, reč je o shaderima. Postoji više vrsta shadera, a najbitniji za početak jesu svakako vertex i pixel shaderi. Njihove funkcionalnosti smo opisali u prethodnoj rečenici, no ne fali ništa još jednom to ponoviti: vertex shader uzima po vertex, tj. tačku 3D modela, a vraća njenu poziciju na 2D ravni ekrana; pixel sahder uzima po tačku na toj ravni i dodeljuje joj boju. Oba ova procesa mogu se ukomplikovati, između njih se mogu dodati drugi delovi procesa i sve to može da bude tema za sopstveni tutorijal. Mi ćemo se, doduše, držati jednostavnosti: prezentovaćemo prvo neke najosnovnije moguće shadere i biti zadovoljni njima. Prvo, vertex shader:

cbuffer MatrixBuffer : register(cb0)
{
	matrix worldMatrix;
	matrix viewMatrix;
	matrix projectionMatrix;
	matrix orthoMatrix;
};

cbuffer SpriteBuffer : register(cb1)
{
	matrix modelMatrix;	
};

struct VertexInputType
{
	float3 position : POSITION;
	float2 texcoord : COLOR;
};

struct PixelInputType
{
	float4 position : SV_POSITION;
	float2 texcoord : TEXCOORD0;
};

PixelInputType main(VertexInputType input)
{
	PixelInputType output;

	output.position.w = 1.0f;
	
	output.position = mul(input.position, worldMatrix);
	output.position = mul(output.position, viewMatrix);		
	output.position = mul(output.position, projectionMatrix);
	output.position = mul(output.position, modelMatrix);
	output.texcoord = input.texcoord;

	return output;
}

Šta radimo ovde? Prvo definišemo sve što nam ulazi u shader program, a to bi bile matrice, modeli (ovde ih zovem Sprite jer radimo 2D), te izgled jednog vertexa i jednog pixela: imaju poziciju i teksturu (tj. tačku teksture tj. boju). Zatim imamo glavnu funkciju shadera (main), koji nam vraća jedan element strukture PixelInputType, a kao ulaz prima VertexInputType. U mainu ne radimo ništa veliko: množimo matrice kao što smo već pričali gore, prosleđujemo koordinate sa teksture baš kako smo je i dobili, ne brinemo o stvarima. Ono sa čim završavamo jesu koordinate jedne tačke i koordinate na teksturi koju ćemo koristiti. Shader ne zna koja je to tekstura, ona će biti tu kad nam zatreba.

Pixel shader izgleda nekako ovako:

Texture2D textureImage;
SamplerState textureSample;

struct PixelInputType
{
	float4 position : SV_POSITION;
	float2 texcoord : TEXCOORD0;
};

float4 main(PixelInputType input) : SV_TARGET
{		
	return textureImage.Sample(textureSample, input.texcoord);	
}

Vidimo da njegov main uzorkuje tačku na datim koordinatama sa teksture (koja je misteriozno tu) i vraća tu boju. To je sve što treba da uradi da bismo imali nacrtanu sliku na ekranu. Sad, iza toga stoji čitava magija: kako je nama tekstura povezana sa shaderom? Kako je model upisan i gde? Ko i kako ovo radi? Kako naučimo sve ovo? To su pitanja koja još uvek more mnoge.

Sada kad imamo ove shadere, snimimo ih u odgovarajuće fajlove (sa .hlsl ekstenzijama, kako bi ih Visual Studio sam kompajlirao i ukazao nam na greške), a onda pređimo na celu infrastrukturu stvari potrebnu da ovo proradi. Za početak, treba nam isti onakav vertex kakav imamo u shaderu, van shadera. Vraćamo se među definicije geometrijskih stvari i dodamo:

struct Vertex
{
	D3DXVECTOR4 position;
	D3DXVECTOR4 texcoords;	
};

Kako nam engine raste, tako će vertex postajati sve veći i veći, uključujući sve više stvari, kao što su normale, tint boja, težine kojima kosti vuku na svoju stranu, indeksi kostiju koji utiču na taj vertex, ali sada nije vreme za to. Vreme je za Shader.h:

#pragma once

#include <assert.h>

#include <string>
#include <map>
#include <vector>

#include "HelpersDirectX.h"
#include <d3d11.h>
#include <d3dx11.h>
#include <d3dx10math.h>

#ifdef _DEBUG
#define AND_ENABLE_DEBUG_IN_DEBUG_MODE	| D3D10_SHADER_DEBUG
#else
#define AND_ENABLE_DEBUG_IN_DEBUG_MODE	
#endif

class GraphicsManagerDirectX;

class Shader
{
public:
	Shader();
	~Shader();

	void AddLayoutFragment(const char* name, int format, int bytesize);

	void Initialize();

	void PrepareVertexShader(std::wstring vertexSourceFilename);
	void PreparePixelShader(std::wstring pixelSourceFilename);	

Jedina nova stvar za koju nikad niste čuli ovde je layout, što je zapravo osobina koja znači “kako nam vertex izgleda, kojim redom ima koja polja”. Sada nekoliko funkcija za rad sa bufferima, što su delovi grafičke memorije u kojima držimo različite informacije koje se vezuju i koriste u shaderima (kao što su teksture, modeli, itd).


	template<typename T>
	void AddConstantBuffer(std::wstring bufferName, int position, int size)
	{
		assert(sizeof(T) % 16 == 0, "Your constant buffer data isn't 16-aligned.");

		D3D11_BUFFER_DESC descriptor;
		descriptor.Usage = D3D11_USAGE_DYNAMIC;
		descriptor.ByteWidth = sizeof(T);
		descriptor.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
		descriptor.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
		descriptor.MiscFlags = 0;
		descriptor.StructureByteStride = 0;
		
		ParametericBuffer parbuf;
		parbuf.position = position;
		parbuf.size = size;
		
		HRESULT ok = this->manager->GetDevice()->CreateBuffer(&descriptor, nullptr, &parbuf.buffer);
		RAGE_QUIT_UNLESS(this->manager->GetDevice()->CreateBuffer(&descriptor, nullptr, &parbuf.buffer),
			L"[Shader.h] Cannot create constant buffer.");
		this->constantBuffers[bufferName] = parbuf;
	}

	template<typename T>
	void SetConstantBufferContents(std::wstring bufferName, T contents)
	{
		auto manager = SoosEngineDirectX::GetInstance().GetAspect<Graphics>();
		ID3D11DeviceContext* context = manager->GetDeviceContext();
		D3D11_MAPPED_SUBRESOURCE mappedResource;
		T* dataPointer;

		ParametericBuffer buf = this->constantBuffers[bufferName];

		RAGE_QUIT_UNLESS(context->Map(
			buf.buffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource),
			L"[Shader.h] Cannot set constant buffer contents.");

		dataPointer = (T*)mappedResource.pData;
		*dataPointer = contents;

		context->Unmap(buf.buffer, 0);
	}

Nastavljamo sa definicijama drugih potrebnih funkcija i nekoliko promenjivih:

	std::wstring OutputShaderErrorMessage(ID3D10Blob* errorMessage, std::wstring filename);
	
	void Use();

	void Shutdown();

	const std::vector<D3D11_INPUT_ELEMENT_DESC>& GetLayout() const;
	const int GetBytesize() const;

private:	
	std::vector<D3D11_INPUT_ELEMENT_DESC> layoutFragments;
	int inputLayerBytesize;

	struct ParametericBuffer
	{
		ID3D11Buffer* buffer;
		int position;
		int size;
	};

	std::map<std::wstring, ParametericBuffer> constantBuffers;	
	
	ID3D11VertexShader* vertexShaderBytecode;
	ID3D11PixelShader* pixelShaderBytecode;
	
	// maybe prepare other shader definitions for later

	ID3D11InputLayout* inputLayout;
};

Prelazimo u Shader.cpp, gde popunjavamo i ostatak:

#include "Shader.h"

#include "SoosEngineDirectX.h"

#include <sstream>
#include <fstream>

Shader::Shader()
	: 
		vertexShaderBytecode(nullptr), 
		pixelShaderBytecode(nullptr),
		inputLayerBytesize(0)
{
	this->AddLayoutFragment("POSITION", DXGI_FORMAT_R32G32B32A32_FLOAT, sizeof(float) * 4);
	this->AddLayoutFragment("TEXCOORDS", DXGI_FORMAT_R32G32B32A32_FLOAT, sizeof(float) * 4);
}

Shader::~Shader() {}

Ovde možemo videti kako se AddLayoutFragment koristi: dodajemo deo input layouta, dajemo mu ime, veličinu i tip. Takođe, destruktor.

void Shader::AddLayoutFragment(const char* name, int format, int bytesize)
{		
	this->layoutFragments.push_back(D3D11_INPUT_ELEMENT_DESC {
		name,
		0,
		(DXGI_FORMAT)format,
		0,
		(this->layoutFragments.size() == 0) ? 0 : D3D11_APPEND_ALIGNED_ELEMENT,
		D3D11_INPUT_PER_VERTEX_DATA,
		0 
	});

	this->inputLayerBytesize += bytesize;
}

Slede nam funkcije koje kompajliraju shadere, koje, iako malo duže, nisu komplikovane. Prvo za vertex shadere:

void Shader::PrepareVertexShader(std::wstring vertexSourceFilename)
{
	auto manager = SoosEngineDirectX::GetInstance().GetAspect<Graphics>();
	ID3D10Blob* errorMessage = nullptr;
	ID3D10Blob* vertexShaderBuffer = nullptr;

	HRESULT result = D3DX11CompileFromFileW(vertexSourceFilename.c_str(), nullptr, nullptr, "main", "vs_5_0",
		D3D10_SHADER_ENABLE_STRICTNESS AND_ENABLE_DEBUG_IN_DEBUG_MODE, 0, nullptr, &vertexShaderBuffer, &errorMessage, nullptr);

	if (FAILED(result))
	{
		if (errorMessage)
		{
			RAGE_QUIT_UNLESS(result, OutputShaderErrorMessage(errorMessage, vertexSourceFilename).c_str());
		}
		else
		{
			RAGE_QUIT_UNLESS(result, L"[Shader.h] Missing vertex shader file.");
		}
	}

	RAGE_QUIT_UNLESS(manager->GetDevice()->CreateVertexShader(vertexShaderBuffer->GetBufferPointer(), 
		vertexShaderBuffer->GetBufferSize(), nullptr, &this->vertexShaderBytecode),
		L"[Shader.h] Cannot create vertex shader.");

	// create input layout
	RAGE_QUIT_UNLESS(manager->GetDevice()->CreateInputLayout(&this->layoutFragments[0], this->layoutFragments.size(),		
		vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(), &this->inputLayout),
		L"[Shader.h] Cannot create input layout");

	vertexShaderBuffer->Release();
}

A zatim slična stvar i za pixel shadere:


void Shader::PreparePixelShader(std::wstring pixelSourceFilename)
{
	auto manager = SoosEngineDirectX::GetInstance().GetAspect<Graphics>();
	ID3D10Blob* errorMessage = nullptr;
	ID3D10Blob* pixelShaderBuffer = nullptr;

	HRESULT result = D3DX11CompileFromFileW(pixelSourceFilename.c_str(), nullptr, nullptr, "main", "ps_5_0",
		D3D10_SHADER_ENABLE_STRICTNESS AND_ENABLE_DEBUG_IN_DEBUG_MODE, 0, nullptr, 
		&pixelShaderBuffer, &errorMessage, nullptr);

	if (FAILED(result))
	{
		if (errorMessage)
		{
			RAGE_QUIT_UNLESS(result, OutputShaderErrorMessage(errorMessage, pixelSourceFilename).c_str());
		}
		else
		{
			RAGE_QUIT_UNLESS(result, L"[Shader.h] Missing pixel shader file.");
		}
	}

	RAGE_QUIT_UNLESS(manager->GetDevice()->CreatePixelShader(pixelShaderBuffer->GetBufferPointer(), 
		pixelShaderBuffer->GetBufferSize(), nullptr, &this->pixelShaderBytecode),
		L"[Shader.h] Cannot create pixel shader.");

	pixelShaderBuffer->Release();
}

Ostaje nam samo da skrpimo krajeve i izbacimo lepe greške (ne treba se stideti greški, samo je ljudski imati ih):

std::wstring Shader::OutputShaderErrorMessage(ID3D10Blob* errorMessage, std::wstring filename)
{
	char* compileErrors;
	unsigned long bufferSize;

	std::ofstream fileOutputStream;
	std::wstringstream shaderNameBuffer;

	shaderNameBuffer << filename << "-error-log.txt";
	fileOutputStream.open(shaderNameBuffer.str().c_str());

	compileErrors = (char*)(errorMessage->GetBufferPointer());
	bufferSize = errorMessage->GetBufferSize();

	for (int i = 0; i < bufferSize; i++)
		fileOutputStream < compileErrors[i];

	fileOutputStream.close();

	errorMessage->Release();
	errorMessage = nullptr;
	
	wchar_t message[100];
	std::swprintf(message, L"Error compiling \"%s\". Look at \"%s\" for help.", 
		filename.c_str(), shaderNameBuffer.str().c_str());

	return message;
}

void Shader::Initialize()
{
	auto manager = SoosEngineDirectX::GetInstance().GetAspect<Graphics>();
	std::vector<ID3D11Buffer*> buffers;
	for (auto it = this->constantBuffers.begin(); it != this->constantBuffers.end(); ++it)
	{		
		buffers.push_back(it->second.buffer);
	}
	manager->GetDeviceContext()->VSSetConstantBuffers(0, this->constantBuffers.size(), &buffers[0]);
}

void Shader::Use()
{
	auto manager = SoosEngineDirectX::GetInstance().GetAspect<Graphics>();
	ID3D11DeviceContext* context = manager->GetDeviceContext();
	context->IASetInputLayout(this->inputLayout);
	context->VSSetShader(this->vertexShaderBytecode, nullptr, 0);
	context->PSSetShader(this->pixelShaderBytecode, nullptr, 0);
	context->GSSetShader(this->geometryShaderBytecode, nullptr, 0);
}

void Shader::Shutdown()
{
	SAFE_RELEASE(this->inputLayout);
	SAFE_RELEASE(this->pixelShaderBytecode);
	SAFE_RELEASE(this->vertexShaderBytecode);
	SAFE_RELEASE(this->geometryShaderBytecode);
}

const std::vector<D3D11_INPUT_ELEMENT_DESC>& Shader::GetLayout() const
{
	return this->layoutFragments;
}

const int Shader::GetBytesize() const
{
	return this->inputLayerBytesize;
}

Toliko što se shadera tiče, sad samo ne smemo zaboraviti u Graphics Manageru napraviti nov shader i dodeliti mu fajlove koje smo napravili prethodno. Druga stvar koju ćemo danas preći jeste kamera, kako nam ona daje matrice koje smo koristili unutar shadera. Počnimo sa Camera.h. Idemo sa krajnje jednostavnim stvarima prvo:

#pragma once

#include <d3dx11.h>
#include <d3dx10math.h>

class Camera
{
public:
	Camera();

	void SetPosition(float x, float y, float z);
	void SetRotation(float x, float y, float z);	
	void SetLookAt(float x, float y, float z);
	void Move(float x, float y, float z);

	D3DXVECTOR3 GetPosition();
	D3DXVECTOR3 GetRotation();
	D3DXVECTOR3 GetLookAt();

	void Render();
	void GetViewMatrix(D3DXMATRIX&);

private:
	float posx, posy, posz;
	float rotx, roty, rotz;
	float lookx, looky, lookz;

	D3DXMATRIX view;
};

Implementacija nije mnogo gora, ako uzmete u obzir šta radimo (pogledati gore u objašnjenje matrica):

#include "Camera.h"

#define DEG_RAD 0.0174532925f

Camera::Camera()
	: posx(0), posy(0), posz(0),
	rotx(0), roty(0), rotz(0),
	lookx(0), looky(0), lookz(1)
{}

void Camera::SetPosition(float x, float y, float z)
{
	this->posx = x;
	this->posy = y;
	this->posz = z;
}

void Camera::SetRotation(float x, float y, float z)
{
	this->rotx = x;
	this->roty = y;
	this->rotz = z;
}

void Camera::Move(float x, float y, float z)
{
	this->posx += x;
	this->posy += y;
	this->posz += z;
}

void Camera::SetLookAt(float x, float y, float z)
{
	this->lookx = x;
	this->looky = y;
	this->lookz = z;
}

D3DXVECTOR3 Camera::GetPosition()
{
	return D3DXVECTOR3(this->posx, this->posy, this->posz);
}

D3DXVECTOR3 Camera::GetRotation()
{
	return D3DXVECTOR3(this->rotx, this->roty, this->rotz);
}

D3DXVECTOR3 Camera::GetLookAt()
{
	return D3DXVECTOR3(this->lookx, this->looky, this->lookz);
}

void Camera::Render()
{
	D3DXVECTOR3 up, position, lookAt;
	float yaw, pitch, roll;
	D3DXMATRIX rotationMatrix;

	up.x = 0.0f;
	up.y = 1.0f;
	up.z = 0.0f;

	position.x = this->posx;
	position.y = this->posy;
	position.z = this->posz;

	lookAt.x = this->lookx;
	lookAt.y = this->looky;
	lookAt.z = this->lookz;

	pitch = this->rotx * DEG_RAD;
	yaw = this->roty * DEG_RAD;
	roll = this->rotz * DEG_RAD;

	D3DXMatrixRotationYawPitchRoll(&rotationMatrix, yaw, pitch, roll);

	D3DXVec3TransformCoord(&lookAt, &lookAt, &rotationMatrix);
	D3DXVec3TransformCoord(&up, &up, &rotationMatrix);
	
	lookAt = position + lookAt;

	D3DXMatrixLookAtLH(&this->view, &position, &lookAt, &up);
}

void Camera::GetViewMatrix(D3DXMATRIX& matrix)
{
	matrix = this->view;
}

Sve je ovo mnogo magično, te se nemojte stideti naći reference za funkcije koje se ovde koriste, kao što je super-hiper-mega-korisna D3DXMatrixRotationYawPitchRoll. Kao i kod shadera, sve što treba da uradimo jeste da stvorimo i pozovemo kameru tamo gde renderujemo, i da nju zasigurno renderujemo pre svega ostalog. Ok, toliko za sad. Sledeći put, modeli i još bafera, zabavi nikad kraja!

PS. Usled tehničkih poteškoća (selidba, itd), sledeći put bi mogao biti tek kroz nekoliko nedelja. Takođe, slična je situacija bila i prethodnih par nedelja, no jedna divna stvar je izašla iz toga! Grafička karta koju imam, koja izdržava sve ove tutorijale (kao da su oni ustvari problematični) je bila data kako bi Industry Entertainment rešio problem u svojoj prelepoj igri (na koju ću zauvek biti pomalo ljubomoran na onaj “ah, zašto ovo nije moje čedo” način), SUPERVERSE! Problem je uspešno rešen, usput budirečeno, te pet poena ide za Ravenclaw (ima li Pottermore-ovaca koji ovo čitaju? Je l’ još neko u mojoj kući?)! Do sledećeg puta, koji će zasigurno biti dobačen brzinom svetlosti iz daleke Kalifornije, laku noć uz ovu sliku iz Superverzuma (sva prava imaju autori, ja samo reklamiram)!