前言
学习目标
- 创建二维向量对象,并掌握向量的基本算法
- 创建三阶矩阵对象,并掌握矩阵的基本算法
知识点
- 向量
- 矩阵
前情回顾
之前我们创建了vue+vite+ts+vitest 项目,并创建了我们所需要的类对象。
接下我们便要完善这些类对象,不过在此之前,我们要先准备好几个常用的数学方法。
1-向量对象
向量是高中的数学知识,没啥难度,大家可以简单看看,理解就好。
- /src/lmm/math/Vector2.ts
import { Matrix3 } from './Matrix3'
class Vector2 {
x = 0
y = 0
readonly isVector2 = true
constructor(x = 0, y = x) {
this.x = x
this.y = y
}
get width() {
return this.x
}
set width(value) {
this.x = value
}
get height() {
return this.y
}
set height(value) {
this.y = value
}
set(x: number, y = x) {
this.x = x
this.y = y
return this
}
setScalar(scalar: number) {
this.x = scalar
this.y = scalar
return this
}
setX(x: number) {
this.x = x
return this
}
setY(y: number) {
this.y = y
return this
}
setComponent(index: number, value: number) {
switch (index) {
case 0:
this.x = value
break
case 1:
this.y = value
break
default:
throw new Error('index is out of range: ' + index)
}
return this
}
getComponent(index: number) {
switch (index) {
case 0:
return this.x
case 1:
return this.y
default:
throw new Error('index is out of range: ' + index)
}
}
clone() {
return new Vector2(this.x, this.y)
}
copy(v: Vector2) {
this.x = v.x
this.y = v.y
return this
}
add(v: Vector2) {
this.x += v.x
this.y += v.y
return this
}
addScalar(s: number) {
this.x += s
this.y += s
return this
}
addVectors(a: Vector2, b: Vector2) {
this.x = a.x + b.x
this.y = a.y + b.y
return this
}
addScaledVector(v: Vector2, s: number) {
this.x += v.x * s
this.y += v.y * s
return this
}
sub(v: Vector2) {
this.x -= v.x
this.y -= v.y
return this
}
subScalar(s: number) {
this.x -= s
this.y -= s
return this
}
subVectors(a: Vector2, b: Vector2) {
this.x = a.x - b.x
this.y = a.y - b.y
return this
}
multiply(v: Vector2) {
this.x *= v.x
this.y *= v.y
return this
}
multiplyScalar(scalar: number) {
this.x *= scalar
this.y *= scalar
return this
}
divide(v: Vector2) {
this.x /= v.x
this.y /= v.y
return this
}
divideScalar(scalar: number) {
return this.multiplyScalar(1 / scalar)
}
min(v: Vector2) {
this.x = Math.min(this.x, v.x)
this.y = Math.min(this.y, v.y)
return this
}
max(v: Vector2) {
this.x = Math.max(this.x, v.x)
this.y = Math.max(this.y, v.y)
return this
}
clamp(min: Vector2, max: Vector2) {
// assumes min < max, componentwise
this.x = Math.max(min.x, Math.min(max.x, this.x))
this.y = Math.max(min.y, Math.min(max.y, this.y))
return this
}
clampScalar(minVal: number, maxVal: number) {
this.x = Math.max(minVal, Math.min(maxVal, this.x))
this.y = Math.max(minVal, Math.min(maxVal, this.y))
return this
}
clampLength(min: number, max: number) {
const length = this.length()
return this.divideScalar(length || 1).multiplyScalar(
Math.max(min, Math.min(max, length))
)
}
floor() {
this.x = Math.floor(this.x)
this.y = Math.floor(this.y)
return this
}
ceil() {
this.x = Math.ceil(this.x)
this.y = Math.ceil(this.y)
return this
}
round() {
this.x = Math.round(this.x)
this.y = Math.round(this.y)
return this
}
roundToZero() {
this.x = this.x < 0 ? Math.ceil(this.x) : Math.floor(this.x)
this.y = this.y < 0 ? Math.ceil(this.y) : Math.floor(this.y)
return this
}
negate() {
this.x = -this.x
this.y = -this.y
return this
}
dot(v: Vector2) {
return this.x * v.x + this.y * v.y
}
cross(v: Vector2) {
return this.x * v.y - this.y * v.x
}
lengthSq() {
return this.x * this.x + this.y * this.y
}
length() {
return Math.sqrt(this.x * this.x + this.y * this.y)
}
manhattanLength() {
return Math.abs(this.x) + Math.abs(this.y)
}
normalize() {
return this.divideScalar(this.length() || 1)
}
angle() {
// computes the angle in radians with respect to the positive x-axis
const angle = Math.atan2(-this.y, -this.x) + Math.PI
return angle
}
distanceTo(v: Vector2) {
return Math.sqrt(this.distanceToSquared(v))
}
distanceToSquared(v: Vector2) {
const dx = this.x - v.x,
dy = this.y - v.y
return dx * dx + dy * dy
}
manhattanDistanceTo(v: Vector2) {
return Math.abs(this.x - v.x) + Math.abs(this.y - v.y)
}
setLength(length: number) {
return this.normalize().multiplyScalar(length)
}
lerp(v: Vector2, alpha: number) {
this.x += (v.x - this.x) * alpha
this.y += (v.y - this.y) * alpha
return this
}
lerpVectors(v1: Vector2, v2: Vector2, alpha: number) {
this.x = v1.x + (v2.x - v1.x) * alpha
this.y = v1.y + (v2.y - v1.y) * alpha
return this
}
equals(v: Vector2) {
return v.x === this.x && v.y === this.y
}
fromArray(array: number[], offset = 0) {
this.x = array[offset]
this.y = array[offset + 1]
return this
}
toArray(array: number[], offset = 0) {
array[offset] = this.x
array[offset + 1] = this.y
return array
}
rotate(angle: number) {
const { x, y } = this
const c = Math.cos(angle)
const s = Math.sin(angle)
this.x = x * c - y * s
this.y = x * s + y * c
return this
}
rotateAround(center: Vector2, angle: number) {
const c = Math.cos(angle),
s = Math.sin(angle)
const x = this.x - center.x
const y = this.y - center.y
this.x = x * c - y * s + center.x
this.y = x * s + y * c + center.y
return this
}
random() {
this.x = Math.random()
this.y = Math.random()
return this
}
lookAt(v: Vector2) {
const p = new Vector2().subVectors(v, this)
const ang = Math.atan2(p.y, p.x)
if (ang < 0) {
return ang + Math.PI * 2
} else {
return ang
}
}
applyMatrix3(m: Matrix3) {
const x = this.x,
y = this.y
const e = m.elements
this.x = e[0] * x + e[3] * y + e[6]
this.y = e[1] * x + e[4] * y + e[7]
return this
}
*[Symbol.iterator]() {
yield this.x
yield this.y
}
}
export { Vector2 }
2-矩阵对象
对于矩阵对象的概念,我在一篇WebGL 的文章里说过,大家可以参考此文章。
- /src/lmm/math/Matrix3.ts
class Matrix3 {
elements: number[] = [1, 0, 0, 0, 1, 0, 0, 0, 1]
set(
n11: number,
n12: number,
n13: number,
n21: number,
n22: number,
n23: number,
n31: number,
n32: number,
n33: number
) {
const te = this.elements
te[0] = n11
te[1] = n21
te[2] = n31
te[3] = n12
te[4] = n22
te[5] = n32
te[6] = n13
te[7] = n23
te[8] = n33
return this
}
identity() {
this.set(1, 0, 0, 0, 1, 0, 0, 0, 1)
return this
}
copy(m: Matrix3) {
const te = this.elements
const me = m.elements
te[0] = me[0]
te[1] = me[1]
te[2] = me[2]
te[3] = me[3]
te[4] = me[4]
te[5] = me[5]
te[6] = me[6]
te[7] = me[7]
te[8] = me[8]
return this
}
setFromMatrix4(m: Matrix3) {
const me = m.elements
this.set(me[0], me[4], me[8], me[1], me[5], me[9], me[2], me[6], me[10])
return this
}
multiply(m: Matrix3) {
return this.multiplyMatrices(this, m)
}
premultiply(m: Matrix3) {
return this.multiplyMatrices(m, this)
}
multiplyMatrices(a: Matrix3, b: Matrix3) {
const ae = a.elements
const be = b.elements
const te = this.elements
const a11 = ae[0],
a12 = ae[3],
a13 = ae[6]
const a21 = ae[1],
a22 = ae[4],
a23 = ae[7]
const a31 = ae[2],
a32 = ae[5],
a33 = ae[8]
const b11 = be[0],
b12 = be[3],
b13 = be[6]
const b21 = be[1],
b22 = be[4],
b23 = be[7]
const b31 = be[2],
b32 = be[5],
b33 = be[8]
te[0] = a11 * b11 + a12 * b21 + a13 * b31
te[3] = a11 * b12 + a12 * b22 + a13 * b32
te[6] = a11 * b13 + a12 * b23 + a13 * b33
te[1] = a21 * b11 + a22 * b21 + a23 * b31
te[4] = a21 * b12 + a22 * b22 + a23 * b32
te[7] = a21 * b13 + a22 * b23 + a23 * b33
te[2] = a31 * b11 + a32 * b21 + a33 * b31
te[5] = a31 * b12 + a32 * b22 + a33 * b32
te[8] = a31 * b13 + a32 * b23 + a33 * b33
return this
}
multiplyScalar(s: number) {
const te = this.elements
te[0] *= s
te[3] *= s
te[6] *= s
te[1] *= s
te[4] *= s
te[7] *= s
te[2] *= s
te[5] *= s
te[8] *= s
return this
}
determinant() {
const te = this.elements
const a = te[0],
b = te[1],
c = te[2],
d = te[3],
e = te[4],
f = te[5],
g = te[6],
h = te[7],
i = te[8]
return a * e * i - a * f * h - b * d * i + b * f * g + c * d * h - c * e * g
}
invert() {
const te = this.elements,
n11 = te[0],
n21 = te[1],
n31 = te[2],
n12 = te[3],
n22 = te[4],
n32 = te[5],
n13 = te[6],
n23 = te[7],
n33 = te[8],
t11 = n33 * n22 - n32 * n23,
t12 = n32 * n13 - n33 * n12,
t13 = n23 * n12 - n22 * n13,
det = n11 * t11 + n21 * t12 + n31 * t13
if (det === 0) return this.set(0, 0, 0, 0, 0, 0, 0, 0, 0)
const detInv = 1 / det
te[0] = t11 * detInv
te[1] = (n31 * n23 - n33 * n21) * detInv
te[2] = (n32 * n21 - n31 * n22) * detInv
te[3] = t12 * detInv
te[4] = (n33 * n11 - n31 * n13) * detInv
te[5] = (n31 * n12 - n32 * n11) * detInv
te[6] = t13 * detInv
te[7] = (n21 * n13 - n23 * n11) * detInv
te[8] = (n22 * n11 - n21 * n12) * detInv
return this
}
transpose() {
let tmp
const m = this.elements
tmp = m[1]
m[1] = m[3]
m[3] = tmp
tmp = m[2]
m[2] = m[6]
m[6] = tmp
tmp = m[5]
m[5] = m[7]
m[7] = tmp
return this
}
transposeIntoArray(r: number[]) {
const m = this.elements
r[0] = m[0]
r[1] = m[3]
r[2] = m[6]
r[3] = m[1]
r[4] = m[4]
r[5] = m[7]
r[6] = m[2]
r[7] = m[5]
r[8] = m[8]
return this
}
setUvTransform(
tx: number,
ty: number,
sx: number,
sy: number,
rotation: number,
cx: number,
cy: number
) {
const c = Math.cos(rotation)
const s = Math.sin(rotation)
this.set(
sx * c,
sx * s,
-sx * (c * cx + s * cy) + cx + tx,
-sy * s,
sy * c,
-sy * (-s * cx + c * cy) + cy + ty,
0,
0,
1
)
return this
}
scale(sx: number, sy: number) {
this.premultiply(_m3.makeScale(sx, sy))
return this
}
rotate(theta: number) {
this.premultiply(_m3.makeRotation(theta))
return this
}
translate(tx: number, ty: number) {
this.premultiply(_m3.makeTranslation(tx, ty))
return this
}
// for 2D Transforms
makeTranslation(x: number, y: number) {
this.set(1, 0, x, 0, 1, y, 0, 0, 1)
return this
}
makeRotation(theta: number) {
// counterclockwise
const c = Math.cos(theta)
const s = Math.sin(theta)
this.set(c, -s, 0, s, c, 0, 0, 0, 1)
return this
}
makeScale(x: number, y: number) {
this.set(x, 0, 0, 0, y, 0, 0, 0, 1)
return this
}
equals(matrix: Matrix3) {
const te = this.elements
const me = matrix.elements
for (let i = 0; i < 9; i++) {
if (te[i] !== me[i]) return false
}
return true
}
fromArray(array: number[], offset = 0) {
for (let i = 0; i < 9; i++) {
this.elements[i] = array[i + offset]
}
return this
}
clone() {
return new Matrix3().fromArray(this.elements)
}
}
const _m3 = new Matrix3()
export { Matrix3 }
对于上面的方法大家可以自己看看,看不懂可以随时跟我说。
之后我们在实战中,用到哪些方法,我会再详细解释。
3-MathUtils.ts
MathUtils.ts是一个常用的数学工具包,里面装着一些杂七杂八的数学方法。
const _lut = ['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '0a', '0b', '0c', '0d', '0e', '0f', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '1a', '1b', '1c', '1d', '1e', '1f', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '2a', '2b', '2c', '2d', '2e', '2f', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '3a', '3b', '3c', '3d', '3e', '3f', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '4a', '4b', '4c', '4d', '4e', '4f', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '5a', '5b', '5c', '5d', '5e', '5f', '60', '61', '62', '63','64', '65', '66', '67', '68', '69', '6a', '6b', '6c', '6d', '6e', '6f', '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '7a', '7b', '7c', '7d', '7e', '7f', '80', '81', '82', '83', '84', '85', '86', '87', '88', '89', '8a', '8b', '8c', '8d', '8e', '8f', '90', '91', '92', '93', '94', '95', '96', '97', '98', '99', '9a', '9b', '9c', '9d', '9e', '9f', 'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'aa', 'ab', 'ac', 'ad', 'ae', 'af', 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7', 'b8', 'b9', 'ba', 'bb', 'bc', 'bd', 'be', 'bf', 'c0', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7','c8', 'c9', 'ca', 'cb', 'cc', 'cd', 'ce', 'cf', 'd0', 'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9', 'da', 'db', 'dc', 'dd', 'de', 'df', 'e0', 'e1', 'e2', 'e3', 'e4', 'e5', 'e6', 'e7', 'e8', 'e9', 'ea', 'eb', 'ec', 'ed', 'ee', 'ef', 'f0', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'fa', 'fb', 'fc', 'fd', 'fe', 'ff']
let _seed = 1234567
const DEG2RAD = Math.PI / 180
const RAD2DEG = 180 / Math.PI
// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136
function generateUUID() {
const d0 = (Math.random() * 0xffffffff) | 0
const d1 = (Math.random() * 0xffffffff) | 0
const d2 = (Math.random() * 0xffffffff) | 0
const d3 = (Math.random() * 0xffffffff) | 0
const uuid =
_lut[d0 & 0xff] +
_lut[(d0 >> 8) & 0xff] +
_lut[(d0 >> 16) & 0xff] +
_lut[(d0 >> 24) & 0xff] +
'-' +
_lut[d1 & 0xff] +
_lut[(d1 >> 8) & 0xff] +
'-' +
_lut[((d1 >> 16) & 0x0f) | 0x40] +
_lut[(d1 >> 24) & 0xff] +
'-' +
_lut[(d2 & 0x3f) | 0x80] +
_lut[(d2 >> 8) & 0xff] +
'-' +
_lut[(d2 >> 16) & 0xff] +
_lut[(d2 >> 24) & 0xff] +
_lut[d3 & 0xff] +
_lut[(d3 >> 8) & 0xff] +
_lut[(d3 >> 16) & 0xff] +
_lut[(d3 >> 24) & 0xff]
// .toLowerCase() here flattens concatenated strings to save heap memory space.
return uuid.toLowerCase()
}
function clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(max, value))
}
// compute euclidean modulo of m % n
// https://en.wikipedia.org/wiki/Modulo_operation
function euclideanModulo(n: number, m: number) {
return ((n % m) + m) % m
}
// Linear mapping from range <a1, a2> to range <b1, b2>
function mapLinear(x: number, a1: number, a2: number, b1: number, b2: number) {
return b1 + ((x - a1) * (b2 - b1)) / (a2 - a1)
}
// https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/inverse-lerp-a-super-useful-yet-often-overlooked-function-r5230/
function inverseLerp(x: number, y: number, value: number) {
if (x !== y) {
return (value - x) / (y - x)
} else {
return 0
}
}
// https://en.wikipedia.org/wiki/Linear_interpolation
function lerp(x: number, y: number, t: number) {
return (1 - t) * x + t * y
}
// http://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/
function damp(x: number, y: number, lambda: number, dt: number) {
return lerp(x, y, 1 - Math.exp(-lambda * dt))
}
// https://www.desmos.com/calculator/vcsjnyz7x4
function pingpong(x: number, length = 1) {
return length - Math.abs(euclideanModulo(x, length * 2) - length)
}
// http://en.wikipedia.org/wiki/Smoothstep
function smoothstep(x: number, min: number, max: number) {
if (x <= min) return 0
if (x >= max) return 1
x = (x - min) / (max - min)
return x * x * (3 - 2 * x)
}
function smootherstep(x: number, min: number, max: number) {
if (x <= min) return 0
if (x >= max) return 1
x = (x - min) / (max - min)
return x * x * x * (x * (x * 6 - 15) + 10)
}
// Random integer from <low, high> interval
function randInt(low: number, high: number) {
return low + Math.floor(Math.random() * (high - low + 1))
}
// Random float from <low, high> interval
function randFloat(low: number, high: number) {
return low + Math.random() * (high - low)
}
// Random float from <-range/2, range/2> interval
function randFloatSpread(range: number) {
return range * (0.5 - Math.random())
}
// Deterministic pseudo-random float in the interval [ 0, 1 ]
function seededRandom(s: number) {
if (s !== undefined) _seed = s
// Mulberry32 generator
let t = (_seed += 0x6d2b79f5)
t = Math.imul(t ^ (t >>> 15), t | 1)
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
function degToRad(degrees: number) {
return degrees * DEG2RAD
}
function radToDeg(radians: number) {
return radians * RAD2DEG
}
function isPowerOfTwo(value: number) {
return (value & (value - 1)) === 0 && value !== 0
}
function ceilPowerOfTwo(value: number) {
return Math.pow(2, Math.ceil(Math.log(value) / Math.LN2))
}
function floorPowerOfTwo(value: number) {
return Math.pow(2, Math.floor(Math.log(value) / Math.LN2))
}
export {
DEG2RAD,
RAD2DEG,
generateUUID,
clamp,
euclideanModulo,
mapLinear,
inverseLerp,
lerp,
damp,
pingpong,
smoothstep,
smootherstep,
randInt,
randFloat,
randFloatSpread,
seededRandom,
degToRad,
radToDeg,
isPowerOfTwo,
ceilPowerOfTwo,
floorPowerOfTwo,
}
我在后面主要用到的就是generateUUID() 方法,用于生成一个基本上不可能重复的唯一id。
总结
向量、矩阵是图形学的基础,需要大家对其有一个透彻且扎实理解,推荐大家看一下闫令琪的games101。
© 版权声明
文章版权归作者所有,未经允许请勿转载,侵权请联系 admin@trc20.tw 删除。
THE END