У меня есть этот фрагмент кода, который я бы хотел улучшить:
std::optional<IntersectionInfo> Scene::intersects(const Ray& ray) const
{
float closest = std::numeric_limits<float>::max();
int index = -1;
int i = 0;
for(auto& sphere : _spheres)
{
auto [b, d] = Intersections::intersects(ray, sphere._shape);
if(b && d < closest)
{
closest = d;
index = i;
}
i++;
}
i = 0;
bool isPlane = false;
for(auto& plane : _planes)
{
auto [b, d] = Intersections::intersects(ray, plane._shape);
if(b && d < closest)
{
closest = d;
index = i;
isPlane = true;
}
i++;
}
i = 0;
bool isBox = false;
for(auto& box : _boxes)
{
auto [b, d] = Intersections::intersects(ray, box._shape);
if(b && d < closest)
{
closest = d;
index = i;
isBox = true;
}
}
i = 0;
bool isTri = false;
for(auto& tri : _triangles)
{
auto [b, d] = Intersections::intersects(ray, tri._shape);
if(b && d < closest)
{
closest = d;
index = i;
isTri = true;
}
}
if(index != -1)
{
IntersectionInfo info;
info._intersection = ray._position + ray._direction * closest;
if(isTri)
{
info._normal = Intersections::computeIntersectionNormal(ray, info._intersection, _triangles[index]._shape);
info._material = *_triangles[index]._material;
}
else if(isBox)
{
info._normal = Intersections::computeIntersectionNormal(ray, info._intersection, _boxes[index]._shape);
info._material = *_boxes[index]._material;
}
else if(isPlane)
{
info._normal = Intersections::computeIntersectionNormal(ray, info._intersection, _planes[index]._shape);
info._material = *_planes[index]._material;
}
else
{
info._normal = Intersections::computeIntersectionNormal(ray, info._intersection, _spheres[index]._shape);
info._material = *_spheres[index]._material;
}
info._intersection += info._normal * 0.001f;
return info;
}
return {};
}
Эта функция работает с несколькими векторами (_spheres, _planes, _boxes и _triangles), которые хранят разные типы. Поскольку код синтаксически идентичен (но пересекается, и вызовы computeIntersectionNormal варьируются в зависимости от типа ввода), я хотел бы найти способ его улучшить.
Очевидным решением было бы использовать наследование и иметь один вектор, хранящий Shape, который, однако, будет иметь виртуальные члены для пересечений и computeInteresctionNormal:
- Я не хочу изменять существующие структуры типов только ради этой функции.
- Эта функция является горячим циклом моей программы, наследование показало видимую стоимость.
Я бы также хотел избежать макросов (если они действительно не простые).
Я придумал это:
enum class ShapeType
{
None,
Sphere,
Plane,
Box,
Triangle
};
template<typename Shape>
std::function<IntersectionInfo()> intersectsWithShapes(const std::vector<MaterialShape<Shape>>& materialShapes, const Ray& ray, ShapeType currentType, float& closest, int& index, ShapeType& type)
{
int i = 0;
for(const auto& materialShape : materialShapes)
{
auto [b, d] = Intersections::intersects(ray, materialShape._shape);
if(b && d < closest)
{
closest = d;
index = i;
type = currentType;
}
i++;
}
return [&]()
{
IntersectionInfo info;
info._intersection = ray._position + ray._direction * closest;
info._normal = Intersections::computeIntersectionNormal(ray, info._intersection, materialShapes[index]._shape);
info._material = *materialShapes[index]._material;
info._intersection += info._normal * 0.001f;
return info;
};
}
std::optional<IntersectionInfo> Scene::intersects(const Ray& ray) const
{
float closest = std::numeric_limits<float>::max();
int index = -1;
auto type = ShapeType::None;
auto F1 = intersectsWithShapes(_spheres, ray, ShapeType::Sphere, closest, index, type);
auto F2 = intersectsWithShapes(_planes, ray, ShapeType::Plane, closest, index, type);
auto F3 = intersectsWithShapes(_boxes, ray, ShapeType::Box, closest, index, type);
auto F4 = intersectsWithShapes(_triangles, ray, ShapeType::Triangle, closest, index, type);
decltype(F1) F;
switch(type)
{
case ShapeType::None: return {};
case ShapeType::Sphere: F = F1; break;
case ShapeType::Plane: F = F2; break;
case ShapeType::Box: F = F3; break;
case ShapeType::Triangle: F = F4; break;
}
return F();
}
Я предпочитаю это вышеупомянутой функции, потому что добавление фигуры проще и менее подвержено ошибкам, а весь интересный код находится в небольшой функции. Но это не идеально, потому что теперь Scene :: corrects () полностью состоит из шаблонного кода, не очевидно, почему beansWithShapes возвращает лямбду, и это приводит к видимым затратам (на этот раз только в отладочной сборке).
1 ответ
Не могли бы мы использовать вторую функцию-шаблон вместо лямбды? например:
template<class Shape>
IntersectionInfo getIntersectionInfo(std::vector<MaterialShape<Shape>> const& materialShapes, Ray const& ray, float closest, std::size_t index)
{
auto position = ray._position + ray._direction * closest;
auto normal = Intersections::computeIntersectionNormal(ray, position, materialShapes[index]._shape);
auto material = *materialShapes[index]._material;
position += info._normal * 0.001f;
return { position, normal, material };
};
Затем мы можем вызвать его прямо из оператора switch:
std::optional<IntersectionInfo> Scene::intersects(const Ray& ray) const
{
float closest = std::numeric_limits<float>::max();
int index = -1;
auto type = ShapeType::None;
intersectsWithShapes(_spheres, ray, ShapeType::Sphere, closest, index, type);
intersectsWithShapes(_planes, ray, ShapeType::Plane, closest, index, type);
intersectsWithShapes(_boxes, ray, ShapeType::Box, closest, index, type);
intersectsWithShapes(_triangles, ray, ShapeType::Triangle, closest, index, type);
switch(type)
{
case ShapeType::None: return {};
case ShapeType::Sphere: return getIntersectionInfo(_spheres, ray, closest, index);
case ShapeType::Plane: return getIntersectionInfo(_planes, ray, closest, index);
case ShapeType::Box: return getIntersectionInfo(_boxes, ray, closest, index);
case ShapeType::Triangle: return getIntersectionInfo(_triangles, ray, closest, index);
}
return { };
}
Другая возможность — использовать std::variant
для типа фигуры. Затем мы можем хранить все формы в одном контейнере и использовать std::visit
для отправки функциям, которым требуются определенные типы, например:
using Shape = std::variant<Sphere, Plane, Box, Triangle>;
std::optional<IntersectionInfo> Scene::intersects(Ray const& ray) const
{
auto hit = false;
auto closestDistance = std::numeric_limits<float>::max();
auto closestIndex = std::size_t(-1);
auto const intersects = [&] (auto const& shape) { return Intersections::intersects(ray, shape) };
for (auto i = std::size_t{ 0 }; i != _shapes.size(); ++i)
{
auto [b, d] = std::visit(intersects, _shapes[i]._shape); // _shape is now the "Shape" variant type
if (!b) continue;
if (d >= closest) continue;
hit = true;
closestDistance = d;
closestIndex = i;
}
if (!hit) return { };
auto const getInfo = [&] (auto const& shape)
{
auto position = ray._position + ray._direction * closestDistance;
auto normal = Intersections::computeIntersectionNormal(ray, position, shape);
auto material = *_shapes[closestIndex]._material;
position += info._normal * 0.001f;
return { position, normal, material };
};
return std::visit(getInfo, _shapes[closestIndex]._shape);
}
std::visit
приведёт вариант к правильному типу и передаст его лямбде.
Лямбда принимает auto
аргумент, что фактически означает, что у него есть шаблонный оператор вызова:
struct anonymous_lambda_type
{
template<class ShapeT>
auto operator()(ShapeT const& shape) { ... };
}
Вместо этого мы могли бы определить наш собственный функтор, используя перегрузку, если бы нам нужно было изменить поведение в зависимости от конкретного типа (чего здесь нет):
struct ray_intersection
{
std::tuple<bool, float> operator()(Sphere const& sphere) { ... }
std::tuple<bool, float> operator()(Plane const& plane) { ... }
...
};
Вариант будет отправлен в фигуру с такими же накладными расходами, как и виртуальная функция. Создание каждой формы в ее собственной коллекции происходит быстрее просто потому, что она сохраняет косвенное обращение в вызове функции, но обрабатывает все подобные вещи вместе, поэтому код для одной формы остается в кеше.
— JDłuosz
Наверное. Я думаю, что мы, вероятно, захотим реализовать структуру ускорения (BVH, сетку и т. Д.), Прежде чем беспокоиться об этом.
— user673679