颜色可以数字化的由红色(Red)、绿色(Green)和蓝色(Blue)三个分量组成,它们通常被缩写为RGB。这三个不同的分量组合在一起几乎可以表示存在的任何一种颜色。

(一)观察物体

(1)物体的片段着色器

#version 330 core
out vec4 color;uniform vec3 objectColor;
uniform vec3 lightColor;void main()
{color = vec4(lightColor * objectColor, 1.0f);
}

当我们把光源的颜色与物体的颜色相乘,所得到的就是这个物体所反射该光源的颜色(也就是我们感知到的颜色)。

冯氏光照模型

冯氏光照模型的主要结构由3个元素组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。这些光照元素看起来像下面这样:

  • 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上也仍然有一些光亮(月亮、一个来自远处的光),所以物体永远不会是完全黑暗的。我们使用环境光照来模拟这种情况,也就是无论如何永远都给物体一些颜色。
  • 漫反射光照(Diffuse Lighting):模拟一个发光物对物体的方向性影响(Directional Impact)。它是冯氏光照模型最显著的组成部分。面向光源的一面比其他面会更亮。
  • 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色,相比于物体的颜色更倾向于光的颜色。

1、环境光照

把环境光照添加到场景里非常简单。我们用光的颜色乘以一个(数值)很小常量环境因子得到环境光照颜色(物体片段着色器)

void main()
{float ambientStrength = 0.1f;vec3 ambient = ambientStrength * lightColor;
}

2、漫反射

我们需要些什么来计算漫反射光照?

  • 法向量:一个垂直于顶点表面的向量。
  • 定向的光线:作为光的位置和片段的位置之间的向量差的方向向量。为了计算这个光线,我们需要光的位置向量和片段的位置向量。

法向量

我们可以简单的把法线数据手工添加到顶点数据中。更新的顶点数据数组可以在这里找到。试着去想象一下,这些法向量真的是垂直于立方体的各个面的表面的(一个立方体由6个面组成)。

因为我们向顶点数组添加了额外的数据,所以我们应该更新物体的顶点着色器:

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
...

现在我们已经向每个顶点添加了一个法向量,已经更新了顶点着色器,我们还要更新顶点属性指针(Vertex Attibute Pointer)。注意,发光物使用同样的顶点数组作为它的顶点数据,然而发光物的着色器没有使用新添加的法向量。我们不会更新发光物的着色器或者属性配置,但是我们必须至少修改一下顶点属性指针来适应新的顶点数组的大小:

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid * )0);
glEnableVertexAttribArray(0);

我们只想使用每个顶点的前三个浮点数,并且我们忽略后三个浮点数,所以我们只需要把步长参数改成GLfloat尺寸的6倍就行了。

所有光照的计算需要在片段着色器里进行,所以我们需要把法向量由顶点着色器传递到片段着色器。我们这么做:

out vec3 Normal;
void main()
{gl_Position = projection * view * model * vec4(position, 1.0f);Normal = normal;
}

剩下要做的事情是,在片段着色器中定义相应的输入值:

in vec3 Normal;

现在我们已经把法向量从顶点着色器传到了片段着色器。可是,目前片段着色器里,我们都是在世界空间坐标中进行计算的,所以,我们不是应该把法向量转换为世界空间坐标吗?

正规矩阵被定义为“模型矩阵左上角的逆矩阵的转置矩阵”。真拗口,如果你不明白这是什么意思,别担心;我们还没有讨论逆矩阵(Inverse Matrix)和转置矩阵(Transpose Matrix)。注意,定义正规矩阵的大多资源就像应用到模型观察矩阵(Model-view Matrix)上的操作一样,但是由于我们只在世界空间工作(而不是在观察空间),我们只使用模型矩阵。

在顶点着色器中,我们可以使用inversetranspose函数自己生成正规矩阵,inversetranspose函数对所有类型矩阵都有效。注意,我们也要把这个被处理过的矩阵强制转换为3×3矩阵,这是为了保证它失去了平移属性,之后它才能乘以法向量。

Normal = mat3(transpose(inverse(model))) * normal;

即可解决这一问题

定向的光线

每个顶点现在都有了法向量,但是我们仍然需要光的位置向量和片段的位置向量。

1、光的位置向量

我们可以简单的在片段着色器中把它声明为uniform:

uniform vec3 lightPos;

然后再游戏循环中(外面也可以,因为它不会变)更新uniform。我们使用在前面教程中声明的lightPos向量作为光源位置:

GLint lightPosLoc = glGetUniformLocation(lightingShader.Program, "lightPos");
glUniform3f(lightPosLoc, lightPos.x, lightPos.y, lightPos.z);

2、片段的位置(Position)

我们会在世界空间中进行所有的光照计算,因此我们需要一个在世界空间中的顶点位置。我们可以通过把顶点位置属性乘以模型矩阵(Model Matrix,只用模型矩阵不需要用观察和投影矩阵)来把它变换到世界空间坐标。这个在顶点着色器中很容易完成,所以让我们就声明一个输出(out)变量,然后计算它的世界空间坐标:

out vec3 FragPos;
out vec3 Normal;void main()
{gl_Position = projection * view * model * vec4(position, 1.0f);FragPos = vec3(model * vec4(position, 1.0f));Normal = normal;
}

最后,在片段着色器中添加相应的输入变量。

in vec3 FragPos;

现在,所有需要的变量都设置好了,我们可以在片段着色器中开始光照的计算了。

3、计算光源和片段位置之间的方向向量

前面提到,光的方向向量是光的位置向量与片段的位置向量之间的向量差。你可能记得,在变换教程中,我们简单的通过两个向量相减的方式计算向量差。我们同样希望确保所有相关向量最后都转换为单位向量,所以我们把法线和方向向量这个结果都进行标准化:

vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);

4、散射光颜色

我们对normlightDir向量进行点乘,来计算光对当前片段的实际的散射影响。结果值再乘以光的颜色,得到散射因子。两个向量之间的角度越大,散射因子就会越小:

float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;

如果两个向量之间的角度大于90度,点乘的结果就会变成负数,这样会导致散射因子变为负数。为此,我们使用max函数返回两个参数之间较大的参数,从而保证散射因子不会变成负数。负数的颜色是没有实际定义的,所以最好避免它,除非你是那种古怪的艺术家。

5、最终颜色

既然我们有了一个环境光照颜色和一个散射光颜色,我们把它们相加,然后把结果乘以物体的颜色,来获得片段最后的输出颜色。

vec3 result = (ambient + diffuse) * objectColor;
color = vec4(result, 1.0f);

我们在现实生活中看到某一物体的颜色并不是这个物体的真实颜色,而是它所反射(Reflected)的颜色。换句话说,那些不能被物体吸收(Absorb)的颜色(被反射的颜色)就是我们能够感知到的物体的颜色。

3、镜面光照

和环境光照一样,镜面光照(Specular Lighting)同样依据光的方向向量和物体的法向量,但是这次它也会依据观察方向,例如玩家是从什么方向看着这个片段的。镜面光照根据光的反射特性。如果我们想象物体表面像一面镜子一样,那么,无论我们从哪里去看那个表面所反射的光,镜面光照都会达到最大化。你可以从下面的图片看到效果:

我们通过反射法向量周围光的方向计算反射向量。然后我们计算反射向量和视线方向的角度,如果之间的角度越小,那么镜面光的作用就会越大。它的作用效果就是,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光。

观察向量

是镜面光照的一个附加变量,我们可以使用观察者世界空间位置(Viewer’s World Space Position)和片段的位置来计算。之后,我们计算镜面光亮度,用它乘以光的颜色,在用它加上作为之前计算的光照颜色。

为了得到观察者的世界空间坐标,我们简单地使用摄像机对象的位置坐标代替(它就是观察者)。所以我们把另一个uniform添加到片段着色器,把相应的摄像机位置坐标传给片段着色器:

uniform vec3 viewPos;GLint viewPosLoc = glGetUniformLocation(lightingShader.Program, "viewPos");
glUniform3f(viewPosLoc, camera.Position.x, camera.Position.y, camera.Position.z);

计算高光亮度

1、我们定义一个镜面强度(Specular Intensity)变量specularStrength

给镜面高光一个中等亮度颜色,这样就不会产生过度的影响了。

float specularStrength = 0.5f;

2、计算视线方向坐标,和沿法线轴的对应的反射坐标:

vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);

需要注意的是我们使用了lightDir向量的相反数。reflect函数要求的第一个是从光源指向片段位置的向量,但是lightDir当前是从片段指向光源的向量(由先前我们计算lightDir向量时,(减数和被减数)减法的顺序决定)。为了保证我们得到正确的reflect坐标,我们通过lightDir向量的相反数获得它的方向的反向。第二个参数要求是一个法向量,所以我们提供的是已标准化的norm向量。

3、计算镜面亮度分量

float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;

我们先计算视线方向与反射方向的点乘(确保它不是负值),然后得到它的32次幂。这个32是高光的发光值(Shininess)。一个物体的发光值越高,反射光的能力越强,散射得越少,高光点越小。

4、计算了全部的光照元素

把它添加到环境光颜色和散射光颜色里,然后再乘以物体颜色:

vec3 result = (ambient + diffuse + specular) * objectColor;
color = vec4(result, 1.0f);

我们现在为冯氏光照计算了全部的光照元素。

(2)uniform变量赋值

1、对于片段着色器

GLint objectColorLoc = glGetUniformLocation(ourShader.Program, "objectColor");GLint lightColorLoc = glGetUniformLocation(ourShader.Program, "lightColor");GLint lightPosLoc = glGetUniformLocation(ourShader.Program, "lightPos");GLint viewPosLoc = glGetUniformLocation(ourShader.Program, "viewPos");glUniform3f(objectColorLoc, 1.0f, 0.5f, 0.3f);  //珊瑚红glUniform3f(lightColorLoc, 1.0f, 1.0f, 1.0f);  //把光源设置为白色glUniform3f(lightPosLoc, lightPos.x, lightPos.y, lightPos.z);glUniform3f(viewPosLoc, camera.GetPosition().x, camera.GetPosition().y, camera.GetPosition().z);  //使用摄像机对象的位置坐标代替

要注意的是,当我们修改顶点或者片段着色器后,灯的位置或颜色也会随之改变,这并不是我们想要的效果。我们不希望灯对象的颜色在接下来的教程中因光照计算的结果而受到影响,而希望它能够独立。希望表示灯不受其他光照的影响而一直保持明亮(这样它才更像是一个真实的光源)。

2、对于顶点着色器

GLuint modelLoc = glGetUniformLocation(ourShader.Program, "model");  //到 vs 找到那个 model 变量GLuint viewLoc = glGetUniformLocation(ourShader.Program, "view");  //到 vs 找到那个 view 变量GLuint projectionLoc = glGetUniformLocation(ourShader.Program, "projection");  //到 vs 找到那个 projection 变量glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));

3、绑定

GLuint VAO, VBO;  //VAO:Vertex Array Object   VBO:Vertex Buffer Object传数据glGenVertexArrays(1, &VAO);  //创建 VAOglGenBuffers(1, &VBO);glBindVertexArray(VAO);  //设当前直线glBindBuffer(GL_ARRAY_BUFFER, VBO);  //VAO 和 VBO 成对出现// transfer the data:传数据glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);  //静态访问,几乎不修改// set the attributeglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,6 * sizeof(GLfloat), (GLvoid *)0);  //0:对应调色器里 location 的值;3:对应 vec3 三个量;GL_FLOAT:浮点型;GL_FALSE:;6*sizeof(GLfloat):对应 Buffer 里传的数据;(GLvoid*)0:从第 0 个位置开始glEnableVertexAttribArray(0);// 法向量glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE,6 * sizeof(GLfloat), (GLvoid *)(3 * sizeof(GLfloat)));  //1:对应调色器里 location 的值;3:对应 vec3 三个量;GL_FLOAT:浮点型;GL_FALSE:;6*sizeof(GLfloat):对应 Buffer 里传的数据;(GLvoid *)(3 * sizeof(GLfloat)):从第 3 个位置开始glEnableVertexAttribArray(1);glBindBuffer(GL_ARRAY_BUFFER, 0);glBindVertexArray(0);

4、画图

// Draw the triangleglBindVertexArray(VAO);  //使用 VAO,直接绑定glDrawArrays(GL_TRIANGLES, 0, 36);  //画三角形,总共有 36 个顶点//glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);glBindVertexArray(0);

二、光源

1、灯的顶点数据

同物体的顶点数据

2、为灯创建一个新的VAO

因为我们还要创建一个表示灯(光源)的立方体,所以我们还要为这个灯创建一个特殊的VAO。当然我们也可以让这个灯和其他物体使用同一个VAO然后对他的model(模型)矩阵做一些变换,然而接下来的教程中我们会频繁地对顶点数据做一些改变并且需要改变属性对应指针设置,我们并不想因此影响到灯(我们只在乎灯的位置),因此我们有必要为灯创建一个新的VAO。

GLuint lightVAO;glGenVertexArrays(1, &lightVAO);// 绑定光源 VAOglBindVertexArray(lightVAO);// 只需要绑定VBO不用再次设置VBO的数据,因为容器(物体)的VBO数据中已经包含了正确的立方体顶点数据glBindBuffer(GL_ARRAY_BUFFER, VBO);// 设置光源的顶点属性指针(仅设置灯的顶点数据)glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,6 * sizeof(GLfloat), (GLvoid *)0);  //0:对应调色器里 location 的值;3:对应 vec3 三个量;GL_FLOAT:浮点型;GL_FALSE:;6*sizeof(GLfloat):对应 Buffer 里传的数据;(GLvoid*)0:从第 0 个位置开始glEnableVertexAttribArray(0);glBindBuffer(GL_ARRAY_BUFFER, 0);glBindVertexArray(0);

这段代码对你来说应该非常直观。既然我们已经创建了表示灯和被照物体的立方体,我们只需要再定义一个东西就行了了,那就是片段着色器

3、为灯创建另外的一套着色器程序

为了实现这个目的,我们需要为灯创建另外的一套着色器程序,从而能保证它能够在其他光照着色器变化的时候保持不变。顶点着色器和我们当前的顶点着色器是一样的,所以你可以直接把灯的顶点着色器复制过来。片段着色器保证了灯的颜色一直是亮的,我们通过给灯定义一个常量的白色来实现:

#version 330 core
out vec4 color;void main()
{color = vec4(1.0f); //设置四维向量的所有元素为 1.0f
}

当我们想要绘制我们的物体的时候,我们需要使用刚刚定义的光照着色器绘制箱子(或者可能是其它的一些物体),让我们想要绘制灯的时候,我们会使用灯的着色器。在之后的教程里我们会逐步升级这个光照着色器从而能够缓慢的实现更真实的效果。

4、赋值Unfiorm

lightShader.Use();
modelLoc = glGetUniformLocation(lightShader.Program, "model");  //到 vs 找到那个 model 变量viewLoc = glGetUniformLocation(lightShader.Program, "view");  //到 vs 找到那个 view 变量projectionLoc = glGetUniformLocation(lightShader.Program, "projection");  //到 vs 找到那个 projection 变量// 对模型进行操作model = glm::translate(model, lightPos);  //平移光源model = glm::scale(model, glm::vec3(0.2f));  //缩放光源// 传入数据glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));lightColorLoc = glGetUniformLocation(lightShader.Program, "lightColor");glUniform3f(lightColorLoc, 1.0f, 1.0f, 1.0f);

调用灯的着色器

// 光源调色器Shader lightShader = Shader("light.vs", "light.frag");

4、定义一个光源的位置

使用这个灯立方体的主要目的是为了让我们知道光源在场景中的具体位置。我们通常在场景中定义一个光源的位置,但这只是一个位置,它并没有视觉意义。为了显示真正的灯,我们将表示光源的灯立方体绘制在与光源同样的位置。我们将使用我们为它新建的片段着色器让它保持它一直处于白色状态,不受场景中的光照影响。

我们声明一个全局vec3变量来表示光源在场景的世界空间坐标中的位置:

glm::vec3 lightPos(1.2f, 1.0f, 2.0f);

5、灯变换

然后我们把灯平移到这儿,当然我们需要对它进行缩放,让它不那么明显:


model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f));

6、绘制灯立方体

// 绘制光源对象glBindVertexArray(lightVAO);  //使用 VAO,直接绑定glDrawArrays(GL_TRIANGLES, 0, 36);  //画三角形,总共有 36 个顶点glBindVertexArray(0);

请把上述的所有代码片段放在你程序中合适的位置,这样我们就能有一个干净的光照实验场地了。

打包灯为一个类

1、生成灯的对象

 Light lightModel = Light();

2、设置灯位置并变换

// 设置光源坐标
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
//旋转角度为 0.01flightPos = glm::rotate(lightPos, 0.01f, glm::vec3(1.0f, 1.0f, 0.0f));

3、调用着色器

// 光源调色器Shader lightShader = Shader("light.vs", "light.frag");

4、渲染

// 画光源lightShader.Use();// 设置模型、视图和投影矩阵 uniformmodelLoc = glGetUniformLocation(lightShader.Program, "model");  //到 vs 找到那个 model 变量viewLoc = glGetUniformLocation(lightShader.Program, "view");  //到 vs 找到那个 view 变量projectionLoc = glGetUniformLocation(lightShader.Program, "projection");  //到 vs 找到那个 projection 变量// 对模型进行操作model = glm::translate(model, lightPos);  //平移光源model = glm::scale(model, glm::vec3(0.2f));  //缩放光源// 传入数据glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));lightColorLoc = glGetUniformLocation(lightShader.Program, "lightColor");glUniform3f(lightColorLoc, 1.0f, 1.0f, 1.0f);
lightModel.Draw(lightShader);

参考链接:

https://blog.csdn.net/Wonz5130/article/details/84098270

https://learnopengl-cn.readthedocs.io/zh/latest/02%20Li

南邮——计算机图像学——光照、冯氏光照模型相关推荐

  1. 南邮——计算机图像学——会动的立方体(变换)

    注意,当矩阵相乘时我们先写位移再写缩放变换的.建议您在组合矩阵时,先进行缩放操作,然后是旋转,最后才是位移,否则它们会(消极地)互相影响.比如,如果你先位移再缩放,位移的向量也会同样被缩放(译注:比如 ...

  2. 【OpenGL学习笔记⑧】——键盘控制正方体+光源【冯氏光照模型 光照原理 环境光照+漫反射光照+镜面光照】

    ✅ 重点参考了 LearnOpenGL CN 的内容,但大部分知识内容,小编已作改写,以方便读者理解. 文章目录 零. 成果预览图 一. 光照原理与投光物的配置 1.1 光照原理 1.2 投光物 二. ...

  3. Opengl-光照-基本光照-冯氏(千万好好理解后面所有的延伸基本都是基于这个的)

    前言 前面我们基本理解了怎么模拟光,怎么设置光的颜色以及物体的颜色来非常不生动形象的模拟光在计算机中.肯定在想,怎么能真的像生活中那样物体可以反光,然后有凉的地方也有不凉的地方,光也有强弱这种?其实前 ...

  4. matlab数学实验 南邮,matlab实验练习题(计算机)-南邮-matlab-数学实验大作业答案

    matlab实验练习题(计算机)-南邮-matlab-数学实验大作业答案 1"MATLAB"练习题练习题要求:抄题.写出操作命令.运行结果,并根据要求,贴上运行图.1.求的所有根. ...

  5. 请简述gouraud光照模型_OpenGL ES for Android(冯氏光照)

    冯氏光照模型 想要模拟真实世界的光照效果是比较困难的,我们使用一种叫做冯氏光照模型(Phong Lighting Model)的模型来实现近似的效果.冯氏光照模型的主要结构由3个分量组成:环境(Amb ...

  6. 南邮/南邮计算机考研经验贴2022

    南邮/南京邮电大学计算机考研经验贴2022 写在前面,22考研总分400+,数据结构140+,数据结构算是取得了比较好的成绩,下面介绍一下我这一年数据结构如何学习的. 有疑问可以进群一起交流. 一.书 ...

  7. OpenGL phong lighting冯氏光照的实例

    OpenGL phong lighting冯氏光照 先上图,再解答. 完整主要的源代码 源代码剖析 先上图,再解答. 完整主要的源代码 #include <shader.h> #

  8. 2018南邮全国计算机大赛,关于举办2018年南京邮电大学第十届数学建模竞赛通知...

    为了提高学生的综合素质,增强创新意识,培养学生应用数学知识解决实际工程问题的能力,激发学生学习数学的积极性,同时选拔参加2018年全国大学生数学建模竞赛和2019年美国大学生数学建模竞赛的队员,经学校 ...

  9. 杭电和南邮计算机考研哪个好考,南京邮电大学研究生,二本考南邮研究生!

    南邮的研究生好考么 你好!当然可以的啊 其实考博士主要还是要导师推荐 仅代表个人观点,不喜勿喷,谢谢. [15届考研生]教南邮和杭电的选择 想问一下了解情况的同学,学长,老师,研究生考试,南京邮电大学 ...

最新文章

  1. 心系AI的百度,这次能翻身吗?
  2. EF中Take和Skip的区别
  3. String类中IndexOf与SubString
  4. JavaScript基础之Number对象和Math对象
  5. 20那天android得知
  6. gdb的user-define command
  7. TOP命令及参数解析
  8. 微信小程序实现定位功能
  9. php 开源企业网站,TayCMS 免费开源企业网站建站系统 For PHP v1.8
  10. 163邮箱注册登录官网是什么?163邮箱登陆流程在这里
  11. 傅里叶分解、叠加《matplotlib学习》
  12. 如何使用python自动化查四六级成绩
  13. flac转换成mp3,flac转mp3方法
  14. 腾讯视频弹幕屏蔽js
  15. 平安的互联网布局新探索 平安天下通引领金融服务再升级
  16. 软件的版权和专利辨析
  17. 最近点对问题(蛮力法和分治法)
  18. 计算机通过变频器模拟输入控制电机正反转,用电脑控制的工频高压系统
  19. xinxin -用while循环计算皇帝的棋盘
  20. Qt实现Qchart的打印和打印预览的几种方法

热门文章

  1. oracle数据误删怎么恢复,Oracle数据误删了怎么恢复
  2. 程序员如何管理自己的代码
  3. CSS3 转换解析及制作立方体相册
  4. html打印页眉页脚_HTML5基本元素:页眉,导航和页脚
  5. 关于安卓上pcm文件转wav全是噪音解决办法
  6. 考研时间安排和考研内容
  7. 【高光谱、多光谱和全色图像融合】
  8. 如何钓鱼(如何钓鱼的方法)
  9. 标签类目体系(面向业务的数据资产设计方法论)-读书笔记8(完结)
  10. 轻松在线制作各种Logo标志