bevy_feronia 0.8.2

Foliage/grass scattering tools and wind simulation shaders/materials that prioritize visual fidelity/artistic freedom, a declarative api and modularity.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
#import bevy_pbr::mesh_view_bindings::{view, lights, globals, clusterable_objects}
#import bevy_pbr::shadows::fetch_directional_shadow
#import bevy_pbr::shadows::fetch_point_shadow
#import bevy_pbr::mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT
#import bevy_pbr::clustered_forward::{fragment_cluster_index, unpack_clusterable_object_index_ranges, get_clusterable_object_id}
#import bevy_pbr::pbr_types
#import bevy_pbr::pbr_functions
#import bevy_pbr::mesh_types::MESH_FLAGS_SHADOW_RECEIVER_BIT
#import bevy_pbr::mesh_functions::mesh_normal_local_to_world
#import bevy_pbr::view_transformations::position_world_to_clip
#import bevy_pbr::utils::rand_f
#import bevy_pbr::pbr_types::PbrInput,

#import bevy_feronia::wind::Wind
#import bevy_feronia::instancing::bindings::material_uniforms
#import bevy_feronia::types::{SampledNoise, DisplacedVertex, InstanceInfo}
#import bevy_feronia::displace::displace_vertex_and_calc_normal
#import bevy_feronia::noise::sample_noise

#import bevy_eidolon::render::utils
#import bevy_eidolon::render::bindings::instance_uniforms
#import bevy_eidolon::render::io_types::Vertex

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,

#ifdef VISIBILITY_RANGE_DITHER
    @location(0) @interpolate(flat) visibility_range_dither: i32,
#endif

    @location(1) @interpolate(linear, centroid) world_position: vec4<f32>,
    @location(2) @interpolate(linear, centroid) world_normal: vec3<f32>,
    @location(3) @interpolate(linear, centroid) uv: vec2<f32>,
    @location(4) @interpolate(linear, centroid) world_tangent: vec4<f32>,
    @location(5) @interpolate(linear, centroid) local_pos: vec3<f32>,

    @location(6) @interpolate(linear, centroid) curve_factor: f32,
#ifdef AMBIENT_OCCLUSION
    @location(7) @interpolate(linear, centroid) ao: f32,
#endif

    @location(8) i_batch_id: u32,

#ifdef SUBSURFACE_SCATTERING
    @location(9) thinness_factor: f32,
#endif
};


@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
    var out: VertexOutput;
    var scale = vertex.i_pos_scale.w;
    var translation = vertex.i_pos_scale.xyz;
    var world_from_local_matrix: mat4x4<f32>;

    let batch = instance_uniforms[vertex.i_batch_id];

#ifdef BILLBOARDING
    world_from_local_matrix = mat4x4<f32>(
        vec4<f32>(scale, 0.0, 0.0, 0.0),
        vec4<f32>(0.0, scale, 0.0, 0.0),
        vec4<f32>(0.0, 0.0, scale, 0.0),
        vec4<f32>(translation, 1.0)
    );
   var final_matrix = batch.world_from_local * world_from_local_matrix;
#else
   let final_matrix = utils::calc_instance_world_matrix(
        vertex.i_pos_scale,
        vertex.i_rotation,
        batch.world_from_local
    );
#endif

    var instance: InstanceInfo;
    instance.world_from_local = final_matrix;
    instance.instance_position = final_matrix[3];
    instance.wrapped_time = globals.time;
    instance.instance_index = vertex.i_index;
    instance.edge_correction_factor = material_uniforms.edge_correction_factor;
    instance.seed = vertex.i_seed;

// TODO
#ifdef STATIC_BEND
    let static_bend = material_uniforms.static_bend_direction
                    * material_uniforms.static_bend_strength;
#endif

    let wind = material_uniforms.current;
    let noise = sample_noise(instance, wind, vertex.position);

    let displaced = displace_vertex_and_calc_normal(
        wind,
        noise,
        vertex.position,
        instance,
#ifdef STATIC_BEND
        static_bend,
        material_uniforms.static_bend_control_point,
        material_uniforms.static_bend_min_max,
#endif
#ifdef VERTEX_NORMALS
        vertex.normal,
#endif
#ifdef VERTEX_TANGENTS
        vertex.tangent,
#endif
#ifdef VERTEX_UVS_A
        vertex.uv
#endif
    );

    out.world_position = displaced.world_position;
    out.world_normal = displaced.world_normal;
    out.local_pos = vertex.position;
    out.world_tangent = displaced.world_tangent;
    out.clip_position = position_world_to_clip(out.world_position.xyz);

#ifdef VERTEX_UVS_A
    out.uv = vertex.uv;
#endif

#ifdef AMBIENT_OCCLUSION
    let height_range = wind.aabb_max.y - wind.aabb_min.y;
    let normalized_height = saturate((vertex.position.y - wind.aabb_min.y) / max(height_range, 0.0001));

    out.ao = normalized_height;
#endif

#ifdef VISIBILITY_RANGE_DITHER
    out.visibility_range_dither = utils::get_visibility_range_dither_level(
        batch.visibility_range,
        final_matrix[3]
    );
#endif

#ifdef SUBSURFACE_SCATTERING
    let local_pos = vertex.position;

    let height = wind.aabb_max.y - wind.aabb_min.y;

    let inverse_height = 1.0 / max(height, 0.00001);

    // TODO support thickness texture instead of `thinness_factor`
    out.thinness_factor = saturate((local_pos.y - wind.aabb_min.y) * inverse_height);
 #endif

#ifdef CURVE_NORMALS
    out.curve_factor = material_uniforms.curve_factor;
#endif

    out.i_batch_id = vertex.i_batch_id;

    return out;
}

@fragment
fn fragment(
    in: VertexOutput,
    @builtin(front_facing) is_front: bool,
) -> @location(0) vec4<f32> {

#ifdef VISIBILITY_RANGE_DITHER
    #ifndef SHADOW_PASS
        bevy_pbr::pbr_functions::visibility_range_dither(in.clip_position, in.visibility_range_dither);
    #endif
#endif

    let batch = instance_uniforms[in.i_batch_id];

#ifdef MATERIAL_DEBUG
    return batch.color;
#endif

    let h = saturate(1.0 - in.uv.y);
    var albedo = batch.color.rgb;
    let bottom_height = material_uniforms.gradient_start;
    let bottom_strength = saturate(1.0 - (h / max(bottom_height, 0.0001)));
    albedo = mix(
        albedo,
        material_uniforms.bottom_color.rgb,
        bottom_strength * material_uniforms.tint_factor
    );

    let top_start_height = material_uniforms.gradient_end;
    let top_range = max(1.0 - top_start_height, 0.0001);
    let top_strength = saturate((h - top_start_height) / top_range);
    albedo = mix(
        albedo,
        material_uniforms.top_color.rgb,
        top_strength * material_uniforms.tint_factor
    );

#ifdef AMBIENT_OCCLUSION
    let ambient_occlusion = mix(0.01, 1.0, in.ao);
    albedo = albedo * ambient_occlusion;
#endif

#ifdef STANDARD_PBR
    var pbr_input: pbr_types::PbrInput = pbr_types::pbr_input_new();

    pbr_input.material.base_color = vec4<f32>(albedo, 1.0);
    pbr_input.material.perceptual_roughness = material_uniforms.roughness;
    pbr_input.material.metallic = material_uniforms.metallic;
    pbr_input.material.reflectance = vec3<f32>(material_uniforms.reflectance);
    pbr_input.frag_coord = in.clip_position;
    pbr_input.world_position = in.world_position;
    pbr_input.is_orthographic = view.clip_from_view[3].w == 1.0;
    pbr_input.flags = MESH_FLAGS_SHADOW_RECEIVER_BIT;

    var N = normalize(in.world_normal);
    var T = in.world_tangent;

    if !is_front {
        N = -N;
        T = -T;
    }


#ifdef CURVE_NORMALS
    let signed_norm_x = in.uv.x * 2.0 - 1.0;
    let curve_angle = in.curve_factor /2. * abs(signed_norm_x);
    let clamped_angle = clamp(curve_angle, 0.0, 1.4);
    let offset_mag = sin(clamped_angle);

    let curve_offset_world = in.world_tangent.xyz * offset_mag * sign(signed_norm_x);

    N = normalize(N + curve_offset_world);
#endif

    pbr_input.world_normal = N;
    pbr_input.N = N;
    pbr_input.V = pbr_functions::calculate_view(
        in.world_position,
        pbr_input.is_orthographic
    );

 #ifdef SUBSURFACE_SCATTERING
    let sss_glow = calc_sss_lighting(
        material_uniforms.subsurface_scattering_scale,
        material_uniforms.subsurface_scattering_intensity,
        pbr_input,
        in.thinness_factor
    );

    #ifdef DEBUG_SSS
        return vec4<f32>(sss_glow, 1.0);
    #else
        pbr_input.material.emissive = vec4<f32>(
            pbr_input.material.emissive.rgb + (sss_glow * pbr_input.material.base_color.rgb),
            pbr_input.material.emissive.a
        );
    #endif
#endif

    var output_color = pbr_functions::apply_pbr_lighting(pbr_input);
    output_color = pbr_functions::main_pass_post_lighting_processing(
        pbr_input,
        output_color
    );
    return output_color;

#else // NOT STANDARD_PBR
    var pbr_color = vec4<f32>(albedo, 1.0);
    var N = normalize(in.world_normal);
    var T = in.world_tangent;

    if !is_front {
        N = -N;
        T = -T;
    }

    #ifndef DIRECTIONAL_LIGHTS
    #ifndef POINT_LIGHTS
        return pbr_color;
    #endif
    #endif

    #ifdef CURVE_NORMALS
    #ifdef CURVE_NORMALS
        let signed_norm_x = in.uv.x * 2.0 - 1.0;
        let curve_angle = in.curve_factor * abs(signed_norm_x);
        let clamped_angle = clamp(curve_angle, 0.0, 1.4); // ~80 deg
        let offset_mag = sin(clamped_angle);

        let curve_offset_world = in.world_tangent.xyz * offset_mag * sign(signed_norm_x);

        N = normalize(N + curve_offset_world);
    #endif

    #endif
        // TODO move to module/file
        // Implements a simplified Blinn-Phong reflection model
        // for specular highlights, combined with a standard Lambertian diffuse term.
        //
        // 1. Blinn-Phong: Blinn, J. F. (1977). "Models of light reflection
        //    for computer synthesized pictures". SIGGRAPH '77.
        //
        // 2. Phong (used in bevy_procedural_grass):
        //    Phong, B. T. (1975). "Illumination for computer generated pictures".
        //    Communications of the ACM.

        var final_color_rgb = pbr_color.rgb * lights.ambient_color.rgb * material_uniforms.ambient_light_intensity;
        var final_specular = vec3<f32>(0.);

        let V = normalize(view.world_position.xyz - in.world_position.xyz);

        let view_z = dot(vec4<f32>(
            view.view_from_world[0].z,
            view.view_from_world[1].z,
            view.view_from_world[2].z,
            view.view_from_world[3].z
        ),  in.world_position);

    #ifdef DIRECTIONAL_LIGHTS
        for (var i = 0u; i < lights.n_directional_lights; i = i + 1u) {
            let sun = lights.directional_lights[i];
            let L = sun.direction_to_light;

            let scaled_light_color = sun.color.rgb * material_uniforms.light_intensity;

            // Translucency
            let NdotL_raw = dot(N, L);
            let NdotL_front = saturate(NdotL_raw);
            let NdotL_back = saturate(-NdotL_raw) * material_uniforms.translucency;
            let NdotL = NdotL_front + NdotL_back;

            // Specular Term
            let H = normalize(V + L);
            let NdotH = saturate(dot(N, H));
            let specular_factor = pow(NdotH, material_uniforms.specular_power);

            let shadow = fetch_directional_shadow(
                i,
                in.world_position,
                N,
                view_z
            );
            let final_shadow = clamp(shadow, 0.1, 1.);

            // Accumulate Diffuse
            final_color_rgb += pbr_color.rgb * scaled_light_color * NdotL * final_shadow * material_uniforms.diffuse_scaling;

            //  Accumulate Specular
            if NdotL_raw > 0. {
                final_specular += scaled_light_color * specular_factor * material_uniforms.specular_strength * shadow;
            }
        }
    #endif // DIRECTIONAL_LIGHTS

    #ifdef POINT_LIGHTS
        let is_orthographic = view.clip_from_view[3].w == 1.;
        let cluster_index = fragment_cluster_index(
            in.clip_position.xy,
            view_z,
            is_orthographic
        );
        let ranges = unpack_clusterable_object_index_ranges(cluster_index);

        for (var i = ranges.first_point_light_index_offset; i < ranges.first_spot_light_index_offset; i = i + 1u) {
            let light_id = get_clusterable_object_id(i);
            let light = clusterable_objects.data[light_id];

            let light_position = light.position_radius.xyz;
            let scaled_light_color = light.color_inverse_square_range.rgb * material_uniforms.light_intensity;
            let inverse_square_range = light.color_inverse_square_range.w;

            if (inverse_square_range <= 0.) { continue; }

            // Skip out of range lights
            let range_sq = 1. / inverse_square_range;
            let light_vector = light_position - in.world_position.xyz;
            let distance_sq = dot(light_vector, light_vector);

            if (distance_sq > range_sq) { continue; }

            let L = normalize(light_vector);

            let range_factor = distance_sq * inverse_square_range;
            let smooth_falloff = saturate(1.0 - range_factor);
            let attenuation = smooth_falloff * smooth_falloff;

            // Translucency
            let NdotL_raw = dot(N, L);
            let NdotL_front = saturate(NdotL_raw);
            let NdotL_back = saturate(-NdotL_raw) * material_uniforms.translucency;
            let NdotL = NdotL_front + NdotL_back;

            // Specular Term
            let H = normalize(V + L);
            let NdotH = saturate(dot(N, H));
            let specular_factor = pow(NdotH, material_uniforms.specular_power);

            var shadow = 1.;
            if ((light.flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) {
                shadow = fetch_point_shadow(light_id, in.world_position, N);
            }
            let final_shadow = clamp(shadow, 0.1, 1.);

            // Accumulate Diffuse
            final_color_rgb += pbr_color.rgb * scaled_light_color * attenuation * NdotL * final_shadow * material_uniforms.diffuse_scaling;

            //  Accumulate Specular
            if NdotL_raw > 0. {
                final_specular += scaled_light_color * attenuation * specular_factor * material_uniforms.specular_strength * shadow;
            }
        }
    #endif // POINT_LIGHTS

        final_color_rgb += final_specular;

        var final_color = vec4<f32>(final_color_rgb, pbr_color.w);

        return final_color;
    #endif // NOT STANDARD_PBR
}

// TODO make reusable/expose/tweakable, see /extension/fragment.wgsl
#ifdef SUBSURFACE_SCATTERING
// GDC 2011 "Approximating Translucency" Implementation
// Source: Barré-Brisebois, C., & Bouchard, M. (2011). Approximating Translucency for a
// Fast, Cheap and Convincing Subsurface Scattering Look. Game Developers Conference.


// Controls how much the surface normal influences the scattered light direction.
// A value of 0.0 makes light appear to pass straight through.
// A value of 1.0 makes light appear to be heavily scattered by the surface.
const SSS_DISTORTION: f32 = 0.1;

const SSS_WRAP: f32 = 1.5;
const SSS_WRAP_INV: f32 = 1.0 / (1.0 + SSS_WRAP);


fn calc_sss_lighting(scale:f32, intensity:f32, pbr_input: PbrInput, thinness_factor: f32) -> vec3<f32> {
    var sss_light = vec3<f32>(0.0);

    let view_pos = view.view_from_world * pbr_input.world_position;
    let view_z = view_pos.z;
    let cluster_index = fragment_cluster_index(pbr_input.frag_coord.xy, view_z, pbr_input.is_orthographic);
    let ranges = unpack_clusterable_object_index_ranges(cluster_index);

    // Point lights
    for (var i = ranges.first_point_light_index_offset; i < ranges.first_spot_light_index_offset; i = i + 1u) {
        let light_id = get_clusterable_object_id(i);
        let light = clusterable_objects.data[light_id];

        // Skip if covered by shadow
        var shadow = 1.0;
        if (light.flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT)!= 0u {
            shadow = fetch_point_shadow(light_id, pbr_input.world_position, pbr_input.world_normal);
        }

        if (shadow <= 0.0) { continue; }

        // Skip invalid range lights
        let light_position = light.position_radius.xyz;
        let scaled_light_color = light.color_inverse_square_range.rgb * material_uniforms.light_intensity;
        let inverse_square_range = light.color_inverse_square_range.w;

        if (inverse_square_range <= 0.0) { continue; }

        // Skip out of range lights
        let range_sq = 1.0 / inverse_square_range;
        let light_vector = light_position - pbr_input.world_position.xyz;
        let distance_sq = dot(light_vector, light_vector);

        if (distance_sq > range_sq) { continue; }

        let L = normalize(light_vector);

        let att_factor = saturate(1.0 - distance_sq / range_sq);
        let attenuation = att_factor * att_factor;
        let light_contribution = scaled_light_color * attenuation;

        // --- GDC 2011 SSS MODEL ---
        let H = normalize(L + pbr_input.world_normal * SSS_DISTORTION);

        let back_scatter_dot = saturate(dot(pbr_input.V, -H));

        // pow(dot, 8) * SSS_SCALE
        let t = back_scatter_dot * back_scatter_dot;
        let t2 = t * t;
        let back_scatter = t2 * t2 * scale;

        let front_scatter = saturate((dot(pbr_input.world_normal, L) + SSS_WRAP) * SSS_WRAP_INV);

        let sss_factor = back_scatter + front_scatter;

        sss_light += sss_factor * light_contribution * shadow;
    }

    // Directional lights
    for (var i = 0u; i < lights.n_directional_lights; i = i + 1u) {
        // Skip if covered by shadow
        let shadow = fetch_directional_shadow(i, pbr_input.world_position, pbr_input.world_normal, view_z);
        if (shadow <= 0.0) { continue; }

        let sun = lights.directional_lights[i];
        let L = sun.direction_to_light;

        // GDC 2011 SSS model
        let H = normalize(L + pbr_input.world_normal * SSS_DISTORTION);

        let back_scatter_dot = saturate(dot(pbr_input.V, -H));

        // pow(dot, 8) * SSS_SCALE
        let t = back_scatter_dot * back_scatter_dot;
        let t2 = t * t;
        let back_scatter = t2 * t2 * scale;

        let front_scatter = saturate((dot(pbr_input.world_normal, L) + SSS_WRAP) * SSS_WRAP_INV);

        let sss_factor = back_scatter + front_scatter;
        let light_contribution = sun.color.rgb * material_uniforms.light_intensity;

        sss_light += sss_factor * light_contribution * shadow;
    }

    return sss_light * intensity * thinness_factor;
}
#endif // SUBSURFACE_SCATTERING