几种光源

Learn Opengl学习笔记


定向光(Directional Light)

当一个光源很远的时候,来自光源的每条光线接近于平行。这看起来就像所有的光线来自于同一个方向,无论物体和观察者在哪儿。当一个光源被设置为无限远时,它被称为定向光(Directional Light),因为所有的光线都有着同一个方向;它会独立于光源的位置。

我们知道的定向光源的一个好例子是,太阳。来自于太阳的所有的光线都被定义为平行光:

因为所有的光线都是平行的,对于场景中的每个物体光的方向都保持一致,物体和光源的位置保持怎样的关系都无所谓。由于光的方向向量保持一致,光照计算会和场景中的其他物体相似。

我们可以通过定义一个光的方向向量,来模拟这样一个定向光,而不是使用光的位置向量。着色器计算保持大致相同的要求,这次我们直接使用光的方向向量来代替用 lightDir 向量和 position 向量的计算:

//定向光 
struct DirectionLight
{
    vec3 direction;
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
uniform DirectionLight light;
...
void main()
{
    vec3 lightDir = normalize(-light.direction);
    ...
}

注意,我们首先对light.direction向量取反。目前我们使用的光照计算需要光的方向作为一个来自片段朝向的光源的方向,但是人们通常更习惯定义一个定向光作为一个全局方向,它从光源发出。所以我们必须对全局光的方向向量取反来改变它的方向;它现在是一个方向向量指向光源。同时,确保对向量进行标准化处理。

作为结果的lightDir向量被使用在diffusespecular计算之前。

为了清晰地强调一个定向光对所有物体都有同样的影响, 定义n个不同的箱子位置,为每个箱子生成不同的模型矩阵,每个模型矩阵包含相应的本地到世界变换:

    glBindVertexArray(vao);
    for (int i=0; i<4; i++) {
        Mat4* model = new Mat4();
        model->rotate(Vec3(1, 1, 0), CC_DEGREES_TO_RADIANS(60));
        model->translate(cubpos[i]);
        GLuint modeloc = glGetUniformLocation(program->getProgram(), "model");
        glUniformMatrix4fv(modeloc, 1, GL_FALSE, model->m);
        
        //把model 矩阵 求逆 、转置后传给vertex shader
        Mat4* traninversemodel = new Mat4(*model);
        traninversemodel->inverse();
        traninversemodel->transpose();
        glUniformMatrix4fv(glGetUniformLocation(program->getProgram(), "tranInverseModel"), 1, GL_FALSE, traninversemodel->m);
        
        glDrawArrays(GL_TRIANGLES, 0, 36);
    }
    glBindVertexArray(0);


不要忘记定义光源的方向(注意,我们把方向定义为:从光源处发出的方向;在下面,你可以快速看到光的方向的指向):
glUniform3f(glGetUniformLocation(program->getProgram(), "light.direction"), -0.2, -1.0, -0.3);

点光源(Point Light)

点光是一个在时间里有位置的光源,它向所有方向发光,光线随距离增加逐渐变暗。想象灯泡和火炬作为投光物,它们可以扮演点光的角色。


衰减(Attenuation)

随着光线穿越距离的变远使得亮度也相应地减少的现象,通常称之为衰减(Attenuation)。一种随着距离减少亮度的方式是使用线性等式。这样的一个随着距离减少亮度的线性方程,可以使远处的物体更暗。然而,这样的线性方程效果会有点假。在真实世界,通常光在近处时非常亮,但是一个光源的亮度,开始的时候减少的非常快,之后随着距离的增加,减少的速度会慢下来。我们需要一种不同的方程来减少光的亮度。

下面的方程把一个片段的光的亮度除以一个已经计算出来的衰减值,这个值根据光源的远近得到:


在这里 d 代表片段到光源的距离。为了计算衰减值,我们定义3个(可配置)项:常数 Kc 一次 Kl 二次 Kq

  • 常数项通常是1.0,它的作用是保证分母永远不会比1小,因为它可以利用一定的距离增加亮度,这个结果不会影响到我们所寻找的。
  • 一次项用于与距离值相乘,这会以线性的方式减少亮度。
  • 二次项用于与距离的平方相乘,为光源设置一个亮度的二次递减。二次项在距离比较近的时候相比一次项会比一次项更小,但是当距离更远的时候比一次项更大。

由于二次项的光会以线性方式减少,指导距离足够大的时候,就会超过一次项,之后,光的亮度会减少的更快。最后的效果就是光在近距离时亮度很高,但是距离变远亮度迅速降低,最后亮度降低速度再次变慢。下面的图展示了在100以内的范围,这样的衰减效果。

你可以看到当距离很近的时候光有最强的亮度,但是随着距离增大,亮度明显减弱,大约接近100的时候,就会慢下来。这就是我们想要的。

但是,我们把这三个项设置为什么值呢?正确的值的设置由很多因素决定:环境、你希望光所覆盖的距离范围、光的类型等。大多数场合,这是经验的问题,也要适度调整。下面的表格展示一些各项的值,它们模拟现实(某种类型的)光源,覆盖特定的半径(距离)。第一栏定义一个光的距离,它覆盖所给定的项。这些值是大多数光的良好开始,它是来自Ogre3D的维基的礼物:

距离 常数项 一次项 二次项
7 1.0 0.7 1.8
13 1.0 0.35 0.44
20 1.0 0.22 0.20
32 1.0 0.14 0.07
50 1.0 0.09 0.032
65 1.0 0.07 0.017
100 1.0 0.045 0.0075
160 1.0 0.027 0.0028
200 1.0 0.022 0.0019
325 1.0 0.014 0.0007
600 1.0 0.007 0.0002
3250 1.0 0.0014 0.000007

就像你所看到的,常数项 Kc 一直都是1.0。一次项 Kl 为了覆盖更远的距离通常很小,二次项 Kq 就更小了。尝试用这些值进行实验,看看它们在你的实现中各自的效果。我们的环境中,32到100的距离对大多数光通常就足够了。

为了实现衰减,在着色器中我们会需要三个额外数值:也就是公式的常量、一次项和二次项。最好把它们储存在之前定义的Light结构体中。要注意的是我们计算 lightDir ,就是在前面的教程中我们所做的,不是像之前的定向光的那部分。

//lightmap.fsh, 物体的fragment shader, 实现光照贴图

varying vec3 v_normal; //需要把法向量由顶点着色器传递到片段着色器
varying vec3 v_fragpos; //片段的位置,用以计算每个片段的法向量
varying vec2 v_texCoord; //纹理坐标

//uniform vec3 objectColor;
//uniform vec3 lightColor;
//uniform vec3 lightpos; //传入灯光的位置
uniform vec3 viewPos; //摄像机位置, 观察者的世界空间坐标,我们简单地使用摄像机对象的位置坐标代替(它就是观察者)

struct Material
{
    sampler2D diffuse; //移除amibient材质颜色向量,因为ambient颜色绝大多数情况等于diffuse颜色,所以不需要分别去储存它
    sampler2D specular;
    float shininess;
}; //材质结构体
uniform Material mater;

//点光源
struct PointLight
{
    vec3 position;
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    float constant; //衰减公式的常熟
    float linear; //一次常数
    float quadratic; //2次常数
};
uniform PointLight light;

void main()
{
    vec3 texdiffuse = vec3(texture2D(mater.diffuse, v_texCoord));

    //环境光
    vec3 ambient = light.ambient * texdiffuse;
    
    //漫反射
    vec3 norm = normalize(v_normal);
    vec3 lightdir = normalize(light.position - v_fragpos);//normalize(lightpos - v_fragpos);
    float diff = max(dot(norm, lightdir), 0.0);
    vec3 diffuse = light.diffuse * diff * texdiffuse;
    
    //镜面反射
    float specularStrenth = 0.5; //镜面强度
    vec3 viewDir = normalize(viewPos - v_fragpos);
    vec3 reflectDir = reflect(-lightdir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), mater.shininess); //这个32是高光的发光值(Shininess)。一个物体的发光值越高,反射光的能力越强,散射得越少,高光点越小。
    vec3 specular = light.specular * spec * vec3(texture2D(mater.specular, v_texCoord));
    
    //点光源的衰减
    float dist = length(light.position - v_fragpos);
    float attenuation = 1.0 / (light.constant + light.linear * dist + light.quadratic * dist * dist);
    ambient *= attenuation;
    diffuse *= attenuation;
    specular *= attenuation;
    
    vec3 result = ambient + diffuse + specular;
    gl_FragColor = vec4(result, 1.0);
}

在片段着色器中实现衰减很直接:我们根据公式简单的计算衰减值,在乘以ambientdiffusespecular元素。

我们需要将光源的距离提供给公式;还记得我们是怎样计算向量的长度吗?我们可以通过获取片段和光源之间的不同向量把向量的长度结果作为距离项。我们可以使用GLSL的内建length函数做这件事.

然后,我们在OpenGL中设置这些项:我们希望光覆盖50的距离,所以我们会使用上面的表格中合适的常数项、一次项和二次项:

  glUniform3f(glGetUniformLocation(program->getProgram(), "light.position"), lightpos.x, lightpos.y, lightpos.z); //点光源的位置
    glUniform1f(glGetUniformLocation(program->getProgram(), "light.constant"), 1.0); //点光源的衰减系数
    glUniform1f(glGetUniformLocation(program->getProgram(), "light.linear"), 0.09);
    glUniform1f(glGetUniformLocation(program->getProgram(), "light.quadratic"), 0.032);


聚光(Spotlight)

聚光是一种位于环境中某处的光源,它不是向所有方向照射,而是只朝某个方向照射。结果是只有一个聚光照射方向的确定半径内的物体才会被照亮,其他的都保持黑暗。聚光的好例子是路灯或手电筒。

OpenGL中的聚光用世界空间位置,一个方向和一个指定了聚光半径的切光角来表示。我们计算的每个片段,如果片段在聚光的切光方向之间(就是在圆锥体内),我们就会把片段照亮。下面的图可以让你明白聚光是如何工作的:

  • LightDir:从片段指向光源的向量。
  • SpotDir:聚光所指向的方向。
  • Phi(Φ:定义聚光半径的切光角。每个落在这个角度之外的,聚光都不会照亮。
  • Theta(Θ)LightDir向量和SpotDir向量之间的角度。值应该比值小,这样才会在聚光内。

所以我们大致要做的是,计算LightDir向量和SpotDir向量的点乘(返回两个单位向量的点乘,还记得吗?),然后在和切光角对比。现在你应该明白聚光是我们下面将创建的手电筒的范例。

手电筒(Flashlight)是一个坐落在观察者位置的聚光,通常瞄准玩家透视图的前面。基本上说,一个手电筒是一个普通的聚光,但是根据玩家的位置和方向持续的更新它的位置和方向。

所以我们需要为片段着色器提供的值,是聚光的位置向量(来计算光的方向坐标),聚光的方向向量和切光角。我们可以把这些值储存在SpotLight结构体中:

//聚光、手电筒
struct SpotLight
{
    vec3 position; //聚光的位置向量 , 手电筒的位置
    vec3 direction; //聚光的方向向量, 手电筒的光照中心线方向
    float cutOff; //切光角 的余弦值, 聚光辐射范围角度的一半,
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    float constant; //衰减公式的常熟
    float linear; //一次常数
    float quadratic; //2次常数
};
uniform SpotLight light;

我们把这些适当的值传给 片段着色器:

//聚光的参数
    glUniform3f(glGetUniformLocation(program->getProgram(), "light.position"), cam->Position.x, cam->Position.y, cam->Position.z);
    glUniform3f(glGetUniformLocation(program->getProgram(), "light.direction"), cam->Front.x, cam->Front.y, cam->Front.z);
    glUniform1f(glGetUniformLocation(program->getProgram(), "light.cutOff"), cosf(CC_DEGREES_TO_RADIANS(12.5)));
    glUniform1f(glGetUniformLocation(program->getProgram(), "light.constant"), 1.0); //聚光灯的衰减系数
    glUniform1f(glGetUniformLocation(program->getProgram(), "light.linear"), 0.09);
    glUniform1f(glGetUniformLocation(program->getProgram(), "light.quadratic"), 0.032);

我们为切光角设置一个角度,但是我们根据一个角度计算了余弦值,把这个余弦结果传给了片段着色器。这么做的原因是在片段着色器中,我们计算 LightDir SpotDir 向量的点乘,而点乘返回一个余弦值,不是一个角度,所以我们不能直接把一个角度和余弦值对比。为了获得这个角度,我们必须计算点乘结果的反余弦,这个操作开销是很大的。所以为了节约一些性能,我们先计算给定切光角的余弦值,然后把结果传递给片段着色器。由于每个角度都被表示为余弦了,我们可以直接对比它们,而不用进行任何开销高昂的操作。

现在剩下要做的是计算 θ 值,用它和 ϕ 值对比,以决定我们是否在或不在聚光的内部:

vec3 lightdir = normalize(light.position - v_fragpos); //从物体到光源的方向向量
    //判断光照是否在聚光灯的照射范围内
    float theta = dot(lightdir, normalize(-light.direction));
    
    if (theta < light.cutOff) {
        vec3 texdiffuse = vec3(texture2D(mater.diffuse, v_texCoord));
        
        //环境光
        vec3 ambient = light.ambient * texdiffuse;
        
        //漫反射
        vec3 norm = normalize(v_normal);
        float diff = max(dot(norm, lightdir), 0.0);
        vec3 diffuse = light.diffuse * diff * texdiffuse;
        
        //镜面反射
        float specularStrenth = 0.5; //镜面强度
        vec3 viewDir = normalize(viewPos - v_fragpos);
        vec3 reflectDir = reflect(-lightdir, norm);
        float spec = pow(max(dot(viewDir, reflectDir), 0.0), mater.shininess); //这个32是高光的发光值(Shininess)。一个物体的发光值越高,反射光的能力越强,散射得越少,高光点越小。
        vec3 specular = light.specular * spec * vec3(texture2D(mater.specular, v_texCoord));
        
        //点光源的衰减
        float dist = length(light.position - v_fragpos);
        float attenuation = 1.0 / (light.constant + light.linear * dist + light.quadratic * dist * dist);
        ambient *= attenuation;
        diffuse *= attenuation;
        specular *= attenuation;
        
        vec3 result = ambient + diffuse + specular;
        gl_FragColor = vec4(result, 1.0);
    }
    else{
        gl_FragColor = vec4(light.ambient + vec3(texture2D(mater.diffuse, v_texCoord)), 1.0);
    }

我们首先计算 lightDir 和取反的 direction 向量的点乘(它是取反过的因为我们想要向量指向光源,而不是从光源作为指向出发点。 确保对所有相关向量进行了标准化处理。

if条件中使用>符号还是<符号?为了在聚光以内,theta不是应该比光的切光值更小吗?这没错,但是不要忘了,角度值是以余弦值来表示的,一个0度的角表示为1.0的余弦值,当一个角是90度的时候被表示为0.0的余弦值,你可以在这里看到:

现在你可以看到,余弦越是接近1.0,角度就越小。这就解释了为什么θ需要比切光值更大了。切光值当前被设置为12.5的余弦,它等于0.9978,所以θ的余弦值在0.9979和1.0之间,片段会在聚光内,被照亮。


平滑边缘

片段着色器一旦到达了聚光的圆锥边缘,它就立刻黑了下来,却没有任何平滑减弱的过度。一个真实的聚光的光会在它的边界处平滑减弱的。为创建聚光的平滑边,我们希望去模拟的聚光有一个内圆锥和外圆锥。我们可以把内圆锥设置为前面部分定义的圆锥,我们希望外圆锥从内边到外边逐步的变暗。

为创建外圆锥,我们简单定义另一个余弦值,它代表聚光的方向向量和外圆锥的向量(等于它的半径)的角度。然后,如果片段在内圆锥和外圆锥之间,就会给它计算出一个0.0到1.0之间的亮度。如果片段在内圆锥以内这个亮度就等于1.0,如果在外面就是0.0。

我们可以使用下面的公式计算这样的值:


   float epsilon = light.cutOff - light.outerCutOff;
    float intensity = 0.0;
    if (theta > light.cutOff) {
        intensity = 1.0;
    }else if(theta > light.outerCutOff){
        intensity = (theta - light.outerCutOff) / epsilon;
    }else{
        intensity = 0.0;
    }

片段着色器代码:

//spotlight.fsh, 实现聚光, 手电筒效果,加边缘光滑效果

varying vec3 v_normal; //需要把法向量由顶点着色器传递到片段着色器
varying vec3 v_fragpos; //片段的位置,用以计算每个片段的法向量
varying vec2 v_texCoord; //纹理坐标

//uniform vec3 objectColor;
//uniform vec3 lightColor;
//uniform vec3 lightpos; //传入灯光的位置
uniform vec3 viewPos; //摄像机位置, 观察者的世界空间坐标,我们简单地使用摄像机对象的位置坐标代替(它就是观察者)

struct Material
{
    sampler2D diffuse; //移除amibient材质颜色向量,因为ambient颜色绝大多数情况等于diffuse颜色,所以不需要分别去储存它
    sampler2D specular;
    float shininess;
}; //材质结构体
uniform Material mater;

//聚光、手电筒
struct SpotLight
{
    vec3 position; //聚光的位置向量 , 手电筒的位置
    vec3 direction; //聚光的方向向量, 手电筒的光照中心线方向
    float cutOff; //切光角 的余弦值, 聚光辐射范围角度的一半,
    float outerCutOff; //外圆锥的切广角的余弦值
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    float constant; //衰减公式的常熟
    float linear; //一次常数
    float quadratic; //2次常数
};
uniform SpotLight light;

void main()
{
    vec3 lightdir = normalize(light.position - v_fragpos); //从物体到光源的方向向量
    //判断光照是否在聚光灯的照射范围内
    float theta = dot(lightdir, normalize(-light.direction));
    float epsilon = light.cutOff - light.outerCutOff;
//    float intensity = clamp((theta - light.outerCutOff) / epsilon,0.0, 1.0);
    float intensity = 0.0;
    if (theta > light.cutOff) { //在内圆锥以内这个亮度就等于1.0
        intensity = 1.0;
    }else if(theta > light.outerCutOff){ //在内圆锥和外圆锥之间,就会给它计算出一个0.0到1.0之间的亮度
        intensity = (theta - light.outerCutOff) / epsilon;
    }else{ //在外圆锥之外就是0.0
        intensity = 0.0;
    }
    
    
    vec3 texdiffuse = vec3(texture2D(mater.diffuse, v_texCoord));
    
    //环境光
    vec3 ambient = light.ambient * texdiffuse;
    
    //漫反射
    vec3 norm = normalize(v_normal);
    float diff = max(dot(norm, lightdir), 0.0);
    vec3 diffuse = light.diffuse * diff * texdiffuse;
    diffuse *= intensity;
    
    //镜面反射
    float specularStrenth = 0.5; //镜面强度
    vec3 viewDir = normalize(viewPos - v_fragpos);
    vec3 reflectDir = reflect(-lightdir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), mater.shininess); //这个32是高光的发光值(Shininess)。一个物体的发光值越高,反射光的能力越强,散射得越少,高光点越小。
    vec3 specular = light.specular * spec * vec3(texture2D(mater.specular, v_texCoord));
    specular *= intensity;
    
    //点光源的衰减
    float dist = length(light.position - v_fragpos);
    float attenuation = 1.0 / (light.constant + light.linear * dist + light.quadratic * dist * dist);
    ambient *= attenuation;
    diffuse *= attenuation;
    specular *= attenuation;
    
    vec3 result = ambient + diffuse + specular;
    gl_FragColor = vec4(result, 1.0);
 
}


多光源

当我们在场景中使用多个光源时一般使用以下途径:创建一个代表输出颜色的向量。每一个光源都对输出颜色贡献一些颜色。因此,场景中的每个光源将进行独立运算,并且运算结果都对最终的输出颜色有一定影响。下面是使用这种方式进行多光源运算的一般结构:

void main()
{
    // 一些属性
    vec3 norm = normalize(v_normal);
    vec3 viewDir = normalize(viewPos - v_fragpos);
    
    // 第一步,计算平行光照
    vec3 result = makeDirLight(dirLight, norm, viewDir);
    // 第二步,计算顶点光照
    for(int i = 0; i < ptlightsNum; i++){
        result += makePointLight(pointLights[i], norm, v_fragpos, viewDir);
    }
    // 第三步,计算 Spot light
    result += makeSpotLight(spotLight, norm, v_fragpos, viewDir);
    
    gl_FragColor = vec4(result, 1.0);
}

然后为每种光定义一种结构体,再定义一种 函数 来计算光照。片段着色器:

//multilight.fsh, 多种光源综合使用,平行光(阳光)、点光源(蜡烛光)、聚光灯(手电筒)。

varying vec3 v_normal; //需要把法向量由顶点着色器传递到片段着色器
varying vec3 v_fragpos; //片段的位置,用以计算每个片段的法向量
varying vec2 v_texCoord; //纹理坐标

uniform vec3 viewPos; //摄像机位置, 观察者的世界空间坐标,我们简单地使用摄像机对象的位置坐标代替(它就是观察者)

struct Material
{
    sampler2D diffuse; //移除amibient材质颜色向量,因为ambient颜色绝大多数情况等于diffuse颜色,所以不需要分别去储存它
    sampler2D specular;
    float shininess;
}; //材质结构体
uniform Material mater;
//定向光
struct DirectionLight
{
    vec3 direction;
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
uniform DirectionLight dirLight;
//点光源
struct PointLight
{
    vec3 position;
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    float constant; //衰减公式的常熟
    float linear; //一次常数
    float quadratic; //2次常数
};
#define ptlightsNum 4
uniform PointLight pointLights[ptlightsNum]; //定义4组点光源
//聚光、手电筒
struct SpotLight
{
    vec3 position; //聚光的位置向量 , 手电筒的位置
    vec3 direction; //聚光的方向向量, 手电筒的光照中心线方向
    float cutOff; //切光角 的余弦值, 聚光辐射范围角度的一半,
    float outerCutOff; //外圆锥的切广角的余弦值
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    float constant; //衰减公式的常熟
    float linear; //一次常数
    float quadratic; //2次常数
};
uniform SpotLight spotLight;

//平行光计算
vec3 makeDirLight(DirectionLight light, vec3 normal, vec3 viewDir)
{
    vec3 texdiffuse = vec3(texture2D(mater.diffuse, v_texCoord));
    
    //环境光
    vec3 ambient = light.ambient * texdiffuse;
    
    //漫反射
    vec3 lightdir = normalize(-light.direction);//normalize(lightpos - v_fragpos);
    float diff = max(dot(normal, lightdir), 0.0);
    vec3 diffuse = light.diffuse * diff * texdiffuse;
    
    //镜面反射
    vec3 reflectDir = reflect(-lightdir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), mater.shininess); //这个32是高光的发光值(Shininess)。一个物体的发光值越高,反射光的能力越强,散射得越少,高光点越小。
    vec3 specular = light.specular * spec * vec3(texture2D(mater.specular, v_texCoord));
    
    vec3 result = ambient + diffuse + specular;
    return result;
}

//计算点光源(Point Light)
vec3 makePointLight(PointLight light, vec3 normal, vec3 fragpos, vec3 viewDir)
{
    vec3 texdiffuse = vec3(texture2D(mater.diffuse, v_texCoord));
    
    //环境光
    vec3 ambient = light.ambient * texdiffuse;
    
    //漫反射
    vec3 lightdir = normalize(light.position - fragpos);
    float diff = max(dot(normal, lightdir), 0.0);
    vec3 diffuse = light.diffuse * diff * texdiffuse;
    
    //镜面反射
    vec3 reflectDir = reflect(-lightdir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), mater.shininess); //这个32是高光的发光值(Shininess)。一个物体的发光值越高,反射光的能力越强,散射得越少,高光点越小。
    vec3 specular = light.specular * spec * vec3(texture2D(mater.specular, v_texCoord));
    
    //点光源的衰减
    float dist = length(light.position - fragpos);
    float attenuation = 1.0 / (light.constant + light.linear * dist + light.quadratic * dist * dist);
    ambient *= attenuation;
    diffuse *= attenuation;
    specular *= attenuation;
    
    vec3 result = ambient + diffuse + specular;
    return result;
}

vec3 makeSpotLight(SpotLight light, vec3 normal, vec3 fragpos, vec3 viewDir)
{
    vec3 lightdir = normalize(light.position - fragpos); //从物体到光源的方向向量
    //判断光照是否在聚光灯的照射范围内
    float theta = dot(lightdir, normalize(-light.direction));
    float epsilon = light.cutOff - light.outerCutOff;
    float intensity = 0.0;
    if (theta > light.cutOff) { //在内圆锥以内这个亮度就等于1.0
        intensity = 1.0;
    }
    else if(theta > light.outerCutOff){ //在内圆锥和外圆锥之间,就会给它计算出一个0.0到1.0之间的亮度
        intensity = (theta - light.outerCutOff) / epsilon;
    }else{ //在外圆锥之外就是0.0
        intensity = 0.0;
    }
    
    
    vec3 texdiffuse = vec3(texture2D(mater.diffuse, v_texCoord));
    
    //环境光
    vec3 ambient = light.ambient * texdiffuse;
    
    //漫反射
    float diff = max(dot(normal, lightdir), 0.0);
    vec3 diffuse = light.diffuse * diff * texdiffuse;
    diffuse *= intensity;
    
    //镜面反射
    vec3 reflectDir = reflect(-lightdir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), mater.shininess); //这个32是高光的发光值(Shininess)。一个物体的发光值越高,反射光的能力越强,散射得越少,高光点越小。
    vec3 specular = light.specular * spec * vec3(texture2D(mater.specular, v_texCoord));
    specular *= intensity;
    
    //点光源的衰减
    float dist = length(light.position - fragpos);
    float attenuation = 1.0 / (light.constant + light.linear * dist + light.quadratic * dist * dist);
    ambient *= attenuation;
    diffuse *= attenuation;
    specular *= attenuation;
    
    vec3 result = ambient + diffuse + specular;
    return result;
}

void main()
{
    // 一些属性
    vec3 norm = normalize(v_normal);
    vec3 viewDir = normalize(viewPos - v_fragpos);
    
    // 第一步,计算平行光照
    vec3 result = makeDirLight(dirLight, norm, viewDir);
    // 第二步,计算顶点光照
    for(int i = 0; i < ptlightsNum; i++){
        result += makePointLight(pointLights[i], norm, v_fragpos, viewDir);
    }
    // 第三步,计算 Spot light
    result += makeSpotLight(spotLight, norm, v_fragpos, viewDir);
    
    gl_FragColor = vec4(result, 1.0);
}

全部代码:

//
//  OpenGLMultiLight.cpp
//  shaderTest
//  
//  Created by MacSBL on 2017/1/10.
//
//

#include "OpenGLMultiLight.h"

bool OpenGLMultiLight::init()
{
    if (!Layer::init()) {
        return false;
    }
    origin = Director::getInstance()->getVisibleOrigin();
    vsize = Director::getInstance()->getVisibleSize();
    
    GLfloat vertices[] = {
        // Positions           // Normals           // Texture Coords
        -0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  0.0f, 0.0f,
        0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  1.0f, 0.0f,
        0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  1.0f, 1.0f,
        0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  0.0f, 0.0f,
        
        -0.5f, -0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   0.0f, 0.0f,
        0.5f, -0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   1.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   1.0f, 1.0f,
        0.5f,  0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   0.0f, 0.0f,
        
        -0.5f,  0.5f,  0.5f, -1.0f,  0.0f,  0.0f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f, -1.0f,  0.0f,  0.0f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f, -1.0f,  0.0f,  0.0f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f, -1.0f,  0.0f,  0.0f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f, -1.0f,  0.0f,  0.0f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f, -1.0f,  0.0f,  0.0f,  1.0f, 0.0f,
        
        0.5f,  0.5f,  0.5f,  1.0f,  0.0f,  0.0f,  1.0f, 0.0f,
        0.5f,  0.5f, -0.5f,  1.0f,  0.0f,  0.0f,  1.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  1.0f,  0.0f,  0.0f,  0.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  1.0f,  0.0f,  0.0f,  0.0f, 1.0f,
        0.5f, -0.5f,  0.5f,  1.0f,  0.0f,  0.0f,  0.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f,  0.0f,  0.0f,  1.0f, 0.0f,
        
        -0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,  0.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,  1.0f, 1.0f,
        0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,  1.0f, 0.0f,
        0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,  1.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,  0.0f, 1.0f,
        
        -0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f,  0.0f, 1.0f,
        0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f,  1.0f, 1.0f,
        0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,  1.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f,  0.0f, 1.0f
    };
//    auto difsprite = Sprite::create("container2.png");
//    diffuseTexture = difsprite->getTexture()->getName();
    
    auto spcsprite = Sprite::create("container2_specular.png");
    specularTexture = spcsprite->getTexture()->getName();
    
    //以下方法绑定纹理失败,原因: glTexImage2D 中的2个 GL_RGB 都改为 GL_RGBA即可。 跟图片是否有alpha通道有关??
    //绑定纹理
    glGenTextures(1, &diffuseTexture);
    glBindTexture(GL_TEXTURE_2D, diffuseTexture);
    
   
    //纹理环绕方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    //纹理过滤
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

     //加载纹理
    Image* img = new Image();
    img->initWithImageFile("container2.png");
    GLsizei width = img->getWidth();
    GLsizei height = img->getHeight();
    unsigned char* imgdata = img->getData();
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imgdata);
    glGenerateMipmap(GL_TEXTURE_2D);
    //释放内存,解绑texture
    CC_SAFE_DELETE(img);
    glBindTexture(GL_TEXTURE_2D, 0);
 
    //——————————————————————————————————————————————————————————————————————————————————————————————
    //设置 glprogram
    auto program = new GLProgram();
    program->initWithFilenames("directionlight.vsh", "multilight.fsh");
    program->link();
    this->setGLProgram(program);
    //——————————————————————————————————————————————————————————————————————————————————————————————
    
    //物体的vao
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);
    
    GLuint vbo;
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    
    //把顶点数据传给 object.vsh vertex shader 的第一个属性(索引为0)
    GLuint posloc = glGetAttribLocation(program->getProgram(), "a_position");
    glVertexAttribPointer(posloc, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(posloc);
    
    //把法向量传给 object.vsh 的a_normal属性,注意:这里不能想当然的认为vertex shader中第2个attribute就是索引为1,必须通过program实事求是的取出它的location
    GLuint normloc = glGetAttribLocation(program->getProgram(), "a_normal");
    glEnableVertexAttribArray(normloc);
    glVertexAttribPointer(normloc, 3, GL_FLOAT, GL_FALSE, 8*sizeof(GLfloat), (GLvoid*)(3*sizeof(GLfloat)));
    
    //纹理坐标传给 vertex shader
    GLuint texloc = glGetAttribLocation(program->getProgram(), "a_texCoord");
    glEnableVertexAttribArray(texloc);
    glVertexAttribPointer(texloc, 2, GL_FLOAT, GL_FALSE, 8*sizeof(GLfloat), (GLvoid*)(6*sizeof(GLfloat)));
    
    glBindVertexArray(0);
    
    
    //灯光的vao, vbo 跟上边一样,灯光和物体使用同一个顶点数组
    glGenVertexArrays(1, &lightvao);
    glBindVertexArray(lightvao);
    
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    
    //把顶点数据传给 light.vsh vertex shader 的第一个属性(索引为0)
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);
    
    glBindVertexArray(0);
    //——————————————————————————————————————————————————————————————————————————————————————————————
    
    lamProgram = new GLProgram();
    lamProgram->initWithFilenames("lamp.vsh", "lamp.fsh");
    lamProgram->link();
    
    //——————————————————————————————————————————————————————————————————————————————————————————————
    //设置转换矩阵
//    model = new Mat4();
//    model->rotate(Vec3(1, 1, 0), CC_DEGREES_TO_RADIANS(60));
    
    //camera
    cam = new MyCamera(Vec3(0, 0, 3));
    view = cam->GetViewMatrix();
    
    projection = new Mat4();
    Mat4::createPerspective(cam->Zoom, vsize.width / vsize.height, 0.1, 1000, projection);
    
//    lightpos = Vec3(0.6, 0.5, 0.7);
    
    Director::getInstance()->setDepthTest(true);
    
    cubpos[0] = {0, 0, 0};
    cubpos[1] = {2, -0.5, -3};
    cubpos[2] = {-1, -2, -2};
    cubpos[3] = {1, 0.5, -1.6};
    
    
    pointLightPos[0] = Vec3( 0.7f,  0.2f,  2.0f);
    pointLightPos[1] = Vec3( 2.3f, -3.3f, -4.0f);
    pointLightPos[2] = Vec3(-4.0f,  2.0f, -12.0f);
    pointLightPos[3] = Vec3( 0.0f,  0.0f, -3.0f);
    
    
    //touch事件
    auto elistener = EventListenerTouchOneByOne::create();
    elistener->onTouchBegan = CC_CALLBACK_2(OpenGLMultiLight::onTouchBegan, this);
    elistener->onTouchMoved = CC_CALLBACK_2(OpenGLMultiLight::onTouchMoved, this);
    elistener->onTouchEnded = CC_CALLBACK_2(OpenGLMultiLight::onTouchEnded, this);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(elistener, this);
    
    return true;
}

void OpenGLMultiLight::visit(cocos2d::Renderer *render, const cocos2d::Mat4 &parentTransform, uint32_t parentflag)
{
    Layer::visit(render, parentTransform, parentflag);
    _command.init(_globalZOrder);
    _command.func = CC_CALLBACK_0(OpenGLMultiLight::onDraw, this);
    Director::getInstance()->getRenderer()->addCommand(&_command);
}

void OpenGLMultiLight::onDraw()
{
    auto program = this->getGLProgram();
    program->use();
    
    
    //物体的 model、view、project 矩阵
    view = cam->GetViewMatrix();
    Mat4::createPerspective(cam->Zoom, vsize.width / vsize.height, 0.1, 1000, projection);
    
    
    
    GLuint viewloc = glGetUniformLocation(program->getProgram(), "view");
    glUniformMatrix4fv(viewloc, 1, GL_FALSE, view->m);
    
    GLuint proloc = glGetUniformLocation(program->getProgram(), "projection");
    glUniformMatrix4fv(proloc, 1, GL_FALSE, projection->m);
    
    
    
    //direction light
    glUniform3f(glGetUniformLocation(program->getProgram(), "dirLight.direction"), -0.2, -1.0, -0.3);
    glUniform3f(glGetUniformLocation(program->getProgram(), "dirLight.ambient"), 0.05, 0.05, 0.05);
    glUniform3f(glGetUniformLocation(program->getProgram(), "dirLight.diffuse"), 0.4, 0.4, 0.4);
    glUniform3f(glGetUniformLocation(program->getProgram(), "dirLight.specular"), 0.5, 0.5, 0.5);
    
    //4个point light
    glUniform3f(glGetUniformLocation(program->getProgram(), "pointLights[0].position"), pointLightPos[0].x, pointLightPos[0].y, pointLightPos[0].z); //点光源的位置
    glUniform3f(glGetUniformLocation(program->getProgram(), "pointLights[0].ambient"), 0.05, 0.05, 0.05);
    glUniform3f(glGetUniformLocation(program->getProgram(), "pointLights[0].diffuse"), 0.8, 0.8, 0.8);
    glUniform3f(glGetUniformLocation(program->getProgram(), "pointLights[0].specular"), 1, 1, 1);
    glUniform1f(glGetUniformLocation(program->getProgram(), "pointLights[0].constant"), 1.0); //点光源的衰减系数
    glUniform1f(glGetUniformLocation(program->getProgram(), "pointLights[0].linear"), 0.09);
    glUniform1f(glGetUniformLocation(program->getProgram(), "pointLights[0].quadratic"), 0.032);
    
    glUniform3f(glGetUniformLocation(program->getProgram(), "pointLights[1].position"), pointLightPos[1].x, pointLightPos[1].y, pointLightPos[1].z); //点光源的位置
    glUniform3f(glGetUniformLocation(program->getProgram(), "pointLights[1].ambient"), 0.05, 0.05, 0.05);
    glUniform3f(glGetUniformLocation(program->getProgram(), "pointLights[1].diffuse"), 0.8, 0.8, 0.8);
    glUniform3f(glGetUniformLocation(program->getProgram(), "pointLights[1].specular"), 1, 1, 1);
    glUniform1f(glGetUniformLocation(program->getProgram(), "pointLights[1].constant"), 1.0); //点光源的衰减系数
    glUniform1f(glGetUniformLocation(program->getProgram(), "pointLights[1].linear"), 0.09);
    glUniform1f(glGetUniformLocation(program->getProgram(), "pointLights[1].quadratic"), 0.032);
    
    glUniform3f(glGetUniformLocation(program->getProgram(), "pointLights[2].position"), pointLightPos[2].x, pointLightPos[2].y, pointLightPos[2].z); //点光源的位置
    glUniform3f(glGetUniformLocation(program->getProgram(), "pointLights[2].ambient"), 0.05, 0.05, 0.05);
    glUniform3f(glGetUniformLocation(program->getProgram(), "pointLights[2].diffuse"), 0.8, 0.8, 0.8);
    glUniform3f(glGetUniformLocation(program->getProgram(), "pointLights[2].specular"), 1, 1, 1);
    glUniform1f(glGetUniformLocation(program->getProgram(), "pointLights[2].constant"), 1.0); //点光源的衰减系数
    glUniform1f(glGetUniformLocation(program->getProgram(), "pointLights[2].linear"), 0.09);
    glUniform1f(glGetUniformLocation(program->getProgram(), "pointLights[2].quadratic"), 0.032);
    
    glUniform3f(glGetUniformLocation(program->getProgram(), "pointLights[3].position"), pointLightPos[3].x, pointLightPos[3].y, pointLightPos[3].z); //点光源的位置
    glUniform3f(glGetUniformLocation(program->getProgram(), "pointLights[3].ambient"), 0.05, 0.05, 0.05);
    glUniform3f(glGetUniformLocation(program->getProgram(), "pointLights[3].diffuse"), 0.8, 0.8, 0.8);
    glUniform3f(glGetUniformLocation(program->getProgram(), "pointLights[3].specular"), 1, 1, 1);
    glUniform1f(glGetUniformLocation(program->getProgram(), "pointLights[3].constant"), 1.0); //点光源的衰减系数
    glUniform1f(glGetUniformLocation(program->getProgram(), "pointLights[3].linear"), 0.09);
    glUniform1f(glGetUniformLocation(program->getProgram(), "pointLights[3].quadratic"), 0.032);


    //聚光spot light 的参数
    glUniform3f(glGetUniformLocation(program->getProgram(), "spotLight.position"), cam->Position.x, cam->Position.y, cam->Position.z);
    glUniform3f(glGetUniformLocation(program->getProgram(), "spotLight.direction"), cam->Front.x, cam->Front.y, cam->Front.z);
    glUniform1f(glGetUniformLocation(program->getProgram(), "spotLight.cutOff"), cosf(CC_DEGREES_TO_RADIANS(12.5)));
    glUniform1f(glGetUniformLocation(program->getProgram(), "spotLight.outerCutOff"), cosf(CC_DEGREES_TO_RADIANS(17.5)));
    glUniform3f(glGetUniformLocation(program->getProgram(), "spotLight.ambient"), 0.05, 0.05, 0.05);
    glUniform3f(glGetUniformLocation(program->getProgram(), "spotLight.diffuse"), 1, 1, 1);
    glUniform3f(glGetUniformLocation(program->getProgram(), "spotLight.specular"), 1, 1, 1);
    glUniform1f(glGetUniformLocation(program->getProgram(), "spotLight.constant"), 1.0); //聚光灯的衰减系数
    glUniform1f(glGetUniformLocation(program->getProgram(), "spotLight.linear"), 0.09);
    glUniform1f(glGetUniformLocation(program->getProgram(), "spotLight.quadratic"), 0.032);

    
    GL::bindTexture2DN(0, diffuseTexture);
    glUniform1i(glGetUniformLocation(program->getProgram(), "mater.diffuse"), 0);
    GL::bindTexture2DN(1, specularTexture);
    glUniform1i(glGetUniformLocation(program->getProgram(), "mater.specular"), 1);
    glUniform1f(glGetUniformLocation(program->getProgram(), "mater.shininess"), 64.0);
    
    GLuint viewposloc = glGetUniformLocation(program->getProgram(), "viewPos");
    glUniform3f(viewposloc, cam->Position.x, cam->Position.y, cam->Position.z);
    
    
    glBindVertexArray(vao);
    for (int i=0; i<4; i++) {
        Mat4* model = new Mat4();
        model->rotate(Vec3(1, 1, 0), CC_DEGREES_TO_RADIANS(60));
        model->translate(cubpos[i]);
        GLuint modeloc = glGetUniformLocation(program->getProgram(), "model");
        glUniformMatrix4fv(modeloc, 1, GL_FALSE, model->m);
        
        //把model 矩阵 求逆 、转置后传给vertex shader
        Mat4* traninversemodel = new Mat4(*model);
        traninversemodel->inverse();
        traninversemodel->transpose();
        glUniformMatrix4fv(glGetUniformLocation(program->getProgram(), "tranInverseModel"), 1, GL_FALSE, traninversemodel->m);
        
        glDrawArrays(GL_TRIANGLES, 0, 36);
    }
    glBindVertexArray(0);
    
    //灯光的model、view、project 矩阵
    lamProgram->use();
    
    
    viewloc = glGetUniformLocation(lamProgram->getProgram(), "view");
    glUniformMatrix4fv(viewloc, 1, GL_FALSE, view->m);
    
    proloc = glGetUniformLocation(lamProgram->getProgram(), "projection");
    glUniformMatrix4fv(proloc, 1, GL_FALSE, projection->m);
    
    glBindVertexArray(lightvao);
    for (int i=0; i<4; i++) {
        GLuint modeloc = glGetUniformLocation(lamProgram->getProgram(), "model");
        auto lightmodel = new Mat4();
        lightmodel->translate(pointLightPos[i]);
        lightmodel->rotate(Vec3(0, 1, 1), CC_DEGREES_TO_RADIANS(30));
        lightmodel->scale(0.2);
        glUniformMatrix4fv(modeloc, 1, GL_FALSE, lightmodel->m);
        glDrawArrays(GL_TRIANGLES, 0, 36);
    }
    
    glBindVertexArray(0);
}
#pragma mark touch

bool OpenGLMultiLight::onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *evt)
{
    return true;
}

void OpenGLMultiLight::onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *evt)
{
    Vec2 curpos = touch->getLocationInView();
    Vec2 prepos = touch->getPreviousLocationInView();
    GLfloat dx = curpos.x - prepos.x;
    GLfloat dy = curpos.y - prepos.y;
    
    //移动摄像机
    GLfloat camspeed = 0.05f;
        if (curpos.y - prepos.y > 0) { //w
            cam->ProcessKeyboard(Cam_Move::FORWARD, camspeed);
        }else if (curpos.y - prepos.y < 0){ //s
            cam->ProcessKeyboard(Cam_Move::BACKWARD, camspeed);
        }
        else if (curpos.x - prepos.x < 0){ //a
             cam->ProcessKeyboard(Cam_Move::LEFT, camspeed);
        }else if (curpos.x - prepos.x > 0){ //d
            cam->ProcessKeyboard(Cam_Move::RIGHT, camspeed);
        }
    //(3)旋转摄像机
    
//    cam->ProcessMouseMovement(dx, dy);
           //(4)缩放
//    if(fov >= 1 && fov <= 45){
//        fov -= dx * camspeed;
//    }
//    if(fov <= 1){
//        fov = 1;
//    }
//    if(fov >= 45){
//        fov = 45;
//    }
}

void OpenGLMultiLight::onTouchEnded(cocos2d::Touch *touch, cocos2d::Event *evt)
{
    
}





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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值