GAMES 202 作业0 完成方案,潜在问题与代码解读:

目录

1.  运行方案

2.  代码完成方案与解读

I.     着色器的实现

II.     (Phong) Material材料类的实现和解读

 3. 可能出现的问题

I. 模型偶尔加载不出来(刷新若干次后恢复正常)

II. 模型完全加载不出来(黑屏/只有光源/...)


文首指路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值后,代码分三步分别计算了环境光颜色,漫反射颜色,镜面反射颜色。通过计算以下公式:

L=L_a+L_d+L_s=k_aI_a+k_d\cdot \frac{I}{r^2}\cdot \max(n\cdot l,0)+k_s\cdot \frac{I}{r^2} \cdot\max(r\cdot v,0)^p

整合即可得到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. 模型完全加载不出来(黑屏/只有光源/...)

可以就以下两种可能的方向进行检查:

  • 很有可能是在复制代码时引入了空格,导致路径错误等问题。请着重检查修改过的文件。
  • 也可能是网络问题,导致某些链接加载不出来,指路另一篇介绍,可参考他的解决方式。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值