nightshade 0.13.3

A cross-platform data-oriented game engine.
Documentation
const PI: f32 = 3.141592653589793;
const MAX_STEPS: i32 = 64;

const SHAPE_BOX: u32 = 0u;
const SHAPE_CYLINDER: u32 = 1u;
const SHAPE_SPHERE: u32 = 2u;

const FLOW_WATERFALL: u32 = 0u;
const FLOW_MIST: u32 = 1u;
const FLOW_CASCADE: u32 = 2u;

struct Uniforms {
    view: mat4x4<f32>,
    projection: mat4x4<f32>,
    view_projection: mat4x4<f32>,
    camera_position: vec4<f32>,
    time: f32,
    _pad0: f32,
    _pad1: f32,
    _pad2: f32,
}

struct WaterMaterial {
    base_color: vec4<f32>,
    water_color: vec4<f32>,
    wave_height: f32,
    choppy: f32,
    speed: f32,
    freq: f32,
    specular_strength: f32,
    fresnel_power: f32,
    volume_shape: u32,
    volume_flow_type: u32,
    volume_size: vec3<f32>,
    is_volumetric: u32,
    flow_direction: vec2<f32>,
    flow_strength: f32,
    _flow_padding: f32,
}

struct WaterInstance {
    model_0: vec4<f32>,
    model_1: vec4<f32>,
    model_2: vec4<f32>,
    model_3: vec4<f32>,
    material_index: u32,
    _pad0: u32,
    _pad1: u32,
    _pad2: u32,
}

struct VertexInput {
    @location(0) position: vec3<f32>,
    @location(1) normal: vec3<f32>,
    @location(2) tex_coords: vec2<f32>,
}

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) world_pos: vec3<f32>,
    @location(1) local_pos: vec3<f32>,
    @location(2) @interpolate(flat) material_index: u32,
    @location(3) @interpolate(flat) instance_index: u32,
}

@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var<storage, read> materials: array<WaterMaterial>;
@group(0) @binding(2) var<storage, read> instances: array<WaterInstance>;
@group(0) @binding(3) var<storage, read> visible_indices: array<u32>;

fn hash3(p: vec3<f32>) -> f32 {
    var p3 = fract(p * 0.1031);
    p3 = p3 + dot(p3, p3.yzx + 33.33);
    return fract((p3.x + p3.y) * p3.z);
}

fn noise3d(p: vec3<f32>) -> f32 {
    let i = floor(p);
    let f = fract(p);
    let u = f * f * (3.0 - 2.0 * f);

    return mix(
        mix(
            mix(hash3(i + vec3<f32>(0.0, 0.0, 0.0)), hash3(i + vec3<f32>(1.0, 0.0, 0.0)), u.x),
            mix(hash3(i + vec3<f32>(0.0, 1.0, 0.0)), hash3(i + vec3<f32>(1.0, 1.0, 0.0)), u.x),
            u.y
        ),
        mix(
            mix(hash3(i + vec3<f32>(0.0, 0.0, 1.0)), hash3(i + vec3<f32>(1.0, 0.0, 1.0)), u.x),
            mix(hash3(i + vec3<f32>(0.0, 1.0, 1.0)), hash3(i + vec3<f32>(1.0, 1.0, 1.0)), u.x),
            u.y
        ),
        u.z
    );
}

fn fbm3d(p: vec3<f32>, octaves: i32) -> f32 {
    var value = 0.0;
    var amplitude = 0.5;
    var frequency = 1.0;

    for (var index = 0; index < octaves; index = index + 1) {
        value = value + amplitude * noise3d(p * frequency);
        amplitude = amplitude * 0.5;
        frequency = frequency * 2.0;
    }
    return value;
}

fn sdf_box(p: vec3<f32>, half_size: vec3<f32>) -> f32 {
    let d = abs(p) - half_size;
    return length(max(d, vec3<f32>(0.0))) + min(max(d.x, max(d.y, d.z)), 0.0);
}

fn sdf_cylinder(p: vec3<f32>, radius: f32, half_height: f32) -> f32 {
    let d = vec2<f32>(length(p.xz) - radius, abs(p.y) - half_height);
    return min(max(d.x, d.y), 0.0) + length(max(d, vec2<f32>(0.0)));
}

fn sdf_ellipsoid(p: vec3<f32>, radii: vec3<f32>) -> f32 {
    let k0 = length(p / radii);
    let k1 = length(p / (radii * radii));
    return k0 * (k0 - 1.0) / k1;
}

fn sdf_volume(p: vec3<f32>, shape: u32, size: vec3<f32>) -> f32 {
    let half_size = size * 0.5;
    if shape == SHAPE_CYLINDER {
        return sdf_cylinder(p, half_size.x, half_size.y);
    } else if shape == SHAPE_SPHERE {
        return sdf_ellipsoid(p, half_size);
    } else {
        return sdf_box(p, half_size);
    }
}

fn density_waterfall(normalized_pos: vec3<f32>, time: f32, mat: WaterMaterial) -> f32 {
    let flow_speed = mat.speed * 2.0;
    let flow_offset = vec3<f32>(0.0, time * flow_speed, 0.0);

    let stretched_pos = vec3<f32>(
        normalized_pos.x * 3.0,
        normalized_pos.y * 0.5,
        normalized_pos.z * 3.0
    );

    let noise_pos = stretched_pos * mat.freq * 2.0 + flow_offset;
    var density = fbm3d(noise_pos, 4);

    let height_factor = smoothstep(-1.0, -0.6, normalized_pos.y) * (1.0 - smoothstep(0.6, 1.0, normalized_pos.y));
    density = density * height_factor;

    let turbulence = fbm3d(noise_pos * 2.0 + vec3<f32>(17.3, 0.0, 23.7), 3) * mat.choppy * 0.1;
    density = density + turbulence;

    return density;
}

fn density_mist(normalized_pos: vec3<f32>, time: f32, mat: WaterMaterial) -> f32 {
    let slow_time = time * mat.speed * 0.2;

    let rise_offset = vec3<f32>(0.0, -slow_time * 2.0, 0.0);
    let drift_offset = vec3<f32>(slow_time * 0.3, 0.0, slow_time * 0.2);

    let noise_pos1 = normalized_pos * mat.freq * 1.5 + rise_offset + drift_offset;
    let noise_pos2 = normalized_pos * mat.freq * 3.0 + rise_offset * 0.7 - drift_offset;

    var density = fbm3d(noise_pos1, 3) * 0.6;
    density = density + fbm3d(noise_pos2, 2) * 0.4;

    density = smoothstep(0.3, 0.7, density);

    let dist_from_center = length(normalized_pos.xz);
    let center_falloff = 1.0 - smoothstep(0.0, 1.0, dist_from_center);
    density = density * center_falloff * center_falloff;

    let height_falloff = 1.0 - smoothstep(-0.5, 0.8, normalized_pos.y);
    let bottom_falloff = smoothstep(-1.0, -0.5, normalized_pos.y);
    density = density * height_falloff * bottom_falloff;

    let wisps = noise3d(normalized_pos * 6.0 + rise_offset * 3.0);
    density = density * smoothstep(-0.2, 0.3, wisps);

    return density * 0.25;
}

fn density_cascade(normalized_pos: vec3<f32>, time: f32, mat: WaterMaterial) -> f32 {
    let flow_speed = mat.speed * 1.5;
    let flow_offset = vec3<f32>(0.0, time * flow_speed, 0.0);

    let cascade_pos = normalized_pos * mat.freq * 4.0 + flow_offset;

    var density = 0.0;

    for (var index = 0; index < 3; index = index + 1) {
        let offset = vec3<f32>(f32(index) * 7.3, f32(index) * 3.7, f32(index) * 5.1);
        let cascade_noise = fbm3d(cascade_pos + offset, 3);
        let cascade = smoothstep(0.4, 0.6, cascade_noise);
        density = density + cascade * 0.4;
    }

    let height_factor = 1.0 - smoothstep(0.5, 1.0, normalized_pos.y);
    density = density * height_factor;

    let dist_from_center = length(normalized_pos.xz);
    let center_factor = 1.0 - smoothstep(0.0, 0.8, dist_from_center);
    density = density * center_factor;

    return density;
}

fn volume_density(local_pos: vec3<f32>, time: f32, mat: WaterMaterial) -> f32 {
    let half_size = mat.volume_size * 0.5;

    let sdf_dist = sdf_volume(local_pos, mat.volume_shape, mat.volume_size);
    if sdf_dist > 0.0 {
        return 0.0;
    }

    let normalized_pos = local_pos / half_size;

    var density: f32;
    if mat.volume_flow_type == FLOW_MIST {
        density = density_mist(normalized_pos, time, mat);
    } else if mat.volume_flow_type == FLOW_CASCADE {
        density = density_cascade(normalized_pos, time, mat);
    } else {
        density = density_waterfall(normalized_pos, time, mat);
    }

    let edge_factor = 1.0 - smoothstep(-0.15, 0.0, sdf_dist);
    density = density * edge_factor;

    return clamp(density * 1.5, 0.0, 1.0);
}

fn ray_volume_intersection(ray_origin: vec3<f32>, ray_dir: vec3<f32>, shape: u32, size: vec3<f32>) -> vec2<f32> {
    let half_size = size * 0.5;
    let inv_dir = 1.0 / ray_dir;
    let t1 = (-half_size - ray_origin) * inv_dir;
    let t2 = (half_size - ray_origin) * inv_dir;
    let t_min_vec = min(t1, t2);
    let t_max_vec = max(t1, t2);
    let t_near = max(max(t_min_vec.x, t_min_vec.y), t_min_vec.z);
    let t_far = min(min(t_max_vec.x, t_max_vec.y), t_max_vec.z);
    return vec2<f32>(max(t_near, 0.0), t_far);
}

@vertex
fn vs_main(
    vertex: VertexInput,
    @builtin(instance_index) instance_index: u32
) -> VertexOutput {
    var out: VertexOutput;

    let visible_idx = visible_indices[instance_index];
    let instance = instances[visible_idx];

    let model = mat4x4<f32>(
        instance.model_0,
        instance.model_1,
        instance.model_2,
        instance.model_3
    );

    let mat = materials[instance.material_index];
    let scaled_pos = vertex.position * mat.volume_size;
    let world_pos = (model * vec4<f32>(scaled_pos, 1.0)).xyz;

    out.world_pos = world_pos;
    out.local_pos = scaled_pos;
    out.clip_position = uniforms.view_projection * vec4<f32>(world_pos, 1.0);
    out.material_index = instance.material_index;
    out.instance_index = visible_idx;

    return out;
}

struct FragmentOutput {
    @location(0) color: vec4<f32>,
    @builtin(frag_depth) depth: f32,
}

@fragment
fn fs_main(in: VertexOutput) -> FragmentOutput {
    let mat = materials[in.material_index];
    let camera_pos = uniforms.camera_position.xyz;

    let ray_origin = camera_pos;
    let ray_dir = normalize(in.world_pos - camera_pos);

    let instance = instances[in.instance_index];
    let model = mat4x4<f32>(
        instance.model_0,
        instance.model_1,
        instance.model_2,
        instance.model_3
    );
    let model_inv = mat4x4<f32>(
        vec4<f32>(1.0, 0.0, 0.0, 0.0),
        vec4<f32>(0.0, 1.0, 0.0, 0.0),
        vec4<f32>(0.0, 0.0, 1.0, 0.0),
        vec4<f32>(-model[3].x, -model[3].y, -model[3].z, 1.0)
    );

    let local_ray_origin = (model_inv * vec4<f32>(ray_origin, 1.0)).xyz;
    let local_ray_dir = normalize((model_inv * vec4<f32>(ray_dir, 0.0)).xyz);

    let t_bounds = ray_volume_intersection(local_ray_origin, local_ray_dir, mat.volume_shape, mat.volume_size);

    if t_bounds.x > t_bounds.y {
        discard;
    }

    let step_size = 1.0 / f32(MAX_STEPS);
    let ray_length = t_bounds.y - t_bounds.x;
    let actual_step = ray_length * step_size;

    var accumulated_color = vec3<f32>(0.0);
    var accumulated_alpha = 0.0;

    let sun_direction = normalize(vec3<f32>(0.5, 0.8, 0.3));
    let sun_color = vec3<f32>(1.0, 0.95, 0.85);
    let ambient = vec3<f32>(0.3, 0.4, 0.5);

    for (var index = 0; index < MAX_STEPS; index = index + 1) {
        if accumulated_alpha > 0.95 {
            break;
        }

        let t = t_bounds.x + (f32(index) + 0.5) * actual_step;
        let sample_pos = local_ray_origin + local_ray_dir * t;

        let density = volume_density(sample_pos, uniforms.time, mat);

        if density > 0.01 {
            let foam_amount = smoothstep(0.3, 0.7, density);
            let base_water = mat.base_color.rgb;
            let foam_white = vec3<f32>(0.9, 0.95, 1.0);
            var sample_color = mix(base_water, foam_white, foam_amount);

            let light_density = volume_density(sample_pos + sun_direction * 0.1, uniforms.time, mat);
            let shadow = exp(-light_density * 2.0);
            let lighting = ambient + sun_color * shadow * 0.7;
            sample_color = sample_color * lighting;

            let sample_alpha = density * actual_step * 4.0;

            accumulated_color = accumulated_color + sample_color * sample_alpha * (1.0 - accumulated_alpha);
            accumulated_alpha = accumulated_alpha + sample_alpha * (1.0 - accumulated_alpha);
        }
    }

    if accumulated_alpha < 0.01 {
        discard;
    }

    let view_dir = -ray_dir;
    let fresnel = pow(1.0 - max(dot(view_dir, vec3<f32>(0.0, 0.0, 1.0)), 0.0), mat.fresnel_power);
    accumulated_color = accumulated_color + vec3<f32>(0.1, 0.15, 0.2) * fresnel * accumulated_alpha;

    var out: FragmentOutput;
    out.color = vec4<f32>(accumulated_color, accumulated_alpha);
    if t_bounds.x > 0.001 {
        let entry_world = ray_origin + ray_dir * t_bounds.x;
        let entry_clip = uniforms.view_projection * vec4<f32>(entry_world, 1.0);
        out.depth = entry_clip.z / entry_clip.w;
    } else {
        out.depth = in.clip_position.z;
    }
    return out;
}