ray tracing in one weekend 个人记录

1. ppm图片格式

图片格式的一种

  • PBM是位图(bitmap)仅有黑与白,没有灰
  • PGM是灰度图(grayscale)
  • PPM是通过RGB三种颜色显现的图像(pixmaps)

示例:

P3
200 100
255
0 253 51
1 253 51
…………
100 253 31

写ppm文件:

// main()
//像素的颜色基于图像坐标
	int nx = 200, ny = 100;
	std::ofstream fout("test1.ppm");
	fout << "P3" << endl << nx << " " << ny << endl << 255 << endl; //P is capital
	for (int j = ny - 1; j >= 0; j--)
	{
		for (int i = 0; i < nx; i++)
		{
			vec3 rgb(float(i) / float(nx), float(j) / float(ny), 0.2);
			fout << int(255.99*rgb[0]) << " " << int(255.99*rgb[1]) << " " << int(255.99*rgb[2]) << endl;
		}
	}
	fout.close();

在这里插入图片描述

2. 光线 ray

光线追踪的基础就是光线 ray = origin+t*direction

class Ray
{
public:
	Ray(){}
	Ray(const vec3& o, const vec3& d):o(o), d(d){}
	vec3 origin() const { return o; }
	vec3 direction() const { return d; }
	vec3 point_at_t(float t) { return o + t * d; }
private:
	vec3 o, d;
};

应用:
建立坐标系:z=-1是图像平面
在这里插入图片描述

// main()
//像素的颜色基于ray
	int nx = 200, ny = 100;
	vec3 left_lower_corner(-2, -1, -1), up(0, 2, 0), right(4, 0, 0), origin(0, 0, 0);
	std::ofstream fout("test2.ppm");
	fout << "P3" << endl << nx << " " << ny << endl << 255 << endl; //P is capital
	for (int j = ny - 1; j >= 0; j--)
	{
		for (int i = 0; i < nx; i++)
		{
			float u = float(i) / float(nx), v = float(j) / float(ny);
			Ray r(origin, left_lower_corner + u * right + v * up);
			vec3 rgb = color(r);
			fout << int(255.99*rgb[0]) << " " << int(255.99*rgb[1]) << " " << int(255.99*rgb[2]) << endl;
		}
	}
	fout.close();

设置颜色(背景),此处随意设置t为[0,1]区间范围内的值,然后从两个颜色中插值得到最后的颜色,blended_value = (1-t)start_value + tend_value:

vec3 color(const Ray& r)
{
	vec3 unit_direction = normalize(r.direction());
	float t = 0.5*(unit_direction[1] + 1.0);
	return vec3(1.0 - t, 1.0 - t, 1.0 - t) + vec3(t*0.5, t*0.7, t*1.0);
}

在这里插入图片描述

3. 一个球

是否与球相交,即为下式(一元二次方程)是否存在解:
( x − c x ) ∗ ( x − c x ) + ( y − c y ) ∗ ( y − c y ) + ( z − c z ) ∗ ( z − c z ) = R ∗ R (x-cx)*(x-cx) + (y-cy)*(y-cy) + (z-cz)*(z-cz) = R*R (xcx)(xcx)+(ycy)(ycy)+(zcz)(zcz)=RR

= &gt; d o t ( ( p − C ) ( p − C ) ) = ( x − c x ) ∗ ( x − c x ) + ( y − c y ) ∗ ( y − c y ) + ( z − c z ) ∗ ( z − c z ) =&gt;dot((p-C)(p-C)) = (x-cx)*(x-cx) + (y-cy)*(y-cy) + (z-cz)*(z-cz) =>dot((pC)(pC))=(xcx)(xcx)+(ycy)(ycy)+(zcz)(zcz)

&lt; = = &gt; d o t ( ( A + t ∗ B − C ) , ( A + t ∗ B − C ) ) = R ∗ R &lt;==&gt;dot((A + t*B - C),(A + t*B - C)) = R*R <==>dot((A+tBC),(A+tBC))=RR

= &gt; t ∗ t ∗ d o t ( B , B ) + 2 ∗ t ∗ d o t ( A − C , A − C ) + d o t ( C , C ) − R ∗ R = 0 =&gt;t*t*dot(B,B) + 2*t*dot(A-C,A-C) + dot(C,C) - R*R = 0 =>ttdot(B,B)+2tdot(AC,AC)+dot(C,C)RR=0
t为ray的参数,A为ray的origin点,B为ray的direction,C为球心坐标,R为半径

bool hit_sphere(const vec3& center, float radius, const Ray& r)
{
	/// A:r.origin() B:r.direction() C:center
	/// t*t*dot(B, B) + 2 * t*dot(B, A - C) + dot(A - C, A - C) - R * R = 0;
	vec3 CA = r.origin() - center;
	float a = dot(r.direction(), r.direction()),
		b = 2 * dot(r.direction(), CA),
		c = dot(CA, CA) - radius * radius;
	return b * b - 4 * a*c > 0;
}

vec3 color(const Ray& r)
{
	// the color for hit
	if (hit_sphere(vec3(0, 0, 1), 0.5, r))
		return vec3(1, 0, 0);
	// background
	vec3 unit_direction = normalize(r.direction());
	float t = 0.5*(unit_direction[1] + 1.0);
	return vec3(1.0 - t, 1.0 - t, 1.0 - t) + vec3(t*0.5, t*0.7, t*1.0);
}

比上图多了球

4. 法向量

法向量是从圆心指向切点
在这里插入图片描述

float hit_sphere(const vec3& center, float radius, const Ray& r)
{
	/// A:r.origin() B:r.direction() C:center
	/// t*t*dot(B, B) + 2 * t*dot(B, A - C) + dot(A - C, A - C) - R * R = 0;
	vec3 CA = r.origin() - center;
	float a = dot(r.direction(), r.direction()),
		b = 2 * dot(r.direction(), CA),
		c = dot(CA, CA) - radius * radius,
		discriminant = b * b - 4 * a*c;
	if (discriminant < 0)
		return -1;
	else
		return (-b - sqrt(discriminant)) / (2.0*a);
}

vec3 color(const Ray& r)
{
	vec3 center(0, 0, -1);
	// the color for hit
	float t = hit_sphere(center, 0.5, r);
	if (t > 0)
	{
		vec3 N = normalize(r.point_at_t(t) - center);  //[-1,1]
		return vec3(0.5)*(N + vec3(1));
	}
	// background
	vec3 unit_direction = normalize(r.direction());
	t = 0.5*(unit_direction[1] + 1.0);
	return vec3(1.0 - t, 1.0 - t, 1.0 - t) + vec3(t*0.5, t*0.7, t*1.0);
}

在这里插入图片描述

5. 多个球

Hitable.h

#pragma once
#include <list>
#include "Ray.h"
using std::list;

struct hit_record
{
	float t;
	vec3 p, normal;
};

// object
class Hitable
{
public:
	virtual bool hit(const Ray& r, float t_min, float t_max, hit_record& rec) const = 0;
};

class Sphere : public Hitable
{
public:
	Sphere() {}
	Sphere(vec3 center, float radius):center(center), radius(radius){}
	virtual bool hit(const Ray& r, float t_min, float t_max, hit_record& rec) const;
private:
	vec3 center;
	float radius;
};

//object list
class Hitable_list : public Hitable
{
public:
	Hitable_list() {}
	Hitable_list(list<Hitable*>& l) :l(l) {}
	virtual bool hit(const Ray& r, float t_min, float t_max, hit_record& rec) const;

private:
	list<Hitable*> l;
};

Hitable.cpp

  • 球的相交求根判断(-b - sqrt(discriminant)) / (2.0*a) 约去2
  • 多个球计算最近相交点的法向量,作为当前像素颜色
#include "Hitable.h"

bool Sphere::hit(const Ray& r, float t_min, float t_max, hit_record& rec) const
{
	/// A:r.origin() B:r.direction() C:center
	/// t*t*dot(B, B) + 2 * t*dot(B, A - C) + dot(A - C, A - C) - R * R = 0;
	vec3 CA = r.origin() - center;
	float a = dot(r.direction(), r.direction()),
		b = dot(r.direction(), CA),
		c = dot(CA, CA) - radius * radius,
		discriminant = b * b - a*c;
	if (discriminant < 0)
	{
		return false;
	}
	else
	{
		float solution = (-b - sqrt(discriminant)) / a;
		if (solution > t_min && solution < t_max)
		{
			rec.t = solution;
			rec.p = r.point_at_t(solution);
			rec.normal = (rec.p - center) / radius;
			return true;
		}
		solution = (-b + sqrt(discriminant)) / a;
		if (solution > t_min && solution < t_max)
		{
			rec.t = solution;
			rec.p = r.point_at_t(solution);
			rec.normal = (rec.p - center) / radius;
			return true;
		}
	}
}

bool Hitable_list::hit(const Ray& r, float t_min, float t_max, hit_record& rec) const
{
	hit_record tmp_record;
	bool hit_anything = false;
	double closet_t_so_far = t_max;
	for (auto object : l)
	{
		if (object->hit(r, t_min, closet_t_so_far, tmp_record))
		{
			hit_anything = true;
			closet_t_so_far = tmp_record.t;
			rec = tmp_record;
		}
	}
	return hit_anything;
}

color()函数

vec3 color(const Ray& r, Hitable_list& object_list)
{
	// the color for hit
	hit_record rec;
	if (object_list.hit(r, 0, INT_MAX, rec))
	{
		return vec3(0.5)*(rec.normal + vec3(1));
	}
	// background
	vec3 unit_direction = normalize(r.direction());
	float t = 0.5*(unit_direction[1] + 1.0);
	return vec3(1.0 - t, 1.0 - t, 1.0 - t) + vec3(t*0.5, t*0.7, t*1.0);
}

main中

	//add
	list<Hitable*> list;
	list.push_back(new Sphere(vec3(-1, -1, -1), 1));
	list.push_back(new Sphere(vec3(0, 0, -1), 0.5));
	Hitable_list object_list(list);
	
	//change
	vec3 rgb = color(r, object_list);

在这里插入图片描述

6. 反走样

  • 添加相机
// camera
class Camera
{
public:
	Camera()
	{
		left_lower_corner = vec3(-2, -1, -1);
		up = vec3(0, 2, 0);
		right = vec3(4, 0, 0);
		origin = vec3(0, 0, 0);
	}
	Camera(vec3& left_lower_corner, vec3& up, vec3& right, vec3& origin):
		left_lower_corner(left_lower_corner), up(up), right(right), origin(origin)
	{}
	Ray get_ray(float u, float v){return Ray(origin, left_lower_corner + u * right + v * up - origin);}

private:
	vec3 left_lower_corner, up, right, origin;
};
  • 每个像素点内采样,取normal平均值作为颜色
#define random_float_0_1() rand()/double(RAND_MAX)

//change in main -- in for
			vec3 rgb(0);
			for (int s = 0; s < ns; s++)
			{
				float u = (float(i) + random_float_0_1()) / float(nx),
					v = (float(j) + random_float_0_1()) / float(ny);
				rgb += color(cam.get_ray(u, v), object_list);
			}
			rgb /= ns;

在这里插入图片描述 在这里插入图片描述

7. 漫反射

漫反射不发光,吸收周围的光,吸收的越多表面越暗;而漫反射的反射方向是任意的。
在这里插入图片描述

  • 出射光线 PS:ray和object的交点P,normal为N,以(P+N)为圆心做单位元,在此单位圆内随机采样漫反射方向
  • 递归求交,不断漫反射,直至不再与物体相交
vec3 random_in_unit_sphere()
{
	vec3 p;
	do {
		p = vec3(2*random_float_0_1(), 2*random_float_0_1(), 2*random_float_0_1()) - vec3(1);
	} while (length(p) >= 1.0);
	return p;
}
vec3 color(const Ray& r, Hitable_list& object_list)
{
	// the color for hit
	hit_record rec;
	if (object_list.hit(r, 0, INT_MAX, rec))
	{
		//return vec3(0.5)*(rec.normal + vec3(1));
		vec3 s = rec.p + rec.normal + random_in_unit_sphere();
		return color(Ray(rec.p, s-rec.p), object_list)*vec3(0.5);
	}
	// background
	vec3 unit_direction = normalize(r.direction());
	float t = 0.5*(unit_direction[1] + 1.0);
	return vec3(1.0 - t, 1.0 - t, 1.0 - t) + vec3(t*0.5, t*0.7, t*1.0);
}

在这里插入图片描述
颜色变浅,表明吸收的光线变少,结果边界处阴影明显

rgb = sqrt(rgb);

在这里插入图片描述
因为精度问题,需要修改t_min为0.001

//if (object_list.hit(r, 0, INT_MAX, rec)) =>
if (object_list.hit(r, 0.001, INT_MAX, rec))

8. 镜面反射

统一描述物质的材质:

  • 产生散射(反射、透射)的出射光线
  • 散射后的光强衰弱程度
class Material
{
public:
	virtual bool scatter(const Ray& r_in, const hit_record& rec, 
		vec3& attenuation, Ray& scattered) const = 0; // attenuation:less scattered:direction
};

Lambertian

class Lambertian :public Material
{
public:
	Lambertian(const vec3& a):albedo(a){}
	virtual bool Lambertian::scatter(const Ray& r_in, const hit_record& rec, vec3& attenuation, Ray& scattered) const
{
	vec3 s = rec.p + rec.normal + random_in_unit_sphere();
	attenuation = albedo;
	scattered = Ray(rec.p, s - rec.p);
	return true;
}
private:
	vec3 albedo;
};

镜面反射

  • 反射方向 = V+2B, B = -N.dot(V)
  • 发生反射的条件为出射方向和normal之间的夹角小于90°
    在这里插入图片描述
vec3 reflect(const vec3& v, const vec3& n)
{
	return v - 2 * dot(v, n)*n;
}

class Metal :public Material
{
public:
	Metal(const vec3& a) :albedo(a) {}
	virtual bool scatter(const Ray& r_in, const hit_record& rec, vec3& attenuation, Ray& scattered) const;
private:
	vec3 albedo;
};

bool Metal::scatter(const Ray& r_in, const hit_record& rec, vec3& attenuation, Ray& scattered) const
{
	vec3 ref = reflect(normalize(r_in.direction()), rec.normal);
	attenuation = albedo;
	scattered = Ray(rec.p, ref);
	return (dot(scattered.direction(), rec.normal) >0);
}

结构修改

hit_record中多了Material的指针,记录交点的材质以便计算颜色 rec.material_ptr->scatter(r, rec, attenuation, scattered)

class Material;
struct hit_record
{
	float t;
	vec3 p, normal;
	Material* material_ptr;
};

in Sphere::hit

rec.material_ptr = material;

color()函数修改:

vec3 color(const Ray& r, Hitable_list& object_list, const int depth)
{
	// the color for hit
	hit_record rec;
	if (object_list.hit(r, 0.001, INT_MAX, rec))
	{
		Ray scattered;
		vec3 attenuation;
		if (depth < 50 && rec.material_ptr->scatter(r, rec, attenuation, scattered))
		{
			return attenuation * color(scattered, object_list, depth+1);
		}
		else
			return vec3(0);
	}
	// background
	vec3 unit_direction = normalize(r.direction());
	float t = 0.5*(unit_direction[1] + 1.0);
	return vec3(1.0 - t, 1.0 - t, 1.0 - t) + vec3(t*0.5, t*0.7, t*1.0);
}

main

	// change 1
	list<Hitable*> list;
	list.push_back(new Sphere(vec3(-1, -1, -1), 1, new Lambertian(vec3(0.8,0.3,0.3))));
	list.push_back(new Sphere(vec3(0, 0, -1), 0.5, new Metal(vec3(0.0, 0.0, 0.8))));
	Hitable_list object_list(list);

	// change 2
	rgb += color(cam.get_ray(u, v), object_list, 0);

在这里插入图片描述

模糊

	scattered = Ray(rec.p, ref + fuzzier * random_in_unit_sphere());

在这里插入图片描述 在这里插入图片描述

9. 折射

透明材料如水、玻璃和钻石都是介质。当一束光线击中它们时,就会分裂成反射光线和折射(透射)光线。我们将通过在反射和折射之间随机选择来处理这个问题,并且每个交互作用只产生一个散射光线。
在这里插入图片描述
c o s θ 1 = − N ⋅ L cosθ_1=-N·L cosθ1=NL
n 1 ⋅ s i n θ 1 = n 2 ⋅ s i n θ 2 n_1·sinθ_1 = n_2·sinθ_2 n1sinθ1=n2sinθ2
η = n 1 / n 1 = s i n θ 2 / s i n θ 1 η=n_1/n_1=sinθ_2/sinθ_1 η=n1/n1=sinθ2/sinθ1
c o s θ 2 = s q r t ( 1 − s i n 2 θ 2 ) = s q r t ( 1 − ( 1 / η ⋅ s i n θ 1 ) 2 ) = s q r t ( 1 − ( 1 / η 2 ) ( 1 − c o s 2 θ 1 ) ) cosθ_2=sqrt(1-sin^2θ_2) = sqrt(1-(1/η·sinθ_1)^2)=sqrt(1-(1/η^2)(1-cos^2θ_1)) cosθ2=sqrt(1sin2θ2)=sqrt(1(1/ηsinθ1)2)=sqrt(1(1/η2)(1cos2θ1))

将L和T分解为两个向量, l 1 l_1 l1 t 1 t_1 t1的方向是相同的, ∣ l 1 ∣ = ∣ L ∣ s i n θ 1 = s i n θ 1 |l_1|=|L|sinθ_1=sinθ_1 l1=Lsinθ1=sinθ1 ∣ t 1 ∣ = ∣ T ∣ s i n θ 2 = s i n θ 2 |t_1|=|T|sinθ_2=sinθ_2 t1=Tsinθ2=sinθ2,则 t 1 = ( s i n θ 2 / s i n θ 1 ) ∗ l 1 = ( 1 / η ) ∗ l 1 t_1=(sinθ_2/sinθ_1)*l_1=(1/η)*l_1 t1=(sinθ2/sinθ1)l1=(1/η)l1

∵ ∣ l 2 ∣ = ∣ L ∣ c o s θ 1 = c o s θ 1 ∵|l_2|=|L|cosθ_1=cosθ_1 l2=Lcosθ1=cosθ1
=> l 2 = − N c o s θ 1 l_2=-Ncosθ_1 l2=Ncosθ1
=> l 1 = L − l 1 = L + N c o s θ 1 l_1=L-l_1=L+Ncosθ_1 l1=Ll1=L+Ncosθ1

∴ t 1 = ( 1 / η ) ∗ ( L + N c o s θ 1 ) ∴t_1=(1/η)*(L+Ncosθ_1) t1=(1/η)(L+Ncosθ1)

∣ t 1 ∣ 2 + ∣ t 2 ∣ 2 = ∣ T ∣ 2 = 1 |t_1|^2+|t_2|^2=|T|^2=1 t12+t22=T2=1
又∵同理 t 2 = − N ∗ t 2 = − s q r t ( 1 − ∣ t 1 ∣ 2 ) ∗ N t_2=-N*t_2=-sqrt(1-|t_1|^2)*N t2=Nt2=sqrt(1t12)N
∴ t 2 = − c o s θ 2 ∗ N ∴t_2 = -cosθ_2*N t2=cosθ2N

∴ T = t 1 + t 2 = ( 1 / η ) ( L + N ⋅ c o s θ 1 ) − N ⋅ c o s θ 2 ∴T=t_1+t_2=(1/η)(L+N·cosθ_1)-N·cosθ_2 T=t1+t2=(1/η)(L+Ncosθ1)Ncosθ2

[注] θ 2 &lt; 90 ° θ_2&lt;90° θ2<90°时才有折射角

bool refract(const vec3& v, const vec3& n, float ni_over_nt, vec3& refracted)
{
	//n sin(theta) = n’ sin(theta’)
	vec3 L = normalize(v);
	float cos1 = dot(-L, n);
	float discriminant = 1.0 - ni_over_nt * ni_over_nt*(1 - cos1 * cos1);
	if (discriminant > 0) {
		refracted = ni_over_nt * (L + n * cos1) - n * sqrt(discriminant);
		return true;
	}
	else
		return false;
}
//发生折射的概率,schlick:近似地计算出不同入射角旳菲涅耳反射比
float schlick(float cosine, float ref_idx)
{
	float r0 = (1 - ref_idx) / (1 + ref_idx);
	r0 = r0 * r0;
	return r0 + (1 - r0)*pow((1 - cosine), 5);
}

bool Dielectric::scatter(const Ray& r_in, const hit_record& rec, vec3& attenuation, Ray& scattered) const {
	// reflection
	vec3 reflected = reflect(r_in.direction(), rec.normal);

	// refraction
	vec3 outward_normal;
	float ni_over_nt;
	attenuation = vec3(1.0, 1.0, 1.0);
	vec3 refracted;
	float reflect_prob;
	float cosine;
	if (dot(r_in.direction(), rec.normal) > 0) {
		outward_normal = -rec.normal;
		ni_over_nt = ref_idx;
		//cosine = ref_idx * dot(r_in.direction(), rec.normal) / r_in.direction().length();
		cosine = dot(r_in.direction(), rec.normal) / r_in.direction().length();
		cosine = sqrt(1 - ref_idx * ref_idx*(1 - cosine * cosine));
	}
	else {
		outward_normal = rec.normal;
		ni_over_nt = 1.0 / ref_idx;
		cosine = -dot(r_in.direction(), rec.normal) / r_in.direction().length();
	}

	// 
	if (refract(r_in.direction(), outward_normal, ni_over_nt, refracted))
		reflect_prob = schlick(cosine, ref_idx);
	else
		reflect_prob = 1.0;

	// choose
	if (random_float_0_1() < reflect_prob)
		scattered = Ray(rec.p, reflected);
	else
		scattered = Ray(rec.p, refracted);
	return true;
}

在这里插入图片描述 在这里插入图片描述
半径为负,则可以是normal指向球内,有泡泡的感觉:

	list.push_back(new Sphere(vec3(-1, 0, -1), -0.45, new Dielectric(1.5)));

在这里插入图片描述

10. 相机位置

z=-1为成像平面,fov以y方向为准。
在这里插入图片描述

Camera::Camera(vec3& lookfrom, vec3& lookat, vec3& vup, float vfov, float aspect_ratio)
	:origin(lookfrom)
{
	float half_height = tan(vfov * PI / 360),
		half_width = aspect_ratio * half_height;
	vec3 w(normalize(lookfrom - lookat)),
		u(normalize(cross(vup, w))),
		v(cross(w, u));
	left_lower_corner = lookfrom - half_width * u - half_height * v - w;
	up = 2 * half_height*v;
	right = 2 * half_width*u;
}

在这里插入图片描述 在这里插入图片描述

11. 虚化

Defocus Blur 散焦模糊,即虚化。利用光圈和焦距实现。
引入aperture(光圈),focus_dist(焦距) 2个参数,来实现画面的虚化效果。以下为原书代码:

class camera
{
    vec3 origin;
    vec3 u,v,w;
    vec3 horizontal;
    vec3 vertical;
    vec3 lower_left_corner;
    float len_radius;

public :
    camera(vec3 lookfrom, vec3 lookat, vec3 vup, float vfov, float aspect, float aperture, float focus_dist)
    {
        len_radius = aperture/2;
        float theta = vfov*M_PI/180;
        float half_height = tan(theta/2);
        float half_width = aspect * half_height;
        origin = lookfrom;

        w = unit_vector(lookfrom - lookat);
        u = unit_vector(cross(vup, w));
        v = cross(w,u);

        lower_left_corner = origin - half_width*focus_dist*u - half_height*focus_dist*v - focus_dist*w;
        horizontal = 2*half_width*focus_dist*u;
        vertical = 2*half_height*focus_dist*v;
    }

    ray get_ray(float s,float t)
    {
        vec3 rd = len_radius * random_in_unit_disk();
        vec3 offset = u * rd.x() +v*rd.y();
        return ray(origin + offset,lower_left_corner+s*horizontal + t*vertical - origin - offset);
    }

    vec3 random_in_unit_disk()
    {
        vec3 p;
        do{
            p = 2.0*vec3(drand48(),drand48(),0)-vec3(1,1,0);
        }while (dot(p,p)>=1.0);
        return p;
    }
};

最终效果

vec3 color(const Ray& r, Hitable_list& object_list, const int depth)
{
	// the color for hit
	hit_record rec;
	if (object_list.hit(r, 0.001, INT_MAX, rec))
	{
		Ray scattered;
		vec3 attenuation;
		if (depth < 50 && rec.material_ptr->scatter(r, rec, attenuation, scattered))
		{
			return attenuation * color(scattered, object_list, depth+1);
		}
		else
			return vec3(0);
	}
	// background
	vec3 unit_direction = normalize(r.direction());
	float t = 0.5*(unit_direction[1] + 1.0);
	return vec3(1.0 - t, 1.0 - t, 1.0 - t) + vec3(t*0.5, t*0.7, t*1.0);
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值