Search across packages, versions, files, declarations, and comments. Use multiple terms to narrow results (AND), and add @vertex, @fragment, or @compute to filter by shader stage.
1// The use of layered macro/micro noise, secondary motions (S-curve, bop)
2// and the methods for normal curving and edge correction are all heavily inspired by:
4// "Ghost of Tsushima" and Eric Wohllaib
5// of Sucker Punch Productions and the GDC 2021 talk:
6// "Advanced Graphics Summit: Procedural Grass in 'Ghost of Tsushima'".
8#define_import_path bevy_feronia::displace
9#import bevy_pbr::mesh_functions::{mesh_normal_local_to_world, mesh_tangent_local_to_world}
10#import bevy_pbr::mesh_view_bindings::view
11#import bevy_render::view::{position_world_to_view, position_view_to_world}
14#import bevy_feronia::types::{SampledNoise, DisplacedVertex, InstanceInfo}
15#import bevy_feronia::wind::{Wind, BindlessWindIndices}
16#import bevy_feronia::noise::sample_noise
18struct CurveResult {
19 local_pos: vec3<f32>,
20 slope: vec3<f32>,
21 twist: f32,
22 height_factor: f32,
23 local_wind_dir: vec3<f32>
26fn displace_vertex_and_calc_normal(
27 wind: Wind,
28 noise: SampledNoise,
29 vertex_pos: vec3<f32>,
30 instance: InstanceInfo,
31#ifdef STATIC_BEND
32 static_bend: vec2<f32>,
33#endif
34#ifdef VERTEX_NORMALS
35 normal: vec3<f32>,
36#endif
37#ifdef VERTEX_TANGENTS
38 tangent: vec4<f32>,
39#endif
40#ifdef VERTEX_UVS_A
41 uv: vec2<f32>
42#endif
43) -> DisplacedVertex {
44 var result: DisplacedVertex;
46 // Macro (wind bend, static bend)
47 let curve_data = calculate_macro_curve(
48 vertex_pos,
49 wind,
50 noise,
51 instance,
52 #ifdef STATIC_BEND
53 static_bend
54 #endif
55 );
57 // Micro, s-curve, bop
58 let final_local_pos = apply_micro_details(
59 curve_data.local_pos,
60 curve_data,
61 wind,
62 instance,
63 noise
64 );
66 result.world_position = instance.world_from_local * vec4<f32>(final_local_pos, 1.0);
68 #ifdef BILLBOARDING
69 result.world_position = vec4<f32>(
70 billboarding(
71 wind,
72 instance,
73 final_local_pos,
74 final_local_pos - vertex_pos
75 ),
76 1.0
77 );
78 #endif
80#ifdef FAST_NORMALS
81 // Uses the original, un-displaced world-space normals.
82 //
83 // Will have incorrect lighting as the normals will not match the displaced vertex positions.
84 //
85 // The mesh should ideally be modeled with its "growth" axis along Y-Up (`+Y`)
86 // and its "face" pointing along Z-Up (`+Z`).
87 //
88 // Should be used for performance reasons and/or on static or barely wind affected objects.
90 #ifdef VERTEX_NORMALS
91 result.world_normal = mesh_normal_local_to_world(normal, instance.instance_index);
92 #else
93 result.world_normal = normalize(instance.world_from_local[2].xyz);
94 #endif
96 #ifdef VERTEX_TANGENTS
97 result.world_tangent = mesh_tangent_local_to_world(instance.world_from_local, tangent, instance.instance_index);
98 #else
99 let world_tangent_xyz = normalize(instance.world_from_local[0].xyz);
100 result.world_tangent = vec4<f32>(world_tangent_xyz, 1.0);
101 #endif
103#else
104#ifdef ANALYTICAL_NORMALS
105 // Calculates normals using a mathematical approximation of the
106 // displacement.
107 //
108 // Should be faster than numerical sampling but less
109 // accurate, as it only accounts for static_bend, twist,
110 // and macro_wind, ignoring high-frequency displacements.
111 //
112 // The mesh should ideally be modeled with its "growth" axis along Y-Up (`+Y`)
113 // and its "face" pointing along Z-Up (`+Z`).
114 //
115 // Typically used for billboarded foliage or flat meshes like grass.
117 let local_spine_direction = normalize(vec3<f32>(curve_data.slope.x, 1.0, curve_data.slope.z));
119 let cos_twist = cos(curve_data.twist);
120 let sin_twist = sin(curve_data.twist);
122 let local_width_direction = vec3<f32>(cos_twist, 0.0, sin_twist);
123 let model_rotation_matrix = mat3x3<f32>(
124 instance.world_from_local[0].xyz,
125 instance.world_from_local[1].xyz,
126 instance.world_from_local[2].xyz
127 );
129 let world_spine = normalize(model_rotation_matrix * local_spine_direction);
130 let world_width = normalize(model_rotation_matrix * local_width_direction);
132 result.world_normal = normalize(cross(world_width, world_spine));
133 result.world_tangent = vec4<f32>(world_width, 1.0);
135#else
136 // Calculates normals numerically by sampling neighboring positions.
137 //
138 // Should be the most accurate, but most expensive path, as it runs the full
139 // displacement logic on the neighbors to find the surface direction.
140 //
141 // Typically used for complex foliage like non-billboarded bushes, trees.
143 #ifdef VERTEX_NORMALS
144 let local_normal = normal;
145 #else
146 let local_normal = vec3<f32>(0.0, 0.0, 1.0);
147 #endif
149 #ifdef VERTEX_TANGENTS
150 let local_tangent = tangent.xyz;
151 let tangent_sign = tangent.w;
152 #else
153 let local_tangent = vec3<f32>(1.0, 0.0, 0.0);
154 let tangent_sign = 1.0;
155 #endif
157 let local_bitangent = normalize(cross(local_normal, local_tangent) * tangent_sign);
159 // Too small, e.g. 0.01, causes flicker on simple geometry
160 let sample_offset = 0.05;
161 let base_displaced_pos = final_local_pos;
163 // Sample Neighbor along Tangent
164 let neighbor_tangent_origin = vertex_pos + local_tangent * sample_offset;
165 let noise_tangent = sample_noise(instance, neighbor_tangent_origin);
166 let curve_tangent = calculate_macro_curve(neighbor_tangent_origin, wind, noise_tangent, instance,
167 #ifdef STATIC_BEND
168 static_bend
169 #endif
170 );
171 let neighbor_tangent_displaced = apply_micro_details(curve_tangent.local_pos, curve_tangent, wind, instance, noise_tangent);
173 // Sample Neighbor along Bitangent
174 let neighbor_bitangent_origin = vertex_pos + local_bitangent * sample_offset;
175 let noise_bitangent = sample_noise(instance, neighbor_bitangent_origin);
176 let curve_bitangent = calculate_macro_curve(neighbor_bitangent_origin, wind, noise_bitangent, instance,
177 #ifdef STATIC_BEND
178 static_bend
179 #endif
180 );
181 let neighbor_bitangent_displaced = apply_micro_details(curve_bitangent.local_pos, curve_bitangent, wind, instance, noise_bitangent);
183 // Deltas
184 let delta_tangent = neighbor_tangent_displaced - base_displaced_pos;
185 let delta_bitangent = neighbor_bitangent_displaced - base_displaced_pos;
187 let computed_local_normal = normalize(cross(delta_tangent, delta_bitangent) * tangent_sign);
189 // Prevent back-facing normals if displacement is extreme
190 let dot_alignment = dot(computed_local_normal, local_normal);
191 let safe_local_normal = select(computed_local_normal, local_normal, dot_alignment < 0.1);
193 let model_rotation_matrix = mat3x3<f32>(
194 instance.world_from_local[0].xyz,
195 instance.world_from_local[1].xyz,
196 instance.world_from_local[2].xyz
197 );
199 result.world_normal = normalize(model_rotation_matrix * safe_local_normal);
201 let world_tangent_vec = normalize(model_rotation_matrix * delta_tangent);
203 result.world_tangent = vec4<f32>(
204 normalize(world_tangent_vec - dot(world_tangent_vec, result.world_normal) * result.world_normal),
205 tangent_sign
206 );
207#endif
208#endif
210#ifdef EDGE_CORRECTION
211#ifdef VERTEX_UVS_A
212 let edge_correction_offset = calculate_edge_correction(
213 result.world_position.xyz,
214 result.world_normal,
215 uv.x,
216 wind.edge_correction_factor
217 );
219 result.world_position += vec4<f32>(edge_correction_offset, 0.);
220#endif
221#endif
223 return result;
226fn calculate_macro_curve(
227 local_pos: vec3<f32>,
228 wind: Wind,
229 noise: SampledNoise,
230 instance: InstanceInfo,
231#ifdef STATIC_BEND
232 static_bend: vec2<f32>,
233#endif
234) -> CurveResult {
235 var result: CurveResult;
237 let height_range = max(wind.aabb_max.y - wind.aabb_min.y, 0.001);
239 // Normalized progress along the vertical axis (0.0 at bottom, 1.0 at top)
240 let height_progress = clamp((local_pos.y - wind.aabb_min.y) / height_range, 0.0, 1.0);
242 result.height_factor = height_progress;
244 result.twist = 0.0;
245 result.local_wind_dir = vec3<f32>(0.0);
246 var target_bend_vector = vec2<f32>(0.0);
248 #ifdef STATIC_BEND
249 target_bend_vector += static_bend;
250 #endif
252 #ifdef WIND_AFFECTED
253 // Get scale from the instance matrix to apply wind correctly in local space
254 let scale_x = length(instance.world_from_local[0].xyz);
255 let scale_y = length(instance.world_from_local[1].xyz);
256 let scale_z = length(instance.world_from_local[2].xyz);
258 let rotation_matrix = mat3x3<f32>(
259 instance.world_from_local[0].xyz / max(scale_x, 0.001),
260 instance.world_from_local[1].xyz / max(scale_y, 0.001),
261 instance.world_from_local[2].xyz / max(scale_z, 0.001)
262 );
264 let world_wind_direction = vec3<f32>(wind.direction.x, 0.0, wind.direction.y);
265 let local_wind_unscaled = transpose(rotation_matrix) * world_wind_direction;
266 result.local_wind_dir = normalize(local_wind_unscaled);
268 let macro_noise_value = clamp(noise.macro_noise, 0.001, 0.999) * 2.0 - 1.0;
270 let scale_compensation = vec2<f32>(1.0 / max(scale_x, 0.001), 1.0 / max(scale_z, 0.001));
272 let wind_force = vec2<f32>(local_wind_unscaled.x, local_wind_unscaled.z)
273 * wind.strength
274 * macro_noise_value
275 * scale_compensation;
277 target_bend_vector += wind_force;
279 #ifndef BILLBOARDING
280 result.twist = macro_noise_value * wind.twist_strength * height_progress;
281 #endif
282 #endif
284 let total_bend_amount = length(target_bend_vector);
286 // Limit bending to prevent the mesh from curling into itself
287 let max_allowed_bend = height_range * 0.95;
288 let safe_bend_amount = min(total_bend_amount, max_allowed_bend);
289 let bend_factor = clamp(total_bend_amount / height_range, 0.0, 1.0);
290 let bend_stiffness = 0.33;
292 // Estimate vertical height
293 var tip_height = sqrt(max(height_range * height_range - safe_bend_amount * safe_bend_amount, 0.0));
294 // The resulting Bezier curve arc is longer than the estimated straight-line distance.
295 // Compensate by shortening the grass to prevent it from appearing to "grow" or stretch as it bends outward.
296 let stretch_compensation = 1.0 - (bend_factor * 0.1);
297 tip_height *= stretch_compensation;
299 // Quadratic Bezier
300 // P0: Start
301 // P1: Control Point
302 // P2: End Point
303 let point_end = vec3<f32>(target_bend_vector.x, tip_height, target_bend_vector.y);
304 let point_start = vec3<f32>(0.0, 0.0, 0.0);
306 // Adjust control point height based on how much we are bending, i.e.,
307 // pushing the curve up and making the tip bend more than the base.
308 let control_point_y_factor = mix(0.5, 0.6, bend_factor);
309 let control_point_y = height_range * control_point_y_factor;
311 let point_control = vec3<f32>(
312 target_bend_vector.x * bend_stiffness,
313 control_point_y,
314 target_bend_vector.y * bend_stiffness
315 );
317 // B(t) = (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2
318 let inverse_progress = 1.0 - height_progress;
319 let bezier_position = (inverse_progress * inverse_progress) * point_start
320 + (2.0 * inverse_progress * height_progress) * point_control
321 + (height_progress * height_progress) * point_end;
323 // B'(t) = 2(1-t)(P1-P0) + 2t(P2-P1)
324 let bezier_tangent = 2.0 * inverse_progress * (point_control - point_start)
325 + 2.0 * height_progress * (point_end - point_control);
327 // Twist
328 let cos_twist = cos(result.twist);
329 let sin_twist = sin(result.twist);
330 let twisted_x = local_pos.x * cos_twist - local_pos.z * sin_twist;
331 let twisted_z = local_pos.x * sin_twist + local_pos.z * cos_twist;
333 // Calculate delta and apply to vertex.
334 let spine_delta = bezier_position - vec3<f32>(0.0, local_pos.y - wind.aabb_min.y, 0.0);
336 result.local_pos = vec3<f32>(
337 twisted_x + spine_delta.x,
338 local_pos.y + spine_delta.y,
339 twisted_z + spine_delta.z
340 );
342 let slope_xz = vec2<f32>(bezier_tangent.x, bezier_tangent.z) / max(bezier_tangent.y, 0.001);
344 result.slope = vec3<f32>(slope_xz.x, 1.0, slope_xz.y);
346 return result;
349fn apply_micro_details(
350 pos: vec3<f32>,
351 curve_data: CurveResult,
352 wind: Wind,
353 instance: InstanceInfo,
354 noise: SampledNoise
355) -> vec3<f32> {
356 var final_pos = pos;
358#ifdef WIND_AFFECTED
359#ifndef WIND_LOW_QUALITY
361 let forward_dir = curve_data.local_wind_dir;
363 let up_dir = vec3<f32>(0.0, 1.0, 0.0);
364 let right_dir = normalize(cross(forward_dir, up_dir));
366 // Micro
367 let micro_noise_val = (clamp(noise.micro_noise, 0.001, 0.999) * 2.0 - 1.0);
368 let micro_offset = forward_dir * (micro_noise_val * wind.micro_strength * 0.5 * curve_data.height_factor);
370 // S-Curve
371 let s_curve_seed = noise.phase_noise.x * 6.28;
372 let s_curve_lag = curve_data.height_factor * wind.s_curve_frequency;
373 let s_curve_input = instance.wrapped_time * wind.s_curve_speed + s_curve_seed + s_curve_lag;
375 let s_primary_oscillation = sin(s_curve_input);
376 let s_secondary_oscillation = cos(s_curve_input * 0.7) * 0.5;
378 let s_curve_offset = (forward_dir * s_primary_oscillation + right_dir * s_secondary_oscillation)
379 * wind.s_curve_strength
380 * curve_data.height_factor;
382 // Bop
383 let bop_seed = noise.phase_noise.y * 6.28;
384 let bop_input = instance.wrapped_time * wind.bop_speed + bop_seed + s_curve_lag;
385 let bop_val = sin(bop_input);
387 let bop_offset = vec3<f32>(0.0, bop_val * wind.bop_strength * curve_data.height_factor, 0.0);
389 final_pos += micro_offset + s_curve_offset + bop_offset;
390#endif
391#endif
393 return final_pos;
396fn billboarding(
397 wind: Wind,
398 instance: InstanceInfo,
399 local_displaced_pos: vec3<f32>,
400 total_offset: vec3<f32>
401) -> vec3<f32> {
402 let billboard_anchor_point = instance.instance_position;
404 let billboard_matrix = calculate_billboard_matrix(
405 billboard_anchor_point,
406 view.world_position.xyz,
407 instance.world_from_local
408 );
410 let billboard_base_pos = billboard_anchor_point.xyz + (billboard_matrix * local_displaced_pos);
412 return billboard_base_pos + vec3(0.0, total_offset.y, 0.0);
415fn calculate_billboard_matrix(
416 instance_position: vec4<f32>,
417 camera_world_pos: vec3<f32>,
418 world_from_local: mat4x4<f32>
419) -> mat3x3<f32> {
420 let scale = vec3<f32>(
421 length(world_from_local[0].xyz),
422 length(world_from_local[1].xyz),
423 length(world_from_local[2].xyz)
424 );
426 let to_camera_vector = camera_world_pos - instance_position.xyz;
427 let billboard_z = normalize(vec3<f32>(to_camera_vector.x, 0.0, to_camera_vector.z));
428 let billboard_y = vec3<f32>(0.0, 1.0, 0.0);
429 let billboard_x = normalize(cross(billboard_y, billboard_z));
431 return mat3x3<f32>(billboard_x * scale.x, billboard_y * scale.y, billboard_z * scale.z);
434fn calculate_edge_correction(
435 world_pos: vec3<f32>,
436 world_normal: vec3<f32>,
437 uv_x: f32,
438 edge_correction_factor: f32
439) -> vec3<f32> {
440 let signed_edge_factor = uv_x * 2.0 - 1.0;
442 let to_camera_dir = normalize(view.world_position.xyz - world_pos);
444 let world_up = vec3<f32>(0.0, 1.0, 0.0);
445 let view_side_dir = normalize(cross(to_camera_dir, world_up));
447 // Normal orthogonal to camera/view vec
448 let normal_dot_view = dot(world_normal, to_camera_dir);
449 let grazing_angle_factor = pow(1.0 - abs(normal_dot_view), 2.0);
451 // Fade out correction when looking top-down
452 let top_down_factor = abs(dot(to_camera_dir, world_up));
453 let top_down_fade = pow(1.0 - top_down_factor, 0.5);
455 let strength = grazing_angle_factor * edge_correction_factor * top_down_fade;
457 let correction_shift = view_side_dir * -signed_edge_factor;
459 return correction_shift * strength;
Contents