From 6a667d4ff7a3d06339c9a6e4d2a3a898ee06276c Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Wed, 15 Feb 2023 13:19:05 +0800 Subject: [PATCH] =?UTF-8?q?=E7=B2=92=E5=AD=90=E7=89=B9=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data/desc.json | 2 +- src/main.ts | 1 + src/plugin/particle/camera.ts | 172 +++++++++++++- src/plugin/particle/particle.ts | 239 +++++++++++++++++-- src/plugin/particle/render.ts | 264 ++++++++++++++++++++- src/plugin/webgl/canvas.ts | 4 +- src/plugin/webgl/{martrix.ts => matrix.ts} | 26 +- src/types/util.d.ts | 8 +- 8 files changed, 666 insertions(+), 50 deletions(-) rename src/plugin/webgl/{martrix.ts => matrix.ts} (88%) diff --git a/src/data/desc.json b/src/data/desc.json index 1fa349f..638778d 100644 --- a/src/data/desc.json +++ b/src/data/desc.json @@ -54,7 +54,7 @@ "
", "游戏作者:古祠", "
", - "本塔遵循MIT开源协议,你可随意使用本塔的任何代码,不需要作者授权,也可以随意用于商业用途。", + "本塔遵循MIT开源协议。查看开源协议", "
", "BGM来源:网易云音乐等", "
", diff --git a/src/main.ts b/src/main.ts index 7cc4045..90be7f5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import App from './App.vue'; import App2 from './App2.vue'; import './styles.less'; import 'ant-design-vue/dist/antd.dark.css'; +import './plugin/particle/render'; createApp(App).mount('#root'); createApp(App2).mount('#root2'); diff --git a/src/plugin/particle/camera.ts b/src/plugin/particle/camera.ts index 42ed59b..636473a 100644 --- a/src/plugin/particle/camera.ts +++ b/src/plugin/particle/camera.ts @@ -1,11 +1,31 @@ -import { Matrix4 } from '../webgl/martrix'; +import { TimingFn } from 'mutate-animate'; +import { Matrix4 } from '../webgl/matrix'; import { Renderer } from './render'; -type Position3D = [number, number, number]; +export type Position3D = [number, number, number]; + +type OneParameterAnimationType = 'x' | 'y' | 'z'; +type TwoParameterAnimationType = 'xy' | 'yx' | 'xz' | 'zx' | 'yz' | 'zy'; +type ThreeParameterAnimationType = + | 'xyz' + | 'xzy' + | 'yxz' + | 'yzx' + | 'zxy' + | 'zyx'; +type CameraAnimationTarget = 'eye' | 'at' | 'up'; + +interface CameraAnimationType { + 1: OneParameterAnimationType; + 2: TwoParameterAnimationType; + 3: ThreeParameterAnimationType; +} export class Camera { /** 视图矩阵 */ - matrix!: Matrix4; + view!: Matrix4; + /** 投影矩阵 */ + projection!: Matrix4; /** 绑定的渲染器 */ renderer?: Renderer; @@ -13,8 +33,12 @@ export class Camera { this.reset(); } + /** + * 初始化视角矩阵 + */ reset() { - this.matrix = new Matrix4(); + this.view = new Matrix4(); + this.projection = new Matrix4(); } /** @@ -39,7 +63,7 @@ export class Camera { * @param up 上方向 */ lookAt(eye: Position3D, at: Position3D, up: Position3D) { - this.matrix = this.calLookAt(eye, at, up); + this.view = this.calLookAt(eye, at, up); } /** @@ -49,9 +73,62 @@ export class Camera { * @param up 上方向 */ transform(eye: Position3D, at: Position3D, up: Position3D) { - this.matrix.multipy(this.calLookAt(eye, at, up)); + this.view.multipy(this.calLookAt(eye, at, up)); } + /** + * 设置透视投影矩阵 + * @param fov 垂直视角,即摄像机视锥体的上下平面夹角,单位角度 + * @param aspect 近裁剪面的长宽比,即视野的长宽比 + * @param near 近裁剪面的距离,即最近能看多远 + * @param far 远裁剪面的距离,即最远能看多远 + */ + setPerspective(fov: number, aspect: number, near: number, far: number) { + this.projection = this.calPerspective(fov, aspect, near, far); + } + + /** + * 设置正交投影矩阵 + * @param left 可视空间的左边界 + * @param right 可视空间的右边界 + * @param bottom 可视空间的下边界 + * @param top 可视空间的上边界 + * @param near 近裁剪面的距离,即最近能看多远 + * @param far 远裁剪面的距离,即最远能看多远 + */ + setOrthogonal( + left: number, + right: number, + bottom: number, + top: number, + near: number, + far: number + ) { + this.projection = this.calOrthogonal( + left, + right, + bottom, + top, + near, + far + ); + } + + /** + * 更新视角 + */ + update() { + this.renderer?.render(); + } + + applyAnimate( + target: CameraAnimationTarget, + type: CameraAnimationType[N], + time: number = 1000, + timing?: TimingFn, + relative: boolean = false + ) {} + /** * 计算摄像机变换矩阵 * @see https://github.com/bad4iz/cuon-matrix/blob/main/src/Matrix4/Matrix4.ts @@ -92,9 +169,90 @@ export class Camera { matrix[0] = [sx, sy, sz, 0]; matrix[1] = [ux, uy, uz, 0]; matrix[2] = [-fx, -fy, -fz, 0]; - matrix[4] = [0, 0, 0, 1]; + matrix[3] = [0, 0, 0, 1]; matrix.translate(-eyeX, -eyeY, -eyeZ); return matrix; } + + /** + * 计算透视矩阵 + * @see https://github.com/bad4iz/cuon-matrix/blob/main/src/Matrix4/Matrix4.ts + * @param fovy 垂直视角,即摄像机视锥体的上下平面夹角 + * @param aspect 近裁剪面的长宽比,即视野的长宽比 + * @param near 近裁剪面的距离,即最近能看多远 + * @param far 远裁剪面的距离,即最远能看多远 + */ + private calPerspective( + fov: number, + aspect: number, + near: number, + far: number + ) { + if (near === far || aspect === 0) { + throw new Error( + `No sence can be set, because near === far or aspect === 0.` + ); + } + if (near <= 0 || far <= 0) { + throw new Error(`near and far must be positive.`); + } + + fov = (Math.PI * fov) / 180 / 2; + const s = Math.sin(fov); + if (s === 0) { + throw new Error( + `Cannot set perspectivity, because sin(fov) === 0.` + ); + } + + const rd = 1 / (far - near); + const ct = Math.cos(fov) / s; + + const matrix = new Matrix4(); + + matrix[0] = [ct / aspect, 0, 0, 0]; + matrix[1] = [0, ct, 0, 0]; + matrix[2] = [0, 0, -(far + near) * rd, -2 * near * far * rd]; + matrix[3] = [0, 0, -1, 0]; + + return matrix; + } + + /** + * 设置正交投影矩阵 + * @param left 可视空间的左边界 + * @param right 可视空间的右边界 + * @param bottom 可视空间的下边界 + * @param top 可视空间的上边界 + * @param near 近裁剪面的距离,即最近能看多远 + * @param far 远裁剪面的距离,即最远能看多远 + */ + private calOrthogonal( + left: number, + right: number, + bottom: number, + top: number, + near: number, + far: number + ) { + if (left === right || bottom === top || near === far) { + throw new Error( + `Cannot set Orthogonality, because left === right or top === bottom or near === far.` + ); + } + + const rw = 1 / (right - left); + const rh = 1 / (top - bottom); + const rd = 1 / (far - near); + + const matrix = new Matrix4(); + + matrix[0] = [2 * rw, 0, 0, -(right + left) * rw]; + matrix[1] = [0, 2 * rh, 0, -(top + bottom) * rh]; + matrix[2] = [0, 0, -2 * rd, -(far + near) * rd]; + matrix[3] = [0, 0, 0, 1]; + + return matrix; + } } diff --git a/src/plugin/particle/particle.ts b/src/plugin/particle/particle.ts index 481136f..6f3643c 100644 --- a/src/plugin/particle/particle.ts +++ b/src/plugin/particle/particle.ts @@ -3,30 +3,47 @@ import { has } from '../utils'; import { Camera } from './camera'; import { Renderer } from './render'; +export type ParticleColor = [number, number, number, number]; + interface ParticleThreshold { radius: number; color: number; - position: number; + posX: number; + posY: number; + posZ: number; } -interface ParticleOne { +export interface ParticleOne { x: number; y: number; z: number; r: number; + color: ParticleColor; } interface Loc3D extends Loc { z: number; } +interface ParticleInfo { + pos: Loc3D; + density: number; + color: ParticleColor; + radius: number; + threshold: ParticleThreshold; +} + export class Particle { /** 绑定的摄像机 */ camera?: Camera; /** 粒子中心位置 */ - pos: Loc3D; - /** 粒子密度 */ - density: number; + pos: Loc3D = { x: 0, y: 0, z: 0 }; + /** 粒子密度,即粒子总数 */ + density: number = 50; + /** 粒子颜色 */ + color: ParticleColor = [0, 0, 0, 0]; + /** 每个粒子的半径 */ + radius: number = 2; /** 渲染器 */ renderer?: Renderer; @@ -37,17 +54,22 @@ export class Particle { private needUpdate: boolean = false; private ticker: Ticker = new Ticker(); + /** 设置信息前的信息 */ + private originInfo: DeepPartial = {}; + /** 各个属性的阈值 */ threshold: ParticleThreshold = { radius: 2, - color: 16, - position: 50 + color: 0.1, + posX: 0.1, + posY: 0.1, + posZ: 0.1 }; - constructor(density: number, x: number, y: number, z: number) { - this.pos = { x, y, z }; - this.density = density; - this.ticker.add(this.updateParticleData); + constructor() { + this.ticker.add(() => { + this.updateParticleData.call(this); + }); } /** @@ -55,9 +77,53 @@ export class Particle { * @param x 横坐标 * @param y 纵坐标 */ - setPos(x?: number, y?: number): Particle { - has(x) && (this.pos.x = x); - has(y) && (this.pos.y = y); + setPos(x?: number, y?: number, z?: number): Particle { + this.originInfo.pos ??= {}; + if (has(x)) { + this.pos.x = x; + this.originInfo.pos.x = x; + } + if (has(y)) { + this.pos.y = y; + this.originInfo.pos.y = y; + } + if (has(z)) { + this.pos.z = z; + this.originInfo.pos.z = z; + } + this.needUpdate = true; + return this; + } + + /** + * 设置粒子的密度,即粒子总数 + * @param density 密度 + */ + setDensity(density: number): Particle { + this.density = density; + this.originInfo.density = density; + this.needUpdate = true; + return this; + } + + /** + * 设置粒子的颜色 + * @param color 颜色 + */ + setColor(color: ParticleColor) { + this.color = color; + this.originInfo.color = color; + this.needUpdate = true; + return this; + } + + /** + * 设置粒子的半径 + * @param radius 半径 + */ + setRadius(radius: number) { + this.radius = radius; + this.originInfo.radius = radius; this.needUpdate = true; return this; } @@ -67,19 +133,18 @@ export class Particle { * @param data 阈值信息 */ setThreshold(data: Partial): Particle { - const { radius, color, position } = data; - has(radius) && (this.threshold.radius = radius); - has(color) && (this.threshold.radius = color); - has(position) && (this.threshold.radius = position); + this.originInfo.threshold ??= {}; + for (const [key, value] of Object.entries(data) as [ + keyof ParticleThreshold, + any + ][]) { + this.threshold[key] = value; + this.originInfo.threshold[key] = value; + } this.needUpdate = true; return this; } - /** - * 生成粒子 - */ - generate() {} - /** * 添加到一个渲染器上 * @param renderer 渲染器 @@ -102,11 +167,137 @@ export class Particle { this.needUpdate = true; } + /** + * 生成粒子,注意该函数会删除当前的所有粒子,然后再重新生成 + */ + generate() { + const particles = this.generateNewParticles(this.density); + this.list = particles; + } + /** * 每帧执行的粒子更新器 */ private updateParticleData() { - if (!this.needUpdate) return; + if (!this.needUpdate || this.list.length === 0) return; this.needUpdate = false; + + // check number + if (this.list.length > this.density) { + this.list.splice(this.density); + } else if (this.list.length < this.density) { + this.list.push( + ...this.generateNewParticles(this.density - this.list.length) + ); + } + + // check radius + if (has(this.originInfo.radius)) { + if (this.radius !== this.originInfo.radius) { + const delta = this.radius - this.originInfo.radius; + this.list.forEach(v => { + v.r += delta; + }); + } + } + + // check color + if (has(this.originInfo.color)) { + if (!core.same(this.color, this.originInfo.color)) { + const r = this.color[0] - this.originInfo.color[0]!; + const g = this.color[1] - this.originInfo.color[1]!; + const b = this.color[2] - this.originInfo.color[2]!; + const a = this.color[3] - this.originInfo.color[3]!; + this.list.forEach(v => { + v.color[0] += r; + v.color[1] += g; + v.color[2] += b; + v.color[3] += a; + }); + } + } + + // check position + if (has(this.originInfo.pos)) { + if (!core.same(this.pos, this.originInfo.pos)) { + const x = this.pos.x - this.originInfo.pos.x!; + const y = this.pos.y - this.originInfo.pos.y!; + const z = this.pos.z - this.originInfo.pos.z!; + this.list.forEach(v => { + v.x += x; + v.y += y; + v.z += z; + }); + } + } + + // check threshold + if (has(this.originInfo.threshold)) { + for (const [key, v] of Object.entries(this.threshold) as [ + keyof ParticleThreshold, + any + ][]) { + const now = v; + const origin = this.originInfo.threshold[key]; + if (origin === now || !has(origin)) { + continue; + } + const ratio = now / origin; + if (key === 'posX') { + this.list.forEach(v => { + v.x = (v.x - this.pos.x) * ratio + this.pos.x; + }); + } else if (key === 'posY') { + this.list.forEach(v => { + v.y = (v.y - this.pos.y) * ratio + this.pos.y; + }); + } else if (key === 'posZ') { + this.list.forEach(v => { + v.z = (v.z - this.pos.z) * ratio + this.pos.z; + }); + } else if (key === 'radius') { + this.list.forEach(v => { + v.r = (v.r - this.radius) * ratio + this.radius; + }); + } else { + this.list.forEach(v => { + v.color = v.color.map((v, i) => { + return (v - this.color[i]) * ratio + this.color[i]; + }) as ParticleColor; + }); + } + } + } + + this.render(); + } + + /** + * 生成指定数量的粒子 + * @param num 生成数量 + */ + private generateNewParticles(num: number): ParticleOne[] { + const res: ParticleOne[] = new Array(num); + const { posX, posY, posZ, radius, color } = this.threshold; + for (let i = 0; i < num; i++) { + const p: ParticleOne = { + x: this.pos.x + (Math.random() - 0.5) * 2 * posX, + y: this.pos.y + (Math.random() - 0.5) * 2 * posY, + z: this.pos.z + (Math.random() - 0.5) * 2 * posZ, + r: this.radius + (Math.random() - 0.5) * 2 * radius, + color: [0, 0, 0, 0].map( + (v, i) => this.color[i] + (Math.random() - 0.5) * 2 * color + ) as ParticleColor + }; + res[i] = p; + } + return res; + } + + /** + * 渲染这个粒子 + */ + private render() { + this.renderer?.render(this); } } diff --git a/src/plugin/particle/render.ts b/src/plugin/particle/render.ts index c4398bc..5fed02c 100644 --- a/src/plugin/particle/render.ts +++ b/src/plugin/particle/render.ts @@ -1,6 +1,50 @@ +import { circle, sleep, Ticker } from 'mutate-animate'; +import { has } from '../utils'; +import { createProgram } from '../webgl/canvas'; +import { Matrix4 } from '../webgl/matrix'; import { isWebGLSupported } from '../webgl/utils'; -import { Camera } from './camera'; -import { Particle } from './particle'; +import { Camera, Position3D } from './camera'; +import { Particle, ParticleColor, ParticleOne } from './particle'; + +// 顶点着色器与片元着色器 +// 很像C对吧(但这不是C,是glsl +const vshader = ` + attribute vec4 position; + attribute vec4 color; + attribute vec2 radius; + uniform mat4 camera; + uniform mat4 projection; + varying vec4 vColor; + varying vec4 vPosition; + varying float vRadius; + + void main() { + vec4 p = projection * camera * position; + gl_Position = p; + vColor = color; + vPosition = p; + vRadius = radius.x; + gl_PointSize = vRadius; + } +`; +const fshader = ` + #ifdef GL_ES + precision mediump float; + #endif + + varying vec4 vColor; + varying vec4 vPosition; + varying float vRadius; + + void main() { + vec2 position = gl_PointCoord.xy; + if (distance(position, vec2(0.5)) > 0.5) { + discard; + } else { + gl_FragColor = vColor; + } + } +`; export class Renderer { /** 粒子列表 */ @@ -13,12 +57,43 @@ export class Renderer { camera?: Camera; /** 缩放比例 */ ratio: number = devicePixelRatio; + /** gl的程序对象 */ + program: WebGLProgram; - constructor() { + /** 画布缓冲区 */ + private buffer: WebGLBuffer; + /** 各个attribute的内存地址 */ + private attribLocation: Record = {}; + /** 各个uniform的内存地址 */ + private uniformLocation: Record = {}; + + private static readonly attributes: string[] = [ + 'position', + 'color', + 'radius' + ]; + private static readonly uniforms: string[] = ['camera', 'projection']; + + constructor(width?: number, height?: number) { if (!isWebGLSupported) { throw new Error(`Your service or browser does not support webgl!`); } + this.canvas.style.width = `${width}px`; + this.canvas.style.height = `${height}px`; + if (has(width)) { + this.canvas.width = width * devicePixelRatio; + } + if (has(height)) { + this.canvas.height = height * devicePixelRatio; + } this.gl = this.canvas.getContext('webgl')!; + this.program = createProgram(this.gl, vshader, fshader); + this.gl.clearColor(0.0, 0.0, 0.0, 0.0); + this.buffer = this.bindBuffer(); + this.getGLVariblesLocation(); + this.gl.enable(this.gl.BLEND); + this.gl.enable(this.gl.DEPTH_TEST); + this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); } /** @@ -80,4 +155,187 @@ export class Renderer { if (index === -1) return; this.particleList.splice(index, 1); } + + /** + * 设置画布的背景色 + * @param color 背景色 + */ + setBackground(color: ParticleColor) { + this.gl.clearColor(...color); + } + + /** + * 渲染所有或单个粒子 + */ + render(particle?: Particle | number) { + const { position, color } = this.attribLocation; + const { camera } = this.uniformLocation; + if (!has(position) || !has(color)) { + throw new Error(`Unexpected unset of attribute location`); + } + if (!has(camera)) { + throw new Error(`Unexpected unset of uniform location`); + } + this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); + if (!has(particle)) this.particleList.forEach(v => this.renderOne(v)); + else { + const p = + typeof particle === 'number' + ? this.particleList[particle] + : particle; + this.renderOne(p); + } + } + + /** + * 绑定画布的缓冲区 + * @returns 绑定的缓冲区 + */ + private bindBuffer() { + const buffer = this.gl.createBuffer(); + if (!buffer) throw this.notSupport(); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer); + return buffer; + } + + /** + * 更新一个粒子的缓冲区数据 + * @param array 粒子的粒子元素数组 + */ + private updateOneParticleBufferData(array: ParticleOne[]) { + const particleArray = new Float32Array( + array + .map(v => { + const [r, g, b, a] = v.color; + return [v.x, v.y, v.z, r, g, b, a, v.r, 0]; + }) + .flat() + ); + this.gl.bufferData( + this.gl.ARRAY_BUFFER, + particleArray, + this.gl.DYNAMIC_DRAW + ); + return particleArray; + } + + /** + * 获取gl变量的内存地址 + */ + private getGLVariblesLocation() { + Renderer.attributes.forEach(v => { + this.attribLocation[v] = this.gl.getAttribLocation(this.program, v); + }); + Renderer.uniforms.forEach(v => { + const loc = this.gl.getUniformLocation(this.program, v); + if (!loc) { + throw new Error(`Cannot get the location of uniform '${v}'`); + } + this.uniformLocation[v] = loc; + }); + } + + /** + * 渲染某一个粒子 + * @param particle 要渲染的粒子 + */ + private renderOne(particle: Particle) { + const arr = this.updateOneParticleBufferData(particle.list); + const size = arr.BYTES_PER_ELEMENT; + + const { position, color, radius } = this.attribLocation; + const { camera, projection } = this.uniformLocation; + + // 给gl变量赋值 + this.gl.vertexAttribPointer( + position, + 3, + this.gl.FLOAT, + false, + size * 9, + 0 + ); + this.gl.vertexAttribPointer( + color, + 4, + this.gl.FLOAT, + false, + size * 9, + size * 3 + ); + this.gl.vertexAttribPointer( + radius, + 2, + this.gl.FLOAT, + false, + size * 9, + size * 7 + ); + this.gl.enableVertexAttribArray(position); + this.gl.enableVertexAttribArray(color); + this.gl.enableVertexAttribArray(radius); + const matrix = new Matrix4(); + const c = + this.camera?.view.toWebGLFloat32Array() ?? + matrix.toWebGLFloat32Array(); + const p = + this.camera?.projection.toWebGLFloat32Array() ?? + matrix.toWebGLFloat32Array(); + + this.gl.uniformMatrix4fv(camera, false, c); + this.gl.uniformMatrix4fv(projection, false, p); + + // 绘制 + this.gl.drawArrays(this.gl.POINTS, 0, particle.list.length); + } + + private notSupport() { + throw new Error(`Your service or browser does not support webgl!`); + } } + +window.addEventListener('load', async () => { + const renderer = new Renderer( + 480 * core.domStyle.scale, + 480 * core.domStyle.scale + ); + const particle = new Particle(); + const camera = new Camera(); + renderer.bindCamera(camera); + particle.appendTo(renderer); + renderer.append(core.dom.gameDraw); + camera.lookAt([1, 1, 5], [0, 0, 0], [0, 1, 0]); + camera.setPerspective(20, 1, 1, 100); + + console.log(camera.view, camera.projection); + + particle.setColor([0.3, 0.6, 0.7, 0.7]); + particle.setRadius(3); + particle.setDensity(1000); + particle.setThreshold({ + posX: 0.2, + posY: 0.2, + posZ: 10, + radius: 0, + color: 0 + }); + particle.generate(); + + renderer.canvas.style.position = 'absolute'; + renderer.canvas.style.zIndex = '160'; + + renderer.render(); + + await sleep(5000); + const now: Position3D = [1, 1, 5]; + const path = circle(1, 1000, [0, 0]); + let f = 0; + new Ticker().add(() => { + camera.lookAt(now, [0, 0, 0], [0, 1, 0]); + const [x, y] = path(f / 1000 / 2000); + f++; + now[0] = x; + now[1] = y; + renderer.render(); + }); +}); diff --git a/src/plugin/webgl/canvas.ts b/src/plugin/webgl/canvas.ts index b4a70c2..6f777c7 100644 --- a/src/plugin/webgl/canvas.ts +++ b/src/plugin/webgl/canvas.ts @@ -87,6 +87,8 @@ export function createProgram( throw new Error(`Program link fail: ${err}`); } + gl.useProgram(program); + return program; } @@ -113,7 +115,7 @@ export function loadShader( gl.compileShader(shader); // 检查是否编译成功 - const compiled = gl.getShaderParameter(gl, gl.COMPILE_STATUS); + const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); if (!compiled) { const err = gl.getShaderInfoLog(shader); throw new Error(`Shader compile fail: ${err}`); diff --git a/src/plugin/webgl/martrix.ts b/src/plugin/webgl/matrix.ts similarity index 88% rename from src/plugin/webgl/martrix.ts rename to src/plugin/webgl/matrix.ts index ec6692d..450ba03 100644 --- a/src/plugin/webgl/martrix.ts +++ b/src/plugin/webgl/matrix.ts @@ -1,3 +1,4 @@ +import { cloneDeep } from 'lodash'; import { has } from '../utils'; export class Matrix extends Array { @@ -40,26 +41,31 @@ export class Matrix extends Array { ); } const n = this.length; - const arr = this.map(v => v.slice()); + const arr = Array.from(this).map(v => v.slice()); for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { + this[i][j] = 0; for (let k = 0; k < n; k++) { - this[i][j] = arr[i][k] * matrix[k][j]; + this[i][j] += arr[i][k] * matrix[k][j]; } } } + return this; } } export class Matrix4 extends Matrix { constructor(...n: number[][]) { - n ??= [ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 1] - ]; + if (n.length === 0) { + n = [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ]; + } + if (n.length !== 4) { throw new TypeError(`The length of delivered array must be 4.`); } @@ -142,7 +148,7 @@ export class Matrix4 extends Matrix { */ transpose(target: 'this' | 'new' = 'new'): Matrix4 { const t = target === 'this' ? this : new Matrix4(); - const arr = this.map(v => v.slice()); + const arr = Array.from(this).map(v => v.slice()); for (let i = 0; i < 4; i++) { for (let j = 0; j < 4; j++) { t[i][j] = arr[j][i]; @@ -155,6 +161,6 @@ export class Matrix4 extends Matrix { * 转换成列主序的Float32Array,用于webgl */ toWebGLFloat32Array(): Float32Array { - return new Float32Array(this.transpose().flat()); + return new Float32Array(Array.from(this.transpose()).flat()); } } diff --git a/src/types/util.d.ts b/src/types/util.d.ts index f3a7697..e6d0611 100644 --- a/src/types/util.d.ts +++ b/src/types/util.d.ts @@ -822,12 +822,12 @@ type DeepReadonly = { }; /** - * 深度可选一个对象,使其所有属性都 + * 深度可选一个对象,使其所有属性都可选 */ type DeepPartial = { [P in keyof T]?: T[P] extends number | string | boolean ? T[P] - : DeepReadonly; + : DeepPartial; }; /** @@ -836,7 +836,7 @@ type DeepPartial = { type DeepRequired = { [P in keyof T]-?: T[P] extends number | string | boolean ? T[P] - : DeepReadonly; + : DeepRequired; }; /** @@ -852,7 +852,7 @@ type Writable = { type DeepWritable = { -readonly [P in keyof T]: T[P] extends number | string | boolean ? T[P] - : DeepReadonly; + : DeepWritable; }; /**