Qt例子学习笔记 - Examples/Qt-6.2.0/qt3d/instanced-arrays-qml

71 篇文章 4 订阅
本文深入探讨了Qt3D的框架图(FrameGraph)概念,这是一种数据驱动的方法,用于配置渲染器以满足特定的3D场景需求。文章通过实例展示了前向渲染、多视口渲染和延迟渲染的FrameGraph配置,强调了其灵活性和可扩展性。同时,讲解了帧图的构建规则和渲染过程,以及如何在运行时动态调整以适应不同场景和硬件。
摘要由CSDN通过智能技术生成

main.cpp

#include "instancebuffer.h"
#include <Qt3DQuickExtras/qt3dquickwindow.h>
#include <Qt3DQuick/QQmlAspectEngine>
#include <QQmlEngine>
#include <QQmlContext>
#include <QGuiApplication>

int main(int argc, char* argv[])
{
    QGuiApplication app(argc, argv);
    Qt3DExtras::Quick::Qt3DQuickWindow view;

    InstanceBuffer buffer;
    view.engine()->qmlEngine()->rootContext()->setContextProperty("_instanceBuffer", &buffer);
    view.setSource(QUrl("qrc:/main.qml"));
    view.show();

    return app.exec();
}

main.qml

import Qt3D.Core 2.0
import Qt3D.Render 2.0
import Qt3D.Input 2.0
import Qt3D.Extras 2.0
import QtQuick 2.2 as QQ2

Entity {
    id: sceneRoot
    //定义将渲染场景的视点
    Camera {
        id: camera
        projectionType: CameraLens.PerspectiveProjection
        fieldOfView: 22.5
        aspectRatio: 16/9
        nearPlane : 0.1
        farPlane : 1000.0
        position: Qt.vector3d(9.5, 7.0, 45.0)
        upVector: Qt.vector3d(0.0, 1.0, 0.0)
        viewCenter: Qt.vector3d(9.5, 0.5, 0.0)
    }
    //FirstPersonCameraController 允许从第一人称视角控制场景相机
    FirstPersonCameraController { camera: camera }

    //RenderSettings 类型保存与渲染过程相关的设置并托管活动的 FrameGraph
    //Qt 3D 渲染方面允许渲染算法完全由数据驱动
    //控制数据结构称为框架图。
    //类似于 Qt 3D ECS(实体组件系统)允许您通过从实体和组件树构建场景来定义所谓的场景图,
    //框架图也是一种树结构,但用于不同的目的。 即,控制场景的渲染方式。
    //在渲染单个帧的过程中,3D 渲染器可能会多次更改状态。
    //这些状态变化的数量和性质不仅取决于在场景中找到了哪些材质(着色器、网格几何体、纹理和统一变量)
    //还取决于您使用的高级渲染方案。
    //例如,使用传统的简单前向渲染方案与使用延迟渲染方法非常不同。
    //其他功能(例如反射、阴影、多个视口和早期 z-fill 通道)都会更改渲//染器需要在帧过程中设置的状态以及何时需要发生这些状态更改。
    //相比之下,
    //负责绘制 Qt Quick 2 场景的 Qt Quick 2 场景图渲染器在 C++ 中进行了硬连接,
    //以执行诸如批处理基元和渲染不透明项目,然后渲染透明项目之类的操作。
    //在 Qt Quick 2 的情况下,这完全没问题,因为它涵盖了所有要求。
    //从上面列出的一些示例中可以看出,鉴于可用的渲染方法众多,
    //这种硬连线渲染器不太可能足够灵活用于通用 3D 场景。
    //或者,如果渲染器可以足够灵活以涵盖所有此类情况,则其性能可能会因过于通用而受到影响。
    //更糟糕的是,一直在研究更多的渲染方法。
    //因此,我们需要一种既灵活又可扩展,同时又易于使用和维护的方法。 输入框架图!
    //帧图中的每个节点定义了渲染器将用于渲染场景的配置的一部分。
    //framegraph 树中节点的位置决定了以该节点为根的子树何时何地将成为渲染管道中的活动配置。
    //正如我们稍后将看到的,渲染器遍历这棵树,以便在帧中的每个点建立渲染算法所需的状态。
    //显然,如果您只想在屏幕上渲染一个简单的立方体,您可能会认为这太过分了。
    //但是,一旦您想开始制作稍微复杂的场景,这就会派上用场。
    //对于常见情况,Qt 3D 提供了一些开箱即用的示例框架图。
    //我们将通过展示一些示例和由此产生的框架图来展示框架图概念的灵活性。
    //请注意,与由实体和组件组成的场景图不同,框架图仅由嵌套节点组成,
    //,这些节点都是 Qt3DRender::QFrameGraphNode 的子类。
    //这是因为 framegraph 节点不是我们虚拟世界中的模拟对象,而是支持信息。
    //我们很快就会看到如何构建我们的第一个简单的框架图,
    //但在此之前,我们将介绍可用的框架图节点。
    //同样与场景图树一样,QML 和 C++ API 是一对一的匹配,因此您可以选择最喜欢的那个。 为了可读性和简洁性,本文选择了 QML API。
    //框架图的美妙之处在于结合了这些简单的节点类型
    //可以配置渲染器以满足您的特定需求,而根本不涉及任何繁琐的低级 C/C++ 渲染代码。
    //FrameGraph Rules
    //为了构建一个正确运行的框架图树,您应该了解一些有关如何遍历它以及如何将其提供给 Qt 3D 渲染器的规则。
    //Setting the Framegraph
    //FrameGraph 树应该分配给 QRenderSettings 组件的
    //activeFrameGraph 属性
    //它本身是 Qt 3D 场景中根实体的一个组件。这就是使它成为渲染器的活动帧图的原因。
    //当然,由于这是一个 QML 属性绑定,
    //活动框架图(或其部分)可以在运行时动态更改。
    //例如,如果您想对室内和室外场景使用不同的渲染方法或启用或禁用某些特殊效果。
    /*
        Entity
        {
            id:sceneRoot
            components:RenderSettings
            {
                activeFrameGraph:...//FrameGraph tree
            }
        }
    */

    //注意:activeFrameGraph 是 QML 中 FrameGraph 组件的默认属性。
    /*
        Entity
        {
            id:sceneRoot
            components:RenderSettings
            {
                ...//FrameGraph tree
            }
        }
    */
    //如何使用框架图
    //Qt 3D 渲染器执行帧图树的深度优先遍历。
    //请注意,因为遍历是深度优先,所以定义节点的顺序很重要。
    //当渲染器到达 framegraph 的叶子节点时,
    //它将所有由从叶子节点到根节点的路径指定的状态收集在一起。
    //这定义了用于渲染帧的一部分的状态。
    //如果您对 Qt 3D 的内部结构感兴趣,则此状态集合称为 RenderView。
    //给定 RenderView 中包含的配置,渲染器将要渲染的场景图形中的所有实体收集在一起,
    //并从中构建一组 RenderCommands 并将它们与 RenderView 相关联。
    //RenderView 和一组 RenderCommands 的组合被传递给 OpenGL。
    //当对帧图中的每个叶节点重复此操作时,
    //帧就完成了,渲染器调用 QOpenGLContext::swapBuffers() 来显示帧。
    //从本质上讲,framegraph 是一种用于配置 Qt 3D 渲染器的数据驱动方法
    //由于其数据驱动的性质,我们可以在运行时更改配置,
    //允许非 C++ 开发人员或设计人员更改框架的结构,
    //并尝试新的渲染方法,而无需编写数千行样板代码。
    
    //Framegraph Examples
    //现在您知道编写框架图树时要遵守的规则,
    //我们将回顾几个例子并分解它们。
    
    //A Simple Forward Renderer
    //前向渲染是指您以传统方式使用 OpenGL
    //并一次一个对象直接渲染到后台缓冲区,并在我们进行时对每个对象进行着色。
    //这与我们渲染到中间 G 缓冲区的延迟渲染相反。
    //这是一个可用于前向渲染的简单 FrameGraph:
    /*
        Viewport
        {
            normalizedRect:Qt.rect(0.0, 0.0, 1.0, 1.0)
            property alias camera: cameraSelector.camera

            clearBuffers
            {
                buffers:ClearBuffers.ColorDepthBuffer

                CameraSelector
                {
                    id:cameraSelector
                }
            }
        }
    */
    /*
    //如您所见,这棵树只有一个叶子,总共由 3 个节点组成,如下图所示。
                                        RenderView

    Viewport(0,0,1,1)                |-viewPort(0,0,1,1)
                                     |
    ClearBuffer------------------->  |-TechniqueFilter
                                     |
    Camera Selector                  |-CameraSelector
    (Main Camera)                    |(Main Camera)
    */
    //使用上面定义的规则,这个框架图树产生了一个具有以下配置的 RenderView:
    //叶节点 -> 渲染视图
    //填满整个屏幕的视口(使用标准化坐标以方便支持嵌套视口)
    //颜色和深度缓冲区设置为清除
    //在公开的相机属性中指定的相机
    //几个不同的 FrameGraph 树可以产生相同的渲染结果。
    //只要从叶子到根收集的状态相同,结果也将相同。
    //最好将最长时间保持不变的状态放在更靠近框架图根的位置
    //因为这将导致叶节点更少,因此整体渲染视图更少。
    /*
        Viewport
        {
            normalizedRect:Qt.rect(0.0, 0.0, 1.0, 1.0)
            property alias camera:cameraSelector.camera

            CameraSelector
            {
                id:cameraSelector

                clearBuffers
                {
                    buffers:ClearBuffers.ColorDepthBuffer
                }
            }
        }
    

        CameraSelector
        {
            ViewPort   
            {
                normalizedRect:Qt.rect(0.0,0.0,1.0,1.0)

                ClearBuffers
                {
                    buffers:ClearBuffers.ColorDepthBuffer
                }
            }
        }
    */

    //多视口FrameGraph
    //让我们继续看一个稍微复杂一些的示例,
    //该示例从 4 个虚拟摄像机的角度将场景图形渲染到窗口的 4 个象限中。
    //这是 3D CAD 或建模工具的常见配置,
    //或者可以进行调整以帮助渲染赛车游戏或闭路电视摄像机显示器中的后视镜。

    /*
        Viewport
        {
            id:mainViewport
            normalizedRect:Qt.rect(0,0,1,1)
            property alias Camera:cameraSelectorTopLeftViewport.camera
            property alias Camera:cameraSelectorTopRightViewport.camera
            property alias Camera: cameraSelectorBottomLeftViewport.camera
            property alias Camera: cameraSelectorBottonRightViewport.camera

            ClearBuffers
            {
                buffers:ClearBuffers.ColorDepthBuffer
            }

            Viewport
            {
                id:topLeftViewport
                normalizedRect:Qt.rect(0,0,0.5,0.5)
                CameraSelector{id:cameraSelectorTopLeftViewport}
            }

            Viewport
            {
                id:topRightViewport
                normalizedRect:Qt.rect(0.5,0.0.5,0.5)
                CameraSelector{id:cameraSelectorTopRightViewport}
            }

            Viewport
            {
                id:bottomLeftViewport
                normalizedRect:Qt.rect(0, 0.5, 0.5, 0.5)
                CameraSelector{id:cameraSelectorBottomLeftViewport}
            }

            Viewport
            {
                id:bottomRightViewport
                normalizedRect:Qt.rect(0.5, 0.5, 0.5, 0.5)
                CameraSelector{id:cameraSelectorBottomRightViewport}
            }
        }
    */
    //这棵树有 5 片叶子,有点复杂。
    //遵循与之前相同的规则,我们从 FrameGraph 构造 5 个 RenderView 对象。
    //下图显示了前两个 RenderView 的构造。
    //其余的渲染视图与第二个图非常相似,只是其他子树。
    //完整地,创建的渲染视图是:
    //渲染视图 (1)
    //定义的全屏视口
    //颜色和深度缓冲区设置为清除
    //渲染视图 (2)
    //定义的全屏视口
    //定义的子视口(渲染视口将相对于其父视口进行缩放)
    //指定的 CameraSelector
    //渲染视图 (3)
    //定义的全屏视口
    //定义的子视口(渲染视口将相对于其父视口进行缩放)
    //指定的 CameraSelector
    //渲染视图 (4)
    //定义的全屏视口
    //定义的子视口(渲染视口将相对于其父视口进行缩放)
    //指定的 CameraSelector
    //渲染视图 (5)
    //定义的全屏视口
    //定义的子视口(渲染视口将相对于其父视口进行缩放)
    //指定的 CameraSelector
    //但是,在这种情况下,顺序很重要。
    //如果 ClearBuffers 节点是最后一个而不是第一个,这将导致黑屏,原因很简单,
    //所有内容都将在经过如此仔细的渲染后立即被清除。
    //出于类似的原因,它不能用作 FrameGraph 的根
    //因为这会导致调用为我们的每个视口清除整个屏幕。
    //尽管 FrameGraph 的声明顺序很重要,
    //但 Qt 3D 能够并行处理每个 RenderView,
    //因为每个 RenderView 都独立于其他渲染视图
    //以便在 RenderView 的状态有效时生成一组要提交的 RenderCommand。
    //Qt 3D 使用基于任务的并行方法,
    //该方法自然会随着可用内核的数量而扩展。 这在前一个示例的下图中显示
    //RenderViews 的 RenderCommands 可以跨多个内核并行生成,
    //只要我们注意在专用的 OpenGL 提交线程上以正确的顺序提交 RenderViews,生成的场景就会被正确渲染。
    //Deferred Renderer
    //在渲染方面,与前向渲染相比,延迟渲染在渲染器配置方面是一种不同的野兽。
    //延迟渲染不是绘制每个网格并应用着色器效果对其进行着色,而是采用了两次渲染通道方法。
    //首先,场景中的所有网格都使用相同的着色器绘制
    //该着色器通常为每个片段输出至少四个值:
    //世界法向量
    //颜色(或其他一些材料属性)
    //深度
    //世界位置向量
    //这些值中的每一个都将存储在纹理中。
    //法线、颜色、深度和位置纹理形成所谓的 G 缓冲区。
    //在第一遍期间,屏幕上没有绘制任何内容,而是绘制到 G-Buffer 中以备以后使用
    //绘制完所有网格后,G-Buffer 将填充相机当前可以看到的所有网格。
    //然后,通过从 G 缓冲区纹理读取法线、颜色和位置值并将颜色输出到全屏四边形,
    //使用第二个渲染通道将场景渲染到具有最终颜色阴影的后台缓冲区。
    //该技术的优点是复杂效果所需的大量计算能力仅在第二遍期间仅用于相机实际看到的元素。
    //第一遍不会消耗太多处理能力,因为每个网格都是使用简单的着色器绘制的。
    //因此,延迟渲染将着色和照明与场景中的对象数量分离,而是将其与屏幕(和 G 缓冲区)的分辨率耦合。
    //由于能够以额外的 GPU 内存使用为代价使用大量动态灯光,因此该技术已在许多游戏中使用。
    /*
        Viewport
        {
            id:root
            normalizedRect:Qt.rect(0.0, 0.0, 1.0, 1.0)

            property GBuffer gBuffer
            property alias camera:secenCameraSelector.camera
            property alias sceneLayer:sceneLayerFilter.layer
            property alias screenQuadLayer: screenQuadLayerFilter.layers

            RenderSurfaceSelector
            {
                CameraSelector
                {
                    id:sceneCameraSelector
                    //Fill G-Buffer
                    LayerFilter
                    {
                        id:sceneLayerFilter
                        RenderTargetSelector
                        {
                            id:gBufferTargetSelector
                            target:gBuffer

                            ClearBuffers
                            {
                                buffers:ClearBuffers.ColorDepthBuffer

                                RenderPassFilter
                                {
                                    id:geometryPass
                                    matchAny:FilterKey
                                    {
                                        name:"pass"
                                        value:"geometry"
                                    }
                                }
                            }
                        }
                    }
                    TechiqueFilter
                    {
                        parameter:
                        [
                            Parameter{ name:"color";value:gBuffer.color},
                            Parameter{ name:"position";value:gBuffer.position},
                            Parameter{ name:"normal"; value:gBuffer.normal},
                            Parameter{ name:"depth";value:gBuffer.depth}
                        ]

                        RenderStateSet
                        {
                            //Render FullScreen Quad
                            renderStates:
                            [
                                BlendEquation{ blendFunction: BlendEquation.Add },
                                BlendEquationArguments{
                                    sourceRgb:BlendEquationArguments.SourceAlpha
                                    destinationRgb:BlendEquationArguments.DestinationColor
                                }
                            ]
                            
                            LayerFilter
                            {
                                id:screenQuadLayerFilter
                                ClearBuffers
                                {
                                    buffers:ClearBuffers.ColorDepthBuffer
                                    RenderPassFilter
                                    {
                                        mathAny:FilterKey
                                        {
                                            name:"pass"
                                            value:"final"
                                        }
                                        parameters:Parameter
                                        {
                                            name:"winSize"
                                            value:Qt.size(1024,768)
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            } 
        }
    */
    /*
                    Viewport
                    (0,0,1,1)
        ----------------------------------
        |                                |
     Layer Filter                  Layer Filter
     (scene)                       (screen quad)
        |                                |
  Render Target Selector             Clear Buffer
        |                                |
    Clear Buffer                    Render Pass Filter
        |                           (name="final")
  Render Pass Filter
  (name = "geometry")
        |
  Camera Selector
  (Main Camera)
        |
    */
    //生成的渲染视图是:
    //渲染视图 (1)
    //指定应该使用哪个相机
    //定义一个充满整个屏幕的视口
    //选择图层组件场景图层的所有实体
    //将 gBuffer 设置为活动渲染目标
    //清除当前绑定的渲染目标(gBuffer)上的颜色和深度
    //仅选择场景中具有与 RenderPassFilter 中的注释匹配的材质和技术的实体
    //渲染视图 (2)
    //定义一个充满整个屏幕的视口
    //选择图层组件 screenQuadLayer 的所有实体
    //清除当前绑定的帧缓冲区(屏幕)上的颜色和深度缓冲区
    //仅选择场景中具有与 RenderPassFilter 中的注释匹配的材质和技术的实体

    //框架图的其他好处
    //由于 FrameGraph 树完全由数据驱动并且可以在运行时动态修改,因此您可以
    //为不同的平台和硬件拥有不同的框架图树,并在运行时选择最合适的
    //在场景中轻松添加和启用可视化调试
    //根据您需要为场景的特定区域渲染的内容的性质,使用不同的 FrameGraph 树
    //无需修改 Qt 3D 的内部结构即可实现新的渲染技术
    //结论
    //我们已经介绍了 FrameGraph 和组成它的节点类型。
    //然后我们继续讨论几个例子来说明框架图的构建规则以及 Qt 3D 引擎如何在幕后使用框架图。
    //到现在为止,您应该对 FrameGraph 以及如何使用它有一个很好的概述(也许可以将早期的 z-fill 传递添加到前向渲染器)。
    //此外,您应该始终牢记 FrameGraph 是一个供您使用的工具,这样您就不会受到 Qt 3D 开箱即用提供的渲染器和材料的束缚。
    



    components: [
        RenderSettings {
            activeFrameGraph: ForwardRenderer {
                clearColor: Qt.rgba(0, 0.5, 1, 1)
                camera: camera
            }
        },
        // Event Source will be set by the Qt3DQuickWindow
        InputSettings { }
    ]

    Material {
        id: instancedPhongMaterial
        effect: Effect {
            techniques: [
                Technique {
                    graphicsApiFilter {
                        api: GraphicsApiFilter.OpenGL
                        profile: GraphicsApiFilter.CoreProfile
                        minorVersion: 2
                        majorVersion: 3
                    }
                    filterKeys: FilterKey { name: "renderingStyle"; value: "forward" }
                    renderPasses: RenderPass {
                        shaderProgram: ShaderProgram {
                            vertexShaderCode: loadSource("qrc:/gl3/instanced.vert")
                            fragmentShaderCode: loadSource("qrc:/gl3/instanced.frag")
                        }
                    }
                },
                Technique {
                    graphicsApiFilter {
                        api: GraphicsApiFilter.RHI
                        profile: GraphicsApiFilter.NoProfile
                        minorVersion: 1
                        majorVersion: 0
                    }
                    filterKeys: FilterKey { name: "renderingStyle"; value: "forward" }
                    renderPasses: RenderPass {
                        shaderProgram: ShaderProgram {
                            vertexShaderCode: loadSource("qrc:/gl45/instanced.vert")
                            fragmentShaderCode: loadSource("qrc:/gl45/instanced.frag")
                        }
                    }
                }
            ]
        }
    }

    // Create a GeometryRenderer component that uses the standard CylinderGeometry to
    // create the base vertex and index data buffers and attributes.
    GeometryRenderer {
        id: cylinderMeshInstanced
        enabled: instanceCount != 0

        geometry: CylinderGeometry {
            rings: 50
            slices: 30
            radius: 0.3
            length: 3.0

            attributes: [ instanceDataAttribute ]
        }

        // Use our buffer created from C++ as per-instance position data to render
        // many instances (copies) of the base cylinder geometry in a single OpenGL
        // draw call where supported (OpenGL >=3.3 or OpenGL ES 3). On older versions
        // of OpenGL the instancing is emulated on the CPU using multiple draw calls.
        QQ2.SequentialAnimation {
            running: true
            loops: QQ2.Animation.Infinite

            QQ2.NumberAnimation {
                target: cylinderMeshInstanced
                property: "instanceCount"
                duration: 5000
                from: 0
                to: _instanceBuffer.instanceCount
            }

            QQ2.PauseAnimation {
                duration: 3000
            }

            QQ2.NumberAnimation {
                target: cylinderMeshInstanced
                property: "instanceCount"
                duration: 5000
                from: _instanceBuffer.instanceCount
                to: 0
            }

            QQ2.PauseAnimation {
                duration: 3000
            }
        }

        Attribute {
            id: instanceDataAttribute
            name: "pos"
            attributeType: Attribute.VertexAttribute
            vertexBaseType: Attribute.Float
            vertexSize: 3
            divisor: 1
            buffer: _instanceBuffer
        }
    }

    Entity {
        id: torusEntity
        components: [ cylinderMeshInstanced, instancedPhongMaterial ]
    }
}

instancebuffer.h

#ifndef INSTANCEBUFFER_H
#define INSTANCEBUFFER_H

#include <Qt3DCore/QBuffer>
//QBuffer为原始数据提供数据存储,以便稍后用作顶点或统一体。
//QBuffer::QBuffer(Qt3DCore::QNode *parent = nullptr)
//用父级构造一个新的 QBuffer。
//[signal] void QBuffer::dataAvailable()
//当数据可用时发出此信号。
//[signal] void QBuffer::dataChanged(const QByteArray &bytes)
//当数据改变时,这个信号是用字节发出的。
//QByteArray QBuffer::data() const
//返回数据。
//void QBuffer::setData(const QByteArray &bytes)
//将字节设置为数据。
//[invokable] void QBuffer::updateData(int offset, const QByteArray &bytes)
//通过用偏移量的字节替换数据来更新数据

class InstanceBuffer : public Qt3DCore::QBuffer
{
    Q_OBJECT
    Q_PROPERTY(int instanceCount READ instanceCount WRITE setInstanceCount NOTIFY instanceCountChanged)

public:
    InstanceBuffer(Qt3DCore::QNode *parent = 0);

    int instanceCount() const;

public slots:
    void setInstanceCount(int instanceCount);

signals:
    void instanceCountChanged(int instanceCount);

private:
    int m_instanceCount;
};

#endif // INSTANCEBUFFER_H

instancebuffer.cpp

#include "instancebuffer.h"

#include <QtGui/qvector3d.h>

static const int rowCount = 20;
static const int colCount = 20;
static const int maxInstanceCount = rowCount * colCount;

InstanceBuffer::InstanceBuffer(Qt3DCore::QNode *parent)
    : Qt3DCore::QBuffer(parent)
    , m_instanceCount(maxInstanceCount)
{
    // 创建一些每个实例的数据 - 每个实例的位置
    QByteArray ba;
    ba.resize(maxInstanceCount * sizeof(QVector3D));
    QVector3D *posData = reinterpret_cast<QVector3D *>(ba.data());
    for (int j = 0; j < rowCount; ++j) {
        const float z = float(j);
        for (int i = 0; i < colCount; ++i) {
            const float x = float(i);
            const QVector3D pos(x, 0.0f, z);
            *posData = pos;
            ++posData;
        }
    }

    // 将数据放入缓冲区
    setData(ba);
}

int InstanceBuffer::instanceCount() const
{
    return m_instanceCount;
}

void InstanceBuffer::setInstanceCount(int instanceCount)
{
    if (m_instanceCount == instanceCount)
        return;

    m_instanceCount = instanceCount;
    emit instanceCountChanged(instanceCount);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值