Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Water gun 'water stream'

Discussion in 'Shaders' started by AndrewAPrice, Aug 2, 2013.

  1. AndrewAPrice

    AndrewAPrice

    Joined:
    May 14, 2013
    Posts:
    25
    Hello forum.

    I'd like to apologize if this is the wrong forum to post under, I don't think it's a 'technical issue', and this seems to be the most relevant forum to ask for graphical advice on.

    My game is a first person shooter that involves waterguns. It has a toon theme, so I'm not looking for anything overly realistic, just like a blue stream that comes out, so you can spray other people:
    $1048293-Royalty-Free-RF-Clip-Art-Illustration-Of-A-Cartoon-Boy-Spraying-A-Soaker-Gun.jpg

    My attempts have been with particles. Here's what it looks like from the side;
    $bad-watergun-effect.jpg
    This was made using a basic sprite, and tinted it blue.. From the first person view, it looks okay, but from the side, it looks bad.

    It's far to 'thin'.. If I try to increase the opacity, it looks like I'm shooting out 'dots' rather than a stream of water, so to compensate I try to set up the particle emitter's rate, but even at ridiculously high levels like 1,000 per second, it's still somewhat noticeable (and I have performance concerns that if we have 30 or so people running and spraying their water guns at once, that's spawning 30,000 particles per second.)

    Would it be possible to achieve something solid and toon-like like this;
    $good-watergun-effect.jpg
    How would I do it? I'll appreciate any assistance that you could give me.
     
  2. AndrewAPrice

    AndrewAPrice

    Joined:
    May 14, 2013
    Posts:
    25
    I was thinking of doing a screen-space metaball effect?

    I am using Unity Free, and I'm not familiar with multiple-cameras/rendering to texture, so it will be a learning experience.

    Camera 1 - Render depth pass...
    Camera 2 to texture - Render the particles using additive rendering.. Read depth pass from earlier, so only show visible particles..
    Back to Camera 1 - Render the scene (without particles) then at the very end, render the Camera 2 texture fullscreen.. put metaball code inside of the pixel shader (if r > 0.5 draw water)...

    Would this work?
     
  3. aubergine

    aubergine

    Joined:
    Sep 12, 2009
    Posts:
    2,878
    You should extrude a planar mesh along the trajectory and use a shader like this on it.

    EDIT: Awesome textures by the way :)
     
    Last edited: Aug 2, 2013
  4. AndrewAPrice

    AndrewAPrice

    Joined:
    May 14, 2013
    Posts:
    25
    Aubergine - that is a good idea, but I would think the mesh would move along with the character.. and that would look weird - if the player is shooting and turns or moves, the whole stream shouldn't turn at once, it needs a a delay.

    To my metaball idea - Render to texture isn't supported in Unity Free... :(

    Perhaps I could 'software render' it in C# (I know my matrix and projection math) and write it into a texture.. I'm still thinking about how I will do this..
     
  5. AndrewAPrice

    AndrewAPrice

    Joined:
    May 14, 2013
    Posts:
    25
    Aubergine - you are a genius.. (Thanks for complimenting my textures. I got them from the asset store!) At first I was thinking of a static mesh from the gun, that swung around when you moved the gun, but it does not have to be..

    I was thinking "if only there was a way to render it like a string.. string? rope? wait a minute.. how do they render ropes?" then it came to me... Spawn particles, but every particle connected together in a single squirt is rendered as a single line... Like shooting a piece of string.. That way they fly through the air like a contiguous 'squirt' of water yet act like particles with physics. I will have to add some code that if 2 particles get too far away, the 'squirt' will break.

    The line renderer looks promising. It always faces the camera, so it always looks 'thick'. My only concern is that if you look at it from the ends it looks very flat.

    Because I am going for the toon look, I am probably going to render it in solid cyan.. So it might be possible to put a round particle on either end of the line, the same width as the the line. That would round off the edges nicely.
     
  6. AndrewAPrice

    AndrewAPrice

    Joined:
    May 14, 2013
    Posts:
    25
    Success!!!!

    $best-watergun-effect.jpg

    I did what I posted above...!

    I'm no longer using particles.. I created a 'SquirtManager' - a squirt is a connected line of 'squirt particles'. If particles get too far away, the 'squirt' breaks into two.

    The 'squirts' are rendered dynamically using GL - they are lines that always face the camera, with either end having a billboard quad with a circle texture attached, this makes it look thick from all angles (unlike the Line Renderer which looks thin at the end.)
     
    Rocker-Marshall likes this.
  7. Felix King

    Felix King

    Joined:
    May 2, 2012
    Posts:
    23
    Great job, MessiahAndrw! You mind to elaborate a bit on your approach? Maybe a code snippet? I am trying to achieve something similar but I'm going nuts over it. I tried working with particles and as well with the trail renderer but it never gave me satisfactory results :(
     
  8. thienhaflash

    thienhaflash

    Joined:
    Jun 16, 2012
    Posts:
    513
    Hey, I'm about to do a water gun, too. Can you post a webplayer demo so I can have a reference ?
     
  9. AndrewAPrice

    AndrewAPrice

    Joined:
    May 14, 2013
    Posts:
    25
    Here's how I did it. When the user presses the mouse button, I create a Squirt object that represents a single connected stream. About 5 times a second I add a SquirtParticle to the Squirt object. The SquirtParticle has a position/velocity so it spawns at the tip of the gun and heads in the direction the gun is facing.

    If two SquirtParticle next to each other in the Squirt are more than 4 units apart, I split the Squirt object into two (this happens if the user is squirting in one direction then suddenly turns around). If the user also releases the mouse button, then presses it again, it creates another Squirt object so there are two distinct disconnected squirts. If a SquirtParticle hits something, it does damage and destroys that SquirtParticle, which may either split a Squirt into two, or destroy the Squirt if there are no more SquirtParticles in it.

    For each Squirt in the game world, I loop through and draw a 1.0 world-units camera-facing thick line between the SquirtParticles. I then draw a solid circle particle at the front and back of the squirt to give it a solid feeling.

    Here is my code that handles drawing:

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4.  
    5. public class SquirtParticle {
    6.     public Vector3 Position;
    7.     public Vector3 Velocity;
    8.     public int Team;
    9.    
    10.     public SquirtParticle(Vector3 position, Vector3 velocity, int team) {
    11.         Position = position;
    12.         Velocity = velocity;
    13.         Team = team;
    14.     }
    15. }
    16.  
    17. public class Squirt {
    18.     public bool Alive = true;
    19.     public List<SquirtParticle> Particles = new List<SquirtParticle>();
    20.     public Squirt NextSquirt = null;
    21.    
    22.     public void AddParticle(SquirtParticle p) {
    23.         Particles.Add (p);  
    24.     }
    25. }
    26.  
    27. public class SquirtManager : MonoBehaviour {
    28.    
    29.     public Material material;
    30.     public Material ends;
    31.    
    32.     public List<Squirt> Squirts = new List<Squirt>();
    33.    
    34.     public float Width;
    35.     private float halfWidth;
    36.     public float Gravity;
    37.     public float BreakingDistanceSquared = 4.0f;
    38.     public ParticleSystem particleSystem;
    39.    
    40.     void Start() {
    41.         /*
    42.         Squirt s = new Squirt();
    43.         s.AddParticle(new SquirtParticle(transform.position + new Vector3(0, 0, 2), Vector3.up));
    44.         s.AddParticle(new SquirtParticle(transform.position + new Vector3(0, 2, 0), Vector3.up));
    45.         s.AddParticle(new SquirtParticle(new Vector3(2, 0, 0), Vector3.up));
    46.         s.AddParticle(new SquirtParticle(new Vector3(2, 4, 5), Vector3.up));
    47.         AddSquirt(s);*/
    48.        
    49.         OnValidate();
    50.     }
    51.    
    52.     void OnValidate() {
    53.         halfWidth = Width / 2.0f;
    54.     }
    55.    
    56.     public void AddSquirt(Squirt s) {
    57.         Squirts.Add(s);
    58.     }
    59.                
    60.     public Squirt AddParticle(Squirt lastSquirt, SquirtParticle p) {
    61.         if(lastSquirt != null) {
    62.             while(lastSquirt.NextSquirt != null)
    63.                 lastSquirt = lastSquirt.NextSquirt;
    64.         }
    65.        
    66.         bool newSquirt;
    67.         if(lastSquirt == null || !lastSquirt.Alive)
    68.             newSquirt = true;
    69.         else {
    70.             int i = lastSquirt.Particles.Count;
    71.             if(i > 0 && (lastSquirt.Particles[i - 1].Position - p.Position).sqrMagnitude > BreakingDistanceSquared)
    72.                 newSquirt = true;
    73.             else
    74.                 newSquirt = false;
    75.         }
    76.        
    77.         if(newSquirt) {
    78.             // add to a new squrt
    79.             Squirt newS = new Squirt();
    80.             AddSquirt(newS);
    81.             newS.AddParticle(p);
    82.             return newS;
    83.         } else {
    84.             lastSquirt.AddParticle(p);
    85.             return lastSquirt;
    86.         }
    87.     }
    88.    
    89.     void UpdateSquirt(float grav, float delta, Squirt s, ref Stack<Squirt> squirtsToAdd) {
    90.         List<SquirtParticle> partsToDelete = null;
    91.         int i = 0;
    92.         SquirtParticle previous = null;
    93.         bool breakChain = false;
    94.         int maxPart = s.Particles.Count - 1;
    95.        
    96.         foreach(SquirtParticle p in s.Particles) {
    97.             p.Velocity.y -= grav;
    98.             Vector3 v = p.Velocity * delta;
    99.             float mag = v.magnitude;
    100.             v /= mag;
    101.                
    102.             bool die = false;
    103.             //if(Physics.CheckSphere(p.Position, halfWidth)) {
    104.             //    die = true;
    105.                
    106.                 RaycastHit[] hits = Physics.RaycastAll(p.Position, v, mag);
    107.                 if(hits.Length > 0)
    108.                     die = true;
    109.                 foreach(RaycastHit hit in hits) {
    110.                     Vector3 newVelocity = Vector3.Reflect(v, hit.normal);
    111.                    
    112.                     particleSystem.Emit(p.Position, newVelocity * 3.0f, 0.3f, 0.2f, Color.white);
    113.                
    114.                     if(hit.collider.gameObject.tag == "Player") {
    115.                         PlayerController player = hit.collider.gameObject.GetComponent<PlayerController>();
    116.                         if(player.Team != p.Team) // no friendly fire
    117.                             player.Hit(p.Velocity);
    118.                     }
    119.                 }
    120.             //}
    121.            
    122.             if(p.Position.y < 0.0f)
    123.                 die = true;
    124.                
    125.             if(die) {
    126.                 particleSystem.Emit(p.Position, p.Velocity * -0.2f, 0.3f, 0.2f, Color.white);
    127.    
    128.                 // delete this particle
    129.                 if(partsToDelete == null)
    130.                     partsToDelete = new List<SquirtParticle>();
    131.                
    132.                 partsToDelete.Add(p);
    133.                 previous = null;
    134.                
    135.                 if(i != 0 && i != maxPart) {
    136.                     // start at next
    137.                     i++;
    138.                     breakChain = true;
    139.                     break;
    140.                 }
    141.             } else {          
    142.                 p.Position += v;
    143.             }
    144.            
    145.             if(previous != null) {
    146.                 float dist = (p.Position - previous.Position).sqrMagnitude;
    147.                 if(dist >= BreakingDistanceSquared) {
    148.                     breakChain = true;
    149.                     break;
    150.                 }
    151.             }
    152.            
    153.             i++;
    154.             previous = p;
    155.         }
    156.        
    157.         if(breakChain) {
    158.             Squirt newS = new Squirt();
    159.            
    160.             newS.Particles = s.Particles.GetRange(i, maxPart + 1 - i);
    161.             s.Particles.RemoveRange(i, maxPart + 1 - i);
    162.             if(squirtsToAdd == null)
    163.                 squirtsToAdd = new Stack<Squirt>();
    164.            
    165.             squirtsToAdd.Push (newS);
    166.             if(s.NextSquirt == null)
    167.                 s.NextSquirt = newS;
    168.         }
    169.            
    170.         if(partsToDelete != null) {
    171.             // there particles to delete
    172.             foreach(SquirtParticle p in partsToDelete)
    173.                 s.Particles.Remove(p);
    174.            
    175.             if(s.Particles.Count == 0) {
    176.                 s.Alive = false;
    177.             }
    178.         }
    179.     }
    180.    
    181.     void FixedUpdate() {
    182.         // do physics here
    183.         float delta = Time.fixedDeltaTime;
    184.         float grav = Gravity * delta;
    185.        
    186.         List<Squirt> squirtsToDelete = null;
    187.         Stack<Squirt> squirtsToAdd = null;
    188.        
    189.         foreach(Squirt s in Squirts) {
    190.             UpdateSquirt (grav, delta, s, ref squirtsToAdd);
    191.             if(!s.Alive) {
    192.                 // delete this squirt
    193.                 if(squirtsToDelete == null)
    194.                     squirtsToDelete = new List<Squirt>();
    195.                
    196.                 squirtsToDelete.Add(s);
    197.             }
    198.         }
    199.        
    200.         if(squirtsToAdd != null) {
    201.             // a squirt broke, add its additional parts
    202.             while(squirtsToAdd.Count > 0) {
    203.                 Squirt s = squirtsToAdd.Pop();
    204.                 UpdateSquirt(grav, delta, s, ref squirtsToAdd);
    205.                 if(s.Alive) {
    206.                     Squirts.Add(s);
    207.                 }
    208.             }          
    209.         }
    210.        
    211.         if(squirtsToDelete != null) {
    212.             // there are squirts to delete
    213.             foreach(Squirt s in squirtsToDelete)
    214.                 Squirts.Remove(s);
    215.         }
    216.     }
    217.    
    218.     void OnRenderObject() {
    219.         Camera camera = Camera.current;
    220.         Vector3 cameraPlaneX = camera.transform.right;
    221.         Vector3 cameraPlaneY = camera.transform.up;
    222.         Vector3 cameraDir = camera.transform.forward;
    223.    
    224.         Vector3 x = new Vector3(cameraPlaneX.x * halfWidth,
    225.             cameraPlaneX.y * halfWidth,
    226.             cameraPlaneX.z * halfWidth);
    227.         Vector3 y = new Vector3(cameraPlaneY.x * halfWidth,
    228.             cameraPlaneY.y * halfWidth,
    229.             cameraPlaneY.z * halfWidth);
    230.         Vector3 z = cameraDir * halfWidth;
    231.    
    232.         material.SetPass(0);  
    233.         GL.PushMatrix();
    234.         //GL.MultMatrix(gameObject.transform.localToWorldMatrix);
    235.         GL.SetRevertBackfacing(false);
    236.         DrawEnds(ref x, ref y);
    237.         DrawLines(ref x, ref y, ref camera);
    238.         GL.Color(Color.white);
    239.         GL.PopMatrix();
    240.     }
    241.    
    242.     void DrawEnds(ref Vector3 x, ref Vector3 y) {
    243.         ends.SetPass(0);      
    244.        
    245.         GL.Begin(GL.TRIANGLES);
    246.        
    247.         foreach(Squirt s in Squirts) {
    248.             foreach(SquirtParticle p in s.Particles) {
    249.                 // draw ends
    250.                 GL.TexCoord2(1, 0);
    251.                 GL.Vertex(p.Position + x - y);
    252.                 GL.TexCoord2(0, 0);
    253.                 GL.Vertex(p.Position - x - y);
    254.                 GL.TexCoord2(1, 1);
    255.                 GL.Vertex(p.Position + x + y);
    256.                
    257.                 //GL.TexCoord2(1, 1);
    258.                 GL.Vertex(p.Position + x + y);
    259.                 GL.TexCoord2(0, 0);
    260.                 GL.Vertex(p.Position - x - y);
    261.                 GL.TexCoord2(0, 1);
    262.                 GL.Vertex(p.Position - x + y);
    263.             }
    264.         }
    265.         GL.End ();
    266.     }
    267.    
    268.     void DrawLines(ref Vector3 x, ref Vector3 y, ref Camera camera) {
    269.         material.SetPass(0);
    270.         GL.Begin(GL.TRIANGLES);
    271.        
    272.         foreach(Squirt s in Squirts) {
    273.             SquirtParticle previous = null;
    274.             Vector3 startCam = Vector3.zero;
    275.             foreach(SquirtParticle p in s.Particles) {
    276.                 if(previous != null) {
    277.                     // draw line between previous and p
    278.                     Vector3 endCam = camera.WorldToScreenPoint(p.Position);
    279.                     startCam.z = 0;
    280.                     endCam.z = 0;
    281.                    
    282.                     Vector3 a, b, c, d;
    283.                     Vector3 n = Vector3.Cross(startCam, endCam);
    284.                     Vector3 l = Vector3.Cross(n, endCam-startCam);
    285.                     l.Normalize();
    286.                     l = new Vector3(x.x * l.x + y.x * l.y,
    287.                         x.y * l.x + y.y * l.y,
    288.                         x.z * l.x + y.z * l.y);
    289.        
    290.                     a = (previous.Position + l);
    291.                     b = (previous.Position - l);
    292.                     c = (p.Position + l);
    293.                     d = (p.Position - l);
    294.                    
    295.                     // draw line
    296.                     GL.Vertex(a);
    297.                     GL.Vertex(b);
    298.                     GL.Vertex(c);
    299.                
    300.                     GL.Vertex(c);
    301.                     GL.Vertex(b);
    302.                     GL.Vertex(d);
    303.                    
    304.                     startCam = endCam;
    305.                 } else {
    306.                     startCam = camera.WorldToScreenPoint(p.Position);
    307.                 }
    308.                
    309.                 previous = p;
    310.             }
    311.         }
    312.         GL.End ();
    313.     }
    314. }
    315.  
     
  10. FuzzyQuills

    FuzzyQuills

    Joined:
    Jun 8, 2013
    Posts:
    2,871
    @aubergine: AN ELECTRIC SHADER?! Can it do lightning bolts? :)

    @MessiahAndrw: Nice! wish I had the skill to figure that out. and nice water stream!