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
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
// The use of layered macro/micro noise, secondary motions (S-curve, bop)
// and the methods for normal curving and edge correction are all heavily inspired by:

// "Ghost of Tsushima" and Eric Wohllaib
// of Sucker Punch Productions and the GDC 2021 talk:
// "Advanced Graphics Summit: Procedural Grass in 'Ghost of Tsushima'".

#define_import_path bevy_feronia::displace

#import bevy_pbr::mesh_view_bindings::view

#import bevy_feronia::types::{SampledNoise, DisplacedVertex, InstanceInfo}
#import bevy_feronia::wind::Wind
#import bevy_feronia::noise::sample_noise

struct CurveResult {
    local_pos: vec3<f32>,
    tangent: vec3<f32>,
    twist: f32,
    height_factor: f32,
    local_wind_dir: vec3<f32>
}

fn displace_vertex_position(
    wind: Wind,
    noise: SampledNoise,
    vertex_pos: vec3<f32>,
    instance: InstanceInfo,
#ifdef STATIC_BEND
    static_bend: vec2<f32>,
    static_bend_control_point: vec2<f32>,
    static_bend_min_max: vec2<f32>,
#endif
) -> vec3<f32> {
    let curve_data = calc_macro_curve(
        vertex_pos,
        wind,
        noise,
        instance,
    #ifdef STATIC_BEND
        static_bend,
        static_bend_control_point,
        static_bend_min_max
    #endif
    );

    let final_local_pos = apply_micro_details(
        curve_data.local_pos,
        curve_data,
        wind,
        instance,
        noise
    );

    var world_pos = (instance.world_from_local * vec4<f32>(final_local_pos, 1.0)).xyz;

    #ifdef BILLBOARDING
    world_pos = billboarding(
        wind,
        instance,
        final_local_pos,
        final_local_pos - vertex_pos
    );
    #endif

    return world_pos;
}

fn displace_vertex_and_calc_normal(
    wind: Wind,
    noise: SampledNoise,
    vertex_pos: vec3<f32>,
    instance: InstanceInfo,
#ifdef STATIC_BEND
    static_bend: vec2<f32>,
    static_bend_control_point: vec2<f32>,
    static_bend_min_max: vec2<f32>,
#endif
#ifdef VERTEX_NORMALS
    normal: vec3<f32>,
#endif
#ifdef VERTEX_TANGENTS
    tangent: vec4<f32>,
#endif
#ifdef VERTEX_UVS_A
    uv: vec2<f32>
#endif
) -> DisplacedVertex {
    var result: DisplacedVertex;

    // Macro, i.e., wind bend, static bend
    let curve_data = calc_macro_curve(
        vertex_pos,
        wind,
        noise,
        instance,
    #ifdef STATIC_BEND
        static_bend,
        static_bend_control_point,
        static_bend_min_max
    #endif
    );

    // Micro, i.e., s-curve, bop
    let final_local_pos = apply_micro_details(
        curve_data.local_pos,
        curve_data,
        wind,
        instance,
        noise
    );

    result.world_position = instance.world_from_local * vec4<f32>(final_local_pos, 1.0);

    #ifdef BILLBOARDING
    result.world_position = vec4<f32>(
        billboarding(
            wind,
            instance,
            final_local_pos,
            final_local_pos - vertex_pos
        ),
        1.0
    );
    #endif

#ifdef FAST_NORMALS
    // Uses the original, un-displaced world-space normals.
    //
    // Will have incorrect lighting as the normals will not match the displaced vertex positions.
    //
    // The mesh should ideally be modeled with its "growth" axis along Y-Up (`+Y`)
    // and its "face" pointing along Z-Up (`+Z`).
    //
    // Should be used for performance reasons and/or on static or barely wind affected objects.
    let rotation_matrix = mat3x3<f32>(
        instance.world_from_local[0].xyz,
        instance.world_from_local[1].xyz,
        instance.world_from_local[2].xyz
    );

    #ifdef VERTEX_NORMALS
        result.world_normal = normalize(rotation_matrix * normal);
    #else
        result.world_normal = normalize(instance.world_from_local[2].xyz);
    #endif

    #ifdef VERTEX_TANGENTS
        result.world_tangent = vec4<f32>(
            normalize(rotation_matrix * tangent.xyz),
            tangent.w
        );
    #else
        let world_tangent_xyz = normalize(instance.world_from_local[0].xyz);
        result.world_tangent = vec4<f32>(world_tangent_xyz, 1.0);
    #endif

#else
#ifdef ANALYTICAL_NORMALS
    // Calculates normals using a mathematical approximation of the
    // displacement.
    //
    // Should be faster than numerical sampling but less
    // accurate, as it only accounts for static_bend, twist,
    // and macro_wind, ignoring high-frequency displacements.
    //
    // The mesh should ideally be modeled with its "growth" axis along Y-Up (`+Y`)
    // and its "face" pointing along Z-Up (`+Z`).
    //
    // Typically used for billboarded foliage or flat meshes like grass.
    let local_spine_direction = curve_data.tangent;

    let cos_twist = cos(curve_data.twist);
    let sin_twist = sin(curve_data.twist);

    let local_width_direction = vec3<f32>(cos_twist, 0.0, sin_twist);

    var rotation_matrix: mat3x3<f32>;

    #ifdef BILLBOARDING
        rotation_matrix = calc_billboard_matrix(
            instance.instance_position,
            view.world_position.xyz,
            instance.world_from_local
        );
    #else
        rotation_matrix = mat3x3<f32>(
            instance.world_from_local[0].xyz,
            instance.world_from_local[1].xyz,
            instance.world_from_local[2].xyz
        );
    #endif

    let world_spine = normalize(rotation_matrix * local_spine_direction);
    let world_width = normalize(rotation_matrix * local_width_direction);

    let raw_normal = cross(world_width, world_spine);
    let len_sq = dot(raw_normal, raw_normal);
    let safe_normal = select(vec3<f32>(0.0, 1.0, 0.0), raw_normal, len_sq > 1.0e-6);

    result.world_normal = normalize(safe_normal);
    result.world_tangent = vec4<f32>(world_width, 1.0);

#else
    // Calculates normals numerically by sampling neighboring positions.
    //
    // Should be the most accurate, but most expensive path, as it runs the full
    // displacement logic on the neighbors to find the surface direction.
    //
    // Typically used for complex foliage like non-billboarded bushes, trees.

    #ifdef VERTEX_NORMALS
        let local_normal = normal;
    #else
        let local_normal = vec3<f32>(0.0, 0.0, 1.0);
    #endif

    #ifdef VERTEX_TANGENTS
        let local_tangent = tangent.xyz;
        let tangent_sign = tangent.w;
    #else
        let local_tangent = vec3<f32>(1.0, 0.0, 0.0);
        let tangent_sign = 1.0;
    #endif

    let local_bitangent = normalize(cross(local_normal, local_tangent) * tangent_sign);

    // TODO expose/calc
    // Too small, e.g. 0.01 causes flicker on simple wider geometry, too large causes flicker/wrap around on thin geometry
    let sample_offset = 0.01;
    let base_displaced_pos = final_local_pos;

    // Sample Neighbor along Tangent
    let neighbor_tangent_origin = vertex_pos + local_tangent * sample_offset;
    let noise_tangent = sample_noise(instance, wind, neighbor_tangent_origin);
    let curve_tangent = calc_macro_curve(neighbor_tangent_origin, wind, noise_tangent, instance,
        #ifdef STATIC_BEND
        static_bend,
        static_bend_control_point,
        static_bend_min_max
        #endif
    );
    let neighbor_tangent_displaced = apply_micro_details(curve_tangent.local_pos, curve_tangent, wind, instance, noise_tangent);

    // Sample Neighbor along Bitangent
    let neighbor_bitangent_origin = vertex_pos + local_bitangent * sample_offset;
    let noise_bitangent = sample_noise(instance, wind, neighbor_bitangent_origin);
    let curve_bitangent = calc_macro_curve(neighbor_bitangent_origin, wind, noise_bitangent, instance,
        #ifdef STATIC_BEND
        static_bend,
        static_bend_control_point,
        static_bend_min_max
        #endif
    );
    let neighbor_bitangent_displaced = apply_micro_details(curve_bitangent.local_pos, curve_bitangent, wind, instance, noise_bitangent);

    // Deltas
    let delta_tangent = neighbor_tangent_displaced - base_displaced_pos;
    let delta_bitangent = neighbor_bitangent_displaced - base_displaced_pos;

    let raw_normal = cross(delta_tangent, delta_bitangent) * tangent_sign;
    let len_sq = dot(raw_normal, raw_normal);
    let computed_local_normal = select(local_normal, normalize(raw_normal), len_sq > 1.0e-6);

    // Prevent back-facing normals if displacement is extreme
    let dot_alignment = dot(computed_local_normal, local_normal);
    let safe_local_normal = select(computed_local_normal, local_normal, dot_alignment < 0.1);

    var rotation_matrix: mat3x3<f32>;

    #ifdef BILLBOARDING
        rotation_matrix = calc_billboard_matrix(
            instance.instance_position,
            view.world_position.xyz,
            instance.world_from_local
        );
    #else
        rotation_matrix = mat3x3<f32>(
            instance.world_from_local[0].xyz,
            instance.world_from_local[1].xyz,
            instance.world_from_local[2].xyz
        );
    #endif

    result.world_normal = normalize(rotation_matrix * safe_local_normal);

    let world_tangent_vec = normalize(rotation_matrix * delta_tangent);

    result.world_tangent = vec4<f32>(
        normalize(world_tangent_vec - dot(world_tangent_vec, result.world_normal) * result.world_normal),
        tangent_sign
    );
#endif
#endif

#ifdef EDGE_CORRECTION
#ifdef VERTEX_UVS_A
    let edge_correction_offset = calc_edge_correction(
        result.world_position.xyz,
        result.world_normal,
        uv.x,
        instance.edge_correction_factor
    );

    result.world_position += vec4<f32>(edge_correction_offset, 0.);
#endif
#endif

    return result;
}

fn calc_macro_curve(
    local_pos: vec3<f32>,
    wind: Wind,
    noise: SampledNoise,
    instance: InstanceInfo,
#ifdef STATIC_BEND
    static_bend: vec2<f32>,
    static_bend_control_point: vec2<f32>,
    static_bend_min_max: vec2<f32>
#endif
) -> CurveResult {
    var result: CurveResult;
    let height_range = max(wind.aabb_max.y - wind.aabb_min.y, 0.001);
    let height_progress = clamp((local_pos.y - wind.aabb_min.y) / height_range, 0.0, 1.0);

    result.height_factor = height_progress;
    result.twist = 0.0;

    #ifdef WIND_AFFECTED
        // Get scale from the instance matrix to apply wind correctly in local space
        let scale_x = length(instance.world_from_local[0].xyz);
        let scale_y = length(instance.world_from_local[1].xyz);
        let scale_z = length(instance.world_from_local[2].xyz);

        let rotation_matrix = mat3x3<f32>(
            instance.world_from_local[0].xyz / max(scale_x, 0.001),
            instance.world_from_local[1].xyz / max(scale_y, 0.001),
            instance.world_from_local[2].xyz / max(scale_z, 0.001)
        );

        let world_wind_direction = vec3<f32>(wind.direction.x, 0.0, wind.direction.y);
        let local_wind_unscaled = transpose(rotation_matrix) * world_wind_direction;

        let safe_wind_vec = local_wind_unscaled + vec3<f32>(1.0e-5, 0.0, 0.0);
        result.local_wind_dir = normalize(safe_wind_vec);
    #else
        result.local_wind_dir = vec3<f32>(1.0, 0.0, 0.0);
    #endif

    var target_bend_vector = vec2<f32>(0.0);

    #ifdef STATIC_BEND
        target_bend_vector += static_bend;
    #endif
    #ifndef STATIC_BEND
        let static_bend_min_max = vec2<f32>(0.0, 0.0);
    #endif

    // Unpack seed
    let seed = unpack2x16unorm(instance.seed);
    let instance_pos = instance.world_from_local[3].xyz;
    let strength_variance = mix(static_bend_min_max.x, static_bend_min_max.y, seed.y);

    target_bend_vector *= strength_variance;
    let total_bend_amount = length(target_bend_vector);

    // Limit bending to prevent the mesh from curling into itself
    let max_allowed_bend = height_range * 0.95;
    let safe_bend_amount = min(total_bend_amount, max_allowed_bend);
    let bend_factor = clamp(total_bend_amount / height_range, 0.0, 1.0);

    // Estimate vertical height
    var tip_height = sqrt(max(height_range * height_range - safe_bend_amount * safe_bend_amount, 0.0));

    // The resulting Bezier curve arc is longer than the estimated straight-line distance.
    // Compensate by shortening the grass to prevent it from appearing to "grow" or stretch as it bends outward.
    let stretch_compensation = 1.0 - (bend_factor * 0.1);
    tip_height *= stretch_compensation;

    // Quadratic Bezier
    // P0: Start
    // P1: Control Point
    // P2: End Point
    let point_end = vec3<f32>(target_bend_vector.x, tip_height, target_bend_vector.y);
    let point_start = vec3<f32>(0.0, 0.0, 0.0);

    #ifdef STATIC_BEND
        let bend_stiffness = static_bend_control_point.x;
        let control_point_y_factor = static_bend_control_point.y;
    #else
        let bend_stiffness = 0.33;
        let control_point_y_factor = 0.5;
    #endif

    // Adjust control point height based on how much we are bending, i.e.,
    // pushing the curve up and making the tip bend more than the base.
    let control_point_y = height_range * control_point_y_factor;
    let point_control = vec3<f32>(
        target_bend_vector.x * bend_stiffness,
        control_point_y,
        target_bend_vector.y * bend_stiffness
    );

    // B(t) = (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2
    let inverse_progress = 1.0 - height_progress;
    let bezier_position = (inverse_progress * inverse_progress) * point_start
              + (2.0 * inverse_progress * height_progress) * point_control
              + (height_progress * height_progress) * point_end;

    // B'(t) = 2(1-t)(P1-P0) + 2t(P2-P1)
    let bezier_tangent = 2.0 * inverse_progress * (point_control - point_start)
                       + 2.0 * height_progress * (point_end - point_control);

    let effective_spine_y = height_progress * height_range;
    let spine_delta = bezier_position - vec3<f32>(0.0, effective_spine_y, 0.0);

    result.local_pos = vec3<f32>(
        local_pos.x + spine_delta.x,
        local_pos.y + spine_delta.y,
        local_pos.z + spine_delta.z
    );

    var final_tangent = bezier_tangent;

    #ifdef WIND_AFFECTED
        let forward_dir = result.local_wind_dir;

        let macro_noise = noise.macro_noise * 2.0 - 1.0;
        let wind_strength = macro_noise * wind.strength;
        let h = result.height_factor;

        let macro_wind_offset = forward_dir * (wind_strength * h * h);
        result.local_pos += macro_wind_offset;

        let wind_derivative = forward_dir * (wind_strength * 2.0 * h);
        final_tangent += wind_derivative;

        #ifndef BILLBOARDING
            result.twist = macro_noise * wind.twist_strength * height_progress;
        #endif
    #endif

    result.tangent = normalize(final_tangent + vec3<f32>(0.0, 1.0e-5, 0.0));

    return result;
}

fn apply_micro_details(
    pos: vec3<f32>,
    curve_data: CurveResult,
    wind: Wind,
    instance: InstanceInfo,
    noise: SampledNoise
) -> vec3<f32> {
    var final_pos = pos;

#ifdef WIND_AFFECTED
#ifndef WIND_LOW_QUALITY
    let forward_dir = curve_data.local_wind_dir;
    let up_dir = vec3<f32>(0.0, 1.0, 0.0);
    let right_dir = normalize(cross(forward_dir, up_dir));

    // Micro
    let micro_noise = noise.micro_noise * 2.0 - 1.0;
    let micro = forward_dir * (micro_noise * wind.micro_strength * curve_data.height_factor);

    // S-Curve
    let s_curve_seed = noise.phase_noise.x * 6.28;
    let s_curve_lag = curve_data.height_factor * wind.s_curve_frequency;
    let s_curve_input = instance.wrapped_time * wind.s_curve_speed + s_curve_seed + s_curve_lag;

    let s_primary_oscillation = sin(s_curve_input);
    let s_secondary_oscillation = cos(s_curve_input * 0.7) * 0.5;

    let s_curve = (forward_dir * s_primary_oscillation + right_dir * s_secondary_oscillation)
                 * wind.s_curve_strength
                 * curve_data.height_factor;

    // Bop
    let bop_seed = noise.phase_noise.y * 6.28;
    let bop_input = instance.wrapped_time * wind.bop_speed + bop_seed + s_curve_lag;
    let bop_val = sin(bop_input);

    let bop = vec3<f32>(0.0, bop_val * wind.bop_strength * curve_data.height_factor, 0.0);

    final_pos += micro + s_curve + bop;
#endif
#endif

    return final_pos;
}

fn billboarding(
    wind: Wind,
    instance: InstanceInfo,
    local_displaced_pos: vec3<f32>,
    total_offset: vec3<f32>
) -> vec3<f32> {
    let billboard_anchor_point = instance.instance_position;
    let billboard_matrix = calc_billboard_matrix(
        billboard_anchor_point,
        view.world_position.xyz,
        instance.world_from_local
    );

    let billboard_pos = billboard_anchor_point.xyz + (billboard_matrix * local_displaced_pos);

    return billboard_pos;
}

fn calc_billboard_matrix(
    instance_position: vec4<f32>,
    camera_world_pos: vec3<f32>,
    world_from_local: mat4x4<f32>
) -> mat3x3<f32> {
    let scale = vec3<f32>(
        length(world_from_local[0].xyz),
        length(world_from_local[1].xyz),
        length(world_from_local[2].xyz)
    );

    let to_camera_vector = camera_world_pos - instance_position.xyz;
    let billboard_z = normalize(vec3<f32>(to_camera_vector.x, 0.0, to_camera_vector.z));
    let billboard_y = vec3<f32>(0.0, 1.0, 0.0);
    let billboard_x = normalize(cross(billboard_y, billboard_z));

    return mat3x3<f32>(billboard_x * scale.x, billboard_y * scale.y, billboard_z * scale.z);
}

// TODO requires previous camera/view and normals so currently broken with temporal fx
fn calc_edge_correction(
    world_pos: vec3<f32>,
    world_normal: vec3<f32>,
    uv_x: f32,
    edge_correction_factor: f32
) -> vec3<f32> {
    let signed_edge_factor = uv_x * 2.0 - 1.0;

    let to_camera_dir = normalize(view.world_position.xyz - world_pos);

    let world_up = vec3<f32>(0.0, 1.0, 0.0);
    let view_side_dir = normalize(cross(to_camera_dir, world_up));

    // Normal orthogonal to camera/view vec
    let normal_dot_view = dot(world_normal, to_camera_dir);
    let grazing_angle_factor = pow(1.0 - abs(normal_dot_view), 2.0);

    // Fade out correction when looking top-down
    let top_down_factor = abs(dot(to_camera_dir, world_up));
    let top_down_fade = pow(1.0 - top_down_factor, 0.5);

    // TODO remove * 0.
    let strength = grazing_angle_factor * edge_correction_factor * top_down_fade;

    let correction_shift = view_side_dir * -signed_edge_factor;

    return correction_shift * strength;
}