2 the vec3 class

写在前面:
19年的ggj要到了,来广州之前还信誓旦旦地说这次的ggj要用unreal!现在看来这个计划应该是要鸽了。可能会用回creator吧?可能当一个观众?God knows

正文
《ray tracing in one weekend》的第二章,造了一个3D向量轮子:Vec3
向量的作用,顾名思义就是表示方向的一个度量
打个比方,我们面前有一个盒子,为了描述盒子的体积,需要几个数字?
简单,三个数字,长(depth),宽(width),高(heigth),就可以描述清楚了。

很好,让我们换个表诉方式,如果盒子最内侧那个点是原点(0,0,0)
表述盒子最外侧那个点的坐标,是啥呢?
不出意外,应该就等于盒子的长宽高(x=width,y=height,z=depth)

继续,我们再换个表述方式,从最内侧的点(原点)指向最外侧的点的方向,如何去表达呢?
简单,将两个点的坐标相减就可以得到向量v = (x=width,y=height,z=depth) - (x=0,y=0,z=0)

在这里就会引入一个问题:
表达方向的向量和表达位置的坐标,用三个数字来表示的时候,可能会是相同的表达式(比如上面的情况,任何坐标减去原点得到的向量,数值和坐标一样),一个无头无尾,没有上下文的三个数(x,y,z),我们如何知道它表示的是向量,还是坐标呢?
通常我们会引入第四个数 w,用(x,y,z,w)来解决上面的问题
w = 0 表示这是一个向量,w = 1 表示这是一个坐标
对于(x,y,z,w)这种形式的四元组,我们称为齐次坐标。
怎么理解齐次坐标?
只要x,y,z 和 w 保持相同的倍数,都是同一个齐次坐标,
打个简单的比方: 我们称 (x,y,z,w) 和 (2 x, 2 y, 2 z, 2 w)是同一个东西
因为他们都可以通过除以最后一个数得到相同的表达式(x/w,y/w,z/w,1)
对于w = 0 表示一个向量,我们怎么去理解呢?
我们试试 (x/w, y/w, z/w, w/w)
当然 0 是不能除以 0 的
不过如果假设w是一个无限接近于0的正数
这时(x/w,y/w,z/w)就会变成一个非常大的数,那么这就像一条超长的射线,射向无限远,是不是就有种向量的感觉,指向某个地方
虽然听起来像科幻片,不过这段理解是在阅读《real time shadows》中,构建shadow volumns时看到的说法。或许有误。但也不是什么见不得光的想法。
当然 w = 0 和 w = 1,在仿射变换中,是一个开关translate矩阵作用的开关,设计得非常精妙,能解决问题当然也就足够了。这背后的想法很简单,坐标依赖于参考点,在移动的时候值会变化,而向量不论如何移动,值都是不变的。

回到标题,第二章并没有构建Vec4,而是一个Vec3,向量的基本操作不外乎操作x,y,z属性,做加减乘除调制,求长度,求归一化,求点乘和叉乘。

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
class Vec3 {
// 构造
constructor(x = 0, y = 0, z = 0) {
this.x = x
this.y = y
this.z = z
}
// 加
add(vec) {
return new Vec3(this.x + vec.x, this.y + vec.y, this.z + vec.z)
}
// 减
sub(vec) {
return new Vec3(this.x - vec.x, this.y - vec.y, this.z - vec.z)
}
// 乘
mul(num) {
return new Vec3(this.x * num, this.y * num, this.z * num)
}
// 除
div(num) {
let inv = 1 / num
return new Vec3(this.x * inv, this.y * inv, this.z * inv)
}
// 调制
mod(vec) {
return new Vec3(this.x * vec.x, this.y * vec.y, this.z * vec.z)
}
addSelf(vec) {
this.x += vec.x,this.y += vec.y,this.z += vec.z
return this
}
subSelf(vec) {
this.x -= vec.x,this.y -= vec.y,this.z -= vec.z
return this
}
mulSelf(num) {
this.x *= num, this.y *= num, this.z *= num
return this
}
divSelf(num) {
let inv = 1 / num
this.x *= inv, this.y *= inv, this.z *= inv
return this
}
modSelf(vec) {
this.x *= vec.x, this.y *= vec.y, ths.z *= vec.z
return this
}
// 长度
length() {
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z)
}
sqrLength() {
return this.x * this.x + this.y * this.y + this.z * this.z
}
// 归一化
normalize() {
let invLen = 1/Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z)
return new Vec3(this.x * invLen, this.y * invLen, this.z * invLen)
}
normalizeSelf() {
let invLen = 1/Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z)
this.x *= invLen, this.y *= invLen, this.z *= invLen
return this
}
// 点乘
dot(vec) {
return this.x * vec.x + this.y * vec.y + this.z * vec.z
}

//叉乘
cross(vec) {
return new Vec3(
this.y * vec.z - this.z * vec.y,
this.z * vec.x - this.x * vec.z,
this.x * vec.y - this.y * vec.x,
)
}
}

最后对第一章的绘制做了点小修改,使用Vec3来存储每个像素的结果,当然最后的画面也和第一张是一样的

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
<html>
<canvas width="256" height="256" id="screen"></canvas>
<!-- 引入向量模块 -->
<script src="./vec.js"></script>
<script>
function render(canvas)
{
let ctx = canvas.getContext("2d")
let w = canvas.attributes.width.value
let h = canvas.attributes.height.value
ctx.fillStyle = "rgb(0,0,0)"
ctx.fillRect(0, 0, w, h)
let imgdata = ctx.getImageData(0, 0, w, h)
let pixels = imgdata.data
let i = 0
for(let y = 0; y < h; y++) {
let sy = 1 - y / h
for(let x = 0; x < w; x++)
{
let sx = x / w
// 使用向量存储每个向量的结果
vec = new Vec3(sx,sy,0.2)
pixels[i ] = vec.x * 255
pixels[i + 1] = vec.y * 255
pixels[i + 2] = vec.z * 255
pixels[i + 3] = 1 * 255
i+=4
}
}
ctx.putImageData(imgdata, 0, 0)
}
let canvas = document.getElementById('screen')
render(canvas)
</script>
</html>