Skip to content

Commit d3bd4db

Browse files
authored
Merge pull request #14942 from yomboprime/moses
Stabilized webgl_gpgpu_water simulation
2 parents 6e1dc70 + 71e474c commit d3bd4db

File tree

1 file changed

+231
-23
lines changed

1 file changed

+231
-23
lines changed

examples/webgl_gpgpu_water.html

Lines changed: 231 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!DOCTYPE html>
1+
<!DOCTYPE html>
22
<html lang="en">
33
<head>
44
<title>three.js webgl - gpgpu - water</title>
@@ -55,18 +55,16 @@
5555
uniform vec2 mousePos;
5656
uniform float mouseSize;
5757
uniform float viscosityConstant;
58-
59-
#define deltaTime ( 1.0 / 60.0 )
60-
#define GRAVITY_CONSTANT ( resolution.x * deltaTime * 3.0 )
58+
uniform float heightCompensation;
6159

6260
void main() {
6361

6462
vec2 cellSize = 1.0 / resolution.xy;
6563

6664
vec2 uv = gl_FragCoord.xy * cellSize;
6765

68-
// heightmapValue.x == height
69-
// heightmapValue.y == velocity
66+
// heightmapValue.x == height from previous frame
67+
// heightmapValue.y == height from penultimate frame
7068
// heightmapValue.z, heightmapValue.w not used
7169
vec4 heightmapValue = texture2D( heightmap, uv );
7270

@@ -76,20 +74,14 @@
7674
vec4 east = texture2D( heightmap, uv + vec2( cellSize.x, 0.0 ) );
7775
vec4 west = texture2D( heightmap, uv + vec2( - cellSize.x, 0.0 ) );
7876

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;
8978

9079
// Mouse influence
9180
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;
9385

9486
gl_FragColor = heightmapValue;
9587

@@ -122,6 +114,94 @@
122114
}
123115

124116
</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>
125205

126206
<!-- This is the water visualization shader, copied from the MeshPhongMaterial and modified: -->
127207
<script id="waterVertexShader" type="x-shader/x-vertex">
@@ -230,6 +310,14 @@
230310
var heightmapVariable;
231311
var waterUniforms;
232312
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;
233321

234322
var simplex = new SimplexNoise();
235323

@@ -307,27 +395,37 @@
307395

308396
var effectController = {
309397
mouseSize: 20.0,
310-
viscosity: 0.03
398+
viscosity: 0.98,
399+
spheresEnabled: spheresEnabled
311400
};
312401

313402
var valuesChanger = function() {
314403

315404
heightmapVariable.material.uniforms.mouseSize.value = effectController.mouseSize;
316405
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+
}
317412

318413
};
319414

320415
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 );
322418
var buttonSmooth = {
323419
smoothWater: function() {
324-
smoothWater();
420+
smoothWater();
325421
}
326422
};
327423
gui.add( buttonSmooth, 'smoothWater' );
328424

329425

330426
initWater();
427+
428+
createSpheres();
331429

332430
valuesChanger();
333431

@@ -402,7 +500,8 @@
402500

403501
heightmapVariable.material.uniforms.mousePos = { value: new THREE.Vector2( 10000, 10000 ) };
404502
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 };
406505
heightmapVariable.material.defines.BOUNDS = BOUNDS.toFixed( 1 );
407506

408507
var error = gpuCompute.init();
@@ -413,6 +512,28 @@
413512
// Create compute shader to smooth the water surface and velocity
414513
smoothShader = gpuCompute.createShaderMaterial( document.getElementById( 'smoothFragmentShader' ).textContent, { texture: { value: null } } );
415514

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+
416537
}
417538

418539
function fillTexture( texture ) {
@@ -440,8 +561,8 @@
440561
var x = i * 128 / WIDTH;
441562
var y = j * 128 / WIDTH;
442563

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 ];
445566
pixels[ p + 2 ] = 0;
446567
pixels[ p + 3 ] = 1;
447568

@@ -468,6 +589,89 @@
468589

469590
}
470591

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+
471675

472676
function onWindowResize() {
473677

@@ -557,6 +761,10 @@
557761
// Do the gpu computation
558762
gpuCompute.compute();
559763

764+
if ( spheresEnabled ) {
765+
sphereDynamics();
766+
}
767+
560768
// Get compute output in custom uniform
561769
waterUniforms.heightmap.value = gpuCompute.getCurrentRenderTarget( heightmapVariable ).texture;
562770

0 commit comments

Comments
 (0)