Light mapping позволяет рассчитать освещение статичных объектов всего один раз. Так как карты запекаются ещё на стадии разработки игры, это позволяет использовать сложные алгоритмы освещения. Динамический расчёт таких сложных алгоритмов сейчас и в ближайшем будущем невозможен.
Я давно думал о запекании света в вершины, во первых их намного меньше чем пикселов, во вторых плотность сетки больше там, где больше деталей. К тому же с ростом производительности видеокарт выросла и плотность сетки.
Вчера я решил начать изучать эту тему, и написал простейший алгоритм для рассеянного освещения.
Cуть алгоритма: из каждой вершины мы пускаем лучи во всех направлениях, и при столкновении с объектом записываем капельку тени в общую яркость вершины.
Для начала нужно сгенерировать лучи которые равномерно распределены по сфере.
Random.onUnitSphere конечно проще но равномерность этого метода оставляет желать лучшего. Так же можно распределить лучи по широте и долготе, но на полюсах сферы плотность будит намного больше чем на экваторе, поэтому я решил остановиться на кубическом варианте: генерируется одна сторона куба по (x,y), затем копируется на остальные пять.
Vector3[] getRays(int sideCount)
{
int sideCount2 = sideCount * sideCount;
Vector3[] _rays = new Vector3[sideCount2 * 6];
float step = 2f / sideCount;
float startVal = -1f + step * 0.5f;
float tempX = startVal;
float tempY = startVal;
int i = 0;
for (int y = 0; y < sideCount; y++)
{
for (int x = 0; x < sideCount; x++)
{
const float MagicVal = 0.841f;
_rays[i++] = new Vector3(Mathf.Asin(tempX * MagicVal),
Mathf.Asin(tempY * MagicVal), 1f);
tempY += step;
}
tempX += step;
tempY = startVal;
}
for (int j = 0; j < sideCount2; j++)
{
_rays[i++] = new Vector3(_rays[j].x, _rays[j].z, -_rays[j].y);
_rays[i++] = new Vector3(_rays[j].x, -_rays[j].y, -_rays[j].z);
_rays[i++] = new Vector3(_rays[j].x, -_rays[j].z, _rays[j].y);
_rays[i++] = new Vector3(_rays[j].z, _rays[j].y, -_rays[j].x);
_rays[i++] = new Vector3(-_rays[j].z, _rays[j].y, _rays[j].x);
}
for (int j = 0; j < _rays.Length; j++) _rays[j].Normalize();
return _rays;
}
Да, есть тут пара строчек магического кода, но они уменьшают повышение плотности к граням куба.
Количество лучей равно side Count*side Count*6; ниже виден результат работы метода с входным значением 10:

А вот и сам метод:
void AddAO(Mesh mesh, MeshCollider mc, Transform transform)
{
Vector3[] vertices = mesh.vertices;
Vector3[] normals = mesh.normals;
Color[] colors = new Color[vertices.Length];
int samplesLength = rays.Length;
float shadowSmple = 1f / samplesLength;
for (int i = 0; i < vertices.Length; i++)
{
Vector3 vertex = vertices[i];
vertex += normals[i] * 0.001f;//чтобы точка была над поверхностью
vertex = transform.TransformPoint(vertex);
float light = 1f;
for (int j = 0; j < samplesLength; j++)
{
Ray ray = new Ray(vertex, rays[j]);
RaycastHit hit;
if (mc.Raycast(ray, out hit, 10f)) light -= shadowSmple;
}
if (light < 0f) light = 0f;
colors[i] = new Color(light, light, light);
}
mesh.colors = colors;
}
Ниже показан результат его работы. Как видно, даже 24 лучей на вершину достаточно, от 150 до 15000 результат почти не меняется.

Алгоритм имеет минусы, например результат может содержать артефакты как на картинке ниже. Меш имеет сдвоеные вершины со своими нормалями, которые проникают внутрь, и лучи пущенные изнутри меша не сталкиваются с нужными треугольниками.
Так же большим минусом является время работы алгоритма (количество вершин)*(количество треугольников)*(кольчество лучей).
Комментариев нет:
Отправить комментарий