DX11 游戏开发笔记 (五) 假灯光特效 及多个聚光灯的实现

7 篇文章 2 订阅
5 篇文章 0 订阅

上文我们小小的练习了下点光源的实现,理解phong式光照模型,本文自然就是讲解聚光灯的,并实现多个聚光灯,给它们加点控制。

 

聚光灯与点光源有所不同,其有一个聚束范围,一个方向,在范围外的我们认为不受聚光灯的影响,其粗略的模型公式就是计算光线与光源设定的中心方向的夹角,当其超出一个范围值,我们便将其的漫反射颜色和镜面反射颜色设为0;

 

超级简单的,利用单位向量d和-l的点乘就能计算出夹角的余弦值,设置好余弦值就可以控制角度。

那么同上文,我们要给出聚光灯的结构体,去描述聚光灯的行为属性,你可以理解为我们将数据抽象为聚光灯,亦可理解为我们将聚光灯的基本构造实例化为数据。

HLSL:

struct SpotlLight
{
	float4   Ambient;
	float4   Diffuse;
	float4   Specular;

	float3   Position;
	float      Range;

	float3   Direction;
	float      spot;

	float3   Att;//attenuation
	float      Pad;

};

对应的C++结构体:

	struct  Light_desc
	{
		Light_desc(){ ZeroMemory(this, sizeof(this)); }

		XMFLOAT4   Ambient;
		XMFLOAT4   Diffuse;
		XMFLOAT4   Specular;

		XMFLOAT3   Position;
		float      Range;

		XMFLOAT3   Direction;
		float      spot;

		XMFLOAT3   Att;//attenuation
		float      Pad;

	};

其中spot是用来控制聚光灯的圆锥体区域大小,默认大于1,其实就是 -L 和 d 的夹角余弦值的幂指数s。

当spot越大,余弦值的变化也就越明显,自然聚光灯的高亮范围就越小,但是整体亮度却变低了,不符合我们把聚光灯放近一点的特性,故我们在原龙书的基础上加了点改变,

color += color*saturate(Ambient_Color + Diffuse_Color + Specular_Color)* g_SpotLight[i].Spot/2;

我们给最终颜色乘上一个spot的正比例,来抵消余弦次幂带来的不切实际的变化,达到增加亮度的效果,下面为效果图:

 

spot为3

 

 

spot为8

 

 

 

 

Number One :单身party     聚光灯 singledog

 

 

 

ok,看完成果,我们就要探究过程了,首先是单个聚光灯从配置到实现:

void Light::Init_Light()
{
	Light_Desc.Ambient = XMFLOAT4(0.6f, 0.6f, 0.6f, 1.0f);
	Light_Desc.Diffuse = XMFLOAT4(0.6f, 0.6f, 0.6f, 1.0f);
	Light_Desc.Specular = XMFLOAT4(0.6f, 0.6f, 0.8f, 1.0f);
	Light_Desc.Position = XMFLOAT3(-2.0f, 0.4f, 0.4f);
	Light_Desc.Range = float(20.f);
	Light_Desc.Direction = XMFLOAT3(-0.0f, -1.0f, -1.0f);
	Light_Desc.spot = float(4.0f);
	Light_Desc.Att = XMFLOAT3(0.01f, 0.01f, 0.01f);
	Light_Desc.Pad = 0;
}

非常简单,我在Light里面就可以初始化数据了,在这里初始化并创建我们这个灯光实例不对的,但是就一个灯光,我管它呢,我懒得去其它地方赋值,

配置shader文件:

void  Light::Deploy_Shader(ID3DX11Effect* effect)
{

	m_fxLight = effect->GetVariableBySemantic("SPOTLIGHT");

}

在循环中传递数据:

void Light::Frame_Light()
{
	m_vCameraPosition = D3DXVECTOR3(Light_Desc.Position.x, Light_Desc.Position.y, Light_Desc.Position.z);
	m_vLookVector = D3DXVECTOR3(Light_Desc.Direction.x, Light_Desc.Direction.y, Light_Desc.Direction.z);

	D3DXVec3Normalize(&m_vLookVector, &m_vLookVector);

	//正交并规范化m_vRightVector
	D3DXVec3Cross(&m_vRightVector, &m_vUpVector, &m_vLookVector);
	D3DXVec3Normalize(&m_vRightVector, &m_vRightVector);

	//正交并规范化m_vUpVector
	D3DXVec3Cross(&m_vUpVector, &m_vLookVector, &m_vRightVector);

	m_fxLight->SetRawValue(&Light_Desc, 0, sizeof(Light_Desc));
}

注意,我在Frame中给light添加了一个控制向量正交化,

其实就是把camera的代码复制过来而已:

void    Light::Rotate_RightVector(float fAngle)
{
	D3DXMATRIX R;
	//D3DXMatrixRotationX(&R, fAngle);
	D3DXMatrixRotationAxis(&R, &m_vRightVector, fAngle);//创建出绕m_vRightVector旋转fAngle个角度的R矩阵
	D3DXVec3TransformCoord(&m_vUpVector, &m_vUpVector, &R);//让m_vUpVector向量绕m_vRightVector旋转fAngle个角度
	D3DXVec3TransformCoord(&m_vLookVector, &m_vLookVector, &R);//让m_vLookVector向量绕m_vRightVector旋转fAngle个角度
	Light_Desc.Direction = XMFLOAT3(m_vLookVector.x, m_vLookVector.y, m_vLookVector.z);
}

void     Light::Rotate_UpVector(float fAngle)
{
	D3DXMATRIX R;
	D3DXMatrixRotationAxis(&R, &m_vUpVector, fAngle);//创建出绕m_vUpVector旋转fAngle个角度的R矩阵
	//D3DXMatrixRotationY(&R, fAngle);
	D3DXVec3TransformCoord(&m_vRightVector, &m_vRightVector, &R);//让m_vRightVector向量绕m_vUpVector旋转fAngle个角度
	D3DXVec3TransformCoord(&m_vLookVector, &m_vLookVector, &R);//让m_vLookVector向量绕m_vUpVector旋转fAngle个角度
	Light_Desc.Direction = XMFLOAT3(m_vLookVector.x, m_vLookVector.y, m_vLookVector.z);
}

本着嫌少不嫌多的原则,我也把其控制代码贴出来,具体可以参见源码的Light.cpp;

 

 

到这里,我们light的C++文件就写好了,只要在合适的地方调用它:

初始化:

BOOL graphicsclass::Init_Graphics()
{

	m_light = new Light;

	m_light->Init_Light();

	m_light->Deploy_Shader(m_shader->GetIeffect());

}

 

循环渲染: 

BOOL  graphicsclass::Frame_Graphics()
{

	m_light->Frame_Light();

}

 

 控制:

	Light*  light = graphics_class->Ilight();

	if (input_class->IsRightMouseButtonDown(0))
	{
		light->Rotate_UpVector(input_class->MouseDX()*-0.003f);
		light->Rotate_RightVector(input_class->MouseDY()*-0.006f);

	}

 

 清除:

void   graphicsclass::Clean_Graphics()
{

	m_light->Clean_Light();

	delete m_light;        m_light = NULL;
}

至此,我们C++基本构造就已经完成。接下来就是简单的HLSL配置;

跟点光源区别不大,加点东西就行。

 

影子量:常量buff

#include "Lighthead.fx"

Texture2D ShaderTexture;  //纹理资源

cbuffer  cbPerFrame
{
	SpotlLight       g_SpotLight:SPOTLIGHT;
}


cbuffer  cbPerObject
{
	float4x4	g_worldViewProj : WORLDVIEWPROJECTION;
	float4x4	g_world : WORLD;
	float4x4	g_worldInvTranspose : WORLDINVTRANSPOSE;
}

 

 顶点:

struct  VertexIn
{
	float3 Pos:POSITION;
	float3 Normal:NORMAL;
	float2 Tex:TEXCOORD0;
};

struct  VertexOut
{
	float4 PosH          :SV_POSITION;
	float3 PosW          :POSITION;
	float3 Normal        :NORMAL;
	float2 Tex           :TEXCOORD0;
	float4 DiffuseColor  : TEXCOORD1;
	float4 SpecularColor : TEXCOORD2;
	float3 lightVec      :TEXCOORD3;

};

这里我将一些数据放在VertexOut下的纹理数据组里面,并没有什么特殊意义,就是想用用看,其实跟你定义个临时变量差不多,看您随意,以后我们会学习到纹理数组更多的用途,现在把它看为容器就行。

顶点变化函数

VertexOut VS(VertexIn In )
{
	VertexOut  Out = (VertexOut)0;

	Out.PosH = mul(float4(In.Pos, 1.0f), g_worldViewProj);
	float4 posW = mul(float4(In.Pos, 1.0f), g_world);
	Out.PosW = posW.xyz;
	Out.Normal = mul(In.Normal, (float3x3)g_worldInvTranspose);
	Out.Normal = normalize(Out.Normal);
	Out.Tex = In.Tex;
	Out.DiffuseColor = float4(0.0f, 0.0f, 0.0f, 0.0f);
	Out.SpecularColor = float4(0.0f, 0.0f, 0.0f, 0.0f);
	Out.lightVec = g_SpotLight.Position - Out.PosW;
	return Out;
	
}

没什么新的变化,很容易理解,这里我计算lightVec 主要是不想在像素着色器中计算,这里计算,然后光栅化插值,会好那么一点点。

像素着色函数:

这里跟点光源的处理相同

float4  PS_02(VertexOut In) :SV_Target
{
	float4 Texcolor;
	float  d = length(In.lightVec);
	float4 Ambient = g_SpotLight.Ambient;

	Texcolor = ShaderTexture.Sample(samTriLinear, In.Tex);

	if (d > g_SpotLight.Range)
	{
		return Texcolor*(In.DiffuseColor + In.SpecularColor + Ambient);//saturate
	}

	In.lightVec /= d;

	float diffuseFactor = dot(In.lightVec, In.Normal);

	float SpecularFactor = float(0.7f);

	float3 CameraPosition = float3(0.f, 5.0f, -5.0f);

	float3 toEye = CameraPosition - In.PosW;

		[flatten]

	if (diffuseFactor>0.0f)
	{
		float3 v = reflect(-In.lightVec, In.Normal);
		float specFactor = pow(max(dot(v, toEye), 0.0f), SpecularFactor);
		In.DiffuseColor = diffuseFactor*g_SpotLight.Diffuse;
		In.SpecularColor = SpecularFactor*g_SpotLight.Specular;
	}

  

 

新增的变化:

    float3 dir = normalize(g_SpotLight.Direction);

	float spot= max(dot(-In.lightVec, dir), 0.0f);

	float min = 0.85f;

	if (spot>= min)
	{

		float spot = pow(fTensity, g_SpotLight.spot);
		float Att = spot / dot(g_SpotLight.Att, d); /*float3(1.0f, d, d*d)*/

		Ambient *= spot;
		In.DiffuseColor *= Att * 30 * spot;
		In.SpecularColor *= Att * 30 * spot;
	}
	else
	{
		In.DiffuseColor = 0;
		In.SpecularColor = 0;
	}

	float4 color;
	color = Texcolor*saturate(In.DiffuseColor + In.SpecularColor + Ambient)*g_SpotLight.spot/2;//saturate

	return color;

}

值得注意的是,我这里并没有使用龙书的代码,而是采用简单的边界控制实现聚光灯,效果如下图:

 

好像还不错,但是边界像素颗粒比较严重,所以我们设置一个过渡区去优化。

    float min = 0.85f;
	float max = 1.0f;
	if (spot >= min)
	{
                float fTensity = spot;
		float spot = pow(spot , g_SpotLight.spot);
		float Att = spot / dot(g_SpotLight.Att, d); /*float3(1.0f, d, d*d)*/

		Ambient *= spot;

		fTensity = clamp(fTensity, min, max) - min;
		In.DiffuseColor *= Att*fTensity * 30 * spot;
		In.SpecularColor *= Att*fTensity * 30 * spot;
	}

 

clamp函数返回min至max中的一个值,这个函数是什么意思了,就是fTensity大于MAX就等于MAX,小于MIN就等于MIN,效果如下图:

 

还不错,这样我们对灯光的运用就很熟练了,所以我们可以来点新的花样

实现代码:


	float X_Distance = abs(In.PosW.x);
	float Z_Distance = abs(In.PosW.z);

	float MIN = 0.8f;
	float MAX = 1.8f;

	if (X_Distance <= MIN || Z_Distance <= MIN)
	{	
		In.DiffuseColor *= 0.6f;
		In.SpecularColor *= 0.6f;
		
	}

	else if (X_Distance >= MAX && Z_Distance >= MAX)
	{
		In.DiffuseColor *= 0.6f;
		In.SpecularColor *= 0.6f;
	}
	else
	{	
		In.DiffuseColor = 0;
		In.SpecularColor = 0;
	}

	float Att = 1.0f / dot(g_SpotLight.Att, d);

	In.DiffuseColor *= Att;
	In.SpecularColor *= Att;
	

	float4 color;
	color = Texcolor*saturate(In.DiffuseColor + In.SpecularColor + Ambient)*g_SpotLight.spot / 3;//saturate
	return color;
}

 

很简单吧,但是特效可是十足的,稍微改动一下代码:

  else if (X_Distance >= MAX && Z_Distance >= MAX)

                ||
                ||     
             ___||___
             \      /
              \    /
               \  /
                \/
  else if (X_Distance >= MAX || Z_Distance >= MAX)

效果又不一样:

是不是很令人心动,更多特效等待您的发掘。

 

 

 

文房四宝:佳灯相伴

本例我们实现四个聚光灯,相应成彰,首先给light.cpp来个扩建。

class  Light
{

	Light_desc*    Light_Desc;
}

头文件很简单的改变,创建灯光结构体指针。

完整文件:

#pragma once
#include "Util.h"


class  Light
{
public:
	Light(int light_num);
	~ Light();
private:
	struct  Light_desc
	{
		Light_desc(){ ZeroMemory(this, sizeof(this)); }

		XMFLOAT4   Ambient;
		XMFLOAT4   Diffuse;
		XMFLOAT4   Specular;

		XMFLOAT3   Position;
		float      Range;

		XMFLOAT3   Direction;
		float      spot;

		XMFLOAT3   Att;//attenuation
		float      Pad;

	};
private:
	D3DXVECTOR3*	m_vRightVector;// 右分量向量
	D3DXVECTOR3*	m_vUpVector; //上分量向量
	D3DXVECTOR3*	m_vLookVector; // 观察方向向量
	D3DXVECTOR3*	m_vLightPosition; // 光源位置
private:
	ID3DX11EffectVariable       *m_fxLight;
	size_t           m_lightNum;
public:
	Light_desc*    Light_Desc;
	void          Init_Light();
	void          Deploy_Shader(ID3DX11Effect*);
	void          Frame_Light();
	void          Clean_Light();
	void          Rotate_RightVector(float);
	void          Rotate_UpVector(float);

};

 

 

light.cpp:很简单的改变,就是将单例改为数组。

构造析构:

Light::Light(int light_num)
{
	    m_lightNum = light_num;
		m_fxLight = NULL;
		Light_Desc = NULL;

		
}

Light::~Light()
{
	m_lightNum = 0;
	m_fxLight = NULL;
	Light_Desc = NULL;
	
}

 初始化:

void Light::Init_Light()
{
	Light_Desc = new Light_desc[m_lightNum];
	
	m_vRightVector = new D3DXVECTOR3[m_lightNum];
	m_vUpVector = new D3DXVECTOR3[m_lightNum];
	m_vLookVector = new D3DXVECTOR3[m_lightNum];
	m_vLightPosition = new D3DXVECTOR3[m_lightNum];

	for (size_t i = 0; i < m_lightNum; i++)
	{
		m_vRightVector[i] = D3DXVECTOR3(1.f, 0.f, 0.f);// 右分量向量
		m_vUpVector[i] = D3DXVECTOR3(0.f, 1.f, 0.f);// 上分量向量
		m_vLookVector[i] = D3DXVECTOR3(0.0f, 0.0f, 1.0f); // 观察方向向量
		m_vLightPosition[i] = D3DXVECTOR3(0.0f, 0.0f, 0.0f);// 光源位置
	}
}

 

配置shader文件:

void  Light::Deploy_Shader(ID3DX11Effect* effect)
{

	m_fxLight = effect->GetVariableByName("g_SpotLight");
}

 

循环部分:

void Light::Frame_Light()
{
	
	for (size_t i = 0; i < m_lightNum; i++)
	{
	//	m_vLightPosition[i] = D3DXVECTOR3(Light_Desc[i].Position.x, Light_Desc[i].Position.y, Light_Desc[i].Position.z);
		m_vLookVector[i] = D3DXVECTOR3(Light_Desc[i].Direction.x, Light_Desc[i].Direction.y, Light_Desc[i].Direction.z);

		D3DXVec3Normalize(&m_vLookVector[i], &m_vLookVector[i]);

		//正交并规范化m_vRightVector
		D3DXVec3Cross(&m_vRightVector[i], &m_vUpVector[i], &m_vLookVector[i]);
		D3DXVec3Normalize(&m_vRightVector[i], &m_vRightVector[i]);

		//正交并规范化m_vUpVector
		D3DXVec3Cross(&m_vUpVector[i], &m_vLookVector[i], &m_vRightVector[i]);
	}
	m_fxLight->SetRawValue(Light_Desc,0, m_lightNum* sizeof(Light_desc));
}

控制部分和回收资源部分就不贴代码了,很简单的重复工作,

读者看到这里请注意,本例函数返回值大部分为void,这并不值得

提倡,在重要且容易出错的函数内,应实现一次或多次判断,避免函数

在错误的道路上越走越远,那样你去寻找错误来源时就不会很伤脑筋。

 

在灯光的上级抽象类中给聚光灯赋值:


void graphicsclass::Create_Mutil_Light(int light_num)
{

	m_light = new Light(light_num);
	m_light->Init_Light();
	for (size_t i = 0; i < light_num; i++)
	{
	m_light->Light_Desc[i].Ambient =XMFLOAT4(0.1f, 0.1f, 0.1f, 1.0f);
	m_light->Light_Desc[i].Diffuse = XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f);
	m_light->Light_Desc[i].Specular = XMFLOAT4(0.4f, 0.4f, 0.4f, 1.0f);
	m_light->Light_Desc[i].Range = float(20.f);
	m_light->Light_Desc[i].spot = float(8.0f);
	m_light->Light_Desc[i].Att = XMFLOAT3(0.1f, 0.1f, 0.1f);
	m_light->Light_Desc[i].Pad = (float)i;
	m_light->Light_Desc[i].Direction = XMFLOAT3(0.0f, -1.0f, -1.0f);
	XMVECTOR dir= XMVector3Normalize(XMLoadFloat3(&m_light->Light_Desc[i].Direction));  
	XMStoreFloat3(&m_light->Light_Desc[i].Direction, dir);
	//m_light[i].Light_Desc.Light_Contribution = XMFLOAT4(0,0,0,0);
	m_light->Deploy_Shader(m_shader->GetIeffect());
	}

	m_light->Light_Desc[0].Position = XMFLOAT3(4.0f, 0.8f, -4.0f);
	m_light->Light_Desc[0].Diffuse = XMFLOAT4(0.2f, 0.2f, 0.1f, 1.0f);

	m_light->Light_Desc[1].Position = XMFLOAT3(-4.0f, 0.8f, -4.0f);
	m_light->Light_Desc[1].Diffuse = XMFLOAT4(0.8f, 0.1f, 0.1f, 1.0f);

	m_light->Light_Desc[2].Position = XMFLOAT3(-4.0f, 0.8f, 4.0f);
	m_light->Light_Desc[2].Diffuse = XMFLOAT4(0.1f, 0.1f, 0.8f, 1.0f);

	m_light->Light_Desc[3].Position = XMFLOAT3(4.0f, 0.8f, 4.0f);
	m_light->Light_Desc[3].Diffuse = XMFLOAT4(0.1f, 0.9f, 0.1f, 1.0f);
	
}
#include"graphics.h"

graphicsclass::graphicsclass()
{
	m_light = NULL;

}

graphicsclass::~graphicsclass()
{

}

BOOL graphicsclass::Init_Graphics()
{
	
	Create_Mutil_Light(4);

m_light->Deploy_Shader(m_shader->GetIeffect());

	return TRUE;
}

BOOL  graphicsclass::Frame_Graphics()
{

    m_light->Frame_Light();

}

void   graphicsclass::Clean_Graphics()
{

    m_light->Clean_Light();

	delete m_light;        m_light = NULL;
	
}

 

HLSL:就只有像素着色器需要改动:简单到令人心疼。

float4  PS(VertexOut In, uniform int lightCount) :SV_Target
{

	float4 color = (float4)0;

	float4 TexColor = (float4)0;

	float3 normal = normalize(In.Normal);

	float3 CameraPosition = float3(0.0f, 10.0f, -10.0f);

	float3 ViewDir = normalize(CameraPosition - In.PosW);

	float4 Ambient_Color = (float4)0;

	float4 Diffuse_Color = (float4)0;

	float4 Specular_Color = (float4)0;

	float  Mat_Specular = 0.1f;


	TexColor = ShaderTexture.Sample(samTriLinear, In.Tex);

	[unroll]
	for (int i = 0; i < lightCount; i++)
	{

		ComputeSpotLight(Mat_Specular, g_SpotLight[i], In.PosW, normal, CameraPosition,
			Ambient_Color, Diffuse_Color, Specular_Color);

		Ambient_Color += float4(0.1f,0.1f,0.1f,1.0f);

		color +=Ambient_Color + Diffuse_Color + Specular_Color;
	}

	color = TexColor*saturate(color);
	return color;

}

ComputeSpotLight:来源于龙书11,其实只是把函数整合一下而已:

void ComputeSpotLight(float Specular, SpotLight L, float3 pos, float3
	normal, float3 toEye,
	out float4 ambient, out float4 diffuse, out float4
	spec)
{
	// 初始化输出变量.
	ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
	diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
	spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
	// 从表面指向光源的光照矢量
	float3 lightVec = L.Position - pos;
	// 表面离开光源的距离
	float d = length(lightVec);
	// Range test.
	if (d > L.Range)
		return;
	// 规范化光照矢量
	lightVec /= d;
	// 计算环境光
	ambient = L.Ambient;
	// 计算漫反射和镜面光,provided the surface is in
	// the line of site of the light.
	float diffuseFactor = dot(lightVec, normal);
	// Flatten 避免动态分支
	[flatten]
	if (diffuseFactor > 0.0f)
	{
		float3 v = reflect(-lightVec, normal);
		float specFactor = pow(max(dot(v, toEye), 0.0f),Specular);
		diffuse = diffuseFactor * L.Diffuse;
		spec = specFactor* L.Specular;
	}
	// Scale by spotlight factor and attenuate.
	float cos_spot = pow(max(dot(-lightVec, L.Direction), 0.0f), L.Spot);
	// Scale by spotlight factor and attenuate.
	float att = cos_spot / dot(L.Att, float3(1.0f, d, d*d));
	ambient *= cos_spot;
	diffuse *= att;
	spec *= att;
}

其实这里有个小纰漏,我们改变灯光的范围是通过控制余弦值的幂,上面有提及,这会是整体亮度降低,不符合我们将聚光灯收束的特性:聚光灯内部更亮,外围越暗,在数学意义上解释来说:y=cos(x)^n^{};当n=1时,从0到\pi/2积分,面积为1,当n=2时,面积为\pi/4,而我们的面积(光通量)应该是相等的,所以我们的提升光圈范围内的y值。

所以我们加了个小函数:

float cos_spot = pow(max(dot(-lightVec, L.Direction), 0.0f), L.Spot);
	float Amend_spot = pow(abs((L.Spot+10) / L.Spot), L.Spot);
	// Scale by spotlight factor and attenuate.
	float att = cos_spot / dot(L.Att, float3(1.0f, d, d*d));
	ambient *= cos_spot*Amend_spot;
	diffuse *= att*Amend_spot;
	spec *= att*Amend_spot;

当spot越大时,其函数值越小,对光通量的影响也就越小,以至于在spot比较大时,颜色计算可以忽略Amend_spot ,但无论怎么讲,此函数还是有效的的使光圈内的光通量增加,虽然不是按照正确的方式,这样即使是spot很大,我们仍然能让光圈内的亮度到达一个比较高的值。

spot=800时    对比图:

                                      

当然这个函数明显是错误的,因为spot=800时,光圈应该只有左边的大小,如果读者想实现真正的聚光灯,嗯恩,你数学得不错。

ok,总结到这基本上就要说byebye了,有一点提一下,如果读者使用了Amend_spot ,一定要把它放到cpu中去处理,这样太浪费了。

 

愿惠比寿老爷爷祝福我们。

 

源码链接:

https://pan.baidu.com/s/1NGt12oXXvmwYplv8tGoZAg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值