2
0
Эх сурвалжийг харах

Merge pull request #14942 from yomboprime/moses

Stabilized webgl_gpgpu_water simulation
Mr.doob 6 жил өмнө
parent
commit
d3bd4db57d

+ 231 - 23
examples/webgl_gpgpu_water.html

@@ -1,4 +1,4 @@
-<!DOCTYPE html>
+ <!DOCTYPE html>
 <html lang="en">
 	<head>
 		<title>three.js webgl - gpgpu - water</title>
@@ -55,9 +55,7 @@
 			uniform vec2 mousePos;
 			uniform float mouseSize;
 			uniform float viscosityConstant;
-
-			#define deltaTime ( 1.0 / 60.0 )
-			#define GRAVITY_CONSTANT ( resolution.x * deltaTime * 3.0 )
+			uniform float heightCompensation;
 
 			void main()	{
 
@@ -65,8 +63,8 @@
 
 				vec2 uv = gl_FragCoord.xy * cellSize;
 
-				// heightmapValue.x == height
-				// heightmapValue.y == velocity
+				// heightmapValue.x == height from previous frame
+				// heightmapValue.y == height from penultimate frame
 				// heightmapValue.z, heightmapValue.w not used
 				vec4 heightmapValue = texture2D( heightmap, uv );
 
@@ -76,20 +74,14 @@
 				vec4 east = texture2D( heightmap, uv + vec2( cellSize.x, 0.0 ) );
 				vec4 west = texture2D( heightmap, uv + vec2( - cellSize.x, 0.0 ) );
 
-				float sump = north.x + south.x + east.x + west.x - 4.0 * heightmapValue.x;
-
-				float accel = sump * GRAVITY_CONSTANT;
-
-				// Dynamics
-				heightmapValue.y += accel;
-				heightmapValue.x += heightmapValue.y * deltaTime;
-
-				// Viscosity
-				heightmapValue.x += sump * viscosityConstant;
+				float newHeight = ( ( north.x + south.x + east.x + west.x ) * 0.5 - heightmapValue.y ) * viscosityConstant;
 
 				// Mouse influence
 				float mousePhase = clamp( length( ( uv - vec2( 0.5 ) ) * BOUNDS - vec2( mousePos.x, - mousePos.y ) ) * PI / mouseSize, 0.0, PI );
-				heightmapValue.x += cos( mousePhase ) + 1.0;
+				newHeight += ( cos( mousePhase ) + 1.0 ) * 0.28;
+				
+				heightmapValue.y = heightmapValue.x;
+				heightmapValue.x = newHeight;
 
 				gl_FragColor = heightmapValue;
 
@@ -122,6 +114,94 @@
 			}
 
 		</script>
+		
+		<!-- This is a 'compute shader' to read the current level and normal of water at a point -->
+		<!-- It is used with a variable of size 1x1 -->
+		<script id="readWaterLevelFragmentShader" type="x-shader/x-fragment">
+
+			uniform vec2 point1;
+		
+			uniform sampler2D texture;
+		
+			// Integer to float conversion from https://stackoverflow.com/questions/17981163/webgl-read-pixels-from-floating-point-render-target
+			
+			float shift_right( float v, float amt ) {
+
+				v = floor( v ) + 0.5;
+				return floor( v / exp2( amt ) );
+
+			}
+
+			float shift_left( float v, float amt ) { 
+
+				return floor( v * exp2( amt ) + 0.5 );
+
+			}
+
+			float mask_last( float v, float bits ) {
+
+				return mod( v, shift_left( 1.0, bits ) );
+
+			}
+
+			float extract_bits( float num, float from, float to ) {
+
+				from = floor( from + 0.5 ); to = floor( to + 0.5 );
+				return mask_last( shift_right( num, from ), to - from );
+
+			}
+
+			vec4 encode_float( float val ) {
+				if ( val == 0.0 ) return vec4( 0, 0, 0, 0 );
+				float sign = val > 0.0 ? 0.0 : 1.0;
+				val = abs( val );
+				float exponent = floor( log2( val ) );
+				float biased_exponent = exponent + 127.0;
+				float fraction = ( ( val / exp2( exponent ) ) - 1.0 ) * 8388608.0;
+				float t = biased_exponent / 2.0;
+				float last_bit_of_biased_exponent = fract( t ) * 2.0;
+				float remaining_bits_of_biased_exponent = floor( t );
+				float byte4 = extract_bits( fraction, 0.0, 8.0 ) / 255.0;
+				float byte3 = extract_bits( fraction, 8.0, 16.0 ) / 255.0;
+				float byte2 = ( last_bit_of_biased_exponent * 128.0 + extract_bits( fraction, 16.0, 23.0 ) ) / 255.0;
+				float byte1 = ( sign * 128.0 + remaining_bits_of_biased_exponent ) / 255.0;
+				return vec4( byte4, byte3, byte2, byte1 );
+			}
+
+			void main()	{
+
+				vec2 cellSize = 1.0 / resolution.xy;
+
+				float waterLevel = texture2D( texture, point1 ).x;
+
+				vec2 normal = vec2(
+					( texture2D( texture, point1 + vec2( - cellSize.x, 0 ) ).x - texture2D( texture, point1 + vec2( cellSize.x, 0 ) ).x ) * WIDTH / BOUNDS,
+					( texture2D( texture, point1 + vec2( 0, - cellSize.y ) ).x - texture2D( texture, point1 + vec2( 0, cellSize.y ) ).x ) * WIDTH / BOUNDS );
+
+				if ( gl_FragCoord.x < 1.5 ) {
+
+					gl_FragColor = encode_float( waterLevel );
+				
+				}
+				else if ( gl_FragCoord.x < 2.5 ) {
+
+					gl_FragColor = encode_float( normal.x );
+
+				}
+				else if ( gl_FragCoord.x < 3.5 ) {
+
+					gl_FragColor = encode_float( normal.y );
+
+				}
+				else {
+
+					gl_FragColor = encode_float( 0.0 );
+
+				}
+
+			}
+
+		</script>
 
 		<!-- This is the water visualization shader, copied from the MeshPhongMaterial and modified: -->
 		<script id="waterVertexShader" type="x-shader/x-vertex">
@@ -230,6 +310,14 @@
 			var heightmapVariable;
 			var waterUniforms;
 			var smoothShader;
+			var readWaterLevelShader;
+			var readWaterLevelRenderTarget;
+			var readWaterLevelImage;
+			var waterNormal = new THREE.Vector3();
+			
+			var NUM_SPHERES = 5;
+			var spheres = [];
+			var spheresEnabled = true;
 
 			var simplex = new SimplexNoise();
 
@@ -307,27 +395,37 @@
 
 				var effectController = {
 					mouseSize: 20.0,
-					viscosity: 0.03
+					viscosity: 0.98,
+					spheresEnabled: spheresEnabled
 				};
 
 				var valuesChanger = function() {
 
 					heightmapVariable.material.uniforms.mouseSize.value = effectController.mouseSize;
 					heightmapVariable.material.uniforms.viscosityConstant.value = effectController.viscosity;
+					spheresEnabled = effectController.spheresEnabled;
+					for ( var i = 0; i < NUM_SPHERES; i++ ) {
+						if ( spheres[ i ] ) {
+							spheres[ i ].visible = spheresEnabled;
+						}
+					}
 
 				};
 
 				gui.add( effectController, "mouseSize", 1.0, 100.0, 1.0 ).onChange( valuesChanger );
-				gui.add( effectController, "viscosity", 0.0, 0.1, 0.001 ).onChange( valuesChanger );
+				gui.add( effectController, "viscosity", 0.9, 0.999, 0.001 ).onChange( valuesChanger );
+				gui.add( effectController, "spheresEnabled", 0, 1, 1 ).onChange( valuesChanger );
 				var buttonSmooth = {
 				    smoothWater: function() {
-					smoothWater();
+						smoothWater();
 				    }
 				};
 				gui.add( buttonSmooth, 'smoothWater' );
 
 
 				initWater();
+				
+				createSpheres();
 
 				valuesChanger();
 
@@ -402,7 +500,8 @@
 
 				heightmapVariable.material.uniforms.mousePos = { value: new THREE.Vector2( 10000, 10000 ) };
 				heightmapVariable.material.uniforms.mouseSize = { value: 20.0 };
-				heightmapVariable.material.uniforms.viscosityConstant = { value: 0.03 };
+				heightmapVariable.material.uniforms.viscosityConstant = { value: 0.98 };
+				heightmapVariable.material.uniforms.heightCompensation = { value: 0 };
 				heightmapVariable.material.defines.BOUNDS = BOUNDS.toFixed( 1 );
 
 				var error = gpuCompute.init();
@@ -413,6 +512,28 @@
 				// Create compute shader to smooth the water surface and velocity
 				smoothShader = gpuCompute.createShaderMaterial( document.getElementById( 'smoothFragmentShader' ).textContent, { texture: { value: null } } );
 
+				// Create compute shader to read water level
+				readWaterLevelShader = gpuCompute.createShaderMaterial( document.getElementById( 'readWaterLevelFragmentShader' ).textContent, {
+					point1: { value: new THREE.Vector2() },
+					texture: { value: null }
+				} );
+				readWaterLevelShader.defines.WIDTH = WIDTH.toFixed( 1 );
+				readWaterLevelShader.defines.BOUNDS = BOUNDS.toFixed( 1 );
+				
+				// Create a 4x1 pixel image and a render target (Uint8, 4 channels, 1 byte per channel) to read water height and orientation
+				readWaterLevelImage = new Uint8Array( 4 * 1 * 4 );
+
+				readWaterLevelRenderTarget = new THREE.WebGLRenderTarget( 4, 1, {
+					wrapS: THREE.ClampToEdgeWrapping,
+					wrapT: THREE.ClampToEdgeWrapping,
+					minFilter: THREE.NearestFilter,
+					magFilter: THREE.NearestFilter,
+					format: THREE.RGBAFormat,
+					type: THREE.UnsignedByteType,
+					stencilBuffer: false,
+					depthBuffer: false
+				} );
+
 			}
 
 			function fillTexture( texture ) {
@@ -440,8 +561,8 @@
 						var x = i * 128 / WIDTH;
 						var y = j * 128 / WIDTH;
 
-					        pixels[ p + 0 ] = noise( x, y, 123.4 );
-						pixels[ p + 1 ] = 0;
+						pixels[ p + 0 ] = noise( x, y, 123.4 );
+						pixels[ p + 1 ] = pixels[ p + 0 ];
 						pixels[ p + 2 ] = 0;
 						pixels[ p + 3 ] = 1;
 
@@ -468,6 +589,89 @@
 				
 			}
 
+			function createSpheres() {
+
+				var sphereTemplate = new THREE.Mesh( new THREE.SphereBufferGeometry( 4, 24, 12 ), new THREE.MeshPhongMaterial( { color: 0xFFFF00 } ) );
+
+				for ( var i = 0; i < NUM_SPHERES; i++ ) {
+
+					var sphere = sphereTemplate;
+					if ( i < NUM_SPHERES - 1 ) {
+						sphere = sphereTemplate.clone();
+					}
+
+					sphere.position.x = ( Math.random() - 0.5 ) * BOUNDS * 0.7;
+					sphere.position.z = ( Math.random() - 0.5 ) * BOUNDS * 0.7;
+					
+					sphere.userData.velocity = new THREE.Vector3();
+
+					scene.add( sphere );
+
+					spheres[ i ] = sphere;
+
+				}
+
+			}
+
+			function sphereDynamics() {
+
+				var currentRenderTarget = gpuCompute.getCurrentRenderTarget( heightmapVariable );
+
+				readWaterLevelShader.uniforms.texture.value = currentRenderTarget.texture;
+				var gl = renderer.context;
+
+				for ( var i = 0; i < NUM_SPHERES; i++ ) {
+
+					var sphere = spheres[ i ];
+					
+					if ( sphere ) {
+
+						// Read water level and orientation
+						var u = 0.5 * sphere.position.x / BOUNDS_HALF + 0.5;
+						var v = 1 - ( 0.5 * sphere.position.z / BOUNDS_HALF + 0.5 );
+						readWaterLevelShader.uniforms.point1.value.set( u, v );
+						gpuCompute.doRenderTarget( readWaterLevelShader, readWaterLevelRenderTarget );
+						gl.readPixels( 0, 0, 4, 1, gl.RGBA, gl.UNSIGNED_BYTE, readWaterLevelImage );
+						var pixels = new Float32Array( readWaterLevelImage.buffer );
+						
+						// Get orientation
+						waterNormal.set( pixels[ 1 ], 0, - pixels[ 2 ] );
+						
+						var pos = sphere.position;
+						
+						// Set height
+						pos.y = pixels[ 0 ];
+						
+						// Move sphere
+						waterNormal.multiplyScalar( 0.1 );
+						sphere.userData.velocity.add( waterNormal );
+						sphere.userData.velocity.multiplyScalar( 0.998 );
+						pos.add( sphere.userData.velocity );
+						
+						if ( pos.x < - BOUNDS_HALF ) {
+							pos.x = - BOUNDS_HALF + 0.001;
+							sphere.userData.velocity.x *= - 0.3;
+						}
+						else if ( pos.x > BOUNDS_HALF ) {
+							pos.x = BOUNDS_HALF - 0.001;
+							sphere.userData.velocity.x *= - 0.3;
+						}
+
+						if ( pos.z < - BOUNDS_HALF ) {
+							pos.z = - BOUNDS_HALF + 0.001;
+							sphere.userData.velocity.z *= - 0.3;
+						}
+						else if ( pos.z > BOUNDS_HALF ) {
+							pos.z = BOUNDS_HALF - 0.001;
+							sphere.userData.velocity.z *= - 0.3;
+						}
+
+					}
+
+				}
+
+			}
+
 
 			function onWindowResize() {
 
@@ -557,6 +761,10 @@
 				// Do the gpu computation
 				gpuCompute.compute();
 
+				if ( spheresEnabled ) {
+					sphereDynamics();
+				}
+
 				// Get compute output in custom uniform
 				waterUniforms.heightmap.value = gpuCompute.getCurrentRenderTarget( heightmapVariable ).texture;