32-zBuffer算法

参考 https://www.bilibili.com/video/BV1y3411S7e1

交叉条

zBuffer消隐

伪深度

重心坐标插值

zBuffer消隐藏

算法设计

完整代码

// 32-zBuffer算法
// 参考 https://www.bilibili.com/video/BV1y3411S7e1

#define UNICODE
#include <Windows.h>
#include <Windowsx.h>
#include <math.h>
#define WINDOW_TEXT L"32-zBuffer算法"
#define ROUND(d)  int(floor(d)+0.5)  // 四舍五入
#define PI        3.1415926
#define RGB2RGB(c) RGB(c.red*255,c.green*255,c.blue*255)

struct RGB24   // RGB颜色
{
public:
    RGB24() :red(0), green(0), blue(0) {};
    RGB24(double red, double green, double blue) :red(red), green(green), blue(blue) {};
    friend RGB24 operator + (const RGB24& clr0, const RGB24& clr1)
    {
        return  RGB24(clr0.red + clr1.red, clr0.green + clr1.green, clr0.blue + clr1.blue);
    };
    friend RGB24 operator * (const RGB24& clr, double scalar)
    {
        return  RGB24(clr.red * scalar, clr.green * scalar, clr.blue * scalar);
    }
    friend RGB24 operator * (double scalar, const RGB24& clr)
    {
        return clr * scalar;
    }

public:
    double red;
    double green;
    double blue;
};

struct Point2  // 二维点
{
    double x;
    double y;
    double w;
    Point2() :x(0), y(0), w(1) {}
    Point2(double x, double y) :x(x), y(y), w(1) {}

    friend Point2 operator + (Point2 pt0, Point2 pt1)
    {
        return  Point2(pt0.x + pt1.x, pt0.y + pt1.y);
    }

    friend Point2 operator * (Point2 pt, double n)
    {
        return  Point2(pt.x * n, pt.y * n);
    }

    friend Point2 operator * (double n, Point2 pt)
    {
        return  Point2(pt.x * n, pt.y * n);
    }
};

struct Point3 :Point2  // 三维点
{
    double z;
    RGB24  c;
    Point3() :z(0),c(0,0,0) {}
    Point3(double x, double y, double z) :Point2(x, y), z(z), c(0, 0, 0) {}
    friend Point3 operator + (Point3 pt0, Point3 pt1)
    {
        return  Point3(pt0.x + pt1.x, pt0.y + pt1.y, pt0.z + pt1.z);
    }

    friend Point3 operator * (double scalar, const Point3& pt)
    {
        return  Point3(pt.x * scalar, pt.y * scalar, pt.z * scalar);
    }

    friend Point3 operator * (const Point3& pt, double scalar)
    {
        return  Point3(pt.x * scalar, pt.y * scalar, pt.z * scalar);
    }
    friend Point3 operator / (const Point3& pt, double scalar)
    {
        return  Point3(pt.x / scalar, pt.y / scalar, pt.z / scalar);
    }


    double DotProduct(const Point3& p0, const Point3& p1)   // 向量的点积
    {
        return(p0.x * p1.x + p0.y * p1.y + p0.z * p1.z);
    }

    Point3 CrossProduct(const Point3& v0, const Point3& v1) // 向量的叉积
    {
        return Point3(v0.y * v1.z - v0.z * v1.y,
            v0.z * v1.x - v0.x * v1.z,
            v0.x * v1.y - v0.y * v1.x);
    }
};

class  Transform3
{
public:
    Point3* _P;          // 顶点数组
    int     _ptNumber;   // 顶点数量
    double  _M[4][4];    // 变换矩阵
public:
    Transform3() :_P(NULL), _ptNumber(0)
    {
        memset(_M, 0, sizeof(_M));
    };
    ~Transform3() {};

    void MultiplyMatrix()  //矩阵相乘
    {
        Point3* PTemp = new Point3[_ptNumber];
        for (int i = 0; i < _ptNumber; i++)
        {
            PTemp[i] = _P[i];
        }
        for (int i = 0; i < _ptNumber; i++)
        {
            _P[i].x = _M[0][0] * PTemp[i].x + _M[0][1] * PTemp[i].y + _M[0][2] * PTemp[i].z + _M[3][0]; // * PTemp[i].w
            _P[i].y = _M[1][0] * PTemp[i].x + _M[1][1] * PTemp[i].y + _M[1][2] * PTemp[i].z + _M[3][1]; // * PTemp[i].w
            _P[i].z = _M[2][0] * PTemp[i].x + _M[2][1] * PTemp[i].y + _M[2][2] * PTemp[i].z + _M[3][2]; // * PTemp[i].w
            _P[i].w = _M[3][0] * PTemp[i].x + _M[3][1] * PTemp[i].y + _M[3][2] * PTemp[i].z + _M[3][3]; // * PTemp[i].w
        }

        delete[]PTemp;
    }

    void SetMatrix(Point3* P, int ptNumber) //顶点数组初始化
    {
        _P = P;
        _ptNumber = ptNumber;
    }
    void Identity()
    {
        _M[0][0] = 1.0, _M[0][1] = 0.0, _M[0][2] = 0.0, _M[0][3] = 0.0;
        _M[1][0] = 0.0, _M[1][1] = 1.0, _M[1][2] = 0.0, _M[1][3] = 0.0;
        _M[2][0] = 0.0, _M[2][1] = 0.0, _M[2][2] = 1.0, _M[2][3] = 0.0;
        _M[3][0] = 0.0, _M[3][1] = 0.0, _M[3][2] = 0.0, _M[3][3] = 1.0;
    }

    void Translate(double tx, double ty, double tz)//平移变换
    {
        Identity();
        _M[3][0] = tx;
        _M[3][1] = ty;
        _M[3][2] = tz;
        MultiplyMatrix();
    }

    void Scale(double sx, double sy, double sz)//缩放变换
    {
        Identity();
        _M[0][0] = sx;
        _M[1][1] = sy;
        _M[2][2] = sz;
        MultiplyMatrix();
    }

    void RotateX(double beta)//绕X轴旋转变换
    {
        Identity();
        beta = beta * PI / 180;
        _M[1][1] = cos(beta), _M[1][2] = -sin(beta);
        _M[2][1] = sin(beta), _M[2][2] = cos(beta);
        MultiplyMatrix();
    }

    void  RotateY(double beta)//绕Y轴旋转变换
    {
        Identity();
        beta = beta * PI / 180;
        _M[0][0] = cos(beta), _M[0][2] = sin(beta);
        _M[2][0] = -sin(beta), _M[2][2] = cos(beta);
        MultiplyMatrix();
    }

    void  RotateZ(double beta)//绕Z轴旋转变换
    {
        Identity();
        beta = beta * PI / 180;
        _M[0][0] = cos(beta), _M[0][1] = -sin(beta);
        _M[1][0] = sin(beta), _M[1][1] = cos(beta);
        MultiplyMatrix();
    }

};

class  Projection  // 透视投影
{
public:
    Point3 Eye;   // 视点
    double R;     // 视点球坐标
    double Phi;   // 
    double Theta; // 
    double D;     // 
    double K[8];  // 透视常数
    double Near;  // 近剪切面
    double Far;   // 远剪切面
public:
    Projection()
    {       
        R = 1000;
        D = 800;
        Phi = 90;
        Theta = 0;      // 世界坐标z轴正上方 看向原点
        Near = D;
        Far = 2000;
        InitialParameter();
    }
    ~Projection() {}

    void InitialParameter() // 初始化参数
    {
        K[0] = sin(PI * Theta / 180);
        K[1] = sin(PI * Phi / 180);
        K[2] = cos(PI * Theta / 180);
        K[3] = cos(PI * Phi / 180);
        K[4] = K[1] * K[2];
        K[5] = K[0] * K[1];
        K[6] = K[2] * K[3];
        K[7] = K[0] * K[3];
        Eye = Point3(R * K[5], R * K[5], R * K[4]);  // 设置视点
    }

    void SetEye(double r, double phi, double theta)   // 设置视点
    {
        R = r;
        Phi = phi;
        Theta = theta;
        InitialParameter();
    }

    Point3 GetEye()
    {
        return Eye;
    }

    Point2 OrthographicProjection(Point3 worldPoint) // 正交投影
    {
        return Point2(worldPoint.x, worldPoint.y);
    }

    Point2 CavalierProjection(Point3 worldPoint)     // 斜等侧投影
    {
        Point2 screenPoint; // 屏幕坐标
        double cota = 1;
        double beta = PI / 4;
        screenPoint.x = worldPoint.x - worldPoint.z * cota * cos(beta);
        screenPoint.y = worldPoint.y - worldPoint.z * cota * cos(beta);
        return screenPoint;
    }

    Point2 CabinetProjection(Point3 worldPoint)      // 斜二侧投影
    {
        Point2 screenPoint; // 屏幕坐标
        double cota = 0.5;
        double beta = PI / 4;
        screenPoint.x = worldPoint.x - worldPoint.z * cota * cos(beta);
        screenPoint.y = worldPoint.y - worldPoint.z * cota * cos(beta);
        return screenPoint;
    }

    Point2 PerspectiveProjection2(Point3 worldPoint) // 二维透视投影
    {
        Point3 viewPoint;  // 观察坐标系
        viewPoint.x = K[2] * worldPoint.x - K[0] * worldPoint.z;
        viewPoint.y = -K[7] * worldPoint.x + K[1] * worldPoint.y - K[6] * worldPoint.z;
        viewPoint.z = -K[5] * worldPoint.x - K[3] * worldPoint.y - K[4] * worldPoint.z + R;

        Point2 screenPoint; // 屏幕坐标
        screenPoint.x = D * viewPoint.x / viewPoint.z;
        screenPoint.y = D * viewPoint.y / viewPoint.z;

        return screenPoint;
    }

    Point3 PerspectiveProjection3(Point3 worldPoint) // 三维坐标点
    {
        Point3 viewPoint;  // 观察坐标系
        viewPoint.x = K[2] * worldPoint.x - K[0] * worldPoint.z;
        viewPoint.y = -K[7] * worldPoint.x + K[1] * worldPoint.y - K[6] * worldPoint.z;
        viewPoint.z = -K[5] * worldPoint.x - K[3] * worldPoint.y - K[4] * worldPoint.z + R;
        viewPoint.c = worldPoint.c;

        Point3 screenPoint; // 屏幕坐标
        screenPoint.x = D * viewPoint.x / viewPoint.z;
        screenPoint.y = D * viewPoint.y / viewPoint.z;
        screenPoint.z = Far * (1 - Near / viewPoint.z) / (Far - Near);
        screenPoint.c = viewPoint.c;
        return screenPoint;
    }

};

class ZBuffer // 消隐
{
    Point3   P0, P1, P2;  // 三角形顶点坐标
    double** _zBuffer;     // 深度缓冲区
    int      _nWidth;      // 缓冲区宽
    int      _nHeight;     // 缓冲区高

public:
    ZBuffer() {};
    ~ZBuffer() 
    {
        if (_zBuffer)
        {
            for (int i = 0; i < _nWidth; i++)
            {
               delete[] _zBuffer[i] ;
               _zBuffer[i] = NULL;
            }
            
            delete[] _zBuffer;
            _zBuffer = NULL;
        }

    };
    void SetPoint(Point3 *p)  // 顶点构造
    {
        P0 = p[0];
        P1 = p[1];
        P2 = p[2];
    }
    void InitialDepthBuffer(int nWidth,int nHeight) // 初始化深度缓存区
    {
        _nWidth   = nWidth;
        _nHeight  = nHeight;
        _zBuffer  = new double* [nWidth];
        for (int i = 0; i < nWidth; i++)
        {
            _zBuffer[i] = new double[nHeight];
        }
       
    }
    void ClearDepthBuffer(double nDepth)
    {         
        // 初始化深度缓冲区
        for (int i = 0; i < _nWidth; i++) // 初始化深度缓冲区
        {
            for (int j = 0; j < _nHeight; j++)
            {
                _zBuffer[i][j] = nDepth;
            }
        }

    }
    void FillTriangle(HDC hdc)  // 重心坐标填充三角形
    {       
        int xMin = ROUND(min(min(P0.x, P1.x), P2.x));  // 包围盒左下坐标
        int yMin = ROUND(min(min(P0.y, P1.y), P2.y));
        int xMax = ROUND(max(max(P0.x, P1.x), P2.x));  // 包围盒右上坐标
        int yMax = ROUND(max(max(P0.y, P1.y), P2.y));

        for (int y = yMin; y <= yMax; y++)
        {
            for (int x = xMin; x <= xMax; x++)
            {
                double area = P0.x * P1.y + P1.x * P2.y + P2.x * P0.y - P2.x * P1.y - P1.x * P0.y - P0.x * P2.y;
                double area0 = x * P1.y + P1.x * P2.y + P2.x * y - P2.x * P1.y - P1.x * y - x * P2.y;
                double area1 = P0.x * y + x * P2.y + P2.x * P0.y - P2.x * y - x * P0.y - P0.x * P2.y;
                double area2 = P0.x * P1.y + P1.x * y + x * P0.y - x * P1.y - P1.x * P0.y - P0.x * y;
                
                // 计算重心坐标
                double alpha = area0 / area;
                double beta  = area1 / area;
                double gamma = area2 / area;

                if (alpha >= 0 && beta >= 0 && gamma >= 0)
                {
                    RGB24 crColor = alpha * P0.c + beta * P1.c + gamma * P2.c; // 计算三角形内内任一点颜色
                    double zDepth = alpha * P0.z + beta * P1.z + gamma * P2.z; // 计算三角形内内任一点深度
                    
                    //  左手坐标系,Z轴指向屏幕内,z越小距离屏幕越近。
                    //  _nWidth / 2 _nHeight / 2 半宽高 消除负数
                    if (zDepth <= _zBuffer[x + _nWidth / 2][y + _nHeight / 2]) // ZBuffer
                    {
                        _zBuffer[x + _nWidth / 2][y + _nHeight / 2] = zDepth;
                        SetPixelV(hdc, ROUND(x), ROUND(y), RGB2RGB(crColor));
                        
                    }
                    
                }

            }

        }

    }
};

class Face
{
public:  
    int ptIndex[4];    // 面的顶点索引
public:
    Face()  {};
    ~Face() {};

};

class Bar     // 长方体
{
public:
    Point3     m_V[8];         // 顶点数组
private:
    int        m_a, m_b, m_c;  // 长 宽 高
    RGB24      m_ClrLeft;      // 左半部分颜色
    RGB24      m_ClrRight;     // 右半部分颜色
    Face       m_F[6];         // 6个小面
    Projection m_projection;   // 投影
    ZBuffer    m_pZBuffer;
public:
    Bar() {};
    ~Bar() {};

    void SetParameter(int nLength,int nWidth,int nHeight, RGB24 clrLeft,RGB24 clrRight) //设置参数
    {
        m_a = nLength;
        m_b = nWidth;
        m_c = nHeight;
        m_ClrLeft = clrLeft;
        m_ClrRight = clrRight;

    }

    void ReadVertex() // 读入点表
    {
        m_V[0].x = -m_a / 2.0;  m_V[0].y = -m_b / 2.0; m_V[0].z = -m_c / 2.0; m_V[0].c = m_ClrLeft;
        m_V[1].x = +m_a / 2.0;  m_V[1].y = -m_b / 2.0; m_V[1].z = -m_c / 2.0; m_V[1].c = m_ClrRight;
        m_V[2].x = +m_a / 2.0;  m_V[2].y = +m_b / 2.0; m_V[2].z = -m_c / 2.0; m_V[2].c = m_ClrRight;
        m_V[3].x = -m_a / 2.0;  m_V[3].y = +m_b / 2.0; m_V[3].z = -m_c / 2.0; m_V[3].c = m_ClrLeft;
        m_V[4].x = -m_a / 2.0;  m_V[4].y = -m_b / 2.0; m_V[4].z = +m_c / 2.0; m_V[4].c = m_ClrLeft;
        m_V[5].x = +m_a / 2.0;  m_V[5].y = -m_b / 2.0; m_V[5].z = +m_c / 2.0; m_V[5].c = m_ClrRight;
        m_V[6].x = +m_a / 2.0;  m_V[6].y = +m_b / 2.0; m_V[6].z = +m_c / 2.0; m_V[6].c = m_ClrRight;
        m_V[7].x = -m_a / 2.0;  m_V[7].y = +m_b / 2.0; m_V[7].z = +m_c / 2.0; m_V[7].c = m_ClrLeft;
    }

    void ReadFacet()  // 读入面表
    {
        m_F[0].ptIndex[0] = 4; m_F[0].ptIndex[1] = 5; m_F[0].ptIndex[2] = 6; m_F[0].ptIndex[3] = 7;
        m_F[1].ptIndex[0] = 0; m_F[1].ptIndex[1] = 3; m_F[1].ptIndex[2] = 2; m_F[1].ptIndex[3] = 1;
        m_F[2].ptIndex[0] = 0; m_F[2].ptIndex[1] = 4; m_F[2].ptIndex[2] = 7; m_F[2].ptIndex[3] = 3;
        m_F[3].ptIndex[0] = 1; m_F[3].ptIndex[1] = 2; m_F[3].ptIndex[2] = 6; m_F[3].ptIndex[3] = 5;
        m_F[4].ptIndex[0] = 2; m_F[4].ptIndex[1] = 3; m_F[4].ptIndex[2] = 7; m_F[4].ptIndex[3] = 6;
        m_F[4].ptIndex[0] = 2; m_F[4].ptIndex[1] = 3; m_F[4].ptIndex[2] = 7; m_F[4].ptIndex[3] = 6;
        m_F[5].ptIndex[0] = 0; m_F[5].ptIndex[1] = 1; m_F[5].ptIndex[2] = 5; m_F[5].ptIndex[3] = 4;
    }
    void Draw(HDC hdc,ZBuffer* pZbuffer)  // 绘图
    {
        for (int nFace = 0; nFace < 6; nFace++) // 立方体6个面
        {

            Point3 points[4];
            for (int nPoint = 0; nPoint < 4; nPoint++) // 立方体4个顶点
            {
                points[nPoint] = m_projection.PerspectiveProjection3(m_V[m_F[nFace].ptIndex[nPoint]]);// 投影
            }

            // 左侧三角形
            Point3 RDP[3] = { points[0], points[1], points[2] };
            pZbuffer->SetPoint(RDP);
            pZbuffer->FillTriangle(hdc);

            // 右侧三角形
            Point3 LDP[3] = { points[0], points[2], points[3] };
            pZbuffer->SetPoint(LDP);
            pZbuffer->FillTriangle(hdc);

        }
                
    }

};

static bool     g_Play = false;
static wchar_t* g_str = L"左键旋转";

int             g_NumberObject;      // 场景中物体数量
Bar             g_bar[4];            // 交插条数量
Transform3      g_transform[4];      // 坐标变化
ZBuffer*        m_pZBuffer ;
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    PAINTSTRUCT ps;
    HDC hdc;
    RECT rect;
    static double alpha;
    static double beta;

    int a = 500;  // 长度
    int b = 60;   // 宽度
    int c = 300;  // 厚度
    int d = 120;  // 交叉头
    int e = 160;  // 收缩宽度

    RGB24 red   (1.0, 0.0, 0.0);
    RGB24 green (0.0, 1.0, 0.0);
    RGB24 blue  (0.0, 0.0, 1.0);
    RGB24 yellow(1.0, 1.0, 0.0);

    g_NumberObject = 4;

    g_bar[0].SetParameter(a, b, c, green, red);
    g_bar[1].SetParameter(a, b, c, red, green);
    g_bar[2].SetParameter(a, b, e, blue, yellow);
    g_bar[3].SetParameter(a, b, e, yellow, blue);


    for (int i = 0; i < g_NumberObject; i++)
    {
        g_bar[i].ReadVertex();
        g_bar[i].ReadFacet();
        g_transform[i].SetMatrix(g_bar[i].m_V, 8);

    }

    g_transform[0].Translate(0,  d + b / 2.0, 0);    // 顶面上方条位置
    g_transform[1].Translate(0, -d - b / 2.0, 0);   // 底面上方条位置
    g_transform[2].RotateZ(90);                     // 左侧上方条位置
    g_transform[2].Translate(-d - b / 2.0, 0, 0);   // 左侧上方条位置
    g_transform[3].RotateZ(90);                     // 右侧上方条位置
    g_transform[3].Translate(d + b / 2.0, 0, 0);    // 右侧上方条位置   

    switch (uMsg)
    {
    case WM_TIMER:
        alpha += 0.5;
        beta += 0.5;
        InvalidateRect(hwnd, NULL, FALSE);
        return 0;

    case WM_LBUTTONDOWN:
        g_Play = !g_Play;
        if (g_Play)
        {
            SetTimer(hwnd, 0, 0, NULL);
        }
        else
        {
            KillTimer(hwnd, 0);
        }
        return 0;

    case WM_PAINT:
    {
        hdc = BeginPaint(hwnd, &ps);
        GetClientRect(hwnd, &rect);
        SetMapMode(hdc, MM_ANISOTROPIC);
        SetWindowExtEx(hdc, rect.right, rect.bottom, NULL);
        SetViewportExtEx(hdc, rect.right, -rect.bottom, NULL);
        SetViewportOrgEx(hdc, rect.right / 2, rect.bottom / 2, NULL);
        // 内存DC
        HDC     memDC = CreateCompatibleDC(hdc);                                   // 创建兼容DC 画板
        HBITMAP newBitmap = CreateCompatibleBitmap(hdc, rect.right, rect.bottom);  // 创建画布
        HGDIOBJ oldBitmap = SelectObject(memDC, newBitmap);                        // 将画布选入画板
        //FillRect(memDC, &rect, (HBRUSH)(COLOR_WINDOW + 1));
        DrawText(memDC, g_str, wcslen(g_str), &rect, NULL);
        SetMapMode(memDC, MM_ANISOTROPIC);
        SetWindowExtEx(memDC, rect.right, rect.bottom, NULL);
        SetViewportExtEx(memDC, rect.right, -rect.bottom, NULL);
        SetViewportOrgEx(memDC, rect.right / 2, rect.bottom / 2, NULL);

        // 绘制图形
        { 
            // 清空缓冲区
            m_pZBuffer->ClearDepthBuffer(1000);
            
            if (g_Play)
            {
                 for (int i = 0; i < g_NumberObject; i++)
                 {
                     g_transform[i].RotateX(alpha);
                     g_transform[i].RotateY(beta);                
                 }             
            }

            for (int i = 0; i < g_NumberObject; i++)
            {
                g_bar[i].Draw(memDC, m_pZBuffer);
            }
           
        }

        // 内存dc复制到设备
        BitBlt(hdc, ROUND(-rect.right / 2), ROUND(-rect.bottom / 2), rect.right, rect.bottom, memDC, ROUND(-rect.right / 2), ROUND(-rect.bottom / 2), SRCCOPY);
        SelectObject(memDC, oldBitmap);
        DeleteObject(newBitmap);
        DeleteDC(memDC);
        EndPaint(hwnd, &ps);
    }
    return 0;

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;

    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow)
{
    // 创建深度缓冲区
    m_pZBuffer = new  ZBuffer;
    m_pZBuffer->InitialDepthBuffer(1000.0, 1000.0); // 初始化深度缓冲区         
   

    const wchar_t CLASS_NAME[] = L"CAG";
    WNDCLASS wc = { };

    wc.style = CS_HREDRAW | CS_VREDRAW; // 重新绘制整个工作区
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = CLASS_NAME;
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    RegisterClass(&wc);

    HWND hwnd = CreateWindowEx(0, CLASS_NAME,WINDOW_TEXT,WS_OVERLAPPEDWINDOW,         
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
        NULL, NULL,  hInstance,  NULL);

    if (hwnd == NULL)
    {
        return 0;
    }
    ShowWindow(hwnd, nCmdShow);
    MSG msg = { };
    while (GetMessage(&msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    delete m_pZBuffer; // 删除 zBuffer

    return 0;  
}

总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值