Closed Vigilans closed 5 years ago
来自 zjd 同学在暑假时网易游戏制作比赛里的一个「鲲」模型。
3D 模型利用 Blender 绘制而成,将 .blender 文件导出至某种 3D 模型文件,再导入至 WebGL 代码即可。
来自 WebGL Samples 网站里一个 Aquarium 样例中的「Meidum-Fish-A」模型。
Aquarium 源代码里直接提供高度与我们定义的 WebGL 模型协议相适配的 json 文件。
直接利用图形学群里提供的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;
}
@zjdx1998 有空的话,请在这里编辑一下跟踪球具体的实现方法。
程序中准备了以下全局变量:
rAngle
: number, rAxis
: vec3, rMatrix
: mat4trackingMouse
: bool, trackballMove
: boollastPos
, curX
, curY
, startX
, startY
: vec3与以下全局函数:
trackballView
: 请 @zjdx1998 解释。startMotion
: 开始运动跟踪球;stopMotion
: 停止运动跟踪球;mouseMotion
: 运动期间对跟踪球的控制。然后,添加对鼠标的监听逻辑:
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;
}
即实现了跟踪球效果。
每个动物在显示前大致经历了以下过程:
vertex shader
中,三维的顶点等属性通过逻辑变换映射到二维的clip space
上,这个空间的范围是[-1.0, 1.0] × [-1.0, 1.0],经过fragment shader
对顶点所包的像素上色后,返回给前端。clip space
位图放大,拉伸后显示在屏幕上。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;
}
}
uniform u_Direction
属性,指向动物的当前朝向就差不多了)。 这一部分就是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
函数接收如下两个参数:
mainLoop
开始执行时都会调用此回调函数,以达到每帧动画更新数据的效果。render
会为该回调提供一个this: Canvas
参数。因此,为了实现每帧更新u_MVMatrix
与u_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()
进行渲染了。
主题:几何对象与变换应用实例
具体任务
要求
选做