大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 WebGL 的基础开始说起』系列文章的第 14 篇文章。本系列文章从 WebGL 基本运作机制以及使用的原理开始介绍,最後建构出绘制 3D、光影效果之网页。介绍完 WebGL 运作方式与 2D transform 後,本章节讲述的是建构、transform 并渲染 3D 物件,如果在阅读本文时觉得有什麽未知的东西被当成已知的,可能可以在前面的文章中找到相关的内容
有了 perspective 投影以及加入反向 cameraMatrix
的 viewkMatrix
,我们拥有一套系统来模拟现实生活中眼睛、相机在想要的位置进行成像,方法也跟场景中的 3D 物件类似:在 cameraMatrix
中加入想要的 transform。同时也可以来比较一下 orthogonal (live 版本)与 perspective (live 版本)投影的差别,最大的差别大概就是物件在不同 z 轴位置时成像的『远近』了:
matrix4.lookAt()
但是相机往往不会直直地往 -z 方向看,而且常常要对着某个目标,因此再介绍一个常用的 function:
matrix4.lookAt(
cameraPosition,
target,
up,
)
其前两个参数意义蛮明显的,分别是相机要放在什麽位置、看着的目标;接着不知道读者在阅读这系列文章时,有没有常常歪着头看萤幕,对,up
就是控制这件事情,如果传入 [0, 1, 0]
即表示正正的看,没有歪着头看
关於 matrix4.lookAt()
的实做,想当然尔会有 cameraPosition
的平移,因此矩阵的一部分已经知道:
[
?, ?, ?, 0,
?, ?, ?, 0,
?, ?, ?, 0,
cameraPosition.x, cameraPosition.y, cameraPosition.z, 1,
]
剩下的 ?
部份则是相机的方向,首先需要知道从 cameraPosition
到 target
的方向向量 k
,接着拿 up
与 k
向量做外积得到与两者都垂直的向量 i
,最後拿 k
, i
做外积得到与两者都垂直的向量 j
,我们就得到 3Blue1Brown 这部 Youtube 影片 -- 三维线性变换 所说的变换矩阵的『基本矢量』,同时为了避免缩放,i
, j
, k
都应为单位向量
在上面这段提到 3 个新的运算:向量差异、外积、单位矩阵化,根据公式在 lib/matrix.js
中实做这几个 function:
export const matrix4 = {
// ...
subtractVectors: (a, b) => ([
a[0] - b[0], a[1] - b[1], a[2] - b[2]
]),
cross: (a, b) => ([
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]),
normalize: v => {
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
// make sure we don't divide by 0.
if (length > 0.00001) {
return [v[0] / length, v[1] / length, v[2] / length];
} else {
return [0, 0, 0];
}
},
// ...
}
最後把 matrix4.lookAt()
实做起来:
export const matrix4 = {
// ...
lookAt: (cameraPosition, target, up) => {
const kHat = matrix4.normalize(
matrix4.subtractVectors(cameraPosition, target)
);
const iHat = matrix4.normalize(matrix4.cross(up, kHat));
const jHat = matrix4.normalize(matrix4.cross(kHat, iHat));
return [
iHat[0], iHat[1], iHat[2], 0,
jHat[0], jHat[1], jHat[2], 0,
kHat[0], kHat[1], kHat[2], 0,
cameraPosition[0],
cameraPosition[1],
cameraPosition[2],
1,
];
},
// ...
}
一样,电脑中与上方 3Blue1Brown 影片中数学惯例用的行列是相反的,同时可能会有两个疑问:最核心的相机方向 normalize(subtractVectors(cameraPosition, target))
为何是 kHat
? 而且 subtractVectors()
算出来的向量其实是从 target
到 cameraPosition
的方向?matrix4.perspective()
看着的方向是 -z,要把 -z 转换成 target
到 cameraPosition
的方向,这个转换就是 kHat
,又因 "-"z 的关系使得 subtractVectors()
的参数得反向
matrix4.lookAt()
回到主程序,让 cameraMatrix
使用 matrix4.lookAt()
产生的矩阵:
const cameraMatrix = matrix4.lookAt([250, 0, 400], [250, 0, 0], [0, 1, 0]);
笔者使相机位置与之前 translate 时的位置相同,而且目标会使得相机平平的往 -z 看过去,因此改完之後不会有变化:
为了看出 matrix4.lookAt()
的功能,接下来加入相机位置的控制,不过这次不要再用 <input type='range' />
的 slider 了,笔者决定使用键盘 WASD/上下左右、用滑鼠/触控按住画面上下左右半部来移动相机
因为要接入的事件很多,而且这些事件都是按下开始移动,放开时候停止,因此让这些事件 handler 设定相机的速度,再由 requestAnimationFrame
的回圈来进行相机位置的更新,我们加上这两个状态:
// async function setup() {
// ...
return {
gl,
program, attributes, uniforms,
buffers, modelBufferArrays,
state: {
fieldOfView: 45 * Math.PI / 180,
translate: [150, 100, 0],
rotate: [degToRad(210), degToRad(30), degToRad(0)],
scale: [1, 1, 1],
+ cameraPosition: [250, 0, 400],
+ cameraVelocity: [0, 0, 0],
},
time: 0,
};
在 render()
中让 matrix4.lookAt()
串上刚建立的状态:
- const cameraMatrix = matrix4.lookAt([250, 0, 400], [250, 0, 0], [0, 1, 0]);
+ const cameraMatrix = matrix4.lookAt(state.cameraPosition, [250, 0, 0], [0, 1, 0]);
启用 startLoop
,使用 cameraVelocity
来更新 cameraPosition
:
function startLoop(app, now = 0) {
const timeDiff = now - app.time;
app.time = now;
app.state.cameraPosition[0] += app.state.cameraVelocity[0] * timeDiff;
app.state.cameraPosition[1] += app.state.cameraVelocity[1] * timeDiff;
app.state.cameraPosition[2] += app.state.cameraVelocity[2] * timeDiff;
document.getElementById('camera-position').textContent = (
`cameraPosition: [${app.state.cameraPosition.map(f => f.toFixed(2)).join(', ')}]`
);
render(app, timeDiff);
requestAnimationFrame(now => startLoop(app, now));
}
同时笔者打算在画面上面显示当前的 cameraPosition
,因此得在 HTML 加入 <p id='camera-position'></p>
,最後就是监听 keydown
, keyup
, mousedown
, mouseup
, touchstart
, touchend
并处理这些事件,这些程序码比较冗长、琐碎,笔者就不放在文章中了,有需要可以在完整程序码中找到:
就可以用比较直觉的方法在 xy 平面上移动相机罗,在手机上用起来像是这样:
<<: Pima Indians diabetes dataset 考古溯源 & model prediction
CMoney的日子在周五画上了休止符,尽管最後一个多月的时间遇到了防疫升级,采线上的方式进行,甚至和...
标题标签** <h1> - <h6> (一级标题 - 六级标题) 文字粗体...
自己常常在写程序的时候,因为习惯一种写法就很自然写下去,不太会去思考为什麽要这样用,就像每天早上都会...
tags: OC 30 day NSObject 是什麽? 是Foundation 框架中的类,在这...
基本上,JSX 单纯只是 React.createElement(component, props...