光线追踪(RayTracing)算法理论与实践(二)平面、材质、联合光线与物体求交

提要

经过上次的学习,我们已经可以建立一个简单的光线追踪的场景,接下来,我们继续我们的征程。

今天要得到的最终效果如下:



光线与物体求交

在光线追踪算法中,最重要的就是求光线与物体的相交,也就是实现IntersectResult Object::isIntersected(CRay _ray)方法。  因为我求得交点之后就可以对该点的像素进行计算,然后显示,后续的很多效果(透明,反射....)还有算法的优化加速,都是在对相交算法的改进。

之前已经讨论了光线与球体的相交,今天讨论平面,三角形和多个物体,且不考虑折射和反射。

平面

平面在空间几何中可以用一个向量(法向量)和平面中的一点P0来表示。

平面就是满足下式的点集:n.(P-P0)=0

得到:n.P=d;d=n.P0;


给定射线p(t)=p0+tu,平面方程为n.p+d=0,将p(t)带入到平面方程,最后求得t:

t=(-d-(n.p0))/(n.u)


三角形

渲染中的基本图元 包括点,线,还有三角形,建模中多面体大部分都是用三角形的拼接来表示。

首先来看一下空间三角形的表示。

假设用空间上a,b,c三点来表示一个三角形。


则三角平面内的任意一点可以表示为




法线向量表示为

将光线向量e + td带入,得到:

当B>0,r>0且B+r<1的时候,P在三角形内。
对方程进行变换并改写成矩阵的形式:




由克莱莫法则求得B,r,t



其中
 

则三角形和光线求交的伪代码可以表示为:

[cpp]   view plain  copy
  1. boolean raytri (ray r, vector3 a, vector3 b, vector3 c, interval [t0 , t1 ])  
  2. compute t  
  3. if (t < t0 ) or (t > t1 ) then  
  4.     return false  
  5. compute γ  
  6.     if (γ < 0) or (γ > 1) then  
  7. return false  
  8. compute β  
  9. if (β < 0) or (β > 1 − γ) then  
  10.     return false  
  11. return true  

平面多边形
已知条件是平面的m个顶点(p1...pm)还有平面的法向量n

平面内的向量和法向量时垂直的,即点乘结果为0,可以且接方程:

                         (p − p1 ) · = 0

其中p e + td,解得:

这样我们就可以 求得p点,如果p在平面内,则光线与平面相交,否则不相交。
但是如何知道p是否在平面内呢?
siggragh course上的解决方法是:从相交点往平面内的任意方向做一条射线,如果射线与多边形的边的交点数量为奇数则p在平面内,否则在平面外。
这个很好理解,用笔画一下 就知道了。

一个场景
 一个场景包含很多个图元,球体,正方体。。。
 回到光线追踪的原理,从摄像机发射一条射线之后,当场景中只有单个物体,只需计算一个,就可以得到一个result,当场景中又多个物体的时候,我们需要做的就是计算多次,取距离最近的那个result。
伪代码
[cpp]   view plain  copy
  1. hit = false  
  2. for each object o in the group do  
  3.     if (o is hit at ray parameter t and t ∈ [t0 , t1 ]) then  
  4.       hit = true  
  5.       hitobject = o  
  6.       t1 = t  
  7. return hit  



平面类的实现


平面类我们就可以用代码这样来描述:

[cpp]   view plain  copy
  1. <span style="font-size:14px;">#ifndef Plane_H  
  2. #define Plane_H  
  3. #include "gvector3.h"  
  4. #include "intersectresult.h"  
  5. #include "cray.h"  
  6. #include "checkermaterial.h"  
  7. #include "cobject.h"  
  8. class Plane:public CObject  
  9. {  
  10.     public:  
  11.         Plane();  
  12.         Plane(const GVector3& _normal,float _d);  
  13.         virtual ~Plane();  
  14.         virtual IntersectResult isIntersected(CRay& RAY);  
  15.     protected:  
  16.     private:  
  17.     //法向量  
  18.     GVector3 normal;  
  19.     //到原点的距离  
  20.     float d;  
  21. };  
  22.   
  23. #endif // Plane_H  
  24. </span>  

接下来是求光线到平面的距离。


代码实现如下:

[cpp]   view plain  copy
  1. <span style="font-size:14px;">#include "plane.h"  
  2.   
  3. Plane::Plane()  
  4. {  
  5.     //ctor  
  6. }  
  7.   
  8. Plane::~Plane()  
  9. {  
  10.     //dtor  
  11. }  
  12.   
  13. Plane::Plane(const GVector3& _normal,float _d)  
  14. {  
  15.     normal=_normal;  
  16.     d=_d;  
  17. }  
  18. IntersectResult Plane::isIntersected(CRay& ray)  
  19. {  
  20.         IntersectResult result = IntersectResult::noHit();  
  21.         float a = ray.getDirection().dotMul(this->normal);  
  22.         if (a <0)  
  23.         {  
  24.         result.isHit=1;  
  25.         result.object = this;  
  26.         float b = this->normal.dotMul(ray.getOrigin()-normal*d);  
  27.         result.distance = -b / a;  
  28.         result.position = ray.getPoint(result.distance);  
  29.         result.normal = this->normal;  
  30.         return result;  
  31.         }  
  32. }  
  33. </span>  

接下来我们在场景中创建一个平面,然后渲染深度,得到下面的效果:



材质

在真实世界中,白色物体在绿光照射下看起来是绿色而不是白色,红色物体在绿光照射下看起来是黑色,而有的同样颜色的物体在同样的光照下亮度却不同,这都是由物体的材质不同造成的。
首先在项目中添加一个颜色类,然后定义一些方法。

color.h

[cpp]   view plain  copy
  1. #ifndef COLOR_H  
  2. #define COLOR_H  
  3. #include <stdlib.h>  
  4. #include <stdio.h>  
  5. #include <cmath>  
  6. #include <iostream>  
  7. using namespace std;  
  8.   
  9. class Color  
  10. {  
  11.     public:  
  12.         float r;  
  13.         float g;  
  14.         float b;  
  15.         Color();  
  16.         Color(float _r,float _g,float _b);  
  17.         Color add(const Color& c);  
  18.         Color multiply(float s) const;  
  19.         Color modulate(const Color& c) const;  
  20.         void saturate();  
  21.         void show();  
  22.         virtual ~Color();  
  23.         static inline Color black(){ return Color(0,0,0); }  
  24.         static inline Color white(){ return Color(1,1,1); }  
  25.         static inline Color red()  { return Color(1,0,0); }  
  26.         static inline Color green(){ return Color(0,1,0); }  
  27.         static inline Color blue() { return Color(0,0,1); }  
  28.     protected:  
  29.     private:  
  30.   
  31. };  
  32.   
  33. #endif // COLOR_H  

color.cpp

[cpp]   view plain  copy
  1. #include "color.h"  
  2.   
  3. Color::Color()  
  4. {  
  5.     //ctor  
  6. }  
  7.   
  8. Color::~Color()  
  9. {  
  10.     //dtor  
  11. }  
  12. Color::Color(float _r,float _g,float _b)  
  13. {  
  14.     r=_r;g=_g;b=_b;  
  15. }  
  16. Color Color::add(const Color& c)  
  17. {  
  18.     return Color(r + c.r, g + c.g, b + c.b);  
  19. }  
  20. Color Color::multiply(float s) const  
  21. {  
  22.     return Color(r * s, g * s, b * s);  
  23. }  
  24. Color Color::modulate(const Color&c) const  
  25. {  
  26.      return Color(r * c.r, g * c.g, b * c.b);  
  27. }  
  28. void Color::saturate()  
  29. {  
  30.     r = r>1.0?1.0:r;  
  31.     g = g>1.0?1.0:g;  
  32.     b = b>1.0?1.0:b;  
  33. }  
  34. void Color::show()  
  35. {  
  36.     cout<<"r:"<<r<<"g:"<<g<<"b:"<<b<<endl;  
  37. }  

然后是定义一个材质的基类,后面要实现的各种材质都继承它:

material.h

[cpp]   view plain  copy
  1. #ifndef Material_H  
  2. #define Material_H  
  3. #include "gvector3.h"  
  4. #include "intersectresult.h"  
  5. #include "cray.h"  
  6. #include "color.h"  
  7. class Material  
  8. {  
  9.     public:  
  10.         Material();  
  11.         Material(float _reflectiveness);  
  12.         float getRef();  
  13.         void setRef(float _reflectiveness);  
  14.         virtual ~Material();  
  15.         virtual Color sample(const CRay& ray,const GVector3& position,const GVector3& normal);  
  16.     protected:  
  17.         float reflectiveness;  
  18.     private:  
  19.   
  20. };  
  21.   
  22. #endif // Material_H  

material.cpp

[cpp]   view plain  copy
  1. #include "material.h"  
  2.   
  3. Material::Material()  
  4. {  
  5.     //ctor  
  6. }  
  7.   
  8. Material::Material(float _reflectiveness)  
  9. {  
  10.     reflectiveness=_reflectiveness;  
  11. }  
  12. Material::~Material()  
  13. {  
  14.     //dtor  
  15. }  
  16. float Material::getRef()  
  17. {  
  18.     return reflectiveness;  
  19. }  
  20. void Material::setRef(float _reflectiveness)  
  21. {  
  22.     reflectiveness=_reflectiveness;  
  23. }  
  24. Color Material::sample(const CRay& ray,const GVector3& position,const GVector3& normal)  
  25. {  
  26.     cout<<"Base sample!"<<endl;  
  27. }  

实现两种材质,一种是棋盘材质,一种phong材质。

checkermaterial.h

[cpp]   view plain  copy
  1. #ifndef CHECKERMATERIAL_H  
  2. #define CHECKERMATERIAL_H  
  3. #include "material.h"  
  4. #include "color.h"  
  5. #include <stdlib.h>  
  6. class CheckerMaterial:public Material  
  7. {  
  8.     public:  
  9.         CheckerMaterial();  
  10.         CheckerMaterial(float _scale,float _reflectiveness=0);  
  11.         virtual ~CheckerMaterial();  
  12.         virtual Color sample(const CRay& ray,const GVector3& position,const GVector3& normal);  
  13.     protected:  
  14.     private:  
  15.     float scale;  
  16. };  
  17.   
  18. #endif // CHECKERMATERIAL_H  

checkermaterial.cpp

[cpp]   view plain  copy
  1. #include "checkermaterial.h"  
  2.   
  3. CheckerMaterial::CheckerMaterial()  
  4. {  
  5.     //ctor  
  6. }  
  7. CheckerMaterial::CheckerMaterial(float _scale,float _reflectiveness)  
  8. {  
  9.     scale=_scale;  
  10.     reflectiveness=_reflectiveness;  
  11. }  
  12. CheckerMaterial::~CheckerMaterial()  
  13. {  
  14.     //dtor  
  15. }  
  16. Color CheckerMaterial::sample(const CRay& ray,const GVector3& position,const GVector3& normal)  
  17. {  
  18.     float d=abs((floor(position.x * this->scale) + floor(position.z * this->scale)));  
  19.     d=fmod(d,2);  
  20.     return  d < 1 ? Color::black() : Color::white();  
  21. }  

phongmaterial.h

[cpp]   view plain  copy
  1. #ifndef PHONGMATERIAL_H  
  2. #define PHONGMATERIAL_H  
  3. #include"gvector3.h"  
  4. #include "color.h"  
  5. #include "cray.h"  
  6. #include "material.h"  
  7. // global temp  
  8. static GVector3 lightDir = GVector3(1, 1, 1).normalize();  
  9. static Color lightColor = Color::white();  
  10. class PhongMaterial:public Material  
  11. {  
  12.     public:  
  13.         PhongMaterial();  
  14.         PhongMaterial(const Color& _diffuse,const Color& _specular,const float& _shininess,float _reflectiveness=0);  
  15.         virtual Color sample(const CRay& ray,const GVector3& position,const GVector3& normal);  
  16.         virtual ~PhongMaterial();  
  17.     protected:  
  18.     private:  
  19.         Color   diffuse;  
  20.         Color   specular;  
  21.         float   shininess;  
  22. };  
  23.   
  24. #endif // PHONGMATERIAL_H  

phongmaterial.cpp

[cpp]   view plain  copy
  1. #include "phongmaterial.h"  
  2.   
  3. PhongMaterial::PhongMaterial()  
  4. {  
  5.     //ctor  
  6. }  
  7. PhongMaterial::PhongMaterial(const Color& _diffuse,const Color& _specular,const float& _shininess,float _reflectiveness)  
  8. {  
  9.     diffuse=_diffuse;  
  10.     specular=_specular;  
  11.     shininess=_shininess;  
  12.     reflectiveness=_reflectiveness;  
  13. }  
  14. PhongMaterial::~PhongMaterial()  
  15. {  
  16.     //dtor  
  17. }  
  18. Color PhongMaterial::sample(const CRay& ray,const GVector3& position,const GVector3& normal)  
  19. {  
  20.     float NdotL = normal.dotMul(lightDir);  
  21.     GVector3 H = (lightDir-ray.getDirection()).normalize();  
  22.     float NdotH = normal.dotMul(H);  
  23.     Color diffuseTerm = this->diffuse.multiply(std::max(NdotL, (float)0));  
  24.     Color specularTerm = this->specular.multiply(pow(std::max(NdotH, (float)0), this->shininess));  
  25.     return lightColor.modulate(diffuseTerm.add(specularTerm));  
  26. }  

试着来渲染一下。

[cpp]   view plain  copy
  1. void renderDepth()  
  2. {  
  3.     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
  4.     glLoadIdentity();                                   // Reset The View  
  5.     glTranslatef(-0.5f,-0.5f,-1.0f);  
  6.     glPointSize(2.0);  
  7.     PerspectiveCamera camera( GVector3(0, 10, 10),GVector3(0, -0.5, -1),GVector3(0, 1, 0), 90);  
  8.     Plane plane1(GVector3(0, 1, 0),1.0);  
  9.     CSphere sphere1(GVector3(0, 5, -10), 5.0);  
  10.     plane1.material=new CheckerMaterial(0.1f);  
  11.     sphere1.material=new PhongMaterial(Color::red(), Color::white(), 16);  
  12.     long maxDepth=20;  
  13.   
  14.     float dx=1.0f/WINDOW_WIDTH;  
  15.     float dy=1.0f/WINDOW_HEIGHT;  
  16.     float dD=255.0f/maxDepth;  
  17.     glBegin(GL_POINTS);  
  18.     for (long y = 0; y < WINDOW_HEIGHT; ++y)  
  19.     {  
  20.         float sy = 1 - dy*y;  
  21.         for (long x = 0; x < WINDOW_WIDTH; ++x)  
  22.         {  
  23.             float sx =dx*x;  
  24.             CRay ray(camera.generateRay(sx, sy));  
  25.             IntersectResult result = sphere1.isIntersected(ray);  
  26.             //IntersectResult result = plane1.isIntersected(ray);  
  27.             if (result.isHit)  
  28.             {  
  29.                 Color color = sphere1.material->sample(ray, result.position, result.normal);  
  30.                 //Color color =plane1.material->sample(ray, result.position, result.normal);  
  31.                 color.saturate();  
  32.                 //color.show();  
  33.                 glColor3ub(color.r*255,color.g*255,color.b*255);  
  34.                 glVertex2f(sx,sy);  
  35.             }  
  36.         }  
  37.     }  
  38.     glEnd();  
  39.     // 交换缓冲区  
  40.     glfwSwapBuffers();  
  41. }  

结果如下:





材质这一块有很多东西可以来探讨,而且它和光照联系的很紧密,这里先不探讨。

联合

之前的渲染测试我们都只渲染了单个的物体,现在我们需要在场景中显示多个物体,就上开篇的那副图一样。


创建一个union类,在渲染的时候将要渲染的东西都丟进去。

union.h

[cpp]   view plain  copy
  1. #ifndef UNION_H  
  2. #define UNION_H  
  3. #include "cobject.h"  
  4. #include <vector>  
  5. using namespace std;  
  6.   
  7. class Union:public CObject  
  8. {  
  9.     public:  
  10.         Union();  
  11.         virtual ~Union();  
  12.         void push(CObject* object);  
  13.         virtual IntersectResult isIntersected(CRay& _ray);  
  14.     protected:  
  15.     private:  
  16.     vector<CObject*> cobjects;  
  17. };  
  18.   
  19. #endif // UNION_H  

union.cpp

[cpp]   view plain  copy
  1. #include "union.h"  
  2.   
  3. Union::Union()  
  4. {  
  5.     //ctor  
  6. }  
  7.   
  8. Union::~Union()  
  9. {  
  10.     //dtor  
  11. }  
  12. void Union::push(CObject* object)  
  13.   
  14. {  
  15.     cobjects.push_back(object);  
  16. }  
  17. IntersectResult Union::isIntersected(CRay& _ray)  
  18. {  
  19.     const float Infinity=1e30;  
  20.     float minDistance = Infinity;  
  21.     IntersectResult minResult = IntersectResult::noHit();  
  22.     long size=this->cobjects.size();  
  23.     for (long i=0;i<size;++i){  
  24.         IntersectResult result = this->cobjects[i]->isIntersected(_ray);  
  25.         if (result.object && (result.distance < minDistance)) {  
  26.             minDistance = result.distance;  
  27.             minResult = result;  
  28.         }  
  29.     }  
  30.     return minResult;  
  31. }  

最后的 渲染代码
[cpp]   view plain  copy
  1. void renderUnion()  
  2. {  
  3.     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
  4.     glLoadIdentity();                                   // Reset The View  
  5.     glTranslatef(-0.5f,-0.5f,-1.0f);  
  6.     glPointSize(2.0);  
  7.     PerspectiveCamera camera( GVector3(0, 10, 10),GVector3(0, -0.5, -1),GVector3(0, 1, 0), 90);  
  8.     Plane* plane1=new Plane(GVector3(0, 1, 0),1.0);  
  9.     CSphere* sphere1=new CSphere(GVector3(-2, 5, -10), 5.0);  
  10.     CSphere* sphere2=new CSphere(GVector3(5, 5, -10), 3.0);  
  11.     plane1->material=new CheckerMaterial(0.1f);  
  12.     sphere1->material=new PhongMaterial(Color::red(), Color::white(), 16);  
  13.     sphere2->material=new PhongMaterial(Color::blue(), Color::white(), 16);  
  14.     Union scence;  
  15.     scence.push(plane1);  
  16.     scence.push(sphere1);  
  17.     scence.push(sphere2);  
  18.     long maxDepth=20;  
  19.   
  20.     float dx=1.0f/WINDOW_WIDTH;  
  21.     float dy=1.0f/WINDOW_HEIGHT;  
  22.     float dD=255.0f/maxDepth;  
  23.     glBegin(GL_POINTS);  
  24.     for (long y = 0; y < WINDOW_HEIGHT; ++y)  
  25.     {  
  26.         float sy = 1 - dy*y;  
  27.         for (long x = 0; x < WINDOW_WIDTH; ++x)  
  28.         {  
  29.             float sx =dx*x;  
  30.             CRay ray(camera.generateRay(sx, sy));  
  31.             IntersectResult result = scence.isIntersected(ray);  
  32.             //IntersectResult result = plane1.isIntersected(ray);  
  33.             if (result.isHit)  
  34.             {  
  35.                 Color color = result.object->material->sample(ray, result.position, result.normal);  
  36.                 //Color color =plane1.material->sample(ray, result.position, result.normal);  
  37.                 color.saturate();  
  38.                 //color.show();  
  39.                 glColor3ub(color.r*255,color.g*255,color.b*255);  
  40.                 glVertex2f(sx,sy);  
  41.             }  
  42.         }  
  43.     }  
  44.     glEnd();  
  45.     // 交换缓冲区  
  46.     glfwSwapBuffers();  
  47. }  

运行结果



最后我们来加一点反射的效果,在main.cpp中添加一个函数。

[cpp]   view plain  copy
  1. Color rayTraceRecursive(CObject* scene,CRay& ray,long maxReflect)   
  2. {  
  3.     IntersectResult result = scene->isIntersected(ray);  
  4.     if (result.object)  
  5.         {  
  6.             float reflectiveness = result.object->material->getRef();  
  7.             Color color = result.object->material->sample(ray, result.position, result.normal);  
  8.             color = color.multiply(1 - reflectiveness);  
  9.             if ((reflectiveness > 0) && (maxReflect > 0))  
  10.              {  
  11.                 GVector3 r = result.normal*(-2 * result.normal.dotMul(ray.getDirection()))+ray.getDirection();  
  12.                 CRay ray = CRay(result.position, r);  
  13.                 Color reflectedColor = rayTraceRecursive(scene, ray, maxReflect - 1);  
  14.                 color = color.add(reflectedColor.multiply(reflectiveness));  
  15.             }  
  16.                     return color;  
  17.         }else return Color::black();  
  18. }  

渲染一下

[cpp]   view plain  copy
  1. void renderRecursive()  
  2. {  
  3.     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
  4.     glLoadIdentity();                                   // Reset The View  
  5.     glTranslatef(-0.5f,-0.5f,-1.0f);  
  6.     glPointSize(2.0);  
  7.     PerspectiveCamera camera( GVector3(0, 10, 10),GVector3(0, -0.5, -1),GVector3(0, 1, 0), 90);  
  8.     Plane* plane1=new Plane(GVector3(0, 1, 0),1.0);  
  9.     CSphere* sphere1=new CSphere(GVector3(-2, 5, -2), 4.0);  
  10.     CSphere* sphere2=new CSphere(GVector3(5, 5, -7), 3.0);  
  11.     plane1->material=new CheckerMaterial(0.1f,0.5f);  
  12.     sphere1->material=new PhongMaterial(Color::red(), Color::white(), 16,0.25);  
  13.     sphere2->material=new PhongMaterial(Color::green(), Color::white(), 16,0.25);  
  14.     Union scene;  
  15.     scene.push(plane1);  
  16.     scene.push(sphere1);  
  17.     scene.push(sphere2);  
  18.     long maxDepth=20;  
  19.     long maxReflect=5;  
  20.     float dx=1.0f/WINDOW_WIDTH;  
  21.     float dy=1.0f/WINDOW_HEIGHT;  
  22.     float dD=255.0f/maxDepth;  
  23.     glBegin(GL_POINTS);  
  24.     for (long y = 0; y < WINDOW_HEIGHT; ++y)  
  25.     {  
  26.         float sy = 1 - dy*y;  
  27.         for (long x = 0; x < WINDOW_WIDTH; ++x)  
  28.         {  
  29.             float sx =dx*x;  
  30.             CRay ray(camera.generateRay(sx, sy));  
  31.             IntersectResult result = scene.isIntersected(ray);  
  32.             //IntersectResult result = plane1.isIntersected(ray);  
  33.             if (result.isHit)  
  34.             {  
  35.                 Color color = rayTraceRecursive(&scene, ray, maxReflect);  
  36.                 //Color color = result.object->material->sample(ray, result.position, result.normal);  
  37.                 //Color color =plane1.material->sample(ray, result.position, result.normal);  
  38.                 color.saturate();  
  39.                 //color.show();  
  40.                 glColor3ub(color.r*255,color.g*255,color.b*255);  
  41.                 glVertex2f(sx,sy);  
  42.             }  
  43.         }  
  44.     }  
  45.     glEnd();  
  46.     // 交换缓冲区  
  47.     glfwSwapBuffers();  
  48. }  

结果就是最上面的那幅图了。


结语

以前自己只玩过一些opengl的东西,不过那些都有现成的接口让你掉,原理上也不用理解得很深,往往一两句语句就可以实现一个简单的效果。

而现在,从原理到实现,每一句代码都需要先在真实世界中想清楚,然后抽象成代码,不管对编程技巧还是数学功底都会有很高的要求,所以在编写这些代码的时候我又回头去看C++ primer,学线性代数。。当然收获也很大。

源码可以点这里下载。

参考

用JavaScript玩转计算机图形学(一)光线追踪入门-http://www.cnblogs.com/miloyip/archive/2010/03/29/1698953.html
光线追踪技术的理论和实践(面向对象)-http://blog.csdn.net/zhangci226/article/details/5664313
Wikipedia, Ray Tracing
计算机图形学(第三版)(美)赫恩 著,(美)巴克 著。
Ray Object Intersections -  http://siggraph.org/education/materials/HyperGraph/raytrace/rtinter0.htm












  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值