Факторизуйте функцию, в которой наследование нежелательно

У меня есть этот фрагмент кода, который я бы хотел улучшить:

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 ответ
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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *