Hazel游戏引擎(112)可视化包围盒

文中若有代码、术语等错误,欢迎指正

前言

  • 此节目的

    实现一个复选框点击后,场景可以渲染出图形相应的包围盒

  • 如何实现

    使用上两节加的渲染LineRect来渲染盒状包围盒。

    至于圆的包围盒,一样绘制圆,只不过控制厚度,从而实现圆环形状=圆包围盒

  • 实现细节

    • 渲染包围盒的代码应放在哪?

      应放在编辑层,而不是场景层。

      • 场景层是渲染场景内的物体的,而至于包围盒不属于场景,若放在场景里,需要向编辑层获取是否显示包围盒的控制, 这样会导致紊乱。

      • 放在编辑层,等渲染物体后再渲染包围盒,这使得物体与调试相关的渲染分开,更好扩展。

    • 重点

      包围盒需跟随对应的物体,所以包围盒的transform如何正确是个重点,需基于物体的平移、旋转、缩放。

代码

void EditorLayer::OnOverlayRender()
{	// 两个不同摄像机
    if (m_SceneState == SceneState::Play) {
        Entity camera = m_ActiveScene->GetPrimaryCameraEntity();
        // Caemra类没有视图矩阵,所以需传入transform计算视图矩阵
        Renderer2D::BeginScene(camera.GetComponent<CameraComponent>().camera, camera.GetComponent<TransformComponent>().GetTransform());
    }
    else {
        // EditorCamera,可以直接获取投影视图矩阵,所以不需要transform
        Renderer2D::BeginScene(m_EditorCamera);
    }
    if (m_ShowPhysicsColliders) {// 由复选框控制
        
        // 重点/
        // 包围盒需跟随对应的物体,包围盒的transform需基于物体的平移、旋转、缩放。
        // Box Colliders
        {
            auto view = m_ActiveScene->GetAllEntitiesWith<TransformComponent, BoxCollider2DComponent>();
            for (auto entity : view) {
                auto [tc, bc2d] = view.get<TransformComponent, BoxCollider2DComponent>(entity);
                // 0.001fZ轴偏移量
                glm::vec3 translation = tc.Translation + glm::vec3(bc2d.Offset, 0.001f);
                glm::vec3 scale = tc.Scale * glm::vec3(bc2d.Size * 2.0f, 1.0f); // 注意bc2d.Size需乘以2,以免缩小一半

                // Cherno的代码,只绕着Z轴旋转 rotation的z值
                //glm::mat4 transform = glm::translate(glm::mat4(1.0f), translation)
                //	* glm::rotate(glm::mat4(1.0f), tc.Rotation.z, glm::vec3(0.0f, 0.0f, 1.0f))// 围绕z旋转的角度
                //	* glm::scale(glm::mat4(1.0f), scale);

                // 应改成:跟随物体的旋转角度而旋转
                // 第一种rotation计算方式,有bug,旋转相反,待解决
                //glm::mat4 rotation = glm::rotate(glm::mat4(1.0f), tc.Rotation.x, { 1,0,0 })
                //    * glm::rotate(glm::mat4(1.0f), tc.Rotation.y, { 0, 1, 0 })
                //    * glm::rotate(glm::mat4(1.0f), tc.Rotation.z, { 0, 0, 1 });
                
                // 第二种rotation计算方式 用四元数获得矩阵
                glm::mat4 rotation = glm::toMat4(glm::quat(tc.Rotation));

                glm::mat4 transform = glm::translate(glm::mat4(1.0f), translation)
                    * rotation
                    * glm::scale(glm::mat4(1.0f), scale);

                Renderer2D::DrawRect(transform, glm::vec4(0, 1, 0, 1));// 绿色的包围盒
            }
        }
        // Circle Colliders
        {
            auto view = m_ActiveScene->GetAllEntitiesWith<TransformComponent, CircleCollider2DComponent>();
            for (auto entity : view) {
                auto [tc, cc2d] = view.get<TransformComponent, CircleCollider2DComponent>(entity);
                // 0.001fZ轴偏移量
                glm::vec3 translation = tc.Translation + glm::vec3(cc2d.Offset, 0.001f);
                glm::vec3 scale = tc.Scale * glm::vec3(cc2d.Radius * 2);// 注意cc2d.Radius需乘以2,以免缩小一半
                
                // 错误写法
                //glm::mat4 transform = glm::translate(glm::mat4(1.0f), translation)
                //    * glm::scale(glm::mat4(1.0f), scale);
                
                // 第二种rotation计算方式 用四元数获得矩阵
                glm::mat4 rotation = glm::toMat4(glm::quat(tc.Rotation));// 新增的///

                glm::mat4 transform = glm::translate(glm::mat4(1.0f), translation)
                    * rotation// 新增的///
                    * glm::scale(glm::mat4(1.0f), scale);

                Renderer2D::DrawCircle(transform, glm::vec4(0, 1, 0, 1), 0.01f);//绿色的包围盒, 第三个参数控制呈现圆环
            }
        }
    }
    Renderer2D::EndScene();
}

在EditorLayer的Update函数中,并在渲染完场景的物体后,再调用OnOverlayRender函数

关于摄像机移到物体后面效果

  • 引入

    // 0.001fZ轴偏移量,设置盒子的位置在物体的前面一点点
    glm::vec3 translation = tc.Translation + glm::vec3(bc2d.Offset, 0.001f);
    

    可见正对物体时,包围盒在物体的前面

  • 当摄像机移到物体后面时

    可以发现有个小BUG,Quad物体的绿色包围盒依旧能看到,但是Circle的包围盒却看不到了。

    Cherno说可以根据摄像机的位置决定圆的包围盒画在物体的前方还是后方即可解决此问题。

    我自己理解的一些(可能有错):

    • Quad的包围盒能看见

      • Quad的包围盒z轴依旧大于Quad,当前摄像机并没有处在z轴位置看物体,自然能投影看到包围盒
      • Quad的包围盒是用DrawLine画线画的,而Quad本身是用三角形索引画的,两个draw不一样
    • Circle的包围盒看不见——待解决

      只能提出下面几点

      • Circle的包围盒是用同画圆的方式画的,Circle本身也是用画圆的方式画的
      • Circle的包围盒是用thickness控制边缘能有颜色,其它部分alpha为0
      • 有可能的是Circle的glsl导致的

      即使圆物体本身边缘alpha为0,圆包围盒依旧看不清,应该是glsl的问题吧?

我发现的Bug

Bug1.1:Quad设置旋转角度后,包围盒不跟随Quad

  • 错误写法1:包围盒的transform矩阵

    // Cherno的代码,只绕着Z轴旋转 rotation的z值
    glm::mat4 transform = glm::translate(glm::mat4(1.0f), translation)
        * glm::rotate(glm::mat4(1.0f), tc.Rotation.z, glm::vec3(0.0f, 0.0f, 1.0f))// 围绕z旋转的角度
        * glm::scale(glm::mat4(1.0f), scale);
    

    当修改Quad的x、y、z旋转角度后,包围盒的位置是不正确的,如下:

    image-20230809221952678

  • 有点小Bug的写法2:包围盒的transform矩阵

    包围盒的旋转角度应该考虑到Quad的x、y、z3个角度值

    // 应改成:跟随物体的旋转角度而旋转
    // 第一种rotation计算方式,有bug,旋转相反,待解决
    glm::mat4 rotation = glm::rotate(glm::mat4(1.0f), tc.Rotation.x, { 1,0,0 })
        * glm::rotate(glm::mat4(1.0f), tc.Rotation.y, { 0, 1, 0 })
        * glm::rotate(glm::mat4(1.0f), tc.Rotation.z, { 0, 0, 1 });
    
    glm::mat4 transform = glm::translate(glm::mat4(1.0f), translation)
        * rotation
        * glm::scale(glm::mat4(1.0f), scale);
    

    此写法有点小Bug,包围盒的位置与Quad的位置相反

    请添加图片描述

  • 正确写法

    //第二种rotation计算方式 用四元数获得矩阵
    glm::mat4 rotation = glm::toMat4(glm::quat(tc.Rotation));
    
    glm::mat4 transform = glm::translate(glm::mat4(1.0f), translation)
        * rotation
        * glm::scale(glm::mat4(1.0f), scale);
    

    这样写,包围盒才会与Quad的旋转角度位置一致

    image-20230809222635532

Bug1.2:盒型包围盒设置Offset后,包围盒物理计算错误

  • Bug解释

    当设置Quad包围盒的offset后运行

    发现偏移位置的Box包围的不参与物理计算,参与物理计算的还是Quad本身顶点位置

  • 当时不知道解决方法,猜测是Box2D设置包围盒boxShape的地方没有加上偏移导致的

    // 3.1定义Box包围盒
    b2PolygonShape boxShape;
    // TODO:待完善!
    boxShape.SetAsBox(bc2d.Size.x * transform.Scale.x, bc2d.Size.y * transform.Scale.y);// 包围盒跟随物体的size而变化
    
  • 后续

    在HazelGitHUb上的ISSUE有人提出这个问题并且有 解决方法,并在117节Cherno有讲

    是需要加上包围盒的偏移

    void Scene::OnPhysics2DStart(){
        // 错误写法
        //boxShape.SetAsBox(bc2d.Size.x * transform.Scale.x, bc2d.Size.y * transform.Scale.y);
        // 正确写法:加上了包围盒的偏移位置
        // 包围盒的计算范围跟随物体的size、偏移位置而变化
        boxShape.SetAsBox(bc2d.Size.x * transform.Scale.x, bc2d.Size.y * transform.Scale.y,
                          b2Vec2(bc2d.Offset.x, bc2d.Offset.y), 0);// 包括偏移
    
    void EditorLayer::OnOverlayRender(){	
    for (auto entity : view) {
        auto [tc, bc2d] = view.get<TransformComponent, BoxCollider2DComponent>(entity);
        // 方法1:不行
        // 0.001fZ轴偏移量
        glm::vec3 translation = tc.Translation + glm::vec3(bc2d.Offset, 0.001f);
        glm::vec3 scale = tc.Scale * glm::vec3(bc2d.Size * 2.0f, 1.0f); // 注意bc2d.Size需乘以2,以免缩小一半
    	glm::mat4 rotation = glm::toMat4(glm::quat(tc.Rotation));
        // 若将偏移位置先和物体的位置相加后再与旋转相乘,会导致包围盒运算时的很奇怪,所以不正确!!!!
        //glm::mat4 transform = glm::translate(glm::mat4(1.0f), translation)
        //	* rotation
        //	* glm::scale(glm::mat4(1.0f), scale);
    	
        // 方法2:可以
        // 应该先物体的位置乘以旋转再乘偏移量才正确
        glm::mat4 transform = glm::translate(glm::mat4(1.0f), tc.Translation)// tc.Translation != translation
            * rotation
            * glm::translate(glm::mat4(1.0f), glm::vec3(bc2d.Offset, 0.001f))// 包围盒的位置还需要算上偏移位置
            * glm::scale(glm::mat4(1.0f), scale);
    
        Renderer2D::DrawRect(transform, glm::vec4(0, 1, 0, 1));// 绿色的包围盒
    }
    
    • 效果奇怪写法:若将偏移位置和物体的位置相加再与旋转相乘

      glm::vec3 translation = tc.Translation + glm::vec3(bc2d.Offset, 0.001f);
      glm::vec3 scale = tc.Scale * glm::vec3(bc2d.Size * 2.0f, 1.0f); // 注意bc2d.Size需乘以2,以免缩小一半
      // 若将偏移位置先和物体的位置相加后再与旋转相乘,会导致包围盒运算时的很奇怪,所以不正确!!!!
      glm::mat4 rotation = glm::toMat4(glm::quat(tc.Rotation));
      
      glm::mat4 transform = glm::translate(glm::mat4(1.0f), translation)
          * rotation
          * glm::scale(glm::mat4(1.0f), scale);
      

      2.1偏移位置先相加再偏移

      有围绕着一个点运动感觉

    • 效果正确写法:物体的位置先乘以旋转,再乘偏移量才正确

      glm::vec3 translation = tc.Translation + glm::vec3(bc2d.Offset, 0.001f);
      glm::vec3 scale = tc.Scale * glm::vec3(bc2d.Size * 2.0f, 1.0f);
      glm::mat4 rotation = glm::toMat4(glm::quat(tc.Rotation));
      
      glm::mat4 transform = glm::translate(glm::mat4(1.0f), tc.Translation)// tc.Translation != translation
          * rotation
          * glm::translate(glm::mat4(1.0f), glm::vec3(bc2d.Offset, 0.001f))// 包围盒的位置还需要算上偏移位置
          * glm::scale(glm::mat4(1.0f), scale);
      

      2.2后偏移

Bug2.1:Circle设置旋转角度后,包围盒不跟随Circle

  • Bug解释:和1.1差不多

    原始写法

    // Circle Colliders
    {
        auto view = m_ActiveScene->GetAllEntitiesWith<TransformComponent, CircleCollider2DComponent>();
        for (auto entity : view) {
            auto [tc, cc2d] = view.get<TransformComponent, CircleCollider2DComponent>(entity);
    
            glm::vec3 translation = tc.Translation + glm::vec3(cc2d.Offset, 0.001f);
            glm::vec3 scale = tc.Scale * glm::vec3(cc2d.Radius * 2);// 注意:需*2
    
            glm::mat4 transform = glm::translate(glm::mat4(1.0f), translation)
                * glm::scale(glm::mat4(1.0f), scale);
    
            Renderer2D::DrawCircle(transform, glm::vec4(0, 1, 0, 1), 0.01f);// 绿色的包围盒, 第三个参数控制呈现圆环
        }
    }
    

    请添加图片描述

  • 正确写法

    // Circle Colliders
    auto view = m_ActiveScene->GetAllEntitiesWith<TransformComponent, CircleCollider2DComponent>();
    for (auto entity : view) {
        auto [tc, cc2d] = view.get<TransformComponent, CircleCollider2DComponent>(entity);
        // 0.001fZ轴偏移量
        glm::vec3 translation = tc.Translation + glm::vec3(cc2d.Offset, 0.001f);
        glm::vec3 scale = tc.Scale * glm::vec3(cc2d.Radius * 2);// 注意cc2d.Radius需乘以2,以免缩小一半
        // 第二种rotation计算方式 用四元数获得矩阵
        glm::mat4 rotation = glm::toMat4(glm::quat(tc.Rotation));// 新增的///
    
        glm::mat4 transform = glm::translate(glm::mat4(1.0f), translation)
            * rotation// 新增的///
            * glm::scale(glm::mat4(1.0f), scale);
    
        Renderer2D::DrawCircle(transform, glm::vec4(0, 1, 0, 1), 0.01f);//绿色的包围盒, 第三个参数控制呈现圆环
    }
    

    image-20230809230504655

Bug2.2:圆形包围盒设置Offset后,物理效果奇怪

  • 解释Bug

    当圆形包围盒的offset设置后运行,发现

    1. 圆形包围盒的物理计算不正确
    2. 圆形包围盒能进入障碍物的盒子包围盒里面
    3. 圆形包围盒运动奇怪

    3.1圆形运动不正确

  • 尝试解决方法1

    经过测试固定旋转效果好一点,但是依旧不尽人意,因此固定Rotation不是个好方法

    请添加图片描述

  • 尝试解决方法2

    确定Box2D设置圆形包围盒circleShape是如下代码

    if (entity.HasComponent<CircleCollider2DComponent>()) {
        auto& cc2d = entity.GetComponent<CircleCollider2DComponent>();
        // 3.1定义圆形包围盒
        b2CircleShape circleShape;
        circleShape.m_p.Set(cc2d.Offset.x, cc2d.Offset.y); // 需要偏移位置!!!
        circleShape.m_radius = transform.Scale.x * cc2d.Radius;// 参与物理计算的范围跟随物体的scale变化
        // 3.2定义fixture,fixture包含定义的包围盒
        b2FixtureDef fixtureDef;
        fixtureDef.shape = &circleShape;
        fixtureDef.density = cc2d.Density;
        fixtureDef.friction = cc2d.Friction;
        fixtureDef.restitution = cc2d.Restitution;
        fixtureDef.restitutionThreshold = cc2d.RestitutionThreshold;
        // 3.3定义主体的fixture
        body->CreateFixture(&fixtureDef);
    }
    
    • 错误原因是同Bug1.2

      圆形包围盒的transform矩阵计算错误,导致物理模拟计算奇怪

    将圆形包围盒的transform矩阵改成如下

    // Circle Colliders
    auto view = m_ActiveScene->GetAllEntitiesWith<TransformComponent, CircleCollider2DComponent>();
    for (auto entity : view) {
        auto [tc, cc2d] = view.get<TransformComponent, CircleCollider2DComponent>(entity);
        // 0.001fZ轴偏移量
        glm::vec3 translation = tc.Translation + glm::vec3(cc2d.Offset, 0.001f);
        glm::vec3 scale = tc.Scale * glm::vec3(cc2d.Radius * 2);// 注意cc2d.Radius需乘以2,以免缩小一半
        // 第二种rotation计算方式 用四元数获得矩阵
        glm::mat4 rotation = glm::toMat4(glm::quat(tc.Rotation));
    
        // 若将偏移位置先和物体的位置相加后再与旋转相乘,会导致包围盒的位置很奇怪,所以不正确
        //glm::mat4 transform = glm::translate(glm::mat4(1.0f), translation)
        //  * rotation
        //	* glm::scale(glm::mat4(1.0f), scale);
    
        glm::mat4 transform = glm::translate(glm::mat4(1.0f), tc.Translation)
            * rotation
            * glm::translate(glm::mat4(1.0f), glm::vec3(cc2d.Offset, 0.001f))// 包围盒的位置还需要算上偏移位置
            * glm::scale(glm::mat4(1.0f), scale);
    
        Renderer2D::DrawCircle(transform, glm::vec4(0, 1, 0, 1), 0.01f);// 绿色的包围盒, 第三个参数控制呈现圆环
    }
    

    3.2圆形运动正确-fix rotation

Cherno遇到的Bug

Bug1:Quad的包围盒小了一半

但只是显示包围盒小了一半,实际物理运算时还是正常大小

  • 解释Bug

    image-20230809232844483

  • 原因

    因为计算包围盒transform的Scale的Size未乘2

    glm::vec3 scale = tc.Scale * glm::vec3(bc2d.Size, 1.0f);
    
  • 修改为

    glm::vec3 scale = tc.Scale * glm::vec3(bc2d.Size*2, 1.0f);
    
  • 详细说明Bug

    包围盒的Size为0.5,tc.Scale = 1, 代入计算得scale=0.5,即缩小了一半。

    而这调用的DrawRect传入的参数是Transform,这Transform会和圆实际为Quad的四个顶点相乘得出最终位置,即世界坐标,因transform矩阵的scale部分为0.5,从而顶点位置围成的Quad会缩小一半范围

    Renderer2D::DrawRect(transform, glm::vec4(0, 1, 0, 1));// 绿色的包围盒
    void Renderer2D::DrawRect(const glm::mat4& transform, const glm::vec4& color, int entityID)
    {
        glm::vec3 lineVertices[4];
        for (size_t i = 0; i < 4; i++)
        {
            lineVertices[i] = transform * s_Data.QuadVertexPosition[i]; // quad的顶点位置正好是rect的顶点位置
        }
        DrawLine(lineVertices[0], lineVertices[1], color);
        DrawLine(lineVertices[1], lineVertices[2], color);
        DrawLine(lineVertices[2], lineVertices[3], color);
        DrawLine(lineVertices[3], lineVertices[0], color);
    }
    
  • 为什么定义包围盒boxShape的Size是0.5

    boxShape.SetAsBox(bc2d.Size.x * transform.Scale.x, bc2d.Size.y * transform.Scale.y,
                      b2Vec2(bc2d.Offset.x, bc2d.Offset.y), 0);
    

    因为box2D自身的规定,Size为0.5,参与实际物理计算的是长宽为1大小的box

    • 提一下我们当前定义Quad的4个顶点的局部位置,xy是与中心点(0, 0)相差0.5,形成长宽为1的Quad。

          // 设置quad的初始位置
          s_Data.QuadVertexPosition[0] = { -0.5f, -0.5f, 0.0f, 1.0f };
          s_Data.QuadVertexPosition[1] = {  0.5f, -0.5f, 0.0f, 1.0f };
          s_Data.QuadVertexPosition[2] = {  0.5f,  0.5f, 0.0f, 1.0f };
          s_Data.QuadVertexPosition[3] = { -0.5f,  0.5f, 0.0f, 1.0f };
          ......
          void Renderer2D::DrawQuad(const glm::mat4& transform, const glm::vec4& color, int entityID)
          {
              for (size_t i = 0; i < quadVertexCount; i++) {
                  s_Data.QuadVertexBufferPtr->Position = transform * s_Data.QuadVertexPosition[i];
                  ......
              }  
      

      这可以更好的理解为什么box2D规定size0.5代表长宽1的box盒子

Bug2:的包围盒小了一半

但只是显示包围盒小了一半,实际物理运算时还是正常大小

  • 解释bug

  • 原因

    计算包围盒的transform的Scale部分的Radius没乘2

    glm::vec3 scale = tc.Scale * glm::vec3(cc2d.Radius);	// scale = 0.5
    glm::mat4 transform = glm::translate(glm::mat4(1.0f), translation)
    						* glm::scale(glm::mat4(1.0f), scale);
    
  • 解决方法

    将计算包围盒的transform的Scale部分的Radius乘2

    glm::vec3 scale = tc.Scale * glm::vec3(cc2d.Radius * 2);	// scale = 1
    glm::mat4 transform = glm::translate(glm::mat4(1.0f), translation)
    						* glm::scale(glm::mat4(1.0f), scale);
    
  • 详细说明

    这个transform是包围盒跟随物体的变化矩阵,决定了绘画圆形包围盒4个顶点的世界位置。

    Renderer2D::DrawCircle(transform, glm::vec4(0, 1, 0, 1), 0.01f);
    void Renderer2D::DrawCircle(const glm::mat4& transform, const glm::vec4& color, float thickness, float fade, int entityID)
    {
        for (size_t i = 0; i < quadVertexCount; i++) {
            s_Data.CircleVertexBufferPtr->WorldPosition = transform * s_Data.QuadVertexPosition[i]; 
            ......
    

    绘制圆形如何变成了圆环,是由thickness配合glsl代码控制的,可以看前几节。

Bug3:圆形放大,但是实际参与物理计算的包围盒没放大

  • 解释Bug

    请添加图片描述

    圆形scale为3,圆的包围盒虽然显示正确,虽然绘图能正确绘制,但是实际运行物理模拟计算还是为1

    (注意区分:这不是下面Bug2所说的,渲染的包围盒小一半,而是参与物理模拟计算小了)

  • 原因

    参与物理计算的圆形包围盒的radius半径没有跟随物体的放大缩小

    circleShape.m_radius = cc2d.Radius;
    

    物体放大为3倍了,而包围盒参与计算的radius没有放大,还是0.5,所以会有bug,应该为1.5的

  • 应该改为

    circleShape.m_radius = transform.Scale.x * cc2d.Radius;
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘建杰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值