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