GAMES202-作业0-框架解读

这一系列博客主要是为了记录自己对GAMES202课程的作业完成情况.

本博客主要是因为自己个人对于JavaScript以及HTML了解不多, 需要专门解读作业0的框架.

目前还没完全写完, 但是我觉得足够了, 有些没看的后面等我看了再及时更新.

解读参考:

  1. WebGL API
  2. Three.js
  3. gl-matrix
  4. WebGLFundamentals

注:: 1. 强烈建议阅读此代码框架是对照着官网实例来看, 或者自己先通看一遍. 2. 在运行过程中老是会存在模型加载不出来的方式, 这里有一个解决方案


index.html

参考该链接所知, 在html中我们需要给出canvas元素

因此我们可以看到在这个项目的index.html中给出了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
<head>
<style>
...
#glcanvas {
top: 0;
width: 100%;
height: 100%;
}
</style>
</head>

<body>
<canvas id="glcanvas"></canvas>
</body>
</html>

中间的各种script 主要看engine.js, 其中包含有main函数:


engine.js

在这个js文件中包含了main()函数. 在main中首先获取了canvas

这里为了方便, 先忽略掉整个有关gui的代码

注: 这里很多都有相应的文档说明, 因此我这里尽量列出文档.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API/Tutorial/Getting_started_with_WebGL
// 参照上文的指导链接
const canvas = document.querySelector('#glcanvas');
canvas.width = window.screen.width;
canvas.height = window.screen.height;
const gl = canvas.getContext('webgl');
if (!gl) {
alert('Unable to initialize WebGL. Your browser or machine may not support it.');
return;
}

// https://threejs.org/docs/#api/zh/cameras/PerspectiveCamera
// https://threejs.org/docs/#examples/zh/controls/OrbitControls
// camera和相应控制器的参考链接
// perspective camera: 透视相机: 这个摄像机使用透视投影来进行投影
const camera = new THREE.PerspectiveCamera(75, gl.canvas.clientWidth / gl.canvas.clientHeight, 0.1, 1000);
// 轨道控制器:使得相机围绕目标进行轨道运动
const cameraControls = new THREE.OrbitControls(camera, canvas);
cameraControls.enableZoom = true;
// ... 省略相机参数设置代码

function setSize(width, height) {
camera.aspect = width / height;
// 更新摄像机投影矩阵, 在任何参数被改变后必须被调用
camera.updateProjectionMatrix();
}
setSize(canvas.clientWidth, canvas.clientHeight);
window.addEventListener('resize', () => setSize(canvas.clientWidth, canvas.clientHeight));

// camera的继承体系: PerspectiveCamera <- Camera <- Object, 这个position()的方法是在 Object 中的
camera.position.set(cameraPosition[0], cameraPosition[1], cameraPosition[2]);
cameraControls.target.set(0, 1, 0);
//...

接下来, 有两个句代码分别创建了pointLightWebGLRender, 这里最重要的就是进入了WebGLRenderrender()函数.

值得一提的是: 这里的addLight()方法:

addLight(light) { this.lights.push({ entity: light, meshRender: new MeshRender(this.gl, light.mesh, light.mat) }); }

其实是为Light创建了一个meshRender, meshRender后面会说

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   // 有关 PointLight 以及 loadOBJ 的解读请看附录
const pointLight = new PointLight(250, [1, 1, 1]);
const renderer = new WebGLRenderer(gl, camera);
renderer.addLight(pointLight);
loadOBJ(renderer, 'assets/mary/', 'Marry');

// 主循环, 在主循环调用 renderer 的render()函数
function mainLoop(now) {
cameraControls.update();

renderer.render(guiParams);
requestAnimationFrame(mainLoop);
}
//https://developer.mozilla.org/zh-CN/docs/Web/API/window/requestAnimationFrame
requestAnimationFrame(mainLoop);

WebGLRenderer.js(渲染器)

这个文件按照参考链接中的教程2的后半部分即可

这些操作很多都是固定操作,

两层循环中: 1. 外层循环为对light也即光源进行Render; 2. 然后进入内层,对每一个mesh进行Render.

draw()函数 这里对每个mesh调用了draw()函数, 在本框架中, 所有mesh包括light其所使用的Render均为MeshRender.js中定义的.

对于光源: 默认在addLight时就是用的MeshRender

对于Mesh, 则在loadOBJ.jsloadOBJ()添加(在engine.js)中被调用(关于loadOBJ的解读见附录).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
render(guiParams) {
const gl = this.gl;
// 这些函数都在 WebGLRenderingContext下
// https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/enable
gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear to black, fully opaque
gl.clearDepth(1.0); // Clear everything
// 激活深度比较, 并且更新深度缓冲区
gl.enable(gl.DEPTH_TEST); // Enable depth testing
// 更新深度缓冲区的比较函数设置为 小于等于
gl.depthFunc(gl.LEQUAL); // Near things obscure far things

// 清空了颜色缓冲区和深度缓冲区
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

// Handle light
const timer = Date.now() * 0.00025;
let lightPos = [ Math.sin(timer * 6) * 100,
Math.cos(timer * 4) * 150,
Math.cos(timer * 2) * 100 ];

if (this.lights.length != 0) {
for (let l = 0; l < this.lights.length; l++) {
let trans = new TRSTransform(lightPos);
this.lights[l].meshRender.draw(this.camera, trans);

for (let i = 0; i < this.meshes.length; i++) {
const mesh = this.meshes[i];

const modelTranslation = [guiParams.modelTransX, guiParams.modelTransY, guiParams.modelTransZ];
const modelScale = [guiParams.modelScaleX, guiParams.modelScaleY, guiParams.modelScaleZ];
let meshTrans = new TRSTransform(modelTranslation, modelScale);

// tell webgl to use this program when drawing
this.gl.useProgram(mesh.shader.program.glShaderProgram);
// 更新 lightPos
this.gl.uniform3fv(mesh.shader.program.uniforms.uLightPos, lightPos);
mesh.draw(this.camera, meshTrans);
}
}
} else {
// Handle mesh(no light)
for (let i = 0; i < this.meshes.length; i++) {
const mesh = this.meshes[i];
let trans = new TRSTransform();
mesh.draw(this.camera, trans);
}
}
}

MeshRender.js

同理, 请参考教程2

注:: 在这个类的构造函数中调用了this.material.compile(gl), 实现了两个shader的编译. 因此, 在draw()函数结束就算是完成了整个框架.

这个文件中包含许多的if语句, 其是一一对应的, 主要是为了进行Buffer的绑定.

这里面对于整个框架理解比较关键的是uniformattribute的绑定. 这个步骤就是为了向vertex shaderFragment shader传递JS的变量.

其中除了Material.js中定义的四个uniform是通用的(也即我们知道类型), 其他的都需要自己设置类型. 所以才会有后面的循环(这里可以看出常用的6个类型)

这里摘抄三个变量的绑定方式:

Vertex shaders need data. They can get that data in 3 ways.

  1. Attributes (data pulled from buffers)
  2. Uniforms (values that stay the same for all vertices of a single draw call)
  3. Textures (data from pixels/texels)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
draw(camera, transform) {
const gl = this.gl;

let modelViewMatrix = mat4.create();
let projectionMatrix = mat4.create();

camera.updateMatrixWorld();
// https://threejs.org/docs/?q=mat#api/zh/math/Matrix4
// 将当前矩阵翻转为它的逆矩阵
mat4.invert(modelViewMatrix, camera.matrixWorld.elements);
// 下面这两个分别为平移和缩放, 一般当作一个
mat4.translate(modelViewMatrix, modelViewMatrix, transform.translate);
mat4.scale(modelViewMatrix, modelViewMatrix, transform.scale);
mat4.copy(projectionMatrix, camera.projectionMatrix.elements);

// tell WebGl how to pull out the positions from the position
// buffer into the vertexPosition attribute
if (this.mesh.hasVertices) {
// pull out 2 values per iteration
const numComponents = 3;
const type = gl.FLOAT;
const normalize = false;
// how many bytes to get from one set of values to the next
const stride = 0;
const offset = 0;
gl.bindBuffer(gl.ARRAY_BUFFER, this.#vertexBuffer);
gl.vertexAttribPointer(
this.shader.program.attribs[this.mesh.verticesName],
numComponents,
type,
normalize,
stride,
offset);
// https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/enableVertexAttribArray
// 无论怎样, 都需要你使用 enableVertexAttribArray()方法来激活每一个属性以便使用
gl.enableVertexAttribArray(
this.shader.program.attribs[this.mesh.verticesName]);
}
// ... ...

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.#indicesBuffer);
gl.useProgram(this.shader.program.glShaderProgram); // tell WebGL to use our program when drawing

// https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/uniformMatrix
gl.uniformMatrix4fv(
this.shader.program.uniforms.uProjectionMatrix,
false,
projectionMatrix);
gl.uniformMatrix4fv(
this.shader.program.uniforms.uModelViewMatrix,
false,
modelViewMatrix);

// Specific the camera uniforms
gl.uniform3fv(
this.shader.program.uniforms.uCameraPos,
[camera.position.x, camera.position.y, camera.position.z]);

for (let k in this.material.uniforms) {
if (this.material.uniforms[k].type == 'matrix4fv') {
gl.uniformMatrix4fv(
this.shader.program.uniforms[k],
false,
this.material.uniforms[k].value);
} else if (this.material.uniforms[k].type == '3fv') {
gl.uniform3fv(
this.shader.program.uniforms[k],
this.material.uniforms[k].value);
} else if (this.material.uniforms[k].type == '1f') {
gl.uniform1f(
this.shader.program.uniforms[k],
this.material.uniforms[k].value);
} else if (this.material.uniforms[k].type == '1i') {
gl.uniform1i(
this.shader.program.uniforms[k],
this.material.uniforms[k].value);
} else if (this.material.uniforms[k].type == 'texture') {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.material.uniforms[k].value.texture);
gl.uniform1i(this.shader.program.uniforms[k], 0);
}
}

{
const vertexCount = this.mesh.count;
const type = gl.UNSIGNED_SHORT;
const offset = 0;
// https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/drawElements
gl.drawElements(gl.TRIANGLES, vertexCount, type, offset); // 三角形单元
}
}

Material

这个类是个材料基类, 集中包含了我们要传给WebGl的参数: uniforms

其中, 这四个uniforms是必须基本上都要有的.

1
this.#flatten_uniforms = ['uModelViewMatrix', 'uProjectionMatrix', 'uCameraPos', 'uLightPos'];

至于其他的参数, 则需要我们自己定义派生类, 来向这里添加.(添加到uniforms中), 一定要注意自己添加的请标明类型

注: 其实可以参考作业0让我们写的派生类

至于编译这一步, 是固定步骤, 我上面给的参考教程中也有示例.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Material {
// https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API/Data#uniforms
// uniforms 与 attribute 的区别可以参考上面的链接.
#flatten_uniforms;
#flatten_attribs;
#vsSrc;
#fsSrc;
// Uniforms is a map, attribs is a Array
constructor(uniforms, attribs, vsSrc, fsSrc) {
this.uniforms = uniforms;
this.attribs = attribs;
this.#vsSrc = vsSrc;
this.#fsSrc = fsSrc;

this.#flatten_uniforms = ['uModelViewMatrix', 'uProjectionMatrix', 'uCameraPos', 'uLightPos'];
for (let k in uniforms) {
this.#flatten_uniforms.push(k);
}
this.#flatten_attribs = attribs;
}

setMeshAttribs(extraAttribs) {
for (let i = 0; i < extraAttribs.length; i++) {
this.#flatten_attribs.push(extraAttribs[i]);
}
}

compile(gl) {
return new Shader(gl, this.#vsSrc, this.#fsSrc,
{
uniforms: this.#flatten_uniforms,
attribs: this.#flatten_attribs
});
}
}

作业0---Shader

phongShader

作业0的任务是写出phongShader, 有了之前的基础, 明确好uniform``attribute以及varying的区别即可明白所有变量的作用.

至于主函数: vertexShader负责完成modelView以及投影变换; fragmentShader则主要是套公式.

glsl所提供的函数可以在glsl docbook上来找.

1
2
3
// Blinn-Phong Reflection Model
// L = La + Ld + Ls
// = kaIa + kd(I/r^2)max(0,n dot l) + ks(I/r^2)max(o,n dot h)^p

PhongMaterial

继承自Material, 主要是完成编译Shader并拿到你所创建的uniform的位置.

注:

  1. uniform的绑定和attribute的buffer的创建是在MeshRender.js中完成的
  2. attribute是后添加的, 再创建材料时不用添加(是先和buffer绑定后再添加的).

loadOBJ

创建所创建的PhongMaterial, 并创建MeshRender

附录(其他文件):

Shader.js

这个文件完全按照实例教程中的Add 2D content to a WebGl Context来即可

Light

1
2
3
4
5
6
7
8
9
10
11
12
13
// 这里 Emissive Mater继承自 Material
// 其需要四个参数, 添加自己特别的uniforms
class EmissiveMaterial extends Material {
constructor(lightIntensity, lightColor) {
super({
'uLigIntensity': { type: '1f', value: lightIntensity },
'uLightColor': { type: '3fv', value: lightColor }
}, [], LightCubeVertexShader, LightCubeFragmentShader);

this.intensity = lightIntensity;
this.color = lightColor;
}
}

LoadOBJ

这里用到的很多类都是Three.js中的库, 详情可以参考代码中给出的参考链接.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// https://threejs.org/docs/#examples/zh/loaders/OBJLoader
// https://threejs.org/docs/#api/zh/loaders/managers/LoadingManager
function loadOBJ(renderer, path, name) {

const manager = new THREE.LoadingManager();
// 当一个过程完成的时候调用这个函数
manager.onProgress = function (item, loaded, total) {
console.log(item, loaded, total);
};


function onProgress(xhr) {
if (xhr.lengthComputable) {
const percentComplete = xhr.loaded / xhr.total * 100;
console.log('model ' + Math.round(percentComplete, 2) + '% downloaded');
}
}
function onError() { }

// https://threejs.org/docs/#examples/en/loaders/MTLLoader
// mtl: The Material Template Library format(MTL)
// Parse a mtl text structure and return a MTLLoader.MaterialCreator instance.
new THREE.MTLLoader(manager)
.setPath(path)
// .load(url:String, onLoad: Function, onProgress: Function. onError:Function)
.load(name + '.mtl', function (materials) {
materials.preload();
new THREE.OBJLoader(manager)
.setMaterials(materials)
.setPath(path)
.load(name + '.obj', function (object) {
// 在对象以及后代中执行的回调函数
// 相当于一个循环
object.traverse(function (child) {
if (child.isMesh) {
// https://threejs.org/docs/?q=Mesh#api/zh/objects/Mesh
let geo = child.geometry;
let mat;
if (Array.isArray(child.material)) mat = child.material[0];
else mat = child.material;

var indices = Array.from({ length: geo.attributes.position.count }, (v, k) => k);
let mesh = new Mesh({ name: 'aVertexPosition', array: geo.attributes.position.array },
{ name: 'aNormalPosition', array: geo.attributes.normal.array },
{ name: 'aTextureCoord', array: geo.attributes.uv.array },
indices);

let colorMap = null;
if (mat.map != null) colorMap = new Texture(renderer.gl, mat.map.image);
// MARK: You can change the myMaterial object to your own Material instance

let myMaterial = new PhongMaterial(mat.color.toArray(), colorMap, mat.specular.toArray(),
renderer.lights[0].entity.mat.intensity);

let meshRender = new MeshRender(renderer.gl, mesh, myMaterial);
renderer.addMesh(meshRender);
}
});
}, onProgress, onError);
});
}

PointLight

PointLight构造函数接收两个参数, 一个是光照强度(lightIntensity), 另一个是光的颜色(lightColor):

  • 默认 meshcube(): 就是大小位于[-1,1]的小立方体
  • Light的材质是 EmissiveMaterial: 接收两个参数
1
2
3
4
5
6
class PointLight {
constructor(lightIntensity, lightColor) {
this.mesh = Mesh.cube(); // 位于 Mesh.js中的一个静态成员函数, 就是生成一个小立方体, 这里不在展开
this.mat = new EmissiveMaterial(lightIntensity, lightColor); // 位于 Light.js
}
}