RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / user-281229

Alex Nem's questions

Martin Hope
Alex Nem
Asked: 2024-12-16 22:11:32 +0000 UTC

套接字、C++ 和 boost asio

  • 6

我正在尝试弄清楚套接字是如何工作的。我使用boost asio库,但我仍然没有完全理解一些点:

假设 TCP 连接已成功建立并且套接字已打开。现在我想“向服务器发出请求”并发送某条数据:

template<typename P>
void send_with_payload(Socket& socket, const P& payload)
{
    // Общий размер запроса
    const auto len = sizeof(Header) + sizeof(P);

    // Заголовок
    buffer_copy(buffer_->prepare(sizeof(Header)), boost::asio::buffer(&header_, sizeof(Header)));
    buffer_->commit(sizeof(Header));

    // Полезная нагрузка
    buffer_copy(buffer_->prepare(sizeof(P)), boost::asio::buffer(&payload, sizeof(P)));
    buffer_->commit(sizeof(P));

    // Писать в сокет (покуда не будет записано всё)
    boost::system::error_code error;
    write(socket, *buffer_, boost::asio::transfer_exactly(len), error);
    if(error.failed())
    {
        buffer_->consume(buffer_->size());
        throw std::runtime_error(error.message());
    }

    buffer_->consume(len);
}

而此时问题就出现了:

该线boost::asio::transfer_exactly本质上是一个终止条件。这意味着对套接字的写入将继续,直到“写入”指定的字节数。但写下来到底意味着什么呢?这是否意味着它已在另一面被读取?而且还没读完,流量就被堵住了? (这正是实践所表明的,但我不确定这种行为是否正确)或者另一侧根本不需要同时读取,数据仍然会发送,但会被“暂停”通过套接字中的服务器(对它们的诽谤,你可以随时执行,并且客户端不会等待服务器读取它们)?或者也许这两种选择都是可能的?

第二点(服务器端)。鉴于录音只录制过一次,“读几次”的正确性如何?例如:

首先,我们从套接字读取数据的第一部分:

void receive(Socket& socket
    , const TypeBits& types
    , const Operation& op = Operation::O_UNKNOWN)
{
    // Читаем ТОЛЬКО заголовок (чтение прекращается когда получен его размер)
    boost::system::error_code error;
    read(socket, *buffer_, boost::asio::transfer_at_least(sizeof(Header)), error);

    auto* data = const_cast<void*>(buffer_->data().data());
    header_ = *static_cast<Header*>(data);

    buffer_->consume(sizeof(Header));
    header_received_ = true;
}

我们第二次读取剩余部分(根据第一次读取时获得的数据,确定剩余部分的大小)

template<typename P>
class RequestWithPayload final : public Request
{
public:
    explicit RequestWithPayload(Request* base)
    : base_(base)
    {}

    ~RequestWithPayload() override = default;

    void receive_payload(Socket& socket) override
    {
        // Читаем "полезную нагрузку" (чтение завершается когда получен размер полезной нагрузки)
        boost::system::error_code error;
        read(socket, *buffer_, boost::asio::transfer_exactly(sizeof(P)), error);
        
        auto* data = const_cast<void*>(buffer_->data().data());
        payload_ = *static_cast<P*>(data);
        buffer_->consume(sizeof(P));
    }

    const P& payload()
    {
        return payload_;
    }

protected:
    P payload_;
    Request* base_;
};

我想做的事情(一次写入 - 两次读取)不起作用(服务器在第二次读取时挂起并永远等待),我不确定这是否是因为它对于套接字来说从根本上来说是错误的(每次我们写,我们立即需要一切并阅读),或出于其他原因。

我将很高兴收到详细的答复。

PS感谢评论,我发现了一个错误,导致服务器在第二次读取时挂起。阅读时,一切都是关于至少transfer_at_least。有必要读取固定且精确的大小(transfer_exactly)。但总的来说,提出的问题仍然是相关的:

  • 当流被阻塞时 - 要解除阻塞,是否需要第二方参与(读取发送的数据)?
  • 如果是,具体是哪一个? (阅读所有内容/阅读一些内容/尝试阅读)
  • 如果不是,解除线程阻塞的标准是什么,函数在什么时候返回控制权?
c++
  • 1 个回答
  • 52 Views
Martin Hope
Alex Nem
Asked: 2022-08-05 03:15:05 +0000 UTC

std::initializer_list 对象初始化 (C++)

  • 0

我有一个接受参数的函数std::initializer_list

void CEventManager::sendEvent(EEventType eEventType, const std::initializer_list<CEventArg> &rcEventArgs) noexcept
{
    // Найти список (вектор) в ассоциативном массиве (map) По типу события
    auto it = m_cEventHandlers.find(eEventType);
    // Если найден
    if (it != m_cEventHandlers.end())
    {
        // Пройтись по всем элементам списка (слушателям) и вызывать функцию обработки события
        for (auto& handler : it->second)
        {
            handler->handleEvent(eEventType, rcEventArgs);
        }
    }
}

我使用完全initializer_list不是vector因为我在某处读到这是一个非常快速和高效的容器,它允许您实现可变数量参数的传输。

我需要通过使用向量初始化 rcEventArgs 参数来调用此函数。也就是说,我有一个向量,其中包含我想传递的参数列表。

void CEventManager::update([[maybe_unused]] float fDelta)
{
    // Заблокировать чтение/запись массива отложенных событий
    m_cMutexQueuedEvents.lock();

    // Последнее отложенное событие
    auto& rsEventInfo = m_cQueuedEvents.back();
    
    // Список аргументов события
    auto cArgList = std::initializer_list<CEventArg>(/*Как добавить сюда мои аргументы из вектора динамически???*/);
    
    // Отправить
    this->sendEvent(rsEventInfo.m_eEventType, cArgList);
    
    // Удалить отложенное событие из списка
    m_cQueuedEvents.pop_back();

    // Разблокировать чтение/запись массива отложенных событий
    m_cMutexQueuedEvents.unlock();
}

但是我没有找到明确的答案,有没有可能,如果有,怎么做?

c++ инициализация
  • 1 个回答
  • 25 Views
Martin Hope
Alex Nem
Asked: 2022-06-19 23:51:12 +0000 UTC

C++ 中的继承和接口

  • 0

假设我有一个完全虚拟/抽象类INode(接口),它的所有方法都是纯虚拟的。随后,我想已经从它继承一个CNode将实现这些方法的类。我也想创建一个子接口,例如INodeSpatial,也是完全虚拟的,它会继承所有父级的虚拟方法+添加一些自己的虚拟方法。稍后我想从它继承CNodeSpatial所有这些方法将被实现的地方。嗯,等等..

但这里有以下内容:

  1. CNode继承自的类INode将实现虚方法INode
  2. 再次,全班CNodeSpatial希望只来自 virtual INodeSpatial,而不是来自CNode,这意味着CNodeSpatial我将不得不描述在CNode
  3. 有一个想法不这样做 - 继承CNodeSpatial和INodeSpatial继承(已经描述CNode了所有虚拟方法的实现)。INode然后我将不得不只描述那些在INodeSpatial

但问题来了:

这有多好和正确?事实证明,该类CNodeSpatial是两次继承自INode(因为它INode是 andCNode和 and的父级INodeSpatial)。那么有可能吗?这会导致未来出现问题吗?这种方法在多大程度上符合“良好做法”?

例子:

接口

class INode
{
public:
    virtual void methodA() = 0;
    virtual void methodB() = 0;
    virtual ~INode() = default;
};

class INodeSpatial : public INode
{
public:
    virtual void methodC(int foo) = 0;
    virtual void methodD(int foo) = 0;
    ~INodeSpatial() override = default;
};

“真实”课程:

class CNode : public INode
{
public:
    void methodA() override
    {
        //TODO: Implementation of INode methods in CNode
    }

    void methodB() override
    {
        //TODO: Implementation of INode methods in CNode
    }
};

class CNodeSpatial : public INodeSpatial
{
public:
    void methodA() override
    {
        //TODO: Implementation of INode methods in CNodeSpatial (already done in CNode)
    }

    void methodB() override
    {
        //TODO: Implementation of INode methods in CNodeSpatial (already done in CNode)
    }

    void methodC([[maybe_unused]] int foo) override
    {
        //TODO: Implementation of INodeSpatial methods
    }

    void methodD([[maybe_unused]] int foo) override
    {
        //TODO: Implementation of INodeSpatial methods
    }
};

如您所见,该类CNodeSpatial必须复制已经在 中描述的实现CNode,因为继承完全来自虚拟接口。但是如果 AND 继承自СNode,问题似乎就消失了:

class CNodeSpatial : public INodeSpatial, public CNode
{
public:
    void methodC([[maybe_unused]] int foo) override
    {
        //TODO: Implementation of INodeSpatial methods
    }

    void methodD([[maybe_unused]] int foo) override
    {
        //TODO: Implementation of INodeSpatial methods
    }
};

但最后,事实证明,该类实际上是从 INode 继承了 2 次,这让我感到困惑。这可以?他们这样做吗?以后会不会出问题?

在此处输入图像描述

c++ ооп
  • 2 个回答
  • 83 Views
Martin Hope
Alex Nem
Asked: 2021-10-22 18:39:13 +0000 UTC

给定角度内半球内的随机向量(最优算法)

  • 0

在追踪光线时,通常需要在半球内生成一个随机矢量,该矢量与原始矢量偏离某个给定角度。在互联网上,这个问题没有明确而详细的答案。虽然我数学不是很强,但我还是想了解一下是怎么回事,而不是随便拿一个现成的公式。

再次,简单介绍一下问题的本质:

有一些向量,有一些角度(通常在这种情况下它被称为立体角,但我可能是错的),你需要选择一些在这个角度内的随机向量。角度可以全部为180度,并覆盖整个半球。

我知道这项任务与世界一样古老,但是,我仍然希望有人能仔细研究一下愚蠢的人道主义者是如何正确完成这项任务的。而且,你可以从数学的角度给出最优化的例子,也可以从代码的角度给出最优化的例子。他们经常不同。谢谢你。

математика
  • 1 个回答
  • 10 Views
Martin Hope
Alex Nem
Asked: 2020-10-06 03:17:01 +0000 UTC

Vulkan Ray Tracing (VK_KHR_ray_tracing) - 访问顶点属性

  • 4

我正在尝试在 Vulkan 上掌握光线追踪,主要信息来源是本教程。我想出了如何构建 BLAS 和 TLAS,创建了一个 SBT 表,最后开始跟踪场景: 在此处输入图像描述

我正要继续简单照明的实现,然后出现了一个问题,我在教程中找不到答案..

如何获取顶点属性?

最让我困惑的是:

  1. 在我最初为光栅器制作的渲染器中,场景由一组网格描述,其中每个网格都有自己的顶点和索引缓冲区。
  2. 在光线追踪的情况下,这些缓冲区用作 BLAS(低级加速结构)的基础,但 BLAS 仅包含位置,并且无法访问 BLAS 中的属性。
  3. 有一个 TLAS(顶级加速结构),它基本上设置了光线追踪的场景。有一个实例这样的概念,它已经引用了 BLAS(也就是说,场景可以由许多不同的缓冲区来描述,而不仅仅是一个,正如它已经暗示的那样)
  4. 有了这一切,关于单个实例的顶点属性什么都没有说,并且着色器在相交期间可以获得的最大值是重心坐标和一些gl_PrimitiveID(据我了解,存储三角形的索引),以及作为gl_InstanceID(据我了解,TLAS 中的 instance'a 索引)

所有这些都引出了以下问题:

  1. 这一切是否意味着我需要将几何图形重新加载到某种 SSBO 中才能访问顶点属性。
  2. 如果是这样,那么如果我的场景是由不同的顶点缓冲区描述的,而不是一个大的,在这种情况下我该怎么办?事实证明,您需要创建某种将存储在 SSBO 中的二维数组,其键将是实例 ID?
  3. 如果是这样,并且如果制作了这样的数组,是否意味着除了顶点本身之外还需要存储变换,以便获得属性的实际值(例如,法线)?

我上面提到的教程显示了以下着色器代码:

layout(binding = 2, set = 1, scalar) buffer ScnDesc { sceneDesc i[]; } scnDesc;
layout(binding = 5, set = 1, scalar) buffer Vertices { Vertex v[]; } vertices[];
layout(binding = 6, set = 1) buffer Indices { uint i[]; } indices[];

// Object of this instance
uint objId = scnDesc.i[gl_InstanceID].objId;

// Indices of the triangle
ivec3 ind = ivec3(indices[nonuniformEXT(objId)].i[3 * gl_PrimitiveID + 0],   //
                indices[nonuniformEXT(objId)].i[3 * gl_PrimitiveID + 1],   //
                indices[nonuniformEXT(objId)].i[3 * gl_PrimitiveID + 2]);  //
// Vertex of the triangle
Vertex v0 = vertices[nonuniformEXT(objId)].v[ind.x];
Vertex v1 = vertices[nonuniformEXT(objId)].v[ind.y];
Vertex v2 = vertices[nonuniformEXT(objId)].v[ind.z];

但我不太明白这里发生了什么。什么是 nonuniformEXT,为什么会这样?还有一个顶点数组和一个索引数组作为描述符......一个一维数组......好像它包含场景的所有几何形状。

总的来说,我对此完全感到困惑,如果有人能阐明这一切,我将不胜感激。

c++
  • 1 个回答
  • 10 Views
Martin Hope
Alex Nem
Asked: 2020-08-21 03:03:52 +0000 UTC

Assimp 骨骼动画 (C++)

  • 2

我正在尝试使用 Assimp 库在 C++/Vulkan 中实现骨骼动画。我设法加载了骨架本身,一切似乎都井井有条。现在可以控制每个骨骼并指定相对于绑定变换的局部变换。一切似乎都运行良好。在代码中它看起来像这样

    // Загрузка геометрии
    auto ar2rGeometry = vk::helpers::LoadVulkanGeometryMesh(_vkRenderer,"Ar2r-Devil-Pinky.dae", true);
    // Загрузка скелета
    auto skeleton = vk::helpers::LoadVulkanMeshSkeleton("Ar2r-Devil-Pinky.dae");

    // Добавление меша на сцену, его настройка и установка скелета
    auto Ar2r = _vkRenderer->addMeshToScene(ar2rGeometry);
    Ar2r->setPosition({0.0f, 0.0f, 0.0f}, false);
    Ar2r->setScale({2.0f, 2.0f, 2.0f});
    Ar2r->setSkeleton(std::move(skeleton));

    // Доступ к костям скелета
    auto torso = Ar2r->getSkeletonPtr()->getRootBone()->getChildrenBones()[0];
    auto leg1 = Ar2r->getSkeletonPtr()->getRootBone()->getChildrenBones()[1];
    auto leg2 = Ar2r->getSkeletonPtr()->getRootBone()->getChildrenBones()[2];
    auto neck = torso->getChildrenBones()[0];

    // Управление костями
    torso->setLocalTransform(glm::rotate(glm::mat4(1.0f),glm::radians(45.0f),{1.0f, 0.0f, 0.0f}));
    neck->setLocalTransform(glm::rotate(glm::mat4(1.0f),glm::radians(-20.0f),{1.0f, 0.0f, 0.0f}));
    leg1->setLocalTransform(glm::rotate(glm::mat4(1.0f),glm::radians(30.0f),{1.0f, 0.0f, 0.0f}));
    leg2->setLocalTransform(glm::rotate(glm::mat4(1.0f),glm::radians(-30.0f),{1.0f, 0.0f, 0.0f}));

骨骼的软件控制(直接来自代码)似乎工作得很好。结果,我可以使用骨骼使网格变形。

不变形 有变形

以防万一,我将附上计算分支矩阵(骨骼及其所有后代)的代码。这是 SkeletonBone 类的一个方法。

            void calculateBranch(bool callUpdateCallbackFunction = true, unsigned calcFlags = CalcFlags::eFullTransform | CalcFlags::eBindTransform | CalcFlags::eInverseBindTransform)
            {
                // Если у кости есть родительская кость
                if(pParentBone_ != nullptr)
                {
                    // Общая initial (bind) трансформация для кости учитывает текущую и родительскую (что в свою очередь справедливо и для родительской)
                    if(calcFlags & CalcFlags::eBindTransform)
                        totalBindTransform_ = pParentBone_->totalBindTransform_ * this->localBindTransform_;

                    // Общая полная (с учетом задаваемой) трансформация кости (смещаем на localTransform_, затем на initial, затем на общую родительскую трансформацию)
                    if(calcFlags & CalcFlags::eFullTransform)
                        totalTransform_ = pParentBone_->totalTransform_ * this->localBindTransform_ * this->localTransform_;
                }
                // Если нет родительской кости - считать кость корневой
                else
                {
                    if(calcFlags & CalcFlags::eBindTransform)
                        totalBindTransform_ = this->localBindTransform_;

                    if(calcFlags & CalcFlags::eFullTransform)
                        totalTransform_ = this->localBindTransform_ * this->localTransform_;
                }

                // Инвертированная матрица bind трансформации
                if(calcFlags & CalcFlags::eInverseBindTransform)
                    totalBindTransformInverse_ = glm::inverse(totalBindTransform_);

                // Если есть указатель на объект скелета и индекс валиден
                if(pSkeleton_ != nullptr && index_ < pSkeleton_->modelSpaceFinalTransforms_.size())
                {
                    // Итоговая матрица трансформации для точек находящихся в пространстве модели
                    // Поскольку общая трансформация кости работает с вершинами находящимися в пространстве модели,
                    // они в начале должны быть переведены в пространство кости.
                    pSkeleton_->modelSpaceFinalTransforms_[index_] = totalTransform_ * totalBindTransformInverse_;

                    // Для ситуаций, если вершины задаются сразу в пространстве кости
                    pSkeleton_->boneSpaceFinalTransforms_[index_] = totalTransform_;
                }

                // Рекурсивно выполнить для дочерних элементов (если они есть)
                if(!this->childrenBones_.empty()){
                    for(auto& childBone : this->childrenBones_){
                        childBone->calculateBranch(false, calcFlags);
                    }
                }

                // Если нужно вызвать функцию обновления UBO
                if(callUpdateCallbackFunction && this->pSkeleton_ != nullptr && this->pSkeleton_->updateCallback_ != nullptr){
                    this->pSkeleton_->updateCallback_();
                }
            }

我还将附上用于加载骨架信息的函数的代码:

    /**
     * Загрузка скелета из файла 3D-моделей
     * @param filename Имя файла в папке Models
     * @return Объект скелета
     */
    vk::scene::UniqueSkeleton LoadVulkanMeshSkeleton(const std::string &filename)
    {
        // Итоговый скелет
        vk::scene::UniqueSkeleton skeleton = std::make_unique<vk::scene::Skeleton>();

        // Полный путь к файлу
        auto path = ::tools::ExeDir().append("..\\Models\\").append(filename);

        // Импортер Assimp
        Assimp::Importer importer;

        // Получить сцену
        const aiScene* scene = importer.ReadFile(path.c_str(),
                aiProcess_Triangulate |
                aiProcess_JoinIdenticalVertices |
                //aiProcess_PreTransformVertices |
                aiProcess_FlipWindingOrder |
                aiProcess_PopulateArmatureData
        );

        // Если не удалось загрузить
        if(scene == nullptr){
            throw std::runtime_error(std::string("Can't load geometry from (").append(path).append(")").c_str());
        }

        // Если нет геометрических мешей
        if(!scene->HasMeshes()){
            throw std::runtime_error(std::string("Can't find any geometry meshes from (").append(path).append(")").c_str());
        }

        // Первый меш сцены
        auto pFirstMesh = scene->mMeshes[0];

        // Если у меша есть кости
        if(pFirstMesh->HasBones())
        {
            // Инициализировать скелет
            skeleton = std::make_unique<vk::scene::Skeleton>(pFirstMesh->mNumBones);

            // Ассоциативный массив костей Assimp
            std::unordered_map<std::string, aiBone*> bones{};
            // Ассоциативный массив индексов костей
            std::unordered_map<std::string, size_t> indices{};

            // Пройтись по костям скелета и заполнить ассоциативные массив костей и индексов для доступа по именам
            for(size_t i = 0; i < pFirstMesh->mNumBones; i++)
            {
                bones[pFirstMesh->mBones[i]->mName.C_Str()] = pFirstMesh->mBones[i];
                indices[pFirstMesh->mBones[i]->mName.C_Str()] = i;
            }

            // Установить значение корневой кости скелета
            auto rootBone = pFirstMesh->mBones[0];
            skeleton->getRootBone()->setLocalBindTransform(ToGlmMat4(rootBone->mNode->mTransformation));

            // Добавление дочерних костей
            RecursivePopulateSkeleton(pFirstMesh->mBones[0]->mName.C_Str(), skeleton->getRootBone(), bones, indices, scene);
        }

        // Отдать скелет
        return skeleton;
    }

    /**
     * Рекурсивное заполнение данных скелета
     * @param assimpBoneName Наименование кости assimp
     * @param bone Текущая кость
     * @param assimpBones Ассоциативный массив костей assimp (ключ - имя кости)
     * @param assimpBoneIndices Ассоциативный массив индексов костей (ключ - имя кости)
     */
    static inline void RecursivePopulateSkeleton(const std::string& assimpBoneName,
                                   const vk::scene::SkeletonBonePtr& bone,
                                   const std::unordered_map<std::string, aiBone*>& assimpBones,
                                   const std::unordered_map<std::string, size_t>& assimpBoneIndices,
                                   const aiScene* scene)
    {
        // Получить текущую кость assimp
        auto assimpBone = assimpBones.at(assimpBoneName);

        // Если у кости есть потомки
        if(assimpBone->mNode->mNumChildren > 0)
        {
            // Пройтись по ним
            for(size_t i = 0; i < assimpBone->mNode->mNumChildren; i++)
            {
                // Получить необходимые данные о потомке
                auto childNode = assimpBone->mNode->mChildren[i];

                // Если такого индекса кости не обнаружено - пропуск итерации
                if(assimpBoneIndices.find(childNode->mName.C_Str()) == assimpBoneIndices.end())
                    continue;

                // Индекс кости
                auto childIndex = assimpBoneIndices.at(childNode->mName.C_Str());

                // Добавить нового потомка в текущую кость
                auto child = bone->addChildBone(childIndex, ToGlmMat4(childNode->mTransformation),glm::mat4(1.0f));

                // Рекурсивно выполнить эту функцию для потомка
                RecursivePopulateSkeleton(childNode->mName.C_Str(), child, assimpBones, assimpBoneIndices, scene);
            }
        }
    }

正如我上面所写,加载骨架工作正常。作为绑定转换,在加载时,我在 Assimp 节点(节点)处使用 mTransformation 矩阵。结果,一切都正确加载,网格以绑定姿势显示。对于每个单独的骨骼,您可以设置一个额外的(相对于绑定的局部)动画,并且一切正常。

然后我决定尝试使用 Assimp 从 Collada 文件中加载动画信息。

从各种教程来看,关键帧中的变换应该在 LOCAL 骨骼空间(相对于父骨骼)。我尝试下载此信息。我是这样做的:

        // Пройтись по набору анимаций сцены
        for(size_t i = 0; i < scene->mNumAnimations; i++)
        {
            // Указатель на анимацию Assimp
            auto pAiAnimation = scene->mAnimations[i];
            // Кол-во ключевых кадров (считаем что у всех каналов одинаковое кол-во ключевых кадров)
            auto keyframesCount = pAiAnimation->mChannels[0]->mNumRotationKeys;

            // Продолжительность в тиках (1 тик - 1 м/с)
            auto duration = static_cast<float>(pAiAnimation->mDuration);

            // Создать анимацию
            auto animation = std::make_shared<vk::scene::SkeletonAnimation>(duration);

            // Пройтись по ключевым кадрам
            for(size_t f = 0; f < keyframesCount; f++)
            {
                // Время кадра
                auto frameTime = static_cast<float>(pAiAnimation->mChannels[0]->mRotationKeys[f].mTime);
                // Создать кадр
                vk::scene::SkeletonAnimation::Keyframe keyframe(frameTime,totalBones);

                // Пройтись по всем костям
                for(size_t j = 0; j < pAiAnimation->mNumChannels; j++)
                {
                    // Указатель на канал (кость) Assimp
                    auto pAiBoneChannel = pAiAnimation->mChannels[j];

                    // Получить индекс кости
                    auto boneIndex = indices.at(pAiBoneChannel->mNodeName.C_Str());
                    
                    // Установить трансформацию кости в кадре
                    keyframe.setBonePosition(boneIndex,{
                            ToGlmVec3(pAiBoneChannel->mPositionKeys[f].mValue),
                            ToGlmQuat(pAiBoneChannel->mRotationKeys[f].mValue),
                            ToGlmVec3(pAiBoneChannel->mScalingKeys[f].mValue)
                    });
                }

                // Добавить ключевой кадр
                animation->addKeyFrame(keyframe);
            }

            // Добавить анимацию
            animations.push_back(animation);
        }

从 Assimp 转换为 GLM 的方法:

    static inline glm::vec3 ToGlmVec3(const aiVector3D &v) { return glm::vec3(v.x, v.y, v.z); }
    static inline glm::vec2 ToGlmVec2(const aiVector3D &v) { return glm::vec2(v.x, v.y); }
    static inline glm::quat ToGlmQuat(const aiQuaternion &q) { return glm::quat(q.w, q.x, q.y, q.z); }
    static inline glm::mat4 ToGlmMat4(const aiMatrix4x4 &m) { return glm::transpose(glm::make_mat4(&m.a1)); }
    static inline glm::mat4 ToGlmMat4(const aiMatrix3x3 &m) { return glm::transpose(glm::make_mat3(&m.a1)); }

但这只是一个下载。接下来,我尝试简单地从第一帧中获取骨骼变换数据。

            // Получить положения костей для кадра 0
            auto bonePositions = this->skeletonAnimation_->getKeyFrames()[0].getBonePositions();

            // Пройтись по положениям костей
            for(size_t i = 0; i < bonePositions.size(); i++)
            {
                // Получить кость скелета
                auto bone = this->getSkeletonPtr()->getBoneByIndex(i);

                // Получить матрицы трансформации
                auto scaleM = glm::scale(glm::mat4(1.0f),bonePositions[i].scaling);
                auto rotM = glm::toMat4(bonePositions[i].rotation);
                auto translateM = glm::transpose(glm::translate(glm::mat4(1.0f),bonePositions[i].location));

                // Установить локальную трансформацию
                bone->setLocalTransform(rotM, false);
            }
            
            // Пересчитать матрицы
            this->getSkeletonPtr()->getRootBone()->calculateBranch(true);

正如你从代码中看到的那样,我决定只从旋转开始(事实上,只有它们在那里改变了)。结果,我得到了一些不足之处:

在此处输入图像描述

我尝试应用其他矩阵(偏移和缩放),但没有帮助。我试图将四元数转换为通常的欧拉角,看看值是什么——而那里的值是从哪里来的完全无法理解。

我的错误是什么?也许我对 Assimp 的功能一无所知?Assimp (mPositionKeys, mRotationKeys, mScalingKeys) 返回的数据在什么空间?

c++
  • 1 个回答
  • 10 Views
Martin Hope
Alex Nem
Asked: 2020-05-25 02:44:09 +0000 UTC

检查 DLL 中指针的有效性

  • 2

我想写一个DLL。为了使 DLL 尽可能通用,我决定使用最小的“C”接口和句柄。也就是说,在库本身中,OOP 和 STL 被充分利用,但只导出了使用句柄操作的函数。小例子:

.h 文件

/**
 * \brief Хендл ресурса статической геометрии
 */
typedef void* RglGeometryResource;

/**
 * \brief Создание ресурса геометрии
 * \param vertices Массив исходных вершин
 * \param indices Массив индексов
 */
extern "C" RENDERERGL_API RglGeometryResource __cdecl rglCreateGeometryResource(RglVertex* vertices, unsigned vertexCount, unsigned* indices, unsigned indexCount);

/**
 * \brief Уничтожение ресурса геометрии
 * \param resource Указатель на хендл ресурса
 */
extern "C" RENDERERGL_API void __cdecl rglDestroyGeometryResource(RglGeometryResource* resource);

.cpp 文件

/**
* \brief Создание ресурса геометрии
* \param vertices Массив исходных вершин
* \param indices Массив индексов
*/
RglGeometryResource rglCreateGeometryResource(RglVertex* vertices, unsigned vertexCount, unsigned* indices, unsigned indexCount)
{
    auto resource = new GeometryResource(std::vector<RglVertex>(vertices, vertices + vertexCount), std::vector<GLuint>(indices, indices + indexCount));
    return reinterpret_cast<RglGeometryResource>(resource);
}

/**
* \brief Уничтожение ресурса геометрии
* \param resource Хендл ресурса
*/
void rglDestroyGeometryResource(RglGeometryResource* resource)
{
    GeometryResource* pGeometryResource = reinterpret_cast<GeometryResource*>(*resource);
    delete pGeometryResource;
    *resource = nullptr;
}

也就是说,在将连接到第三方项目的 .h 文件中,我根本不包含库中存在的必要类,它们仅包含在我执行各种操作的 .cpp 文件中与他们的操作。事实上,我在 OOP C++ 库上得到了类似 C 包装器的东西。

哦,是的,RglVertex示例中点亮的结构是一个完全由普通标准字段(float)组成的结构,所以它在库本身和使用它的应用程序中相同的概率非常高(这就是为什么我在界面和内部都安全地使用它)

结构RglVertex:

struct RglVertex
{
    struct { float x; float y; float z; } position;
    struct { float r; float g; float b; } color;
    struct { float u; float v; } uv;
    struct { float x; float y; float z; } normal;
};

一切似乎都合适且有效,但是

我想 - 如果有人删除当前正在使用的资源会发生什么(即提前调用 rglDestroyGeometryResource),我如何在库本身中检查指针不再有效?nullptr,我在删除后分配,因为它分配给一个副本,而不是分配给“在库本身内部”的指针......

问题:

如何使检查库本身中指针的有效性成为可能?什么是最正确的方法?是否有可能在这里以某种方式使用智能指针,它们可以在这种情况下提供帮助(例如,资源创建函数将返回一个智能指针的句柄,而不是资源本身,或类似的东西),还是值得使用一些是完全不同的方法吗?

提前致谢。

c++
  • 1 个回答
  • 10 Views
Martin Hope
Alex Nem
Asked: 2020-05-06 02:11:36 +0000 UTC

从 DLL 到 C++ 的独立于编译器的类导出

  • 1

我决定创建一个带有 OpenGL 渲染器的 DLL 库,以便以后可以在任何地方(嗯,几乎)使用它。也就是不依赖于编译器和CRT的版本,把.lib和.h文件连接到你的项目就足够了,不管版本和配置都可以使用。听说用通常的方式导出类是不行的,推荐使用C接口,只导出函数。但是由于我的库是由 OOP 构想的,我仍然想找到一种方法来以某种方式导出这些相同的类。结果,我遇到了所谓的“工厂模式”,即导出创建对象并返回指向它的指针的方法。问题似乎解决了,但出现了一些问题。在描述这种方法的文章中(它在这里),建议执行以下操作:

  • 创建具有完全虚拟方法的基类(A 类)(以及清理内存的销毁方法)
  • 从上述基类(A)创建一个后代类(B类),其中所有虚拟方法都将被覆盖
  • 创建从DLL导出的对象创建方法,其中,在动态内存中(使用new)创建一个对象,但返回一个指向基类对象的指针(据我了解,指针是强制转换的)

结果,在我的代码中,它看起来像这样:

基类(接口):

#pragma once

#ifdef RENDERERGL_EXPORTS
#define RENDERERGL_API __declspec(dllexport)
#else
#define RENDERERGL_API __declspec(dllimport)
#endif

/**
 * \brief Интерфейсный класс для рендерера
 */
class RendererGLInterface
{
public:
    virtual void destroy() = 0;
    virtual ~RendererGLInterface(){};
};

后代类(主类,.h 文件)

#pragma once
#include "RendererInterface.h"

/**
 * \brief Основной класс рендерера
 */
class RendererGL : RendererGLInterface
{
private:
public:
    /**
     * \brief Очистка памяти (вызов деструктора)
     */
    void destroy() override;

    /**
     * \brief Деструктор
     */
    ~RendererGL();
};

/**
 * \brief Экспортируемая функция создания рендерера
 * \return Указатель на объект рендерера
 */
extern "C" RENDERERGL_API RendererGLInterface* __cdecl CreateRenderer();

后代类,实现(.cpp 文件)

#include "Include/RendererGL.h"

void RendererGL::destroy()
{
    delete this;
}

RendererGL::~RendererGL()
{
}

RendererGLInterface* CreateRenderer()
{
    return reinterpret_cast<RendererGLInterface*>(new RendererGL());
}

一切似乎都很好,但是..

  • 单独的接口类(带有虚拟方法)有什么意义。为什么不能返回不是基类型的指针,而是类本身类型的指针?
  • 我正确理解需要销毁方法,因为必须从 DLL 中删除类对象?但是通过删除自己有多好delete this?创建一个像“freeA”这样会删除A类对象的导出方法会更正确吗?
  • 也许目前已经有一些更方便的导出方式?来自 STL 的标准智能指针能否以某种方式让生活更轻松(如果可以,如何)?

提前致谢。

c++
  • 1 个回答
  • 10 Views
Martin Hope
Alex Nem
Asked: 2020-01-28 01:02:55 +0000 UTC

OpenGL中顶点索引和颜色插值的奇怪之处

  • 4

我注意到 OpenGL 中有一个与顶点索引相关的有趣功能,但到目前为止我不明白为什么会发生这种情况。

那么重点是什么:

可见面的顶点遍历顺序是顺时针的。有4个顶点(一个普通的矩形),它们的设置大致如下: 数组中顶点的顺序

在数组中它们是这样的:

    vertices[DefaultGeometryType::PLANE] = {
        { { (size / 2), (size / 2), 0.0f },{ 1.0f,0.0f,0.0f },{ 1.0f,1.0f } },
        { { (size / 2), -(size / 2), 0.0f },{ 0.0f,1.0f,0.0f },{ 1.0f,0.0f } },
        { { -(size / 2), -(size / 2), 0.0f },{ 0.0f,0.0f,1.0f },{ 0.0f,0.0f } },
        { { -(size / 2), (size / 2),  0.0f },{ 1.0f,1.0f,0.0f },{ 0.0f,1.0f } },
    };

然后我有一个该正方形的索引数组。它看起来像这样:

    indices[DefaultGeometryType::PLANE] = {
        0,1,2,0,2,3
    };

因此,事实证明,正方形被分成两个三角形,它们的共同顶点为 0 和 2(对角线 0-2 将是这两个三角形的共同边)。结果,我得到了这张照片:

索引第一方式

一切似乎都井然有序。然后我决定稍微改变索引,这样两个三角形的共同边就不是 0-2 而是 3-1 的对角线。我改变了这样的索引:

3,0,1,1,2,3

最后,广场开始变成这样:

第二种方式的索引

同意,情况完全不同。颜色的插值似乎不太正确。我为索引顺序尝试了不同的选项,但事实证明,当矩形被对角线分割时 0-2 - 一切都很好,但是当 3-1 - 这样一个奇怪的画面。但为什么?也许这是某种不正确的顶点索引?是否有任何正确索引的规则?还是我错过了什么,这实际上是应该的?

c++
  • 1 个回答
  • 10 Views
Martin Hope
Alex Nem
Asked: 2020-01-19 09:41:36 +0000 UTC

使用阴影体积的阴影

  • 1

看来我已经走到了死胡同。我逐渐了解图形编程,我决定尝试以阴影映射的替代方式实现阴影,即使用阴影体积技术(也称为模板阴影)。现在我将描述我是如何理解该算法的,可能我的理解存在错误,因此我无法理解这个东西,这就是我写这个的原因:

  • 对于每个光源,都为对象定义了轮廓边缘。这些是位于两个多边形交界处的边(一个指向光源,另一个不指向光源)
  • 这些边缘从光源延伸到远处(最好是无限远)。拉伸本身可以通过将来自源的向量边缘的点添加到每个点乘以某个值来完成。
  • 添加边的拉长顶点后,对于每条边,沿着这些顶点构建一个面,结果,这一切形成了一种锥形体积(另外,为了正确操作,您还应该添加“盖子”在本卷的结尾和开头)
  • 然后这些锥体(即体积)被绘制到深度和模板缓冲区中。此外,在开始时,绘制通过z-test的面部多边形的像素(此时,模板缓冲区中的值增加1),然后再次通过z的非面部多边形的像素-测试被绘制(这一次模板缓冲区中的值减少)。结果,模板缓冲区中保留了一个区域,该区域的模板缓冲区的值保持单位,并且会有阴影
  • 渲染发生时,也会进行stencil测试,渲染只发生在stencil buffer为零的那些地方(即场景被绘制,但阴影区域被忽略)
  • 然后,为了使有阴影的区域不是完全黑色,您可以在单独的 pass 中渲染背景颜色,将其叠加在上一个 pass 的结果上(使用混合)

问题:

如果我对算法的描述是正确的,那么如何实现不同来源的阴影相互叠加的效果呢?例如,如果有多个光源,其中一个光源的阴影会与另一个光源的阴影重叠,那么在这个地方阴影应该会变得更暗,但是如果你按照上面描述的算法,这不会发生(因为有将只是模板缓冲区中的一个区域)在演示该算法操作的各种视频中,我遇到了 RIGHT 效果(例如,这里 - https://www.youtube.com/watch?v=GqyXT6nsxCQ),即是,来自不同来源的阴影重叠并变暗,但如何?

还有用其他光源照亮阴影区域的问题。也就是说,阴影应该只覆盖来自产生它的光源的光,但是这个算法似乎并没有暗示这样的事情。同时,再次,有一个视频实现了这种行为 - https://www.youtube.com/watch?v=69xKosYal-Y

在实践中,我决定建立一个阴影体积,但在继续之前,我想先从理论上理解——按照这个算法,你如何能够达到视频中的结果?在理解这种方法时我到底错过了什么?我将不胜感激详细的解释。

c++
  • 1 个回答
  • 10 Views
Martin Hope
Alex Nem
Asked: 2020-12-17 00:01:03 +0000 UTC

着色器中未成形块的 std140 对齐(OpenGL、GLSL)

  • 2

看起来我不太明白在将统一缓冲区传递给着色器时如何正确对齐。

据我了解,如果使用统一缓冲区而不是常规 unform 变量将数据传输到着色器,那么写入这些缓冲区的数据必须对齐。可以进行这种对齐的格式之一是 std140。

std140 标记规则是这样的:

标量值(int、float、bool - 占用 4 个字节)。向量要么是 8 字节(vec1,vec2)要么是 16 字节(vec3,vec4)。矩阵就像多个向量(取决于维度)。结构的大小等于其所有成员的大小(考虑到上述规则),如有必要,填充到 16 的重数。

在着色器中,我声明了以下结构:

// Структура описывающая параметры мапинга текстуры
struct TextureMapping
{
    vec2 offset;
    vec2 origin;
    vec2 scale;
    mat4 rotation;
};

原来结构本身应该占用 8 + 8 + 8 + 64 字节(88 字节)最多为 16 的倍数(最接近的值是 16 的倍数是 96),少了 8 个字节。但稍后会详细介绍。

接下来,我声明一个使用这些结构的统一块:

// UBO-блок с параметрами маппинга текстур
layout(std140) uniform textureMapping
{
    TextureMapping diffuseTexMapping;
    TextureMapping specularTexMapping;
    TextureMapping bumpTexMapping;
};

然后,已经在 C++ 代码中,我创建了这个结构的类似物,以便将数据传递给着色器。这一切看起来像这样:

结构本身

struct TextureBasicMapping
{
    glm::vec2 offset;    // Сдвиг текстуры
    glm::vec2 origin;    // Центральная точка
    glm::vec2 scale;     // Масштабирование
    glm::mat4 rotation;  // Поворот (используется только часть 2*2)
};

然后,我自己创建统一缓冲区,因为它有 3 个这样的结构:

// Создать UBO-буфер для параметров маппинга текстуры и выделить память
glGenBuffers(1, &uboTextureMapping_);
glBindBuffer(GL_UNIFORM_BUFFER, uboTextureMapping_);
glBufferData(GL_UNIFORM_BUFFER, 3 * sizeof(TextureBasicMapping), nullptr, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);

但不要忘记,最多 16 个重数,该结构TextureBasicMapping缺少 8 个字节。事实证明,为了正确地将数据传输到着色器,您需要在结构本身的某处添加这个 8 字节填充。所以,我不太明白在哪里。

经过无数次测试,结果证明,为了正确传输数据,这个非常 8 字节的填充必须在glm::mat4 rotation. 结果,结构开始看起来像这样:

struct TextureBasicMapping
{
    glm::vec2 offset;    // Сдвиг текстуры
    glm::vec2 origin;    // Центральная точка
    glm::vec4 scale;     // Масштабирование (vec2 + 8 байт выравнивания)
    glm::mat4 rotation;  // Поворот (используется только часть 2*2)
};

但为什么会在那里?为什么不是最后?为什么不是一开始?它是如何工作的?我将不胜感激详细的解释。

c++
  • 1 个回答
  • 10 Views
Martin Hope
Alex Nem
Asked: 2020-07-04 00:18:57 +0000 UTC

复制和移动(C++ 中的移动语义)

  • 7

有一种感觉,我不太了解(或根本不了解)移动(通过右值引用)在 C++ 中是如何工作的,以及如何在类中正确组织/使用移动复制构造函数/运算符。

一点关于目前的情况

我决定编写一个“数据缓冲区”类,以便能够更方便地操作这个缓冲区。实际上,它由指向数据数组的指针、数组中元素的数量和几个方法组成。我这里展示了一个稍微简化的版本,它的本质没有太大变化:

class A
{
private:
    unsigned int size;
    char * data;

public:
    // Конструктор по умолчанию (инициализирует пустой объект без данных)
    A() :size(0), data(nullptr){}

    // Еще конструктор. Инициализирует буфер размера size, заполняет его значением clearValue
    A(unsigned int size, char clearValue = 'a') :size(size),data(new char[this->size]){
        std::fill_n(this->data, this->size, clearValue);
    }

    // Удаляет динамически выделенные данные data
    ~A(){
        delete[] data;
    }
};

然后我尝试做这样的事情:

A a;
a = A(100);

data最后发现对象中的指针a不再有效(当程序/函数结束时,一切都中断了)。根据我的假设,这是因为我通过调用构造函数得到的对象,A(100)然后我分配给a它的对象是临时的,并且在分配之后,它的析构函数立即触发,清除了谎言在指针处data。原来是指针的值被复制了,数据已经被析构函数杀死了(还是我错了?)。

想到的第一个想法是“没有足够的复制构造函数”。我决定写它,它看起来像这样:

A(const A& other) :size(other.size), data(other.data ? new char[other.size] : nullptr)
{
    if(other.data) memcpy(this->data, other.data, other.size);
}

但这并没有帮助。a = A(100);看来复制构造函数根本不参与操作。谷歌搜索了一下,我遇到了各种“移动/分配/复制”的习语,没有真正理解本质,我决定缺少移动构造函数。添加它,不知何故它看起来像这样:

    A(A&& other):A()
    {
        std::swap(this->size, other.size);
        std::swap(this->data, other.data);
    }

然后神秘的事情发生了。=在我尝试做的地方,环境开始强调操作符是一个错误a = A(100),并且代码完全停止编译。环境给出了这样的解释——“函数 A::oprator=(A const a&) 不能被引用,因为这个函数已经被删除了。” 它在哪里被移除?为什么去掉?怎么回事,我不知道。结果,我决定重新定义这个 operator =,结果是这样的:

    A& operator=(A other)
    {
        std::swap(this->size, other.size);
        std::swap(this->data, other.data);
        return *this;
    }

你瞧,现在一切正常。

剩下的问题就这么多:

我什至做了什么?究竟何时调用移动构造函数,何时调用复制构造函数?为什么重新定义移动构造函数时A(A&& other):A()操作符停止工作=并且必须显式定义它?运算符=中做与移动构造函数中几乎相同的事情有什么意义,毕竟,我可以只使用移动运算符?处理所有这些的正确方法是什么?

我完全感到困惑,我很高兴能逐点给出一致的解释。

c++
  • 3 个回答
  • 10 Views
Martin Hope
Alex Nem
Asked: 2020-07-02 00:28:42 +0000 UTC

C++ 中的动态和静态 n 维数组

  • 2

看来我不太明白什么是 C++ 中的二维(n 维)数组,它们是用于 RAM 的,以及在堆栈上声明的二维数组是这样的:

RGBQUAD frameBuffer[300][300];

不同于动态声明的二维数组:

RGBQUAD ** frame = new RGBQUAD*[height];

for (uint32_t i = 0; i < height; i++)
{
    frame[i] = new RGBQUAD[width];
    std::fill_n(frame[i], width, clearColor);
}

现在稍微介绍一下任务本身

我决定尝试编写类似software-renderera的东西,我决定从最基本的点开始(即在屏幕上显示它)。在 WinAPI 中,当然有绘制和设置单个像素颜色的工具,但是在对这个主题进行了一些修改后,我意识到正确的做法是将帧准备为位图图像,然后显示此图像在窗户上。也就是说,按照计划,我将形成一个位图,我会以一定的频率将其显示在窗口上,从而得到类似动画的东西。就其本身而言,我所拥有的图片的显示是这样进行的:

// Cоздать временный bitmap (4 байта на пиксель), последний параметр типа void*
// Это может быть как одномерный так и двумерный массив структур RGBQUAD
HBITMAP hBitMap = CreateBitmap(width, height, 1, 8 * 4, pixels);

// Получить device context окна
HDC hdc = GetDC(hWnd);

// Временный DC для переноса bit-map'а
HDC srcHdc = CreateCompatibleDC(hdc);

// Связать bit-map с временным DC
SelectObject(srcHdc, hBitMap);

// Копировать содержимое временного DC в DC окна
BitBlt(
    hdc,    // HDC назначения
    0,      // Начало вставки по оси X
    0,      // Начало вставки по оси Y
    width,  // Ширина
    height, // Высота
    srcHdc, // Исходный HDC (из которого будут копироваться данные)
    0,      // Начало считывания по оси X
    0,      // Начало считывания по оси Y
    SRCCOPY // Копировать
);

// Уничтожить bit-map
DeleteObject(hBitMap);
// Уничтожить временный DC
DeleteDC(srcHdc);
// Уничтожить DC
DeleteDC(hdc);

一切正常,令人惊讶的是,它同时使用一维和二维结构数组作为参数pixels(在函数中CreateBitmap)。好像C ++中没有动态声明的二维数组被视为一维..然后我尝试在堆上动态分配相同的帧缓冲区(二维数组),即事实上,创建一个指向数组的指针数组,这正是我所做的)。最后,一切都停止了工作,现在该功能CreateBitmap不明白这一点。然后我尝试动态分配一个一维数组,如下所示:

RGBQUAD * frame = new RGBQUAD[width * height];
std::fill_n(frame, width * height, clearColor);

一切都立即开始起作用。

问:怎么了?我错过了什么?在 C++ 中动态声明的二维数组是否与以通常方式声明的二维数组不同?有什么区别?提前致谢。

c++
  • 2 个回答
  • 10 Views
Martin Hope
Alex Nem
Asked: 2020-06-12 19:57:17 +0000 UTC

从最基础的 Linux(编译、安装内核等)开始

  • 10

我一生都在使用 Windows,突然间我想了解一点 Linux。我想从非常非常基础的角度来处理这整个事情,但是我觉得没有足够的理解和理论知识和想法。有许多问题,在我看来,这些问题的答案可以填补我对 Linux 是什么以及它是什么的理解中的一些空白。所以:

  • 我是否正确理解有一个单一且独特的 Linux 内核,在此基础上开发了所有这些众多发行版(Ubuntu、Debian、Mint 等),同时可以安装这个非常干净的内核,而不是来自发行版的一些?
  • 我是否正确理解内核本身本质上已经是一个操作系统,只是没有图形界面(类似于 DOS),并且 GUI 本身已经可以单独安装?
  • 听过很多次,内核通常需要先编译再安装。是否可以获得已经编译好的内核,是否以这种形式分发?尝试在 Windows 上编译相同的 Linux 内核是否是个好主意,这甚至可能吗?
  • 我多次听说过各种各样的包管理器,借助它们可以在基于 Linux 的系统上安装各种软件,而每个这样的操作系统(Ubuntu、Debian、Mint 等)都有自己的包管理器。但是干净的核心呢?怎么放东西?Linux 软件一般是如何分发的?只在开源中,有后续编译?还有诸如安装文件、.exe-shniki 之类的东西——通常不使用?
  • 如果只有一台没有安装操作系统的 PC,Linux 内核通常是如何安装在上面的?

目前,仅此而已。提前致谢。

linux
  • 4 个回答
  • 10 Views
Martin Hope
Alex Nem
Asked: 2020-05-14 02:54:50 +0000 UTC

库中用于回调的自己的模板类(析构函数的问题)

  • 2

再一次问好。我继续一点一点地为 Windows 编写我的 GUI 库,同时学习 C++。我在各种资料中读到,从您的库中导出标准 STL 类不是很好,因为将来在具有不同版本 STL 的环境中使用此类库时可能会导致各种困难(如果事实上这不是案例-纠正我)。因此,为了不导出 std:: 函数,我决定编写类似我自己的回调的东西。经过几个小时的谷歌搜索,我们成功地诞生了以下课程:

/**
 * \brief Частичное шаблонное объявление
 * \detail Обеспечивает возможность указания параметров шаблона в виде "ReturnType(Arguments...)" 
 * \tparam T Возвращаемый тип функции обратного вызова
 */
template<typename T>
class WQUERY_API Callback;

/**
 * \brief Класс-обретка над функцией (либо любым callable объектом) обратного вызова
 * \tparam ReturnType Возвращаемый тип функции обратного вызова
 * \tparam Arguments Аргументы функции обратного вызова
 */
template <typename ReturnType, typename ... Arguments>
class Callback<ReturnType(Arguments...)>
{
private:
    ReturnType(*wrapperFunction_)(Arguments... args, void*);  // Указатель на функцию-обретку (вызывает настоящий callable объект)
    void *pCallable_;                                         // Указатель на callabl'e объект объект

public:
    /**
     * \brief Конкструктор по умолчанию
     */
    Callback() :pCallable_(nullptr) {}

    /**
     * \brief Установка функции обратного вызова
     * \tparam T Тип callabl'e объекта
     * \param pCallable Указатель на функцию (callable объект)
     */
    template<class T>
    void Set(T pCallable)
    {
        // Выделить память под функцию (callable объект) и скопировать содежимое в эту память
        // если это лямбда, потребуется столько памяти, сколько занимают ее "захваченные" переменные
        this->pCallable_ = malloc(sizeof(T));
        memcpy(this->pCallable_, &pCallable, sizeof(T));

        // Создать функцию "обертку", которая инициирует вызов callable объекта с аргументами args
        this->wrapperFunction_ = [](Arguments... args, void *callable) ->ReturnType {
            return (*reinterpret_cast<T*>(callable))(args...);
        };
    }

    /**
     * \brief Вызов функции обертки
     * \param args Аргументы
     * \return Значение, возвращаемое оберткой (идентично тому что вернет pCallable_)
     */
    ReturnType Invoke(Arguments... args)
    {
        if (this->IsSet())
        {
            return this->wrapperFunction_(args..., this->pCallable_);
        }

        return ReturnType();
    }

    /**
     * \brief Сброс функции (очистить память и сделать не вызываемой)
     */
    void Unset()
    {
        if (this->pCallable_) {
            free(this->pCallable_);
            this->pCallable_ = nullptr;
        }
    }

    /**
     * \brief Установлено ли
     * \return Состояние
     */
    bool IsSet() const
    {
        return this->pCallable_ && this->wrapperFunction_;
    }

    /**
     * \brief Деструктор (срабсывает функцию)
     */
    ~Callback()
    {
        this->Unset();
    }
}

计划以某种方式使用这个东西:

1)在Window类(或控件类)中,用Callback类的一组对象声明一个结构

    struct
    {
        Callback<bool()> onClose;
        Callback<void()> onPaint;
        Callback<void(unsigned int type, Vector2D<int> newSizes)> onResized;
        Callback<void(unsigned int code)> onKeyDown;
        Callback<void(unsigned int code)> onKeyUp;
        Callback<void(char symbol)> onTyping;
        Callback<void(Vector2D<int> cursor)> onMouseMove;
        Callback<void(Vector2D<int> cursor, MouseKeys type)> onMouseKeyDown;
        Callback<void(Vector2D<int> cursor, MouseKeys type)> onMouseKeyUp;
    } events;

2)使用库时,回调将像这样指定

wquery::Window window1;
window1.events.onClose.Set([]() {std::cout << "Test" << std::endl; return true; });

3)在窗口过程中,在正确的地方,函数将被调用(如果它们已安装)

    case WM_CLOSE:
        if (window)
        {
            if(window->events.onClose.IsSet())
            {
                if (!window->events.onClose.Invoke()) return 0;
            }

            if(window->closesProgram_) PostQuitMessage(0);
        }
        ShowWindow(hWnd, SW_HIDE);
        break;

起初,一切都很顺利,你可以同时给 Set 函数的参数一个 lambda 和一个常规函数指针,它们被完美地调用了。但是对Window析构函数的调用,不幸的是,在执行过程中开始产生如下错误: 在此处输入图像描述

原来,这个错误是由Callback类本身的内存释放产生的。而这个特殊的时刻

    void Unset()
    {
        if (this->pCallable_) {
            free(this->pCallable_);
            this->pCallable_ = nullptr;
        }
    }

目前,我非常不清楚问题出在哪里,因为我没有删除我无权访问的内存,而是删除了 COPYED 内存(在 Set 函数中,它正在复制)。有趣的是,这个类在库之外没有问题。也许我在这个复制过程中遗漏了一些重要的细节?或者也许你应该以不同的方式来实现你的回调?

c++
  • 1 个回答
  • 10 Views
Martin Hope
Alex Nem
Asked: 2020-04-22 03:11:54 +0000 UTC

抽象类继承者的函数链式调用

  • 0

你好。一般来说,我想让对象可以在“链”中调用函数,即像这样:

window1.SetTitle("My Program")
    ->SetSize({ 640,480 })
    ->SetPosition({ 100,100 })
    ->SetClosesProgram(true)
    ->Show();

为此,在每个函数中,我返回一个指向当前对象的指针(即 I do return this)。

一切都会好起来的,但现在是继承的时候了。我有一个抽象类Control,有一个完全虚拟的方法和一组基本方法(例如SetText其他方法),所有这些方法看起来像这样:

Control* Control::SetSomething(int something)
{
    //Тут оснвной код метода

    return this;
}

但事实是,其他类将继承自该类(例如Label、Button等TextBox),而它们可能具有其他一些不在基类中的方法。当一个对象被创建时,例如一个类Label(继承自一个抽象类Control),那么所有这些将被继承的基本方法Control都会给出一个类型指针Control*,而不是任何方式Label*,这将不允许“链呼吁”全面落实。然后有必要将所有这些基本方法虚拟化,并在每个继承者中重新定义它们,但这会使继承变得毫无意义。

问题:是否可以在 C++ 中实现类似的东西?也许还有其他一些方法?也许是某种对运算符的虚拟重新定义的技巧?我想知道如何正确地做到这一点。先感谢您。

c++
  • 1 个回答
  • 10 Views
Martin Hope
Alex Nem
Asked: 2020-04-20 07:04:08 +0000 UTC

警告“X 类必须有一个接口 dll 供 Y 类的客户端使用”

  • 0

你好。我继续理解 C++ 的复杂性。这次我决定尝试编写一个动态库(即 .dll)来简化 Windows GUI 的工作。导出所有类和必要的函数 - __declspec(dllexport). 一切都很好,但事实是,在许多课程中,按计划,我应该有负责各种事件的属性,它们采用 lambdas 的形式(例如std::function<void()> onPaint_,和其他)。当我将 lambdas 添加为我的类的属性时,编译过程中开始出现警告。即,这是内容:

类“std::function”必须有一个 dll 接口供客户端使用类“wquery::Window”

我不太明白什么是本质,什么意思?Lamba,这本质上是模板类的对象(或者我弄错了)?也就是说,我不能在接口中使用一些在他们的类中有 .. no .. 的第三方对象 __declspec(dllexport)?或者是什么?是不是因为..在使用.dll的平台上,这些对象的类可能与构建时使用的不一样?是这样吗?但是,完成这项任务的正确方法是什么?

我尝试使用指针(即std::function<void()>* onPaint_),然后警告消失,出现另一个问题 - 当我只有一个指向 lambda 对象的指针时,如何将 lambda 引用为函数(调用它)?也就是说,如果早些时候我可以这样做 - window->onPaint_(),现在这当然行不通。我试图做这样的事情 - *(window->onPaint_)(),但编译器也发誓。

总的来说,我想了解如何最好地处理这一切。也许我在做一些根本错误的事情?提前致谢。

c++
  • 1 个回答
  • 10 Views
Martin Hope
Alex Nem
Asked: 2020-01-16 01:34:46 +0000 UTC

在循环遍历该向量时从向量中删除对象

  • 0

为了在 C++ 中热身,我正在 WinApi 上编写一个小型 OOP 插件,以便您可以在几行代码中创建窗口并执行必要的基本操作(例如更改文本、大小、在控件上设置事件、等等)。计划它的用途是这样的:

WqWindow::WqBegin();

WqWindow w;
WqButton b(&w);
WqTextBox tb(&w);

tb.SetPosition(WqPosition(50, 10))
    ->SetSize(WqSize(200, 25))
    ->SetText("");

b.SetText("Cool button")
    ->SetSize(WqSize(150, 25))
    ->SetPosition(WqPosition(50, 50))
    ->SetAcnhor(WqControlAnchor(false, false, true, false))
    ->SetOnClick([&b, &tb, &w]() { cout << "Clicked!" << endl; });

w.SetTitle("Cool window")
    ->SetSize(WqSize(400, 400))
    ->ClosesProgram(false)
    ->SetOnClose([]() { cout << "Closed!" << endl; return true; })
    ->Show();

WqWindow::WqEnd();

原理是这样的——创建控件时,将指向它的指针添加到窗口对象(WqWindow)的元素向量中,删除时,在析构函数中,从向量中删除该元素。大致是这样的

//Убираем из списка элементов управления окна
this->window_->controls_.erase(std::remove(this->window_->controls_.begin(), this->window_->controls_.end(), this), this->window_->controls_.end());

按钮和其他元素本身的事件在窗口过程中处理。原理大致如下:我们获取导致事件的窗口句柄(HWND),使用自定义指针(在创建 WqWindow 窗口时分配),我们获取关联的 WqWindow 对象并访问控制向量。我们循环遍历它们,并调用这些元素的 lambda 函数。它看起来像这样:

    case WM_COMMAND:
        if (wqWindow && !wqWindow->controls_.empty()) {
            const std::vector<WqControl*> safeControlPointers(wqWindow->controls_);

            for (WqControl * control : safeControlPointers) 
            {
                if (control->initialized_)
                {
                    if (HIWORD(wParam) == EN_CHANGE) {
                        if (control && control->ControlClassName() == "Edit" && control->GetHWND() == (HWND)(lParam)) {
                            WqTextBox * pTextBox = ((WqTextBox*)control);
                            if (pTextBox->onChanged_) {
                                pTextBox->onChanged_();
                            }
                        }
                    }
                    else {
                        if (control->ControlClassName() == "Button" && control->GetHWND() == (HWND)(lParam)) {
                            WqButton * pButton = ((WqButton*)control);
                            if (pButton->onClick_) {
                                pButton->onClick_();
                            }
                        }
                    }
                }
            }
        }
        break;

然后我想 - 如果这个库的用户想要删除(通过调用析构函数)lamb 表达式中的一些控件,例如像这样:

->SetOnClick([&b, &tb, &w]() { tb.~WqTextBox(); });

在这种情况下,我只是添加了 safeControlPointers 变量,这样当通过向量时,我们就像使用向量的副本一样工作,而不是使用大小会改变的向量。似乎一切都应该井井有条,但在执行过程中(按下按钮时)仍然发生错误。事实是,在向量的副本中有一个指向某个对象的指针,该对象以某种方式被删除了。我不知道怎么做,但以下内容帮助了我——我在 WqControl 中声明了 control->initialized_ 标志,它在构造函数中变为真,并在析构函数中设置为flase。在循环中,您可能注意到了检查

if (control->initialized_)

正是由于这一点,按下按钮时程序没有中断。我不完全明白为什么(毕竟,对象被破坏,从内存中删除,所以访问它的任何成员是不可能的)。如果你能解释这是怎么可能的,我会很高兴。好吧,我并没有就此止步,而是决定尝试创建一个 TextBox(我后来决定单击时删除),而不是在堆栈上,而是在堆上(即使用 new 运算符),然后调用 delete on点击。这就是一切最终崩溃的地方。并且没有检查帮助(指针空虚和其他)。

问题:如何实现类似的机制,以便在单击时可以删除其他元素?解决这个问题的正确方法是什么?提前致谢。

c++
  • 1 个回答
  • 10 Views

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    我看不懂措辞

    • 1 个回答
  • Marko Smith

    请求的模块“del”不提供名为“default”的导出

    • 3 个回答
  • Marko Smith

    "!+tab" 在 HTML 的 vs 代码中不起作用

    • 5 个回答
  • Marko Smith

    我正在尝试解决“猜词”的问题。Python

    • 2 个回答
  • Marko Smith

    可以使用哪些命令将当前指针移动到指定的提交而不更改工作目录中的文件?

    • 1 个回答
  • Marko Smith

    Python解析野莓

    • 1 个回答
  • Marko Smith

    问题:“警告:检查最新版本的 pip 时出错。”

    • 2 个回答
  • Marko Smith

    帮助编写一个用值填充变量的循环。解决这个问题

    • 2 个回答
  • Marko Smith

    尽管依赖数组为空,但在渲染上调用了 2 次 useEffect

    • 2 个回答
  • Marko Smith

    数据不通过 Telegram.WebApp.sendData 发送

    • 1 个回答
  • Martin Hope
    Alexandr_TT 2020年新年大赛! 2020-12-20 18:20:21 +0000 UTC
  • Martin Hope
    Alexandr_TT 圣诞树动画 2020-12-23 00:38:08 +0000 UTC
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5