某人用Java搞了一个流体力学的演示:http://grantkot.com/MPM/Liquid.html。
下面是 HTML 5版的流体力学演示(推荐使用Chrome浏览器浏览):
效果演示
不过,这仅仅是个开始。某同学将其发布上了reddit.com,于是,全世界的同学们开始给力了。
Flash的开发者首先不服,搞了个 flash版(带源码):http://wonderfl.net/c/yxe9
看到了Flash版,Javascript+HTML5的同学们也不干了,于是出现HTML5版(带源码):http://www.music.mcgill.ca/~sinclair/content/blog/liquid_simulator_ported_to_canvas
不过性能慢了很多,所以,又有人优化了一下HTML5版的程序:http://jsbin.com/unovo4
SVG的同学们也不甘寂寞,不过,那真叫一个慢啊:http://ulo.pe/js-liquid-svg/
这个时候,C/C++同学出来了,使用SDL库也搞了一个:http://q3k.org/fluidsim.zip
短短几天里,被人重写成各种语言。
下面看看在HTML 5里面的实现:
001 |
< canvas width = "400" height = "400" id = "liquid" ></ canvas >< script > |
002 |
/** |
003 |
* This version: |
004 |
* Copyright Stephen Sinclair (radarsat1) ( http://www.music.mcgill.ca/~sinclair ) |
005 |
* MIT License ( http://www.opensource.org/licenses/mit-license.php ) |
006 |
* Downloaded from: http://www.music.mcgill.ca/~sinclair/blog |
007 |
*/ |
008 |
009 |
/** |
010 |
* Flash version: |
011 |
* Copyright iunpin ( http://wonderfl.net/user/iunpin ) |
012 |
* MIT License ( http://www.opensource.org/licenses/mit-license.php ) |
013 |
* Downloaded from: http://wonderfl.net/c/6eu4 |
014 |
*/ |
015 |
016 |
/** |
017 |
* Original Java version: |
019 |
*/ |
020 |
021 |
var canvas; |
022 |
var context; |
023 |
var running = false; |
024 |
var width = 0; |
025 |
var height = 0; |
026 |
var liquidTest; |
027 |
var step = 0; |
028 |
029 |
function LiquidTest(gsizeX, gsizeY, particlesX, particlesY) |
030 |
{ |
031 |
this.particles = []; |
032 |
033 |
this.gsizeX = gsizeX; |
034 |
this.gsizeY = gsizeY; |
035 |
036 |
this.grid = [[]]; //Nodes |
037 |
this.active = []; //Nodes |
038 |
this.water = new Material(1.0, 1.0, 1.0, 1.0, 1.0, 1.0); |
039 |
this.pressed = false; |
040 |
this.pressedprev = false; |
041 |
042 |
this.mx = 0; |
043 |
this.my = 0; |
044 |
this.mxprev = 0; |
045 |
this.myprev = 0; |
046 |
047 |
this.init = function() |
048 |
{ |
049 |
var i = 0, j = 0; |
050 |
this.grid = []; |
051 |
for (i = 0; i < this.gsizeX ; i++) |
052 |
{ |
053 |
this.grid.push([]); |
054 |
for ( j = 0 ; j < this.gsizeY; j++) |
055 |
{ |
056 |
this.grid[i].push(new Node()); |
057 |
} |
058 |
} |
059 |
060 |
var p; |
061 |
for ( i = 0 ; i < particlesX; i++) |
062 |
for ( j = 0 ; j < particlesY; j++) |
063 |
{ |
064 |
p = new Particle(this.water, i + 4, j + 4, 0.0, 0.0); |
065 |
this.particles.push(p); |
066 |
} |
067 |
} |
068 |
069 |
this.paint = function () |
070 |
{ |
071 |
context.clearRect(0, 0, width, height); |
072 |
073 |
context.beginPath(); |
074 |
for (var pi in this.particles) |
075 |
{ |
076 |
var p = this .particles[pi]; |
077 |
line(4.0 * p.x, 4.0 * p.y, |
078 |
4.0 * (p.x - p.u), 4.0 * (p.y - p.v)); |
079 |
} |
080 |
081 |
context.stroke(); |
082 |
} |
083 |
084 |
this.simulate = function () |
085 |
{ |
086 |
var drag = false ; |
087 |
var mdx = 0 .0, mdy = 0 .0; |
088 |
089 |
if (this.pressed && this.pressedprev) |
090 |
{ |
091 |
drag = true ; |
092 |
mdx = 0 .25 * (this.mx - this.mxprev); |
093 |
mdy = 0 .25 * (this.my - this.myprev); |
094 |
} |
095 |
096 |
this.pressedprev = this.pressed; |
097 |
this.mxprev = this.mx; |
098 |
this.myprev = this.my; |
099 |
100 |
for (var n in this.active) |
101 |
this.active[n].clear(); |
102 |
this.active.length = 0 ; |
103 |
104 |
var i, j; |
105 |
var x, y, phi; |
106 |
var fx = 0 .0, fy = 0 .0; |
107 |
for (var pi in this.particles) |
108 |
{ |
109 |
var p = this .particles[pi]; |
110 |
p.cx = parseInt (p.x - 0.5); |
111 |
p.cy = parseInt (p.y - 0.5); |
112 |
113 |
x = p .cx - p.x; |
114 |
p.px[0] = (0.5 * x * x + 1.5 * x + 1.125); |
115 |
p.gx[0] = (x + 1.5); |
116 |
x += 1.0; |
117 |
p.px[1] = (-x * x + 0.75); |
118 |
p.gx[1] = (-2.0 * x); |
119 |
x += 1.0; |
120 |
p.px[2] = (0.5 * x * x - 1.5 * x + 1.125); |
121 |
p.gx[2] = (x - 1.5); |
122 |
123 |
y = p .cy - p.y; |
124 |
p.py[0] = (0.5 * y * y + 1.5 * y + 1.125); |
125 |
p.gy[0] = (y + 1.5); |
126 |
y += 1.0; |
127 |
p.py[1] = (-y * y + 0.75); |
128 |
p.gy[1] = (-2.0 * y); |
129 |
y += 1.0; |
130 |
p.py[2] = (0.5 * y * y - 1.5 * y + 1.125); |
131 |
p.gy[2] = (y - 1.5); |
132 |
133 |
for (var i = 0 ; i < 3; i++) |
134 |
{ |
135 |
for (var j = 0 ; j < 3; j++) |
136 |
{ |
137 |
var n = this .grid[p.cx + i][p.cy + j]; |
138 |
if (!n.active) |
139 |
{ |
140 |
this.active.push(n); |
141 |
n.active = true ; |
142 |
} |
143 |
phi = p.px[i] * p.py[j]; |
144 |
n.m += phi * p.mat.m; |
145 |
n.d += phi; |
146 |
n.gx += p.gx[i] * p.py[j]; |
147 |
n.gy += p.px[i] * p.gy[j]; |
148 |
} |
149 |
} |
150 |
} |
151 |
152 |
var density, pressure, weight; |
153 |
var n01, n02; |
154 |
var n11, n12; |
155 |
var cx, cy; |
156 |
var cxi, cyi; |
157 |
158 |
var pdx, pdy; |
159 |
var C20, C02, C30, C03; |
160 |
var csum1, csum2; |
161 |
var C21, C31, C12, C13, C11; |
162 |
163 |
var u, u2, u3; |
164 |
var v, v2, v3; |
165 |
166 |
for (var pi in this.particles) |
167 |
{ |
168 |
var p = this .particles[pi]; |
169 |
170 |
cx = parseInt (p.x); |
171 |
cy = parseInt (p.y); |
172 |
cxi = cx + 1; |
173 |
cyi = cy + 1; |
174 |
175 |
n01 = this .grid[cx][cy]; |
176 |
n02 = this .grid[cx][cyi]; |
177 |
n11 = this .grid[cxi][cy]; |
178 |
n12 = this .grid[cxi][cyi]; |
179 |
180 |
pdx = n11 .d - n01.d; |
181 |
pdy = n02 .d - n01.d; |
182 |
C20 = 3 .0 * pdx - n11.gx - 2.0 * n01.gx; |
183 |
C02 = 3 .0 * pdy - n02.gy - 2.0 * n01.gy; |
184 |
C30 = -2.0 * pdx + n11.gx + n01.gx; |
185 |
C03 = -2.0 * pdy + n02.gy + n01.gy; |
186 |
csum1 = n01 .d + n01.gy + C02 + C03; |
187 |
csum2 = n01 .d + n01.gx + C20 + C30; |
188 |
C21 = 3 .0 * n12.d - 2.0 * n02.gx - n12.gx - 3.0 * csum1 - C20; |
189 |
C31 = -2.0 * n12.d + n02.gx + n12.gx + 2.0 * csum1 - C30; |
190 |
C12 = 3 .0 * n12.d - 2.0 * n11.gy - n12.gy - 3.0 * csum2 - C02; |
191 |
C13 = -2.0 * n12.d + n11.gy + n12.gy + 2.0 * csum2 - C03; |
192 |
C11 = n02 .gx - C13 - C12 - n01.gx; |
193 |
194 |
u = p .x - cx; |
195 |
u2 = u * u; |
196 |
u3 = u * u2; |
197 |
v = p .y - cy; |
198 |
v2 = v * v; |
199 |
v3 = v * v2; |
200 |
density = n01 .d + n01.gx * u + n01.gy * v + C20 * u2 + C02 * v2 + C30 * u3 + C03 * v3 + C21 * u2 * v + C31 * u3 * v + C12 * u * v2 + C13 * u * v3 + C11 * u * v; |
201 |
202 |
pressure = density - 1.0; |
203 |
if (pressure > 2.0) |
204 |
pressure = 2.0; |
205 |
206 |
fx = 0.0; |
207 |
fy = 0.0; |
208 |
209 |
if (p.x < 4.0 ) |
210 |
fx += p.mat.m * (4.0 - p.x); |
211 |
else if (p.x > this.gsizeX - 5) |
212 |
fx += p.mat.m * (this.gsizeX - 5 - p.x); |
213 |
214 |
if (p.y < 4.0 ) |
215 |
fy += p.mat.m * (4.0 - p.y); |
216 |
else if (p.y > this.gsizeY - 5) |
217 |
fy += p.mat.m * (this.gsizeY - 5 - p.y); |
218 |
219 |
if (drag) |
220 |
{ |
221 |
var vx = Math.abs(p.x - 0.25 * this.mx); |
222 |
var vy = Math.abs(p.y - 0.25 * this.my); |
223 |
if ((vx < 10.0 ) && (vy < 10.0)) |
224 |
{ |
225 |
weight = p .mat.m * (1.0 - vx * 0.10) * (1.0 - vy * 0.10); |
226 |
fx += weight * (mdx - p.u); |
227 |
fy += weight * (mdy - p.v); |
228 |
} |
229 |
} |
230 |
231 |
for ( i = 0 ; i < 3; i++) |
232 |
{ |
233 |
for ( j = 0 ; j < 3; j++) |
234 |
{ |
235 |
n = this .grid[(p.cx + i)][(p.cy + j)]; |
236 |
phi = p.px[i] * p.py[j]; |
237 |
n.ax += -((p.gx[i] * p.py[j]) * pressure) + fx * phi; |
238 |
n.ay += -((p.px[i] * p.gy[j]) * pressure) + fy * phi; |
239 |
} |
240 |
} |
241 |
} |
242 |
243 |
for (var ni in this.active) |
244 |
{ |
245 |
var n = this .active[ni]; |
246 |
if (n.m > 0.0) |
247 |
{ |
248 |
n.ax /= n.m; |
249 |
n.ay /= n.m; |
250 |
n.ay += 0.03; |
251 |
} |
252 |
} |
253 |
254 |
var mu, mv; |
255 |
for (var pi in this.particles) |
256 |
{ |
257 |
var p = this.particles[pi]; |
258 |
for (i = 0; i < 3 ; i++) |
259 |
{ |
260 |
for ( j = 0 ; j < 3; j++) |
261 |
{ |
262 |
n = this .grid[(p.cx + i)][(p.cy + j)]; |
263 |
phi = p.px[i] * p.py[j]; |
264 |
p.u += phi * n.ax; |
265 |
p.v += phi * n.ay; |
266 |
} |
267 |
} |
268 |
mu = p .mat.m * p.u; |
269 |
mv = p .mat.m * p.v; |
270 |
for ( i = 0 ; i < 3; i++) |
271 |
{ |
272 |
for ( j = 0 ; j < 3; j++) |
273 |
{ |
274 |
n = this .grid[(p.cx + i)][(p.cy + j)]; |
275 |
phi = p.px[i] * p.py[j]; |
276 |
n.u += phi * mu; |
277 |
n.v += phi * mv; |
278 |
} |
279 |
} |
280 |
} |
281 |
282 |
for (var ni in this.active) |
283 |
{ |
284 |
var n = this .active[ni]; |
285 |
if (n.m > 0.0) |
286 |
{ |
287 |
n.u /= n.m; |
288 |
n.v /= n.m; |
289 |
} |
290 |
} |
291 |
292 |
var gu, gv; |
293 |
for (var pi in this.particles) |
294 |
{ |
295 |
var p = this.particles[pi]; |
296 |
gu = 0.0; |
297 |
gv = 0.0; |
298 |
for (var i = 0; i < 3 ; i++) |
299 |
{ |
300 |
for (var j = 0 ; j < 3; j++) |
301 |
{ |
302 |
var n = this .grid[(p.cx + i)][(p.cy + j)]; |
303 |
phi = p.px[i] * p.py[j]; |
304 |
gu += phi * n.u; |
305 |
gv += phi * n.v; |
306 |
} |
307 |
} |
308 |
p.x += gu; |
309 |
p.y += gv; |
310 |
p.u += 1.0 * (gu - p.u); |
311 |
p.v += 1.0 * (gv - p.v); |
312 |
if (p.x < 1.0) |
313 |
{ |
314 |
p.x = (1.0 + Math.random() * 0.01); |
315 |
p.u = 0 .0; |
316 |
} |
317 |
else if (p.x > this.gsizeX - 2) |
318 |
{ |
319 |
p.x = (this.gsizeX - 2 - Math.random() * 0.01); |
320 |
p.u = 0.0; |
321 |
} |
322 |
if (p.y < 1.0 ) |
323 |
{ |
324 |
p.y = (1.0 + Math.random() * 0.01); |
325 |
p.v = 0 .0; |
326 |
} |
327 |
else if (p.y > this.gsizeY - 2) |
328 |
{ |
329 |
p.y = (this.gsizeY - 2 - Math.random() * 0.01); |
330 |
p.v = 0.0; |
331 |
} |
332 |
} |
333 |
} |
334 |
335 |
this.init(); |
336 |
} |
337 |
338 |
function Node() |
339 |
{ |
340 |
this.m = 0; |
341 |
this.d = 0; |
342 |
this.gx = 0; |
343 |
this.gy = 0; |
344 |
this.u = 0; |
345 |
this.v = 0; |
346 |
this.ax = 0; |
347 |
this.ay = 0; |
348 |
this.active = false; |
349 |
|
350 |
this.clear = function() |
351 |
{ |
352 |
this.m = this.d = this.gx = this.gy = this.u = this.v = this.ax = this.ay = 0.0; |
353 |
this.active = false; |
354 |
} |
355 |
} |
356 |
357 |
function Particle(mat, x, y, u, v) |
358 |
{ |
359 |
this.mat = mat; |
360 |
this.x = x; |
361 |
this.y = y; |
362 |
this.u = u; |
363 |
this.v = v; |
364 |
365 |
this.dudx = 0; |
366 |
this.dudy = 0; |
367 |
this.dvdx = 0; |
368 |
this.dvdy = 0; |
369 |
this.cx = 0; |
370 |
this.cy = 0; |
371 |
372 |
this.px = [0,0,0]; |
373 |
this.py = [0,0,0]; |
374 |
this.gx = [0,0,0]; |
375 |
this.gy = [0,0,0]; |
376 |
} |
377 |
378 |
function Material(m, rd, k, v, d, g) |
379 |
{ |
380 |
this.m = m; |
381 |
this.rd = rd; |
382 |
this.k = k; |
383 |
this.v = v; |
384 |
this.d = d; |
385 |
this.g = g; |
386 |
} |
387 |
388 |
function line(x1,y1,x2,y2) { |
389 |
context.moveTo(x1,y1); |
390 |
context.lineTo(x2,y2); |
391 |
} |
392 |
393 |
function getPosition(obj) { |
394 |
var p = obj.offsetParent; |
395 |
var left = obj.offsetLeft; |
396 |
var top = obj.offsetTop; |
397 |
if (p) { |
398 |
var pos = getPosition(p); |
399 |
left += pos[0]; |
400 |
top += pos[1]; |
401 |
} |
402 |
return [left, top]; |
403 |
} |
404 |
405 |
function mouseMoved(event) |
406 |
{ |
407 |
var pos = getPosition(canvas); |
408 |
liquidTest.mx = event.pageX - pos[0]; |
409 |
liquidTest.my = event.pageY - pos[1]; |
410 |
} |
411 |
412 |
function mousePressed(event) |
413 |
{ |
414 |
liquidTest.pressed = true; |
415 |
} |
416 |
417 |
function mouseReleased(event) |
418 |
{ |
419 |
liquidTest.pressed = false; |
420 |
} |
421 |
422 |
function stop() |
423 |
{ |
424 |
running = false; |
425 |
} |
426 |
427 |
function start() |
428 |
{ |
429 |
running = true; |
430 |
draw(); |
431 |
} |
432 |
433 |
function restart(gsizeX, gsizeY, particlesX, particlesY) |
434 |
{ |
435 |
liquidTest = new LiquidTest(gsizeX, gsizeY, particlesX, particlesY); |
436 |
running = true; |
437 |
draw(); |
438 |
} |
439 |
440 |
function draw() |
441 |
{ |
442 |
// clear |
443 |
444 |
// advance simulation |
445 |
liquidTest.simulate(); |
446 |
447 |
step ++; |
448 |
} |
449 |
450 |
function init() { |
451 |
canvas = document.getElementById('liquid'); |
452 |
width = canvas.width; |
453 |
height = canvas.height; |
454 |
context = canvas.getContext('2d'); |
455 |
context.strokeStyle = "#0000FF"; |
456 |
457 |
canvas.onmousedown = mousePressed; |
458 |
canvas.onmouseup = mouseReleased; |
459 |
canvas.onmousemove = mouseMoved; |
460 |
461 |
liquidTest = new LiquidTest(100, 100, 50, 50); |
462 |
463 |
start(); |
464 |
} |
465 |
466 |
setInterval(draw, 33); |
467 |
setInterval("liquidTest.paint()", 33); |
468 |
469 |
init(); |
470 |
</ script > |