Skip to content

Commit 0916969

Browse files
authored
quantisation and procedural ocean shaders (#118)
* shader: add quantisation to OKLAB port * chore: add additional resources to readme * shader: 'super fast procedural ocean' port from shadertoy
1 parent 50e176f commit 0916969

File tree

4 files changed

+485
-0
lines changed

4 files changed

+485
-0
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ See the [guide](./CONTRIBUTING.md)
143143
- [GLSL2WGSL converter](https://eliotbo.github.io/glsl2wgsl/), it's a mixed bag..
144144
- [bevy_shadertoy_wgsl](https://github.com/eliotbo/bevy_shadertoy_wgsl)
145145
- [Alain's blog](https://alain.xyz/blog)
146+
- [GM Shaders](https://gmshaders.com/)
147+
- [WGSL function reference](https://webgpufundamentals.org/webgpu/lessons/webgpu-wgsl-function-reference.html)
148+
- [My blog, sometimes I write about shaders/gp-gpu programming](https://jeremyfwebb.ninja/blog)
146149

147150
---
148151

@@ -180,6 +183,10 @@ See the [guide](./CONTRIBUTING.md)
180183
<td><img src="readme_assets/shadertoy-default-gif-example.gif" alt="screenshot" width="50%"></td>
181184
<td><img src="readme_assets/zippy-zap-example.gif" alt="screenshot" width="59%"></td>
182185
</tr>
186+
<tr>
187+
<td><img src="readme_assets/procedural-ocean-example.gif" alt="screenshot" width="50%"></td>
188+
<!-- <td><img src="readme_assets/zippy-zap-example.gif" alt="screenshot" width="59%"></td> -->
189+
</tr>
183190
</table>
184191

185192
---
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
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

Comments
 (0)