@woosh/meep-engine 2.48.15 → 2.48.16

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/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "description": "Fully featured ECS game engine written in JavaScript",
6
6
  "type": "module",
7
7
  "author": "Alexander Goldring",
8
- "version": "2.48.15",
8
+ "version": "2.48.16",
9
9
  "main": "build/meep.module.js",
10
10
  "module": "build/meep.module.js",
11
11
  "exports": {
@@ -34,12 +34,8 @@ export default class Quaternion {
34
34
 
35
35
  normalize(): void
36
36
 
37
- __setThreeEuler(threeEuler: any): void
38
-
39
37
  __setFromEuler(x: number, y: number, z: number, order?: string): void
40
38
 
41
- threeSetRotationMatrix(result: Matrix4): void
42
-
43
39
  setFromRotationMatrix(m: Matrix4): void
44
40
 
45
41
  decodeFromUint32(uint: number): void
@@ -138,7 +138,10 @@ class RenderSystem extends System {
138
138
 
139
139
  function handleRotationChange() {
140
140
  const m = renderable.object;
141
- transform.rotation.__setThreeEuler(m.rotation);
141
+ const euler = m.rotation;
142
+
143
+ transform.rotation.__setFromEuler(euler.x, euler.y, euler.z, euler.order);
144
+
142
145
  updateMeshTransform(renderable);
143
146
  updateNodeByTransformAndBBB(renderable.bvh, renderable.boundingBox, transform);
144
147
  }
@@ -1,10 +1,24 @@
1
- import { clamp } from "../../../../core/math/clamp.js";
2
- import { lerp } from "../../../../core/math/lerp.js";
1
+ import { scs3d_sample_linear } from "./scs3d_sample_linear.js";
3
2
 
4
3
  export class SingleChannelSampler3D {
5
4
 
6
5
  data = new Float32Array(0)
7
- #resolution = [0, 0, 0]
6
+ #resolution = new Uint32Array([0, 0, 0])
7
+
8
+ /**
9
+ *
10
+ * @param {Float32Array} data
11
+ * @param {ArrayLike<number>|Uint32Array|number[]} resolution
12
+ * @return {SingleChannelSampler3D}
13
+ */
14
+ static from(data, resolution) {
15
+ const r = new SingleChannelSampler3D();
16
+
17
+ r.data = data;
18
+ r.#resolution.set(resolution);
19
+
20
+ return r;
21
+ }
8
22
 
9
23
  set resolution(v) {
10
24
  const width = v[0];
@@ -66,64 +80,7 @@ export class SingleChannelSampler3D {
66
80
  const res_x = resolution[0];
67
81
  const res_y = resolution[1];
68
82
  const res_z = resolution[2];
69
-
70
- const x_max = res_x - 1;
71
- const y_max = res_y - 1;
72
- const z_max = res_z - 1;
73
-
74
- const x_clamped = clamp(x, 0, x_max);
75
- const y_clamped = clamp(y, 0, y_max);
76
- const z_clamped = clamp(z, 0, z_max);
77
-
78
- const x0 = x_clamped | 0;
79
- const y0 = y_clamped | 0;
80
- const z0 = z_clamped | 0;
81
-
82
- const f_x = x_clamped - x0;
83
- const f_y = y_clamped - y0;
84
- const f_z = z_clamped - z0;
85
-
86
- const x1 = x_clamped === x0 ? x0 : x0 + 1;
87
- const y1 = y_clamped === y0 ? y0 : y0 + 1;
88
- const z1 = z_clamped === z0 ? z0 : z0 + 1;
89
-
90
- // get 8 points
91
- const data = this.data;
92
-
93
- const z0_offset = z0 * (res_x * res_y);
94
-
95
- const y0_offset = y0 * res_x;
96
-
97
- const x0_offset = x0;
98
- const x1_offset = x1;
99
-
100
- const x0y0z0 = data[z0_offset + y0_offset + x0_offset];
101
- const x1y0z0 = data[z0_offset + y0_offset + x1_offset];
102
-
103
- const y1_offset = y1 * res_x;
104
-
105
- const x0y1z0 = data[z0_offset + y1_offset + x0_offset];
106
- const x1y1z0 = data[z0_offset + y1_offset + x1_offset];
107
-
108
- const z1_offset = z1 * (res_x * res_y);
109
-
110
- const x0y0z1 = data[z1_offset + y0_offset + x0_offset];
111
- const x1y0z1 = data[z1_offset + y0_offset + x1_offset];
112
-
113
- const x0y1z1 = data[z1_offset + y1_offset + x0_offset];
114
- const x1y1z1 = data[z1_offset + y1_offset + x1_offset];
115
-
116
- // filter on z0
117
- const lerp_z0_y0 = lerp(x0y0z0, x1y0z0, f_x);
118
- const lerp_z0_y1 = lerp(x0y1z0, x1y1z0, f_x);
119
- const lerp_z0 = lerp(lerp_z0_y0, lerp_z0_y1, f_y);
120
-
121
- // filter on z1
122
- const lerp_z1_y0 = lerp(x0y0z1, x1y0z1, f_x);
123
- const lerp_z1_y1 = lerp(x0y1z1, x1y1z1, f_x);
124
- const lerp_z1 = lerp(lerp_z1_y0, lerp_z1_y1, f_y);
125
-
126
- return lerp(lerp_z0, lerp_z1, f_z);
83
+ return scs3d_sample_linear(this.data, res_x, res_y, res_z, x, y, z);
127
84
  }
128
85
 
129
86
  /**
@@ -0,0 +1,71 @@
1
+ import { clamp } from "../../../../core/math/clamp.js";
2
+ import { lerp } from "../../../../core/math/lerp.js";
3
+
4
+ /**
5
+ *
6
+ * @param {Float32Array} data
7
+ * @param {number} res_x
8
+ * @param {number} res_y
9
+ * @param {number} res_z
10
+ * @param {number} x
11
+ * @param {number} y
12
+ * @param {number} z
13
+ * @return {number}
14
+ */
15
+ export function scs3d_sample_linear(data, res_x, res_y, res_z, x, y, z) {
16
+ const x_max = res_x - 1;
17
+ const y_max = res_y - 1;
18
+ const z_max = res_z - 1;
19
+
20
+ const x_clamped = clamp(x, 0, x_max);
21
+ const y_clamped = clamp(y, 0, y_max);
22
+ const z_clamped = clamp(z, 0, z_max);
23
+
24
+ const x0 = x_clamped | 0;
25
+ const y0 = y_clamped | 0;
26
+ const z0 = z_clamped | 0;
27
+
28
+ const f_x = x_clamped - x0;
29
+ const f_y = y_clamped - y0;
30
+ const f_z = z_clamped - z0;
31
+
32
+ const x1 = x_clamped === x0 ? x0 : x0 + 1;
33
+ const y1 = y_clamped === y0 ? y0 : y0 + 1;
34
+ const z1 = z_clamped === z0 ? z0 : z0 + 1;
35
+
36
+ // get 8 points
37
+ const z0_offset = z0 * (res_x * res_y);
38
+
39
+ const y0_offset = y0 * res_x;
40
+
41
+ const x0_offset = x0;
42
+ const x1_offset = x1;
43
+
44
+ const x0y0z0 = data[z0_offset + y0_offset + x0_offset];
45
+ const x1y0z0 = data[z0_offset + y0_offset + x1_offset];
46
+
47
+ const y1_offset = y1 * res_x;
48
+
49
+ const x0y1z0 = data[z0_offset + y1_offset + x0_offset];
50
+ const x1y1z0 = data[z0_offset + y1_offset + x1_offset];
51
+
52
+ const z1_offset = z1 * (res_x * res_y);
53
+
54
+ const x0y0z1 = data[z1_offset + y0_offset + x0_offset];
55
+ const x1y0z1 = data[z1_offset + y0_offset + x1_offset];
56
+
57
+ const x0y1z1 = data[z1_offset + y1_offset + x0_offset];
58
+ const x1y1z1 = data[z1_offset + y1_offset + x1_offset];
59
+
60
+ // filter on z0
61
+ const lerp_z0_y0 = lerp(x0y0z0, x1y0z0, f_x);
62
+ const lerp_z0_y1 = lerp(x0y1z0, x1y1z0, f_x);
63
+ const lerp_z0 = lerp(lerp_z0_y0, lerp_z0_y1, f_y);
64
+
65
+ // filter on z1
66
+ const lerp_z1_y0 = lerp(x0y0z1, x1y0z1, f_x);
67
+ const lerp_z1_y1 = lerp(x0y1z1, x1y1z1, f_x);
68
+ const lerp_z1 = lerp(lerp_z1_y0, lerp_z1_y1, f_y);
69
+
70
+ return lerp(lerp_z0, lerp_z1, f_z);
71
+ }
@@ -0,0 +1,218 @@
1
+ export class Fluid {
2
+ constructor(density, numX, numY, h) {
3
+ this.density = density;
4
+ this.numX = numX + 2;
5
+ this.numY = numY + 2;
6
+ this.numCells = this.numX * this.numY;
7
+ this.h = h;
8
+ this.u = new Float32Array(this.numCells);
9
+ this.v = new Float32Array(this.numCells);
10
+ this.newU = new Float32Array(this.numCells);
11
+ this.newV = new Float32Array(this.numCells);
12
+ this.p = new Float32Array(this.numCells);
13
+ this.s = new Float32Array(this.numCells);
14
+ this.m = new Float32Array(this.numCells);
15
+ this.newM = new Float32Array(this.numCells);
16
+ this.m.fill(1.0)
17
+ }
18
+
19
+ integrate(dt, gravity) {
20
+ var n = this.numY;
21
+ for (var i = 1; i < this.numX; i++) {
22
+ for (var j = 1; j < this.numY-1; j++) {
23
+ if (this.s[i*n + j] != 0.0 && this.s[i*n + j-1] != 0.0)
24
+ this.v[i*n + j] += gravity * dt;
25
+ }
26
+ }
27
+ }
28
+
29
+ solveIncompressibility(numIters, dt) {
30
+
31
+ var n = this.numY;
32
+ var cp = this.density * this.h / dt;
33
+
34
+ for (var iter = 0; iter < numIters; iter++) {
35
+
36
+ for (var i = 1; i < this.numX-1; i++) {
37
+ for (var j = 1; j < this.numY-1; j++) {
38
+
39
+ if (this.s[i*n + j] == 0.0)
40
+ continue;
41
+
42
+ var s = this.s[i*n + j];
43
+ var sx0 = this.s[(i-1)*n + j];
44
+ var sx1 = this.s[(i+1)*n + j];
45
+ var sy0 = this.s[i*n + j-1];
46
+ var sy1 = this.s[i*n + j+1];
47
+ var s = sx0 + sx1 + sy0 + sy1;
48
+ if (s == 0.0)
49
+ continue;
50
+
51
+ var div = this.u[(i+1)*n + j] - this.u[i*n + j] +
52
+ this.v[i*n + j+1] - this.v[i*n + j];
53
+
54
+ var p = -div / s;
55
+ p *= scene.overRelaxation;
56
+ this.p[i*n + j] += cp * p;
57
+
58
+ this.u[i*n + j] -= sx0 * p;
59
+ this.u[(i+1)*n + j] += sx1 * p;
60
+ this.v[i*n + j] -= sy0 * p;
61
+ this.v[i*n + j+1] += sy1 * p;
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ extrapolate() {
68
+ var n = this.numY;
69
+ for (var i = 0; i < this.numX; i++) {
70
+ this.u[i*n + 0] = this.u[i*n + 1];
71
+ this.u[i*n + this.numY-1] = this.u[i*n + this.numY-2];
72
+ }
73
+ for (var j = 0; j < this.numY; j++) {
74
+ this.v[0*n + j] = this.v[1*n + j];
75
+ this.v[(this.numX-1)*n + j] = this.v[(this.numX-2)*n + j]
76
+ }
77
+ }
78
+
79
+ sampleField(x, y, field) {
80
+ var n = this.numY;
81
+ var h = this.h;
82
+ var h1 = 1.0 / h;
83
+ var h2 = 0.5 * h;
84
+
85
+ x = Math.max(Math.min(x, this.numX * h), h);
86
+ y = Math.max(Math.min(y, this.numY * h), h);
87
+
88
+ var dx = 0.0;
89
+ var dy = 0.0;
90
+
91
+ var f;
92
+
93
+ switch (field) {
94
+ case U_FIELD: f = this.u; dy = h2; break;
95
+ case V_FIELD: f = this.v; dx = h2; break;
96
+ case S_FIELD: f = this.m; dx = h2; dy = h2; break
97
+
98
+ }
99
+
100
+ var x0 = Math.min(Math.floor((x-dx)*h1), this.numX-1);
101
+ var tx = ((x-dx) - x0*h) * h1;
102
+ var x1 = Math.min(x0 + 1, this.numX-1);
103
+
104
+ var y0 = Math.min(Math.floor((y-dy)*h1), this.numY-1);
105
+ var ty = ((y-dy) - y0*h) * h1;
106
+ var y1 = Math.min(y0 + 1, this.numY-1);
107
+
108
+ var sx = 1.0 - tx;
109
+ var sy = 1.0 - ty;
110
+
111
+ var val = sx*sy * f[x0*n + y0] +
112
+ tx*sy * f[x1*n + y0] +
113
+ tx*ty * f[x1*n + y1] +
114
+ sx*ty * f[x0*n + y1];
115
+
116
+ return val;
117
+ }
118
+
119
+ avgU(i, j) {
120
+ var n = this.numY;
121
+ var u = (this.u[i*n + j-1] + this.u[i*n + j] +
122
+ this.u[(i+1)*n + j-1] + this.u[(i+1)*n + j]) * 0.25;
123
+ return u;
124
+
125
+ }
126
+
127
+ avgV(i, j) {
128
+ var n = this.numY;
129
+ var v = (this.v[(i-1)*n + j] + this.v[i*n + j] +
130
+ this.v[(i-1)*n + j+1] + this.v[i*n + j+1]) * 0.25;
131
+ return v;
132
+ }
133
+
134
+ advectVel(dt) {
135
+
136
+ this.newU.set(this.u);
137
+ this.newV.set(this.v);
138
+
139
+ var n = this.numY;
140
+ var h = this.h;
141
+ var h2 = 0.5 * h;
142
+
143
+ for (var i = 1; i < this.numX; i++) {
144
+ for (var j = 1; j < this.numY; j++) {
145
+
146
+ cnt++;
147
+
148
+ // u component
149
+ if (this.s[i*n + j] != 0.0 && this.s[(i-1)*n + j] != 0.0 && j < this.numY - 1) {
150
+ var x = i*h;
151
+ var y = j*h + h2;
152
+ var u = this.u[i*n + j];
153
+ var v = this.avgV(i, j);
154
+ // var v = this.sampleField(x,y, V_FIELD);
155
+ x = x - dt*u;
156
+ y = y - dt*v;
157
+ u = this.sampleField(x,y, U_FIELD);
158
+ this.newU[i*n + j] = u;
159
+ }
160
+ // v component
161
+ if (this.s[i*n + j] != 0.0 && this.s[i*n + j-1] != 0.0 && i < this.numX - 1) {
162
+ var x = i*h + h2;
163
+ var y = j*h;
164
+ var u = this.avgU(i, j);
165
+ // var u = this.sampleField(x,y, U_FIELD);
166
+ var v = this.v[i*n + j];
167
+ x = x - dt*u;
168
+ y = y - dt*v;
169
+ v = this.sampleField(x,y, V_FIELD);
170
+ this.newV[i*n + j] = v;
171
+ }
172
+ }
173
+ }
174
+
175
+ this.u.set(this.newU);
176
+ this.v.set(this.newV);
177
+ }
178
+
179
+ advectSmoke(dt) {
180
+
181
+ this.newM.set(this.m);
182
+
183
+ var n = this.numY;
184
+ var h = this.h;
185
+ var h2 = 0.5 * h;
186
+
187
+ for (var i = 1; i < this.numX-1; i++) {
188
+ for (var j = 1; j < this.numY-1; j++) {
189
+
190
+ if (this.s[i*n + j] != 0.0) {
191
+ var u = (this.u[i*n + j] + this.u[(i+1)*n + j]) * 0.5;
192
+ var v = (this.v[i*n + j] + this.v[i*n + j+1]) * 0.5;
193
+ var x = i*h + h2 - dt*u;
194
+ var y = j*h + h2 - dt*v;
195
+
196
+ this.newM[i*n + j] = this.sampleField(x,y, S_FIELD);
197
+ }
198
+ }
199
+ }
200
+ this.m.set(this.newM);
201
+ }
202
+
203
+ // ----------------- end of simulator ------------------------------
204
+
205
+
206
+ simulate(dt, gravity, numIters) {
207
+
208
+ this.integrate(dt, gravity);
209
+
210
+ this.p.fill(0.0);
211
+ this.solveIncompressibility(numIters, dt);
212
+
213
+ this.extrapolate();
214
+ this.advectVel(dt);
215
+ this.advectSmoke(dt);
216
+ }
217
+ }
218
+
@@ -1,22 +1,12 @@
1
- import { clamp } from "../../../core/math/clamp.js";
2
- import { lerp } from "../../../core/math/lerp.js";
3
1
 
4
- /**
5
- *
6
- * @param {Float32Array} data
7
- * @param {number[]} resolution
8
- */
9
- function v3_grid_apply_advection_forward(data, resolution) {
10
-
11
- }
12
-
13
- /**
14
- *
15
- * @param {Float32Array} data
16
- * @param {number[]} resolution
17
- */
18
- function v3_grid_apply_advection_reverse(data, resolution) {
19
2
 
3
+ class FluidAttribute {
4
+ name = "unnamed"
5
+ /**
6
+ *
7
+ * @type {Float32Array}
8
+ */
9
+ data = null
20
10
  }
21
11
 
22
12
  /**
@@ -26,117 +16,84 @@ function v3_grid_apply_advection_reverse(data, resolution) {
26
16
  * @see Inspired by GDC talk "Interactive Wind and Vegetation in 'God of War'" - https://www.youtube.com/watch?v=MKX45_riWQA
27
17
  */
28
18
  export class FluidField {
29
- data = new Float32Array(0)
19
+
20
+ #attributes = [];
21
+
22
+
30
23
  #size = [0, 0, 0]
31
24
  #resolution = [0, 0, 0]
32
25
 
26
+ buffer = null;
33
27
 
34
- build() {
35
- this.data = new Float32Array(
36
- this.#resolution[0] * this.#resolution[1] * this.#resolution[2] * 3
37
- );
38
- }
28
+ addAttribute(name) {
29
+ const attribute = new FluidAttribute();
30
+ attribute.name = name;
39
31
 
40
- /**
41
- *
42
- * @param {number[]} out
43
- * @param {number} u
44
- * @param {number} v
45
- * @param {number} w
46
- */
47
- sample_linear_uv(out, u, v, w) {
48
- const max_x = this.#resolution[0] - 1;
49
- const max_y = this.#resolution[1] - 1;
50
- const max_z = this.#resolution[2] - 1;
32
+ const index = this.#attributes.length;
33
+
34
+ this.#attributes.push(attribute);
51
35
 
52
- this.sample_linear(out, u * max_x, v * max_y, w * max_z);
36
+ return index;
37
+ }
38
+
39
+ #findAttribute(name) {
40
+ return this.#attributes.find(a => a.name === name);
53
41
  }
54
42
 
55
43
  /**
56
44
  *
57
- * @param {number[]} out
58
- * @param {number} x
59
- * @param {number} y
60
- * @param {number} z
45
+ * @param {string} name
46
+ * @return {Float32Array}
61
47
  */
62
- sample_linear(out, x, y, z) {
63
- out[0] = this.sample_channel_linear(x, y, z, 0);
64
- out[1] = this.sample_channel_linear(x, y, z, 1);
65
- out[2] = this.sample_channel_linear(x, y, z, 2);
48
+ getAttributeData(name) {
49
+ return this.#findAttribute(name)?.data;
50
+ }
51
+
52
+ getAttributeCount() {
53
+ return this.#attributes.length;
66
54
  }
67
55
 
68
56
  /**
69
57
  *
70
- * @param {number} x
71
- * @param {number} y
72
- * @param {number} z
73
- * @param {number} channel
74
- * @returns {number}
58
+ * @param {ArrayBuffer} buffer
59
+ * @param {number} offset
75
60
  */
76
- sample_channel_linear(x, y, z, channel) {
77
- const resolution = this.#resolution;
61
+ attachBuffer(buffer, offset = 0) {
78
62
 
79
- const res_x = resolution[0];
80
- const res_y = resolution[1];
81
- const res_z = resolution[2];
63
+ const attribute_count = this.#attributes.length;
82
64
 
83
- const x_max = res_x - 1;
84
- const y_max = res_y - 1;
85
- const z_max = res_z - 1;
65
+ const cell_count = this.#resolution[0] * this.#resolution[1] * this.#resolution[2];
86
66
 
87
- const x_clamped = clamp(x, 0, x_max);
88
- const y_clamped = clamp(y, 0, y_max);
89
- const z_clamped = clamp(z, 0, z_max);
67
+ for (let i = 0; i < attribute_count; i++) {
68
+ const attribute = this.#attributes[i];
90
69
 
91
- const x0 = x_clamped | 0;
92
- const y0 = y_clamped | 0;
93
- const z0 = z_clamped | 0;
70
+ const array = new Float32Array(buffer, offset + i * cell_count * 4, cell_count);
94
71
 
95
- const f_x = x_clamped - x0;
96
- const f_y = y_clamped - y0;
97
- const f_z = z_clamped - z0;
98
72
 
99
- const x1 = x_clamped === x0 ? x0 : x0 + 1;
100
- const y1 = y_clamped === y0 ? y0 : y0 + 1;
101
- const z1 = z_clamped === z0 ? z0 : z0 + 1;
73
+ attribute.data = array;
74
+ }
102
75
 
103
- // get 8 points
104
- const data = this.data;
105
-
106
- const z0_offset = z0 * (res_x * res_y) * 3;
107
-
108
- const y0_offset = y0 * res_x * 3;
109
-
110
- const x0_offset = x0 * 3 + channel;
111
- const x1_offset = x1 * 3 + channel;
76
+ this.buffer = buffer;
77
+ }
112
78
 
113
- const x0y0z0 = data[z0_offset + y0_offset + x0_offset];
114
- const x1y0z0 = data[z0_offset + y0_offset + x1_offset];
79
+ setAttributeAt(attribute_index, x, y, z, value) {
115
80
 
116
- const y1_offset = y1 * res_x * 3;
81
+ const attribute = this.#attributes[attribute_index];
117
82
 
118
- const x0y1z0 = data[z0_offset + y1_offset + x0_offset];
119
- const x1y1z0 = data[z0_offset + y1_offset + x1_offset];
83
+ const data = attribute.data;
120
84
 
121
- const z1_offset = z1 * (res_x * res_y) * 3;
85
+ data[z * this.#resolution[0] * this.#resolution[1] + y * this.#resolution[0] + x] = value;
122
86
 
123
- const x0y0z1 = data[z1_offset + y0_offset + x0_offset];
124
- const x1y0z1 = data[z1_offset + y0_offset + x1_offset];
87
+ }
125
88
 
126
- const x0y1z1 = data[z1_offset + y1_offset + x0_offset];
127
- const x1y1z1 = data[z1_offset + y1_offset + x1_offset];
89
+ build() {
90
+ const attribute_count = this.#attributes.length;
128
91
 
129
- // filter on z0
130
- const lerp_z0_y0 = lerp(x0y0z0, x1y0z0, f_x);
131
- const lerp_z0_y1 = lerp(x0y1z0, x1y1z0, f_x);
132
- const lerp_z0 = lerp(lerp_z0_y0, lerp_z0_y1, f_y);
92
+ const cell_count = this.#resolution[0] * this.#resolution[1] * this.#resolution[2];
133
93
 
134
- // filter on z1
135
- const lerp_z1_y0 = lerp(x0y0z1, x1y0z1, f_x);
136
- const lerp_z1_y1 = lerp(x0y1z1, x1y1z1, f_x);
137
- const lerp_z1 = lerp(lerp_z1_y0, lerp_z1_y1, f_y);
94
+ const data_buffer = new ArrayBuffer(cell_count * attribute_count * 4);
138
95
 
139
- return lerp(lerp_z0, lerp_z1, f_z);
96
+ this.attachBuffer(data_buffer);
140
97
  }
141
98
 
142
99
  set size(v) {
@@ -158,4 +115,8 @@ export class FluidField {
158
115
  this.#resolution[1] = height;
159
116
  this.#resolution[2] = depth
160
117
  }
118
+
119
+ get resolution() {
120
+ return this.#resolution;
121
+ }
161
122
  }
@@ -0,0 +1,95 @@
1
+ import { v3_grid_apply_diffusion } from "./solver/v3_grid_apply_diffusion.js";
2
+ import { v3_grid_apply_advection_forward } from "./solver/v3_grid_apply_advection_forward.js";
3
+
4
+
5
+ export class FluidSimulator {
6
+
7
+ /**
8
+ *
9
+ * @type {ArrayBuffer|null}
10
+ */
11
+ buffer = null;
12
+
13
+ ensure_buffer(size) {
14
+ if (this.buffer === null || this.buffer.byteLength < size) {
15
+ this.buffer = new ArrayBuffer(size);
16
+ }
17
+ }
18
+
19
+ /**
20
+ *
21
+ * @param {FluidField} field
22
+ * @param {number} time_delta_in_seconds
23
+ */
24
+ step(field, time_delta_in_seconds) {
25
+ // create buffer big enough to store all attribute twice
26
+ const resolution = field.resolution;
27
+
28
+ const attribute_cell_count = resolution[0] * resolution[1] * resolution[2];
29
+ const attribute_byte_size = attribute_cell_count * 4;
30
+ const field_byte_size = field.getAttributeCount() * attribute_byte_size;
31
+
32
+ this.ensure_buffer(field_byte_size * 2);
33
+
34
+ const sim_buffer = this.buffer;
35
+
36
+ // remember current data
37
+ new Uint8Array(sim_buffer, 0, field_byte_size).set(new Uint8Array(field.buffer, 0, field_byte_size))
38
+
39
+ let ping_pong_index = 0;
40
+
41
+ const DIFFUSE_STEPS = 2;
42
+
43
+ for (let i = 0; i < 3; i++) {
44
+ // perform diffusion
45
+ let source = new Float32Array(sim_buffer, attribute_byte_size * i, attribute_cell_count)
46
+ let target = new Float32Array(sim_buffer, field_byte_size + attribute_byte_size * i, attribute_cell_count);
47
+
48
+ const ping_pong = [source, target];
49
+
50
+ ping_pong_index = 0;
51
+
52
+ for (let j = 0; j < DIFFUSE_STEPS; j++) {
53
+
54
+ source = ping_pong[ping_pong_index % 2];
55
+ target = ping_pong[(ping_pong_index + 1) % 2];
56
+
57
+ v3_grid_apply_diffusion(target, source, resolution);
58
+
59
+ ping_pong_index++;
60
+ }
61
+
62
+ }
63
+
64
+ ping_pong_index = DIFFUSE_STEPS;
65
+
66
+ const inputs = [];
67
+ const outputs = [];
68
+
69
+ for (let i = 0; i < 3; i++) {
70
+ const source = ping_pong_index % 2
71
+ const target = (ping_pong_index + 1) % 2
72
+
73
+ inputs[i] = new Float32Array(
74
+ sim_buffer, source * field_byte_size + attribute_byte_size * i, attribute_cell_count
75
+ );
76
+
77
+ outputs[i] = new Float32Array(
78
+ sim_buffer, target * field_byte_size + attribute_byte_size * i, attribute_cell_count
79
+ );
80
+ }
81
+
82
+ ping_pong_index++;
83
+
84
+ // advection
85
+ v3_grid_apply_advection_forward(outputs, inputs, resolution, time_delta_in_seconds);
86
+
87
+
88
+ if (ping_pong_index % 2 !== 0) {
89
+ // data is not at source
90
+ field.attachBuffer(sim_buffer, field_byte_size);
91
+ } else {
92
+ field.attachBuffer(sim_buffer, 0);
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,165 @@
1
+ import EmptyView from "../../../view/elements/EmptyView.js";
2
+ import { float2uint8 } from "../../../core/binary/float2uint8.js";
3
+ import { max2 } from "../../../core/math/max2.js";
4
+ import { Sampler2D } from "../../graphics/texture/sampler/Sampler2D.js";
5
+ import { CanvasView } from "../../../view/elements/CanvasView.js";
6
+
7
+ export class SliceVisualiser extends EmptyView {
8
+
9
+ #data = null;
10
+ #views = [];
11
+ #slices = [];
12
+ #resolution = [0, 0, 0];
13
+ #imageData = null;
14
+ #channelCount = 1
15
+ #drawMethod = (out, out_offset, sample, channelCount) => {
16
+ let i = 0;
17
+
18
+ for (; i < channelCount; i++) {
19
+ out[out_offset + i] = float2uint8(sample[i])
20
+ }
21
+
22
+ if (channelCount < 4) {
23
+ out[out_offset + 3] = 255;
24
+ }
25
+
26
+ }
27
+
28
+
29
+ /**
30
+ *
31
+ */
32
+ constructor() {
33
+ super({ classList: ['slice-visualizer'] });
34
+
35
+
36
+ this.on.linked.add(this.layout, this);
37
+ this.size.onChanged.add(this.layout, this);
38
+ }
39
+
40
+ draw() {
41
+
42
+ const resolution_x = this.#resolution[0];
43
+ const resolution_y = this.#resolution[1];
44
+ const resolution_z = this.#resolution[2];
45
+
46
+ const imageData = this.#imageData;
47
+
48
+ const slice_size = resolution_x * resolution_y;
49
+
50
+ const data = this.#data;
51
+
52
+ const sample = new Float32Array(this.#channelCount);
53
+
54
+ for (let i = 0; i < resolution_z; i++) {
55
+ const view = this.#views[i];
56
+
57
+ for (let j = 0; j < slice_size; j++) {
58
+ const j4 = j * 4;
59
+
60
+ for (let k = 0; k < this.#channelCount; k++) {
61
+ const channel = data[slice_size * k + j];
62
+
63
+ sample[k] = channel;
64
+ }
65
+
66
+ this.#drawMethod(imageData.data, j4, sample, this.#channelCount);
67
+
68
+ }
69
+
70
+ view.context2d.putImageData(imageData, 0, 0);
71
+ }
72
+ }
73
+
74
+ layout() {
75
+
76
+ const resolution_x = this.#resolution[0];
77
+ const resolution_y = this.#resolution[1];
78
+ const resolution_z = this.#resolution[2];
79
+
80
+ const width = this.size.x;
81
+
82
+ const grid_width = max2(1, Math.floor(width / resolution_x));
83
+
84
+ for (let i = 0; i < resolution_z; i++) {
85
+ const view = this.#views[i];
86
+
87
+ const grid_x = i % grid_width;
88
+ const grid_y = Math.floor(i / grid_width);
89
+
90
+ view.position.set(grid_x * resolution_x, grid_y * resolution_y);
91
+ }
92
+ }
93
+
94
+ /**
95
+ *
96
+ * @param {ArrayBuffer} data
97
+ * @param {number[]} resolution
98
+ */
99
+ setData(
100
+ data,
101
+ resolution) {
102
+ this.#data = new Float32Array(data);
103
+ this.#resolution = resolution;
104
+
105
+
106
+ const resolution_x = this.#resolution[0];
107
+ const resolution_y = this.#resolution[1];
108
+ const resolution_z = this.#resolution[2];
109
+
110
+ const slice_size = resolution_x * resolution_y;
111
+
112
+ /**
113
+ *
114
+ * @type {ImageData|null}
115
+ */
116
+ this.#imageData = null;
117
+
118
+ let slice_index = 0;
119
+
120
+ for (; slice_index < resolution_z; slice_index++) {
121
+ this.#slices[slice_index] = new Sampler2D(new Float32Array(data, slice_index * slice_size * 4, slice_size), 1, resolution_x, resolution_y);
122
+
123
+ let child;
124
+ if (slice_index + 1 >= this.#views.length) {
125
+ child = new CanvasView();
126
+ this.#views[slice_index] = child
127
+ this.addChild(child);
128
+ } else {
129
+ child = this.#views[slice_index];
130
+ }
131
+
132
+ child.size.set(resolution_x, resolution_y);
133
+
134
+ if (this.#imageData === null) {
135
+ this.#imageData = child.context2d.getImageData(0, 0, resolution_x, resolution_y);
136
+ }
137
+
138
+ child.css({
139
+ position: "absolute",
140
+ left: 0,
141
+ top: 0
142
+ });
143
+
144
+ this.#views[slice_index] = child;
145
+
146
+ this.addChild(child);
147
+ }
148
+
149
+ //remove left-overs
150
+
151
+ for (; slice_index < this.#slices.length; slice_index++) {
152
+
153
+ this.#slices.splice(slice_index, 1);
154
+ const view = this.#views.splice(slice_index, 1)[0];
155
+
156
+ this.removeChild(view);
157
+
158
+ slice_index--;
159
+
160
+ }
161
+
162
+ this.layout();
163
+ }
164
+
165
+ }
@@ -1,116 +1,18 @@
1
1
  import { EngineHarness } from "../../EngineHarness.js";
2
- import { SingleChannelSampler3D } from "../../graphics/texture/3d/SingleChannelSampler3D.js";
3
2
  import GUIElementSystem from "../../ecs/gui/GUIElementSystem.js";
4
3
  import EntityBuilder from "../../ecs/EntityBuilder.js";
5
4
  import GUIElement from "../../ecs/gui/GUIElement.js";
6
- import EmptyView from "../../../view/elements/EmptyView.js";
7
- import { Sampler2D } from "../../graphics/texture/sampler/Sampler2D.js";
8
- import { CanvasView } from "../../../view/elements/CanvasView.js";
9
- import { float2uint8 } from "../../../core/binary/float2uint8.js";
10
- import { clamp } from "../../../core/math/clamp.js";
11
- import { randomIntegerBetween } from "../../../core/math/random/randomIntegerBetween.js";
12
- import { seededRandom } from "../../../core/math/random/seededRandom.js";
13
5
  import { BehaviorComponent } from "../../intelligence/behavior/ecs/BehaviorComponent.js";
14
6
  import { BehaviorSystem } from "../../intelligence/behavior/ecs/BehaviorSystem.js";
15
- import { v3_grid_apply_diffusion } from "./solver/v3_grid_apply_diffusion.js";
16
- import { max2 } from "../../../core/math/max2.js";
17
7
  import { MetricCollection } from "../../development/performance/MetricCollection.js";
18
8
  import { PeriodicConsolePrinter } from "../../development/performance/monitor/PeriodicConsolePrinter.js";
19
9
  import { MetricStatistics } from "../../development/performance/MetricStatistics.js";
10
+ import { FluidField } from "./FluidField.js";
11
+ import { FluidSimulator } from "./FluidSimulator.js";
12
+ import { SliceVisualiser } from "./SliceVisualiser.js";
20
13
 
21
14
  const harness = new EngineHarness();
22
15
 
23
- class SliceVisualiser extends EmptyView {
24
-
25
- /**
26
- *
27
- * @param {SingleChannelSampler3D} source
28
- */
29
- constructor(source) {
30
- super({ classList: ['slice-visualizer'] });
31
-
32
- this.source = source;
33
-
34
- const slices = [];
35
- const views = [];
36
-
37
- const resolution = source.resolution;
38
- const resolution_x = resolution[0];
39
- const resolution_y = resolution[1];
40
- const resolution_z = resolution[2];
41
-
42
- const slice_size = resolution_x * resolution_y;
43
-
44
- /**
45
- *
46
- * @type {ImageData|null}
47
- */
48
- let imageData = null;
49
-
50
- for (let i = 0; i < resolution_z; i++) {
51
- slices[i] = new Sampler2D(new Float32Array(source.data.buffer, i * slice_size * 4, slice_size), 1, resolution_x, resolution_y);
52
-
53
- const child = new CanvasView();
54
- child.size.set(resolution_x, resolution_y);
55
-
56
- if (imageData === null) {
57
- imageData = child.context2d.getImageData(0, 0, resolution_x, resolution_y);
58
- }
59
-
60
- child.css({
61
- position: "absolute",
62
- left: 0,
63
- top: 0
64
- });
65
-
66
- views[i] = child;
67
-
68
- this.addChild(child);
69
- }
70
-
71
- this.draw = () => {
72
-
73
- for (let i = 0; i < resolution_z; i++) {
74
- const view = views[i];
75
- const slice = slices[i];
76
-
77
- for (let j = 0; j < slice_size; j++) {
78
- const j4 = j * 4;
79
-
80
-
81
- const uint8_value = float2uint8(slice.data[j]);
82
-
83
- imageData.data[j4] = clamp(uint8_value, 0, 255);
84
- imageData.data[j4 + 3] = 255;
85
- }
86
-
87
- view.context2d.putImageData(imageData, 0, 0);
88
- }
89
-
90
- };
91
-
92
- const layout = () => {
93
- const width = this.size.x;
94
-
95
- const grid_width = max2(1, Math.floor(width / resolution_x));
96
-
97
- for (let i = 0; i < resolution_z; i++) {
98
- const view = views[i];
99
-
100
- const grid_x = i % grid_width;
101
- const grid_y = Math.floor(i / grid_width);
102
-
103
- view.position.set(grid_x * resolution_x, grid_y * resolution_y);
104
- }
105
- }
106
-
107
- this.on.linked.add(layout);
108
- this.size.onChanged.add(layout);
109
- }
110
-
111
-
112
- }
113
-
114
16
  const metrics = new MetricCollection();
115
17
 
116
18
  metrics.create({ name: 'sim' })
@@ -123,56 +25,49 @@ function main(engine) {
123
25
 
124
26
  const ecd = engine.entityManager.dataset;
125
27
 
126
- const sampler = new SingleChannelSampler3D();
28
+ const field = new FluidField();
127
29
 
128
- sampler.resolution = [32, 32, 16];
30
+ field.addAttribute('x');
31
+ field.addAttribute('y');
32
+ field.addAttribute('z');
129
33
 
130
- const random = seededRandom();
34
+ field.resolution = [32, 32, 64];
131
35
 
132
- sampler.data.fill(1000, 20, 40);
36
+ field.build();
133
37
 
134
- for (let i = 0; i < 100; i++) {
38
+ field.setAttributeAt(0, 15, 16, 0, -100);
39
+ field.setAttributeAt(0, 17, 16, 0, 100);
40
+ field.setAttributeAt(1, 16, 15, 0, -100);
41
+ field.setAttributeAt(1, 16, 17, 0, 100);
135
42
 
136
- sampler.write(
137
- randomIntegerBetween(random, 0, 16),
138
- randomIntegerBetween(random, 0, 16),
139
- randomIntegerBetween(random, 0, 4),
140
- random()
141
- );
43
+ field.setAttributeAt(2, 15, 16, 0, 100);
44
+ field.setAttributeAt(2, 17, 16, 0, 100);
45
+ field.setAttributeAt(2, 16, 15, 0, 100);
46
+ field.setAttributeAt(2, 16, 17, 0, 100);
142
47
 
143
- }
144
48
 
49
+ const sim = new FluidSimulator();
145
50
 
146
- const slice_view = new SliceVisualiser(sampler);
51
+
52
+ const slice_view = new SliceVisualiser();
147
53
 
148
54
  slice_view.size.set(432, 1000);
149
55
  slice_view.scale.setScalar(2);
150
56
  slice_view.transformOrigin.set(0, 0);
151
57
 
152
- const sampler_0 = sampler.clone();
153
- const sampler_1 = sampler.clone();
154
-
155
- const ping_pong = [sampler_0, sampler_1];
156
- let ping_pong_index = 0;
58
+ slice_view.setData(field.buffer, field.resolution);
157
59
 
158
60
 
159
61
  new EntityBuilder()
160
62
  .add(BehaviorComponent.looping_function(time_delta => {
161
63
 
162
- const source = ping_pong[ping_pong_index % 2];
163
- const target = ping_pong[(ping_pong_index + 1) % 2];
164
-
165
- ping_pong_index++;
166
-
167
64
  const t0 = performance.now();
168
65
 
169
- for (let i = 0; i < 5; i++) {
170
- v3_grid_apply_diffusion(target.data, source.data, source.resolution);
171
- }
66
+ sim.step(field, 0.015);
172
67
 
173
68
  metrics.get('sim').record(performance.now() - t0);
174
69
 
175
- sampler.copy(target);
70
+ slice_view.setData(field.buffer, field.resolution);
176
71
 
177
72
  slice_view.draw();
178
73
 
@@ -0,0 +1,62 @@
1
+ import { scs3d_sample_linear } from "../../../graphics/texture/3d/scs3d_sample_linear.js";
2
+
3
+ /**
4
+ *
5
+ * @param {Float32Array[]} outputs
6
+ * @param {Float32Array[]} inputs
7
+ * @param {number[]} resolution
8
+ * @param {number} time_delta
9
+ */
10
+ export function v3_grid_apply_advection_forward(outputs, inputs, resolution, time_delta) {
11
+
12
+ const res_x = resolution[0];
13
+ const res_y = resolution[1];
14
+ const res_z = resolution[2];
15
+
16
+ const input_x = inputs[0];
17
+ const input_y = inputs[1];
18
+ const input_z = inputs[2];
19
+
20
+ const slice_size = res_y * res_x;
21
+
22
+ for (
23
+ let z = 0;
24
+ z < res_z - 1;
25
+ z++
26
+ ) {
27
+
28
+ const current_z_slice_offset = z * slice_size;
29
+
30
+ for (
31
+ let y = 0;
32
+ y < res_y;
33
+ y++
34
+ ) {
35
+ const current_y_line_offset = y * res_x;
36
+
37
+ for (let x = 0; x < res_x; x++) {
38
+
39
+
40
+ const current_index = current_z_slice_offset + current_y_line_offset + x;
41
+
42
+ const v_x = input_x[current_index]
43
+ const v_y = input_y[current_index]
44
+ const v_z = input_z[current_index]
45
+
46
+ // compute previous position of the particle
47
+ const s_x = x - v_x * time_delta;
48
+ const s_y = y - v_y * time_delta;
49
+ const s_z = z - v_z * time_delta;
50
+
51
+ // sample values at previous position
52
+ const new_x = scs3d_sample_linear(input_x, res_x, res_y, res_z, s_x, s_y, s_z);
53
+ const new_y = scs3d_sample_linear(input_y, res_x, res_y, res_z, s_x, s_y, s_z);
54
+ const new_z = scs3d_sample_linear(input_z, res_x, res_y, res_z, s_x, s_y, s_z);
55
+
56
+ outputs[0][current_index] = new_x;
57
+ outputs[1][current_index] = new_y;
58
+ outputs[2][current_index] = new_z;
59
+ }
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ *
3
+ * @param {Float32Array} data
4
+ * @param {number[]} resolution
5
+ */
6
+ export function v3_grid_apply_advection_reverse(data, resolution) {
7
+
8
+ }
@@ -5,18 +5,32 @@
5
5
  * @param {number[]} resolution
6
6
  */
7
7
  export function v3_grid_apply_diffusion(output, input, resolution) {
8
+ let sum = 0;
9
+
8
10
  const res_x = resolution[0];
9
11
  const res_y = resolution[1];
10
12
  const res_z = resolution[2];
11
13
 
12
- const factor = 1 / 7;
13
-
14
- let sum = 0;
15
14
 
16
15
  const slice_size = res_y * res_x;
17
16
 
18
- for (let z = 0; z < res_z; z++) {
19
- for (let y = 0; y < res_y; y++) {
17
+ // TODO consider taking inspiration from blur filters and performing convolution in 3 separate steps, one per axis. This would require fewer samples in total and potentially offer better cache utilization
18
+
19
+ const last_z_slice_offset = (res_z - 1) * slice_size;
20
+ const last_y_line_offset = (res_y - 1) * res_x;
21
+
22
+ for (
23
+ let current_z_slice_offset = 0;
24
+ current_z_slice_offset <= last_z_slice_offset;
25
+ current_z_slice_offset += slice_size
26
+ ) {
27
+
28
+ for (
29
+ let current_y_line_offset = 0;
30
+ current_y_line_offset <= last_y_line_offset;
31
+ current_y_line_offset += res_x
32
+ ) {
33
+
20
34
  for (let x = 0; x < res_x; x++) {
21
35
 
22
36
  let sample_count = 1;
@@ -24,39 +38,40 @@ export function v3_grid_apply_diffusion(output, input, resolution) {
24
38
  sum = 0;
25
39
 
26
40
  // sample cells around
27
- if (z > 0) {
41
+ if (current_z_slice_offset > 0) {
28
42
  sample_count++;
29
- sum += input[(z - 1) * slice_size + y * res_x + x]
43
+ sum += input[current_z_slice_offset - slice_size + current_y_line_offset + x]
30
44
  }
31
45
 
32
- if (y > 0) {
46
+
47
+ if (current_y_line_offset > 0) {
33
48
  sample_count++;
34
- sum += input[z * slice_size + (y - 1) * res_x + x]
49
+ sum += input[current_z_slice_offset + current_y_line_offset - res_x + x]
35
50
  }
36
51
 
37
52
  if (x > 0) {
38
53
  sample_count++;
39
- sum += input[z * slice_size + y * res_x + (x - 1)]
54
+ sum += input[current_z_slice_offset + current_y_line_offset + (x - 1)]
40
55
  }
41
56
 
42
57
 
43
- const current = z * slice_size + y * res_x + x;
58
+ const current = current_z_slice_offset + current_y_line_offset + x;
44
59
 
45
60
  sum += input[current];
46
61
 
47
62
  if (x < res_x - 1) {
48
63
  sample_count++;
49
- sum += input[z * slice_size + y * res_x + (x + 1)]
64
+ sum += input[current_z_slice_offset + current_y_line_offset + (x + 1)]
50
65
  }
51
66
 
52
- if (y < res_y - 1) {
67
+ if (current_y_line_offset < last_y_line_offset) {
53
68
  sample_count++;
54
- sum += input[z * slice_size + (y + 1) * res_x + x]
69
+ sum += input[current_z_slice_offset + current_y_line_offset + res_x + x]
55
70
  }
56
71
 
57
- if (z < res_z - 1) {
72
+ if (current_z_slice_offset < last_z_slice_offset) {
58
73
  sample_count++;
59
- sum += input[(z + 1) * slice_size + y * res_x + x]
74
+ sum += input[current_z_slice_offset + slice_size + current_y_line_offset + x]
60
75
  }
61
76
 
62
77
  output[current] = sum / sample_count;