# 4.3 - Texture Wrapping Modes

## What We're Learning

You've learned how to sample textures, and you've learned how to filter them to keep them sharp or smooth. Now we encounter a new problem: what happens when a mesh is bigger than its texture?

If you map a 1-meter texture onto a 10-meter wall, do you stretch it? Do you tile it like wallpaper? What happens at the edges?

These behaviors are controlled by **Texture Wrapping Modes** (also known as Address Modes). In this article, you'll master:

*   **The Address Mode Enum**: Understanding Repeat, MirrorRepeat, ClampToEdge, and ClampToBorder.
    
*   **Sampler Configuration**: How to change these settings in Bevy without reloading textures.
    
*   **UV Math**: How to manipulate coordinates in WGSL to create scrolling, rotating, and scaling effects.
    
*   **Seamless Tiling**: Techniques to make repeated textures look natural.
    

## Understanding UV Coordinates and Wrapping

To understand wrapping, we have to look at the math of the **Sampler**.

When your Fragment Shader calls `textureSample(texture, sampler, uv)`, the GPU looks at the UV coordinates.

*   **Standard UVs** are between `0.0` and `1.0`. `(0,0)` is usually the top-left (or bottom-left, depending on API), and `(1,1)` is the opposite corner.
    
*   **Out-of-Bounds UVs** are anything less than `0.0` or greater than `1.0`.
    

### The Sampler's Job

The texture image is a finite grid of colored pixels (texels). It has no concept of "infinity." If you ask for the pixel at `1.5`, the image says "I don't have that."

The **Sampler** is the translator. It takes your request for 1.5, applies a specific **Addressing Rule**, and translates it into a valid coordinate within the `[0, 1]` range before fetching the color.

If you don't define this rule, the behavior is undefined (though usually defaults to clamping). In Bevy, we define these rules explicitly to get the artistic control we need.

## The Four Primary Wrapping Modes

There are four standard modes available in WebGPU (and thus Bevy). Let's look at how they handle a UV coordinate of `1.5` (50% past the edge).

### 1\. Repeat Mode (`Repeat`)

This is the standard "tiling" mode. It ignores the integer part of the coordinate and keeps only the fractional part.

*   **Logic**: `Final UV = fract(Input UV)`
    
*   **Result**: `1.5` becomes `0.5`. `2.5` becomes `0.5`.
    
*   **Visual**: The image repeats infinitely.
    
*   **Use Case**: Floors, brick walls, grass, or any surface that needs to cover a large area with a small texture.
    

```plaintext
|   Texture   |   Texture   |   Texture   |
(0.0)-----(1.0)(0.0)-----(1.0)(0.0)-----(1.0)
```

### 2\. Mirror Repeat Mode (`MirrorRepeat`)

Similar to Repeat, but it flips the image on every other integer step.

*   **Logic**: Integers `0..1` are normal. `1..2` are flipped. `2..3` are normal.
    
*   **Result**: Creates a seamless "ping-pong" effect.
    
*   **Use Case**: Making non-seamless textures look continuous (e.g., generic noise, marble, or wood grain), though it can look like a kaleidoscope if the texture has distinct features.
    

```plaintext
|   Texture   |  erutxeT    |   Texture   |
(0.0)-----(1.0)(1.0)-----(0.0)(0.0)-----(1.0)
```

### 3\. Clamp to Edge (`ClampToEdge`)

The sampler simply refuses to go past `0.0` or `1.0`.

*   **Logic**: `Final UV = clamp(Input UV, 0.0, 1.0)`
    
*   **Result**: `1.5` becomes `1.0`.
    
*   **Visual**: The pixels at the very edge of the image are stretched infinitely in that direction.
    
*   **Use Case**:
    
    *   **UI Elements**: Prevents a button icon from tiling if the quad is slightly too big.
        
    *   **Skyboxes/Panoramas**: Ensures the edges don't bleed into the opposite side.
        
    *   **Texture Atlases**: Vital for preventing neighboring sprites from bleeding into the current one.
        

```plaintext
|   Texture   |
>>>>>>>>>>>|             |<<<<<<<<<<<<<
Stretched  (0.0)-----(1.0)  Stretched
```

### 4\. Clamp to Border (`ClampToBorder`)

If the UV is out of bounds, return a specific "border color" instead of sampling the texture.

*   **Logic**: `if (UV < 0 || UV > 1) return BorderColor;`
    
*   **Result**: The texture appears once, surrounded by the border color.
    
*   **Limit**: In Bevy (and WebGPU), the border color is not fully customizable in the high-level API due to hardware constraints. It typically defaults to **Transparent Black** (0,0,0,0).
    
*   **Use Case**: Decals that shouldn't repeat, or debugging to see exactly where your UVs are going out of bounds.
    

## Technical Deep Dive

### Configuring Samplers in Bevy

In Bevy, the wrapping mode is part of the `ImageSampler`. By default, Bevy imports textures with `ClampToEdge` (or sometimes `Repeat` depending on the file type/loader settings), but explicit configuration is always safer.

You configure this using the `ImageSamplerDescriptor` struct. You can specify the mode for each axis independently:

*   **U (Horizontal)**: `address_mode_u`
    
*   **V (Vertical)**: `address_mode_v`
    
*   **W (Depth)**: `address_mode_w` (Only used for 3D textures, like volume data)
    

Here is how you configure a sampler in a Bevy system:

```rust
use bevy::image::{ImageAddressMode, ImageSampler, ImageSamplerDescriptor};

fn configure_tiling_texture(
    mut images: ResMut<Assets<Image>>,
    my_texture_handle: Res<MyTextureHandle>,
) {
    if let Some(image) = images.get_mut(&my_texture_handle.0) {
        image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
            // Repeat horizontally
            address_mode_u: ImageAddressMode::Repeat,
            // Clamp vertically (great for side-scrolling backgrounds)
            address_mode_v: ImageAddressMode::ClampToEdge,
            // Standard filtering settings
            mag_filter: bevy::image::ImageFilterMode::Linear,
            min_filter: bevy::image::ImageFilterMode::Linear,
            ..default()
        });
    }
}
```

> **Note**: Modifying the sampler on an existing image asset triggers an update on the GPU automatically. You don't need to re-upload the texture data.

### UV Manipulation in WGSL

While the Sampler handles the *rules* for out-of-bounds UVs, you often need to push the UVs out of bounds intentionally to create effects. This is done inside the Fragment Shader.

#### 1\. Tiling (Scaling)

To repeat a texture, you multiply the UV coordinates.

*   **Math**: `uv * tiling_factor`
    
*   **Why**: If UV goes from `0` to `1`, multiplying by `4.0` makes it go from `0` to `4`. The Sampler then wraps this `4` times.
    

```rust
let tiling = vec2<f32>(4.0, 4.0); // Repeat 4x4 times
let tiled_uv = in.uv * tiling;
let color = textureSample(my_texture, my_sampler, tiled_uv);
```

#### 2\. Scrolling (Offset)

To animate a texture (like a conveyor belt or flowing water), you add values to the UVs over time.

```rust
// 'time' is a uniform passed from Rust
let scroll_speed = vec2<f32>(0.5, 0.0); 
let scrolled_uv = in.uv + (scroll_speed * time);
let color = textureSample(my_texture, my_sampler, scrolled_uv);
```

#### 3\. Rotation

Rotating UVs is slightly trickier because you need to rotate around a specific pivot point (usually the center, `0.5, 0.5`).

```rust
fn rotate_uv(uv: vec2<f32>, rotation: f32) -> vec2<f32> {
    let pivot = vec2<f32>(0.5, 0.5);
    let s = sin(rotation);
    let c = cos(rotation);
    
    // 1. Move UV so pivot is at (0,0)
    let uv_centered = uv - pivot;
    
    // 2. Rotate
    let rotated = vec2<f32>(
        uv_centered.x * c - uv_centered.y * s,
        uv_centered.x * s + uv_centered.y * c
    );
    
    // 3. Move back
    return rotated + pivot;
}
```

### Common Pitfalls

1.  **Non-Seamless Textures**: If you use `Repeat` on a photograph or a texture with distinct edges, you will see hard lines where the tiles meet. **MirrorRepeat** is a quick hack to hide this, but the best solution is to author textures that are specifically "tileable" (left edge matches right edge).
    
2.  **Texture Bleeding (Atlas)**: If you use a Texture Atlas (spritesheet), you usually want `ClampToEdge`. If you use `Repeat`, your character's head might start displaying their feet from the frame below!
    
3.  **UI Stretching**: A common bug in UI rendering is setting `ClampToEdge` on a 9-patch image but getting the UVs wrong, resulting in the edge pixels stretching all the way across the button.
    

## Advanced Techniques & Considerations

Before we jump into the demo, let's cover a few advanced topics that separate basic texture usage from professional shader work.

### 1\. The Art of Seamless Textures

Using Repeat mode reveals the truth about your texture: is it seamless?

A seamless texture is one where the left edge perfectly matches the right edge, and the top matches the bottom. If they don't match, you get visible "seams" - hard lines that ruin the illusion of a continuous surface.

If you don't have a seamless texture, you have two options:

1.  **The "Lazy" Fix**: Switch to MirrorRepeat. By flipping the texture, the edges always meet identical pixels. It eliminates hard lines but can create strange "Rorschach test" patterns.
    
2.  **The "Pro" Fix**: Edit the texture. In tools like Photoshop or GIMP, you "offset" the image by 50% horizontally and vertically so the edges move to the center. You then paint over the visible seams in the center. When you offset it back, the edges are now perfect.
    

### 2\. Polar Coordinates

One of the coolest uses of wrapping modes is converting Cartesian coordinates `(x, y)` to Polar coordinates `(angle, radius)`. This allows you to wrap a rectangular texture into a circle or a spiral.

```rust
fn cartesian_to_polar(uv: vec2<f32>) -> vec2<f32> {
    // 1. Center the UVs from [0,1] to [-0.5, 0.5]
    let centered = uv - 0.5;
    
    // 2. Calculate Angle and Radius
    let radius = length(centered) * 2.0; // Scale so edge is at 1.0
    let angle = atan2(centered.y, centered.x);
    
    // 3. Normalize Angle from [-PI, PI] to [0, 1]
    let normalized_angle = (angle / (2.0 * 3.14159)) + 0.5;
    
    return vec2<f32>(normalized_angle, radius);
}
```

If you use Repeat mode with this function:

*   The texture wraps around the center point endlessly.
    
*   The "seam" where 0 and 1 meet becomes invisible (if the texture is seamless horizontally).
    

### 3\. Hardware Reality: The "Border Color" Limit

You might wonder: "Can I set the border color to Hot Pink for debugging?"

In older graphics APIs (OpenGL), yes. In modern WebGPU (and thus Bevy's abstraction), **no**. Hardware support for arbitrary border colors is inconsistent across platforms (especially mobile).

In Bevy 0.16, ClampToBorder typically defaults to **Transparent Black** `(0, 0, 0, 0)`. If you need a specific colored border for a gameplay mechanic (e.g., a red warning zone), you usually have to implement that logic manually in the shader code, as we will do in our demo.

### 4\. Performance

Does `Repeat` cost more than `Clamp`?

*   **Generally, no.** Texture wrapping is handled by dedicated hardware units on the GPU. The cost difference is negligible.
    
*   **Cache Locality**: The only minor performance hit comes from "Dependent Texture Reads." If your UV math gets incredibly complex (randomly jumping from UV `0.1` to `900.5`), the GPU's texture cache becomes ineffective because it can't predict which pixels to load next. For standard tiling and scrolling, this is never an issue.
    

* * *

## Complete Example

We are going to build a **Wrapping Mode Playground**. This interactive demo lets you cycle through different wrapping modes in real-time while animating UVs (scrolling, rotating, and scaling). It perfectly illustrates how the sampler behaves when UVs go beyond the standard 0.0 to 1.0 range.

### Our Goal

1.  **Interactive Sampler**: Change the ImageAddressMode of a texture dynamically using keyboard input.
    
2.  **UV Animation**: Implement tiling, scrolling, and rotation in WGSL to force UVs out of bounds.
    
3.  **Visual Debugging**: Create a shader that can highlight exactly where UVs cross the texture boundaries.
    

### The Shader (`assets/shaders/d04_03_wrapping_demo.wgsl`)

This shader accepts a set of uniforms to transform the UV coordinates. It includes a helper function rotate\_uv to handle rotation around the center. It also includes a debug feature: when enabled, it mixes the texture color with a raw UV visualization so you can see the coordinate grid.

```rust
#import bevy_pbr::mesh_functions
#import bevy_pbr::view_transformations::position_world_to_clip

struct WrappingMaterial {
    tiling: vec2<f32>,
    offset: vec2<f32>,
    scroll_speed: vec2<f32>,
    time: f32,
    rotation: f32,
    blend_factor: f32,
    _padding: f32,
}

@group(2) @binding(0)
var texture: texture_2d<f32>;

@group(2) @binding(1)
var texture_sampler: sampler;

@group(2) @binding(2)
var<uniform> material: WrappingMaterial;

struct VertexInput {
    @builtin(instance_index) instance_index: u32,
    @location(0) position: vec3<f32>,
    @location(1) normal: vec3<f32>,
    @location(2) uv: vec2<f32>,
}

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) world_position: vec3<f32>,
    @location(1) world_normal: vec3<f32>,
    @location(2) uv: vec2<f32>,
}

@vertex
fn vertex(in: VertexInput) -> VertexOutput {
    var out: VertexOutput;

    let model = mesh_functions::get_world_from_local(in.instance_index);
    let world_position = mesh_functions::mesh_position_local_to_world(
        model,
        vec4<f32>(in.position, 1.0)
    );

    out.clip_position = position_world_to_clip(world_position.xyz);
    out.world_position = world_position.xyz;
    out.world_normal = mesh_functions::mesh_normal_local_to_world(
        in.normal,
        in.instance_index
    );
    out.uv = in.uv;

    return out;
}

// Rotate UV coordinates around center (0.5, 0.5)
fn rotate_uv(uv: vec2<f32>, angle: f32) -> vec2<f32> {
    let center = vec2<f32>(0.5, 0.5);
    let uv_centered = uv - center;

    let cos_a = cos(angle);
    let sin_a = sin(angle);

    let rotated = vec2<f32>(
        uv_centered.x * cos_a - uv_centered.y * sin_a,
        uv_centered.x * sin_a + uv_centered.y * cos_a
    );

    return rotated + center;
}

// Visualize UV coordinates as color (Red = U, Green = V)
fn uv_debug_color(uv: vec2<f32>) -> vec3<f32> {
    return vec3<f32>(fract(uv.x), fract(uv.y), 0.0);
}

// Check if UV is out of bounds
fn is_out_of_bounds(uv: vec2<f32>) -> bool {
    return uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0;
}

@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
    // 1. Start with base UV
    var uv = in.uv;

    // 2. Apply transformations
    uv = rotate_uv(uv, material.rotation);
    uv = uv + material.offset;
    uv = uv + material.scroll_speed * material.time;
    uv = uv * material.tiling;

    // 3. Sample texture
    // The wrapping mode (Repeat, Clamp, etc.) is handled here by the sampler
    var texture_color = textureSample(texture, texture_sampler, uv);

    // 4. Optional: Debug Visualization
    // Mix the texture with the UV grid based on blend_factor
    let debug_color = vec4<f32>(uv_debug_color(uv), 1.0);
    let color = mix(texture_color, debug_color, material.blend_factor);

    // Highlight out-of-bounds regions in Red when in Debug mode
    // This helps visualize where the border actually is.
    if (material.blend_factor > 0.5 && is_out_of_bounds(uv)) {
        return vec4<f32>(color.rgb + vec3<f32>(0.3, 0.0, 0.0), color.a);
    }

    return color;
}
```

### The Rust Material (`src/materials/d04_03_wrapping_demo.rs`)

This material struct holds the texture handle and the uniform data. We use the `AsBindGroup` derive macro to automatically generate the binding layout for Bevy. Note that we define a separate `WrappingMaterialUniforms` struct that implements `ShaderType` to ensure our data is strictly aligned for the GPU.

```rust
use bevy::prelude::*;
use bevy::render::render_resource::{AsBindGroup, ShaderRef};

mod uniforms {
    #![allow(dead_code)]

    use bevy::prelude::*;
    use bevy::render::render_resource::ShaderType;

    #[derive(ShaderType, Debug, Clone, Copy)]
    pub struct WrappingMaterial {
        pub tiling: Vec2,
        pub offset: Vec2,
        pub scroll_speed: Vec2,
        pub time: f32,
        pub rotation: f32,
        pub blend_factor: f32, // 0.0 = texture only, 1.0 = UV debug
        pub _padding: f32,
    }

    impl Default for WrappingMaterial {
        fn default() -> Self {
            Self {
                tiling: Vec2::ONE,
                offset: Vec2::ZERO,
                scroll_speed: Vec2::ZERO,
                time: 0.0,
                rotation: 0.0,
                blend_factor: 0.0,
                _padding: 0.0,
            }
        }
    }
}

pub use uniforms::WrappingMaterial as WrappingMaterialUniforms;

#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
pub struct WrappingMaterial {
    #[texture(0)]
    #[sampler(1)]
    pub texture: Handle<Image>,

    #[uniform(2)]
    pub uniforms: WrappingMaterialUniforms,
}

impl Material for WrappingMaterial {
    fn vertex_shader() -> ShaderRef {
        "shaders/d04_03_wrapping_demo.wgsl".into()
    }

    fn fragment_shader() -> ShaderRef {
        "shaders/d04_03_wrapping_demo.wgsl".into()
    }
}
```

Don't forget to add it to src/materials/[mod.rs](http://mod.rs):

```rust
pub mod d04_03_wrapping_demo;
```

### The Demo Module (`src/demos/d04_03_wrapping_demo.rs`)

This module handles the logic. It spawns a procedurally generated checkerboard texture (easier to see wrapping seams than a photo). The handle\_input system listens for key presses and modifies the Image asset's sampler field directly. This updates the GPU resource without needing to reload the texture.

```rust
use crate::materials::d04_03_wrapping_demo::{WrappingMaterial, WrappingMaterialUniforms};
use bevy::image::{ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor};
use bevy::prelude::*;
use bevy::render::render_asset::RenderAssetUsages;
use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat};
use std::f32::consts::PI;

#[derive(Resource)]
struct DemoState {
    current_mode: WrapMode,
    scrolling_enabled: bool,
    rotation_enabled: bool,
    debug_mode: bool,
}

#[derive(Debug, Clone, Copy, PartialEq)]
enum WrapMode {
    Repeat,
    ClampToEdge,
    MirrorRepeat,
    ClampToBorder,
}

impl WrapMode {
    fn next(&self) -> Self {
        match self {
            WrapMode::Repeat => WrapMode::ClampToEdge,
            WrapMode::ClampToEdge => WrapMode::MirrorRepeat,
            WrapMode::MirrorRepeat => WrapMode::ClampToBorder,
            WrapMode::ClampToBorder => WrapMode::Repeat,
        }
    }

    fn to_address_mode(&self) -> ImageAddressMode {
        match self {
            WrapMode::Repeat => ImageAddressMode::Repeat,
            WrapMode::ClampToEdge => ImageAddressMode::ClampToEdge,
            WrapMode::MirrorRepeat => ImageAddressMode::MirrorRepeat,
            WrapMode::ClampToBorder => ImageAddressMode::ClampToBorder,
        }
    }

    fn name(&self) -> &str {
        match self {
            WrapMode::Repeat => "Repeat",
            WrapMode::ClampToEdge => "Clamp to Edge",
            WrapMode::MirrorRepeat => "Mirror Repeat",
            WrapMode::ClampToBorder => "Clamp to Border",
        }
    }
}

pub fn run() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(MaterialPlugin::<WrappingMaterial>::default())
        .insert_resource(DemoState {
            current_mode: WrapMode::Repeat,
            scrolling_enabled: true,
            rotation_enabled: false,
            debug_mode: false,
        })
        .add_systems(Startup, setup)
        .add_systems(Update, (handle_input, update_materials, update_ui))
        .run();
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<WrappingMaterial>>,
    mut images: ResMut<Assets<Image>>,
) {
    let texture_handle = create_checkerboard_texture(&mut images);

    let material_handle = materials.add(WrappingMaterial {
        texture: texture_handle,
        uniforms: WrappingMaterialUniforms {
            tiling: Vec2::new(3.0, 3.0),
            scroll_speed: Vec2::new(0.2, 0.1),
            ..Default::default()
        },
    });

    commands.spawn((
        Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.0))),
        MeshMaterial3d(material_handle),
        Transform::from_rotation(Quat::from_rotation_x(PI / 2.0)),
    ));

    commands.spawn((
        DirectionalLight {
            illuminance: 10_000.0,
            shadows_enabled: false,
            ..default()
        },
        Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, -PI / 4.0, PI / 4.0, 0.0)),
    ));

    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0.0, 5.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));

    commands.spawn((
        Text::new(""),
        Node {
            position_type: PositionType::Absolute,
            top: Val::Px(10.0),
            left: Val::Px(10.0),
            padding: UiRect::all(Val::Px(10.0)),
            ..default()
        },
        TextColor(Color::WHITE),
        BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)),
    ));
}

fn create_checkerboard_texture(images: &mut ResMut<Assets<Image>>) -> Handle<Image> {
    let size = 64;
    let mut data = vec![0u8; (size * size * 4) as usize];

    for y in 0..size {
        for x in 0..size {
            let checker = ((x / 8) + (y / 8)) % 2;
            let base_idx = ((y * size + x) * 4) as usize;
            let val = if checker == 0 { 255 } else { 50 };
            data[base_idx] = val;
            data[base_idx + 1] = val;
            data[base_idx + 2] = val;
            data[base_idx + 3] = 255;
        }
    }

    let mut image = Image::new_fill(
        Extent3d {
            width: size,
            height: size,
            depth_or_array_layers: 1,
        },
        TextureDimension::D2,
        &data,
        TextureFormat::Rgba8UnormSrgb,
        RenderAssetUsages::default(),
    );

    image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
        address_mode_u: ImageAddressMode::Repeat,
        address_mode_v: ImageAddressMode::Repeat,
        mag_filter: ImageFilterMode::Nearest,
        min_filter: ImageFilterMode::Nearest,
        ..Default::default()
    });

    images.add(image)
}

fn handle_input(
    keyboard: Res<ButtonInput<KeyCode>>,
    mut state: ResMut<DemoState>,
    mut images: ResMut<Assets<Image>>,
    mut materials: ResMut<Assets<WrappingMaterial>>,
) {
    if keyboard.just_pressed(KeyCode::Space) {
        state.current_mode = state.current_mode.next();
        println!("Switching to mode: {:?}", state.current_mode);

        // LOGIC FIX:
        // When using Clamp, we want to center the texture on the quad to see the edges.
        // Math: UV range [0, 1].
        // Offset -0.25 -> [-0.25, 0.75].
        // Scale 2.0 -> [-0.5, 1.5].
        // Result: Texture (0-1) is centered, surrounded by out-of-bounds UVs.

        let (new_tiling, new_offset, enable_scroll) = match state.current_mode {
            WrapMode::ClampToEdge | WrapMode::ClampToBorder => {
                (Vec2::splat(2.0), Vec2::splat(-0.25), false)
            }
            _ => (Vec2::splat(3.0), Vec2::ZERO, true),
        };

        state.scrolling_enabled = enable_scroll;
        state.rotation_enabled = enable_scroll; // Also disable rotation for clarity

        // Apply settings to all materials
        for (_, mat) in materials.iter_mut() {
            mat.uniforms.tiling = new_tiling;
            mat.uniforms.offset = new_offset;
        }

        // Apply sampler settings
        for (_, material) in materials.iter() {
            if let Some(image) = images.get_mut(&material.texture) {
                let mode = state.current_mode.to_address_mode();
                image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
                    address_mode_u: mode,
                    address_mode_v: mode,
                    mag_filter: ImageFilterMode::Nearest,
                    min_filter: ImageFilterMode::Nearest,
                    ..Default::default()
                });
            }
        }
    }

    if keyboard.just_pressed(KeyCode::KeyS) {
        state.scrolling_enabled = !state.scrolling_enabled;
    }
    if keyboard.just_pressed(KeyCode::KeyR) {
        state.rotation_enabled = !state.rotation_enabled;
    }
    if keyboard.just_pressed(KeyCode::KeyD) {
        state.debug_mode = !state.debug_mode;
    }
}

fn update_materials(
    time: Res<Time>,
    state: Res<DemoState>,
    keyboard: Res<ButtonInput<KeyCode>>,
    mut materials: ResMut<Assets<WrappingMaterial>>,
) {
    for (_, material) in materials.iter_mut() {
        material.uniforms.time = time.elapsed_secs();

        material.uniforms.rotation = if state.rotation_enabled {
            time.elapsed_secs() * 0.3
        } else {
            0.0
        };

        material.uniforms.scroll_speed = if state.scrolling_enabled {
            Vec2::new(0.2, 0.1)
        } else {
            Vec2::ZERO
        };

        material.uniforms.blend_factor = if state.debug_mode { 0.5 } else { 0.0 };

        if keyboard.pressed(KeyCode::Equal) {
            material.uniforms.tiling *= 1.02;
        }
        if keyboard.pressed(KeyCode::Minus) {
            material.uniforms.tiling *= 0.98;
            material.uniforms.tiling = material.uniforms.tiling.max(Vec2::splat(0.1));
        }
    }
}

fn update_ui(state: Res<DemoState>, mut query: Query<&mut Text>) {
    for mut text in &mut query {
        **text = format!(
            "TEXTURE WRAPPING MODES\n\
             [Space] Mode: {}\n\
             [S] Scroll: {}\n\
             [R] Rotate: {}\n\
             [D] Debug UV: {}\n\
             [+/-] Zoom Tiling",
            state.current_mode.name(),
            if state.scrolling_enabled { "ON" } else { "OFF" },
            if state.rotation_enabled { "ON" } else { "OFF" },
            if state.debug_mode { "ON" } else { "OFF" },
        );
    }
}
```

Don't forget to add it to `src/demos/mod.rs`:

```rust
pub mod d04_03_wrapping_demo;
```

And register it in `src/main.rs`:

```rust
Demo {
    number: "4.3",
    title: "Texture Wrapping Modes",
    run: demos::d04_03_wrapping_demo::run,
},
```

### Running the Demo

This demo allows you to see exactly how each wrapping mode behaves when the UV coordinates exceed the 0-1 range.

#### Controls

<table style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Key</p></th><th colspan="1" rowspan="1"><p>Action</p></th></tr><tr><td colspan="1" rowspan="1"><p><strong>Space</strong></p></td><td colspan="1" rowspan="1"><p>Cycle Wrapping Mode (Repeat → ClampToEdge → MirrorRepeat → ClampToBorder)</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>S</strong></p></td><td colspan="1" rowspan="1"><p>Toggle Scrolling animation</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>R</strong></p></td><td colspan="1" rowspan="1"><p>Toggle Rotation animation</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>D</strong></p></td><td colspan="1" rowspan="1"><p>Toggle Debug Mode (Red overlay on out-of-bounds UVs)</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>+ / -</strong></p></td><td colspan="1" rowspan="1"><p>Zoom Tiling in and out</p></td></tr></tbody></table>

#### What You're Seeing

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1765830669944/fd97391c-9a11-4b6b-9eb0-efc828a05add.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1765830676749/506645d8-3c25-461c-84ce-fd6a56c81d2f.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1765830682650/0a349d16-1675-4038-9763-b7e1aa1cc113.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1765830688831/ac25290a-cb7c-49dc-8aae-ac35fb2421d9.png align="center")

1.  **Repeat**: The checkerboard pattern continues infinitely.
    
2.  **ClampToEdge**: The edge pixels (black or white squares) are stretched infinitely outwards.
    
3.  **MirrorRepeat**: The pattern reverses at every boundary, creating a seamless (though kaleidoscopic) look.
    
4.  **ClampToBorder**: The texture disappears outside the center, replaced by the border color (transparent black).
    
    *   Note: Press **D** in this mode to see the red debug overlay, which proves that the UVs are indeed out of bounds!
        

## Key Takeaways

1.  **Samplers Control Edges**: The texture image ends at 1.0. The Sampler decides what happens after that.
    
2.  **Explicit Configuration**: Always configure your ImageSamplerDescriptor explicitly for the effect you want (e.g., Clamp for UI, Repeat for floors).
    
3.  **UVs are Flexible**: You can multiply (tile), add (scroll), and rotate UVs in the shader to create dynamic effects without changing the mesh geometry.
    
4.  **Mirroring Hides Seams**: MirrorRepeat is a useful tool for procedurally texturing surfaces with non-seamless noise textures.
    

## What's Next?

We've mastered single textures - sampling, filtering, and wrapping. But most realistic materials aren't made of just one image. A brick wall has color, but it also has bumpiness (normal maps), shininess (roughness maps), and ambient shadows (AO maps).

In the next article, we will combine multiple textures into a single shader to create a complete **PBR Material**.

*Next up:* ***4.4 - Multi-Texture Materials***

* * *

## Quick Reference

### Wrapping Modes Cheat Sheet

<table style="min-width: 75px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Mode</p></th><th colspan="1" rowspan="1"><p>Behavior</p></th><th colspan="1" rowspan="1"><p>Best For</p></th></tr><tr><td colspan="1" rowspan="1"><p><strong>Repeat</strong></p></td><td colspan="1" rowspan="1"><p>fract(uv)</p></td><td colspan="1" rowspan="1"><p>Floors, Walls, Tiling Patterns</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>ClampToEdge</strong></p></td><td colspan="1" rowspan="1"><p>clamp(uv, 0.0, 1.0)</p></td><td colspan="1" rowspan="1"><p>UI, Skyboxes, Sprites</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>MirrorRepeat</strong></p></td><td colspan="1" rowspan="1"><p>Flips every integer</p></td><td colspan="1" rowspan="1"><p>Noise, Marble, Seamless-ing</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>ClampToBorder</strong></p></td><td colspan="1" rowspan="1"><p>Returns Border Color</p></td><td colspan="1" rowspan="1"><p>Decals, Debugging</p></td></tr></tbody></table>

### Bevy Configuration

```rust
// Standard Repeating Sampler
ImageSampler::Descriptor(ImageSamplerDescriptor {
    address_mode_u: ImageAddressMode::Repeat,
    address_mode_v: ImageAddressMode::Repeat,
    // ... filtering settings
    ..default()
})
```
