refactor: reorganize package structure and decouple framework packages (#338)

* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -0,0 +1,112 @@
/**
* Animation3DSystem - System for updating 3D animations.
* Animation3DSystem - 3D 动画更新系统。
*/
import { EntitySystem, Matcher, ECSSystem, Entity, Time } from '@esengine/ecs-framework';
import { Animation3DComponent } from '../Animation3DComponent';
import { SkeletonComponent } from '../SkeletonComponent';
import { MeshComponent } from '../MeshComponent';
import { AnimationEvaluator } from '../animation/AnimationEvaluator';
/**
* System for updating 3D animation playback.
* 用于更新 3D 动画播放的系统。
*
* Queries all entities with Animation3DComponent,
* updates animation time, and applies animation values to skeleton bones.
* 查询所有具有 Animation3DComponent 的实体,
* 更新动画时间,并将动画值应用到骨骼。
*/
@ECSSystem('Animation3D', { updateOrder: 100 })
export class Animation3DSystem extends EntitySystem {
private evaluator: AnimationEvaluator;
constructor() {
super(Matcher.empty().all(Animation3DComponent).all(MeshComponent));
this.evaluator = new AnimationEvaluator();
}
/**
* Process entities each frame.
* 每帧处理实体。
*/
protected override process(entities: readonly Entity[]): void {
const deltaTime = Time.deltaTime;
for (const entity of entities) {
if (!entity.enabled) continue;
this.updateEntity(entity, deltaTime);
}
}
/**
* Update a single entity's animation.
* 更新单个实体的动画。
*/
private updateEntity(entity: Entity, deltaTime: number): void {
const anim = entity.getComponent(Animation3DComponent);
const mesh = entity.getComponent(MeshComponent);
if (!anim || !mesh) return;
// Initialize animation clips from mesh asset if needed
// 如果需要,从网格资产初始化动画片段
if (anim.clips.length === 0 && mesh.meshAsset?.animations) {
anim.setClips(mesh.meshAsset.animations);
// Auto-play if configured
// 如果配置了自动播放
if (anim.playOnAwake && anim.clips.length > 0) {
anim.play();
}
}
// Update animation time
// 更新动画时间
anim.updateTime(deltaTime);
// Apply animation to skeleton
// 将动画应用到骨骼
if (anim.isPlaying && anim.currentClip) {
this.applyAnimation(entity, anim);
}
}
/**
* Apply animation values to skeleton.
* 将动画值应用到骨骼。
*/
private applyAnimation(entity: Entity, anim: Animation3DComponent): void {
const skeleton = entity.getComponent(SkeletonComponent);
const clip = anim.currentClip;
if (!clip || !skeleton?.isLoaded) return;
// Evaluate animation at current time
// 在当前时间评估动画
const evaluatedValues = this.evaluator.evaluate(clip, anim.currentTime);
// Apply values to skeleton bones
// 将值应用到骨骼
for (const [nodeIndex, value] of evaluatedValues) {
if (value.path === 'translation') {
skeleton.setBoneTransform(nodeIndex, {
position: value.value as [number, number, number]
});
} else if (value.path === 'rotation') {
skeleton.setBoneTransform(nodeIndex, {
rotation: value.value as [number, number, number, number]
});
} else if (value.path === 'scale') {
skeleton.setBoneTransform(nodeIndex, {
scale: value.value as [number, number, number]
});
}
}
// Mark skeleton as dirty for matrix update
// 标记骨骼为脏以更新矩阵
skeleton.markDirty();
}
}

View File

@@ -0,0 +1,124 @@
/**
* MeshAssetLoaderSystem - System for loading mesh assets on demand.
* MeshAssetLoaderSystem - 按需加载网格资产的系统。
*/
import { EntitySystem, Matcher, ECSSystem, Entity } from '@esengine/ecs-framework';
import type { IAssetManager, IGLTFAsset } from '@esengine/asset-system';
import { MeshComponent } from '../MeshComponent';
/**
* System for loading mesh assets when modelGuid changes.
* 当 modelGuid 变化时加载网格资产的系统。
*
* This system monitors MeshComponents and loads their model assets
* when the modelGuid property is set and the asset isn't loaded yet.
* 此系统监视 MeshComponent 并在设置 modelGuid 属性且资产尚未加载时加载其模型资产。
*/
@ECSSystem('MeshAssetLoader', { updateOrder: 50 })
export class MeshAssetLoaderSystem extends EntitySystem {
private assetManager: IAssetManager | null = null;
private loadingSet: Set<string> = new Set();
constructor() {
super(Matcher.empty().all(MeshComponent));
}
/**
* Set the asset manager for loading assets.
* 设置用于加载资产的资产管理器。
*/
public setAssetManager(manager: IAssetManager): void {
this.assetManager = manager;
}
/**
* Process entities each frame.
* 每帧处理实体。
*/
protected override process(entities: readonly Entity[]): void {
for (const entity of entities) {
if (!entity.enabled) continue;
this.checkAndLoadAsset(entity);
}
}
/**
* Check if a mesh component needs its asset loaded.
* 检查网格组件是否需要加载其资产。
*/
private checkAndLoadAsset(entity: Entity): void {
const mesh = entity.getComponent(MeshComponent);
if (!mesh) return;
// Skip if no modelGuid
// 如果没有 modelGuid 则跳过
if (!mesh.modelGuid) return;
// Skip if already loaded
// 如果已加载则跳过
if (mesh.isLoaded) return;
// Skip if already loading
// 如果正在加载则跳过
const loadKey = `${entity.id}:${mesh.modelGuid}`;
if (this.loadingSet.has(loadKey)) return;
// Start loading
// 开始加载
this.loadingSet.add(loadKey);
this.loadMeshAsset(entity, mesh, loadKey);
}
/**
* Load a mesh asset using the asset manager.
* 使用资产管理器加载网格资产。
*/
private async loadMeshAsset(entity: Entity, mesh: MeshComponent, loadKey: string): Promise<void> {
try {
if (!this.assetManager) {
console.warn('[MeshAssetLoaderSystem] No asset manager available');
return;
}
const modelGuid = mesh.modelGuid;
// Try to load using asset manager
// 尝试使用资产管理器加载
console.log(`[MeshAssetLoaderSystem] Loading: ${modelGuid}`);
// Check if it's a GUID or a path
// 检查是否是 GUID 还是路径
const isGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(modelGuid);
let result;
if (isGuid) {
result = await this.assetManager.loadAsset<IGLTFAsset>(modelGuid);
} else {
result = await this.assetManager.loadAssetByPath<IGLTFAsset>(modelGuid);
}
// Check if entity still exists and has the same modelGuid
// 检查实体是否仍然存在且 modelGuid 是否相同
if (!entity.enabled || mesh.modelGuid !== modelGuid) {
return;
}
// IAssetLoadResult contains: asset, handle, metadata, loadTime
// API throws on error, returns result directly on success
// IAssetLoadResult 包含asset, handle, metadata, loadTime
// API 在错误时抛出异常,成功时直接返回结果
if (result && result.asset) {
mesh.meshAsset = result.asset;
console.log(`[MeshAssetLoaderSystem] Loaded: ${modelGuid} (${result.asset.meshes?.length ?? 0} meshes)`);
} else {
console.warn(`[MeshAssetLoaderSystem] No asset returned for ${modelGuid}`);
}
} catch (error) {
console.error(`[MeshAssetLoaderSystem] Failed to load ${mesh.modelGuid}:`, error);
} finally {
this.loadingSet.delete(loadKey);
}
}
}

View File

@@ -0,0 +1,304 @@
/**
* MeshRenderSystem - System for rendering 3D meshes.
* MeshRenderSystem - 3D 网格渲染系统。
*/
import { EntitySystem, Matcher, ECSSystem, Entity } from '@esengine/ecs-framework';
import type { EngineBridge } from '@esengine/ecs-engine-bindgen';
import { TransformComponent } from '@esengine/engine-core';
import type { IMeshData } from '@esengine/asset-system';
import { MeshComponent } from '../MeshComponent';
/**
* System for rendering 3D mesh components.
* 用于渲染 3D 网格组件的系统。
*
* Queries all entities with MeshComponent and TransformComponent,
* builds interleaved vertex data, and submits to the Rust engine.
* 查询所有具有 MeshComponent 和 TransformComponent 的实体,
* 构建交错顶点数据并提交到 Rust 引擎。
*/
@ECSSystem('MeshRender', { updateOrder: 900 })
export class MeshRenderSystem extends EntitySystem {
private bridge: EngineBridge | null;
// Reusable buffers for performance
// 可重用缓冲区以提高性能
private vertexBuffer: Float32Array = new Float32Array(0);
private transformBuffer: Float32Array = new Float32Array(16);
constructor(bridge: EngineBridge | null = null) {
super(Matcher.empty().all(MeshComponent).all(TransformComponent));
this.bridge = bridge;
}
/**
* Set the engine bridge (can be called after construction).
* 设置引擎桥接(可在构造后调用)。
*/
public setEngineBridge(bridge: EngineBridge): void {
this.bridge = bridge;
}
// 调试帧计数 | Debug frame counter
private _frameCount = 0;
private _lastLogTime = 0;
/**
* Process entities each frame.
* 每帧处理实体。
*/
protected override process(entities: readonly Entity[]): void {
this._frameCount++;
if (!this.bridge) {
if (this._frameCount % 300 === 1) {
console.warn('[MeshRenderSystem] No bridge available');
}
return;
}
// Check if in 3D mode (mode 1 = 3D)
// 检查是否在 3D 模式
const renderMode = this.bridge.getRenderMode();
// Debug: log mode and entity count periodically
// 调试:定期记录模式和实体数量
const now = Date.now();
if (now - this._lastLogTime > 3000) {
this._lastLogTime = now;
console.log(`[MeshRenderSystem] Mode: ${renderMode}, Entities: ${entities.length}`);
// Log mesh status for each entity
// 记录每个实体的网格状态
for (const entity of entities) {
const mesh = entity.getComponent(MeshComponent);
if (mesh) {
console.log(` - Entity ${entity.name}: modelGuid=${mesh.modelGuid?.substring(0, 8)}..., isLoaded=${mesh.isLoaded}, meshCount=${mesh.allMeshes.length}`);
}
}
}
if (renderMode !== 1) {
// 2D mode, skip 3D rendering
// 2D 模式,跳过 3D 渲染
return;
}
for (const entity of entities) {
if (!entity.enabled) continue;
this.renderEntity(entity);
}
}
// 调试:上次提交时间 | Debug: last submit time
private _lastSubmitLogTime = 0;
/**
* Render a single entity's mesh.
* 渲染单个实体的网格。
*/
private renderEntity(entity: Entity): void {
const mesh = entity.getComponent(MeshComponent);
const transform = entity.getComponent(TransformComponent);
if (!mesh || !transform || !mesh.visible || !mesh.isLoaded) {
// Debug skip reason
// 调试跳过原因
const now = Date.now();
if (now - this._lastSubmitLogTime > 5000) {
this._lastSubmitLogTime = now;
const reason = !mesh ? 'no mesh' :
!transform ? 'no transform' :
!mesh.visible ? 'not visible' :
!mesh.isLoaded ? 'not loaded' : 'unknown';
console.log(`[MeshRenderSystem] Skip ${entity.name}: ${reason}`);
}
return;
}
// Get all meshes to render
// 获取所有要渲染的网格
const meshesToRender = mesh.allMeshes;
if (meshesToRender.length === 0) {
console.log(`[MeshRenderSystem] Skip ${entity.name}: no meshes`);
return;
}
// Build world transform matrix
// 构建世界变换矩阵
this.buildTransformMatrix(transform);
// Debug: log transform
// 调试:记录变换
const now = Date.now();
if (now - this._lastSubmitLogTime > 5000) {
this._lastSubmitLogTime = now;
const pos = transform.position;
console.log(`[MeshRenderSystem] Rendering ${entity.name}: ${meshesToRender.length} meshes`);
console.log(` Transform: pos(${pos.x?.toFixed(2) ?? 0}, ${pos.y?.toFixed(2) ?? 0}, ${pos.z?.toFixed(2) ?? 0})`);
}
// Render each mesh
// 渲染每个网格
for (let i = 0; i < meshesToRender.length; i++) {
const meshData = meshesToRender[i];
if (!meshData) continue;
// Build interleaved vertex data
// 构建交错顶点数据
const vertexData = this.buildVertexData(meshData);
if (!vertexData) {
console.warn(`[MeshRenderSystem] Failed to build vertex data for mesh ${i}`);
continue;
}
// Get material and texture IDs
// 获取材质和纹理 ID
const materialId = mesh.runtimeMaterialIds[i] ?? 0;
const textureId = mesh.runtimeTextureIds[i] ?? 0;
// Debug: log submission
// 调试:记录提交
if (now - this._lastSubmitLogTime < 100) {
console.log(` Submitting mesh ${i}: ${vertexData.length / 9} vertices, ${meshData.indices.length} indices`);
}
// Submit to engine
// 提交到引擎
try {
this.bridge!.submitSimpleMesh3D(
vertexData,
new Uint32Array(meshData.indices),
this.transformBuffer,
materialId,
textureId
);
} catch (e) {
console.error(`[MeshRenderSystem] submitSimpleMesh3D failed:`, e);
}
}
}
/**
* Build 4x4 transform matrix from TransformComponent.
* 从 TransformComponent 构建 4x4 变换矩阵。
*/
private buildTransformMatrix(transform: TransformComponent): void {
// Get world position, rotation, scale with safe defaults
// 获取世界位置、旋转、缩放(带安全默认值)
const rawPos = transform.worldPosition;
const rawRot = transform.worldRotation; // Euler angles in degrees
const rawScl = transform.worldScale;
// Safe extraction with defaults for 2D components
// 2D 组件的安全提取(带默认值)
const pos = { x: rawPos.x ?? 0, y: rawPos.y ?? 0, z: rawPos.z ?? 0 };
const rot = { x: rawRot.x ?? 0, y: rawRot.y ?? 0, z: rawRot.z ?? 0 };
const scl = { x: rawScl.x ?? 1, y: rawScl.y ?? 1, z: rawScl.z ?? 1 };
// Convert rotation to radians
// 将旋转转换为弧度
const rx = (rot.x * Math.PI) / 180;
const ry = (rot.y * Math.PI) / 180;
const rz = (rot.z * Math.PI) / 180;
// Build rotation matrix (ZYX order)
// 构建旋转矩阵ZYX 顺序)
const cx = Math.cos(rx), sx = Math.sin(rx);
const cy = Math.cos(ry), sy = Math.sin(ry);
const cz = Math.cos(rz), sz = Math.sin(rz);
// Combined rotation matrix
// 组合旋转矩阵
const r00 = cy * cz;
const r01 = cy * sz;
const r02 = -sy;
const r10 = sx * sy * cz - cx * sz;
const r11 = sx * sy * sz + cx * cz;
const r12 = sx * cy;
const r20 = cx * sy * cz + sx * sz;
const r21 = cx * sy * sz - sx * cz;
const r22 = cx * cy;
// Build column-major 4x4 matrix with scale and translation
// 构建带缩放和平移的列优先 4x4 矩阵
const m = this.transformBuffer;
// Column 0
m[0] = r00 * scl.x;
m[1] = r10 * scl.x;
m[2] = r20 * scl.x;
m[3] = 0;
// Column 1
m[4] = r01 * scl.y;
m[5] = r11 * scl.y;
m[6] = r21 * scl.y;
m[7] = 0;
// Column 2
m[8] = r02 * scl.z;
m[9] = r12 * scl.z;
m[10] = r22 * scl.z;
m[11] = 0;
// Column 3 (translation)
m[12] = pos.x;
m[13] = pos.y;
m[14] = pos.z;
m[15] = 1;
}
/**
* Build interleaved vertex data for simple 3D mesh.
* 构建简化 3D 网格的交错顶点数据。
*
* Format: [x, y, z, u, v, r, g, b, a] per vertex (9 floats)
* 格式:每个顶点 [x, y, z, u, v, r, g, b, a]9 个浮点数)
*/
private buildVertexData(meshData: IMeshData): Float32Array | null {
const vertices = meshData.vertices;
const uvs = meshData.uvs;
const colors = meshData.colors;
if (!vertices || vertices.length === 0) return null;
const vertexCount = vertices.length / 3;
const floatsPerVertex = 9;
const totalFloats = vertexCount * floatsPerVertex;
// Resize buffer if needed
// 如果需要,调整缓冲区大小
if (this.vertexBuffer.length < totalFloats) {
this.vertexBuffer = new Float32Array(totalFloats);
}
const hasUVs = uvs && uvs.length >= vertexCount * 2;
const hasColors = colors && colors.length >= vertexCount * 4;
for (let i = 0; i < vertexCount; i++) {
const vBase = i * 3;
const uvBase = i * 2;
const colorBase = i * 4;
const outBase = i * floatsPerVertex;
// Position
this.vertexBuffer[outBase] = vertices[vBase];
this.vertexBuffer[outBase + 1] = vertices[vBase + 1];
this.vertexBuffer[outBase + 2] = vertices[vBase + 2];
// UV
this.vertexBuffer[outBase + 3] = hasUVs ? uvs![uvBase] : 0;
this.vertexBuffer[outBase + 4] = hasUVs ? uvs![uvBase + 1] : 0;
// Color (RGBA)
this.vertexBuffer[outBase + 5] = hasColors ? colors![colorBase] : 1;
this.vertexBuffer[outBase + 6] = hasColors ? colors![colorBase + 1] : 1;
this.vertexBuffer[outBase + 7] = hasColors ? colors![colorBase + 2] : 1;
this.vertexBuffer[outBase + 8] = hasColors ? colors![colorBase + 3] : 1;
}
return this.vertexBuffer.subarray(0, totalFloats);
}
}

View File

@@ -0,0 +1,186 @@
/**
* SkeletonBakingSystem - System for baking skeleton matrices.
* SkeletonBakingSystem - 骨骼矩阵烘焙系统。
*/
import { EntitySystem, Matcher, ECSSystem, Entity } from '@esengine/ecs-framework';
import { SkeletonComponent, type BoneTransform } from '../SkeletonComponent';
import { MeshComponent } from '../MeshComponent';
/**
* System for computing skeleton bone matrices.
* 用于计算骨骼矩阵的系统。
*
* Runs after Animation3DSystem to compute world matrices and final skinning matrices.
* 在 Animation3DSystem 之后运行,计算世界矩阵和最终蒙皮矩阵。
*/
@ECSSystem('SkeletonBaking', { updateOrder: 110 })
export class SkeletonBakingSystem extends EntitySystem {
// Temporary matrix for calculations
// 用于计算的临时矩阵
private tempMatrix: Float32Array = new Float32Array(16);
private tempMatrix2: Float32Array = new Float32Array(16);
constructor() {
super(Matcher.empty().all(SkeletonComponent).all(MeshComponent));
}
/**
* Process entities each frame.
* 每帧处理实体。
*/
protected override process(entities: readonly Entity[]): void {
for (const entity of entities) {
if (!entity.enabled) continue;
this.updateEntity(entity);
}
}
/**
* Update a single entity's skeleton matrices.
* 更新单个实体的骨骼矩阵。
*/
private updateEntity(entity: Entity): void {
const skeleton = entity.getComponent(SkeletonComponent);
if (!skeleton || !skeleton.isLoaded || !skeleton.isDirty()) {
return;
}
const joints = skeleton.joints;
const boneTransforms = skeleton.boneTransforms;
// Phase 1: Compute world matrices (parent-to-child order)
// 阶段1: 计算世界矩阵(父到子顺序)
for (let i = 0; i < joints.length; i++) {
const joint = joints[i];
const localTransform = boneTransforms[i];
// Build local transform matrix
// 构建局部变换矩阵
this.buildTransformMatrix(localTransform, this.tempMatrix);
if (joint.parentIndex >= 0) {
// Multiply parent world matrix by local matrix
// 将父世界矩阵乘以局部矩阵
const parentWorld = skeleton.getWorldMatrix(joint.parentIndex);
if (parentWorld) {
this.multiplyMatrices(parentWorld, this.tempMatrix, this.tempMatrix2);
skeleton.setWorldMatrix(i, this.tempMatrix2);
} else {
skeleton.setWorldMatrix(i, this.tempMatrix);
}
} else {
// Root bone - world matrix is local matrix
// 根骨骼 - 世界矩阵就是局部矩阵
skeleton.setWorldMatrix(i, this.tempMatrix);
}
}
// Phase 2: Compute final matrices (world * inverseBindMatrix)
// 阶段2: 计算最终矩阵(世界矩阵 * 逆绑定矩阵)
for (let i = 0; i < joints.length; i++) {
const joint = joints[i];
const worldMatrix = skeleton.getWorldMatrix(i);
if (worldMatrix && joint.inverseBindMatrix) {
this.multiplyMatrices(worldMatrix, joint.inverseBindMatrix, this.tempMatrix);
skeleton.setFinalMatrix(i, this.tempMatrix);
}
}
// Clear dirty flag
// 清除脏标记
skeleton.clearDirty();
}
/**
* Build 4x4 transform matrix from BoneTransform.
* 从 BoneTransform 构建 4x4 变换矩阵。
*/
private buildTransformMatrix(transform: BoneTransform, out: Float32Array): void {
const [px, py, pz] = transform.position;
const [qx, qy, qz, qw] = transform.rotation;
const [sx, sy, sz] = transform.scale;
// Build rotation matrix from quaternion
// 从四元数构建旋转矩阵
const x2 = qx + qx;
const y2 = qy + qy;
const z2 = qz + qz;
const xx = qx * x2;
const xy = qx * y2;
const xz = qx * z2;
const yy = qy * y2;
const yz = qy * z2;
const zz = qz * z2;
const wx = qw * x2;
const wy = qw * y2;
const wz = qw * z2;
// Column 0 (with scale)
out[0] = (1 - (yy + zz)) * sx;
out[1] = (xy + wz) * sx;
out[2] = (xz - wy) * sx;
out[3] = 0;
// Column 1 (with scale)
out[4] = (xy - wz) * sy;
out[5] = (1 - (xx + zz)) * sy;
out[6] = (yz + wx) * sy;
out[7] = 0;
// Column 2 (with scale)
out[8] = (xz + wy) * sz;
out[9] = (yz - wx) * sz;
out[10] = (1 - (xx + yy)) * sz;
out[11] = 0;
// Column 3 (translation)
out[12] = px;
out[13] = py;
out[14] = pz;
out[15] = 1;
}
/**
* Multiply two 4x4 matrices (column-major).
* 乘以两个 4x4 矩阵(列优先)。
*/
private multiplyMatrices(a: Float32Array, b: Float32Array, out: Float32Array): void {
const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3];
const a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7];
const a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11];
const a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
let b0, b1, b2, b3;
// Column 0
b0 = b[0]; b1 = b[1]; b2 = b[2]; b3 = b[3];
out[0] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
out[1] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
out[2] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
out[3] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
// Column 1
b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7];
out[4] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
out[5] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
out[6] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
out[7] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
// Column 2
b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11];
out[8] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
out[9] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
out[10] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
out[11] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
// Column 3
b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15];
out[12] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
out[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
out[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
out[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
}
}