How difficult would it be to recreate the Quake 1 liquid effect? There's an article here that describes how it's done: https://fdossena.com/?p=quakeFluids/i.md apparently the distance of view doesn't affect how it appears, so the wave size is consistent at all distances. https://fdossena.com/quakeFluids/demo.webm
Code (CSharp): Shader "Unlit/QuakeLiquid" { Properties { _MainTex ("Texture", 2D) = "white" {} _WaveFrequency ("Wave Frequency", Float) = 0.5 _WaveScale ("Wave Scale", Float) = 0.8 _WaveAmplitude ("Wave Amplitude", Float) = 0.15 } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; float _WaveScale, _WaveAmplitude, _WaveFrequency; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { float2 sinTime = (i.uv.yx * _WaveScale + _Time.y * _WaveFrequency) * UNITY_PI; float2 uv = i.uv + float2(sin(sinTime.x), sin(sinTime.y)) * _WaveAmplitude; fixed4 col = tex2D(_MainTex, uv); UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } } }
Wow, that's perfect, thankyou @bgolus ! I was interested about this part: I'm interested, what specifically is supposedly so difficult to replicate with modern shaders?
The author is wrong. The above shader almost exactly replicates Quake’s liquid (though the parameterization is different). It was difficult (actually impossible for many GPUs still being used) when most of the Quake ports to hardware renderers were written, 15 years ago... Today it’s trivial unless you’re trying to run it on 10 year old mobile phones.
I guess it could be argued it’s still difficult to implement Quake’s liquid exactly the same way as Quake did on modern GPUs ... but there’s no need to use the same implementation if the results are identical.
I've been trying the shader as a fullscreen effect but I'm getting some edge distortion/fringing: Does the shader need to overshoot or would it be an issue with blitting?
A quick look at the original Quake software renderer, when underwater it appears they expand the rendered image slightly to account for the distortion. Not necessary when applied on a surface because it presumably has a repeating texture. But for a post process you'd need to scale the screen space UV prior to warping. You probably also want to correct for the aspect ratio in the wave amplitude. Code (csharp): // correct wave amplitude to match the aspect ratio float2 waveAmplitude = float2(_ScreenParams.y / _ScreenParams.x * _WaveAmplitude, _WaveAmplitude); // scale the uvs to account for wave amplitude float2 uv = (i.uv - 0.5) / (1 + abs(waveAmplitude)) + 0.5; uv += float2(sin(sinTime.x), sin(sinTime.y)) * waveAmplitude;
Thanks! I did try making the rendertexture slightly bigger than the screen width and height, but it didn't work. Is changing the screen space UV different to the size of the rendertexture (I'm using graphics.blit)?
The problem is you were distorting the uvs so you were seeing outside the 0.0 to 1.0 range of the texture. Making the render texture a higher resolution doesn't fix anything since the uv range is normalized to the extents of the texture. The idea with the above code is scaling the UV so that before distortion some amount of the render texture is off screen so that after distortion nothing outside the 0.0 to 1.0 range is visible. I also might have the math wrong. Might need to do / (1 + abs(waveAmplitude) * 2.0), or do * (1 - abs(waveAmplitude) * 2.0). Brain isn't totally working today... actually pretty sure it's that last one and not what's above.
Thanks, I tried both modifications and the waves are no longer working with either (although the scene shows okay), am I doing this right? Code (CSharp): Shader "Unlit/QuakeLiquid" { Properties { _MainTex("Texture", 2D) = "white" {} _WaveFrequency("Wave Frequency", Float) = 0.5 _WaveScale("Wave Scale", Float) = 0.8 _WaveAmplitude("Wave Amplitude", Float) = 0.15 } SubShader { Tags { "RenderType" = "Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; float _WaveScale, _WaveAmplitude, _WaveFrequency; v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag(v2f i) : SV_Target { float2 waveAmplitude = float2(_ScreenParams.y / _ScreenParams.x * _WaveAmplitude, _WaveAmplitude); float2 sinTime = (i.uv.yx * _WaveScale + _Time.y * _WaveFrequency) * UNITY_PI; float2 uv = (i.uv - 0.5) * (1 - abs(waveAmplitude) * 2.0) + 0.5; fixed4 col = tex2D(_MainTex, uv); UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } } }
Here's a version of the shader fixed up for use as an image effect. Makes one minor change from the original in that the amplitude is scaled by the wave scale so you can more easily modify the wave scale without the distortion going crazy or fading too much. Also corrects the UV used for the sine wave phase offset so that you get that "wiggle" look from the original instead of just looking like scrolling sine waves. Code (CSharp): Shader "Hidden/QuakeUnderWater" { Properties { _MainTex ("Texture", 2D) = "white" {} _WaveFrequency ("Wave Frequency", Float) = 0.5 _WaveScale ("Wave Scale", Float) = 0.8 _WaveAmplitude ("Wave Amplitude", Range(0,0.33)) = 0.15 } SubShader { // No culling or depth Cull Off ZWrite Off ZTest Always Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } sampler2D _MainTex; float _WaveScale, _WaveAmplitude, _WaveFrequency; fixed4 frag (v2f i) : SV_Target { float aspect = _ScreenParams.y / _ScreenParams.x; float2 waveAmplitude = float2(aspect * _WaveAmplitude, _WaveAmplitude) / _WaveScale; float2 uv = (i.uv - 0.5) * (1.0 - abs(waveAmplitude) * 2.0) + 0.5; float2 sinTime = (float2((uv.y - 0.5) * aspect, uv.x - 0.5) * _WaveScale + _Time.y * _WaveFrequency) * UNITY_PI; uv += float2(sin(sinTime.x), sin(sinTime.y)) * waveAmplitude; fixed4 col = tex2D(_MainTex, uv); return col; } ENDCG } } }
Thank you SO much, this works perfectly! As a side note, would you recommend adding a color tint to the shader or doing it via a canvas? If the shader was given an RGBA value, the alpha could presumably be used for the amount of tint and the color for the hue, would this is be possible in the same shader?
Well, I'm kinda proud I did this all by myself and nothing broke, but I don't think the alphas working properly Code (CSharp): Shader "Hidden/QuakeUnderWater" { Properties { _MainTex("Texture", 2D) = "white" {} _WaveFrequency("Wave Frequency", Float) = 0.5 _WaveScale("Wave Scale", Float) = 0.8 _WaveAmplitude("Wave Amplitude", Range(0,0.33)) = 0.15 _Color("Color Tint", Color) = (1,1,1,1) } SubShader { // No culling or depth Cull Off ZWrite Off ZTest Always Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } sampler2D _MainTex; float _WaveScale, _WaveAmplitude, _WaveFrequency; float4 _Color; fixed4 frag(v2f i) : SV_Target { float aspect = _ScreenParams.y / _ScreenParams.x; float2 waveAmplitude = float2(aspect * _WaveAmplitude, _WaveAmplitude) / _WaveScale; float2 uv = (i.uv - 0.5) * (1.0 - abs(waveAmplitude) * 2.0) + 0.5; float2 sinTime = (float2((uv.y - 0.5) * aspect, uv.x - 0.5) * _WaveScale + _Time.y * _WaveFrequency) * UNITY_PI; uv += float2(sin(sinTime.x), sin(sinTime.y)) * waveAmplitude; fixed4 col = tex2D(_MainTex, uv) * _Color; return col; } ENDCG } } }
Sorry, I tried to interpret your shader with the additions of additional color and lighting effects. But alas, nothing works, could you please see what the error is. Code (CSharp): Shader "Custom/Water" { //Based on bgolus QuakeLiquid shader; Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _Color("Color", Color) = (1,1,1,1) _Amplitude("Amplitude", Float) = 0.12 _Scale("Scale", Float) = 0.7 _Frequency("Frequency", Float) = 0.2 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM // Upgrade NOTE: excluded shader from DX11; has structs without semantics (struct appdata members normal) #pragma exclude_renderers d3d11 #pragma surface surf Lambert #pragma vertex vert #include "UnityCG.cginc" sampler2D _MainTex; float4 _MainTex_ST; struct Input { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal; }; fixed4 _Color; float _Amplitude; float _Scale; float _Frequency; Input vert(inout appdata v) { Input o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } void surf (Input IN, inout SurfaceOutput o) { float2 DeltaTime = (IN.uv.yx * _Scale + _Time.y * _Frequency) * UNITY_PI; float2 uv = IN.uv + float2(sin(DeltaTime.x), sin(DeltaTime.y)) * _Amplitude; // Albedo comes from a texture tinted by color fixed4 c = tex2D (_MainTex, uv) * _Color; o.Albedo = c.rgb; } ENDCG } FallBack "Diffuse" }