1+ // Water Shader with Interactive Controls
2+ // Port of "Water" shader from https://www.shadertoy.com/view/Ms2SD1
3+ // Original by afl_ext (MIT License)
4+ // Modifications: Toggleable sun, speed/zoom controls, hopefully informative? comments
5+
6+ #import bevy_sprite :: mesh2d_view_bindings :: globals
7+ #import shadplay :: shader_utils :: common :: {NEG_HALF_PI , shader_toy_default , rotate2D , PI }
8+ #import bevy_render :: view :: View
9+ #import bevy_sprite :: mesh2d_vertex_output :: VertexOutput
10+
11+ @group (0 ) @binding (0 ) var <uniform > view : View ;
12+
13+ // CONTROLS
14+ const DRAG_MULT : f32 = 0 .38 ; // Hydrodynamic drag coefficient
15+ const WATER_DEPTH : f32 = 1 .0 ; // Vertical scale of water volume
16+ const CAMERA_HEIGHT : f32 = 1 .5 ; // Camera elevation above water
17+ const ITERATIONS_RAYMARCH : i32 = 12 ; // Balance quality/performance for raymarching
18+ const ITERATIONS_NORMAL : i32 = 36 ; // Higher quality for normal calculations
19+ const SPEED : f32 = 0 .60 ; // SLOW MOTION
20+ const SUN_ON : f32 = 1 .0 ; // 0.0 will disable the sun, which runis the lightning, but is still cool.
21+ const ZOOM : f32 = 0 .85 ; // I dunno how to solve the fisheye problem yet this creates..
22+
23+ /// Creates a 3x3 rotation matrix using axis-angle representation
24+ /// axis Normalized rotation axis
25+ /// angle Rotation angle in radians
26+ /// 3x3 rotation matrix
27+ fn create_rotation_matrix_axis_angle (axis : vec3f , angle : f32 ) -> mat3x3f {
28+ let s = sin (angle );
29+ let c = cos (angle );
30+ let oc = 1 .0 - c ;
31+
32+ return mat3x3f (
33+ oc * axis . x * axis . x + c , oc * axis . x * axis . y - axis . z * s , oc * axis . z * axis . x + axis . y * s ,
34+ oc * axis . x * axis . y + axis . z * s , oc * axis . y * axis . y + c , oc * axis . y * axis . z - axis . x * s ,
35+ oc * axis . z * axis . x - axis . y * s , oc * axis . y * axis . z + axis . x * s , oc * axis . z * axis . z + c
36+ );
37+ }
38+
39+ /// Calculates wave height and horizontal derivative at given position
40+ /// position 2D surface position
41+ /// direction Wave propagation direction
42+ /// frequency Spatial frequency of wave
43+ /// timeshift Time-based phase shift
44+ /// vec2(wave_height, x_derivative)
45+ fn wavedx (position : vec2f , direction : vec2f , frequency : f32 , timeshift : f32 ) -> vec2f {
46+ let phase = dot (direction , position ) * frequency + timeshift ;
47+ let wave = exp (sin (phase ) - 1 .0 ); // Exponential sine wave for sharp crests
48+ let dx = wave * cos (phase ); // Derivative for horizontal displacement
49+ return vec2f (wave , - dx );
50+ }
51+
52+ /// Generates multi-octave wave pattern using fractal summation
53+ /// i_position 2D surface position
54+ /// iterations Number of octaves to accumulate
55+ /// time Current time adjusted by speed control
56+ /// Heightmap value at position
57+ fn get_waves (i_position : vec2f , iterations : i32 , time : f32 ) -> f32 {
58+ let wave_phase_shift = length (i_position ) * 0 .1 ; // Position-based phase variation
59+ var iter : f32 = 0 .0 ; // Wave seed for directional variation
60+ var frequency : f32 = 1 .0 ; // Frequency multiplier per octave
61+ var time_multiplier : f32 = 2 .0 ; // Time scaling per octave
62+ var weight : f32 = 1 .0 ; // Energy preservation weight
63+ var sum_values : f32 = 0 .0 ; // Accumulated wave heights
64+ var sum_weights : f32 = 0 .0 ; // Normalization factor
65+
66+ for (var i : i32 = 0 ; i < iterations ; i += 1 ) {
67+ // Generate pseudo-random wave direction from seed
68+ let dir = vec2f (sin (iter ), cos (iter ));
69+
70+ // Calculate wave height and displacement
71+ let wave_data = wavedx (i_position , dir , frequency , time * time_multiplier + wave_phase_shift );
72+
73+ // Advect position based on wave displacement
74+ let new_pos = i_position + dir * wave_data . y * weight * DRAG_MULT ;
75+
76+ // Accumulate weighted results
77+ sum_values += wave_data . x * weight ;
78+ sum_weights += weight ;
79+
80+ // Prepare next octave with decreased influence
81+ weight = mix (weight , 0 .0 , 0 .2 ); // Energy preservation falloff
82+ frequency *= 1 .18 ; // Increase frequency geometrically
83+ time_multiplier *= 1 .07 ; // Accelerate time variation
84+ iter += 1232 .399963 ; // Arbitrary seed increment
85+ }
86+
87+ return sum_values / sum_weights ; // Normalize accumulated values
88+ }
89+
90+ /// Raymarching through water volume to find surface intersection
91+ /// camera_pos Camera position in world space
92+ /// start Ray start position (water surface)
93+ /// end Ray end position (water floor)
94+ /// depth Vertical depth of water volume
95+ /// time Time adjusted by speed control
96+ /// Distance from camera to intersection point
97+ fn raymarch_water (camera_pos : vec3f , start : vec3f , end : vec3f , depth : f32 , time : f32 ) -> f32 {
98+ var pos : vec3f = start ;
99+ let dir = normalize (end - start );
100+
101+ // Adaptive step size based on water depth
102+ for (var i : i32 = 0 ; i < 64 ; i += 1 ) {
103+ let wave_height = get_waves (pos . xz , ITERATIONS_RAYMARCH , time ) * depth - depth ;
104+
105+ // Check intersection with wave surface
106+ if (abs (wave_height - pos . y ) < 0 .01 ) {
107+ return distance (pos , camera_pos );
108+ }
109+
110+ // March forward proportionally to height difference
111+ pos += dir * (pos . y - wave_height );
112+ }
113+ return distance (start , camera_pos ); // Fallback for no intersection
114+ }
115+
116+ /// Calculates surface normal using finite differences
117+ /// pos 2D surface position
118+ /// epsilon Sampling distance for normal calculation
119+ /// depth Water depth for height scaling
120+ /// time Time adjusted by speed control
121+ /// Normalized surface normal vector
122+ fn calculate_normal (pos : vec2f , epsilon : f32 , depth : f32 , time : f32 ) -> vec3f {
123+ let dx = vec2f (epsilon , 0 .0 );
124+ let dz = vec2f (0 .0 , epsilon );
125+
126+ // Central sample
127+ let h_center = get_waves (pos , ITERATIONS_NORMAL , time ) * depth ;
128+
129+ // X-axis neighbors
130+ let h_left = get_waves (pos - dx , ITERATIONS_NORMAL , time ) * depth ;
131+ let h_right = get_waves (pos + dx , ITERATIONS_NORMAL , time ) * depth ;
132+
133+ // Z-axis neighbors
134+ let h_down = get_waves (pos - dz , ITERATIONS_NORMAL , time ) * depth ;
135+ let h_up = get_waves (pos + dz , ITERATIONS_NORMAL , time ) * depth ;
136+
137+ // Finite difference approximation
138+ return normalize (vec3f (h_left - h_right , 2 .0 * epsilon , h_down - h_up ));
139+ }
140+
141+ /// Generates view ray based on fragment coordinates and zoom
142+ /// frag_coord Fragment position in screen space
143+ /// zoom Camera zoom level (1.0 = default)
144+ /// Normalized view direction vector
145+ fn get_ray (frag_coord : vec2f , zoom : f32 ) -> vec3f {
146+ let viewport = view . viewport . zw ;
147+ let uv = vec2f (
148+ frag_coord . x / viewport . x ,
149+ 1 .0 - (frag_coord . y / viewport . y ) // Flip Y for Bevy coordinate system
150+ );
151+
152+ // Convert to normalized device coordinates
153+ let clip = (uv * 2 .0 - 1 .0 ) * vec2f (viewport . x / viewport . y , 1 .0 );
154+
155+ // Adjust FOV with zoom (larger zoom = narrower FOV)
156+ return normalize (vec3f (clip . x , clip . y , 1 .5 / zoom ));
157+ }
158+
159+ /// Computes sun direction with optional animation
160+ /// time Time adjusted by speed control
161+ /// enabled Sun toggle state
162+ /// Normalized sun direction vector (zero vector when disabled)
163+ fn get_sun_direction (time : f32 , enabled : f32 ) -> vec3f {
164+ // Base direction with vertical oscillation
165+ let animated_y = 0 .5 + sin (time * 0 .2 + 2 .6 ) * 0 .45 ;
166+ let raw_dir = vec3f (- 0 .07735 , animated_y , 0 .57735 );
167+
168+ // Apply toggle and normalize
169+ return normalize (raw_dir ) * step (0 .5 , enabled );
170+ }
171+
172+ /// Approximates atmospheric scattering with physically-inspired terms
173+ /// ray_dir View direction vector (normalized)
174+ /// sun_dir Sun direction vector (normalized)
175+ /// RGB atmospheric color
176+ fn extra_cheap_atmosphere (ray_dir : vec3f , sun_dir : vec3f ) -> vec3f {
177+ // Horizon darkening effect
178+ let inv_ray_y = 1 .0 / (ray_dir . y * 1 .0 + 0 .1 );
179+
180+ // Sun altitude effect
181+ let sun_altitude = 1 .0 / (sun_dir . y * 11 .0 + 1 .0 );
182+ let sun_dot = dot (sun_dir , ray_dir );
183+
184+ // Mie scattering approximation
185+ let mie_scattering = pow (max (sun_dot , 0 .0 ), 8 .0 ) * inv_ray_y * 0 .2 ;
186+
187+ // Base sky color affected by sun position
188+ let base_sky = mix (
189+ vec3f (1 .0 ),
190+ max (vec3f (0 .0 ), vec3f (1 .0 ) - vec3f (5 .5 ,13 .0 ,22 .4 )/ 22 .4 ),
191+ sun_altitude
192+ );
193+
194+ // Blue sky color with horizon effect
195+ let blue = vec3f (5 .5 ,13 .0 ,22 .4 )/ 22 .4 * base_sky ;
196+ let horizon = max (
197+ vec3f (0 .0 ),
198+ blue - vec3f (5 .5 ,13 .0 ,22 .4 )*0 .002 * (inv_ray_y - 6 .0 *sun_dir . y *sun_dir . y )
199+ );
200+
201+ // Final atmospheric color with view angle effects
202+ return horizon * inv_ray_y * (0 .24 + pow (abs (sun_dot ), 2 .0 )*0 .24 ) * (1 .0 + pow (1 .0 - ray_dir . y , 3 .0 ));
203+ }
204+
205+ /// Calculates sun disk intensity
206+ /// dir View direction vector
207+ /// sun_dir Sun direction vector
208+ /// Sun brightness scalar
209+ fn get_sun (dir : vec3f , sun_dir : vec3f ) -> f32 {
210+ return pow (max (0 .0 , dot (dir , sun_dir )), 720 .0 ) * 210 .0 ;
211+ }
212+
213+ /// ACES filmic tone mapping operator
214+ /// Reference: https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
215+ /// color Input linear HDR color
216+ /// Tonemapped color in sRGB space
217+ fn aces_tonemap (color : vec3f ) -> vec3f {
218+ // ACES input matrix (RGB to LMS)
219+ let M1 = mat3x3f (
220+ 0 .59719 , 0 .07600 , 0 .02840 ,
221+ 0 .35458 , 0 .90834 , 0 .13383 ,
222+ 0 .04823 , 0 .01566 , 0 .83777
223+ );
224+
225+ // ACES output matrix (LMS to RGB)
226+ let M2 = mat3x3f (
227+ 1 .60475 , - 0 .10208 , - 0 .00327 ,
228+ - 0 .53108 , 1 .10813 , - 0 .07276 ,
229+ - 0 .07367 , - 0 .00605 , 1 .07602
230+ );
231+
232+ // Apply input matrix transform
233+ let v = M1 * color ;
234+
235+ // Apply tone curve
236+ let a = v * (v + 0 .0245786 ) - 0 .000090537 ;
237+ let b = v * (0 .983729 * v + 0 .4329510 ) + 0 .238081 ;
238+ let tone = a / b ;
239+
240+ // Apply output matrix and clamp
241+ let x = M2 * tone ;
242+ return pow (clamp (x , vec3f (0 .0 ), vec3f (1 .0 )), vec3f (1 .0 / 2 .2 ));
243+ }
244+ /// Computes the intersection distance between a ray and a plane
245+ /// ray_origin Origin point of the ray
246+ /// ray_dir Normalized direction vector of the ray
247+ /// plane_point Any point on the plane
248+ /// plane_normal Normalized normal vector of the plane
249+ /// Distance along ray to intersection (negative if behind origin)
250+ fn intersect_plane (ray_origin : vec3f , ray_dir : vec3f , plane_point : vec3f , plane_normal : vec3f ) -> f32 {
251+ // Calculate denominator (cosine of angle between ray and plane normal)
252+ let denom = dot (plane_normal , ray_dir );
253+
254+ // Avoid division by zero (ray parallel to plane)
255+ if (abs (denom ) > 1e-6 ) {
256+ // Calculate signed distance from ray origin to plane
257+ let t = dot (plane_point - ray_origin , plane_normal ) / denom ;
258+ return t ;
259+ }
260+
261+ // Return large negative value if no intersection
262+ return - 1e6 ;
263+ }
264+
265+ @fragment
266+ fn fragment (input : VertexOutput ) -> @location (0 ) vec4f {
267+ // Adjust time with speed control
268+ let adjusted_time = globals . time *SPEED ;
269+
270+ // Generate view ray with zoom control
271+ let ray = get_ray (input . position . xy , ZOOM );
272+
273+ // Early exit for sky rendering
274+ if (ray . y >= 0 .0 ) {
275+ let sun_dir = get_sun_direction (adjusted_time , SUN_ON );
276+ let atmos = extra_cheap_atmosphere (ray , sun_dir ) * 0 .5 ;
277+ let sun = get_sun (ray , sun_dir );
278+ return vec4f (aces_tonemap ((atmos + sun ) * 2 .0 ), 1 .0 );
279+ }
280+
281+ // Water plane definitions
282+ let water_top = vec3f (0 .0 , 0 .0 , 0 .0 );
283+ let water_bottom = vec3f (0 .0 , - WATER_DEPTH , 0 .0 );
284+
285+ // Animated camera position
286+ let origin = vec3f (
287+ adjusted_time * 0 .2 , // Horizontal movement over time
288+ CAMERA_HEIGHT ,
289+ 1 .0 // Z-position controlled by zoom in get_ray
290+ );
291+
292+ // Calculate water plane intersections
293+ let t_top = intersect_plane (origin , ray , water_top , vec3f (0 .0 ,1 .0 ,0 .0 ));
294+ let t_bottom = intersect_plane (origin , ray , water_bottom , vec3f (0 .0 ,1 .0 ,0 .0 ));
295+ let hit_top = origin + ray * t_top ;
296+ let hit_bottom = origin + ray * t_bottom ;
297+
298+ // Raymarch through water volume
299+ let dist = raymarch_water (origin , hit_top , hit_bottom , WATER_DEPTH , adjusted_time );
300+ let surface_pos = origin + ray * dist ;
301+
302+ // Calculate surface normal with distance-based smoothing
303+ var normal = calculate_normal (surface_pos . xz , 0 .01 , WATER_DEPTH , adjusted_time );
304+ normal = mix (normal , vec3f (0 .0 ,1 .0 ,0 .0 ), 0 .8 *min (1 .0 , sqrt (dist *0 .01 )*1 .1 ));
305+
306+ // Fresnel effect calculation (I do not understand the magics here...)
307+ let fresnel = 0 .04 + 0 .96 *pow (1 .0 - max (0 .0 , dot (- normal , ray )),5 .0 );
308+
309+ // Reflection vector with upward bias
310+ var refl_dir = reflect (ray , normal );
311+ refl_dir . y = abs (refl_dir . y ); // Prevent downward reflections
312+
313+ // Atmosphere and sun contributions
314+ let sun_dir = get_sun_direction (adjusted_time , SUN_ON );
315+ let reflection = extra_cheap_atmosphere (refl_dir , sun_dir )*0 .5 + get_sun (refl_dir , sun_dir );
316+
317+ // Subsurface scattering approximation
318+ let depth_factor = (surface_pos . y + WATER_DEPTH )/ WATER_DEPTH ;
319+ let scattering = vec3f (0 .0293 , 0 .0698 , 0 .1717 )*0 .1 * (0 .2 + depth_factor );
320+
321+ // Final color composition
322+ let final_color = fresnel *reflection + scattering ;
323+ return vec4f (aces_tonemap (final_color *2 .0 ), 1 .0 );
324+ }
0 commit comments