Hey. I came across this post: https://gamedev.stackexchange.com/q...-render-2d-top-down-tiled-directed-water-flow And the shader was presented in the answer. I tried to reproduce it, but did not get the effect that its author received. My try: Code (CSharp): Shader "2DWater" { Properties { _MainTexture("MainTexture", 2D) = "white" {} _Flow ("Flow", 2D) = "white" {} _Wave ("Wave", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment Frag #include "UnityCG.cginc" // Colour texture / atlas for my tileset. sampler2D _MainTexture; // Flowmap texture. sampler2D _Flow; // Wave surface texture. sampler2D _Wave; // Tiling of the wave pattern texture. float _WaveDensity = 0.5; // Scrolling speed for the wave flow. float _WaveSpeed = 5.0; // Scaling from my world size of 8x8 tiles // to the 0...1 float2 inverseFlowmapSize = (float2)(1.0f/8.0f); struct v2f { // Projected position of tile vertex. float4 vertex : SV_POSITION; // Tint colour (not used in this effect, but handy to have. fixed4 color : COLOR; // UV coordinates of the tile in the tile atlas. float2 texcoord : TEXCOORD0; // Worldspace coordinates, used to look up into the flow map. float2 flowPos : TEXCOORD1; }; v2f vert(appdata_full IN) { v2f OUT; // Save xy world position into flow UV channel. OUT.flowPos = mul(unity_ObjectToWorld, IN.vertex).xy; // Conventional projection & pass-throughs... OUT.vertex = UnityObjectToClipPos(IN.vertex); OUT.texcoord = IN.texcoord; OUT.color = IN.color; return OUT; } float2 WaveAmount(float2 uv, float2 sampleSite) { // Sample from the flow map texture without any mipmapping/filtering. // Convert to a vector in the -1...1 range. float2 flowVector = tex2Dgrad(_Flow, sampleSite * inverseFlowmapSize, 0, 0).xy * 2.0f - 1.0f; // Optionally, you can skip this step, and actually encode // a flow speed into the flow map texture too. // I just enforce a 1.0 length for consistency without getting fussy. flowVector = normalize(flowVector); // I displace the UVs a little for each sample, so that adjacent // tiles flowing the same direction don't repeat exactly. float2 waveUV = uv * _WaveDensity + sin((3.3f * sampleSite.xy + sampleSite.yx) * 1.0f); // Subtract the flow direction scaled by time // to make the wave pattern scroll this way. waveUV -= flowVector * _Time * _WaveSpeed; // I use tex2DGrad here to avoid mipping down // undesireably near tile boundaries. float wave = tex2Dgrad(_Wave, waveUV, ddx(uv) * _WaveDensity, ddy(uv) * _WaveDensity); // Calculate the squared distance of this flowmap pixel center // from our drawn position, and use it to fade the flow // influence smoothly toward 0 as we get further away. float2 offset = uv - sampleSite; float fade = 1.0 - saturate(dot(offset, offset)); return float2(wave * fade, fade); } fixed4 Frag(v2f IN) : COLOR { // Sample the tilemap texture. fixed4 c = tex2D(_MainTexture, IN.texcoord); // In my case, I just select the water areas based on // how blue they are. A more robust method would be // to encode this into an alpha mask or similar. float waveBlend = saturate(3.0f * (c.b - 0.4f)); // Skip the water effect if we're not in water. if(waveBlend == 0.0f) return c * IN.color; float2 flowUV = IN.flowPos; // Clamp to the bottom-left flowmap pixel // that influences this location. float2 bottomLeft = floor(flowUV); // Sum up the wave contributions from the four // closest flow map pixels. float2 wave = WaveAmount(flowUV, bottomLeft); wave += WaveAmount(flowUV, bottomLeft + float2(1, 0)); wave += WaveAmount(flowUV, bottomLeft + float2(1, 1)); wave += WaveAmount(flowUV, bottomLeft + float2(0, 1)); // We store total influence in the y channel, // so we can divide it out for a weighted average. wave.x /= wave.y; // Here I tint the "low" parts a darker blue. c = lerp(c, c*c + float4(0, 0, 0.05, 0), waveBlend * 0.5f * saturate(1.2f - 4.0f * wave.x)); // Then brighten the peaks. c += waveBlend * saturate((wave.x - 0.4f) * 20.0f) * 0.1f; // And finally return the tinted colour. return c * IN.color; } ENDCG } } } Could you tell me what my mistake is?
It would help if you would describe what IS working and what is NOT working. I see an animated image for the effect you want, but your example you're using different input images, and your image isn't animated. So ... without mindreading, I ahve no idea what you're saying is wrong?
This shader is not mine. As far as I understand, his task is to make the static image of water to move, but for some reason this does not work as it should. I also understand that the direction of the water flow is set using the Flow texture of 8x8 pixels (field float2 inverseFlowmapSize = (float2) (1.0f / 8.0f); also points to this), but the movement still doesn’t happen.
No movement at all? Are you testing in game or in editor? By default, the editor won't animate shaders
Try deleting lines 108 and 109. But, really ... until you start describing exactly what's happening vs what you expect, there's not much to help with.
If I could describe what is happening in it line by line, I think I would not ask the forum for help))
I'm not asking you to explain the entire shader, I'm asking you to describe what you expected to see on screen vs what you actually see. This is the first step in fixing a problem - until you do that, you haven't started. Also, debugging simple shaders like this one is very easy, you just need to do small edits and see what happens. Unlike most shaders, this is very simple code, 100% of it can be learned from scratch in a couple of hours or less. I would strongly recommend you do some basic tutorials in scripting, so that you feel confident editing the shader line by line to see what happens. This will save you time right now, and save you lots of time in future. (You don't need to understand every line of code, but you need to know the basics of how lines of code are executed one after the other, and roughly what they do.)
well the issue is that it doesn't do anything, water doesn't scroll. it has _Time there so something should animate, but it also uses worldspace XY coordinates, so could be problem with the mesh location, scale of your mesh, or how the UV maps should be in the mesh?
I am at the stage of studying shaders. I read the Cg tutorial and rummaged through the documentation for ShaderLab. But I still don’t understand some points. For example: wave += WaveAmount(flowUV, bottomLeft + float2(1, 0)); wouldn't bottomLeft always be 0.0? That is, the lower left corner of the square? Why is the addition operation bottomLeft + float2 (1, 0) performed in the parameters, if you could just specify float2 (1, 0) and have the same effect? float2 flowVector = tex2Dgrad(_Flow, sampleSite * inverseFlowmapSize, 0, 0).xy * 2.0f - 1.0f; this thing I can’t understand at all
bottomLeft is calculated here: Code (CSharp): // Clamp to the bottom-left flowmap pixel // that influences this location. float2 bottomLeft = floor(flowUV); UV's don't have to go 0...1, they can go 2 ... 3 ... 4 ... etc. So, one common approach in shaders is to UV-map across an area with - say - UV 0...10, and then you can use floor(UV) to get a repeating 0..1, 0..1, 0..1 ... but also get the full 0...1, ...2, ...3, ...4 at the same time (for different parts of your shader).
as far as I understand, the movement should occur on the _Wave texture (the last in the inspector window on the screenshot) using tex2Dgrad(_Wave, waveUV, ddx (uv) * _WaveDensity, ddy (uv) * _WaveDensity); where waveUV coordinates that are obtained using flowVector * _Time * _WaveSpeed; but the problem is that I don’t quite understand the line float2 flowVector = tex2Dgrad (_Flow, sampleSite * inverseFlowmapSize, 0, 0) .xy * 2.0f - 1.0f; to imagine further calculations
My approach is usually to delete big parts of the shader, make a simpler version, and build it up in stages. That way, eventually you find the part that isn't doing what you expected it to do, and you can fix it. e.g. on line 122, you could immediately return the value of wave - that will show you if the function that calculates "wave" is even working at all. Something like: Code (CSharp): float4 result = float4(0,0,0,1); // black, fully opaque result.rg = wave; // assign the wave to the R and G channels in result return result;
...Or at Line 115, do the same with flowUV, and then with bottomLeft: try returning each of them, see if they look like they have the values you'd expect if they were correct.
As per the comment on the line above, the author is using it to disable mipmapping (vs using a normal tex2D). I'm guessing: so that the shader samples the blocky, sharp-edged flowmap, rather than automatically blurring it by using mipmaps. It would be worth returning this value immediately (see above), and then compare that to returning: tex2D( _Flow, sampleSite * inverseFlowmapSize) ...you should be able to clearly see the difference/purpose that way
I read theoretical material about mipmap, and that tex2D can take a lower quality texture, since the object can be far from the camera. sampleSite * inverseFlowmapSize I don’t understand this calculation, since it seems to me that every time WaveAmount(flowUV, bottomLeft); will have bottomLeft as 0,0
See: https://forum.unity.com/threads/2d-water-flow-shader.784802/#post-5234033 BUT ... even better ... try return'ing that variable as the R and G channels, and see what it is across your whole mesh - you can see for yourself. If it's always 0,0 it'll be black everywhere (I don't think it will be though, unless that's part of the bug)
I tried to do so, and was very surprised because tex2D(_Flow, bottomLeft + float2 (1.0)) returns but I figured it would be only
Try just returning bottomLeft itself - so you can see what actual values it contains (if you return tex2D( ..., bottomLeft), then you're obscuring the value by mixing it with your texture, so you still don't know what value it has).
Thank you for the method by which I can now output values to an object, and then just use the eyedropper to see what happens on each pixel, this is very cool! So, I understand more now what flowUV is. The values that take real world coordinates inside, and if you move the object, then a palette from black to red (along the x axis) and from black to green (along the y axis) will be visible, provided that the swizzle will be blackColor.rg = flowUV.xy; bollomLeft there will always be 0.0 in WaveAmount(flowUV, bottomLeft); since the nearest integer value for all values from 0 to 1 will be 0. Therefore, the expression tex2Dgrad(_Flow, bottomLeft * inverseFlowmapSize, 0, 0).xy will take the lower left pixel in the _Flow texture, whose values are r == ~0.77, and g == ~0.95 (I indicate approximately, since I do not remember the exact values). there are also additional actions * 2.0f - 1.0f; which turn ~ 0.77 and ~ 0.95 into ~ 0.55 and ~ 0.90 respectively and the result is always the same for all situations: Code (CSharp): WaveAmount(flowUV, bottomLeft); or WaveAmount(flowUV, bottomLeft + float2(1, 0)); or WaveAmount(flowUV, bottomLeft + float2(1, 1)); or WaveAmount(flowUV, bottomLeft + float2(0, 1)); always returns the same values ~ 0.55 and ~ 0.90 I understand the normalization stage, I see no reason to write something about it But then there is some kind of calculation float2 waveUV = uv * _WaveDensity + sin((3.3f * sampleSite.xy + sampleSite.yx) * 1.0f); that I don’t understand and which displays this: then I did all 3 offsets that were: Code (CSharp): WaveAmount(flowUV, bottomLeft + float2(1, 0)); or WaveAmount(flowUV, bottomLeft + float2(1, 1)); or WaveAmount(flowUV, bottomLeft + float2(0, 1)); and in all these situations there was a shift in the corresponding directions (for example, with + float2(1, 1), the lower left black square turned yellow) and here is the line that caused me a lot of questions: waveUV -= flowVector * _Time * _WaveSpeed; as far as I understand, it should make the colors in the grid above change with time, but this does not happen BUT, if I delete * _WaveSpeed part, then the grid starts to gradually change its colors in some places to black and at that moment my brains began to boil and I ceased to understand why this was happening ...
Looking purely at this line ... we can see it is taking the UV, and subtracting a value from it (which means it subtracts from U and V simultaneously). We know - by definition - that has the effect that anything using that UV will seem to "flow" up and to the right (because it's sampling a bit lower and to the left on each timestep ... more and more over time). NOW ... if that is "gradually chang[ing] ... to black", that means it's eventually sampling a UV location that returns 0, i.e. the color black. ...which suggests that it's sampling off the edge of the texture, and off the edge of the texture, it's returning 0. SO .... check your texture import settings! What clamp mode are you using? Maybe you need to set your texture to clamp-mode = repeat?
Yes you are right! I thought that the default texture in the "repeat" mode, but it is not. This is what happened when I set the _Wave texture to "repeat" mode: And two questions remain: 1) Why does it work without "_WaveSpeed"? 2) Why is the texture not moving?
Reading the code, _WaveSpeed appears to be a fixed value: the number 5. i.e. it merely increases/decreases the overall speed. If you remove it, that's the same as having a _WaveSpeed of 1 (because you were multiplying by _WaveSpeed). I don't know what else is wrong now, but ... keep debugging
I also thought that this is just an acceleration of movement, but this is not so, since it simply does not work with this value. On the example of the gif, which I threw off at the beginning, it is clear that the texture is moving, but I don’t understand which part of the code moves this texture, it doesn’t move for me.
Maybe someone else can help me in solving this problem please? The final result that I managed to get is still not like what was on the GIF animation
A couple of things the post you're working off of is assuming, and some bugs. The flow texture is supposed to be an 8x8 texture with sRGB turned off. The example image they're showing is just that 8x8 texture zoomed in a bunch, you'd need to scale that down to an 8x8 for it to work properly. You could use a texture of any size if you change from using the hardcoded inverseFlowmapSize to adding float4 _Flow_TexelSize; to your shader and using _Flow_TexelSize.xy in place of it. They're assuming each "tile" is 1x1 in world space. If your tile scale is different, it won't work. The _Time in line line waveUV -= flowVector * _Time * _WaveSpeed; should be _Time.x or _Time.y, otherwise you'll get the waves panning in different speeds on the x and y. Also, you can get away with not having the _WaveSpeed on that line because what's actually causing things to move is that time variable itself. _WaveSpeed is just a multiplier on it. If you don't have _WaveSpeed as a material property, and aren't setting it via script, it might default to 0.0 which means nothing moves.
I am very glad that you were able to answer this post! Thanks you. As for the 8x8 flow texture, in my comments #4 I noticed this point, but I really did not know that sRGB should be turned off. Regarding the variable _WaveSpeed, in the end I brought it to the inspector to speed up or slow down the movement. I replaced the rigid snapping to the size of the 8x8 texture, as you said, replacing the variable inverseFlowmapSize on float4 _Flow_TexelSize. The scale of my tile is 1 for x and 1 for y, but the scale still does not affect it. In the comments of the shader, it was indicated that it only affects the blue color, that is, I can use any texture that has a blue color as water. And if I understood everything correctly, then the shader works in world coordinates, and in fact it doesn’t matter at what point in space my tiles are, it replaces it everywhere. The result is still the same, the movement occurs, but there is no flow effect. Perhaps it should not be in the shader, and this is done with the help of any additional manipulations? Here are shader code: Code (CSharp): Shader "2DWater" { Properties { _MainTexture("MainTexture", 2D) = "white" {} _Flow ("Flow", 2D) = "white" {} _Wave ("Wave", 2D) = "white" {} } SubShader { Pass { CGPROGRAM #pragma target 3.0 #pragma vertex vert #pragma fragment Frag #include "UnityCG.cginc" // Colour texture / atlas for my tileset. sampler2D _MainTexture; // Flowmap texture. sampler2D _Flow; // Wave surface texture. sampler2D _Wave; float2 flowVector1 = float2(1, 1); // Tiling of the wave pattern texture. float _WaveDensity = 0.1; // Scrolling speed for the wave flow. float _WaveSpeed = 5.0; float4 _Flow_TexelSize; struct v2f { // Projected position of tile vertex. float4 vertex : SV_POSITION; // Tint colour (not used in this effect, but handy to have. fixed4 color : COLOR; // UV coordinates of the tile in the tile atlas. float2 texcoord : TEXCOORD0; // Worldspace coordinates, used to look up into the flow map. float2 flowPos : TEXCOORD1; }; v2f vert(appdata_full IN) { v2f OUT; // Save xy world position into flow UV channel. OUT.flowPos = mul(unity_ObjectToWorld, IN.vertex).xy; // Conventional projection & pass-throughs... OUT.vertex = UnityObjectToClipPos(IN.vertex); OUT.texcoord = IN.texcoord; OUT.color = IN.color; return OUT; } float2 WaveAmount(float2 uv, float2 sampleSite) { // Sample from the flow map texture without any mipmapping/filtering. // Convert to a vector in the -1...1 range. float2 flowVector = tex2Dgrad(_Flow, sampleSite * _Flow_TexelSize, 0, 0).xy * 2.0f - 1.0f; // Optionally, you can skip this step, and actually encode // a flow speed into the flow map texture too. // I just enforce a 1.0 length for consistency without getting fussy. flowVector = normalize(flowVector); // I displace the UVs a little for each sample, so that adjacent // tiles flowing the same direction don't repeat exactly. float2 waveUV = uv * _WaveDensity + sin((3.3f * sampleSite.xy + sampleSite.yx) * 1.0f); // Subtract the flow direction scaled by time // to make the wave pattern scroll this way. waveUV -= flowVector * _Time * 0.2f; // I use tex2DGrad here to avoid mipping down // undesireably near tile boundaries. float wave = tex2Dgrad(_Wave, waveUV, ddx(uv) * _WaveDensity, ddy(uv) * _WaveDensity); // Calculate the squared distance of this flowmap pixel center // from our drawn position, and use it to fade the flow // influence smoothly toward 0 as we get further away. float2 offset = uv - sampleSite; float fade = 1.0 - saturate(dot(offset, offset)); return float2(wave * fade, fade); } fixed4 Frag(v2f IN) : COLOR { // Sample the tilemap texture. fixed4 c = tex2D(_MainTexture, IN.texcoord); // In my case, I just select the water areas based on // how blue they are. A more robust method would be // to encode this into an alpha mask or similar. float waveBlend = saturate(3.0f * (c.b - 0.4f)); // Skip the water effect if we're not in water. if(waveBlend == 0.0f) return c * IN.color; float2 flowUV = IN.flowPos; // Clamp to the bottom-left flowmap pixel // that influences this location. float2 bottomLeft = floor(flowUV); // Sum up the wave contributions from the four // closest flow map pixels. float2 wave = WaveAmount(flowUV, bottomLeft); wave += WaveAmount(flowUV, bottomLeft + float2(1, 0)); wave += WaveAmount(flowUV, bottomLeft + float2(1, 1)); wave += WaveAmount(flowUV, bottomLeft + float2(0, 1)); // We store total influence in the y channel, // so we can divide it out for a weighted average. wave.x /= wave.y; // Here I tint the "low" parts a darker blue. c = lerp(c, c*c + float4(0, 0, 0.05, 0), waveBlend * 0.5f * saturate(1.2f - 4.0f * wave.x)); // Then brighten the peaks. c += waveBlend * saturate((wave.x - 0.4f) * 20.0f) * 0.1f; // And finally return the tinted colour. return c * IN.color; } ENDCG } } }