diff --git a/Cargo.toml b/Cargo.toml index 0b64a82ed3..e2bf5307db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2865,6 +2865,17 @@ description = "A custom shader that builds on the standard material" category = "Shaders" wasm = true +[[example]] +name = "stochastic_sampling" +path = "examples/shader/stochastic_sampling.rs" +doc-scrape-examples = true + +[package.metadata.example.stochastic_sampling] +name = "Stochastic Sampling" +description = "A custom shader that avoids repetition artifacts when tiling a texture" +category = "Shaders" +wasm = true + [[example]] name = "shader_prepass" path = "examples/shader/shader_prepass.rs" diff --git a/assets/shaders/stochastic_sampling.wgsl b/assets/shaders/stochastic_sampling.wgsl new file mode 100644 index 0000000000..188372abe0 --- /dev/null +++ b/assets/shaders/stochastic_sampling.wgsl @@ -0,0 +1,87 @@ +#import bevy_sprite::mesh2d_vertex_output::VertexOutput +#import bevy_sprite::mesh2d_view_bindings::globals +#import bevy_sprite::mesh2d_functions + +fn permute_3_(x: vec3) -> vec3 { + return (((x * 34.) + 1.) * x) % vec3(289.); +} + +// Noise implementation from https://github.com/johanhelsing/noisy_bevy/blob/v0.8.0/assets/noisy_bevy.wgsl +fn simplex_noise_2d(v: vec2) -> f32 { + let C = vec4( + 0.211324865405187, // (3.0 - sqrt(3.0)) / 6.0 + 0.366025403784439, // 0.5 * (sqrt(3.0) - 1.0) + -0.577350269189626, // -1.0 + 2.0 * C.x + 0.024390243902439 // 1.0 / 41.0 + ); + + // first corner + var i = floor(v + dot(v, C.yy)); + let x0 = v - i + dot(i, C.xx); + + // other corners + var i1 = select(vec2(0., 1.), vec2(1., 0.), x0.x > x0.y); + var x12 = x0.xyxy + C.xxzz - vec4(i1, 0., 0.); + + // permutations + i = i % vec2(289.); + + let p = permute_3_(permute_3_(i.y + vec3(0., i1.y, 1.)) + i.x + vec3(0., i1.x, 1.)); + var m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), vec3(0.)); + m *= m; + m *= m; + + // gradients: 41 points uniformly over a line, mapped onto a diamond + // the ring size, 17*17 = 289, is close to a multiple of 41 (41*7 = 287) + let x = 2. * fract(p * C.www) - 1.; + let h = abs(x) - 0.5; + let ox = floor(x + 0.5); + let a0 = x - ox; + + // normalize gradients implicitly by scaling m + // approximation of: m *= inversesqrt(a0 * a0 + h * h); + m = m * (1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h)); + + // compute final noise value at P + let g = vec3(a0.x * x0.x + h.x * x0.y, a0.yz * x12.xz + h.yz * x12.yw); + return 130. * dot(m, g); +} + +fn sum(v: vec4) -> f32 { + return v.x+v.y+v.z; +} + +// Stochastic sampling method from https://iquilezles.org/articles/texturerepetition/ +fn stochastic_sampling(uv: vec2, dx: vec2, dy: vec2, s: f32) -> vec4 { + + // sample variation pattern + let frequency_scale = 5.0; + let amplitude_scale = 0.3; + let k = simplex_noise_2d(uv.xy / frequency_scale) * amplitude_scale; + + // compute index from 0-7 + let index = k * 8.0; + let i = floor(index); + let f = fract(index); + + // offsets for the different virtual patterns from 0 to 7 + let offa = sin(vec2(3.0,7.0)*(i+0.0)); // can replace with any other hash + let offb = sin(vec2(3.0,7.0)*(i+1.0)); // can replace with any other hash + + // sample the two closest virtual patterns + let cola = textureSampleGrad(texture, texture_sampler, uv + s * offa, dx, dy); + let colb = textureSampleGrad(texture, texture_sampler, uv + s * offb, dx, dy); + + // interpolate between the two virtual patterns + return mix(cola, colb, smoothstep(0.2,0.8,f - 0.1*sum(cola-colb)) ); +} + +@group(2) @binding(1) var texture: texture_2d; +@group(2) @binding(2) var texture_sampler: sampler; + +@fragment +fn fragment(in: VertexOutput) -> @location(0) vec4 { + let speed = 0.5; + let s = smoothstep(0.4, 0.6, sin(globals.time * speed)); + return stochastic_sampling(in.uv, dpdx(in.uv), dpdy(in.uv), s); +} \ No newline at end of file diff --git a/assets/textures/rocks.png b/assets/textures/rocks.png new file mode 100644 index 0000000000..93107241d8 Binary files /dev/null and b/assets/textures/rocks.png differ diff --git a/examples/README.md b/examples/README.md index 29420e66c5..9bd2d8deec 100644 --- a/examples/README.md +++ b/examples/README.md @@ -476,6 +476,7 @@ Example | Description [Post Processing - Custom Render Pass](../examples/shader/custom_post_processing.rs) | A custom post processing effect, using a custom render pass that runs after the main pass [Shader Defs](../examples/shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader) [Specialized Mesh Pipeline](../examples/shader/specialized_mesh_pipeline.rs) | Demonstrates how to write a specialized mesh pipeline +[Stochastic Sampling](../examples/shader/stochastic_sampling.rs) | A custom shader that avoids repetition artifacts when tiling a texture [Storage Buffer](../examples/shader/storage_buffer.rs) | A shader that shows how to bind a storage buffer using a custom material. [Texture Binding Array (Bindless Textures)](../examples/shader/texture_binding_array.rs) | A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures). diff --git a/examples/shader/stochastic_sampling.rs b/examples/shader/stochastic_sampling.rs new file mode 100644 index 0000000000..0cecade5a0 --- /dev/null +++ b/examples/shader/stochastic_sampling.rs @@ -0,0 +1,75 @@ +//! Demonstrates using a custom extension to the `StandardMaterial` to create a repeating texture that avoids seams +//! by using stochastic sampling. This example uses a custom shader to achieve the effect. +use bevy::image::{ImageAddressMode, ImageSamplerDescriptor}; +use bevy::prelude::*; +use bevy::render::mesh::VertexAttributeValues; +use bevy::render::render_resource::{AsBindGroup, ShaderRef}; +use bevy::sprite::{Material2d, Material2dPlugin}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(ImagePlugin { + default_sampler: ImageSamplerDescriptor { + address_mode_u: ImageAddressMode::Repeat, + address_mode_v: ImageAddressMode::Repeat, + address_mode_w: ImageAddressMode::Repeat, + ..Default::default() + }, + })) + .add_plugins(Material2dPlugin::::default()) + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut commands: Commands, + asset_server: Res, + mut materials: ResMut>, + mut meshes: ResMut>, +) { + commands.spawn(Camera2d); + let texture = asset_server.load("textures/rocks.png"); + commands.spawn(( + Mesh2d(meshes.add(repeating_quad(10.0))), + MeshMaterial2d(materials.add(CustomMaterial { + texture: Some(texture), + })), + Transform::default(), + )); +} + +// This struct defines the data that will be passed to your shader +#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] +struct CustomMaterial { + #[texture(1)] + #[sampler(2)] + texture: Option>, +} + +/// This example uses a shader source file from the assets subdirectory +const SHADER_ASSET_PATH: &str = "shaders/stochastic_sampling.wgsl"; + +/// The Material trait is very configurable, but comes with sensible defaults for all methods. +/// You only need to implement functions for features that need non-default behavior. See the Material api docs for details! +impl Material2d for CustomMaterial { + fn fragment_shader() -> ShaderRef { + SHADER_ASSET_PATH.into() + } +} + +/// Creates a quad where the texture repeats n times in both directions. +fn repeating_quad(n: f32) -> Mesh { + let mut mesh: Mesh = Rectangle::from_length(1000.0).into(); + let uv_attribute = mesh.attribute_mut(Mesh::ATTRIBUTE_UV_0).unwrap(); + // The format of the UV coordinates should be Float32x2. + let VertexAttributeValues::Float32x2(uv_attribute) = uv_attribute else { + panic!("Unexpected vertex format, expected Float32x2."); + }; + // The default `Rectangle`'s texture coordinates are in the range of `0..=1`. Values outside + // this range cause the texture to repeat. + for uv_coord in uv_attribute.iter_mut() { + uv_coord[0] *= n; + uv_coord[1] *= n; + } + mesh +}