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.0and1.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.0or greater than1.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.5becomes0.5.2.5becomes0.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.
| 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..1are normal.1..2are flipped.2..3are 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.
| 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.5becomes1.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.
| 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_uV (Vertical):
address_mode_vW (Depth):
address_mode_w(Only used for 3D textures, like volume data)
Here is how you configure a sampler in a Bevy system:
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_factorWhy: If UV goes from
0to1, multiplying by4.0makes it go from0to4. The Sampler then wraps this4times.
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.
// '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).
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
Non-Seamless Textures: If you use
Repeaton 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).Texture Bleeding (Atlas): If you use a Texture Atlas (spritesheet), you usually want
ClampToEdge. If you useRepeat, your character's head might start displaying their feet from the frame below!UI Stretching: A common bug in UI rendering is setting
ClampToEdgeon 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:
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.
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.
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.1to900.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
Interactive Sampler: Change the ImageAddressMode of a texture dynamically using keyboard input.
UV Animation: Implement tiling, scrolling, and rotation in WGSL to force UVs out of bounds.
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.
#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.
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:
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.
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:
pub mod d04_03_wrapping_demo;
And register it in src/main.rs:
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
Key | Action |
|---|---|
Space | Cycle Wrapping Mode (Repeat → ClampToEdge → MirrorRepeat → ClampToBorder) |
S | Toggle Scrolling animation |
R | Toggle Rotation animation |
D | Toggle Debug Mode (Red overlay on out-of-bounds UVs) |
+ / - | Zoom Tiling in and out |
What You're Seeing
Repeat: The checkerboard pattern continues infinitely.
ClampToEdge: The edge pixels (black or white squares) are stretched infinitely outwards.
MirrorRepeat: The pattern reverses at every boundary, creating a seamless (though kaleidoscopic) look.
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
Samplers Control Edges: The texture image ends at 1.0. The Sampler decides what happens after that.
Explicit Configuration: Always configure your ImageSamplerDescriptor explicitly for the effect you want (e.g., Clamp for UI, Repeat for floors).
UVs are Flexible: You can multiply (tile), add (scroll), and rotate UVs in the shader to create dynamic effects without changing the mesh geometry.
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
Mode | Behavior | Best For |
|---|---|---|
Repeat | fract(uv) | Floors, Walls, Tiling Patterns |
ClampToEdge | clamp(uv, 0.0, 1.0) | UI, Skyboxes, Sprites |
MirrorRepeat | Flips every integer | Noise, Marble, Seamless-ing |
ClampToBorder | Returns Border Color | Decals, Debugging |
Bevy Configuration
// Standard Repeating Sampler
ImageSampler::Descriptor(ImageSamplerDescriptor {
address_mode_u: ImageAddressMode::Repeat,
address_mode_v: ImageAddressMode::Repeat,
// ... filtering settings
..default()
})





