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,258 @@
|
|
|
1
|
+
import { TriadeGrid } from '../../core/TriadeGrid';
|
|
2
|
+
|
|
3
|
+
export class OceanWebGLRenderer {
|
|
4
|
+
private gl: WebGL2RenderingContext;
|
|
5
|
+
private program: WebGLProgram;
|
|
6
|
+
private indexCount: number = 0;
|
|
7
|
+
|
|
8
|
+
private texRho: WebGLTexture;
|
|
9
|
+
private texUx: WebGLTexture;
|
|
10
|
+
private texUy: WebGLTexture;
|
|
11
|
+
private texObst: WebGLTexture;
|
|
12
|
+
|
|
13
|
+
private globalW: number;
|
|
14
|
+
private globalH: number;
|
|
15
|
+
private chunkSize: number;
|
|
16
|
+
|
|
17
|
+
constructor(canvas: HTMLCanvasElement, cols: number, rows: number, chunkSize: number) {
|
|
18
|
+
this.chunkSize = chunkSize;
|
|
19
|
+
this.globalW = cols * chunkSize;
|
|
20
|
+
this.globalH = rows * chunkSize;
|
|
21
|
+
|
|
22
|
+
const gl = canvas.getContext('webgl2');
|
|
23
|
+
if (!gl) throw new Error("WebGL2 non supporté.");
|
|
24
|
+
this.gl = gl;
|
|
25
|
+
|
|
26
|
+
// Extensions pour Float32 Textures
|
|
27
|
+
this.gl.getExtension("EXT_color_buffer_float");
|
|
28
|
+
this.gl.getExtension("OES_texture_float_linear");
|
|
29
|
+
|
|
30
|
+
const vsSource = `#version 300 es
|
|
31
|
+
in vec2 a_position;
|
|
32
|
+
out vec2 v_uv;
|
|
33
|
+
out vec3 v_worldPos;
|
|
34
|
+
uniform sampler2D u_rho; // We sample density in Vertex Shader!
|
|
35
|
+
uniform sampler2D u_obst; // Also sample obstacles in Vertex Shader!
|
|
36
|
+
|
|
37
|
+
void main() {
|
|
38
|
+
v_uv = a_position * 0.5 + 0.5;
|
|
39
|
+
v_uv.y = 1.0 - v_uv.y;
|
|
40
|
+
|
|
41
|
+
// VTF: Vertex Texture Fetch (Real 3D heights from LBM Density)
|
|
42
|
+
float rho = texture(u_rho, v_uv).r;
|
|
43
|
+
float obst = texture(u_obst, v_uv).r;
|
|
44
|
+
float h = 0.0;
|
|
45
|
+
if (obst > 0.5) {
|
|
46
|
+
h = 5.0; // Island elevated plateau
|
|
47
|
+
} else {
|
|
48
|
+
h = (rho - 1.0) * 8.0; // Wave extrusion amplitude (softened)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
vec3 p = vec3(a_position.x, a_position.y, h);
|
|
52
|
+
v_worldPos = p;
|
|
53
|
+
|
|
54
|
+
// 3D Camera / Perspective Math
|
|
55
|
+
float angleX = -1.1; // Look down tilt
|
|
56
|
+
float cosX = cos(angleX);
|
|
57
|
+
float sinX = sin(angleX);
|
|
58
|
+
|
|
59
|
+
// Apply Pitch rotation
|
|
60
|
+
float yRot = p.y * cosX - p.z * sinX;
|
|
61
|
+
float zRot = p.y * sinX + p.z * cosX;
|
|
62
|
+
|
|
63
|
+
// Move camera back
|
|
64
|
+
zRot -= 2.2;
|
|
65
|
+
|
|
66
|
+
// Perspective projection
|
|
67
|
+
float fov = 1.7;
|
|
68
|
+
gl_Position = vec4(p.x * fov, (yRot - 0.2) * fov, zRot, -zRot);
|
|
69
|
+
}`;
|
|
70
|
+
|
|
71
|
+
const fsSource = `#version 300 es
|
|
72
|
+
precision highp float;
|
|
73
|
+
precision highp sampler2D;
|
|
74
|
+
|
|
75
|
+
in vec2 v_uv;
|
|
76
|
+
in vec3 v_worldPos;
|
|
77
|
+
uniform sampler2D u_rho; // LBM Density -> Heights
|
|
78
|
+
uniform sampler2D u_ux;
|
|
79
|
+
uniform sampler2D u_uy;
|
|
80
|
+
uniform sampler2D u_obst;
|
|
81
|
+
|
|
82
|
+
out vec4 outColor;
|
|
83
|
+
|
|
84
|
+
void main() {
|
|
85
|
+
float obst = texture(u_obst, v_uv).r;
|
|
86
|
+
if (obst > 0.5) {
|
|
87
|
+
// Island material
|
|
88
|
+
vec3 islandTop = vec3(0.5, 0.6, 0.3); // Grass / Forest
|
|
89
|
+
vec3 islandSide = vec3(0.7, 0.6, 0.4); // Sand / Rock
|
|
90
|
+
|
|
91
|
+
// Read local normals to detect cliffs
|
|
92
|
+
float oL = textureOffset(u_obst, v_uv, ivec2(-1, 0)).r;
|
|
93
|
+
float oR = textureOffset(u_obst, v_uv, ivec2(1, 0)).r;
|
|
94
|
+
float oB = textureOffset(u_obst, v_uv, ivec2(0, -1)).r;
|
|
95
|
+
float oT = textureOffset(u_obst, v_uv, ivec2(0, 1)).r;
|
|
96
|
+
float sumO = oL + oR + oB + oT;
|
|
97
|
+
|
|
98
|
+
if (sumO < 3.5) {
|
|
99
|
+
outColor = vec4(islandSide, 1.0); // Cliff / Beach
|
|
100
|
+
} else {
|
|
101
|
+
outColor = vec4(islandTop, 1.0); // Plateau core
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Sample density around the pixel to compute normal vector
|
|
107
|
+
float rL = textureOffset(u_rho, v_uv, ivec2(-1, 0)).r;
|
|
108
|
+
float rR = textureOffset(u_rho, v_uv, ivec2(1, 0)).r;
|
|
109
|
+
float rB = textureOffset(u_rho, v_uv, ivec2(0, -1)).r; // Inverted Y due to texture flip
|
|
110
|
+
float rT = textureOffset(u_rho, v_uv, ivec2(0, 1)).r;
|
|
111
|
+
|
|
112
|
+
// Generate Normal from heightmap (Density is height proxy)
|
|
113
|
+
float strength = 25.0; // Softened normals
|
|
114
|
+
vec3 normal = normalize(vec3((rL - rR) * strength, (rB - rT) * strength, 1.0));
|
|
115
|
+
|
|
116
|
+
// Lighting setup
|
|
117
|
+
vec3 lightDir = normalize(vec3(0.5, 0.8, 0.5)); // Sun angle
|
|
118
|
+
vec3 viewDir = normalize(vec3(0.0, -0.6, 1.0)); // Looking at the water
|
|
119
|
+
|
|
120
|
+
float ux = texture(u_ux, v_uv).r;
|
|
121
|
+
float uy = texture(u_uy, v_uv).r;
|
|
122
|
+
float speed = sqrt(ux*ux + uy*uy);
|
|
123
|
+
|
|
124
|
+
// Water Base Color based on speed (currents)
|
|
125
|
+
vec3 waterDeep = vec3(0.02, 0.2, 0.5);
|
|
126
|
+
vec3 waterShallow = vec3(0.0, 0.6, 0.9);
|
|
127
|
+
vec3 waterColor = mix(waterDeep, waterShallow, clamp(speed * 80.0, 0.0, 1.0));
|
|
128
|
+
|
|
129
|
+
// Diffuse Lighting (Lambert)
|
|
130
|
+
float diff = max(dot(normal, lightDir), 0.0);
|
|
131
|
+
vec3 diffuse = waterColor * (diff * 0.7 + 0.3); // + ambient
|
|
132
|
+
|
|
133
|
+
// Specular Lighting (Phong)
|
|
134
|
+
vec3 reflectDir = reflect(-lightDir, normal);
|
|
135
|
+
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 48.0);
|
|
136
|
+
vec3 specular = vec3(1.0, 1.0, 1.0) * spec * 0.8;
|
|
137
|
+
|
|
138
|
+
// Foam based on density peaks (breaking waves)
|
|
139
|
+
float rho_center = texture(u_rho, v_uv).r;
|
|
140
|
+
float foam = (rho_center > 1.015) ? clamp((rho_center - 1.015) * 15.0, 0.0, 1.0) : 0.0;
|
|
141
|
+
|
|
142
|
+
outColor = vec4(diffuse + specular + foam, 1.0);
|
|
143
|
+
}`;
|
|
144
|
+
|
|
145
|
+
this.program = this.createProgram(vsSource, fsSource);
|
|
146
|
+
|
|
147
|
+
// Generate 256x256 High-Res Grid Mesh for 3D displacement
|
|
148
|
+
const segments = 256;
|
|
149
|
+
const vertices = new Float32Array((segments + 1) * (segments + 1) * 2);
|
|
150
|
+
let ptr = 0;
|
|
151
|
+
for (let y = 0; y <= segments; y++) {
|
|
152
|
+
for (let x = 0; x <= segments; x++) {
|
|
153
|
+
vertices[ptr++] = (x / segments) * 2.0 - 1.0;
|
|
154
|
+
vertices[ptr++] = 1.0 - (y / segments) * 2.0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const indices = new Uint32Array(segments * segments * 6);
|
|
159
|
+
let iPtr = 0;
|
|
160
|
+
for (let y = 0; y < segments; y++) {
|
|
161
|
+
for (let x = 0; x < segments; x++) {
|
|
162
|
+
const i = x + y * (segments + 1);
|
|
163
|
+
indices[iPtr++] = i;
|
|
164
|
+
indices[iPtr++] = i + 1;
|
|
165
|
+
indices[iPtr++] = i + segments + 1;
|
|
166
|
+
indices[iPtr++] = i + segments + 1;
|
|
167
|
+
indices[iPtr++] = i + 1;
|
|
168
|
+
indices[iPtr++] = i + segments + 2;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
this.indexCount = indices.length;
|
|
172
|
+
|
|
173
|
+
const posBuf = this.gl.createBuffer();
|
|
174
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, posBuf);
|
|
175
|
+
this.gl.bufferData(this.gl.ARRAY_BUFFER, vertices, this.gl.STATIC_DRAW);
|
|
176
|
+
|
|
177
|
+
const indexBuf = this.gl.createBuffer()!;
|
|
178
|
+
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuf);
|
|
179
|
+
this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, indices, this.gl.STATIC_DRAW);
|
|
180
|
+
|
|
181
|
+
this.texRho = this.createTexture();
|
|
182
|
+
this.texUx = this.createTexture();
|
|
183
|
+
this.texUy = this.createTexture();
|
|
184
|
+
this.texObst = this.createTexture();
|
|
185
|
+
|
|
186
|
+
this.gl.useProgram(this.program);
|
|
187
|
+
this.bindTextureUniform("u_rho", 0);
|
|
188
|
+
this.bindTextureUniform("u_ux", 1);
|
|
189
|
+
this.bindTextureUniform("u_uy", 2);
|
|
190
|
+
this.bindTextureUniform("u_obst", 3);
|
|
191
|
+
|
|
192
|
+
const posLoc = this.gl.getAttribLocation(this.program, "a_position");
|
|
193
|
+
this.gl.enableVertexAttribArray(posLoc);
|
|
194
|
+
this.gl.vertexAttribPointer(posLoc, 2, this.gl.FLOAT, false, 0, 0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private createTexture() {
|
|
198
|
+
const tex = this.gl.createTexture()!;
|
|
199
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, tex);
|
|
200
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR); // Linear for smoother visual
|
|
201
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
|
|
202
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
203
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
204
|
+
|
|
205
|
+
// Allocate full global Toric map texture
|
|
206
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.R32F, this.globalW, this.globalH, 0, this.gl.RED, this.gl.FLOAT, null);
|
|
207
|
+
return tex;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private bindTextureUniform(name: string, unit: number) {
|
|
211
|
+
const loc = this.gl.getUniformLocation(this.program, name);
|
|
212
|
+
this.gl.uniform1i(loc, unit);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private updateSubTexture(tex: WebGLTexture, unit: number, xOff: number, yOff: number, data: Float32Array) {
|
|
216
|
+
this.gl.activeTexture(this.gl.TEXTURE0 + unit);
|
|
217
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, tex);
|
|
218
|
+
this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, xOff, yOff, this.chunkSize, this.chunkSize, this.gl.RED, this.gl.FLOAT, data);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
public render(grid: TriadeGrid) {
|
|
222
|
+
this.gl.useProgram(this.program);
|
|
223
|
+
|
|
224
|
+
// Upload each chunk into the master textures using O(1) subset uploads
|
|
225
|
+
for (let y = 0; y < grid.rows; y++) {
|
|
226
|
+
for (let x = 0; x < grid.cols; x++) {
|
|
227
|
+
const cube = grid.cubes[y][x];
|
|
228
|
+
if (!cube) continue;
|
|
229
|
+
|
|
230
|
+
const xOff = x * this.chunkSize;
|
|
231
|
+
const yOff = y * this.chunkSize;
|
|
232
|
+
|
|
233
|
+
// Face mapping for OceanEngine (Rho = Face 20)
|
|
234
|
+
this.updateSubTexture(this.texRho, 0, xOff, yOff, cube.faces[20]);
|
|
235
|
+
this.updateSubTexture(this.texUx, 1, xOff, yOff, cube.faces[18]);
|
|
236
|
+
this.updateSubTexture(this.texUy, 2, xOff, yOff, cube.faces[19]);
|
|
237
|
+
this.updateSubTexture(this.texObst, 3, xOff, yOff, cube.faces[22]);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Draw 3D Surface
|
|
242
|
+
this.gl.drawElements(this.gl.TRIANGLES, this.indexCount, this.gl.UNSIGNED_INT, 0);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private createProgram(vsSource: string, fsSource: string) {
|
|
246
|
+
const vs = this.gl.createShader(this.gl.VERTEX_SHADER)!;
|
|
247
|
+
this.gl.shaderSource(vs, vsSource);
|
|
248
|
+
this.gl.compileShader(vs);
|
|
249
|
+
const fs = this.gl.createShader(this.gl.FRAGMENT_SHADER)!;
|
|
250
|
+
this.gl.shaderSource(fs, fsSource);
|
|
251
|
+
this.gl.compileShader(fs);
|
|
252
|
+
const prog = this.gl.createProgram()!;
|
|
253
|
+
this.gl.attachShader(prog, vs);
|
|
254
|
+
this.gl.attachShader(prog, fs);
|
|
255
|
+
this.gl.linkProgram(prog);
|
|
256
|
+
return prog;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { TriadeMasterBuffer } from '../../core/TriadeMasterBuffer';
|
|
2
|
+
import { TriadeGrid } from '../../core/TriadeGrid';
|
|
3
|
+
import { OceanEngine } from './OceanEngine';
|
|
4
|
+
|
|
5
|
+
export interface Boat {
|
|
6
|
+
x: number; // Global coordinates
|
|
7
|
+
y: number;
|
|
8
|
+
vx: number;
|
|
9
|
+
vy: number;
|
|
10
|
+
length: number;
|
|
11
|
+
angle: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class OceanWorld {
|
|
15
|
+
public readonly cols: number;
|
|
16
|
+
public readonly rows: number;
|
|
17
|
+
public readonly chunkSize: number;
|
|
18
|
+
public readonly globalSizeW: number;
|
|
19
|
+
public readonly globalSizeH: number;
|
|
20
|
+
|
|
21
|
+
public grid: TriadeGrid;
|
|
22
|
+
public boats: Boat[] = [];
|
|
23
|
+
public keys = { up: false, down: false, left: false, right: false };
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
masterBuffer: TriadeMasterBuffer,
|
|
27
|
+
cols: number = 2,
|
|
28
|
+
rows: number = 2,
|
|
29
|
+
chunkSize: number = 64
|
|
30
|
+
) {
|
|
31
|
+
this.cols = cols;
|
|
32
|
+
this.rows = rows;
|
|
33
|
+
this.chunkSize = chunkSize;
|
|
34
|
+
this.globalSizeW = cols * chunkSize;
|
|
35
|
+
this.globalSizeH = rows * chunkSize;
|
|
36
|
+
|
|
37
|
+
// Create the TriadeGrid with continuous periodic boundaries
|
|
38
|
+
// 24 faces are required by OceanEngine (LBM D2Q9 + Macro + Bio)
|
|
39
|
+
this.grid = new TriadeGrid(cols, rows, chunkSize, masterBuffer, () => new OceanEngine(), 24, true);
|
|
40
|
+
|
|
41
|
+
this.reset();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public reset() {
|
|
45
|
+
const w = [4 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 36, 1 / 36, 1 / 36, 1 / 36];
|
|
46
|
+
|
|
47
|
+
// Reset distributions and macros
|
|
48
|
+
for (let y = 0; y < this.rows; y++) {
|
|
49
|
+
for (let x = 0; x < this.cols; x++) {
|
|
50
|
+
const cube = this.grid.cubes[y][x]!;
|
|
51
|
+
cube.faces[20].fill(1.0); // rho = 1.0
|
|
52
|
+
cube.faces[18].fill(0.0); // ux = 0
|
|
53
|
+
cube.faces[19].fill(0.0); // uy = 0
|
|
54
|
+
for (let k = 0; k < 9; k++) {
|
|
55
|
+
cube.faces[k].fill(w[k]); // w_k * rho (1.0)
|
|
56
|
+
cube.faces[k + 9].fill(w[k]); // Init swap buffer too!
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Initialize Global Obstacles (Central Island)
|
|
62
|
+
const cx = this.globalSizeW / 2;
|
|
63
|
+
const cy = this.globalSizeH / 2;
|
|
64
|
+
for (let y = 0; y < this.rows; y++) {
|
|
65
|
+
for (let x = 0; x < this.cols; x++) {
|
|
66
|
+
const cube = this.grid.cubes[y][x]!;
|
|
67
|
+
const obst = cube.faces[22];
|
|
68
|
+
obst.fill(0.0); // Reset all to water
|
|
69
|
+
for (let ly = 0; ly < this.chunkSize; ly++) {
|
|
70
|
+
for (let lx = 0; lx < this.chunkSize; lx++) {
|
|
71
|
+
const globalX = (x * this.chunkSize + lx);
|
|
72
|
+
const globalY = (y * this.chunkSize + ly);
|
|
73
|
+
const dx = globalX - cx;
|
|
74
|
+
const dy = globalY - cy;
|
|
75
|
+
// Island of radius 20
|
|
76
|
+
if (dx * dx + dy * dy < 400) {
|
|
77
|
+
obst[ly * this.chunkSize + lx] = 1.0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Reset boats
|
|
85
|
+
for (let b of this.boats) {
|
|
86
|
+
b.x = 20; b.y = 20;
|
|
87
|
+
b.vx = 0; b.vy = 0;
|
|
88
|
+
b.angle = 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Force sync once
|
|
92
|
+
this.grid.compute([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 22]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public addBoat(globalX: number, globalY: number, length: number = 20) {
|
|
96
|
+
this.boats.push({ x: globalX, y: globalY, vx: 0, vy: 0, length, angle: 0 });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public step() {
|
|
100
|
+
// 1. Math Step for all chunks + Boundary Exchange
|
|
101
|
+
// Synchronize LBM distributions [0..8], Bio data [21], AND Macros [18, 19, 20]
|
|
102
|
+
// Syncing Macros ensures the Boat doesn't read 0 velocity exactly on the boundary cell!
|
|
103
|
+
this.grid.compute([0, 1, 2, 3, 4, 5, 6, 7, 8, 18, 19, 20, 21]);
|
|
104
|
+
|
|
105
|
+
// 2. Global Boat Physics
|
|
106
|
+
for (const boat of this.boats) {
|
|
107
|
+
// Find which chunk the boat is currently in
|
|
108
|
+
const gx = Math.floor(boat.x);
|
|
109
|
+
const gy = Math.floor(boat.y);
|
|
110
|
+
|
|
111
|
+
// Toric world wrap around
|
|
112
|
+
const safeGx = (gx + this.globalSizeW) % this.globalSizeW;
|
|
113
|
+
const safeGy = (gy + this.globalSizeH) % this.globalSizeH;
|
|
114
|
+
|
|
115
|
+
const chunkX = Math.floor(safeGx / this.chunkSize);
|
|
116
|
+
const chunkY = Math.floor(safeGy / this.chunkSize);
|
|
117
|
+
const localX = safeGx % this.chunkSize;
|
|
118
|
+
const localY = safeGy % this.chunkSize;
|
|
119
|
+
|
|
120
|
+
const cube = this.grid.cubes[chunkY][chunkX];
|
|
121
|
+
if (cube) {
|
|
122
|
+
// Read macro velocities from the localized chunk
|
|
123
|
+
const uxFace = cube.faces[18];
|
|
124
|
+
const uyFace = cube.faces[19];
|
|
125
|
+
|
|
126
|
+
const localIdx = localY * this.chunkSize + localX;
|
|
127
|
+
const fux = uxFace[localIdx];
|
|
128
|
+
const fuy = uyFace[localIdx];
|
|
129
|
+
|
|
130
|
+
// --- Player Controls (WASD / ZQSD) ---
|
|
131
|
+
const engineForce = 0.15;
|
|
132
|
+
const turnForce = 0.05;
|
|
133
|
+
|
|
134
|
+
if (this.keys.up) {
|
|
135
|
+
boat.vx += Math.cos(boat.angle) * engineForce;
|
|
136
|
+
boat.vy += Math.sin(boat.angle) * engineForce;
|
|
137
|
+
}
|
|
138
|
+
if (this.keys.down) {
|
|
139
|
+
boat.vx -= Math.cos(boat.angle) * engineForce * 0.5;
|
|
140
|
+
boat.vy -= Math.sin(boat.angle) * engineForce * 0.5;
|
|
141
|
+
}
|
|
142
|
+
if (this.keys.left) boat.angle -= turnForce;
|
|
143
|
+
if (this.keys.right) boat.angle += turnForce;
|
|
144
|
+
|
|
145
|
+
// Si le joueur ne contrôle pas, le moteur tourne au ralenti tout droit
|
|
146
|
+
if (!this.keys.up && !this.keys.down && !this.keys.left && !this.keys.right) {
|
|
147
|
+
boat.vx += Math.cos(boat.angle) * 0.01;
|
|
148
|
+
boat.vy += Math.sin(boat.angle) * 0.01;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Advection: Boat is carried by ocean currents
|
|
152
|
+
boat.vx += fux * 0.25 - boat.vx * 0.05; // Force de l'eau + Friction hydrodynamique
|
|
153
|
+
boat.vy += fuy * 0.25 - boat.vy * 0.05;
|
|
154
|
+
|
|
155
|
+
// Move boat
|
|
156
|
+
boat.x += boat.vx;
|
|
157
|
+
boat.y += boat.vy;
|
|
158
|
+
|
|
159
|
+
// --- HARD LIMIT ON ISLAND COLLISION ---
|
|
160
|
+
// Island radius is approx 20. We stop the boat from entering.
|
|
161
|
+
let cDx = boat.x - this.globalSizeW / 2;
|
|
162
|
+
if (Math.abs(cDx) > this.globalSizeW / 2) cDx = cDx > 0 ? cDx - this.globalSizeW : cDx + this.globalSizeW;
|
|
163
|
+
let cDy = boat.y - this.globalSizeH / 2;
|
|
164
|
+
if (Math.abs(cDy) > this.globalSizeH / 2) cDy = cDy > 0 ? cDy - this.globalSizeH : cDy + this.globalSizeH;
|
|
165
|
+
|
|
166
|
+
const newDist = Math.sqrt(cDx * cDx + cDy * cDy);
|
|
167
|
+
const islandR = 24; // Buffer radius
|
|
168
|
+
if (newDist < islandR) {
|
|
169
|
+
const angleFromIsland = Math.atan2(cDy, cDx);
|
|
170
|
+
|
|
171
|
+
// Repulse strictly to the border of the island
|
|
172
|
+
boat.x += Math.cos(angleFromIsland) * (islandR - newDist);
|
|
173
|
+
boat.y += Math.sin(angleFromIsland) * (islandR - newDist);
|
|
174
|
+
|
|
175
|
+
// Kill velocity
|
|
176
|
+
boat.vx *= 0.1;
|
|
177
|
+
boat.vy *= 0.1;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Toric world wrap for exact coordinates
|
|
181
|
+
boat.x = (boat.x + this.globalSizeW) % this.globalSizeW;
|
|
182
|
+
boat.y = (boat.y + this.globalSizeH) % this.globalSizeH;
|
|
183
|
+
|
|
184
|
+
// Update orientation
|
|
185
|
+
if (Math.abs(boat.vx) > 0.01 || Math.abs(boat.vy) > 0.01) {
|
|
186
|
+
boat.angle = Math.atan2(boat.vy, boat.vx);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Paramètres Globaux du "Touilleur" (Tempête/Vortex)
|
|
193
|
+
public setVortexParams(strength: number, radius: number) {
|
|
194
|
+
for (let y = 0; y < this.rows; y++) {
|
|
195
|
+
for (let x = 0; x < this.cols; x++) {
|
|
196
|
+
const engine = this.grid.cubes[y][x]?.engine as OceanEngine;
|
|
197
|
+
if (engine) {
|
|
198
|
+
engine.params.vortexStrength = strength;
|
|
199
|
+
engine.params.vortexRadius = radius;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Helper to render a specific chunk to an isolated Canvas Context
|
|
207
|
+
*/
|
|
208
|
+
public renderChunk(chunkX: number, chunkY: number, ctx: CanvasRenderingContext2D, imageData: ImageData) {
|
|
209
|
+
const cube = this.grid.cubes[chunkY][chunkX];
|
|
210
|
+
if (!cube) return;
|
|
211
|
+
|
|
212
|
+
const size = this.chunkSize;
|
|
213
|
+
const bio = cube.faces[21];
|
|
214
|
+
const ux = cube.faces[18];
|
|
215
|
+
const uy = cube.faces[19];
|
|
216
|
+
const obst = cube.faces[22];
|
|
217
|
+
const data = imageData.data;
|
|
218
|
+
|
|
219
|
+
for (let i = 0; i < size * size; i++) {
|
|
220
|
+
const j = i * 4;
|
|
221
|
+
if (obst[i] > 0.5) {
|
|
222
|
+
data[j] = 20; data[j + 1] = 20; data[j + 2] = 40; data[j + 3] = 255;
|
|
223
|
+
} else {
|
|
224
|
+
const speed = Math.sqrt(ux[i] * ux[i] + uy[i] * uy[i]);
|
|
225
|
+
const intensity = speed * 1200;
|
|
226
|
+
data[j] = Math.min(255, bio[i] * 40 + intensity * 0.1);
|
|
227
|
+
data[j + 1] = Math.min(255, bio[i] * 120 + intensity * 0.4);
|
|
228
|
+
data[j + 2] = Math.min(255, intensity * 2.5 + 20);
|
|
229
|
+
data[j + 3] = 255;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
ctx.putImageData(imageData, 0, 0);
|
|
233
|
+
|
|
234
|
+
// Draw boats that belong to THIS chunk
|
|
235
|
+
ctx.fillStyle = "#fff";
|
|
236
|
+
for (const boat of this.boats) {
|
|
237
|
+
// Check if boat is in this chunk visually
|
|
238
|
+
// (We convert global boat coords to local chunk coords to draw them relative to this Canvas)
|
|
239
|
+
const localBoatX = boat.x - chunkX * size;
|
|
240
|
+
const localBoatY = boat.y - chunkY * size;
|
|
241
|
+
|
|
242
|
+
// Simple culling (draw if it's within chunk bounds or slightly outside to handle edges)
|
|
243
|
+
if (localBoatX >= -boat.length && localBoatX <= size + boat.length &&
|
|
244
|
+
localBoatY >= -boat.length && localBoatY <= size + boat.length) {
|
|
245
|
+
ctx.save();
|
|
246
|
+
ctx.translate(localBoatX, localBoatY);
|
|
247
|
+
ctx.rotate(boat.angle);
|
|
248
|
+
ctx.fillRect(-boat.length / 2, -2, boat.length, 4);
|
|
249
|
+
ctx.restore();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Interaction globally mapped
|
|
255
|
+
public setInteraction(globalX: number, globalY: number, active: boolean) {
|
|
256
|
+
const safeGx = (globalX + this.globalSizeW) % this.globalSizeW;
|
|
257
|
+
const safeGy = (globalY + this.globalSizeH) % this.globalSizeH;
|
|
258
|
+
|
|
259
|
+
const chunkX = Math.floor(safeGx / this.chunkSize);
|
|
260
|
+
const chunkY = Math.floor(safeGy / this.chunkSize);
|
|
261
|
+
const localX = safeGx % this.chunkSize;
|
|
262
|
+
const localY = safeGy % this.chunkSize;
|
|
263
|
+
|
|
264
|
+
// Reset all chunks interactions first to avoid "sticky" mouse
|
|
265
|
+
for (let y = 0; y < this.rows; y++) {
|
|
266
|
+
for (let x = 0; x < this.cols; x++) {
|
|
267
|
+
const engine = this.grid.cubes[y][x]?.engine as OceanEngine;
|
|
268
|
+
if (engine) engine.interaction.active = false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const cube = this.grid.cubes[chunkY][chunkX];
|
|
273
|
+
if (cube) {
|
|
274
|
+
const engine = cube.engine as OceanEngine;
|
|
275
|
+
engine.interaction.mouseX = localX;
|
|
276
|
+
engine.interaction.mouseY = localY;
|
|
277
|
+
engine.interaction.active = active;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { TriadeMasterBuffer } from './TriadeMasterBuffer';
|
|
2
|
+
import type { ITriadeEngine } from '../engines/ITriadeEngine';
|
|
3
|
+
|
|
4
|
+
export class TriadeCubeV2 {
|
|
5
|
+
public readonly mapSize: number;
|
|
6
|
+
public readonly faces: Float32Array[] = [];
|
|
7
|
+
public readonly offset: number; // Added this property
|
|
8
|
+
public engine: ITriadeEngine | null = null;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param mapSize La résolution (N x N)
|
|
12
|
+
* @param masterBuffer L'allocateur de RAM central de l'Orchestrateur
|
|
13
|
+
*/
|
|
14
|
+
constructor(mapSize: number, masterBuffer: TriadeMasterBuffer, numFaces: number = 6) {
|
|
15
|
+
this.mapSize = mapSize;
|
|
16
|
+
|
|
17
|
+
// 1. Demander la réservation de l'espace sur la bande mémoire (O(1) Memory Layout)
|
|
18
|
+
this.offset = masterBuffer.allocateCube(mapSize, numFaces);
|
|
19
|
+
|
|
20
|
+
// 2. Création des Vues
|
|
21
|
+
const floatCount = mapSize * mapSize;
|
|
22
|
+
const bytesPerFace = floatCount * Float32Array.BYTES_PER_ELEMENT;
|
|
23
|
+
|
|
24
|
+
// Génération des N "Views" mathématiques
|
|
25
|
+
for (let i = 0; i < numFaces; i++) {
|
|
26
|
+
this.faces.push(
|
|
27
|
+
new Float32Array(
|
|
28
|
+
masterBuffer.buffer,
|
|
29
|
+
this.offset + (i * bytesPerFace),
|
|
30
|
+
floatCount
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Injecte le cerveau mathématique dans ce paquet mémoire.
|
|
38
|
+
*/
|
|
39
|
+
setEngine(engine: ITriadeEngine) {
|
|
40
|
+
this.engine = engine;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Exécute le calcul de la Frame (O1, Wavefront, Automates)
|
|
45
|
+
*/
|
|
46
|
+
compute() {
|
|
47
|
+
if (!this.engine) {
|
|
48
|
+
console.warn("[TriadeCubeV2] Aucun Engine (Cerveau) n'a été assigné à ce cube.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.engine.compute(this.faces, this.mapSize);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Helper pour vider une face spécifique */
|
|
55
|
+
clearFace(faceIndex: number) {
|
|
56
|
+
this.faces[faceIndex].fill(0);
|
|
57
|
+
}
|
|
58
|
+
}
|