Vigilans / TypeGL

WebGL helper library for computer graphics course assignments
https://vigilans.github.io/TypeGL/
MIT License
4 stars 1 forks source link

研讨课 2 #3

Closed Vigilans closed 5 years ago

Vigilans commented 5 years ago

主题:几何对象与变换应用实例 

具体任务

要求

选做

Vigilans commented 5 years ago

3D动物设计

动物一

来自 zjd 同学在暑假时网易游戏制作比赛里的一个「鲲」模型。 image

3D 模型利用 Blender 绘制而成,将 .blender 文件导出至某种 3D 模型文件,再导入至 WebGL 代码即可。

动物二

来自 WebGL Samples 网站里一个 Aquarium 样例中的「Meidum-Fish-A」模型。 image

Aquarium 源代码里直接提供高度与我们定义的 WebGL 模型协议相适配的 json 文件

动物三

Vigilans commented 5 years ago

3D变换逻辑

直接利用图形学群里提供的MV.js

为了让 js 能够兼容在 ts 文件内,需要在 script.ts 中添加所要使用的函数的声明:

import "MV.js"

declare function mat4();
declare function flatten(v);
declare function rotateY(theta);
declare function rotate(theta, axis);
declare function scalem(x, y, z);
declare function mult(x, y);
declare function translate(x, y, z);

在 [vertex shader]() 文件中添加 uniform 变量 u_MVMatrix,将其与 a_Position 相乘即可实现变换:

uniform mat4 u_MVMatrix;
attribute vec4 a_Position;

void main() {
    v_Position = u_MVMatrix * a_Position;
    gl_Position = v_Position;
}

Vigilans commented 5 years ago

鼠标跟踪球逻辑

实现算法

@zjdx1998 有空的话,请在这里编辑一下跟踪球具体的实现方法。

代码应用

程序中准备了以下全局变量:

与以下全局函数:

然后,添加对鼠标的监听逻辑:

canvas.addEventListener("mousedown", event => { ..., startMotion(x, y); });
canvas.addEventListener("mouseup", event => { ..., stopMotion(x, y); });
canvas.addEventListener("mousemove", event => { ..., mouthMotion(x, y); render(); });

最后,在vertex shader中添加视角旋转矩阵:

uniform mat4 u_RMatrix;
attribute vec4 a_Position;

void main() {
    v_Position = u_RMatrix * a_Position;
    gl_Position = v_Position;
}

即实现了跟踪球效果。


Vigilans commented 5 years ago

动物交互逻辑

每个动物在显示前大致经历了以下过程:

vertex shader中的变换代码大致如下:

uniform mat4 u_MVMatrix;
uniform mat4 u_RMatrix;
attribute vec4 a_Position;

void main() {
    gl_Position = u_RMatrix * u_MVMatrix * a_Position;
}

可以看到,作为顶点数据的a_Position,在整个程序运行期间是不变的,除去让视角动起来的u_RMatrix,真正让动物在clip space动起来的只有u_MVMatrix

因此,我们要对每个动物单独维护一份用于更新u_MVMatrix的全局控制数据。 每个控制数据通过以下接口定义:

interface Controller {
    rotateAngle?: number; // 转动角度
    offsetX?: number; // X轴平移
    offsetY?: number; // Y轴平移
    scale?: number; // 缩放
}

接下来,我们定义各动物的控制数据数组和当前操作动物的指针:

let nowPtr: number;
let ctrl: [Controller, Controller]; // 我们只显示两个动物,就设置两个控制器

此时,我们就可以通过键盘监听来设置控制数据了:

document.onkeydown = function (event) {
    var e = event || window.event || arguments.callee.caller.arguments[0];
    // 1: 49; 2: 50 --> 按1/2选择当前指向动物
    if (e && e.keyCode == 49) {
        nowPtr = 0; // 指向第一个动物("鲲")
    }
    if (e && e.keyCode == 50) {
        nowPtr = 1; // 指向第二个动物("水族馆鱼")
    }
    // ↑ 38 ↓ 40 ← 37 →39 --> 方向键控制动物移动
    // z 90 c 67 --> z c 控制旋转
    // w 87 s 83 --> w s 控制缩放
    if (e && e.keyCode == 38) {
        ctrl[nowPtr].offsetY += 0.05;
    }
    if (e && e.keyCode == 40) {
        ctrl[nowPtr].offsetY -= 0.05;
    }
    if (e && e.keyCode == 37) {
        ctrl[nowPtr].offsetX -= 0.05;
    }
    if (e && e.keyCode == 39) {
        ctrl[nowPtr].offsetX += 0.05;
    }
    if (e && e.keyCode == 90) {
        ctrl[nowPtr].rotateAngle++;
    }
    if (e && e.keyCode == 67) {
        ctrl[nowPtr].rotateAngle--;
    }
    if (e && e.keyCode == 87) { //w 放大
        ctrl[nowPtr].scale += 0.005;
    }
    if (e && e.keyCode == 83) { //s 缩小
        ctrl[nowPtr].scale -= 0.005;
        if (ctrl[nowPtr].scale < 0) ctrl[nowPtr].scale = 1;
    }
}

Vigilans commented 5 years ago

模型加载与渲染

这一部分就是async main函数所完成的事了。

加载

本次研讨中,加载的均是已经按我们定义的协议解释好的JSON文件。 按以下方式来从外部文件中读取JSON:

let data = JSON.parse(await (await fetch('filename.json')).text()));

之后,将获取的JSON对象进一步处理,将其转成完全适配于{ [key: string]: WebGLAttribute }的形式:

// 以下例子是读取水族馆鱼JSON后的处理
// 由于鲲的数据在之前已完全处理成合乎协议的JSON数据,因此直接使用即可

// 读取的json文件是一个model数组,每个model里又有 texture/fields 等多种属性
// 其中,fields就是由 { 属性名: 属性数据 } 组成的attribute的键值列表。
let auqa_fish = data.models[0].fields as { [key: string]: WebGLAttribute };
// 删去 binormal, texCoord, tangent 这三个我们用不到的attribute
delete aqua_fish.binormal, aqua_fish.texCoord, aqua_fish.tangent;

接下来,便可直接通过Canvas类的newObject方法创建WebGL渲染对象了:

c.newObject(
    await c.sourceByFile("fish.glslv", "fish.glslf"), // 从文件中读取shader代码
    c.gl.TRIANGLES, // 以三角形方式绘制
    aqua_fish, // aqua_fish已是合乎协议的对象
    { // uniform数据的键值列表
        u_MVMatrix: flatten(scalem(0.2, 0.2, 0.2)), // 这里可以设置下动物的初始放缩倍数
        u_RMatrix: flatten(mat4()),
        u_Color: [1, 0.5, 0, 1]
    }
);

创建好的WebGLRenderingObject将会同时存进Canvas中,供下面的render()函数使用。

渲染

在该次研讨中,为了实现动画效果,将 Canvas类中的render函数 进行了改进。render函数中的主要逻辑现由一个闭包函数mainLoop实现,通过requestAnimeFrame(mainLoop)即可达到动画效果。

现在,render函数接收如下两个参数:

因此,为了实现每帧更新u_MVMatrixu_RMatrix,我们往render中传入如下回调:

let callback = (cv: Canvas) => {
    let uniforms = cv.objectsToDraw[nowPtr].uniforms; // 只更新当前控制的动物的u_MVMatrix
    let ctr = ctrl[nowPtr];
    let S = scalem(ctr.scale, ctr.scale, ctr.scale);
    let R = rotateY(ctr.rotateAngle);
    let T = translate(ctr.offsetX, ctr.offsetY, 0);
    uniforms.u_MVMatrix = flatten(mult(mult(S, R), T));
    uniforms.u_Color = [1, 0.5, 0, 1]; // 颜色是随便定义的

    rMatrix = mult(rMatrix, rotate(rAngle, rAxis));
    for (let { uniforms } of cv.objectsToDraw) {
        uniforms.u_RMatrix = flatten(rMatrix);  // 但是要更新所有动物的u_RMatrix,因为转动的是视角
    }
}

此外,由于render函数不止在async main中被调用,因此将render + callback的调用再次封装一下:

let render = () => c.render(callback, true);

此后便可直接调用render()进行渲染了。

Vigilans commented 5 years ago

第二次演示PPT: https://github.com/Vigilans/CG-18-19-2/blob/master/defense/2.i-is-fish.pptx