how to draw 3d water cycle

From our sponsor: What if Your Project Management Tool Was Fast and Intuitive? Attempt Shortcut.

In this tutorial we're going to build a water-like effect with a bit of basic math, a sail, and postprocessing. No fluid simulation, GPGPU, or whatsoever of that complicated stuff. Nosotros're going to depict pretty circles in a canvas, and distort the scene with the consequence.

We recommend that yous become familiar with the nuts of Three.js because nosotros'll omit some of the setup. Only don't worry, most of the tutorial will deal with good erstwhile JavaScript and the sail API. Feel complimentary to chime in if y'all don't feel likewise confident on the Three.js parts.

The effect is divided into 2 chief parts:

  1. Capturing and drawing the ripples to a canvas
  2. Displacing the rendered scene with postprocessing

Let's commencement with updating and drawing the ripples since that's what constitutes the core of the effect.

Making the ripples

The showtime idea that comes to listen is to utilize the current mouse position as a uniform and then simply displace the scene and call it a mean solar day. But that would mean simply having 1 ripple that ever remains at the mouse'due south position. We want something more interesting, and then nosotros want many independent ripples moving at dissimilar positions. For that nosotros'll need to keep rail of each i of them.

We're going to create a WaterTexture class to manage everything related to the ripples:

  1. Capture every mouse move equally a new ripple in an array.
  2. Draw the ripples to a canvass
  3. Erase the ripples when their lifespan is over
  4. Move the ripples using their initial momentum

For at present, let's begin coding by creating our principal App class.

          import { WaterTexture } from './WaterTexture'; class App{     constructor(){         this.waterTexture = new WaterTexture({ debug: true });                  this.tick = this.tick.bind(this);     	this.init();     }     init(){         this.tick();     }     tick(){         this.waterTexture.update();         requestAnimationFrame(this.tick);     } } const myApp = new App();        

Let's create our ripple director WaterTexture with a teeny-tiny 64px canvas.

          export class WaterTexture{   constructor(options) {     this.size = 64;       this.radius = this.size * 0.1;      this.width = this.height = this.size;     if (options.debug) {       this.width = window.innerWidth;       this.height = window.innerHeight;       this.radius = this.width * 0.05;     }            this.initTexture();       if(options.debug) document.body.append(this.canvas);   }     // Initialize our sail   initTexture() {     this.canvass = document.createElement("canvas");     this.canvas.id = "WaterTexture";     this.canvas.width = this.width;     this.canvas.height = this.height;     this.ctx = this.canvas.getContext("2nd");     this.clear(); 	   }   clear() {     this.ctx.fillStyle = "black";     this.ctx.fillRect(0, 0, this.sheet.width, this.sail.superlative);   }   update(){} }        

Note that for development purposes there is a debug option to mount the sail to the DOM and give it a bigger size. In the end effect nosotros won't be using this pick.

At present we tin get ahead and start calculation some of the logic to brand our ripples work:

  1. On constructor() add
    1. this.points array to keep all our ripples
    2. this.radius for the max-radius of a ripple
    3. this.maxAge for the max-age of a ripple
  2. On Update(),
    1. clear the canvas
    2. sing happy birthday to each ripple, and remove those older than this.maxAge
    3. describe each ripple
  3. Create AddPoint(), which is going to accept a normalized position and add a new point to the array.
          class WaterTexture(){     constructor(){         this.size = 64;         this.radius = this.size * 0.1;                  this.points = [];         this.maxAge = 64;         ...     }     ...     addPoint(point){ 		this.points.push({ x: bespeak.x, y: betoken.y, age: 0 });     } 	update(){         this.clear();         this.points.forEach(indicate => {             point.historic period += 1;             if(signal.age > this.maxAge){                 this.points.splice(i, 1);             }         })         this.points.forEach(bespeak => {             this.drawPoint(point);         })     } }        

Note that AddPoint() receives normalized values, from 0 to 1. If the canvas happens to resize, nosotros can use the normalized points to describe using the correct size.

Let's create drawPoint(point) to kickoff drawing the ripples: Catechumen the normalized point coordinates into canvas coordinates. Then, draw a happy lilliputian circle:

          class WaterTexture(){     ...     drawPoint(point) {         // Convert normalized position into canvas coordinates         let pos = {             x: point.ten * this.width,             y: point.y * this.pinnacle         }         const radius = this.radius;                           this.ctx.beginPath();         this.ctx.arc(pos.ten, pos.y, radius, 0, Math.PI * two);         this.ctx.fill();     } }        

For our ripples to accept a strong push at the centre and a weak forcefulness at the edges, we'll make our circumvolve a Radial Gradient, which looses transparency as it moves to the edges.

Radial Gradients create a dithering-like effect when a lot of them overlap. It looks stylish but non as smooth as what nosotros want it to look similar.

To make our ripples smooth, we'll use the circumvolve's shadow instead of using the circumvolve itself. Shadows give us the gradient-like consequence without the dithering-like effect. The difference is in the way shadows are painted to the canvas.

Since we only desire to see the shadow and not the flat-colored circumvolve, nosotros'll give the shadow a high offset. And we'll move the circumvolve in the opposite management.

As the ripple gets older, we'll reduce it's opacity until it disappears:

          export form WaterTexture(){     ...     drawPoint(point) {         ...          const ctx = this.ctx;         // Lower the opacity equally it gets older         let intensity = i.;         intensity = i. - point.age / this.maxAge;                  let colour = "255,255,255";                  allow kickoff = this.width * 5.;         // 1. Give the shadow a high offset.         ctx.shadowOffsetX = outset;          ctx.shadowOffsetY = start;          ctx.shadowBlur = radius * ane;          ctx.shadowColor = `rgba(${color},${0.2 * intensity})`;                    this.ctx.beginPath();         this.ctx.fillStyle = "rgba(255,0,0,1)";         // 2. Motility the circle to the other direction of the outset         this.ctx.arc(pos.x - beginning, pos.y - beginning, radius, 0, Math.PI * ii);         this.ctx.fill up();     } }        

To innovate interactivity, we'll add the mousemove event listener to app class and ship the normalized mouse position to WaterTexture.

          import { WaterTexture } from './WaterTexture'; grade App { 	... 	init(){         window.addEventListener('mousemove', this.onMouseMove.demark(this));         this.tick(); 	} 	onMouseMove(ev){         const signal = { 			x: ev.clientX/ window.innerWidth,  			y: ev.clientY/ window.innerHeight,          }         this.waterTexture.addPoint(point); 	} }        

Nifty, now we've created a disappearing trail of ripples. Now, let'south give them some momentum!

Momentum

To give momentum to a ripple, we demand its direction and forcefulness. Whenever we create a new ripple, we'll compare its position with the last ripple. Then we'll calculate its unit vector and force.

On every update, we'll update the ripples' positions with their unit vector and position. And as they get older nosotros'll move them slower and slower until they retire or become live on a farm. Whatever happens first.

          export lass WaterTexture{ 	...     constructor(){         ...         this.terminal = zilch;     }     addPoint(betoken){         let force = 0;         allow vx = 0;         let vy = 0;         const final = this.last;         if(last){             const relativeX = point.ten - concluding.x;             const relativeY = betoken.y - terminal.y;             // Distance formula             const distanceSquared = relativeX * relativeX + relativeY * relativeY;             const distance = Math.sqrt(distanceSquared);             // Calculate Unit of measurement Vector             vx = relativeX / altitude;             vy = relativeY / altitude;                          strength = Math.min(distanceSquared * 10000,i.);         }                  this.last = {             x: indicate.x,             y: point.y         }         this.points.push({ x: indicate.x, y: betoken.y, historic period: 0, strength, vx, vy });     } 	 	update(){         this.articulate();         let agePart = i. / this.maxAge;         this.points.forEach((point,i) => {             allow slowAsOlder = (1.- signal.age / this.maxAge)             let forcefulness = point.force * agePart * slowAsOlder;               point.x += betoken.vx * forcefulness;               indicate.y += point.vy * force;             point.age += 1;             if(point.age > this.maxAge){                 this.points.splice(i, 1);             }         })         this.points.forEach(bespeak => {             this.drawPoint(point);         })     } }        

Note that instead of using the final ripple in the assortment, nosotros use a dedicated this.last. This mode, our ripples always have a point of reference to summate their strength and unit vector.

Let's fine-tune the intensity with some easings. Instead of just decreasing until information technology's removed, nosotros'll make information technology increase at the first and and then decrease:

          const easeOutSine = (t, b, c, d) => {   return c * Math.sin((t / d) * (Math.PI / two)) + b; };  const easeOutQuad = (t, b, c, d) => {   t /= d;   return -c * t * (t - ii) + b; };  export form WaterTexture(){ 	drawPoint(point){ 	... 	allow intensity = ane.;         if (point.age < this.maxAge * 0.iii) {           intensity = easeOutSine(point.historic period / (this.maxAge * 0.3), 0, 1, 1);         } else {           intensity = easeOutQuad(             1 - (point.age - this.maxAge * 0.three) / (this.maxAge * 0.seven),             0,             1,             1           );         }         intensity *= point.force;         ... 	} }        

Now we're finished with creating and updating the ripples. It'due south looking amazing.

But how do we apply what we take painted to the canvass to distort our concluding scene?

Canvas every bit a texture

Let's utilize the canvas as a texture, hence the name WaterTexture. We are going to draw our ripples on the sheet, and use it as a texture in a postprocessing shader.

Get-go, allow's make a texture using our sheet and refresh/update that texture at the terminate of every update:

          import * as THREE from 'three' class WaterTexture(){ 	initTexture(){ 		... 		this.texture = new THREE.Texture(this.sail); 	} 	update(){         ... 		this.texture.needsUpdate = true; 	} }        

By creating a texture of our sail, nosotros tin can sample our sail like nosotros would with whatsoever other texture. But how is this useful to us? Our ripples are but white spots on the canvass.

In the distortion shader, we're going to need the direction and intensity of the distortion for each pixel. If you recall, we already take the management and force of each ripple. But how do we communicate that to the shader?

Encoding data in the color channels

Instead of thinking of the canvas as a identify where we draw happy little clouds, nosotros are going to remember virtually the canvas' color channels as places to shop our data and read them later on on our vertex shader.

In the Cherry-red and Green channels, nosotros'll store the unit vector of the ripple. In the Blue aqueduct, we'll store the intensity of the ripple.

Since RGB channels range from 0 to 255, we demand to transport our data that range to normalize it. So, we'll transform the unit of measurement vector range (-1 to 1) and the intensity range (0 to 1) into 0 to 255.

          class WaterEffect {     drawPoint(signal){ 		...          		// Insert data to color channels         // RG = Unit vector         let red = ((point.vx + 1) / 2) * 255;         let green = ((point.vy + i) / ii) * 255;         // B = Unit vector         let blue = intensity * 255;         allow color = `${red}, ${green}, ${blue}`;                   permit offset = this.size * 5;         ctx.shadowOffsetX = commencement;          ctx.shadowOffsetY = offset;          ctx.shadowBlur = radius * one;          ctx.shadowColor = `rgba(${colour},${0.two * intensity})`;           this.ctx.beginPath();         this.ctx.fillStyle = "rgba(255,0,0,one)";         this.ctx.arc(pos.x - kickoff, pos.y - commencement, radius, 0, Math.PI * two);         this.ctx.fill();     } }        

Annotation: Remember how nosotros painted the canvas black? When our shader reads that pixel, it'southward going to utilize a distortion of 0, merely distorting where our ripples are painting.

Look at the pretty color our beautiful information gives the ripples now!

With that, we're finished with the ripples. Next, we'll create our scene and apply the distortion to the result.

Creating a basic Three.js scene

For this effect, it doesn't thing what nosotros return. And so, nosotros'll just have a single plane to showcase the effect. But feel gratis to create an awesome-looking scene and share it with the states in the comments!

Since nosotros're washed with WaterTexture, don't forget to plow the debug pick to fake.

          import * as THREE from "three"; import { WaterTexture } from './WaterTexture';  class App {     constructor(){         this.waterTexture = new WaterTexture({ debug: false });                  this.renderer = new THREE.WebGLRenderer({           antialias: simulated         });         this.renderer.setSize(window.innerWidth, window.innerHeight);         this.renderer.setPixelRatio(window.devicePixelRatio);         certificate.trunk.append(this.renderer.domElement);                  this.camera = new Three.PerspectiveCamera(           45,           window.innerWidth / window.innerHeight,           0.ane,           10000         );         this.camera.position.z = 50;                  this.touchTexture = new TouchTexture();                  this.tick = this.tick.bind(this);         this.onMouseMove = this.onMouseMove.demark(this);                  this.init();          }     addPlane(){         let geometry = new THREE.PlaneBufferGeometry(v,5,1,one);         let material = new THREE.MeshNormalMaterial();         allow mesh = new THREE.Mesh(geometry, material);                  window.addEventListener("mousemove", this.onMouseMove);         this.scene.add together(mesh);     }     init(){     	this.addPlane();      	this.tick();     }     render(){         this.renderer.render(this.scene, this.photographic camera);     }     tick(){         this.return();         this.waterTexture.update();         requrestAnimationFrame(this.tick);     } }        

Applying the distortion to the rendered scene

Nosotros are going to use postprocessing to apply the water-like effect to our return.

Postprocessing allows you to add together effects or filters after (post) your scene is rendered (processing). Like any kind of image issue or filter you might meet on snapchat or Instagram, there is a lot of absurd stuff you can exercise with postprocessing.

For our case, we'll render our scene normally with a RenderPass, and apply the event on top of it with a custom EffectPass.

Permit's return our scene with postprocessing'due south EffectComposer instead of the Three.js renderer.

Note that EffectComposer works by going through its passes on each return. Information technology doesn't render anything unless information technology has a laissez passer for it. We need to add the render of our scene using a RenderPass :

          import { EffectComposer, RenderPass } from 'postprocessing' course App{     constructor(){         ... 		this.composer = new EffectComposer(this.renderer);          this.clock = new Three.Clock();         ...     }     initComposer(){         const renderPass = new RenderPass(this.scene, this.camera);              this.composer.addPass(renderPass);     }     init(){     	this.initComposer();     	...     }     render(){         this.composer.return(this.clock.getDelta());     } }        

Things should look about the same. Only now we get-go adding custom postprocessing effects.

We are going to create the WaterEffect class that extends postprocessing'due south Effect. It is going to receive the canvass texture in the constructor and arrive a compatible in its fragment shader.

In the fragment shader, we'll distort the UVs using postprocessing's function mainUv using our canvas texture. Postprocessing is then going to have these UVs and sample our regular scene distorted.

Although nosotros'll just use postprocessing'southward mainUv office, at that place are a lot of interesting functions y'all can use. I recommend you check out the wiki for more data!

Since we already take the unit of measurement vector and intensity, we only need to multiply them together. Only since the texture values are normalized we need to convert our unit vector from a range of 1 to 0, into a range of -1 to 0:

          import * equally Three from "three"; import { Upshot } from "postprocessing";  export class WaterEffect extends Event {   constructor(texture) {     super("WaterEffect", fragment, {       uniforms: new Map([["uTexture", new THREE.Uniform(texture)]])     });   } } export default WaterEffect;  const fragment = ` uniform sampler2D uTexture; #define PI 3.14159265359  void mainUv(inout vec2 uv) {         vec4 tex = texture2D(uTexture, uv); 		// Convert normalized values into regular unit vector         bladder vx = -(tex.r *2. - 1.);         float vy = -(tex.g *ii. - one.); 		// Normalized intensity works only fine for intensity         bladder intensity = tex.b;         float maxAmplitude = 0.2;         uv.ten += vx * intensity * maxAmplitude;         uv.y += vy * intensity * maxAmplitude;     } `;        

Nosotros'll then instantiate WaterEffect with our canvas texture and add it as an EffectPass after our RenderPass. Then we'll make sure our composer only renders the last upshot to the screen:

          import { WaterEffect } from './WaterEffect' import { EffectPass } from 'postprocessing' class App{     ... 	initComposer() {         const renderPass = new RenderPass(this.scene, this.camera);         this.waterEffect = new WaterEffect(  this.touchTexture.texture);          const waterPass = new EffectPass(this.camera, this.waterEffect);          renderPass.renderToScreen = simulated;         waterPass.renderToScreen = true;         this.composer.addPass(renderPass);         this.composer.addPass(waterPass); 	} }        

And here nosotros have the final result!

An awesome and fun effect to play with!

Conclusion

Through this article, we've created ripples, encoded their data into the color channels and used it in a postprocessing effect to distort our render.

That's a lot of complicated-sounding words! Swell work, pat yourself on the back or accomplish out on Twitter and I'll do it for you 🙂

But there'due south however a lot more to explore:

  1. Drawing the ripples with a hollow circle
  2. Giving the ripples an actual radial-slope
  3. Expanding the ripples as they get older
  4. Or using the canvass as a texture technique to create interactive particles as in Bruno's commodity.

We promise y'all enjoyed this tutorial and had a fun time making ripples. If you have any questions, don't hesitate to comment below or on Twitter!

guzmansaire1998.blogspot.com

Source: https://tympanus.net/codrops/2019/10/08/creating-a-water-like-distortion-effect-with-three-js/

0 Response to "how to draw 3d water cycle"

Postar um comentário

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel