首页
学习
活动
专区
圈层
工具
发布

Ray实现
EN

Code Review用户
提问于 2020-09-16 21:23:12
回答 2查看 203关注 0票数 5

Problem语句/上下文:

表示射线,给定原点和方向。此外,还定义了一个min_tmax_t值。这些值定义了沿射线的距离,使描述射线上的空隙成为可能。射线类的建议是支持2d射线跟踪(一种2D射线跟踪可视化工具),如以下图片所示:

实现:

我提议的C++17解决方案:

代码语言:javascript
复制
#pragma once
#ifndef Flatland_Ray_ceb9b0b4_4236_4bfe_b918_11a02c72ad7c_h
#define Flatland_Ray_ceb9b0b4_4236_4bfe_b918_11a02c72ad7c_h

#include "flatland/core/namespace.h"
#include "flatland/math/point.h"
#include "flatland/math/vector.h"

#include 
#include 

FLATLAND_BEGIN_NAMESPACE

template 
class Ray {
public:
    Ray(const PointType &origin, const VectorType &direction, const ScalarType min_t, const ScalarType max_t)
            : origin(origin), direction(direction), min_t(min_t), max_t(max_t) {
    }

    PointType operator()(const float t) const {
        assert(isDirectionVectorNormalized());
        return origin + t * direction;
    }

    bool isDirectionVectorNormalized() const {
        const ScalarType epsilon = static_cast(0.001);
        return  direction.norm() - static_cast(1.0) < epsilon;
    }

public:
    PointType origin;
    VectorType direction;
    ScalarType min_t;
    ScalarType max_t;
    enum { dimension = Dimension};
};

template 
using Ray2 = Ray, Vector2, ScalarType, 2>;

template 
using Ray3 = Ray, Vector3, ScalarType, 3>;

using Ray2f = Ray2;
using Ray2d = Ray2;
using Ray3f = Ray3;
using Ray3d = Ray3;

namespace internal {
    template 
    std::string convertTypeToString() {
        std::string typeAsString = "UnknownType";
        if(std::is_same::value)
            typeAsString = "i";
        if(std::is_same::value)
            typeAsString = "f";
        if(std::is_same::value)
            typeAsString = "d";
        return typeAsString;
    }

    template 
    std::string dimensionAsString() {
        std::string dimensionAsString = "UnknownDimension";
        if(Dimension == 2)
            dimensionAsString = "2";
        if(Dimension == 3)
            dimensionAsString = "3";

        return dimensionAsString;
    }
}

template 
std::ostream &operator<<(std::ostream &os, const Ray &r) {
    auto typeAsString = internal::convertTypeToString();
    auto dimensionAsString = internal::dimensionAsString();

    os << "Ray" << dimensionAsString << typeAsString << "[" << std::endl
       << "  origin = " << r.origin << "," << std::endl
       << "  direction = " << r.direction << "," << std::endl
       << "  min_t = " << r.min_t << "," << std::endl
       << "  max_t = " << r.max_t << "" << std::endl
       << "]" << std::endl;

    return os;
}

FLATLAND_END_NAMESPACE

#endif // end define Flatland_Ray_ceb9b0b4_4236_4bfe_b918_11a02c72ad7c_h

My测试:

代码语言:javascript
复制
#include "flatland/math/ray.h"

#include 

using namespace Flatland;

TEST(Ray2f, GivenDirectionOriginMaxMinT_WhenRayIsInitialized_ThenInitzalizedRayValues) {
    Point2f origin(0.0f, 0.0f);
    Vector2f direction(1.0f, 0.0f);

    Ray2f r(origin, direction, 1.0f, 10.0f);

    EXPECT_THAT(r.origin.x(), 0.0f);
    EXPECT_THAT(r.origin.y(), 0.0f);
    EXPECT_THAT(r.direction.x(), 1.0f);
    EXPECT_THAT(r.direction.y(), 0.0f);
    EXPECT_THAT(r.min_t, 1.0f);
    EXPECT_THAT(r.max_t, 10.0f);
}

TEST(Ray2f, GivenNormalizedRay_WhenCheckingIfRayIsNormalized_ThenNormalizedIsTrue) {
    Point2f origin(0.0f, 0.0f);
    Vector2f direction(1.0f, 0.0f);

    Ray2f r(origin, direction, 1.0f, 10.0f);

    EXPECT_TRUE(r.isDirectionVectorNormalized());
}

TEST(Ray2f, GivenNonNormalizedRay_WhenCheckingIfRayIsNormalized_ThenNormalizedIsFalse) {
    Point2f origin(0.0f, 0.0f);
    Vector2f direction(2.0f, 0.0f);

    Ray2f r(origin, direction, 1.0f, 10.0f);

    EXPECT_FALSE(r.isDirectionVectorNormalized());
}

TEST(Ray2f, GivenANormalizedRay_WhenDeterminPointAtMinAndMaxT_ThenExpectStartAndEndPoint) {
    Point2f origin(0.0f, 0.0f);
    Vector2f direction(1.0f, 0.0f);

    Ray2f r(origin, direction, 1.0f, 10.0f);

    Point2f start = r(r.min_t);
    Point2f end = r(r.max_t);

    EXPECT_THAT(start.x(), 1.0f);
    EXPECT_THAT(start.y(), 0.0f);
    EXPECT_THAT(end.x(), 10.0f);
    EXPECT_THAT(end.y(), 0.0f);
}

TEST(Ray2f, GivenARay_WhenPrintedToStdOutput_ExpectRayAsStringRepresentation) {
    testing::internal::CaptureStdout();

    Point2f origin(0.0f, 0.0f);
    Vector2f direction(1.0f, 0.0f);

    Ray2f r(origin, direction, 1.0f, 10.0f);

    std::cout << r;

    std::string output = testing::internal::GetCapturedStdout();

    EXPECT_THAT(output, ::testing::HasSubstr("Ray2f["));
    EXPECT_THAT(output, ::testing::HasSubstr("origin ="));
    EXPECT_THAT(output, ::testing::HasSubstr("direction ="));
    EXPECT_THAT(output, ::testing::HasSubstr("min_t = 1"));
    EXPECT_THAT(output, ::testing::HasSubstr("max_t = 10"));
    EXPECT_THAT(output, ::testing::HasSubstr("]"));
}

TEST(Ray2d, GivenARay2d_WhenPrintedToStdOutput_ExpectRayTypeRay2d) {
    testing::internal::CaptureStdout();

    Point2d origin(0.0, 0.0);
    Vector2d direction(1.0, 0.0);
    Ray2d r(origin, direction, 1.0, 10.0);

    std::cout << r;

    std::string output = testing::internal::GetCapturedStdout();

    EXPECT_THAT(output, ::testing::HasSubstr("Ray2d["));
    EXPECT_THAT(output, ::testing::HasSubstr("origin ="));
    EXPECT_THAT(output, ::testing::HasSubstr("direction ="));
    EXPECT_THAT(output, ::testing::HasSubstr("min_t = 1"));
    EXPECT_THAT(output, ::testing::HasSubstr("max_t = 10"));
    EXPECT_THAT(output, ::testing::HasSubstr("]"));
}

TEST(Ray3f, GivenARay3f_WhenPrintedToStdOutput_ExpectRayTypeRay3f) {
    testing::internal::CaptureStdout();

    Point3f origin(0.0f, 0.0f, 0.0f);
    Vector3f direction(1.0f, 0.0f, 0.0f);
    Ray3f r(origin, direction, 1.0f, 10.0f);

    std::cout << r;

    std::string output = testing::internal::GetCapturedStdout();

    EXPECT_THAT(output, ::testing::HasSubstr("Ray3f["));
    EXPECT_THAT(output, ::testing::HasSubstr("origin ="));
    EXPECT_THAT(output, ::testing::HasSubstr("direction ="));
    EXPECT_THAT(output, ::testing::HasSubstr("min_t = 1"));
    EXPECT_THAT(output, ::testing::HasSubstr("max_t = 10"));
    EXPECT_THAT(output, ::testing::HasSubstr("]"));
}

TEST(internal_convertTypToString, GivenIntegralType_WhenConverTypeToString_ThenShortStringRepersentationExpected) {
    EXPECT_THAT(internal::convertTypeToString(), ::testing::HasSubstr("i"));
    EXPECT_THAT(internal::convertTypeToString(), ::testing::HasSubstr("f"));
    EXPECT_THAT(internal::convertTypeToString(), ::testing::HasSubstr("d"));
}

TEST(internal_dimensionAsString, GivenRayDimension_WhenConvertingToStringRepresentation_ThenExpectValidNumber) {
    EXPECT_THAT(internal::dimensionAsString<2u>(), ::testing::HasSubstr("2"));
    EXPECT_THAT(internal::dimensionAsString<3u>(), ::testing::HasSubstr("3"));
}

问题:

实现

  • 从C++17的角度看:我应该使用的语言是否有更现代的特性?
  • 我正在考虑在类似于Raytemplate class Ray中更改PointType,然后在类中使用D11。但我认为,如果我将来要改变PointType (目前的特征是在你的引擎盖下使用--也许我以后会换别的东西),那就更难了。关于这一点,我会怎么看呢?

测试

  • 你认为考试中有太多的重复吗?
  • 是否有任何重要的边缘情况,我也应该考虑?
  • 还有其他暗示吗?建议?

您对实现和测试是否有其他的反馈、改进?

EN

回答 2

Code Review用户

回答已采纳

发布于 2020-09-16 22:17:31

保持简单

使用接吻原则,不要使事情变得过于复杂。使用更多的语言特性本身不应该是一个目标。此外,不要在类中添加超出必要的内容。你为什么要跟踪dimension?全班没有任何东西能利用它。维度是PointTypeVectorType的属性,因此它在某种程度上已经存在了。类似地,指定ScalarType与底层标量类型的PointTypeVectorType不同是否有意义?

class Ray没有什么是私有的。考虑让它成为一个struct。而且,就像现在一样,您甚至不需要显式地为它创建构造函数。这是一个结构,去掉了一些不必要的行李:

代码语言:javascript
复制
template 
struct Ray {
    using ScalarType = PointType::value_type;

    PointType operator()(const float t) const {
        assert(isDirectionVectorNormalized());
        return origin + t * direction;
    }

    bool isDirectionVectorNormalized() const {
        const ScalarType epsilon{0.001};
        return direction.norm() - ScalarType{1.0} < epsilon;
    }

    PointType origin{};
    VectorType direction{};
    ScalarType min_t{};
    ScalarType max_t{};
};

在这里,我假设PointType包含有关它存储的值的底层类型的信息。我还使用了这样的事实:我们可以构造一个类型的值,而不是static_cast<>。它节省了一些打字的时间。

那么如何得到一个Ray的维数呢?同样,我将确保您的PointType包含该信息,因此要获得射线的维度,只需编写如下内容:

代码语言:javascript
复制
Ray<...> ray = ...;
auto dimension = ray.origin.dimension;

使origindirection

无法使用不同的类型

创建一个Ray, Vector3>意味着什么?这没有意义。要么我只使用一种模板类型:

代码语言:javascript
复制
template 
struct Ray {
    ...
    VectorType origin;
    VectorType direction;
};

或者使用一些SFINAE技巧来执行维度和基本标量类型是相同的。

强制规范化

如果您不应该使用未规范化的Ray构造direction,那么我将只编写一个使direction规范化的构造函数,而不是在运行时使用assert()来检查这一点:

代码语言:javascript
复制
template <...>
struct Ray {
    Ray(const PointType &origin, const VectorType &direction, ...)
        : origin(origin)
        , direction(direction.normalized())
        , ...

还请注意,您的函数isDirectionNormalized()忘记接受差值的绝对值。浮点比较很难,实施规范化就回避了这个问题。

关于测试

你做太多测试了吗?也许吧。老实说,我不会编写只检查构造函数是否正确地将值复制到成员变量中的测试用例,因为这是一个非常基本的语言函数,如果不起作用,您就会遇到更大的问题。但是,如果您有一个使direction规范化的构造函数,那么我肯定会测试它。

在测试std::ostream操作符Ray时,我不会写到std::cout,然后让您的测试库跳过圈来获得标准输出,而是创建一个std::ostringstream来捕获输出。而且,我不会检查是否存在子字符串,而是验证整个字符串是否与预期的完全相同:

代码语言:javascript
复制
Ray2f r({1.0f, 2.0f}, {3.0f, 4.0f}, 5.0f, 6.0f);
std::ostringstream os;
os << r;

EXPECT_THAT(os.str(), std::string(R"(Ray2f[
  origin = {1.0, 2.0},
  direciton = {3.0, 4.0},
  min_t = 5.0,
  max_t = 6.0
])"));
票数 5
EN

Code Review用户

发布于 2020-09-17 15:08:23

convertTypeToStringdimensionAsString的实现有点低效和不自然。首先,不需要在分配给它并最终返回的函数中使用一个变量。其次,不应将未知或不可接受的值表示为字符串。从用户的角度来考虑它--不得不与"UnknownType“这样的”神奇值“进行比较是不太好的(也不适合改变)。此外,您已经在编译时获得了类型信息,那么为什么不使用它呢?

例如,我们可以写:

代码语言:javascript
复制
template 
std::string convertTypeToString();

template <>
std::string convertTypeToString() {
    return "i";
}

template <>
std::string convertTypeToString() {
    return "f";
}

template <>
std::string convertTypeToString() {
    return "d";
}

否则,您在编译时会遇到错误,如果您尝试这样做,比如convertTypeToString();。如果您愿意,还可以使用static_assert输出更有意义的错误消息,比如“未实现”之类的错误消息。

您最好通过使用constexpr和类来实现这个完整的编译时:

代码语言:javascript
复制
template 
struct convertTypeToString;

template <>
struct convertTypeToString {
    constexpr static char value = 'i';
};

// Later on, you just do
convertTypeToString::value;
票数 2
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/249457

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档