|
1 | | -<!DOCTYPE html> |
| 1 | + <!DOCTYPE html> |
2 | 2 | <html lang="en"> |
3 | 3 | <head> |
4 | 4 | <title>three.js webgl - gpgpu - water</title> |
|
55 | 55 | uniform vec2 mousePos; |
56 | 56 | uniform float mouseSize; |
57 | 57 | uniform float viscosityConstant; |
58 | | - |
59 | | - #define deltaTime ( 1.0 / 60.0 ) |
60 | | - #define GRAVITY_CONSTANT ( resolution.x * deltaTime * 3.0 ) |
| 58 | + uniform float heightCompensation; |
61 | 59 |
|
62 | 60 | void main() { |
63 | 61 |
|
64 | 62 | vec2 cellSize = 1.0 / resolution.xy; |
65 | 63 |
|
66 | 64 | vec2 uv = gl_FragCoord.xy * cellSize; |
67 | 65 |
|
68 | | - // heightmapValue.x == height |
69 | | - // heightmapValue.y == velocity |
| 66 | + // heightmapValue.x == height from previous frame |
| 67 | + // heightmapValue.y == height from penultimate frame |
70 | 68 | // heightmapValue.z, heightmapValue.w not used |
71 | 69 | vec4 heightmapValue = texture2D( heightmap, uv ); |
72 | 70 |
|
|
76 | 74 | vec4 east = texture2D( heightmap, uv + vec2( cellSize.x, 0.0 ) ); |
77 | 75 | vec4 west = texture2D( heightmap, uv + vec2( - cellSize.x, 0.0 ) ); |
78 | 76 |
|
79 | | - float sump = north.x + south.x + east.x + west.x - 4.0 * heightmapValue.x; |
80 | | - |
81 | | - float accel = sump * GRAVITY_CONSTANT; |
82 | | - |
83 | | - // Dynamics |
84 | | - heightmapValue.y += accel; |
85 | | - heightmapValue.x += heightmapValue.y * deltaTime; |
86 | | - |
87 | | - // Viscosity |
88 | | - heightmapValue.x += sump * viscosityConstant; |
| 77 | + float newHeight = ( ( north.x + south.x + east.x + west.x ) * 0.5 - heightmapValue.y ) * viscosityConstant; |
89 | 78 |
|
90 | 79 | // Mouse influence |
91 | 80 | float mousePhase = clamp( length( ( uv - vec2( 0.5 ) ) * BOUNDS - vec2( mousePos.x, - mousePos.y ) ) * PI / mouseSize, 0.0, PI ); |
92 | | - heightmapValue.x += cos( mousePhase ) + 1.0; |
| 81 | + newHeight += ( cos( mousePhase ) + 1.0 ) * 0.28; |
| 82 | + |
| 83 | + heightmapValue.y = heightmapValue.x; |
| 84 | + heightmapValue.x = newHeight; |
93 | 85 |
|
94 | 86 | gl_FragColor = heightmapValue; |
95 | 87 |
|
|
122 | 114 | } |
123 | 115 |
|
124 | 116 | </script> |
| 117 | + |
| 118 | + <!-- This is a 'compute shader' to read the current level and normal of water at a point --> |
| 119 | + <!-- It is used with a variable of size 1x1 --> |
| 120 | + <script id="readWaterLevelFragmentShader" type="x-shader/x-fragment"> |
| 121 | + |
| 122 | + uniform vec2 point1; |
| 123 | + |
| 124 | + uniform sampler2D texture; |
| 125 | + |
| 126 | + // Integer to float conversion from https://stackoverflow.com/questions/17981163/webgl-read-pixels-from-floating-point-render-target |
| 127 | + |
| 128 | + float shift_right( float v, float amt ) { |
| 129 | + |
| 130 | + v = floor( v ) + 0.5; |
| 131 | + return floor( v / exp2( amt ) ); |
| 132 | + |
| 133 | + } |
| 134 | + |
| 135 | + float shift_left( float v, float amt ) { |
| 136 | + |
| 137 | + return floor( v * exp2( amt ) + 0.5 ); |
| 138 | + |
| 139 | + } |
| 140 | + |
| 141 | + float mask_last( float v, float bits ) { |
| 142 | + |
| 143 | + return mod( v, shift_left( 1.0, bits ) ); |
| 144 | + |
| 145 | + } |
| 146 | + |
| 147 | + float extract_bits( float num, float from, float to ) { |
| 148 | + |
| 149 | + from = floor( from + 0.5 ); to = floor( to + 0.5 ); |
| 150 | + return mask_last( shift_right( num, from ), to - from ); |
| 151 | + |
| 152 | + } |
| 153 | + |
| 154 | + vec4 encode_float( float val ) { |
| 155 | + if ( val == 0.0 ) return vec4( 0, 0, 0, 0 ); |
| 156 | + float sign = val > 0.0 ? 0.0 : 1.0; |
| 157 | + val = abs( val ); |
| 158 | + float exponent = floor( log2( val ) ); |
| 159 | + float biased_exponent = exponent + 127.0; |
| 160 | + float fraction = ( ( val / exp2( exponent ) ) - 1.0 ) * 8388608.0; |
| 161 | + float t = biased_exponent / 2.0; |
| 162 | + float last_bit_of_biased_exponent = fract( t ) * 2.0; |
| 163 | + float remaining_bits_of_biased_exponent = floor( t ); |
| 164 | + float byte4 = extract_bits( fraction, 0.0, 8.0 ) / 255.0; |
| 165 | + float byte3 = extract_bits( fraction, 8.0, 16.0 ) / 255.0; |
| 166 | + float byte2 = ( last_bit_of_biased_exponent * 128.0 + extract_bits( fraction, 16.0, 23.0 ) ) / 255.0; |
| 167 | + float byte1 = ( sign * 128.0 + remaining_bits_of_biased_exponent ) / 255.0; |
| 168 | + return vec4( byte4, byte3, byte2, byte1 ); |
| 169 | + } |
| 170 | + |
| 171 | + void main() { |
| 172 | + |
| 173 | + vec2 cellSize = 1.0 / resolution.xy; |
| 174 | + |
| 175 | + float waterLevel = texture2D( texture, point1 ).x; |
| 176 | + |
| 177 | + vec2 normal = vec2( |
| 178 | + ( texture2D( texture, point1 + vec2( - cellSize.x, 0 ) ).x - texture2D( texture, point1 + vec2( cellSize.x, 0 ) ).x ) * WIDTH / BOUNDS, |
| 179 | + ( texture2D( texture, point1 + vec2( 0, - cellSize.y ) ).x - texture2D( texture, point1 + vec2( 0, cellSize.y ) ).x ) * WIDTH / BOUNDS ); |
| 180 | + |
| 181 | + if ( gl_FragCoord.x < 1.5 ) { |
| 182 | + |
| 183 | + gl_FragColor = encode_float( waterLevel ); |
| 184 | + |
| 185 | + } |
| 186 | + else if ( gl_FragCoord.x < 2.5 ) { |
| 187 | + |
| 188 | + gl_FragColor = encode_float( normal.x ); |
| 189 | + |
| 190 | + } |
| 191 | + else if ( gl_FragCoord.x < 3.5 ) { |
| 192 | + |
| 193 | + gl_FragColor = encode_float( normal.y ); |
| 194 | + |
| 195 | + } |
| 196 | + else { |
| 197 | + |
| 198 | + gl_FragColor = encode_float( 0.0 ); |
| 199 | + |
| 200 | + } |
| 201 | + |
| 202 | + } |
| 203 | + |
| 204 | + </script> |
125 | 205 |
|
126 | 206 | <!-- This is the water visualization shader, copied from the MeshPhongMaterial and modified: --> |
127 | 207 | <script id="waterVertexShader" type="x-shader/x-vertex"> |
|
230 | 310 | var heightmapVariable; |
231 | 311 | var waterUniforms; |
232 | 312 | var smoothShader; |
| 313 | + var readWaterLevelShader; |
| 314 | + var readWaterLevelRenderTarget; |
| 315 | + var readWaterLevelImage; |
| 316 | + var waterNormal = new THREE.Vector3(); |
| 317 | + |
| 318 | + var NUM_SPHERES = 5; |
| 319 | + var spheres = []; |
| 320 | + var spheresEnabled = true; |
233 | 321 |
|
234 | 322 | var simplex = new SimplexNoise(); |
235 | 323 |
|
|
307 | 395 |
|
308 | 396 | var effectController = { |
309 | 397 | mouseSize: 20.0, |
310 | | - viscosity: 0.03 |
| 398 | + viscosity: 0.98, |
| 399 | + spheresEnabled: spheresEnabled |
311 | 400 | }; |
312 | 401 |
|
313 | 402 | var valuesChanger = function() { |
314 | 403 |
|
315 | 404 | heightmapVariable.material.uniforms.mouseSize.value = effectController.mouseSize; |
316 | 405 | heightmapVariable.material.uniforms.viscosityConstant.value = effectController.viscosity; |
| 406 | + spheresEnabled = effectController.spheresEnabled; |
| 407 | + for ( var i = 0; i < NUM_SPHERES; i++ ) { |
| 408 | + if ( spheres[ i ] ) { |
| 409 | + spheres[ i ].visible = spheresEnabled; |
| 410 | + } |
| 411 | + } |
317 | 412 |
|
318 | 413 | }; |
319 | 414 |
|
320 | 415 | gui.add( effectController, "mouseSize", 1.0, 100.0, 1.0 ).onChange( valuesChanger ); |
321 | | - gui.add( effectController, "viscosity", 0.0, 0.1, 0.001 ).onChange( valuesChanger ); |
| 416 | + gui.add( effectController, "viscosity", 0.9, 0.999, 0.001 ).onChange( valuesChanger ); |
| 417 | + gui.add( effectController, "spheresEnabled", 0, 1, 1 ).onChange( valuesChanger ); |
322 | 418 | var buttonSmooth = { |
323 | 419 | smoothWater: function() { |
324 | | - smoothWater(); |
| 420 | + smoothWater(); |
325 | 421 | } |
326 | 422 | }; |
327 | 423 | gui.add( buttonSmooth, 'smoothWater' ); |
328 | 424 |
|
329 | 425 |
|
330 | 426 | initWater(); |
| 427 | + |
| 428 | + createSpheres(); |
331 | 429 |
|
332 | 430 | valuesChanger(); |
333 | 431 |
|
|
402 | 500 |
|
403 | 501 | heightmapVariable.material.uniforms.mousePos = { value: new THREE.Vector2( 10000, 10000 ) }; |
404 | 502 | heightmapVariable.material.uniforms.mouseSize = { value: 20.0 }; |
405 | | - heightmapVariable.material.uniforms.viscosityConstant = { value: 0.03 }; |
| 503 | + heightmapVariable.material.uniforms.viscosityConstant = { value: 0.98 }; |
| 504 | + heightmapVariable.material.uniforms.heightCompensation = { value: 0 }; |
406 | 505 | heightmapVariable.material.defines.BOUNDS = BOUNDS.toFixed( 1 ); |
407 | 506 |
|
408 | 507 | var error = gpuCompute.init(); |
|
413 | 512 | // Create compute shader to smooth the water surface and velocity |
414 | 513 | smoothShader = gpuCompute.createShaderMaterial( document.getElementById( 'smoothFragmentShader' ).textContent, { texture: { value: null } } ); |
415 | 514 |
|
| 515 | + // Create compute shader to read water level |
| 516 | + readWaterLevelShader = gpuCompute.createShaderMaterial( document.getElementById( 'readWaterLevelFragmentShader' ).textContent, { |
| 517 | + point1: { value: new THREE.Vector2() }, |
| 518 | + texture: { value: null } |
| 519 | + } ); |
| 520 | + readWaterLevelShader.defines.WIDTH = WIDTH.toFixed( 1 ); |
| 521 | + readWaterLevelShader.defines.BOUNDS = BOUNDS.toFixed( 1 ); |
| 522 | + |
| 523 | + // Create a 4x1 pixel image and a render target (Uint8, 4 channels, 1 byte per channel) to read water height and orientation |
| 524 | + readWaterLevelImage = new Uint8Array( 4 * 1 * 4 ); |
| 525 | + |
| 526 | + readWaterLevelRenderTarget = new THREE.WebGLRenderTarget( 4, 1, { |
| 527 | + wrapS: THREE.ClampToEdgeWrapping, |
| 528 | + wrapT: THREE.ClampToEdgeWrapping, |
| 529 | + minFilter: THREE.NearestFilter, |
| 530 | + magFilter: THREE.NearestFilter, |
| 531 | + format: THREE.RGBAFormat, |
| 532 | + type: THREE.UnsignedByteType, |
| 533 | + stencilBuffer: false, |
| 534 | + depthBuffer: false |
| 535 | + } ); |
| 536 | + |
416 | 537 | } |
417 | 538 |
|
418 | 539 | function fillTexture( texture ) { |
|
440 | 561 | var x = i * 128 / WIDTH; |
441 | 562 | var y = j * 128 / WIDTH; |
442 | 563 |
|
443 | | - pixels[ p + 0 ] = noise( x, y, 123.4 ); |
444 | | - pixels[ p + 1 ] = 0; |
| 564 | + pixels[ p + 0 ] = noise( x, y, 123.4 ); |
| 565 | + pixels[ p + 1 ] = pixels[ p + 0 ]; |
445 | 566 | pixels[ p + 2 ] = 0; |
446 | 567 | pixels[ p + 3 ] = 1; |
447 | 568 |
|
|
468 | 589 |
|
469 | 590 | } |
470 | 591 |
|
| 592 | + function createSpheres() { |
| 593 | + |
| 594 | + var sphereTemplate = new THREE.Mesh( new THREE.SphereBufferGeometry( 4, 24, 12 ), new THREE.MeshPhongMaterial( { color: 0xFFFF00 } ) ); |
| 595 | + |
| 596 | + for ( var i = 0; i < NUM_SPHERES; i++ ) { |
| 597 | + |
| 598 | + var sphere = sphereTemplate; |
| 599 | + if ( i < NUM_SPHERES - 1 ) { |
| 600 | + sphere = sphereTemplate.clone(); |
| 601 | + } |
| 602 | + |
| 603 | + sphere.position.x = ( Math.random() - 0.5 ) * BOUNDS * 0.7; |
| 604 | + sphere.position.z = ( Math.random() - 0.5 ) * BOUNDS * 0.7; |
| 605 | + |
| 606 | + sphere.userData.velocity = new THREE.Vector3(); |
| 607 | + |
| 608 | + scene.add( sphere ); |
| 609 | + |
| 610 | + spheres[ i ] = sphere; |
| 611 | + |
| 612 | + } |
| 613 | + |
| 614 | + } |
| 615 | + |
| 616 | + function sphereDynamics() { |
| 617 | + |
| 618 | + var currentRenderTarget = gpuCompute.getCurrentRenderTarget( heightmapVariable ); |
| 619 | + |
| 620 | + readWaterLevelShader.uniforms.texture.value = currentRenderTarget.texture; |
| 621 | + var gl = renderer.context; |
| 622 | + |
| 623 | + for ( var i = 0; i < NUM_SPHERES; i++ ) { |
| 624 | + |
| 625 | + var sphere = spheres[ i ]; |
| 626 | + |
| 627 | + if ( sphere ) { |
| 628 | + |
| 629 | + // Read water level and orientation |
| 630 | + var u = 0.5 * sphere.position.x / BOUNDS_HALF + 0.5; |
| 631 | + var v = 1 - ( 0.5 * sphere.position.z / BOUNDS_HALF + 0.5 ); |
| 632 | + readWaterLevelShader.uniforms.point1.value.set( u, v ); |
| 633 | + gpuCompute.doRenderTarget( readWaterLevelShader, readWaterLevelRenderTarget ); |
| 634 | + gl.readPixels( 0, 0, 4, 1, gl.RGBA, gl.UNSIGNED_BYTE, readWaterLevelImage ); |
| 635 | + var pixels = new Float32Array( readWaterLevelImage.buffer ); |
| 636 | + |
| 637 | + // Get orientation |
| 638 | + waterNormal.set( pixels[ 1 ], 0, - pixels[ 2 ] ); |
| 639 | + |
| 640 | + var pos = sphere.position; |
| 641 | + |
| 642 | + // Set height |
| 643 | + pos.y = pixels[ 0 ]; |
| 644 | + |
| 645 | + // Move sphere |
| 646 | + waterNormal.multiplyScalar( 0.1 ); |
| 647 | + sphere.userData.velocity.add( waterNormal ); |
| 648 | + sphere.userData.velocity.multiplyScalar( 0.998 ); |
| 649 | + pos.add( sphere.userData.velocity ); |
| 650 | + |
| 651 | + if ( pos.x < - BOUNDS_HALF ) { |
| 652 | + pos.x = - BOUNDS_HALF + 0.001; |
| 653 | + sphere.userData.velocity.x *= - 0.3; |
| 654 | + } |
| 655 | + else if ( pos.x > BOUNDS_HALF ) { |
| 656 | + pos.x = BOUNDS_HALF - 0.001; |
| 657 | + sphere.userData.velocity.x *= - 0.3; |
| 658 | + } |
| 659 | + |
| 660 | + if ( pos.z < - BOUNDS_HALF ) { |
| 661 | + pos.z = - BOUNDS_HALF + 0.001; |
| 662 | + sphere.userData.velocity.z *= - 0.3; |
| 663 | + } |
| 664 | + else if ( pos.z > BOUNDS_HALF ) { |
| 665 | + pos.z = BOUNDS_HALF - 0.001; |
| 666 | + sphere.userData.velocity.z *= - 0.3; |
| 667 | + } |
| 668 | + |
| 669 | + } |
| 670 | + |
| 671 | + } |
| 672 | + |
| 673 | + } |
| 674 | + |
471 | 675 |
|
472 | 676 | function onWindowResize() { |
473 | 677 |
|
|
557 | 761 | // Do the gpu computation |
558 | 762 | gpuCompute.compute(); |
559 | 763 |
|
| 764 | + if ( spheresEnabled ) { |
| 765 | + sphereDynamics(); |
| 766 | + } |
| 767 | + |
560 | 768 | // Get compute output in custom uniform |
561 | 769 | waterUniforms.heightmap.value = gpuCompute.getCurrentRenderTarget( heightmapVariable ).texture; |
562 | 770 |
|
|
0 commit comments