hypercube-compute 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/demo/index.html +78 -0
- package/demo/package-lock.json +1016 -0
- package/demo/package.json +15 -0
- package/demo/src/main.ts +153 -0
- package/demo/vite.config.ts +9 -0
- package/dist/index.d.mts +321 -0
- package/dist/index.d.ts +321 -0
- package/dist/index.js +1392 -0
- package/dist/index.mjs +1351 -0
- package/docs/assets/index-BLyglqQr.js +1 -0
- package/docs/index.html +78 -0
- package/package.json +29 -0
- package/src/Triade.ts +45 -0
- package/src/addons/ocean-simulation/OceanEngine.ts +208 -0
- package/src/addons/ocean-simulation/OceanSimulatorAddon.ts +145 -0
- package/src/addons/ocean-simulation/OceanWebGLRenderer.ts +258 -0
- package/src/addons/ocean-simulation/OceanWorld.ts +280 -0
- package/src/core/TriadeCubeV2.ts +58 -0
- package/src/core/TriadeGrid.ts +119 -0
- package/src/core/TriadeMasterBuffer.ts +37 -0
- package/src/engines/AerodynamicsEngine.ts +134 -0
- package/src/engines/EcosystemEngineO1.ts +73 -0
- package/src/engines/GameOfLifeEngine.ts +61 -0
- package/src/engines/HeatmapEngine.ts +60 -0
- package/src/engines/ITriadeEngine.ts +29 -0
- package/src/index.ts +26 -0
- package/src/io/CanvasAdapter.ts +41 -0
- package/src/io/WebGLAdapter.ts +129 -0
- package/src/templates/BlankEngine.ts +48 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var U=Object.defineProperty;var G=(t,s,c)=>s in t?U(t,s,{enumerable:!0,configurable:!0,writable:!0,value:c}):t[s]=c;var w=(t,s,c)=>G(t,typeof s!="symbol"?s+"":s,c);(function(){const s=document.createElement("link").relList;if(s&&s.supports&&s.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))n(e);new MutationObserver(e=>{for(const o of e)if(o.type==="childList")for(const g of o.addedNodes)g.tagName==="LINK"&&g.rel==="modulepreload"&&n(g)}).observe(document,{childList:!0,subtree:!0});function c(e){const o={};return e.integrity&&(o.integrity=e.integrity),e.referrerPolicy&&(o.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?o.credentials="include":e.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function n(e){if(e.ep)return;e.ep=!0;const o=c(e);fetch(e.href,o)}})();var Z=class{constructor(t=100*1024*1024){w(this,"buffer");w(this,"offset",0);this.buffer=new ArrayBuffer(t)}allocateCube(t,s=6){const e=t*t*Float32Array.BYTES_PER_ELEMENT*s;if(this.offset+e>this.buffer.byteLength)throw new Error(`[TriadeMasterBuffer] Out Of Memory. Impossible d'allouer ${e} bytes supplémentaires.`);const o=this.offset;return this.offset+=e,o}getUsedMemoryInMB(){return(this.offset/(1024*1024)).toFixed(2)+" MB"}},H=class{constructor(t,s,c=6){w(this,"mapSize");w(this,"faces",[]);w(this,"offset");w(this,"engine",null);this.mapSize=t,this.offset=s.allocateCube(t,c);const n=t*t,e=n*Float32Array.BYTES_PER_ELEMENT;for(let o=0;o<c;o++)this.faces.push(new Float32Array(s.buffer,this.offset+o*e,n))}setEngine(t){this.engine=t}compute(){if(!this.engine){console.warn("[TriadeCubeV2] Aucun Engine (Cerveau) n'a été assigné à ce cube.");return}this.engine.compute(this.faces,this.mapSize)}clearFace(t){this.faces[t].fill(0)}},K=class{constructor(t,s,c,n,e,o=6,g=!0){w(this,"cubes",[]);w(this,"cols");w(this,"rows");w(this,"cubeSize");w(this,"isPeriodic");this.cols=t,this.rows=s,this.cubeSize=c,this.isPeriodic=g;for(let l=0;l<s;l++){this.cubes[l]=[];for(let u=0;u<t;u++){const h=new H(c,n,o);h.setEngine(e()),this.cubes[l][u]=h}}}compute(t=0){var c;for(let n=0;n<this.rows;n++)for(let e=0;e<this.cols;e++)(c=this.cubes[n][e])==null||c.compute();const s=Array.isArray(t)?t:[t];for(const n of s)this.synchronizeBoundaries(n)}synchronizeBoundaries(t){const s=this.cubeSize,c=s-1,n=s-2;for(let e=0;e<this.rows;e++)for(let o=0;o<this.cols;o++){const l=this.cubes[e][o].faces[t];if(o<this.cols-1||this.isPeriodic){const h=this.cubes[e][(o+1)%this.cols].faces[t];for(let d=1;d<c;d++)h[d*s+0]=l[d*s+n]}if(o>0||this.isPeriodic){const h=this.cubes[e][(o-1+this.cols)%this.cols].faces[t];for(let d=1;d<c;d++)h[d*s+c]=l[d*s+1]}}for(let e=0;e<this.rows;e++)for(let o=0;o<this.cols;o++){const l=this.cubes[e][o].faces[t];(e<this.rows-1||this.isPeriodic)&&this.cubes[(e+1)%this.rows][o].faces[t].set(l.subarray(n*s,n*s+s),0),(e>0||this.isPeriodic)&&this.cubes[(e-1+this.rows)%this.rows][o].faces[t].set(l.subarray(1*s,1*s+s),c*s)}}},V=class{constructor(){w(this,"dragScore",0);w(this,"initialized",!1)}get name(){return"Lattice Boltzmann D2Q9 (O(1))"}compute(t,s){const c=s,n=t[18],e=t[19],o=t[20],g=t[21],l=[0,1,0,-1,0,1,-1,-1,1],u=[0,0,1,0,-1,1,1,-1,-1],h=[4/9,1/9,1/9,1/9,1/9,1/36,1/36,1/36,1/36],d=[0,3,4,1,2,7,8,5,6],v=.12,_=1.95;if(!this.initialized){for(let i=0;i<c*c;i++){const a=v,p=0,y=a*a+p*p;for(let x=0;x<9;x++){const P=l[x]*a+u[x]*p,r=h[x]*1*(1+3*P+4.5*P*P-1.5*y);t[x][i]=r,t[x+9][i]=r}}this.initialized=!0}let m=0;for(let i=1;i<c-1;i++)for(let b=1;b<c-1;b++){const a=i*c+b;if(n[a]>0){for(let r=1;r<9;r++){const E=b-l[r],B=(i-u[r])*c+E;t[d[r]+9][B]=t[r][B],r===1&&(m+=t[1][B])}e[a]=0,o[a]=0;continue}let p=0,y=0,x=0;for(let r=0;r<9;r++){const E=t[r][a];p+=E,y+=l[r]*E,x+=u[r]*E}b===1&&(y=v*p,x=0),p>0&&(y/=p,x/=p),e[a]=y,o[a]=x;const P=y*y+x*x;for(let r=0;r<9;r++){const E=l[r]*y+u[r]*x,N=h[r]*p*(1+3*E+4.5*E*E-1.5*P),B=t[r][a]*(1-_)+N*_;let Y=b+l[r],S=i+u[r];S<1?S=c-2:S>c-2&&(S=1),Y>c-2&&(Y=c-2),t[r+9][S*c+Y]=B}}for(let i=0;i<9;i++)t[i].set(t[i+9]);this.dragScore=this.dragScore*.9+m*1e3*.1;for(let i=1;i<c-1;i++)for(let b=1;b<c-1;b++){const a=i*c+b,p=o[a+1]-o[a-1],y=e[a+c]-e[a-c];g[a]=p-y}}};const M=document.getElementById("triade-canvas"),I=M.getContext("2d"),D=2,F=2,f=64,L=D*f,O=F*f,W=new Z,A=new K(D,F,f,W,()=>new V,22,!0),Q=L/2,$=O/2,T=15;for(let t=0;t<F;t++)for(let s=0;s<D;s++){const c=A.cubes[t][s];if(!c)continue;const n=c.faces[18];for(let e=0;e<f;e++)for(let o=0;o<f;o++){const g=s*f+o,l=t*f+e;(g-Q)**2+(l-$)**2<T*T?n[e*f+o]=1:n[e*f+o]=0}}let q=!1;M.addEventListener("mousedown",t=>{q=!0,X(t)});window.addEventListener("mouseup",()=>q=!1);M.addEventListener("mousemove",t=>{q&&X(t)});function X(t){var g;const s=M.getBoundingClientRect(),c=L/s.width,n=O/s.height,e=Math.floor((t.clientX-s.left)*c),o=Math.floor((t.clientY-s.top)*n);if(e>=0&&e<L&&o>=0&&o<O){const l=Math.floor(e/f),u=Math.floor(o/f),h=e%f,d=o%f,v=(g=A.cubes[u])==null?void 0:g[l];if(!v)return;const _=2;for(let m=-_;m<=_;m++)for(let i=-_;i<=_;i++){const b=h+i,a=d+m;b>=0&&b<f&&a>=0&&a<f&&(v.faces[18][a*f+b]=1)}}}const z=I.createImageData(L,O),C=z.data;function R(){requestAnimationFrame(R),A.compute([0,1,2,3,4,5,6,7,8,18,19,20,21]);for(let s=0;s<F;s++)for(let c=0;c<D;c++){const n=A.cubes[s][c];if(!n)continue;const e=n.faces[18],o=n.faces[19],g=n.faces[20],l=n.faces[21];for(let u=0;u<f;u++)for(let h=0;h<f;h++){const d=u*f+h,v=c*f+h,m=((s*f+u)*L+v)*4;if(e[d]>.5)C[m]=150,C[m+1]=150,C[m+2]=150,C[m+3]=255;else{const i=l[d],b=o[d]**2+g[d]**2,a=Math.max(0,i*2e4),p=Math.max(0,-i*2e4),y=Math.min(255,b*1e4);C[m]=Math.min(255,a+y*.2),C[m+1]=Math.min(255,y*.5),C[m+2]=Math.min(255,p+y),C[m+3]=255}}}const t=document.createElement("canvas");t.width=L,t.height=O,t.getContext("2d").putImageData(z,0,0),I.clearRect(0,0,M.width,M.height),I.imageSmoothingEnabled=!1,I.drawImage(t,0,0,M.width,M.height)}R();
|
package/docs/index.html
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Triade Engine Demo</title>
|
|
8
|
+
<style>
|
|
9
|
+
body,
|
|
10
|
+
html {
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
background: #0a0a0a;
|
|
14
|
+
color: white;
|
|
15
|
+
font-family: 'Inter', sans-serif;
|
|
16
|
+
height: 100vh;
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
canvas {
|
|
25
|
+
box-shadow: 0 0 40px rgba(0, 255, 136, 0.1);
|
|
26
|
+
border-radius: 8px;
|
|
27
|
+
border: 1px solid #222;
|
|
28
|
+
image-rendering: pixelated;
|
|
29
|
+
width: 512px;
|
|
30
|
+
height: 512px;
|
|
31
|
+
background: #111;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.header {
|
|
35
|
+
margin-bottom: 25px;
|
|
36
|
+
text-align: center;
|
|
37
|
+
z-index: 10;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
h1 {
|
|
41
|
+
margin: 0;
|
|
42
|
+
font-size: 28px;
|
|
43
|
+
color: #00ff88;
|
|
44
|
+
letter-spacing: -0.5px;
|
|
45
|
+
font-weight: 600;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
p {
|
|
49
|
+
margin: 8px 0 0;
|
|
50
|
+
color: #888;
|
|
51
|
+
font-size: 15px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.tag {
|
|
55
|
+
display: inline-block;
|
|
56
|
+
padding: 4px 10px;
|
|
57
|
+
border-radius: 12px;
|
|
58
|
+
background: rgba(0, 255, 136, 0.1);
|
|
59
|
+
color: #00ff88;
|
|
60
|
+
font-size: 12px;
|
|
61
|
+
font-weight: bold;
|
|
62
|
+
margin-bottom: 15px;
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
65
|
+
<script type="module" crossorigin src="./assets/index-BLyglqQr.js"></script>
|
|
66
|
+
</head>
|
|
67
|
+
|
|
68
|
+
<body>
|
|
69
|
+
<div class="header">
|
|
70
|
+
<div class="tag">Zero-Allocation Compute</div>
|
|
71
|
+
<h1>🌊 Triade GodMode O(1)</h1>
|
|
72
|
+
<p>Lattice Boltzmann (LBM D2Q9) Fluid Mechanics Simulation.<br /><b>Click & Drag</b> on the grid to draw solid
|
|
73
|
+
walls. The fluid flows from Left to Right!</p>
|
|
74
|
+
</div>
|
|
75
|
+
<canvas id="triade-canvas" width="512" height="512"></canvas>
|
|
76
|
+
</body>
|
|
77
|
+
|
|
78
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hypercube-compute",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "A high-performance O(1) tensor-based compute engine for Web and Node.js environments.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
10
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"tensor",
|
|
15
|
+
"compute",
|
|
16
|
+
"O(1)",
|
|
17
|
+
"engine",
|
|
18
|
+
"physics",
|
|
19
|
+
"webassembly",
|
|
20
|
+
"web-workers",
|
|
21
|
+
"lattice-boltzmann"
|
|
22
|
+
],
|
|
23
|
+
"author": "Helron1977",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"tsup": "^8.0.2",
|
|
27
|
+
"typescript": "^5.3.3"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/Triade.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { TriadeMasterBuffer } from './core/TriadeMasterBuffer';
|
|
2
|
+
import { TriadeCubeV2 } from './core/TriadeCubeV2';
|
|
3
|
+
import type { ITriadeEngine } from './engines/ITriadeEngine';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Chef d'orchestre de la V2.
|
|
7
|
+
* Il possède la VRAM globale partagée et gère les instanciations de Cubes sans fragmentation RAM.
|
|
8
|
+
*/
|
|
9
|
+
export class Triade {
|
|
10
|
+
private _masterBuffer: TriadeMasterBuffer;
|
|
11
|
+
public cubes: Map<string, TriadeCubeV2> = new Map();
|
|
12
|
+
|
|
13
|
+
get masterBuffer() {
|
|
14
|
+
return this._masterBuffer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param vRamAllocMegabytes Taille du ArrayBuffer en Mega-Octets (par defaut 50MB)
|
|
19
|
+
*/
|
|
20
|
+
constructor(vRamAllocMegabytes: number = 50) {
|
|
21
|
+
this._masterBuffer = new TriadeMasterBuffer(vRamAllocMegabytes * 1024 * 1024);
|
|
22
|
+
console.log(`[Triade.js SDK] Initialized with ${vRamAllocMegabytes}MB of Raw Buffer Memory.`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Forge un nouveau Cube (View Paging) depuis le Buffer Maître.
|
|
27
|
+
*/
|
|
28
|
+
public createCube(name: string, mapSize: number, engine: ITriadeEngine, numFaces: number = 6): TriadeCubeV2 {
|
|
29
|
+
if (this.cubes.has(name)) {
|
|
30
|
+
throw new Error(`Cube avec le nom ${name} existe déjà.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const cube = new TriadeCubeV2(mapSize, this._masterBuffer, numFaces);
|
|
34
|
+
cube.setEngine(engine);
|
|
35
|
+
this.cubes.set(name, cube);
|
|
36
|
+
return cube;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Accès sécurisé à un cube pour UI/Render logic.
|
|
41
|
+
*/
|
|
42
|
+
public getCube(id: string): TriadeCubeV2 | undefined {
|
|
43
|
+
return this.cubes.get(id);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { ITriadeEngine } from '../../engines/ITriadeEngine';
|
|
2
|
+
|
|
3
|
+
export interface OceanEngineParams {
|
|
4
|
+
tau_0: number;
|
|
5
|
+
smagorinsky: number;
|
|
6
|
+
cflLimit: number;
|
|
7
|
+
bioDiffusion: number;
|
|
8
|
+
bioGrowth: number;
|
|
9
|
+
vortexRadius: number;
|
|
10
|
+
vortexStrength: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class OceanEngine implements ITriadeEngine {
|
|
14
|
+
public readonly name = "OceanEngine";
|
|
15
|
+
|
|
16
|
+
// Re-use lab-perfect constants
|
|
17
|
+
private readonly w = [4 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 36, 1 / 36, 1 / 36, 1 / 36];
|
|
18
|
+
private readonly cx = [0, 1, 0, -1, 0, 1, -1, -1, 1];
|
|
19
|
+
private readonly cy = [0, 0, 1, 0, -1, 1, 1, -1, -1];
|
|
20
|
+
private readonly opp = [0, 3, 4, 1, 2, 7, 8, 5, 6];
|
|
21
|
+
|
|
22
|
+
// Caches to avoid per-frame allocations
|
|
23
|
+
private feq_cache = new Float32Array(9);
|
|
24
|
+
private pulled_f = new Float32Array(9);
|
|
25
|
+
|
|
26
|
+
public params: OceanEngineParams = {
|
|
27
|
+
tau_0: 0.8,
|
|
28
|
+
smagorinsky: 0.2,
|
|
29
|
+
cflLimit: 0.38,
|
|
30
|
+
bioDiffusion: 0.05,
|
|
31
|
+
bioGrowth: 0.0005,
|
|
32
|
+
vortexRadius: 28,
|
|
33
|
+
vortexStrength: 0.02
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
public stats = {
|
|
37
|
+
maxU: 0,
|
|
38
|
+
avgTau: 0
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// UI Input simulation (will be fed by the high-level framework/addon)
|
|
42
|
+
public interaction = {
|
|
43
|
+
mouseX: 0,
|
|
44
|
+
mouseY: 0,
|
|
45
|
+
active: false
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
constructor() { }
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Entry point: Orchestrates LBM and Bio steps
|
|
52
|
+
*/
|
|
53
|
+
compute(faces: Float32Array[], size: number): void {
|
|
54
|
+
this.stepLBM(faces, size);
|
|
55
|
+
this.stepBio(faces, size);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private stepLBM(m: Float32Array[], size: number): void {
|
|
59
|
+
const rho = m[20], ux = m[18], uy = m[19], obst = m[22];
|
|
60
|
+
|
|
61
|
+
let maxU = 0;
|
|
62
|
+
let sumTau = 0;
|
|
63
|
+
let activeCells = 0;
|
|
64
|
+
|
|
65
|
+
// 0. CLEAR NEXT FRAME BUFFERS
|
|
66
|
+
for (let k = 0; k < 9; k++) m[k + 9].fill(0);
|
|
67
|
+
|
|
68
|
+
const mx = this.interaction.mouseX;
|
|
69
|
+
const my = this.interaction.mouseY;
|
|
70
|
+
const isForcing = this.interaction.active;
|
|
71
|
+
const vr2 = this.params.vortexRadius * this.params.vortexRadius;
|
|
72
|
+
|
|
73
|
+
// 1. PULL-STREAMING & COLLISION (O1 Optimized)
|
|
74
|
+
// We only compute inner domain. Ghost cells (0 and size-1) are managed by Grid Boundary Exchange.
|
|
75
|
+
for (let y = 1; y < size - 1; y++) {
|
|
76
|
+
for (let x = 1; x < size - 1; x++) {
|
|
77
|
+
const i = y * size + x;
|
|
78
|
+
|
|
79
|
+
if (obst[i] > 0.5) {
|
|
80
|
+
for (let k = 0; k < 9; k++) m[k + 9][i] = this.w[k];
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- PULL STREAMING ---
|
|
85
|
+
let r = 0, vx = 0, vy = 0;
|
|
86
|
+
|
|
87
|
+
for (let k = 0; k < 9; k++) {
|
|
88
|
+
const nx = x - this.cx[k];
|
|
89
|
+
const ny = y - this.cy[k];
|
|
90
|
+
|
|
91
|
+
const ni = ny * size + nx;
|
|
92
|
+
if (obst[ni] > 0.5) {
|
|
93
|
+
this.pulled_f[k] = m[this.opp[k]][i]; // Bounce back against internal obstacles
|
|
94
|
+
} else {
|
|
95
|
+
this.pulled_f[k] = m[k][ni]; // Stream from adjacent cell (can be Ghost Cell)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
r += this.pulled_f[k];
|
|
99
|
+
vx += this.pulled_f[k] * this.cx[k];
|
|
100
|
+
vy += this.pulled_f[k] * this.cy[k];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Stability Clamping
|
|
104
|
+
let isShockwave = false;
|
|
105
|
+
if (r < 0.8) { r = 0.8; isShockwave = true; }
|
|
106
|
+
else if (r > 1.2) { r = 1.2; isShockwave = true; }
|
|
107
|
+
else if (r < 0.0001) r = 0.0001;
|
|
108
|
+
|
|
109
|
+
vx /= r;
|
|
110
|
+
vy /= r;
|
|
111
|
+
|
|
112
|
+
// Vortex Forcing
|
|
113
|
+
if (isForcing) {
|
|
114
|
+
const dx = x - mx;
|
|
115
|
+
const dy = y - my;
|
|
116
|
+
const dist2 = dx * dx + dy * dy;
|
|
117
|
+
if (dist2 < vr2) {
|
|
118
|
+
const forceScale = this.params.vortexStrength * 0.005 * (1.0 - Math.sqrt(dist2) / this.params.vortexRadius);
|
|
119
|
+
vx += -dy * forceScale;
|
|
120
|
+
vy += dx * forceScale;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const v2 = vx * vx + vy * vy;
|
|
125
|
+
const speed = Math.sqrt(v2);
|
|
126
|
+
if (speed > maxU) maxU = speed;
|
|
127
|
+
|
|
128
|
+
let u2_clamped = v2;
|
|
129
|
+
if (speed > this.params.cflLimit) {
|
|
130
|
+
const scale = this.params.cflLimit / speed;
|
|
131
|
+
vx *= scale;
|
|
132
|
+
vy *= scale;
|
|
133
|
+
u2_clamped = vx * vx + vy * vy;
|
|
134
|
+
isShockwave = true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Store Macros
|
|
138
|
+
rho[i] = r;
|
|
139
|
+
ux[i] = vx;
|
|
140
|
+
uy[i] = vy;
|
|
141
|
+
|
|
142
|
+
// --- COLLISION ---
|
|
143
|
+
if (isShockwave) {
|
|
144
|
+
for (let k = 0; k < 9; k++) {
|
|
145
|
+
const cu = 3 * (this.cx[k] * vx + this.cy[k] * vy);
|
|
146
|
+
m[k + 9][i] = this.w[k] * r * (1 + cu + 0.5 * cu * cu - 1.5 * u2_clamped);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
let Pxx = 0, Pyy = 0, Pxy = 0;
|
|
150
|
+
|
|
151
|
+
for (let k = 0; k < 9; k++) {
|
|
152
|
+
const cu = 3 * (this.cx[k] * vx + this.cy[k] * vy);
|
|
153
|
+
this.feq_cache[k] = this.w[k] * r * (1 + cu + 0.5 * cu * cu - 1.5 * u2_clamped);
|
|
154
|
+
|
|
155
|
+
const fneq = this.pulled_f[k] - this.feq_cache[k];
|
|
156
|
+
Pxx += fneq * this.cx[k] * this.cx[k];
|
|
157
|
+
Pyy += fneq * this.cy[k] * this.cy[k];
|
|
158
|
+
Pxy += fneq * this.cx[k] * this.cy[k];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const S_norm = Math.sqrt(2 * (Pxx * Pxx + Pyy * Pyy + 2 * Pxy * Pxy));
|
|
162
|
+
let tau_eff = this.params.tau_0 + this.params.smagorinsky * S_norm;
|
|
163
|
+
if (isNaN(tau_eff) || tau_eff < 0.505) tau_eff = 0.505;
|
|
164
|
+
|
|
165
|
+
sumTau += tau_eff;
|
|
166
|
+
activeCells++;
|
|
167
|
+
|
|
168
|
+
for (let k = 0; k < 9; k++) {
|
|
169
|
+
m[k + 9][i] = this.pulled_f[k] - (this.pulled_f[k] - this.feq_cache[k]) / tau_eff;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (activeCells > 0) this.stats.avgTau = sumTau / activeCells;
|
|
176
|
+
this.stats.maxU = maxU;
|
|
177
|
+
|
|
178
|
+
// 4. MEMORY SWAP
|
|
179
|
+
for (let k = 0; k < 9; k++) {
|
|
180
|
+
const tmp = m[k];
|
|
181
|
+
m[k] = m[k + 9];
|
|
182
|
+
m[k + 9] = tmp;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private stepBio(m: Float32Array[], size: number): void {
|
|
187
|
+
const bio = m[21];
|
|
188
|
+
const bio_next = m[17]; // Proxy temp
|
|
189
|
+
const area = size * size;
|
|
190
|
+
|
|
191
|
+
for (let y = 1; y < size - 1; y++) {
|
|
192
|
+
for (let x = 1; x < size - 1; x++) {
|
|
193
|
+
const i = y * size + x;
|
|
194
|
+
|
|
195
|
+
const lap = bio[i - 1] + bio[i + 1] + bio[i - size] + bio[i + size] - 4 * bio[i];
|
|
196
|
+
let next = bio[i] + this.params.bioDiffusion * lap + this.params.bioGrowth * bio[i] * (1 - bio[i]);
|
|
197
|
+
|
|
198
|
+
if (next < 0) next = 0;
|
|
199
|
+
if (next > 1) next = 1;
|
|
200
|
+
bio_next[i] = next;
|
|
201
|
+
|
|
202
|
+
// Bio no longer acts as physical obstacle to allow the fixed Island to persist.
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (let i = 0; i < area; i++) bio[i] = bio_next[i];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { TriadeMasterBuffer } from '../../core/TriadeMasterBuffer';
|
|
2
|
+
import { TriadeCubeV2 } from '../../core/TriadeCubeV2';
|
|
3
|
+
import { OceanEngine } from './OceanEngine';
|
|
4
|
+
|
|
5
|
+
export interface Boat {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
vx: number;
|
|
9
|
+
vy: number;
|
|
10
|
+
length: number;
|
|
11
|
+
angle: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class OceanSimulatorAddon {
|
|
15
|
+
public readonly size: number;
|
|
16
|
+
private master: TriadeMasterBuffer;
|
|
17
|
+
private cube: TriadeCubeV2;
|
|
18
|
+
private engine: OceanEngine;
|
|
19
|
+
|
|
20
|
+
public boats: Boat[] = [];
|
|
21
|
+
|
|
22
|
+
constructor(size: number = 256) {
|
|
23
|
+
this.size = size;
|
|
24
|
+
|
|
25
|
+
// 1. Initialize Triade Memory System (24 faces for LBM + Bio + Temp)
|
|
26
|
+
this.master = new TriadeMasterBuffer();
|
|
27
|
+
this.cube = new TriadeCubeV2(size, this.master, 24);
|
|
28
|
+
|
|
29
|
+
// 2. Initialize Engine
|
|
30
|
+
this.engine = new OceanEngine();
|
|
31
|
+
this.cube.setEngine(this.engine);
|
|
32
|
+
|
|
33
|
+
// 3. Initialize distributions to perfect rest equilibrium
|
|
34
|
+
const w = [4 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 36, 1 / 36, 1 / 36, 1 / 36];
|
|
35
|
+
for (let k = 0; k < 9; k++) {
|
|
36
|
+
this.cube.faces[k].fill(w[k]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Engine parameters access
|
|
42
|
+
*/
|
|
43
|
+
get params() { return this.engine.params; }
|
|
44
|
+
get stats() { return this.engine.stats; }
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* High-level Boat API
|
|
48
|
+
*/
|
|
49
|
+
addBoat(x: number, y: number, length: number = 20): void {
|
|
50
|
+
this.boats.push({ x, y, vx: 0, vy: 0, length, angle: 0 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Global Step: Fluid -> Bio -> Boats
|
|
55
|
+
*/
|
|
56
|
+
step(): void {
|
|
57
|
+
// Run Triade Math Engine
|
|
58
|
+
this.cube.compute();
|
|
59
|
+
|
|
60
|
+
// Run Boat Physics
|
|
61
|
+
const ux = this.cube.faces[18];
|
|
62
|
+
const uy = this.cube.faces[19];
|
|
63
|
+
const size = this.size;
|
|
64
|
+
|
|
65
|
+
for (const boat of this.boats) {
|
|
66
|
+
// Sample fluid velocity under boat
|
|
67
|
+
const ix = Math.floor(boat.x);
|
|
68
|
+
const iy = Math.floor(boat.y);
|
|
69
|
+
|
|
70
|
+
if (ix >= 0 && ix < size && iy >= 0 && iy < size) {
|
|
71
|
+
const i = iy * size + ix;
|
|
72
|
+
const fux = ux[i];
|
|
73
|
+
const fuy = uy[i];
|
|
74
|
+
|
|
75
|
+
// Advection: Boat is carried by fluid
|
|
76
|
+
boat.vx += fux * 0.15 - boat.vx * 0.05; // Force + Friction
|
|
77
|
+
boat.vy += fuy * 0.15 - boat.vy * 0.05;
|
|
78
|
+
|
|
79
|
+
// Move boat
|
|
80
|
+
boat.x += boat.vx;
|
|
81
|
+
boat.y += boat.vy;
|
|
82
|
+
|
|
83
|
+
// Update orientation based on velocity
|
|
84
|
+
if (Math.abs(boat.vx) > 0.01 || Math.abs(boat.vy) > 0.01) {
|
|
85
|
+
boat.angle = Math.atan2(boat.vy, boat.vx);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Periodic bounds for boats (optional)
|
|
89
|
+
boat.x = (boat.x + size) % size;
|
|
90
|
+
boat.y = (boat.y + size) % size;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Interaction bridge
|
|
97
|
+
*/
|
|
98
|
+
setInteraction(x: number, y: number, active: boolean): void {
|
|
99
|
+
this.engine.interaction.mouseX = x;
|
|
100
|
+
this.engine.interaction.mouseY = y;
|
|
101
|
+
this.engine.interaction.active = active;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Helper to render everything to a canvas
|
|
106
|
+
*/
|
|
107
|
+
render(ctx: CanvasRenderingContext2D, imageData: ImageData): void {
|
|
108
|
+
const size = this.size;
|
|
109
|
+
const bio = this.cube.faces[21];
|
|
110
|
+
const ux = this.cube.faces[18];
|
|
111
|
+
const uy = this.cube.faces[19];
|
|
112
|
+
const obst = this.cube.faces[22];
|
|
113
|
+
const data = imageData.data;
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < size * size; i++) {
|
|
116
|
+
const j = i * 4;
|
|
117
|
+
if (obst[i] > 0.5) {
|
|
118
|
+
data[j] = 20; data[j + 1] = 20; data[j + 2] = 40; data[j + 3] = 255;
|
|
119
|
+
} else {
|
|
120
|
+
const speed = Math.sqrt(ux[i] * ux[i] + uy[i] * uy[i]);
|
|
121
|
+
const intensity = speed * 1200;
|
|
122
|
+
data[j] = Math.min(255, bio[i] * 40 + intensity * 0.1);
|
|
123
|
+
data[j + 1] = Math.min(255, bio[i] * 120 + intensity * 0.4);
|
|
124
|
+
data[j + 2] = Math.min(255, intensity * 2.5 + 20);
|
|
125
|
+
data[j + 3] = 255;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
ctx.putImageData(imageData, 0, 0);
|
|
129
|
+
|
|
130
|
+
// Draw Boats
|
|
131
|
+
ctx.fillStyle = "#fff";
|
|
132
|
+
for (const boat of this.boats) {
|
|
133
|
+
ctx.save();
|
|
134
|
+
ctx.translate(boat.x, boat.y);
|
|
135
|
+
ctx.rotate(boat.angle);
|
|
136
|
+
ctx.fillRect(-boat.length / 2, -2, boat.length, 4);
|
|
137
|
+
ctx.restore();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Access to raw buffers for custom rendering */
|
|
142
|
+
getRawFaces() {
|
|
143
|
+
return this.cube.faces;
|
|
144
|
+
}
|
|
145
|
+
}
|