目录
文首指路GAMES202主页,作业0百度网盘
GAMES202 第0次作业还是相对简单的,只要按照文件的步骤一步一步跟着做,基本上没有什么难度。本文的目的分以下几点:
1. 运行方案
框架中提供了两种运行方案,分别是“Visual Studio Code 插件搭建本地服务器”和“Node.js 搭建本地服务器 ”,我认为前者使用vscode更简单方便,因此本文只介绍该方法。
如果还没有装vscode,可以参考vscode安装教程中的安装方法,该文中步骤还是非常详细的。
安装完后在 Visual Studio Code 中安装插件 Live Server (如下图)
安装完插件后在编辑器任意界面使用 Ctrl+Shift+P 调出命令行窗口,输入 Live Server: Open with Live Server ,浏览器自动打开指定地址的本地服务器,至此作业框架的运行就算完成了。
2. 代码完成方案与解读
具体完成方案可以按照文档中的代码写入并完成整个代码框架,本文主要解读一下文档中的代码具体含义:
I. 着色器的实现
首先我们增加了Phong 模型的 vertexShader 的定义:
const PhongVertexShader = `
attribute vec3 aVertexPosition;
attribute vec3 aNormalPosition;
attribute vec2 aTextureCoord;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying highp vec2 vTextureCoord;
varying highp vec3 vFragPos;
varying highp vec3 vNormal;
void main(void) {
vFragPos = aVertexPosition;
vNormal = aNormalPosition;
gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aVertexPosition , 1.0);
vTextureCoord = aTextureCoord;
}`;
上述代码介绍了顶点着色器的实现,其中
- 传入的信息是顶点的基本信息(attribute后的变量,顶点坐标/法线方向/纹理坐标)
- MV矩阵和P矩阵由uniform 语句传入作为全局变量,用于决定该顶点在屏幕上的哪个像素。
- 传出的信息同样是顶点的信息,本代码直接赋值为传入信息、
实现过程中
gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aVertexPosition , 1.0);
该语句根据MVP矩阵的定义得到,不熟悉的可以回顾一下GAMES101的第四节课,有详细介绍MVP矩阵的推导。
接着在同一个文件里增加了对 Phong 模型中片元着色器的实现,我们分段来进行解读:
ifdef GL_ES
precision mediump float;
#endif
上述代码声明了默认的浮点数精度,以便在不同的硬件平台上保持一致性。接下来,着色器声明了以下全局变量(uniform)和从顶点着色器中传入的变量(varying):(含义均已在注释中)
// Uniforms
uniform sampler2D uSampler; // 纹理
uniform vec3 uKd; // 漫反射系数(*)
uniform vec3 uKs; // 镜面反射系数(*)
uniform vec3 uLightPos; // 光源坐标
uniform vec3 uCameraPos; // 摄像机坐标
uniform float uLightIntensity; // 光源强度
uniform int uTextureSample; // 纹理采样标志
varying highp vec2 vTextureCoord; // 纹理坐标
varying highp vec3 vFragPos; // 片段位置
varying highp vec3 vNormal; // 法线
在主函数中:
void main(void) {
vec3 color;
// 若使用纹理,则color赋值为gamma校正后的纹理颜色,否则使用漫反射系数颜色
if (uTextureSample == 1) {
color = pow(texture2D(uSampler, vTextureCoord).rgb, vec3(2.2));
} else {
color = uKd;
}
// 环境光颜色计算: K_a*L_a
vec3 ambient = 0.05 * color;
// 单位化光线/法线方向,并计算漫反射各分量
vec3 lightDir = normalize(uLightPos - vFragPos);
vec3 normal = normalize(vNormal);
float diff = max(dot(lightDir, normal), 0.0);
float light_atten_coff = uLightIntensity / length(uLightPos - vFragPos);
// 计算漫反射颜色 L_d=k_d*(I/r^2)*max(0,n·l)
vec3 diffuse = diff * light_atten_coff * color;
// 计算视线方向,反射方向,并计算镜面反射各分量
vec3 viewDir = normalize(uCameraPos - vFragPos);
float spec = 0.0;
vec3 reflectDir = reflect(-lightDir, normal);
spec = pow(max(dot(viewDir, reflectDir), 0.0), 35.0);
// 计算镜面反射颜色 L_s=k_s*(I/r^2)*(max(0,n·h)^p)
vec3 specular = uKs * light_atten_coff * spec;
// 计算最终颜色进行gamma校正
gl_FragColor = vec4(pow((ambient + diffuse + specular), vec3(1.0 / 2.2)), 1.0);
}
在计算完color值后,代码分三步分别计算了环境光颜色,漫反射颜色,镜面反射颜色。通过计算以下公式:
整合即可得到Phong模型下片元的颜色。
值得注意的是此代码实现的是Phong模型而非Bling-Phong模型的结果,不过在上述代码中通过简单更改即可实现Bling-Phong模型的,笔者使用以下代码生成Bling-Phong模型的镜面反射颜色:
vec3 viewDir = normalize(uCameraPos - vFragPos);
float spec = 0.0;
vec3 halfDir = normalize(viewDir + lightDir);
spec = pow (max(dot(normal , halfDir), 0.0), 32.0);
vec3 specular = uKs * light_atten_coff * spec;
II. (Phong) Material材料类的实现和解读
在实现Phong Material之前,先看一下Material的代码:
class Material {
// 私有成员变量,用于存储统一变量和属性变量
#flatten_uniforms;
#flatten_attribs;
#vsSrc;
#fsSrc;
...
Material中构造了四个私有成员变量,其中:
- #flatten_uniforms: 包含传递给着色器程序的uniform变量的名称数组。
- #flatten_attribs: 包含传递给着色器程序的attribute变量的名称数组。
- #vsSrc 和 #fsSrc: 存储顶点/片段着色器的源码。
/**
* 构造函数
* @param {Object} uniforms - 存储uniform变量
* @param {Array} attribs - 存储attribute变量
* @param {string} vsSrc - 顶点着色器源码
* @param {string} fsSrc - 片段着色器源码
*/
constructor(uniforms, attribs, vsSrc, fsSrc) {
// 赋值给实例
this.uniforms = uniforms;
this.attribs = attribs;
this.#vsSrc = vsSrc;
this.#fsSrc = fsSrc;
// 展平uniform和attribute变量名列表
this.#flatten_uniforms = ['uModelViewMatrix', 'uProjectionMatrix', 'uCameraPos', 'uLightPos'];
for (let k in uniforms) {
this.#flatten_uniforms.push(k);
}
this.#flatten_attribs = attribs;
}
构造函数中给实例赋值了 uniform / attribute 变量和着色器源码,并且构建了 #flatten_attribs 和 #flatten_uniforms 两个变量名数组。
但是有个疑问出现了,为什么需要同时在类里储存 uniform / attribute和 #flatten_attribs / #flatten_uniforms呢?
实际上,两个" # "字符开头的数组所存储的名称字符串,是为了在编译着色器时告诉 WebGL 这些变量需要传递到着色器中。而实际传递的值(例如矩阵、向量等)是在使用这个Material类编译着色器后,在 WebGL 渲染循环中通过gl.uniform*系列方法传递的。因此两者区别在于:
- uniforms 关注的是变量的实际值,这些值会在渲染过程中动态改变。
- #flatten_uniforms 关注的是变量的名称列表,用于一次性告知着色器需要哪些变量,这个列表在着色器编译后通常不再变化。
接下来在Material中,增加了两个函数分别用于:设置网格的额外属性和编译着色器程序。
/**
* 设置网格的额外属性
* @param {Array} extraAttribs - 额外的attribute变量
*/
setMeshAttribs(extraAttribs) {
for (let i = 0; i < extraAttribs.length; i++) {
this.#flatten_attribs.push(extraAttribs[i]);
}
}
/**
* 编译着色器程序
* @param {WebGLRenderingContext} gl - WebGL渲染上下文
* @returns {Shader} 返回一个Shader实例
*/
compile(gl) {
return new Shader(gl, this.#vsSrc, this.#fsSrc, {
uniforms: this.#flatten_uniforms,
attribs: this.#flatten_attribs
});
}
}
在看完Material文件后,我们再看看PhongMaterial.js中需要补充的内容:
class PhongMaterial extends Material {
/**
* 创建一个 PhongMaterial 的实例。
* @param {vec3f} color 材质的颜色
* @param {Texture} colorMap 材质的纹理对象
* @param {vec3f} specular 材质的镜面反射系数
* @param {float} intensity 光照强度
* @memberof PhongMaterial
*/
constructor(color, colorMap, specular, intensity) {
let textureSample = 0; // 初始化纹理采样标志
if (colorMap != null) {
textureSample = 1;
// 使用super调用父类构造函数
super({
'uTextureSample': { type: '1i', value: textureSample },
'uSampler': { type: 'texture', value: colorMap },
'uKd': { type: '3fv', value: color },
'uKs': { type: '3fv', value: specular },
'uLightIntensity': { type: '1f', value: intensity }
}, [], PhongVertexShader, PhongFragmentShader);
} else {
// 如果不存在纹理,仅传递必要的统一变量
super({
'uTextureSample': { type: '1i', value: textureSample },
'uKd': { type: '3fv', value: color },
'uKs': { type: '3fv', value: specular },
'uLightIntensity': { type: '1f', value: intensity }
}, [], PhongVertexShader, PhongFragmentShader);
}
}
}
简单来说,在Material的子类的PhongMaterial的构造函数中,使用super调用了其父类的构造函数,并对父类构造函数中所需的uniform变量赋值。
- 当不存在纹理时,将utextureSample赋值为0,并只需设置光强、漫/镜面反射系数即可。
- 否则,将utextureSample赋值为1,除了设置以上变量外,额外传入一张纹理图片作为uSampler
构建完PhongMaterial类后,我们在loadOBJ文件中40~56行替换成以下代码来生成PhongMaterial的实例:
let myMaterial = new PhongMaterial(mat.color.toArray(), colorMap , mat.specular.toArray(),renderer.lights[0].entity.mat. intensity);
最后更改index.html来将PhongMaterial类导入项目,如果按照以上步骤完成后,在编辑器任意界面使用 Ctrl+Shift+P 调出命令行窗口,输入 Live Server: Open with Live Server,不出意外的话就能看到结果啦!
3. 可能出现的问题
I. 模型偶尔加载不出来(刷新若干次后恢复正常)
这是材质加载的问题,在loadOBJ.js中,有这样一个语句materials.preload();这个语句会进行资源的异步加载,但随便将该materials进行了传参,此时materials可能还没加载完成,导致在渲染模型的时候无法正确应用材质。解决方法是在Index.html中加入以下代码声明材质的预加载:
<link rel="preload" href="/assets/mary/MC003_Kozakura_Mari.png" as="image" type="image/png" crossorigin/>
II. 模型完全加载不出来(黑屏/只有光源/...)
可以就以下两种可能的方向进行检查:
- 很有可能是在复制代码时引入了空格,导致路径错误等问题。请着重检查修改过的文件。
-
也可能是网络问题,导致某些链接加载不出来,指路另一篇介绍,可参考他的解决方式。