nightshade 0.13.0

A cross-platform data-oriented game engine.
Documentation
struct CameraUniforms {
    view: mat4x4<f32>,
    projection: mat4x4<f32>,
    view_projection: mat4x4<f32>,
    camera_position: vec4<f32>,
}

struct GrassUniforms {
    time: f32,
    wind_strength: f32,
    wind_frequency: f32,
    sss_intensity: f32,
    wind_direction: vec2<f32>,
    interaction_strength: f32,
    _padding: f32,
    sun_direction: vec3<f32>,
    sun_intensity: f32,
    sun_color: vec3<f32>,
    ambient_intensity: f32,
}

struct GrassInstance {
    position: vec3<f32>,
    rotation: f32,
    height: f32,
    width: f32,
    species_index: u32,
    lod: u32,
    base_color: vec4<f32>,
    tip_color: vec4<f32>,
    bend: vec2<f32>,
    _padding: vec2<f32>,
}

struct GrassSpeciesData {
    base_color: vec4<f32>,
    tip_color: vec4<f32>,
    sss_color: vec4<f32>,
    sss_intensity: f32,
    specular_power: f32,
    specular_strength: f32,
    blade_curvature: f32,
}

struct VertexInput {
    @builtin(vertex_index) vertex_index: u32,
    @builtin(instance_index) instance_index: u32,
}

struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @location(0) world_position: vec3<f32>,
    @location(1) normal: vec3<f32>,
    @location(2) blade_height_factor: f32,
    @location(3) base_color: vec4<f32>,
    @location(4) tip_color: vec4<f32>,
    @location(5) sss_color: vec3<f32>,
    @location(6) sss_intensity: f32,
}

@group(0) @binding(0) var<uniform> camera: CameraUniforms;
@group(0) @binding(1) var<uniform> grass_uniforms: GrassUniforms;
@group(0) @binding(2) var<storage, read> instances: array<GrassInstance>;
@group(0) @binding(3) var<storage, read> visible_indices: array<u32>;
@group(0) @binding(4) var<storage, read> species_data: array<GrassSpeciesData>;

const BLADE_VERTICES: u32 = 7u;
const PI: f32 = 3.14159265359;

fn blade_vertex_position(vertex_index: u32, height: f32, width: f32, curvature: f32) -> vec3<f32> {
    let segment = vertex_index / 2u;
    let side = f32(vertex_index % 2u) * 2.0 - 1.0;

    let t = f32(segment) / 3.0;

    let curve_offset = curvature * t * t;

    let segment_width = width * (1.0 - t * 0.8);

    var position: vec3<f32>;
    position.x = side * segment_width * 0.5;
    position.y = t * height;
    position.z = curve_offset;

    if vertex_index == 6u {
        position.x = 0.0;
        position.y = height;
        position.z = curvature;
    }

    return position;
}

fn wind_displacement(world_pos: vec3<f32>, height_factor: f32, time: f32, strength: f32, frequency: f32, direction: vec2<f32>) -> vec3<f32> {
    let base_freq = frequency * 0.5;
    let gust_freq = frequency * 2.0;

    let wave1 = sin(world_pos.x * base_freq + time * 1.5) * 0.5 + 0.5;
    let wave2 = sin(world_pos.z * base_freq * 0.7 + time * 1.2) * 0.5 + 0.5;
    let wave3 = sin((world_pos.x + world_pos.z) * gust_freq + time * 3.0) * 0.3;

    let combined_wave = (wave1 * wave2 + wave3) * strength * height_factor * height_factor;

    var displacement: vec3<f32>;
    displacement.x = direction.x * combined_wave;
    displacement.y = -combined_wave * 0.1;
    displacement.z = direction.y * combined_wave;

    return displacement;
}

fn rotation_matrix_y(angle: f32) -> mat3x3<f32> {
    let c = cos(angle);
    let s = sin(angle);
    return mat3x3<f32>(
        vec3<f32>(c, 0.0, s),
        vec3<f32>(0.0, 1.0, 0.0),
        vec3<f32>(-s, 0.0, c)
    );
}

@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
    var output: VertexOutput;

    let visible_index = visible_indices[input.instance_index];
    let instance = instances[visible_index];
    let species = species_data[instance.species_index];

    let curvature = species.blade_curvature;
    var local_pos = blade_vertex_position(input.vertex_index, instance.height, instance.width, curvature);

    let height_factor = local_pos.y / instance.height;

    let rot = rotation_matrix_y(instance.rotation);
    local_pos = rot * local_pos;

    let wind_disp = wind_displacement(
        instance.position,
        height_factor,
        grass_uniforms.time,
        grass_uniforms.wind_strength,
        grass_uniforms.wind_frequency,
        grass_uniforms.wind_direction
    );

    let bend_disp = vec3<f32>(
        instance.bend.x * height_factor * height_factor * grass_uniforms.interaction_strength,
        0.0,
        instance.bend.y * height_factor * height_factor * grass_uniforms.interaction_strength
    );

    var world_pos = instance.position + local_pos + wind_disp + bend_disp;

    let tangent = normalize(vec3<f32>(
        rot[0][0],
        0.0,
        rot[2][0]
    ));
    let bitangent = vec3<f32>(0.0, 1.0, 0.0);
    var normal = cross(bitangent, tangent);

    let wind_tilt = wind_disp.x * 0.5 + wind_disp.z * 0.5;
    normal = normalize(normal + vec3<f32>(wind_tilt * 0.3, 0.0, wind_tilt * 0.3));

    output.position = camera.view_projection * vec4<f32>(world_pos, 1.0);
    output.world_position = world_pos;
    output.normal = normal;
    output.blade_height_factor = height_factor;
    output.base_color = instance.base_color;
    output.tip_color = instance.tip_color;
    output.sss_color = species.sss_color.rgb;
    output.sss_intensity = species.sss_intensity * grass_uniforms.sss_intensity;

    return output;
}

fn kajiya_kay_specular(light_dir: vec3<f32>, view_dir: vec3<f32>, tangent: vec3<f32>, power: f32) -> f32 {
    let half_vec = normalize(light_dir + view_dir);

    let dot_th = dot(tangent, half_vec);
    let sin_th = sqrt(max(0.0, 1.0 - dot_th * dot_th));

    return pow(sin_th, power);
}

fn subsurface_scattering(light_dir: vec3<f32>, view_dir: vec3<f32>, normal: vec3<f32>, thickness: f32) -> f32 {
    let back_light = dot(-light_dir, view_dir);
    let sss_amount = saturate(back_light * 0.5 + 0.5);
    let edge_factor = pow(1.0 - saturate(dot(normal, view_dir)), 2.0);

    return sss_amount * thickness * (1.0 + edge_factor);
}

@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
    let base_color = mix(input.base_color.rgb, input.tip_color.rgb, input.blade_height_factor);

    let view_dir = normalize(camera.camera_position.xyz - input.world_position);
    let light_dir = normalize(grass_uniforms.sun_direction);

    let ndotl = dot(input.normal, light_dir);
    let wrap_diffuse = (ndotl + 0.5) / 1.5;
    let diffuse = max(0.0, wrap_diffuse);

    let tangent = normalize(cross(input.normal, vec3<f32>(0.0, 1.0, 0.0)));
    let specular = kajiya_kay_specular(light_dir, view_dir, tangent, 64.0) * 0.3;

    let sss = subsurface_scattering(light_dir, view_dir, input.normal, input.blade_height_factor) * input.sss_intensity;
    let sss_contribution = input.sss_color * sss * grass_uniforms.sun_color * grass_uniforms.sun_intensity;

    var final_color = base_color * grass_uniforms.ambient_intensity;
    final_color += base_color * diffuse * grass_uniforms.sun_color * grass_uniforms.sun_intensity;
    final_color += specular * grass_uniforms.sun_color;
    final_color += sss_contribution;

    let distance_to_camera = length(camera.camera_position.xyz - input.world_position);
    let distance_fade = 1.0 - smoothstep(180.0, 200.0, distance_to_camera);

    let tip_fade = smoothstep(0.9, 1.0, input.blade_height_factor);
    let alpha = (1.0 - tip_fade * 0.5) * distance_fade;

    return vec4<f32>(final_color, alpha);
}