import * as RE from 'rogue-engine' import * as THREE from 'three' // https://codesandbox.io/s/prototypes-pygsc7?file=/prototypes/water/001_gerstner.html:0-10387 // https://catlikecoding.com/unity/tutorials/flow/waves/ // Old Fungi Version // https://sketchpunk.bitbucket.io/src/fungi_v4/002_gerstner_waves.html // https://bitbucket.org/sketchpunk/sketchpunk.bitbucket.io/src/c036dfb4b5a93425220b4bb482c0e96e1edc77cf/src/fungi_v4/002_gerstner_waves.html#lines-304 export default class GerstnerWavesComponent extends RE.Component { wave: GerstnerWave = new GerstnerWave(2, 12) vertexShader = `#version 300 es in vec3 position; in vec3 normal; in vec2 uv; uniform mat4 modelMatrix; uniform mat4 viewMatrix; uniform mat4 projectionMatrix; uniform float clock; out vec3 fragNorm; out vec3 fragWPos; ///////////////////////////////////////////////////////////////// // #region GERSTNER WAVE const float PI_2 = 6.283185307179586; uniform vec4 Waves[3]; // XY : Direction, Z: Steepness, W: Wavelength uniform int WaveCnt; uniform float WaveSpeed; uniform int WaveXZ; uniform float GridSize; uniform float GridRes; // https://catlikecoding.com/unity/tutorials/flow/waves/ // XY : norm( Dir.xy ), Z : Steepness( 0->1 ), W : 0 to Total Grid Divisions, Max gives one wave vec3 gerstnerWave( vec2 dir, float steepness, float wavelength, vec2 pnt, float time, out vec3 tangent, out vec3 binormal ){ float k = PI_2 / wavelength; // Phase Increment float c = sqrt( 9.8 / k ); // Phase Speed. Higher the wave, the faster it moves, Gravity Constant float f = k * ( dot( dir, pnt ) - c * time ); // Frequency - Specific time in Phase : PhaseInc * ( Angle - PhaseSpeed * Time ) float a = steepness / k; // Amptitude, Steep=1, app is at max where mesh will start to loop onto self. // cache results for multi reuse. float sin_f = sin( f ); float cos_f = cos( f ); float acos_f = a * cos_f; float scos_f = steepness * cos_f; float ssin_f = steepness * sin_f; /*==================================== Must Init outside function as vec3 tangent = vec3( 1.0, 0.0, 0.0 ); vec3 binormal = vec3( 0.0, 0.0, 1.0 ); normal is normalize(cross(binormal, tangent)); */ tangent += vec3( -dir.x * dir.x * ssin_f, dir.x * scos_f, -dir.x * dir.y * ssin_f ); binormal += vec3( -dir.x * dir.y * ssin_f, dir.y * scos_f, -dir.y * dir.y * ssin_f ); return vec3( dir.x * acos_f, a * sin_f, dir.y * acos_f ); } // #endregion ///////////////////////////////////////////////////////////////// // #region GRID const float FLOOR = 0.001; vec2 gridCenterCoord( vec2 p ){ float inc = GridSize / GridRes; p += vec2( GridSize * 0.5 ); // Shift so top Left is at origin return floor( p / vec2( inc ) ); } vec2 gridCoord( vec2 p ){ float inc = GridSize / GridRes; return floor( p / vec2( inc ) ); // return p / vec2( inc ); } // #endregion ///////////////////////////////////////////////////////////////// void main(){ vec3 pos = position; fragNorm = normal; if( pos.y > FLOOR ){ vec2 coord = gridCoord( pos.xz ); vec3 offset = vec3( 0.0 ); vec3 tangent = vec3( 1.0, 0.0, 0.0 ); // Normal is Up, So tangent is Right vec3 binormal = vec3( 0.0, 0.0, 1.0 ); // ... and Binormal is Forward float time = clock * WaveSpeed; if( WaveCnt > 0 ) offset += gerstnerWave( normalize(Waves[0].xy), Waves[0].z, Waves[0].w, coord, time, tangent, binormal ); if( WaveCnt > 1 ) offset += gerstnerWave( normalize(Waves[1].xy), Waves[1].z, Waves[1].w, coord, time, tangent, binormal ); if( WaveCnt > 2 ) offset += gerstnerWave( normalize(Waves[2].xy), Waves[2].z, Waves[2].w, coord, time, tangent, binormal ); if( WaveXZ == 1 ){ // Only keep height change offset.x = 0.0; offset.z = 0.0; }else{ // keep Wave in cube bounds if( coord.x < 1.0 || coord.x >= GridRes ) offset.x = 0.0; if( coord.y < 1.0 || coord.y >= GridRes ) offset.z = 0.0; } pos += offset; // Only Apply Wave Normals on top face, use normal to find it if( normal.x < 0.001 && normal.z < 0.001 && normal.y > 0.9 ){ fragNorm = normalize( cross( binormal, tangent ) ); } } vec4 wPos = modelMatrix * vec4( pos, 1.0 ); fragWPos = wPos.xyz; gl_Position = projectionMatrix * viewMatrix * wPos; }` fragmentShader = `#version 300 es precision mediump float; in vec3 fragNorm; in vec3 fragWPos; uniform vec3 cameraPosition; out vec4 outColor; //////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////// const vec3 lightWPos = vec3( 10.0, 10.0, 10.0 ); void main(){ // vec3 N = normalize( cross( dFdx(fragWPos), dFdy(fragWPos) ) ); vec3 N = normalize( fragNorm ); // float ns = 6.0; // Normal is to light, scale it up to improve renderering // vec3 N = normalize( vec3( fragNorm.x * ns, fragNorm.y, fragNorm.z * ns ) ); vec3 L = normalize( lightWPos - fragWPos ); float NdL = clamp( dot( L, N ), 0.0, 1.0 ); outColor = vec4( vec3( NdL ), 1.0 ); // if( NdL <= 0.0 ) outColor.rgb = vec3( 1.0, 0.0, 0.0 ); }` elapsed = 0 awake() { } start() { this.elapsed = 0; let objectThree = (this.object3d as any) let geometry: THREE.BoxGeometry = objectThree.geometry; let gridSize = geometry.parameters.width//2 let gridRes = geometry.parameters.width * 6//12 objectThree.material = new THREE.RawShaderMaterial( { depthTest: true, transparent: false, uniforms: { clock : { value : 0 }, Waves : { value : new Float32Array( [ 0.5,0.5,0.09,10, 0,1,0.05,5, 0.3,0.7,0.01,2, ]) }, WaveCnt : { value : 3 }, WaveSpeed : { value : 1.0 }, WaveXZ : { value : 1 }, GridSize : { value : gridSize }, GridRes : { value : gridRes }, }, vertexShader: this.vertexShader, fragmentShader: this.fragmentShader, } ) this.wave = new GerstnerWave(gridSize, gridRes) } update() { this.elapsed += RE.Runtime.deltaTime; (this.object3d as any).material.uniforms.clock.value = this.elapsed } getHeightAt(position: THREE.Vector3) { let height = 0 let coords = new THREE.Vector2(position.x, position.z) this.wave.waves.forEach((wave) => { height += this.wave.calcWaveH(wave, coords, this.elapsed) }) return height } } RE.registerComponent(GerstnerWavesComponent); // Compute height of the position class GerstnerWave { minH = 1; gridSize = 2; gridRes = 12; waveSpeed = 1.0; constructor(gridSize, gridRes) { this.gridSize = gridSize//2 this.gridRes = gridRes//12 } waves = [ { dir:[ 0.5,0.5 ], steep:0.09, wavelen:10 }, { dir:[ 0,1 ], steep:0.05, wavelen:5 }, { dir:[ 0.3,0.7 ], steep:0.01, wavelen:2 }, ]; static dot(a: THREE.Vector2, b: THREE.Vector2) { return a.x * b.x + a.y * b.y; } calcWaveH( w, coord, time ){ const dir = new THREE.Vector2( w.dir[0], w.dir[1] ).normalize(); const k = 6.283185307179586 / w.wavelen; // Phase Increment const c = Math.sqrt( 9.8 / k ); // Phase Speed. Higher the wave, the faster it moves, Gravity Constant const f = k * ( GerstnerWave.dot( dir, coord ) - c * time ); // Frequency - Specific time in Phase : PhaseInc * ( Angle - PhaseSpeed * Time ) const a = w.steep / k; // Amptitude, Steep=1, app is at max where mesh will start to loop onto self. // cache results for multi reuse. const sin_f = Math.sin( f ); // const cos_f = Math.cos( f ); // const acos_f = a * cos_f; // Gerstner Position // return [ // dir.x * acos_f, // a * sin_f, // dir.y * acos_f // ]; // Just the Y value return a * sin_f; } getHit( p, clock=0 ){ const coord = this.gridCoord( p ); const time = clock * this.waveSpeed; let h = 0; for( const w of this.waves ){ h += this.calcWaveH( w, coord, time ); } return [ p[0], this.minH + h, p[2] ]; } gridCoord( p ){ const inc = this.gridSize / this.gridRes; return {x: p[0] / inc , y: p[2] / inc }; } }